cartethyia 2.5.3 → 2.5.6
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 +13 -4
- package/README.md +2 -2
- package/dist/dashboard/assets/{BannedPage-hfCzi-Ux.js → BannedPage-CADDq4IU.js} +1 -1
- package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DEDyKBlC.js → CliToolsPage-DuO1Z7dq.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
- package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-bsLuZmYH.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-_UWhwziJ.js → CustomizationPage-BR5HHvbi.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DDVIfZGW.js → LoginPage-V5faV9V2.js} +1 -1
- package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
- package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
- package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
- package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-D6pmQ86L.js → SetupPage-UB690SE2.js} +1 -1
- package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-DWvKiuSl.js → SortableList-BLujdX7N.js} +1 -1
- package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CmkUrgdj.js → StudioPage-D5C2raAc.js} +3 -3
- package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
- package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
- package/dist/dashboard/assets/{assert-NHwYq95t.js → assert-C9FD_JwJ.js} +1 -1
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/{badge-6uzDDvqs.js → badge-CR72dasT.js} +1 -1
- package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
- package/dist/dashboard/assets/{banner-4_WNg2WZ.js → banner-CI9_eSfO.js} +1 -1
- package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
- package/dist/dashboard/assets/{card--AtFedAq.js → card-DS7CQEsO.js} +1 -1
- package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-B3e7dx3N.js → cli-tools-CNqzrfdb.js} +1 -1
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-NreUx6c8.js → clipboard-button-611pSsaX.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
- package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/database-Dz9N3zGd.js +1 -0
- package/dist/dashboard/assets/database-Dz9N3zGd.js.gz +0 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
- package/dist/dashboard/assets/entry-B9Xvtnfj.js +1 -0
- package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
- package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
- package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-D_eM3IgG.js → github-badge-sltk1qUi.js} +1 -1
- package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
- package/dist/dashboard/assets/{index-BydyKaDz.js → index-CmxZr14F.js} +2 -2
- package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BcNGF8Gr.js → inline-BhY2sSnO.js} +1 -1
- package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
- package/dist/dashboard/assets/{input-BG41et9G.js → input-V1umFiBk.js} +1 -1
- package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Gh_W4OR_.js → list-row-1zzZYM0B.js} +1 -1
- package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
- package/dist/dashboard/assets/{live-BJGYRBnR.js → live-BivGfuUs.js} +1 -1
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js +1 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-BkoLM5Ba.js → mutation-46P0oWMe.js} +1 -1
- package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-D7wX2vM5.js → query-keys-BFhGpwR1.js} +1 -1
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CcusqXA7.js → routing-CAb4F37R.js} +1 -1
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/{select-BO6h8yNv.js → select-BYR_5K2c.js} +2 -2
- package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
- package/dist/dashboard/assets/{settings-mpmXTgUa.js → settings-DNX9sydi.js} +1 -1
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DwLmjUFK.js → stack-BHTfXdz_.js} +1 -1
- package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
- package/dist/dashboard/assets/{state-DYdQmdAl.js → state-mPDRlQkl.js} +1 -1
- package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOmJxg7H.js +1 -0
- package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
- package/dist/dashboard/assets/terminal-0_IjPWTk.js +1 -0
- package/dist/dashboard/assets/terminal-0_IjPWTk.js.gz +0 -0
- package/dist/dashboard/assets/tokens-COzario9.js +1 -0
- package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
- package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
- package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-w4u4CVue.js → useMutation-Dem0lXQj.js} +1 -1
- package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
- package/dist/dashboard/assets/zap-C4EeVW4q.js +1 -0
- package/dist/dashboard/assets/zap-C4EeVW4q.js.gz +0 -0
- package/dist/dashboard/donation/qris-risun-marke.png +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +57 -57
- package/migrations/README.md +52 -52
- package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
- package/package.json +96 -95
- package/scripts/cli/admin.ts +125 -0
- package/scripts/cli/autostart.ts +237 -0
- package/scripts/cli/{ui/background.ts → background.ts} +1 -1
- package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -248
- package/scripts/cli/doctor.ts +37 -0
- package/scripts/cli/{support/env.ts → env.ts} +99 -99
- package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
- package/scripts/cli/instance.ts +224 -0
- package/scripts/cli/main.ts +88 -18
- package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
- package/scripts/cli/port.ts +100 -0
- package/scripts/cli/{support/services.ts → services.ts} +88 -88
- package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
- package/scripts/cli/{command/start.ts → start.ts} +10 -5
- package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
- package/scripts/cli/tray/index.ts +62 -12
- package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
- package/scripts/cli/update.ts +40 -0
- package/scripts/cli/version.ts +67 -0
- package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
- package/src/boot/app.ts +11 -7
- package/src/boot/dependencies.ts +50 -19
- package/src/boot/timeout.ts +2 -13
- package/src/config/cooldowns.ts +186 -138
- package/src/config/network.ts +8 -14
- package/src/config/providers.ts +7 -10
- package/src/config/runtime.ts +76 -76
- package/src/config/security.ts +15 -0
- package/src/config/spec.ts +52 -7
- package/src/config/telemetry.ts +26 -0
- package/src/config/timeouts.ts +0 -6
- package/src/console/admin/model-abuse/contracts.ts +28 -6
- package/src/console/admin/model-abuse/routes.ts +56 -3
- package/src/console/api-keys/bucket-spend.ts +15 -4
- package/src/console/api-keys/contracts.ts +410 -414
- package/src/console/api-keys/routes.ts +881 -880
- package/src/console/auth/credentials.ts +3 -3
- package/src/console/auth/setup-service.ts +2 -2
- package/src/console/backup/contracts.ts +15 -2
- package/src/console/backup/export-endpoint.ts +6 -2
- package/src/console/backup/nine-router.ts +2 -5
- package/src/console/backup/service.ts +4 -0
- package/src/console/backup/store.ts +192 -29
- package/src/console/backup/validate.ts +3 -45
- package/src/console/cli-tools/fs-ops.ts +487 -487
- package/src/console/console-router-guards.ts +1 -1
- package/src/console/console-router.ts +8 -0
- package/src/console/data-management/contracts.ts +100 -0
- package/src/console/data-management/operations.ts +234 -0
- package/src/console/data-management/routes.ts +103 -0
- package/src/console/domain-registration.ts +45 -1
- package/src/console/observability/contracts.ts +524 -485
- package/src/console/observability/live.ts +22 -0
- package/src/console/observability/store.ts +1138 -1018
- package/src/console/observability/usage-dimensions.ts +4 -4
- package/src/console/providers/catalog/catalog-projections.ts +6 -7
- package/src/console/providers/catalog/contracts.ts +6 -0
- package/src/console/providers/catalog/model-operations.ts +5 -2
- package/src/console/providers/catalog/provider-operations.ts +24 -4
- package/src/console/providers/catalog/public-model-store.ts +8 -11
- package/src/console/providers/catalog/routes.ts +7 -0
- package/src/console/providers/catalog/store.ts +1553 -1532
- package/src/console/providers/oauth/routes.ts +8 -12
- package/src/console/quota/account-quota-shared.ts +52 -39
- package/src/console/quota/account-quota-tenant.ts +6 -6
- package/src/console/quota/quota-view.ts +1 -1
- package/src/console/quota/refresh.ts +441 -429
- package/src/console/remote-support/contracts.ts +61 -0
- package/src/console/remote-support/routes.ts +65 -0
- package/src/console/remote-support/service.ts +207 -0
- package/src/console/response-compression.ts +0 -3
- package/src/console/routing/pools/import-parse.ts +206 -0
- package/src/console/routing/pools/operations.ts +543 -501
- package/src/console/routing/pools/probe-result.ts +3 -2
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/routes.ts +181 -164
- package/src/console/routing/pools/store.ts +8 -8
- package/src/console/settings/contracts.ts +95 -2
- package/src/console/settings/store.ts +81 -3
- package/src/console/shared/cursor.ts +8 -8
- package/src/console/shared/errors.ts +1 -3
- package/src/console/updates/routes.ts +16 -0
- package/src/main.ts +10 -0
- package/src/network/http2-bidi.ts +2 -4
- package/src/network/pool/agent.ts +478 -478
- package/src/network/pool/selector.ts +5 -4
- package/src/network/retry.ts +1 -2
- package/src/network/ssrf.ts +5 -4
- package/src/observability/event-compaction.ts +317 -0
- package/src/observability/event-day-index.ts +440 -0
- package/src/observability/event-log.ts +155 -6
- package/src/observability/logger.ts +6 -6
- package/src/observability/payload-capture.ts +158 -33
- package/src/observability/payload-store.ts +82 -61
- package/src/observability/redaction.ts +5 -6
- package/src/observability/request-notify.ts +43 -0
- package/src/observability/telemetry-status.ts +8 -9
- package/src/observability/token-speed.ts +5 -4
- package/src/persistence/db-mode.ts +7 -7
- package/src/persistence/health-event-flush.ts +36 -0
- package/src/persistence/readiness.ts +1 -1
- package/src/persistence/schema.ts +75 -29
- package/src/persistence/shared-cache.ts +1 -2
- package/src/persistence/stores/api-key-store.ts +18 -6
- package/src/persistence/stores/share-store.ts +4 -3
- package/src/persistence/stores/telemetry-store.ts +33 -11
- package/src/protocol/canonical-model.ts +11 -3
- package/src/protocol/codecs.ts +139 -6
- package/src/protocol/gateway-error.ts +6 -4
- package/src/protocol/image-clamp.ts +19 -1
- package/src/protocol/registry.ts +8 -0
- package/src/protocol/request/chat.ts +21 -1
- package/src/protocol/request/codex.ts +10 -1
- package/src/protocol/request/gemini.ts +9 -2
- package/src/protocol/request/messages.ts +18 -7
- package/src/protocol/request/responses.ts +9 -1
- package/src/protocol/response/chat.ts +34 -4
- package/src/protocol/response/codex.ts +7 -4
- package/src/protocol/response/dsml.ts +140 -28
- package/src/protocol/response/responses.ts +11 -5
- package/src/protocol/stream-error-frames.ts +4 -5
- package/src/providers/adapters/compatible-adapter.ts +3 -0
- package/src/providers/authentication/jwt-validator.ts +9 -10
- package/src/providers/authentication/oauth-refresh-service.ts +9 -11
- package/src/providers/bundled-discovery-providers.ts +122 -119
- package/src/providers/configured-openai-providers.ts +4 -4
- package/src/providers/default-registry.ts +5 -4
- package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
- package/src/providers/discovery/catalog/catalog-format.ts +3 -6
- package/src/providers/discovery/models-dev-catalog.ts +354 -354
- package/src/providers/discovery/openai-model-discovery.ts +5 -0
- package/src/providers/discovery/openrouter-free-roster.ts +15 -17
- package/src/providers/discovery/probe-phases.ts +4 -4
- package/src/providers/discovery/probe-wire.ts +6 -13
- package/src/providers/discovery/probing-service.ts +1117 -1045
- package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
- package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
- package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
- package/src/providers/integrations/buddy/buddy-media.ts +2 -2
- package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
- package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
- package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
- package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
- package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
- package/src/providers/integrations/claude/claude.ts +608 -608
- package/src/providers/integrations/cline/cline-quota.ts +27 -20
- package/src/providers/integrations/cline/cline.ts +537 -539
- package/src/providers/integrations/codex/codex-headers.ts +25 -39
- package/src/providers/integrations/codex/codex-oauth.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +2 -29
- package/src/providers/integrations/commandcode/commandcode.ts +674 -675
- package/src/providers/integrations/commandcode/discovery.ts +117 -116
- package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
- package/src/providers/integrations/cursor/cursor-quota.ts +309 -0
- package/src/providers/integrations/cursor/cursor-request.ts +616 -471
- package/src/providers/integrations/cursor/cursor-session-manager.ts +319 -281
- package/src/providers/integrations/cursor/cursor.ts +1333 -960
- package/src/providers/integrations/cursor/protocol/messages.ts +25 -0
- package/src/providers/integrations/factory/factory-models.ts +3 -5
- package/src/providers/integrations/factory/factory.ts +8 -1
- package/src/providers/integrations/gemini.ts +6 -1
- package/src/providers/integrations/grok/grok-install-id.ts +2 -3
- package/src/providers/integrations/grok/grok-quota.ts +279 -177
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -0
- package/src/providers/integrations/grok/grok-ratelimit.ts +218 -0
- package/src/providers/integrations/grok/grok.ts +6 -7
- package/src/providers/integrations/hermes.ts +14 -18
- package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
- package/src/providers/integrations/kiro/kiro.ts +1 -1
- package/src/providers/integrations/mistral.ts +6 -6
- package/src/providers/integrations/opencode/fingerprint.ts +3 -3
- package/src/providers/integrations/opencode/provider.ts +19 -20
- package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
- package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
- package/src/providers/integrations/qoder/qoder-quota.ts +192 -0
- package/src/providers/integrations/qoder/qoder.ts +884 -865
- package/src/providers/integrations/shared/connect.ts +6 -6
- package/src/providers/integrations/trae/trae-catalog.ts +1 -2
- package/src/providers/integrations/trae/trae-quota.ts +384 -0
- package/src/providers/integrations/trae/trae.ts +2 -2
- package/src/providers/integrations/zcode/oauth.ts +6 -1
- package/src/providers/operations/account-health-service.ts +75 -27
- package/src/providers/operations/account-reset-service.ts +1 -1
- package/src/providers/operations/client-versions.ts +6 -6
- package/src/providers/operations/custom-cli-headers.ts +3 -4
- package/src/providers/operations/provider-catalog-seeder.ts +95 -95
- package/src/providers/operations/provider-catalog-service.ts +3 -4
- package/src/providers/provider-contracts.ts +7 -0
- package/src/providers/provider-metadata.ts +10 -8
- package/src/providers/provider-registry.ts +6 -7
- package/src/providers/provider-usage.ts +1 -1
- package/src/providers/quota/quota-support.ts +8 -0
- package/src/security/abuse.ts +15 -10
- package/src/security/access-control.ts +144 -146
- package/src/security/admission/contracts.ts +11 -0
- package/src/security/admission/in-memory-store.ts +4 -3
- package/src/security/admission/lease-sweep.ts +6 -6
- package/src/security/admission/reasons.ts +5 -1
- package/src/security/admission/redis-store.ts +211 -210
- package/src/security/admission/service.ts +4 -0
- package/src/security/api-key-auth.ts +27 -17
- package/src/security/ip-boundary.ts +155 -15
- package/src/security/model-abuse.ts +366 -94
- package/src/security/outbound-headers.ts +3 -4
- package/src/shared/sleep.ts +4 -0
- package/src/transport/dispatch/attempt-finalize.ts +47 -5
- package/src/transport/dispatch/attempt-loop.ts +531 -530
- package/src/transport/dispatch/generation.ts +6 -0
- package/src/transport/dispatch/proxy-request.ts +24 -1
- package/src/transport/dispatch/ratelimit-capture.ts +37 -0
- package/src/transport/dispatch/stream-bridge.ts +1 -3
- package/src/transport/dispatch/streaming/finalize.ts +1 -1
- package/src/transport/dispatch/streaming/stream.ts +19 -6
- package/src/transport/dispatch/upstream.ts +9 -0
- package/src/transport/failure-policy.ts +162 -8
- package/src/transport/guards/auth-guard.ts +7 -4
- package/src/transport/guards/concurrency-guard.ts +0 -5
- package/src/transport/guards/error-lifecycle.ts +13 -6
- package/src/transport/guards/pipeline.ts +15 -3
- package/src/transport/guards/request-context.ts +8 -2
- package/src/transport/request/inflight.ts +5 -6
- package/src/transport/request/preparer.ts +9 -8
- package/src/transport/request/state.ts +21 -8
- package/src/transport/request/tenant-preferences.ts +60 -0
- package/src/transport/routing/admission.ts +4 -0
- package/src/transport/routing/route-catalog.ts +65 -10
- package/src/transport/routing/route-model.ts +46 -14
- package/src/transport/routing/router.ts +21 -14
- package/src/transport/surface/completion.ts +2 -3
- package/src/transport/surface/dialects.ts +5 -6
- package/src/transport/surface/responses/encode.ts +38 -0
- package/src/transport/surface/responses/parse.ts +4 -0
- package/src/transport/translation/capabilities.ts +2 -3
- package/src/transport/translation/quirks.ts +3 -3
- package/src/workers/buddy-growth-tasks.ts +5 -10
- package/src/workers/daily-checkin.ts +17 -23
- package/src/workers/quota-refresh-sweep.ts +26 -26
- package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js +0 -1
- package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js +0 -1
- package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-B53YYMng.js +0 -1
- package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +0 -7
- package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +0 -1
- package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +0 -1
- package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-B47uOydr.js +0 -56
- package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-uNywYlfv.js +0 -1
- package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
- package/dist/dashboard/assets/app-DxivpfYG.css +0 -2
- package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
- package/dist/dashboard/assets/assert-NHwYq95t.js.gz +0 -0
- package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
- package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
- package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BtpsJU98.js +0 -1
- package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js +0 -1
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/drawer-WwwtzihF.js +0 -1
- package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
- package/dist/dashboard/assets/entry-CkryrIea.js +0 -4
- package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
- package/dist/dashboard/assets/entry-TJIye_jT.js +0 -1
- package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
- package/dist/dashboard/assets/entry-jy9_4-Ke.js +0 -1
- package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
- package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
- package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
- package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
- package/dist/dashboard/assets/layout-C8mer_Ak.js +0 -1
- package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
- package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-DeVLDA7I.js +0 -1
- package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js +0 -2
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
- package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
- package/dist/dashboard/assets/select-BO6h8yNv.js.gz +0 -0
- package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js +0 -1
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
- package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
- package/dist/dashboard/assets/switch-CidSfbG7.js +0 -1
- package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +0 -1
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
- package/scripts/cli/command/admin.ts +0 -65
- package/scripts/cli/command/doctor.ts +0 -257
- package/scripts/cli/command/update.ts +0 -23
- package/scripts/cli/gateway/drain-token.ts +0 -78
- package/scripts/cli/gateway/instance-lock.ts +0 -105
- package/scripts/cli/gateway/stop.ts +0 -51
- package/scripts/cli/support/bun.ts +0 -27
- package/scripts/cli/support/version.ts +0 -37
- /package/dist/dashboard/assets/{app-CafWtVXA.js → app-BIt446yE.js} +0 -0
- /package/dist/dashboard/assets/{app-CafWtVXA.js.gz → app-BIt446yE.js.gz} +0 -0
- /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
|
@@ -1,501 +1,543 @@
|
|
|
1
|
-
// Network-pool control-plane operations: the store-backed mutations and reads
|
|
2
|
-
// behind the routes. Depends only on the contracts and the shared helpers;
|
|
3
|
-
// `routes.ts` is the only HTTP surface.
|
|
4
|
-
import { ConsoleDomainError, requireTenantScope } from "../../shared/errors";
|
|
5
|
-
import type { PoolHealthEvent } from "../../../network/pool-health-machine";
|
|
6
|
-
import type { AccessDecision } from "../../../security/access-control";
|
|
7
|
-
import {
|
|
8
|
-
deployRelay as deployRelayWorker,
|
|
9
|
-
isRelayTarget,
|
|
10
|
-
type RelayDeployRequest,
|
|
11
|
-
} from "./relay-deploy";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
type
|
|
25
|
-
type
|
|
26
|
-
type
|
|
27
|
-
type
|
|
28
|
-
type
|
|
29
|
-
type
|
|
30
|
-
type
|
|
31
|
-
type
|
|
32
|
-
type
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
await config.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
request
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
await config.
|
|
228
|
-
await config.auditSink?.record({
|
|
229
|
-
access: a,
|
|
230
|
-
action: "network_pool.
|
|
231
|
-
target:
|
|
232
|
-
detail: {
|
|
233
|
-
});
|
|
234
|
-
await config.snapshotInvalidator?.invalidate();
|
|
235
|
-
return sanitizePoolResponse(
|
|
236
|
-
},
|
|
237
|
-
async
|
|
238
|
-
access: AccessDecision | undefined,
|
|
239
|
-
poolId: string,
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
async
|
|
301
|
-
access: AccessDecision | undefined,
|
|
302
|
-
poolId: string,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
*
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (!config.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
): Promise<
|
|
410
|
-
const a = requireTenantScope(access, "dashboard:read");
|
|
411
|
-
if (
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
1
|
+
// Network-pool control-plane operations: the store-backed mutations and reads
|
|
2
|
+
// behind the routes. Depends only on the contracts and the shared helpers;
|
|
3
|
+
// `routes.ts` is the only HTTP surface.
|
|
4
|
+
import { ConsoleDomainError, requireTenantScope } from "../../shared/errors";
|
|
5
|
+
import type { PoolHealthEvent } from "../../../network/pool-health-machine";
|
|
6
|
+
import type { AccessDecision } from "../../../security/access-control";
|
|
7
|
+
import {
|
|
8
|
+
deployRelay as deployRelayWorker,
|
|
9
|
+
isRelayTarget,
|
|
10
|
+
type RelayDeployRequest,
|
|
11
|
+
} from "./relay-deploy";
|
|
12
|
+
import { parseProxyFile } from "./import-parse";
|
|
13
|
+
import { SPEED_TEST_MAX_BYTES, SPEED_TEST_MIN_BYTES } from "./speed-test-sizes";
|
|
14
|
+
import {
|
|
15
|
+
MAX_BATCH_PROBE_TARGETS,
|
|
16
|
+
PROVIDER_PROBE_BATCH_CONCURRENCY,
|
|
17
|
+
POOL_BATCH_PROBE_CONCURRENCY,
|
|
18
|
+
POOL_ROUTING_STRATEGIES,
|
|
19
|
+
normalizePoolConfig,
|
|
20
|
+
sanitizePoolResponse,
|
|
21
|
+
validateEndpoint,
|
|
22
|
+
validatePoolLimits,
|
|
23
|
+
validateTransportConfig,
|
|
24
|
+
type CreateNetworkPoolRequest,
|
|
25
|
+
type HealthCheckResult,
|
|
26
|
+
type PoolBatchProviderProbeResult,
|
|
27
|
+
type PoolProviderProbeResult,
|
|
28
|
+
type NetworkPoolConfig,
|
|
29
|
+
type NetworkPoolRecord,
|
|
30
|
+
type NetworkPoolResponse,
|
|
31
|
+
type PoolBatchProbeResult,
|
|
32
|
+
type PoolSpeedTestResult,
|
|
33
|
+
type PoolStrategySetting,
|
|
34
|
+
} from "./contracts";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether a provider entry is a retired egress rather than a refilling
|
|
38
|
+
* allowance. Read from the stored flag, never from the duration.
|
|
39
|
+
*/
|
|
40
|
+
function isBlockedEntry(entry: { readonly blocked?: boolean }): boolean {
|
|
41
|
+
return entry.blocked === true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createNetworkPoolOperations(config: NetworkPoolConfig) {
|
|
45
|
+
const operations = {
|
|
46
|
+
async listPools(access: AccessDecision | undefined): Promise<NetworkPoolResponse[]> {
|
|
47
|
+
const a = requireTenantScope(access, "dashboard:read");
|
|
48
|
+
const rawPools = await config.store.list(a.tenantId);
|
|
49
|
+
return Promise.all(
|
|
50
|
+
rawPools.map(async (r) => {
|
|
51
|
+
const res = sanitizePoolResponse(r);
|
|
52
|
+
const cooldowns = await config.poolSelector?.getPoolCooldowns(r.id);
|
|
53
|
+
const inflight = config.poolSelector
|
|
54
|
+
? await config.poolSelector.getInflightAuthoritative(r.id)
|
|
55
|
+
: res.inflight;
|
|
56
|
+
return {
|
|
57
|
+
...res,
|
|
58
|
+
inflight,
|
|
59
|
+
available: Math.max(0, res.maxInflight - inflight),
|
|
60
|
+
utilization: res.maxInflight > 0 ? Math.min(1, inflight / res.maxInflight) : 1,
|
|
61
|
+
...(cooldowns && cooldowns.length > 0
|
|
62
|
+
? {
|
|
63
|
+
providerCooldowns: cooldowns.map((c) => ({
|
|
64
|
+
providerId: c.providerId,
|
|
65
|
+
until: new Date(c.until).toISOString(),
|
|
66
|
+
reason: c.reason,
|
|
67
|
+
blocked: isBlockedEntry(c),
|
|
68
|
+
})),
|
|
69
|
+
}
|
|
70
|
+
: {}),
|
|
71
|
+
};
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
async getPoolDetail(
|
|
76
|
+
access: AccessDecision | undefined,
|
|
77
|
+
poolId: string,
|
|
78
|
+
): Promise<NetworkPoolResponse> {
|
|
79
|
+
const a = requireTenantScope(access, "dashboard:read");
|
|
80
|
+
const rec = await config.store.get(a.tenantId, poolId);
|
|
81
|
+
if (!rec) throw new ConsoleDomainError("pool_not_found", 404, `Pool ${poolId} not found`);
|
|
82
|
+
const res = sanitizePoolResponse(rec);
|
|
83
|
+
const cooldowns = await config.poolSelector?.getPoolCooldowns(poolId);
|
|
84
|
+
const inflight = config.poolSelector
|
|
85
|
+
? await config.poolSelector.getInflightAuthoritative(poolId)
|
|
86
|
+
: res.inflight;
|
|
87
|
+
return {
|
|
88
|
+
...res,
|
|
89
|
+
inflight,
|
|
90
|
+
available: Math.max(0, res.maxInflight - inflight),
|
|
91
|
+
utilization: res.maxInflight > 0 ? Math.min(1, inflight / res.maxInflight) : 1,
|
|
92
|
+
...(cooldowns && cooldowns.length > 0
|
|
93
|
+
? {
|
|
94
|
+
providerCooldowns: cooldowns.map((c) => ({
|
|
95
|
+
providerId: c.providerId,
|
|
96
|
+
until: new Date(c.until).toISOString(),
|
|
97
|
+
reason: c.reason,
|
|
98
|
+
blocked: isBlockedEntry(c),
|
|
99
|
+
})),
|
|
100
|
+
}
|
|
101
|
+
: {}),
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
async clearPoolCooldown(
|
|
105
|
+
access: AccessDecision | undefined,
|
|
106
|
+
poolId: string,
|
|
107
|
+
providerId?: string,
|
|
108
|
+
): Promise<{ success: boolean }> {
|
|
109
|
+
requireTenantScope(access, "dashboard:write");
|
|
110
|
+
if (providerId) {
|
|
111
|
+
await config.poolSelector?.clearProviderCooldown(poolId, providerId);
|
|
112
|
+
} else {
|
|
113
|
+
const active = await config.poolSelector?.getPoolCooldowns(poolId);
|
|
114
|
+
if (active) {
|
|
115
|
+
for (const c of active) {
|
|
116
|
+
await config.poolSelector?.clearProviderCooldown(poolId, c.providerId);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { success: true };
|
|
121
|
+
},
|
|
122
|
+
async getStrategy(access: AccessDecision | undefined): Promise<PoolStrategySetting> {
|
|
123
|
+
const a = requireTenantScope(access, "dashboard:read");
|
|
124
|
+
return config.store.getStrategy(a.tenantId);
|
|
125
|
+
},
|
|
126
|
+
async updateStrategy(
|
|
127
|
+
access: AccessDecision | undefined,
|
|
128
|
+
request: Partial<PoolStrategySetting>,
|
|
129
|
+
): Promise<PoolStrategySetting> {
|
|
130
|
+
const a = requireTenantScope(access, "dashboard:write");
|
|
131
|
+
const current = await config.store.getStrategy(a.tenantId);
|
|
132
|
+
const strategy = request.strategy ?? current.strategy;
|
|
133
|
+
// Runtime check despite the type: the HTTP body is cast at the route
|
|
134
|
+
// boundary, so a hand-crafted request can carry any string here.
|
|
135
|
+
if (!POOL_ROUTING_STRATEGIES.includes(strategy)) {
|
|
136
|
+
throw new ConsoleDomainError("invalid_pool", 400, "Pool strategy is invalid");
|
|
137
|
+
}
|
|
138
|
+
const rotateCount = request.rotateCount ?? current.rotateCount;
|
|
139
|
+
if (!Number.isInteger(rotateCount) || rotateCount < 1 || rotateCount > 1000) {
|
|
140
|
+
throw new ConsoleDomainError(
|
|
141
|
+
"invalid_pool_limits",
|
|
142
|
+
400,
|
|
143
|
+
"rotateCount must be an integer between 1 and 1000",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const saved = await config.store.setStrategy(a.tenantId, { strategy, rotateCount });
|
|
147
|
+
await config.auditSink?.record({
|
|
148
|
+
access: a,
|
|
149
|
+
action: "network_pool.strategy_updated",
|
|
150
|
+
target: a.tenantId,
|
|
151
|
+
detail: { strategy: saved.strategy, rotateCount: saved.rotateCount },
|
|
152
|
+
});
|
|
153
|
+
await config.snapshotInvalidator?.invalidate();
|
|
154
|
+
return saved;
|
|
155
|
+
},
|
|
156
|
+
async importPools(
|
|
157
|
+
access: AccessDecision | undefined,
|
|
158
|
+
request: {
|
|
159
|
+
readonly content: string;
|
|
160
|
+
readonly maxInflight?: number | undefined;
|
|
161
|
+
readonly weight?: number | undefined;
|
|
162
|
+
readonly family?: string | null | undefined;
|
|
163
|
+
readonly labelPrefix?: string | undefined;
|
|
164
|
+
},
|
|
165
|
+
): Promise<{ created: number; skipped: readonly { line: number; reason: string }[] }> {
|
|
166
|
+
const a = requireTenantScope(access, "dashboard:write");
|
|
167
|
+
const parsed = parseProxyFile(request.content);
|
|
168
|
+
const skipped = [...parsed.skipped];
|
|
169
|
+
let created = 0;
|
|
170
|
+
for (const [index, row] of parsed.rows.entries()) {
|
|
171
|
+
try {
|
|
172
|
+
await operations.createPool(a, {
|
|
173
|
+
kind: row.kind,
|
|
174
|
+
endpoint: row.endpoint,
|
|
175
|
+
...(row.credential === null ? {} : { credential: row.credential }),
|
|
176
|
+
...(request.maxInflight === undefined ? {} : { maxInflight: request.maxInflight }),
|
|
177
|
+
...(request.weight === undefined ? {} : { weight: request.weight }),
|
|
178
|
+
...(request.family === undefined ? {} : { family: request.family }),
|
|
179
|
+
...(row.label !== null
|
|
180
|
+
? { label: row.label }
|
|
181
|
+
: request.labelPrefix
|
|
182
|
+
? { label: `${request.labelPrefix} ${index + 1}` }
|
|
183
|
+
: {}),
|
|
184
|
+
});
|
|
185
|
+
created += 1;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
// One bad node must not abort the list: the rest are still worth
|
|
188
|
+
// having, and the reason is reported against its own line.
|
|
189
|
+
skipped.push({
|
|
190
|
+
line: index + 1,
|
|
191
|
+
reason: error instanceof Error ? error.message.slice(0, 160) : "rejected",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { created, skipped };
|
|
196
|
+
},
|
|
197
|
+
async createPool(
|
|
198
|
+
access: AccessDecision | undefined,
|
|
199
|
+
request: CreateNetworkPoolRequest,
|
|
200
|
+
): Promise<NetworkPoolResponse> {
|
|
201
|
+
const a = requireTenantScope(access, "dashboard:write");
|
|
202
|
+
const normalizedConfig = normalizePoolConfig(request.config);
|
|
203
|
+
const endpoint = request.endpoint;
|
|
204
|
+
validateEndpoint(request.kind, endpoint, config.ssrfPolicy);
|
|
205
|
+
validateTransportConfig(request.kind, normalizedConfig);
|
|
206
|
+
validatePoolLimits(request.maxInflight, request.weight);
|
|
207
|
+
const record: NetworkPoolRecord = {
|
|
208
|
+
id: crypto.randomUUID(),
|
|
209
|
+
kind: request.kind,
|
|
210
|
+
endpoint,
|
|
211
|
+
maxInflight: request.maxInflight ?? 10,
|
|
212
|
+
weight: request.weight ?? 100,
|
|
213
|
+
status: "active",
|
|
214
|
+
inflight: 0,
|
|
215
|
+
consecutiveFailures: 0,
|
|
216
|
+
tenantId: a.tenantId,
|
|
217
|
+
createdAt: new Date().toISOString(),
|
|
218
|
+
...(request.label === undefined ? {} : { label: request.label }),
|
|
219
|
+
...(Object.keys(normalizedConfig).length > 0 ? { config: normalizedConfig } : {}),
|
|
220
|
+
...(request.credential === undefined ? {} : { credential: request.credential }),
|
|
221
|
+
...(request.credential !== undefined ? { hasCredential: true } : {}),
|
|
222
|
+
// Absent or `null` both mean unmetered; a positive number is the
|
|
223
|
+
// allowance. The body schema already rejects zero and negatives.
|
|
224
|
+
...(typeof request.quotaBytes === "number" ? { quotaBytes: request.quotaBytes } : {}),
|
|
225
|
+
...(request.family === undefined ? {} : { family: request.family }),
|
|
226
|
+
};
|
|
227
|
+
await config.store.create(record);
|
|
228
|
+
await config.auditSink?.record({
|
|
229
|
+
access: a,
|
|
230
|
+
action: "network_pool.created",
|
|
231
|
+
target: record.id,
|
|
232
|
+
detail: { kind: record.kind, hasCredential: record.hasCredential === true },
|
|
233
|
+
});
|
|
234
|
+
await config.snapshotInvalidator?.invalidate();
|
|
235
|
+
return sanitizePoolResponse(record);
|
|
236
|
+
},
|
|
237
|
+
async updatePool(
|
|
238
|
+
access: AccessDecision | undefined,
|
|
239
|
+
poolId: string,
|
|
240
|
+
request: Partial<CreateNetworkPoolRequest>,
|
|
241
|
+
): Promise<NetworkPoolResponse> {
|
|
242
|
+
const a = requireTenantScope(access, "dashboard:write");
|
|
243
|
+
validatePoolLimits(request.maxInflight, request.weight);
|
|
244
|
+
if (
|
|
245
|
+
request.endpoint !== undefined ||
|
|
246
|
+
request.kind !== undefined ||
|
|
247
|
+
request.config !== undefined
|
|
248
|
+
) {
|
|
249
|
+
const existing = await config.store.get(a.tenantId, poolId);
|
|
250
|
+
if (!existing) throw new ConsoleDomainError("pool_not_found", 404, `Pool ${poolId} not found`);
|
|
251
|
+
const effectiveKind = request.kind ?? existing.kind;
|
|
252
|
+
const effectiveEndpoint = request.endpoint ?? existing.endpoint;
|
|
253
|
+
const mergedConfig = { ...(existing.config ?? {}), ...(request.config ?? {}) };
|
|
254
|
+
const normalizedConfig = normalizePoolConfig(mergedConfig);
|
|
255
|
+
validateEndpoint(effectiveKind, effectiveEndpoint, config.ssrfPolicy);
|
|
256
|
+
validateTransportConfig(effectiveKind, normalizedConfig);
|
|
257
|
+
request = { ...request, endpoint: effectiveEndpoint, config: normalizedConfig };
|
|
258
|
+
}
|
|
259
|
+
const updated = await config.store.update(
|
|
260
|
+
a.tenantId,
|
|
261
|
+
poolId,
|
|
262
|
+
request as Partial<NetworkPoolRecord>,
|
|
263
|
+
);
|
|
264
|
+
if (!updated) throw new ConsoleDomainError("pool_not_found", 404, `Pool ${poolId} not found`);
|
|
265
|
+
// Any mutation can invalidate the cached dial agent (endpoint, credential,
|
|
266
|
+
// kind, config, or a disable via status) — release unconditionally so the
|
|
267
|
+
// next dispatch rebuilds against the fresh row rather than reusing a
|
|
268
|
+
// stale agent/delegate bound to the pre-update configuration.
|
|
269
|
+
await config.poolAgentReleaser?.releasePool(poolId, a.tenantId);
|
|
270
|
+
await config.auditSink?.record({
|
|
271
|
+
access: a,
|
|
272
|
+
action: "network_pool.updated",
|
|
273
|
+
target: poolId,
|
|
274
|
+
detail: { fields: Object.keys(request) },
|
|
275
|
+
});
|
|
276
|
+
await config.snapshotInvalidator?.invalidate();
|
|
277
|
+
return sanitizePoolResponse(updated);
|
|
278
|
+
},
|
|
279
|
+
async deletePool(
|
|
280
|
+
access: AccessDecision | undefined,
|
|
281
|
+
poolId: string,
|
|
282
|
+
): Promise<{ success: boolean }> {
|
|
283
|
+
const a = requireTenantScope(access, "dashboard:write");
|
|
284
|
+
const ok = await config.store.delete(a.tenantId, poolId);
|
|
285
|
+
if (!ok) throw new ConsoleDomainError("pool_not_found", 404, `Pool ${poolId} not found`);
|
|
286
|
+
await config.poolAgentReleaser?.releasePool(poolId, a.tenantId);
|
|
287
|
+
// Drop the pool's cooldown markers and their index set with it. Without
|
|
288
|
+
// this the Redis keys outlive the row, so a pool later reusing the id
|
|
289
|
+
// inherits cooldowns it never earned — and the index set, which has no
|
|
290
|
+
// TTL of its own, would keep listing them.
|
|
291
|
+
await config.poolSelector?.clearPoolCooldowns(poolId);
|
|
292
|
+
await config.auditSink?.record({
|
|
293
|
+
access: a,
|
|
294
|
+
action: "network_pool.deleted",
|
|
295
|
+
target: poolId,
|
|
296
|
+
});
|
|
297
|
+
await config.snapshotInvalidator?.invalidate();
|
|
298
|
+
return { success: true };
|
|
299
|
+
},
|
|
300
|
+
async listPoolHealthEvents(
|
|
301
|
+
access: AccessDecision | undefined,
|
|
302
|
+
poolId: string,
|
|
303
|
+
): Promise<readonly PoolHealthEvent[]> {
|
|
304
|
+
const a = requireTenantScope(access, "dashboard:read");
|
|
305
|
+
return config.store.listHealthEvents(a.tenantId, poolId);
|
|
306
|
+
},
|
|
307
|
+
async recoverPool(
|
|
308
|
+
access: AccessDecision | undefined,
|
|
309
|
+
poolId: string,
|
|
310
|
+
): Promise<{ success: true }> {
|
|
311
|
+
const a = requireTenantScope(access, "dashboard:write");
|
|
312
|
+
const recovered = await config.store.recover(a.tenantId, poolId);
|
|
313
|
+
if (!recovered) throw new ConsoleDomainError("pool_not_found", 404, `Pool ${poolId} not found`);
|
|
314
|
+
await config.poolAgentReleaser?.releasePool(poolId, a.tenantId);
|
|
315
|
+
await config.auditSink?.record({
|
|
316
|
+
access: a,
|
|
317
|
+
action: "network_pool.recovered",
|
|
318
|
+
target: poolId,
|
|
319
|
+
});
|
|
320
|
+
await config.snapshotInvalidator?.invalidate();
|
|
321
|
+
return { success: true };
|
|
322
|
+
},
|
|
323
|
+
async healthCheck(
|
|
324
|
+
access: AccessDecision | undefined,
|
|
325
|
+
poolId: string,
|
|
326
|
+
): Promise<HealthCheckResult> {
|
|
327
|
+
const a = requireTenantScope(access, "dashboard:read");
|
|
328
|
+
const result = await config.store.healthCheck(a.tenantId, poolId);
|
|
329
|
+
if (result.httpStatus !== undefined) {
|
|
330
|
+
await config.poolAgentReleaser?.releasePool(poolId, a.tenantId);
|
|
331
|
+
await config.snapshotInvalidator?.invalidate();
|
|
332
|
+
}
|
|
333
|
+
return result;
|
|
334
|
+
},
|
|
335
|
+
/**
|
|
336
|
+
* Measures download throughput through one saved pool.
|
|
337
|
+
*
|
|
338
|
+
* Read-scoped like the health check: it dials the tenant's own pool and
|
|
339
|
+
* changes no state. A pool that cannot be dialed reports `failed` rather
|
|
340
|
+
* than throwing, so a batch of these returns a per-pool verdict.
|
|
341
|
+
*/
|
|
342
|
+
async speedTest(
|
|
343
|
+
access: AccessDecision | undefined,
|
|
344
|
+
poolId: string,
|
|
345
|
+
bytes: number,
|
|
346
|
+
): Promise<PoolSpeedTestResult> {
|
|
347
|
+
const a = requireTenantScope(access, "dashboard:read");
|
|
348
|
+
if (!config.store.speedTest) {
|
|
349
|
+
throw new ConsoleDomainError("not_supported", 400, "Speed tests are unavailable");
|
|
350
|
+
}
|
|
351
|
+
if (!Number.isInteger(bytes) || bytes < SPEED_TEST_MIN_BYTES || bytes > SPEED_TEST_MAX_BYTES) {
|
|
352
|
+
throw new ConsoleDomainError(
|
|
353
|
+
"invalid_speed_test_size",
|
|
354
|
+
422,
|
|
355
|
+
`Payload must be between ${SPEED_TEST_MIN_BYTES} and ${SPEED_TEST_MAX_BYTES} bytes`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return config.store.speedTest(a.tenantId, poolId, bytes);
|
|
359
|
+
},
|
|
360
|
+
/**
|
|
361
|
+
* Asks one provider whether it will serve traffic from this pool.
|
|
362
|
+
*
|
|
363
|
+
* Read-only by default: an operator testing a proxy wants to know its
|
|
364
|
+
* state, not to change it. `block` is the explicit opt-in that also
|
|
365
|
+
* retires the pool for that provider.
|
|
366
|
+
*/
|
|
367
|
+
async providerProbe(
|
|
368
|
+
access: AccessDecision | undefined,
|
|
369
|
+
poolId: string,
|
|
370
|
+
providerId: string,
|
|
371
|
+
): Promise<PoolProviderProbeResult> {
|
|
372
|
+
const a = requireTenantScope(access, "dashboard:read");
|
|
373
|
+
if (!config.store.providerProbe) {
|
|
374
|
+
throw new ConsoleDomainError("not_supported", 400, "Provider probing is unavailable");
|
|
375
|
+
}
|
|
376
|
+
const result = await config.store.providerProbe(a.tenantId, poolId, providerId);
|
|
377
|
+
if (!config.poolSelector) return result;
|
|
378
|
+
// The verdict *is* the enforcement. Storing it and leaving the pool in
|
|
379
|
+
// rotation is what produced "I tested every pool, some are red, and it
|
|
380
|
+
// still picked a red one": the red was a colour, not a decision.
|
|
381
|
+
//
|
|
382
|
+
// `blocked` retires the egress for this provider, so the next dispatch
|
|
383
|
+
// skips it. Anything else clears a previous block, because the address is
|
|
384
|
+
// serving again — an operator who replaced a proxy must not have to find
|
|
385
|
+
// a separate button to un-retire it.
|
|
386
|
+
if (result.status === "blocked") {
|
|
387
|
+
await config.poolSelector.flagProviderBlocked(
|
|
388
|
+
poolId,
|
|
389
|
+
providerId,
|
|
390
|
+
"Upstream refused this egress address",
|
|
391
|
+
);
|
|
392
|
+
return { ...result, blockedPool: true };
|
|
393
|
+
}
|
|
394
|
+
await config.poolSelector.clearProviderCooldown(poolId, providerId);
|
|
395
|
+
return { ...result, blockedPool: false };
|
|
396
|
+
},
|
|
397
|
+
/**
|
|
398
|
+
* Probes many unsaved pool definitions against one provider.
|
|
399
|
+
*
|
|
400
|
+
* Same shape and bounds as `probeAdHocPoolBatch`: the add form tests a
|
|
401
|
+
* pasted list before saving it, and a per-endpoint verdict is what it
|
|
402
|
+
* prints. A single failing probe becomes that endpoint's result rather
|
|
403
|
+
* than failing the batch.
|
|
404
|
+
*/
|
|
405
|
+
async probeAdHocProviderBatch(
|
|
406
|
+
access: AccessDecision | undefined,
|
|
407
|
+
requests: readonly CreateNetworkPoolRequest[],
|
|
408
|
+
providerId: string,
|
|
409
|
+
): Promise<readonly PoolBatchProviderProbeResult[]> {
|
|
410
|
+
const a = requireTenantScope(access, "dashboard:read");
|
|
411
|
+
if (requests.length === 0) return [];
|
|
412
|
+
if (requests.length > MAX_BATCH_PROBE_TARGETS) {
|
|
413
|
+
throw new ConsoleDomainError(
|
|
414
|
+
"batch_too_large",
|
|
415
|
+
422,
|
|
416
|
+
`At most ${MAX_BATCH_PROBE_TARGETS} targets per batch`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
if (!config.store.probeAdHocProvider) {
|
|
420
|
+
throw new ConsoleDomainError("not_supported", 400, "Provider probing is unavailable");
|
|
421
|
+
}
|
|
422
|
+
const probe = config.store.probeAdHocProvider.bind(config.store);
|
|
423
|
+
const results: PoolBatchProviderProbeResult[] = new Array(requests.length);
|
|
424
|
+
let cursor = 0;
|
|
425
|
+
const worker = async () => {
|
|
426
|
+
while (cursor < requests.length) {
|
|
427
|
+
const index = cursor++;
|
|
428
|
+
const request = requests[index]!;
|
|
429
|
+
let result: PoolProviderProbeResult;
|
|
430
|
+
try {
|
|
431
|
+
result = await probe(a.tenantId, request, providerId);
|
|
432
|
+
} catch (error) {
|
|
433
|
+
result = {
|
|
434
|
+
poolId: request.endpoint,
|
|
435
|
+
providerId,
|
|
436
|
+
status: "unreachable",
|
|
437
|
+
reason: error instanceof Error ? error.message : "Probe failed",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
results[index] = { endpoint: request.endpoint, result };
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
await Promise.all(
|
|
444
|
+
Array.from({ length: Math.min(PROVIDER_PROBE_BATCH_CONCURRENCY, requests.length) }, () => worker()),
|
|
445
|
+
);
|
|
446
|
+
return results;
|
|
447
|
+
},
|
|
448
|
+
async probeAdHocPool(
|
|
449
|
+
access: AccessDecision | undefined,
|
|
450
|
+
request: CreateNetworkPoolRequest,
|
|
451
|
+
): Promise<HealthCheckResult> {
|
|
452
|
+
const a = requireTenantScope(access, "dashboard:read");
|
|
453
|
+
if (config.store.probeAdHoc) {
|
|
454
|
+
return config.store.probeAdHoc(a.tenantId, request);
|
|
455
|
+
}
|
|
456
|
+
throw new ConsoleDomainError("not_supported", 400, "Ad-hoc pool probing is unavailable");
|
|
457
|
+
},
|
|
458
|
+
/**
|
|
459
|
+
* Probes many unsaved pool definitions in one call.
|
|
460
|
+
*
|
|
461
|
+
* The dashboard tests a pasted list before saving it; doing that one
|
|
462
|
+
* request per proxy made a 100-line paste take a minute. The dials still
|
|
463
|
+
* happen individually — only the round-trips are batched — and run with
|
|
464
|
+
* bounded concurrency so a large list does not open every socket at once.
|
|
465
|
+
*/
|
|
466
|
+
async probeAdHocPoolBatch(
|
|
467
|
+
access: AccessDecision | undefined,
|
|
468
|
+
requests: readonly CreateNetworkPoolRequest[],
|
|
469
|
+
): Promise<readonly PoolBatchProbeResult[]> {
|
|
470
|
+
const a = requireTenantScope(access, "dashboard:read");
|
|
471
|
+
if (requests.length === 0) return [];
|
|
472
|
+
if (requests.length > MAX_BATCH_PROBE_TARGETS) {
|
|
473
|
+
throw new ConsoleDomainError(
|
|
474
|
+
"batch_too_large",
|
|
475
|
+
422,
|
|
476
|
+
`At most ${MAX_BATCH_PROBE_TARGETS} targets per batch`,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
if (!config.store.probeAdHoc) {
|
|
480
|
+
throw new ConsoleDomainError("not_supported", 400, "Ad-hoc pool probing is unavailable");
|
|
481
|
+
}
|
|
482
|
+
const probe = config.store.probeAdHoc.bind(config.store);
|
|
483
|
+
const results: PoolBatchProbeResult[] = new Array(requests.length);
|
|
484
|
+
let cursor = 0;
|
|
485
|
+
const worker = async () => {
|
|
486
|
+
while (cursor < requests.length) {
|
|
487
|
+
const index = cursor++;
|
|
488
|
+
const request = requests[index]!;
|
|
489
|
+
// A single failing probe must not fail the batch: the caller wants a
|
|
490
|
+
// per-endpoint verdict, so an error becomes that endpoint's result.
|
|
491
|
+
let result: HealthCheckResult;
|
|
492
|
+
try {
|
|
493
|
+
result = await probe(a.tenantId, request);
|
|
494
|
+
} catch (error) {
|
|
495
|
+
result = {
|
|
496
|
+
poolId: request.endpoint,
|
|
497
|
+
status: "unhealthy",
|
|
498
|
+
errorMessage: error instanceof Error ? error.message : "Probe failed",
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
results[index] = { endpoint: request.endpoint, kind: request.kind, result };
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
const workers = Array.from(
|
|
505
|
+
{ length: Math.min(POOL_BATCH_PROBE_CONCURRENCY, requests.length) },
|
|
506
|
+
() => worker(),
|
|
507
|
+
);
|
|
508
|
+
await Promise.all(workers);
|
|
509
|
+
return results;
|
|
510
|
+
},
|
|
511
|
+
/**
|
|
512
|
+
* Deploys a hosted relay (Cloudflare/Vercel/Deno) and registers its URL as
|
|
513
|
+
* an active HTTP pool. The provider API token is used for the deploy and
|
|
514
|
+
* never persisted; the relay URL is a public host, which is what the pool
|
|
515
|
+
* stores. The deploy fetch is the SSRF-validated one, so the provider API
|
|
516
|
+
* call is checked like every other egress.
|
|
517
|
+
*/
|
|
518
|
+
async deployRelay(
|
|
519
|
+
access: AccessDecision | undefined,
|
|
520
|
+
request: RelayDeployRequest,
|
|
521
|
+
): Promise<NetworkPoolResponse & { relayUrl: string }> {
|
|
522
|
+
const a = requireTenantScope(access, "dashboard:write");
|
|
523
|
+
if (!config.relayFetch)
|
|
524
|
+
throw new ConsoleDomainError("not_supported", 503, "Relay deployment is unavailable");
|
|
525
|
+
if (!isRelayTarget(request.target))
|
|
526
|
+
throw new ConsoleDomainError("invalid_request", 422, `Unsupported relay target: ${String(request.target)}`);
|
|
527
|
+
const result = await deployRelayWorker(config.relayFetch, request);
|
|
528
|
+
const created = await operations.createPool(access, {
|
|
529
|
+
kind: "http",
|
|
530
|
+
endpoint: result.relayUrl,
|
|
531
|
+
label: request.projectName?.trim() || `${result.target} relay`,
|
|
532
|
+
});
|
|
533
|
+
await config.auditSink?.record({
|
|
534
|
+
access: a,
|
|
535
|
+
action: "network_pool.relay_deployed",
|
|
536
|
+
target: created.id,
|
|
537
|
+
detail: { target: result.target, relayUrl: result.relayUrl },
|
|
538
|
+
});
|
|
539
|
+
return { ...created, relayUrl: result.relayUrl };
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
return operations;
|
|
543
|
+
}
|