cartethyia 2.5.2 → 2.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/dist/dashboard/assets/{BannedPage-MsQaeQTs.js → BannedPage-C2orBsI2.js} +1 -1
  2. package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
  3. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +1 -0
  4. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolsPage-DfWHTJ7v.js → CliToolsPage-BV6pxVQo.js} +1 -1
  6. package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
  7. package/dist/dashboard/assets/{CombosPage-BrCBQDAc.js → CombosPage-Bsan4679.js} +1 -1
  8. package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
  9. package/dist/dashboard/assets/{ConsoleLogPage-C6Uh33er.js → ConsoleLogPage-CW-wPmJ6.js} +1 -1
  10. package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
  11. package/dist/dashboard/assets/{CustomizationPage-8aS9hyJu.js → CustomizationPage-0p8uX7vB.js} +1 -1
  12. package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
  13. package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js +1 -0
  14. package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
  15. package/dist/dashboard/assets/{LoginPage-C8hKyXBe.js → LoginPage-BvzCx8Kr.js} +1 -1
  16. package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
  17. package/dist/dashboard/assets/{ModelPicker-CWkTUtpX.js → ModelPicker-BY1aKEkP.js} +1 -1
  18. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
  19. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +1 -0
  20. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
  21. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +3 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
  23. package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js +1 -0
  24. package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
  25. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +7 -0
  26. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
  27. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +1 -0
  28. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
  29. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +1 -0
  30. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
  31. package/dist/dashboard/assets/{SetupPage-CXjUlfqr.js → SetupPage-DDbMQsoo.js} +1 -1
  32. package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
  33. package/dist/dashboard/assets/{SortableList-C-_oD3F2.js → SortableList-CB3Z7VTf.js} +1 -1
  34. package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
  35. package/dist/dashboard/assets/{StudioPage-u9IxmYOI.js → StudioPage-D95UCPHI.js} +3 -3
  36. package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
  37. package/dist/dashboard/assets/{UsagePage-BEVLVrwP.js → UsagePage-Cnae9SWA.js} +13 -13
  38. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
  39. package/dist/dashboard/assets/{api-keys-uNywYlfv.js → api-keys-VY68Mhd1.js} +1 -1
  40. package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
  41. package/dist/dashboard/assets/app-COlK95oi.css +2 -0
  42. package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
  43. package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-Chi7pCnt.js} +1 -1
  44. package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
  45. package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-D4pQapKj.js} +1 -1
  46. package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
  47. package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-DH6cg-_B.js} +1 -1
  48. package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
  49. package/dist/dashboard/assets/{card--AtFedAq.js → card-BKqlUOe4.js} +1 -1
  50. package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
  51. package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-fTgljTGA.js} +1 -1
  52. package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
  53. package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-D5Ys_sM0.js} +1 -1
  54. package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
  55. package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
  56. package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
  57. package/dist/dashboard/assets/{dialog-BtpsJU98.js → dialog-BYj-8WuK.js} +1 -1
  58. package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
  59. package/dist/dashboard/assets/{drawer-WwwtzihF.js → drawer-CmXIZ6H1.js} +1 -1
  60. package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
  61. package/dist/dashboard/assets/entry-CBg5zeu8.js +1 -0
  62. package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
  63. package/dist/dashboard/assets/entry-CQBr6U9c.js +1 -0
  64. package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-CgdSAzEV.js +4 -0
  66. package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
  67. package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-BlptL8Uf.js} +1 -1
  68. package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
  69. package/dist/dashboard/assets/{index-DCBtROoK.js → index-js6TJvdr.js} +2 -2
  70. package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
  71. package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-DUWDSmDV.js} +1 -1
  72. package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
  73. package/dist/dashboard/assets/{input-BG41et9G.js → input-LVjgZq4f.js} +1 -1
  74. package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
  75. package/dist/dashboard/assets/{layout-C8mer_Ak.js → layout-CPfxpsfy.js} +1 -1
  76. package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
  77. package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-DupvLfDx.js} +1 -1
  78. package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
  79. package/dist/dashboard/assets/{live-BJGYRBnR.js → live-C4pEJUqI.js} +1 -1
  80. package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
  81. package/dist/dashboard/assets/model-bans-BRiuufkB.js +1 -0
  82. package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
  83. package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-B4ROwk8L.js} +1 -1
  84. package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
  85. package/dist/dashboard/assets/numbers-YNcHvRY7.js +2 -0
  86. package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
  87. package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-gmjzC3RS.js} +1 -1
  88. package/dist/dashboard/assets/{query-keys-D7wX2vM5.js.gz → query-keys-gmjzC3RS.js.gz} +0 -0
  89. package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-C7tBT5o-.js} +1 -1
  90. package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
  91. package/dist/dashboard/assets/{select-DsX5QT8K.js → select-WUCXorXw.js} +2 -2
  92. package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
  93. package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-C_zKl1GS.js} +1 -1
  94. package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
  95. package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-_3O0F8Xn.js} +1 -1
  96. package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
  97. package/dist/dashboard/assets/{state-DYdQmdAl.js → state-BgTxHcJS.js} +1 -1
  98. package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
  99. package/dist/dashboard/assets/switch-DEjTtOWf.js +1 -0
  100. package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
  101. package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
  102. package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
  103. package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-BWSC0UAS.js} +1 -1
  104. package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
  105. package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
  106. package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
  107. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  108. package/dist/dashboard/index.html +1 -1
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +54 -54
  111. package/package.json +1 -1
  112. package/scripts/cli/autostart.ts +237 -0
  113. package/scripts/cli/command/console-menu.ts +244 -246
  114. package/scripts/cli/command/tray.ts +208 -204
  115. package/scripts/cli/command/update.ts +27 -10
  116. package/scripts/cli/main.ts +28 -1
  117. package/src/boot/app.ts +693 -693
  118. package/src/boot/dependencies.ts +646 -616
  119. package/src/config/spec.ts +81 -2
  120. package/src/config/telemetry.ts +46 -0
  121. package/src/console/api-keys/bucket-spend.ts +15 -4
  122. package/src/console/backup/contracts.ts +15 -2
  123. package/src/console/backup/routes.ts +52 -1
  124. package/src/console/backup/service.ts +60 -4
  125. package/src/console/console-router.ts +8 -0
  126. package/src/console/data-management/contracts.ts +93 -0
  127. package/src/console/data-management/operations.ts +229 -0
  128. package/src/console/data-management/routes.ts +104 -0
  129. package/src/console/domain-registration.ts +39 -0
  130. package/src/console/observability/contracts.ts +524 -485
  131. package/src/console/observability/store.ts +1115 -1018
  132. package/src/console/providers/catalog/contracts.ts +6 -0
  133. package/src/console/providers/catalog/provider-operations.ts +20 -0
  134. package/src/console/providers/catalog/routes.ts +7 -0
  135. package/src/console/providers/catalog/store.ts +22 -1
  136. package/src/console/quota/account-quota-shared.ts +52 -39
  137. package/src/console/quota/refresh.ts +441 -429
  138. package/src/console/routing/pools/import-parse.ts +206 -0
  139. package/src/console/routing/pools/operations.ts +543 -501
  140. package/src/console/routing/pools/routes.ts +181 -164
  141. package/src/console/settings/contracts.ts +37 -0
  142. package/src/console/settings/store.ts +51 -0
  143. package/src/console/share/share-router.ts +6 -9
  144. package/src/console/updates/routes.ts +16 -0
  145. package/src/main.ts +10 -0
  146. package/src/observability/event-compaction.ts +315 -0
  147. package/src/observability/event-day-index.ts +442 -0
  148. package/src/observability/event-log.ts +152 -3
  149. package/src/observability/payload-capture.ts +373 -344
  150. package/src/observability/payload-store.ts +440 -366
  151. package/src/observability/telemetry-status.ts +28 -16
  152. package/src/persistence/health-event-flush.ts +36 -0
  153. package/src/persistence/schema.ts +1402 -1360
  154. package/src/persistence/stores/api-key-store.ts +6 -0
  155. package/src/persistence/stores/share-store.ts +583 -556
  156. package/src/persistence/stores/telemetry-store.ts +237 -214
  157. package/src/protocol/codecs.ts +1 -2
  158. package/src/protocol/image-clamp.ts +19 -1
  159. package/src/protocol/registry.ts +8 -0
  160. package/src/protocol/request/gemini.ts +9 -2
  161. package/src/protocol/request/messages.ts +14 -2
  162. package/src/protocol/response/gemini.ts +18 -4
  163. package/src/providers/adapters/compatible-adapter.ts +3 -0
  164. package/src/providers/bundled-discovery-providers.ts +122 -119
  165. package/src/providers/configured-openai-providers.ts +136 -134
  166. package/src/providers/default-registry.ts +5 -4
  167. package/src/providers/discovery/openai-model-discovery.ts +5 -0
  168. package/src/providers/discovery/openrouter-free-roster.ts +15 -17
  169. package/src/providers/integrations/antigravity/antigravity-quota.ts +11 -3
  170. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +4 -4
  171. package/src/providers/integrations/buddy/workbuddy/shared.ts +201 -200
  172. package/src/providers/integrations/cline/cline-quota.ts +132 -128
  173. package/src/providers/integrations/codex/codex-headers.ts +25 -39
  174. package/src/providers/integrations/codex/codex.ts +2 -29
  175. package/src/providers/integrations/cursor/cursor-quota.ts +310 -0
  176. package/src/providers/integrations/cursor/cursor-request.ts +616 -471
  177. package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
  178. package/src/providers/integrations/cursor/cursor.ts +1485 -960
  179. package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
  180. package/src/providers/integrations/factory/factory.ts +8 -1
  181. package/src/providers/integrations/gemini.ts +6 -1
  182. package/src/providers/integrations/grok/grok-quota.ts +279 -177
  183. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
  184. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
  185. package/src/providers/integrations/opencode/provider.ts +531 -532
  186. package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
  187. package/src/providers/integrations/tokenharbor.ts +2 -2
  188. package/src/providers/integrations/trae/trae-catalog.ts +5 -3
  189. package/src/providers/integrations/trae/trae-quota.ts +384 -0
  190. package/src/providers/operations/account-health-service.ts +1523 -1457
  191. package/src/providers/operations/client-versions.ts +9 -2
  192. package/src/providers/provider-contracts.ts +7 -0
  193. package/src/providers/provider-metadata.ts +13 -1
  194. package/src/providers/quota/quota-support.ts +8 -0
  195. package/src/security/api-key-auth.ts +504 -494
  196. package/src/transport/dispatch/attempt-finalize.ts +46 -5
  197. package/src/transport/dispatch/generation.ts +6 -0
  198. package/src/transport/dispatch/proxy-request.ts +33 -1
  199. package/src/transport/dispatch/ratelimit-capture.ts +37 -0
  200. package/src/transport/dispatch/streaming/stream.ts +30 -2
  201. package/src/transport/dispatch/upstream.ts +9 -0
  202. package/src/transport/request/preparer.ts +7 -6
  203. package/src/transport/request/state.ts +9 -0
  204. package/src/transport/request/tenant-preferences.ts +60 -0
  205. package/src/transport/routing/route-catalog.ts +854 -799
  206. package/dist/dashboard/assets/BannedPage-MsQaeQTs.js.gz +0 -0
  207. package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js +0 -1
  208. package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js.gz +0 -0
  209. package/dist/dashboard/assets/CliToolsPage-DfWHTJ7v.js.gz +0 -0
  210. package/dist/dashboard/assets/CombosPage-BrCBQDAc.js.gz +0 -0
  211. package/dist/dashboard/assets/ConsoleLogPage-C6Uh33er.js.gz +0 -0
  212. package/dist/dashboard/assets/CustomizationPage-8aS9hyJu.js.gz +0 -0
  213. package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js +0 -1
  214. package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js.gz +0 -0
  215. package/dist/dashboard/assets/LoginPage-C8hKyXBe.js.gz +0 -0
  216. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
  217. package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js +0 -1
  218. package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js.gz +0 -0
  219. package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js +0 -3
  220. package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js.gz +0 -0
  221. package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js +0 -1
  222. package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js.gz +0 -0
  223. package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js +0 -7
  224. package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js.gz +0 -0
  225. package/dist/dashboard/assets/QuotaPage-BihkgQSq.js +0 -1
  226. package/dist/dashboard/assets/QuotaPage-BihkgQSq.js.gz +0 -0
  227. package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js +0 -1
  228. package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js.gz +0 -0
  229. package/dist/dashboard/assets/SetupPage-CXjUlfqr.js.gz +0 -0
  230. package/dist/dashboard/assets/SortableList-C-_oD3F2.js.gz +0 -0
  231. package/dist/dashboard/assets/StudioPage-u9IxmYOI.js.gz +0 -0
  232. package/dist/dashboard/assets/UsagePage-BEVLVrwP.js.gz +0 -0
  233. package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
  234. package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
  235. package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
  236. package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
  237. package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
  238. package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
  239. package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
  240. package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
  241. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
  242. package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
  243. package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
  244. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  245. package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
  246. package/dist/dashboard/assets/entry-Dj9AR0gY.js +0 -4
  247. package/dist/dashboard/assets/entry-Dj9AR0gY.js.gz +0 -0
  248. package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
  249. package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
  250. package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
  251. package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
  252. package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
  253. package/dist/dashboard/assets/index-DCBtROoK.js.gz +0 -0
  254. package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
  255. package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
  256. package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
  257. package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
  258. package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
  259. package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
  260. package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
  261. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  262. package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
  263. package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
  264. package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
  265. package/dist/dashboard/assets/select-DsX5QT8K.js.gz +0 -0
  266. package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
  267. package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
  268. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  269. package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
  270. package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
  271. package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
  272. package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
  273. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
  274. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  275. package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
  276. /package/dist/dashboard/assets/{app-CafWtVXA.js → app-DCQZRmHY.js} +0 -0
  277. /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-DCQZRmHY.js.gz} +0 -0
