codex-multi-auth 2.16.0 → 2.17.2

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 (303) hide show
  1. package/.codex-plugin/plugin.json +9 -9
  2. package/LICENSE +21 -21
  3. package/README.md +487 -463
  4. package/assets/codex-multi-auth-icon.svg +39 -39
  5. package/assets/readme-hero.svg +31 -31
  6. package/config/README.md +31 -31
  7. package/config/codex-legacy.json +1221 -1221
  8. package/config/codex-modern.json +423 -423
  9. package/config/minimal-codex.json +15 -15
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +8 -3
  12. package/dist/lib/account-policy.d.ts +4 -0
  13. package/dist/lib/account-policy.d.ts.map +1 -1
  14. package/dist/lib/account-policy.js +8 -0
  15. package/dist/lib/accounts/rate-limits.d.ts.map +1 -1
  16. package/dist/lib/accounts/rate-limits.js +2 -0
  17. package/dist/lib/accounts.d.ts +15 -3
  18. package/dist/lib/accounts.d.ts.map +1 -1
  19. package/dist/lib/accounts.js +231 -30
  20. package/dist/lib/api-route-store.d.ts +24 -0
  21. package/dist/lib/api-route-store.d.ts.map +1 -0
  22. package/dist/lib/api-route-store.js +145 -0
  23. package/dist/lib/auth/account-access.d.ts +115 -0
  24. package/dist/lib/auth/account-access.d.ts.map +1 -0
  25. package/dist/lib/auth/account-access.js +214 -0
  26. package/dist/lib/auth/token-utils.d.ts +54 -1
  27. package/dist/lib/auth/token-utils.d.ts.map +1 -1
  28. package/dist/lib/auth/token-utils.js +92 -7
  29. package/dist/lib/cli.d.ts +1 -1
  30. package/dist/lib/cli.d.ts.map +1 -1
  31. package/dist/lib/cli.js +4 -0
  32. package/dist/lib/codex-cli/state.d.ts +7 -0
  33. package/dist/lib/codex-cli/state.d.ts.map +1 -1
  34. package/dist/lib/codex-cli/state.js +18 -0
  35. package/dist/lib/codex-cli/writer.d.ts.map +1 -1
  36. package/dist/lib/codex-cli/writer.js +60 -8
  37. package/dist/lib/codex-manager/account-manager-commands.d.ts.map +1 -1
  38. package/dist/lib/codex-manager/account-manager-commands.js +1 -0
  39. package/dist/lib/codex-manager/account-pool-write.d.ts +3 -1
  40. package/dist/lib/codex-manager/account-pool-write.d.ts.map +1 -1
  41. package/dist/lib/codex-manager/account-pool-write.js +4 -0
  42. package/dist/lib/codex-manager/api-login-menu.d.ts +14 -0
  43. package/dist/lib/codex-manager/api-login-menu.d.ts.map +1 -0
  44. package/dist/lib/codex-manager/api-login-menu.js +224 -0
  45. package/dist/lib/codex-manager/commands/account.d.ts.map +1 -1
  46. package/dist/lib/codex-manager/commands/account.js +27 -0
  47. package/dist/lib/codex-manager/commands/best.d.ts.map +1 -1
  48. package/dist/lib/codex-manager/commands/best.js +6 -2
  49. package/dist/lib/codex-manager/commands/check.d.ts +7 -1
  50. package/dist/lib/codex-manager/commands/check.d.ts.map +1 -1
  51. package/dist/lib/codex-manager/commands/check.js +24 -3
  52. package/dist/lib/codex-manager/commands/forecast.d.ts +1 -1
  53. package/dist/lib/codex-manager/commands/forecast.d.ts.map +1 -1
  54. package/dist/lib/codex-manager/commands/forecast.js +7 -3
  55. package/dist/lib/codex-manager/commands/report.d.ts.map +1 -1
  56. package/dist/lib/codex-manager/commands/report.js +5 -1
  57. package/dist/lib/codex-manager/commands/resets.d.ts +2 -0
  58. package/dist/lib/codex-manager/commands/resets.d.ts.map +1 -0
  59. package/dist/lib/codex-manager/commands/resets.js +67 -0
  60. package/dist/lib/codex-manager/commands/rotation.d.ts +2 -2
  61. package/dist/lib/codex-manager/commands/rotation.d.ts.map +1 -1
  62. package/dist/lib/codex-manager/commands/rotation.js +63 -12
  63. package/dist/lib/codex-manager/commands/status.d.ts +12 -1
  64. package/dist/lib/codex-manager/commands/status.d.ts.map +1 -1
  65. package/dist/lib/codex-manager/commands/status.js +166 -22
  66. package/dist/lib/codex-manager/commands/uninstall.js +1 -1
  67. package/dist/lib/codex-manager/formatters/quota-formatters.d.ts.map +1 -1
  68. package/dist/lib/codex-manager/formatters/quota-formatters.js +3 -2
  69. package/dist/lib/codex-manager/health-check.d.ts +3 -0
  70. package/dist/lib/codex-manager/health-check.d.ts.map +1 -1
  71. package/dist/lib/codex-manager/health-check.js +57 -7
  72. package/dist/lib/codex-manager/help.d.ts.map +1 -1
  73. package/dist/lib/codex-manager/help.js +4 -2
  74. package/dist/lib/codex-manager/login-flow.d.ts.map +1 -1
  75. package/dist/lib/codex-manager/login-flow.js +69 -7
  76. package/dist/lib/codex-manager/login-menu-data.d.ts.map +1 -1
  77. package/dist/lib/codex-manager/login-menu-data.js +21 -8
  78. package/dist/lib/codex-manager/login-oauth.d.ts +3 -0
  79. package/dist/lib/codex-manager/login-oauth.d.ts.map +1 -1
  80. package/dist/lib/codex-manager/login-oauth.js +6 -1
  81. package/dist/lib/codex-manager/login-workspace-choice.d.ts +11 -0
  82. package/dist/lib/codex-manager/login-workspace-choice.d.ts.map +1 -0
  83. package/dist/lib/codex-manager/login-workspace-choice.js +37 -0
  84. package/dist/lib/codex-manager/persist-selected-account.d.ts.map +1 -1
  85. package/dist/lib/codex-manager/persist-selected-account.js +2 -1
  86. package/dist/lib/codex-manager/quota-cache-helpers.d.ts +7 -1
  87. package/dist/lib/codex-manager/quota-cache-helpers.d.ts.map +1 -1
  88. package/dist/lib/codex-manager/quota-cache-helpers.js +34 -19
  89. package/dist/lib/codex-manager/repair-commands.d.ts +13 -0
  90. package/dist/lib/codex-manager/repair-commands.d.ts.map +1 -1
  91. package/dist/lib/codex-manager/repair-commands.js +144 -9
  92. package/dist/lib/codex-manager.d.ts.map +1 -1
  93. package/dist/lib/codex-manager.js +25 -6
  94. package/dist/lib/concurrency.d.ts +3 -0
  95. package/dist/lib/concurrency.d.ts.map +1 -0
  96. package/dist/lib/concurrency.js +31 -0
  97. package/dist/lib/context-budget-response.js +8 -8
  98. package/dist/lib/context-overflow.js +10 -10
  99. package/dist/lib/model-route-policy.d.ts +52 -0
  100. package/dist/lib/model-route-policy.d.ts.map +1 -0
  101. package/dist/lib/model-route-policy.js +159 -0
  102. package/dist/lib/oauth-success.html +125 -125
  103. package/dist/lib/policy/runtime-policy.d.ts +1 -0
  104. package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
  105. package/dist/lib/policy/runtime-policy.js +3 -0
  106. package/dist/lib/preemptive-quota-scheduler.d.ts +2 -0
  107. package/dist/lib/preemptive-quota-scheduler.d.ts.map +1 -1
  108. package/dist/lib/preemptive-quota-scheduler.js +4 -0
  109. package/dist/lib/prompts/codex-host-bridge.js +138 -138
  110. package/dist/lib/prompts/codex.js +79 -79
  111. package/dist/lib/quota-cache.d.ts +7 -3
  112. package/dist/lib/quota-cache.d.ts.map +1 -1
  113. package/dist/lib/quota-cache.js +91 -59
  114. package/dist/lib/quota-probe.d.ts +5 -0
  115. package/dist/lib/quota-probe.d.ts.map +1 -1
  116. package/dist/lib/quota-probe.js +26 -2
  117. package/dist/lib/quota-readiness.d.ts +7 -2
  118. package/dist/lib/quota-readiness.d.ts.map +1 -1
  119. package/dist/lib/quota-readiness.js +25 -4
  120. package/dist/lib/request/client-cancellation.d.ts +5 -0
  121. package/dist/lib/request/client-cancellation.d.ts.map +1 -0
  122. package/dist/lib/request/client-cancellation.js +7 -0
  123. package/dist/lib/request/response-outcome.d.ts +25 -0
  124. package/dist/lib/request/response-outcome.d.ts.map +1 -0
  125. package/dist/lib/request/response-outcome.js +49 -0
  126. package/dist/lib/request/stream-failover-runtime.d.ts +2 -1
  127. package/dist/lib/request/stream-failover-runtime.d.ts.map +1 -1
  128. package/dist/lib/request/stream-failover-runtime.js +29 -10
  129. package/dist/lib/runtime/account-check.d.ts.map +1 -1
  130. package/dist/lib/runtime/account-check.js +2 -8
  131. package/dist/lib/runtime/account-model-catalog.d.ts +64 -0
  132. package/dist/lib/runtime/account-model-catalog.d.ts.map +1 -0
  133. package/dist/lib/runtime/account-model-catalog.js +154 -0
  134. package/dist/lib/runtime/account-pool.d.ts +3 -1
  135. package/dist/lib/runtime/account-pool.d.ts.map +1 -1
  136. package/dist/lib/runtime/account-pool.js +4 -0
  137. package/dist/lib/runtime/account-reset-credits.d.ts +42 -0
  138. package/dist/lib/runtime/account-reset-credits.d.ts.map +1 -0
  139. package/dist/lib/runtime/account-reset-credits.js +82 -0
  140. package/dist/lib/runtime/api-model-capabilities.d.ts +30 -0
  141. package/dist/lib/runtime/api-model-capabilities.d.ts.map +1 -0
  142. package/dist/lib/runtime/api-model-capabilities.js +434 -0
  143. package/dist/lib/runtime/api-model-runtime.d.ts +42 -0
  144. package/dist/lib/runtime/api-model-runtime.d.ts.map +1 -0
  145. package/dist/lib/runtime/api-model-runtime.js +358 -0
  146. package/dist/lib/runtime/app-bind.d.ts +19 -1
  147. package/dist/lib/runtime/app-bind.d.ts.map +1 -1
  148. package/dist/lib/runtime/app-bind.js +89 -11
  149. package/dist/lib/runtime/automatic-account-checks.d.ts +35 -0
  150. package/dist/lib/runtime/automatic-account-checks.d.ts.map +1 -0
  151. package/dist/lib/runtime/automatic-account-checks.js +140 -0
  152. package/dist/lib/runtime/automatic-subscription-checks.d.ts +7 -0
  153. package/dist/lib/runtime/automatic-subscription-checks.d.ts.map +1 -0
  154. package/dist/lib/runtime/automatic-subscription-checks.js +54 -0
  155. package/dist/lib/runtime/catalog-capabilities.d.ts +8 -0
  156. package/dist/lib/runtime/catalog-capabilities.d.ts.map +1 -0
  157. package/dist/lib/runtime/catalog-capabilities.js +61 -0
  158. package/dist/lib/runtime/inference-activity.d.ts +15 -0
  159. package/dist/lib/runtime/inference-activity.d.ts.map +1 -0
  160. package/dist/lib/runtime/inference-activity.js +95 -0
  161. package/dist/lib/runtime/model-discovery-status.d.ts +100 -0
  162. package/dist/lib/runtime/model-discovery-status.d.ts.map +1 -0
  163. package/dist/lib/runtime/model-discovery-status.js +387 -0
  164. package/dist/lib/runtime/native-account-storage.d.ts +11 -0
  165. package/dist/lib/runtime/native-account-storage.d.ts.map +1 -0
  166. package/dist/lib/runtime/native-account-storage.js +50 -0
  167. package/dist/lib/runtime/native-account-sync.d.ts +19 -0
  168. package/dist/lib/runtime/native-account-sync.d.ts.map +1 -0
  169. package/dist/lib/runtime/native-account-sync.js +62 -0
  170. package/dist/lib/runtime/native-binding-lock.d.ts +3 -0
  171. package/dist/lib/runtime/native-binding-lock.d.ts.map +1 -0
  172. package/dist/lib/runtime/native-binding-lock.js +5 -0
  173. package/dist/lib/runtime/native-client-auth.d.ts +3 -0
  174. package/dist/lib/runtime/native-client-auth.d.ts.map +1 -0
  175. package/dist/lib/runtime/native-client-auth.js +42 -0
  176. package/dist/lib/runtime/native-provider-config.d.ts +4 -0
  177. package/dist/lib/runtime/native-provider-config.d.ts.map +1 -0
  178. package/dist/lib/runtime/native-provider-config.js +52 -0
  179. package/dist/lib/runtime/native-rate-limits.d.ts +18 -0
  180. package/dist/lib/runtime/native-rate-limits.d.ts.map +1 -0
  181. package/dist/lib/runtime/native-rate-limits.js +135 -0
  182. package/dist/lib/runtime/reset-credit-routing.d.ts +41 -0
  183. package/dist/lib/runtime/reset-credit-routing.d.ts.map +1 -0
  184. package/dist/lib/runtime/reset-credit-routing.js +66 -0
  185. package/dist/lib/runtime/reset-credits.d.ts +101 -0
  186. package/dist/lib/runtime/reset-credits.d.ts.map +1 -0
  187. package/dist/lib/runtime/reset-credits.js +162 -0
  188. package/dist/lib/runtime/response-output-history.d.ts +17 -0
  189. package/dist/lib/runtime/response-output-history.d.ts.map +1 -0
  190. package/dist/lib/runtime/response-output-history.js +85 -0
  191. package/dist/lib/runtime/responses-websocket.d.ts +31 -0
  192. package/dist/lib/runtime/responses-websocket.d.ts.map +1 -0
  193. package/dist/lib/runtime/responses-websocket.js +636 -0
  194. package/dist/lib/runtime/resume-picker.d.ts +41 -0
  195. package/dist/lib/runtime/resume-picker.d.ts.map +1 -0
  196. package/dist/lib/runtime/resume-picker.js +169 -0
  197. package/dist/lib/runtime/rotation-account-selection.d.ts +6 -0
  198. package/dist/lib/runtime/rotation-account-selection.d.ts.map +1 -1
  199. package/dist/lib/runtime/rotation-account-selection.js +63 -0
  200. package/dist/lib/runtime/rotation-proxy-state.d.ts +33 -0
  201. package/dist/lib/runtime/rotation-proxy-state.d.ts.map +1 -1
  202. package/dist/lib/runtime/rotation-server-types.d.ts +18 -0
  203. package/dist/lib/runtime/rotation-server-types.d.ts.map +1 -1
  204. package/dist/lib/runtime/runtime-capability-failures.d.ts +14 -0
  205. package/dist/lib/runtime/runtime-capability-failures.d.ts.map +1 -0
  206. package/dist/lib/runtime/runtime-capability-failures.js +76 -0
  207. package/dist/lib/runtime/runtime-observability.d.ts +7 -0
  208. package/dist/lib/runtime/runtime-observability.d.ts.map +1 -1
  209. package/dist/lib/runtime/runtime-observability.js +42 -2
  210. package/dist/lib/runtime/subscription-first-use.d.ts +10 -0
  211. package/dist/lib/runtime/subscription-first-use.d.ts.map +1 -0
  212. package/dist/lib/runtime/subscription-first-use.js +64 -0
  213. package/dist/lib/runtime/subscription-quota-event.d.ts +6 -0
  214. package/dist/lib/runtime/subscription-quota-event.d.ts.map +1 -0
  215. package/dist/lib/runtime/subscription-quota-event.js +31 -0
  216. package/dist/lib/runtime/subscription-quota-order.d.ts +17 -0
  217. package/dist/lib/runtime/subscription-quota-order.d.ts.map +1 -0
  218. package/dist/lib/runtime/subscription-quota-order.js +31 -0
  219. package/dist/lib/runtime/workspace-model-scopes.d.ts +15 -0
  220. package/dist/lib/runtime/workspace-model-scopes.d.ts.map +1 -0
  221. package/dist/lib/runtime/workspace-model-scopes.js +34 -0
  222. package/dist/lib/runtime-constants.d.ts +3 -0
  223. package/dist/lib/runtime-constants.d.ts.map +1 -1
  224. package/dist/lib/runtime-constants.js +5 -0
  225. package/dist/lib/runtime-paths.d.ts.map +1 -1
  226. package/dist/lib/runtime-paths.js +4 -1
  227. package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
  228. package/dist/lib/runtime-rotation-proxy.js +852 -42
  229. package/dist/lib/schemas.d.ts +20 -0
  230. package/dist/lib/schemas.d.ts.map +1 -1
  231. package/dist/lib/schemas.js +3 -0
  232. package/dist/lib/storage/credential-sidecars.d.ts +13 -0
  233. package/dist/lib/storage/credential-sidecars.d.ts.map +1 -0
  234. package/dist/lib/storage/credential-sidecars.js +52 -0
  235. package/dist/lib/storage/file-lock.d.ts +9 -0
  236. package/dist/lib/storage/file-lock.d.ts.map +1 -0
  237. package/dist/lib/storage/file-lock.js +186 -0
  238. package/dist/lib/storage/flagged-storage.d.ts.map +1 -1
  239. package/dist/lib/storage/flagged-storage.js +12 -0
  240. package/dist/lib/storage/path-state.d.ts +2 -0
  241. package/dist/lib/storage/path-state.d.ts.map +1 -1
  242. package/dist/lib/storage/path-state.js +9 -0
  243. package/dist/lib/storage/paths.js +4 -4
  244. package/dist/lib/storage/pending-auth.d.ts +18 -0
  245. package/dist/lib/storage/pending-auth.d.ts.map +1 -0
  246. package/dist/lib/storage/pending-auth.js +155 -0
  247. package/dist/lib/storage/public-types.d.ts +13 -0
  248. package/dist/lib/storage/public-types.d.ts.map +1 -1
  249. package/dist/lib/storage/record-identity.d.ts +16 -0
  250. package/dist/lib/storage/record-identity.d.ts.map +1 -0
  251. package/dist/lib/storage/record-identity.js +16 -0
  252. package/dist/lib/storage/snapshot-merge.d.ts +16 -0
  253. package/dist/lib/storage/snapshot-merge.d.ts.map +1 -0
  254. package/dist/lib/storage/snapshot-merge.js +206 -0
  255. package/dist/lib/storage/transactions.d.ts.map +1 -1
  256. package/dist/lib/storage/transactions.js +5 -4
  257. package/dist/lib/storage.d.ts +6 -0
  258. package/dist/lib/storage.d.ts.map +1 -1
  259. package/dist/lib/storage.js +77 -7
  260. package/dist/lib/ui/auth-menu-builder.d.ts +2 -0
  261. package/dist/lib/ui/auth-menu-builder.d.ts.map +1 -1
  262. package/dist/lib/ui/auth-menu-builder.js +1 -0
  263. package/dist/lib/ui/auth-menu.d.ts.map +1 -1
  264. package/dist/lib/ui/auth-menu.js +1 -0
  265. package/dist/lib/ui/check-progress.d.ts +8 -0
  266. package/dist/lib/ui/check-progress.d.ts.map +1 -0
  267. package/dist/lib/ui/check-progress.js +30 -0
  268. package/dist/lib/ui/format.d.ts +3 -1
  269. package/dist/lib/ui/format.d.ts.map +1 -1
  270. package/dist/lib/ui/format.js +8 -0
  271. package/dist/lib/ui/ui-copy.d.ts +2 -2
  272. package/dist/lib/ui/ui-copy.js +2 -2
  273. package/dist/lib/usage/pricing.js +2 -2
  274. package/dist/lib/usage/usage-extraction.d.ts +3 -1
  275. package/dist/lib/usage/usage-extraction.d.ts.map +1 -1
  276. package/dist/lib/usage/usage-extraction.js +46 -9
  277. package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
  278. package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
  279. package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
  280. package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
  281. package/node_modules/@codex-ai/plugin/package.json +9 -9
  282. package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
  283. package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
  284. package/node_modules/@codex-ai/sdk/package.json +8 -8
  285. package/package.json +200 -197
  286. package/scripts/codex-app-launcher.js +653 -653
  287. package/scripts/codex-app-router.js +373 -366
  288. package/scripts/codex-bin-resolver.js +313 -281
  289. package/scripts/codex-multi-auth.js +59 -59
  290. package/scripts/codex-routing.js +62 -61
  291. package/scripts/codex.js +6642 -6564
  292. package/scripts/install-codex-auth-utils.js +213 -213
  293. package/scripts/mcodex.js +367 -367
  294. package/scripts/postinstall.js +114 -114
  295. package/scripts/preuninstall.js +265 -265
  296. package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
  297. package/vendor/codex-ai-plugin/dist/index.js +2 -2
  298. package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
  299. package/vendor/codex-ai-plugin/dist/tool.js +29 -29
  300. package/vendor/codex-ai-plugin/package.json +9 -9
  301. package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
  302. package/vendor/codex-ai-sdk/dist/index.js +2 -2
  303. package/vendor/codex-ai-sdk/package.json +8 -8
