@iislee/opencodex 2.11.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 (476) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/assets/architecture.png +0 -0
  4. package/assets/banner.png +0 -0
  5. package/assets/claude-code-models.gif +0 -0
  6. package/assets/codex-app-picker.png +0 -0
  7. package/bin/ocx.mjs +451 -0
  8. package/bin/package-main.mjs +9 -0
  9. package/gui/dist/assets/index-DTpMHS4F.js +67 -0
  10. package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
  11. package/gui/dist/favicon.png +0 -0
  12. package/gui/dist/icons.svg +24 -0
  13. package/gui/dist/index.html +25 -0
  14. package/gui/dist/logo.png +0 -0
  15. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  16. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/claude.svg +1 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/cursor.svg +2 -0
  25. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  26. package/gui/dist/provider-icons/discord.svg +1 -0
  27. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  28. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  29. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok-color.svg +1 -0
  34. package/gui/dist/provider-icons/grok.svg +1 -0
  35. package/gui/dist/provider-icons/groq-color.svg +1 -0
  36. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  37. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  38. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  39. package/gui/dist/provider-icons/kiro.svg +14 -0
  40. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  41. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  42. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  43. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  44. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  45. package/gui/dist/provider-icons/openai.svg +1 -0
  46. package/gui/dist/provider-icons/opencode.svg +1 -0
  47. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  48. package/gui/dist/provider-icons/pi.svg +21 -0
  49. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  50. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  51. package/gui/dist/provider-icons/telegram.svg +1 -0
  52. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  53. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  54. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  55. package/package.json +102 -0
  56. package/src/AGENTS.md +28 -0
  57. package/src/adapters/anthropic-image-guard.ts +251 -0
  58. package/src/adapters/anthropic-image-normalize.ts +518 -0
  59. package/src/adapters/anthropic.ts +1003 -0
  60. package/src/adapters/azure.ts +36 -0
  61. package/src/adapters/base.ts +72 -0
  62. package/src/adapters/client-fingerprint.ts +59 -0
  63. package/src/adapters/cursor/arg-codec.ts +38 -0
  64. package/src/adapters/cursor/arg-normalize.ts +104 -0
  65. package/src/adapters/cursor/cursor-errors.ts +165 -0
  66. package/src/adapters/cursor/discovery.ts +276 -0
  67. package/src/adapters/cursor/effort-map.ts +127 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +211 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/kv-store.ts +52 -0
  72. package/src/adapters/cursor/live-models.ts +153 -0
  73. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  74. package/src/adapters/cursor/live-transport.ts +1214 -0
  75. package/src/adapters/cursor/mcp-config.ts +42 -0
  76. package/src/adapters/cursor/mcp-manager.ts +333 -0
  77. package/src/adapters/cursor/message-mapper.ts +49 -0
  78. package/src/adapters/cursor/native-exec-common.ts +55 -0
  79. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  80. package/src/adapters/cursor/native-exec-fs.ts +329 -0
  81. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  82. package/src/adapters/cursor/native-exec-network.ts +43 -0
  83. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  84. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  85. package/src/adapters/cursor/native-exec.ts +576 -0
  86. package/src/adapters/cursor/protobuf-events.ts +563 -0
  87. package/src/adapters/cursor/protobuf-request.ts +714 -0
  88. package/src/adapters/cursor/request-builder.ts +255 -0
  89. package/src/adapters/cursor/thread-continuity.ts +67 -0
  90. package/src/adapters/cursor/tool-definitions.ts +505 -0
  91. package/src/adapters/cursor/transport-retry.ts +132 -0
  92. package/src/adapters/cursor/transport.ts +57 -0
  93. package/src/adapters/cursor/types.ts +52 -0
  94. package/src/adapters/cursor.ts +196 -0
  95. package/src/adapters/google-antigravity-replay.ts +303 -0
  96. package/src/adapters/google-antigravity-wire.ts +108 -0
  97. package/src/adapters/google-errors.ts +85 -0
  98. package/src/adapters/google-http.ts +100 -0
  99. package/src/adapters/google-tool-schema.ts +173 -0
  100. package/src/adapters/google-truncation.ts +13 -0
  101. package/src/adapters/google-wire-compiler.ts +232 -0
  102. package/src/adapters/google.ts +758 -0
  103. package/src/adapters/identity.ts +44 -0
  104. package/src/adapters/image.ts +23 -0
  105. package/src/adapters/kiro-constants.ts +16 -0
  106. package/src/adapters/kiro-errors.ts +197 -0
  107. package/src/adapters/kiro-events.ts +179 -0
  108. package/src/adapters/kiro-images.ts +129 -0
  109. package/src/adapters/kiro-retry.ts +312 -0
  110. package/src/adapters/kiro-thinking.ts +96 -0
  111. package/src/adapters/kiro-tool-fallback.ts +36 -0
  112. package/src/adapters/kiro-tools.ts +215 -0
  113. package/src/adapters/kiro-truncation.ts +33 -0
  114. package/src/adapters/kiro-wire.ts +129 -0
  115. package/src/adapters/kiro.ts +1898 -0
  116. package/src/adapters/mimo-free.ts +263 -0
  117. package/src/adapters/openai-chat.ts +1005 -0
  118. package/src/adapters/openai-responses.ts +1137 -0
  119. package/src/adapters/run-turn-queue.ts +114 -0
  120. package/src/adapters/tool-catalog-nudge.ts +71 -0
  121. package/src/adapters/upstream-http-error.ts +48 -0
  122. package/src/bridge.ts +1619 -0
  123. package/src/chat/inbound.ts +295 -0
  124. package/src/chat/outbound.ts +765 -0
  125. package/src/claude/agents-inject.ts +243 -0
  126. package/src/claude/alias.ts +149 -0
  127. package/src/claude/auth-detect.ts +229 -0
  128. package/src/claude/auth-mode-migration.ts +32 -0
  129. package/src/claude/auth-mode.ts +62 -0
  130. package/src/claude/context-windows.ts +189 -0
  131. package/src/claude/desktop-3p-guard.ts +35 -0
  132. package/src/claude/desktop-3p-paths.ts +84 -0
  133. package/src/claude/desktop-3p.ts +381 -0
  134. package/src/claude/desktop-health.ts +26 -0
  135. package/src/claude/desktop-profile.ts +263 -0
  136. package/src/claude/gateway-cache.ts +70 -0
  137. package/src/claude/inbound-debug.ts +163 -0
  138. package/src/claude/inbound.ts +509 -0
  139. package/src/claude/model-info.ts +151 -0
  140. package/src/claude/outbound.ts +872 -0
  141. package/src/cli/access.ts +108 -0
  142. package/src/cli/account-api.ts +268 -0
  143. package/src/cli/account-auth.ts +223 -0
  144. package/src/cli/account-extended.ts +350 -0
  145. package/src/cli/account.ts +275 -0
  146. package/src/cli/agent-driven.ts +70 -0
  147. package/src/cli/agent.ts +184 -0
  148. package/src/cli/catalog-prewarm.ts +27 -0
  149. package/src/cli/claude-desktop.ts +188 -0
  150. package/src/cli/claude.ts +286 -0
  151. package/src/cli/codex-shim-autorestore.ts +45 -0
  152. package/src/cli/combo.ts +119 -0
  153. package/src/cli/config-command.ts +145 -0
  154. package/src/cli/debug.ts +228 -0
  155. package/src/cli/doctor.ts +930 -0
  156. package/src/cli/export-command.ts +187 -0
  157. package/src/cli/help.ts +354 -0
  158. package/src/cli/index.ts +1113 -0
  159. package/src/cli/init.ts +224 -0
  160. package/src/cli/integrations.ts +142 -0
  161. package/src/cli/interactive-confirm.ts +133 -0
  162. package/src/cli/internal-dispatch.ts +20 -0
  163. package/src/cli/models-runtime.ts +212 -0
  164. package/src/cli/models.ts +336 -0
  165. package/src/cli/observe.ts +117 -0
  166. package/src/cli/opencode.ts +586 -0
  167. package/src/cli/pi.ts +188 -0
  168. package/src/cli/provider-runtime.ts +162 -0
  169. package/src/cli/provider.ts +463 -0
  170. package/src/cli/runtime-api.ts +325 -0
  171. package/src/cli/star-prompt.ts +155 -0
  172. package/src/cli/status-oauth.ts +78 -0
  173. package/src/cli/status.ts +321 -0
  174. package/src/cli/sync-cloud.ts +283 -0
  175. package/src/cli/system-command.ts +112 -0
  176. package/src/cli/tray-proxy.ts +52 -0
  177. package/src/cli/v2.ts +173 -0
  178. package/src/cli.ts +10 -0
  179. package/src/clients/config-export.ts +377 -0
  180. package/src/clients/effective-status.ts +385 -0
  181. package/src/clients/probes/agy.ts +55 -0
  182. package/src/clients/probes/cc-switch.ts +110 -0
  183. package/src/clients/probes/claude.ts +90 -0
  184. package/src/clients/probes/codex.ts +125 -0
  185. package/src/clients/probes/grok.ts +29 -0
  186. package/src/clients/probes/opencode.ts +109 -0
  187. package/src/clients/probes/paseo.ts +55 -0
  188. package/src/clients/probes/pi.ts +55 -0
  189. package/src/cloud/onedrive-auth.ts +666 -0
  190. package/src/cloud/onedrive-graph.ts +108 -0
  191. package/src/cloud/settings.ts +75 -0
  192. package/src/cloud/sync.ts +212 -0
  193. package/src/cloud/types.ts +56 -0
  194. package/src/cloud/vault.ts +89 -0
  195. package/src/codex/account-id.ts +34 -0
  196. package/src/codex/account-label.ts +34 -0
  197. package/src/codex/account-lifecycle.ts +55 -0
  198. package/src/codex/account-namespace-match.ts +63 -0
  199. package/src/codex/account-namespaces.ts +149 -0
  200. package/src/codex/account-pause.ts +20 -0
  201. package/src/codex/account-runtime-state.ts +31 -0
  202. package/src/codex/account-store.ts +517 -0
  203. package/src/codex/account-usability.ts +20 -0
  204. package/src/codex/app-server-processes.ts +756 -0
  205. package/src/codex/auth-api.ts +1540 -0
  206. package/src/codex/auth-collision.ts +107 -0
  207. package/src/codex/auth-context.ts +352 -0
  208. package/src/codex/autostart-health.ts +149 -0
  209. package/src/codex/catalog/aggregation.ts +378 -0
  210. package/src/codex/catalog/bundled.ts +251 -0
  211. package/src/codex/catalog/effort.ts +355 -0
  212. package/src/codex/catalog/metadata.ts +180 -0
  213. package/src/codex/catalog/parsing.ts +456 -0
  214. package/src/codex/catalog/provider-fetch.ts +902 -0
  215. package/src/codex/catalog/sync.ts +620 -0
  216. package/src/codex/catalog.ts +12 -0
  217. package/src/codex/data/upstream-models.json +830 -0
  218. package/src/codex/exec-invocation.ts +22 -0
  219. package/src/codex/features.ts +969 -0
  220. package/src/codex/history-migration-guardian.ts +102 -0
  221. package/src/codex/history-provider.ts +776 -0
  222. package/src/codex/home.ts +206 -0
  223. package/src/codex/inject.ts +799 -0
  224. package/src/codex/injected-marker.ts +72 -0
  225. package/src/codex/journal.ts +163 -0
  226. package/src/codex/main-account-cache.ts +32 -0
  227. package/src/codex/main-account.ts +40 -0
  228. package/src/codex/model-cache.ts +227 -0
  229. package/src/codex/paths.ts +65 -0
  230. package/src/codex/plugins-doctor.ts +242 -0
  231. package/src/codex/pool-rotation.ts +225 -0
  232. package/src/codex/project-config-warnings.ts +411 -0
  233. package/src/codex/quota.ts +411 -0
  234. package/src/codex/refresh.ts +53 -0
  235. package/src/codex/routing.ts +1477 -0
  236. package/src/codex/runtime.ts +538 -0
  237. package/src/codex/shim.ts +1189 -0
  238. package/src/codex/subagent-defaults.ts +550 -0
  239. package/src/codex/subagent-model-fallback.ts +469 -0
  240. package/src/codex/sync.ts +130 -0
  241. package/src/codex/warmup.ts +192 -0
  242. package/src/codex/websocket-registry.ts +100 -0
  243. package/src/combos/failover.ts +140 -0
  244. package/src/combos/index.ts +41 -0
  245. package/src/combos/request.ts +62 -0
  246. package/src/combos/resolve.ts +232 -0
  247. package/src/combos/types.ts +326 -0
  248. package/src/config.ts +2356 -0
  249. package/src/generated/jawcode-model-metadata.ts +104 -0
  250. package/src/github/star-state.ts +203 -0
  251. package/src/grok/inject.ts +545 -0
  252. package/src/grok/status.ts +121 -0
  253. package/src/grok/sync.ts +103 -0
  254. package/src/grok/usage-hook/report.mjs +348 -0
  255. package/src/grok/usage-hook.ts +278 -0
  256. package/src/images/artifacts.ts +516 -0
  257. package/src/images/fulfill-video.ts +163 -0
  258. package/src/images/fulfill.ts +149 -0
  259. package/src/images/index.ts +4 -0
  260. package/src/images/loop.ts +829 -0
  261. package/src/images/plan.ts +133 -0
  262. package/src/images/synthetic-tool.ts +133 -0
  263. package/src/images/types.ts +41 -0
  264. package/src/images/xai-client.ts +141 -0
  265. package/src/images/xai-video-client.ts +163 -0
  266. package/src/index.ts +22 -0
  267. package/src/lib/abort.ts +146 -0
  268. package/src/lib/admin-secrets.ts +25 -0
  269. package/src/lib/admission.ts +83 -0
  270. package/src/lib/app-owned-memory-stores.ts +173 -0
  271. package/src/lib/app-owned-memory.ts +265 -0
  272. package/src/lib/bounded-body.ts +202 -0
  273. package/src/lib/bun-binary-validator.d.mts +3 -0
  274. package/src/lib/bun-binary-validator.mjs +18 -0
  275. package/src/lib/bun-runtime.ts +71 -0
  276. package/src/lib/bun-stream-caps.ts +126 -0
  277. package/src/lib/config-ownership.ts +360 -0
  278. package/src/lib/crash-guard.ts +344 -0
  279. package/src/lib/debug-log-buffer.ts +83 -0
  280. package/src/lib/debug-settings.ts +108 -0
  281. package/src/lib/debug.ts +31 -0
  282. package/src/lib/destination-policy.ts +316 -0
  283. package/src/lib/errors.ts +364 -0
  284. package/src/lib/eventstream-decoder.ts +253 -0
  285. package/src/lib/gcp-adc.ts +341 -0
  286. package/src/lib/injection-debug-log.ts +58 -0
  287. package/src/lib/open-url.ts +25 -0
  288. package/src/lib/pinned-http.ts +151 -0
  289. package/src/lib/privacy.ts +20 -0
  290. package/src/lib/process-control.ts +165 -0
  291. package/src/lib/provider-outbound.ts +170 -0
  292. package/src/lib/provider-url.ts +14 -0
  293. package/src/lib/proxy-env.ts +18 -0
  294. package/src/lib/redact.ts +105 -0
  295. package/src/lib/retry-after.ts +55 -0
  296. package/src/lib/service-secrets.ts +25 -0
  297. package/src/lib/shadow-call.ts +30 -0
  298. package/src/lib/sidecar-tracker.ts +52 -0
  299. package/src/lib/sse-decoder.ts +323 -0
  300. package/src/lib/state-store-registrations.ts +109 -0
  301. package/src/lib/state-store-sweeper.ts +184 -0
  302. package/src/lib/test-home-guard.ts +90 -0
  303. package/src/lib/token-estimate.ts +69 -0
  304. package/src/lib/translator-budget.ts +356 -0
  305. package/src/lib/upstream-retry.ts +239 -0
  306. package/src/lib/win-exec.ts +115 -0
  307. package/src/lib/win-paths.ts +68 -0
  308. package/src/lib/windows-elevation.ts +705 -0
  309. package/src/lib/windows-secret-acl.ts +514 -0
  310. package/src/lib/winsw.ts +375 -0
  311. package/src/oauth/anthropic-routing.ts +594 -0
  312. package/src/oauth/anthropic.ts +177 -0
  313. package/src/oauth/callback-server.ts +294 -0
  314. package/src/oauth/chatgpt.ts +150 -0
  315. package/src/oauth/cursor.ts +211 -0
  316. package/src/oauth/github-copilot.ts +428 -0
  317. package/src/oauth/google-antigravity.ts +230 -0
  318. package/src/oauth/health.ts +399 -0
  319. package/src/oauth/index.ts +1174 -0
  320. package/src/oauth/key-providers.ts +108 -0
  321. package/src/oauth/kimi.ts +213 -0
  322. package/src/oauth/kiro-credentials.ts +726 -0
  323. package/src/oauth/kiro.ts +577 -0
  324. package/src/oauth/local-token-detect.ts +121 -0
  325. package/src/oauth/log.ts +48 -0
  326. package/src/oauth/login-cli.ts +163 -0
  327. package/src/oauth/pkce.ts +15 -0
  328. package/src/oauth/store.ts +630 -0
  329. package/src/oauth/token-guardian.ts +303 -0
  330. package/src/oauth/types.ts +62 -0
  331. package/src/oauth/xai.ts +241 -0
  332. package/src/pi/extensions.ts +72 -0
  333. package/src/pi/home.ts +42 -0
  334. package/src/pi/index.ts +40 -0
  335. package/src/pi/models.ts +278 -0
  336. package/src/pi/packages.ts +219 -0
  337. package/src/pi/settings.ts +365 -0
  338. package/src/pi/status.ts +68 -0
  339. package/src/pi/sync.ts +75 -0
  340. package/src/providers/alibaba-region-backup.ts +75 -0
  341. package/src/providers/alibaba-region-migration.ts +156 -0
  342. package/src/providers/alibaba-region-startup.ts +36 -0
  343. package/src/providers/antigravity-models.ts +205 -0
  344. package/src/providers/api-keys.ts +140 -0
  345. package/src/providers/base-url-choices.ts +64 -0
  346. package/src/providers/context-cap.ts +65 -0
  347. package/src/providers/derive.ts +339 -0
  348. package/src/providers/free-directory.ts +184 -0
  349. package/src/providers/github-copilot-transport.ts +56 -0
  350. package/src/providers/key-failover.ts +203 -0
  351. package/src/providers/kiro-models.ts +67 -0
  352. package/src/providers/label.ts +19 -0
  353. package/src/providers/model-discovery.ts +356 -0
  354. package/src/providers/openai-sidecar.ts +175 -0
  355. package/src/providers/openai-tier-startup.ts +27 -0
  356. package/src/providers/openai-tiers.ts +301 -0
  357. package/src/providers/openai-virtual-models.ts +82 -0
  358. package/src/providers/openrouter-routing.ts +102 -0
  359. package/src/providers/provider-id-rewrite.ts +150 -0
  360. package/src/providers/quota.ts +1260 -0
  361. package/src/providers/registry.ts +1600 -0
  362. package/src/providers/slug-codec.ts +67 -0
  363. package/src/providers/xai-transport.ts +141 -0
  364. package/src/reasoning-effort.ts +135 -0
  365. package/src/responses/compaction.ts +117 -0
  366. package/src/responses/parser.ts +656 -0
  367. package/src/responses/reasoning-envelope.ts +52 -0
  368. package/src/responses/schema.ts +159 -0
  369. package/src/responses/spill-store.ts +394 -0
  370. package/src/responses/state.ts +895 -0
  371. package/src/responses/tool-groups.ts +19 -0
  372. package/src/router.ts +425 -0
  373. package/src/server/adapter-resolve.ts +80 -0
  374. package/src/server/auth-cors.ts +530 -0
  375. package/src/server/chat-completions.ts +368 -0
  376. package/src/server/claude-messages.ts +914 -0
  377. package/src/server/effort-policy.ts +172 -0
  378. package/src/server/gui-static.ts +123 -0
  379. package/src/server/image-retry.ts +42 -0
  380. package/src/server/images.ts +476 -0
  381. package/src/server/index.ts +1126 -0
  382. package/src/server/lifecycle.ts +227 -0
  383. package/src/server/live.ts +598 -0
  384. package/src/server/management/agent-settings-routes.ts +1169 -0
  385. package/src/server/management/api-access.ts +141 -0
  386. package/src/server/management/api-key-usage.ts +167 -0
  387. package/src/server/management/body.ts +35 -0
  388. package/src/server/management/clients-routes.ts +63 -0
  389. package/src/server/management/cloud-sync-routes.ts +266 -0
  390. package/src/server/management/combo-routes.ts +220 -0
  391. package/src/server/management/config-routes.ts +422 -0
  392. package/src/server/management/context.ts +31 -0
  393. package/src/server/management/logs-usage-routes.ts +707 -0
  394. package/src/server/management/model-routes.ts +525 -0
  395. package/src/server/management/oauth-account-routes.ts +563 -0
  396. package/src/server/management/provider-routes.ts +556 -0
  397. package/src/server/management/shared.ts +277 -0
  398. package/src/server/management/sidebar-routes.ts +90 -0
  399. package/src/server/management/system-restart.ts +179 -0
  400. package/src/server/management/system-routes.ts +117 -0
  401. package/src/server/management/usage-summary-cache.ts +86 -0
  402. package/src/server/management-api.ts +215 -0
  403. package/src/server/management-auth.ts +267 -0
  404. package/src/server/memory-watchdog.ts +156 -0
  405. package/src/server/port-reclaim.ts +307 -0
  406. package/src/server/ports.ts +116 -0
  407. package/src/server/proxy-liveness.ts +201 -0
  408. package/src/server/relay-eager.ts +313 -0
  409. package/src/server/relay.ts +1049 -0
  410. package/src/server/request-decompress.ts +132 -0
  411. package/src/server/request-log-conversation.ts +168 -0
  412. package/src/server/request-log.ts +1046 -0
  413. package/src/server/responses/collaboration.ts +354 -0
  414. package/src/server/responses/compact.ts +384 -0
  415. package/src/server/responses/core.ts +2758 -0
  416. package/src/server/responses/encrypted-payload.ts +308 -0
  417. package/src/server/responses/fetch-helpers.ts +157 -0
  418. package/src/server/responses/passthrough-error.ts +78 -0
  419. package/src/server/responses/terminal-guard.ts +230 -0
  420. package/src/server/responses/upstream-error.ts +48 -0
  421. package/src/server/responses-image-gen-repair.ts +132 -0
  422. package/src/server/responses-item-id-repair.ts +224 -0
  423. package/src/server/responses.ts +9 -0
  424. package/src/server/search.ts +136 -0
  425. package/src/server/sse-payload-rewrite.ts +175 -0
  426. package/src/server/startup-action-control.ts +308 -0
  427. package/src/server/startup-health-cache.ts +113 -0
  428. package/src/server/system-env.ts +413 -0
  429. package/src/server/windows-tcp-drop.ts +184 -0
  430. package/src/server/windows-tray-control.ts +41 -0
  431. package/src/server/ws-bridge.ts +471 -0
  432. package/src/service.ts +2554 -0
  433. package/src/stall-timeout.ts +20 -0
  434. package/src/storage/cleanup-job.ts +57 -0
  435. package/src/storage/cleanup.ts +3085 -0
  436. package/src/storage/policy-job.ts +457 -0
  437. package/src/storage/policy-scheduler.ts +40 -0
  438. package/src/storage/policy-worker.ts +59 -0
  439. package/src/storage/policy.ts +527 -0
  440. package/src/storage/restore-job.ts +299 -0
  441. package/src/storage/restore-worker.ts +58 -0
  442. package/src/storage/scanner.ts +238 -0
  443. package/src/storage/storage-mutation-coordinator.ts +139 -0
  444. package/src/storage/worker-lifecycle.ts +215 -0
  445. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  446. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  447. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  448. package/src/tray/assets/opencodex-tray.png +0 -0
  449. package/src/tray/windows-tray.ps1 +290 -0
  450. package/src/tray/windows.ts +730 -0
  451. package/src/types.ts +1237 -0
  452. package/src/update/badge.ts +72 -0
  453. package/src/update/index.ts +407 -0
  454. package/src/update/job.ts +1520 -0
  455. package/src/update/notify.ts +257 -0
  456. package/src/update/npm-invocation.d.mts +23 -0
  457. package/src/update/npm-invocation.mjs +94 -0
  458. package/src/update/tray-update-plan.d.mts +18 -0
  459. package/src/update/tray-update-plan.mjs +38 -0
  460. package/src/usage/cost.ts +0 -0
  461. package/src/usage/debug.ts +97 -0
  462. package/src/usage/expected-prices.ts +164 -0
  463. package/src/usage/log.ts +658 -0
  464. package/src/usage/summary.ts +585 -0
  465. package/src/usage/totals.ts +14 -0
  466. package/src/vision/anthropic-describe.ts +185 -0
  467. package/src/vision/describe.ts +125 -0
  468. package/src/vision/index.ts +467 -0
  469. package/src/web-search/anthropic-executor.ts +189 -0
  470. package/src/web-search/executor.ts +105 -0
  471. package/src/web-search/format-result.ts +89 -0
  472. package/src/web-search/index.ts +196 -0
  473. package/src/web-search/loop.ts +664 -0
  474. package/src/web-search/parse.ts +220 -0
  475. package/src/web-search/progress-stream.ts +342 -0
  476. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,630 @@
