cartethyia 2.7.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +107 -106
- package/README.md +25 -2
- package/dist/dashboard/assets/{BannedPage-P76QLGJA.js → BannedPage-D-J75hBM.js} +1 -1
- package/dist/dashboard/assets/BannedPage-D-J75hBM.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-DdBggJS9.js → CliToolDetailPage-CVe6RaBH.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-aN1iXhhP.js → CliToolsPage-Dd8ZSamp.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-DE-F1nxZ.js → CombosPage-QoJ6foxo.js} +1 -1
- package/dist/dashboard/assets/CombosPage-QoJ6foxo.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-3CUxMy2X.js → CustomizationPage-CKUFTcW4.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DubdQcvX.js +1 -0
- package/dist/dashboard/assets/LoginPage-DubdQcvX.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-DKVslqG3.js → ModelPicker-BH8Tehdi.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js +4 -0
- package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js +9 -0
- package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-BjldQwIU.js → QuotaPage-ATpaxrbl.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-ATpaxrbl.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-DUIoLRxk.js → SettingsPage-CETophpx.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CETophpx.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BdgECjqq.js +1 -0
- package/dist/dashboard/assets/SetupPage-BdgECjqq.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-fI7JkdIB.js → SortableList-CAK-EN9z.js} +1 -1
- package/dist/dashboard/assets/SortableList-CAK-EN9z.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CHVZqskZ.js → StudioPage-B7W2_RhT.js} +3 -3
- package/dist/dashboard/assets/StudioPage-B7W2_RhT.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-6T4EFDjW.js +56 -0
- package/dist/dashboard/assets/UsagePage-6T4EFDjW.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-DFY_fZU4.js → api-keys-CUi6UXsR.js} +1 -1
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css +2 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
- package/dist/dashboard/assets/{assert-CwdnZARt.js → assert-67xXH5rf.js} +1 -1
- package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
- package/dist/dashboard/assets/{badge-Dr-uf0Au.js → badge-DC2L43Mj.js} +1 -1
- package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BddmMdoS.js → banner-BBVcb0J3.js} +1 -1
- package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
- package/dist/dashboard/assets/{card-CK0PXWlX.js → card-Cca1XC6G.js} +1 -1
- package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CcB6mGh2.js → cli-tools-KvsWa1Jl.js} +1 -1
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DGb2P9Yx.js → clipboard-button-DUzEbynq.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-LkfuL50u.js → dialog-DFvWhwTa.js} +1 -1
- package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-ZbEZ_84c.js → drawer-CFM2sMX1.js} +1 -1
- package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CWBY5A0f.js → entry-BLBLxX0h.js} +3 -3
- package/dist/dashboard/assets/entry-BLBLxX0h.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BJfsVMvA.js → entry-Bqy8RFvM.js} +1 -1
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CvC1-Zqx.js → entry-_xHo5x6r.js} +1 -1
- package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
- package/dist/dashboard/assets/formatters-mpBPqAQ8.js +1 -0
- package/dist/dashboard/assets/formatters-mpBPqAQ8.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-BxpznFLt.js → github-badge-C9e8Ts7e.js} +1 -1
- package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
- package/dist/dashboard/assets/{index-CDzP3ovJ.js → index-D6QzEC_n.js} +2 -2
- package/dist/dashboard/assets/index-D6QzEC_n.js.gz +0 -0
- package/dist/dashboard/assets/{inline-Ae13MQ5w.js → inline-BFUcvllj.js} +1 -1
- package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
- package/dist/dashboard/assets/{input-D23fo7cp.js → input-C_ILAvBb.js} +1 -1
- package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CSsxjj5P.js → layout-CBIyT6Rt.js} +1 -1
- package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-dmbwZMDY.js → list-row-ebJ_-Nsb.js} +1 -1
- package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
- package/dist/dashboard/assets/{live-BG1oUsZZ.js → live-D0mm--ME.js} +1 -1
- package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-DSF-e4ho.js → mutation-sMD965oO.js} +1 -1
- package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-Bm_aECY7.js → numbers-BO9dKaFr.js} +1 -1
- package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
- package/dist/dashboard/assets/providers/inception.svg +5 -0
- package/dist/dashboard/assets/providers/inception.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CF56nYIV.js → query-keys-CfL_Zdoh.js} +1 -1
- package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CeA71n7G.js → routing-BnIpi1IJ.js} +1 -1
- package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/{select-KIT0rC6G.js → select-Cj5Nw43g.js} +1 -1
- package/dist/dashboard/assets/select-Cj5Nw43g.js.gz +0 -0
- package/dist/dashboard/assets/{settings-B_2gkePf.js → settings-BAFhDgX8.js} +1 -1
- package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
- package/dist/dashboard/assets/{stack-D7WOSzcX.js → stack-EMrOMt4I.js} +1 -1
- package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
- package/dist/dashboard/assets/{state-BBxtKZOQ.js → state-CWoo7ty5.js} +1 -1
- package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DNS_D5Ea.js → switch-CCjT6IkF.js} +1 -1
- package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BgoxdoMk.js → useMutation-DjkLHUcE.js} +1 -1
- package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +54 -53
- package/migrations/full-postgres/0024_add_pool_provider_cooldowns.sql +12 -0
- package/migrations/full-postgres/0025_add_pool_random_strategy.sql +3 -0
- package/migrations/lite-sqlite3/0024_add_pool_provider_cooldowns.sql +12 -0
- package/migrations/lite-sqlite3/0025_add_pool_random_strategy.sql +20 -0
- package/package.json +96 -96
- package/scripts/cli/console-menu.ts +5 -2
- package/scripts/cli/tray.ts +21 -6
- package/src/boot/app.ts +684 -714
- package/src/boot/dependencies.ts +65 -62
- package/src/config/cooldowns.ts +16 -0
- package/src/config/providers.ts +13 -1
- package/src/config/resources.ts +70 -70
- package/src/config/security.ts +37 -37
- package/src/config/spec.ts +371 -358
- package/src/console/console-router.ts +233 -225
- package/src/console/domain-registration.ts +398 -381
- package/src/console/providers/catalog/catalog-projections.ts +2 -6
- package/src/console/providers/catalog/contracts.ts +19 -0
- package/src/console/providers/catalog/provider-operations.ts +38 -0
- package/src/console/providers/catalog/routes.ts +14 -0
- package/src/console/providers/catalog/store-helpers.ts +326 -0
- package/src/console/providers/catalog/store.ts +1292 -1563
- package/src/console/quota/account-quota-global.ts +2 -2
- package/src/console/quota/account-quota-shared.ts +1 -1
- package/src/console/quota/account-quota-tenant.ts +35 -12
- package/src/console/quota/quota-view.ts +2 -2
- package/src/console/routing/pools/contracts.ts +41 -1
- package/src/console/routing/pools/operations.ts +183 -4
- package/src/console/routing/pools/relay-deploy.ts +17 -4
- package/src/console/routing/pools/routes.ts +30 -0
- package/src/console/routing/pools/store.ts +98 -8
- package/src/main.ts +183 -167
- package/src/network/outbound-fetch.ts +226 -365
- package/src/network/pool/agent.ts +8 -0
- package/src/network/pool/pool-state.ts +7 -0
- package/src/network/pool/selector.ts +110 -2
- package/src/network/pool-cooldown-store.ts +252 -0
- package/src/network/pool-health-machine.ts +5 -3
- package/src/network/pool-health.ts +36 -9
- package/src/observability/event-query.ts +57 -0
- package/src/observability/metrics.ts +467 -467
- package/src/observability/runtime-metrics.ts +269 -269
- package/src/persistence/health-event-flush.ts +2 -0
- package/src/persistence/schema.ts +19 -0
- package/src/persistence/usage-flush.ts +8 -3
- package/src/protocol/canonical-model.ts +1 -1
- package/src/protocol/dsml.ts +377 -0
- package/src/protocol/gateway-error.ts +232 -232
- package/src/{providers/provider-reasoning.ts → protocol/reasoning.ts} +10 -9
- package/src/protocol/request/chat.ts +7 -7
- package/src/protocol/request/codex.ts +2 -2
- package/src/protocol/request/gemini.ts +1 -1
- package/src/protocol/request/messages.ts +6 -6
- package/src/protocol/request/responses.ts +7 -7
- package/src/protocol/response/dsml.ts +12 -133
- package/src/{transport → protocol}/translation/attachment-integrity.ts +2 -2
- package/src/{transport → protocol}/translation/cache-controls.ts +1 -1
- package/src/{transport → protocol}/translation/capabilities.ts +2 -2
- package/src/{transport → protocol}/translation/normalize.ts +1 -1
- package/src/{transport → protocol}/translation/quirks.ts +1 -1
- package/src/{transport → protocol}/translation/search.ts +1 -1
- package/src/{transport → protocol}/translation/thinking.ts +23 -1
- package/src/{transport → protocol}/translation/tool-id.ts +1 -1
- package/src/{transport → protocol}/translation/tool-repair.ts +2 -2
- package/src/protocol/upstream-parsing.ts +176 -0
- package/src/providers/adapters/compatible-adapter.ts +11 -2
- package/src/providers/authentication/oauth-refresh-service.ts +9 -5
- package/src/providers/default-registry.ts +21 -5
- package/src/providers/discovery/probe-phases.ts +16 -3
- package/src/providers/discovery/probing-service.ts +4 -1
- package/src/providers/integrations/anthropic.ts +1 -1
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -4
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +141 -8
- package/src/providers/integrations/buddy/buddy-checkin.ts +5 -1
- package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -82
- package/src/providers/integrations/buddy/buddy-identity.ts +25 -0
- package/src/providers/integrations/buddy/buddy-server-tools.ts +240 -0
- package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +36 -0
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +6 -31
- package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -0
- package/src/providers/integrations/buddy/codebuddy/shared.ts +19 -4
- package/src/providers/integrations/buddy/turing-device-token.ts +262 -0
- package/src/providers/integrations/buddy/workbuddy/provider.ts +2 -0
- package/src/providers/integrations/buddy/workbuddy/shared.ts +56 -3
- package/src/providers/integrations/claude/claude.ts +1 -1
- package/src/providers/integrations/cloudflareai/cloudflareai.ts +1 -1
- package/src/providers/integrations/codex/codex-errors.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +1 -1
- package/src/providers/integrations/factory/factory.ts +1 -1
- package/src/providers/integrations/github/github.ts +1 -1
- package/src/providers/integrations/grok/grok.ts +1 -1
- package/src/providers/integrations/inception/provider.ts +91 -0
- package/src/providers/integrations/inferhub.ts +1 -1
- package/src/providers/integrations/kiro/kiro-request.ts +1 -1
- package/src/providers/integrations/mistral.ts +193 -19
- package/src/providers/integrations/opencode/fingerprint.ts +19 -2
- package/src/providers/integrations/opencode/prompt.ts +111 -0
- package/src/providers/integrations/opencode/provider.ts +101 -23
- package/src/providers/integrations/opencode/tool-definitions.ts +4 -4
- package/src/providers/integrations/qoder/qoder-checkin.ts +303 -0
- package/src/providers/integrations/qoder/qoder.ts +1641 -1458
- package/src/providers/integrations/shared/install-id.ts +2 -1
- package/src/providers/integrations/trae/trae-cache.ts +110 -0
- package/src/providers/integrations/trae/trae-oauth.ts +29 -4
- package/src/providers/integrations/trae/trae-protocol.ts +2 -1
- package/src/providers/integrations/trae/trae.ts +106 -23
- package/src/providers/operations/account-error-classification.ts +1660 -0
- package/src/providers/operations/account-health-service.ts +621 -2097
- package/src/providers/operations/account-reset-service.ts +2 -0
- package/src/providers/provider-contracts.ts +0 -2
- package/src/providers/provider-metadata.ts +25 -4
- package/src/providers/provider-probe.ts +2 -2
- package/src/{console → providers}/quota/cache.ts +1 -1
- package/src/{console → providers}/quota/refresh.ts +5 -5
- package/src/transport/dispatch/attempt-finalize.ts +114 -12
- package/src/transport/dispatch/attempt-loop.ts +3 -0
- package/src/transport/dispatch/empty-stream.ts +224 -217
- package/src/transport/dispatch/generation.ts +37 -55
- package/src/transport/dispatch/handler-deps.ts +32 -0
- package/src/transport/dispatch/leases.ts +9 -4
- package/src/transport/dispatch/proxy-request.ts +6 -30
- package/src/transport/dispatch/responses-compact.ts +22 -43
- package/src/transport/dispatch/streaming/finalize.ts +3 -0
- package/src/transport/dispatch/streaming/stream.ts +0 -8
- package/src/transport/dispatch/systemone.ts +22 -42
- package/src/transport/dispatch/token-count.ts +23 -39
- package/src/transport/dispatch/upstream.ts +1 -3
- package/src/transport/dispatch/websearch.ts +22 -41
- package/src/transport/failure-policy.ts +59 -178
- package/src/transport/guards/abuse-guard.ts +10 -0
- package/src/transport/guards/auth-guard.ts +109 -109
- package/src/transport/guards/error-lifecycle.ts +1 -1
- package/src/transport/guards/pipeline.ts +220 -220
- package/src/transport/guards/request-context.ts +245 -238
- package/src/transport/request/degradation.ts +20 -1
- package/src/transport/request/inflight.ts +4 -114
- package/src/transport/request/preparer.ts +6 -6
- package/src/transport/request/state.ts +66 -38
- package/src/transport/request/tenant-preferences.ts +1 -1
- package/src/transport/routing/route-catalog.ts +41 -31
- package/src/transport/routing/router.ts +2 -2
- package/src/transport/surface/messages/parse.ts +1 -1
- package/src/workers/buddy-growth-tasks.ts +346 -425
- package/src/workers/daily-checkin.ts +17 -6
- package/src/workers/quota-refresh-sweep.ts +2 -2
- package/src/workers/scheduler.ts +17 -1
- package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-B_0FUKWc.js +0 -1
- package/dist/dashboard/assets/LoginPage-B_0FUKWc.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-DKVslqG3.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js +0 -1
- package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js +0 -7
- package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BjldQwIU.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-_bCaTM0v.js +0 -1
- package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CHVZqskZ.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DiFBymHj.js +0 -56
- package/dist/dashboard/assets/UsagePage-DiFBymHj.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DFY_fZU4.js.gz +0 -0
- package/dist/dashboard/assets/app-BOJ1_xUB.css +0 -2
- package/dist/dashboard/assets/app-BOJ1_xUB.css.gz +0 -0
- package/dist/dashboard/assets/assert-CwdnZARt.js.gz +0 -0
- package/dist/dashboard/assets/badge-Dr-uf0Au.js.gz +0 -0
- package/dist/dashboard/assets/banner-BddmMdoS.js.gz +0 -0
- package/dist/dashboard/assets/card-CK0PXWlX.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CcB6mGh2.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DGb2P9Yx.js.gz +0 -0
- package/dist/dashboard/assets/cog-CdWhsajs.js +0 -1
- package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
- package/dist/dashboard/assets/dialog-LkfuL50u.js.gz +0 -0
- package/dist/dashboard/assets/drawer-ZbEZ_84c.js.gz +0 -0
- package/dist/dashboard/assets/entry-BJfsVMvA.js.gz +0 -0
- package/dist/dashboard/assets/entry-CWBY5A0f.js.gz +0 -0
- package/dist/dashboard/assets/entry-CvC1-Zqx.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-BxpznFLt.js.gz +0 -0
- package/dist/dashboard/assets/index-CDzP3ovJ.js.gz +0 -0
- package/dist/dashboard/assets/inline-Ae13MQ5w.js.gz +0 -0
- package/dist/dashboard/assets/input-D23fo7cp.js.gz +0 -0
- package/dist/dashboard/assets/layout-CSsxjj5P.js.gz +0 -0
- package/dist/dashboard/assets/list-row-dmbwZMDY.js.gz +0 -0
- package/dist/dashboard/assets/live-BG1oUsZZ.js.gz +0 -0
- package/dist/dashboard/assets/mutation-DSF-e4ho.js.gz +0 -0
- package/dist/dashboard/assets/numbers-Bm_aECY7.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CF56nYIV.js.gz +0 -0
- package/dist/dashboard/assets/routing-CeA71n7G.js.gz +0 -0
- package/dist/dashboard/assets/select-KIT0rC6G.js.gz +0 -0
- package/dist/dashboard/assets/settings-B_2gkePf.js.gz +0 -0
- package/dist/dashboard/assets/stack-D7WOSzcX.js.gz +0 -0
- package/dist/dashboard/assets/state-BBxtKZOQ.js.gz +0 -0
- package/dist/dashboard/assets/switch-DNS_D5Ea.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BgoxdoMk.js.gz +0 -0
- /package/dist/dashboard/assets/{app-CaXZ9SCn.js → app-BaWd0puM.js} +0 -0
- /package/dist/dashboard/assets/{app-CaXZ9SCn.js.gz → app-BaWd0puM.js.gz} +0 -0
|
@@ -3,7 +3,7 @@ import type { Elysia } from "elysia";
|
|
|
3
3
|
import { and, eq, isNull } from "drizzle-orm";
|
|
4
4
|
import { adminAuditLog, providerAccounts } from "../../persistence/schema";
|
|
5
5
|
import { requireGlobalAdmin } from "../shared/errors";
|
|
6
|
-
import { GLOBAL_QUOTA_LENS, invalidateQuotaCache } from "
|
|
6
|
+
import { GLOBAL_QUOTA_LENS, invalidateQuotaCache } from "../../providers/quota/cache";
|
|
7
7
|
import { invalidateCredentialCache } from "../../providers/operations/provider-credential-service";
|
|
8
8
|
import {
|
|
9
9
|
QUOTA_REFRESH_TIMEOUT_MS,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
signalFetch,
|
|
13
13
|
timeoutSignal,
|
|
14
14
|
type QuotaRefreshDeps,
|
|
15
|
-
} from "
|
|
15
|
+
} from "../../providers/quota/refresh";
|
|
16
16
|
import { sanitizeGlobalAccount } from "./quota-view";
|
|
17
17
|
import {
|
|
18
18
|
ACCOUNT_STATUSES,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ConsoleAccessResolver } from "../auth/access";
|
|
2
2
|
import type { AuditRecorder } from "../auth/audit-recorder";
|
|
3
3
|
import { markClineApiKeyCredential } from "../../providers/integrations/cline/cline-quota";
|
|
4
|
-
import { type QuotaRefreshDeps } from "
|
|
4
|
+
import { type QuotaRefreshDeps } from "../../providers/quota/refresh";
|
|
5
5
|
import type { AccountTestEgressDeps } from "./account-test-egress";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
getCachedQuotaEntries,
|
|
15
15
|
getCachedQuotaEntry,
|
|
16
16
|
invalidateQuotaCache,
|
|
17
|
-
} from "
|
|
17
|
+
} from "../../providers/quota/cache";
|
|
18
18
|
import {
|
|
19
19
|
QUOTA_REFRESH_TIMEOUT_MS,
|
|
20
20
|
refreshAccountQuota,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
timeoutSignal,
|
|
23
23
|
type QuotaRefreshDeps,
|
|
24
24
|
type QuotaRefreshFailureCode,
|
|
25
|
-
} from "
|
|
25
|
+
} from "../../providers/quota/refresh";
|
|
26
26
|
import { selectAccountTestEgress } from "./account-test-egress";
|
|
27
27
|
import type { ValidatedFetch } from "../../network/outbound-fetch";
|
|
28
28
|
import type { ProviderId } from "../../providers/provider-contracts";
|
|
@@ -39,6 +39,10 @@ import {
|
|
|
39
39
|
type BuddyActivityReportResult,
|
|
40
40
|
type DailyCheckinResult,
|
|
41
41
|
} from "../../providers/integrations/buddy/buddy-checkin";
|
|
42
|
+
import {
|
|
43
|
+
fetchQoderDailyCheckin,
|
|
44
|
+
isQoderCheckinProvider,
|
|
45
|
+
} from "../../providers/integrations/qoder/qoder-checkin";
|
|
42
46
|
import { buddyAccountUid } from "../../providers/integrations/buddy/buddy-oauth-shared";
|
|
43
47
|
import {
|
|
44
48
|
supportsAccountReset,
|
|
@@ -99,12 +103,20 @@ function toBuddyUsageView(
|
|
|
99
103
|
};
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
/** One account's check-in through the buddy billing facade. */
|
|
106
|
+
/** One account's check-in through the buddy billing facade (Qoder uses its own surface). */
|
|
103
107
|
async function runAccountCheckin(
|
|
104
108
|
account: { id: string; providerId: string },
|
|
105
109
|
credential: string,
|
|
106
110
|
forceClaim: boolean,
|
|
107
111
|
): Promise<DailyCheckinResult> {
|
|
112
|
+
if (isQoderCheckinProvider(account.providerId)) {
|
|
113
|
+
return fetchQoderDailyCheckin({
|
|
114
|
+
providerId: account.providerId,
|
|
115
|
+
accountId: account.id,
|
|
116
|
+
credential,
|
|
117
|
+
fetcher: globalThis.fetch,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
108
120
|
return fetchDailyCheckin({
|
|
109
121
|
providerId: account.providerId,
|
|
110
122
|
accountId: account.id,
|
|
@@ -153,10 +165,14 @@ async function runDailyGrowthForAccount(
|
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
// Report leg: needs a uid, which only a JWT carries, so an opaque API key
|
|
156
|
-
// fails closed instead of reporting under another account's identity.
|
|
168
|
+
// fails closed instead of reporting under another account's identity. Qoder
|
|
169
|
+
// has no activity-report leg — its daily grant is purely the check-in.
|
|
170
|
+
const qoderAccount = isQoderCheckinProvider(String(providerId));
|
|
157
171
|
const userId = buddyAccountUid(credential);
|
|
158
172
|
let report: { ok: boolean; message: string };
|
|
159
|
-
if (
|
|
173
|
+
if (qoderAccount) {
|
|
174
|
+
report = { ok: true, message: "Qoder has no activity-report leg" };
|
|
175
|
+
} else if (userId === undefined || userId.length === 0) {
|
|
160
176
|
report = { ok: false, message: "Report needs an OAuth account (no uid in this credential)" };
|
|
161
177
|
} else {
|
|
162
178
|
try {
|
|
@@ -180,13 +196,20 @@ async function runDailyGrowthForAccount(
|
|
|
180
196
|
|
|
181
197
|
let checkin: { ok: boolean; message: string };
|
|
182
198
|
try {
|
|
183
|
-
const result =
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
199
|
+
const result = qoderAccount
|
|
200
|
+
? await fetchQoderDailyCheckin({
|
|
201
|
+
providerId: String(providerId),
|
|
202
|
+
accountId: account.id,
|
|
203
|
+
credential,
|
|
204
|
+
fetcher,
|
|
205
|
+
})
|
|
206
|
+
: await fetchDailyCheckin({
|
|
207
|
+
providerId,
|
|
208
|
+
accountId: account.id,
|
|
209
|
+
credential,
|
|
210
|
+
forceClaim: true,
|
|
211
|
+
fetcher,
|
|
212
|
+
});
|
|
190
213
|
checkin =
|
|
191
214
|
result.state === "error"
|
|
192
215
|
? { ok: false, message: result.error ?? "Daily check-in failed" }
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { providerAccounts } from "../../persistence/schema";
|
|
2
2
|
import type { ProviderQuotaResult } from "../../providers/quota/quota-contracts";
|
|
3
3
|
import { log } from "../../observability/logger";
|
|
4
|
-
import { GLOBAL_QUOTA_LENS } from "
|
|
4
|
+
import { GLOBAL_QUOTA_LENS } from "../../providers/quota/cache";
|
|
5
5
|
import {
|
|
6
6
|
QUOTA_REFRESH_TIMEOUT_MS,
|
|
7
7
|
refreshAccountQuota,
|
|
8
8
|
signalFetch,
|
|
9
9
|
type QuotaRefreshDeps,
|
|
10
10
|
type QuotaRefreshTarget,
|
|
11
|
-
} from "
|
|
11
|
+
} from "../../providers/quota/refresh";
|
|
12
12
|
|
|
13
13
|
/** Shape returned for quota data on dashboard account responses. */
|
|
14
14
|
export interface QuotaRefreshView {
|
|
@@ -221,7 +221,7 @@ export type PoolStatus = (typeof POOL_STATUSES)[number];
|
|
|
221
221
|
* direct callers against the same list (mirrors `POOL_STATUSES`). Values must
|
|
222
222
|
* stay aligned with the `pool_routing_strategy` enum in `schema.ts`.
|
|
223
223
|
*/
|
|
224
|
-
export const POOL_ROUTING_STRATEGIES = ["least_loaded", "round_robin"] as const;
|
|
224
|
+
export const POOL_ROUTING_STRATEGIES = ["least_loaded", "round_robin", "random"] as const;
|
|
225
225
|
export type PoolRoutingStrategyValue = (typeof POOL_ROUTING_STRATEGIES)[number];
|
|
226
226
|
|
|
227
227
|
export interface PoolStrategySetting {
|
|
@@ -351,12 +351,22 @@ export interface NetworkPoolStore {
|
|
|
351
351
|
list(tenantId: string): Promise<readonly NetworkPoolRecord[]>;
|
|
352
352
|
get(tenantId: string, poolId: string): Promise<NetworkPoolRecord | undefined>;
|
|
353
353
|
create(record: NetworkPoolRecord): Promise<void>;
|
|
354
|
+
/** Inserts many rows in one statement; returns how many landed. */
|
|
355
|
+
createMany?(records: readonly NetworkPoolRecord[]): Promise<number>;
|
|
354
356
|
update(
|
|
355
357
|
tenantId: string,
|
|
356
358
|
poolId: string,
|
|
357
359
|
patch: Partial<NetworkPoolRecord>,
|
|
358
360
|
): Promise<NetworkPoolRecord | undefined>;
|
|
361
|
+
/** Marks many rows `deleted`; returns how many matched. */
|
|
362
|
+
updateManyStatus?(
|
|
363
|
+
tenantId: string,
|
|
364
|
+
poolIds: readonly string[],
|
|
365
|
+
status: NetworkPoolRecord["status"],
|
|
366
|
+
): Promise<number>;
|
|
359
367
|
delete(tenantId: string, poolId: string): Promise<boolean>;
|
|
368
|
+
/** Deletes many rows in one statement; returns how many matched. */
|
|
369
|
+
deleteMany?(tenantId: string, poolIds: readonly string[]): Promise<number>;
|
|
360
370
|
healthCheck(tenantId: string, poolId: string): Promise<HealthCheckResult>;
|
|
361
371
|
/** Measures download throughput through the pool; never throws for a slow
|
|
362
372
|
* or refused transfer — a failed measurement is itself the result. */
|
|
@@ -379,6 +389,36 @@ export interface NetworkPoolStore {
|
|
|
379
389
|
): Promise<PoolProviderProbeResult>;
|
|
380
390
|
listHealthEvents(tenantId: string, poolId: string): Promise<readonly PoolHealthEvent[]>;
|
|
381
391
|
recover(tenantId: string, poolId: string): Promise<boolean>;
|
|
392
|
+
/**
|
|
393
|
+
* Drops the durable provider-cooldown markers on one pool row — the mirror
|
|
394
|
+
* the selector restores from at boot. The in-memory/Redis state is cleared
|
|
395
|
+
* separately by the selector; this keeps the two from drifting after a
|
|
396
|
+
* restart. `onlyBlocked` limits the drop to retired-egress markers, mirroring
|
|
397
|
+
* `clearProviderBlockedOnly`: a 429 probe may prove an address was never
|
|
398
|
+
* retired but must not release its live cooldown. Optional (and a no-op in
|
|
399
|
+
* tests without a backing row store).
|
|
400
|
+
*/
|
|
401
|
+
dropProviderCooldownMarker?(
|
|
402
|
+
tenantId: string,
|
|
403
|
+
poolId: string,
|
|
404
|
+
providerId: string,
|
|
405
|
+
onlyBlocked: boolean,
|
|
406
|
+
): Promise<void>;
|
|
407
|
+
/** Drops every provider's marker on one row (disable/replace paths). */
|
|
408
|
+
dropAllProviderCooldownMarkers?(tenantId: string, poolId: string): Promise<void>;
|
|
409
|
+
/**
|
|
410
|
+
* Writes a probe-measured 429 into the durable mirror and the pool's health
|
|
411
|
+
* history. The probe is the only writer that can *flag* a cooldown from the
|
|
412
|
+
* control plane — its 429 is a fresh measurement of the same condition a
|
|
413
|
+
* dispatch-side 429 records, so it lands in both stores and appears as
|
|
414
|
+
* activity. Optional; absent in tests without a backing row store.
|
|
415
|
+
*/
|
|
416
|
+
recordProviderProbeCooldown?(
|
|
417
|
+
tenantId: string,
|
|
418
|
+
poolId: string,
|
|
419
|
+
providerId: string,
|
|
420
|
+
marker: { until: number; reason: string; blocked?: boolean },
|
|
421
|
+
): Promise<void>;
|
|
382
422
|
/** Per-tenant pool-group selection strategy; an absent row reads as
|
|
383
423
|
* `DEFAULT_POOL_STRATEGY`. */
|
|
384
424
|
getStrategy(tenantId: string): Promise<PoolStrategySetting>;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// behind the routes. Depends only on the contracts and the shared helpers;
|
|
3
3
|
// `routes.ts` is the only HTTP surface.
|
|
4
4
|
import { ConsoleDomainError, requireTenantScope } from "../../shared/errors";
|
|
5
|
+
import { providerRateLimitIsIpScoped } from "../../../providers/provider-metadata";
|
|
6
|
+
import { resolvePoolProviderRateLimitCooldownMs } from "../../../config/cooldowns";
|
|
5
7
|
import type { PoolHealthEvent } from "../../../network/pool-health-machine";
|
|
6
8
|
import type { AccessDecision } from "../../../security/access-control";
|
|
7
9
|
import {
|
|
@@ -28,6 +30,7 @@ import {
|
|
|
28
30
|
type NetworkPoolConfig,
|
|
29
31
|
type NetworkPoolRecord,
|
|
30
32
|
type NetworkPoolResponse,
|
|
33
|
+
type PoolStatus,
|
|
31
34
|
type PoolBatchProbeResult,
|
|
32
35
|
type PoolSpeedTestResult,
|
|
33
36
|
type PoolStrategySetting,
|
|
@@ -194,6 +197,130 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
|
|
|
194
197
|
}
|
|
195
198
|
return { created, skipped };
|
|
196
199
|
},
|
|
200
|
+
async createPoolBatch(
|
|
201
|
+
access: AccessDecision | undefined,
|
|
202
|
+
requests: readonly CreateNetworkPoolRequest[],
|
|
203
|
+
): Promise<{ created: NetworkPoolResponse[]; failed: number }> {
|
|
204
|
+
const a = requireTenantScope(access, "dashboard:write");
|
|
205
|
+
let failed = 0;
|
|
206
|
+
const records: NetworkPoolRecord[] = [];
|
|
207
|
+
for (const request of requests) {
|
|
208
|
+
try {
|
|
209
|
+
const normalizedConfig = normalizePoolConfig(request.config);
|
|
210
|
+
const endpoint = request.endpoint;
|
|
211
|
+
validateEndpoint(request.kind, endpoint, config.ssrfPolicy);
|
|
212
|
+
validateTransportConfig(request.kind, normalizedConfig);
|
|
213
|
+
validatePoolLimits(request.maxInflight, request.weight);
|
|
214
|
+
records.push({
|
|
215
|
+
id: crypto.randomUUID(),
|
|
216
|
+
kind: request.kind,
|
|
217
|
+
endpoint,
|
|
218
|
+
maxInflight: request.maxInflight ?? 10,
|
|
219
|
+
weight: request.weight ?? 100,
|
|
220
|
+
status: "active",
|
|
221
|
+
inflight: 0,
|
|
222
|
+
consecutiveFailures: 0,
|
|
223
|
+
tenantId: a.tenantId,
|
|
224
|
+
createdAt: new Date().toISOString(),
|
|
225
|
+
...(request.label === undefined ? {} : { label: request.label }),
|
|
226
|
+
...(Object.keys(normalizedConfig).length > 0 ? { config: normalizedConfig } : {}),
|
|
227
|
+
...(request.credential === undefined ? {} : { credential: request.credential }),
|
|
228
|
+
...(request.credential !== undefined ? { hasCredential: true } : {}),
|
|
229
|
+
...(typeof request.quotaBytes === "number" ? { quotaBytes: request.quotaBytes } : {}),
|
|
230
|
+
...(request.family === undefined ? {} : { family: request.family }),
|
|
231
|
+
});
|
|
232
|
+
} catch {
|
|
233
|
+
failed += 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// All rows land in one statement: a long paste was one round trip per
|
|
237
|
+
// proxy, which made a 40-line import take minutes.
|
|
238
|
+
let created: NetworkPoolResponse[] = [];
|
|
239
|
+
if (records.length > 0) {
|
|
240
|
+
if (config.store.createMany) {
|
|
241
|
+
await config.store.createMany(records);
|
|
242
|
+
} else {
|
|
243
|
+
for (const record of records) await config.store.create(record);
|
|
244
|
+
}
|
|
245
|
+
created = records.map((record) => sanitizePoolResponse(record));
|
|
246
|
+
await config.auditSink?.record({
|
|
247
|
+
access: a,
|
|
248
|
+
action: "network_pool.created",
|
|
249
|
+
target: created[0]!.id,
|
|
250
|
+
detail: { count: created.length, failed },
|
|
251
|
+
});
|
|
252
|
+
await config.snapshotInvalidator?.invalidate();
|
|
253
|
+
}
|
|
254
|
+
return { created, failed };
|
|
255
|
+
},
|
|
256
|
+
async deletePoolBatch(
|
|
257
|
+
access: AccessDecision | undefined,
|
|
258
|
+
poolIds: readonly string[],
|
|
259
|
+
): Promise<{ deleted: number; failed: number }> {
|
|
260
|
+
const a = requireTenantScope(access, "dashboard:write");
|
|
261
|
+
let ok = 0;
|
|
262
|
+
if (config.store.deleteMany) {
|
|
263
|
+
ok = await config.store.deleteMany(a.tenantId, poolIds);
|
|
264
|
+
} else {
|
|
265
|
+
for (const poolId of poolIds) {
|
|
266
|
+
try {
|
|
267
|
+
if (await config.store.delete(a.tenantId, poolId)) ok += 1;
|
|
268
|
+
} catch {
|
|
269
|
+
// Missing rows are not failures for a bulk delete.
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Cooldown markers and cached agents are best-effort here: the rows are
|
|
274
|
+
// already gone, so a failed cleanup must not report a failed delete.
|
|
275
|
+
await Promise.all(
|
|
276
|
+
poolIds.map(async (poolId) => {
|
|
277
|
+
await config.poolAgentReleaser?.releasePool(poolId, a.tenantId).catch(() => undefined);
|
|
278
|
+
await config.poolSelector?.clearPoolCooldowns(poolId).catch(() => undefined);
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
await config.auditSink?.record({
|
|
282
|
+
access: a,
|
|
283
|
+
action: "network_pool.deleted",
|
|
284
|
+
target: poolIds[0] ?? "",
|
|
285
|
+
detail: { count: ok, failed: poolIds.length - ok },
|
|
286
|
+
});
|
|
287
|
+
if (ok > 0) await config.snapshotInvalidator?.invalidate();
|
|
288
|
+
return { deleted: ok, failed: poolIds.length - ok };
|
|
289
|
+
},
|
|
290
|
+
async setPoolStatusBatch(
|
|
291
|
+
access: AccessDecision | undefined,
|
|
292
|
+
poolIds: readonly string[],
|
|
293
|
+
status: PoolStatus,
|
|
294
|
+
): Promise<{ updated: number }> {
|
|
295
|
+
const a = requireTenantScope(access, "dashboard:write");
|
|
296
|
+
let updated = 0;
|
|
297
|
+
if (config.store.updateManyStatus) {
|
|
298
|
+
updated = await config.store.updateManyStatus(a.tenantId, poolIds, status);
|
|
299
|
+
} else {
|
|
300
|
+
for (const poolId of poolIds) {
|
|
301
|
+
try {
|
|
302
|
+
if (await config.store.update(a.tenantId, poolId, { status })) updated += 1;
|
|
303
|
+
} catch {
|
|
304
|
+
// Missing rows are not failures for a bulk status change.
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Any mutation can invalidate the cached dial agent; the batch releases
|
|
309
|
+
// each selected pool's agent so the next dispatch rebuilds fresh.
|
|
310
|
+
await Promise.all(
|
|
311
|
+
poolIds.map(async (poolId) => {
|
|
312
|
+
await config.poolAgentReleaser?.releasePool(poolId, a.tenantId).catch(() => undefined);
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
315
|
+
await config.auditSink?.record({
|
|
316
|
+
access: a,
|
|
317
|
+
action: "network_pool.updated",
|
|
318
|
+
target: poolIds[0] ?? "",
|
|
319
|
+
detail: { status, count: updated },
|
|
320
|
+
});
|
|
321
|
+
if (updated > 0) await config.snapshotInvalidator?.invalidate();
|
|
322
|
+
return { updated };
|
|
323
|
+
},
|
|
197
324
|
async createPool(
|
|
198
325
|
access: AccessDecision | undefined,
|
|
199
326
|
request: CreateNetworkPoolRequest,
|
|
@@ -255,6 +382,15 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
|
|
|
255
382
|
validateEndpoint(effectiveKind, effectiveEndpoint, config.ssrfPolicy);
|
|
256
383
|
validateTransportConfig(effectiveKind, normalizedConfig);
|
|
257
384
|
request = { ...request, endpoint: effectiveEndpoint, config: normalizedConfig };
|
|
385
|
+
// A changed endpoint is a different egress address, so its cooldowns
|
|
386
|
+
// — like its probe verdict — no longer describe this pool. Drop the
|
|
387
|
+
// durable markers with the volatile ones, or a replaced proxy would
|
|
388
|
+
// come back online and immediately be skipped because the previous
|
|
389
|
+
// address's flags were restored at boot.
|
|
390
|
+
if (request.endpoint !== undefined && request.endpoint !== existing.endpoint) {
|
|
391
|
+
await config.poolSelector?.clearPoolCooldowns(poolId).catch(() => undefined);
|
|
392
|
+
await config.store.dropAllProviderCooldownMarkers?.(a.tenantId, poolId);
|
|
393
|
+
}
|
|
258
394
|
}
|
|
259
395
|
const updated = await config.store.update(
|
|
260
396
|
a.tenantId,
|
|
@@ -380,18 +516,61 @@ export function createNetworkPoolOperations(config: NetworkPoolConfig) {
|
|
|
380
516
|
// still picked a red one": the red was a colour, not a decision.
|
|
381
517
|
//
|
|
382
518
|
// `blocked` retires the egress for this provider, so the next dispatch
|
|
383
|
-
// skips it.
|
|
519
|
+
// skips it. `allowed` clears a previous block, because the address is
|
|
384
520
|
// serving again — an operator who replaced a proxy must not have to find
|
|
385
521
|
// a separate button to un-retire it.
|
|
522
|
+
//
|
|
523
|
+
// `blocked` retires the egress — but only for a provider whose limits
|
|
524
|
+
// follow the address. `flagProviderBlocked` refuses everyone else, so the
|
|
525
|
+
// verdict reported back is what actually happened, not what was asked.
|
|
386
526
|
if (result.status === "blocked") {
|
|
387
|
-
await config.poolSelector.flagProviderBlocked(
|
|
527
|
+
const entry = await config.poolSelector.flagProviderBlocked(
|
|
388
528
|
poolId,
|
|
389
529
|
providerId,
|
|
390
530
|
"Upstream refused this egress address",
|
|
391
531
|
);
|
|
392
|
-
|
|
532
|
+
if (entry.blocked === true) {
|
|
533
|
+
// The retirement claim lands in the mirror and the health history
|
|
534
|
+
// with its provider named, like every other verdict — an enforced
|
|
535
|
+
// state the operator cannot see is the "colour, not a decision"
|
|
536
|
+
// failure mode again.
|
|
537
|
+
await config.store.recordProviderProbeCooldown?.(a.tenantId, poolId, providerId, {
|
|
538
|
+
until: entry.until,
|
|
539
|
+
reason: entry.reason,
|
|
540
|
+
blocked: true,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return { ...result, blockedPool: entry.blocked === true };
|
|
544
|
+
}
|
|
545
|
+
// A measured success releases the address — in rotation and in the
|
|
546
|
+
// durable mirror the next boot restores from.
|
|
547
|
+
if (result.status === "allowed") {
|
|
548
|
+
await config.poolSelector.clearProviderCooldown(poolId, providerId);
|
|
549
|
+
await config.store.dropProviderCooldownMarker?.(a.tenantId, poolId, providerId, false);
|
|
550
|
+
return { ...result, blockedPool: false };
|
|
551
|
+
}
|
|
552
|
+
// `throttled` (429) proves the address is *serving* — its rate limiter
|
|
553
|
+
// answered — so a stale "retired" claim is corrected. And the throttle
|
|
554
|
+
// itself is a measurement: the egress just demonstrated it is out of
|
|
555
|
+
// allowance, which is exactly when dispatch should skip it **and** the
|
|
556
|
+
// operator sees why. Flag the same (configured) window a dispatch-side
|
|
557
|
+
// 429 would, in rotation memory *and* in the durable mirror, plus the
|
|
558
|
+
// health event the activity panel lists. Only a 429 gap was leaving a
|
|
559
|
+
// rate-limited pool invisible and unflagged.
|
|
560
|
+
if (result.status === "throttled") {
|
|
561
|
+
await config.poolSelector.clearProviderBlockedOnly(poolId, providerId);
|
|
562
|
+
await config.store.dropProviderCooldownMarker?.(a.tenantId, poolId, providerId, true);
|
|
563
|
+
if (providerRateLimitIsIpScoped(providerId)) {
|
|
564
|
+
const durationMs = resolvePoolProviderRateLimitCooldownMs();
|
|
565
|
+
const reason = "Operator probe measured a provider 429";
|
|
566
|
+
await config.poolSelector.flagProviderCooldown(poolId, providerId, durationMs, reason);
|
|
567
|
+
await config.store.recordProviderProbeCooldown?.(a.tenantId, poolId, providerId, {
|
|
568
|
+
until: Date.now() + durationMs,
|
|
569
|
+
reason,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
393
572
|
}
|
|
394
|
-
|
|
573
|
+
// `unreachable` learned nothing about the address, so nothing changes.
|
|
395
574
|
return { ...result, blockedPool: false };
|
|
396
575
|
},
|
|
397
576
|
/**
|
|
@@ -63,7 +63,14 @@ const handler = {
|
|
|
63
63
|
}
|
|
64
64
|
try {
|
|
65
65
|
const response = await fetch(targetUrl, init);
|
|
66
|
-
|
|
66
|
+
// Forward the body verbatim. Content-encoding/content-length must be
|
|
67
|
+
// stripped: the relay's own runtime already decoded the stream it hands
|
|
68
|
+
// us, so re-sending the original gzip headers makes the caller decompress
|
|
69
|
+
// twice (ZlibError). The body bytes are already plaintext here.
|
|
70
|
+
const headers = new Headers(response.headers);
|
|
71
|
+
headers.delete("content-encoding");
|
|
72
|
+
headers.delete("content-length");
|
|
73
|
+
return new Response(response.body, { status: response.status, headers });
|
|
67
74
|
} catch (error) {
|
|
68
75
|
return new Response(JSON.stringify({ error: String((error && error.message) || error) }), {
|
|
69
76
|
status: 502,
|
|
@@ -217,10 +224,14 @@ async function deployVercel(
|
|
|
217
224
|
// PATCH is used rather than POST /v1/projects because the project already
|
|
218
225
|
// exists once the first deployment lands; the first deploy below creates it,
|
|
219
226
|
// and this call is what pins its region before that deploy runs its build.
|
|
227
|
+
//
|
|
228
|
+
// `ssoProtection: null` disables Vercel Authentication (SSO) on the project.
|
|
229
|
+
// Without it every deployment is protected and answers `302 → vercel.com/
|
|
230
|
+
// sso-api` to anonymous callers — the relay URL exists but is not public.
|
|
220
231
|
await fetchImpl(`https://api.vercel.com/v9/projects/${encodeURIComponent(projectName)}`, {
|
|
221
232
|
method: "PATCH",
|
|
222
233
|
headers: auth,
|
|
223
|
-
body: JSON.stringify({ serverlessFunctionRegion: region }),
|
|
234
|
+
body: JSON.stringify({ serverlessFunctionRegion: region, ssoProtection: null }),
|
|
224
235
|
}).catch(() => undefined);
|
|
225
236
|
const deployRes = await fetchImpl("https://api.vercel.com/v13/deployments", {
|
|
226
237
|
method: "POST",
|
|
@@ -243,11 +254,13 @@ async function deployVercel(
|
|
|
243
254
|
throw new GatewayError("platform_unavailable", deployRes.status, `Vercel deploy failed: ${await readError(deployRes)}`);
|
|
244
255
|
// Pin the region again after the deploy created the project: the PATCH above
|
|
245
256
|
// is a no-op on first deploy (the project does not exist yet), and a redeploy
|
|
246
|
-
// is what Vercel requires for a region change to take effect anyway.
|
|
257
|
+
// is what Vercel requires for a region change to take effect anyway. The SSO
|
|
258
|
+
// protection reset is repeated for the same reason — the first PATCH had no
|
|
259
|
+
// project to act on.
|
|
247
260
|
await fetchImpl(`https://api.vercel.com/v9/projects/${encodeURIComponent(projectName)}`, {
|
|
248
261
|
method: "PATCH",
|
|
249
262
|
headers: auth,
|
|
250
|
-
body: JSON.stringify({ serverlessFunctionRegion: region }),
|
|
263
|
+
body: JSON.stringify({ serverlessFunctionRegion: region, ssoProtection: null }),
|
|
251
264
|
}).catch(() => undefined);
|
|
252
265
|
await fetchImpl("https://api.vercel.com/v13/deployments", {
|
|
253
266
|
method: "POST",
|
|
@@ -87,6 +87,36 @@ export function createNetworkPoolRoutes(config: NetworkPoolConfig): Elysia {
|
|
|
87
87
|
.delete("/:poolId", async ({ request, params }) => {
|
|
88
88
|
return await factory.deletePool(config.accessResolver(request), params.poolId);
|
|
89
89
|
})
|
|
90
|
+
.post(
|
|
91
|
+
"/bulk/create",
|
|
92
|
+
{ body: t.Object({ targets: t.Array(createPoolBody, { minItems: 1, maxItems: MAX_BATCH_PROBE_TARGETS }) }) },
|
|
93
|
+
async ({ request, body, set }) => {
|
|
94
|
+
set.status = 200;
|
|
95
|
+
return await factory.createPoolBatch(
|
|
96
|
+
config.accessResolver(request),
|
|
97
|
+
body.targets as CreateNetworkPoolRequest[],
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
.post(
|
|
102
|
+
"/bulk/delete",
|
|
103
|
+
{ body: t.Object({ ids: t.Array(t.String(), { minItems: 1 }) }) },
|
|
104
|
+
async ({ request, body }) => {
|
|
105
|
+
return await factory.deletePoolBatch(config.accessResolver(request), body.ids);
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
.post(
|
|
109
|
+
"/bulk/status",
|
|
110
|
+
{
|
|
111
|
+
body: t.Object({
|
|
112
|
+
ids: t.Array(t.String(), { minItems: 1 }),
|
|
113
|
+
status: literalUnion(POOL_STATUSES),
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
async ({ request, body }) => {
|
|
117
|
+
return await factory.setPoolStatusBatch(config.accessResolver(request), body.ids, body.status);
|
|
118
|
+
},
|
|
119
|
+
)
|
|
90
120
|
.get("/:poolId/health-events", async ({ request, params }) => {
|
|
91
121
|
return await factory.listPoolHealthEvents(config.accessResolver(request), params.poolId);
|
|
92
122
|
})
|