@@ -0,0 +1,229 @@
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
+ * Whether hourly files may be dropped from the console. Off unless a caller
39
+ * opts in: an hourly file still inside the compaction window is the only
40
+ * copy of its requests, so removing one loses real history.
41
+ */
42
+ readonly allowHourlyReclaim: boolean;
43
+ /**
44
+ * The health & error log store, backing the console's only row-level purge.
45
+ * Absent (a reduced console composition) means the purge is not offered
46
+ * rather than failing to boot.
47
+ */
48
+ readonly healthEvents?:
49
+ | {
50
+ count(): Promise<number>;
51
+ purgeAll(): Promise<number>;
52
+ }
53
+ | undefined;
54
+ }
55
+
56
+ interface Scan {
57
+ readonly bytes: number;
58
+ readonly files: number;
59
+ }
60
+
61
+ async function scan(
62
+ directory: string,
63
+ match: (name: string) => boolean,
64
+ modifiedBefore?: number,
65
+ ): Promise<Scan> {
66
+ const names = await readdir(directory).catch(() => [] as string[]);
67
+ let bytes = 0;
68
+ let files = 0;
69
+ for (const name of names) {
70
+ if (!match(name)) continue;
71
+ const info = await stat(join(directory, name)).catch(() => undefined);
72
+ if (!info?.isFile()) continue;
73
+ if (modifiedBefore !== undefined && info.mtimeMs >= modifiedBefore) continue;
74
+ bytes += info.size;
75
+ files += 1;
76
+ }
77
+ return { bytes, files };
78
+ }
79
+
80
+ const isIndex = (name: string): boolean => name.endsWith(".index.json");
81
+ const isHourly = (name: string): boolean => name.endsWith(".events.jsonl");
82
+ // Both extensions: old `.jsonb` files are still on disk and still counted.
83
+ const isFrame = (name: string): boolean =>
84
+ name.endsWith(".jsonl") || name.endsWith(".jsonb");
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
+ async function usage(): Promise<DiskUsageResponse> {
96
+ const retentionCutoff = Date.now() - config.retentionDays * 24 * 60 * 60_000;
97
+ const payloadCutoff = Date.now() - config.payloadRetentionMs;
98
+
99
+ const [hourly, compacted, staleCompacted, frames, staleFrames, healthEventCount] = await Promise.all([
100
+ scan(config.telemetryDir, isHourly),
101
+ scan(config.telemetryDir, isIndex),
102
+ scan(config.telemetryDir, (name) => isIndex(name) && dayBefore(name, retentionCutoff)),
103
+ scan(payloadDir(), isFrame),
104
+ scan(payloadDir(), isFrame, payloadCutoff),
105
+ config.healthEvents?.count(),
106
+ ]);
107
+
108
+ const buckets: DiskUsageBucket[] = [
109
+ {
110
+ id: "telemetry_hourly",
111
+ label: "Daily request files",
112
+ bytes: hourly.bytes,
113
+ files: hourly.files,
114
+ note: "Feeds the request list and detail drawer.",
115
+ reclaimableBytes: null,
116
+ reclaimableFiles: null,
117
+ },
118
+ {
119
+ id: "telemetry_compacted",
120
+ label: "Day indexes",
121
+ bytes: compacted.bytes,
122
+ files: compacted.files,
123
+ note: "Feeds usage totals, breakdowns and the chart.",
124
+ reclaimableBytes: staleCompacted.bytes,
125
+ reclaimableFiles: staleCompacted.files,
126
+ },
127
+ {
128
+ id: "payload_frames",
129
+ label: "Payload frames",
130
+ bytes: frames.bytes,
131
+ files: frames.files,
132
+ note: `Captured bodies, expiring ${Math.round(
133
+ config.payloadRetentionMs / 60_000,
134
+ )} min after write.`,
135
+ reclaimableBytes: staleFrames.bytes,
136
+ reclaimableFiles: staleFrames.files,
137
+ },
138
+ ];
139
+
140
+ return {
141
+ buckets,
142
+ totalBytes: buckets.reduce((sum, bucket) => sum + bucket.bytes, 0),
143
+ retentionDays: config.retentionDays,
144
+ payloadRetentionMs: config.payloadRetentionMs,
145
+ ...(healthEventCount === undefined ? {} : { healthEventCount }),
146
+ };
147
+ }
148
+
149
+ async function reclaim(action: ReclaimAction): Promise<ReclaimResponse> {
150
+ if (action === "payload_frames") {
151
+ // Delegated rather than reimplemented: frame expiry lives inside each
152
+ // file, so a file's mtime alone cannot say whether it still holds a live
153
+ // frame. Measuring around the call is how the bytes are reported without
154
+ // duplicating that rule.
155
+ const before = await scan(payloadDir(), isFrame);
156
+ // `before` is the expiry bound: a frame still live at this instant is
157
+ // kept. The second argument covers files that cannot be parsed — one
158
+ // written after that bound might still hold a live frame in the part
159
+ // that failed to parse, so it is left for a later pass.
160
+ const now = new Date();
161
+ const deletedFiles = await prunePayloadFrames(
162
+ now,
163
+ new Date(now.getTime() - config.payloadRetentionMs),
164
+ );
165
+ const after = await scan(payloadDir(), isFrame);
166
+ return {
167
+ action,
168
+ deletedFiles,
169
+ reclaimedBytes: Math.max(0, before.bytes - after.bytes),
170
+ };
171
+ }
172
+ if (action === "telemetry_retention") {
173
+ // Only day indexes past retention, and only together with their events.
174
+ // Deleting one without the other is the failure this avoids: an index
175
+ // with no events reports totals for requests that are gone.
176
+ const cutoff = Date.now() - config.retentionDays * 24 * 60 * 60_000;
177
+ return unlinkWhere(action, config.telemetryDir, (name) =>
178
+ (isIndex(name) || isHourly(name)) && dayBefore(name, cutoff),
179
+ );
180
+ }
181
+ if (!config.allowHourlyReclaim) {
182
+ throw new ReclaimNotAllowedError(
183
+ "Hourly files are the only copy of their requests until compaction folds them up, so the console will not delete them.",
184
+ );
185
+ }
186
+ return unlinkWhere(action, config.telemetryDir, isHourly);
187
+ }
188
+
189
+ /** The one row-level deletion the console offers. */
190
+ async function purge(target: PurgeTarget): Promise<number> {
191
+ switch (target) {
192
+ case "health_events": {
193
+ const store = config.healthEvents;
194
+ if (store === undefined) throw new PurgeNotWiredError();
195
+ return await store.purgeAll();
196
+ }
197
+ }
198
+ }
199
+
200
+ return { usage, reclaim, purge };
201
+ }
202
+
203
+ async function unlinkWhere(
204
+ action: ReclaimAction,
205
+ directory: string,
206
+ match: (name: string) => boolean,
207
+ ): Promise<ReclaimResponse> {
208
+ const names = await readdir(directory).catch(() => [] as string[]);
209
+ let deletedFiles = 0;
210
+ let reclaimedBytes = 0;
211
+ for (const name of names) {
212
+ if (!match(name)) continue;
213
+ const path = join(directory, name);
214
+ // Measured before the unlink, since the size is gone afterwards.
215
+ const size = await stat(path)
216
+ .then((value) => value.size)
217
+ .catch(() => 0);
218
+ const removed = await unlink(path).then(
219
+ () => true,
220
+ () => false,
221
+ );
222
+ if (!removed) continue;
223
+ deletedFiles += 1;
224
+ reclaimedBytes += size;
225
+ }
226
+ // The response names the action, so a caller that fired two reclaims can
227
+ // tell which result is which rather than both reading the same thing.
228
+ return { action, deletedFiles, reclaimedBytes };
229
+ }
@@ -0,0 +1,104 @@
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
+ t.Literal("telemetry_hourly"),
15
+ ]),
16
+ });
17
+
18
+ const purgeBody = t.Object({
19
+ target: t.Literal("health_events"),
20
+ });
21
+
22
+ /**
23
+ * Console routes for disk housekeeping.
24
+ *
25
+ * Reclaim and flush both need `dashboard:write`. Reading is deliberately the
26
+ * same scope as the rest of the console rather than something narrower: the
27
+ * sizes are not sensitive, but splitting the two would mean a caller who can
28
+ * already write could not see what they are about to affect.
29
+ */
30
+ export function createDataManagementRoutes(
31
+ config: DataManagementConfig & {
32
+ readonly accessResolver: (request: Request) => AccessDecision | undefined;
33
+ readonly auditSink?: {
34
+ record(entry: {
35
+ readonly access: AccessDecision;
36
+ readonly action: string;
37
+ readonly target: string | null;
38
+ readonly detail?: Record<string, unknown>;
39
+ }): Promise<void> | void;
40
+ } | undefined;
41
+ readonly flusher?: { flush(): Promise<void> } | undefined;
42
+ },
43
+ ): Elysia {
44
+ const operations = createDataManagementOperations(config);
45
+
46
+ return new Elysia({ prefix: "/data-management" })
47
+ .get("/usage", async ({ request, set }) => {
48
+ try {
49
+ const access = requireAnyScope(config.accessResolver(request), WRITE_SCOPES);
50
+ void access;
51
+ return await operations.usage();
52
+ } catch (error) {
53
+ return errorResponse(error, set, "Could not read disk usage");
54
+ }
55
+ })
56
+ .post("/reclaim", { body: reclaimBody }, async ({ request, body, set }) => {
57
+ try {
58
+ const access = requireAnyScope(config.accessResolver(request), WRITE_SCOPES);
59
+ const action = body.action as ReclaimAction;
60
+ const result: ReclaimResponse = await operations.reclaim(action);
61
+ await config.auditSink?.record({
62
+ access,
63
+ action: `data_management.reclaim.${action}`,
64
+ target: access.tenantId ?? null,
65
+ detail: { deletedFiles: result.deletedFiles, reclaimedBytes: result.reclaimedBytes },
66
+ });
67
+ set.status = 200;
68
+ return result;
69
+ } catch (error) {
70
+ return errorResponse(error, set, "Reclaim failed");
71
+ }
72
+ })
73
+ .post("/flush", async ({ request, set }) => {
74
+ try {
75
+ const access = requireAnyScope(config.accessResolver(request), WRITE_SCOPES);
76
+ await config.flusher?.flush();
77
+ await config.auditSink?.record({
78
+ access,
79
+ action: "data_management.flush",
80
+ target: access.tenantId ?? null,
81
+ });
82
+ set.status = 200;
83
+ return { flushed: true };
84
+ } catch (error) {
85
+ return errorResponse(error, set, "Flush failed");
86
+ }
87
+ })
88
+ .post("/purge", { body: purgeBody }, async ({ request, body, set }) => {
89
+ try {
90
+ const access = requireAnyScope(config.accessResolver(request), WRITE_SCOPES);
91
+ const deleted: number = await operations.purge(body.target as PurgeTarget);
92
+ await config.auditSink?.record({
93
+ access,
94
+ action: `data_management.purge.${body.target}`,
95
+ target: access.tenantId ?? null,
96
+ detail: { deleted },
97
+ });
98
+ set.status = 200;
99
+ return { target: body.target, deleted } satisfies { target: PurgeTarget; deleted: number };
100
+ } catch (error) {
101
+ return errorResponse(error, set, "Purge failed");
102
+ }
103
+ }) as unknown as Elysia;
104
+ }
@@ -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";
@@ -36,6 +39,9 @@ import { createUpdateRoutes } from "./updates/routes";
36
39
  import { createBackupRoutes } from "./backup/routes";
