cartethyia 2.7.9 → 2.8.1

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 (496) hide show
  1. package/.env.example +6 -5
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BlC_xdkX.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-0yfhSI1C.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
  31. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage--s-EfEIL.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-2hc12Uz2.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
  44. package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
  45. package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-BMcj1xZE.js +1 -0
  47. package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
  49. package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
  51. package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
  53. package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-DaH6g9hL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-BYKBCzjk.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
  59. package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-Dpx_UXaS.js} +1 -1
  61. package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-BV7-1NoM.js} +1 -1
  63. package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
  65. package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
  67. package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
  69. package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
  71. package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-Dl-cuLK0.js} +1 -1
  73. package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
  75. package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-BxhbQ_JS.js} +1 -1
  77. package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-D49Y4ptn.js} +1 -1
  79. package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
  81. package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
  83. package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
  85. package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-iUoI8GeE.js} +1 -1
  87. package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
  89. package/dist/dashboard/assets/providers/sensenova.png +0 -0
  90. package/dist/dashboard/assets/providers/zro.svg +3 -0
  91. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  92. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  93. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  94. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
  95. package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
  96. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
  97. package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
  98. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
  99. package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
  100. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
  101. package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
  102. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
  103. package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
  104. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
  105. package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
  106. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
  107. package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
  108. package/dist/dashboard/index.html +2 -2
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +54 -53
  111. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  112. package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
  113. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  114. package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
  115. package/package.json +1 -1
  116. package/src/boot/app.ts +11 -14
  117. package/src/boot/dependencies.ts +5 -7
  118. package/src/boot/flush.ts +20 -0
  119. package/src/boot/lifecycle.ts +7 -5
  120. package/src/config/providers.ts +6 -5
  121. package/src/config/routing.ts +26 -0
  122. package/src/config/spec-metadata.ts +72 -0
  123. package/src/config/spec.ts +28 -8
  124. package/src/console/api-keys/contracts.ts +12 -1
  125. package/src/console/api-keys/routes.ts +40 -28
  126. package/src/console/auth/account-reset.ts +7 -9
  127. package/src/console/backup/contracts.ts +4 -0
  128. package/src/console/backup/export-endpoint.ts +7 -5
  129. package/src/console/backup/nine-router.ts +71 -10
  130. package/src/console/backup/service.ts +8 -1
  131. package/src/console/cli-tools/service.ts +6 -12
  132. package/src/console/console-router-guards.ts +23 -16
  133. package/src/console/console-router.ts +1 -1
  134. package/src/console/domain-registration.ts +7 -4
  135. package/src/console/inferhub/client.ts +1 -1
  136. package/src/console/inferhub/normalize.ts +24 -28
  137. package/src/console/inferhub/types.ts +3 -0
  138. package/src/console/ip-bans/routes.ts +7 -13
  139. package/src/console/observability/live.ts +2 -2
  140. package/src/console/observability/store.ts +0 -53
  141. package/src/console/providers/catalog/contracts.ts +9 -3
  142. package/src/console/providers/catalog/model-operations.ts +2 -3
  143. package/src/console/providers/catalog/provider-operations.ts +1 -1
  144. package/src/console/providers/catalog/routes.ts +3 -2
  145. package/src/console/providers/catalog/store.ts +1914 -1902
  146. package/src/console/providers/detail/routes.ts +1 -0
  147. package/src/console/providers/detail/store.ts +4 -0
  148. package/src/console/providers/oauth/callback-listener.ts +30 -11
  149. package/src/console/providers/oauth/device-egress.ts +123 -0
  150. package/src/console/providers/oauth/routes.ts +123 -16
  151. package/src/console/quota/account-quota-global.ts +14 -24
  152. package/src/console/quota/account-quota-shared.ts +49 -0
  153. package/src/console/quota/account-quota-tenant.ts +11 -23
  154. package/src/console/quota/account-test-egress.ts +2 -2
  155. package/src/console/quota/daily-growth-job.ts +1 -1
  156. package/src/console/quota/quota-view.ts +19 -3
  157. package/src/console/routing/pools/contracts.ts +1 -1
  158. package/src/console/routing/pools/operations.ts +32 -47
  159. package/src/console/routing/pools/probe-verdict.ts +2 -2
  160. package/src/console/routing/pools/relay-deploy.ts +1 -1
  161. package/src/console/routing/pools/store.ts +4 -4
  162. package/src/console/shared/errors.ts +33 -6
  163. package/src/main.ts +2 -3
  164. package/src/network/client.ts +67 -0
  165. package/src/network/egress/bidi-session.ts +322 -0
  166. package/src/network/egress/dial.ts +201 -0
  167. package/src/network/egress/factory.ts +191 -0
  168. package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
  169. package/src/network/egress/tls-profile.ts +123 -0
  170. package/src/network/egress/ws.ts +175 -0
  171. package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
  172. package/src/network/{types.ts → pool/agent-config.ts} +1 -1
  173. package/src/network/pool/agent.ts +2 -2
  174. package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
  175. package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
  176. package/src/network/pool/resolver.ts +10 -849
  177. package/src/network/pool/selector.ts +184 -41
  178. package/src/network/ssrf.ts +7 -7
  179. package/src/network/{retry.ts → util/retry.ts} +1 -1
  180. package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
  181. package/src/observability/event-log.ts +7 -361
  182. package/src/observability/performance-metrics.ts +9 -0
  183. package/src/observability/runtime-metrics.ts +14 -9
  184. package/src/observability/telemetry-buffer.ts +16 -26
  185. package/src/observability/telemetry-reconciliation.ts +9 -37
  186. package/src/observability/telemetry-row.ts +125 -0
  187. package/src/persistence/db-mode.ts +4 -1
  188. package/src/persistence/dialect.ts +18 -0
  189. package/src/persistence/migrate.ts +6 -3
  190. package/src/persistence/schema.ts +17 -0
  191. package/src/persistence/ttl-cache.ts +21 -3
  192. package/src/protocol/codecs.ts +59 -2
  193. package/src/protocol/dsml.ts +34 -86
  194. package/src/protocol/markup-stream-parser.ts +195 -0
  195. package/src/protocol/reasoning-text.ts +19 -0
  196. package/src/protocol/request/chat.ts +10 -10
  197. package/src/protocol/request/gemini.ts +8 -2
  198. package/src/protocol/request/messages.ts +34 -22
  199. package/src/protocol/response/chat.ts +12 -20
  200. package/src/protocol/response/messages.ts +34 -7
  201. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  202. package/src/protocol/response/responses.ts +18 -2
  203. package/src/protocol/stop-reason.ts +23 -1
  204. package/src/protocol/think-split.ts +112 -0
  205. package/src/protocol/translation/dialect/qwen.ts +26 -58
  206. package/src/protocol/translation/quirks.ts +15 -0
  207. package/src/protocol/upstream/send-claude-messages.ts +2 -2
  208. package/src/providers/adapters/compatible-adapter.ts +76 -12
  209. package/src/providers/authentication/oauth-client.ts +40 -6
  210. package/src/providers/authentication/oauth-device-flow.ts +105 -11
  211. package/src/providers/authentication/oauth-flow-store.ts +82 -1
  212. package/src/providers/authentication/oauth-refresh-service.ts +766 -758
  213. package/src/providers/configured-provider.ts +148 -0
  214. package/src/providers/default-registry.ts +137 -36
  215. package/src/providers/discovery/discovery-types.ts +7 -0
  216. package/src/providers/discovery/probe-phases.ts +20 -6
  217. package/src/providers/discovery/probing-service.ts +22 -2
  218. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  219. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  220. package/src/providers/integrations/antigravity/antigravity-protocol.ts +16 -1
  221. package/src/providers/integrations/buddy/account.ts +969 -0
  222. package/src/providers/integrations/buddy/capabilities.ts +12 -12
  223. package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
  224. package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
  225. package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
  226. package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
  227. package/src/providers/integrations/buddy/headers.ts +311 -0
  228. package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
  229. package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
  230. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  231. package/src/providers/integrations/cline/cline-oauth.ts +10 -5
  232. package/src/providers/integrations/cline/cline.ts +5 -6
  233. package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
  234. package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
  235. package/src/providers/integrations/clixad/provider.ts +145 -0
  236. package/src/providers/integrations/codex/codex-oauth.ts +8 -5
  237. package/src/providers/integrations/commandcode/quota.ts +22 -28
  238. package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
  239. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  240. package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
  241. package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
  242. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  243. package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
  244. package/src/providers/integrations/cursor/cursor.ts +574 -110
  245. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  246. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  247. package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
  248. package/src/providers/integrations/deepai/deepai-common.ts +145 -0
  249. package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
  250. package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
  251. package/src/providers/integrations/deepai/provider.ts +42 -0
  252. package/src/providers/integrations/devin/devin-auth.ts +7 -0
  253. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  254. package/src/providers/integrations/factory/factory-oauth.ts +33 -51
  255. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  256. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  257. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  258. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  259. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  260. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  261. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  262. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  263. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  264. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  265. package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
  266. package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
  267. package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
  268. package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
  269. package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
  270. package/src/providers/integrations/freepi/freepi-session.ts +158 -0
  271. package/src/providers/integrations/freepi/freepi.ts +80 -54
  272. package/src/providers/integrations/github/github-discovery.ts +30 -13
  273. package/src/providers/integrations/github/github-oauth.ts +217 -44
  274. package/src/providers/integrations/github/github-quota.ts +22 -5
  275. package/src/providers/integrations/github/github.ts +40 -32
  276. package/src/providers/integrations/grok/grok-oauth.ts +52 -49
  277. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  278. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  279. package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
  280. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
  281. package/src/providers/integrations/hermes/index.ts +2 -0
  282. package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
  283. package/src/providers/integrations/kilo/kilo.ts +1 -1
  284. package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
  285. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  286. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  287. package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
  288. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  289. package/src/providers/integrations/kiro/kiro.ts +15 -19
  290. package/src/providers/integrations/muse/muse-oauth.ts +5 -5
  291. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  292. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  293. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  294. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  295. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  296. package/src/providers/integrations/openrouter/provider.ts +1 -1
  297. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  298. package/src/providers/integrations/perch/perch-protocol.ts +0 -6
  299. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  300. package/src/providers/integrations/perch/perch.ts +6 -16
  301. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  302. package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
  303. package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
  304. package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
  305. package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
  306. package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
  307. package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
  308. package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
  309. package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
  310. package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
  311. package/src/providers/integrations/qoder/qoder-types.ts +9 -0
  312. package/src/providers/integrations/qoder/qoder.ts +3 -26
  313. package/src/providers/integrations/sensenova/provider.ts +144 -0
  314. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  315. package/src/providers/integrations/trae/trae-quota.ts +21 -29
  316. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  317. package/src/providers/integrations/trae/trae.ts +7 -24
  318. package/src/providers/integrations/vercel/provider.ts +117 -0
  319. package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
  320. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  321. package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
  322. package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
  323. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  324. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  325. package/src/providers/integrations/zro/zro.ts +205 -0
  326. package/src/providers/operations/account-error-classification.ts +199 -307
  327. package/src/providers/operations/account-health-service.ts +82 -1
  328. package/src/providers/operations/account-refresh-status.ts +36 -37
  329. package/src/providers/operations/account-tier.ts +15 -16
  330. package/src/providers/operations/client-version-resolver.ts +1 -1
  331. package/src/providers/operations/client-versions.ts +100 -81
  332. package/src/providers/operations/cooldown-rules.ts +11 -16
  333. package/src/providers/operations/oauth-refresh-lead.ts +46 -22
  334. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  335. package/src/providers/operations/provider-catalog-service.ts +1 -1
  336. package/src/providers/operations/provider-credential-service.ts +118 -51
  337. package/src/providers/operations/provider-rules/github.ts +48 -0
  338. package/src/providers/operations/provider-rules/grok.ts +33 -0
  339. package/src/providers/operations/provider-rules/inception.ts +49 -0
  340. package/src/providers/operations/provider-rules/index.ts +32 -0
  341. package/src/providers/operations/provider-rules/perch.ts +37 -0
  342. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  343. package/src/providers/operations/provider-rules/trae.ts +81 -0
  344. package/src/providers/operations/session-resolution.ts +17 -1
  345. package/src/providers/provider-contracts.ts +6 -4
  346. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  347. package/src/providers/provider-metadata.ts +70 -10
  348. package/src/providers/provider-probe.ts +15 -1
  349. package/src/providers/provider-registry.ts +42 -33
  350. package/src/providers/quota/quota-contracts.ts +2 -0
  351. package/src/providers/quota/quota-limit-windows.ts +16 -5
  352. package/src/providers/quota/quota-support.ts +1 -1
  353. package/src/providers/quota/quota-window-parser.ts +90 -18
  354. package/src/providers/quota/refresh.ts +100 -21
  355. package/src/security/api-key-auth-cache.ts +1 -1
  356. package/src/security/api-key-auth.ts +6 -2
  357. package/src/security/ip-boundary.ts +1 -1
  358. package/src/transport/dispatch/attempt-finalize.ts +15 -14
  359. package/src/transport/dispatch/attempt-loop.ts +73 -19
  360. package/src/transport/dispatch/generation.ts +2 -8
  361. package/src/transport/dispatch/handler-deps.ts +1 -1
  362. package/src/transport/dispatch/leases.ts +1 -1
  363. package/src/transport/dispatch/native-shared.ts +25 -0
  364. package/src/transport/dispatch/proxy-request.ts +8 -46
  365. package/src/transport/dispatch/responses-compact.ts +2 -8
  366. package/src/transport/dispatch/retry-policy.ts +1 -1
  367. package/src/transport/dispatch/streaming/finalize.ts +10 -7
  368. package/src/transport/dispatch/streaming/stream.ts +9 -37
  369. package/src/transport/dispatch/systemone.ts +2 -8
  370. package/src/transport/dispatch/token-count.ts +2 -6
  371. package/src/transport/dispatch/upstream.ts +63 -0
  372. package/src/transport/drain-endpoint.ts +31 -4
  373. package/src/transport/failure-policy.ts +16 -30
  374. package/src/transport/guards/body-policy.ts +7 -1
  375. package/src/transport/guards/error-lifecycle.ts +13 -1
  376. package/src/transport/guards/request-context.ts +22 -14
  377. package/src/transport/request/preparer.ts +81 -71
  378. package/src/transport/routing/bounded-lru.ts +14 -0
  379. package/src/transport/routing/drain-reserve.ts +129 -0
  380. package/src/transport/routing/plan-gate.ts +7 -1
  381. package/src/transport/routing/round-robin.ts +60 -0
  382. package/src/transport/routing/route-catalog.ts +123 -111
  383. package/src/transport/routing/route-model.ts +51 -9
  384. package/src/transport/routing/router.ts +393 -32
  385. package/src/transport/shutdown-notice.ts +1 -1
  386. package/src/transport/surface/adapters.ts +11 -2
  387. package/src/transport/surface/chat/adapter.ts +2 -2
  388. package/src/transport/surface/chat/encode.ts +47 -4
  389. package/src/transport/surface/chat/parse.ts +2 -12
  390. package/src/transport/surface/completion.ts +44 -33
  391. package/src/transport/surface/content-parts.ts +21 -0
  392. package/src/transport/surface/messages/adapter.ts +7 -1
  393. package/src/transport/surface/messages/encode.ts +11 -3
  394. package/src/transport/surface/messages/parse.ts +2 -1
  395. package/src/transport/surface/messages/stream.ts +33 -17
  396. package/src/transport/surface/responses/encode.ts +22 -5
  397. package/src/transport/surface/responses/parse.ts +3 -11
  398. package/src/workers/buddy-growth-tasks.ts +8 -8
  399. package/src/workers/daily-checkin.ts +4 -4
  400. package/src/workers/quota-refresh-sweep.ts +1 -1
  401. package/src/workers/zcode-gift-claim.ts +1 -1
  402. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  403. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  404. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  405. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  406. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  407. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  408. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  409. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  410. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  411. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  412. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  413. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  414. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  415. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  416. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  417. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  418. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  419. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
  420. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  421. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  422. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  423. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  424. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  425. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  426. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  427. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  428. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  429. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  430. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  431. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  432. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  433. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  434. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  435. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  436. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  437. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  438. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  439. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  440. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  441. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  442. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  443. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  444. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  445. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  446. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  447. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  448. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  449. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  450. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  451. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  452. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  453. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  454. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  455. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  456. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  457. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  458. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  459. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  460. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  461. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  462. package/src/network/pool-health.ts +0 -210
  463. package/src/providers/bundled-discovery-providers.ts +0 -98
  464. package/src/providers/configured-openai-providers.ts +0 -120
  465. package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
  466. package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
  467. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
  468. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
  469. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
  470. package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
  471. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
  472. package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
  473. package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
  474. package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
  475. package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
  476. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
  477. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
  478. package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
  479. package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
  480. package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
  481. package/src/providers/integrations/qoder/chat-proto.ts +0 -328
  482. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
  483. package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
  484. package/src/providers/operations/model-discovery-cache.ts +0 -34
  485. package/src/providers/operations/probe-fetch.ts +0 -15
  486. package/src/providers/operations/provider-version-cache.ts +0 -30
  487. package/src/providers/provider-capabilities.ts +0 -33
  488. package/src/providers/provider-credential-envelope.ts +0 -43
  489. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
  490. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
  491. /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
  492. /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
  493. /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
  494. /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
  495. /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
  496. /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
