cartethyia 2.4.5 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (415) hide show
  1. package/.env.example +4 -1
  2. package/README.md +63 -13
  3. package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
  4. package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
  5. package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
  7. package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
  8. package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-D3xqZbN-.js} +1 -1
  9. package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
  10. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
  11. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js +1 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
  14. package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-CTTpCb25.js} +1 -1
  15. package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js +1 -0
  17. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-DHoB3Agn.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
  20. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +1 -0
  21. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  22. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
  23. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js.gz +0 -0
  26. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
  27. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
  31. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-CvjZxHE4.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
  36. package/dist/dashboard/assets/SetupPage-D3nza7LH.js +1 -0
  37. package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
  38. package/dist/dashboard/assets/{SortableList-Bol1ASmI.js → SortableList-Dtt12WT1.js} +1 -1
  39. package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
  40. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js +31 -0
  41. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
  42. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  43. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
  44. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
  45. package/dist/dashboard/assets/api-keys-BmDbsior.js +1 -0
  46. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  47. package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
  48. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  49. package/dist/dashboard/assets/{app-BDJWMThu.js → app-C-Y4oZT0.js} +3 -3
  50. package/dist/dashboard/assets/app-C-Y4oZT0.js.gz +0 -0
  51. package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
  52. package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
  53. package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
  54. package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
  55. package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
  56. package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-DpXT4Ixt.js} +1 -1
  57. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  58. package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-CSAAWLJv.js} +1 -1
  59. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  60. package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
  61. package/dist/dashboard/assets/{card-BTdbRFFg.js → card-DEhRXXw4.js} +1 -1
  62. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  63. package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
  64. package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
  66. package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
  67. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js +1 -0
  68. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  69. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +1 -0
  70. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  71. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  72. package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
  73. package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BvQeBMfP.js} +1 -1
  74. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  75. package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
  76. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  77. package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
  78. package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-CMwPXwQB.js} +1 -1
  79. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  80. package/dist/dashboard/assets/entry-D4O2229X.js +1 -0
  81. package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
  82. package/dist/dashboard/assets/entry-DKvr5Q9K.js +1 -0
  83. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  84. package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
  85. package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
  86. package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
  87. package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
  88. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  89. package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
  90. package/dist/dashboard/assets/github-badge-zliLvBwr.js +1 -0
  91. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  92. package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
  93. package/dist/dashboard/assets/{index-C4hXfAMs.js → index-CXm3FIyt.js} +2 -2
  94. package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
  95. package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1IlZZ8H.js} +1 -1
  96. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  97. package/dist/dashboard/assets/{input-Bn4_kNav.js → input-BpVZdrbc.js} +1 -1
  98. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  99. package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
  100. package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
  101. package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
  102. package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BioLW7gN.js} +1 -1
  103. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  104. package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-CwVcGAIc.js} +1 -1
  105. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  106. package/dist/dashboard/assets/{live-YlifPYox.js → live-yfEJFF9T.js} +1 -1
  107. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  108. package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
  109. package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
  110. package/dist/dashboard/assets/model-bans-CT-TxSNX.js +1 -0
  111. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  112. package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
  113. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  114. package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
  115. package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
  116. package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
  117. package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
  118. package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
  119. package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
  120. package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
  121. package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
  122. package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
  123. package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
  124. package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
  125. package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
  126. package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
  127. package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
  128. package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
  129. package/dist/dashboard/assets/providers/cursor.webp +0 -0
  130. package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
  131. package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
  132. package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
  133. package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
  134. package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
  135. package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
  136. package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
  137. package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
  138. package/dist/dashboard/assets/providers/trae.webp +0 -0
  139. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  140. package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
  141. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  142. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  143. package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
  144. package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
  145. package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
  146. package/dist/dashboard/assets/routing-Cs6jl49L.js +1 -0
  147. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  148. package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
  149. package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
  150. package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
  151. package/dist/dashboard/assets/select-C0Rer3gF.js.gz +0 -0
  152. package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
  153. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  154. package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
  155. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  156. package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-C9_uUjaA.js} +1 -1
  157. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  158. package/dist/dashboard/assets/{state-DHKdg-pm.js → state-DA0B95UQ.js} +1 -1
  159. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  160. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  161. package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-Cittxy-x.js} +1 -1
  162. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  163. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
  164. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  165. package/dist/dashboard/assets/toast-CNt_jzO4.js +2 -0
  166. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  167. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  168. package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
  169. package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
  170. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  171. package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
  172. package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
  173. package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-BQKMcJ1H.js} +1 -1
  174. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
  175. package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
  176. package/dist/dashboard/index.html +1 -1
  177. package/dist/dashboard/index.html.gz +0 -0
  178. package/dist/dashboard/precompressed-assets.json +122 -0
  179. package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
  180. package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
  181. package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
  182. package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
  183. package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
  184. package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
  185. package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
  186. package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
  187. package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
  188. package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
  189. package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
  190. package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
  191. package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
  192. package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
  193. package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
  194. package/package.json +26 -30
  195. package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
  196. package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
  197. package/scripts/cli/command/admin.ts +65 -0
  198. package/scripts/cli/command/console-menu.ts +246 -0
  199. package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
  200. package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
  201. package/scripts/cli/command/start.ts +77 -0
  202. package/scripts/cli/command/tray.ts +165 -0
  203. package/scripts/cli/command/update.ts +23 -0
  204. package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
  205. package/scripts/cli/gateway/instance-lock.ts +105 -0
  206. package/scripts/cli/gateway/spawn.ts +173 -0
  207. package/scripts/cli/gateway/stop.ts +51 -0
  208. package/scripts/cli/main.ts +94 -0
  209. package/scripts/cli/support/bun.ts +27 -0
  210. package/scripts/cli/support/env.ts +99 -0
  211. package/scripts/cli/support/platform.ts +64 -0
  212. package/scripts/cli/support/services.ts +88 -0
  213. package/scripts/cli/support/startup-signal.ts +23 -0
  214. package/scripts/cli/{version.ts → support/version.ts} +1 -1
  215. package/scripts/cli/tray/hidden-window.vbs +28 -0
  216. package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
  217. package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
  218. package/scripts/cli/ui/background.ts +110 -0
  219. package/src/boot/app.ts +23 -7
  220. package/src/boot/dependencies.ts +55 -13
  221. package/src/boot/elysia-typebox-bridge.ts +37 -0
  222. package/src/boot/lifecycle.ts +2 -0
  223. package/src/config/cooldowns.ts +138 -138
  224. package/src/config/providers.ts +31 -5
  225. package/src/config/runtime.ts +19 -3
  226. package/src/config/spec.ts +20 -0
  227. package/src/console/api-keys/contracts.ts +414 -391
  228. package/src/console/api-keys/routes.ts +880 -870
  229. package/src/console/auth/account-reset-store.ts +14 -0
  230. package/src/console/auth/account-reset.ts +3 -2
  231. package/src/console/auth/setup-service.ts +6 -3
  232. package/src/console/backup/routes.ts +8 -0
  233. package/src/console/cli-tools/contracts.ts +6 -6
  234. package/src/console/console-router.ts +5 -3
  235. package/src/console/dashboard-assets.ts +81 -11
  236. package/src/console/domain-registration.ts +9 -2
  237. package/src/console/observability/contracts.ts +475 -423
  238. package/src/console/observability/routes.ts +31 -6
  239. package/src/console/observability/store.ts +1015 -912
  240. package/src/console/providers/catalog/contracts.ts +31 -0
  241. package/src/console/providers/catalog/provider-operations.ts +8 -0
  242. package/src/console/providers/catalog/public-model-store.ts +91 -60
  243. package/src/console/providers/catalog/store.ts +1532 -1521
  244. package/src/console/providers/oauth/callback-listener.ts +18 -3
  245. package/src/console/providers/oauth/routes.ts +38 -15
  246. package/src/console/quota/account-quota-tenant.ts +9 -6
  247. package/src/console/quota/refresh.ts +19 -0
  248. package/src/console/response-compression.ts +69 -2
  249. package/src/console/routing/pools/probe-verdict.ts +72 -0
  250. package/src/console/share/share-router.ts +7 -4
  251. package/src/console/share/share-stats.ts +12 -5
  252. package/src/console/shared/read-through-cache.ts +133 -7
  253. package/src/console/updates/repository-updates.ts +39 -34
  254. package/src/console/updates/routes.ts +162 -4
  255. package/src/main.ts +3 -0
  256. package/src/network/http2-bidi.ts +85 -0
  257. package/src/network/outbound-fetch.ts +87 -24
  258. package/src/network/pool/agent.ts +478 -460
  259. package/src/network/pool/resolver.ts +1104 -544
  260. package/src/network/pool-health-machine.ts +12 -5
  261. package/src/observability/event-log.ts +29 -8
  262. package/src/observability/metrics.ts +40 -13
  263. package/src/observability/payload-capture.ts +344 -324
  264. package/src/observability/runtime-metrics.ts +7 -0
  265. package/src/persistence/db-pg.ts +9 -3
  266. package/src/persistence/db-sqlite.ts +497 -495
  267. package/src/persistence/health-event-flush.ts +137 -0
  268. package/src/persistence/migrate.ts +181 -129
  269. package/src/persistence/schema.ts +1360 -1257
  270. package/src/persistence/settings-revision.ts +25 -0
  271. package/src/persistence/shared-cache.ts +217 -0
  272. package/src/persistence/stores/share-store.ts +556 -528
  273. package/src/persistence/usage-flush.ts +61 -1
  274. package/src/protocol/canonical-model.ts +23 -1
  275. package/src/protocol/response/chat.ts +143 -10
  276. package/src/protocol/response/codex.ts +9 -75
  277. package/src/protocol/response/omp-tool-call-markup.ts +221 -0
  278. package/src/protocol/tool-identity.ts +43 -9
  279. package/src/providers/authentication/jwt-validator.ts +130 -187
  280. package/src/providers/authentication/oauth-flow-store.ts +16 -9
  281. package/src/providers/default-registry.ts +177 -1
  282. package/src/providers/discovery/catalog/catalog-format.ts +20 -0
  283. package/src/providers/discovery/models-dev-catalog.ts +39 -22
  284. package/src/providers/discovery/openai-model-discovery.ts +39 -3
  285. package/src/providers/discovery/openrouter-free-roster.ts +98 -0
  286. package/src/providers/discovery/probe-wire.ts +29 -5
  287. package/src/providers/discovery/probing-service.ts +1022 -1001
  288. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +42 -2
  289. package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
  290. package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
  291. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +22 -5
  292. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
  293. package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
  294. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
  295. package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
  296. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
  297. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
  298. package/src/providers/integrations/buddy/codebuddy/shared.ts +115 -73
  299. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
  300. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
  301. package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
  302. package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
  303. package/src/providers/integrations/claude/claude.ts +606 -582
  304. package/src/providers/integrations/cline/cline-quota.ts +18 -16
  305. package/src/providers/integrations/cline/cline.ts +539 -468
  306. package/src/providers/integrations/codex/codex.ts +5 -8
  307. package/src/providers/integrations/commandcode/commandcode.ts +675 -541
  308. package/src/providers/integrations/commandcode/discovery.ts +116 -0
  309. package/src/providers/integrations/commandcode/quota.ts +160 -0
  310. package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
  311. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
  312. package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
  313. package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
  314. package/src/providers/integrations/cursor/cursor-request.ts +471 -0
  315. package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
  316. package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
  317. package/src/providers/integrations/cursor/cursor.ts +960 -0
  318. package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
  319. package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
  320. package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
  321. package/src/providers/integrations/factory/factory-oauth.ts +7 -11
  322. package/src/providers/integrations/github/github.ts +4 -4
  323. package/src/providers/integrations/hermes.ts +79 -8
  324. package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
  325. package/src/providers/integrations/kilo/kilo.ts +153 -146
  326. package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
  327. package/src/providers/integrations/opencode/provider.ts +532 -499
  328. package/src/providers/integrations/openrouter/provider.ts +139 -281
  329. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
  330. package/src/providers/integrations/qoder/qoder-oauth.ts +310 -263
  331. package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
  332. package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
  333. package/src/providers/integrations/qoder/qoder.ts +865 -730
  334. package/src/providers/integrations/shared/install-id.ts +29 -2
  335. package/src/providers/integrations/trae/trae-catalog.ts +190 -0
  336. package/src/providers/integrations/trae/trae-headers.ts +149 -0
  337. package/src/providers/integrations/trae/trae-oauth.ts +773 -0
  338. package/src/providers/integrations/trae/trae-protocol.ts +400 -0
  339. package/src/providers/integrations/trae/trae-realm.ts +220 -0
  340. package/src/providers/integrations/trae/trae.ts +458 -0
  341. package/src/providers/operations/account-health-service.ts +107 -10
  342. package/src/providers/operations/client-versions.ts +143 -6
  343. package/src/providers/operations/model-catalog-cache.ts +18 -3
  344. package/src/providers/operations/provider-catalog-seeder.ts +95 -86
  345. package/src/providers/operations/provider-catalog-service.ts +76 -0
  346. package/src/providers/operations/provider-credential-service.ts +46 -7
  347. package/src/providers/provider-contracts.ts +76 -0
  348. package/src/providers/provider-metadata.ts +78 -6
  349. package/src/providers/provider-model-definition.ts +373 -349
  350. package/src/security/api-key-auth.ts +126 -8
  351. package/src/transport/contracts.ts +3 -0
  352. package/src/transport/dispatch/attempt-finalize.ts +39 -2
  353. package/src/transport/dispatch/attempt-loop.ts +530 -513
  354. package/src/transport/dispatch/empty-stream.ts +17 -26
  355. package/src/transport/dispatch/generation.ts +88 -21
  356. package/src/transport/dispatch/leases.ts +99 -2
  357. package/src/transport/dispatch/proxy-request.ts +60 -3
  358. package/src/transport/dispatch/streaming/finalize.ts +16 -1
  359. package/src/transport/dispatch/streaming/stream.ts +22 -2
  360. package/src/transport/dispatch/upstream.ts +19 -1
  361. package/src/transport/failure-policy.ts +20 -0
  362. package/src/transport/guards/auth-guard.ts +26 -1
  363. package/src/transport/guards/body-policy.ts +5 -4
  364. package/src/transport/request/preparer.ts +103 -0
  365. package/src/transport/request/state.ts +14 -0
  366. package/src/transport/routing/plan-gate.ts +45 -0
  367. package/src/transport/routing/route-catalog.ts +799 -738
  368. package/src/transport/routing/route-model.ts +52 -1
  369. package/src/transport/routing/router.ts +51 -0
  370. package/src/transport/surface/chat/encode.ts +18 -31
  371. package/src/transport/surface/messages/stream.ts +3 -2
  372. package/src/transport/translation/tool-repair.ts +37 -16
  373. package/src/workers/account-identity-sweep.ts +14 -12
  374. package/src/workers/daily-checkin.ts +62 -0
  375. package/src/workers/quota-refresh-sweep.ts +27 -2
  376. package/dist/dashboard/assets/BannedPage-DUrXpp-X.js +0 -1
  377. package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
  378. package/dist/dashboard/assets/ConsoleLogPage-AisQzCI1.js +0 -1
  379. package/dist/dashboard/assets/HealthEventsModal-BNflDm_f.js +0 -1
  380. package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
  381. package/dist/dashboard/assets/OverviewPage-C3NIzsDx.js +0 -1
  382. package/dist/dashboard/assets/ProviderDetailPage-BK8i5B5X.js +0 -3
  383. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
  384. package/dist/dashboard/assets/ProvidersPage-CvCVgm1e.js +0 -1
  385. package/dist/dashboard/assets/ProxyPage-DjabkuzP.js +0 -7
  386. package/dist/dashboard/assets/QuotaPage-Dxe0B0iu.js +0 -1
  387. package/dist/dashboard/assets/SetupPage-BaD4P_jW.js +0 -1
  388. package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
  389. package/dist/dashboard/assets/UsagePage-Djes-Epy.js +0 -56
  390. package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
  391. package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
  392. package/dist/dashboard/assets/assert-Bwh6DJTK.js +0 -1
  393. package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
  394. package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
  395. package/dist/dashboard/assets/entry-Cpi0rph1.js +0 -4
  396. package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
  397. package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
  398. package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
  399. package/dist/dashboard/assets/model-bans-CVvHZ-qz.js +0 -1
  400. package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
  401. package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
  402. package/dist/dashboard/assets/routing-jntw9-Ui.js +0 -1
  403. package/dist/dashboard/assets/select-CGUuTQmj.js +0 -41
  404. package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
  405. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
  406. package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
  407. package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
  408. package/scripts/cli/cli.ts +0 -771
  409. package/scripts/operations/probe-egress.ts +0 -335
  410. package/scripts/operations/restart.ts +0 -120
  411. package/scripts/shared/env.ts +0 -269
  412. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
  413. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
  414. /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
  415. /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
@@ -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
  }