@bitkyc08/opencodex 2.86.0 → 2.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
  4. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic/beta-allowlist.ts +9 -8
  10. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -23
  12. package/src/adapters/anthropic-progress.ts +84 -0
  13. package/src/adapters/anthropic.ts +108 -21
  14. package/src/adapters/base.ts +2 -0
  15. package/src/adapters/claude-cli/adapter.ts +111 -23
  16. package/src/adapters/claude-cli/profiles.ts +2 -1
  17. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  18. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  19. package/src/adapters/coding-agent/protocol.ts +1 -1
  20. package/src/adapters/coding-agent/turn.ts +18 -2
  21. package/src/adapters/dsh-account.ts +97 -0
  22. package/src/adapters/google.ts +35 -8
  23. package/src/adapters/openai-chat/messages.ts +4 -3
  24. package/src/adapters/openai-chat/passthrough.ts +8 -2
  25. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  26. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  27. package/src/adapters/openai-responses/passthrough.ts +57 -125
  28. package/src/adapters/openai-responses/request-strips.ts +25 -3
  29. package/src/adapters/openai-responses/transport.ts +81 -0
  30. package/src/adapters/qoder/adapter.ts +4 -2
  31. package/src/adapters/registry.ts +5 -0
  32. package/src/adapters/zed.ts +26 -1
  33. package/src/advisor/activity.ts +55 -0
  34. package/src/advisor/budget.ts +62 -0
  35. package/src/advisor/consult.ts +4 -1
  36. package/src/advisor/context.ts +16 -4
  37. package/src/advisor/evidence-tool.ts +21 -0
  38. package/src/advisor/evidence.ts +117 -0
  39. package/src/advisor/runtime.ts +79 -31
  40. package/src/advisor/settings.ts +11 -2
  41. package/src/bridge/errors.ts +19 -1
  42. package/src/bridge/internal.ts +3 -1
  43. package/src/bridge/response-json.ts +18 -3
  44. package/src/bridge/sse.ts +58 -8
  45. package/src/bridge/terminal-refusal.ts +59 -0
  46. package/src/claude/desktop-3p-library.ts +32 -7
  47. package/src/claude/desktop-3p.ts +55 -13
  48. package/src/claude/desktop-first-party.ts +1 -1
  49. package/src/claude/desktop-picker-platform.ts +4 -0
  50. package/src/claude/desktop-picker.ts +15 -8
  51. package/src/claude/desktop-system-proxy.ts +50 -4
  52. package/src/claude/inbound-content-options.ts +11 -0
  53. package/src/claude/inbound.ts +33 -0
  54. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  55. package/src/claude/intercept/picker-runtime.ts +6 -5
  56. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  57. package/src/claude/intercept/picker-trust.ts +19 -0
  58. package/src/claude/intercept/port-policy.ts +30 -0
  59. package/src/claude/intercept/runtime.ts +97 -94
  60. package/src/claude/outbound.ts +38 -13
  61. package/src/cli/account-api.ts +1 -0
  62. package/src/cli/account-policy-dto.ts +6 -0
  63. package/src/cli/account-policy.ts +34 -5
  64. package/src/cli/account-preservation.ts +44 -0
  65. package/src/cli/account.ts +8 -2
  66. package/src/cli/advisor.ts +17 -3
  67. package/src/cli/capabilities-accounts.ts +20 -1
  68. package/src/cli/capabilities-agents-routing.ts +5 -2
  69. package/src/cli/capabilities-base.ts +38 -10
  70. package/src/cli/capabilities-observe-system.ts +63 -3
  71. package/src/cli/capabilities-provider-models.ts +11 -0
  72. package/src/cli/codex-cli-update.ts +151 -41
  73. package/src/cli/codex-provider.ts +44 -0
  74. package/src/cli/combo-input.ts +10 -1
  75. package/src/cli/combo-result.ts +1 -1
  76. package/src/cli/combo.ts +2 -1
  77. package/src/cli/cross-home-owner.ts +17 -14
  78. package/src/cli/dispatch.ts +25 -4
  79. package/src/cli/doctor.ts +6 -1
  80. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  81. package/src/cli/help.ts +3 -2
  82. package/src/cli/index.ts +1 -1
  83. package/src/cli/integration-plan-dto.ts +1 -0
  84. package/src/cli/log-history.ts +172 -0
  85. package/src/cli/models-runtime-subcommands.ts +1 -0
  86. package/src/cli/models-runtime.ts +72 -8
  87. package/src/cli/observe-stream.ts +2 -0
  88. package/src/cli/observe.ts +16 -4
  89. package/src/cli/provider.ts +27 -5
  90. package/src/cli/registry.ts +28 -8
  91. package/src/cli/runtime-api.ts +5 -3
  92. package/src/cli/system-command.ts +40 -8
  93. package/src/cli/uninstall-client-state.ts +32 -0
  94. package/src/cli/usage-compare.ts +309 -0
  95. package/src/cli/usage-model-search.ts +4 -4
  96. package/src/cli/usage-report.ts +2 -2
  97. package/src/cli/v2.ts +1 -1
  98. package/src/client/machine-listener.ts +1 -3
  99. package/src/client/runtime.ts +1 -0
  100. package/src/clients/client-path.ts +51 -0
  101. package/src/clients/config-export/contracts.ts +2 -1
  102. package/src/clients/config-export.ts +18 -44
  103. package/src/clients/gentle-shell-path.ts +87 -0
  104. package/src/codex/account-auto-switch.ts +2 -0
  105. package/src/codex/account-identity.ts +29 -0
  106. package/src/codex/account-label.ts +1 -1
  107. package/src/codex/account-lifecycle.ts +21 -5
  108. package/src/codex/account-preservation.ts +28 -0
  109. package/src/codex/account-use-remaining.ts +23 -0
  110. package/src/codex/app-server-processes.ts +97 -7
  111. package/src/codex/app-server-restart-service.ts +55 -4
  112. package/src/codex/auth-api/account-list.ts +4 -0
  113. package/src/codex/auth-api/account-pause-group.ts +6 -50
  114. package/src/codex/auth-api/account-preservation.ts +57 -0
  115. package/src/codex/auth-api/routes.ts +8 -0
  116. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  117. package/src/codex/auth-api.ts +0 -1
  118. package/src/codex/auth-context.ts +2 -2
  119. package/src/codex/catalog/auto-review.ts +20 -3
  120. package/src/codex/catalog/build-entries.ts +9 -1
  121. package/src/codex/catalog/combo-member.ts +14 -5
  122. package/src/codex/catalog/configured-tiers.ts +1 -1
  123. package/src/codex/catalog/gather-capture.ts +2 -0
  124. package/src/codex/catalog/metadata.ts +11 -9
  125. package/src/codex/catalog/model-hints.ts +21 -16
  126. package/src/codex/catalog/model-visibility.ts +3 -1
  127. package/src/codex/catalog/parsing.ts +30 -86
  128. package/src/codex/catalog/provider-models.ts +36 -34
  129. package/src/codex/catalog/routed-gather.ts +12 -13
  130. package/src/codex/cli-install-provenance.ts +105 -17
  131. package/src/codex/cli-update-plan.ts +545 -0
  132. package/src/codex/config-write-lock.ts +23 -2
  133. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  134. package/src/codex/desktop-app-restart.ts +21 -0
  135. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  136. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  137. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  138. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  139. package/src/codex/features.ts +85 -45
  140. package/src/codex/inject/history-failure.ts +27 -0
  141. package/src/codex/inject/restore.ts +24 -2
  142. package/src/codex/inject/routing-classify.ts +4 -3
  143. package/src/codex/inject.ts +18 -27
  144. package/src/codex/main-account-hard-lock.ts +30 -1
  145. package/src/codex/native-profile-startup.ts +1 -1
  146. package/src/codex/ocx-compaction-history.ts +12 -10
  147. package/src/codex/paths.ts +86 -1
  148. package/src/codex/provider-routing.ts +174 -0
  149. package/src/codex/refresh.ts +2 -2
  150. package/src/codex/routing/health-store.ts +1 -1
  151. package/src/codex/routing/selection.ts +4 -3
  152. package/src/codex/routing/thread-affinity.ts +1 -1
  153. package/src/codex/signed-in-identity.ts +43 -0
  154. package/src/codex/subagent-model-fallback.ts +1 -1
  155. package/src/codex/sync.ts +6 -1
  156. package/src/codex/user-identity.ts +16 -5
  157. package/src/codex/windows-local-appdata.ts +84 -0
  158. package/src/combos/failover.ts +6 -1
  159. package/src/combos/index.ts +1 -0
  160. package/src/combos/resolve.ts +46 -10
  161. package/src/combos/types.ts +15 -0
  162. package/src/config/diagnostics.ts +33 -6
  163. package/src/config/live-reconcile.ts +38 -2
  164. package/src/config/load-degrade.ts +17 -0
  165. package/src/config/opencode-wire-upgrade.ts +54 -0
  166. package/src/config/opencode-zen-upgrade.ts +52 -0
  167. package/src/config/owner-registry.ts +22 -17
  168. package/src/config/provider-validation.ts +45 -2
  169. package/src/config/schema/config-schema.ts +22 -0
  170. package/src/config/schema/leaf-validators.ts +38 -9
  171. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  172. package/src/config/schema/native-reasoning-retention.ts +26 -0
  173. package/src/config.ts +2 -0
  174. package/src/generated/compatibility-version.json +608 -284
  175. package/src/grok/sync.ts +1 -1
  176. package/src/images/loop.ts +46 -8
  177. package/src/integrations/catalog-refresh.ts +1 -1
  178. package/src/integrations/mutation-plan.ts +1 -0
  179. package/src/integrations/registry.ts +8 -0
  180. package/src/lib/codex-restart-contract.ts +15 -0
  181. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  182. package/src/lib/empty-tool-catalog.ts +62 -0
  183. package/src/lib/errors.ts +19 -3
  184. package/src/lib/gcp-adc.ts +96 -9
  185. package/src/lib/gcp-credential-input.ts +12 -0
  186. package/src/lib/lab-live-route-production.ts +5 -0
  187. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  188. package/src/lib/redact.ts +2 -2
  189. package/src/oauth/account-quota-rank.ts +69 -2
  190. package/src/oauth/anthropic-account-refusal.ts +20 -2
  191. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  192. package/src/oauth/anthropic-routing.ts +1 -21
  193. package/src/oauth/anthropic.ts +38 -7
  194. package/src/oauth/callback-server.ts +27 -1
  195. package/src/oauth/chatgpt-device.ts +1 -1
  196. package/src/oauth/chatgpt-shared.ts +83 -0
  197. package/src/oauth/chatgpt.ts +8 -72
  198. package/src/oauth/dsh.ts +203 -0
  199. package/src/oauth/generic-account-failover.ts +33 -7
  200. package/src/oauth/generic-session-affinity.ts +125 -0
  201. package/src/oauth/index.ts +184 -109
  202. package/src/oauth/local-token-detect.ts +21 -1
  203. package/src/oauth/login-cli-errors.ts +44 -0
  204. package/src/oauth/login-cli.ts +69 -7
  205. package/src/oauth/login-flow-state.ts +371 -23
  206. package/src/oauth/meta-muse-errors.ts +41 -0
  207. package/src/oauth/meta-muse-keychain.ts +52 -0
  208. package/src/oauth/meta-muse.ts +51 -84
  209. package/src/oauth/model-discovery-auth.ts +3 -0
  210. package/src/oauth/pool-settings-capability.ts +4 -2
  211. package/src/oauth/refresh-flight-retirement.ts +48 -0
  212. package/src/oauth/session-affinity-key.ts +23 -0
  213. package/src/oauth/store-anthropic-instance.ts +9 -2
  214. package/src/oauth/store.ts +12 -5
  215. package/src/protocols/encoders/adapter-events.ts +5 -4
  216. package/src/protocols/encoders/messages.ts +9 -6
  217. package/src/protocols/provider-summary.ts +2 -1
  218. package/src/providers/api-key-cooldowns.ts +49 -0
  219. package/src/providers/api-key-resolve.ts +11 -6
  220. package/src/providers/api-key-response-ownership.ts +57 -0
  221. package/src/providers/api-key-selection.ts +31 -6
  222. package/src/providers/api-keys.ts +214 -3
  223. package/src/providers/claude-cli-usage.ts +96 -0
  224. package/src/providers/dsh-account-destination.ts +31 -0
  225. package/src/providers/dsh-account-models.ts +41 -0
  226. package/src/providers/gcp-credential-cleanup.ts +59 -0
  227. package/src/providers/key-failover.ts +54 -50
  228. package/src/providers/key-store.ts +7 -2
  229. package/src/providers/model-rename-fields.ts +3 -0
  230. package/src/providers/model-rename-migration.ts +2 -2
  231. package/src/providers/openai-tiers-destination.ts +11 -2
  232. package/src/providers/openai-tiers.ts +5 -12
  233. package/src/providers/opencode-go-transport.ts +3 -1
  234. package/src/providers/quota/account-cache.ts +6 -5
  235. package/src/providers/quota/account-row.ts +34 -0
  236. package/src/providers/quota/antigravity.ts +28 -7
  237. package/src/providers/quota/dsh-account.ts +70 -0
  238. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  239. package/src/providers/quota-types.ts +2 -0
  240. package/src/providers/quota.ts +14 -20
  241. package/src/providers/registry/entries-core.ts +37 -8
  242. package/src/providers/registry/types.ts +1 -1
  243. package/src/providers/request-pacing.ts +77 -19
  244. package/src/providers/routed-config.ts +339 -0
  245. package/src/providers/service-tier.ts +2 -1
  246. package/src/quota/window-mapping.ts +5 -2
  247. package/src/responses/anthropic-progress.ts +34 -0
  248. package/src/responses/assistant-replay.ts +35 -0
  249. package/src/responses/compaction-history-framing.ts +65 -0
  250. package/src/responses/compaction.ts +6 -1
  251. package/src/responses/incomplete-reason.ts +31 -0
  252. package/src/responses/parser.ts +50 -5
  253. package/src/responses/reasoning-envelope.ts +4 -0
  254. package/src/responses/reasoning-replay-cache.ts +24 -2
  255. package/src/responses/reasoning-retention.ts +173 -0
  256. package/src/responses/retained-compaction.ts +46 -0
  257. package/src/responses/schema.ts +23 -0
  258. package/src/router.ts +8 -321
  259. package/src/routing/health.ts +37 -22
  260. package/src/routing/history/indexer.ts +6 -5
  261. package/src/routing/history/ingest.ts +6 -2
  262. package/src/routing/profile.ts +1 -1
  263. package/src/server/admission-model-scope.ts +17 -0
  264. package/src/server/auth-cors.ts +11 -0
  265. package/src/server/chat-completions.ts +7 -1
  266. package/src/server/chat-native-reasoning.ts +123 -0
  267. package/src/server/chat-native-sse.ts +110 -7
  268. package/src/server/chat-native.ts +76 -17
  269. package/src/server/claude-messages.ts +55 -414
  270. package/src/server/effort-policy.ts +1 -1
  271. package/src/server/images.ts +15 -2
  272. package/src/server/index/model-alias-id.ts +36 -0
  273. package/src/server/index/optional-listeners.ts +2 -0
  274. package/src/server/index/serve-options.ts +9 -1
  275. package/src/server/index/startup-warnings.ts +2 -0
  276. package/src/server/index.ts +1 -1
  277. package/src/server/inference/advisor-diagnostics.ts +31 -0
  278. package/src/server/management/advisor-routes.ts +15 -4
  279. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  280. package/src/server/management/combo-routes.ts +176 -50
  281. package/src/server/management/companion-routes.ts +1 -1
  282. package/src/server/management/config-routes.ts +75 -4
  283. package/src/server/management/cursor-integration-routes.ts +1 -1
  284. package/src/server/management/model-routes.ts +11 -6
  285. package/src/server/management/model-rows.ts +14 -0
  286. package/src/server/management/native-integration-routes.ts +2 -7
  287. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  288. package/src/server/management/oauth-account-routes.ts +87 -14
  289. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  290. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  291. package/src/server/management/provider-overwrite-carry.ts +7 -0
  292. package/src/server/management/provider-routes.ts +25 -30
  293. package/src/server/management/route-registry.ts +10 -1
  294. package/src/server/management-api.ts +3 -1
  295. package/src/server/messages-native-tool-guard.ts +185 -0
  296. package/src/server/messages-native.ts +101 -21
  297. package/src/server/messages-shared.ts +431 -0
  298. package/src/server/relay.ts +1 -1
  299. package/src/server/request-log-terminal-status.ts +1 -1
  300. package/src/server/request-log.ts +6 -1
  301. package/src/server/responses/account-change-state.ts +8 -2
  302. package/src/server/responses/adapter-continuation.ts +13 -1
  303. package/src/server/responses/adapter-delivery.ts +19 -6
  304. package/src/server/responses/adapter-dispatch.ts +19 -2
  305. package/src/server/responses/advisor-retention.ts +117 -0
  306. package/src/server/responses/advisor-slot.ts +73 -41
  307. package/src/server/responses/codex-ws-wire.ts +15 -8
  308. package/src/server/responses/compact.ts +303 -125
  309. package/src/server/responses/compaction-recovery.ts +18 -10
  310. package/src/server/responses/compaction-retention.ts +61 -0
  311. package/src/server/responses/context-overflow.ts +11 -6
  312. package/src/server/responses/core-auth.ts +6 -0
  313. package/src/server/responses/core-codex-account.ts +11 -1
  314. package/src/server/responses/core-combo-failure.ts +2 -0
  315. package/src/server/responses/core-combo.ts +120 -36
  316. package/src/server/responses/core-errors.ts +3 -3
  317. package/src/server/responses/core-lifetime.ts +5 -2
  318. package/src/server/responses/core-opaque-recovery.ts +35 -0
  319. package/src/server/responses/core-options.ts +5 -0
  320. package/src/server/responses/core-pipeline.ts +141 -0
  321. package/src/server/responses/core-replay.ts +18 -2
  322. package/src/server/responses/core.ts +2 -115
  323. package/src/server/responses/fetch-helpers.ts +73 -19
  324. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  325. package/src/server/responses/native-response-control.ts +4 -2
  326. package/src/server/responses/passthrough-delivery.ts +3 -1
  327. package/src/server/responses/passthrough-dispatch.ts +15 -13
  328. package/src/server/responses/request-prepare.ts +76 -12
  329. package/src/server/responses/request-sidecar-auth.ts +2 -2
  330. package/src/server/responses/request-transport.ts +104 -27
  331. package/src/server/responses/response-effects.ts +1 -0
  332. package/src/server/responses/run-turn-execution.ts +96 -30
  333. package/src/server/responses/sidecar-execution.ts +20 -2
  334. package/src/server/responses/terminal-guard.ts +2 -16
  335. package/src/service/auth-preflight.ts +71 -0
  336. package/src/service/cli.ts +4 -2
  337. package/src/service/definition.ts +97 -0
  338. package/src/service/guards.ts +1 -61
  339. package/src/service/health-sentinel-linux.ts +112 -0
  340. package/src/service/health-sentinel-main.ts +76 -57
  341. package/src/service/health.ts +5 -89
  342. package/src/service/launchd.ts +27 -7
  343. package/src/service/logging.ts +72 -9
  344. package/src/service/repair.ts +2 -1
  345. package/src/service/state.ts +7 -8
  346. package/src/service/systemd.ts +1 -1
  347. package/src/service/windows-guarded-manager.ts +2 -1
  348. package/src/service/windows-ops.ts +1 -1
  349. package/src/service/windows-scheduler.ts +2 -7
  350. package/src/service/windows-taskxml.ts +2 -2
  351. package/src/service/windows-wscript.ts +14 -0
  352. package/src/service.ts +4 -2
  353. package/src/sidecar/auth.ts +2 -0
  354. package/src/types/config.ts +64 -3
  355. package/src/types/opencode-go.ts +61 -0
  356. package/src/types/opencode-zen.ts +29 -0
  357. package/src/types/provider.ts +32 -6
  358. package/src/types/request.ts +23 -2
  359. package/src/types/wire.ts +29 -37
  360. package/src/types.ts +1 -0
  361. package/src/update/npm-invocation.mjs +12 -2
  362. package/src/usage/cost.ts +48 -12
  363. package/src/usage/expected-prices.ts +3 -0
  364. package/src/usage/ledger-retention.ts +2 -1
  365. package/src/usage/ledger-scanner.ts +6 -5
  366. package/src/usage/log.ts +17 -8
  367. package/src/usage/summary.ts +22 -9
  368. package/src/usage/user-cost-overlays.ts +14 -19
  369. package/src/usage/user-cost-validation.ts +38 -0
  370. package/src/vision/eligibility.ts +13 -0
  371. package/src/web-search/loop.ts +55 -5
  372. package/src/web-search/progress-stream.ts +4 -1
  373. package/src/web-search/run-turn-loop.ts +9 -1
  374. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  375. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  376. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  377. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  378. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
