cartethyia 2.7.3 → 2.7.4

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 (352) hide show
  1. package/dist/dashboard/assets/{BannedPage-2TJD37Hb.js → BannedPage-ChG5svFb.js} +1 -1
  2. package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
  3. package/dist/dashboard/assets/{CliToolDetailPage-xefJNRmJ.js → CliToolDetailPage-BIcWQun7.js} +1 -1
  4. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolsPage-Rj9iHQ3F.js → CliToolsPage-Bf2VelDe.js} +1 -1
  6. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
  7. package/dist/dashboard/assets/{CombosPage-JEbYJFxw.js → CombosPage-DCgm5g6g.js} +1 -1
  8. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
  9. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +1 -0
  10. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
  11. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +1 -0
  12. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
  13. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +1 -0
  14. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
  15. package/dist/dashboard/assets/{LoginPage-C4w7vea-.js → LoginPage-U6AKaJcF.js} +1 -1
  16. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
  17. package/dist/dashboard/assets/{ModelPicker-BtPcAfot.js → ModelPicker-BMS2t8BA.js} +1 -1
  18. package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
  19. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +1 -0
  20. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
  21. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +5 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
  23. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +1 -0
  24. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
  25. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js +9 -0
  26. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
  27. package/dist/dashboard/assets/QuotaPage-L0skzML8.js +1 -0
  28. package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
  29. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +1 -0
  30. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
  31. package/dist/dashboard/assets/{SetupPage-BQS0Rx_u.js → SetupPage-f0DE1h0-.js} +1 -1
  32. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
  33. package/dist/dashboard/assets/{SortableList-BAL5npBC.js → SortableList-ClzpwyET.js} +1 -1
  34. package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
  35. package/dist/dashboard/assets/{StudioPage-TP7voAQ0.js → StudioPage-Cq5tW7hW.js} +3 -3
  36. package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
  37. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +56 -0
  38. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
  39. package/dist/dashboard/assets/api-keys-C38iXoO4.js +1 -0
  40. package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
  41. package/dist/dashboard/assets/app-a159k0ub.css +2 -0
  42. package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
  43. package/dist/dashboard/assets/assert-DbRA8EG2.js +15 -0
  44. package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
  45. package/dist/dashboard/assets/{badge-B_DMpMJN.js → badge-D9CS6dZk.js} +1 -1
  46. package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
  47. package/dist/dashboard/assets/{banner-DMrMPmth.js → banner-DympBNq4.js} +1 -1
  48. package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
  49. package/dist/dashboard/assets/{card-Be5Uzamx.js → card-ibRWU_uT.js} +1 -1
  50. package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
  51. package/dist/dashboard/assets/cli-tools-63qBGaRw.js +1 -0
  52. package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
  53. package/dist/dashboard/assets/{clipboard-button-DOfrVmsi.js → clipboard-button-DJm-9pz7.js} +1 -1
  54. package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
  55. package/dist/dashboard/assets/{dialog-C9omdjQt.js → dialog-8fiE6tec.js} +1 -1
  56. package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
  57. package/dist/dashboard/assets/{drawer-Bm8WKaGY.js → drawer-CqGYeOrd.js} +1 -1
  58. package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
  59. package/dist/dashboard/assets/{entry-q5HwZ8nN.js → entry-BSAsYsmV.js} +1 -1
  60. package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
  61. package/dist/dashboard/assets/entry-CiIQULZD.js +4 -0
  62. package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
  63. package/dist/dashboard/assets/entry-Daz0lJ-W.js +1 -0
  64. package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
  65. package/dist/dashboard/assets/{formatters-BTScrQpe.js → formatters-D-MTJxMr.js} +1 -1
  66. package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
  67. package/dist/dashboard/assets/{github-badge-Orjuzemr.js → github-badge-CPK47xQx.js} +1 -1
  68. package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
  69. package/dist/dashboard/assets/{index-cPrX5vn6.js → index-DBNPOfhV.js} +2 -2
  70. package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
  71. package/dist/dashboard/assets/{inline-Dvux8w-0.js → inline-BT0zntEr.js} +1 -1
  72. package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
  73. package/dist/dashboard/assets/{input-CU1Rgf3p.js → input-Da2ZViWg.js} +1 -1
  74. package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
  75. package/dist/dashboard/assets/{layout-C_7A6H55.js → layout-BXNeLw6M.js} +1 -1
  76. package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
  77. package/dist/dashboard/assets/{list-row-CzFvogbj.js → list-row-BycOrrIV.js} +1 -1
  78. package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
  79. package/dist/dashboard/assets/live-DCQNZpF7.js +1 -0
  80. package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
  81. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
  82. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  83. package/dist/dashboard/assets/{mutation-JJGQm8TD.js → mutation-Rr0TIvcj.js} +1 -1
  84. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  85. package/dist/dashboard/assets/network-9Yj2ZyLo.js +1 -0
  86. package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
  87. package/dist/dashboard/assets/{numbers-BNDZnsUE.js → numbers-CEagj6ZZ.js} +2 -2
  88. package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-CLaNz8Xz.js → query-keys-BloKtaXJ.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
  91. package/dist/dashboard/assets/quota-Caw7wqci.js +1 -0
  92. package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
  93. package/dist/dashboard/assets/routing-CO9zCUus.js +1 -0
  94. package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
  95. package/dist/dashboard/assets/{select-B3iTetHN.js → select-BIYIBkMU.js} +2 -2
  96. package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
  97. package/dist/dashboard/assets/settings-jS3-PNSA.js +1 -0
  98. package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
  99. package/dist/dashboard/assets/{stack-KrOrm7JZ.js → stack-DfgnFDWY.js} +1 -1
  100. package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
  101. package/dist/dashboard/assets/{state-DLS7IDMy.js → state-CkfdYWel.js} +1 -1
  102. package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
  103. package/dist/dashboard/assets/{switch-42ItvbAs.js → switch-SYm237HG.js} +1 -1
  104. package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
  105. package/dist/dashboard/assets/{useMutation-Dmnfh1Ue.js → useMutation-BQqCRNIi.js} +1 -1
  106. package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
  107. package/dist/dashboard/index.html +2 -2
  108. package/dist/dashboard/index.html.gz +0 -0
  109. package/dist/dashboard/precompressed-assets.json +55 -54
  110. package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +9 -10
  111. package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
  112. package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
  113. package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
  114. package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
  115. package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +9 -10
  116. package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
  117. package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
  118. package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
  119. package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
  120. package/package.json +2 -1
  121. package/src/boot/app.ts +9 -8
  122. package/src/config/network.ts +7 -3
  123. package/src/console/api-keys/contracts.ts +52 -0
  124. package/src/console/api-keys/routes.ts +93 -4
  125. package/src/console/audit/routes.ts +3 -3
  126. package/src/console/auth/account-reset.ts +4 -4
  127. package/src/console/auth/session.ts +3 -3
  128. package/src/console/backup/contracts.ts +2 -0
  129. package/src/console/backup/export-endpoint.ts +6 -6
  130. package/src/console/backup/routes.ts +3 -3
  131. package/src/console/cli-tools/routes.ts +3 -3
  132. package/src/console/console-router-guards.ts +2 -2
  133. package/src/console/console-router.ts +4 -4
  134. package/src/console/data-management/routes.ts +3 -3
  135. package/src/console/domain-registration.ts +2 -2
  136. package/src/console/observability/contracts.ts +4 -0
  137. package/src/console/observability/live.ts +3 -3
  138. package/src/console/observability/logs.ts +3 -3
  139. package/src/console/observability/performance.ts +3 -3
  140. package/src/console/observability/routes.ts +3 -3
  141. package/src/console/observability/store.ts +16 -16
  142. package/src/console/providers/catalog/contracts.ts +43 -22
  143. package/src/console/providers/catalog/provider-operations.ts +17 -2
  144. package/src/console/providers/catalog/routes.ts +3 -3
  145. package/src/console/providers/catalog/store-helpers.ts +349 -326
  146. package/src/console/providers/catalog/store.ts +1523 -1292
  147. package/src/console/providers/detail/routes.ts +3 -3
  148. package/src/console/providers/oauth/routes.ts +3 -3
  149. package/src/console/quota/account-quota-tenant.ts +26 -1
  150. package/src/console/quota/account-test-egress.ts +4 -4
  151. package/src/console/routing/model/routes.ts +3 -3
  152. package/src/console/routing/pools/contracts.ts +11 -8
  153. package/src/console/routing/pools/operations.ts +14 -7
  154. package/src/console/routing/pools/probe-verdict.ts +8 -3
  155. package/src/console/routing/pools/relay-deploy.ts +9 -9
  156. package/src/console/routing/pools/routes.ts +3 -3
  157. package/src/console/routing/pools/store.ts +21 -1
  158. package/src/console/settings/routes.ts +3 -3
  159. package/src/console/share/share-router.ts +12 -12
  160. package/src/console/share/share-stats.ts +105 -35
  161. package/src/console/shared/read-through-cache.ts +38 -2
  162. package/src/console/updates/routes.ts +7 -4
  163. package/src/network/outbound-fetch.ts +75 -4
  164. package/src/network/pool/resolver.ts +39 -39
  165. package/src/network/pool-cooldown-store.ts +71 -16
  166. package/src/network/pool-health-machine.ts +21 -1
  167. package/src/network/rate-limit-verdict.ts +68 -0
  168. package/src/network/ssrf.ts +4 -4
  169. package/src/observability/event-query.ts +320 -16
  170. package/src/observability/telemetry-status.ts +1 -1
  171. package/src/persistence/db-sqlite.ts +7 -2
  172. package/src/persistence/readiness.ts +18 -6
  173. package/src/persistence/schema.ts +66 -7
  174. package/src/persistence/stores/api-key-store.ts +99 -3
  175. package/src/persistence/stores/telemetry-store.ts +1 -1
  176. package/src/protocol/codecs.ts +6 -6
  177. package/src/protocol/gateway-error.ts +6 -3
  178. package/src/protocol/image-generation.ts +2 -2
  179. package/src/protocol/response/chat.ts +2 -2
  180. package/src/protocol/response/codex.ts +1 -1
  181. package/src/protocol/response/gemini.ts +1 -1
  182. package/src/protocol/response/messages.ts +5 -5
  183. package/src/protocol/response/responses.ts +2 -2
  184. package/src/protocol/sse-decoder.ts +14 -3
  185. package/src/protocol/translation/capabilities.ts +2 -2
  186. package/src/protocol/upstream/send-claude-messages.ts +3 -3
  187. package/src/providers/adapters/base-adapter.ts +1 -1
  188. package/src/providers/adapters/compatible-adapter.ts +2 -2
  189. package/src/providers/authentication/oauth-refresh-service.ts +2 -2
  190. package/src/providers/configured-provider.ts +1 -1
  191. package/src/providers/discovery/probe-phases.ts +2 -2
  192. package/src/providers/discovery/probing-service.ts +12 -12
  193. package/src/providers/integrations/agentrouter/agentrouter.ts +2 -3
  194. package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
  195. package/src/providers/integrations/anthropic.ts +4 -11
  196. package/src/providers/integrations/antigravity/antigravity.ts +13 -24
  197. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
  198. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
  199. package/src/providers/integrations/buddy/buddy-identity.ts +22 -1
  200. package/src/providers/integrations/buddy/buddy-media.ts +16 -16
  201. package/src/providers/integrations/buddy/codebuddy/provider.ts +1 -1
  202. package/src/providers/integrations/buddy/workbuddy/provider.ts +1 -1
  203. package/src/providers/integrations/claude/claude-betas.ts +1 -1
  204. package/src/providers/integrations/claude/claude-credentials.ts +1 -1
  205. package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
  206. package/src/providers/integrations/claude/claude.ts +3 -10
  207. package/src/providers/integrations/cline/cline.ts +1 -1
  208. package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
  209. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  210. package/src/providers/integrations/codex/codex.ts +14 -15
  211. package/src/providers/integrations/commandcode/commandcode.ts +4 -4
  212. package/src/providers/integrations/commandcode/quota.ts +5 -1
  213. package/src/providers/integrations/cursor/cursor-http2.ts +11 -11
  214. package/src/providers/integrations/cursor/cursor.ts +15 -15
  215. package/src/providers/integrations/factory/factory.ts +4 -4
  216. package/src/providers/integrations/gemini.ts +11 -11
  217. package/src/providers/integrations/github/github.ts +6 -6
  218. package/src/providers/integrations/grok/grok.ts +2 -6
  219. package/src/providers/integrations/inferhub.ts +4 -11
  220. package/src/providers/integrations/kimi/kimi.ts +4 -11
  221. package/src/providers/integrations/kiro/kiro.ts +10 -10
  222. package/src/providers/integrations/muse/muse.ts +6 -9
  223. package/src/providers/integrations/perplexity.ts +2 -2
  224. package/src/providers/integrations/qoder/qoder-catalog.ts +47 -9
  225. package/src/providers/integrations/qoder/qoder-checkin.ts +26 -8
  226. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +9 -8
  227. package/src/providers/integrations/qoder/qoder-realm.ts +15 -0
  228. package/src/providers/integrations/qoder/qoder-umid.ts +12 -13
  229. package/src/providers/integrations/qoder/qoder-virtual.ts +2 -2
  230. package/src/providers/integrations/qoder/qoder.ts +188 -22
  231. package/src/providers/integrations/tokenharbor.ts +2 -2
  232. package/src/providers/integrations/trae/trae.ts +6 -9
  233. package/src/providers/integrations/zcode/oauth.ts +1 -1
  234. package/src/providers/operations/provider-catalog-service.ts +4 -4
  235. package/src/providers/operations/provider-credential-service.ts +16 -4
  236. package/src/providers/operations/upstream-deadline.ts +3 -3
  237. package/src/providers/provider-contracts.ts +47 -3
  238. package/src/providers/provider-credential-resolver.ts +1 -1
  239. package/src/providers/provider-registry.ts +4 -4
  240. package/src/providers/quota/quota-contracts.ts +24 -10
  241. package/src/providers/quota/refresh.ts +2 -2
  242. package/src/providers/search/search-provider.ts +5 -5
  243. package/src/security/abuse.ts +4 -4
  244. package/src/security/admission/reasons.ts +17 -17
  245. package/src/security/api-key-auth.ts +4 -4
  246. package/src/shared/coerce.ts +19 -0
  247. package/src/transport/dispatch/attempt-finalize.ts +4 -3
  248. package/src/transport/dispatch/attempt-loop.ts +10 -7
  249. package/src/transport/dispatch/empty-stream.ts +6 -6
  250. package/src/transport/dispatch/generation.ts +9 -9
  251. package/src/transport/dispatch/leases.ts +8 -8
  252. package/src/transport/dispatch/proxy-request.ts +7 -7
  253. package/src/transport/dispatch/responses-compact.ts +11 -11
  254. package/src/transport/dispatch/retry-policy.ts +4 -67
  255. package/src/transport/dispatch/stream-bridge.ts +1 -1
  256. package/src/transport/dispatch/streaming/finalize.ts +9 -8
  257. package/src/transport/dispatch/systemone.ts +8 -8
  258. package/src/transport/dispatch/token-count.ts +9 -9
  259. package/src/transport/dispatch/websearch.ts +6 -6
  260. package/src/transport/drain-endpoint.ts +2 -2
  261. package/src/transport/failure-policy.ts +13 -6
  262. package/src/transport/guards/auth-guard.ts +6 -5
  263. package/src/transport/guards/body-policy.ts +4 -4
  264. package/src/transport/guards/error-lifecycle.ts +18 -3
  265. package/src/transport/guards/guard-errors.ts +7 -3
  266. package/src/transport/guards/headroom-guard.ts +1 -1
  267. package/src/transport/guards/pipeline.ts +6 -6
  268. package/src/transport/guards/readiness-guard.ts +16 -2
  269. package/src/transport/guards/request-context.ts +3 -3
  270. package/src/transport/request/preparer.ts +20 -20
  271. package/src/transport/request/state.ts +4 -7
  272. package/src/transport/routing/route-catalog.ts +97 -2
  273. package/src/transport/routing/route-model.ts +7 -7
  274. package/src/transport/routing/router.ts +2 -2
  275. package/src/transport/shutdown-notice.ts +19 -7
  276. package/src/transport/surface/completion.ts +1 -1
  277. package/src/transport/surface/messages/encode.ts +1 -1
  278. package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
  279. package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
  280. package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
  281. package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
  282. package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js +0 -1
  283. package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
  284. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +0 -1
  285. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
  286. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +0 -1
  287. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
  288. package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
  289. package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
  290. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +0 -1
  291. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
  292. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +0 -4
  293. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
  294. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +0 -1
  295. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
  296. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +0 -9
  297. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
  298. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js +0 -1
  299. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
  300. package/dist/dashboard/assets/SettingsPage-CDZjTt87.js +0 -1
  301. package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
  302. package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
  303. package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
  304. package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
  305. package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js +0 -56
  306. package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
  307. package/dist/dashboard/assets/api-keys-MbI7e7rt.js +0 -1
  308. package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
  309. package/dist/dashboard/assets/app-B2vyPoII.css +0 -2
  310. package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
  311. package/dist/dashboard/assets/assert-C0YYuZJ4.js +0 -1
  312. package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
  313. package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
  314. package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
  315. package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
  316. package/dist/dashboard/assets/cli-tools-muFRphDu.js +0 -1
  317. package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
  318. package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
  319. package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
  320. package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
  321. package/dist/dashboard/assets/entry-DS90gUks.js +0 -1
  322. package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
  323. package/dist/dashboard/assets/entry-j40LJotO.js +0 -4
  324. package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
  325. package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
  326. package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
  327. package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
  328. package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
  329. package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
  330. package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
  331. package/dist/dashboard/assets/layout-C_7A6H55.js.gz +0 -0
  332. package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
  333. package/dist/dashboard/assets/live-KaG15HLQ.js +0 -1
  334. package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
  335. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
  336. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
  337. package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
  338. package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
  339. package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
  340. package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
  341. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  342. package/dist/dashboard/assets/routing-BEH32QOC.js +0 -1
  343. package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
  344. package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
  345. package/dist/dashboard/assets/settings-Dt3Eqbnq.js +0 -1
  346. package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
  347. package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
  348. package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
  349. package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
  350. package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
  351. /package/dist/dashboard/assets/{app-wvwCi4Eh.js → app-BayTTy01.js} +0 -0
  352. /package/dist/dashboard/assets/{app-wvwCi4Eh.js.gz → app-BayTTy01.js.gz} +0 -0
