codex-multi-auth 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +21 -1
  2. package/README.md +16 -3
  3. package/assets/readme-hero.svg +31 -31
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +38 -65
  7. package/dist/index.js.map +1 -1
  8. package/dist/lib/accounts.d.ts.map +1 -1
  9. package/dist/lib/accounts.js +29 -10
  10. package/dist/lib/accounts.js.map +1 -1
  11. package/dist/lib/codex-cli/sync.d.ts +9 -16
  12. package/dist/lib/codex-cli/sync.d.ts.map +1 -1
  13. package/dist/lib/codex-cli/sync.js +51 -284
  14. package/dist/lib/codex-cli/sync.js.map +1 -1
  15. package/dist/lib/codex-manager/settings-hub.d.ts +44 -2
  16. package/dist/lib/codex-manager/settings-hub.d.ts.map +1 -1
  17. package/dist/lib/codex-manager/settings-hub.js +746 -117
  18. package/dist/lib/codex-manager/settings-hub.js.map +1 -1
  19. package/dist/lib/codex-manager.d.ts.map +1 -1
  20. package/dist/lib/codex-manager.js +240 -216
  21. package/dist/lib/codex-manager.js.map +1 -1
  22. package/dist/lib/context-overflow.js +10 -10
  23. package/dist/lib/entitlement-cache.d.ts +8 -5
  24. package/dist/lib/entitlement-cache.d.ts.map +1 -1
  25. package/dist/lib/entitlement-cache.js +24 -9
  26. package/dist/lib/entitlement-cache.js.map +1 -1
  27. package/dist/lib/named-backup-export.d.ts +69 -0
  28. package/dist/lib/named-backup-export.d.ts.map +1 -0
  29. package/dist/lib/named-backup-export.js +207 -0
  30. package/dist/lib/named-backup-export.js.map +1 -0
  31. package/dist/lib/oc-chatgpt-import-adapter.d.ts +64 -0
  32. package/dist/lib/oc-chatgpt-import-adapter.d.ts.map +1 -0
  33. package/dist/lib/oc-chatgpt-import-adapter.js +522 -0
  34. package/dist/lib/oc-chatgpt-import-adapter.js.map +1 -0
  35. package/dist/lib/oc-chatgpt-orchestrator.d.ts +80 -0
  36. package/dist/lib/oc-chatgpt-orchestrator.d.ts.map +1 -0
  37. package/dist/lib/oc-chatgpt-orchestrator.js +130 -0
  38. package/dist/lib/oc-chatgpt-orchestrator.js.map +1 -0
  39. package/dist/lib/oc-chatgpt-target-detection.d.ts +59 -0
  40. package/dist/lib/oc-chatgpt-target-detection.d.ts.map +1 -0
  41. package/dist/lib/oc-chatgpt-target-detection.js +327 -0
  42. package/dist/lib/oc-chatgpt-target-detection.js.map +1 -0
  43. package/dist/lib/prompts/codex.js +2 -2
  44. package/dist/lib/runtime-paths.d.ts.map +1 -1
  45. package/dist/lib/runtime-paths.js +21 -1
  46. package/dist/lib/runtime-paths.js.map +1 -1
  47. package/dist/lib/storage.d.ts +67 -16
  48. package/dist/lib/storage.d.ts.map +1 -1
  49. package/dist/lib/storage.js +797 -169
  50. package/dist/lib/storage.js.map +1 -1
  51. package/dist/lib/ui/copy.d.ts +15 -1
  52. package/dist/lib/ui/copy.d.ts.map +1 -1
  53. package/dist/lib/ui/copy.js +15 -1
  54. package/dist/lib/ui/copy.js.map +1 -1
  55. package/package.json +130 -131
  56. package/scripts/bench-format/hashline-v2.mjs +642 -642
  57. package/scripts/bench-format/render.mjs +496 -496
  58. package/scripts/bench-format/stats.mjs +54 -54
  59. package/scripts/bench-format/tasks.mjs +151 -151
  60. package/scripts/benchmark-render-dashboard.mjs +49 -49
  61. package/scripts/install-codex-auth.js +9 -9
  62. package/scripts/test-all-models.sh +7 -7
  63. package/scripts/test-model-matrix.js +138 -46
  64. package/scripts/validate-model-map.sh +7 -7
package/LICENSE CHANGED
@@ -1 +1,21 @@
1
-
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ndycode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -17,6 +17,7 @@ Codex CLI-first multi-account OAuth manager for the official `@openai/codex` CLI
17
17
  - Multi-account OAuth pool with health-aware selection and automatic failover
