cartethyia 2.4.4 → 2.4.6

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 (304) hide show
  1. package/.env.example +4 -1
  2. package/README.md +18 -0
  3. package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
  4. package/dist/dashboard/assets/{BannedPage-TDue2X0p.js → BannedPage-QhvbJB9o.js} +1 -1
  5. package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolDetailPage-qaHXuiI5.js → CliToolDetailPage-Bcy150Jo.js} +1 -1
  7. package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
  8. package/dist/dashboard/assets/{CliToolsPage-BFuQBKez.js → CliToolsPage-DyNXefph.js} +1 -1
  9. package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
  10. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +1 -0
  11. package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js +1 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
  14. package/dist/dashboard/assets/{CustomizationPage-BhglcqyR.js → CustomizationPage-D6jEU7nL.js} +1 -1
  15. package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
  16. package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js +1 -0
  17. package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-CeqROJrT.js → LoginPage-BJlMVN4U.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
  20. package/dist/dashboard/assets/ModelPicker-Br3CXEau.js +1 -0
  21. package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-CRXSAITY.js → OverviewPage-CQxXbo2g.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +3 -0
  25. package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
  26. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
  27. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +1 -0
  28. package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
  29. package/dist/dashboard/assets/ProxyPage-CXiumA53.js +7 -0
  30. package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
  31. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +1 -0
  32. package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
  33. package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-BHW4Zt06.js} +1 -1
  34. package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
  35. package/dist/dashboard/assets/SetupPage-VOYC_lIa.js +1 -0
  36. package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
  37. package/dist/dashboard/assets/SortableList-DpOTnGuH.js +1 -0
  38. package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
  39. package/dist/dashboard/assets/StudioPage-BNIDLiU6.js +31 -0
  40. package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
  41. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  42. package/dist/dashboard/assets/{UsagePage-B6LA4ewF.js → UsagePage-BAFaSDQV.js} +14 -14
  43. package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
  44. package/dist/dashboard/assets/api-keys-BETfaMYZ.js +1 -0
  45. package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
  46. package/dist/dashboard/assets/app-BLCvGdzb.css +2 -0
  47. package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
  48. package/dist/dashboard/assets/{app-BDJWMThu.js → app-Cjq7nrwu.js} +3 -3
  49. package/dist/dashboard/assets/app-Cjq7nrwu.js.gz +0 -0
  50. package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
  51. package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
  52. package/dist/dashboard/assets/{assert-Bwh6DJTK.js → assert-7J6_LVD3.js} +1 -1
  53. package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
  54. package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
  55. package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-BTyGk_DL.js} +1 -1
  56. package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
  57. package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-bujTWYM4.js} +1 -1
  58. package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
  59. package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
  60. package/dist/dashboard/assets/{card-BTdbRFFg.js → card-B0Bnx5CA.js} +1 -1
  61. package/dist/dashboard/assets/card-B0Bnx5CA.js.gz +0 -0
  62. package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
  63. package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
  64. package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
  66. package/dist/dashboard/assets/cli-tools-DE2ug812.js +1 -0
  67. package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
  68. package/dist/dashboard/assets/clipboard-button-R_rArH8S.js +1 -0
  69. package/dist/dashboard/assets/clipboard-button-R_rArH8S.js.gz +0 -0
  70. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  71. package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
  72. package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BWIP_EvW.js} +1 -1
  73. package/dist/dashboard/assets/dialog-BWIP_EvW.js.gz +0 -0
  74. package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
  75. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  76. package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
  77. package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-BKcgVugF.js} +1 -1
  78. package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
  79. package/dist/dashboard/assets/entry-7dpgvpJ3.js +4 -0
  80. package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
  81. package/dist/dashboard/assets/entry-CcWkGR4a.js +1 -0
  82. package/dist/dashboard/assets/entry-CcWkGR4a.js.gz +0 -0
  83. package/dist/dashboard/assets/entry-kTiTFrvh.js +1 -0
  84. package/dist/dashboard/assets/entry-kTiTFrvh.js.gz +0 -0
  85. package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
  86. package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
  87. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  88. package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
  89. package/dist/dashboard/assets/github-badge-Booc5mNe.js +1 -0
  90. package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
  91. package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
  92. package/dist/dashboard/assets/{index-BA_ni9Ix.js → index-D70pChfn.js} +2 -2
  93. package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
  94. package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1TCqM2D.js} +1 -1
  95. package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
  96. package/dist/dashboard/assets/{input-Bn4_kNav.js → input-B9oyU93y.js} +1 -1
  97. package/dist/dashboard/assets/input-B9oyU93y.js.gz +0 -0
  98. package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
  99. package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
  100. package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
  101. package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BArzXirQ.js} +1 -1
  102. package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
  103. package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-BVZzaoHF.js} +1 -1
  104. package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
  105. package/dist/dashboard/assets/{live-YlifPYox.js → live-EmuIayqp.js} +1 -1
  106. package/dist/dashboard/assets/live-EmuIayqp.js.gz +0 -0
  107. package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
  108. package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
  109. package/dist/dashboard/assets/{model-bans-CVvHZ-qz.js → model-bans-CSPQ9xB0.js} +1 -1
  110. package/dist/dashboard/assets/model-bans-CSPQ9xB0.js.gz +0 -0
  111. package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
  112. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  113. package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
  114. package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
  115. package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
  116. package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
  117. package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
  118. package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
  119. package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
  120. package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
  121. package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
  122. package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
  123. package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
  124. package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
  125. package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
  126. package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
  127. package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
  128. package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
  129. package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
  130. package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
  131. package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
  132. package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
  133. package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
  134. package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
  135. package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
  136. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  137. package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
  138. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  139. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  140. package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
  141. package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
  142. package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
  143. package/dist/dashboard/assets/{routing-jntw9-Ui.js → routing-BExgxxBU.js} +1 -1
  144. package/dist/dashboard/assets/routing-BExgxxBU.js.gz +0 -0
  145. package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
  146. package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
  147. package/dist/dashboard/assets/{select-CGUuTQmj.js → select-C9mMoEpJ.js} +1 -1
  148. package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
  149. package/dist/dashboard/assets/settings-C4_Gn_wq.js +1 -0
  150. package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
  151. package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
  152. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
  153. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  154. package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-Ca5HxI5i.js} +1 -1
  155. package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
  156. package/dist/dashboard/assets/{state-DHKdg-pm.js → state-CzUPfLXo.js} +1 -1
  157. package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
  158. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  159. package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-DOUIky_i.js} +1 -1
  160. package/dist/dashboard/assets/switch-DOUIky_i.js.gz +0 -0
  161. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
  162. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  163. package/dist/dashboard/assets/toast-C76WrMhc.js +2 -0
  164. package/dist/dashboard/assets/toast-C76WrMhc.js.gz +0 -0
  165. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  166. package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
  167. package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
  168. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  169. package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
  170. package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
  171. package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-D87FZwH0.js} +1 -1
  172. package/dist/dashboard/assets/useMutation-D87FZwH0.js.gz +0 -0
  173. package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
  174. package/dist/dashboard/index.html +1 -1
  175. package/dist/dashboard/index.html.gz +0 -0
  176. package/dist/dashboard/precompressed-assets.json +123 -0
  177. package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
  178. package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
  179. package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
  180. package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
  181. package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
  182. package/package.json +25 -30
  183. package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
  184. package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
  185. package/scripts/cli/command/admin.ts +65 -0
  186. package/scripts/cli/command/console-menu.ts +227 -0
  187. package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
  188. package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
  189. package/scripts/cli/command/start.ts +13 -0
  190. package/scripts/cli/command/tray.ts +165 -0
  191. package/scripts/cli/command/update.ts +23 -0
  192. package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
  193. package/scripts/cli/gateway/spawn.ts +101 -0
  194. package/scripts/cli/gateway/stop.ts +51 -0
  195. package/scripts/cli/main.ts +94 -0
  196. package/scripts/cli/support/bun.ts +27 -0
  197. package/scripts/cli/support/env.ts +99 -0
  198. package/scripts/cli/support/platform.ts +64 -0
  199. package/scripts/cli/support/services.ts +88 -0
  200. package/scripts/cli/support/startup-signal.ts +23 -0
  201. package/scripts/cli/support/version.ts +37 -0
  202. package/scripts/cli/tray/hidden-window.vbs +28 -0
  203. package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
  204. package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
  205. package/scripts/cli/ui/background.ts +49 -0
  206. package/src/boot/app.ts +3 -1
  207. package/src/boot/dependencies.ts +36 -13
  208. package/src/config/providers.ts +27 -1
  209. package/src/config/spec.ts +11 -0
  210. package/src/console/backup/routes.ts +8 -0
  211. package/src/console/cli-tools/contracts.ts +6 -6
  212. package/src/console/console-router.ts +5 -3
  213. package/src/console/dashboard-assets.ts +81 -11
  214. package/src/console/observability/contracts.ts +54 -22
  215. package/src/console/observability/routes.ts +31 -6
  216. package/src/console/observability/store.ts +68 -28
  217. package/src/console/providers/catalog/public-model-store.ts +91 -60
  218. package/src/console/providers/catalog/store.ts +5 -1
  219. package/src/console/quota/account-quota-tenant.ts +9 -6
  220. package/src/console/response-compression.ts +69 -2
  221. package/src/console/routing/pools/probe-verdict.ts +72 -0
  222. package/src/console/share/share-router.ts +7 -4
  223. package/src/console/share/share-stats.ts +12 -5
  224. package/src/console/shared/read-through-cache.ts +133 -7
  225. package/src/console/updates/repository-updates.ts +39 -34
  226. package/src/network/outbound-fetch.ts +113 -25
  227. package/src/network/tls/bun-client-hello.ts +112 -0
  228. package/src/observability/event-log.ts +29 -8
  229. package/src/observability/metrics.ts +40 -13
  230. package/src/observability/runtime-metrics.ts +7 -0
  231. package/src/persistence/schema.ts +40 -2
  232. package/src/protocol/response/codex.ts +9 -75
  233. package/src/protocol/tool-identity.ts +43 -9
  234. package/src/providers/authentication/jwt-validator.ts +130 -187
  235. package/src/providers/authentication/oauth-flow-store.ts +16 -9
  236. package/src/providers/default-registry.ts +4 -0
  237. package/src/providers/discovery/catalog/catalog-format.ts +20 -0
  238. package/src/providers/discovery/models-dev-catalog.ts +39 -22
  239. package/src/providers/discovery/probe-wire.ts +29 -5
  240. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +3 -2
  241. package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
  242. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +4 -0
  243. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
  244. package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
  245. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
  246. package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
  247. package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -2
  248. package/src/providers/integrations/buddy/codebuddy/shared.ts +106 -73
  249. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
  250. package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
  251. package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
  252. package/src/providers/integrations/codex/codex.ts +5 -8
  253. package/src/providers/integrations/factory/factory-oauth.ts +7 -11
  254. package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
  255. package/src/providers/integrations/opencode/provider.ts +42 -3
  256. package/src/providers/integrations/qoder/qoder-oauth.ts +3 -9
  257. package/src/providers/integrations/shared/install-id.ts +29 -2
  258. package/src/providers/operations/client-versions.ts +11 -1
  259. package/src/providers/operations/model-catalog-cache.ts +18 -3
  260. package/src/providers/operations/provider-catalog-service.ts +76 -0
  261. package/src/transport/dispatch/attempt-finalize.ts +7 -1
  262. package/src/transport/dispatch/attempt-loop.ts +9 -7
  263. package/src/transport/dispatch/empty-stream.ts +17 -26
  264. package/src/transport/failure-policy.ts +20 -0
  265. package/src/transport/routing/route-catalog.ts +42 -3
  266. package/src/transport/surface/chat/encode.ts +18 -31
  267. package/src/transport/surface/messages/stream.ts +3 -2
  268. package/src/transport/translation/tool-repair.ts +37 -16
  269. package/src/workers/account-identity-sweep.ts +14 -12
  270. package/src/workers/daily-checkin.ts +62 -0
  271. package/src/workers/quota-refresh-sweep.ts +27 -2
  272. package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
  273. package/dist/dashboard/assets/ConsoleLogPage-vkZBxMVo.js +0 -1
  274. package/dist/dashboard/assets/HealthEventsModal-DDkHfwpV.js +0 -1
  275. package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
  276. package/dist/dashboard/assets/ProviderDetailPage-cNAD_6uN.js +0 -3
  277. package/dist/dashboard/assets/ProvidersPage-Cv5tdxlx.js +0 -1
  278. package/dist/dashboard/assets/ProxyPage-CWvMfzjj.js +0 -7
  279. package/dist/dashboard/assets/QuotaPage-IYQOOHR4.js +0 -1
  280. package/dist/dashboard/assets/SetupPage-BPDNo3Ek.js +0 -1
  281. package/dist/dashboard/assets/SortableList-Bol1ASmI.js +0 -1
  282. package/dist/dashboard/assets/StudioPage-BE5DJONZ.js +0 -31
  283. package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
  284. package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
  285. package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
  286. package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
  287. package/dist/dashboard/assets/entry-DTiWhIY-.js +0 -4
  288. package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
  289. package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
  290. package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
  291. package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
  292. package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
  293. package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
  294. package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
  295. package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
  296. package/scripts/cli/cli.ts +0 -766
  297. package/scripts/cli/version.ts +0 -66
  298. package/scripts/operations/probe-egress.ts +0 -335
  299. package/scripts/operations/restart.ts +0 -120
  300. package/scripts/shared/env.ts +0 -269
  301. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
  302. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
  303. /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
  304. /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
