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
|
@@ -28,9 +28,9 @@ export class ConsoleCredentialService {
|
|
|
28
28
|
* undecodable hashes: a corrupted `password_hash` row must surface as an
|
|
29
29
|
* ordinary failed login (counted by the lockout service), never as a 500
|
|
30
30
|
* that lets an attacker probe hash integrity or bypass the failure counter.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* A hash that does not start with `$argon2id$` is rejected here rather than
|
|
32
|
+
* verified — it can only originate from a foreign scheme this deployment
|
|
33
|
+
* never issued.
|
|
34
34
|
*/
|
|
35
35
|
async verifyPassword(password: string, hash: string): Promise<boolean> {
|
|
36
36
|
if (
|
|
@@ -160,8 +160,8 @@ export class FirstBootSetupService {
|
|
|
160
160
|
// First boot creates the default tenant, the platform-admin user, and the
|
|
161
161
|
// gateway key. The advisory lock is what stops two concurrent boots from
|
|
162
162
|
// both passing the "no admin yet" check and racing to create them, so a
|
|
163
|
-
// handle that cannot take the lock must fail closed — the
|
|
164
|
-
//
|
|
163
|
+
// handle that cannot take the lock must fail closed — running the setup
|
|
164
|
+
// unserialized lets two boots both pass the check.
|
|
165
165
|
await this.db.transaction(async (transaction) => {
|
|
166
166
|
await transaction.execute(firstBootSetupLock());
|
|
167
167
|
await create(transaction as unknown as CartethyiaDatabase);
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* as sensitive as the database.
|
|
21
21
|
*/
|
|
22
22
|
import { getTableColumns, getTableName, type Column, type Table } from "drizzle-orm";
|
|
23
|
+
import { resolveMaxBodyBytes } from "../../config/runtime";
|
|
23
24
|
import {
|
|
24
25
|
apiKeys,
|
|
25
26
|
cliToolMappings,
|
|
@@ -45,8 +46,20 @@ import {
|
|
|
45
46
|
export const BACKUP_APP = "cartethyia";
|
|
46
47
|
export const BACKUP_VERSION = 1;
|
|
47
48
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Upper bound for restore payloads.
|
|
51
|
+
*
|
|
52
|
+
* Deliberately not larger than the listener's own body cap. It used to claim
|
|
53
|
+
* 8 GiB while the server admitted 4 MiB, so the number described a limit that
|
|
54
|
+
* could never be reached: a file above the listener's cap is cut off before
|
|
55
|
+
* this check runs, and a large upload fails as a reset connection rather than
|
|
56
|
+
* the 413 this constant promises. The bound now states what the install
|
|
57
|
+
* actually admits, so the message an operator sees matches what happened.
|
|
58
|
+
*
|
|
59
|
+
* Raise `CARTETHYIA_SERVER_MAX_BODY_BYTES` to accept larger backups; this
|
|
60
|
+
* constant follows it rather than the two drifting apart again.
|
|
61
|
+
*/
|
|
62
|
+
export const MAX_BACKUP_BYTES = resolveMaxBodyBytes();
|
|
50
63
|
|
|
51
64
|
/** Defense-in-depth row cap per table, after the byte bound. */
|
|
52
65
|
export const MAX_ROWS_PER_TABLE = 500_000;
|
|
@@ -47,10 +47,14 @@ export function createBackupExportHandler(
|
|
|
47
47
|
deps: BackupExportDeps,
|
|
48
48
|
): (context: { request: Request; body: unknown }) => Promise<Response> {
|
|
49
49
|
return async ({ request, body }) => {
|
|
50
|
+
// `policy_rejected` / `authentication_failed`, not `invalid_request` for
|
|
51
|
+
// both: neither refusal is about the body. The first is where the request
|
|
52
|
+
// came from, the second is the credential it carried — and `invalid_request`
|
|
53
|
+
// told an operator to fix a body that was never the problem.
|
|
50
54
|
if (!isLoopbackPeer(deps.resolvePeerAddress(request)))
|
|
51
|
-
throw new GatewayError("
|
|
55
|
+
throw new GatewayError("policy_rejected", 403, "backup export is only available from loopback");
|
|
52
56
|
if (!tokenMatches(request.headers.get("x-drain-token"), deps.token))
|
|
53
|
-
throw new GatewayError("
|
|
57
|
+
throw new GatewayError("authentication_failed", 401, "invalid export token");
|
|
54
58
|
|
|
55
59
|
const directory =
|
|
56
60
|
body !== null && typeof body === "object" && "directory" in body
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import type { BackupPayload, BackupRow } from "./contracts";
|
|
23
23
|
import { BACKUP_APP, BACKUP_VERSION } from "./contracts";
|
|
24
|
+
import { isRecord } from "../../protocol/codecs";
|
|
24
25
|
import { hashSecret } from "../../security/crypto";
|
|
25
26
|
import {
|
|
26
27
|
AUTH_STATE_KEYS,
|
|
@@ -97,7 +98,7 @@ const OAUTH_PROVIDERS = new Set(["claude", "codex", "antigravity", "kimi", "clin
|
|
|
97
98
|
|
|
98
99
|
/** The non-secret per-account config, as our `auth_state` bag. */
|
|
99
100
|
function authStateOf(entry: Row): Record<string, unknown> {
|
|
100
|
-
const specific = isRecord(entry.providerSpecificData) ? entry.providerSpecificData : {};
|
|
101
|
+
const specific = isRecord(entry.providerSpecificData) ? entry.providerSpecificData as Row : {};
|
|
101
102
|
const state: Record<string, unknown> = {};
|
|
102
103
|
for (const key of AUTH_STATE_KEYS) {
|
|
103
104
|
const value = specific[key];
|
|
@@ -111,10 +112,6 @@ function emailOf(entry: Row): Record<string, unknown> {
|
|
|
111
112
|
return email === null ? {} : { account_email: email };
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
function isRecord(value: unknown): value is Row {
|
|
115
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
115
|
/**
|
|
119
116
|
* Splits `user:pass@host` userinfo out of a proxy URL.
|
|
120
117
|
*
|
|
@@ -374,6 +374,10 @@ export class BackupService {
|
|
|
374
374
|
const validation = validateRestorePayload(candidate, "");
|
|
375
375
|
const warnings = validation.ok ? validation.value.warnings : [];
|
|
376
376
|
const supported = new Set(validation.ok ? [...validation.value.tables.keys()].map(tableName) : []);
|
|
377
|
+
// `tenants` is diverted to `validation.value.tenantRows` during validate —
|
|
378
|
+
// not included in `tables` — so it would show as "unsupported" in the
|
|
379
|
+
// preview for any backup that carries it. Mark it supported explicitly.
|
|
380
|
+
if (validation.ok) supported.add("tenants");
|
|
377
381
|
|
|
378
382
|
const rows: BackupInspectionRow[] = [];
|
|
379
383
|
let total = 0;
|
|
@@ -283,13 +283,24 @@ export async function exportBackup(
|
|
|
283
283
|
const config: Record<string, readonly BackupRow[]> = {};
|
|
284
284
|
const telemetry: Record<string, readonly BackupRow[]> = {};
|
|
285
285
|
const counts: Record<string, number> = {};
|
|
286
|
-
const configSet = new Set<Table>([...CONFIG_TABLES
|
|
286
|
+
const configSet = new Set<Table>([...CONFIG_TABLES]);
|
|
287
287
|
const telemetrySet = new Set<Table>(TELEMETRY_TABLES);
|
|
288
288
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
289
|
+
// Table reads are independent tenant-scoped `SELECT`s plus one disk read for
|
|
290
|
+
// the event file; firing them concurrently cuts a many-table export from
|
|
291
|
+
// one round-trip-at-a-time to the single slowest query. The DB driver pools
|
|
292
|
+
// these; the table list is bounded by `CONFIG_TABLES + TELEMETRY_TABLES`
|
|
293
|
+
// (small and known), so fan-out is bounded without an extra limiter.
|
|
294
|
+
const reads = tables.map(async (table) =>
|
|
295
|
+
[
|
|
296
|
+
table,
|
|
297
|
+
table === telemetryEvents ? await exportEventFile(tenantId) : await exportTable(db, table, tenantId),
|
|
298
|
+
] as const,
|
|
299
|
+
);
|
|
300
|
+
// Preserve the caller's `tables` order when writing into the output maps so
|
|
301
|
+
// the payload's key order is deterministic across runs.
|
|
302
|
+
const settled = await Promise.all(reads);
|
|
303
|
+
for (const [table, rows] of settled) {
|
|
293
304
|
const name = tableName(table);
|
|
294
305
|
counts[name] = rows.length;
|
|
295
306
|
if (configSet.has(table)) config[name] = rows;
|
|
@@ -517,12 +528,22 @@ async function clearOwnedRows(
|
|
|
517
528
|
}
|
|
518
529
|
|
|
519
530
|
/**
|
|
520
|
-
* Inserts rows
|
|
531
|
+
* Inserts rows in bulk with a per-row fallback for diagnostics.
|
|
521
532
|
*
|
|
522
533
|
* `conflictTarget` is supplied for tables a restore may not delete from: the
|
|
523
534
|
* payload's rows are upserted by the table's unique key, so re-importing a
|
|
524
535
|
* backup updates what is already there instead of failing on the first
|
|
525
536
|
* duplicate.
|
|
537
|
+
*
|
|
538
|
+
* Historically rows went in one statement at a time so a single bad row could
|
|
539
|
+
* name its own failure. On a real backup — thousands of telemetry events, a
|
|
540
|
+
* tenant's models and accounts — that cost one DB round-trip per row and the
|
|
541
|
+
* restore stalled on network latency rather than the server's write path. The
|
|
542
|
+
* fast path collapses those statements into one `INSERT ... VALUES (...), ...`
|
|
543
|
+
* per chunk; if a chunk fails for any reason (bad row, constraint, oversized
|
|
544
|
+
* parameter list) the chunk is re-run one row at a time so the diagnostic
|
|
545
|
+
* surface is unchanged. The slow path is only reached on real errors, so a
|
|
546
|
+
* clean restore pays zero penalty for the diagnostic.
|
|
526
547
|
*/
|
|
527
548
|
async function insertRows(
|
|
528
549
|
tx: CartethyiaDatabase,
|
|
@@ -535,46 +556,171 @@ async function insertRows(
|
|
|
535
556
|
readonly ignoreConflicts?: boolean;
|
|
536
557
|
} = {},
|
|
537
558
|
): Promise<number> {
|
|
538
|
-
|
|
559
|
+
// Normalise rows up front: filter skips, drop empty-value rows (no
|
|
560
|
+
// insertable columns), and project to Drizzle values. Doing it once keeps
|
|
561
|
+
// the two paths from diverging in what counts as insertable.
|
|
562
|
+
const prepared: Record<string, unknown>[] = [];
|
|
539
563
|
for (const row of rows) {
|
|
540
564
|
if (options.skipKeys !== undefined && options.keyOf !== undefined && options.skipKeys.has(options.keyOf(row))) {
|
|
541
565
|
continue;
|
|
542
566
|
}
|
|
543
567
|
const values = toValues(table, row);
|
|
544
568
|
if (Object.keys(values).length === 0) continue;
|
|
569
|
+
prepared.push(values);
|
|
570
|
+
}
|
|
571
|
+
if (prepared.length === 0) return 0;
|
|
572
|
+
|
|
573
|
+
// The `set` clause for upserts: every column the payload carries is written,
|
|
574
|
+
// so a re-import restores the file's state rather than leaving stale values
|
|
575
|
+
// behind. `excluded` is the row that failed to insert, and column names come
|
|
576
|
+
// from the schema rather than the JS key — guessing the database spelling
|
|
577
|
+
// from a camelCase key breaks the moment the two differ.
|
|
578
|
+
const columnNameMap = new Map(
|
|
579
|
+
Object.entries(getTableColumns(table)).map(([key, column]) => [key, column.name]),
|
|
580
|
+
);
|
|
581
|
+
const buildUpsertSet = (values: Record<string, unknown>): Record<string, unknown> =>
|
|
582
|
+
Object.fromEntries(
|
|
583
|
+
Object.keys(values).map((key) => {
|
|
584
|
+
const name = columnNameMap.get(key);
|
|
585
|
+
if (name === undefined) throw new Error(`${tableName(table)} has no column ${key}`);
|
|
586
|
+
return [key, sql.raw(`excluded."${name}"`)];
|
|
587
|
+
}),
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
// One row's path, kept as its own function so the bulk fallback can reuse it
|
|
591
|
+
// without duplicating the control flow.
|
|
592
|
+
const insertOne = async (values: Record<string, unknown>): Promise<number> => {
|
|
545
593
|
const statement = insertRow(tx, table, values);
|
|
546
594
|
if (options.ignoreConflicts) {
|
|
547
|
-
if (options.conflictTarget === undefined)
|
|
595
|
+
if (options.conflictTarget === undefined) {
|
|
548
596
|
throw new Error(`ignoreConflicts requires a target for ${tableName(table)}`);
|
|
597
|
+
}
|
|
549
598
|
const insertedRows = await statement
|
|
550
599
|
.onConflictDoNothing({ target: options.conflictTarget as never })
|
|
551
600
|
.returning();
|
|
552
|
-
|
|
553
|
-
continue;
|
|
601
|
+
return insertedRows.length;
|
|
554
602
|
}
|
|
555
603
|
if (options.conflictTarget === undefined) {
|
|
556
604
|
await statement;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
605
|
+
return 1;
|
|
606
|
+
}
|
|
607
|
+
await statement.onConflictDoUpdate({
|
|
608
|
+
target: options.conflictTarget as never,
|
|
609
|
+
set: buildUpsertSet(values) as never,
|
|
610
|
+
});
|
|
611
|
+
return 1;
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
// Bulk chunk size: keeps each statement's parameter count comfortably under
|
|
615
|
+
// Postgres's 65535 limit even for the widest table. 500 rows × ~60 columns
|
|
616
|
+
// leaves plenty of headroom; a narrower table just means a faster chunk.
|
|
617
|
+
const BULK_CHUNK = 500;
|
|
618
|
+
|
|
619
|
+
// Attempts one chunk in a single statement. Returns the inserted count on
|
|
620
|
+
// success, or `null` to signal "fall back to per-row for this chunk". The
|
|
621
|
+
// chunk is wrapped in a savepoint so a bulk failure can be rolled back
|
|
622
|
+
// *inside* the outer restore transaction — without this, Postgres marks the
|
|
623
|
+
// outer transaction as aborted on the first error and the per-row diagnostic
|
|
624
|
+
// fallback would itself fail with "current transaction is aborted".
|
|
625
|
+
const insertChunk = async (
|
|
626
|
+
chunk: readonly Record<string, unknown>[],
|
|
627
|
+
): Promise<{ count: number } | { error: unknown }> => {
|
|
628
|
+
try {
|
|
629
|
+
// `tx.transaction(...)` nests as a `SAVEPOINT ... ROLLBACK TO SAVEPOINT`
|
|
630
|
+
// on both Postgres and SQLite drivers Drizzle supports here, which is
|
|
631
|
+
// exactly what the fallback needs: on failure the outer transaction
|
|
632
|
+
// stays writable and the per-row path can isolate the offending row.
|
|
633
|
+
const count = await tx.transaction(async (innerTx) => {
|
|
634
|
+
// Drizzle types the nested transaction callback with the dialect's
|
|
635
|
+
// own transaction handle; cast once to a named local with a reason,
|
|
636
|
+
// never inline on member access.
|
|
637
|
+
const inner = innerTx as unknown as CartethyiaDatabase; // nested-tx handle; mirrors applyRestore's outer `scoped` cast on line 607.
|
|
638
|
+
const statement = inner.insert(table).values(chunk as never);
|
|
639
|
+
if (options.ignoreConflicts) {
|
|
640
|
+
if (options.conflictTarget === undefined) {
|
|
641
|
+
throw new Error(`ignoreConflicts requires a target for ${tableName(table)}`);
|
|
642
|
+
}
|
|
643
|
+
const insertedRows = await statement
|
|
644
|
+
.onConflictDoNothing({ target: options.conflictTarget as never })
|
|
645
|
+
.returning();
|
|
646
|
+
return insertedRows.length;
|
|
647
|
+
}
|
|
648
|
+
if (options.conflictTarget === undefined) {
|
|
649
|
+
await statement;
|
|
650
|
+
return chunk.length;
|
|
651
|
+
}
|
|
652
|
+
// For a bulk upsert `excluded.*` references the row the driver tried
|
|
653
|
+
// to insert, so each column mapping is the same regardless of which
|
|
654
|
+
// row in the chunk conflicts. Build the set clause from the first
|
|
655
|
+
// row's keys — every prepared row for one table carries the same key
|
|
656
|
+
// set because `toValues` is deterministic for a given schema.
|
|
657
|
+
const firstRow = chunk[0];
|
|
658
|
+
if (firstRow === undefined) return 0;
|
|
659
|
+
await statement.onConflictDoUpdate({
|
|
660
|
+
target: options.conflictTarget as never,
|
|
661
|
+
set: buildUpsertSet(firstRow) as never,
|
|
662
|
+
});
|
|
663
|
+
return chunk.length;
|
|
575
664
|
});
|
|
665
|
+
return { count };
|
|
666
|
+
} catch (error) {
|
|
667
|
+
// Signal "re-run row by row": a bulk failure must not swallow the
|
|
668
|
+
// specific row's error, because the per-row path is what names the
|
|
669
|
+
// offending row to the operator. Keep the bulk error too: when the
|
|
670
|
+
// per-row fallback also fails (connection drop, aborted transaction,
|
|
671
|
+
// oversized statement), the derived row error would otherwise hide the
|
|
672
|
+
// real cause from the operator.
|
|
673
|
+
return { error };
|
|
576
674
|
}
|
|
577
|
-
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
let inserted = 0;
|
|
678
|
+
// Group adjacent rows that share the same column key set. Different rows in
|
|
679
|
+
// one table can carry different keys (an optional column omitted when the
|
|
680
|
+
// payload cell is absent; `parent_key_id` present on child api_keys but not
|
|
681
|
+
// parents), and Drizzle's bulk `.values([...])` compiles one SQL statement
|
|
682
|
+
// with a column list taken from the first row — so a bulk of mixed shapes
|
|
683
|
+
// produces malformed SQL. Grouping by signature keeps each bulk statement
|
|
684
|
+
// uniform while preserving row order within the group.
|
|
685
|
+
//
|
|
686
|
+
// Signature is a stable `|`-joined list of sorted keys; empty columns were
|
|
687
|
+
// already dropped by the `toValues` → `Object.keys(values).length === 0`
|
|
688
|
+
// check above, so empty signatures never occur here.
|
|
689
|
+
const signatureOf = (values: Record<string, unknown>): string =>
|
|
690
|
+
Object.keys(values).sort().join("|");
|
|
691
|
+
let groupStart = 0;
|
|
692
|
+
while (groupStart < prepared.length) {
|
|
693
|
+
const groupSignature = signatureOf(prepared[groupStart]!);
|
|
694
|
+
let groupEnd = groupStart + 1;
|
|
695
|
+
while (
|
|
696
|
+
groupEnd < prepared.length &&
|
|
697
|
+
signatureOf(prepared[groupEnd]!) === groupSignature &&
|
|
698
|
+
groupEnd - groupStart < BULK_CHUNK
|
|
699
|
+
) {
|
|
700
|
+
groupEnd += 1;
|
|
701
|
+
}
|
|
702
|
+
const chunk = prepared.slice(groupStart, groupEnd);
|
|
703
|
+
const bulk = await insertChunk(chunk);
|
|
704
|
+
if ("count" in bulk) {
|
|
705
|
+
inserted += bulk.count;
|
|
706
|
+
} else {
|
|
707
|
+
// Diagnostic fallback: let the single statement that actually errors
|
|
708
|
+
// reach the caller with the row that caused it. If the fallback itself
|
|
709
|
+
// fails, the row-level error is derived — chain the original bulk error
|
|
710
|
+
// so a connection-level fault is not reported as a bad row.
|
|
711
|
+
try {
|
|
712
|
+
for (const values of chunk) {
|
|
713
|
+
inserted += await insertOne(values);
|
|
714
|
+
}
|
|
715
|
+
} catch (perRowError) {
|
|
716
|
+
const bulkMessage = bulk.error instanceof Error ? bulk.error.message : String(bulk.error);
|
|
717
|
+
throw new Error(
|
|
718
|
+
`bulk insert into ${tableName(table)} failed (${bulkMessage}); per-row diagnostic also failed`,
|
|
719
|
+
{ cause: perRowError },
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
groupStart = groupEnd;
|
|
578
724
|
}
|
|
579
725
|
return inserted;
|
|
580
726
|
}
|
|
@@ -610,6 +756,14 @@ export async function applyRestore(
|
|
|
610
756
|
// its PK is `id`, so a conflict means "already there" rather than an error.
|
|
611
757
|
// The payload's tenant is forced to the restoring tenant — a payload naming
|
|
612
758
|
// someone else's tenant id must not create or claim that tenant.
|
|
759
|
+
//
|
|
760
|
+
// Current exports deliberately omit the `tenants` section, so
|
|
761
|
+
// `tenantRows` is normally empty. Child rows still carry
|
|
762
|
+
// `tenant_id = tenantId`, which means a restore into a tenant that does
|
|
763
|
+
// not exist yet would fail the `api_keys` (and other) foreign keys unless
|
|
764
|
+
// we ensure the receiving tenant here. The minimal placeholder row is
|
|
765
|
+
// written only for a tenant that did not already exist; on conflict it is
|
|
766
|
+
// a no-op and the existing tenant's name/status are left untouched.
|
|
613
767
|
if (validation.tenantRows.length > 0) {
|
|
614
768
|
let inserted = 0;
|
|
615
769
|
for (const row of validation.tenantRows) {
|
|
@@ -621,6 +775,15 @@ export async function applyRestore(
|
|
|
621
775
|
inserted += result.length;
|
|
622
776
|
}
|
|
623
777
|
restored["tenants"] = inserted;
|
|
778
|
+
} else {
|
|
779
|
+
const result = await insertRow(scoped, TENANT_TABLE, {
|
|
780
|
+
id: tenantId,
|
|
781
|
+
name: "restored",
|
|
782
|
+
status: "active",
|
|
783
|
+
})
|
|
784
|
+
.onConflictDoNothing()
|
|
785
|
+
.returning({ id: sql<number>`1` });
|
|
786
|
+
restored["tenants"] = result.length;
|
|
624
787
|
}
|
|
625
788
|
|
|
626
789
|
const replaceTables = order.filter(
|
|
@@ -36,18 +36,13 @@ import {
|
|
|
36
36
|
type ValidatedRestore,
|
|
37
37
|
type ValidatedTable,
|
|
38
38
|
} from "./contracts";
|
|
39
|
-
import { DEFAULT_API_KEY_LABEL } from "../api-keys/contracts";
|
|
40
39
|
import {
|
|
41
|
-
apiKeys,
|
|
42
40
|
modelCombos,
|
|
43
41
|
networkPools,
|
|
44
42
|
providerRoutingSettings,
|
|
45
43
|
shareLinks,
|
|
46
44
|
} from "../../persistence/schema";
|
|
47
45
|
|
|
48
|
-
const LEGACY_PROVIDER_USER_AGENT = "codex_cli_rs/0.156.1";
|
|
49
|
-
const CURRENT_PROVIDER_USER_AGENT = "claude-cli/2.1.293 (external, cli)";
|
|
50
|
-
|
|
51
46
|
/**
|
|
52
47
|
* Provider ids that were renamed, old spelling to current.
|
|
53
48
|
*
|
|
@@ -68,51 +63,14 @@ function currentProviderId(value: unknown): unknown {
|
|
|
68
63
|
return RENAMED_PROVIDER_IDS[value] ?? value;
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
function legacyModelList(value: unknown): readonly string[] | undefined {
|
|
72
|
-
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) return undefined;
|
|
73
|
-
return value;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function normalizeLegacyApiKeyPolicy(row: Record<string, unknown>): Record<string, unknown> {
|
|
77
|
-
const allowlist = legacyModelList(row["model_allowlist"]);
|
|
78
|
-
const denylist = legacyModelList(row["model_denylist"]);
|
|
79
|
-
if (row["model_access_mode"] !== undefined || row["model_list"] !== undefined) return row;
|
|
80
|
-
if (denylist !== undefined && denylist.length > 0) {
|
|
81
|
-
return { ...row, model_access_mode: "blacklist", model_list: denylist };
|
|
82
|
-
}
|
|
83
|
-
if (allowlist !== undefined) {
|
|
84
|
-
return { ...row, model_access_mode: "whitelist", model_list: allowlist };
|
|
85
|
-
}
|
|
86
|
-
return row;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
66
|
function normalizeLegacyRow(table: Table, row: Record<string, unknown>): Record<string, unknown> {
|
|
90
67
|
if (row["provider_id"] !== undefined) {
|
|
91
68
|
const renamed = currentProviderId(row["provider_id"]);
|
|
92
69
|
if (renamed !== row["provider_id"]) row = { ...row, provider_id: renamed };
|
|
93
70
|
}
|
|
94
|
-
if (
|
|
95
|
-
table === apiKeys &&
|
|
96
|
-
row["is_default"] === undefined &&
|
|
97
|
-
(row["key_mode"] === undefined || row["key_mode"] === "personal") &&
|
|
98
|
-
row["parent_key_id"] == null &&
|
|
99
|
-
row["label"] === DEFAULT_API_KEY_LABEL
|
|
100
|
-
) {
|
|
101
|
-
return { ...row, is_default: true };
|
|
102
|
-
}
|
|
103
71
|
if (table === providerRoutingSettings) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (normalized["skip_proxy"] === undefined && legacySkipProxy !== undefined) {
|
|
107
|
-
normalized = { ...normalized, skip_proxy: legacySkipProxy };
|
|
108
|
-
}
|
|
109
|
-
if (normalized["provider_id"] === "cline") {
|
|
110
|
-
normalized = { ...normalized, skip_proxy: true };
|
|
111
|
-
}
|
|
112
|
-
if (normalized["user_agent"] === LEGACY_PROVIDER_USER_AGENT) {
|
|
113
|
-
normalized = { ...normalized, user_agent: CURRENT_PROVIDER_USER_AGENT };
|
|
114
|
-
}
|
|
115
|
-
return normalized;
|
|
72
|
+
if (row["provider_id"] === "cline") return { ...row, skip_proxy: true };
|
|
73
|
+
return row;
|
|
116
74
|
}
|
|
117
75
|
if (table === networkPools && row["kind"] === "bridge") {
|
|
118
76
|
return { ...row, kind: "https" };
|
|
@@ -342,7 +300,7 @@ export function validateRestorePayload(payload: unknown, tenantId: string): Rest
|
|
|
342
300
|
// the current single policy before dropping retired column names.
|
|
343
301
|
const sourceRow = normalizeLegacyRow(
|
|
344
302
|
table,
|
|
345
|
-
|
|
303
|
+
row,
|
|
346
304
|
);
|
|
347
305
|
// A stale backup may still carry columns the schema has since dropped.
|
|
348
306
|
// Those are accepted but removed here, so they never reach the store
|