1
+ /**
2
+ * OAuth token store at ~/.opencodex/auth.json, keyed by provider name.
3
+ *
4
+ * Multiauth shape (260706): each provider value is a ProviderAccountSet
5
+ * `{ activeAccountId, accounts: [{ id, credential, needsReauth?, addedAt? }] }`.
6
+ * Legacy single-credential values (`{ access, refresh, expires, ... }`) normalize on load,
7
+ * and the first new-shape persist writes a one-time `auth.json.pre-multiauth` backup so a
8
+ * downgraded loader (which silently drops unknown shapes) cannot destroy refresh tokens.
9
+ *
10
+ * Exceptions:
11
+ * - `chatgpt` stays single-slot (always replaced): codex-auth-api uses it as a scratch slot
12
+ * for Codex pool logins, which have their own ledger (codex-accounts.json).
13
+ * - Credentials without identity (no accountId/email) replace the active slot
14
+ * instead of appending: their refresh tokens rotate, so a derived id would duplicate the
15
+ * same human on every re-login. Kimi extracts JWT `user_id`/`sub` as accountId; Cursor
16
+ * extracts JWT `sub` — both append distinct accounts under multiauth.
17
+ */
18
+ import { createHash, randomUUID } from "node:crypto";
19
+ import { chmodSync, closeSync, copyFileSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret } from "../config";
22
+ import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
23
+ import { recordOwnedConfigPath } from "../lib/config-ownership";
24
+ import { MAX_PENDING_OAUTH_MUTATIONS } from "../lib/translator-budget";
25
+ import {
26
+ captureConfigGeneration,
27
+ type GenerationContext,
28
+ } from "../lib/state-store-sweeper";
29
+ import { validateCopilotApiBaseUrl } from "./github-copilot";
30
+ import type { OAuthCredentialSource, OAuthCredentials, ProviderAccount, ProviderAccountSet } from "./types";
31
+
32
+ type AuthStore = Record<string, ProviderAccountSet>;
33
+ let lastReconciledGeneration = 0;
34
+ let liveOAuthAccountKeys = new Set<string>();
35
+
36
+ function oauthAccountKey(provider: string, accountId: string): string {
37
+ return `${provider}\0${accountId}`;
38
+ }
39
+
40
+ export function reconcileOAuthReauthState(context: GenerationContext): number {
41
+ if (context.generation <= lastReconciledGeneration) return 0;
42
+ liveOAuthAccountKeys = new Set(context.oauthAccountKeys);
43
+ lastReconciledGeneration = context.generation;
44
+ return 0;
45
+ }
46
+
47
+ /** Providers whose account set is pinned to a single slot (see module doc). */
48
+ const SINGLE_SLOT_PROVIDERS = new Set(["chatgpt"]);
49
+
50
+ export function getAuthStorePath(): string {
51
+ return join(getConfigDir(), "auth.json");
52
+ }
53
+ export function getAuthStoreLockPath(): string { return join(getConfigDir(), "auth.store.lock"); }
54
+ export function getAuthRefreshIntentLockPath(provider: string, accountId: string): string {
55
+ const safeProvider = provider.replace(/[^a-zA-Z0-9_-]/g, "_");
56
+ const accountHash = createHash("sha256").update(accountId).digest("hex").slice(0, 24);
57
+ return join(getConfigDir(), `auth.refresh.${safeProvider}.${accountHash}.lock`);
58
+ }
59
+ export function getAuthRefreshIntentPath(provider: string, accountId: string): string {
60
+ return `${getAuthRefreshIntentLockPath(provider, accountId)}.json`;
61
+ }
62
+ export interface OAuthRefreshIntent { version: 1; provider: string; accountId: string; generation: string; createdAt: number; flightId?: string; staleOwner?: true; uncertain?: true }
63
+ function parseOAuthRefreshIntent(
64
+ provider: string,
65
+ accountId: string,
66
+ raw: string,
67
+ ): OAuthRefreshIntent {
68
+ const value = JSON.parse(raw) as Partial<OAuthRefreshIntent>;
69
+ if (
70
+ value.version !== 1
71
+ || value.provider !== provider
72
+ || value.accountId !== accountId
73
+ || typeof value.generation !== "string"
74
+ || typeof value.createdAt !== "number"
75
+ || (value.flightId !== undefined && typeof value.flightId !== "string")
76
+ || (value.staleOwner !== undefined && value.staleOwner !== true)
77
+ ) {
78
+ return { version: 1, provider, accountId, generation: "", createdAt: 0, uncertain: true };
79
+ }
80
+ return value as OAuthRefreshIntent;
81
+ }
82
+
83
+ export function readOAuthRefreshIntent(provider: string, accountId: string): OAuthRefreshIntent | undefined {
84
+ const path = getAuthRefreshIntentPath(provider, accountId);
85
+ try {
86
+ hardenConfigDir();
87
+ hardenExistingSecret(path);
88
+ return parseOAuthRefreshIntent(provider, accountId, readFileSync(path, "utf8"));
89
+ } catch (error) {
90
+ if (errorCode(error) === "ENOENT") return undefined;
91
+ return { version: 1, provider, accountId, generation: "", createdAt: 0, uncertain: true };
92
+ }
93
+ }
94
+
95
+ /** Observe-only intent read for diagnostics: no chmod/ACL hardening. */
96
+ export function peekOAuthRefreshIntent(provider: string, accountId: string): OAuthRefreshIntent | undefined {
97
+ const path = getAuthRefreshIntentPath(provider, accountId);
98
+ try {
99
+ return parseOAuthRefreshIntent(provider, accountId, readFileSync(path, "utf8"));
100
+ } catch (error) {
101
+ if (errorCode(error) === "ENOENT") return undefined;
102
+ return { version: 1, provider, accountId, generation: "", createdAt: 0, uncertain: true };
103
+ }
104
+ }
105
+ export function writeOAuthRefreshIntent(provider: string, accountId: string, generation: string, createdAt = Date.now(), flightId?: string): void {
106
+ const dir = getConfigDir();
107
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
108
+ hardenConfigDir();
109
+ const intent: OAuthRefreshIntent = { version: 1, provider, accountId, generation, createdAt, ...(flightId ? { flightId } : {}) };
110
+ atomicWriteFile(getAuthRefreshIntentPath(provider, accountId), `${JSON.stringify(intent)}\n`);
111
+ }
112
+ export function markOAuthRefreshIntentStaleOwner(provider: string, accountId: string, generation: string, flightId: string): boolean {
113
+ const current = readOAuthRefreshIntent(provider, accountId);
114
+ if (current?.uncertain || current?.generation !== generation || current.flightId !== flightId) return false;
115
+ atomicWriteFile(getAuthRefreshIntentPath(provider, accountId), `${JSON.stringify({ ...current, staleOwner: true })}\n`);
116
+ return true;
117
+ }
118
+ export function clearOAuthRefreshIntent(provider: string, accountId: string, generation: string): boolean {
119
+ const current = readOAuthRefreshIntent(provider, accountId);
120
+ if (!current || current.generation !== generation) return false;
121
+ try { unlinkSync(getAuthRefreshIntentPath(provider, accountId)); return true; }
122
+ catch (error) { if (errorCode(error) === "ENOENT") return false; throw error; }
123
+ }
124
+ export function credentialGeneration(cred: OAuthCredentials): string {
125
+ return createHash("sha256").update(JSON.stringify([cred.refresh, cred.access, cred.expires])).digest("hex");
126
+ }
127
+
128
+ function loadAuthStoreInternal(): { store: AuthStore; hadLegacy: boolean } {
129
+ const path = getAuthStorePath();
130
+ hardenConfigDir();
131
+ hardenExistingSecret(path);
132
+ if (!existsSync(path)) return { store: {}, hadLegacy: false };
133
+ try {
134
+ return normalizeAuthStore(JSON.parse(readFileSync(path, "utf-8")));
135
+ } catch {
136
+ backupInvalidConfig(path);
137
+ return { store: {}, hadLegacy: false };
138
+ }
139
+ }
140
+
141
+ export function loadAuthStore(): AuthStore {
142
+ return loadAuthStoreInternal().store;
143
+ }
144
+
145
+ /**
146
+ * Observe-only auth store read for diagnostics (`ocx doctor` / status).
147
+ * Does not chmod paths or backup invalid JSON — corrupt files are treated as empty.
148
+ */
149
+ export function peekAuthStore(): AuthStore {
150
+ const path = getAuthStorePath();
151
+ if (!existsSync(path)) return {};
152
+ try {
153
+ return normalizeAuthStore(JSON.parse(readFileSync(path, "utf-8"))).store;
154
+ } catch {
155
+ return {};
156
+ }
157
+ }
158
+
159
+ function persist(store: AuthStore): void {
160
+ const dir = getConfigDir();
161
+ assertNotRealHomeUnderTest(dir);
162
+ if (!existsSync(dir)) {
163
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
164
+ } else {
165
+ try { chmodSync(dir, 0o700); } catch { /* best-effort on existing dir */ }
166
+ }
167
+ hardenConfigDir();
168
+ atomicWriteFile(getAuthStorePath(), JSON.stringify(store, null, 2) + "\n");
169
+ }
170
+
171
+ export class OAuthFileLockError extends Error { readonly code = "OAUTH_FILE_LOCK_UNAVAILABLE"; constructor(message: string, options?: { cause?: unknown }) { super(message, options); this.name = "OAuthFileLockError"; } }
172
+ interface LockSnapshot { bytes: string; dev: number; ino: number; mtimeMs: number; size: number }
173
+ export interface OAuthFileLockOptions { path: string; waitTimeoutMs?: number; staleAfterMs?: number; pollMinMs?: number; pollMaxMs?: number; sleep?: (ms: number) => Promise<void>; now?: () => number; random?: () => number; beforeStaleUnlink?: () => void; beforeReleaseUnlink?: () => void; beforeFailedCreateUnlink?: () => void; writeMetadata?: (fd: number, bytes: string) => void }
174
+ export interface OAuthFileLockGuard { readonly ownerId: string; release(): void }
175
+ function errorCode(error: unknown): string | undefined { return error && typeof error === "object" && "code" in error ? String((error as { code?: unknown }).code) : undefined; }
176
+ function snapshot(path: string): LockSnapshot { const bytes = readFileSync(path, "utf8"); const s = statSync(path); return { bytes, dev:s.dev, ino:s.ino, mtimeMs:s.mtimeMs, size:s.size }; }
177
+ function sameSnapshot(a: LockSnapshot,b: LockSnapshot): boolean { return a.bytes===b.bytes&&a.dev===b.dev&&a.ino===b.ino&&a.mtimeMs===b.mtimeMs&&a.size===b.size; }
178
+ function sameFd(a: LockSnapshot,b: ReturnType<typeof fstatSync>): boolean { return a.dev===b.dev&&a.ino===b.ino&&a.mtimeMs===b.mtimeMs&&a.size===b.size; }
179
+ export function createOAuthFileLock(options: OAuthFileLockOptions): { acquire(): Promise<OAuthFileLockGuard> } {
180
+ const wait=options.waitTimeoutMs??5000, stale=options.staleAfterMs??120000, min=options.pollMinMs??25,max=options.pollMaxMs??100,sleep=options.sleep??(ms=>Bun.sleep(ms)),now=options.now??Date.now,random=options.random??Math.random,write=options.writeMetadata??((fd,b)=>writeFileSync(fd,b,"utf8"));
181
+ if(wait<0||stale<=0||min<0||max<min) throw new OAuthFileLockError("Invalid OAuth file-lock timing options");
182
+ return { async acquire() { hardenConfigDir(); recordOwnedConfigPath(getConfigDir(),options.path); if(!existsSync(getConfigDir())) mkdirSync(getConfigDir(),{recursive:true,mode:0o700}); const ownerId=randomUUID(),started=now(); for(;;){ let fd:number|undefined; try { fd=openSync(options.path,"wx",0o600); const bytes=`${JSON.stringify({version:1,ownerId,pid:process.pid,createdAt:now()})}\n`; write(fd,bytes); const fs=fstatSync(fd); closeSync(fd); fd=undefined; const owned=snapshot(options.path); if(owned.bytes!==bytes||!sameFd(owned,fs)) throw new OAuthFileLockError("OAuth lock changed during creation"); let released=false; return {ownerId,release(){if(released)return;released=true;try{const a=snapshot(options.path);if(!sameSnapshot(owned,a))return;options.beforeReleaseUnlink?.();const b=snapshot(options.path);if(sameSnapshot(owned,b))unlinkSync(options.path);}catch(e){if(errorCode(e)!=="ENOENT")console.warn(`[oauth] lock release failed: ${e instanceof Error?e.message:String(e)}`);}}}; } catch(e) { if(fd!==undefined){let fs;try{fs=fstatSync(fd);}catch{}try{closeSync(fd);}catch{}if(fs)try{const a=snapshot(options.path);if(sameFd(a,fs)){options.beforeFailedCreateUnlink?.();const b=snapshot(options.path);if(sameSnapshot(a,b)&&sameFd(b,fs))unlinkSync(options.path);}}catch{}} if(errorCode(e)!=="EEXIST")throw e instanceof OAuthFileLockError?e:new OAuthFileLockError("Could not create OAuth file lock",{cause:e}); }
183
+ try{const a=snapshot(options.path);let created=a.mtimeMs;try{const p=JSON.parse(a.bytes);if(typeof p.createdAt==="number")created=Math.max(created,p.createdAt);}catch{}if(now()-created>stale){options.beforeStaleUnlink?.();const b=snapshot(options.path);if(sameSnapshot(a,b))unlinkSync(options.path);continue;}}catch(e){if(errorCode(e)==="ENOENT")continue;throw new OAuthFileLockError("Could not inspect OAuth file lock",{cause:e});} const elapsed=now()-started;if(elapsed>=wait)throw new OAuthFileLockError(`Timed out after ${wait}ms waiting for OAuth file lock`);await sleep(Math.min(wait-elapsed,min+Math.floor(random()*(max-min+1)))); } } };
184
+ }
185
+ /** Wait long enough for slow IdP refreshes (e.g. Cursor 15s × 3 attempts) before timing out. */
186
+ export const OAUTH_REFRESH_LOCK_WAIT_MS = 60_000;
187
+
188
+ export function createOAuthRefreshIntentLock(provider:string,accountId:string,overrides:Partial<OAuthFileLockOptions>={}) {
189
+ return createOAuthFileLock({
190
+ path: getAuthRefreshIntentLockPath(provider, accountId),
191
+ staleAfterMs: 120000,
192
+ waitTimeoutMs: OAUTH_REFRESH_LOCK_WAIT_MS,
193
+ ...overrides,
194
+ });
195
+ }
196
+
197
+ /**
198
+ * One-time downgrade safety net: the first time we persist the NEW shape over a file that
199
+ * still contains legacy single-credential entries, keep a pristine copy. An older opencodex
200
+ * would silently drop the new shape (normalizeCredential -> null) and then persist an empty
201
+ * store, destroying refresh tokens; the backup makes that recoverable.
202
+ */
203
+ function backupLegacyOnce(): void {
204
+ const path = getAuthStorePath();
205
+ const backup = `${path}.pre-multiauth`;
206
+ if (!existsSync(path) || existsSync(backup)) return;
207
+ try {
208
+ copyFileSync(path, backup);
209
+ try { chmodSync(backup, 0o600); } catch { /* best-effort */ }
210
+ } catch { /* best-effort */ }
211
+ }
212
+
213
+ function isCredentialSource(value: unknown): value is OAuthCredentialSource {
214
+ return value === "oauth" || value === "local-cli" || value === "credential-file" || value === "environment" || value === "manual";
215
+ }
216
+
217
+ function normalizeCredential(cred: unknown): OAuthCredentials | null {
218
+ if (!cred || typeof cred !== "object") return null;
219
+ const candidate = cred as Partial<OAuthCredentials>;
220
+ if (typeof candidate.access !== "string" || typeof candidate.refresh !== "string" || typeof candidate.expires !== "number") {
221
+ return null;
222
+ }
223
+ const normalized: OAuthCredentials = {
224
+ access: candidate.access,
225
+ refresh: candidate.refresh,
226
+ expires: candidate.expires,
227
+ };
228
+ if (typeof candidate.email === "string" && candidate.email.length > 0) normalized.email = candidate.email;
229
+ if (typeof candidate.accountId === "string" && candidate.accountId.length > 0) normalized.accountId = candidate.accountId;
230
+ if (isCredentialSource(candidate.source)) normalized.source = candidate.source;
231
+ if (typeof candidate.projectId === "string" && candidate.projectId.length > 0) normalized.projectId = candidate.projectId;
232
+ if (typeof candidate.apiBaseUrl === "string" && candidate.apiBaseUrl.length > 0) {
233
+ // Persist only allowlisted Copilot origins; drop anything else so auth.json cannot
234
+ // become an SSRF springboard across reloads.
235
+ const validated = validateCopilotApiBaseUrl(candidate.apiBaseUrl);
236
+ if (validated) normalized.apiBaseUrl = validated;
237
+ }
238
+ if (candidate.kiro && typeof candidate.kiro === "object") {
239
+ const kiro = candidate.kiro;
240
+ const clean = (value: unknown, max: number): string | undefined => {
241
+ if (typeof value !== "string") return undefined;
242
+ const trimmed = value.trim();
243
+ return trimmed && trimmed.length <= max && !/[\x00-\x1f\x7f]/.test(trimmed) ? trimmed : undefined;
244
+ };
245
+ const profileArn = clean(kiro.profileArn, 1024);
246
+ const ssoRegion = clean(kiro.ssoRegion, 64);
247
+ const apiRegion = clean(kiro.apiRegion, 64);
248
+ const clientId = clean(kiro.clientId, 4096);
249
+ const clientSecret = clean(kiro.clientSecret, 4096);
250
+ if (profileArn || ssoRegion || apiRegion || clientId || clientSecret) {
251
+ normalized.kiro = {
252
+ ...(profileArn ? { profileArn } : {}),
253
+ ...(ssoRegion ? { ssoRegion } : {}),
254
+ ...(apiRegion ? { apiRegion } : {}),
255
+ ...(clientId ? { clientId } : {}),
256
+ ...(clientSecret ? { clientSecret } : {}),
257
+ };
258
+ }
259
+ }
260
+ return normalized;
261
+ }
262
+
263
+ /**
264
+ * Stable short account id. MUST be deterministic for a given credential: legacy
265
+ * single-credential stores are re-normalized on EVERY load without being persisted,
266
+ * so a time-salted id would differ between two loads (getAccountSet vs
267
+ * getAccountCredential), surfacing as a spurious OAuthLoginRequiredError and making
268
+ * refresh persists silently miss the account (rotated refresh token lost).
269
+ */
270
+ function newAccountId(cred: OAuthCredentials): string {
271
+ const identity = cred.accountId ?? cred.email ?? cred.refresh;
272
+ return createHash("sha256").update(identity).digest("hex").slice(0, 8);
273
+ }
274
+
275
+ function normalizeAccount(value: unknown): ProviderAccount | null {
276
+ if (!value || typeof value !== "object") return null;
277
+ const candidate = value as Partial<ProviderAccount>;
278
+ if (typeof candidate.id !== "string" || candidate.id.length === 0) return null;
279
+ const credential = normalizeCredential(candidate.credential);
280
+ if (!credential) return null;
281
+ const account: ProviderAccount = { id: candidate.id, credential };
282
+ if (typeof candidate.alias === "string" && candidate.alias.trim()) account.alias = candidate.alias.trim();
283
+ if (candidate.needsReauth === true) account.needsReauth = true;
284
+ if (typeof candidate.addedAt === "number") account.addedAt = candidate.addedAt;
285
+ return account;
286
+ }
287
+
288
+ function normalizeAccountSet(raw: unknown): { set: ProviderAccountSet | null; wasLegacy: boolean } {
289
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { set: null, wasLegacy: false };
290
+ const candidate = raw as Partial<ProviderAccountSet>;
291
+ if (Array.isArray(candidate.accounts)) {
292
+ const accounts = candidate.accounts.map(normalizeAccount).filter((a): a is ProviderAccount => a !== null);
293
+ if (accounts.length === 0) return { set: null, wasLegacy: false };
294
+ const active = typeof candidate.activeAccountId === "string" && accounts.some(a => a.id === candidate.activeAccountId)
295
+ ? candidate.activeAccountId
296
+ : accounts[0]!.id;
297
+ return { set: { activeAccountId: active, accounts }, wasLegacy: false };
298
+ }
299
+ // Legacy single-credential value.
300
+ const cred = normalizeCredential(raw);
301
+ if (!cred) return { set: null, wasLegacy: false };
302
+ const id = newAccountId(cred);
303
+ return { set: { activeAccountId: id, accounts: [{ id, credential: cred }] }, wasLegacy: true };
304
+ }
305
+
306
+ function normalizeAuthStore(raw: unknown): { store: AuthStore; hadLegacy: boolean } {
307
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { store: {}, hadLegacy: false };
308
+ const normalized: AuthStore = {};
309
+ let hadLegacy = false;
310
+ for (const [provider, value] of Object.entries(raw)) {
311
+ const { set, wasLegacy } = normalizeAccountSet(value);
312
+ if (set) normalized[provider] = set;
313
+ if (wasLegacy) hadLegacy = true;
314
+ }
315
+ return { store: normalized, hadLegacy };
316
+ }
317
+
318
+ /**
319
+ * In-process write serialization: every mutation runs load-modify-persist under this queue so
320
+ * a guardian refresh persisting a non-active account cannot roll back a concurrent
321
+ * active-account switch (lost update). Cross-process races are accepted (single proxy).
322
+ */
323
+ const OAUTH_MUTATION_WAIT_MS = 30_000;
324
+ const oauthMutationEncoder = new TextEncoder();
325
+ let mutationTail: Promise<void> = Promise.resolve();
326
+ let pendingMutations = 0;
327
+ let mutationCurrentBytes = 0;
328
+ let mutationHighWaterBytes = 0;
329
+ interface QueuedOAuthMutation {
330
+ started: boolean;
331
+ settled: boolean;
332
+ timeout?: ReturnType<typeof setTimeout>;
333
+ run(): Promise<void>;
334
+ }
335
+ const mutationWaiters: QueuedOAuthMutation[] = [];
336
+ let mutationRunning = false;
337
+
338
+ export class OAuthMutationBusyError extends Error {
339
+ readonly code = "oauth_mutation_busy";
340
+ constructor(message = "OAuth mutation queue is busy") {
341
+ super(message);
342
+ this.name = "OAuthMutationBusyError";
343
+ }
344
+ }
345
+
346
+ export function oauthMutationTailSnapshot(): { currentBytes: number; highWaterBytes: number; active: number } {
347
+ return { currentBytes: mutationCurrentBytes, highWaterBytes: mutationHighWaterBytes, active: pendingMutations };
348
+ }
349
+
350
+ function retainedClosureStringBytes(values: readonly unknown[]): number {
351
+ const visit = (value: unknown): number => {
352
+ if (typeof value === "string") return oauthMutationEncoder.encode(value).byteLength;
353
+ if (Array.isArray(value)) return value.reduce((sum, entry) => sum + visit(entry), 0);
354
+ if (value && typeof value === "object") {
355
+ return Object.entries(value as Record<string, unknown>)
356
+ .reduce((sum, [key, entry]) => sum + oauthMutationEncoder.encode(key).byteLength + visit(entry), 0);
357
+ }
358
+ return 0;
359
+ };
360
+ return values.reduce<number>((sum, value) => sum + visit(value), 0);
361
+ }
362
+
363
+ function drainOAuthMutations(): void {
364
+ if (mutationRunning) return;
365
+ const next = mutationWaiters.shift();
366
+ if (!next) return;
367
+ if (next.settled) {
368
+ drainOAuthMutations();
369
+ return;
370
+ }
371
+ next.started = true;
372
+ if (next.timeout) clearTimeout(next.timeout);
373
+ mutationRunning = true;
374
+ mutationTail = next.run().finally(() => {
375
+ mutationRunning = false;
376
+ drainOAuthMutations();
377
+ });
378
+ }
379
+
380
+ function serializeMutation<T>(work: () => Promise<T>, retainedValues: readonly unknown[], waitMs = OAUTH_MUTATION_WAIT_MS): Promise<T> {
381
+ if (pendingMutations >= MAX_PENDING_OAUTH_MUTATIONS) return Promise.reject(new OAuthMutationBusyError());
382
+ pendingMutations += 1;
383
+ const retainedBytes = retainedClosureStringBytes(retainedValues);
384
+ mutationCurrentBytes += retainedBytes;
385
+ mutationHighWaterBytes = Math.max(mutationHighWaterBytes, mutationCurrentBytes);
386
+
387
+ let resolveResult!: (value: T | PromiseLike<T>) => void;
388
+ let rejectResult!: (reason?: unknown) => void;
389
+ const result = new Promise<T>((resolve, reject) => {
390
+ resolveResult = resolve;
391
+ rejectResult = reject;
392
+ });
393
+ const entry: QueuedOAuthMutation = {
394
+ started: false,
395
+ settled: false,
396
+ async run() {
397
+ try {
398
+ resolveResult(await work());
399
+ } catch (error) {
400
+ rejectResult(error);
401
+ } finally {
402
+ release();
403
+ }
404
+ },
405
+ };
406
+ const release = () => {
407
+ if (entry.settled) return;
408
+ entry.settled = true;
409
+ pendingMutations -= 1;
410
+ mutationCurrentBytes = Math.max(0, mutationCurrentBytes - retainedBytes);
411
+ };
412
+ entry.timeout = setTimeout(() => {
413
+ if (entry.started || entry.settled) return;
414
+ const index = mutationWaiters.indexOf(entry);
415
+ if (index >= 0) mutationWaiters.splice(index, 1);
416
+ release();
417
+ rejectResult(new OAuthMutationBusyError("OAuth mutation queue wait timed out"));
418
+ }, waitMs);
419
+ // Only unref the long default wait. Short waitMs (tests) must stay ref'd:
420
+ // on Windows Bun under `bun test --isolate`, an unref'd timer can fail to
421
+ // fire while the head mutation holds an unresolved Promise, hanging the
422
+ // waiter forever (#827 / full admission queue hang on windows-latest).
423
+ if (waitMs >= OAUTH_MUTATION_WAIT_MS) entry.timeout.unref?.();
424
+ mutationWaiters.push(entry);
425
+ drainOAuthMutations();
426
+ return result;
427
+ }
428
+ export function mutateStore<T>(fn:(store:AuthStore)=>T|Promise<T>, retainedValues: readonly unknown[] = [], options?: { waitMs?: number }):Promise<T>{return serializeMutation(async()=>{const guard=await createOAuthFileLock({path:getAuthStoreLockPath(),staleAfterMs:30000}).acquire();try{
429
+ const { store, hadLegacy } = loadAuthStoreInternal();
430
+ if (hadLegacy) backupLegacyOnce();
431
+ const result = await fn(store);
432
+ persist(store);
433
+ return result;
434
+ }finally{guard.release();}}, retainedValues, options?.waitMs);
435
+ }
436
+
437
+ /** The ACTIVE account's credential for a provider (what requests should use). */
438
+ export function getCredential(provider: string): OAuthCredentials | null {
439
+ const set = loadAuthStore()[provider];
440
+ if (!set) return null;
441
+ return set.accounts.find(a => a.id === set.activeAccountId)?.credential ?? null;
442
+ }
443
+
444
+ /**
445
+ * Persist a credential as the ACTIVE account. Identity-matching (accountId ?? email) upserts
446
+ * the same human's slot; a new identity appends a new account. Credentials without identity
447
+ * (rotating refresh tokens would fabricate duplicates) and single-slot providers replace the
448
+ * active slot / whole set instead.
449
+ */
450
+ export async function saveCredential(
451
+ provider: string,
452
+ cred: OAuthCredentials,
453
+ opts: { preserveIdentityless?: boolean } = {},
454
+ ): Promise<void> {
455
+ const safe = normalizeCredential(cred);
456
+ if (!safe) return;
457
+ await mutateStore(store => {
458
+ const set = store[provider];
459
+ const identity = safe.accountId ?? safe.email;
460
+ if (!set || SINGLE_SLOT_PROVIDERS.has(provider)) {
461
+ const id = newAccountId(safe);
462
+ store[provider] = { activeAccountId: id, accounts: [{ id, credential: safe, addedAt: Date.now() }] };
463
+ return;
464
+ }
465
+ if (identity) {
466
+ const existing = set.accounts.find(a => (a.credential.accountId ?? a.credential.email) === identity);
467
+ if (existing) {
468
+ existing.credential = safe;
469
+ delete existing.needsReauth;
470
+ set.activeAccountId = existing.id;
471
+ return;
472
+ }
473
+ // Legacy migration: a pre-identity row (no accountId/email) for this provider is the
474
+ // SAME human re-logging in after the identity extraction shipped — upgrading the
475
+ // active identity-less row in place prevents a stale duplicate that stays selectable
476
+ // and would re-refresh into a second row with the same identity.
477
+ const active = set.accounts.find(a => a.id === set.activeAccountId);
478
+ if (!opts.preserveIdentityless && active && active.credential.accountId === undefined && active.credential.email === undefined) {
479
+ active.credential = safe;
480
+ delete active.needsReauth;
481
+ return;
482
+ }
483
+ const id = newAccountId(safe);
484
+ set.accounts.push({ id, credential: safe, addedAt: Date.now() });
485
+ set.activeAccountId = id;
486
+ return;
487
+ }
488
+ // No identity: replace the active slot in place (single-account semantics).
489
+ const active = set.accounts.find(a => a.id === set.activeAccountId);
490
+ if (active) {
491
+ active.credential = safe;
492
+ delete active.needsReauth;
493
+ } else {
494
+ const id = newAccountId(safe);
495
+ set.accounts.push({ id, credential: safe, addedAt: Date.now() });
496
+ set.activeAccountId = id;
497
+ }
498
+ }, [provider, safe]);
499
+ }
500
+
501
+ /** Remove the ACTIVE account; remaining accounts promote the first one. */
502
+ export async function removeCredential(provider: string): Promise<void> {
503
+ await mutateStore(store => {
504
+ const set = store[provider];
505
+ if (!set) return;
506
+ set.accounts = set.accounts.filter(a => a.id !== set.activeAccountId);
507
+ if (set.accounts.length === 0) {
508
+ delete store[provider];
509
+ return;
510
+ }
511
+ set.activeAccountId = set.accounts[0]!.id;
512
+ }, [provider]);
513
+ }
514
+
515
+ // ---------------------------------------------------------------------------
516
+ // Multi-account API
517
+ // ---------------------------------------------------------------------------
518
+
519
+ export function getAccountSet(provider: string): ProviderAccountSet | null {
520
+ return loadAuthStore()[provider] ?? null;
521
+ }
522
+
523
+ export function listAccounts(provider: string): ProviderAccount[] {
524
+ return loadAuthStore()[provider]?.accounts ?? [];
525
+ }
526
+
527
+ export function listLiveOAuthAccountKeys(
528
+ providerNames: ReadonlySet<string>,
529
+ ): ReadonlySet<string> {
530
+ const keys = new Set<string>();
531
+ for (const [provider, accountSet] of Object.entries(loadAuthStore())) {
532
+ if (!providerNames.has(provider)) continue;
533
+ for (const account of accountSet.accounts) keys.add(`${provider}\0${account.id}`);
534
+ }
535
+ return keys;
536
+ }
537
+
538
+ export function getAccountCredential(provider: string, accountId: string): OAuthCredentials | null {
539
+ return loadAuthStore()[provider]?.accounts.find(a => a.id === accountId)?.credential ?? null;
540
+ }
541
+
542
+ /** Persist a refreshed credential for a SPECIFIC account without touching activeAccountId. */
543
+ export async function saveAccountCredential(provider: string, accountId: string, cred: OAuthCredentials): Promise<void> {
544
+ const safe = normalizeCredential(cred);
545
+ if (!safe) return;
546
+ await mutateStore(store => {
547
+ const account = store[provider]?.accounts.find(a => a.id === accountId);
548
+ if (!account) return;
549
+ account.credential = safe;
550
+ delete account.needsReauth;
551
+ }, [provider, accountId, safe]);
552
+ }
553
+
554
+ export async function setActiveAccount(provider: string, accountId: string): Promise<boolean> {
555
+ return await mutateStore(store => {
556
+ const set = store[provider];
557
+ if (!set || !set.accounts.some(a => a.id === accountId)) return false;
558
+ set.activeAccountId = accountId;
559
+ return true;
560
+ }, [provider, accountId]);
561
+ }
562
+
563
+ export async function setAccountAlias(provider: string, accountId: string, alias: string | undefined): Promise<boolean> {
564
+ return await mutateStore(store => {
565
+ const account = store[provider]?.accounts.find(a => a.id === accountId);
566
+ if (!account) return false;
567
+ if (alias) account.alias = alias;
568
+ else delete account.alias;
569
+ return true;
570
+ }, [provider, accountId, alias]);
571
+ }
572
+
573
+ /** Remove one account by id; active removal promotes the first remaining account. */
574
+ export async function removeAccount(provider: string, accountId: string): Promise<boolean> {
575
+ const removed = await mutateStore(store => {
576
+ const set = store[provider];
577
+ if (!set) return false;
578
+ const before = set.accounts.length;
579
+ set.accounts = set.accounts.filter(a => a.id !== accountId);
580
+ if (set.accounts.length === before) return false;
581
+ if (set.accounts.length === 0) {
582
+ delete store[provider];
583
+ return true;
584
+ }
585
+ if (set.activeAccountId === accountId) set.activeAccountId = set.accounts[0]!.id;
586
+ return true;
587
+ }, [provider, accountId]);
588
+ return removed;
589
+ }
590
+
591
+ /** Replace or clear a provider account set (used for transactional Kiro add-account rollback). */
592
+ export async function replaceProviderAccountSet(
593
+ provider: string,
594
+ set: ProviderAccountSet | null,
595
+ ): Promise<void> {
596
+ await mutateStore(store => {
597
+ if (!set || set.accounts.length === 0) {
598
+ delete store[provider];
599
+ return;
600
+ }
601
+ store[provider] = {
602
+ activeAccountId: set.activeAccountId,
603
+ accounts: set.accounts.map(account => ({
604
+ id: account.id,
605
+ credential: { ...account.credential, ...(account.credential.kiro ? { kiro: { ...account.credential.kiro } } : {}) },
606
+ ...(account.alias ? { alias: account.alias } : {}),
607
+ ...(account.needsReauth ? { needsReauth: true } : {}),
608
+ ...(account.addedAt !== undefined ? { addedAt: account.addedAt } : {}),
609
+ })),
610
+ };
611
+ }, [provider, set]);
612
+ }
613
+
614
+ export async function markAccountNeedsReauth(
615
+ provider: string,
616
+ accountId: string,
617
+ needsReauth: boolean,
618
+ writerGeneration = captureConfigGeneration(),
619
+ ): Promise<void> {
620
+ if (writerGeneration < lastReconciledGeneration && !liveOAuthAccountKeys.has(oauthAccountKey(provider, accountId))) return;
621
+ await mutateStore(store => {
622
+ const account = store[provider]?.accounts.find(a => a.id === accountId);
623
+ if (!account) return;
624
+ if (needsReauth) account.needsReauth = true;
625
+ else delete account.needsReauth;
626
+ }, [provider, accountId]);
627
+ }
628
+
629
+ export async function mergeAccountCredential(provider:string,accountId:string,credential:OAuthCredentials,opts:{expectedGeneration?:string;afterPrePersistRead?:()=>void|Promise<void>}={}):Promise<{superseded:false}|{superseded:true;stored:OAuthCredentials}>{const safe=normalizeCredential(credential);if(!safe)throw new Error("Refusing to persist invalid OAuth credential");return await mutateStore(async store=>{await opts.afterPrePersistRead?.();const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account)throw new Error(`OAuth account disappeared before persist: ${provider}`);if(opts.expectedGeneration!==undefined&&credentialGeneration(account.credential)!==opts.expectedGeneration)return{superseded:true,stored:account.credential};account.credential=safe;delete account.needsReauth;return{superseded:false};},[provider,accountId,safe,opts.expectedGeneration]);}
630
+ export async function markAccountNeedsReauthIfGeneration(provider:string,accountId:string,generation:string,writerGeneration=captureConfigGeneration()):Promise<boolean>{const key=oauthAccountKey(provider,accountId);if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;return await mutateStore(store=>{const account=store[provider]?.accounts.find(x=>x.id===accountId);if(!account?.credential||credentialGeneration(account.credential)!==generation)return false;if(writerGeneration<lastReconciledGeneration&&!liveOAuthAccountKeys.has(key))return false;account.needsReauth=true;return true;},[provider,accountId,generation]);}