cartethyia 2.7.9 → 2.8.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.
Files changed (496) hide show
  1. package/.env.example +6 -5
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BlC_xdkX.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-0yfhSI1C.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
  31. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage--s-EfEIL.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-2hc12Uz2.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
  44. package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
  45. package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-BMcj1xZE.js +1 -0
  47. package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
  49. package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
  51. package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
  53. package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-DaH6g9hL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-BYKBCzjk.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
  59. package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-Dpx_UXaS.js} +1 -1
  61. package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-BV7-1NoM.js} +1 -1
  63. package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
  65. package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
  67. package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
  69. package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
  71. package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-Dl-cuLK0.js} +1 -1
  73. package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
  75. package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-BxhbQ_JS.js} +1 -1
  77. package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-D49Y4ptn.js} +1 -1
  79. package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
  81. package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
  83. package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
  85. package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-iUoI8GeE.js} +1 -1
  87. package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
  89. package/dist/dashboard/assets/providers/sensenova.png +0 -0
  90. package/dist/dashboard/assets/providers/zro.svg +3 -0
  91. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  92. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  93. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  94. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
  95. package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
  96. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
  97. package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
  98. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
  99. package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
  100. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
  101. package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
  102. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
  103. package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
  104. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
  105. package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
  106. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
  107. package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
  108. package/dist/dashboard/index.html +2 -2
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +54 -53
  111. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  112. package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
  113. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  114. package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
  115. package/package.json +1 -1
  116. package/src/boot/app.ts +11 -14
  117. package/src/boot/dependencies.ts +5 -7
  118. package/src/boot/flush.ts +20 -0
  119. package/src/boot/lifecycle.ts +7 -5
  120. package/src/config/providers.ts +6 -5
  121. package/src/config/routing.ts +26 -0
  122. package/src/config/spec-metadata.ts +72 -0
  123. package/src/config/spec.ts +28 -8
  124. package/src/console/api-keys/contracts.ts +12 -1
  125. package/src/console/api-keys/routes.ts +40 -28
  126. package/src/console/auth/account-reset.ts +7 -9
  127. package/src/console/backup/contracts.ts +4 -0
  128. package/src/console/backup/export-endpoint.ts +7 -5
  129. package/src/console/backup/nine-router.ts +71 -10
  130. package/src/console/backup/service.ts +8 -1
  131. package/src/console/cli-tools/service.ts +6 -12
  132. package/src/console/console-router-guards.ts +23 -16
  133. package/src/console/console-router.ts +1 -1
  134. package/src/console/domain-registration.ts +7 -4
  135. package/src/console/inferhub/client.ts +1 -1
  136. package/src/console/inferhub/normalize.ts +24 -28
  137. package/src/console/inferhub/types.ts +3 -0
  138. package/src/console/ip-bans/routes.ts +7 -13
  139. package/src/console/observability/live.ts +2 -2
  140. package/src/console/observability/store.ts +0 -53
  141. package/src/console/providers/catalog/contracts.ts +9 -3
  142. package/src/console/providers/catalog/model-operations.ts +2 -3
  143. package/src/console/providers/catalog/provider-operations.ts +1 -1
  144. package/src/console/providers/catalog/routes.ts +3 -2
  145. package/src/console/providers/catalog/store.ts +1914 -1902
  146. package/src/console/providers/detail/routes.ts +1 -0
  147. package/src/console/providers/detail/store.ts +4 -0
  148. package/src/console/providers/oauth/callback-listener.ts +30 -11
  149. package/src/console/providers/oauth/device-egress.ts +123 -0
  150. package/src/console/providers/oauth/routes.ts +123 -16
  151. package/src/console/quota/account-quota-global.ts +14 -24
  152. package/src/console/quota/account-quota-shared.ts +49 -0
  153. package/src/console/quota/account-quota-tenant.ts +11 -23
  154. package/src/console/quota/account-test-egress.ts +2 -2
  155. package/src/console/quota/daily-growth-job.ts +1 -1
  156. package/src/console/quota/quota-view.ts +19 -3
  157. package/src/console/routing/pools/contracts.ts +1 -1
  158. package/src/console/routing/pools/operations.ts +32 -47
  159. package/src/console/routing/pools/probe-verdict.ts +2 -2
  160. package/src/console/routing/pools/relay-deploy.ts +1 -1
  161. package/src/console/routing/pools/store.ts +4 -4
  162. package/src/console/shared/errors.ts +33 -6
  163. package/src/main.ts +2 -3
  164. package/src/network/client.ts +67 -0
  165. package/src/network/egress/bidi-session.ts +322 -0
  166. package/src/network/egress/dial.ts +201 -0
  167. package/src/network/egress/factory.ts +191 -0
  168. package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
  169. package/src/network/egress/tls-profile.ts +123 -0
  170. package/src/network/egress/ws.ts +175 -0
  171. package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
  172. package/src/network/{types.ts → pool/agent-config.ts} +1 -1
  173. package/src/network/pool/agent.ts +2 -2
  174. package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
  175. package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
  176. package/src/network/pool/resolver.ts +10 -849
  177. package/src/network/pool/selector.ts +184 -41
  178. package/src/network/ssrf.ts +7 -7
  179. package/src/network/{retry.ts → util/retry.ts} +1 -1
  180. package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
  181. package/src/observability/event-log.ts +7 -361
  182. package/src/observability/performance-metrics.ts +9 -0
  183. package/src/observability/runtime-metrics.ts +14 -9
  184. package/src/observability/telemetry-buffer.ts +16 -26
  185. package/src/observability/telemetry-reconciliation.ts +9 -37
  186. package/src/observability/telemetry-row.ts +125 -0
  187. package/src/persistence/db-mode.ts +4 -1
  188. package/src/persistence/dialect.ts +18 -0
  189. package/src/persistence/migrate.ts +6 -3
  190. package/src/persistence/schema.ts +17 -0
  191. package/src/persistence/ttl-cache.ts +21 -3
  192. package/src/protocol/codecs.ts +59 -2
  193. package/src/protocol/dsml.ts +34 -86
  194. package/src/protocol/markup-stream-parser.ts +195 -0
  195. package/src/protocol/reasoning-text.ts +19 -0
  196. package/src/protocol/request/chat.ts +10 -10
  197. package/src/protocol/request/gemini.ts +8 -2
  198. package/src/protocol/request/messages.ts +34 -22
  199. package/src/protocol/response/chat.ts +12 -20
  200. package/src/protocol/response/messages.ts +34 -7
  201. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  202. package/src/protocol/response/responses.ts +18 -2
  203. package/src/protocol/stop-reason.ts +23 -1
  204. package/src/protocol/think-split.ts +112 -0
  205. package/src/protocol/translation/dialect/qwen.ts +26 -58
  206. package/src/protocol/translation/quirks.ts +15 -0
  207. package/src/protocol/upstream/send-claude-messages.ts +2 -2
  208. package/src/providers/adapters/compatible-adapter.ts +76 -12
  209. package/src/providers/authentication/oauth-client.ts +40 -6
  210. package/src/providers/authentication/oauth-device-flow.ts +105 -11
  211. package/src/providers/authentication/oauth-flow-store.ts +82 -1
  212. package/src/providers/authentication/oauth-refresh-service.ts +766 -758
  213. package/src/providers/configured-provider.ts +148 -0
  214. package/src/providers/default-registry.ts +137 -36
  215. package/src/providers/discovery/discovery-types.ts +7 -0
  216. package/src/providers/discovery/probe-phases.ts +20 -6
  217. package/src/providers/discovery/probing-service.ts +22 -2
  218. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  219. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  220. package/src/providers/integrations/antigravity/antigravity-protocol.ts +16 -1
  221. package/src/providers/integrations/buddy/account.ts +969 -0
  222. package/src/providers/integrations/buddy/capabilities.ts +12 -12
  223. package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
  224. package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
  225. package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
  226. package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
  227. package/src/providers/integrations/buddy/headers.ts +311 -0
  228. package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
  229. package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
  230. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  231. package/src/providers/integrations/cline/cline-oauth.ts +10 -5
  232. package/src/providers/integrations/cline/cline.ts +5 -6
  233. package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
  234. package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
  235. package/src/providers/integrations/clixad/provider.ts +145 -0
  236. package/src/providers/integrations/codex/codex-oauth.ts +8 -5
  237. package/src/providers/integrations/commandcode/quota.ts +22 -28
  238. package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
  239. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  240. package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
  241. package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
  242. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  243. package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
  244. package/src/providers/integrations/cursor/cursor.ts +574 -110
  245. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  246. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  247. package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
  248. package/src/providers/integrations/deepai/deepai-common.ts +145 -0
  249. package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
  250. package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
  251. package/src/providers/integrations/deepai/provider.ts +42 -0
  252. package/src/providers/integrations/devin/devin-auth.ts +7 -0
  253. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  254. package/src/providers/integrations/factory/factory-oauth.ts +33 -51
  255. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  256. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  257. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  258. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  259. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  260. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  261. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  262. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  263. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  264. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  265. package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
  266. package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
  267. package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
  268. package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
  269. package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
  270. package/src/providers/integrations/freepi/freepi-session.ts +158 -0
  271. package/src/providers/integrations/freepi/freepi.ts +80 -54
  272. package/src/providers/integrations/github/github-discovery.ts +30 -13
  273. package/src/providers/integrations/github/github-oauth.ts +217 -44
  274. package/src/providers/integrations/github/github-quota.ts +22 -5
  275. package/src/providers/integrations/github/github.ts +40 -32
  276. package/src/providers/integrations/grok/grok-oauth.ts +52 -49
  277. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  278. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  279. package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
  280. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
  281. package/src/providers/integrations/hermes/index.ts +2 -0
  282. package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
  283. package/src/providers/integrations/kilo/kilo.ts +1 -1
  284. package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
  285. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  286. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  287. package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
  288. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  289. package/src/providers/integrations/kiro/kiro.ts +15 -19
  290. package/src/providers/integrations/muse/muse-oauth.ts +5 -5
  291. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  292. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  293. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  294. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  295. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  296. package/src/providers/integrations/openrouter/provider.ts +1 -1
  297. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  298. package/src/providers/integrations/perch/perch-protocol.ts +0 -6
  299. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  300. package/src/providers/integrations/perch/perch.ts +6 -16
  301. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  302. package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
  303. package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
  304. package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
  305. package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
  306. package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
  307. package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
  308. package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
  309. package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
  310. package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
  311. package/src/providers/integrations/qoder/qoder-types.ts +9 -0
  312. package/src/providers/integrations/qoder/qoder.ts +3 -26
  313. package/src/providers/integrations/sensenova/provider.ts +144 -0
  314. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  315. package/src/providers/integrations/trae/trae-quota.ts +21 -29
  316. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  317. package/src/providers/integrations/trae/trae.ts +7 -24
  318. package/src/providers/integrations/vercel/provider.ts +117 -0
  319. package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
  320. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  321. package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
  322. package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
  323. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  324. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  325. package/src/providers/integrations/zro/zro.ts +205 -0
  326. package/src/providers/operations/account-error-classification.ts +199 -307
  327. package/src/providers/operations/account-health-service.ts +82 -1
  328. package/src/providers/operations/account-refresh-status.ts +36 -37
  329. package/src/providers/operations/account-tier.ts +15 -16
  330. package/src/providers/operations/client-version-resolver.ts +1 -1
  331. package/src/providers/operations/client-versions.ts +100 -81
  332. package/src/providers/operations/cooldown-rules.ts +11 -16
  333. package/src/providers/operations/oauth-refresh-lead.ts +46 -22
  334. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  335. package/src/providers/operations/provider-catalog-service.ts +1 -1
  336. package/src/providers/operations/provider-credential-service.ts +118 -51
  337. package/src/providers/operations/provider-rules/github.ts +48 -0
  338. package/src/providers/operations/provider-rules/grok.ts +33 -0
  339. package/src/providers/operations/provider-rules/inception.ts +49 -0
  340. package/src/providers/operations/provider-rules/index.ts +32 -0
  341. package/src/providers/operations/provider-rules/perch.ts +37 -0
  342. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  343. package/src/providers/operations/provider-rules/trae.ts +81 -0
  344. package/src/providers/operations/session-resolution.ts +17 -1
  345. package/src/providers/provider-contracts.ts +6 -4
  346. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  347. package/src/providers/provider-metadata.ts +70 -10
  348. package/src/providers/provider-probe.ts +15 -1
  349. package/src/providers/provider-registry.ts +42 -33
  350. package/src/providers/quota/quota-contracts.ts +2 -0
  351. package/src/providers/quota/quota-limit-windows.ts +16 -5
  352. package/src/providers/quota/quota-support.ts +1 -1
  353. package/src/providers/quota/quota-window-parser.ts +90 -18
  354. package/src/providers/quota/refresh.ts +100 -21
  355. package/src/security/api-key-auth-cache.ts +1 -1
  356. package/src/security/api-key-auth.ts +6 -2
  357. package/src/security/ip-boundary.ts +1 -1
  358. package/src/transport/dispatch/attempt-finalize.ts +15 -14
  359. package/src/transport/dispatch/attempt-loop.ts +73 -19
  360. package/src/transport/dispatch/generation.ts +2 -8
  361. package/src/transport/dispatch/handler-deps.ts +1 -1
  362. package/src/transport/dispatch/leases.ts +1 -1
  363. package/src/transport/dispatch/native-shared.ts +25 -0
  364. package/src/transport/dispatch/proxy-request.ts +8 -46
  365. package/src/transport/dispatch/responses-compact.ts +2 -8
  366. package/src/transport/dispatch/retry-policy.ts +1 -1
  367. package/src/transport/dispatch/streaming/finalize.ts +10 -7
  368. package/src/transport/dispatch/streaming/stream.ts +9 -37
  369. package/src/transport/dispatch/systemone.ts +2 -8
  370. package/src/transport/dispatch/token-count.ts +2 -6
  371. package/src/transport/dispatch/upstream.ts +63 -0
  372. package/src/transport/drain-endpoint.ts +31 -4
  373. package/src/transport/failure-policy.ts +16 -30
  374. package/src/transport/guards/body-policy.ts +7 -1
  375. package/src/transport/guards/error-lifecycle.ts +13 -1
  376. package/src/transport/guards/request-context.ts +22 -14
  377. package/src/transport/request/preparer.ts +81 -71
  378. package/src/transport/routing/bounded-lru.ts +14 -0
  379. package/src/transport/routing/drain-reserve.ts +129 -0
  380. package/src/transport/routing/plan-gate.ts +7 -1
  381. package/src/transport/routing/round-robin.ts +60 -0
  382. package/src/transport/routing/route-catalog.ts +123 -111
  383. package/src/transport/routing/route-model.ts +51 -9
  384. package/src/transport/routing/router.ts +393 -32
  385. package/src/transport/shutdown-notice.ts +1 -1
  386. package/src/transport/surface/adapters.ts +11 -2
  387. package/src/transport/surface/chat/adapter.ts +2 -2
  388. package/src/transport/surface/chat/encode.ts +47 -4
  389. package/src/transport/surface/chat/parse.ts +2 -12
  390. package/src/transport/surface/completion.ts +44 -33
  391. package/src/transport/surface/content-parts.ts +21 -0
  392. package/src/transport/surface/messages/adapter.ts +7 -1
  393. package/src/transport/surface/messages/encode.ts +11 -3
  394. package/src/transport/surface/messages/parse.ts +2 -1
  395. package/src/transport/surface/messages/stream.ts +33 -17
  396. package/src/transport/surface/responses/encode.ts +22 -5
  397. package/src/transport/surface/responses/parse.ts +3 -11
  398. package/src/workers/buddy-growth-tasks.ts +8 -8
  399. package/src/workers/daily-checkin.ts +4 -4
  400. package/src/workers/quota-refresh-sweep.ts +1 -1
  401. package/src/workers/zcode-gift-claim.ts +1 -1
  402. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  403. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  404. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  405. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  406. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  407. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  408. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  409. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  410. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  411. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  412. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  413. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  414. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  415. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  416. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  417. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  418. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  419. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
  420. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  421. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  422. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  423. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  424. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  425. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  426. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  427. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  428. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  429. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  430. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  431. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  432. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  433. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  434. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  435. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  436. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  437. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  438. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  439. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  440. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  441. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  442. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  443. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  444. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  445. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  446. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  447. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  448. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  449. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  450. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  451. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  452. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  453. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  454. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  455. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  456. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  457. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  458. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  459. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  460. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  461. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  462. package/src/network/pool-health.ts +0 -210
  463. package/src/providers/bundled-discovery-providers.ts +0 -98
  464. package/src/providers/configured-openai-providers.ts +0 -120
  465. package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
  466. package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
  467. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
  468. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
  469. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
  470. package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
  471. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
  472. package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
  473. package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
  474. package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
  475. package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
  476. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
  477. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
  478. package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
  479. package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
  480. package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
  481. package/src/providers/integrations/qoder/chat-proto.ts +0 -328
  482. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
  483. package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
  484. package/src/providers/operations/model-discovery-cache.ts +0 -34
  485. package/src/providers/operations/probe-fetch.ts +0 -15
  486. package/src/providers/operations/provider-version-cache.ts +0 -30
  487. package/src/providers/provider-capabilities.ts +0 -33
  488. package/src/providers/provider-credential-envelope.ts +0 -43
  489. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
  490. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
  491. /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
  492. /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
  493. /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
  494. /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
  495. /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
  496. /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
