codex-multi-auth 2.17.2 → 2.18.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 (405) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +86 -425
  3. package/config/README.md +1 -1
  4. package/config/codex-legacy.json +12 -212
  5. package/config/codex-modern.json +4 -80
  6. package/config/minimal-codex.json +1 -1
  7. package/dist/index.d.ts +0 -1
  8. package/dist/lib/account-policy.d.ts +18 -2
  9. package/dist/lib/account-policy.js +97 -40
  10. package/dist/lib/accounts/rate-limits.d.ts +0 -1
  11. package/dist/lib/accounts.d.ts +0 -1
  12. package/dist/lib/accounts.js +4 -5
  13. package/dist/lib/api-route-store.d.ts +0 -1
  14. package/dist/lib/audit.d.ts +0 -1
  15. package/dist/lib/audit.js +43 -12
  16. package/dist/lib/auth/account-access.d.ts +0 -1
  17. package/dist/lib/auth/account-access.js +5 -1
  18. package/dist/lib/auth/auth.d.ts +4 -3
  19. package/dist/lib/auth/auth.js +220 -28
  20. package/dist/lib/auth/browser.d.ts +0 -1
  21. package/dist/lib/auth/callback-guidance.d.ts +0 -1
  22. package/dist/lib/auth/device-auth.d.ts +0 -1
  23. package/dist/lib/auth/index.d.ts +0 -1
  24. package/dist/lib/auth/org-override.d.ts +0 -1
  25. package/dist/lib/auth/server.d.ts +1 -2
  26. package/dist/lib/auth/server.js +100 -53
  27. package/dist/lib/auth/token-utils.d.ts +0 -1
  28. package/dist/lib/auth-rate-limit.d.ts +0 -1
  29. package/dist/lib/budget-guard.d.ts +16 -2
  30. package/dist/lib/budget-guard.js +123 -43
  31. package/dist/lib/capability-policy.d.ts +0 -1
  32. package/dist/lib/circuit-breaker.d.ts +20 -1
  33. package/dist/lib/circuit-breaker.js +24 -5
  34. package/dist/lib/cli.d.ts +0 -1
  35. package/dist/lib/codex-cli/index.d.ts +0 -1
  36. package/dist/lib/codex-cli/observability.d.ts +0 -1
  37. package/dist/lib/codex-cli/state.d.ts +0 -1
  38. package/dist/lib/codex-cli/sync.d.ts +0 -1
  39. package/dist/lib/codex-cli/writer.d.ts +0 -1
  40. package/dist/lib/codex-manager/account-credentials.d.ts +0 -1
  41. package/dist/lib/codex-manager/account-manager-commands.d.ts +0 -1
  42. package/dist/lib/codex-manager/account-pool-write.d.ts +0 -1
  43. package/dist/lib/codex-manager/active-account-sync.d.ts +6 -0
  44. package/dist/lib/codex-manager/active-account-sync.js +336 -0
  45. package/dist/lib/codex-manager/api-login-menu.d.ts +0 -1
  46. package/dist/lib/codex-manager/backend-category-entry.d.ts +0 -1
  47. package/dist/lib/codex-manager/backend-category-helpers.d.ts +0 -1
  48. package/dist/lib/codex-manager/backend-category-prompt.d.ts +0 -1
  49. package/dist/lib/codex-manager/backend-settings-controller.d.ts +0 -1
  50. package/dist/lib/codex-manager/backend-settings-entry.d.ts +0 -1
  51. package/dist/lib/codex-manager/backend-settings-helpers.d.ts +0 -1
  52. package/dist/lib/codex-manager/backend-settings-prompt.d.ts +0 -1
  53. package/dist/lib/codex-manager/backend-settings-schema.d.ts +0 -1
  54. package/dist/lib/codex-manager/behavior-settings-panel.d.ts +0 -1
  55. package/dist/lib/codex-manager/commands/account.d.ts +8 -3
  56. package/dist/lib/codex-manager/commands/account.js +46 -34
  57. package/dist/lib/codex-manager/commands/best.d.ts +0 -1
  58. package/dist/lib/codex-manager/commands/bridge.d.ts +0 -1
  59. package/dist/lib/codex-manager/commands/budget.d.ts +8 -3
  60. package/dist/lib/codex-manager/commands/budget.js +6 -4
  61. package/dist/lib/codex-manager/commands/check.d.ts +0 -1
  62. package/dist/lib/codex-manager/commands/config-explain.d.ts +0 -1
  63. package/dist/lib/codex-manager/commands/debug-bundle.d.ts +0 -1
  64. package/dist/lib/codex-manager/commands/forecast.d.ts +0 -1
  65. package/dist/lib/codex-manager/commands/history.d.ts +0 -1
  66. package/dist/lib/codex-manager/commands/init-config.d.ts +0 -1
  67. package/dist/lib/codex-manager/commands/integrations.d.ts +0 -1
  68. package/dist/lib/codex-manager/commands/limits.d.ts +0 -1
  69. package/dist/lib/codex-manager/commands/models.d.ts +0 -1
  70. package/dist/lib/codex-manager/commands/monitor.d.ts +0 -1
  71. package/dist/lib/codex-manager/commands/report.d.ts +0 -1
  72. package/dist/lib/codex-manager/commands/resets.d.ts +0 -1
  73. package/dist/lib/codex-manager/commands/resets.js +9 -1
  74. package/dist/lib/codex-manager/commands/rotation.d.ts +0 -1
  75. package/dist/lib/codex-manager/commands/status.d.ts +0 -1
  76. package/dist/lib/codex-manager/commands/switch.d.ts +0 -1
  77. package/dist/lib/codex-manager/commands/uninstall.d.ts +0 -1
  78. package/dist/lib/codex-manager/commands/unpin.d.ts +0 -1
  79. package/dist/lib/codex-manager/commands/usage.d.ts +0 -1
  80. package/dist/lib/codex-manager/commands/verify.d.ts +0 -1
  81. package/dist/lib/codex-manager/commands/why-selected.d.ts +0 -1
  82. package/dist/lib/codex-manager/commands/workspace.d.ts +0 -1
  83. package/dist/lib/codex-manager/dashboard-display-panel.d.ts +0 -1
  84. package/dist/lib/codex-manager/dashboard-formatters.d.ts +0 -1
  85. package/dist/lib/codex-manager/dashboard-settings-controller.d.ts +0 -1
  86. package/dist/lib/codex-manager/dashboard-settings-data.d.ts +0 -1
  87. package/dist/lib/codex-manager/dashboard-settings-entry.d.ts +0 -1
  88. package/dist/lib/codex-manager/experimental-settings-entry.d.ts +0 -1
  89. package/dist/lib/codex-manager/experimental-settings-prompt.d.ts +0 -1
  90. package/dist/lib/codex-manager/experimental-settings-schema.d.ts +0 -1
  91. package/dist/lib/codex-manager/experimental-sync-target-entry.d.ts +0 -1
  92. package/dist/lib/codex-manager/experimental-sync-target.d.ts +0 -1
  93. package/dist/lib/codex-manager/forecast-report-shared.d.ts +0 -1
  94. package/dist/lib/codex-manager/formatters/account-formatters.d.ts +0 -1
  95. package/dist/lib/codex-manager/formatters/index.d.ts +0 -1
  96. package/dist/lib/codex-manager/formatters/model-formatters.d.ts +0 -1
  97. package/dist/lib/codex-manager/formatters/quota-formatters.d.ts +0 -1
  98. package/dist/lib/codex-manager/formatters/text-style.d.ts +0 -1
  99. package/dist/lib/codex-manager/health-check.d.ts +0 -1
  100. package/dist/lib/codex-manager/help.d.ts +0 -1
  101. package/dist/lib/codex-manager/login-action-panel.d.ts +0 -1
  102. package/dist/lib/codex-manager/login-flow.d.ts +0 -1
  103. package/dist/lib/codex-manager/login-menu-actions.d.ts +0 -1
  104. package/dist/lib/codex-manager/login-menu-data.d.ts +0 -1
  105. package/dist/lib/codex-manager/login-oauth.d.ts +0 -1
  106. package/dist/lib/codex-manager/login-workspace-choice.d.ts +0 -1
  107. package/dist/lib/codex-manager/manual-callback.d.ts +0 -1
  108. package/dist/lib/codex-manager/persist-selected-account.d.ts +0 -1
  109. package/dist/lib/codex-manager/quota-cache-helpers.d.ts +0 -1
  110. package/dist/lib/codex-manager/rate-limit-markers.d.ts +0 -1
  111. package/dist/lib/codex-manager/repair-commands.d.ts +0 -1
  112. package/dist/lib/codex-manager/settings-hub/backend.d.ts +0 -1
  113. package/dist/lib/codex-manager/settings-hub/dashboard.d.ts +0 -1
  114. package/dist/lib/codex-manager/settings-hub/experimental.d.ts +0 -1
  115. package/dist/lib/codex-manager/settings-hub/index.d.ts +0 -1
  116. package/dist/lib/codex-manager/settings-hub/shared.d.ts +0 -1
  117. package/dist/lib/codex-manager/settings-hub-entry.d.ts +0 -1
  118. package/dist/lib/codex-manager/settings-hub-menu.d.ts +0 -1
  119. package/dist/lib/codex-manager/settings-hub-prompt.d.ts +0 -1
  120. package/dist/lib/codex-manager/settings-hub.d.ts +0 -1
  121. package/dist/lib/codex-manager/settings-panels.d.ts +0 -1
  122. package/dist/lib/codex-manager/settings-persist-utils.d.ts +0 -1
  123. package/dist/lib/codex-manager/settings-preview.d.ts +0 -1
  124. package/dist/lib/codex-manager/settings-write-queue.d.ts +0 -1
  125. package/dist/lib/codex-manager/statusline-settings-panel.d.ts +0 -1
  126. package/dist/lib/codex-manager/theme-settings-panel.d.ts +0 -1
  127. package/dist/lib/codex-manager/unified-settings-controller.d.ts +0 -1
  128. package/dist/lib/codex-manager/unified-settings-entry.d.ts +0 -1
  129. package/dist/lib/codex-manager.d.ts +1 -2
  130. package/dist/lib/codex-manager.js +5 -95
  131. package/dist/lib/concurrency.d.ts +0 -1
  132. package/dist/lib/config-schema.d.ts +0 -1
  133. package/dist/lib/config.d.ts +0 -1
  134. package/dist/lib/config.js +46 -241
  135. package/dist/lib/constants.d.ts +0 -1
  136. package/dist/lib/context-budget/model-context-windows.d.ts +1 -2
  137. package/dist/lib/context-budget/model-context-windows.js +10 -13
  138. package/dist/lib/context-budget-guard.d.ts +0 -1
  139. package/dist/lib/context-budget-response.d.ts +0 -1
  140. package/dist/lib/context-overflow.d.ts +0 -1
  141. package/dist/lib/dashboard-settings.d.ts +0 -1
  142. package/dist/lib/entitlement-cache.d.ts +0 -1
  143. package/dist/lib/env-parsing.d.ts +13 -1
  144. package/dist/lib/env-parsing.js +21 -0
  145. package/dist/lib/errors.d.ts +0 -1
  146. package/dist/lib/forecast.d.ts +0 -1
  147. package/dist/lib/fs-retry.d.ts +0 -1
  148. package/dist/lib/health.d.ts +0 -1
  149. package/dist/lib/index.d.ts +0 -1
  150. package/dist/lib/integration-generators.d.ts +0 -1
  151. package/dist/lib/live-account-sync.d.ts +0 -1
  152. package/dist/lib/local-bridge.d.ts +0 -1
  153. package/dist/lib/local-bridge.js +4 -1
  154. package/dist/lib/local-client-tokens.d.ts +0 -1
  155. package/dist/lib/local-client-tokens.js +125 -30
  156. package/dist/lib/logger.d.ts +0 -1
  157. package/dist/lib/logger.js +11 -1
  158. package/dist/lib/model-capability-matrix.d.ts +0 -1
  159. package/dist/lib/model-route-policy.d.ts +1 -2
  160. package/dist/lib/named-backup-export.d.ts +0 -1
  161. package/dist/lib/oc-chatgpt-import-adapter.d.ts +0 -1
  162. package/dist/lib/oc-chatgpt-orchestrator.d.ts +0 -1
  163. package/dist/lib/oc-chatgpt-target-detection.d.ts +3 -3
  164. package/dist/lib/oc-chatgpt-target-detection.js +40 -13
  165. package/dist/lib/parallel-probe.d.ts +0 -1
  166. package/dist/lib/policy/runtime-policy-cache.d.ts +80 -0
  167. package/dist/lib/policy/runtime-policy-cache.js +310 -0
  168. package/dist/lib/policy/runtime-policy.d.ts +1 -2
  169. package/dist/lib/policy/runtime-policy.js +46 -19
  170. package/dist/lib/preemptive-quota-scheduler.d.ts +0 -1
  171. package/dist/lib/proactive-refresh.d.ts +0 -1
  172. package/dist/lib/prompts/codex-host-bridge.d.ts +0 -1
  173. package/dist/lib/prompts/codex.d.ts +0 -1
  174. package/dist/lib/prompts/fetch-utils.d.ts +0 -1
  175. package/dist/lib/prompts/host-codex-prompt.d.ts +0 -1
  176. package/dist/lib/quota-cache.d.ts +0 -1
  177. package/dist/lib/quota-cache.js +60 -36
  178. package/dist/lib/quota-probe.d.ts +0 -1
  179. package/dist/lib/quota-probe.js +5 -2
  180. package/dist/lib/quota-readiness.d.ts +0 -1
  181. package/dist/lib/recovery/constants.d.ts +0 -1
  182. package/dist/lib/recovery/storage.d.ts +0 -1
  183. package/dist/lib/recovery/types.d.ts +0 -1
  184. package/dist/lib/recovery.d.ts +0 -1
  185. package/dist/lib/redaction.d.ts +0 -1
  186. package/dist/lib/refresh-guardian.d.ts +0 -1
  187. package/dist/lib/refresh-lease.d.ts +0 -1
  188. package/dist/lib/refresh-queue.d.ts +0 -1
  189. package/dist/lib/request/client-cancellation.d.ts +0 -1
  190. package/dist/lib/request/error-classification.d.ts +0 -1
  191. package/dist/lib/request/error-classification.js +41 -52
  192. package/dist/lib/request/failover-config.d.ts +0 -1
  193. package/dist/lib/request/failure-policy.d.ts +0 -1
  194. package/dist/lib/request/fetch-helpers.d.ts +0 -1
  195. package/dist/lib/request/fetch-helpers.js +1 -1
  196. package/dist/lib/request/headers.d.ts +0 -1
  197. package/dist/lib/request/helpers/input-utils.d.ts +0 -1
  198. package/dist/lib/request/helpers/input-utils.js +88 -34
  199. package/dist/lib/request/helpers/model-map.d.ts +2 -3
  200. package/dist/lib/request/helpers/model-map.js +120 -78
  201. package/dist/lib/request/helpers/tool-utils.d.ts +0 -1
  202. package/dist/lib/request/helpers/tool-utils.js +421 -4
  203. package/dist/lib/request/index.d.ts +0 -1
  204. package/dist/lib/request/rate-limit-backoff.d.ts +0 -1
  205. package/dist/lib/request/rate-limit-decision.d.ts +0 -1
  206. package/dist/lib/request/request-attempt-budget.d.ts +0 -1
  207. package/dist/lib/request/request-init.d.ts +0 -1
  208. package/dist/lib/request/request-resilience.d.ts +0 -1
  209. package/dist/lib/request/request-transformer.d.ts +1 -2
  210. package/dist/lib/request/request-transformer.js +51 -26
  211. package/dist/lib/request/response-compaction.d.ts +0 -1
  212. package/dist/lib/request/response-handler.d.ts +0 -1
  213. package/dist/lib/request/response-metadata.d.ts +0 -1
  214. package/dist/lib/request/response-outcome.d.ts +0 -1
  215. package/dist/lib/request/stream-failover-runtime.d.ts +0 -1
  216. package/dist/lib/request/stream-failover.d.ts +0 -1
  217. package/dist/lib/request/stream-failover.js +28 -4
  218. package/dist/lib/request/token-refresh.d.ts +0 -1
  219. package/dist/lib/request/url-rewriting.d.ts +0 -1
  220. package/dist/lib/request/wait-utils.d.ts +0 -1
  221. package/dist/lib/rotation.d.ts +0 -1
  222. package/dist/lib/routing-mutex.d.ts +0 -1
  223. package/dist/lib/routing-profiles.d.ts +16 -2
  224. package/dist/lib/routing-profiles.js +119 -44
  225. package/dist/lib/runtime/account-check-helpers.d.ts +0 -1
  226. package/dist/lib/runtime/account-check-types.d.ts +0 -1
  227. package/dist/lib/runtime/account-check.d.ts +0 -1
  228. package/dist/lib/runtime/account-manager-cache-entry.d.ts +0 -1
  229. package/dist/lib/runtime/account-manager-cache.d.ts +0 -1
  230. package/dist/lib/runtime/account-model-catalog.d.ts +1 -2
  231. package/dist/lib/runtime/account-pool.d.ts +0 -1
  232. package/dist/lib/runtime/account-reset-credits.d.ts +0 -1
  233. package/dist/lib/runtime/account-reset-credits.js +8 -2
  234. package/dist/lib/runtime/account-scope.d.ts +0 -1
  235. package/dist/lib/runtime/account-select-event.d.ts +0 -1
  236. package/dist/lib/runtime/account-selection.d.ts +0 -1
  237. package/dist/lib/runtime/account-state.d.ts +0 -1
  238. package/dist/lib/runtime/account-status.d.ts +0 -1
  239. package/dist/lib/runtime/account-storage-scope-entry.d.ts +0 -1
  240. package/dist/lib/runtime/api-model-capabilities.d.ts +0 -1
  241. package/dist/lib/runtime/api-model-runtime.d.ts +0 -1
  242. package/dist/lib/runtime/app-bind.d.ts +0 -1
  243. package/dist/lib/runtime/app-helper-selection.d.ts +0 -1
  244. package/dist/lib/runtime/auth-facade.d.ts +0 -1
  245. package/dist/lib/runtime/automatic-account-checks.d.ts +0 -1
  246. package/dist/lib/runtime/automatic-subscription-checks.d.ts +0 -1
  247. package/dist/lib/runtime/browser-oauth-flow.d.ts +0 -1
  248. package/dist/lib/runtime/capability-boost.d.ts +0 -1
  249. package/dist/lib/runtime/catalog-capabilities.d.ts +0 -1
  250. package/dist/lib/runtime/config-toml.d.ts +0 -1
  251. package/dist/lib/runtime/context-budget-settings.d.ts +0 -1
  252. package/dist/lib/runtime/event-handler.d.ts +0 -1
  253. package/dist/lib/runtime/first-run.d.ts +0 -1
  254. package/dist/lib/runtime/flagged-verify-types.d.ts +0 -1
  255. package/dist/lib/runtime/hydrate-emails.d.ts +0 -1
  256. package/dist/lib/runtime/inference-activity.d.ts +0 -1
  257. package/dist/lib/runtime/live-sync-entry.d.ts +0 -1
  258. package/dist/lib/runtime/live-sync.d.ts +0 -1
  259. package/dist/lib/runtime/loader-setup.d.ts +0 -1
  260. package/dist/lib/runtime/login-menu-accounts.d.ts +0 -1
  261. package/dist/lib/runtime/manual-oauth-flow.d.ts +0 -1
  262. package/dist/lib/runtime/model-discovery-status.d.ts +0 -1
  263. package/dist/lib/runtime/native-account-storage.d.ts +0 -1
  264. package/dist/lib/runtime/native-account-sync.d.ts +0 -1
  265. package/dist/lib/runtime/native-binding-lock.d.ts +0 -1
  266. package/dist/lib/runtime/native-client-auth.d.ts +0 -1
  267. package/dist/lib/runtime/native-provider-config.d.ts +0 -1
  268. package/dist/lib/runtime/native-rate-limits.d.ts +2 -1
  269. package/dist/lib/runtime/native-rate-limits.js +23 -6
  270. package/dist/lib/runtime/preemptive-quota.d.ts +0 -1
  271. package/dist/lib/runtime/quota-headers.d.ts +0 -1
  272. package/dist/lib/runtime/quota-probe.d.ts +0 -1
  273. package/dist/lib/runtime/quota-settings.d.ts +0 -1
  274. package/dist/lib/runtime/refresh-guardian-entry.d.ts +0 -1
  275. package/dist/lib/runtime/refresh-guardian.d.ts +0 -1
  276. package/dist/lib/runtime/request-init.d.ts +0 -1
  277. package/dist/lib/runtime/reset-credit-routing.d.ts +0 -1
  278. package/dist/lib/runtime/reset-credits.d.ts +0 -1
  279. package/dist/lib/runtime/response-output-history.d.ts +0 -1
  280. package/dist/lib/runtime/responses-websocket.d.ts +0 -1
  281. package/dist/lib/runtime/resume-picker.d.ts +0 -1
  282. package/dist/lib/runtime/rotation-account-selection.d.ts +0 -1
  283. package/dist/lib/runtime/rotation-proxy-state.d.ts +0 -1
  284. package/dist/lib/runtime/rotation-server-types.d.ts +0 -1
  285. package/dist/lib/runtime/rotation-storage-meta.d.ts +0 -1
  286. package/dist/lib/runtime/rotation-token-refresh.d.ts +0 -1
  287. package/dist/lib/runtime/runtime-capability-failures.d.ts +0 -1
  288. package/dist/lib/runtime/runtime-current-account.d.ts +0 -1
  289. package/dist/lib/runtime/runtime-observability.d.ts +0 -1
  290. package/dist/lib/runtime/runtime-services.d.ts +0 -1
  291. package/dist/lib/runtime/session-affinity-entry.d.ts +0 -1
  292. package/dist/lib/runtime/session-recovery.d.ts +0 -1
  293. package/dist/lib/runtime/status-marker.d.ts +0 -1
  294. package/dist/lib/runtime/storage-scope.d.ts +0 -1
  295. package/dist/lib/runtime/subscription-first-use.d.ts +0 -1
  296. package/dist/lib/runtime/subscription-quota-event.d.ts +0 -1
  297. package/dist/lib/runtime/subscription-quota-order.d.ts +0 -1
  298. package/dist/lib/runtime/toast.d.ts +0 -1
  299. package/dist/lib/runtime/ui-runtime-entry.d.ts +0 -1
  300. package/dist/lib/runtime/ui-runtime.d.ts +0 -1
  301. package/dist/lib/runtime/verify-flagged.d.ts +0 -1
  302. package/dist/lib/runtime/workspace-model-scopes.d.ts +0 -1
  303. package/dist/lib/runtime-constants.d.ts +0 -1
  304. package/dist/lib/runtime-paths.d.ts +0 -1
  305. package/dist/lib/runtime-rotation-proxy.d.ts +1 -1
  306. package/dist/lib/runtime-rotation-proxy.js +133 -0
  307. package/dist/lib/schemas.d.ts +35 -13
  308. package/dist/lib/schemas.js +19 -2
  309. package/dist/lib/session-affinity.d.ts +0 -1
  310. package/dist/lib/shutdown.d.ts +0 -1
  311. package/dist/lib/storage/account-clear-entry.d.ts +0 -1
  312. package/dist/lib/storage/account-clear.d.ts +0 -1
  313. package/dist/lib/storage/account-match-utils.d.ts +0 -1
  314. package/dist/lib/storage/account-persistence.d.ts +0 -1
  315. package/dist/lib/storage/account-port.d.ts +0 -1
  316. package/dist/lib/storage/account-save-entry.d.ts +0 -1
  317. package/dist/lib/storage/account-save.d.ts +9 -2
  318. package/dist/lib/storage/account-save.js +4 -1
  319. package/dist/lib/storage/account-snapshot.d.ts +0 -1
  320. package/dist/lib/storage/backup-metadata-builder.d.ts +0 -1
  321. package/dist/lib/storage/backup-metadata.d.ts +0 -1
  322. package/dist/lib/storage/backup-paths.d.ts +0 -1
  323. package/dist/lib/storage/backup-restore.d.ts +0 -1
  324. package/dist/lib/storage/cache-artifacts.d.ts +0 -1
  325. package/dist/lib/storage/credential-sidecars.d.ts +0 -1
  326. package/dist/lib/storage/error-hints.d.ts +0 -1
  327. package/dist/lib/storage/file-lock.d.ts +0 -1
  328. package/dist/lib/storage/file-lock.js +33 -1
  329. package/dist/lib/storage/file-paths.d.ts +0 -1
  330. package/dist/lib/storage/fixture-guards.d.ts +0 -1
  331. package/dist/lib/storage/flagged-entry.d.ts +0 -1
  332. package/dist/lib/storage/flagged-load-entry.d.ts +0 -1
  333. package/dist/lib/storage/flagged-save-entry.d.ts +0 -1
  334. package/dist/lib/storage/flagged-storage-file.d.ts +0 -1
  335. package/dist/lib/storage/flagged-storage-io.d.ts +0 -1
  336. package/dist/lib/storage/flagged-storage.d.ts +0 -1
  337. package/dist/lib/storage/gitignore.d.ts +0 -1
  338. package/dist/lib/storage/hash.d.ts +0 -1
  339. package/dist/lib/storage/health.d.ts +0 -1
  340. package/dist/lib/storage/identity.d.ts +0 -1
  341. package/dist/lib/storage/import-export.d.ts +0 -1
  342. package/dist/lib/storage/json-store-lock.d.ts +105 -0
  343. package/dist/lib/storage/json-store-lock.js +259 -0
  344. package/dist/lib/storage/metadata-section.d.ts +0 -1
  345. package/dist/lib/storage/migrations.d.ts +0 -1
  346. package/dist/lib/storage/migrations.js +31 -18
  347. package/dist/lib/storage/named-backup-entry.d.ts +0 -1
  348. package/dist/lib/storage/named-backups-entry.d.ts +0 -1
  349. package/dist/lib/storage/named-backups.d.ts +0 -1
  350. package/dist/lib/storage/path-state.d.ts +0 -1
  351. package/dist/lib/storage/paths.d.ts +0 -1
  352. package/dist/lib/storage/pending-auth.d.ts +9 -3
  353. package/dist/lib/storage/pending-auth.js +30 -8
  354. package/dist/lib/storage/project-migration.d.ts +0 -1
  355. package/dist/lib/storage/public-types.d.ts +0 -1
  356. package/dist/lib/storage/record-identity.d.ts +0 -1
  357. package/dist/lib/storage/record-utils.d.ts +0 -1
  358. package/dist/lib/storage/restore-assessment.d.ts +0 -1
  359. package/dist/lib/storage/restore-backup-entry.d.ts +0 -1
  360. package/dist/lib/storage/restore-metadata.d.ts +0 -1
  361. package/dist/lib/storage/save-retry.d.ts +0 -1
  362. package/dist/lib/storage/snapshot-inspectors.d.ts +0 -1
  363. package/dist/lib/storage/snapshot-merge.d.ts +0 -1
  364. package/dist/lib/storage/storage-parser.d.ts +2 -1
  365. package/dist/lib/storage/storage-parser.js +10 -2
  366. package/dist/lib/storage/transactions.d.ts +0 -1
  367. package/dist/lib/storage.d.ts +15 -2
  368. package/dist/lib/storage.js +612 -25
  369. package/dist/lib/synthetic-response.d.ts +0 -1
  370. package/dist/lib/table-formatter.d.ts +0 -1
  371. package/dist/lib/temp-path.d.ts +0 -1
  372. package/dist/lib/tools/hashline-tools.d.ts +0 -1
  373. package/dist/lib/types.d.ts +0 -1
  374. package/dist/lib/ui/ansi.d.ts +0 -1
  375. package/dist/lib/ui/auth-menu-builder.d.ts +0 -1
  376. package/dist/lib/ui/auth-menu.d.ts +0 -1
  377. package/dist/lib/ui/check-progress.d.ts +0 -1
  378. package/dist/lib/ui/confirm.d.ts +0 -1
  379. package/dist/lib/ui/display-width.d.ts +0 -1
  380. package/dist/lib/ui/format.d.ts +0 -1
  381. package/dist/lib/ui/index.d.ts +0 -1
  382. package/dist/lib/ui/runtime.d.ts +0 -1
  383. package/dist/lib/ui/select.d.ts +0 -1
  384. package/dist/lib/ui/theme.d.ts +0 -1
  385. package/dist/lib/ui/ui-copy.d.ts +1 -2
  386. package/dist/lib/ui/ui-copy.js +1 -1
  387. package/dist/lib/unified-settings.d.ts +13 -11
  388. package/dist/lib/unified-settings.js +22 -14
  389. package/dist/lib/update-notice.d.ts +0 -1
  390. package/dist/lib/usage/index.d.ts +0 -1
  391. package/dist/lib/usage/ledger.d.ts +0 -1
  392. package/dist/lib/usage/pricing.d.ts +1 -2
  393. package/dist/lib/usage/pricing.js +41 -16
  394. package/dist/lib/usage/redaction.d.ts +0 -1
  395. package/dist/lib/usage/stream-usage-deferral.d.ts +0 -1
  396. package/dist/lib/usage/types.d.ts +0 -1
  397. package/dist/lib/usage/usage-extraction.d.ts +0 -1
  398. package/dist/lib/utils.d.ts +21 -1
  399. package/dist/lib/utils.js +31 -0
  400. package/dist/lib/wsl.d.ts +0 -1
  401. package/package.json +11 -5
  402. package/scripts/codex-bin-resolver.js +20 -1
  403. package/scripts/codex-multi-auth.js +7 -1
  404. package/scripts/codex.js +385 -89
  405. package/skills/codex-auth-setup/SKILL.md +73 -0
