From 684ec0b39012063a84f6b12aa1d53363899f0722 Mon Sep 17 00:00:00 2001 From: Gagan Date: Mon, 6 Jul 2026 16:12:53 +0530 Subject: [PATCH] fix(apps): device-flow diagnostics + bounded identity retries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified the wire calls are correct (both endpoints behave with this client id from a plain node run), so instrument the runtime path instead of guessing: - log every poll outcome ([GitHubAuth] lines) at the token exchange, the identity fetch, and the IPC handler - identity failures now retry at most 3 polls then fail loudly with the HTTP status — never silently degrade to 'pending' forever - explicit User-Agent on all GitHub calls (defensive) --- apps/x/apps/main/src/ipc.ts | 4 +++- apps/x/packages/core/src/apps/github-auth.ts | 24 ++++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index fb26ac67..f831930a 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -1697,7 +1697,9 @@ export function setupIpcHandlers() { return result; }, 'githubAuth:poll': async () => { - return githubAuth.pollDeviceFlow(); + const result = await githubAuth.pollDeviceFlow(); + console.log(`[GitHubAuth] poll result → ${result.status}`); + return result; }, 'githubAuth:status': async () => { return githubAuth.getAuthStatus(); diff --git a/apps/x/packages/core/src/apps/github-auth.ts b/apps/x/packages/core/src/apps/github-auth.ts index c13f8f17..1de109a9 100644 --- a/apps/x/packages/core/src/apps/github-auth.ts +++ b/apps/x/packages/core/src/apps/github-auth.ts @@ -37,7 +37,10 @@ type PendingFlow = { expiresAt: number; /** Token already issued but the identity fetch failed — retry that step. */ issuedToken?: string; + identityAttempts?: number; }; + +const GH_HEADERS = { 'Accept': 'application/json', 'User-Agent': 'rowboat-apps' }; let pending: PendingFlow | null = null; async function readAuth(): Promise { @@ -58,7 +61,7 @@ export async function startDeviceFlow(): Promise<{ userCode: string; verificatio // GitHub's OAuth endpoints take form-encoded params (JSON is not reliable). const res = await fetch('https://github.com/login/device/code', { method: 'POST', - headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, + headers: { ...GH_HEADERS, 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: GITHUB_OAUTH_CLIENT_ID, scope: 'public_repo' }).toString(), }); if (!res.ok) throw new Error(`device_code_failed: HTTP ${res.status}`); @@ -92,7 +95,7 @@ export async function pollDeviceFlow(): Promise { if (!accessToken) { const res = await fetch('https://github.com/login/oauth/access_token', { method: 'POST', - headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, + headers: { ...GH_HEADERS, 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: GITHUB_OAUTH_CLIENT_ID, device_code: pending.deviceCode, @@ -100,6 +103,7 @@ export async function pollDeviceFlow(): Promise { }).toString(), }); const body = await res.json() as { access_token?: string; error?: string; error_description?: string }; + console.log(`[GitHubAuth] poll: http=${res.status} error=${body.error ?? 'none'} token=${body.access_token ? 'ISSUED' : 'no'}`); if (body.error === 'authorization_pending') return { status: 'pending' }; if (body.error === 'slow_down') { @@ -127,11 +131,21 @@ export async function pollDeviceFlow(): Promise { pending.issuedToken = accessToken; } - // Identity: cache login alongside the token. + // Identity: cache login alongside the token. Bounded retries — a + // persistent failure must surface, never spin as "pending" forever. const userRes = await fetch('https://api.github.com/user', { - headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github+json' }, + headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github+json', 'User-Agent': 'rowboat-apps' }, }); - if (!userRes.ok) return { status: 'pending' }; // retry identity next poll + console.log(`[GitHubAuth] identity: http=${userRes.status}`); + if (!userRes.ok) { + pending.identityAttempts = (pending.identityAttempts ?? 0) + 1; + if (pending.identityAttempts >= 3) { + const detail = await userRes.text().catch(() => ''); + pending = null; + throw new Error(`identity_failed: GET /user → HTTP ${userRes.status} ${detail.slice(0, 160)}`); + } + return { status: 'pending' }; + } const user = await userRes.json() as { login: string }; const auth: StoredAuth = { login: user.login, createdAt: new Date().toISOString() };