@camstack/system 1.0.2

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 (262) hide show
  1. package/dist/addon/addon-api-factory.d.ts +35 -0
  2. package/dist/addon-routes/addon-route-registry.d.ts +37 -0
  3. package/dist/addon-runner.js +599 -0
  4. package/dist/addon-runner.mjs +597 -0
  5. package/dist/auth/api-key-manager.d.ts +26 -0
  6. package/dist/auth/auth-manager.d.ts +109 -0
  7. package/dist/auth/parse-record.d.ts +18 -0
  8. package/dist/auth/scope-matcher.d.ts +7 -0
  9. package/dist/auth/scoped-token-manager.d.ts +40 -0
  10. package/dist/auth/totp-manager.d.ts +51 -0
  11. package/dist/auth/user-manager.d.ts +34 -0
  12. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.d.ts +53 -0
  13. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +259 -0
  14. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +251 -0
  15. package/dist/builtins/addon-pages-aggregator/dedupe-pages.d.ts +6 -0
  16. package/dist/builtins/addon-pages-aggregator/index.d.ts +1 -0
  17. package/dist/builtins/addon-pages-aggregator/index.js +8 -0
  18. package/dist/builtins/addon-pages-aggregator/index.mjs +2 -0
  19. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.d.ts +47 -0
  20. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +228 -0
  21. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +220 -0
  22. package/dist/builtins/addon-widgets-aggregator/index.d.ts +1 -0
  23. package/dist/builtins/addon-widgets-aggregator/index.js +8 -0
  24. package/dist/builtins/addon-widgets-aggregator/index.mjs +2 -0
  25. package/dist/builtins/alerts/alerts.addon.d.ts +81 -0
  26. package/dist/builtins/alerts/alerts.addon.js +601 -0
  27. package/dist/builtins/alerts/alerts.addon.mjs +595 -0
  28. package/dist/builtins/alerts/index.d.ts +1 -0
  29. package/dist/builtins/alerts/index.js +4 -0
  30. package/dist/builtins/alerts/index.mjs +2 -0
  31. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +147 -0
  32. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2229 -0
  33. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2220 -0
  34. package/dist/builtins/backup-orchestrator/cron-helpers.d.ts +23 -0
  35. package/dist/builtins/backup-orchestrator/destination-policy.d.ts +72 -0
  36. package/dist/builtins/backup-orchestrator/download-helpers.d.ts +12 -0
  37. package/dist/builtins/backup-orchestrator/index.d.ts +2 -0
  38. package/dist/builtins/backup-orchestrator/index.js +8 -0
  39. package/dist/builtins/backup-orchestrator/index.mjs +2 -0
  40. package/dist/builtins/backup-orchestrator/manifest-store.d.ts +77 -0
  41. package/dist/builtins/console-logging/console-destination.d.ts +13 -0
  42. package/dist/builtins/console-logging/console-logging.addon.d.ts +25 -0
  43. package/dist/builtins/console-logging/index.d.ts +3 -0
  44. package/dist/builtins/console-logging/index.js +104 -0
  45. package/dist/builtins/console-logging/index.mjs +95 -0
  46. package/dist/builtins/device-manager/device-config-contribution.d.ts +32 -0
  47. package/dist/builtins/device-manager/device-event-propagator.d.ts +26 -0
  48. package/dist/builtins/device-manager/device-link-overlay.d.ts +23 -0
  49. package/dist/builtins/device-manager/device-link-resolver.d.ts +15 -0
  50. package/dist/builtins/device-manager/device-manager.addon.d.ts +452 -0
  51. package/dist/builtins/device-manager/device-manager.addon.js +3299 -0
  52. package/dist/builtins/device-manager/device-manager.addon.mjs +3292 -0
  53. package/dist/builtins/device-manager/index.d.ts +2 -0
  54. package/dist/builtins/device-manager/index.js +8 -0
  55. package/dist/builtins/device-manager/index.mjs +2 -0
  56. package/dist/builtins/hub-forwarder/hub-forwarder-destination.d.ts +44 -0
  57. package/dist/builtins/hub-forwarder/hub-forwarder.addon.d.ts +15 -0
  58. package/dist/builtins/hub-forwarder/index.d.ts +3 -0
  59. package/dist/builtins/hub-forwarder/index.js +154 -0
  60. package/dist/builtins/hub-forwarder/index.mjs +145 -0
  61. package/dist/builtins/local-auth/auth-schema.d.ts +26 -0
  62. package/dist/builtins/local-auth/index.d.ts +1 -0
  63. package/dist/builtins/local-auth/index.js +4 -0
  64. package/dist/builtins/local-auth/index.mjs +2 -0
  65. package/dist/builtins/local-auth/local-auth.addon.d.ts +18 -0
  66. package/dist/builtins/local-auth/local-auth.addon.js +8094 -0
  67. package/dist/builtins/local-auth/local-auth.addon.mjs +8063 -0
  68. package/dist/builtins/local-auth/oauth-grants.d.ts +45 -0
  69. package/dist/builtins/local-auth/oauth-session-manager.d.ts +50 -0
  70. package/dist/builtins/local-network/index.d.ts +2 -0
  71. package/dist/builtins/local-network/index.js +10 -0
  72. package/dist/builtins/local-network/index.mjs +2 -0
  73. package/dist/builtins/local-network/local-network.addon.d.ts +150 -0
  74. package/dist/builtins/local-network/local-network.addon.js +489 -0
  75. package/dist/builtins/local-network/local-network.addon.mjs +477 -0
  76. package/dist/builtins/native-metrics/index.d.ts +2 -0
  77. package/dist/builtins/native-metrics/native-metrics-provider.d.ts +48 -0
  78. package/dist/builtins/native-metrics/native-metrics.addon.d.ts +73 -0
  79. package/dist/builtins/native-metrics/native-metrics.addon.js +922 -0
  80. package/dist/builtins/native-metrics/native-metrics.addon.mjs +914 -0
  81. package/dist/builtins/platform-probe/hardware-decode-accel-probe.d.ts +37 -0
  82. package/dist/builtins/platform-probe/hardware-encoder-probe.d.ts +13 -0
  83. package/dist/builtins/platform-probe/index.d.ts +22 -0
  84. package/dist/builtins/platform-probe/index.js +834 -0
  85. package/dist/builtins/platform-probe/index.mjs +822 -0
  86. package/dist/builtins/platform-probe/inference-config-resolver.d.ts +29 -0
  87. package/dist/builtins/platform-probe/intel-accelerators.d.ts +11 -0
  88. package/dist/builtins/platform-probe/platform-scorer.d.ts +30 -0
  89. package/dist/builtins/platform-probe/runtime-packages.d.ts +6 -0
  90. package/dist/builtins/remote-access-orchestrator/enabled-providers-reconcile.d.ts +96 -0
  91. package/dist/builtins/remote-access-orchestrator/index.d.ts +1 -0
  92. package/dist/builtins/remote-access-orchestrator/index.js +8 -0
  93. package/dist/builtins/remote-access-orchestrator/index.mjs +2 -0
  94. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts +40 -0
  95. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +214 -0
  96. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +208 -0
  97. package/dist/builtins/shared/settle-sources.d.ts +22 -0
  98. package/dist/builtins/snapshot/index.d.ts +2 -0
  99. package/dist/builtins/snapshot/index.js +494 -0
  100. package/dist/builtins/snapshot/index.mjs +488 -0
  101. package/dist/builtins/snapshot/snapshot.addon.d.ts +120 -0
  102. package/dist/builtins/sqlite-storage/config-store.d.ts +8 -0
  103. package/dist/builtins/sqlite-storage/device-store.d.ts +23 -0
  104. package/dist/builtins/sqlite-storage/filesystem-browse-provider.d.ts +25 -0
  105. package/dist/builtins/sqlite-storage/filesystem-storage-provider.d.ts +83 -0
  106. package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.ts +32 -0
  107. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +396 -0
  108. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +388 -0
  109. package/dist/builtins/sqlite-storage/index.d.ts +8 -0
  110. package/dist/builtins/sqlite-storage/index.js +62 -0
  111. package/dist/builtins/sqlite-storage/index.mjs +49 -0
  112. package/dist/builtins/sqlite-storage/integration-registry.d.ts +27 -0
  113. package/dist/builtins/sqlite-storage/path-guard.d.ts +4 -0
  114. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +102 -0
  115. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +14 -0
  116. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +644 -0
  117. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +636 -0
  118. package/dist/builtins/storage-orchestrator/index.d.ts +6 -0
  119. package/dist/builtins/storage-orchestrator/index.js +10 -0
  120. package/dist/builtins/storage-orchestrator/index.mjs +2 -0
  121. package/dist/builtins/storage-orchestrator/location-store.d.ts +49 -0
  122. package/dist/builtins/storage-orchestrator/provider-discovery.d.ts +10 -0
  123. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +103 -0
  124. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1138 -0
  125. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1128 -0
  126. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +236 -0
  127. package/dist/builtins/storage-orchestrator/storage-pressure-manager.d.ts +38 -0
  128. package/dist/builtins/system-backup/system-backup.service.d.ts +137 -0
  129. package/dist/builtins/system-config/index.d.ts +1 -0
  130. package/dist/builtins/system-config/index.js +8 -0
  131. package/dist/builtins/system-config/index.mjs +2 -0
  132. package/dist/builtins/system-config/system-config.addon.d.ts +10 -0
  133. package/dist/builtins/system-config/system-config.addon.js +232 -0
  134. package/dist/builtins/system-config/system-config.addon.mjs +226 -0
  135. package/dist/builtins/winston-logging/index.d.ts +3 -0
  136. package/dist/builtins/winston-logging/index.js +156 -0
  137. package/dist/builtins/winston-logging/index.mjs +144 -0
  138. package/dist/builtins/winston-logging/winston-destination.d.ts +21 -0
  139. package/dist/builtins/winston-logging/winston-logging.addon.d.ts +19 -0
  140. package/dist/chunk-CNf5ZN-e.mjs +37 -0
  141. package/dist/chunk-Cek0wNdY.js +64 -0
  142. package/dist/download/model-download-service.d.ts +41 -0
  143. package/dist/download/model-downloader.d.ts +31 -0
  144. package/dist/events/event-bus.d.ts +10 -0
  145. package/dist/events/system-event-bus.d.ts +14 -0
  146. package/dist/feature/feature-manager.d.ts +11 -0
  147. package/dist/formatter-B7qW8bPJ.mjs +162 -0
  148. package/dist/formatter-DqAKDlvN.js +167 -0
  149. package/dist/http/authenticated-file-server.d.ts +53 -0
  150. package/dist/http/data-plane-registry.d.ts +23 -0
  151. package/dist/http/file-data-plane.d.ts +10 -0
  152. package/dist/http/reverse-proxy.d.ts +15 -0
  153. package/dist/index.d.ts +82 -0
  154. package/dist/index.js +93485 -0
  155. package/dist/index.mjs +93179 -0
  156. package/dist/intel-accelerators-Gg0P5mnl.js +20 -0
  157. package/dist/intel-accelerators-hGgpZ0pX.mjs +19 -0
  158. package/dist/kernel/addon-class-resolver.d.ts +4 -0
  159. package/dist/kernel/addon-engine-manager.d.ts +22 -0
  160. package/dist/kernel/addon-health-monitor.d.ts +154 -0
  161. package/dist/kernel/addon-installer.d.ts +208 -0
  162. package/dist/kernel/addon-loader.d.ts +106 -0
  163. package/dist/kernel/addon-manifest.d.ts +77 -0
  164. package/dist/kernel/capability-handle.d.ts +46 -0
  165. package/dist/kernel/capability-registry.d.ts +412 -0
  166. package/dist/kernel/config-manager.d.ts +212 -0
  167. package/dist/kernel/config-schema.d.ts +93 -0
  168. package/dist/kernel/custom-action-registry.d.ts +23 -0
  169. package/dist/kernel/deps/addon-deps-manager.d.ts +19 -0
  170. package/dist/kernel/deps/manifest-native-deps.d.ts +25 -0
  171. package/dist/kernel/deps/manifest-python-deps.d.ts +20 -0
  172. package/dist/kernel/device-registry.d.ts +29 -0
  173. package/dist/kernel/fs-utils.d.ts +41 -0
  174. package/dist/kernel/hwaccel/hwaccel-resolver.d.ts +19 -0
  175. package/dist/kernel/hwaccel/hwaccel-service.d.ts +4 -0
  176. package/dist/kernel/index.d.ts +74 -0
  177. package/dist/kernel/infra-capabilities.d.ts +13 -0
  178. package/dist/kernel/moleculer/addon-context-factory.d.ts +91 -0
  179. package/dist/kernel/moleculer/addon-data-plane-facility.d.ts +19 -0
  180. package/dist/kernel/moleculer/addon-runner.d.ts +1 -0
  181. package/dist/kernel/moleculer/addon-service-factory.d.ts +50 -0
  182. package/dist/kernel/moleculer/broker-factory.d.ts +50 -0
  183. package/dist/kernel/moleculer/cap-usage-registry.d.ts +46 -0
  184. package/dist/kernel/moleculer/capabilities-access.d.ts +21 -0
  185. package/dist/kernel/moleculer/child-addon-call-dispatch.d.ts +46 -0
  186. package/dist/kernel/moleculer/child-cap-dispatch.d.ts +20 -0
  187. package/dist/kernel/moleculer/cluster-secret.d.ts +15 -0
  188. package/dist/kernel/moleculer/core-cap-service.d.ts +50 -0
  189. package/dist/kernel/moleculer/crash-supervisor.d.ts +50 -0
  190. package/dist/kernel/moleculer/device-cap-proxy.d.ts +79 -0
  191. package/dist/kernel/moleculer/event-bus-core.d.ts +53 -0
  192. package/dist/kernel/moleculer/event-bus.d.ts +53 -0
  193. package/dist/kernel/moleculer/hub-log-forwarder.d.ts +36 -0
  194. package/dist/kernel/moleculer/hub-service.d.ts +35 -0
  195. package/dist/kernel/moleculer/node-registry.d.ts +126 -0
  196. package/dist/kernel/moleculer/process-context.d.ts +4 -0
  197. package/dist/kernel/moleculer/process-service.d.ts +72 -0
  198. package/dist/kernel/moleculer/provider-registry.d.ts +28 -0
  199. package/dist/kernel/moleculer/readiness-context.d.ts +62 -0
  200. package/dist/kernel/moleculer/readiness-service.d.ts +7 -0
  201. package/dist/kernel/moleculer/register-node-client.d.ts +35 -0
  202. package/dist/kernel/moleculer/remote-logger.d.ts +43 -0
  203. package/dist/kernel/moleculer/resilient-cap-call.d.ts +28 -0
  204. package/dist/kernel/moleculer/stream-probe-service.d.ts +9 -0
  205. package/dist/kernel/moleculer/trpc-links.d.ts +189 -0
  206. package/dist/kernel/moleculer/typed-array-serde.d.ts +25 -0
  207. package/dist/kernel/moleculer/worker-device-restore.d.ts +10 -0
  208. package/dist/kernel/provider-kind-drift.d.ts +12 -0
  209. package/dist/kernel/restart-coordinator.d.ts +90 -0
  210. package/dist/kernel/storage-location-registry.d.ts +40 -0
  211. package/dist/kernel/transport/cap-action-name.d.ts +100 -0
  212. package/dist/kernel/transport/cap-route-resolver.d.ts +148 -0
  213. package/dist/kernel/transport/cap-route.d.ts +148 -0
  214. package/dist/kernel/transport/child-cap-protocol.d.ts +136 -0
  215. package/dist/kernel/transport/create-local-transport.d.ts +7 -0
  216. package/dist/kernel/transport/frame-codec.d.ts +7 -0
  217. package/dist/kernel/transport/index.d.ts +27 -0
  218. package/dist/kernel/transport/local-child-client.d.ts +136 -0
  219. package/dist/kernel/transport/local-child-registry.d.ts +179 -0
  220. package/dist/kernel/transport/local-endpoint-path.d.ts +6 -0
  221. package/dist/kernel/transport/local-transport.d.ts +46 -0
  222. package/dist/kernel/transport/parent-unowned-call.d.ts +75 -0
  223. package/dist/kernel/transport/socket-channel.d.ts +27 -0
  224. package/dist/kernel/transport/uds-event-bridge.d.ts +36 -0
  225. package/dist/kernel/transport/uds-event-bus.d.ts +22 -0
  226. package/dist/kernel/transport/uds-local-transport.d.ts +18 -0
  227. package/dist/kernel/transport/uds-log-ingest.d.ts +28 -0
  228. package/dist/kernel/transport/uds-logger.d.ts +44 -0
  229. package/dist/kernel/utils/ring-buffer.d.ts +15 -0
  230. package/dist/kernel/workspace-detect.d.ts +9 -0
  231. package/dist/lifecycle/lifecycle-state-machine.d.ts +28 -0
  232. package/dist/logging/formatter.d.ts +30 -0
  233. package/dist/logging/log-manager.d.ts +54 -0
  234. package/dist/logging/log-ring-buffer.d.ts +47 -0
  235. package/dist/logging/partitioned-log-buffer.d.ts +35 -0
  236. package/dist/logging/scoped-logger.d.ts +17 -0
  237. package/dist/main-DNnMW7Z2.js +9983 -0
  238. package/dist/main-rtjOwPBR.mjs +9976 -0
  239. package/dist/manifest-python-deps-D1DbAQEv.js +6724 -0
  240. package/dist/manifest-python-deps-DZsKTbs1.mjs +6315 -0
  241. package/dist/network/network-quality.d.ts +11 -0
  242. package/dist/notification/notification-service.d.ts +37 -0
  243. package/dist/notification/toast-service.d.ts +22 -0
  244. package/dist/pipeline/engine-manager-resolver.d.ts +15 -0
  245. package/dist/pipeline/pipeline-runner.d.ts +8 -0
  246. package/dist/pipeline/pipeline-validator.d.ts +13 -0
  247. package/dist/process/resource-monitor.d.ts +11 -0
  248. package/dist/python/python-env-manager.d.ts +12 -0
  249. package/dist/repl/interfaces.d.ts +31 -0
  250. package/dist/repl/repl-engine.d.ts +8 -0
  251. package/dist/resource-monitor-ClDGFyf6.mjs +57 -0
  252. package/dist/resource-monitor-IIEanuJt.js +74 -0
  253. package/dist/settle-sources-Bhsy57y-.js +38 -0
  254. package/dist/settle-sources-CDtNC8ub.mjs +33 -0
  255. package/dist/storage/fs-storage-backend.d.ts +40 -0
  256. package/dist/storage/storage-location-manager.d.ts +23 -0
  257. package/dist/storage/storage-manager.d.ts +83 -0
  258. package/dist/tar-BgAEMRBR.js +5434 -0
  259. package/dist/tar-ByMOPNM0.mjs +5429 -0
  260. package/dist/tls/cert-manager.d.ts +26 -0
  261. package/dist/tls/index.d.ts +1 -0
  262. package/package.json +343 -0
