@remodex/rmx 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (588) hide show
  1. package/AGENTS_INSTALL.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +584 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-CZqebSPQ.css +1 -0
  11. package/gui/dist/assets/index-CkETtt7P.js +71 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/fonts/google-sans-cyrillic.woff2 +0 -0
  14. package/gui/dist/fonts/google-sans-latin.woff2 +0 -0
  15. package/gui/dist/icons.svg +24 -0
  16. package/gui/dist/index.html +25 -0
  17. package/gui/dist/logo.png +0 -0
  18. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  19. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  20. package/gui/dist/provider-icons/claude-color.svg +1 -0
  21. package/gui/dist/provider-icons/cline-color.svg +16 -0
  22. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  23. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  24. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  25. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  26. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  27. package/gui/dist/provider-icons/discord.svg +1 -0
  28. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  29. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini-color.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.svg +1 -0
  34. package/gui/dist/provider-icons/groq-color.svg +1 -0
  35. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  36. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  37. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  38. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  39. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  40. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  41. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  42. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  43. package/gui/dist/provider-icons/openai.svg +1 -0
  44. package/gui/dist/provider-icons/opencode.svg +2 -0
  45. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  46. package/gui/dist/provider-icons/pi.svg +21 -0
  47. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  48. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  49. package/gui/dist/provider-icons/telegram.svg +1 -0
  50. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  51. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  52. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  53. package/package.json +118 -0
  54. package/src/AGENTS.md +28 -0
  55. package/src/adapters/anthropic-image-guard.ts +251 -0
  56. package/src/adapters/anthropic-image-normalize.ts +518 -0
  57. package/src/adapters/anthropic.ts +1205 -0
  58. package/src/adapters/azure.ts +36 -0
  59. package/src/adapters/base.ts +83 -0
  60. package/src/adapters/client-fingerprint.ts +59 -0
  61. package/src/adapters/command-code.ts +453 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/cursor-errors.ts +165 -0
  65. package/src/adapters/cursor/discovery.ts +276 -0
  66. package/src/adapters/cursor/effort-map.ts +139 -0
  67. package/src/adapters/cursor/exec-policy.ts +88 -0
  68. package/src/adapters/cursor/framing.ts +250 -0
  69. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  70. package/src/adapters/cursor/kv-store.ts +52 -0
  71. package/src/adapters/cursor/live-models.ts +153 -0
  72. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  73. package/src/adapters/cursor/live-transport.ts +1235 -0
  74. package/src/adapters/cursor/mcp-config.ts +42 -0
  75. package/src/adapters/cursor/mcp-manager.ts +333 -0
  76. package/src/adapters/cursor/message-mapper.ts +49 -0
  77. package/src/adapters/cursor/native-exec-common.ts +59 -0
  78. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  79. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  80. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  81. package/src/adapters/cursor/native-exec-network.ts +43 -0
  82. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  83. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  84. package/src/adapters/cursor/native-exec.ts +604 -0
  85. package/src/adapters/cursor/protobuf-events.ts +735 -0
  86. package/src/adapters/cursor/protobuf-request.ts +719 -0
  87. package/src/adapters/cursor/request-builder.ts +280 -0
  88. package/src/adapters/cursor/thread-continuity.ts +67 -0
  89. package/src/adapters/cursor/tool-definitions.ts +621 -0
  90. package/src/adapters/cursor/transport-retry.ts +132 -0
  91. package/src/adapters/cursor/transport.ts +57 -0
  92. package/src/adapters/cursor/types.ts +59 -0
  93. package/src/adapters/cursor.ts +196 -0
  94. package/src/adapters/google-antigravity-replay.ts +520 -0
  95. package/src/adapters/google-antigravity-wire.ts +140 -0
  96. package/src/adapters/google-errors.ts +85 -0
  97. package/src/adapters/google-http.ts +100 -0
  98. package/src/adapters/google-tool-schema.ts +173 -0
  99. package/src/adapters/google-truncation.ts +24 -0
  100. package/src/adapters/google-wire-compiler.ts +232 -0
  101. package/src/adapters/google.ts +859 -0
  102. package/src/adapters/identity.ts +77 -0
  103. package/src/adapters/image.ts +23 -0
  104. package/src/adapters/kiro-constants.ts +16 -0
  105. package/src/adapters/kiro-errors.ts +208 -0
  106. package/src/adapters/kiro-events.ts +197 -0
  107. package/src/adapters/kiro-images.ts +129 -0
  108. package/src/adapters/kiro-retry.ts +312 -0
  109. package/src/adapters/kiro-thinking.ts +104 -0
  110. package/src/adapters/kiro-tool-fallback.ts +36 -0
  111. package/src/adapters/kiro-tools.ts +224 -0
  112. package/src/adapters/kiro-truncation.ts +33 -0
  113. package/src/adapters/kiro-wire.ts +129 -0
  114. package/src/adapters/kiro.ts +1924 -0
  115. package/src/adapters/mimo-free.ts +263 -0
  116. package/src/adapters/openai-chat.ts +1265 -0
  117. package/src/adapters/openai-responses.ts +1309 -0
  118. package/src/adapters/run-turn-queue.ts +114 -0
  119. package/src/adapters/tool-catalog-nudge.ts +71 -0
  120. package/src/adapters/upstream-http-error.ts +48 -0
  121. package/src/android-remote/assets.ts +218 -0
  122. package/src/android-remote/attachments.ts +168 -0
  123. package/src/android-remote/auth.ts +237 -0
  124. package/src/android-remote/cloudflare-provisioning.ts +409 -0
  125. package/src/android-remote/cloudflare-secret.ts +106 -0
  126. package/src/android-remote/cloudflare-tunnel.ts +488 -0
  127. package/src/android-remote/cloudflared.ts +286 -0
  128. package/src/android-remote/codex-app-server.ts +565 -0
  129. package/src/android-remote/desktop-history-page.ts +936 -0
  130. package/src/android-remote/desktop-ipc.ts +3643 -0
  131. package/src/android-remote/desktop-ownership-store.ts +98 -0
  132. package/src/android-remote/desktop-project-registration.ts +129 -0
  133. package/src/android-remote/desktop-session-stream.ts +1110 -0
  134. package/src/android-remote/desktop-workspace-state.ts +443 -0
  135. package/src/android-remote/file-change-parser.ts +112 -0
  136. package/src/android-remote/gateway.ts +9108 -0
  137. package/src/android-remote/mutation-store.ts +299 -0
  138. package/src/android-remote/projection.ts +1780 -0
  139. package/src/android-remote/queued-turn-store.ts +248 -0
  140. package/src/android-remote/session-command-recovery.ts +1384 -0
  141. package/src/android-remote/store.ts +466 -0
  142. package/src/android-remote/thread-reconciliation.ts +133 -0
  143. package/src/android-remote/thread-source-paths.ts +307 -0
  144. package/src/android-remote/thread-stream.ts +546 -0
  145. package/src/android-remote/turn-activity.ts +235 -0
  146. package/src/android-remote/user-message-identity.ts +94 -0
  147. package/src/bridge.ts +1793 -0
  148. package/src/chat/inbound.ts +295 -0
  149. package/src/chat/outbound.ts +821 -0
  150. package/src/claude/agents-inject.ts +267 -0
  151. package/src/claude/alias.ts +149 -0
  152. package/src/claude/auth-detect.ts +229 -0
  153. package/src/claude/auth-mode-migration.ts +32 -0
  154. package/src/claude/auth-mode.ts +62 -0
  155. package/src/claude/context-windows.ts +189 -0
  156. package/src/claude/desktop-3p-guard.ts +35 -0
  157. package/src/claude/desktop-3p-paths.ts +84 -0
  158. package/src/claude/desktop-3p.ts +601 -0
  159. package/src/claude/desktop-health.ts +26 -0
  160. package/src/claude/desktop-profile.ts +263 -0
  161. package/src/claude/gateway-cache.ts +70 -0
  162. package/src/claude/inbound-debug.ts +163 -0
  163. package/src/claude/inbound.ts +519 -0
  164. package/src/claude/model-info.ts +154 -0
  165. package/src/claude/outbound.ts +898 -0
  166. package/src/cli/access.ts +108 -0
  167. package/src/cli/account-api.ts +296 -0
  168. package/src/cli/account-auth.ts +250 -0
  169. package/src/cli/account-catalog-refresh.ts +14 -0
  170. package/src/cli/account-extended.ts +476 -0
  171. package/src/cli/account-main.ts +317 -0
  172. package/src/cli/account.ts +297 -0
  173. package/src/cli/agent-driven.ts +70 -0
  174. package/src/cli/agent.ts +184 -0
  175. package/src/cli/catalog-prewarm.ts +27 -0
  176. package/src/cli/claude-desktop.ts +211 -0
  177. package/src/cli/claude.ts +302 -0
  178. package/src/cli/codex-shim-autorestore.ts +45 -0
  179. package/src/cli/codex-shim-readiness.ts +69 -0
  180. package/src/cli/combo.ts +124 -0
  181. package/src/cli/config-command.ts +183 -0
  182. package/src/cli/debug.ts +228 -0
  183. package/src/cli/desktop-first-run.ts +25 -0
  184. package/src/cli/doctor.ts +1022 -0
  185. package/src/cli/export-command.ts +201 -0
  186. package/src/cli/help.ts +370 -0
  187. package/src/cli/index.ts +1565 -0
  188. package/src/cli/init.ts +224 -0
  189. package/src/cli/integrations.ts +225 -0
  190. package/src/cli/interactive-confirm.ts +133 -0
  191. package/src/cli/internal-dispatch.ts +35 -0
  192. package/src/cli/launcher-context.ts +77 -0
  193. package/src/cli/models-runtime.ts +224 -0
  194. package/src/cli/models.ts +340 -0
  195. package/src/cli/observe.ts +170 -0
  196. package/src/cli/opencode.ts +587 -0
  197. package/src/cli/provider-runtime.ts +179 -0
  198. package/src/cli/provider.ts +476 -0
  199. package/src/cli/ready.ts +301 -0
  200. package/src/cli/route-policy.ts +92 -0
  201. package/src/cli/runtime-api.ts +328 -0
  202. package/src/cli/star-prompt.ts +211 -0
  203. package/src/cli/status-oauth.ts +78 -0
  204. package/src/cli/status.ts +321 -0
  205. package/src/cli/system-command.ts +196 -0
  206. package/src/cli/system-restart-client.ts +146 -0
  207. package/src/cli/tray-proxy.ts +205 -0
  208. package/src/cli/v2.ts +200 -0
  209. package/src/cli.ts +10 -0
  210. package/src/clients/config-export.ts +1109 -0
  211. package/src/codex/account-id.ts +34 -0
  212. package/src/codex/account-label.ts +34 -0
  213. package/src/codex/account-lifecycle.ts +172 -0
  214. package/src/codex/account-namespace-match.ts +63 -0
  215. package/src/codex/account-namespaces.ts +195 -0
  216. package/src/codex/account-pause.ts +20 -0
  217. package/src/codex/account-priority.ts +83 -0
  218. package/src/codex/account-runtime-state.ts +31 -0
  219. package/src/codex/account-store.ts +517 -0
  220. package/src/codex/account-usability.ts +40 -0
  221. package/src/codex/admission.ts +263 -0
  222. package/src/codex/app-server-processes.ts +799 -0
  223. package/src/codex/auth-api.ts +2098 -0
  224. package/src/codex/auth-collision.ts +107 -0
  225. package/src/codex/auth-context.ts +480 -0
  226. package/src/codex/autostart-health.ts +156 -0
  227. package/src/codex/catalog/account-models.ts +67 -0
  228. package/src/codex/catalog/aggregation.ts +471 -0
  229. package/src/codex/catalog/bundled.ts +533 -0
  230. package/src/codex/catalog/effort.ts +432 -0
  231. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  232. package/src/codex/catalog/kinds.ts +2 -0
  233. package/src/codex/catalog/metadata.ts +287 -0
  234. package/src/codex/catalog/native-models.ts +7 -0
  235. package/src/codex/catalog/parsing.ts +503 -0
  236. package/src/codex/catalog/provider-fetch.ts +2267 -0
  237. package/src/codex/catalog/sync.ts +1606 -0
  238. package/src/codex/catalog-admission.ts +197 -0
  239. package/src/codex/catalog-refresh-status.ts +87 -0
  240. package/src/codex/catalog-write-serialization.ts +242 -0
  241. package/src/codex/catalog.ts +15 -0
  242. package/src/codex/codex-write-lock.ts +384 -0
  243. package/src/codex/convergence-types.ts +593 -0
  244. package/src/codex/convergence.ts +580 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/desktop-client-processes.ts +521 -0
  249. package/src/codex/exec-invocation.ts +22 -0
  250. package/src/codex/features.ts +1091 -0
  251. package/src/codex/generation.ts +202 -0
  252. package/src/codex/history-job.ts +347 -0
  253. package/src/codex/history-lock.ts +242 -0
  254. package/src/codex/history-migration-guardian.ts +115 -0
  255. package/src/codex/history-provider.ts +1075 -0
  256. package/src/codex/history-transition.ts +105 -0
  257. package/src/codex/history-worker.ts +204 -0
  258. package/src/codex/home.ts +206 -0
  259. package/src/codex/inject-coordination.ts +257 -0
  260. package/src/codex/inject.ts +1857 -0
  261. package/src/codex/injected-marker.ts +79 -0
  262. package/src/codex/integration-record.ts +266 -0
  263. package/src/codex/internal/catalog-writer.ts +203 -0
  264. package/src/codex/internal/history-writer.ts +105 -0
  265. package/src/codex/journal.ts +172 -0
  266. package/src/codex/main-account-cache.ts +56 -0
  267. package/src/codex/main-account.ts +40 -0
  268. package/src/codex/management-convergence.ts +114 -0
  269. package/src/codex/model-cache.ts +267 -0
  270. package/src/codex/native-main-admission.ts +47 -0
  271. package/src/codex/native-main-auth-temp.ts +187 -0
  272. package/src/codex/native-main-claim.ts +167 -0
  273. package/src/codex/native-main-lock-file.ts +162 -0
  274. package/src/codex/native-main-owner.ts +329 -0
  275. package/src/codex/native-profile-api.ts +247 -0
  276. package/src/codex/native-profile-manager.ts +1531 -0
  277. package/src/codex/native-profile-processes.ts +121 -0
  278. package/src/codex/native-profile-recovery.ts +99 -0
  279. package/src/codex/native-profile-stage-store.ts +387 -0
  280. package/src/codex/native-profile-startup.ts +348 -0
  281. package/src/codex/native-profile-store.ts +855 -0
  282. package/src/codex/native-profile-types.ts +120 -0
  283. package/src/codex/native-residue.ts +691 -0
  284. package/src/codex/paths.ts +78 -0
  285. package/src/codex/plugins-doctor.ts +242 -0
  286. package/src/codex/pool-rotation.ts +295 -0
  287. package/src/codex/project-config-warnings.ts +426 -0
  288. package/src/codex/prompt-journal.ts +311 -0
  289. package/src/codex/prompt-layers.ts +967 -0
  290. package/src/codex/prompt-lock.ts +143 -0
  291. package/src/codex/provider-adoption.ts +242 -0
  292. package/src/codex/quota-rejection.ts +224 -0
  293. package/src/codex/quota.ts +494 -0
  294. package/src/codex/refresh.ts +60 -0
  295. package/src/codex/routing.ts +1855 -0
  296. package/src/codex/runtime.ts +659 -0
  297. package/src/codex/shim.ts +1215 -0
  298. package/src/codex/subagent-defaults.ts +557 -0
  299. package/src/codex/subagent-model-fallback.ts +560 -0
  300. package/src/codex/sync.ts +238 -0
  301. package/src/codex/transition-state.ts +612 -0
  302. package/src/codex/upstream-host-health.ts +368 -0
  303. package/src/codex/user-identity.ts +374 -0
  304. package/src/codex/warmup.ts +192 -0
  305. package/src/codex/websocket-registry.ts +100 -0
  306. package/src/codex/write-coordination.ts +114 -0
  307. package/src/combos/failover.ts +140 -0
  308. package/src/combos/index.ts +44 -0
  309. package/src/combos/request.ts +64 -0
  310. package/src/combos/resolve.ts +232 -0
  311. package/src/combos/types.ts +392 -0
  312. package/src/config.ts +3270 -0
  313. package/src/generated/model-metadata.ts +144 -0
  314. package/src/github/star-state.ts +203 -0
  315. package/src/grok/inject.ts +540 -0
  316. package/src/grok/inspect.ts +45 -0
  317. package/src/grok/status.ts +127 -0
  318. package/src/grok/sync.ts +66 -0
  319. package/src/images/artifacts.ts +516 -0
  320. package/src/images/fulfill-video.ts +163 -0
  321. package/src/images/fulfill.ts +149 -0
  322. package/src/images/index.ts +4 -0
  323. package/src/images/loop.ts +922 -0
  324. package/src/images/plan.ts +133 -0
  325. package/src/images/synthetic-tool.ts +133 -0
  326. package/src/images/types.ts +41 -0
  327. package/src/images/xai-client.ts +141 -0
  328. package/src/images/xai-video-client.ts +163 -0
  329. package/src/index.ts +22 -0
  330. package/src/integrations/config-io.ts +151 -0
  331. package/src/integrations/journal.ts +315 -0
  332. package/src/integrations/merge.ts +135 -0
  333. package/src/integrations/native/ownership-preflight.ts +202 -0
  334. package/src/integrations/ownership.ts +111 -0
  335. package/src/integrations/registry.ts +108 -0
  336. package/src/integrations/serialize.ts +235 -0
  337. package/src/integrations/state.ts +290 -0
  338. package/src/integrations/store.ts +103 -0
  339. package/src/integrations/writer.ts +492 -0
  340. package/src/lib/abort.ts +146 -0
  341. package/src/lib/admin-secrets.ts +25 -0
  342. package/src/lib/admission.ts +83 -0
  343. package/src/lib/app-owned-memory-stores.ts +173 -0
  344. package/src/lib/app-owned-memory.ts +265 -0
  345. package/src/lib/bounded-body.ts +242 -0
  346. package/src/lib/bun-binary-validator.d.mts +3 -0
  347. package/src/lib/bun-binary-validator.mjs +18 -0
  348. package/src/lib/bun-runtime.ts +184 -0
  349. package/src/lib/bun-stream-caps.ts +127 -0
  350. package/src/lib/config-ownership.ts +438 -0
  351. package/src/lib/crash-guard.ts +344 -0
  352. package/src/lib/debug-log-buffer.ts +83 -0
  353. package/src/lib/debug-settings.ts +108 -0
  354. package/src/lib/debug.ts +31 -0
  355. package/src/lib/destination-policy.ts +316 -0
  356. package/src/lib/errors.ts +364 -0
  357. package/src/lib/eventstream-decoder.ts +253 -0
  358. package/src/lib/gcp-adc.ts +341 -0
  359. package/src/lib/injection-debug-log.ts +58 -0
  360. package/src/lib/local-management-attestation.ts +51 -0
  361. package/src/lib/open-url.ts +25 -0
  362. package/src/lib/pinned-http.ts +182 -0
  363. package/src/lib/privacy.ts +20 -0
  364. package/src/lib/process-control.ts +168 -0
  365. package/src/lib/provider-environment.ts +470 -0
  366. package/src/lib/provider-outbound.ts +203 -0
  367. package/src/lib/provider-url.ts +14 -0
  368. package/src/lib/proxy-env.ts +18 -0
  369. package/src/lib/redact.ts +510 -0
  370. package/src/lib/remodex-home.ts +616 -0
  371. package/src/lib/retry-after.ts +55 -0
  372. package/src/lib/service-secrets.ts +178 -0
  373. package/src/lib/shadow-call.ts +54 -0
  374. package/src/lib/sidecar-tracker.ts +52 -0
  375. package/src/lib/sse-decoder.ts +364 -0
  376. package/src/lib/state-store-registrations.ts +109 -0
  377. package/src/lib/state-store-sweeper.ts +184 -0
  378. package/src/lib/system-restart-contract.ts +73 -0
  379. package/src/lib/test-home-guard.ts +98 -0
  380. package/src/lib/token-estimate.ts +69 -0
  381. package/src/lib/translator-budget.ts +366 -0
  382. package/src/lib/upstream-reachability.ts +91 -0
  383. package/src/lib/upstream-retry.ts +508 -0
  384. package/src/lib/win-exec.ts +115 -0
  385. package/src/lib/win-paths.ts +68 -0
  386. package/src/lib/windows-elevation.ts +705 -0
  387. package/src/lib/windows-secret-acl.ts +817 -0
  388. package/src/lib/windows-user-principal.ts +283 -0
  389. package/src/lib/winsw.ts +402 -0
  390. package/src/model-sources.ts +73 -0
  391. package/src/oauth/anthropic-routing.ts +594 -0
  392. package/src/oauth/anthropic.ts +177 -0
  393. package/src/oauth/callback-server.ts +294 -0
  394. package/src/oauth/chatgpt.ts +150 -0
  395. package/src/oauth/command-code.ts +239 -0
  396. package/src/oauth/cursor.ts +231 -0
  397. package/src/oauth/github-copilot.ts +428 -0
  398. package/src/oauth/google-antigravity.ts +230 -0
  399. package/src/oauth/health.ts +443 -0
  400. package/src/oauth/index.ts +1280 -0
  401. package/src/oauth/key-providers.ts +128 -0
  402. package/src/oauth/kimi.ts +213 -0
  403. package/src/oauth/kiro-credentials.ts +726 -0
  404. package/src/oauth/kiro.ts +621 -0
  405. package/src/oauth/local-token-detect.ts +121 -0
  406. package/src/oauth/log.ts +48 -0
  407. package/src/oauth/login-cli.ts +163 -0
  408. package/src/oauth/pkce.ts +15 -0
  409. package/src/oauth/store.ts +655 -0
  410. package/src/oauth/token-guardian.ts +309 -0
  411. package/src/oauth/types.ts +62 -0
  412. package/src/oauth/xai.ts +241 -0
  413. package/src/providers/alibaba-region-backup.ts +75 -0
  414. package/src/providers/alibaba-region-migration.ts +156 -0
  415. package/src/providers/alibaba-region-startup.ts +36 -0
  416. package/src/providers/antigravity-models.ts +317 -0
  417. package/src/providers/api-keys.ts +140 -0
  418. package/src/providers/base-url-choices.ts +64 -0
  419. package/src/providers/codex-capacity.ts +288 -0
  420. package/src/providers/command-code-efforts.ts +85 -0
  421. package/src/providers/context-cap.ts +73 -0
  422. package/src/providers/derive.ts +451 -0
  423. package/src/providers/free-directory.ts +187 -0
  424. package/src/providers/github-copilot-transport.ts +56 -0
  425. package/src/providers/google-vertex-location.ts +14 -0
  426. package/src/providers/key-failover.ts +271 -0
  427. package/src/providers/kiro-models.ts +67 -0
  428. package/src/providers/label.ts +19 -0
  429. package/src/providers/model-discovery-limits.ts +16 -0
  430. package/src/providers/model-discovery.ts +361 -0
  431. package/src/providers/openai-sidecar.ts +235 -0
  432. package/src/providers/openai-tier-startup.ts +27 -0
  433. package/src/providers/openai-tiers.ts +301 -0
  434. package/src/providers/openai-virtual-models.ts +83 -0
  435. package/src/providers/openrouter-routing.ts +102 -0
  436. package/src/providers/provider-id-rewrite.ts +179 -0
  437. package/src/providers/quota.ts +1942 -0
  438. package/src/providers/reasoning-capabilities.ts +336 -0
  439. package/src/providers/registry.ts +2375 -0
  440. package/src/providers/slug-codec.ts +74 -0
  441. package/src/providers/xai-transport.ts +149 -0
  442. package/src/reasoning-effort.ts +243 -0
  443. package/src/responses/compaction.ts +124 -0
  444. package/src/responses/hosted-tool-policy.ts +9 -0
  445. package/src/responses/parser.ts +714 -0
  446. package/src/responses/reasoning-envelope.ts +60 -0
  447. package/src/responses/reasoning-replay-cache.ts +106 -0
  448. package/src/responses/schema.ts +159 -0
  449. package/src/responses/spill-store.ts +431 -0
  450. package/src/responses/state.ts +1039 -0
  451. package/src/responses/tool-groups.ts +19 -0
  452. package/src/router.ts +802 -0
  453. package/src/routing/analytics.ts +377 -0
  454. package/src/routing/capability.ts +205 -0
  455. package/src/routing/cost.ts +77 -0
  456. package/src/routing/evaluator.ts +444 -0
  457. package/src/routing/health.ts +401 -0
  458. package/src/routing/history/cursor.ts +43 -0
  459. package/src/routing/history/indexer.ts +605 -0
  460. package/src/routing/history/schema.ts +72 -0
  461. package/src/routing/profile-namespace.ts +15 -0
  462. package/src/routing/profile.ts +424 -0
  463. package/src/routing/quota.ts +145 -0
  464. package/src/routing/request-evidence.ts +45 -0
  465. package/src/routing/trace.ts +686 -0
  466. package/src/server/adapter-resolve.ts +83 -0
  467. package/src/server/auth-cors.ts +606 -0
  468. package/src/server/chat-completions.ts +379 -0
  469. package/src/server/claude-messages.ts +980 -0
  470. package/src/server/effort-policy.ts +251 -0
  471. package/src/server/github-copilot-responses-repair.ts +338 -0
  472. package/src/server/gui-static.ts +152 -0
  473. package/src/server/image-retry.ts +42 -0
  474. package/src/server/images.ts +485 -0
  475. package/src/server/index.ts +1633 -0
  476. package/src/server/lifecycle.ts +482 -0
  477. package/src/server/live.ts +609 -0
  478. package/src/server/management/agent-settings-routes.ts +1180 -0
  479. package/src/server/management/android-remote-routes.ts +390 -0
  480. package/src/server/management/api-access.ts +141 -0
  481. package/src/server/management/api-key-usage.ts +167 -0
  482. package/src/server/management/body.ts +35 -0
  483. package/src/server/management/combo-routes.ts +244 -0
  484. package/src/server/management/config-routes.ts +602 -0
  485. package/src/server/management/context.ts +88 -0
  486. package/src/server/management/integration-routes.ts +538 -0
  487. package/src/server/management/logs-usage-routes.ts +516 -0
  488. package/src/server/management/model-routes.ts +519 -0
  489. package/src/server/management/model-rows.ts +143 -0
  490. package/src/server/management/native-integration-routes.ts +781 -0
  491. package/src/server/management/oauth-account-routes.ts +573 -0
  492. package/src/server/management/provider-routes.ts +781 -0
  493. package/src/server/management/request-history-routes.ts +191 -0
  494. package/src/server/management/routing-analytics-routes.ts +74 -0
  495. package/src/server/management/routing-profile-routes.ts +384 -0
  496. package/src/server/management/shared.ts +277 -0
  497. package/src/server/management/sidebar-routes.ts +106 -0
  498. package/src/server/management/sync-response.ts +69 -0
  499. package/src/server/management/system-restart.ts +433 -0
  500. package/src/server/management/system-routes.ts +141 -0
  501. package/src/server/management/usage-summary-cache.ts +86 -0
  502. package/src/server/management-api.ts +269 -0
  503. package/src/server/management-auth.ts +353 -0
  504. package/src/server/memory-watchdog.ts +156 -0
  505. package/src/server/port-reclaim.ts +307 -0
  506. package/src/server/ports.ts +156 -0
  507. package/src/server/proxy-liveness.ts +326 -0
  508. package/src/server/proxy-stop.ts +92 -0
  509. package/src/server/readiness.ts +99 -0
  510. package/src/server/relay-eager.ts +353 -0
  511. package/src/server/relay.ts +1179 -0
  512. package/src/server/request-decompress.ts +132 -0
  513. package/src/server/request-log-conversation.ts +168 -0
  514. package/src/server/request-log.ts +1072 -0
  515. package/src/server/responses/collaboration.ts +409 -0
  516. package/src/server/responses/compact.ts +710 -0
  517. package/src/server/responses/core.ts +3561 -0
  518. package/src/server/responses/encrypted-payload.ts +308 -0
  519. package/src/server/responses/fetch-helpers.ts +171 -0
  520. package/src/server/responses/passthrough-error.ts +78 -0
  521. package/src/server/responses/policy-fallback.ts +152 -0
  522. package/src/server/responses/terminal-guard.ts +230 -0
  523. package/src/server/responses/upstream-error.ts +48 -0
  524. package/src/server/responses-image-gen-repair.ts +132 -0
  525. package/src/server/responses-item-id-repair.ts +272 -0
  526. package/src/server/responses-json-events.ts +52 -0
  527. package/src/server/responses-model-rewrite.ts +29 -0
  528. package/src/server/responses-snapshot-repair.ts +621 -0
  529. package/src/server/responses.ts +10 -0
  530. package/src/server/search.ts +181 -0
  531. package/src/server/sse-frame-buffer.ts +292 -0
  532. package/src/server/sse-payload-rewrite.ts +263 -0
  533. package/src/server/startup-action-control.ts +308 -0
  534. package/src/server/startup-health-cache.ts +119 -0
  535. package/src/server/system-env.ts +418 -0
  536. package/src/server/windows-tcp-drop.ts +184 -0
  537. package/src/server/windows-tray-control.ts +41 -0
  538. package/src/server/ws-bridge.ts +470 -0
  539. package/src/service-manager-probe.ts +824 -0
  540. package/src/service.ts +3011 -0
  541. package/src/stall-timeout.ts +20 -0
  542. package/src/storage/cleanup-job.ts +57 -0
  543. package/src/storage/cleanup.ts +3085 -0
  544. package/src/storage/policy-job.ts +457 -0
  545. package/src/storage/policy-scheduler.ts +40 -0
  546. package/src/storage/policy-worker.ts +59 -0
  547. package/src/storage/policy.ts +527 -0
  548. package/src/storage/restore-job.ts +299 -0
  549. package/src/storage/restore-worker.ts +58 -0
  550. package/src/storage/scanner.ts +238 -0
  551. package/src/storage/storage-mutation-coordinator.ts +139 -0
  552. package/src/storage/worker-lifecycle.ts +215 -0
  553. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  554. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  555. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  556. package/src/tray/assets/opencodex-tray.png +0 -0
  557. package/src/tray/windows-tray.ps1 +364 -0
  558. package/src/tray/windows.ts +738 -0
  559. package/src/types.ts +1531 -0
  560. package/src/update/badge.ts +72 -0
  561. package/src/update/desktop-release.ts +1620 -0
  562. package/src/update/index.ts +402 -0
  563. package/src/update/job.ts +1906 -0
  564. package/src/update/notify.ts +261 -0
  565. package/src/update/npm-cache-preflight.d.mts +47 -0
  566. package/src/update/npm-cache-preflight.mjs +201 -0
  567. package/src/update/npm-invocation.d.mts +23 -0
  568. package/src/update/npm-invocation.mjs +94 -0
  569. package/src/update/tray-update-plan.d.mts +18 -0
  570. package/src/update/tray-update-plan.mjs +38 -0
  571. package/src/usage/cost.ts +0 -0
  572. package/src/usage/debug.ts +97 -0
  573. package/src/usage/expected-prices.ts +283 -0
  574. package/src/usage/log.ts +695 -0
  575. package/src/usage/summary.ts +585 -0
  576. package/src/usage/totals.ts +14 -0
  577. package/src/vision/anthropic-describe.ts +185 -0
  578. package/src/vision/describe.ts +127 -0
  579. package/src/vision/index.ts +558 -0
  580. package/src/vision/reasoning.ts +55 -0
  581. package/src/web-search/anthropic-executor.ts +189 -0
  582. package/src/web-search/executor.ts +105 -0
  583. package/src/web-search/format-result.ts +89 -0
  584. package/src/web-search/index.ts +196 -0
  585. package/src/web-search/loop.ts +791 -0
  586. package/src/web-search/parse.ts +235 -0
  587. package/src/web-search/progress-stream.ts +342 -0
  588. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,1280 @@
