cartethyia 2.7.2 → 2.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (405) hide show
  1. package/.env.example +2 -0
  2. package/dist/dashboard/assets/{BannedPage-XaUmiBrC.js → BannedPage-ChG5svFb.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
  4. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js +1 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
  6. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js +1 -0
  7. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-CXGS_Ct-.js → CombosPage-DCgm5g6g.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
  10. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +1 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
  12. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +1 -0
  13. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-BKtAPYVF.js → HealthEventsModal-DzSWzE_j.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-BdmRLnq2.js → LoginPage-U6AKaJcF.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-BH8Tehdi.js → ModelPicker-BMS2t8BA.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +5 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
  24. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +1 -0
  25. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
  26. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js +9 -0
  27. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
  28. package/dist/dashboard/assets/QuotaPage-L0skzML8.js +1 -0
  29. package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
  30. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +1 -0
  31. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
  32. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js +1 -0
  33. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
  34. package/dist/dashboard/assets/{SortableList-Dqf4zpS-.js → SortableList-ClzpwyET.js} +1 -1
  35. package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
  36. package/dist/dashboard/assets/{StudioPage-Byj2lAEm.js → StudioPage-Cq5tW7hW.js} +3 -3
  37. package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
  38. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +56 -0
  39. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
  40. package/dist/dashboard/assets/api-keys-C38iXoO4.js +1 -0
  41. package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
  42. package/dist/dashboard/assets/app-a159k0ub.css +2 -0
  43. package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
  44. package/dist/dashboard/assets/arrow-up-59ms1iiT.js +1 -0
  45. package/dist/dashboard/assets/arrow-up-59ms1iiT.js.gz +0 -0
  46. package/dist/dashboard/assets/assert-DbRA8EG2.js +15 -0
  47. package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DC2L43Mj.js → badge-D9CS6dZk.js} +1 -1
  49. package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-BBVcb0J3.js → banner-DympBNq4.js} +1 -1
  51. package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-Cca1XC6G.js → card-ibRWU_uT.js} +1 -1
  53. package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
  54. package/dist/dashboard/assets/cli-tools-63qBGaRw.js +1 -0
  55. package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-DUzEbynq.js → clipboard-button-DJm-9pz7.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-DFvWhwTa.js → dialog-8fiE6tec.js} +1 -1
  59. package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
  60. package/dist/dashboard/assets/drawer-CqGYeOrd.js +1 -0
  61. package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-Bqy8RFvM.js → entry-BSAsYsmV.js} +1 -1
  63. package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
  64. package/dist/dashboard/assets/entry-CiIQULZD.js +4 -0
  65. package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-Daz0lJ-W.js +1 -0
  67. package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
  68. package/dist/dashboard/assets/{concurrency-BDF0ok7y.js → fingerprint-pattern-BWuHtbW4.js} +1 -1
  69. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  70. package/dist/dashboard/assets/{formatters-CWWZ-uCZ.js → formatters-D-MTJxMr.js} +1 -1
  71. package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
  72. package/dist/dashboard/assets/{github-badge-C9e8Ts7e.js → github-badge-CPK47xQx.js} +1 -1
  73. package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
  74. package/dist/dashboard/assets/{index-DFVFrnWW.js → index-DBNPOfhV.js} +2 -2
  75. package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
  76. package/dist/dashboard/assets/{inline-BFUcvllj.js → inline-BT0zntEr.js} +1 -1
  77. package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
  78. package/dist/dashboard/assets/{input-C_ILAvBb.js → input-Da2ZViWg.js} +1 -1
  79. package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
  80. package/dist/dashboard/assets/{layout-CBIyT6Rt.js → layout-BXNeLw6M.js} +1 -1
  81. package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
  82. package/dist/dashboard/assets/{list-row-ebJ_-Nsb.js → list-row-BycOrrIV.js} +1 -1
  83. package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
  84. package/dist/dashboard/assets/live-DCQNZpF7.js +1 -0
  85. package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
  86. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
  87. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  88. package/dist/dashboard/assets/{mutation-sMD965oO.js → mutation-Rr0TIvcj.js} +1 -1
  89. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  90. package/dist/dashboard/assets/network-9Yj2ZyLo.js +1 -0
  91. package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
  92. package/dist/dashboard/assets/{numbers-BO9dKaFr.js → numbers-CEagj6ZZ.js} +2 -2
  93. package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
  94. package/dist/dashboard/assets/{query-keys-CfL_Zdoh.js → query-keys-BloKtaXJ.js} +1 -1
  95. package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
  96. package/dist/dashboard/assets/quota-Caw7wqci.js +1 -0
  97. package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
  98. package/dist/dashboard/assets/routing-CO9zCUus.js +1 -0
  99. package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
  100. package/dist/dashboard/assets/{select-DBNVLdUu.js → select-BIYIBkMU.js} +2 -2
  101. package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
  102. package/dist/dashboard/assets/settings-2-Ds9DQp0t.js +1 -0
  103. package/dist/dashboard/assets/settings-2-Ds9DQp0t.js.gz +0 -0
  104. package/dist/dashboard/assets/settings-jS3-PNSA.js +1 -0
  105. package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
  106. package/dist/dashboard/assets/{stack-EMrOMt4I.js → stack-DfgnFDWY.js} +1 -1
  107. package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
  108. package/dist/dashboard/assets/{state-CWoo7ty5.js → state-CkfdYWel.js} +1 -1
  109. package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
  110. package/dist/dashboard/assets/{switch-CCjT6IkF.js → switch-SYm237HG.js} +1 -1
  111. package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
  112. package/dist/dashboard/assets/{useMutation-DjkLHUcE.js → useMutation-BQqCRNIi.js} +1 -1
  113. package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
  114. package/dist/dashboard/index.html +3 -2
  115. package/dist/dashboard/index.html.gz +0 -0
  116. package/dist/dashboard/precompressed-assets.json +118 -115
  117. package/migrations/full-postgres/0026_add_qoder_native_identity.sql +20 -0
  118. package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +21 -0
  119. package/migrations/full-postgres/0028_add_family_template_key_telemetry.sql +40 -0
  120. package/migrations/full-postgres/0029_null_share_child_policy_columns.sql +38 -0
  121. package/migrations/full-postgres/0030_add_api_key_max_children.sql +10 -0
  122. package/migrations/full-postgres/0031_tenant_system_prompts.sql +16 -0
  123. package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
  124. package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
  125. package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
  126. package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
  127. package/migrations/lite-sqlite3/0026_add_qoder_native_identity.sql +15 -0
  128. package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +21 -0
  129. package/migrations/lite-sqlite3/0028_add_family_template_key_telemetry.sql +40 -0
  130. package/migrations/lite-sqlite3/0029_null_share_child_policy_columns.sql +38 -0
  131. package/migrations/lite-sqlite3/0030_add_api_key_max_children.sql +10 -0
  132. package/migrations/lite-sqlite3/0031_tenant_system_prompts.sql +10 -0
  133. package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
  134. package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
  135. package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
  136. package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
  137. package/package.json +2 -1
  138. package/src/boot/app.ts +9 -8
  139. package/src/config/network.ts +7 -3
  140. package/src/console/api-keys/bucket-spend.ts +7 -7
  141. package/src/console/api-keys/contracts.ts +475 -410
  142. package/src/console/api-keys/routes.ts +211 -23
  143. package/src/console/audit/routes.ts +3 -3
  144. package/src/console/auth/account-reset.ts +4 -4
  145. package/src/console/auth/session.ts +3 -3
  146. package/src/console/backup/contracts.ts +2 -0
  147. package/src/console/backup/export-endpoint.ts +14 -17
  148. package/src/console/backup/routes.ts +43 -11
  149. package/src/console/backup/service.ts +11 -6
  150. package/src/console/backup/store.ts +384 -88
  151. package/src/console/cli-tools/routes.ts +3 -3
  152. package/src/console/console-router-guards.ts +2 -2
  153. package/src/console/console-router.ts +4 -4
  154. package/src/console/dashboard-assets.ts +16 -1
  155. package/src/console/data-management/routes.ts +3 -3
  156. package/src/console/domain-registration.ts +4 -2
  157. package/src/console/observability/contracts.ts +4 -0
  158. package/src/console/observability/live.ts +3 -3
  159. package/src/console/observability/logs.ts +3 -3
  160. package/src/console/observability/performance.ts +3 -3
  161. package/src/console/observability/routes.ts +3 -3
  162. package/src/console/observability/store.ts +16 -16
  163. package/src/console/providers/catalog/contracts.ts +43 -22
  164. package/src/console/providers/catalog/provider-operations.ts +17 -2
  165. package/src/console/providers/catalog/routes.ts +3 -3
  166. package/src/console/providers/catalog/store-helpers.ts +349 -326
  167. package/src/console/providers/catalog/store.ts +1523 -1292
  168. package/src/console/providers/detail/routes.ts +3 -3
  169. package/src/console/providers/oauth/routes.ts +3 -3
  170. package/src/console/quota/account-quota-tenant.ts +34 -2
  171. package/src/console/quota/account-quota.ts +4 -1
  172. package/src/console/quota/account-test-egress.ts +4 -4
  173. package/src/console/quota/native-identity.ts +189 -0
  174. package/src/console/remote-support/contracts.ts +12 -6
  175. package/src/console/remote-support/service.ts +2 -2
  176. package/src/console/routing/model/routes.ts +3 -3
  177. package/src/console/routing/pools/contracts.ts +11 -8
  178. package/src/console/routing/pools/operations.ts +14 -7
  179. package/src/console/routing/pools/probe-verdict.ts +8 -3
  180. package/src/console/routing/pools/relay-deploy.ts +9 -9
  181. package/src/console/routing/pools/routes.ts +3 -3
  182. package/src/console/routing/pools/store.ts +21 -1
  183. package/src/console/settings/routes.ts +3 -3
  184. package/src/console/share/share-router.ts +51 -18
  185. package/src/console/share/share-stats.ts +158 -49
  186. package/src/console/share/share-usage.ts +24 -7
  187. package/src/console/shared/read-through-cache.ts +38 -2
  188. package/src/console/system-prompt/contracts.ts +102 -0
  189. package/src/console/system-prompt/routes.ts +165 -0
  190. package/src/console/updates/routes.ts +7 -4
  191. package/src/main.ts +11 -0
  192. package/src/network/outbound-fetch.ts +75 -4
  193. package/src/network/pool/resolver.ts +39 -39
  194. package/src/network/pool-cooldown-store.ts +71 -16
  195. package/src/network/pool-health-machine.ts +21 -1
  196. package/src/network/rate-limit-verdict.ts +68 -0
  197. package/src/network/ssrf.ts +4 -4
  198. package/src/observability/event-query.ts +437 -22
  199. package/src/observability/telemetry-status.ts +1 -1
  200. package/src/persistence/db-pg.ts +42 -0
  201. package/src/persistence/db-sqlite.ts +7 -2
  202. package/src/persistence/readiness.ts +18 -6
  203. package/src/persistence/schema.ts +162 -1
  204. package/src/persistence/stores/api-key-store.ts +849 -581
  205. package/src/persistence/stores/share-store.ts +65 -13
  206. package/src/persistence/stores/telemetry-store.ts +58 -2
  207. package/src/persistence/system-prompt-store.ts +104 -0
  208. package/src/protocol/codecs.ts +6 -6
  209. package/src/protocol/gateway-error.ts +6 -3
  210. package/src/protocol/image-generation.ts +2 -2
  211. package/src/protocol/reasoning.ts +65 -1
  212. package/src/protocol/request/chat.ts +1 -0
  213. package/src/protocol/response/chat.ts +2 -2
  214. package/src/protocol/response/codex.ts +1 -1
  215. package/src/protocol/response/gemini.ts +1 -1
  216. package/src/protocol/response/messages.ts +5 -5
  217. package/src/protocol/response/responses.ts +2 -2
  218. package/src/protocol/sse-decoder.ts +14 -3
  219. package/src/protocol/translation/capabilities.ts +2 -2
  220. package/src/protocol/upstream/send-claude-messages.ts +3 -3
  221. package/src/providers/adapters/base-adapter.ts +1 -1
  222. package/src/providers/adapters/compatible-adapter.ts +2 -2
  223. package/src/providers/authentication/oauth-refresh-service.ts +2 -2
  224. package/src/providers/configured-provider.ts +1 -1
  225. package/src/providers/discovery/probe-phases.ts +2 -2
  226. package/src/providers/discovery/probing-service.ts +12 -12
  227. package/src/providers/integrations/agentrouter/agentrouter.ts +2 -3
  228. package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
  229. package/src/providers/integrations/anthropic.ts +4 -11
  230. package/src/providers/integrations/antigravity/antigravity.ts +33 -47
  231. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
  232. package/src/providers/integrations/buddy/buddy-chat-shared.ts +30 -2
  233. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
  234. package/src/providers/integrations/buddy/buddy-identity.ts +22 -1
  235. package/src/providers/integrations/buddy/buddy-media.ts +21 -17
  236. package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
  237. package/src/providers/integrations/buddy/workbuddy/provider.ts +6 -2
  238. package/src/providers/integrations/claude/claude-betas.ts +1 -1
  239. package/src/providers/integrations/claude/claude-credentials.ts +1 -1
  240. package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
  241. package/src/providers/integrations/claude/claude.ts +3 -10
  242. package/src/providers/integrations/cline/cline.ts +8 -1
  243. package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
  244. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  245. package/src/providers/integrations/codex/codex.ts +14 -15
  246. package/src/providers/integrations/commandcode/commandcode.ts +4 -4
  247. package/src/providers/integrations/commandcode/quota.ts +5 -1
  248. package/src/providers/integrations/cursor/cursor-http2.ts +11 -11
  249. package/src/providers/integrations/cursor/cursor.ts +15 -15
  250. package/src/providers/integrations/factory/factory.ts +4 -4
  251. package/src/providers/integrations/gemini.ts +11 -11
  252. package/src/providers/integrations/github/github.ts +26 -7
  253. package/src/providers/integrations/grok/grok.ts +2 -6
  254. package/src/providers/integrations/hermes.ts +5 -1
  255. package/src/providers/integrations/inferhub.ts +4 -11
  256. package/src/providers/integrations/kilo/kilo.ts +4 -0
  257. package/src/providers/integrations/kimi/kimi.ts +4 -11
  258. package/src/providers/integrations/kiro/kiro.ts +10 -10
  259. package/src/providers/integrations/muse/muse.ts +6 -9
  260. package/src/providers/integrations/openrouter/provider.ts +4 -0
  261. package/src/providers/integrations/perplexity.ts +2 -2
  262. package/src/providers/integrations/qoder/qoder-catalog.ts +47 -9
  263. package/src/providers/integrations/qoder/qoder-checkin.ts +242 -51
  264. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +157 -0
  265. package/src/providers/integrations/qoder/qoder-oauth.ts +12 -3
  266. package/src/providers/integrations/qoder/qoder-realm.ts +15 -0
  267. package/src/providers/integrations/qoder/qoder-umid.ts +287 -0
  268. package/src/providers/integrations/qoder/qoder-virtual.ts +2 -2
  269. package/src/providers/integrations/qoder/qoder.ts +192 -24
  270. package/src/providers/integrations/tokenharbor.ts +2 -2
  271. package/src/providers/integrations/trae/trae.ts +6 -9
  272. package/src/providers/integrations/zcode/oauth.ts +1 -1
  273. package/src/providers/operations/account-error-classification.ts +6 -8
  274. package/src/providers/operations/account-health-service.ts +14 -16
  275. package/src/providers/operations/client-versions.ts +37 -22
  276. package/src/providers/operations/provider-catalog-service.ts +4 -4
  277. package/src/providers/operations/provider-credential-service.ts +16 -4
  278. package/src/providers/operations/upstream-deadline.ts +3 -3
  279. package/src/providers/provider-contracts.ts +55 -3
  280. package/src/providers/provider-credential-resolver.ts +1 -1
  281. package/src/providers/provider-registry.ts +4 -4
  282. package/src/providers/quota/quota-contracts.ts +24 -10
  283. package/src/providers/quota/refresh.ts +2 -2
  284. package/src/providers/search/search-provider.ts +5 -5
  285. package/src/security/abuse.ts +4 -4
  286. package/src/security/access-control.ts +19 -0
  287. package/src/security/admission/reasons.ts +17 -17
  288. package/src/security/api-key-auth.ts +18 -5
  289. package/src/shared/coerce.ts +19 -0
  290. package/src/transport/dispatch/attempt-finalize.ts +4 -3
  291. package/src/transport/dispatch/attempt-loop.ts +10 -7
  292. package/src/transport/dispatch/empty-stream.ts +6 -6
  293. package/src/transport/dispatch/generation.ts +9 -9
  294. package/src/transport/dispatch/leases.ts +8 -8
  295. package/src/transport/dispatch/proxy-request.ts +38 -9
  296. package/src/transport/dispatch/responses-compact.ts +11 -11
  297. package/src/transport/dispatch/retry-policy.ts +4 -67
  298. package/src/transport/dispatch/stream-bridge.ts +1 -1
  299. package/src/transport/dispatch/streaming/finalize.ts +9 -8
  300. package/src/transport/dispatch/streaming/stream.ts +16 -1
  301. package/src/transport/dispatch/systemone.ts +8 -8
  302. package/src/transport/dispatch/token-count.ts +9 -9
  303. package/src/transport/dispatch/upstream.ts +10 -0
  304. package/src/transport/dispatch/websearch.ts +6 -6
  305. package/src/transport/drain-endpoint.ts +2 -2
  306. package/src/transport/failure-policy.ts +13 -6
  307. package/src/transport/guards/auth-guard.ts +6 -5
  308. package/src/transport/guards/body-policy.ts +4 -4
  309. package/src/transport/guards/error-lifecycle.ts +18 -3
  310. package/src/transport/guards/guard-errors.ts +7 -3
  311. package/src/transport/guards/headroom-guard.ts +1 -1
  312. package/src/transport/guards/pipeline.ts +6 -6
  313. package/src/transport/guards/readiness-guard.ts +16 -2
  314. package/src/transport/guards/request-context.ts +3 -3
  315. package/src/transport/request/preparer.ts +37 -21
  316. package/src/transport/request/state.ts +4 -7
  317. package/src/transport/request/system-prompt-injection.ts +89 -0
  318. package/src/transport/request/tenant-preferences.ts +43 -0
  319. package/src/transport/routing/admission.ts +13 -3
  320. package/src/transport/routing/route-catalog.ts +104 -2
  321. package/src/transport/routing/route-model.ts +60 -7
  322. package/src/transport/routing/router.ts +306 -256
  323. package/src/transport/shutdown-notice.ts +19 -7
  324. package/src/transport/surface/completion.ts +1 -1
  325. package/src/transport/surface/messages/encode.ts +1 -1
  326. package/src/workers/daily-checkin.ts +2 -0
  327. package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
  328. package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js +0 -1
  329. package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
  330. package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js +0 -1
  331. package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
  332. package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
  333. package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js +0 -1
  334. package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
  335. package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js +0 -1
  336. package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
  337. package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
  338. package/dist/dashboard/assets/LoginPage-BdmRLnq2.js.gz +0 -0
  339. package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
  340. package/dist/dashboard/assets/OverviewPage-CqMxibJB.js +0 -1
  341. package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
  342. package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +0 -4
  343. package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js.gz +0 -0
  344. package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js +0 -1
  345. package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
  346. package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +0 -9
  347. package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
  348. package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js +0 -1
  349. package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
  350. package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js +0 -1
  351. package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
  352. package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +0 -1
  353. package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
  354. package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
  355. package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
  356. package/dist/dashboard/assets/UsagePage-CTkLAuRc.js +0 -56
  357. package/dist/dashboard/assets/UsagePage-CTkLAuRc.js.gz +0 -0
  358. package/dist/dashboard/assets/api-keys-CUi6UXsR.js +0 -1
  359. package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
  360. package/dist/dashboard/assets/app-KvsUJUMF.css +0 -2
  361. package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
  362. package/dist/dashboard/assets/assert-67xXH5rf.js +0 -1
  363. package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
  364. package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
  365. package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
  366. package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
  367. package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js +0 -1
  368. package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
  369. package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
  370. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  371. package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
  372. package/dist/dashboard/assets/drawer-CFM2sMX1.js +0 -1
  373. package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
  374. package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
  375. package/dist/dashboard/assets/entry-ClCMCWWQ.js +0 -4
  376. package/dist/dashboard/assets/entry-ClCMCWWQ.js.gz +0 -0
  377. package/dist/dashboard/assets/entry-_xHo5x6r.js +0 -1
  378. package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
  379. package/dist/dashboard/assets/formatters-CWWZ-uCZ.js.gz +0 -0
  380. package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
  381. package/dist/dashboard/assets/index-DFVFrnWW.js.gz +0 -0
  382. package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
  383. package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
  384. package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
  385. package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
  386. package/dist/dashboard/assets/live-D0mm--ME.js +0 -1
  387. package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
  388. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
  389. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
  390. package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
  391. package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
  392. package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
  393. package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
  394. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  395. package/dist/dashboard/assets/routing-BnIpi1IJ.js +0 -1
  396. package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
  397. package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
  398. package/dist/dashboard/assets/settings-BAFhDgX8.js +0 -1
  399. package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
  400. package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
  401. package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
  402. package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
  403. package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
  404. /package/dist/dashboard/assets/{app-BaWd0puM.js → app-BayTTy01.js} +0 -0
  405. /package/dist/dashboard/assets/{app-BaWd0puM.js.gz → app-BayTTy01.js.gz} +0 -0