@@ -13,47 +13,46 @@
13
13
  * Only asymmetric algorithms are accepted. `none` and HMAC (`HS*`) are
14
14
  * rejected so a token cannot downgrade to a shared-secret or unsigned form.
15
15
  */
16
- import { constants, createPublicKey, verify as cryptoVerify } from "node:crypto";
17
- import type { KeyObject } from "node:crypto";
16
+ import {
17
+ compactVerify,
18
+ createRemoteJWKSet,
19
+ customFetch,
20
+ decodeJwt,
21
+ decodeProtectedHeader,
22
+ } from "jose";
23
+ import type { JSONWebKeySet, RemoteJWKSet } from "jose";
18
24
  import { TtlCache } from "../../persistence/ttl-cache";
19
25
  import { resolveRedisClient } from "../../persistence/redis";
20
26
 
27
+ /**
28
+ * Algorithms this module will verify.
29
+ *
30
+ * `jose` performs the cryptography, so the entry is now only the key type —
31
+ * the digest, RSA-PSS padding and salt length were implementation details of
32
+ * the hand-rolled `cryptoVerify` call this replaced. The allowlist itself is
33
+ * unchanged: asymmetric only, so `none` and the HMAC (`HS*`) family can never
34
+ * be negotiated down to.
35
+ */
21
36
  interface AlgorithmSpec {
22
- readonly hash: string;
23
37
  readonly kty: "RSA" | "EC";
24
- /** RSA-PSS padding, when the algorithm is PS*; RSA-PKCS1 is the default. */
25
- readonly padding?: number;
26
- readonly saltLength?: number;
27
38
  }
