cartethyia 2.7.3 → 2.7.5

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 (625) hide show
  1. package/.env.example +2 -0
  2. package/README.md +17 -5
  3. package/dist/dashboard/assets/{BannedPage-2TJD37Hb.js → BannedPage-Cj15GsMp.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-Cj15GsMp.js.gz +0 -0
  5. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js +1 -0
  6. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolDetailPage-xefJNRmJ.js → CliToolDetailPage-DZKmVuJN.js} +1 -1
  8. package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
  9. package/dist/dashboard/assets/{CliToolsPage-Rj9iHQ3F.js → CliToolsPage-BIccZmeL.js} +1 -1
  10. package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js.gz +0 -0
  11. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js +1 -0
  12. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js.gz +0 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js +1 -0
  14. package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js.gz +0 -0
  15. package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js +1 -0
  16. package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js.gz +0 -0
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js +656 -0
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-C4w7vea-.js → LoginPage-DkXb6dx-.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-BtPcAfot.js → ModelPicker-DnQPSIA8.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-DnQPSIA8.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js +5 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js.gz +0 -0
  31. package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js +9 -0
  32. package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-BQS0Rx_u.js → SetupPage-WTa0sktC.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-TP7voAQ0.js → StudioPage-BfAb5-o_.js} +3 -3
  40. package/dist/dashboard/assets/StudioPage-BfAb5-o_.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-l29JROPv.js +1 -0
  44. package/dist/dashboard/assets/api-keys-l29JROPv.js.gz +0 -0
  45. package/dist/dashboard/assets/app--PJWq8_C.css +2 -0
  46. package/dist/dashboard/assets/app--PJWq8_C.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-C0YYuZJ4.js → assert-C0K-c4eo.js} +1 -1
  48. package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-B_DMpMJN.js → badge-DfbhQU4k.js} +1 -1
  50. package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-DMrMPmth.js → banner-BllT4LcF.js} +1 -1
  52. package/dist/dashboard/assets/banner-BllT4LcF.js.gz +0 -0
  53. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  54. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  55. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  56. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  57. package/dist/dashboard/assets/{card-Be5Uzamx.js → card-CtkX66Wd.js} +1 -1
  58. package/dist/dashboard/assets/card-CtkX66Wd.js.gz +0 -0
  59. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js +1 -0
  60. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js.gz +0 -0
  61. package/dist/dashboard/assets/{cli-tools-muFRphDu.js → cli-tools-CQXVMn4P.js} +1 -1
  62. package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
  63. package/dist/dashboard/assets/{clipboard-button-DOfrVmsi.js → clipboard-button-BQqBQd2n.js} +1 -1
  64. package/dist/dashboard/assets/clipboard-button-BQqBQd2n.js.gz +0 -0
  65. package/dist/dashboard/assets/{dialog-C9omdjQt.js → dialog-BEK7KP0H.js} +1 -1
  66. package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
  67. package/dist/dashboard/assets/{drawer-Bm8WKaGY.js → drawer-BrTEZ8zu.js} +1 -1
  68. package/dist/dashboard/assets/drawer-BrTEZ8zu.js.gz +0 -0
  69. package/dist/dashboard/assets/entry-CHWxbS56.js +1 -0
  70. package/dist/dashboard/assets/entry-CHWxbS56.js.gz +0 -0
  71. package/dist/dashboard/assets/{entry-q5HwZ8nN.js → entry-EBHJ3DrE.js} +1 -1
  72. package/dist/dashboard/assets/entry-EBHJ3DrE.js.gz +0 -0
  73. package/dist/dashboard/assets/entry-LCS85hiq.js +4 -0
  74. package/dist/dashboard/assets/entry-LCS85hiq.js.gz +0 -0
  75. package/dist/dashboard/assets/{formatters-BTScrQpe.js → formatters-BAACA727.js} +1 -1
  76. package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
  77. package/dist/dashboard/assets/{github-badge-Orjuzemr.js → github-badge-YSCE4UC2.js} +1 -1
  78. package/dist/dashboard/assets/github-badge-YSCE4UC2.js.gz +0 -0
  79. package/dist/dashboard/assets/{index-cPrX5vn6.js → index-FQlj0HUT.js} +2 -2
  80. package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
  81. package/dist/dashboard/assets/{inline-Dvux8w-0.js → inline-C4gU5iOL.js} +1 -1
  82. package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
  83. package/dist/dashboard/assets/{input-CU1Rgf3p.js → input-BHHX_Tmm.js} +1 -1
  84. package/dist/dashboard/assets/input-BHHX_Tmm.js.gz +0 -0
  85. package/dist/dashboard/assets/{layout-C_7A6H55.js → layout-CajrYUnD.js} +1 -1
  86. package/dist/dashboard/assets/{layout-C_7A6H55.js.gz → layout-CajrYUnD.js.gz} +0 -0
  87. package/dist/dashboard/assets/{list-row-CzFvogbj.js → list-row-Bx2I7XUe.js} +1 -1
  88. package/dist/dashboard/assets/list-row-Bx2I7XUe.js.gz +0 -0
  89. package/dist/dashboard/assets/live-DNNqRbj2.js +1 -0
  90. package/dist/dashboard/assets/live-DNNqRbj2.js.gz +0 -0
  91. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
  92. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  93. package/dist/dashboard/assets/{mutation-JJGQm8TD.js → mutation-Rr0TIvcj.js} +1 -1
  94. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  95. package/dist/dashboard/assets/network-C5UBACrJ.js +1 -0
  96. package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
  97. package/dist/dashboard/assets/numbers-CUWAZK4J.js +2 -0
  98. package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
  99. package/dist/dashboard/assets/pagination-Xsi9Qx04.js +1 -0
  100. package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
  101. package/dist/dashboard/assets/{query-keys-CLaNz8Xz.js → query-keys-BloKtaXJ.js} +1 -1
  102. package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
  103. package/dist/dashboard/assets/quota-DvQbD_sr.js +1 -0
  104. package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
  105. package/dist/dashboard/assets/{routing-BEH32QOC.js → routing-in0f_IfK.js} +1 -1
  106. package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
  107. package/dist/dashboard/assets/{select-B3iTetHN.js → select-BLULNqTp.js} +2 -2
  108. package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
  109. package/dist/dashboard/assets/settings-CcGczj6I.js +1 -0
  110. package/dist/dashboard/assets/settings-CcGczj6I.js.gz +0 -0
  111. package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
  112. package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
  113. package/dist/dashboard/assets/{stack-KrOrm7JZ.js → stack-D2lIR0tg.js} +1 -1
  114. package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
  115. package/dist/dashboard/assets/{state-DLS7IDMy.js → state-DTARjfiW.js} +1 -1
  116. package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
  117. package/dist/dashboard/assets/{switch-42ItvbAs.js → switch-c0oPSnqW.js} +1 -1
  118. package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
  119. package/dist/dashboard/assets/{useMutation-Dmnfh1Ue.js → useMutation-m8WGNPrz.js} +1 -1
  120. package/dist/dashboard/assets/useMutation-m8WGNPrz.js.gz +0 -0
  121. package/dist/dashboard/console-backgrounds.jpg +0 -0
  122. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  123. package/dist/dashboard/index.html +2 -2
  124. package/dist/dashboard/index.html.gz +0 -0
  125. package/dist/dashboard/precompressed-assets.json +59 -57
  126. package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +9 -10
  127. package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
  128. package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
  129. package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
  130. package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
  131. package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
  132. package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
  133. package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
  134. package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
  135. package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
  136. package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
  137. package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +9 -10
  138. package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
  139. package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
  140. package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
  141. package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
  142. package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
  143. package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
  144. package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
  145. package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
  146. package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
  147. package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
  148. package/package.json +4 -5
  149. package/scripts/cli/update.ts +15 -41
  150. package/src/boot/app.ts +67 -10
  151. package/src/boot/dependencies.ts +786 -732
  152. package/src/boot/lifecycle.ts +11 -12
  153. package/src/config/cooldowns.ts +34 -55
  154. package/src/config/network.ts +8 -4
  155. package/src/config/package-paths.ts +1 -1
  156. package/src/config/providers.ts +13 -14
  157. package/src/config/resources.ts +0 -5
  158. package/src/config/runtime.ts +2 -5
  159. package/src/config/spec.ts +26 -6
  160. package/src/console/api-keys/ban-constants.ts +38 -0
  161. package/src/console/api-keys/contracts.ts +92 -31
  162. package/src/console/api-keys/routes.ts +131 -6
  163. package/src/console/audit/routes.ts +3 -3
  164. package/src/console/auth/access.ts +4 -4
  165. package/src/console/auth/account-reset.ts +5 -5
  166. package/src/console/auth/audit-recorder.ts +1 -1
  167. package/src/console/auth/lockout.ts +7 -3
  168. package/src/console/auth/row-coercion.ts +57 -12
  169. package/src/console/auth/session-resolver.ts +1 -1
  170. package/src/console/auth/session-service.ts +8 -2
  171. package/src/console/auth/session.ts +40 -34
  172. package/src/console/auth/setup-service.ts +4 -3
  173. package/src/console/auth/user-store.ts +1 -1
  174. package/src/console/backup/9router-map.ts +0 -3
  175. package/src/console/backup/contracts.ts +11 -9
  176. package/src/console/backup/export-endpoint.ts +11 -12
  177. package/src/console/backup/nine-router.ts +1 -7
  178. package/src/console/backup/routes.ts +32 -36
  179. package/src/console/backup/service.ts +468 -468
  180. package/src/console/backup/store.ts +47 -29
  181. package/src/console/backup/validate.ts +11 -1
  182. package/src/console/cli-tools/contracts.ts +7 -7
  183. package/src/console/cli-tools/injectors/cline.ts +110 -110
  184. package/src/console/cli-tools/injectors/codex.ts +181 -181
  185. package/src/console/cli-tools/injectors/copilot.ts +108 -108
  186. package/src/console/cli-tools/injectors/driver.ts +247 -247
  187. package/src/console/cli-tools/injectors/jcode.ts +115 -115
  188. package/src/console/cli-tools/injectors/openclaw.ts +161 -161
  189. package/src/console/cli-tools/injectors/opencode.ts +153 -153
  190. package/src/console/cli-tools/routes.ts +4 -4
  191. package/src/console/cli-tools/service.ts +1 -1
  192. package/src/console/cli-tools/store.ts +2 -2
  193. package/src/console/console-router-guards.ts +2 -2
  194. package/src/console/console-router.ts +36 -4
  195. package/src/console/dashboard-assets.ts +9 -9
  196. package/src/console/data-management/routes.ts +3 -3
  197. package/src/console/domain-registration.ts +18 -4
  198. package/src/console/inferhub/client.ts +0 -7
  199. package/src/console/inferhub/key-store.ts +1 -1
  200. package/src/console/inferhub/normalize.ts +2 -2
  201. package/src/console/inferhub/routes.ts +1 -1
  202. package/src/console/inferhub/types.ts +1 -1
  203. package/src/console/observability/contracts.ts +5 -1
  204. package/src/console/observability/live.ts +34 -21
  205. package/src/console/observability/logs.ts +4 -4
  206. package/src/console/observability/performance.ts +3 -3
  207. package/src/console/observability/routes.ts +3 -3
  208. package/src/console/observability/sse.ts +62 -8
  209. package/src/console/observability/store.ts +36 -20
  210. package/src/console/providers/catalog/catalog-projections.ts +1 -1
  211. package/src/console/providers/catalog/contracts.ts +82 -22
  212. package/src/console/providers/catalog/model-operations.ts +1 -1
  213. package/src/console/providers/catalog/provider-operations.ts +110 -17
  214. package/src/console/providers/catalog/public-model-store.ts +0 -0
  215. package/src/console/providers/catalog/routes.ts +30 -6
  216. package/src/console/providers/catalog/store-helpers.ts +349 -326
  217. package/src/console/providers/catalog/store.ts +1662 -1292
  218. package/src/console/providers/detail/routes.ts +4 -4
  219. package/src/console/providers/oauth/callback-listener.ts +100 -18
  220. package/src/console/providers/oauth/routes.ts +95 -16
  221. package/src/console/quota/account-quota-global.ts +41 -3
  222. package/src/console/quota/account-quota-shared.ts +10 -3
  223. package/src/console/quota/account-quota-tenant.ts +285 -67
  224. package/src/console/quota/account-test-egress.ts +4 -4
  225. package/src/console/quota/daily-growth-job.ts +308 -0
  226. package/src/console/quota/native-identity.ts +2 -2
  227. package/src/console/quota/quota-view.ts +1 -1
  228. package/src/console/remote-support/routes.ts +1 -1
  229. package/src/console/remote-support/service.ts +2 -2
  230. package/src/console/routing/model/contracts.ts +1 -1
  231. package/src/console/routing/model/routes.ts +3 -3
  232. package/src/console/routing/pools/contracts.ts +13 -10
  233. package/src/console/routing/pools/import-parse.ts +3 -3
  234. package/src/console/routing/pools/operations.ts +14 -7
  235. package/src/console/routing/pools/probe-verdict.ts +8 -3
  236. package/src/console/routing/pools/relay-deploy.ts +12 -12
  237. package/src/console/routing/pools/routes.ts +3 -3
  238. package/src/console/routing/pools/store.ts +21 -1
  239. package/src/console/settings/contracts.ts +29 -2
  240. package/src/console/settings/routes.ts +3 -3
  241. package/src/console/settings/store.ts +20 -0
  242. package/src/console/share/share-router.ts +112 -44
  243. package/src/console/share/share-stats.ts +109 -39
  244. package/src/console/share/share-usage.ts +1 -1
  245. package/src/console/shared/errors.ts +2 -2
  246. package/src/console/shared/object.ts +18 -0
  247. package/src/console/shared/read-through-cache.ts +42 -6
  248. package/src/console/shared/transaction.ts +14 -0
  249. package/src/console/system-prompt/contracts.ts +89 -12
  250. package/src/console/system-prompt/routes.ts +98 -45
  251. package/src/console/updates/apply-update.ts +244 -0
  252. package/src/console/updates/routes.ts +256 -98
  253. package/src/main.ts +11 -5
  254. package/src/network/http2-bidi.ts +1 -1
  255. package/src/network/ip-utils.ts +298 -0
  256. package/src/network/outbound-fetch.ts +76 -5
  257. package/src/network/pool/agent.ts +7 -6
  258. package/src/network/pool/byte-accounting.ts +1 -1
  259. package/src/network/pool/resolver.ts +55 -51
  260. package/src/network/pool/selector.ts +150 -136
  261. package/src/network/pool-cooldown-store.ts +71 -16
  262. package/src/network/pool-health-machine.ts +22 -2
  263. package/src/network/rate-limit-verdict.ts +68 -0
  264. package/src/network/retry.ts +1 -1
  265. package/src/network/ssrf.ts +24 -75
  266. package/src/observability/event-log.ts +2 -2
  267. package/src/observability/event-query.ts +286 -214
  268. package/src/observability/log-sanitize.ts +1 -1
  269. package/src/observability/metrics.ts +4 -4
  270. package/src/observability/payload-capture.ts +6 -5
  271. package/src/observability/payload-store.ts +1 -1
  272. package/src/observability/runtime-metrics.ts +1 -1
  273. package/src/observability/telemetry-buffer.ts +12 -3
  274. package/src/observability/telemetry-reconciliation.ts +3 -3
  275. package/src/observability/telemetry-status.ts +1 -1
  276. package/src/observability/token-speed.ts +1 -1
  277. package/src/persistence/connection-url.ts +1 -1
  278. package/src/persistence/db-handle.ts +1 -1
  279. package/src/persistence/db-pg.ts +2 -2
  280. package/src/persistence/db-sqlite.ts +10 -5
  281. package/src/persistence/dialect.ts +5 -1
  282. package/src/persistence/readiness.ts +19 -7
  283. package/src/persistence/redis.ts +1 -1
  284. package/src/persistence/schema.ts +177 -45
  285. package/src/persistence/shared-cache.ts +2 -2
  286. package/src/persistence/sort-order.ts +1 -1
  287. package/src/persistence/stores/api-key-store.ts +109 -4
  288. package/src/persistence/stores/share-store.ts +20 -8
  289. package/src/persistence/stores/telemetry-store.ts +1 -1
  290. package/src/persistence/system-prompt-store.ts +63 -18
  291. package/src/persistence/tenant-preferences.ts +1 -1
  292. package/src/persistence/ttl-cache.ts +3 -3
  293. package/src/persistence/usage-flush.ts +7 -2
  294. package/src/protocol/canonical-model.ts +1 -1
  295. package/src/protocol/codecs.ts +12 -16
  296. package/src/protocol/dsml.ts +3 -49
  297. package/src/protocol/gateway-error.ts +6 -3
  298. package/src/protocol/image-clamp.ts +2 -2
  299. package/src/protocol/image-generation.ts +2 -2
  300. package/src/protocol/reasoning-text.ts +1 -1
  301. package/src/protocol/reasoning.ts +7 -52
  302. package/src/protocol/registry.ts +50 -15
  303. package/src/protocol/request/chat.ts +46 -19
  304. package/src/protocol/request/codex.ts +1 -1
  305. package/src/protocol/request/messages.ts +46 -14
  306. package/src/protocol/request/responses.ts +19 -10
  307. package/src/protocol/response/chat.ts +27 -12
  308. package/src/protocol/response/codex.ts +2 -2
  309. package/src/protocol/response/gemini.ts +1 -1
  310. package/src/protocol/response/messages.ts +6 -6
  311. package/src/protocol/response/omp-tool-call-markup.ts +1 -1
  312. package/src/protocol/response/responses.ts +3 -3
  313. package/src/protocol/sse-decoder.ts +64 -4
  314. package/src/protocol/stop-reason.ts +2 -2
  315. package/src/protocol/think-split.ts +347 -0
  316. package/src/protocol/translation/attachment-integrity.ts +3 -3
  317. package/src/protocol/translation/cache-controls.ts +8 -20
  318. package/src/protocol/translation/capabilities.ts +8 -8
  319. package/src/protocol/translation/dialect/deepseek.ts +10 -0
  320. package/src/protocol/translation/dialect/inband.ts +39 -0
  321. package/src/protocol/translation/dialect/model-thinking.ts +89 -0
  322. package/src/protocol/translation/dialect/qwen.ts +114 -0
  323. package/src/protocol/translation/dialect/resolver.ts +83 -0
  324. package/src/protocol/translation/dialect/rules.ts +93 -0
  325. package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
  326. package/src/protocol/translation/dialect/types.ts +58 -0
  327. package/src/protocol/translation/quirks.ts +18 -4
  328. package/src/protocol/translation/thinking.ts +12 -252
  329. package/src/protocol/translation/tool-id.ts +3 -3
  330. package/src/protocol/translation/tool-repair.ts +1 -1
  331. package/src/protocol/upstream/post-json.ts +6 -1
  332. package/src/protocol/upstream/send-claude-messages.ts +3 -3
  333. package/src/providers/adapters/base-adapter.ts +1 -1
  334. package/src/providers/adapters/compatible-adapter.ts +44 -5
  335. package/src/providers/authentication/jwt-validator.ts +0 -5
  336. package/src/providers/authentication/oauth-client.ts +254 -253
  337. package/src/providers/authentication/oauth-flow-store.ts +42 -7
  338. package/src/providers/authentication/oauth-refresh-service.ts +2 -2
  339. package/src/providers/bundled-discovery-providers.ts +6 -30
  340. package/src/providers/configured-openai-providers.ts +82 -97
  341. package/src/providers/configured-provider.ts +1 -1
  342. package/src/providers/default-registry.ts +23 -35
  343. package/src/providers/discovery/catalog/catalog-format.ts +13 -2
  344. package/src/providers/discovery/discovery-types.ts +0 -1
  345. package/src/providers/discovery/openai-model-discovery.ts +2 -2
  346. package/src/providers/discovery/probe-phases.ts +50 -15
  347. package/src/providers/discovery/probing-service.ts +18 -20
  348. package/src/providers/integrations/agentrouter/agentrouter.ts +14 -5
  349. package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
  350. package/src/providers/integrations/anthropic.ts +4 -11
  351. package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
  352. package/src/providers/integrations/antigravity/antigravity-protocol.ts +0 -14
  353. package/src/providers/integrations/antigravity/antigravity.ts +15 -26
  354. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +2 -2
  355. package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
  356. package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
  357. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
  358. package/src/providers/integrations/buddy/buddy-identity.ts +108 -1
  359. package/src/providers/integrations/buddy/buddy-media.ts +16 -16
  360. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
  361. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
  362. package/src/providers/integrations/buddy/codebuddy/provider.ts +5 -16
  363. package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
  364. package/src/providers/integrations/buddy/workbuddy/provider.ts +6 -10
  365. package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
  366. package/src/providers/integrations/claude/claude-betas.ts +1 -1
  367. package/src/providers/integrations/claude/claude-cch.ts +148 -148
  368. package/src/providers/integrations/claude/claude-credentials.ts +1 -1
  369. package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
  370. package/src/providers/integrations/claude/claude.ts +3 -10
  371. package/src/providers/integrations/claude/fallback-credit.ts +3 -7
  372. package/src/providers/integrations/cline/cline.ts +2 -2
  373. package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
  374. package/src/providers/integrations/codex/codex-device-code.ts +101 -101
  375. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  376. package/src/providers/integrations/codex/codex-oauth.ts +0 -1
  377. package/src/providers/integrations/codex/codex.ts +14 -15
  378. package/src/providers/integrations/commandcode/commandcode.ts +76 -116
  379. package/src/providers/integrations/commandcode/quota.ts +132 -48
  380. package/src/providers/integrations/cursor/cursor-http2.ts +11 -14
  381. package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
  382. package/src/providers/integrations/cursor/cursor-request.ts +0 -87
  383. package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
  384. package/src/providers/integrations/cursor/cursor.ts +53 -20
  385. package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
  386. package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
  387. package/src/providers/integrations/factory/factory.ts +13 -13
  388. package/src/providers/integrations/gemini.ts +11 -11
  389. package/src/providers/integrations/github/github-discovery.ts +55 -10
  390. package/src/providers/integrations/github/github-oauth.ts +6 -5
  391. package/src/providers/integrations/github/github.ts +47 -20
  392. package/src/providers/integrations/grok/grok-oauth.ts +261 -261
  393. package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
  394. package/src/providers/integrations/grok/grok-quota.ts +16 -7
  395. package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
  396. package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
  397. package/src/providers/integrations/grok/grok.ts +88 -42
  398. package/src/providers/integrations/inferhub.ts +4 -11
  399. package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
  400. package/src/providers/integrations/kimi/kimi.ts +4 -11
  401. package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
  402. package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
  403. package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
  404. package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
  405. package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
  406. package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
  407. package/src/providers/integrations/kiro/kiro-request.ts +8 -11
  408. package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
  409. package/src/providers/integrations/kiro/kiro.ts +91 -78
  410. package/src/providers/integrations/muse/muse-oauth.ts +0 -1
  411. package/src/providers/integrations/muse/muse-quota.ts +0 -4
  412. package/src/providers/integrations/muse/muse.ts +6 -9
  413. package/src/providers/integrations/opencode/fingerprint.ts +7 -5
  414. package/src/providers/integrations/opencode/provider.ts +17 -24
  415. package/src/providers/integrations/perplexity.ts +2 -2
  416. package/src/providers/integrations/qoder/qoder-catalog.ts +132 -17
  417. package/src/providers/integrations/qoder/qoder-checkin.ts +26 -8
  418. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +9 -8
  419. package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
  420. package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
  421. package/src/providers/integrations/qoder/qoder-realm.ts +22 -28
  422. package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
  423. package/src/providers/integrations/qoder/qoder-umid.ts +33 -38
  424. package/src/providers/integrations/qoder/qoder-virtual.ts +14 -2
  425. package/src/providers/integrations/qoder/qoder.ts +521 -63
  426. package/src/providers/integrations/shared/connect.ts +0 -54
  427. package/src/providers/integrations/shared/hash.ts +15 -0
  428. package/src/providers/integrations/shared/session-cache.ts +136 -0
  429. package/src/providers/integrations/tokenharbor.ts +2 -2
  430. package/src/providers/integrations/trae/trae-cache.ts +7 -87
  431. package/src/providers/integrations/trae/trae-catalog.ts +1 -6
  432. package/src/providers/integrations/trae/trae-protocol.ts +17 -8
  433. package/src/providers/integrations/trae/trae-realm.ts +0 -7
  434. package/src/providers/integrations/trae/trae.ts +59 -19
  435. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  436. package/src/providers/integrations/zcode/oauth.ts +1 -1
  437. package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
  438. package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
  439. package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
  440. package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
  441. package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
  442. package/src/providers/operations/account-error-classification.ts +131 -116
  443. package/src/providers/operations/account-health-service.ts +31 -39
  444. package/src/providers/operations/account-reset-service.ts +58 -1
  445. package/src/providers/operations/cli-platform.ts +31 -5
  446. package/src/providers/operations/client-version-resolver.ts +4 -6
  447. package/src/providers/operations/client-versions.ts +10 -7
  448. package/src/providers/operations/provider-catalog-seeder.ts +2 -0
  449. package/src/providers/operations/provider-catalog-service.ts +22 -6
  450. package/src/providers/operations/provider-credential-service.ts +35 -6
  451. package/src/providers/operations/upstream-deadline.ts +3 -3
  452. package/src/providers/provider-contracts.ts +65 -10
  453. package/src/providers/provider-credential-resolver.ts +1 -1
  454. package/src/providers/provider-metadata.ts +17 -2
  455. package/src/providers/provider-model-definition.ts +22 -1
  456. package/src/providers/provider-probe.ts +4 -2
  457. package/src/providers/provider-registry.ts +4 -4
  458. package/src/providers/provider-usage.ts +81 -16
  459. package/src/providers/quota/cache.ts +29 -0
  460. package/src/providers/quota/quota-contracts.ts +28 -10
  461. package/src/providers/quota/refresh.ts +29 -6
  462. package/src/providers/quota/snapshot.ts +94 -0
  463. package/src/providers/search/search-provider.ts +6 -8
  464. package/src/providers/search/search-providers.ts +2 -1
  465. package/src/security/abuse.ts +8 -8
  466. package/src/security/admission/reasons.ts +17 -17
  467. package/src/security/admission/service.ts +2 -2
  468. package/src/security/admission/ttl.ts +61 -61
  469. package/src/security/api-key-auth.ts +20 -8
  470. package/src/security/client-router-fingerprint.ts +4 -4
  471. package/src/security/credential-codec.ts +3 -3
  472. package/src/security/csrf.ts +2 -2
  473. package/src/security/ip-boundary.ts +16 -189
  474. package/src/security/model-access-rule.ts +1 -1
  475. package/src/security/outbound-headers.ts +18 -0
  476. package/src/shared/coerce.ts +19 -0
  477. package/src/shared/invoke-guard.ts +47 -0
  478. package/src/transport/contracts.ts +13 -4
  479. package/src/transport/dispatch/attempt-finalize.ts +5 -4
  480. package/src/transport/dispatch/attempt-loop.ts +10 -7
  481. package/src/transport/dispatch/capture-policy.ts +34 -37
  482. package/src/transport/dispatch/empty-stream.ts +28 -25
  483. package/src/transport/dispatch/generation.ts +33 -53
  484. package/src/transport/dispatch/leases.ts +8 -8
  485. package/src/transport/dispatch/native-shared.ts +127 -0
  486. package/src/transport/dispatch/proxy-request.ts +10 -8
  487. package/src/transport/dispatch/responses-compact.ts +35 -56
  488. package/src/transport/dispatch/retry-policy.ts +4 -67
  489. package/src/transport/dispatch/stream-bridge.ts +1 -1
  490. package/src/transport/dispatch/streaming/encoder.ts +18 -72
  491. package/src/transport/dispatch/streaming/finalize.ts +11 -10
  492. package/src/transport/dispatch/streaming/stream.ts +7 -3
  493. package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
  494. package/src/transport/dispatch/systemone.ts +27 -55
  495. package/src/transport/dispatch/token-count.ts +49 -46
  496. package/src/transport/dispatch/websearch.ts +24 -50
  497. package/src/transport/drain-endpoint.ts +3 -3
  498. package/src/transport/failure-policy.ts +19 -6
  499. package/src/transport/guards/auth-guard.ts +6 -11
  500. package/src/transport/guards/body-policy.ts +7 -7
  501. package/src/transport/guards/error-lifecycle.ts +18 -3
  502. package/src/transport/guards/guard-errors.ts +7 -3
  503. package/src/transport/guards/headroom-guard.ts +2 -2
  504. package/src/transport/guards/pipeline.ts +7 -7
  505. package/src/transport/guards/readiness-guard.ts +16 -2
  506. package/src/transport/guards/request-context.ts +15 -20
  507. package/src/transport/request/preparer.ts +306 -27
  508. package/src/transport/request/rtk/compress-request.ts +3 -3
  509. package/src/transport/request/rtk/rtk-constants.ts +2 -2
  510. package/src/transport/request/rtk/rtk-filters.ts +8 -8
  511. package/src/transport/request/state.ts +5 -20
  512. package/src/transport/request/system-prompt-injection.ts +74 -11
  513. package/src/transport/request/tenant-preferences.ts +22 -0
  514. package/src/transport/routing/candidate-index.ts +2 -2
  515. package/src/transport/routing/round-robin.ts +1 -1
  516. package/src/transport/routing/route-catalog.ts +113 -19
  517. package/src/transport/routing/route-model.ts +131 -38
  518. package/src/transport/routing/router.ts +154 -39
  519. package/src/transport/shutdown-notice.ts +21 -9
  520. package/src/transport/surface/adapters.ts +3 -3
  521. package/src/transport/surface/chat/encode.ts +7 -7
  522. package/src/transport/surface/chat/parse.ts +7 -7
  523. package/src/transport/surface/completion.ts +3 -3
  524. package/src/transport/surface/content-parts.ts +1 -1
  525. package/src/transport/surface/dialects.ts +2 -2
  526. package/src/transport/surface/messages/encode.ts +1 -1
  527. package/src/transport/surface/messages/parse.ts +3 -3
  528. package/src/transport/surface/responses/adapter.ts +1 -1
  529. package/src/transport/surface/responses/errors.ts +0 -9
  530. package/src/transport/vision-handoff-cache.ts +58 -0
  531. package/src/transport/vision-handoff.ts +376 -0
  532. package/src/workers/account-identity-sweep.ts +2 -2
  533. package/src/workers/buddy-growth-tasks.ts +392 -346
  534. package/src/workers/daily-checkin.ts +102 -49
  535. package/src/workers/maintenance-lease.ts +2 -35
  536. package/src/workers/oauth-refresh-sweep.ts +8 -1
  537. package/src/workers/quota-refresh-sweep.ts +120 -82
  538. package/src/workers/scheduler.ts +67 -4
  539. package/src/workers/sweep-runner.ts +24 -5
  540. package/src/workers/zcode-gift-claim.ts +108 -0
  541. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
  542. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
  543. package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
  544. package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
  545. package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
  546. package/dist/dashboard/assets/CombosPage-JEbYJFxw.js +0 -1
  547. package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
  548. package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js +0 -1
  549. package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
  550. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +0 -1
  551. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
  552. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +0 -1
  553. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
  554. package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
  555. package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
  556. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +0 -1
  557. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
  558. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +0 -4
  559. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
  560. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
  561. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  562. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +0 -1
  563. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
  564. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +0 -9
  565. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
  566. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js +0 -1
  567. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
  568. package/dist/dashboard/assets/SettingsPage-CDZjTt87.js +0 -1
  569. package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
  570. package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
  571. package/dist/dashboard/assets/SortableList-BAL5npBC.js +0 -1
  572. package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
  573. package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
  574. package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js +0 -56
  575. package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
  576. package/dist/dashboard/assets/api-keys-MbI7e7rt.js +0 -1
  577. package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
  578. package/dist/dashboard/assets/app-B2vyPoII.css +0 -2
  579. package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
  580. package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
  581. package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
  582. package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
  583. package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
  584. package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
  585. package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
  586. package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
  587. package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
  588. package/dist/dashboard/assets/entry-DS90gUks.js +0 -1
  589. package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
  590. package/dist/dashboard/assets/entry-j40LJotO.js +0 -4
  591. package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
  592. package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
  593. package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
  594. package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
  595. package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
  596. package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
  597. package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
  598. package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
  599. package/dist/dashboard/assets/live-KaG15HLQ.js +0 -1
  600. package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
  601. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
  602. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
  603. package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
  604. package/dist/dashboard/assets/numbers-BNDZnsUE.js +0 -2
  605. package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
  606. package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
  607. package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
  608. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  609. package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
  610. package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
  611. package/dist/dashboard/assets/settings-Dt3Eqbnq.js +0 -1
  612. package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
  613. package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
  614. package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
  615. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
  616. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  617. package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
  618. package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
  619. package/src/network/response-headers.ts +0 -39
  620. package/src/protocol/response/dsml.ts +0 -38
  621. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
  622. /package/dist/dashboard/assets/{app-wvwCi4Eh.js → app-Bgd3zANQ.js} +0 -0
  623. /package/dist/dashboard/assets/{app-wvwCi4Eh.js.gz → app-Bgd3zANQ.js.gz} +0 -0
  624. /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
  625. /package/dist/dashboard/assets/brand/{og_image.webp → og_image_initial.webp} +0 -0