18
18
  - Project-scoped account storage under `~/.codex/multi-auth/projects/<project-key>/...`
19
19
  - Interactive dashboard for account actions and settings
20
+ - Experimental settings tab for staged sync, backup, and refresh-guard controls
20
21
  - Forecast, report, fix, and doctor commands for operational safety
21
22
  - Flagged account verification and restore flow
22
23
  - Session affinity and live account sync controls
@@ -199,6 +200,18 @@ codex auth forecast --live
199
200
 
200
201
  ---
201
202
 
203
+ ## Experimental Settings Highlights
204
+
205
+ The Settings menu now includes an `Experimental` section for staged features:
206
+
207
+ - preview-first sync into `oc-chatgpt-multi-auth`
208
+ - named local pool backup export with filename prompt
209
+ - refresh guard toggle and interval controls moved out of Backend Controls
210
+
211
+ These flows are intentionally non-destructive by default: sync previews before apply, destination-only accounts are preserved, and backup filename collisions fail safely.
212
+
213
+ ---
214
+
202
215
  ## Troubleshooting
203
216
 
204
217
  <details open>
@@ -265,9 +278,9 @@ codex auth doctor --json
265
278
 
266
279
  ## Release Notes
267
280
 
268
- - Current stable: [docs/releases/v0.1.5.md](docs/releases/v0.1.5.md)
269
- - Previous stable: [docs/releases/v0.1.4.md](docs/releases/v0.1.4.md)
270
- - Earlier stable: [docs/releases/v0.1.3.md](docs/releases/v0.1.3.md)
281
+ - Current stable: [docs/releases/v0.1.9.md](docs/releases/v0.1.9.md)
282
+ - Previous stable: [docs/releases/v0.1.8.md](docs/releases/v0.1.8.md)
283
+ - Earlier stable: [docs/releases/v0.1.7.md](docs/releases/v0.1.7.md)
271
284
  - Archived prerelease: [docs/releases/v0.1.0-beta.0.md](docs/releases/v0.1.0-beta.0.md)
272
285
 
273
286
  ## License
@@ -1,31 +1,31 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="300" viewBox="0 0 1200 300" role="img" aria-label="Codex Multi-Auth OAuth">
2
- <defs>
3
- <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
4
- <stop offset="0%" stop-color="#0f172a" />
5
- <stop offset="100%" stop-color="#1f2937" />
6
- </linearGradient>
7
- <linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
8
- <stop offset="0%" stop-color="#38bdf8" />
9
- <stop offset="100%" stop-color="#22d3ee" />
10
- </linearGradient>
11
- </defs>
12
- <rect width="1200" height="300" rx="28" fill="url(#bg)" />
13
- <g opacity="0.12">
14
- <circle cx="1020" cy="70" r="120" fill="#38bdf8" />
15
- <circle cx="150" cy="260" r="140" fill="#22d3ee" />
16
- </g>
17
- <g fill="none" stroke="url(#accent)" stroke-width="8" stroke-linecap="round" stroke-linejoin="round">
18
- <path d="M160 95 L100 150 L160 205" />
19
- <path d="M280 95 L340 150 L280 205" />
20
- <path d="M205 80 L235 220" />
21
- </g>
22
- <text x="420" y="120" fill="#e2e8f0" font-size="42" font-family="'IBM Plex Sans','Segoe UI',Arial,sans-serif" font-weight="600">
23
- Codex Multi-Auth OAuth
24
- </text>
25
- <text x="420" y="170" fill="#cbd5f5" font-size="24" font-family="'IBM Plex Sans','Segoe UI',Arial,sans-serif">
26
- ChatGPT Plus/Pro auth for GPT-5.2 and Codex models
27
- </text>
28
- <text x="420" y="220" fill="#7dd3fc" font-size="18" font-family="'IBM Plex Sans','Segoe UI',Arial,sans-serif">
29
- One-command install, modern variants, full tool support
30
- </text>
31
- </svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="300" viewBox="0 0 1200 300" role="img" aria-label="Codex Multi-Auth OAuth">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0%" stop-color="#0f172a" />
5
+ <stop offset="100%" stop-color="#1f2937" />
6
+ </linearGradient>
7
+ <linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
8
+ <stop offset="0%" stop-color="#38bdf8" />
9
+ <stop offset="100%" stop-color="#22d3ee" />
10
+ </linearGradient>
11
+ </defs>
12
+ <rect width="1200" height="300" rx="28" fill="url(#bg)" />
13
+ <g opacity="0.12">
14
+ <circle cx="1020" cy="70" r="120" fill="#38bdf8" />
15
+ <circle cx="150" cy="260" r="140" fill="#22d3ee" />
16
+ </g>
17
+ <g fill="none" stroke="url(#accent)" stroke-width="8" stroke-linecap="round" stroke-linejoin="round">
18
+ <path d="M160 95 L100 150 L160 205" />
19
+ <path d="M280 95 L340 150 L280 205" />
20
+ <path d="M205 80 L235 220" />
21
+ </g>
22
+ <text x="420" y="120" fill="#e2e8f0" font-size="42" font-family="'IBM Plex Sans','Segoe UI',Arial,sans-serif" font-weight="600">
23
+ Codex Multi-Auth OAuth
24
+ </text>
25
+ <text x="420" y="170" fill="#cbd5f5" font-size="24" font-family="'IBM Plex Sans','Segoe UI',Arial,sans-serif">
26
+ ChatGPT Plus/Pro auth for GPT-5.2 and Codex models
27
+ </text>
28
+ <text x="420" y="220" fill="#7dd3fc" font-size="18" font-family="'IBM Plex Sans','Segoe UI',Arial,sans-serif">
29
+ One-command install, modern variants, full tool support
30
+ </text>
31
+ </svg>
package/dist/index.d.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  * For production applications, use the OpenAI Platform API: https://platform.openai.com/
19
19
  *
