codex-multi-auth 0.1.7 → 0.1.8

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 (57) hide show
  1. package/LICENSE +21 -1
  2. package/README.md +13 -0
  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 +2 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/lib/codex-cli/sync.d.ts +9 -16
  9. package/dist/lib/codex-cli/sync.d.ts.map +1 -1
  10. package/dist/lib/codex-cli/sync.js +51 -284
  11. package/dist/lib/codex-cli/sync.js.map +1 -1
  12. package/dist/lib/codex-manager/settings-hub.d.ts +44 -2
  13. package/dist/lib/codex-manager/settings-hub.d.ts.map +1 -1
  14. package/dist/lib/codex-manager/settings-hub.js +746 -117
  15. package/dist/lib/codex-manager/settings-hub.js.map +1 -1
  16. package/dist/lib/codex-manager.d.ts.map +1 -1
  17. package/dist/lib/codex-manager.js +3 -8
  18. package/dist/lib/codex-manager.js.map +1 -1
  19. package/dist/lib/context-overflow.js +10 -10
  20. package/dist/lib/named-backup-export.d.ts +69 -0
  21. package/dist/lib/named-backup-export.d.ts.map +1 -0
  22. package/dist/lib/named-backup-export.js +207 -0
  23. package/dist/lib/named-backup-export.js.map +1 -0
  24. package/dist/lib/oc-chatgpt-import-adapter.d.ts +64 -0
  25. package/dist/lib/oc-chatgpt-import-adapter.d.ts.map +1 -0
  26. package/dist/lib/oc-chatgpt-import-adapter.js +506 -0
  27. package/dist/lib/oc-chatgpt-import-adapter.js.map +1 -0
  28. package/dist/lib/oc-chatgpt-orchestrator.d.ts +80 -0
  29. package/dist/lib/oc-chatgpt-orchestrator.d.ts.map +1 -0
  30. package/dist/lib/oc-chatgpt-orchestrator.js +130 -0
  31. package/dist/lib/oc-chatgpt-orchestrator.js.map +1 -0
  32. package/dist/lib/oc-chatgpt-target-detection.d.ts +59 -0
  33. package/dist/lib/oc-chatgpt-target-detection.d.ts.map +1 -0
  34. package/dist/lib/oc-chatgpt-target-detection.js +327 -0
  35. package/dist/lib/oc-chatgpt-target-detection.js.map +1 -0
  36. package/dist/lib/prompts/codex.js +2 -2
  37. package/dist/lib/runtime-paths.d.ts.map +1 -1
  38. package/dist/lib/runtime-paths.js +21 -1
  39. package/dist/lib/runtime-paths.js.map +1 -1
  40. package/dist/lib/storage.d.ts +42 -3
  41. package/dist/lib/storage.d.ts.map +1 -1
  42. package/dist/lib/storage.js +519 -51
  43. package/dist/lib/storage.js.map +1 -1
  44. package/dist/lib/ui/copy.d.ts +15 -1
  45. package/dist/lib/ui/copy.d.ts.map +1 -1
  46. package/dist/lib/ui/copy.js +15 -1
  47. package/dist/lib/ui/copy.js.map +1 -1
  48. package/package.json +132 -133
  49. package/scripts/bench-format/hashline-v2.mjs +642 -642
  50. package/scripts/bench-format/render.mjs +496 -496
  51. package/scripts/bench-format/stats.mjs +54 -54
  52. package/scripts/bench-format/tasks.mjs +151 -151
  53. package/scripts/benchmark-render-dashboard.mjs +49 -49
  54. package/scripts/install-codex-auth.js +9 -9
  55. package/scripts/test-all-models.sh +7 -7
  56. package/scripts/test-model-matrix.js +138 -46
  57. package/scripts/validate-model-map.sh +7 -7
@@ -1,12 +1,14 @@
1
- import { promises as fs, existsSync } from "node:fs";
2
- import { basename, dirname, join } from "node:path";
1
+ import { AsyncLocalStorage } from "node:async_hooks";
3
2
  import { createHash } from "node:crypto";
3
+ import { existsSync, promises as fs } from "node:fs";
4
+ import { basename, dirname, join } from "node:path";
4
5
  import { ACCOUNT_LIMITS } from "./constants.js";
5
6
  import { createLogger } from "./logger.js";
7
+ import { exportNamedBackupFile, resolveNamedBackupPath, } from "./named-backup-export.js";
6
8
  import { MODEL_FAMILIES } from "./prompts/codex.js";
7
9
  import { AnyAccountStorageSchema, getValidationErrors } from "./schemas.js";
8
- import { getConfigDir, getProjectConfigDir, getProjectGlobalConfigDir, findProjectRoot, resolvePath, resolveProjectStorageIdentityRoot, } from "./storage/paths.js";
9
10
  import { migrateV1ToV3, } from "./storage/migrations.js";
11
+ import { findProjectRoot, getConfigDir, getProjectConfigDir, getProjectGlobalConfigDir, resolvePath, resolveProjectStorageIdentityRoot, } from "./storage/paths.js";
10
12
  const log = createLogger("storage");
11
13
  const ACCOUNTS_FILE_NAME = "openai-codex-accounts.json";
12
14
  const FLAGGED_ACCOUNTS_FILE_NAME = "openai-codex-flagged-accounts.json";
@@ -16,6 +18,7 @@ const ACCOUNTS_WAL_SUFFIX = ".wal";
16
18
  const ACCOUNTS_BACKUP_HISTORY_DEPTH = 3;
17
19
  const BACKUP_COPY_MAX_ATTEMPTS = 5;
18
20
  const BACKUP_COPY_BASE_DELAY_MS = 10;
21
+ const RESET_MARKER_SUFFIX = ".reset-intent";
19
22
  let storageBackupEnabled = true;
20
23
  let lastAccountsSaveTimestamp = 0;
