Skip to content

fix(copilot): renew GitHub auth instead of asking for repeated sign-ins - #102

Merged
FreddyJD merged 2 commits into
mainfrom
fix/copilot-session-refresh
Sep 13, 2026
Merged

FreddyJD merged 2 commits into
mainfrom
fix/copilot-session-refresh

Conversation

@FreddyJD

@FreddyJD FreddyJD commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Goal

Routine Copilot token expiry should renew silently, not send users through GitHub sign-in again.

Compared With Main

Main's #94 adds account-specific model discovery, tenant-assigned API routing, and fail-closed model policies. It does not persist or renew expiring GitHub OAuth credentials. This PR keeps that behavior and adds silent refresh for both credential layers.

Merged main at 50c19c1 and resolved all three conflicts. Version is now 0.0.97 in package.json and package-lock.json, following main's 0.0.96.

Changes

  • Persist access token, refresh token, and expiry metadata using the existing OS secret storage. Legacy bare-token credentials continue working.
  • Renew expiring GitHub OAuth credentials before the Copilot exchange and on exchange 401 when GitHub supplied a refresh token. Persist each rotated credential for the next launch.
  • Share concurrent renewals so rotating refresh tokens are spent once per in-flight credential renewal.
  • Refresh short-lived Copilot IDE tokens using refresh_in and server-relative expiry, and silently retry rejected inference/discovery requests.
  • Preserve the tenant-assigned endpoint for model discovery, Chat Completions, and Responses.
  • Keep a stable login id across OAuth rotations. A new regression test exposed that main's access-token-keyed model cache would mistake successful rotation for an account switch and empty the picker. Rotation now preserves that login identity; reconnects get a new one.
  • Record the rejected authorization on model discovery as well as inference, so a delayed 401 cannot discard a newer IDE token.
  • Retain disconnect/account-switch protections and fail-closed tenant policies. Do not leak OAuth response bodies or mislabel subscription/keychain failures as a need to reconnect.

Verification

  • npm run build: passed, including node/web typechecks
  • npm run smoke:app: 731 checks passed, plus the Copilot auth regression suite
  • npm run smoke:shared: 1094 checks passed
  • npm run smoke:store and smoke:i18n: passed
  • npm run i18n: all 652-key catalogs in sync
  • Changed-file Prettier check and git diff --check: passed

Auth tests use mocked HTTP and a throwaway database, not a real GitHub account. Coverage includes persistence/restart, legacy tokens, rotation, 20-way concurrency, clock skew, exchange-401 recovery, disconnect/account-switch races, malformed/secret-bearing errors, tenant endpoints, rotation during discovery, concurrent inference renewal, and delayed discovery 401s. Main's tenant-policy regression tests also pass.

Limits

Normal expiry no longer requires another login when a usable refresh token is available. Revoked authorization, expired refresh credentials, or an already-expired legacy token whose refresh data was never saved can still require a reconnect. This is not a promise that sign-in can never be required. The reporter's original account-specific error was unavailable, so that exact production failure was not independently reproduced.

FreddyJD and others added 2 commits September 13, 2026 13:51
Copilot auth has two credentials: the stored GitHub OAuth token and the
short-lived Copilot IDE token exchanged from it. Several bugs turned a
routine token expiry into a fresh sign-in:

- the device flow dropped refresh_token/expires_in, so an expiring GitHub
  login had no way to renew and could only be fixed by reconnecting
- concurrent turns each ran their own exchange; a rotating refresh token
  could be spent twice
- a late 401 for an old IDE token wiped the newer token another request
  had just installed, and the cache could outlive a disconnect or an
  account switch
- expires_at was trusted against the local clock, so a skewed clock kept
  sending expired tokens
- a 403 (no subscription) and a locked keychain both told the user to
  reconnect, which fixes neither

Store the full credential (access + refresh + expiries), renew it before
the exchange and on a 401 from it, share one renewal per credential, key
the cache to the credential so disconnects/switches invalidate it, honour
refresh_in and the server Date header, and keep OAuth error bodies out of
user-facing messages. Legacy bare-token credentials still work untouched.

Covered by test/copilot.ts in the app smoke run: persistence across
restart, rotation, concurrency, clock skew, disconnect/switch races,
delayed 401s, and failure classification, all against mocked HTTP.
Keep main's account-assigned API routing and tenant model policies alongside
silent OAuth renewal. Give each login a stable id across token rotations so
successful refreshes do not empty the model picker, and record discovery's
rejected authorization so late 401s cannot discard a newer IDE token.

Add regression coverage for discovery-time OAuth rotation, concurrent
inference renewal, tenant endpoints, and delayed discovery 401s. Adapt
main's auth fixtures and bump package/lockfile to 0.0.97.

Verified build, 731 app smoke checks plus Copilot auth regressions, 1094
shared checks, store/i18n tests, catalog sync, and changed-file formatting.

Co-authored-by: Roxy <299891354+roxy-commits@users.noreply.github.com>
@FreddyJD

Copy link
Copy Markdown
Collaborator Author

Conflicts resolved in a572d71. Both tenant-specific model discovery/routing from main and silent token renewal are preserved. Added regression coverage for OAuth rotation during discovery and delayed discovery 401s. All seven CI checks now pass, including builds and Electron tests on Windows, macOS, and Linux. Version: 0.0.97.

@FreddyJD
FreddyJD merged commit 9df8a8e into main Sep 13, 2026
7 checks passed
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