@@ -1,7 +1,10 @@
1
- import { createHash } from "node:crypto";
1
+ import { mergeAccountCooldown, mergeAccountSnapshot, mergeAccountRuntimeObservations } from "./storage/snapshot-merge.js";
2
+ import { deriveAccountRecordId, resolveAccountRecordId } from "./storage/record-identity.js";
3
+ export { resolveAccountRecordId } from "./storage/record-identity.js";
4
+ import { existsSync } from "node:fs";
2
5
  import { saveAccountsWithRetry } from "./storage/save-retry.js";
3
6
  import { createLogger } from "./logger.js";
4
- import { getStoragePath, loadAccounts, readPinAndGenFromDisk, saveAccounts, findMatchingAccountIndex, withAccountStorageTransaction, } from "./storage.js";
7
+ import { getStoragePath, loadAccounts, readPinAndGenFromDisk, saveAccounts, findMatchingAccountIndex, withAccountStorageTransaction, ACCOUNT_STORAGE_UNREADABLE, recordPendingAuth, } from "./storage.js";
5
8
  import { MODEL_FAMILIES } from "./request/helpers/model-map.js";
6
9
  import { getHealthTracker, getTokenTracker, resetTrackers, selectHybridAccount, } from "./rotation.js";
7
10
  import { withRoutingMutex, } from "./routing-mutex.js";
