codex-multi-auth 2.15.0 → 2.17.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 (272) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +22 -1
  3. package/config/README.md +2 -2
  4. package/config/codex-legacy.json +109 -184
  5. package/config/codex-modern.json +44 -114
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +10 -4
  8. package/dist/lib/account-policy.d.ts +4 -0
  9. package/dist/lib/account-policy.d.ts.map +1 -1
  10. package/dist/lib/account-policy.js +8 -0
  11. package/dist/lib/accounts/rate-limits.d.ts.map +1 -1
  12. package/dist/lib/accounts/rate-limits.js +2 -0
  13. package/dist/lib/accounts.d.ts +21 -3
  14. package/dist/lib/accounts.d.ts.map +1 -1
  15. package/dist/lib/accounts.js +229 -17
  16. package/dist/lib/api-route-store.d.ts +24 -0
  17. package/dist/lib/api-route-store.d.ts.map +1 -0
  18. package/dist/lib/api-route-store.js +145 -0
  19. package/dist/lib/auth/account-access.d.ts +115 -0
  20. package/dist/lib/auth/account-access.d.ts.map +1 -0
  21. package/dist/lib/auth/account-access.js +214 -0
  22. package/dist/lib/auth/token-utils.d.ts +54 -1
  23. package/dist/lib/auth/token-utils.d.ts.map +1 -1
  24. package/dist/lib/auth/token-utils.js +92 -7
  25. package/dist/lib/cli.d.ts +1 -1
  26. package/dist/lib/cli.d.ts.map +1 -1
  27. package/dist/lib/cli.js +4 -0
  28. package/dist/lib/codex-cli/state.d.ts +7 -0
  29. package/dist/lib/codex-cli/state.d.ts.map +1 -1
  30. package/dist/lib/codex-cli/state.js +18 -0
  31. package/dist/lib/codex-cli/writer.d.ts.map +1 -1
  32. package/dist/lib/codex-cli/writer.js +60 -8
  33. package/dist/lib/codex-manager/account-manager-commands.d.ts.map +1 -1
  34. package/dist/lib/codex-manager/account-manager-commands.js +1 -0
  35. package/dist/lib/codex-manager/account-pool-write.d.ts +3 -1
  36. package/dist/lib/codex-manager/account-pool-write.d.ts.map +1 -1
  37. package/dist/lib/codex-manager/account-pool-write.js +4 -0
  38. package/dist/lib/codex-manager/api-login-menu.d.ts +14 -0
  39. package/dist/lib/codex-manager/api-login-menu.d.ts.map +1 -0
  40. package/dist/lib/codex-manager/api-login-menu.js +224 -0
  41. package/dist/lib/codex-manager/commands/account.d.ts.map +1 -1
  42. package/dist/lib/codex-manager/commands/account.js +27 -0
  43. package/dist/lib/codex-manager/commands/best.d.ts.map +1 -1
  44. package/dist/lib/codex-manager/commands/best.js +6 -2
  45. package/dist/lib/codex-manager/commands/check.d.ts +7 -1
  46. package/dist/lib/codex-manager/commands/check.d.ts.map +1 -1
  47. package/dist/lib/codex-manager/commands/check.js +24 -3
  48. package/dist/lib/codex-manager/commands/forecast.d.ts.map +1 -1
  49. package/dist/lib/codex-manager/commands/forecast.js +5 -1
  50. package/dist/lib/codex-manager/commands/report.d.ts.map +1 -1
  51. package/dist/lib/codex-manager/commands/report.js +5 -1
  52. package/dist/lib/codex-manager/commands/resets.d.ts +2 -0
  53. package/dist/lib/codex-manager/commands/resets.d.ts.map +1 -0
  54. package/dist/lib/codex-manager/commands/resets.js +67 -0
  55. package/dist/lib/codex-manager/commands/rotation.d.ts +2 -2
  56. package/dist/lib/codex-manager/commands/rotation.d.ts.map +1 -1
  57. package/dist/lib/codex-manager/commands/rotation.js +63 -12
  58. package/dist/lib/codex-manager/commands/status.d.ts +12 -1
  59. package/dist/lib/codex-manager/commands/status.d.ts.map +1 -1
  60. package/dist/lib/codex-manager/commands/status.js +166 -22
  61. package/dist/lib/codex-manager/commands/uninstall.js +1 -1
  62. package/dist/lib/codex-manager/formatters/quota-formatters.d.ts.map +1 -1
  63. package/dist/lib/codex-manager/formatters/quota-formatters.js +3 -2
  64. package/dist/lib/codex-manager/health-check.d.ts +3 -0
  65. package/dist/lib/codex-manager/health-check.d.ts.map +1 -1
  66. package/dist/lib/codex-manager/health-check.js +56 -6
  67. package/dist/lib/codex-manager/help.d.ts.map +1 -1
  68. package/dist/lib/codex-manager/help.js +4 -2
  69. package/dist/lib/codex-manager/login-flow.d.ts.map +1 -1
  70. package/dist/lib/codex-manager/login-flow.js +69 -7
  71. package/dist/lib/codex-manager/login-menu-data.d.ts.map +1 -1
  72. package/dist/lib/codex-manager/login-menu-data.js +14 -5
  73. package/dist/lib/codex-manager/login-oauth.d.ts +3 -0
  74. package/dist/lib/codex-manager/login-oauth.d.ts.map +1 -1
  75. package/dist/lib/codex-manager/login-oauth.js +6 -1
  76. package/dist/lib/codex-manager/login-workspace-choice.d.ts +11 -0
  77. package/dist/lib/codex-manager/login-workspace-choice.d.ts.map +1 -0
  78. package/dist/lib/codex-manager/login-workspace-choice.js +37 -0
  79. package/dist/lib/codex-manager/persist-selected-account.d.ts.map +1 -1
  80. package/dist/lib/codex-manager/persist-selected-account.js +2 -1
  81. package/dist/lib/codex-manager/repair-commands.d.ts +13 -0
  82. package/dist/lib/codex-manager/repair-commands.d.ts.map +1 -1
  83. package/dist/lib/codex-manager/repair-commands.js +143 -8
  84. package/dist/lib/codex-manager.d.ts.map +1 -1
  85. package/dist/lib/codex-manager.js +25 -6
  86. package/dist/lib/concurrency.d.ts +3 -0
  87. package/dist/lib/concurrency.d.ts.map +1 -0
  88. package/dist/lib/concurrency.js +31 -0
  89. package/dist/lib/context-budget/model-context-windows.d.ts +1 -1
  90. package/dist/lib/context-budget/model-context-windows.d.ts.map +1 -1
  91. package/dist/lib/context-budget/model-context-windows.js +9 -10
  92. package/dist/lib/model-route-policy.d.ts +52 -0
  93. package/dist/lib/model-route-policy.d.ts.map +1 -0
  94. package/dist/lib/model-route-policy.js +159 -0
  95. package/dist/lib/policy/runtime-policy.d.ts +1 -0
  96. package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
  97. package/dist/lib/policy/runtime-policy.js +3 -0
  98. package/dist/lib/preemptive-quota-scheduler.d.ts +2 -0
  99. package/dist/lib/preemptive-quota-scheduler.d.ts.map +1 -1
  100. package/dist/lib/preemptive-quota-scheduler.js +4 -0
  101. package/dist/lib/prompts/codex.d.ts.map +1 -1
  102. package/dist/lib/prompts/codex.js +3 -1
  103. package/dist/lib/quota-probe.d.ts +5 -0
  104. package/dist/lib/quota-probe.d.ts.map +1 -1
  105. package/dist/lib/quota-probe.js +26 -2
  106. package/dist/lib/request/client-cancellation.d.ts +5 -0
  107. package/dist/lib/request/client-cancellation.d.ts.map +1 -0
  108. package/dist/lib/request/client-cancellation.js +7 -0
  109. package/dist/lib/request/error-classification.d.ts.map +1 -1
  110. package/dist/lib/request/error-classification.js +39 -25
  111. package/dist/lib/request/fetch-helpers.js +1 -1
  112. package/dist/lib/request/helpers/model-map.d.ts +12 -2
  113. package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
  114. package/dist/lib/request/helpers/model-map.js +174 -147
  115. package/dist/lib/request/response-outcome.d.ts +25 -0
  116. package/dist/lib/request/response-outcome.d.ts.map +1 -0
  117. package/dist/lib/request/response-outcome.js +49 -0
  118. package/dist/lib/request/stream-failover-runtime.d.ts +2 -1
  119. package/dist/lib/request/stream-failover-runtime.d.ts.map +1 -1
  120. package/dist/lib/request/stream-failover-runtime.js +29 -10
  121. package/dist/lib/runtime/account-check.d.ts.map +1 -1
  122. package/dist/lib/runtime/account-check.js +2 -8
  123. package/dist/lib/runtime/account-model-catalog.d.ts +64 -0
  124. package/dist/lib/runtime/account-model-catalog.d.ts.map +1 -0
  125. package/dist/lib/runtime/account-model-catalog.js +154 -0
  126. package/dist/lib/runtime/account-pool.d.ts +3 -1
  127. package/dist/lib/runtime/account-pool.d.ts.map +1 -1
  128. package/dist/lib/runtime/account-pool.js +4 -0
  129. package/dist/lib/runtime/account-reset-credits.d.ts +42 -0
  130. package/dist/lib/runtime/account-reset-credits.d.ts.map +1 -0
  131. package/dist/lib/runtime/account-reset-credits.js +82 -0
  132. package/dist/lib/runtime/api-model-capabilities.d.ts +30 -0
  133. package/dist/lib/runtime/api-model-capabilities.d.ts.map +1 -0
  134. package/dist/lib/runtime/api-model-capabilities.js +434 -0
  135. package/dist/lib/runtime/api-model-runtime.d.ts +42 -0
  136. package/dist/lib/runtime/api-model-runtime.d.ts.map +1 -0
  137. package/dist/lib/runtime/api-model-runtime.js +358 -0
  138. package/dist/lib/runtime/app-bind.d.ts +19 -1
  139. package/dist/lib/runtime/app-bind.d.ts.map +1 -1
  140. package/dist/lib/runtime/app-bind.js +79 -9
  141. package/dist/lib/runtime/automatic-account-checks.d.ts +31 -0
  142. package/dist/lib/runtime/automatic-account-checks.d.ts.map +1 -0
  143. package/dist/lib/runtime/automatic-account-checks.js +113 -0
  144. package/dist/lib/runtime/automatic-subscription-checks.d.ts +7 -0
  145. package/dist/lib/runtime/automatic-subscription-checks.d.ts.map +1 -0
  146. package/dist/lib/runtime/automatic-subscription-checks.js +57 -0
  147. package/dist/lib/runtime/catalog-capabilities.d.ts +8 -0
  148. package/dist/lib/runtime/catalog-capabilities.d.ts.map +1 -0
  149. package/dist/lib/runtime/catalog-capabilities.js +61 -0
  150. package/dist/lib/runtime/inference-activity.d.ts +15 -0
  151. package/dist/lib/runtime/inference-activity.d.ts.map +1 -0
  152. package/dist/lib/runtime/inference-activity.js +95 -0
  153. package/dist/lib/runtime/model-discovery-status.d.ts +100 -0
  154. package/dist/lib/runtime/model-discovery-status.d.ts.map +1 -0
  155. package/dist/lib/runtime/model-discovery-status.js +387 -0
  156. package/dist/lib/runtime/native-account-storage.d.ts +11 -0
  157. package/dist/lib/runtime/native-account-storage.d.ts.map +1 -0
  158. package/dist/lib/runtime/native-account-storage.js +50 -0
  159. package/dist/lib/runtime/native-account-sync.d.ts +19 -0
  160. package/dist/lib/runtime/native-account-sync.d.ts.map +1 -0
  161. package/dist/lib/runtime/native-account-sync.js +62 -0
  162. package/dist/lib/runtime/native-binding-lock.d.ts +3 -0
  163. package/dist/lib/runtime/native-binding-lock.d.ts.map +1 -0
  164. package/dist/lib/runtime/native-binding-lock.js +5 -0
  165. package/dist/lib/runtime/native-client-auth.d.ts +3 -0
  166. package/dist/lib/runtime/native-client-auth.d.ts.map +1 -0
  167. package/dist/lib/runtime/native-client-auth.js +42 -0
  168. package/dist/lib/runtime/native-provider-config.d.ts +4 -0
  169. package/dist/lib/runtime/native-provider-config.d.ts.map +1 -0
  170. package/dist/lib/runtime/native-provider-config.js +52 -0
  171. package/dist/lib/runtime/native-rate-limits.d.ts +18 -0
  172. package/dist/lib/runtime/native-rate-limits.d.ts.map +1 -0
  173. package/dist/lib/runtime/native-rate-limits.js +135 -0
  174. package/dist/lib/runtime/reset-credit-routing.d.ts +41 -0
  175. package/dist/lib/runtime/reset-credit-routing.d.ts.map +1 -0
  176. package/dist/lib/runtime/reset-credit-routing.js +66 -0
  177. package/dist/lib/runtime/reset-credits.d.ts +101 -0
  178. package/dist/lib/runtime/reset-credits.d.ts.map +1 -0
  179. package/dist/lib/runtime/reset-credits.js +162 -0
  180. package/dist/lib/runtime/response-output-history.d.ts +17 -0
  181. package/dist/lib/runtime/response-output-history.d.ts.map +1 -0
  182. package/dist/lib/runtime/response-output-history.js +85 -0
  183. package/dist/lib/runtime/responses-websocket.d.ts +31 -0
  184. package/dist/lib/runtime/responses-websocket.d.ts.map +1 -0
  185. package/dist/lib/runtime/responses-websocket.js +636 -0
  186. package/dist/lib/runtime/resume-picker.d.ts +41 -0
  187. package/dist/lib/runtime/resume-picker.d.ts.map +1 -0
  188. package/dist/lib/runtime/resume-picker.js +169 -0
  189. package/dist/lib/runtime/rotation-account-selection.d.ts +6 -0
  190. package/dist/lib/runtime/rotation-account-selection.d.ts.map +1 -1
  191. package/dist/lib/runtime/rotation-account-selection.js +63 -0
  192. package/dist/lib/runtime/rotation-proxy-state.d.ts +33 -0
  193. package/dist/lib/runtime/rotation-proxy-state.d.ts.map +1 -1
  194. package/dist/lib/runtime/rotation-server-types.d.ts +18 -0
  195. package/dist/lib/runtime/rotation-server-types.d.ts.map +1 -1
  196. package/dist/lib/runtime/runtime-capability-failures.d.ts +14 -0
  197. package/dist/lib/runtime/runtime-capability-failures.d.ts.map +1 -0
  198. package/dist/lib/runtime/runtime-capability-failures.js +76 -0
  199. package/dist/lib/runtime/runtime-observability.d.ts +7 -0
  200. package/dist/lib/runtime/runtime-observability.d.ts.map +1 -1
  201. package/dist/lib/runtime/runtime-observability.js +42 -2
  202. package/dist/lib/runtime/subscription-first-use.d.ts +10 -0
  203. package/dist/lib/runtime/subscription-first-use.d.ts.map +1 -0
  204. package/dist/lib/runtime/subscription-first-use.js +64 -0
  205. package/dist/lib/runtime/subscription-quota-event.d.ts +6 -0
  206. package/dist/lib/runtime/subscription-quota-event.d.ts.map +1 -0
  207. package/dist/lib/runtime/subscription-quota-event.js +31 -0
  208. package/dist/lib/runtime/subscription-quota-order.d.ts +17 -0
  209. package/dist/lib/runtime/subscription-quota-order.d.ts.map +1 -0
  210. package/dist/lib/runtime/subscription-quota-order.js +31 -0
  211. package/dist/lib/runtime/workspace-model-scopes.d.ts +15 -0
  212. package/dist/lib/runtime/workspace-model-scopes.d.ts.map +1 -0
  213. package/dist/lib/runtime/workspace-model-scopes.js +34 -0
  214. package/dist/lib/runtime-constants.d.ts +3 -0
  215. package/dist/lib/runtime-constants.d.ts.map +1 -1
  216. package/dist/lib/runtime-constants.js +5 -0
  217. package/dist/lib/runtime-paths.d.ts.map +1 -1
  218. package/dist/lib/runtime-paths.js +4 -1
  219. package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
  220. package/dist/lib/runtime-rotation-proxy.js +855 -46
  221. package/dist/lib/schemas.d.ts +20 -0
  222. package/dist/lib/schemas.d.ts.map +1 -1
  223. package/dist/lib/schemas.js +3 -0
  224. package/dist/lib/storage/credential-sidecars.d.ts +13 -0
  225. package/dist/lib/storage/credential-sidecars.d.ts.map +1 -0
  226. package/dist/lib/storage/credential-sidecars.js +52 -0
  227. package/dist/lib/storage/file-lock.d.ts +9 -0
  228. package/dist/lib/storage/file-lock.d.ts.map +1 -0
  229. package/dist/lib/storage/file-lock.js +186 -0
  230. package/dist/lib/storage/flagged-storage.d.ts.map +1 -1
  231. package/dist/lib/storage/flagged-storage.js +12 -0
  232. package/dist/lib/storage/path-state.d.ts +2 -0
  233. package/dist/lib/storage/path-state.d.ts.map +1 -1
  234. package/dist/lib/storage/path-state.js +9 -0
  235. package/dist/lib/storage/paths.js +4 -4
  236. package/dist/lib/storage/pending-auth.d.ts +17 -0
  237. package/dist/lib/storage/pending-auth.d.ts.map +1 -0
  238. package/dist/lib/storage/pending-auth.js +153 -0
  239. package/dist/lib/storage/public-types.d.ts +13 -0
  240. package/dist/lib/storage/public-types.d.ts.map +1 -1
  241. package/dist/lib/storage/snapshot-merge.d.ts +16 -0
  242. package/dist/lib/storage/snapshot-merge.d.ts.map +1 -0
  243. package/dist/lib/storage/snapshot-merge.js +206 -0
  244. package/dist/lib/storage/transactions.d.ts.map +1 -1
  245. package/dist/lib/storage/transactions.js +5 -4
  246. package/dist/lib/storage.d.ts +6 -0
  247. package/dist/lib/storage.d.ts.map +1 -1
  248. package/dist/lib/storage.js +77 -7
  249. package/dist/lib/ui/auth-menu-builder.d.ts +2 -0
  250. package/dist/lib/ui/auth-menu-builder.d.ts.map +1 -1
  251. package/dist/lib/ui/auth-menu-builder.js +1 -0
  252. package/dist/lib/ui/auth-menu.d.ts.map +1 -1
  253. package/dist/lib/ui/auth-menu.js +1 -0
  254. package/dist/lib/ui/check-progress.d.ts +8 -0
  255. package/dist/lib/ui/check-progress.d.ts.map +1 -0
  256. package/dist/lib/ui/check-progress.js +30 -0
  257. package/dist/lib/ui/format.d.ts +3 -1
  258. package/dist/lib/ui/format.d.ts.map +1 -1
  259. package/dist/lib/ui/format.js +8 -0
  260. package/dist/lib/ui/ui-copy.d.ts +2 -2
  261. package/dist/lib/ui/ui-copy.js +2 -2
  262. package/dist/lib/usage/pricing.d.ts +19 -2
  263. package/dist/lib/usage/pricing.d.ts.map +1 -1
  264. package/dist/lib/usage/pricing.js +101 -40
  265. package/dist/lib/usage/usage-extraction.d.ts +3 -1
  266. package/dist/lib/usage/usage-extraction.d.ts.map +1 -1
  267. package/dist/lib/usage/usage-extraction.js +46 -9
  268. package/package.json +7 -4
  269. package/scripts/codex-app-router.js +7 -0
  270. package/scripts/codex-bin-resolver.js +40 -8
  271. package/scripts/codex-routing.js +1 -0
  272. package/scripts/codex.js +209 -126
