Skip to content

Commit be45b0b

Browse files
committed
fix(codex-auth): stop identity-based duplicate blocking
1 parent e8c0c49 commit be45b0b

3 files changed

Lines changed: 10 additions & 42 deletions

File tree

devlog/_plan/260624_codex-multi-auth-security-implementation/40_phase40-manual-import-disable.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ Required imports:
219219
Assertions:
220220

221221
- `checkAccountIdCollision("shared-team-account", "member-b@example.test")` returns no collision when an existing pool account has the same ChatGPT account id but a different email;
222-
- `checkAccountIdCollision("shared-team-account", "MEMBER-A@example.test")` returns collision for the same normalized email.
222+
- Correction 2026-06-27: `checkAccountIdCollision("shared-team-account", "MEMBER-A@example.test")` also returns no collision. ChatGPT account id plus email is not an authoritative duplicate key because one user can legitimately hold both personal and business subscriptions. Duplicate prevention remains scoped to local alias/credential lifecycle checks.
223223

224224
### MODIFY `src/codex-auth-api.ts` OAuth wording
225225

src/codex-auth-collision.ts

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { existsSync, readFileSync } from "node:fs";
22
import { join } from "node:path";
33
import os from "node:os";
4-
import { getCodexAccountCredential, listCodexAccountIds } from "./codex-account-store";
5-
import { loadConfig } from "./config";
6-
import { extractAccountId, extractEmail } from "./oauth/chatgpt";
4+
import { extractAccountId } from "./oauth/chatgpt";
75

86
export function readCodexTokens(): { access_token: string; account_id: string; id_token?: string } | null {
97
try {
@@ -28,39 +26,11 @@ export function getMainChatgptAccountId(): string | null {
2826
return extractAccountId(tokens.id_token, tokens.access_token) ?? (tokens.account_id || null);
2927
}
3028

31-
function getMainChatgptEmail(): string | null {
32-
const tokens = readCodexTokens();
33-
if (!tokens) return null;
34-
return extractEmail(tokens.id_token, tokens.access_token) ?? null;
35-
}
36-
37-
function normalizedEmail(email: string | undefined | null): string | null {
38-
const trimmed = email?.trim().toLowerCase();
39-
return trimmed || null;
40-
}
41-
42-
function poolEmailForId(id: string): string | null {
43-
const account = (loadConfig().codexAccounts ?? []).find(a => a.id === id);
44-
return normalizedEmail(account?.email);
45-
}
46-
47-
// Business/Team members can share chatgpt_account_id, so require email match too.
29+
// ChatGPT account ids and emails are not authoritative duplicate keys:
30+
// one user can legitimately hold both personal and business subscriptions.
4831
export function checkAccountIdCollision(
49-
chatgptAccountId: string,
50-
email?: string | null,
32+
_chatgptAccountId: string,
33+
_email?: string | null,
5134
): { collision: true; reason: string } | { collision: false } {
52-
const candidateEmail = normalizedEmail(email);
53-
const mainId = getMainChatgptAccountId();
54-
const mainEmail = getMainChatgptEmail();
55-
if (mainId && mainId === chatgptAccountId && (!candidateEmail || !mainEmail || mainEmail === candidateEmail)) {
56-
return { collision: true, reason: "This account is your main Codex login. Use a different account for the pool." };
57-
}
58-
for (const poolId of listCodexAccountIds()) {
59-
const cred = getCodexAccountCredential(poolId);
60-
const poolEmail = poolEmailForId(poolId);
61-
if (cred && cred.chatgptAccountId === chatgptAccountId && (!candidateEmail || !poolEmail || poolEmail === candidateEmail)) {
62-
return { collision: true, reason: `Account is already in the pool (${poolId}).` };
63-
}
64-
}
6535
return { collision: false };
6636
}

tests/codex-auth-collision.test.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,11 @@ describe("codex auth account collision", () => {
5454
});
5555
});
5656

57-
test("rejects the same team member added twice", async () => {
57+
test("allows the same email and account id because personal and business subscriptions can coexist", async () => {
5858
seedAccount("team-member-a", "member-a@example.test", "shared-team-account");
5959

60-
const result = checkAccountIdCollision("shared-team-account", "MEMBER-A@example.test");
61-
expect(result.collision).toBe(true);
62-
if (result.collision) {
63-
expect(result.reason).toContain("Account is already in the pool");
64-
}
60+
expect(checkAccountIdCollision("shared-team-account", "MEMBER-A@example.test")).toEqual({
61+
collision: false,
62+
});
6563
});
6664
});

0 commit comments

Comments
 (0)