@@ -1,6 +1,7 @@
1
1
  import { existsSync, promises as fs, readFileSync } from "node:fs";
2
2
  import { basename, dirname, join } from "node:path";
3
3
  import { ACCOUNT_LIMITS } from "./constants.js";
4
+ import { parseIntegerEnv } from "./env-parsing.js";
4
5
  import { StorageError } from "./errors.js";
5
6
  import { withFileOperationRetry, withRetry } from "./fs-retry.js";
6
7
  import { createLogger } from "./logger.js";
@@ -69,6 +70,68 @@ const BACKUP_COPY_MAX_ATTEMPTS = 5;
69
70
  const BACKUP_COPY_BASE_DELAY_MS = 10;
70
71
  let storageBackupEnabled = true;
71
72
  let lastAccountsSaveTimestamp = 0;
73
+ /**
74
+ * Default minimum age of the newest rotating `.bak` snapshot before a save
75
+ * pays for another full 3-slot rotation. Rotating backups are crash-recovery
76
+ * insurance, not a changelog: inside the window the primary-write path skips
77
+ * ~6 fs ops and ~2x payload bytes of staged copies per save. A credential
78
+ * change (account count or refresh-token set) still forces a fresh slot — a
79
+ * backup that predates an in-window rotation can only offer spent tokens — a
80
+ * missing `.bak` always rotates, and
81
+ * `CODEX_AUTH_STORAGE_BACKUP_MIN_INTERVAL_MS=0` restores the legacy
82
+ * rotate-on-every-save behavior.
83
+ */
84
+ const ACCOUNTS_BACKUP_MIN_INTERVAL_DEFAULT_MS = 30_000;
85
+ function backupRotationMinIntervalMs() {
86
+ const parsed = parseIntegerEnv(process.env.CODEX_AUTH_STORAGE_BACKUP_MIN_INTERVAL_MS);
87
+ if (parsed === undefined) {
88
+ return ACCOUNTS_BACKUP_MIN_INTERVAL_DEFAULT_MS;
89
+ }
90
+ return Math.max(0, parsed);
91
+ }
92
+ const lastKnownPrimaryState = new Map();
93
+ const baselinelessSaveWarnedPaths = new Set();
94
+ function notePrimaryState(path, stats, sha256) {
95
+ if (!stats && sha256 === undefined)
96
+ return;
97
+ const fingerprint = { sha256 };
98
+ if (stats) {
99
+ if (Number.isFinite(stats.mtimeMs))
100
+ fingerprint.mtimeMs = stats.mtimeMs;
101
+ if (Number.isFinite(stats.ctimeMs))
102
+ fingerprint.ctimeMs = stats.ctimeMs;
103
+ if (Number.isFinite(stats.size))
104
+ fingerprint.size = stats.size;
105
+ // ino can exceed 2^53 on some filesystems; a lossy value would compare
106
+ // equal for two different files, so only record exact integers.
107
+ if (Number.isSafeInteger(stats.ino))
108
+ fingerprint.ino = stats.ino;
109
+ }
110
+ if (fingerprint.mtimeMs === undefined &&
111
+ fingerprint.ctimeMs === undefined &&
112
+ fingerprint.size === undefined &&
113
+ fingerprint.ino === undefined &&
114
+ fingerprint.sha256 === undefined) {
115
+ return;
116
+ }
117
+ lastKnownPrimaryState.set(path, fingerprint);
118
+ }
119
+ /**
120
+ * Metadata-only identity check, used when no content hash was recorded. A
121
+ * foreign write bumps ctime on POSIX even when the writer preserves mtime
122
+ * (utimes), and a rename-replacement changes ino — together they catch the
123
+ * equal-mtime collisions a mtime-only comparison missed.
124
+ */
125
+ function primaryFingerprintMatches(last, stats) {
126
+ return (last.mtimeMs !== undefined &&
127
+ last.ctimeMs !== undefined &&
128
+ last.size !== undefined &&
129
+ last.ino !== undefined &&
130
+ last.mtimeMs === stats.mtimeMs &&
131
+ last.ctimeMs === stats.ctimeMs &&
132
+ last.size === stats.size &&
133
+ last.ino === stats.ino);
134
+ }
72
135
  async function ensureGitignore(storagePath) {
73
136
  const state = getStoragePathState();
74
137
  if (!state.currentStoragePath)
@@ -201,6 +264,185 @@ async function createRotatingAccountsBackup(path) {
201
264
  }
202
265
  }
