cartethyia 2.7.8 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (519) hide show
  1. package/.env.example +3 -3
  2. package/README.md +29 -59
  3. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-BRWKPQPf.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BP-Y5iHN.js → CliToolsPage-CXsaOAO2.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  17. package/dist/dashboard/assets/{InjectSystemPromptPanel-Cv-2Ol_K.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-bKfz_b1K.js → LoginPage-CgoPc5B9.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-B-WQxx_H.js → ModelPicker-D9bFQern.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-emlTuxz6.js → ProxyPage-BwNC_-Cx.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-CctTga2C.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-CUZrKs9r.js → SetupPage-febLU4r1.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-jWmUdDpy.js → StudioPage-M6fvR5Wz.js} +1 -1
  40. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-BFOlvrJC.js +1 -0
  44. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  45. package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
  46. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-CidQVoNo.js → assert-CqTLw5e7.js} +1 -1
  48. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-DyI_mJfi.js → badge-C83EqLB0.js} +1 -1
  50. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  51. package/dist/dashboard/assets/ban-durations-B58FyRxC.js +1 -0
  52. package/dist/dashboard/assets/ban-durations-B58FyRxC.js.gz +0 -0
  53. package/dist/dashboard/assets/{banner-B-yXtCqb.js → banner-D1xn7pIe.js} +1 -1
  54. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  55. package/dist/dashboard/assets/brand/bansos_page.webp +0 -0
  56. package/dist/dashboard/assets/brand/denia_bansos.webp +0 -0
  57. package/dist/dashboard/assets/brand/favicon.webp +0 -0
  58. package/dist/dashboard/assets/brand/login_register.webp +0 -0
  59. package/dist/dashboard/assets/brand/login_register_initial.webp +0 -0
  60. package/dist/dashboard/assets/brand/og_bansos.webp +0 -0
  61. package/dist/dashboard/assets/brand/og_image.webp +0 -0
  62. package/dist/dashboard/assets/brand/sidebar_ico.webp +0 -0
  63. package/dist/dashboard/assets/{card-_HwAEqFF.js → card-DU3Lq5Lm.js} +1 -1
  64. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  65. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js +1 -0
  66. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  67. package/dist/dashboard/assets/{clipboard-button-ByjCk0o9.js → clipboard-button-CEL5Sg_J.js} +1 -1
  68. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  69. package/dist/dashboard/assets/{dialog-BZhOlfz-.js → dialog-BjFR5UHy.js} +1 -1
  70. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  71. package/dist/dashboard/assets/{drawer-BADqIRYP.js → drawer-HvBW0AjI.js} +1 -1
  72. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  73. package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
  74. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  75. package/dist/dashboard/assets/{entry-COaC-irN.js → entry-CECVELyy.js} +1 -1
  76. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  77. package/dist/dashboard/assets/entry-DYYofQ7W.js +1 -0
  78. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  79. package/dist/dashboard/assets/{formatters-DcHJyRiu.js → formatters-DLW4i8zK.js} +1 -1
  80. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  81. package/dist/dashboard/assets/{index-CgKmFngv.js → index-1-fCqe64.js} +2 -2
  82. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  83. package/dist/dashboard/assets/{inline-B2BirKry.js → inline-DGvU2oLK.js} +1 -1
  84. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  85. package/dist/dashboard/assets/{input-D-qV9dKY.js → input-iNnO1X2K.js} +1 -1
  86. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  87. package/dist/dashboard/assets/{layout-B7tVWhtV.js → layout-0q8UGypb.js} +1 -1
  88. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  89. package/dist/dashboard/assets/{list-row-CtkGE3bF.js → list-row-C0w2bQjJ.js} +1 -1
  90. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  91. package/dist/dashboard/assets/{live-DGizF06-.js → live-M5aJ_xCa.js} +1 -1
  92. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  93. package/dist/dashboard/assets/{mutation-GU7XdEjz.js → mutation-BQ0ccH_A.js} +1 -1
  94. package/dist/dashboard/assets/mutation-BQ0ccH_A.js.gz +0 -0
  95. package/dist/dashboard/assets/{network-C9UE-bOr.js → network-BKeQGT3K.js} +1 -1
  96. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  97. package/dist/dashboard/assets/{numbers-BnAd22a_.js → numbers-CivAk3da.js} +1 -1
  98. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  99. package/dist/dashboard/assets/{pagination-BAywyMzS.js → pagination-CpzvI5ep.js} +1 -1
  100. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  101. package/dist/dashboard/assets/providers/devin.svg +9 -0
  102. package/dist/dashboard/assets/providers/devin.svg.gz +0 -0
  103. package/dist/dashboard/assets/providers/windsurf.svg +10 -0
  104. package/dist/dashboard/assets/providers/windsurf.svg.gz +0 -0
  105. package/dist/dashboard/assets/providers/zro.svg +3 -0
  106. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  107. package/dist/dashboard/assets/{query-keys-YSoQOUh6.js → query-keys-IKLh0YFd.js} +1 -1
  108. package/dist/dashboard/assets/query-keys-IKLh0YFd.js.gz +0 -0
  109. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  110. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  111. package/dist/dashboard/assets/routing-BFQX2VBc.js +1 -0
  112. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  113. package/dist/dashboard/assets/{select-BCOcv8WK.js → select-CXbck0jG.js} +2 -2
  114. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  115. package/dist/dashboard/assets/settings-C_XvMHh9.js +1 -0
  116. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  117. package/dist/dashboard/assets/{stack-ByKt3Q1g.js → stack-DggRiCxE.js} +1 -1
  118. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  119. package/dist/dashboard/assets/{state-DXbGTx7N.js → state-ClFzfbYp.js} +1 -1
  120. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  121. package/dist/dashboard/assets/story/25817331.webp +0 -0
  122. package/dist/dashboard/assets/story/Shorekeeper.webp +0 -0
  123. package/dist/dashboard/assets/story/cartethyia-god.webp +0 -0
  124. package/dist/dashboard/assets/story/fleurdelys_plus.webp +0 -0
  125. package/dist/dashboard/assets/story/jinhsi-blossom.webp +0 -0
  126. package/dist/dashboard/assets/story/phrolova-a.webp +0 -0
  127. package/dist/dashboard/assets/story/requestdeniawokkjpg.webp +0 -0
  128. package/dist/dashboard/assets/story/wuthering-waves-hiyuki-aemeath-hiyuki-rover.webp +0 -0
  129. package/dist/dashboard/assets/{switch-ZeTsW623.js → switch-CTIFxKqS.js} +1 -1
  130. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  131. package/dist/dashboard/assets/{useMutation-CbCP4w1o.js → useMutation-BClG3yWj.js} +1 -1
  132. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  133. package/dist/dashboard/console-backgrounds.webp +0 -0
  134. package/dist/dashboard/console-backgrounds_initial.webp +0 -0
  135. package/dist/dashboard/donation/qris-risun-marke.webp +0 -0
  136. package/dist/dashboard/index.html +3 -3
  137. package/dist/dashboard/index.html.gz +0 -0
  138. package/dist/dashboard/precompressed-assets.json +59 -56
  139. package/migrations/full-postgres/0044_add_models_warning_metadata.sql +10 -0
  140. package/migrations/full-postgres/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  141. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  142. package/migrations/lite-sqlite3/0044_add_models_warning_metadata.sql +11 -0
  143. package/migrations/lite-sqlite3/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  144. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  145. package/package.json +1 -1
  146. package/src/boot/app.ts +10 -13
  147. package/src/boot/dependencies.ts +12 -4
  148. package/src/boot/flush.ts +20 -0
  149. package/src/boot/lifecycle.ts +7 -5
  150. package/src/config/cooldowns.ts +1 -1
  151. package/src/config/routing.ts +26 -0
  152. package/src/config/spec-metadata.ts +72 -0
  153. package/src/config/spec.ts +20 -6
  154. package/src/console/api-keys/contracts.ts +12 -1
  155. package/src/console/api-keys/routes.ts +50 -31
  156. package/src/console/auth/account-reset.ts +7 -9
  157. package/src/console/backup/contracts.ts +4 -0
  158. package/src/console/backup/export-endpoint.ts +7 -5
  159. package/src/console/cli-tools/service.ts +6 -12
  160. package/src/console/console-router-guards.ts +23 -16
  161. package/src/console/console-router.ts +7 -0
  162. package/src/console/dashboard-assets.ts +5 -5
  163. package/src/console/domain-registration.ts +27 -2
  164. package/src/console/inferhub/normalize.ts +24 -28
  165. package/src/console/inferhub/types.ts +3 -0
  166. package/src/console/ip-bans/routes.ts +93 -0
  167. package/src/console/observability/store.ts +0 -53
  168. package/src/console/providers/catalog/catalog-projections.ts +5 -2
  169. package/src/console/providers/catalog/contracts.ts +123 -0
  170. package/src/console/providers/catalog/discover-constants.ts +6 -0
  171. package/src/console/providers/catalog/model-operations.ts +122 -10
  172. package/src/console/providers/catalog/provider-operations.ts +116 -8
  173. package/src/console/providers/catalog/public-model-store.ts +0 -0
  174. package/src/console/providers/catalog/routes.ts +65 -4
  175. package/src/console/providers/catalog/store-helpers.ts +30 -1
  176. package/src/console/providers/catalog/store.ts +249 -4
  177. package/src/console/quota/account-quota-global.ts +97 -19
  178. package/src/console/quota/account-quota-shared.ts +49 -0
  179. package/src/console/quota/account-quota-tenant.ts +6 -18
  180. package/src/console/quota/native-identity.ts +2 -0
  181. package/src/console/quota/quota-view.ts +19 -3
  182. package/src/console/routing/pools/operations.ts +31 -46
  183. package/src/console/settings/contracts.ts +18 -0
  184. package/src/console/settings/store.ts +32 -0
  185. package/src/console/shared/errors.ts +33 -6
  186. package/src/console/system-prompt/contracts.ts +0 -1
  187. package/src/main.ts +3 -3
  188. package/src/network/pool/byte-accounting.ts +3 -2
  189. package/src/network/pool/selector.ts +182 -39
  190. package/src/network/pool-cooldown-store.ts +89 -14
  191. package/src/network/pool-health.ts +49 -61
  192. package/src/network/rate-limit-verdict.ts +1 -1
  193. package/src/network/ssrf.ts +6 -6
  194. package/src/observability/event-log.ts +7 -361
  195. package/src/observability/event-query.ts +18 -4
  196. package/src/observability/performance-metrics.ts +9 -0
  197. package/src/observability/runtime-metrics.ts +14 -9
  198. package/src/observability/telemetry-buffer.ts +15 -25
  199. package/src/observability/telemetry-reconciliation.ts +9 -37
  200. package/src/observability/telemetry-row.ts +125 -0
  201. package/src/persistence/db-mode.ts +4 -1
  202. package/src/persistence/dialect.ts +30 -0
  203. package/src/persistence/migrate.ts +6 -3
  204. package/src/persistence/schema.ts +28 -2
  205. package/src/persistence/ttl-cache.ts +20 -2
  206. package/src/protocol/codecs.ts +1 -1
  207. package/src/protocol/dsml.ts +35 -87
  208. package/src/protocol/gateway-error.ts +1 -0
  209. package/src/protocol/markup-stream-parser.ts +195 -0
  210. package/src/protocol/reasoning-text.ts +19 -0
  211. package/src/protocol/reasoning.ts +1 -1
  212. package/src/protocol/request/chat.ts +14 -14
  213. package/src/protocol/request/gemini.ts +8 -2
  214. package/src/protocol/request/messages.ts +4 -5
  215. package/src/protocol/response/chat.ts +12 -20
  216. package/src/protocol/response/gemini.ts +6 -1
  217. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  218. package/src/protocol/response/responses.ts +18 -2
  219. package/src/protocol/sse-decoder.ts +25 -8
  220. package/src/protocol/stop-reason.ts +23 -1
  221. package/src/protocol/stream-error-frames.ts +13 -3
  222. package/src/protocol/think-split.ts +112 -0
  223. package/src/protocol/translation/dialect/qwen.ts +26 -58
  224. package/src/protocol/translation/quirks.ts +1 -1
  225. package/src/protocol/translation/tool-repair.ts +2 -2
  226. package/src/protocol/upstream/send-claude-messages.ts +15 -4
  227. package/src/protocol/upstream-parsing.ts +1 -1
  228. package/src/providers/adapters/compatible-adapter.ts +78 -17
  229. package/src/providers/authentication/oauth-client.ts +1 -1
  230. package/src/providers/authentication/oauth-device-flow.ts +98 -8
  231. package/src/providers/authentication/oauth-refresh-service.ts +153 -59
  232. package/src/providers/configured-provider.ts +148 -0
  233. package/src/providers/default-registry.ts +117 -27
  234. package/src/providers/discovery/discovery-types.ts +12 -0
  235. package/src/providers/discovery/probe-phases.ts +74 -33
  236. package/src/providers/discovery/probing-service.ts +293 -165
  237. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  238. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  239. package/src/providers/integrations/antigravity/antigravity-protocol.ts +121 -2
  240. package/src/providers/integrations/antigravity/antigravity.ts +13 -2
  241. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +72 -34
  242. package/src/providers/integrations/buddy/buddy-chat-shared.ts +191 -35
  243. package/src/providers/integrations/buddy/buddy-checkin.ts +17 -39
  244. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +2 -2
  245. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +5 -5
  246. package/src/providers/integrations/buddy/buddy-identity.ts +89 -6
  247. package/src/providers/integrations/buddy/buddy-media.ts +4 -5
  248. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +2 -2
  249. package/src/providers/integrations/buddy/buddy-quota-shared.ts +1 -1
  250. package/src/providers/integrations/buddy/buddy-usage-shared.ts +1 -3
  251. package/src/providers/integrations/buddy/capabilities.ts +6 -43
  252. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +2 -1
  253. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +23 -1
  254. package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
  255. package/src/providers/integrations/buddy/codebuddy/shared.ts +38 -27
  256. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  257. package/src/providers/integrations/cline/cline.ts +2 -2
  258. package/src/providers/integrations/commandcode/quota.ts +22 -28
  259. package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
  260. package/src/providers/integrations/cursor/cursor-catalog.ts +2 -0
  261. package/src/providers/integrations/cursor/cursor-exec.ts +745 -0
  262. package/src/providers/integrations/cursor/cursor-http2.ts +2 -0
  263. package/src/providers/integrations/cursor/cursor-request.ts +35 -20
  264. package/src/providers/integrations/cursor/cursor-shared.ts +26 -5
  265. package/src/providers/integrations/cursor/cursor.ts +139 -70
  266. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  267. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  268. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +26 -100
  269. package/src/providers/integrations/devin/devin-auth.ts +353 -0
  270. package/src/providers/integrations/devin/devin-catalog.ts +207 -0
  271. package/src/providers/integrations/devin/devin-http.ts +91 -0
  272. package/src/providers/integrations/devin/devin-quota.ts +214 -0
  273. package/src/providers/integrations/devin/devin-shared.ts +111 -0
  274. package/src/providers/integrations/devin/devin.ts +539 -0
  275. package/src/providers/integrations/devin/protocol/devin-proto.ts +558 -0
  276. package/src/providers/integrations/devin/protocol/framing.ts +124 -0
  277. package/src/providers/integrations/factory/factory-oauth.ts +17 -44
  278. package/src/providers/integrations/factory/factory.ts +2 -2
  279. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  280. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  281. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  282. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  283. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  284. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  285. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  286. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  287. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  288. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  289. package/src/providers/integrations/freepi/freepi-catalog.ts +60 -0
  290. package/src/providers/integrations/freepi/freepi-discovery.ts +82 -0
  291. package/src/providers/integrations/freepi/freepi-headers.ts +21 -0
  292. package/src/providers/integrations/freepi/freepi-oauth.ts +142 -0
  293. package/src/providers/integrations/freepi/freepi-protocol.ts +242 -0
  294. package/src/providers/integrations/freepi/freepi-quota.ts +126 -0
  295. package/src/providers/integrations/freepi/freepi-realm.ts +35 -0
  296. package/src/providers/integrations/freepi/freepi.ts +292 -0
  297. package/src/providers/integrations/gemini.ts +2 -2
  298. package/src/providers/integrations/grok/grok-oauth.ts +48 -45
  299. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  300. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  301. package/src/providers/integrations/hermes/hermes-oauth.ts +94 -0
  302. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +13 -21
  303. package/src/providers/integrations/hermes/index.ts +2 -0
  304. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  305. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  306. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  307. package/src/providers/integrations/kiro/kiro.ts +15 -19
  308. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  309. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  310. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  311. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  312. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  313. package/src/providers/integrations/perch/perch-catalog.ts +105 -0
  314. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  315. package/src/providers/integrations/perch/perch-oauth.ts +170 -0
  316. package/src/providers/integrations/perch/perch-protocol.ts +208 -0
  317. package/src/providers/integrations/perch/perch-quota.ts +123 -0
  318. package/src/providers/integrations/perch/perch-realm.ts +82 -0
  319. package/src/providers/integrations/perch/perch-system-prompt.ts +13 -0
  320. package/src/providers/integrations/perch/perch.ts +305 -0
  321. package/src/providers/integrations/perch/saffron-system-prompt.txt +8 -0
  322. package/src/providers/integrations/qoder/chat-proto.ts +328 -328
  323. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  324. package/src/providers/integrations/qoder/qoder-adapter.ts +90 -40
  325. package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
  326. package/src/providers/integrations/qoder/qoder-cache.ts +32 -0
  327. package/src/providers/integrations/qoder/qoder-catalog.ts +26 -6
  328. package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
  329. package/src/providers/integrations/qoder/qoder-checkin.ts +6 -6
  330. package/src/providers/integrations/qoder/qoder-grpc.ts +210 -7
  331. package/src/providers/integrations/qoder/qoder-ide.ts +5 -108
  332. package/src/providers/integrations/qoder/qoder-identity-cache.ts +184 -0
  333. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +83 -18
  334. package/src/providers/integrations/qoder/qoder-realm.ts +9 -6
  335. package/src/providers/integrations/qoder/qoder-umid.ts +109 -342
  336. package/src/providers/integrations/qoder/qoder-virtual.ts +33 -1
  337. package/src/providers/integrations/qoder/qoder.ts +3 -7
  338. package/src/providers/integrations/trae/trae-catalog.ts +9 -14
  339. package/src/providers/integrations/trae/trae-headers.ts +36 -11
  340. package/src/providers/integrations/trae/trae-oauth.ts +14 -17
  341. package/src/providers/integrations/trae/trae-protocol.ts +12 -11
  342. package/src/providers/integrations/trae/trae-quota.ts +96 -36
  343. package/src/providers/integrations/trae/trae-realm.ts +74 -15
  344. package/src/providers/integrations/trae/trae.ts +99 -26
  345. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  346. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  347. package/src/providers/integrations/zro/zro.ts +204 -0
  348. package/src/providers/operations/account-error-classification.ts +235 -278
  349. package/src/providers/operations/account-health-service.ts +273 -26
  350. package/src/providers/operations/account-refresh-status.ts +269 -0
  351. package/src/providers/operations/account-tier.ts +66 -0
  352. package/src/providers/operations/client-version-resolver.ts +1 -1
  353. package/src/providers/operations/client-versions.ts +42 -150
  354. package/src/providers/operations/cooldown-rules.ts +56 -0
  355. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  356. package/src/providers/operations/provider-catalog-seeder.ts +4 -0
  357. package/src/providers/operations/provider-catalog-service.ts +1 -1
  358. package/src/providers/operations/provider-credential-service.ts +102 -32
  359. package/src/providers/operations/provider-rules/github.ts +48 -0
  360. package/src/providers/operations/provider-rules/grok.ts +33 -0
  361. package/src/providers/operations/provider-rules/inception.ts +49 -0
  362. package/src/providers/operations/provider-rules/index.ts +32 -0
  363. package/src/providers/operations/provider-rules/perch.ts +37 -0
  364. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  365. package/src/providers/operations/provider-rules/trae.ts +81 -0
  366. package/src/providers/provider-contracts.ts +33 -2
  367. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  368. package/src/providers/provider-metadata.ts +99 -14
  369. package/src/providers/provider-model-definition.ts +36 -0
  370. package/src/providers/provider-probe.ts +15 -1
  371. package/src/providers/provider-registry.ts +42 -33
  372. package/src/providers/quota/quota-limit-windows.ts +16 -5
  373. package/src/providers/quota/quota-support.ts +1 -1
  374. package/src/providers/quota/quota-window-parser.ts +90 -18
  375. package/src/providers/quota/refresh.ts +96 -18
  376. package/src/security/abuse.ts +71 -0
  377. package/src/security/api-key-auth-cache.ts +1 -1
  378. package/src/security/api-key-auth.ts +6 -2
  379. package/src/transport/dispatch/attempt-finalize.ts +14 -13
  380. package/src/transport/dispatch/attempt-loop.ts +71 -17
  381. package/src/transport/dispatch/generation.ts +2 -8
  382. package/src/transport/dispatch/native-shared.ts +25 -0
  383. package/src/transport/dispatch/proxy-request.ts +9 -46
  384. package/src/transport/dispatch/responses-compact.ts +2 -8
  385. package/src/transport/dispatch/streaming/finalize.ts +9 -6
  386. package/src/transport/dispatch/streaming/stream.ts +9 -36
  387. package/src/transport/dispatch/systemone.ts +2 -8
  388. package/src/transport/dispatch/token-count.ts +2 -6
  389. package/src/transport/dispatch/upstream.ts +67 -1
  390. package/src/transport/drain-endpoint.ts +31 -4
  391. package/src/transport/failure-policy.ts +129 -37
  392. package/src/transport/guards/body-policy.ts +7 -1
  393. package/src/transport/guards/error-lifecycle.ts +13 -1
  394. package/src/transport/guards/request-context.ts +22 -14
  395. package/src/transport/request/preparer.ts +81 -71
  396. package/src/transport/routing/bounded-lru.ts +14 -0
  397. package/src/transport/routing/drain-reserve.ts +129 -0
  398. package/src/transport/routing/plan-gate.ts +7 -1
  399. package/src/transport/routing/round-robin.ts +60 -0
  400. package/src/transport/routing/route-catalog.ts +123 -111
  401. package/src/transport/routing/route-model.ts +51 -9
  402. package/src/transport/routing/router.ts +418 -33
  403. package/src/transport/surface/adapters.ts +11 -2
  404. package/src/transport/surface/chat/adapter.ts +2 -2
  405. package/src/transport/surface/chat/encode.ts +47 -4
  406. package/src/transport/surface/chat/parse.ts +2 -12
  407. package/src/transport/surface/completion.ts +44 -33
  408. package/src/transport/surface/content-parts.ts +21 -0
  409. package/src/transport/surface/messages/adapter.ts +9 -2
  410. package/src/transport/surface/messages/encode.ts +11 -3
  411. package/src/transport/surface/messages/parse.ts +2 -1
  412. package/src/transport/surface/messages/stream.ts +33 -17
  413. package/src/transport/surface/responses/encode.ts +34 -6
  414. package/src/transport/surface/responses/parse.ts +3 -11
  415. package/src/workers/daily-checkin.ts +1 -1
  416. package/src/workers/quota-refresh-sweep.ts +1 -1
  417. package/src/workers/scheduler.ts +22 -4
  418. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js +0 -1
  419. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
  420. package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
  421. package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
  422. package/dist/dashboard/assets/CombosPage-B7JHozVa.js +0 -1
  423. package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
  424. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js +0 -1
  425. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
  426. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +0 -1
  427. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
  428. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +0 -1
  429. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
  430. package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
  431. package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
  432. package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
  433. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js +0 -1
  434. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
  435. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +0 -7
  436. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
  437. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
  438. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  439. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +0 -1
  440. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
  441. package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
  442. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js +0 -1
  443. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
  444. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js +0 -1
  445. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
  446. package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
  447. package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
  448. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js +0 -56
  449. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
  450. package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
  451. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  452. package/dist/dashboard/assets/app-BEsU8hKs.css +0 -2
  453. package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
  454. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  455. package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
  456. package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
  457. package/dist/dashboard/assets/brand/bansos_page.png +0 -0
  458. package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
  459. package/dist/dashboard/assets/brand/favicon.jpg +0 -0
  460. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  461. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  462. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  463. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  464. package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
  465. package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
  466. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
  467. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  468. package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
  469. package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
  470. package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
  471. package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
  472. package/dist/dashboard/assets/entry-Ca7LSCet.js +0 -4
  473. package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
  474. package/dist/dashboard/assets/entry-PDd27uCd.js +0 -1
  475. package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
  476. package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
  477. package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
  478. package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
  479. package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
  480. package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
  481. package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
  482. package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
  483. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
  484. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  485. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  486. package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
  487. package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
  488. package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
  489. package/dist/dashboard/assets/query-keys-YSoQOUh6.js.gz +0 -0
  490. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  491. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  492. package/dist/dashboard/assets/routing-fnCsxgNO.js +0 -1
  493. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  494. package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
  495. package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
  496. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  497. package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
  498. package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
  499. package/dist/dashboard/assets/switch-ZeTsW623.js.gz +0 -0
  500. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  501. package/dist/dashboard/console-backgrounds.jpg +0 -0
  502. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  503. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  504. package/src/providers/bundled-discovery-providers.ts +0 -98
  505. package/src/providers/configured-openai-providers.ts +0 -120
  506. package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
  507. package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
  508. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
  509. package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
  510. package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
  511. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
  512. package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
  513. package/src/providers/operations/model-discovery-cache.ts +0 -34
  514. package/src/providers/operations/probe-fetch.ts +0 -15
  515. package/src/providers/operations/provider-version-cache.ts +0 -30
  516. package/src/providers/provider-capabilities.ts +0 -33
  517. package/src/providers/provider-credential-envelope.ts +0 -43
  518. /package/dist/dashboard/assets/{app-CTxoqyYB.js → app-DIXLjojg.js} +0 -0
  519. /package/dist/dashboard/assets/{app-CTxoqyYB.js.gz → app-DIXLjojg.js.gz} +0 -0
