oc-codex-multi-auth 6.19.0 → 6.20.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 (72) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +180 -79
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/accounts/persistence.d.ts +22 -0
  5. package/dist/lib/accounts/persistence.d.ts.map +1 -1
  6. package/dist/lib/accounts/persistence.js +79 -0
  7. package/dist/lib/accounts/persistence.js.map +1 -1
  8. package/dist/lib/accounts/rate-limits.d.ts +22 -0
  9. package/dist/lib/accounts/rate-limits.d.ts.map +1 -1
  10. package/dist/lib/accounts/rate-limits.js +25 -0
  11. package/dist/lib/accounts/rate-limits.js.map +1 -1
  12. package/dist/lib/accounts/rotation.d.ts +22 -23
  13. package/dist/lib/accounts/rotation.d.ts.map +1 -1
  14. package/dist/lib/accounts/rotation.js +62 -37
  15. package/dist/lib/accounts/rotation.js.map +1 -1
  16. package/dist/lib/accounts/stale-state.d.ts +31 -0
  17. package/dist/lib/accounts/stale-state.d.ts.map +1 -1
  18. package/dist/lib/accounts/stale-state.js +56 -1
  19. package/dist/lib/accounts/stale-state.js.map +1 -1
  20. package/dist/lib/accounts/state.d.ts +6 -0
  21. package/dist/lib/accounts/state.d.ts.map +1 -1
  22. package/dist/lib/accounts/state.js +15 -1
  23. package/dist/lib/accounts/state.js.map +1 -1
  24. package/dist/lib/auth/login-runner.d.ts +3 -0
  25. package/dist/lib/auth/login-runner.d.ts.map +1 -1
  26. package/dist/lib/auth/login-runner.js +35 -0
  27. package/dist/lib/auth/login-runner.js.map +1 -1
  28. package/dist/lib/codex-usage.d.ts +13 -5
  29. package/dist/lib/codex-usage.d.ts.map +1 -1
  30. package/dist/lib/codex-usage.js +33 -21
  31. package/dist/lib/codex-usage.js.map +1 -1
  32. package/dist/lib/parallel-probe.d.ts.map +1 -1
  33. package/dist/lib/parallel-probe.js +4 -2
  34. package/dist/lib/parallel-probe.js.map +1 -1
  35. package/dist/lib/request/fetch-helpers.d.ts +24 -0
  36. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  37. package/dist/lib/request/fetch-helpers.js +66 -20
  38. package/dist/lib/request/fetch-helpers.js.map +1 -1
  39. package/dist/lib/schemas.d.ts +9 -0
  40. package/dist/lib/schemas.d.ts.map +1 -1
  41. package/dist/lib/schemas.js +3 -0
  42. package/dist/lib/schemas.js.map +1 -1
  43. package/dist/lib/storage/flagged.d.ts.map +1 -1
  44. package/dist/lib/storage/flagged.js +5 -0
  45. package/dist/lib/storage/flagged.js.map +1 -1
  46. package/dist/lib/storage/migrations.d.ts +21 -0
  47. package/dist/lib/storage/migrations.d.ts.map +1 -1
  48. package/dist/lib/storage/migrations.js.map +1 -1
  49. package/dist/lib/storage/normalize.d.ts.map +1 -1
  50. package/dist/lib/storage/normalize.js +57 -1
  51. package/dist/lib/storage/normalize.js.map +1 -1
  52. package/dist/lib/tools/codex-doctor.d.ts.map +1 -1
  53. package/dist/lib/tools/codex-doctor.js +8 -66
  54. package/dist/lib/tools/codex-doctor.js.map +1 -1
  55. package/dist/lib/tools/codex-health.d.ts.map +1 -1
  56. package/dist/lib/tools/codex-health.js +7 -1
  57. package/dist/lib/tools/codex-health.js.map +1 -1
  58. package/dist/lib/tools/codex-list.d.ts.map +1 -1
  59. package/dist/lib/tools/codex-list.js +14 -1
  60. package/dist/lib/tools/codex-list.js.map +1 -1
  61. package/dist/lib/tools/codex-status.d.ts.map +1 -1
  62. package/dist/lib/tools/codex-status.js +6 -1
  63. package/dist/lib/tools/codex-status.js.map +1 -1
  64. package/dist/lib/tools/doctor-repair.d.ts +10 -0
  65. package/dist/lib/tools/doctor-repair.d.ts.map +1 -0
  66. package/dist/lib/tools/doctor-repair.js +86 -0
  67. package/dist/lib/tools/doctor-repair.js.map +1 -0
  68. package/dist/lib/tools/index.d.ts +3 -0
  69. package/dist/lib/tools/index.d.ts.map +1 -1
  70. package/dist/lib/tools/index.js.map +1 -1
  71. package/package.json +1 -1
  72. package/scripts/install-oc-codex-multi-auth-core.js +133 -6