@@ -1,7 +1,9 @@
1
+ import { mergeAccountCooldown, mergeAccountSnapshot, mergeAccountRuntimeObservations } from "./storage/snapshot-merge.js";
1
2
  import { createHash } from "node:crypto";
3
+ import { existsSync } from "node:fs";
2
4
  import { saveAccountsWithRetry } from "./storage/save-retry.js";
3
5
  import { createLogger } from "./logger.js";
4
- import { getStoragePath, loadAccounts, readPinAndGenFromDisk, saveAccounts, findMatchingAccountIndex, withAccountStorageTransaction, } from "./storage.js";
6
+ import { getStoragePath, loadAccounts, readPinAndGenFromDisk, saveAccounts, findMatchingAccountIndex, withAccountStorageTransaction, ACCOUNT_STORAGE_UNREADABLE, recordPendingAuth, } from "./storage.js";
5
7
  import { MODEL_FAMILIES } from "./request/helpers/model-map.js";
6
8
  import { getHealthTracker, getTokenTracker, resetTrackers, selectHybridAccount, } from "./rotation.js";
7
9
  import { withRoutingMutex, } from "./routing-mutex.js";
@@ -17,7 +19,7 @@ import { getStoragePathState, runWithStoragePathState, } from "./storage/path-st
17
19
  export { extractAccountId, extractAccountEmail, getAccountIdCandidates, selectBestAccountCandidate, resolveRuntimeRequestIdentity, shouldUpdateAccountIdFromToken, resolveRequestAccountId, sanitizeEmail, } from "./auth/token-utils.js";