1
+ import type { KiroOAuthMetadata, OAuthController, OAuthCredentials } from "./types";
2
+ import { parseCallbackInput } from "./callback-server";
3
+ import type { OcxConfig, OcxProviderConfig, RefreshPolicy } from "../types";
4
+ import { loadConfig, resolveEnvValue, saveConfig } from "../config";
5
+ import { maskEmail } from "../lib/privacy";
6
+ import { KiroTokenRefreshError, environmentKiroRoutingMetadata, loginKiro, refreshKiroToken, settleKiroLoginTransaction } from "./kiro";
7
+ import { getAccountCredential, getAccountSet, removeAccount, saveAccountCredential, saveCredential, setActiveAccount, getCredential, credentialGeneration, createOAuthRefreshIntentLock, mergeAccountCredential, markAccountNeedsReauthIfGeneration, readOAuthRefreshIntent, writeOAuthRefreshIntent, markOAuthRefreshIntentStaleOwner, clearOAuthRefreshIntent, normalizeAuthStoreBuffer, OAuthMutationBusyError } from "./store";
8
+ import { loginXai, refreshXaiToken, XAI_LOCAL_CLI_DETACH_WARNING, XaiTokenRequestError } from "./xai";
9
+ import { ANTHROPIC_OAUTH_BETA, AnthropicTokenError, loginAnthropic, refreshAnthropicToken } from "./anthropic";
10
+ import { loginKimi, refreshKimiToken } from "./kimi";
11
+ import { loginChatGPT, refreshChatGPTToken } from "./chatgpt";
12
+ import { loginAntigravity, refreshAntigravityToken } from "./google-antigravity";
13
+ import { loginCursor, refreshCursorToken } from "./cursor";
14
+ import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUrl } from "./github-copilot";
15
+ import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
16
+ import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
17
+ import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
18
+ import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
19
+ import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../providers/registry";
20
+ import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
21
+ import { resolveProviderTransport } from "../providers/xai-transport";
22
+ import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
23
+ import { logOAuthEvent } from "./log";
24
+ import { captureConfigGeneration, sweepExpiredOnWrite, type GenerationContext } from "../lib/state-store-sweeper";
25
+ import { retainedUtf8Bytes } from "../lib/admission";
26
+ import { randomUUID } from "node:crypto";
27
+ export {
28
+ CODEX_HEALTH_AUTH_FAILED_NOTE,
29
+ CODEX_HEALTH_MANAGEMENT_API_UNAVAILABLE_NOTE,
30
+ CODEX_HEALTH_UNAVAILABLE_NOTE,
31
+ MASKED_ACCOUNT_FALLBACK,
32
+ collectOAuthHealthEntries,
33
+ collectOAuthHealthEntriesForCli,
34
+ detectOAuthWarning,
35
+ oauthAccountHealthFields,
36
+ oauthHealthLabel,
37
+ oauthHealthSummary,
38
+ projectCodexAccountHealth,
39
+ projectOAuthAccountHealth,
40
+ projectStoredOAuthAccountHealth,
41
+ type CodexHealthSource,
42
+ type OAuthAccountHealth,
43
+ type OAuthAccountHealthFields,
44
+ type OAuthCliHealthReport,
45
+ type OAuthHealthEntry,
46
+ type OAuthHealthLabel,
47
+ } from "./health";
48
+ export { OAUTH_REFRESH_LOCK_WAIT_MS, peekAuthStore, peekOAuthRefreshIntent } from "./store";
49
+ import { codexAccountNamespaceProviderCollisionError } from "../codex/account-namespace-match";
50
+
51
+ const REFRESH_SKEW_MS = 60_000;
52
+ export interface OAuthAccessSnapshot {
53
+ provider: string;
54
+ accountId: string;
55
+ generation: string;
56
+ accessToken: string;
57
+ /** Cloud Code Assist project selected during Antigravity login. */
58
+ projectId?: string;
59
+ /** Safe request-routing subset; refresh-only Kiro client secrets never leave the credential store. */
60
+ kiro?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion">;
61
+ }
62
+
63
+ export interface ObservedOAuthAccessSnapshot extends OAuthAccessSnapshot {
64
+ /** Allowlisted provider API origin consumed by GitHub Copilot model discovery. */
65
+ apiBaseUrl?: string;
66
+ }
67
+
68
+ export type OAuthActiveTokenObservation =
69
+ | { readonly kind: "available"; readonly snapshot: ObservedOAuthAccessSnapshot }
70
+ | { readonly kind: "missing" }
71
+ | { readonly kind: "malformed" }
72
+ | { readonly kind: "needs-reauth" }
73
+ | { readonly kind: "expired" }
74
+ | { readonly kind: "near-expiry" }
75
+ | { readonly kind: "unsupported" };
76
+
77
+ const MAX_OAUTH_TOKEN_REFRESH_FLIGHTS = 32;
78
+ const OAUTH_TOKEN_REFRESH_FLIGHT_STALE_MS = 120_000;
79
+ interface OAuthRefreshFlightEvidence { flightId: string; dispatched: boolean }
80
+ interface OAuthTokenRefreshFlight extends OAuthRefreshFlightEvidence { promise: Promise<OAuthAccessSnapshot>; startedAt: number; abort: AbortController }
81
+ const tokenRefreshes = new Map<string, OAuthTokenRefreshFlight>();
82
+ export class OAuthTokenRefreshBusyError extends Error {
83
+ readonly code = "OAUTH_TOKEN_REFRESH_BUSY";
84
+ readonly retryable = true;
85
+ constructor() { super("OAuth token refresh capacity reached"); this.name = "OAuthTokenRefreshBusyError"; }
86
+ }
87
+ export class OAuthTokenRefreshStaleError extends Error {
88
+ readonly code = "OAUTH_TOKEN_REFRESH_STALE";
89
+ readonly retryable = true;
90
+ constructor() { super("OAuth token refresh owner became stale"); this.name = "OAuthTokenRefreshStaleError"; }
91
+ }
92
+
93
+ /** Focused owner-identity tests only. Synthetic owners retain no account data. */
94
+ export function seedOAuthTokenRefreshFlightsForTests(rows: Array<{ key: string; startedAt?: number; flightId?: string; dispatched?: boolean }>): {
95
+ promises: Promise<OAuthAccessSnapshot>[];
96
+ cleanup: () => void;
97
+ } {
98
+ const inserted: OAuthTokenRefreshFlight[] = [];
99
+ const promises = rows.map(({ key, startedAt, flightId, dispatched }) => {
100
+ const abort = new AbortController();
101
+ const promise = new Promise<OAuthAccessSnapshot>((_resolve, reject) => {
102
+ abort.signal.addEventListener("abort", () => reject(abort.signal.reason), { once: true });
103
+ });
104
+ const flight = { promise, startedAt: startedAt ?? Date.now(), abort, flightId: flightId ?? randomUUID(), dispatched: dispatched ?? false };
105
+ tokenRefreshes.set(key, flight);
106
+ inserted.push(flight);
107
+ return promise;
108
+ });
109
+ return {
110
+ promises,
111
+ cleanup() {
112
+ for (const [key, flight] of tokenRefreshes) {
113
+ if (!inserted.includes(flight)) continue;
114
+ tokenRefreshes.delete(key);
115
+ flight.abort.abort(new Error("test cleanup"));
116
+ }
117
+ },
118
+ };
119
+ }
120
+ const XAI_PERMANENT_FAILURE_TTL_MS=30_000;
121
+ const permanentRefreshFailures=new Map<string,number>();
122
+ interface XaiRefreshDeps { intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; now?:()=>number; afterPrePersistRead?:()=>void|Promise<void>; signal?: AbortSignal }
123
+ interface AnthropicRefreshDeps { intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; now?:()=>number; afterPrePersistRead?:()=>void|Promise<void>; signal?: AbortSignal; flight?: OAuthRefreshFlightEvidence; replacedStaleFlight?: OAuthRefreshFlightEvidence }
124
+ interface GenericRefreshDeps { intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; afterPrePersistRead?:()=>void|Promise<void>; signal?: AbortSignal }
125
+ function verdictKey(p:string,a:string,c:OAuthCredentials){return `${p}\0${a}\0${credentialGeneration(c)}`;}
126
+ function cached(p:string,a:string,c:OAuthCredentials,now:()=>number){const k=verdictKey(p,a,c),u=permanentRefreshFailures.get(k);if(u===undefined)return false;if(u<=now()){permanentRefreshFailures.delete(k);return false;}return true;}
127
+ export function sweepExpiredXaiPermanentFailureVerdicts(now=Date.now()):number{let removed=0;for(const[key,until]of permanentRefreshFailures){if(until>now)continue;permanentRefreshFailures.delete(key);removed+=1;}return removed;}
128
+
129
+ export interface LoginOpts { forceLogin?: boolean; /** When set, persist into this account slot and require matching identity. */ reauthAccountId?: string }
130
+
131
+ export interface LoginFlowLifecycle {
132
+ /** Runs after background credential/config persistence settles, before status becomes done. */
133
+ onSettled?: () => void | Promise<void>;
134
+ }
135
+
136
+ interface OAuthProviderDef {
137
+ login(ctrl: OAuthController, opts?: LoginOpts): Promise<OAuthCredentials>;
138
+ refresh(
139
+ refreshToken: string,
140
+ signal?: AbortSignal,
141
+ credential?: OAuthCredentials,
142
+ ): Promise<OAuthCredentials>;
143
+ /** provider entry written into config.json on first login. */
144
+ providerConfig: OcxProviderConfig;
145
+ defaultModel: string;
146
+ /**
147
+ * Built-in proactive-refresh policy, risk-tiered by the provider's ToS exposure (devlog
148
+ * 260703_oauth-multi-account-refresh-and-tos). A user's per-provider `config.providers[x].refreshPolicy`
149
+ * overrides this. Default when unset here: "lazy-only".
150
+ */
151
+ defaultRefreshPolicy?: RefreshPolicy;
152
+ }
153
+
154
+ function oauthConfig(id: string): OcxProviderConfig {
155
+ const config = deriveOAuthProviderConfig(id);
156
+ if (!config) throw new Error(`OAuth provider missing from registry: ${id}`);
157
+ return config;
158
+ }
159
+
160
+ function oauthDefaultModel(id: string): string {
161
+ const model = deriveOAuthDefaultModel(id);
162
+ if (!model) throw new Error(`OAuth provider missing default model in registry: ${id}`);
163
+ return model;
164
+ }
165
+
166
+ export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
167
+ "command-code": {
168
+ // Add-account/reauth must not reimport the current local CLI credential.
169
+ login: (ctrl, opts) => loginCommandCode(ctrl, { importLocal: opts?.forceLogin ? "off" : "fallback" }),
170
+ refresh: refreshCommandCodeToken,
171
+ providerConfig: oauthConfig("command-code"),
172
+ defaultModel: oauthDefaultModel("command-code"),
173
+ defaultRefreshPolicy: "disabled",
174
+ },
175
+ xai: {
176
+ // forceLogin skips the local grok-cli import so a SECOND account can be chosen in the browser.
177
+ login: (ctrl, opts) => loginXai(ctrl, { importLocal: opts?.forceLogin ? "off" : "fallback" }),
178
+ refresh: refreshXaiToken,
179
+ providerConfig: oauthConfig("xai"),
180
+ defaultModel: oauthDefaultModel("xai"),
181
+ },
182
+ anthropic: {
183
+ login: (ctrl, opts) => loginAnthropic(ctrl, { importLocal: opts?.forceLogin ? "off" : "fallback" }),
184
+ refresh: refreshAnthropicToken,
185
+ providerConfig: oauthConfig("anthropic"),
186
+ defaultModel: oauthDefaultModel("anthropic"),
187
+ // Anthropic actively server-side-blocks subscription OAuth outside its own clients (Feb 2026).
188
+ // Never generate background refresh traffic for it — grade 20, highest ToS risk.
189
+ defaultRefreshPolicy: "disabled",
190
+ },
191
+ kimi: {
192
+ login: (ctrl) => loginKimi(ctrl),
193
+ refresh: refreshKimiToken,
194
+ providerConfig: oauthConfig("kimi"),
195
+ defaultModel: oauthDefaultModel("kimi"),
196
+ },
197
+ kiro: {
198
+ login: (ctrl, opts) => loginKiro(ctrl, { forceLogin: opts?.forceLogin }),
199
+ refresh: (rt, signal, credential) => refreshKiroToken(rt, signal, credential),
200
+ providerConfig: oauthConfig("kiro"),
201
+ defaultModel: oauthDefaultModel("kiro"),
202
+ },
203
+ "google-antigravity": {
204
+ login: (ctrl, opts) => loginAntigravity(ctrl, { forceAccountSelect: opts?.forceLogin === true }),
205
+ refresh: refreshAntigravityToken,
206
+ providerConfig: oauthConfig("google-antigravity"),
207
+ defaultModel: oauthDefaultModel("google-antigravity"),
208
+ },
209
+ cursor: {
210
+ login: (ctrl, opts) => loginCursor(ctrl, undefined, { forceLogin: opts?.forceLogin }),
211
+ refresh: refreshCursorToken,
212
+ providerConfig: oauthConfig("cursor"),
213
+ defaultModel: oauthDefaultModel("cursor"),
214
+ },
215
+ "github-copilot": {
216
+ login: (ctrl) => loginGithubCopilot(ctrl),
217
+ refresh: (rt, signal) => refreshGithubCopilotToken(rt, signal),
218
+ providerConfig: oauthConfig("github-copilot"),
219
+ defaultModel: oauthDefaultModel("github-copilot"),
220
+ // Unofficial Copilot bridge — keep proactive traffic lazy-only (no background guardian spam).
221
+ defaultRefreshPolicy: "lazy-only",
222
+ },
223
+ chatgpt: {
224
+ login: loginChatGPT,
225
+ refresh: (rt) => refreshChatGPTToken(rt),
226
+ providerConfig: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward" as const },
227
+ defaultModel: "gpt-5.4",
228
+ },
229
+ };
230
+
231
+ export function isOAuthProvider(name: string): boolean {
232
+ return name in OAUTH_PROVIDERS;
233
+ }
234
+
235
+ export function isPublicOAuthProvider(name: string): boolean {
236
+ return name !== "chatgpt" && isOAuthProvider(name);
237
+ }
238
+
239
+ function isRefreshPolicy(value: unknown): value is RefreshPolicy {
240
+ return value === "proactive" || value === "lazy-only" || value === "disabled";
241
+ }
242
+
243
+ /**
244
+ * The effective proactive-refresh policy for a provider: the user's per-provider
245
+ * `config.providers[provider].refreshPolicy` if set, else the provider def's risk-tiered default,
246
+ * else "lazy-only". The guardian acts only when this resolves to "proactive".
247
+ */
248
+ export function resolveRefreshPolicy(provider: string, config: OcxConfig): RefreshPolicy {
249
+ const override = config.providers[provider]?.refreshPolicy;
250
+ if (isRefreshPolicy(override)) return override;
251
+ const def = OAUTH_PROVIDERS[provider];
252
+ return def?.defaultRefreshPolicy ?? "lazy-only";
253
+ }
254
+
255
+ /** The discovered project id stored on an OAuth credential (Antigravity CCA), if any. */
256
+ export function getOAuthCredentialProjectId(provider: string): string | undefined {
257
+ return getCredential(provider)?.projectId;
258
+ }
259
+
260
+ /** Allowlisted Copilot API origin from the active credential, if still valid. */
261
+ export function getOAuthCredentialApiBaseUrl(provider: string): string | undefined {
262
+ return validateCopilotApiBaseUrl(getCredential(provider)?.apiBaseUrl);
263
+ }
264
+
265
+ /** Provider ids that support real OAuth login (drives the GUI's "Log in with …" buttons). */
266
+ export function listOAuthProviders(): string[] {
267
+ return Object.keys(OAUTH_PROVIDERS).filter(isPublicOAuthProvider);
268
+ }
269
+
270
+ export class UnsupportedOAuthProviderError extends Error {
271
+ constructor(provider: string) {
272
+ super(`Unsupported OAuth provider in config: ${provider}`);
273
+ this.name = "UnsupportedOAuthProviderError";
274
+ }
275
+ }
276
+
277
+ export class OAuthLoginRequiredError extends Error {
278
+ constructor(provider: string) {
279
+ super(`Not logged in to ${provider}. Run: rmx login ${provider}`);
280
+ this.name = "OAuthLoginRequiredError";
281
+ }
282
+ }
283
+
284
+ function accessSnapshot(provider: string, accountId: string, cred: OAuthCredentials): OAuthAccessSnapshot {
285
+ const storedKiroRouting = {
286
+ ...(cred.kiro?.profileArn ? { profileArn: cred.kiro.profileArn } : {}),
287
+ ...(cred.kiro?.apiRegion ? { apiRegion: cred.kiro.apiRegion } : {}),
288
+ ...(cred.kiro?.ssoRegion ? { ssoRegion: cred.kiro.ssoRegion } : {}),
289
+ };
290
+ return {
291
+ provider,
292
+ accountId,
293
+ generation: credentialGeneration(cred),
294
+ accessToken: cred.access,
295
+ ...(cred.projectId ? { projectId: cred.projectId } : {}),
296
+ // Stored account metadata remains authoritative. Metadata-less legacy/environment credentials
297
+ // may use explicit environment routing, but never borrow the currently signed-in local CLI account.
298
+ ...(provider === "kiro"
299
+ ? {
300
+ kiro: Object.keys(storedKiroRouting).length > 0
301
+ ? storedKiroRouting
302
+ : environmentKiroRoutingMetadata() ?? {},
303
+ }
304
+ : {}),
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Observe the active OAuth token from an auth-store buffer supplied by its filesystem owner.
310
+ * Missing, malformed, reauth-required, and expiring credentials are typed no-token outcomes;
311
+ * this path never refreshes, locks, hardens, backs up, or persists credentials.
312
+ */
313
+ export function observeActiveOAuthAccessToken(
314
+ provider: string,
315
+ authStoreBuffer: Uint8Array | null,
316
+ now = Date.now(),
317
+ ): OAuthActiveTokenObservation {
318
+ const authStore = normalizeAuthStoreBuffer(authStoreBuffer);
319
+ if (authStore.kind === "absent") return { kind: "missing" };
320
+ if (authStore.kind === "malformed") return { kind: "malformed" };
321
+ if (!isOAuthProvider(provider)) return { kind: "unsupported" };
322
+
323
+ const accountSet = authStore.store[provider];
324
+ const account = accountSet?.accounts.find(candidate => candidate.id === accountSet.activeAccountId);
325
+ if (!account) return { kind: "missing" };
326
+ if (account.needsReauth) return { kind: "needs-reauth" };
327
+ if (account.credential.expires <= now) return { kind: "expired" };
328
+ if (account.credential.expires <= now + REFRESH_SKEW_MS) return { kind: "near-expiry" };
329
+
330
+ const apiBaseUrl = validateCopilotApiBaseUrl(account.credential.apiBaseUrl);
331
+ return {
332
+ kind: "available",
333
+ snapshot: {
334
+ ...accessSnapshot(provider, account.id, account.credential),
335
+ ...(apiBaseUrl ? { apiBaseUrl } : {}),
336
+ },
337
+ };
338
+ }
339
+
340
+ async function resolveAccessSnapshotForAccount(
341
+ provider: string,
342
+ accountId: string,
343
+ rejectedGeneration?: string,
344
+ ): Promise<OAuthAccessSnapshot> {
345
+ const def = OAUTH_PROVIDERS[provider];
346
+ if (!def) throw new UnsupportedOAuthProviderError(provider);
347
+ const cred = getAccountCredential(provider, accountId);
348
+ if (!cred) throw new OAuthLoginRequiredError(provider);
349
+ const current = accessSnapshot(provider, accountId, cred);
350
+ if (rejectedGeneration !== undefined && current.generation !== rejectedGeneration) return current;
351
+ if (rejectedGeneration === undefined && cred.expires > Date.now() + REFRESH_SKEW_MS) return current;
352
+
353
+ const key = `${provider}\u0000${accountId}`;
354
+ let existing = tokenRefreshes.get(key);
355
+ let replacedStaleFlight: OAuthRefreshFlightEvidence | undefined;
356
+ if (existing && Date.now() - existing.startedAt <= OAUTH_TOKEN_REFRESH_FLIGHT_STALE_MS) {
357
+ logOAuthEvent("OAuth refresh joined existing operation", { provider, accountId });
358
+ return existing.promise;
359
+ }
360
+ if (existing) {
361
+ replacedStaleFlight = { flightId: existing.flightId, dispatched: existing.dispatched };
362
+ existing.abort.abort(new OAuthTokenRefreshStaleError());
363
+ if (tokenRefreshes.get(key) === existing) tokenRefreshes.delete(key);
364
+ existing = undefined;
365
+ }
366
+ if (tokenRefreshes.size >= MAX_OAUTH_TOKEN_REFRESH_FLIGHTS) throw new OAuthTokenRefreshBusyError();
367
+
368
+ const abort = new AbortController();
369
+ const flight: OAuthTokenRefreshFlight = {
370
+ promise: undefined as unknown as Promise<OAuthAccessSnapshot>,
371
+ startedAt: Date.now(),
372
+ abort,
373
+ flightId: randomUUID(),
374
+ dispatched: false,
375
+ };
376
+ const refresh = (async (): Promise<OAuthAccessSnapshot> => {
377
+ const accessToken = await refreshAndPersistAccessToken(provider, accountId, def, cred, abort.signal, flight, replacedStaleFlight);
378
+ const persisted = getAccountCredential(provider, accountId);
379
+ if (!persisted) throw new OAuthLoginRequiredError(provider);
380
+ if (persisted.access !== accessToken) {
381
+ throw new Error(`OAuth refresh persisted an unexpected access token for ${provider}`);
382
+ }
383
+ return accessSnapshot(provider, accountId, persisted);
384
+ })().catch(error => {
385
+ if (abort.signal.reason instanceof OAuthTokenRefreshStaleError) throw abort.signal.reason;
386
+ throw error;
387
+ }).finally(() => {
388
+ if (tokenRefreshes.get(key) === flight) tokenRefreshes.delete(key);
389
+ });
390
+ flight.promise = refresh;
391
+ tokenRefreshes.set(key, flight);
392
+ return refresh;
393
+ }
394
+
395
+ export async function getValidAccessTokenSnapshot(provider: string): Promise<OAuthAccessSnapshot> {
396
+ const set = getAccountSet(provider);
397
+ if (!set) throw new OAuthLoginRequiredError(provider);
398
+ return resolveAccessSnapshotForAccount(provider, set.activeAccountId);
399
+ }
400
+
401
+ /** Providers whose upstream-401 replay path may force a snapshot refresh. */
402
+ const FORCE_REFRESH_PROVIDERS = new Set(["xai", "github-copilot", "kiro"]);
403
+
404
+ export async function forceRefreshOAuthAccessSnapshot(
405
+ rejected: OAuthAccessSnapshot,
406
+ ): Promise<OAuthAccessSnapshot> {
407
+ if (!FORCE_REFRESH_PROVIDERS.has(rejected.provider)) throw new UnsupportedOAuthProviderError(rejected.provider);
408
+ return resolveAccessSnapshotForAccount(rejected.provider, rejected.accountId, rejected.generation);
409
+ }
410
+
411
+ /** Return a valid access token for the ACTIVE account, refreshing + persisting if expired. */
412
+ export async function getValidAccessToken(provider: string): Promise<string> {
413
+ return (await getValidAccessTokenSnapshot(provider)).accessToken;
414
+ }
415
+
416
+ /**
417
+ * Account-scoped token resolver (multiauth): refresh is single-flighted per
418
+ * (provider, account), and the rotated credential is persisted for THAT account only —
419
+ * a guardian refresh of a background account never switches the active account.
420
+ */
421
+ export async function getValidAccessTokenForAccount(provider: string, accountId: string): Promise<string> {
422
+ return (await resolveAccessSnapshotForAccount(provider, accountId)).accessToken;
423
+ }
424
+
425
+ /** Terminal refresh failures (revoked/rotated-away grants) — retrying cannot succeed. */
426
+ function isTerminalRefreshError(err: unknown): boolean {
427
+ const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
428
+ return msg.includes("invalid_grant")
429
+ || msg.includes("refresh_token_reused")
430
+ || msg.includes("revoked")
431
+ // GitHub Copilot refresh surfaces allowlisted OAuth codes (github-copilot.ts):
432
+ || msg.includes("access_denied")
433
+ || msg.includes("expired_token");
434
+ }
435
+ function terminal(error:unknown):boolean{
436
+ if(error instanceof XaiTokenRequestError)return ["invalid_grant","refresh_token_reused","revoked_token"].includes(error.oauthError??"");
437
+ if(error instanceof AnthropicTokenError)return (error.httpStatus===400||error.httpStatus===401)&&["invalid_grant","refresh_token_reused","revoked","revoked_token","refresh_token_revoked"].includes(error.oauthError??"");
438
+ if(error instanceof KiroTokenRefreshError)return (error.httpStatus===400||error.httpStatus===401)&&error.oauthError!==undefined;
439
+ return isTerminalRefreshError(error);
440
+ }
441
+ function authoritative(stored:OAuthCredentials,active:boolean,now:()=>number):OAuthCredentials{if(stored.source!=="local-cli")return stored;const disk=detectGrokCliToken();if(!disk)return stored;const allowed=isSameGrokIdentity(stored,disk)||(active&&!hasComparableGrokIdentity(stored,disk));return allowed&&shouldAdoptGrokGeneration(stored,disk,now(),REFRESH_SKEW_MS)?disk:stored;}
442
+ function merged(fresh: OAuthCredentials, previous: OAuthCredentials): OAuthCredentials {
443
+ return {
444
+ ...fresh,
445
+ source: previous.source === "local-cli" ? "oauth" : fresh.source ?? previous.source ?? "oauth",
446
+ ...(fresh.projectId === undefined && previous.projectId ? { projectId: previous.projectId } : {}),
447
+ ...(fresh.apiBaseUrl === undefined && previous.apiBaseUrl ? { apiBaseUrl: previous.apiBaseUrl } : {}),
448
+ ...(fresh.email === undefined && previous.email ? { email: previous.email } : {}),
449
+ ...(fresh.accountId === undefined && previous.accountId ? { accountId: previous.accountId } : {}),
450
+ ...(fresh.kiro === undefined && previous.kiro ? { kiro: previous.kiro } : {}),
451
+ };
452
+ }
453
+ export async function refreshXaiAccountWithLock(provider:string,accountId:string,def:OAuthProviderDef,callerCredential:OAuthCredentials,deps:XaiRefreshDeps={}):Promise<string>{const writerGeneration=captureConfigGeneration();const now=deps.now??Date.now;const guard=await(deps.intentLock??createOAuthRefreshIntentLock(provider,accountId)).acquire();try{const stored=getAccountCredential(provider,accountId);if(!stored)throw new OAuthLoginRequiredError(provider);const active=getAccountSet(provider)?.activeAccountId===accountId,candidate=authoritative(stored,active,now);if(credentialGeneration(candidate)!==credentialGeneration(callerCredential)&&candidate.expires>now()+REFRESH_SKEW_MS){if(credentialGeneration(candidate)!==credentialGeneration(stored)){const o=await mergeAccountCredential(provider,accountId,candidate,{expectedGeneration:credentialGeneration(stored),afterPrePersistRead:deps.afterPrePersistRead});if(o.superseded){if(o.stored.expires>now()+REFRESH_SKEW_MS)return o.stored.access;throw new OAuthLoginRequiredError(provider);}}return candidate.access;}if(cached(provider,accountId,candidate,now))throw new OAuthLoginRequiredError(provider);const generation=credentialGeneration(candidate);try{const fresh=merged(await def.refresh(candidate.refresh,deps.signal),candidate);const o=await mergeAccountCredential(provider,accountId,fresh,{expectedGeneration:generation,afterPrePersistRead:deps.afterPrePersistRead});if(o.superseded){if(o.stored.expires>now()+REFRESH_SKEW_MS)return o.stored.access;throw new OAuthLoginRequiredError(provider);}permanentRefreshFailures.delete(verdictKey(provider,accountId,candidate));if(candidate.source==="local-cli")console.warn(XAI_LOCAL_CLI_DETACH_WARNING);return fresh.access;}catch(error){if(error instanceof OAuthMutationBusyError){permanentRefreshFailures.delete(verdictKey(provider,accountId,candidate));throw error;}if(!terminal(error))throw error;const failedAt=now();permanentRefreshFailures.set(verdictKey(provider,accountId,candidate),failedAt+XAI_PERMANENT_FAILURE_TTL_MS);sweepExpiredOnWrite(failedAt);await markAccountNeedsReauthIfGeneration(provider,accountId,generation,writerGeneration);throw new OAuthLoginRequiredError(provider);}}finally{guard.release();}}
454
+
455
+ function newerClaudeCredential(stored: OAuthCredentials, now: number): OAuthCredentials | undefined {
456
+ if (stored.source !== "local-cli") return undefined;
457
+ const disk = detectClaudeCodeToken();
458
+ if (!disk || disk.expires <= now + REFRESH_SKEW_MS) return undefined;
459
+ return credentialGeneration(disk) !== credentialGeneration(stored) ? disk : undefined;
460
+ }
461
+
462
+ export async function refreshAnthropicAccountWithLock(
463
+ provider: string,
464
+ accountId: string,
465
+ def: OAuthProviderDef,
466
+ callerCredential: OAuthCredentials,
467
+ deps: AnthropicRefreshDeps = {},
468
+ ): Promise<string> {
469
+ const writerGeneration = captureConfigGeneration();
470
+ const now = deps.now ?? Date.now;
471
+ const guard = await (deps.intentLock ?? createOAuthRefreshIntentLock(provider, accountId)).acquire();
472
+ try {
473
+ const stored = getAccountCredential(provider, accountId);
474
+ if (!stored) throw new OAuthLoginRequiredError(provider);
475
+ const account = getAccountSet(provider)?.accounts.find(candidate => candidate.id === accountId);
476
+ const generation = credentialGeneration(stored);
477
+ let pendingIntent = readOAuthRefreshIntent(provider, accountId);
478
+ const disk = newerClaudeCredential(stored, now());
479
+ if (disk) {
480
+ const outcome = await mergeAccountCredential(provider, accountId, disk, {
481
+ expectedGeneration: credentialGeneration(stored),
482
+ afterPrePersistRead: deps.afterPrePersistRead,
483
+ });
484
+ if (outcome.superseded) {
485
+ if (pendingIntent) clearOAuthRefreshIntent(provider, accountId, pendingIntent.generation);
486
+ if (outcome.stored.expires > now() + REFRESH_SKEW_MS) return outcome.stored.access;
487
+ throw new OAuthLoginRequiredError(provider);
488
+ }
489
+ if (pendingIntent) clearOAuthRefreshIntent(provider, accountId, pendingIntent.generation);
490
+ return disk.access;
491
+ }
492
+ if (!pendingIntent?.uncertain && pendingIntent?.generation === generation) {
493
+ if (pendingIntent.staleOwner) throw new OAuthTokenRefreshStaleError();
494
+ if (deps.replacedStaleFlight && pendingIntent.flightId === deps.replacedStaleFlight.flightId) {
495
+ if (deps.replacedStaleFlight.dispatched) {
496
+ markOAuthRefreshIntentStaleOwner(provider, accountId, generation, deps.replacedStaleFlight.flightId);
497
+ throw new OAuthTokenRefreshStaleError();
498
+ }
499
+ clearOAuthRefreshIntent(provider, accountId, generation);
500
+ pendingIntent = undefined;
501
+ }
502
+ }
503
+ if (pendingIntent?.uncertain || pendingIntent?.generation === generation) {
504
+ await markAccountNeedsReauthIfGeneration(provider, accountId, generation, writerGeneration);
505
+ throw new OAuthLoginRequiredError(provider);
506
+ }
507
+ if (pendingIntent) clearOAuthRefreshIntent(provider, accountId, pendingIntent.generation);
508
+ if (account?.needsReauth) {
509
+ throw new OAuthLoginRequiredError(provider);
510
+ }
511
+ if (credentialGeneration(stored) !== credentialGeneration(callerCredential) && stored.expires > now() + REFRESH_SKEW_MS) {
512
+ return stored.access;
513
+ }
514
+
515
+ try {
516
+ writeOAuthRefreshIntent(provider, accountId, generation, now(), deps.flight?.flightId);
517
+ if (deps.signal?.aborted) throw deps.signal.reason;
518
+ if (deps.flight) deps.flight.dispatched = true;
519
+ const fresh = merged(await def.refresh(stored.refresh, deps.signal), stored);
520
+ const outcome = await mergeAccountCredential(provider, accountId, fresh, {
521
+ expectedGeneration: generation,
522
+ afterPrePersistRead: deps.afterPrePersistRead,
523
+ });
524
+ if (outcome.superseded) {
525
+ clearOAuthRefreshIntent(provider, accountId, generation);
526
+ if (outcome.stored.expires > now() + REFRESH_SKEW_MS) return outcome.stored.access;
527
+ throw new OAuthLoginRequiredError(provider);
528
+ }
529
+ clearOAuthRefreshIntent(provider, accountId, generation);
530
+ return fresh.access;
531
+ } catch (error) {
532
+ if (error instanceof OAuthMutationBusyError) throw error;
533
+ if (!terminal(error)) throw error;
534
+ await markAccountNeedsReauthIfGeneration(provider, accountId, generation, writerGeneration);
535
+ clearOAuthRefreshIntent(provider, accountId, generation);
536
+ throw new OAuthLoginRequiredError(provider);
537
+ }
538
+ } finally {
539
+ guard.release();
540
+ }
541
+ }
542
+
543
+ export async function refreshGenericAccountWithLock(
544
+ provider: string,
545
+ accountId: string,
546
+ def: OAuthProviderDef,
547
+ callerCredential: OAuthCredentials,
548
+ deps: GenericRefreshDeps = {},
549
+ ): Promise<string> {
550
+ const writerGeneration = captureConfigGeneration();
551
+ logOAuthEvent("OAuth refresh started", { provider, accountId });
552
+ const guard = await (deps.intentLock ?? createOAuthRefreshIntentLock(provider, accountId)).acquire();
553
+ try {
554
+ const stored = getAccountCredential(provider, accountId);
555
+ if (!stored) throw new OAuthLoginRequiredError(provider);
556
+ if (
557
+ credentialGeneration(stored) !== credentialGeneration(callerCredential)
558
+ && stored.expires > Date.now() + REFRESH_SKEW_MS
559
+ ) {
560
+ logOAuthEvent("OAuth refresh joined existing operation", { provider, accountId });
561
+ return stored.access;
562
+ }
563
+ const generation = credentialGeneration(stored);
564
+ try {
565
+ const fresh = merged(await def.refresh(stored.refresh, deps.signal, stored), stored);
566
+ const outcome = await mergeAccountCredential(provider, accountId, fresh, {
567
+ expectedGeneration: generation,
568
+ afterPrePersistRead: deps.afterPrePersistRead,
569
+ });
570
+ if (outcome.superseded) {
571
+ if (outcome.stored.expires > Date.now() + REFRESH_SKEW_MS) return outcome.stored.access;
572
+ throw new OAuthLoginRequiredError(provider);
573
+ }
574
+ logOAuthEvent("OAuth credentials rotated and persisted", { provider, accountId });
575
+ return fresh.access;
576
+ } catch (error) {
577
+ if (error instanceof OAuthMutationBusyError) throw error;
578
+ if (!terminal(error)) throw error;
579
+ await markAccountNeedsReauthIfGeneration(provider, accountId, generation, writerGeneration);
580
+ throw new OAuthLoginRequiredError(provider);
581
+ }
582
+ } finally {
583
+ guard.release();
584
+ }
585
+ }
586
+
587
+ async function refreshAndPersistAccessToken(
588
+ provider: string,
589
+ accountId: string,
590
+ def: OAuthProviderDef,
591
+ cred: OAuthCredentials,
592
+ signal?: AbortSignal,
593
+ flight?: OAuthRefreshFlightEvidence,
594
+ replacedStaleFlight?: OAuthRefreshFlightEvidence,
595
+ ): Promise<string> {
596
+ if (provider === "xai") return refreshXaiAccountWithLock(provider, accountId, def, cred, { signal });
597
+ if (provider === "anthropic") return refreshAnthropicAccountWithLock(provider, accountId, def, cred, { signal, flight, replacedStaleFlight });
598
+ return refreshGenericAccountWithLock(provider, accountId, def, cred, { signal });
599
+ }
600
+
601
+ /**
602
+ * Shared bearer-token resolver for /models listing — used by BOTH server.ts:fetchAllModels and
603
+ * codex-catalog.ts:fetchProviderModels so OAuth providers' models are listed once logged in.
604
+ * Returns undefined for forward-mode or oauth-not-logged-in (caller skips).
605
+ */
606
+ export async function resolveModelsAuthToken(name: string, prov: OcxProviderConfig): Promise<string | undefined> {
607
+ if (prov.authMode === "forward") return undefined;
608
+ if (prov.authMode === "oauth") {
609
+ try {
610
+ return await getValidAccessToken(name);
611
+ } catch {
612
+ return undefined;
613
+ }
614
+ }
615
+ return resolveEnvValue(prov.apiKey);
616
+ }
617
+
618
+ function modelDiscoveryTransportSeed(providerName: string, prov: OcxProviderConfig): OcxProviderConfig {
619
+ const entry = getProviderRegistryEntry(providerName);
620
+ if (
621
+ prov.authMode !== "oauth"
622
+ || entry?.authKind !== "oauth"
623
+ || entry.allowBaseUrlOverride === true
624
+ || /\{[^}]*\}/.test(entry.baseUrl)
625
+ || !providerMatchesRegistryTransport(providerName, prov)
626
+ ) {
627
+ return prov;
628
+ }
629
+ // Normal routing pins fixed OAuth presets before adapter-specific transport resolution.
630
+ // Discovery must do the same so a stale or modified config baseUrl never receives a token.
631
+ return { ...prov, adapter: entry.adapter, baseUrl: entry.baseUrl };
632
+ }
633
+
634
+ /**
635
+ * Provider-correct model-discovery request (URL + headers), so both model-listing paths fetch the
636
+ * LIVE catalog correctly per adapter. Anthropic is the special case: its endpoint is `/v1/models`
637
+ * (not `/models`), it needs `anthropic-version`, and it authenticates with `x-api-key` by default
638
+ * (or `Authorization: Bearer` when `apiKeyTransport = "bearer"`), plus the OAuth beta for oauth
639
+ * mode — not a bare Bearer. Google (ai-studio mode)
640
+ * is the other special case: `x-goog-api-key` + `/v1beta/models`, returning `{ models: [...] }`.
641
+ * The catalog authority gate intentionally degrades that non-OpenAI shape to stale/static data.
642
+ * Antigravity uses its CCA `:fetchAvailableModels` RPC; everyone else uses the OpenAI-style
643
+ * `/models` + Bearer with a `{ data: [{ id, owned_by? }] }` response.
644
+ */
645
+ export interface ModelsRequestObservedAuth {
646
+ readonly oauthApiBaseUrl?: string;
647
+ }
648
+
649
+ export function buildModelsRequest(
650
+ prov: OcxProviderConfig,
651
+ apiKey: string | undefined,
652
+ providerName = "",
653
+ observedAuth?: ModelsRequestObservedAuth,
654
+ ): { method?: "POST"; url: string; headers: Record<string, string> } {
655
+ const transportSeed = modelDiscoveryTransportSeed(providerName, prov);
656
+ const copilotApiBaseUrl = observedAuth === undefined
657
+ ? (providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(providerName) : undefined)
658
+ : observedAuth.oauthApiBaseUrl;
659
+ const effectiveProvider = resolveProviderTransport(
660
+ providerName,
661
+ transportSeed,
662
+ undefined,
663
+ copilotApiBaseUrl,
664
+ );
665
+ const headers: Record<string, string> = { ...(effectiveProvider.headers ?? {}) };
666
+ const discoveryUrl = (defaultUrl: string): string => resolveProviderModelDiscoveryUrl(
667
+ providerName,
668
+ prov,
669
+ effectiveProvider.baseUrl,
670
+ defaultUrl,
671
+ );
672
+ if (effectiveGoogleMode(providerName, effectiveProvider) === "cloud-code-assist") {
673
+ headers.Accept = "application/json";
674
+ headers["Content-Type"] = "application/json";
675
+ headers["User-Agent"] = ANTIGRAVITY_REQUEST_UA;
676
+ if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
677
+ return {
678
+ method: "POST",
679
+ url: discoveryUrl(`${effectiveProvider.baseUrl.replace(/\/+$/, "")}/v1internal:fetchAvailableModels`),
680
+ headers,
681
+ };
682
+ }
683
+ if (effectiveGoogleMode(providerName, effectiveProvider) === "ai-studio") {
684
+ // Generative Language API: API key goes in x-goog-api-key (never Authorization: Bearer),
685
+ // models live under /v1beta (v1 misses preview models), and pageSize maxes at 1000 —
686
+ // enough to list everything without a pageToken loop. Vertex/antigravity keep the
687
+ // generic branch (they fall back to their static model lists).
688
+ if (apiKey) headers["x-goog-api-key"] = apiKey;
689
+ return { url: discoveryUrl(`${effectiveProvider.baseUrl}/v1beta/models?pageSize=1000`), headers };
690
+ }
691
+ if (effectiveProvider.adapter === "anthropic") {
692
+ const base = effectiveProvider.baseUrl.replace(/\/v1\/?$/, "");
693
+ headers["anthropic-version"] = "2023-06-01";
694
+ if (effectiveProvider.authMode === "oauth") {
695
+ headers["anthropic-beta"] = ANTHROPIC_OAUTH_BETA;
696
+ if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
697
+ } else if (apiKey) {
698
+ if (effectiveProvider.apiKeyTransport === "bearer") headers["Authorization"] = `Bearer ${apiKey}`;
699
+ else headers["x-api-key"] = apiKey;
700
+ }
701
+ return { url: discoveryUrl(`${base}/v1/models?limit=1000`), headers };
702
+ }
703
+ if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
704
+ return { url: discoveryUrl(`${effectiveProvider.baseUrl}/models`), headers };
705
+ }
706
+
707
+ /**
708
+ * Refresh OAuth-managed provider presets (`models`, `noReasoningModels`, and a stale `defaultModel`)
709
+ * from the registry so a proxy update that revises a provider's models — e.g. dropping deprecated
710
+ * Claude snapshots or adding a new grok endpoint not in the live `/models` — reaches EXISTING
711
+ * configs on the next `rmx start`, instead of only fresh installs. The live `/models` fetch stays
712
+ * the primary source; this keeps the static fallback (and models-not-in-/models) current.
713
+ *
714
+ * Only touches providers that are registry-managed AND still `authMode: "oauth"`. Preset fields
715
+ * are refreshed, while the registry's `liveModels` default is normally filled only when no value
716
+ * is stored. Persists + returns true when anything changed.
717
+ */
718
+ function cloneProviderField(value: unknown): unknown {
719
+ if (Array.isArray(value)) return [...value];
720
+ if (value && typeof value === "object") return JSON.parse(JSON.stringify(value));
721
+ return value;
722
+ }
723
+
724
+ const OAUTH_RECONCILE_FIELDS: (keyof OcxProviderConfig)[] = [
725
+ "models",
726
+ "contextWindow",
727
+ "modelContextWindows",
728
+ "defaultMaxOutputTokens",
729
+ "modelMaxOutputTokens",
730
+ "modelInputModalities",
731
+ "noReasoningModels",
732
+ "noVisionModels",
733
+ "reasoningEfforts",
734
+ "modelReasoningEfforts",
735
+ "reasoningEffortMap",
736
+ "modelReasoningEffortMap",
737
+ "noTemperatureModels",
738
+ "noTopPModels",
739
+ "noPenaltyModels",
740
+ "autoToolChoiceOnlyModels",
741
+ "preserveReasoningContentModels",
742
+ ];
743
+
744
+ const GOOGLE_ANTIGRAVITY_PROVIDER = "google-antigravity";
745
+ const GOOGLE_ANTIGRAVITY_LIVE_DISCOVERY_VERSION = 2 as const;
746
+
747
+ /** Only migrate the three-model experimental seed; an operator's later `liveModels: false` wins. */
748
+ function isLegacyCommandCodeStaticCatalog(provider: OcxProviderConfig): boolean {
749
+ return provider.liveModels === false
750
+ && provider.defaultModel === "deepseek-v4-flash"
751
+ && JSON.stringify(provider.models) === JSON.stringify(["deepseek-v4-flash", "kimi-k3", "glm-5.2"]);
752
+ }
753
+
754
+ function isLegacyAntigravityStaticCatalog(provider: OcxProviderConfig): boolean {
755
+ return provider.liveModels === false
756
+ && provider.adapter === "google"
757
+ && provider.baseUrl === "https://daily-cloudcode-pa.googleapis.com"
758
+ && provider.authMode === "oauth"
759
+ && provider.googleMode === "cloud-code-assist"
760
+ && provider.defaultModel === "gemini-3.6-flash"
761
+ && JSON.stringify(provider.models) === JSON.stringify([
762
+ "gemini-3.6-flash",
763
+ "gemini-3.1-pro",
764
+ "gemini-3.1-flash-image",
765
+ "claude-sonnet-4-6",
766
+ "claude-opus-4-6-thinking",
767
+ "gpt-oss-120b-medium",
768
+ ]);
769
+ }
770
+
771
+ /** Promote only the versioned canonical static seed; unmarked `liveModels: false` remains user intent. */
772
+ function migrateLegacyAntigravityStaticCatalog(config: OcxConfig): boolean {
773
+ if (config.googleAntigravityStaticCatalogVersion !== 1) return false;
774
+ const provider = config.providers[GOOGLE_ANTIGRAVITY_PROVIDER];
775
+ if (provider && isLegacyAntigravityStaticCatalog(provider)) provider.liveModels = true;
776
+ config.googleAntigravityStaticCatalogVersion = GOOGLE_ANTIGRAVITY_LIVE_DISCOVERY_VERSION;
777
+ return true;
778
+ }
779
+
780
+ export function reconcileOAuthProviders(config: OcxConfig): boolean {
781
+ let changed = migrateLegacyAntigravityStaticCatalog(config);
782
+ for (const [name, prov] of Object.entries(config.providers)) {
783
+ const def = OAUTH_PROVIDERS[name];
784
+ if (name === "command-code" && isLegacyCommandCodeStaticCatalog(prov)) {
785
+ // The former experimental preset was the exact three-model seed above. It was not a user
786
+ // choice to disable discovery, so promote only that shape to the account live catalog.
787
+ prov.liveModels = true;
788
+ changed = true;
789
+ }
790
+ if (!def || prov.authMode !== "oauth") continue;
791
+ const preset = def.providerConfig;
792
+ for (const field of OAUTH_RECONCILE_FIELDS) {
793
+ if (JSON.stringify(prov[field]) === JSON.stringify(preset[field])) continue;
794
+ if (preset[field] !== undefined) {
795
+ prov[field] = cloneProviderField(preset[field]) as never;
796
+ } else {
797
+ delete prov[field];
798
+ }
799
+ changed = true;
800
+ }
801
+ if (prov.liveModels === undefined && preset.liveModels !== undefined) {
802
+ prov.liveModels = preset.liveModels;
803
+ changed = true;
804
+ }
805
+ // Heal a defaultModel that no longer exists in the refreshed list (e.g. a deprecated snapshot).
806
+ // Skip providers without a static preset `models` list: for live-discovery providers
807
+ // (e.g. command-code OAuth) the account-scoped catalog is not enumerable here, so any
808
+ // persisted defaultModel is a user selection and must not be overwritten by the seed.
809
+ if (prov.defaultModel && preset.defaultModel && preset.models && preset.models.length > 0 && !(prov.models ?? []).includes(prov.defaultModel)) {
810
+ prov.defaultModel = preset.defaultModel;
811
+ changed = true;
812
+ }
813
+ }
814
+ if (changed) saveConfig(config);
815
+ return changed;
816
+ }
817
+
818
+ /** Runtime guards: provider config is intentionally passthrough, so persisted fields may be malformed. */
819
+ function preservableApiKeyPool(value: unknown): NonNullable<OcxProviderConfig["apiKeyPool"]> | undefined {
820
+ if (!Array.isArray(value)) return undefined;
821
+ const pool: NonNullable<OcxProviderConfig["apiKeyPool"]> = [];
822
+ const ids = new Set<string>();
823
+ const keys = new Set<string>();
824
+ for (const entry of value as unknown[]) {
825
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
826
+ const candidate = entry as Record<string, unknown>;
827
+ const id = typeof candidate.id === "string" ? candidate.id.trim() : "";
828
+ const key = sanitizeApiKeyValue(candidate.key);
829
+ if (!id || !key || ids.has(id) || keys.has(key)) continue;
830
+ const label = typeof candidate.label === "string" ? candidate.label : undefined;
831
+ const addedAt = typeof candidate.addedAt === "number" && Number.isFinite(candidate.addedAt)
832
+ ? candidate.addedAt
833
+ : undefined;
834
+ ids.add(id);
835
+ keys.add(key);
836
+ pool.push({
837
+ id,
838
+ key,
839
+ ...(label !== undefined ? { label } : {}),
840
+ ...(addedAt !== undefined ? { addedAt } : {}),
841
+ });
842
+ }
843
+ // `apiKey` remains the routing source of truth. Keep valid alternate slots even when a
844
+ // hand-edited config left the pool out of sync, rather than deleting usable credentials.
845
+ return pool.length > 0 ? pool : undefined;
846
+ }
847
+
848
+ /**
849
+ * Add/refresh an OAuth provider's config entry on a config object (does not persist).
850
+ *
851
+ * Providers whose registry entry sets `allowKeyAuthOverride` (xai, github-copilot) can be
852
+ * billed through a stored API key instead of the OAuth login (router.ts honors
853
+ * `authMode: "key"` for them). A blind preset overwrite here deletes `apiKey`/`apiKeyPool`
854
+ * on every OAuth login, silently destroying the stored key and forcing a re-paste — and it
855
+ * flips billing back to the subscription without the user asking. Carry the key fields over
856
+ * and keep key billing while usable key material remains and the user was not explicitly on
857
+ * oauth. If the final key was removed and only the old key mode remains, let the OAuth
858
+ * preset restore `authMode: "oauth"` so the newly saved OAuth credential can be used.
859
+ *
860
+ * After preservation, `apiKey` always has exactly one matching pool entry (inserting via the
861
+ * same content-derived id as the API-key manager when the active key was missing from the
862
+ * pool). Key mode reflects stored user intent (explicit `"key"` or omitted mode with safe
863
+ * key material) — never whether the login CLI process can resolve an env reference. Env-backed
864
+ * availability is decided at proxy routing time in `router.ts`.
865
+ */
866
+ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
867
+ if (provider === "chatgpt") return;
868
+ const def = OAUTH_PROVIDERS[provider];
869
+ if (!def) return;
870
+ if (provider === GOOGLE_ANTIGRAVITY_PROVIDER) migrateLegacyAntigravityStaticCatalog(config);
871
+ const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
872
+ if (namespaceCollision) throw new Error(namespaceCollision);
873
+ const existing = config.providers[provider];
874
+ const next: OcxProviderConfig = { ...def.providerConfig };
875
+ // `liveModels` is a user-facing provider toggle. Preserve either explicit setting across login;
876
+ // Antigravity's CCA discovery now uses its real RPC, so legacy `true` remains a valid choice.
877
+ if (typeof existing?.liveModels === "boolean" && !isLegacyCommandCodeStaticCatalog(existing)) {
878
+ next.liveModels = existing.liveModels;
879
+ }
880
+ // The Command Code protocol-version pin is an operator compatibility control. A re-login,
881
+ // add-account, or reauth rebuilds the row from the preset, which has no version; carry the
882
+ // existing pin so authentication changes do not silently revert the documented control.
883
+ if (existing?.commandCodeVersion !== undefined) {
884
+ next.commandCodeVersion = existing.commandCodeVersion;
885
+ }
886
+ if (existing && getProviderRegistryEntry(provider)?.allowKeyAuthOverride === true) {
887
+ // Shared sanitizeApiKeyValue trim / no-CRLF checks from api-key pool writes.
888
+ let storedApiKey = sanitizeApiKeyValue(existing.apiKey);
889
+ const storedApiKeyPool = preservableApiKeyPool(existing.apiKeyPool);
890
+ // Unsafe/blank active key with a usable pool: promote the first safe pool entry so
891
+ // key billing keeps working instead of falling back to oauth while pool keys remain.
892
+ if (storedApiKey === undefined && storedApiKeyPool && storedApiKeyPool.length > 0) {
893
+ storedApiKey = storedApiKeyPool[0]!.key;
894
+ }
895
+ if (storedApiKey !== undefined) {
896
+ const pool = storedApiKeyPool ? [...storedApiKeyPool] : [];
897
+ // Keep routing and listProviderApiKeys in sync: never leave a hidden active key that
898
+ // is absent from the pool (listing would fall back to pool[0] as "active").
899
+ if (!pool.some(entry => entry.key === storedApiKey)) {
900
+ pool.push({ id: apiKeyPoolEntryId(storedApiKey), key: storedApiKey });
901
+ }
902
+ next.apiKey = storedApiKey;
903
+ next.apiKeyPool = pool;
904
+ const previousModeAllowsKey = existing.authMode === "key" || existing.authMode === undefined;
905
+ if (previousModeAllowsKey) next.authMode = "key";
906
+ }
907
+ }
908
+ config.providers[provider] = next;
909
+ }
910
+
911
+ interface RunLoginDeps {
912
+ saveCredential?: typeof saveCredential;
913
+ saveAccountCredential?: typeof saveAccountCredential;
914
+ loadConfig?: typeof loadConfig;
915
+ saveConfig?: typeof saveConfig;
916
+ settleKiroLoginTransaction?: typeof settleKiroLoginTransaction;
917
+ removeAccount?: typeof removeAccount;
918
+ setActiveAccount?: typeof setActiveAccount;
919
+ }
920
+
921
+ /** Roll back only accounts created by this forced login, preserving concurrent refreshes of others. */
922
+ async function rollbackForcedKiroAccountWrite(
923
+ provider: string,
924
+ previousActiveId: string | undefined,
925
+ previousAccountIds: ReadonlySet<string>,
926
+ deps: Pick<RunLoginDeps, "removeAccount" | "setActiveAccount">,
927
+ ): Promise<void> {
928
+ const set = getAccountSet(provider);
929
+ if (!set) return;
930
+ for (const account of [...set.accounts]) {
931
+ if (previousAccountIds.has(account.id)) continue;
932
+ await (deps.removeAccount ?? removeAccount)(provider, account.id);
933
+ }
934
+ if (previousActiveId && getAccountCredential(provider, previousActiveId)) {
935
+ await (deps.setActiveAccount ?? setActiveAccount)(provider, previousActiveId);
936
+ }
937
+ }
938
+
939
+ /** Run the login flow, persist the credential + upsert the provider entry to disk, return cred. */
940
+ export async function runLogin(
941
+ provider: string,
942
+ ctrl: OAuthController,
943
+ opts?: LoginOpts,
944
+ deps: RunLoginDeps = {},
945
+ ): Promise<OAuthCredentials> {
946
+ const def = OAUTH_PROVIDERS[provider];
947
+ if (!def) throw new UnsupportedOAuthProviderError(provider);
948
+ const loadLatestConfig = deps.loadConfig ?? loadConfig;
949
+ const saveLatestConfig = deps.saveConfig ?? saveConfig;
950
+ if (provider !== "chatgpt") {
951
+ const preflightConfig = loadLatestConfig();
952
+ const namespaceCollision = codexAccountNamespaceProviderCollisionError(
953
+ preflightConfig.codexAccountNamespaces,
954
+ provider,
955
+ );
956
+ if (namespaceCollision) throw new Error(namespaceCollision);
957
+ }
958
+ // loginKiro keys its pending CLI-session transaction by object identity. Keep this exact object
959
+ // for settlement even when source normalization below creates a derived credential object.
960
+ const shouldRollbackKiroAccounts = provider === "kiro" && opts?.forceLogin === true;
961
+ const previousKiroAccounts = shouldRollbackKiroAccounts ? getAccountSet(provider) : undefined;
962
+ const previousKiroActiveId = previousKiroAccounts?.activeAccountId;
963
+ const previousKiroAccountIds = new Set(previousKiroAccounts?.accounts.map(account => account.id) ?? []);
964
+ const rawCred = await def.login(ctrl, opts);
965
+ const cred: OAuthCredentials = rawCred.source ? rawCred : { ...rawCred, source: "oauth" };
966
+ const settleKiroTransaction = deps.settleKiroLoginTransaction ?? settleKiroLoginTransaction;
967
+ try {
968
+ // Validate the provider row before credential persistence. A namespace claimed during the
969
+ // credential write is handled again below before the latest row is re-upserted.
970
+ if (provider !== "chatgpt") {
971
+ const preCommitConfig = loadLatestConfig();
972
+ upsertOAuthProvider(preCommitConfig, provider);
973
+ }
974
+ if (opts?.reauthAccountId) {
975
+ const existing = getAccountCredential(provider, opts.reauthAccountId);
976
+ if (!existing) throw new Error(`Unknown account for reauth: ${opts.reauthAccountId}`);
977
+ if (!existing.accountId && !existing.email) {
978
+ throw new Error("Could not verify signed-in account identity for reauth.");
979
+ }
980
+ const identityMatches = existing.accountId && cred.accountId
981
+ ? existing.accountId === cred.accountId
982
+ : existing.email && cred.email
983
+ ? existing.email.toLowerCase() === cred.email.toLowerCase()
984
+ : false;
985
+ if (!identityMatches) {
986
+ throw new Error("Signed-in account does not match the selected account. Sign in with the same account.");
987
+ }
988
+ await (deps.saveAccountCredential ?? saveAccountCredential)(provider, opts.reauthAccountId, cred);
989
+ } else {
990
+ await (deps.saveCredential ?? saveCredential)(provider, cred, {
991
+ preserveIdentityless: provider === "kiro" && opts?.forceLogin === true,
992
+ });
993
+ }
994
+ if (provider !== "chatgpt") {
995
+ // Re-run against post-credential state so same-provider API-key additions, removals,
996
+ // and active-key switches survive. A late namespace claim wins over provider creation.
997
+ const latestConfig = loadLatestConfig();
998
+ const lateCollision = codexAccountNamespaceProviderCollisionError(
999
+ latestConfig.codexAccountNamespaces,
1000
+ provider,
1001
+ );
1002
+ if (lateCollision) {
1003
+ throw new Error(
1004
+ `${lateCollision}. The credential for "${provider}" was saved, but the provider entry was not written. `
1005
+ + "Rename the account selector, then re-run the login.",
1006
+ );
1007
+ }
1008
+ upsertOAuthProvider(latestConfig, provider);
1009
+ saveLatestConfig(latestConfig);
1010
+ }
1011
+ } catch (error) {
1012
+ const errors: unknown[] = [error];
1013
+ if (shouldRollbackKiroAccounts) {
1014
+ try {
1015
+ await rollbackForcedKiroAccountWrite(provider, previousKiroActiveId, previousKiroAccountIds, deps);
1016
+ } catch (rollbackError) {
1017
+ errors.push(rollbackError);
1018
+ }
1019
+ }
1020
+ try {
1021
+ settleKiroTransaction(rawCred, false);
1022
+ } catch (restoreError) {
1023
+ errors.push(restoreError);
1024
+ }
1025
+ if (errors.length > 1) {
1026
+ throw new AggregateError(
1027
+ errors,
1028
+ "Kiro login persistence failed and the previous Kiro CLI session could not be restored.",
1029
+ );
1030
+ }
1031
+ throw error;
1032
+ }
1033
+ settleKiroTransaction(rawCred, true);
1034
+ if (provider !== "chatgpt") {
1035
+ try {
1036
+ const { clearModelCache } = await import("../codex/model-cache");
1037
+ const { clearGatherRoutedModelsInflight } = await import("../codex/catalog");
1038
+ clearModelCache(provider);
1039
+ clearGatherRoutedModelsInflight();
1040
+ const { clearAccountQuotaCache, clearProviderQuotaCache } = await import("../providers/quota");
1041
+ clearProviderQuotaCache();
1042
+ clearAccountQuotaCache(provider);
1043
+ } catch {
1044
+ // Optional state modules may be unavailable in tightly scoped unit tests.
1045
+ }
1046
+ }
1047
+ return cred;
1048
+ }
1049
+
1050
+ /**
1051
+ * GUI async login: start the flow, return the auth URL EARLY (the flow keeps running in the
1052
+ * background until the callback server captures the redirect), with a concurrency guard and an
1053
+ * error surfaced via getLoginStatus().
1054
+ *
1055
+ * Manual fallback: when the browser cannot reach the loopback callback (remote GUI, SSH, blocked
1056
+ * localhost), the GUI can POST the final redirect URL or authorization code via
1057
+ * submitManualLoginCode(), which feeds OAuthController.onManualCodeInput.
1058
+ */
1059
+ const loginState = new Map<string, { error?: string; done: boolean }>();
1060
+ const loginAbort = new Map<string, AbortController>();
1061
+
1062
+ /** Pending paste for a login in progress: either a waiter or a stashed early submission. */
1063
+ interface ManualCodeSlot {
1064
+ pendingInput?: string;
1065
+ resolve?: (value: string) => void;
1066
+ /** Registered by the callback flow so submits can validate state synchronously. */
1067
+ expectedState?: string;
1068
+ }
1069
+ const loginManual = new Map<string, ManualCodeSlot>();
1070
+ const OAUTH_PENDING_CODE_MAX_BYTES = 4 * 1024;
1071
+ let lastOAuthFlowReconciledGeneration = 0;
1072
+
1073
+ export function reconcileOAuthFlowState(context: GenerationContext): number {
1074
+ if (context.generation <= lastOAuthFlowReconciledGeneration) return 0;
1075
+ let removed = 0;
1076
+ for (const [provider, state] of loginState) {
1077
+ if (context.providerNames.has(provider) || !state.done || loginAbort.has(provider)) continue;
1078
+ if (loginState.delete(provider)) removed += 1;
1079
+ if (loginManual.delete(provider)) removed += 1;
1080
+ if (loginAbort.delete(provider)) removed += 1;
1081
+ }
1082
+ lastOAuthFlowReconciledGeneration = context.generation;
1083
+ return removed;
1084
+ }
1085
+
1086
+ function clearManualCodeSlot(provider: string): void {
1087
+ loginManual.delete(provider);
1088
+ }
1089
+
1090
+ function ensureManualCodeSlot(provider: string): ManualCodeSlot {
1091
+ let slot = loginManual.get(provider);
1092
+ if (!slot) {
1093
+ slot = {};
1094
+ loginManual.set(provider, slot);
1095
+ }
1096
+ return slot;
1097
+ }
1098
+
1099
+ /** Wait for a GUI/CLI paste of the OAuth redirect URL or code (or return a stashed early submit). */
1100
+ function waitForManualLoginCode(provider: string, signal: AbortSignal, expectedState?: string): Promise<string> {
1101
+ if (signal.aborted) {
1102
+ return Promise.reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
1103
+ }
1104
+ const slot = ensureManualCodeSlot(provider);
1105
+ if (expectedState !== undefined) slot.expectedState = expectedState;
1106
+ if (slot.pendingInput !== undefined) {
1107
+ const value = slot.pendingInput;
1108
+ slot.pendingInput = undefined;
1109
+ return Promise.resolve(value);
1110
+ }
1111
+ return new Promise<string>((resolve, reject) => {
1112
+ const onAbort = () => {
1113
+ if (slot.resolve === resolve) slot.resolve = undefined;
1114
+ reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
1115
+ };
1116
+ signal.addEventListener("abort", onAbort, { once: true });
1117
+ slot.resolve = (value: string) => {
1118
+ signal.removeEventListener("abort", onAbort);
1119
+ if (slot.resolve === resolve) slot.resolve = undefined;
1120
+ resolve(value);
1121
+ };
1122
+ });
1123
+ }
1124
+
1125
+ /**
1126
+ * Feed a pasted redirect URL or authorization code into an in-progress GUI login.
1127
+ * Returns ok:false when no login is waiting (or input is empty). Invalid pastes are accepted
1128
+ * here and re-prompted by the OAuth callback loop if they cannot be parsed / fail state checks.
1129
+ */
1130
+ export function submitManualLoginCode(provider: string, input: string): { ok: true } | { ok: false; error: string } {
1131
+ const trimmed = input.trim();
1132
+ if (!trimmed) return { ok: false, error: "empty code" };
1133
+ if (retainedUtf8Bytes(trimmed) > OAUTH_PENDING_CODE_MAX_BYTES) return { ok: false, error: "code too large" };
1134
+ const st = loginState.get(provider);
1135
+ if (!st || st.done) return { ok: false, error: "no login in progress" };
1136
+ const slot = ensureManualCodeSlot(provider);
1137
+ // Synchronous validation (validated request/ack): reject un-parseable input and
1138
+ // authorization responses (url/query kind) whose state is missing or mismatched
1139
+ // once the flow has registered its expected state. Raw codes stay in-session-PKCE
1140
+ // protected. Early posts (flow not yet waiting, no expectedState) are stashed and
1141
+ // re-validated by the callback loop.
1142
+ const parsed = parseCallbackInput(trimmed);
1143
+ // Command Code's manual fallback accepts a pasted JSON callback payload
1144
+ // (`{ apiKey, state, ... }`) which has no `code` param. Let it through the
1145
+ // shared gate so the provider-specific parser can validate it.
1146
+ const isCommandCodeJson = provider === "command-code" && trimmed.startsWith("{") && !parsed.code;
1147
+ if (!parsed.code && !isCommandCodeJson) return { ok: false, error: "no authorization code found in input" };
1148
+ if (parsed.kind !== "raw" && slot.expectedState !== undefined) {
1149
+ if (parsed.state === undefined) return { ok: false, error: "redirect URL is missing the state parameter" };
1150
+ if (parsed.state !== slot.expectedState) return { ok: false, error: "state mismatch — paste the redirect URL from THIS login attempt" };
1151
+ }
1152
+ if (slot.resolve) {
1153
+ const resolve = slot.resolve;
1154
+ slot.resolve = undefined;
1155
+ resolve(trimmed);
1156
+ } else {
1157
+ // Race: GUI may POST before the flow reaches onManualCodeInput — stash for the waiter.
1158
+ slot.pendingInput = trimmed;
1159
+ }
1160
+ return { ok: true };
1161
+ }
1162
+
1163
+ export interface OAuthAccountSummary { id: string; alias?: string; email?: string; active: boolean; needsReauth?: boolean; expiresAt?: number }
1164
+
1165
+ export function getLoginStatus(provider: string): { loggedIn: boolean; email?: string; source?: OAuthCredentials["source"]; error?: string; done: boolean; activeAccountId?: string; accounts?: OAuthAccountSummary[] } {
1166
+ const cred = getCredential(provider);
1167
+ const st = loginState.get(provider);
1168
+ const set = getAccountSet(provider);
1169
+ const accounts: OAuthAccountSummary[] | undefined = set?.accounts.map(a => ({
1170
+ id: a.id,
1171
+ ...(a.alias ? { alias: a.alias } : {}),
1172
+ email: maskEmail(a.credential.email) ?? undefined,
1173
+ active: a.id === set.activeAccountId,
1174
+ ...(a.needsReauth ? { needsReauth: true } : {}),
1175
+ expiresAt: a.credential.expires,
1176
+ }));
1177
+ return {
1178
+ loggedIn: !!cred,
1179
+ email: maskEmail(cred?.email) ?? undefined,
1180
+ source: cred?.source,
1181
+ error: st?.error,
1182
+ done: st?.done ?? false,
1183
+ ...(set ? { activeAccountId: set.activeAccountId, accounts } : {}),
1184
+ };
1185
+ }
1186
+
1187
+ /** Token-safe per-provider login state for the CLI `rmx status` logins section (no tokens, masked email). */
1188
+ export function oauthLoginSummary(): Array<{ provider: string; loggedIn: boolean; email?: string }> {
1189
+ return listOAuthProviders().map(provider => {
1190
+ const status = getLoginStatus(provider);
1191
+ return { provider, loggedIn: status.loggedIn, ...(status.email ? { email: status.email } : {}) };
1192
+ });
1193
+ }
1194
+
1195
+ export function clearLoginState(provider: string): void {
1196
+ loginAbort.get(provider)?.abort("cleared");
1197
+ loginAbort.delete(provider);
1198
+ clearManualCodeSlot(provider);
1199
+ loginState.delete(provider);
1200
+ }
1201
+
1202
+ export function cancelLoginFlow(provider: string): boolean {
1203
+ const ctrl = loginAbort.get(provider);
1204
+ const existing = loginState.get(provider);
1205
+ if (!ctrl && (!existing || existing.done)) return false;
1206
+ ctrl?.abort("cancelled");
1207
+ loginAbort.delete(provider);
1208
+ clearManualCodeSlot(provider);
1209
+ loginState.set(provider, { done: true, error: "Login cancelled" });
1210
+ return true;
1211
+ }
1212
+
1213
+ export async function startLoginFlow(
1214
+ provider: string,
1215
+ opts?: LoginOpts,
1216
+ lifecycle?: LoginFlowLifecycle,
1217
+ ): Promise<{ url: string; instructions?: string; deviceCode?: string }> {
1218
+ const def = OAUTH_PROVIDERS[provider];
1219
+ if (!def) throw new UnsupportedOAuthProviderError(provider);
1220
+ const existing = loginState.get(provider);
1221
+ if (existing && !existing.done) {
1222
+ throw new Error(`A login for ${provider} is already in progress`);
1223
+ }
1224
+ clearManualCodeSlot(provider);
1225
+ loginState.set(provider, { done: false });
1226
+ const abort = new AbortController();
1227
+ loginAbort.set(provider, abort);
1228
+ return new Promise((resolve, reject) => {
1229
+ let urlResolved = false;
1230
+ const ctrl: OAuthController = {
1231
+ onAuth: ({ url, instructions, deviceCode }) => {
1232
+ urlResolved = true;
1233
+ resolve({ url, instructions, deviceCode });
1234
+ },
1235
+ onProgress: () => {},
1236
+ // GUI fallback when the browser cannot hit the loopback callback server.
1237
+ onManualCodeInput: (expectedState?: string) => waitForManualLoginCode(provider, abort.signal, expectedState),
1238
+ signal: abort.signal,
1239
+ };
1240
+ const settle = async (error?: unknown): Promise<void> => {
1241
+ let finalError = error;
1242
+ try {
1243
+ await lifecycle?.onSettled?.();
1244
+ } catch (settleError) {
1245
+ // A successful credential/config commit is not fully live until its owner reconciles the
1246
+ // runtime config. For an already-failed login, keep the original recovery error.
1247
+ if (finalError === undefined) finalError = settleError;
1248
+ }
1249
+ if (finalError === undefined) {
1250
+ loginAbort.delete(provider);
1251
+ clearManualCodeSlot(provider);
1252
+ loginState.set(provider, { done: true });
1253
+ // Local-token import (grok-cli / Claude Code keychain) completes WITHOUT firing onAuth —
1254
+ // resolve so the GUI call returns instead of hanging.
1255
+ if (!urlResolved) resolve({ url: "", instructions: "Logged in via an existing local CLI/keychain token — no browser needed." });
1256
+ return;
1257
+ }
1258
+
1259
+ const e = finalError;
1260
+ loginAbort.delete(provider);
1261
+ clearManualCodeSlot(provider);
1262
+ const msg = e instanceof Error ? e.message : String(e);
1263
+ loginState.set(provider, { done: true, error: msg });
1264
+ if (!urlResolved) reject(e);
1265
+ };
1266
+ // Background: runLogin persists the credential + provider entry to disk. The lifecycle hook
1267
+ // lets a long-lived server config adopt that settled state before clients observe done=true.
1268
+ void runLogin(provider, ctrl, opts).then(
1269
+ () => settle(),
1270
+ (e: unknown) => settle(e),
1271
+ ).catch((e: unknown) => {
1272
+ // settle catches lifecycle failures, so this is only a defensive promise-boundary guard.
1273
+ loginAbort.delete(provider);
1274
+ clearManualCodeSlot(provider);
1275
+ const msg = e instanceof Error ? e.message : String(e);
1276
+ loginState.set(provider, { done: true, error: msg });
1277
+ if (!urlResolved) reject(e);
1278
+ });
1279
+ });
1280
+ }