20
20
  * @license MIT with Usage Disclaimer (see LICENSE file)
21
- * @author numman-ali
21
+ * @author ndycode
22
22
  * @repository https://github.com/ndycode/codex-multi-auth
23
23
 
24
24
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,kBAAkB,CAAC;AAmL5D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAg2H/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,KAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,kBAAkB,CAAC;AAmL5D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MA60H/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,KAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@
18
18
  * For production applications, use the OpenAI Platform API: https://platform.openai.com/
19
19
  *
20
20
  * @license MIT with Usage Disclaimer (see LICENSE file)
21
- * @author numman-ali
21
+ * @author ndycode
22
22
  * @repository https://github.com/ndycode/codex-multi-auth
23
23
 
24
24
  */
@@ -34,7 +34,7 @@ import { initLogger, logRequest, logDebug, logInfo, logWarn, logError, setCorrel
34
34
  import { checkAndNotify } from "./lib/auto-update-checker.js";
35
35
  import { handleContextOverflow } from "./lib/context-overflow.js";
36
36
  import { AccountManager, getAccountIdCandidates, extractAccountEmail, extractAccountId, formatAccountLabel, formatCooldown, formatWaitTime, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, resolveRequestAccountId, parseRateLimitReason, lookupCodexCliTokensByEmail, isCodexCliSyncEnabled, } from "./lib/accounts.js";
37
- import { getStoragePath, loadAccounts, saveAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, exportAccounts, importAccounts, loadFlaggedAccounts, saveFlaggedAccounts, clearFlaggedAccounts, normalizeEmailKey, StorageError, formatStorageErrorHint, setStorageBackupEnabled, } from "./lib/storage.js";
37
+ import { getStoragePath, loadAccounts, saveAccounts, withAccountStorageTransaction, clearAccounts, setStoragePath, exportAccounts, importAccounts, loadFlaggedAccounts, saveFlaggedAccounts, clearFlaggedAccounts, findMatchingAccountIndex, StorageError, formatStorageErrorHint, setStorageBackupEnabled, } from "./lib/storage.js";
38
38
  import { createCodexHeaders, extractRequestUrl, handleErrorResponse, handleSuccessResponse, getUnsupportedCodexModelInfo, resolveUnsupportedCodexFallbackModel, refreshAndUpdateToken, rewriteUrlForCodex, shouldRefreshToken, transformRequestForCodex, } from "./lib/request/fetch-helpers.js";
39
39
  import { applyFastSessionDefaults } from "./lib/request/request-transformer.js";
40
40
  import { getRateLimitBackoff, RATE_LIMIT_SHORT_RETRY_THRESHOLD_MS, resetRateLimitBackoff, } from "./lib/request/rate-limit-backoff.js";
@@ -315,24 +315,6 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
315
315
  const now = Date.now();
316
316
  const stored = replaceAll ? null : loadedStorage;
317
317
  const accounts = stored?.accounts ? [...stored.accounts] : [];
318
- const indexByRefreshToken = new Map();
319
- const indexByAccountId = new Map();
320
- const indexByEmail = new Map();
321
- for (let i = 0; i < accounts.length; i += 1) {
322
- const account = accounts[i];
323
- if (!account)
324
- continue;
325
- if (account.refreshToken) {
326
- indexByRefreshToken.set(account.refreshToken, i);
327
- }
328
- if (account.accountId) {
329
- indexByAccountId.set(account.accountId, i);
330
- }
331
- const emailKey = normalizeEmailKey(account.email);
332
- if (emailKey) {
333
- indexByEmail.set(emailKey, i);
334
- }
335
- }
336
318
  for (const result of results) {
337
319
  const accountId = result.accountIdOverride ?? extractAccountId(result.access);
338
320
  const accountIdSource = accountId
@@ -341,16 +323,14 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
341
323
  : undefined;
342
324
  const accountLabel = result.accountLabel;
343
325
  const accountEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
344
- const existingByEmail = accountEmail && indexByEmail.has(accountEmail)
345
- ? indexByEmail.get(accountEmail)
346
- : undefined;
347
- const existingById = accountId && indexByAccountId.has(accountId)
348
- ? indexByAccountId.get(accountId)
349
- : undefined;
350
- const existingByToken = indexByRefreshToken.get(result.refresh);
351
- const existingIndex = existingById ?? existingByEmail ?? existingByToken;
326
+ const existingIndex = findMatchingAccountIndex(accounts, {
327
+ accountId,
328
+ email: accountEmail,
329
+ refreshToken: result.refresh,
330
+ }, {
331
+ allowUniqueAccountIdFallbackWithoutEmail: true,
332
+ });
352
333
  if (existingIndex === undefined) {
353
- const newIndex = accounts.length;
354
334
  accounts.push({
355
335
  accountId,
356
336
  accountIdSource,
@@ -362,20 +342,11 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
362
342
  addedAt: now,
363
343
  lastUsed: now,
364
344
  });
365
- indexByRefreshToken.set(result.refresh, newIndex);
366
- if (accountId) {
367
- indexByAccountId.set(accountId, newIndex);
368
- }
369
- if (accountEmail) {
370
- indexByEmail.set(accountEmail, newIndex);
371
- }
372
345
  continue;
373
346
  }
374
347
  const existing = accounts[existingIndex];
375
348
  if (!existing)
376
349
  continue;
377
- const oldToken = existing.refreshToken;
378
- const oldEmail = existing.email;
379
350
  const nextEmail = accountEmail ?? sanitizeEmail(existing.email);
380
351
  const nextAccountId = accountId ?? existing.accountId;
381
352
  const nextAccountIdSource = accountId ? accountIdSource ?? existing.accountIdSource : existing.accountIdSource;
@@ -391,21 +362,6 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
391
362
  expiresAt: result.expires,
392
363
  lastUsed: now,
393
364
  };
394
- if (oldToken !== result.refresh) {
395
- indexByRefreshToken.delete(oldToken);
396
- indexByRefreshToken.set(result.refresh, existingIndex);
397
- }
398
- if (accountId) {
399
- indexByAccountId.set(accountId, existingIndex);
400
- }
401
- const oldEmailKey = normalizeEmailKey(oldEmail);
402
- const nextEmailKey = normalizeEmailKey(nextEmail);
403
- if (oldEmailKey && oldEmailKey !== nextEmailKey) {
404
- indexByEmail.delete(oldEmailKey);
405
- }
406
- if (nextEmailKey) {
407
- indexByEmail.set(nextEmailKey, existingIndex);
408
- }
409
365
  }