18
20
  export { parseRateLimitReason, getQuotaKey, clampNonNegativeInt, clearExpiredRateLimits, clearAllRateLimits, isRateLimitedForQuotaKey, isRateLimitedForFamily, formatWaitTime, } from "./accounts/rate-limits.js";
19
21
  export { lookupCodexCliTokensByEmail, isCodexCliSyncEnabled, } from "./codex-cli/state.js";
20
- import { extractAccountId, extractAccountEmail, shouldUpdateAccountIdFromToken, sanitizeEmail, } from "./auth/token-utils.js";
22
+ import { codexCliAccountIdFor, extractAccountId, extractAccountEmail, shouldUpdateAccountIdFromToken, sanitizeEmail, } from "./auth/token-utils.js";
21
23
  import { clampNonNegativeInt, getQuotaKey, clearExpiredRateLimits, clearAllRateLimits, isRateLimitedForFamily, formatWaitTime, } from "./accounts/rate-limits.js";
22
24
  const log = createLogger("accounts");
23
25
  let nextRuntimeCircuitKeyId = 0;
@@ -37,7 +39,7 @@ function deriveAccountRecordId(account) {
37
39
  ].join("\u0000");
38
40
  return `record:${createHash("sha256").update(seed).digest("hex")}`;
39
41
  }