@@ -1,4 +1,4 @@
1
- import { Elysia, t } from "elysia";
1
+ import { Elysia, t, type AnyElysia } from "elysia";
2
2
  import { ConsoleDomainError, errorResponse, requireTenantScope } from "../../shared/errors";
3
3
  import { literalUnion } from "../../shared/elysia-schema";
4
4
  import type { AccessDecision } from "../../../security/access-control";
@@ -160,7 +160,7 @@ const updateRoutingBody = t.Object({
160
160
  });
161
161
 
162
162
 
163
- export function createProviderDetailRoutes(config: ProviderDetailConfig): Elysia {
163
+ export function createProviderDetailRoutes(config: ProviderDetailConfig): AnyElysia {
164
164
  const factory = createProviderDetailOperations(config);
165
165
  return new Elysia({ prefix: "/providers" })
166
166
  .get("/:providerId/routing", async ({ request, params, set }) => {
@@ -191,5 +191,5 @@ export function createProviderDetailRoutes(config: ProviderDetailConfig): Elysia
191
191
  return providerDetailErrorResponse(e, set);
192
192
  }
193
193
  },
194
- ) as unknown as Elysia;
194
+ )
195
195
  }
@@ -9,7 +9,7 @@ import type { ProviderRegistry } from "../../../providers/provider-registry";
9
9
  import { browserAuthorizeRedirectUri } from "../../../config/providers";