@@ -1,31 +1,25 @@
1
1
  /**
2
2
  * `PoolAgentResolver` — builds, caches, and reaps the per-pool egress agents
3
- * on demand — and `ValidatedNetworkBindingFactory`, which resolves upstream
4
- * names once through the SSRF policy and hands dispatch a validated fetch
5
- * (direct or pool-bound).
3
+ * on demand.
4
+ *
5
+ * Lifecycle: `resolveAgent` single-flights agent construction per
6
+ * `tenantId:poolId` by caching the build *promise* — concurrent requests over
7
+ * the same pool share one build, and a failed build evicts itself so the next
8
+ * caller retries. Each agent holds persistent sockets (`keepAlive: true`) and
9
+ * carries the SSRF policy so every connect re-validates the proxy host.
6
10
  */
7
11
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
8
12
  import {
9
13
  isAddressAllowed,
10
14
  resolveAllAddresses,
11
- resolveAndValidateOnce,
12
- type ValidatedDestination,
13
15
  } from "../ssrf";
14
- import { createValidatedFetch, type ValidatedFetch } from "../outbound-fetch";
15
- import { GatewayError } from "../../protocol/gateway-error";
16
16
  import type { SsrfPolicy } from "../../config/network";
17
17
  import { log } from "../../observability/logger";
