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/security/csrf.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { isRecord } from "../protocol/codecs";
|
|
|
16
16
|
* but cannot read the cookie value, so it cannot forge the header.
|
|
17
17
|
*/
|
|
18
18
|
export const CSRF_COOKIE_NAME = "csrf_token";
|
|
19
|
-
|
|
19
|
+
const CSRF_HEADER_NAME = "x-csrf-token";
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Name of the session cookie whose presence gates CSRF validation in
|
|
@@ -30,7 +30,7 @@ export const SESSION_COOKIE_NAME = "session_token";
|
|
|
30
30
|
* `SessionCookiePolicy` structurally satisfies this (extra fields are fine),
|
|
31
31
|
* so call sites keep passing their existing policy object unchanged.
|
|
32
32
|
*/
|
|
33
|
-
|
|
33
|
+
interface CsrfCookiePolicy {
|
|
34
34
|
readonly maxAge: number;
|
|
35
35
|
readonly sameSite: "Lax" | "Strict" | "None";
|
|
36
36
|
readonly secure: boolean;
|
|
@@ -3,185 +3,25 @@
|
|
|
3
3
|
import { isIP } from "node:net";
|
|
4
4
|
import type { TrustedProxyBoundary } from "../config/network";
|
|
5
5
|
import { log } from "../observability/logger";
|
|
6
|
+
import {
|
|
7
|
+
cidrContainsAddress,
|
|
8
|
+
ipv4ToNumber,
|
|
9
|
+
ipv6ToBigInt,
|
|
10
|
+
isLoopbackAddress,
|
|
11
|
+
unwrapMappedIpv4,
|
|
12
|
+
} from "../network/ip-utils";
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
const parts = address.split(".");
|
|
9
|
-
if (parts.length !== 4 || parts.some((part) => !/^\d+$/.test(part))) return undefined;
|
|
10
|
-
const octets = parts.map(Number);
|
|
11
|
-
if (octets.some((part) => part < 0 || part > 255)) return undefined;
|
|
12
|
-
const [a, b, c, d] = octets;
|
|
13
|
-
if (a === undefined || b === undefined || c === undefined || d === undefined) return undefined;
|
|
14
|
-
return ((a * 256 + b) * 256 + c) * 256 + d;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function ipv6ToBigInt(address: string): bigint | undefined {
|
|
18
|
-
const value = address.toLowerCase().split("%", 1)[0] ?? "";
|
|
19
|
-
if (value.includes(".")) return undefined;
|
|
20
|
-
const halves = value.split("::");
|
|
21
|
-
if (halves.length > 2) return undefined;
|
|
22
|
-
const left = halves[0] ? halves[0].split(":") : [];
|
|
23
|
-
const right = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
|
|
24
|
-
if (halves.length === 1 && left.length !== 8) return undefined;
|
|
25
|
-
const missing = 8 - left.length - right.length;
|
|
26
|
-
if (missing < 0 || (halves.length === 2 && missing < 1)) return undefined;
|
|
27
|
-
const groups = [...left, ...Array.from({ length: missing }, () => "0"), ...right];
|
|
28
|
-
if (groups.length !== 8 || groups.some((part) => !/^[0-9a-f]{1,4}$/.test(part)))
|
|
29
|
-
return undefined;
|
|
30
|
-
let result = 0n;
|
|
31
|
-
for (const group of groups) result = (result << 16n) | BigInt(Number.parseInt(group, 16));
|
|
32
|
-
return result;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Unwraps an IPv4-mapped IPv6 address to plain IPv4.
|
|
37
|
-
*
|
|
38
|
-
* Bun reports an IPv4 loopback peer as `::ffff:127.0.0.1` on Windows, and
|
|
39
|
-
* `isIP` classifies it as IPv6 — so `ipv6ToBigInt` (which bails on a dotted
|
|
40
|
-
* quad) never matched `127.0.0.1/32`. The trust check therefore failed for the
|
|
41
|
-
* exact peer `TRUSTED_PROXY_CIDRS=127.0.0.1/32` is meant to cover, forwarded
|
|
42
|
-
* headers were ignored, and every cloudflared request stored the loopback peer
|
|
43
|
-
* as the client address. Accepts both the dotted and the hex (`::ffff:7f00:1`)
|
|
44
|
-
* spelling.
|
|
45
|
-
*/
|
|
46
|
-
function unwrapMappedIpv4(address: string): string {
|
|
47
|
-
const value = address.toLowerCase().split("%", 1)[0] ?? "";
|
|
48
|
-
// Recognize the mapped range STRUCTURALLY, not by its compressed spelling. The
|
|
49
|
-
// old `startsWith("::ffff:")` test missed any form that writes the leading zero
|
|
50
|
-
// groups out — `0:0:0:0:0:ffff:7f00:1` is the same host as `::ffff:127.0.0.1`,
|
|
51
|
-
// but it failed the prefix test, fell through to `ipv6ToBigInt`, and was treated
|
|
52
|
-
// as an unrelated IPv6 address. The trust check then refused a peer the allowlist
|
|
53
|
-
// covered, and `canonicalClientIpKey` minted a second key for one host — which is
|
|
54
|
-
// stored on issued child keys for consistent usage reporting and audit.
|
|
55
|
-
//
|
|
56
|
-
// A mapped address is `::ffff:<32 bits>`, i.e. the first 80 bits are zero and the
|
|
57
|
-
// next 16 are `0xffff`. Matching that is the same test `isIP` uses internally.
|
|
58
|
-
const tail = mappedIpv4Tail(value);
|
|
59
|
-
if (tail === undefined) return address;
|
|
60
|
-
// `mappedIpv4Tail` always returns two hex groups, so the dotted and hex
|
|
61
|
-
// spellings have already converged by here.
|
|
62
|
-
const groups = tail.split(":");
|
|
63
|
-
if (groups.length !== 2) return address;
|
|
64
|
-
const high = Number.parseInt(groups[0] ?? "", 16);
|
|
65
|
-
const low = Number.parseInt(groups[1] ?? "", 16);
|
|
66
|
-
if (!Number.isInteger(high) || !Number.isInteger(low) || high < 0 || high > 0xffff || low < 0 || low > 0xffff)
|
|
67
|
-
return address;
|
|
68
|
-
return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* The 32-bit tail of an IPv4-mapped IPv6 address, or `undefined` when `value` is
|
|
73
|
-
* not one. Accepts every spelling of the same address: `::ffff:1.2.3.4`,
|
|
74
|
-
* `::ffff:102:304`, `0:0:0:0:0:ffff:102:304`, and `::ffff:0:102:304`.
|
|
75
|
-
*
|
|
76
|
-
* Works by normalizing to exactly eight 16-bit groups first. That is the only way
|
|
77
|
-
* to compare spellings: a dotted tail is written as ONE colon-separated token but
|
|
78
|
-
* occupies TWO group slots, so any check that counts `split(":")` entries gets the
|
|
79
|
-
* position of the `ffff` marker wrong.
|
|
80
|
-
*/
|
|
81
|
-
function mappedIpv4Tail(value: string): string | undefined {
|
|
82
|
-
const groups = normalizeIpv6Groups(value);
|
|
83
|
-
if (groups === undefined) return undefined;
|
|
84
|
-
// The first five groups (80 bits) must be zero, and the sixth must be `ffff`.
|
|
85
|
-
for (let index = 0; index < 5; index += 1) {
|
|
86
|
-
if (groups[index] !== "0") return undefined;
|
|
87
|
-
}
|
|
88
|
-
if (groups[5] !== "ffff") return undefined;
|
|
89
|
-
const high = groups[6];
|
|
90
|
-
const low = groups[7];
|
|
91
|
-
if (high === undefined || low === undefined) return undefined;
|
|
92
|
-
return `${high}:${low}`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* An IPv6 literal as exactly eight lowercase 16-bit hex groups, or `undefined`
|
|
97
|
-
* when it is not one. Expands a `::` run and converts a trailing dotted quad into
|
|
98
|
-
* the two groups it represents.
|
|
99
|
-
*/
|
|
100
|
-
function normalizeIpv6Groups(value: string): string[] | undefined {
|
|
101
|
-
const text = value.trim();
|
|
102
|
-
if (text.length === 0) return undefined;
|
|
103
|
-
const doubleIndex = text.indexOf("::");
|
|
104
|
-
// More than one `::` is invalid, as is a `::` inside a group.
|
|
105
|
-
if (doubleIndex !== text.lastIndexOf("::")) return undefined;
|
|
106
|
-
|
|
107
|
-
const headText = doubleIndex >= 0 ? text.slice(0, doubleIndex) : text;
|
|
108
|
-
const tailText = doubleIndex >= 0 ? text.slice(doubleIndex + 2) : "";
|
|
109
|
-
const head = headText.length > 0 ? headText.split(":") : [];
|
|
110
|
-
const tail = tailText.length > 0 ? tailText.split(":") : [];
|
|
111
|
-
|
|
112
|
-
// A dotted quad is a single token that fills two groups.
|
|
113
|
-
const expandDotted = (tokens: string[]): string[] | undefined => {
|
|
114
|
-
const last = tokens[tokens.length - 1];
|
|
115
|
-
if (last === undefined || !last.includes(".")) return tokens;
|
|
116
|
-
const quad = last.split(".");
|
|
117
|
-
if (quad.length !== 4) return undefined;
|
|
118
|
-
const octets = quad.map((part) => Number(part));
|
|
119
|
-
if (octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return undefined;
|
|
120
|
-
const [a, b, c, d] = octets;
|
|
121
|
-
if (a === undefined || b === undefined || c === undefined || d === undefined) return undefined;
|
|
122
|
-
const high = ((a << 8) | b).toString(16);
|
|
123
|
-
const low = ((c << 8) | d).toString(16);
|
|
124
|
-
return [...tokens.slice(0, -1), high, low];
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const expandedHead = expandDotted(head);
|
|
128
|
-
const expandedTail = expandDotted(tail);
|
|
129
|
-
if (expandedHead === undefined || expandedTail === undefined) return undefined;
|
|
130
|
-
|
|
131
|
-
let full: string[];
|
|
132
|
-
if (doubleIndex >= 0) {
|
|
133
|
-
const missing = 8 - expandedHead.length - expandedTail.length;
|
|
134
|
-
// `::` must stand for at least one group, so exactly-8 without it is invalid.
|
|
135
|
-
if (missing < 1) return undefined;
|
|
136
|
-
full = [...expandedHead, ...Array.from({ length: missing }, () => "0"), ...expandedTail];
|
|
137
|
-
} else {
|
|
138
|
-
full = expandedHead;
|
|
139
|
-
}
|
|
140
|
-
if (full.length !== 8) return undefined;
|
|
141
|
-
const normalized: string[] = [];
|
|
142
|
-
for (const group of full) {
|
|
143
|
-
if (!/^[0-9a-f]{1,4}$/.test(group)) return undefined;
|
|
144
|
-
normalized.push(Number.parseInt(group, 16).toString(16));
|
|
145
|
-
}
|
|
146
|
-
return normalized;
|
|
147
|
-
}
|
|
14
|
+
export { isLoopbackAddress };
|
|
148
15
|
|
|
149
16
|
function matchesCidr(address: string, cidr: string): boolean {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
//
|
|
159
|
-
// Requiring a run of digits also refuses the exotic spellings `Number` accepts
|
|
160
|
-
// for zero (`-0`, `+0`, `0x0`, `0b0`, `0o0`, `0.0`), all of which are typos
|
|
161
|
-
// rather than a deliberate "trust everyone".
|
|
162
|
-
const prefixText = rawPrefix === undefined ? undefined : rawPrefix.trim();
|
|
163
|
-
if (prefixText !== undefined && !/^\d+$/.test(prefixText)) return false;
|
|
164
|
-
// A mapped peer and a plain-IPv4 allowlist entry describe the same host.
|
|
165
|
-
const subject = unwrapMappedIpv4(address);
|
|
166
|
-
const base = unwrapMappedIpv4(network);
|
|
167
|
-
const family = isIP(subject);
|
|
168
|
-
if (family === 4) {
|
|
169
|
-
const value = ipv4ToNumber(subject);
|
|
170
|
-
const networkValue = ipv4ToNumber(base);
|
|
171
|
-
if (value === undefined || networkValue === undefined) return false;
|
|
172
|
-
const prefix = prefixText === undefined ? 32 : Number(prefixText);
|
|
173
|
-
if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) return false;
|
|
174
|
-
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
|
|
175
|
-
return ((value >>> 0) & mask) === ((networkValue >>> 0) & mask);
|
|
176
|
-
}
|
|
177
|
-
if (family !== 6) return false;
|
|
178
|
-
const value = ipv6ToBigInt(subject);
|
|
179
|
-
const networkValue = ipv6ToBigInt(base);
|
|
180
|
-
if (value === undefined || networkValue === undefined) return false;
|
|
181
|
-
const prefix = prefixText === undefined ? 128 : Number(prefixText);
|
|
182
|
-
if (!Number.isInteger(prefix) || prefix < 0 || prefix > 128) return false;
|
|
183
|
-
const shift = BigInt(128 - prefix);
|
|
184
|
-
return (value >> shift) === (networkValue >> shift);
|
|
17
|
+
// `unwrapMapped: true`: a mapped peer and a plain-IPv4 allowlist entry
|
|
18
|
+
// describe the same host (Bun reports the IPv4 loopback peer as
|
|
19
|
+
// `::ffff:127.0.0.1` on Windows). `requirePrefix: false`: a bare address
|
|
20
|
+
// (`"127.0.0.1"`) means "this exact host".
|
|
21
|
+
return cidrContainsAddress(address, cidr, {
|
|
22
|
+
requirePrefix: false,
|
|
23
|
+
unwrapMapped: true,
|
|
24
|
+
});
|
|
185
25
|
}
|
|
186
26
|
|
|
187
27
|
function isTrustedPeer(peer: string, boundary: TrustedProxyBoundary): boolean {
|
|
@@ -206,20 +46,7 @@ const CLIENT_IDENTITY_HEADERS = [
|
|
|
206
46
|
|
|
207
47
|
/**
|
|
208
48
|
* True when `address` is a loopback address.
|
|
209
|
-
*
|
|
210
|
-
* Structural, not a string-prefix test: `127.0.0.1`, `::1`, and the mapped
|
|
211
|
-
* `::ffff:127.0.0.1` a Windows/Bun peer arrives as are all the same host, and
|
|
212
|
-
* a prefix test would miss the mapped form — the exact shape that hid this
|
|
213
|
-
* class of bug before.
|
|
214
49
|
*/
|
|
215
|
-
export function isLoopbackAddress(address: string): boolean {
|
|
216
|
-
const normalized = unwrapMappedIpv4(address.trim().split("%", 1)[0] ?? "");
|
|
217
|
-
if (normalized === "::1") return true;
|
|
218
|
-
if (isIP(normalized) !== 4) return false;
|
|
219
|
-
const value = ipv4ToNumber(normalized);
|
|
220
|
-
// 127.0.0.0/8 — the whole loopback block, not just 127.0.0.1.
|
|
221
|
-
return value !== undefined && ((value >>> 0) & 0xff000000) === (127 << 24);
|
|
222
|
-
}
|
|
223
50
|
|
|
224
51
|
/**
|
|
225
52
|
* Whether a peer may vouch for forwarded headers *for client-address
|
|
@@ -24,7 +24,7 @@ import type { ApiKeyModelAccessMode } from "../persistence/schema";
|
|
|
24
24
|
export type ModelRejectionReason = "model-denied" | "model-not-allowed";
|
|
25
25
|
|
|
26
26
|
/** The subset of an authorization snapshot this rule reads. */
|
|
27
|
-
|
|
27
|
+
interface ModelAccessPolicy {
|
|
28
28
|
readonly model_access_mode?: ApiKeyModelAccessMode | null | undefined;
|
|
29
29
|
readonly model_list?: readonly string[] | ReadonlySet<string> | null | undefined;
|
|
30
30
|
}
|
|
@@ -21,6 +21,24 @@ export const X_FRAME_OPTIONS = "DENY";
|
|
|
21
21
|
*/
|
|
22
22
|
export const BADGE_IMAGE_ORIGIN = "https://img.shields.io";
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Invariant: upstream response headers are NEVER forwarded wholesale to
|
|
26
|
+
* callers. There is no denylist-and-forward step on the egress path — the
|
|
27
|
+
* success projection (`proxySuccessHeaders` in
|
|
28
|
+
* `transport/dispatch/upstream.ts`) rebuilds the caller-facing header set from
|
|
29
|
+
* scratch (`cache-control: no-store`, `x-request-id`, and
|
|
30
|
+
* `GATEWAY_SECURITY_HEADERS` below), so credential-bearing upstream fields
|
|
31
|
+
* (`set-cookie`, `www-authenticate`, `proxy-authorization`) and hop-by-hop
|
|
32
|
+
* headers can never leak through by omission from a strip list.
|
|
33
|
+
*
|
|
34
|
+
* Any future feature that wants to pass upstream response fields to a caller
|
|
35
|
+
* must copy only the specific content-bearing headers it needs — do not
|
|
36
|
+
* reintroduce a "forward everything except" filter. A module implementing
|
|
37
|
+
* exactly that (`src/network/response-headers.ts` + `stripResponseHeaders`)
|
|
38
|
+
* existed once and was deleted as dead code (see git history for it), because
|
|
39
|
+
* every egress path had already moved to the rebuild-from-scratch projection.
|
|
40
|
+
*/
|
|
41
|
+
|
|
24
42
|
/**
|
|
25
43
|
* Security headers on every `/v1` response, whichever path produced it.
|
|
26
44
|
*
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InvokeGuard — a process-local foreground-activity gauge (G-03).
|
|
3
|
+
*
|
|
4
|
+
* Every foreground request path signals its activity here: the console control
|
|
5
|
+
* plane bumps the counter for the duration of each request, and the proxy data
|
|
6
|
+
* plane exposes its in-flight count through `setProxyBusySource`. The scheduled
|
|
7
|
+
* worker registry (and the sweep runner between waves) consults
|
|
8
|
+
* `foregroundBusy()` before starting background work, so a page open / an
|
|
9
|
+
* invoke in progress is never forced to share the single event loop with a
|
|
10
|
+
* heavy sweep.
|
|
11
|
+
*
|
|
12
|
+
* The defer policy is at the call sites (scheduler: bounded pause before a
|
|
13
|
+
* tick; sweep runner: bounded pause between waves). This module only owns the
|
|
14
|
+
* gauge — it never decides when to pause.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Console request counter. Bumped per in-flight console request. */
|
|
18
|
+
let consoleActivity = 0;
|
|
19
|
+
|
|
20
|
+
/** Optional live source of "the proxy plane is busy" (the in-flight store). */
|
|
21
|
+
let proxyBusySource: (() => boolean) | undefined;
|
|
22
|
+
|
|
23
|
+
/** Registers the proxy plane's busy source (wired once from the app root). */
|
|
24
|
+
export function setProxyBusySource(source: () => boolean): void {
|
|
25
|
+
proxyBusySource = source;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Marks a foreground console request; returns the matching end callback. */
|
|
29
|
+
export function beginConsoleActivity(): () => void {
|
|
30
|
+
consoleActivity += 1;
|
|
31
|
+
let ended = false;
|
|
32
|
+
return () => {
|
|
33
|
+
if (ended) return;
|
|
34
|
+
ended = true;
|
|
35
|
+
consoleActivity = Math.max(0, consoleActivity - 1);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Live foreground-request count (console counter + proxy source if wired). */
|
|
40
|
+
export function foregroundActivityCount(): number {
|
|
41
|
+
return consoleActivity + (proxyBusySource?.() === true ? 1 : 0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** True when any foreground work is in flight right now. */
|
|
45
|
+
export function foregroundBusy(): boolean {
|
|
46
|
+
return consoleActivity > 0 || (proxyBusySource?.() ?? false);
|
|
47
|
+
}
|
|
@@ -54,12 +54,12 @@ export interface CanonicalAdapter {
|
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
56
|
/** One native service: the path it is served at and the kind its models carry. */
|
|
57
|
-
|
|
57
|
+
interface NativeServiceRoute {
|
|
58
58
|
readonly serviceKind: Exclude<ServiceKind, "llm">;
|
|
59
59
|
readonly path: string;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
const NATIVE_SERVICES: readonly NativeServiceRoute[] = [
|
|
63
63
|
{ serviceKind: "systemone", path: "/v1/systemone" },
|
|
64
64
|
{ serviceKind: "websearch", path: "/v1/search" },
|
|
65
65
|
{ serviceKind: "imageGeneration", path: "/v1/images/generations" },
|
|
@@ -75,7 +75,7 @@ export const NATIVE_SERVICE_PATHS: readonly string[] = NATIVE_SERVICES.map(
|
|
|
75
75
|
);
|
|
76
76
|
|
|
77
77
|
/** Whether `path` is served by a native service route. */
|
|
78
|
-
|
|
78
|
+
function isNativeServicePath(path: string | undefined): boolean {
|
|
79
79
|
return path !== undefined && NATIVE_SERVICE_PATHS.includes(path);
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -97,6 +97,15 @@ export function isNativeServicePath(path: string | undefined): boolean {
|
|
|
97
97
|
*/
|
|
98
98
|
export const TOKEN_COUNT_PATH = "/v1/messages/count_tokens";
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The native Codex compaction route (`POST /v1/responses/compact`).
|
|
102
|
+
*
|
|
103
|
+
* Not a native service (it has no `serviceKind` of its own — Codex rows are
|
|
104
|
+
* `llm`-adjacent), so it is declared separately from `NATIVE_SERVICE_PATHS`,
|
|
105
|
+
* but its body is equally opaque and must skip the canonical chat stage.
|
|
106
|
+
*/
|
|
107
|
+
export const RESPONSES_COMPACT_PATH = "/v1/responses/compact";
|
|
108
|
+
|
|
100
109
|
/**
|
|
101
110
|
* Whether `path` reads a JSON body that is *not* a canonical chat request.
|
|
102
111
|
*
|
|
@@ -105,7 +114,7 @@ export const TOKEN_COUNT_PATH = "/v1/messages/count_tokens";
|
|
|
105
114
|
* will either reject a body the upstream accepts or plan it twice.
|
|
106
115
|
*/
|
|
107
116
|
export function isOpaqueBodyRoute(path: string | undefined): boolean {
|
|
108
|
-
return isNativeServicePath(path) || path === TOKEN_COUNT_PATH;
|
|
117
|
+
return isNativeServicePath(path) || path === TOKEN_COUNT_PATH || path === RESPONSES_COMPACT_PATH;
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
/** The route path serving `serviceKind`, when one is declared. */
|
|
@@ -394,7 +394,7 @@ let captureFailureReported = false;
|
|
|
394
394
|
* deployment condition that does not change per request, so repeating it for
|
|
395
395
|
* every request would bury the rest of the log without adding information.
|
|
396
396
|
*/
|
|
397
|
-
|
|
397
|
+
function reportCaptureFailure(error: unknown): void {
|
|
398
398
|
if (captureFailureReported) return;
|
|
399
399
|
captureFailureReported = true;
|
|
400
400
|
const directory = payloadDirectory();
|
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
// Tenant payload-capture policy resolved once and shared by dispatch attempts.
|
|
2
2
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
3
|
-
import {
|
|
3
|
+
import { resolvePayloadCapture } from "../../observability/payload-capture";
|
|
4
4
|
import type { TelemetryPayloadDepth, TelemetryPayloadMode } from "../../console/settings/contracts";
|
|
5
5
|
|
|
6
|
+
/** The resolved payload-capture preference, memoized on the request state. */
|
|
7
|
+
export type PayloadCapturePreference =
|
|
8
|
+
| {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
readonly mode: TelemetryPayloadMode;
|
|
11
|
+
readonly depth: TelemetryPayloadDepth;
|
|
12
|
+
readonly retentionMs: number;
|
|
13
|
+
}
|
|
14
|
+
| { readonly ok: false };
|
|
15
|
+
|
|
16
|
+
/** The slice of request state that memoizes the capture preference. */
|
|
17
|
+
export type CapturePreferenceState = { payloadCapturePreference?: PayloadCapturePreference };
|
|
18
|
+
|
|
19
|
+
/** Resolves the tenant's capture preference once, memoized on the request state. */
|
|
20
|
+
async function resolveCapturePreference(
|
|
21
|
+
db: CartethyiaDatabase,
|
|
22
|
+
tenantId: string | null,
|
|
23
|
+
state: CapturePreferenceState,
|
|
24
|
+
): Promise<PayloadCapturePreference> {
|
|
25
|
+
let preference = state.payloadCapturePreference;
|
|
26
|
+
if (preference === undefined) {
|
|
27
|
+
preference = await resolvePayloadCapture(db, tenantId);
|
|
28
|
+
state.payloadCapturePreference = preference;
|
|
29
|
+
}
|
|
30
|
+
return preference;
|
|
31
|
+
}
|
|
32
|
+
|
|
6
33
|
/** Per-stage body cap for the provider request/response tee. */
|
|
7
34
|
export function providerCaptureMaxBytes(depth?: TelemetryPayloadDepth): number {
|
|
8
35
|
if (depth === "moderate") return 2 * 1024 * 1024;
|
|
@@ -14,14 +41,9 @@ export function providerCaptureMaxBytes(depth?: TelemetryPayloadDepth): number {
|
|
|
14
41
|
export async function shouldCaptureProviderBodyForRequest(
|
|
15
42
|
db: CartethyiaDatabase,
|
|
16
43
|
tenantId: string | null,
|
|
17
|
-
state:
|
|
44
|
+
state: CapturePreferenceState,
|
|
18
45
|
): Promise<boolean> {
|
|
19
|
-
|
|
20
|
-
state.payloadCapturePreference = await resolvePayloadCapture(db, tenantId);
|
|
21
|
-
}
|
|
22
|
-
const resolved = state.payloadCapturePreference as
|
|
23
|
-
| { readonly ok: true; readonly mode: string }
|
|
24
|
-
| { readonly ok: false };
|
|
46
|
+
const resolved = await resolveCapturePreference(db, tenantId, state);
|
|
25
47
|
return resolved.ok === true && resolved.mode === "full";
|
|
26
48
|
}
|
|
27
49
|
|
|
@@ -29,14 +51,9 @@ export async function shouldCaptureProviderBodyForRequest(
|
|
|
29
51
|
export async function resolveCaptureModeForRequest(
|
|
30
52
|
db: CartethyiaDatabase,
|
|
31
53
|
tenantId: string | null,
|
|
32
|
-
state:
|
|
54
|
+
state: CapturePreferenceState,
|
|
33
55
|
): Promise<TelemetryPayloadMode> {
|
|
34
|
-
|
|
35
|
-
state.payloadCapturePreference = await resolvePayloadCapture(db, tenantId);
|
|
36
|
-
}
|
|
37
|
-
const resolved = state.payloadCapturePreference as
|
|
38
|
-
| { readonly ok: true; readonly mode: TelemetryPayloadMode }
|
|
39
|
-
| { readonly ok: false };
|
|
56
|
+
const resolved = await resolveCapturePreference(db, tenantId, state);
|
|
40
57
|
return resolved.ok === true ? resolved.mode : "none";
|
|
41
58
|
}
|
|
42
59
|
|
|
@@ -44,32 +61,12 @@ export async function resolveCaptureModeForRequest(
|
|
|
44
61
|
export async function resolveCaptureDepthForRequest(
|
|
45
62
|
db: CartethyiaDatabase,
|
|
46
63
|
tenantId: string | null,
|
|
47
|
-
state:
|
|
64
|
+
state: CapturePreferenceState,
|
|
48
65
|
): Promise<TelemetryPayloadDepth> {
|
|
49
|
-
|
|
50
|
-
state.payloadCapturePreference = await resolvePayloadCapture(db, tenantId);
|
|
51
|
-
}
|
|
52
|
-
const resolved = state.payloadCapturePreference as
|
|
53
|
-
| { readonly ok: true; readonly depth: TelemetryPayloadDepth }
|
|
54
|
-
| { readonly ok: false };
|
|
66
|
+
const resolved = await resolveCapturePreference(db, tenantId, state);
|
|
55
67
|
return resolved.ok === true ? resolved.depth : "minimum";
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
/** Resolve the tenant's payload retention once per request. */
|
|
59
|
-
export async function resolveCaptureRetentionForRequest(
|
|
60
|
-
db: CartethyiaDatabase,
|
|
61
|
-
tenantId: string | null,
|
|
62
|
-
state: { payloadCapturePreference?: unknown },
|
|
63
|
-
): Promise<number> {
|
|
64
|
-
if (state.payloadCapturePreference === undefined) {
|
|
65
|
-
state.payloadCapturePreference = await resolvePayloadCapture(db, tenantId);
|
|
66
|
-
}
|
|
67
|
-
const resolved = state.payloadCapturePreference as
|
|
68
|
-
| { readonly ok: true; readonly retentionMs: number }
|
|
69
|
-
| { readonly ok: false };
|
|
70
|
-
return resolved.ok === true ? resolved.retentionMs : payloadRetentionMs();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
70
|
/** Combined body cap for the four payload panels at one configured depth. */
|
|
74
71
|
export function captureDepthMaxBytes(depth: TelemetryPayloadDepth): number {
|
|
75
72
|
if (depth === "moderate") return 8 * 1024 * 1024;
|
|
@@ -52,13 +52,13 @@ export type EmptyStreamFetch = (
|
|
|
52
52
|
* unjittered, so a provider returning 200-with-no-events could drive an
|
|
53
53
|
* unbounded wait, and concurrent clients could resynchronize their retries.
|
|
54
54
|
*/
|
|
55
|
-
|
|
55
|
+
interface EmptyStreamPolicy {
|
|
56
56
|
readonly maxEmptyReplays?: number;
|
|
57
57
|
readonly firstEventTimeoutMs?: number;
|
|
58
58
|
readonly sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
interface EmptyStreamResult {
|
|
62
62
|
readonly response: Response;
|
|
63
63
|
readonly tally: StreamTally;
|
|
64
64
|
readonly endpoint: string;
|
|
@@ -112,8 +112,7 @@ export async function fetchWithEmptyStreamRecovery(
|
|
|
112
112
|
|
|
113
113
|
for (let endpointIndex = 0; endpointIndex < endpoints.length; endpointIndex++) {
|
|
114
114
|
const endpoint = endpoints[endpointIndex]!;
|
|
115
|
-
const
|
|
116
|
-
const replaysAllowed = isLastEndpoint ? maxReplays : maxReplays;
|
|
115
|
+
const replaysAllowed = maxReplays;
|
|
117
116
|
|
|
118
117
|
for (let replay = 0; replay <= replaysAllowed; replay++) {
|
|
119
118
|
// Check abort before each fetch
|
|
@@ -167,12 +166,7 @@ export async function fetchWithEmptyStreamRecovery(
|
|
|
167
166
|
// Empty — replay if we haven't exhausted this endpoint's budget
|
|
168
167
|
if (replay < replaysAllowed) {
|
|
169
168
|
const delay = fallbackRetryDelayMs(replay);
|
|
170
|
-
|
|
171
|
-
await sleep(delay, deps.signal);
|
|
172
|
-
} catch (error) {
|
|
173
|
-
if (error instanceof GatewayError) throw error;
|
|
174
|
-
throw error;
|
|
175
|
-
}
|
|
169
|
+
await sleep(delay, deps.signal);
|
|
176
170
|
}
|
|
177
171
|
}
|
|
178
172
|
}
|
|
@@ -196,9 +190,22 @@ async function fetchWithTimeout(
|
|
|
196
190
|
signal: AbortSignal,
|
|
197
191
|
timeoutMs: number,
|
|
198
192
|
): Promise<Response> {
|
|
199
|
-
// Race the fetch against a first-event timeout
|
|
193
|
+
// Race the fetch against a first-event timeout. The timer and the abort
|
|
194
|
+
// listener are both cleaned up once the race settles, so a successful fetch
|
|
195
|
+
// leaves neither behind.
|
|
196
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
197
|
+
const onAbort = (): void => {
|
|
198
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
199
|
+
// A client cancellation is the caller's signal; the fetch already races the
|
|
200
|
+
// same abort signal, so rejecting here only surfaces the 499 envelope.
|
|
201
|
+
timeoutPromiseReject(
|
|
202
|
+
new GatewayError("transport_closed", 499, "The request was aborted (request {requestId}). Send it again whenever ready 🙃"),
|
|
203
|
+
);
|
|
204
|
+
};
|
|
205
|
+
let timeoutPromiseReject!: (reason: unknown) => void;
|
|
200
206
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
201
|
-
|
|
207
|
+
timeoutPromiseReject = reject;
|
|
208
|
+
timer = setTimeout(() => {
|
|
202
209
|
// A first-event timeout is the gateway waiting on a silent upstream (e.g.
|
|
203
210
|
// a provider that holds the request in a queue), not the client leaving.
|
|
204
211
|
// It must surface as a deadline failure, never as a `transport_closed`
|
|
@@ -206,19 +213,15 @@ async function fetchWithTimeout(
|
|
|
206
213
|
// otherwise hide a real upstream wait behind "cancelled by client".
|
|
207
214
|
reject(new GatewayError("deadline_exceeded", 504, "First event timeout (request {requestId}). Some things just take longer 🐢", {}, "upstream"));
|
|
208
215
|
}, timeoutMs);
|
|
209
|
-
signal.addEventListener("abort", () => {
|
|
210
|
-
clearTimeout(timer);
|
|
211
|
-
reject(new GatewayError("transport_closed", 499, "The request was aborted (request {requestId}). Send it again whenever ready 🙃"));
|
|
212
|
-
}, { once: true });
|
|
213
216
|
});
|
|
214
|
-
|
|
217
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
215
218
|
try {
|
|
216
219
|
return await Promise.race([
|
|
217
220
|
fetch(url, { headers: request.headers, body: request.body, signal }),
|
|
218
221
|
timeoutPromise,
|
|
219
222
|
]);
|
|
220
|
-
}
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
+
} finally {
|
|
224
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
225
|
+
signal.removeEventListener("abort", onAbort);
|
|
223
226
|
}
|
|
224
227
|
}
|