10
10
  import { log } from "../../../observability/logger";
11
11
  import { GatewayError } from "../../../protocol/gateway-error";
12
- import { Elysia, t } from "elysia";
12
+ import { Elysia, t, type AnyElysia } from "elysia";
13
13
  import { ConsoleDomainError, errorResponse, requireScope } from "../../shared/errors";
14
14
  import { generateOAuthState, generatePkcePair } from "../../../providers/authentication/oauth-flow-store";
15
15
  import type { OAuthFlowStorage } from "../../../providers/authentication/oauth-flow-store";
@@ -639,7 +639,7 @@ const importBody = t.Object({
639
639
  accountLabel: t.Optional(t.String()),
640
640
  });
641
641
 
642
- export function createOAuthLoginRoutes(config: OAuthLoginConfig): Elysia {
642
+ export function createOAuthLoginRoutes(config: OAuthLoginConfig): AnyElysia {
643
643
  const factory = createOAuthLoginOperations(config);
644
644
  return new Elysia({ prefix: "/providers" })
645
645
  .post(
@@ -721,6 +721,6 @@ export function createOAuthLoginRoutes(config: OAuthLoginConfig): Elysia {
721
721
  return oauthErrorResponse(e, set);
722
722
  }
723
723
  },
724
- ) as unknown as Elysia;
724
+ )
725
725
  }
