codex-multi-auth 1.1.11 → 1.2.0

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 (44) hide show
  1. package/README.md +19 -4
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +185 -29
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/accounts.d.ts +22 -2
  6. package/dist/lib/accounts.d.ts.map +1 -1
  7. package/dist/lib/accounts.js +84 -1
  8. package/dist/lib/accounts.js.map +1 -1
  9. package/dist/lib/auth/browser.d.ts +1 -0
  10. package/dist/lib/auth/browser.d.ts.map +1 -1
  11. package/dist/lib/auth/browser.js +13 -0
  12. package/dist/lib/auth/browser.js.map +1 -1
  13. package/dist/lib/auth/token-utils.d.ts +16 -0
  14. package/dist/lib/auth/token-utils.d.ts.map +1 -1
  15. package/dist/lib/auth/token-utils.js +13 -0
  16. package/dist/lib/auth/token-utils.js.map +1 -1
  17. package/dist/lib/codex-manager/settings-hub.d.ts +21 -0
  18. package/dist/lib/codex-manager/settings-hub.d.ts.map +1 -1
  19. package/dist/lib/codex-manager/settings-hub.js +48 -65
  20. package/dist/lib/codex-manager/settings-hub.js.map +1 -1
  21. package/dist/lib/codex-manager.d.ts +1 -0
  22. package/dist/lib/codex-manager.d.ts.map +1 -1
  23. package/dist/lib/codex-manager.js +365 -33
  24. package/dist/lib/codex-manager.js.map +1 -1
  25. package/dist/lib/request/fetch-helpers.d.ts +18 -0
  26. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  27. package/dist/lib/request/fetch-helpers.js +157 -0
  28. package/dist/lib/request/fetch-helpers.js.map +1 -1
  29. package/dist/lib/schemas.d.ts +7 -0
  30. package/dist/lib/schemas.d.ts.map +1 -1
  31. package/dist/lib/schemas.js +7 -1
  32. package/dist/lib/schemas.js.map +1 -1
  33. package/dist/lib/storage/migrations.d.ts +5 -2
  34. package/dist/lib/storage/migrations.d.ts.map +1 -1
  35. package/dist/lib/storage/migrations.js.map +1 -1
  36. package/dist/lib/storage.d.ts +10 -0
  37. package/dist/lib/storage.d.ts.map +1 -1
  38. package/dist/lib/storage.js +191 -40
  39. package/dist/lib/storage.js.map +1 -1
  40. package/dist/lib/ui/copy.d.ts +24 -4
  41. package/dist/lib/ui/copy.d.ts.map +1 -1
  42. package/dist/lib/ui/copy.js +24 -4
  43. package/dist/lib/ui/copy.js.map +1 -1
  44. package/package.json +4 -3
@@ -1,10 +1,10 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { createHash } from "node:crypto";
3
3
  import { existsSync, promises as fs } from "node:fs";
4
- import { basename, dirname, join } from "node:path";
4
+ import { basename, dirname, isAbsolute, join, relative } from "node:path";
5
5
  import { ACCOUNT_LIMITS } from "./constants.js";
6
6
  import { createLogger } from "./logger.js";
7
- import { exportNamedBackupFile, resolveNamedBackupPath, } from "./named-backup-export.js";
7
+ import { exportNamedBackupFile, getNamedBackupRoot, resolveNamedBackupPath, } from "./named-backup-export.js";
8
8
  import { MODEL_FAMILIES } from "./prompts/codex.js";
9
9
  import { AnyAccountStorageSchema, getValidationErrors } from "./schemas.js";
10
10
  import { migrateV1ToV3, } from "./storage/migrations.js";
@@ -21,6 +21,71 @@ const BACKUP_COPY_BASE_DELAY_MS = 10;
21
21
  const RESET_MARKER_SUFFIX = ".reset-intent";
22
22
  let storageBackupEnabled = true;
23
23
  let lastAccountsSaveTimestamp = 0;