40
- function resolveAccountRecordId(account) {
42
+ export function resolveAccountRecordId(account) {
41
43
  const stored = account.recordId?.trim();
42
44
  return stored || deriveAccountRecordId(account);
43
45
  }
@@ -147,7 +149,11 @@ export const MAX_DEBOUNCED_SAVE_RETRIES = 5;
147
149
  /** Ceiling for the debounced-save retry backoff. */
148
150
  export const MAX_SAVE_RETRY_DELAY_MS = 8_000;
149
151
  export class AccountManager {
152
+ persistenceBaseline = null;
150
153
  accounts = [];
154
+ persistedWorkspaceSelections = new WeakMap();
155
+ persistedWorkspaces = new WeakMap();
156
+ hadPersistedStorage = false;
151
157
  cursorByFamily = initFamilyState(0);
152
158
  currentAccountIndexByFamily = initFamilyState(-1);
153
159
  lastToastAccountIndex = -1;
@@ -272,6 +278,7 @@ export class AccountManager {
272
278
  }
273
279
  constructor(authFallback, stored) {
274
280
  this.storagePathState = { ...getStoragePathState() };
281
+ this.hadPersistedStorage = existsSync(this.resolveSelectionStoragePath());
275
282
  const fallbackAccountId = extractAccountId(authFallback?.access)?.trim() || undefined;
276
283
  const fallbackAccountEmail = sanitizeEmail(extractAccountEmail(authFallback?.access));
277
284
  // Hydrate pin/gen from the stored snapshot. Validation (range, integer)
@@ -333,7 +340,7 @@ export class AccountManager {
333
340
  : account.refreshToken;
334
341
  return {
335
342
  index,
336
- recordId: resolveAccountRecordId({ ...account, refreshToken }),
343
+ recordId: resolveAccountRecordId(account),
337
344
  accountId: matchesFallback
338
345
  ? (fallbackAccountId ?? account.accountId)
339
346
  : account.accountId,
@@ -360,6 +367,7 @@ export class AccountManager {
360
367
  authInvalidationErrorCode: account.authInvalidationErrorCode,
361
368
  workspaces: account.workspaces,
362
369
  currentWorkspaceIndex: account.currentWorkspaceIndex,
370
+ codexCliMirror: account.codexCliMirror,
363
371
  };
364
372
  })
365
373
  .filter((account) => account !== null);
@@ -396,6 +404,8 @@ export class AccountManager {
396
404
  this.cursorByFamily[family] = nextIndex;
397
405
  }
398
406
  }
407
+ this.persistenceBaseline = stored ? structuredClone(stored) : null;
408
+ this.rememberWorkspaceSelections();
399
409
  return;
400
410
  }
401
411
  if (authFallback) {
@@ -427,6 +437,64 @@ export class AccountManager {
427
437
  this.cursorByFamily[family] = 0;
428
438
  }
429
439
  }
440
+ this.persistenceBaseline = stored ? structuredClone(stored) : null;
441
+ this.rememberWorkspaceSelections();
442
+ }
443
+ /** Reconcile external workspace policy by ID, preserving health changes not superseded on disk. */
444
+ syncWorkspaceSelections(current) {
445
+ if (!current)
446
+ return false;
447
+ let changed = false;
448
+ for (const account of this.accounts) {
449
+ const matches = current.accounts.filter(disk => disk.recordId && disk.recordId === account.recordId);
450
+ const key = getAccountIdentityKey(account);
451
+ const candidates = matches.length ? matches : current.accounts.filter(disk => key && getAccountIdentityKey(disk) === key);
452
+ if (candidates.length !== 1)
453
+ continue;
454
+ const disk = candidates[0];
455
+ if (!disk)
456
+ continue;
457
+ const localSelectedId = account.workspaces?.[account.currentWorkspaceIndex ?? 0]?.id;
458
+ const baseline = this.persistedWorkspaces.get(account);
459
+ if (JSON.stringify(disk.workspaces) !== JSON.stringify(baseline)) {
460
+ account.workspaces = disk.workspaces?.map(workspace => {
461
+ const previous = baseline?.find(item => item.id === workspace.id);
462
+ const local = account.workspaces?.find(item => item.id === workspace.id);
463
+ return previous && local && workspace.enabled === previous.enabled && workspace.disabledAt === previous.disabledAt
464
+ ? { ...workspace, enabled: local.enabled, disabledAt: local.disabledAt }
465
+ : { ...workspace };
466
+ });
467
+ this.persistedWorkspaces.set(account, structuredClone(disk.workspaces));
468
+ changed = true;
469
+ }
470
+ const selectedId = disk.workspaces?.[disk.currentWorkspaceIndex ?? 0]?.id;
471
+ const preferredId = selectedId !== this.persistedWorkspaceSelections.get(account) ? selectedId : localSelectedId;
472
+ let index = account.workspaces?.findIndex(workspace => workspace.id === preferredId) ?? -1;
473
+ if (index < 0)
474
+ index = account.workspaces?.findIndex(workspace => workspace.id === selectedId) ?? -1;
475
+ if (index < 0)
476
+ index = 0;
477
+ if (account.workspaces?.length && account.currentWorkspaceIndex !== index) {
478
+ account.currentWorkspaceIndex = index;
479
+ changed = true;
480
+ }
481
+ this.persistedWorkspaceSelections.set(account, selectedId);
482
+ }
483
+ return changed;
484
+ }
485
+ rememberWorkspaceSelections(snapshot) {
486
+ this.accounts.forEach(account => {
487
+ // recordId first; the identity key only when defined and unique, since
488
+ // duplicate identities (or two missing ones) would pick another row's baseline.
489
+ const rows = snapshot?.accounts ?? [];
490
+ const byRecord = account.recordId ? rows.filter(row => row.recordId === account.recordId) : [];
491
+ const key = getAccountIdentityKey(account);
492
+ const matches = byRecord.length ? byRecord : key ? rows.filter(row => getAccountIdentityKey(row) === key) : [];
493
+ const saved = (matches.length === 1 ? matches[0] : undefined) ?? account;
494
+ this.persistedWorkspaceSelections.set(account, saved.workspaces?.[saved.currentWorkspaceIndex ?? 0]?.id);
495
+ this.persistedWorkspaces.set(account, structuredClone(saved.workspaces));
496
+ });
497
+ this.hadPersistedStorage ||= existsSync(this.resolveSelectionStoragePath());
430
498
  }