@@ -257,8 +257,28 @@ async function readStandaloneStorage(path) {
257
257
  try {
258
258
  const raw = await readFile(path, "utf-8");
259
259
  const parsed = JSON.parse(raw);
260
+ // Shape validation, not just parse validation: a JSON array, scalar, or
261
+ // object without an `accounts` array is unreadable by the plugin runtime
262
+ // too (normalizeAccountStorage rejects it), so reporting it as a healthy
263
+ // empty pool (exit 0, "No accounts configured") hides the corruption
264
+ // from scripted callers that key on exit codes.
265
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
266
+ return { storage: null, error: "Storage file must be a JSON object with an accounts array." };
267
+ }
268
+ // Forward-compat mirror of the runtime guard: a newer schema version
269
+ // must not be shown as readable accounts by this build.
270
+ const version = parsed.version;
271
+ if (typeof version === "number" && Number.isFinite(version) && version > 3) {
272
+ return {
273
+ storage: null,
274
+ error: `Unsupported account storage schema version ${version}; this build supports up to version 3.`,
275
+ };
276
+ }
277
+ if (!Array.isArray(parsed.accounts)) {
278
+ return { storage: null, error: "Storage file must be a JSON object with an accounts array." };
279
+ }
260
280
  return {
261
- storage: parsed && typeof parsed === "object" ? normalizeStandaloneStorage(parsed) : null,
281
+ storage: normalizeStandaloneStorage(parsed),
262
282
  error: null,
263
283
  };
264
284
  } catch (error) {
@@ -417,6 +437,7 @@ function summarizeStandaloneAccounts(storage, includeSensitive, tag) {
417
437
  tags: Array.isArray(account?.accountTags) ? account.accountTags : [],
418
438
  note: account?.accountNote,
419
439
  rateLimitResetTimes: account?.rateLimitResetTimes ?? {},
440
+ quotaExhaustedUntil: account?.quotaExhaustedUntil,
420
441
  };
421
442
  });
422
443
  }
@@ -440,6 +461,8 @@ function printStandaloneResult(command, payload, json) {
440
461
  }
441
462
  }
442
463
  if (payload.error) console.log(`Error: ${payload.error}`);
464
+ for (const fix of payload.appliedFixes ?? []) console.log(`Fixed: ${fix}`);
465
+ for (const error of payload.fixErrors ?? []) console.log(`Repair failed: ${error}`);
443
466
  if (payload.nextAction) console.log(`Next: ${payload.nextAction}`);
444
467
  }
445
468
 
