{"record":{"id":"74a15e9c7e42bf26","repo":"coleam00/Archon","slug":"failed-to-extract-the-chatgpt-account-id-from-the","errorCode":null,"errorMessage":"Failed to extract the ChatGPT account id from the OpenAI access token.","messagePattern":"Failed to extract the ChatGPT account id from the OpenAI access token\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/core/src/credentials/openai-oauth.ts","lineNumber":251,"sourceCode":"  }\n  const prevRefresh = typeof previous?.refresh === 'string' ? previous.refresh : '';\n  const refresh = typeof json.refresh_token === 'string' ? json.refresh_token : prevRefresh;\n  if (!refresh) {\n    throw new Error(`OpenAI token ${operation} response missing refresh_token.`);\n  }\n  const prevIdToken = typeof previous?.id_token === 'string' ? previous.id_token : '';\n  const idToken = typeof json.id_token === 'string' && json.id_token ? json.id_token : prevIdToken;\n  if (!idToken) {\n    // Fail loud: an id_token-less credential reproduces the exact #1924\n    // breakage (\"invalid ID token format\" in the Codex CLI) — never store one.\n    throw new Error(\n      `OpenAI token ${operation} response did not include an id_token (required by the Codex CLI).`\n    );\n  }\n  const prevAccountId = typeof previous?.accountId === 'string' ? previous.accountId : '';\n  const accountId = accountIdFromAccessToken(access) ?? prevAccountId;\n  if (!accountId) {\n    throw new Error('Failed to extract the ChatGPT account id from the OpenAI access token.');\n  }\n  return {\n    // Preserve any extra fields a future token response taught us to keep.\n    ...(previous ?? {}),\n    access,\n    refresh,\n    expires: Date.now() + expiresIn * 1000,\n    accountId,\n    id_token: idToken,\n  };\n}\n\n/**\n * Exchange a pasted authorization code for the full credential blob\n * (access/refresh/expiry, ChatGPT account id, and — unlike Pi — the\n * `id_token`). Throws with a descriptive message on any missing field.\n */\nexport async function exchangeOpenAiAuthorizationCode(","sourceCodeStart":233,"sourceCodeEnd":269,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/core/src/credentials/openai-oauth.ts#L233-L269","documentation":"After obtaining access/refresh/id tokens, credentialsFromTokenResponse decodes the ChatGPT account id from the access token (accountIdFromAccessToken, typically from the JWT's chatgpt_account_id claim) and falls back to previous.accountId. It throws when no account id can be derived from either source, since the account id is required for Codex/API calls.","triggerScenarios":"The access token's payload contains no recognizable account-id claim AND the previous credential has no accountId string.","commonSituations":"Token issued for an API-key style flow rather than the ChatGPT OAuth flow, so no chatgpt account claim exists; an unexpected token format after a provider change; a corrupted or truncated access token; workspace/org membership changes producing a token without the expected claim.","solutions":["Decode the access token (base64 JWT payload) and inspect its claims to see what account identifier is present.","Re-run the OAuth login to obtain a fresh ChatGPT-issued access token.","Pass the previous credential so its accountId is reused when only the access token was refreshed.","Confirm the token came from the ChatGPT OAuth flow (auth.openai.com), not a plain API-key exchange that has no account claim."],"exampleFix":"// before\nconst creds = credentialsFromTokenResponse(apiKeyExchangeJson, 'exchange');\n// after: use the ChatGPT OAuth flow token (contains account claim) or supply previous\nconst creds = credentialsFromTokenResponse(oauthJson, 'exchange', previousCreds);","handlingStrategy":"validation","validationCode":"function accountIdFromAccessToken(token) {\n  try {\n    const payload = JSON.parse(atob(token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')));\n    return payload['https://api.openai.com/auth']?.chatgpt_account_id ?? payload.chatgpt_account_id ?? null;\n  } catch { return null; }\n}\n// pre-check before calling the exchange\nif (!accountIdFromAccessToken(newAccessToken) && !previous?.accountId) throw new Error('no account id derivable');","typeGuard":"function hasAccountId(c: { accountId?: unknown; access?: string }): c is { accountId: string } {\n  if (typeof c.accountId === 'string' && c.accountId) return true;\n  return !!c.access && !!accountIdFromAccessToken(c.access);\n}","tryCatchPattern":"try {\n  creds = credentialsFromTokenResponse(json, op, previous);\n} catch (e) {\n  if (e.message.includes('ChatGPT account id')) {\n    console.error('access token lacks chatgpt account claim; decode it:', decodeJwtPayload(json.access_token));\n    throw new Error('token not from ChatGPT OAuth flow; re-run login');\n  }\n  throw e;\n}","preventionTips":["Obtain access tokens via the ChatGPT OAuth flow (auth.openai.com), not plain API-key exchanges — only those carry the account claim.","Keep previous.accountId across refreshes so fallback covers rotation.","Decode and inspect the JWT payload when integrating; verify the account claim before shipping config changes.","Watch for OpenAI claim-format changes when upgrading; log the payload (redacted) on failure."],"tags":["oauth","openai","jwt","account-id","auth"],"backgroundTag":"jwt-missing-claim","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}