410
366
  if (accounts.length === 0)
411
367
  return;
@@ -1023,6 +979,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1023
979
  const accountCount = accountManager.getAccountCount();
1024
980
  const attempted = new Set();
1025
981
  let restartAccountTraversalWithFallback = false;
982
+ let retryNextAccountBeforeFallback = false;
1026
983
  let usedPreferredSessionAccount = false;
1027
984
  const capabilityBoostByAccount = {};
1028
985
  const accountSnapshotSource = accountManager;
@@ -1111,22 +1068,23 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1111
1068
  const hadAccountId = !!account.accountId;
1112
1069
  const tokenAccountId = extractAccountId(accountAuth.access);
1113
1070
  const accountId = resolveRequestAccountId(account.accountId, account.accountIdSource, tokenAccountId);
1114
- const entitlementAccountKey = resolveEntitlementAccountKey({
1115
- accountId: hadAccountId ? account.accountId : undefined,
1116
- email: account.email,
1117
- index: account.index,
1118
- });
1119
1071
  if (!accountId) {
1120
1072
  accountManager.markAccountCoolingDown(account, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS, "auth-failure");
1121
1073
  accountManager.saveToDiskDebounced();
1122
1074
  continue;
1123
1075
  }
1076
+ const resolvedEmail = extractAccountEmail(accountAuth.access) ?? account.email;
1077
+ const entitlementAccountKey = resolveEntitlementAccountKey({
1078
+ accountId: account.accountId ?? accountId,
1079
+ email: resolvedEmail,
1080
+ refreshToken: account.refreshToken,
1081
+ index: account.index,
1082
+ });
1124
1083
  account.accountId = accountId;