18
18
  import type { TransportKind, PoolAgent } from "./agent";
19
19
  import { isIP } from "node:net";
20
- import * as tls from "node:tls";
21
- import * as http2 from "node:http2";
22
- import type { Duplex } from "node:stream";
23
- import WebSocket, { type ClientOptions, type RawData } from "ws";
24
20
  import { isProxyAgentPair, createHttpProxyAgent, createSocks5Agent, PoolBindingError } from "./agent";
25
- import { Agent as HttpsAgent } from "node:https";
26
- import { parseAgentConfig, ProxyConfigError, type AgentConfig } from "../types";
27
- import type { ProviderWebSocketSession, ValidatedOutboundWebSocket, ValidatedOutboundHttp2Bidi } from "../../providers/provider-contracts";
28
- import type { Http2BidiEndReason, Http2BidiOpenOptions, ValidatedHttp2BidiStream } from "../http2-bidi";
21
+ import { parseAgentConfig, ProxyConfigError, type AgentConfig } from "./agent-config";
22
+ import type { ValidatedOutboundHttp2Bidi } from "../../providers/provider-contracts";
29
23
 
30
24
  /** Idle time after which an unused pool agent is destroyed. */
31
25
  const POOL_AGENT_IDLE_MS = 10 * 60_000;
@@ -33,22 +27,6 @@ const POOL_AGENT_IDLE_MS = 10 * 60_000;
33
27
  const MAX_POOL_AGENTS = 1000;
34
28
  /** Minimum gap between idle-time sweeps; the per-resolve scan is the hot cost. */
35
29
  const POOL_AGENT_SWEEP_INTERVAL_MS = 30_000;
36
- /** Time allowed for one HTTP/2 connection (TCP + TLS + preface) to establish. */
37
- const HTTP2_CONNECT_TIMEOUT_MS = 15_000;
38
- /** Read-idle bound after which a bidi stream is declared dead. */
39
- const HTTP2_READ_IDLE_MS = 120_000;
40
- /** Buffered inbound bytes before a bidi stream is refused as unbounded. */
41
- const HTTP2_MAX_BUFFERED_BYTES = 64 * 1024 * 1024;
42
- /**
43
- * Session memory ceiling for one HTTP/2 session.
44
- *
45
- * Node and Bun count a session's unsent bytes against `maxSessionMemory`
46
- * (10 MB by default) and, once over it, fail every new stream with
47
- * `ENHANCE_YOUR_CALM`. A bidi Run request and its KV replies run to many
48
- * megabytes and the caller holds those bytes anyway, so the default only adds
49
- * a failure mode.
50
- */
51
- const HTTP2_SESSION_MAX_MEMORY_MB = 256;
52
30
 
