cartethyia 2.7.4 → 2.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (595) hide show
  1. package/.env.example +5 -0
  2. package/README.md +18 -6
  3. package/dist/dashboard/assets/BannedPage-B1N603cM.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-B1N603cM.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js +3 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js.gz +0 -0
  7. package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js +1 -0
  8. package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-D762MIkp.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-D762MIkp.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js.gz +0 -0
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js +899 -0
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js.gz +0 -0
  19. package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js +1 -0
  20. package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-BMS2t8BA.js → ModelPicker-C6WlpU5E.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js +5 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-lsrx7NYg.js → ProxyPage-DxjvUzSD.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-DxjvUzSD.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js.gz +0 -0
  37. package/dist/dashboard/assets/SetupPage-44BZrxFC.js +1 -0
  38. package/dist/dashboard/assets/SetupPage-44BZrxFC.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-Cq5tW7hW.js → StudioPage-DRPavCdU.js} +4 -4
  40. package/dist/dashboard/assets/StudioPage-DRPavCdU.js.gz +0 -0
  41. package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js +1 -0
  42. package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js.gz +0 -0
  43. package/dist/dashboard/assets/UsagePage-B7M7LvMM.js +56 -0
  44. package/dist/dashboard/assets/UsagePage-B7M7LvMM.js.gz +0 -0
  45. package/dist/dashboard/assets/api-keys-KvX83mRe.js +1 -0
  46. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  47. package/dist/dashboard/assets/app-DRnQLhsc.js +9 -0
  48. package/dist/dashboard/assets/app-DRnQLhsc.js.gz +0 -0
  49. package/dist/dashboard/assets/app-mweRcsKp.css +2 -0
  50. package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
  51. package/dist/dashboard/assets/assert-CidQVoNo.js +1 -0
  52. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  53. package/dist/dashboard/assets/{badge-D9CS6dZk.js → badge-BaX2GTrn.js} +1 -1
  54. package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
  55. package/dist/dashboard/assets/{banner-DympBNq4.js → banner-DFH4Bkte.js} +1 -1
  56. package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
  57. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  58. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  59. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  60. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  61. package/dist/dashboard/assets/{card-ibRWU_uT.js → card-CxDw0lK6.js} +1 -1
  62. package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
  63. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js +1 -0
  64. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js +1 -0
  66. package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js.gz +0 -0
  67. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +1 -0
  68. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  69. package/dist/dashboard/assets/{clipboard-button-DJm-9pz7.js → clipboard-button-DfS1ntiq.js} +1 -1
  70. package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
  71. package/dist/dashboard/assets/dialog-BK-NRr5u.js +1 -0
  72. package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
  73. package/dist/dashboard/assets/{drawer-CqGYeOrd.js → drawer-QqPul6sC.js} +1 -1
  74. package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
  75. package/dist/dashboard/assets/entry-BMlYJVFj.js +1 -0
  76. package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
  77. package/dist/dashboard/assets/entry-D6WvojeD.js +4 -0
  78. package/dist/dashboard/assets/entry-D6WvojeD.js.gz +0 -0
  79. package/dist/dashboard/assets/entry-Dmx7gBiN.js +1 -0
  80. package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
  81. package/dist/dashboard/assets/{formatters-D-MTJxMr.js → formatters-u7JPB5BX.js} +1 -1
  82. package/dist/dashboard/assets/formatters-u7JPB5BX.js.gz +0 -0
  83. package/dist/dashboard/assets/house-h9AZlJuT.js +1 -0
  84. package/dist/dashboard/assets/house-h9AZlJuT.js.gz +0 -0
  85. package/dist/dashboard/assets/{index-DBNPOfhV.js → index-Ko4dLRnV.js} +2 -2
  86. package/dist/dashboard/assets/index-Ko4dLRnV.js.gz +0 -0
  87. package/dist/dashboard/assets/{inline-BT0zntEr.js → inline-CHJQwxLa.js} +1 -1
  88. package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
  89. package/dist/dashboard/assets/{input-Da2ZViWg.js → input-BlmlizVV.js} +1 -1
  90. package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
  91. package/dist/dashboard/assets/{layout-BXNeLw6M.js → layout-Bz93I-yE.js} +1 -1
  92. package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
  93. package/dist/dashboard/assets/{list-row-BycOrrIV.js → list-row-Ml16KCfu.js} +1 -1
  94. package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
  95. package/dist/dashboard/assets/live-7_QpnYGu.js +1 -0
  96. package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
  97. package/dist/dashboard/assets/{mutation-Rr0TIvcj.js → mutation-GU7XdEjz.js} +1 -1
  98. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  99. package/dist/dashboard/assets/network-CuMDaTzT.js +1 -0
  100. package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
  101. package/dist/dashboard/assets/numbers-CoqpaDMx.js +2 -0
  102. package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
  103. package/dist/dashboard/assets/pagination-B5aOoweZ.js +1 -0
  104. package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
  105. package/dist/dashboard/assets/providers/zai.webp +0 -0
  106. package/dist/dashboard/assets/providers/zcode.webp +0 -0
  107. package/dist/dashboard/assets/{query-keys-BloKtaXJ.js → query-keys-YSoQOUh6.js} +1 -1
  108. package/dist/dashboard/assets/{query-keys-BloKtaXJ.js.gz → query-keys-YSoQOUh6.js.gz} +0 -0
  109. package/dist/dashboard/assets/quota-DJ1QlAOq.js +1 -0
  110. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  111. package/dist/dashboard/assets/routing-fnCsxgNO.js +1 -0
  112. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  113. package/dist/dashboard/assets/{select-BIYIBkMU.js → select-BHqpd-bg.js} +2 -2
  114. package/dist/dashboard/assets/select-BHqpd-bg.js.gz +0 -0
  115. package/dist/dashboard/assets/settings-BxslsErn.js +1 -0
  116. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  117. package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
  118. package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
  119. package/dist/dashboard/assets/{stack-DfgnFDWY.js → stack-j7cXjgYX.js} +1 -1
  120. package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
  121. package/dist/dashboard/assets/{state-CkfdYWel.js → state-DHmEnLa2.js} +1 -1
  122. package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
  123. package/dist/dashboard/assets/{switch-SYm237HG.js → switch-DmZcyTIC.js} +1 -1
  124. package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
  125. package/dist/dashboard/assets/{useMutation-BQqCRNIi.js → useMutation-CbCP4w1o.js} +1 -1
  126. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  127. package/dist/dashboard/console-backgrounds.jpg +0 -0
  128. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  129. package/dist/dashboard/index.html +2 -2
  130. package/dist/dashboard/index.html.gz +0 -0
  131. package/dist/dashboard/precompressed-assets.json +60 -59
  132. package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
  133. package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
  134. package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
  135. package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
  136. package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
  137. package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
  138. package/migrations/full-postgres/0042_add_telemetry_degenerate_stop.sql +14 -0
  139. package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
  140. package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
  141. package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
  142. package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
  143. package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
  144. package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
  145. package/migrations/lite-sqlite3/0042_add_telemetry_degenerate_stop.sql +12 -0
  146. package/package.json +4 -6
  147. package/scripts/cli/update.ts +15 -41
  148. package/src/boot/app.ts +58 -2
  149. package/src/boot/dependencies.ts +794 -732
  150. package/src/boot/lifecycle.ts +11 -12
  151. package/src/config/cooldowns.ts +34 -55
  152. package/src/config/network.ts +1 -1
  153. package/src/config/package-paths.ts +1 -1
  154. package/src/config/providers.ts +19 -14
  155. package/src/config/resources.ts +0 -5
  156. package/src/config/runtime.ts +2 -5
  157. package/src/config/spec.ts +33 -6
  158. package/src/config/usage.ts +27 -0
  159. package/src/console/api-keys/ban-constants.ts +38 -0
  160. package/src/console/api-keys/contracts.ts +71 -62
  161. package/src/console/api-keys/routes.ts +56 -24
  162. package/src/console/auth/access.ts +4 -4
  163. package/src/console/auth/account-reset.ts +1 -1
  164. package/src/console/auth/audit-recorder.ts +1 -1
  165. package/src/console/auth/lockout.ts +7 -3
  166. package/src/console/auth/row-coercion.ts +57 -12
  167. package/src/console/auth/session-resolver.ts +1 -1
  168. package/src/console/auth/session-service.ts +8 -2
  169. package/src/console/auth/session.ts +37 -31
  170. package/src/console/auth/setup-service.ts +4 -3
  171. package/src/console/auth/user-store.ts +1 -1
  172. package/src/console/backup/9router-map.ts +0 -3
  173. package/src/console/backup/contracts.ts +9 -9
  174. package/src/console/backup/export-endpoint.ts +5 -6
  175. package/src/console/backup/nine-router.ts +1 -7
  176. package/src/console/backup/routes.ts +29 -33
  177. package/src/console/backup/service.ts +468 -468
  178. package/src/console/backup/store.ts +47 -29
  179. package/src/console/backup/validate.ts +11 -1
  180. package/src/console/cli-tools/contracts.ts +43 -299
  181. package/src/console/cli-tools/fs-ops.ts +29 -0
  182. package/src/console/cli-tools/injectors/claude.ts +3 -3
  183. package/src/console/cli-tools/injectors/codex.ts +181 -181
  184. package/src/console/cli-tools/injectors/driver.ts +150 -247
  185. package/src/console/cli-tools/injectors/kilo.ts +146 -21
  186. package/src/console/cli-tools/injectors/opencode.ts +176 -153
  187. package/src/console/cli-tools/routes.ts +7 -1
  188. package/src/console/cli-tools/service.ts +8 -7
  189. package/src/console/cli-tools/store.ts +2 -2
  190. package/src/console/console-router.ts +32 -0
  191. package/src/console/dashboard-assets.ts +9 -9
  192. package/src/console/domain-registration.ts +16 -2
  193. package/src/console/inferhub/client.ts +0 -7
  194. package/src/console/inferhub/key-store.ts +1 -1
  195. package/src/console/inferhub/normalize.ts +2 -2
  196. package/src/console/inferhub/routes.ts +1 -1
  197. package/src/console/inferhub/types.ts +1 -1
  198. package/src/console/observability/contracts.ts +7 -1
  199. package/src/console/observability/live.ts +31 -18
  200. package/src/console/observability/logs.ts +1 -1
  201. package/src/console/observability/sse.ts +62 -8
  202. package/src/console/observability/store.ts +23 -4
  203. package/src/console/providers/catalog/catalog-projections.ts +1 -1
  204. package/src/console/providers/catalog/contracts.ts +39 -0
  205. package/src/console/providers/catalog/model-operations.ts +1 -1
  206. package/src/console/providers/catalog/provider-operations.ts +95 -17
  207. package/src/console/providers/catalog/public-model-store.ts +0 -0
  208. package/src/console/providers/catalog/routes.ts +27 -3
  209. package/src/console/providers/catalog/store.ts +147 -8
  210. package/src/console/providers/detail/routes.ts +1 -1
  211. package/src/console/providers/oauth/callback-listener.ts +100 -18
  212. package/src/console/providers/oauth/routes.ts +92 -13
  213. package/src/console/quota/account-quota-global.ts +41 -3
  214. package/src/console/quota/account-quota-shared.ts +10 -3
  215. package/src/console/quota/account-quota-tenant.ts +259 -66
  216. package/src/console/quota/daily-growth-job.ts +308 -0
  217. package/src/console/quota/native-identity.ts +2 -2
  218. package/src/console/quota/quota-view.ts +1 -1
  219. package/src/console/remote-support/routes.ts +1 -1
  220. package/src/console/remote-support/service.ts +2 -2
  221. package/src/console/routing/model/contracts.ts +1 -1
  222. package/src/console/routing/pools/contracts.ts +2 -2
  223. package/src/console/routing/pools/import-parse.ts +3 -3
  224. package/src/console/routing/pools/relay-deploy.ts +3 -3
  225. package/src/console/settings/contracts.ts +29 -2
  226. package/src/console/settings/store.ts +20 -0
  227. package/src/console/share/share-router.ts +100 -32
  228. package/src/console/share/share-stats.ts +11 -13
  229. package/src/console/share/share-usage.ts +1 -1
  230. package/src/console/shared/errors.ts +2 -2
  231. package/src/console/shared/object.ts +18 -0
  232. package/src/console/shared/read-through-cache.ts +4 -4
  233. package/src/console/shared/transaction.ts +14 -0
  234. package/src/console/system-prompt/contracts.ts +89 -12
  235. package/src/console/system-prompt/routes.ts +98 -45
  236. package/src/console/updates/apply-update.ts +244 -0
  237. package/src/console/updates/routes.ts +252 -97
  238. package/src/main.ts +11 -5
  239. package/src/network/http2-bidi.ts +1 -1
  240. package/src/network/ip-utils.ts +298 -0
  241. package/src/network/outbound-fetch.ts +1 -1
  242. package/src/network/pool/agent.ts +7 -6
  243. package/src/network/pool/byte-accounting.ts +1 -1
  244. package/src/network/pool/resolver.ts +16 -12
  245. package/src/network/pool/selector.ts +150 -136
  246. package/src/network/pool-health-machine.ts +1 -1
  247. package/src/network/rate-limit-verdict.ts +1 -1
  248. package/src/network/retry.ts +1 -1
  249. package/src/network/ssrf.ts +20 -71
  250. package/src/observability/event-log.ts +9 -2
  251. package/src/observability/event-query.ts +61 -291
  252. package/src/observability/log-sanitize.ts +1 -1
  253. package/src/observability/metrics.ts +4 -4
  254. package/src/observability/payload-capture.ts +6 -5
  255. package/src/observability/payload-store.ts +1 -1
  256. package/src/observability/runtime-metrics.ts +1 -1
  257. package/src/observability/telemetry-buffer.ts +19 -3
  258. package/src/observability/telemetry-reconciliation.ts +3 -3
  259. package/src/observability/token-speed.ts +1 -1
  260. package/src/persistence/connection-url.ts +1 -1
  261. package/src/persistence/db-handle.ts +1 -1
  262. package/src/persistence/db-pg.ts +2 -2
  263. package/src/persistence/db-sqlite.ts +3 -3
  264. package/src/persistence/dialect.ts +5 -1
  265. package/src/persistence/readiness.ts +1 -1
  266. package/src/persistence/redis.ts +1 -1
  267. package/src/persistence/schema.ts +121 -40
  268. package/src/persistence/shared-cache.ts +2 -2
  269. package/src/persistence/sort-order.ts +1 -1
  270. package/src/persistence/stores/api-key-store.ts +29 -2
  271. package/src/persistence/stores/share-store.ts +20 -8
  272. package/src/persistence/system-prompt-store.ts +63 -18
  273. package/src/persistence/tenant-preferences.ts +1 -1
  274. package/src/persistence/ttl-cache.ts +3 -3
  275. package/src/persistence/usage-flush.ts +7 -2
  276. package/src/protocol/canonical-model.ts +1 -1
  277. package/src/protocol/codecs.ts +6 -10
  278. package/src/protocol/dsml.ts +8 -125
  279. package/src/protocol/image-clamp.ts +2 -2
  280. package/src/protocol/reasoning-text.ts +1 -1
  281. package/src/protocol/reasoning.ts +7 -52
  282. package/src/protocol/registry.ts +50 -15
  283. package/src/protocol/request/chat.ts +46 -19
  284. package/src/protocol/request/codex.ts +1 -1
  285. package/src/protocol/request/messages.ts +46 -14
  286. package/src/protocol/request/responses.ts +19 -10
  287. package/src/protocol/response/chat.ts +25 -10
  288. package/src/protocol/response/codex.ts +1 -1
  289. package/src/protocol/response/messages.ts +1 -1
  290. package/src/protocol/response/omp-tool-call-markup.ts +1 -1
  291. package/src/protocol/response/responses.ts +1 -1
  292. package/src/protocol/sse-decoder.ts +50 -1
  293. package/src/protocol/stop-reason.ts +2 -2
  294. package/src/protocol/think-split.ts +347 -0
  295. package/src/protocol/translation/attachment-integrity.ts +3 -3
  296. package/src/protocol/translation/cache-controls.ts +8 -20
  297. package/src/protocol/translation/capabilities.ts +6 -6
  298. package/src/protocol/translation/dialect/deepseek.ts +10 -0
  299. package/src/protocol/translation/dialect/inband.ts +39 -0
  300. package/src/protocol/translation/dialect/model-thinking.ts +89 -0
  301. package/src/protocol/translation/dialect/qwen.ts +114 -0
  302. package/src/protocol/translation/dialect/resolver.ts +83 -0
  303. package/src/protocol/translation/dialect/rules.ts +93 -0
  304. package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
  305. package/src/protocol/translation/dialect/types.ts +58 -0
  306. package/src/protocol/translation/quirks.ts +18 -4
  307. package/src/protocol/translation/thinking.ts +12 -252
  308. package/src/protocol/translation/tool-id.ts +3 -3
  309. package/src/protocol/translation/tool-repair.ts +1 -1
  310. package/src/protocol/upstream/post-json.ts +6 -1
  311. package/src/providers/adapters/compatible-adapter.ts +71 -4
  312. package/src/providers/authentication/jwt-validator.ts +0 -5
  313. package/src/providers/authentication/oauth-client.ts +254 -253
  314. package/src/providers/authentication/oauth-flow-store.ts +42 -7
  315. package/src/providers/bundled-discovery-providers.ts +6 -30
  316. package/src/providers/configured-openai-providers.ts +82 -97
  317. package/src/providers/default-registry.ts +35 -37
  318. package/src/providers/discovery/catalog/catalog-format.ts +13 -2
  319. package/src/providers/discovery/discovery-types.ts +0 -1
  320. package/src/providers/discovery/openai-model-discovery.ts +2 -2
  321. package/src/providers/discovery/probe-phases.ts +48 -13
  322. package/src/providers/discovery/probing-service.ts +6 -8
  323. package/src/providers/integrations/agentrouter/agentrouter.ts +12 -2
  324. package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
  325. package/src/providers/integrations/antigravity/antigravity-protocol.ts +88 -28
  326. package/src/providers/integrations/antigravity/antigravity-quota.ts +18 -6
  327. package/src/providers/integrations/antigravity/antigravity.ts +22 -13
  328. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
  329. package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
  330. package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
  331. package/src/providers/integrations/buddy/buddy-identity.ts +86 -0
  332. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
  333. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
  334. package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -15
  335. package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
  336. package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -9
  337. package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
  338. package/src/providers/integrations/claude/claude-cch.ts +148 -148
  339. package/src/providers/integrations/claude/fallback-credit.ts +3 -7
  340. package/src/providers/integrations/cline/cline.ts +1 -1
  341. package/src/providers/integrations/codex/codex-device-code.ts +101 -101
  342. package/src/providers/integrations/codex/codex-oauth.ts +0 -1
  343. package/src/providers/integrations/commandcode/commandcode.ts +75 -115
  344. package/src/providers/integrations/commandcode/quota.ts +131 -51
  345. package/src/providers/integrations/cursor/cursor-http2.ts +0 -3
  346. package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
  347. package/src/providers/integrations/cursor/cursor-request.ts +0 -87
  348. package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
  349. package/src/providers/integrations/cursor/cursor.ts +35 -3
  350. package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
  351. package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
  352. package/src/providers/integrations/factory/factory.ts +9 -9
  353. package/src/providers/integrations/github/github-discovery.ts +70 -12
  354. package/src/providers/integrations/github/github-oauth.ts +184 -14
  355. package/src/providers/integrations/github/github.ts +134 -26
  356. package/src/providers/integrations/grok/grok-oauth.ts +261 -261
  357. package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
  358. package/src/providers/integrations/grok/grok-quota.ts +16 -7
  359. package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
  360. package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
  361. package/src/providers/integrations/grok/grok.ts +86 -36
  362. package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
  363. package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
  364. package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
  365. package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
  366. package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
  367. package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
  368. package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
  369. package/src/providers/integrations/kiro/kiro-request.ts +8 -11
  370. package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
  371. package/src/providers/integrations/kiro/kiro.ts +80 -68
  372. package/src/providers/integrations/muse/muse-oauth.ts +0 -1
  373. package/src/providers/integrations/muse/muse-quota.ts +0 -4
  374. package/src/providers/integrations/opencode/fingerprint.ts +7 -5
  375. package/src/providers/integrations/opencode/provider.ts +17 -24
  376. package/src/providers/integrations/qoder/qoder-catalog.ts +94 -11
  377. package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
  378. package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
  379. package/src/providers/integrations/qoder/qoder-realm.ts +10 -31
  380. package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
  381. package/src/providers/integrations/qoder/qoder-umid.ts +21 -25
  382. package/src/providers/integrations/qoder/qoder-virtual.ts +12 -0
  383. package/src/providers/integrations/qoder/qoder.ts +422 -71
  384. package/src/providers/integrations/shared/connect.ts +0 -54
  385. package/src/providers/integrations/shared/hash.ts +15 -0
  386. package/src/providers/integrations/shared/session-cache.ts +136 -0
  387. package/src/providers/integrations/trae/trae-cache.ts +7 -87
  388. package/src/providers/integrations/trae/trae-catalog.ts +1 -6
  389. package/src/providers/integrations/trae/trae-protocol.ts +17 -8
  390. package/src/providers/integrations/trae/trae-realm.ts +0 -7
  391. package/src/providers/integrations/trae/trae.ts +51 -8
  392. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  393. package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
  394. package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
  395. package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
  396. package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
  397. package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
  398. package/src/providers/operations/account-error-classification.ts +198 -133
  399. package/src/providers/operations/account-health-service.ts +31 -39
  400. package/src/providers/operations/account-reset-service.ts +58 -1
  401. package/src/providers/operations/byok-wire-profile.ts +19 -15
  402. package/src/providers/operations/cli-platform.ts +31 -5
  403. package/src/providers/operations/client-version-resolver.ts +4 -6
  404. package/src/providers/operations/client-versions.ts +10 -7
  405. package/src/providers/operations/provider-catalog-seeder.ts +2 -0
  406. package/src/providers/operations/provider-catalog-service.ts +18 -2
  407. package/src/providers/operations/provider-credential-service.ts +19 -2
  408. package/src/providers/provider-contracts.ts +18 -7
  409. package/src/providers/provider-metadata.ts +17 -2
  410. package/src/providers/provider-model-definition.ts +22 -1
  411. package/src/providers/provider-probe.ts +4 -2
  412. package/src/providers/provider-usage.ts +50 -19
  413. package/src/providers/quota/cache.ts +29 -0
  414. package/src/providers/quota/quota-contracts.ts +4 -0
  415. package/src/providers/quota/refresh.ts +27 -4
  416. package/src/providers/quota/snapshot.ts +94 -0
  417. package/src/providers/search/search-provider.ts +1 -3
  418. package/src/providers/search/search-providers.ts +2 -1
  419. package/src/security/abuse.ts +4 -4
  420. package/src/security/admission/service.ts +13 -6
  421. package/src/security/admission/ttl.ts +61 -61
  422. package/src/security/api-key-auth.ts +17 -3
  423. package/src/security/client-router-fingerprint.ts +4 -4
  424. package/src/security/credential-codec.ts +3 -3
  425. package/src/security/csrf.ts +2 -2
  426. package/src/security/ip-boundary.ts +16 -189
  427. package/src/security/model-access-rule.ts +1 -1
  428. package/src/security/outbound-headers.ts +18 -0
  429. package/src/shared/invoke-guard.ts +47 -0
  430. package/src/transport/contracts.ts +13 -4
  431. package/src/transport/dispatch/attempt-finalize.ts +70 -22
  432. package/src/transport/dispatch/attempt-loop.ts +22 -0
  433. package/src/transport/dispatch/capture-policy.ts +34 -37
  434. package/src/transport/dispatch/empty-stream.ts +23 -20
  435. package/src/transport/dispatch/generation.ts +25 -45
  436. package/src/transport/dispatch/native-shared.ts +127 -0
  437. package/src/transport/dispatch/proxy-request.ts +3 -1
  438. package/src/transport/dispatch/responses-compact.ts +25 -46
  439. package/src/transport/dispatch/streaming/encoder.ts +18 -72
  440. package/src/transport/dispatch/streaming/finalize.ts +2 -2
  441. package/src/transport/dispatch/streaming/stream.ts +7 -3
  442. package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
  443. package/src/transport/dispatch/systemone.ts +20 -48
  444. package/src/transport/dispatch/token-count.ts +41 -38
  445. package/src/transport/dispatch/websearch.ts +19 -45
  446. package/src/transport/drain-endpoint.ts +1 -1
  447. package/src/transport/failure-policy.ts +21 -1
  448. package/src/transport/guards/auth-guard.ts +0 -6
  449. package/src/transport/guards/body-policy.ts +3 -3
  450. package/src/transport/guards/error-lifecycle.ts +4 -0
  451. package/src/transport/guards/headroom-guard.ts +1 -1
  452. package/src/transport/guards/pipeline.ts +1 -1
  453. package/src/transport/guards/request-context.ts +12 -17
  454. package/src/transport/request/preparer.ts +286 -7
  455. package/src/transport/request/rtk/compress-request.ts +3 -3
  456. package/src/transport/request/rtk/rtk-constants.ts +2 -2
  457. package/src/transport/request/rtk/rtk-filters.ts +8 -8
  458. package/src/transport/request/state.ts +7 -13
  459. package/src/transport/request/system-prompt-injection.ts +74 -11
  460. package/src/transport/request/tenant-preferences.ts +22 -0
  461. package/src/transport/routing/candidate-index.ts +2 -2
  462. package/src/transport/routing/round-robin.ts +1 -1
  463. package/src/transport/routing/route-catalog.ts +17 -18
  464. package/src/transport/routing/route-model.ts +126 -33
  465. package/src/transport/routing/router.ts +159 -37
  466. package/src/transport/shutdown-notice.ts +2 -2
  467. package/src/transport/surface/adapters.ts +3 -3
  468. package/src/transport/surface/chat/encode.ts +7 -7
  469. package/src/transport/surface/chat/parse.ts +7 -7
  470. package/src/transport/surface/completion.ts +2 -2
  471. package/src/transport/surface/content-parts.ts +1 -1
  472. package/src/transport/surface/dialects.ts +2 -2
  473. package/src/transport/surface/messages/parse.ts +3 -3
  474. package/src/transport/surface/reasoning-summary.ts +9 -14
  475. package/src/transport/surface/responses/adapter.ts +1 -1
  476. package/src/transport/surface/responses/errors.ts +0 -9
  477. package/src/transport/vision-handoff-cache.ts +58 -0
  478. package/src/transport/vision-handoff.ts +431 -0
  479. package/src/workers/account-identity-sweep.ts +2 -2
  480. package/src/workers/buddy-growth-tasks.ts +392 -346
  481. package/src/workers/daily-checkin.ts +102 -49
  482. package/src/workers/maintenance-lease.ts +2 -35
  483. package/src/workers/oauth-refresh-sweep.ts +8 -1
  484. package/src/workers/quota-refresh-sweep.ts +120 -82
  485. package/src/workers/scheduler.ts +67 -4
  486. package/src/workers/sweep-runner.ts +24 -5
  487. package/src/workers/zcode-gift-claim.ts +108 -0
  488. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
  489. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
  490. package/dist/dashboard/assets/BannedPage-ChG5svFb.js +0 -1
  491. package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
  492. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js +0 -1
  493. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
  494. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js +0 -1
  495. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
  496. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js +0 -1
  497. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
  498. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +0 -1
  499. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
  500. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +0 -1
  501. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
  502. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +0 -1
  503. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
  504. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js +0 -1
  505. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
  506. package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
  507. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +0 -1
  508. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
  509. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +0 -5
  510. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
  511. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
  512. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  513. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +0 -1
  514. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
  515. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
  516. package/dist/dashboard/assets/QuotaPage-L0skzML8.js +0 -1
  517. package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
  518. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +0 -1
  519. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
  520. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js +0 -1
  521. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
  522. package/dist/dashboard/assets/SortableList-ClzpwyET.js +0 -1
  523. package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
  524. package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
  525. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js +0 -1
  526. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  527. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +0 -56
  528. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
  529. package/dist/dashboard/assets/api-keys-C38iXoO4.js +0 -1
  530. package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
  531. package/dist/dashboard/assets/app-BayTTy01.js +0 -9
  532. package/dist/dashboard/assets/app-BayTTy01.js.gz +0 -0
  533. package/dist/dashboard/assets/app-a159k0ub.css +0 -2
  534. package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
  535. package/dist/dashboard/assets/assert-DbRA8EG2.js +0 -15
  536. package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
  537. package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
  538. package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
  539. package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
  540. package/dist/dashboard/assets/cli-tools-63qBGaRw.js +0 -1
  541. package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
  542. package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
  543. package/dist/dashboard/assets/dialog-8fiE6tec.js +0 -1
  544. package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
  545. package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
  546. package/dist/dashboard/assets/entry-BSAsYsmV.js +0 -1
  547. package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
  548. package/dist/dashboard/assets/entry-CiIQULZD.js +0 -4
  549. package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
  550. package/dist/dashboard/assets/entry-Daz0lJ-W.js +0 -1
  551. package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
  552. package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
  553. package/dist/dashboard/assets/github-badge-CPK47xQx.js +0 -1
  554. package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
  555. package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
  556. package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
  557. package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
  558. package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
  559. package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
  560. package/dist/dashboard/assets/live-DCQNZpF7.js +0 -1
  561. package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
  562. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  563. package/dist/dashboard/assets/network-9Yj2ZyLo.js +0 -1
  564. package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
  565. package/dist/dashboard/assets/numbers-CEagj6ZZ.js +0 -2
  566. package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
  567. package/dist/dashboard/assets/providers/zai-coding-plan.svg +0 -6
  568. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  569. package/dist/dashboard/assets/quota-Caw7wqci.js +0 -1
  570. package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
  571. package/dist/dashboard/assets/routing-CO9zCUus.js +0 -1
  572. package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
  573. package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
  574. package/dist/dashboard/assets/settings-jS3-PNSA.js +0 -1
  575. package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
  576. package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
  577. package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
  578. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
  579. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  580. package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
  581. package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
  582. package/src/console/cli-tools/injectors/cline.ts +0 -110
  583. package/src/console/cli-tools/injectors/copilot.ts +0 -108
  584. package/src/console/cli-tools/injectors/cowork.ts +0 -64
  585. package/src/console/cli-tools/injectors/deepseek-tui.ts +0 -85
  586. package/src/console/cli-tools/injectors/droid.ts +0 -96
  587. package/src/console/cli-tools/injectors/grok-build.ts +0 -70
  588. package/src/console/cli-tools/injectors/hermes.ts +0 -101
  589. package/src/console/cli-tools/injectors/jcode.ts +0 -115
  590. package/src/console/cli-tools/injectors/openclaw.ts +0 -161
  591. package/src/network/response-headers.ts +0 -39
  592. package/src/protocol/response/dsml.ts +0 -38
  593. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
  594. /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
  595. /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;