24
+ async function collectNamedBackups(storagePath) {
25
+ const backupRoot = getNamedBackupRoot(storagePath);
26
+ let entries;
27
+ try {
28
+ entries = await fs.readdir(backupRoot, {
29
+ withFileTypes: true,
30
+ encoding: "utf8",
31
+ });
32
+ }
33
+ catch (error) {
34
+ const code = error.code;
35
+ if (code === "ENOENT")
36
+ return [];
37
+ throw error;
38
+ }
39
+ const candidates = [];
40
+ for (const entry of entries) {
41
+ if (!entry.isFile())
42
+ continue;
43
+ if (!entry.name.toLowerCase().endsWith(".json"))
44
+ continue;
45
+ const candidatePath = join(backupRoot, entry.name);
46
+ try {
47
+ const statsBefore = await fs.stat(candidatePath);
48
+ const { normalized } = await loadAccountsFromPath(candidatePath);
49
+ if (!normalized || normalized.accounts.length === 0)
50
+ continue;
51
+ const statsAfter = await fs.stat(candidatePath).catch(() => null);
52
+ if (statsAfter && statsAfter.mtimeMs !== statsBefore.mtimeMs) {
53
+ log.debug("backup file changed between stat and load, mtime may be stale", {
54
+ candidatePath,
55
+ fileName: entry.name,
56
+ beforeMtimeMs: statsBefore.mtimeMs,
57
+ afterMtimeMs: statsAfter.mtimeMs,
58
+ });
59
+ }
60
+ candidates.push({
61
+ path: candidatePath,
62
+ fileName: entry.name,
63
+ accountCount: normalized.accounts.length,
64
+ mtimeMs: statsBefore.mtimeMs,
65
+ });
66
+ }
67
+ catch (error) {
68
+ log.debug("Skipping named backup candidate after loadAccountsFromPath/fs.stat failure", {
69
+ candidatePath,
70
+ fileName: entry.name,
71
+ error: error instanceof Error
72
+ ? {
73
+ message: error.message,
74
+ stack: error.stack,
75
+ }
76
+ : String(error),
77
+ });
78
+ continue;
79
+ }
80
+ }
81
+ candidates.sort((left, right) => {
82
+ const mtimeDelta = right.mtimeMs - left.mtimeMs;
83
+ if (mtimeDelta !== 0)
84
+ return mtimeDelta;
85
+ return left.fileName.localeCompare(right.fileName);
86
+ });
87
+ return candidates;
88
+ }
24
89
  /**
25
90
  * Custom error class for storage operations with platform-aware hints.
26
91
  */
@@ -98,11 +163,12 @@ function looksLikeSyntheticFixtureStorage(storage) {
98
163
  return storage.accounts.every((account) => looksLikeSyntheticFixtureAccount(account));
99
164
  }
100
165
  async function ensureGitignore(storagePath) {
101
- if (!currentStoragePath)
166
+ const state = getStoragePathState();
167
+ if (!state.currentStoragePath)
102
168
  return;
103
169
  const configDir = dirname(storagePath);
104
170
  const inferredProjectRoot = dirname(configDir);
105
- const candidateRoots = [currentProjectRoot, inferredProjectRoot].filter((root) => typeof root === "string" && root.length > 0);
171
+ const candidateRoots = [state.currentProjectRoot, inferredProjectRoot].filter((root) => typeof root === "string" && root.length > 0);
106
172
  const projectRoot = candidateRoots.find((root) => existsSync(join(root, ".git")));
107
173
  if (!projectRoot)
108
174
  return;
@@ -127,10 +193,20 @@ async function ensureGitignore(storagePath) {
127
193
  log.warn("Failed to update .gitignore", { error: String(error) });
128
194
  }
129
195
  }
130
- let currentStoragePath = null;
131
- let currentLegacyProjectStoragePath = null;
132
- let currentLegacyWorktreeStoragePath = null;
133
- let currentProjectRoot = null;
196
+ let currentStorageState = {
197
+ currentStoragePath: null,
198
+ currentLegacyProjectStoragePath: null,
199
+ currentLegacyWorktreeStoragePath: null,
200
+ currentProjectRoot: null,
201
+ };
202
+ const storagePathStateContext = new AsyncLocalStorage();
203
+ function getStoragePathState() {
204
+ return storagePathStateContext.getStore() ?? currentStorageState;
205
+ }
206
+ function setStoragePathState(state) {
207
+ currentStorageState = state;
208
+ storagePathStateContext.enterWith(state);
209
+ }
134
210
  export function setStorageBackupEnabled(enabled) {
135
211
  storageBackupEnabled = enabled;
136
212
  }
