cartethyia 2.4.6 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -13
- package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js +1 -0
- package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-Bcy150Jo.js → CliToolDetailPage-Bb9CNuoh.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bb9CNuoh.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DyNXefph.js → CliToolsPage-BXUaRCQQ.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BXUaRCQQ.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CisNZOUt.js +1 -0
- package/dist/dashboard/assets/CombosPage-CisNZOUt.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CATpAEOz.js → ConsoleLogPage-BOpI_LOH.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-BOpI_LOH.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-D6jEU7nL.js → CustomizationPage-DAkdvJ4k.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-DAkdvJ4k.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-CZkpn0a7.js → HealthEventsModal-DeT-VXHd.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-DeT-VXHd.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BJlMVN4U.js → LoginPage-CytxSeGP.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CytxSeGP.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-Br3CXEau.js → ModelPicker-oQ-SDXp8.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js +1 -0
- package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js +7 -0
- package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js +1 -0
- package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-BHW4Zt06.js → SettingsPage-8-eVadhc.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-8-eVadhc.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-VOYC_lIa.js → SetupPage-uEIXPyGO.js} +1 -1
- package/dist/dashboard/assets/SetupPage-uEIXPyGO.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DCY0TPTZ.js +1 -0
- package/dist/dashboard/assets/SortableList-DCY0TPTZ.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-BNIDLiU6.js → StudioPage-DqK5E26W.js} +4 -4
- package/dist/dashboard/assets/StudioPage-DqK5E26W.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-38JfiZ0H.js +56 -0
- package/dist/dashboard/assets/UsagePage-38JfiZ0H.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BETfaMYZ.js → api-keys-BmDbsior.js} +1 -1
- package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
- package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
- package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
- package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
- package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BTyGk_DL.js → badge-DpXT4Ixt.js} +1 -1
- package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
- package/dist/dashboard/assets/{banner-bujTWYM4.js → banner-CSAAWLJv.js} +1 -1
- package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
- package/dist/dashboard/assets/{card-B0Bnx5CA.js → card-DEhRXXw4.js} +1 -1
- package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-DE2ug812.js → cli-tools-B0wBq-RL.js} +1 -1
- package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-R_rArH8S.js → clipboard-button-DOo6EU8O.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-BWIP_EvW.js → dialog-BvQeBMfP.js} +1 -1
- package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BKcgVugF.js → drawer-CMwPXwQB.js} +1 -1
- package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
- package/dist/dashboard/assets/entry--yj84zk3.js +4 -0
- package/dist/dashboard/assets/entry--yj84zk3.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CcWkGR4a.js → entry-D4O2229X.js} +1 -1
- package/dist/dashboard/assets/{entry-CcWkGR4a.js.gz → entry-D4O2229X.js.gz} +0 -0
- package/dist/dashboard/assets/{entry-kTiTFrvh.js → entry-DKvr5Q9K.js} +1 -1
- package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-Booc5mNe.js → github-badge-zliLvBwr.js} +1 -1
- package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
- package/dist/dashboard/assets/{index-D70pChfn.js → index-CcD1eSJ6.js} +2 -2
- package/dist/dashboard/assets/index-CcD1eSJ6.js.gz +0 -0
- package/dist/dashboard/assets/{inline-D1TCqM2D.js → inline-D1IlZZ8H.js} +1 -1
- package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
- package/dist/dashboard/assets/{input-B9oyU93y.js → input-BpVZdrbc.js} +1 -1
- package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BArzXirQ.js → layout-BioLW7gN.js} +1 -1
- package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BVZzaoHF.js → list-row-CwVcGAIc.js} +1 -1
- package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
- package/dist/dashboard/assets/{live-EmuIayqp.js → live-yfEJFF9T.js} +1 -1
- package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CSPQ9xB0.js → model-bans-CT-TxSNX.js} +1 -1
- package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
- package/dist/dashboard/assets/providers/cursor.webp +0 -0
- package/dist/dashboard/assets/providers/trae.webp +0 -0
- package/dist/dashboard/assets/{routing-BExgxxBU.js → routing-Cs6jl49L.js} +1 -1
- package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
- package/dist/dashboard/assets/select-XPJ-dIw3.js +41 -0
- package/dist/dashboard/assets/select-XPJ-dIw3.js.gz +0 -0
- package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
- package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
- package/dist/dashboard/assets/{stack-Ca5HxI5i.js → stack-C9_uUjaA.js} +1 -1
- package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
- package/dist/dashboard/assets/{state-CzUPfLXo.js → state-DA0B95UQ.js} +1 -1
- package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DOUIky_i.js → switch-Cittxy-x.js} +1 -1
- package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
- package/dist/dashboard/assets/{toast-C76WrMhc.js → toast-CNt_jzO4.js} +1 -1
- package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-D87FZwH0.js → useMutation-BQKMcJ1H.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +51 -52
- package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
- package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
- package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
- package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
- package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
- package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
- package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
- package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
- package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
- package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
- package/package.json +3 -2
- package/scripts/cli/command/console-menu.ts +30 -11
- package/scripts/cli/command/start.ts +77 -13
- package/scripts/cli/command/tray.ts +47 -8
- package/scripts/cli/command/update.ts +1 -1
- package/scripts/cli/gateway/instance-lock.ts +105 -0
- package/scripts/cli/gateway/spawn.ts +173 -101
- package/scripts/cli/ui/background.ts +110 -49
- package/src/boot/app.ts +20 -6
- package/src/boot/dependencies.ts +19 -0
- package/src/boot/elysia-typebox-bridge.ts +37 -0
- package/src/boot/lifecycle.ts +2 -0
- package/src/config/cooldowns.ts +138 -138
- package/src/config/providers.ts +5 -5
- package/src/config/runtime.ts +19 -3
- package/src/config/spec.ts +9 -0
- package/src/console/api-keys/contracts.ts +414 -391
- package/src/console/api-keys/routes.ts +880 -870
- package/src/console/auth/account-reset-store.ts +14 -0
- package/src/console/auth/account-reset.ts +3 -2
- package/src/console/auth/setup-service.ts +6 -3
- package/src/console/domain-registration.ts +9 -2
- package/src/console/observability/contracts.ts +475 -455
- package/src/console/observability/store.ts +1015 -952
- package/src/console/providers/catalog/contracts.ts +31 -0
- package/src/console/providers/catalog/provider-operations.ts +8 -0
- package/src/console/providers/catalog/store.ts +1532 -1525
- package/src/console/providers/oauth/callback-listener.ts +18 -3
- package/src/console/providers/oauth/routes.ts +38 -15
- package/src/console/quota/refresh.ts +19 -0
- package/src/console/updates/routes.ts +162 -4
- package/src/main.ts +3 -0
- package/src/network/http2-bidi.ts +85 -0
- package/src/network/outbound-fetch.ts +1 -26
- package/src/network/pool/agent.ts +478 -460
- package/src/network/pool/resolver.ts +1104 -544
- package/src/network/pool-health-machine.ts +12 -5
- package/src/observability/payload-capture.ts +344 -324
- package/src/persistence/db-pg.ts +9 -3
- package/src/persistence/db-sqlite.ts +497 -495
- package/src/persistence/health-event-flush.ts +137 -0
- package/src/persistence/migrate.ts +181 -129
- package/src/persistence/schema.ts +1360 -1295
- package/src/persistence/settings-revision.ts +25 -0
- package/src/persistence/shared-cache.ts +217 -0
- package/src/persistence/stores/share-store.ts +556 -528
- package/src/persistence/usage-flush.ts +61 -1
- package/src/protocol/canonical-model.ts +23 -1
- package/src/protocol/response/chat.ts +143 -10
- package/src/protocol/response/omp-tool-call-markup.ts +221 -0
- package/src/providers/default-registry.ts +173 -1
- package/src/providers/discovery/openai-model-discovery.ts +39 -3
- package/src/providers/discovery/openrouter-free-roster.ts +98 -0
- package/src/providers/discovery/probing-service.ts +1022 -1001
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +39 -0
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +18 -5
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
- package/src/providers/integrations/buddy/codebuddy/shared.ts +15 -6
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
- package/src/providers/integrations/buddy/workbuddy/shared.ts +3 -3
- package/src/providers/integrations/claude/claude.ts +606 -582
- package/src/providers/integrations/cline/cline-quota.ts +18 -16
- package/src/providers/integrations/cline/cline.ts +539 -468
- package/src/providers/integrations/commandcode/commandcode.ts +675 -541
- package/src/providers/integrations/commandcode/discovery.ts +116 -0
- package/src/providers/integrations/commandcode/quota.ts +160 -0
- package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
- package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
- package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
- package/src/providers/integrations/cursor/cursor-request.ts +471 -0
- package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
- package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
- package/src/providers/integrations/cursor/cursor.ts +960 -0
- package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
- package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
- package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
- package/src/providers/integrations/github/github.ts +4 -4
- package/src/providers/integrations/hermes.ts +79 -8
- package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
- package/src/providers/integrations/kilo/kilo.ts +153 -146
- package/src/providers/integrations/opencode/provider.ts +532 -538
- package/src/providers/integrations/openrouter/provider.ts +139 -281
- package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +310 -257
- package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
- package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
- package/src/providers/integrations/qoder/qoder.ts +865 -730
- package/src/providers/integrations/trae/trae-catalog.ts +190 -0
- package/src/providers/integrations/trae/trae-headers.ts +149 -0
- package/src/providers/integrations/trae/trae-oauth.ts +773 -0
- package/src/providers/integrations/trae/trae-protocol.ts +400 -0
- package/src/providers/integrations/trae/trae-realm.ts +220 -0
- package/src/providers/integrations/trae/trae.ts +458 -0
- package/src/providers/operations/account-health-service.ts +107 -10
- package/src/providers/operations/client-versions.ts +133 -6
- package/src/providers/operations/provider-catalog-seeder.ts +95 -86
- package/src/providers/operations/provider-credential-service.ts +46 -7
- package/src/providers/provider-contracts.ts +76 -0
- package/src/providers/provider-metadata.ts +78 -6
- package/src/providers/provider-model-definition.ts +373 -349
- package/src/security/api-key-auth.ts +126 -8
- package/src/transport/contracts.ts +3 -0
- package/src/transport/dispatch/attempt-finalize.ts +32 -1
- package/src/transport/dispatch/attempt-loop.ts +530 -515
- package/src/transport/dispatch/generation.ts +88 -21
- package/src/transport/dispatch/leases.ts +99 -2
- package/src/transport/dispatch/proxy-request.ts +60 -3
- package/src/transport/dispatch/streaming/finalize.ts +16 -1
- package/src/transport/dispatch/streaming/stream.ts +22 -2
- package/src/transport/dispatch/upstream.ts +19 -1
- package/src/transport/guards/auth-guard.ts +26 -1
- package/src/transport/guards/body-policy.ts +5 -4
- package/src/transport/request/preparer.ts +103 -0
- package/src/transport/request/state.ts +14 -0
- package/src/transport/routing/plan-gate.ts +45 -0
- package/src/transport/routing/route-catalog.ts +799 -777
- package/src/transport/routing/route-model.ts +52 -1
- package/src/transport/routing/router.ts +51 -0
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js +0 -1
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +0 -1
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js +0 -7
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +0 -1
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js +0 -1
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js +0 -56
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css +0 -2
- package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
- package/dist/dashboard/assets/assert-7J6_LVD3.js +0 -1
- package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
- package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
- package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
- package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js +0 -4
- package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
- package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
- package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
- package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
- package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
- package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
- package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
- package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
- package/dist/dashboard/assets/select-C9mMoEpJ.js +0 -41
- package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js +0 -1
- package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
- package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
- package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
- package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
- package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
- package/src/network/tls/bun-client-hello.ts +0 -112
- /package/dist/dashboard/assets/{app-Cjq7nrwu.js → app-C-Y4oZT0.js} +0 -0
- /package/dist/dashboard/assets/{app-Cjq7nrwu.js.gz → app-C-Y4oZT0.js.gz} +0 -0
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* already spent.
|
|
26
26
|
*/
|
|
27
27
|
import { log } from "../../../observability/logger";
|
|
28
|
+
import { ConsoleDomainError } from "../../shared/errors";
|
|
28
29
|
|
|
29
30
|
/** A loopback endpoint a callback can actually be delivered to. */
|
|
30
31
|
export interface LoopbackCallbackEndpoint {
|
|
@@ -224,10 +225,15 @@ export class OAuthCallbackListener {
|
|
|
224
225
|
try {
|
|
225
226
|
servers.push(this.#serve({ hostname: "127.0.0.1", port: endpoint.port, fetch: handler }));
|
|
226
227
|
} catch (cause) {
|
|
227
|
-
|
|
228
|
+
// A domain error, not a bare Error: the console route maps an untyped
|
|
229
|
+
// failure to a generic `internal_error`, which hid an actionable "free
|
|
230
|
+
// the port" message behind "Failed to start OAuth".
|
|
231
|
+
throw new ConsoleDomainError(
|
|
232
|
+
"oauth_callback_port_unavailable",
|
|
233
|
+
409,
|
|
228
234
|
`OAuth callback port ${endpoint.port} is in use, so the login redirect to ${endpoint.path} cannot be delivered. ` +
|
|
229
235
|
`Free port ${endpoint.port} and retry.`,
|
|
230
|
-
{
|
|
236
|
+
{ port: endpoint.port, path: endpoint.path },
|
|
231
237
|
);
|
|
232
238
|
}
|
|
233
239
|
try {
|
|
@@ -253,7 +259,16 @@ export class OAuthCallbackListener {
|
|
|
253
259
|
}
|
|
254
260
|
const error = url.searchParams.get("error");
|
|
255
261
|
const state = url.searchParams.get("state") ?? "";
|
|
256
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Not every authorization server hands back a `code`. Trae's page redirects
|
|
264
|
+
* with the refresh token already in the query (`?isRedirect=true&
|
|
265
|
+
* refreshToken=…`), and the client's parser reads the whole query. So an
|
|
266
|
+
* absent `code` falls back to the query itself — the client decides whether
|
|
267
|
+
* it can redeem it, and one that cannot still fails with its own message
|
|
268
|
+
* rather than a bare 400 here.
|
|
269
|
+
*/
|
|
270
|
+
const codeParam = url.searchParams.get("code") ?? "";
|
|
271
|
+
const code = codeParam.length > 0 ? codeParam : url.search.slice(1);
|
|
257
272
|
if (error !== null) {
|
|
258
273
|
// A denial carries the state only when the server echoes one; clear it
|
|
259
274
|
// when present so a retry starts a fresh flow.
|
|
@@ -28,14 +28,22 @@ import type { ConsoleAccessResolver } from "../../auth/access";
|
|
|
28
28
|
* identity the provider itself reports (an email or org name), which is stable
|
|
29
29
|
* across logins, so it is what a re-login replaces.
|
|
30
30
|
*
|
|
31
|
-
* A provider
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
31
|
+
* A label the provider never resolved is *not* that identity. Callers fill the
|
|
32
|
+
* gap with the provider id (`cursor`, `github`), and hashing that made every
|
|
33
|
+
* login of every account of that provider collide: the second login matched the
|
|
34
|
+
* first row and replaced it, so adding an account silently consumed the one
|
|
35
|
+
* already there. Only a label the provider actually reported — or the refresh
|
|
36
|
+
* token, which still catches an exact replay — may be the identity.
|
|
37
|
+
*
|
|
38
|
+
* Test seam: exported so the merge rule can be pinned without a database.
|
|
36
39
|
*/
|
|
37
|
-
function oauthIdentityFingerprint(input: {
|
|
38
|
-
|
|
40
|
+
export function oauthIdentityFingerprint(input: {
|
|
41
|
+
readonly label: string;
|
|
42
|
+
readonly providerId: string;
|
|
43
|
+
} & OAuthExchangeResult): string {
|
|
44
|
+
const label = input.label.trim();
|
|
45
|
+
const resolved = label.length > 0 && label.toLowerCase() !== input.providerId.trim().toLowerCase();
|
|
46
|
+
return hashSecret(resolved ? label : input.refresh);
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
export class DrizzleOAuthAccountStore implements OAuthAccountStore {
|
|
@@ -46,7 +54,7 @@ export class DrizzleOAuthAccountStore implements OAuthAccountStore {
|
|
|
46
54
|
providerId: string,
|
|
47
55
|
input: { readonly label: string } & OAuthExchangeResult,
|
|
48
56
|
): Promise<PersistedOAuthAccount> {
|
|
49
|
-
const credentialFingerprint = oauthIdentityFingerprint(input);
|
|
57
|
+
const credentialFingerprint = oauthIdentityFingerprint({ ...input, providerId });
|
|
50
58
|
const sameAccount = and(
|
|
51
59
|
eq(providerAccounts.providerId, providerId),
|
|
52
60
|
// The identity index coalesces a null tenant to a sentinel, so "same
|
|
@@ -375,12 +383,32 @@ export function createOAuthLoginOperations(config: OAuthLoginConfig) {
|
|
|
375
383
|
}
|
|
376
384
|
const state = generateOAuthState();
|
|
377
385
|
const { codeVerifier, codeChallenge } = generatePkcePair();
|
|
386
|
+
/**
|
|
387
|
+
* A client whose authorize URL depends on an upstream discovery runs it
|
|
388
|
+
* here, before the URL is built: `buildAuthorizeUrl` is synchronous by
|
|
389
|
+
* contract, so a client cannot fetch inside it.
|
|
390
|
+
*
|
|
391
|
+
* Trae is the reason this is wired up: its login host comes from
|
|
392
|
+
* `GetLoginGuidance`, and without this step the URL was built with no
|
|
393
|
+
* host and the login failed at the first click. Kiro declares the hook
|
|
394
|
+
* as a no-op, so nothing that already worked changes.
|
|
395
|
+
*/
|
|
396
|
+
const authorizeRequest = {
|
|
397
|
+
state,
|
|
398
|
+
codeChallenge,
|
|
399
|
+
redirectUri: client.browserRedirectUri ?? browserAuthorizeRedirectUri(),
|
|
400
|
+
...(parameters === undefined ? {} : { parameters }),
|
|
401
|
+
};
|
|
402
|
+
const prepared =
|
|
403
|
+
typeof client.prepareAuthorize === "function"
|
|
404
|
+
? await client.prepareAuthorize(authorizeRequest)
|
|
405
|
+
: authorizeRequest;
|
|
378
406
|
// A client that names its own allowlisted redirect URI wins over the
|
|
379
407
|
// gateway default: OpenAI registers exactly `http://localhost:1455/auth/callback`
|
|
380
408
|
// for the Codex client and answers any other value with
|
|
381
409
|
// `invalid_request` before consent, so the shared default cannot serve
|
|
382
410
|
// every provider.
|
|
383
|
-
const redirectUri =
|
|
411
|
+
const redirectUri = authorizeRequest.redirectUri;
|
|
384
412
|
// Bind the redirect's loopback port before the flow is recorded. The
|
|
385
413
|
// advertised URI names the operator's own machine, so a login that only
|
|
386
414
|
// advertises it leaves the browser on a dead page and the code stranded
|
|
@@ -404,12 +432,7 @@ export function createOAuthLoginOperations(config: OAuthLoginConfig) {
|
|
|
404
432
|
// deliver itself; a non-loopback redirect (a custom scheme, a remote
|
|
405
433
|
// host) returns false and keeps the manual path.
|
|
406
434
|
return {
|
|
407
|
-
authorizeUrl: client.buildAuthorizeUrl(
|
|
408
|
-
state,
|
|
409
|
-
codeChallenge,
|
|
410
|
-
redirectUri,
|
|
411
|
-
...(parameters === undefined ? {} : { parameters }),
|
|
412
|
-
}),
|
|
435
|
+
authorizeUrl: client.buildAuthorizeUrl(prepared),
|
|
413
436
|
state,
|
|
414
437
|
};
|
|
415
438
|
},
|
|
@@ -377,6 +377,25 @@ async function runQuotaRefresh(
|
|
|
377
377
|
// unsupported") is itself the answer the page must render, and caching it
|
|
378
378
|
// stops every page open from re-hitting a provider that just refused us.
|
|
379
379
|
await setCachedQuota(targetLens(target), target.accountId, quota, deps.redis);
|
|
380
|
+
// Persist the plan the provider reported, because routing gates on it: some
|
|
381
|
+
// upstreams refuse a model on a free plan and nothing but the account's own
|
|
382
|
+
// plan says which. The cache above is a short in-process TTL and empty after
|
|
383
|
+
// a restart, so it cannot be the only copy — the row is what survives one.
|
|
384
|
+
//
|
|
385
|
+
// A failed fetch leaves the last known plan in place rather than clearing
|
|
386
|
+
// it. The refresh failing is evidence about the network or the credential,
|
|
387
|
+
// not evidence that the account's plan changed, and clearing on failure
|
|
388
|
+
// would drop the router back to "unknown" on every transient error.
|
|
389
|
+
if (quota.plan !== null) {
|
|
390
|
+
try {
|
|
391
|
+
await deps.db
|
|
392
|
+
.update(providerAccounts)
|
|
393
|
+
.set({ quotaPlan: quota.plan })
|
|
394
|
+
.where(eq(providerAccounts.id, target.accountId));
|
|
395
|
+
} catch (error) {
|
|
396
|
+
log.warn(`[quota] failed to record plan for account=${target.accountId}`, error as Error);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
380
399
|
// A quota surface that names the account adopts the identity — but only over
|
|
381
400
|
// a default label. An operator's custom rename is never overwritten, and the
|
|
382
401
|
// CASE keeps the read and the write in one statement so a concurrent rename
|
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
import { Elysia } from "elysia";
|
|
2
|
-
import { consoleErrorHandler } from "../shared/errors";
|
|
3
|
-
import {
|
|
2
|
+
import { consoleErrorHandler, requireTenantScope } from "../shared/errors";
|
|
3
|
+
import { consoleSseResponse, createConsoleSseStream } from "../observability/sse";
|
|
4
|
+
import type { ObservabilityStore } from "../observability/contracts";
|
|
5
|
+
import type { ConsoleAccessResolver } from "../auth/access";
|
|
6
|
+
import { fetchLatestRelease, type LatestRelease } from "./repository-updates";
|
|
7
|
+
import { isNewerRelease } from "./version-compare";
|
|
8
|
+
|
|
9
|
+
const MEMORY_SAMPLE_INTERVAL_MS = 10_000;
|
|
10
|
+
const UPDATE_SAMPLE_INTERVAL_MS = 60_000;
|
|
11
|
+
const HIGH_MEMORY_PERCENT = 85;
|
|
12
|
+
const HIGH_MEMORY_CLEAR_PERCENT = 80;
|
|
13
|
+
const MEMORY_GROWTH_WINDOW_MS = 60_000;
|
|
14
|
+
const MEMORY_GROWTH_RATIO = 1.2;
|
|
15
|
+
const MAX_MEMORY_SAMPLES = 8;
|
|
16
|
+
const RELEASES_URL = "https://github.com/risunCodex/Cartethyia/releases";
|
|
17
|
+
|
|
18
|
+
export interface NotificationEvent {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly kind: "memory-high" | "memory-growth" | "update-available";
|
|
21
|
+
readonly title: string;
|
|
22
|
+
readonly message: string;
|
|
23
|
+
readonly createdAt: string;
|
|
24
|
+
readonly level: "warning" | "info";
|
|
25
|
+
readonly href?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function memoryNotifications(
|
|
29
|
+
samples: readonly { readonly at: number; readonly rss: number; readonly percent: number }[],
|
|
30
|
+
previous: { high: boolean; growth: boolean },
|
|
31
|
+
): { notifications: NotificationEvent[]; high: boolean; growth: boolean } {
|
|
32
|
+
const latest = samples.at(-1);
|
|
33
|
+
if (latest === undefined) return { notifications: [], ...previous };
|
|
34
|
+
const notifications: NotificationEvent[] = [];
|
|
35
|
+
const high = previous.high
|
|
36
|
+
? latest.percent >= HIGH_MEMORY_CLEAR_PERCENT
|
|
37
|
+
: latest.percent >= HIGH_MEMORY_PERCENT;
|
|
38
|
+
if (high && !previous.high) {
|
|
39
|
+
notifications.push({
|
|
40
|
+
id: "memory-high",
|
|
41
|
+
kind: "memory-high",
|
|
42
|
+
title: "High gateway memory",
|
|
43
|
+
message: `Gateway RSS is using ${latest.percent.toFixed(1)}% of system memory.`,
|
|
44
|
+
createdAt: new Date(latest.at).toISOString(),
|
|
45
|
+
level: "warning",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const oldest = samples.find((sample) => latest.at - sample.at >= MEMORY_GROWTH_WINDOW_MS);
|
|
50
|
+
const growth = oldest !== undefined &&
|
|
51
|
+
latest.rss >= oldest.rss * MEMORY_GROWTH_RATIO &&
|
|
52
|
+
samples.filter((sample) => sample.at >= oldest.at).every((sample, index, window) =>
|
|
53
|
+
index === 0 || sample.rss >= (window[index - 1]?.rss ?? sample.rss),
|
|
54
|
+
);
|
|
55
|
+
if (growth && !previous.growth && !high) {
|
|
56
|
+
notifications.push({
|
|
57
|
+
id: "memory-growth",
|
|
58
|
+
kind: "memory-growth",
|
|
59
|
+
title: "Sustained memory growth",
|
|
60
|
+
message: `Gateway RSS grew ${Math.round((latest.rss / oldest.rss - 1) * 100)}% over the last minute.`,
|
|
61
|
+
createdAt: new Date(latest.at).toISOString(),
|
|
62
|
+
level: "warning",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
notifications,
|
|
67
|
+
high: high && latest.percent >= HIGH_MEMORY_CLEAR_PERCENT,
|
|
68
|
+
growth,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
4
71
|
|
|
5
72
|
/**
|
|
6
73
|
* Read-only update discovery for the console sidebar.
|
|
@@ -10,8 +77,99 @@ import { fetchLatestRelease } from "./repository-updates";
|
|
|
10
77
|
* answers `null` rather than an error: an absent update badge is the honest
|
|
11
78
|
* result when the upstream cannot be reached.
|
|
12
79
|
*/
|
|
13
|
-
export function createUpdateRoutes(
|
|
80
|
+
export function createUpdateRoutes(config: {
|
|
81
|
+
readonly accessResolver: ConsoleAccessResolver;
|
|
82
|
+
readonly observabilityStore: ObservabilityStore;
|
|
83
|
+
readonly fetchRelease?: () => Promise<LatestRelease | null>;
|
|
84
|
+
}): Elysia {
|
|
85
|
+
const fetchRelease = config.fetchRelease ?? fetchLatestRelease;
|
|
14
86
|
return new Elysia()
|
|
15
87
|
.error(consoleErrorHandler("Update check failed"))
|
|
16
|
-
.get("/updates/release", async () => await
|
|
88
|
+
.get("/updates/release", async () => await fetchRelease())
|
|
89
|
+
.get("/notifications/stream", ({ request }) => {
|
|
90
|
+
const tenantId = requireTenantScope(
|
|
91
|
+
config.accessResolver(request),
|
|
92
|
+
"dashboard:read",
|
|
93
|
+
).tenantId;
|
|
94
|
+
return consoleSseResponse(
|
|
95
|
+
createConsoleSseStream(request.signal, (sender) => {
|
|
96
|
+
let closed = false;
|
|
97
|
+
let memoryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
98
|
+
let updateTimer: ReturnType<typeof setTimeout> | undefined;
|
|
99
|
+
let latestTag: string | undefined;
|
|
100
|
+
let memoryState = { high: false, growth: false };
|
|
101
|
+
let samples: Array<{ at: number; rss: number; percent: number }> = [];
|
|
102
|
+
|
|
103
|
+
const publishMemory = async (): Promise<void> => {
|
|
104
|
+
if (closed) return;
|
|
105
|
+
try {
|
|
106
|
+
const at = Date.now();
|
|
107
|
+
// Fresh, not `health`: that read is cached for 10 s and this
|
|
108
|
+
// samples on that exact cadence, so the cached bytes would make
|
|
109
|
+
// consecutive samples identical and a growth trend unreadable.
|
|
110
|
+
const live = await config.observabilityStore.liveMemory();
|
|
111
|
+
samples = [
|
|
112
|
+
...samples,
|
|
113
|
+
{ at, rss: live.memory_bytes, percent: live.memory_percent },
|
|
114
|
+
].filter((sample) => at - sample.at <= MEMORY_GROWTH_WINDOW_MS * 2)
|
|
115
|
+
.slice(-MAX_MEMORY_SAMPLES);
|
|
116
|
+
const computed = memoryNotifications(samples, memoryState);
|
|
117
|
+
memoryState = { high: computed.high, growth: computed.growth };
|
|
118
|
+
for (const notification of computed.notifications) sender.send("notification", notification);
|
|
119
|
+
} catch {
|
|
120
|
+
// A transient check must not terminate the feed or hide later events.
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const publishUpdate = async (): Promise<void> => {
|
|
125
|
+
if (closed) return;
|
|
126
|
+
try {
|
|
127
|
+
const release = await fetchRelease();
|
|
128
|
+
if (release === null || release.tag_name === latestTag) return;
|
|
129
|
+
const health = (await config.observabilityStore.health(tenantId)).value;
|
|
130
|
+
if (!isNewerRelease(health.version, release.tag_name)) return;
|
|
131
|
+
latestTag = release.tag_name;
|
|
132
|
+
sender.send("notification", {
|
|
133
|
+
id: `update-${release.tag_name}`,
|
|
134
|
+
kind: "update-available",
|
|
135
|
+
title: "Cartethyia update available",
|
|
136
|
+
message: `Version ${release.tag_name} is available; this gateway runs ${health.version}.`,
|
|
137
|
+
createdAt: new Date().toISOString(),
|
|
138
|
+
level: "info",
|
|
139
|
+
href: RELEASES_URL,
|
|
140
|
+
} satisfies NotificationEvent);
|
|
141
|
+
} catch {
|
|
142
|
+
// Release discovery is best-effort and cannot interrupt memory sampling.
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const scheduleMemory = (): void => {
|
|
147
|
+
memoryTimer = setTimeout(() => {
|
|
148
|
+
void publishMemory().finally(() => {
|
|
149
|
+
if (!closed) scheduleMemory();
|
|
150
|
+
});
|
|
151
|
+
}, MEMORY_SAMPLE_INTERVAL_MS);
|
|
152
|
+
};
|
|
153
|
+
const scheduleUpdate = (): void => {
|
|
154
|
+
updateTimer = setTimeout(() => {
|
|
155
|
+
void publishUpdate().finally(() => {
|
|
156
|
+
if (!closed) scheduleUpdate();
|
|
157
|
+
});
|
|
158
|
+
}, UPDATE_SAMPLE_INTERVAL_MS);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
void publishMemory().finally(() => {
|
|
162
|
+
if (!closed) scheduleMemory();
|
|
163
|
+
});
|
|
164
|
+
void publishUpdate().finally(() => {
|
|
165
|
+
if (!closed) scheduleUpdate();
|
|
166
|
+
});
|
|
167
|
+
return () => {
|
|
168
|
+
closed = true;
|
|
169
|
+
clearTimeout(memoryTimer);
|
|
170
|
+
clearTimeout(updateTimer);
|
|
171
|
+
};
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
}) as unknown as Elysia;
|
|
17
175
|
}
|
package/src/main.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Must run before any module that registers a route: it wires Elysia's TypeBox
|
|
2
|
+
// bridge statically, which the source-run path needs under NODE_ENV=production.
|
|
3
|
+
import "./boot/elysia-typebox-bridge";
|
|
1
4
|
import { createGatewayApp, createGatewayShell } from "./boot/app";
|
|
2
5
|
import { bootstrap } from "./boot/lifecycle";
|
|
3
6
|
import type { CartethyiaBoot } from "./boot/lifecycle";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Transport-neutral bidirectional HTTP/2 egress contract.
|
|
2
|
+
//
|
|
3
|
+
// Cursor's Run is a Connect-RPC bidi stream: one long-lived writable request
|
|
4
|
+
// whose response body is an unbounded stream of protobuf frames. Neither
|
|
5
|
+
// `fetch` nor the WebSocket session contract can express it — `fetch` needs the
|
|
6
|
+
// whole body up front, and a WebSocket is a different protocol — so the egress
|
|
7
|
+
// boundary gets its own contract.
|
|
8
|
+
//
|
|
9
|
+
// This module is transport-neutral on purpose: it declares *what* a validated
|
|
10
|
+
// bidi stream is, and `network/pool/resolver.ts` decides *how* one is dialed
|
|
11
|
+
// (direct TLS to a pinned, SSRF-validated address, or through the selected
|
|
12
|
+
// tenant pool's CONNECT/SOCKS tunnel). A provider adapter consumes the contract
|
|
13
|
+
// and never opens a socket of its own.
|
|
14
|
+
|
|
15
|
+
/** Response metadata of one opened bidi stream. */
|
|
16
|
+
export interface Http2BidiResponse {
|
|
17
|
+
/** HTTP/2 `:status`; `0` before the response headers arrive. */
|
|
18
|
+
readonly status: number;
|
|
19
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Terminal reason a bidi stream ended. */
|
|
23
|
+
export type Http2BidiEndReason =
|
|
24
|
+
/** The server closed the stream cleanly. */
|
|
25
|
+
| { readonly kind: "remote-clean-close" }
|
|
26
|
+
/** The server failed it: an HTTP status, a gRPC trailer, or a transport error. */
|
|
27
|
+
| { readonly kind: "remote-error"; readonly error: Error }
|
|
28
|
+
/** The client closed or aborted it. */
|
|
29
|
+
| { readonly kind: "local-close" };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One validated bidirectional HTTP/2 stream.
|
|
33
|
+
*
|
|
34
|
+
* Bytes in are an async iterable (so a provider pumps frames naturally and
|
|
35
|
+
* backpressure comes from the consumer); bytes out are `write`, which resolves
|
|
36
|
+
* once the chunk has drained — a bidi Run can carry megabytes, and an
|
|
37
|
+
* unawaited write is how a stream overruns its session memory and dies with
|
|
38
|
+
* `ENHANCE_YOUR_CALM`.
|
|
39
|
+
*/
|
|
40
|
+
export interface ValidatedHttp2BidiStream {
|
|
41
|
+
readonly response: Http2BidiResponse;
|
|
42
|
+
/** Response trailers, populated when the server sends them (gRPC status). */
|
|
43
|
+
readonly trailers: Readonly<Record<string, string>>;
|
|
44
|
+
/** Response body bytes, in arrival order. */
|
|
45
|
+
readonly body: AsyncIterable<Uint8Array>;
|
|
46
|
+
/** Writes one request chunk, resolving when it has drained. */
|
|
47
|
+
write(chunk: Uint8Array): Promise<void>;
|
|
48
|
+
/** Half-closes the request side; the response may still be streaming. */
|
|
49
|
+
end(): void;
|
|
50
|
+
/** Tears the stream down immediately. Idempotent. */
|
|
51
|
+
abort(reason?: unknown): void;
|
|
52
|
+
/** Registers a terminal listener; the returned function unregisters it. */
|
|
53
|
+
onTerminal(listener: (reason: Http2BidiEndReason) => void): () => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Options for opening one validated bidi stream. */
|
|
57
|
+
export interface Http2BidiOpenOptions {
|
|
58
|
+
readonly signal?: AbortSignal;
|
|
59
|
+
/** Bytes read idle before the stream is declared dead. */
|
|
60
|
+
readonly readIdleMs?: number;
|
|
61
|
+
/** Upper bound on buffered inbound bytes before the stream is refused. */
|
|
62
|
+
readonly maxBufferedBytes?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Host allowlist applied on every new connection, in addition to the SSRF
|
|
65
|
+
* policy. A long-lived stream carries the account credential for its whole
|
|
66
|
+
* life, so "is this host one the caller may reach at all" is a per-provider
|
|
67
|
+
* decision the binding enforces rather than trusts the adapter to apply.
|
|
68
|
+
*/
|
|
69
|
+
readonly allowHostname?: (hostname: string) => boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The dialer contract a provider adapter is handed.
|
|
74
|
+
*
|
|
75
|
+
* Declared here rather than in the provider layer so the dependency runs one
|
|
76
|
+
* way: `provider-contracts` imports this module, never the reverse. It used to
|
|
77
|
+
* be declared there and re-exported from here, which made the two import each
|
|
78
|
+
* other — type-only, so it never deadlocked, but it meant this
|
|
79
|
+
* transport-neutral contract had to know about providers.
|
|
80
|
+
*/
|
|
81
|
+
export type ValidatedOutboundHttp2Bidi = (
|
|
82
|
+
url: URL,
|
|
83
|
+
headers: Readonly<Record<string, string>>,
|
|
84
|
+
options?: Http2BidiOpenOptions,
|
|
85
|
+
) => Promise<ValidatedHttp2BidiStream>;
|
|
@@ -9,7 +9,6 @@ import type { SsrfPolicy } from "../config/network";
|
|
|
9
9
|
import { isProxyAgentPair, type ProxyAgentPair } from "./pool/agent";
|
|
10
10
|
import { metrics } from "../observability/metrics";
|
|
11
11
|
import { trackNetworkCall } from "../observability/performance-metrics";
|
|
12
|
-
import { bunClientHelloOptions } from "./tls/bun-client-hello";
|
|
13
12
|
import { stripResponseHeaders } from "./response-headers";
|
|
14
13
|
// Validated fetch — single egress with ProxyAgent support
|
|
15
14
|
export interface ValidatedFetchOptions {
|
|
@@ -18,14 +17,6 @@ export interface ValidatedFetchOptions {
|
|
|
18
17
|
readonly maxRedirects?: number;
|
|
19
18
|
readonly resolveFn?: (hostname: string, signal: AbortSignal) => Promise<readonly string[]>;
|
|
20
19
|
readonly agent?: HttpAgent | HttpsAgent | ProxyAgentPair;
|
|
21
|
-
/**
|
|
22
|
-
* Handshake with Bun's cipher list, curve list and ALPN instead of
|
|
23
|
-
* OpenSSL's defaults. Opt-in, and off by default: see
|
|
24
|
-
* `network/tls/bun-client-hello.ts` — ciphers and curves match exactly, but
|
|
25
|
-
* the extension list cannot be shaped from Node, so this is a closer hello
|
|
26
|
-
* rather than an identical one.
|
|
27
|
-
*/
|
|
28
|
-
readonly bunClientHello?: boolean;
|
|
29
20
|
}
|
|
30
21
|
export type ValidatedFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
31
22
|
|
|
@@ -164,7 +155,6 @@ async function pinnedFetch(
|
|
|
164
155
|
init: RequestInit,
|
|
165
156
|
resolvedAddress: string | undefined,
|
|
166
157
|
agent?: HttpAgent | HttpsAgent,
|
|
167
|
-
useBunHello = false,
|
|
168
158
|
): Promise<Response> {
|
|
169
159
|
const request = url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
170
160
|
const headers = new Headers(init.headers);
|
|
@@ -232,15 +222,6 @@ async function pinnedFetch(
|
|
|
232
222
|
signal: init.signal ?? undefined,
|
|
233
223
|
...(agent ? { agent } : { agent: directAgent, lookup }),
|
|
234
224
|
...(url.protocol === "https:" ? { servername: url.hostname } : {}),
|
|
235
|
-
// Bun's cipher list, curve list, ALPN and version span, when this
|
|
236
|
-
// request opted into it. See `network/tls/bun-client-hello.ts` for
|
|
237
|
-
// what this does and does not match: ciphers and curves land exactly;
|
|
238
|
-
// the extension list cannot be shaped from Node at all.
|
|
239
|
-
//
|
|
240
|
-
// Opt-in, and off by default. It changes the TLS handshake of every
|
|
241
|
-
// request it touches, and no provider needs it until the free-tier
|
|
242
|
-
// gate is shown to reject the default hello.
|
|
243
|
-
...(useBunHello && url.protocol === "https:" ? bunClientHelloOptions() : {}),
|
|
244
225
|
},
|
|
245
226
|
(response) => {
|
|
246
227
|
const responseHeaders = stripResponseHeaders(response.headers);
|
|
@@ -345,13 +326,7 @@ export function createValidatedFetch(options: ValidatedFetchOptions = {}): Valid
|
|
|
345
326
|
response = await options.fetchFn(url, requestInit);
|
|
346
327
|
} else {
|
|
347
328
|
const agent = agentForTarget(configuredAgent, url.protocol);
|
|
348
|
-
response = await pinnedFetch(
|
|
349
|
-
url,
|
|
350
|
-
requestInit,
|
|
351
|
-
resolvedAddress!,
|
|
352
|
-
agent,
|
|
353
|
-
options.bunClientHello === true,
|
|
354
|
-
);
|
|
329
|
+
response = await pinnedFetch(url, requestInit, resolvedAddress!, agent);
|
|
355
330
|
}
|
|
356
331
|
if (response.status < 300 || response.status >= 400) {
|
|
357
332
|
trackNetworkCall(url.hostname, performance.now() - startedAt);
|