package/src/boot/app.ts CHANGED
@@ -10,7 +10,7 @@ import { chatAdapter } from "../transport/surface/chat/adapter";
10
10
  import { responsesAdapter } from "../transport/surface/responses/adapter";
11
11
  import { messagesAdapter } from "../transport/surface/messages/adapter";
12
12
  import { completionAdapter } from "../transport/surface/completion";
13
- import { SurfaceAdapterRegistry } from "../transport/surface/adapters";
13
+ import { SurfaceAdapterRegistry, type SurfaceAdapter } from "../transport/surface/adapters";
14
14
  import { GatewayError, publicGatewayErrorBody } from "../protocol/gateway-error";
15
15
  import { shutdownError, shutdownNotice } from "../transport/shutdown-notice";
16
16
  import type { CanonicalAdapter } from "../transport/contracts";
@@ -47,7 +47,7 @@ import {
47
47
  import { createTokenCountHandler } from "../transport/dispatch/token-count";
48
48
 
49
49
  import { createTransportPipeline } from "../transport/guards/pipeline";
50
- import { createDrainHandler, isLoopbackPeer, tokenMatches } from "../transport/drain-endpoint";
50
+ import { createDrainHandler, requireLoopbackAdmin } from "../transport/drain-endpoint";
51
51
  import { createConsoleAccountResetHandler } from "../console/auth/account-reset";
52
52
  import { resetConsoleAuthState } from "../console/auth/account-reset-store";
53
53
  import { createBackupExportHandler } from "../console/backup/export-endpoint";
@@ -210,7 +210,7 @@ export function createGatewayApp(deps: GatewayAppDeps) {
210
210
  deps.mode === "route-only"
211
211
  ? (deps.checkRedis ?? checkRedisConnection)
212
212
  : checkRedisConnection;
213
- const registry = new SurfaceAdapterRegistry([
213
+ const registry = new SurfaceAdapterRegistry<SurfaceAdapter & CanonicalAdapter>([
214
214
  chatAdapter,
215
215
  responsesAdapter,
216
216
  messagesAdapter,
@@ -424,12 +424,7 @@ export function createGatewayApp(deps: GatewayAppDeps) {
424
424
  db: deps.db,
425
425
  stateStore: requestStateStore,
426
426
  surfaceRegistry: registry,
427
- adapters: new Map<string, CanonicalAdapter>([
428
- ["chat", chatAdapter],
429
- ["responses", responsesAdapter],
430
- ["messages", messagesAdapter],
431
- ["completion", completionAdapter],
432
- ]),
427
+ adapters: new Map<string, CanonicalAdapter>(registry.bySurface()),
433
428
  preparer: deps.proxyPreparer,
434
429
  readiness: deps.readiness,
435
430
  trustedProxyBoundary: deps.trustedProxyBoundary,
@@ -652,10 +647,12 @@ export function createGatewayApp(deps: GatewayAppDeps) {
652
647
  // well-formed. What refused it is where it came from — the client asked
653
648
  // for something only the local host may ask. `invalid_request` told an
654
649
  // operator to fix the body, when the body was never the problem.
655
- if (!isLoopbackPeer(peerAddresses.get(request) ?? null))
656
- throw new GatewayError("policy_rejected", 403, "task trigger is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂");
657
- if (!tokenMatches(request.headers.get("x-drain-token"), token))
658
- throw new GatewayError("authentication_failed", 401, "invalid drain token (request {requestId}). Wrong key, right door — check the token 🔑");
650
+ requireLoopbackAdmin(request, {
651
+ peer: peerAddresses.get(request) ?? null,
652
+ token,
653
+ policyRejectedMessage: "task trigger is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
654
+ authenticationFailedMessage: "invalid drain token (request {requestId}). Wrong key, right door — check the token 🔑",
655
+ });
659
656
  const name = params.name;
660
657
  if (!tasks.names().includes(name))
661
658
  throw new GatewayError("not_found", 404, `no such task: ${name} (request {requestId}). Nothing here but dust — check the task name 🌙`);
@@ -39,7 +39,7 @@ import { setPendingUsageSource } from "../security/api-key-auth";
39
39
  import { HealthEventFlushBuffer, setHealthEventBuffer } from "../persistence/health-event-flush";
40
40
  import { subscribePeerSettingsRevisions } from "../persistence/settings-revision";
41
41
  import { deleteExpiredSessions } from "../console/auth/session-service";
42
- import { InMemoryIpAbuseStore, IpAbuseProtectionService, RedisIpAbuseStore } from "../security/abuse";
42
+ import { InMemoryIpAbuseStore, IpAbuseProtectionService, RedisIpAbuseStore, type IpAbuseStore } from "../security/abuse";
43
43
  import { checkReadiness } from "../persistence/readiness";
44
44
  import { ProxyRequestPreparer } from "../transport/request/preparer";
45
45
  import { DrizzleNetworkPoolLoader } from "../network/pool/loader";
@@ -62,7 +62,7 @@ import {
62
62
  } from "../workers/buddy-growth-tasks";
63
63
  import { createAccountSecretResolver } from "../providers/operations/provider-credential-service";
64
64
  import { quotaCacheSize } from "../providers/quota/cache";
65
- import { modelCatalogCacheSize } from "../providers/operations/model-catalog-cache";
65
+ import { modelCatalogCacheSize } from "../providers/operations/provider-caches";
66
66
  import { preferencesReaderFor } from "../persistence/tenant-preferences";
67
67
  import { sweepExpiredCooldowns } from "../providers/operations/account-health-service";
68
68
  import { DrizzleTelemetryStore } from "../persistence/stores/telemetry-store";
@@ -104,6 +104,8 @@ export interface ProductionDeps {
104
104
  proxyPreparer: ProxyRequestPreparer;
105
105
  networkBindingFactory: ValidatedNetworkBindingFactory;
106
106
  ipAbuseProtection: IpAbuseProtectionService;
107
+ /** The abuse store backing manual IP bans and the ban list. */
108
+ ipAbuseStore: IpAbuseStore;
107
109
  readiness: () => Promise<ReadinessCheckResult>;
108
110
  scheduledTasks: ScheduledTaskRegistry;
109
111
  poolAgentResolver: PoolAgentResolver;
@@ -593,6 +595,11 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
593
595
  name: "client-version-monitor",
594
596
  intervalMs: CLIENT_VERSION_MONITOR_INTERVAL_MS,
595
597
  initialDelayMs: CLIENT_VERSION_INITIAL_DELAY_MS,
598
+ // Pure outbound reads with no shared rows/files: they must not be gated
599
+ // by the process-wide busy latch, or a nearly-continuous heavy sweep
600
+ // (quota-refresh) starves this monitor at its single initial tick and
601
+ // every 30-minute interval after it.
602
+ skipGate: true,
596
603
  run: refreshProviderClientVersions,
597
604
  });
598
605
 
@@ -606,13 +613,13 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
606
613
  // first open after an expiry always blocks on the provider.
607
614
  //
608
615
  // It also carries the daily check-in ride-along: each account refreshed
609
- // here gets its once-per-day WorkBuddy/CodeBuddy credit grant claimed in
616
+ // here gets its once-per-day CodeBuddy credit grant claimed in
610
617
  // the same pass (one attempt per account per day, guarded by a day
611
618
  // marker), so no second timer duplicates the credential resolution.
612
619
  //
613
620
  // Registered in **every** mode, not only when Redis is present: gating it on
614
621
  // Redis would cost Lite proactive quota warming and the check-in ride-along
615
- // above that claims the once-per-day WorkBuddy/CodeBuddy credit grant. The
622
+ // above that claims the once-per-day CodeBuddy credit grant. The
616
623
  // whole sweep chain accepts `RedisClient | undefined` and falls back to the
617
624
  // in-process TTL cache and an in-process day marker, so Redis is an
618
625
  // optimization here rather than a precondition — which is exactly what
@@ -739,6 +746,7 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
739
746
  }),
740
747
  networkBindingFactory,
741
748
  ipAbuseProtection,
749
+ ipAbuseStore: ipStore,
742
750
  readiness,
743
751
  scheduledTasks,
744
752
  poolAgentResolver,
@@ -0,0 +1,20 @@
1
+ // Shared drain sequence for the process's queued writers. The console's manual
2
+ // "flush now" and the shutdown coordinator's telemetry flush must drain exactly
3
+ // the same buffers in the same order — a fourth writer added to one path but
4
+ // not the other would silently stop reaching the disk on one of them.
5
+ //
6
+ // The deps are the minimal write surface each buffer exposes: a `flush()` that
7
+ // resolves when the buffer is drained. The concrete buffers satisfy it, and a
8
+ // test can substitute stubs without casting.
9
+ export interface FlushAllBuffersDeps {
10
+ readonly telemetryBuffer: { flush(): Promise<void> };
11
+ readonly usageFlush: { flush(): Promise<void> };
12
+ readonly healthEventFlush: { flush(): Promise<void> };
13
+ }
14
+
15
+ /** Drains every buffered writer, in the canonical order. */
16
+ export async function flushAllBuffers(deps: FlushAllBuffersDeps): Promise<void> {
17
+ await deps.telemetryBuffer.flush();
18
+ await deps.usageFlush.flush();
19
+ await deps.healthEventFlush.flush();
20
+ }
@@ -2,6 +2,7 @@ import { closeDb } from "../persistence/boot";
2
2
  import { closeRedis } from "../persistence/redis";
3
3
  import { buildProductionDeps } from "./dependencies";
4
4
  import type { ProductionDeps } from "./dependencies";
5
+ import { flushAllBuffers } from "./flush";
5
6
  import { withTimeout } from "./timeout";
6
7
  import { resolveShutdownDrainWindowMs } from "../config/runtime";
7
8
  import { log } from "../observability/logger";
@@ -35,8 +36,11 @@ export class ShutdownCoordinator {
35
36
  ) {
36
37
  this.hooks = hooks ?? {};
37
38
  this.drainWindowMs = opts?.drainWindowMs ?? 20_000;
38
- this.drainTimeoutMs = opts?.drainTimeoutMs ?? 5_000;
39
- this.flushTimeoutMs = opts?.flushTimeoutMs ?? 2_000;
39
+ // Defaults mirror what `bootstrap()` passes (8_000 / 1_000; drainWindowMs
40
+ // 20_000 is the spec default `resolveShutdownDrainWindowMs()` returns), so
41
+ // reasoning about drain timing from the class alone matches production.
42
+ this.drainTimeoutMs = opts?.drainTimeoutMs ?? 8_000;
43
+ this.flushTimeoutMs = opts?.flushTimeoutMs ?? 1_000;
40
44
  }
41
45
 
42
46
  /**
@@ -217,9 +221,7 @@ export async function bootstrap(): Promise<CartethyiaBoot> {
217
221
  shutdownCoordinator: new ShutdownCoordinator(
218
222
  {
219
223
  flushTelemetry: async () => {
220
- await deps.telemetryBuffer.flush();
221
- await deps.usageFlush.flush();
222
- await deps.healthEventFlush.flush();
224
+ await flushAllBuffers(deps);
223
225
  },
224
226
  closePools: async () => {
225
227
  await boot.server?.stop();
@@ -119,7 +119,7 @@ export function resolveAccountAuthRejectedCooldownMs(): number {
119
119
  * Deliberately a *base*, not the 24h flat penalty it replaced. `11140` has two
120
120
  * documented causes our classifier cannot separate from the response alone: the
121
121
  * user's prompt failed a safety review, **and** sending the wrong desktop
122
- * `User-Agent` brand (`workbuddy/shared.ts`). A flat 24h meant that after we
122
+ * `User-Agent` brand (`codebuddy/shared.ts`). A flat 24h meant that after we
123
123
  * fixed a header regression every buddy account stayed dead for up to a further
124
124
  * day, and one unsafe prompt cost a whole account a day.
125
125
  *
@@ -0,0 +1,26 @@
1
+ // Routing configuration knobs.
2
+ import { CONFIG_SPEC, readInt } from "./spec";
3
+
4
+ /** Routing reserve percent (0–100). Accounts at/below this remaining-quota % are demoted. */
5
+ export function accountReservePct(): number {
6
+ return readInt(
7
+ "CARTETHYIA_ACCOUNT_RESERVE_PCT",
8
+ CONFIG_SPEC.CARTETHYIA_ACCOUNT_RESERVE_PCT,
9
+ );
10
+ }
11
+
12
+ /** Session-sticky warm window in ms. 0 disables stickiness. */
13
+ export function routingStickyWarmMs(): number {
14
+ return readInt(
15
+ "CARTETHYIA_ROUTING_STICKY_WARM_MS",
16
+ CONFIG_SPEC.CARTETHYIA_ROUTING_STICKY_WARM_MS,
17
+ );
18
+ }
19
+
20
+ /** LRU cap on remembered session→account sticky state per provider. */
21
+ export function routingStickyMaxSessions(): number {
22
+ return readInt(
23
+ "CARTETHYIA_ROUTING_STICKY_MAX_SESSIONS",
24
+ CONFIG_SPEC.CARTETHYIA_ROUTING_STICKY_MAX_SESSIONS,
25
+ );
26
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Shared metadata for config docs and tests.
3
+ *
4
+ * `CONFIG_SPEC` declares the gateway's spec-driven variables. These constants
5
+ * name the deliberate asymmetries around it so `.env.example`, the generated
6
+ * reference, and the drift tests all project the same exception lists.
7
+ */
8
+
9
+ /** Keys in `CONFIG_SPEC` that are intentionally absent from `.env.example`. */
10
+ export const SPEC_ONLY_ENV_EXAMPLE_KEYS: Readonly<Record<string, true>> = {
11
+ // Setup wizard writes this directly; a sample would mislead copy/paste.
12
+ CARTETHYIA_API_KEY: true,
13
+ // Opt-in diagnostics; sampling them would nudge operators to enable them.
14
+ CARTETHYIA_FORCED_GC: true,
15
+ CARTETHYIA_RUNTIME_METRICS_INTERVAL_MS: true,
16
+ // Advanced knobs documented in the generated configuration reference.
17
+ CARTETHYIA_DISABLED_TASKS: true,
18
+ // Install-identity directory; only operators who need a stable identity
19
+ // across restarts touch it, and the reference covers it.
20
+ CARTETHYIA_INSTALL_ID_DIR: true,
21
+ CARTETHYIA_OAUTH_CALLBACK_PORT: true,
22
+ // Provider-specific overrides. Readers live in `src/config/providers.ts`;
23
+ // the majority of installs do not need them in the starter sample.
24
+ CARTETHYIA_KIMI_OAUTH_HOST: true,
25
+ CARTETHYIA_KIMI_QUOTA_BASE_URL: true,
26
+ CARTETHYIA_GITHUB_ENTERPRISE_DOMAIN: true,
27
+ CARTETHYIA_OPENCODE_UNAVAILABLE_FREE: true,
28
+ // Telemetry retention is deployment-level and documented in the reference.
29
+ CARTETHYIA_TELEMETRY_RETENTION_DAYS: true,
30
+ CARTETHYIA_TELEMETRY_PAYLOAD_MAX_BYTES: true,
31
+ };
32
+
33
+ /** Keys in `.env.example` that `CONFIG_SPEC` does not declare. */
34
+ export const ENV_EXAMPLE_ONLY_KEYS: Readonly<Record<string, true>> = {
35
+ // Read outside `src/config/` by their owning modules.
36
+ CARTETHYIA_DB_MODE: true,
37
+ NODE_ENV: true,
38
+ LOG_LEVEL: true,
39
+ DATABASE_URL: true,
40
+ DATABASE_POOL_MAX: true,
41
+ DATABASE_POOL_IDLE_FLOOR: true,
42
+ REDIS_URL: true,
43
+ CARTETHYIA_DATA_DIR: true,
44
+ CARTETHYIA_TELEMETRY_PAYLOAD_DIR: true,
45
+ CARTETHYIA_CONFIG_DIR: true,
46
+ DASHBOARD_PORT: true,
47
+ VITE_BACKEND_URL: true,
48
+ // Test-runner-only knob, not read by the gateway runtime.
49
+ CARTETHYIA_TEST_JOBS: true,
50
+ };
51
+
52
+ /**
53
+ * Operator-visible variables that are intentionally outside `CONFIG_SPEC` and
54
+ * therefore omitted from the generated configuration reference.
55
+ */
56
+ export const CONFIG_REFERENCE_EXTERNAL_VARIABLES = [
57
+ "CARTETHYIA_DB_MODE",
58
+ "DATABASE_URL",
59
+ "REDIS_URL",
60
+ "NODE_ENV",
61
+ "LOG_LEVEL",
62
+ "DATABASE_POOL_MAX",
63
+ "DATABASE_POOL_IDLE_FLOOR",
64
+ "CARTETHYIA_DATA_DIR",
65
+ "CARTETHYIA_TELEMETRY_PAYLOAD_DIR",
66
+ "CARTETHYIA_CONFIG_DIR",
67
+ "DASHBOARD_PORT",
68
+ "VITE_BACKEND_URL",
69
+ "CARTETHYIA_TEST_DATABASE_URL",
70
+ "CARTETHYIA_TEST_JOBS",
71
+ "CARTETHYIA_REDIS_URL",
72
+ ] as const;
@@ -173,12 +173,6 @@ export const CONFIG_SPEC = {
173
173
  */
174
174
  CARTETHYIA_DISABLED_TASKS: { kind: "list" },
175
175
 
176
- /**
177
- * Force the Qoder UMID bridge to the mini-ELF bundle on Linux
178
- * (`1` = always mini, `0` = never mini; absent = auto-detect containers).
179
- */
180
- CARTETHYIA_QODER_UMID_MINI: { kind: "int", default: 0, min: 0, max: 1 },
181
-
182
176
  /**
183
177
  * How long a day's telemetry is kept, in days.
184
178
  *
@@ -270,6 +264,26 @@ export const CONFIG_SPEC = {
270
264
  CARTETHYIA_ACCOUNT_AUTH_REJECTED_COOLDOWN_MS: { kind: "int", default: 300_000, min: 0, max: 86_400_000 },
271
265
  CARTETHYIA_ACCOUNT_POLICY_BLOCK_COOLDOWN_MS: { kind: "int", default: 300_000, min: 0, max: 86_400_000 },
272
266
  CARTETHYIA_ACCOUNT_DISABLED_RECOVERY_MS: { kind: "int", default: 86_400_000, min: 0, max: 604_800_000 },
267
+ /**
268
+ * Routing reserve: an account whose remaining quota fraction falls at or
269
+ * below this percent is demoted behind non-reserve siblings during ordering
270
+ * (it is still eligible and still routed — a reserve is a preference, never
271
+ * an exclusion). 0 disables reserve demotion for every account.
272
+ */
273
+ CARTETHYIA_ACCOUNT_RESERVE_PCT: { kind: "int", default: 20, min: 0, max: 100 },
274
+ /**
275
+ * Session stickiness warm window (milliseconds). Within this window after an
276
+ * account last served a session id, routing prefers that account when it is
277
+ * still eligible — prompt-cache warmth is worth more than perfect load
278
+ * balance. Past the window, the sticky account is only an ordering hint.
279
+ * 0 disables stickiness.
280
+ */
281
+ CARTETHYIA_ROUTING_STICKY_WARM_MS: { kind: "int", default: 300_000, min: 0, max: 86_400_000 },
282
+ /**
283
+ * Hard ceiling on distinct session ids whose sticky state is retained in
284
+ * memory per provider (LRU). Evicted sessions simply lose their preference.
285
+ */
286
+ CARTETHYIA_ROUTING_STICKY_MAX_SESSIONS: { kind: "int", default: 256, min: 1, max: 10_000 },
273
287
 
274
288
  // Proxy pool agents
275
289
  CARTETHYIA_PROXY_MAX_SOCKETS: { kind: "int", default: 100, min: 1, max: 100_000 },
@@ -11,7 +11,7 @@ import { type ShareLinkStore, type ShareLinkSummary } from "../../persistence/st
11
11
  import { API_KEY_MODES, API_KEY_CONCURRENCY_SCOPES, type ApiKeyMode, type ApiKeyModelAccessMode, type ShareLinkKind } from "../../persistence/schema";
12
12
  import { isValidTenantKeyScope, type AccessScope } from "../../security/access-control";
13
13
  import { normalizeClientRouterId } from "../../security/client-router-fingerprint";
14
- import type { ShareActivityPort } from "../share/share-usage";
14
+ import type { ShareActivityPort, SharedKeySummary } from "../share/share-usage";
15
15
  import { ConsoleDomainError } from "../shared/errors";
16
16
  import { pickDefined } from "../shared/object";
17
17
  import type { ConsoleAccessResolver } from "../auth/access";
@@ -35,6 +35,17 @@ export type {
35
35
  SharedKeySummary,
36
36
  SharedKeyActivityDetail,
37
37
  } from "../share/share-usage";
38
+
39
+ /** Paged response of the share-child list endpoint. */
40
+ export interface SharedKeyPage {
41
+ readonly rows: readonly SharedKeySummary[];
42
+ readonly total: number;
43
+ readonly active: number;
44
+ readonly revoked: number;
45
+ /** Family-wide lifetime tokens consumed, independent of the page. */
46
+ readonly consumed: number;
47
+ }
48
+
38
49
  /** How a share template's concurrency cap applies to its recipients. */
39
50
  export type ConcurrencyScope = (typeof API_KEY_CONCURRENCY_SCOPES)[number];
40
51
 
@@ -78,25 +78,53 @@ function parseSharedChildrenQuery(query: URLSearchParams): SharedChildrenPageOpt
78
78
  return { limit, offset, sortBy, order, status };
79
79
  }
80
80
 
81
+ /**
82
+ * Drops the auth-cache snapshot for a key family: the parent first, then
83
+ * every child.
84
+ *
85
+ * Parent policy is live-inherited by children at auth time, so their cached
86
+ * snapshots must die with the parent — otherwise a denylist / budget edit
87
+ * looks like a no-op for up to AUTH_CACHE_TTL_MS per child.
88
+ */
89
+ function invalidateFamilyCaches(keyId: string, children: readonly ApiKeyRecord[]): void {
90
+ invalidateApiKeyCache(keyId);
91
+ for (const child of children) invalidateApiKeyCache(child.id);
92
+ }
93
+
94
+ /**
95
+ * Purges admission counters for a key family: the parent first, then every
96
+ * child.
97
+ *
98
+ * Share children admit under the parent id, so the parent's counter must die
99
+ * with a revoked/regenerated parent too — otherwise a family budget can be
100
+ * bypassed by a counter seeded under the old credential.
101
+ */
102
+ async function purgeFamilyAdmission(
103
+ admissionService: { purgeKey(apiKeyId: string): Promise<void> },
104
+ keyId: string,
105
+ children: readonly ApiKeyRecord[],
106
+ ): Promise<void> {
107
+ await Promise.all([
108
+ admissionService.purgeKey(keyId),
109
+ ...children.map((child) => admissionService.purgeKey(child.id)),
110
+ ]);
111
+ }
112
+
81
113
  export function createApiKeyOperations(config: ApiKeyConfig) {
82
114
  const operations = {
83
- async listKeys(access: AccessDecision | undefined): Promise<ApiKeyResponse[]> {
115
+ async listKeys(access: AccessDecision | undefined, includeChildren = false): Promise<ApiKeyResponse[]> {
84
116
  const authorized = requireTenantScope(access, "dashboard:read");
85
117
  const records = await config.store.list(authorized.tenantId);
86
118
  const topLevel = records.filter(
87
119
  (record) => record.revokedAt === undefined && record.parentKeyId === undefined,
88
120
  );
89
- // A share template's own counter is the live family accumulator now:
90
- // every recipient commit adds to it (see ApiKeyAdmissionService
91
- // commitUsage), so reporting it raw gives the enforced family total.
92
- // No telemetry add-on is needed, and adding it here would double-count.
93
- return Promise.all(
94
- topLevel.map(async (record) => {
95
- const response = sanitizeApiKeyResponse(record);
96
- if (record.keyMode !== "share") return response;
97
- return response;
98
- }),
99
- );
121
+ // Share children are omitted from the main key list (they belong to a
122
+ // template), but the Banned Users search wants them so a child can be
123
+ // found and banned by hand.
124
+ const visible = includeChildren
125
+ ? records.filter((record) => record.revokedAt === undefined)
126
+ : topLevel;
127
+ return visible.map(sanitizeApiKeyResponse);
100
128
  },
101
129
  async listActiveBannedKeys(access: AccessDecision | undefined): Promise<ApiKeyResponse[]> {
102
130
  const authorized = requireTenantScope(access, "dashboard:read");
@@ -337,11 +365,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
337
365
  });
338
366
  if (!updated) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
339
367
  const children = await config.store.listChildren(authorized.tenantId, keyId);
340
- // Parent policy is live-inherited by children at auth time, so their
341
- // cached snapshots must die with the parent — otherwise a denylist /
342
- // budget edit looks like a no-op for up to AUTH_CACHE_TTL_MS per child.
343
- invalidateApiKeyCache(keyId);
344
- for (const child of children) invalidateApiKeyCache(child.id);
368
+ invalidateFamilyCaches(keyId, children);
345
369
  // The allow/deny list and prefix decide which models `/v1/models`
346
370
  // returns, which is a different cache from the auth snapshot: the
347
371
  // listing is cached per (tenant, key, mode, list, prefix), so editing
@@ -391,10 +415,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
391
415
  // Drop stale admission counters so a newly lowered one-time/recurring
392
416
  // budget cannot be bypassed by a counter seeded under the looser limit.
393
417
  // Share children admit under the parent id, so purge the parent too.
394
- await Promise.all([
395
- config.admissionService.purgeKey(keyId),
396
- ...children.map((child) => config.admissionService.purgeKey(child.id)),
397
- ]);
418
+ await purgeFamilyAdmission(config.admissionService, keyId, children);
398
419
  }
399
420
  // Adding a daily/monthly ceiling to a key that already spent in the
400
421
  // current bucket must not hand it a full fresh budget: seed the counter
@@ -441,12 +462,8 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
441
462
  const children = await config.store.listChildren(authorized.tenantId, keyId);
442
463
  const removed = await config.store.remove(authorized.tenantId, keyId);
443
464
  if (!removed) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
444
- invalidateApiKeyCache(keyId);
445
- for (const child of children) invalidateApiKeyCache(child.id);
446
- await Promise.all([
447
- config.admissionService.purgeKey(keyId),
448
- ...children.map((child) => config.admissionService.purgeKey(child.id)),
449
- ]);
465
+ invalidateFamilyCaches(keyId, children);
466
+ await purgeFamilyAdmission(config.admissionService, keyId, children);
450
467
  await config.auditSink?.record({
451
468
  access: authorized,
452
469
  action: "api_key.deleted",
@@ -620,8 +637,9 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
620
637
  keyPrefix: generated.prefix,
621
638
  });
622
639
  if (!updated) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
623
- invalidateApiKeyCache(keyId);
624
- await config.admissionService.purgeKey(keyId);
640
+ // Regeneration is personal-key only, so the family is the key itself.
641
+ invalidateFamilyCaches(keyId, []);
642
+ await purgeFamilyAdmission(config.admissionService, keyId, []);
625
643
  const shareStore = config.shareStore;
626
644
  let share: ShareKeyResponse | null = null;
627
645
  if (shareStore) {
@@ -921,9 +939,10 @@ function jsonNull(): Response {
921
939
  export function createApiKeyRoutes(config: ApiKeyConfig): AnyElysia {
922
940
  const factory = createApiKeyOperations(config);
923
941
  return new Elysia({ prefix: "/api-keys" })
924
- .get("/", async ({ request, set }) => {
942
+ .get("/", async ({ request, set, query }) => {
925
943
  try {
926
- return await factory.listKeys(config.accessResolver(request));
944
+ const includeChildren = query?.["includeChildren"] === "1" || query?.["includeChildren"] === "true";
945
+ return await factory.listKeys(config.accessResolver(request), includeChildren);
927
946
  } catch (error) {
928
947
  return errorResponse(error, set, "API-key operation failed");
929
948
  }
@@ -23,7 +23,7 @@
23
23
  * the operator to type it rather than press Enter on a menu row.
24
24
  */
25
25
  import { GatewayError } from "../../protocol/gateway-error";
26
- import { isLoopbackPeer, tokenMatches } from "../../transport/drain-endpoint";
26
+ import { requireLoopbackAdmin } from "../../transport/drain-endpoint";
27
27
 
28
28
  interface ConsoleAccountResetDeps {
29
29
  /** Shared secret; the route is registered only when this is set. */
@@ -40,14 +40,12 @@ export function createConsoleAccountResetHandler(
40
40
  deps: ConsoleAccountResetDeps,
41
41
  ): (context: { request: Request; body: unknown }) => Promise<Response> {
42
42
  return async ({ request, body }) => {
43
- if (!isLoopbackPeer(deps.resolvePeerAddress(request)))
44
- throw new GatewayError(
45
- "policy_rejected",
46
- 403,
47
- "console account reset is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
48
- );
49
- if (!tokenMatches(request.headers.get("x-drain-token"), deps.token))
50
- throw new GatewayError("authentication_failed", 401, "invalid reset token (request {requestId}). Wrong key, right door — check the token 🔑");
43
+ requireLoopbackAdmin(request, {
44
+ peer: deps.resolvePeerAddress(request),
45
+ token: deps.token,
46
+ policyRejectedMessage: "console account reset is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
47
+ authenticationFailedMessage: "invalid reset token (request {requestId}). Wrong key, right door — check the token 🔑",
48
+ });
51
49
  const confirm =
52
50
  body !== null && typeof body === "object" && "confirm" in body
53
51
  ? body.confirm
@@ -20,6 +20,8 @@
20
20
  * as sensitive as the database.
21
21
  */
22
22
  import { getTableColumns, getTableName, type Column, type Table } from "drizzle-orm";
23
+ import type { BackupInspection } from "./service";
24
+ import type { DeleteAllScope } from "./store";
23
25
  import { resolveMaxBodyBytes } from "../../config/runtime";
24
26
  import {
25
27
  apiKeys,
@@ -393,6 +395,8 @@ export type BackupSection = "config" | "telemetry";
393
395
 
394
396
  export const BACKUP_SECTIONS: readonly BackupSection[] = ["config", "telemetry"];
395
397
 
398
+ export type { BackupInspection, DeleteAllScope };
399
+
396
400
  export interface BackupPayload {
397
401
  readonly app: typeof BACKUP_APP;
398
402
  readonly version: number;
@@ -22,7 +22,7 @@ import { mkdirSync, writeFileSync } from "node:fs";
22
22
  import { homedir } from "node:os";
23
23
  import { join } from "node:path";
24
24
  import { GatewayError } from "../../protocol/gateway-error";
25
- import { isLoopbackPeer, tokenMatches } from "../../transport/drain-endpoint";
25
+ import { requireLoopbackAdmin } from "../../transport/drain-endpoint";
26
26
  import { tablesForSection } from "./contracts";
27
27
  import { compressBackupStream, exportBackupStream } from "./store";
28
28
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
@@ -52,10 +52,12 @@ export function createBackupExportHandler(
52
52
  // both: neither refusal is about the body. The first is where the request
53
53
  // came from, the second is the credential it carried — and `invalid_request`
54
54
  // told an operator to fix a body that was never the problem.
55
- if (!isLoopbackPeer(deps.resolvePeerAddress(request)))
56
- throw new GatewayError("policy_rejected", 403, "backup export is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂");
57
- if (!tokenMatches(request.headers.get("x-drain-token"), deps.token))
58
- throw new GatewayError("authentication_failed", 401, "invalid export token (request {requestId}). Wrong key, right door — check the token 🔑");
55
+ requireLoopbackAdmin(request, {
56
+ peer: deps.resolvePeerAddress(request),
57
+ token: deps.token,
58
+ policyRejectedMessage: "backup export is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
59
+ authenticationFailedMessage: "invalid export token (request {requestId}). Wrong key, right door — check the token 🔑",
60
+ });
59
61
 
60
62
  const directory =
61
63
  body !== null && typeof body === "object" && "directory" in body
@@ -8,7 +8,6 @@ import type {
8
8
  CliMappingSettings,
9
9
  DownloadResult,
10
10
  ToolDef,
11
- ToolInjector,
12
11
  ToolStatus,
13
12
  } from "./contracts";
14
13
  import { TOOL_IDS, TOOL_REGISTRY, type ToolId, type ToolRegistryEntry } from "./contracts";
@@ -47,11 +46,6 @@ function toToolRegistryEntry(def: ToolDef): ToolRegistryEntry {
47
46
  };
48
47
  }
49
48
 
50
- function injectorFor(toolId: string): ToolInjector | null {
51
- const injector = INJECTORS[toolId as ToolId];
52
- return injector ?? null;
53
- }
54
-
55
49
  /** Operator-facing summary of what an apply actually did. */
56
50
  function applyMessage(outcome: ApplyOutcome, toolId: string, fileMessage: string): string {
57
51
  switch (outcome) {
@@ -139,8 +133,8 @@ export class CliToolService {
139
133
  }
140
134
 
141
135
  async getStatus(toolId: string): Promise<ToolStatus | null> {
142
- const injector = injectorFor(toolId);
143
- if (injector === null) return null;
136
+ const injector = INJECTORS[toolId as ToolId];
137
+ if (injector === undefined) return null;
144
138
  return injector.getStatus();
145
139
  }
146
140
 
@@ -249,8 +243,8 @@ export class CliToolService {
249
243
  }
250
244
 
251
245
  async downloadConfig(toolId: string, input: ApplyInput): Promise<DownloadResult | null> {
252
- const injector = injectorFor(toolId);
253
- if (injector === null) return null;
246
+ const injector = INJECTORS[toolId as ToolId];
247
+ if (injector === undefined) return null;
254
248
  if (!input.endpoint || !input.apiKey) return null;
255
249
  try {
256
250
  return await injector.download(input);
@@ -281,8 +275,8 @@ export class CliToolService {
281
275
  apiKeyId: string,
282
276
  input: ApplyInput & { mode?: "file" | "remote" | "both" },
283
277
  ): Promise<ApplyConfigResult | null> {
284
- const injector = injectorFor(toolId);
285
- if (injector === null) return null;
278
+ const injector = INJECTORS[toolId as ToolId];
279
+ if (injector === undefined) return null;
286
280
  const mode = input.mode ?? "both";
287
281
  const wantsFile = mode === "file" || mode === "both";
288
282
  const wantsRemote = mode === "remote" || mode === "both";