@@ -8,11 +8,12 @@ import { randomBytes } from "node:crypto";
8
8
  import type { ApiKeyRecord, ApiKeyStore } from "../../persistence/stores/api-key-store";
9
9
  import { hashSecret } from "../../security/crypto";
10
10
  import { type ShareLinkStore, type ShareLinkSummary } from "../../persistence/stores/share-store";
11
- import { API_KEY_MODES, type ApiKeyMode, type ApiKeyModelAccessMode, type ShareLinkKind } from "../../persistence/schema";
11
+ import { API_KEY_MODES, API_KEY_CONCURRENCY_SCOPES, type ApiKeyMode, type ApiKeyModelAccessMode, type ShareLinkKind } from "../../persistence/schema";
12
12
  import { isValidTenantKeyScope, type AccessScope } from "../../security/access-control";
13
13
  import { normalizeClientRouterId } from "../../security/client-router-fingerprint";
14
14
  import type { ShareActivityPort } from "../share/share-usage";
15
15
  import { ConsoleDomainError } from "../shared/errors";
16
+ import { pickDefined } from "../shared/object";
16
17
  import type { ConsoleAccessResolver } from "../auth/access";
17
18
  import type { ApiKeyAdmissionService } from "../../security/admission/service";
18
19
  import {
@@ -34,6 +35,9 @@ export type {
34
35
  SharedKeySummary,
35
36
  SharedKeyActivityDetail,
36
37
  } from "../share/share-usage";
38
+ /** How a share template's concurrency cap applies to its recipients. */
39
+ export type ConcurrencyScope = (typeof API_KEY_CONCURRENCY_SCOPES)[number];
40
+
37
41
  /** Input accepted when creating or editing a key. Null limits mean unlimited. */
38
42
  export interface CreateApiKeyRequest {
39
43
  enabled?: boolean;
@@ -49,6 +53,13 @@ export interface CreateApiKeyRequest {
49
53
  monthlyTokenLimit?: number | null;
50
54
  lifetimeTokenBudget?: number | null;
51
55
  maxConcurrentRequests?: number | null;
56
+ /**
57
+ * How `maxConcurrentRequests` applies to a share template's family:
58
+ * `family` (default) = one cap across all recipient keys (a single shared
59
+ * admission counter); `per_key` = each recipient key gets its own cap and
60
+ * independent counter. Ignored/meaningless for personal keys.
61
+ */
62
+ concurrencyScope?: ConcurrencyScope;
52
63
  /** Share-template slot cap; only meaningful when keyMode === "share". */
53
64
  maxChildren?: number | null;
54
65
  modelPrefix?: string;
@@ -90,6 +101,7 @@ export interface ApiKeyResponse {
90
101
  readonly modelList?: readonly string[];
91
102
  readonly clientRouterDenylist?: readonly string[];
92
103
  readonly maxConcurrentRequests?: number;
104
+ readonly concurrencyScope?: ConcurrencyScope;
93
105
  readonly maxChildren?: number;
94
106
  readonly notesTitle?: string;
95
107
  readonly notesSubtitle?: string;
@@ -101,6 +113,11 @@ export interface ApiKeyResponse {
101
113
  readonly sharePopupBody?: string;
102
114
  readonly createdAt: string;
103
115
  readonly revokedAt?: string;
116
+ /** Ban state; see the schema columns for the contract. */
117
+ readonly bannedAt?: string;
118
+ readonly banReason?: string;
119
+ /** When the ban lifts on its own; absent means permanent. */
120
+ readonly bannedUntil?: string;
104
121
  readonly tokensConsumed: number;
105
122
  }
106
123
  /** Creation result; plaintext is returned only when a personal key is minted. */
@@ -113,6 +130,31 @@ export interface UpdateApiKeyResponse extends ApiKeyResponse {
113
130
  readonly secret?: string;
114
131
  }
115
132
 
133
+ /**
134
+ * Ban reasons offered by the console dropdown, and the durations the ban
135
+ * endpoint accepts. These are pure lists, kept in `ban-constants.ts` (no
136
+ * `node:*` imports) so the dashboard can import them without dragging Node
137
+ * crypto into the browser bundle. Re-exported here for backend consumers
138
+ * (`routes.ts` validates against `BAN_DURATION_CHOICES`).
139
+ */
140
+ export {
141
+ API_KEY_BAN_REASONS,
142
+ BAN_DURATION_CHOICES,
143
+ } from "./ban-constants";
144
+
145
+ /** What a bare "ban this key" click uses: the shortest, most reversible choice. */
146
+ export const DEFAULT_BAN_MINUTES: number = 60;
147
+
148
+ /** Body accepted by the ban endpoint. */
149
+ export interface BanApiKeyRequest {
150
+ readonly reason: string;
151
+ /**
152
+ * Ban duration in whole minutes from {@link BAN_DURATION_CHOICES}; `null`
153
+ * means permanent. Absent = {@link DEFAULT_BAN_MINUTES}.
154
+ */
155
+ readonly minutes?: number | null;
156
+ }
157
+
116
158
  /** API-key route dependency boundary. */
117
159
  export interface ApiKeyConfig {
118
160
  readonly store: ApiKeyStore;
@@ -206,16 +248,20 @@ export function generateApiKeySecret(prefix = DEFAULT_API_KEY_PREFIX): {
206
248
  return { secret, hash: hashSecret(secret), prefix };
207
249
  }
208
250
 
209
- /** Normalizes a caller-supplied prefix; falls back to the default. */
210
- export function resolveKeyPrefix(raw: string | undefined): string {
251
+ /** Normalizes a caller-supplied prefix; falls back to `fallback`. */
252
+ function resolvePrefix(raw: string | undefined, fallback: string): string {
211
253
  const trimmed = raw?.trim();
212
- return trimmed && trimmed.length > 0 ? trimmed : DEFAULT_API_KEY_PREFIX;
254
+ return trimmed && trimmed.length > 0 ? trimmed : fallback;
255
+ }
256
+
257
+ /** Normalizes a caller-supplied prefix; falls back to the personal default. */
258
+ export function resolveKeyPrefix(raw: string | undefined): string {
259
+ return resolvePrefix(raw, DEFAULT_API_KEY_PREFIX);
213
260
  }
214
261
 
215
- /** Resolves the share-key prefix; falls back to the default. */
262
+ /** Resolves the share-key prefix; falls back to the share default. */
216
263
  export function resolveShareKeyPrefix(raw: string | undefined): string {
217
- const trimmed = raw?.trim();
218
- return !trimmed ? DEFAULT_SHARE_API_KEY_PREFIX : trimmed;
264
+ return resolvePrefix(raw, DEFAULT_SHARE_API_KEY_PREFIX);
219
265
  }
220
266
 
221
267
  /** Maximum hint the enroll page accepts; the dashboard caps its input there. */
@@ -300,6 +346,19 @@ export function validateApiKeyRequest(request: CreateApiKeyRequest): readonly Ac
300
346
  finitePositive(request.monthlyTokenLimit, "monthlyTokenLimit");
301
347
  finitePositive(request.lifetimeTokenBudget, "lifetimeTokenBudget");
302
348
  finitePositive(request.maxConcurrentRequests, "maxConcurrentRequests");
349
+ if (
350
+ request.concurrencyScope !== undefined &&
351
+ !(API_KEY_CONCURRENCY_SCOPES as readonly string[]).includes(request.concurrencyScope)
352
+ ) {
353
+ throw new ConsoleDomainError("invalid_limits", 400, "concurrencyScope must be family or per_key");
354
+ }
355
+ if (request.concurrencyScope !== undefined && keyMode !== "share") {
356
+ throw new ConsoleDomainError(
357
+ "invalid_limits",
358
+ 400,
359
+ "concurrencyScope is only valid for share templates",
360
+ );
361
+ }
303
362
  if (request.maxChildren !== undefined && request.maxChildren !== null) {
304
363
  finitePositive(request.maxChildren, "maxChildren");
305
364
  if (request.maxChildren > 10_000) {
@@ -375,31 +434,33 @@ export function sanitizeApiKeyResponse(record: ApiKeyRecord): ApiKeyResponse {
375
434
  * renaming the key cannot change which key is the default.
376
435
  */
377
436
  isDefault: isDefaultGatewayApiKey(record),
378
- ...(keyPrefix === undefined ? {} : { keyPrefix }),
379
- ...(record.parentKeyId === undefined ? {} : { parentKeyId: record.parentKeyId }),
380
- ...(record.notesTitle === undefined ? {} : { notesTitle: record.notesTitle }),
381
- ...(record.notesSubtitle === undefined ? {} : { notesSubtitle: record.notesSubtitle }),
382
- ...(record.notesBody === undefined ? {} : { notesBody: record.notesBody }),
383
- ...(record.sharePopupEnabled === undefined ? {} : { sharePopupEnabled: record.sharePopupEnabled }),
384
- ...(record.sharePopupImageMime === undefined
385
- ? {}
386
- : { sharePopupImageMime: record.sharePopupImageMime }),
387
- ...(record.sharePopupTitle === undefined ? {} : { sharePopupTitle: record.sharePopupTitle }),
388
- ...(record.sharePopupBody === undefined ? {} : { sharePopupBody: record.sharePopupBody }),
389
- ...(record.requestsPerMinute === undefined ? {} : { requestsPerMinute: record.requestsPerMinute }),
390
- ...(record.dailyTokenLimit === undefined ? {} : { dailyTokenLimit: record.dailyTokenLimit }),
391
- ...(record.monthlyTokenLimit === undefined ? {} : { monthlyTokenLimit: record.monthlyTokenLimit }),
392
- ...(record.lifetimeTokenBudget === undefined ? {} : { lifetimeTokenBudget: record.lifetimeTokenBudget }),
393
- ...(record.maxConcurrentRequests === undefined ? {} : { maxConcurrentRequests: record.maxConcurrentRequests }),
394
- ...(record.maxChildren === undefined ? {} : { maxChildren: record.maxChildren }),
395
- ...(record.modelPrefix === undefined ? {} : { modelPrefix: record.modelPrefix }),
396
- ...(record.modelAccessMode === undefined ? {} : { modelAccessMode: record.modelAccessMode }),
397
- ...(record.modelList === undefined ? {} : { modelList: record.modelList }),
398
- ...(record.clientRouterDenylist === undefined
399
- ? {}
400
- : { clientRouterDenylist: record.clientRouterDenylist }),
437
+ ...pickDefined({
438
+ keyPrefix,
439
+ parentKeyId: record.parentKeyId,
440
+ notesTitle: record.notesTitle,
441
+ notesSubtitle: record.notesSubtitle,
442
+ notesBody: record.notesBody,
443
+ sharePopupEnabled: record.sharePopupEnabled,
444
+ sharePopupImageMime: record.sharePopupImageMime,
445
+ sharePopupTitle: record.sharePopupTitle,
446
+ sharePopupBody: record.sharePopupBody,
447
+ requestsPerMinute: record.requestsPerMinute,
448
+ dailyTokenLimit: record.dailyTokenLimit,
449
+ monthlyTokenLimit: record.monthlyTokenLimit,
450
+ lifetimeTokenBudget: record.lifetimeTokenBudget,
451
+ maxConcurrentRequests: record.maxConcurrentRequests,
452
+ concurrencyScope: record.concurrencyScope,
453
+ maxChildren: record.maxChildren,
454
+ modelPrefix: record.modelPrefix,
455
+ modelAccessMode: record.modelAccessMode,
456
+ modelList: record.modelList,
457
+ clientRouterDenylist: record.clientRouterDenylist,
458
+ revokedAt: record.revokedAt?.toISOString(),
459
+ bannedAt: record.bannedAt?.toISOString(),
460
+ banReason: record.banReason,
461
+ bannedUntil: record.bannedUntil?.toISOString(),
462
+ }),
401
463
  createdAt: record.createdAt.toISOString(),
402
- ...(record.revokedAt === undefined ? {} : { revokedAt: record.revokedAt.toISOString() }),
403
464
  tokensConsumed: record.tokensConsumed,
404
465
  };
405
466
  }
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Owns validation, CRUD operations, and route wiring through the ApiKeyStore.
4
4
 
5
- import { Elysia, t } from "elysia";
5
+ import { Elysia, t, type AnyElysia } from "elysia";
6
6
  import { randomBytes, randomUUID } from "node:crypto";
7
7
  import { decryptCredentialToString, encryptCredential } from "../../security/crypto";
8
8
  import { invalidateApiKeyCache } from "../../security/api-key-auth";
@@ -11,8 +11,8 @@ import { generateShareSlug, hashShareToken } from "../../persistence/stores/shar
11
11
  import type { AccessDecision } from "../../security/access-control";
12
12
  import { ConsoleDomainError, errorResponse, requireTenantScope } from "../shared/errors";
13
13
  import { literalUnion } from "../shared/elysia-schema";
14
- import { API_KEY_MODES, API_KEY_MODEL_ACCESS_MODES, type ShareLinkKind } from "../../persistence/schema";
15
- import type { ApiKeyConfig, ApiKeyResponse, UpdateApiKeyResponse } from "./contracts";
14
+ import { API_KEY_MODES, API_KEY_MODEL_ACCESS_MODES, API_KEY_CONCURRENCY_SCOPES, type ShareLinkKind } from "../../persistence/schema";
15
+ import type { ApiKeyConfig, ApiKeyResponse, BanApiKeyRequest, UpdateApiKeyResponse } from "./contracts";
16
16
  import type {
17
17
  ApiKeyPatch,
18
18
  ApiKeyRecord,
@@ -20,6 +20,9 @@ import type {
20
20
  SharedChildrenPageOptions,
21
21
  } from "../../persistence/stores/api-key-store";
22
22
  import {
23
+ API_KEY_BAN_REASONS,
24
+ BAN_DURATION_CHOICES,
25
+ DEFAULT_BAN_MINUTES,
23
26
  generateApiKeySecret,
24
27
  isDefaultGatewayApiKey,
25
28
  parseSharePopupImage,
@@ -38,6 +41,7 @@ import {
38
41
  } from "./contracts";
39
42
  import type { SharedKeyActivityDetail, SharedKeySummary } from "../share/share-usage";
40
43
  import { popupImageBytes } from "./share-popup-image";
44
+ import { log } from "../../observability/logger";
41
45
 
42
46
  const SHARED_CHILD_SORT_COLUMNS = new Set<SharedChildSortColumn>([
43
47
  "createdAt",
@@ -48,6 +52,9 @@ const SHARED_CHILD_SORT_COLUMNS = new Set<SharedChildSortColumn>([
48
52
  "enabled",
49
53
  ]);
50
54
 
55
+ /** The offered ban durations as a set of minute values, hoisted once. */
56
+ const BAN_DURATION_MINUTES = new Set(BAN_DURATION_CHOICES.map((choice) => choice.minutes));
57
+
51
58
  /** Parses and bounds the paginated share-child list query. */
52
59
  function parseSharedChildrenQuery(query: URLSearchParams): SharedChildrenPageOptions {
53
60
  const limitRaw = query.get("limit");
@@ -93,7 +100,16 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
93
100
  if (record.keyMode !== "share") return response;
94
101
  const family = await config.store
95
102
  .sumFamilyTelemetryConsumed(authorized.tenantId, record.id)
96
- .catch(() => 0);
103
+ .catch((error) => {
104
+ // A failed sum must not fail the whole list, but it must not
105
+ // be silent either: reporting the share template's spend as 0
106
+ // under-reports consumed tokens in the console list.
107
+ log.warn(
108
+ `[api-keys] sumFamilyTelemetryConsumed failed for key ${record.id}; reporting 0`,
109
+ error as Error,
110
+ );
111
+ return 0;
112
+ });
97
113
  return { ...response, tokensConsumed: response.tokensConsumed + family };
98
114
  }),
99
115
  );
@@ -185,6 +201,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
185
201
  ...(request.monthlyTokenLimit == null ? {} : { monthlyTokenLimit: request.monthlyTokenLimit }),
186
202
  ...(request.lifetimeTokenBudget == null ? {} : { lifetimeTokenBudget: request.lifetimeTokenBudget }),
187
203
  ...(request.maxConcurrentRequests == null ? {} : { maxConcurrentRequests: request.maxConcurrentRequests }),
204
+ ...(request.concurrencyScope !== undefined ? { concurrencyScope: request.concurrencyScope } : {}),
188
205
  ...(request.maxChildren == null ? {} : { maxChildren: request.maxChildren }),
189
206
  ...(request.modelPrefix === undefined ? {} : { modelPrefix: request.modelPrefix }),
190
207
  modelAccessMode: request.modelAccessMode ?? "whitelist",
@@ -320,6 +337,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
320
337
  ...(patchRequest.monthlyTokenLimit === undefined ? {} : { monthlyTokenLimit: patchRequest.monthlyTokenLimit }),
321
338
  ...(patchRequest.lifetimeTokenBudget === undefined ? {} : { lifetimeTokenBudget: patchRequest.lifetimeTokenBudget }),
322
339
  ...(patchRequest.maxConcurrentRequests === undefined ? {} : { maxConcurrentRequests: patchRequest.maxConcurrentRequests }),
340
+ ...(patchRequest.concurrencyScope !== undefined ? { concurrencyScope: patchRequest.concurrencyScope } : {}),
323
341
  ...(patchRequest.maxChildren === undefined ? {} : { maxChildren: patchRequest.maxChildren }),
324
342
  ...(patchRequest.modelPrefix === undefined ? {} : { modelPrefix: patchRequest.modelPrefix }),
325
343
  ...(patchRequest.modelAccessMode === undefined ? {} : { modelAccessMode: patchRequest.modelAccessMode }),
@@ -775,6 +793,86 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
775
793
  });
776
794
  return { success: true };
777
795
  },
796
+ async banKey(
797
+ access: AccessDecision | undefined,
798
+ keyId: string,
799
+ request: BanApiKeyRequest,
800
+ ): Promise<ApiKeyResponse> {
801
+ const authorized = requireTenantScope(access, "dashboard:write");
802
+ const reason = request.reason.trim();
803
+ if (reason.length === 0)
804
+ throw new ConsoleDomainError("invalid_request", 400, "reason is required");
805
+ // Parse the duration choice. `null` is permanent; absent defaults to one
806
+ // hour. Anything not on the offered list is rejected rather than clamped —
807
+ // clamping would answer a request for 90 days with a week and report
808
+ // success.
809
+ const chosenMinutes = request.minutes === undefined ? DEFAULT_BAN_MINUTES : request.minutes;
810
+ if (!BAN_DURATION_MINUTES.has(chosenMinutes)) {
811
+ throw new ConsoleDomainError(
812
+ "invalid_request",
813
+ 400,
814
+ "Unsupported ban duration. Choose one of the offered durations.",
815
+ );
816
+ }
817
+ const bannedUntil = chosenMinutes === null ? null : new Date(Date.now() + chosenMinutes * 60_000);
818
+ const current = await config.store.get(authorized.tenantId, keyId);
819
+ if (!current || current.revokedAt !== undefined)
820
+ throw new ConsoleDomainError("key_not_found", 404, "Key not found");
821
+ const banned = await config.store.ban(authorized.tenantId, keyId, reason, bannedUntil);
822
+ if (!banned) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
823
+ // A banned child key does not affect its share template or sibling keys:
824
+ // `store.ban` only turns off the one row named by `keyId`, and the auth
825
+ // path rejects per key hash. The template keeps issuing working children.
826
+ // A banned key must leave the auth cache for every process at once.
827
+ invalidateApiKeyCache();
828
+ await config.auditSink?.record({
829
+ access: authorized,
830
+ action: "api_key.banned",
831
+ target: keyId,
832
+ detail: {
833
+ reason,
834
+ // The human-readable label is derived server-side from the machine
835
+ // `reason`, so the audit trail cannot be skewed by a client-authored
836
+ // label. Unknown reasons (custom text) simply carry no label; the
837
+ // free-text `reason` above is still recorded verbatim.
838
+ ...(API_KEY_BAN_REASONS.find((r) => r.value === reason)?.label === undefined
839
+ ? {}
840
+ : { reasonLabel: API_KEY_BAN_REASONS.find((r) => r.value === reason)!.label }),
841
+ ...(bannedUntil === null ? { permanent: true } : { bannedUntil: bannedUntil.toISOString() }),
842
+ },
843
+ });
844
+ // The ban mutates the row already fetched; derive the response from that
845
+ // record instead of re-fetching after the write. Match the store's own
846
+ // mapping: a `null` (permanent) `bannedUntil` is omitted, not carried.
847
+ return sanitizeApiKeyResponse({
848
+ ...current,
849
+ bannedAt: new Date(),
850
+ banReason: reason,
851
+ ...(bannedUntil ? { bannedUntil } : {}),
852
+ });
853
+ },
854
+ async unbanKey(
855
+ access: AccessDecision | undefined,
856
+ keyId: string,
857
+ ): Promise<ApiKeyResponse> {
858
+ const authorized = requireTenantScope(access, "dashboard:write");
859
+ const current = await config.store.get(authorized.tenantId, keyId);
860
+ if (!current || current.revokedAt !== undefined)
861
+ throw new ConsoleDomainError("key_not_found", 404, "Key not found");
862
+ const unbanned = await config.store.unban(authorized.tenantId, keyId);
863
+ if (!unbanned) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
864
+ invalidateApiKeyCache();
865
+ await config.auditSink?.record({
866
+ access: authorized,
867
+ action: "api_key.unbanned",
868
+ target: keyId,
869
+ });
870
+ // Unban clears all three ban columns and re-enables the key; derive the
871
+ // response from the pre-fetch instead of re-fetching after the write.
872
+ const { bannedAt: _bannedAt, banReason: _banReason, bannedUntil: _bannedUntil, ...currentRest } = current;
873
+ const unbannedRecord: ApiKeyRecord = { ...currentRest, enabled: true };
874
+ return sanitizeApiKeyResponse(unbannedRecord);
875
+ },
778
876
  };
779
877
  return operations;
780
878
  }
@@ -793,6 +891,7 @@ const apiKeyBody = t.Object({
793
891
  monthlyTokenLimit: t.Optional(t.Union([t.Number(), t.Null()])),
794
892
  lifetimeTokenBudget: t.Optional(t.Union([t.Number(), t.Null()])),
795
893
  maxConcurrentRequests: t.Optional(t.Union([t.Number(), t.Null()])),
894
+ concurrencyScope: t.Optional(literalUnion(API_KEY_CONCURRENCY_SCOPES)),
796
895
  modelPrefix: t.Optional(t.String()),
797
896
  modelAccessMode: t.Optional(literalUnion(API_KEY_MODEL_ACCESS_MODES)),
798
897
  modelList: t.Optional(t.Array(t.String())),
@@ -830,7 +929,7 @@ function jsonNull(): Response {
830
929
  });
831
930
  }
832
931
  /** Creates tenant-bound API-key routes with request-local authorization. */
833
- export function createApiKeyRoutes(config: ApiKeyConfig): Elysia {
932
+ export function createApiKeyRoutes(config: ApiKeyConfig): AnyElysia {
834
933
  const factory = createApiKeyOperations(config);
835
934
  return new Elysia({ prefix: "/api-keys" })
836
935
  .get("/", async ({ request, set }) => {
@@ -950,6 +1049,32 @@ export function createApiKeyRoutes(config: ApiKeyConfig): Elysia {
950
1049
  return errorResponse(error, set, "API-key operation failed");
951
1050
  }
952
1051
  })
1052
+ .post(
1053
+ "/:keyId/ban",
1054
+ {
1055
+ body: t.Object({
1056
+ reason: t.String(),
1057
+ // `reasonLabel` is deliberately not accepted: the audit label is
1058
+ // derived server-side from `reason` (see `banKey`), so a client
1059
+ // cannot author the human-readable audit entry.
1060
+ minutes: t.Optional(t.Union([t.Number({ minimum: 1 }), t.Null()])),
1061
+ }),
1062
+ },
1063
+ async ({ request, params, body, set }) => {
1064
+ try {
1065
+ return await factory.banKey(config.accessResolver(request), params.keyId, body);
1066
+ } catch (error) {
1067
+ return errorResponse(error, set, "API-key operation failed");
1068
+ }
1069
+ },
1070
+ )
1071
+ .post("/:keyId/unban", async ({ request, params, set }) => {
1072
+ try {
1073
+ return await factory.unbanKey(config.accessResolver(request), params.keyId);
1074
+ } catch (error) {
1075
+ return errorResponse(error, set, "API-key operation failed");
1076
+ }
1077
+ })
953
1078
  .get("/:keyId/share", async ({ request, params, set }) => {
954
1079
  try {
955
1080
  const share = await factory.getShare(config.accessResolver(request), params.keyId, {
@@ -988,5 +1113,5 @@ export function createApiKeyRoutes(config: ApiKeyConfig): Elysia {
988
1113
  } catch (error) {
989
1114
  return errorResponse(error, set, "API-key operation failed");
990
1115
  }
991
- }) as unknown as Elysia;
1116
+ });
992
1117
  }
@@ -1,4 +1,4 @@
1
- import { Elysia, t } from "elysia";
1
+ import { Elysia, t, type AnyElysia } from "elysia";
2
2
  import { errorResponse, requireGlobalAdmin, requireTenantScope } from "../shared/errors";
3
3
  import { parseQueryLimit } from "../shared/query";
4
4
  import type { AuditRoutesConfig } from "./contracts";
@@ -10,7 +10,7 @@ const listQuery = t.Object({
10
10
  actor: t.Optional(t.String()),
11
11
  });
12
12
 
13
- export function createAuditRoutes(config: AuditRoutesConfig): Elysia {
13
+ export function createAuditRoutes(config: AuditRoutesConfig): AnyElysia {
14
14
  return new Elysia().get("/audit", { query: listQuery }, async ({ request, query, set }) => {
15
15
  try {
16
16
  const resolved = config.accessResolver(request);
@@ -28,5 +28,5 @@ export function createAuditRoutes(config: AuditRoutesConfig): Elysia {
28
28
  } catch (e) {
29
29
  return errorResponse(e, set, "Audit read failed");
30
30
  }
31
- }) as unknown as Elysia;
31
+ });
32
32
  }
@@ -1,4 +1,4 @@
1
- import type { AccessDecision } from "../../security/access-control";
2
-
3
- /** Resolves the authenticated console request to a tenant-scoped decision. */
4
- export type ConsoleAccessResolver = (request: Request) => AccessDecision | undefined;
1
+ import type { AccessDecision } from "../../security/access-control";
2
+
3
+ /** Resolves the authenticated console request to a tenant-scoped decision. */
4
+ export type ConsoleAccessResolver = (request: Request) => AccessDecision | undefined;
@@ -25,7 +25,7 @@
25
25
  import { GatewayError } from "../../protocol/gateway-error";
26
26
  import { isLoopbackPeer, tokenMatches } from "../../transport/drain-endpoint";
27
27
 
28
- export interface ConsoleAccountResetDeps {
28
+ interface ConsoleAccountResetDeps {
29
29
  /** Shared secret; the route is registered only when this is set. */
30
30
  readonly token: string;
31
31
  /** Removes every console user and session. Resolves to the number removed. */
@@ -42,12 +42,12 @@ export function createConsoleAccountResetHandler(
42
42
  return async ({ request, body }) => {
43
43
  if (!isLoopbackPeer(deps.resolvePeerAddress(request)))
44
44
  throw new GatewayError(
45
- "invalid_request",
45
+ "policy_rejected",
46
46
  403,
47
- "console account reset is only available from loopback",
47
+ "console account reset is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
48
48
  );
49
49
  if (!tokenMatches(request.headers.get("x-drain-token"), deps.token))
50
- throw new GatewayError("invalid_request", 401, "invalid reset token");
50
+ throw new GatewayError("authentication_failed", 401, "invalid reset token (request {requestId}). Wrong key, right door — check the token 🔑");
51
51
  const confirm =
52
52
  body !== null && typeof body === "object" && "confirm" in body
53
53
  ? body.confirm
@@ -56,7 +56,7 @@ export function createConsoleAccountResetHandler(
56
56
  throw new GatewayError(
57
57
  "invalid_request",
58
58
  400,
59
- `send {"confirm":"${CONFIRMATION}"} to confirm; this deletes every console account`,
59
+ `send {"confirm":"${CONFIRMATION}"} to confirm (request {requestId}); this deletes every console account. Make sure you mean it — there's no undo 🫂`,
60
60
  );
61
61
 
62
62
  const removed = await deps.resetConsoleAccount();
@@ -8,7 +8,7 @@ import {
8
8
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
9
9
  import { type AccessDecision } from "../../security/access-control";
10
10
 
11
- export interface AuditRecordInput {
11
+ interface AuditRecordInput {
12
12
  readonly access: AccessDecision;
13
13
  readonly action: string;
14
14
  readonly target: string;
@@ -7,8 +7,9 @@ import {
7
7
  } from "../../persistence/schema";
8
8
 
9
9
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
10
+ import { log } from "../../observability/logger";
10
11
 
11
- export class ConsoleLockoutStore {
12
+ class ConsoleLockoutStore {
12
13
  constructor(private readonly db: CartethyiaDatabase) {}
13
14
 
14
15
  async getLockedUntil(ip: string): Promise<Date | null | undefined> {
@@ -143,8 +144,11 @@ export class ConsoleLockoutService {
143
144
  enforcement: "full_network_lockout",
144
145
  },
145
146
  });
146
- } catch {
147
- // Best-effort audit logging; the ban itself remains enforced via the DB row.
147
+ } catch (error) {
148
+ // Best-effort audit logging; the ban itself remains enforced via the DB
149
+ // row. A failed write is logged — for a security-relevant event
150
+ // (`security.ip_banned`) total silence would hide a broken audit trail.
151
+ log.error(`[auth] failed to record ip_banned audit for ${ip}`, error as Error, reason);
148
152
  }
149
153
  }
150
154
 
@@ -3,7 +3,7 @@ import type { ConsoleSession, ConsoleUser } from "../../persistence/schema";
3
3
 
4
4
  import { isRecord } from "../../protocol/codecs";
5
5
 
6
- export function asDate(value: unknown): Date | undefined {
6
+ function asDate(value: unknown): Date | undefined {
7
7
  if (value instanceof Date && !Number.isNaN(value.getTime())) return value;
8
8
  if (typeof value === "string" || typeof value === "number") {
9
9
  const date = new Date(value);
@@ -14,33 +14,78 @@ export function asDate(value: unknown): Date | undefined {
14
14
 
15
15
  export function sessionRecord(value: unknown): ConsoleSession | undefined {
16
16
  if (!isRecord(value)) return undefined;
17
+ const id = value.id;
18
+ const userId = value.userId;
19
+ const sessionToken = value.sessionToken;
20
+ const ipAddress = value.ipAddress;
21
+ const userAgent = value.userAgent;
17
22
  if (
18
- typeof value.id !== "string" ||
19
- typeof value.userId !== "string" ||
20
- typeof value.sessionToken !== "string"
23
+ typeof id !== "string" ||
24
+ typeof userId !== "string" ||
25
+ typeof sessionToken !== "string"
21
26
  ) {
22
27
  return undefined;
23
28
  }
24
29
  const expiresAt = asDate(value.expiresAt);
25
30
  const createdAt = asDate(value.createdAt);
26
31
  if (!expiresAt || !createdAt) return undefined;
27
- return { ...value, expiresAt, createdAt } as unknown as ConsoleSession;
32
+ // Construct the result explicitly field-by-field so the output shape is
33
+ // exactly what was validated — arbitrary extra keys on a driver row must
34
+ // not survive into the typed session.
35
+ return {
36
+ id,
37
+ userId,
38
+ sessionToken,
39
+ ...(ipAddress === null || typeof ipAddress === "string" ? { ipAddress } : { ipAddress: null }),
40
+ ...(userAgent === null || typeof userAgent === "string" ? { userAgent } : { userAgent: null }),
41
+ expiresAt,
42
+ createdAt,
43
+ };
28
44
  }
29
45
 
30
46
  export function userRecord(value: unknown): ConsoleUser | undefined {
31
47
  if (!isRecord(value)) return undefined;
48
+ const id = value.id;
49
+ const username = value.username;
50
+ const passwordHash = value.passwordHash;
51
+ const isActive = value.isActive;
32
52
  if (
33
- typeof value.id !== "string" ||
34
- typeof value.username !== "string" ||
35
- typeof value.passwordHash !== "string" ||
36
- typeof value.isActive !== "boolean"
53
+ typeof id !== "string" ||
54
+ typeof username !== "string" ||
55
+ typeof passwordHash !== "string" ||
56
+ typeof isActive !== "boolean"
37
57
  ) {
38
58
  return undefined;
39
59
  }
40
- return value as unknown as ConsoleUser;
60
+ // Timestamps are coerced through `asDate` because SQLite hands them back as
61
+ // text; a login path that rejects non-Date values must still work there.
62
+ const createdAt = asDate(value.createdAt);
63
+ const updatedAt = asDate(value.updatedAt);
64
+ if (!createdAt || !updatedAt) return undefined;
65
+ const tenantId = value.tenantId;
66
+ const email = value.email;
67
+ const displayName = value.displayName;
68
+ const isFirstBoot = value.isFirstBoot;
69
+ const isPlatformAdmin = value.isPlatformAdmin;
70
+ // Construct the result explicitly field-by-field so the output shape is
71
+ // exactly what was validated — arbitrary extra keys on a driver row must
72
+ // not survive into the typed user.
73
+ return {
74
+ id,
75
+ tenantId: typeof tenantId === "string" ? tenantId : "",
76
+ username,
77
+ email: typeof email === "string" ? email : null,
78
+ passwordHash,
79
+ displayName: typeof displayName === "string" ? displayName : null,
80
+ isActive,
81
+ isPlatformAdmin: isPlatformAdmin === true,
82
+ isFirstBoot: isFirstBoot === true,
83
+ createdAt,
84
+ updatedAt,
85
+ };
41
86
  }
42
87
 
43
- export function mutationAffectedRows(value: unknown): number | undefined {
88
+ function mutationAffectedRows(value: unknown): number | undefined {
44
89
  if (!isRecord(value)) return undefined;
45
90
  for (const key of ["rowCount", "rowsAffected", "changes"] as const) {
46
91
  const count = value[key];
@@ -50,7 +95,7 @@ export function mutationAffectedRows(value: unknown): number | undefined {
50
95
  }
51
96
 
52
97
  /** Awaitable Drizzle mutation builder that may expose `.returning()`. */
53
- export interface ReturningBuilder extends PromiseLike<unknown> {
98
+ interface ReturningBuilder extends PromiseLike<unknown> {
54
99
  returning?: (...columns: unknown[]) => PromiseLike<unknown[]>;
55
100
  }
56
101
 
@@ -45,7 +45,7 @@ export async function resolveConsoleAccess(
45
45
  }
46
46
 
47
47
  /** The signed-in console user behind a request, or `null` when unauthenticated. */
48
- export interface ResolvedConsoleUser {
48
+ interface ResolvedConsoleUser {
49
49
  readonly id: string;
50
50
  readonly sessionId: string;
51
51
  readonly tenantId: string;
@@ -58,7 +58,7 @@ function newSessionToken(): string {
58
58
  return randomBytes(32).toString("base64url");
59
59
  }
60
60
 
61
- export class ConsoleSessionStore {
61
+ class ConsoleSessionStore {
62
62
  constructor(private readonly db: CartethyiaDatabase) {}
63
63
 
64
64
  async create(newSession: {
@@ -73,7 +73,13 @@ export class ConsoleSessionStore {
73
73
  const inserted = await returningRows(this.db.insert(consoleSessions).values(newSession));
74
74
  assertMutationApplied(inserted, "Console session");
75
75
  const persisted = sessionRecord(inserted.rows[0]);
76
- return persisted ?? ({ ...newSession } as unknown as ConsoleSession);
76
+ if (!persisted) {
77
+ // `assertMutationApplied` already refused a failed insert, so an
78
+ // unshaped row here is a driver/schema mismatch — pretending success by
79
+ // returning the input cast to ConsoleSession would mask it.
80
+ throw new Error("Console session row could not be coerced after insert");
81
+ }
82
+ return persisted;
77
83
  }
78
84
 
79
85
  async findValidByTokenHash(tokenHash: string, now: Date): Promise<ConsoleSession | null> {