@@ -364,21 +364,21 @@ function connectValidatedWebSocket(
364
364
  if (settled) return;
365
365
  settled = true;
366
366
  socket.terminate();
367
- reject(new GatewayError("transport_unavailable", 502, "WebSocket handshake timed out"));
367
+ reject(new GatewayError("transport_unavailable", 502, "WebSocket handshake timed out (request {requestId}). Some things just take longer 🐢"));
368
368
  }, 15_000);
369
369
  const abort = (): void => {
370
370
  if (settled) return;
371
371
  settled = true;
372
372
  clearTimeout(timeout);
373
373
  socket.terminate();
374
- reject(new GatewayError("transport_closed", 499, "WebSocket connection was cancelled"));
374
+ reject(new GatewayError("transport_closed", 499, "WebSocket connection was cancelled (request {requestId}). Send it again whenever ready 🙃"));
375
375
  };
376
376
  const onError = (): void => {
377
377
  if (settled) return;
378
378
  settled = true;
379
379
  clearTimeout(timeout);
380
380
  signal.removeEventListener("abort", abort);
381
- reject(new GatewayError("transport_unavailable", 502, "WebSocket connection failed"));
381
+ reject(new GatewayError("transport_unavailable", 502, "WebSocket connection failed (request {requestId}). The connection ghosted us — come back in a moment 👻"));
382
382
  };