@@ -17,7 +20,7 @@ import { getStoragePathState, runWithStoragePathState, } from "./storage/path-st
17
20
  export { extractAccountId, extractAccountEmail, getAccountIdCandidates, selectBestAccountCandidate, resolveRuntimeRequestIdentity, shouldUpdateAccountIdFromToken, resolveRequestAccountId, sanitizeEmail, } from "./auth/token-utils.js";
18
21
  export { parseRateLimitReason, getQuotaKey, clampNonNegativeInt, clearExpiredRateLimits, clearAllRateLimits, isRateLimitedForQuotaKey, isRateLimitedForFamily, formatWaitTime, } from "./accounts/rate-limits.js";
19
22
  export { lookupCodexCliTokensByEmail, isCodexCliSyncEnabled, } from "./codex-cli/state.js";
20
- import { extractAccountId, extractAccountEmail, shouldUpdateAccountIdFromToken, sanitizeEmail, } from "./auth/token-utils.js";
23
+ import { codexCliAccountIdFor, extractAccountId, extractAccountEmail, shouldUpdateAccountIdFromToken, sanitizeEmail, } from "./auth/token-utils.js";
21
24
  import { clampNonNegativeInt, getQuotaKey, clearExpiredRateLimits, clearAllRateLimits, isRateLimitedForFamily, formatWaitTime, } from "./accounts/rate-limits.js";