726
726
 
@@ -43,6 +43,10 @@ import {
43
43
  fetchQoderDailyCheckin,
44
44
  isQoderCheckinProvider,
45
45
  } from "../../providers/integrations/qoder/qoder-checkin";
46
+ import {
47
+ DrizzleQoderNativeIdentityStore,
48
+ type QoderNativeIdentityStore,
49
+ } from "../../providers/integrations/qoder/qoder-native-identity-store";
46
50
  import { buddyAccountUid } from "../../providers/integrations/buddy/buddy-oauth-shared";
47
51
  import {
48
52
  supportsAccountReset,
@@ -50,6 +54,7 @@ import {
50
54
  listAccountResetCredits,
51
55
  } from "../../providers/operations/account-reset-service";
52
56
  import {
57
+ enqueueBackgroundQuotaRefresh,
53
58
  pendingQuotaRefreshes,
54
59
  toQuotaAccountHealth,
55
60
  toQuotaView,
@@ -108,6 +113,7 @@ async function runAccountCheckin(
108
113
  account: { id: string; providerId: string },
109
114
  credential: string,
110
115
  forceClaim: boolean,
116
+ nativeIdentityStore?: QoderNativeIdentityStore,
111
117
  ): Promise<DailyCheckinResult> {
112
118
  if (isQoderCheckinProvider(account.providerId)) {
113
119
  return fetchQoderDailyCheckin({
@@ -115,6 +121,7 @@ async function runAccountCheckin(
115
121
  accountId: account.id,
116
122
  credential,
117
123
  fetcher: globalThis.fetch,
124
+ ...(nativeIdentityStore ? { nativeIdentityStore } : {}),
118
125
  });
119
126
  }
120
127
  return fetchDailyCheckin({
@@ -202,6 +209,7 @@ async function runDailyGrowthForAccount(
202
209
  accountId: account.id,
203
210
  credential,
204
211
  fetcher,
212
+ nativeIdentityStore: new DrizzleQoderNativeIdentityStore(refreshDeps.db),
205
213
  })
206
214
  : await fetchDailyCheckin({
207
215
  providerId,
@@ -362,6 +370,15 @@ export function registerAccountQuotaTenantRoutes(
362
370
  icon: providerId,
363
371
  }));
364
372
  let refreshing = 0;
373
+ // Cold-cache backfill: the overview renders whatever is cached, and the
374
+ // periodic sweep owns freshness — but with many accounts (e.g. a large
375
+ // Qoder roster) the sweep's per-pass cap cannot keep every cache warm,
376
+ // so a page open would show accounts with no badge for minutes. Enqueue
377
+ // a bounded background refresh for accounts whose cache entry is missing
378
+ // (not merely stale), so the badge fills in without waiting for the next
379
+ // sweep pass and without fanning out to the whole roster at once.
380
+ const coldCacheCap = 20;
381
+ let coldCacheEnqueued = 0;
365
382
  // The sweep's day markers answer "attempted today" without an upstream
366
383
  // request; only check-in-capable accounts can carry one, so only they
367
384
  // are read.
@@ -397,6 +414,21 @@ export function registerAccountQuotaTenantRoutes(
397
414
  const canRefresh = providerRegistry.hasQuotaCollector(row.providerId);
398
415
  const isRefreshing = canRefresh && (pending.has(row.id) || entry === undefined);
399
416
  if (isRefreshing) refreshing += 1;
417
+ // Backfill a missing cache entry on page open (bounded, see above).
418
+ if (
419
+ canRefresh &&
420
+ entry === undefined &&
421
+ !pending.has(row.id) &&
422
+ coldCacheEnqueued < coldCacheCap
423
+ ) {
424
+ coldCacheEnqueued += 1;
425
+ enqueueBackgroundQuotaRefresh(refreshDeps, {
426
+ accountId: row.id,
427
+ providerId: row.providerId,
428
+ tenantId: row.tenantId,
429
+ credentialKind: row.credentialKind,
430
+ });
431
+ }
400
432
  return {
401
433
  id: row.id,
402
434
  provider: row.providerId,
@@ -518,7 +550,7 @@ export function registerAccountQuotaTenantRoutes(
518
550
  throw new GatewayError(
519
551
  "proxy_pool_unavailable",
520
552
  503,
521
- "Account test cannot select a proxy because proxy routing is not configured.",
553
+ "Account test cannot select a proxy because proxy routing is not configured (request {requestId}). No bridge to cross — configure a pool first 🏗️",
522
554
  { reason: "proxy_routing_unconfigured" },
523
555
  "network",
524
556
  );
@@ -884,7 +916,7 @@ export function registerAccountQuotaTenantRoutes(
884
916
  account.providerId as ProviderId,
885
917
  account.id,
886
918
  );
887
- result = await runAccountCheckin(account, credential, true);
919
+ result = await runAccountCheckin(account, credential, true, new DrizzleQoderNativeIdentityStore(db));
888
920
  } catch (error) {
889
921
  result = {
890
922
  providerId: account.providerId,
@@ -2,13 +2,15 @@ import { Elysia } from "elysia";
2
2
  import { consoleErrorHandler } from "../shared/errors";
3
3
  import { registerAccountQuotaGlobalRoutes } from "./account-quota-global";
4
4
  import { registerAccountQuotaTenantRoutes } from "./account-quota-tenant";
5
+ import { registerNativeIdentityRoutes } from "./native-identity";
5
6
  import { createQuotaRefreshDeps, type AccountQuotaRoutesDeps } from "./account-quota-shared";
6
7
 
7
8
  /**
8
9
  * Mounts the account/quota console routes: the tenant-scoped group (the
9
10
  * caller's own accounts) and the platform-admin group (shared accounts). Each
10
11
  * group lives in its own module and authorises every route with its own
11
- * access check.
12
+ * access check. The native-identity routes are mounted once, here, because
13
+ * they serve both owned and shared accounts with per-account authorization.
12
14
  */
13
15
  export function createAccountQuotaRoutes(deps: AccountQuotaRoutesDeps): Elysia {
14
16
  const refreshDeps = createQuotaRefreshDeps(deps);
@@ -18,5 +20,6 @@ export function createAccountQuotaRoutes(deps: AccountQuotaRoutesDeps): Elysia {
18
20
  app.error(consoleErrorHandler("Quota operation failed"));
19
21
  registerAccountQuotaTenantRoutes(app, deps, refreshDeps);
20
22
  registerAccountQuotaGlobalRoutes(app, deps, refreshDeps);
23
+ registerNativeIdentityRoutes(app, { db: deps.db, accessResolver: deps.accessResolver });
21
24
  return app;
22
25
  }
@@ -138,7 +138,7 @@ export async function selectAccountTestEgress(
138
138
  throw new GatewayError(
139
139
  "proxy_pool_unavailable",
140
140
  503,
141
- "No active proxy pool is available because coordination is unavailable.",
141
+ "No active proxy pool is available because coordination is unavailable (request {requestId}). The bouncer stepped out — come back shortly 🚪",
142
142
  { reason: "coordination_unavailable" },
143
143
  "network",
144
144
  );
@@ -159,10 +159,10 @@ export async function selectAccountTestEgress(
159
159
  const status = failure.reason === "at_capacity" ? 429 : 503;
160
160
  const message =
161
161
  failure.reason === "at_capacity"
162
- ? "Every proxy pool for this provider is at its concurrency limit. Retry shortly, or add a pool or raise its capacity."
162
+ ? "Every proxy pool for this provider is at its concurrency limit (request {requestId}). Retry shortly, or add a pool or raise its capacity. The pools are full right now — chill 🌊"
163
163
  : failure.reason === "cooldown"
164
- ? "Every proxy pool for this provider is cooling down after upstream failures. Retry once the cooldown resets."
165
- : "No active proxy pool is configured for this tenant and provider. Add one, or route this provider through direct egress.";
164
+ ? "Every proxy pool for this provider is cooling down after upstream failures (request {requestId}). Retry once the cooldown resets. The proxies are catching their breath — zzz 💤"
165
+ : "No active proxy pool is configured for this tenant and provider (request {requestId}). Add one, or route this provider through direct egress. No bridge to cross — configure a pool first 🏗️";
166
166
  throw new GatewayError(
167
167
  code,
168
168
  status,
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Qoder native-identity console operations, shared by the tenant-scoped and
3
+ * global-admin account route groups.
4
+ *
5
+ * The identity is the account's device identity (minted seed). The routes
6
+ * surface it for export/import/regenerate from the provider-detail screen;
7
+ * the device-uniqueness invariant lives in the store (and the partial unique
8
+ * index behind it).
9
+ */
10
+ import { and, eq, isNull } from "drizzle-orm";
11
+ import { Elysia, t } from "elysia";
12
+ import type { CartethyiaDatabase } from "../../persistence/db-handle";
13
+ import { providerAccounts, type NativeIdentityRecord } from "../../persistence/schema";
14
+ import {
15
+ allocateQoderNativeIdentity,
16
+ DrizzleQoderNativeIdentityStore,
17
+ type QoderNativeIdentityStore,
18
+ } from "../../providers/integrations/qoder/qoder-native-identity-store";
19
+ import { ConsoleDomainError, requireGlobalAdmin, requireTenantScope } from "../shared/errors";
20
+ import type { ConsoleAccessResolver } from "../auth/access";
21
+
22
+ /** True when `token` looks like a plausible machine token. */
23
+ export function isValidNativeIdentityToken(token: string): boolean {
24
+ return typeof token === "string" && token.length >= 8 && token.length <= 512;
25
+ }
26
+
27
+ /**
28
+ * Allocates a fresh device identity for regenerate. Injected so tests can stub
29
+ * the native bridge; the default runs the real `runtime-info` bridge through
30
+ * the store.
31
+ */
32
+ export type NativeIdentityAllocator = (args: {
33
+ providerId: string;
34
+ accountId: string;
35
+ avoidToken?: string;
36
+ }) => Promise<NativeIdentityRecord | null>;
37
+
38
+ function defaultAllocator(store: QoderNativeIdentityStore): NativeIdentityAllocator {
39
+ return ({ providerId, accountId, avoidToken }) =>
40
+ allocateQoderNativeIdentity(store, providerId, accountId, {
41
+ ...(avoidToken !== undefined ? { avoidToken } : {}),
42
+ });
43
+ }
44
+
45
+ /** Loads one account row, scoped by tenant (null = global/shared). */
46
+ async function loadAccount(
47
+ db: CartethyiaDatabase,
48
+ accountId: string,
49
+ tenantId: string | null,
50
+ ): Promise<{ providerId: string; nativeIdentity: NativeIdentityRecord | null } | undefined> {
51
+ const rows = await db
52
+ .select({
53
+ providerId: providerAccounts.providerId,
54
+ nativeIdentity: providerAccounts.nativeIdentity,
55
+ })
56
+ .from(providerAccounts)
57
+ .where(
58
+ tenantId === null
59
+ ? and(eq(providerAccounts.id, accountId), isNull(providerAccounts.tenantId))
60
+ : and(eq(providerAccounts.id, accountId), eq(providerAccounts.tenantId, tenantId)),
61
+ )
62
+ .limit(1);
63
+ return rows[0];
64
+ }
65
+
66
+ export interface NativeIdentityOps {
67
+ get(accountId: string, tenantId: string | null): Promise<NativeIdentityRecord | null>;
68
+ put(accountId: string, tenantId: string | null, record: NativeIdentityRecord): Promise<NativeIdentityRecord>;
69
+ regenerate(accountId: string, tenantId: string | null): Promise<NativeIdentityRecord | null>;
70
+ }
71
+
72
+ export function createNativeIdentityOps(
73
+ db: CartethyiaDatabase,
74
+ allocator: NativeIdentityAllocator = defaultAllocator(new DrizzleQoderNativeIdentityStore(db)),
75
+ ): NativeIdentityOps {
76
+ const store = new DrizzleQoderNativeIdentityStore(db);
77
+ return {
78
+ async get(accountId, tenantId) {
79
+ const account = await loadAccount(db, accountId, tenantId);
80
+ // A missing row under the narrowed tenant scope means the account is not
81
+ // visible to this caller (cross-tenant read) — report not-found, the
82
+ // same convention the account routes use, rather than echoing null.
83
+ if (!account) throw new ConsoleDomainError("not_found", 404, "Account not found");
84
+ return account.nativeIdentity;
85
+ },
86
+ async put(accountId, tenantId, record) {
87
+ const account = await loadAccount(db, accountId, tenantId);
88
+ if (!account) throw new ConsoleDomainError("not_found", 404, "Account not found");
89
+ if (!isValidNativeIdentityToken(record.machineToken)) {
90
+ throw new ConsoleDomainError("invalid_identity", 400, "machineToken must be a non-empty string (8–512 chars)");
91
+ }
92
+ if (!record.machineType || !record.machineCode) {
93
+ throw new ConsoleDomainError("invalid_identity", 400, "machineToken, machineType, and machineCode are required");
94
+ }
95
+ // Device uniqueness: another account already owns this token? The pool is
96
+ // global across Qoder accounts (both realms), matching the unique index.
97
+ const occupied = await store.occupied(accountId);
98
+ if (occupied.has(record.machineToken)) {
99
+ throw new ConsoleDomainError("slot_taken", 409, `Native identity token is already bound to another account`);
100
+ }
101
+ await store.bind(account.providerId, accountId, record);
102
+ return record;
103
+ },
104
+ async regenerate(accountId, tenantId) {
105
+ const account = await loadAccount(db, accountId, tenantId);
106
+ if (!account) throw new ConsoleDomainError("not_found", 404, "Account not found");
107
+ return allocator({
108
+ providerId: account.providerId,
109
+ accountId,
110
+ ...(account.nativeIdentity?.machineToken !== undefined ? { avoidToken: account.nativeIdentity.machineToken } : {}),
111
+ });
112
+ },
113
+ };
114
+ }
115
+
116
+ /**
117
+ * The authorization decision for one account's native-identity route.
118
+ *
119
+ * Account ownership decides which rule applies: shared accounts
120
+ * (`tenant_id IS NULL`) need platform admin; tenant-owned accounts need the
121
+ * tenant scope for the owning tenant.
122
+ */
123
+ export interface NativeIdentityScope {
124
+ readonly tenantId: string | null;
125
+ }
126
+
127
+ /**
128
+ * Registers the native-identity routes on `app` (path
129
+ * `/provider-accounts/:id/native-identity`).
130
+ *
131
+ * `authorize` performs the access check based on the account's owner and
132
+ * returns the tenant scope to load the row under, so a shared account is
133
+ * reachable only by a platform admin and a tenant-owned account only by its
134
+ * tenant.
135
+ */
136
+ export function registerNativeIdentityRoutes(
137
+ app: Elysia,
138
+ deps: { readonly db: CartethyiaDatabase; readonly accessResolver: ConsoleAccessResolver },
139
+ allocator?: NativeIdentityAllocator,
140
+ ): void {
141
+ const ops = createNativeIdentityOps(deps.db, allocator);
142
+ const authorize = async (
143
+ request: Request,
144
+ accountId: string,
145
+ scope: "dashboard:read" | "dashboard:write",
146
+ ): Promise<NativeIdentityScope> => {
147
+ const rows = await deps.db
148
+ .select({ tenantId: providerAccounts.tenantId })
149
+ .from(providerAccounts)
150
+ .where(eq(providerAccounts.id, accountId))
151
+ .limit(1);
152
+ if (rows.length === 0) {
153
+ throw new ConsoleDomainError("not_found", 404, "Account not found");
154
+ }
155
+ const ownerTenantId = rows[0]!.tenantId;
156
+ if (ownerTenantId === null) {
157
+ requireGlobalAdmin(deps.accessResolver(request));
158
+ return { tenantId: null };
159
+ }
160
+ const access = requireTenantScope(deps.accessResolver(request), scope);
161
+ return { tenantId: access.tenantId };
162
+ };
163
+ app
164
+ .get("/provider-accounts/:id/native-identity", async ({ request, params }) => {
165
+ const { tenantId } = await authorize(request, params.id, "dashboard:read");
166
+ return { identity: await ops.get(params.id, tenantId) };
167
+ })
168
+ .put(
169
+ "/provider-accounts/:id/native-identity",
170
+ {
171
+ body: t.Object({
172
+ machineToken: t.String(),
173
+ machineType: t.String(),
174
+ machineCode: t.String(),
175
+ seed: t.Optional(t.String()),
176
+ }),
177
+ },
178
+ async ({ request, params, body }) => {
179
+ const { tenantId } = await authorize(request, params.id, "dashboard:write");
180
+ const identity = await ops.put(params.id, tenantId, body as NativeIdentityRecord);
181
+ return { identity };
182
+ },
183
+ )
184
+ .post("/provider-accounts/:id/native-identity/regenerate", async ({ request, params }) => {
185
+ const { tenantId } = await authorize(request, params.id, "dashboard:write");
186
+ const identity = await ops.regenerate(params.id, tenantId);
187
+ return { identity };
188
+ });
189
+ }
@@ -51,12 +51,18 @@ export interface RemoteSupportStartResponse extends RemoteSupportStatus {
51
51
  }
52
52
 
53
53
  /** Human-readable install guidance, keyed by tool. Never an installer. */
54
- export const REMOTE_SUPPORT_INSTALL_HINT: Readonly<Record<RemoteSupportTool, string>> = {
55
- cloudflared:
56
- "cloudflared is not installed. Install it yourself (winget install Cloudflare.cloudflared) and try again.",
57
- tailscale:
58
- "tailscale is not installed. Install it yourself (winget install Tailscale.Tailscale), run `tailscale up` once, then try again.",
59
- };
54
+ export function remoteSupportInstallHint(tool: RemoteSupportTool): string {
55
+ // Cross-OS install hint: pick the host's package manager. A Windows-only
56
+ // hint on Linux/macOS would be nonsense (and vice versa).
57
+ const win = process.platform === "win32";
58
+ const mac = process.platform === "darwin";
59
+ const installCommand = (pkg: string): string =>
60
+ win ? `winget install ${pkg}` : mac ? `brew install ${pkg}` : `sudo apt-get install -y ${pkg}`;
61
+ if (tool === "cloudflared") {
62
+ return `cloudflared is not installed. Install it yourself (${installCommand("cloudflared")}) and try again.`;
63
+ }
64
+ return `tailscale is not installed. Install it yourself (${installCommand("tailscale")}), run \`tailscale up\` once, then try again.`;
65
+ }
60
66
 
61
67
  export function isRemoteSupportTool(value: unknown): value is RemoteSupportTool {
62
68
  return typeof value === "string" && (REMOTE_SUPPORT_TOOLS as readonly string[]).includes(value);
@@ -18,7 +18,7 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, write
18
18
  import { join } from "node:path";
19
19
  import { resolveUserConfigDir } from "../../config/package-paths";
20
20
  import {
21
- REMOTE_SUPPORT_INSTALL_HINT,
21
+ remoteSupportInstallHint,
22
22
  REMOTE_SUPPORT_TOOLS,
23
23
  type RemoteSupportBlocker,
24
24
  type RemoteSupportStatus,
@@ -430,7 +430,7 @@ export function createRemoteSupportService(config: RemoteSupportServiceConfig):
430
430
  active: null,
431
431
  url: null,
432
432
  blocker: "not_installed",
433
- message: REMOTE_SUPPORT_INSTALL_HINT[tool],
433
+ message: remoteSupportInstallHint(tool),
434
434
  port,
435
435
  installed: await detectInstalled(),
436
436
  };
@@ -1,5 +1,5 @@
1
1
  // Model routing control-plane HTTP routes. The only module that touches Elysia.
2
- import { Elysia, t } from "elysia";
2
+ import { Elysia, t, type AnyElysia } from "elysia";
3
3
  import { errorResponse } from "../../shared/errors";
4
4
  import { literalUnion } from "../../shared/elysia-schema";
5
5
  import { modelComboStrategy, type ComboStrategy } from "../../../persistence/schema";
@@ -41,7 +41,7 @@ const updateComboBody = t.Object({
41
41
  members: t.Optional(t.Array(t.String(), { minItems: 1 })),
42
42
  strategy: t.Optional(comboStrategySchema),
43
43
  });
44
- export function createModelRoutingRoutes(config: ModelRoutingConfig): Elysia {
44
+ export function createModelRoutingRoutes(config: ModelRoutingConfig): AnyElysia {
45
45
  const factory = createModelRoutingOperations(config);
46
46
  return new Elysia({ prefix: "/routing" })
47
47
  .get("/aliases", async ({ request, set }) => {
@@ -132,5 +132,5 @@ export function createModelRoutingRoutes(config: ModelRoutingConfig): Elysia {
132
132
  } catch (e) {
133
133
  return modelRoutingErrorResponse(e, set);
134
134
  }
135
- }) as unknown as Elysia;
135
+ });
136
136
  }
@@ -188,7 +188,7 @@ export function validateTransportConfig(
188
188
  case "http":
189
189
  case "https":
190
190
  if (cfg.timeout !== undefined && typeof cfg.timeout !== "number")
191
- throw new ConsoleDomainError("invalid_config", 400, "HTTP timeout must be a number");
191
+ throw new ConsoleDomainError("invalid_pool", 400, "HTTP timeout must be a number", { field: "timeout" });
192
192
  return;
193
193
  case "socks5":
194
194
  return;
@@ -196,9 +196,10 @@ export function validateTransportConfig(
196
196
  // Compile-time exhaustiveness: new TransportKind variants must add a case.
197
197
  kind satisfies never;
198
198
  throw new ConsoleDomainError(
199
- "invalid_config",
199
+ "invalid_pool",
200
200
  400,
201
201
  `Unsupported transport kind: ${String(kind)}`,
202
+ { field: "kind" },
202
203
  );
203
204
  }
204
205
  }
@@ -494,10 +495,10 @@ export function validateEndpoint(
494
495
  try {
495
496
  u = new URL(candidate);
496
497
  } catch {
497
- throw new ConsoleDomainError("invalid_endpoint", 400, "Endpoint must be host:port");
498
+ throw new ConsoleDomainError("invalid_pool", 400, "Endpoint must be host:port", { field: "endpoint" });
498
499
  }
499
500
  if (!u.hostname || !u.port)
500
- throw new ConsoleDomainError("invalid_endpoint", 400, "Endpoint must be host:port");
501
+ throw new ConsoleDomainError("invalid_pool", 400, "Endpoint must be host:port", { field: "endpoint" });
501
502
  if (u.hostname === "localhost" || u.hostname.endsWith(".internal"))
502
503
  throw new ConsoleDomainError("ssrf_rejected", 400, "Private endpoint rejected");
503
504
  if (isIP(u.hostname) !== 0 && !isAddressAllowed(u.hostname, policy))
@@ -508,10 +509,10 @@ export function validateEndpoint(
508
509
  try {
509
510
  u = new URL(endpoint);
510
511
  } catch {
511
- throw new ConsoleDomainError("invalid_endpoint", 400, "Endpoint must be a valid http(s) URL");
512
+ throw new ConsoleDomainError("invalid_pool", 400, "Endpoint must be a valid http(s) URL", { field: "endpoint" });
512
513
  }
513
514
  if (!["http:", "https:"].includes(u.protocol)) {
514
- throw new ConsoleDomainError("invalid_endpoint", 400, "Endpoint must be a valid http(s) URL");
515
+ throw new ConsoleDomainError("invalid_pool", 400, "Endpoint must be a valid http(s) URL", { field: "endpoint" });
515
516
  }
516
517
  if (u.hostname === "localhost" || u.hostname.endsWith(".internal")) {
517
518
  throw new ConsoleDomainError("ssrf_rejected", 400, "Private endpoint rejected");
@@ -529,9 +530,10 @@ export function validatePoolLimits(maxInflight?: number, weight?: number): void
529
530
  (!Number.isInteger(maxInflight) || maxInflight < 1 || maxInflight > MAX_POOL_INFLIGHT)
530
531
  ) {
531
532
  throw new ConsoleDomainError(
532
- "invalid_pool_limits",
533
+ "invalid_pool",
533
534
  400,
534
535
  "maxInflight must be an integer between 1 and 1000000",
536
+ { field: "maxInflight" },
535
537
  );
536
538
  }
537
539
  if (
@@ -539,9 +541,10 @@ export function validatePoolLimits(maxInflight?: number, weight?: number): void
539
541
  (!Number.isInteger(weight) || weight < 1 || weight > MAX_POOL_WEIGHT)
540
542
  ) {
541
543
  throw new ConsoleDomainError(
542
- "invalid_pool_limits",
544
+ "invalid_pool",
543
545
  400,
544
546
  "weight must be an integer between 1 and 1000",
547
+ { field: "weight" },
545
548
  );
546
549
  }
547
550
  }
@@ -141,9 +141,10 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
141
141
  const rotateCount = request.rotateCount ?? current.rotateCount;
142
142
  if (!Number.isInteger(rotateCount) || rotateCount < 1 || rotateCount > 1000) {
143
143
  throw new ConsoleDomainError(
144
- "invalid_pool_limits",
144
+ "invalid_pool",
145
145
  400,
146
146
  "rotateCount must be an integer between 1 and 1000",
147
+ { field: "rotateCount" },
147
148
  );
148
149
  }
149
150
  const saved = await config.store.setStrategy(a.tenantId, { strategy, rotateCount });
@@ -447,6 +448,12 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
447
448
  const a = requireTenantScope(access, "dashboard:write");
448
449
  const recovered = await config.store.recover(a.tenantId, poolId);
449
450
  if (!recovered) throw new ConsoleDomainError("pool_not_found", 404, `Pool ${poolId} not found`);
451
+ // Recover is the operator's "make this pool healthy again": besides
452
+ // resetting the pool-level state, drop every provider-rate-limit marker
453
+ // (volatile and durable) so a just-cleared pool is not still excluded
454
+ // by a stale 429 flag.
455
+ await config.poolSelector?.clearPoolCooldowns(poolId).catch(() => undefined);
456
+ await config.store.dropAllProviderCooldownMarkers?.(a.tenantId, poolId);
450
457
  await config.poolAgentReleaser?.releasePool(poolId, a.tenantId);
451
458
  await config.auditSink?.record({
452
459
  access: a,
@@ -482,7 +489,7 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
482
489
  ): Promise<PoolSpeedTestResult> {
483
490
  const a = requireTenantScope(access, "dashboard:read");
484
491
  if (!config.store.speedTest) {
485
- throw new ConsoleDomainError("not_supported", 400, "Speed tests are unavailable");
492
+ throw new ConsoleDomainError("capability_unsupported", 400, "Speed tests are unavailable");
486
493
  }
487
494
  if (!Number.isInteger(bytes) || bytes < SPEED_TEST_MIN_BYTES || bytes > SPEED_TEST_MAX_BYTES) {
488
495
  throw new ConsoleDomainError(
@@ -507,7 +514,7 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
507
514
  ): Promise<PoolProviderProbeResult> {
508
515
  const a = requireTenantScope(access, "dashboard:read");
509
516
  if (!config.store.providerProbe) {
510
- throw new ConsoleDomainError("not_supported", 400, "Provider probing is unavailable");
517
+ throw new ConsoleDomainError("capability_unsupported", 400, "Provider probing is unavailable");
511
518
  }
512
519
  const result = await config.store.providerProbe(a.tenantId, poolId, providerId);
513
520
  if (!config.poolSelector) return result;
@@ -596,7 +603,7 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
596
603
  );
597
604
  }
598
605
  if (!config.store.probeAdHocProvider) {
599
- throw new ConsoleDomainError("not_supported", 400, "Provider probing is unavailable");
606
+ throw new ConsoleDomainError("capability_unsupported", 400, "Provider probing is unavailable");
600
607
  }
601
608
  const probe = config.store.probeAdHocProvider.bind(config.store);
602
609
  const results: PoolBatchProviderProbeResult[] = new Array(requests.length);
@@ -632,7 +639,7 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
632
639
  if (config.store.probeAdHoc) {
633
640
  return config.store.probeAdHoc(a.tenantId, request);
634
641
  }
635
- throw new ConsoleDomainError("not_supported", 400, "Ad-hoc pool probing is unavailable");
642
+ throw new ConsoleDomainError("capability_unsupported", 400, "Ad-hoc pool probing is unavailable");
636
643
  },
637
644
  /**
638
645
  * Probes many unsaved pool definitions in one call.
@@ -656,7 +663,7 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
656
663
  );
657
664
  }
658
665
  if (!config.store.probeAdHoc) {
659
- throw new ConsoleDomainError("not_supported", 400, "Ad-hoc pool probing is unavailable");
666
+ throw new ConsoleDomainError("capability_unsupported", 400, "Ad-hoc pool probing is unavailable");
660
667
  }
661
668
  const probe = config.store.probeAdHoc.bind(config.store);
662
669
  const results: PoolBatchProbeResult[] = new Array(requests.length);
@@ -700,7 +707,7 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
700
707
  ): Promise<NetworkPoolResponse & { relayUrl: string }> {
701
708
  const a = requireTenantScope(access, "dashboard:write");
702
709
  if (!config.relayFetch)
703
- throw new ConsoleDomainError("not_supported", 503, "Relay deployment is unavailable");
710
+ throw new ConsoleDomainError("capability_unsupported", 503, "Relay deployment is unavailable");
704
711
  if (!isRelayTarget(request.target))
705
712
  throw new ConsoleDomainError("invalid_request", 422, `Unsupported relay target: ${String(request.target)}`);
706
713
  const result = await deployRelayWorker(config.relayFetch, request);
@@ -15,8 +15,16 @@
15
15
  * the address works (`allowed`, `throttled`) or says nothing about it
16
16
  * (`unreachable`), and treating "says nothing" as "refused" would drop a usable
17
17
  * pool for good.
18
+ *
19
+ * The 403-free-tier and 429 verdicts are the probe's reading of the same facts
20
+ * `classifyEgressFailure` (in `network/rate-limit-verdict`) reads on the
21
+ * dispatch path — for the probe they arrive as an HTTP status + body instead of
22
+ * a `GatewayError`, so the shared free-tier regex is borrowed from there to
23
+ * keep the two classifiers' boundaries honest.
18
24
  */
19
25
 
26
+ import { FREE_TIER_REFUSAL } from "../../../network/rate-limit-verdict";
27
+
20
28
  export type ProbeVerdict = "allowed" | "blocked" | "throttled" | "unreachable";
21
29
 
22
30
  export interface ProbeAnswer {
@@ -25,9 +33,6 @@ export interface ProbeAnswer {
25
33
  readonly reason?: string;
26
34
  }
27
35
 
28
- /** The upstream's own wording for "this address is retired". */
29
- const FREE_TIER_REFUSAL = /freetier|free tier can only be used/i;
30
-
31
36
  /**
32
37
  * Classifies one upstream response.
33
38
  *