@@ -119,34 +131,16 @@ export interface UpdateApiKeyResponse extends ApiKeyResponse {
119
131
  }
120
132
 
121
133
  /**
122
- * Ban reasons offered by the console dropdown. The first two are the operator's
123
- * own: "Instant Ban — Suka suka Gw" for the arbitrary ban, and a template for
124
- * suspicious traffic. The rest are generic, and an operator can still type a
125
- * custom reason — the dropdown is a convenience, not a closed set.
126
- */
127
- export const API_KEY_BAN_REASONS: readonly { readonly value: string; readonly label: string }[] = [
128
- { value: "instant_arbitrary", label: "Instant Ban — Suka suka Gw" },
129
- { value: "traffic_suspicious", label: "Your traffic looks suspicious" },
130
- { value: "abuse", label: "Abuse of the gateway" },
131
- { value: "policy_violation", label: "Policy violation" },
132
- { value: "compromised", label: "Key may be compromised" },
133
- { value: "quota_exceeded", label: "Quota abused" },
134
- { value: "other", label: "Other / custom" },
135
- ] as const;
136
-
137
- /**
138
- * Ban durations the console offers, in whole minutes. `null` is permanent.
139
- * Mirrored by the dashboard (`dashboard/src/lib/ban-durations.ts`) so the UI
140
- * and the API agree on one list.
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`).
141
139
  */
