cartethyia 2.4.5 → 2.5.0

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 (415) hide show
  1. package/.env.example +4 -1
  2. package/README.md +63 -13
  3. package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
  4. package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
  5. package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
  7. package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
  8. package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-D3xqZbN-.js} +1 -1
  9. package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
  10. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
  11. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js +1 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
  14. package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-CTTpCb25.js} +1 -1
  15. package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js +1 -0
  17. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-DHoB3Agn.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
  20. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +1 -0
  21. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  22. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
  23. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js.gz +0 -0
  26. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
  27. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
  31. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-CvjZxHE4.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
  36. package/dist/dashboard/assets/SetupPage-D3nza7LH.js +1 -0
  37. package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
  38. package/dist/dashboard/assets/{SortableList-Bol1ASmI.js → SortableList-Dtt12WT1.js} +1 -1
  39. package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
  40. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js +31 -0
  41. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
  42. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  43. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
  44. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
  45. package/dist/dashboard/assets/api-keys-BmDbsior.js +1 -0
  46. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  47. package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
  48. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  49. package/dist/dashboard/assets/{app-BDJWMThu.js → app-C-Y4oZT0.js} +3 -3
  50. package/dist/dashboard/assets/app-C-Y4oZT0.js.gz +0 -0
  51. package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
  52. package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
  53. package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
  54. package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
  55. package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
  56. package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-DpXT4Ixt.js} +1 -1
  57. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  58. package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-CSAAWLJv.js} +1 -1
  59. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  60. package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
  61. package/dist/dashboard/assets/{card-BTdbRFFg.js → card-DEhRXXw4.js} +1 -1
  62. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  63. package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
  64. package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
  66. package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
  67. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js +1 -0
  68. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  69. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +1 -0
  70. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  71. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  72. package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
  73. package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BvQeBMfP.js} +1 -1
  74. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  75. package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
  76. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  77. package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
  78. package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-CMwPXwQB.js} +1 -1
  79. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  80. package/dist/dashboard/assets/entry-D4O2229X.js +1 -0
  81. package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
  82. package/dist/dashboard/assets/entry-DKvr5Q9K.js +1 -0
  83. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  84. package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
  85. package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
  86. package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
  87. package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
  88. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  89. package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
  90. package/dist/dashboard/assets/github-badge-zliLvBwr.js +1 -0
  91. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  92. package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
  93. package/dist/dashboard/assets/{index-C4hXfAMs.js → index-CXm3FIyt.js} +2 -2
  94. package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
  95. package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1IlZZ8H.js} +1 -1
  96. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  97. package/dist/dashboard/assets/{input-Bn4_kNav.js → input-BpVZdrbc.js} +1 -1
  98. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  99. package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
  100. package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
  101. package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
  102. package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BioLW7gN.js} +1 -1
  103. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  104. package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-CwVcGAIc.js} +1 -1
  105. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  106. package/dist/dashboard/assets/{live-YlifPYox.js → live-yfEJFF9T.js} +1 -1
  107. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  108. package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
  109. package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
  110. package/dist/dashboard/assets/model-bans-CT-TxSNX.js +1 -0
  111. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  112. package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
  113. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  114. package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
  115. package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
  116. package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
  117. package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
  118. package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
  119. package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
  120. package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
  121. package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
  122. package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
  123. package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
  124. package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
  125. package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
  126. package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
  127. package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
  128. package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
  129. package/dist/dashboard/assets/providers/cursor.webp +0 -0
  130. package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
  131. package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
  132. package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
  133. package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
  134. package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
  135. package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
  136. package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
  137. package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
  138. package/dist/dashboard/assets/providers/trae.webp +0 -0
  139. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  140. package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
  141. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  142. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  143. package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
  144. package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
  145. package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
  146. package/dist/dashboard/assets/routing-Cs6jl49L.js +1 -0
  147. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  148. package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
  149. package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
  150. package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
  151. package/dist/dashboard/assets/select-C0Rer3gF.js.gz +0 -0
  152. package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
  153. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  154. package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
  155. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  156. package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-C9_uUjaA.js} +1 -1
  157. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  158. package/dist/dashboard/assets/{state-DHKdg-pm.js → state-DA0B95UQ.js} +1 -1
  159. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  160. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  161. package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-Cittxy-x.js} +1 -1
  162. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  163. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
  164. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  165. package/dist/dashboard/assets/toast-CNt_jzO4.js +2 -0
  166. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  167. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  168. package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
  169. package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
  170. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  171. package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
  172. package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
  173. package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-BQKMcJ1H.js} +1 -1
  174. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
  175. package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
  176. package/dist/dashboard/index.html +1 -1
  177. package/dist/dashboard/index.html.gz +0 -0
  178. package/dist/dashboard/precompressed-assets.json +122 -0
  179. package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
  180. package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
  181. package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
  182. package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
  183. package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
  184. package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
  185. package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
  186. package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
  187. package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
  188. package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
  189. package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
  190. package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
  191. package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
  192. package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
  193. package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
  194. package/package.json +26 -30
  195. package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
  196. package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
  197. package/scripts/cli/command/admin.ts +65 -0
  198. package/scripts/cli/command/console-menu.ts +246 -0
  199. package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
  200. package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
  201. package/scripts/cli/command/start.ts +77 -0
  202. package/scripts/cli/command/tray.ts +165 -0
  203. package/scripts/cli/command/update.ts +23 -0
  204. package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
  205. package/scripts/cli/gateway/instance-lock.ts +105 -0
  206. package/scripts/cli/gateway/spawn.ts +173 -0
  207. package/scripts/cli/gateway/stop.ts +51 -0
  208. package/scripts/cli/main.ts +94 -0
  209. package/scripts/cli/support/bun.ts +27 -0
  210. package/scripts/cli/support/env.ts +99 -0
  211. package/scripts/cli/support/platform.ts +64 -0
  212. package/scripts/cli/support/services.ts +88 -0
  213. package/scripts/cli/support/startup-signal.ts +23 -0
  214. package/scripts/cli/{version.ts → support/version.ts} +1 -1
  215. package/scripts/cli/tray/hidden-window.vbs +28 -0
  216. package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
  217. package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
  218. package/scripts/cli/ui/background.ts +110 -0
  219. package/src/boot/app.ts +23 -7
  220. package/src/boot/dependencies.ts +55 -13
  221. package/src/boot/elysia-typebox-bridge.ts +37 -0
  222. package/src/boot/lifecycle.ts +2 -0
  223. package/src/config/cooldowns.ts +138 -138
  224. package/src/config/providers.ts +31 -5
  225. package/src/config/runtime.ts +19 -3
  226. package/src/config/spec.ts +20 -0
  227. package/src/console/api-keys/contracts.ts +414 -391
  228. package/src/console/api-keys/routes.ts +880 -870
  229. package/src/console/auth/account-reset-store.ts +14 -0
  230. package/src/console/auth/account-reset.ts +3 -2
  231. package/src/console/auth/setup-service.ts +6 -3
  232. package/src/console/backup/routes.ts +8 -0
  233. package/src/console/cli-tools/contracts.ts +6 -6
  234. package/src/console/console-router.ts +5 -3
  235. package/src/console/dashboard-assets.ts +81 -11
  236. package/src/console/domain-registration.ts +9 -2
  237. package/src/console/observability/contracts.ts +475 -423
  238. package/src/console/observability/routes.ts +31 -6
  239. package/src/console/observability/store.ts +1015 -912
  240. package/src/console/providers/catalog/contracts.ts +31 -0
  241. package/src/console/providers/catalog/provider-operations.ts +8 -0
  242. package/src/console/providers/catalog/public-model-store.ts +91 -60
  243. package/src/console/providers/catalog/store.ts +1532 -1521
  244. package/src/console/providers/oauth/callback-listener.ts +18 -3
  245. package/src/console/providers/oauth/routes.ts +38 -15
  246. package/src/console/quota/account-quota-tenant.ts +9 -6
  247. package/src/console/quota/refresh.ts +19 -0
  248. package/src/console/response-compression.ts +69 -2
  249. package/src/console/routing/pools/probe-verdict.ts +72 -0
  250. package/src/console/share/share-router.ts +7 -4
  251. package/src/console/share/share-stats.ts +12 -5
  252. package/src/console/shared/read-through-cache.ts +133 -7
  253. package/src/console/updates/repository-updates.ts +39 -34
  254. package/src/console/updates/routes.ts +162 -4
  255. package/src/main.ts +3 -0
  256. package/src/network/http2-bidi.ts +85 -0
  257. package/src/network/outbound-fetch.ts +87 -24
  258. package/src/network/pool/agent.ts +478 -460
  259. package/src/network/pool/resolver.ts +1104 -544
  260. package/src/network/pool-health-machine.ts +12 -5
  261. package/src/observability/event-log.ts +29 -8
  262. package/src/observability/metrics.ts +40 -13
  263. package/src/observability/payload-capture.ts +344 -324
  264. package/src/observability/runtime-metrics.ts +7 -0
  265. package/src/persistence/db-pg.ts +9 -3
  266. package/src/persistence/db-sqlite.ts +497 -495
  267. package/src/persistence/health-event-flush.ts +137 -0
  268. package/src/persistence/migrate.ts +181 -129
  269. package/src/persistence/schema.ts +1360 -1257
  270. package/src/persistence/settings-revision.ts +25 -0
  271. package/src/persistence/shared-cache.ts +217 -0
  272. package/src/persistence/stores/share-store.ts +556 -528
  273. package/src/persistence/usage-flush.ts +61 -1
  274. package/src/protocol/canonical-model.ts +23 -1
  275. package/src/protocol/response/chat.ts +143 -10
  276. package/src/protocol/response/codex.ts +9 -75
  277. package/src/protocol/response/omp-tool-call-markup.ts +221 -0
  278. package/src/protocol/tool-identity.ts +43 -9
  279. package/src/providers/authentication/jwt-validator.ts +130 -187
  280. package/src/providers/authentication/oauth-flow-store.ts +16 -9
  281. package/src/providers/default-registry.ts +177 -1
  282. package/src/providers/discovery/catalog/catalog-format.ts +20 -0
  283. package/src/providers/discovery/models-dev-catalog.ts +39 -22
  284. package/src/providers/discovery/openai-model-discovery.ts +39 -3
  285. package/src/providers/discovery/openrouter-free-roster.ts +98 -0
  286. package/src/providers/discovery/probe-wire.ts +29 -5
  287. package/src/providers/discovery/probing-service.ts +1022 -1001
  288. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +42 -2
  289. package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
  290. package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
  291. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +22 -5
  292. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
  293. package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
  294. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
  295. package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
  296. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
  297. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
  298. package/src/providers/integrations/buddy/codebuddy/shared.ts +115 -73
  299. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
  300. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
  301. package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
  302. package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
  303. package/src/providers/integrations/claude/claude.ts +606 -582
  304. package/src/providers/integrations/cline/cline-quota.ts +18 -16
  305. package/src/providers/integrations/cline/cline.ts +539 -468
  306. package/src/providers/integrations/codex/codex.ts +5 -8
  307. package/src/providers/integrations/commandcode/commandcode.ts +675 -541
  308. package/src/providers/integrations/commandcode/discovery.ts +116 -0
  309. package/src/providers/integrations/commandcode/quota.ts +160 -0
  310. package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
  311. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
  312. package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
  313. package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
  314. package/src/providers/integrations/cursor/cursor-request.ts +471 -0
  315. package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
  316. package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
  317. package/src/providers/integrations/cursor/cursor.ts +960 -0
  318. package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
  319. package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
  320. package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
  321. package/src/providers/integrations/factory/factory-oauth.ts +7 -11
  322. package/src/providers/integrations/github/github.ts +4 -4
  323. package/src/providers/integrations/hermes.ts +79 -8
  324. package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
  325. package/src/providers/integrations/kilo/kilo.ts +153 -146
  326. package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
  327. package/src/providers/integrations/opencode/provider.ts +532 -499
  328. package/src/providers/integrations/openrouter/provider.ts +139 -281
  329. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
  330. package/src/providers/integrations/qoder/qoder-oauth.ts +310 -263
  331. package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
  332. package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
  333. package/src/providers/integrations/qoder/qoder.ts +865 -730
  334. package/src/providers/integrations/shared/install-id.ts +29 -2
  335. package/src/providers/integrations/trae/trae-catalog.ts +190 -0
  336. package/src/providers/integrations/trae/trae-headers.ts +149 -0
  337. package/src/providers/integrations/trae/trae-oauth.ts +773 -0
  338. package/src/providers/integrations/trae/trae-protocol.ts +400 -0
  339. package/src/providers/integrations/trae/trae-realm.ts +220 -0
  340. package/src/providers/integrations/trae/trae.ts +458 -0
  341. package/src/providers/operations/account-health-service.ts +107 -10
  342. package/src/providers/operations/client-versions.ts +143 -6
  343. package/src/providers/operations/model-catalog-cache.ts +18 -3
  344. package/src/providers/operations/provider-catalog-seeder.ts +95 -86
  345. package/src/providers/operations/provider-catalog-service.ts +76 -0
  346. package/src/providers/operations/provider-credential-service.ts +46 -7
  347. package/src/providers/provider-contracts.ts +76 -0
  348. package/src/providers/provider-metadata.ts +78 -6
  349. package/src/providers/provider-model-definition.ts +373 -349
  350. package/src/security/api-key-auth.ts +126 -8
  351. package/src/transport/contracts.ts +3 -0
  352. package/src/transport/dispatch/attempt-finalize.ts +39 -2
  353. package/src/transport/dispatch/attempt-loop.ts +530 -513
  354. package/src/transport/dispatch/empty-stream.ts +17 -26
  355. package/src/transport/dispatch/generation.ts +88 -21
  356. package/src/transport/dispatch/leases.ts +99 -2
  357. package/src/transport/dispatch/proxy-request.ts +60 -3
  358. package/src/transport/dispatch/streaming/finalize.ts +16 -1
  359. package/src/transport/dispatch/streaming/stream.ts +22 -2
  360. package/src/transport/dispatch/upstream.ts +19 -1
  361. package/src/transport/failure-policy.ts +20 -0
  362. package/src/transport/guards/auth-guard.ts +26 -1
  363. package/src/transport/guards/body-policy.ts +5 -4
  364. package/src/transport/request/preparer.ts +103 -0
  365. package/src/transport/request/state.ts +14 -0
  366. package/src/transport/routing/plan-gate.ts +45 -0
  367. package/src/transport/routing/route-catalog.ts +799 -738
  368. package/src/transport/routing/route-model.ts +52 -1
  369. package/src/transport/routing/router.ts +51 -0
  370. package/src/transport/surface/chat/encode.ts +18 -31
  371. package/src/transport/surface/messages/stream.ts +3 -2
  372. package/src/transport/translation/tool-repair.ts +37 -16
  373. package/src/workers/account-identity-sweep.ts +14 -12
  374. package/src/workers/daily-checkin.ts +62 -0
  375. package/src/workers/quota-refresh-sweep.ts +27 -2
  376. package/dist/dashboard/assets/BannedPage-DUrXpp-X.js +0 -1
  377. package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
  378. package/dist/dashboard/assets/ConsoleLogPage-AisQzCI1.js +0 -1
  379. package/dist/dashboard/assets/HealthEventsModal-BNflDm_f.js +0 -1
  380. package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
  381. package/dist/dashboard/assets/OverviewPage-C3NIzsDx.js +0 -1
  382. package/dist/dashboard/assets/ProviderDetailPage-BK8i5B5X.js +0 -3
  383. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
  384. package/dist/dashboard/assets/ProvidersPage-CvCVgm1e.js +0 -1
  385. package/dist/dashboard/assets/ProxyPage-DjabkuzP.js +0 -7
  386. package/dist/dashboard/assets/QuotaPage-Dxe0B0iu.js +0 -1
  387. package/dist/dashboard/assets/SetupPage-BaD4P_jW.js +0 -1
  388. package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
  389. package/dist/dashboard/assets/UsagePage-Djes-Epy.js +0 -56
  390. package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
  391. package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
  392. package/dist/dashboard/assets/assert-Bwh6DJTK.js +0 -1
  393. package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
  394. package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
  395. package/dist/dashboard/assets/entry-Cpi0rph1.js +0 -4
  396. package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
  397. package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
  398. package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
  399. package/dist/dashboard/assets/model-bans-CVvHZ-qz.js +0 -1
  400. package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
  401. package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
  402. package/dist/dashboard/assets/routing-jntw9-Ui.js +0 -1
  403. package/dist/dashboard/assets/select-CGUuTQmj.js +0 -41
  404. package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
  405. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
  406. package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
  407. package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
  408. package/scripts/cli/cli.ts +0 -771
  409. package/scripts/operations/probe-egress.ts +0 -335
  410. package/scripts/operations/restart.ts +0 -120
  411. package/scripts/shared/env.ts +0 -269
  412. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
  413. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
  414. /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
  415. /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