37
40
  import { BackupService } from "./backup/service";
38
41
  import type { ConsoleCredentialService } from "./auth/credentials";
42
+ import { resolveTelemetryRetentionDays } from "../config/telemetry";
43
+ import { payloadRetentionMs } from "../observability/payload-capture";
44
+ import { TELEMETRY_EVENT_DIR } from "../observability/event-log";
39
45
 
40
46
  import { InMemoryOAuthFlowStore, OAuthFlowStore } from "../providers/authentication/oauth-flow-store";
41
47
  import { OAuthCallbackListener } from "./providers/oauth/callback-listener";
@@ -80,6 +86,14 @@ export interface ConsoleDomainContext {
80
86
  readonly routeSnapshotService: { invalidate(): Promise<number> };
81
87
  readonly poolSelector: NetworkPoolSelector;
82
88
  readonly telemetryBuffer: TelemetryBatchBuffer;
89
+ /**
90
+ * Queued writers the console can drain on demand. Optional because a flush
91
+ * is only meaningful when the process owns the buffers; a caller that does
92
+ * not pass them simply loses the button rather than failing to boot.
93
+ */
94
+ readonly flushBuffers?: {
95
+ flush(): Promise<void>;
96
+ };
83
97
  /** Owns the live in-flight gauge the console surfaces. */
84
98
  readonly stateStore?: LiveInFlightSource;
85
99
  readonly admissionService: Pick<ApiKeyAdmissionService, "purgeKey">;
@@ -268,6 +282,31 @@ export function registerConsoleDomains(
268
282
  relayFetch: createValidatedFetch({ policy: resolveSsrfPolicy() }),
269
283
  }));