22
25
  const log = createLogger("accounts");
23
26
  let nextRuntimeCircuitKeyId = 0;
@@ -28,19 +31,6 @@ function getAccountCircuitKey(account) {
28
31
  }
29
32
  return account.circuitKeyId;
30
33
  }
31
- function deriveAccountRecordId(account) {
32
- const seed = [
33
- account.addedAt,
34
- account.accountId?.trim() ?? "",
35
- account.email?.trim().toLowerCase() ?? "",
36
- account.refreshToken.trim(),
37
- ].join("\u0000");
38
- return `record:${createHash("sha256").update(seed).digest("hex")}`;
39
- }
40
- function resolveAccountRecordId(account) {
41
- const stored = account.recordId?.trim();
42
- return stored || deriveAccountRecordId(account);
43
- }
44
34
  export function getRuntimeTrackerKey(account) {
45
35
  if (account._runtimeTrackerKey !== undefined) {
46
36
  return account._runtimeTrackerKey;
@@ -147,7 +137,11 @@ export const MAX_DEBOUNCED_SAVE_RETRIES = 5;
147
137
  /** Ceiling for the debounced-save retry backoff. */
148
138
  export const MAX_SAVE_RETRY_DELAY_MS = 8_000;
149
139
  export class AccountManager {
140
+ persistenceBaseline = null;
150
141
  accounts = [];
142
+ persistedWorkspaceSelections = new WeakMap();
143
+ persistedWorkspaces = new WeakMap();
144
+ hadPersistedStorage = false;
151
145
  cursorByFamily = initFamilyState(0);
152
146
  currentAccountIndexByFamily = initFamilyState(-1);
153
147
  lastToastAccountIndex = -1;
@@ -272,6 +266,7 @@ export class AccountManager {
272
266
  }
273
267
  constructor(authFallback, stored) {
274
268
  this.storagePathState = { ...getStoragePathState() };
269
+ this.hadPersistedStorage = existsSync(this.resolveSelectionStoragePath());
275
270
  const fallbackAccountId = extractAccountId(authFallback?.access)?.trim() || undefined;
276
271
  const fallbackAccountEmail = sanitizeEmail(extractAccountEmail(authFallback?.access));
277
272
  // Hydrate pin/gen from the stored snapshot. Validation (range, integer)
@@ -333,7 +328,7 @@ export class AccountManager {
333
328
  : account.refreshToken;
334
329
  return {
335
330
  index,
336
- recordId: resolveAccountRecordId({ ...account, refreshToken }),
331
+ recordId: resolveAccountRecordId(account),
337
332
  accountId: matchesFallback
338
333
  ? (fallbackAccountId ?? account.accountId)
339
334
  : account.accountId,
@@ -360,6 +355,7 @@ export class AccountManager {
360
355
  authInvalidationErrorCode: account.authInvalidationErrorCode,
361
356
  workspaces: account.workspaces,
362
357
  currentWorkspaceIndex: account.currentWorkspaceIndex,
358
+ codexCliMirror: account.codexCliMirror,
363
359
  };
364
360
  })
365
361
  .filter((account) => account !== null);
@@ -396,6 +392,8 @@ export class AccountManager {
396
392
  this.cursorByFamily[family] = nextIndex;
397
393
  }
398
394
  }
395
+ this.persistenceBaseline = stored ? structuredClone(stored) : null;
396
+ this.rememberWorkspaceSelections();
399
397
  return;
400
398
  }
401
399
  if (authFallback) {
@@ -427,6 +425,64 @@ export class AccountManager {
427
425
  this.cursorByFamily[family] = 0;
428
426
  }
429
427
  }
428
+ this.persistenceBaseline = stored ? structuredClone(stored) : null;
429
+ this.rememberWorkspaceSelections();
430
+ }
431
+ /** Reconcile external workspace policy by ID, preserving health changes not superseded on disk. */
432
+ syncWorkspaceSelections(current) {
433
+ if (!current)
434
+ return false;
435
+ let changed = false;
436
+ for (const account of this.accounts) {
437
+ const matches = current.accounts.filter(disk => disk.recordId && disk.recordId === account.recordId);
438
+ const key = getAccountIdentityKey(account);
439
+ const candidates = matches.length ? matches : current.accounts.filter(disk => key && getAccountIdentityKey(disk) === key);
440
+ if (candidates.length !== 1)
441
+ continue;
442
+ const disk = candidates[0];
443
+ if (!disk)
444
+ continue;
445
+ const localSelectedId = account.workspaces?.[account.currentWorkspaceIndex ?? 0]?.id;
446
+ const baseline = this.persistedWorkspaces.get(account);
447
+ if (JSON.stringify(disk.workspaces) !== JSON.stringify(baseline)) {
448
+ account.workspaces = disk.workspaces?.map(workspace => {
449
+ const previous = baseline?.find(item => item.id === workspace.id);
450
+ const local = account.workspaces?.find(item => item.id === workspace.id);
451
+ return previous && local && workspace.enabled === previous.enabled && workspace.disabledAt === previous.disabledAt
452
+ ? { ...workspace, enabled: local.enabled, disabledAt: local.disabledAt }
453
+ : { ...workspace };
454
+ });
455
+ this.persistedWorkspaces.set(account, structuredClone(disk.workspaces));
456
+ changed = true;
457
+ }
458
+ const selectedId = disk.workspaces?.[disk.currentWorkspaceIndex ?? 0]?.id;
459
+ const preferredId = selectedId !== this.persistedWorkspaceSelections.get(account) ? selectedId : localSelectedId;
460
+ let index = account.workspaces?.findIndex(workspace => workspace.id === preferredId) ?? -1;
461
+ if (index < 0)
462
+ index = account.workspaces?.findIndex(workspace => workspace.id === selectedId) ?? -1;
463
+ if (index < 0)
464
+ index = 0;
465
+ if (account.workspaces?.length && account.currentWorkspaceIndex !== index) {
466
+ account.currentWorkspaceIndex = index;
467
+ changed = true;
468
+ }
469
+ this.persistedWorkspaceSelections.set(account, selectedId);
470
+ }
471
+ return changed;
472
+ }
473
+ rememberWorkspaceSelections(snapshot) {
474
+ this.accounts.forEach(account => {
475
+ // recordId first; the identity key only when defined and unique, since
476
+ // duplicate identities (or two missing ones) would pick another row's baseline.
477
+ const rows = snapshot?.accounts ?? [];
478
+ const byRecord = account.recordId ? rows.filter(row => row.recordId === account.recordId) : [];
479
+ const key = getAccountIdentityKey(account);
480
+ const matches = byRecord.length ? byRecord : key ? rows.filter(row => getAccountIdentityKey(row) === key) : [];
481
+ const saved = (matches.length === 1 ? matches[0] : undefined) ?? account;
482
+ this.persistedWorkspaceSelections.set(account, saved.workspaces?.[saved.currentWorkspaceIndex ?? 0]?.id);
483
+ this.persistedWorkspaces.set(account, structuredClone(saved.workspaces));
484
+ });
485
+ this.hadPersistedStorage ||= existsSync(this.resolveSelectionStoragePath());
430
486
  }
431
487
  getAccountCount() {
432
488
  return this.accounts.length;
@@ -615,7 +671,7 @@ export class AccountManager {
615
671
  if (!account)
616
672
  return;
617
673
  await setCodexCliActiveSelection({
618
- accountId: account.accountId,
674
+ accountId: codexCliAccountIdFor(account, account.access),
619
675
  email: account.email,
620
676
  accessToken: account.access,
621
677
  refreshToken: account.refreshToken,
@@ -1281,6 +1337,7 @@ export class AccountManager {
1281
1337
  : undefined,
1282
1338
  workspaces: account.workspaces,
1283
1339
  currentWorkspaceIndex: account.currentWorkspaceIndex,
1340
+ codexCliMirror: account.codexCliMirror,
1284
1341
  };
1285
1342
  }),
1286
1343
  activeIndex,
@@ -1299,13 +1356,16 @@ export class AccountManager {
1299
1356
  const nextEmail = sanitizeEmail(extractAccountEmail(auth.access));
1300
1357
  try {
1301
1358
  return await withAccountStorageTransaction(async (_current, persist) => {
1359
+ if (!_current && this.hadPersistedStorage)
1360
+ throw Object.assign(new Error("Account storage was removed; reload before saving."), { code: "ESTALE" });
1361
+ this.syncWorkspaceSelections(_current);
1302
1362
  // Snapshot the live in-memory pool under the storage lock so refresh
1303
1363
  // persistence merges against the latest account state. Reconcile the
1304
1364
  // selection first for the same reason `saveToDisk` does: this write
1305
1365
  // carries `pinnedAccountIndex`/`affinityGeneration` too, so a stale
1306
1366
  // pair here would clobber a `switch` that landed since the last save.
1307
1367
  this.reconcileSelectionFromDisk();
1308
- const nextStorage = structuredClone(this.buildStorageSnapshot());
1368
+ const nextStorage = structuredClone(this.reconcileTokensFromDisk(this.buildStorageSnapshot(), _current));
1309
1369
  const storageIndex = findAccountIndexByIdentity(nextStorage.accounts, source, auth);
1310
1370
  if (storageIndex === undefined) {
1311
1371
  log.warn("Unable to resolve refreshed account for persistence", {
@@ -1354,7 +1414,8 @@ export class AccountManager {
1354
1414
  this.clearAccountCooldown(liveAccount);
1355
1415
  this.clearAuthFailures(liveAccount);
1356
1416
  try {
1357
- await persist(nextStorage);
1417
+ await this.persistSnapshot(_current, nextStorage, persist, storedAccount);
1418
+ this.rememberWorkspaceSelections(nextStorage);
1358
1419
  }
1359
1420
  catch (error) {
1360
1421
  liveAccount.access = previousLiveAccountState.access;
@@ -1399,7 +1460,8 @@ export class AccountManager {
1399
1460
  }
1400
1461
  return liveAccount;
1401
1462
  }
1402
- await persist(nextStorage);
1463
+ await this.persistSnapshot(_current, nextStorage, persist, storedAccount);
1464
+ this.rememberWorkspaceSelections(nextStorage);
1403
1465
  log.warn("Unable to resolve refreshed live account after persistence", {
1404
1466
  sourceIndex: source.index,
1405
1467
  });
@@ -1407,6 +1469,52 @@ export class AccountManager {
1407
1469
  });
1408
1470
  }
1409
1471
  catch (error) {
1472
+ // The refresh already rotated the token upstream; the old one is spent.
1473
+ // A locked or unreadable accounts file must not discard the only valid
1474
+ // credential, so keep it live and let the retrying debounced save
1475
+ // persist it once the file can be read.
1476
+ // A write that still fails after the storage retries (e.g. a Windows lock)
1477
+ // spends the old token just the same, so it is rescued like an unreadable file.
1478
+ const live = [ACCOUNT_STORAGE_UNREADABLE, "ELOCKED", "EACCES"].includes(error.code ?? "") || isRetryableAuthPersistenceError(error)
1479
+ ? this.getAccountByIdentity(source, auth)
1480
+ : null;
1481
+ if (live) {
1482
+ const priorRefreshToken = live.refreshToken;
1483
+ this.updateFromAuth(live, auth);
1484
+ live.enabled = true;
1485
+ if (live.cooldownReason === "auth-failure")
1486
+ this.clearAccountCooldown(live);
1487
+ this.clearAuthFailures(live);
1488
+ // The debounced save needs this process to survive and the file to
1489
+ // unlock. Journal the rotated credential beside the pool so the next
1490
+ // load (any process) applies it even if neither happens.
1491
+ try {
1492
+ await runWithStoragePathState(this.storagePathState, () => recordPendingAuth(getStoragePath(), {
1493
+ priorRefreshToken,
1494
+ recordId: live.recordId,
1495
+ refreshToken: auth.refresh,
1496
+ accessToken: auth.access,
1497
+ expiresAt: auth.expires,
1498
+ at: nowMs(),
1499
+ }));
1500
+ // Loads now see the journaled token, so it is the persisted state
1501
+ // that later merges (and a further rotation) compare against.
1502
+ const baselineRow = this.persistenceBaseline?.accounts.find(row => row.refreshToken === priorRefreshToken);
1503
+ if (baselineRow)
1504
+ Object.assign(baselineRow, { refreshToken: auth.refresh, accessToken: auth.access, expiresAt: auth.expires });
1505
+ log.warn("Account storage could not be read or written; rotated credential journaled until it can be saved", {
1506
+ sourceIndex: source.index,
1507
+ });
1508
+ }
1509
+ catch (journalError) {
1510
+ 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.", {
1511
+ sourceIndex: source.index,
1512
+ code: typeof journalError.code === "string" && /^[A-Z_]{1,40}$/.test(journalError.code ?? "") ? journalError.code : "INVALID_PENDING_AUTH",
1513
+ });
1514
+ }
1515
+ this.saveToDiskDebounced();
1516
+ return live;
1517
+ }
1410
1518
  throw new CodexAuthError(ERROR_MESSAGES.TOKEN_REFRESH_FAILED, {
1411
1519
  retryable: isRetryableAuthPersistenceError(error),
1412
1520
  cause: error,
@@ -1671,14 +1779,93 @@ export class AccountManager {
1671
1779
  }
1672
1780
  return account;
1673
1781
  }
1674
- async saveToDisk() {
1782
+ async persistSnapshot(current, proposed, persist, refreshed, runtimeOnConflict = false) {
1783
+ // Preserve initial/missing-store creation. An intentionally cleared store has
1784
+ // distinct metadata and must still win over this manager's stale inventory.
1785
+ let merged;
1786
+ let rescuedBaseline;
1787
+ try {
1788
+ merged = current ? mergeAccountSnapshot(this.persistenceBaseline, current, proposed) : proposed;
1789
+ }
1790
+ catch (error) {
1791
+ if (runtimeOnConflict && error.code === "ESTALE" && current && this.persistenceBaseline) {
1792
+ await persist(mergeAccountRuntimeObservations(this.persistenceBaseline, current, proposed));
1793
+ log.warn("Saved runtime observations; conflicting account edits remain pending and require reload or reconciliation.");
1794
+ return;
1795
+ }
1796
+ if (error.code !== "ESTALE" || !refreshed?.recordId || !current)
1797
+ throw error;
1798
+ // A rotated credential must not be discarded because an unrelated user edit
1799
+ // conflicts. Persist only its auth delta under this same transaction lock.
1800
+ const matches = (row) => resolveAccountRecordId(row) === refreshed.recordId;
1801
+ const prior = this.persistenceBaseline?.accounts.find(matches);
1802
+ const index = current.accounts.findIndex(matches);
1803
+ const disk = current.accounts[index];
1804
+ if (!prior || !disk || disk.refreshToken !== prior.refreshToken || disk.enabled === false || disk.authInvalidatedAt)
1805
+ throw error;
1806
+ merged = structuredClone(current);
1807
+ const row = merged.accounts[index];
1808
+ if (!row || !this.persistenceBaseline)
1809
+ throw error;
1810
+ rescuedBaseline = structuredClone(this.persistenceBaseline);
1811
+ const baselineRow = rescuedBaseline.accounts.find(matches);
1812
+ if (!baselineRow)
1813
+ throw error;
1814
+ const cooldown = mergeAccountCooldown(prior, disk, refreshed);
1815
+ // Persist the stable identity before rotating a legacy row's identity material.
1816
+ row.recordId = refreshed.recordId;
1817
+ baselineRow.recordId = refreshed.recordId;
1818
+ // Only authentication was committed. Pending additions, removals and user
1819
+ // edits must still differ from their old baseline on the next save. The
1820
+ // refresh can also move the identity and clears auth blockers; a restart
1821
+ // must not bring the old identity or cooldown back.
1822
+ for (const field of ["accessToken", "refreshToken", "expiresAt", "accountId", "accountIdSource", "email", "authInvalidatedAt", "authInvalidationErrorCode", "coolingDownUntil", "cooldownReason"]) {
1823
+ const value = refreshed[field];
1824
+ if (value === undefined) {
1825
+ delete row[field];
1826
+ delete baselineRow[field];
1827
+ }
1828
+ else {
1829
+ Object.assign(row, { [field]: value });
1830
+ Object.assign(baselineRow, { [field]: value });
1831
+ }
1832
+ }
1833
+ Object.assign(row, cooldown);
1834
+ }
1835
+ await persist(merged);
1836
+ if (refreshed) {
1837
+ const saved = merged.accounts.find(row => row.recordId === refreshed.recordId);
1838
+ const live = this.accounts.find(row => row.recordId === refreshed.recordId);
1839
+ // A successful refresh must retain blockers learned by another request,
1840
+ // including an observation made while the storage write was pending.
1841
+ if (saved && live)
1842
+ Object.assign(live, mergeAccountCooldown(refreshed, saved, live));
1843
+ }
1844
+ // Keep the baseline in this manager's inventory/intent space. Adopting added
1845
+ // disk records here would misread their absence in the next save as deletion.
1846
+ this.persistenceBaseline = rescuedBaseline ?? structuredClone(proposed);
1847
+ if (refreshed) {
1848
+ const saved = merged.accounts.find(row => row.recordId === refreshed.recordId);
1849
+ const baseline = this.persistenceBaseline.accounts.find(row => row.recordId === refreshed.recordId);
1850
+ // The adopted blocker is now known persisted state, so a later explicit
1851
+ // clear must compare against it rather than the pre-refresh clear.
1852
+ if (saved && baseline)
1853
+ Object.assign(baseline, { coolingDownUntil: saved.coolingDownUntil, cooldownReason: saved.cooldownReason });
1854
+ }
1855
+ }
1856
+ async saveToDisk(runtimeOnConflict = false) {
1675
1857
  await runWithStoragePathState(this.storagePathState, async () => {
1676
1858
  await withAccountStorageTransaction(async (current, persist) => {
1859
+ if (!current && this.hadPersistedStorage)
1860
+ throw Object.assign(new Error("Account storage was removed; reload before saving."), { code: "ESTALE" });
1677
1861
  // Reconcile against the disk state loaded under the storage lock so a
1678
1862
  // routine save does not clobber a token another process just rotated
1679
1863
  // (stress audit H3) or a pin the CLI just wrote (#474).
1680
1864
  this.reconcileSelectionFromDisk();
1681
- await persist(this.reconcileTokensFromDisk(this.buildStorageSnapshot(), current));
1865
+ this.syncWorkspaceSelections(current);
1866
+ const snapshot = this.reconcileTokensFromDisk(this.buildStorageSnapshot(), current);
1867
+ await this.persistSnapshot(current, snapshot, persist, undefined, runtimeOnConflict);
1868
+ this.rememberWorkspaceSelections(snapshot);
1682
1869
  });
1683
1870
  });
1684
1871
  }
@@ -1693,7 +1880,7 @@ export class AccountManager {
1693
1880
  if (this.pendingSave) {
1694
1881
  await this.pendingSave;
1695
1882
  }
1696
- this.pendingSave = this.saveToDisk().finally(() => {
1883
+ this.pendingSave = this.saveToDisk(true).finally(() => {
1697
1884
  this.pendingSave = null;
1698
1885
  });
1699
1886
  await this.pendingSave;
@@ -1735,7 +1922,7 @@ export class AccountManager {
1735
1922
  if (this.saveDebounceTimer) {
1736
1923
  clearTimeout(this.saveDebounceTimer);
1737
1924
  this.saveDebounceTimer = null;
1738
- await this.saveToDisk();
1925
+ await this.saveToDisk(true);
1739
1926
  }
1740
1927
  if (this.pendingSave) {
1741
1928
  await this.pendingSave;
@@ -1754,10 +1941,15 @@ export class AccountManager {
1754
1941
  }
1755
1942
  const resetIndex = account.workspaces.findIndex((workspace) => workspace.isDefault === true);
1756
1943
  for (const workspace of account.workspaces) {
1757
- workspace.enabled = true;
1758
- delete workspace.disabledAt;
1944
+ // Only undo runtime health disablement, never an explicit workspace exclusion.
1945
+ if (workspace.disabledAt !== undefined) {
1946
+ workspace.enabled = true;
1947
+ delete workspace.disabledAt;
1948
+ }
1949
+ }
1950
+ if (!account.workspaces[account.currentWorkspaceIndex ?? -1]) {
1951
+ account.currentWorkspaceIndex = resetIndex >= 0 ? resetIndex : 0;
1759
1952
  }
1760
- account.currentWorkspaceIndex = resetIndex >= 0 ? resetIndex : 0;
1761
1953
  }
1762
1954
  getCurrentWorkspace(account) {
1763
1955
  if (!account.workspaces || account.workspaces.length === 0) {
@@ -1766,6 +1958,15 @@ export class AccountManager {
1766
1958
  const idx = account.currentWorkspaceIndex ?? 0;
1767
1959
  return account.workspaces[idx] ?? null;
1768
1960
  }
1961
+ /** Disable a specific request workspace without changing the interactive selection. */
1962
+ disableWorkspace(account, workspaceId) {
1963
+ const workspace = account.workspaces?.find(item => item.id === workspaceId);
1964
+ if (!workspace || workspace.enabled === false)
1965
+ return false;
1966
+ workspace.enabled = false;
1967
+ workspace.disabledAt = nowMs();
1968
+ return true;
1969
+ }
1769
1970
  disableCurrentWorkspace(account, expectedWorkspaceId) {
1770
1971
  if (!account.workspaces || account.workspaces.length === 0) {
1771
1972
  return false;
@@ -1873,7 +2074,7 @@ export function formatAccountLabel(account, index) {
1873
2074
  * render these (e.g. only when more than one workspace exists) and supply the
1874
2075
  * leading indent.
1875
2076
  */
1876
- export function formatWorkspaceLines(account, indent = " ") {
2077
+ export function formatWorkspaceLines(account, indent = " ", selectionLabel = "active") {
1877
2078
  const workspaces = account?.workspaces;
1878
2079
  if (!workspaces || workspaces.length === 0)
1879
2080
  return [];
@@ -1885,7 +2086,7 @@ export function formatWorkspaceLines(account, indent = " ") {
1885
2086
  const idSuffix = id.length > 6 ? id.slice(-6) : id;
1886
2087
  const tags = [];
1887
2088
  if (isActive)
1888
- tags.push("active");
2089
+ tags.push(selectionLabel);
1889
2090
  if (workspace.enabled === false)
1890
2091
  tags.push("disabled");
1891
2092
  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
+ }