cartethyia 2.5.4 → 2.5.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 (395) hide show
  1. package/.env.example +13 -4
  2. package/README.md +2 -2
  3. package/dist/dashboard/assets/{BannedPage-C2orBsI2.js → BannedPage-CADDq4IU.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BV6pxVQo.js → CliToolsPage-DuO1Z7dq.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-CW-wPmJ6.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-0p8uX7vB.js → CustomizationPage-BR5HHvbi.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
  15. package/dist/dashboard/assets/{HealthEventsModal-BjqSMfko.js → HealthEventsModal-mUfcknqm.js} +1 -1
  16. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-BvzCx8Kr.js → LoginPage-V5faV9V2.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
  19. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
  20. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProvidersPage-Js9jUPSb.js → ProvidersPage-uPiGVTTK.js} +1 -1
  26. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
  27. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
  28. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-DDbMQsoo.js → SetupPage-UB690SE2.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-CB3Z7VTf.js → SortableList-BLujdX7N.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-D95UCPHI.js → StudioPage-D5C2raAc.js} +3 -3
  38. package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
  39. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
  40. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
  41. package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
  42. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  43. package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
  44. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  45. package/dist/dashboard/assets/{assert-Chi7pCnt.js → assert-C9FD_JwJ.js} +1 -1
  46. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  47. package/dist/dashboard/assets/{badge-D4pQapKj.js → badge-CR72dasT.js} +1 -1
  48. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  49. package/dist/dashboard/assets/{banner-DH6cg-_B.js → banner-CI9_eSfO.js} +1 -1
  50. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  51. package/dist/dashboard/assets/{card-BKqlUOe4.js → card-DS7CQEsO.js} +1 -1
  52. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  53. package/dist/dashboard/assets/{cli-tools-fTgljTGA.js → cli-tools-CNqzrfdb.js} +1 -1
  54. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  55. package/dist/dashboard/assets/{clipboard-button-D5Ys_sM0.js → clipboard-button-611pSsaX.js} +1 -1
  56. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  57. package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
  58. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  59. package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
  60. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  61. package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
  62. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CQBr6U9c.js → entry-B9Xvtnfj.js} +1 -1
  64. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
  66. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
  68. package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
  69. package/dist/dashboard/assets/{github-badge-BlptL8Uf.js → github-badge-sltk1qUi.js} +1 -1
  70. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  71. package/dist/dashboard/assets/{index-js6TJvdr.js → index-CmxZr14F.js} +2 -2
  72. package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
  73. package/dist/dashboard/assets/{inline-DUWDSmDV.js → inline-BhY2sSnO.js} +1 -1
  74. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  75. package/dist/dashboard/assets/{input-LVjgZq4f.js → input-V1umFiBk.js} +1 -1
  76. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  77. package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
  78. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  79. package/dist/dashboard/assets/{list-row-DupvLfDx.js → list-row-1zzZYM0B.js} +1 -1
  80. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  81. package/dist/dashboard/assets/{live-C4pEJUqI.js → live-BivGfuUs.js} +1 -1
  82. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  83. package/dist/dashboard/assets/{model-bans-BRiuufkB.js → model-bans-CmffjLuG.js} +1 -1
  84. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-B4ROwk8L.js → mutation-46P0oWMe.js} +1 -1
  86. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  87. package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
  88. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-gmjzC3RS.js → query-keys-BFhGpwR1.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  91. package/dist/dashboard/assets/{routing-C7tBT5o-.js → routing-CAb4F37R.js} +1 -1
  92. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  93. package/dist/dashboard/assets/{select-WUCXorXw.js → select-BYR_5K2c.js} +1 -1
  94. package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
  95. package/dist/dashboard/assets/{settings-C_zKl1GS.js → settings-DNX9sydi.js} +1 -1
  96. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  97. package/dist/dashboard/assets/{stack-_3O0F8Xn.js → stack-BHTfXdz_.js} +1 -1
  98. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  99. package/dist/dashboard/assets/{state-BgTxHcJS.js → state-mPDRlQkl.js} +1 -1
  100. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  101. package/dist/dashboard/assets/{switch-DEjTtOWf.js → switch-DOmJxg7H.js} +1 -1
  102. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  103. package/dist/dashboard/assets/tokens-COzario9.js +1 -0
  104. package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
  105. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
  106. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
  107. package/dist/dashboard/assets/{useMutation-BWSC0UAS.js → useMutation-Dem0lXQj.js} +1 -1
  108. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  109. package/dist/dashboard/index.html +1 -1
  110. package/dist/dashboard/index.html.gz +0 -0
  111. package/dist/dashboard/precompressed-assets.json +54 -54
  112. package/migrations/README.md +52 -52
  113. package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
  114. package/package.json +96 -95
  115. package/scripts/cli/admin.ts +125 -0
  116. package/scripts/cli/{ui/background.ts → background.ts} +1 -1
  117. package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -244
  118. package/scripts/cli/doctor.ts +37 -0
  119. package/scripts/cli/{support/env.ts → env.ts} +99 -99
  120. package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
  121. package/scripts/cli/instance.ts +224 -0
  122. package/scripts/cli/main.ts +61 -18
  123. package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
  124. package/scripts/cli/port.ts +100 -0
  125. package/scripts/cli/{support/services.ts → services.ts} +88 -88
  126. package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
  127. package/scripts/cli/{command/start.ts → start.ts} +10 -5
  128. package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
  129. package/scripts/cli/tray/index.ts +62 -12
  130. package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
  131. package/scripts/cli/{command/update.ts → update.ts} +39 -39
  132. package/scripts/cli/version.ts +67 -0
  133. package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
  134. package/src/boot/app.ts +697 -693
  135. package/src/boot/dependencies.ts +647 -646
  136. package/src/boot/timeout.ts +2 -13
  137. package/src/config/cooldowns.ts +186 -138
  138. package/src/config/network.ts +8 -14
  139. package/src/config/providers.ts +7 -10
  140. package/src/config/runtime.ts +76 -76
  141. package/src/config/security.ts +15 -0
  142. package/src/config/spec.ts +20 -30
  143. package/src/config/telemetry.ts +0 -20
  144. package/src/config/timeouts.ts +0 -6
  145. package/src/console/admin/model-abuse/contracts.ts +28 -6
  146. package/src/console/admin/model-abuse/routes.ts +56 -3
  147. package/src/console/api-keys/contracts.ts +410 -414
  148. package/src/console/api-keys/routes.ts +881 -880
  149. package/src/console/auth/credentials.ts +3 -3
  150. package/src/console/auth/setup-service.ts +2 -2
  151. package/src/console/backup/export-endpoint.ts +6 -2
  152. package/src/console/backup/nine-router.ts +2 -5
  153. package/src/console/backup/service.ts +4 -0
  154. package/src/console/backup/store.ts +192 -29
  155. package/src/console/backup/validate.ts +3 -45
  156. package/src/console/cli-tools/fs-ops.ts +487 -487
  157. package/src/console/console-router-guards.ts +1 -1
  158. package/src/console/data-management/contracts.ts +13 -6
  159. package/src/console/data-management/operations.ts +23 -18
  160. package/src/console/data-management/routes.ts +0 -1
  161. package/src/console/domain-registration.ts +10 -5
  162. package/src/console/observability/live.ts +22 -0
  163. package/src/console/observability/store.ts +39 -16
  164. package/src/console/observability/usage-dimensions.ts +4 -4
  165. package/src/console/providers/catalog/catalog-projections.ts +6 -7
  166. package/src/console/providers/catalog/model-operations.ts +5 -2
  167. package/src/console/providers/catalog/provider-operations.ts +4 -4
  168. package/src/console/providers/catalog/public-model-store.ts +8 -11
  169. package/src/console/providers/catalog/store.ts +1553 -1553
  170. package/src/console/providers/oauth/routes.ts +8 -12
  171. package/src/console/quota/account-quota-tenant.ts +6 -6
  172. package/src/console/quota/quota-view.ts +1 -1
  173. package/src/console/remote-support/contracts.ts +61 -0
  174. package/src/console/remote-support/routes.ts +65 -0
  175. package/src/console/remote-support/service.ts +207 -0
  176. package/src/console/response-compression.ts +0 -3
  177. package/src/console/routing/pools/probe-result.ts +3 -2
  178. package/src/console/routing/pools/probe-verdict.ts +132 -132
  179. package/src/console/routing/pools/store.ts +8 -8
  180. package/src/console/settings/contracts.ts +58 -2
  181. package/src/console/settings/store.ts +30 -3
  182. package/src/console/shared/cursor.ts +8 -8
  183. package/src/console/shared/errors.ts +1 -3
  184. package/src/network/http2-bidi.ts +2 -4
  185. package/src/network/pool/agent.ts +478 -478
  186. package/src/network/pool/selector.ts +5 -4
  187. package/src/network/retry.ts +1 -2
  188. package/src/network/ssrf.ts +5 -4
  189. package/src/observability/event-compaction.ts +80 -78
  190. package/src/observability/event-day-index.ts +13 -15
  191. package/src/observability/event-log.ts +4 -4
  192. package/src/observability/logger.ts +6 -6
  193. package/src/observability/payload-capture.ts +151 -33
  194. package/src/observability/payload-store.ts +397 -440
  195. package/src/observability/redaction.ts +5 -6
  196. package/src/observability/request-notify.ts +43 -0
  197. package/src/observability/telemetry-status.ts +8 -9
  198. package/src/observability/token-speed.ts +5 -4
  199. package/src/persistence/db-mode.ts +7 -7
  200. package/src/persistence/readiness.ts +1 -1
  201. package/src/persistence/schema.ts +1406 -1402
  202. package/src/persistence/shared-cache.ts +1 -2
  203. package/src/persistence/stores/api-key-store.ts +18 -6
  204. package/src/persistence/stores/share-store.ts +4 -3
  205. package/src/persistence/stores/telemetry-store.ts +236 -237
  206. package/src/protocol/canonical-model.ts +11 -3
  207. package/src/protocol/codecs.ts +138 -4
  208. package/src/protocol/gateway-error.ts +6 -4
  209. package/src/protocol/request/chat.ts +21 -1
  210. package/src/protocol/request/codex.ts +10 -1
  211. package/src/protocol/request/messages.ts +4 -5
  212. package/src/protocol/request/responses.ts +9 -1
  213. package/src/protocol/response/chat.ts +34 -4
  214. package/src/protocol/response/codex.ts +7 -4
  215. package/src/protocol/response/dsml.ts +140 -28
  216. package/src/protocol/response/responses.ts +11 -5
  217. package/src/protocol/stream-error-frames.ts +4 -5
  218. package/src/providers/authentication/jwt-validator.ts +9 -10
  219. package/src/providers/authentication/oauth-refresh-service.ts +9 -11
  220. package/src/providers/configured-openai-providers.ts +134 -136
  221. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  222. package/src/providers/discovery/catalog/catalog-format.ts +3 -6
  223. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  224. package/src/providers/discovery/probe-phases.ts +4 -4
  225. package/src/providers/discovery/probe-wire.ts +6 -13
  226. package/src/providers/discovery/probing-service.ts +1117 -1045
  227. package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
  228. package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
  229. package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
  230. package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
  231. package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
  232. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
  233. package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
  234. package/src/providers/integrations/buddy/buddy-media.ts +2 -2
  235. package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
  236. package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
  237. package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
  238. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
  239. package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
  240. package/src/providers/integrations/claude/claude.ts +608 -608
  241. package/src/providers/integrations/cline/cline-quota.ts +132 -132
  242. package/src/providers/integrations/cline/cline.ts +537 -539
  243. package/src/providers/integrations/codex/codex-oauth.ts +1 -1
  244. package/src/providers/integrations/commandcode/commandcode.ts +674 -675
  245. package/src/providers/integrations/commandcode/discovery.ts +117 -116
  246. package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
  247. package/src/providers/integrations/cursor/cursor-quota.ts +309 -310
  248. package/src/providers/integrations/cursor/cursor.ts +11 -163
  249. package/src/providers/integrations/factory/factory-models.ts +3 -5
  250. package/src/providers/integrations/grok/grok-install-id.ts +2 -3
  251. package/src/providers/integrations/grok/grok.ts +6 -7
  252. package/src/providers/integrations/hermes.ts +14 -18
  253. package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
  254. package/src/providers/integrations/kiro/kiro.ts +1 -1
  255. package/src/providers/integrations/mistral.ts +6 -6
  256. package/src/providers/integrations/opencode/fingerprint.ts +3 -3
  257. package/src/providers/integrations/opencode/provider.ts +8 -8
  258. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
  259. package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
  260. package/src/providers/integrations/qoder/qoder.ts +884 -865
  261. package/src/providers/integrations/shared/connect.ts +6 -6
  262. package/src/providers/integrations/trae/trae-catalog.ts +1 -2
  263. package/src/providers/integrations/trae/trae.ts +2 -2
  264. package/src/providers/integrations/zcode/oauth.ts +6 -1
  265. package/src/providers/operations/account-health-service.ts +1528 -1523
  266. package/src/providers/operations/account-reset-service.ts +1 -1
  267. package/src/providers/operations/client-versions.ts +6 -6
  268. package/src/providers/operations/custom-cli-headers.ts +3 -4
  269. package/src/providers/operations/provider-catalog-seeder.ts +95 -95
  270. package/src/providers/operations/provider-catalog-service.ts +3 -4
  271. package/src/providers/provider-metadata.ts +4 -8
  272. package/src/providers/provider-registry.ts +6 -7
  273. package/src/providers/provider-usage.ts +1 -1
  274. package/src/security/abuse.ts +15 -10
  275. package/src/security/access-control.ts +144 -146
  276. package/src/security/admission/contracts.ts +11 -0
  277. package/src/security/admission/in-memory-store.ts +4 -3
  278. package/src/security/admission/lease-sweep.ts +6 -6
  279. package/src/security/admission/reasons.ts +5 -1
  280. package/src/security/admission/redis-store.ts +211 -210
  281. package/src/security/admission/service.ts +4 -0
  282. package/src/security/api-key-auth.ts +504 -504
  283. package/src/security/ip-boundary.ts +155 -15
  284. package/src/security/model-abuse.ts +366 -94
  285. package/src/security/outbound-headers.ts +3 -4
  286. package/src/shared/sleep.ts +4 -0
  287. package/src/transport/dispatch/attempt-finalize.ts +47 -5
  288. package/src/transport/dispatch/attempt-loop.ts +531 -530
  289. package/src/transport/dispatch/stream-bridge.ts +1 -3
  290. package/src/transport/dispatch/streaming/finalize.ts +1 -1
  291. package/src/transport/dispatch/streaming/stream.ts +5 -6
  292. package/src/transport/failure-policy.ts +162 -8
  293. package/src/transport/guards/auth-guard.ts +7 -4
  294. package/src/transport/guards/concurrency-guard.ts +0 -5
  295. package/src/transport/guards/error-lifecycle.ts +13 -6
  296. package/src/transport/guards/pipeline.ts +15 -3
  297. package/src/transport/guards/request-context.ts +8 -2
  298. package/src/transport/request/inflight.ts +5 -6
  299. package/src/transport/request/preparer.ts +2 -2
  300. package/src/transport/request/state.ts +12 -8
  301. package/src/transport/routing/admission.ts +4 -0
  302. package/src/transport/routing/route-catalog.ts +8 -1
  303. package/src/transport/routing/route-model.ts +46 -14
  304. package/src/transport/routing/router.ts +21 -14
  305. package/src/transport/surface/completion.ts +2 -3
  306. package/src/transport/surface/dialects.ts +5 -6
  307. package/src/transport/surface/responses/encode.ts +38 -0
  308. package/src/transport/surface/responses/parse.ts +4 -0
  309. package/src/transport/translation/capabilities.ts +2 -3
  310. package/src/transport/translation/quirks.ts +3 -3
  311. package/src/workers/buddy-growth-tasks.ts +5 -10
  312. package/src/workers/daily-checkin.ts +17 -23
  313. package/src/workers/quota-refresh-sweep.ts +26 -26
  314. package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
  315. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +0 -1
  316. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
  317. package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
  318. package/dist/dashboard/assets/CombosPage-Bsan4679.js +0 -1
  319. package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
  320. package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
  321. package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
  322. package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
  323. package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
  324. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js +0 -1
  325. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
  326. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +0 -1
  327. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
  328. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +0 -3
  329. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
  330. package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
  331. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +0 -7
  332. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
  333. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +0 -1
  334. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
  335. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +0 -1
  336. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
  337. package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
  338. package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
  339. package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
  340. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js +0 -56
  341. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
  342. package/dist/dashboard/assets/api-keys-VY68Mhd1.js +0 -1
  343. package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
  344. package/dist/dashboard/assets/app-COlK95oi.css +0 -2
  345. package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
  346. package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
  347. package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
  348. package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
  349. package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
  350. package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
  351. package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
  352. package/dist/dashboard/assets/dialog-BYj-8WuK.js +0 -1
  353. package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
  354. package/dist/dashboard/assets/drawer-CmXIZ6H1.js +0 -1
  355. package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
  356. package/dist/dashboard/assets/entry-CBg5zeu8.js +0 -1
  357. package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
  358. package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
  359. package/dist/dashboard/assets/entry-CgdSAzEV.js +0 -4
  360. package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
  361. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  362. package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
  363. package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
  364. package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
  365. package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
  366. package/dist/dashboard/assets/layout-CPfxpsfy.js +0 -1
  367. package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
  368. package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
  369. package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
  370. package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
  371. package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
  372. package/dist/dashboard/assets/numbers-YNcHvRY7.js +0 -2
  373. package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
  374. package/dist/dashboard/assets/query-keys-gmjzC3RS.js.gz +0 -0
  375. package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
  376. package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
  377. package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
  378. package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
  379. package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
  380. package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
  381. package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
  382. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  383. package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
  384. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  385. package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
  386. package/scripts/cli/command/admin.ts +0 -65
  387. package/scripts/cli/command/doctor.ts +0 -257
  388. package/scripts/cli/gateway/drain-token.ts +0 -78
  389. package/scripts/cli/gateway/instance-lock.ts +0 -105
  390. package/scripts/cli/gateway/stop.ts +0 -51
  391. package/scripts/cli/support/bun.ts +0 -27
  392. package/scripts/cli/support/version.ts +0 -37
  393. /package/dist/dashboard/assets/{app-DCQZRmHY.js → app-BIt446yE.js} +0 -0
  394. /package/dist/dashboard/assets/{app-DCQZRmHY.js.gz → app-BIt446yE.js.gz} +0 -0
  395. /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