142
- export const BAN_DURATION_CHOICES: readonly { readonly minutes: number | null; readonly label: string }[] = [
143
- { minutes: 60, label: "1 hour" },
144
- { minutes: 6 * 60, label: "6 hours" },
145
- { minutes: 12 * 60, label: "12 hours" },
146
- { minutes: 24 * 60, label: "1 day" },
147
- { minutes: 7 * 24 * 60, label: "7 days" },
148
- { minutes: null, label: "Permanent" },
149
- ];
140
+ export {
141
+ API_KEY_BAN_REASONS,
142
+ BAN_DURATION_CHOICES,
143
+ } from "./ban-constants";
150
144
 
151
145
  /** What a bare "ban this key" click uses: the shortest, most reversible choice. */
152
146
  export const DEFAULT_BAN_MINUTES: number = 60;
@@ -154,7 +148,6 @@ export const DEFAULT_BAN_MINUTES: number = 60;
154
148
  /** Body accepted by the ban endpoint. */
155
149
  export interface BanApiKeyRequest {
156
150
  readonly reason: string;
157
- readonly reasonLabel?: string;
158
151
  /**
159
152
  * Ban duration in whole minutes from {@link BAN_DURATION_CHOICES}; `null`
160
153
  * means permanent. Absent = {@link DEFAULT_BAN_MINUTES}.
@@ -255,16 +248,20 @@ export function generateApiKeySecret(prefix = DEFAULT_API_KEY_PREFIX): {
255
248
  return { secret, hash: hashSecret(secret), prefix };
256
249
  }
257
250
 
258
- /** Normalizes a caller-supplied prefix; falls back to the default. */
259
- 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 {
260
253
  const trimmed = raw?.trim();
261
- return trimmed && trimmed.length > 0 ? trimmed : DEFAULT_API_KEY_PREFIX;
254
+ return trimmed && trimmed.length > 0 ? trimmed : fallback;
262
255
  }
263
256
 
264
- /** Resolves the share-key prefix; falls back to the default. */
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);
260
+ }
261
+
262
+ /** Resolves the share-key prefix; falls back to the share default. */
265
263
  export function resolveShareKeyPrefix(raw: string | undefined): string {
266
- const trimmed = raw?.trim();
267
- return !trimmed ? DEFAULT_SHARE_API_KEY_PREFIX : trimmed;
264
+ return resolvePrefix(raw, DEFAULT_SHARE_API_KEY_PREFIX);
268
265
  }
