cartethyia 2.4.5 → 2.5.0
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 +4 -1
- package/README.md +63 -13
- package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
- package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
- package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-D3xqZbN-.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
- package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-CTTpCb25.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-DHoB3Agn.js} +1 -1
- package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +1 -0
- package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
- package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
- package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
- package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-CvjZxHE4.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-D3nza7LH.js +1 -0
- package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Bol1ASmI.js → SortableList-Dtt12WT1.js} +1 -1
- package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CbK3hXvU.js +31 -0
- package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
- package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BmDbsior.js +1 -0
- package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
- package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
- package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
- package/dist/dashboard/assets/{app-BDJWMThu.js → app-C-Y4oZT0.js} +3 -3
- package/dist/dashboard/assets/app-C-Y4oZT0.js.gz +0 -0
- package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
- package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
- package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
- package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
- package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-DpXT4Ixt.js} +1 -1
- package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-CSAAWLJv.js} +1 -1
- package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
- package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
- package/dist/dashboard/assets/{card-BTdbRFFg.js → card-DEhRXXw4.js} +1 -1
- package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
- package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
- package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
- package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
- package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-B0wBq-RL.js +1 -0
- package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +1 -0
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
- package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
- package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BvQeBMfP.js} +1 -1
- package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
- package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-CMwPXwQB.js} +1 -1
- package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
- package/dist/dashboard/assets/entry-D4O2229X.js +1 -0
- package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
- package/dist/dashboard/assets/entry-DKvr5Q9K.js +1 -0
- package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
- package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
- package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
- package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
- package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-zliLvBwr.js +1 -0
- package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
- package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
- package/dist/dashboard/assets/{index-C4hXfAMs.js → index-CXm3FIyt.js} +2 -2
- package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1IlZZ8H.js} +1 -1
- package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
- package/dist/dashboard/assets/{input-Bn4_kNav.js → input-BpVZdrbc.js} +1 -1
- package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
- package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
- package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
- package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
- package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BioLW7gN.js} +1 -1
- package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-CwVcGAIc.js} +1 -1
- package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
- package/dist/dashboard/assets/{live-YlifPYox.js → live-yfEJFF9T.js} +1 -1
- package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
- package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
- package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CT-TxSNX.js +1 -0
- package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
- package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
- package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
- package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
- package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
- package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
- package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
- package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
- package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
- package/dist/dashboard/assets/providers/cursor.webp +0 -0
- package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
- package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
- package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
- package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
- package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
- package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
- package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
- package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
- package/dist/dashboard/assets/providers/trae.webp +0 -0
- package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
- package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
- package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
- package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
- package/dist/dashboard/assets/routing-Cs6jl49L.js +1 -0
- package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
- package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
- package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
- package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
- package/dist/dashboard/assets/select-C0Rer3gF.js.gz +0 -0
- package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
- package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
- package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-C9_uUjaA.js} +1 -1
- package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
- package/dist/dashboard/assets/{state-DHKdg-pm.js → state-DA0B95UQ.js} +1 -1
- package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-Cittxy-x.js} +1 -1
- package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
- package/dist/dashboard/assets/toast-CNt_jzO4.js +2 -0
- package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
- package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
- package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-BQKMcJ1H.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
- package/dist/dashboard/assets/wrench-B6E1oEuY.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 +122 -0
- package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
- package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
- package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
- package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
- package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
- package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
- package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
- package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
- package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
- package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
- package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
- package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
- package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
- package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
- package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
- package/package.json +26 -30
- package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
- package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
- package/scripts/cli/command/admin.ts +65 -0
- package/scripts/cli/command/console-menu.ts +246 -0
- package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
- package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
- package/scripts/cli/command/start.ts +77 -0
- package/scripts/cli/command/tray.ts +165 -0
- package/scripts/cli/command/update.ts +23 -0
- package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
- package/scripts/cli/gateway/instance-lock.ts +105 -0
- package/scripts/cli/gateway/spawn.ts +173 -0
- package/scripts/cli/gateway/stop.ts +51 -0
- package/scripts/cli/main.ts +94 -0
- package/scripts/cli/support/bun.ts +27 -0
- package/scripts/cli/support/env.ts +99 -0
- package/scripts/cli/support/platform.ts +64 -0
- package/scripts/cli/support/services.ts +88 -0
- package/scripts/cli/support/startup-signal.ts +23 -0
- package/scripts/cli/{version.ts → support/version.ts} +1 -1
- package/scripts/cli/tray/hidden-window.vbs +28 -0
- package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
- package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
- package/scripts/cli/ui/background.ts +110 -0
- package/src/boot/app.ts +23 -7
- package/src/boot/dependencies.ts +55 -13
- package/src/boot/elysia-typebox-bridge.ts +37 -0
- package/src/boot/lifecycle.ts +2 -0
- package/src/config/cooldowns.ts +138 -138
- package/src/config/providers.ts +31 -5
- package/src/config/runtime.ts +19 -3
- package/src/config/spec.ts +20 -0
- package/src/console/api-keys/contracts.ts +414 -391
- package/src/console/api-keys/routes.ts +880 -870
- package/src/console/auth/account-reset-store.ts +14 -0
- package/src/console/auth/account-reset.ts +3 -2
- package/src/console/auth/setup-service.ts +6 -3
- package/src/console/backup/routes.ts +8 -0
- package/src/console/cli-tools/contracts.ts +6 -6
- package/src/console/console-router.ts +5 -3
- package/src/console/dashboard-assets.ts +81 -11
- package/src/console/domain-registration.ts +9 -2
- package/src/console/observability/contracts.ts +475 -423
- package/src/console/observability/routes.ts +31 -6
- package/src/console/observability/store.ts +1015 -912
- package/src/console/providers/catalog/contracts.ts +31 -0
- package/src/console/providers/catalog/provider-operations.ts +8 -0
- package/src/console/providers/catalog/public-model-store.ts +91 -60
- package/src/console/providers/catalog/store.ts +1532 -1521
- package/src/console/providers/oauth/callback-listener.ts +18 -3
- package/src/console/providers/oauth/routes.ts +38 -15
- package/src/console/quota/account-quota-tenant.ts +9 -6
- package/src/console/quota/refresh.ts +19 -0
- package/src/console/response-compression.ts +69 -2
- package/src/console/routing/pools/probe-verdict.ts +72 -0
- package/src/console/share/share-router.ts +7 -4
- package/src/console/share/share-stats.ts +12 -5
- package/src/console/shared/read-through-cache.ts +133 -7
- package/src/console/updates/repository-updates.ts +39 -34
- package/src/console/updates/routes.ts +162 -4
- package/src/main.ts +3 -0
- package/src/network/http2-bidi.ts +85 -0
- package/src/network/outbound-fetch.ts +87 -24
- package/src/network/pool/agent.ts +478 -460
- package/src/network/pool/resolver.ts +1104 -544
- package/src/network/pool-health-machine.ts +12 -5
- package/src/observability/event-log.ts +29 -8
- package/src/observability/metrics.ts +40 -13
- package/src/observability/payload-capture.ts +344 -324
- package/src/observability/runtime-metrics.ts +7 -0
- package/src/persistence/db-pg.ts +9 -3
- package/src/persistence/db-sqlite.ts +497 -495
- package/src/persistence/health-event-flush.ts +137 -0
- package/src/persistence/migrate.ts +181 -129
- package/src/persistence/schema.ts +1360 -1257
- package/src/persistence/settings-revision.ts +25 -0
- package/src/persistence/shared-cache.ts +217 -0
- package/src/persistence/stores/share-store.ts +556 -528
- package/src/persistence/usage-flush.ts +61 -1
- package/src/protocol/canonical-model.ts +23 -1
- package/src/protocol/response/chat.ts +143 -10
- package/src/protocol/response/codex.ts +9 -75
- package/src/protocol/response/omp-tool-call-markup.ts +221 -0
- package/src/protocol/tool-identity.ts +43 -9
- package/src/providers/authentication/jwt-validator.ts +130 -187
- package/src/providers/authentication/oauth-flow-store.ts +16 -9
- package/src/providers/default-registry.ts +177 -1
- package/src/providers/discovery/catalog/catalog-format.ts +20 -0
- package/src/providers/discovery/models-dev-catalog.ts +39 -22
- package/src/providers/discovery/openai-model-discovery.ts +39 -3
- package/src/providers/discovery/openrouter-free-roster.ts +98 -0
- package/src/providers/discovery/probe-wire.ts +29 -5
- package/src/providers/discovery/probing-service.ts +1022 -1001
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +42 -2
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +22 -5
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
- package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
- package/src/providers/integrations/buddy/codebuddy/shared.ts +115 -73
- package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
- package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
- package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
- package/src/providers/integrations/claude/claude.ts +606 -582
- package/src/providers/integrations/cline/cline-quota.ts +18 -16
- package/src/providers/integrations/cline/cline.ts +539 -468
- package/src/providers/integrations/codex/codex.ts +5 -8
- package/src/providers/integrations/commandcode/commandcode.ts +675 -541
- package/src/providers/integrations/commandcode/discovery.ts +116 -0
- package/src/providers/integrations/commandcode/quota.ts +160 -0
- package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
- package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
- package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
- package/src/providers/integrations/cursor/cursor-request.ts +471 -0
- package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
- package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
- package/src/providers/integrations/cursor/cursor.ts +960 -0
- package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
- package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
- package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
- package/src/providers/integrations/factory/factory-oauth.ts +7 -11
- package/src/providers/integrations/github/github.ts +4 -4
- package/src/providers/integrations/hermes.ts +79 -8
- package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
- package/src/providers/integrations/kilo/kilo.ts +153 -146
- package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
- package/src/providers/integrations/opencode/provider.ts +532 -499
- package/src/providers/integrations/openrouter/provider.ts +139 -281
- package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +310 -263
- package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
- package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
- package/src/providers/integrations/qoder/qoder.ts +865 -730
- package/src/providers/integrations/shared/install-id.ts +29 -2
- package/src/providers/integrations/trae/trae-catalog.ts +190 -0
- package/src/providers/integrations/trae/trae-headers.ts +149 -0
- package/src/providers/integrations/trae/trae-oauth.ts +773 -0
- package/src/providers/integrations/trae/trae-protocol.ts +400 -0
- package/src/providers/integrations/trae/trae-realm.ts +220 -0
- package/src/providers/integrations/trae/trae.ts +458 -0
- package/src/providers/operations/account-health-service.ts +107 -10
- package/src/providers/operations/client-versions.ts +143 -6
- package/src/providers/operations/model-catalog-cache.ts +18 -3
- package/src/providers/operations/provider-catalog-seeder.ts +95 -86
- package/src/providers/operations/provider-catalog-service.ts +76 -0
- package/src/providers/operations/provider-credential-service.ts +46 -7
- package/src/providers/provider-contracts.ts +76 -0
- package/src/providers/provider-metadata.ts +78 -6
- package/src/providers/provider-model-definition.ts +373 -349
- package/src/security/api-key-auth.ts +126 -8
- package/src/transport/contracts.ts +3 -0
- package/src/transport/dispatch/attempt-finalize.ts +39 -2
- package/src/transport/dispatch/attempt-loop.ts +530 -513
- package/src/transport/dispatch/empty-stream.ts +17 -26
- package/src/transport/dispatch/generation.ts +88 -21
- package/src/transport/dispatch/leases.ts +99 -2
- package/src/transport/dispatch/proxy-request.ts +60 -3
- package/src/transport/dispatch/streaming/finalize.ts +16 -1
- package/src/transport/dispatch/streaming/stream.ts +22 -2
- package/src/transport/dispatch/upstream.ts +19 -1
- package/src/transport/failure-policy.ts +20 -0
- package/src/transport/guards/auth-guard.ts +26 -1
- package/src/transport/guards/body-policy.ts +5 -4
- package/src/transport/request/preparer.ts +103 -0
- package/src/transport/request/state.ts +14 -0
- package/src/transport/routing/plan-gate.ts +45 -0
- package/src/transport/routing/route-catalog.ts +799 -738
- package/src/transport/routing/route-model.ts +52 -1
- package/src/transport/routing/router.ts +51 -0
- package/src/transport/surface/chat/encode.ts +18 -31
- package/src/transport/surface/messages/stream.ts +3 -2
- package/src/transport/translation/tool-repair.ts +37 -16
- package/src/workers/account-identity-sweep.ts +14 -12
- package/src/workers/daily-checkin.ts +62 -0
- package/src/workers/quota-refresh-sweep.ts +27 -2
- package/dist/dashboard/assets/BannedPage-DUrXpp-X.js +0 -1
- package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-AisQzCI1.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BNflDm_f.js +0 -1
- package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
- package/dist/dashboard/assets/OverviewPage-C3NIzsDx.js +0 -1
- package/dist/dashboard/assets/ProviderDetailPage-BK8i5B5X.js +0 -3
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-CvCVgm1e.js +0 -1
- package/dist/dashboard/assets/ProxyPage-DjabkuzP.js +0 -7
- package/dist/dashboard/assets/QuotaPage-Dxe0B0iu.js +0 -1
- package/dist/dashboard/assets/SetupPage-BaD4P_jW.js +0 -1
- package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
- package/dist/dashboard/assets/UsagePage-Djes-Epy.js +0 -56
- package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
- package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
- package/dist/dashboard/assets/assert-Bwh6DJTK.js +0 -1
- package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
- package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
- package/dist/dashboard/assets/entry-Cpi0rph1.js +0 -4
- package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
- package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
- package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
- package/dist/dashboard/assets/model-bans-CVvHZ-qz.js +0 -1
- package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
- package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
- package/dist/dashboard/assets/routing-jntw9-Ui.js +0 -1
- package/dist/dashboard/assets/select-CGUuTQmj.js +0 -41
- package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
- package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
- package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
- package/scripts/cli/cli.ts +0 -771
- package/scripts/operations/probe-egress.ts +0 -335
- package/scripts/operations/restart.ts +0 -120
- package/scripts/shared/env.ts +0 -269
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
- /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
- /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
|
@@ -1,495 +1,497 @@
|
|
|
1
|
-
/** Embedded backend: bun:sqlite in the gateway's data dir. */
|
|
2
|
-
import { Database, type SQLQueryBindings } from "bun:sqlite";
|
|
3
|
-
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
4
|
-
import { getTableColumns, getTableName } from "drizzle-orm";
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
6
|
-
import { mkdirSync, readFileSync } from "node:fs";
|
|
7
|
-
import { dirname } from "node:path";
|
|
8
|
-
import { resolvePackagePath } from "../config/package-paths";
|
|
9
|
-
import { fullSchema, type DatabaseHandle } from "./db-handle";
|
|
10
|
-
import {
|
|
11
|
-
BASELINE_MIGRATION_ID,
|
|
12
|
-
assertMigrationHistorySupported,
|
|
13
|
-
baselineSchemaMarker,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
db
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
SQLITE_PREPARED.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
statement.
|
|
78
|
-
statement.
|
|
79
|
-
statement.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
column.mapFromDriverValue
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
// a
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
if (typeof read === "
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
readonly
|
|
139
|
-
readonly
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
readonly
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* a nested statement
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (kind === "
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
* `
|
|
259
|
-
* are
|
|
260
|
-
*
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
*
|
|
265
|
-
*
|
|
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
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
const
|
|
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
|
-
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
return text
|
|
406
|
-
|
|
407
|
-
.replaceAll("
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
db.exec("PRAGMA
|
|
425
|
-
db.exec("PRAGMA
|
|
426
|
-
db.exec("PRAGMA
|
|
427
|
-
db.exec("PRAGMA
|
|
428
|
-
db.exec("PRAGMA
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
.
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
...(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
}
|
|
1
|
+
/** Embedded backend: bun:sqlite in the gateway's data dir. */
|
|
2
|
+
import { Database, type SQLQueryBindings } from "bun:sqlite";
|
|
3
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
4
|
+
import { getTableColumns, getTableName } from "drizzle-orm";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import { resolvePackagePath } from "../config/package-paths";
|
|
9
|
+
import { fullSchema, type DatabaseHandle } from "./db-handle";
|
|
10
|
+
import {
|
|
11
|
+
BASELINE_MIGRATION_ID,
|
|
12
|
+
assertMigrationHistorySupported,
|
|
13
|
+
baselineSchemaMarker,
|
|
14
|
+
acceptedBaselineSchemaMarkers,
|
|
15
|
+
MIGRATION_LEDGER_TABLE,
|
|
16
|
+
migrationFiles,
|
|
17
|
+
migrationIdFor,
|
|
18
|
+
splitStatements,
|
|
19
|
+
} from "./migrate";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Ledger DDL for the SQLite backend.
|
|
23
|
+
*
|
|
24
|
+
* The shared ledger DDL is PostgreSQL (`timestamptz`, `now()`), and this
|
|
25
|
+
* backend never sends PostgreSQL DDL. The shape is the same table the
|
|
26
|
+
* readiness check reads, so a ledger written here is a ledger read there.
|
|
27
|
+
*/
|
|
28
|
+
const SQLITE_LEDGER_DDL = `CREATE TABLE IF NOT EXISTS ${MIGRATION_LEDGER_TABLE} (
|
|
29
|
+
migration_id text PRIMARY KEY,
|
|
30
|
+
applied_at text NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
31
|
+
)`;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Opens the embedded database at `file`.
|
|
35
|
+
*
|
|
36
|
+
* `:memory:` is the test backend and is used as given. A filesystem path has
|
|
37
|
+
* its parent created first, because bun:sqlite does not create directories.
|
|
38
|
+
* Foreign keys are off unless asked for, so the pragma is set on every
|
|
39
|
+
* connection rather than trusted to a default. WAL keeps a reader from
|
|
40
|
+
* blocking the single writer.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Databases whose `prepare`/`exec` have already been wrapped.
|
|
44
|
+
*
|
|
45
|
+
* Keyed weakly so a closed in-memory database is not retained by the registry.
|
|
46
|
+
*/
|
|
47
|
+
const SQLITE_PREPARED = new WeakSet<Database>();
|
|
48
|
+
|
|
49
|
+
export function createSqliteDatabase(file: string): Database {
|
|
50
|
+
if (file !== ":memory:") mkdirSync(dirname(file), { recursive: true });
|
|
51
|
+
const db = new Database(file);
|
|
52
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
53
|
+
if (file !== ":memory:") applyFilePragmas(db);
|
|
54
|
+
// Guarded: this function has to run per database, but a caller may hand it
|
|
55
|
+
// one that already went through here (a test builds a handle over a booted
|
|
56
|
+
// handle). Wrapping `prepare` twice ran the translation twice, and the
|
|
57
|
+
// second pass mangled SQL the first had already rewritten — `unrecognized
|
|
58
|
+
// token: ":"` from a placeholder the first pass produced.
|
|
59
|
+
if (SQLITE_PREPARED.has(db)) return db;
|
|
60
|
+
SQLITE_PREPARED.add(db);
|
|
61
|
+
const prepare = db.prepare.bind(db);
|
|
62
|
+
db.prepare = ((text: string) => {
|
|
63
|
+
const statement = prepare(sqliteSql(text));
|
|
64
|
+
// bun:sqlite rejects a Date binding ("Binding expected string, TypedArray,
|
|
65
|
+
// boolean, number, bigint or null"), and Drizzle's pg-core timestamp
|
|
66
|
+
// columns bind `Date` objects. The schema is shared with PostgreSQL, so
|
|
67
|
+
// the conversion lives here: every timestamp column is `text`, and an ISO
|
|
68
|
+
// string is exactly what those columns store.
|
|
69
|
+
const bind = (value: unknown): SQLQueryBindings =>
|
|
70
|
+
value instanceof Date ? value.toISOString() : (value as SQLQueryBindings);
|
|
71
|
+
const run = statement.run.bind(statement);
|
|
72
|
+
const all = statement.all.bind(statement);
|
|
73
|
+
const get = statement.get.bind(statement);
|
|
74
|
+
// Drizzle's bun-sqlite session reads rows through `values`, not `all`.
|
|
75
|
+
const values = statement.values.bind(statement);
|
|
76
|
+
const bound = (args: unknown[]): SQLQueryBindings[] => args.map(bind);
|
|
77
|
+
statement.run = ((...args: unknown[]) => run(...bound(args))) as typeof statement.run;
|
|
78
|
+
statement.all = ((...args: unknown[]) => all(...bound(args))) as typeof statement.all;
|
|
79
|
+
statement.get = ((...args: unknown[]) => get(...bound(args))) as typeof statement.get;
|
|
80
|
+
statement.values = ((...args: unknown[]) => values(...bound(args))) as typeof statement.values;
|
|
81
|
+
return statement;
|
|
82
|
+
}) as Database["prepare"];
|
|
83
|
+
const exec = db.exec.bind(db);
|
|
84
|
+
db.exec = ((text: string) => exec(sqliteSql(text))) as Database["exec"];
|
|
85
|
+
return db;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Teaches the shared pg-core columns to read what bun:sqlite returns.
|
|
90
|
+
*
|
|
91
|
+
* The schema is written for node-postgres, whose driver hands booleans back
|
|
92
|
+
* as booleans and timestamps as `Date`. bun:sqlite hands back the stored
|
|
93
|
+
* value unchanged: an integer for a boolean, text for a timestamp. A
|
|
94
|
+
* column's `mapFromDriverValue` is what turns a driver value into the type
|
|
95
|
+
* the schema declares, and Drizzle calls it for every read — a select, a
|
|
96
|
+
* join, a `returning()`. The pg-core boolean column returns its value
|
|
97
|
+
* unchanged and the timestamp column only converts strings, so neither
|
|
98
|
+
* produces what the schema promises from bun:sqlite's values. Replacing the
|
|
99
|
+
* mapper on each column instance fixes every read at once. Writes are
|
|
100
|
+
* untouched: booleans already bind as integers, and `Date` bindings are
|
|
101
|
+
* converted to ISO strings in `createSqliteDatabase`.
|
|
102
|
+
*/
|
|
103
|
+
function installSqliteColumnMapping(schema: Record<string, unknown>): void {
|
|
104
|
+
|
|
105
|
+
for (const value of Object.values(schema)) {
|
|
106
|
+
const columns = tableColumns(value);
|
|
107
|
+
if (columns === undefined) continue;
|
|
108
|
+
const table = getTableName(Object.values(columns)[0]!.table as Parameters<typeof getTableName>[0]);
|
|
109
|
+
for (const column of Object.values(columns)) {
|
|
110
|
+
if (column.columnType === "PgBoolean") {
|
|
111
|
+
column.mapFromDriverValue = (read) => read === null ? null : read === 1 || read === true;
|
|
112
|
+
} else if (column.columnType === "PgTimestamp") {
|
|
113
|
+
const convert = column.mapFromDriverValue.bind(column);
|
|
114
|
+
column.mapFromDriverValue = (read) => {
|
|
115
|
+
// A column declared `text` keeps whatever it was bound as, so a
|
|
116
|
+
// timestamp can come back as the ISO string the writer produces, or
|
|
117
|
+
// as a number — or the digits of one — when a row was restored from
|
|
118
|
+
// a source that stored epoch milliseconds. `new Date` reads the
|
|
119
|
+
// digits as a year and returns an invalid date, so those are
|
|
120
|
+
// handled before the string conversion.
|
|
121
|
+
if (typeof read === "number") return new Date(read);
|
|
122
|
+
if (typeof read === "string" && /^\d+$/.test(read)) return new Date(Number(read));
|
|
123
|
+
return convert(read);
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Registered after the branches above replace it, so execute and the
|
|
127
|
+
// builder call the same function. A column neither branch touches keeps
|
|
128
|
+
// its own mapper, which is what already stringifies a numeric and parses
|
|
129
|
+
// jsonb. execute is the one read that never called it.
|
|
130
|
+
if (table !== undefined) {
|
|
131
|
+
columnMapping.set(mappingKey(table, column.name), column.mapFromDriverValue.bind(column));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface MappedColumn {
|
|
138
|
+
readonly name: string;
|
|
139
|
+
readonly columnType: string;
|
|
140
|
+
readonly table: object;
|
|
141
|
+
mapFromDriverValue: (value: unknown) => unknown;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* `table.column` to the mapper that turns bun:sqlite's stored value into the
|
|
146
|
+
* type the schema declares. `db.execute` reads through `prepare().all()`, which
|
|
147
|
+
* never calls a column mapper, so the alias applies these itself. A name that
|
|
148
|
+
* two tables share maps to the same type everywhere in this schema, and a
|
|
149
|
+
* column with no entry is returned as stored.
|
|
150
|
+
*/
|
|
151
|
+
const columnMapping = new Map<string, (value: unknown) => unknown>();
|
|
152
|
+
|
|
153
|
+
function mappingKey(table: string, column: string): string {
|
|
154
|
+
return `${table}.${column}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** One row from `prepare().all()`, with every known column mapped. */
|
|
158
|
+
function mapExecuteRow(row: Record<string, unknown>, table: string | undefined): Record<string, unknown> {
|
|
159
|
+
if (table === undefined) return row;
|
|
160
|
+
const mapped: Record<string, unknown> = {};
|
|
161
|
+
for (const [column, value] of Object.entries(row)) {
|
|
162
|
+
const convert = columnMapping.get(mappingKey(table, column));
|
|
163
|
+
mapped[column] = value === null || convert === undefined ? value : convert(value);
|
|
164
|
+
}
|
|
165
|
+
return mapped;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The table a raw statement reads, when it reads exactly one.
|
|
170
|
+
*
|
|
171
|
+
* `SELECT 1` and a join read columns from nowhere or from several tables, and
|
|
172
|
+
* neither can be mapped by name. Both are returned as stored, which is what
|
|
173
|
+
* they already were.
|
|
174
|
+
*/
|
|
175
|
+
interface RenderedStatement {
|
|
176
|
+
readonly sql: string;
|
|
177
|
+
readonly params: SQLQueryBindings[];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A Drizzle `sql` value as bun:sqlite runs it: `?` placeholders and the values
|
|
182
|
+
* that fill them, in order.
|
|
183
|
+
*
|
|
184
|
+
* A chunk is text, a nested statement, or a bound value. Text is concatenated,
|
|
185
|
+
* a nested statement is rendered in place, and everything else is one `?`.
|
|
186
|
+
* `Date` values are converted here too, because this path prepares through the
|
|
187
|
+
* raw database rather than through the wrapper in `createSqliteDatabase`.
|
|
188
|
+
*/
|
|
189
|
+
function renderExecute(query: unknown): RenderedStatement | undefined {
|
|
190
|
+
const chunks = (query as { queryChunks?: unknown[] }).queryChunks;
|
|
191
|
+
if (chunks === undefined) return undefined;
|
|
192
|
+
const params: SQLQueryBindings[] = [];
|
|
193
|
+
const sql = chunks.map((chunk) => renderChunk(chunk, params)).join("");
|
|
194
|
+
return { sql, params };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function renderChunk(chunk: unknown, params: SQLQueryBindings[]): string {
|
|
198
|
+
if (Array.isArray(chunk)) {
|
|
199
|
+
return `(${chunk.map((part) => renderChunk(part, params)).join(", ")})`;
|
|
200
|
+
}
|
|
201
|
+
if (typeof chunk === "object" && chunk !== null && "queryChunks" in chunk) {
|
|
202
|
+
return (chunk as { queryChunks: unknown[] }).queryChunks
|
|
203
|
+
.map((part) => renderChunk(part, params))
|
|
204
|
+
.join("");
|
|
205
|
+
}
|
|
206
|
+
if (typeof chunk === "object" && chunk !== null && Array.isArray((chunk as { value?: unknown }).value)) {
|
|
207
|
+
return ((chunk as { value: string[] }).value).join("");
|
|
208
|
+
}
|
|
209
|
+
// A table, column, or quoted name is SQL text, not a bound value. Rendering
|
|
210
|
+
// one as a placeholder produces `DELETE FROM ?`, which SQLite rejects with
|
|
211
|
+
// `near "?": syntax error` — the failure the payload sweep logs every pass.
|
|
212
|
+
const kind = drizzleKind(chunk);
|
|
213
|
+
if (kind === "Table") {
|
|
214
|
+
return `"${getTableName(chunk as Parameters<typeof getTableName>[0])}"`;
|
|
215
|
+
}
|
|
216
|
+
if (kind === "Name") return `"${(chunk as { value: string }).value}"`;
|
|
217
|
+
if (kind === "Column") return `"${(chunk as { name: string }).name}"`;
|
|
218
|
+
const value = chunk instanceof Date ? chunk.toISOString() : chunk;
|
|
219
|
+
params.push(value as SQLQueryBindings);
|
|
220
|
+
return "?";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
function drizzleKind(chunk: unknown): string | undefined {
|
|
225
|
+
if (typeof chunk !== "object" || chunk === null) return undefined;
|
|
226
|
+
// A pg column's own kind is `PgUUID` or `PgTimestamp`, not `Column`. Walk
|
|
227
|
+
// the constructors: the base `Column` marks every one of them.
|
|
228
|
+
let ctor = chunk.constructor as { [entityKind]?: unknown } | null;
|
|
229
|
+
while (ctor !== null && ctor !== Function.prototype) {
|
|
230
|
+
const kind = ctor[entityKind];
|
|
231
|
+
if (kind === "Column" || kind === "Table" || kind === "Name") return kind;
|
|
232
|
+
ctor = Object.getPrototypeOf(ctor) as { [entityKind]?: unknown } | null;
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const entityKind = Symbol.for("drizzle:entityKind");
|
|
238
|
+
function singleTable(sql: string): string | undefined {
|
|
239
|
+
const tables = [...sql.matchAll(/\b(?:from|join)\s+"?([a-z0-9_]+)"?/gi)].map((match) => match[1]);
|
|
240
|
+
const distinct = new Set(tables);
|
|
241
|
+
return distinct.size === 1 ? [...distinct][0] : undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The columns of one Drizzle table, or undefined for everything else in the schema. */
|
|
245
|
+
function tableColumns(value: unknown): Record<string, MappedColumn> | undefined {
|
|
246
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
247
|
+
try {
|
|
248
|
+
const columns = getTableColumns(value as Parameters<typeof getTableColumns>[0]);
|
|
249
|
+
return Object.keys(columns).length > 0 ? columns as Record<string, MappedColumn> : undefined;
|
|
250
|
+
} catch {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The handle stores run on.
|
|
257
|
+
*
|
|
258
|
+
* `db` is the bun-sqlite Drizzle instance. It is not the node-postgres
|
|
259
|
+
* `CartethyiaDatabase` the stores are typed against — the two query builders
|
|
260
|
+
* are different types — so the conversion happens once, here. The stores
|
|
261
|
+
* exercise only the builder surface both drivers share.
|
|
262
|
+
*/
|
|
263
|
+
/**
|
|
264
|
+
* The three statements the auth path runs on a cache miss, prepared once.
|
|
265
|
+
*
|
|
266
|
+
* Drizzle rebuilds the query on every call, and that rebuild measured 60x the
|
|
267
|
+
* cost of the lookup itself. These are static SQL against an index, so there
|
|
268
|
+
* is nothing for a builder to decide per call. The column list is exactly the
|
|
269
|
+
* `api_keys` row the store maps, in the order `findActiveByHash` reads it.
|
|
270
|
+
*/
|
|
271
|
+
const AUTH_KEY_COLUMNS = [
|
|
272
|
+
"id", "tenant_id", "key_hash", "key_mode", "parent_key_id", "issued_client_ip",
|
|
273
|
+
"issued_client_ip_key", "label", "scopes", "requests_per_minute", "daily_token_limit",
|
|
274
|
+
"monthly_token_limit", "lifetime_token_budget", "lifetime_tokens_consumed",
|
|
275
|
+
"max_concurrent_requests", "model_access_mode", "model_list", "client_router_denylist",
|
|
276
|
+
"model_prefix", "created_at", "enabled", "revoked_at", "key_prefix", "key_encrypted",
|
|
277
|
+
"notes_title", "notes_subtitle", "notes_body", "is_default", "sort_index",
|
|
278
|
+
].join(", ");
|
|
279
|
+
|
|
280
|
+
export interface SqliteAuthStatements {
|
|
281
|
+
/** One active key by its hash. The auth path's first read. */
|
|
282
|
+
readonly byHash: { get: (hash: string) => Record<string, unknown> | null };
|
|
283
|
+
/** One active key by its id. Run only for a key that has a parent. */
|
|
284
|
+
readonly byId: { get: (id: string) => Record<string, unknown> | null };
|
|
285
|
+
/** Lifetime consumption across one key's children, revoked included. */
|
|
286
|
+
readonly childrenConsumed: { get: (parentId: string) => { total: number } | null };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function prepareAuthStatements(sqlite: Database): SqliteAuthStatements {
|
|
290
|
+
const active = `enabled = 1 AND revoked_at IS NULL`;
|
|
291
|
+
return {
|
|
292
|
+
byHash: sqlite.prepare(
|
|
293
|
+
`SELECT ${AUTH_KEY_COLUMNS} FROM api_keys WHERE key_hash = ? AND ${active} LIMIT 1`,
|
|
294
|
+
) as SqliteAuthStatements["byHash"],
|
|
295
|
+
byId: sqlite.prepare(
|
|
296
|
+
`SELECT ${AUTH_KEY_COLUMNS} FROM api_keys WHERE id = ? AND ${active} LIMIT 1`,
|
|
297
|
+
) as SqliteAuthStatements["byId"],
|
|
298
|
+
childrenConsumed: sqlite.prepare(
|
|
299
|
+
`SELECT coalesce(sum(lifetime_tokens_consumed), 0) AS total FROM api_keys WHERE parent_key_id = ?`,
|
|
300
|
+
) as SqliteAuthStatements["childrenConsumed"],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
// A reloaded module must repatch the columns even when the handle is the
|
|
306
|
+
// one booted before the reload. See `buildSqliteHandle`.
|
|
307
|
+
installSqliteColumnMapping(fullSchema);
|
|
308
|
+
|
|
309
|
+
export function buildSqliteHandle(sqlite: Database): DatabaseHandle {
|
|
310
|
+
// Reinstalled on every call, not guarded. `bun --hot` keeps the booted
|
|
311
|
+
// handle in `globalThis` and never rebuilds it, so a mapping installed
|
|
312
|
+
// once at boot dies with the module it patched: booleans came back as
|
|
313
|
+
// integers and login rejected a valid user as `unknown_user`. The
|
|
314
|
+
// reloaded module calls this again and repatches the live columns.
|
|
315
|
+
installSqliteColumnMapping(fullSchema);
|
|
316
|
+
const built = drizzle(sqlite, { schema: fullSchema });
|
|
317
|
+
// node-postgres exposes `execute` for raw SQL. bun-sqlite exposes `run` and
|
|
318
|
+
// `all` instead, and the stores were written against `execute`. The alias
|
|
319
|
+
// goes through `exec` rather than `run`: `run` prepares a statement per
|
|
320
|
+
// call, and a raw statement here carries its values inlined, so there is
|
|
321
|
+
// nothing to bind. Affected rows are read back from `changes()`.
|
|
322
|
+
const execute = (query: unknown): { rowCount: number; rows: unknown[] } => {
|
|
323
|
+
// Rendered the way the bun-sqlite session renders it: `?` placeholders and
|
|
324
|
+
// a params list. Joining the chunks kept only their text, so an
|
|
325
|
+
// interpolated value was dropped and the statement reached SQLite with a
|
|
326
|
+
// hole in it.
|
|
327
|
+
const rendered = renderExecute(query);
|
|
328
|
+
if (rendered === undefined) throw new Error("sqlite execute: query has no SQL text");
|
|
329
|
+
// `all` for a statement that returns rows, `exec` otherwise. Readiness
|
|
330
|
+
// reads the migration ledger through `execute` and decides "applied" from
|
|
331
|
+
// `result.rows`; an alias that only reported `changes()` made every select
|
|
332
|
+
// look empty, so a migrated database stayed "not ready" forever.
|
|
333
|
+
const returnsRows = /^\s*(select|with|pragma)\b/i.test(rendered.sql);
|
|
334
|
+
if (returnsRows) {
|
|
335
|
+
// `all()` returns stored values, not mapped ones, so a timestamp comes
|
|
336
|
+
// back as text and a boolean as 0 or 1. The builder path maps through
|
|
337
|
+
// the column; this is the only read that does not, and the backup
|
|
338
|
+
// import compares these values against the builder's. Map them here.
|
|
339
|
+
const rows = sqlite.prepare(rendered.sql).all(...rendered.params) as Record<string, unknown>[];
|
|
340
|
+
const table = singleTable(rendered.sql);
|
|
341
|
+
return { rowCount: rows.length, rows: rows.map((row) => mapExecuteRow(row, table)) };
|
|
342
|
+
}
|
|
343
|
+
sqlite.prepare(rendered.sql).run(...rendered.params);
|
|
344
|
+
const changed = sqlite.prepare("SELECT changes() AS c").get() as { c: number } | null;
|
|
345
|
+
return { rowCount: changed?.c ?? 0, rows: [] };
|
|
346
|
+
};
|
|
347
|
+
// A transaction callback receives a fresh builder, not this handle, so the
|
|
348
|
+
// `execute` alias above would not reach it. First-boot setup takes its lock
|
|
349
|
+
// through `transaction.execute`, and without the alias that call throws and
|
|
350
|
+
// setup can never complete on the SQLite backend.
|
|
351
|
+
const transaction = built.transaction.bind(built);
|
|
352
|
+
const db = Object.assign(built, {
|
|
353
|
+
execute,
|
|
354
|
+
transaction: (fn: (tx: unknown) => unknown) =>
|
|
355
|
+
transaction((tx: object) => fn(Object.assign(tx, { execute }))),
|
|
356
|
+
});
|
|
357
|
+
return {
|
|
358
|
+
kind: "sqlite",
|
|
359
|
+
db: db as unknown as DatabaseHandle["db"],
|
|
360
|
+
sqlite,
|
|
361
|
+
auth: prepareAuthStatements(sqlite),
|
|
362
|
+
query: (text, params) => {
|
|
363
|
+
// bun:sqlite types `all` as a tuple spread, which a `readonly unknown[]`
|
|
364
|
+
// cannot satisfy. The values are the bound parameters the caller built;
|
|
365
|
+
// the cast is confined to this one call.
|
|
366
|
+
const bound = (params ?? []) as Parameters<ReturnType<Database["prepare"]>["all"]>;
|
|
367
|
+
const rows = sqlite.prepare(text).all(...bound) as unknown[];
|
|
368
|
+
return Promise.resolve({ rows });
|
|
369
|
+
},
|
|
370
|
+
exec: (script) => {
|
|
371
|
+
sqlite.exec(script);
|
|
372
|
+
return Promise.resolve();
|
|
373
|
+
},
|
|
374
|
+
close: () => {
|
|
375
|
+
sqlite.close();
|
|
376
|
+
return Promise.resolve();
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Applies the SQLite migration set in order.
|
|
383
|
+
*
|
|
384
|
+
* Same ledger, same skip-when-recorded rule, and the same one-file-per-
|
|
385
|
+
* transaction atomicity as the PostgreSQL runner. Two deliberate absences,
|
|
386
|
+
* both consequences of being the only process: no advisory lock, because a
|
|
387
|
+
* second process cannot open the file for writing, and no
|
|
388
|
+
* `to_regclass` probe, because SQLite has no schemas — `sqlite_master`
|
|
389
|
+
* answers the same question.
|
|
390
|
+
*/
|
|
391
|
+
/**
|
|
392
|
+
* Rewrites the two PostgreSQL functions the schema's defaults emit.
|
|
393
|
+
*
|
|
394
|
+
* `gen_random_uuid()` becomes a quoted uuid. `now()` becomes a quoted UTC
|
|
395
|
+
* timestamp. Both are matched only as function calls, so a column or string
|
|
396
|
+
* that merely contains the words is left alone.
|
|
397
|
+
*/
|
|
398
|
+
/**
|
|
399
|
+
* Rewrites the two PostgreSQL functions the schema's defaults emit, and only
|
|
400
|
+
* those. A statement that contains neither is returned as-is, so the common
|
|
401
|
+
* case — every read, and every write that supplies its own values — pays one
|
|
402
|
+
* `includes` and nothing else.
|
|
403
|
+
*/
|
|
404
|
+
function sqliteSql(text: string): string {
|
|
405
|
+
if (!text.includes("gen_random_uuid()") && !text.includes("now()")) return text;
|
|
406
|
+
return text
|
|
407
|
+
.replaceAll("gen_random_uuid()", () => `'${randomUUID()}'`)
|
|
408
|
+
.replaceAll("now()", () => `'${new Date().toISOString()}'`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Pragmas for a file-backed database. Each one removes a stall or a fsync the
|
|
413
|
+
* default pays per commit, and none of them weakens crash safety below what
|
|
414
|
+
* the usage flush already accepts (at most the last second of deltas).
|
|
415
|
+
*
|
|
416
|
+
* `synchronous = NORMAL` is durable against a process crash under WAL; only a
|
|
417
|
+
* power loss can drop the last commit. `busy_timeout` makes a writer wait
|
|
418
|
+
* instead of failing instantly when a sweep holds the lock. The cache is 8 MB:
|
|
419
|
+
* large enough for auth and the active catalog working set, without reserving
|
|
420
|
+
* 64 MB that an idle gateway never touches. `temp_store` keeps sorts off disk.
|
|
421
|
+
* The checkpoint interval keeps a request from stalling on one.
|
|
422
|
+
*/
|
|
423
|
+
function applyFilePragmas(db: Database): void {
|
|
424
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
425
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
426
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
427
|
+
db.exec("PRAGMA cache_size = -8192");
|
|
428
|
+
db.exec("PRAGMA temp_store = MEMORY");
|
|
429
|
+
db.exec("PRAGMA wal_autocheckpoint = 1000");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** The SQLite set lives beside the PostgreSQL one, never inside it. */
|
|
433
|
+
export function resolveSqliteMigrationsFolder(): string {
|
|
434
|
+
return resolvePackagePath("migrations", "lite-sqlite3");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export async function applySqliteMigrations(
|
|
438
|
+
sqlite: Database,
|
|
439
|
+
folder = resolveSqliteMigrationsFolder(),
|
|
440
|
+
): Promise<void> {
|
|
441
|
+
sqlite.exec(SQLITE_LEDGER_DDL);
|
|
442
|
+
const appliedRows = sqlite
|
|
443
|
+
.prepare(`SELECT migration_id FROM ${MIGRATION_LEDGER_TABLE}`)
|
|
444
|
+
.all() as { migration_id: string }[];
|
|
445
|
+
const applied = new Set(appliedRows.map((row) => row.migration_id));
|
|
446
|
+
|
|
447
|
+
const tenants = sqlite
|
|
448
|
+
.prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'tenants'")
|
|
449
|
+
.get() as { present: number } | null;
|
|
450
|
+
const files = migrationFiles(folder);
|
|
451
|
+
const baselineFile = files.find((file) => migrationIdFor(file) === BASELINE_MIGRATION_ID);
|
|
452
|
+
const baselineSource =
|
|
453
|
+
baselineFile === undefined ? undefined : readFileSync(baselineFile, "utf8");
|
|
454
|
+
const baselineMarker =
|
|
455
|
+
baselineSource === undefined ? undefined : baselineSchemaMarker(baselineSource);
|
|
456
|
+
assertMigrationHistorySupported(
|
|
457
|
+
applied,
|
|
458
|
+
[
|
|
459
|
+
...files.map(migrationIdFor),
|
|
460
|
+
...(baselineMarker === undefined ? [] : [baselineMarker]),
|
|
461
|
+
...(baselineSource === undefined ? [] : acceptedBaselineSchemaMarkers(baselineSource)),
|
|
462
|
+
],
|
|
463
|
+
tenants !== null,
|
|
464
|
+
baselineMarker,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
for (const file of files) {
|
|
468
|
+
const migrationId = migrationIdFor(file);
|
|
469
|
+
if (applied.has(migrationId)) continue;
|
|
470
|
+
const source = readFileSync(file, "utf8");
|
|
471
|
+
const record = sqlite.prepare(
|
|
472
|
+
`INSERT INTO ${MIGRATION_LEDGER_TABLE} (migration_id) VALUES (?)`,
|
|
473
|
+
);
|
|
474
|
+
try {
|
|
475
|
+
sqlite.transaction(() => {
|
|
476
|
+
for (const statement of splitStatements(source)) {
|
|
477
|
+
// The generated baseline opens with a comment block, and a
|
|
478
|
+
// breakpoint at the end of a file leaves an empty part. `exec`
|
|
479
|
+
// rejects both, so only text that still contains SQL is sent.
|
|
480
|
+
const sql = statement.replace(/--[^\n]*/g, "").trim();
|
|
481
|
+
if (sql.length === 0) continue;
|
|
482
|
+
sqlite.exec(sql);
|
|
483
|
+
}
|
|
484
|
+
record.run(migrationId);
|
|
485
|
+
if (migrationId === BASELINE_MIGRATION_ID && baselineMarker !== undefined) {
|
|
486
|
+
record.run(baselineMarker);
|
|
487
|
+
}
|
|
488
|
+
})();
|
|
489
|
+
applied.add(migrationId);
|
|
490
|
+
if (migrationId === BASELINE_MIGRATION_ID && baselineMarker !== undefined) {
|
|
491
|
+
applied.add(baselineMarker);
|
|
492
|
+
}
|
|
493
|
+
} catch (error) {
|
|
494
|
+
throw new Error(`Migration ${migrationId} failed: ${String(error)}`, { cause: error });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|