@@ -28,9 +28,9 @@ export class ConsoleCredentialService {
28
28
  * undecodable hashes: a corrupted `password_hash` row must surface as an
29
29
  * ordinary failed login (counted by the lockout service), never as a 500
30
30
  * that lets an attacker probe hash integrity or bypass the failure counter.
31
- * Legacy/migrated hashes are rejected here rather than verified — a hash
32
- * that does not start with `$argon2id$` can only originate from a foreign
33
- * scheme this deployment never issued.
31
+ * A hash that does not start with `$argon2id$` is rejected here rather than
32
+ * verified — it can only originate from a foreign scheme this deployment
33
+ * never issued.
34
34
  */
35
35
  async verifyPassword(password: string, hash: string): Promise<boolean> {
36
36
  if (
@@ -160,8 +160,8 @@ export class FirstBootSetupService {
160
160
  // First boot creates the default tenant, the platform-admin user, and the
161
161
  // gateway key. The advisory lock is what stops two concurrent boots from
162
162
  // both passing the "no admin yet" check and racing to create them, so a
163
- // handle that cannot take the lock must fail closed — the previous
164
- // `typeof` probe skipped the lock and ran the setup unserialized.
163
+ // handle that cannot take the lock must fail closed — running the setup
164
+ // unserialized lets two boots both pass the check.
165
165
  await this.db.transaction(async (transaction) => {
166
166
  await transaction.execute(firstBootSetupLock());
167
167
  await create(transaction as unknown as CartethyiaDatabase);
@@ -47,10 +47,14 @@ export function createBackupExportHandler(
47
47
  deps: BackupExportDeps,
48
48
  ): (context: { request: Request; body: unknown }) => Promise<Response> {
49
49
  return async ({ request, body }) => {
50
+ // `policy_rejected` / `authentication_failed`, not `invalid_request` for
51
+ // both: neither refusal is about the body. The first is where the request
52
+ // came from, the second is the credential it carried — and `invalid_request`
53
+ // told an operator to fix a body that was never the problem.
50
54
  if (!isLoopbackPeer(deps.resolvePeerAddress(request)))
51
- throw new GatewayError("invalid_request", 403, "backup export is only available from loopback");
55
+ throw new GatewayError("policy_rejected", 403, "backup export is only available from loopback");
52
56
  if (!tokenMatches(request.headers.get("x-drain-token"), deps.token))
53
- throw new GatewayError("invalid_request", 401, "invalid export token");
57
+ throw new GatewayError("authentication_failed", 401, "invalid export token");
54
58
 
55
59
  const directory =
56
60
  body !== null && typeof body === "object" && "directory" in body
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import type { BackupPayload, BackupRow } from "./contracts";
23
23
  import { BACKUP_APP, BACKUP_VERSION } from "./contracts";
24
+ import { isRecord } from "../../protocol/codecs";
24
25
  import { hashSecret } from "../../security/crypto";
25
26
  import {
26
27
  AUTH_STATE_KEYS,
@@ -97,7 +98,7 @@ const OAUTH_PROVIDERS = new Set(["claude", "codex", "antigravity", "kimi", "clin
97
98
 
98
99
  /** The non-secret per-account config, as our `auth_state` bag. */
99
100
  function authStateOf(entry: Row): Record<string, unknown> {
100
- const specific = isRecord(entry.providerSpecificData) ? entry.providerSpecificData : {};
101
+ const specific = isRecord(entry.providerSpecificData) ? entry.providerSpecificData as Row : {};
101
102
  const state: Record<string, unknown> = {};
102
103
  for (const key of AUTH_STATE_KEYS) {
103
104
  const value = specific[key];
@@ -111,10 +112,6 @@ function emailOf(entry: Row): Record<string, unknown> {
111
112
  return email === null ? {} : { account_email: email };
112
113
  }
113
114
 
114
- function isRecord(value: unknown): value is Row {
115
- return typeof value === "object" && value !== null && !Array.isArray(value);
116
- }
117
-
118
115
  /**
119
116
  * Splits `user:pass@host` userinfo out of a proxy URL.
120
117
  *
@@ -374,6 +374,10 @@ export class BackupService {
374
374
  const validation = validateRestorePayload(candidate, "");
375
375
  const warnings = validation.ok ? validation.value.warnings : [];
376
376
  const supported = new Set(validation.ok ? [...validation.value.tables.keys()].map(tableName) : []);
377
+ // `tenants` is diverted to `validation.value.tenantRows` during validate —
378
+ // not included in `tables` — so it would show as "unsupported" in the
379
+ // preview for any backup that carries it. Mark it supported explicitly.
380
+ if (validation.ok) supported.add("tenants");
377
381
 
378
382
  const rows: BackupInspectionRow[] = [];
379
383
  let total = 0;
@@ -283,13 +283,24 @@ export async function exportBackup(
283
283
  const config: Record<string, readonly BackupRow[]> = {};
284
284
  const telemetry: Record<string, readonly BackupRow[]> = {};
285
285
  const counts: Record<string, number> = {};
286
- const configSet = new Set<Table>([...CONFIG_TABLES, TENANT_TABLE]);
286
+ const configSet = new Set<Table>([...CONFIG_TABLES]);
287
287
  const telemetrySet = new Set<Table>(TELEMETRY_TABLES);
288
288
 
289
- for (const table of tables) {
290
- const rows = table === telemetryEvents
291
- ? await exportEventFile(tenantId)
292
- : await exportTable(db, table, tenantId);
289
+ // Table reads are independent tenant-scoped `SELECT`s plus one disk read for
290
+ // the event file; firing them concurrently cuts a many-table export from
291
+ // one round-trip-at-a-time to the single slowest query. The DB driver pools
292
+ // these; the table list is bounded by `CONFIG_TABLES + TELEMETRY_TABLES`
293
+ // (small and known), so fan-out is bounded without an extra limiter.
294
+ const reads = tables.map(async (table) =>
295
+ [
296
+ table,
297
+ table === telemetryEvents ? await exportEventFile(tenantId) : await exportTable(db, table, tenantId),
298
+ ] as const,
299
+ );
300
+ // Preserve the caller's `tables` order when writing into the output maps so
301
+ // the payload's key order is deterministic across runs.
302
+ const settled = await Promise.all(reads);
303
+ for (const [table, rows] of settled) {
293
304
  const name = tableName(table);
294
305
  counts[name] = rows.length;
295
306
  if (configSet.has(table)) config[name] = rows;
@@ -517,12 +528,22 @@ async function clearOwnedRows(
517
528
  }
518
529
 
519
530
  /**
520
- * Inserts rows one at a time so a single bad row names its own failure.
531
+ * Inserts rows in bulk with a per-row fallback for diagnostics.
521
532
  *
522
533
  * `conflictTarget` is supplied for tables a restore may not delete from: the
523
534
  * payload's rows are upserted by the table's unique key, so re-importing a
524
535
  * backup updates what is already there instead of failing on the first
525
536
  * duplicate.
537
+ *
538
+ * Historically rows went in one statement at a time so a single bad row could
539
+ * name its own failure. On a real backup — thousands of telemetry events, a
540
+ * tenant's models and accounts — that cost one DB round-trip per row and the
541
+ * restore stalled on network latency rather than the server's write path. The
542
+ * fast path collapses those statements into one `INSERT ... VALUES (...), ...`
543
+ * per chunk; if a chunk fails for any reason (bad row, constraint, oversized
544
+ * parameter list) the chunk is re-run one row at a time so the diagnostic
545
+ * surface is unchanged. The slow path is only reached on real errors, so a
546
+ * clean restore pays zero penalty for the diagnostic.
526
547
  */
527
548
  async function insertRows(
528
549
  tx: CartethyiaDatabase,
@@ -535,46 +556,171 @@ async function insertRows(
535
556
  readonly ignoreConflicts?: boolean;
536
557
  } = {},
537
558
  ): Promise<number> {
538
- let inserted = 0;
559
+ // Normalise rows up front: filter skips, drop empty-value rows (no
560
+ // insertable columns), and project to Drizzle values. Doing it once keeps
561
+ // the two paths from diverging in what counts as insertable.
562
+ const prepared: Record<string, unknown>[] = [];
539
563
  for (const row of rows) {
540
564
  if (options.skipKeys !== undefined && options.keyOf !== undefined && options.skipKeys.has(options.keyOf(row))) {
541
565
  continue;
542
566
  }
543
567
  const values = toValues(table, row);
544
568
  if (Object.keys(values).length === 0) continue;
569
+ prepared.push(values);
570
+ }
571
+ if (prepared.length === 0) return 0;
572
+
573
+ // The `set` clause for upserts: every column the payload carries is written,
574
+ // so a re-import restores the file's state rather than leaving stale values
575
+ // behind. `excluded` is the row that failed to insert, and column names come
576
+ // from the schema rather than the JS key — guessing the database spelling
577
+ // from a camelCase key breaks the moment the two differ.
578
+ const columnNameMap = new Map(
579
+ Object.entries(getTableColumns(table)).map(([key, column]) => [key, column.name]),
580
+ );
581
+ const buildUpsertSet = (values: Record<string, unknown>): Record<string, unknown> =>
582
+ Object.fromEntries(
583
+ Object.keys(values).map((key) => {
584
+ const name = columnNameMap.get(key);
585
+ if (name === undefined) throw new Error(`${tableName(table)} has no column ${key}`);
586
+ return [key, sql.raw(`excluded."${name}"`)];
587
+ }),
588
+ );
589
+
590
+ // One row's path, kept as its own function so the bulk fallback can reuse it
591
+ // without duplicating the control flow.
592
+ const insertOne = async (values: Record<string, unknown>): Promise<number> => {
545
593
  const statement = insertRow(tx, table, values);
546
594
  if (options.ignoreConflicts) {
547
- if (options.conflictTarget === undefined)
595
+ if (options.conflictTarget === undefined) {
548
596
  throw new Error(`ignoreConflicts requires a target for ${tableName(table)}`);
597
+ }
549
598
  const insertedRows = await statement
550
599
  .onConflictDoNothing({ target: options.conflictTarget as never })
551
600
  .returning();
552
- inserted += insertedRows.length;
553
- continue;
601
+ return insertedRows.length;
554
602
  }
555
603
  if (options.conflictTarget === undefined) {
556
604
  await statement;
557
- } else {
558
- // Every column the payload carries is written, so a re-import restores
559
- // the file's state rather than leaving stale values behind. `excluded`
560
- // is the row that failed to insert, and the column names come from the
561
- // schema rather than from the JS key — guessing the database spelling
562
- // from a camelCase key breaks the moment the two differ.
563
- const names = new Map(
564
- Object.entries(getTableColumns(table)).map(([key, column]) => [key, column.name]),
565
- );
566
- await statement.onConflictDoUpdate({
567
- target: options.conflictTarget as never,
568
- set: Object.fromEntries(
569
- Object.keys(values).map((key) => {
570
- const name = names.get(key);
571
- if (name === undefined) throw new Error(`${tableName(table)} has no column ${key}`);
572
- return [key, sql.raw(`excluded."${name}"`)];
573
- }),
574
- ) as never,
605
+ return 1;
606
+ }
607
+ await statement.onConflictDoUpdate({
608
+ target: options.conflictTarget as never,
609
+ set: buildUpsertSet(values) as never,
610
+ });
611
+ return 1;
612
+ };
613
+
614
+ // Bulk chunk size: keeps each statement's parameter count comfortably under
615
+ // Postgres's 65535 limit even for the widest table. 500 rows × ~60 columns
616
+ // leaves plenty of headroom; a narrower table just means a faster chunk.
617
+ const BULK_CHUNK = 500;
618
+
619
+ // Attempts one chunk in a single statement. Returns the inserted count on
620
+ // success, or `null` to signal "fall back to per-row for this chunk". The
621
+ // chunk is wrapped in a savepoint so a bulk failure can be rolled back
622
+ // *inside* the outer restore transaction — without this, Postgres marks the
623
+ // outer transaction as aborted on the first error and the per-row diagnostic
624
+ // fallback would itself fail with "current transaction is aborted".
625
+ const insertChunk = async (
626
+ chunk: readonly Record<string, unknown>[],
627
+ ): Promise<{ count: number } | { error: unknown }> => {
628
+ try {
629
+ // `tx.transaction(...)` nests as a `SAVEPOINT ... ROLLBACK TO SAVEPOINT`
630
+ // on both Postgres and SQLite drivers Drizzle supports here, which is
631
+ // exactly what the fallback needs: on failure the outer transaction
632
+ // stays writable and the per-row path can isolate the offending row.
633
+ const count = await tx.transaction(async (innerTx) => {
634
+ // Drizzle types the nested transaction callback with the dialect's
635
+ // own transaction handle; cast once to a named local with a reason,
636
+ // never inline on member access.
637
+ const inner = innerTx as unknown as CartethyiaDatabase; // nested-tx handle; mirrors applyRestore's outer `scoped` cast on line 607.
638
+ const statement = inner.insert(table).values(chunk as never);
639
+ if (options.ignoreConflicts) {
640
+ if (options.conflictTarget === undefined) {
641
+ throw new Error(`ignoreConflicts requires a target for ${tableName(table)}`);
642
+ }
643
+ const insertedRows = await statement
644
+ .onConflictDoNothing({ target: options.conflictTarget as never })
645
+ .returning();
646
+ return insertedRows.length;
647
+ }
648
+ if (options.conflictTarget === undefined) {
649
+ await statement;
650
+ return chunk.length;
651
+ }
652
+ // For a bulk upsert `excluded.*` references the row the driver tried
653
+ // to insert, so each column mapping is the same regardless of which
654
+ // row in the chunk conflicts. Build the set clause from the first
655
+ // row's keys — every prepared row for one table carries the same key
656
+ // set because `toValues` is deterministic for a given schema.
657
+ const firstRow = chunk[0];
658
+ if (firstRow === undefined) return 0;
659
+ await statement.onConflictDoUpdate({
660
+ target: options.conflictTarget as never,
661
+ set: buildUpsertSet(firstRow) as never,
662
+ });
663
+ return chunk.length;
575
664
  });
665
+ return { count };
666
+ } catch (error) {
667
+ // Signal "re-run row by row": a bulk failure must not swallow the
668
+ // specific row's error, because the per-row path is what names the
669
+ // offending row to the operator. Keep the bulk error too: when the
670
+ // per-row fallback also fails (connection drop, aborted transaction,
671
+ // oversized statement), the derived row error would otherwise hide the
672
+ // real cause from the operator.
673
+ return { error };
576
674
  }
577
- inserted += 1;
675
+ };
676
+
677
+ let inserted = 0;
678
+ // Group adjacent rows that share the same column key set. Different rows in
679
+ // one table can carry different keys (an optional column omitted when the
680
+ // payload cell is absent; `parent_key_id` present on child api_keys but not
681
+ // parents), and Drizzle's bulk `.values([...])` compiles one SQL statement
682
+ // with a column list taken from the first row — so a bulk of mixed shapes
683
+ // produces malformed SQL. Grouping by signature keeps each bulk statement
684
+ // uniform while preserving row order within the group.
685
+ //
686
+ // Signature is a stable `|`-joined list of sorted keys; empty columns were
687
+ // already dropped by the `toValues` → `Object.keys(values).length === 0`
688
+ // check above, so empty signatures never occur here.
689
+ const signatureOf = (values: Record<string, unknown>): string =>
690
+ Object.keys(values).sort().join("|");
691
+ let groupStart = 0;
692
+ while (groupStart < prepared.length) {
693
+ const groupSignature = signatureOf(prepared[groupStart]!);
694
+ let groupEnd = groupStart + 1;
695
+ while (
696
+ groupEnd < prepared.length &&
697
+ signatureOf(prepared[groupEnd]!) === groupSignature &&
698
+ groupEnd - groupStart < BULK_CHUNK
699
+ ) {
700
+ groupEnd += 1;
701
+ }
702
+ const chunk = prepared.slice(groupStart, groupEnd);
703
+ const bulk = await insertChunk(chunk);
704
+ if ("count" in bulk) {
705
+ inserted += bulk.count;
706
+ } else {
707
+ // Diagnostic fallback: let the single statement that actually errors
708
+ // reach the caller with the row that caused it. If the fallback itself
709
+ // fails, the row-level error is derived — chain the original bulk error
710
+ // so a connection-level fault is not reported as a bad row.
711
+ try {
712
+ for (const values of chunk) {
713
+ inserted += await insertOne(values);
714
+ }
715
+ } catch (perRowError) {
716
+ const bulkMessage = bulk.error instanceof Error ? bulk.error.message : String(bulk.error);
717
+ throw new Error(
718
+ `bulk insert into ${tableName(table)} failed (${bulkMessage}); per-row diagnostic also failed`,
719
+ { cause: perRowError },
720
+ );
721
+ }
722
+ }
723
+ groupStart = groupEnd;
578
724
  }
579
725
  return inserted;
580
726
  }
@@ -610,6 +756,14 @@ export async function applyRestore(
610
756
  // its PK is `id`, so a conflict means "already there" rather than an error.
611
757
  // The payload's tenant is forced to the restoring tenant — a payload naming
612
758
  // someone else's tenant id must not create or claim that tenant.
759
+ //
760
+ // Current exports deliberately omit the `tenants` section, so
761
+ // `tenantRows` is normally empty. Child rows still carry
762
+ // `tenant_id = tenantId`, which means a restore into a tenant that does
763
+ // not exist yet would fail the `api_keys` (and other) foreign keys unless
764
+ // we ensure the receiving tenant here. The minimal placeholder row is
765
+ // written only for a tenant that did not already exist; on conflict it is
766
+ // a no-op and the existing tenant's name/status are left untouched.
613
767
  if (validation.tenantRows.length > 0) {
614
768
  let inserted = 0;
615
769
  for (const row of validation.tenantRows) {
@@ -621,6 +775,15 @@ export async function applyRestore(
621
775
  inserted += result.length;
622
776
  }
623
777
  restored["tenants"] = inserted;
778
+ } else {
779
+ const result = await insertRow(scoped, TENANT_TABLE, {
780
+ id: tenantId,
781
+ name: "restored",
782
+ status: "active",
783
+ })
784
+ .onConflictDoNothing()
785
+ .returning({ id: sql<number>`1` });
786
+ restored["tenants"] = result.length;
624
787
  }
625
788
 
626
789
  const replaceTables = order.filter(
@@ -36,18 +36,13 @@ import {
36
36
  type ValidatedRestore,
37
37
  type ValidatedTable,
38
38
  } from "./contracts";
39
- import { DEFAULT_API_KEY_LABEL } from "../api-keys/contracts";
40
39
  import {
41
- apiKeys,
42
40
  modelCombos,
43
41
  networkPools,
44
42
  providerRoutingSettings,
45
43
  shareLinks,
46
44
  } from "../../persistence/schema";
47
45
 
48
- const LEGACY_PROVIDER_USER_AGENT = "codex_cli_rs/0.156.1";
49
- const CURRENT_PROVIDER_USER_AGENT = "claude-cli/2.1.293 (external, cli)";
50
-
51
46
  /**
52
47
  * Provider ids that were renamed, old spelling to current.
53
48
  *
@@ -68,51 +63,14 @@ function currentProviderId(value: unknown): unknown {
68
63
  return RENAMED_PROVIDER_IDS[value] ?? value;
69
64
  }
70
65
 
71
- function legacyModelList(value: unknown): readonly string[] | undefined {
72
- if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) return undefined;
73
- return value;
74
- }
75
-
76
- function normalizeLegacyApiKeyPolicy(row: Record<string, unknown>): Record<string, unknown> {
77
- const allowlist = legacyModelList(row["model_allowlist"]);
78
- const denylist = legacyModelList(row["model_denylist"]);
79
- if (row["model_access_mode"] !== undefined || row["model_list"] !== undefined) return row;
80
- if (denylist !== undefined && denylist.length > 0) {
81
- return { ...row, model_access_mode: "blacklist", model_list: denylist };
82
- }
83
- if (allowlist !== undefined) {
84
- return { ...row, model_access_mode: "whitelist", model_list: allowlist };
85
- }
86
- return row;
87
- }
88
-
89
66
  function normalizeLegacyRow(table: Table, row: Record<string, unknown>): Record<string, unknown> {
90
67
  if (row["provider_id"] !== undefined) {
91
68
  const renamed = currentProviderId(row["provider_id"]);
92
69
  if (renamed !== row["provider_id"]) row = { ...row, provider_id: renamed };
93
70
  }
94
- if (
95
- table === apiKeys &&
96
- row["is_default"] === undefined &&
97
- (row["key_mode"] === undefined || row["key_mode"] === "personal") &&
98
- row["parent_key_id"] == null &&
99
- row["label"] === DEFAULT_API_KEY_LABEL
100
- ) {
101
- return { ...row, is_default: true };
102
- }
103
71
  if (table === providerRoutingSettings) {
104
- const { bypass_proxy: legacySkipProxy, ...current } = row;
105
- let normalized = current;
106
- if (normalized["skip_proxy"] === undefined && legacySkipProxy !== undefined) {
107
- normalized = { ...normalized, skip_proxy: legacySkipProxy };
108
- }
109
- if (normalized["provider_id"] === "cline") {
110
- normalized = { ...normalized, skip_proxy: true };
111
- }
112
- if (normalized["user_agent"] === LEGACY_PROVIDER_USER_AGENT) {
113
- normalized = { ...normalized, user_agent: CURRENT_PROVIDER_USER_AGENT };
114
- }
115
- return normalized;
72
+ if (row["provider_id"] === "cline") return { ...row, skip_proxy: true };
73
+ return row;
116
74
  }
117
75
  if (table === networkPools && row["kind"] === "bridge") {
118
76
  return { ...row, kind: "https" };
@@ -342,7 +300,7 @@ export function validateRestorePayload(payload: unknown, tenantId: string): Rest
342
300
  // the current single policy before dropping retired column names.
343
301
  const sourceRow = normalizeLegacyRow(
344
302
  table,
345
- table === apiKeys ? normalizeLegacyApiKeyPolicy(row) : row,
303
+ row,
346
304
  );
347
305
  // A stale backup may still carry columns the schema has since dropped.
348
306
  // Those are accepted but removed here, so they never reach the store