cartethyia 2.5.3 → 2.5.6

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 (456) hide show
  1. package/.env.example +13 -4
  2. package/README.md +2 -2
  3. package/dist/dashboard/assets/{BannedPage-hfCzi-Ux.js → BannedPage-CADDq4IU.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-DEDyKBlC.js → CliToolsPage-DuO1Z7dq.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-bsLuZmYH.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-_UWhwziJ.js → CustomizationPage-BR5HHvbi.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-DDVIfZGW.js → LoginPage-V5faV9V2.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
  19. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
  20. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
  25. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js +1 -0
  26. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
  27. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
  28. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-D6pmQ86L.js → SetupPage-UB690SE2.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-DWvKiuSl.js → SortableList-BLujdX7N.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-CmkUrgdj.js → StudioPage-D5C2raAc.js} +3 -3
  38. package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
  39. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
  40. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
  41. package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
  42. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  43. package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
  44. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  45. package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-C9FD_JwJ.js} +1 -1
  46. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  47. package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-CR72dasT.js} +1 -1
  48. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  49. package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-CI9_eSfO.js} +1 -1
  50. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  51. package/dist/dashboard/assets/{card--AtFedAq.js → card-DS7CQEsO.js} +1 -1
  52. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  53. package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-CNqzrfdb.js} +1 -1
  54. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  55. package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-611pSsaX.js} +1 -1
  56. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  57. package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
  58. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  59. package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
  60. package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
  61. package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
  62. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  63. package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
  64. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-B9Xvtnfj.js +1 -0
  66. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
  68. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  69. package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
  70. package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
  71. package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-sltk1qUi.js} +1 -1
  72. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  73. package/dist/dashboard/assets/{index-BydyKaDz.js → index-CmxZr14F.js} +2 -2
  74. package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
  75. package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-BhY2sSnO.js} +1 -1
  76. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  77. package/dist/dashboard/assets/{input-BG41et9G.js → input-V1umFiBk.js} +1 -1
  78. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  79. package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
  80. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  81. package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-1zzZYM0B.js} +1 -1
  82. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  83. package/dist/dashboard/assets/{live-BJGYRBnR.js → live-BivGfuUs.js} +1 -1
  84. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  85. package/dist/dashboard/assets/model-bans-CmffjLuG.js +1 -0
  86. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  87. package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-46P0oWMe.js} +1 -1
  88. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  89. package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
  90. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  91. package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-BFhGpwR1.js} +1 -1
  92. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  93. package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-CAb4F37R.js} +1 -1
  94. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  95. package/dist/dashboard/assets/{select-BO6h8yNv.js → select-BYR_5K2c.js} +2 -2
  96. package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
  97. package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-DNX9sydi.js} +1 -1
  98. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  99. package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-BHTfXdz_.js} +1 -1
  100. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  101. package/dist/dashboard/assets/{state-DYdQmdAl.js → state-mPDRlQkl.js} +1 -1
  102. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  103. package/dist/dashboard/assets/switch-DOmJxg7H.js +1 -0
  104. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  105. package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
  106. package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
  107. package/dist/dashboard/assets/tokens-COzario9.js +1 -0
  108. package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
  109. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
  110. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
  111. package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-Dem0lXQj.js} +1 -1
  112. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  113. package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
  114. package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
  115. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  116. package/dist/dashboard/index.html +1 -1
  117. package/dist/dashboard/index.html.gz +0 -0
  118. package/dist/dashboard/precompressed-assets.json +57 -57
  119. package/migrations/README.md +52 -52
  120. package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
  121. package/package.json +96 -95
  122. package/scripts/cli/admin.ts +125 -0
  123. package/scripts/cli/autostart.ts +237 -0
  124. package/scripts/cli/{ui/background.ts → background.ts} +1 -1
  125. package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -248
  126. package/scripts/cli/doctor.ts +37 -0
  127. package/scripts/cli/{support/env.ts → env.ts} +99 -99
  128. package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
  129. package/scripts/cli/instance.ts +224 -0
  130. package/scripts/cli/main.ts +88 -18
  131. package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
  132. package/scripts/cli/port.ts +100 -0
  133. package/scripts/cli/{support/services.ts → services.ts} +88 -88
  134. package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
  135. package/scripts/cli/{command/start.ts → start.ts} +10 -5
  136. package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
  137. package/scripts/cli/tray/index.ts +62 -12
  138. package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
  139. package/scripts/cli/update.ts +40 -0
  140. package/scripts/cli/version.ts +67 -0
  141. package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
  142. package/src/boot/app.ts +11 -7
  143. package/src/boot/dependencies.ts +50 -19
  144. package/src/boot/timeout.ts +2 -13
  145. package/src/config/cooldowns.ts +186 -138
  146. package/src/config/network.ts +8 -14
  147. package/src/config/providers.ts +7 -10
  148. package/src/config/runtime.ts +76 -76
  149. package/src/config/security.ts +15 -0
  150. package/src/config/spec.ts +52 -7
  151. package/src/config/telemetry.ts +26 -0
  152. package/src/config/timeouts.ts +0 -6
  153. package/src/console/admin/model-abuse/contracts.ts +28 -6
  154. package/src/console/admin/model-abuse/routes.ts +56 -3
  155. package/src/console/api-keys/bucket-spend.ts +15 -4
  156. package/src/console/api-keys/contracts.ts +410 -414
  157. package/src/console/api-keys/routes.ts +881 -880
  158. package/src/console/auth/credentials.ts +3 -3
  159. package/src/console/auth/setup-service.ts +2 -2
  160. package/src/console/backup/contracts.ts +15 -2
  161. package/src/console/backup/export-endpoint.ts +6 -2
  162. package/src/console/backup/nine-router.ts +2 -5
  163. package/src/console/backup/service.ts +4 -0
  164. package/src/console/backup/store.ts +192 -29
  165. package/src/console/backup/validate.ts +3 -45
  166. package/src/console/cli-tools/fs-ops.ts +487 -487
  167. package/src/console/console-router-guards.ts +1 -1
  168. package/src/console/console-router.ts +8 -0
  169. package/src/console/data-management/contracts.ts +100 -0
  170. package/src/console/data-management/operations.ts +234 -0
  171. package/src/console/data-management/routes.ts +103 -0
  172. package/src/console/domain-registration.ts +45 -1
  173. package/src/console/observability/contracts.ts +524 -485
  174. package/src/console/observability/live.ts +22 -0
  175. package/src/console/observability/store.ts +1138 -1018
  176. package/src/console/observability/usage-dimensions.ts +4 -4
  177. package/src/console/providers/catalog/catalog-projections.ts +6 -7
  178. package/src/console/providers/catalog/contracts.ts +6 -0
  179. package/src/console/providers/catalog/model-operations.ts +5 -2
  180. package/src/console/providers/catalog/provider-operations.ts +24 -4
  181. package/src/console/providers/catalog/public-model-store.ts +8 -11
  182. package/src/console/providers/catalog/routes.ts +7 -0
  183. package/src/console/providers/catalog/store.ts +1553 -1532
  184. package/src/console/providers/oauth/routes.ts +8 -12
  185. package/src/console/quota/account-quota-shared.ts +52 -39
  186. package/src/console/quota/account-quota-tenant.ts +6 -6
  187. package/src/console/quota/quota-view.ts +1 -1
  188. package/src/console/quota/refresh.ts +441 -429
  189. package/src/console/remote-support/contracts.ts +61 -0
  190. package/src/console/remote-support/routes.ts +65 -0
  191. package/src/console/remote-support/service.ts +207 -0
  192. package/src/console/response-compression.ts +0 -3
  193. package/src/console/routing/pools/import-parse.ts +206 -0
  194. package/src/console/routing/pools/operations.ts +543 -501
  195. package/src/console/routing/pools/probe-result.ts +3 -2
  196. package/src/console/routing/pools/probe-verdict.ts +132 -132
  197. package/src/console/routing/pools/routes.ts +181 -164
  198. package/src/console/routing/pools/store.ts +8 -8
  199. package/src/console/settings/contracts.ts +95 -2
  200. package/src/console/settings/store.ts +81 -3
  201. package/src/console/shared/cursor.ts +8 -8
  202. package/src/console/shared/errors.ts +1 -3
  203. package/src/console/updates/routes.ts +16 -0
  204. package/src/main.ts +10 -0
  205. package/src/network/http2-bidi.ts +2 -4
  206. package/src/network/pool/agent.ts +478 -478
  207. package/src/network/pool/selector.ts +5 -4
  208. package/src/network/retry.ts +1 -2
  209. package/src/network/ssrf.ts +5 -4
  210. package/src/observability/event-compaction.ts +317 -0
  211. package/src/observability/event-day-index.ts +440 -0
  212. package/src/observability/event-log.ts +155 -6
  213. package/src/observability/logger.ts +6 -6
  214. package/src/observability/payload-capture.ts +158 -33
  215. package/src/observability/payload-store.ts +82 -61
  216. package/src/observability/redaction.ts +5 -6
  217. package/src/observability/request-notify.ts +43 -0
  218. package/src/observability/telemetry-status.ts +8 -9
  219. package/src/observability/token-speed.ts +5 -4
  220. package/src/persistence/db-mode.ts +7 -7
  221. package/src/persistence/health-event-flush.ts +36 -0
  222. package/src/persistence/readiness.ts +1 -1
  223. package/src/persistence/schema.ts +75 -29
  224. package/src/persistence/shared-cache.ts +1 -2
  225. package/src/persistence/stores/api-key-store.ts +18 -6
  226. package/src/persistence/stores/share-store.ts +4 -3
  227. package/src/persistence/stores/telemetry-store.ts +33 -11
  228. package/src/protocol/canonical-model.ts +11 -3
  229. package/src/protocol/codecs.ts +139 -6
  230. package/src/protocol/gateway-error.ts +6 -4
  231. package/src/protocol/image-clamp.ts +19 -1
  232. package/src/protocol/registry.ts +8 -0
  233. package/src/protocol/request/chat.ts +21 -1
  234. package/src/protocol/request/codex.ts +10 -1
  235. package/src/protocol/request/gemini.ts +9 -2
  236. package/src/protocol/request/messages.ts +18 -7
  237. package/src/protocol/request/responses.ts +9 -1
  238. package/src/protocol/response/chat.ts +34 -4
  239. package/src/protocol/response/codex.ts +7 -4
  240. package/src/protocol/response/dsml.ts +140 -28
  241. package/src/protocol/response/responses.ts +11 -5
  242. package/src/protocol/stream-error-frames.ts +4 -5
  243. package/src/providers/adapters/compatible-adapter.ts +3 -0
  244. package/src/providers/authentication/jwt-validator.ts +9 -10
  245. package/src/providers/authentication/oauth-refresh-service.ts +9 -11
  246. package/src/providers/bundled-discovery-providers.ts +122 -119
  247. package/src/providers/configured-openai-providers.ts +4 -4
  248. package/src/providers/default-registry.ts +5 -4
  249. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  250. package/src/providers/discovery/catalog/catalog-format.ts +3 -6
  251. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  252. package/src/providers/discovery/openai-model-discovery.ts +5 -0
  253. package/src/providers/discovery/openrouter-free-roster.ts +15 -17
  254. package/src/providers/discovery/probe-phases.ts +4 -4
  255. package/src/providers/discovery/probe-wire.ts +6 -13
  256. package/src/providers/discovery/probing-service.ts +1117 -1045
  257. package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
  258. package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
  259. package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
  260. package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
  261. package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
  262. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
  263. package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
  264. package/src/providers/integrations/buddy/buddy-media.ts +2 -2
  265. package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
  266. package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
  267. package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
  268. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
  269. package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
  270. package/src/providers/integrations/claude/claude.ts +608 -608
  271. package/src/providers/integrations/cline/cline-quota.ts +27 -20
  272. package/src/providers/integrations/cline/cline.ts +537 -539
  273. package/src/providers/integrations/codex/codex-headers.ts +25 -39
  274. package/src/providers/integrations/codex/codex-oauth.ts +1 -1
  275. package/src/providers/integrations/codex/codex.ts +2 -29
  276. package/src/providers/integrations/commandcode/commandcode.ts +674 -675
  277. package/src/providers/integrations/commandcode/discovery.ts +117 -116
  278. package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
  279. package/src/providers/integrations/cursor/cursor-quota.ts +309 -0
  280. package/src/providers/integrations/cursor/cursor-request.ts +616 -471
  281. package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
  282. package/src/providers/integrations/cursor/cursor.ts +1333 -960
  283. package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
  284. package/src/providers/integrations/factory/factory-models.ts +3 -5
  285. package/src/providers/integrations/factory/factory.ts +8 -1
  286. package/src/providers/integrations/gemini.ts +6 -1
  287. package/src/providers/integrations/grok/grok-install-id.ts +2 -3
  288. package/src/providers/integrations/grok/grok-quota.ts +279 -177
  289. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
  290. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
  291. package/src/providers/integrations/grok/grok.ts +6 -7
  292. package/src/providers/integrations/hermes.ts +14 -18
  293. package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
  294. package/src/providers/integrations/kiro/kiro.ts +1 -1
  295. package/src/providers/integrations/mistral.ts +6 -6
  296. package/src/providers/integrations/opencode/fingerprint.ts +3 -3
  297. package/src/providers/integrations/opencode/provider.ts +19 -20
  298. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
  299. package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
  300. package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
  301. package/src/providers/integrations/qoder/qoder.ts +884 -865
  302. package/src/providers/integrations/shared/connect.ts +6 -6
  303. package/src/providers/integrations/trae/trae-catalog.ts +1 -2
  304. package/src/providers/integrations/trae/trae-quota.ts +384 -0
  305. package/src/providers/integrations/trae/trae.ts +2 -2
  306. package/src/providers/integrations/zcode/oauth.ts +6 -1
  307. package/src/providers/operations/account-health-service.ts +75 -27
  308. package/src/providers/operations/account-reset-service.ts +1 -1
  309. package/src/providers/operations/client-versions.ts +6 -6
  310. package/src/providers/operations/custom-cli-headers.ts +3 -4
  311. package/src/providers/operations/provider-catalog-seeder.ts +95 -95
  312. package/src/providers/operations/provider-catalog-service.ts +3 -4
  313. package/src/providers/provider-contracts.ts +7 -0
  314. package/src/providers/provider-metadata.ts +10 -8
  315. package/src/providers/provider-registry.ts +6 -7
  316. package/src/providers/provider-usage.ts +1 -1
  317. package/src/providers/quota/quota-support.ts +8 -0
  318. package/src/security/abuse.ts +15 -10
  319. package/src/security/access-control.ts +144 -146
  320. package/src/security/admission/contracts.ts +11 -0
  321. package/src/security/admission/in-memory-store.ts +4 -3
  322. package/src/security/admission/lease-sweep.ts +6 -6
  323. package/src/security/admission/reasons.ts +5 -1
  324. package/src/security/admission/redis-store.ts +211 -210
  325. package/src/security/admission/service.ts +4 -0
  326. package/src/security/api-key-auth.ts +27 -17
  327. package/src/security/ip-boundary.ts +155 -15
  328. package/src/security/model-abuse.ts +366 -94
  329. package/src/security/outbound-headers.ts +3 -4
  330. package/src/shared/sleep.ts +4 -0
  331. package/src/transport/dispatch/attempt-finalize.ts +47 -5
  332. package/src/transport/dispatch/attempt-loop.ts +531 -530
  333. package/src/transport/dispatch/generation.ts +6 -0
  334. package/src/transport/dispatch/proxy-request.ts +24 -1
  335. package/src/transport/dispatch/ratelimit-capture.ts +37 -0
  336. package/src/transport/dispatch/stream-bridge.ts +1 -3
  337. package/src/transport/dispatch/streaming/finalize.ts +1 -1
  338. package/src/transport/dispatch/streaming/stream.ts +19 -6
  339. package/src/transport/dispatch/upstream.ts +9 -0
  340. package/src/transport/failure-policy.ts +162 -8
  341. package/src/transport/guards/auth-guard.ts +7 -4
  342. package/src/transport/guards/concurrency-guard.ts +0 -5
  343. package/src/transport/guards/error-lifecycle.ts +13 -6
  344. package/src/transport/guards/pipeline.ts +15 -3
  345. package/src/transport/guards/request-context.ts +8 -2
  346. package/src/transport/request/inflight.ts +5 -6
  347. package/src/transport/request/preparer.ts +9 -8
  348. package/src/transport/request/state.ts +21 -8
  349. package/src/transport/request/tenant-preferences.ts +60 -0
  350. package/src/transport/routing/admission.ts +4 -0
  351. package/src/transport/routing/route-catalog.ts +65 -10
  352. package/src/transport/routing/route-model.ts +46 -14
  353. package/src/transport/routing/router.ts +21 -14
  354. package/src/transport/surface/completion.ts +2 -3
  355. package/src/transport/surface/dialects.ts +5 -6
  356. package/src/transport/surface/responses/encode.ts +38 -0
  357. package/src/transport/surface/responses/parse.ts +4 -0
  358. package/src/transport/translation/capabilities.ts +2 -3
  359. package/src/transport/translation/quirks.ts +3 -3
  360. package/src/workers/buddy-growth-tasks.ts +5 -10
  361. package/src/workers/daily-checkin.ts +17 -23
  362. package/src/workers/quota-refresh-sweep.ts +26 -26
  363. package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
  364. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +0 -1
  365. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
  366. package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
  367. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js +0 -1
  368. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
  369. package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
  370. package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
  371. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +0 -1
  372. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
  373. package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
  374. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js +0 -1
  375. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
  376. package/dist/dashboard/assets/OverviewPage-B53YYMng.js +0 -1
  377. package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
  378. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +0 -3
  379. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
  380. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +0 -1
  381. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
  382. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +0 -7
  383. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
  384. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +0 -1
  385. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
  386. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +0 -1
  387. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
  388. package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
  389. package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
  390. package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
  391. package/dist/dashboard/assets/UsagePage-B47uOydr.js +0 -56
  392. package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
  393. package/dist/dashboard/assets/api-keys-uNywYlfv.js +0 -1
  394. package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
  395. package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
  396. package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
  397. package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
  398. package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
  399. package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
  400. package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
  401. package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
  402. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
  403. package/dist/dashboard/assets/dialog-BtpsJU98.js +0 -1
  404. package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
  405. package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
  406. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  407. package/dist/dashboard/assets/drawer-WwwtzihF.js +0 -1
  408. package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
  409. package/dist/dashboard/assets/entry-CkryrIea.js +0 -4
  410. package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
  411. package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
  412. package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
  413. package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
  414. package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
  415. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  416. package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
  417. package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
  418. package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
  419. package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
  420. package/dist/dashboard/assets/layout-C8mer_Ak.js +0 -1
  421. package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
  422. package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
  423. package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
  424. package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
  425. package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
  426. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  427. package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
  428. package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
  429. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  430. package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
  431. package/dist/dashboard/assets/select-BO6h8yNv.js.gz +0 -0
  432. package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
  433. package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
  434. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  435. package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
  436. package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
  437. package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
  438. package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
  439. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
  440. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  441. package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
  442. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  443. package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
  444. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  445. package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
  446. package/scripts/cli/command/admin.ts +0 -65
  447. package/scripts/cli/command/doctor.ts +0 -257
  448. package/scripts/cli/command/update.ts +0 -23
  449. package/scripts/cli/gateway/drain-token.ts +0 -78
  450. package/scripts/cli/gateway/instance-lock.ts +0 -105
  451. package/scripts/cli/gateway/stop.ts +0 -51
  452. package/scripts/cli/support/bun.ts +0 -27
  453. package/scripts/cli/support/version.ts +0 -37
  454. /package/dist/dashboard/assets/{app-CafWtVXA.js → app-BIt446yE.js} +0 -0
  455. /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-BIt446yE.js.gz} +0 -0
  456. /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