28
39
 
29
- /** Algorithms this module will verify, mapped to their digest and key type. */
30
40
  const SUPPORTED_ALGORITHMS: Readonly<Record<string, AlgorithmSpec>> = Object.freeze({
31
- RS256: { hash: "sha256", kty: "RSA" },
32
- RS384: { hash: "sha384", kty: "RSA" },
33
- RS512: { hash: "sha512", kty: "RSA" },
34
- PS256: {
35
- hash: "sha256",
36
- kty: "RSA",
37
- padding: constants.RSA_PKCS1_PSS_PADDING,
38
- saltLength: constants.RSA_PSS_SALTLEN_DIGEST,
39
- },
40
- PS384: {
41
- hash: "sha384",
42
- kty: "RSA",
43
- padding: constants.RSA_PKCS1_PSS_PADDING,
44
- saltLength: constants.RSA_PSS_SALTLEN_DIGEST,
45
- },
46
- PS512: {
47
- hash: "sha512",
48
- kty: "RSA",
49
- padding: constants.RSA_PKCS1_PSS_PADDING,
50
- saltLength: constants.RSA_PSS_SALTLEN_DIGEST,
51
- },
52
- ES256: { hash: "sha256", kty: "EC" },
53
- ES384: { hash: "sha384", kty: "EC" },
54
- ES512: { hash: "sha512", kty: "EC" },
41
+ RS256: { kty: "RSA" },
42
+ RS384: { kty: "RSA" },
43
+ RS512: { kty: "RSA" },
44
+ PS256: { kty: "RSA" },
45
+ PS384: { kty: "RSA" },
46
+ PS512: { kty: "RSA" },
47
+ ES256: { kty: "EC" },
48
+ ES384: { kty: "EC" },
49
+ ES512: { kty: "EC" },
55
50
  });
56
51
 