@@ -0,0 +1,1128 @@
1
+ import * as path$1 from "node:path";
2
+ import { BaseAddon, StorageLocationTypeSchema, parseJsonObject, storageCapability } from "@camstack/types";
3
+ import * as fs from "node:fs/promises";
4
+ import { buildStorageLocationRegistry } from "@camstack/system";
5
+ //#region src/builtins/storage-orchestrator/storage-orchestrator.service.ts
6
+ var StorageOrchestratorService = class {
7
+ logger;
8
+ getProviders;
9
+ locationStore;
10
+ locations = /* @__PURE__ */ new Map();
11
+ /**
12
+ * Declaration-driven cardinality source, injected by the addon after
13
+ * the kernel aggregates `storageLocations` declarations. `null` until
14
+ * `setRegistry` runs — `upsertLocation` then treats every type as
15
+ * `'multi'` (no singleton enforcement), the safe default for the
16
+ * in-memory-only / early-boot path.
17
+ */
18
+ registry = null;
19
+ /**
20
+ * Injected `providerId → nodeLocal` resolver (see {@link NodeLocalResolver}).
21
+ * `null` until {@link setNodeLocalResolver} runs — `upsertLocation` then
22
+ * skips the node-local requirement check entirely (every provider is
23
+ * treated as node-agnostic), the safe default for the in-memory-only /
24
+ * early-boot path that predates the resolver wiring.
25
+ */
26
+ nodeLocalResolver = null;
27
+ constructor(logger, getProviders, locationStore = null) {
28
+ this.logger = logger;
29
+ this.getProviders = getProviders;
30
+ this.locationStore = locationStore;
31
+ }
32
+ /** True once a persistence store is wired (constructor or {@link attachStore}). */
33
+ hasStore() {
34
+ return this.locationStore !== null;
35
+ }
36
+ /**
37
+ * Late-bind the persistence store (production boot: the `settings-store`
38
+ * cap registers after the orchestrator initializes, so the service runs
39
+ * in-memory-only until then). Idempotent — a second call once a store is
40
+ * wired is a no-op.
41
+ *
42
+ * Reconciles both directions so operator edits survive a reboot:
43
+ * 1. hydrate — DB rows win over any early in-memory seed (restores
44
+ * operator config like `minFreePercent` that the pre-store boot
45
+ * can't see), with the same `isSystem` upgrade as {@link initialize};
46
+ * 2. backfill — in-memory locations the DB doesn't have yet (the
47
+ * pre-store seed defaults on a fresh install) are persisted, so the
48
+ * store becomes the durable source of truth from here on.
49
+ */
50
+ async attachStore(store) {
51
+ if (this.locationStore) return;
52
+ this.locationStore = store;
53
+ const rows = await store.loadAll();
54
+ const dbIds = new Set(rows.map((r) => r.id));
55
+ for (const loc of rows) {
56
+ const upgraded = !loc.isSystem && loc.id === `${loc.type}:default` ? {
57
+ ...loc,
58
+ isSystem: true
59
+ } : loc;
60
+ this.locations.set(upgraded.id, upgraded);
61
+ if (upgraded !== loc) store.upsert(upgraded).catch(() => {});
62
+ }
63
+ const backfill = [...this.locations.values()].filter((l) => !dbIds.has(l.id));
64
+ for (const loc of backfill) store.upsert(loc).catch((err) => {
65
+ this.logger.warn("storage-orchestrator: attachStore backfill persist failed", { meta: {
66
+ id: loc.id,
67
+ error: err instanceof Error ? err.message : String(err)
68
+ } });
69
+ });
70
+ this.logger.info("storage-orchestrator: store attached + reconciled", { meta: {
71
+ hydrated: rows.length,
72
+ backfilled: backfill.length,
73
+ total: this.locations.size
74
+ } });
75
+ }
76
+ /**
77
+ * Inject the declaration-driven cardinality source. Called once by the
78
+ * addon after the kernel aggregates every addon's `storageLocations`
79
+ * declarations into a `StorageLocationRegistry`.
80
+ */
81
+ setRegistry(registry) {
82
+ this.registry = registry;
83
+ }
84
+ /**
85
+ * Inject the synchronous `providerId → nodeLocal` resolver (SP1). Called
86
+ * by the addon from a cached snapshot of every `storage-provider`'s
87
+ * `getProviderInfo().nodeLocal`, refreshed when the provider collection
88
+ * changes. Powers the node-local upsert guard and the boot backfill.
89
+ */
90
+ setNodeLocalResolver(resolver) {
91
+ this.nodeLocalResolver = resolver;
92
+ }
93
+ /**
94
+ * The full set of addon-declared storage locations, as aggregated by
95
+ * the kernel and injected via {@link setRegistry}. Powers the admin-UI
96
+ * Data screen's per-declaration grouping. Empty until the registry is
97
+ * injected (early-boot / in-memory-only path).
98
+ */
99
+ listDeclarations() {
100
+ return this.registry ? this.registry.list() : [];
101
+ }
102
+ /**
103
+ * Hydrate the in-memory map from the persistence layer. Called once
104
+ * by the addon during `onInitialize`, before the eager seed pass.
105
+ * Idempotent — running it twice with the same store contents
106
+ * produces the same map. No-op when the service was constructed
107
+ * without a store.
108
+ *
109
+ * Errors loading individual rows are tolerated: the bad row is
110
+ * skipped and logged so a single corrupted record doesn't lock the
111
+ * whole orchestrator out of boot. (The store-side validation pass
112
+ * before insert means only schema-invalid SQL state can produce
113
+ * such a row.)
114
+ */
115
+ async initialize() {
116
+ if (!this.locationStore) return;
117
+ const rows = await this.locationStore.loadAll();
118
+ let upgraded = 0;
119
+ for (const loc of rows) if (!loc.isSystem && loc.id === `${loc.type}:default`) {
120
+ const upgradedLoc = {
121
+ ...loc,
122
+ isSystem: true
123
+ };
124
+ this.locations.set(upgradedLoc.id, upgradedLoc);
125
+ this.locationStore.upsert(upgradedLoc).catch((err) => {
126
+ this.logger.warn("storage-orchestrator: isSystem upgrade persist failed", { meta: {
127
+ id: upgradedLoc.id,
128
+ error: err instanceof Error ? err.message : String(err)
129
+ } });
130
+ });
131
+ upgraded++;
132
+ } else this.locations.set(loc.id, loc);
133
+ this.logger.info("storage-orchestrator: hydrated locations from store", { meta: {
134
+ loaded: this.locations.size,
135
+ isSystemUpgraded: upgraded
136
+ } });
137
+ }
138
+ /**
139
+ * Boot backfill (SP1): stamp `nodeId` on every persisted node-local
140
+ * location that lacks one. Single-node deployments seeded their
141
+ * filesystem locations before `nodeId` existed — they live on `hubNodeId`
142
+ * (the hub). Node-agnostic (remote) locations are left untouched: their
143
+ * absent `nodeId` is correct (reachable from any node).
144
+ *
145
+ * Idempotent — a location that already carries a `nodeId` (any value) is
146
+ * never re-touched, so re-running on a populated map (or a re-boot) is a
147
+ * no-op. A plain local update, not versioned-migration machinery: only
148
+ * the rows missing `nodeId` are mutated + persisted.
149
+ *
150
+ * No-op until the node-local resolver is wired (it classifies which
151
+ * providers are node-local).
152
+ */
153
+ async backfillNodeIds(hubNodeId) {
154
+ const resolver = this.nodeLocalResolver;
155
+ if (!resolver) return;
156
+ let stamped = 0;
157
+ for (const [id, loc] of [...this.locations]) {
158
+ if (loc.nodeId) continue;
159
+ if (resolver(loc.providerId) !== true) continue;
160
+ const updated = {
161
+ ...loc,
162
+ nodeId: hubNodeId,
163
+ updatedAt: Date.now()
164
+ };
165
+ this.locations.set(id, updated);
166
+ stamped++;
167
+ if (this.locationStore) try {
168
+ await this.locationStore.upsert(updated);
169
+ } catch (err) {
170
+ this.logger.error("storage-orchestrator: nodeId backfill persist failed", { meta: {
171
+ id,
172
+ error: err instanceof Error ? err.message : String(err)
173
+ } });
174
+ }
175
+ }
176
+ if (stamped > 0) this.logger.info("storage-orchestrator: backfilled nodeId on node-local locations", { meta: {
177
+ stamped,
178
+ hubNodeId
179
+ } });
180
+ }
181
+ listLocations(filter) {
182
+ const all = [...this.locations.values()];
183
+ return filter?.type ? all.filter((l) => l.type === filter.type) : all;
184
+ }
185
+ getDefaultLocation(type) {
186
+ for (const loc of this.locations.values()) if (loc.type === type && loc.isDefault) return loc;
187
+ return null;
188
+ }
189
+ /**
190
+ * Insert or update a location. If `isDefault: true`, atomically
191
+ * demotes any other default for the same type — operators always see
192
+ * exactly one default per type.
193
+ *
194
+ * When a persistence store is wired (Task 6), the new record AND any
195
+ * implicitly-demoted siblings are persisted before the in-memory map
196
+ * mutation returns. Persistence errors propagate to the caller.
197
+ */
198
+ upsertLocation(input) {
199
+ const now = Date.now();
200
+ const existing = this.locations.get(input.id);
201
+ if (!existing) {
202
+ if ((this.registry?.cardinalityOf(input.type) ?? "multi") === "single") {
203
+ const already = [...this.locations.values()].find((l) => l.type === input.type);
204
+ if (already) throw new Error(`Storage type "${input.type}" is single — only one location allowed (existing: "${already.id}"). Edit it instead of adding a new one.`);
205
+ }
206
+ }
207
+ if (this.nodeLocalResolver?.(input.providerId) === true && !input.nodeId) input = {
208
+ ...input,
209
+ nodeId: "hub"
210
+ };
211
+ if (existing?.isSystem === true) input = {
212
+ ...input,
213
+ isSystem: true
214
+ };
215
+ const next = {
216
+ ...input,
217
+ createdAt: existing?.createdAt ?? now,
218
+ updatedAt: now
219
+ };
220
+ const demoted = [];
221
+ if (next.isDefault) {
222
+ for (const [otherId, otherLoc] of this.locations) if (otherLoc.type === next.type && otherLoc.id !== next.id && otherLoc.isDefault) {
223
+ const updated = {
224
+ ...otherLoc,
225
+ isDefault: false,
226
+ updatedAt: now
227
+ };
228
+ this.locations.set(otherId, updated);
229
+ demoted.push(updated);
230
+ }
231
+ }
232
+ this.locations.set(next.id, next);
233
+ this.logger.debug("storage-orchestrator: upsertLocation", { meta: {
234
+ id: next.id,
235
+ type: next.type,
236
+ providerId: next.providerId,
237
+ isDefault: next.isDefault
238
+ } });
239
+ if (this.locationStore) {
240
+ const store = this.locationStore;
241
+ (async () => {
242
+ try {
243
+ for (const d of demoted) await store.upsert(d);
244
+ await store.upsert(next);
245
+ } catch (err) {
246
+ this.logger.error("storage-orchestrator: upsert persistence failed", { meta: {
247
+ id: next.id,
248
+ error: err instanceof Error ? err.message : String(err)
249
+ } });
250
+ }
251
+ })();
252
+ }
253
+ return next;
254
+ }
255
+ /**
256
+ * Remove a location. Refuses to remove the default of a type unless a
257
+ * sibling default exists (defensive — `upsertLocation` already ensures
258
+ * at most one default per type, but the logic guards against future
259
+ * bypass paths e.g. SQLite migration that imports two defaults).
260
+ *
261
+ * Persistence (Task 6) mirrors the delete asynchronously, with errors
262
+ * routed to the logger — see `upsertLocation` for the rationale.
263
+ */
264
+ deleteLocation(id) {
265
+ const loc = this.locations.get(id);
266
+ if (!loc) throw new Error(`Storage location "${id}" not found`);
267
+ if (loc.isSystem) throw new Error(`Storage location "${id}" is system-managed and cannot be deleted. Edit its config (path / providerId) instead.`);
268
+ if (loc.isDefault) {
269
+ if (![...this.locations.values()].find((l) => l.type === loc.type && l.id !== id && l.isDefault)) throw new Error(`Cannot delete default location "${id}" for type "${loc.type}" — promote another location to default first`);
270
+ }
271
+ this.locations.delete(id);
272
+ if (this.locationStore) this.locationStore.delete(id).catch((err) => {
273
+ this.logger.error("storage-orchestrator: delete persistence failed", { meta: {
274
+ id,
275
+ error: err instanceof Error ? err.message : String(err)
276
+ } });
277
+ });
278
+ }
279
+ /**
280
+ * Remove system-seeded locations whose type is no longer declared by any
281
+ * addon (stale defaults from a removed location type). Operator-added
282
+ * (non-system) locations of an undeclared type are KEPT but warned — the
283
+ * operator owns them. Requires the registry to be set first.
284
+ *
285
+ * FAIL-SAFE: if the registry is EMPTY (no addon declared any location), we
286
+ * refuse to prune anything. An empty registry almost always means
287
+ * declarations failed to load (boot ordering, a stale install) — pruning
288
+ * "everything undeclared" in that state would wipe every system location
289
+ * (data/logs/recordings/…). Better to keep stale rows than destroy live ones.
290
+ */
291
+ pruneUndeclaredSystemLocations() {
292
+ if (!this.registry) return;
293
+ if (this.registry.list().length === 0) {
294
+ this.logger.warn("storage-orchestrator: skipping prune — no location declarations loaded (fail-safe)");
295
+ return;
296
+ }
297
+ for (const [id, loc] of [...this.locations]) {
298
+ if (this.registry.cardinalityOf(loc.type) !== null) continue;
299
+ if (loc.isSystem) {
300
+ this.locations.delete(id);
301
+ if (this.locationStore) this.locationStore.delete(id).catch((err) => {
302
+ this.logger.error("storage-orchestrator: prune persistence failed", { meta: {
303
+ id,
304
+ error: err instanceof Error ? err.message : String(err)
305
+ } });
306
+ });
307
+ this.logger.info("storage-orchestrator: pruned stale system location", { meta: {
308
+ id,
309
+ type: loc.type
310
+ } });
311
+ } else this.logger.warn("storage-orchestrator: location of undeclared type kept (operator-owned)", { meta: {
312
+ id,
313
+ type: loc.type
314
+ } });
315
+ }
316
+ }
317
+ /**
318
+ * Resolve a `StorageLocationRef` to a concrete `StorageLocation`.
319
+ * - bare type (e.g. `'backups'`) → default location of that type
320
+ * - fully-qualified id (e.g. `'backups:nas-01'`) → that exact instance
321
+ *
322
+ * Throws an actionable error when nothing matches — every consumer of
323
+ * the singleton `storage` cap funnels through here, so the error
324
+ * message is what operators see when the orchestrator's view of the
325
+ * world doesn't match expectations.
326
+ */
327
+ resolveRef(ref) {
328
+ if (ref.includes(":")) {
329
+ const loc = this.locations.get(ref);
330
+ if (!loc) throw new Error(`Storage location "${ref}" not found`);
331
+ return loc;
332
+ }
333
+ const def = this.getDefaultLocation(ref);
334
+ if (!def) throw new Error(`No default storage location configured for type "${ref}" — register one or pass a fully-qualified id (\`<type>:<slug>\`)`);
335
+ return def;
336
+ }
337
+ /**
338
+ * Find the storage-provider that backs a given location. Lookup is by
339
+ * `location.providerId` against `getProviderInfo().providerId` from
340
+ * each registered collection provider. Throws with both the missing
341
+ * providerId and the offending location id so the operator can pick
342
+ * the right place to fix the config.
343
+ */
344
+ async getProviderFor(location) {
345
+ const providers = this.getProviders();
346
+ for (const p of providers) if ((await p.getProviderInfo()).providerId === location.providerId) return p;
347
+ throw new Error(`No storage-provider registered for providerId "${location.providerId}" (location "${location.id}")`);
348
+ }
349
+ /** Convenience: lookup by id (returns `undefined` if not found). */
350
+ getLocationById(id) {
351
+ return this.locations.get(id);
352
+ }
353
+ /**
354
+ * Seed one default `StorageLocation` per addon-declared location that
355
+ * doesn't already have one. Idempotent — re-running with the same
356
+ * declarations on a populated map is a no-op (each declared `id`
357
+ * already resolves to an existing `<id>:default`).
358
+ *
359
+ * Convention: the bare-type ref `'recordings'` resolves via
360
+ * `getDefaultLocation('recordings')` (walks `isDefault === true`); the
361
+ * actual stored `id` is `'recordings:default'` so it satisfies the
362
+ * `^[a-z][a-z0-9-]*:[a-z0-9-]+$` regex on `StorageLocationSchema.id`.
363
+ *
364
+ * `basePath` is the storage root (e.g. `<CAMSTACK_DATA>`). Each
365
+ * location's `config.basePath` is `<basePath>/<id>`, except when the
366
+ * declaration sets `defaultsTo` — then it inherits the resolved root of
367
+ * the referenced location's default instance (sharing the same root
368
+ * directory for derivative slots like `recordingsLow` → `recordings`).
369
+ */
370
+ seedDefaults(input) {
371
+ let added = 0;
372
+ const rootOf = (id) => path$1.join(input.basePath, id);
373
+ for (const d of input.declarations) {
374
+ if (this.hasAnyLocationOfType(d.id)) continue;
375
+ const base = d.defaultsTo ? this.getLocationById(`${d.defaultsTo}:default`)?.config["basePath"] ?? rootOf(d.defaultsTo) : rootOf(d.id);
376
+ this.upsertLocation({
377
+ id: `${d.id}:default`,
378
+ type: d.id,
379
+ displayName: d.displayName,
380
+ providerId: input.providerId,
381
+ config: { basePath: base },
382
+ isDefault: true,
383
+ isSystem: true
384
+ });
385
+ added++;
386
+ }
387
+ if (added > 0) this.logger.info("storage-orchestrator: seeded default locations", { meta: {
388
+ added,
389
+ basePath: input.basePath
390
+ } });
391
+ }
392
+ /** Internal: check whether any location of the given type exists. */
393
+ hasAnyLocationOfType(type) {
394
+ for (const loc of this.locations.values()) if (loc.type === type) return true;
395
+ return false;
396
+ }
397
+ };
398
+ //#endregion
399
+ //#region src/builtins/storage-orchestrator/location-store.ts
400
+ var STORAGE_LOCATIONS_TABLE = "storage_locations";
401
+ /**
402
+ * Reserved key under which the cluster `nodeId` (SP1) is persisted INSIDE
403
+ * the `config` JSON blob. Storing it in the blob rather than a dedicated
404
+ * column avoids a destructive `ensureTable` DROP+recreate on deploy (a
405
+ * newly-declared column counts as a missing column → the structured
406
+ * backend drops the table, wiping operator location customizations such
407
+ * as `recordings:default`'s `minFreePercent`). The double-underscore
408
+ * prefix keeps it out of the provider-facing config namespace
409
+ * (`basePath`, `minFreePercent`, …) — the mapper strips it on read so
410
+ * API consumers and providers never see it.
411
+ */
412
+ var NODE_ID_CONFIG_KEY = "__nodeId";
413
+ var STORAGE_LOCATIONS_SCHEMA = {
414
+ columns: [
415
+ {
416
+ name: "id",
417
+ type: "TEXT",
418
+ primaryKey: true
419
+ },
420
+ {
421
+ name: "type",
422
+ type: "TEXT",
423
+ notNull: true
424
+ },
425
+ {
426
+ name: "display_name",
427
+ type: "TEXT",
428
+ notNull: true
429
+ },
430
+ {
431
+ name: "provider_id",
432
+ type: "TEXT",
433
+ notNull: true
434
+ },
435
+ {
436
+ name: "config",
437
+ type: "TEXT",
438
+ notNull: true,
439
+ defaultValue: "{}"
440
+ },
441
+ {
442
+ name: "is_default",
443
+ type: "INTEGER",
444
+ notNull: true,
445
+ defaultValue: 0
446
+ },
447
+ {
448
+ name: "is_system",
449
+ type: "INTEGER",
450
+ notNull: true,
451
+ defaultValue: 0
452
+ },
453
+ {
454
+ name: "created_at",
455
+ type: "INTEGER",
456
+ notNull: true
457
+ },
458
+ {
459
+ name: "updated_at",
460
+ type: "INTEGER",
461
+ notNull: true
462
+ }
463
+ ],
464
+ indexes: [{
465
+ name: "idx_storage_locations_type",
466
+ columns: ["type"]
467
+ }]
468
+ };
469
+ /**
470
+ * SQLite-backed implementation of `ILocationStore` — uses the
471
+ * `settings-store` cap's structured-table surface (`ensureTable` /
472
+ * `tableInsert` / `tableUpdate` / `tableDelete` / `tableQuery`). Wired
473
+ * to the live `ISettingsBackend` provider obtained from
474
+ * `kernel.capabilityRegistry.getSingleton('settings-store')`.
475
+ *
476
+ * The cap-router surface (`ctx.api.settingsStore.*`) does NOT expose
477
+ * the `tableXxx` methods — they're only on the raw `ISettingsBackend`.
478
+ * Same constraint that drives `IntegrationRegistry` to take a direct
479
+ * provider reference instead of going through the cap.
480
+ */
481
+ var SqliteLocationStore = class {
482
+ backend;
483
+ tableEnsured = false;
484
+ constructor(backend) {
485
+ this.backend = backend;
486
+ }
487
+ /**
488
+ * Lazy `ensureTable` — keeps the constructor cheap and async-free.
489
+ * Called from every mutation/read path; the backend's `ensureTable`
490
+ * is itself idempotent (additive migration), so repeated calls are
491
+ * effectively no-ops after the first.
492
+ */
493
+ async ensureTable() {
494
+ if (this.tableEnsured) return;
495
+ if (!this.backend.ensureTable) throw new Error("SqliteLocationStore: settings backend does not implement ensureTable — expected the SQLite settings backend, got a backend without structured-table support");
496
+ await this.backend.ensureTable(STORAGE_LOCATIONS_TABLE, STORAGE_LOCATIONS_SCHEMA);
497
+ this.tableEnsured = true;
498
+ }
499
+ async loadAll() {
500
+ await this.ensureTable();
501
+ if (!this.backend.tableQuery) return [];
502
+ const rows = await this.backend.tableQuery(STORAGE_LOCATIONS_TABLE, { orderBy: {
503
+ field: "created_at",
504
+ direction: "asc"
505
+ } });
506
+ const out = [];
507
+ for (const row of rows) {
508
+ const mapped = mapRowToLocation(row);
509
+ if (mapped) out.push(mapped);
510
+ }
511
+ return out;
512
+ }
513
+ /**
514
+ * Replace-or-insert. Implemented as `delete` + `insert` rather than
515
+ * the conditional `tableUpdate` path because the orchestrator
516
+ * serializes every mutation through the in-memory map before
517
+ * dispatching to the store — there's no contention to lose, and the
518
+ * delete-then-insert path is unconditionally simpler.
519
+ */
520
+ async upsert(loc) {
521
+ await this.ensureTable();
522
+ if (!this.backend.tableDelete || !this.backend.tableInsert) throw new Error("SqliteLocationStore: backend missing tableDelete/tableInsert");
523
+ await this.backend.tableDelete(STORAGE_LOCATIONS_TABLE, { id: loc.id });
524
+ await this.backend.tableInsert(STORAGE_LOCATIONS_TABLE, mapLocationToRow(loc));
525
+ }
526
+ async delete(id) {
527
+ await this.ensureTable();
528
+ if (!this.backend.tableDelete) throw new Error("SqliteLocationStore: backend missing tableDelete");
529
+ await this.backend.tableDelete(STORAGE_LOCATIONS_TABLE, { id });
530
+ }
531
+ };
532
+ function mapLocationToRow(loc) {
533
+ const storedConfig = loc.nodeId !== void 0 ? {
534
+ ...loc.config,
535
+ [NODE_ID_CONFIG_KEY]: loc.nodeId
536
+ } : { ...loc.config };
537
+ return {
538
+ id: loc.id,
539
+ type: loc.type,
540
+ display_name: loc.displayName,
541
+ provider_id: loc.providerId,
542
+ config: JSON.stringify(storedConfig),
543
+ is_default: loc.isDefault ? 1 : 0,
544
+ is_system: loc.isSystem ? 1 : 0,
545
+ created_at: loc.createdAt,
546
+ updated_at: loc.updatedAt
547
+ };
548
+ }
549
+ /**
550
+ * Decode a row read from the structured table. Returns `null` when the
551
+ * row's `type` doesn't parse against the Zod enum — the caller skips
552
+ * the row and logs. Defensive against schema drift across upgrades
553
+ * (e.g. an older build wrote a since-removed `recordings-archive`
554
+ * type and the new build no longer recognises it).
555
+ */
556
+ function mapRowToLocation(row) {
557
+ const typeRaw = row["type"];
558
+ const parsedType = StorageLocationTypeSchema.safeParse(typeRaw);
559
+ if (!parsedType.success) return null;
560
+ const type = parsedType.data;
561
+ const id = String(row["id"] ?? "");
562
+ if (!id) return null;
563
+ const configRaw = row["config"];
564
+ const storedConfig = typeof configRaw === "string" && configRaw.length > 0 ? parseJsonObject(configRaw) ?? {} : {};
565
+ const nodeIdRaw = storedConfig[NODE_ID_CONFIG_KEY];
566
+ const nodeId = typeof nodeIdRaw === "string" && nodeIdRaw.length > 0 ? nodeIdRaw : void 0;
567
+ const config = { ...storedConfig };
568
+ delete config[NODE_ID_CONFIG_KEY];
569
+ return {
570
+ id,
571
+ type,
572
+ displayName: String(row["display_name"] ?? ""),
573
+ providerId: String(row["provider_id"] ?? ""),
574
+ config,
575
+ ...nodeId !== void 0 ? { nodeId } : {},
576
+ isDefault: row["is_default"] === 1 || row["is_default"] === true,
577
+ isSystem: row["is_system"] === 1 || row["is_system"] === true,
578
+ createdAt: Number(row["created_at"] ?? 0),
579
+ updatedAt: Number(row["updated_at"] ?? 0)
580
+ };
581
+ }
582
+ //#endregion
583
+ //#region src/builtins/storage-orchestrator/provider-discovery.ts
584
+ async function collectProviderInfos(providers, onError) {
585
+ const out = [];
586
+ for (const [index, p] of providers.entries()) try {
587
+ const info = await p.getProviderInfo();
588
+ out.push(info);
589
+ } catch (err) {
590
+ onError(p, err, index);
591
+ }
592
+ return out;
593
+ }
594
+ //#endregion
595
+ //#region src/builtins/storage-orchestrator/storage-pressure-manager.ts
596
+ var DEFAULT_MAX_ROUNDS = 8;
597
+ var DEFAULT_HYSTERESIS_PERCENT = 1;
598
+ var StoragePressureManager = class {
599
+ deps;
600
+ constructor(deps) {
601
+ this.deps = deps;
602
+ }
603
+ /** Relieve every managed location currently under its threshold. */
604
+ async sweep() {
605
+ const thresholds = await this.deps.getLocationThresholds();
606
+ for (const [locationId, threshold] of thresholds) {
607
+ if (threshold <= 0) continue;
608
+ try {
609
+ await this.relieveLocation(locationId, threshold);
610
+ } catch (err) {
611
+ this.deps.logger.warn("storage pressure: relief failed for location", { meta: {
612
+ locationId,
613
+ error: err instanceof Error ? err.message : String(err)
614
+ } });
615
+ }
616
+ }
617
+ }
618
+ async relieveLocation(locationId, threshold) {
619
+ const maxRounds = this.deps.maxRounds ?? DEFAULT_MAX_ROUNDS;
620
+ const hysteresis = this.deps.hysteresisPercent ?? DEFAULT_HYSTERESIS_PERCENT;
621
+ for (let round = 0; round < maxRounds; round++) {
622
+ const free = await this.deps.getFreePercent(locationId);
623
+ if (free >= threshold) return;
624
+ const total = await this.deps.getVolumeTotalBytes(locationId);
625
+ const deficitPercent = threshold - free + hysteresis;
626
+ const targetBytes = Math.ceil(total * deficitPercent / 100);
627
+ if (targetBytes <= 0) return;
628
+ const providers = this.deps.getEvictableProviders();
629
+ const usages = await Promise.all(providers.map(async (p) => ({
630
+ p,
631
+ bytes: (await p.getEvictableUsage({ locationId })).bytes
632
+ })));
633
+ const totalEvictable = usages.reduce((s, u) => s + u.bytes, 0);
634
+ if (totalEvictable <= 0) {
635
+ this.deps.logger.debug("storage pressure: breach but no evictable data on location", { meta: {
636
+ locationId,
637
+ freePercent: free,
638
+ threshold
639
+ } });
640
+ return;
641
+ }
642
+ let reclaimed = 0;
643
+ for (const u of usages) {
644
+ if (u.bytes <= 0) continue;
645
+ const share = Math.max(1, Math.ceil(targetBytes * u.bytes / totalEvictable));
646
+ const res = await u.p.evict({
647
+ locationId,
648
+ targetBytes: share
649
+ });
650
+ reclaimed += res.reclaimedBytes;
651
+ }
652
+ this.deps.logger.info("storage pressure: relief round", { meta: {
653
+ locationId,
654
+ round,
655
+ freePercent: free,
656
+ threshold,
657
+ targetBytes,
658
+ reclaimedBytes: reclaimed
659
+ } });
660
+ if (reclaimed <= 0) return;
661
+ }
662
+ }
663
+ };
664
+ //#endregion
665
+ //#region src/builtins/storage-orchestrator/storage-orchestrator.addon.ts
666
+ /**
667
+ * `storage-orchestrator` builtin — singleton owner of the consumer-
668
+ * facing `storage` cap.
669
+ *
670
+ * Responsibilities:
671
+ * - Hold the `locationId → StorageLocation` map (in-memory; Task 6
672
+ * adds persistence).
673
+ * - Resolve `StorageLocationRef` → concrete `StorageLocation`.
674
+ * - Dispatch every cap method to the right `storage-provider`
675
+ * (collection cap, registered by `filesystem-storage` and future
676
+ * SFTP / S3 / WebDAV addons).
677
+ * - Track ownership of upload / download sessions so chunked I/O
678
+ * (which carries no `location` after the first hop) can route
679
+ * subsequent chunks to the same provider.
680
+ * - First-boot seed defaults from addon-declared location types (Task 5).
681
+ * Re-runs on every `capability:provider-registered` event so providers
682
+ * that register after the orchestrator boots also get seeded — the
683
+ * `hasAnyLocationOfType` guard makes the operation idempotent.
684
+ *
685
+ * Task 6 will plug a persistence backend in front of the in-memory map.
686
+ * Task 7 will migrate consumers off the legacy storage shim.
687
+ */
688
+ var STORAGE_PROVIDER_CAP_NAME = "storage-provider";
689
+ var STORAGE_EVICTABLE_CAP_NAME = "storage-evictable";
690
+ /**
691
+ * Single-node default: existing node-local locations live on the hub.
692
+ * The boot backfill (SP1) stamps this on any node-local location seeded
693
+ * before `nodeId` existed.
694
+ */
695
+ var HUB_NODE_ID = "hub";
696
+ /** How often the pressure manager re-evaluates free space per location. */
697
+ var PRESSURE_SWEEP_INTERVAL_MS = 6e4;
698
+ /**
699
+ * Raw event category emitted by `CapabilityRegistry.registerProvider` —
700
+ * not in the typed `EventCategory` enum because it lives in the kernel
701
+ * infra layer (every cap-bound event is a custom-shape payload).
702
+ */
703
+ var CAP_PROVIDER_REGISTERED_CATEGORY = "capability:provider-registered";
704
+ var StorageOrchestratorAddon = class extends BaseAddon {
705
+ service = null;
706
+ pressureManager = null;
707
+ pressureTimer = null;
708
+ pressureSweepInFlight = false;
709
+ /**
710
+ * `uploadId` → providerId. Populated on `beginUpload`, consulted on
711
+ * every subsequent `writeChunk` / `finalizeUpload` / `abortUpload`,
712
+ * cleared on terminal calls. The provider holds the actual session
713
+ * state (open file descriptor, buffered offsets, …) — we just
714
+ * remember which provider owns it.
715
+ */
716
+ uploadOwners = /* @__PURE__ */ new Map();
717
+ /** Symmetric to `uploadOwners` for download sessions. */
718
+ downloadOwners = /* @__PURE__ */ new Map();
719
+ /**
720
+ * Cached `providerId → nodeLocal` snapshot (SP1). Backs the orchestrator's
721
+ * synchronous `NodeLocalResolver` — `getProviderInfo()` is async, so we
722
+ * keep a sync cache and refresh it whenever the provider collection
723
+ * changes (alongside the seed pass). Absent providerId → resolver returns
724
+ * `undefined` (treated as node-agnostic).
725
+ */
726
+ nodeLocalByProvider = /* @__PURE__ */ new Map();
727
+ /**
728
+ * Disposers run on `onShutdown` — currently the eventBus subscription
729
+ * for `capability:provider-registered` events used by the lazy seed
730
+ * fallback. Stored separately from the `BaseAddon` disposer chain so
731
+ * it can be inspected in tests.
732
+ */
733
+ seedSubscriptionDisposers = [];
734
+ constructor() {
735
+ super({});
736
+ }
737
+ async onInitialize() {
738
+ const getProviders = () => {
739
+ const reg = this.ctx.kernel.capabilityRegistry;
740
+ if (!reg) return [];
741
+ return reg.getCollection(STORAGE_PROVIDER_CAP_NAME);
742
+ };
743
+ const locationStore = this.resolveLocationStore();
744
+ const service = new StorageOrchestratorService(this.ctx.logger, getProviders, locationStore);
745
+ this.service = service;
746
+ service.setNodeLocalResolver((providerId) => this.nodeLocalByProvider.get(providerId));
747
+ await service.initialize();
748
+ const provider = {
749
+ listLocations: async ({ type }) => {
750
+ return type !== void 0 ? service.listLocations({ type }) : service.listLocations();
751
+ },
752
+ getDefaultLocation: async ({ type }) => service.getDefaultLocation(type),
753
+ listLocationDeclarations: async () => service.listDeclarations(),
754
+ upsertLocation: async (input) => service.upsertLocation(input),
755
+ deleteLocation: async ({ id }) => {
756
+ service.deleteLocation(id);
757
+ },
758
+ testLocation: async ({ id }) => {
759
+ const loc = service.getLocationById(id);
760
+ if (!loc) return {
761
+ ok: false,
762
+ error: `Location "${id}" not found`
763
+ };
764
+ try {
765
+ return (await service.getProviderFor(loc)).testLocation({ config: loc.config });
766
+ } catch (err) {
767
+ return {
768
+ ok: false,
769
+ error: err instanceof Error ? err.message : String(err)
770
+ };
771
+ }
772
+ },
773
+ listProviders: async () => {
774
+ return collectProviderInfos(getProviders(), (_provider, err, index) => {
775
+ this.ctx.logger.warn("storage-orchestrator: getProviderInfo failed", { meta: {
776
+ providerIndex: index,
777
+ error: err instanceof Error ? err.message : String(err)
778
+ } });
779
+ });
780
+ },
781
+ testConfig: async ({ providerId, config }) => {
782
+ const providers = getProviders();
783
+ for (const p of providers) try {
784
+ if ((await p.getProviderInfo()).providerId !== providerId) continue;
785
+ return p.testLocation({ config });
786
+ } catch (err) {
787
+ return {
788
+ ok: false,
789
+ error: err instanceof Error ? err.message : String(err)
790
+ };
791
+ }
792
+ return {
793
+ ok: false,
794
+ error: `No storage-provider registered for providerId "${providerId}"`
795
+ };
796
+ },
797
+ resolve: async ({ location, relativePath }) => {
798
+ const loc = service.resolveRef(location);
799
+ return (await service.getProviderFor(loc)).resolve({
800
+ location: loc,
801
+ relativePath
802
+ });
803
+ },
804
+ write: async ({ location, relativePath, data }) => {
805
+ const loc = service.resolveRef(location);
806
+ return (await service.getProviderFor(loc)).write({
807
+ location: loc,
808
+ relativePath,
809
+ data
810
+ });
811
+ },
812
+ read: async ({ location, relativePath }) => {
813
+ const loc = service.resolveRef(location);
814
+ return (await service.getProviderFor(loc)).read({
815
+ location: loc,
816
+ relativePath
817
+ });
818
+ },
819
+ exists: async ({ location, relativePath }) => {
820
+ const loc = service.resolveRef(location);
821
+ return (await service.getProviderFor(loc)).exists({
822
+ location: loc,
823
+ relativePath
824
+ });
825
+ },
826
+ list: async ({ location, prefix }) => {
827
+ const loc = service.resolveRef(location);
828
+ const p = await service.getProviderFor(loc);
829
+ return prefix !== void 0 ? p.list({
830
+ location: loc,
831
+ prefix
832
+ }) : p.list({ location: loc });
833
+ },
834
+ delete: async ({ location, relativePath }) => {
835
+ const loc = service.resolveRef(location);
836
+ return (await service.getProviderFor(loc)).delete({
837
+ location: loc,
838
+ relativePath
839
+ });
840
+ },
841
+ getAvailableSpace: async ({ location }) => {
842
+ const loc = service.resolveRef(location);
843
+ return (await service.getProviderFor(loc)).getAvailableSpace({ location: loc });
844
+ },
845
+ beginUpload: async ({ location, relativePath, sizeBytes }) => {
846
+ const loc = service.resolveRef(location);
847
+ const p = await service.getProviderFor(loc);
848
+ const result = sizeBytes !== void 0 ? await p.beginUpload({
849
+ location: loc,
850
+ relativePath,
851
+ sizeBytes
852
+ }) : await p.beginUpload({
853
+ location: loc,
854
+ relativePath
855
+ });
856
+ this.uploadOwners.set(result.uploadId, loc.providerId);
857
+ return result;
858
+ },
859
+ writeChunk: async (input) => {
860
+ return (await this.resolveByUploadId(input.uploadId)).writeChunk(input);
861
+ },
862
+ finalizeUpload: async (input) => {
863
+ const p = await this.resolveByUploadId(input.uploadId);
864
+ try {
865
+ return await p.finalizeUpload(input);
866
+ } finally {
867
+ this.uploadOwners.delete(input.uploadId);
868
+ }
869
+ },
870
+ abortUpload: async (input) => {
871
+ const p = await this.resolveByUploadId(input.uploadId);
872
+ try {
873
+ return await p.abortUpload(input);
874
+ } finally {
875
+ this.uploadOwners.delete(input.uploadId);
876
+ }
877
+ },
878
+ beginDownload: async ({ location, relativePath }) => {
879
+ const loc = service.resolveRef(location);
880
+ const result = await (await service.getProviderFor(loc)).beginDownload({
881
+ location: loc,
882
+ relativePath
883
+ });
884
+ this.downloadOwners.set(result.downloadId, loc.providerId);
885
+ return result;
886
+ },
887
+ readChunk: async (input) => {
888
+ return (await this.resolveByDownloadId(input.downloadId)).readChunk(input);
889
+ },
890
+ endDownload: async (input) => {
891
+ const p = await this.resolveByDownloadId(input.downloadId);
892
+ try {
893
+ return await p.endDownload(input);
894
+ } finally {
895
+ this.downloadOwners.delete(input.downloadId);
896
+ }
897
+ }
898
+ };
899
+ await this.seedFromDeclarations();
900
+ const eventBus = this.ctx.eventBus;
901
+ if (eventBus) {
902
+ const unsub = eventBus.subscribe({ category: CAP_PROVIDER_REGISTERED_CATEGORY }, (event) => {
903
+ this.ensureStoreWired();
904
+ if (event.data?.capability !== STORAGE_PROVIDER_CAP_NAME) return;
905
+ this.seedFromDeclarations();
906
+ });
907
+ this.seedSubscriptionDisposers.push(unsub);
908
+ }
909
+ this.ensureStoreWired();
910
+ this.pressureManager = new StoragePressureManager({
911
+ logger: this.ctx.logger,
912
+ getFreePercent: (id) => this.locationFreePercent(id),
913
+ getVolumeTotalBytes: (id) => this.locationVolumeTotalBytes(id),
914
+ getEvictableProviders: () => this.ctx.kernel.capabilityRegistry?.getCollection(STORAGE_EVICTABLE_CAP_NAME) ?? [],
915
+ getLocationThresholds: () => this.resolveLocationThresholds()
916
+ });
917
+ this.pressureTimer = setInterval(() => {
918
+ this.runPressureSweep();
919
+ }, PRESSURE_SWEEP_INTERVAL_MS);
920
+ this.ctx.logger.info("Storage orchestrator initialized");
921
+ return [{
922
+ capability: storageCapability,
923
+ provider
924
+ }];
925
+ }
926
+ async onShutdown() {
927
+ for (const dispose of this.seedSubscriptionDisposers) try {
928
+ dispose();
929
+ } catch {}
930
+ this.seedSubscriptionDisposers.length = 0;
931
+ if (this.pressureTimer) {
932
+ clearInterval(this.pressureTimer);
933
+ this.pressureTimer = null;
934
+ }
935
+ this.pressureManager = null;
936
+ this.uploadOwners.clear();
937
+ this.downloadOwners.clear();
938
+ this.service = null;
939
+ }
940
+ /**
941
+ * Resolve the live `ISettingsBackend` from the capability registry
942
+ * and wrap it in a `SqliteLocationStore`. Returns `null` when the
943
+ * settings-store cap isn't registered yet — the service falls back
944
+ * to in-memory only. Production boot order (sqlite-settings runs
945
+ * before the orchestrator) makes this the rare path.
946
+ */
947
+ resolveLocationStore(quiet = false) {
948
+ const reg = this.ctx.kernel.capabilityRegistry;
949
+ if (!reg) return null;
950
+ const backend = reg.getSingleton("settings-store");
951
+ if (!backend) {
952
+ if (!quiet) this.ctx.logger.warn("storage-orchestrator: no settings-store provider yet — running in-memory until it registers");
953
+ return null;
954
+ }
955
+ if (!backend.ensureTable || !backend.tableInsert) {
956
+ if (!quiet) this.ctx.logger.warn("storage-orchestrator: settings backend lacks structured-table support — running in-memory only");
957
+ return null;
958
+ }
959
+ return new SqliteLocationStore(backend);
960
+ }
961
+ /**
962
+ * Late-wire the persistence store once the `settings-store` cap registers.
963
+ * Production boot order means the store is usually absent at
964
+ * `onInitialize`; this runs on every `capability:provider-registered`
965
+ * event until it succeeds. Single-flighted + idempotent (the service
966
+ * ignores a second attach). Without this, operator edits (e.g. a
967
+ * location's `minFreePercent`) live only in memory and are wiped on every
968
+ * restart — the service never hydrates from / persists to SQLite.
969
+ */
970
+ storeWireInFlight = null;
971
+ async ensureStoreWired() {
972
+ const service = this.service;
973
+ if (!service || service.hasStore()) return;
974
+ if (this.storeWireInFlight) return this.storeWireInFlight;
975
+ const store = this.resolveLocationStore(true);
976
+ if (!store) return;
977
+ this.ctx.logger.info("storage-orchestrator: settings-store now available — wiring persistence");
978
+ this.storeWireInFlight = service.attachStore(store).then(() => this.seedFromDeclarations()).catch((err) => {
979
+ this.ctx.logger.error("storage-orchestrator: attachStore failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
980
+ }).finally(() => {
981
+ this.storeWireInFlight = null;
982
+ });
983
+ return this.storeWireInFlight;
984
+ }
985
+ storageProviders() {
986
+ const reg = this.ctx.kernel.capabilityRegistry;
987
+ if (!reg) return [];
988
+ return reg.getCollection(STORAGE_PROVIDER_CAP_NAME);
989
+ }
990
+ /** Single-flighted pressure sweep — never overlaps a slow eviction round. */
991
+ async runPressureSweep() {
992
+ if (this.pressureSweepInFlight || !this.pressureManager) return;
993
+ this.pressureSweepInFlight = true;
994
+ try {
995
+ await this.pressureManager.sweep();
996
+ } catch (err) {
997
+ this.ctx.logger.warn("storage pressure sweep failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
998
+ } finally {
999
+ this.pressureSweepInFlight = false;
1000
+ }
1001
+ }
1002
+ /**
1003
+ * locationId → resolved `minFreePercent`. The policy is intrinsic to the
1004
+ * location's provider (`shouldSaveDiskSpace` gates the guard, `minFreePercent`
1005
+ * is the default), overridable per-location via `config.minFreePercent`.
1006
+ * Locations with the guard off (or threshold 0) are omitted. Moved here from
1007
+ * the recorder so disk-pressure is a single core concern.
1008
+ */
1009
+ async resolveLocationThresholds() {
1010
+ const out = /* @__PURE__ */ new Map();
1011
+ const svc = this.service;
1012
+ if (!svc) return out;
1013
+ const infos = await collectProviderInfos(this.storageProviders(), () => {});
1014
+ const byProvider = new Map(infos.map((i) => [i.providerId, i]));
1015
+ for (const loc of svc.listLocations()) {
1016
+ const info = byProvider.get(loc.providerId);
1017
+ if (!info || !info.shouldSaveDiskSpace) continue;
1018
+ const override = loc.config["minFreePercent"];
1019
+ const threshold = typeof override === "number" && Number.isFinite(override) && override >= 0 ? override : info.minFreePercent ?? 0;
1020
+ if (threshold > 0) out.set(loc.id, threshold);
1021
+ }
1022
+ return out;
1023
+ }
1024
+ locationBasePath(locationId) {
1025
+ const bp = (this.service?.listLocations().find((l) => l.id === locationId))?.config["basePath"];
1026
+ return typeof bp === "string" && bp.length > 0 ? bp : null;
1027
+ }
1028
+ /** Free capacity (%) on a location's volume via `statfs`; 100 (guard inert) when unstattable. */
1029
+ async locationFreePercent(locationId) {
1030
+ const bp = this.locationBasePath(locationId);
1031
+ if (!bp) return 100;
1032
+ try {
1033
+ const st = await fs.statfs(bp);
1034
+ if (st.blocks <= 0) return 100;
1035
+ return st.bavail / st.blocks * 100;
1036
+ } catch {
1037
+ return 100;
1038
+ }
1039
+ }
1040
+ /** Total bytes on a location's volume via `statfs`; 0 when unstattable. */
1041
+ async locationVolumeTotalBytes(locationId) {
1042
+ const bp = this.locationBasePath(locationId);
1043
+ if (!bp) return 0;
1044
+ try {
1045
+ const st = await fs.statfs(bp);
1046
+ return st.blocks * st.bsize;
1047
+ } catch {
1048
+ return 0;
1049
+ }
1050
+ }
1051
+ /**
1052
+ * Seed default storage locations from the addon-declared
1053
+ * `storageLocations`, aggregated by the kernel into a
1054
+ * `StorageLocationRegistry`. Idempotent — re-running on a populated
1055
+ * map is a no-op (each declared id's `<id>:default` is already in the
1056
+ * service's map).
1057
+ *
1058
+ * `basePath` is the filesystem storage root, falling back to
1059
+ * `process.env.CAMSTACK_DATA ?? path.resolve(process.cwd(), 'camstack-data')`.
1060
+ * Every declared default is attributed to the `filesystem-storage`
1061
+ * provider; operators repoint individual locations (or add remote
1062
+ * SFTP/S3/WebDAV instances) through the admin UI afterwards.
1063
+ */
1064
+ async seedFromDeclarations() {
1065
+ if (!this.service) return;
1066
+ await this.refreshNodeLocalCache();
1067
+ const registry = buildStorageLocationRegistry(this.ctx.kernel.listStorageLocationDeclarations?.() ?? []);
1068
+ this.service.setRegistry(registry);
1069
+ this.service.pruneUndeclaredSystemLocations();
1070
+ const basePath = process.env["CAMSTACK_DATA"] ?? path$1.resolve(process.cwd(), "camstack-data");
1071
+ this.service.seedDefaults({
1072
+ providerId: "filesystem-storage",
1073
+ basePath,
1074
+ declarations: registry.list()
1075
+ });
1076
+ await this.service.backfillNodeIds(HUB_NODE_ID);
1077
+ }
1078
+ /**
1079
+ * Rebuild the `providerId → nodeLocal` cache from every registered
1080
+ * `storage-provider`'s `getProviderInfo()`. Cheap registry scan; runs on
1081
+ * boot and on every provider-registered reconcile. Failed probes are
1082
+ * skipped (the provider stays absent → treated as node-agnostic).
1083
+ */
1084
+ async refreshNodeLocalCache() {
1085
+ const reg = this.ctx.kernel.capabilityRegistry;
1086
+ if (!reg) return;
1087
+ const infos = await collectProviderInfos(reg.getCollection(STORAGE_PROVIDER_CAP_NAME), (_p, err, index) => {
1088
+ this.ctx.logger.warn("storage-orchestrator: getProviderInfo failed (nodeLocal cache)", { meta: {
1089
+ providerIndex: index,
1090
+ error: err instanceof Error ? err.message : String(err)
1091
+ } });
1092
+ });
1093
+ for (const info of infos) this.nodeLocalByProvider.set(info.providerId, info.nodeLocal);
1094
+ }
1095
+ async resolveByUploadId(uploadId) {
1096
+ const providerId = this.uploadOwners.get(uploadId);
1097
+ if (!providerId) throw new Error(`Unknown uploadId "${uploadId}" — orchestrator has no record of beginUpload`);
1098
+ return this.providerByProviderId(providerId, `uploadId "${uploadId}"`);
1099
+ }
1100
+ async resolveByDownloadId(downloadId) {
1101
+ const providerId = this.downloadOwners.get(downloadId);
1102
+ if (!providerId) throw new Error(`Unknown downloadId "${downloadId}" — orchestrator has no record of beginDownload`);
1103
+ return this.providerByProviderId(providerId, `downloadId "${downloadId}"`);
1104
+ }
1105
+ /**
1106
+ * Look up a `storage-provider` by `providerId` only — used for
1107
+ * upload / download dispatch where the original `StorageLocation` is
1108
+ * no longer in scope. Throws if the provider has gone away (e.g.
1109
+ * addon unloaded mid-session).
1110
+ */
1111
+ async providerByProviderId(providerId, sessionDescriptor) {
1112
+ if (!this.service) throw new Error("storage-orchestrator: service not initialized");
1113
+ const probe = {
1114
+ id: `__session:${sessionDescriptor}`,
1115
+ type: "data",
1116
+ displayName: sessionDescriptor,
1117
+ providerId,
1118
+ config: {},
1119
+ isDefault: false,
1120
+ isSystem: false,
1121
+ createdAt: 0,
1122
+ updatedAt: 0
1123
+ };
1124
+ return this.service.getProviderFor(probe);
1125
+ }
1126
+ };
1127
+ //#endregion
1128
+ export { StorageOrchestratorAddon, StorageOrchestratorAddon as default, StorageOrchestratorService as n, SqliteLocationStore as t };