@@ -163,6 +163,7 @@ const updateRoutingBody = t.Object({
163
163
  usageFloor: t.Optional(t.Nullable(t.Integer({ minimum: 0, maximum: 1_000_000 }))),
164
164
  usageFloorUnit: t.Optional(t.Union([t.Literal("credit"), t.Literal("percent")])),
165
165
  skipProxy: t.Optional(t.Boolean()),
166
+ deviceFlowProxy: t.Optional(t.Union([t.Literal("random"), t.Literal("direct")])),
166
167
  userAgent: t.Optional(t.String({ minLength: 0, maxLength: 4096 })),
167
168
  customUserAgent: t.Optional(t.Nullable(t.String({ minLength: 0, maxLength: 4096 }))),
168
169
  });
@@ -40,6 +40,7 @@ function mapRow(row: typeof providerRoutingSettings.$inferSelect): ProviderRouti
40
40
  usageFloorUnit: row.usageFloorUnit === "credit" ? "credit" : "percent",
41
41
  enabled: row.enabled,
42
42
  skipProxy: row.skipProxy,
43
+ deviceFlowProxy: row.deviceFlowProxy === "direct" ? "direct" : "random",
43
44
  userAgent: row.userAgent,
44
45
  customUserAgent: row.customUserAgent,
45
46
  };