52
+ /** Fetch timeout for a JWKS document, matching jose's own default. */
53
+ const JWKS_FETCH_TIMEOUT_MS = 5_000;
54
+
55
+
57
56
  interface ParsedJwt {
58
57
  readonly header: Record<string, unknown>;
59
58
  readonly payload: Record<string, unknown>;
@@ -61,23 +60,17 @@ interface ParsedJwt {
61
60
  readonly signedInput: string;
62
61
  }
63
62
 
64
- function decodeJsonObject(segment: string): Record<string, unknown> | undefined {
65
- if (segment.length === 0 || !/^[A-Za-z0-9_-]+$/.test(segment)) return undefined;
66
- try {
67
- const value = JSON.parse(Buffer.from(segment, "base64url").toString("utf8")) as unknown;
68
- return value !== null && typeof value === "object" && !Array.isArray(value)
69
- ? (value as Record<string, unknown>)
70
- : undefined;
71
- } catch {
72
- return undefined;
73
- }
74
- }
75
-
76
63
  /**
77
64
  * Strictly parses a compact JWS. Returns `undefined` for anything that is not a
78
65
  * three-part token with JSON header/payload objects, a non-empty `alg`, and a
79
66
  * non-empty signature — so `alg: none` and structurally broken tokens are never
80
67
  * treated as JWTs.
68
+ *
69
+ * Header and payload decoding delegates to `jose`, which enforces the same
70
+ * strictness the hand-rolled decoder did (compact three-part shape, base64url
71
+ * decoding to a JSON *object* — never an array or a scalar) and is the same
72
+ * decoder the verification path uses, so what is inspected is exactly what is
73
+ * later verified.
81
74
  */
82
75
  export function parseJwt(token: string): ParsedJwt | undefined {
83
76
  if (typeof token !== "string") return undefined;
@@ -86,9 +79,14 @@ export function parseJwt(token: string): ParsedJwt | undefined {
86
79
  const headerSegment = parts[0] ?? "";
87
80
  const payloadSegment = parts[1] ?? "";
88
81
  const signature = parts[2] ?? "";
89
- const header = decodeJsonObject(headerSegment);
90
- const payload = decodeJsonObject(payloadSegment);
91
- if (!header || !payload) return undefined;
82
+ let header: Record<string, unknown>;
83
+ let payload: Record<string, unknown>;
84
+ try {
85
+ header = decodeProtectedHeader(token) as Record<string, unknown>;
86
+ payload = decodeJwt(token) as Record<string, unknown>;
87
+ } catch {
88
+ return undefined;
89
+ }
92
90
  if (typeof header.alg !== "string" || header.alg.length === 0) return undefined;
93
91
  if (header.alg === "none" || signature.length === 0) return undefined;
94
92
  return { header, payload, signature, signedInput: `${headerSegment}.${payloadSegment}` };
@@ -167,14 +165,6 @@ export function validateJwtClaims(
167
165
  return undefined;
168
166
  }
169
167
 
170
- interface Jwk {
171
- readonly kty?: unknown;
172
- readonly kid?: unknown;
173
- readonly use?: unknown;
174
- readonly alg?: unknown;
175
- readonly [key: string]: unknown;
176
- }
177
-
178
168
  interface JwksVerifierOptions {
179
169
  readonly url: string;
180
170
  readonly fetch?: typeof fetch;
@@ -186,191 +176,144 @@ interface JwksVerifierOptions {
186
176
 
187
177
  const DEFAULT_JWKS_CACHE_TTL_MS = 5 * 60 * 1_000;
188
178
  const DEFAULT_MIN_REFRESH_INTERVAL_MS = 30_000;
189
- const MAX_JWKS_KEYS = 64;
190
179
 
191
180
  /** Shared JWKS window, matching the local verifier cache so peers reuse fetched keys. */
192
181
  const JWKS_SHARED_TTL_SECONDS = 300;
193
182
  const JWKS_SHARED_KEY_PREFIX = "jwks:v1:";
194
183
 
195
- /** Parses a cached JWKS value into keys, or `undefined` when it is unusable. */
196
- function parseSharedJwks(raw: string): readonly Jwk[] | undefined {
197
- try {
198
- const parsed = JSON.parse(raw) as unknown;
199
- if (!Array.isArray(parsed)) return undefined;
200
- const keys = parsed.filter((key): key is Jwk => key !== null && typeof key === "object");
201
- return keys.length > 0 ? keys : undefined;
202
- } catch {
203
- return undefined;
204
- }
184
+
185
+ /**
186
+ * A JWKS document shared with peer instances, with the timestamp it was
187
+ * fetched at. Redis-backed so a fleet does not each re-fetch the issuer.
188
+ */
189
+ interface SharedJwks {
190
+ readonly jwks: JSONWebKeySet;
191
+ readonly uat: number;
205
192
  }
206
193
 
207
- /** Reads a peer instance's cached JWKS. Never throws; a miss falls through to fetch. */
208
- async function readSharedJwks(url: string): Promise<readonly Jwk[] | undefined> {
194
+ async function readSharedJwksDocument(url: string): Promise<SharedJwks | undefined> {
209
195
  const redis = resolveRedisClient();
210
196
  if (!redis) return undefined;
211
197
  try {
212
198
  const hit = await redis.get(`${JWKS_SHARED_KEY_PREFIX}${url}`);
213
- return hit === null ? undefined : parseSharedJwks(hit);
199
+ if (hit === null) return undefined;
200
+ const parsed = JSON.parse(hit) as unknown;
201
+ if (parsed === null || typeof parsed !== "object") return undefined;
202
+ const { uat, jwks } = parsed as { uat?: unknown; jwks?: unknown };
203
+ if (typeof uat !== "number" || !Number.isFinite(uat)) return undefined;
204
+ if (jwks === null || typeof jwks !== "object") return undefined;
205
+ return { jwks: jwks as JSONWebKeySet, uat };
214
206
  } catch {
215
207
  return undefined;
216
208
  }
217
209
  }
218
210
 
219
- /** Publishes fetched keys for peer instances. Best-effort: a failure is not a request failure. */
220
- async function writeSharedJwks(url: string, keys: readonly Jwk[]): Promise<void> {
211
+ /** Publishes a fetched document for peer instances. Best-effort: never throws. */
212
+ async function writeSharedJwksDocument(url: string, jwks: JSONWebKeySet): Promise<void> {
221
213
  const redis = resolveRedisClient();
222
214
  if (!redis) return;
223
215
  try {
224
216
  await redis.set(
225
217
  `${JWKS_SHARED_KEY_PREFIX}${url}`,
226
- JSON.stringify(keys),
218
+ JSON.stringify({ uat: Date.now(), jwks } satisfies SharedJwks),
227
219
  "EX",
228
220
  JWKS_SHARED_TTL_SECONDS,
229
221
  );
230
222
  } catch {
231
- // Shared visibility only; the local cache stays authoritative.
223
+ // Shared visibility only; each instance's own cache stays authoritative.
232
224
  }
233
225
  }
234
226
 
235
- interface CachedJwks {
236
- readonly keys: readonly Jwk[];
237
- readonly fetchedAt: number;
238
- }
239
-
240
- function publicKeyFromJwk(jwk: Jwk): KeyObject | undefined {
241
- try {
242
- return createPublicKey({ key: jwk as unknown as JsonWebKey, format: "jwk" });
243
- } catch {
244
- return undefined;
245
- }
227
+ function jsonResponse(body: unknown): Response {
228
+ return new Response(JSON.stringify(body), {
229
+ status: 200,
230
+ headers: { "content-type": "application/json" },
231
+ });
246
232
  }
247
233
 
248
234
  /**
249
- * Verifies compact-JWS signatures against a provider's JWKS endpoint. Keys are
250
- * cached with a TTL and bounded count; an unknown `kid` triggers at most one
251
- * rate-limited refresh so key rotation is picked up without hammering the
252
- * issuer.
235
+ * Verifies compact-JWS signatures against a provider's JWKS endpoint.
236
+ *
237
+ * Key fetching, caching, in-flight deduplication, the minimum refresh interval
238
+ * on an unknown `kid`, and `kid`-rotation pickup are all delegated to
239
+ * `jose`'s `createRemoteJWKSet` — the same algorithm this class used to
240
+ * reimplement by hand. `jose` additionally honours each JWK's `use` and
241
+ * `key_ops`, so an encryption key published alongside the signing keys cannot
242
+ * be selected for verification.
243
+ *
244
+ * The algorithm is checked against `SUPPORTED_ALGORITHMS` *before* any key is
245
+ * resolved, so the asymmetric-only allowlist (no `none`, no HMAC) is
246
+ * unchanged by the migration.
253
247
  */
254
248
  export class JwksVerifier {
255
- readonly #url: string;
256
- readonly #fetch: typeof fetch;
257
- readonly #clock: () => number;
258
- readonly #cacheTtlMs: number;
259
- readonly #minRefreshIntervalMs: number;
260
- #cache: CachedJwks | undefined;
261
- #inflight: Promise<readonly Jwk[]> | undefined;
262
- #lastRefreshAt = 0;
249
+ readonly #keys: RemoteJWKSet;
250
+ /** Whether this instance has fetched from the issuer yet. */
251
+ #fetched = false;
263
252
 
264
253
  constructor(options: JwksVerifierOptions) {
265
- this.#url = options.url;
266
- this.#fetch = options.fetch ?? globalThis.fetch;
267
- this.#clock = options.clock ?? (() => Date.now());
268
- this.#cacheTtlMs = options.cacheTtlMs ?? DEFAULT_JWKS_CACHE_TTL_MS;
269
- this.#minRefreshIntervalMs = options.minRefreshIntervalMs ?? DEFAULT_MIN_REFRESH_INTERVAL_MS;
270
- }
271
-
272
- async #fetchKeysRemote(): Promise<readonly Jwk[]> {
273
- const response = await this.#fetch(this.#url, {
274
- headers: { accept: "application/json" },
275
- signal: AbortSignal.timeout(5_000),
254
+ const fetchFn = options.fetch ?? globalThis.fetch;
255
+ const url = options.url;
256
+ const cacheTtlMs = options.cacheTtlMs ?? DEFAULT_JWKS_CACHE_TTL_MS;
257
+ this.#keys = createRemoteJWKSet(new URL(url), {
258
+ timeoutDuration: JWKS_FETCH_TIMEOUT_MS,
259
+ cacheMaxAge: cacheTtlMs,
260
+ cooldownDuration: options.minRefreshIntervalMs ?? DEFAULT_MIN_REFRESH_INTERVAL_MS,
261
+ [customFetch]: async (target, init) => {
262
+ // First resolution only: a peer's copy saves an issuer round-trip.
263
+ // A later fetch is a rotation reload, which must reach the issuer
264
+ // rather than re-read the set that just failed to match the `kid`.
265
+ if (!this.#fetched) {
266
+ const shared = await readSharedJwksDocument(url);
267
+ if (shared !== undefined && Date.now() - shared.uat < cacheTtlMs) {
268
+ this.#fetched = true;
269
+ return jsonResponse(shared.jwks);
270
+ }
271
+ }
272
+ const response = await fetchFn(target, init as RequestInit);
273
+ if (response.ok) {
274
+ try {
275
+ const body = (await response.clone().json()) as unknown;
276
+ if (body !== null && typeof body === "object" && Array.isArray((body as { keys?: unknown }).keys)) {
277
+ void writeSharedJwksDocument(url, body as JSONWebKeySet);
278
+ }
279
+ } catch {
280
+ // Publishing is best-effort.
281
+ }
282
+ return response;
283
+ }
284
+ return response;
285
+ },
276
286
  });
277
- if (!response.ok) throw new Error(`JWKS request failed (${response.status})`);
278
- const body = (await response.json()) as unknown;
279
- const keys = body !== null && typeof body === "object" ? (body as { keys?: unknown }).keys : undefined;
280
- if (!Array.isArray(keys)) throw new Error("JWKS response missing keys array");
281
- return keys.slice(0, MAX_JWKS_KEYS).filter(
282
- (key): key is Jwk => key !== null && typeof key === "object",
283
- );
284
287
  }
285
288
 
286
289
  /**
287
- * Resolves keys for this URL, reading a peer instance's shared copy first on a
288
- * normal read. A forced refresh — the unknown-`kid` retry that picks up
289
- * rotation — skips the shared copy and fetches the issuer, then republishes,
290
- * so rotation is not delayed by the shared TTL.
290
+ * Verifies `token`'s signature and returns the parsed token on success.
291
+ *
292
+ * Returns `undefined` for anything unverifiable — a non-JWT, an algorithm
293
+ * outside the allowlist, a JWKS fetch failure, or a bad signature. A JWKS
294
+ * outage therefore fails closed rather than trusting an unverified token.
291
295
  */
292
- async #fetchKeys(useSharedCache: boolean): Promise<readonly Jwk[]> {
293
- if (useSharedCache) {
294
- const shared = await readSharedJwks(this.#url);
295
- if (shared !== undefined) return shared;
296
- }
297
- const keys = await this.#fetchKeysRemote();
298
- await writeSharedJwks(this.#url, keys);
299
- return keys;
300
- }
301
-
302
- async #getKeys(forceRefresh: boolean): Promise<readonly Jwk[]> {
303
- const now = this.#clock();
304
- if (!forceRefresh && this.#cache && now - this.#cache.fetchedAt < this.#cacheTtlMs) {
305
- return this.#cache.keys;
306
- }
307
- if (this.#inflight) return this.#inflight;
308
- const request = this.#fetchKeys(!forceRefresh)
309
- .then((keys) => {
310
- this.#cache = { keys, fetchedAt: this.#clock() };
311
- this.#lastRefreshAt = this.#clock();
312
- return keys;
313
- })
314
- .finally(() => {
315
- this.#inflight = undefined;
316
- });
317
- this.#inflight = request;
318
- return request;
319
- }
320
-
321
- /** Verifies `token`'s signature and returns the parsed token on success. */
322
296
  async verify(token: string): Promise<ParsedJwt | undefined> {
323
297
  const parsed = parseJwt(token);
324
298
  if (!parsed) return undefined;
325
299
  const algorithm = SUPPORTED_ALGORITHMS[parsed.header.alg as string];
326
300
  if (!algorithm) return undefined;
327
-
328
- const kid = typeof parsed.header.kid === "string" ? parsed.header.kid : undefined;
329
- let keys: readonly Jwk[];
330
301
  try {
331
- keys = await this.#getKeys(false);
302
+ // `compactVerify` checks the signature only. Registered claims stay the
303
+ // responsibility of `validateJwtClaims`, whose failure vocabulary
304
+ // ("expired", "issuer_mismatch", …) the console surfaces verbatim;
305
+ // `jwtVerify` would replace it with its own, and requires `iss`/`aud`
306
+ // that an OAuth access token need not carry.
307
+ await compactVerify(token, this.#keys, {
308
+ algorithms: [parsed.header.alg as string],
309
+ });
310
+ return parsed;
332
311
  } catch {
333
312
  return undefined;
334
313
  }
335
- let candidates = selectKeys(keys, kid, algorithm.kty);
336
- if (candidates.length === 0 && this.#clock() - this.#lastRefreshAt >= this.#minRefreshIntervalMs) {
337
- try {
338
- candidates = selectKeys(await this.#getKeys(true), kid, algorithm.kty);
339
- } catch {
340
- return undefined;
341
- }
342
- }
343
- if (candidates.length === 0) return undefined;
344
-
345
- const data = Buffer.from(parsed.signedInput, "utf8");
346
- const signature = Buffer.from(parsed.signature, "base64url");
347
- for (const jwk of candidates) {
348
- const key = publicKeyFromJwk(jwk);
349
- if (!key) continue;
350
- const verifyOptions =
351
- algorithm.kty === "EC"
352
- ? { key, dsaEncoding: "ieee-p1363" as const }
353
- : algorithm.padding === undefined || algorithm.saltLength === undefined
354
- ? { key }
355
- : { key, padding: algorithm.padding, saltLength: algorithm.saltLength };
356
- try {
357
- if (cryptoVerify(algorithm.hash, data, verifyOptions, signature)) return parsed;
358
- } catch {
359
- continue;
360
- }
361
- }
362
- return undefined;
363
314
  }
364
315
  }
365
316
 
366
- function selectKeys(keys: readonly Jwk[], kid: string | undefined, kty: "RSA" | "EC"): readonly Jwk[] {
367
- return keys.filter((jwk) => {
368
- if (jwk.kty !== kty) return false;
369
- if (kid !== undefined && jwk.kid !== kid) return false;
370
- return true;
371
- });
372
- }
373
-
374
317
  /**
375
318
  * One verifier per URL, bounded and time-limited.
376
319
  *
@@ -1,6 +1,7 @@
1
1
  // OAuth client contracts, registries, PKCE state, and ephemeral flow storage.
2
2
 
3
3
  import { createHash, randomBytes } from "node:crypto";
4
+ import { base64url, decodeJwt } from "jose";
4
5
  import type { RedisClient } from "../../persistence/redis";
5
6
  import { TtlCache } from "../../persistence/ttl-cache";
6
7
 
@@ -286,8 +287,14 @@ interface PkcePair {
286
287
  readonly codeChallenge: string;
287
288
  }
288
289
 
290
+ /**
291
+ * Base64url without padding, via `jose`.
292
+ *
293
+ * Shared so PKCE verifiers/challenges and provider-specific encoders cannot
294
+ * drift apart on padding or alphabet handling.
295
+ */
289
296
  export function base64UrlEncode(input: Buffer): string {
290
- return input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
297
+ return base64url.encode(input);
291
298
  }
292
299
 
293
300
  /** Generates a fresh PKCE verifier/challenge pair using the S256 transform. */
@@ -398,17 +405,17 @@ export function expiryFromSeconds(value: unknown, fallbackSec = 3600): Date {
398
405
 
399
406
  /**
400
407
  * Decodes a JWT payload without verifying (OAuth metadata only — the trust
401
- * boundary stays at TLS + issuer). Strict 3-part shape with an object
402
- * (non-array) payload; anything else yields `undefined`.
408
+ * boundary stays at TLS + issuer).
409
+ *
410
+ * Delegates to `jose`'s `decodeJwt`, which enforces exactly the contract
411
+ * callers rely on: compact three-part shape, a base64url payload decoding to
412
+ * UTF-8 JSON that is an object (not an array, not a scalar). Anything else
413
+ * yields `undefined` rather than throwing — a credential that is simply not a
414
+ * JWT (an opaque token) is a normal outcome here, not an error.
403
415
  */
404
416
  export function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
405
- const parts = token.split(".");
406
- if (parts.length !== 3) return undefined;
407
417
  try {
408
- const value = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8")) as unknown;
409
- return value !== null && typeof value === "object" && !Array.isArray(value)
410
- ? (value as Record<string, unknown>)
411
- : undefined;
418
+ return decodeJwt(token) as Record<string, unknown>;
412
419
  } catch {
413
420
  return undefined;
414
421
  }
@@ -274,6 +274,10 @@ export const PROVIDER_CAPABILITIES = {
274
274
  loadAdapter: async () => createApiKeyAdapter((await import("./integrations/opencode/provider")).OPENCODE_GO_SPEC),
275
275
  loadModels: async () => (await import("./integrations/opencode/provider")).OPENCODE_GO_MODELS,
276
276
  loadModelDiscovery: openAIModelDiscovery("opencode-go", { transformBaseUrl: (baseUrl) => `${baseUrl.replace(/\/+$/, "")}/zen/go/v1`, headers: (credential) => ({ authorization: `Bearer ${credential}` }) }),
277
+ // The Go plan publishes a real usage surface; the anonymous free tier has
278
+ // none (it is rate-limited by egress IP, not by a balance), so this is
279
+ // deliberately registered on the keyed tier only.
280
+ loadQuotaCollector: async () => (await import("./integrations/opencode/opencode-go-quota")).fetchOpenCodeGoQuota,
277
281
  },
278
282
  agentrouter: {
279
283
  loadAdapter: async () => (await import("./integrations/agentrouter/agentrouter")).createAgentRouterAdapter(),
@@ -166,6 +166,16 @@ const decoder = new TextDecoder();
166
166
 
167
167
  const FLAG_REASONING = 1;
168
168
  const FLAG_TOOL_CALL = 2;
169
+
170
+ /**
171
+ * Ceiling on {@link PackedCatalog}'s row-metadata cache.
172
+ *
173
+ * Sized well under the catalog's 8941 rows: a gateway resolves a working set of
174
+ * models, not the whole catalog, and every entry is rebuilt from the blob in a
175
+ * few typed-array reads. Holding more than this trades RAM for reads measured
176
+ * in nanoseconds.
177
+ */
178
+ export const METADATA_CACHE_MAX_ENTRIES = 2048;
169
179
  const SLASH = 0x2f;
170
180
 
171
181
  /** Lowercases and trims an ASCII id into the exact bytes the blob stores. */
@@ -182,6 +192,12 @@ export function lowerAscii(value: string): Uint8Array {
182
192
  * Holds typed-array views (zero-copy) and nothing else. Metadata objects are
183
193
  * created only for rows a caller actually resolves, and cached, so a long-lived
184
194
  * process pays for the handful of ids it queried rather than for all 8090.
195
+ *
196
+ * That cache is **bounded**: an entry is rebuilt from the blob in a few
197
+ * typed-array reads, so holding one at the cost of another is never worth it,
198
+ * and an unbounded memo over 8941 rows is memory the operator pays for at idle
199
+ * whether or not those rows are ever queried again. Bounded LRU, oldest
200
+ * insertion evicted first — the same shape the rest of the codebase uses.
185
201
  */
186
202
  export class PackedCatalog {
187
203
  private readonly bytes: Uint8Array;
@@ -399,6 +415,10 @@ export class PackedCatalog {
399
415
  reasoning: (flags & FLAG_REASONING) !== 0,
400
416
  toolCall: (flags & FLAG_TOOL_CALL) !== 0,
401
417
  };
418
+ if (this.metadataCache.size >= METADATA_CACHE_MAX_ENTRIES) {
419
+ const oldest = this.metadataCache.keys().next().value;
420
+ if (oldest !== undefined) this.metadataCache.delete(oldest);
421
+ }
402
422
  this.metadataCache.set(index, metadata);
403
423
  return metadata;
404
424
  }
@@ -134,25 +134,38 @@ const UNKNOWN_COST: ModelsDevModelCost = {
134
134
  };
135
135
 
136
136
  /**
137
- * Decoded once, at module load.
137
+ * The packed catalog, decompressed on first use and memoized.
138
138
  *
139
139
  * Brotli + base64, not `JSON.parse`: the blob is columnar, so this allocates
140
140
  * one buffer and takes typed-array views over it rather than materialising
141
141
  * 8900 objects. `CATALOG_BLOB_BYTES` is the decompressed length, checked here
142
- * so a consumer paired with a half-regenerated blob fails at boot rather than
143
- * resolving a truncated catalog.
142
+ * so a consumer paired with a half-regenerated blob fails on first access
143
+ * rather than resolving a truncated catalog.
144
+ *
145
+ * The decode is lazy on purpose. It is ~105 KB of base64 that expands to a
146
+ * much larger buffer, and it used to run at module load — so every process,
147
+ * including the ones that never resolve a model (a CLI subcommand, a migration
148
+ * run, a test that only touches the console), paid it on the boot path.
149
+ * Deferring it moves that cost to the first catalog lookup and pays it once.
144
150
  */
145
- const CATALOG_BYTES = brotliDecompressSync(Buffer.from(CATALOG_BLOB_BROTLI_BASE64, "base64"));
146
- if (CATALOG_BYTES.byteLength !== CATALOG_BLOB_BYTES) {
147
- throw new Error(
148
- `The packed model catalog is ${CATALOG_BYTES.byteLength} B, expected ` +
149
- `${CATALOG_BLOB_BYTES} B — regenerate it.`,
150
- );
151
- }
151
+ let catalogCache: PackedCatalog | undefined;
152
152
 
153
- // `PackedCatalog` copies into a 4-byte-aligned buffer of its own, so the
154
- // decompressed buffer is dropped here rather than held beside the copy.
155
- const CATALOG = new PackedCatalog(CATALOG_BYTES);
153
+ function catalog(): PackedCatalog {
154
+ const cached = catalogCache;
155
+ if (cached !== undefined) return cached;
156
+ const bytes = brotliDecompressSync(Buffer.from(CATALOG_BLOB_BROTLI_BASE64, "base64"));
157
+ if (bytes.byteLength !== CATALOG_BLOB_BYTES) {
158
+ throw new Error(
159
+ `The packed model catalog is ${bytes.byteLength} B, expected ` +
160
+ `${CATALOG_BLOB_BYTES} B — regenerate it.`,
161
+ );
162
+ }
163
+ // `PackedCatalog` copies into a 4-byte-aligned buffer of its own, so the
164
+ // decompressed buffer is dropped here rather than held beside the copy.
165
+ const packed = new PackedCatalog(bytes);
166
+ catalogCache = packed;
167
+ return packed;
168
+ }
156
169
 
157
170
  /**
158
171
  * Resolves model metadata and pricing exclusively from the packed base catalog
@@ -160,9 +173,13 @@ const CATALOG = new PackedCatalog(CATALOG_BYTES);
160
173
  */
161
174
  export class ModelsDevCatalog {
162
175
  /** When the generator fetched the upstream catalogs. */
163
- readonly fetchedAt: string = CATALOG.fetchedAt;
176
+ get fetchedAt(): string {
177
+ return catalog().fetchedAt;
178
+ }
164
179
  /** Every provider key the catalog files. */
165
- readonly providers: readonly string[] = CATALOG.providers;
180
+ get providers(): readonly string[] {
181
+ return catalog().providers;
182
+ }
166
183
 
167
184
  /**
168
185
  * Metadata for an exact `(providerIndex, idLower)` pair, or `undefined`.
@@ -172,19 +189,19 @@ export class ModelsDevCatalog {
172
189
  * matches exactly keeps winning.
173
190
  */
174
191
  private exactMetadata(providerLower: string, idLower: Uint8Array): ModelsDevModelMetadata | undefined {
175
- const direct = CATALOG.exactIndex(CATALOG.providerIndex(providerLower), idLower);
176
- if (direct >= 0) return CATALOG.metadataAt(direct);
192
+ const direct = catalog().exactIndex(catalog().providerIndex(providerLower), idLower);
193
+ if (direct >= 0) return catalog().metadataAt(direct);
177
194
  const mapped = CATALOG_PROVIDER_IDS[providerLower];
178
195
  if (mapped === undefined) return undefined;
179
- return CATALOG.metadataAt(CATALOG.exactIndex(CATALOG.providerIndex(mapped), idLower));
196
+ return catalog().metadataAt(catalog().exactIndex(catalog().providerIndex(mapped), idLower));
180
197
  }
181
198
 
182
199
  /** Metadata rows whose *bare* id equals `bare`, in catalog order. */
183
200
  private metadataForBare(bare: Uint8Array): ModelsDevModelMetadata[] {
184
- const [start, end] = CATALOG.bareRange(bare);
201
+ const [start, end] = catalog().bareRange(bare);
185
202
  const out: ModelsDevModelMetadata[] = [];
186
203
  for (let position = start; position < end; position += 1) {
187
- const metadata = CATALOG.metadataAt(CATALOG.bareOrderAt(position));
204
+ const metadata = catalog().metadataAt(catalog().bareOrderAt(position));
188
205
  if (metadata !== undefined) out.push(metadata);
189
206
  }
190
207
  return out;
@@ -327,8 +344,8 @@ export class ModelsDevCatalog {
327
344
  return own;
328
345
  }
329
346
  const idLower = lowerAscii(modelId);
330
- const row = CATALOG.globalCostRow(bareBytes(idLower));
331
- const global = row >= 0 ? CATALOG.rowCost(row) : undefined;
347
+ const row = catalog().globalCostRow(bareBytes(idLower));
348
+ const global = row >= 0 ? catalog().rowCost(row) : undefined;
332
349
  if (global !== undefined && global.pricing_model !== "unknown") return global;
333
350
  return own ?? global ?? UNKNOWN_COST;
334
351
  }
@@ -172,17 +172,41 @@ export function staticEndpointForWire(
172
172
  return catalog.get(providerId)?.find((def) => def.wireFamily === wireFamily)?.endpointPath;
173
173
  }
174
174
 
175
+ /**
176
+ * Compiled-pattern memo.
177
+ *
178
+ * `matchModelWireFamily` runs per model on every discovery pass and on every
179
+ * manual registration, and it used to compile each rule's `RegExp` inside that
180
+ * loop — so the same handful of patterns were recompiled once per model per
181
+ * pass. Keyed by the pattern source, which is exactly what `new RegExp(source)`
182
+ * derives from.
183
+ *
184
+ * A pattern that fails to compile is not memoized; the tolerance stays at read
185
+ * time because an invalid pattern is rejected at write time, and a stored one
186
+ * must not throw during a listing.
187
+ */
188
+ const compiledPatternCache = new Map<string, RegExp>();
189
+
190
+ function compilePattern(pattern: string): RegExp | undefined {
191
+ const cached = compiledPatternCache.get(pattern);
192
+ if (cached !== undefined) return cached;
193
+ try {
194
+ const compiled = new RegExp(pattern);
195
+ compiledPatternCache.set(pattern, compiled);
196
+ return compiled;
197
+ } catch {
198
+ return undefined;
199
+ }
200
+ }
201
+
175
202
  function matchModelWireFamily(
176
203
  modelId: string,
177
204
  rules: ReadonlyArray<{ pattern: string; wire_family: WireFamily }> | undefined,
178
205
  ): WireFamily | undefined {
179
206
  if (!rules?.length) return undefined;
180
207
  for (const rule of rules) {
181
- try {
182
- if (new RegExp(rule.pattern).test(modelId)) return rule.wire_family as WireFamily;
183
- } catch {
184
- // Invalid pattern is rejected at write time; be tolerant at read time.
185
- }
208
+ const pattern = compilePattern(rule.pattern);
209
+ if (pattern !== undefined && pattern.test(modelId)) return rule.wire_family as WireFamily;
186
210
  }
187
211
  return undefined;
188
212
  }