@@ -14,7 +14,7 @@ import { parseCookieValue, SESSION_COOKIE_NAME, isCsrfValid } from "../security/
14
14
  /**
15
15
  * Stateless double-submit CSRF guard for unsafe `/console/api/*` mutations.
16
16
  * Compares the `x-csrf-token` header against the readable `csrf_token`
17
- * cookie — zero database I/O, so dashboard writes no longer pay a token
17
+ * cookie — zero database I/O, so dashboard writes pay no token
18
18
  * SELECT + UPDATE on top of the session lookup, and there is no token
19
19
  * store whose outage could masquerade as an auth failure.
20
20
  */
@@ -48,6 +48,13 @@ export interface ConsoleApiCompositionDeps {
48
48
  readonly routeSnapshotService: { invalidate(): Promise<number> };
49
49
  readonly poolSelector: NetworkPoolSelector;
50
50
  readonly telemetryBuffer: TelemetryBatchBuffer;
51
+ /**
52
+ * Queued writers the console can drain on demand (telemetry, usage, health
53
+ * events). Optional for the same reason the other reduced composition
54
+ * surface is: absent means the flush control is not offered rather than the
55
+ * console failing to build.
56
+ */
57
+ readonly flushBuffers?: { flush(): Promise<void> };
51
58
  /**
52
59
  * The request state store owns the live in-flight gauge, so the console reads
53
60
  * its snapshot/subscription from here rather than a free-standing module.
@@ -189,6 +196,7 @@ export function createConsoleRouter(deps: ConsoleApiCompositionDeps): Elysia {
189
196
  admissionService: deps.admissionService,
190
197
  ...(deps.modelStrikes ? { modelStrikes: deps.modelStrikes } : {}),
191
198
  readRoutingAccountInflight: deps.readRoutingAccountInflight,
199
+ ...(deps.flushBuffers ? { flushBuffers: deps.flushBuffers } : {}),
192
200
  credentialService,
193
201
  // The backup surface re-authenticates the operator, so it needs the current
194
202
  // user's hash. Read from the session on the request that asks for it, never
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Disk housekeeping: what the telemetry and payload directories hold, and the
3
+ * reclaim actions an operator can run from the console.
4
+ *
5
+ * Everything here is files on disk, except one deliberate row-level action.
6
+ * Reclaiming a file is safe because the data it held is already past the point
7
+ * where the gateway would read it: payload frames expire after the tenant's
8
+ * chosen retention, and telemetry files past the retention window are outside
9
+ * every query's scan bound. Deleting rows is a different decision with a different
10
+ * blast radius, and the only row data the console offers purging is the health
11
+ * & error log — purely an operator's diagnostic trail with no request-path
12
+ * reader, unlike everything else in this panel.
13
+ *
14
+ * Sizes are bytes as `number`. A directory is scanned on each read rather than
15
+ * cached, because the point of the panel is to show what is true now — a
16
+ * stale size would be worse than none.
17
+ */
18
+
19
+ /** One measured category of on-disk data. */
20
+ export interface DiskUsageBucket {
21
+ /** Stable id, so the dashboard can key rows without matching on copy. */
22
+ readonly id: "telemetry_events" | "telemetry_index" | "payload_frames";
23
+ readonly label: string;
24
+ readonly bytes: number;
25
+ readonly files: number;
26
+ /** Why this data exists, shown next to the size. */
27
+ readonly note: string;
28
+ /** Bytes reclaimable right now, when that is knowable without a dry run. */
29
+ readonly reclaimableBytes: number | null;
30
+ readonly reclaimableFiles: number | null;
31
+ }
32
+
33
+ export interface DiskUsageResponse {
34
+ readonly buckets: readonly DiskUsageBucket[];
35
+ readonly totalBytes: number;
36
+ /** Retention in days; telemetry older than this is outside every scan. */
37
+ readonly retentionDays: number;
38
+ /** Payload frames live this long by default before they are reclaimable. */
39
+ readonly payloadRetentionMs: number;
40
+ /**
41
+ * The longest retention a tenant may have chosen. A frame is only counted as
42
+ * reclaimable once it is older than *this*, because a tenant who raised the
43
+ * window has frames the default would call dead while their rows still
44
+ * address them.
45
+ */
46
+ readonly payloadRetentionMaxMs: number;
47
+ /**
48
+ * Stored health & error rows, for the purge row's label. Absent when the
49
+ * composition does not wire the health-event store (a reduced console),
50
+ * which is also when the purge control is not offered.
51
+ */
52
+ readonly healthEventCount?: number;
53
+ }
54
+
55
+ export type ReclaimAction = "payload_frames" | "telemetry_retention";
56
+
57
+ export interface ReclaimResponse {
58
+ readonly action: ReclaimAction;
59
+ readonly deletedFiles: number;
60
+ readonly reclaimedBytes: number;
61
+ }
62
+
63
+ /** Row-level deletion targets. The health & error log is the only one. */
64
+ export type PurgeTarget = "health_events";
65
+
66
+ export interface PurgeResponse {
67
+ readonly target: PurgeTarget;
68
+ /** Rows actually deleted — the number the operator's decision produced. */
69
+ readonly deleted: number;
70
+ }
71
+
72
+ export interface FlushResponse {
73
+ readonly flushed: boolean;
74
+ }
75
+
76
+ /**
77
+ * A reclaim the caller is not allowed to make.
78
+ *
79
+ * Kept as a named class so the route maps it to 403 rather than letting it
80
+ * surface as a 500 — an operator asking for something out of policy is a
81
+ * normal request, not a failure.
82
+ */
83
+ export class ReclaimNotAllowedError extends Error {
84
+ readonly status = 403;
85
+
86
+ constructor(message: string) {
87
+ super(message);
88
+ this.name = "ReclaimNotAllowedError";
89
+ }
90
+ }
91
+
92
+ /** A purge whose backing store is not wired in this composition. */
93
+ export class PurgeNotWiredError extends Error {
94
+ readonly status = 501;
95
+
96
+ constructor() {
97
+ super("Health & error log purging is not offered in this deployment's console composition.");
98
+ this.name = "PurgeNotWiredError";
99
+ }
100
+ }
@@ -0,0 +1,234 @@
1
+ import { readdir, stat, unlink } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { payloadDirectory, prunePayloadFrames } from "../../observability/payload-store";
4
+ import type {
5
+ DiskUsageBucket,
6
+ DiskUsageResponse,
7
+ PurgeTarget,
8
+ ReclaimAction,
9
+ ReclaimResponse,
10
+ } from "./contracts";
11
+ import { PurgeNotWiredError, ReclaimNotAllowedError } from "./contracts";
12
+
13
+ /**
14
+ * Reads and reclaims the on-disk telemetry and payload directories, and
15
+ * purges the health & error log — the console's one row-level deletion.
16
+ *
17
+ * Reclaiming files is safe because the data is already past the point the
18
+ * gateway would read it: payload frames expire minutes after they are written,
19
+ * and telemetry past the retention window sits outside every query's scan
20
+ * bound. The health & error log purge is the deliberate exception: the rows are
21
+ * an operator's diagnostic trail that no request-path code reads, so clearing
22
+ * them costs nothing but the history itself.
23
+ *
24
+ * The telemetry directory is injected rather than read from its module
25
+ * constant so the scanner can be pointed at a fixture. The payload directory
26
+ * is not: reclaim goes through `prunePayloadFrames`, which resolves the
27
+ * directory itself, and a second path would let the two drift so the scanner
28
+ * measured one place while the prune emptied another. The health-event store
29
+ * hook is injected for the same reason: this module should know its numbers,
30
+ * not how storage reaches them.
31
+ */
32
+
33
+ export interface DataManagementConfig {
34
+ readonly telemetryDir: string;
35
+ readonly retentionDays: number;
36
+ readonly payloadRetentionMs: number;
37
+ /**
38
+ * The longest payload retention a tenant may have chosen, and therefore the
39
+ * only cutoff a frame file can be judged dead against without reading it.
40
+ * Defaults to `payloadRetentionMs` for callers that predate per-tenant
41
+ * retention, which is then the true ceiling in that deployment.
42
+ */
43
+ readonly payloadRetentionMaxMs?: number;
44
+ /**
45
+ * The health & error log store, backing the console's only row-level purge.
46
+ * Absent (a reduced console composition) means the purge is not offered
47
+ * rather than failing to boot.
48
+ */
49
+ readonly healthEvents?:
50
+ | {
51
+ count(): Promise<number>;
52
+ purgeAll(): Promise<number>;
53
+ }
54
+ | undefined;
55
+ }
56
+
57
+ interface Scan {
58
+ readonly bytes: number;
59
+ readonly files: number;
60
+ }
61
+
62
+ async function scan(
63
+ directory: string,
64
+ match: (name: string) => boolean,
65
+ modifiedBefore?: number,
66
+ ): Promise<Scan> {
67
+ const names = await readdir(directory).catch(() => [] as string[]);
68
+ let bytes = 0;
69
+ let files = 0;
70
+ for (const name of names) {
71
+ if (!match(name)) continue;
72
+ const info = await stat(join(directory, name)).catch(() => undefined);
73
+ if (!info?.isFile()) continue;
74
+ if (modifiedBefore !== undefined && info.mtimeMs >= modifiedBefore) continue;
75
+ bytes += info.size;
76
+ files += 1;
77
+ }
78
+ return { bytes, files };
79
+ }
80
+
81
+ const isIndex = (name: string): boolean => name.endsWith(".index.json");
82
+ const isHourly = (name: string): boolean => name.endsWith(".events.jsonl");
83
+ const isFrame = (name: string): boolean =>
84
+ name.endsWith(".jsonl");
85
+
86
+ /** The UTC day a telemetry file name starts with, compared to a cutoff. */
87
+ function dayBefore(name: string, cutoff: number): boolean {
88
+ const at = Date.parse(`${name.slice(0, 10)}T00:00:00Z`);
89
+ return Number.isFinite(at) && at < cutoff;
90
+ }
91
+
92
+ export function createDataManagementOperations(config: DataManagementConfig) {
93
+ const payloadDir = (): string => payloadDirectory();
94
+ /**
95
+ * The only cutoff a frame file can be judged dead against without parsing it.
96
+ *
97
+ * A file's mtime says when it was last written, not what its frames' tenants
98
+ * chose — so a file written an hour ago can hold a frame with a week to live.
99
+ * The ceiling is therefore the bound, and the default retention is only used
100
+ * when no ceiling was configured.
101
+ */
102
+ const payloadMax = (): number => config.payloadRetentionMaxMs ?? config.payloadRetentionMs;
103
+
104
+ async function usage(): Promise<DiskUsageResponse> {
105
+ const retentionCutoff = Date.now() - config.retentionDays * 24 * 60 * 60_000;
106
+ const payloadCutoff = Date.now() - payloadMax();
107
+
108
+ const [hourly, compacted, staleCompacted, frames, staleFrames, healthEventCount] = await Promise.all([
109
+ scan(config.telemetryDir, isHourly),
110
+ scan(config.telemetryDir, isIndex),
111
+ scan(config.telemetryDir, (name) => isIndex(name) && dayBefore(name, retentionCutoff)),
112
+ scan(payloadDir(), isFrame),
113
+ scan(payloadDir(), isFrame, payloadCutoff),
114
+ config.healthEvents?.count(),
115
+ ]);
116
+
117
+ const buckets: DiskUsageBucket[] = [
118
+ {
119
+ id: "telemetry_events",
120
+ label: "Request event files",
121
+ bytes: hourly.bytes,
122
+ files: hourly.files,
123
+ note: "Feeds the request list and detail drawer.",
124
+ reclaimableBytes: null,
125
+ reclaimableFiles: null,
126
+ },
127
+ {
128
+ id: "telemetry_index",
129
+ label: "Day indexes",
130
+ bytes: compacted.bytes,
131
+ files: compacted.files,
132
+ note: "Feeds usage totals, breakdowns and the chart.",
133
+ reclaimableBytes: staleCompacted.bytes,
134
+ reclaimableFiles: staleCompacted.files,
135
+ },
136
+ {
137
+ id: "payload_frames",
138
+ label: "Payload frames",
139
+ bytes: frames.bytes,
140
+ files: frames.files,
141
+ note: `Captured bodies, expiring ${Math.round(
142
+ config.payloadRetentionMs / 60_000,
143
+ )} min after write by default.`,
144
+ reclaimableBytes: staleFrames.bytes,
145
+ reclaimableFiles: staleFrames.files,
146
+ },
147
+ ];
148
+
149
+ return {
150
+ buckets,
151
+ totalBytes: buckets.reduce((sum, bucket) => sum + bucket.bytes, 0),
152
+ retentionDays: config.retentionDays,
153
+ payloadRetentionMs: config.payloadRetentionMs,
154
+ payloadRetentionMaxMs: payloadMax(),
155
+ ...(healthEventCount === undefined ? {} : { healthEventCount }),
156
+ };
157
+ }
158
+
159
+ async function reclaim(action: ReclaimAction): Promise<ReclaimResponse> {
160
+ if (action === "payload_frames") {
161
+ // Delegated rather than reimplemented: frame expiry lives inside each
162
+ // file, so a file's mtime alone cannot say whether it still holds a live
163
+ // frame. Measuring around the call is how the bytes are reported without
164
+ // duplicating that rule.
165
+ const before = await scan(payloadDir(), isFrame);
166
+ // `before` is the expiry bound: a frame still live at this instant is
167
+ // kept. The second argument covers files that cannot be parsed — one
168
+ // written after that bound might still hold a live frame in the part
169
+ // that failed to parse, so it is left for a later pass.
170
+ const now = new Date();
171
+ const deletedFiles = await prunePayloadFrames(
172
+ now,
173
+ new Date(now.getTime() - payloadMax()),
174
+ );
175
+ const after = await scan(payloadDir(), isFrame);
176
+ return {
177
+ action,
178
+ deletedFiles,
179
+ reclaimedBytes: Math.max(0, before.bytes - after.bytes),
180
+ };
181
+ }
182
+ if (action === "telemetry_retention") {
183
+ // Only day indexes past retention, and only together with their events.
184
+ // Deleting one without the other is the failure this avoids: an index
185
+ // with no events reports totals for requests that are gone.
186
+ const cutoff = Date.now() - config.retentionDays * 24 * 60 * 60_000;
187
+ return unlinkWhere(action, config.telemetryDir, (name) =>
188
+ (isIndex(name) || isHourly(name)) && dayBefore(name, cutoff),
189
+ );
190
+ }
191
+ throw new ReclaimNotAllowedError(`Unknown reclaim action: ${action}`);
192
+ }
193
+
194
+ /** The one row-level deletion the console offers. */
195
+ async function purge(target: PurgeTarget): Promise<number> {
196
+ switch (target) {
197
+ case "health_events": {
198
+ const store = config.healthEvents;
199
+ if (store === undefined) throw new PurgeNotWiredError();
200
+ return await store.purgeAll();
201
+ }
202
+ }
203
+ }
204
+
205
+ return { usage, reclaim, purge };
206
+ }
207
+
208
+ async function unlinkWhere(
209
+ action: ReclaimAction,
210
+ directory: string,
211
+ match: (name: string) => boolean,
212
+ ): Promise<ReclaimResponse> {
213
+ const names = await readdir(directory).catch(() => [] as string[]);
214
+ let deletedFiles = 0;
215
+ let reclaimedBytes = 0;
216
+ for (const name of names) {
217
+ if (!match(name)) continue;
218
+ const path = join(directory, name);
219
+ // Measured before the unlink, since the size is gone afterwards.
220
+ const size = await stat(path)
221
+ .then((value) => value.size)
222
+ .catch(() => 0);
223
+ const removed = await unlink(path).then(
224
+ () => true,
225
+ () => false,
226
+ );
227
+ if (!removed) continue;
228
+ deletedFiles += 1;
229
+ reclaimedBytes += size;
230
+ }
231
+ // The response names the action, so a caller that fired two reclaims can
232
+ // tell which result is which rather than both reading the same thing.
233
+ return { action, deletedFiles, reclaimedBytes };
234
+ }
@@ -0,0 +1,103 @@
1
+ import { Elysia } from "elysia";
2
+ import { t } from "elysia";
3
+ import { errorResponse, requireAnyScope } from "../shared/errors";
4
+ import type { AccessDecision } from "../../security/access-control";
5
+ import { createDataManagementOperations, type DataManagementConfig } from "./operations";
6
+ import type { PurgeTarget, ReclaimAction, ReclaimResponse } from "./contracts";
7
+
8
+ const WRITE_SCOPES = ["dashboard:write"] as const;
9
+
10
+ const reclaimBody = t.Object({
11
+ action: t.Union([
12
+ t.Literal("payload_frames"),
13
+ t.Literal("telemetry_retention"),
14
+ ]),
15
+ });
16
+
17
+ const purgeBody = t.Object({
18
+ target: t.Literal("health_events"),
19
+ });
20
+
21
+ /**
22
+ * Console routes for disk housekeeping.
23
+ *
24
+ * Reclaim and flush both need `dashboard:write`. Reading is deliberately the
25
+ * same scope as the rest of the console rather than something narrower: the
26
+ * sizes are not sensitive, but splitting the two would mean a caller who can
27
+ * already write could not see what they are about to affect.
28
+ */
29
+ export function createDataManagementRoutes(
30
+ config: DataManagementConfig & {
31
+ readonly accessResolver: (request: Request) => AccessDecision | undefined;
32
+ readonly auditSink?: {
33
+ record(entry: {
34
+ readonly access: AccessDecision;
35
+ readonly action: string;
36
+ readonly target: string | null;
37
+ readonly detail?: Record<string, unknown>;
38
+ }): Promise<void> | void;
39
+ } | undefined;
40
+ readonly flusher?: { flush(): Promise<void> } | undefined;
41
+ },
42
+ ): Elysia {
43
+ const operations = createDataManagementOperations(config);
44
+
45
+ return new Elysia({ prefix: "/data-management" })
46
+ .get("/usage", async ({ request, set }) => {
47
+ try {
48
+ const access = requireAnyScope(config.accessResolver(request), WRITE_SCOPES);
49
+ void access;
50
+ return await operations.usage();
51
+ } catch (error) {
52
+ return errorResponse(error, set, "Could not read disk usage");
53
+ }
54
+ })
55
+ .post("/reclaim", { body: reclaimBody }, async ({ request, body, set }) => {
56
+ try {
57
+ const access = requireAnyScope(config.accessResolver(request), WRITE_SCOPES);
58
+ const action = body.action as ReclaimAction;
59
+ const result: ReclaimResponse = await operations.reclaim(action);
60
+ await config.auditSink?.record({
61
+ access,
62
+ action: `data_management.reclaim.${action}`,
63
+ target: access.tenantId ?? null,
64
+ detail: { deletedFiles: result.deletedFiles, reclaimedBytes: result.reclaimedBytes },
65
+ });
66
+ set.status = 200;
67
+ return result;
68
+ } catch (error) {
69
+ return errorResponse(error, set, "Reclaim failed");
70
+ }
71
+ })
72
+ .post("/flush", async ({ request, set }) => {
73
+ try {
74
+ const access = requireAnyScope(config.accessResolver(request), WRITE_SCOPES);
75
+ await config.flusher?.flush();
76
+ await config.auditSink?.record({
77
+ access,
78
+ action: "data_management.flush",
79
+ target: access.tenantId ?? null,
80
+ });
81
+ set.status = 200;
82
+ return { flushed: true };
83
+ } catch (error) {
84
+ return errorResponse(error, set, "Flush failed");
85
+ }
86
+ })
87
+ .post("/purge", { body: purgeBody }, async ({ request, body, set }) => {
88
+ try {
89
+ const access = requireAnyScope(config.accessResolver(request), WRITE_SCOPES);
90
+ const deleted: number = await operations.purge(body.target as PurgeTarget);
91
+ await config.auditSink?.record({
92
+ access,
93
+ action: `data_management.purge.${body.target}`,
94
+ target: access.tenantId ?? null,
95
+ detail: { deleted },
96
+ });
97
+ set.status = 200;
98
+ return { target: body.target, deleted } satisfies { target: PurgeTarget; deleted: number };
99
+ } catch (error) {
100
+ return errorResponse(error, set, "Purge failed");
101
+ }
102
+ }) as unknown as Elysia;
103
+ }
@@ -23,6 +23,9 @@ import { createNetworkPoolRoutes } from "./routing/pools/routes";
23
23
  import { DrizzleModelRoutingStore } from "./routing/model/store";
24
24
  import { DrizzleNetworkPoolStore } from "./routing/pools/store";
25
25
  import { createRuntimeSettingsRoutes } from "./settings/routes";
26
+ import { createDataManagementRoutes } from "./data-management/routes";
27
+ import { healthEventBuffer } from "../persistence/health-event-flush";
28
+ import { countAllHealthEvents, deleteAllHealthEvents } from "../providers/operations/account-health-service";
26
29
  import { DrizzleRuntimeSettingsStore } from "./settings/store";
27
30
  import { createObservabilityRoutes } from "./observability/routes";
28
31
  import { DrizzleObservabilityStore } from "./observability/store";
@@ -33,9 +36,15 @@ import { createModelAbuseRoutes } from "./admin/model-abuse/routes";
33
36
  import { DrizzleAuditReadStore } from "./audit/store";
34
37
  import { createPerformanceRoutes } from "./observability/performance";
35
38
  import { createUpdateRoutes } from "./updates/routes";
39
+ import { createRemoteSupportRoutes } from "./remote-support/routes";
40
+ import { createRemoteSupportService } from "./remote-support/service";
41
+ import { resolvePort } from "../config/runtime";
36
42
  import { createBackupRoutes } from "./backup/routes";
37
43
  import { BackupService } from "./backup/service";
38
44
  import type { ConsoleCredentialService } from "./auth/credentials";
45
+ import { resolveTelemetryRetentionDays } from "../config/telemetry";
46
+ import { MAX_PAYLOAD_RETENTION_MS, payloadRetentionMs } from "../observability/payload-capture";
47
+ import { TELEMETRY_EVENT_DIR } from "../observability/event-log";
39
48
 
40
49
  import { InMemoryOAuthFlowStore, OAuthFlowStore } from "../providers/authentication/oauth-flow-store";
41
50
  import { OAuthCallbackListener } from "./providers/oauth/callback-listener";
@@ -80,6 +89,14 @@ export interface ConsoleDomainContext {
80
89
  readonly routeSnapshotService: { invalidate(): Promise<number> };
81
90
  readonly poolSelector: NetworkPoolSelector;
82
91
  readonly telemetryBuffer: TelemetryBatchBuffer;
92
+ /**
93
+ * Queued writers the console can drain on demand. Optional because a flush
94
+ * is only meaningful when the process owns the buffers; a caller that does
95
+ * not pass them simply loses the button rather than failing to boot.
96
+ */
97
+ readonly flushBuffers?: {
98
+ flush(): Promise<void>;
99
+ };
83
100
  /** Owns the live in-flight gauge the console surfaces. */
84
101
  readonly stateStore?: LiveInFlightSource;
85
102
  readonly admissionService: Pick<ApiKeyAdmissionService, "purgeKey">;
@@ -227,6 +244,10 @@ export function registerConsoleDomains(
227
244
  accessResolver: ctx.accessResolver,
228
245
  observabilityStore,
229
246
  }));
