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