fix(auth): guard-timer every auth/token fetch (incl. the refresh self-loop)#110
Merged
Conversation
…-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
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.
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.
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
fetchhas no default timeout, and none of the five GitHub/Copilot auth+discovery fetches set anAbortSignal. 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:getCopilotToken(mint + refresh loop)getDeviceCode,getGitHubUser,getCopilotUsagepollAccessToken(per attempt)continuePlus a self-expiry deadline on
pollAccessToken(from the device code's ownexpires_in) so thepollingstate always terminates into a terminal error even if the upstream never returnsexpired_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
timeoutevent 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+knipclean.