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.
- package/.env.example +13 -4
- package/README.md +2 -2
- package/dist/dashboard/assets/{BannedPage-hfCzi-Ux.js → BannedPage-CADDq4IU.js} +1 -1
- package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DEDyKBlC.js → CliToolsPage-DuO1Z7dq.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
- package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-bsLuZmYH.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-_UWhwziJ.js → CustomizationPage-BR5HHvbi.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DDVIfZGW.js → LoginPage-V5faV9V2.js} +1 -1
- package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
- package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
- package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
- package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-D6pmQ86L.js → SetupPage-UB690SE2.js} +1 -1
- package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-DWvKiuSl.js → SortableList-BLujdX7N.js} +1 -1
- package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CmkUrgdj.js → StudioPage-D5C2raAc.js} +3 -3
- package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
- package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
- package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-C9FD_JwJ.js} +1 -1
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-CR72dasT.js} +1 -1
- package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
- package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-CI9_eSfO.js} +1 -1
- package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
- package/dist/dashboard/assets/{card--AtFedAq.js → card-DS7CQEsO.js} +1 -1
- package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-CNqzrfdb.js} +1 -1
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-611pSsaX.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
- package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
- package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
- package/dist/dashboard/assets/entry-B9Xvtnfj.js +1 -0
- package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
- package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
- package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-sltk1qUi.js} +1 -1
- package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
- package/dist/dashboard/assets/{index-BydyKaDz.js → index-CmxZr14F.js} +2 -2
- package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-BhY2sSnO.js} +1 -1
- package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
- package/dist/dashboard/assets/{input-BG41et9G.js → input-V1umFiBk.js} +1 -1
- package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-1zzZYM0B.js} +1 -1
- package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
- package/dist/dashboard/assets/{live-BJGYRBnR.js → live-BivGfuUs.js} +1 -1
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js +1 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-46P0oWMe.js} +1 -1
- package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-BFhGpwR1.js} +1 -1
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-CAb4F37R.js} +1 -1
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/{select-BO6h8yNv.js → select-BYR_5K2c.js} +2 -2
- package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
- package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-DNX9sydi.js} +1 -1
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-BHTfXdz_.js} +1 -1
- package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
- package/dist/dashboard/assets/{state-DYdQmdAl.js → state-mPDRlQkl.js} +1 -1
- package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOmJxg7H.js +1 -0
- package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
- package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
- package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
- package/dist/dashboard/assets/tokens-COzario9.js +1 -0
- package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
- package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
- package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-Dem0lXQj.js} +1 -1
- package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
- package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
- package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
- package/dist/dashboard/donation/qris-risun-marke.png +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +57 -57
- package/migrations/README.md +52 -52
- package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
- package/package.json +96 -95
- package/scripts/cli/admin.ts +125 -0
- package/scripts/cli/autostart.ts +237 -0
- package/scripts/cli/{ui/background.ts → background.ts} +1 -1
- package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -248
- package/scripts/cli/doctor.ts +37 -0
- package/scripts/cli/{support/env.ts → env.ts} +99 -99
- package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
- package/scripts/cli/instance.ts +224 -0
- package/scripts/cli/main.ts +88 -18
- package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
- package/scripts/cli/port.ts +100 -0
- package/scripts/cli/{support/services.ts → services.ts} +88 -88
- package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
- package/scripts/cli/{command/start.ts → start.ts} +10 -5
- package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
- package/scripts/cli/tray/index.ts +62 -12
- package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
- package/scripts/cli/update.ts +40 -0
- package/scripts/cli/version.ts +67 -0
- package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
- package/src/boot/app.ts +11 -7
- package/src/boot/dependencies.ts +50 -19
- package/src/boot/timeout.ts +2 -13
- package/src/config/cooldowns.ts +186 -138
- package/src/config/network.ts +8 -14
- package/src/config/providers.ts +7 -10
- package/src/config/runtime.ts +76 -76
- package/src/config/security.ts +15 -0
- package/src/config/spec.ts +52 -7
- package/src/config/telemetry.ts +26 -0
- package/src/config/timeouts.ts +0 -6
- package/src/console/admin/model-abuse/contracts.ts +28 -6
- package/src/console/admin/model-abuse/routes.ts +56 -3
- package/src/console/api-keys/bucket-spend.ts +15 -4
- package/src/console/api-keys/contracts.ts +410 -414
- package/src/console/api-keys/routes.ts +881 -880
- package/src/console/auth/credentials.ts +3 -3
- package/src/console/auth/setup-service.ts +2 -2
- package/src/console/backup/contracts.ts +15 -2
- package/src/console/backup/export-endpoint.ts +6 -2
- package/src/console/backup/nine-router.ts +2 -5
- package/src/console/backup/service.ts +4 -0
- package/src/console/backup/store.ts +192 -29
- package/src/console/backup/validate.ts +3 -45
- package/src/console/cli-tools/fs-ops.ts +487 -487
- package/src/console/console-router-guards.ts +1 -1
- package/src/console/console-router.ts +8 -0
- package/src/console/data-management/contracts.ts +100 -0
- package/src/console/data-management/operations.ts +234 -0
- package/src/console/data-management/routes.ts +103 -0
- package/src/console/domain-registration.ts +45 -1
- package/src/console/observability/contracts.ts +524 -485
- package/src/console/observability/live.ts +22 -0
- package/src/console/observability/store.ts +1138 -1018
- package/src/console/observability/usage-dimensions.ts +4 -4
- package/src/console/providers/catalog/catalog-projections.ts +6 -7
- package/src/console/providers/catalog/contracts.ts +6 -0
- package/src/console/providers/catalog/model-operations.ts +5 -2
- package/src/console/providers/catalog/provider-operations.ts +24 -4
- package/src/console/providers/catalog/public-model-store.ts +8 -11
- package/src/console/providers/catalog/routes.ts +7 -0
- package/src/console/providers/catalog/store.ts +1553 -1532
- package/src/console/providers/oauth/routes.ts +8 -12
- package/src/console/quota/account-quota-shared.ts +52 -39
- package/src/console/quota/account-quota-tenant.ts +6 -6
- package/src/console/quota/quota-view.ts +1 -1
- package/src/console/quota/refresh.ts +441 -429
- package/src/console/remote-support/contracts.ts +61 -0
- package/src/console/remote-support/routes.ts +65 -0
- package/src/console/remote-support/service.ts +207 -0
- package/src/console/response-compression.ts +0 -3
- package/src/console/routing/pools/import-parse.ts +206 -0
- package/src/console/routing/pools/operations.ts +543 -501
- package/src/console/routing/pools/probe-result.ts +3 -2
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/routes.ts +181 -164
- package/src/console/routing/pools/store.ts +8 -8
- package/src/console/settings/contracts.ts +95 -2
- package/src/console/settings/store.ts +81 -3
- package/src/console/shared/cursor.ts +8 -8
- package/src/console/shared/errors.ts +1 -3
- package/src/console/updates/routes.ts +16 -0
- package/src/main.ts +10 -0
- package/src/network/http2-bidi.ts +2 -4
- package/src/network/pool/agent.ts +478 -478
- package/src/network/pool/selector.ts +5 -4
- package/src/network/retry.ts +1 -2
- package/src/network/ssrf.ts +5 -4
- package/src/observability/event-compaction.ts +317 -0
- package/src/observability/event-day-index.ts +440 -0
- package/src/observability/event-log.ts +155 -6
- package/src/observability/logger.ts +6 -6
- package/src/observability/payload-capture.ts +158 -33
- package/src/observability/payload-store.ts +82 -61
- package/src/observability/redaction.ts +5 -6
- package/src/observability/request-notify.ts +43 -0
- package/src/observability/telemetry-status.ts +8 -9
- package/src/observability/token-speed.ts +5 -4
- package/src/persistence/db-mode.ts +7 -7
- package/src/persistence/health-event-flush.ts +36 -0
- package/src/persistence/readiness.ts +1 -1
- package/src/persistence/schema.ts +75 -29
- package/src/persistence/shared-cache.ts +1 -2
- package/src/persistence/stores/api-key-store.ts +18 -6
- package/src/persistence/stores/share-store.ts +4 -3
- package/src/persistence/stores/telemetry-store.ts +33 -11
- package/src/protocol/canonical-model.ts +11 -3
- package/src/protocol/codecs.ts +139 -6
- package/src/protocol/gateway-error.ts +6 -4
- package/src/protocol/image-clamp.ts +19 -1
- package/src/protocol/registry.ts +8 -0
- package/src/protocol/request/chat.ts +21 -1
- package/src/protocol/request/codex.ts +10 -1
- package/src/protocol/request/gemini.ts +9 -2
- package/src/protocol/request/messages.ts +18 -7
- package/src/protocol/request/responses.ts +9 -1
- package/src/protocol/response/chat.ts +34 -4
- package/src/protocol/response/codex.ts +7 -4
- package/src/protocol/response/dsml.ts +140 -28
- package/src/protocol/response/responses.ts +11 -5
- package/src/protocol/stream-error-frames.ts +4 -5
- package/src/providers/adapters/compatible-adapter.ts +3 -0
- package/src/providers/authentication/jwt-validator.ts +9 -10
- package/src/providers/authentication/oauth-refresh-service.ts +9 -11
- package/src/providers/bundled-discovery-providers.ts +122 -119
- package/src/providers/configured-openai-providers.ts +4 -4
- package/src/providers/default-registry.ts +5 -4
- package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
- package/src/providers/discovery/catalog/catalog-format.ts +3 -6
- package/src/providers/discovery/models-dev-catalog.ts +354 -354
- package/src/providers/discovery/openai-model-discovery.ts +5 -0
- package/src/providers/discovery/openrouter-free-roster.ts +15 -17
- package/src/providers/discovery/probe-phases.ts +4 -4
- package/src/providers/discovery/probe-wire.ts +6 -13
- package/src/providers/discovery/probing-service.ts +1117 -1045
- package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
- package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
- package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
- package/src/providers/integrations/buddy/buddy-media.ts +2 -2
- package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
- package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
- package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
- package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
- package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
- package/src/providers/integrations/claude/claude.ts +608 -608
- package/src/providers/integrations/cline/cline-quota.ts +27 -20
- package/src/providers/integrations/cline/cline.ts +537 -539
- package/src/providers/integrations/codex/codex-headers.ts +25 -39
- package/src/providers/integrations/codex/codex-oauth.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +2 -29
- package/src/providers/integrations/commandcode/commandcode.ts +674 -675
- package/src/providers/integrations/commandcode/discovery.ts +117 -116
- package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
- package/src/providers/integrations/cursor/cursor-quota.ts +309 -0
- package/src/providers/integrations/cursor/cursor-request.ts +616 -471
- package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
- package/src/providers/integrations/cursor/cursor.ts +1333 -960
- package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
- package/src/providers/integrations/factory/factory-models.ts +3 -5
- package/src/providers/integrations/factory/factory.ts +8 -1
- package/src/providers/integrations/gemini.ts +6 -1
- package/src/providers/integrations/grok/grok-install-id.ts +2 -3
- package/src/providers/integrations/grok/grok-quota.ts +279 -177
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
- package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
- package/src/providers/integrations/grok/grok.ts +6 -7
- package/src/providers/integrations/hermes.ts +14 -18
- package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
- package/src/providers/integrations/kiro/kiro.ts +1 -1
- package/src/providers/integrations/mistral.ts +6 -6
- package/src/providers/integrations/opencode/fingerprint.ts +3 -3
- package/src/providers/integrations/opencode/provider.ts +19 -20
- package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
- package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
- package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
- package/src/providers/integrations/qoder/qoder.ts +884 -865
- package/src/providers/integrations/shared/connect.ts +6 -6
- package/src/providers/integrations/trae/trae-catalog.ts +1 -2
- package/src/providers/integrations/trae/trae-quota.ts +384 -0
- package/src/providers/integrations/trae/trae.ts +2 -2
- package/src/providers/integrations/zcode/oauth.ts +6 -1
- package/src/providers/operations/account-health-service.ts +75 -27
- package/src/providers/operations/account-reset-service.ts +1 -1
- package/src/providers/operations/client-versions.ts +6 -6
- package/src/providers/operations/custom-cli-headers.ts +3 -4
- package/src/providers/operations/provider-catalog-seeder.ts +95 -95
- package/src/providers/operations/provider-catalog-service.ts +3 -4
- package/src/providers/provider-contracts.ts +7 -0
- package/src/providers/provider-metadata.ts +10 -8
- package/src/providers/provider-registry.ts +6 -7
- package/src/providers/provider-usage.ts +1 -1
- package/src/providers/quota/quota-support.ts +8 -0
- package/src/security/abuse.ts +15 -10
- package/src/security/access-control.ts +144 -146
- package/src/security/admission/contracts.ts +11 -0
- package/src/security/admission/in-memory-store.ts +4 -3
- package/src/security/admission/lease-sweep.ts +6 -6
- package/src/security/admission/reasons.ts +5 -1
- package/src/security/admission/redis-store.ts +211 -210
- package/src/security/admission/service.ts +4 -0
- package/src/security/api-key-auth.ts +27 -17
- package/src/security/ip-boundary.ts +155 -15
- package/src/security/model-abuse.ts +366 -94
- package/src/security/outbound-headers.ts +3 -4
- package/src/shared/sleep.ts +4 -0
- package/src/transport/dispatch/attempt-finalize.ts +47 -5
- package/src/transport/dispatch/attempt-loop.ts +531 -530
- package/src/transport/dispatch/generation.ts +6 -0
- package/src/transport/dispatch/proxy-request.ts +24 -1
- package/src/transport/dispatch/ratelimit-capture.ts +37 -0
- package/src/transport/dispatch/stream-bridge.ts +1 -3
- package/src/transport/dispatch/streaming/finalize.ts +1 -1
- package/src/transport/dispatch/streaming/stream.ts +19 -6
- package/src/transport/dispatch/upstream.ts +9 -0
- package/src/transport/failure-policy.ts +162 -8
- package/src/transport/guards/auth-guard.ts +7 -4
- package/src/transport/guards/concurrency-guard.ts +0 -5
- package/src/transport/guards/error-lifecycle.ts +13 -6
- package/src/transport/guards/pipeline.ts +15 -3
- package/src/transport/guards/request-context.ts +8 -2
- package/src/transport/request/inflight.ts +5 -6
- package/src/transport/request/preparer.ts +9 -8
- package/src/transport/request/state.ts +21 -8
- package/src/transport/request/tenant-preferences.ts +60 -0
- package/src/transport/routing/admission.ts +4 -0
- package/src/transport/routing/route-catalog.ts +65 -10
- package/src/transport/routing/route-model.ts +46 -14
- package/src/transport/routing/router.ts +21 -14
- package/src/transport/surface/completion.ts +2 -3
- package/src/transport/surface/dialects.ts +5 -6
- package/src/transport/surface/responses/encode.ts +38 -0
- package/src/transport/surface/responses/parse.ts +4 -0
- package/src/transport/translation/capabilities.ts +2 -3
- package/src/transport/translation/quirks.ts +3 -3
- package/src/workers/buddy-growth-tasks.ts +5 -10
- package/src/workers/daily-checkin.ts +17 -23
- package/src/workers/quota-refresh-sweep.ts +26 -26
- package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js +0 -1
- package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js +0 -1
- package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-B53YYMng.js +0 -1
- package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +0 -7
- package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +0 -1
- package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +0 -1
- package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-B47uOydr.js +0 -56
- package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-uNywYlfv.js +0 -1
- package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
- package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
- package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
- package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
- package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
- package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
- package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BtpsJU98.js +0 -1
- package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/drawer-WwwtzihF.js +0 -1
- package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
- package/dist/dashboard/assets/entry-CkryrIea.js +0 -4
- package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
- package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
- package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
- package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
- package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
- package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
- package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
- package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
- package/dist/dashboard/assets/layout-C8mer_Ak.js +0 -1
- package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
- package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
- package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
- package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
- package/dist/dashboard/assets/select-BO6h8yNv.js.gz +0 -0
- package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
- package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
- package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
- package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
- package/scripts/cli/command/admin.ts +0 -65
- package/scripts/cli/command/doctor.ts +0 -257
- package/scripts/cli/command/update.ts +0 -23
- package/scripts/cli/gateway/drain-token.ts +0 -78
- package/scripts/cli/gateway/instance-lock.ts +0 -105
- package/scripts/cli/gateway/stop.ts +0 -51
- package/scripts/cli/support/bun.ts +0 -27
- package/scripts/cli/support/version.ts +0 -37
- /package/dist/dashboard/assets/{app-CafWtVXA.js → app-BIt446yE.js} +0 -0
- /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-BIt446yE.js.gz} +0 -0
- /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
|
|
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,
|