21
24
  /**
@@ -59,6 +62,7 @@ export function formatStorageErrorHint(error, path) {
59
62
  }
60
63
  }
61
64
  let storageMutex = Promise.resolve();
65
+ const transactionSnapshotContext = new AsyncLocalStorage();
62
66
  function withStorageLock(fn) {
63
67
  const previousMutex = storageMutex;
64
68
  let releaseLock;
@@ -69,8 +73,12 @@ function withStorageLock(fn) {
69
73
  }
70
74
  function looksLikeSyntheticFixtureAccount(account) {
71
75
  const email = typeof account.email === "string" ? account.email.trim().toLowerCase() : "";
72
- const refreshToken = typeof account.refreshToken === "string" ? account.refreshToken.trim().toLowerCase() : "";
73
- const accountId = typeof account.accountId === "string" ? account.accountId.trim().toLowerCase() : "";
76
+ const refreshToken = typeof account.refreshToken === "string"
77
+ ? account.refreshToken.trim().toLowerCase()
78
+ : "";
79
+ const accountId = typeof account.accountId === "string"
80
+ ? account.accountId.trim().toLowerCase()
81
+ : "";
74
82
  if (!/^account\d+@example\.com$/.test(email)) {
75
83
  return false;
76
84
  }
@@ -104,7 +112,10 @@ async function ensureGitignore(storagePath) {
104
112
  if (existsSync(gitignorePath)) {
105
113
  content = await fs.readFile(gitignorePath, "utf-8");
106
114
  const lines = content.split("\n").map((l) => l.trim());
107
- if (lines.includes(".codex") || lines.includes(".codex/") || lines.includes("/.codex") || lines.includes("/.codex/")) {
115
+ if (lines.includes(".codex") ||
116
+ lines.includes(".codex/") ||
117
+ lines.includes("/.codex") ||
118
+ lines.includes("/.codex/")) {
108
119
  return;
109
120
  }
110
121
  }
@@ -152,6 +163,10 @@ async function getAccountsBackupRecoveryCandidatesWithDiscovery(path) {
152
163
  continue;
153
164
  if (!entry.name.startsWith(candidatePrefix))
154
165
  continue;
166
+ if (isCacheLikeBackupArtifactName(entry.name))
167
+ continue;
168
+ if (entry.name.endsWith(RESET_MARKER_SUFFIX))
169
+ continue;
155
170
  if (entry.name.endsWith(".tmp"))
156
171
  continue;
157
172
  if (entry.name.includes(".rotate."))
@@ -232,7 +247,9 @@ async function createRotatingAccountsBackup(path) {
232
247
  continue;
233
248
  }
234
249
  const stagedPath = buildStagedPath(currentPath, `slot-${i}`);
235
- await copyFileWithRetry(previousPath, stagedPath, { allowMissingSource: true });
250
+ await copyFileWithRetry(previousPath, stagedPath, {
251
+ allowMissingSource: true,
252
+ });
236
253
  if (existsSync(stagedPath)) {
237
254
  stagedWrites.push({ targetPath: currentPath, stagedPath });
238
255
  }
@@ -244,7 +261,10 @@ async function createRotatingAccountsBackup(path) {
244
261
  const latestStagedPath = buildStagedPath(latestBackupPath, "latest");
245
262
  await copyFileWithRetry(path, latestStagedPath);
246
263
  if (existsSync(latestStagedPath)) {
247
- stagedWrites.push({ targetPath: latestBackupPath, stagedPath: latestStagedPath });
264
+ stagedWrites.push({
265
+ targetPath: latestBackupPath,
266
+ stagedPath: latestStagedPath,
267
+ });
248
268
  }
249
269
  for (const stagedWrite of stagedWrites) {
250
270
  await renameFileWithRetry(stagedWrite.stagedPath, stagedWrite.targetPath);
@@ -266,7 +286,8 @@ async function createRotatingAccountsBackup(path) {
266
286
  }
267
287
  function isRotatingBackupTempArtifact(storagePath, candidatePath) {
268
288
  const backupPrefix = `${storagePath}${ACCOUNTS_BACKUP_SUFFIX}`;
269
- if (!candidatePath.startsWith(backupPrefix) || !candidatePath.endsWith(".tmp")) {
289
+ if (!candidatePath.startsWith(backupPrefix) ||
290
+ !candidatePath.endsWith(".tmp")) {
270
291
  return false;
271
292
  }
272
293
  const suffix = candidatePath.slice(backupPrefix.length);
@@ -283,7 +304,9 @@ function isRotatingBackupTempArtifact(storagePath, candidatePath) {
283
304
  async function cleanupStaleRotatingBackupArtifacts(path) {
284
305
  const directoryPath = dirname(path);
285
306
  try {
286
- const directoryEntries = await fs.readdir(directoryPath, { withFileTypes: true });
307
+ const directoryEntries = await fs.readdir(directoryPath, {
308
+ withFileTypes: true,
309
+ });
287
310
  const staleArtifacts = directoryEntries
288
311
  .filter((entry) => entry.isFile())
289
312
  .map((entry) => join(directoryPath, entry.name))
@@ -316,6 +339,198 @@ async function cleanupStaleRotatingBackupArtifacts(path) {
316
339
  function computeSha256(value) {
317
340
  return createHash("sha256").update(value).digest("hex");
318
341
  }
342
+ function getIntentionalResetMarkerPath(path) {
343
+ return `${path}${RESET_MARKER_SUFFIX}`;
344
+ }
345
+ function createEmptyStorageWithMetadata(restoreEligible, restoreReason) {
346
+ return {
347
+ version: 3,
348
+ accounts: [],
349
+ activeIndex: 0,
350
+ activeIndexByFamily: {},
351
+ restoreEligible,
352
+ restoreReason,
353
+ };
354
+ }
355
+ function withRestoreMetadata(storage, restoreEligible, restoreReason) {
356
+ return {
357
+ ...storage,
358
+ restoreEligible,
359
+ restoreReason,
360
+ };
361
+ }
362
+ function isCacheLikeBackupArtifactName(entryName) {
363
+ return entryName.toLowerCase().includes(".cache");
364
+ }
365
+ async function statSnapshot(path) {
366
+ try {
367
+ const stats = await fs.stat(path);
368
+ return { exists: true, bytes: stats.size, mtimeMs: stats.mtimeMs };
369
+ }
370
+ catch (error) {
371
+ const code = error.code;
372
+ if (code !== "ENOENT") {
373
+ log.warn("Failed to stat backup candidate", {
374
+ path,
375
+ error: String(error),
376
+ });
377
+ }
378
+ return { exists: false };
379
+ }
380
+ }
381
+ async function describeAccountSnapshot(path, kind, index) {
382
+ const stats = await statSnapshot(path);
383
+ if (!stats.exists) {
384
+ return { kind, path, index, exists: false, valid: false };
385
+ }
386
+ try {
387
+ const { normalized, schemaErrors, storedVersion } = await loadAccountsFromPath(path);
388
+ return {
389
+ kind,
390
+ path,
391
+ index,
392
+ exists: true,
393
+ valid: !!normalized,
394
+ bytes: stats.bytes,
395
+ mtimeMs: stats.mtimeMs,
396
+ version: typeof storedVersion === "number" ? storedVersion : undefined,
397
+ accountCount: normalized?.accounts.length,
398
+ schemaErrors: schemaErrors.length > 0 ? schemaErrors : undefined,
399
+ };
400
+ }
401
+ catch (error) {
402
+ const code = error.code;
403
+ if (code !== "ENOENT") {
404
+ log.warn("Failed to inspect account snapshot", {
405
+ path,
406
+ error: String(error),
407
+ });
408
+ }
409
+ return {
410
+ kind,
411
+ path,
412
+ index,
413
+ exists: true,
414
+ valid: false,
415
+ bytes: stats.bytes,
416
+ mtimeMs: stats.mtimeMs,
417
+ };
418
+ }
419
+ }
420
+ async function describeAccountsWalSnapshot(path) {
421
+ const stats = await statSnapshot(path);
422
+ if (!stats.exists) {
423
+ return { kind: "accounts-wal", path, exists: false, valid: false };
424
+ }
425
+ try {
426
+ const raw = await fs.readFile(path, "utf-8");
427
+ const parsed = JSON.parse(raw);
428
+ if (!isRecord(parsed)) {
429
+ return {
430
+ kind: "accounts-wal",
431
+ path,
432
+ exists: true,
433
+ valid: false,
434
+ bytes: stats.bytes,
435
+ mtimeMs: stats.mtimeMs,
436
+ };
437
+ }
438
+ const entry = parsed;
439
+ if (entry.version !== 1 ||
440
+ typeof entry.content !== "string" ||
441
+ typeof entry.checksum !== "string" ||
442
+ computeSha256(entry.content) !== entry.checksum) {
443
+ return {
444
+ kind: "accounts-wal",
445
+ path,
446
+ exists: true,
447
+ valid: false,
448
+ bytes: stats.bytes,
449
+ mtimeMs: stats.mtimeMs,
450
+ };
451
+ }
452
+ const { normalized, storedVersion, schemaErrors } = parseAndNormalizeStorage(JSON.parse(entry.content));
453
+ return {
454
+ kind: "accounts-wal",
455
+ path,
456
+ exists: true,
457
+ valid: !!normalized,
458
+ bytes: stats.bytes,
459
+ mtimeMs: stats.mtimeMs,
460
+ version: typeof storedVersion === "number" ? storedVersion : undefined,
461
+ accountCount: normalized?.accounts.length,
462
+ schemaErrors: schemaErrors.length > 0 ? schemaErrors : undefined,
463
+ };
464
+ }
465
+ catch {
466
+ return {
467
+ kind: "accounts-wal",
468
+ path,
469
+ exists: true,
470
+ valid: false,
471
+ bytes: stats.bytes,
472
+ mtimeMs: stats.mtimeMs,
473
+ };
474
+ }
475
+ }
476
+ async function loadFlaggedAccountsFromPath(path) {
477
+ const content = await fs.readFile(path, "utf-8");
478
+ const data = JSON.parse(content);
479
+ return normalizeFlaggedStorage(data);
480
+ }
481
+ async function describeFlaggedSnapshot(path, kind, index) {
482
+ const stats = await statSnapshot(path);
483
+ if (!stats.exists) {
484
+ return { kind, path, index, exists: false, valid: false };
485
+ }
486
+ try {
487
+ const storage = await loadFlaggedAccountsFromPath(path);
488
+ return {
489
+ kind,
490
+ path,
491
+ index,
492
+ exists: true,
493
+ valid: true,
494
+ bytes: stats.bytes,
495
+ mtimeMs: stats.mtimeMs,
496
+ version: storage.version,
497
+ flaggedCount: storage.accounts.length,
498
+ };
499
+ }
500
+ catch (error) {
501
+ const code = error.code;
502
+ if (code !== "ENOENT") {
503
+ log.warn("Failed to inspect flagged snapshot", {
504
+ path,
505
+ error: String(error),
506
+ });
507
+ }
508
+ return {
509
+ kind,
510
+ path,
511
+ index,
512
+ exists: true,
513
+ valid: false,
514
+ bytes: stats.bytes,
515
+ mtimeMs: stats.mtimeMs,
516
+ };
517
+ }
518
+ }
519
+ function latestValidSnapshot(snapshots) {
520
+ return snapshots
521
+ .filter((snapshot) => snapshot.valid)
522
+ .sort((left, right) => (right.mtimeMs ?? 0) - (left.mtimeMs ?? 0))[0];
523
+ }
524
+ function buildMetadataSection(storagePath, snapshots) {
525
+ const latestValid = latestValidSnapshot(snapshots);
526
+ return {
527
+ storagePath,
528
+ latestValidPath: latestValid?.path,
529
+ snapshotCount: snapshots.length,
530
+ validSnapshotCount: snapshots.filter((snapshot) => snapshot.valid).length,
531
+ snapshots,
532
+ };
533
+ }
319
534
  export function getLastAccountsSaveTimestamp() {
320
535
  return lastAccountsSaveTimestamp;
321
536
  }
@@ -335,7 +550,9 @@ export function setStoragePath(projectPath) {
335
550
  currentLegacyProjectStoragePath = join(getProjectConfigDir(projectRoot), ACCOUNTS_FILE_NAME);
336
551
  const previousWorktreeScopedPath = join(getProjectGlobalConfigDir(projectRoot), ACCOUNTS_FILE_NAME);
337
552
  currentLegacyWorktreeStoragePath =
338
- previousWorktreeScopedPath !== currentStoragePath ? previousWorktreeScopedPath : null;
553
+ previousWorktreeScopedPath !== currentStoragePath
554
+ ? previousWorktreeScopedPath
555
+ : null;
339
556
  }
340
557
  else {
341
558
  currentStoragePath = null;
@@ -360,6 +577,15 @@ export function getStoragePath() {
360
577
  }
361
578
  return join(getConfigDir(), ACCOUNTS_FILE_NAME);
362
579
  }
580
+ export function buildNamedBackupPath(name) {
581
+ return resolveNamedBackupPath(name, getStoragePath());
582
+ }
583
+ export async function exportNamedBackup(name, options) {
584
+ return exportNamedBackupFile(name, {
585
+ getStoragePath,
586
+ exportAccounts,
587
+ }, options);
588
+ }
363
589
  export function getFlaggedAccountsPath() {
364
590
  return join(dirname(getStoragePath()), FLAGGED_ACCOUNTS_FILE_NAME);
365
591
  }
@@ -370,8 +596,13 @@ async function migrateLegacyProjectStorageIfNeeded(persist = saveAccounts) {
370
596
  if (!currentStoragePath) {
371
597
  return null;
372
598
  }
373
- const candidatePaths = [currentLegacyWorktreeStoragePath, currentLegacyProjectStoragePath]
374
- .filter((path) => typeof path === "string" && path.length > 0 && path !== currentStoragePath)
599
+ const candidatePaths = [
600
+ currentLegacyWorktreeStoragePath,
601
+ currentLegacyProjectStoragePath,
602
+ ]
603
+ .filter((path) => typeof path === "string" &&
604
+ path.length > 0 &&
605
+ path !== currentStoragePath)
375
606
  .filter((path, index, all) => all.indexOf(path) === index);
376
607
  if (candidatePaths.length === 0) {
377
608
  return null;
@@ -565,7 +796,8 @@ export function deduplicateAccountsByEmail(accounts) {
565
796
  const existingAddedAt = existing.addedAt || 0;
566
797
  const candidateAddedAt = account.addedAt || 0;
567
798
  const isNewer = candidateLastUsed > existingLastUsed ||
568
- (candidateLastUsed === existingLastUsed && candidateAddedAt > existingAddedAt);
799
+ (candidateLastUsed === existingLastUsed &&
800
+ candidateAddedAt > existingAddedAt);
569
801
  if (isNewer) {
570
802
  emailToNewestIndex.set(email, i);
571
803
  }
@@ -637,7 +869,9 @@ export function normalizeAccountStorage(data) {
637
869
  const baseStorage = fromVersion === 1
638
870
  ? migrateV1ToV3(data)
639
871
  : data;
640
- const validAccounts = rawAccounts.filter((account) => isRecord(account) && typeof account.refreshToken === "string" && !!account.refreshToken.trim());
872
+ const validAccounts = rawAccounts.filter((account) => isRecord(account) &&
873
+ typeof account.refreshToken === "string" &&
874
+ !!account.refreshToken.trim());
641
875
  const deduplicatedAccounts = deduplicateAccountsByEmail(deduplicateAccountsByKey(validAccounts));
642
876
  const activeIndex = (() => {
643
877
  if (deduplicatedAccounts.length === 0)
@@ -684,10 +918,88 @@ export function normalizeAccountStorage(data) {
684
918
  export async function loadAccounts() {
685
919
  return loadAccountsInternal(saveAccounts);
686
920
  }
921
+ export async function getBackupMetadata() {
922
+ const storagePath = getStoragePath();
923
+ const walPath = getAccountsWalPath(storagePath);
924
+ const accountCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(storagePath);
925
+ const accountSnapshots = [
926
+ await describeAccountSnapshot(storagePath, "accounts-primary"),
927
+ await describeAccountsWalSnapshot(walPath),
928
+ ];
929
+ for (const [index, candidate] of accountCandidates.entries()) {
930
+ const kind = candidate === `${storagePath}.bak`
931
+ ? "accounts-backup"
932
+ : candidate.startsWith(`${storagePath}.bak.`)
933
+ ? "accounts-backup-history"
934
+ : "accounts-discovered-backup";
935
+ accountSnapshots.push(await describeAccountSnapshot(candidate, kind, index));
936
+ }
937
+ const flaggedPath = getFlaggedAccountsPath();
938
+ const flaggedCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(flaggedPath);
939
+ const flaggedSnapshots = [
940
+ await describeFlaggedSnapshot(flaggedPath, "flagged-primary"),
941
+ ];
942
+ for (const [index, candidate] of flaggedCandidates.entries()) {
943
+ const kind = candidate === `${flaggedPath}.bak`
944
+ ? "flagged-backup"
945
+ : candidate.startsWith(`${flaggedPath}.bak.`)
946
+ ? "flagged-backup-history"
947
+ : "flagged-discovered-backup";
948
+ flaggedSnapshots.push(await describeFlaggedSnapshot(candidate, kind, index));
949
+ }
950
+ return {
951
+ accounts: buildMetadataSection(storagePath, accountSnapshots),
952
+ flaggedAccounts: buildMetadataSection(flaggedPath, flaggedSnapshots),
953
+ };
954
+ }
955
+ export async function getRestoreAssessment() {
956
+ const storagePath = getStoragePath();
957
+ const resetMarkerPath = getIntentionalResetMarkerPath(storagePath);
958
+ const backupMetadata = await getBackupMetadata();
959
+ if (existsSync(resetMarkerPath)) {
960
+ return {
961
+ storagePath,
962
+ restoreEligible: false,
963
+ restoreReason: "intentional-reset",
964
+ backupMetadata,
965
+ };
966
+ }
967
+ const primarySnapshot = backupMetadata.accounts.snapshots.find((snapshot) => snapshot.kind === "accounts-primary");
968
+ if (!primarySnapshot?.exists) {
969
+ return {
970
+ storagePath,
971
+ restoreEligible: true,
972
+ restoreReason: "missing-storage",
973
+ latestSnapshot: backupMetadata.accounts.latestValidPath
974
+ ? backupMetadata.accounts.snapshots.find((snapshot) => snapshot.path === backupMetadata.accounts.latestValidPath)
975
+ : undefined,
976
+ backupMetadata,
977
+ };
978
+ }
979
+ if (primarySnapshot.valid && primarySnapshot.accountCount === 0) {
980
+ return {
981
+ storagePath,
982
+ restoreEligible: true,
983
+ restoreReason: "empty-storage",
984
+ latestSnapshot: primarySnapshot,
985
+ backupMetadata,
986
+ };
987
+ }
988
+ return {
989
+ storagePath,
990
+ restoreEligible: false,
991
+ latestSnapshot: backupMetadata.accounts.latestValidPath
992
+ ? backupMetadata.accounts.snapshots.find((snapshot) => snapshot.path === backupMetadata.accounts.latestValidPath)
993
+ : undefined,
994
+ backupMetadata,
995
+ };
996
+ }
687
997
  function parseAndNormalizeStorage(data) {
688
998
  const schemaErrors = getValidationErrors(AnyAccountStorageSchema, data);
689
999
  const normalized = normalizeAccountStorage(data);
690
- const storedVersion = isRecord(data) ? data.version : undefined;
1000
+ const storedVersion = isRecord(data)
1001
+ ? data.version
1002
+ : undefined;
691
1003
  return { normalized, storedVersion, schemaErrors };
692
1004
  }
693
1005
  async function loadAccountsFromPath(path) {
@@ -697,8 +1009,15 @@ async function loadAccountsFromPath(path) {
697
1009
  }
698
1010
  async function loadAccountsFromJournal(path) {
699
1011
  const walPath = getAccountsWalPath(path);
1012
+ const resetMarkerPath = getIntentionalResetMarkerPath(path);
1013
+ if (existsSync(resetMarkerPath)) {
1014
+ return null;
1015
+ }
700
1016
  try {
701
1017
  const raw = await fs.readFile(walPath, "utf-8");
1018
+ if (existsSync(resetMarkerPath)) {
1019
+ return null;
1020
+ }
702
1021
  const parsed = JSON.parse(raw);
703
1022
  if (!isRecord(parsed))
704
1023
  return null;
@@ -722,13 +1041,17 @@ async function loadAccountsFromJournal(path) {
722
1041
  catch (error) {
723
1042
  const code = error.code;
724
1043
  if (code !== "ENOENT") {
725
- log.warn("Failed to load account WAL journal", { path: walPath, error: String(error) });
1044
+ log.warn("Failed to load account WAL journal", {
1045
+ path: walPath,
1046
+ error: String(error),
1047
+ });
726
1048
  }
727
1049
  return null;
728
1050
  }
729
1051
  }
730
1052
  async function loadAccountsInternal(persistMigration) {
731
1053
  const path = getStoragePath();
1054
+ const resetMarkerPath = getIntentionalResetMarkerPath(path);
732
1055
  await cleanupStaleRotatingBackupArtifacts(path);
733
1056
  const migratedLegacyStorage = persistMigration
734
1057
  ? await migrateLegacyProjectStorageIfNeeded(persistMigration)
@@ -736,19 +1059,32 @@ async function loadAccountsInternal(persistMigration) {
736
1059
  try {
737
1060
  const { normalized, storedVersion, schemaErrors } = await loadAccountsFromPath(path);
738
1061
  if (schemaErrors.length > 0) {
739
- log.warn("Account storage schema validation warnings", { errors: schemaErrors.slice(0, 5) });
1062
+ log.warn("Account storage schema validation warnings", {
1063
+ errors: schemaErrors.slice(0, 5),
1064
+ });
740
1065
  }
741
1066
  if (normalized && storedVersion !== normalized.version) {
742
- log.info("Migrating account storage to v3", { from: storedVersion, to: normalized.version });
1067
+ log.info("Migrating account storage to v3", {
1068
+ from: storedVersion,
1069
+ to: normalized.version,
1070
+ });
743
1071
  if (persistMigration) {
744
1072
  try {
745
1073
  await persistMigration(normalized);
746
1074
  }
747
1075
  catch (saveError) {
748
- log.warn("Failed to persist migrated storage", { error: String(saveError) });
1076
+ log.warn("Failed to persist migrated storage", {
1077
+ error: String(saveError),
1078
+ });
749
1079
  }
750
1080
  }
751
1081
  }
1082
+ if (existsSync(resetMarkerPath)) {
1083
+ return createEmptyStorageWithMetadata(false, "intentional-reset");
1084
+ }
1085
+ if (normalized && normalized.accounts.length === 0) {
1086
+ return withRestoreMetadata(normalized, true, "empty-storage");
1087
+ }
752
1088
  const primaryLooksSynthetic = looksLikeSyntheticFixtureStorage(normalized);
753
1089
  if (storageBackupEnabled && normalized && primaryLooksSynthetic) {
754
1090
  const backupCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
@@ -797,6 +1133,9 @@ async function loadAccountsInternal(persistMigration) {
797
1133
  }
798
1134
  catch (error) {
799
1135
  const code = error.code;
1136
+ if (existsSync(resetMarkerPath)) {
1137
+ return createEmptyStorageWithMetadata(false, "intentional-reset");
1138
+ }
800
1139
  if (code === "ENOENT" && migratedLegacyStorage) {
801
1140
  return migratedLegacyStorage;
802
1141
  }
@@ -815,6 +1154,9 @@ async function loadAccountsInternal(persistMigration) {
815
1154
  }
816
1155
  return recoveredFromWal;
817
1156
  }
1157
+ if (existsSync(resetMarkerPath)) {
1158
+ return createEmptyStorageWithMetadata(false, "intentional-reset");
1159
+ }
818
1160
  if (storageBackupEnabled) {
819
1161
  const backupCandidates = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
820
1162
  for (const backupPath of backupCandidates) {
@@ -827,7 +1169,10 @@ async function loadAccountsInternal(persistMigration) {
827
1169
  });
828
1170
  }
829
1171
  if (backup.normalized) {
830
- log.warn("Recovered account storage from backup file", { path, backupPath });
1172
+ log.warn("Recovered account storage from backup file", {
1173
+ path,
1174
+ backupPath,
1175
+ });
831
1176
  if (persistMigration) {
832
1177
  try {
833
1178
  await persistMigration(backup.normalized);
@@ -856,11 +1201,15 @@ async function loadAccountsInternal(persistMigration) {
856
1201
  if (code !== "ENOENT") {
857
1202
  log.error("Failed to load account storage", { error: String(error) });
858
1203
  }
1204
+ if (code === "ENOENT") {
1205
+ return createEmptyStorageWithMetadata(true, "missing-storage");
1206
+ }
859
1207
  return null;
860
1208
  }
861
1209
  }
862
1210
  async function saveAccountsUnlocked(storage) {
863
1211
  const path = getStoragePath();
1212
+ const resetMarkerPath = getIntentionalResetMarkerPath(path);
864
1213
  const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
865
1214
  const tempPath = `${path}.${uniqueSuffix}.tmp`;
866
1215
  const walPath = getAccountsWalPath(path);
@@ -870,7 +1219,9 @@ async function saveAccountsUnlocked(storage) {
870
1219
  if (looksLikeSyntheticFixtureStorage(storage)) {
871
1220
  try {
872
1221
  const existing = await loadNormalizedStorageFromPath(path, "existing account storage");
873
- if (existing && existing.accounts.length > 0 && !looksLikeSyntheticFixtureStorage(existing)) {
1222
+ if (existing &&
1223
+ existing.accounts.length > 0 &&
1224
+ !looksLikeSyntheticFixtureStorage(existing)) {
874
1225
  throw new StorageError("Refusing to overwrite non-synthetic account storage with synthetic fixture payload", "EINVALID", path, "Detected synthetic fixture-like account payload. Use explicit account import/login commands instead.");
875
1226
  }
876
1227
  }
@@ -916,6 +1267,12 @@ async function saveAccountsUnlocked(storage) {
916
1267
  for (let attempt = 0; attempt < 5; attempt++) {
917
1268
  try {
918
1269
  await fs.rename(tempPath, path);
1270
+ try {
1271
+ await fs.unlink(resetMarkerPath);
1272
+ }
1273
+ catch {
1274
+ // Best effort cleanup.
1275
+ }
919
1276
  lastAccountsSaveTimestamp = Date.now();
920
1277
  try {
921
1278
  await fs.unlink(walPath);
@@ -929,7 +1286,7 @@ async function saveAccountsUnlocked(storage) {
929
1286
  const code = renameError.code;
930
1287
  if (code === "EPERM" || code === "EBUSY") {
931
1288
  lastError = renameError;
932
- await new Promise(r => setTimeout(r, 10 * Math.pow(2, attempt)));
1289
+ await new Promise((r) => setTimeout(r, 10 * 2 ** attempt));
933
1290
  continue;
934
1291
  }
935
1292
  throw renameError;
@@ -959,8 +1316,16 @@ async function saveAccountsUnlocked(storage) {
959
1316
  }
960
1317
  export async function withAccountStorageTransaction(handler) {
961
1318
  return withStorageLock(async () => {
962
- const current = await loadAccountsInternal(saveAccountsUnlocked);
963
- return handler(current, saveAccountsUnlocked);
1319
+ const state = {
1320
+ snapshot: await loadAccountsInternal(saveAccountsUnlocked),
1321
+ active: true,
1322
+ };
1323
+ const current = state.snapshot;
1324
+ const persist = async (storage) => {
1325
+ await saveAccountsUnlocked(storage);
1326
+ state.snapshot = storage;
1327
+ };
1328
+ return transactionSnapshotContext.run(state, () => handler(current, persist));
964
1329
  });
965
1330
  }
966
1331
  /**
@@ -982,8 +1347,10 @@ export async function saveAccounts(storage) {
982
1347
  export async function clearAccounts() {
983
1348
  return withStorageLock(async () => {
984
1349
  const path = getStoragePath();
1350
+ const resetMarkerPath = getIntentionalResetMarkerPath(path);
985
1351
  const walPath = getAccountsWalPath(path);
986
- const backupPaths = getAccountsBackupRecoveryCandidates(path);
1352
+ const backupPaths = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
1353
+ await fs.writeFile(resetMarkerPath, JSON.stringify({ version: 1, createdAt: Date.now() }), { encoding: "utf-8", mode: 0o600 });
987
1354
  const clearPath = async (targetPath) => {
988
1355
  try {
989
1356
  await fs.unlink(targetPath);
@@ -999,7 +1366,11 @@ export async function clearAccounts() {
999
1366
  }
1000
1367
  };
1001
1368
  try {
1002
- await Promise.all([clearPath(path), clearPath(walPath), ...backupPaths.map(clearPath)]);
1369
+ await Promise.all([
1370
+ clearPath(path),
1371
+ clearPath(walPath),
1372
+ ...backupPaths.map(clearPath),
1373
+ ]);
1003
1374
  }
1004
1375
  catch {
1005
1376
  // Individual path cleanup is already best-effort with per-artifact logging.
@@ -1014,13 +1385,22 @@ function normalizeFlaggedStorage(data) {
1014
1385
  for (const rawAccount of data.accounts) {
1015
1386
  if (!isRecord(rawAccount))
1016
1387
  continue;
1017
- const refreshToken = typeof rawAccount.refreshToken === "string" ? rawAccount.refreshToken.trim() : "";
1388
+ const refreshToken = typeof rawAccount.refreshToken === "string"
1389
+ ? rawAccount.refreshToken.trim()
1390
+ : "";
1018
1391
  if (!refreshToken)
1019
1392
  continue;
1020
- const flaggedAt = typeof rawAccount.flaggedAt === "number" ? rawAccount.flaggedAt : Date.now();
1021
- const isAccountIdSource = (value) => value === "token" || value === "id_token" || value === "org" || value === "manual";
1393
+ const flaggedAt = typeof rawAccount.flaggedAt === "number"
1394
+ ? rawAccount.flaggedAt
1395
+ : Date.now();
1396
+ const isAccountIdSource = (value) => value === "token" ||
1397
+ value === "id_token" ||
1398
+ value === "org" ||
1399
+ value === "manual";
1022
1400
  const isSwitchReason = (value) => value === "rate-limit" || value === "initial" || value === "rotation";
1023
- const isCooldownReason = (value) => value === "auth-failure" || value === "network-error" || value === "rate-limit";
1401
+ const isCooldownReason = (value) => value === "auth-failure" ||
1402
+ value === "network-error" ||
1403
+ value === "rate-limit";
1024
1404
  let rateLimitResetTimes;
1025
1405
  if (isRecord(rawAccount.rateLimitResetTimes)) {
1026
1406
  const normalizedRateLimits = {};
@@ -1045,19 +1425,33 @@ function normalizeFlaggedStorage(data) {
1045
1425
  const normalized = {
1046
1426
  refreshToken,
1047
1427
  addedAt: typeof rawAccount.addedAt === "number" ? rawAccount.addedAt : flaggedAt,
1048
- lastUsed: typeof rawAccount.lastUsed === "number" ? rawAccount.lastUsed : flaggedAt,
1049
- accountId: typeof rawAccount.accountId === "string" ? rawAccount.accountId : undefined,
1428
+ lastUsed: typeof rawAccount.lastUsed === "number"
1429
+ ? rawAccount.lastUsed
1430
+ : flaggedAt,
1431
+ accountId: typeof rawAccount.accountId === "string"
1432
+ ? rawAccount.accountId
1433
+ : undefined,
1050
1434
  accountIdSource,
1051
- accountLabel: typeof rawAccount.accountLabel === "string" ? rawAccount.accountLabel : undefined,
1435
+ accountLabel: typeof rawAccount.accountLabel === "string"
1436
+ ? rawAccount.accountLabel
1437
+ : undefined,
1052
1438
  email: typeof rawAccount.email === "string" ? rawAccount.email : undefined,
1053
- enabled: typeof rawAccount.enabled === "boolean" ? rawAccount.enabled : undefined,
1439
+ enabled: typeof rawAccount.enabled === "boolean"
1440
+ ? rawAccount.enabled
1441
+ : undefined,
1054
1442
  lastSwitchReason,
1055
1443
  rateLimitResetTimes,
1056
- coolingDownUntil: typeof rawAccount.coolingDownUntil === "number" ? rawAccount.coolingDownUntil : undefined,
1444
+ coolingDownUntil: typeof rawAccount.coolingDownUntil === "number"
1445
+ ? rawAccount.coolingDownUntil
1446
+ : undefined,
1057
1447
  cooldownReason,
1058
1448
  flaggedAt,
1059
- flaggedReason: typeof rawAccount.flaggedReason === "string" ? rawAccount.flaggedReason : undefined,
1060
- lastError: typeof rawAccount.lastError === "string" ? rawAccount.lastError : undefined,
1449
+ flaggedReason: typeof rawAccount.flaggedReason === "string"
1450
+ ? rawAccount.flaggedReason
1451
+ : undefined,
1452
+ lastError: typeof rawAccount.lastError === "string"
1453
+ ? rawAccount.lastError
1454
+ : undefined,
1061
1455
  };
1062
1456
  byRefreshToken.set(refreshToken, normalized);
1063
1457
  }
@@ -1068,16 +1462,24 @@ function normalizeFlaggedStorage(data) {
1068
1462
  }
1069
1463
  export async function loadFlaggedAccounts() {
1070
1464
  const path = getFlaggedAccountsPath();
1465
+ const resetMarkerPath = getIntentionalResetMarkerPath(path);
1071
1466
  const empty = { version: 1, accounts: [] };
1072
1467
  try {
1073
1468
  const content = await fs.readFile(path, "utf-8");
1074
1469
  const data = JSON.parse(content);
1075
- return normalizeFlaggedStorage(data);
1470
+ const loaded = normalizeFlaggedStorage(data);
1471
+ if (existsSync(resetMarkerPath)) {
1472
+ return empty;
1473
+ }
1474
+ return loaded;
1076
1475
  }
1077
1476
  catch (error) {
1078
1477
  const code = error.code;
1079
1478
  if (code !== "ENOENT") {
1080
- log.error("Failed to load flagged account storage", { path, error: String(error) });
1479
+ log.error("Failed to load flagged account storage", {
1480
+ path,
1481
+ error: String(error),
1482
+ });
1081
1483
  return empty;
1082
1484
  }
1083
1485
  }
@@ -1117,13 +1519,31 @@ export async function loadFlaggedAccounts() {
1117
1519
  export async function saveFlaggedAccounts(storage) {
1118
1520
  return withStorageLock(async () => {
1119
1521
  const path = getFlaggedAccountsPath();
1522
+ const markerPath = getIntentionalResetMarkerPath(path);
1120
1523
  const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
1121
1524
  const tempPath = `${path}.${uniqueSuffix}.tmp`;
1122
1525
  try {
1123
1526
  await fs.mkdir(dirname(path), { recursive: true });
1527
+ if (existsSync(path)) {
1528
+ try {
1529
+ await fs.copyFile(path, `${path}.bak`);
1530
+ }
1531
+ catch (backupError) {
1532
+ log.warn("Failed to create flagged backup snapshot", {
1533
+ path,
1534
+ error: String(backupError),
1535
+ });
1536
+ }
1537
+ }
1124
1538
  const content = JSON.stringify(normalizeFlaggedStorage(storage), null, 2);
1125
1539
  await fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
1126
1540
  await fs.rename(tempPath, path);
1541
+ try {
1542
+ await fs.unlink(markerPath);
1543
+ }
1544
+ catch {
1545
+ // Best effort cleanup.
1546
+ }
1127
1547
  }
1128
1548
  catch (error) {
1129
1549
  try {
@@ -1132,20 +1552,48 @@ export async function saveFlaggedAccounts(storage) {
1132
1552
  catch {
1133
1553
  // Ignore cleanup failures.
1134
1554
  }
1135
- log.error("Failed to save flagged account storage", { path, error: String(error) });
1555
+ log.error("Failed to save flagged account storage", {
1556
+ path,
1557
+ error: String(error),
1558
+ });
1136
1559
  throw error;
1137
1560
  }
1138
1561
  });
1139
1562
  }
1140
1563
  export async function clearFlaggedAccounts() {
1141
1564
  return withStorageLock(async () => {
1565
+ const path = getFlaggedAccountsPath();
1566
+ const markerPath = getIntentionalResetMarkerPath(path);
1142
1567
  try {
1143
- await fs.unlink(getFlaggedAccountsPath());
1568
+ await fs.writeFile(markerPath, "reset", {
1569
+ encoding: "utf-8",
1570
+ mode: 0o600,
1571
+ });
1144
1572
  }
1145
1573
  catch (error) {
1146
- const code = error.code;
1147
- if (code !== "ENOENT") {
1148
- log.error("Failed to clear flagged account storage", { error: String(error) });
1574
+ log.error("Failed to write flagged reset marker", {
1575
+ path,
1576
+ markerPath,
1577
+ error: String(error),
1578
+ });
1579
+ throw error;
1580
+ }
1581
+ const backupPaths = await getAccountsBackupRecoveryCandidatesWithDiscovery(path);
1582
+ for (const candidate of [path, ...backupPaths, markerPath]) {
1583
+ try {
1584
+ await fs.unlink(candidate);
1585
+ }
1586
+ catch (error) {
1587
+ const code = error.code;
1588
+ if (code !== "ENOENT") {
1589
+ log.error("Failed to clear flagged account storage", {
1590
+ path: candidate,
1591
+ error: String(error),
1592
+ });
1593
+ if (candidate === path) {
1594
+ throw error;
1595
+ }
1596
+ }
1149
1597
  }
1150
1598
  }
1151
1599
  });
@@ -1156,19 +1604,34 @@ export async function clearFlaggedAccounts() {
1156
1604
  * @param force - If true, overwrite existing file (default: true)
1157
1605
  * @throws Error if file exists and force is false, or if no accounts to export
1158
1606
  */
