cartethyia 2.4.6 → 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/README.md +45 -13
- package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
- package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-Bcy150Jo.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DyNXefph.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-CATpAEOz.js → ConsoleLogPage-u6W1UXSc.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-D6jEU7nL.js → CustomizationPage-CTTpCb25.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-CZkpn0a7.js → HealthEventsModal-C5XpWlvy.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BJlMVN4U.js → LoginPage-DHoB3Agn.js} +1 -1
- package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-Br3CXEau.js → ModelPicker-oQ-SDXp8.js} +1 -1
- 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-BHW4Zt06.js → SettingsPage-CvjZxHE4.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-VOYC_lIa.js → SetupPage-D3nza7LH.js} +1 -1
- package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-Dtt12WT1.js +1 -0
- package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-BNIDLiU6.js → StudioPage-CbK3hXvU.js} +4 -4
- package/dist/dashboard/assets/StudioPage-CbK3hXvU.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-BETfaMYZ.js → api-keys-BmDbsior.js} +1 -1
- 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/assert-BHfaei-X.js +1 -0
- package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BTyGk_DL.js → badge-DpXT4Ixt.js} +1 -1
- package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
- package/dist/dashboard/assets/{banner-bujTWYM4.js → banner-CSAAWLJv.js} +1 -1
- package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
- package/dist/dashboard/assets/{card-B0Bnx5CA.js → card-DEhRXXw4.js} +1 -1
- package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-DE2ug812.js → cli-tools-B0wBq-RL.js} +1 -1
- package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-R_rArH8S.js → clipboard-button-DOo6EU8O.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BWIP_EvW.js → dialog-BvQeBMfP.js} +1 -1
- package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BKcgVugF.js → drawer-CMwPXwQB.js} +1 -1
- package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CcWkGR4a.js → entry-D4O2229X.js} +1 -1
- package/dist/dashboard/assets/{entry-CcWkGR4a.js.gz → entry-D4O2229X.js.gz} +0 -0
- package/dist/dashboard/assets/{entry-kTiTFrvh.js → entry-DKvr5Q9K.js} +1 -1
- 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/{github-badge-Booc5mNe.js → github-badge-zliLvBwr.js} +1 -1
- package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
- package/dist/dashboard/assets/{index-D70pChfn.js → index-CXm3FIyt.js} +2 -2
- package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
- package/dist/dashboard/assets/{inline-D1TCqM2D.js → inline-D1IlZZ8H.js} +1 -1
- package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
- package/dist/dashboard/assets/{input-B9oyU93y.js → input-BpVZdrbc.js} +1 -1
- package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BArzXirQ.js → layout-BioLW7gN.js} +1 -1
- package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BVZzaoHF.js → list-row-CwVcGAIc.js} +1 -1
- package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
- package/dist/dashboard/assets/{live-EmuIayqp.js → live-yfEJFF9T.js} +1 -1
- package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CSPQ9xB0.js → model-bans-CT-TxSNX.js} +1 -1
- package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
- package/dist/dashboard/assets/providers/cursor.webp +0 -0
- package/dist/dashboard/assets/providers/trae.webp +0 -0
- package/dist/dashboard/assets/{routing-BExgxxBU.js → routing-Cs6jl49L.js} +1 -1
- package/dist/dashboard/assets/routing-Cs6jl49L.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/{stack-Ca5HxI5i.js → stack-C9_uUjaA.js} +1 -1
- package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
- package/dist/dashboard/assets/{state-CzUPfLXo.js → state-DA0B95UQ.js} +1 -1
- package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DOUIky_i.js → switch-Cittxy-x.js} +1 -1
- package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
- package/dist/dashboard/assets/{toast-C76WrMhc.js → toast-CNt_jzO4.js} +1 -1
- package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-D87FZwH0.js → useMutation-BQKMcJ1H.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQKMcJ1H.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 +51 -52
- 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/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 +3 -2
- package/scripts/cli/command/console-menu.ts +30 -11
- package/scripts/cli/command/start.ts +77 -13
- package/scripts/cli/command/update.ts +1 -1
- package/scripts/cli/gateway/instance-lock.ts +105 -0
- package/scripts/cli/gateway/spawn.ts +173 -101
- package/scripts/cli/ui/background.ts +110 -49
- package/src/boot/app.ts +20 -6
- package/src/boot/dependencies.ts +19 -0
- 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 +5 -5
- package/src/config/runtime.ts +19 -3
- package/src/config/spec.ts +9 -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/domain-registration.ts +9 -2
- package/src/console/observability/contracts.ts +475 -455
- package/src/console/observability/store.ts +1015 -952
- package/src/console/providers/catalog/contracts.ts +31 -0
- package/src/console/providers/catalog/provider-operations.ts +8 -0
- package/src/console/providers/catalog/store.ts +1532 -1525
- package/src/console/providers/oauth/callback-listener.ts +18 -3
- package/src/console/providers/oauth/routes.ts +38 -15
- package/src/console/quota/refresh.ts +19 -0
- 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 +1 -26
- 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/payload-capture.ts +344 -324
- 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 -1295
- 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/omp-tool-call-markup.ts +221 -0
- package/src/providers/default-registry.ts +173 -1
- package/src/providers/discovery/openai-model-discovery.ts +39 -3
- package/src/providers/discovery/openrouter-free-roster.ts +98 -0
- package/src/providers/discovery/probing-service.ts +1022 -1001
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +39 -0
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +18 -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 +15 -6
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
- package/src/providers/integrations/buddy/workbuddy/shared.ts +3 -3
- 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/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/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/provider.ts +532 -538
- 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 -257
- 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/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 +133 -6
- package/src/providers/operations/provider-catalog-seeder.ts +95 -86
- 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 +32 -1
- package/src/transport/dispatch/attempt-loop.ts +530 -515
- 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/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 -777
- package/src/transport/routing/route-model.ts +52 -1
- package/src/transport/routing/router.ts +51 -0
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js +0 -1
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +0 -1
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js +0 -7
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +0 -1
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js +0 -1
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js +0 -56
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css +0 -2
- package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
- package/dist/dashboard/assets/assert-7J6_LVD3.js +0 -1
- package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
- package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
- package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
- package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js +0 -4
- package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
- package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
- package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
- package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
- package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
- package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
- package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
- package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
- package/dist/dashboard/assets/select-C9mMoEpJ.js +0 -41
- package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js +0 -1
- package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
- package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
- package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
- package/src/network/tls/bun-client-hello.ts +0 -112
- /package/dist/dashboard/assets/{app-Cjq7nrwu.js → app-C-Y4oZT0.js} +0 -0
- /package/dist/dashboard/assets/{app-Cjq7nrwu.js.gz → app-C-Y4oZT0.js.gz} +0 -0
|
@@ -1,391 +1,414 @@
|
|
|
1
|
-
// Console API-key domain contracts: request/response shapes and pure helpers.
|
|
2
|
-
//
|
|
3
|
-
// Single source for key creation validation and response sanitization;
|
|
4
|
-
// routes consume it, they do not redeclare it.
|
|
5
|
-
|
|
6
|
-
import type { AuditSink } from "../audit/contracts";
|
|
7
|
-
import { randomBytes } from "node:crypto";
|
|
8
|
-
import type { ApiKeyRecord, ApiKeyStore } from "../../persistence/stores/api-key-store";
|
|
9
|
-
import { hashSecret } from "../../security/crypto";
|
|
10
|
-
import { type ShareLinkStore, type ShareLinkSummary } from "../../persistence/stores/share-store";
|
|
11
|
-
import { API_KEY_MODES, type ApiKeyMode, type ApiKeyModelAccessMode, type ShareLinkKind } from "../../persistence/schema";
|
|
12
|
-
import { isValidTenantKeyScope, type AccessScope } from "../../security/access-control";
|
|
13
|
-
import { normalizeClientRouterId } from "../../security/client-router-fingerprint";
|
|
14
|
-
import type { ShareActivityPort } from "../share/share-usage";
|
|
15
|
-
import { ConsoleDomainError } from "../shared/errors";
|
|
16
|
-
import type { ConsoleAccessResolver } from "../auth/access";
|
|
17
|
-
import type { ApiKeyAdmissionService } from "../../security/admission/service";
|
|
18
|
-
import {
|
|
19
|
-
SHARE_POPUP_IMAGE_MAX_BYTES,
|
|
20
|
-
SHARE_POPUP_IMAGE_MIMES,
|
|
21
|
-
sniffSharePopupImageMime,
|
|
22
|
-
type SharePopupImageMime,
|
|
23
|
-
} from "./share-popup-image";
|
|
24
|
-
|
|
25
|
-
// Re-exported so consumers reach the image bounds from this contract module.
|
|
26
|
-
// `share-popup-image.ts` is the pure authority; it stays import-free so the
|
|
27
|
-
// dashboard can read the same bounds its upload control enforces.
|
|
28
|
-
export {
|
|
29
|
-
SHARE_POPUP_IMAGE_MAX_BYTES,
|
|
30
|
-
SHARE_POPUP_IMAGE_MIMES,
|
|
31
|
-
type SharePopupImageMime,
|
|
32
|
-
} from "./share-popup-image";
|
|
33
|
-
export type {
|
|
34
|
-
SharedKeySummary,
|
|
35
|
-
SharedKeyActivityDetail,
|
|
36
|
-
} from "../share/share-usage";
|
|
37
|
-
/** Input accepted when creating or editing a key. Null limits mean unlimited. */
|
|
38
|
-
export interface CreateApiKeyRequest {
|
|
39
|
-
enabled?: boolean;
|
|
40
|
-
label?: string;
|
|
41
|
-
keyMode?: ApiKeyMode;
|
|
42
|
-
scopes?: readonly string[];
|
|
43
|
-
/** Public leading fragment used for generated keys; defaults to the product prefix. */
|
|
44
|
-
keyPrefix?: string;
|
|
45
|
-
/** Owner-supplied raw key value; valid only for personal keys. */
|
|
46
|
-
key?: string;
|
|
47
|
-
requestsPerMinute?: number | null;
|
|
48
|
-
dailyTokenLimit?: number | null;
|
|
49
|
-
monthlyTokenLimit?: number | null;
|
|
50
|
-
lifetimeTokenBudget?: number | null;
|
|
51
|
-
maxConcurrentRequests?: number | null;
|
|
52
|
-
modelPrefix?: string;
|
|
53
|
-
modelAccessMode?: ApiKeyModelAccessMode;
|
|
54
|
-
modelList?: readonly string[];
|
|
55
|
-
/** Client-router ids this key refuses; see `client-router-fingerprint.ts`. */
|
|
56
|
-
clientRouterDenylist?: readonly string[];
|
|
57
|
-
notesTitle?: string;
|
|
58
|
-
notesSubtitle?: string;
|
|
59
|
-
notesBody?: string;
|
|
60
|
-
sharePopupEnabled?: boolean | null;
|
|
61
|
-
/**
|
|
62
|
-
* Uploaded popup art as a `data:<mime>;base64,…` URL. `null` clears it and
|
|
63
|
-
* omitting it leaves the stored image alone. There is no remote-URL form: a
|
|
64
|
-
* hotlink leaked each visitor's IP to a third party and rotted on host moves.
|
|
65
|
-
*/
|
|
66
|
-
sharePopupImage?: string | null;
|
|
67
|
-
sharePopupTitle?: string;
|
|
68
|
-
sharePopupBody?: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Public key representation; it never contains a secret, hash, or IP-key digest. */
|
|
72
|
-
export interface ApiKeyResponse {
|
|
73
|
-
readonly id: string;
|
|
74
|
-
readonly label: string;
|
|
75
|
-
readonly enabled: boolean;
|
|
76
|
-
readonly keyMode: ApiKeyMode;
|
|
77
|
-
readonly scopes: readonly AccessScope[];
|
|
78
|
-
/** The tenant's mandatory gateway key: rotatable, never revocable. */
|
|
79
|
-
readonly isDefault: boolean;
|
|
80
|
-
readonly keyPrefix?: string;
|
|
81
|
-
readonly parentKeyId?: string;
|
|
82
|
-
readonly requestsPerMinute?: number;
|
|
83
|
-
readonly dailyTokenLimit?: number;
|
|
84
|
-
readonly monthlyTokenLimit?: number;
|
|
85
|
-
readonly lifetimeTokenBudget?: number;
|
|
86
|
-
readonly modelPrefix?: string;
|
|
87
|
-
readonly modelAccessMode?: ApiKeyModelAccessMode;
|
|
88
|
-
readonly modelList?: readonly string[];
|
|
89
|
-
readonly clientRouterDenylist?: readonly string[];
|
|
90
|
-
readonly maxConcurrentRequests?: number;
|
|
91
|
-
readonly notesTitle?: string;
|
|
92
|
-
readonly notesSubtitle?: string;
|
|
93
|
-
readonly notesBody?: string;
|
|
94
|
-
readonly sharePopupEnabled?: boolean;
|
|
95
|
-
/** Mime of the stored popup art; its bytes are served by a dedicated route. */
|
|
96
|
-
readonly sharePopupImageMime?: string;
|
|
97
|
-
readonly sharePopupTitle?: string;
|
|
98
|
-
readonly sharePopupBody?: string;
|
|
99
|
-
readonly createdAt: string;
|
|
100
|
-
readonly revokedAt?: string;
|
|
101
|
-
readonly tokensConsumed: number;
|
|
102
|
-
}
|
|
103
|
-
/** Creation result; plaintext is returned only when a personal key is minted. */
|
|
104
|
-
export interface CreateApiKeyResponse extends ApiKeyResponse {
|
|
105
|
-
readonly secret?: string;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Edit result carries one new secret on personal rotation or template conversion. */
|
|
109
|
-
export interface UpdateApiKeyResponse extends ApiKeyResponse {
|
|
110
|
-
readonly secret?: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** API-key route dependency boundary. */
|
|
114
|
-
export interface ApiKeyConfig {
|
|
115
|
-
readonly store: ApiKeyStore;
|
|
116
|
-
readonly accessResolver: ConsoleAccessResolver;
|
|
117
|
-
readonly auditSink?: AuditSink;
|
|
118
|
-
readonly shareStore?: ShareLinkStore;
|
|
119
|
-
readonly shareActivity?: ShareActivityPort;
|
|
120
|
-
readonly admissionService: Pick<ApiKeyAdmissionService, "purgeKey"> & {
|
|
121
|
-
seedBuckets?(request: {
|
|
122
|
-
readonly apiKeyId: string;
|
|
123
|
-
readonly now?: number;
|
|
124
|
-
readonly daily?: number;
|
|
125
|
-
readonly monthly?: number;
|
|
126
|
-
}): Promise<void>;
|
|
127
|
-
};
|
|
128
|
-
/**
|
|
129
|
-
* Reads tokens already spent in the current daily/monthly bucket, so a
|
|
130
|
-
* newly-added limit is enforced against the whole bucket. Optional: without
|
|
131
|
-
* it a new limit simply starts counting from the moment it was set.
|
|
132
|
-
*/
|
|
133
|
-
readonly bucketSpend?: (
|
|
134
|
-
keyId: string,
|
|
135
|
-
now: Date,
|
|
136
|
-
) => Promise<{ daily: number; monthly: number }>;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Result of minting a share link. The bearer token is returned exactly once. */
|
|
140
|
-
export interface ShareKeyResponse {
|
|
141
|
-
readonly id: string;
|
|
142
|
-
readonly url: string;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
readonly
|
|
151
|
-
readonly
|
|
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
|
-
|
|
177
|
-
/**
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
return { secret, hash: hashSecret(secret) };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
if (request.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
...(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
1
|
+
// Console API-key domain contracts: request/response shapes and pure helpers.
|
|
2
|
+
//
|
|
3
|
+
// Single source for key creation validation and response sanitization;
|
|
4
|
+
// routes consume it, they do not redeclare it.
|
|
5
|
+
|
|
6
|
+
import type { AuditSink } from "../audit/contracts";
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
8
|
+
import type { ApiKeyRecord, ApiKeyStore } from "../../persistence/stores/api-key-store";
|
|
9
|
+
import { hashSecret } from "../../security/crypto";
|
|
10
|
+
import { type ShareLinkStore, type ShareLinkSummary } from "../../persistence/stores/share-store";
|
|
11
|
+
import { API_KEY_MODES, type ApiKeyMode, type ApiKeyModelAccessMode, type ShareLinkKind } from "../../persistence/schema";
|
|
12
|
+
import { isValidTenantKeyScope, type AccessScope } from "../../security/access-control";
|
|
13
|
+
import { normalizeClientRouterId } from "../../security/client-router-fingerprint";
|
|
14
|
+
import type { ShareActivityPort } from "../share/share-usage";
|
|
15
|
+
import { ConsoleDomainError } from "../shared/errors";
|
|
16
|
+
import type { ConsoleAccessResolver } from "../auth/access";
|
|
17
|
+
import type { ApiKeyAdmissionService } from "../../security/admission/service";
|
|
18
|
+
import {
|
|
19
|
+
SHARE_POPUP_IMAGE_MAX_BYTES,
|
|
20
|
+
SHARE_POPUP_IMAGE_MIMES,
|
|
21
|
+
sniffSharePopupImageMime,
|
|
22
|
+
type SharePopupImageMime,
|
|
23
|
+
} from "./share-popup-image";
|
|
24
|
+
|
|
25
|
+
// Re-exported so consumers reach the image bounds from this contract module.
|
|
26
|
+
// `share-popup-image.ts` is the pure authority; it stays import-free so the
|
|
27
|
+
// dashboard can read the same bounds its upload control enforces.
|
|
28
|
+
export {
|
|
29
|
+
SHARE_POPUP_IMAGE_MAX_BYTES,
|
|
30
|
+
SHARE_POPUP_IMAGE_MIMES,
|
|
31
|
+
type SharePopupImageMime,
|
|
32
|
+
} from "./share-popup-image";
|
|
33
|
+
export type {
|
|
34
|
+
SharedKeySummary,
|
|
35
|
+
SharedKeyActivityDetail,
|
|
36
|
+
} from "../share/share-usage";
|
|
37
|
+
/** Input accepted when creating or editing a key. Null limits mean unlimited. */
|
|
38
|
+
export interface CreateApiKeyRequest {
|
|
39
|
+
enabled?: boolean;
|
|
40
|
+
label?: string;
|
|
41
|
+
keyMode?: ApiKeyMode;
|
|
42
|
+
scopes?: readonly string[];
|
|
43
|
+
/** Public leading fragment used for generated keys; defaults to the product prefix. */
|
|
44
|
+
keyPrefix?: string;
|
|
45
|
+
/** Owner-supplied raw key value; valid only for personal keys. */
|
|
46
|
+
key?: string;
|
|
47
|
+
requestsPerMinute?: number | null;
|
|
48
|
+
dailyTokenLimit?: number | null;
|
|
49
|
+
monthlyTokenLimit?: number | null;
|
|
50
|
+
lifetimeTokenBudget?: number | null;
|
|
51
|
+
maxConcurrentRequests?: number | null;
|
|
52
|
+
modelPrefix?: string;
|
|
53
|
+
modelAccessMode?: ApiKeyModelAccessMode;
|
|
54
|
+
modelList?: readonly string[];
|
|
55
|
+
/** Client-router ids this key refuses; see `client-router-fingerprint.ts`. */
|
|
56
|
+
clientRouterDenylist?: readonly string[];
|
|
57
|
+
notesTitle?: string;
|
|
58
|
+
notesSubtitle?: string;
|
|
59
|
+
notesBody?: string;
|
|
60
|
+
sharePopupEnabled?: boolean | null;
|
|
61
|
+
/**
|
|
62
|
+
* Uploaded popup art as a `data:<mime>;base64,…` URL. `null` clears it and
|
|
63
|
+
* omitting it leaves the stored image alone. There is no remote-URL form: a
|
|
64
|
+
* hotlink leaked each visitor's IP to a third party and rotted on host moves.
|
|
65
|
+
*/
|
|
66
|
+
sharePopupImage?: string | null;
|
|
67
|
+
sharePopupTitle?: string;
|
|
68
|
+
sharePopupBody?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Public key representation; it never contains a secret, hash, or IP-key digest. */
|
|
72
|
+
export interface ApiKeyResponse {
|
|
73
|
+
readonly id: string;
|
|
74
|
+
readonly label: string;
|
|
75
|
+
readonly enabled: boolean;
|
|
76
|
+
readonly keyMode: ApiKeyMode;
|
|
77
|
+
readonly scopes: readonly AccessScope[];
|
|
78
|
+
/** The tenant's mandatory gateway key: rotatable, never revocable. */
|
|
79
|
+
readonly isDefault: boolean;
|
|
80
|
+
readonly keyPrefix?: string;
|
|
81
|
+
readonly parentKeyId?: string;
|
|
82
|
+
readonly requestsPerMinute?: number;
|
|
83
|
+
readonly dailyTokenLimit?: number;
|
|
84
|
+
readonly monthlyTokenLimit?: number;
|
|
85
|
+
readonly lifetimeTokenBudget?: number;
|
|
86
|
+
readonly modelPrefix?: string;
|
|
87
|
+
readonly modelAccessMode?: ApiKeyModelAccessMode;
|
|
88
|
+
readonly modelList?: readonly string[];
|
|
89
|
+
readonly clientRouterDenylist?: readonly string[];
|
|
90
|
+
readonly maxConcurrentRequests?: number;
|
|
91
|
+
readonly notesTitle?: string;
|
|
92
|
+
readonly notesSubtitle?: string;
|
|
93
|
+
readonly notesBody?: string;
|
|
94
|
+
readonly sharePopupEnabled?: boolean;
|
|
95
|
+
/** Mime of the stored popup art; its bytes are served by a dedicated route. */
|
|
96
|
+
readonly sharePopupImageMime?: string;
|
|
97
|
+
readonly sharePopupTitle?: string;
|
|
98
|
+
readonly sharePopupBody?: string;
|
|
99
|
+
readonly createdAt: string;
|
|
100
|
+
readonly revokedAt?: string;
|
|
101
|
+
readonly tokensConsumed: number;
|
|
102
|
+
}
|
|
103
|
+
/** Creation result; plaintext is returned only when a personal key is minted. */
|
|
104
|
+
export interface CreateApiKeyResponse extends ApiKeyResponse {
|
|
105
|
+
readonly secret?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Edit result carries one new secret on personal rotation or template conversion. */
|
|
109
|
+
export interface UpdateApiKeyResponse extends ApiKeyResponse {
|
|
110
|
+
readonly secret?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** API-key route dependency boundary. */
|
|
114
|
+
export interface ApiKeyConfig {
|
|
115
|
+
readonly store: ApiKeyStore;
|
|
116
|
+
readonly accessResolver: ConsoleAccessResolver;
|
|
117
|
+
readonly auditSink?: AuditSink;
|
|
118
|
+
readonly shareStore?: ShareLinkStore;
|
|
119
|
+
readonly shareActivity?: ShareActivityPort;
|
|
120
|
+
readonly admissionService: Pick<ApiKeyAdmissionService, "purgeKey"> & {
|
|
121
|
+
seedBuckets?(request: {
|
|
122
|
+
readonly apiKeyId: string;
|
|
123
|
+
readonly now?: number;
|
|
124
|
+
readonly daily?: number;
|
|
125
|
+
readonly monthly?: number;
|
|
126
|
+
}): Promise<void>;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Reads tokens already spent in the current daily/monthly bucket, so a
|
|
130
|
+
* newly-added limit is enforced against the whole bucket. Optional: without
|
|
131
|
+
* it a new limit simply starts counting from the moment it was set.
|
|
132
|
+
*/
|
|
133
|
+
readonly bucketSpend?: (
|
|
134
|
+
keyId: string,
|
|
135
|
+
now: Date,
|
|
136
|
+
) => Promise<{ daily: number; monthly: number }>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Result of minting a share link. The bearer token is returned exactly once. */
|
|
140
|
+
export interface ShareKeyResponse {
|
|
141
|
+
readonly id: string;
|
|
142
|
+
readonly url: string;
|
|
143
|
+
/**
|
|
144
|
+
* The bearer half of the link. Null when the row was written under an
|
|
145
|
+
* encryption key this deployment no longer holds: the page stays reachable
|
|
146
|
+
* from `url`, but the secret cannot be shown again and must be regenerated
|
|
147
|
+
* before the link can authenticate.
|
|
148
|
+
*/
|
|
149
|
+
readonly token: string | null;
|
|
150
|
+
readonly kind: ShareLinkKind;
|
|
151
|
+
readonly expiresAt: string | null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** One share link as shown in the owner's console list. Never carries the token. */
|
|
155
|
+
export interface ShareLinkResponse {
|
|
156
|
+
readonly id: string;
|
|
157
|
+
readonly kind: ShareLinkKind;
|
|
158
|
+
readonly active: boolean;
|
|
159
|
+
readonly createdAt: string;
|
|
160
|
+
readonly expiresAt: string | null;
|
|
161
|
+
readonly lastViewedAt: string | null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Default prefix for newly generated API keys.
|
|
166
|
+
*
|
|
167
|
+
* The separator is a hyphen, not an underscore, and the random tail is a fixed
|
|
168
|
+
* 16 characters: `pn-carte-` for personal keys, `sh-carte-` for the ones a
|
|
169
|
+
* share template issues. Both are metadata shown in the console — nothing
|
|
170
|
+
* looks a key up by its prefix — so changing these does not invalidate rows
|
|
171
|
+
* already stored, which keep the prefix they were issued with.
|
|
172
|
+
*/
|
|
173
|
+
export const DEFAULT_API_KEY_PREFIX = "pn-carte-";
|
|
174
|
+
/** Default prefix used by share templates and the keys they issue. */
|
|
175
|
+
export const DEFAULT_SHARE_API_KEY_PREFIX = "sh-carte-";
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Bytes of entropy behind the random tail.
|
|
179
|
+
*
|
|
180
|
+
* 12 bytes base64url-encodes to exactly 16 characters with no padding, which
|
|
181
|
+
* is the whole reason for the number: the length in the constants above and
|
|
182
|
+
* the length a generated key actually has cannot disagree.
|
|
183
|
+
*/
|
|
184
|
+
const KEY_SECRET_BYTES = 12;
|
|
185
|
+
|
|
186
|
+
/** Label of the default gateway API key seeded at first boot; never revoked. */
|
|
187
|
+
export const DEFAULT_API_KEY_LABEL = "Default Cartethyia API key";
|
|
188
|
+
|
|
189
|
+
/** Recognizes the default key and legacy rows restored before its flag existed. */
|
|
190
|
+
export function isDefaultGatewayApiKey(
|
|
191
|
+
record: Pick<ApiKeyRecord, "isDefault" | "keyMode" | "label" | "parentKeyId">,
|
|
192
|
+
): boolean {
|
|
193
|
+
return record.isDefault || (
|
|
194
|
+
record.keyMode === "personal" &&
|
|
195
|
+
record.parentKeyId === undefined &&
|
|
196
|
+
record.label === DEFAULT_API_KEY_LABEL
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Generates an inbound key and its one-way hash. */
|
|
201
|
+
export function generateApiKeySecret(prefix = DEFAULT_API_KEY_PREFIX): {
|
|
202
|
+
secret: string;
|
|
203
|
+
hash: string;
|
|
204
|
+
prefix: string;
|
|
205
|
+
} {
|
|
206
|
+
const secret = `${prefix}${randomBytes(KEY_SECRET_BYTES).toString("base64url")}`;
|
|
207
|
+
return { secret, hash: hashSecret(secret), prefix };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Normalizes a caller-supplied prefix; falls back to the default. */
|
|
211
|
+
export function resolveKeyPrefix(raw: string | undefined): string {
|
|
212
|
+
const trimmed = raw?.trim();
|
|
213
|
+
return trimmed && trimmed.length > 0 ? trimmed : DEFAULT_API_KEY_PREFIX;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Resolves the share-key prefix and upgrades the legacy default prefix. */
|
|
217
|
+
export function resolveShareKeyPrefix(raw: string | undefined): string {
|
|
218
|
+
const trimmed = raw?.trim();
|
|
219
|
+
return !trimmed || trimmed === "rk_" ? DEFAULT_SHARE_API_KEY_PREFIX : trimmed;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Maximum hint the enroll page accepts; the dashboard caps its input there. */
|
|
223
|
+
export const SHARED_CHILD_HINT_MAX_LENGTH = 20;
|
|
224
|
+
|
|
225
|
+
/** Hashes an owner-supplied personal key; persistence encrypts it for CLI handoff. */
|
|
226
|
+
export function prepareCustomKey(secret: string): { secret: string; hash: string } {
|
|
227
|
+
if (secret.trim().length === 0) {
|
|
228
|
+
throw new ConsoleDomainError("invalid_key", 400, "Custom key must be a non-empty string");
|
|
229
|
+
}
|
|
230
|
+
return { secret, hash: hashSecret(secret) };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function finitePositive(value: number | null | undefined, name: string): void {
|
|
234
|
+
// `null` explicitly clears a previously set limit; `undefined` leaves it
|
|
235
|
+
// unchanged. Only a real non-null number must be finite and positive.
|
|
236
|
+
if (value !== undefined && value !== null && (!Number.isFinite(value) || value < 1)) {
|
|
237
|
+
throw new ConsoleDomainError("invalid_limits", 400, `${name} must be a finite positive number`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Decodes an uploaded popup image into the bytes and mime to persist.
|
|
243
|
+
*
|
|
244
|
+
* Returns `null` for absent/blank input (nothing to store). Throws
|
|
245
|
+
* `invalid_share_popup` for a non-data URL, an oversized payload, or bytes that
|
|
246
|
+
* are not one of the four raster formats the share page may safely serve.
|
|
247
|
+
*/
|
|
248
|
+
export function parseSharePopupImage(
|
|
249
|
+
value: string | null | undefined,
|
|
250
|
+
): { image: Buffer; mime: SharePopupImageMime } | null {
|
|
251
|
+
if (value === null || value === undefined) return null;
|
|
252
|
+
const raw = value.trim();
|
|
253
|
+
if (raw.length === 0) return null;
|
|
254
|
+
const match =
|
|
255
|
+
/^data:([a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+);base64,([A-Za-z0-9+/=]+)$/i.exec(raw);
|
|
256
|
+
const declared = match?.[1]?.toLowerCase();
|
|
257
|
+
const payload = match?.[2];
|
|
258
|
+
if (!payload || declared === undefined || !(SHARE_POPUP_IMAGE_MIMES as readonly string[]).includes(declared)) {
|
|
259
|
+
throw new ConsoleDomainError(
|
|
260
|
+
"invalid_share_popup",
|
|
261
|
+
400,
|
|
262
|
+
"sharePopupImage must be an uploaded PNG, JPEG, WebP, or GIF file",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
const image = Buffer.from(payload, "base64");
|
|
266
|
+
if (image.length === 0 || image.length > SHARE_POPUP_IMAGE_MAX_BYTES) {
|
|
267
|
+
throw new ConsoleDomainError(
|
|
268
|
+
"invalid_share_popup",
|
|
269
|
+
400,
|
|
270
|
+
`sharePopupImage must be at most ${Math.floor(SHARE_POPUP_IMAGE_MAX_BYTES / (1024 * 1024))} MB`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
const mime = sniffSharePopupImageMime(image);
|
|
274
|
+
if (mime === undefined || mime !== declared) {
|
|
275
|
+
throw new ConsoleDomainError(
|
|
276
|
+
"invalid_share_popup",
|
|
277
|
+
400,
|
|
278
|
+
"sharePopupImage bytes do not match a supported image format",
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return { image, mime };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Validates scopes and quota fields before persistence. */
|
|
285
|
+
export function validateApiKeyRequest(request: CreateApiKeyRequest): readonly AccessScope[] {
|
|
286
|
+
const keyMode = request.keyMode ?? "personal";
|
|
287
|
+
if (!(API_KEY_MODES as readonly string[]).includes(keyMode)) {
|
|
288
|
+
throw new ConsoleDomainError("invalid_key_mode", 400, "keyMode must be personal or share");
|
|
289
|
+
}
|
|
290
|
+
if (keyMode === "share" && request.key !== undefined) {
|
|
291
|
+
throw new ConsoleDomainError("invalid_key_mode", 400, "Share templates cannot carry a personal key");
|
|
292
|
+
}
|
|
293
|
+
const scopes = request.scopes ?? ["routing:invoke", "search:invoke"];
|
|
294
|
+
for (const scope of scopes) {
|
|
295
|
+
if (!isValidTenantKeyScope(scope as AccessScope)) {
|
|
296
|
+
throw new ConsoleDomainError("invalid_scope", 400, `Scope is not allowed: ${scope}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
finitePositive(request.requestsPerMinute, "requestsPerMinute");
|
|
300
|
+
finitePositive(request.dailyTokenLimit, "dailyTokenLimit");
|
|
301
|
+
finitePositive(request.monthlyTokenLimit, "monthlyTokenLimit");
|
|
302
|
+
finitePositive(request.lifetimeTokenBudget, "lifetimeTokenBudget");
|
|
303
|
+
finitePositive(request.maxConcurrentRequests, "maxConcurrentRequests");
|
|
304
|
+
parseSharePopupImage(request.sharePopupImage);
|
|
305
|
+
if (request.sharePopupTitle != null && request.sharePopupTitle.length > 120) {
|
|
306
|
+
throw new ConsoleDomainError("invalid_share_popup", 400, "Share popup title is too long");
|
|
307
|
+
}
|
|
308
|
+
if (request.sharePopupBody != null && request.sharePopupBody.length > 1200) {
|
|
309
|
+
throw new ConsoleDomainError("invalid_share_popup", 400, "Share popup body is too long");
|
|
310
|
+
}
|
|
311
|
+
if (
|
|
312
|
+
request.modelPrefix !== undefined &&
|
|
313
|
+
(typeof request.modelPrefix !== "string" || request.modelPrefix.trim().length === 0)
|
|
314
|
+
) {
|
|
315
|
+
throw new ConsoleDomainError(
|
|
316
|
+
"invalid_model_prefix",
|
|
317
|
+
400,
|
|
318
|
+
"modelPrefix must be a non-empty string if provided",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
if (
|
|
322
|
+
request.keyPrefix !== undefined &&
|
|
323
|
+
(typeof request.keyPrefix !== "string" || request.keyPrefix.trim().length === 0)
|
|
324
|
+
) {
|
|
325
|
+
throw new ConsoleDomainError(
|
|
326
|
+
"invalid_key_prefix",
|
|
327
|
+
400,
|
|
328
|
+
"keyPrefix must be a non-empty string if provided",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (request.clientRouterDenylist !== undefined) {
|
|
332
|
+
if (!Array.isArray(request.clientRouterDenylist)) {
|
|
333
|
+
throw new ConsoleDomainError(
|
|
334
|
+
"invalid_client_router_denylist",
|
|
335
|
+
400,
|
|
336
|
+
"clientRouterDenylist must be an array of client-router ids",
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
// An unrecognised id would persist a rule that can never match — the operator
|
|
340
|
+
// would believe a router is blocked while nothing blocks it. Rejecting it is
|
|
341
|
+
// the only outcome that keeps the stored policy honest.
|
|
342
|
+
for (const id of request.clientRouterDenylist) {
|
|
343
|
+
if (typeof id !== "string" || normalizeClientRouterId(id) === undefined) {
|
|
344
|
+
throw new ConsoleDomainError(
|
|
345
|
+
"invalid_client_router_denylist",
|
|
346
|
+
400,
|
|
347
|
+
`Unknown client router: ${String(id)}`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return scopes as AccessScope[];
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Serializes a persisted record without credential material or IP-key digests. */
|
|
356
|
+
export function sanitizeApiKeyResponse(record: ApiKeyRecord): ApiKeyResponse {
|
|
357
|
+
const keyPrefix =
|
|
358
|
+
record.keyMode === "share" ? resolveShareKeyPrefix(record.keyPrefix) : record.keyPrefix;
|
|
359
|
+
return {
|
|
360
|
+
id: record.id,
|
|
361
|
+
label: record.label,
|
|
362
|
+
enabled: record.enabled,
|
|
363
|
+
keyMode: record.keyMode,
|
|
364
|
+
scopes: record.scopes,
|
|
365
|
+
/**
|
|
366
|
+
* Current keys use the persisted flag. The canonical label is a compatibility
|
|
367
|
+
* marker for legacy restores and is promoted to the flag on the next update.
|
|
368
|
+
*/
|
|
369
|
+
isDefault: isDefaultGatewayApiKey(record),
|
|
370
|
+
...(keyPrefix === undefined ? {} : { keyPrefix }),
|
|
371
|
+
...(record.parentKeyId === undefined ? {} : { parentKeyId: record.parentKeyId }),
|
|
372
|
+
...(record.notesTitle === undefined ? {} : { notesTitle: record.notesTitle }),
|
|
373
|
+
...(record.notesSubtitle === undefined ? {} : { notesSubtitle: record.notesSubtitle }),
|
|
374
|
+
...(record.notesBody === undefined ? {} : { notesBody: record.notesBody }),
|
|
375
|
+
...(record.sharePopupEnabled === undefined ? {} : { sharePopupEnabled: record.sharePopupEnabled }),
|
|
376
|
+
...(record.sharePopupImageMime === undefined
|
|
377
|
+
? {}
|
|
378
|
+
: { sharePopupImageMime: record.sharePopupImageMime }),
|
|
379
|
+
...(record.sharePopupTitle === undefined ? {} : { sharePopupTitle: record.sharePopupTitle }),
|
|
380
|
+
...(record.sharePopupBody === undefined ? {} : { sharePopupBody: record.sharePopupBody }),
|
|
381
|
+
...(record.requestsPerMinute === undefined ? {} : { requestsPerMinute: record.requestsPerMinute }),
|
|
382
|
+
...(record.dailyTokenLimit === undefined ? {} : { dailyTokenLimit: record.dailyTokenLimit }),
|
|
383
|
+
...(record.monthlyTokenLimit === undefined ? {} : { monthlyTokenLimit: record.monthlyTokenLimit }),
|
|
384
|
+
...(record.lifetimeTokenBudget === undefined ? {} : { lifetimeTokenBudget: record.lifetimeTokenBudget }),
|
|
385
|
+
...(record.maxConcurrentRequests === undefined ? {} : { maxConcurrentRequests: record.maxConcurrentRequests }),
|
|
386
|
+
...(record.modelPrefix === undefined ? {} : { modelPrefix: record.modelPrefix }),
|
|
387
|
+
...(record.modelAccessMode === undefined ? {} : { modelAccessMode: record.modelAccessMode }),
|
|
388
|
+
...(record.modelList === undefined ? {} : { modelList: record.modelList }),
|
|
389
|
+
...(record.clientRouterDenylist === undefined
|
|
390
|
+
? {}
|
|
391
|
+
: { clientRouterDenylist: record.clientRouterDenylist }),
|
|
392
|
+
createdAt: record.createdAt.toISOString(),
|
|
393
|
+
...(record.revokedAt === undefined ? {} : { revokedAt: record.revokedAt.toISOString() }),
|
|
394
|
+
tokensConsumed: record.tokensConsumed,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function parseRequest(value: unknown): CreateApiKeyRequest {
|
|
399
|
+
if (typeof value !== "object" || value === null)
|
|
400
|
+
throw new ConsoleDomainError("invalid_request", 400, "Request body must be an object");
|
|
401
|
+
return value as CreateApiKeyRequest;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Maps a persisted share link to the console response (ISO timestamps, no token). */
|
|
405
|
+
export function mapShareLinkResponse(link: ShareLinkSummary): ShareLinkResponse {
|
|
406
|
+
return {
|
|
407
|
+
id: link.id,
|
|
408
|
+
kind: link.kind,
|
|
409
|
+
active: link.active,
|
|
410
|
+
createdAt: link.createdAt.toISOString(),
|
|
411
|
+
expiresAt: link.expiresAt === null ? null : link.expiresAt.toISOString(),
|
|
412
|
+
lastViewedAt: link.lastViewedAt === null ? null : link.lastViewedAt.toISOString(),
|
|
413
|
+
};
|
|
414
|
+
}
|