cartethyia 2.5.1 → 2.5.3

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 (235) hide show
  1. package/.env.example +5 -1
  2. package/dist/dashboard/assets/{AccountCooldown-ChpdzxRH.js → AccountCooldown-Bjo92Qat.js} +1 -1
  3. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
  4. package/dist/dashboard/assets/{BannedPage-ChqnTRpZ.js → BannedPage-hfCzi-Ux.js} +1 -1
  5. package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +1 -0
  7. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
  8. package/dist/dashboard/assets/{CliToolsPage-BXUaRCQQ.js → CliToolsPage-DEDyKBlC.js} +1 -1
  9. package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
  10. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js +1 -0
  11. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js +1 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
  14. package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js +1 -0
  15. package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
  16. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +1 -0
  17. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
  18. package/dist/dashboard/assets/LoginPage-DDVIfZGW.js +1 -0
  19. package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
  20. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js +1 -0
  21. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
  22. package/dist/dashboard/assets/OverviewPage-B53YYMng.js +1 -0
  23. package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +3 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
  26. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +1 -0
  27. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
  28. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +7 -0
  29. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
  30. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +1 -0
  31. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
  32. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +1 -0
  33. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
  34. package/dist/dashboard/assets/SetupPage-D6pmQ86L.js +1 -0
  35. package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
  36. package/dist/dashboard/assets/SortableList-DWvKiuSl.js +1 -0
  37. package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-DqK5E26W.js → StudioPage-CmkUrgdj.js} +3 -3
  39. package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
  40. package/dist/dashboard/assets/UsagePage-B47uOydr.js +56 -0
  41. package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BmDbsior.js → api-keys-uNywYlfv.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
  44. package/dist/dashboard/assets/{app-C-Y4oZT0.js → app-CafWtVXA.js} +1 -1
  45. package/dist/dashboard/assets/{app-C-Y4oZT0.js.gz → app-CafWtVXA.js.gz} +0 -0
  46. package/dist/dashboard/assets/app-DxivpfYG.css +2 -0
  47. package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
  48. package/dist/dashboard/assets/{assert-BHfaei-X.js → assert-NHwYq95t.js} +1 -1
  49. package/dist/dashboard/assets/{assert-BHfaei-X.js.gz → assert-NHwYq95t.js.gz} +0 -0
  50. package/dist/dashboard/assets/badge-6uzDDvqs.js +1 -0
  51. package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
  52. package/dist/dashboard/assets/banner-4_WNg2WZ.js +1 -0
  53. package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
  54. package/dist/dashboard/assets/card--AtFedAq.js +1 -0
  55. package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
  56. package/dist/dashboard/assets/{cli-tools-B0wBq-RL.js → cli-tools-B3e7dx3N.js} +1 -1
  57. package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
  58. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js +1 -0
  59. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
  60. package/dist/dashboard/assets/dialog-BtpsJU98.js +1 -0
  61. package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
  62. package/dist/dashboard/assets/drawer-WwwtzihF.js +1 -0
  63. package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
  64. package/dist/dashboard/assets/entry-CkryrIea.js +4 -0
  65. package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-TJIye_jT.js +1 -0
  67. package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
  68. package/dist/dashboard/assets/{entry-D4O2229X.js → entry-jy9_4-Ke.js} +1 -1
  69. package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
  70. package/dist/dashboard/assets/github-badge-D_eM3IgG.js +1 -0
  71. package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
  72. package/dist/dashboard/assets/{index-CcD1eSJ6.js → index-BydyKaDz.js} +2 -2
  73. package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
  74. package/dist/dashboard/assets/inline-BcNGF8Gr.js +1 -0
  75. package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
  76. package/dist/dashboard/assets/input-BG41et9G.js +1 -0
  77. package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
  78. package/dist/dashboard/assets/layout-C8mer_Ak.js +1 -0
  79. package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
  80. package/dist/dashboard/assets/list-row-Gh_W4OR_.js +1 -0
  81. package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
  82. package/dist/dashboard/assets/{live-yfEJFF9T.js → live-BJGYRBnR.js} +1 -1
  83. package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
  84. package/dist/dashboard/assets/{model-bans-CT-TxSNX.js → model-bans-DeVLDA7I.js} +1 -1
  85. package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
  86. package/dist/dashboard/assets/numbers-Bs4LOOuf.js +2 -0
  87. package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
  88. package/dist/dashboard/assets/{routing-Cs6jl49L.js → routing-CcusqXA7.js} +1 -1
  89. package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
  90. package/dist/dashboard/assets/select-BO6h8yNv.js +41 -0
  91. package/dist/dashboard/assets/select-BO6h8yNv.js.gz +0 -0
  92. package/dist/dashboard/assets/{settings-CfcTcOyU.js → settings-mpmXTgUa.js} +1 -1
  93. package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
  94. package/dist/dashboard/assets/stack-DwLmjUFK.js +1 -0
  95. package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
  96. package/dist/dashboard/assets/state-DYdQmdAl.js +1 -0
  97. package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
  98. package/dist/dashboard/assets/switch-CidSfbG7.js +1 -0
  99. package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
  100. package/dist/dashboard/assets/{useMutation-BQKMcJ1H.js → useMutation-w4u4CVue.js} +1 -1
  101. package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
  102. package/dist/dashboard/index.html +1 -1
  103. package/dist/dashboard/index.html.gz +0 -0
  104. package/dist/dashboard/precompressed-assets.json +51 -54
  105. package/package.json +1 -1
  106. package/scripts/cli/command/console-menu.ts +248 -246
  107. package/scripts/cli/command/tray.ts +208 -204
  108. package/src/boot/app.ts +3 -2
  109. package/src/config/spec.ts +26 -2
  110. package/src/console/backup/routes.ts +52 -1
  111. package/src/console/backup/service.ts +60 -4
  112. package/src/console/observability/contracts.ts +11 -1
  113. package/src/console/observability/store.ts +9 -6
  114. package/src/console/share/share-router.ts +6 -9
  115. package/src/observability/payload-capture.ts +366 -344
  116. package/src/observability/payload-store.ts +12 -2
  117. package/src/observability/telemetry-status.ts +28 -16
  118. package/src/persistence/stores/api-key-store.ts +6 -0
  119. package/src/persistence/stores/share-store.ts +583 -556
  120. package/src/persistence/stores/telemetry-store.ts +1 -1
  121. package/src/protocol/response/gemini.ts +18 -4
  122. package/src/providers/discovery/openrouter-free-roster.ts +3 -11
  123. package/src/providers/discovery/probe-phases.ts +42 -5
  124. package/src/providers/discovery/probing-service.ts +25 -2
  125. package/src/providers/integrations/antigravity/antigravity-quota.ts +11 -3
  126. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +4 -4
  127. package/src/providers/integrations/buddy/workbuddy/shared.ts +201 -200
  128. package/src/providers/integrations/claude/claude.ts +3 -1
  129. package/src/providers/integrations/cline/cline-quota.ts +8 -11
  130. package/src/providers/integrations/cloudflareai/cloudflareai-models.ts +3 -1
  131. package/src/providers/integrations/inferhub.ts +3 -1
  132. package/src/providers/integrations/kilo/kilo.ts +0 -1
  133. package/src/providers/integrations/kiro/kiro-catalog.ts +8 -2
  134. package/src/providers/integrations/opencode/provider.ts +532 -532
  135. package/src/providers/integrations/openrouter/provider.ts +5 -15
  136. package/src/providers/integrations/tokenharbor.ts +2 -2
  137. package/src/providers/integrations/trae/trae-catalog.ts +5 -3
  138. package/src/providers/operations/account-health-service.ts +23 -0
  139. package/src/providers/operations/client-versions.ts +9 -2
  140. package/src/providers/provider-metadata.ts +7 -1
  141. package/src/security/api-key-auth.ts +23 -0
  142. package/src/transport/dispatch/attempt-finalize.ts +46 -5
  143. package/src/transport/dispatch/proxy-request.ts +9 -0
  144. package/src/transport/dispatch/streaming/stream.ts +16 -2
  145. package/src/transport/guards/auth-guard.ts +2 -3
  146. package/src/transport/routing/route-catalog.ts +806 -799
  147. package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
  148. package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js.gz +0 -0
  149. package/dist/dashboard/assets/CliToolDetailPage-Bb9CNuoh.js +0 -1
  150. package/dist/dashboard/assets/CliToolDetailPage-Bb9CNuoh.js.gz +0 -0
  151. package/dist/dashboard/assets/CliToolsPage-BXUaRCQQ.js.gz +0 -0
  152. package/dist/dashboard/assets/CombosPage-CisNZOUt.js +0 -1
  153. package/dist/dashboard/assets/CombosPage-CisNZOUt.js.gz +0 -0
  154. package/dist/dashboard/assets/ConsoleLogPage-BOpI_LOH.js +0 -1
  155. package/dist/dashboard/assets/ConsoleLogPage-BOpI_LOH.js.gz +0 -0
  156. package/dist/dashboard/assets/CustomizationPage-DAkdvJ4k.js +0 -1
  157. package/dist/dashboard/assets/CustomizationPage-DAkdvJ4k.js.gz +0 -0
  158. package/dist/dashboard/assets/HealthEventsModal-DeT-VXHd.js +0 -1
  159. package/dist/dashboard/assets/HealthEventsModal-DeT-VXHd.js.gz +0 -0
  160. package/dist/dashboard/assets/LoginPage-CytxSeGP.js +0 -1
  161. package/dist/dashboard/assets/LoginPage-CytxSeGP.js.gz +0 -0
  162. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +0 -1
  163. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  164. package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js +0 -1
  165. package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js.gz +0 -0
  166. package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js +0 -3
  167. package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js.gz +0 -0
  168. package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js +0 -1
  169. package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js.gz +0 -0
  170. package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js +0 -7
  171. package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js.gz +0 -0
  172. package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js +0 -1
  173. package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js.gz +0 -0
  174. package/dist/dashboard/assets/SettingsPage-8-eVadhc.js +0 -1
  175. package/dist/dashboard/assets/SettingsPage-8-eVadhc.js.gz +0 -0
  176. package/dist/dashboard/assets/SetupPage-uEIXPyGO.js +0 -1
  177. package/dist/dashboard/assets/SetupPage-uEIXPyGO.js.gz +0 -0
  178. package/dist/dashboard/assets/SortableList-DCY0TPTZ.js +0 -1
  179. package/dist/dashboard/assets/SortableList-DCY0TPTZ.js.gz +0 -0
  180. package/dist/dashboard/assets/StudioPage-DqK5E26W.js.gz +0 -0
  181. package/dist/dashboard/assets/UsagePage-38JfiZ0H.js +0 -56
  182. package/dist/dashboard/assets/UsagePage-38JfiZ0H.js.gz +0 -0
  183. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  184. package/dist/dashboard/assets/app-BbpSarVV.css +0 -2
  185. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  186. package/dist/dashboard/assets/badge-DpXT4Ixt.js +0 -1
  187. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  188. package/dist/dashboard/assets/banner-CSAAWLJv.js +0 -1
  189. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  190. package/dist/dashboard/assets/card-DEhRXXw4.js +0 -1
  191. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  192. package/dist/dashboard/assets/check-D69FzdvC.js +0 -1
  193. package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
  194. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  195. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +0 -1
  196. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  197. package/dist/dashboard/assets/copy-ngmQ9zcZ.js +0 -1
  198. package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
  199. package/dist/dashboard/assets/dialog-BvQeBMfP.js +0 -1
  200. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  201. package/dist/dashboard/assets/drawer-CMwPXwQB.js +0 -1
  202. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  203. package/dist/dashboard/assets/entry--yj84zk3.js +0 -4
  204. package/dist/dashboard/assets/entry--yj84zk3.js.gz +0 -0
  205. package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
  206. package/dist/dashboard/assets/entry-DKvr5Q9K.js +0 -1
  207. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  208. package/dist/dashboard/assets/github-badge-zliLvBwr.js +0 -1
  209. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  210. package/dist/dashboard/assets/index-CcD1eSJ6.js.gz +0 -0
  211. package/dist/dashboard/assets/inline-D1IlZZ8H.js +0 -1
  212. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  213. package/dist/dashboard/assets/input-BpVZdrbc.js +0 -1
  214. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  215. package/dist/dashboard/assets/layout-BioLW7gN.js +0 -1
  216. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  217. package/dist/dashboard/assets/list-row-CwVcGAIc.js +0 -1
  218. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  219. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  220. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  221. package/dist/dashboard/assets/numbers-CFL4_Dqc.js +0 -1
  222. package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
  223. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  224. package/dist/dashboard/assets/select-XPJ-dIw3.js +0 -41
  225. package/dist/dashboard/assets/select-XPJ-dIw3.js.gz +0 -0
  226. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  227. package/dist/dashboard/assets/stack-C9_uUjaA.js +0 -1
  228. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  229. package/dist/dashboard/assets/state-DA0B95UQ.js +0 -1
  230. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  231. package/dist/dashboard/assets/switch-Cittxy-x.js +0 -1
  232. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  233. package/dist/dashboard/assets/toast-CNt_jzO4.js +0 -2
  234. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  235. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
