cartethyia 2.5.10 → 2.6.0
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 +8 -0
- package/dist/dashboard/assets/{BannedPage-Cd30Z0b9.js → BannedPage-l6XMnfr9.js} +1 -1
- package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-wf5u-ww6.js → CliToolDetailPage-DwOzAW45.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DykPvuck.js → CliToolsPage-BT3Up3QA.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-DwvSD3vs.js → CombosPage-6kYIUpzR.js} +1 -1
- package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CIr7KDex.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-82X0DDdH.js → CustomizationPage-C7m-H1Rz.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-CS5mPuVF.js → LoginPage-CbphLvjg.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-C0UCUsVJ.js → ModelPicker-DVny4S5_.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-vdja5ZPK.js → ProvidersPage-Dfb2896L.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-BB4CNm4-.js → ProxyPage-BaCIdAUt.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-FadUOl9S.js → SetupPage-D4g6vOGc.js} +1 -1
- package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Cfy6_Khv.js → SortableList-C93i14BD.js} +1 -1
- package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CM92fgJf.js → StudioPage-SgQpymjm.js} +1 -1
- package/dist/dashboard/assets/{StudioPage-CM92fgJf.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
- package/dist/dashboard/assets/{UsagePage-BT4MyQeT.js → UsagePage-BFME-eba.js} +2 -2
- package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
- package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
- package/dist/dashboard/assets/{app-B7pVRhJ1.js → app-2KhtHCv9.js} +1 -1
- package/dist/dashboard/assets/{app-B7pVRhJ1.js.gz → app-2KhtHCv9.js.gz} +0 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
- package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
- package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
- package/dist/dashboard/assets/{badge-Bvyr7D0u.js → badge-CKsF123d.js} +1 -1
- package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DIvKAZih.js → banner-8j7uChA9.js} +1 -1
- package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
- package/dist/dashboard/assets/{card-D5_gYIS_.js → card-CdsSlWA3.js} +1 -1
- package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
- package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-B6vDBtbm.js → clipboard-button-uL5aYLfW.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-CpjrgdwJ.js → dialog-C07hr5_G.js} +1 -1
- package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BWmt3b5y.js → drawer-BPO1TcKH.js} +1 -1
- package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
- package/dist/dashboard/assets/{entry-C_oakV3z.js → entry-Bz_CtDfC.js} +1 -1
- package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-RRHHWGxh.js → github-badge-DmQreSDd.js} +1 -1
- package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
- package/dist/dashboard/assets/{index-Bhey_rg_.js → index-Bblu6erR.js} +2 -2
- package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
- package/dist/dashboard/assets/{inline-C2_EQ5Uf.js → inline-C9dCYaBZ.js} +1 -1
- package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
- package/dist/dashboard/assets/{input-BMRlQ450.js → input-CjCnXweJ.js} +1 -1
- package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
- package/dist/dashboard/assets/{layout-XR5xP8ew.js → layout-V6y8DAkl.js} +1 -1
- package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Cw07dioc.js → list-row-CM1AaG_y.js} +1 -1
- package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
- package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
- package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
- package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
- package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-DrdA9oSM.js → numbers-BOevJzdz.js} +1 -1
- package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
- package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
- package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
- package/dist/dashboard/assets/{select-CxVVB__N.js → select-C980c4cz.js} +1 -1
- package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
- package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
- package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
- package/dist/dashboard/assets/{stack-uPOwM9Hs.js → stack-_gfTdLVB.js} +1 -1
- package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
- package/dist/dashboard/assets/{state-D1Yz79iL.js → state-2c5V4zo7.js} +1 -1
- package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
- package/dist/dashboard/assets/{switch-sevsRrRo.js → switch-Dkke7Naf.js} +1 -1
- package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
- package/dist/dashboard/assets/useMutation-EDFRkn1i.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 +51 -51
- package/migrations/README.md +52 -52
- package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
- package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
- package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/package.json +96 -95
- package/scripts/cli/admin.ts +126 -128
- package/scripts/cli/autostart.ts +237 -237
- package/scripts/cli/background.ts +120 -110
- package/scripts/cli/bin/carte.cjs +8 -8
- package/scripts/cli/bin/cartethyia.cjs +167 -167
- package/scripts/cli/console-menu.ts +39 -30
- package/scripts/cli/controller.ts +145 -0
- package/scripts/cli/env.ts +99 -99
- package/scripts/cli/gateway.ts +264 -260
- package/scripts/cli/instance.ts +234 -224
- package/scripts/cli/main.ts +188 -167
- package/scripts/cli/platform.ts +64 -64
- package/scripts/cli/prompt.ts +217 -79
- package/scripts/cli/services.ts +88 -88
- package/scripts/cli/setup.ts +445 -445
- package/scripts/cli/start.ts +85 -85
- package/scripts/cli/startup-signal.ts +23 -23
- package/scripts/cli/telemetry.ts +70 -0
- package/scripts/cli/tray/index.ts +263 -263
- package/scripts/cli/tray/windows.ps1 +107 -107
- package/scripts/cli/tray.ts +29 -4
- package/scripts/cli/update.ts +39 -39
- package/scripts/cli/version.ts +67 -67
- package/scripts/cli/wordmark.ts +58 -58
- package/src/boot/app.ts +16 -1
- package/src/boot/dependencies.ts +128 -25
- package/src/config/resources.ts +137 -0
- package/src/config/runtime.ts +133 -76
- package/src/config/spec.ts +30 -29
- package/src/config/telemetry.ts +0 -25
- package/src/console/api-keys/bucket-spend.ts +14 -17
- package/src/console/api-keys/contracts.ts +410 -410
- package/src/console/api-keys/routes.ts +12 -0
- package/src/console/audit/store.ts +2 -3
- package/src/console/auth/setup-service.ts +8 -1
- package/src/console/backup/routes.ts +7 -0
- package/src/console/backup/service.ts +463 -463
- package/src/console/backup/store.ts +16 -11
- package/src/console/backup/validate.ts +1 -4
- package/src/console/cli-tools/fs-ops.ts +18 -2
- package/src/console/data-management/contracts.ts +1 -8
- package/src/console/data-management/operations.ts +61 -48
- package/src/console/data-management/routes.ts +4 -2
- package/src/console/domain-registration.ts +25 -3
- package/src/console/inferhub/client.ts +139 -0
- package/src/console/inferhub/key-store.ts +129 -0
- package/src/console/inferhub/normalize.ts +347 -0
- package/src/console/inferhub/routes.ts +411 -0
- package/src/console/inferhub/store.ts +109 -0
- package/src/console/inferhub/types.ts +183 -0
- package/src/console/observability/contracts.ts +2 -41
- package/src/console/observability/store.ts +176 -371
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/store.ts +23 -36
- package/src/console/quota/account-quota-shared.ts +52 -52
- package/src/console/quota/account-quota-tenant.ts +16 -24
- package/src/console/quota/refresh.ts +441 -441
- package/src/console/routing/pools/import-parse.ts +206 -206
- package/src/console/routing/pools/operations.ts +543 -543
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/routes.ts +181 -181
- package/src/console/settings/contracts.ts +42 -40
- package/src/console/settings/store.ts +25 -25
- package/src/console/share/share-stats.ts +22 -4
- package/src/console/share/share-usage.ts +30 -4
- package/src/console/shared/cursor.ts +15 -0
- package/src/console/updates/routes.ts +12 -0
- package/src/main.ts +13 -5
- package/src/network/pool/resolver.ts +1104 -1104
- package/src/observability/client-name.ts +22 -0
- package/src/observability/event-log.ts +5 -29
- package/src/observability/event-query.ts +690 -0
- package/src/observability/log-ring.ts +26 -2
- package/src/observability/log-sanitize.ts +118 -0
- package/src/observability/logger.ts +17 -13
- package/src/observability/metrics.ts +35 -0
- package/src/observability/payload-capture.ts +114 -41
- package/src/observability/payload-store.ts +249 -24
- package/src/observability/raw-event-writer.ts +118 -0
- package/src/observability/runtime-metrics.ts +71 -1
- package/src/observability/telemetry-buffer.ts +100 -17
- package/src/observability/telemetry-reconciliation.ts +417 -0
- package/src/persistence/boot.ts +20 -9
- package/src/persistence/db-pg.ts +13 -0
- package/src/persistence/db-sqlite.ts +569 -497
- package/src/persistence/dialect.ts +3 -2
- package/src/persistence/migrate.ts +181 -181
- package/src/persistence/schema.ts +1496 -1406
- package/src/persistence/shared-cache.ts +5 -1
- package/src/persistence/stores/api-key-store.ts +581 -581
- package/src/persistence/stores/telemetry-store.ts +124 -63
- package/src/protocol/reasoning-text.ts +30 -0
- package/src/protocol/response/chat.ts +33 -7
- package/src/protocol/response/codex.ts +14 -2
- package/src/protocol/response/messages.ts +5 -2
- package/src/providers/authentication/oauth-flow-store.ts +3 -10
- package/src/providers/bundled-discovery-providers.ts +122 -122
- package/src/providers/default-registry.ts +10 -0
- package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
- package/src/providers/discovery/models-dev-catalog.ts +354 -354
- package/src/providers/discovery/probe-phases.ts +43 -7
- package/src/providers/discovery/probing-service.ts +70 -32
- package/src/providers/integrations/antigravity/antigravity.ts +9 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
- package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
- package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
- package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
- package/src/providers/integrations/claude/claude-quota.ts +2 -2
- package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
- package/src/providers/integrations/claude/claude.ts +3 -4
- package/src/providers/integrations/cline/cline.ts +94 -8
- package/src/providers/integrations/codex/codex.ts +61 -37
- package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
- package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
- package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
- package/src/providers/integrations/cursor/cursor-request.ts +11 -0
- package/src/providers/integrations/cursor/cursor.ts +29 -17
- package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
- package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
- package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
- package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
- package/src/providers/integrations/grok/grok-quota.ts +279 -279
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
- package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
- package/src/providers/integrations/hermes.ts +7 -1
- package/src/providers/integrations/inferhub.ts +201 -73
- package/src/providers/integrations/kilo/kilo.ts +152 -152
- package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
- package/src/providers/integrations/openrouter/provider.ts +129 -129
- package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
- package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
- package/src/providers/integrations/trae/trae-oauth.ts +4 -26
- package/src/providers/integrations/trae/trae-quota.ts +389 -384
- package/src/providers/integrations/trae/trae-realm.ts +15 -2
- package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- package/src/providers/operations/account-health-service.ts +605 -279
- package/src/providers/provider-metadata.ts +12 -1
- package/src/providers/provider-model-definition.ts +373 -373
- package/src/providers/quota/quota-contracts.ts +3 -7
- package/src/providers/search/search-provider.ts +2 -6
- package/src/security/admission/contracts.ts +10 -0
- package/src/security/admission/in-memory-store.ts +89 -14
- package/src/security/admission/lua.ts +191 -182
- package/src/security/admission/redis-store.ts +30 -0
- package/src/security/admission/service.ts +18 -0
- package/src/security/api-key-auth-cache.ts +96 -0
- package/src/security/api-key-auth.ts +11 -100
- package/src/security/client-router-fingerprint.ts +3 -7
- package/src/security/ip-boundary.ts +7 -3
- package/src/shared/coerce.ts +67 -0
- package/src/shared/hash.ts +6 -0
- package/src/shared/time.ts +7 -0
- package/src/transport/dispatch/attempt-finalize.ts +26 -138
- package/src/transport/dispatch/attempt-loop.ts +103 -81
- package/src/transport/dispatch/capture-policy.ts +78 -0
- package/src/transport/dispatch/proxy-request.ts +4 -2
- package/src/transport/dispatch/streaming/encoder.ts +111 -5
- package/src/transport/dispatch/streaming/stream.ts +19 -2
- package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
- package/src/transport/failure-policy.ts +27 -0
- package/src/transport/guards/abuse-guard.ts +2 -2
- package/src/transport/guards/auth-guard.ts +17 -0
- package/src/transport/guards/body-policy.ts +3 -2
- package/src/transport/guards/concurrency-guard.ts +89 -13
- package/src/transport/guards/early-rejection-capture.ts +43 -3
- package/src/transport/guards/error-lifecycle.ts +10 -2
- package/src/transport/guards/guard-errors.ts +23 -0
- package/src/transport/guards/headroom-guard.ts +41 -7
- package/src/transport/guards/pipeline.ts +24 -0
- package/src/transport/guards/request-context.ts +2 -1
- package/src/transport/routing/admission.ts +51 -9
- package/src/transport/routing/bounded-lru.ts +49 -0
- package/src/transport/routing/candidate-index.ts +88 -0
- package/src/transport/routing/reservations.ts +121 -7
- package/src/transport/routing/round-robin.ts +10 -22
- package/src/transport/routing/route-catalog.ts +26 -13
- package/src/transport/routing/route-model.ts +22 -2
- package/src/transport/routing/router.ts +53 -31
- package/src/transport/surface/chat/encode.ts +2 -3
- package/src/transport/surface/chat/parse.ts +2 -1
- package/src/transport/surface/dialects.ts +11 -3
- package/src/transport/surface/messages/encode.ts +2 -1
- package/src/transport/surface/messages/parse.ts +3 -1
- package/src/transport/surface/messages/stream.ts +2 -6
- package/src/workers/maintenance-lease.ts +200 -0
- package/src/workers/scheduler.ts +101 -1
- package/dist/dashboard/assets/BannedPage-Cd30Z0b9.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-wf5u-ww6.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DykPvuck.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DwvSD3vs.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CIr7KDex.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-82X0DDdH.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-CS5mPuVF.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C0UCUsVJ.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js +0 -1
- package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-vdja5ZPK.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BB4CNm4-.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js +0 -1
- package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js +0 -1
- package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-FadUOl9S.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-Cfy6_Khv.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BT4MyQeT.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
- package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/badge-Bvyr7D0u.js.gz +0 -0
- package/dist/dashboard/assets/banner-DIvKAZih.js.gz +0 -0
- package/dist/dashboard/assets/card-D5_gYIS_.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-B6vDBtbm.js.gz +0 -0
- package/dist/dashboard/assets/dialog-CpjrgdwJ.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BWmt3b5y.js.gz +0 -0
- package/dist/dashboard/assets/entry-B-gvimrg.js +0 -1
- package/dist/dashboard/assets/entry-B-gvimrg.js.gz +0 -0
- package/dist/dashboard/assets/entry-C_oakV3z.js.gz +0 -0
- package/dist/dashboard/assets/entry-Dj0cHfEc.js +0 -4
- package/dist/dashboard/assets/entry-Dj0cHfEc.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-RRHHWGxh.js.gz +0 -0
- package/dist/dashboard/assets/index-Bhey_rg_.js.gz +0 -0
- package/dist/dashboard/assets/inline-C2_EQ5Uf.js.gz +0 -0
- package/dist/dashboard/assets/input-BMRlQ450.js.gz +0 -0
- package/dist/dashboard/assets/layout-XR5xP8ew.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Cw07dioc.js.gz +0 -0
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DrdA9oSM.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/select-CxVVB__N.js.gz +0 -0
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/stack-uPOwM9Hs.js.gz +0 -0
- package/dist/dashboard/assets/state-D1Yz79iL.js.gz +0 -0
- package/dist/dashboard/assets/switch-sevsRrRo.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
- package/src/observability/event-compaction.ts +0 -317
- package/src/observability/event-day-index.ts +0 -439
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What one provider-probe answer means about the egress.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from the store so the console, the CLI probe
|
|
5
|
-
* (`scripts/tools/probe-egress.ts`) and the tests cannot drift into three
|
|
6
|
-
* readings of the same response. The rule that matters is not obvious:
|
|
7
|
-
*
|
|
8
|
-
* **The provider's IP gate runs before it validates the body.** So a `4xx`
|
|
9
|
-
* complaint about the payload proves the address was accepted — it is the
|
|
10
|
-
* probe's request that is wrong, not the proxy. Mapping that to `unreachable`
|
|
11
|
-
* ("the probe could not complete") blamed the proxy for our own defect and hid
|
|
12
|
-
* working egresses behind a verdict that said the pool was unusable.
|
|
13
|
-
*
|
|
14
|
-
* Only the free-tier refusal retires an address. Every other answer either says
|
|
15
|
-
* the address works (`allowed`, `throttled`) or says nothing about it
|
|
16
|
-
* (`unreachable`), and treating "says nothing" as "refused" would drop a usable
|
|
17
|
-
* pool for good.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
export type ProbeVerdict = "allowed" | "blocked" | "throttled" | "unreachable";
|
|
21
|
-
|
|
22
|
-
export interface ProbeAnswer {
|
|
23
|
-
readonly status: ProbeVerdict;
|
|
24
|
-
/** Operator-facing reason; absent when the answer needs none. */
|
|
25
|
-
readonly reason?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** The upstream's own wording for "this address is retired". */
|
|
29
|
-
const FREE_TIER_REFUSAL = /freetier|free tier can only be used/i;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Classifies one upstream response.
|
|
33
|
-
*
|
|
34
|
-
* `407` is separated from the generic 4xx branch on purpose: it comes from the
|
|
35
|
-
* proxy front door, never from the provider, so it is a credential problem on
|
|
36
|
-
* the tunnel and says nothing about whether the address is refused.
|
|
37
|
-
*/
|
|
38
|
-
export function classifyProviderProbeAnswer(httpStatus: number, body: string): ProbeAnswer {
|
|
39
|
-
if (httpStatus === 403 && FREE_TIER_REFUSAL.test(body)) {
|
|
40
|
-
return { status: "blocked", reason: "Upstream refused this egress address" };
|
|
41
|
-
}
|
|
42
|
-
if (httpStatus === 407) {
|
|
43
|
-
return { status: "unreachable", reason: "Proxy requires authentication" };
|
|
44
|
-
}
|
|
45
|
-
if (httpStatus === 429) {
|
|
46
|
-
return { status: "throttled", reason: "Address accepted; rate limit spent" };
|
|
47
|
-
}
|
|
48
|
-
if (httpStatus >= 200 && httpStatus < 300) {
|
|
49
|
-
return { status: "allowed" };
|
|
50
|
-
}
|
|
51
|
-
if (httpStatus >= 500) {
|
|
52
|
-
return { status: "unreachable", reason: `Upstream answered ${httpStatus}` };
|
|
53
|
-
}
|
|
54
|
-
// Any other 4xx: the request reached the upstream's own validation, which
|
|
55
|
-
// only happens once the address is accepted.
|
|
56
|
-
return {
|
|
57
|
-
status: "allowed",
|
|
58
|
-
reason: `Reachable; upstream answered ${httpStatus} for the probe body`,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Error codes that mean "this attempt never reached the upstream", not "the
|
|
64
|
-
* upstream refused this address".
|
|
65
|
-
*
|
|
66
|
-
* Every one of them is worth a second attempt, and the reason is the same in
|
|
67
|
-
* each case: the failure is a property of one socket, not of the egress. A
|
|
68
|
-
* proxy that has just been dialled for the first time has to resolve, open a
|
|
69
|
-
* tunnel, and complete a TLS handshake, and any of those steps can be refused
|
|
70
|
-
* or reset by a middlebox that accepts the very next attempt — which is exactly
|
|
71
|
-
* the "fails once, works on retry" report. Retiring the pool on the first one
|
|
72
|
-
* blames the address for a cold-start race.
|
|
73
|
-
*/
|
|
74
|
-
const RETRYABLE_TRANSPORT_CODES = new Set([
|
|
75
|
-
"ECONNREFUSED",
|
|
76
|
-
"ECONNRESET",
|
|
77
|
-
"ECONNABORTED",
|
|
78
|
-
"EHOSTUNREACH",
|
|
79
|
-
"ENETUNREACH",
|
|
80
|
-
"ENETRESET",
|
|
81
|
-
"EPIPE",
|
|
82
|
-
"ETIMEDOUT",
|
|
83
|
-
"EAI_AGAIN",
|
|
84
|
-
"UND_ERR_SOCKET",
|
|
85
|
-
"UND_ERR_CONNECT_TIMEOUT",
|
|
86
|
-
"ECONNECT_TIMEOUT",
|
|
87
|
-
]);
|
|
88
|
-
/** How many `cause` links to follow; wraps are shallow in practice. */
|
|
89
|
-
const PROBE_ERROR_CAUSE_DEPTH = 5;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Whether one probe error should be retried before it becomes a verdict.
|
|
93
|
-
*
|
|
94
|
-
* Deliberately narrow: an abort is *our* deadline firing, so retrying it would
|
|
95
|
-
* spend the whole probe budget on a slow egress, and an unknown error is left
|
|
96
|
-
* alone rather than guessed at. Only transport codes that describe a single
|
|
97
|
-
* dead connection qualify.
|
|
98
|
-
*/
|
|
99
|
-
export function isRetryableProbeError(error: unknown): boolean {
|
|
100
|
-
|
|
101
|
-
// Walk the `cause` chain: a transport failure is routinely wrapped — the
|
|
102
|
-
// proxy layer raises `ProxyConnectHandshakeError` whose *message* carries the
|
|
103
|
-
// real code, with nothing on `code`. Checking only the top error would miss
|
|
104
|
-
// exactly the failure this exists for.
|
|
105
|
-
let current: unknown = error;
|
|
106
|
-
for (let depth = 0; depth < PROBE_ERROR_CAUSE_DEPTH && current !== undefined && current !== null; depth += 1) {
|
|
107
|
-
if (!(current instanceof Error)) break;
|
|
108
|
-
if (current.name === "AbortError" || current.name === "TimeoutError") return false;
|
|
109
|
-
// `code` is not on the base `Error`; narrow before reading it rather than
|
|
110
|
-
// asserting a shape the runtime may not have.
|
|
111
|
-
const code: string | undefined =
|
|
112
|
-
"code" in current && typeof current.code === "string" ? current.code : undefined;
|
|
113
|
-
if (code !== undefined && RETRYABLE_TRANSPORT_CODES.has(code.toUpperCase())) return true;
|
|
114
|
-
const message = String(current.message ?? "");
|
|
115
|
-
// The wrapped code is often only in the message, so match it there too.
|
|
116
|
-
for (const candidate of RETRYABLE_TRANSPORT_CODES) {
|
|
117
|
-
if (message.includes(candidate)) return true;
|
|
118
|
-
}
|
|
119
|
-
if (
|
|
120
|
-
message.includes("fetch failed") ||
|
|
121
|
-
message.includes("socket connection was closed unexpectedly") ||
|
|
122
|
-
message.includes("other side closed") ||
|
|
123
|
-
message.startsWith("terminated") ||
|
|
124
|
-
/connection (?:was|is) closed/i.test(message)
|
|
125
|
-
) {
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
current = current.cause;
|
|
129
|
-
}
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
|
|
1
|
+
/**
|
|
2
|
+
* What one provider-probe answer means about the egress.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the store so the console, the CLI probe
|
|
5
|
+
* (`scripts/tools/probe-egress.ts`) and the tests cannot drift into three
|
|
6
|
+
* readings of the same response. The rule that matters is not obvious:
|
|
7
|
+
*
|
|
8
|
+
* **The provider's IP gate runs before it validates the body.** So a `4xx`
|
|
9
|
+
* complaint about the payload proves the address was accepted — it is the
|
|
10
|
+
* probe's request that is wrong, not the proxy. Mapping that to `unreachable`
|
|
11
|
+
* ("the probe could not complete") blamed the proxy for our own defect and hid
|
|
12
|
+
* working egresses behind a verdict that said the pool was unusable.
|
|
13
|
+
*
|
|
14
|
+
* Only the free-tier refusal retires an address. Every other answer either says
|
|
15
|
+
* the address works (`allowed`, `throttled`) or says nothing about it
|
|
16
|
+
* (`unreachable`), and treating "says nothing" as "refused" would drop a usable
|
|
17
|
+
* pool for good.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type ProbeVerdict = "allowed" | "blocked" | "throttled" | "unreachable";
|
|
21
|
+
|
|
22
|
+
export interface ProbeAnswer {
|
|
23
|
+
readonly status: ProbeVerdict;
|
|
24
|
+
/** Operator-facing reason; absent when the answer needs none. */
|
|
25
|
+
readonly reason?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The upstream's own wording for "this address is retired". */
|
|
29
|
+
const FREE_TIER_REFUSAL = /freetier|free tier can only be used/i;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Classifies one upstream response.
|
|
33
|
+
*
|
|
34
|
+
* `407` is separated from the generic 4xx branch on purpose: it comes from the
|
|
35
|
+
* proxy front door, never from the provider, so it is a credential problem on
|
|
36
|
+
* the tunnel and says nothing about whether the address is refused.
|
|
37
|
+
*/
|
|
38
|
+
export function classifyProviderProbeAnswer(httpStatus: number, body: string): ProbeAnswer {
|
|
39
|
+
if (httpStatus === 403 && FREE_TIER_REFUSAL.test(body)) {
|
|
40
|
+
return { status: "blocked", reason: "Upstream refused this egress address" };
|
|
41
|
+
}
|
|
42
|
+
if (httpStatus === 407) {
|
|
43
|
+
return { status: "unreachable", reason: "Proxy requires authentication" };
|
|
44
|
+
}
|
|
45
|
+
if (httpStatus === 429) {
|
|
46
|
+
return { status: "throttled", reason: "Address accepted; rate limit spent" };
|
|
47
|
+
}
|
|
48
|
+
if (httpStatus >= 200 && httpStatus < 300) {
|
|
49
|
+
return { status: "allowed" };
|
|
50
|
+
}
|
|
51
|
+
if (httpStatus >= 500) {
|
|
52
|
+
return { status: "unreachable", reason: `Upstream answered ${httpStatus}` };
|
|
53
|
+
}
|
|
54
|
+
// Any other 4xx: the request reached the upstream's own validation, which
|
|
55
|
+
// only happens once the address is accepted.
|
|
56
|
+
return {
|
|
57
|
+
status: "allowed",
|
|
58
|
+
reason: `Reachable; upstream answered ${httpStatus} for the probe body`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Error codes that mean "this attempt never reached the upstream", not "the
|
|
64
|
+
* upstream refused this address".
|
|
65
|
+
*
|
|
66
|
+
* Every one of them is worth a second attempt, and the reason is the same in
|
|
67
|
+
* each case: the failure is a property of one socket, not of the egress. A
|
|
68
|
+
* proxy that has just been dialled for the first time has to resolve, open a
|
|
69
|
+
* tunnel, and complete a TLS handshake, and any of those steps can be refused
|
|
70
|
+
* or reset by a middlebox that accepts the very next attempt — which is exactly
|
|
71
|
+
* the "fails once, works on retry" report. Retiring the pool on the first one
|
|
72
|
+
* blames the address for a cold-start race.
|
|
73
|
+
*/
|
|
74
|
+
const RETRYABLE_TRANSPORT_CODES = new Set([
|
|
75
|
+
"ECONNREFUSED",
|
|
76
|
+
"ECONNRESET",
|
|
77
|
+
"ECONNABORTED",
|
|
78
|
+
"EHOSTUNREACH",
|
|
79
|
+
"ENETUNREACH",
|
|
80
|
+
"ENETRESET",
|
|
81
|
+
"EPIPE",
|
|
82
|
+
"ETIMEDOUT",
|
|
83
|
+
"EAI_AGAIN",
|
|
84
|
+
"UND_ERR_SOCKET",
|
|
85
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
86
|
+
"ECONNECT_TIMEOUT",
|
|
87
|
+
]);
|
|
88
|
+
/** How many `cause` links to follow; wraps are shallow in practice. */
|
|
89
|
+
const PROBE_ERROR_CAUSE_DEPTH = 5;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether one probe error should be retried before it becomes a verdict.
|
|
93
|
+
*
|
|
94
|
+
* Deliberately narrow: an abort is *our* deadline firing, so retrying it would
|
|
95
|
+
* spend the whole probe budget on a slow egress, and an unknown error is left
|
|
96
|
+
* alone rather than guessed at. Only transport codes that describe a single
|
|
97
|
+
* dead connection qualify.
|
|
98
|
+
*/
|
|
99
|
+
export function isRetryableProbeError(error: unknown): boolean {
|
|
100
|
+
|
|
101
|
+
// Walk the `cause` chain: a transport failure is routinely wrapped — the
|
|
102
|
+
// proxy layer raises `ProxyConnectHandshakeError` whose *message* carries the
|
|
103
|
+
// real code, with nothing on `code`. Checking only the top error would miss
|
|
104
|
+
// exactly the failure this exists for.
|
|
105
|
+
let current: unknown = error;
|
|
106
|
+
for (let depth = 0; depth < PROBE_ERROR_CAUSE_DEPTH && current !== undefined && current !== null; depth += 1) {
|
|
107
|
+
if (!(current instanceof Error)) break;
|
|
108
|
+
if (current.name === "AbortError" || current.name === "TimeoutError") return false;
|
|
109
|
+
// `code` is not on the base `Error`; narrow before reading it rather than
|
|
110
|
+
// asserting a shape the runtime may not have.
|
|
111
|
+
const code: string | undefined =
|
|
112
|
+
"code" in current && typeof current.code === "string" ? current.code : undefined;
|
|
113
|
+
if (code !== undefined && RETRYABLE_TRANSPORT_CODES.has(code.toUpperCase())) return true;
|
|
114
|
+
const message = String(current.message ?? "");
|
|
115
|
+
// The wrapped code is often only in the message, so match it there too.
|
|
116
|
+
for (const candidate of RETRYABLE_TRANSPORT_CODES) {
|
|
117
|
+
if (message.includes(candidate)) return true;
|
|
118
|
+
}
|
|
119
|
+
if (
|
|
120
|
+
message.includes("fetch failed") ||
|
|
121
|
+
message.includes("socket connection was closed unexpectedly") ||
|
|
122
|
+
message.includes("other side closed") ||
|
|
123
|
+
message.startsWith("terminated") ||
|
|
124
|
+
/connection (?:was|is) closed/i.test(message)
|
|
125
|
+
) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
current = current.cause;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
@@ -1,181 +1,181 @@
|
|
|
1
|
-
// Network-pool control-plane HTTP routes. The only module that touches Elysia.
|
|
2
|
-
import { Elysia, t } from "elysia";
|
|
3
|
-
import { consoleErrorHandler } from "../../shared/errors";
|
|
4
|
-
import { literalUnion } from "../../shared/elysia-schema";
|
|
5
|
-
import { RELAY_TARGETS, type RelayDeployRequest } from "./relay-deploy";
|
|
6
|
-
import { RELAY_REGIONS } from "./relay-regions";
|
|
7
|
-
import { SPEED_TEST_DEFAULT_BYTES } from "./speed-test-sizes";
|
|
8
|
-
import {
|
|
9
|
-
MAX_BATCH_PROBE_TARGETS,
|
|
10
|
-
POOL_KINDS,
|
|
11
|
-
POOL_ROUTING_STRATEGIES,
|
|
12
|
-
POOL_STATUSES,
|
|
13
|
-
type CreateNetworkPoolRequest,
|
|
14
|
-
type NetworkPoolConfig,
|
|
15
|
-
type PoolStrategySetting,
|
|
16
|
-
} from "./contracts";
|
|
17
|
-
import { createNetworkPoolOperations } from "./operations";
|
|
18
|
-
import { parseProxyFile } from "./import-parse";
|
|
19
|
-
|
|
20
|
-
const poolKindSchema = literalUnion(POOL_KINDS);
|
|
21
|
-
const createPoolBody = t.Object({
|
|
22
|
-
kind: poolKindSchema,
|
|
23
|
-
label: t.Optional(t.String()),
|
|
24
|
-
endpoint: t.String(),
|
|
25
|
-
credential: t.Optional(t.String()),
|
|
26
|
-
maxInflight: t.Optional(t.Number()),
|
|
27
|
-
weight: t.Optional(t.Number()),
|
|
28
|
-
status: t.Optional(literalUnion(POOL_STATUSES)),
|
|
29
|
-
config: t.Optional(t.Record(t.String(), t.Unknown())),
|
|
30
|
-
/** Egress allowance in bytes; `null` clears it back to unmetered. */
|
|
31
|
-
quotaBytes: t.Optional(t.Union([t.Number({ minimum: 1 }), t.Null()])),
|
|
32
|
-
/** Operator-set family/tag naming which family this node belongs to. */
|
|
33
|
-
family: t.Optional(t.Union([t.String(), t.Null()])),
|
|
34
|
-
});
|
|
35
|
-
const deployRelayBody = t.Object({
|
|
36
|
-
target: literalUnion(RELAY_TARGETS),
|
|
37
|
-
token: t.String({ minLength: 1 }),
|
|
38
|
-
accountId: t.Optional(t.String()),
|
|
39
|
-
projectName: t.Optional(t.String()),
|
|
40
|
-
/** Vercel function region; ignored for targets that have no region control. */
|
|
41
|
-
region: t.Optional(literalUnion(RELAY_REGIONS)),
|
|
42
|
-
});
|
|
43
|
-
const testBatchBody = t.Object({
|
|
44
|
-
targets: t.Array(createPoolBody, { minItems: 1, maxItems: MAX_BATCH_PROBE_TARGETS }),
|
|
45
|
-
});
|
|
46
|
-
const updatePoolBody = t.Partial(createPoolBody);
|
|
47
|
-
/**
|
|
48
|
-
* A pasted proxy list. `content` is the file's text; the shapes accepted are
|
|
49
|
-
* documented on {@link parseProxyFile}. `maxInflight` and `weight` apply to
|
|
50
|
-
* every imported row, because a file carries no per-row tuning.
|
|
51
|
-
*/
|
|
52
|
-
const importPoolsBody = t.Object({
|
|
53
|
-
content: t.String({ minLength: 1 }),
|
|
54
|
-
maxInflight: t.Optional(t.Number()),
|
|
55
|
-
weight: t.Optional(t.Number()),
|
|
56
|
-
family: t.Optional(t.Union([t.String(), t.Null()])),
|
|
57
|
-
labelPrefix: t.Optional(t.String()),
|
|
58
|
-
});
|
|
59
|
-
export function createNetworkPoolRoutes(config: NetworkPoolConfig): Elysia {
|
|
60
|
-
const factory = createNetworkPoolOperations(config);
|
|
61
|
-
return new Elysia({ prefix: "/network/pools" })
|
|
62
|
-
.error(consoleErrorHandler("Network pool operation failed"))
|
|
63
|
-
.get("/", async ({ request }) => {
|
|
64
|
-
return await factory.listPools(config.accessResolver(request));
|
|
65
|
-
})
|
|
66
|
-
.get("/:poolId", async ({ request, params }) => {
|
|
67
|
-
return await factory.getPoolDetail(config.accessResolver(request), params.poolId);
|
|
68
|
-
})
|
|
69
|
-
.post("/", { body: createPoolBody }, async ({ request, body, set }) => {
|
|
70
|
-
set.status = 201;
|
|
71
|
-
return await factory.createPool(
|
|
72
|
-
config.accessResolver(request),
|
|
73
|
-
body as CreateNetworkPoolRequest,
|
|
74
|
-
);
|
|
75
|
-
})
|
|
76
|
-
.post("/import", { body: importPoolsBody }, async ({ request, body, set }) => {
|
|
77
|
-
set.status = 200;
|
|
78
|
-
return await factory.importPools(config.accessResolver(request), body);
|
|
79
|
-
})
|
|
80
|
-
.patch("/:poolId", { body: updatePoolBody }, async ({ request, params, body }) => {
|
|
81
|
-
return await factory.updatePool(
|
|
82
|
-
config.accessResolver(request),
|
|
83
|
-
params.poolId,
|
|
84
|
-
body as Partial<CreateNetworkPoolRequest>,
|
|
85
|
-
);
|
|
86
|
-
})
|
|
87
|
-
.delete("/:poolId", async ({ request, params }) => {
|
|
88
|
-
return await factory.deletePool(config.accessResolver(request), params.poolId);
|
|
89
|
-
})
|
|
90
|
-
.get("/:poolId/health-events", async ({ request, params }) => {
|
|
91
|
-
return await factory.listPoolHealthEvents(config.accessResolver(request), params.poolId);
|
|
92
|
-
})
|
|
93
|
-
.post("/:poolId/recover", async ({ request, params }) => {
|
|
94
|
-
return await factory.recoverPool(config.accessResolver(request), params.poolId);
|
|
95
|
-
})
|
|
96
|
-
.post(
|
|
97
|
-
"/:poolId/provider-probe",
|
|
98
|
-
{ body: t.Object({ providerId: t.String() }) },
|
|
99
|
-
async ({ request, params, body }) => {
|
|
100
|
-
return await factory.providerProbe(
|
|
101
|
-
config.accessResolver(request),
|
|
102
|
-
params.poolId,
|
|
103
|
-
body.providerId,
|
|
104
|
-
);
|
|
105
|
-
},
|
|
106
|
-
)
|
|
107
|
-
.post("/:poolId/health-check", async ({ request, params }) => {
|
|
108
|
-
return await factory.healthCheck(config.accessResolver(request), params.poolId);
|
|
109
|
-
})
|
|
110
|
-
.post(
|
|
111
|
-
"/:poolId/speed-test",
|
|
112
|
-
{ body: t.Optional(t.Object({ bytes: t.Optional(t.Number()) })) },
|
|
113
|
-
async ({ request, params, body }) => {
|
|
114
|
-
return await factory.speedTest(
|
|
115
|
-
config.accessResolver(request),
|
|
116
|
-
params.poolId,
|
|
117
|
-
body?.bytes ?? SPEED_TEST_DEFAULT_BYTES,
|
|
118
|
-
);
|
|
119
|
-
},
|
|
120
|
-
)
|
|
121
|
-
.post("/test", { body: createPoolBody }, async ({ request, body }) => {
|
|
122
|
-
return await factory.probeAdHocPool(
|
|
123
|
-
config.accessResolver(request),
|
|
124
|
-
body as CreateNetworkPoolRequest,
|
|
125
|
-
);
|
|
126
|
-
})
|
|
127
|
-
.post("/test-batch", { body: testBatchBody }, async ({ request, body }) => {
|
|
128
|
-
return await factory.probeAdHocPoolBatch(
|
|
129
|
-
config.accessResolver(request),
|
|
130
|
-
body.targets as CreateNetworkPoolRequest[],
|
|
131
|
-
);
|
|
132
|
-
})
|
|
133
|
-
.post(
|
|
134
|
-
"/test-batch/provider",
|
|
135
|
-
{
|
|
136
|
-
body: t.Object({
|
|
137
|
-
providerId: t.String(),
|
|
138
|
-
targets: t.Array(createPoolBody, { minItems: 1, maxItems: MAX_BATCH_PROBE_TARGETS }),
|
|
139
|
-
}),
|
|
140
|
-
},
|
|
141
|
-
async ({ request, body }) => {
|
|
142
|
-
return await factory.probeAdHocProviderBatch(
|
|
143
|
-
config.accessResolver(request),
|
|
144
|
-
body.targets as CreateNetworkPoolRequest[],
|
|
145
|
-
body.providerId,
|
|
146
|
-
);
|
|
147
|
-
},
|
|
148
|
-
)
|
|
149
|
-
.post(
|
|
150
|
-
"/:poolId/cooldowns/clear",
|
|
151
|
-
{ body: t.Optional(t.Object({ providerId: t.Optional(t.String()) })) },
|
|
152
|
-
async ({ request, params, body }) => {
|
|
153
|
-
return await factory.clearPoolCooldown(
|
|
154
|
-
config.accessResolver(request),
|
|
155
|
-
params.poolId,
|
|
156
|
-
body?.providerId,
|
|
157
|
-
);
|
|
158
|
-
},
|
|
159
|
-
)
|
|
160
|
-
.post("/relay/deploy", { body: deployRelayBody }, async ({ request, body }) => {
|
|
161
|
-
return await factory.deployRelay(config.accessResolver(request), body as RelayDeployRequest);
|
|
162
|
-
})
|
|
163
|
-
.get("/strategy", async ({ request }) => {
|
|
164
|
-
return await factory.getStrategy(config.accessResolver(request));
|
|
165
|
-
})
|
|
166
|
-
.patch(
|
|
167
|
-
"/strategy",
|
|
168
|
-
{
|
|
169
|
-
body: t.Object({
|
|
170
|
-
strategy: t.Optional(literalUnion(POOL_ROUTING_STRATEGIES)),
|
|
171
|
-
rotateCount: t.Optional(t.Number()),
|
|
172
|
-
}),
|
|
173
|
-
},
|
|
174
|
-
async ({ request, body }) => {
|
|
175
|
-
return await factory.updateStrategy(
|
|
176
|
-
config.accessResolver(request),
|
|
177
|
-
body as Partial<PoolStrategySetting>,
|
|
178
|
-
);
|
|
179
|
-
},
|
|
180
|
-
) as unknown as Elysia;
|
|
181
|
-
}
|
|
1
|
+
// Network-pool control-plane HTTP routes. The only module that touches Elysia.
|
|
2
|
+
import { Elysia, t } from "elysia";
|
|
3
|
+
import { consoleErrorHandler } from "../../shared/errors";
|
|
4
|
+
import { literalUnion } from "../../shared/elysia-schema";
|
|
5
|
+
import { RELAY_TARGETS, type RelayDeployRequest } from "./relay-deploy";
|
|
6
|
+
import { RELAY_REGIONS } from "./relay-regions";
|
|
7
|
+
import { SPEED_TEST_DEFAULT_BYTES } from "./speed-test-sizes";
|
|
8
|
+
import {
|
|
9
|
+
MAX_BATCH_PROBE_TARGETS,
|
|
10
|
+
POOL_KINDS,
|
|
11
|
+
POOL_ROUTING_STRATEGIES,
|
|
12
|
+
POOL_STATUSES,
|
|
13
|
+
type CreateNetworkPoolRequest,
|
|
14
|
+
type NetworkPoolConfig,
|
|
15
|
+
type PoolStrategySetting,
|
|
16
|
+
} from "./contracts";
|
|
17
|
+
import { createNetworkPoolOperations } from "./operations";
|
|
18
|
+
import { parseProxyFile } from "./import-parse";
|
|
19
|
+
|
|
20
|
+
const poolKindSchema = literalUnion(POOL_KINDS);
|
|
21
|
+
const createPoolBody = t.Object({
|
|
22
|
+
kind: poolKindSchema,
|
|
23
|
+
label: t.Optional(t.String()),
|
|
24
|
+
endpoint: t.String(),
|
|
25
|
+
credential: t.Optional(t.String()),
|
|
26
|
+
maxInflight: t.Optional(t.Number()),
|
|
27
|
+
weight: t.Optional(t.Number()),
|
|
28
|
+
status: t.Optional(literalUnion(POOL_STATUSES)),
|
|
29
|
+
config: t.Optional(t.Record(t.String(), t.Unknown())),
|
|
30
|
+
/** Egress allowance in bytes; `null` clears it back to unmetered. */
|
|
31
|
+
quotaBytes: t.Optional(t.Union([t.Number({ minimum: 1 }), t.Null()])),
|
|
32
|
+
/** Operator-set family/tag naming which family this node belongs to. */
|
|
33
|
+
family: t.Optional(t.Union([t.String(), t.Null()])),
|
|
34
|
+
});
|
|
35
|
+
const deployRelayBody = t.Object({
|
|
36
|
+
target: literalUnion(RELAY_TARGETS),
|
|
37
|
+
token: t.String({ minLength: 1 }),
|
|
38
|
+
accountId: t.Optional(t.String()),
|
|
39
|
+
projectName: t.Optional(t.String()),
|
|
40
|
+
/** Vercel function region; ignored for targets that have no region control. */
|
|
41
|
+
region: t.Optional(literalUnion(RELAY_REGIONS)),
|
|
42
|
+
});
|
|
43
|
+
const testBatchBody = t.Object({
|
|
44
|
+
targets: t.Array(createPoolBody, { minItems: 1, maxItems: MAX_BATCH_PROBE_TARGETS }),
|
|
45
|
+
});
|
|
46
|
+
const updatePoolBody = t.Partial(createPoolBody);
|
|
47
|
+
/**
|
|
48
|
+
* A pasted proxy list. `content` is the file's text; the shapes accepted are
|
|
49
|
+
* documented on {@link parseProxyFile}. `maxInflight` and `weight` apply to
|
|
50
|
+
* every imported row, because a file carries no per-row tuning.
|
|
51
|
+
*/
|
|
52
|
+
const importPoolsBody = t.Object({
|
|
53
|
+
content: t.String({ minLength: 1 }),
|
|
54
|
+
maxInflight: t.Optional(t.Number()),
|
|
55
|
+
weight: t.Optional(t.Number()),
|
|
56
|
+
family: t.Optional(t.Union([t.String(), t.Null()])),
|
|
57
|
+
labelPrefix: t.Optional(t.String()),
|
|
58
|
+
});
|
|
59
|
+
export function createNetworkPoolRoutes(config: NetworkPoolConfig): Elysia {
|
|
60
|
+
const factory = createNetworkPoolOperations(config);
|
|
61
|
+
return new Elysia({ prefix: "/network/pools" })
|
|
62
|
+
.error(consoleErrorHandler("Network pool operation failed"))
|
|
63
|
+
.get("/", async ({ request }) => {
|
|
64
|
+
return await factory.listPools(config.accessResolver(request));
|
|
65
|
+
})
|
|
66
|
+
.get("/:poolId", async ({ request, params }) => {
|
|
67
|
+
return await factory.getPoolDetail(config.accessResolver(request), params.poolId);
|
|
68
|
+
})
|
|
69
|
+
.post("/", { body: createPoolBody }, async ({ request, body, set }) => {
|
|
70
|
+
set.status = 201;
|
|
71
|
+
return await factory.createPool(
|
|
72
|
+
config.accessResolver(request),
|
|
73
|
+
body as CreateNetworkPoolRequest,
|
|
74
|
+
);
|
|
75
|
+
})
|
|
76
|
+
.post("/import", { body: importPoolsBody }, async ({ request, body, set }) => {
|
|
77
|
+
set.status = 200;
|
|
78
|
+
return await factory.importPools(config.accessResolver(request), body);
|
|
79
|
+
})
|
|
80
|
+
.patch("/:poolId", { body: updatePoolBody }, async ({ request, params, body }) => {
|
|
81
|
+
return await factory.updatePool(
|
|
82
|
+
config.accessResolver(request),
|
|
83
|
+
params.poolId,
|
|
84
|
+
body as Partial<CreateNetworkPoolRequest>,
|
|
85
|
+
);
|
|
86
|
+
})
|
|
87
|
+
.delete("/:poolId", async ({ request, params }) => {
|
|
88
|
+
return await factory.deletePool(config.accessResolver(request), params.poolId);
|
|
89
|
+
})
|
|
90
|
+
.get("/:poolId/health-events", async ({ request, params }) => {
|
|
91
|
+
return await factory.listPoolHealthEvents(config.accessResolver(request), params.poolId);
|
|
92
|
+
})
|
|
93
|
+
.post("/:poolId/recover", async ({ request, params }) => {
|
|
94
|
+
return await factory.recoverPool(config.accessResolver(request), params.poolId);
|
|
95
|
+
})
|
|
96
|
+
.post(
|
|
97
|
+
"/:poolId/provider-probe",
|
|
98
|
+
{ body: t.Object({ providerId: t.String() }) },
|
|
99
|
+
async ({ request, params, body }) => {
|
|
100
|
+
return await factory.providerProbe(
|
|
101
|
+
config.accessResolver(request),
|
|
102
|
+
params.poolId,
|
|
103
|
+
body.providerId,
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
.post("/:poolId/health-check", async ({ request, params }) => {
|
|
108
|
+
return await factory.healthCheck(config.accessResolver(request), params.poolId);
|
|
109
|
+
})
|
|
110
|
+
.post(
|
|
111
|
+
"/:poolId/speed-test",
|
|
112
|
+
{ body: t.Optional(t.Object({ bytes: t.Optional(t.Number()) })) },
|
|
113
|
+
async ({ request, params, body }) => {
|
|
114
|
+
return await factory.speedTest(
|
|
115
|
+
config.accessResolver(request),
|
|
116
|
+
params.poolId,
|
|
117
|
+
body?.bytes ?? SPEED_TEST_DEFAULT_BYTES,
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
.post("/test", { body: createPoolBody }, async ({ request, body }) => {
|
|
122
|
+
return await factory.probeAdHocPool(
|
|
123
|
+
config.accessResolver(request),
|
|
124
|
+
body as CreateNetworkPoolRequest,
|
|
125
|
+
);
|
|
126
|
+
})
|
|
127
|
+
.post("/test-batch", { body: testBatchBody }, async ({ request, body }) => {
|
|
128
|
+
return await factory.probeAdHocPoolBatch(
|
|
129
|
+
config.accessResolver(request),
|
|
130
|
+
body.targets as CreateNetworkPoolRequest[],
|
|
131
|
+
);
|
|
132
|
+
})
|
|
133
|
+
.post(
|
|
134
|
+
"/test-batch/provider",
|
|
135
|
+
{
|
|
136
|
+
body: t.Object({
|
|
137
|
+
providerId: t.String(),
|
|
138
|
+
targets: t.Array(createPoolBody, { minItems: 1, maxItems: MAX_BATCH_PROBE_TARGETS }),
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
async ({ request, body }) => {
|
|
142
|
+
return await factory.probeAdHocProviderBatch(
|
|
143
|
+
config.accessResolver(request),
|
|
144
|
+
body.targets as CreateNetworkPoolRequest[],
|
|
145
|
+
body.providerId,
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
.post(
|
|
150
|
+
"/:poolId/cooldowns/clear",
|
|
151
|
+
{ body: t.Optional(t.Object({ providerId: t.Optional(t.String()) })) },
|
|
152
|
+
async ({ request, params, body }) => {
|
|
153
|
+
return await factory.clearPoolCooldown(
|
|
154
|
+
config.accessResolver(request),
|
|
155
|
+
params.poolId,
|
|
156
|
+
body?.providerId,
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
.post("/relay/deploy", { body: deployRelayBody }, async ({ request, body }) => {
|
|
161
|
+
return await factory.deployRelay(config.accessResolver(request), body as RelayDeployRequest);
|
|
162
|
+
})
|
|
163
|
+
.get("/strategy", async ({ request }) => {
|
|
164
|
+
return await factory.getStrategy(config.accessResolver(request));
|
|
165
|
+
})
|
|
166
|
+
.patch(
|
|
167
|
+
"/strategy",
|
|
168
|
+
{
|
|
169
|
+
body: t.Object({
|
|
170
|
+
strategy: t.Optional(literalUnion(POOL_ROUTING_STRATEGIES)),
|
|
171
|
+
rotateCount: t.Optional(t.Number()),
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
async ({ request, body }) => {
|
|
175
|
+
return await factory.updateStrategy(
|
|
176
|
+
config.accessResolver(request),
|
|
177
|
+
body as Partial<PoolStrategySetting>,
|
|
178
|
+
);
|
|
179
|
+
},
|
|
180
|
+
) as unknown as Elysia;
|
|
181
|
+
}
|