431
499
  getAccountCount() {
432
500
  return this.accounts.length;
@@ -615,7 +683,7 @@ export class AccountManager {
615
683
  if (!account)
616
684
  return;
617
685
  await setCodexCliActiveSelection({
618
- accountId: account.accountId,
686
+ accountId: codexCliAccountIdFor(account, account.access),
619
687
  email: account.email,
620
688
  accessToken: account.access,
621
689
  refreshToken: account.refreshToken,
@@ -1281,6 +1349,7 @@ export class AccountManager {
1281
1349
  : undefined,
1282
1350
  workspaces: account.workspaces,
1283
1351
  currentWorkspaceIndex: account.currentWorkspaceIndex,
1352
+ codexCliMirror: account.codexCliMirror,
1284
1353
  };
1285
1354
  }),
1286
1355
  activeIndex,
@@ -1299,13 +1368,16 @@ export class AccountManager {
1299
1368
  const nextEmail = sanitizeEmail(extractAccountEmail(auth.access));
1300
1369
  try {
1301
1370
  return await withAccountStorageTransaction(async (_current, persist) => {
1371
+ if (!_current && this.hadPersistedStorage)
1372
+ throw Object.assign(new Error("Account storage was removed; reload before saving."), { code: "ESTALE" });
1373
+ this.syncWorkspaceSelections(_current);
1302
1374
  // Snapshot the live in-memory pool under the storage lock so refresh
1303
1375
  // persistence merges against the latest account state. Reconcile the
1304
1376
  // selection first for the same reason `saveToDisk` does: this write
1305
1377
  // carries `pinnedAccountIndex`/`affinityGeneration` too, so a stale
1306
1378
  // pair here would clobber a `switch` that landed since the last save.
1307
1379
  this.reconcileSelectionFromDisk();
1308
- const nextStorage = structuredClone(this.buildStorageSnapshot());
1380
+ const nextStorage = structuredClone(this.reconcileTokensFromDisk(this.buildStorageSnapshot(), _current));
1309
1381
  const storageIndex = findAccountIndexByIdentity(nextStorage.accounts, source, auth);
1310
1382
  if (storageIndex === undefined) {
1311
1383
  log.warn("Unable to resolve refreshed account for persistence", {
@@ -1354,7 +1426,8 @@ export class AccountManager {
1354
1426
  this.clearAccountCooldown(liveAccount);
1355
1427
  this.clearAuthFailures(liveAccount);
1356
1428
  try {
1357
- await persist(nextStorage);
1429
+ await this.persistSnapshot(_current, nextStorage, persist, storedAccount);
1430
+ this.rememberWorkspaceSelections(nextStorage);
1358
1431
  }
1359
1432
  catch (error) {
1360
1433
  liveAccount.access = previousLiveAccountState.access;
@@ -1399,7 +1472,8 @@ export class AccountManager {
1399
1472
  }
1400
1473
  return liveAccount;
1401
1474
  }
1402
- await persist(nextStorage);
1475
+ await this.persistSnapshot(_current, nextStorage, persist, storedAccount);
1476
+ this.rememberWorkspaceSelections(nextStorage);
1403
1477
  log.warn("Unable to resolve refreshed live account after persistence", {
1404
1478
  sourceIndex: source.index,
1405
1479
  });
@@ -1407,6 +1481,51 @@ export class AccountManager {
1407
1481
  });
1408
1482
  }
1409
1483
  catch (error) {
1484
+ // The refresh already rotated the token upstream; the old one is spent.
1485
+ // A locked or unreadable accounts file must not discard the only valid
1486
+ // credential, so keep it live and let the retrying debounced save
1487
+ // persist it once the file can be read.
1488
+ // A write that still fails after the storage retries (e.g. a Windows lock)
1489
+ // spends the old token just the same, so it is rescued like an unreadable file.
1490
+ const live = [ACCOUNT_STORAGE_UNREADABLE, "ELOCKED", "EACCES"].includes(error.code ?? "") || isRetryableAuthPersistenceError(error)
1491
+ ? this.getAccountByIdentity(source, auth)
1492
+ : null;
1493
+ if (live) {
1494
+ const priorRefreshToken = live.refreshToken;
1495
+ this.updateFromAuth(live, auth);
1496
+ live.enabled = true;
1497
+ if (live.cooldownReason === "auth-failure")
1498
+ this.clearAccountCooldown(live);
1499
+ this.clearAuthFailures(live);
1500
+ // The debounced save needs this process to survive and the file to
1501
+ // unlock. Journal the rotated credential beside the pool so the next
1502
+ // load (any process) applies it even if neither happens.
1503
+ try {
1504
+ await runWithStoragePathState(this.storagePathState, () => recordPendingAuth(getStoragePath(), {
1505
+ priorRefreshToken,
1506
+ refreshToken: auth.refresh,
1507
+ accessToken: auth.access,
1508
+ expiresAt: auth.expires,
1509
+ at: nowMs(),
1510
+ }));
1511
+ // Loads now see the journaled token, so it is the persisted state
1512
+ // that later merges (and a further rotation) compare against.
1513
+ const baselineRow = this.persistenceBaseline?.accounts.find(row => row.refreshToken === priorRefreshToken);
1514
+ if (baselineRow)
1515
+ Object.assign(baselineRow, { refreshToken: auth.refresh, accessToken: auth.access, expiresAt: auth.expires });
1516
+ log.warn("Account storage could not be read or written; rotated credential journaled until it can be saved", {
1517
+ sourceIndex: source.index,
1518
+ });
1519
+ }
1520
+ catch (journalError) {
1521
+ log.error("Account storage could not be read or written and the rotated credential could not be journaled; it is only in memory. Keep this process running until the accounts file is readable, or the account will need a re-login.", {
1522
+ sourceIndex: source.index,
1523
+ code: typeof journalError.code === "string" && /^[A-Z_]{1,40}$/.test(journalError.code ?? "") ? journalError.code : "INVALID_PENDING_AUTH",
1524
+ });
1525
+ }
1526
+ this.saveToDiskDebounced();
1527
+ return live;
1528
+ }
1410
1529
  throw new CodexAuthError(ERROR_MESSAGES.TOKEN_REFRESH_FAILED, {
1411
1530
  retryable: isRetryableAuthPersistenceError(error),
1412
1531
  cause: error,
@@ -1671,14 +1790,93 @@ export class AccountManager {
1671
1790
  }
1672
1791
  return account;
1673
1792
  }
1674
- async saveToDisk() {
1793
+ async persistSnapshot(current, proposed, persist, refreshed, runtimeOnConflict = false) {
1794
+ // Preserve initial/missing-store creation. An intentionally cleared store has
1795
+ // distinct metadata and must still win over this manager's stale inventory.
1796
+ let merged;
1797
+ let rescuedBaseline;
1798
+ try {
1799
+ merged = current ? mergeAccountSnapshot(this.persistenceBaseline, current, proposed) : proposed;
1800
+ }
1801
+ catch (error) {
1802
+ if (runtimeOnConflict && error.code === "ESTALE" && current && this.persistenceBaseline) {
1803
+ await persist(mergeAccountRuntimeObservations(this.persistenceBaseline, current, proposed));
1804
+ log.warn("Saved runtime observations; conflicting account edits remain pending and require reload or reconciliation.");
1805
+ return;
1806
+ }
1807
+ if (error.code !== "ESTALE" || !refreshed?.recordId || !current)
1808
+ throw error;
1809
+ // A rotated credential must not be discarded because an unrelated user edit
1810
+ // conflicts. Persist only its auth delta under this same transaction lock.
1811
+ const matches = (row) => resolveAccountRecordId(row) === refreshed.recordId;
1812
+ const prior = this.persistenceBaseline?.accounts.find(matches);
1813
+ const index = current.accounts.findIndex(matches);
1814
+ const disk = current.accounts[index];
1815
+ if (!prior || !disk || disk.refreshToken !== prior.refreshToken || disk.enabled === false || disk.authInvalidatedAt)
1816
+ throw error;
1817
+ merged = structuredClone(current);
1818
+ const row = merged.accounts[index];
1819
+ if (!row || !this.persistenceBaseline)
1820
+ throw error;
1821
+ rescuedBaseline = structuredClone(this.persistenceBaseline);
1822
+ const baselineRow = rescuedBaseline.accounts.find(matches);
1823
+ if (!baselineRow)
1824
+ throw error;
1825
+ const cooldown = mergeAccountCooldown(prior, disk, refreshed);
1826
+ // Persist the stable identity before rotating a legacy row's identity material.
1827
+ row.recordId = refreshed.recordId;
1828
+ baselineRow.recordId = refreshed.recordId;
1829
+ // Only authentication was committed. Pending additions, removals and user
1830
+ // edits must still differ from their old baseline on the next save. The
1831
+ // refresh can also move the identity and clears auth blockers; a restart
1832
+ // must not bring the old identity or cooldown back.
1833
+ for (const field of ["accessToken", "refreshToken", "expiresAt", "accountId", "accountIdSource", "email", "authInvalidatedAt", "authInvalidationErrorCode", "coolingDownUntil", "cooldownReason"]) {
1834
+ const value = refreshed[field];
1835
+ if (value === undefined) {
1836
+ delete row[field];
1837
+ delete baselineRow[field];
1838
+ }
1839
+ else {
1840
+ Object.assign(row, { [field]: value });
1841
+ Object.assign(baselineRow, { [field]: value });
1842
+ }
1843
+ }
1844
+ Object.assign(row, cooldown);
1845
+ }
1846
+ await persist(merged);
1847
+ if (refreshed) {
1848
+ const saved = merged.accounts.find(row => row.recordId === refreshed.recordId);
1849
+ const live = this.accounts.find(row => row.recordId === refreshed.recordId);
1850
+ // A successful refresh must retain blockers learned by another request,
1851
+ // including an observation made while the storage write was pending.
1852
+ if (saved && live)
1853
+ Object.assign(live, mergeAccountCooldown(refreshed, saved, live));
1854
+ }
1855
+ // Keep the baseline in this manager's inventory/intent space. Adopting added
1856
+ // disk records here would misread their absence in the next save as deletion.
1857
+ this.persistenceBaseline = rescuedBaseline ?? structuredClone(proposed);
1858
+ if (refreshed) {
1859
+ const saved = merged.accounts.find(row => row.recordId === refreshed.recordId);
1860
+ const baseline = this.persistenceBaseline.accounts.find(row => row.recordId === refreshed.recordId);
1861
+ // The adopted blocker is now known persisted state, so a later explicit
1862
+ // clear must compare against it rather than the pre-refresh clear.
1863
+ if (saved && baseline)
1864
+ Object.assign(baseline, { coolingDownUntil: saved.coolingDownUntil, cooldownReason: saved.cooldownReason });
1865
+ }
1866
+ }
1867
+ async saveToDisk(runtimeOnConflict = false) {
1675
1868
  await runWithStoragePathState(this.storagePathState, async () => {
1676
1869
  await withAccountStorageTransaction(async (current, persist) => {
1870
+ if (!current && this.hadPersistedStorage)
1871
+ throw Object.assign(new Error("Account storage was removed; reload before saving."), { code: "ESTALE" });
1677
1872
  // Reconcile against the disk state loaded under the storage lock so a
1678
1873
  // routine save does not clobber a token another process just rotated
1679
1874
  // (stress audit H3) or a pin the CLI just wrote (#474).
1680
1875
  this.reconcileSelectionFromDisk();
1681
- await persist(this.reconcileTokensFromDisk(this.buildStorageSnapshot(), current));
1876
+ this.syncWorkspaceSelections(current);
1877
+ const snapshot = this.reconcileTokensFromDisk(this.buildStorageSnapshot(), current);
1878
+ await this.persistSnapshot(current, snapshot, persist, undefined, runtimeOnConflict);
1879
+ this.rememberWorkspaceSelections(snapshot);
1682
1880
  });
1683
1881
  });
1684
1882
  }
@@ -1693,7 +1891,7 @@ export class AccountManager {
1693
1891
  if (this.pendingSave) {
1694
1892
  await this.pendingSave;
1695
1893
  }
1696
- this.pendingSave = this.saveToDisk().finally(() => {
1894
+ this.pendingSave = this.saveToDisk(true).finally(() => {
1697
1895
  this.pendingSave = null;
1698
1896
  });
1699
1897
  await this.pendingSave;
@@ -1735,7 +1933,7 @@ export class AccountManager {
1735
1933
  if (this.saveDebounceTimer) {
1736
1934
  clearTimeout(this.saveDebounceTimer);
1737
1935
  this.saveDebounceTimer = null;
1738
- await this.saveToDisk();
1936
+ await this.saveToDisk(true);
1739
1937
  }
1740
1938
  if (this.pendingSave) {
1741
1939
  await this.pendingSave;
@@ -1754,10 +1952,15 @@ export class AccountManager {
1754
1952
  }
1755
1953
  const resetIndex = account.workspaces.findIndex((workspace) => workspace.isDefault === true);
1756
1954
  for (const workspace of account.workspaces) {
1757
- workspace.enabled = true;
1758
- delete workspace.disabledAt;
1955
+ // Only undo runtime health disablement, never an explicit workspace exclusion.
1956
+ if (workspace.disabledAt !== undefined) {
1957
+ workspace.enabled = true;
1958
+ delete workspace.disabledAt;
1959
+ }
1960
+ }
1961
+ if (!account.workspaces[account.currentWorkspaceIndex ?? -1]) {
1962
+ account.currentWorkspaceIndex = resetIndex >= 0 ? resetIndex : 0;
1759
1963
  }
1760
- account.currentWorkspaceIndex = resetIndex >= 0 ? resetIndex : 0;
1761
1964
  }
1762
1965
  getCurrentWorkspace(account) {
1763
1966
  if (!account.workspaces || account.workspaces.length === 0) {
@@ -1766,6 +1969,15 @@ export class AccountManager {
1766
1969
  const idx = account.currentWorkspaceIndex ?? 0;
1767
1970
  return account.workspaces[idx] ?? null;
1768
1971
  }
1972
+ /** Disable a specific request workspace without changing the interactive selection. */
1973
+ disableWorkspace(account, workspaceId) {
1974
+ const workspace = account.workspaces?.find(item => item.id === workspaceId);
1975
+ if (!workspace || workspace.enabled === false)
1976
+ return false;
1977
+ workspace.enabled = false;
1978
+ workspace.disabledAt = nowMs();
1979
+ return true;
1980
+ }
1769
1981
  disableCurrentWorkspace(account, expectedWorkspaceId) {
1770
1982
  if (!account.workspaces || account.workspaces.length === 0) {
1771
1983
  return false;
@@ -1873,7 +2085,7 @@ export function formatAccountLabel(account, index) {
1873
2085
  * render these (e.g. only when more than one workspace exists) and supply the
1874
2086
  * leading indent.
1875
2087
  */
1876
- export function formatWorkspaceLines(account, indent = " ") {
2088
+ export function formatWorkspaceLines(account, indent = " ", selectionLabel = "active") {
1877
2089
  const workspaces = account?.workspaces;
1878
2090
  if (!workspaces || workspaces.length === 0)
1879
2091
  return [];
@@ -1885,7 +2097,7 @@ export function formatWorkspaceLines(account, indent = " ") {
1885
2097
  const idSuffix = id.length > 6 ? id.slice(-6) : id;
1886
2098
  const tags = [];
1887
2099
  if (isActive)
1888
- tags.push("active");
2100
+ tags.push(selectionLabel);
1889
2101
  if (workspace.enabled === false)
1890
2102
  tags.push("disabled");
1891
2103
  const tagLabel = tags.length > 0 ? ` (${tags.join(", ")})` : "";
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ declare const routeSchema: z.ZodObject<{
3
+ id: z.ZodString;
4
+ label: z.ZodString;
5
+ kind: z.ZodEnum<{
6
+ api: "api";
7
+ zdr: "zdr";
8
+ }>;
9
+ apiKey: z.ZodString;
10
+ enabled: z.ZodBoolean;
11
+ priority: z.ZodNumber;
12
+ visibleModels: z.ZodArray<z.ZodString>;
13
+ knownModels: z.ZodOptional<z.ZodArray<z.ZodString>>;
14
+ probeCapabilities: z.ZodOptional<z.ZodBoolean>;
15
+ accessPrograms: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
16
+ modelAccessPrograms: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>>;
17
+ }, z.core.$strip>;
18
+ export type ApiRouteCredential = z.infer<typeof routeSchema>;
19
+ export declare function getApiRoutesPath(): string;
20
+ export declare function loadApiRoutes(path?: string): Promise<ApiRouteCredential[]>;
21
+ export declare function saveApiRoutes(routes: ApiRouteCredential[], path?: string, expected?: ApiRouteCredential[]): Promise<void>;
22
+ export declare function updateApiModelDiscovery(route: ApiRouteCredential, models: string[], path?: string): Promise<ApiRouteCredential>;
23
+ export {};
24
+ //# sourceMappingURL=api-route-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-route-store.d.ts","sourceRoot":"","sources":["../../lib/api-route-store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAcxB,QAAA,MAAM,WAAW;;;;;;;;;;;;;;;iBAsBf,CAAC;AAKH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAC7D,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAWD,wBAAsB,aAAa,CAClC,IAAI,SAAqB,GACvB,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAwB/B;AACD,wBAAsB,aAAa,CAClC,MAAM,EAAE,kBAAkB,EAAE,EAC5B,IAAI,SAAqB,EACzB,QAAQ,CAAC,EAAE,kBAAkB,EAAE,GAC7B,OAAO,CAAC,IAAI,CAAC,CAgCf;AAKD,wBAAgB,uBAAuB,CACtC,KAAK,EAAE,kBAAkB,EACzB,MAAM,EAAE,MAAM,EAAE,EAChB,IAAI,SAAqB,GACvB,OAAO,CAAC,kBAAkB,CAAC,CAS7B"}
@@ -0,0 +1,145 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { z } from "zod";
4
+ import { getCodexMultiAuthDir } from "./runtime-paths.js";
5
+ import { tempPathFor } from "./temp-path.js";
6
+ import { withRetry } from "./fs-retry.js";
7
+ import { withStorageLock } from "./storage/transactions.js";
8
+ import { withFileTransactionLock } from "./storage/file-lock.js";
9
+ const modelId = z
10
+ .string()
11
+ .min(1)
12
+ .max(200)
13
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
14
+ const programId = z.string().min(1).max(80).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
15
+ const accessPrograms = z.record(programId, z.array(programId).max(32))
16
+ .refine(value => Object.keys(value).length <= 16);
17
+ const routeSchema = z.object({
18
+ id: z
19
+ .string()
20
+ .min(1)
21
+ .max(80)
22
+ .regex(/^[A-Za-z0-9_-]+$/),
23
+ label: z
24
+ .string()
25
+ .min(1)
26
+ .max(80)
27
+ .regex(/^[^\x00-\x1f\x7f]+$/),
28
+ kind: z.enum(["api", "zdr"]),
29
+ apiKey: z.string().min(1).max(1024).regex(/^\S+$/),
30
+ enabled: z.boolean(),
31
+ priority: z.number().int().min(0).max(9),
32
+ visibleModels: z.array(modelId).max(1000),
33
+ knownModels: z.array(modelId).max(10000).optional(),
34
+ probeCapabilities: z.boolean().optional(),
35
+ /** Administrator-confirmed credential entitlements absent from API model discovery. */
36
+ accessPrograms: accessPrograms.optional(),
37
+ modelAccessPrograms: z.record(modelId, accessPrograms)
38
+ .refine(value => Object.keys(value).length <= 1000).optional(),
39
+ });
40
+ const schema = z.object({
41
+ version: z.literal(1),
42
+ routes: z.array(routeSchema).max(100),
43
+ });
44
+ export function getApiRoutesPath() {
45
+ return join(getCodexMultiAuthDir(), "api-routes.json");
46
+ }
47
+ function validate(value) {
48
+ const parsed = schema.safeParse(value);
49
+ if (!parsed.success ||
50
+ new Set(parsed.data.routes.map((r) => r.id)).size !==
51
+ parsed.data.routes.length)
52
+ throw new Error("Invalid API route configuration");
53
+ return parsed.data.routes;
54
+ }
55
+ export async function loadApiRoutes(path = getApiRoutesPath()) {
56
+ let raw;
57
+ try {
58
+ raw = await withRetry(async () => {
59
+ const handle = await fs.open(path, "r");
60
+ try {
61
+ const buffer = Buffer.alloc(1024 * 1024 + 1);
62
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
63
+ if (bytesRead > 1024 * 1024)
64
+ throw new Error("Invalid API route configuration");
65
+ return buffer.subarray(0, bytesRead).toString("utf8");
66
+ }
67
+ finally {
68
+ await handle.close();
69
+ }
70
+ }, { maxAttempts: 6, backoffMs: 25 });
71
+ }
72
+ catch (error) {
73
+ if (error.code === "ENOENT")
74
+ return [];
75
+ throw new Error("Unable to read API route configuration");
76
+ }
77
+ try {
78
+ return validate(JSON.parse(raw));
79
+ }
80
+ catch {
81
+ throw new Error("Invalid API route configuration");
82
+ }
83
+ }
84
+ export async function saveApiRoutes(routes, path = getApiRoutesPath(), expected) {
85
+ const validated = validate({ version: 1, routes });
86
+ const serialized = JSON.stringify({ version: 1, routes: validated }) + "\n";
87
+ if (Buffer.byteLength(serialized) > 1024 * 1024)
88
+ throw new Error("API route configuration is too large");
89
+ await withStorageLock(() => withFileTransactionLock(path, async () => {
90
+ await fs.mkdir(dirname(path), { recursive: true, mode: 0o700 });
91
+ const temp = tempPathFor(path);
92
+ try {
93
+ if (expected &&
94
+ JSON.stringify(await loadApiRoutes(path)) !==
95
+ JSON.stringify(validate({ version: 1, routes: expected }))) {
96
+ throw new Error("API route configuration changed; reopen the menu before saving.");
97
+ }
98
+ await fs.writeFile(temp, serialized, { mode: 0o600, flag: "wx" });
99
+ await withRetry(() => fs.rename(temp, path), {
100
+ maxAttempts: 6,
101
+ backoffMs: 25,
102
+ });
103
+ }
104
+ finally {
105
+ await withRetry(() => fs.unlink(temp), {
106
+ maxAttempts: 6,
107
+ backoffMs: 25,
108
+ }).catch((error) => {
109
+ if (error.code !== "ENOENT")
110
+ throw error;
111
+ });
112
+ }
113
+ }));
114
+ }
115
+ /** Persist a discovery baseline and new GPT text IDs without undoing menu edits.
116
+ * Callers supply IDs already filtered for the Responses endpoint family. */
117
+ let discoveryUpdates = Promise.resolve();
118
+ export function updateApiModelDiscovery(route, models, path = getApiRoutesPath()) {
119
+ const result = discoveryUpdates.then(() => updateApiModelDiscoverySerial(route, models, path));
120
+ discoveryUpdates = result.then(() => undefined, () => undefined);
121
+ return result;
122
+ }
123
+ async function updateApiModelDiscoverySerial(route, models, path = getApiRoutesPath()) {
124
+ const routes = await loadApiRoutes(path);
125
+ const current = routes.find((r) => r.id === route.id && r.apiKey === route.apiKey && r.kind === route.kind);
126
+ if (!current)
127
+ return { ...route, enabled: false, visibleModels: [] };
128
+ if (!current.enabled)
129
+ return current;
130
+ const known = new Set(current.knownModels ?? models);
131
+ const visible = new Set(current.visibleModels);
132
+ for (const id of models) {
133
+ if (!known.has(id) && /^gpt-/i.test(id))
134
+ visible.add(id);
135
+ known.add(id);
136
+ }
137
+ const updated = {
138
+ ...current,
139
+ knownModels: [...known],
140
+ visibleModels: [...visible],
141
+ };
142
+ if (JSON.stringify(updated) !== JSON.stringify(current))
143
+ await saveApiRoutes(routes.map((r) => (r.id === current.id ? updated : r)), path, routes);
144
+ return updated;
145
+ }