cartethyia 2.4.5 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (415) hide show
  1. package/.env.example +4 -1
  2. package/README.md +63 -13
  3. package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
  4. package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
  5. package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
  7. package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
  8. package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-D3xqZbN-.js} +1 -1
  9. package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
  10. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
  11. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js +1 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
  14. package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-CTTpCb25.js} +1 -1
  15. package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js +1 -0
  17. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-DHoB3Agn.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
  20. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +1 -0
  21. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  22. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
  23. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js.gz +0 -0
  26. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
  27. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
  31. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-CvjZxHE4.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
  36. package/dist/dashboard/assets/SetupPage-D3nza7LH.js +1 -0
  37. package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
  38. package/dist/dashboard/assets/{SortableList-Bol1ASmI.js → SortableList-Dtt12WT1.js} +1 -1
  39. package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
  40. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js +31 -0
  41. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
  42. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  43. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
  44. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
  45. package/dist/dashboard/assets/api-keys-BmDbsior.js +1 -0
  46. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  47. package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
  48. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  49. package/dist/dashboard/assets/{app-BDJWMThu.js → app-C-Y4oZT0.js} +3 -3
  50. package/dist/dashboard/assets/app-C-Y4oZT0.js.gz +0 -0
  51. package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
  52. package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
  53. package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
  54. package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
  55. package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
  56. package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-DpXT4Ixt.js} +1 -1
  57. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  58. package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-CSAAWLJv.js} +1 -1
  59. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  60. package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
  61. package/dist/dashboard/assets/{card-BTdbRFFg.js → card-DEhRXXw4.js} +1 -1
  62. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  63. package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
  64. package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
  66. package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
  67. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js +1 -0
  68. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  69. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +1 -0
  70. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  71. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  72. package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
  73. package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BvQeBMfP.js} +1 -1
  74. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  75. package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
  76. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  77. package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
  78. package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-CMwPXwQB.js} +1 -1
  79. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  80. package/dist/dashboard/assets/entry-D4O2229X.js +1 -0
  81. package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
  82. package/dist/dashboard/assets/entry-DKvr5Q9K.js +1 -0
  83. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  84. package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
  85. package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
  86. package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
  87. package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
  88. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  89. package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
  90. package/dist/dashboard/assets/github-badge-zliLvBwr.js +1 -0
  91. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  92. package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
  93. package/dist/dashboard/assets/{index-C4hXfAMs.js → index-CXm3FIyt.js} +2 -2
  94. package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
  95. package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1IlZZ8H.js} +1 -1
  96. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  97. package/dist/dashboard/assets/{input-Bn4_kNav.js → input-BpVZdrbc.js} +1 -1
  98. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  99. package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
  100. package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
  101. package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
  102. package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BioLW7gN.js} +1 -1
  103. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  104. package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-CwVcGAIc.js} +1 -1
  105. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  106. package/dist/dashboard/assets/{live-YlifPYox.js → live-yfEJFF9T.js} +1 -1
  107. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  108. package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
  109. package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
  110. package/dist/dashboard/assets/model-bans-CT-TxSNX.js +1 -0
  111. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  112. package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
  113. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  114. package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
  115. package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
  116. package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
  117. package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
  118. package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
  119. package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
  120. package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
  121. package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
  122. package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
  123. package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
  124. package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
  125. package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
  126. package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
  127. package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
  128. package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
  129. package/dist/dashboard/assets/providers/cursor.webp +0 -0
  130. package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
  131. package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
  132. package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
  133. package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
  134. package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
  135. package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
  136. package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
  137. package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
  138. package/dist/dashboard/assets/providers/trae.webp +0 -0
  139. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  140. package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
  141. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  142. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  143. package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
  144. package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
  145. package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
  146. package/dist/dashboard/assets/routing-Cs6jl49L.js +1 -0
  147. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  148. package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
  149. package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
  150. package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
  151. package/dist/dashboard/assets/select-C0Rer3gF.js.gz +0 -0
  152. package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
  153. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  154. package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
  155. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  156. package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-C9_uUjaA.js} +1 -1
  157. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  158. package/dist/dashboard/assets/{state-DHKdg-pm.js → state-DA0B95UQ.js} +1 -1
  159. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  160. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  161. package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-Cittxy-x.js} +1 -1
  162. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  163. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
  164. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  165. package/dist/dashboard/assets/toast-CNt_jzO4.js +2 -0
  166. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  167. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  168. package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
  169. package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
  170. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  171. package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
  172. package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
  173. package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-BQKMcJ1H.js} +1 -1
  174. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
  175. package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
  176. package/dist/dashboard/index.html +1 -1
  177. package/dist/dashboard/index.html.gz +0 -0
  178. package/dist/dashboard/precompressed-assets.json +122 -0
  179. package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
  180. package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
  181. package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
  182. package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
  183. package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
  184. package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
  185. package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
  186. package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
  187. package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
  188. package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
  189. package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
  190. package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
  191. package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
  192. package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
  193. package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
  194. package/package.json +26 -30
  195. package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
  196. package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
  197. package/scripts/cli/command/admin.ts +65 -0
  198. package/scripts/cli/command/console-menu.ts +246 -0
  199. package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
  200. package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
  201. package/scripts/cli/command/start.ts +77 -0
  202. package/scripts/cli/command/tray.ts +165 -0
  203. package/scripts/cli/command/update.ts +23 -0
  204. package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
  205. package/scripts/cli/gateway/instance-lock.ts +105 -0
  206. package/scripts/cli/gateway/spawn.ts +173 -0
  207. package/scripts/cli/gateway/stop.ts +51 -0
  208. package/scripts/cli/main.ts +94 -0
  209. package/scripts/cli/support/bun.ts +27 -0
  210. package/scripts/cli/support/env.ts +99 -0
  211. package/scripts/cli/support/platform.ts +64 -0
  212. package/scripts/cli/support/services.ts +88 -0
  213. package/scripts/cli/support/startup-signal.ts +23 -0
  214. package/scripts/cli/{version.ts → support/version.ts} +1 -1
  215. package/scripts/cli/tray/hidden-window.vbs +28 -0
  216. package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
  217. package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
  218. package/scripts/cli/ui/background.ts +110 -0
  219. package/src/boot/app.ts +23 -7
  220. package/src/boot/dependencies.ts +55 -13
  221. package/src/boot/elysia-typebox-bridge.ts +37 -0
  222. package/src/boot/lifecycle.ts +2 -0
  223. package/src/config/cooldowns.ts +138 -138
  224. package/src/config/providers.ts +31 -5
  225. package/src/config/runtime.ts +19 -3
  226. package/src/config/spec.ts +20 -0
  227. package/src/console/api-keys/contracts.ts +414 -391
  228. package/src/console/api-keys/routes.ts +880 -870
  229. package/src/console/auth/account-reset-store.ts +14 -0
  230. package/src/console/auth/account-reset.ts +3 -2
  231. package/src/console/auth/setup-service.ts +6 -3
  232. package/src/console/backup/routes.ts +8 -0
  233. package/src/console/cli-tools/contracts.ts +6 -6
  234. package/src/console/console-router.ts +5 -3
  235. package/src/console/dashboard-assets.ts +81 -11
  236. package/src/console/domain-registration.ts +9 -2
  237. package/src/console/observability/contracts.ts +475 -423
  238. package/src/console/observability/routes.ts +31 -6
  239. package/src/console/observability/store.ts +1015 -912
  240. package/src/console/providers/catalog/contracts.ts +31 -0
  241. package/src/console/providers/catalog/provider-operations.ts +8 -0
  242. package/src/console/providers/catalog/public-model-store.ts +91 -60
  243. package/src/console/providers/catalog/store.ts +1532 -1521
  244. package/src/console/providers/oauth/callback-listener.ts +18 -3
  245. package/src/console/providers/oauth/routes.ts +38 -15
  246. package/src/console/quota/account-quota-tenant.ts +9 -6
  247. package/src/console/quota/refresh.ts +19 -0
  248. package/src/console/response-compression.ts +69 -2
  249. package/src/console/routing/pools/probe-verdict.ts +72 -0
  250. package/src/console/share/share-router.ts +7 -4
  251. package/src/console/share/share-stats.ts +12 -5
  252. package/src/console/shared/read-through-cache.ts +133 -7
  253. package/src/console/updates/repository-updates.ts +39 -34
  254. package/src/console/updates/routes.ts +162 -4
  255. package/src/main.ts +3 -0
  256. package/src/network/http2-bidi.ts +85 -0
  257. package/src/network/outbound-fetch.ts +87 -24
  258. package/src/network/pool/agent.ts +478 -460
  259. package/src/network/pool/resolver.ts +1104 -544
  260. package/src/network/pool-health-machine.ts +12 -5
  261. package/src/observability/event-log.ts +29 -8
  262. package/src/observability/metrics.ts +40 -13
  263. package/src/observability/payload-capture.ts +344 -324
  264. package/src/observability/runtime-metrics.ts +7 -0
  265. package/src/persistence/db-pg.ts +9 -3
  266. package/src/persistence/db-sqlite.ts +497 -495
  267. package/src/persistence/health-event-flush.ts +137 -0
  268. package/src/persistence/migrate.ts +181 -129
  269. package/src/persistence/schema.ts +1360 -1257
  270. package/src/persistence/settings-revision.ts +25 -0
  271. package/src/persistence/shared-cache.ts +217 -0
  272. package/src/persistence/stores/share-store.ts +556 -528
  273. package/src/persistence/usage-flush.ts +61 -1
  274. package/src/protocol/canonical-model.ts +23 -1
  275. package/src/protocol/response/chat.ts +143 -10
  276. package/src/protocol/response/codex.ts +9 -75
  277. package/src/protocol/response/omp-tool-call-markup.ts +221 -0
  278. package/src/protocol/tool-identity.ts +43 -9
  279. package/src/providers/authentication/jwt-validator.ts +130 -187
  280. package/src/providers/authentication/oauth-flow-store.ts +16 -9
  281. package/src/providers/default-registry.ts +177 -1
  282. package/src/providers/discovery/catalog/catalog-format.ts +20 -0
  283. package/src/providers/discovery/models-dev-catalog.ts +39 -22
  284. package/src/providers/discovery/openai-model-discovery.ts +39 -3
  285. package/src/providers/discovery/openrouter-free-roster.ts +98 -0
  286. package/src/providers/discovery/probe-wire.ts +29 -5
  287. package/src/providers/discovery/probing-service.ts +1022 -1001
  288. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +42 -2
  289. package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
  290. package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
  291. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +22 -5
  292. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
  293. package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
  294. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
  295. package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
  296. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
  297. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
  298. package/src/providers/integrations/buddy/codebuddy/shared.ts +115 -73
  299. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
  300. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
  301. package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
  302. package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
  303. package/src/providers/integrations/claude/claude.ts +606 -582
  304. package/src/providers/integrations/cline/cline-quota.ts +18 -16
  305. package/src/providers/integrations/cline/cline.ts +539 -468
  306. package/src/providers/integrations/codex/codex.ts +5 -8
  307. package/src/providers/integrations/commandcode/commandcode.ts +675 -541
  308. package/src/providers/integrations/commandcode/discovery.ts +116 -0
  309. package/src/providers/integrations/commandcode/quota.ts +160 -0
  310. package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
  311. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
  312. package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
  313. package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
  314. package/src/providers/integrations/cursor/cursor-request.ts +471 -0
  315. package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
  316. package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
  317. package/src/providers/integrations/cursor/cursor.ts +960 -0
  318. package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
  319. package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
  320. package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
  321. package/src/providers/integrations/factory/factory-oauth.ts +7 -11
  322. package/src/providers/integrations/github/github.ts +4 -4
  323. package/src/providers/integrations/hermes.ts +79 -8
  324. package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
  325. package/src/providers/integrations/kilo/kilo.ts +153 -146
  326. package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
  327. package/src/providers/integrations/opencode/provider.ts +532 -499
  328. package/src/providers/integrations/openrouter/provider.ts +139 -281
  329. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
  330. package/src/providers/integrations/qoder/qoder-oauth.ts +310 -263
  331. package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
  332. package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
  333. package/src/providers/integrations/qoder/qoder.ts +865 -730
  334. package/src/providers/integrations/shared/install-id.ts +29 -2
  335. package/src/providers/integrations/trae/trae-catalog.ts +190 -0
  336. package/src/providers/integrations/trae/trae-headers.ts +149 -0
  337. package/src/providers/integrations/trae/trae-oauth.ts +773 -0
  338. package/src/providers/integrations/trae/trae-protocol.ts +400 -0
  339. package/src/providers/integrations/trae/trae-realm.ts +220 -0
  340. package/src/providers/integrations/trae/trae.ts +458 -0
  341. package/src/providers/operations/account-health-service.ts +107 -10
  342. package/src/providers/operations/client-versions.ts +143 -6
  343. package/src/providers/operations/model-catalog-cache.ts +18 -3
  344. package/src/providers/operations/provider-catalog-seeder.ts +95 -86
  345. package/src/providers/operations/provider-catalog-service.ts +76 -0
  346. package/src/providers/operations/provider-credential-service.ts +46 -7
  347. package/src/providers/provider-contracts.ts +76 -0
  348. package/src/providers/provider-metadata.ts +78 -6
  349. package/src/providers/provider-model-definition.ts +373 -349
  350. package/src/security/api-key-auth.ts +126 -8
  351. package/src/transport/contracts.ts +3 -0
  352. package/src/transport/dispatch/attempt-finalize.ts +39 -2
  353. package/src/transport/dispatch/attempt-loop.ts +530 -513
  354. package/src/transport/dispatch/empty-stream.ts +17 -26
  355. package/src/transport/dispatch/generation.ts +88 -21
  356. package/src/transport/dispatch/leases.ts +99 -2
  357. package/src/transport/dispatch/proxy-request.ts +60 -3
  358. package/src/transport/dispatch/streaming/finalize.ts +16 -1
  359. package/src/transport/dispatch/streaming/stream.ts +22 -2
  360. package/src/transport/dispatch/upstream.ts +19 -1
  361. package/src/transport/failure-policy.ts +20 -0
  362. package/src/transport/guards/auth-guard.ts +26 -1
  363. package/src/transport/guards/body-policy.ts +5 -4
  364. package/src/transport/request/preparer.ts +103 -0
  365. package/src/transport/request/state.ts +14 -0
  366. package/src/transport/routing/plan-gate.ts +45 -0
  367. package/src/transport/routing/route-catalog.ts +799 -738
  368. package/src/transport/routing/route-model.ts +52 -1
  369. package/src/transport/routing/router.ts +51 -0
  370. package/src/transport/surface/chat/encode.ts +18 -31
  371. package/src/transport/surface/messages/stream.ts +3 -2
  372. package/src/transport/translation/tool-repair.ts +37 -16
  373. package/src/workers/account-identity-sweep.ts +14 -12
  374. package/src/workers/daily-checkin.ts +62 -0
  375. package/src/workers/quota-refresh-sweep.ts +27 -2
  376. package/dist/dashboard/assets/BannedPage-DUrXpp-X.js +0 -1
  377. package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
  378. package/dist/dashboard/assets/ConsoleLogPage-AisQzCI1.js +0 -1
  379. package/dist/dashboard/assets/HealthEventsModal-BNflDm_f.js +0 -1
  380. package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
  381. package/dist/dashboard/assets/OverviewPage-C3NIzsDx.js +0 -1
  382. package/dist/dashboard/assets/ProviderDetailPage-BK8i5B5X.js +0 -3
  383. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
  384. package/dist/dashboard/assets/ProvidersPage-CvCVgm1e.js +0 -1
  385. package/dist/dashboard/assets/ProxyPage-DjabkuzP.js +0 -7
  386. package/dist/dashboard/assets/QuotaPage-Dxe0B0iu.js +0 -1
  387. package/dist/dashboard/assets/SetupPage-BaD4P_jW.js +0 -1
  388. package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
  389. package/dist/dashboard/assets/UsagePage-Djes-Epy.js +0 -56
  390. package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
  391. package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
  392. package/dist/dashboard/assets/assert-Bwh6DJTK.js +0 -1
  393. package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
  394. package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
  395. package/dist/dashboard/assets/entry-Cpi0rph1.js +0 -4
  396. package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
  397. package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
  398. package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
  399. package/dist/dashboard/assets/model-bans-CVvHZ-qz.js +0 -1
  400. package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
  401. package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
  402. package/dist/dashboard/assets/routing-jntw9-Ui.js +0 -1
  403. package/dist/dashboard/assets/select-CGUuTQmj.js +0 -41
  404. package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
  405. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
  406. package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
  407. package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
  408. package/scripts/cli/cli.ts +0 -771
  409. package/scripts/operations/probe-egress.ts +0 -335
  410. package/scripts/operations/restart.ts +0 -120
  411. package/scripts/shared/env.ts +0 -269
  412. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
  413. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
  414. /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
  415. /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