270
284
  console.use(createRuntimeSettingsRoutes({ store: runtimeSettingsStore, accessResolver: ctx.accessResolver, auditSink: ctx.auditRecorder }));
285
+ console.use(
286
+ createDataManagementRoutes({
287
+ telemetryDir: TELEMETRY_EVENT_DIR,
288
+ retentionDays: resolveTelemetryRetentionDays(),
289
+ payloadRetentionMs: payloadRetentionMs(),
290
+ // Hourly files are the only copy of their requests until compaction
291
+ // folds them up, so the console does not delete them on request.
292
+ allowHourlyReclaim: false,
293
+ accessResolver: ctx.accessResolver,
294
+ auditSink: ctx.auditRecorder,
295
+ flusher: ctx.flushBuffers,
296
+ // The console's one row-level purge: the health & error log is a
297
+ // diagnostic trail no request-path code reads. Dropping the buffered
298
+ // writes before the DELETE is what makes it sound — otherwise a batch
299
+ // in flight would land behind the purge and resurrect history that was
300
+ // just counted as deleted.
301
+ healthEvents: {
302
+ count: () => countAllHealthEvents(ctx.db),
303
+ purgeAll: async () => {
304
+ await healthEventBuffer()?.drain();
305
+ return await deleteAllHealthEvents(ctx.db);
306
+ },
307
+ },
308
+ }),
309
+ );
271
310
  console.use(
272
311
  createBackupRoutes({
273
312
  accessResolver: ctx.accessResolver,