fix(linear): send personal API keys without Bearer prefix#1119
Merged
zbigniewsobiecki merged 2 commits intodevfrom Apr 15, 2026
Merged
fix(linear): send personal API keys without Bearer prefix#1119zbigniewsobiecki merged 2 commits intodevfrom
zbigniewsobiecki merged 2 commits intodevfrom
Conversation
Linear personal API keys (lin_api_*) are sent bare in the Authorization header — the `Bearer` prefix is OAuth-only, and using it with personal keys triggers HTTP 400. The router's two Linear API call sites (platformClients/linear.ts, bot-identity-resolvers.ts) had diverged from the canonical client (src/linear/client.ts) and used the OAuth pattern, breaking acknowledgment-comment posting and silently disabling the Linear bot-identity self-loop check. Also: - Fix the misleading docblock at src/linear/client.ts:8 that documented `Bearer <api_key>` while the code correctly used a bare key — future maintainers would have copied the doc and reintroduced the bug. - Improve linearGraphQL error messages in both the canonical and the router-side helpers to include the response body. Without it the failure surface was just an HTTP status, which made this very bug invisible until source-comparison. Test coverage: - Asserts each of postComment / deleteComment / updateComment sends bare API key, no Bearer prefix. - Asserts the GraphQL mutation body and variables. - Asserts the warning on HTTP failure includes the response body so diagnostics aren't lost again. Out of scope (separate follow-up): linearLabels are stored as free-text names but Linear's issueUpdate.labelIds requires UUIDs, so the `cascade-processing` label isn't being applied. Same shape as the status-mapping bug — needs a wizard UX change to fetch + present a label dropdown. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Required because linearGraphQL now reads response.text() to include the body in HTTP-error messages. The factory was incomplete (only mocked ok/status/json) and would have failed for any real Response.text() call. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
5 tasks
zbigniewsobiecki
added a commit
that referenced
this pull request
Apr 16, 2026
…arness (dormant)
Landed the dormant manifest infrastructure that spec 006 is built on:
- PMProviderManifest interface + sub-types (src/integrations/pm/manifest.ts)
One declarative contract per provider: id, label, category, credential
roles, webhook route + verifier, payload parser, router adapter,
extractProjectIdFromJob hook, PMIntegration, trigger handlers, platform
client factory, optional isSelfAuthoredHook + createLabel.
- pmProviderRegistry (src/integrations/pm/registry.ts)
Process-singleton registry; registerPMProvider enforces unique ids so
forgotten renames surface as runtime errors at startup.
- Shared helpers in src/integrations/pm/_shared/:
- auth-headers.ts — linearAuthHeader (bare key), githubAuthHeader
(Bearer + Accept + api-version), jiraAuthHeader (Basic). Guards
against the Bearer-prefix regression fixed in PR #1119.
- webhook-verifier.ts — makeHmacSha256Verifier factory; header-prefix
tolerant, opt-out semantics when secret is null.
- label-id-resolver.ts — UUID-validating label resolver, encapsulates
the check currently duplicated in src/pm/linear/adapter.ts.
- project-id-extractor.ts — extractProjectIdFromJobViaRegistry iterates
the manifest registry; wired into src/router/worker-env.ts as a
first-check fallback so the legacy per-provider branches still fire
for Trello/JIRA/Linear until plans 006/2-006/4 migrate them.
- Conformance harness (tests/unit/integrations/pm-conformance.test.ts)
Iterates listPMProviders() and asserts 11 contract invariants per
manifest. Exercised against TestProvider fixture (tests/helpers/
testPMProvider.ts) in this PR; real providers join in plans 006/2-4.
- pm.discovery tRPC router (src/api/routers/pm-discovery.ts)
Registry-driven listProviders + providerCredentialRoles endpoints.
Lives alongside the legacy integrationsDiscovery router during the
migration window. Mounted at pm.discovery.* in appRouter.
- Frontend provider-wizard registry + generic step renderer
(web/src/components/projects/pm-providers/)
Parallel frontend registry keyed by the same id as the backend
manifest. renderManifestStep helper wired into pm-wizard.tsx ahead of
the legacy per-provider branches; falls back when no wizard is
registered. In this PR, zero providers are registered, so the wizard
behavior is byte-for-byte identical to main.
- src/integrations/README.md — full rewrite as the manifest-first
author's guide with a transitional note that Trello/JIRA/Linear are
migrating in plans 006/2-4. Legacy section kept at the bottom.
- CLAUDE.md — integration abstraction pointer updated to reflect the
hybrid state (manifest for PM, legacy IntegrationModule for SCM +
alerting).
Tests: 42 new (7687 pre-existing + 42 = 7729 total, all green).
Build, lint, typecheck all pass.
No operator-visible changes. Trello/JIRA/Linear continue to register
through bootstrap.ts and builtins.ts; the new registry is consulted
first and returns empty, so legacy paths handle every real request.
Plan: docs/plans/006-pm-integration-plug-and-play/1-infrastructure.md.done
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4 tasks
zbigniewsobiecki
added a commit
that referenced
this pull request
Apr 18, 2026
Task 8: tests/unit/integrations/auth-header-provenance.test.ts greps the
src tree for `Bearer ${...}` / bare string-concat auth-header patterns
outside src/integrations/pm/_shared/auth-headers.ts. Post-#1119, the PM
provider code is clean; 4 non-PM files (Sentry, GitHub SCM, OpenRouter
LLM) are explicitly accept-listed with reasons — all out of spec 009
scope.
Task 9: Biome can't natively express the required string-pattern rule,
so lefthook.yml runs the provenance test at pre-commit (~250ms) —
failures surface at commit time, not just test time. Equivalent to a
custom Biome rule; matches plan 009/1 task 9's fallback guidance.
Also tightened the expanded conformance harness + fake-lifecycle test
to eliminate Biome complexity/non-null-assertion warnings.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
After #1117 and #1118, Linear webhooks reach the router and trigger workers correctly, but every dispatch logs:
The CASCADE acknowledgment comment never appears on the Linear issue.
Root cause
The router's two Linear API call sites diverged from the canonical client and used the OAuth
Bearerpattern with personal API keys (lin_api_*), which Linear rejects with HTTP 400.src/linear/client.ts:65(canonical)Authorization: apiKeysrc/router/platformClients/linear.ts:23Authorization: 'Bearer ' + apiKeysrc/router/bot-identity-resolvers.ts:98Authorization: 'Bearer ' + creds.apiKeyThe bot-identity bug is more insidious:
if (!response.ok) return nullsilently disabled Linear self-loop prevention. No incident yet because comment-mention triggering on Linear isn't fully wired here, but it's a latent loop hazard.A misleading docblock at
src/linear/client.ts:8claimedBearer <api_key>— wrong, would have caused the next maintainer to reintroduce the bug.Fix
Bearerprefix in both router-side call sites (one-line change each, with a comment explaining why).linearGraphQLerror messages in both the canonical and the router-side helpers to include the response body. Without that, this very bug was invisible from logs alone — observability, not rug-sweeping.makeHttpErrorResponsetest factory intests/unit/pm/linear/client.test.tsto mocktext()(required by the new error path).Tests
New
LinearPlatformClientdescribe block intests/unit/router/platformClients.test.ts:postComment/deleteComment/updateCommentsend bare API key, noBearerprefixpostCommentsends the correct GraphQL mutation + variables ({ issueId, body })All 7676 tests pass; lint and typecheck clean.
Verification post-merge
After deploy-dev, trigger any Linear status transition that fires an agent. Router log should show:
instead of
Failed to post Linear comment ... HTTP error 400. The Linear issue itself should display the CASCADE bot's ack comment.Out of scope (separate follow-up — flagged for the next PR)
linearLabelsis filled by free-text input in the wizard (pm-wizard-linear-steps.tsx:207-220) and stored as label names. The adapter (src/pm/linear/adapter.ts:154-165) passes those values tolinearClient.addLabel→updateIssue({ labelIds: [...] }), but Linear'sissueUpdate.labelIdsrequires UUIDs. So thecascade-processinglabel is silently never applied. Same shape as #1117 (status-mapping bug); needs a wizard UX change to fetch labels and present a dropdown of `{ label: name, value: id }`. Will be the next PR.Test plan
npm run lint,npm run typecheck, fullnpm testcleanLinear comment postedlog + visible ack comment on the Linear issue🤖 Generated with Claude Code