@@ -1,34 +1,39 @@
1
- /**
2
- * Public release discovery for the console's update indicator.
3
- *
4
- * The browser never talks to GitHub directly: a dashboard-side fetch would put
5
- * every console user's GitHub rate limit behind one shared, unauthenticated
6
- * origin, and a CORS or rate-limit failure would surface as a console error
7
- * nobody can act on. The request is made here, server-side, and any failure
8
- * degrades to "no update known" rather than an error the operator must read.
9
- */
10
- const REPOSITORY = "risunCode/Cartethyia";
11
-
12
- /** Timeout for one GitHub read. A hung read must not hold a console query open. */
13
- const REQUEST_TIMEOUT_MS = 10_000;
14
-
15
- export interface LatestRelease {
16
- readonly tag_name: string;
17
- readonly html_url: string;
18
- }
19
-
20
- /** Fetches the latest public release. Returns `null` when none is known. */
21
- export async function fetchLatestRelease(): Promise<LatestRelease | null> {
22
- try {
23
- const response = await fetch(`https://api.github.com/repos/${REPOSITORY}/releases/latest`, {
24
- headers: { accept: "application/vnd.github+json", "user-agent": "Cartethyia-updater" },
25
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
26
- });
27
- if (!response.ok) return null;
28
- const value = (await response.json()) as { readonly tag_name?: unknown; readonly html_url?: unknown };
29
- if (typeof value.tag_name !== "string" || typeof value.html_url !== "string") return null;
30
- return { tag_name: value.tag_name, html_url: value.html_url };
31
- } catch {
32
- return null;
33
- }
34
- }
1
+ /**
2
+ * Public release discovery for the console's update indicator.
3
+ *
4
+ * The browser never talks to GitHub directly: a dashboard-side fetch would put
5
+ * every console user's GitHub rate limit behind one shared, unauthenticated
6
+ * origin, and a CORS or rate-limit failure would surface as a console error
7
+ * nobody can act on. The request is made here, server-side, and any failure
8
+ * degrades to "no update known" rather than an error the operator must read.
9
+ *
10
+ * Releases are read from the ALTERNATIVE repository (`risunCodex`), not the
11
+ * primary: the primary repository is hidden from public view, so its releases
12
+ * endpoint answers 404 and the console would report "no update known" forever —
13
+ * a silent failure that reads exactly like "you are up to date".
14
+ */
15
+ const REPOSITORY = "risunCodex/Cartethyia";
16
+
17
+ /** Timeout for one GitHub read. A hung read must not hold a console query open. */
18
+ const REQUEST_TIMEOUT_MS = 10_000;
19
+
20
+ export interface LatestRelease {
21
+ readonly tag_name: string;
22
+ readonly html_url: string;
23
+ }
24
+
25
+ /** Fetches the latest public release. Returns `null` when none is known. */
26
+ export async function fetchLatestRelease(): Promise<LatestRelease | null> {
27
+ try {
28
+ const response = await fetch(`https://api.github.com/repos/${REPOSITORY}/releases/latest`, {
29
+ headers: { accept: "application/vnd.github+json", "user-agent": "Cartethyia-updater" },
30
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
31
+ });
32
+ if (!response.ok) return null;
33
+ const value = (await response.json()) as { readonly tag_name?: unknown; readonly html_url?: unknown };
34
+ if (typeof value.tag_name !== "string" || typeof value.html_url !== "string") return null;
35
+ return { tag_name: value.tag_name, html_url: value.html_url };
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
@@ -1,6 +1,73 @@
1
1
  import { Elysia } from "elysia";
2
- import { consoleErrorHandler } from "../shared/errors";
3
- import { fetchLatestRelease } from "./repository-updates";
2
+ import { consoleErrorHandler, requireTenantScope } from "../shared/errors";
3
+ import { consoleSseResponse, createConsoleSseStream } from "../observability/sse";
4
+ import type { ObservabilityStore } from "../observability/contracts";
5
+ import type { ConsoleAccessResolver } from "../auth/access";
6
+ import { fetchLatestRelease, type LatestRelease } from "./repository-updates";
7
+ import { isNewerRelease } from "./version-compare";
8
+
9
+ const MEMORY_SAMPLE_INTERVAL_MS = 10_000;
10
+ const UPDATE_SAMPLE_INTERVAL_MS = 60_000;
11
+ const HIGH_MEMORY_PERCENT = 85;
12
+ const HIGH_MEMORY_CLEAR_PERCENT = 80;
13
+ const MEMORY_GROWTH_WINDOW_MS = 60_000;
14
+ const MEMORY_GROWTH_RATIO = 1.2;
15
+ const MAX_MEMORY_SAMPLES = 8;
16
+ const RELEASES_URL = "https://github.com/risunCodex/Cartethyia/releases";
17
+
18
+ export interface NotificationEvent {
19
+ readonly id: string;
20
+ readonly kind: "memory-high" | "memory-growth" | "update-available";
21
+ readonly title: string;
22
+ readonly message: string;
23
+ readonly createdAt: string;
24
+ readonly level: "warning" | "info";
25
+ readonly href?: string;
26
+ }
27
+
28
+ export function memoryNotifications(
29
+ samples: readonly { readonly at: number; readonly rss: number; readonly percent: number }[],
30
+ previous: { high: boolean; growth: boolean },
31
+ ): { notifications: NotificationEvent[]; high: boolean; growth: boolean } {
32
+ const latest = samples.at(-1);
33
+ if (latest === undefined) return { notifications: [], ...previous };
34
+ const notifications: NotificationEvent[] = [];
35
+ const high = previous.high
36
+ ? latest.percent >= HIGH_MEMORY_CLEAR_PERCENT
37
+ : latest.percent >= HIGH_MEMORY_PERCENT;
38
+ if (high && !previous.high) {
39
+ notifications.push({
40
+ id: "memory-high",
41
+ kind: "memory-high",
42
+ title: "High gateway memory",
43
+ message: `Gateway RSS is using ${latest.percent.toFixed(1)}% of system memory.`,
44
+ createdAt: new Date(latest.at).toISOString(),
45
+ level: "warning",
46
+ });
47
+ }
48
+
49
+ const oldest = samples.find((sample) => latest.at - sample.at >= MEMORY_GROWTH_WINDOW_MS);
50
+ const growth = oldest !== undefined &&
51
+ latest.rss >= oldest.rss * MEMORY_GROWTH_RATIO &&
52
+ samples.filter((sample) => sample.at >= oldest.at).every((sample, index, window) =>
53
+ index === 0 || sample.rss >= (window[index - 1]?.rss ?? sample.rss),
54
+ );
55
+ if (growth && !previous.growth && !high) {
56
+ notifications.push({
57
+ id: "memory-growth",
58
+ kind: "memory-growth",
59
+ title: "Sustained memory growth",
60
+ message: `Gateway RSS grew ${Math.round((latest.rss / oldest.rss - 1) * 100)}% over the last minute.`,
61
+ createdAt: new Date(latest.at).toISOString(),
62
+ level: "warning",
63
+ });
64
+ }
65
+ return {
66
+ notifications,
67
+ high: high && latest.percent >= HIGH_MEMORY_CLEAR_PERCENT,
68
+ growth,
69
+ };
70
+ }
4
71
 
5
72
  /**
6
73
  * Read-only update discovery for the console sidebar.
@@ -10,8 +77,99 @@ import { fetchLatestRelease } from "./repository-updates";
10
77
  * answers `null` rather than an error: an absent update badge is the honest
11
78
  * result when the upstream cannot be reached.
12
79
  */
13
- export function createUpdateRoutes(): Elysia {
80
+ export function createUpdateRoutes(config: {
81
+ readonly accessResolver: ConsoleAccessResolver;
82
+ readonly observabilityStore: ObservabilityStore;
83
+ readonly fetchRelease?: () => Promise<LatestRelease | null>;
84
+ }): Elysia {
85
+ const fetchRelease = config.fetchRelease ?? fetchLatestRelease;
14
86
  return new Elysia()
15
87
  .error(consoleErrorHandler("Update check failed"))
16
- .get("/updates/release", async () => await fetchLatestRelease()) as unknown as Elysia;
88
+ .get("/updates/release", async () => await fetchRelease())
89
+ .get("/notifications/stream", ({ request }) => {
90
+ const tenantId = requireTenantScope(
91
+ config.accessResolver(request),
92
+ "dashboard:read",
93
+ ).tenantId;
94
+ return consoleSseResponse(
95
+ createConsoleSseStream(request.signal, (sender) => {
96
+ let closed = false;
97
+ let memoryTimer: ReturnType<typeof setTimeout> | undefined;
98
+ let updateTimer: ReturnType<typeof setTimeout> | undefined;
99
+ let latestTag: string | undefined;
100
+ let memoryState = { high: false, growth: false };
101
+ let samples: Array<{ at: number; rss: number; percent: number }> = [];
102
+
103
+ const publishMemory = async (): Promise<void> => {
104
+ if (closed) return;
105
+ try {
106
+ const at = Date.now();
107
+ // Fresh, not `health`: that read is cached for 10 s and this
108
+ // samples on that exact cadence, so the cached bytes would make
109
+ // consecutive samples identical and a growth trend unreadable.
110
+ const live = await config.observabilityStore.liveMemory();
111
+ samples = [
112
+ ...samples,
113
+ { at, rss: live.memory_bytes, percent: live.memory_percent },
114
+ ].filter((sample) => at - sample.at <= MEMORY_GROWTH_WINDOW_MS * 2)
115
+ .slice(-MAX_MEMORY_SAMPLES);
116
+ const computed = memoryNotifications(samples, memoryState);
117
+ memoryState = { high: computed.high, growth: computed.growth };
118
+ for (const notification of computed.notifications) sender.send("notification", notification);
119
+ } catch {
120
+ // A transient check must not terminate the feed or hide later events.
121
+ }
122
+ };
123
+
124
+ const publishUpdate = async (): Promise<void> => {
125
+ if (closed) return;
126
+ try {
127
+ const release = await fetchRelease();
128
+ if (release === null || release.tag_name === latestTag) return;
129
+ const health = (await config.observabilityStore.health(tenantId)).value;
130
+ if (!isNewerRelease(health.version, release.tag_name)) return;
131
+ latestTag = release.tag_name;
132
+ sender.send("notification", {
133
+ id: `update-${release.tag_name}`,
134
+ kind: "update-available",
135
+ title: "Cartethyia update available",
136
+ message: `Version ${release.tag_name} is available; this gateway runs ${health.version}.`,
137
+ createdAt: new Date().toISOString(),
138
+ level: "info",
139
+ href: RELEASES_URL,
140
+ } satisfies NotificationEvent);
141
+ } catch {
142
+ // Release discovery is best-effort and cannot interrupt memory sampling.
143
+ }
144
+ };
145
+
146
+ const scheduleMemory = (): void => {
147
+ memoryTimer = setTimeout(() => {
148
+ void publishMemory().finally(() => {
149
+ if (!closed) scheduleMemory();
150
+ });
151
+ }, MEMORY_SAMPLE_INTERVAL_MS);
152
+ };
153
+ const scheduleUpdate = (): void => {
154
+ updateTimer = setTimeout(() => {
155
+ void publishUpdate().finally(() => {
156
+ if (!closed) scheduleUpdate();
157
+ });
158
+ }, UPDATE_SAMPLE_INTERVAL_MS);
159
+ };
160
+
161
+ void publishMemory().finally(() => {
162
+ if (!closed) scheduleMemory();
163
+ });
164
+ void publishUpdate().finally(() => {
165
+ if (!closed) scheduleUpdate();
166
+ });
167
+ return () => {
168
+ closed = true;
169
+ clearTimeout(memoryTimer);
170
+ clearTimeout(updateTimer);
171
+ };
172
+ }),
173
+ );
174
+ }) as unknown as Elysia;
17
175
  }
package/src/main.ts CHANGED
@@ -1,3 +1,6 @@
1
+ // Must run before any module that registers a route: it wires Elysia's TypeBox
2
+ // bridge statically, which the source-run path needs under NODE_ENV=production.
3
+ import "./boot/elysia-typebox-bridge";
1
4
  import { createGatewayApp, createGatewayShell } from "./boot/app";
2
5
  import { bootstrap } from "./boot/lifecycle";
3
6
  import type { CartethyiaBoot } from "./boot/lifecycle";
@@ -0,0 +1,85 @@
1
+ // Transport-neutral bidirectional HTTP/2 egress contract.
2
+ //
3
+ // Cursor's Run is a Connect-RPC bidi stream: one long-lived writable request
4
+ // whose response body is an unbounded stream of protobuf frames. Neither
5
+ // `fetch` nor the WebSocket session contract can express it — `fetch` needs the
6
+ // whole body up front, and a WebSocket is a different protocol — so the egress
7
+ // boundary gets its own contract.
8
+ //
9
+ // This module is transport-neutral on purpose: it declares *what* a validated
10
+ // bidi stream is, and `network/pool/resolver.ts` decides *how* one is dialed
11
+ // (direct TLS to a pinned, SSRF-validated address, or through the selected
12
+ // tenant pool's CONNECT/SOCKS tunnel). A provider adapter consumes the contract
13
+ // and never opens a socket of its own.
14
+
15
+ /** Response metadata of one opened bidi stream. */
16
+ export interface Http2BidiResponse {
17
+ /** HTTP/2 `:status`; `0` before the response headers arrive. */
18
+ readonly status: number;
19
+ readonly headers: Readonly<Record<string, string>>;
20
+ }
21
+
22
+ /** Terminal reason a bidi stream ended. */
23
+ export type Http2BidiEndReason =
24
+ /** The server closed the stream cleanly. */
25
+ | { readonly kind: "remote-clean-close" }
26
+ /** The server failed it: an HTTP status, a gRPC trailer, or a transport error. */
27
+ | { readonly kind: "remote-error"; readonly error: Error }
28
+ /** The client closed or aborted it. */
29
+ | { readonly kind: "local-close" };
30
+
31
+ /**
32
+ * One validated bidirectional HTTP/2 stream.
33
+ *
34
+ * Bytes in are an async iterable (so a provider pumps frames naturally and
35
+ * backpressure comes from the consumer); bytes out are `write`, which resolves
36
+ * once the chunk has drained — a bidi Run can carry megabytes, and an
37
+ * unawaited write is how a stream overruns its session memory and dies with
38
+ * `ENHANCE_YOUR_CALM`.
39
+ */
40
+ export interface ValidatedHttp2BidiStream {
41
+ readonly response: Http2BidiResponse;
42
+ /** Response trailers, populated when the server sends them (gRPC status). */
43
+ readonly trailers: Readonly<Record<string, string>>;
44
+ /** Response body bytes, in arrival order. */
45
+ readonly body: AsyncIterable<Uint8Array>;
46
+ /** Writes one request chunk, resolving when it has drained. */
47
+ write(chunk: Uint8Array): Promise<void>;
48
+ /** Half-closes the request side; the response may still be streaming. */
49
+ end(): void;
50
+ /** Tears the stream down immediately. Idempotent. */
51
+ abort(reason?: unknown): void;
52
+ /** Registers a terminal listener; the returned function unregisters it. */
53
+ onTerminal(listener: (reason: Http2BidiEndReason) => void): () => void;
54
+ }
55
+
56
+ /** Options for opening one validated bidi stream. */
57
+ export interface Http2BidiOpenOptions {
58
+ readonly signal?: AbortSignal;
59
+ /** Bytes read idle before the stream is declared dead. */
60
+ readonly readIdleMs?: number;
61
+ /** Upper bound on buffered inbound bytes before the stream is refused. */
62
+ readonly maxBufferedBytes?: number;
63
+ /**
64
+ * Host allowlist applied on every new connection, in addition to the SSRF
65
+ * policy. A long-lived stream carries the account credential for its whole
66
+ * life, so "is this host one the caller may reach at all" is a per-provider
67
+ * decision the binding enforces rather than trusts the adapter to apply.
68
+ */
69
+ readonly allowHostname?: (hostname: string) => boolean;
70
+ }
71
+
72
+ /**
73
+ * The dialer contract a provider adapter is handed.
74
+ *
75
+ * Declared here rather than in the provider layer so the dependency runs one
76
+ * way: `provider-contracts` imports this module, never the reverse. It used to
77
+ * be declared there and re-exported from here, which made the two import each
78
+ * other — type-only, so it never deadlocked, but it meant this
79
+ * transport-neutral contract had to know about providers.
80
+ */
81
+ export type ValidatedOutboundHttp2Bidi = (
82
+ url: URL,
83
+ headers: Readonly<Record<string, string>>,
84
+ options?: Http2BidiOpenOptions,
85
+ ) => Promise<ValidatedHttp2BidiStream>;
@@ -34,6 +34,57 @@ function agentForTarget(
34
34
  return agent;
35
35
  }
36
36
 
37
+ /**
38
+ * Keep-alive options for the direct (non-pool) egress agents.
39
+ *
40
+ * Matched to `network/pool/agent.ts`, which builds pool agents from
41
+ * `resolveProxyMaxSockets()` / `resolveProxyMaxFreeSockets()` and
42
+ * `PROXY_KEEP_ALIVE_TIMEOUT_MS`. Direct egress has no per-pool config to
43
+ * read, so it uses the documented defaults those resolvers return.
44
+ */
45
+ const DIRECT_AGENT_OPTIONS = {
46
+ keepAlive: true,
47
+ maxSockets: 100,
48
+ maxFreeSockets: 20,
49
+ timeout: 60_000,
50
+ } as const;
51
+
52
+ /**
53
+ * An agent whose socket key includes the SSRF-validated address.
54
+ *
55
+ * The direct (non-pool) path pins egress by passing a `lookup` that returns the
56
+ * one validated address, but `Agent#getName` — the keep-alive socket key —
57
+ * hashes only host/port/localAddress/family. Two requests to the same
58
+ * `host:port` that validated to *different* addresses therefore share a pooled
59
+ * socket, and the second one silently egresses to the first one's address:
60
+ * the pin is bypassed without any error surfacing.
61
+ *
62
+ * Including the address in the key makes each validated address its own socket
63
+ * bucket, so a re-resolved address always dials fresh while the *same* address
64
+ * still reuses its socket (keep-alive is preserved, not defeated).
65
+ *
66
+ * This is a real gap, not a theoretical one: with no agent at all the request
67
+ * falls back to the global agent, which has the same key shape *and* is shared
68
+ * with unrelated traffic.
69
+ */
70
+ class PinnedAddressAgent extends HttpAgent {
71
+ override getName(options: Record<string, unknown> = {}): string {
72
+ const base = super.getName(options as never);
73
+ const pinned = (options["lookup"] as { pinnedAddress?: string } | undefined)?.pinnedAddress;
74
+ return `${base}:${pinned ?? "-"}`;
75
+ }
76
+ }
77
+ class PinnedAddressHttpsAgent extends HttpsAgent {
78
+ override getName(options: Record<string, unknown> = {}): string {
79
+ const base = super.getName(options as never);
80
+ const pinned = (options["lookup"] as { pinnedAddress?: string } | undefined)?.pinnedAddress;
81
+ return `${base}:${pinned ?? "-"}`;
82
+ }
83
+ }
84
+
85
+ const DIRECT_HTTP_AGENT = new PinnedAddressAgent(DIRECT_AGENT_OPTIONS);
86
+ const DIRECT_HTTPS_AGENT = new PinnedAddressHttpsAgent(DIRECT_AGENT_OPTIONS);
87
+
37
88
  /**
38
89
  * Builds the application-relay request used by hosted relay front doors.
39
90
  * These endpoints intentionally do not receive CONNECT: they accept the
@@ -99,7 +150,8 @@ function decompressBody(
99
150
  return { stream: body.pipe(decoder), encoding: undefined };
100
151
  }
101
152
 
102
- async function pinnedFetch( url: URL,
153
+ async function pinnedFetch(
154
+ url: URL,
103
155
  init: RequestInit,
104
156
  resolvedAddress: string | undefined,
105
157
  agent?: HttpAgent | HttpsAgent,
@@ -126,27 +178,38 @@ async function pinnedFetch( url: URL,
126
178
  outboundHeaders["content-length"] =
127
179
  typeof body === "string" ? String(Buffer.byteLength(body, "utf8")) : String(body.byteLength);
128
180
  }
129
- const lookup = ((
130
- _hostname: string,
131
- options: { all?: boolean; family?: number } | number,
132
- callback: (
133
- error: Error | null,
134
- address?: string | Array<{ address: string; family: number }>,
135
- family?: number,
136
- ) => void,
137
- ): void => {
138
- if (resolvedAddress === undefined) {
139
- callback(new Error("no validated address available"));
140
- return;
141
- }
142
- const opts = typeof options === "number" ? { family: options } : options;
143
- const family = resolvedAddress.includes(":") ? 6 : 4;
144
- if (opts?.all) {
145
- callback(null, [{ address: resolvedAddress, family }] as never);
146
- return;
147
- }
148
- callback(null, resolvedAddress, family);
149
- }) as unknown as NonNullable<RequestOptions["lookup"]>;
181
+ const lookup = Object.assign(
182
+ (
183
+ _hostname: string,
184
+ options: { all?: boolean; family?: number } | number,
185
+ callback: (
186
+ error: Error | null,
187
+ address?: string | Array<{ address: string; family: number }>,
188
+ family?: number,
189
+ ) => void,
190
+ ): void => {
191
+ if (resolvedAddress === undefined) {
192
+ callback(new Error("no validated address available"));
193
+ return;
194
+ }
195
+ const opts = typeof options === "number" ? { family: options } : options;
196
+ const family = resolvedAddress.includes(":") ? 6 : 4;
197
+ if (opts?.all) {
198
+ callback(null, [{ address: resolvedAddress, family }] as never);
199
+ return;
200
+ }
201
+ callback(null, resolvedAddress, family);
202
+ },
203
+ // Read by `PinnedAddressAgent#getName` so the keep-alive socket key
204
+ // carries the validated address. See the class comment for why.
205
+ { pinnedAddress: resolvedAddress },
206
+ ) as unknown as NonNullable<RequestOptions["lookup"]>;
207
+ // Direct egress gets a dedicated agent as well as the pinning `lookup`.
208
+ // Passing only `lookup` left the request on the global agent, whose socket
209
+ // key ignores `lookup` — so a re-resolved address could reuse a socket
210
+ // dialed against an earlier one and egress past the SSRF check. The direct
211
+ // agent keys its sockets on the pinned address, so the pin holds.
212
+ const directAgent = url.protocol === "https:" ? DIRECT_HTTPS_AGENT : DIRECT_HTTP_AGENT;
150
213
  return new Promise<Response>((resolve, reject) => {
151
214
  const nodeRequest = request(
152
215
  {
@@ -157,7 +220,7 @@ async function pinnedFetch( url: URL,
157
220
  method: init.method ?? "GET",
158
221
  headers: outboundHeaders,
159
222
  signal: init.signal ?? undefined,
160
- ...(agent ? { agent } : { lookup }),
223
+ ...(agent ? { agent } : { agent: directAgent, lookup }),
161
224
  ...(url.protocol === "https:" ? { servername: url.hostname } : {}),
162
225
  },
163
226
  (response) => {
@@ -230,7 +293,7 @@ export function createValidatedFetch(options: ValidatedFetchOptions = {}): Valid
230
293
  // The egress dials the pool/relay, which resolves the target itself.
231
294
  // Counting these lets ops confirm how much traffic depends on
232
295
  // proxy-resolved DNS rather than the local system resolver.
233
- metrics.cartethyia_proxy_dial_dns_fallback_total.inc();
296
+ metrics.cartethyia_proxy_dial_proxy_resolved_total.inc();
234
297
  }
235
298
  try {
236
299
  if (!poolDialsTarget) {