383
383
  socket.once("open", () => {
384
384
  if (settled) return;
@@ -398,7 +398,7 @@ function connectValidatedWebSocket(
398
398
  function agentForWebSocket(agent: PoolAgent): import("http").Agent | import("https").Agent {
399
399
  if (isProxyAgentPair(agent)) return agent.https;
400
400
  if (agent instanceof HttpsAgent) return agent;
401
- throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool does not support WSS");
401
+ throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool does not support WSS (request {requestId}). This bridge doesn't speak WSS — reconfigure the pool 🔧");
402
402
  }
403
403
 
404
404
  /**
@@ -411,20 +411,20 @@ function agentForWebSocket(agent: PoolAgent): import("http").Agent | import("htt
411
411
  */
412
412
  function assertBidiTarget(url: URL): void {
413
413
  if (url.protocol !== "https:") {
414
- throw new GatewayError("invalid_request", 400, "validated HTTP/2 egress requires https");
414
+ throw new GatewayError("invalid_request", 400, "validated HTTP/2 egress requires https (request {requestId}). Check the shape of what you sent — then resend 🪞");
415
415
  }
416
416
  if (url.username || url.password || url.hash) {
417
- throw new GatewayError("invalid_request", 400, "invalid validated HTTP/2 target");
417
+ throw new GatewayError("invalid_request", 400, "invalid validated HTTP/2 target (request {requestId}). Check the shape of what you sent — then resend 🪞");
418
418
  }
419
419
  if (url.port !== "" && url.port !== "443") {
420
- throw new GatewayError("invalid_request", 400, "validated HTTP/2 egress runs on port 443");
420
+ throw new GatewayError("invalid_request", 400, "validated HTTP/2 egress runs on port 443 (request {requestId}). Check the shape of what you sent — then resend 🪞");
421
421
  }
422
422
  const hostname = url.hostname;
423
423
  if (hostname.length === 0) {
424
- throw new GatewayError("invalid_request", 400, "HTTP/2 target hostname is required");
424
+ throw new GatewayError("invalid_request", 400, "HTTP/2 target hostname is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
425
425
  }
426
426
  if (isIP(hostname) !== 0) {
427
- throw new GatewayError("invalid_request", 400, "HTTP/2 target must be a hostname, not an IP literal");
427
+ throw new GatewayError("invalid_request", 400, "HTTP/2 target must be a hostname, not an IP literal (request {requestId}). Check the shape of what you sent — then resend 🪞");
428
428
  }
429
429
  }
430
430
 
@@ -455,13 +455,13 @@ function dialDirect(
455
455
  const onAbort = (): void => {
456
456
  cleanup();
457
457
  socket.destroy(new Error("HTTP/2 connection aborted"));
458
- reject(new GatewayError("transport_closed", 499, "HTTP/2 connection was cancelled"));
458
+ reject(new GatewayError("transport_closed", 499, "HTTP/2 connection was cancelled (request {requestId}). Send it again whenever ready 🙃"));
459
459
  };
460
460
  const timer = setTimeout(() => {
461
461
  cleanup();
462
462
  socket.destroy();
463
463
  reject(
464
- new GatewayError("transport_unavailable", 502, "HTTP/2 connection timed out", {}, "network"),
464
+ new GatewayError("transport_unavailable", 502, "HTTP/2 connection timed out (request {requestId}). Some things just take longer 🐢", {}, "network"),
465
465
  );
466
466
  }, HTTP2_CONNECT_TIMEOUT_MS);
467
467
  socket.once("secureConnect", () => {
@@ -474,7 +474,7 @@ function dialDirect(
474
474
  new GatewayError(
475
475
  "transport_unavailable",
476
476
  502,
477
- `HTTP/2 connection failed: ${error.message}`,
477
+ `HTTP/2 connection failed: ${error.message} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
478
478
  {},
479
479
  "network",
480
480
  ),
@@ -506,7 +506,7 @@ function dialThroughPool(
506
506
  throw new GatewayError(
507
507
  "proxy_pool_unhealthy",
508
508
  503,
509
- "configured proxy pool does not support HTTPS tunneling",
509
+ "configured proxy pool does not support HTTPS tunneling (request {requestId}). This bridge doesn't speak HTTPS tunnels — reconfigure the pool 🔧",
510
510
  );
511
511
  }
512
512
  return new Promise<tls.TLSSocket>((resolve, reject) => {
@@ -519,14 +519,14 @@ function dialThroughPool(
519
519
  ? new GatewayError(
520
520
  "proxy_pool_unhealthy",
521
521
  503,
522
- "Configured proxy pool could not be established.",
522
+ "Configured proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧",
523
523
  { reason: error.message.slice(0, 200) },
524
524
  "network",
525
525
  )
526
526
  : new GatewayError(
527
527
  "transport_unavailable",
528
528
  502,
529
- `HTTP/2 tunnel failed: ${error?.message ?? "no socket"}`,
529
+ `HTTP/2 tunnel failed: ${error?.message ?? "no socket"} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
530
530
  {},
531
531
  "network",
532
532
  ),
@@ -546,13 +546,13 @@ function dialThroughPool(
546
546
  const onAbort = (): void => {
547
547
  cleanup();
548
548
  secured.destroy();
549
- reject(new GatewayError("transport_closed", 499, "HTTP/2 connection was cancelled"));
549
+ reject(new GatewayError("transport_closed", 499, "HTTP/2 connection was cancelled (request {requestId}). Send it again whenever ready 🙃"));
550
550
  };
551
551
  const timer = setTimeout(() => {
552
552
  cleanup();
553
553
  secured.destroy();
554
554
  reject(
555
- new GatewayError("transport_unavailable", 502, "HTTP/2 connection timed out", {}, "network"),
555
+ new GatewayError("transport_unavailable", 502, "HTTP/2 connection timed out (request {requestId}). Some things just take longer 🐢", {}, "network"),
556
556
  );
557
557
  }, HTTP2_CONNECT_TIMEOUT_MS);
558
558
  secured.once("secureConnect", () => {
@@ -565,7 +565,7 @@ function dialThroughPool(
565
565
  new GatewayError(
566
566
  "transport_unavailable",
567
567
  502,
568
- `HTTP/2 TLS handshake failed: ${tlsError.message}`,
568
+ `HTTP/2 TLS handshake failed: ${tlsError.message} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
569
569
  {},
570
570
  "network",
571
571
  ),
@@ -636,7 +636,7 @@ class Http2BidiSession implements ValidatedHttp2BidiStream {
636
636
  new GatewayError(
637
637
  "platform_unavailable",
638
638
  502,
639
- "HTTP/2 response exceeded the buffer bound",
639
+ "HTTP/2 response exceeded the buffer bound (request {requestId}). The upstream sent more than we can hold — retry 🌙",
640
640
  {},
641
641
  "network",
642
642
  ),
@@ -646,7 +646,7 @@ class Http2BidiSession implements ValidatedHttp2BidiStream {
646
646
  stream.on("end", () => this.finishInbound());
647
647
  stream.on("error", (error) => this.fail(error));
648
648
  stream.on("aborted", () =>
649
- this.fail(new GatewayError("transport_unavailable", 502, "HTTP/2 stream aborted by remote", {}, "network")),
649
+ this.fail(new GatewayError("transport_unavailable", 502, "HTTP/2 stream aborted by remote (request {requestId}). The connection ghosted us — come back in a moment 👻", {}, "network")),
650
650
  );
651
651
  stream.on("close", () => {
652
652
  this.finishInbound();
@@ -699,7 +699,7 @@ class Http2BidiSession implements ValidatedHttp2BidiStream {
699
699
  new GatewayError(
700
700
  "transport_unavailable",
701
701
  504,
702
- `HTTP/2 stream read-idle timeout after ${this.readIdleMs}ms — connection presumed dead`,
702
+ `HTTP/2 stream read-idle timeout after ${this.readIdleMs}ms — connection presumed dead (request {requestId}). The connection ghosted us — come back in a moment 👻`,
703
703
  {},
704
704
  "network",
705
705
  ),
@@ -836,7 +836,7 @@ function openBidiStream(
836
836
  new GatewayError(
837
837
  "transport_unavailable",
838
838
  502,
839
- `HTTP/2 session failed: ${error.message}`,
839
+ `HTTP/2 session failed: ${error.message} (request {requestId}). The connection ghosted us — come back in a moment 👻`,
840
840
  {},
841
841
  "network",
842
842
  ),
@@ -871,7 +871,7 @@ function openBidiStream(
871
871
  // Already destroying.
872
872
  }
873
873
  reject(
874
- new GatewayError("transport_unavailable", 502, "HTTP/2 session timed out", {}, "network"),
874
+ new GatewayError("transport_unavailable", 502, "HTTP/2 session timed out (request {requestId}). Some things just take longer 🐢", {}, "network"),
875
875
  );
876
876
  }, HTTP2_CONNECT_TIMEOUT_MS);
877
877
  session.once("error", onError);
@@ -895,7 +895,7 @@ class WsSession implements ProviderWebSocketSession {
895
895
  this.#signal = signal;
896
896
  socket.on("message", (data: RawData, isBinary: boolean) => {
897
897
  if (isBinary) {
898
- this.#fail(new GatewayError("transport_unavailable", 502, "unexpected binary WebSocket frame"));
898
+ this.#fail(new GatewayError("transport_unavailable", 502, "unexpected binary WebSocket frame (request {requestId}). The connection ghosted us — come back in a moment 👻"));
899
899
  return;
900
900
  }
901
901
  const message = data.toString();
@@ -907,14 +907,14 @@ class WsSession implements ProviderWebSocketSession {
907
907
  this.#messages.push(message);
908
908
  }
909
909
  });
910
- socket.on("close", () => this.#fail(new GatewayError("transport_closed", 502, "WebSocket connection closed")));
911
- socket.on("error", () => this.#fail(new GatewayError("transport_unavailable", 502, "WebSocket connection failed")));
910
+ socket.on("close", () => this.#fail(new GatewayError("transport_closed", 502, "WebSocket connection closed (request {requestId}). The connection ghosted us — come back in a moment 👻")));
911
+ socket.on("error", () => this.#fail(new GatewayError("transport_unavailable", 502, "WebSocket connection failed (request {requestId}). The connection ghosted us — come back in a moment 👻")));
912
912
  }
913
913
 
914
914
  send(message: string): void {
915
915
  if (this.#closed) throw this.#closed;
916
916
  if (this.#socket.readyState !== WebSocket.OPEN) {
917
- throw new GatewayError("transport_closed", 499, "WebSocket connection is not open");
917
+ throw new GatewayError("transport_closed", 499, "WebSocket connection is not open (request {requestId}). Send it again whenever ready 🙃");
918
918
  }
919
919
  this.#socket.send(message, { binary: false });
920
920
  }
@@ -927,11 +927,11 @@ class WsSession implements ProviderWebSocketSession {
927
927
  return new Promise<string>((resolve, reject) => {
928
928
  const timeout = setTimeout(() => {
929
929
  cleanup();
930
- reject(new GatewayError("transport_unavailable", 504, "WebSocket receive timed out"));
930
+ reject(new GatewayError("transport_unavailable", 504, "WebSocket receive timed out (request {requestId}). Some things just take longer 🐢"));
931
931
  }, 45_000);
932
932
  const onAbort = (): void => {
933
933
  cleanup();
934
- reject(new GatewayError("transport_closed", 499, "WebSocket receive was cancelled"));
934
+ reject(new GatewayError("transport_closed", 499, "WebSocket receive was cancelled (request {requestId}). Send it again whenever ready 🙃"));
935
935
  };
936
936
  const cleanup = (): void => {
937
937
  clearTimeout(timeout);
@@ -997,7 +997,7 @@ export class ValidatedNetworkBindingFactory {
997
997
  throw new GatewayError(
998
998
  "proxy_pool_unhealthy",
999
999
  503,
1000
- "Configured proxy pool could not be established.",
1000
+ "Configured proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧",
1001
1001
  { poolId: networkPoolId, reason: error.message.slice(0, 200) },
1002
1002
  "network",
1003
1003
  );
@@ -1009,27 +1009,27 @@ export class ValidatedNetworkBindingFactory {
1009
1009
  webSocket(networkPoolId?: string, tenantId?: string): ValidatedOutboundWebSocket {
1010
1010
  return async (url, headers, signal) => {
1011
1011
  if (url.protocol !== "wss:") {
1012
- throw new GatewayError("invalid_request", 400, "validated WebSocket egress requires wss");
1012
+ throw new GatewayError("invalid_request", 400, "validated WebSocket egress requires wss (request {requestId}). Check the shape of what you sent — then resend 🪞");
1013
1013
  }
1014
1014
  if (url.username || url.password || url.hash || (url.port !== "" && url.port !== "443")) {
1015
- throw new GatewayError("invalid_request", 400, "invalid validated WebSocket target");
1015
+ throw new GatewayError("invalid_request", 400, "invalid validated WebSocket target (request {requestId}). Check the shape of what you sent — then resend 🪞");
1016
1016
  }
1017
1017
  const hostname = url.hostname;
1018
- if (!hostname) throw new GatewayError("invalid_request", 400, "WebSocket target hostname is required");
1018
+ if (!hostname) throw new GatewayError("invalid_request", 400, "WebSocket target hostname is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
1019
1019
  const destination = await this.resolve(hostname, signal, 443);
1020
1020
  if (!isAddressAllowed(destination.resolvedAddress, this.policy)) {
1021
- throw new GatewayError("invalid_request", 400, "unsafe upstream address rejected");
1021
+ throw new GatewayError("invalid_request", 400, "unsafe upstream address rejected (request {requestId}). Check the shape of what you sent — then resend 🪞");
1022
1022
  }
1023
1023
  let agent: PoolAgent | undefined;
1024
1024
  if (networkPoolId || tenantId) {
1025
1025
  if (!networkPoolId || !tenantId || !this.poolResolver) {
1026
- throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool could not be established");
1026
+ throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧");
1027
1027
  }
1028
1028
  try {
1029
1029
  agent = await this.poolResolver.resolveWebSocketAgent(networkPoolId, tenantId);
1030
1030
  } catch (error) {
1031
1031
  if (error instanceof PoolBindingError) {
1032
- throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool could not be established");
1032
+ throw new GatewayError("proxy_pool_unhealthy", 503, "configured WebSocket proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧");
1033
1033
  }
1034
1034
  throw error;
1035
1035
  }
@@ -1058,7 +1058,7 @@ export class ValidatedNetworkBindingFactory {
1058
1058
  assertBidiTarget(url);
1059
1059
  const { hostname } = url;
1060
1060
  if (options?.allowHostname !== undefined && !options.allowHostname(hostname)) {
1061
- throw new GatewayError("invalid_request", 400, "HTTP/2 target host is not allowlisted");
1061
+ throw new GatewayError("invalid_request", 400, "HTTP/2 target host is not allowlisted (request {requestId}). All roads blocked — time to find a new route 🗺️");
1062
1062
  }
1063
1063
  const signal = options?.signal ?? AbortSignal.timeout(HTTP2_CONNECT_TIMEOUT_MS);
1064
1064
  // Validation runs on every new connection, not once per capability: a
@@ -1066,7 +1066,7 @@ export class ValidatedNetworkBindingFactory {
1066
1066
  // resolved safely the first time is not safe the twentieth.
1067
1067
  const destination = await this.resolve(hostname, signal, 443);
1068
1068
  if (!isAddressAllowed(destination.resolvedAddress, this.policy)) {
1069
- throw new GatewayError("invalid_request", 400, "unsafe upstream address rejected");
1069
+ throw new GatewayError("invalid_request", 400, "unsafe upstream address rejected (request {requestId}). All roads blocked — time to find a new route 🗺️");
1070
1070
  }
1071
1071
  // Mirrors `fetch`: no pool selected (or none configured) means validated
1072
1072
  // *direct* egress, which is already resolved and SSRF-checked above.
@@ -1086,7 +1086,7 @@ export class ValidatedNetworkBindingFactory {
1086
1086
  throw new GatewayError(
1087
1087
  "proxy_pool_unhealthy",
1088
1088
  503,
1089
- "Configured proxy pool could not be established.",
1089
+ "Configured proxy pool could not be established (request {requestId}). This bridge looks shaky — repair or reroute 🔧",
1090
1090
  { poolId: networkPoolId, reason: error.message.slice(0, 200) },
1091
1091
  "network",
1092
1092
  );
@@ -18,6 +18,7 @@ import type { CartethyiaDatabase } from "../persistence/db-handle";
18
18
  import { networkPools, healthEvents } from "../persistence/schema";
19
19
  import { log } from "../observability/logger";
20
20
  import { providerRateLimitIsIpScoped } from "../providers/provider-metadata";
21
+ import { resolvePoolProviderRateLimitCooldownMs } from "../config/cooldowns";
21
22
  import { PROVIDER_BLOCK_MS, type ProxyCooldownEntry } from "./pool/selector";
22
23
 
23
24
  type PoolProviderCooldownMap = NonNullable<
@@ -120,6 +121,37 @@ export async function dropAllProviderCooldownMarkers(
120
121
  }
121
122
  }
122
123
 
124
+ /**
125
+ * Removes expired provider markers from one pool row, keeping live cooldowns
126
+ * and permanent blocks intact. Returns how many markers were dropped.
127
+ *
128
+ * The selector and Redis prune expired entries on read, but the durable JSON
129
+ * mirror is only ever written, never swept — so a marker that expired while
130
+ * the process was down survives on the row forever. Harmless to routing (boot
131
+ * restores only future-dated markers), but it bloats the row and makes the
132
+ * backup/restore surface carry dead state.
133
+ */
134
+ export async function pruneExpiredProviderCooldownMarkers(
135
+ db: CartethyiaDatabase,
136
+ poolId: string,
137
+ ): Promise<number> {
138
+ try {
139
+ const map = await readMarkerMap(db, poolId);
140
+ const now = Date.now();
141
+ const expiredKeys: string[] = [];
142
+ for (const [providerKey, marker] of Object.entries(map)) {
143
+ if (marker.until <= now) expiredKeys.push(providerKey);
144
+ }
145
+ if (expiredKeys.length === 0) return 0;
146
+ for (const providerKey of expiredKeys) delete map[providerKey];
147
+ await writeMarkerMap(db, poolId, map);
148
+ return expiredKeys.length;
149
+ } catch (error) {
150
+ log.warn("pool provider cooldown mirror prune failed", { networkPoolId: poolId, error: String(error) });
151
+ return 0;
152
+ }
153
+ }
154
+
123
155
  /**
124
156
  * Writes a probe-measured verdict the same way a dispatch-side event would be
125
157
  * recorded: one mirror marker and one audit row.
@@ -201,19 +233,22 @@ function probeEndpointOf(endpointConfig: unknown): string {
201
233
  /**
202
234
  * Re-enforces the stored probe verdicts at boot (new: replay, read-ahead).
203
235
  *
204
- * A stored `blocked` verdict is a *state*, not a decoration. If the marker the
205
- * probe flagged failed to persist, or the verdict predated the marker column,
206
- * the egress would return to rotation after restart and get refused again —
207
- * dispatch spending a real request to rediscover a refusal the row already
208
- * knew. So the boot read reconciles: a fresh (endpoint still matches) `blocked`
209
- * verdict for an address-keyed provider becomes a block flag, even when the
210
- * mirror has nothing (or has an older, cooler entry — the retirement claim is
211
- * the sharper fact and wins).
236
+ * A stored verdict is a *state*, not a decoration. If the marker the probe
237
+ * flagged failed to persist, or the verdict predated the marker column, the
238
+ * egress would return to rotation after restart and get refused again — dispatch
239
+ * spending a real request to rediscover a refusal the row already knew. So the
240
+ * boot read reconciles:
212
241
  *
213
- * `throttled` is deliberately **not** replayed into a cooldown: the cooldown
214
- * the probe flagged already carries the measured window, and blind-replaying
215
- * one from a verdict that predates the restart could outlive the upstream's
216
- * actual window.
242
+ * - A fresh `blocked` verdict for an address-keyed provider becomes a block
243
+ * flag, even when the mirror has nothing (or has an older, cooler entry — the
244
+ * retirement claim is the sharper fact and wins).
245
+ * - A fresh `throttled` verdict for an address-keyed provider becomes the same
246
+ * cooldown a dispatch-side 429 would have written. The probe measured the
247
+ * rate-limit window itself (`provider_probe_at` is when it observed the 429),
248
+ * so the replayed window starts at that measurement, not at boot — without
249
+ * the monotonic clock, a restart mid-window would shorten the cool-off the
250
+ * probe proved. The configured default window is applied when the probe did
251
+ * not record a duration.
217
252
  *
218
253
  * Returns how many verdicts were replayed.
219
254
  */
@@ -221,6 +256,7 @@ export async function restorePoolProbeVerdicts(
221
256
  selector: { hydrateProviderCooldown(entry: ProxyCooldownEntry): void },
222
257
  db: CartethyiaDatabase,
223
258
  ): Promise<number> {
259
+ const probeCooldownMs = resolvePoolProviderRateLimitCooldownMs();
224
260
  const rows = await db
225
261
  .select({
226
262
  id: networkPools.id,
@@ -230,21 +266,40 @@ export async function restorePoolProbeVerdicts(
230
266
  providerProbeStatus: networkPools.providerProbeStatus,
231
267
  providerProbeReason: networkPools.providerProbeReason,
232
268
  providerProbeEndpoint: networkPools.providerProbeEndpoint,
269
+ providerProbeAt: networkPools.providerProbeAt,
233
270
  })
234
271
  .from(networkPools);
235
272
  let replayed = 0;
236
273
  for (const row of rows) {
237
274
  if (row.status !== "active") continue;
238
- if (row.providerProbeStatus !== "blocked") continue;
275
+ if (row.providerProbeStatus !== "blocked" && row.providerProbeStatus !== "throttled") continue;
239
276
  if (row.providerProbeEndpoint !== probeEndpointOf(row.endpointConfig)) continue;
240
277
  const providerId = row.providerProbeProviderId;
241
278
  if (typeof providerId !== "string" || !providerRateLimitIsIpScoped(providerId)) continue;
279
+ if (row.providerProbeStatus === "blocked") {
280
+ selector.hydrateProviderCooldown({
281
+ poolId: row.id,
282
+ providerId: providerId.toLowerCase(),
283
+ until: Date.now() + PROVIDER_BLOCK_MS,
284
+ reason: row.providerProbeReason ?? "Upstream refused this egress address",
285
+ blocked: true,
286
+ });
287
+ replayed += 1;
288
+ continue;
289
+ }
290
+ // `throttled`: replay the cooldown the probe measured. The upstream's
291
+ // window began when the probe saw the 429, so the marker expires at
292
+ // `probeAt + window`, unless that is already past — an allowance that
293
+ // refilled while the process was down must not be resurrected.
294
+ const probeAt = row.providerProbeAt?.getTime();
295
+ const until =
296
+ probeAt !== undefined ? probeAt + probeCooldownMs : Date.now() + probeCooldownMs;
297
+ if (until <= Date.now()) continue;
242
298
  selector.hydrateProviderCooldown({
243
299
  poolId: row.id,
244
300
  providerId: providerId.toLowerCase(),
245
- until: Date.now() + PROVIDER_BLOCK_MS,
246
- reason: row.providerProbeReason ?? "Upstream refused this egress address",
247
- blocked: true,
301
+ until,
302
+ reason: row.providerProbeReason ?? "Operator probe measured a provider 429",
248
303
  });
249
304
  replayed += 1;
250
305
  }
@@ -5,6 +5,7 @@ import type { CartethyiaDatabase } from "../persistence/db-handle";
5
5
  import { healthEvents, networkPools } from "../persistence/schema";
6
6
  import { drainPoolByteDelta } from "./pool/byte-accounting";
7
7
  import { resolvePoolCooldownMs } from "../config/cooldowns";
8
+ import { dropProviderCooldownMarker, pruneExpiredProviderCooldownMarkers } from "./pool-cooldown-store";
8
9
  export interface PoolHealthEvent {
9
10
  readonly id: string;
10
11
  readonly networkPoolId: string;
@@ -54,7 +55,14 @@ function poolFaultCategory(error: unknown, origin: string | undefined): string |
54
55
  export async function recordPoolDispatchOutcome(
55
56
  db: CartethyiaDatabase,
56
57
  poolId: string,
57
- input: { readonly succeeded: boolean; readonly error?: unknown; readonly errorOrigin?: string; readonly snapshotInvalidator?: { invalidate(): unknown } },
58
+ input: {
59
+ readonly succeeded: boolean;
60
+ readonly error?: unknown;
61
+ readonly errorOrigin?: string;
62
+ /** The provider the attempt dialed; used to clear its stale egress marker on success. */
63
+ readonly providerId?: string;
64
+ readonly snapshotInvalidator?: { invalidate(): unknown };
65
+ },
58
66
  ): Promise<void> {
59
67
  const category = input.succeeded ? undefined : poolFaultCategory(input.error, input.errorOrigin);
60
68
  // Metered bytes are banked even when health is untouched: an upstream-origin
@@ -95,6 +103,14 @@ export async function recordPoolDispatchOutcome(
95
103
  lastErrorCategory = null;
96
104
  lastErrorAt = null;
97
105
  lastSuccessAt = now;
106
+ // A completed dispatch proves the egress is serving again for this
107
+ // provider — clear its stale rate-limit marker (the selector's local
108
+ // entry and the Redis copy) so a spent pool is not kept out of rotation
109
+ // after it demonstrably recovered. Fire-and-forget: a failed clear must
110
+ // never fail a completed request.
111
+ if (input.providerId !== undefined) {
112
+ void dropProviderCooldownMarker(db, poolId, input.providerId).catch(() => undefined);
113
+ }
98
114
  } else {
99
115
  failures += 1;
100
116
  // Every failed probe parks the pool: a pool that cannot carry traffic is
@@ -252,5 +268,9 @@ export async function sweepExpiredPoolCooldowns(db: CartethyiaDatabase): Promise
252
268
  await db.update(networkPools).set({ status: "active", consecutiveFailures: 0, cooldownUntil: null, lastRecoveredAt: now }).where(inArray(networkPools.id, expired.map((row) => row.id)));
253
269
  await db.insert(healthEvents).values(expired.map((row) => ({ id: randomUUID(), entityKind: "pool" as const, networkPoolId: row.id, fromStatus: row.status, toStatus: "active" as const, reason: "Cooldown elapsed — auto-recovered", errorCategory: null, createdAt: now })));
254
270
  for (const row of expired) publishPoolHealth({ poolId: row.id, tenantId: row.tenantId, status: "active", consecutiveFailures: 0 });
271
+ // The pool-level cooldown elapsed, so the same window's provider markers are
272
+ // expired too. Drop them from the durable JSON mirror — the selector and
273
+ // Redis prune on read, but the row's stale copy would otherwise live forever.
274
+ await Promise.all(expired.map((row) => pruneExpiredProviderCooldownMarkers(db, row.id)));
255
275
  return expired.length;
256
276
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Unified egress-failure classification for ip-scoped, rate-limited providers.
3
+ *
4
+ * One 429 is read in several places — dispatch (`attempt-loop`, streaming
5
+ * `finalize`), the provider probe, account health, and the pool health machine
6
+ * each had their own vocabulary. The two questions that decide whether a probe
7
+ * *pool* (not an account) is responsible for a failure are:
8
+ *
9
+ * - Did the upstream count this request against the egress address? Only a
10
+ * provider whose limits follow the IP (`ipScopedRateLimit`) qualifies — for
11
+ * everyone else the 429 belongs to the credential that was dialed, and
12
+ * cooling the pool would sideline healthy accounts.
13
+ * - Is the address itself retired (free-tier 403) rather than merely
14
+ * throttled (429)? A block is terminal; a cooldown refills.
15
+ *
16
+ * This module is the single source of truth for those two facts. Dispatch and
17
+ * the probe both read it, so the operator's "test" and gateway's own dial
18
+ * cannot disagree about which egress is spent.
19
+ */
20
+ import { GatewayError } from "../protocol/gateway-error";
21
+ import { providerRateLimitIsIpScoped } from "../providers/provider-metadata";
22
+
23
+ /** What one upstream failure means for the egress that carried it. */
24
+ export type EgressVerdict =
25
+ | { kind: "none" }
26
+ | { kind: "cooldown"; retryAfterMs?: number }
27
+ | { kind: "blocked" };
28
+
29
+ /** The upstream's own wording for a retired free-tier address. */
30
+ export const FREE_TIER_REFUSAL = /freetier|free tier can only be used/i;
31
+
32
+ /**
33
+ * Classifies one upstream failure against one provider.
34
+ *
35
+ * `providerId` is the caller's own candidate, not a field read off the error:
36
+ * the in-stream error frames the protocol modules build carry no `providerId`
37
+ * detail, so reading it from `details` would silently disable the IP-scoped
38
+ * rule on exactly the streaming path it exists for.
39
+ */
40
+ export function classifyEgressFailure(error: unknown, providerId: string): EgressVerdict {
41
+ if (!(error instanceof GatewayError) || error.origin !== "upstream") {
42
+ return { kind: "none" };
43
+ }
44
+ if (!providerRateLimitIsIpScoped(providerId)) return { kind: "none" };
45
+
46
+ // A provider-scoped 429 exhausted the address's allowance: cool the pool
47
+ // until the window refills.
48
+ if (error.status === 429 && error.details.rateLimitScope === "provider") {
49
+ const retryAfterMs =
50
+ typeof error.details.retryAfterMs === "number" ? error.details.retryAfterMs : undefined;
51
+ return retryAfterMs === undefined ? { kind: "cooldown" } : { kind: "cooldown", retryAfterMs };
52
+ }
53
+
54
+ // A free-tier 403 retires the address: the request was never read, so the
55
+ // same egress cannot succeed no matter what the payload says. Deliberately
56
+ // narrower than "any 403" — a content-policy or licence refusal is also a
57
+ // 403 and must not retire a healthy proxy.
58
+ if (error.status === 403) {
59
+ const providerCode =
60
+ typeof error.details.providerCode === "string" ? error.details.providerCode : "";
61
+ const raw = String(error.details.raw ?? "");
62
+ if (/freetier|free.?tier/i.test(providerCode) || FREE_TIER_REFUSAL.test(`${error.message} ${raw}`)) {
63
+ return { kind: "blocked" };
64
+ }
65
+ }
66
+
67
+ return { kind: "none" };
68
+ }
@@ -170,7 +170,7 @@ export function validateResolvedAddresses(
170
170
  policy: SsrfPolicy,
171
171
  ): string {
172
172
  if (addresses.length === 0 || addresses.some((address) => !isAddressAllowed(address, policy))) {
173
- throw new GatewayError("invalid_request", 400, "The upstream address this route resolves to is not allowed. Point the provider at a permitted host.");
173
+ throw new GatewayError("invalid_request", 400, "The upstream address this route resolves to is not allowed (request {requestId}). Point the provider at a permitted host. All roads blocked — time to find a new route 🗺️");
174
174
  }
175
175
  return addresses[0]!;
176
176
  }
@@ -227,7 +227,7 @@ export async function resolveAllAddresses(
227
227
  throw new GatewayError(
228
228
  "transport_unavailable",
229
229
  502,
230
- "upstream DNS resolution failed",
230
+ "upstream DNS resolution failed (request {requestId}). The connection ghosted us — come back in a moment 👻",
231
231
  { cause: String(error) },
232
232
  "network",
233
233
  );
@@ -238,8 +238,8 @@ function abortErrorForSignal(signal: AbortSignal): GatewayError {
238
238
  const reason = signal.reason;
239
239
  if (reason instanceof GatewayError) return reason;
240
240
  if (reason instanceof DOMException && reason.name === "TimeoutError")
241
- return new GatewayError("deadline_exceeded", 504, "request deadline exceeded", {}, "cartethyia");
242
- return new GatewayError("transport_closed", 499, "request was cancelled");
241
+ return new GatewayError("deadline_exceeded", 504, "request deadline exceeded (request {requestId}). Some things just take longer 🐢", {}, "cartethyia");
242
+ return new GatewayError("transport_closed", 499, "The request was cancelled (request {requestId}). Send it again whenever ready 🙃");
243
243
  }
244
244
 
245
245
  export async function resolveAndValidateOnce(