@@ -509,9 +532,29 @@ export async function runWarmCommand(parsed, options = {}) {
509
532
  // persisted to the SAME file the rest of the toolchain reads.
510
533
  storageMod.setStoragePathDirect(storagePath);
511
534
 
512
- const storage = await storageMod.loadAccounts();
535
+ let storage = null;
536
+ try {
537
+ storage = await storageMod.loadAccounts();
538
+ } catch (error) {
539
+ // Typed storage errors (e.g. UNSUPPORTED_SCHEMA_VERSION) carry the
540
+ // upgrade hint; surface them rather than crashing the CLI.
541
+ const hint = error && typeof error.hint === "string" ? ` ${error.hint}` : "";
542
+ const payload = { command: "warm", storagePath, error: `${formatErrorForLog(error)}${hint}` };
543
+ printWarmResult(payload, parsed.json);
544
+ return { exitCode: 1, action: "warm", storagePath };
545
+ }
513
546
  const accounts = Array.isArray(storage?.accounts) ? storage.accounts : [];
514
547
  if (accounts.length === 0) {
548
+ // `loadAccounts` swallows parse/IO errors and returns null. Probe the
549
+ // file so a corrupt storage fails like `status`/`doctor` do (exit 1)
550
+ // instead of reporting a healthy empty pool. ENOENT stays a silent
551
+ // empty pool: a missing file legitimately means no accounts yet.
552
+ const probe = await readStandaloneStorage(storagePath);
553
+ if (probe.error) {
554
+ const payload = { command: "warm", storagePath, error: probe.error };
555
+ printWarmResult(payload, parsed.json);
556
+ return { exitCode: 1, action: "warm", storagePath };
557
+ }
515
558
  const payload = {
516
559
  command: "warm",
517
560
  storagePath,
@@ -615,9 +658,27 @@ export async function runLimitsCommand(parsed, options = {}) {
615
658
  // persisted to the SAME file the rest of the toolchain reads.
616
659
  storageMod.setStoragePathDirect(storagePath);
617
660
 
618
- const storage = await storageMod.loadAccounts();
661
+ let storage = null;
662
+ try {
663
+ storage = await storageMod.loadAccounts();
664
+ } catch (error) {
665
+ // Typed storage errors (e.g. UNSUPPORTED_SCHEMA_VERSION) carry the
666
+ // upgrade hint; surface them rather than crashing the CLI.
667
+ const hint = error && typeof error.hint === "string" ? ` ${error.hint}` : "";
668
+ const payload = { command: "limits", storagePath, error: `${formatErrorForLog(error)}${hint}` };
669
+ printLimitsResult(payload, parsed.json);
670
+ return { exitCode: 1, action: "limits", storagePath };
671
+ }
619
672
  const accounts = Array.isArray(storage?.accounts) ? storage.accounts : [];
620
673
  if (accounts.length === 0) {
674
+ // Same probe contract as `warm`: a corrupt file exits 1 like
675
+ // `status`/`doctor`; a missing file stays a silent empty pool.
676
+ const probe = await readStandaloneStorage(storagePath);
677
+ if (probe.error) {
678
+ const payload = { command: "limits", storagePath, error: probe.error };
679
+ printLimitsResult(payload, parsed.json);
680
+ return { exitCode: 1, action: "limits", storagePath };
681
+ }
621
682
  const payload = {
622
683
  command: "limits",
623
684
  storagePath,
@@ -658,6 +719,7 @@ export async function runLimitsCommand(parsed, options = {}) {
658
719
  label: account.accountLabel ?? `Account ${index + 1}`,
659
720
  email: maskValue(account.email, parsed.includeSensitive),
660
721
  rateLimitResetTimes: account.rateLimitResetTimes ?? {},
722
+ quotaExhaustedUntil: account.quotaExhaustedUntil,
661
723
  };
662
724
  try {
663
725
  const { accessToken } = await usageMod.ensureCodexUsageAccessToken({ storage, account });
@@ -771,7 +833,68 @@ export async function runStandaloneCommand(command, argv = [], options = {}) {
771
833
  }
772
834
  const { env = process.env } = options;
773
835
  const storagePath = getStandaloneStoragePath(parsed, env);
774
- const { storage, error } = await readStandaloneStorage(storagePath);
836
+ const repairRequested = command === "doctor" && parsed.fix;
837
+ let storage = null;
838
+ let error = null;
839
+ if (parsed.configPath || !repairRequested) {
840
+ ({ storage, error } = await readStandaloneStorage(storagePath));
841
+ }
842
+ const appliedFixes = [];
843
+ const fixErrors = [];
844
+ if (repairRequested && !error) {
845
+ const previousKeychain = process.env.CODEX_KEYCHAIN;
846
+ try {
847
+ const loadDoctorRuntime = options.loadDoctorRuntime ?? (() => loadDistModules(
848
+ ["storage.js", "tools/doctor-repair.js", "shutdown.js"], "doctor",
849
+ ));
850
+ const [storageMod, repairMod, shutdownMod] = await loadDoctorRuntime();
851
+ // A CLI file selection must not read or replace the global keychain pool.
852
+ if (parsed.configPath) process.env.CODEX_KEYCHAIN = "0";
853
+ storageMod.setStoragePathDirect(storagePath);
854
+ shutdownMod.setShutdownOwnsProcess(true);
855
+ try {
856
+ storage = await storageMod.loadAccounts();
857
+ } catch (loadError) {
858
+ // Typed storage errors (UNSUPPORTED_SCHEMA_VERSION, unknown V2)
859
+ // carry exact in-tree copy plus an upgrade/recovery hint, and the
860
+ // load never got far enough to attempt a repair. Surface them on
861
+ // the error channel (exit 1) instead of the generic catch below,
862
+ // which is reserved for unknown throws so upstream failure text
863
+ // never reaches output unredacted.
864
+ if (loadError && typeof loadError.code === "string") {
865
+ const hint = typeof loadError.hint === "string" ? ` ${loadError.hint}` : "";
866
+ error = `${formatErrorForLog(loadError)}${hint}`;
867
+ } else {
868
+ throw loadError;
869
+ }
870
+ }
871
+ if (!storage && !error) {
872
+ // `loadAccounts` swallows JSON parse/IO errors and returns null. In
873
+ // default-path mode the pre-read above was skipped (keychain routing
874
+ // may own the pool), so probe the JSON file here: a corrupt file must
875
+ // surface as a parse error (exit 1) instead of "No accounts
876
+ // configured" (exit 0). ENOENT stays silent - a missing file with an
877
+ // empty keychain legitimately means no accounts yet. Skipped when a
878
+ // typed load error already set `error`; the probe would only
879
+ // overwrite the precise schema message with its own paraphrase.
880
+ const probe = await readStandaloneStorage(storagePath);
881
+ if (probe.error) error = probe.error;
882
+ }
883
+ if (!error) {
884
+ const repair = await repairMod.repairDoctorAccounts(storage?.accounts ?? []);
885
+ appliedFixes.push(...repair.appliedFixes);
886
+ fixErrors.push(...repair.fixErrors);
887
+ storage = (await storageMod.loadAccounts()) ?? storage;
888
+ }
889
+ } catch {
890
+ fixErrors.push("Doctor repair could not complete. Check the selected storage file and installed runtime.");
891
+ } finally {
892
+ if (parsed.configPath) {
893
+ if (previousKeychain === undefined) delete process.env.CODEX_KEYCHAIN;
894
+ else process.env.CODEX_KEYCHAIN = previousKeychain;
895
+ }
896
+ }
897
+ }
775
898
  const accounts = summarizeStandaloneAccounts(storage, parsed.includeSensitive, parsed.tag);
776
899
  const totalAccounts = Array.isArray(storage?.accounts) ? storage.accounts.length : 0;
777
900
  const payload = {
@@ -790,7 +913,11 @@ export async function runStandaloneCommand(command, argv = [], options = {}) {
790
913
  } else if (command === "doctor") {
791
914
  payload.message = error ? "Storage could not be parsed." : totalAccounts > 0 ? "Local diagnostics completed." : "No accounts configured.";
792
915
  payload.deep = parsed.deep;
793
- payload.fixApplied = parsed.fix ? false : undefined;
916
+ payload.fixApplied = parsed.fix ? appliedFixes.length > 0 : undefined;
917
+ if (parsed.fix) {
918
+ payload.appliedFixes = appliedFixes;
919
+ payload.fixErrors = fixErrors;
920
+ }
794
921
  payload.nextAction = totalAccounts > 0 ? "Run oc-codex-multi-auth health --json for scriptable checks." : "Run opencode auth login.";
795
922
  } else if (command === "health") {
796
923
  payload.healthyCount = accounts.filter((account) => account.enabled && account.hasRefreshToken).length;
@@ -799,7 +926,7 @@ export async function runStandaloneCommand(command, argv = [], options = {}) {
799
926
  payload.message = totalAccounts > 0 ? "Account storage loaded." : "No accounts configured.";
800
927
  }
801
928
  printStandaloneResult(command, payload, parsed.json);
802
- return { exitCode: error ? 1 : 0, action: command, storagePath };
929
+ return { exitCode: error || fixErrors.length > 0 ? 1 : 0, action: command, storagePath };
803
930
  }
804
931
 
805
932
  // Top-level keys inside `provider.openai` that the installer owns absolutely.