cartethyia 2.7.3 → 2.7.4
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/dist/dashboard/assets/{BannedPage-2TJD37Hb.js → BannedPage-ChG5svFb.js} +1 -1
- package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-xefJNRmJ.js → CliToolDetailPage-BIcWQun7.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-Rj9iHQ3F.js → CliToolsPage-Bf2VelDe.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-JEbYJFxw.js → CombosPage-DCgm5g6g.js} +1 -1
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-C4w7vea-.js → LoginPage-U6AKaJcF.js} +1 -1
- package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BtPcAfot.js → ModelPicker-BMS2t8BA.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +1 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +5 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js +9 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js +1 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-BQS0Rx_u.js → SetupPage-f0DE1h0-.js} +1 -1
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-BAL5npBC.js → SortableList-ClzpwyET.js} +1 -1
- package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-TP7voAQ0.js → StudioPage-Cq5tW7hW.js} +3 -3
- package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +56 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js +1 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
- package/dist/dashboard/assets/app-a159k0ub.css +2 -0
- package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js +15 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
- package/dist/dashboard/assets/{badge-B_DMpMJN.js → badge-D9CS6dZk.js} +1 -1
- package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DMrMPmth.js → banner-DympBNq4.js} +1 -1
- package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
- package/dist/dashboard/assets/{card-Be5Uzamx.js → card-ibRWU_uT.js} +1 -1
- package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js +1 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DOfrVmsi.js → clipboard-button-DJm-9pz7.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-C9omdjQt.js → dialog-8fiE6tec.js} +1 -1
- package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-Bm8WKaGY.js → drawer-CqGYeOrd.js} +1 -1
- package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
- package/dist/dashboard/assets/{entry-q5HwZ8nN.js → entry-BSAsYsmV.js} +1 -1
- package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js +4 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js +1 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-BTScrQpe.js → formatters-D-MTJxMr.js} +1 -1
- package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-Orjuzemr.js → github-badge-CPK47xQx.js} +1 -1
- package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
- package/dist/dashboard/assets/{index-cPrX5vn6.js → index-DBNPOfhV.js} +2 -2
- package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
- package/dist/dashboard/assets/{inline-Dvux8w-0.js → inline-BT0zntEr.js} +1 -1
- package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
- package/dist/dashboard/assets/{input-CU1Rgf3p.js → input-Da2ZViWg.js} +1 -1
- package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
- package/dist/dashboard/assets/{layout-C_7A6H55.js → layout-BXNeLw6M.js} +1 -1
- package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-CzFvogbj.js → list-row-BycOrrIV.js} +1 -1
- package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js +1 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-JJGQm8TD.js → mutation-Rr0TIvcj.js} +1 -1
- package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js +1 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BNDZnsUE.js → numbers-CEagj6ZZ.js} +2 -2
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CLaNz8Xz.js → query-keys-BloKtaXJ.js} +1 -1
- package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js +1 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js +1 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
- package/dist/dashboard/assets/{select-B3iTetHN.js → select-BIYIBkMU.js} +2 -2
- package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js +1 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
- package/dist/dashboard/assets/{stack-KrOrm7JZ.js → stack-DfgnFDWY.js} +1 -1
- package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
- package/dist/dashboard/assets/{state-DLS7IDMy.js → state-CkfdYWel.js} +1 -1
- package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
- package/dist/dashboard/assets/{switch-42ItvbAs.js → switch-SYm237HG.js} +1 -1
- package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-Dmnfh1Ue.js → useMutation-BQqCRNIi.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +55 -54
- package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +9 -10
- package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
- package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
- package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
- package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
- package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +9 -10
- package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
- package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
- package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
- package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
- package/package.json +2 -1
- package/src/boot/app.ts +9 -8
- package/src/config/network.ts +7 -3
- package/src/console/api-keys/contracts.ts +52 -0
- package/src/console/api-keys/routes.ts +93 -4
- package/src/console/audit/routes.ts +3 -3
- package/src/console/auth/account-reset.ts +4 -4
- package/src/console/auth/session.ts +3 -3
- package/src/console/backup/contracts.ts +2 -0
- package/src/console/backup/export-endpoint.ts +6 -6
- package/src/console/backup/routes.ts +3 -3
- package/src/console/cli-tools/routes.ts +3 -3
- package/src/console/console-router-guards.ts +2 -2
- package/src/console/console-router.ts +4 -4
- package/src/console/data-management/routes.ts +3 -3
- package/src/console/domain-registration.ts +2 -2
- package/src/console/observability/contracts.ts +4 -0
- package/src/console/observability/live.ts +3 -3
- package/src/console/observability/logs.ts +3 -3
- package/src/console/observability/performance.ts +3 -3
- package/src/console/observability/routes.ts +3 -3
- package/src/console/observability/store.ts +16 -16
- package/src/console/providers/catalog/contracts.ts +43 -22
- package/src/console/providers/catalog/provider-operations.ts +17 -2
- package/src/console/providers/catalog/routes.ts +3 -3
- package/src/console/providers/catalog/store-helpers.ts +349 -326
- package/src/console/providers/catalog/store.ts +1523 -1292
- package/src/console/providers/detail/routes.ts +3 -3
- package/src/console/providers/oauth/routes.ts +3 -3
- package/src/console/quota/account-quota-tenant.ts +26 -1
- package/src/console/quota/account-test-egress.ts +4 -4
- package/src/console/routing/model/routes.ts +3 -3
- package/src/console/routing/pools/contracts.ts +11 -8
- package/src/console/routing/pools/operations.ts +14 -7
- package/src/console/routing/pools/probe-verdict.ts +8 -3
- package/src/console/routing/pools/relay-deploy.ts +9 -9
- package/src/console/routing/pools/routes.ts +3 -3
- package/src/console/routing/pools/store.ts +21 -1
- package/src/console/settings/routes.ts +3 -3
- package/src/console/share/share-router.ts +12 -12
- package/src/console/share/share-stats.ts +105 -35
- package/src/console/shared/read-through-cache.ts +38 -2
- package/src/console/updates/routes.ts +7 -4
- package/src/network/outbound-fetch.ts +75 -4
- package/src/network/pool/resolver.ts +39 -39
- package/src/network/pool-cooldown-store.ts +71 -16
- package/src/network/pool-health-machine.ts +21 -1
- package/src/network/rate-limit-verdict.ts +68 -0
- package/src/network/ssrf.ts +4 -4
- package/src/observability/event-query.ts +320 -16
- package/src/observability/telemetry-status.ts +1 -1
- package/src/persistence/db-sqlite.ts +7 -2
- package/src/persistence/readiness.ts +18 -6
- package/src/persistence/schema.ts +66 -7
- package/src/persistence/stores/api-key-store.ts +99 -3
- package/src/persistence/stores/telemetry-store.ts +1 -1
- package/src/protocol/codecs.ts +6 -6
- package/src/protocol/gateway-error.ts +6 -3
- package/src/protocol/image-generation.ts +2 -2
- package/src/protocol/response/chat.ts +2 -2
- package/src/protocol/response/codex.ts +1 -1
- package/src/protocol/response/gemini.ts +1 -1
- package/src/protocol/response/messages.ts +5 -5
- package/src/protocol/response/responses.ts +2 -2
- package/src/protocol/sse-decoder.ts +14 -3
- package/src/protocol/translation/capabilities.ts +2 -2
- package/src/protocol/upstream/send-claude-messages.ts +3 -3
- package/src/providers/adapters/base-adapter.ts +1 -1
- package/src/providers/adapters/compatible-adapter.ts +2 -2
- package/src/providers/authentication/oauth-refresh-service.ts +2 -2
- package/src/providers/configured-provider.ts +1 -1
- package/src/providers/discovery/probe-phases.ts +2 -2
- package/src/providers/discovery/probing-service.ts +12 -12
- package/src/providers/integrations/agentrouter/agentrouter.ts +2 -3
- package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
- package/src/providers/integrations/anthropic.ts +4 -11
- package/src/providers/integrations/antigravity/antigravity.ts +13 -24
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
- package/src/providers/integrations/buddy/buddy-identity.ts +22 -1
- package/src/providers/integrations/buddy/buddy-media.ts +16 -16
- package/src/providers/integrations/buddy/codebuddy/provider.ts +1 -1
- package/src/providers/integrations/buddy/workbuddy/provider.ts +1 -1
- package/src/providers/integrations/claude/claude-betas.ts +1 -1
- package/src/providers/integrations/claude/claude-credentials.ts +1 -1
- package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
- package/src/providers/integrations/claude/claude.ts +3 -10
- package/src/providers/integrations/cline/cline.ts +1 -1
- package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
- package/src/providers/integrations/codex/codex-errors.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +14 -15
- package/src/providers/integrations/commandcode/commandcode.ts +4 -4
- package/src/providers/integrations/commandcode/quota.ts +5 -1
- package/src/providers/integrations/cursor/cursor-http2.ts +11 -11
- package/src/providers/integrations/cursor/cursor.ts +15 -15
- package/src/providers/integrations/factory/factory.ts +4 -4
- package/src/providers/integrations/gemini.ts +11 -11
- package/src/providers/integrations/github/github.ts +6 -6
- package/src/providers/integrations/grok/grok.ts +2 -6
- package/src/providers/integrations/inferhub.ts +4 -11
- package/src/providers/integrations/kimi/kimi.ts +4 -11
- package/src/providers/integrations/kiro/kiro.ts +10 -10
- package/src/providers/integrations/muse/muse.ts +6 -9
- package/src/providers/integrations/perplexity.ts +2 -2
- package/src/providers/integrations/qoder/qoder-catalog.ts +47 -9
- package/src/providers/integrations/qoder/qoder-checkin.ts +26 -8
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +9 -8
- package/src/providers/integrations/qoder/qoder-realm.ts +15 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +12 -13
- package/src/providers/integrations/qoder/qoder-virtual.ts +2 -2
- package/src/providers/integrations/qoder/qoder.ts +188 -22
- package/src/providers/integrations/tokenharbor.ts +2 -2
- package/src/providers/integrations/trae/trae.ts +6 -9
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- package/src/providers/operations/provider-catalog-service.ts +4 -4
- package/src/providers/operations/provider-credential-service.ts +16 -4
- package/src/providers/operations/upstream-deadline.ts +3 -3
- package/src/providers/provider-contracts.ts +47 -3
- package/src/providers/provider-credential-resolver.ts +1 -1
- package/src/providers/provider-registry.ts +4 -4
- package/src/providers/quota/quota-contracts.ts +24 -10
- package/src/providers/quota/refresh.ts +2 -2
- package/src/providers/search/search-provider.ts +5 -5
- package/src/security/abuse.ts +4 -4
- package/src/security/admission/reasons.ts +17 -17
- package/src/security/api-key-auth.ts +4 -4
- package/src/shared/coerce.ts +19 -0
- package/src/transport/dispatch/attempt-finalize.ts +4 -3
- package/src/transport/dispatch/attempt-loop.ts +10 -7
- package/src/transport/dispatch/empty-stream.ts +6 -6
- package/src/transport/dispatch/generation.ts +9 -9
- package/src/transport/dispatch/leases.ts +8 -8
- package/src/transport/dispatch/proxy-request.ts +7 -7
- package/src/transport/dispatch/responses-compact.ts +11 -11
- package/src/transport/dispatch/retry-policy.ts +4 -67
- package/src/transport/dispatch/stream-bridge.ts +1 -1
- package/src/transport/dispatch/streaming/finalize.ts +9 -8
- package/src/transport/dispatch/systemone.ts +8 -8
- package/src/transport/dispatch/token-count.ts +9 -9
- package/src/transport/dispatch/websearch.ts +6 -6
- package/src/transport/drain-endpoint.ts +2 -2
- package/src/transport/failure-policy.ts +13 -6
- package/src/transport/guards/auth-guard.ts +6 -5
- package/src/transport/guards/body-policy.ts +4 -4
- package/src/transport/guards/error-lifecycle.ts +18 -3
- package/src/transport/guards/guard-errors.ts +7 -3
- package/src/transport/guards/headroom-guard.ts +1 -1
- package/src/transport/guards/pipeline.ts +6 -6
- package/src/transport/guards/readiness-guard.ts +16 -2
- package/src/transport/guards/request-context.ts +3 -3
- package/src/transport/request/preparer.ts +20 -20
- package/src/transport/request/state.ts +4 -7
- package/src/transport/routing/route-catalog.ts +97 -2
- package/src/transport/routing/route-model.ts +7 -7
- package/src/transport/routing/router.ts +2 -2
- package/src/transport/shutdown-notice.ts +19 -7
- package/src/transport/surface/completion.ts +1 -1
- package/src/transport/surface/messages/encode.ts +1 -1
- package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +0 -4
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +0 -9
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js +0 -1
- package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CDZjTt87.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js +0 -56
- package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-MbI7e7rt.js +0 -1
- package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
- package/dist/dashboard/assets/app-B2vyPoII.css +0 -2
- package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
- package/dist/dashboard/assets/assert-C0YYuZJ4.js +0 -1
- package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
- package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
- package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
- package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-muFRphDu.js +0 -1
- package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
- package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
- package/dist/dashboard/assets/entry-DS90gUks.js +0 -1
- package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
- package/dist/dashboard/assets/entry-j40LJotO.js +0 -4
- package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
- package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
- package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
- package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
- package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
- package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
- package/dist/dashboard/assets/layout-C_7A6H55.js.gz +0 -0
- package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
- package/dist/dashboard/assets/live-KaG15HLQ.js +0 -1
- package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
- package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/routing-BEH32QOC.js +0 -1
- package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
- package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
- package/dist/dashboard/assets/settings-Dt3Eqbnq.js +0 -1
- package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
- package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
- package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
- package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
- /package/dist/dashboard/assets/{app-wvwCi4Eh.js → app-BayTTy01.js} +0 -0
- /package/dist/dashboard/assets/{app-wvwCi4Eh.js.gz → app-BayTTy01.js.gz} +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Owns validation, CRUD operations, and route wiring through the ApiKeyStore.
|
|
4
4
|
|
|
5
|
-
import { Elysia, t } from "elysia";
|
|
5
|
+
import { Elysia, t, type AnyElysia } from "elysia";
|
|
6
6
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
7
7
|
import { decryptCredentialToString, encryptCredential } from "../../security/crypto";
|
|
8
8
|
import { invalidateApiKeyCache } from "../../security/api-key-auth";
|
|
@@ -12,7 +12,7 @@ import type { AccessDecision } from "../../security/access-control";
|
|
|
12
12
|
import { ConsoleDomainError, errorResponse, requireTenantScope } from "../shared/errors";
|
|
13
13
|
import { literalUnion } from "../shared/elysia-schema";
|
|
14
14
|
import { API_KEY_MODES, API_KEY_MODEL_ACCESS_MODES, type ShareLinkKind } from "../../persistence/schema";
|
|
15
|
-
import type { ApiKeyConfig, ApiKeyResponse, UpdateApiKeyResponse } from "./contracts";
|
|
15
|
+
import type { ApiKeyConfig, ApiKeyResponse, BanApiKeyRequest, UpdateApiKeyResponse } from "./contracts";
|
|
16
16
|
import type {
|
|
17
17
|
ApiKeyPatch,
|
|
18
18
|
ApiKeyRecord,
|
|
@@ -20,6 +20,8 @@ import type {
|
|
|
20
20
|
SharedChildrenPageOptions,
|
|
21
21
|
} from "../../persistence/stores/api-key-store";
|
|
22
22
|
import {
|
|
23
|
+
BAN_DURATION_CHOICES,
|
|
24
|
+
DEFAULT_BAN_MINUTES,
|
|
23
25
|
generateApiKeySecret,
|
|
24
26
|
isDefaultGatewayApiKey,
|
|
25
27
|
parseSharePopupImage,
|
|
@@ -775,6 +777,69 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
775
777
|
});
|
|
776
778
|
return { success: true };
|
|
777
779
|
},
|
|
780
|
+
async banKey(
|
|
781
|
+
access: AccessDecision | undefined,
|
|
782
|
+
keyId: string,
|
|
783
|
+
request: BanApiKeyRequest,
|
|
784
|
+
): Promise<ApiKeyResponse> {
|
|
785
|
+
const authorized = requireTenantScope(access, "dashboard:write");
|
|
786
|
+
const reason = request.reason.trim();
|
|
787
|
+
if (reason.length === 0)
|
|
788
|
+
throw new ConsoleDomainError("invalid_request", 400, "reason is required");
|
|
789
|
+
// Parse the duration choice. `null` is permanent; absent defaults to one
|
|
790
|
+
// hour. Anything not on the offered list is rejected rather than clamped —
|
|
791
|
+
// clamping would answer a request for 90 days with a week and report
|
|
792
|
+
// success.
|
|
793
|
+
const chosenMinutes = request.minutes === undefined ? DEFAULT_BAN_MINUTES : request.minutes;
|
|
794
|
+
const allowed = new Set(BAN_DURATION_CHOICES.map((choice) => choice.minutes));
|
|
795
|
+
if (!allowed.has(chosenMinutes)) {
|
|
796
|
+
throw new ConsoleDomainError(
|
|
797
|
+
"invalid_request",
|
|
798
|
+
400,
|
|
799
|
+
"Unsupported ban duration. Choose one of the offered durations.",
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
const bannedUntil = chosenMinutes === null ? null : new Date(Date.now() + chosenMinutes * 60_000);
|
|
803
|
+
const current = await config.store.get(authorized.tenantId, keyId);
|
|
804
|
+
if (!current || current.revokedAt !== undefined)
|
|
805
|
+
throw new ConsoleDomainError("key_not_found", 404, "Key not found");
|
|
806
|
+
const banned = await config.store.ban(authorized.tenantId, keyId, reason, bannedUntil);
|
|
807
|
+
if (!banned) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
|
|
808
|
+
// A banned child key does not affect its share template or sibling keys:
|
|
809
|
+
// `store.ban` only turns off the one row named by `keyId`, and the auth
|
|
810
|
+
// path rejects per key hash. The template keeps issuing working children.
|
|
811
|
+
// A banned key must leave the auth cache for every process at once.
|
|
812
|
+
invalidateApiKeyCache();
|
|
813
|
+
await config.auditSink?.record({
|
|
814
|
+
access: authorized,
|
|
815
|
+
action: "api_key.banned",
|
|
816
|
+
target: keyId,
|
|
817
|
+
detail: { reason, ...(bannedUntil === null ? { permanent: true } : { bannedUntil: bannedUntil.toISOString() }) },
|
|
818
|
+
});
|
|
819
|
+
const record = await config.store.get(authorized.tenantId, keyId);
|
|
820
|
+
if (!record) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
|
|
821
|
+
return sanitizeApiKeyResponse(record);
|
|
822
|
+
},
|
|
823
|
+
async unbanKey(
|
|
824
|
+
access: AccessDecision | undefined,
|
|
825
|
+
keyId: string,
|
|
826
|
+
): Promise<ApiKeyResponse> {
|
|
827
|
+
const authorized = requireTenantScope(access, "dashboard:write");
|
|
828
|
+
const current = await config.store.get(authorized.tenantId, keyId);
|
|
829
|
+
if (!current || current.revokedAt !== undefined)
|
|
830
|
+
throw new ConsoleDomainError("key_not_found", 404, "Key not found");
|
|
831
|
+
const unbanned = await config.store.unban(authorized.tenantId, keyId);
|
|
832
|
+
if (!unbanned) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
|
|
833
|
+
invalidateApiKeyCache();
|
|
834
|
+
await config.auditSink?.record({
|
|
835
|
+
access: authorized,
|
|
836
|
+
action: "api_key.unbanned",
|
|
837
|
+
target: keyId,
|
|
838
|
+
});
|
|
839
|
+
const record = await config.store.get(authorized.tenantId, keyId);
|
|
840
|
+
if (!record) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
|
|
841
|
+
return sanitizeApiKeyResponse(record);
|
|
842
|
+
},
|
|
778
843
|
};
|
|
779
844
|
return operations;
|
|
780
845
|
}
|
|
@@ -830,7 +895,7 @@ function jsonNull(): Response {
|
|
|
830
895
|
});
|
|
831
896
|
}
|
|
832
897
|
/** Creates tenant-bound API-key routes with request-local authorization. */
|
|
833
|
-
export function createApiKeyRoutes(config: ApiKeyConfig):
|
|
898
|
+
export function createApiKeyRoutes(config: ApiKeyConfig): AnyElysia {
|
|
834
899
|
const factory = createApiKeyOperations(config);
|
|
835
900
|
return new Elysia({ prefix: "/api-keys" })
|
|
836
901
|
.get("/", async ({ request, set }) => {
|
|
@@ -950,6 +1015,30 @@ export function createApiKeyRoutes(config: ApiKeyConfig): Elysia {
|
|
|
950
1015
|
return errorResponse(error, set, "API-key operation failed");
|
|
951
1016
|
}
|
|
952
1017
|
})
|
|
1018
|
+
.post(
|
|
1019
|
+
"/:keyId/ban",
|
|
1020
|
+
{
|
|
1021
|
+
body: t.Object({
|
|
1022
|
+
reason: t.String(),
|
|
1023
|
+
reasonLabel: t.Optional(t.String()),
|
|
1024
|
+
minutes: t.Optional(t.Union([t.Number({ minimum: 1 }), t.Null()])),
|
|
1025
|
+
}),
|
|
1026
|
+
},
|
|
1027
|
+
async ({ request, params, body, set }) => {
|
|
1028
|
+
try {
|
|
1029
|
+
return await factory.banKey(config.accessResolver(request), params.keyId, body);
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
return errorResponse(error, set, "API-key operation failed");
|
|
1032
|
+
}
|
|
1033
|
+
},
|
|
1034
|
+
)
|
|
1035
|
+
.post("/:keyId/unban", async ({ request, params, set }) => {
|
|
1036
|
+
try {
|
|
1037
|
+
return await factory.unbanKey(config.accessResolver(request), params.keyId);
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
return errorResponse(error, set, "API-key operation failed");
|
|
1040
|
+
}
|
|
1041
|
+
})
|
|
953
1042
|
.get("/:keyId/share", async ({ request, params, set }) => {
|
|
954
1043
|
try {
|
|
955
1044
|
const share = await factory.getShare(config.accessResolver(request), params.keyId, {
|
|
@@ -988,5 +1077,5 @@ export function createApiKeyRoutes(config: ApiKeyConfig): Elysia {
|
|
|
988
1077
|
} catch (error) {
|
|
989
1078
|
return errorResponse(error, set, "API-key operation failed");
|
|
990
1079
|
}
|
|
991
|
-
})
|
|
1080
|
+
});
|
|
992
1081
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Elysia, t } from "elysia";
|
|
1
|
+
import { Elysia, t, type AnyElysia } from "elysia";
|
|
2
2
|
import { errorResponse, requireGlobalAdmin, requireTenantScope } from "../shared/errors";
|
|
3
3
|
import { parseQueryLimit } from "../shared/query";
|
|
4
4
|
import type { AuditRoutesConfig } from "./contracts";
|
|
@@ -10,7 +10,7 @@ const listQuery = t.Object({
|
|
|
10
10
|
actor: t.Optional(t.String()),
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
export function createAuditRoutes(config: AuditRoutesConfig):
|
|
13
|
+
export function createAuditRoutes(config: AuditRoutesConfig): AnyElysia {
|
|
14
14
|
return new Elysia().get("/audit", { query: listQuery }, async ({ request, query, set }) => {
|
|
15
15
|
try {
|
|
16
16
|
const resolved = config.accessResolver(request);
|
|
@@ -28,5 +28,5 @@ export function createAuditRoutes(config: AuditRoutesConfig): Elysia {
|
|
|
28
28
|
} catch (e) {
|
|
29
29
|
return errorResponse(e, set, "Audit read failed");
|
|
30
30
|
}
|
|
31
|
-
})
|
|
31
|
+
});
|
|
32
32
|
}
|
|
@@ -42,12 +42,12 @@ export function createConsoleAccountResetHandler(
|
|
|
42
42
|
return async ({ request, body }) => {
|
|
43
43
|
if (!isLoopbackPeer(deps.resolvePeerAddress(request)))
|
|
44
44
|
throw new GatewayError(
|
|
45
|
-
"
|
|
45
|
+
"policy_rejected",
|
|
46
46
|
403,
|
|
47
|
-
"console account reset is only available from loopback",
|
|
47
|
+
"console account reset is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
|
|
48
48
|
);
|
|
49
49
|
if (!tokenMatches(request.headers.get("x-drain-token"), deps.token))
|
|
50
|
-
throw new GatewayError("
|
|
50
|
+
throw new GatewayError("authentication_failed", 401, "invalid reset token (request {requestId}). Wrong key, right door — check the token 🔑");
|
|
51
51
|
const confirm =
|
|
52
52
|
body !== null && typeof body === "object" && "confirm" in body
|
|
53
53
|
? body.confirm
|
|
@@ -56,7 +56,7 @@ export function createConsoleAccountResetHandler(
|
|
|
56
56
|
throw new GatewayError(
|
|
57
57
|
"invalid_request",
|
|
58
58
|
400,
|
|
59
|
-
`send {"confirm":"${CONFIRMATION}"} to confirm; this deletes every console account
|
|
59
|
+
`send {"confirm":"${CONFIRMATION}"} to confirm (request {requestId}); this deletes every console account. Make sure you mean it — there's no undo 🫂`,
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
const removed = await deps.resetConsoleAccount();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { asCookie, clearCsrfCookie, newCsrfToken, setCsrfCookie } from "../../security/csrf";
|
|
2
|
-
import { Elysia, t } from "elysia";
|
|
2
|
+
import { Elysia, t, type AnyElysia } from "elysia";
|
|
3
3
|
import { eq } from "drizzle-orm";
|
|
4
4
|
import { isRecord } from "../../protocol/codecs";
|
|
5
5
|
import { userRecord, returningRows, assertMutationApplied } from "./row-coercion";
|
|
@@ -217,7 +217,7 @@ export function createConsoleAuthRoutes(
|
|
|
217
217
|
cookiePolicy: SessionCookiePolicy = defaultSessionCookiePolicy,
|
|
218
218
|
trustedProxyBoundary: TrustedProxyBoundary = { mode: "disabled" },
|
|
219
219
|
resolvePeerAddress: (request: Request) => string | undefined = () => undefined,
|
|
220
|
-
):
|
|
220
|
+
): AnyElysia {
|
|
221
221
|
const app = new Elysia({ prefix: "/auth" });
|
|
222
222
|
const policy = cookiePolicy;
|
|
223
223
|
const database = db;
|
|
@@ -532,5 +532,5 @@ export function createConsoleAuthRoutes(
|
|
|
532
532
|
},
|
|
533
533
|
);
|
|
534
534
|
|
|
535
|
-
return app
|
|
535
|
+
return app;
|
|
536
536
|
}
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
poolRoutingSettings,
|
|
34
34
|
providerAccounts,
|
|
35
35
|
providerOauthStates,
|
|
36
|
+
providerPoolAccounts,
|
|
36
37
|
providerRoutingSettings,
|
|
37
38
|
providers,
|
|
38
39
|
shareLinks,
|
|
@@ -96,6 +97,7 @@ export const CONFIG_TABLES = [
|
|
|
96
97
|
models,
|
|
97
98
|
providerRoutingSettings,
|
|
98
99
|
providerAccounts,
|
|
100
|
+
providerPoolAccounts,
|
|
99
101
|
poolRoutingSettings,
|
|
100
102
|
networkPools,
|
|
101
103
|
providers,
|
|
@@ -52,20 +52,20 @@ export function createBackupExportHandler(
|
|
|
52
52
|
// came from, the second is the credential it carried — and `invalid_request`
|
|
53
53
|
// told an operator to fix a body that was never the problem.
|
|
54
54
|
if (!isLoopbackPeer(deps.resolvePeerAddress(request)))
|
|
55
|
-
throw new GatewayError("policy_rejected", 403, "backup export is only available from loopback");
|
|
55
|
+
throw new GatewayError("policy_rejected", 403, "backup export is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂");
|
|
56
56
|
if (!tokenMatches(request.headers.get("x-drain-token"), deps.token))
|
|
57
|
-
throw new GatewayError("authentication_failed", 401, "invalid export token");
|
|
57
|
+
throw new GatewayError("authentication_failed", 401, "invalid export token (request {requestId}). Wrong key, right door — check the token 🔑");
|
|
58
58
|
|
|
59
59
|
const directory =
|
|
60
60
|
body !== null && typeof body === "object" && "directory" in body
|
|
61
61
|
? body.directory
|
|
62
62
|
: undefined;
|
|
63
63
|
if (directory !== undefined && typeof directory !== "string")
|
|
64
|
-
throw new GatewayError("invalid_request", 400, "directory must be a string");
|
|
64
|
+
throw new GatewayError("invalid_request", 400, "directory must be a string (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
65
65
|
|
|
66
66
|
const tenantId = await deps.resolveTenantId();
|
|
67
67
|
if (tenantId === null)
|
|
68
|
-
throw new GatewayError("invalid_request", 409, "no tenant exists; run setup first");
|
|
68
|
+
throw new GatewayError("invalid_request", 409, "no tenant exists; run setup first (request {requestId}). Nothing here but dust — run setup first 🌙");
|
|
69
69
|
|
|
70
70
|
// One gzip pass over the streamed document: a tenant with hundreds of
|
|
71
71
|
// thousands of rows is read page by page and only the compressed bytes
|
|
@@ -83,9 +83,9 @@ export function createBackupExportHandler(
|
|
|
83
83
|
writeFileSync(path, gzipped);
|
|
84
84
|
} catch (error) {
|
|
85
85
|
throw new GatewayError(
|
|
86
|
-
"
|
|
86
|
+
"internal_error",
|
|
87
87
|
500,
|
|
88
|
-
`could not write ${path}: ${error instanceof Error ? error.message : "unknown error"}
|
|
88
|
+
`could not write ${path}: ${error instanceof Error ? error.message : "unknown error"} (request {requestId}). Even we have bad days — retry, and quote the request id if it persists 🐻`,
|
|
89
89
|
);
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* The body bound is enforced here, before the JSON parse, because the whole
|
|
15
15
|
* point of the limit is to not buffer an unbounded file.
|
|
16
16
|
*/
|
|
17
|
-
import { Elysia, t } from "elysia";
|
|
17
|
+
import { Elysia, t, type AnyElysia } from "elysia";
|
|
18
18
|
import type { ConsoleAccessResolver } from "../auth/access";
|
|
19
19
|
import { invalidateApiKeyCache } from "../../security/api-key-auth";
|
|
20
20
|
import { invalidatePublicModels } from "../providers/catalog/public-model-store";
|
|
@@ -139,7 +139,7 @@ function payloadDocument(chunks: AsyncGenerator<string, void, void>): ReadableSt
|
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
export function createBackupRoutes(config: BackupRoutesConfig):
|
|
142
|
+
export function createBackupRoutes(config: BackupRoutesConfig): AnyElysia {
|
|
143
143
|
return new Elysia({ prefix: "/backup" })
|
|
144
144
|
.get("/export", { query: exportQuery }, async ({ request, query, set }) => {
|
|
145
145
|
try {
|
|
@@ -304,5 +304,5 @@ export function createBackupRoutes(config: BackupRoutesConfig): Elysia {
|
|
|
304
304
|
} catch (error) {
|
|
305
305
|
return errorResponse(error, set, "Backup inspection failed");
|
|
306
306
|
}
|
|
307
|
-
})
|
|
307
|
+
});
|
|
308
308
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ConsoleAccessResolver } from "../auth/access";
|
|
2
2
|
import type { AuditSink } from "../audit/contracts";
|
|
3
|
-
import { Elysia, t } from "elysia";
|
|
3
|
+
import { Elysia, t, type AnyElysia } from "elysia";
|
|
4
4
|
import { errorResponse, requireTenantScope } from "../shared/errors";
|
|
5
5
|
import type { CliToolService } from "./service";
|
|
6
6
|
import type { ApplyInput, CliMappingInput, CliModelMapping } from "./contracts";
|
|
@@ -143,7 +143,7 @@ const mappingBody = t.Object({
|
|
|
143
143
|
),
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
-
export function createCliToolsRoutes(config: CliToolsRoutesConfig):
|
|
146
|
+
export function createCliToolsRoutes(config: CliToolsRoutesConfig): AnyElysia {
|
|
147
147
|
const { service, accessResolver, auditSink, snapshotInvalidator } = config;
|
|
148
148
|
|
|
149
149
|
return new Elysia({ prefix: "/cli-tools" })
|
|
@@ -335,5 +335,5 @@ export function createCliToolsRoutes(config: CliToolsRoutesConfig): Elysia {
|
|
|
335
335
|
} catch (e) {
|
|
336
336
|
return errorResponse(e, set, "CLI tool operation failed");
|
|
337
337
|
}
|
|
338
|
-
})
|
|
338
|
+
});
|
|
339
339
|
}
|
|
@@ -35,7 +35,7 @@ export function createConsoleCsrfMiddleware(): Elysia {
|
|
|
35
35
|
// CSRF enforcement applies exactly when a session cookie is present.
|
|
36
36
|
if (!token) return;
|
|
37
37
|
if (!isCsrfValid(request))
|
|
38
|
-
throw new GatewayError("
|
|
38
|
+
throw new GatewayError("policy_rejected", 403, "CSRF validation failed (request {requestId}). Not allowed by policy — no hard feelings 🫂");
|
|
39
39
|
})
|
|
40
40
|
.as("plugin");
|
|
41
41
|
}
|
|
@@ -102,7 +102,7 @@ export function createConsoleMutationLimiterMiddleware(): Elysia {
|
|
|
102
102
|
? Math.ceil(CONSOLE_MUTATION_WINDOW_MS / 1000)
|
|
103
103
|
: Math.max(1, Math.ceil((oldest + CONSOLE_MUTATION_WINDOW_MS - now) / 1000));
|
|
104
104
|
set.headers["retry-after"] = String(retryAfterSeconds);
|
|
105
|
-
throw new GatewayError("
|
|
105
|
+
throw new GatewayError("rate_limited", 429, "Console mutation rate limit exceeded (request {requestId}). Retry shortly. You've maxed this window — pace yourself ⏳");
|
|
106
106
|
}
|
|
107
107
|
hits.push(now);
|
|
108
108
|
consoleMutationHits.set(key, hits);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* composition time; concrete Drizzle stores are the defaults, so this file
|
|
5
5
|
* is the single place that knows them.
|
|
6
6
|
*/
|
|
7
|
-
import { Elysia } from "elysia";
|
|
7
|
+
import { Elysia, type AnyElysia } from "elysia";
|
|
8
8
|
import { log } from "../observability/logger";
|
|
9
9
|
import { resolveConsoleAccess, resolveConsoleUser } from "./auth/session-resolver";
|
|
10
10
|
import { ConsoleCredentialService } from "./auth/credentials";
|
|
@@ -100,7 +100,7 @@ function readApiKeyBearer(request: Request): string | undefined {
|
|
|
100
100
|
return match?.[1];
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
export function createConsoleRouter(deps: ConsoleApiCompositionDeps):
|
|
103
|
+
export function createConsoleRouter(deps: ConsoleApiCompositionDeps): AnyElysia {
|
|
104
104
|
const { db, routeSnapshotService, poolSelector, telemetryBuffer, redis } = deps;
|
|
105
105
|
/**
|
|
106
106
|
* Per-request cache: `beforeHandle` resolves the session asynchronously, and
|
|
@@ -207,7 +207,7 @@ export function createConsoleRouter(deps: ConsoleApiCompositionDeps): Elysia {
|
|
|
207
207
|
return user === null ? null : { passwordHash: user.passwordHash };
|
|
208
208
|
},
|
|
209
209
|
};
|
|
210
|
-
registerConsoleDomains(console
|
|
210
|
+
registerConsoleDomains(console, ctx);
|
|
211
211
|
// Compress the JSON control plane on the way out.
|
|
212
212
|
//
|
|
213
213
|
// `wrap` is used rather than a lifecycle hook because this Elysia beta does
|
|
@@ -229,5 +229,5 @@ export function createConsoleRouter(deps: ConsoleApiCompositionDeps): Elysia {
|
|
|
229
229
|
gzipResponse(request, response),
|
|
230
230
|
),
|
|
231
231
|
);
|
|
232
|
-
return console
|
|
232
|
+
return console;
|
|
233
233
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Elysia } from "elysia";
|
|
1
|
+
import { Elysia, type AnyElysia } from "elysia";
|
|
2
2
|
import { t } from "elysia";
|
|
3
3
|
import { errorResponse, requireAnyScope } from "../shared/errors";
|
|
4
4
|
import type { AccessDecision } from "../../security/access-control";
|
|
@@ -39,7 +39,7 @@ export function createDataManagementRoutes(
|
|
|
39
39
|
} | undefined;
|
|
40
40
|
readonly flusher?: { flush(): Promise<void> } | undefined;
|
|
41
41
|
},
|
|
42
|
-
):
|
|
42
|
+
): AnyElysia {
|
|
43
43
|
const operations = createDataManagementOperations(config);
|
|
44
44
|
|
|
45
45
|
return new Elysia({ prefix: "/data-management" })
|
|
@@ -101,5 +101,5 @@ export function createDataManagementRoutes(
|
|
|
101
101
|
} catch (error) {
|
|
102
102
|
return errorResponse(error, set, "Purge failed");
|
|
103
103
|
}
|
|
104
|
-
})
|
|
104
|
+
});
|
|
105
105
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type AnyElysia } from "elysia";
|
|
2
2
|
import { ConsoleDomainError } from "./shared/errors";
|
|
3
3
|
import { createCliToolsRoutes } from "./cli-tools/routes";
|
|
4
4
|
import { createAccountQuotaRoutes } from "./quota/account-quota";
|
|
@@ -133,7 +133,7 @@ import type { ProbeOutboundBinding } from "../providers/discovery/probing-servic
|
|
|
133
133
|
|
|
134
134
|
|
|
135
135
|
export function registerConsoleDomains(
|
|
136
|
-
console:
|
|
136
|
+
console: AnyElysia,
|
|
137
137
|
ctx: ConsoleDomainContext,
|
|
138
138
|
): void {
|
|
139
139
|
const observabilityStore = new DrizzleObservabilityStore(ctx.db, ctx.redis);
|
|
@@ -191,6 +191,10 @@ export interface UsageRequestItem {
|
|
|
191
191
|
apiKeyId?: string;
|
|
192
192
|
userAgent?: string;
|
|
193
193
|
clientName?: string;
|
|
194
|
+
/** The proxy pool that carried this request, when one was used. */
|
|
195
|
+
networkPoolId?: string;
|
|
196
|
+
/** Operator-facing label of the pool; absent when the pool is gone. */
|
|
197
|
+
networkPoolLabel?: string;
|
|
194
198
|
estimatedCost?: number;
|
|
195
199
|
/** Provider-billed credits; absent when the upstream reported no credit field. */
|
|
196
200
|
creditUsed?: number;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Every endpoint requires `dashboard:read`. The global request count is
|
|
8
8
|
// process-level; pool usage and health are filtered to the caller's tenant.
|
|
9
9
|
|
|
10
|
-
import { Elysia } from "elysia";
|
|
10
|
+
import { Elysia, type AnyElysia } from "elysia";
|
|
11
11
|
import { errorResponse, requireScope } from "../shared/errors";
|
|
12
12
|
import type { ConsoleAccessResolver } from "../auth/access";
|
|
13
13
|
/**
|
|
@@ -86,7 +86,7 @@ function withPoolBytes<T extends { poolId: string; currentInflight?: number }>(
|
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
export function createLiveRoutes(config: LiveConfig):
|
|
89
|
+
export function createLiveRoutes(config: LiveConfig): AnyElysia {
|
|
90
90
|
return new Elysia()
|
|
91
91
|
.get("/live/requests/stream", ({ request, set }) => {
|
|
92
92
|
try {
|
|
@@ -176,5 +176,5 @@ export function createLiveRoutes(config: LiveConfig): Elysia {
|
|
|
176
176
|
};
|
|
177
177
|
}),
|
|
178
178
|
);
|
|
179
|
-
})
|
|
179
|
+
});
|
|
180
180
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// metadata and their args are redacted at the source, the same audience
|
|
9
9
|
// that already reads request/response bodies on Usage.
|
|
10
10
|
|
|
11
|
-
import { Elysia, t } from "elysia";
|
|
11
|
+
import { Elysia, t, type AnyElysia } from "elysia";
|
|
12
12
|
import { ConsoleDomainError, errorResponse, requireScope } from "../shared/errors";
|
|
13
13
|
import { parseQueryLimit } from "../shared/query";
|
|
14
14
|
import type { ConsoleAccessResolver } from "../auth/access";
|
|
@@ -37,7 +37,7 @@ const logsQuery = t.Object({
|
|
|
37
37
|
limit: t.Optional(t.String()),
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
export function createLogRoutes(config: LogsConfig):
|
|
40
|
+
export function createLogRoutes(config: LogsConfig): AnyElysia {
|
|
41
41
|
const readLogs = (access: AccessDecision | undefined, level?: string, limitRaw?: string) => {
|
|
42
42
|
requireScope(access, "dashboard:read");
|
|
43
43
|
if (level !== undefined && !isLogLevel(level)) {
|
|
@@ -83,5 +83,5 @@ export function createLogRoutes(config: LogsConfig): Elysia {
|
|
|
83
83
|
});
|
|
84
84
|
}),
|
|
85
85
|
);
|
|
86
|
-
})
|
|
86
|
+
});
|
|
87
87
|
}
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
// this instance's own module graph and egress), so the route is guarded by
|
|
6
6
|
// `platform:admin` like the other instance-level operations.
|
|
7
7
|
|
|
8
|
-
import { Elysia } from "elysia";
|
|
8
|
+
import { Elysia, type AnyElysia } from "elysia";
|
|
9
9
|
import { errorResponse, requireGlobalAdmin } from "../shared/errors";
|
|
10
10
|
import { performanceMetricsSnapshot } from "../../observability/performance-metrics";
|
|
11
11
|
import type { PerformanceConfig } from "./performance-contracts";
|
|
12
12
|
|
|
13
|
-
export function createPerformanceRoutes(config: PerformanceConfig):
|
|
13
|
+
export function createPerformanceRoutes(config: PerformanceConfig): AnyElysia {
|
|
14
14
|
return new Elysia()
|
|
15
15
|
.get("/system/performance", ({ request, set }) => {
|
|
16
16
|
try {
|
|
@@ -19,5 +19,5 @@ export function createPerformanceRoutes(config: PerformanceConfig): Elysia {
|
|
|
19
19
|
} catch (e) {
|
|
20
20
|
return errorResponse(e, set, "Performance operation failed");
|
|
21
21
|
}
|
|
22
|
-
})
|
|
22
|
+
});
|
|
23
23
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Elysia, t } from "elysia";
|
|
1
|
+
import { Elysia, t, type AnyElysia } from "elysia";
|
|
2
2
|
import { ConsoleDomainError, consoleErrorHandler } from "../shared/errors";
|
|
3
3
|
import { USAGE_DIMENSIONS, type UsageDimension } from "./usage-dimensions";
|
|
4
4
|
import { createObservabilityOperations, type ObservabilityConfig } from "./contracts";
|
|
@@ -18,7 +18,7 @@ const telemetryDetailQuery = t.Object({
|
|
|
18
18
|
includePayload: t.Optional(t.String()),
|
|
19
19
|
createdAt: t.Optional(t.String()),
|
|
20
20
|
});
|
|
21
|
-
export function createObservabilityRoutes(config: ObservabilityConfig):
|
|
21
|
+
export function createObservabilityRoutes(config: ObservabilityConfig): AnyElysia {
|
|
22
22
|
const factory = createObservabilityOperations(config);
|
|
23
23
|
return new Elysia()
|
|
24
24
|
.error(consoleErrorHandler("Observability operation failed"))
|
|
@@ -114,5 +114,5 @@ export function createObservabilityRoutes(config: ObservabilityConfig): Elysia {
|
|
|
114
114
|
query.createdAt,
|
|
115
115
|
);
|
|
116
116
|
},
|
|
117
|
-
)
|
|
117
|
+
);
|
|
118
118
|
}
|
|
@@ -7,11 +7,8 @@ import { type TelemetryEventRecord } from "../../observability/event-log";
|
|
|
7
7
|
import {
|
|
8
8
|
breakdownTelemetry,
|
|
9
9
|
chartTelemetry,
|
|
10
|
-
healthTelemetry,
|
|
11
10
|
queryTelemetryEvents as queryTelemetryEventPage,
|
|
12
|
-
|
|
13
|
-
summarizeUsagePerformanceSql,
|
|
14
|
-
summarizeTelemetry,
|
|
11
|
+
summarizeUsageWithPerformance,
|
|
15
12
|
} from "../../observability/event-query";
|
|
16
13
|
import { clientNameFromUserAgent } from "../../observability/client-name";
|
|
17
14
|
|
|
@@ -145,6 +142,7 @@ function mapUsageRequestItem(
|
|
|
145
142
|
: {}),
|
|
146
143
|
...(event.endpoint ? { endpoint: event.endpoint } : {}),
|
|
147
144
|
...(event.apiKeyId ? { apiKeyId: event.apiKeyId } : {}),
|
|
145
|
+
...(event.networkPoolId ? { networkPoolId: event.networkPoolId } : {}),
|
|
148
146
|
...(event.userAgent ? { userAgent: event.userAgent } : {}),
|
|
149
147
|
...(event.userAgent ? { clientName: clientNameFromUserAgent(event.userAgent) } : {}),
|
|
150
148
|
...(event.clientIp
|
|
@@ -326,11 +324,10 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
326
324
|
// cost on the dashboard. `healthTelemetry` returns the totals and the
|
|
327
325
|
// database computes the percentiles.
|
|
328
326
|
const since = new Date(Date.now() - HEALTH_WINDOW_MS);
|
|
329
|
-
const totals = await
|
|
330
|
-
const performance = await summarizeHealthPerformance(this.db, tenantId, since);
|
|
327
|
+
const { summary: totals, performance } = await summarizeUsageWithPerformance(this.db, tenantId, since);
|
|
331
328
|
total = totals.requests;
|
|
332
329
|
errors = totals.errors;
|
|
333
|
-
avg =
|
|
330
|
+
avg = Math.round(performance.avgDurationMs ?? 0);
|
|
334
331
|
p95 = performance.latencyP95Ms ?? 0;
|
|
335
332
|
p99 = performance.latencyP99Ms ?? 0;
|
|
336
333
|
const inputTokens = totals.inputTokens;
|
|
@@ -417,8 +414,8 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
417
414
|
// Totals AND the top-model/top-provider counts come from SQL: the old
|
|
418
415
|
// shape parsed the whole retained window into an array to count two
|
|
419
416
|
// dimensions in JavaScript.
|
|
420
|
-
const [summary, models, providers] = await Promise.all([
|
|
421
|
-
|
|
417
|
+
const [{ summary }, models, providers] = await Promise.all([
|
|
418
|
+
summarizeUsageWithPerformance(this.db, tenantId, since),
|
|
422
419
|
breakdownTelemetry(this.db, tenantId, "model", since),
|
|
423
420
|
breakdownTelemetry(this.db, tenantId, "provider", since),
|
|
424
421
|
]);
|
|
@@ -468,12 +465,11 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
468
465
|
private async computeSummary(tenantId: string, period: string): Promise<UsageSummaryResponse> {
|
|
469
466
|
const since = this.periodSince(period);
|
|
470
467
|
// Totals, status breakdown, and latency percentiles all computed in SQL.
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
const [summary,
|
|
474
|
-
|
|
468
|
+
// The summary totals and performance figures come from ONE combined window
|
|
469
|
+
// scan (one percentile pass), not two independent scans.
|
|
470
|
+
const [{ summary, performance }, statuses] = await Promise.all([
|
|
471
|
+
summarizeUsageWithPerformance(this.db, tenantId, since),
|
|
475
472
|
breakdownTelemetry(this.db, tenantId, "status", since),
|
|
476
|
-
summarizeUsagePerformanceSql(this.db, tenantId, since),
|
|
477
473
|
]);
|
|
478
474
|
const inputTokens = summary.inputTokens;
|
|
479
475
|
const cachedTokens = summary.cachedInputTokens;
|
|
@@ -655,7 +651,7 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
655
651
|
}
|
|
656
652
|
|
|
657
653
|
private async computeCache(tenantId: string, period: string): Promise<UsageCacheResponse> {
|
|
658
|
-
const summary = await
|
|
654
|
+
const { summary } = await summarizeUsageWithPerformance(this.db, tenantId, this.periodSince(period));
|
|
659
655
|
const inputTokens = summary.inputTokens;
|
|
660
656
|
const outputTokens = summary.outputTokens;
|
|
661
657
|
const cachedTokens = summary.cachedInputTokens;
|
|
@@ -766,7 +762,11 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
|
|
|
766
762
|
const item = mapUsageRequestItem(event, hideClientIp);
|
|
767
763
|
const keyLabel = event.apiKeyId ? keyLabels?.get(event.apiKeyId) : undefined;
|
|
768
764
|
const labeled = keyLabel === undefined ? item : { ...item, apiKeyLabel: keyLabel };
|
|
769
|
-
const presented = {
|
|
765
|
+
const presented = {
|
|
766
|
+
...labeled,
|
|
767
|
+
proxy,
|
|
768
|
+
...(event.networkPoolId ? { networkPoolLabel: proxy } : {}),
|
|
769
|
+
};
|
|
770
770
|
// Filter the payload table directly instead of joining `telemetry_events`
|
|
771
771
|
// only to re-apply the tenant predicate: `telemetry_payloads` carries
|
|
772
772
|
// `tenant_id` itself, so the join added a second table's rows to filter for
|