From c73c419d093aa732ae806742cf2b712acc5797f3 Mon Sep 17 00:00:00 2001 From: Hermes Hunter Date: Fri, 10 Jul 2026 11:39:41 +0700 Subject: [PATCH] fix(codex): avoid bare-email OAuth dedup (#2477) Only update an existing Codex OAuth row when both rows share the same chatgptAccountId, so a second Codex login no longer overwrites the first account's rotated token pair. Also fall back to workspaceId || chatgptAccountId || accountId for the chatgpt-account-id header. --- open-sse/executors/codex.js | 15 +++++++++++---- src/lib/db/repos/connectionsRepo.js | 13 ++++++++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/open-sse/executors/codex.js b/open-sse/executors/codex.js index 25109ef3..3b04445f 100644 --- a/open-sse/executors/codex.js +++ b/open-sse/executors/codex.js @@ -199,10 +199,17 @@ export class CodexExecutor extends BaseExecutor { headers["session_id"] = this._currentSessionId || credentials?.connectionId || "default"; // Identify client type to Codex backend (matches official codex CLI) if (!headers["originator"]) headers["originator"] = "codex_cli_rs"; - // Workspace binding header — improves account scope + cache affinity - const workspaceId = credentials?.providerSpecificData?.workspaceId || credentials?.providerSpecificData?.chatgptAccountId; - if (typeof workspaceId === "string" && workspaceId && !headers["ChatGPT-Account-ID"]) { - headers["ChatGPT-Account-ID"] = workspaceId; + // Account/workspace binding header — required when multiple Codex accounts + // are configured. OAuth import stores ChatGPT account ID as chatgptAccountId; + // older/custom rows may use workspaceId/accountId. Prefer explicit workspaceId + // but fall back to chatgptAccountId so requests don't cross-bind to the wrong + // OpenAI account and surface as token_invalid after adding another account. + const accountId = + credentials?.providerSpecificData?.workspaceId || + credentials?.providerSpecificData?.chatgptAccountId || + credentials?.providerSpecificData?.accountId; + if (typeof accountId === "string" && accountId && !headers["ChatGPT-Account-ID"]) { + headers["ChatGPT-Account-ID"] = accountId; } return headers; } diff --git a/src/lib/db/repos/connectionsRepo.js b/src/lib/db/repos/connectionsRepo.js index 00e6da59..4181843f 100644 --- a/src/lib/db/repos/connectionsRepo.js +++ b/src/lib/db/repos/connectionsRepo.js @@ -113,7 +113,18 @@ export async function createProviderConnection(data) { const incomingWs = data.providerSpecificData?.chatgptAccountId; existing = all.find(c => { if (c.authType !== "oauth" || c.email !== data.email) return false; - // Workspace providers (Codex) use workspace ID when both sides have it + + // Codex/OpenAI can issue multiple OAuth grants for the same email. + // Refresh tokens are rotated single-use; collapsing a new login onto an + // existing bare-email row overwrites the first account's token pair and + // makes it look "invalid" after adding a second account. Only update an + // existing Codex row when both rows expose the same ChatGPT account ID. + if (data.provider === "codex") { + const existingWs = c.providerSpecificData?.chatgptAccountId; + return !!incomingWs && !!existingWs && incomingWs === existingWs; + } + + // Workspace providers use workspace ID when both sides have it const existingWs = c.providerSpecificData?.chatgptAccountId; if (incomingWs && existingWs) return incomingWs === existingWs; if (incomingWs && !existingWs) return false;