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,191 @@
1
+ /**
2
+ * `ValidatedNetworkBindingFactory` — one binding point for every validated
3
+ * egress capability: fetch, profile-fetch, HTTP/2 bidi, WebSocket, and
4
+ * one-shot DNS+SSRF resolution.
5
+ *
6
+ * Direct egress resolves and validates *every* DNS answer for the hostname,
7
+ * pins the chosen public address, and keeps TLS SNI pointed at the validated
8
+ * hostname — so a rebind between check and connect cannot retarget the socket,
9
+ * and the certificate still validates against the name the caller asked for.
10
+ * Pooled egress reuses the selected tenant pool's CONNECT/SOCKS tunnel and
11
+ * accounts the tunnel's bytes to that pool, exactly as `fetch` does.
12
+ */
13
+ import { GatewayError } from "../../protocol/gateway-error";
14
+ import type { SsrfPolicy } from "../../config/network";
15
+ import { createValidatedFetch, type ValidatedFetch } from "./fetch";
16
+ import { isAddressAllowed, resolveAndValidateOnce, type ValidatedDestination } from "../ssrf";
17
+ import { tlsProfileOf, type TlsProfile } from "./tls-profile";
18
+ import { assertBidiTarget, dialDirect, dialThroughPool } from "./dial";
19
+ import { openBidiStream } from "./bidi-session";
20
+ import { connectValidatedWebSocket } from "./ws";
21
+ import type { ValidatedOutboundWebSocket, ValidatedOutboundHttp2Bidi } from "../../providers/provider-contracts";
22
+ import { PoolBindingError, type PoolAgent } from "../pool/agent";
23
+ import type { PoolAgentResolver } from "../pool/resolver";
24
+
25
+ export class ValidatedNetworkBindingFactory {
26
+ private readonly directFetch: ValidatedFetch;
27
+ /**
28
+ * One `ValidatedFetch` per resolved pool agent. Building the closure (dialer,
29
+ * relay setup, redirect loop) on every request through a pool was pure
30
+ * churn; keying by the agent object means a released agent is naturally
31
+ * collected (new builds mint a new agent) so no explicit invalidation is
32
+ * needed.
33
+ */
34
+ private readonly pooledFetchCache = new WeakMap<PoolAgent, ValidatedFetch>();
35
+ /** Default TLS ClientHello profile for direct https dials. */
36
+ private readonly tlsProfile: TlsProfile;
37
+
38
+ constructor(
39
+ private readonly policy: SsrfPolicy = {},
40
+ fetchFn?: typeof fetch,
41
+ private readonly poolResolver?: PoolAgentResolver,
42
+ tlsProfile: TlsProfile = "default",
43
+ ) {
44
+ this.directFetch = createValidatedFetch({
45
+ policy,
46
+ ...(fetchFn ? { fetchFn } : {}),
47
+ tlsProfile,
48
+ });
49
+ this.tlsProfile = tlsProfile;
50
+ }
51
+
52
+ /**
53
+ * One-shot DNS resolution + SSRF validation for direct egress targets:
54
+ * every resolved address (all families, not just the first answer) must
55
+ * pass the SSRF policy, and the validated destination pins the address so
56
+ * the subsequent socket cannot be re-pointed at a private/link-local host
57
+ * by a DNS rebind between check and connect.
58
+ */
59
+ resolve(hostname: string, signal: AbortSignal, port = 443): Promise<ValidatedDestination> {
60
+ return resolveAndValidateOnce(hostname, this.policy, signal, port);
61
+ }
62
+
63
+ /**
64
+ * Without `networkPoolId` (or when no pool resolver is configured), dispatch
65
+ * through validated direct egress. With one, route through that pool's
66
+ * validated agent. A pool configuration failure is returned to the caller;
67
+ * configured egress is never silently replaced with direct traffic.
68
+ */
69
+ fetch(networkPoolId?: string, tenantId?: string): ValidatedFetch {
70
+ if (!networkPoolId || !this.poolResolver || !tenantId) return this.directFetch;
71
+ const { poolResolver, policy } = this;
72
+ return async (input, init) => {
73
+ try {
74
+ const agent = await poolResolver.resolveAgent(networkPoolId, tenantId);
75
+ let validated = this.pooledFetchCache.get(agent);
76
+ if (validated === undefined) {
77
+ validated = createValidatedFetch({ policy, agent });
78
+ this.pooledFetchCache.set(agent, validated);
79
+ }
80
+ return await validated(input, init);
81
+ } catch (error) {
82
+ if (error instanceof PoolBindingError) {
83
+ throw new GatewayError(
84
+ "proxy_pool_unhealthy",
85
+ 503,
86
+ "Configured proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧",
87
+ { poolId: networkPoolId, reason: error.message.slice(0, 200) },
88
+ "network",
89
+ );
90
+ }
91
+ throw error;
92
+ }
93
+ };
94
+ }
95
+
96
+ webSocket(networkPoolId?: string, tenantId?: string): ValidatedOutboundWebSocket {
97
+ return async (url, headers, signal) => {
98
+ if (url.protocol !== "wss:") {
99
+ throw new GatewayError("invalid_request", 400, "validated WebSocket egress requires wss (request {requestId}). Check the shape of what you sent — then resend 🪞");
100
+ }
101
+ if (url.username || url.password || url.hash || (url.port !== "" && url.port !== "443")) {
102
+ throw new GatewayError("invalid_request", 400, "invalid validated WebSocket target (request {requestId}). Check the shape of what you sent — then resend 🪞");
103
+ }
104
+ const hostname = url.hostname;
105
+ if (!hostname) throw new GatewayError("invalid_request", 400, "WebSocket target hostname is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
106
+ const destination = await this.resolve(hostname, signal, 443);
107
+ if (!isAddressAllowed(destination.resolvedAddress, this.policy)) {
108
+ throw new GatewayError("invalid_request", 400, "unsafe upstream address rejected (request {requestId}). Check the shape of what you sent — then resend 🪞");
109
+ }
110
+ let agent: PoolAgent | undefined;
111
+ if (networkPoolId || tenantId) {
112
+ if (!networkPoolId || !tenantId || !this.poolResolver) {
113
+ throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧");
114
+ }
115
+ try {
116
+ agent = await this.poolResolver.resolveAgent(networkPoolId, tenantId);
117
+ } catch (error) {
118
+ if (error instanceof PoolBindingError) {
119
+ throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧");
120
+ }
121
+ throw error;
122
+ }
123
+ }
124
+ return connectValidatedWebSocket(url, headers, signal, destination, agent);
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Validated bidirectional HTTP/2 egress for one writable streaming request.
130
+ *
131
+ * Direct egress resolves and validates *every* DNS answer for the hostname,
132
+ * pins the chosen public address, and keeps TLS SNI pointed at the validated
133
+ * hostname — so a rebind between check and connect cannot retarget the socket,
134
+ * and the certificate still validates against the name the caller asked for.
135
+ * Pooled egress reuses the selected tenant pool's CONNECT/SOCKS tunnel and
136
+ * accounts the tunnel's bytes to that pool, exactly as `fetch` does.
137
+ *
138
+ * Credentials in the authority, fragments, non-443 ports, IP literals, and
139
+ * hostnames outside `allowHostname` are refused before anything is dialed: a
140
+ * bidi stream carries the account credential for its whole lifetime, so the
141
+ * target is checked once here rather than trusted to each caller.
142
+ */
143
+ http2Bidi(networkPoolId?: string, tenantId?: string): ValidatedOutboundHttp2Bidi {
144
+ return async (url, headers, options) => {
145
+ assertBidiTarget(url);
146
+ const { hostname } = url;
147
+ if (options?.allowHostname !== undefined && !options.allowHostname(hostname)) {
148
+ throw new GatewayError("invalid_request", 400, "HTTP/2 target host is not allowlisted (request {requestId}). All roads blocked — time to find a new route 🗺️");
149
+ }
150
+ const signal = options?.signal ?? AbortSignal.timeout(15_000);
151
+ const profile = tlsProfileOf(this.tlsProfile);
152
+ // Validation runs on every new connection, not once per capability: a
153
+ // long-lived Run is re-opened after a session dies, and a hostname that
154
+ // resolved safely the first time is not safe the twentieth.
155
+ const destination = await this.resolve(hostname, signal, 443);
156
+ if (!isAddressAllowed(destination.resolvedAddress, this.policy)) {
157
+ throw new GatewayError("invalid_request", 400, "unsafe upstream address rejected (request {requestId}). All roads blocked — time to find a new route 🗺️");
158
+ }
159
+ // Mirrors `fetch`: no pool selected (or none configured) means validated
160
+ // *direct* egress, which is already resolved and SSRF-checked above.
161
+ // Requiring a pool whenever a tenant id was supplied made bidi-only
162
+ // providers (Cursor) unreachable on every install without a pool, even
163
+ // though the equivalent `fetch` path dials direct there.
164
+ if (networkPoolId !== undefined || tenantId !== undefined) {
165
+ if (networkPoolId === undefined || tenantId === undefined || this.poolResolver === undefined) {
166
+ const socket = await dialDirect(destination, signal, profile);
167
+ return openBidiStream(socket, url, headers, options ?? {});
168
+ }
169
+ let agent: PoolAgent;
170
+ try {
171
+ agent = await this.poolResolver.resolveAgent(networkPoolId, tenantId);
172
+ } catch (error) {
173
+ if (error instanceof PoolBindingError) {
174
+ throw new GatewayError(
175
+ "proxy_pool_unhealthy",
176
+ 503,
177
+ "Configured proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧",
178
+ { poolId: networkPoolId, reason: error.message.slice(0, 200) },
179
+ "network",
180
+ );
181
+ }
182
+ throw error;
183
+ }
184
+ const socket = await dialThroughPool(agent, hostname, signal, profile);
185
+ return openBidiStream(socket, url, headers, options ?? {});
186
+ }
187
+ const socket = await dialDirect(destination, signal, profile);
188
+ return openBidiStream(socket, url, headers, options ?? {});
189
+ };
190
+ }
191
+ }
@@ -1,14 +1,77 @@
1
1
  // Validated fetch: SSRF-guarded outbound fetch with optional pool-agent support.
2
- import { GatewayError } from "../protocol/gateway-error";
2
+ import { GatewayError } from "../../protocol/gateway-error";
3
3
  import { request as httpRequest, Agent as HttpAgent } from "node:http";
4
4
  import { request as httpsRequest, Agent as HttpsAgent } from "node:https";
5
5
  import { Agent as UndiciAgent, fetch as undiciFetch, type UndiciRequestInit } from "undici";
6
6
  import { SocksProxyAgent } from "socks-proxy-agent";
7
- import { resolveAllAddresses, validateResolvedAddresses } from "./ssrf";
8
- import type { SsrfPolicy } from "../config/network";
9
- import { isProxyAgentPair, type ProxyAgentPair } from "./pool/agent";
10
- import { metrics } from "../observability/metrics";
11
- import { trackNetworkCall } from "../observability/performance-metrics";
7
+ import { resolveAllAddresses, validateResolvedAddresses } from "../ssrf";
8
+ import type { SsrfPolicy } from "../../config/network";
9
+ import { isProxyAgentPair, type ProxyAgentPair } from "../pool/agent";
10
+ import { metrics } from "../../observability/metrics";
11
+ import { trackNetworkCall } from "../../observability/performance-metrics";
12
+ import { nodeTlsOptions, tlsProfileOf, type TlsProfile, type TlsProfileConfig } from "./tls-profile";
13
+
14
+ /**
15
+ * Keep-alive socket lifetime for outbound direct dials.
16
+ *
17
+ * Some upstream edges (accelerated hosting, CDN front doors) drop idle or
18
+ * silently-errored keep-alive connections without sending a TCP RST. undici
19
+ * keeps those dead sockets in its pool — the default `keepAliveMaxTimeout`
20
+ * is 600s, and an upstream `Keep-Alive` header can push it even higher — so a
21
+ * retry reuses a poisoned socket and hangs until the connect timeout. Capping
22
+ * the TTL (both fields) keeps a dead socket from being reused; a fresh dial
23
+ * is also replayed once when a pooled socket dies before response headers.
24
+ */
25
+ export const DEFAULT_KEEPALIVE_TIMEOUT_MS = 4_000;
26
+ /** Fresh-socket retry agent: keep-alive effectively disabled (1 ms). */
27
+ const FRESH_SOCKET_KEEPALIVE_TIMEOUT_MS = 1;
28
+
29
+ /** Error codes that prove a pooled keep-alive socket died mid-flight. */
30
+ const STALE_SOCKET_ERROR_CODES: ReadonlySet<string> = new Set([
31
+ "UND_ERR_SOCKET",
32
+ "ECONNRESET",
33
+ "EPIPE",
34
+ "UND_ERR_HEADERS_TIMEOUT",
35
+ ]);
36
+
37
+ /** Error codes that prove the host itself is unreachable — never a stale socket. */
38
+ const NON_STALE_SOCKET_ERROR_CODES: ReadonlySet<string> = new Set([
39
+ "ENOTFOUND",
40
+ "EAI_AGAIN",
41
+ "ECONNREFUSED",
42
+ "EHOSTUNREACH",
43
+ "ENETUNREACH",
44
+ "UND_ERR_CONNECT_TIMEOUT",
45
+ "UND_ERR_INVALID_ARG",
46
+ ]);
47
+
48
+ /** Message fragments that prove the other side closed a pooled connection. */
49
+ const STALE_SOCKET_ERROR_SUBSTRINGS: readonly string[] = [
50
+ "other side closed",
51
+ "socket hang up",
52
+ ];
53
+
54
+ /** True when `error` proves a pooled keep-alive socket died before headers. */
55
+ export function isStaleSocketError(error: unknown): boolean {
56
+ if (!(error instanceof Error) || error.name === "AbortError") return false;
57
+ const cause = (error as { cause?: unknown }).cause;
58
+ const code =
59
+ (cause !== null && typeof cause === "object" && "code" in cause
60
+ ? (cause as { code?: unknown }).code
61
+ : undefined) ?? (error as { code?: unknown }).code;
62
+ if (typeof code === "string") {
63
+ if (NON_STALE_SOCKET_ERROR_CODES.has(code)) return false;
64
+ if (STALE_SOCKET_ERROR_CODES.has(code)) return true;
65
+ }
66
+ const message = `${error.message} ${cause instanceof Error ? cause.message : ""}`.toLowerCase();
67
+ return STALE_SOCKET_ERROR_SUBSTRINGS.some((fragment) => message.includes(fragment));
68
+ }
69
+
70
+ /** True when the request body can be safely replayed (not a locked stream). */
71
+ function isBodyReplaySafe(body: BodyInit | null | undefined): boolean {
72
+ if (body === null || body === undefined) return true;
73
+ return !(typeof body === "object" && "locked" in body && body.locked === true);
74
+ }
12
75
  // Validated fetch — single egress with ProxyAgent support
13
76
  interface ValidatedFetchOptions {
14
77
  readonly fetchFn?: typeof fetch;
@@ -27,6 +90,18 @@ interface ValidatedFetchOptions {
27
90
  * Defaults to undici's fetch; injectable for offline tests.
28
91
  */
29
92
  readonly socksFetch?: (input: RequestInfo | URL, init?: UndiciRequestInit) => Promise<Response>;
93
+ /**
94
+ * TLS ClientHello profile for direct https dials. `"default"` (or absent)
95
+ * keeps the runtime's own TLS stack; a named profile (see `tls-profile.ts`)
96
+ * dials through a Node https agent configured with that profile's OpenSSL
97
+ * options, so the wire fingerprint matches the impersonated client.
98
+ */
99
+ readonly tlsProfile?: TlsProfile;
100
+ /**
101
+ * CA bundle for profile dials (`https.Agent({ ca })`). Defaults to the
102
+ * system store; set it to trust a private/custom CA.
103
+ */
104
+ readonly ca?: string | Buffer | Array<string | Buffer>;
30
105
  }
31
106
  export type ValidatedFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
32
107
 
@@ -91,7 +166,7 @@ function socksDispatcherFor(agent: SocksProxyAgent): UndiciAgent {
91
166
  let dispatcher = socksDispatcherCache.get(agent);
92
167
  if (!dispatcher) {
93
168
  dispatcher = new UndiciAgent({
94
- connect: async (opts) => {
169
+ connect: async (opts: Record<string, unknown>) => {
95
170
  const o = opts as {
96
171
  hostname?: string;
97
172
  port?: number | string;
@@ -108,14 +183,63 @@ function socksDispatcherFor(agent: SocksProxyAgent): UndiciAgent {
108
183
  } as never,
109
184
  );
110
185
  },
111
- });
186
+ // Cap the pooled socket lifetime so a silently-dropped keep-alive
187
+ // connection is not reused by a later request.
188
+ keepAliveTimeout: DEFAULT_KEEPALIVE_TIMEOUT_MS,
189
+ keepAliveMaxTimeout: DEFAULT_KEEPALIVE_TIMEOUT_MS,
190
+ } as unknown as ConstructorParameters<typeof UndiciAgent>[0]);
112
191
  socksDispatcherCache.set(agent, dispatcher);
113
192
  }
114
193
  return dispatcher;
115
194
  }
116
195
 
117
196
  /**
118
- * One dial, always through `fetch`.
197
+ * A Node `https` agent whose connections are dialed with a chosen TLS profile.
198
+ *
199
+ * The agent's `createConnection` is the only knob that reaches the ClientHello
200
+ * through OpenSSL: it lets `tls.connect` run with the profile's cipher list,
201
+ * TLS 1.3 suite order, ALPN, and version bounds. Used only when a profile is
202
+ * requested; the runtime-native fetch stays the default for everything else.
203
+ */
204
+ export function createProfileAgent(profile: TlsProfileConfig, ca?: string | Buffer | Array<string | Buffer>): HttpsAgent {
205
+ // Straight option passthrough — no custom `createConnection`. Bun's
206
+ // Node-compat layer ignores a user-supplied `createConnection` on the agent
207
+ // (verified live 2026-11-11: the dial failed with ERR_SOCKET_CLOSED and no
208
+ // ClientHello reached the sniffer), but it honors the standard TLS options
209
+ // on the agent itself — `ALPNProtocols` switched the ALPN extension and
210
+ // `ciphers` narrowed the offered suite list in capture. The request path
211
+ // stays the plain https client with the profile's fingerprint.
212
+ return new HttpsAgent({
213
+ ...nodeTlsOptions(profile),
214
+ ...(ca === undefined ? {} : { ca }),
215
+ } as unknown as HttpsAgent);
216
+ }
217
+
218
+ /**
219
+ * A shared undici dispatcher whose keep-alive socket lifetime is capped.
220
+ *
221
+ * Direct dials go through the runtime's own fetch; when a socks dispatcher is
222
+ * in play (pool egress) the same TTL cap applies. A fresh-socket dispatcher
223
+ * (`keepAliveTimeout: 1ms`) backs the stale-socket replay so a retry never
224
+ * reuses the poisoned connection.
225
+ */
226
+ let directDispatcherCache: { agent: UndiciAgent; fresh: UndiciAgent } | undefined;
227
+ function directDispatchers(): { agent: UndiciAgent; fresh: UndiciAgent } {
228
+ directDispatcherCache ??= {
229
+ agent: new UndiciAgent({
230
+ keepAliveTimeout: DEFAULT_KEEPALIVE_TIMEOUT_MS,
231
+ keepAliveMaxTimeout: DEFAULT_KEEPALIVE_TIMEOUT_MS,
232
+ } as unknown as ConstructorParameters<typeof UndiciAgent>[0]),
233
+ fresh: new UndiciAgent({
234
+ keepAliveTimeout: FRESH_SOCKET_KEEPALIVE_TIMEOUT_MS,
235
+ keepAliveMaxTimeout: FRESH_SOCKET_KEEPALIVE_TIMEOUT_MS,
236
+ } as unknown as ConstructorParameters<typeof UndiciAgent>[0]),
237
+ };
238
+ return directDispatcherCache;
239
+ }
240
+
241
+ /**
242
+ * Dial one request, always through `fetch`.
119
243
  *
120
244
  * The runtime's own fetch (Bun/Undici) is the transport the clients this
121
245
  * gateway impersonates actually use, and several upstreams meter per client
@@ -141,6 +265,7 @@ async function dial(
141
265
  proxyUrl: string | undefined,
142
266
  dispatcher?: UndiciAgent,
143
267
  socksFetch: (input: RequestInfo | URL, init?: UndiciRequestInit) => Promise<Response> = undiciFetch,
268
+ freshSocket = false,
144
269
  ): Promise<Response> {
145
270
  const headers = new Headers(init.headers);
146
271
  const rawBody = init.body;
@@ -157,13 +282,16 @@ async function dial(
157
282
  return await socksFetch(url, {
158
283
  ...init,
159
284
  headers,
160
- dispatcher,
285
+ dispatcher: freshSocket ? directDispatchers().fresh : dispatcher,
161
286
  });
162
287
  }
163
288
  return await fetchFn(url, {
164
289
  ...init,
165
290
  headers,
166
291
  ...(proxyUrl === undefined ? {} : { proxy: proxyUrl }),
292
+ // Fresh-socket replay uses the capped undici dispatcher directly so a
293
+ // retry never reuses the pooled connection that just died.
294
+ ...(freshSocket ? { dispatcher: directDispatchers().fresh } : {}),
167
295
  });
168
296
  }
169
297
 
@@ -177,7 +305,7 @@ async function dial(
177
305
  * after the SSRF resolve/validate pass and only when no pool/relay agent owns
178
306
  * the dial, so the security boundary is unchanged.
179
307
  */
180
- function nodeDial(url: URL, init: RequestInit): Promise<Response> {
308
+ function nodeDial(url: URL, init: RequestInit, agent?: HttpsAgent): Promise<Response> {
181
309
  return new Promise((resolve, reject) => {
182
310
  const mod = url.protocol === "http:" ? httpRequest : httpsRequest;
183
311
  const headers: Record<string, string> = {};
@@ -195,8 +323,10 @@ function nodeDial(url: URL, init: RequestInit): Promise<Response> {
195
323
  payload = body;
196
324
  } else if (body instanceof Uint8Array) {
197
325
  payload = Buffer.from(body);
326
+ } else if (body instanceof ArrayBuffer) {
327
+ payload = Buffer.from(new Uint8Array(body));
198
328
  } else if (body !== null && body !== undefined) {
199
- reject(new TypeError("nodeDial only supports string/Buffer/Uint8Array bodies"));
329
+ reject(new TypeError("nodeDial only supports string/Buffer/Uint8Array/ArrayBuffer bodies"));
200
330
  return;
201
331
  }
202
332
  if (payload !== undefined) headers["content-length"] = String(payload.byteLength);
@@ -205,20 +335,30 @@ function nodeDial(url: URL, init: RequestInit): Promise<Response> {
205
335
  {
206
336
  method: (init.method ?? "GET").toUpperCase(),
207
337
  headers,
338
+ ...(agent === undefined ? {} : { agent }),
208
339
  ...(init.signal === undefined ? {} : { signal: init.signal as AbortSignal }),
209
340
  },
210
341
  (res) => {
211
- const chunks: Buffer[] = [];
212
- res.on("data", (c: Buffer) => chunks.push(c));
213
- res.on("end", () => {
214
- resolve(
215
- new Response(Buffer.concat(chunks), {
216
- status: res.statusCode ?? 200,
217
- ...(res.statusMessage === undefined ? {} : { statusText: res.statusMessage }),
218
- headers: res.headers as unknown as HeadersInit,
219
- }),
220
- );
342
+ // The response body is bridged into a web stream so callers can
343
+ // consume it incrementally (SSE chat streams) instead of buffering
344
+ // the entire payload before resolving.
345
+ const bodyStream = new ReadableStream<Uint8Array>({
346
+ start(controller) {
347
+ res.on("data", (c: Buffer) => controller.enqueue(new Uint8Array(c)));
348
+ res.on("end", () => controller.close());
349
+ res.on("error", (err: Error) => controller.error(err));
350
+ },
351
+ cancel() {
352
+ res.destroy();
353
+ },
221
354
  });
355
+ resolve(
356
+ new Response(bodyStream, {
357
+ status: res.statusCode ?? 200,
358
+ ...(res.statusMessage === undefined ? {} : { statusText: res.statusMessage }),
359
+ headers: res.headers as unknown as HeadersInit,
360
+ }),
361
+ );
222
362
  },
223
363
  );
224
364
  req.on("error", reject);
@@ -304,15 +444,41 @@ export function createValidatedFetch(options: ValidatedFetchOptions = {}): Valid
304
444
  } else {
305
445
  const method = (requestInit.method ?? "GET").toUpperCase();
306
446
  const hasBody = requestInit.body !== undefined && requestInit.body !== null;
307
- // The runtime fetch cannot carry a body on GET/HEAD, but Qoder's
308
- // model-list endpoint requires a signed GET body. When no pool/relay
309
- // owns the dial and no injected test fetch is present, dial through the
310
- // Node http(s) client (post SSRF validation). Every other case keeps
311
- // the existing fetch path.
312
- if ((method === "GET" || method === "HEAD") && hasBody && !socksAgent && options.fetchFn === undefined) {
447
+ const profile = tlsProfileOf(options.tlsProfile ?? "default");
448
+ // A TLS profile request is a *custom ClientHello* dial: it goes
449
+ // through a Node https agent configured with the profile's OpenSSL
450
+ // options, so the wire fingerprint matches the impersonated client.
451
+ // This path is direct-only (never a pool/relay dial) and rejects
452
+ // bodies on GET/HEAD like the runtime fetch does — that shape is
453
+ // still handled by `nodeDial` below.
454
+ if (profile !== undefined && !socksAgent && configuredAgent === undefined) {
455
+ response = await nodeDial(url, requestInit, createProfileAgent(profile, options.ca));
456
+ } else if ((method === "GET" || method === "HEAD") && hasBody && !socksAgent && options.fetchFn === undefined) {
457
+ // The runtime fetch cannot carry a body on GET/HEAD, but Qoder's
458
+ // model-list endpoint requires a signed GET body. When no pool/relay
459
+ // owns the dial and no injected test fetch is present, dial through the
460
+ // Node http(s) client (post SSRF validation). Every other case keeps
461
+ // the existing fetch path.
313
462
  response = await nodeDial(url, requestInit);
314
463
  } else {
315
- response = await dial(url, requestInit, fetchFn, proxyUrl, socksAgent ? socksDispatcherFor(socksAgent) : undefined, socksFetch);
464
+ try {
465
+ response = await dial(url, requestInit, fetchFn, proxyUrl, socksAgent ? socksDispatcherFor(socksAgent) : undefined, socksFetch);
466
+ } catch (error) {
467
+ // A pooled keep-alive socket that died before response headers is
468
+ // replayed once on a fresh socket. Only errors that *prove* the
469
+ // socket died qualify; a bare fetch failure (or an abort, or a
470
+ // locked body) is never replayed — the upstream may already hold
471
+ // that request and a replay would deliver it twice.
472
+ if (
473
+ !requestInit.signal?.aborted &&
474
+ isBodyReplaySafe(requestInit.body) &&
475
+ isStaleSocketError(error)
476
+ ) {
477
+ response = await dial(url, requestInit, fetchFn, proxyUrl, socksAgent ? socksDispatcherFor(socksAgent) : undefined, socksFetch, true);
478
+ } else {
479
+ throw error;
480
+ }
481
+ }
316
482
  }
317
483
  }
318
484
  if (response.status < 300 || response.status >= 400) {
@@ -0,0 +1,123 @@
1
+ /**
2
+ * TLS client-profile registry — "ClientHello/custom" egress support.
3
+ *
4
+ * The runtime's own TLS (Bun/BoringSSL) has a fixed ClientHello shape, which
5
+ * is fine when the client we impersonate is itself Bun — Qoder's desktop
6
+ * worker runs on Bun and its captured ClientHello (2026-11-11, `api2.qoder.sh`
7
+ * hijack) matches Bun's default byte-for-byte. But when an upstream meters per
8
+ * transport fingerprint, the egress layer must be able to dial with a *chosen*
9
+ * TLS profile instead of the runtime default.
10
+ *
11
+ * Node's `tls`/`https` stack (OpenSSL) honors `ciphers`, `ciphersuites`,
12
+ * `ALPNProtocols`, and `minVersion`/`maxVersion`, and Bun's Node-compat layer
13
+ * forwards those options into its own handshake — verified live (2026-11-11):
14
+ * an `https.Agent({ ALPNProtocols: ["h2"] })` changes the ALPN extension, and
15
+ * `ciphers` changes the offered cipher list. So a profile is real: it is the
16
+ * set of OpenSSL options that reproduce a target client's ClientHello.
17
+ *
18
+ * Profiles are pure data + one helper. They never open sockets; the dial
19
+ * paths (`outbound-fetch.ts`, `pool/resolver.ts`) apply them.
20
+ */
21
+
22
+ /**
23
+ * One re-playable TLS fingerprint.
24
+ *
25
+ * `ciphers` names the TLS 1.2 suite list in OpenSSL order (the order the
26
+ * ClientHello advertises); `ciphersuites` names the TLS 1.3 suites in order.
27
+ * `alpn` is the ALPN list in preference order. When a field is absent the
28
+ * dial path leaves the runtime default in place for that knob.
29
+ */
30
+ export interface TlsProfileConfig {
31
+ /** Stable id used to look the profile up (e.g. `"qoder-app"`). */
32
+ readonly id: string;
33
+ /** Human description; not sent on the wire. */
34
+ readonly label: string;
35
+ /** OpenSSL TLS 1.2 cipher list, client order. */
36
+ readonly ciphers?: string;
37
+ /** OpenSSL TLS 1.3 cipher-suites list, client order. */
38
+ readonly ciphersuites?: string;
39
+ /** ALPN protocols in preference order. */
40
+ readonly alpn?: readonly string[];
41
+ /** OpenSSL/Node TLS version floor (`"TLSv1.2"` / `"TLSv1.3"`). */
42
+ readonly minVersion?: string;
43
+ /** OpenSSL/Node TLS version ceiling (`"TLSv1.3"`). */
44
+ readonly maxVersion?: string;
45
+ }
46
+
47
+ /** Profile selector accepted by egress options. `"default"` = runtime default. */
48
+ export type TlsProfile = "default" | string;
49
+
50
+ /**
51
+ * The Qoder Desktop 0.5.7 chat fingerprint, from the live ClientHello capture
52
+ * (`documentation/research/qoder/2026-11-11-qoder-desktop-0.5.7-chat-surface-evidence.md`
53
+ * §9, `hijack/conn.log`): 18 suites in this exact order, ALPN `http/1.1`
54
+ * only, TLS 1.2–1.3. This is the profile to dial with when an upstream expects
55
+ * the Qoder desktop client's transport identity.
56
+ */
57
+ export const QODER_APP_TLS_PROFILE: TlsProfileConfig = {
58
+ id: "qoder-app",
59
+ label: "Qoder Desktop 0.5.7 (Bun/1.4.x worker) COSY chat",
60
+ // TLS 1.3 suites, client order (capture: 1301 1302 1303).
61
+ ciphersuites:
62
+ "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256",
63
+ // TLS 1.2 suites, client order (capture: c02f c02b c030 c02c c027 cca9
64
+ // cca8 c009 c013 c00a c014 009c 009d 002f 0035).
65
+ ciphers:
66
+ "ECDHE-RSA-AES128-GCM-SHA256:" +
67
+ "ECDHE-ECDSA-AES128-GCM-SHA256:" +
68
+ "ECDHE-RSA-AES256-GCM-SHA384:" +
69
+ "ECDHE-ECDSA-AES256-GCM-SHA384:" +
70
+ "ECDHE-RSA-AES128-CBC-SHA256:" +
71
+ "ECDHE-ECDSA-CHACHA20-POLY1305:" +
72
+ "ECDHE-RSA-CHACHA20-POLY1305:" +
73
+ "ECDHE-ECDSA-AES128-CBC-SHA:" +
74
+ "ECDHE-RSA-AES128-CBC-SHA:" +
75
+ "ECDHE-ECDSA-AES256-CBC-SHA:" +
76
+ "ECDHE-RSA-AES256-CBC-SHA:" +
77
+ "AES128-GCM-SHA256:" +
78
+ "AES256-GCM-SHA384:" +
79
+ "AES128-CBC-SHA:" +
80
+ "AES256-CBC-SHA",
81
+ alpn: ["http/1.1"],
82
+ minVersion: "TLSv1.2",
83
+ maxVersion: "TLSv1.3",
84
+ };
85
+
86
+ /** Built-in profiles by id. */
87
+ export const TLS_PROFILES: Readonly<Record<string, TlsProfileConfig>> = {
88
+ [QODER_APP_TLS_PROFILE.id]: QODER_APP_TLS_PROFILE,
89
+ };
90
+
91
+ /** Resolves a selector to a concrete profile; `"default"`/unknown → `undefined`. */
92
+ export function tlsProfileOf(profile: TlsProfile): TlsProfileConfig | undefined {
93
+ if (profile === undefined || profile === "default") return undefined;
94
+ return TLS_PROFILES[profile];
95
+ }
96
+
97
+ export interface NodeTlsOptions {
98
+ ciphers?: string;
99
+ ciphersuites?: string;
100
+ ALPNProtocols?: readonly string[];
101
+ minVersion?: string;
102
+ maxVersion?: string;
103
+ }
104
+
105
+ /**
106
+ * Maps a profile to the `tls`/`https` option set. `alpn` is included by
107
+ * default; pass `includeAlpn: false` for transports that fix their own ALPN
108
+ * (HTTP/2 bidi always negotiates `h2`).
109
+ */
110
+ export function nodeTlsOptions(
111
+ profile: TlsProfileConfig,
112
+ includeAlpn = true,
113
+ ): NodeTlsOptions {
114
+ const out: NodeTlsOptions = {};
115
+ if (profile.ciphers !== undefined) out.ciphers = profile.ciphers;
116
+ if (profile.ciphersuites !== undefined) out.ciphersuites = profile.ciphersuites;
117
+ if (includeAlpn && profile.alpn !== undefined && profile.alpn.length > 0) {
118
+ out.ALPNProtocols = [...profile.alpn];
119
+ }
120
+ if (profile.minVersion !== undefined) out.minVersion = profile.minVersion;
121
+ if (profile.maxVersion !== undefined) out.maxVersion = profile.maxVersion;
122
+ return out;
123
+ }