Skip to content

fix(linear): send personal API keys without Bearer prefix#1119

Merged
zbigniewsobiecki merged 2 commits intodevfrom
fix/linear-api-bearer-prefix
Apr 15, 2026
Merged

fix(linear): send personal API keys without Bearer prefix#1119
zbigniewsobiecki merged 2 commits intodevfrom
fix/linear-api-bearer-prefix

Conversation

@zbigniewsobiecki
Copy link
Copy Markdown
Member

Summary

After #1117 and #1118, Linear webhooks reach the router and trigger workers correctly, but every dispatch logs:

[PlatformClient] Failed to post Linear comment: Error: Linear API HTTP error 400

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 Bearer pattern with personal API keys (lin_api_*), which Linear rejects with HTTP 400.

File Header Correct?
src/linear/client.ts:65 (canonical) Authorization: apiKey
src/router/platformClients/linear.ts:23 Authorization: 'Bearer ' + apiKey
src/router/bot-identity-resolvers.ts:98 Authorization: 'Bearer ' + creds.apiKey

The bot-identity bug is more insidious: if (!response.ok) return null silently 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:8 claimed Bearer <api_key> — wrong, would have caused the next maintainer to reintroduce the bug.

Fix

  • Drop the Bearer prefix in both router-side call sites (one-line change each, with a comment explaining why).
  • Correct the misleading docblock.
  • Improve linearGraphQL error 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.
  • Update makeHttpErrorResponse test factory in tests/unit/pm/linear/client.test.ts to mock text() (required by the new error path).

Tests

New LinearPlatformClient describe block in tests/unit/router/platformClients.test.ts:

  • postComment / deleteComment / updateComment send bare API key, no Bearer prefix
  • postComment sends the correct GraphQL mutation + variables ({ issueId, body })
  • On HTTP failure, the warning includes the response body (so future diagnostics aren't lost)
  • Returns null without calling fetch when credentials are missing

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:

[PlatformClient] Linear comment posted for issue: <uuid>

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)

linearLabels is 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 to linearClient.addLabelupdateIssue({ labelIds: [...] }), but Linear's issueUpdate.labelIds requires UUIDs. So the cascade-processing label 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

  • Local: npm run lint, npm run typecheck, full npm test clean
  • New tests cover all three Linear platform-client methods + error-body inclusion
  • Post-deploy: trigger Linear transition; confirm Linear comment posted log + visible ack comment on the Linear issue

🤖 Generated with Claude Code

zbigniewsobiecki and others added 2 commits April 15, 2026 22:13
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
Copy link
Copy Markdown

codecov Bot commented Apr 15, 2026

Codecov Report

❌ Patch coverage is 83.33333% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/router/bot-identity-resolvers.ts 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@zbigniewsobiecki zbigniewsobiecki merged commit 77a6f1b into dev Apr 15, 2026
9 checks passed
@zbigniewsobiecki zbigniewsobiecki deleted the fix/linear-api-bearer-prefix branch April 15, 2026 22:20
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant