{"record":{"id":"e783878b747c1c9b","repo":"coleam00/Archon","slug":"oauth-state-mismatch","errorCode":null,"errorMessage":"OAuth state mismatch.","messagePattern":"OAuth state mismatch\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/core/src/credentials/oauth-bridge.ts","lineNumber":219,"sourceCode":"\n/**\n * The Archon-owned ChatGPT/Codex manual login (#1924): build the authorize\n * URL (PKCE), surface it on the session, wait for the pasted redirect URL /\n * code via the same `codeDeferred` the Pi manual flows use (so poll(code) and\n * abort semantics are identical), then exchange it directly — capturing the\n * `id_token` Pi drops. Runs NO local callback server (the #1963 wedge\n * pattern); the user pastes the final redirect URL or code back instead.\n */\nasync function runOpenAiManualLogin(session: OAuthSession): Promise<OpenAiOAuthCredentials> {\n  const flow = createOpenAiAuthorizeFlow();\n  session.url = flow.url;\n  if (session.mode === 'pending') session.mode = 'manual';\n  session.firstSignal.resolve(true);\n  // Rejected by abortSession on cancel/supersede/expiry — same as Pi flows.\n  const input = await session.codeDeferred.promise;\n  const parsed = parseOpenAiAuthorizationInput(input);\n  if (parsed.state && parsed.state !== flow.state) {\n    throw new Error('OAuth state mismatch.');\n  }\n  if (!parsed.code) {\n    throw new Error('Missing authorization code.');\n  }\n  // Returns its true type — the loginPromise join is typed as delivery.ts's\n  // loose `OAuthCredentials`, which this satisfies structurally (no cast).\n  return exchangeOpenAiAuthorizationCode(parsed.code, flow.verifier, session.abort.signal);\n}\n\n/**\n * Begin a subscription login for a vendor (anthropic/openai/github-copilot;\n * legacy claude/codex/copilot ids accepted). Kicks off the held login —\n * Pi's `login()` for anthropic/github-copilot, the Archon-owned PKCE flow for\n * openai — and returns once the first signal has populated the URL (manual)\n * or user-code (device), or a short timeout elapses.\n */\nexport async function startOAuth(userId: string, providerId: string): Promise<StartOAuthResult> {\n  // Expired sessions may also hold a callback server — include them in the","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/core/src/credentials/oauth-bridge.ts#L201-L237","documentation":"Thrown by runOpenAiManualLogin when the user pastes an OpenAI OAuth authorization response whose `state` parameter is present but does not match the `state` issued for the in-flight PKCE flow. The state parameter is a CSRF protection binding the pasted callback to this login session; a mismatch means the code belongs to a different/older login attempt.","triggerScenarios":"During manual OpenAI subscription login, supplying a code via the deferred prompt (session.codeDeferred) where parseOpenAiAuthorizationInput extracts a state that differs from flow.state — pasting a callback URL from a previous login attempt, a different browser session, or a stale/stopped flow.","commonSituations":"Re-pasting an old authorization URL after the login was cancelled and restarted; running two logins concurrently and mixing up their callback URLs; copying the redirect from a different Archon instance.","solutions":["Complete the login again and paste the authorization response from that exact attempt (same state value).","Cancel any other concurrent OpenAI logins so only one flow is live, then retry.","Copy the full redirect URL from the browser tab the current login opened — not an older one.","If the state keeps mismatching, restart the login to mint a fresh flow and state."],"exampleFix":"// before: pasting stale URL from a cancelled attempt\nhttps://auth.openai.com/oauth/callback?state=OLD_STATE&code=...\n// after: fresh response from the current login attempt\nhttps://auth.openai.com/oauth/callback?state=CURRENT_STATE&code=...","handlingStrategy":"retry","validationCode":"import { parseOpenAiAuthorizationInput } from './openai-oauth';\nfunction inputMatchesFlow(input: string, flow: { state: string }): boolean {\n  const parsed = parseOpenAiAuthorizationInput(input);\n  return !parsed.state || parsed.state === flow.state;\n}","typeGuard":null,"tryCatchPattern":"try {\n  const creds = await loginPromise;\n} catch (e) {\n  if ((e as Error).message === 'OAuth state mismatch.') {\n    // restart the login flow and paste the response from the NEW attempt\n  } else throw e;\n}","preventionTips":["Always paste the redirect URL from the browser tab the current login opened.","Cancel/supersede stale login attempts before starting a new one.","Run one OpenAI login at a time per install to avoid mixing flows.","If unsure, restart the login — state is per-attempt and never reusable."],"tags":["oauth","csrf","openai","state-mismatch"],"backgroundTag":"oauth-state-mismatch","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}