From c874051a8680b6a2902d9818bd294b5d447c0824 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Thu, 13 Aug 2026 10:12:56 +0900 Subject: [PATCH] fix(auth): prefer exact account identity on import --- src/oauth/store.ts | 15 ++++------ tests/account-import.test.ts | 55 ++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/oauth/store.ts b/src/oauth/store.ts index 478f9c73fd..b1f8206d08 100644 --- a/src/oauth/store.ts +++ b/src/oauth/store.ts @@ -541,15 +541,7 @@ export async function upsertCredentialByIdentity( } return await mutateStore(store => { const set = store[provider]; - const matches = (account: ProviderAccount): boolean => { - if (safe.accountId) { - if (account.credential.accountId) return account.credential.accountId === safe.accountId; - return Boolean( - safe.email - && account.credential.email - && account.credential.email.toLowerCase() === safe.email.toLowerCase(), - ); - } + const matchesEmailOnly = (account: ProviderAccount): boolean => { if (account.credential.accountId) return false; return Boolean( safe.email @@ -557,7 +549,10 @@ export async function upsertCredentialByIdentity( && account.credential.email.toLowerCase() === safe.email.toLowerCase(), ); }; - const existing = set?.accounts.find(matches); + const existing = safe.accountId + ? set?.accounts.find(account => account.credential.accountId === safe.accountId) + ?? set?.accounts.find(matchesEmailOnly) + : set?.accounts.find(matchesEmailOnly); if (existing && set) { existing.credential = safe; delete existing.needsReauth; diff --git a/tests/account-import.test.ts b/tests/account-import.test.ts index de3d4b65c4..5a8605a463 100644 --- a/tests/account-import.test.ts +++ b/tests/account-import.test.ts @@ -422,6 +422,61 @@ describe("Cockpit account-import atomic identity upsert", () => { expect(JSON.parse(readFileSync(join(testHome, "auth.json"), "utf8"))[ACCOUNT_IMPORT_PROVIDER].accounts).toHaveLength(1); }); + test("prefers an exact accountId row over an earlier email-only legacy row", async () => { + testHome = mkdtempSync(join(tmpdir(), "ocx-account-import-store-")); + process.env.OPENCODEX_HOME = testHome; + const authPath = join(testHome, "auth.json"); + writeFileSync(authPath, JSON.stringify({ + [ACCOUNT_IMPORT_PROVIDER]: { + activeAccountId: "legacy-email-row", + accounts: [ + { + id: "legacy-email-row", + credential: { + access: "access-legacy", + refresh: "refresh-legacy", + expires: 1, + email: "shared@example.com", + projectId: "project-legacy", + }, + addedAt: 1, + }, + { + id: "stable-subject-row", + credential: { + access: "access-stable", + refresh: "refresh-stable", + expires: 1, + email: "shared@example.com", + accountId: "google-subject-1", + projectId: "project-stable", + }, + addedAt: 2, + }, + ], + }, + }), { mode: 0o600 }); + + expect(await upsertCredentialByIdentity(ACCOUNT_IMPORT_PROVIDER, { + access: "access-updated", + refresh: "refresh-updated", + expires: 2, + email: "SHARED@example.com", + accountId: "google-subject-1", + projectId: "project-updated", + })).toBe("updated"); + + const set = getAccountSet(ACCOUNT_IMPORT_PROVIDER); + expect(set?.accounts).toHaveLength(2); + const legacyCredential = set?.accounts.find(account => account.id === "legacy-email-row")?.credential; + expect(legacyCredential?.access).toBe("access-legacy"); + expect(legacyCredential?.accountId).toBeUndefined(); + expect(set?.accounts.find(account => account.id === "stable-subject-row")?.credential) + .toMatchObject({ access: "access-updated", accountId: "google-subject-1" }); + expect(set?.accounts.filter(account => account.credential.accountId === "google-subject-1")) + .toHaveLength(1); + }); + test("does not overwrite a stable accountId row from an incoming email-only credential", async () => { testHome = mkdtempSync(join(tmpdir(), "ocx-account-import-store-")); process.env.OPENCODEX_HOME = testHome;