269
266
 
270
267
  /** Maximum hint the enroll page accepts; the dashboard caps its input there. */
@@ -349,6 +346,19 @@ export function validateApiKeyRequest(request: CreateApiKeyRequest): readonly Ac
349
346
  finitePositive(request.monthlyTokenLimit, "monthlyTokenLimit");
350
347
  finitePositive(request.lifetimeTokenBudget, "lifetimeTokenBudget");
351
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
+ }
352
362
  if (request.maxChildren !== undefined && request.maxChildren !== null) {
353
363
  finitePositive(request.maxChildren, "maxChildren");
354
364
  if (request.maxChildren > 10_000) {
@@ -424,34 +434,33 @@ export function sanitizeApiKeyResponse(record: ApiKeyRecord): ApiKeyResponse {
424
434
  * renaming the key cannot change which key is the default.
425
435
  */
426
436
  isDefault: isDefaultGatewayApiKey(record),
427
- ...(keyPrefix === undefined ? {} : { keyPrefix }),
428
- ...(record.parentKeyId === undefined ? {} : { parentKeyId: record.parentKeyId }),
429
- ...(record.notesTitle === undefined ? {} : { notesTitle: record.notesTitle }),
430
- ...(record.notesSubtitle === undefined ? {} : { notesSubtitle: record.notesSubtitle }),
431
- ...(record.notesBody === undefined ? {} : { notesBody: record.notesBody }),
432
- ...(record.sharePopupEnabled === undefined ? {} : { sharePopupEnabled: record.sharePopupEnabled }),
433
- ...(record.sharePopupImageMime === undefined
434
- ? {}
435
- : { sharePopupImageMime: record.sharePopupImageMime }),
436
- ...(record.sharePopupTitle === undefined ? {} : { sharePopupTitle: record.sharePopupTitle }),
437
- ...(record.sharePopupBody === undefined ? {} : { sharePopupBody: record.sharePopupBody }),
438
- ...(record.requestsPerMinute === undefined ? {} : { requestsPerMinute: record.requestsPerMinute }),
439
- ...(record.dailyTokenLimit === undefined ? {} : { dailyTokenLimit: record.dailyTokenLimit }),
440
- ...(record.monthlyTokenLimit === undefined ? {} : { monthlyTokenLimit: record.monthlyTokenLimit }),
441
- ...(record.lifetimeTokenBudget === undefined ? {} : { lifetimeTokenBudget: record.lifetimeTokenBudget }),
442
- ...(record.maxConcurrentRequests === undefined ? {} : { maxConcurrentRequests: record.maxConcurrentRequests }),
443
- ...(record.maxChildren === undefined ? {} : { maxChildren: record.maxChildren }),
444
- ...(record.modelPrefix === undefined ? {} : { modelPrefix: record.modelPrefix }),
445
- ...(record.modelAccessMode === undefined ? {} : { modelAccessMode: record.modelAccessMode }),
446
- ...(record.modelList === undefined ? {} : { modelList: record.modelList }),
447
- ...(record.clientRouterDenylist === undefined
448
- ? {}
449
- : { 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
+ }),
450
463
  createdAt: record.createdAt.toISOString(),
451
- ...(record.revokedAt === undefined ? {} : { revokedAt: record.revokedAt.toISOString() }),
452
- ...(record.bannedAt === undefined ? {} : { bannedAt: record.bannedAt.toISOString() }),
453
- ...(record.banReason === undefined ? {} : { banReason: record.banReason }),
454
- ...(record.bannedUntil === undefined ? {} : { bannedUntil: record.bannedUntil.toISOString() }),
455
464
  tokensConsumed: record.tokensConsumed,
456
465
  };
457
466
  }
@@ -11,7 +11,7 @@ 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";
14
+ import { API_KEY_MODES, API_KEY_MODEL_ACCESS_MODES, API_KEY_CONCURRENCY_SCOPES, type ShareLinkKind } from "../../persistence/schema";
15
15
  import type { ApiKeyConfig, ApiKeyResponse, BanApiKeyRequest, UpdateApiKeyResponse } from "./contracts";
16
16
  import type {
17
17
  ApiKeyPatch,
@@ -20,6 +20,7 @@ import type {
20
20
  SharedChildrenPageOptions,
21
21
  } from "../../persistence/stores/api-key-store";
22
22
  import {
23
+ API_KEY_BAN_REASONS,
23
24
  BAN_DURATION_CHOICES,
24
25
  DEFAULT_BAN_MINUTES,
25
26
  generateApiKeySecret,
@@ -50,6 +51,9 @@ const SHARED_CHILD_SORT_COLUMNS = new Set<SharedChildSortColumn>([
50
51
  "enabled",
51
52
  ]);
52
53
 
54
+ /** The offered ban durations as a set of minute values, hoisted once. */
55
+ const BAN_DURATION_MINUTES = new Set(BAN_DURATION_CHOICES.map((choice) => choice.minutes));
56
+
53
57
  /** Parses and bounds the paginated share-child list query. */
54
58
  function parseSharedChildrenQuery(query: URLSearchParams): SharedChildrenPageOptions {
55
59
  const limitRaw = query.get("limit");
@@ -82,24 +86,23 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
82
86
  const topLevel = records.filter(
83
87
  (record) => record.revokedAt === undefined && record.parentKeyId === undefined,
84
88
  );
85
- // A share template's own counter never moves for child traffic, so
86
- // reporting it raw would show a template as untouched while the gateway
87
- // is refusing its recipients on the parent budget. Report the figure
88
- // the gateway actually enforces: the template's counter plus every
89
- // telemetry total the family ever recorded — revoked/deleted children
90
- // included (their spend is already spent), so the number survives
91
- // cleanup and matches the share page.
89
+ // A share template's own counter is the live family accumulator now:
90
+ // every recipient commit adds to it (see ApiKeyAdmissionService
91
+ // commitUsage), so reporting it raw gives the enforced family total.
92
+ // No telemetry add-on is needed, and adding it here would double-count.
92
93
  return Promise.all(
93
94
  topLevel.map(async (record) => {
94
95
  const response = sanitizeApiKeyResponse(record);
95
96
  if (record.keyMode !== "share") return response;
96
- const family = await config.store
97
- .sumFamilyTelemetryConsumed(authorized.tenantId, record.id)
98
- .catch(() => 0);
99
- return { ...response, tokensConsumed: response.tokensConsumed + family };
97
+ return response;
100
98
  }),
101
- );
102
- },
99
+ );
100
+ },
101
+ async listActiveBannedKeys(access: AccessDecision | undefined): Promise<ApiKeyResponse[]> {
102
+ const authorized = requireTenantScope(access, "dashboard:read");
103
+ const records = await config.store.listActiveBanned(authorized.tenantId);
104
+ return records.map(sanitizeApiKeyResponse);
105
+ },
103
106
  /** Persists a new credential order. `keyIds` must name every top-level key
104
107
  the tenant has, so a partial list cannot silently renumber rows. */
105
108
  async reorderKeys(access: AccessDecision | undefined, keyIds: readonly string[]): Promise<void> {
@@ -187,6 +190,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
187
190
  ...(request.monthlyTokenLimit == null ? {} : { monthlyTokenLimit: request.monthlyTokenLimit }),
188
191
  ...(request.lifetimeTokenBudget == null ? {} : { lifetimeTokenBudget: request.lifetimeTokenBudget }),
189
192
  ...(request.maxConcurrentRequests == null ? {} : { maxConcurrentRequests: request.maxConcurrentRequests }),
193
+ ...(request.concurrencyScope !== undefined ? { concurrencyScope: request.concurrencyScope } : {}),
190
194
  ...(request.maxChildren == null ? {} : { maxChildren: request.maxChildren }),
191
195
  ...(request.modelPrefix === undefined ? {} : { modelPrefix: request.modelPrefix }),
192
196
  modelAccessMode: request.modelAccessMode ?? "whitelist",
@@ -322,6 +326,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
322
326
  ...(patchRequest.monthlyTokenLimit === undefined ? {} : { monthlyTokenLimit: patchRequest.monthlyTokenLimit }),
323
327
  ...(patchRequest.lifetimeTokenBudget === undefined ? {} : { lifetimeTokenBudget: patchRequest.lifetimeTokenBudget }),
324
328
  ...(patchRequest.maxConcurrentRequests === undefined ? {} : { maxConcurrentRequests: patchRequest.maxConcurrentRequests }),
329
+ ...(patchRequest.concurrencyScope !== undefined ? { concurrencyScope: patchRequest.concurrencyScope } : {}),
325
330
  ...(patchRequest.maxChildren === undefined ? {} : { maxChildren: patchRequest.maxChildren }),
326
331
  ...(patchRequest.modelPrefix === undefined ? {} : { modelPrefix: patchRequest.modelPrefix }),
327
332
  ...(patchRequest.modelAccessMode === undefined ? {} : { modelAccessMode: patchRequest.modelAccessMode }),
@@ -791,8 +796,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
791
796
  // clamping would answer a request for 90 days with a week and report
792
797
  // success.
793
798
  const chosenMinutes = request.minutes === undefined ? DEFAULT_BAN_MINUTES : request.minutes;
794
- const allowed = new Set(BAN_DURATION_CHOICES.map((choice) => choice.minutes));
795
- if (!allowed.has(chosenMinutes)) {
799
+ if (!BAN_DURATION_MINUTES.has(chosenMinutes)) {
796
800
  throw new ConsoleDomainError(
797
801
  "invalid_request",
798
802
  400,
@@ -814,11 +818,27 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
814
818
  access: authorized,
815
819
  action: "api_key.banned",
816
820
  target: keyId,
817
- detail: { reason, ...(bannedUntil === null ? { permanent: true } : { bannedUntil: bannedUntil.toISOString() }) },
821
+ detail: {
822
+ reason,
823
+ // The human-readable label is derived server-side from the machine
824
+ // `reason`, so the audit trail cannot be skewed by a client-authored
825
+ // label. Unknown reasons (custom text) simply carry no label; the
826
+ // free-text `reason` above is still recorded verbatim.
827
+ ...(API_KEY_BAN_REASONS.find((r) => r.value === reason)?.label === undefined
828
+ ? {}
829
+ : { reasonLabel: API_KEY_BAN_REASONS.find((r) => r.value === reason)!.label }),
830
+ ...(bannedUntil === null ? { permanent: true } : { bannedUntil: bannedUntil.toISOString() }),
831
+ },
832
+ });
833
+ // The ban mutates the row already fetched; derive the response from that
834
+ // record instead of re-fetching after the write. Match the store's own
835
+ // mapping: a `null` (permanent) `bannedUntil` is omitted, not carried.
836
+ return sanitizeApiKeyResponse({
837
+ ...current,
838
+ bannedAt: new Date(),
839
+ banReason: reason,
840
+ ...(bannedUntil ? { bannedUntil } : {}),
818
841
  });
819
- const record = await config.store.get(authorized.tenantId, keyId);
820
- if (!record) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
821
- return sanitizeApiKeyResponse(record);
822
842
  },
823
843
  async unbanKey(
824
844
  access: AccessDecision | undefined,
@@ -836,9 +856,11 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
836
856
  action: "api_key.unbanned",
837
857
  target: keyId,
838
858
  });
839
- const record = await config.store.get(authorized.tenantId, keyId);
840
- if (!record) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
841
- return sanitizeApiKeyResponse(record);
859
+ // Unban clears all three ban columns and re-enables the key; derive the
860
+ // response from the pre-fetch instead of re-fetching after the write.
861
+ const { bannedAt: _bannedAt, banReason: _banReason, bannedUntil: _bannedUntil, ...currentRest } = current;
862
+ const unbannedRecord: ApiKeyRecord = { ...currentRest, enabled: true };
863
+ return sanitizeApiKeyResponse(unbannedRecord);
842
864
  },