1159
- export async function exportAccounts(filePath, force = true) {
1607
+ export async function exportAccounts(filePath, force = true, beforeCommit) {
1160
1608
  const resolvedPath = resolvePath(filePath);
1161
1609
  if (!force && existsSync(resolvedPath)) {
1162
1610
  throw new Error(`File already exists: ${resolvedPath}`);
1163
1611
  }
1164
- const storage = await withAccountStorageTransaction((current) => Promise.resolve(current));
1612
+ const transactionState = transactionSnapshotContext.getStore();
1613
+ const storage = transactionState?.active
1614
+ ? transactionState.snapshot
1615
+ : await withAccountStorageTransaction((current) => Promise.resolve(current));
1165
1616
  if (!storage || storage.accounts.length === 0) {
1166
1617
  throw new Error("No accounts to export");
1167
1618
  }
1168
1619
  await fs.mkdir(dirname(resolvedPath), { recursive: true });
1169
- const content = JSON.stringify(storage, null, 2);
1620
+ await beforeCommit?.(resolvedPath);
1621
+ if (!force && existsSync(resolvedPath)) {
1622
+ throw new Error(`File already exists: ${resolvedPath}`);
1623
+ }
1624
+ const content = JSON.stringify({
1625
+ version: storage.version,
1626
+ accounts: storage.accounts,
1627
+ activeIndex: storage.activeIndex,
1628
+ activeIndexByFamily: storage.activeIndexByFamily,
1629
+ }, null, 2);
1170
1630
  await fs.writeFile(resolvedPath, content, { encoding: "utf-8", mode: 0o600 });
1171
- log.info("Exported accounts", { path: resolvedPath, count: storage.accounts.length });
1631
+ log.info("Exported accounts", {
1632
+ path: resolvedPath,
1633
+ count: storage.accounts.length,
1634
+ });
1172
1635
  }
1173
1636
  /**
1174
1637
  * Imports accounts from a JSON file, merging with existing accounts.
@@ -1194,7 +1657,7 @@ export async function importAccounts(filePath) {
1194
1657
  if (!normalized) {
1195
1658
  throw new Error("Invalid account storage format");
1196
1659
  }
1197
- const { imported: importedCount, total, skipped: skippedCount } = await withAccountStorageTransaction(async (existing, persist) => {
1660
+ const { imported: importedCount, total, skipped: skippedCount, } = await withAccountStorageTransaction(async (existing, persist) => {
1198
1661
  const existingAccounts = existing?.accounts ?? [];
1199
1662
  const existingActiveIndex = existing?.activeIndex ?? 0;
1200
1663
  const merged = [...existingAccounts, ...normalized.accounts];
@@ -1216,7 +1679,12 @@ export async function importAccounts(filePath) {
1216
1679
  const skipped = normalized.accounts.length - imported;
1217
1680
  return { imported, total: deduplicatedAccounts.length, skipped };
1218
1681
  });
1219
- log.info("Imported accounts", { path: resolvedPath, imported: importedCount, skipped: skippedCount, total });
1682
+ log.info("Imported accounts", {
1683
+ path: resolvedPath,
1684
+ imported: importedCount,
1685
+ skipped: skippedCount,
1686
+ total,
1687
+ });
1220
1688
  return { imported: importedCount, total, skipped: skippedCount };
1221
1689
  }
1222
1690
  //# sourceMappingURL=storage.js.map