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
@@ -14,7 +14,7 @@ import { MODEL_FAMILIES } from "./prompts/codex.js";
14
14
  import { fetchCodexQuotaSnapshot, formatQuotaSnapshotLine, } from "./quota-probe.js";
15
15
  import { queuedRefresh } from "./refresh-queue.js";
16
16
  import { loadQuotaCache, saveQuotaCache, } from "./quota-cache.js";
17
- import { getStoragePath, loadFlaggedAccounts, loadAccounts, saveFlaggedAccounts, saveAccounts, setStoragePath, } from "./storage.js";
17
+ import { clearAccounts, findMatchingAccountIndex, getStoragePath, loadFlaggedAccounts, loadAccounts, saveFlaggedAccounts, saveAccounts, setStoragePath, withAccountAndFlaggedStorageTransaction, withAccountStorageTransaction, } from "./storage.js";
18
18
  import { getCodexCliAuthPath, getCodexCliConfigPath, loadCodexCliState, } from "./codex-cli/state.js";
19
19
  import { setCodexCliActiveSelection } from "./codex-cli/writer.js";
20
20
  import { ANSI } from "./ui/ansi.js";
@@ -322,6 +322,26 @@ function normalizeQuotaEmail(email) {
322
322
  const normalized = sanitizeEmail(email);
323
323
  return normalized && normalized.length > 0 ? normalized : null;
324
324
  }
