cartethyia 2.7.4 → 2.7.5
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 +2 -0
- package/README.md +17 -5
- package/dist/dashboard/assets/{BannedPage-ChG5svFb.js → BannedPage-Cj15GsMp.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Cj15GsMp.js.gz +0 -0
- package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js +1 -0
- package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-BIcWQun7.js → CliToolDetailPage-DZKmVuJN.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-Bf2VelDe.js → CliToolsPage-BIccZmeL.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DEEKkYD-.js +1 -0
- package/dist/dashboard/assets/CombosPage-DEEKkYD-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-ggeMSjte.js → CustomizationPage-BKj_r0rp.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js +656 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-U6AKaJcF.js → LoginPage-DkXb6dx-.js} +1 -1
- package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BMS2t8BA.js → ModelPicker-DnQPSIA8.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-DnQPSIA8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js +1 -0
- package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js +5 -0
- package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-lsrx7NYg.js → ProxyPage-5SnUSy3j.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js +1 -0
- package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js +1 -0
- package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-f0DE1h0-.js → SetupPage-WTa0sktC.js} +1 -1
- package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-Cq5tW7hW.js → StudioPage-BfAb5-o_.js} +3 -3
- package/dist/dashboard/assets/StudioPage-BfAb5-o_.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js +56 -0
- package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-l29JROPv.js +1 -0
- package/dist/dashboard/assets/api-keys-l29JROPv.js.gz +0 -0
- package/dist/dashboard/assets/app--PJWq8_C.css +2 -0
- package/dist/dashboard/assets/app--PJWq8_C.css.gz +0 -0
- package/dist/dashboard/assets/assert-C0K-c4eo.js +1 -0
- package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
- package/dist/dashboard/assets/{badge-D9CS6dZk.js → badge-DfbhQU4k.js} +1 -1
- package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DympBNq4.js → banner-BllT4LcF.js} +1 -1
- package/dist/dashboard/assets/banner-BllT4LcF.js.gz +0 -0
- package/dist/dashboard/assets/brand/login_register.jpg +0 -0
- package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
- package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
- package/dist/dashboard/assets/brand/og_image.jpg +0 -0
- package/dist/dashboard/assets/{card-ibRWU_uT.js → card-CtkX66Wd.js} +1 -1
- package/dist/dashboard/assets/card-CtkX66Wd.js.gz +0 -0
- package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js +1 -0
- package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CQXVMn4P.js +1 -0
- package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DJm-9pz7.js → clipboard-button-BQqBQd2n.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-BQqBQd2n.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-8fiE6tec.js → dialog-BEK7KP0H.js} +1 -1
- package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CqGYeOrd.js → drawer-BrTEZ8zu.js} +1 -1
- package/dist/dashboard/assets/drawer-BrTEZ8zu.js.gz +0 -0
- package/dist/dashboard/assets/entry-CHWxbS56.js +1 -0
- package/dist/dashboard/assets/entry-CHWxbS56.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BSAsYsmV.js → entry-EBHJ3DrE.js} +1 -1
- package/dist/dashboard/assets/entry-EBHJ3DrE.js.gz +0 -0
- package/dist/dashboard/assets/entry-LCS85hiq.js +4 -0
- package/dist/dashboard/assets/entry-LCS85hiq.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-D-MTJxMr.js → formatters-BAACA727.js} +1 -1
- package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-CPK47xQx.js → github-badge-YSCE4UC2.js} +1 -1
- package/dist/dashboard/assets/github-badge-YSCE4UC2.js.gz +0 -0
- package/dist/dashboard/assets/{index-DBNPOfhV.js → index-FQlj0HUT.js} +2 -2
- package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BT0zntEr.js → inline-C4gU5iOL.js} +1 -1
- package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
- package/dist/dashboard/assets/{input-Da2ZViWg.js → input-BHHX_Tmm.js} +1 -1
- package/dist/dashboard/assets/input-BHHX_Tmm.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BXNeLw6M.js → layout-CajrYUnD.js} +1 -1
- package/dist/dashboard/assets/layout-CajrYUnD.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BycOrrIV.js → list-row-Bx2I7XUe.js} +1 -1
- package/dist/dashboard/assets/list-row-Bx2I7XUe.js.gz +0 -0
- package/dist/dashboard/assets/live-DNNqRbj2.js +1 -0
- package/dist/dashboard/assets/live-DNNqRbj2.js.gz +0 -0
- package/dist/dashboard/assets/network-C5UBACrJ.js +1 -0
- package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CUWAZK4J.js +2 -0
- package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
- package/dist/dashboard/assets/pagination-Xsi9Qx04.js +1 -0
- package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
- package/dist/dashboard/assets/quota-DvQbD_sr.js +1 -0
- package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
- package/dist/dashboard/assets/routing-in0f_IfK.js +1 -0
- package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
- package/dist/dashboard/assets/{select-BIYIBkMU.js → select-BLULNqTp.js} +1 -1
- package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
- package/dist/dashboard/assets/settings-CcGczj6I.js +1 -0
- package/dist/dashboard/assets/settings-CcGczj6I.js.gz +0 -0
- package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
- package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DfgnFDWY.js → stack-D2lIR0tg.js} +1 -1
- package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
- package/dist/dashboard/assets/{state-CkfdYWel.js → state-DTARjfiW.js} +1 -1
- package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
- package/dist/dashboard/assets/{switch-SYm237HG.js → switch-c0oPSnqW.js} +1 -1
- package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BQqCRNIi.js → useMutation-m8WGNPrz.js} +1 -1
- package/dist/dashboard/assets/useMutation-m8WGNPrz.js.gz +0 -0
- package/dist/dashboard/console-backgrounds.jpg +0 -0
- package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +56 -55
- package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
- package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
- package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
- package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
- package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
- package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
- package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
- package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
- package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
- package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
- package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
- package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
- package/package.json +4 -6
- package/scripts/cli/update.ts +15 -41
- package/src/boot/app.ts +58 -2
- package/src/boot/dependencies.ts +786 -732
- package/src/boot/lifecycle.ts +11 -12
- package/src/config/cooldowns.ts +34 -55
- package/src/config/network.ts +1 -1
- package/src/config/package-paths.ts +1 -1
- package/src/config/providers.ts +13 -14
- package/src/config/resources.ts +0 -5
- package/src/config/runtime.ts +2 -5
- package/src/config/spec.ts +26 -6
- package/src/console/api-keys/ban-constants.ts +38 -0
- package/src/console/api-keys/contracts.ts +71 -62
- package/src/console/api-keys/routes.ts +48 -12
- package/src/console/auth/access.ts +4 -4
- package/src/console/auth/account-reset.ts +1 -1
- package/src/console/auth/audit-recorder.ts +1 -1
- package/src/console/auth/lockout.ts +7 -3
- package/src/console/auth/row-coercion.ts +57 -12
- package/src/console/auth/session-resolver.ts +1 -1
- package/src/console/auth/session-service.ts +8 -2
- package/src/console/auth/session.ts +37 -31
- package/src/console/auth/setup-service.ts +4 -3
- package/src/console/auth/user-store.ts +1 -1
- package/src/console/backup/9router-map.ts +0 -3
- package/src/console/backup/contracts.ts +9 -9
- package/src/console/backup/export-endpoint.ts +5 -6
- package/src/console/backup/nine-router.ts +1 -7
- package/src/console/backup/routes.ts +29 -33
- package/src/console/backup/service.ts +468 -468
- package/src/console/backup/store.ts +47 -29
- package/src/console/backup/validate.ts +11 -1
- package/src/console/cli-tools/contracts.ts +7 -7
- package/src/console/cli-tools/injectors/cline.ts +110 -110
- package/src/console/cli-tools/injectors/codex.ts +181 -181
- package/src/console/cli-tools/injectors/copilot.ts +108 -108
- package/src/console/cli-tools/injectors/driver.ts +247 -247
- package/src/console/cli-tools/injectors/jcode.ts +115 -115
- package/src/console/cli-tools/injectors/openclaw.ts +161 -161
- package/src/console/cli-tools/injectors/opencode.ts +153 -153
- package/src/console/cli-tools/routes.ts +1 -1
- package/src/console/cli-tools/service.ts +1 -1
- package/src/console/cli-tools/store.ts +2 -2
- package/src/console/console-router.ts +32 -0
- package/src/console/dashboard-assets.ts +9 -9
- package/src/console/domain-registration.ts +16 -2
- package/src/console/inferhub/client.ts +0 -7
- package/src/console/inferhub/key-store.ts +1 -1
- package/src/console/inferhub/normalize.ts +2 -2
- package/src/console/inferhub/routes.ts +1 -1
- package/src/console/inferhub/types.ts +1 -1
- package/src/console/observability/contracts.ts +1 -1
- package/src/console/observability/live.ts +31 -18
- package/src/console/observability/logs.ts +1 -1
- package/src/console/observability/sse.ts +62 -8
- package/src/console/observability/store.ts +20 -4
- package/src/console/providers/catalog/catalog-projections.ts +1 -1
- package/src/console/providers/catalog/contracts.ts +39 -0
- package/src/console/providers/catalog/model-operations.ts +1 -1
- package/src/console/providers/catalog/provider-operations.ts +95 -17
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/routes.ts +27 -3
- package/src/console/providers/catalog/store.ts +147 -8
- package/src/console/providers/detail/routes.ts +1 -1
- package/src/console/providers/oauth/callback-listener.ts +100 -18
- package/src/console/providers/oauth/routes.ts +92 -13
- package/src/console/quota/account-quota-global.ts +41 -3
- package/src/console/quota/account-quota-shared.ts +10 -3
- package/src/console/quota/account-quota-tenant.ts +259 -66
- package/src/console/quota/daily-growth-job.ts +308 -0
- package/src/console/quota/native-identity.ts +2 -2
- package/src/console/quota/quota-view.ts +1 -1
- package/src/console/remote-support/routes.ts +1 -1
- package/src/console/remote-support/service.ts +2 -2
- package/src/console/routing/model/contracts.ts +1 -1
- package/src/console/routing/pools/contracts.ts +2 -2
- package/src/console/routing/pools/import-parse.ts +3 -3
- package/src/console/routing/pools/relay-deploy.ts +3 -3
- package/src/console/settings/contracts.ts +29 -2
- package/src/console/settings/store.ts +20 -0
- package/src/console/share/share-router.ts +100 -32
- package/src/console/share/share-stats.ts +4 -4
- package/src/console/share/share-usage.ts +1 -1
- package/src/console/shared/errors.ts +2 -2
- package/src/console/shared/object.ts +18 -0
- package/src/console/shared/read-through-cache.ts +4 -4
- package/src/console/shared/transaction.ts +14 -0
- package/src/console/system-prompt/contracts.ts +89 -12
- package/src/console/system-prompt/routes.ts +98 -45
- package/src/console/updates/apply-update.ts +244 -0
- package/src/console/updates/routes.ts +252 -97
- package/src/main.ts +11 -5
- package/src/network/http2-bidi.ts +1 -1
- package/src/network/ip-utils.ts +298 -0
- package/src/network/outbound-fetch.ts +1 -1
- package/src/network/pool/agent.ts +7 -6
- package/src/network/pool/byte-accounting.ts +1 -1
- package/src/network/pool/resolver.ts +16 -12
- package/src/network/pool/selector.ts +150 -136
- package/src/network/pool-health-machine.ts +1 -1
- package/src/network/rate-limit-verdict.ts +1 -1
- package/src/network/retry.ts +1 -1
- package/src/network/ssrf.ts +20 -71
- package/src/observability/event-log.ts +2 -2
- package/src/observability/event-query.ts +59 -291
- package/src/observability/log-sanitize.ts +1 -1
- package/src/observability/metrics.ts +4 -4
- package/src/observability/payload-capture.ts +6 -5
- package/src/observability/payload-store.ts +1 -1
- package/src/observability/runtime-metrics.ts +1 -1
- package/src/observability/telemetry-buffer.ts +12 -3
- package/src/observability/telemetry-reconciliation.ts +3 -3
- package/src/observability/token-speed.ts +1 -1
- package/src/persistence/connection-url.ts +1 -1
- package/src/persistence/db-handle.ts +1 -1
- package/src/persistence/db-pg.ts +2 -2
- package/src/persistence/db-sqlite.ts +3 -3
- package/src/persistence/dialect.ts +5 -1
- package/src/persistence/readiness.ts +1 -1
- package/src/persistence/redis.ts +1 -1
- package/src/persistence/schema.ts +113 -40
- package/src/persistence/shared-cache.ts +2 -2
- package/src/persistence/sort-order.ts +1 -1
- package/src/persistence/stores/api-key-store.ts +10 -1
- package/src/persistence/stores/share-store.ts +20 -8
- package/src/persistence/system-prompt-store.ts +63 -18
- package/src/persistence/tenant-preferences.ts +1 -1
- package/src/persistence/ttl-cache.ts +3 -3
- package/src/persistence/usage-flush.ts +7 -2
- package/src/protocol/canonical-model.ts +1 -1
- package/src/protocol/codecs.ts +6 -10
- package/src/protocol/dsml.ts +3 -49
- package/src/protocol/image-clamp.ts +2 -2
- package/src/protocol/reasoning-text.ts +1 -1
- package/src/protocol/reasoning.ts +7 -52
- package/src/protocol/registry.ts +50 -15
- package/src/protocol/request/chat.ts +46 -19
- package/src/protocol/request/codex.ts +1 -1
- package/src/protocol/request/messages.ts +46 -14
- package/src/protocol/request/responses.ts +19 -10
- package/src/protocol/response/chat.ts +25 -10
- package/src/protocol/response/codex.ts +1 -1
- package/src/protocol/response/messages.ts +1 -1
- package/src/protocol/response/omp-tool-call-markup.ts +1 -1
- package/src/protocol/response/responses.ts +1 -1
- package/src/protocol/sse-decoder.ts +50 -1
- package/src/protocol/stop-reason.ts +2 -2
- package/src/protocol/think-split.ts +347 -0
- package/src/protocol/translation/attachment-integrity.ts +3 -3
- package/src/protocol/translation/cache-controls.ts +8 -20
- package/src/protocol/translation/capabilities.ts +6 -6
- package/src/protocol/translation/dialect/deepseek.ts +10 -0
- package/src/protocol/translation/dialect/inband.ts +39 -0
- package/src/protocol/translation/dialect/model-thinking.ts +89 -0
- package/src/protocol/translation/dialect/qwen.ts +114 -0
- package/src/protocol/translation/dialect/resolver.ts +83 -0
- package/src/protocol/translation/dialect/rules.ts +93 -0
- package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
- package/src/protocol/translation/dialect/types.ts +58 -0
- package/src/protocol/translation/quirks.ts +18 -4
- package/src/protocol/translation/thinking.ts +12 -252
- package/src/protocol/translation/tool-id.ts +3 -3
- package/src/protocol/translation/tool-repair.ts +1 -1
- package/src/protocol/upstream/post-json.ts +6 -1
- package/src/providers/adapters/compatible-adapter.ts +42 -3
- package/src/providers/authentication/jwt-validator.ts +0 -5
- package/src/providers/authentication/oauth-client.ts +254 -253
- package/src/providers/authentication/oauth-flow-store.ts +42 -7
- package/src/providers/bundled-discovery-providers.ts +6 -30
- package/src/providers/configured-openai-providers.ts +82 -97
- package/src/providers/default-registry.ts +23 -35
- package/src/providers/discovery/catalog/catalog-format.ts +13 -2
- package/src/providers/discovery/discovery-types.ts +0 -1
- package/src/providers/discovery/openai-model-discovery.ts +2 -2
- package/src/providers/discovery/probe-phases.ts +48 -13
- package/src/providers/discovery/probing-service.ts +6 -8
- package/src/providers/integrations/agentrouter/agentrouter.ts +12 -2
- package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +0 -14
- package/src/providers/integrations/antigravity/antigravity.ts +2 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
- package/src/providers/integrations/buddy/buddy-identity.ts +86 -0
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
- package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -15
- package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
- package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -9
- package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
- package/src/providers/integrations/claude/claude-cch.ts +148 -148
- package/src/providers/integrations/claude/fallback-credit.ts +3 -7
- package/src/providers/integrations/cline/cline.ts +1 -1
- package/src/providers/integrations/codex/codex-device-code.ts +101 -101
- package/src/providers/integrations/codex/codex-oauth.ts +0 -1
- package/src/providers/integrations/commandcode/commandcode.ts +75 -115
- package/src/providers/integrations/commandcode/quota.ts +131 -51
- package/src/providers/integrations/cursor/cursor-http2.ts +0 -3
- package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
- package/src/providers/integrations/cursor/cursor-request.ts +0 -87
- package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
- package/src/providers/integrations/cursor/cursor.ts +38 -5
- package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
- package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
- package/src/providers/integrations/factory/factory.ts +9 -9
- package/src/providers/integrations/github/github-discovery.ts +55 -10
- package/src/providers/integrations/github/github-oauth.ts +6 -5
- package/src/providers/integrations/github/github.ts +41 -14
- package/src/providers/integrations/grok/grok-oauth.ts +261 -261
- package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
- package/src/providers/integrations/grok/grok-quota.ts +16 -7
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
- package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
- package/src/providers/integrations/grok/grok.ts +86 -36
- package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
- package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
- package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
- package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
- package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
- package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
- package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
- package/src/providers/integrations/kiro/kiro-request.ts +8 -11
- package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
- package/src/providers/integrations/kiro/kiro.ts +81 -68
- package/src/providers/integrations/muse/muse-oauth.ts +0 -1
- package/src/providers/integrations/muse/muse-quota.ts +0 -4
- package/src/providers/integrations/opencode/fingerprint.ts +7 -5
- package/src/providers/integrations/opencode/provider.ts +17 -24
- package/src/providers/integrations/qoder/qoder-catalog.ts +85 -8
- package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
- package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
- package/src/providers/integrations/qoder/qoder-realm.ts +10 -31
- package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +21 -25
- package/src/providers/integrations/qoder/qoder-virtual.ts +12 -0
- package/src/providers/integrations/qoder/qoder.ts +354 -62
- package/src/providers/integrations/shared/connect.ts +0 -54
- package/src/providers/integrations/shared/hash.ts +15 -0
- package/src/providers/integrations/shared/session-cache.ts +136 -0
- package/src/providers/integrations/trae/trae-cache.ts +7 -87
- package/src/providers/integrations/trae/trae-catalog.ts +1 -6
- package/src/providers/integrations/trae/trae-protocol.ts +17 -8
- package/src/providers/integrations/trae/trae-realm.ts +0 -7
- package/src/providers/integrations/trae/trae.ts +53 -10
- package/src/providers/integrations/xai/xai-oauth.ts +5 -5
- package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
- package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
- package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
- package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
- package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
- package/src/providers/operations/account-error-classification.ts +131 -116
- package/src/providers/operations/account-health-service.ts +31 -39
- package/src/providers/operations/account-reset-service.ts +58 -1
- package/src/providers/operations/cli-platform.ts +31 -5
- package/src/providers/operations/client-version-resolver.ts +4 -6
- package/src/providers/operations/client-versions.ts +10 -7
- package/src/providers/operations/provider-catalog-seeder.ts +2 -0
- package/src/providers/operations/provider-catalog-service.ts +18 -2
- package/src/providers/operations/provider-credential-service.ts +19 -2
- package/src/providers/provider-contracts.ts +18 -7
- package/src/providers/provider-metadata.ts +17 -2
- package/src/providers/provider-model-definition.ts +22 -1
- package/src/providers/provider-probe.ts +4 -2
- package/src/providers/provider-usage.ts +81 -16
- package/src/providers/quota/cache.ts +29 -0
- package/src/providers/quota/quota-contracts.ts +4 -0
- package/src/providers/quota/refresh.ts +27 -4
- package/src/providers/quota/snapshot.ts +94 -0
- package/src/providers/search/search-provider.ts +1 -3
- package/src/providers/search/search-providers.ts +2 -1
- package/src/security/abuse.ts +4 -4
- package/src/security/admission/service.ts +2 -2
- package/src/security/admission/ttl.ts +61 -61
- package/src/security/api-key-auth.ts +16 -4
- package/src/security/client-router-fingerprint.ts +4 -4
- package/src/security/credential-codec.ts +3 -3
- package/src/security/csrf.ts +2 -2
- package/src/security/ip-boundary.ts +16 -189
- package/src/security/model-access-rule.ts +1 -1
- package/src/security/outbound-headers.ts +18 -0
- package/src/shared/invoke-guard.ts +47 -0
- package/src/transport/contracts.ts +13 -4
- package/src/transport/dispatch/attempt-finalize.ts +1 -1
- package/src/transport/dispatch/capture-policy.ts +34 -37
- package/src/transport/dispatch/empty-stream.ts +23 -20
- package/src/transport/dispatch/generation.ts +25 -45
- package/src/transport/dispatch/native-shared.ts +127 -0
- package/src/transport/dispatch/proxy-request.ts +3 -1
- package/src/transport/dispatch/responses-compact.ts +25 -46
- package/src/transport/dispatch/streaming/encoder.ts +18 -72
- package/src/transport/dispatch/streaming/finalize.ts +2 -2
- package/src/transport/dispatch/streaming/stream.ts +7 -3
- package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
- package/src/transport/dispatch/systemone.ts +20 -48
- package/src/transport/dispatch/token-count.ts +41 -38
- package/src/transport/dispatch/websearch.ts +19 -45
- package/src/transport/drain-endpoint.ts +1 -1
- package/src/transport/failure-policy.ts +6 -0
- package/src/transport/guards/auth-guard.ts +0 -6
- package/src/transport/guards/body-policy.ts +3 -3
- package/src/transport/guards/headroom-guard.ts +1 -1
- package/src/transport/guards/pipeline.ts +1 -1
- package/src/transport/guards/request-context.ts +12 -17
- package/src/transport/request/preparer.ts +286 -7
- package/src/transport/request/rtk/compress-request.ts +3 -3
- package/src/transport/request/rtk/rtk-constants.ts +2 -2
- package/src/transport/request/rtk/rtk-filters.ts +8 -8
- package/src/transport/request/state.ts +1 -13
- package/src/transport/request/system-prompt-injection.ts +74 -11
- package/src/transport/request/tenant-preferences.ts +22 -0
- package/src/transport/routing/candidate-index.ts +2 -2
- package/src/transport/routing/round-robin.ts +1 -1
- package/src/transport/routing/route-catalog.ts +17 -18
- package/src/transport/routing/route-model.ts +126 -33
- package/src/transport/routing/router.ts +152 -37
- package/src/transport/shutdown-notice.ts +2 -2
- package/src/transport/surface/adapters.ts +3 -3
- package/src/transport/surface/chat/encode.ts +7 -7
- package/src/transport/surface/chat/parse.ts +7 -7
- package/src/transport/surface/completion.ts +2 -2
- package/src/transport/surface/content-parts.ts +1 -1
- package/src/transport/surface/dialects.ts +2 -2
- package/src/transport/surface/messages/parse.ts +3 -3
- package/src/transport/surface/responses/adapter.ts +1 -1
- package/src/transport/surface/responses/errors.ts +0 -9
- package/src/transport/vision-handoff-cache.ts +58 -0
- package/src/transport/vision-handoff.ts +376 -0
- package/src/workers/account-identity-sweep.ts +2 -2
- package/src/workers/buddy-growth-tasks.ts +392 -346
- package/src/workers/daily-checkin.ts +102 -49
- package/src/workers/maintenance-lease.ts +2 -35
- package/src/workers/oauth-refresh-sweep.ts +8 -1
- package/src/workers/quota-refresh-sweep.ts +120 -82
- package/src/workers/scheduler.ts +67 -4
- package/src/workers/sweep-runner.ts +24 -5
- package/src/workers/zcode-gift-claim.ts +108 -0
- package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
- package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
- package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js +0 -1
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +0 -1
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +0 -5
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js +0 -1
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-ClzpwyET.js +0 -1
- package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +0 -56
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js +0 -1
- package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
- package/dist/dashboard/assets/app-a159k0ub.css +0 -2
- package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js +0 -15
- package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
- package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
- package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
- package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js +0 -1
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
- package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
- package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js +0 -4
- package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js +0 -1
- package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
- package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
- package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
- package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
- package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
- package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
- package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js +0 -1
- package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js +0 -1
- package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js +0 -2
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js +0 -1
- package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js +0 -1
- package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
- package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js +0 -1
- package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
- package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
- package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
- package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
- package/src/network/response-headers.ts +0 -39
- package/src/protocol/response/dsml.ts +0 -38
- package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
- /package/dist/dashboard/assets/{app-BayTTy01.js → app-Bgd3zANQ.js} +0 -0
- /package/dist/dashboard/assets/{app-BayTTy01.js.gz → app-Bgd3zANQ.js.gz} +0 -0
- /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
- /package/dist/dashboard/assets/brand/{og_image.webp → og_image_initial.webp} +0 -0
package/src/main.ts
CHANGED
|
@@ -33,6 +33,12 @@ const boot = Manifest.isCapturing()
|
|
|
33
33
|
? undefined
|
|
34
34
|
: (globalThis.__cartethyiaBoot ??= await bootstrap());
|
|
35
35
|
|
|
36
|
+
// Hoisted once: each resolver is an env parse + range check on a cold path, and
|
|
37
|
+
// the values are boot-stable, so computing them repeatedly buys nothing.
|
|
38
|
+
const maxBodyBytes = resolveMaxBodyBytes();
|
|
39
|
+
const topology = resolveProcessTopology();
|
|
40
|
+
const drainToken = resolveDrainToken();
|
|
41
|
+
|
|
36
42
|
const app = boot
|
|
37
43
|
? createGatewayApp({
|
|
38
44
|
mode: "production",
|
|
@@ -49,7 +55,7 @@ const app = boot
|
|
|
49
55
|
telemetryBuffer: boot.deps.telemetryBuffer,
|
|
50
56
|
resolveOAuthRefresher: boot.deps.resolveOAuthRefresher,
|
|
51
57
|
oauthRefreshService: boot.deps.oauthRefreshService,
|
|
52
|
-
maxBodyBytes
|
|
58
|
+
maxBodyBytes,
|
|
53
59
|
scheduledTasks: boot.deps.scheduledTasks,
|
|
54
60
|
// The sampler is constructed in the dependency graph and already
|
|
55
61
|
// sampling on its own interval; without passing it here the app cannot
|
|
@@ -60,8 +66,8 @@ const app = boot
|
|
|
60
66
|
shutdownCoordinator: boot.shutdownCoordinator,
|
|
61
67
|
// Signal-free graceful stop for platforms where a catchable signal cannot
|
|
62
68
|
// be delivered (Windows). Off unless `CARTETHYIA_DRAIN_TOKEN` is set.
|
|
63
|
-
...(
|
|
64
|
-
? { drainToken
|
|
69
|
+
...(drainToken !== undefined
|
|
70
|
+
? { drainToken, triggerDrain: () => shutdown("SIGTERM") }
|
|
65
71
|
: {}),
|
|
66
72
|
// The console mounts in every boot: without REDIS_URL it runs on the
|
|
67
73
|
// in-process memory backend like the rest of the gateway.
|
|
@@ -141,10 +147,10 @@ if (boot) {
|
|
|
141
147
|
// left always-on: with one process declared, enabling it lets an
|
|
142
148
|
// unrelated process on the same port silently take half the traffic —
|
|
143
149
|
// which is worse than a bind error, because nothing reports it.
|
|
144
|
-
reusePort:
|
|
150
|
+
reusePort: topology.multiProcess,
|
|
145
151
|
// 8 MiB cap on request bodies keeps a rogue client from allocating
|
|
146
152
|
// gigabytes of memory in the ingress path before validation rejects.
|
|
147
|
-
maxRequestBodySize:
|
|
153
|
+
maxRequestBodySize: maxBodyBytes,
|
|
148
154
|
},
|
|
149
155
|
() => {
|
|
150
156
|
log.info(`Cartethyia listening on :${port} (Bun ${Bun.version})`);
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// and never opens a socket of its own.
|
|
14
14
|
|
|
15
15
|
/** Response metadata of one opened bidi stream. */
|
|
16
|
-
|
|
16
|
+
interface Http2BidiResponse {
|
|
17
17
|
/** HTTP/2 `:status`; `0` before the response headers arrive. */
|
|
18
18
|
readonly status: number;
|
|
19
19
|
readonly headers: Readonly<Record<string, string>>;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// Shared IP parsing primitives: IPv4 → number, IPv6 → groups/bytes/BigInt,
|
|
2
|
+
// CIDR containment, and loopback detection.
|
|
3
|
+
//
|
|
4
|
+
// Consolidates the two independent copies that used to live in `ssrf.ts` and
|
|
5
|
+
// `security/ip-boundary.ts`. The two modules' semantics differ **on purpose**,
|
|
6
|
+
// so every shared entry point carries the divergence as an explicit opt-in
|
|
7
|
+
// rather than silently picking one behavior:
|
|
8
|
+
//
|
|
9
|
+
// - **Dotted-quad IPv6** (`::ffff:1.2.3.4`): the SSRF guard accepts it (the DNS
|
|
10
|
+
// layer can hand it one); the client-identity layer deliberately fail-closes
|
|
11
|
+
// its BigInt key path on a dotted tail so an unparseable spelling can never
|
|
12
|
+
// mint a database uniqueness key.
|
|
13
|
+
// - **CIDR**: the SSRF guard requires an explicit `/prefix` (a bare address
|
|
14
|
+
// matches nothing — fail-closed on an allowlist) and does not unwrap
|
|
15
|
+
// IPv4-mapped spellings; the trusted-proxy boundary treats a bare address as
|
|
16
|
+
// "this exact host" and unwraps mapped spellings, because Bun reports an
|
|
17
|
+
// IPv4 loopback peer as `::ffff:127.0.0.1` on Windows.
|
|
18
|
+
import { isIP } from "node:net";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* IPv4 literal as a 32-bit unsigned number, or `undefined` when `address` is
|
|
22
|
+
* not a plain dotted quad. Decimal octets only — the exotic spellings `Number`
|
|
23
|
+
* accepts (`0x`, leading `+`, octal) are refused, because this value feeds
|
|
24
|
+
* allowlist comparisons where a lenient parse would widen the match.
|
|
25
|
+
*/
|
|
26
|
+
export function ipv4ToNumber(address: string): number | undefined {
|
|
27
|
+
const parts = address.split(".");
|
|
28
|
+
if (parts.length !== 4 || parts.some((part) => !/^\d+$/.test(part))) return undefined;
|
|
29
|
+
const octets = parts.map(Number);
|
|
30
|
+
if (octets.some((part) => part < 0 || part > 255)) return undefined;
|
|
31
|
+
const [a, b, c, d] = octets;
|
|
32
|
+
if (a === undefined || b === undefined || c === undefined || d === undefined) return undefined;
|
|
33
|
+
return ((a * 256 + b) * 256 + c) * 256 + d;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Same parse as {@link ipv4ToNumber}, but a refusal is an error. The SSRF
|
|
38
|
+
* guard's internal call sites only ever reach the parser with an `isIP`-validated
|
|
39
|
+
* string, so a malformed octet there is an invariant breach, not input handling.
|
|
40
|
+
*/
|
|
41
|
+
export function ipv4ToNumberStrict(address: string): number {
|
|
42
|
+
const value = ipv4ToNumber(address);
|
|
43
|
+
if (value === undefined) throw new Error("invalid IPv4");
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* An IPv6 literal as exactly eight lowercase 16-bit hex groups, or `undefined`
|
|
49
|
+
* when it is not one. Expands a `::` run (which must stand for at least one
|
|
50
|
+
* group) and — with `allowDottedQuad` — converts a trailing dotted quad into
|
|
51
|
+
* the two groups it represents.
|
|
52
|
+
*
|
|
53
|
+
* `allowDottedQuad` is the preserved SSRF/boundary divergence: the SSRF guard
|
|
54
|
+
* passes `true` (DNS answers can arrive as `::ffff:1.2.3.4`), the client-identity
|
|
55
|
+
* key path leaves the default (`false`) so a dotted spelling never reaches a
|
|
56
|
+
* database uniqueness key. The zone id (`%eth0`) is stripped; it identifies an
|
|
57
|
+
* interface, not an address.
|
|
58
|
+
*/
|
|
59
|
+
export function normalizeIpv6Groups(
|
|
60
|
+
value: string,
|
|
61
|
+
opts: { allowDottedQuad?: boolean } = {},
|
|
62
|
+
): string[] | undefined {
|
|
63
|
+
const allowDottedQuad = opts.allowDottedQuad ?? false;
|
|
64
|
+
const text = value.toLowerCase().trim().split("%", 1)[0] ?? "";
|
|
65
|
+
if (text.length === 0) return undefined;
|
|
66
|
+
const doubleIndex = text.indexOf("::");
|
|
67
|
+
// More than one `::` is invalid, as is a `::` inside a group.
|
|
68
|
+
if (doubleIndex !== text.lastIndexOf("::")) return undefined;
|
|
69
|
+
|
|
70
|
+
const headText = doubleIndex >= 0 ? text.slice(0, doubleIndex) : text;
|
|
71
|
+
const tailText = doubleIndex >= 0 ? text.slice(doubleIndex + 2) : "";
|
|
72
|
+
const head = headText.length > 0 ? headText.split(":") : [];
|
|
73
|
+
const tail = tailText.length > 0 ? tailText.split(":") : [];
|
|
74
|
+
|
|
75
|
+
// A dotted quad is a single token that fills two groups.
|
|
76
|
+
const expandDotted = (tokens: string[]): string[] | undefined => {
|
|
77
|
+
const last = tokens[tokens.length - 1];
|
|
78
|
+
if (last === undefined || !last.includes(".")) return tokens;
|
|
79
|
+
if (!allowDottedQuad) return undefined;
|
|
80
|
+
const quad = last.split(".");
|
|
81
|
+
if (quad.length !== 4) return undefined;
|
|
82
|
+
const octets = quad.map((part) => Number(part));
|
|
83
|
+
if (octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return undefined;
|
|
84
|
+
const [a, b, c, d] = octets;
|
|
85
|
+
if (a === undefined || b === undefined || c === undefined || d === undefined) return undefined;
|
|
86
|
+
const high = ((a << 8) | b).toString(16);
|
|
87
|
+
const low = ((c << 8) | d).toString(16);
|
|
88
|
+
return [...tokens.slice(0, -1), high, low];
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const expandedHead = expandDotted(head);
|
|
92
|
+
const expandedTail = expandDotted(tail);
|
|
93
|
+
if (expandedHead === undefined || expandedTail === undefined) return undefined;
|
|
94
|
+
|
|
95
|
+
let full: string[];
|
|
96
|
+
if (doubleIndex >= 0) {
|
|
97
|
+
const missing = 8 - expandedHead.length - expandedTail.length;
|
|
98
|
+
// `::` must stand for at least one group, so exactly-8 without it is invalid.
|
|
99
|
+
if (missing < 1) return undefined;
|
|
100
|
+
full = [...expandedHead, ...Array.from({ length: missing }, () => "0"), ...expandedTail];
|
|
101
|
+
} else {
|
|
102
|
+
full = expandedHead;
|
|
103
|
+
}
|
|
104
|
+
if (full.length !== 8) return undefined;
|
|
105
|
+
const normalized: string[] = [];
|
|
106
|
+
for (const group of full) {
|
|
107
|
+
if (!/^[0-9a-f]{1,4}$/.test(group)) return undefined;
|
|
108
|
+
normalized.push(Number.parseInt(group, 16).toString(16));
|
|
109
|
+
}
|
|
110
|
+
return normalized;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The 16 bytes of an IPv6 literal, or `undefined` when it is not one. */
|
|
114
|
+
export function ipv6GroupsToBytes(groups: readonly string[]): Uint8Array | undefined {
|
|
115
|
+
if (groups.length !== 8) return undefined;
|
|
116
|
+
const bytes = new Uint8Array(16);
|
|
117
|
+
for (let index = 0; index < 8; index += 1) {
|
|
118
|
+
const group = groups[index]!;
|
|
119
|
+
if (!/^[0-9a-f]{1,4}$/.test(group)) return undefined;
|
|
120
|
+
const n = Number.parseInt(group, 16);
|
|
121
|
+
bytes[index * 2] = n >>> 8;
|
|
122
|
+
bytes[index * 2 + 1] = n & 255;
|
|
123
|
+
}
|
|
124
|
+
return bytes;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The 128-bit value of an IPv6 literal, or `undefined` when it is not one. */
|
|
128
|
+
export function ipv6GroupsToBigInt(groups: readonly string[]): bigint | undefined {
|
|
129
|
+
if (groups.length !== 8) return undefined;
|
|
130
|
+
let result = 0n;
|
|
131
|
+
for (const group of groups) {
|
|
132
|
+
if (!/^[0-9a-f]{1,4}$/.test(group)) return undefined;
|
|
133
|
+
result = (result << 16n) | BigInt(Number.parseInt(group, 16));
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 128-bit value of an IPv6 literal; `allowDottedQuad` opts into the dotted tail. */
|
|
139
|
+
export function ipv6ToBigInt(address: string, opts: { allowDottedQuad?: boolean } = {}): bigint | undefined {
|
|
140
|
+
const groups = normalizeIpv6Groups(address, opts);
|
|
141
|
+
return groups === undefined ? undefined : ipv6GroupsToBigInt(groups);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The 32-bit tail of an IPv4-mapped IPv6 address, or `undefined` when `value` is
|
|
146
|
+
* not one. Accepts every spelling of the same address: `::ffff:1.2.3.4`,
|
|
147
|
+
* `::ffff:102:304`, `0:0:0:0:0:ffff:102:304`, and `::ffff:0:102:304`.
|
|
148
|
+
*
|
|
149
|
+
* Works by normalizing to exactly eight 16-bit groups first. That is the only way
|
|
150
|
+
* to compare spellings: a dotted tail is written as ONE colon-separated token but
|
|
151
|
+
* occupies TWO group slots, so any check that counts `split(":")` entries gets the
|
|
152
|
+
* position of the `ffff` marker wrong.
|
|
153
|
+
*/
|
|
154
|
+
function mappedIpv4Tail(value: string): string | undefined {
|
|
155
|
+
const groups = normalizeIpv6Groups(value, { allowDottedQuad: true });
|
|
156
|
+
if (groups === undefined) return undefined;
|
|
157
|
+
// The first five groups (80 bits) must be zero, and the sixth must be `ffff`.
|
|
158
|
+
for (let index = 0; index < 5; index += 1) {
|
|
159
|
+
if (groups[index] !== "0") return undefined;
|
|
160
|
+
}
|
|
161
|
+
if (groups[5] !== "ffff") return undefined;
|
|
162
|
+
const high = groups[6];
|
|
163
|
+
const low = groups[7];
|
|
164
|
+
if (high === undefined || low === undefined) return undefined;
|
|
165
|
+
return `${high}:${low}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Unwraps an IPv4-mapped IPv6 address to plain IPv4.
|
|
170
|
+
*
|
|
171
|
+
* Bun reports an IPv4 loopback peer as `::ffff:127.0.0.1` on Windows, and
|
|
172
|
+
* `isIP` classifies it as IPv6 — so a parser that bails on a dotted quad never
|
|
173
|
+
* matched `127.0.0.1/32`. Accepts both the dotted and the hex (`::ffff:7f00:1`)
|
|
174
|
+
* spelling.
|
|
175
|
+
*/
|
|
176
|
+
export function unwrapMappedIpv4(address: string): string {
|
|
177
|
+
const value = address.toLowerCase().split("%", 1)[0] ?? "";
|
|
178
|
+
// Recognize the mapped range STRUCTURALLY, not by its compressed spelling. The
|
|
179
|
+
// old `startsWith("::ffff:")` test missed any form that writes the leading zero
|
|
180
|
+
// groups out — `0:0:0:0:0:ffff:7f00:1` is the same host as `::ffff:127.0.0.1`,
|
|
181
|
+
// but it failed the prefix test, fell through to the generic IPv6 parse, and
|
|
182
|
+
// was treated as an unrelated IPv6 address. The trust check then refused a peer
|
|
183
|
+
// the allowlist covered, and `canonicalClientIpKey` minted a second key for one
|
|
184
|
+
// host — which is stored on issued child keys for consistent usage reporting
|
|
185
|
+
// and audit.
|
|
186
|
+
//
|
|
187
|
+
// A mapped address is `::ffff:<32 bits>`, i.e. the first 80 bits are zero and the
|
|
188
|
+
// next 16 are `0xffff`. Matching that is the same test `isIP` uses internally.
|
|
189
|
+
const tail = mappedIpv4Tail(value);
|
|
190
|
+
if (tail === undefined) return address;
|
|
191
|
+
// `mappedIpv4Tail` always returns two hex groups, so the dotted and hex
|
|
192
|
+
// spellings have already converged by here.
|
|
193
|
+
const groups = tail.split(":");
|
|
194
|
+
if (groups.length !== 2) return address;
|
|
195
|
+
const high = Number.parseInt(groups[0] ?? "", 16);
|
|
196
|
+
const low = Number.parseInt(groups[1] ?? "", 16);
|
|
197
|
+
if (
|
|
198
|
+
!Number.isInteger(high) ||
|
|
199
|
+
!Number.isInteger(low) ||
|
|
200
|
+
high < 0 ||
|
|
201
|
+
high > 0xffff ||
|
|
202
|
+
low < 0 ||
|
|
203
|
+
low > 0xffff
|
|
204
|
+
)
|
|
205
|
+
return address;
|
|
206
|
+
return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface CidrContainsOptions {
|
|
210
|
+
/**
|
|
211
|
+
* `true` (SSRF guard): the CIDR must carry an explicit `/prefix` — a bare
|
|
212
|
+
* address matches nothing, so a misconfigured allowlist entry fails closed.
|
|
213
|
+
* `false` (trusted-proxy boundary): a bare address means "this exact host".
|
|
214
|
+
*/
|
|
215
|
+
requirePrefix?: boolean;
|
|
216
|
+
/**
|
|
217
|
+
* `true` (trusted-proxy boundary): unwrap IPv4-mapped spellings on both the
|
|
218
|
+
* subject and the network before comparing, so `::ffff:127.0.0.1` and
|
|
219
|
+
* `127.0.0.1/32` describe the same host. `false` (SSRF guard): compare the
|
|
220
|
+
* literal as written.
|
|
221
|
+
*/
|
|
222
|
+
unwrapMapped?: boolean;
|
|
223
|
+
/**
|
|
224
|
+
* The SSRF guard's IPv6 parse accepts the dotted-quad spelling (`::ffff:1.2.3.4`,
|
|
225
|
+
* which DNS answers can take); the boundary path refuses it. Defaults to
|
|
226
|
+
* `false`; the SSRF call site passes `true`.
|
|
227
|
+
*/
|
|
228
|
+
allowDottedQuad?: boolean;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Whether `address` falls inside `cidr`. The blank-prefix refusal
|
|
233
|
+
* (`"10.0.0.0/"` must NOT mean /0) is shared by both former implementations and
|
|
234
|
+
* is preserved unconditionally: `Number("")` is 0 and /0 would admit every
|
|
235
|
+
* address of the family — for the SSRF guard that includes the link-local
|
|
236
|
+
* metadata endpoint the guard exists to refuse, and for the trusted-proxy
|
|
237
|
+
* boundary it would believe any caller's `X-Forwarded-For`.
|
|
238
|
+
*/
|
|
239
|
+
export function cidrContainsAddress(
|
|
240
|
+
address: string,
|
|
241
|
+
cidr: string,
|
|
242
|
+
opts: CidrContainsOptions = {},
|
|
243
|
+
): boolean {
|
|
244
|
+
const requirePrefix = opts.requirePrefix ?? true;
|
|
245
|
+
const unwrapMapped = opts.unwrapMapped ?? false;
|
|
246
|
+
const allowDottedQuad = opts.allowDottedQuad ?? false;
|
|
247
|
+
const [network, rawPrefix] = cidr.split("/");
|
|
248
|
+
if (!network) return false;
|
|
249
|
+
const prefixText = rawPrefix === undefined ? undefined : rawPrefix.trim();
|
|
250
|
+
// Requiring a run of digits also refuses the exotic spellings `Number`
|
|
251
|
+
// accepts for zero (`-0`, `+0`, `0x0`, `0b0`, `0o0`, `0.0`) — typos, not a
|
|
252
|
+
// deliberate "match everything".
|
|
253
|
+
if (prefixText !== undefined && !/^\d+$/.test(prefixText)) return false;
|
|
254
|
+
if (requirePrefix && prefixText === undefined) return false;
|
|
255
|
+
const subject = unwrapMapped ? unwrapMappedIpv4(address) : address;
|
|
256
|
+
const base = unwrapMapped ? unwrapMappedIpv4(network) : network;
|
|
257
|
+
const family = isIP(subject);
|
|
258
|
+
if (family !== isIP(base)) return false;
|
|
259
|
+
const maxPrefix = family === 4 ? 32 : 128;
|
|
260
|
+
const prefix = prefixText === undefined ? maxPrefix : Number(prefixText);
|
|
261
|
+
if (!Number.isInteger(prefix) || prefix < 0 || prefix > maxPrefix) return false;
|
|
262
|
+
if (family === 4) {
|
|
263
|
+
const value = ipv4ToNumber(subject);
|
|
264
|
+
const networkValue = ipv4ToNumber(base);
|
|
265
|
+
if (value === undefined || networkValue === undefined) return false;
|
|
266
|
+
if (prefix === 0) return true;
|
|
267
|
+
const mask = (0xffffffff << (32 - prefix)) >>> 0;
|
|
268
|
+
return ((value >>> 0) & mask) === ((networkValue >>> 0) & mask);
|
|
269
|
+
}
|
|
270
|
+
if (family !== 6) return false;
|
|
271
|
+
const value = ipv6ToBigInt(subject, { allowDottedQuad });
|
|
272
|
+
const networkValue = ipv6ToBigInt(base, { allowDottedQuad });
|
|
273
|
+
if (value === undefined || networkValue === undefined) return false;
|
|
274
|
+
const shift = BigInt(128 - prefix);
|
|
275
|
+
return (value >> shift) === (networkValue >> shift);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* True when `address` is a loopback address.
|
|
280
|
+
*
|
|
281
|
+
* Structural, not a string-prefix test: `127.0.0.1`, `::1`, and the mapped
|
|
282
|
+
* `::ffff:127.0.0.1` a Windows/Bun peer arrives as are all the same host, and
|
|
283
|
+
* a prefix test would miss the mapped form — the exact shape that hid this
|
|
284
|
+
* class of bug before.
|
|
285
|
+
*
|
|
286
|
+
* `transport/drain-endpoint.ts` keeps its own weaker `isLoopbackPeer` (a
|
|
287
|
+
* `::ffff:` prefix strip that misses the `0:0:0:0:0:ffff:7f00:1` spelling);
|
|
288
|
+
* unifying it widens the admin gate's acceptance set and is deferred — switch
|
|
289
|
+
* it to this predicate as a deliberate, separately-landed behavior widening.
|
|
290
|
+
*/
|
|
291
|
+
export function isLoopbackAddress(address: string): boolean {
|
|
292
|
+
const normalized = unwrapMappedIpv4(address.trim().split("%", 1)[0] ?? "");
|
|
293
|
+
if (normalized === "::1") return true;
|
|
294
|
+
if (isIP(normalized) !== 4) return false;
|
|
295
|
+
const value = ipv4ToNumber(normalized);
|
|
296
|
+
// 127.0.0.0/8 — the whole loopback block, not just 127.0.0.1.
|
|
297
|
+
return value !== undefined && ((value >>> 0) & 0xff000000) === (127 << 24);
|
|
298
|
+
}
|
|
@@ -10,7 +10,7 @@ import { isProxyAgentPair, type ProxyAgentPair } from "./pool/agent";
|
|
|
10
10
|
import { metrics } from "../observability/metrics";
|
|
11
11
|
import { trackNetworkCall } from "../observability/performance-metrics";
|
|
12
12
|
// Validated fetch — single egress with ProxyAgent support
|
|
13
|
-
|
|
13
|
+
interface ValidatedFetchOptions {
|
|
14
14
|
readonly fetchFn?: typeof fetch;
|
|
15
15
|
readonly policy?: SsrfPolicy;
|
|
16
16
|
readonly maxRedirects?: number;
|
|
@@ -44,7 +44,7 @@ export type TransportKind = (typeof TRANSPORT_KINDS)[number];
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
/** Config keys reserved for endpoint/label; every other key is opaque non-secret transport config. */
|
|
47
|
-
|
|
47
|
+
const RESERVED_ENDPOINT_CONFIG_KEYS: Record<string, true> = {
|
|
48
48
|
endpoint: true,
|
|
49
49
|
label: true,
|
|
50
50
|
};
|
|
@@ -122,9 +122,11 @@ function createSsrfLookup(policy: SsrfPolicy): NonNullable<RequestOptions["looku
|
|
|
122
122
|
family?: number,
|
|
123
123
|
) => void,
|
|
124
124
|
): void => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
// No outer AbortController/timer here: `resolveAllAddresses` already races
|
|
126
|
+
// its own 5s deadline against the OS lookup and maps the failure to a
|
|
127
|
+
// deterministic `transport_unavailable` 502. A second timer would only
|
|
128
|
+
// re-race it with a different error class (499/504 depending on timing).
|
|
129
|
+
void resolveAllAddresses(hostname)
|
|
128
130
|
.then((addrs) => {
|
|
129
131
|
try {
|
|
130
132
|
validateResolvedAddresses(addrs, policy);
|
|
@@ -145,8 +147,7 @@ function createSsrfLookup(policy: SsrfPolicy): NonNullable<RequestOptions["looku
|
|
|
145
147
|
}
|
|
146
148
|
cb(null, first, family);
|
|
147
149
|
})
|
|
148
|
-
.catch((error: unknown) => cb(error as Error))
|
|
149
|
-
.finally(() => clearTimeout(timer));
|
|
150
|
+
.catch((error: unknown) => cb(error as Error));
|
|
150
151
|
}) as unknown as NonNullable<RequestOptions["lookup"]>;
|
|
151
152
|
}
|
|
152
153
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/** Running totals for one pool. */
|
|
17
|
-
|
|
17
|
+
interface PoolByteTotals {
|
|
18
18
|
/** Bytes sent to the proxy (uploads, TLS handshake, CONNECT). */
|
|
19
19
|
readonly sent: number;
|
|
20
20
|
/** Bytes received from the proxy (downloads). */
|
|
@@ -50,7 +50,7 @@ const HTTP2_MAX_BUFFERED_BYTES = 64 * 1024 * 1024;
|
|
|
50
50
|
*/
|
|
51
51
|
const HTTP2_SESSION_MAX_MEMORY_MB = 256;
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
interface PoolAgentResolverDeps {
|
|
54
54
|
readonly ssrfPolicy?: SsrfPolicy;
|
|
55
55
|
readonly resolveFn?: (hostname: string, signal: AbortSignal) => Promise<readonly string[]>;
|
|
56
56
|
/** Database used to load pool rows. */
|
|
@@ -256,11 +256,6 @@ export class PoolAgentResolver {
|
|
|
256
256
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
/** Observable cache size for the runtime metrics sampler. */
|
|
260
|
-
agentCount(): number {
|
|
261
|
-
return this.agents.size;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
259
|
async releasePool(poolId: string, tenantId: string): Promise<void> {
|
|
265
260
|
const cacheKey = `${tenantId}:${poolId}`;
|
|
266
261
|
const pending = this.agents.get(cacheKey);
|
|
@@ -274,10 +269,6 @@ export class PoolAgentResolver {
|
|
|
274
269
|
}
|
|
275
270
|
}
|
|
276
271
|
}
|
|
277
|
-
async resolveWebSocketAgent(poolId: string, tenantId: string): Promise<PoolAgent> {
|
|
278
|
-
return this.resolveAgent(poolId, tenantId);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
272
|
private async buildAgent(poolId: string, tenantId: string): Promise<PoolAgent> {
|
|
282
273
|
const row = await this.loader.load(poolId);
|
|
283
274
|
if (!row) throw new PoolBindingError(`network pool ${poolId} not found`);
|
|
@@ -959,6 +950,14 @@ class WsSession implements ProviderWebSocketSession {
|
|
|
959
950
|
}
|
|
960
951
|
export class ValidatedNetworkBindingFactory {
|
|
961
952
|
private readonly directFetch: ValidatedFetch;
|
|
953
|
+
/**
|
|
954
|
+
* One `ValidatedFetch` per resolved pool agent. Building the closure (dialer,
|
|
955
|
+
* relay setup, redirect loop) on every request through a pool was pure
|
|
956
|
+
* churn; keying by the agent object means a released agent is naturally
|
|
957
|
+
* collected (new builds mint a new agent) so no explicit invalidation is
|
|
958
|
+
* needed.
|
|
959
|
+
*/
|
|
960
|
+
private readonly pooledFetchCache = new WeakMap<PoolAgent, ValidatedFetch>();
|
|
962
961
|
|
|
963
962
|
constructor(
|
|
964
963
|
private readonly policy: SsrfPolicy = {},
|
|
@@ -991,7 +990,12 @@ export class ValidatedNetworkBindingFactory {
|
|
|
991
990
|
return async (input, init) => {
|
|
992
991
|
try {
|
|
993
992
|
const agent = await poolResolver.resolveAgent(networkPoolId, tenantId);
|
|
994
|
-
|
|
993
|
+
let validated = this.pooledFetchCache.get(agent);
|
|
994
|
+
if (validated === undefined) {
|
|
995
|
+
validated = createValidatedFetch({ policy, agent });
|
|
996
|
+
this.pooledFetchCache.set(agent, validated);
|
|
997
|
+
}
|
|
998
|
+
return await validated(input, init);
|
|
995
999
|
} catch (error) {
|
|
996
1000
|
if (error instanceof PoolBindingError) {
|
|
997
1001
|
throw new GatewayError(
|
|
@@ -1026,7 +1030,7 @@ export class ValidatedNetworkBindingFactory {
|
|
|
1026
1030
|
throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧");
|
|
1027
1031
|
}
|
|
1028
1032
|
try {
|
|
1029
|
-
agent = await this.poolResolver.
|
|
1033
|
+
agent = await this.poolResolver.resolveAgent(networkPoolId, tenantId);
|
|
1030
1034
|
} catch (error) {
|
|
1031
1035
|
if (error instanceof PoolBindingError) {
|
|
1032
1036
|
throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧");
|