@@ -1,4 +1,6 @@
1
+ import { MuseLoginError, museLoginErrorMessage } from "./meta-muse-errors";
1
2
  import type { KiroOAuthMetadata, OAuthController, OAuthCredentials } from "./types";
3
+ import { mapFill } from "../providers/resolved-model-policy-merge";
2
4
  import { initializeProviderModelSelection } from "../providers/initial-model-selection";
3
5
  import type { OcxConfig, OcxProviderConfig, RefreshPolicy } from "../types";
4
6
  import { getDefaultConfig, initializePersistedConfigIfMissing, loadConfig, mutatePersistedConfig, observeInitialConfigState, saveConfig } from "../config";
@@ -48,6 +50,7 @@ import { validateDevinApiBaseUrl } from "./devin/api-base";
48
50
  import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUrl } from "./github-copilot";
49
51
  import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
50
52
  import { loginMetaMuse, refreshMetaMuseToken } from "./meta-muse";
53
+ import { DSH_ACCOUNT_AUTH } from "./dsh";
51
54
  import { loginOrcaRouter, orcaRouterInferenceBaseUrl, refreshOrcaRouterKey } from "./orcarouter";
52
55
  import { ANTIGRAVITY_DISCOVERY_UA } from "../adapters/google-antigravity-wire";