@@ -1,495 +1,497 @@
1
- /** Embedded backend: bun:sqlite in the gateway's data dir. */
2
- import { Database, type SQLQueryBindings } from "bun:sqlite";
3
- import { drizzle } from "drizzle-orm/bun-sqlite";
4
- import { getTableColumns, getTableName } from "drizzle-orm";
5
- import { randomUUID } from "node:crypto";
6
- import { mkdirSync, readFileSync } from "node:fs";
7
- import { dirname } from "node:path";
8
- import { resolvePackagePath } from "../config/package-paths";
9
- import { fullSchema, type DatabaseHandle } from "./db-handle";
10
- import {
11
- BASELINE_MIGRATION_ID,
12
- assertMigrationHistorySupported,
13
- baselineSchemaMarker,
14
- MIGRATION_LEDGER_TABLE,
15
- migrationFiles,
16
- migrationIdFor,
17
- splitStatements,
18
- } from "./migrate";
19
-
20
- /**
21
- * Ledger DDL for the SQLite backend.
22
- *
23
- * The shared ledger DDL is PostgreSQL (`timestamptz`, `now()`), and this
24
- * backend never sends PostgreSQL DDL. The shape is the same table the
25
- * readiness check reads, so a ledger written here is a ledger read there.
26
- */
27
- const SQLITE_LEDGER_DDL = `CREATE TABLE IF NOT EXISTS ${MIGRATION_LEDGER_TABLE} (
28
- migration_id text PRIMARY KEY,
29
- applied_at text NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
30
- )`;
31
-
32
- /**
33
- * Opens the embedded database at `file`.
34
- *
35
- * `:memory:` is the test backend and is used as given. A filesystem path has
36
- * its parent created first, because bun:sqlite does not create directories.
37
- * Foreign keys are off unless asked for, so the pragma is set on every
38
- * connection rather than trusted to a default. WAL keeps a reader from
39
- * blocking the single writer.
40
- */
41
- /**
42
- * Databases whose `prepare`/`exec` have already been wrapped.
43
- *
44
- * Keyed weakly so a closed in-memory database is not retained by the registry.
45
- */
46
- const SQLITE_PREPARED = new WeakSet<Database>();
47
-
48
- export function createSqliteDatabase(file: string): Database {
49
- if (file !== ":memory:") mkdirSync(dirname(file), { recursive: true });
50
- const db = new Database(file);
51
- db.exec("PRAGMA foreign_keys = ON");
52
- if (file !== ":memory:") applyFilePragmas(db);
53
- // Guarded: this function has to run per database, but a caller may hand it
54
- // one that already went through here (a test builds a handle over a booted
55
- // handle). Wrapping `prepare` twice ran the translation twice, and the
56
- // second pass mangled SQL the first had already rewritten — `unrecognized
57
- // token: ":"` from a placeholder the first pass produced.
58
- if (SQLITE_PREPARED.has(db)) return db;
59
- SQLITE_PREPARED.add(db);
60
- const prepare = db.prepare.bind(db);
61
- db.prepare = ((text: string) => {
62
- const statement = prepare(sqliteSql(text));
63
- // bun:sqlite rejects a Date binding ("Binding expected string, TypedArray,
64
- // boolean, number, bigint or null"), and Drizzle's pg-core timestamp
65
- // columns bind `Date` objects. The schema is shared with PostgreSQL, so
66
- // the conversion lives here: every timestamp column is `text`, and an ISO
67
- // string is exactly what those columns store.
68
- const bind = (value: unknown): SQLQueryBindings =>
69
- value instanceof Date ? value.toISOString() : (value as SQLQueryBindings);
70
- const run = statement.run.bind(statement);
71
- const all = statement.all.bind(statement);
72
- const get = statement.get.bind(statement);
73
- // Drizzle's bun-sqlite session reads rows through `values`, not `all`.
74
- const values = statement.values.bind(statement);
75
- const bound = (args: unknown[]): SQLQueryBindings[] => args.map(bind);
76
- statement.run = ((...args: unknown[]) => run(...bound(args))) as typeof statement.run;
77
- statement.all = ((...args: unknown[]) => all(...bound(args))) as typeof statement.all;
78
- statement.get = ((...args: unknown[]) => get(...bound(args))) as typeof statement.get;
79
- statement.values = ((...args: unknown[]) => values(...bound(args))) as typeof statement.values;
80
- return statement;
81
- }) as Database["prepare"];
82
- const exec = db.exec.bind(db);
83
- db.exec = ((text: string) => exec(sqliteSql(text))) as Database["exec"];
84
- return db;
85
- }
86
-
87
- /**
88
- * Teaches the shared pg-core columns to read what bun:sqlite returns.
89
- *
90
- * The schema is written for node-postgres, whose driver hands booleans back
91
- * as booleans and timestamps as `Date`. bun:sqlite hands back the stored
92
- * value unchanged: an integer for a boolean, text for a timestamp. A
93
- * column's `mapFromDriverValue` is what turns a driver value into the type
94
- * the schema declares, and Drizzle calls it for every read — a select, a
95
- * join, a `returning()`. The pg-core boolean column returns its value
96
- * unchanged and the timestamp column only converts strings, so neither
97
- * produces what the schema promises from bun:sqlite's values. Replacing the
98
- * mapper on each column instance fixes every read at once. Writes are
99
- * untouched: booleans already bind as integers, and `Date` bindings are
100
- * converted to ISO strings in `createSqliteDatabase`.
101
- */
102
- function installSqliteColumnMapping(schema: Record<string, unknown>): void {
103
-
104
- for (const value of Object.values(schema)) {
105
- const columns = tableColumns(value);
106
- if (columns === undefined) continue;
107
- const table = getTableName(Object.values(columns)[0]!.table as Parameters<typeof getTableName>[0]);
108
- for (const column of Object.values(columns)) {
109
- if (column.columnType === "PgBoolean") {
110
- column.mapFromDriverValue = (read) => read === null ? null : read === 1 || read === true;
111
- } else if (column.columnType === "PgTimestamp") {
112
- const convert = column.mapFromDriverValue.bind(column);
113
- column.mapFromDriverValue = (read) => {
114
- // A column declared `text` keeps whatever it was bound as, so a
115
- // timestamp can come back as the ISO string the writer produces, or
116
- // as a number — or the digits of one — when a row was restored from
117
- // a source that stored epoch milliseconds. `new Date` reads the
118
- // digits as a year and returns an invalid date, so those are
119
- // handled before the string conversion.
120
- if (typeof read === "number") return new Date(read);
121
- if (typeof read === "string" && /^\d+$/.test(read)) return new Date(Number(read));
122
- return convert(read);
123
- };
124
- }
125
- // Registered after the branches above replace it, so execute and the
126
- // builder call the same function. A column neither branch touches keeps
127
- // its own mapper, which is what already stringifies a numeric and parses
128
- // jsonb. execute is the one read that never called it.
129
- if (table !== undefined) {
130
- columnMapping.set(mappingKey(table, column.name), column.mapFromDriverValue.bind(column));
131
- }
132
- }
133
- }
134
- }
135
-
136
- interface MappedColumn {
137
- readonly name: string;
138
- readonly columnType: string;
139
- readonly table: object;
140
- mapFromDriverValue: (value: unknown) => unknown;
141
- }
142
-
143
- /**
144
- * `table.column` to the mapper that turns bun:sqlite's stored value into the
145
- * type the schema declares. `db.execute` reads through `prepare().all()`, which
146
- * never calls a column mapper, so the alias applies these itself. A name that
147
- * two tables share maps to the same type everywhere in this schema, and a
148
- * column with no entry is returned as stored.
149
- */
150
- const columnMapping = new Map<string, (value: unknown) => unknown>();
151
-
152
- function mappingKey(table: string, column: string): string {
153
- return `${table}.${column}`;
154
- }
155
-
156
- /** One row from `prepare().all()`, with every known column mapped. */
157
- function mapExecuteRow(row: Record<string, unknown>, table: string | undefined): Record<string, unknown> {
158
- if (table === undefined) return row;
159
- const mapped: Record<string, unknown> = {};
160
- for (const [column, value] of Object.entries(row)) {
161
- const convert = columnMapping.get(mappingKey(table, column));
162
- mapped[column] = value === null || convert === undefined ? value : convert(value);
163
- }
164
- return mapped;
165
- }
166
-
167
- /**
168
- * The table a raw statement reads, when it reads exactly one.
169
- *
170
- * `SELECT 1` and a join read columns from nowhere or from several tables, and
171
- * neither can be mapped by name. Both are returned as stored, which is what
172
- * they already were.
173
- */
174
- interface RenderedStatement {
175
- readonly sql: string;
176
- readonly params: SQLQueryBindings[];
177
- }
178
-
179
- /**
180
- * A Drizzle `sql` value as bun:sqlite runs it: `?` placeholders and the values
181
- * that fill them, in order.
182
- *
183
- * A chunk is text, a nested statement, or a bound value. Text is concatenated,
184
- * a nested statement is rendered in place, and everything else is one `?`.
185
- * `Date` values are converted here too, because this path prepares through the
186
- * raw database rather than through the wrapper in `createSqliteDatabase`.
187
- */
188
- function renderExecute(query: unknown): RenderedStatement | undefined {
189
- const chunks = (query as { queryChunks?: unknown[] }).queryChunks;
190
- if (chunks === undefined) return undefined;
191
- const params: SQLQueryBindings[] = [];
192
- const sql = chunks.map((chunk) => renderChunk(chunk, params)).join("");
193
- return { sql, params };
194
- }
195
-
196
- function renderChunk(chunk: unknown, params: SQLQueryBindings[]): string {
197
- if (Array.isArray(chunk)) {
198
- return `(${chunk.map((part) => renderChunk(part, params)).join(", ")})`;
199
- }
200
- if (typeof chunk === "object" && chunk !== null && "queryChunks" in chunk) {
201
- return (chunk as { queryChunks: unknown[] }).queryChunks
202
- .map((part) => renderChunk(part, params))
203
- .join("");
204
- }
205
- if (typeof chunk === "object" && chunk !== null && Array.isArray((chunk as { value?: unknown }).value)) {
206
- return ((chunk as { value: string[] }).value).join("");
207
- }
208
- // A table, column, or quoted name is SQL text, not a bound value. Rendering
209
- // one as a placeholder produces `DELETE FROM ?`, which SQLite rejects with
210
- // `near "?": syntax error` — the failure the payload sweep logs every pass.
211
- const kind = drizzleKind(chunk);
212
- if (kind === "Table") {
213
- return `"${getTableName(chunk as Parameters<typeof getTableName>[0])}"`;
214
- }
215
- if (kind === "Name") return `"${(chunk as { value: string }).value}"`;
216
- if (kind === "Column") return `"${(chunk as { name: string }).name}"`;
217
- const value = chunk instanceof Date ? chunk.toISOString() : chunk;
218
- params.push(value as SQLQueryBindings);
219
- return "?";
220
- }
221
-
222
-
223
- function drizzleKind(chunk: unknown): string | undefined {
224
- if (typeof chunk !== "object" || chunk === null) return undefined;
225
- // A pg column's own kind is `PgUUID` or `PgTimestamp`, not `Column`. Walk
226
- // the constructors: the base `Column` marks every one of them.
227
- let ctor = chunk.constructor as { [entityKind]?: unknown } | null;
228
- while (ctor !== null && ctor !== Function.prototype) {
229
- const kind = ctor[entityKind];
230
- if (kind === "Column" || kind === "Table" || kind === "Name") return kind;
231
- ctor = Object.getPrototypeOf(ctor) as { [entityKind]?: unknown } | null;
232
- }
233
- return undefined;
234
- }
235
-
236
- const entityKind = Symbol.for("drizzle:entityKind");
237
- function singleTable(sql: string): string | undefined {
238
- const tables = [...sql.matchAll(/\b(?:from|join)\s+"?([a-z0-9_]+)"?/gi)].map((match) => match[1]);
239
- const distinct = new Set(tables);
240
- return distinct.size === 1 ? [...distinct][0] : undefined;
241
- }
242
-
243
- /** The columns of one Drizzle table, or undefined for everything else in the schema. */
244
- function tableColumns(value: unknown): Record<string, MappedColumn> | undefined {
245
- if (typeof value !== "object" || value === null) return undefined;
246
- try {
247
- const columns = getTableColumns(value as Parameters<typeof getTableColumns>[0]);
248
- return Object.keys(columns).length > 0 ? columns as Record<string, MappedColumn> : undefined;
249
- } catch {
250
- return undefined;
251
- }
252
- }
253
-
254
- /**
255
- * The handle stores run on.
256
- *
257
- * `db` is the bun-sqlite Drizzle instance. It is not the node-postgres
258
- * `CartethyiaDatabase` the stores are typed against — the two query builders
259
- * are different types — so the conversion happens once, here. The stores
260
- * exercise only the builder surface both drivers share.
261
- */
262
- /**
263
- * The three statements the auth path runs on a cache miss, prepared once.
264
- *
265
- * Drizzle rebuilds the query on every call, and that rebuild measured 60x the
266
- * cost of the lookup itself. These are static SQL against an index, so there
267
- * is nothing for a builder to decide per call. The column list is exactly the
268
- * `api_keys` row the store maps, in the order `findActiveByHash` reads it.
269
- */
270
- const AUTH_KEY_COLUMNS = [
271
- "id", "tenant_id", "key_hash", "key_mode", "parent_key_id", "issued_client_ip",
272
- "issued_client_ip_key", "label", "scopes", "requests_per_minute", "daily_token_limit",
273
- "monthly_token_limit", "lifetime_token_budget", "lifetime_tokens_consumed",
274
- "max_concurrent_requests", "model_access_mode", "model_list", "client_router_denylist",
275
- "model_prefix", "created_at", "enabled", "revoked_at", "key_prefix", "key_encrypted",
276
- "notes_title", "notes_subtitle", "notes_body", "is_default", "sort_index",
277
- ].join(", ");
278
-
279
- export interface SqliteAuthStatements {
280
- /** One active key by its hash. The auth path's first read. */
281
- readonly byHash: { get: (hash: string) => Record<string, unknown> | null };
282
- /** One active key by its id. Run only for a key that has a parent. */
283
- readonly byId: { get: (id: string) => Record<string, unknown> | null };
284
- /** Lifetime consumption across one key's children, revoked included. */
285
- readonly childrenConsumed: { get: (parentId: string) => { total: number } | null };
286
- }
287
-
288
- export function prepareAuthStatements(sqlite: Database): SqliteAuthStatements {
289
- const active = `enabled = 1 AND revoked_at IS NULL`;
290
- return {
291
- byHash: sqlite.prepare(
292
- `SELECT ${AUTH_KEY_COLUMNS} FROM api_keys WHERE key_hash = ? AND ${active} LIMIT 1`,
293
- ) as SqliteAuthStatements["byHash"],
294
- byId: sqlite.prepare(
295
- `SELECT ${AUTH_KEY_COLUMNS} FROM api_keys WHERE id = ? AND ${active} LIMIT 1`,
296
- ) as SqliteAuthStatements["byId"],
297
- childrenConsumed: sqlite.prepare(
298
- `SELECT coalesce(sum(lifetime_tokens_consumed), 0) AS total FROM api_keys WHERE parent_key_id = ?`,
299
- ) as SqliteAuthStatements["childrenConsumed"],
300
- };
301
- }
302
-
303
-
304
- // A reloaded module must repatch the columns even when the handle is the
305
- // one booted before the reload. See `buildSqliteHandle`.
306
- installSqliteColumnMapping(fullSchema);
307
-
308
- export function buildSqliteHandle(sqlite: Database): DatabaseHandle {
309
- // Reinstalled on every call, not guarded. `bun --hot` keeps the booted
310
- // handle in `globalThis` and never rebuilds it, so a mapping installed
311
- // once at boot dies with the module it patched: booleans came back as
312
- // integers and login rejected a valid user as `unknown_user`. The
313
- // reloaded module calls this again and repatches the live columns.
314
- installSqliteColumnMapping(fullSchema);
315
- const built = drizzle(sqlite, { schema: fullSchema });
316
- // node-postgres exposes `execute` for raw SQL. bun-sqlite exposes `run` and
317
- // `all` instead, and the stores were written against `execute`. The alias
318
- // goes through `exec` rather than `run`: `run` prepares a statement per
319
- // call, and a raw statement here carries its values inlined, so there is
320
- // nothing to bind. Affected rows are read back from `changes()`.
321
- const execute = (query: unknown): { rowCount: number; rows: unknown[] } => {
322
- // Rendered the way the bun-sqlite session renders it: `?` placeholders and
323
- // a params list. Joining the chunks kept only their text, so an
324
- // interpolated value was dropped and the statement reached SQLite with a
325
- // hole in it.
326
- const rendered = renderExecute(query);
327
- if (rendered === undefined) throw new Error("sqlite execute: query has no SQL text");
328
- // `all` for a statement that returns rows, `exec` otherwise. Readiness
329
- // reads the migration ledger through `execute` and decides "applied" from
330
- // `result.rows`; an alias that only reported `changes()` made every select
331
- // look empty, so a migrated database stayed "not ready" forever.
332
- const returnsRows = /^\s*(select|with|pragma)\b/i.test(rendered.sql);
333
- if (returnsRows) {
334
- // `all()` returns stored values, not mapped ones, so a timestamp comes
335
- // back as text and a boolean as 0 or 1. The builder path maps through
336
- // the column; this is the only read that does not, and the backup
337
- // import compares these values against the builder's. Map them here.
338
- const rows = sqlite.prepare(rendered.sql).all(...rendered.params) as Record<string, unknown>[];
339
- const table = singleTable(rendered.sql);
340
- return { rowCount: rows.length, rows: rows.map((row) => mapExecuteRow(row, table)) };
341
- }
342
- sqlite.prepare(rendered.sql).run(...rendered.params);
343
- const changed = sqlite.prepare("SELECT changes() AS c").get() as { c: number } | null;
344
- return { rowCount: changed?.c ?? 0, rows: [] };
345
- };
346
- // A transaction callback receives a fresh builder, not this handle, so the
347
- // `execute` alias above would not reach it. First-boot setup takes its lock
348
- // through `transaction.execute`, and without the alias that call throws and
349
- // setup can never complete on the SQLite backend.
350
- const transaction = built.transaction.bind(built);
351
- const db = Object.assign(built, {
352
- execute,
353
- transaction: (fn: (tx: unknown) => unknown) =>
354
- transaction((tx: object) => fn(Object.assign(tx, { execute }))),
355
- });
356
- return {
357
- kind: "sqlite",
358
- db: db as unknown as DatabaseHandle["db"],
359
- sqlite,
360
- auth: prepareAuthStatements(sqlite),
361
- query: (text, params) => {
362
- // bun:sqlite types `all` as a tuple spread, which a `readonly unknown[]`
363
- // cannot satisfy. The values are the bound parameters the caller built;
364
- // the cast is confined to this one call.
365
- const bound = (params ?? []) as Parameters<ReturnType<Database["prepare"]>["all"]>;
366
- const rows = sqlite.prepare(text).all(...bound) as unknown[];
367
- return Promise.resolve({ rows });
368
- },
369
- exec: (script) => {
370
- sqlite.exec(script);
371
- return Promise.resolve();
372
- },
373
- close: () => {
374
- sqlite.close();
375
- return Promise.resolve();
376
- },
377
- };
378
- }
379
-
380
- /**
381
- * Applies the SQLite migration set in order.
382
- *
383
- * Same ledger, same skip-when-recorded rule, and the same one-file-per-
384
- * transaction atomicity as the PostgreSQL runner. Two deliberate absences,
385
- * both consequences of being the only process: no advisory lock, because a
386
- * second process cannot open the file for writing, and no
387
- * `to_regclass` probe, because SQLite has no schemas — `sqlite_master`
388
- * answers the same question.
389
- */
390
- /**
391
- * Rewrites the two PostgreSQL functions the schema's defaults emit.
392
- *
393
- * `gen_random_uuid()` becomes a quoted uuid. `now()` becomes a quoted UTC
394
- * timestamp. Both are matched only as function calls, so a column or string
395
- * that merely contains the words is left alone.
396
- */
397
- /**
398
- * Rewrites the two PostgreSQL functions the schema's defaults emit, and only
399
- * those. A statement that contains neither is returned as-is, so the common
400
- * case — every read, and every write that supplies its own values — pays one
401
- * `includes` and nothing else.
402
- */
403
- function sqliteSql(text: string): string {
404
- if (!text.includes("gen_random_uuid()") && !text.includes("now()")) return text;
405
- return text
406
- .replaceAll("gen_random_uuid()", () => `'${randomUUID()}'`)
407
- .replaceAll("now()", () => `'${new Date().toISOString()}'`);
408
- }
409
-
410
- /**
411
- * Pragmas for a file-backed database. Each one removes a stall or a fsync the
412
- * default pays per commit, and none of them weakens crash safety below what
413
- * the usage flush already accepts (at most the last second of deltas).
414
- *
415
- * `synchronous = NORMAL` is durable against a process crash under WAL; only a
416
- * power loss can drop the last commit. `busy_timeout` makes a writer wait
417
- * instead of failing instantly when a sweep holds the lock. The cache is 8 MB:
418
- * large enough for auth and the active catalog working set, without reserving
419
- * 64 MB that an idle gateway never touches. `temp_store` keeps sorts off disk.
420
- * The checkpoint interval keeps a request from stalling on one.
421
- */
422
- function applyFilePragmas(db: Database): void {
423
- db.exec("PRAGMA journal_mode = WAL");
424
- db.exec("PRAGMA synchronous = NORMAL");
425
- db.exec("PRAGMA busy_timeout = 5000");
426
- db.exec("PRAGMA cache_size = -8192");
427
- db.exec("PRAGMA temp_store = MEMORY");
428
- db.exec("PRAGMA wal_autocheckpoint = 1000");
429
- }
430
-
431
- /** The SQLite set lives beside the PostgreSQL one, never inside it. */
432
- export function resolveSqliteMigrationsFolder(): string {
433
- return resolvePackagePath("migrations", "lite-sqlite3");
434
- }
435
-
436
- export async function applySqliteMigrations(
437
- sqlite: Database,
438
- folder = resolveSqliteMigrationsFolder(),
439
- ): Promise<void> {
440
- sqlite.exec(SQLITE_LEDGER_DDL);
441
- const appliedRows = sqlite
442
- .prepare(`SELECT migration_id FROM ${MIGRATION_LEDGER_TABLE}`)
443
- .all() as { migration_id: string }[];
444
- const applied = new Set(appliedRows.map((row) => row.migration_id));
445
-
446
- const tenants = sqlite
447
- .prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'tenants'")
448
- .get() as { present: number } | null;
449
- const files = migrationFiles(folder);
450
- const baselineFile = files.find((file) => migrationIdFor(file) === BASELINE_MIGRATION_ID);
451
- const baselineMarker =
452
- baselineFile === undefined
453
- ? undefined
454
- : baselineSchemaMarker(readFileSync(baselineFile, "utf8"));
455
- assertMigrationHistorySupported(
456
- applied,
457
- [
458
- ...files.map(migrationIdFor),
459
- ...(baselineMarker === undefined ? [] : [baselineMarker]),
460
- ],
461
- tenants !== null,
462
- baselineMarker,
463
- );
464
-
465
- for (const file of files) {
466
- const migrationId = migrationIdFor(file);
467
- if (applied.has(migrationId)) continue;
468
- const source = readFileSync(file, "utf8");
469
- const record = sqlite.prepare(
470
- `INSERT INTO ${MIGRATION_LEDGER_TABLE} (migration_id) VALUES (?)`,
471
- );
472
- try {
473
- sqlite.transaction(() => {
474
- for (const statement of splitStatements(source)) {
475
- // The generated baseline opens with a comment block, and a
476
- // breakpoint at the end of a file leaves an empty part. `exec`
477
- // rejects both, so only text that still contains SQL is sent.
478
- const sql = statement.replace(/--[^\n]*/g, "").trim();
479
- if (sql.length === 0) continue;
480
- sqlite.exec(sql);
481
- }
482
- record.run(migrationId);
483
- if (migrationId === BASELINE_MIGRATION_ID && baselineMarker !== undefined) {
484
- record.run(baselineMarker);
485
- }
486
- })();
487
- applied.add(migrationId);
488
- if (migrationId === BASELINE_MIGRATION_ID && baselineMarker !== undefined) {
489
- applied.add(baselineMarker);
490
- }
491
- } catch (error) {
492
- throw new Error(`Migration ${migrationId} failed: ${String(error)}`, { cause: error });
493
- }
494
- }
495
- }
1
+ /** Embedded backend: bun:sqlite in the gateway's data dir. */
2
+ import { Database, type SQLQueryBindings } from "bun:sqlite";
3
+ import { drizzle } from "drizzle-orm/bun-sqlite";
4
+ import { getTableColumns, getTableName } from "drizzle-orm";
5
+ import { randomUUID } from "node:crypto";
6
+ import { mkdirSync, readFileSync } from "node:fs";
7
+ import { dirname } from "node:path";
8
+ import { resolvePackagePath } from "../config/package-paths";
9
+ import { fullSchema, type DatabaseHandle } from "./db-handle";
10
+ import {
11
+ BASELINE_MIGRATION_ID,
12
+ assertMigrationHistorySupported,
13
+ baselineSchemaMarker,
14
+ acceptedBaselineSchemaMarkers,
15
+ MIGRATION_LEDGER_TABLE,
16
+ migrationFiles,
17
+ migrationIdFor,
18
+ splitStatements,
19
+ } from "./migrate";
20
+
21
+ /**
22
+ * Ledger DDL for the SQLite backend.
23
+ *
24
+ * The shared ledger DDL is PostgreSQL (`timestamptz`, `now()`), and this
25
+ * backend never sends PostgreSQL DDL. The shape is the same table the
26
+ * readiness check reads, so a ledger written here is a ledger read there.
27
+ */
28
+ const SQLITE_LEDGER_DDL = `CREATE TABLE IF NOT EXISTS ${MIGRATION_LEDGER_TABLE} (
29
+ migration_id text PRIMARY KEY,
30
+ applied_at text NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
31
+ )`;
32
+
33
+ /**
34
+ * Opens the embedded database at `file`.
35
+ *
36
+ * `:memory:` is the test backend and is used as given. A filesystem path has
37
+ * its parent created first, because bun:sqlite does not create directories.
38
+ * Foreign keys are off unless asked for, so the pragma is set on every
39
+ * connection rather than trusted to a default. WAL keeps a reader from
40
+ * blocking the single writer.
41
+ */
42
+ /**
43
+ * Databases whose `prepare`/`exec` have already been wrapped.
44
+ *
45
+ * Keyed weakly so a closed in-memory database is not retained by the registry.
46
+ */
47
+ const SQLITE_PREPARED = new WeakSet<Database>();
48
+
49
+ export function createSqliteDatabase(file: string): Database {
50
+ if (file !== ":memory:") mkdirSync(dirname(file), { recursive: true });
51
+ const db = new Database(file);
52
+ db.exec("PRAGMA foreign_keys = ON");
53
+ if (file !== ":memory:") applyFilePragmas(db);
54
+ // Guarded: this function has to run per database, but a caller may hand it
55
+ // one that already went through here (a test builds a handle over a booted
56
+ // handle). Wrapping `prepare` twice ran the translation twice, and the
57
+ // second pass mangled SQL the first had already rewritten — `unrecognized
58
+ // token: ":"` from a placeholder the first pass produced.
59
+ if (SQLITE_PREPARED.has(db)) return db;
60
+ SQLITE_PREPARED.add(db);
61
+ const prepare = db.prepare.bind(db);
62
+ db.prepare = ((text: string) => {
63
+ const statement = prepare(sqliteSql(text));
64
+ // bun:sqlite rejects a Date binding ("Binding expected string, TypedArray,
65
+ // boolean, number, bigint or null"), and Drizzle's pg-core timestamp
66
+ // columns bind `Date` objects. The schema is shared with PostgreSQL, so
67
+ // the conversion lives here: every timestamp column is `text`, and an ISO
68
+ // string is exactly what those columns store.
69
+ const bind = (value: unknown): SQLQueryBindings =>
70
+ value instanceof Date ? value.toISOString() : (value as SQLQueryBindings);
71
+ const run = statement.run.bind(statement);
72
+ const all = statement.all.bind(statement);
73
+ const get = statement.get.bind(statement);
74
+ // Drizzle's bun-sqlite session reads rows through `values`, not `all`.
75
+ const values = statement.values.bind(statement);
76
+ const bound = (args: unknown[]): SQLQueryBindings[] => args.map(bind);
77
+ statement.run = ((...args: unknown[]) => run(...bound(args))) as typeof statement.run;
78
+ statement.all = ((...args: unknown[]) => all(...bound(args))) as typeof statement.all;
79
+ statement.get = ((...args: unknown[]) => get(...bound(args))) as typeof statement.get;
80
+ statement.values = ((...args: unknown[]) => values(...bound(args))) as typeof statement.values;
81
+ return statement;
82
+ }) as Database["prepare"];
83
+ const exec = db.exec.bind(db);
84
+ db.exec = ((text: string) => exec(sqliteSql(text))) as Database["exec"];
85
+ return db;
86
+ }
87
+
88
+ /**
89
+ * Teaches the shared pg-core columns to read what bun:sqlite returns.
90
+ *
91
+ * The schema is written for node-postgres, whose driver hands booleans back
92
+ * as booleans and timestamps as `Date`. bun:sqlite hands back the stored
93
+ * value unchanged: an integer for a boolean, text for a timestamp. A
94
+ * column's `mapFromDriverValue` is what turns a driver value into the type
95
+ * the schema declares, and Drizzle calls it for every read — a select, a
96
+ * join, a `returning()`. The pg-core boolean column returns its value
97
+ * unchanged and the timestamp column only converts strings, so neither
98
+ * produces what the schema promises from bun:sqlite's values. Replacing the
99
+ * mapper on each column instance fixes every read at once. Writes are
100
+ * untouched: booleans already bind as integers, and `Date` bindings are
101
+ * converted to ISO strings in `createSqliteDatabase`.
102
+ */
103
+ function installSqliteColumnMapping(schema: Record<string, unknown>): void {
104
+
105
+ for (const value of Object.values(schema)) {
106
+ const columns = tableColumns(value);
107
+ if (columns === undefined) continue;
108
+ const table = getTableName(Object.values(columns)[0]!.table as Parameters<typeof getTableName>[0]);
109
+ for (const column of Object.values(columns)) {
110
+ if (column.columnType === "PgBoolean") {
111
+ column.mapFromDriverValue = (read) => read === null ? null : read === 1 || read === true;
112
+ } else if (column.columnType === "PgTimestamp") {
113
+ const convert = column.mapFromDriverValue.bind(column);
114
+ column.mapFromDriverValue = (read) => {
115
+ // A column declared `text` keeps whatever it was bound as, so a
116
+ // timestamp can come back as the ISO string the writer produces, or
117
+ // as a number — or the digits of one — when a row was restored from
118
+ // a source that stored epoch milliseconds. `new Date` reads the
119
+ // digits as a year and returns an invalid date, so those are
120
+ // handled before the string conversion.
121
+ if (typeof read === "number") return new Date(read);
122
+ if (typeof read === "string" && /^\d+$/.test(read)) return new Date(Number(read));
123
+ return convert(read);
124
+ };
125
+ }
126
+ // Registered after the branches above replace it, so execute and the
127
+ // builder call the same function. A column neither branch touches keeps
128
+ // its own mapper, which is what already stringifies a numeric and parses
129
+ // jsonb. execute is the one read that never called it.
130
+ if (table !== undefined) {
131
+ columnMapping.set(mappingKey(table, column.name), column.mapFromDriverValue.bind(column));
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ interface MappedColumn {
138
+ readonly name: string;
139
+ readonly columnType: string;
140
+ readonly table: object;
141
+ mapFromDriverValue: (value: unknown) => unknown;
142
+ }
143
+
144
+ /**
145
+ * `table.column` to the mapper that turns bun:sqlite's stored value into the
146
+ * type the schema declares. `db.execute` reads through `prepare().all()`, which
147
+ * never calls a column mapper, so the alias applies these itself. A name that
148
+ * two tables share maps to the same type everywhere in this schema, and a
149
+ * column with no entry is returned as stored.
150
+ */
151
+ const columnMapping = new Map<string, (value: unknown) => unknown>();
152
+
153
+ function mappingKey(table: string, column: string): string {
154
+ return `${table}.${column}`;
155
+ }
156
+
157
+ /** One row from `prepare().all()`, with every known column mapped. */
158
+ function mapExecuteRow(row: Record<string, unknown>, table: string | undefined): Record<string, unknown> {
159
+ if (table === undefined) return row;
160
+ const mapped: Record<string, unknown> = {};
161
+ for (const [column, value] of Object.entries(row)) {
162
+ const convert = columnMapping.get(mappingKey(table, column));
163
+ mapped[column] = value === null || convert === undefined ? value : convert(value);
164
+ }
165
+ return mapped;
166
+ }
167
+
168
+ /**
169
+ * The table a raw statement reads, when it reads exactly one.
170
+ *
171
+ * `SELECT 1` and a join read columns from nowhere or from several tables, and
172
+ * neither can be mapped by name. Both are returned as stored, which is what
173
+ * they already were.
174
+ */
175
+ interface RenderedStatement {
176
+ readonly sql: string;
177
+ readonly params: SQLQueryBindings[];
178
+ }
179
+
180
+ /**
181
+ * A Drizzle `sql` value as bun:sqlite runs it: `?` placeholders and the values
182
+ * that fill them, in order.
183
+ *
184
+ * A chunk is text, a nested statement, or a bound value. Text is concatenated,
185
+ * a nested statement is rendered in place, and everything else is one `?`.
186
+ * `Date` values are converted here too, because this path prepares through the
187
+ * raw database rather than through the wrapper in `createSqliteDatabase`.
188
+ */
189
+ function renderExecute(query: unknown): RenderedStatement | undefined {
190
+ const chunks = (query as { queryChunks?: unknown[] }).queryChunks;
191
+ if (chunks === undefined) return undefined;
192
+ const params: SQLQueryBindings[] = [];
193
+ const sql = chunks.map((chunk) => renderChunk(chunk, params)).join("");
194
+ return { sql, params };
195
+ }
196
+
197
+ function renderChunk(chunk: unknown, params: SQLQueryBindings[]): string {
198
+ if (Array.isArray(chunk)) {
199
+ return `(${chunk.map((part) => renderChunk(part, params)).join(", ")})`;
200
+ }
201
+ if (typeof chunk === "object" && chunk !== null && "queryChunks" in chunk) {
202
+ return (chunk as { queryChunks: unknown[] }).queryChunks
203
+ .map((part) => renderChunk(part, params))
204
+ .join("");
205
+ }
206
+ if (typeof chunk === "object" && chunk !== null && Array.isArray((chunk as { value?: unknown }).value)) {
207
+ return ((chunk as { value: string[] }).value).join("");
208
+ }
209
+ // A table, column, or quoted name is SQL text, not a bound value. Rendering
210
+ // one as a placeholder produces `DELETE FROM ?`, which SQLite rejects with
211
+ // `near "?": syntax error` — the failure the payload sweep logs every pass.
212
+ const kind = drizzleKind(chunk);
213
+ if (kind === "Table") {
214
+ return `"${getTableName(chunk as Parameters<typeof getTableName>[0])}"`;
215
+ }
216
+ if (kind === "Name") return `"${(chunk as { value: string }).value}"`;
217
+ if (kind === "Column") return `"${(chunk as { name: string }).name}"`;
218
+ const value = chunk instanceof Date ? chunk.toISOString() : chunk;
219
+ params.push(value as SQLQueryBindings);
220
+ return "?";
221
+ }
222
+
223
+
224
+ function drizzleKind(chunk: unknown): string | undefined {
225
+ if (typeof chunk !== "object" || chunk === null) return undefined;
226
+ // A pg column's own kind is `PgUUID` or `PgTimestamp`, not `Column`. Walk
227
+ // the constructors: the base `Column` marks every one of them.
228
+ let ctor = chunk.constructor as { [entityKind]?: unknown } | null;
229
+ while (ctor !== null && ctor !== Function.prototype) {
230
+ const kind = ctor[entityKind];
231
+ if (kind === "Column" || kind === "Table" || kind === "Name") return kind;
232
+ ctor = Object.getPrototypeOf(ctor) as { [entityKind]?: unknown } | null;
233
+ }
234
+ return undefined;
235
+ }
236
+
237
+ const entityKind = Symbol.for("drizzle:entityKind");
238
+ function singleTable(sql: string): string | undefined {
239
+ const tables = [...sql.matchAll(/\b(?:from|join)\s+"?([a-z0-9_]+)"?/gi)].map((match) => match[1]);
240
+ const distinct = new Set(tables);
241
+ return distinct.size === 1 ? [...distinct][0] : undefined;
242
+ }
243
+
244
+ /** The columns of one Drizzle table, or undefined for everything else in the schema. */
245
+ function tableColumns(value: unknown): Record<string, MappedColumn> | undefined {
246
+ if (typeof value !== "object" || value === null) return undefined;
247
+ try {
248
+ const columns = getTableColumns(value as Parameters<typeof getTableColumns>[0]);
249
+ return Object.keys(columns).length > 0 ? columns as Record<string, MappedColumn> : undefined;
250
+ } catch {
251
+ return undefined;
252
+ }
253
+ }
254
+
255
+ /**
256
+ * The handle stores run on.
257
+ *
258
+ * `db` is the bun-sqlite Drizzle instance. It is not the node-postgres
259
+ * `CartethyiaDatabase` the stores are typed against — the two query builders
260
+ * are different types — so the conversion happens once, here. The stores
261
+ * exercise only the builder surface both drivers share.
262
+ */
263
+ /**
264
+ * The three statements the auth path runs on a cache miss, prepared once.
265
+ *
266
+ * Drizzle rebuilds the query on every call, and that rebuild measured 60x the
267
+ * cost of the lookup itself. These are static SQL against an index, so there
268
+ * is nothing for a builder to decide per call. The column list is exactly the
269
+ * `api_keys` row the store maps, in the order `findActiveByHash` reads it.
270
+ */
271
+ const AUTH_KEY_COLUMNS = [
272
+ "id", "tenant_id", "key_hash", "key_mode", "parent_key_id", "issued_client_ip",
273
+ "issued_client_ip_key", "label", "scopes", "requests_per_minute", "daily_token_limit",
274
+ "monthly_token_limit", "lifetime_token_budget", "lifetime_tokens_consumed",
275
+ "max_concurrent_requests", "model_access_mode", "model_list", "client_router_denylist",
276
+ "model_prefix", "created_at", "enabled", "revoked_at", "key_prefix", "key_encrypted",
277
+ "notes_title", "notes_subtitle", "notes_body", "is_default", "sort_index",
278
+ ].join(", ");
279
+
280
+ export interface SqliteAuthStatements {
281
+ /** One active key by its hash. The auth path's first read. */
282
+ readonly byHash: { get: (hash: string) => Record<string, unknown> | null };
283
+ /** One active key by its id. Run only for a key that has a parent. */
284
+ readonly byId: { get: (id: string) => Record<string, unknown> | null };
285
+ /** Lifetime consumption across one key's children, revoked included. */
286
+ readonly childrenConsumed: { get: (parentId: string) => { total: number } | null };
287
+ }
288
+
289
+ export function prepareAuthStatements(sqlite: Database): SqliteAuthStatements {
290
+ const active = `enabled = 1 AND revoked_at IS NULL`;
291
+ return {
292
+ byHash: sqlite.prepare(
293
+ `SELECT ${AUTH_KEY_COLUMNS} FROM api_keys WHERE key_hash = ? AND ${active} LIMIT 1`,
294
+ ) as SqliteAuthStatements["byHash"],
295
+ byId: sqlite.prepare(
296
+ `SELECT ${AUTH_KEY_COLUMNS} FROM api_keys WHERE id = ? AND ${active} LIMIT 1`,
297
+ ) as SqliteAuthStatements["byId"],
298
+ childrenConsumed: sqlite.prepare(
299
+ `SELECT coalesce(sum(lifetime_tokens_consumed), 0) AS total FROM api_keys WHERE parent_key_id = ?`,
300
+ ) as SqliteAuthStatements["childrenConsumed"],
301
+ };
302
+ }
303
+
304
+
305
+ // A reloaded module must repatch the columns even when the handle is the
306
+ // one booted before the reload. See `buildSqliteHandle`.
307
+ installSqliteColumnMapping(fullSchema);
308
+
309
+ export function buildSqliteHandle(sqlite: Database): DatabaseHandle {
310
+ // Reinstalled on every call, not guarded. `bun --hot` keeps the booted
311
+ // handle in `globalThis` and never rebuilds it, so a mapping installed
312
+ // once at boot dies with the module it patched: booleans came back as
313
+ // integers and login rejected a valid user as `unknown_user`. The
314
+ // reloaded module calls this again and repatches the live columns.
315
+ installSqliteColumnMapping(fullSchema);
316
+ const built = drizzle(sqlite, { schema: fullSchema });
317
+ // node-postgres exposes `execute` for raw SQL. bun-sqlite exposes `run` and
318
+ // `all` instead, and the stores were written against `execute`. The alias
319
+ // goes through `exec` rather than `run`: `run` prepares a statement per
320
+ // call, and a raw statement here carries its values inlined, so there is
321
+ // nothing to bind. Affected rows are read back from `changes()`.
322
+ const execute = (query: unknown): { rowCount: number; rows: unknown[] } => {
323
+ // Rendered the way the bun-sqlite session renders it: `?` placeholders and
324
+ // a params list. Joining the chunks kept only their text, so an
325
+ // interpolated value was dropped and the statement reached SQLite with a
326
+ // hole in it.
327
+ const rendered = renderExecute(query);
328
+ if (rendered === undefined) throw new Error("sqlite execute: query has no SQL text");
329
+ // `all` for a statement that returns rows, `exec` otherwise. Readiness
330
+ // reads the migration ledger through `execute` and decides "applied" from
331
+ // `result.rows`; an alias that only reported `changes()` made every select
332
+ // look empty, so a migrated database stayed "not ready" forever.
333
+ const returnsRows = /^\s*(select|with|pragma)\b/i.test(rendered.sql);
334
+ if (returnsRows) {
335
+ // `all()` returns stored values, not mapped ones, so a timestamp comes
336
+ // back as text and a boolean as 0 or 1. The builder path maps through
337
+ // the column; this is the only read that does not, and the backup
338
+ // import compares these values against the builder's. Map them here.
339
+ const rows = sqlite.prepare(rendered.sql).all(...rendered.params) as Record<string, unknown>[];
340
+ const table = singleTable(rendered.sql);
341
+ return { rowCount: rows.length, rows: rows.map((row) => mapExecuteRow(row, table)) };
342
+ }
343
+ sqlite.prepare(rendered.sql).run(...rendered.params);
344
+ const changed = sqlite.prepare("SELECT changes() AS c").get() as { c: number } | null;
345
+ return { rowCount: changed?.c ?? 0, rows: [] };
346
+ };
347
+ // A transaction callback receives a fresh builder, not this handle, so the
348
+ // `execute` alias above would not reach it. First-boot setup takes its lock
349
+ // through `transaction.execute`, and without the alias that call throws and
350
+ // setup can never complete on the SQLite backend.
351
+ const transaction = built.transaction.bind(built);
352
+ const db = Object.assign(built, {
353
+ execute,
354
+ transaction: (fn: (tx: unknown) => unknown) =>
355
+ transaction((tx: object) => fn(Object.assign(tx, { execute }))),
356
+ });
357
+ return {
358
+ kind: "sqlite",
359
+ db: db as unknown as DatabaseHandle["db"],
360
+ sqlite,
361
+ auth: prepareAuthStatements(sqlite),
362
+ query: (text, params) => {
363
+ // bun:sqlite types `all` as a tuple spread, which a `readonly unknown[]`
364
+ // cannot satisfy. The values are the bound parameters the caller built;
365
+ // the cast is confined to this one call.
366
+ const bound = (params ?? []) as Parameters<ReturnType<Database["prepare"]>["all"]>;
367
+ const rows = sqlite.prepare(text).all(...bound) as unknown[];
368
+ return Promise.resolve({ rows });
369
+ },
370
+ exec: (script) => {
371
+ sqlite.exec(script);
372
+ return Promise.resolve();
373
+ },
374
+ close: () => {
375
+ sqlite.close();
376
+ return Promise.resolve();
377
+ },
378
+ };
379
+ }
380
+
381
+ /**
382
+ * Applies the SQLite migration set in order.
383
+ *
384
+ * Same ledger, same skip-when-recorded rule, and the same one-file-per-
385
+ * transaction atomicity as the PostgreSQL runner. Two deliberate absences,
386
+ * both consequences of being the only process: no advisory lock, because a
387
+ * second process cannot open the file for writing, and no
388
+ * `to_regclass` probe, because SQLite has no schemas — `sqlite_master`
389
+ * answers the same question.
390
+ */
391
+ /**
392
+ * Rewrites the two PostgreSQL functions the schema's defaults emit.
393
+ *
394
+ * `gen_random_uuid()` becomes a quoted uuid. `now()` becomes a quoted UTC
395
+ * timestamp. Both are matched only as function calls, so a column or string
396
+ * that merely contains the words is left alone.
397
+ */
398
+ /**
399
+ * Rewrites the two PostgreSQL functions the schema's defaults emit, and only
400
+ * those. A statement that contains neither is returned as-is, so the common
401
+ * case — every read, and every write that supplies its own values — pays one
402
+ * `includes` and nothing else.
403
+ */
404
+ function sqliteSql(text: string): string {
405
+ if (!text.includes("gen_random_uuid()") && !text.includes("now()")) return text;
406
+ return text
407
+ .replaceAll("gen_random_uuid()", () => `'${randomUUID()}'`)
408
+ .replaceAll("now()", () => `'${new Date().toISOString()}'`);
409
+ }
410
+
411
+ /**
412
+ * Pragmas for a file-backed database. Each one removes a stall or a fsync the
413
+ * default pays per commit, and none of them weakens crash safety below what
414
+ * the usage flush already accepts (at most the last second of deltas).
415
+ *
416
+ * `synchronous = NORMAL` is durable against a process crash under WAL; only a
417
+ * power loss can drop the last commit. `busy_timeout` makes a writer wait
418
+ * instead of failing instantly when a sweep holds the lock. The cache is 8 MB:
419
+ * large enough for auth and the active catalog working set, without reserving
420
+ * 64 MB that an idle gateway never touches. `temp_store` keeps sorts off disk.
421
+ * The checkpoint interval keeps a request from stalling on one.
422
+ */
423
+ function applyFilePragmas(db: Database): void {
424
+ db.exec("PRAGMA journal_mode = WAL");
425
+ db.exec("PRAGMA synchronous = NORMAL");
426
+ db.exec("PRAGMA busy_timeout = 5000");
427
+ db.exec("PRAGMA cache_size = -8192");
428
+ db.exec("PRAGMA temp_store = MEMORY");
429
+ db.exec("PRAGMA wal_autocheckpoint = 1000");
430
+ }
431
+
432
+ /** The SQLite set lives beside the PostgreSQL one, never inside it. */
433
+ export function resolveSqliteMigrationsFolder(): string {
434
+ return resolvePackagePath("migrations", "lite-sqlite3");
435
+ }
436
+
437
+ export async function applySqliteMigrations(
438
+ sqlite: Database,
439
+ folder = resolveSqliteMigrationsFolder(),
440
+ ): Promise<void> {
441
+ sqlite.exec(SQLITE_LEDGER_DDL);
442
+ const appliedRows = sqlite
443
+ .prepare(`SELECT migration_id FROM ${MIGRATION_LEDGER_TABLE}`)
444
+ .all() as { migration_id: string }[];
445
+ const applied = new Set(appliedRows.map((row) => row.migration_id));
446
+
447
+ const tenants = sqlite
448
+ .prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'tenants'")
449
+ .get() as { present: number } | null;
450
+ const files = migrationFiles(folder);
451
+ const baselineFile = files.find((file) => migrationIdFor(file) === BASELINE_MIGRATION_ID);
452
+ const baselineSource =
453
+ baselineFile === undefined ? undefined : readFileSync(baselineFile, "utf8");
454
+ const baselineMarker =
455
+ baselineSource === undefined ? undefined : baselineSchemaMarker(baselineSource);
456
+ assertMigrationHistorySupported(
457
+ applied,
458
+ [
459
+ ...files.map(migrationIdFor),
460
+ ...(baselineMarker === undefined ? [] : [baselineMarker]),
461
+ ...(baselineSource === undefined ? [] : acceptedBaselineSchemaMarkers(baselineSource)),
462
+ ],
463
+ tenants !== null,
464
+ baselineMarker,
465
+ );
466
+
467
+ for (const file of files) {
468
+ const migrationId = migrationIdFor(file);
469
+ if (applied.has(migrationId)) continue;
470
+ const source = readFileSync(file, "utf8");
471
+ const record = sqlite.prepare(
472
+ `INSERT INTO ${MIGRATION_LEDGER_TABLE} (migration_id) VALUES (?)`,
473
+ );
474
+ try {
475
+ sqlite.transaction(() => {
476
+ for (const statement of splitStatements(source)) {
477
+ // The generated baseline opens with a comment block, and a
478
+ // breakpoint at the end of a file leaves an empty part. `exec`
479
+ // rejects both, so only text that still contains SQL is sent.
480
+ const sql = statement.replace(/--[^\n]*/g, "").trim();
481
+ if (sql.length === 0) continue;
482
+ sqlite.exec(sql);
483
+ }
484
+ record.run(migrationId);
485
+ if (migrationId === BASELINE_MIGRATION_ID && baselineMarker !== undefined) {
486
+ record.run(baselineMarker);
487
+ }
488
+ })();
489
+ applied.add(migrationId);
490
+ if (migrationId === BASELINE_MIGRATION_ID && baselineMarker !== undefined) {
491
+ applied.add(baselineMarker);
492
+ }
493
+ } catch (error) {
494
+ throw new Error(`Migration ${migrationId} failed: ${String(error)}`, { cause: error });
495
+ }
496
+ }
497
+ }