843
865
  };
844
866
  return operations;
@@ -858,6 +880,7 @@ const apiKeyBody = t.Object({
858
880
  monthlyTokenLimit: t.Optional(t.Union([t.Number(), t.Null()])),
859
881
  lifetimeTokenBudget: t.Optional(t.Union([t.Number(), t.Null()])),
860
882
  maxConcurrentRequests: t.Optional(t.Union([t.Number(), t.Null()])),
883
+ concurrencyScope: t.Optional(literalUnion(API_KEY_CONCURRENCY_SCOPES)),
861
884
  modelPrefix: t.Optional(t.String()),
862
885
  modelAccessMode: t.Optional(literalUnion(API_KEY_MODEL_ACCESS_MODES)),
863
886
  modelList: t.Optional(t.Array(t.String())),
@@ -905,6 +928,13 @@ export function createApiKeyRoutes(config: ApiKeyConfig): AnyElysia {
905
928
  return errorResponse(error, set, "API-key operation failed");
906
929
  }
907
930
  })
931
+ .get("/banned", async ({ request, set }) => {
932
+ try {
933
+ return await factory.listActiveBannedKeys(config.accessResolver(request));
934
+ } catch (error) {
935
+ return errorResponse(error, set, "API-key operation failed");
936
+ }
937
+ })
908
938
  .post("/reorder", { body: reorderKeysBody }, async ({ request, body, set }) => {
909
939
  try {
910
940
  await factory.reorderKeys(config.accessResolver(request), body.keyIds);
@@ -1020,7 +1050,9 @@ export function createApiKeyRoutes(config: ApiKeyConfig): AnyElysia {
1020
1050
  {
1021
1051
  body: t.Object({
1022
1052
  reason: t.String(),
1023
- reasonLabel: t.Optional(t.String()),
1053
+ // `reasonLabel` is deliberately not accepted: the audit label is
1054
+ // derived server-side from `reason` (see `banKey`), so a client
1055
+ // cannot author the human-readable audit entry.
1024
1056
  minutes: t.Optional(t.Union([t.Number({ minimum: 1 }), t.Null()])),
1025
1057
  }),
1026
1058
  },
@@ -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. */
@@ -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> {