fix(desktop): retry the update check anonymously when the env token is rejected

Wiring GITHUB_TOKEN/GH_TOKEN into fetchGitHubApi made a stale or revoked
token fail the check closed ('api.github.com answered HTTP 401.') on
machines where the anonymous request had always worked. An authenticated
401 now logs once that the env token was rejected and repeats the request
without the Authorization header; every other status is surfaced as before.
envTokenRejected is the pure gate so the decision is unit-tested.
This commit is contained in:
teknium1 authored and Teknium committed 2026-09-16 17:22:39 -07:00
1 parent c7d4d32800
commit e79ba2d7f2
3 files changed
+50 -3

No files matched your search

+11 -1
View File
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
import { test } from 'vitest'
import { githubApiHeaders, githubTokenFromEnv } from './github-api-auth'
import { envTokenRejected, githubApiHeaders, githubTokenFromEnv } from './github-api-auth'
// The exact headers the anonymous update check sends today; the change must
// leave this shape byte-identical when no token is configured.
@@ -32,3 +32,13 @@ test('anonymous sends no Authorization key at all; a token adds the `token` sche
// into every later request that builds from it.
assert.equal('Authorization' in UPDATE_CHECK_HEADERS, false)
})
test('only an authenticated 401 counts as the env token being rejected', () => {
// The anonymous-retry gate: a stale GITHUB_TOKEN must fall back to the
// anonymous request that worked before the token was wired in.
assert.equal(envTokenRejected({ statusCode: 401, authenticated: true }), true)
assert.equal(envTokenRejected({ statusCode: 401, authenticated: false }), false)
assert.equal(envTokenRejected({ statusCode: 403, authenticated: true }), false)
assert.equal(envTokenRejected({ statusCode: 429, authenticated: true }), false)
assert.equal(envTokenRejected(null), false)
})
+11
View File
@@ -43,3 +43,14 @@ export function githubApiHeaders(base: Record<string, string>, token?: string |
return headers
}
/**
* True when api.github.com rejected the env token itself (HTTP 401 on an
* authenticated call). A stale or revoked GITHUB_TOKEN must not fail the
* update check closed — the caller retries anonymously, which is exactly what
* worked before the token was wired in. Anonymous 401s and every other status
* are not the token's fault and are surfaced as-is.
*/
export function envTokenRejected(error: { statusCode?: number; authenticated?: boolean } | null | undefined): boolean {
return error?.statusCode === 401 && error?.authenticated === true
}
+28 -2
View File
@@ -217,7 +217,7 @@ import {
import { startGatewaysAfterUpdateAbort, stopGatewayBeforeUpdate } from './gateway-stop-before-update'
import { probeGatewayWebSocket } from './gateway-ws-probe'
import { registerGitIpc } from './git-ipc'
import { githubApiHeaders, githubTokenFromEnv } from './github-api-auth'
import { envTokenRejected, githubApiHeaders, githubTokenFromEnv } from './github-api-auth'
import { desktopBackendSpawnEnv, guestOnboardingEnabled, skipIntroEnabled } from './guest-onboarding'
import { readAndConsumeHandoffResult } from './handoff-result'
import {
@@ -3337,9 +3337,35 @@ async function checkUpdatesViaLsRemote({ updateRoot, branch, currentSha }) {
// GITHUB_TOKEN / GH_TOKEN from the environment, when present, moves the call
// from the anonymous 60/hour-per-IP budget to the token's 5,000/hour one; the
// header shape is otherwise unchanged. Read per request, never stored.
function fetchGitHubApi(url, accept = 'application/vnd.github+json') {
//
// A token GitHub rejects (401: expired, revoked, malformed) must not turn a
// check that worked anonymously into a hard failure, so the call is retried
// once without it; the rejection is logged once per process.
let warnedRejectedGitHubToken = false
async function fetchGitHubApi(url, accept = 'application/vnd.github+json') {
const token = githubTokenFromEnv(process.env)
try {
return await fetchGitHubApiOnce(url, accept, token)
} catch (error) {
if (!envTokenRejected(error)) {
throw error
}
if (!warnedRejectedGitHubToken) {
warnedRejectedGitHubToken = true
rememberLog(
'[updates] api.github.com rejected the GITHUB_TOKEN / GH_TOKEN from the environment (HTTP 401); ' +
'retrying the update check anonymously'
)
}
return fetchGitHubApiOnce(url, accept, null)
}
}
function fetchGitHubApiOnce(url, accept, token) {
return new Promise((resolve, reject) => {
const req = https.get(
url,