@@ -19,6 +19,7 @@
19
19
  * re-authentication.
20
20
  */
21
21
  import type { CartethyiaDatabase } from "../../persistence/db-handle";
22
+ import type { Table } from "drizzle-orm";
22
23
  import {
23
24
  models,
24
25
  providerAccounts,
@@ -29,7 +30,12 @@ import {
29
30
  } from "../../persistence/schema";
30
31
  import { ConsoleDomainError } from "../shared/errors";
31
32
  import { tablesForSection, tableName } from "./contracts";
32
- import type { BackupSection, RestoreWarning, ValidatedRestore } from "./contracts";
33
+ import type {
34
+ BackupSection,
35
+ RestoreWarning,
36
+ ValidatedRestore,
37
+ ValidatedTable,
38
+ } from "./contracts";
33
39
  import {
34
40
  applyRestore,
35
41
  decompressBackup,
@@ -90,6 +96,53 @@ export interface ImportResult {
90
96
  readonly format: "native" | "nine_router";
91
97
  }
92
98
 
99
+ /**
100
+ * Which parts of a backup file a restore should actually write.
101
+ *
102
+ * Absent or empty means "everything", which is the historical behavior and
103
+ * what the dashboard's checklist defaults to. A table named here but absent
104
+ * from the file is simply not restored — it is not an error, because the
105
+ * checklist is built from an inspection of *this* file and a selection never
106
+ * invents a table.
107
+ */
108
+ export interface BackupRestoreSelection {
109
+ /** Table names to restore; absent or empty restores every table present. */
110
+ readonly tables?: readonly string[];
111
+ }
112
+
113
+ /**
114
+ * Narrows a validated backup to the tables the operator selected.
115
+ *
116
+ * The tenant row is the one thing that is not independently selectable: it is
117
+ * not a table in either section (`BACKUP_SECTIONS` are config and telemetry),
118
+ * so it never appears on the checklist, and every tenant-scoped table
119
+ * cascades from it. Dropping it because it was not named would make a partial
120
+ * restore fail on a foreign key for a table the operator *did* ask for. So it
121
+ * rides along with any config selection and is written only when one is made.
122
+ *
123
+ * A table named in the selection but missing from the file is a no-op rather
124
+ * than an error: the checklist is built from an inspection of this file, but
125
+ * the selection is validated as names only, and refusing to restore anything
126
+ * over one stale name would be worse than ignoring it.
127
+ */
128
+ function restrictToSelection(
129
+ validation: ValidatedRestore,
130
+ selection: BackupRestoreSelection | undefined,
131
+ ): ValidatedRestore {
132
+ const wanted = selection?.tables;
133
+ if (wanted === undefined || wanted.length === 0) return validation;
134
+ const selected = new Set(wanted);
135
+ const tables = new Map<Table, ValidatedTable>();
136
+ for (const [table, entry] of validation.tables) {
137
+ if (selected.has(tableName(table))) tables.set(table, entry);
138
+ }
139
+ return {
140
+ ...validation,
141
+ tables,
142
+ tenantRows: tables.size > 0 ? validation.tenantRows : [],
143
+ };
144
+ }
145
+
93
146
  function sectionsOrDefault(sections: readonly BackupSection[] | undefined): readonly BackupSection[] {
94
147
  if (sections === undefined || sections.length === 0) return ["config"];
95
148
  for (const section of sections) {
@@ -246,6 +299,7 @@ export class BackupService {
246
299
  password: unknown,
247
300
  payload: unknown,
248
301
  tenantId: string,
302
+ selection?: BackupRestoreSelection,
249
303
  ): Promise<ImportResult> {
250
304
  if (typeof password !== "string" || !(await this.options.verifyPassword(password))) {
251
305
  throw new ConsoleDomainError("unauthorized", 401, "password is incorrect");
@@ -258,11 +312,11 @@ export class BackupService {
258
312
 
259
313
  if (detected.kind === "nine_router") {
260
314
  const { payload: converted, report } = convert9RouterBackup(detected.payload, tenantId);
261
- const result = await this.apply(converted, tenantId);
315
+ const result = await this.apply(converted, tenantId, selection);
262
316
  return { ...result, report, format: "nine_router" };
263
317
  }
264
318
 
265
- const result = await this.apply(detected.payload, tenantId);
319
+ const result = await this.apply(detected.payload, tenantId, selection);
266
320
  return { ...result, format: "native" };
267
321
  }
268
322
 
@@ -348,13 +402,15 @@ export class BackupService {
348
402
  private async apply(
349
403
  payload: unknown,
350
404
  tenantId: string,
405
+ selection?: BackupRestoreSelection,
351
406
  ): Promise<{ restored: Record<string, number>; skipped: Record<string, number>; warnings: readonly RestoreWarning[] }> {
352
407
  const validation = validateRestorePayload(payload, tenantId);
353
408
  if (!validation.ok) {
354
409
  throw new ConsoleDomainError("invalid_request", 400, validation.error);
355
410
  }
356
411
  try {
357
- const compatible = await omitRowsForUnavailableProviders(this.options.db, validation.value, tenantId);
412
+ const scoped = restrictToSelection(validation.value, selection);
413
+ const compatible = await omitRowsForUnavailableProviders(this.options.db, scoped, tenantId);
358
414
  const applied = await applyRestore(this.options.db, compatible, restoreOrder(), tenantId);
359
415
  return { ...applied, warnings: compatible.warnings };
360
416
  } catch (error) {
@@ -45,7 +45,17 @@ export interface SystemHealthResponse {
45
45
  * gateway itself grew.
46
46
  */
47
47
  database_in_process: boolean;
48
- cpu_percent: number;
48
+ /**
49
+ * `null` when the platform does not report it — there is no `loadavg` on
50
+ * Windows and Node hands back a zeroed tuple rather than failing, so on
51
+ * Windows this is always `null` rather than a confident-looking `0`.
52
+ *
53
+ * Nullable rather than `NaN`: `NaN` is not a JSON value and serializes to
54
+ * `null` anyway, which means a client validating the field as a number
55
+ * rejects the *whole* health payload. One unavailable metric then reads as
56
+ * every metric being unavailable.
57
+ */
58
+ cpu_percent: number | null;
49
59
  cpu_cores: number;
50
60
  pid: number;
51
61
  platform: string;
@@ -330,12 +330,15 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
330
330
  // syscall and Node reports a zeroed tuple rather than failing. Divided by
331
331
  // cores that is a confident-looking 0.0% on a busy gateway, which is worse
332
332
  // than saying nothing: an operator reads it as "idle" and stops looking.
333
- // Reported as unavailable instead of as a measurement.
334
- const load = loadavg()[0] ?? 0;
335
- const cpuPercent =
336
- process.platform === "win32" || load === 0
337
- ? Number.NaN
338
- : Math.round((load / Math.max(cpus().length, 1)) * 10000) / 100;
333
+ // Reported as null instead of as a measurement.
334
+ //
335
+ // A real zero load is *not* the same thing and is reported as zero: an
336
+ // idle machine genuinely has nothing queued, and collapsing that into
337
+ // "unavailable" would hide a number the platform did supply.
338
+ const cpuPercent: number | null =
339
+ process.platform === "win32"
340
+ ? null
341
+ : Math.round(((loadavg()[0] ?? 0) / Math.max(cpus().length, 1)) * 10000) / 100;
339
342
 
340
343
  let databaseHealthy = true;
341
344
  let total = 0;
@@ -23,7 +23,6 @@ import type { ShareFamilyStats, ShareStatsPort } from "./share-stats";
23
23
  import { consoleSseResponse, createConsoleSseStream } from "../observability/sse";
24
24
  import { GatewayError, publicGatewayErrorBody } from "../../protocol/gateway-error";
25
25
  import {
26
- hashShareToken,
27
26
  type ShareLinkPolicy,
28
27
  type ShareLinkRefusal,
29
28
  type ShareLinkStore,
@@ -224,7 +223,7 @@ async function resolveFamilyStats(
224
223
  stats: ShareStatsPort,
225
224
  token: string,
226
225
  ): Promise<ShareFamilyStats | null> {
227
- const resolved = await shareStore.resolveShareLink(hashShareToken(token));
226
+ const resolved = await shareStore.resolveShareLink(token);
228
227
  if (resolved === null) return null;
229
228
  const templateId = resolved.key.id;
230
229
  const tenantId = resolved.key.tenantId;
@@ -267,8 +266,7 @@ export function createShareRouter(options: ShareRouterOptions): Elysia {
267
266
  if (typeof params.token !== "string") return notFound();
268
267
  const token = params.token;
269
268
  if (token.length < MIN_TOKEN_LENGTH) return notFound();
270
- const tokenHash = hashShareToken(token);
271
- const outcome = await shareStore.resolveShareLinkOutcome(tokenHash);
269
+ const outcome = await shareStore.resolveShareLinkOutcome(token);
272
270
  if (!outcome.ok) return notFound(outcome.refusal);
273
271
  const resolved = outcome.resolution;
274
272
  const row = resolved.key;
@@ -276,7 +274,7 @@ export function createShareRouter(options: ShareRouterOptions): Elysia {
276
274
  const clientIpKey = clientIp === null ? undefined : canonicalClientIpKey(clientIp);
277
275
  const models = await modelsForShare(db, row);
278
276
  const modelInfo = await modelInfoForShare(db, row, models);
279
- void shareStore.touchView(tokenHash).catch(() => undefined);
277
+ void shareStore.touchView(token).catch(() => undefined);
280
278
  const policy = {
281
279
  name: row.name,
282
280
  keyPrefix: row.keyPrefix,
@@ -330,7 +328,7 @@ export function createShareRouter(options: ShareRouterOptions): Elysia {
330
328
  if (typeof params.token !== "string") return notFound();
331
329
  const token = params.token;
332
330
  if (token.length < MIN_TOKEN_LENGTH) return notFound();
333
- const resolved = await shareStore.resolveShareLink(hashShareToken(token));
331
+ const resolved = await shareStore.resolveShareLink(token);
334
332
  const image = resolved?.key.sharePopupImage;
335
333
  const mime = resolved?.key.sharePopupImageMime;
336
334
  if (!image || !mime) return notFound();
@@ -355,8 +353,7 @@ export function createShareRouter(options: ShareRouterOptions): Elysia {
355
353
  if (clientIpKey === undefined)
356
354
  return json({ error: { code: "client_ip_invalid", message: "Client IP could not be normalized" } }, 400);
357
355
 
358
- const tokenHash = hashShareToken(token);
359
- const outcome = await shareStore.resolveShareLinkOutcome(tokenHash);
356
+ const outcome = await shareStore.resolveShareLinkOutcome(token);
360
357
  // A handoff link reveals an existing key; it never mints one.
361
358
  if (!outcome.ok) return notFound(outcome.refusal);
362
359
  const resolved = outcome.resolution;
@@ -374,7 +371,7 @@ export function createShareRouter(options: ShareRouterOptions): Elysia {
374
371
  }
375
372
  const template = resolved.key;
376
373
  const generated = generateApiKeySecret(resolveShareKeyPrefix(template.keyPrefix ?? undefined));
377
- const issued = await shareStore.issueSharedApiKey(tokenHash, {
374
+ const issued = await shareStore.issueSharedApiKey(token, {
378
375
  keyHash: generated.hash,
379
376
  keyPrefix: generated.prefix,
380
377
  clientIp,