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
|
@@ -364,21 +364,21 @@ function connectValidatedWebSocket(
|
|
|
364
364
|
if (settled) return;
|
|
365
365
|
settled = true;
|
|
366
366
|
socket.terminate();
|
|
367
|
-
reject(new GatewayError("transport_unavailable", 502, "WebSocket handshake timed out"));
|
|
367
|
+
reject(new GatewayError("transport_unavailable", 502, "WebSocket handshake timed out (request {requestId}). Some things just take longer 🐢"));
|
|
368
368
|
}, 15_000);
|
|
369
369
|
const abort = (): void => {
|
|
370
370
|
if (settled) return;
|
|
371
371
|
settled = true;
|
|
372
372
|
clearTimeout(timeout);
|
|
373
373
|
socket.terminate();
|
|
374
|
-
reject(new GatewayError("transport_closed", 499, "WebSocket connection was cancelled"));
|
|
374
|
+
reject(new GatewayError("transport_closed", 499, "WebSocket connection was cancelled (request {requestId}). Send it again whenever ready 🙃"));
|
|
375
375
|
};
|
|
376
376
|
const onError = (): void => {
|
|
377
377
|
if (settled) return;
|
|
378
378
|
settled = true;
|
|
379
379
|
clearTimeout(timeout);
|
|
380
380
|
signal.removeEventListener("abort", abort);
|
|
381
|
-
reject(new GatewayError("transport_unavailable", 502, "WebSocket connection failed"));
|
|
381
|
+
reject(new GatewayError("transport_unavailable", 502, "WebSocket connection failed (request {requestId}). The connection ghosted us — come back in a moment 👻"));
|
|
382
382
|
};
|
|
383
383
|
socket.once("open", () => {
|
|
384
384
|
if (settled) return;
|
|
@@ -398,7 +398,7 @@ function connectValidatedWebSocket(
|
|
|
398
398
|
function agentForWebSocket(agent: PoolAgent): import("http").Agent | import("https").Agent {
|
|
399
399
|
if (isProxyAgentPair(agent)) return agent.https;
|
|
400
400
|
if (agent instanceof HttpsAgent) return agent;
|
|
401
|
-
throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool does not support WSS");
|
|
401
|
+
throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool does not support WSS (request {requestId}). This bridge doesn't speak WSS — reconfigure the pool 🔧");
|
|
402
402
|
}
|
|
403
403
|
|
|
404
404
|
/**
|
|
@@ -411,20 +411,20 @@ function agentForWebSocket(agent: PoolAgent): import("http").Agent | import("htt
|
|
|
411
411
|
*/
|
|
412
412
|
function assertBidiTarget(url: URL): void {
|
|
413
413
|
if (url.protocol !== "https:") {
|
|
414
|
-
throw new GatewayError("invalid_request", 400, "validated HTTP/2 egress requires https");
|
|
414
|
+
throw new GatewayError("invalid_request", 400, "validated HTTP/2 egress requires https (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
415
415
|
}
|
|
416
416
|
if (url.username || url.password || url.hash) {
|
|
417
|
-
throw new GatewayError("invalid_request", 400, "invalid validated HTTP/2 target");
|
|
417
|
+
throw new GatewayError("invalid_request", 400, "invalid validated HTTP/2 target (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
418
418
|
}
|
|
419
419
|
if (url.port !== "" && url.port !== "443") {
|
|
420
|
-
throw new GatewayError("invalid_request", 400, "validated HTTP/2 egress runs on port 443");
|
|
420
|
+
throw new GatewayError("invalid_request", 400, "validated HTTP/2 egress runs on port 443 (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
421
421
|
}
|
|
422
422
|
const hostname = url.hostname;
|
|
423
423
|
if (hostname.length === 0) {
|
|
424
|
-
throw new GatewayError("invalid_request", 400, "HTTP/2 target hostname is required");
|
|
424
|
+
throw new GatewayError("invalid_request", 400, "HTTP/2 target hostname is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
425
425
|
}
|
|
426
426
|
if (isIP(hostname) !== 0) {
|
|
427
|
-
throw new GatewayError("invalid_request", 400, "HTTP/2 target must be a hostname, not an IP literal");
|
|
427
|
+
throw new GatewayError("invalid_request", 400, "HTTP/2 target must be a hostname, not an IP literal (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
430
|
|
|
@@ -455,13 +455,13 @@ function dialDirect(
|
|
|
455
455
|
const onAbort = (): void => {
|
|
456
456
|
cleanup();
|
|
457
457
|
socket.destroy(new Error("HTTP/2 connection aborted"));
|
|
458
|
-
reject(new GatewayError("transport_closed", 499, "HTTP/2 connection was cancelled"));
|
|
458
|
+
reject(new GatewayError("transport_closed", 499, "HTTP/2 connection was cancelled (request {requestId}). Send it again whenever ready 🙃"));
|
|
459
459
|
};
|
|
460
460
|
const timer = setTimeout(() => {
|
|
461
461
|
cleanup();
|
|
462
462
|
socket.destroy();
|
|
463
463
|
reject(
|
|
464
|
-
new GatewayError("transport_unavailable", 502, "HTTP/2 connection timed out", {}, "network"),
|
|
464
|
+
new GatewayError("transport_unavailable", 502, "HTTP/2 connection timed out (request {requestId}). Some things just take longer 🐢", {}, "network"),
|
|
465
465
|
);
|
|
466
466
|
}, HTTP2_CONNECT_TIMEOUT_MS);
|
|
467
467
|
socket.once("secureConnect", () => {
|
|
@@ -474,7 +474,7 @@ function dialDirect(
|
|
|
474
474
|
new GatewayError(
|
|
475
475
|
"transport_unavailable",
|
|
476
476
|
502,
|
|
477
|
-
`HTTP/2 connection failed: ${error.message}
|
|
477
|
+
`HTTP/2 connection failed: ${error.message} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
|
|
478
478
|
{},
|
|
479
479
|
"network",
|
|
480
480
|
),
|
|
@@ -506,7 +506,7 @@ function dialThroughPool(
|
|
|
506
506
|
throw new GatewayError(
|
|
507
507
|
"proxy_pool_unhealthy",
|
|
508
508
|
503,
|
|
509
|
-
"configured proxy pool does not support HTTPS tunneling",
|
|
509
|
+
"configured proxy pool does not support HTTPS tunneling (request {requestId}). This bridge doesn't speak HTTPS tunnels — reconfigure the pool 🔧",
|
|
510
510
|
);
|
|
511
511
|
}
|
|
512
512
|
return new Promise<tls.TLSSocket>((resolve, reject) => {
|
|
@@ -519,14 +519,14 @@ function dialThroughPool(
|
|
|
519
519
|
? new GatewayError(
|
|
520
520
|
"proxy_pool_unhealthy",
|
|
521
521
|
503,
|
|
522
|
-
"Configured proxy pool could not be established.",
|
|
522
|
+
"Configured proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧",
|
|
523
523
|
{ reason: error.message.slice(0, 200) },
|
|
524
524
|
"network",
|
|
525
525
|
)
|
|
526
526
|
: new GatewayError(
|
|
527
527
|
"transport_unavailable",
|
|
528
528
|
502,
|
|
529
|
-
`HTTP/2 tunnel failed: ${error?.message ?? "no socket"}
|
|
529
|
+
`HTTP/2 tunnel failed: ${error?.message ?? "no socket"} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
|
|
530
530
|
{},
|
|
531
531
|
"network",
|
|
532
532
|
),
|
|
@@ -546,13 +546,13 @@ function dialThroughPool(
|
|
|
546
546
|
const onAbort = (): void => {
|
|
547
547
|
cleanup();
|
|
548
548
|
secured.destroy();
|
|
549
|
-
reject(new GatewayError("transport_closed", 499, "HTTP/2 connection was cancelled"));
|
|
549
|
+
reject(new GatewayError("transport_closed", 499, "HTTP/2 connection was cancelled (request {requestId}). Send it again whenever ready 🙃"));
|
|
550
550
|
};
|
|
551
551
|
const timer = setTimeout(() => {
|
|
552
552
|
cleanup();
|
|
553
553
|
secured.destroy();
|
|
554
554
|
reject(
|
|
555
|
-
new GatewayError("transport_unavailable", 502, "HTTP/2 connection timed out", {}, "network"),
|
|
555
|
+
new GatewayError("transport_unavailable", 502, "HTTP/2 connection timed out (request {requestId}). Some things just take longer 🐢", {}, "network"),
|
|
556
556
|
);
|
|
557
557
|
}, HTTP2_CONNECT_TIMEOUT_MS);
|
|
558
558
|
secured.once("secureConnect", () => {
|
|
@@ -565,7 +565,7 @@ function dialThroughPool(
|
|
|
565
565
|
new GatewayError(
|
|
566
566
|
"transport_unavailable",
|
|
567
567
|
502,
|
|
568
|
-
`HTTP/2 TLS handshake failed: ${tlsError.message}
|
|
568
|
+
`HTTP/2 TLS handshake failed: ${tlsError.message} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
|
|
569
569
|
{},
|
|
570
570
|
"network",
|
|
571
571
|
),
|
|
@@ -636,7 +636,7 @@ class Http2BidiSession implements ValidatedHttp2BidiStream {
|
|
|
636
636
|
new GatewayError(
|
|
637
637
|
"platform_unavailable",
|
|
638
638
|
502,
|
|
639
|
-
"HTTP/2 response exceeded the buffer bound",
|
|
639
|
+
"HTTP/2 response exceeded the buffer bound (request {requestId}). The upstream sent more than we can hold — retry 🌙",
|
|
640
640
|
{},
|
|
641
641
|
"network",
|
|
642
642
|
),
|
|
@@ -646,7 +646,7 @@ class Http2BidiSession implements ValidatedHttp2BidiStream {
|
|
|
646
646
|
stream.on("end", () => this.finishInbound());
|
|
647
647
|
stream.on("error", (error) => this.fail(error));
|
|
648
648
|
stream.on("aborted", () =>
|
|
649
|
-
this.fail(new GatewayError("transport_unavailable", 502, "HTTP/2 stream aborted by remote", {}, "network")),
|
|
649
|
+
this.fail(new GatewayError("transport_unavailable", 502, "HTTP/2 stream aborted by remote (request {requestId}). The connection ghosted us — come back in a moment 👻", {}, "network")),
|
|
650
650
|
);
|
|
651
651
|
stream.on("close", () => {
|
|
652
652
|
this.finishInbound();
|
|
@@ -699,7 +699,7 @@ class Http2BidiSession implements ValidatedHttp2BidiStream {
|
|
|
699
699
|
new GatewayError(
|
|
700
700
|
"transport_unavailable",
|
|
701
701
|
504,
|
|
702
|
-
`HTTP/2 stream read-idle timeout after ${this.readIdleMs}ms — connection presumed dead
|
|
702
|
+
`HTTP/2 stream read-idle timeout after ${this.readIdleMs}ms — connection presumed dead (request {requestId}). The connection ghosted us — come back in a moment 👻`,
|
|
703
703
|
{},
|
|
704
704
|
"network",
|
|
705
705
|
),
|
|
@@ -836,7 +836,7 @@ function openBidiStream(
|
|
|
836
836
|
new GatewayError(
|
|
837
837
|
"transport_unavailable",
|
|
838
838
|
502,
|
|
839
|
-
`HTTP/2 session failed: ${error.message}
|
|
839
|
+
`HTTP/2 session failed: ${error.message} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
|
|
840
840
|
{},
|
|
841
841
|
"network",
|
|
842
842
|
),
|
|
@@ -871,7 +871,7 @@ function openBidiStream(
|
|
|
871
871
|
// Already destroying.
|
|
872
872
|
}
|
|
873
873
|
reject(
|
|
874
|
-
new GatewayError("transport_unavailable", 502, "HTTP/2 session timed out", {}, "network"),
|
|
874
|
+
new GatewayError("transport_unavailable", 502, "HTTP/2 session timed out (request {requestId}). Some things just take longer 🐢", {}, "network"),
|
|
875
875
|
);
|
|
876
876
|
}, HTTP2_CONNECT_TIMEOUT_MS);
|
|
877
877
|
session.once("error", onError);
|
|
@@ -895,7 +895,7 @@ class WsSession implements ProviderWebSocketSession {
|
|
|
895
895
|
this.#signal = signal;
|
|
896
896
|
socket.on("message", (data: RawData, isBinary: boolean) => {
|
|
897
897
|
if (isBinary) {
|
|
898
|
-
this.#fail(new GatewayError("transport_unavailable", 502, "unexpected binary WebSocket frame"));
|
|
898
|
+
this.#fail(new GatewayError("transport_unavailable", 502, "unexpected binary WebSocket frame (request {requestId}). The connection ghosted us — come back in a moment 👻"));
|
|
899
899
|
return;
|
|
900
900
|
}
|
|
901
901
|
const message = data.toString();
|
|
@@ -907,14 +907,14 @@ class WsSession implements ProviderWebSocketSession {
|
|
|
907
907
|
this.#messages.push(message);
|
|
908
908
|
}
|
|
909
909
|
});
|
|
910
|
-
socket.on("close", () => this.#fail(new GatewayError("transport_closed", 502, "WebSocket connection closed")));
|
|
911
|
-
socket.on("error", () => this.#fail(new GatewayError("transport_unavailable", 502, "WebSocket connection failed")));
|
|
910
|
+
socket.on("close", () => this.#fail(new GatewayError("transport_closed", 502, "WebSocket connection closed (request {requestId}). The connection ghosted us — come back in a moment 👻")));
|
|
911
|
+
socket.on("error", () => this.#fail(new GatewayError("transport_unavailable", 502, "WebSocket connection failed (request {requestId}). The connection ghosted us — come back in a moment 👻")));
|
|
912
912
|
}
|
|
913
913
|
|
|
914
914
|
send(message: string): void {
|
|
915
915
|
if (this.#closed) throw this.#closed;
|
|
916
916
|
if (this.#socket.readyState !== WebSocket.OPEN) {
|
|
917
|
-
throw new GatewayError("transport_closed", 499, "WebSocket connection is not open");
|
|
917
|
+
throw new GatewayError("transport_closed", 499, "WebSocket connection is not open (request {requestId}). Send it again whenever ready 🙃");
|
|
918
918
|
}
|
|
919
919
|
this.#socket.send(message, { binary: false });
|
|
920
920
|
}
|
|
@@ -927,11 +927,11 @@ class WsSession implements ProviderWebSocketSession {
|
|
|
927
927
|
return new Promise<string>((resolve, reject) => {
|
|
928
928
|
const timeout = setTimeout(() => {
|
|
929
929
|
cleanup();
|
|
930
|
-
reject(new GatewayError("transport_unavailable", 504, "WebSocket receive timed out"));
|
|
930
|
+
reject(new GatewayError("transport_unavailable", 504, "WebSocket receive timed out (request {requestId}). Some things just take longer 🐢"));
|
|
931
931
|
}, 45_000);
|
|
932
932
|
const onAbort = (): void => {
|
|
933
933
|
cleanup();
|
|
934
|
-
reject(new GatewayError("transport_closed", 499, "WebSocket receive was cancelled"));
|
|
934
|
+
reject(new GatewayError("transport_closed", 499, "WebSocket receive was cancelled (request {requestId}). Send it again whenever ready 🙃"));
|
|
935
935
|
};
|
|
936
936
|
const cleanup = (): void => {
|
|
937
937
|
clearTimeout(timeout);
|
|
@@ -997,7 +997,7 @@ export class ValidatedNetworkBindingFactory {
|
|
|
997
997
|
throw new GatewayError(
|
|
998
998
|
"proxy_pool_unhealthy",
|
|
999
999
|
503,
|
|
1000
|
-
"Configured proxy pool could not be established.",
|
|
1000
|
+
"Configured proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧",
|
|
1001
1001
|
{ poolId: networkPoolId, reason: error.message.slice(0, 200) },
|
|
1002
1002
|
"network",
|
|
1003
1003
|
);
|
|
@@ -1009,27 +1009,27 @@ export class ValidatedNetworkBindingFactory {
|
|
|
1009
1009
|
webSocket(networkPoolId?: string, tenantId?: string): ValidatedOutboundWebSocket {
|
|
1010
1010
|
return async (url, headers, signal) => {
|
|
1011
1011
|
if (url.protocol !== "wss:") {
|
|
1012
|
-
throw new GatewayError("invalid_request", 400, "validated WebSocket egress requires wss");
|
|
1012
|
+
throw new GatewayError("invalid_request", 400, "validated WebSocket egress requires wss (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
1013
1013
|
}
|
|
1014
1014
|
if (url.username || url.password || url.hash || (url.port !== "" && url.port !== "443")) {
|
|
1015
|
-
throw new GatewayError("invalid_request", 400, "invalid validated WebSocket target");
|
|
1015
|
+
throw new GatewayError("invalid_request", 400, "invalid validated WebSocket target (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
1016
1016
|
}
|
|
1017
1017
|
const hostname = url.hostname;
|
|
1018
|
-
if (!hostname) throw new GatewayError("invalid_request", 400, "WebSocket target hostname is required");
|
|
1018
|
+
if (!hostname) throw new GatewayError("invalid_request", 400, "WebSocket target hostname is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
1019
1019
|
const destination = await this.resolve(hostname, signal, 443);
|
|
1020
1020
|
if (!isAddressAllowed(destination.resolvedAddress, this.policy)) {
|
|
1021
|
-
throw new GatewayError("invalid_request", 400, "unsafe upstream address rejected");
|
|
1021
|
+
throw new GatewayError("invalid_request", 400, "unsafe upstream address rejected (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
1022
1022
|
}
|
|
1023
1023
|
let agent: PoolAgent | undefined;
|
|
1024
1024
|
if (networkPoolId || tenantId) {
|
|
1025
1025
|
if (!networkPoolId || !tenantId || !this.poolResolver) {
|
|
1026
|
-
throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool could not be established");
|
|
1026
|
+
throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧");
|
|
1027
1027
|
}
|
|
1028
1028
|
try {
|
|
1029
1029
|
agent = await this.poolResolver.resolveWebSocketAgent(networkPoolId, tenantId);
|
|
1030
1030
|
} catch (error) {
|
|
1031
1031
|
if (error instanceof PoolBindingError) {
|
|
1032
|
-
throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool could not be established");
|
|
1032
|
+
throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧");
|
|
1033
1033
|
}
|
|
1034
1034
|
throw error;
|
|
1035
1035
|
}
|
|
@@ -1058,7 +1058,7 @@ export class ValidatedNetworkBindingFactory {
|
|
|
1058
1058
|
assertBidiTarget(url);
|
|
1059
1059
|
const { hostname } = url;
|
|
1060
1060
|
if (options?.allowHostname !== undefined && !options.allowHostname(hostname)) {
|
|
1061
|
-
throw new GatewayError("invalid_request", 400, "HTTP/2 target host is not allowlisted");
|
|
1061
|
+
throw new GatewayError("invalid_request", 400, "HTTP/2 target host is not allowlisted (request {requestId}). All roads blocked — time to find a new route 🗺️");
|
|
1062
1062
|
}
|
|
1063
1063
|
const signal = options?.signal ?? AbortSignal.timeout(HTTP2_CONNECT_TIMEOUT_MS);
|
|
1064
1064
|
// Validation runs on every new connection, not once per capability: a
|
|
@@ -1066,7 +1066,7 @@ export class ValidatedNetworkBindingFactory {
|
|
|
1066
1066
|
// resolved safely the first time is not safe the twentieth.
|
|
1067
1067
|
const destination = await this.resolve(hostname, signal, 443);
|
|
1068
1068
|
if (!isAddressAllowed(destination.resolvedAddress, this.policy)) {
|
|
1069
|
-
throw new GatewayError("invalid_request", 400, "unsafe upstream address rejected");
|
|
1069
|
+
throw new GatewayError("invalid_request", 400, "unsafe upstream address rejected (request {requestId}). All roads blocked — time to find a new route 🗺️");
|
|
1070
1070
|
}
|
|
1071
1071
|
// Mirrors `fetch`: no pool selected (or none configured) means validated
|
|
1072
1072
|
// *direct* egress, which is already resolved and SSRF-checked above.
|
|
@@ -1086,7 +1086,7 @@ export class ValidatedNetworkBindingFactory {
|
|
|
1086
1086
|
throw new GatewayError(
|
|
1087
1087
|
"proxy_pool_unhealthy",
|
|
1088
1088
|
503,
|
|
1089
|
-
"Configured proxy pool could not be established.",
|
|
1089
|
+
"Configured proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧",
|
|
1090
1090
|
{ poolId: networkPoolId, reason: error.message.slice(0, 200) },
|
|
1091
1091
|
"network",
|
|
1092
1092
|
);
|
|
@@ -18,6 +18,7 @@ import type { CartethyiaDatabase } from "../persistence/db-handle";
|
|
|
18
18
|
import { networkPools, healthEvents } from "../persistence/schema";
|
|
19
19
|
import { log } from "../observability/logger";
|
|
20
20
|
import { providerRateLimitIsIpScoped } from "../providers/provider-metadata";
|
|
21
|
+
import { resolvePoolProviderRateLimitCooldownMs } from "../config/cooldowns";
|
|
21
22
|
import { PROVIDER_BLOCK_MS, type ProxyCooldownEntry } from "./pool/selector";
|
|
22
23
|
|
|
23
24
|
type PoolProviderCooldownMap = NonNullable<
|
|
@@ -120,6 +121,37 @@ export async function dropAllProviderCooldownMarkers(
|
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Removes expired provider markers from one pool row, keeping live cooldowns
|
|
126
|
+
* and permanent blocks intact. Returns how many markers were dropped.
|
|
127
|
+
*
|
|
128
|
+
* The selector and Redis prune expired entries on read, but the durable JSON
|
|
129
|
+
* mirror is only ever written, never swept — so a marker that expired while
|
|
130
|
+
* the process was down survives on the row forever. Harmless to routing (boot
|
|
131
|
+
* restores only future-dated markers), but it bloats the row and makes the
|
|
132
|
+
* backup/restore surface carry dead state.
|
|
133
|
+
*/
|
|
134
|
+
export async function pruneExpiredProviderCooldownMarkers(
|
|
135
|
+
db: CartethyiaDatabase,
|
|
136
|
+
poolId: string,
|
|
137
|
+
): Promise<number> {
|
|
138
|
+
try {
|
|
139
|
+
const map = await readMarkerMap(db, poolId);
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
const expiredKeys: string[] = [];
|
|
142
|
+
for (const [providerKey, marker] of Object.entries(map)) {
|
|
143
|
+
if (marker.until <= now) expiredKeys.push(providerKey);
|
|
144
|
+
}
|
|
145
|
+
if (expiredKeys.length === 0) return 0;
|
|
146
|
+
for (const providerKey of expiredKeys) delete map[providerKey];
|
|
147
|
+
await writeMarkerMap(db, poolId, map);
|
|
148
|
+
return expiredKeys.length;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
log.warn("pool provider cooldown mirror prune failed", { networkPoolId: poolId, error: String(error) });
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
123
155
|
/**
|
|
124
156
|
* Writes a probe-measured verdict the same way a dispatch-side event would be
|
|
125
157
|
* recorded: one mirror marker and one audit row.
|
|
@@ -201,19 +233,22 @@ function probeEndpointOf(endpointConfig: unknown): string {
|
|
|
201
233
|
/**
|
|
202
234
|
* Re-enforces the stored probe verdicts at boot (new: replay, read-ahead).
|
|
203
235
|
*
|
|
204
|
-
* A stored
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* verdict for an address-keyed provider becomes a block flag, even when the
|
|
210
|
-
* mirror has nothing (or has an older, cooler entry — the retirement claim is
|
|
211
|
-
* the sharper fact and wins).
|
|
236
|
+
* A stored verdict is a *state*, not a decoration. If the marker the probe
|
|
237
|
+
* flagged failed to persist, or the verdict predated the marker column, the
|
|
238
|
+
* egress would return to rotation after restart and get refused again — dispatch
|
|
239
|
+
* spending a real request to rediscover a refusal the row already knew. So the
|
|
240
|
+
* boot read reconciles:
|
|
212
241
|
*
|
|
213
|
-
* `
|
|
214
|
-
* the
|
|
215
|
-
*
|
|
216
|
-
*
|
|
242
|
+
* - A fresh `blocked` verdict for an address-keyed provider becomes a block
|
|
243
|
+
* flag, even when the mirror has nothing (or has an older, cooler entry — the
|
|
244
|
+
* retirement claim is the sharper fact and wins).
|
|
245
|
+
* - A fresh `throttled` verdict for an address-keyed provider becomes the same
|
|
246
|
+
* cooldown a dispatch-side 429 would have written. The probe measured the
|
|
247
|
+
* rate-limit window itself (`provider_probe_at` is when it observed the 429),
|
|
248
|
+
* so the replayed window starts at that measurement, not at boot — without
|
|
249
|
+
* the monotonic clock, a restart mid-window would shorten the cool-off the
|
|
250
|
+
* probe proved. The configured default window is applied when the probe did
|
|
251
|
+
* not record a duration.
|
|
217
252
|
*
|
|
218
253
|
* Returns how many verdicts were replayed.
|
|
219
254
|
*/
|
|
@@ -221,6 +256,7 @@ export async function restorePoolProbeVerdicts(
|
|
|
221
256
|
selector: { hydrateProviderCooldown(entry: ProxyCooldownEntry): void },
|
|
222
257
|
db: CartethyiaDatabase,
|
|
223
258
|
): Promise<number> {
|
|
259
|
+
const probeCooldownMs = resolvePoolProviderRateLimitCooldownMs();
|
|
224
260
|
const rows = await db
|
|
225
261
|
.select({
|
|
226
262
|
id: networkPools.id,
|
|
@@ -230,21 +266,40 @@ export async function restorePoolProbeVerdicts(
|
|
|
230
266
|
providerProbeStatus: networkPools.providerProbeStatus,
|
|
231
267
|
providerProbeReason: networkPools.providerProbeReason,
|
|
232
268
|
providerProbeEndpoint: networkPools.providerProbeEndpoint,
|
|
269
|
+
providerProbeAt: networkPools.providerProbeAt,
|
|
233
270
|
})
|
|
234
271
|
.from(networkPools);
|
|
235
272
|
let replayed = 0;
|
|
236
273
|
for (const row of rows) {
|
|
237
274
|
if (row.status !== "active") continue;
|
|
238
|
-
if (row.providerProbeStatus !== "blocked") continue;
|
|
275
|
+
if (row.providerProbeStatus !== "blocked" && row.providerProbeStatus !== "throttled") continue;
|
|
239
276
|
if (row.providerProbeEndpoint !== probeEndpointOf(row.endpointConfig)) continue;
|
|
240
277
|
const providerId = row.providerProbeProviderId;
|
|
241
278
|
if (typeof providerId !== "string" || !providerRateLimitIsIpScoped(providerId)) continue;
|
|
279
|
+
if (row.providerProbeStatus === "blocked") {
|
|
280
|
+
selector.hydrateProviderCooldown({
|
|
281
|
+
poolId: row.id,
|
|
282
|
+
providerId: providerId.toLowerCase(),
|
|
283
|
+
until: Date.now() + PROVIDER_BLOCK_MS,
|
|
284
|
+
reason: row.providerProbeReason ?? "Upstream refused this egress address",
|
|
285
|
+
blocked: true,
|
|
286
|
+
});
|
|
287
|
+
replayed += 1;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
// `throttled`: replay the cooldown the probe measured. The upstream's
|
|
291
|
+
// window began when the probe saw the 429, so the marker expires at
|
|
292
|
+
// `probeAt + window`, unless that is already past — an allowance that
|
|
293
|
+
// refilled while the process was down must not be resurrected.
|
|
294
|
+
const probeAt = row.providerProbeAt?.getTime();
|
|
295
|
+
const until =
|
|
296
|
+
probeAt !== undefined ? probeAt + probeCooldownMs : Date.now() + probeCooldownMs;
|
|
297
|
+
if (until <= Date.now()) continue;
|
|
242
298
|
selector.hydrateProviderCooldown({
|
|
243
299
|
poolId: row.id,
|
|
244
300
|
providerId: providerId.toLowerCase(),
|
|
245
|
-
until
|
|
246
|
-
reason: row.providerProbeReason ?? "
|
|
247
|
-
blocked: true,
|
|
301
|
+
until,
|
|
302
|
+
reason: row.providerProbeReason ?? "Operator probe measured a provider 429",
|
|
248
303
|
});
|
|
249
304
|
replayed += 1;
|
|
250
305
|
}
|
|
@@ -5,6 +5,7 @@ import type { CartethyiaDatabase } from "../persistence/db-handle";
|
|
|
5
5
|
import { healthEvents, networkPools } from "../persistence/schema";
|
|
6
6
|
import { drainPoolByteDelta } from "./pool/byte-accounting";
|
|
7
7
|
import { resolvePoolCooldownMs } from "../config/cooldowns";
|
|
8
|
+
import { dropProviderCooldownMarker, pruneExpiredProviderCooldownMarkers } from "./pool-cooldown-store";
|
|
8
9
|
export interface PoolHealthEvent {
|
|
9
10
|
readonly id: string;
|
|
10
11
|
readonly networkPoolId: string;
|
|
@@ -54,7 +55,14 @@ function poolFaultCategory(error: unknown, origin: string | undefined): string |
|
|
|
54
55
|
export async function recordPoolDispatchOutcome(
|
|
55
56
|
db: CartethyiaDatabase,
|
|
56
57
|
poolId: string,
|
|
57
|
-
input: {
|
|
58
|
+
input: {
|
|
59
|
+
readonly succeeded: boolean;
|
|
60
|
+
readonly error?: unknown;
|
|
61
|
+
readonly errorOrigin?: string;
|
|
62
|
+
/** The provider the attempt dialed; used to clear its stale egress marker on success. */
|
|
63
|
+
readonly providerId?: string;
|
|
64
|
+
readonly snapshotInvalidator?: { invalidate(): unknown };
|
|
65
|
+
},
|
|
58
66
|
): Promise<void> {
|
|
59
67
|
const category = input.succeeded ? undefined : poolFaultCategory(input.error, input.errorOrigin);
|
|
60
68
|
// Metered bytes are banked even when health is untouched: an upstream-origin
|
|
@@ -95,6 +103,14 @@ export async function recordPoolDispatchOutcome(
|
|
|
95
103
|
lastErrorCategory = null;
|
|
96
104
|
lastErrorAt = null;
|
|
97
105
|
lastSuccessAt = now;
|
|
106
|
+
// A completed dispatch proves the egress is serving again for this
|
|
107
|
+
// provider — clear its stale rate-limit marker (the selector's local
|
|
108
|
+
// entry and the Redis copy) so a spent pool is not kept out of rotation
|
|
109
|
+
// after it demonstrably recovered. Fire-and-forget: a failed clear must
|
|
110
|
+
// never fail a completed request.
|
|
111
|
+
if (input.providerId !== undefined) {
|
|
112
|
+
void dropProviderCooldownMarker(db, poolId, input.providerId).catch(() => undefined);
|
|
113
|
+
}
|
|
98
114
|
} else {
|
|
99
115
|
failures += 1;
|
|
100
116
|
// Every failed probe parks the pool: a pool that cannot carry traffic is
|
|
@@ -252,5 +268,9 @@ export async function sweepExpiredPoolCooldowns(db: CartethyiaDatabase): Promise
|
|
|
252
268
|
await db.update(networkPools).set({ status: "active", consecutiveFailures: 0, cooldownUntil: null, lastRecoveredAt: now }).where(inArray(networkPools.id, expired.map((row) => row.id)));
|
|
253
269
|
await db.insert(healthEvents).values(expired.map((row) => ({ id: randomUUID(), entityKind: "pool" as const, networkPoolId: row.id, fromStatus: row.status, toStatus: "active" as const, reason: "Cooldown elapsed — auto-recovered", errorCategory: null, createdAt: now })));
|
|
254
270
|
for (const row of expired) publishPoolHealth({ poolId: row.id, tenantId: row.tenantId, status: "active", consecutiveFailures: 0 });
|
|
271
|
+
// The pool-level cooldown elapsed, so the same window's provider markers are
|
|
272
|
+
// expired too. Drop them from the durable JSON mirror — the selector and
|
|
273
|
+
// Redis prune on read, but the row's stale copy would otherwise live forever.
|
|
274
|
+
await Promise.all(expired.map((row) => pruneExpiredProviderCooldownMarkers(db, row.id)));
|
|
255
275
|
return expired.length;
|
|
256
276
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified egress-failure classification for ip-scoped, rate-limited providers.
|
|
3
|
+
*
|
|
4
|
+
* One 429 is read in several places — dispatch (`attempt-loop`, streaming
|
|
5
|
+
* `finalize`), the provider probe, account health, and the pool health machine
|
|
6
|
+
* each had their own vocabulary. The two questions that decide whether a probe
|
|
7
|
+
* *pool* (not an account) is responsible for a failure are:
|
|
8
|
+
*
|
|
9
|
+
* - Did the upstream count this request against the egress address? Only a
|
|
10
|
+
* provider whose limits follow the IP (`ipScopedRateLimit`) qualifies — for
|
|
11
|
+
* everyone else the 429 belongs to the credential that was dialed, and
|
|
12
|
+
* cooling the pool would sideline healthy accounts.
|
|
13
|
+
* - Is the address itself retired (free-tier 403) rather than merely
|
|
14
|
+
* throttled (429)? A block is terminal; a cooldown refills.
|
|
15
|
+
*
|
|
16
|
+
* This module is the single source of truth for those two facts. Dispatch and
|
|
17
|
+
* the probe both read it, so the operator's "test" and gateway's own dial
|
|
18
|
+
* cannot disagree about which egress is spent.
|
|
19
|
+
*/
|
|
20
|
+
import { GatewayError } from "../protocol/gateway-error";
|
|
21
|
+
import { providerRateLimitIsIpScoped } from "../providers/provider-metadata";
|
|
22
|
+
|
|
23
|
+
/** What one upstream failure means for the egress that carried it. */
|
|
24
|
+
export type EgressVerdict =
|
|
25
|
+
| { kind: "none" }
|
|
26
|
+
| { kind: "cooldown"; retryAfterMs?: number }
|
|
27
|
+
| { kind: "blocked" };
|
|
28
|
+
|
|
29
|
+
/** The upstream's own wording for a retired free-tier address. */
|
|
30
|
+
export const FREE_TIER_REFUSAL = /freetier|free tier can only be used/i;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Classifies one upstream failure against one provider.
|
|
34
|
+
*
|
|
35
|
+
* `providerId` is the caller's own candidate, not a field read off the error:
|
|
36
|
+
* the in-stream error frames the protocol modules build carry no `providerId`
|
|
37
|
+
* detail, so reading it from `details` would silently disable the IP-scoped
|
|
38
|
+
* rule on exactly the streaming path it exists for.
|
|
39
|
+
*/
|
|
40
|
+
export function classifyEgressFailure(error: unknown, providerId: string): EgressVerdict {
|
|
41
|
+
if (!(error instanceof GatewayError) || error.origin !== "upstream") {
|
|
42
|
+
return { kind: "none" };
|
|
43
|
+
}
|
|
44
|
+
if (!providerRateLimitIsIpScoped(providerId)) return { kind: "none" };
|
|
45
|
+
|
|
46
|
+
// A provider-scoped 429 exhausted the address's allowance: cool the pool
|
|
47
|
+
// until the window refills.
|
|
48
|
+
if (error.status === 429 && error.details.rateLimitScope === "provider") {
|
|
49
|
+
const retryAfterMs =
|
|
50
|
+
typeof error.details.retryAfterMs === "number" ? error.details.retryAfterMs : undefined;
|
|
51
|
+
return retryAfterMs === undefined ? { kind: "cooldown" } : { kind: "cooldown", retryAfterMs };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A free-tier 403 retires the address: the request was never read, so the
|
|
55
|
+
// same egress cannot succeed no matter what the payload says. Deliberately
|
|
56
|
+
// narrower than "any 403" — a content-policy or licence refusal is also a
|
|
57
|
+
// 403 and must not retire a healthy proxy.
|
|
58
|
+
if (error.status === 403) {
|
|
59
|
+
const providerCode =
|
|
60
|
+
typeof error.details.providerCode === "string" ? error.details.providerCode : "";
|
|
61
|
+
const raw = String(error.details.raw ?? "");
|
|
62
|
+
if (/freetier|free.?tier/i.test(providerCode) || FREE_TIER_REFUSAL.test(`${error.message} ${raw}`)) {
|
|
63
|
+
return { kind: "blocked" };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { kind: "none" };
|
|
68
|
+
}
|
package/src/network/ssrf.ts
CHANGED
|
@@ -170,7 +170,7 @@ export function validateResolvedAddresses(
|
|
|
170
170
|
policy: SsrfPolicy,
|
|
171
171
|
): string {
|
|
172
172
|
if (addresses.length === 0 || addresses.some((address) => !isAddressAllowed(address, policy))) {
|
|
173
|
-
throw new GatewayError("invalid_request", 400, "The upstream address this route resolves to is not allowed. Point the provider at a permitted host.");
|
|
173
|
+
throw new GatewayError("invalid_request", 400, "The upstream address this route resolves to is not allowed (request {requestId}). Point the provider at a permitted host. All roads blocked — time to find a new route 🗺️");
|
|
174
174
|
}
|
|
175
175
|
return addresses[0]!;
|
|
176
176
|
}
|
|
@@ -227,7 +227,7 @@ export async function resolveAllAddresses(
|
|
|
227
227
|
throw new GatewayError(
|
|
228
228
|
"transport_unavailable",
|
|
229
229
|
502,
|
|
230
|
-
"upstream DNS resolution failed",
|
|
230
|
+
"upstream DNS resolution failed (request {requestId}). The connection ghosted us — come back in a moment 👻",
|
|
231
231
|
{ cause: String(error) },
|
|
232
232
|
"network",
|
|
233
233
|
);
|
|
@@ -238,8 +238,8 @@ function abortErrorForSignal(signal: AbortSignal): GatewayError {
|
|
|
238
238
|
const reason = signal.reason;
|
|
239
239
|
if (reason instanceof GatewayError) return reason;
|
|
240
240
|
if (reason instanceof DOMException && reason.name === "TimeoutError")
|
|
241
|
-
return new GatewayError("deadline_exceeded", 504, "request deadline exceeded", {}, "cartethyia");
|
|
242
|
-
return new GatewayError("transport_closed", 499, "request was cancelled");
|
|
241
|
+
return new GatewayError("deadline_exceeded", 504, "request deadline exceeded (request {requestId}). Some things just take longer 🐢", {}, "cartethyia");
|
|
242
|
+
return new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
export async function resolveAndValidateOnce(
|