1125
1084
  if (!hadAccountId && tokenAccountId && accountId === tokenAccountId) {
1126
1085
  account.accountIdSource = account.accountIdSource ?? "token";
1127
1086
  }
1128
- account.email =
1129
- extractAccountEmail(accountAuth.access) ?? account.email;
1087
+ account.email = resolvedEmail;
1130
1088
  const entitlementBlock = entitlementCache.isBlocked(entitlementAccountKey, model ?? modelFamily);
1131
1089
  if (entitlementBlock.blocked) {
1132
1090
  runtimeMetrics.accountRotations++;
@@ -1299,6 +1257,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1299
1257
  fallbackApplied: false,
1300
1258
  fallbackReason: "unsupported-model-entitlement",
1301
1259
  });
1260
+ retryNextAccountBeforeFallback = true;
1302
1261
  break;
1303
1262
  }
1304
1263
  const fallbackModel = resolveUnsupportedCodexFallbackModel({
@@ -1626,7 +1585,9 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1626
1585
  if (successAccountForResponse.index !== account.index) {
1627
1586
  accountManager.markSwitched(successAccountForResponse, "rotation", modelFamily);
1628
1587
  }
1629
- const successAccountKey = resolveEntitlementAccountKey(successAccountForResponse);
1588
+ const successAccountKey = successAccountForResponse.index === account.index
1589
+ ? entitlementAccountKey
1590
+ : resolveEntitlementAccountKey(successAccountForResponse);
1630
1591
  accountManager.recordSuccess(successAccountForResponse, modelFamily, model);
1631
1592
  capabilityPolicyStore.recordSuccess(successAccountKey, capabilityModelKey);
1632
1593
  entitlementCache.clear(successAccountKey, capabilityModelKey);
@@ -1639,6 +1600,10 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1639
1600
  }
1640
1601
  return successResponse;
1641
1602
  }
1603
+ if (retryNextAccountBeforeFallback) {
1604
+ retryNextAccountBeforeFallback = false;
1605
+ continue;
1606
+ }
1642
1607
  if (restartAccountTraversalWithFallback) {
1643
1608
  break;
1644
1609
  }
@@ -2331,7 +2296,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2331
2296
  await clearAccounts();
2332
2297
  await clearFlaggedAccounts();
2333
2298
  invalidateAccountManagerCache();
2334
- console.log("\nDeleted all accounts. Starting fresh.\n");
2299
+ console.log("\nCleared saved accounts from active storage. Recovery snapshots remain available. Starting fresh.\n");
2335
2300
  }
2336
2301
  break;
2337
2302
  }
@@ -2398,9 +2363,17 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
2398
2363
  const accountId = resolved.accountIdOverride ?? extractAccountId(resolved.access);
2399
2364
  const label = resolved.accountLabel ?? email ?? accountId ?? "Unknown account";
2400
2365
  logInfo(`Authenticated as: ${label}`);
2401
- const isDuplicate = accounts.some((account) => (accountId &&
2402
- (account.accountIdOverride ?? extractAccountId(account.access)) === accountId) ||
2403
- (email && extractAccountEmail(account.access, account.idToken) === email));
2366
+ const isDuplicate = findMatchingAccountIndex(accounts.map((account) => ({
2367
+ accountId: account.accountIdOverride ?? extractAccountId(account.access),
2368
+ email: sanitizeEmail(extractAccountEmail(account.access, account.idToken)),
2369
+ refreshToken: account.refresh,
2370
+ })), {
2371
+ accountId,
2372
+ email: sanitizeEmail(email),
2373
+ refreshToken: resolved.refresh,
2374
+ }, {
2375
+ allowUniqueAccountIdFallbackWithoutEmail: true,
2376
+ }) !== undefined;
2404
2377
  if (isDuplicate) {
2405
2378
  logWarn(`WARNING: duplicate account login detected (${label}). Existing entry will be updated.`);
2406
2379
  }