@@ -94,6 +95,7 @@ export class DrizzleProviderDetailStore implements ProviderDetailStore {
94
95
  usageFloorUnit: "percent",
95
96
  enabled: false,
96
97
  skipProxy: defaultSkipProxy(providerId),
98
+ deviceFlowProxy: "random",
97
99
  userAgent: buildClaudeCliUserAgent(),
98
100
  customUserAgent: null,
99
101
  };
@@ -117,6 +119,7 @@ export class DrizzleProviderDetailStore implements ProviderDetailStore {
117
119
  usageFloorUnit: patch.usageFloorUnit ?? "percent",
118
120
  enabled: patch.enabled ?? false,
119
121
  skipProxy: patch.skipProxy ?? defaultSkipProxy(providerId),
122
+ deviceFlowProxy: patch.deviceFlowProxy ?? "random",
120
123
  userAgent: patch.userAgent ?? buildClaudeCliUserAgent(),
121
124
  customUserAgent: patch.customUserAgent ?? null,
122
125
  };
@@ -130,6 +133,7 @@ export class DrizzleProviderDetailStore implements ProviderDetailStore {
130
133
  if (patch.usageFloorUnit !== undefined) setClause.usageFloorUnit = patch.usageFloorUnit;
131
134
  if (patch.enabled !== undefined) setClause.enabled = patch.enabled;
132
135
  if (patch.skipProxy !== undefined) setClause.skipProxy = patch.skipProxy;
136
+ if (patch.deviceFlowProxy !== undefined) setClause.deviceFlowProxy = patch.deviceFlowProxy;
133
137
  if (patch.userAgent !== undefined) setClause.userAgent = patch.userAgent;
134
138
  if (patch.customUserAgent !== undefined) setClause.customUserAgent = patch.customUserAgent;
135
139
  const upserted = await this.db
@@ -158,14 +158,14 @@ export class OAuthCallbackListener {
158
158
  /**
159
159
  * The paths one login may deliver its callback on. Kiro's portal is observed
160
160
  * (unpacked IDE 1.4.62) to redirect to both `/oauth/callback` and
161
- * `/signin/callback`; binding only the advertised path left a redirect to the
162
- * other one hitting a 404 and the login hanging. Other providers neither
163
- * register nor expect `/signin/callback`, so adding it is harmless.
161
+ * `/signin/callback`; DeepAI's desktop flow lands on `/auth-callback`.
162
+ * Binding only the advertised path left a redirect to the other one hitting
163
+ * a 404 and the login hanging. Other providers neither register nor expect
164
+ * these, so adding them is harmless.
164
165
  */
165
166
  #callbackPaths(advertised: string): readonly string[] {
166
- return advertised === "/signin/callback"
167
- ? [advertised]
168
- : [advertised, "/signin/callback"];
167
+ const extras = advertised === "/signin/callback" ? [] : ["/signin/callback", "/auth-callback"];
168
+ return [advertised, ...extras];
169
169
  }
170
170
 
171
171
  /** Drops a flow and frees its port when nothing else is waiting on it. */
@@ -235,13 +235,32 @@ export class OAuthCallbackListener {
235
235
  listener.flows.set(state, { providerId, paths, timer, inFlight: false });
236
236
  }
237
237
 
238
- #forget(port: number, listener: PortListener, state: string): void {
238
+ #forget(
239
+ port: number,
240
+ listener: PortListener,
241
+ state: string,
242
+ options: { readonly deferStop?: boolean } = {},
243
+ ): void {
239
244
  const flow = listener.flows.get(state);
240
245
  if (flow !== undefined) clearTimeout(flow.timer);
241
246
  listener.flows.delete(state);
242
247
  if (listener.flows.size > 0) return;
243
248
  this.#ports.delete(port);
244
- for (const server of listener.servers) void server.stop(true);
249
+ const stopServers = () => {
250
+ for (const server of listener.servers) void server.stop(true);
251
+ };
252
+ if (options.deferStop === true) {
253
+ // The stop is called from inside a callback handler that is about to
254
+ // return its Response. `stop(true)` closes active connections, so
255
+ // stopping synchronously here kills the very response the browser is
256
+ // waiting for — the login "succeeds" server-side while the callback tab
257
+ // sees a connection reset (observed live: HTTP 000, and more reliably on
258
+ // deployed/docker hosts). Defer the stop one tick so the response is
259
+ // flushed first; the port is freed a moment later.
260
+ setTimeout(stopServers, 50);
261
+ } else {
262
+ stopServers();
263
+ }
245
264
  log.info(`[oauth] callback listener released on 127.0.0.1:${port}`);
246
265
  }