325
+ function normalizeQuotaAccountId(accountId) {
326
+ if (typeof accountId !== "string")
327
+ return null;
328
+ const trimmed = accountId.trim();
329
+ return trimmed.length > 0 ? trimmed : null;
330
+ }
331
+ function hasUniqueQuotaAccountId(accounts, account) {
332
+ const accountId = normalizeQuotaAccountId(account.accountId);
333
+ if (!accountId)
334
+ return false;
335
+ let matchCount = 0;
336
+ for (const candidate of accounts) {
337
+ if (normalizeQuotaAccountId(candidate.accountId) !== accountId)
338
+ continue;
339
+ matchCount += 1;
340
+ if (matchCount > 1)
341
+ return false;
342
+ }
343
+ return matchCount === 1;
344
+ }
325
345
  function quotaCacheEntryToSnapshot(entry) {
326
346
  return {
327
347
  status: entry.status,
@@ -389,17 +409,20 @@ function formatAccountQuotaSummary(entry) {
389
409
  }
390
410
  return formatQuotaSnapshotLine(quotaCacheEntryToSnapshot(entry));
391
411
  }
392
- function getQuotaCacheEntryForAccount(cache, account) {
393
- if (account.accountId && cache.byAccountId[account.accountId]) {
394
- return cache.byAccountId[account.accountId] ?? null;
395
- }
412
+ function getQuotaCacheEntryForAccount(cache, account, accounts) {
396
413
  const email = normalizeQuotaEmail(account.email);
397
414
  if (email && cache.byEmail[email]) {
398
415
  return cache.byEmail[email] ?? null;
399
416
  }
417
+ const accountId = normalizeQuotaAccountId(account.accountId);
418
+ if (accountId &&
419
+ hasUniqueQuotaAccountId(accounts, account) &&
420
+ cache.byAccountId[accountId]) {
421
+ return cache.byAccountId[accountId] ?? null;
422
+ }
400
423
  return null;
401
424
  }
402
- function updateQuotaCacheForAccount(cache, account, snapshot) {
425
+ function updateQuotaCacheForAccount(cache, account, snapshot, accounts) {
403
426
  const nextEntry = {
404
427
  updatedAt: Date.now(),
405
428
  status: snapshot.status,
@@ -417,25 +440,27 @@ function updateQuotaCacheForAccount(cache, account, snapshot) {
417
440
  },
418
441
  };
419
442
  let changed = false;
420
- if (account.accountId) {
421
- cache.byAccountId[account.accountId] = nextEntry;
422
- changed = true;
423
- }
424
443
  const email = normalizeQuotaEmail(account.email);
425
444
  if (email) {
426
445
  cache.byEmail[email] = nextEntry;
427
446
  changed = true;
447
+ return changed;
448
+ }
449
+ const accountId = normalizeQuotaAccountId(account.accountId);
450
+ if (accountId && hasUniqueQuotaAccountId(accounts, account)) {
451
+ cache.byAccountId[accountId] = nextEntry;
452
+ changed = true;
428
453
  }
429
454
  return changed;
430
455
  }
431
456
  const DEFAULT_MENU_QUOTA_REFRESH_TTL_MS = 5 * 60_000;
432
457
  const MENU_QUOTA_REFRESH_MODEL = "gpt-5-codex";
433
- function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now) {
458
+ function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, accounts) {
434
459
  if (account.enabled === false)
435
460
  return null;
436
461
  if (!hasUsableAccessToken(account, now))
437
462
  return null;
438
- const existing = getQuotaCacheEntryForAccount(cache, account);
463
+ const existing = getQuotaCacheEntryForAccount(cache, account, accounts);
439
464
  if (existing &&
440
465
  typeof existing.updatedAt === "number" &&
441
466
  Number.isFinite(existing.updatedAt) &&
@@ -453,7 +478,7 @@ function resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now) {
453
478
  function collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now()) {
454
479
  const targets = [];
455
480
  for (const account of storage.accounts) {
456
- const probeInput = resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now);
481
+ const probeInput = resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts);
457
482
  if (!probeInput)
458
483
  continue;
459
484
  targets.push({
@@ -467,7 +492,7 @@ function collectMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now
467
492
  function countMenuQuotaRefreshTargets(storage, cache, maxAgeMs, now = Date.now()) {
468
493
  let count = 0;
469
494
  for (const account of storage.accounts) {
470
- if (resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now)) {
495
+ if (resolveMenuQuotaProbeInput(account, cache, maxAgeMs, now, storage.accounts)) {
471
496
  count += 1;
472
497
  }
473
498
  }
@@ -492,7 +517,9 @@ async function refreshQuotaCacheForMenu(storage, cache, maxAgeMs, onProgress) {
492
517
  accessToken: target.accessToken,
493
518
  model: MENU_QUOTA_REFRESH_MODEL,
494
519
  });
495
- changed = updateQuotaCacheForAccount(cache, target.account, snapshot) || changed;
520
+ changed =
521
+ updateQuotaCacheForAccount(cache, target.account, snapshot, storage.accounts) ||
522
+ changed;
496
523
  }
497
524
  catch {
498
525
  // Keep existing cached values if probing fails.
@@ -615,7 +642,9 @@ function toExistingAccountInfo(storage, quotaCache, displaySettings) {
615
642
  const activeIndex = resolveActiveIndex(storage, "codex");
616
643
  const layoutMode = resolveMenuLayoutMode(displaySettings);
617
644
  const baseAccounts = storage.accounts.map((account, index) => {
618
- const entry = quotaCache ? getQuotaCacheEntryForAccount(quotaCache, account) : null;
645
+ const entry = quotaCache
646
+ ? getQuotaCacheEntryForAccount(quotaCache, account, storage.accounts)
647
+ : null;
619
648
  return {
620
649
  index,
621
650
  sourceIndex: index,
@@ -1058,118 +1087,83 @@ async function runOAuthFlow(forceNewLogin) {
1058
1087
  async function persistAccountPool(results, replaceAll) {
1059
1088
  if (results.length === 0)
1060
1089
  return;
1061
- const loadedStorage = replaceAll
1062
- ? null
1063
- : await loadAccounts();
1064
- const now = Date.now();
1065
- const accounts = loadedStorage?.accounts ? [...loadedStorage.accounts] : [];
1066
- const indexByRefreshToken = new Map();
1067
- const indexByAccountId = new Map();
1068
- const indexByEmail = new Map();
1069
- let selectedAccountIndex = null;
1070
- for (let i = 0; i < accounts.length; i += 1) {
1071
- const account = accounts[i];
1072
- if (!account)
1073
- continue;
1074
- if (account.refreshToken)
1075
- indexByRefreshToken.set(account.refreshToken, i);
1076
- if (account.accountId)
1077
- indexByAccountId.set(account.accountId, i);
1078
- if (account.email)
1079
- indexByEmail.set(account.email, i);
1080
- }
1081
- for (const result of results) {
1082
- const tokenAccountId = extractAccountId(result.access);
1083
- const accountId = resolveRequestAccountId(result.accountIdOverride, result.accountIdSource, tokenAccountId);
1084
- const accountIdSource = accountId
1085
- ? (result.accountIdSource ?? (result.accountIdOverride ? "manual" : "token"))
1086
- : undefined;
1087
- const accountLabel = result.accountLabel;
1088
- const accountEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
1089
- const existingByEmail = accountEmail && indexByEmail.has(accountEmail)
1090
- ? indexByEmail.get(accountEmail)
1091
- : undefined;
1092
- const existingById = accountId && indexByAccountId.has(accountId)
1093
- ? indexByAccountId.get(accountId)
1094
- : undefined;
1095
- const existingByToken = indexByRefreshToken.get(result.refresh);
1096
- const existingIndex = existingById ?? existingByEmail ?? existingByToken;
1097
- if (existingIndex === undefined) {
1098
- const newIndex = accounts.length;
1099
- accounts.push({
1090
+ await withAccountStorageTransaction(async (loadedStorage, persist) => {
1091
+ const stored = replaceAll ? null : loadedStorage;
1092
+ const now = Date.now();
1093
+ const accounts = stored?.accounts ? [...stored.accounts] : [];
1094
+ let selectedAccountIndex = null;
1095
+ for (const result of results) {
1096
+ const tokenAccountId = extractAccountId(result.access);
1097
+ const accountId = resolveRequestAccountId(result.accountIdOverride, result.accountIdSource, tokenAccountId);
1098
+ const accountIdSource = accountId
1099
+ ? (result.accountIdSource ?? (result.accountIdOverride ? "manual" : "token"))
1100
+ : undefined;
1101
+ const accountLabel = result.accountLabel;
1102
+ const accountEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
1103
+ const existingIndex = findMatchingAccountIndex(accounts, {
1100
1104
  accountId,
1101
- accountIdSource,
1102
- accountLabel,
1103
1105
  email: accountEmail,
1104
1106
  refreshToken: result.refresh,
1107
+ }, {
1108
+ allowUniqueAccountIdFallbackWithoutEmail: true,
1109
+ });
1110
+ if (existingIndex === undefined) {
1111
+ const newIndex = accounts.length;
1112
+ accounts.push({
1113
+ accountId,
1114
+ accountIdSource,
1115
+ accountLabel,
1116
+ email: accountEmail,
1117
+ refreshToken: result.refresh,
1118
+ accessToken: result.access,
1119
+ expiresAt: result.expires,
1120
+ enabled: true,
1121
+ addedAt: now,
1122
+ lastUsed: now,
1123
+ });
1124
+ selectedAccountIndex = newIndex;
1125
+ continue;
1126
+ }
1127
+ const existing = accounts[existingIndex];
1128
+ if (!existing)
1129
+ continue;
1130
+ const nextEmail = accountEmail ?? existing.email;
1131
+ const nextAccountId = accountId ?? existing.accountId;
1132
+ const nextAccountIdSource = accountId
1133
+ ? (accountIdSource ?? existing.accountIdSource)
1134
+ : existing.accountIdSource;
1135
+ accounts[existingIndex] = {
1136
+ ...existing,
1137
+ accountId: nextAccountId,
1138
+ accountIdSource: nextAccountIdSource,
1139
+ accountLabel: accountLabel ?? existing.accountLabel,
1140
+ email: nextEmail,
1141
+ refreshToken: result.refresh,
1105
1142
  accessToken: result.access,
1106
1143
  expiresAt: result.expires,
1107
1144
  enabled: true,
1108
- addedAt: now,
1109
1145
  lastUsed: now,
1110
- });
1111
- indexByRefreshToken.set(result.refresh, newIndex);
1112
- if (accountId)
1113
- indexByAccountId.set(accountId, newIndex);
1114
- if (accountEmail)
1115
- indexByEmail.set(accountEmail, newIndex);
1116
- selectedAccountIndex = newIndex;
1117
- continue;
1118
- }
1119
- const existing = accounts[existingIndex];
1120
- if (!existing)
1121
- continue;
1122
- const oldToken = existing.refreshToken;
1123
- const oldEmail = existing.email;
1124
- const nextEmail = accountEmail ?? existing.email;
1125
- const nextAccountId = accountId ?? existing.accountId;
1126
- const nextAccountIdSource = accountId
1127
- ? (accountIdSource ?? existing.accountIdSource)
1128
- : existing.accountIdSource;
1129
- accounts[existingIndex] = {
1130
- ...existing,
1131
- accountId: nextAccountId,
1132
- accountIdSource: nextAccountIdSource,
1133
- accountLabel: accountLabel ?? existing.accountLabel,
1134
- email: nextEmail,
1135
- refreshToken: result.refresh,
1136
- accessToken: result.access,
1137
- expiresAt: result.expires,
1138
- enabled: true,
1139
- lastUsed: now,
1140
- };
1141
- if (oldToken !== result.refresh) {
1142
- indexByRefreshToken.delete(oldToken);
1143
- indexByRefreshToken.set(result.refresh, existingIndex);
1144
- }
1145
- if (nextAccountId) {
1146
- indexByAccountId.set(nextAccountId, existingIndex);
1147
- }
1148
- if (oldEmail && oldEmail !== nextEmail) {
1149
- indexByEmail.delete(oldEmail);
1150
- }
1151
- if (nextEmail) {
1152
- indexByEmail.set(nextEmail, existingIndex);
1153
- }
1154
- selectedAccountIndex = existingIndex;
1155
- }
1156
- const fallbackActiveIndex = accounts.length === 0
1157
- ? 0
1158
- : Math.max(0, Math.min(loadedStorage?.activeIndex ?? 0, accounts.length - 1));
1159
- const nextActiveIndex = accounts.length === 0
1160
- ? 0
1161
- : selectedAccountIndex === null
1162
- ? fallbackActiveIndex
1163
- : Math.max(0, Math.min(selectedAccountIndex, accounts.length - 1));
1164
- const activeIndexByFamily = {};
1165
- for (const family of MODEL_FAMILIES) {
1166
- activeIndexByFamily[family] = nextActiveIndex;
1167
- }
1168
- await saveAccounts({
1169
- version: 3,
1170
- accounts,
1171
- activeIndex: nextActiveIndex,
1172
- activeIndexByFamily,
1146
+ };
1147
+ selectedAccountIndex = existingIndex;
1148
+ }
1149
+ const fallbackActiveIndex = accounts.length === 0
1150
+ ? 0
1151
+ : Math.max(0, Math.min(stored?.activeIndex ?? 0, accounts.length - 1));
1152
+ const nextActiveIndex = accounts.length === 0
1153
+ ? 0
1154
+ : selectedAccountIndex === null
1155
+ ? fallbackActiveIndex
1156
+ : Math.max(0, Math.min(selectedAccountIndex, accounts.length - 1));
1157
+ const activeIndexByFamily = {};
1158
+ for (const family of MODEL_FAMILIES) {
1159
+ activeIndexByFamily[family] = nextActiveIndex;
1160
+ }
1161
+ await persist({
1162
+ version: 3,
1163
+ accounts,
1164
+ activeIndex: nextActiveIndex,
1165
+ activeIndexByFamily,
1166
+ });
1173
1167
  });
1174
1168
  }
1175
1169
  async function syncSelectionToCodex(tokens) {
@@ -1279,7 +1273,7 @@ async function runHealthCheck(options = {}) {
1279
1273
  });
1280
1274
  if (quotaCache) {
1281
1275
  quotaCacheChanged =
1282
- updateQuotaCacheForAccount(quotaCache, account, snapshot) || quotaCacheChanged;
1276
+ updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
1283
1277
  }
1284
1278
  healthDetail = formatQuotaSnapshotForDashboard(snapshot, display);
1285
1279
  }
@@ -1349,7 +1343,7 @@ async function runHealthCheck(options = {}) {
1349
1343
  });
1350
1344
  if (quotaCache) {
1351
1345
  quotaCacheChanged =
1352
- updateQuotaCacheForAccount(quotaCache, account, snapshot) || quotaCacheChanged;
1346
+ updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
1353
1347
  }
1354
1348
  healthyMessage = formatQuotaSnapshotForDashboard(snapshot, display);
1355
1349
  }
@@ -1751,7 +1745,7 @@ async function runForecast(args) {
1751
1745
  const account = storage.accounts[i];
1752
1746
  if (account) {
1753
1747
  quotaCacheChanged =
1754
- updateQuotaCacheForAccount(quotaCache, account, liveQuota) || quotaCacheChanged;
1748
+ updateQuotaCacheForAccount(quotaCache, account, liveQuota, storage.accounts) || quotaCacheChanged;
1755
1749
  }
1756
1750
  }
1757
1751
  }
@@ -2007,24 +2001,24 @@ function findExistingAccountIndexForFlagged(storage, flagged, nextRefreshToken,
2007
2001
  const flaggedEmail = sanitizeEmail(flagged.email);
2008
2002
  const candidateAccountId = nextAccountId ?? flagged.accountId;
2009
2003
  const candidateEmail = sanitizeEmail(nextEmail) ?? flaggedEmail;
2010
- for (let i = 0; i < storage.accounts.length; i += 1) {
2011
- const account = storage.accounts[i];
2012
- if (!account)
2013
- continue;
2014
- if (account.refreshToken === flagged.refreshToken || account.refreshToken === nextRefreshToken) {
2015
- return i;
2016
- }
2017
- if (candidateAccountId &&
2018
- typeof account.accountId === "string" &&
2019
- account.accountId === candidateAccountId) {
2020
- return i;
2021
- }
2022
- const existingEmail = sanitizeEmail(account.email);
2023
- if (candidateEmail && existingEmail && existingEmail === candidateEmail) {
2024
- return i;
2025
- }
2026
- }
2027
- return -1;
2004
+ const nextMatchIndex = findMatchingAccountIndex(storage.accounts, {
2005
+ accountId: candidateAccountId,
2006
+ email: candidateEmail,
2007
+ refreshToken: nextRefreshToken,
2008
+ }, {
2009
+ allowUniqueAccountIdFallbackWithoutEmail: true,
2010
+ });
2011
+ if (nextMatchIndex !== undefined) {
2012
+ return nextMatchIndex;
2013
+ }
2014
+ const flaggedMatchIndex = findMatchingAccountIndex(storage.accounts, {
2015
+ accountId: candidateAccountId,
2016
+ email: candidateEmail,
2017
+ refreshToken: flagged.refreshToken,
2018
+ }, {
2019
+ allowUniqueAccountIdFallbackWithoutEmail: true,
2020
+ });
2021
+ return flaggedMatchIndex ?? -1;
2028
2022
  }
2029
2023
  function upsertRecoveredFlaggedAccount(storage, flagged, refreshResult, now) {
2030
2024
  const nextEmail = sanitizeEmail(extractAccountEmail(refreshResult.access, refreshResult.idToken)) ?? flagged.email;
@@ -2129,24 +2123,65 @@ async function runVerifyFlagged(args) {
2129
2123
  console.log("No flagged accounts to check.");
2130
2124
  return 0;
2131
2125
  }
2132
- let storage = await loadAccounts();
2133
- if (!storage) {
2134
- storage = createEmptyAccountStorage();
2135
- }
2136
2126
  let storageChanged = false;
2137
2127
  let flaggedChanged = false;
2138
2128
  const reports = [];
2139
2129
  const nextFlaggedAccounts = [];
2140
2130
  const now = Date.now();
2131
+ const refreshChecks = [];
2141
2132
  for (let i = 0; i < flaggedStorage.accounts.length; i += 1) {
2142
2133
  const flagged = flaggedStorage.accounts[i];
2143
2134
  if (!flagged)
2144
2135
  continue;
2145
2136
  const label = formatAccountLabel(flagged, i);
2146
- const result = await queuedRefresh(flagged.refreshToken);
2147
- if (result.type === "success") {
2148
- if (!options.restore) {
2149
- const nextFlagged = {
2137
+ refreshChecks.push({
2138
+ index: i,
2139
+ flagged,
2140
+ label,
2141
+ result: await queuedRefresh(flagged.refreshToken),
2142
+ });
2143
+ }
2144
+ const applyRefreshChecks = (storage) => {
2145
+ for (const check of refreshChecks) {
2146
+ const { index: i, flagged, label, result } = check;
2147
+ if (result.type === "success") {
2148
+ if (!options.restore) {
2149
+ const nextFlagged = {
2150
+ ...flagged,
2151
+ refreshToken: result.refresh,
2152
+ accessToken: result.access,
2153
+ expiresAt: result.expires,
2154
+ accountId: extractAccountId(result.access) ?? flagged.accountId,
2155
+ accountIdSource: extractAccountId(result.access) ? "token" : flagged.accountIdSource,
2156
+ email: sanitizeEmail(extractAccountEmail(result.access, result.idToken)) ?? flagged.email,
2157
+ lastUsed: now,
2158
+ lastError: undefined,
2159
+ };
2160
+ nextFlaggedAccounts.push(nextFlagged);
2161
+ if (JSON.stringify(nextFlagged) !== JSON.stringify(flagged)) {
2162
+ flaggedChanged = true;
2163
+ }
2164
+ reports.push({
2165
+ index: i,
2166
+ label,
2167
+ outcome: "healthy-flagged",
2168
+ message: "session is healthy (left in flagged list due to --no-restore)",
2169
+ });
2170
+ continue;
2171
+ }
2172
+ const upsertResult = upsertRecoveredFlaggedAccount(storage, flagged, result, now);
2173
+ if (upsertResult.restored) {
2174
+ storageChanged = storageChanged || upsertResult.changed;
2175
+ flaggedChanged = true;
2176
+ reports.push({
2177
+ index: i,
2178
+ label,
2179
+ outcome: "restored",
2180
+ message: upsertResult.message,
2181
+ });
2182
+ continue;
2183
+ }
2184
+ const updatedFlagged = {
2150
2185
  ...flagged,
2151
2186
  refreshToken: result.refresh,
2152
2187
  accessToken: result.access,
@@ -2155,87 +2190,71 @@ async function runVerifyFlagged(args) {
2155
2190
  accountIdSource: extractAccountId(result.access) ? "token" : flagged.accountIdSource,
2156
2191
  email: sanitizeEmail(extractAccountEmail(result.access, result.idToken)) ?? flagged.email,
2157
2192
  lastUsed: now,
2158
- lastError: undefined,
2193
+ lastError: upsertResult.message,
2159
2194
  };
2160
- nextFlaggedAccounts.push(nextFlagged);
2161
- if (JSON.stringify(nextFlagged) !== JSON.stringify(flagged)) {
2195
+ nextFlaggedAccounts.push(updatedFlagged);
2196
+ if (JSON.stringify(updatedFlagged) !== JSON.stringify(flagged)) {
2162
2197
  flaggedChanged = true;
2163
2198
  }
2164
2199
  reports.push({
2165
2200
  index: i,
2166
2201
  label,
2167
- outcome: "healthy-flagged",
2168
- message: "session is healthy (left in flagged list due to --no-restore)",
2169
- });
2170
- continue;
2171
- }
2172
- const upsertResult = upsertRecoveredFlaggedAccount(storage, flagged, result, now);
2173
- if (upsertResult.restored) {
2174
- storageChanged = storageChanged || upsertResult.changed;
2175
- flaggedChanged = true;
2176
- reports.push({
2177
- index: i,
2178
- label,
2179
- outcome: "restored",
2202
+ outcome: "restore-skipped",
2180
2203
  message: upsertResult.message,
2181
2204
  });
2182
2205
  continue;
2183
2206
  }
2184
- const updatedFlagged = {
2207
+ const detail = normalizeFailureDetail(result.message, result.reason);
2208
+ const failedFlagged = {
2185
2209
  ...flagged,
2186
- refreshToken: result.refresh,
2187
- accessToken: result.access,
2188
- expiresAt: result.expires,
2189
- accountId: extractAccountId(result.access) ?? flagged.accountId,
2190
- accountIdSource: extractAccountId(result.access) ? "token" : flagged.accountIdSource,
2191
- email: sanitizeEmail(extractAccountEmail(result.access, result.idToken)) ?? flagged.email,
2192
- lastUsed: now,
2193
- lastError: upsertResult.message,
2210
+ lastError: detail,
2194
2211
  };
2195
- nextFlaggedAccounts.push(updatedFlagged);
2196
- if (JSON.stringify(updatedFlagged) !== JSON.stringify(flagged)) {
2212
+ nextFlaggedAccounts.push(failedFlagged);
2213
+ if ((flagged.lastError ?? "") !== detail) {
2197
2214
  flaggedChanged = true;
2198
2215
  }
2199
2216
  reports.push({
2200
2217
  index: i,
2201
2218
  label,
2202
- outcome: "restore-skipped",
2203
- message: upsertResult.message,
2219
+ outcome: "still-flagged",
2220
+ message: detail,
2204
2221
  });
2205
- continue;
2206
2222
  }
2207
- const detail = normalizeFailureDetail(result.message, result.reason);
2208
- const failedFlagged = {
2209
- ...flagged,
2210
- lastError: detail,
2211
- };
2212
- nextFlaggedAccounts.push(failedFlagged);
2213
- if ((flagged.lastError ?? "") !== detail) {
2214
- flaggedChanged = true;
2223
+ };
2224
+ if (options.restore) {
2225
+ if (options.dryRun) {
2226
+ applyRefreshChecks((await loadAccounts()) ?? createEmptyAccountStorage());
2215
2227
  }
2216
- reports.push({
2217
- index: i,
2218
- label,
2219
- outcome: "still-flagged",
2220
- message: detail,
2221
- });
2228
+ else {
2229
+ await withAccountAndFlaggedStorageTransaction(async (loadedStorage, persist) => {
2230
+ const nextStorage = loadedStorage
2231
+ ? structuredClone(loadedStorage)
2232
+ : createEmptyAccountStorage();
2233
+ applyRefreshChecks(nextStorage);
2234
+ if (!storageChanged) {
2235
+ return;
2236
+ }
2237
+ normalizeDoctorIndexes(nextStorage);
2238
+ await persist(nextStorage, {
2239
+ version: 1,
2240
+ accounts: nextFlaggedAccounts,
2241
+ });
2242
+ });
2243
+ }
2244
+ }
2245
+ else {
2246
+ applyRefreshChecks(createEmptyAccountStorage());
2222
2247
  }
2223
2248
  const remainingFlagged = nextFlaggedAccounts.length;
2224
2249
  const restored = reports.filter((report) => report.outcome === "restored").length;
2225
2250
  const healthyFlagged = reports.filter((report) => report.outcome === "healthy-flagged").length;
2226
2251
  const stillFlagged = reports.filter((report) => report.outcome === "still-flagged").length;
2227
2252
  const changed = storageChanged || flaggedChanged;
2228
- if (!options.dryRun) {
2229
- if (storageChanged) {
2230
- normalizeDoctorIndexes(storage);
2231
- await saveAccounts(storage);
2232
- }
2233
- if (flaggedChanged) {
2234
- await saveFlaggedAccounts({
2235
- version: 1,
2236
- accounts: nextFlaggedAccounts,
2237
- });
2238
- }
2253
+ if (!options.dryRun && flaggedChanged && (!options.restore || !storageChanged)) {
2254
+ await saveFlaggedAccounts({
2255
+ version: 1,
2256
+ accounts: nextFlaggedAccounts,
2257
+ });
2239
2258
  }
2240
2259
  if (options.json) {
2241
2260
  console.log(JSON.stringify({
@@ -2340,7 +2359,7 @@ async function runFix(args) {
2340
2359
  });
2341
2360
  if (quotaCache) {
2342
2361
  quotaCacheChanged =
2343
- updateQuotaCacheForAccount(quotaCache, account, snapshot) || quotaCacheChanged;
2362
+ updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
2344
2363
  }
2345
2364
  reports.push({
2346
2365
  index: i,
@@ -2413,7 +2432,7 @@ async function runFix(args) {
2413
2432
  });
2414
2433
  if (quotaCache) {
2415
2434
  quotaCacheChanged =
2416
- updateQuotaCacheForAccount(quotaCache, account, snapshot) || quotaCacheChanged;
2435
+ updateQuotaCacheForAccount(quotaCache, account, snapshot, storage.accounts) || quotaCacheChanged;
2417
2436
  }
2418
2437
  reports.push({
2419
2438
  index: i,
@@ -2615,6 +2634,12 @@ function normalizeDoctorIndexes(storage) {
2615
2634
  }
2616
2635
  return changed;
2617
2636
  }
2637
+ function getDoctorRefreshTokenKey(refreshToken) {
2638
+ if (typeof refreshToken !== "string")
2639
+ return undefined;
2640
+ const trimmed = refreshToken.trim();
2641
+ return trimmed || undefined;
2642
+ }
2618
2643
  function applyDoctorFixes(storage) {
2619
2644
  let changed = false;
2620
2645
  const actions = [];
@@ -2630,7 +2655,9 @@ function applyDoctorFixes(storage) {
2630
2655
  const account = storage.accounts[i];
2631
2656
  if (!account)
2632
2657
  continue;
2633
- const refreshToken = account.refreshToken.trim();
2658
+ const refreshToken = getDoctorRefreshTokenKey(account.refreshToken);
2659
+ if (!refreshToken)
2660
+ continue;
2634
2661
  const existingTokenIndex = seenRefreshTokens.get(refreshToken);
2635
2662
  if (typeof existingTokenIndex === "number") {
2636
2663
  if (account.enabled !== false) {
@@ -2879,7 +2906,9 @@ async function runDoctor(args) {
2879
2906
  const seenRefreshTokens = new Set();
2880
2907
  let duplicateTokenCount = 0;
2881
2908
  for (const account of storage.accounts) {
2882
- const token = account.refreshToken.trim();
2909
+ const token = getDoctorRefreshTokenKey(account.refreshToken);
2910
+ if (!token)
2911
+ continue;
2883
2912
  if (seenRefreshTokens.has(token)) {
2884
2913
  duplicateTokenCount += 1;
2885
2914
  }
@@ -3106,12 +3135,7 @@ async function runDoctor(args) {
3106
3135
  return summary.error > 0 ? 1 : 0;
3107
3136
  }
3108
3137
  async function clearAccountsAndReset() {
3109
- await saveAccounts({
3110
- version: 3,
3111
- accounts: [],
3112
- activeIndex: 0,
3113
- activeIndexByFamily: {},
3114
- });
3138
+ await clearAccounts();
3115
3139
  }
3116
3140
  async function handleManageAction(storage, menuResult) {
3117
3141
  if (typeof menuResult.switchAccountIndex === "number") {
@@ -3243,7 +3267,7 @@ async function runAuthLogin() {
3243
3267
  if (menuResult.mode === "fresh" && menuResult.deleteAll) {
3244
3268
  await runActionPanel("Reset Accounts", "Deleting all saved accounts", async () => {
3245
3269
  await clearAccountsAndReset();
3246
- console.log("Deleted all accounts.");
3270
+ console.log("Cleared saved accounts from active storage. Recovery snapshots remain available.");
3247
3271
  }, displaySettings);
3248
3272
  continue;
3249
3273
  }