Skip to content

fix(auth): guard-timer every auth/token fetch (incl. the refresh self-loop)#110

Merged
stuffbucket merged 1 commit into
mainfrom
fix/auth-fetch-guard-timeouts
Jun 11, 2026
Merged

fix(auth): guard-timer every auth/token fetch (incl. the refresh self-loop)#110
stuffbucket merged 1 commit into
mainfrom
fix/auth-fetch-guard-timeouts

Conversation

@stuffbucket

Copy link
Copy Markdown
Owner

Addresses your point: an in-progress auth operation that can't complete must eventually time out — the stuck sign-in should have.

Root cause

Production runs on Bun, whose fetch has no default timeout, and none of the five GitHub/Copilot auth+discovery fetches set an AbortSignal. A half-open connection (network drop, captive portal, stalled TLS) could hang the operation forever — most importantly the token-refresh self-loop (signed-in → signed-in, the {s0,s0} you named): a hung mint never returns, the loop never re-checks its abort signal, and it becomes a zombie.

Fix (additive guards — no behavior change)

New src/lib/http-timeouts.ts; AbortSignal.timeout(...) on all five fetches, each landing in the caller's existing failure branch:

fetch timeout on-timeout lands in
getCopilotToken (mint + refresh loop) 30s refresh loop's non-fatal retry / boot's degrade catch
getDeviceCode, getGitHubUser, getCopilotUsage 15s thrown → handler/degrade
pollAccessToken (per attempt) 15s the existing retry continue

Plus a self-expiry deadline on pollAccessToken (from the device code's own expires_in) so the polling state always terminates into a terminal error even if the upstream never returns expired_token. +1 test (self-expires with zero fetches).

Scope

These are the bug-class guards to land now — they bound currently-unbounded waits. The systematic version (a per-state dwell timer on every transitional variant, firing a timeout event into the reducer) belongs to the deferred Phase 3 transition-reducer, which the auth-controller header already anticipates. The shell-side stuck-UI was already bounded in #109.

Full suite green (817 pass); check:fast + knip clean.

…-loop)

Production runs on Bun, whose fetch has NO default timeout, and none of the
GitHub/Copilot auth+discovery fetches set an AbortSignal — so a half-open
connection (network drop, captive portal, stalled TLS) could hang the
operation forever: the token-refresh self-loop (signed-in to signed-in), cold
boot before the server binds, or a device-code poll.

Add AbortSignal.timeout to all five (new src/lib/http-timeouts.ts):
- getCopilotToken (mint + refresh loop) — 30s; on timeout it throws into the
  loop existing non-fatal retry / boot degrade catch.
- getDeviceCode / getGitHubUser / getCopilotUsage — 15s.
- pollAccessToken per-attempt — 15s; lands in the existing retry continue.

Also give pollAccessToken a self-expiry deadline (deviceCode.expires_in) so
the poll always terminates into a terminal error even if the upstream never
returns expired_token. +1 test (self-expiry, zero fetches).

Pure guards — bound currently-unbounded waits, no behavior change. The
systematic per-state dwell timer belongs to the Phase 3 transition reducer;
these are the bug-class fixes to land now. Full suite green (817).
@stuffbucket stuffbucket merged commit 2c74667 into main Jun 11, 2026
4 checks passed
@stuffbucket stuffbucket deleted the fix/auth-fetch-guard-timeouts branch June 11, 2026 02:33
stuffbucket added a commit that referenced this pull request Jun 11, 2026
…ocus (#113)

Completes the gh-reuse feature to a clean state. Two gaps:

1. Errors were handled safely but imprecisely: a stale/no-subscription gh
   account got written + rebooted into, then surfaced a generic 'came back
   unauthenticated' ~20s later. Now POST /gh/use PRE-FLIGHTS the token against
   Copilot (/copilot_internal/user) BEFORE writing/rebooting and returns a
   specific 422 — 'token expired or revoked' (401) vs 'no Copilot subscription'
   (403/404) vs a network message — which the UI shows immediately, no reboot.
   The shell now extracts {error:{message}} so the specific reason renders.
   Also closes the getModels timeout gap (it was missing from the #110 set;
   cacheModels is on the boot critical path).

2. gh discovery was only refreshed on entering the Account section — adding an
   account in gh while settings was already open didn't show up. Now refreshes
   on window focus / visibilitychange (the natural 'returned from the terminal'
   moment) + a manual Refresh button. No polling (gh auth status hits the
   keyring).

+6 pre-flight unit tests (status→message mapping, DI'd usage fn — no
mock.module). Full suite green (824); valid-account import still 200 (no
happy-path regression); shell tsc clean.
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