203
266
  }
267
+ function credentialSignatureOf(accounts) {
268
+ const refreshTokens = new Set();
269
+ for (const account of accounts) {
270
+ if (isRecord(account) &&
271
+ typeof account.refreshToken === "string" &&
272
+ account.refreshToken.length > 0) {
273
+ refreshTokens.add(account.refreshToken);
274
+ }
275
+ }
276
+ return { count: accounts.length, refreshTokens };
277
+ }
278
+ function sameCredentialSignature(a, b) {
279
+ if (a.count !== b.count || a.refreshTokens.size !== b.refreshTokens.size) {
280
+ return false;
281
+ }
282
+ for (const token of a.refreshTokens) {
283
+ if (!b.refreshTokens.has(token))
284
+ return false;
285
+ }
286
+ return true;
287
+ }
288
+ /**
289
+ * Cheap credential-signature probe for the throttle trigger. Reads and parses
290
+ * only the top-level `accounts` shape — far cheaper than the staged copies a
291
+ * rotation costs, and it runs at most once per save inside the throttle
292
+ * window. Returns null when the signature cannot be determined so the caller
293
+ * rotates rather than skipping a snapshot it cannot rule out.
294
+ */
295
+ async function readOnDiskCredentialSignature(path) {
296
+ try {
297
+ const parsed = JSON.parse(await fs.readFile(path, "utf-8"));
298
+ if (isRecord(parsed) && Array.isArray(parsed.accounts)) {
299
+ return credentialSignatureOf(parsed.accounts);
300
+ }
301
+ }
302
+ catch {
303
+ // Missing or unparsable primary: fall through to the null sentinel.
304
+ }
305
+ return null;
306
+ }
307
+ /**
308
+ * Refresh tokens still present in a recovery source: rotating and discovered
309
+ * `.bak*` snapshots plus any WAL journal payload. Pending rotated credentials
310
+ * whose spent token a restore could still resurface must survive pruning, so
311
+ * recovering a throttle-stale backup self-heals to the newest credential via
312
+ * `applyPendingAuth` instead of reviving a token that was spent upstream.
313
+ * Best-effort: unreadable or unparseable sources contribute nothing.
314
+ */
315
+ async function collectRestorableRefreshTokens(path) {
316
+ const tokens = [];
317
+ const collectContent = (raw) => {
318
+ let parsed;
319
+ try {
320
+ parsed = JSON.parse(raw);
321
+ }
322
+ catch {
323
+ return;
324
+ }
325
+ if (!isRecord(parsed) || !Array.isArray(parsed.accounts))
326
+ return;
327
+ for (const account of parsed.accounts) {
328
+ if (isRecord(account) &&
329
+ typeof account.refreshToken === "string" &&
330
+ account.refreshToken.length > 0) {
331
+ tokens.push(account.refreshToken);
332
+ }
333
+ }
334
+ };
335
+ for (const candidate of await getAccountsBackupRecoveryCandidatesWithDiscovery(path)) {
336
+ try {
337
+ collectContent(await fs.readFile(candidate, "utf-8"));
338
+ }
339
+ catch {
340
+ // An unreadable candidate contributes nothing.
341
+ }
342
+ }
343
+ try {
344
+ const walEntry = JSON.parse(await fs.readFile(getAccountsWalPath(path), "utf-8"));
345
+ if (isRecord(walEntry) && typeof walEntry.content === "string") {
346
+ collectContent(walEntry.content);
347
+ }
348
+ }
349
+ catch {
350
+ // No usable WAL payload.
351
+ }
352
+ return tokens;
353
+ }
354
+ /**
355
+ * Gate around `createRotatingAccountsBackup` that throttles rotation churn.
356
+ * Rotates when the newest `.bak` is missing (always preserve at least one
357
+ * recovery snapshot), when it is older than `backupRotationMinIntervalMs()`,
358
+ * or when the incoming write changes the stored credential signature
359
+ * (account count or refresh-token set) — the case where the pre-write state
360
+ * is worth a fresh slot even inside the window.
361
+ * `expectedOnDiskAccounts` lets callers that already read the primary under
362
+ * the lock (the three-way merge path) skip the signature probe.
363
+ */
364
+ async function rotateAccountsBackupIfDue(path, nextAccounts, expectedOnDiskAccounts) {
365
+ const latestBackupPath = getAccountsBackupPath(path);
366
+ let newestBackupAgeMs = null;
367
+ try {
368
+ newestBackupAgeMs = Date.now() - (await fs.stat(latestBackupPath)).mtimeMs;
369
+ }
370
+ catch {
371
+ // Missing or unreadable newest backup: rotate so >=1 snapshot exists.
372
+ newestBackupAgeMs = null;
373
+ }
374
+ const minIntervalMs = backupRotationMinIntervalMs();
375
+ // Age is clamped at 0: a backup whose mtime lands fractionally ahead of
376
+ // Date.now() (fs timestamp granularity vs. the clock) is "fresh", and the
377
+ // clamp also keeps `minIntervalMs = 0` meaning "always rotate".
378
+ if (newestBackupAgeMs !== null && Math.max(0, newestBackupAgeMs) < minIntervalMs) {
379
+ const onDiskSignature = expectedOnDiskAccounts !== undefined
380
+ ? credentialSignatureOf(expectedOnDiskAccounts ?? [])
381
+ : await readOnDiskCredentialSignature(path);
382
+ if (onDiskSignature !== null &&
383
+ sameCredentialSignature(onDiskSignature, credentialSignatureOf(nextAccounts))) {
384
+ log.debug("Skipping account backup rotation inside throttle window", {
385
+ path,
386
+ backupPath: latestBackupPath,
387
+ newestBackupAgeMs,
388
+ minIntervalMs,
389
+ });
390
+ return;
391
+ }
392
+ }
393
+ await createRotatingAccountsBackup(path);
394
+ }
395
+ /**
396
+ * Warn (once per storage path per process) when a save without a merge
397
+ * baseline is about to overwrite a primary this process did not last read or
398
+ * write — the cheap signature of "concurrent modification, no baseline"
399
+ * (last-writer-wins). The WeakMap baseline in `saveAccounts` cannot attach to
400
+ * raw `structuredClone` copies, so this is the residual guard for untracked
401
+ * objects. First-ever saves (no primary), empty primaries, and files whose
402
+ * content (or full stat tuple, when no hash was recorded) matches our last
403
+ * read/write stay quiet; the write is never blocked.
404
+ */
405
+ async function warnOnBaselinelessSave(path) {
406
+ if (baselinelessSaveWarnedPaths.has(path))
407
+ return;
408
+ // No primary on disk (or an unreadable one): nothing exists to silently
409
+ // lose, so the first-ever save stays quiet.
410
+ const stats = await fs.stat(path).catch(() => undefined);
411
+ if (!stats?.isFile() || stats.size === 0)
412
+ return;
413
+ const last = lastKnownPrimaryState.get(path);
414
+ if (last !== undefined) {
415
+ if (last.sha256 !== undefined) {
416
+ // Content-exact check: identical bytes mean nothing was lost to a
417
+ // foreign write regardless of timestamp granularity or an mtime-
418
+ // preserving writer, and different bytes mean one landed even when
419
+ // every stat field coincidentally matches.
420
+ const currentSha256 = await fs
421
+ .readFile(path, "utf-8")
422
+ .then((raw) => computeSha256(raw))
423
+ .catch(() => undefined);
424
+ if (currentSha256 !== undefined) {
425
+ if (currentSha256 === last.sha256)
426
+ return;
427
+ }
428
+ else if (primaryFingerprintMatches(last, stats)) {
429
+ // Content unreadable mid-check: fall back to the metadata tuple.
430
+ return;
431
+ }
432
+ }
433
+ else if (primaryFingerprintMatches(last, stats)) {
434
+ return;
435
+ }
436
+ }
437
+ baselinelessSaveWarnedPaths.add(path);
438
+ const message = "Saving account storage without a merge baseline over a primary file this process did not last read or write; a concurrent writer's changes may be overwritten (last-writer-wins). Load via loadAccounts() or clone via cloneTrackedAccountStorage() so the three-way merge can run.";
439
+ log.warn("Baseline-less account save over externally modified primary", {
440
+ path,
441
+ });
442
+ process.emitWarning(message, {
443
+ code: "CODEX_MULTI_AUTH_BASELINELESS_SAVE",
444
+ });
445
+ }
204
446
  function isRotatingBackupTempArtifact(storagePath, candidatePath) {
205
447
  const backupPrefix = `${storagePath}${ACCOUNTS_BACKUP_SUFFIX}`;
206
448
  if (!candidatePath.startsWith(backupPrefix) ||
@@ -467,7 +709,14 @@ export async function restoreAccountsFromBackup(path, options) {
467
709
  normalizeAccountStorage,
468
710
  isRecord,
469
711
  }),
470
- saveAccounts,
712
+ saveAccounts: async (storage) => {
713
+ // An explicit restore is a deliberate overwrite of the current
714
+ // primary, like the crash-recovery persists: record the file being
715
+ // replaced so the baseline-less-save guard neither misreports it as
716
+ // a foreign write nor spends the once-per-path warning on it.
717
+ notePrimaryState(getStoragePath(), await fs.stat(getStoragePath()).catch(() => undefined));
718
+ await saveAccounts(storage);
719
+ },
471
720
  });