@@ -536,50 +612,115 @@ export function getLastAccountsSaveTimestamp() {
536
612
  }
537
613
  export function setStoragePath(projectPath) {
538
614
  if (!projectPath) {
539
- currentStoragePath = null;
540
- currentLegacyProjectStoragePath = null;
541
- currentLegacyWorktreeStoragePath = null;
542
- currentProjectRoot = null;
615
+ setStoragePathState({
616
+ currentStoragePath: null,
617
+ currentLegacyProjectStoragePath: null,
618
+ currentLegacyWorktreeStoragePath: null,
619
+ currentProjectRoot: null,
620
+ });
543
621
  return;
544
622
  }
545
623
  const projectRoot = findProjectRoot(projectPath);
546
624
  if (projectRoot) {
547
- currentProjectRoot = projectRoot;
548
625
  const identityRoot = resolveProjectStorageIdentityRoot(projectRoot);
549
- currentStoragePath = join(getProjectGlobalConfigDir(identityRoot), ACCOUNTS_FILE_NAME);
550
- currentLegacyProjectStoragePath = join(getProjectConfigDir(projectRoot), ACCOUNTS_FILE_NAME);
626
+ const currentStoragePath = join(getProjectGlobalConfigDir(identityRoot), ACCOUNTS_FILE_NAME);
627
+ const currentLegacyProjectStoragePath = join(getProjectConfigDir(projectRoot), ACCOUNTS_FILE_NAME);
551
628
  const previousWorktreeScopedPath = join(getProjectGlobalConfigDir(projectRoot), ACCOUNTS_FILE_NAME);
552
- currentLegacyWorktreeStoragePath =
553
- previousWorktreeScopedPath !== currentStoragePath
554
- ? previousWorktreeScopedPath
555
- : null;
629
+ const currentLegacyWorktreeStoragePath = previousWorktreeScopedPath !== currentStoragePath
630
+ ? previousWorktreeScopedPath
631
+ : null;
632
+ setStoragePathState({
633
+ currentStoragePath,
634
+ currentLegacyProjectStoragePath,
635
+ currentLegacyWorktreeStoragePath,
636
+ currentProjectRoot: projectRoot,
637
+ });
556
638
  }
557
639
  else {
558
- currentStoragePath = null;
559
- currentLegacyProjectStoragePath = null;
560
- currentLegacyWorktreeStoragePath = null;
561
- currentProjectRoot = null;
640
+ setStoragePathState({
641
+ currentStoragePath: null,
642
+ currentLegacyProjectStoragePath: null,
643
+ currentLegacyWorktreeStoragePath: null,
644
+ currentProjectRoot: null,
645
+ });
562
646
  }
563
647
  }
564
648
  export function setStoragePathDirect(path) {
565
- currentStoragePath = path;
566
- currentLegacyProjectStoragePath = null;
567
- currentLegacyWorktreeStoragePath = null;
568
- currentProjectRoot = null;
649
+ setStoragePathState({
650
+ currentStoragePath: path,
651
+ currentLegacyProjectStoragePath: null,
652
+ currentLegacyWorktreeStoragePath: null,
653
+ currentProjectRoot: null,
654
+ });
569
655
  }
570
656
  /**
571
657
  * Returns the file path for the account storage JSON file.
572
658
  * @returns Absolute path to the accounts.json file
573
659
  */
