Skip to content

fix: Route OAuth-connected Linear calls through linear-proxy - #597

Merged
Ziinc merged 4 commits into
claude/linear-kickoff-label-filterfrom
claude/linear-oauth-proxy
Sep 29, 2026
Merged

Ziinc merged 4 commits into
claude/linear-kickoff-label-filterfrom
claude/linear-oauth-proxy

Conversation

@Ziinc

@Ziinc Ziinc commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Stack 4/4 (based on the auto-kickoff label filter PR #596).

Problem

Settings offers "Connect via OAuth", but with no repo API key every Linear command and the auto-kickoff poller returned "OAuth proxy not ready". The linear-proxy Edge Function had three more problems:

  • It sent the OAuth token without the Bearer scheme Linear requires.
  • It ignored expires_at and refresh_token.
  • It replaced Linear's GraphQL error bodies with a generic error.

Change

Desktop

  • New linear_set_proxy_session(supabase_url, access_token) command.
    • The frontend pushes the session on sign-in and on every token refresh.
    • The push reuses the SSH relay token sync, now moved into src/lib/supabase-token-sync.ts. Relay behavior is unchanged.
  • LinearClientSource::Proxy sends GraphQL to {supabase}/functions/v1/linear-proxy with the user's JWT.
    • A repo API key still takes precedence.
    • With neither, the app shows a clear "not connected" error.
  • Proxy {"error": ...} bodies show as Linear: <message>. A 401/403 from the proxy tells the user to sign in to treq again.
    linear-proxy
  • The request logic now lives in lib.ts, which has no Deno or Supabase imports. index.ts only handles auth and connects lib.ts to Supabase. lib.ts is unit tested under vitest, the same way as _shared/merge-queue.
  • It sends Bearer <token>.
  • It refreshes a grant within 60s of expiry and stores the new token. If the refresh fails, it re-reads the row, because a concurrent request may already have refreshed it. Otherwise it returns 401 "Reconnect Linear".
  • It passes Linear's status and body through.

Testing (red → green)

  • Rust, against wiremock:
    • oauth_session_reaches_linear_through_the_proxy checks the path and the Bearer header.
    • proxy_explains_an_unlinked_linear_account checks the error message.
    • On the previous code these fail with "OAuth proxy not ready" and "Linear rejected the API key (403)". Both pass now.
  • test/linear-proxy/proxy.test.ts (8 tests):
    • Cases: Bearer header, GraphQL errors passed through, 401 → reconnect, refresh then store then use, no refresh while the token is valid, no refresh token, concurrent refresh.
    • Against the previous handler logic, 6 fail. The other 2 check behavior that is meant to stay the same (unlinked account → 403, no refresh while valid). All 8 pass now.
  • Also run: vitest for linear-proxy-auth, remote-relay-auth and useLinearAutoKickoff; deno check and deno fmt; tsc; eslint; oxlint; clippy -D warnings; cargo fmt --check.
    Not run: service-qa against the local Supabase stack, because its Docker images need more disk than the sandbox has. A real Linear OAuth round trip is also untested.
    🤖 Generated with Claude Code
    https://claude.ai/code/session_015cmSSvMnu4gc6NhMhKrt2v

Settings offered "Connect via OAuth", but every Linear command (and the
auto-kickoff poller) returned "OAuth proxy not ready" when a repo had no
API key, so OAuth-connected users had no working integration.

- The frontend pushes the Supabase URL and access token to Rust on sign-in
  and every refresh (shared with the SSH relay token sync). Repos without
  an API key now send GraphQL to the linear-proxy Edge Function with that
  session. An API key still takes precedence.
- linear-proxy sends the OAuth token with the Bearer scheme Linear
  requires, refreshes it when it is within a minute of expiry, and
  passes Linear's body through so GraphQL errors reach the app.
- Proxy errors ("Linear account not linked", expired grant) surface as
  readable messages instead of the API-key text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015cmSSvMnu4gc6NhMhKrt2v
…oauth-proxy

# Conflicts:
#	src-tauri/src/linear.rs
Rust: an OAuth session must reach `{supabase}/functions/v1/linear-proxy`
with the Bearer JWT, and a proxy `{"error": ...}` body must surface as
the message. Both fail on the previous code ("OAuth proxy not ready";
"rejected the API key").

linear-proxy: move request logic to lib.ts (no Deno or Supabase imports)
and test it under vitest like _shared/merge-queue. The tests cover the
Bearer header, GraphQL error pass-through, 401 reconnect, refresh near
expiry, missing refresh token, and a concurrent refresh. Six of the eight
fail against the previous handler; the other two pin unchanged behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015cmSSvMnu4gc6NhMhKrt2v
…oauth-proxy

# Conflicts:
#	src-tauri/src/lib.rs
@Ziinc
Ziinc merged commit 0ef18df into claude/linear-kickoff-label-filter Sep 29, 2026
21 checks passed
@Ziinc
Ziinc deleted the claude/linear-oauth-proxy branch September 29, 2026 13:38
Ziinc added a commit that referenced this pull request Sep 29, 2026
Co-authored-by: Claude <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.

2 participants