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
|
@@ -17,6 +17,10 @@ import { type TelemetryEventRecord } from "../../observability/event-log";
|
|
|
17
17
|
import {
|
|
18
18
|
queryTelemetryEvents as querySqlEvents,
|
|
19
19
|
sumTelemetryTokens,
|
|
20
|
+
familyKpiTotals,
|
|
21
|
+
familyWindowRequests,
|
|
22
|
+
familyHourlyTelemetry,
|
|
23
|
+
familyIpBreakdown,
|
|
20
24
|
} from "../../observability/event-query";
|
|
21
25
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
22
26
|
import { apiKeys } from "../../persistence/schema";
|
|
@@ -72,6 +76,12 @@ export interface ShareTopClientIp {
|
|
|
72
76
|
* the tool rather than the machine.
|
|
73
77
|
*/
|
|
74
78
|
readonly clientType: string | null;
|
|
79
|
+
/**
|
|
80
|
+
* True when this address made a request within {@link ACTIVE_IP_WINDOW_MS}.
|
|
81
|
+
* A recipient watching the link can tell which addresses are still using it
|
|
82
|
+
* versus ones that stopped days ago.
|
|
83
|
+
*/
|
|
84
|
+
readonly active: boolean;
|
|
75
85
|
}
|
|
76
86
|
|
|
77
87
|
export interface ShareFamilyStats {
|
|
@@ -111,6 +121,24 @@ export interface ShareStatsPort {
|
|
|
111
121
|
|
|
112
122
|
const HOUR_MS = 3_600_000;
|
|
113
123
|
|
|
124
|
+
/**
|
|
125
|
+
* How old an address's last request may be and still count as active on the
|
|
126
|
+
* share page. 24 hours matches the page's own "last 24 hours" framing: an IP
|
|
127
|
+
* that made a request since yesterday is "active", one that stopped before
|
|
128
|
+
* that window is "inactive".
|
|
129
|
+
*/
|
|
130
|
+
const ACTIVE_IP_WINDOW_MS = 24 * HOUR_MS;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Recency window for the client-IP ranking's *work set*: the GROUP BY only
|
|
134
|
+
* scans this slice of telemetry instead of the whole retained window every
|
|
135
|
+
* tick. Within the window, addresses are ranked by request volume (highest
|
|
136
|
+
* first), and older addresses fall out of the ranking entirely. 7 days keeps
|
|
137
|
+
* the ranking useful while staying orders of magnitude smaller than the
|
|
138
|
+
* retained window.
|
|
139
|
+
*/
|
|
140
|
+
const IP_WINDOW_MS = 7 * 24 * HOUR_MS;
|
|
141
|
+
|
|
114
142
|
/**
|
|
115
143
|
* Names the client behind a request from its User-Agent.
|
|
116
144
|
*
|
|
@@ -154,7 +182,7 @@ interface FamilyRollup {
|
|
|
154
182
|
totals: ShareFamilyTotals;
|
|
155
183
|
byHour: Map<string, { requests: number; tokens: number }>;
|
|
156
184
|
models: Map<string, { modelId: string; requests: number; tokens: number; rateSum: number; rateCount: number; ttfbSum: number; ttfbCount: number }>;
|
|
157
|
-
ips: Map<string, { ip: string; requests: number; tokens: number; lastSeenAt: string | null; userAgent: string | null }>;
|
|
185
|
+
ips: Map<string, { ip: string; requests: number; tokens: number; lastSeenAt: string | null; userAgent: string | null; active: boolean }>;
|
|
158
186
|
}
|
|
159
187
|
|
|
160
188
|
/**
|
|
@@ -163,7 +191,7 @@ interface FamilyRollup {
|
|
|
163
191
|
*/
|
|
164
192
|
function rollupFamily(
|
|
165
193
|
events: readonly TelemetryEventRecord[],
|
|
166
|
-
bounds: { dayStart: number; monthStart: number; lastHourStart: number; windowStart: number; allowed: ReadonlySet<string> | null },
|
|
194
|
+
bounds: { dayStart: number; monthStart: number; lastHourStart: number; windowStart: number; activeIpCutoff: number; allowed: ReadonlySet<string> | null },
|
|
167
195
|
): FamilyRollup {
|
|
168
196
|
const totals = { requests: 0, errors: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, lastHourRequests: 0, todayTokens: 0, monthTokens: 0 };
|
|
169
197
|
const byHour = new Map<string, { requests: number; tokens: number }>();
|
|
@@ -196,13 +224,14 @@ function rollupFamily(
|
|
|
196
224
|
models.set(event.requestedModel, model);
|
|
197
225
|
}
|
|
198
226
|
if (event.clientIp !== null) {
|
|
199
|
-
const ip = ips.get(event.clientIp) ?? { ip: event.clientIp, requests: 0, tokens: 0, lastSeenAt: null, userAgent: null };
|
|
227
|
+
const ip = ips.get(event.clientIp) ?? { ip: event.clientIp, requests: 0, tokens: 0, lastSeenAt: null, userAgent: null, active: false };
|
|
200
228
|
ip.requests += 1;
|
|
201
229
|
ip.tokens += tokens;
|
|
202
230
|
// Events arrive newest first, so the first one seen is the most recent.
|
|
203
231
|
if (ip.lastSeenAt === null) {
|
|
204
232
|
ip.lastSeenAt = event.createdAt;
|
|
205
233
|
ip.userAgent = event.userAgent;
|
|
234
|
+
ip.active = at >= bounds.activeIpCutoff;
|
|
206
235
|
}
|
|
207
236
|
ips.set(event.clientIp, ip);
|
|
208
237
|
}
|
|
@@ -244,17 +273,18 @@ const HOURS_WINDOW = 24;
|
|
|
244
273
|
/**
|
|
245
274
|
* Cache lifetime for one family's rollup.
|
|
246
275
|
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
276
|
+
* Longer than the SSE tick (30s) so most ticks are served from cache. The
|
|
277
|
+
* cold-compute path reads the family's telemetry windows (24h chart, 7d IP
|
|
278
|
+
* ranking), which on a busy family is the single most expensive thing the
|
|
279
|
+
* share page does; a 5-minute TTL caps that to once per family per 5
|
|
280
|
+
* minutes while the page still reads as live — KPI numbers move with
|
|
281
|
+
* traffic, not on a per-minute cadence.
|
|
252
282
|
*
|
|
253
283
|
* Declared here rather than imported from the router so this port does not
|
|
254
|
-
* depend on it. `SHARE_STATS_STREAM_INTERVAL_MS` must
|
|
255
|
-
*
|
|
284
|
+
* depend on it. `SHARE_STATS_STREAM_INTERVAL_MS` must stay below it, or the
|
|
285
|
+
* tick and cache drift apart and every tick bypasses the cache.
|
|
256
286
|
*/
|
|
257
|
-
export const SHARE_STATS_CACHE_TTL_SECONDS =
|
|
287
|
+
export const SHARE_STATS_CACHE_TTL_SECONDS = 300;
|
|
258
288
|
|
|
259
289
|
/**
|
|
260
290
|
* Cache key for one family's rollup. The family is named by its template key
|
|
@@ -318,17 +348,23 @@ async function computeFamilyStatsBody(
|
|
|
318
348
|
const hourStart = utcHourStart(now);
|
|
319
349
|
const lastHourStart = new Date(now.getTime() - HOUR_MS);
|
|
320
350
|
const windowStart = new Date(hourStart.getTime() - (HOURS_WINDOW - 1) * HOUR_MS);
|
|
351
|
+
// The IP breakdown is bounded to this recency window: a family's "who is
|
|
352
|
+
// using the link" answer only needs addresses that were active recently,
|
|
353
|
+
// and an unbounded GROUP BY over every retained event is the one remaining
|
|
354
|
+
// full-window scan on the share page.
|
|
355
|
+
const ipWindowStart = new Date(now.getTime() - IP_WINDOW_MS);
|
|
321
356
|
|
|
322
|
-
// All-time
|
|
323
|
-
//
|
|
324
|
-
// the
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
const [
|
|
329
|
-
|
|
357
|
+
// All-time family KPI comes from the durable rollup
|
|
358
|
+
// (`telemetry_usage_totals`, charged at write time), not from re-aggregating
|
|
359
|
+
// the entire retained telemetry window on every tick: the share page's tick
|
|
360
|
+
// would otherwise read every event a busy family ever produced just to draw
|
|
361
|
+
// four numbers. Today's/month's spend and the last-hour count are bounded
|
|
362
|
+
// window reads over the time index, which stay small.
|
|
363
|
+
const [kpi, todayTokens, monthTokens, lastHourRequests, template] = await Promise.all([
|
|
364
|
+
familyKpiTotals(db, tenantId, templateKeyId),
|
|
330
365
|
sumTelemetryTokens(db, tenantId, { familyKeyId: templateKeyId, since: dayStart }),
|
|
331
366
|
sumTelemetryTokens(db, tenantId, { familyKeyId: templateKeyId, since: monthStart }),
|
|
367
|
+
familyWindowRequests(db, tenantId, templateKeyId, lastHourStart),
|
|
332
368
|
db
|
|
333
369
|
.select({ lifetime: apiKeys.lifetimeTokensConsumed })
|
|
334
370
|
.from(apiKeys)
|
|
@@ -342,8 +378,9 @@ async function computeFamilyStatsBody(
|
|
|
342
378
|
// across the overview, the share dialog, and this page.
|
|
343
379
|
const templateLifetime = Number(template[0]?.lifetime ?? 0);
|
|
344
380
|
|
|
345
|
-
// The
|
|
346
|
-
//
|
|
381
|
+
// The ranked tables need real events, bounded to the 24-hour window. The
|
|
382
|
+
// totals and hourly chart are computed in SQL, so the page cap here only
|
|
383
|
+
// shapes the model/IP rankings — never the KPI numbers.
|
|
347
384
|
const page = await querySqlEvents(db, {
|
|
348
385
|
tenantId,
|
|
349
386
|
familyApiKeyId: templateKeyId,
|
|
@@ -356,6 +393,7 @@ async function computeFamilyStatsBody(
|
|
|
356
393
|
monthStart: monthStart.getTime(),
|
|
357
394
|
lastHourStart: lastHourStart.getTime(),
|
|
358
395
|
windowStart: windowStart.getTime(),
|
|
396
|
+
activeIpCutoff: now.getTime() - ACTIVE_IP_WINDOW_MS,
|
|
359
397
|
allowed: allowedSet,
|
|
360
398
|
});
|
|
361
399
|
rollup.totals = {
|
|
@@ -363,14 +401,28 @@ async function computeFamilyStatsBody(
|
|
|
363
401
|
// The template counter is the seed on the running total, not a distinct
|
|
364
402
|
// bucket: daily/monthly windows are telemetry-only (the counter has no
|
|
365
403
|
// time axis), so those keep their own sums.
|
|
366
|
-
totalTokens: templateLifetime +
|
|
404
|
+
totalTokens: templateLifetime + kpi.tokens,
|
|
367
405
|
todayTokens,
|
|
368
406
|
monthTokens,
|
|
407
|
+
// Lifetime counts come from the durable rollup, not the page: a family
|
|
408
|
+
// busier than the page cap must not freeze at 500 requests/errors.
|
|
409
|
+
requests: kpi.requests,
|
|
410
|
+
errors: kpi.errors,
|
|
411
|
+
lastHourRequests,
|
|
369
412
|
};
|
|
370
413
|
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
|
|
414
|
+
// The hourly chart is a SQL aggregate over the whole family window, so a
|
|
415
|
+
// busy family's bars stay true. Buckets are filled in JS because only hours
|
|
416
|
+
// that saw traffic are present; a chart drawn from sparse rows would
|
|
417
|
+
// silently compress the axis.
|
|
418
|
+
const bucketSeconds = HOUR_MS / 1000;
|
|
419
|
+
const sqlHourly = await familyHourlyTelemetry(db, tenantId, templateKeyId, windowStart, bucketSeconds);
|
|
420
|
+
const byHour = new Map(
|
|
421
|
+
sqlHourly.map((bucket) => [
|
|
422
|
+
new Date(bucket.at).toISOString(),
|
|
423
|
+
{ requests: bucket.requests, tokens: bucket.inputTokens + bucket.outputTokens },
|
|
424
|
+
]),
|
|
425
|
+
);
|
|
374
426
|
const hourly: ShareHourlyBucket[] = [];
|
|
375
427
|
for (let index = 0; index < HOURS_WINDOW; index += 1) {
|
|
376
428
|
const hour = new Date(windowStart.getTime() + index * HOUR_MS).toISOString();
|
|
@@ -378,6 +430,33 @@ async function computeFamilyStatsBody(
|
|
|
378
430
|
hourly.push({ hour, requests: bucket?.requests ?? 0, tokens: bucket?.tokens ?? 0 });
|
|
379
431
|
}
|
|
380
432
|
|
|
433
|
+
// Client IPs come from a SQL aggregate over a bounded recent window, ranked
|
|
434
|
+
// by request volume — so a family's "who is using the link most" answer
|
|
435
|
+
// stays cheap instead of scanning the whole retained telemetry on every
|
|
436
|
+
// tick. `clientType` is best-effort from the page's events (the most recent
|
|
437
|
+
// 24 hours), which carry the User-Agent.
|
|
438
|
+
const ipRows = await familyIpBreakdown(db, tenantId, templateKeyId, TOP_IPS_LIMIT, ipWindowStart);
|
|
439
|
+
const clientTypeByIp = new Map<string, string | null>();
|
|
440
|
+
for (const event of events) {
|
|
441
|
+
if (event.clientIp !== null && !clientTypeByIp.has(event.clientIp)) {
|
|
442
|
+
clientTypeByIp.set(event.clientIp, clientTypeFromUserAgent(event.userAgent));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const activeIpCutoff = now.getTime() - ACTIVE_IP_WINDOW_MS;
|
|
446
|
+
const clientIps = ipRows.map((row) => {
|
|
447
|
+
const lastSeenAt =
|
|
448
|
+
row.lastSeenAt instanceof Date ? row.lastSeenAt : new Date(String(row.lastSeenAt ?? ""));
|
|
449
|
+
const lastSeenMs = Number.isFinite(lastSeenAt.getTime()) ? lastSeenAt.getTime() : 0;
|
|
450
|
+
return {
|
|
451
|
+
ip: maskClientIp(row.ip),
|
|
452
|
+
requests: row.requests,
|
|
453
|
+
tokens: row.tokens,
|
|
454
|
+
lastSeenAt: lastSeenMs > 0 ? lastSeenAt.toISOString() : null,
|
|
455
|
+
clientType: clientTypeByIp.get(row.ip) ?? null,
|
|
456
|
+
active: lastSeenMs >= activeIpCutoff,
|
|
457
|
+
};
|
|
458
|
+
});
|
|
459
|
+
|
|
381
460
|
return {
|
|
382
461
|
totals: rollup.totals,
|
|
383
462
|
hourly,
|
|
@@ -391,15 +470,6 @@ async function computeFamilyStatsBody(
|
|
|
391
470
|
avgTokensPerSec: row.rateCount > 0 ? row.rateSum / row.rateCount : null,
|
|
392
471
|
avgTtfbMs: row.ttfbCount > 0 ? Math.round(row.ttfbSum / row.ttfbCount) : null,
|
|
393
472
|
})),
|
|
394
|
-
clientIps
|
|
395
|
-
.sort((a, b) => b.requests - a.requests)
|
|
396
|
-
.slice(0, TOP_IPS_LIMIT)
|
|
397
|
-
.map((row) => ({
|
|
398
|
-
ip: maskClientIp(row.ip),
|
|
399
|
-
requests: row.requests,
|
|
400
|
-
tokens: row.tokens,
|
|
401
|
-
lastSeenAt: row.lastSeenAt,
|
|
402
|
-
clientType: clientTypeFromUserAgent(row.userAgent),
|
|
403
|
-
})),
|
|
473
|
+
clientIps,
|
|
404
474
|
};
|
|
405
475
|
}
|
|
@@ -63,6 +63,34 @@ export function etagKey(key: string): string {
|
|
|
63
63
|
return `${key}:etag`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* In-memory fallback store used when no Redis client is configured (the
|
|
68
|
+
* embedded lite backend). Keyed by the same cache key, storing the value and
|
|
69
|
+
* its validator with a TTL. Bounded so a busy gateway cannot grow it without
|
|
70
|
+
* limit.
|
|
71
|
+
*/
|
|
72
|
+
const LOCAL_CACHE_MAX_ENTRIES = 2000;
|
|
73
|
+
const localCache = new Map<string, { at: number; value: string; etag: string }>();
|
|
74
|
+
|
|
75
|
+
function localGet(key: string): { value: string; etag: string } | undefined {
|
|
76
|
+
const entry = localCache.get(key);
|
|
77
|
+
if (entry === undefined) return undefined;
|
|
78
|
+
if (Date.now() - entry.at >= READ_THROUGH_DEFAULT_TTL_SECONDS * 1000) {
|
|
79
|
+
localCache.delete(key);
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return { value: entry.value, etag: entry.etag };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function localSet(key: string, value: string, etag: string): void {
|
|
86
|
+
if (localCache.size >= LOCAL_CACHE_MAX_ENTRIES) {
|
|
87
|
+
// Evict the oldest entry (Map iterates in insertion order).
|
|
88
|
+
const oldest = localCache.keys().next();
|
|
89
|
+
if (!oldest.done) localCache.delete(oldest.value);
|
|
90
|
+
}
|
|
91
|
+
localCache.set(key, { at: Date.now(), value, etag });
|
|
92
|
+
}
|
|
93
|
+
|
|
66
94
|
/**
|
|
67
95
|
* Reads through the cache and returns a strong validator alongside the value.
|
|
68
96
|
*
|
|
@@ -74,7 +102,8 @@ export function etagKey(key: string): string {
|
|
|
74
102
|
* With no cache configured (`client === undefined`) the value is computed and
|
|
75
103
|
* the validator is still returned, so conditional requests keep working for
|
|
76
104
|
* the length of the client's own polling loop; only the cross-instance sharing
|
|
77
|
-
* is lost.
|
|
105
|
+
* is lost. A process-local Map serves the same TTL when Redis is absent, so a
|
|
106
|
+
* poller still gets cache hits.
|
|
78
107
|
*/
|
|
79
108
|
export async function cachedReadWithEtag<T>(
|
|
80
109
|
client: ReadThroughCacheClient | undefined,
|
|
@@ -83,8 +112,15 @@ export async function cachedReadWithEtag<T>(
|
|
|
83
112
|
ttlSeconds: number = READ_THROUGH_DEFAULT_TTL_SECONDS,
|
|
84
113
|
): Promise<CachedResult<T>> {
|
|
85
114
|
if (client === undefined) {
|
|
115
|
+
const entry = localGet(key);
|
|
116
|
+
if (entry !== undefined) {
|
|
117
|
+
return { value: JSON.parse(entry.value) as T, etag: entry.etag };
|
|
118
|
+
}
|
|
86
119
|
const fresh = await compute();
|
|
87
|
-
|
|
120
|
+
const bytes = JSON.stringify(fresh) ?? "null";
|
|
121
|
+
const etag = etagFor(bytes);
|
|
122
|
+
localSet(key, bytes, etag);
|
|
123
|
+
return { value: fresh, etag };
|
|
88
124
|
}
|
|
89
125
|
try {
|
|
90
126
|
const hit = await client.get(key);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Elysia } from "elysia";
|
|
1
|
+
import { Elysia, type AnyElysia } from "elysia";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { consoleErrorHandler, requireTenantScope } from "../shared/errors";
|
|
4
4
|
import { consoleSseResponse, createConsoleSseStream } from "../observability/sse";
|
|
@@ -7,7 +7,10 @@ import type { ConsoleAccessResolver } from "../auth/access";
|
|
|
7
7
|
import { fetchLatestRelease, type LatestRelease } from "./repository-updates";
|
|
8
8
|
import { isNewerRelease } from "./version-compare";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// Memory is sampled per visible tab: ten seconds per stream made a console
|
|
11
|
+
// with many open tabs wake up constantly for numbers nobody is reading. 30 s
|
|
12
|
+
// is still responsive for a memory warning while cutting that work by three.
|
|
13
|
+
const MEMORY_SAMPLE_INTERVAL_MS = 30_000;
|
|
11
14
|
const UPDATE_SAMPLE_INTERVAL_MS = 60_000;
|
|
12
15
|
const HIGH_MEMORY_PERCENT = 85;
|
|
13
16
|
const HIGH_MEMORY_CLEAR_PERCENT = 80;
|
|
@@ -94,7 +97,7 @@ export function createUpdateRoutes(config: {
|
|
|
94
97
|
readonly accessResolver: ConsoleAccessResolver;
|
|
95
98
|
readonly observabilityStore: ObservabilityStore;
|
|
96
99
|
readonly fetchRelease?: () => Promise<LatestRelease | null>;
|
|
97
|
-
}):
|
|
100
|
+
}): AnyElysia {
|
|
98
101
|
const fetchRelease = config.fetchRelease ?? fetchLatestRelease;
|
|
99
102
|
return new Elysia()
|
|
100
103
|
.error(consoleErrorHandler("Update check failed"))
|
|
@@ -199,5 +202,5 @@ export function createUpdateRoutes(config: {
|
|
|
199
202
|
};
|
|
200
203
|
}),
|
|
201
204
|
);
|
|
202
|
-
})
|
|
205
|
+
});
|
|
203
206
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Validated fetch: SSRF-guarded outbound fetch with optional pool-agent support.
|
|
2
2
|
import { GatewayError } from "../protocol/gateway-error";
|
|
3
|
-
import { Agent as HttpAgent } from "node:http";
|
|
4
|
-
import { Agent as HttpsAgent } from "node:https";
|
|
3
|
+
import { request as httpRequest, Agent as HttpAgent } from "node:http";
|
|
4
|
+
import { request as httpsRequest, Agent as HttpsAgent } from "node:https";
|
|
5
5
|
import { Agent as UndiciAgent, fetch as undiciFetch, type UndiciRequestInit } from "undici";
|
|
6
6
|
import { SocksProxyAgent } from "socks-proxy-agent";
|
|
7
7
|
import { resolveAllAddresses, validateResolvedAddresses } from "./ssrf";
|
|
@@ -167,6 +167,66 @@ async function dial(
|
|
|
167
167
|
});
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* One raw GET/HEAD dial that carries a body.
|
|
172
|
+
*
|
|
173
|
+
* The runtime-native fetch (`Bun`/undici) rejects `GET`/`HEAD` requests with a
|
|
174
|
+
* body — "request with GET/HEAD method cannot have body" — but some upstreams
|
|
175
|
+
* validate a signature over the body on a GET (Qoder's `/model/list` does).
|
|
176
|
+
* This is the Node `http`/`https` path for exactly that case. It runs only
|
|
177
|
+
* after the SSRF resolve/validate pass and only when no pool/relay agent owns
|
|
178
|
+
* the dial, so the security boundary is unchanged.
|
|
179
|
+
*/
|
|
180
|
+
function nodeDial(url: URL, init: RequestInit): Promise<Response> {
|
|
181
|
+
return new Promise((resolve, reject) => {
|
|
182
|
+
const mod = url.protocol === "http:" ? httpRequest : httpsRequest;
|
|
183
|
+
const headers: Record<string, string> = {};
|
|
184
|
+
{
|
|
185
|
+
const h = new Headers(init.headers);
|
|
186
|
+
h.forEach((value, key) => {
|
|
187
|
+
headers[key] = value;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
let payload: Buffer | undefined;
|
|
191
|
+
const body = init.body;
|
|
192
|
+
if (typeof body === "string") {
|
|
193
|
+
payload = Buffer.from(body, "utf8");
|
|
194
|
+
} else if (Buffer.isBuffer(body)) {
|
|
195
|
+
payload = body;
|
|
196
|
+
} else if (body instanceof Uint8Array) {
|
|
197
|
+
payload = Buffer.from(body);
|
|
198
|
+
} else if (body !== null && body !== undefined) {
|
|
199
|
+
reject(new TypeError("nodeDial only supports string/Buffer/Uint8Array bodies"));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (payload !== undefined) headers["content-length"] = String(payload.byteLength);
|
|
203
|
+
const req = mod(
|
|
204
|
+
url,
|
|
205
|
+
{
|
|
206
|
+
method: (init.method ?? "GET").toUpperCase(),
|
|
207
|
+
headers,
|
|
208
|
+
...(init.signal === undefined ? {} : { signal: init.signal as AbortSignal }),
|
|
209
|
+
},
|
|
210
|
+
(res) => {
|
|
211
|
+
const chunks: Buffer[] = [];
|
|
212
|
+
res.on("data", (c: Buffer) => chunks.push(c));
|
|
213
|
+
res.on("end", () => {
|
|
214
|
+
resolve(
|
|
215
|
+
new Response(Buffer.concat(chunks), {
|
|
216
|
+
status: res.statusCode ?? 200,
|
|
217
|
+
...(res.statusMessage === undefined ? {} : { statusText: res.statusMessage }),
|
|
218
|
+
headers: res.headers as unknown as HeadersInit,
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
req.on("error", reject);
|
|
225
|
+
if (payload !== undefined) req.write(payload);
|
|
226
|
+
req.end();
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
170
230
|
export function createValidatedFetch(options: ValidatedFetchOptions = {}): ValidatedFetch {
|
|
171
231
|
const policy = options.policy ?? {};
|
|
172
232
|
const maxRedirects = options.maxRedirects ?? policy.maxRedirects ?? 3;
|
|
@@ -242,7 +302,18 @@ export function createValidatedFetch(options: ValidatedFetchOptions = {}): Valid
|
|
|
242
302
|
// serverless front door refuses (400).
|
|
243
303
|
response = await dial(relay.url, relay.init, fetchFn, undefined);
|
|
244
304
|
} else {
|
|
245
|
-
|
|
305
|
+
const method = (requestInit.method ?? "GET").toUpperCase();
|
|
306
|
+
const hasBody = requestInit.body !== undefined && requestInit.body !== null;
|
|
307
|
+
// The runtime fetch cannot carry a body on GET/HEAD, but Qoder's
|
|
308
|
+
// model-list endpoint requires a signed GET body. When no pool/relay
|
|
309
|
+
// owns the dial and no injected test fetch is present, dial through the
|
|
310
|
+
// Node http(s) client (post SSRF validation). Every other case keeps
|
|
311
|
+
// the existing fetch path.
|
|
312
|
+
if ((method === "GET" || method === "HEAD") && hasBody && !socksAgent && options.fetchFn === undefined) {
|
|
313
|
+
response = await nodeDial(url, requestInit);
|
|
314
|
+
} else {
|
|
315
|
+
response = await dial(url, requestInit, fetchFn, proxyUrl, socksAgent ? socksDispatcherFor(socksAgent) : undefined, socksFetch);
|
|
316
|
+
}
|
|
246
317
|
}
|
|
247
318
|
if (response.status < 300 || response.status >= 400) {
|
|
248
319
|
trackNetworkCall(url.hostname, performance.now() - startedAt);
|
|
@@ -264,7 +335,7 @@ export function createValidatedFetch(options: ValidatedFetchOptions = {}): Valid
|
|
|
264
335
|
if (isCrossOriginRedirect && (response.status === 307 || response.status === 308)) {
|
|
265
336
|
const method = (requestInit.method ?? "GET").toUpperCase();
|
|
266
337
|
if (method !== "GET" && method !== "HEAD" && requestInit.body !== undefined) {
|
|
267
|
-
throw new GatewayError("invalid_request", 400, "cross-origin redirect with body blocked");
|
|
338
|
+
throw new GatewayError("invalid_request", 400, "cross-origin redirect with body blocked (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
268
339
|
}
|
|
269
340
|
}
|
|
270
341
|
url = nextUrl;
|