574
660
  export function getStoragePath() {
575
- if (currentStoragePath) {
576
- return currentStoragePath;
661
+ const state = getStoragePathState();
662
+ if (state.currentStoragePath) {
663
+ return state.currentStoragePath;
577
664
  }
578
665
  return join(getConfigDir(), ACCOUNTS_FILE_NAME);
579
666
  }
580
667
  export function buildNamedBackupPath(name) {
581
668
  return resolveNamedBackupPath(name, getStoragePath());
582
669
  }
670
+ export async function getNamedBackups() {
671
+ return collectNamedBackups(getStoragePath());
672
+ }
673
+ export async function restoreAccountsFromBackup(path, options) {
674
+ const backupRoot = getNamedBackupRoot(getStoragePath());
675
+ let resolvedBackupRoot;
676
+ try {
677
+ resolvedBackupRoot = await fs.realpath(backupRoot);
678
+ }
679
+ catch (error) {
680
+ const code = error.code;
681
+ if (code === "ENOENT") {
682
+ throw new Error(`Backup root does not exist: ${backupRoot}`);
683
+ }
684
+ throw error;
685
+ }
686
+ let resolvedBackupPath;
687
+ try {
688
+ resolvedBackupPath = await fs.realpath(path);
689
+ }
690
+ catch (error) {
691
+ const code = error.code;
692
+ if (code === "ENOENT") {
693
+ throw new Error(`Backup file no longer exists: ${path}`);
694
+ }
695
+ throw error;
696
+ }
697
+ const relativePath = relative(resolvedBackupRoot, resolvedBackupPath);
698
+ const isInsideBackupRoot = relativePath.length > 0 &&
699
+ !relativePath.startsWith("..") &&
700
+ !isAbsolute(relativePath);
701
+ if (!isInsideBackupRoot) {
702
+ throw new Error(`Backup path must stay inside ${resolvedBackupRoot}: ${path}`);
703
+ }
704
+ const { normalized } = await (async () => {
705
+ try {
706
+ return await loadAccountsFromPath(resolvedBackupPath);
707
+ }
708
+ catch (error) {
709
+ const code = error.code;
710
+ if (code === "ENOENT") {
711
+ throw new Error(`Backup file no longer exists: ${path}`);
712
+ }
713
+ throw error;
714
+ }
715
+ })();
716
+ if (!normalized || normalized.accounts.length === 0) {
717
+ throw new Error(`Backup does not contain any accounts: ${resolvedBackupPath}`);
718
+ }
719
+ if (options?.persist !== false) {
720
+ await saveAccounts(normalized);
721
+ }
722
+ return normalized;
723
+ }
583
724
  export async function exportNamedBackup(name, options) {
584
725
  return exportNamedBackupFile(name, {
585
726
  getStoragePath,
@@ -593,16 +734,17 @@ function getLegacyFlaggedAccountsPath() {
593
734
  return join(dirname(getStoragePath()), LEGACY_FLAGGED_ACCOUNTS_FILE_NAME);
594
735
  }
595
736
  async function migrateLegacyProjectStorageIfNeeded(persist = saveAccounts) {
596
- if (!currentStoragePath) {
737
+ const state = getStoragePathState();
738
+ if (!state.currentStoragePath) {
597
739
  return null;
598
740
  }
599
741
  const candidatePaths = [
600
- currentLegacyWorktreeStoragePath,
601
- currentLegacyProjectStoragePath,
742
+ state.currentLegacyWorktreeStoragePath,
743
+ state.currentLegacyProjectStoragePath,
602
744
  ]
603
745
  .filter((path) => typeof path === "string" &&
604
746
  path.length > 0 &&
605
- path !== currentStoragePath)
747
+ path !== state.currentStoragePath)
606
748
  .filter((path, index, all) => all.indexOf(path) === index);
607
749
  if (candidatePaths.length === 0) {
608
750
  return null;
@@ -611,7 +753,7 @@ async function migrateLegacyProjectStorageIfNeeded(persist = saveAccounts) {
611
753
  if (existingCandidatePaths.length === 0) {
612
754
  return null;
613
755
  }
614
- let targetStorage = await loadNormalizedStorageFromPath(currentStoragePath, "current account storage");
756
+ let targetStorage = await loadNormalizedStorageFromPath(state.currentStoragePath, "current account storage");
615
757
  let migrated = false;
616
758
  for (const legacyPath of existingCandidatePaths) {
617
759
  const legacyStorage = await loadNormalizedStorageFromPath(legacyPath, "legacy account storage");
@@ -629,7 +771,7 @@ async function migrateLegacyProjectStorageIfNeeded(persist = saveAccounts) {
629
771
  targetStorage = fallbackStorage;
630
772
  log.warn("Failed to persist migrated account storage", {
631
773
  from: legacyPath,
632
- to: currentStoragePath,
774
+ to: state.currentStoragePath,
633
775
  error: String(error),
634
776
  });
635
777
  continue;
@@ -651,14 +793,14 @@ async function migrateLegacyProjectStorageIfNeeded(persist = saveAccounts) {
651
793
  }
652
794
  log.info("Migrated legacy project account storage", {
653
795
  from: legacyPath,
654
- to: currentStoragePath,
796
+ to: state.currentStoragePath,
655
797
  accounts: mergedStorage.accounts.length,
656
798
  });
657
799
  }
658
800
  if (migrated) {
659
801
  return targetStorage;
660
802
  }
661
- if (targetStorage && !existsSync(currentStoragePath)) {
803
+ if (targetStorage && !existsSync(state.currentStoragePath)) {
662
804
  return targetStorage;
663
805
  }
664
806
  return null;
@@ -1437,8 +1579,10 @@ function cloneAccountStorageForPersistence(storage) {
1437
1579
  }
1438
1580
  export async function withAccountStorageTransaction(handler) {
1439
1581
  return withStorageLock(async () => {
1582
+ const storagePath = getStoragePath();
1440
1583
  const state = {
1441
1584
  snapshot: await loadAccountsInternal(saveAccountsUnlocked),
1585
+ storagePath,
1442
1586
  active: true,
1443
1587
  };
1444
1588
  const current = state.snapshot;
@@ -1451,8 +1595,10 @@ export async function withAccountStorageTransaction(handler) {
1451
1595
  }
1452
1596
  export async function withAccountAndFlaggedStorageTransaction(handler) {
1453
1597
  return withStorageLock(async () => {
1598
+ const storagePath = getStoragePath();
1454
1599
  const state = {
1455
1600
  snapshot: await loadAccountsInternal(saveAccountsUnlocked),
1601
+ storagePath,
1456
1602
  active: true,
1457
1603
  };
1458
1604
  const current = state.snapshot;
@@ -1555,7 +1701,8 @@ function normalizeFlaggedStorage(data) {
1555
1701
  const isSwitchReason = (value) => value === "rate-limit" ||
1556
1702
  value === "initial" ||
1557
1703
  value === "rotation" ||
1558
- value === "best";
1704
+ value === "best" ||
1705
+ value === "restore";
1559
1706
  const isCooldownReason = (value) => value === "auth-failure" ||
1560
1707
  value === "network-error" ||
1561
1708
  value === "rate-limit";
@@ -1769,13 +1916,17 @@ export async function clearFlaggedAccounts() {
1769
1916
  */
1770
1917
  export async function exportAccounts(filePath, force = true, beforeCommit) {
1771
1918
  const resolvedPath = resolvePath(filePath);
1919
+ const currentStoragePath = getStoragePath();
1772
1920
  if (!force && existsSync(resolvedPath)) {
1773
1921
  throw new Error(`File already exists: ${resolvedPath}`);
1774
1922
  }
1775
1923
  const transactionState = transactionSnapshotContext.getStore();
1776
- const storage = transactionState?.active
1924
+ const storage = transactionState?.active &&
1925
+ transactionState.storagePath === currentStoragePath
1777
1926
  ? transactionState.snapshot
1778
- : await withAccountStorageTransaction((current) => Promise.resolve(current));
1927
+ : transactionState?.active
1928
+ ? await loadAccountsInternal(saveAccountsUnlocked)
1929
+ : await withAccountStorageTransaction((current) => Promise.resolve(current));
1779
1930
  if (!storage || storage.accounts.length === 0) {
1780
1931
  throw new Error("No accounts to export");
1781
1932
  }