53
31
  interface PoolAgentResolverDeps {
54
32
  readonly ssrfPolicy?: SsrfPolicy;
@@ -118,7 +96,6 @@ function endpointHostname(kind: TransportKind, endpoint: string): string | undef
118
96
  return undefined;
119
97
  }
120
98
 
121
-
122
99
  export interface NetworkPoolRow {
123
100
  readonly id: string;
124
101
  readonly kind: TransportKind;
@@ -132,27 +109,6 @@ export interface NetworkPoolLoader {
132
109
  load(poolId: string): Promise<NetworkPoolRow | undefined>;
133
110
  }
134
111
 
135
- /**
136
- * Per-pool egress agent cache.
137
- *
138
- * Lifecycle: `resolveAgent` single-flights agent construction per
139
- * `tenantId:poolId` by caching the build *promise* — concurrent requests over
140
- * the same pool share one build, and a failed build evicts itself so the next
141
- * caller retries. Each agent holds persistent sockets (`keepAlive: true`) and
142
- * carries the SSRF policy so every connect re-validates the proxy host.
143
- *
144
- * Reaping: entries idle beyond `POOL_AGENT_IDLE_MS` (10 min) are destroyed on
145
- * the next resolve — bounded memory for pools deleted or disabled outside the
146
- * mutation path, while hot entries are never destroyed mid-request. Also enforces
147
- * a hard cardinality ceiling (MAX_POOL_AGENTS) by evicting the least-recently-used
148
- * entries when the cache exceeds the cap.
149
- * `releasePool` tears down eagerly when a pool is mutated; `closeAll` drains
150
- * the cache at shutdown.
151
- *
152
- * Concurrency is deliberately *not* managed here: slot limits are owned by
153
- * `NetworkPoolSelector` (local counters + Redis `proxy:inflight:*`), so
154
- * agents stay pure connection factories.
155
- */
156
112
  export class PoolAgentResolver {
157
113
  private readonly agents = new Map<string, Promise<PoolAgent>>();
158
114
  private readonly lastUsed = new Map<string, number>();
@@ -310,799 +266,4 @@ export class PoolAgentResolver {
310
266
 
311
267
 
312
268
  }
313
- /** Resolves upstream names and supplies the enforced outbound fetch capability. */
314
- function connectValidatedWebSocket(
315
- url: URL,
316
- headers: Readonly<Record<string, string>>,
317
- signal: AbortSignal,
318
- destination: ValidatedDestination,
319
- agent?: PoolAgent,
320
- ): Promise<ProviderWebSocketSession> {
321
- const targetUrl = new URL(url.toString());
322
- targetUrl.protocol = "wss:";
323
- targetUrl.port = "";
324
- targetUrl.hash = "";
325
- const connectionOptions: ClientOptions = {
326
- handshakeTimeout: 15_000,
327
- perMessageDeflate: false,
328
- followRedirects: false,
329
- headers: { ...headers },
330
- ...(agent === undefined ? {} : { agent: agentForWebSocket(agent) }),
331
- ...(agent === undefined
332
- ? {
333
- createConnection: () => {
334
- const socket = tls.connect({
335
- host: destination.resolvedAddress,
336
- port: 443,
337
- servername: destination.hostname,
338
- rejectUnauthorized: true,
339
- ALPNProtocols: ["http/1.1"],
340
- });
341
- const onAbort = (): void => {
342
- socket.destroy(new Error("connection aborted"));
343
- };
344
- signal.addEventListener("abort", onAbort, { once: true });
345
- socket.once("secureConnect", () => signal.removeEventListener("abort", onAbort));
346
- return socket;
347
- },
348
- }
349
- : {}),
350
- };
351
- return new Promise<ProviderWebSocketSession>((resolve, reject) => {
352
- const socket = new WebSocket(targetUrl, connectionOptions);
353
- let settled = false;
354
- const timeout = setTimeout(() => {
355
- if (settled) return;
356
- settled = true;
357
- socket.terminate();
358
- reject(new GatewayError("transport_unavailable", 502, "WebSocket handshake timed out (request {requestId}). Some things just take longer 🐢"));
359
- }, 15_000);
360
- const abort = (): void => {
361
- if (settled) return;
362
- settled = true;
363
- clearTimeout(timeout);
364
- socket.terminate();
365
- reject(new GatewayError("transport_closed", 499, "WebSocket connection was cancelled (request {requestId}). Send it again whenever ready 🙃"));
366
- };
367
- const onError = (): void => {
368
- if (settled) return;
369
- settled = true;
370
- clearTimeout(timeout);
371
- signal.removeEventListener("abort", abort);
372
- reject(new GatewayError("transport_unavailable", 502, "WebSocket connection failed (request {requestId}). The connection ghosted us — come back in a moment 👻"));
373
- };
374
- socket.once("open", () => {
375
- if (settled) return;
376
- settled = true;
377
- clearTimeout(timeout);
378
- signal.removeEventListener("abort", abort);
379
- resolve(new WsSession(socket, signal));
380
- });
381
- socket.once("error", onError);
382
- socket.once("unexpected-response", onError);
383
- signal.addEventListener("abort", abort, { once: true });
384
- if (signal.aborted) abort();
385
- });
386
- }
387
-
388
-
389
- function agentForWebSocket(agent: PoolAgent): import("http").Agent | import("https").Agent {
390
- if (isProxyAgentPair(agent)) return agent.https;
391
- if (agent instanceof HttpsAgent) return agent;
392
- throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool does not support WSS (request {requestId}). This bridge doesn't speak WSS — reconfigure the pool 🔧");
393
- }
394
-
395
- /**
396
- * Rejects a bidi target that cannot be dialed safely.
397
- *
398
- * Every one of these is a property of the URL, not of the DNS answer, so it is
399
- * checked before any resolution: a credential in the authority would be sent to
400
- * whatever host resolves, a non-443 port is not the TLS endpoint the policy
401
- * validated, and an IP literal bypasses the hostname the caller allowlists.
402
- */
403
- function assertBidiTarget(url: URL): void {
404
- if (url.protocol !== "https:") {
405
- throw new GatewayError("invalid_request", 400, "validated HTTP/2 egress requires https (request {requestId}). Check the shape of what you sent — then resend 🪞");
406
- }
407
- if (url.username || url.password || url.hash) {
408
- throw new GatewayError("invalid_request", 400, "invalid validated HTTP/2 target (request {requestId}). Check the shape of what you sent — then resend 🪞");
409
- }
410
- if (url.port !== "" && url.port !== "443") {
411
- throw new GatewayError("invalid_request", 400, "validated HTTP/2 egress runs on port 443 (request {requestId}). Check the shape of what you sent — then resend 🪞");
412
- }
413
- const hostname = url.hostname;
414
- if (hostname.length === 0) {
415
- throw new GatewayError("invalid_request", 400, "HTTP/2 target hostname is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
416
- }
417
- if (isIP(hostname) !== 0) {
418
- throw new GatewayError("invalid_request", 400, "HTTP/2 target must be a hostname, not an IP literal (request {requestId}). Check the shape of what you sent — then resend 🪞");
419
- }
420
- }
421
-
422
- /**
423
- * Opens one TLS socket to a validated destination.
424
- *
425
- * Direct egress dials the pinned address with SNI set to the *validated
426
- * hostname*: pinning is what stops a DNS rebind between check and connect, and
427
- * preserving SNI is what makes the certificate validate against the name the
428
- * caller asked for rather than against an address.
429
- */
430
- function dialDirect(
431
- destination: ValidatedDestination,
432
- signal: AbortSignal,
433
- ): Promise<tls.TLSSocket> {
434
- return new Promise<tls.TLSSocket>((resolve, reject) => {
435
- const socket = tls.connect({
436
- host: destination.resolvedAddress,
437
- port: destination.port,
438
- servername: destination.hostname,
439
- rejectUnauthorized: true,
440
- ALPNProtocols: ["h2"],
441
- });
442
- const cleanup = (): void => {
443
- clearTimeout(timer);
444
- signal.removeEventListener("abort", onAbort);
445
- };
446
- const onAbort = (): void => {
447
- cleanup();
448
- socket.destroy(new Error("HTTP/2 connection aborted"));
449
- reject(new GatewayError("transport_closed", 499, "HTTP/2 connection was cancelled (request {requestId}). Send it again whenever ready 🙃"));
450
- };
451
- const timer = setTimeout(() => {
452
- cleanup();
453
- socket.destroy();
454
- reject(
455
- new GatewayError("transport_unavailable", 502, "HTTP/2 connection timed out (request {requestId}). Some things just take longer 🐢", {}, "network"),
456
- );
457
- }, HTTP2_CONNECT_TIMEOUT_MS);
458
- socket.once("secureConnect", () => {
459
- cleanup();
460
- resolve(socket);
461
- });
462
- socket.once("error", (error) => {
463
- cleanup();
464
- reject(
465
- new GatewayError(
466
- "transport_unavailable",
467
- 502,
468
- `HTTP/2 connection failed: ${error.message} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
469
- {},
470
- "network",
471
- ),
472
- );
473
- });
474
- signal.addEventListener("abort", onAbort, { once: true });
475
- if (signal.aborted) onAbort();
476
- });
477
- }
478
-
479
- /**
480
- * Opens one TLS socket through a pool agent's CONNECT/SOCKS tunnel.
481
- *
482
- * The agent owns the dial and re-validates the proxy host per connect, so the
483
- * tunnel is the pool's egress; the target TLS handshake then runs inside it
484
- * with SNI set to the validated target hostname.
485
- */
486
- function dialThroughPool(
487
- agent: PoolAgent,
488
- hostname: string,
489
- signal: AbortSignal,
490
- ): Promise<tls.TLSSocket> {
491
- const pair = isProxyAgentPair(agent)
492
- ? agent
493
- : agent instanceof HttpsAgent
494
- ? { https: agent }
495
- : undefined;
496
- if (pair === undefined) {
497
- throw new GatewayError(
498
- "proxy_pool_unhealthy",
499
- 503,
500
- "configured proxy pool does not support HTTPS tunneling (request {requestId}). This bridge doesn't speak HTTPS tunnels — reconfigure the pool 🔧",
501
- );
502
- }
503
- return new Promise<tls.TLSSocket>((resolve, reject) => {
504
- pair.https.createConnection(
505
- { host: hostname, port: 443, servername: hostname },
506
- (error: Error | null, socket: Duplex) => {
507
- if (error !== null || socket === null) {
508
- reject(
509
- error instanceof PoolBindingError
510
- ? new GatewayError(
511
- "proxy_pool_unhealthy",
512
- 503,
513
- "Configured proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧",
514
- { reason: error.message.slice(0, 200) },
515
- "network",
516
- )
517
- : new GatewayError(
518
- "transport_unavailable",
519
- 502,
520
- `HTTP/2 tunnel failed: ${error?.message ?? "no socket"} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
521
- {},
522
- "network",
523
- ),
524
- );
525
- return;
526
- }
527
- const secured = tls.connect({
528
- socket: socket as tls.TLSSocket,
529
- servername: hostname,
530
- rejectUnauthorized: true,
531
- ALPNProtocols: ["h2"],
532
- });
533
- const cleanup = (): void => {
534
- clearTimeout(timer);
535
- signal.removeEventListener("abort", onAbort);
536
- };
537
- const onAbort = (): void => {
538
- cleanup();
539
- secured.destroy();
540
- reject(new GatewayError("transport_closed", 499, "HTTP/2 connection was cancelled (request {requestId}). Send it again whenever ready 🙃"));
541
- };
542
- const timer = setTimeout(() => {
543
- cleanup();
544
- secured.destroy();
545
- reject(
546
- new GatewayError("transport_unavailable", 502, "HTTP/2 connection timed out (request {requestId}). Some things just take longer 🐢", {}, "network"),
547
- );
548
- }, HTTP2_CONNECT_TIMEOUT_MS);
549
- secured.once("secureConnect", () => {
550
- cleanup();
551
- resolve(secured);
552
- });
553
- secured.once("error", (tlsError) => {
554
- cleanup();
555
- reject(
556
- new GatewayError(
557
- "transport_unavailable",
558
- 502,
559
- `HTTP/2 TLS handshake failed: ${tlsError.message} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
560
- {},
561
- "network",
562
- ),
563
- );
564
- });
565
- signal.addEventListener("abort", onAbort, { once: true });
566
- if (signal.aborted) onAbort();
567
- },
568
- );
569
- });
570
- }
571
-
572
- /**
573
- * Wraps one connected HTTP/2 stream as a validated bidi stream.
574
- *
575
- * Inbound bytes are exposed as an async iterable whose consumer pace is the
576
- * backpressure: a paused consumer leaves the data unread and lets the HTTP/2
577
- * flow-control window close, which is what keeps a multi-megabyte Run from
578
- * buffering entirely in memory.
579
- */
580
- class Http2BidiSession implements ValidatedHttp2BidiStream {
581
- private status = 0;
582
- private responseHeaders: Record<string, string> = {};
583
- private responseTrailers: Record<string, string> = {};
584
- private readonly chunks: Uint8Array[] = [];
585
- private readonly waiters: Array<{
586
- resolve(value: IteratorResult<Uint8Array>): void;
587
- reject(reason: unknown): void;
588
- }> = [];
589
- private inboundEnded = false;
590
- private inboundFailure: Error | undefined;
591
- private ended = false;
592
- private terminalReason: Http2BidiEndReason | undefined;
593
- private readonly terminalListeners = new Set<(reason: Http2BidiEndReason) => void>();
594
- private readIdleTimer: ReturnType<typeof setTimeout> | undefined;
595
- private bufferedBytes = 0;
596
-
597
- constructor(
598
- private readonly stream: http2.ClientHttp2Stream,
599
- private readonly session: http2.ClientHttp2Session,
600
- private readonly readIdleMs: number,
601
- private readonly maxBufferedBytes: number,
602
- ) {
603
- stream.on("response", (headers) => {
604
- const status = headers[":status"];
605
- this.status = typeof status === "string" ? Number(status) : (status ?? 0);
606
- this.responseHeaders = flattenHeaders(headers);
607
- });
608
- stream.on("trailers", (headers) => {
609
- this.responseTrailers = flattenHeaders(headers);
610
- });
611
- stream.on("data", (chunk: Buffer) => {
612
- this.armReadIdle();
613
- const bytes = new Uint8Array(chunk);
614
- this.bufferedBytes += bytes.byteLength;
615
- const waiter = this.waiters.shift();
616
- if (waiter !== undefined) {
617
- this.bufferedBytes -= bytes.byteLength;
618
- waiter.resolve({ done: false, value: bytes });
619
- return;
620
- }
621
- this.chunks.push(bytes);
622
- // A stream that keeps arriving while nobody reads is unbounded growth,
623
- // not backpressure: pause the socket rather than accumulating until the
624
- // process is out of memory.
625
- if (this.bufferedBytes > this.maxBufferedBytes) {
626
- this.fail(
627
- new GatewayError(
628
- "platform_unavailable",
629
- 502,
630
- "HTTP/2 response exceeded the buffer bound (request {requestId}). The upstream sent more than we can hold — retry 🌙",
631
- {},
632
- "network",
633
- ),
634
- );
635
- }
636
- });
637
- stream.on("end", () => this.finishInbound());
638
- stream.on("error", (error) => this.fail(error));
639
- stream.on("aborted", () =>
640
- this.fail(new GatewayError("transport_unavailable", 502, "HTTP/2 stream aborted by remote (request {requestId}). The connection ghosted us — come back in a moment 👻", {}, "network")),
641
- );
642
- stream.on("close", () => {
643
- this.finishInbound();
644
- this.settleTerminal({ kind: "remote-clean-close" });
645
- });
646
- }
647
-
648
- get response(): { status: number; headers: Readonly<Record<string, string>> } {
649
- return { status: this.status, headers: this.responseHeaders };
650
- }
651
-
652
- get trailers(): Readonly<Record<string, string>> {
653
- return this.responseTrailers;
654
- }
655
-
656
- get body(): AsyncIterable<Uint8Array> {
657
- const self = this;
658
- return {
659
- [Symbol.asyncIterator](): AsyncIterator<Uint8Array> {
660
- return {
661
- next(): Promise<IteratorResult<Uint8Array>> {
662
- return self.nextChunk();
663
- },
664
- return(): Promise<IteratorResult<Uint8Array>> {
665
- self.abort();
666
- return Promise.resolve({ done: true, value: undefined });
667
- },
668
- };
669
- },
670
- };
671
- }
672
-
673
- private nextChunk(): Promise<IteratorResult<Uint8Array>> {
674
- const queued = this.chunks.shift();
675
- if (queued !== undefined) {
676
- this.bufferedBytes -= queued.byteLength;
677
- return Promise.resolve({ done: false, value: queued });
678
- }
679
- if (this.inboundFailure !== undefined) return Promise.reject(this.inboundFailure);
680
- if (this.inboundEnded) return Promise.resolve({ done: true, value: undefined });
681
- return new Promise<IteratorResult<Uint8Array>>((resolve, reject) => {
682
- this.waiters.push({ resolve, reject });
683
- });
684
- }
685
-
686
- private armReadIdle(): void {
687
- clearTimeout(this.readIdleTimer);
688
- this.readIdleTimer = setTimeout(() => {
689
- this.fail(
690
- new GatewayError(
691
- "transport_unavailable",
692
- 504,
693
- `HTTP/2 stream read-idle timeout after ${this.readIdleMs}ms — connection presumed dead (request {requestId}). The connection ghosted us — come back in a moment 👻`,
694
- {},
695
- "network",
696
- ),
697
- );
698
- }, this.readIdleMs);
699
- this.readIdleTimer.unref?.();
700
- }
701
-
702
- private finishInbound(): void {
703
- clearTimeout(this.readIdleTimer);
704
- this.readIdleTimer = undefined;
705
- if (this.inboundFailure !== undefined || this.inboundEnded) return;
706
- this.inboundEnded = true;
707
- const waiter = this.waiters.shift();
708
- waiter?.resolve({ done: true, value: undefined });
709
- }
710
-
711
- private fail(error: Error): void {
712
- if (this.inboundFailure !== undefined) return;
713
- this.inboundFailure = error;
714
- this.chunks.length = 0;
715
- for (const waiter of this.waiters.splice(0)) waiter.reject(error);
716
- this.settleTerminal({ kind: "remote-error", error });
717
- try {
718
- this.stream.destroy(error);
719
- } catch {
720
- // The stream may already be closing.
721
- }
722
- }
723
-
724
- private settleTerminal(reason: Http2BidiEndReason): void {
725
- if (this.terminalReason !== undefined) return;
726
- this.terminalReason = reason;
727
- for (const listener of this.terminalListeners) {
728
- try {
729
- listener(reason);
730
- } catch {
731
- // An observer cannot destabilize teardown.
732
- }
733
- }
734
- this.terminalListeners.clear();
735
- }
736
-
737
- onTerminal(listener: (reason: Http2BidiEndReason) => void): () => void {
738
- this.terminalListeners.add(listener);
739
- return () => this.terminalListeners.delete(listener);
740
- }
741
-
742
- write(chunk: Uint8Array): Promise<void> {
743
- if (this.inboundFailure !== undefined) return Promise.reject(this.inboundFailure);
744
- if (this.ended) return Promise.reject(new Error("HTTP/2 request side already ended"));
745
- return new Promise<void>((resolve, reject) => {
746
- const flushed = this.stream.write(chunk, (error) => {
747
- if (error !== null && error !== undefined) reject(error);
748
- });
749
- if (flushed) {
750
- resolve();
751
- return;
752
- }
753
- // Backpressured: wait for drain. There is no deadline here — a stream
754
- // error or close settles it, and the read-idle watchdog bounds a stream
755
- // that stops answering.
756
- this.stream.once("drain", () => resolve());
757
- });
758
- }
759
-
760
- end(): void {
761
- if (this.ended) return;
762
- this.ended = true;
763
- try {
764
- this.stream.end();
765
- } catch {
766
- // Already closed by the peer or by abort.
767
- }
768
- }
769
-
770
- abort(reason?: unknown): void {
771
- if (this.ended && this.terminalReason !== undefined) return;
772
- this.ended = true;
773
- this.settleTerminal({ kind: "local-close" });
774
- try {
775
- this.stream.destroy(reason instanceof Error ? reason : undefined);
776
- } catch {
777
- // Already destroyed.
778
- }
779
- try {
780
- this.session.close();
781
- } catch {
782
- // Already closing.
783
- }
784
- }
785
- }
786
-
787
- /** Flattens HTTP/2 header objects into a plain string map. */
788
- function flattenHeaders(headers: Record<string, unknown>): Record<string, string> {
789
- const out: Record<string, string> = {};
790
- for (const [name, value] of Object.entries(headers)) {
791
- if (value === undefined || value === null) continue;
792
- if (Array.isArray(value)) out[name] = value.map((entry) => String(entry)).join(", ");
793
- else out[name] = String(value);
794
- }
795
- return out;
796
- }
797
-
798
- /** Opens one validated bidi stream over an already-connected socket. */
799
- function openBidiStream(
800
- socket: tls.TLSSocket,
801
- url: URL,
802
- headers: Readonly<Record<string, string>>,
803
- options: Http2BidiOpenOptions,
804
- ): Promise<ValidatedHttp2BidiStream> {
805
- // `createConnection`, not the `socket` option: the socket is already a
806
- // validated, ALPN-negotiated TLS connection, and handing it through
807
- // `createConnection` is what makes the session adopt it as-is (Node's
808
- // `socket` option applies its own TLS expectations on top).
809
- const session = http2.connect(`https://${url.hostname}`, {
810
- createConnection: () => socket,
811
- maxSessionMemory: HTTP2_SESSION_MAX_MEMORY_MB,
812
- });
813
- return new Promise<ValidatedHttp2BidiStream>((resolve, reject) => {
814
- const cleanup = (): void => {
815
- clearTimeout(timer);
816
- session.removeListener("error", onError);
817
- session.removeListener("connect", onConnect);
818
- };
819
- const onError = (error: Error): void => {
820
- cleanup();
821
- try {
822
- session.destroy();
823
- } catch {
824
- // Already destroying.
825
- }
826
- reject(
827
- new GatewayError(
828
- "transport_unavailable",
829
- 502,
830
- `HTTP/2 session failed: ${error.message} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
831
- {},
832
- "network",
833
- ),
834
- );
835
- };
836
- const onConnect = (): void => {
837
- cleanup();
838
- const stream = session.request(
839
- {
840
- ":method": "POST",
841
- ":path": `${url.pathname}${url.search}`,
842
- ":authority": url.hostname,
843
- ":scheme": "https",
844
- ...headers,
845
- } as unknown as http2.OutgoingHttpHeaders,
846
- { endStream: false },
847
- );
848
- resolve(
849
- new Http2BidiSession(
850
- stream,
851
- session,
852
- options.readIdleMs ?? HTTP2_READ_IDLE_MS,
853
- options.maxBufferedBytes ?? HTTP2_MAX_BUFFERED_BYTES,
854
- ),
855
- );
856
- };
857
- const timer = setTimeout(() => {
858
- cleanup();
859
- try {
860
- session.destroy();
861
- } catch {
862
- // Already destroying.
863
- }
864
- reject(
865
- new GatewayError("transport_unavailable", 502, "HTTP/2 session timed out (request {requestId}). Some things just take longer 🐢", {}, "network"),
866
- );
867
- }, HTTP2_CONNECT_TIMEOUT_MS);
868
- session.once("error", onError);
869
- session.once("connect", onConnect);
870
- });
871
- }
872
-
873
- class WsSession implements ProviderWebSocketSession {
874
- readonly #socket: WebSocket;
875
- readonly #signal: AbortSignal;
876
- readonly #messages: string[] = [];
877
- readonly #waiters: Array<{
878
- resolve(value: string): void;
879
- reject(reason: Error): void;
880
- cleanup(): void;
881
- }> = [];
882
- #closed: Error | undefined;
883
-
884
- constructor(socket: WebSocket, signal: AbortSignal) {
885
- this.#socket = socket;
886
- this.#signal = signal;
887
- socket.on("message", (data: RawData, isBinary: boolean) => {
888
- if (isBinary) {
889
- this.#fail(new GatewayError("transport_unavailable", 502, "unexpected binary WebSocket frame (request {requestId}). The connection ghosted us — come back in a moment 👻"));
890
- return;
891
- }
892
- const message = data.toString();
893
- const waiter = this.#waiters.shift();
894
- if (waiter) {
895
- waiter.cleanup();
896
- waiter.resolve(message);
897
- } else {
898
- this.#messages.push(message);
899
- }
900
- });
901
- socket.on("close", () => this.#fail(new GatewayError("transport_closed", 502, "WebSocket connection closed (request {requestId}). The connection ghosted us — come back in a moment 👻")));
902
- socket.on("error", () => this.#fail(new GatewayError("transport_unavailable", 502, "WebSocket connection failed (request {requestId}). The connection ghosted us — come back in a moment 👻")));
903
- }
904
-
905
- send(message: string): void {
906
- if (this.#closed) throw this.#closed;
907
- if (this.#socket.readyState !== WebSocket.OPEN) {
908
- throw new GatewayError("transport_closed", 499, "WebSocket connection is not open (request {requestId}). Send it again whenever ready 🙃");
909
- }
910
- this.#socket.send(message, { binary: false });
911
- }
912
-
913
- receive(signal: AbortSignal): Promise<string> {
914
- const queued = this.#messages.shift();
915
- if (queued !== undefined) return Promise.resolve(queued);
916
- if (this.#closed) return Promise.reject(this.#closed);
917
- const combined = AbortSignal.any([this.#signal, signal]);
918
- return new Promise<string>((resolve, reject) => {
919
- const timeout = setTimeout(() => {
920
- cleanup();
921
- reject(new GatewayError("transport_unavailable", 504, "WebSocket receive timed out (request {requestId}). Some things just take longer 🐢"));
922
- }, 45_000);
923
- const onAbort = (): void => {
924
- cleanup();
925
- reject(new GatewayError("transport_closed", 499, "WebSocket receive was cancelled (request {requestId}). Send it again whenever ready 🙃"));
926
- };
927
- const cleanup = (): void => {
928
- clearTimeout(timeout);
929
- combined.removeEventListener("abort", onAbort);
930
- };
931
- combined.addEventListener("abort", onAbort, { once: true });
932
- this.#waiters.push({ resolve, reject, cleanup });
933
- if (combined.aborted) onAbort();
934
- });
935
- }
936
-
937
- close(code?: number, reason?: string): void {
938
- if (this.#socket.readyState === WebSocket.CLOSED) return;
939
- this.#socket.close(code, reason);
940
- }
941
-
942
- #fail(error: Error): void {
943
- if (this.#closed) return;
944
- this.#closed = error;
945
- for (const waiter of this.#waiters.splice(0)) {
946
- waiter.cleanup();
947
- waiter.reject(error);
948
- }
949
- }
950
- }
951
- export class ValidatedNetworkBindingFactory {
952
- private readonly directFetch: ValidatedFetch;
953
- /**
954
- * One `ValidatedFetch` per resolved pool agent. Building the closure (dialer,
955
- * relay setup, redirect loop) on every request through a pool was pure
956
- * churn; keying by the agent object means a released agent is naturally
957
- * collected (new builds mint a new agent) so no explicit invalidation is
958
- * needed.
959
- */
960
- private readonly pooledFetchCache = new WeakMap<PoolAgent, ValidatedFetch>();
961
-
962
- constructor(
963
- private readonly policy: SsrfPolicy = {},
964
- fetchFn?: typeof fetch,
965
- private readonly poolResolver?: PoolAgentResolver,
966
- ) {
967
- this.directFetch = createValidatedFetch({ policy, ...(fetchFn ? { fetchFn } : {}) });
968
- }
969
-
970
- /**
971
- * One-shot DNS resolution + SSRF validation for direct egress targets:
972
- * every resolved address (all families, not just the first answer) must
973
- * pass the SSRF policy, and the validated destination pins the address so
974
- * the subsequent socket cannot be re-pointed at a private/link-local host
975
- * by a DNS rebind between check and connect.
976
- */
977
- resolve(hostname: string, signal: AbortSignal, port = 443): Promise<ValidatedDestination> {
978
- return resolveAndValidateOnce(hostname, this.policy, signal, port);
979
- }
980
-
981
- /**
982
- * Without `networkPoolId` (or when no pool resolver is configured), dispatch
983
- * through validated direct egress. With one, route through that pool's
984
- * validated agent. A pool configuration failure is returned to the caller;
985
- * configured egress is never silently replaced with direct traffic.
986
- */
987
- fetch(networkPoolId?: string, tenantId?: string): ValidatedFetch {
988
- if (!networkPoolId || !this.poolResolver || !tenantId) return this.directFetch;
989
- const { poolResolver, policy } = this;
990
- return async (input, init) => {
991
- try {
992
- const agent = await poolResolver.resolveAgent(networkPoolId, tenantId);
993
- let validated = this.pooledFetchCache.get(agent);
994
- if (validated === undefined) {
995
- validated = createValidatedFetch({ policy, agent });
996
- this.pooledFetchCache.set(agent, validated);
997
- }
998
- return await validated(input, init);
999
- } catch (error) {
1000
- if (error instanceof PoolBindingError) {
1001
- throw new GatewayError(
1002
- "proxy_pool_unhealthy",
1003
- 503,
1004
- "Configured proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧",
1005
- { poolId: networkPoolId, reason: error.message.slice(0, 200) },
1006
- "network",
1007
- );
1008
- }
1009
- throw error;
1010
- }
1011
- };
1012
- }
1013
- webSocket(networkPoolId?: string, tenantId?: string): ValidatedOutboundWebSocket {
1014
- return async (url, headers, signal) => {
1015
- if (url.protocol !== "wss:") {
1016
- throw new GatewayError("invalid_request", 400, "validated WebSocket egress requires wss (request {requestId}). Check the shape of what you sent — then resend 🪞");
1017
- }
1018
- if (url.username || url.password || url.hash || (url.port !== "" && url.port !== "443")) {
1019
- throw new GatewayError("invalid_request", 400, "invalid validated WebSocket target (request {requestId}). Check the shape of what you sent — then resend 🪞");
1020
- }
1021
- const hostname = url.hostname;
1022
- if (!hostname) throw new GatewayError("invalid_request", 400, "WebSocket target hostname is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
1023
- const destination = await this.resolve(hostname, signal, 443);
1024
- if (!isAddressAllowed(destination.resolvedAddress, this.policy)) {
1025
- throw new GatewayError("invalid_request", 400, "unsafe upstream address rejected (request {requestId}). Check the shape of what you sent — then resend 🪞");
1026
- }
1027
- let agent: PoolAgent | undefined;
1028
- if (networkPoolId || tenantId) {
1029
- if (!networkPoolId || !tenantId || !this.poolResolver) {
1030
- throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧");
1031
- }
1032
- try {
1033
- agent = await this.poolResolver.resolveAgent(networkPoolId, tenantId);
1034
- } catch (error) {
1035
- if (error instanceof PoolBindingError) {
1036
- throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧");
1037
- }
1038
- throw error;
1039
- }
1040
- }
1041
- return connectValidatedWebSocket(url, headers, signal, destination, agent);
1042
- };
1043
- }
1044
-
1045
- /**
1046
- * Validated bidirectional HTTP/2 egress for one writable streaming request.
1047
- *
1048
- * Direct egress resolves and validates *every* DNS answer for the hostname,
1049
- * pins the chosen public address, and keeps TLS SNI pointed at the validated
1050
- * hostname — so a rebind between check and connect cannot retarget the socket,
1051
- * and the certificate still validates against the name the caller asked for.
1052
- * Pooled egress reuses the selected tenant pool's CONNECT/SOCKS tunnel and
1053
- * accounts the tunnel's bytes to that pool, exactly as `fetch` does.
1054
- *
1055
- * Credentials in the authority, fragments, non-443 ports, IP literals, and
1056
- * hostnames outside `allowHostname` are refused before anything is dialed: a
1057
- * bidi stream carries the account credential for its whole lifetime, so the
1058
- * target is checked once here rather than trusted to each caller.
1059
- */
1060
- http2Bidi(networkPoolId?: string, tenantId?: string): ValidatedOutboundHttp2Bidi {
1061
- return async (url, headers, options) => {
1062
- assertBidiTarget(url);
1063
- const { hostname } = url;
1064
- if (options?.allowHostname !== undefined && !options.allowHostname(hostname)) {
1065
- throw new GatewayError("invalid_request", 400, "HTTP/2 target host is not allowlisted (request {requestId}). All roads blocked — time to find a new route 🗺️");
1066
- }
1067
- const signal = options?.signal ?? AbortSignal.timeout(HTTP2_CONNECT_TIMEOUT_MS);
1068
- // Validation runs on every new connection, not once per capability: a
1069
- // long-lived Run is re-opened after a session dies, and a hostname that
1070
- // resolved safely the first time is not safe the twentieth.
1071
- const destination = await this.resolve(hostname, signal, 443);
1072
- if (!isAddressAllowed(destination.resolvedAddress, this.policy)) {
1073
- throw new GatewayError("invalid_request", 400, "unsafe upstream address rejected (request {requestId}). All roads blocked — time to find a new route 🗺️");
1074
- }
1075
- // Mirrors `fetch`: no pool selected (or none configured) means validated
1076
- // *direct* egress, which is already resolved and SSRF-checked above.
1077
- // Requiring a pool whenever a tenant id was supplied made bidi-only
1078
- // providers (Cursor) unreachable on every install without a pool, even
1079
- // though the equivalent `fetch` path dials direct there.
1080
- if (networkPoolId !== undefined || tenantId !== undefined) {
1081
- if (networkPoolId === undefined || tenantId === undefined || this.poolResolver === undefined) {
1082
- const socket = await dialDirect(destination, signal);
1083
- return openBidiStream(socket, url, headers, options ?? {});
1084
- }
1085
- let agent: PoolAgent;
1086
- try {
1087
- agent = await this.poolResolver.resolveAgent(networkPoolId, tenantId);
1088
- } catch (error) {
1089
- if (error instanceof PoolBindingError) {
1090
- throw new GatewayError(
1091
- "proxy_pool_unhealthy",
1092
- 503,
1093
- "Configured proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧",
1094
- { poolId: networkPoolId, reason: error.message.slice(0, 200) },
1095
- "network",
1096
- );
1097
- }
1098
- throw error;
1099
- }
1100
- const socket = await dialThroughPool(agent, hostname, signal);
1101
- return openBidiStream(socket, url, headers, options ?? {});
1102
- }
1103
- const socket = await dialDirect(destination, signal);
1104
- return openBidiStream(socket, url, headers, options ?? {});
1105
- };
1106
- }
1107
- }
1108
-
269
+ export type { ValidatedOutboundHttp2Bidi };