53
56
  import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
@@ -57,9 +60,12 @@ import { providerModelsUrl, resolveProviderModelDiscoveryUrl } from "../provider
57
60
  import { resolveProviderTransport } from "../providers/xai-transport";
58
61
  import { detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
59
62
  import { logOAuthEvent } from "./log";
63
+ import { MAX_OAUTH_TOKEN_REFRESH_WAITERS, retireSettledOAuthRefreshFlight, tokenRefreshWaiters, type OAuthRefreshFlightEvidence } from "./refresh-flight-retirement";
64
+ export { MAX_OAUTH_TOKEN_REFRESH_WAITERS };
60
65
  import { captureConfigGeneration, sweepExpiredOnWrite } from "../lib/state-store-sweeper";
61
- import { clearManualCodeSlot, ensureManualCodeSlot, kiroLoginSettling, loginAbort, loginState, waitForManualLoginCode, type OAuthLoginHint } from "./login-flow-state";
62
- export { reconcileOAuthFlowState, submitManualLoginCode } from "./login-flow-state";
66
+ import { admitLoginFlow, classifyLoginCodeError, clearManualCodeSlot, CodeLoginUnsupportedError, ensureManualCodeSlot, kiroLoginSettling, latestLoginFlowKey, loginAbort, loginFlowProvider, loginState, recordStartedLoginFlow, settleLoginFlow, waitForManualLoginCode, type LoginCodeError, type OAuthLoginHint } from "./login-flow-state";
67
+ export { cancelLoginFlow, clearLoginState, CodeLoginUnsupportedError, isCodeLoginFlow, loginFlowAccountId, reconcileOAuthFlowState, submitLoginCode, submitManualLoginCode, waitForLoginSettled } from "./login-flow-state";
68
+ export type { LoginCodeError, LoginFlowState } from "./login-flow-state";
63
69
  import { randomUUID } from "node:crypto";
64
70
  export {
65
71
  CODEX_HEALTH_AUTH_FAILED_NOTE,
@@ -132,8 +138,7 @@ export type OAuthActiveTokenObservation =
132
138
 
133
139
  const MAX_OAUTH_TOKEN_REFRESH_FLIGHTS = 32;
134
140
  const OAUTH_TOKEN_REFRESH_FLIGHT_STALE_MS = 120_000;
135
- interface OAuthRefreshFlightEvidence { flightId: string; dispatched: boolean }
136
- interface OAuthTokenRefreshFlight extends OAuthRefreshFlightEvidence { promise: Promise<OAuthAccessSnapshot>; startedAt: number; abort: AbortController }
141
+ interface OAuthTokenRefreshFlight extends OAuthRefreshFlightEvidence { promise: Promise<OAuthAccessSnapshot>; startedAt: number; abort: AbortController; settled?: boolean; retirementDeferredLogged?: boolean }
137
142
  const tokenRefreshes = new Map<string, OAuthTokenRefreshFlight>();
138
143
  export class OAuthTokenRefreshBusyError extends Error {
139
144
  readonly code = "OAUTH_TOKEN_REFRESH_BUSY";
@@ -157,7 +162,8 @@ export function seedOAuthTokenRefreshFlightsForTests(rows: Array<{ key: string;
157
162
  const promise = new Promise<OAuthAccessSnapshot>((_resolve, reject) => {
158
163
  abort.signal.addEventListener("abort", () => reject(abort.signal.reason), { once: true });
159
164
  });
160
- const flight = { promise, startedAt: startedAt ?? Date.now(), abort, flightId: flightId ?? randomUUID(), dispatched: dispatched ?? false };
165
+ const flight: OAuthTokenRefreshFlight = { promise, startedAt: startedAt ?? Date.now(), abort, flightId: flightId ?? randomUUID(), dispatched: dispatched ?? false };
166
+ abort.signal.addEventListener("abort", () => { flight.settled = true; }, { once: true });
161
167
  tokenRefreshes.set(key, flight);
162
168
  inserted.push(flight);
163
169
  return promise;
@@ -192,6 +198,8 @@ export interface LoginOpts {
192
198
  * listener (#3366). Ignored by every other provider.
193
199
  */
194
200
  flow?: ChatGPTLoginFlow;
201
+ /** Code-display login: no callback server; completes from a pasted code (needs `codeRedirectUri`). */
202
+ codeMode?: boolean;
195
203
  }
196
204
 
197
205
  export interface LoginFlowLifecycle {
@@ -220,6 +228,8 @@ export interface OAuthProviderDef {
220
228
  * overrides this. Default when unset here: "lazy-only".
221
229
  */
222
230
  defaultRefreshPolicy?: RefreshPolicy;
231
+ /** Provider-hosted page that DISPLAYS the code; its presence enables `codeMode`. */
232
+ codeRedirectUri?: string;
223
233
  }
224
234
 
225
235
  function oauthConfig(id: string): OcxProviderConfig {
@@ -235,6 +245,11 @@ function oauthDefaultModel(id: string): string {
235
245
  }
236
246
 
237
247
  export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
248
+ "dsh-account": {
249
+ ...DSH_ACCOUNT_AUTH,
250
+ providerConfig: oauthConfig("dsh-account"),
251
+ defaultModel: oauthDefaultModel("dsh-account"),
252
+ },
238
253
  "command-code": {
239
254
  // Add-account/reauth must not reimport the current local CLI credential.
240
255
  login: (ctrl, opts) => loginCommandCode(ctrl, { importLocal: opts?.forceLogin ? "off" : "fallback" }),
@@ -383,6 +398,11 @@ export function isPublicOAuthProvider(name: string): boolean {
383
398
  return name !== "chatgpt" && isOAuthProvider(name);
384
399
  }
385
400
 
401
+ /** Providers that can log in from a displayed code, with no inbound port and no browser here. */
402
+ export function supportsCodeLoginMode(name: string): boolean {
403
+ return OAUTH_PROVIDERS[name]?.codeRedirectUri !== undefined;
404
+ }
405
+
386
406
  function isRefreshPolicy(value: unknown): value is RefreshPolicy {
387
407
  return value === "proactive" || value === "lazy-only" || value === "disabled";
388
408
  }
@@ -452,6 +472,14 @@ export class OAuthProviderPublicationError extends Error {
452
472
  }
453
473
  }
454
474
 
475
+ /** A local configuration conflict found before any credential is written; its text is fixed. */
476
+ export class OAuthProviderNamespaceCollisionError extends Error {
477
+ constructor(message: string) {
478
+ super(message);
479
+ this.name = "OAuthProviderNamespaceCollisionError";
480
+ }
481
+ }
482
+
455
483
  export class OAuthReauthIdentityMismatchError extends Error {
456
484
  constructor() {
457
485
  super("Signed-in account does not match the selected account. Sign in with the same account.");
@@ -475,6 +503,7 @@ class OAuthLoginSupersededError extends Error {
475
503
 
476
504
  /** Project arbitrary OAuth failures onto the small, stable public error vocabulary. */
477
505
  export function publicOAuthAuthenticationErrorMessage(error: unknown): string {
506
+ if (error instanceof MuseLoginError) return museLoginErrorMessage(error.kind, error.status);
478
507
  if (error instanceof OAuthMutationBusyError) {
479
508
  return error.message === "OAuth mutation queue wait timed out"
480
509
  ? "OAuth mutation queue wait timed out"
@@ -484,6 +513,7 @@ export function publicOAuthAuthenticationErrorMessage(error: unknown): string {
484
513
  (error instanceof OAuthLoginRequiredError && isOAuthProvider(error.provider))
485
514
  || error instanceof OAuthAccountPausedError
486
515
  || error instanceof OAuthProviderPublicationError
516
+ || error instanceof OAuthProviderNamespaceCollisionError
487
517
  // Reauth identity outcomes carry fixed, account-free remediation text. Dropping them to the
488
518
  // generic message hides WHICH failure the user must fix (sign in with the selected account).
489
519
  || error instanceof OAuthReauthIdentityMismatchError
@@ -608,45 +638,71 @@ async function resolveAccessSnapshotForAccount(
608
638
  const key = `${provider}\u0000${accountId}`;
609
639
  let existing = tokenRefreshes.get(key);
610
640
  let replacedStaleFlight: OAuthRefreshFlightEvidence | undefined;
611
- if (existing && Date.now() - existing.startedAt <= OAUTH_TOKEN_REFRESH_FLIGHT_STALE_MS) {
612
- logOAuthEvent("OAuth refresh joined existing operation", { provider, accountId });
613
- return existing.promise;
614
- }
615
- if (existing) {
641
+ if (existing && (existing.abort.signal.reason instanceof OAuthTokenRefreshStaleError
642
+ || Date.now() - existing.startedAt > OAUTH_TOKEN_REFRESH_FLIGHT_STALE_MS)) {
616
643
  replacedStaleFlight = { flightId: existing.flightId, dispatched: existing.dispatched };
617
644
  existing.abort.abort(new OAuthTokenRefreshStaleError());
618
- if (tokenRefreshes.get(key) === existing) tokenRefreshes.delete(key);
619
- existing = undefined;
620
645
  }
621
- if (tokenRefreshes.size >= MAX_OAUTH_TOKEN_REFRESH_FLIGHTS) throw new OAuthTokenRefreshBusyError();
622
-
623
- const abort = new AbortController();
624
- const flight: OAuthTokenRefreshFlight = {
625
- promise: undefined as unknown as Promise<OAuthAccessSnapshot>,
626
- startedAt: Date.now(),
627
- abort,
628
- flightId: randomUUID(),
629
- dispatched: false,
630
- };
631
- const refresh = (async (): Promise<OAuthAccessSnapshot> => {
632
- const accessToken = await refreshAndPersistAccessToken(provider, accountId, def, cred, abort.signal, flight, replacedStaleFlight);
633
- const persistedRow = getAccountCredentialWithStatus(provider, accountId);
634
- if (!persistedRow) throw new OAuthLoginRequiredError(provider);
635
- if (persistedRow.paused === true) throw new OAuthAccountPausedError();
636
- const persisted = persistedRow.credential;
637
- if (persisted.access !== accessToken) {
638
- throw new Error(`OAuth refresh persisted an unexpected access token for ${provider}`);
646
+ // Count every consumer, including same-flight joins, until its promise settles. Cancelling
647
+ // a client cannot recycle this permit. Stale-flight abort above still runs under pressure;
648
+ // no replacement starts until the old consumers have actually released enough capacity.
649
+ const waiter = tokenRefreshWaiters.tryAcquire();
650
+ if (!waiter) throw new OAuthTokenRefreshBusyError();
651
+ try {
652
+ if (existing && replacedStaleFlight) {
653
+ // A replacement would wait for this owner's lock anyway. Keep its registry slot
654
+ // through definitive-result cleanup and durable retirement, even with spare capacity.
655
+ // In particular, an ignored abort must not mark a late explicit rejection as uncertain.
656
+ await existing.promise.catch(() => {});
657
+ if (tokenRefreshes.get(key) === existing && !retireSettledOAuthRefreshFlight(tokenRefreshes, key, existing)) {
658
+ throw new OAuthTokenRefreshStaleError();
659
+ }
660
+ // Other admitted replacements may have resumed first and installed the next flight.
661
+ existing = tokenRefreshes.get(key);
639
662
  }
640
- return accessSnapshot(provider, accountId, persisted, oauthProvider);
641
- })().catch(error => {
642
- if (abort.signal.reason instanceof OAuthTokenRefreshStaleError) throw abort.signal.reason;
643
- throw error;
644
- }).finally(() => {
645
- if (tokenRefreshes.get(key) === flight) tokenRefreshes.delete(key);
646
- });
647
- flight.promise = refresh;
648
- tokenRefreshes.set(key, flight);
649
- return refresh;
663
+ if (existing) {
664
+ logOAuthEvent("OAuth refresh joined existing operation", { provider, accountId });
665
+ return await existing.promise;
666
+ }
667
+ if (tokenRefreshes.size >= MAX_OAUTH_TOKEN_REFRESH_FLIGHTS) {
668
+ for (const [staleKey, stale] of tokenRefreshes) {
669
+ if (stale.abort.signal.reason instanceof OAuthTokenRefreshStaleError) retireSettledOAuthRefreshFlight(tokenRefreshes, staleKey, stale);
670
+ }
671
+ if (tokenRefreshes.size >= MAX_OAUTH_TOKEN_REFRESH_FLIGHTS) throw new OAuthTokenRefreshBusyError();
672
+ }
673
+
674
+ const abort = new AbortController();
675
+ const flight: OAuthTokenRefreshFlight = {
676
+ promise: undefined as unknown as Promise<OAuthAccessSnapshot>,
677
+ startedAt: Date.now(),
678
+ abort,
679
+ flightId: randomUUID(),
680
+ dispatched: false,
681
+ };
682
+ const refresh = (async (): Promise<OAuthAccessSnapshot> => {
683
+ const accessToken = await refreshAndPersistAccessToken(provider, accountId, def, cred, abort.signal, flight, replacedStaleFlight);
684
+ const persistedRow = getAccountCredentialWithStatus(provider, accountId);
685
+ if (!persistedRow) throw new OAuthLoginRequiredError(provider);
686
+ if (persistedRow.paused === true) throw new OAuthAccountPausedError();
687
+ const persisted = persistedRow.credential;
688
+ if (persisted.access !== accessToken) {
689
+ throw new Error(`OAuth refresh persisted an unexpected access token for ${provider}`);
690
+ }
691
+ return accessSnapshot(provider, accountId, persisted, oauthProvider);
692
+ })().catch(error => {
693
+ if (abort.signal.reason instanceof OAuthTokenRefreshStaleError) throw abort.signal.reason;
694
+ throw error;
695
+ }).finally(() => {
696
+ flight.settled = true;
697
+ if (tokenRefreshes.get(key) !== flight) return;
698
+ if (abort.signal.reason instanceof OAuthTokenRefreshStaleError) {
699
+ retireSettledOAuthRefreshFlight(tokenRefreshes, key, flight);
700
+ } else tokenRefreshes.delete(key);
701
+ });
702
+ flight.promise = refresh;
703
+ tokenRefreshes.set(key, flight);
704
+ return await refresh;
705
+ } finally { waiter.release(); }
650
706
  }
651
707
 
652
708
  export async function getValidAccessTokenSnapshot(
@@ -666,6 +722,7 @@ const FORCE_REFRESH_PROVIDERS = new Set([
666
722
  "google-antigravity",
667
723
  "orcarouter-oauth",
668
724
  "devin",
725
+ "dsh-account",
669
726
  ]);
670
727
 
671
728
  export async function forceRefreshOAuthAccessSnapshot(
@@ -1239,26 +1296,21 @@ const OAUTH_RECONCILE_FIELDS: (keyof OcxProviderConfig)[] = [
1239
1296
  // preserves explicit saved values.
1240
1297
 
1241
1298
  /**
1242
- * Output-budget fields an OAuth preset may refresh but must never erase.
1243
- *
1244
- * These stay on the reconcile list so a preset that does declare a budget still
1245
- * refreshes the saved row. What changes is the other branch: when the preset
1246
- * declares nothing, the operator's value survives instead of being deleted.
1247
- *
1248
- * Without that, the fields behaved as if they could not be configured at all.
1249
- * No OAuth preset seeds either one, so the delete branch was the only branch
1250
- * these two ever took, and a hand-edited `defaultMaxOutputTokens` was gone
1251
- * before the first turn of the next startup — leaving the adapter's own
1252
- * fallback as the only reachable output cap (#5190).
1253
- *
1254
- * Scoped to the output budget on purpose. The input side (`contextWindow`,
1255
- * `modelContextWindows`) describes what the account's models are, which the
1256
- * preset and live discovery do own; an output budget is a spend decision the
1257
- * operator makes.
1299
+ * Saved limits and per-model declarations are operator input, including Copilot's
1300
+ * long-context evidence. Presets supply missing defaults, not replacements for
1301
+ * configured entries. Catalog rosters, capability lists and seeded reasoning
1302
+ * ladders still refresh below; unseeded tiers remain operator input.
1303
+ * Without provenance, a saved seed cannot be distinguished from an operator edit.
1258
1304
  */
1305
+ const OAUTH_OPERATOR_MAP_FIELDS: ReadonlySet<keyof OcxProviderConfig> = new Set([
1306
+ "modelContextWindows", "modelMaxOutputTokens", "modelInputModalities",
1307
+ "modelReasoningEffortMap", "reasoningEffortMap",
1308
+ ]);
1309
+ const OAUTH_OPERATOR_LIMIT_FIELDS: ReadonlySet<keyof OcxProviderConfig> = new Set([
1310
+ "contextWindow", "defaultMaxOutputTokens",
1311
+ ]);
1259
1312
  const OAUTH_PRESERVE_WHEN_PRESET_UNSET: ReadonlySet<keyof OcxProviderConfig> = new Set([
1260
- "defaultMaxOutputTokens",
1261
- "modelMaxOutputTokens",
1313
+ "reasoningEfforts",
1262
1314
  ]);
1263
1315
 
1264
1316
  const GOOGLE_ANTIGRAVITY_PROVIDER = "google-antigravity";
@@ -1298,7 +1350,23 @@ function applyOAuthPresetCatalog(
1298
1350
  ): void {
1299
1351
  for (const field of OAUTH_RECONCILE_FIELDS) {
1300
1352
  if (JSON.stringify(provider[field]) === JSON.stringify(preset[field])) continue;
1301
- if (preset[field] !== undefined) {
1353
+ if (field === "modelReasoningEfforts") {
1354
+ // Known unmarked ladders are saved catalog seeds, not evidence of an
1355
+ // operator override. Preserve unseeded rows and explicit authoritative
1356
+ // overrides; refresh the preset-owned rows (e.g. xAI's effort ladder).
1357
+ const authoritative = provider.modelReasoningEffortsAuthoritative === true;
1358
+ provider.modelReasoningEfforts = mapFill(
1359
+ authoritative ? preset.modelReasoningEfforts : provider.modelReasoningEfforts,
1360
+ authoritative ? provider.modelReasoningEfforts : preset.modelReasoningEfforts,
1361
+ )[0];
1362
+ } else if (OAUTH_OPERATOR_MAP_FIELDS.has(field)) {
1363
+ provider[field] = mapFill<unknown>(
1364
+ preset[field] as Record<string, unknown> | undefined,
1365
+ provider[field] as Record<string, unknown> | undefined,
1366
+ )[0] as never;
1367
+ } else if (OAUTH_OPERATOR_LIMIT_FIELDS.has(field) && provider[field] !== undefined) {
1368
+ continue;
1369
+ } else if (preset[field] !== undefined) {
1302
1370
  provider[field] = cloneProviderField(preset[field]) as never;
1303
1371
  } else if (!OAUTH_PRESERVE_WHEN_PRESET_UNSET.has(field)) {
1304
1372
  delete provider[field];
@@ -1494,7 +1562,7 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
1494
1562
  if (!def) return;
1495
1563
  if (provider === GOOGLE_ANTIGRAVITY_PROVIDER) migrateLegacyAntigravityStaticCatalog(config);
1496
1564
  const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
1497
- if (namespaceCollision) throw new Error(namespaceCollision);
1565
+ if (namespaceCollision) throw new OAuthProviderNamespaceCollisionError(namespaceCollision);
1498
1566
  const existing = config.providers[provider];
1499
1567
  const providerConfig = def.resolveProviderConfig?.(config) ?? def.providerConfig;
1500
1568
  // Clone operator state, including xAI wire choices and their migration version.
@@ -1561,6 +1629,8 @@ interface RunLoginDeps {
1561
1629
  settleKiroLoginTransaction?: typeof settleKiroLoginTransaction;
1562
1630
  rollbackCredentialWrite?: typeof rollbackCredentialWriteIfMatch;
1563
1631
  assertCurrentOwner?: () => void;
1632
+ /** Told which account row the credential write landed in (an id, never the credential). */
1633
+ onAccountPersisted?: (accountId: string) => void;
1564
1634
  }
1565
1635
 
1566
1636
  /** Run the login flow, persist the credential + upsert the provider entry to disk, return cred. */
@@ -1591,7 +1661,7 @@ export async function runLogin(
1591
1661
  preflightConfig.codexAccountNamespaces,
1592
1662
  provider,
1593
1663
  );
1594
- if (namespaceCollision) throw new Error(namespaceCollision);
1664
+ if (namespaceCollision) throw new OAuthProviderNamespaceCollisionError(namespaceCollision);
1595
1665
  }
1596
1666
  if (provider === "anthropic2" && observeInitialConfigState() !== "exists") {
1597
1667
  throw new OAuthProviderPublicationError(provider, false);
@@ -1640,10 +1710,12 @@ export async function runLogin(
1640
1710
  // Proven same account: a reauth whose tier read failed keeps the stored label.
1641
1711
  const reauthPlan = mergeOAuthPlan(cred, existing);
1642
1712
  const reauthCred = reauthPlan === undefined ? cred : { ...cred, plan: reauthPlan };
1643
- await (deps.saveAccountCredential ?? saveAccountCredential)(provider, opts.reauthAccountId, reauthCred, {
1713
+ const written = await (deps.saveAccountCredential ?? saveAccountCredential)(provider, opts.reauthAccountId, reauthCred, {
1644
1714
  assertBeforePersist: deps.assertCurrentOwner,
1645
1715
  rotateLoginId: true,
1646
1716
  });
1717
+ // A reauth whose account was deleted meanwhile wrote nothing and owns no row to report.
1718
+ if (written) deps.onAccountPersisted?.(opts.reauthAccountId);
1647
1719
  } else {
1648
1720
  const saveOptions = {
1649
1721
  preserveIdentityless: opts?.forceLogin === true || (provider === "anthropic" && cred.source === "local-cli"),
@@ -1655,8 +1727,11 @@ export async function runLogin(
1655
1727
  cred,
1656
1728
  saveOptions,
1657
1729
  );
1730
+ if (kiroCredentialWrite) deps.onAccountPersisted?.(kiroCredentialWrite.accountId);
1658
1731
  } else {
1659
- await (deps.saveCredential ?? saveCredential)(provider, cred, saveOptions);
1732
+ // The receipt names the row written; a stubbed save may return none.
1733
+ const receipt = await (deps.saveCredential ?? saveCredential)(provider, cred, saveOptions);
1734
+ if (receipt) deps.onAccountPersisted?.(receipt.accountId);
1660
1735
  }
1661
1736
  }
1662
1737
  if (provider !== "chatgpt") {
@@ -1711,7 +1786,7 @@ export async function runLogin(
1711
1786
  if (provider !== "chatgpt") {
1712
1787
  try {
1713
1788
  const { clearModelCache } = await import("../codex/model-cache");
1714
- const { clearGatherRoutedModelsInflight } = await import("../codex/catalog");
1789
+ const { clearGatherRoutedModelsInflight } = await import("../codex/catalog/routed-gather");
1715
1790
  clearModelCache(provider);
1716
1791
  clearGatherRoutedModelsInflight();
1717
1792
  const { clearAccountQuotaCache, clearProviderQuotaCache } = await import("../providers/quota");
@@ -1766,9 +1841,12 @@ export interface OAuthAccountSummary {
1766
1841
  * the config at its request boundary and resolves the policy there with `emailMaskingEnabled`.
1767
1842
  * The default masks, so every existing caller keeps today's behaviour.
1768
1843
  */
1769
- export function getLoginStatus(provider: string, maskEmails = true): { loggedIn: boolean; email?: string; source?: OAuthCredentials["source"]; error?: string; done: boolean; hint?: OAuthLoginHint; activeAccountId?: string; accounts?: OAuthAccountSummary[] } {
1844
+ export function getLoginStatus(provider: string, maskEmails = true, flowId?: string): { loggedIn: boolean; email?: string; source?: OAuthCredentials["source"]; error?: string; errorCode?: LoginCodeError; done: boolean; status: "idle" | "pending" | "complete" | "error"; mode?: "callback" | "code"; flowId?: string; expiresAt?: number; hint?: OAuthLoginHint; activeAccountId?: string; accounts?: OAuthAccountSummary[] } {
1770
1845
  const cred = getCredential(provider);
1771
- const st = loginState.get(provider);
1846
+ // `flowId` reads one of several concurrent flows; without it, the provider's newest flow.
1847
+ const flowKey = flowId ?? latestLoginFlowKey(provider);
1848
+ // An explicit flow id from another provider must not leak that flow's lifecycle or hint.
1849
+ const st = flowKey === undefined || loginFlowProvider(flowKey) !== provider ? undefined : loginState.get(flowKey);
1772
1850
  const set = getAccountSet(provider);
1773
1851
  const accounts: OAuthAccountSummary[] | undefined = set?.accounts.map(a => ({
1774
1852
  id: a.id,
@@ -1794,7 +1872,14 @@ export function getLoginStatus(provider: string, maskEmails = true): { loggedIn:
1794
1872
  email: projectEmail(cred?.email, maskEmails) ?? undefined,
1795
1873
  source: cred?.source,
1796
1874
  error: st?.error,
1875
+ ...(st?.errorCode ? { errorCode: st.errorCode } : {}),
1797
1876
  done: st?.done ?? false,
1877
+ // The flow's own lifecycle, which `done` alone cannot express: a finished flow that failed
1878
+ // and one that succeeded both report done:true. Callers poll this.
1879
+ status: !st ? "idle" : !st.done ? "pending" : st.error ? "error" : "complete",
1880
+ ...(st?.mode ? { mode: st.mode } : {}),
1881
+ ...(st?.flowId ? { flowId: st.flowId } : {}),
1882
+ ...(st?.expiresAt !== undefined ? { expiresAt: st.expiresAt } : {}),
1798
1883
  ...(st?.hint && !st.done ? { hint: { url: st.hint.url, instructions: st.hint.instructions, deviceCode: st.hint.deviceCode } } : {}),
1799
1884
  ...(set ? { activeAccountId: set.activeAccountId, accounts } : {}),
1800
1885
  };
@@ -1811,65 +1896,53 @@ export function oauthLoginSummary(maskEmails = true): Array<{ provider: string;
1811
1896
  });
1812
1897
  }
1813
1898
 
1814
- export function clearLoginState(provider: string): void {
1815
- loginAbort.get(provider)?.controller.abort("cleared");
1816
- loginAbort.delete(provider);
1817
- clearManualCodeSlot(provider);
1818
- loginState.delete(provider);
1819
- }
1820
-
1821
- export function cancelLoginFlow(provider: string, flowId?: string): boolean {
1822
- const active = loginAbort.get(provider);
1823
- const existing = loginState.get(provider);
1824
- if (flowId !== undefined && active?.flowId !== flowId) return false;
1825
- if (!active && (!existing || existing.done)) return false;
1826
- active?.controller.abort("cancelled");
1827
- loginAbort.delete(provider);
1828
- clearManualCodeSlot(provider);
1829
- loginState.set(provider, { done: true, error: "Login cancelled" });
1830
- return true;
1831
- }
1832
-
1833
1899
  export async function startLoginFlow(
1834
1900
  provider: string,
1835
1901
  opts?: LoginOpts,
1836
1902
  lifecycle?: LoginFlowLifecycle,
1837
- ): Promise<{ url: string; instructions?: string; deviceCode?: string }> {
1903
+ ): Promise<{ url: string; instructions?: string; deviceCode?: string; expiresAt?: number }> {
1838
1904
  const def = OAUTH_PROVIDERS[provider];
1839
1905
  if (!def) throw new UnsupportedOAuthProviderError(provider);
1840
- const existing = loginState.get(provider);
1841
- if ((existing && !existing.done) || (provider === "kiro" && kiroLoginSettling.has(provider))) {
1842
- throw new Error(`A login for ${provider} is already in progress`);
1843
- }
1844
- clearManualCodeSlot(provider);
1845
- loginState.set(provider, { done: false });
1906
+ // Never silently downgrade to the localhost redirect: the caller asked for code mode
1907
+ // precisely because it cannot reach a localhost page.
1908
+ if (opts?.codeMode && !supportsCodeLoginMode(provider)) throw new CodeLoginUnsupportedError(provider);
1909
+ admitLoginFlow(provider, opts?.codeMode === true);
1910
+ // The flow id is the key of every map below. Callers that own one (the management route, the
1911
+ // codex auth API) supply it so a later paste can be pinned to this exact attempt.
1912
+ const flowId = lifecycle?.flowId ?? randomUUID();
1913
+ const expiresAt = recordStartedLoginFlow(provider, flowId, opts?.codeMode === true);
1846
1914
  const abort = new AbortController();
1847
- loginAbort.set(provider, { controller: abort, flowId: lifecycle?.flowId });
1915
+ loginAbort.set(flowId, { controller: abort, provider });
1848
1916
  if (provider === "kiro") kiroLoginSettling.add(provider);
1849
1917
  return new Promise((resolve, reject) => {
1850
1918
  let urlResolved = false;
1851
1919
  const ctrl: OAuthController = {
1852
1920
  onAuth: ({ url, instructions, deviceCode }) => {
1853
- if (abort.signal.aborted || loginAbort.get(provider)?.controller !== abort) return;
1921
+ if (abort.signal.aborted || loginAbort.get(flowId)?.controller !== abort) return;
1854
1922
  // Device approval can fall back to manual input. Replace, never merge: the
1855
1923
  // previous device code must disappear when the provider changes the next step.
1856
1924
  const hint = { url, instructions, deviceCode };
1857
- loginState.set(provider, { done: false, hint });
1925
+ // Merged, not replaced: the flow's own identity (mode/flowId/expiresAt) was recorded
1926
+ // before the provider produced a URL and must survive every hint update.
1927
+ loginState.set(flowId, { ...loginState.get(flowId), done: false, hint });
1858
1928
  if (!urlResolved) {
1859
1929
  urlResolved = true;
1860
- resolve({ ...hint });
1930
+ resolve({ ...hint, ...(expiresAt !== undefined ? { expiresAt } : {}) });
1861
1931
  }
1862
1932
  },
1863
1933
  onProgress: () => {},
1864
1934
  // GUI fallback when the browser cannot hit the loopback callback server.
1865
- onManualCodeInput: (expectedState?: string) => waitForManualLoginCode(provider, abort.signal, expectedState),
1935
+ onManualCodeInput: (expectedState?: string) => waitForManualLoginCode(flowId, abort.signal, expectedState),
1866
1936
  signal: abort.signal,
1867
1937
  };
1868
1938
  const abandonIfNotOwner = (error?: unknown): boolean => {
1869
- if (loginAbort.get(provider)?.controller === abort) return false;
1939
+ if (loginAbort.get(flowId)?.controller === abort) return false;
1870
1940
  if (!urlResolved) reject(error ?? new Error("OAuth login was superseded"));
1871
1941
  return true;
1872
1942
  };
1943
+ // The row this flow's own credential write landed in, from the store's receipt. A token match
1944
+ // after the fact would miss it once a concurrent login for the same account rotates the token.
1945
+ let persistedAccountId: string | undefined;
1873
1946
  const settle = async (error?: unknown): Promise<void> => {
1874
1947
  // Cancellation deletes this controller and records its own terminal result. A late provider
1875
1948
  // rejection (or an older flow settling after a replacement starts) must not overwrite that
@@ -1885,9 +1958,10 @@ export async function startLoginFlow(
1885
1958
  }
1886
1959
  if (abandonIfNotOwner(finalError)) return;
1887
1960
  if (finalError === undefined) {
1888
- loginAbort.delete(provider);
1889
- clearManualCodeSlot(provider);
1890
- loginState.set(provider, { done: true });
1961
+ loginAbort.delete(flowId);
1962
+ clearManualCodeSlot(flowId);
1963
+ // Answer this flow's caller with the account it wrote, not whichever one is active now.
1964
+ settleLoginFlow(flowId, persistedAccountId ? { accountId: persistedAccountId } : {});
1891
1965
  // Local-token import (grok-cli / Claude Code keychain) completes WITHOUT firing onAuth —
1892
1966
  // resolve so the GUI call returns instead of hanging.
1893
1967
  if (!urlResolved) resolve({ url: "", instructions: "Logged in via an existing local CLI/keychain token — no browser needed." });
@@ -1895,27 +1969,28 @@ export async function startLoginFlow(
1895
1969
  }
1896
1970
 
1897
1971
  const e = finalError;
1898
- loginAbort.delete(provider);
1899
- clearManualCodeSlot(provider);
1972
+ loginAbort.delete(flowId);
1973
+ clearManualCodeSlot(flowId);
1900
1974
  const msg = publicOAuthAuthenticationErrorMessage(e);
1901
- loginState.set(provider, { done: true, error: msg });
1975
+ settleLoginFlow(flowId, { error: msg, errorCode: classifyLoginCodeError(e) });
1902
1976
  if (!urlResolved) reject(e);
1903
1977
  };
1904
1978
  // Background: runLogin persists the credential + provider entry to disk. The lifecycle hook
1905
1979
  // lets a long-lived server config adopt that settled state before clients observe done=true.
1906
1980
  const assertCurrentOwner = (): void => {
1907
- if (loginAbort.get(provider)?.controller !== abort) throw new OAuthLoginSupersededError();
1981
+ if (loginAbort.get(flowId)?.controller !== abort) throw new OAuthLoginSupersededError();
1908
1982
  };
1909
- void runLogin(provider, ctrl, opts, { assertCurrentOwner }).then(
1983
+ const onAccountPersisted = (accountId: string): void => { persistedAccountId = accountId; };
1984
+ void runLogin(provider, ctrl, opts, { assertCurrentOwner, onAccountPersisted }).then(
1910
1985
  () => settle(),
1911
1986
  (e: unknown) => settle(e),
1912
1987
  ).catch((e: unknown) => {
1913
1988
  // settle catches lifecycle failures, so this is only a defensive promise-boundary guard.
1914
1989
  if (abandonIfNotOwner(e)) return;
1915
- loginAbort.delete(provider);
1916
- clearManualCodeSlot(provider);
1990
+ loginAbort.delete(flowId);
1991
+ clearManualCodeSlot(flowId);
1917
1992
  const msg = publicOAuthAuthenticationErrorMessage(e);
1918
- loginState.set(provider, { done: true, error: msg });
1993
+ settleLoginFlow(flowId, { error: msg, errorCode: classifyLoginCodeError(e) });
1919
1994
  if (!urlResolved) reject(e);
1920
1995
  }).finally(() => {
1921
1996
  if (provider === "kiro") kiroLoginSettling.delete(provider);
@@ -3,7 +3,7 @@
3
3
  * Read-only: never writes to external credential stores.
4
4
  * Ported from jawcode packages/ai/src/utils/oauth/local-token-detect.ts (xAI portion).
5
5
  */
6
- import { execSync } from "node:child_process";
6
+ import { execFile, execSync } from "node:child_process";
7
7
  import { existsSync, readFileSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
9
  import { join } from "node:path";
@@ -109,6 +109,26 @@ function readClaudeSecureStorage(): string | null {
109
109
  return readClaudeKeychain() ?? readClaudeCredentialsFile();
110
110
  }
111
111
 
112
+ /** Non-blocking Keychain read for request paths; same item and timeout as the sync reader. */
113
+ function readClaudeKeychainAsync(): Promise<string | null> {
114
+ if (process.platform !== "darwin") return Promise.resolve(null);
115
+ return new Promise(resolve => {
116
+ execFile("security", ["find-generic-password", "-s", CLAUDE_KEYCHAIN_SERVICE, "-w"], { encoding: "utf8", timeout: 5000 },
117
+ (error, stdout) => resolve(error ? null : stdout.trim() || null));
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Async variant of {@link detectClaudeCodeToken} with the same source precedence. A request-path
123
+ * caller must not hold the event loop for a Keychain lookup (up to five seconds).
124
+ */
125
+ export async function detectClaudeCodeTokenAsync(): Promise<OAuthCredentials | null> {
126
+ const raw = process.env.CLAUDE_CONFIG_DIR?.trim()
127
+ ? readClaudeCredentialsFile() ?? await readClaudeKeychainAsync()
128
+ : await readClaudeKeychainAsync() ?? readClaudeCredentialsFile();
129
+ return raw ? parseClaudeOauthPayload(raw) : null;
130
+ }
131
+
112
132
  export function parseClaudeOauthPayload(raw: string): OAuthCredentials | null {
113
133
  try {
114
134
  const data = JSON.parse(raw) as { claudeAiOauth?: { accessToken?: string; refreshToken?: string; expiresAt?: number } };
@@ -0,0 +1,44 @@
1
+ import { AnthropicTokenError } from "./anthropic";
2
+ import { OAuthProviderNamespaceCollisionError, OAuthProviderPublicationError, publicOAuthAuthenticationErrorMessage } from "./index";
3
+ import { XaiTokenRequestError } from "./xai";
4
+ import { AnthropicInstanceCollisionError, AnthropicInstanceConfigReadError } from "./store-anthropic-instance";
5
+
6
+ // Standard RFC 6749 / RFC 6750 / RFC 8628 error codes. A provider's `error` field is printed only
7
+ // when it is one of these, so a body that echoes request parameters there still prints nothing.
8
+ const STANDARD_OAUTH_ERROR_CODES = new Set([
9
+ "invalid_request", "invalid_client", "invalid_grant", "unauthorized_client",
10
+ "unsupported_grant_type", "invalid_scope", "access_denied", "unsupported_response_type",
11
+ "server_error", "temporarily_unavailable", "invalid_token", "insufficient_scope",
12
+ "authorization_pending", "slow_down", "expired_token",
13
+ ]);
14
+
15
+ /** Fixed, machine-readable reason for a failed login; never derived from free-form text. */
16
+ export function oauthLoginFailureDiagnostic(error: unknown): string {
17
+ if (error instanceof OAuthProviderNamespaceCollisionError || error instanceof OAuthProviderPublicationError
18
+ || error instanceof AnthropicInstanceCollisionError || error instanceof AnthropicInstanceConfigReadError) {
19
+ return "configuration_error";
20
+ }
21
+ const status = error instanceof AnthropicTokenError ? error.httpStatus
22
+ : error instanceof XaiTokenRequestError ? error.status : undefined;
23
+ const oauthError = error instanceof AnthropicTokenError || error instanceof XaiTokenRequestError
24
+ ? error.oauthError : undefined;
25
+ const parts: string[] = [];
26
+ if (typeof status === "number" && Number.isInteger(status) && status >= 400 && status <= 599) parts.push(`HTTP ${status}`);
27
+ if (typeof oauthError === "string" && STANDARD_OAUTH_ERROR_CODES.has(oauthError)) parts.push(oauthError);
28
+ if (parts.length > 0) return parts.join(" ");
29
+ const name = error instanceof Error ? error.name : "";
30
+ if (name === "TimeoutError" || name === "AbortError") return "timeout";
31
+ // fetch() reports refused connections and DNS failures as a TypeError carrying a string code; a
32
+ // programming TypeError carries none and stays in the generic bucket.
33
+ const code = error instanceof TypeError ? (error as { code?: unknown }).code : undefined;
34
+ if ((error instanceof AnthropicTokenError && error.requestNotSent)
35
+ || (error instanceof XaiTokenRequestError && error.status === undefined)
36
+ || (typeof code === "string" && code !== "")) return "network_error";
37
+ return "oauth_authentication_failed";
38
+ }
39
+
40
+ /** Keep terminal login failures within the public OAuth error vocabulary. */
41
+ export function reportOAuthLoginError(error: unknown): never {
42
+ console.error(`\n❌ OAuth login failed (${oauthLoginFailureDiagnostic(error)}): ${publicOAuthAuthenticationErrorMessage(error)}`);
43
+ process.exit(1);
44
+ }