cartethyia 2.5.4 → 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-C2orBsI2.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-BV6pxVQo.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-CW-wPmJ6.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-0p8uX7vB.js → CustomizationPage-BR5HHvbi.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-BjqSMfko.js → HealthEventsModal-mUfcknqm.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BvzCx8Kr.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-Js9jUPSb.js → ProvidersPage-uPiGVTTK.js} +1 -1
- 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-DDbMQsoo.js → SetupPage-UB690SE2.js} +1 -1
- package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-CB3Z7VTf.js → SortableList-BLujdX7N.js} +1 -1
- package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-D95UCPHI.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-Chi7pCnt.js → assert-C9FD_JwJ.js} +1 -1
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/{badge-D4pQapKj.js → badge-CR72dasT.js} +1 -1
- package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DH6cg-_B.js → banner-CI9_eSfO.js} +1 -1
- package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
- package/dist/dashboard/assets/{card-BKqlUOe4.js → card-DS7CQEsO.js} +1 -1
- package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-fTgljTGA.js → cli-tools-CNqzrfdb.js} +1 -1
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-D5Ys_sM0.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/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-CQBr6U9c.js → entry-B9Xvtnfj.js} +1 -1
- 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-BlptL8Uf.js → github-badge-sltk1qUi.js} +1 -1
- package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
- package/dist/dashboard/assets/{index-js6TJvdr.js → index-CmxZr14F.js} +2 -2
- package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DUWDSmDV.js → inline-BhY2sSnO.js} +1 -1
- package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
- package/dist/dashboard/assets/{input-LVjgZq4f.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-DupvLfDx.js → list-row-1zzZYM0B.js} +1 -1
- package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
- package/dist/dashboard/assets/{live-C4pEJUqI.js → live-BivGfuUs.js} +1 -1
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-BRiuufkB.js → model-bans-CmffjLuG.js} +1 -1
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-B4ROwk8L.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-gmjzC3RS.js → query-keys-BFhGpwR1.js} +1 -1
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/{routing-C7tBT5o-.js → routing-CAb4F37R.js} +1 -1
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/{select-WUCXorXw.js → select-BYR_5K2c.js} +1 -1
- package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
- package/dist/dashboard/assets/{settings-C_zKl1GS.js → settings-DNX9sydi.js} +1 -1
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/{stack-_3O0F8Xn.js → stack-BHTfXdz_.js} +1 -1
- package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
- package/dist/dashboard/assets/{state-BgTxHcJS.js → state-mPDRlQkl.js} +1 -1
- package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DEjTtOWf.js → switch-DOmJxg7H.js} +1 -1
- package/dist/dashboard/assets/switch-DOmJxg7H.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-BWSC0UAS.js → useMutation-Dem0lXQj.js} +1 -1
- package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -54
- 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/{ui/background.ts → background.ts} +1 -1
- package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -244
- 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 +61 -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/{command/update.ts → update.ts} +39 -39
- package/scripts/cli/version.ts +67 -0
- package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
- package/src/boot/app.ts +697 -693
- package/src/boot/dependencies.ts +647 -646
- 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 +20 -30
- package/src/config/telemetry.ts +0 -20
- 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/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/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/data-management/contracts.ts +13 -6
- package/src/console/data-management/operations.ts +23 -18
- package/src/console/data-management/routes.ts +0 -1
- package/src/console/domain-registration.ts +10 -5
- package/src/console/observability/live.ts +22 -0
- package/src/console/observability/store.ts +39 -16
- package/src/console/observability/usage-dimensions.ts +4 -4
- package/src/console/providers/catalog/catalog-projections.ts +6 -7
- package/src/console/providers/catalog/model-operations.ts +5 -2
- package/src/console/providers/catalog/provider-operations.ts +4 -4
- package/src/console/providers/catalog/public-model-store.ts +8 -11
- package/src/console/providers/catalog/store.ts +1553 -1553
- package/src/console/providers/oauth/routes.ts +8 -12
- package/src/console/quota/account-quota-tenant.ts +6 -6
- package/src/console/quota/quota-view.ts +1 -1
- 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/probe-result.ts +3 -2
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/store.ts +8 -8
- package/src/console/settings/contracts.ts +58 -2
- package/src/console/settings/store.ts +30 -3
- package/src/console/shared/cursor.ts +8 -8
- package/src/console/shared/errors.ts +1 -3
- 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 +80 -78
- package/src/observability/event-day-index.ts +13 -15
- package/src/observability/event-log.ts +4 -4
- package/src/observability/logger.ts +6 -6
- package/src/observability/payload-capture.ts +151 -33
- package/src/observability/payload-store.ts +397 -440
- 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/readiness.ts +1 -1
- package/src/persistence/schema.ts +1406 -1402
- 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 +236 -237
- package/src/protocol/canonical-model.ts +11 -3
- package/src/protocol/codecs.ts +138 -4
- package/src/protocol/gateway-error.ts +6 -4
- package/src/protocol/request/chat.ts +21 -1
- package/src/protocol/request/codex.ts +10 -1
- package/src/protocol/request/messages.ts +4 -5
- 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/authentication/jwt-validator.ts +9 -10
- package/src/providers/authentication/oauth-refresh-service.ts +9 -11
- package/src/providers/configured-openai-providers.ts +134 -136
- 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/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 +132 -132
- package/src/providers/integrations/cline/cline.ts +537 -539
- package/src/providers/integrations/codex/codex-oauth.ts +1 -1
- 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 -310
- package/src/providers/integrations/cursor/cursor.ts +11 -163
- package/src/providers/integrations/factory/factory-models.ts +3 -5
- package/src/providers/integrations/grok/grok-install-id.ts +2 -3
- 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 +8 -8
- 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.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.ts +2 -2
- package/src/providers/integrations/zcode/oauth.ts +6 -1
- package/src/providers/operations/account-health-service.ts +1528 -1523
- 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-metadata.ts +4 -8
- package/src/providers/provider-registry.ts +6 -7
- package/src/providers/provider-usage.ts +1 -1
- 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 +504 -504
- 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/stream-bridge.ts +1 -3
- package/src/transport/dispatch/streaming/finalize.ts +1 -1
- package/src/transport/dispatch/streaming/stream.ts +5 -6
- 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 +2 -2
- package/src/transport/request/state.ts +12 -8
- package/src/transport/routing/admission.ts +4 -0
- package/src/transport/routing/route-catalog.ts +8 -1
- 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-C2orBsI2.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Bsan4679.js +0 -1
- package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js +0 -1
- package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +0 -1
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +0 -7
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +0 -1
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Cnae9SWA.js +0 -56
- package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-VY68Mhd1.js +0 -1
- package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
- package/dist/dashboard/assets/app-COlK95oi.css +0 -2
- package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
- package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
- package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
- package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
- package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BYj-8WuK.js +0 -1
- package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CmXIZ6H1.js +0 -1
- package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
- package/dist/dashboard/assets/entry-CBg5zeu8.js +0 -1
- package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
- package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
- package/dist/dashboard/assets/entry-CgdSAzEV.js +0 -4
- package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
- package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
- package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
- package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
- package/dist/dashboard/assets/layout-CPfxpsfy.js +0 -1
- package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
- package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
- package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
- package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
- package/dist/dashboard/assets/numbers-YNcHvRY7.js +0 -2
- package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-gmjzC3RS.js.gz +0 -0
- package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
- package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
- package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
- package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
- package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
- package/dist/dashboard/assets/switch-DEjTtOWf.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-BWSC0UAS.js.gz +0 -0
- package/scripts/cli/command/admin.ts +0 -65
- package/scripts/cli/command/doctor.ts +0 -257
- 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-DCQZRmHY.js → app-BIt446yE.js} +0 -0
- /package/dist/dashboard/assets/{app-DCQZRmHY.js.gz → app-BIt446yE.js.gz} +0 -0
- /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
|
@@ -1,440 +1,397 @@
|
|
|
1
|
-
import { mkdir, open, readdir, readFile, stat, unlink } from "node:fs/promises";
|
|
2
|
-
import { basename, join } from "node:path";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { gunzipSync, gzipSync } from "node:zlib";
|
|
5
|
-
|
|
6
|
-
export interface PayloadFileReference {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
readonly
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
readonly
|
|
17
|
-
readonly
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
readonly
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
*
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
]
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
parts
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
unparseableWrittenBefore: Date = new Date(0),
|
|
399
|
-
): Promise<number> {
|
|
400
|
-
const directory = payloadDirectory();
|
|
401
|
-
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
402
|
-
let deleted = 0;
|
|
403
|
-
for (const entry of entries) {
|
|
404
|
-
if (!entry.isFile() || !isFrameFileName(entry.name)) continue;
|
|
405
|
-
const path = join(directory, entry.name);
|
|
406
|
-
try {
|
|
407
|
-
if (await fileReclaimable(path, before, unparseableWrittenBefore)) {
|
|
408
|
-
await unlink(path);
|
|
409
|
-
deleted += 1;
|
|
410
|
-
}
|
|
411
|
-
} catch {
|
|
412
|
-
// Stat, read, or unlink failed on this one file. Leave it for the next
|
|
413
|
-
// pass rather than abandoning every remaining file's reclaim.
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
return deleted;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* True when the file can be deleted without losing a frame a row still
|
|
421
|
-
* addresses. A readable file is reclaimable once every frame has expired; a
|
|
422
|
-
* damaged one only once `unparseableWrittenBefore` rules out a live frame in
|
|
423
|
-
* the part that could not be parsed.
|
|
424
|
-
*/
|
|
425
|
-
async function fileReclaimable(
|
|
426
|
-
path: string,
|
|
427
|
-
before: Date,
|
|
428
|
-
unparseableWrittenBefore: Date,
|
|
429
|
-
): Promise<boolean> {
|
|
430
|
-
const data = await readFile(path);
|
|
431
|
-
// Nothing written yet, so nothing to lose. The old test was "shorter than a
|
|
432
|
-
// binary header", which the line format has no need of; a real frame — old
|
|
433
|
-
// or new — is far longer than that anyway.
|
|
434
|
-
if (data.byteLength === 0) return true;
|
|
435
|
-
const liveness = frameLiveness(data, before);
|
|
436
|
-
if (liveness === "live") return false;
|
|
437
|
-
if (liveness === "none") return true;
|
|
438
|
-
const written = await stat(path).then((value) => value.mtimeMs);
|
|
439
|
-
return written <= unparseableWrittenBefore.getTime();
|
|
440
|
-
}
|
|
1
|
+
import { mkdir, open, readdir, readFile, stat, unlink } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
5
|
+
|
|
6
|
+
export interface PayloadFileReference {
|
|
7
|
+
readonly storage: "jsonl-file";
|
|
8
|
+
readonly file: string;
|
|
9
|
+
readonly offset: number;
|
|
10
|
+
readonly length: number;
|
|
11
|
+
readonly checksum: string;
|
|
12
|
+
readonly version: 1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface PayloadFrame {
|
|
16
|
+
readonly version: 1;
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly expiresAt: string;
|
|
19
|
+
readonly payload: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isPayloadFileReference(value: unknown): value is PayloadFileReference {
|
|
23
|
+
if (typeof value !== "object" || value === null) return false;
|
|
24
|
+
const record = value as Record<string, unknown>;
|
|
25
|
+
return (
|
|
26
|
+
record["storage"] === "jsonl-file" &&
|
|
27
|
+
typeof record["file"] === "string" &&
|
|
28
|
+
typeof record["offset"] === "number" &&
|
|
29
|
+
typeof record["length"] === "number" &&
|
|
30
|
+
typeof record["checksum"] === "string" &&
|
|
31
|
+
record["version"] === 1
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds a frame reference from a `telemetry_payloads` row's typed columns.
|
|
37
|
+
* Returns undefined when any column is out of contract so callers treat a
|
|
38
|
+
* corrupt index row as "no payload" rather than path-traversing or reading
|
|
39
|
+
* the wrong bytes.
|
|
40
|
+
*/
|
|
41
|
+
export function payloadReferenceFromRow(row: {
|
|
42
|
+
readonly storage: string;
|
|
43
|
+
readonly file: string;
|
|
44
|
+
readonly offset: number;
|
|
45
|
+
readonly length: number;
|
|
46
|
+
readonly checksum: string;
|
|
47
|
+
readonly version: number;
|
|
48
|
+
}): PayloadFileReference | undefined {
|
|
49
|
+
const candidate = {
|
|
50
|
+
storage: row.storage,
|
|
51
|
+
file: row.file,
|
|
52
|
+
offset: row.offset,
|
|
53
|
+
length: row.length,
|
|
54
|
+
checksum: row.checksum,
|
|
55
|
+
version: row.version,
|
|
56
|
+
};
|
|
57
|
+
return isPayloadFileReference(candidate) ? candidate : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Extension for frame files. */
|
|
61
|
+
const FRAME_FILE_SUFFIX = ".jsonl";
|
|
62
|
+
|
|
63
|
+
function isFrameFileName(name: string): boolean {
|
|
64
|
+
return name.endsWith(FRAME_FILE_SUFFIX);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const MAX_FRAME_BYTES = 256 * 1024 * 1024;
|
|
68
|
+
const DEFAULT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
69
|
+
const DEFAULT_DIRECTORY = "./data/telemetry-payloads";
|
|
70
|
+
let writeTail: Promise<void> = Promise.resolve();
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Single source of truth for the telemetry payload directory: the
|
|
74
|
+
* `CARTETHYIA_TELEMETRY_PAYLOAD_DIR` override when set, otherwise the
|
|
75
|
+
* default `./data/telemetry-payloads`. Every writer and reader must resolve
|
|
76
|
+
* the directory through this function so the two can never drift.
|
|
77
|
+
*/
|
|
78
|
+
export function payloadDirectory(): string {
|
|
79
|
+
return process.env.CARTETHYIA_TELEMETRY_PAYLOAD_DIR?.trim() || DEFAULT_DIRECTORY;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function maxFileBytes(): number {
|
|
83
|
+
const raw = Number(process.env.CARTETHYIA_TELEMETRY_PAYLOAD_FILE_MAX_BYTES ?? DEFAULT_MAX_FILE_BYTES);
|
|
84
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_MAX_FILE_BYTES;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function checksum(value: Buffer | readonly Buffer[]): string {
|
|
88
|
+
const parts = Array.isArray(value) ? value : [value];
|
|
89
|
+
let hash = 2166136261;
|
|
90
|
+
for (const part of parts) {
|
|
91
|
+
for (let index = 0; index < part.byteLength; index += 1) {
|
|
92
|
+
hash ^= part[index]!;
|
|
93
|
+
hash = Math.imul(hash, 16777619);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function fileName(now = new Date()): string {
|
|
100
|
+
return `${now.toISOString().slice(0, 13).replaceAll(":", "-")}-${process.pid}${FRAME_FILE_SUFFIX}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Gzipped frame body.
|
|
105
|
+
*
|
|
106
|
+
* The pieces stay separate until here, because nothing before the write reads
|
|
107
|
+
* them assembled and joining them earlier is a copy of the whole frame. The
|
|
108
|
+
* join happens once, at the moment of compression, and the compressed buffer
|
|
109
|
+
* is what gets written, checksummed, and bounded — so the cap and the
|
|
110
|
+
* reference both describe bytes on disk, not bytes of JSON.
|
|
111
|
+
*/
|
|
112
|
+
function compressFrame(parts: readonly Buffer[]): Buffer {
|
|
113
|
+
return gzipSync(Buffer.concat(parts));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function appendFrame(frame: PayloadFrame, parts?: readonly Buffer[]): Promise<PayloadFileReference> {
|
|
117
|
+
const directory = payloadDirectory();
|
|
118
|
+
await mkdir(directory, { recursive: true });
|
|
119
|
+
// Encoded as a line here rather than at the write, so the cap, the checksum,
|
|
120
|
+
// and the reference all describe the same bytes that land on disk — the
|
|
121
|
+
// prebuilt-parts path and the plain one cannot disagree about the shape.
|
|
122
|
+
const json = parts === undefined
|
|
123
|
+
? Buffer.from(JSON.stringify(frame), "utf8")
|
|
124
|
+
: Buffer.concat(parts);
|
|
125
|
+
const compressed = compressFrame([json]);
|
|
126
|
+
if (compressed.byteLength > MAX_FRAME_BYTES) throw new Error("telemetry payload frame exceeds limit");
|
|
127
|
+
const encoded = toLine(compressed);
|
|
128
|
+
const file = fileName();
|
|
129
|
+
const path = join(directory, file);
|
|
130
|
+
const currentSize = await stat(path).then((value) => value.size).catch(() => 0);
|
|
131
|
+
const offset = currentSize;
|
|
132
|
+
if (offset + encoded.byteLength > maxFileBytes()) {
|
|
133
|
+
const rotated = `${Date.now()}-${randomUUID()}${FRAME_FILE_SUFFIX}`;
|
|
134
|
+
return appendFrameToPath(join(directory, rotated), encoded, basename(rotated));
|
|
135
|
+
}
|
|
136
|
+
return appendFrameToPath(path, encoded, file, offset);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* One frame as a stored line: base64 of the gzipped JSON, newline-terminated.
|
|
141
|
+
*
|
|
142
|
+
* A newline is the frame delimiter, so the file is a text file of base64 lines
|
|
143
|
+
* and a frame's extent is found by reading to the next `\n` — a binary length
|
|
144
|
+
* header would make every frame opaque, unreadable without first decoding a
|
|
145
|
+
* big-endian length and then gunzipping exactly that many bytes.
|
|
146
|
+
*
|
|
147
|
+
* base64 rather than raw gzip bytes because gzip contains newlines, which
|
|
148
|
+
* would make the delimiter ambiguous.
|
|
149
|
+
*/
|
|
150
|
+
function toLine(compressed: Buffer): Buffer {
|
|
151
|
+
return Buffer.from(`${compressed.toString("base64")}\n`, "utf8");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function appendFrameToPath(
|
|
155
|
+
path: string,
|
|
156
|
+
encoded: Buffer,
|
|
157
|
+
knownFile = basename(path),
|
|
158
|
+
knownOffset?: number,
|
|
159
|
+
): Promise<PayloadFileReference> {
|
|
160
|
+
const offset = knownOffset ?? await stat(path).then((value) => value.size).catch(() => 0);
|
|
161
|
+
const handle = await open(path, "a");
|
|
162
|
+
try {
|
|
163
|
+
await handle.write(encoded);
|
|
164
|
+
} finally {
|
|
165
|
+
await handle.close();
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
storage: "jsonl-file",
|
|
169
|
+
file: knownFile,
|
|
170
|
+
offset,
|
|
171
|
+
length: encoded.byteLength,
|
|
172
|
+
checksum: checksum(encoded),
|
|
173
|
+
version: 1,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The frame's body keys, in the same order `buildPayloadRecord` serializes
|
|
179
|
+
* them.
|
|
180
|
+
*
|
|
181
|
+
* The record hands the writer one string per body already serialized, so the
|
|
182
|
+
* two orders must agree or every body lands under the wrong key. The order is
|
|
183
|
+
* drawer order with the legacy `response_body` alias last, for the same reason
|
|
184
|
+
* the record accounts it last: it names no panel of its own and must not
|
|
185
|
+
* spend the shared byte budget ahead of the provider stages.
|
|
186
|
+
*
|
|
187
|
+
* There are five, not four. `buildPayloadRecord` builds one string per surface
|
|
188
|
+
* and `response_body` is the fifth — dropping it here wrote the frame with the
|
|
189
|
+
* first four bodies and silently discarded the last, so a capture whose client
|
|
190
|
+
* response was in the legacy slot came back with that panel empty.
|
|
191
|
+
*/
|
|
192
|
+
const FRAME_BODY_KEYS = [
|
|
193
|
+
"request_body",
|
|
194
|
+
"provider_request_body",
|
|
195
|
+
"provider_response_body",
|
|
196
|
+
"client_response_body",
|
|
197
|
+
"response_body",
|
|
198
|
+
] as const;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The pieces of a frame whose bodies were already serialized while measured.
|
|
202
|
+
*
|
|
203
|
+
* Returned as separate buffers rather than one assembled buffer: the file
|
|
204
|
+
* write accepts a list, and assembling them first is a copy of the whole frame
|
|
205
|
+
* that nothing reads back.
|
|
206
|
+
*/
|
|
207
|
+
function prebuiltFrameParts(
|
|
208
|
+
id: string,
|
|
209
|
+
expiresAt: string,
|
|
210
|
+
capturedAt: string,
|
|
211
|
+
requestId: string | null,
|
|
212
|
+
signals: unknown,
|
|
213
|
+
bodies: readonly string[],
|
|
214
|
+
): Buffer[] {
|
|
215
|
+
const parts = [
|
|
216
|
+
Buffer.from(
|
|
217
|
+
`{"version":1,"id":${JSON.stringify(id)},"expiresAt":${JSON.stringify(expiresAt)},` +
|
|
218
|
+
`"payload":{"request_id":${JSON.stringify(requestId)},"captured_at":${JSON.stringify(capturedAt)},` +
|
|
219
|
+
`"expires_at":${JSON.stringify(expiresAt)}`,
|
|
220
|
+
"utf8",
|
|
221
|
+
),
|
|
222
|
+
];
|
|
223
|
+
for (let index = 0; index < FRAME_BODY_KEYS.length; index += 1) {
|
|
224
|
+
parts.push(Buffer.from(`,"${FRAME_BODY_KEYS[index]}":${bodies[index] ?? "null"}`, "utf8"));
|
|
225
|
+
}
|
|
226
|
+
parts.push(Buffer.from(`,"signals":${JSON.stringify(signals)}}}`, "utf8"));
|
|
227
|
+
return parts;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function writePayloadFrame(
|
|
231
|
+
payload: unknown,
|
|
232
|
+
expiresAt: Date,
|
|
233
|
+
prebuilt?: { readonly requestId: string | null; readonly capturedAt: string; readonly signals: unknown; readonly bodies: readonly string[] },
|
|
234
|
+
): Promise<PayloadFileReference> {
|
|
235
|
+
const id = randomUUID();
|
|
236
|
+
const expires = expiresAt.toISOString();
|
|
237
|
+
const parts = prebuilt
|
|
238
|
+
? prebuiltFrameParts(id, expires, prebuilt.capturedAt, prebuilt.requestId, prebuilt.signals, prebuilt.bodies)
|
|
239
|
+
: undefined;
|
|
240
|
+
const frame: PayloadFrame = { version: 1, id, expiresAt: expires, payload };
|
|
241
|
+
const task = writeTail.then(() => appendFrame(frame, parts));
|
|
242
|
+
writeTail = task.then(() => undefined, () => undefined);
|
|
243
|
+
return task;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function readPayloadFrame(reference: PayloadFileReference): Promise<unknown | undefined> {
|
|
247
|
+
// Basename-constrain the reference: a hostile/restored row carrying
|
|
248
|
+
// `../../x` must not escape the payload directory (path traversal).
|
|
249
|
+
const safeFile = basename(reference.file);
|
|
250
|
+
if (safeFile !== reference.file || safeFile.length === 0) return undefined;
|
|
251
|
+
const path = join(payloadDirectory(), safeFile);
|
|
252
|
+
// Read exactly the frame. Reading the whole file copied every earlier frame
|
|
253
|
+
// in the same hour into memory just to return one of them.
|
|
254
|
+
const handle = await open(path, "r");
|
|
255
|
+
let data: Buffer;
|
|
256
|
+
try {
|
|
257
|
+
const stat = await handle.stat();
|
|
258
|
+
if (reference.offset < 0 || reference.offset + reference.length > stat.size) return undefined;
|
|
259
|
+
data = Buffer.alloc(reference.length);
|
|
260
|
+
await handle.read(data, 0, reference.length, reference.offset);
|
|
261
|
+
} finally {
|
|
262
|
+
await handle.close();
|
|
263
|
+
}
|
|
264
|
+
const frame = data;
|
|
265
|
+
const json = frameJsonFromSlice(frame);
|
|
266
|
+
if (json === undefined) return undefined;
|
|
267
|
+
const parsed = JSON.parse(json.toString("utf8")) as PayloadFrame;
|
|
268
|
+
return parsed.version === 1 ? parsed.payload : undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Decompressed frame body, or undefined when the bytes are not gzip. */
|
|
272
|
+
function gunzipFrame(stored: Buffer): Buffer | undefined {
|
|
273
|
+
try {
|
|
274
|
+
return gunzipSync(stored);
|
|
275
|
+
} catch {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The JSON of one frame read out of a file.
|
|
282
|
+
*
|
|
283
|
+
* A frame is a base64 line — base64 because gzip contains newlines, which would
|
|
284
|
+
* make the newline delimiter ambiguous. The bytes are gunzipped only if they
|
|
285
|
+
* are gzip; `gunzipFrame` returning `undefined` is what marks a slice as not a
|
|
286
|
+
* frame.
|
|
287
|
+
*/
|
|
288
|
+
function frameJsonFromSlice(
|
|
289
|
+
slice: Buffer,
|
|
290
|
+
): Buffer | undefined {
|
|
291
|
+
// Frames are a base64 line. The trailing newline is not part of the payload.
|
|
292
|
+
const text = slice.toString("utf8").trim();
|
|
293
|
+
if (text.length === 0) return undefined;
|
|
294
|
+
const gzipped = Buffer.from(text, "base64");
|
|
295
|
+
return gunzipFrame(gzipped);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* What a whole file's parseable frames say about reclaiming it.
|
|
300
|
+
*
|
|
301
|
+
* `live` — at least one frame is still inside its retention window.
|
|
302
|
+
* `none` — every frame was read and every one has expired.
|
|
303
|
+
* `unknown` — parsing stopped on a torn tail or a corrupt frame, so the rest
|
|
304
|
+
* of the file is unreadable and cannot be assumed empty of live frames.
|
|
305
|
+
*/
|
|
306
|
+
type FrameLiveness = "live" | "none" | "unknown";
|
|
307
|
+
|
|
308
|
+
function frameLiveness(data: Buffer, before: Date): FrameLiveness {
|
|
309
|
+
let offset = 0;
|
|
310
|
+
while (offset < data.byteLength) {
|
|
311
|
+
const newline = data.indexOf(0x0a, offset);
|
|
312
|
+
if (newline <= 0) return "unknown";
|
|
313
|
+
const end = newline + 1;
|
|
314
|
+
const slice = data.subarray(offset, end);
|
|
315
|
+
const json = frameJsonFromSlice(slice);
|
|
316
|
+
if (json === undefined) return "unknown";
|
|
317
|
+
let frame: PayloadFrame;
|
|
318
|
+
try {
|
|
319
|
+
frame = JSON.parse(json.toString("utf8")) as PayloadFrame;
|
|
320
|
+
} catch {
|
|
321
|
+
return "unknown";
|
|
322
|
+
}
|
|
323
|
+
if (new Date(frame.expiresAt).getTime() >= before.getTime()) return "live";
|
|
324
|
+
offset = end;
|
|
325
|
+
}
|
|
326
|
+
// Bytes left over that are too short to hold a frame are an unreadable tail.
|
|
327
|
+
return offset === data.byteLength ? "none" : "unknown";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Reclaims frame files that no longer hold a live payload.
|
|
332
|
+
*
|
|
333
|
+
* Frames are append-only and this pass never rewrites a file. That is a
|
|
334
|
+
* correctness requirement, not an optimization: a `telemetry_payloads` row
|
|
335
|
+
* addresses its body by file + offset + length, so compacting a file in place
|
|
336
|
+
* would shift every later frame and leave live rows pointing at the wrong
|
|
337
|
+
* bytes — the drawer then reads `undefined` and the payload silently vanishes
|
|
338
|
+
* while its row is still current. A file is dropped whole, and only once
|
|
339
|
+
* nothing in it is still live.
|
|
340
|
+
*
|
|
341
|
+
* `unparseableWrittenBefore` bounds the damaged-file case. A region that
|
|
342
|
+
* cannot be parsed cannot prove it holds no live frame, so such a file is kept
|
|
343
|
+
* until its last write is older than that instant — every frame it could hold
|
|
344
|
+
* was written before then and has therefore expired. A caller that cannot
|
|
345
|
+
* supply that bound leaves the default, which keeps damaged files rather than
|
|
346
|
+
* risk dropping a frame a row still addresses.
|
|
347
|
+
*
|
|
348
|
+
* One damaged file must not abort the pass. If a single corrupt frame threw
|
|
349
|
+
* straight out of the loop, every other expired file would stay on disk
|
|
350
|
+
* forever and the volume would only grow — and because a container restart is
|
|
351
|
+
* the usual way a half-written tail appears, the failure would look
|
|
352
|
+
* Docker-specific.
|
|
353
|
+
*/
|
|
354
|
+
export async function prunePayloadFrames(
|
|
355
|
+
before: Date,
|
|
356
|
+
unparseableWrittenBefore: Date = new Date(0),
|
|
357
|
+
): Promise<number> {
|
|
358
|
+
const directory = payloadDirectory();
|
|
359
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
360
|
+
let deleted = 0;
|
|
361
|
+
for (const entry of entries) {
|
|
362
|
+
if (!entry.isFile() || !isFrameFileName(entry.name)) continue;
|
|
363
|
+
const path = join(directory, entry.name);
|
|
364
|
+
try {
|
|
365
|
+
if (await fileReclaimable(path, before, unparseableWrittenBefore)) {
|
|
366
|
+
await unlink(path);
|
|
367
|
+
deleted += 1;
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
// Stat, read, or unlink failed on this one file. Leave it for the next
|
|
371
|
+
// pass rather than abandoning every remaining file's reclaim.
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return deleted;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* True when the file can be deleted without losing a frame a row still
|
|
379
|
+
* addresses. A readable file is reclaimable once every frame has expired; a
|
|
380
|
+
* damaged one only once `unparseableWrittenBefore` rules out a live frame in
|
|
381
|
+
* the part that could not be parsed.
|
|
382
|
+
*/
|
|
383
|
+
async function fileReclaimable(
|
|
384
|
+
path: string,
|
|
385
|
+
before: Date,
|
|
386
|
+
unparseableWrittenBefore: Date,
|
|
387
|
+
): Promise<boolean> {
|
|
388
|
+
const data = await readFile(path);
|
|
389
|
+
// Nothing written yet, so nothing to lose. A real frame is far longer than
|
|
390
|
+
// any header this format could carry.
|
|
391
|
+
if (data.byteLength === 0) return true;
|
|
392
|
+
const liveness = frameLiveness(data, before);
|
|
393
|
+
if (liveness === "live") return false;
|
|
394
|
+
if (liveness === "none") return true;
|
|
395
|
+
const written = await stat(path).then((value) => value.mtimeMs);
|
|
396
|
+
return written <= unparseableWrittenBefore.getTime();
|
|
397
|
+
}
|