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
@@ -0,0 +1,322 @@
1
+ /**
2
+ * HTTP/2 bidi session — one validated bidirectional stream over a dialed TLS
3
+ * socket, plus the session wrapper that drives it.
4
+ *
5
+ * Inbound bytes are exposed as an async iterable whose consumer pace is the
6
+ * backpressure: a paused consumer leaves the data unread and lets the HTTP/2
7
+ * flow-control window close, which is what keeps a multi-megabyte Run from
8
+ * buffering entirely in memory.
9
+ */
10
+ import * as http2 from "node:http2";
11
+ import type { TLSSocket } from "node:tls";
12
+ import { GatewayError } from "../../protocol/gateway-error";
13
+ import type { Http2BidiEndReason, Http2BidiOpenOptions, ValidatedHttp2BidiStream } from "../bidi";
14
+
15
+ export const HTTP2_CONNECT_TIMEOUT_MS = 15_000;
16
+ /** Read-idle bound after which a bidi stream is declared dead. */
17
+ export const HTTP2_READ_IDLE_MS = 120_000;
18
+ /** Buffered inbound bytes before a bidi stream is refused as unbounded. */
19
+ export const HTTP2_MAX_BUFFERED_BYTES = 64 * 1024 * 1024;
20
+ /**
21
+ * Session memory ceiling for one HTTP/2 session.
22
+ *
23
+ * Node and Bun count a session's unsent bytes against `maxSessionMemory`
24
+ * (10 MB by default) and, once over it, fail every new stream with
25
+ * `ENHANCE_YOUR_CALM`. A bidi Run request and its KV replies run to many
26
+ * megabytes and the caller holds those bytes anyway, so the default only adds
27
+ * a failure mode.
28
+ */
29
+ export const HTTP2_SESSION_MAX_MEMORY_MB = 256;
30
+
31
+ class Http2BidiSession implements ValidatedHttp2BidiStream {
32
+ private status = 0;
33
+ private responseHeaders: Record<string, string> = {};
34
+ private responseTrailers: Record<string, string> = {};
35
+ private readonly chunks: Uint8Array[] = [];
36
+ private readonly waiters: Array<{
37
+ resolve(value: IteratorResult<Uint8Array>): void;
38
+ reject(reason: unknown): void;
39
+ }> = [];
40
+ private inboundEnded = false;
41
+ private inboundFailure: Error | undefined;
42
+ private ended = false;
43
+ private terminalReason: Http2BidiEndReason | undefined;
44
+ private readonly terminalListeners = new Set<(reason: Http2BidiEndReason) => void>();
45
+ private readIdleTimer: ReturnType<typeof setTimeout> | undefined;
46
+ private bufferedBytes = 0;
47
+
48
+ constructor(
49
+ private readonly stream: http2.ClientHttp2Stream,
50
+ private readonly session: http2.ClientHttp2Session,
51
+ private readonly readIdleMs: number,
52
+ private readonly maxBufferedBytes: number,
53
+ ) {
54
+ stream.on("response", (headers) => {
55
+ const status = headers[":status"];
56
+ this.status = typeof status === "string" ? Number(status) : (status ?? 0);
57
+ this.responseHeaders = flattenHeaders(headers);
58
+ });
59
+ stream.on("trailers", (headers) => {
60
+ this.responseTrailers = flattenHeaders(headers);
61
+ });
62
+ stream.on("data", (chunk: Buffer) => {
63
+ this.armReadIdle();
64
+ const bytes = new Uint8Array(chunk);
65
+ this.bufferedBytes += bytes.byteLength;
66
+ const waiter = this.waiters.shift();
67
+ if (waiter !== undefined) {
68
+ this.bufferedBytes -= bytes.byteLength;
69
+ waiter.resolve({ done: false, value: bytes });
70
+ return;
71
+ }
72
+ this.chunks.push(bytes);
73
+ // A stream that keeps arriving while nobody reads is unbounded growth,
74
+ // not backpressure: pause the socket rather than accumulating until the
75
+ // process is out of memory.
76
+ if (this.bufferedBytes > this.maxBufferedBytes) {
77
+ this.fail(
78
+ new GatewayError(
79
+ "platform_unavailable",
80
+ 502,
81
+ "HTTP/2 response exceeded the buffer bound (request {requestId}). The upstream sent more than we can hold — retry 🌙",
82
+ {},
83
+ "network",
84
+ ),
85
+ );
86
+ }
87
+ });
88
+ stream.on("end", () => this.finishInbound());
89
+ stream.on("error", (error) => this.fail(error));
90
+ stream.on("aborted", () =>
91
+ 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")),
92
+ );
93
+ stream.on("close", () => {
94
+ this.finishInbound();
95
+ this.settleTerminal({ kind: "remote-clean-close" });
96
+ });
97
+ }
98
+
99
+ get response(): { status: number; headers: Readonly<Record<string, string>> } {
100
+ return { status: this.status, headers: this.responseHeaders };
101
+ }
102
+
103
+ get trailers(): Readonly<Record<string, string>> {
104
+ return this.responseTrailers;
105
+ }
106
+
107
+ get body(): AsyncIterable<Uint8Array> {
108
+ const self = this;
109
+ return {
110
+ [Symbol.asyncIterator](): AsyncIterator<Uint8Array> {
111
+ return {
112
+ next(): Promise<IteratorResult<Uint8Array>> {
113
+ return self.nextChunk();
114
+ },
115
+ return(): Promise<IteratorResult<Uint8Array>> {
116
+ self.abort();
117
+ return Promise.resolve({ done: true, value: undefined });
118
+ },
119
+ };
120
+ },
121
+ };
122
+ }
123
+
124
+ private nextChunk(): Promise<IteratorResult<Uint8Array>> {
125
+ const queued = this.chunks.shift();
126
+ if (queued !== undefined) {
127
+ this.bufferedBytes -= queued.byteLength;
128
+ return Promise.resolve({ done: false, value: queued });
129
+ }
130
+ if (this.inboundFailure !== undefined) return Promise.reject(this.inboundFailure);
131
+ if (this.inboundEnded) return Promise.resolve({ done: true, value: undefined });
132
+ return new Promise<IteratorResult<Uint8Array>>((resolve, reject) => {
133
+ this.waiters.push({ resolve, reject });
134
+ });
135
+ }
136
+
137
+ private armReadIdle(): void {
138
+ clearTimeout(this.readIdleTimer);
139
+ this.readIdleTimer = setTimeout(() => {
140
+ this.fail(
141
+ new GatewayError(
142
+ "transport_unavailable",
143
+ 504,
144
+ `HTTP/2 stream read-idle timeout after ${this.readIdleMs}ms — connection presumed dead (request {requestId}). The connection ghosted us — come back in a moment 👻`,
145
+ {},
146
+ "network",
147
+ ),
148
+ );
149
+ }, this.readIdleMs);
150
+ this.readIdleTimer.unref?.();
151
+ }
152
+
153
+ private finishInbound(): void {
154
+ clearTimeout(this.readIdleTimer);
155
+ this.readIdleTimer = undefined;
156
+ if (this.inboundFailure !== undefined || this.inboundEnded) return;
157
+ this.inboundEnded = true;
158
+ const waiter = this.waiters.shift();
159
+ waiter?.resolve({ done: true, value: undefined });
160
+ }
161
+
162
+ private fail(error: Error): void {
163
+ if (this.inboundFailure !== undefined) return;
164
+ this.inboundFailure = error;
165
+ this.chunks.length = 0;
166
+ for (const waiter of this.waiters.splice(0)) waiter.reject(error);
167
+ this.settleTerminal({ kind: "remote-error", error });
168
+ try {
169
+ this.stream.destroy(error);
170
+ } catch {
171
+ // The stream may already be closing.
172
+ }
173
+ }
174
+
175
+ private settleTerminal(reason: Http2BidiEndReason): void {
176
+ if (this.terminalReason !== undefined) return;
177
+ this.terminalReason = reason;
178
+ for (const listener of this.terminalListeners) {
179
+ try {
180
+ listener(reason);
181
+ } catch {
182
+ // An observer cannot destabilize teardown.
183
+ }
184
+ }
185
+ this.terminalListeners.clear();
186
+ }
187
+
188
+ onTerminal(listener: (reason: Http2BidiEndReason) => void): () => void {
189
+ this.terminalListeners.add(listener);
190
+ return () => this.terminalListeners.delete(listener);
191
+ }
192
+
193
+ write(chunk: Uint8Array): Promise<void> {
194
+ if (this.inboundFailure !== undefined) return Promise.reject(this.inboundFailure);
195
+ if (this.ended) return Promise.reject(new Error("HTTP/2 request side already ended"));
196
+ return new Promise<void>((resolve, reject) => {
197
+ const flushed = this.stream.write(chunk, (error) => {
198
+ if (error !== null && error !== undefined) reject(error);
199
+ });
200
+ if (flushed) {
201
+ resolve();
202
+ return;
203
+ }
204
+ // Backpressured: wait for drain. There is no deadline here — a stream
205
+ // error or close settles it, and the read-idle watchdog bounds a stream
206
+ // that stops answering.
207
+ this.stream.once("drain", () => resolve());
208
+ });
209
+ }
210
+
211
+ end(): void {
212
+ if (this.ended) return;
213
+ this.ended = true;
214
+ try {
215
+ this.stream.end();
216
+ } catch {
217
+ // Already closed by the peer or by abort.
218
+ }
219
+ }
220
+
221
+ abort(reason?: unknown): void {
222
+ if (this.ended && this.terminalReason !== undefined) return;
223
+ this.ended = true;
224
+ this.settleTerminal({ kind: "local-close" });
225
+ try {
226
+ this.stream.destroy(reason instanceof Error ? reason : undefined);
227
+ } catch {
228
+ // Already destroyed.
229
+ }
230
+ try {
231
+ this.session.close();
232
+ } catch {
233
+ // Already closing.
234
+ }
235
+ }
236
+ }
237
+
238
+ /** Flattens HTTP/2 header objects into a plain string map. */
239
+ function flattenHeaders(headers: Record<string, unknown>): Record<string, string> {
240
+ const out: Record<string, string> = {};
241
+ for (const [name, value] of Object.entries(headers)) {
242
+ if (value === undefined || value === null) continue;
243
+ if (Array.isArray(value)) out[name] = value.map((entry) => String(entry)).join(", ");
244
+ else out[name] = String(value);
245
+ }
246
+ return out;
247
+ }
248
+
249
+ /** Opens one validated bidi stream over an already-connected socket. */
250
+ export function openBidiStream(
251
+ socket: TLSSocket,
252
+ url: URL,
253
+ headers: Readonly<Record<string, string>>,
254
+ options: Http2BidiOpenOptions,
255
+ ): Promise<ValidatedHttp2BidiStream> {
256
+ // `createConnection`, not the `socket` option: the socket is already a
257
+ // validated, ALPN-negotiated TLS connection, and handing it through
258
+ // `createConnection` is what makes the session adopt it as-is (Node's
259
+ // `socket` option applies its own TLS expectations on top).
260
+ const session = http2.connect(`https://${url.hostname}`, {
261
+ createConnection: () => socket,
262
+ maxSessionMemory: HTTP2_SESSION_MAX_MEMORY_MB,
263
+ });
264
+ return new Promise<ValidatedHttp2BidiStream>((resolve, reject) => {
265
+ const cleanup = (): void => {
266
+ clearTimeout(timer);
267
+ session.removeListener("error", onError);
268
+ session.removeListener("connect", onConnect);
269
+ };
270
+ const onError = (error: Error): void => {
271
+ cleanup();
272
+ try {
273
+ session.destroy();
274
+ } catch {
275
+ // Already destroying.
276
+ }
277
+ reject(
278
+ new GatewayError(
279
+ "transport_unavailable",
280
+ 502,
281
+ `HTTP/2 session failed: ${error.message} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
282
+ {},
283
+ "network",
284
+ ),
285
+ );
286
+ };
287
+ const onConnect = (): void => {
288
+ cleanup();
289
+ const stream = session.request(
290
+ {
291
+ ":method": "POST",
292
+ ":path": `${url.pathname}${url.search}`,
293
+ ":authority": url.hostname,
294
+ ":scheme": "https",
295
+ ...headers,
296
+ } as unknown as http2.OutgoingHttpHeaders,
297
+ { endStream: false },
298
+ );
299
+ resolve(
300
+ new Http2BidiSession(
301
+ stream,
302
+ session,
303
+ options.readIdleMs ?? HTTP2_READ_IDLE_MS,
304
+ options.maxBufferedBytes ?? HTTP2_MAX_BUFFERED_BYTES,
305
+ ),
306
+ );
307
+ };
308
+ const timer = setTimeout(() => {
309
+ cleanup();
310
+ try {
311
+ session.destroy();
312
+ } catch {
313
+ // Already destroying.
314
+ }
315
+ reject(
316
+ new GatewayError("transport_unavailable", 502, "HTTP/2 session timed out (request {requestId}). Some things just take longer 🐢", {}, "network"),
317
+ );
318
+ }, HTTP2_CONNECT_TIMEOUT_MS);
319
+ session.once("error", onError);
320
+ session.once("connect", onConnect);
321
+ });
322
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Raw validated TLS dials — direct and through a pool tunnel.
3
+ *
4
+ * Direct egress dials the pinned address with SNI set to the *validated
5
+ * hostname*: pinning is what stops a DNS rebind between check and connect, and
6
+ * preserving SNI is what makes the certificate validate against the name the
7
+ * caller asked for rather than against an address.
8
+ *
9
+ * A pool tunnel dial runs the target TLS handshake inside the pool's
10
+ * CONNECT/SOCKS tunnel with SNI set to the validated target hostname.
11
+ *
12
+ * Both accept an optional TLS ClientHello profile (`tls-profile.ts`) so the
13
+ * dial mirrors a target client's transport fingerprint when one is requested.
14
+ */
15
+ import { isIP } from "node:net";
16
+ import * as tls from "node:tls";
17
+ import type { Duplex } from "node:stream";
18
+ import { Agent as HttpsAgent } from "node:https";
19
+ import { GatewayError } from "../../protocol/gateway-error";
20
+ import { isProxyAgentPair, PoolBindingError, type PoolAgent } from "../pool/agent";
21
+ import { nodeTlsOptions, type TlsProfileConfig } from "./tls-profile";
22
+ import type { ValidatedDestination } from "../ssrf";
23
+
24
+ /** Time allowed for one HTTP/2 connection (TCP + TLS + preface) to establish. */
25
+ export const HTTP2_CONNECT_TIMEOUT_MS = 15_000;
26
+
27
+ /**
28
+ * Rejects a bidi target that cannot be dialed safely.
29
+ *
30
+ * Every one of these is a property of the URL, not of the DNS answer, so it is
31
+ * checked before any resolution: a credential in the authority would be sent to
32
+ * whatever host resolves, a non-443 port is not the TLS endpoint the policy
33
+ * validated, and an IP literal bypasses the hostname the caller allowlists.
34
+ */
35
+ export function assertBidiTarget(url: URL): void {
36
+ if (url.protocol !== "https:") {
37
+ throw new GatewayError("invalid_request", 400, "validated HTTP/2 egress requires https (request {requestId}). Check the shape of what you sent — then resend 🪞");
38
+ }
39
+ if (url.username || url.password || url.hash) {
40
+ throw new GatewayError("invalid_request", 400, "invalid validated HTTP/2 target (request {requestId}). Check the shape of what you sent — then resend 🪞");
41
+ }
42
+ if (url.port !== "" && url.port !== "443") {
43
+ 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 🪞");
44
+ }
45
+ const hostname = url.hostname;
46
+ if (hostname.length === 0) {
47
+ throw new GatewayError("invalid_request", 400, "HTTP/2 target hostname is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
48
+ }
49
+ if (isIP(hostname) !== 0) {
50
+ 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 🪞");
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Opens one TLS socket to a validated destination.
56
+ */
57
+ export function dialDirect(
58
+ destination: ValidatedDestination,
59
+ signal: AbortSignal,
60
+ tlsProfile?: TlsProfileConfig,
61
+ ): Promise<tls.TLSSocket> {
62
+ return new Promise<tls.TLSSocket>((resolve, reject) => {
63
+ const socket = tls.connect({
64
+ host: destination.resolvedAddress,
65
+ port: destination.port,
66
+ servername: destination.hostname,
67
+ rejectUnauthorized: true,
68
+ ALPNProtocols: ["h2"],
69
+ ...(tlsProfile === undefined ? {} : { ...nodeTlsOptions(tlsProfile, false) }),
70
+ } as tls.ConnectionOptions);
71
+ const cleanup = (): void => {
72
+ clearTimeout(timer);
73
+ signal.removeEventListener("abort", onAbort);
74
+ };
75
+ const onAbort = (): void => {
76
+ cleanup();
77
+ socket.destroy(new Error("HTTP/2 connection aborted"));
78
+ reject(new GatewayError("transport_closed", 499, "HTTP/2 connection was cancelled (request {requestId}). Send it again whenever ready 🙃"));
79
+ };
80
+ const timer = setTimeout(() => {
81
+ cleanup();
82
+ socket.destroy();
83
+ reject(
84
+ new GatewayError("transport_unavailable", 502, "HTTP/2 connection timed out (request {requestId}). Some things just take longer 🐢", {}, "network"),
85
+ );
86
+ }, HTTP2_CONNECT_TIMEOUT_MS);
87
+ socket.once("secureConnect", () => {
88
+ cleanup();
89
+ resolve(socket);
90
+ });
91
+ socket.once("error", (error) => {
92
+ cleanup();
93
+ reject(
94
+ new GatewayError(
95
+ "transport_unavailable",
96
+ 502,
97
+ `HTTP/2 connection failed: ${error.message} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
98
+ {},
99
+ "network",
100
+ ),
101
+ );
102
+ });
103
+ signal.addEventListener("abort", onAbort, { once: true });
104
+ if (signal.aborted) onAbort();
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Opens one TLS socket through a pool agent's CONNECT/SOCKS tunnel.
110
+ *
111
+ * The agent owns the dial and re-validates the proxy host per connect, so the
112
+ * tunnel is the pool's egress; the target TLS handshake then runs inside it
113
+ * with SNI set to the validated target hostname.
114
+ */
115
+ export function dialThroughPool(
116
+ agent: PoolAgent,
117
+ hostname: string,
118
+ signal: AbortSignal,
119
+ tlsProfile?: TlsProfileConfig,
120
+ ): Promise<tls.TLSSocket> {
121
+ const pair = isProxyAgentPair(agent)
122
+ ? agent
123
+ : agent instanceof HttpsAgent
124
+ ? { https: agent }
125
+ : undefined;
126
+ if (pair === undefined) {
127
+ throw new GatewayError(
128
+ "proxy_pool_unhealthy",
129
+ 503,
130
+ "configured proxy pool does not support HTTPS tunneling (request {requestId}). This bridge doesn't speak HTTPS tunnels — reconfigure the pool 🔧",
131
+ );
132
+ }
133
+ return new Promise<tls.TLSSocket>((resolve, reject) => {
134
+ pair.https.createConnection(
135
+ { host: hostname, port: 443, servername: hostname },
136
+ (error: Error | null, socket: Duplex) => {
137
+ if (error !== null || socket === null) {
138
+ reject(
139
+ error instanceof PoolBindingError
140
+ ? new GatewayError(
141
+ "proxy_pool_unhealthy",
142
+ 503,
143
+ "Configured proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧",
144
+ { reason: error.message.slice(0, 200) },
145
+ "network",
146
+ )
147
+ : new GatewayError(
148
+ "transport_unavailable",
149
+ 502,
150
+ `HTTP/2 tunnel failed: ${error?.message ?? "no socket"} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
151
+ {},
152
+ "network",
153
+ ),
154
+ );
155
+ return;
156
+ }
157
+ const secured = tls.connect({
158
+ socket: socket as tls.TLSSocket,
159
+ servername: hostname,
160
+ rejectUnauthorized: true,
161
+ ALPNProtocols: ["h2"],
162
+ ...(tlsProfile === undefined ? {} : { ...nodeTlsOptions(tlsProfile, false) }),
163
+ } as tls.ConnectionOptions);
164
+ const cleanup = (): void => {
165
+ clearTimeout(timer);
166
+ signal.removeEventListener("abort", onAbort);
167
+ };
168
+ const onAbort = (): void => {
169
+ cleanup();
170
+ secured.destroy();
171
+ reject(new GatewayError("transport_closed", 499, "HTTP/2 connection was cancelled (request {requestId}). Send it again whenever ready 🙃"));
172
+ };
173
+ const timer = setTimeout(() => {
174
+ cleanup();
175
+ secured.destroy();
176
+ reject(
177
+ new GatewayError("transport_unavailable", 502, "HTTP/2 connection timed out (request {requestId}). Some things just take longer 🐢", {}, "network"),
178
+ );
179
+ }, HTTP2_CONNECT_TIMEOUT_MS);
180
+ secured.once("secureConnect", () => {
181
+ cleanup();
182
+ resolve(secured);
183
+ });
184
+ secured.once("error", (tlsError) => {
185
+ cleanup();
186
+ reject(
187
+ new GatewayError(
188
+ "transport_unavailable",
189
+ 502,
190
+ `HTTP/2 TLS handshake failed: ${tlsError.message} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
191
+ {},
192
+ "network",
193
+ ),
194
+ );
195
+ });
196
+ signal.addEventListener("abort", onAbort, { once: true });
197
+ if (signal.aborted) onAbort();
198
+ },
199
+ );
200
+ });
201
+ }