247
+ console.use(createRemoteSupportRoutes({
248
+ accessResolver: ctx.accessResolver,
249
+ service: createRemoteSupportService({ port: resolvePort() }),
250
+ }));
230
251
  console.use(createAuditRoutes({ store: auditReadStore, accessResolver: ctx.accessResolver }));
231
252
  if (ctx.modelStrikes) {
232
253
  console.use(createModelAbuseRoutes({
@@ -267,7 +288,30 @@ export function registerConsoleDomains(
267
288
  // same SSRF-validated fetch as every other egress.
268
289
  relayFetch: createValidatedFetch({ policy: resolveSsrfPolicy() }),
269
290
  }));
270
- console.use(createRuntimeSettingsRoutes({ store: runtimeSettingsStore, accessResolver: ctx.accessResolver, auditSink: ctx.auditRecorder }));
291
+ console.use(createRuntimeSettingsRoutes({ store: runtimeSettingsStore, accessResolver: ctx.accessResolver, auditSink: ctx.auditRecorder, snapshotInvalidator: ctx.routeSnapshotService }));
292
+ console.use(
293
+ createDataManagementRoutes({
294
+ telemetryDir: TELEMETRY_EVENT_DIR,
295
+ retentionDays: resolveTelemetryRetentionDays(),
296
+ payloadRetentionMs: payloadRetentionMs(),
297
+ payloadRetentionMaxMs: MAX_PAYLOAD_RETENTION_MS,
298
+ accessResolver: ctx.accessResolver,
299
+ auditSink: ctx.auditRecorder,
300
+ flusher: ctx.flushBuffers,
301
+ // The console's one row-level purge: the health & error log is a
302
+ // diagnostic trail no request-path code reads. Dropping the buffered
303
+ // writes before the DELETE is what makes it sound — otherwise a batch
304
+ // in flight would land behind the purge and resurrect history that was
305
+ // just counted as deleted.
306
+ healthEvents: {
307
+ count: () => countAllHealthEvents(ctx.db),
308
+ purgeAll: async () => {
309
+ await healthEventBuffer()?.drain();
310
+ return await deleteAllHealthEvents(ctx.db);
311
+ },
312
+ },
313
+ }),
314
+ );
271
315
  console.use(
272
316
  createBackupRoutes({
273
317
  accessResolver: ctx.accessResolver,