247
266
 
@@ -311,7 +330,7 @@ export class OAuthCallbackListener {
311
330
  if (error !== null) {
312
331
  // A denial carries the state only when the server echoes one; clear it
313
332
  // when present so a retry starts a fresh flow.
314
- if (state.length > 0) this.#forget(endpoint.port, listener, state);
333
+ if (state.length > 0) this.#forget(endpoint.port, listener, state, { deferStop: true });
315
334
  return new Response(
316
335
  `<!doctype html><html><body><p>OAuth login failed: ${escapeHtml(error)}</p></body></html>`,
317
336
  { status: 400, headers: { "content-type": "text/html; charset=utf-8" } },
@@ -374,7 +393,7 @@ export class OAuthCallbackListener {
374
393
  Object.keys(callbackParams).length > 0 ? callbackParams : undefined,
375
394
  );
376
395
  } catch (failure) {
377
- if (matchedState !== undefined) this.#forget(endpoint.port, listener, matchedState);
396
+ if (matchedState !== undefined) this.#forget(endpoint.port, listener, matchedState, { deferStop: true });
378
397
  const message = failure instanceof Error ? failure.message : "OAuth login failed";
379
398
  log.error(`[oauth] callback completion failed for ${matched.providerId}: ${message}`);
380
399
  return new Response(
@@ -393,7 +412,7 @@ export class OAuthCallbackListener {
393
412
  headers: { location: result.redirectUrl },
394
413
  });
395
414
  }
396
- if (matchedState !== undefined) this.#forget(endpoint.port, listener, matchedState);
415
+ if (matchedState !== undefined) this.#forget(endpoint.port, listener, matchedState, { deferStop: true });
397
416
  if (!result.ok) {
398
417
  return new Response(
399
418
  `<!doctype html><html><body><p>OAuth login failed: ${escapeHtml(result.message)}</p></body></html>`,
@@ -0,0 +1,123 @@
1
+ // Device-flow OAuth egress resolution.
2
+ //
3
+ // Device-code flows (start + poll) previously always dialed from the
4
+ // gateway's own egress, so every code minted on one host appeared to come
5
+ // from a single IP — which let Cursor/Grok/other device-code providers flag
6
+ // "multiple codes from one IP". The `device_flow_proxy` routing setting says
7
+ // whether to route these requests through one of the tenant's active network
8
+ // pools ("random", the default) or direct ("direct").
9
+ //
10
+ // The resolved fetch is a validated fetch bound to either a pool or the
11
+ // gateway's direct egress, and is injected into the OAuth flow context so the
12
+ // provider client uses it for its device-authorization and token-poll calls.
13
+
14
+ import { eq, and, isNull } from "drizzle-orm";
15
+ import type { DeviceFlowProxyMode } from "../../../persistence/schema";
16
+ import { networkPools, providerRoutingSettings } from "../../../persistence/schema";
17
+ import type { CartethyiaDatabase } from "../../../persistence/db-handle";
18
+ import type { NetworkPoolSelector } from "../../../network/pool/selector";
19
+ import type { ValidatedNetworkBindingFactory } from "../../../network/egress/factory";
20
+ import type { FetchLike } from "../../../providers/authentication/oauth-flow-store";
21
+
22
+ /** A resolved device-flow egress: the fetch to use, plus its release hook. */
23
+ export interface DeviceEgress {
24
+ readonly fetch: FetchLike;
25
+ /** Release an acquired pool slot (no-op for direct egress). */
26
+ readonly release: () => void;
27
+ /** The pool selected, when one was (for correlation/persistence). */
28
+ readonly networkPoolId?: string;
29
+ }
30
+
31
+ /** Dependencies the device-egress resolver needs. */
32
+ export interface DeviceEgressDeps {
33
+ readonly db: CartethyiaDatabase;
34
+ readonly poolSelector: NetworkPoolSelector;
35
+ readonly networkBindingFactory: ValidatedNetworkBindingFactory;
36
+ }
37
+
38
+ /** Reads the provider's `device_flow_proxy` mode; absent rows default to `random`. */
39
+ async function resolveDeviceFlowProxyMode(
40
+ db: CartethyiaDatabase,
41
+ providerId: string,
42
+ tenantId: string | null,
43
+ ): Promise<DeviceFlowProxyMode> {
44
+ if (tenantId !== null) {
45
+ const tenantRow = await db
46
+ .select({ mode: providerRoutingSettings.deviceFlowProxy })
47
+ .from(providerRoutingSettings)
48
+ .where(
49
+ and(
50
+ eq(providerRoutingSettings.providerId, providerId),
51
+ eq(providerRoutingSettings.tenantId, tenantId),
52
+ ),
53
+ )
54
+ .limit(1);
55
+ if (tenantRow[0]) return tenantRow[0].mode === "direct" ? "direct" : "random";
56
+ }
57
+ const globalRow = await db
58
+ .select({ mode: providerRoutingSettings.deviceFlowProxy })
59
+ .from(providerRoutingSettings)
60
+ .where(
61
+ and(
62
+ eq(providerRoutingSettings.providerId, providerId),
63
+ isNull(providerRoutingSettings.tenantId),
64
+ ),
65
+ )
66
+ .limit(1);
67
+ return globalRow[0]?.mode === "direct" ? "direct" : "random";
68
+ }
69
+
70
+ /**
71
+ * Resolves the egress for one device-code flow. `random` selects an active
72
+ * network pool (weighted, cooldown-aware) when one is available and falls
73
+ * back to direct egress when the tenant has no usable pool; `direct` always
74
+ * dials direct. The returned fetch is what the provider client must use for
75
+ * its device-authorization and token-poll requests.
76
+ */
77
+ export async function resolveDeviceEgress(
78
+ deps: DeviceEgressDeps,
79
+ providerId: string,
80
+ tenantId: string | null,
81
+ ): Promise<DeviceEgress> {
82
+ const mode = await resolveDeviceFlowProxyMode(deps.db, providerId, tenantId);
83
+ const direct = (): DeviceEgress => ({
84
+ fetch: deps.networkBindingFactory.fetch(undefined, tenantId ?? undefined),
85
+ release: () => undefined,
86
+ });
87
+
88
+ if (mode === "direct" || tenantId === null) return direct();
89
+
90
+ const poolRows = await deps.db
91
+ .select({
92
+ id: networkPools.id,
93
+ maxInflight: networkPools.maxInflight,
94
+ weight: networkPools.weight,
95
+ })
96
+ .from(networkPools)
97
+ .where(and(eq(networkPools.tenantId, tenantId), eq(networkPools.status, "active")));
98
+
99
+ if (poolRows.length > 0) {
100
+ const limits: Record<string, number> = {};
101
+ const weights: Record<string, number> = {};
102
+ for (const pool of poolRows) {
103
+ limits[pool.id] = pool.maxInflight ?? 10;
104
+ weights[pool.id] = pool.weight ?? 100;
105
+ }
106
+ const slot = await deps.poolSelector.tryAcquireAvailablePool(
107
+ poolRows.map((pool) => pool.id),
108
+ providerId,
109
+ limits,
110
+ weights,
111
+ );
112
+ if (slot) {
113
+ return {
114
+ fetch: deps.networkBindingFactory.fetch(slot.poolId, tenantId ?? undefined),
115
+ release: slot.release,
116
+ networkPoolId: slot.poolId,
117
+ };
118
+ }
119
+ }
120
+
121
+ // No active/usable pool: direct is the safe fallback (never block a login).
122
+ return direct();
123
+ }
@@ -2,11 +2,17 @@ import { providerAccounts, providerOauthStates } from "../../../persistence/sche
2
2
  import { and, eq, isNull, sql } from "drizzle-orm";
3
3
  import { encryptCredential, hashSecret } from "../../../security/crypto";
4
4
  import type { CartethyiaDatabase } from "../../../persistence/db-handle";
5
- import type { OAuthExchangeResult, OAuthLoginClient } from "../../../providers/authentication/oauth-flow-store";
5
+ import type {
6
+ OAuthDeviceStartResult,
7
+ OAuthExchangeResult,
8
+ OAuthLoginClient,
9
+ } from "../../../providers/authentication/oauth-flow-store";
10
+ import { oauthAccountIdentity } from "../../../providers/authentication/oauth-flow-store";
6
11
  import type { OAuthDevicePollResponse } from "../catalog/contracts";
7
12
  import { providerJwtVerification } from "../../../providers/provider-metadata";
8
13
  import type { ProviderRegistry } from "../../../providers/provider-registry";
9
14
  import { browserAuthorizeRedirectUri } from "../../../config/providers";
15
+ import { resolveDeviceEgress, type DeviceEgressDeps } from "./device-egress";
10
16
  import { log } from "../../../observability/logger";
11
17
  import { GatewayError } from "../../../protocol/gateway-error";
12
18
  import { Elysia, t, type AnyElysia } from "elysia";
@@ -41,11 +47,53 @@ export function oauthIdentityFingerprint(input: {
41
47
  readonly label: string;
42
48
  readonly providerId: string;
43
49
  } & OAuthExchangeResult): string {
50
+ const identity = input.accountEmail?.trim() ?? input.accountName?.trim();
51
+ if (identity !== undefined && identity.length > 0) return hashSecret(identity);
44
52
  const label = input.label.trim();
45
- const resolved = label.length > 0 && label.toLowerCase() !== input.providerId.trim().toLowerCase();
53
+ const resolved =
54
+ label.length > 0 &&
55
+ label.toLowerCase() !== input.providerId.trim().toLowerCase() &&
56
+ !isGeneratedOAuthLabel(label, input.providerId);
46
57
  return hashSecret(resolved ? label : input.refresh);
47
58
  }
48
59
 
60
+ /** First unused `<providerId>-N` label for an OAuth account with no upstream identity. */
61
+ export function nextGeneratedOAuthAccountLabel(
62
+ providerId: string,
63
+ existingLabels: readonly string[],
64
+ ): string {
65
+ const taken = new Set(existingLabels.map((label) => label.trim().toLowerCase()));
66
+ for (let n = 1; ; n += 1) {
67
+ const candidate = `${providerId}-${n}`;
68
+ if (!taken.has(candidate.toLowerCase())) return candidate;
69
+ }
70
+ }
71
+
72
+ /** Generated labels are display-only and must never become re-login identity. */
73
+ export function isGeneratedOAuthLabel(label: string, providerId: string): boolean {
74
+ const value = label.trim().toLowerCase();
75
+ const prefix = `${providerId.trim().toLowerCase()}-`;
76
+ return value === providerId.trim().toLowerCase() || (value.startsWith(prefix) && /^\d+$/.test(value.slice(prefix.length)));
77
+ }
78
+
79
+ /** Merges provider-reported identity with the operator label without inventing data. */
80
+ function resolvedOAuthIdentity(
81
+ input: { readonly label: string } & OAuthExchangeResult,
82
+ providerId: string,
83
+ ): { readonly label: string; readonly accountEmail?: string; readonly accountName?: string } {
84
+ const tokenIdentity = oauthAccountIdentity(input);
85
+ const accountEmail = input.accountEmail?.trim() ?? tokenIdentity.accountEmail;
86
+ const accountName = input.accountName?.trim() ?? tokenIdentity.accountName;
87
+ const existing = input.label.trim();
88
+ const unresolved = existing.length === 0 || isGeneratedOAuthLabel(existing, providerId);
89
+ const label = unresolved ? (accountEmail ?? accountName ?? existing) : existing;
90
+ return {
91
+ label,
92
+ ...(accountEmail === undefined ? {} : { accountEmail }),
93
+ ...(accountName === undefined ? {} : { accountName }),
94
+ };
95
+ }
96
+
49
97
  export class DrizzleOAuthAccountStore implements OAuthAccountStore {
50
98
  constructor(private readonly db: CartethyiaDatabase) {}
51
99
 
@@ -54,7 +102,8 @@ export class DrizzleOAuthAccountStore implements OAuthAccountStore {
54
102
  providerId: string,
55
103
  input: { readonly label: string } & OAuthExchangeResult,
56
104
  ): Promise<PersistedOAuthAccount> {
57
- const credentialFingerprint = oauthIdentityFingerprint({ ...input, providerId });
105
+ const identity = resolvedOAuthIdentity(input, providerId);
106
+ const credentialFingerprint = oauthIdentityFingerprint({ ...input, ...identity, providerId });
58
107
  const sameAccount = and(
59
108
  eq(providerAccounts.providerId, providerId),
60
109
  // The identity index coalesces a null tenant to a sentinel, so "same
@@ -68,6 +117,20 @@ export class DrizzleOAuthAccountStore implements OAuthAccountStore {
68
117
  const refreshCiphertext = encryptCredential(input.refresh);
69
118
  const clientSecretCiphertext =
70
119
  input.client_secret === undefined ? undefined : encryptCredential(input.client_secret);
120
+ const identityColumns = {
121
+ ...(input.auth_state === undefined && identity.accountName === undefined
122
+ ? {}
123
+ : {
124
+ authState: {
125
+ ...(input.auth_state ?? {}),
126
+ ...(identity.accountName === undefined ? {} : { accountName: identity.accountName }),
127
+ },
128
+ }),
129
+ ...(identity.accountEmail === undefined ? {} : { accountEmail: identity.accountEmail }),
130
+ ...(identity.accountEmail !== undefined || identity.accountName !== undefined
131
+ ? { identityCheckedAt: new Date() }
132
+ : {}),
133
+ };
71
134
  try {
72
135
  return await this.db.transaction(async (tx) => {
73
136
  const existing = await tx
@@ -76,6 +139,7 @@ export class DrizzleOAuthAccountStore implements OAuthAccountStore {
76
139
  .where(sameAccount)
77
140
  .limit(1);
78
141
  const existingId = existing[0]?.id;
142
+ const label = isGeneratedOAuthLabel(input.label, providerId) ? identity.label : input.label;
79
143
  if (existingId !== undefined) {
80
144
  // A re-login replaces the account it belongs to rather than adding a
81
145
  // second row: the same email is the same account, and the newer
@@ -90,7 +154,7 @@ export class DrizzleOAuthAccountStore implements OAuthAccountStore {
90
154
  // the learned label — but never over an operator's custom rename.
91
155
  // The CASE keeps the read and the write in one statement, so a
92
156
  // concurrent rename cannot lose to the login.
93
- label: sql`CASE WHEN lower(trim(${providerAccounts.label})) = lower(trim(${providerAccounts.providerId})) THEN ${input.label} ELSE ${providerAccounts.label} END`,
157
+ label: sql`CASE WHEN lower(trim(${providerAccounts.label})) = lower(trim(${providerAccounts.providerId})) OR lower(trim(${providerAccounts.label})) = lower(trim(${label})) THEN ${label} ELSE ${providerAccounts.label} END`,
94
158
  credentialCiphertext,
95
159
  credentialKind: "oauth",
96
160
  status: "active",
@@ -106,7 +170,7 @@ export class DrizzleOAuthAccountStore implements OAuthAccountStore {
106
170
  staticToken: false,
107
171
  // Only what this login reported: a flow that learned no profile or
108
172
  // region leaves the stored configuration alone rather than blanking it.
109
- ...(input.auth_state === undefined ? {} : { authState: input.auth_state }),
173
+ ...identityColumns,
110
174
  })
111
175
  .where(eq(providerAccounts.id, existingId));
112
176
  await tx
@@ -119,17 +183,34 @@ export class DrizzleOAuthAccountStore implements OAuthAccountStore {
119
183
  .where(eq(providerOauthStates.providerAccountId, existingId));
120
184
  return { accountId: existingId };
121
185
  }
186
+ const existingLabels = await tx
187
+ .select({ label: providerAccounts.label })
188
+ .from(providerAccounts)
189
+ .where(
190
+ and(
191
+ eq(providerAccounts.providerId, providerId),
192
+ tenantId === null
193
+ ? isNull(providerAccounts.tenantId)
194
+ : eq(providerAccounts.tenantId, tenantId),
195
+ ),
196
+ );
197
+ const generatedLabel = isGeneratedOAuthLabel(label, providerId)
198
+ ? nextGeneratedOAuthAccountLabel(
199
+ providerId,
200
+ existingLabels.map((row) => row.label).filter((value): value is string => typeof value === "string"),
201
+ )
202
+ : label;
122
203
  const rows = await tx
123
204
  .insert(providerAccounts)
124
205
  .values({
125
206
  providerId,
126
207
  tenantId,
127
- label: input.label,
208
+ label: generatedLabel,
128
209
  credentialKind: "oauth",
129
210
  credentialCiphertext,
130
211
  credentialFingerprint,
131
212
  status: "active",
132
- ...(input.auth_state === undefined ? {} : { authState: input.auth_state }),
213
+ ...identityColumns,
133
214
  })
134
215
  .returning({ id: providerAccounts.id });
135
216
  const row = rows[0];
@@ -194,6 +275,8 @@ export interface OAuthLoginConfig {
194
275
  * itself instead of waiting for the operator to paste the URL back.
195
276
  */
196
277
  readonly callbackListener: OAuthCallbackListener;
278
+ /** Resolves device-flow OAuth egress per the `device_flow_proxy` setting. */
279
+ readonly deviceEgress: DeviceEgressDeps;
197
280
  }
198
281
 
199
282
  async function requireClient(
@@ -290,7 +373,7 @@ export async function completeLogin(
290
373
  );
291
374
  }
292
375
  await config.accountStore.persistAccount(flow.tenantId, providerId, {
293
- label: result.accountLabel ?? flow.accountLabel,
376
+ label: result.accountLabel ?? (flow.accountLabel.trim().toLowerCase() === providerId.trim().toLowerCase() ? "" : flow.accountLabel),
294
377
  ...result,
295
378
  });
296
379
  // The exchange succeeded and the account is persisted; now the flow is
@@ -442,7 +525,7 @@ function createOAuthLoginOperations(config: OAuthLoginConfig) {
442
525
  { providerId },
443
526
  );
444
527
  }
445
- const state = generateOAuthState();
528
+ const state = client.generateState?.() ?? generateOAuthState();
446
529
  const { codeVerifier, codeChallenge } = generatePkcePair();
447
530
  /**
448
531
  * A client whose authorize URL depends on an upstream discovery runs it
@@ -547,12 +630,25 @@ function createOAuthLoginOperations(config: OAuthLoginConfig) {
547
630
  `Provider ${providerId} does not support device-code login`,
548
631
  { providerId },
549
632
  );
550
- const started = await client.startDeviceAuth({
633
+ const egress = await resolveDeviceEgress(
634
+ config.deviceEgress,
551
635
  providerId,
552
- tenantId: a.tenantId,
553
- accountLabel,
554
- ...(parameters === undefined ? {} : { parameters }),
555
- });
636
+ a.tenantId,
637
+ );
638
+ let started: OAuthDeviceStartResult;
639
+ try {
640
+ started = await client.startDeviceAuth({
641
+ providerId,
642
+ tenantId: a.tenantId,
643
+ accountLabel,
644
+ fetchFn: egress.fetch,
645
+ ...(parameters === undefined ? {} : { parameters }),
646
+ });
647
+ } finally {
648
+ // The flow has been started; the pool slot is only needed for the
649
+ // start request. Polls re-resolve their own egress.
650
+ egress.release();
651
+ }
556
652
  const { providerState, ...publicStarted } = started;
557
653
  await config.oauthFlowStore.saveDevice(started.deviceAuthId, {
558
654
  providerId,
@@ -585,15 +681,22 @@ function createOAuthLoginOperations(config: OAuthLoginConfig) {
585
681
  409,
586
682
  "Provider does not support device-code login",
587
683
  );
684
+ const egress = await resolveDeviceEgress(
685
+ config.deviceEgress,
686
+ providerId,
687
+ a.tenantId,
688
+ );
588
689
  const polled = await client.pollDeviceAuth(deviceAuthId, {
589
690
  providerId,
590
691
  tenantId: a.tenantId,
591
692
  accountLabel: correlation.accountLabel,
693
+ fetchFn: egress.fetch,
592
694
  ...(correlation.providerState === undefined
593
695
  ? {}
594
696
  : { providerState: correlation.providerState }),
595
697
  ...(correlation.parameters === undefined ? {} : { parameters: correlation.parameters }),
596
698
  });
699
+ egress.release();
597
700
  if (polled.status === "pending")
598
701
  return {
599
702
  status: "pending",
@@ -634,7 +737,11 @@ function createOAuthLoginOperations(config: OAuthLoginConfig) {
634
737
  correlation.tenantId,
635
738
  providerId,
636
739
  {
637
- label: polled.result.accountLabel ?? correlation.accountLabel,
740
+ label:
741
+ polled.result.accountLabel ??
742
+ (correlation.accountLabel.trim().toLowerCase() === providerId.trim().toLowerCase()
743
+ ? ""
744
+ : correlation.accountLabel),
638
745
  ...polled.result,
639
746
  },
640
747
  );
@@ -675,7 +782,7 @@ function createOAuthLoginOperations(config: OAuthLoginConfig) {
675
782
  );
676
783
  }
677
784
  const persisted = await config.accountStore.persistAccount(a.tenantId, providerId, {
678
- label: result.accountLabel ?? accountLabel,
785
+ label: result.accountLabel ?? (accountLabel.trim().toLowerCase() === providerId.trim().toLowerCase() ? "" : accountLabel),
679
786
  ...result,
680
787
  });
681
788
  await config.snapshotInvalidator?.invalidate();
@@ -1,7 +1,7 @@
1
1
  import { t } from "elysia";
2
2
  import type { Elysia } from "elysia";
3
3
  import { and, eq, inArray, isNull, sql } from "drizzle-orm";
4
- import { isSqliteDatabase } from "../../persistence/dialect";
4
+ import { liveModelCooldownExistsSql } from "../../persistence/dialect";
5
5
  import { adminAuditLog, providerAccounts } from "../../persistence/schema";
6
6
  import { requireGlobalAdmin } from "../shared/errors";
7
7
  import {
@@ -23,9 +23,8 @@ import {
23
23
  } from "../../providers/quota/refresh";
24
24
  import { sanitizeGlobalAccount } from "./quota-view";
25
25
  import {
26
- ACCOUNT_STATUSES,
26
+ resolveNextAccountStatus,
27
27
  type AccountQuotaRoutesDeps,
28
- type AccountStatus,
29
28
  } from "./account-quota-shared";
30
29
  import type { ProviderAccountBulkActionResult } from "../providers/catalog/contracts";
31
30
 
@@ -33,16 +32,18 @@ import type { ProviderAccountBulkActionResult } from "../providers/catalog/contr
33
32
  * The same live-cooldown predicate as the tenant-scoped store, evaluated in
34
33
  * SQL so one statement covers the whole shared-account set. `status='cooldown'`
35
34
  * counts even when its deadline elapsed; a live `model_cooldowns` entry counts
36
- * even while `status` is still `active` (D07).
35
+ * even while `status` is still `active` (D07). A row already `disabled` does
36
+ * not: disable keeps the deadline columns as evidence, and counting them would
37
+ * make a second pass look like it still has work. Scoped strictly to shared
38
+ * rows (`tenant_id IS NULL`); the shared JSON subquery lives in the dialect
39
+ * module.
37
40
  */
38
41
  function globalLiveCooldownPredicate(database: unknown): ReturnType<typeof and> {
39
42
  const nowIso = new Date().toISOString();
40
- const liveModelCooldown = isSqliteDatabase(database)
41
- ? sql`EXISTS (SELECT 1 FROM json_each(${providerAccounts.modelCooldowns}) AS entry WHERE json_valid(${providerAccounts.modelCooldowns}) AND json_type(${providerAccounts.modelCooldowns}) = 'object' AND entry.value > ${nowIso})`
42
- : sql`EXISTS (SELECT 1 FROM jsonb_each_text(${providerAccounts.modelCooldowns}) AS entry WHERE jsonb_typeof(${providerAccounts.modelCooldowns}) = 'object' AND entry.value > ${nowIso})`;
43
43
  return and(
44
44
  isNull(providerAccounts.tenantId),
45
- sql`${providerAccounts.status} = 'cooldown' OR ${providerAccounts.cooldownUntil} > ${nowIso} OR ${liveModelCooldown}`,
45
+ sql`${providerAccounts.status} <> 'disabled'`,
46
+ sql`${providerAccounts.status} = 'cooldown' OR ${providerAccounts.cooldownUntil} > ${nowIso} OR ${liveModelCooldownExistsSql(database, nowIso)}`,
46
47
  )!;
47
48
  }
48
49
 
@@ -117,23 +118,12 @@ export function registerAccountQuotaGlobalRoutes(
117
118
  },
118
119
  async ({ request, params, body, set }) => {
119
120
  const access = requireGlobalAdmin(accessResolver(request));
120
- if (body.status !== undefined && !ACCOUNT_STATUSES.includes(body.status as AccountStatus)) {
121
- set.status = 400;
122
- return {
123
- error: `status must be one of: ${ACCOUNT_STATUSES.join(", ")}`,
124
- code: "invalid_status",
125
- };
126
- }
127
- const nextStatus: AccountStatus | undefined =
128
- body.active !== undefined
129
- ? body.active
130
- ? "active"
131
- : "disabled"
132
- : (body.status as AccountStatus | undefined);
133
- if (!nextStatus) {
134
- set.status = 400;
135
- return { error: "active or status is required", code: "invalid_request" };
121
+ const resolved = resolveNextAccountStatus(body);
122
+ if (!resolved.ok) {
123
+ set.status = resolved.status;
124
+ return resolved.body;
136
125
  }
126
+ const nextStatus = resolved.nextStatus;
137
127
  const updated = await db
138
128
  .update(providerAccounts)
139
129
  .set({ status: nextStatus })
@@ -20,6 +20,55 @@ export interface AccountQuotaRoutesDeps extends QuotaRefreshDeps {
20
20
  export const ACCOUNT_STATUSES = ["active", "cooldown", "disabled"] as const;
21
21
  export type AccountStatus = (typeof ACCOUNT_STATUSES)[number];
22
22
 
23
+ /**
24
+ * The merged result of interpreting a PATCH account-status body. On success it
25
+ * carries the resolved `AccountStatus`; on failure it carries the exact 400
26
+ * response the tenant and global routes both return.
27
+ */
28
+ export type ResolveNextAccountStatusResult =
29
+ | { readonly ok: true; readonly nextStatus: AccountStatus }
30
+ | {
31
+ readonly ok: false;
32
+ readonly status: 400;
33
+ readonly body: { readonly error: string; readonly code: string };
34
+ };
35
+
36
+ /**
37
+ * Resolves the next account status from a PATCH body: an explicit `active`
38
+ * boolean wins over `status`, and a missing `status` (or an invalid one) is
39
+ * rejected with the shared 400 shape. Both account PATCH handlers interpret
40
+ * the same input contract, so it lives here once.
41
+ */
42
+ export function resolveNextAccountStatus(body: {
43
+ readonly active?: boolean;
44
+ readonly status?: string;
45
+ }): ResolveNextAccountStatusResult {
46
+ if (body.status !== undefined && !ACCOUNT_STATUSES.includes(body.status as AccountStatus)) {
47
+ return {
48
+ ok: false,
49
+ status: 400,
50
+ body: {
51
+ error: `status must be one of: ${ACCOUNT_STATUSES.join(", ")}`,
52
+ code: "invalid_status",
53
+ },
54
+ };
55
+ }
56
+ const nextStatus: AccountStatus | undefined =
57
+ body.active !== undefined
58
+ ? body.active
59
+ ? "active"
60
+ : "disabled"
61
+ : (body.status as AccountStatus | undefined);
62
+ if (nextStatus === undefined) {
63
+ return {
64
+ ok: false,
65
+ status: 400,
66
+ body: { error: "active or status is required", code: "invalid_request" },
67
+ };
68
+ }
69
+ return { ok: true, nextStatus };
70
+ }
71
+
23
72
  /**
24
73
  * Wraps a stored api_key credential in the envelope its collector expects.
25
74
  *