472
721
  }
473
722
  export async function exportNamedBackup(name, options) {
@@ -508,15 +757,31 @@ async function migrateLegacyProjectStorageIfNeeded(options) {
508
757
  if (existingCandidatePaths.length === 0) {
509
758
  return null;
510
759
  }
511
- const loadCurrentStorageForMigration = async () => loadNormalizedStorageFromPath(currentStoragePath, "current account storage", {
512
- loadAccountsFromPath: (path) => loadAccountsFromPath(path, {
513
- normalizeAccountStorage,
514
- isRecord,
515
- }),
516
- logWarn: (message, details) => {
517
- log.warn(message, details);
518
- },
519
- });
760
+ const loadCurrentStorageForMigration = async () => {
761
+ // Stat before the read so a migration persist of the merged content
762
+ // counts as a state this process knows (baseline-less-save guard).
763
+ // Recorded even when the parse fails: a migration persist that
764
+ // replaces an unreadable current file is a deliberate overwrite, not
765
+ // a foreign modification to warn about.
766
+ let stats;
767
+ try {
768
+ stats = await fs.stat(currentStoragePath);
769
+ }
770
+ catch {
771
+ stats = undefined;
772
+ }
773
+ const storage = await loadNormalizedStorageFromPath(currentStoragePath, "current account storage", {
774
+ loadAccountsFromPath: (path) => loadAccountsFromPath(path, {
775
+ normalizeAccountStorage,
776
+ isRecord,
777
+ }),
778
+ logWarn: (message, details) => {
779
+ log.warn(message, details);
780
+ },
781
+ });
782
+ notePrimaryState(currentStoragePath, stats);
783
+ return storage;
784
+ };
520
785
  const readLiveCurrentStorageIfExportMode = async () => {
521
786
  if (commit || !existsSync(currentStoragePath)) {
522
787
  return { exists: false, storage: null };
@@ -755,17 +1020,289 @@ export function resolveAccountSelectionIndex(accounts, candidate, fallbackIndex
755
1020
  return matchedIndex;
756
1021
  return clampIndex(fallbackIndex, accounts.length);
757
1022
  }
1023
+ /**
1024
+ * Total "newest" order over surviving entries, identical to folding
1025
+ * selectNewestAccount left-to-right over the survivor array: higher
1026
+ * `lastUsed` wins, then higher `addedAt`, then — the fold's behavior on a
1027
+ * full tie — the later array position. Reducing the fold to one comparison
1028
+ * over (lastUsed, addedAt, index) is what lets each identity lookup be
1029
+ * answered from an incrementally maintained index instead of a linear scan.
1030
+ */
1031
+ function compareDedupIndexEntries(a, b) {
1032
+ if (a.lastUsed !== b.lastUsed)
1033
+ return a.lastUsed - b.lastUsed;
1034
+ if (a.addedAt !== b.addedAt)
1035
+ return a.addedAt - b.addedAt;
1036
+ return a.index - b.index;
1037
+ }
1038
+ /**
1039
+ * Incremental "newest surviving owner" index for one identity facet value
1040
+ * (a composite accountId+email pair, an email key, or a refresh token).
1041
+ * Survivor slots join when appended, refresh their freshness when a
1042
+ * newest-wins merge replaces the account stored in the slot, and leave when
1043
+ * a replacement drops the facet. A max-heap with lazy deletion answers the
1044
+ * argmax query in amortized O(log n): stale heap entries are popped on
1045
+ * first sight, so every join/update/leave is paid for exactly once.
1046
+ */
1047
+ class DedupNewestIndex {
1048
+ liveEntries = new Map();
1049
+ heap = [];
1050
+ get size() {
1051
+ return this.liveEntries.size;
1052
+ }
1053
+ set(index, account) {
1054
+ const entry = {
1055
+ index,
1056
+ lastUsed: account.lastUsed || 0,
1057
+ addedAt: account.addedAt || 0,
1058
+ };
1059
+ this.liveEntries.set(index, entry);
1060
+ const heap = this.heap;
1061
+ let slot = heap.length;
1062
+ heap.push(entry);
1063
+ while (slot > 0) {
1064
+ const parent = (slot - 1) >> 1;
1065
+ const parentEntry = heap[parent];
1066
+ if (parentEntry === undefined ||
1067
+ compareDedupIndexEntries(parentEntry, entry) >= 0) {
1068
+ break;
1069
+ }
1070
+ heap[slot] = parentEntry;
1071
+ slot = parent;
1072
+ }
1073
+ heap[slot] = entry;
1074
+ }
1075
+ delete(index) {
1076
+ this.liveEntries.delete(index);
1077
+ }
1078
+ newestIndex() {
1079
+ const heap = this.heap;
1080
+ while (heap.length > 0) {
1081
+ const top = heap[0];
1082
+ if (top !== undefined && this.liveEntries.get(top.index) === top) {
1083
+ return top.index;
1084
+ }
1085
+ this.dropHeapTop();
1086
+ }
1087
+ return undefined;
1088
+ }
1089
+ dropHeapTop() {
1090
+ const heap = this.heap;
1091
+ const last = heap.pop();
1092
+ if (last === undefined || heap.length === 0)
1093
+ return;
1094
+ heap[0] = last;
1095
+ let slot = 0;
1096
+ for (;;) {
1097
+ const left = slot * 2 + 1;
1098
+ const right = left + 1;
1099
+ let best = slot;
1100
+ let bestEntry = heap[best] ?? last;
1101
+ const leftEntry = left < heap.length ? heap[left] : undefined;
1102
+ if (leftEntry !== undefined &&
1103
+ compareDedupIndexEntries(leftEntry, bestEntry) > 0) {
1104
+ best = left;
1105
+ bestEntry = leftEntry;
1106
+ }
1107
+ const rightEntry = right < heap.length ? heap[right] : undefined;
1108
+ if (rightEntry !== undefined &&
1109
+ compareDedupIndexEntries(rightEntry, bestEntry) > 0) {
1110
+ best = right;
1111
+ }
1112
+ if (best === slot)
1113
+ break;
1114
+ const slotEntry = heap[slot];
1115
+ const bestValue = heap[best];
1116
+ if (bestValue !== undefined)
1117
+ heap[slot] = bestValue;
1118
+ if (slotEntry !== undefined)
1119
+ heap[best] = slotEntry;
1120
+ slot = best;
1121
+ }
1122
+ }
1123
+ }
1124
+ function bumpIdentityCount(counts, key, delta) {
1125
+ if (!key)
1126
+ return;
1127
+ const next = (counts.get(key) ?? 0) + delta;
1128
+ if (next <= 0) {
1129
+ counts.delete(key);
1130
+ }
1131
+ else {
1132
+ counts.set(key, next);
1133
+ }
1134
+ }
1135
+ function dedupCompositeKey(ref) {
1136
+ if (!ref.accountId || !ref.emailKey)
1137
+ return undefined;
1138
+ // JSON encoding keeps the pair unambiguous even for adversarial strings.
1139
+ return JSON.stringify([ref.accountId, ref.emailKey]);
1140
+ }
1141
+ /**
1142
+ * One dedup pass in O(n log n): every candidate is matched against the
1143
+ * surviving array through four incrementally maintained indexes (composite
1144
+ * accountId+email, email, refresh token, accountId) that apply the same
1145
+ * tier precedence and vetoes as the linear findMatchingAccountIndex scans
1146
+ * they replace. A newest-wins replacement re-keys every index the loser
1147
+ * owned, so the indexes always describe the current survivor array.
1148
+ */
758
1149
  function deduplicateAccountsByIdentityPass(accounts) {
759
1150
  const deduplicated = [];
1151
+ const refs = [];
1152
+ const compositeIndexes = new Map();
1153
+ const emailIndexes = new Map();
1154
+ const refreshIndexes = new Map();
1155
+ const accountIdOwners = new Map();
1156
+ const addToIndexes = (index, account, ref) => {
1157
+ const compositeKey = dedupCompositeKey(ref);
1158
+ if (compositeKey !== undefined) {
1159
+ let newest = compositeIndexes.get(compositeKey);
1160
+ if (!newest) {
1161
+ newest = new DedupNewestIndex();
1162
+ compositeIndexes.set(compositeKey, newest);
1163
+ }
1164
+ newest.set(index, account);
1165
+ }
1166
+ if (ref.emailKey !== undefined) {
1167
+ let bucket = emailIndexes.get(ref.emailKey);
1168
+ if (!bucket) {
1169
+ bucket = { newest: new DedupNewestIndex(), accountIds: new Map() };
1170
+ emailIndexes.set(ref.emailKey, bucket);
1171
+ }
1172
+ bucket.newest.set(index, account);
1173
+ bumpIdentityCount(bucket.accountIds, ref.accountId, 1);
1174
+ }
1175
+ if (ref.refreshToken !== undefined) {
1176
+ let bucket = refreshIndexes.get(ref.refreshToken);
1177
+ if (!bucket) {
1178
+ bucket = {
1179
+ newest: new DedupNewestIndex(),
1180
+ accountIds: new Map(),
1181
+ emailKeys: new Map(),
1182
+ };
1183
+ refreshIndexes.set(ref.refreshToken, bucket);
1184
+ }
1185
+ bucket.newest.set(index, account);
1186
+ bumpIdentityCount(bucket.accountIds, ref.accountId, 1);
1187
+ bumpIdentityCount(bucket.emailKeys, ref.emailKey, 1);
1188
+ }
1189
+ if (ref.accountId !== undefined) {
1190
+ let owners = accountIdOwners.get(ref.accountId);
1191
+ if (!owners) {
1192
+ owners = new Set();
1193
+ accountIdOwners.set(ref.accountId, owners);
1194
+ }
1195
+ owners.add(index);
1196
+ }
1197
+ };
1198
+ const removeFromIndexes = (index, ref) => {
1199
+ const compositeKey = dedupCompositeKey(ref);
1200
+ if (compositeKey !== undefined) {
1201
+ compositeIndexes.get(compositeKey)?.delete(index);
1202
+ }
1203
+ if (ref.emailKey !== undefined) {
1204
+ const bucket = emailIndexes.get(ref.emailKey);
1205
+ if (bucket) {
1206
+ bucket.newest.delete(index);
1207
+ bumpIdentityCount(bucket.accountIds, ref.accountId, -1);
1208
+ }
1209
+ }
1210
+ if (ref.refreshToken !== undefined) {
1211
+ const bucket = refreshIndexes.get(ref.refreshToken);
1212
+ if (bucket) {
1213
+ bucket.newest.delete(index);
1214
+ bumpIdentityCount(bucket.accountIds, ref.accountId, -1);
1215
+ bumpIdentityCount(bucket.emailKeys, ref.emailKey, -1);
1216
+ }
1217
+ }
1218
+ if (ref.accountId !== undefined) {
1219
+ accountIdOwners.get(ref.accountId)?.delete(index);
1220
+ }
1221
+ };
760
1222
  for (const account of accounts) {
761
1223
  if (!account)
762
1224
  continue;
763
- const existingIndex = findMatchingAccountIndex(deduplicated, account);
1225
+ const candidateRef = toAccountIdentityRef(account);
1226
+ let existingIndex;
1227
+ // Tier 1: composite accountId + email match.
1228
+ if (candidateRef.accountId && candidateRef.emailKey) {
1229
+ const compositeKey = dedupCompositeKey(candidateRef);
1230
+ if (compositeKey !== undefined) {
1231
+ existingIndex = compositeIndexes.get(compositeKey)?.newestIndex();
1232
+ }
1233
+ }
1234
+ // Tier 2: safe email match. Refuse when the email's owners plus the
1235
+ // candidate carry more than one distinct accountId.
1236
+ if (existingIndex === undefined && candidateRef.emailKey) {
1237
+ const bucket = emailIndexes.get(candidateRef.emailKey);
1238
+ if (bucket && bucket.newest.size > 0) {
1239
+ const distinctAccountIds = bucket.accountIds.size +
1240
+ (candidateRef.accountId &&
1241
+ !bucket.accountIds.has(candidateRef.accountId)
1242
+ ? 1
1243
+ : 0);
1244
+ if (distinctAccountIds <= 1) {
1245
+ existingIndex = bucket.newest.newestIndex();
1246
+ }
1247
+ }
1248
+ }
1249
+ // Tier 3: compatible refresh-token match. Refuse on an owner whose
1250
+ // accountId or emailKey conflicts with the candidate's, and on an
1251
+ // ambiguous refresh-only candidate with more than one owner.
1252
+ if (existingIndex === undefined && candidateRef.refreshToken) {
1253
+ const bucket = refreshIndexes.get(candidateRef.refreshToken);
1254
+ if (bucket && bucket.newest.size > 0) {
1255
+ const accountIdConflict = candidateRef.accountId !== undefined &&
1256
+ (bucket.accountIds.size > 1 ||
1257
+ (bucket.accountIds.size === 1 &&
1258
+ !bucket.accountIds.has(candidateRef.accountId)));
1259
+ const emailKeyConflict = candidateRef.emailKey !== undefined &&
1260
+ (bucket.emailKeys.size > 1 ||
1261
+ (bucket.emailKeys.size === 1 &&
1262
+ !bucket.emailKeys.has(candidateRef.emailKey)));
1263
+ const ambiguous = candidateRef.accountId === undefined &&
1264
+ candidateRef.emailKey === undefined &&
1265
+ bucket.newest.size > 1;
1266
+ if (!accountIdConflict && !emailKeyConflict && !ambiguous) {
1267
+ existingIndex = bucket.newest.newestIndex();
1268
+ }
1269
+ }
1270
+ }
1271
+ // Tier 4: unique-accountId fallback. The dedup pass runs
1272
+ // findMatchingAccountIndex with default options, so the fallback is
1273
+ // only reachable for candidates that also carry an email key; a sole
1274
+ // owner still loses on a conflicting email key.
1275
+ if (existingIndex === undefined &&
1276
+ candidateRef.accountId &&
1277
+ candidateRef.emailKey) {
1278
+ const owners = accountIdOwners.get(candidateRef.accountId);
1279
+ if (owners && owners.size === 1) {
1280
+ const only = owners.values().next();
1281
+ if (!only.done) {
1282
+ const ownerEmailKey = refs[only.value]?.emailKey;
1283
+ if (ownerEmailKey === undefined ||
1284
+ ownerEmailKey === candidateRef.emailKey) {
1285
+ existingIndex = only.value;
1286
+ }
1287
+ }
1288
+ }
1289
+ }
764
1290
  if (existingIndex === undefined) {
1291
+ const index = deduplicated.length;
765
1292
  deduplicated.push(account);
1293
+ refs.push(candidateRef);
1294
+ addToIndexes(index, account, candidateRef);
766
1295
  continue;
767
1296
  }
768
- deduplicated[existingIndex] = selectNewestAccount(deduplicated[existingIndex], account);
1297
+ const current = deduplicated[existingIndex];
1298
+ if (selectNewestAccount(current, account) === current)
1299
+ continue;
1300
+ deduplicated[existingIndex] = account;
1301
+ const previousRef = refs[existingIndex];
1302
+ refs[existingIndex] = candidateRef;
1303
+ if (previousRef)
1304
+ removeFromIndexes(existingIndex, previousRef);
1305
+ addToIndexes(existingIndex, account, candidateRef);
769
1306
  }
770
1307
  return deduplicated;
771
1308
  }
@@ -1266,11 +1803,25 @@ async function loadAccountsInternal(persistMigration) {
1266
1803
  const migratedLegacyStorage = persistMigration
1267
1804
  ? await migrateLegacyProjectStorageIfNeeded({ persist: persistMigration })
1268
1805
  : null;
1806
+ // Stat BEFORE the primary read: this fingerprint is the state this process
1807
+ // can claim to know. A writer racing the read makes the record stale, which
1808
+ // is the safe direction for the baseline-less-save guard — never claim
1809
+ // knowledge of content we may not have parsed.
1810
+ let primaryStatsBeforeRead;
1811
+ try {
1812
+ primaryStatsBeforeRead = await fs.stat(path);
1813
+ }
1814
+ catch {
1815
+ primaryStatsBeforeRead = undefined;
1816
+ }
1269
1817
  try {
1270
- const { normalized, storedVersion, schemaErrors } = await loadAccountsFromPath(path, {
1818
+ const { normalized, storedVersion, schemaErrors, contentSha256 } = await loadAccountsFromPath(path, {
1271
1819
  normalizeAccountStorage,
1272
1820
  isRecord,
1273
1821
  });
1822
+ if (normalized) {
1823
+ notePrimaryState(path, primaryStatsBeforeRead, contentSha256);
1824
+ }
1274
1825
  if (schemaErrors.length > 0) {
1275
1826
  log.warn("Account storage schema validation warnings", {
1276
1827
  errors: schemaErrors.slice(0, 5),
@@ -1348,6 +1899,12 @@ async function loadAccountsInternal(persistMigration) {
1348
1899
  return normalized;
1349
1900
  }
1350
1901
  catch (error) {
1902
+ // The primary could not be parsed (or read). Whatever file we stat'd is
1903
+ // the state the WAL/backup recovery persists below deliberately replace,
1904
+ // so record it as known — otherwise those intentional recovery saves
1905
+ // would emit the baseline-less-overwrite warning and spend the
1906
+ // once-per-path guard meant for genuine foreign writes.
1907
+ notePrimaryState(path, primaryStatsBeforeRead);
1351
1908
  const code = error.code;
1352
1909
  if (existsSync(resetMarkerPath)) {
1353
1910
  return createEmptyStorageWithMetadata(false, "intentional-reset");
@@ -1467,14 +2024,17 @@ async function loadAccountsForExport() {
1467
2024
  throw error;
1468
2025
  }
1469
2026
  }
1470
- async function saveAccountsUnlocked(storage) {
1471
- await saveAccountsUnlockedToDisk(storage);
1472
- // A persisted (or superseded) pending rotated credential is no longer needed.
1473
- await prunePendingAuth(getStoragePath(), storage).catch((error) => {
2027
+ async function saveAccountsUnlocked(storage, options) {
2028
+ await saveAccountsUnlockedToDisk(storage, options);
2029
+ // A persisted (or superseded) pending rotated credential is no longer needed —
2030
+ // unless a restorable backup/WAL snapshot still carries the token it replaces,
2031
+ // in which case the entry stays so a stale-backup recovery self-heals.
2032
+ const path = getStoragePath();
2033
+ await prunePendingAuth(path, storage, () => collectRestorableRefreshTokens(path)).catch((error) => {
1474
2034
  log.warn("Failed to prune pending rotated credentials", { code: typeof error.code === "string" && /^[A-Z_]{1,40}$/.test(error.code ?? "") ? error.code : "INVALID_PENDING_AUTH" });
1475
2035
  });
1476
2036
  }
1477
- async function saveAccountsUnlockedToDisk(storage) {
2037
+ async function saveAccountsUnlockedToDisk(storage, options) {
1478
2038
  const path = getStoragePath();
1479
2039
  const resetMarkerPath = getIntentionalResetMarkerPath(path);
1480
2040
  const walPath = getAccountsWalPath(path);
@@ -1513,7 +2073,7 @@ async function saveAccountsUnlockedToDisk(storage) {
1513
2073
  },
1514
2074
  }),
1515
2075
  createSyntheticFixtureError: () => new StorageError("Refusing to overwrite non-synthetic account storage with synthetic fixture payload", "EINVALID", path, "Detected synthetic fixture-like account payload. Use explicit account import/login commands instead."),
1516
- createRotatingAccountsBackup,
2076
+ createRotatingAccountsBackup: (targetPath) => rotateAccountsBackupIfDue(targetPath, storage.accounts, options?.expectedOnDiskAccounts),
1517
2077
  computeSha256,
1518
2078
  writeJournal: async (content, journalPath) => {
1519
2079
  const journalEntry = {
@@ -1571,8 +2131,9 @@ async function saveAccountsUnlockedToDisk(storage) {
1571
2131
  // Ignore cleanup failure.
1572
2132
  }
1573
2133
  },
1574
- onSaved: () => {
2134
+ onSaved: (stats, contentSha256) => {
1575
2135
  lastAccountsSaveTimestamp = Date.now();
2136
+ notePrimaryState(path, stats, contentSha256);
1576
2137
  },
1577
2138
  logWarn: (message, details) => {
1578
2139
  log.warn(message, details);
@@ -1648,14 +2209,18 @@ export async function withAccountAndFlaggedStorageTransaction(handler) {
1648
2209
  const baseline = loadedAccountSnapshots.get(accountStorage);
1649
2210
  const nextAccounts = cloneAccountStorageForPersistence(baseline ? mergeAccountSnapshot(baseline, state.snapshot, accountStorage) : accountStorage);
1650
2211
  const nextFlagged = cloneFlaggedStorageForPersistence(flaggedStorage);
1651
- await saveAccountsUnlocked(nextAccounts);
2212
+ await saveAccountsUnlocked(nextAccounts, {
2213
+ expectedOnDiskAccounts: state.snapshot?.accounts,
2214
+ });
1652
2215
  try {
1653
2216
  await saveFlaggedAccountsUnlocked(nextFlagged);
1654
2217
  state.snapshot = nextAccounts;
1655
2218
  }
1656
2219
  catch (error) {
1657
2220
  try {
1658
- await saveAccountsUnlocked(previousAccounts);
2221
+ await saveAccountsUnlocked(previousAccounts, {
2222
+ expectedOnDiskAccounts: state.snapshot?.accounts,
2223
+ });
1659
2224
  state.snapshot = previousAccounts;
1660
2225
  }
1661
2226
  catch (rollbackError) {
@@ -1709,7 +2274,21 @@ export async function withFlaggedStorageTransaction(handler) {
1709
2274
  * @param storage - Account storage data to save
1710
2275
  * @throws StorageError with platform-aware hints on failure
1711
2276
  */
1712
- /** Clone a loaded working snapshot without losing its optimistic-concurrency baseline. */
2277
+ /**
2278
+ * Clone a loaded working snapshot without losing its optimistic-concurrency baseline.
2279
+ *
2280
+ * Baseline contract: `saveAccounts` only runs the three-way merge
2281
+ * (`mergeAccountSnapshot` against a fresh `loadPrimaryAccountsForMerge` read)
2282
+ * when the saved object is registered in `loadedAccountSnapshots` — and that
2283
+ * WeakMap is keyed by OBJECT IDENTITY, so registration does not survive
2284
+ * `structuredClone`, `JSON.parse(JSON.stringify(...))`, spread copies, or
2285
+ * manual reconstruction. A baseline-less save writes the object as-is
2286
+ * (last-writer-wins): concurrent changes are neither merged nor flagged with
2287
+ * ESTALE, and when the primary on disk differs from the state this process
2288
+ * last read or wrote the save additionally emits a one-time baseline-less
2289
+ * overwrite warning. Use this helper — or mutate the loaded object in place —
2290
+ * to keep the merge armed.
2291
+ */
1713
2292
  export function cloneTrackedAccountStorage(storage) {
1714
2293
  const clone = structuredClone(storage);
1715
2294
  const baseline = loadedAccountSnapshots.get(storage);
@@ -1723,8 +2302,16 @@ export async function saveAccounts(storage) {
1723
2302
  withStorageLock,
1724
2303
  saveUnlocked: async (proposed) => {
1725
2304
  const baseline = loadedAccountSnapshots.get(proposed);
1726
- const snapshot = baseline ? mergeAccountSnapshot(baseline, await loadPrimaryAccountsForMerge(), proposed) : proposed;
1727
- await saveAccountsUnlocked(snapshot);
2305
+ const current = baseline ? await loadPrimaryAccountsForMerge() : null;
2306
+ if (!baseline) {
2307
+ // Without a baseline the three-way merge cannot run — this write is
2308
+ // last-writer-wins. Warn (once) if the primary changed underneath us.
2309
+ await warnOnBaselinelessSave(getStoragePath());
2310
+ }
2311
+ const snapshot = baseline ? mergeAccountSnapshot(baseline, current, proposed) : proposed;
2312
+ await saveAccountsUnlocked(snapshot, {
2313
+ expectedOnDiskAccounts: current?.accounts,
2314
+ });
1728
2315
  if (baseline) {
1729
2316
  for (const key of Object.keys(proposed))
1730
2317
  Reflect.deleteProperty(proposed, key);