@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,2229 @@
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ const require_chunk = require("../../chunk-Cek0wNdY.js");
6
+ const require_tar$1 = require("../../tar-BgAEMRBR.js");
7
+ let node_fs = require("node:fs");
8
+ node_fs = require_chunk.__toESM(node_fs);
9
+ let node_path = require("node:path");
10
+ node_path = require_chunk.__toESM(node_path);
11
+ let _camstack_types = require("@camstack/types");
12
+ let node_crypto = require("node:crypto");
13
+ let node_fs_promises = require("node:fs/promises");
14
+ node_fs_promises = require_chunk.__toESM(node_fs_promises);
15
+ let node_zlib = require("node:zlib");
16
+ node_zlib = require_chunk.__toESM(node_zlib);
17
+ //#region src/builtins/system-backup/system-backup.service.ts
18
+ var import_tar = /* @__PURE__ */ require_chunk.__toESM(require_tar$1.require_tar());
19
+ /**
20
+ * Default set of paths considered part of a "system backup". Includes
21
+ * everything that isn't pure backend code — config, sqlite, addons
22
+ * (runtime overlay), addons-data (per-addon private dir), TLS certs,
23
+ * auto-update marker.
24
+ *
25
+ * Excluded by default: recordings/, media/, models/, logs/, cache/
26
+ * (too large or fully recreatable).
27
+ */
28
+ var DEFAULT_BACKUP_LOCATIONS = [
29
+ "config.yaml",
30
+ "addons",
31
+ "addons-data",
32
+ "tls",
33
+ "auto-update.json"
34
+ ];
35
+ /** Path of the manifest file embedded inside every archive. */
36
+ var ARCHIVE_MANIFEST_NAME = ".camstack-backup-manifest.json";
37
+ /**
38
+ * System-level archive primitives. Construct one per server boot;
39
+ * methods are pure I/O.
40
+ */
41
+ var SystemBackupService = class {
42
+ dataDir;
43
+ logger;
44
+ constructor(dataDir, logger) {
45
+ this.dataDir = dataDir;
46
+ this.logger = logger;
47
+ }
48
+ /**
49
+ * Snapshot the current size + file count of each well-known
50
+ * location. Used by the admin UI to render an opt-in checklist
51
+ * before triggering a backup, and by `createArchive` to derive the
52
+ * embedded manifest.
53
+ */
54
+ statLocations(locations = DEFAULT_BACKUP_LOCATIONS) {
55
+ return locations.map((loc) => {
56
+ const abs = node_path.isAbsolute(loc) ? loc : node_path.join(this.dataDir, loc);
57
+ if (!node_fs.existsSync(abs)) return {
58
+ name: loc,
59
+ sizeBytes: 0,
60
+ fileCount: 0,
61
+ present: false
62
+ };
63
+ const walked = walkAbs(abs, abs);
64
+ return {
65
+ name: loc,
66
+ sizeBytes: walked.reduce((acc, e) => acc + e.sizeBytes, 0),
67
+ fileCount: walked.filter((e) => e.kind === "file").length,
68
+ present: true
69
+ };
70
+ });
71
+ }
72
+ /**
73
+ * Create a tar.gz of the given locations. Uses the `tar` npm package
74
+ * (used by npm itself) instead of shelling out — gives us:
75
+ *
76
+ * - Symlink dereference (`follow: true`) so dev mode's
77
+ * `addons/<name> → packages/<name>` overlay archives content,
78
+ * not just link stubs.
79
+ * - Programmatic walk → we build a manifest of every entry while
80
+ * copying so the archive can self-describe.
81
+ *
82
+ * Locations that don't exist are silently skipped. Throws if zero
83
+ * locations would be included.
84
+ */
85
+ async createArchive(opts) {
86
+ const requested = opts.locations ?? DEFAULT_BACKUP_LOCATIONS;
87
+ const present = [];
88
+ for (const loc of requested) {
89
+ const abs = node_path.isAbsolute(loc) ? loc : node_path.join(this.dataDir, loc);
90
+ if (node_fs.existsSync(abs)) present.push(loc);
91
+ else this.logger.debug("createArchive: skipping missing location", { meta: { location: loc } });
92
+ }
93
+ if (present.length === 0) throw new Error(`createArchive: no locations from {${requested.join(", ")}} exist under ${this.dataDir}`);
94
+ node_fs.mkdirSync(node_path.dirname(opts.archivePath), { recursive: true });
95
+ const entries = [];
96
+ for (const loc of present) {
97
+ const abs = node_path.join(this.dataDir, loc);
98
+ const stat = node_fs.statSync(abs);
99
+ if (stat.isDirectory()) for (const e of walkAbs(abs, this.dataDir)) entries.push(e);
100
+ else entries.push(toArchiveEntry(abs, this.dataDir, stat));
101
+ }
102
+ const manifest = {
103
+ archiveVersion: 1,
104
+ createdAt: Date.now(),
105
+ dataDir: this.dataDir,
106
+ locations: present,
107
+ entries,
108
+ totalBytes: entries.reduce((acc, e) => acc + e.sizeBytes, 0),
109
+ totalFiles: entries.filter((e) => e.kind === "file").length
110
+ };
111
+ const manifestStagePath = `${opts.archivePath}.${process.pid}.manifest.json`;
112
+ node_fs.writeFileSync(manifestStagePath, JSON.stringify(manifest, null, 2), "utf-8");
113
+ this.logger.info("createArchive: tarring", { meta: {
114
+ archivePath: opts.archivePath,
115
+ locations: present,
116
+ totalFiles: manifest.totalFiles,
117
+ totalBytes: manifest.totalBytes
118
+ } });
119
+ try {
120
+ const stagedInDataDir = node_path.join(this.dataDir, ARCHIVE_MANIFEST_NAME);
121
+ node_fs.copyFileSync(manifestStagePath, stagedInDataDir);
122
+ try {
123
+ await import_tar.create({
124
+ file: opts.archivePath,
125
+ gzip: true,
126
+ cwd: this.dataDir,
127
+ follow: true,
128
+ portable: true
129
+ }, [ARCHIVE_MANIFEST_NAME, ...present]);
130
+ } finally {
131
+ try {
132
+ node_fs.rmSync(stagedInDataDir, { force: true });
133
+ } catch {}
134
+ }
135
+ } finally {
136
+ try {
137
+ node_fs.rmSync(manifestStagePath, { force: true });
138
+ } catch {}
139
+ }
140
+ const sizeBytes = node_fs.statSync(opts.archivePath).size;
141
+ return {
142
+ archivePath: opts.archivePath,
143
+ includedLocations: present,
144
+ sizeBytes,
145
+ manifest
146
+ };
147
+ }
148
+ /**
149
+ * Read the embedded manifest from a previously-created archive
150
+ * without extracting payload files. Streams the tar.gz, parses the
151
+ * one entry we care about, then aborts.
152
+ */
153
+ async readArchiveManifest(archivePath) {
154
+ if (!node_fs.existsSync(archivePath)) throw new Error(`readArchiveManifest: ${archivePath} does not exist`);
155
+ return new Promise((resolve, reject) => {
156
+ const chunks = [];
157
+ let found = false;
158
+ let resolved = false;
159
+ const parser = new import_tar.Parse({ onentry: (entry) => {
160
+ if (entry.path === ".camstack-backup-manifest.json" && !found) {
161
+ found = true;
162
+ entry.on("data", (chunk) => chunks.push(chunk));
163
+ entry.on("end", () => {
164
+ try {
165
+ const json = Buffer.concat(chunks).toString("utf-8");
166
+ const parsed = JSON.parse(json);
167
+ resolved = true;
168
+ resolve(parsed);
169
+ } catch (err) {
170
+ resolved = true;
171
+ reject(err instanceof Error ? err : new Error(String(err)));
172
+ }
173
+ });
174
+ } else entry.resume();
175
+ } });
176
+ parser.on("end", () => {
177
+ if (!resolved) resolve(found ? null : null);
178
+ });
179
+ const gunzip = node_zlib.createGunzip();
180
+ gunzip.on("error", (err) => {
181
+ if (!resolved) {
182
+ resolved = true;
183
+ reject(err);
184
+ }
185
+ });
186
+ const reader = node_fs.createReadStream(archivePath);
187
+ reader.on("error", (err) => {
188
+ if (!resolved) {
189
+ resolved = true;
190
+ reject(err);
191
+ }
192
+ });
193
+ reader.pipe(gunzip).pipe(parser);
194
+ });
195
+ }
196
+ /**
197
+ * Extract a previously-created archive over `targetDir`. Used at boot
198
+ * by the restore hook — never call from a running server (would
199
+ * corrupt sqlite mid-run).
200
+ *
201
+ * `locations` is an optional whitelist: only entries whose path
202
+ * starts with one of the locations get extracted. Used by the
203
+ * partial-restore flow where the operator picks a subset of the
204
+ * archive contents to apply (e.g. "restore my devices but keep
205
+ * my current TLS certs").
206
+ */
207
+ async extractArchive(opts) {
208
+ if (!node_fs.existsSync(opts.archivePath)) throw new Error(`extractArchive: ${opts.archivePath} does not exist`);
209
+ node_fs.mkdirSync(opts.targetDir, { recursive: true });
210
+ this.logger.info("extractArchive: untarring", { meta: {
211
+ archivePath: opts.archivePath,
212
+ targetDir: opts.targetDir,
213
+ locations: opts.locations
214
+ } });
215
+ const allowedPrefixes = opts.locations ? opts.locations.map(normalizeLocation) : null;
216
+ await import_tar.extract({
217
+ file: opts.archivePath,
218
+ cwd: opts.targetDir,
219
+ filter: (p) => {
220
+ if (p === ".camstack-backup-manifest.json" || p === `./.camstack-backup-manifest.json`) return false;
221
+ if (!allowedPrefixes) return true;
222
+ const normalized = p.replace(/^\.\//, "");
223
+ return allowedPrefixes.some((prefix) => entryMatchesLocation(normalized, prefix));
224
+ }
225
+ });
226
+ }
227
+ /**
228
+ * Schedule a restore for the next boot by writing a marker file.
229
+ * `locations` (optional) carries through to the boot-time
230
+ * extractArchive call so the operator's "restore only X and Y"
231
+ * choice survives the process restart.
232
+ */
233
+ scheduleRestoreMarker(opts) {
234
+ if (!node_fs.existsSync(opts.archivePath)) throw new Error(`scheduleRestoreMarker: ${opts.archivePath} missing`);
235
+ const markerPath = this.getRestoreMarkerPath();
236
+ const marker = {
237
+ archivePath: opts.archivePath,
238
+ requestedAt: Date.now(),
239
+ source: opts.source,
240
+ ...opts.locations ? { locations: [...opts.locations] } : {}
241
+ };
242
+ node_fs.mkdirSync(node_path.dirname(markerPath), { recursive: true });
243
+ node_fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2), { mode: 384 });
244
+ return markerPath;
245
+ }
246
+ /** Read the pending-restore marker if one is present. */
247
+ readPendingMarker() {
248
+ const markerPath = this.getRestoreMarkerPath();
249
+ if (!node_fs.existsSync(markerPath)) return null;
250
+ try {
251
+ const raw = node_fs.readFileSync(markerPath, "utf-8");
252
+ const parsed = JSON.parse(raw);
253
+ if (typeof parsed === "object" && parsed != null && typeof parsed.archivePath === "string") return parsed;
254
+ } catch (err) {
255
+ this.logger.warn("readPendingMarker: malformed marker — ignoring", { meta: { error: err instanceof Error ? err.message : String(err) } });
256
+ }
257
+ return null;
258
+ }
259
+ /** Drop the marker after a successful boot-time restore. */
260
+ clearPendingMarker() {
261
+ const markerPath = this.getRestoreMarkerPath();
262
+ if (node_fs.existsSync(markerPath)) node_fs.rmSync(markerPath, { force: true });
263
+ }
264
+ getRestoreMarkerPath() {
265
+ return node_path.join(this.dataDir, ".pending-restore.json");
266
+ }
267
+ };
268
+ /**
269
+ * Recursively walk a directory and produce one `ArchiveEntry` per
270
+ * filesystem node. `dataDir` is used to compute the archive-relative
271
+ * path. Symlinks are dereferenced (matches the `follow: true` flag we
272
+ * pass to tar.create).
273
+ */
274
+ function walkAbs(absRoot, dataDir) {
275
+ const out = [];
276
+ walkRec(absRoot, dataDir, out);
277
+ return out;
278
+ }
279
+ function walkRec(abs, dataDir, out) {
280
+ let stat;
281
+ try {
282
+ stat = node_fs.statSync(abs);
283
+ } catch {
284
+ return;
285
+ }
286
+ out.push(toArchiveEntry(abs, dataDir, stat));
287
+ if (stat.isDirectory()) {
288
+ let children;
289
+ try {
290
+ children = node_fs.readdirSync(abs);
291
+ } catch {
292
+ return;
293
+ }
294
+ for (const child of children) walkRec(node_path.join(abs, child), dataDir, out);
295
+ }
296
+ }
297
+ function toArchiveEntry(abs, dataDir, stat) {
298
+ const rel = node_path.relative(dataDir, abs).replace(/\\/g, "/");
299
+ const kind = stat.isDirectory() ? "dir" : stat.isSymbolicLink() ? "symlink" : "file";
300
+ return {
301
+ path: rel || node_path.basename(abs),
302
+ kind,
303
+ sizeBytes: kind === "file" ? stat.size : 0,
304
+ mtime: stat.mtimeMs
305
+ };
306
+ }
307
+ /**
308
+ * Strip leading `./` and trailing `/` so location prefix matching is
309
+ * order-of-comparison invariant. Same shape on both sides of the
310
+ * `entry.path.startsWith(prefix)` test in the extract filter.
311
+ */
312
+ function normalizeLocation(loc) {
313
+ return loc.replace(/^\.\//, "").replace(/\/+$/, "");
314
+ }
315
+ /**
316
+ * Match an archive entry path against a location whitelist prefix.
317
+ * `addons` matches `addons` (file) and `addons/foo/bar.txt` (path
318
+ * inside the dir). The trailing `/` test is what prevents `addons`
319
+ * from spuriously matching `addons-data`.
320
+ */
321
+ function entryMatchesLocation(entryPath, prefix) {
322
+ return entryPath === prefix || entryPath.startsWith(`${prefix}/`);
323
+ }
324
+ //#endregion
325
+ //#region src/builtins/backup-orchestrator/destination-policy.ts
326
+ var TABLE_NAME = "backup_destination_policies";
327
+ var BACKUP_DESTINATION_POLICIES_SCHEMA = { columns: [
328
+ {
329
+ name: "location_id",
330
+ type: "TEXT",
331
+ primaryKey: true
332
+ },
333
+ {
334
+ name: "enabled",
335
+ type: "INTEGER",
336
+ notNull: true,
337
+ defaultValue: 1
338
+ },
339
+ {
340
+ name: "retention_count",
341
+ type: "INTEGER",
342
+ notNull: true,
343
+ defaultValue: 7
344
+ },
345
+ {
346
+ name: "label",
347
+ type: "TEXT"
348
+ },
349
+ {
350
+ name: "cron",
351
+ type: "TEXT"
352
+ },
353
+ {
354
+ name: "last_run_at",
355
+ type: "INTEGER"
356
+ }
357
+ ] };
358
+ var BackupDestinationPolicyService = class {
359
+ backend;
360
+ tableEnsured = false;
361
+ constructor(backend) {
362
+ this.backend = backend;
363
+ }
364
+ /**
365
+ * Lazy `ensureTable` — called from every read/write path. The
366
+ * settings backend's own `ensureTable` is idempotent (additive
367
+ * migration), so repeated calls collapse to a no-op after the first.
368
+ * This keeps the constructor synchronous and avoids a separate
369
+ * `initialize()` step that callers could forget.
370
+ */
371
+ async initialize() {
372
+ await this.ensureTable();
373
+ }
374
+ async ensureTable() {
375
+ if (this.tableEnsured) return;
376
+ if (!this.backend.ensureTable) throw new Error("BackupDestinationPolicyService: settings backend does not implement ensureTable — expected the SQLite settings backend, got a backend without structured-table support");
377
+ await this.backend.ensureTable(TABLE_NAME, BACKUP_DESTINATION_POLICIES_SCHEMA);
378
+ this.tableEnsured = true;
379
+ }
380
+ async list() {
381
+ await this.ensureTable();
382
+ if (!this.backend.tableQuery) return [];
383
+ const rows = await this.backend.tableQuery(TABLE_NAME, { orderBy: {
384
+ field: "location_id",
385
+ direction: "asc"
386
+ } });
387
+ const out = [];
388
+ for (const row of rows) {
389
+ const mapped = mapRowToPolicy(row);
390
+ if (mapped) out.push(mapped);
391
+ }
392
+ return out;
393
+ }
394
+ async get(locationId) {
395
+ await this.ensureTable();
396
+ if (!this.backend.tableGet) return null;
397
+ const row = await this.backend.tableGet(TABLE_NAME, { location_id: locationId });
398
+ return row ? mapRowToPolicy(row) : null;
399
+ }
400
+ /**
401
+ * Replace-or-insert. Implemented as `delete` + `insert` (mirrors the
402
+ * `SqliteLocationStore` contract) so backends that don't surface
403
+ * `tableUpdate` still work.
404
+ */
405
+ async upsert(p) {
406
+ await this.ensureTable();
407
+ if (!this.backend.tableDelete || !this.backend.tableInsert) throw new Error("BackupDestinationPolicyService: backend missing tableDelete/tableInsert");
408
+ await this.backend.tableDelete(TABLE_NAME, { location_id: p.locationId });
409
+ await this.backend.tableInsert(TABLE_NAME, mapPolicyToRow(p));
410
+ }
411
+ async delete(locationId) {
412
+ await this.ensureTable();
413
+ if (!this.backend.tableDelete) throw new Error("BackupDestinationPolicyService: backend missing tableDelete");
414
+ await this.backend.tableDelete(TABLE_NAME, { location_id: locationId });
415
+ }
416
+ /**
417
+ * Auto-create a default policy for a `backups` location seen for the
418
+ * first time (returns the existing policy unchanged when present).
419
+ * The orchestrator calls this lazily on the first `listDestinations`
420
+ * pass so newly-added storage locations are surfaced as enabled
421
+ * destinations without an explicit configuration step.
422
+ *
423
+ * Defaults are passed by the orchestrator (currently:
424
+ * `retentionCount=7` + `cron='0 3 * * *'`) so a fresh destination
425
+ * starts running daily at 03:00 with 7 archives kept. Operator can
426
+ * tune both inline on the destinations table.
427
+ */
428
+ async ensureDefault(locationId, defaults) {
429
+ const existing = await this.get(locationId);
430
+ if (existing) return existing;
431
+ const fresh = {
432
+ locationId,
433
+ enabled: true,
434
+ retentionCount: defaults.retentionCount,
435
+ ...defaults.cron !== void 0 ? { cron: defaults.cron } : {}
436
+ };
437
+ await this.upsert(fresh);
438
+ return fresh;
439
+ }
440
+ };
441
+ function mapPolicyToRow(p) {
442
+ return {
443
+ location_id: p.locationId,
444
+ enabled: p.enabled ? 1 : 0,
445
+ retention_count: p.retentionCount,
446
+ label: p.label ?? null,
447
+ cron: p.cron ?? null,
448
+ last_run_at: p.lastRunAt ?? null
449
+ };
450
+ }
451
+ /**
452
+ * Decode a row pulled from the structured table. Defensive against
453
+ * schema drift / partial migrations: returns `null` on missing
454
+ * `location_id` so corrupt rows skip the result list rather than
455
+ * throwing the whole orchestrator down at boot.
456
+ */
457
+ function mapRowToPolicy(row) {
458
+ const locationId = String(row["location_id"] ?? "");
459
+ if (!locationId) return null;
460
+ const labelRaw = row["label"];
461
+ const label = typeof labelRaw === "string" && labelRaw.length > 0 ? labelRaw : void 0;
462
+ const cronRaw = row["cron"];
463
+ const cron = typeof cronRaw === "string" && cronRaw.length > 0 ? cronRaw : void 0;
464
+ const lastRunRaw = row["last_run_at"];
465
+ const lastRunAt = typeof lastRunRaw === "number" && lastRunRaw > 0 ? lastRunRaw : void 0;
466
+ return {
467
+ locationId,
468
+ enabled: row["enabled"] === 1 || row["enabled"] === true,
469
+ retentionCount: Number(row["retention_count"] ?? 0),
470
+ ...label !== void 0 ? { label } : {},
471
+ ...cron !== void 0 ? { cron } : {},
472
+ ...lastRunAt !== void 0 ? { lastRunAt } : {}
473
+ };
474
+ }
475
+ //#endregion
476
+ //#region src/builtins/backup-orchestrator/manifest-store.ts
477
+ /** File path (relative to the location root) where the index lives. */
478
+ var MANIFESTS_FILENAME = "manifests.json";
479
+ /**
480
+ * Read the per-location manifest. Returns an empty manifest on first
481
+ * use (when `manifests.json` doesn't exist) or when the file is
482
+ * unreadable / corrupt — operators see destination state degrade
483
+ * gracefully rather than the orchestrator crashing.
484
+ */
485
+ async function readLocationManifest(api, locationId) {
486
+ if (!await api.exists.query({
487
+ location: locationId,
488
+ relativePath: "manifests.json"
489
+ }).catch(() => false)) return EMPTY_MANIFEST;
490
+ let bytes;
491
+ try {
492
+ bytes = await api.read.query({
493
+ location: locationId,
494
+ relativePath: MANIFESTS_FILENAME
495
+ });
496
+ } catch {
497
+ return EMPTY_MANIFEST;
498
+ }
499
+ return parseManifestBytes(bytes);
500
+ }
501
+ async function writeLocationManifest(api, locationId, manifest) {
502
+ const json = JSON.stringify(manifest, null, 2);
503
+ const bytes = new TextEncoder().encode(json);
504
+ await api.write.mutate({
505
+ location: locationId,
506
+ relativePath: MANIFESTS_FILENAME,
507
+ data: bytes
508
+ });
509
+ }
510
+ /**
511
+ * Decode the raw bytes of `manifests.json`. Falls back to an empty
512
+ * manifest on JSON-parse / shape-validation failure — same defensive
513
+ * stance as the legacy `LocalBackupService.ensureManifestsLoaded`.
514
+ */
515
+ function parseManifestBytes(bytes) {
516
+ try {
517
+ const text = new TextDecoder().decode(bytes);
518
+ if (text.trim().length === 0) return EMPTY_MANIFEST;
519
+ return normalizeManifest(JSON.parse(text));
520
+ } catch {
521
+ return EMPTY_MANIFEST;
522
+ }
523
+ }
524
+ /**
525
+ * Coerce an arbitrary parsed JSON shape into the canonical manifest
526
+ * type. Bogus archive entries are silently dropped (a corrupt row
527
+ * shouldn't lock the whole location out of operator visibility).
528
+ */
529
+ function normalizeManifest(raw) {
530
+ if (typeof raw !== "object" || raw == null) return EMPTY_MANIFEST;
531
+ const obj = raw;
532
+ const archives = [];
533
+ if (Array.isArray(obj.archives)) for (const a of obj.archives) {
534
+ if (typeof a !== "object" || a == null) continue;
535
+ const cand = a;
536
+ if (typeof cand["id"] !== "string" || typeof cand["filename"] !== "string") continue;
537
+ if (typeof cand["createdAt"] !== "number" || typeof cand["sizeBytes"] !== "number") continue;
538
+ const manifest = cand["manifest"];
539
+ if (typeof manifest !== "object" || manifest == null) continue;
540
+ const entry = {
541
+ id: String(cand["id"]),
542
+ filename: String(cand["filename"]),
543
+ createdAt: Number(cand["createdAt"]),
544
+ sizeBytes: Number(cand["sizeBytes"]),
545
+ ...typeof cand["label"] === "string" ? { label: cand["label"] } : {},
546
+ manifest
547
+ };
548
+ archives.push(entry);
549
+ }
550
+ return {
551
+ version: 1,
552
+ archives
553
+ };
554
+ }
555
+ var EMPTY_MANIFEST = {
556
+ version: 1,
557
+ archives: []
558
+ };
559
+ /**
560
+ * Build a `ManifestArchiveEntry` from the raw inputs collected during
561
+ * a successful upload. Pure transform — no I/O — kept here so the
562
+ * triggerBackup path stays terse.
563
+ */
564
+ function buildArchiveEntry(input) {
565
+ return {
566
+ id: input.id,
567
+ filename: input.filename,
568
+ createdAt: input.createdAt,
569
+ sizeBytes: input.sizeBytes,
570
+ ...input.label !== void 0 ? { label: input.label } : {},
571
+ manifest: input.manifest
572
+ };
573
+ }
574
+ /**
575
+ * Pure retention split: given an archive list and the operator's
576
+ * `retentionCount`, return `{ keep, drop }` where `keep` is the most-
577
+ * recent N archives (newest first) and `drop` is the older overflow.
578
+ *
579
+ * `retentionCount <= 0` is treated as "no pruning" — defensive against
580
+ * a config value that would otherwise wipe every archive on the next
581
+ * write.
582
+ */
583
+ function applyRetention(archives, retentionCount) {
584
+ if (retentionCount <= 0) return {
585
+ keep: archives,
586
+ drop: []
587
+ };
588
+ const sorted = [...archives].toSorted((a, b) => b.createdAt - a.createdAt);
589
+ if (sorted.length <= retentionCount) return {
590
+ keep: sorted,
591
+ drop: []
592
+ };
593
+ return {
594
+ keep: sorted.slice(0, retentionCount),
595
+ drop: sorted.slice(retentionCount)
596
+ };
597
+ }
598
+ /**
599
+ * Per-location async mutex. The orchestrator owns one instance and
600
+ * funnels every read-modify-write of `manifests.json` through it so
601
+ * parallel destination uploads (within the same location) commit in
602
+ * arrival order. Across locations the operations run in parallel —
603
+ * each location keys its own promise chain.
604
+ */
605
+ var LocationManifestLock = class {
606
+ chains = /* @__PURE__ */ new Map();
607
+ async run(locationId, fn) {
608
+ const previous = this.chains.get(locationId) ?? Promise.resolve();
609
+ let resolveNext;
610
+ const next = new Promise((res) => {
611
+ resolveNext = res;
612
+ });
613
+ this.chains.set(locationId, previous.then(() => next));
614
+ try {
615
+ await previous;
616
+ return await fn();
617
+ } finally {
618
+ resolveNext();
619
+ const tail = this.chains.get(locationId);
620
+ if (tail) Promise.resolve(tail).then(() => {
621
+ if (this.chains.get(locationId) === tail) this.chains.delete(locationId);
622
+ }).catch(() => {});
623
+ }
624
+ }
625
+ };
626
+ //#endregion
627
+ //#region src/builtins/backup-orchestrator/download-helpers.ts
628
+ /**
629
+ * Chunked-download helper for the backup orchestrator (Task 12 of
630
+ * the storage-unification refactor).
631
+ *
632
+ * Streams an archive from a destination `StorageLocation` into a
633
+ * local filesystem path via the consumer-facing `storage` cap. Pulled
634
+ * out of `backup-orchestrator.addon.ts` so it can be unit-tested with
635
+ * an in-memory fake `AddonApi['storage']` slice — the addon is hard
636
+ * to bootstrap in vitest because of the BaseAddon ceremony.
637
+ *
638
+ * Cleanup contract:
639
+ * - Successful run leaves the cache file in place + calls
640
+ * `endDownload`.
641
+ * - Mid-stream failure destroys the writeStream, removes the
642
+ * partial cache file, and best-effort calls `endDownload` so the
643
+ * storage provider's session bookkeeping doesn't leak.
644
+ */
645
+ async function downloadArchiveToCache(input) {
646
+ const { downloadId, sizeBytes } = await input.api.beginDownload.mutate({
647
+ location: input.locationId,
648
+ relativePath: input.filename
649
+ });
650
+ const writeStream = node_fs.createWriteStream(input.cachePath);
651
+ let offset = 0;
652
+ try {
653
+ while (offset < sizeBytes) {
654
+ const remaining = sizeBytes - offset;
655
+ const length = Math.min(input.chunkBytes, remaining);
656
+ const chunk = await input.api.readChunk.query({
657
+ downloadId,
658
+ offset,
659
+ length
660
+ });
661
+ if (chunk.byteLength === 0) throw new Error(`backup restore: empty chunk at offset ${offset}/${sizeBytes} from "${input.locationId}/${input.filename}"`);
662
+ writeStream.write(chunk);
663
+ offset += chunk.byteLength;
664
+ }
665
+ await new Promise((resolve, reject) => {
666
+ writeStream.end((err) => {
667
+ if (err) reject(err);
668
+ else resolve();
669
+ });
670
+ });
671
+ try {
672
+ await input.api.endDownload.mutate({ downloadId });
673
+ } catch (err) {
674
+ input.logger.warn("backup restore: endDownload failed (ignoring)", { meta: {
675
+ downloadId,
676
+ error: err instanceof Error ? err.message : String(err)
677
+ } });
678
+ }
679
+ } catch (err) {
680
+ await new Promise((resolve) => {
681
+ writeStream.once("close", () => resolve());
682
+ writeStream.destroy();
683
+ });
684
+ try {
685
+ await node_fs_promises.rm(input.cachePath, { force: true });
686
+ } catch {}
687
+ try {
688
+ await input.api.endDownload.mutate({ downloadId });
689
+ } catch {}
690
+ throw err;
691
+ }
692
+ }
693
+ //#endregion
694
+ //#region ../../node_modules/croner/dist/croner.js
695
+ function T(s) {
696
+ return Date.UTC(s.y, s.m - 1, s.d, s.h, s.i, s.s);
697
+ }
698
+ function D(s, e) {
699
+ return s.y === e.y && s.m === e.m && s.d === e.d && s.h === e.h && s.i === e.i && s.s === e.s;
700
+ }
701
+ function A(s, e) {
702
+ let t = new Date(Date.parse(s));
703
+ if (isNaN(t)) throw new Error("Invalid ISO8601 passed to timezone parser.");
704
+ let r = s.substring(9);
705
+ return r.includes("Z") || r.includes("+") || r.includes("-") ? b(t.getUTCFullYear(), t.getUTCMonth() + 1, t.getUTCDate(), t.getUTCHours(), t.getUTCMinutes(), t.getUTCSeconds(), "Etc/UTC") : b(t.getFullYear(), t.getMonth() + 1, t.getDate(), t.getHours(), t.getMinutes(), t.getSeconds(), e);
706
+ }
707
+ function v(s, e, t) {
708
+ return k(A(s, e), t);
709
+ }
710
+ function k(s, e) {
711
+ let t = new Date(T(s)), r = g(t, s.tz), a = T(s) - T(r), o = new Date(t.getTime() + a), h = g(o, s.tz);
712
+ if (D(h, s)) {
713
+ let u = /* @__PURE__ */ new Date(o.getTime() - 36e5);
714
+ return D(g(u, s.tz), s) ? u : o;
715
+ }
716
+ let l = new Date(o.getTime() + T(s) - T(h));
717
+ if (D(g(l, s.tz), s)) return l;
718
+ if (e) throw new Error("Invalid date passed to fromTZ()");
719
+ return o.getTime() > l.getTime() ? o : l;
720
+ }
721
+ function g(s, e) {
722
+ let t, r;
723
+ try {
724
+ t = new Intl.DateTimeFormat("en-US", {
725
+ timeZone: e,
726
+ year: "numeric",
727
+ month: "numeric",
728
+ day: "numeric",
729
+ hour: "numeric",
730
+ minute: "numeric",
731
+ second: "numeric",
732
+ hour12: !1
733
+ }), r = t.formatToParts(s);
734
+ } catch (i) {
735
+ let a = i instanceof Error ? i.message : String(i);
736
+ throw new RangeError(`toTZ: Invalid timezone '${e}' or date. Please provide a valid IANA timezone (e.g., 'America/New_York', 'Europe/Stockholm'). Original error: ${a}`);
737
+ }
738
+ let n = {
739
+ year: 0,
740
+ month: 0,
741
+ day: 0,
742
+ hour: 0,
743
+ minute: 0,
744
+ second: 0
745
+ };
746
+ for (let i of r) (i.type === "year" || i.type === "month" || i.type === "day" || i.type === "hour" || i.type === "minute" || i.type === "second") && (n[i.type] = parseInt(i.value, 10));
747
+ if (isNaN(n.year) || isNaN(n.month) || isNaN(n.day) || isNaN(n.hour) || isNaN(n.minute) || isNaN(n.second)) throw new Error(`toTZ: Failed to parse all date components from timezone '${e}'. This may indicate an invalid date or timezone configuration. Parsed components: ${JSON.stringify(n)}`);
748
+ return n.hour === 24 && (n.hour = 0), {
749
+ y: n.year,
750
+ m: n.month,
751
+ d: n.day,
752
+ h: n.hour,
753
+ i: n.minute,
754
+ s: n.second,
755
+ tz: e
756
+ };
757
+ }
758
+ function b(s, e, t, r, n, i, a) {
759
+ return {
760
+ y: s,
761
+ m: e,
762
+ d: t,
763
+ h: r,
764
+ i: n,
765
+ s: i,
766
+ tz: a
767
+ };
768
+ }
769
+ var O = [
770
+ 1,
771
+ 2,
772
+ 4,
773
+ 8,
774
+ 16
775
+ ], C = class {
776
+ pattern;
777
+ timezone;
778
+ mode;
779
+ alternativeWeekdays;
780
+ sloppyRanges;
781
+ second;
782
+ minute;
783
+ hour;
784
+ day;
785
+ month;
786
+ dayOfWeek;
787
+ year;
788
+ lastDayOfMonth;
789
+ lastWeekday;
790
+ nearestWeekdays;
791
+ starDOM;
792
+ starDOW;
793
+ starYear;
794
+ useAndLogic;
795
+ constructor(e, t, r) {
796
+ this.pattern = e, this.timezone = t, this.mode = r?.mode ?? "auto", this.alternativeWeekdays = r?.alternativeWeekdays ?? !1, this.sloppyRanges = r?.sloppyRanges ?? !1, this.second = Array(60).fill(0), this.minute = Array(60).fill(0), this.hour = Array(24).fill(0), this.day = Array(31).fill(0), this.month = Array(12).fill(0), this.dayOfWeek = Array(7).fill(0), this.year = Array(1e4).fill(0), this.lastDayOfMonth = !1, this.lastWeekday = !1, this.nearestWeekdays = Array(31).fill(0), this.starDOM = !1, this.starDOW = !1, this.starYear = !1, this.useAndLogic = !1, this.parse();
797
+ }
798
+ parse() {
799
+ if (!(typeof this.pattern == "string" || this.pattern instanceof String)) throw new TypeError("CronPattern: Pattern has to be of type string.");
800
+ this.pattern.indexOf("@") >= 0 && (this.pattern = this.handleNicknames(this.pattern).trim());
801
+ let e = this.pattern.match(/\S+/g) || [""], t = e.length;
802
+ if (e.length < 5 || e.length > 7) throw new TypeError("CronPattern: invalid configuration format ('" + this.pattern + "'), exactly five, six, or seven space separated parts are required.");
803
+ if (this.mode !== "auto") {
804
+ let n;
805
+ switch (this.mode) {
806
+ case "5-part":
807
+ n = 5;
808
+ break;
809
+ case "6-part":
810
+ n = 6;
811
+ break;
812
+ case "7-part":
813
+ n = 7;
814
+ break;
815
+ case "5-or-6-parts":
816
+ n = [5, 6];
817
+ break;
818
+ case "6-or-7-parts":
819
+ n = [6, 7];
820
+ break;
821
+ default: n = 0;
822
+ }
823
+ if (!(Array.isArray(n) ? n.includes(t) : t === n)) {
824
+ let a = Array.isArray(n) ? n.join(" or ") : n.toString();
825
+ throw new TypeError(`CronPattern: mode '${this.mode}' requires exactly ${a} parts, but pattern '${this.pattern}' has ${t} parts.`);
826
+ }
827
+ }
828
+ if (e.length === 5 && e.unshift("0"), e.length === 6 && e.push("*"), e[3].toUpperCase() === "LW" ? (this.lastWeekday = !0, e[3] = "") : e[3].toUpperCase().indexOf("L") >= 0 && (e[3] = e[3].replace(/L/gi, ""), this.lastDayOfMonth = !0), e[3] == "*" && (this.starDOM = !0), e[6] == "*" && (this.starYear = !0), e[4].length >= 3 && (e[4] = this.replaceAlphaMonths(e[4])), e[5].length >= 3 && (e[5] = this.alternativeWeekdays ? this.replaceAlphaDaysQuartz(e[5]) : this.replaceAlphaDays(e[5])), e[5].startsWith("+") && (this.useAndLogic = !0, e[5] = e[5].substring(1), e[5] === "")) throw new TypeError("CronPattern: Day-of-week field cannot be empty after '+' modifier.");
829
+ switch (e[5] == "*" && (this.starDOW = !0), this.pattern.indexOf("?") >= 0 && (e[0] = e[0].replace(/\?/g, "*"), e[1] = e[1].replace(/\?/g, "*"), e[2] = e[2].replace(/\?/g, "*"), e[3] = e[3].replace(/\?/g, "*"), e[4] = e[4].replace(/\?/g, "*"), e[5] = e[5].replace(/\?/g, "*"), e[6] && (e[6] = e[6].replace(/\?/g, "*"))), this.mode) {
830
+ case "5-part":
831
+ e[0] = "0", e[6] = "*";
832
+ break;
833
+ case "6-part":
834
+ e[6] = "*";
835
+ break;
836
+ case "5-or-6-parts":
837
+ e[6] = "*";
838
+ break;
839
+ case "6-or-7-parts": break;
840
+ case "7-part":
841
+ case "auto": break;
842
+ }
843
+ this.throwAtIllegalCharacters(e), this.partToArray("second", e[0], 0, 1), this.partToArray("minute", e[1], 0, 1), this.partToArray("hour", e[2], 0, 1), this.partToArray("day", e[3], -1, 1), this.partToArray("month", e[4], -1, 1);
844
+ let r = this.alternativeWeekdays ? -1 : 0;
845
+ this.partToArray("dayOfWeek", e[5], r, 63), this.partToArray("year", e[6], 0, 1), !this.alternativeWeekdays && this.dayOfWeek[7] && (this.dayOfWeek[0] = this.dayOfWeek[7]);
846
+ }
847
+ partToArray(e, t, r, n) {
848
+ let i = this[e], a = e === "day" && this.lastDayOfMonth, o = e === "day" && this.lastWeekday;
849
+ if (t === "" && !a && !o) throw new TypeError("CronPattern: configuration entry " + e + " (" + t + ") is empty, check for trailing spaces.");
850
+ if (t === "*") return i.fill(n);
851
+ let h = t.split(",");
852
+ if (h.length > 1) for (let l = 0; l < h.length; l++) this.partToArray(e, h[l], r, n);
853
+ else t.indexOf("-") !== -1 && t.indexOf("/") !== -1 ? this.handleRangeWithStepping(t, e, r, n) : t.indexOf("-") !== -1 ? this.handleRange(t, e, r, n) : t.indexOf("/") !== -1 ? this.handleStepping(t, e, r, n) : t !== "" && this.handleNumber(t, e, r, n);
854
+ }
855
+ throwAtIllegalCharacters(e) {
856
+ for (let t = 0; t < e.length; t++) if ((t === 3 ? /[^/*0-9,\-WwLl]+/ : t === 5 ? /[^/*0-9,\-#Ll]+/ : /[^/*0-9,\-]+/).test(e[t])) throw new TypeError("CronPattern: configuration entry " + t + " (" + e[t] + ") contains illegal characters.");
857
+ }
858
+ handleNumber(e, t, r, n) {
859
+ let i = this.extractNth(e, t), a = e.toUpperCase().includes("W");
860
+ if (t !== "day" && a) throw new TypeError("CronPattern: Nearest weekday modifier (W) only allowed in day-of-month.");
861
+ a && (t = "nearestWeekdays");
862
+ let o = parseInt(i[0], 10) + r;
863
+ if (isNaN(o)) throw new TypeError("CronPattern: " + t + " is not a number: '" + e + "'");
864
+ this.setPart(t, o, i[1] || n);
865
+ }
866
+ setPart(e, t, r) {
867
+ if (!Object.prototype.hasOwnProperty.call(this, e)) throw new TypeError("CronPattern: Invalid part specified: " + e);
868
+ if (e === "dayOfWeek") {
869
+ if (t === 7 && (t = 0), t < 0 || t > 6) throw new RangeError("CronPattern: Invalid value for dayOfWeek: " + t);
870
+ this.setNthWeekdayOfMonth(t, r);
871
+ return;
872
+ }
873
+ if (e === "second" || e === "minute") {
874
+ if (t < 0 || t >= 60) throw new RangeError("CronPattern: Invalid value for " + e + ": " + t);
875
+ } else if (e === "hour") {
876
+ if (t < 0 || t >= 24) throw new RangeError("CronPattern: Invalid value for " + e + ": " + t);
877
+ } else if (e === "day" || e === "nearestWeekdays") {
878
+ if (t < 0 || t >= 31) throw new RangeError("CronPattern: Invalid value for " + e + ": " + t);
879
+ } else if (e === "month") {
880
+ if (t < 0 || t >= 12) throw new RangeError("CronPattern: Invalid value for " + e + ": " + t);
881
+ } else if (e === "year" && (t < 1 || t >= 1e4)) throw new RangeError("CronPattern: Invalid value for " + e + ": " + t + " (supported range: 1-9999)");
882
+ this[e][t] = r;
883
+ }
884
+ validateNotNaN(e, t) {
885
+ if (isNaN(e)) throw new TypeError(t);
886
+ }
887
+ validateRange(e, t, r, n, i) {
888
+ if (e > t) throw new TypeError("CronPattern: From value is larger than to value: '" + i + "'");
889
+ if (r !== void 0) {
890
+ if (r === 0) throw new TypeError("CronPattern: Syntax error, illegal stepping: 0");
891
+ if (r > this[n].length) throw new TypeError("CronPattern: Syntax error, steps cannot be greater than maximum value of part (" + this[n].length + ")");
892
+ }
893
+ }
894
+ handleRangeWithStepping(e, t, r, n) {
895
+ if (e.toUpperCase().includes("W")) throw new TypeError("CronPattern: Syntax error, W is not allowed in ranges with stepping.");
896
+ let i = this.extractNth(e, t), a = i[0].match(/^(\d+)-(\d+)\/(\d+)$/);
897
+ if (a === null) throw new TypeError("CronPattern: Syntax error, illegal range with stepping: '" + e + "'");
898
+ let [, o, h, l] = a, y = parseInt(o, 10) + r, u = parseInt(h, 10) + r, d = parseInt(l, 10);
899
+ this.validateNotNaN(y, "CronPattern: Syntax error, illegal lower range (NaN)"), this.validateNotNaN(u, "CronPattern: Syntax error, illegal upper range (NaN)"), this.validateNotNaN(d, "CronPattern: Syntax error, illegal stepping: (NaN)"), this.validateRange(y, u, d, t, e);
900
+ for (let c = y; c <= u; c += d) this.setPart(t, c, i[1] || n);
901
+ }
902
+ extractNth(e, t) {
903
+ let r = e, n;
904
+ if (r.includes("#")) {
905
+ if (t !== "dayOfWeek") throw new Error("CronPattern: nth (#) only allowed in day-of-week field");
906
+ n = r.split("#")[1], r = r.split("#")[0];
907
+ } else if (r.toUpperCase().endsWith("L")) {
908
+ if (t !== "dayOfWeek") throw new Error("CronPattern: L modifier only allowed in day-of-week field (use L alone for day-of-month)");
909
+ n = "L", r = r.slice(0, -1);
910
+ }
911
+ return [r, n];
912
+ }
913
+ handleRange(e, t, r, n) {
914
+ if (e.toUpperCase().includes("W")) throw new TypeError("CronPattern: Syntax error, W is not allowed in a range.");
915
+ let i = this.extractNth(e, t), a = i[0].split("-");
916
+ if (a.length !== 2) throw new TypeError("CronPattern: Syntax error, illegal range: '" + e + "'");
917
+ let o = parseInt(a[0], 10) + r, h = parseInt(a[1], 10) + r;
918
+ this.validateNotNaN(o, "CronPattern: Syntax error, illegal lower range (NaN)"), this.validateNotNaN(h, "CronPattern: Syntax error, illegal upper range (NaN)"), this.validateRange(o, h, void 0, t, e);
919
+ for (let l = o; l <= h; l++) this.setPart(t, l, i[1] || n);
920
+ }
921
+ handleStepping(e, t, r, n) {
922
+ if (e.toUpperCase().includes("W")) throw new TypeError("CronPattern: Syntax error, W is not allowed in parts with stepping.");
923
+ let i = this.extractNth(e, t), a = i[0].split("/");
924
+ if (a.length !== 2) throw new TypeError("CronPattern: Syntax error, illegal stepping: '" + e + "'");
925
+ if (this.sloppyRanges) a[0] === "" && (a[0] = "*");
926
+ else {
927
+ if (a[0] === "") throw new TypeError("CronPattern: Syntax error, stepping with missing prefix ('" + e + "') is not allowed. Use wildcard (*/step) or range (min-max/step) instead.");
928
+ if (a[0] !== "*") throw new TypeError("CronPattern: Syntax error, stepping with numeric prefix ('" + e + "') is not allowed. Use wildcard (*/step) or range (min-max/step) instead.");
929
+ }
930
+ let o = 0;
931
+ a[0] !== "*" && (o = parseInt(a[0], 10) + r);
932
+ let h = parseInt(a[1], 10);
933
+ this.validateNotNaN(h, "CronPattern: Syntax error, illegal stepping: (NaN)"), this.validateRange(0, this[t].length - 1, h, t, e);
934
+ for (let l = o; l < this[t].length; l += h) this.setPart(t, l, i[1] || n);
935
+ }
936
+ replaceAlphaDays(e) {
937
+ return e.replace(/-sun/gi, "-7").replace(/sun/gi, "0").replace(/mon/gi, "1").replace(/tue/gi, "2").replace(/wed/gi, "3").replace(/thu/gi, "4").replace(/fri/gi, "5").replace(/sat/gi, "6");
938
+ }
939
+ replaceAlphaDaysQuartz(e) {
940
+ return e.replace(/sun/gi, "1").replace(/mon/gi, "2").replace(/tue/gi, "3").replace(/wed/gi, "4").replace(/thu/gi, "5").replace(/fri/gi, "6").replace(/sat/gi, "7");
941
+ }
942
+ replaceAlphaMonths(e) {
943
+ return e.replace(/jan/gi, "1").replace(/feb/gi, "2").replace(/mar/gi, "3").replace(/apr/gi, "4").replace(/may/gi, "5").replace(/jun/gi, "6").replace(/jul/gi, "7").replace(/aug/gi, "8").replace(/sep/gi, "9").replace(/oct/gi, "10").replace(/nov/gi, "11").replace(/dec/gi, "12");
944
+ }
945
+ handleNicknames(e) {
946
+ let t = e.trim().toLowerCase();
947
+ if (t === "@yearly" || t === "@annually") return "0 0 1 1 *";
948
+ if (t === "@monthly") return "0 0 1 * *";
949
+ if (t === "@weekly") return "0 0 * * 0";
950
+ if (t === "@daily" || t === "@midnight") return "0 0 * * *";
951
+ if (t === "@hourly") return "0 * * * *";
952
+ if (t === "@reboot") throw new TypeError("CronPattern: @reboot is not supported in this environment. This is an event-based trigger that requires system startup detection.");
953
+ return e;
954
+ }
955
+ setNthWeekdayOfMonth(e, t) {
956
+ if (typeof t != "number" && t.toUpperCase() === "L") this.dayOfWeek[e] = this.dayOfWeek[e] | 32;
957
+ else if (t === 63) this.dayOfWeek[e] = 63;
958
+ else if (t < 6 && t > 0) this.dayOfWeek[e] = this.dayOfWeek[e] | O[t - 1];
959
+ else throw new TypeError(`CronPattern: nth weekday out of range, should be 1-5 or L. Value: ${t}, Type: ${typeof t}`);
960
+ }
961
+ };
962
+ var P = [
963
+ 31,
964
+ 28,
965
+ 31,
966
+ 30,
967
+ 31,
968
+ 30,
969
+ 31,
970
+ 31,
971
+ 30,
972
+ 31,
973
+ 30,
974
+ 31
975
+ ], f = [
976
+ [
977
+ "month",
978
+ "year",
979
+ 0
980
+ ],
981
+ [
982
+ "day",
983
+ "month",
984
+ -1
985
+ ],
986
+ [
987
+ "hour",
988
+ "day",
989
+ 0
990
+ ],
991
+ [
992
+ "minute",
993
+ "hour",
994
+ 0
995
+ ],
996
+ [
997
+ "second",
998
+ "minute",
999
+ 0
1000
+ ]
1001
+ ], m = class s {
1002
+ tz;
1003
+ ms;
1004
+ second;
1005
+ minute;
1006
+ hour;
1007
+ day;
1008
+ month;
1009
+ year;
1010
+ constructor(e, t) {
1011
+ if (this.tz = t, e && e instanceof Date) if (!isNaN(e)) this.fromDate(e);
1012
+ else throw new TypeError("CronDate: Invalid date passed to CronDate constructor");
1013
+ else if (e == null) this.fromDate(/* @__PURE__ */ new Date());
1014
+ else if (e && typeof e == "string") this.fromString(e);
1015
+ else if (e instanceof s) this.fromCronDate(e);
1016
+ else throw new TypeError("CronDate: Invalid type (" + typeof e + ") passed to CronDate constructor");
1017
+ }
1018
+ getLastDayOfMonth(e, t) {
1019
+ return t !== 1 ? P[t] : new Date(Date.UTC(e, t + 1, 0)).getUTCDate();
1020
+ }
1021
+ getLastWeekday(e, t) {
1022
+ let r = this.getLastDayOfMonth(e, t), i = new Date(Date.UTC(e, t, r)).getUTCDay();
1023
+ return i === 0 ? r - 2 : i === 6 ? r - 1 : r;
1024
+ }
1025
+ getNearestWeekday(e, t, r) {
1026
+ let n = this.getLastDayOfMonth(e, t);
1027
+ if (r > n) return -1;
1028
+ let a = new Date(Date.UTC(e, t, r)).getUTCDay();
1029
+ return a === 0 ? r === n ? r - 2 : r + 1 : a === 6 ? r === 1 ? r + 2 : r - 1 : r;
1030
+ }
1031
+ isNthWeekdayOfMonth(e, t, r, n) {
1032
+ let a = new Date(Date.UTC(e, t, r)).getUTCDay(), o = 0;
1033
+ for (let h = 1; h <= r; h++) new Date(Date.UTC(e, t, h)).getUTCDay() === a && o++;
1034
+ if (n & 63 && O[o - 1] & n) return !0;
1035
+ if (n & 32) {
1036
+ let h = this.getLastDayOfMonth(e, t);
1037
+ for (let l = r + 1; l <= h; l++) if (new Date(Date.UTC(e, t, l)).getUTCDay() === a) return !1;
1038
+ return !0;
1039
+ }
1040
+ return !1;
1041
+ }
1042
+ fromDate(e) {
1043
+ if (this.tz !== void 0) if (typeof this.tz == "number") this.ms = e.getUTCMilliseconds(), this.second = e.getUTCSeconds(), this.minute = e.getUTCMinutes() + this.tz, this.hour = e.getUTCHours(), this.day = e.getUTCDate(), this.month = e.getUTCMonth(), this.year = e.getUTCFullYear(), this.apply();
1044
+ else try {
1045
+ let t = g(e, this.tz);
1046
+ this.ms = e.getMilliseconds(), this.second = t.s, this.minute = t.i, this.hour = t.h, this.day = t.d, this.month = t.m - 1, this.year = t.y;
1047
+ } catch (t) {
1048
+ let r = t instanceof Error ? t.message : String(t);
1049
+ throw new TypeError(`CronDate: Failed to convert date to timezone '${this.tz}'. This may happen with invalid timezone names or dates. Original error: ${r}`);
1050
+ }
1051
+ else this.ms = e.getMilliseconds(), this.second = e.getSeconds(), this.minute = e.getMinutes(), this.hour = e.getHours(), this.day = e.getDate(), this.month = e.getMonth(), this.year = e.getFullYear();
1052
+ }
1053
+ fromCronDate(e) {
1054
+ this.tz = e.tz, this.year = e.year, this.month = e.month, this.day = e.day, this.hour = e.hour, this.minute = e.minute, this.second = e.second, this.ms = e.ms;
1055
+ }
1056
+ apply() {
1057
+ if (this.month > 11 || this.month < 0 || this.day > P[this.month] || this.day < 1 || this.hour > 59 || this.minute > 59 || this.second > 59 || this.hour < 0 || this.minute < 0 || this.second < 0) {
1058
+ let e = new Date(Date.UTC(this.year, this.month, this.day, this.hour, this.minute, this.second, this.ms));
1059
+ return this.ms = e.getUTCMilliseconds(), this.second = e.getUTCSeconds(), this.minute = e.getUTCMinutes(), this.hour = e.getUTCHours(), this.day = e.getUTCDate(), this.month = e.getUTCMonth(), this.year = e.getUTCFullYear(), !0;
1060
+ } else return !1;
1061
+ }
1062
+ fromString(e) {
1063
+ if (typeof this.tz == "number") {
1064
+ let t = v(e);
1065
+ this.ms = t.getUTCMilliseconds(), this.second = t.getUTCSeconds(), this.minute = t.getUTCMinutes(), this.hour = t.getUTCHours(), this.day = t.getUTCDate(), this.month = t.getUTCMonth(), this.year = t.getUTCFullYear(), this.apply();
1066
+ } else return this.fromDate(v(e, this.tz));
1067
+ }
1068
+ findNext(e, t, r, n) {
1069
+ return this._findMatch(e, t, r, n, 1);
1070
+ }
1071
+ _findMatch(e, t, r, n, i) {
1072
+ let a = this[t], o;
1073
+ r.lastDayOfMonth && (o = this.getLastDayOfMonth(this.year, this.month));
1074
+ let h = !r.starDOW && t == "day" ? new Date(Date.UTC(this.year, this.month, 1, 0, 0, 0, 0)).getUTCDay() : void 0, l = this[t] + n, y = i === 1 ? (u) => u < r[t].length : (u) => u >= 0;
1075
+ for (let u = l; y(u); u += i) {
1076
+ let d = r[t][u];
1077
+ if (t === "day" && !d) {
1078
+ for (let c = 0; c < r.nearestWeekdays.length; c++) if (r.nearestWeekdays[c]) {
1079
+ let M = this.getNearestWeekday(this.year, this.month, c - n);
1080
+ if (M === -1) continue;
1081
+ if (M === u - n) {
1082
+ d = 1;
1083
+ break;
1084
+ }
1085
+ }
1086
+ }
1087
+ if (t === "day" && r.lastWeekday) {
1088
+ let c = this.getLastWeekday(this.year, this.month);
1089
+ u - n === c && (d = 1);
1090
+ }
1091
+ if (t === "day" && r.lastDayOfMonth && u - n == o && (d = 1), t === "day" && !r.starDOW) {
1092
+ let c = r.dayOfWeek[(h + (u - n - 1)) % 7];
1093
+ if (c && c & 63) c = this.isNthWeekdayOfMonth(this.year, this.month, u - n, c) ? 1 : 0;
1094
+ else if (c) throw new Error(`CronDate: Invalid value for dayOfWeek encountered. ${c}`);
1095
+ r.useAndLogic ? d = d && c : !e.domAndDow && !r.starDOM ? d = d || c : d = d && c;
1096
+ }
1097
+ if (d) return this[t] = u - n, a !== this[t] ? 2 : 1;
1098
+ }
1099
+ return 3;
1100
+ }
1101
+ recurse(e, t, r) {
1102
+ if (r === 0 && !e.starYear) {
1103
+ if (this.year >= 0 && this.year < e.year.length && e.year[this.year] === 0) {
1104
+ let i = -1;
1105
+ for (let a = this.year + 1; a < e.year.length && a < 1e4; a++) if (e.year[a] === 1) {
1106
+ i = a;
1107
+ break;
1108
+ }
1109
+ if (i === -1) return null;
1110
+ this.year = i, this.month = 0, this.day = 1, this.hour = 0, this.minute = 0, this.second = 0, this.ms = 0;
1111
+ }
1112
+ if (this.year >= 1e4) return null;
1113
+ }
1114
+ let n = this.findNext(t, f[r][0], e, f[r][2]);
1115
+ if (n > 1) {
1116
+ let i = r + 1;
1117
+ for (; i < f.length;) this[f[i][0]] = -f[i][2], i++;
1118
+ if (n === 3) {
1119
+ if (this[f[r][1]]++, this[f[r][0]] = -f[r][2], this.apply(), r === 0 && !e.starYear) {
1120
+ for (; this.year >= 0 && this.year < e.year.length && e.year[this.year] === 0 && this.year < 1e4;) this.year++;
1121
+ if (this.year >= 1e4 || this.year >= e.year.length) return null;
1122
+ }
1123
+ return this.recurse(e, t, 0);
1124
+ } else if (this.apply()) return this.recurse(e, t, r - 1);
1125
+ }
1126
+ return r += 1, r >= f.length ? this : (e.starYear ? this.year >= 3e3 : this.year >= 1e4) ? null : this.recurse(e, t, r);
1127
+ }
1128
+ increment(e, t, r) {
1129
+ return this.second += t.interval !== void 0 && t.interval > 1 && r ? t.interval : 1, this.ms = 0, this.apply(), this.recurse(e, t, 0);
1130
+ }
1131
+ decrement(e, t) {
1132
+ return this.second -= t.interval !== void 0 && t.interval > 1 ? t.interval : 1, this.ms = 0, this.apply(), this.recurseBackward(e, t, 0, 0);
1133
+ }
1134
+ recurseBackward(e, t, r, n = 0) {
1135
+ if (n > 1e4) return null;
1136
+ if (r === 0 && !e.starYear) {
1137
+ if (this.year >= 0 && this.year < e.year.length && e.year[this.year] === 0) {
1138
+ let a = -1;
1139
+ for (let o = this.year - 1; o >= 0; o--) if (e.year[o] === 1) {
1140
+ a = o;
1141
+ break;
1142
+ }
1143
+ if (a === -1) return null;
1144
+ this.year = a, this.month = 11, this.day = 31, this.hour = 23, this.minute = 59, this.second = 59, this.ms = 0;
1145
+ }
1146
+ if (this.year < 0) return null;
1147
+ }
1148
+ let i = this.findPrevious(t, f[r][0], e, f[r][2]);
1149
+ if (i > 1) {
1150
+ let a = r + 1;
1151
+ for (; a < f.length;) {
1152
+ let o = f[a][0], h = f[a][2], l = this.getMaxPatternValue(o, e, h);
1153
+ this[o] = l, a++;
1154
+ }
1155
+ if (i === 3) {
1156
+ if (this[f[r][1]]--, r === 0) {
1157
+ let y = this.getLastDayOfMonth(this.year, this.month);
1158
+ this.day > y && (this.day = y);
1159
+ }
1160
+ if (r === 1) if (this.day <= 0) this.day = 1;
1161
+ else {
1162
+ let y = this.year, u = this.month;
1163
+ for (; u < 0;) u += 12, y--;
1164
+ for (; u > 11;) u -= 12, y++;
1165
+ let d = u !== 1 ? P[u] : new Date(Date.UTC(y, u + 1, 0)).getUTCDate();
1166
+ this.day > d && (this.day = d);
1167
+ }
1168
+ this.apply();
1169
+ let o = f[r][0], h = f[r][2], l = this.getMaxPatternValue(o, e, h);
1170
+ if (o === "day") {
1171
+ let y = this.getLastDayOfMonth(this.year, this.month);
1172
+ this[o] = Math.min(l, y);
1173
+ } else this[o] = l;
1174
+ if (this.apply(), r === 0) {
1175
+ let y = f[1][2], u = this.getMaxPatternValue("day", e, y), d = this.getLastDayOfMonth(this.year, this.month), c = Math.min(u, d);
1176
+ c !== this.day && (this.day = c, this.hour = this.getMaxPatternValue("hour", e, f[2][2]), this.minute = this.getMaxPatternValue("minute", e, f[3][2]), this.second = this.getMaxPatternValue("second", e, f[4][2]));
1177
+ }
1178
+ if (r === 0 && !e.starYear) {
1179
+ for (; this.year >= 0 && this.year < e.year.length && e.year[this.year] === 0;) this.year--;
1180
+ if (this.year < 0) return null;
1181
+ }
1182
+ return this.recurseBackward(e, t, 0, n + 1);
1183
+ } else if (this.apply()) return this.recurseBackward(e, t, r - 1, n + 1);
1184
+ }
1185
+ return r += 1, r >= f.length ? this : this.year < 0 ? null : this.recurseBackward(e, t, r, n + 1);
1186
+ }
1187
+ getMaxPatternValue(e, t, r) {
1188
+ if (e === "day" && t.lastDayOfMonth) return this.getLastDayOfMonth(this.year, this.month);
1189
+ if (e === "day" && !t.starDOW) return this.getLastDayOfMonth(this.year, this.month);
1190
+ for (let n = t[e].length - 1; n >= 0; n--) if (t[e][n]) return n - r;
1191
+ return t[e].length - 1 - r;
1192
+ }
1193
+ findPrevious(e, t, r, n) {
1194
+ return this._findMatch(e, t, r, n, -1);
1195
+ }
1196
+ getDate(e) {
1197
+ return e || this.tz === void 0 ? new Date(this.year, this.month, this.day, this.hour, this.minute, this.second, this.ms) : typeof this.tz == "number" ? new Date(Date.UTC(this.year, this.month, this.day, this.hour, this.minute - this.tz, this.second, this.ms)) : k(b(this.year, this.month + 1, this.day, this.hour, this.minute, this.second, this.tz), !1);
1198
+ }
1199
+ getTime() {
1200
+ return this.getDate(!1).getTime();
1201
+ }
1202
+ match(e, t) {
1203
+ if (!e.starYear && (this.year < 0 || this.year >= e.year.length || e.year[this.year] === 0)) return !1;
1204
+ for (let r = 0; r < f.length; r++) {
1205
+ let n = f[r][0], i = f[r][2], a = this[n];
1206
+ if (a + i < 0 || a + i >= e[n].length) return !1;
1207
+ let o = e[n][a + i];
1208
+ if (n === "day") {
1209
+ if (!o) {
1210
+ for (let h = 0; h < e.nearestWeekdays.length; h++) if (e.nearestWeekdays[h]) {
1211
+ let l = this.getNearestWeekday(this.year, this.month, h - i);
1212
+ if (l !== -1 && l === a) {
1213
+ o = 1;
1214
+ break;
1215
+ }
1216
+ }
1217
+ }
1218
+ if (e.lastWeekday) a === this.getLastWeekday(this.year, this.month) && (o = 1);
1219
+ if (e.lastDayOfMonth) a === this.getLastDayOfMonth(this.year, this.month) && (o = 1);
1220
+ if (!e.starDOW) {
1221
+ let h = new Date(Date.UTC(this.year, this.month, 1, 0, 0, 0, 0)).getUTCDay(), l = e.dayOfWeek[(h + (a - 1)) % 7];
1222
+ l && l & 63 && (l = this.isNthWeekdayOfMonth(this.year, this.month, a, l) ? 1 : 0), e.useAndLogic ? o = o && l : !t.domAndDow && !e.starDOM ? o = o || l : o = o && l;
1223
+ }
1224
+ }
1225
+ if (!o) return !1;
1226
+ }
1227
+ return !0;
1228
+ }
1229
+ };
1230
+ function R(s) {
1231
+ if (s === void 0 && (s = {}), delete s.name, s.legacyMode !== void 0 && s.domAndDow === void 0 ? s.domAndDow = !s.legacyMode : s.domAndDow === void 0 && (s.domAndDow = !1), s.legacyMode = !s.domAndDow, s.paused = s.paused === void 0 ? !1 : s.paused, s.maxRuns = s.maxRuns === void 0 ? Infinity : s.maxRuns, s.catch = s.catch === void 0 ? !1 : s.catch, s.interval = s.interval === void 0 ? 0 : parseInt(s.interval.toString(), 10), s.utcOffset = s.utcOffset === void 0 ? void 0 : parseInt(s.utcOffset.toString(), 10), s.dayOffset = s.dayOffset === void 0 ? 0 : parseInt(s.dayOffset.toString(), 10), s.unref = s.unref === void 0 ? !1 : s.unref, s.mode = s.mode === void 0 ? "auto" : s.mode, s.alternativeWeekdays = s.alternativeWeekdays === void 0 ? !1 : s.alternativeWeekdays, s.sloppyRanges = s.sloppyRanges === void 0 ? !1 : s.sloppyRanges, ![
1232
+ "auto",
1233
+ "5-part",
1234
+ "6-part",
1235
+ "7-part",
1236
+ "5-or-6-parts",
1237
+ "6-or-7-parts"
1238
+ ].includes(s.mode)) throw new Error("CronOptions: mode must be one of 'auto', '5-part', '6-part', '7-part', '5-or-6-parts', or '6-or-7-parts'.");
1239
+ if (s.startAt && (s.startAt = new m(s.startAt, s.timezone)), s.stopAt && (s.stopAt = new m(s.stopAt, s.timezone)), s.interval !== null) {
1240
+ if (isNaN(s.interval)) throw new Error("CronOptions: Supplied value for interval is not a number");
1241
+ if (s.interval < 0) throw new Error("CronOptions: Supplied value for interval can not be negative");
1242
+ }
1243
+ if (s.utcOffset !== void 0) {
1244
+ if (isNaN(s.utcOffset)) throw new Error("CronOptions: Invalid value passed for utcOffset, should be number representing minutes offset from UTC.");
1245
+ if (s.utcOffset < -870 || s.utcOffset > 870) throw new Error("CronOptions: utcOffset out of bounds.");
1246
+ if (s.utcOffset !== void 0 && s.timezone) throw new Error("CronOptions: Combining 'utcOffset' with 'timezone' is not allowed.");
1247
+ }
1248
+ if (s.unref !== !0 && s.unref !== !1) throw new Error("CronOptions: Unref should be either true, false or undefined(false).");
1249
+ if (s.dayOffset !== void 0 && s.dayOffset !== 0 && isNaN(s.dayOffset)) throw new Error("CronOptions: Invalid value passed for dayOffset, should be a number representing days to offset.");
1250
+ return s;
1251
+ }
1252
+ function p(s) {
1253
+ return Object.prototype.toString.call(s) === "[object Function]" || typeof s == "function" || s instanceof Function;
1254
+ }
1255
+ function _(s) {
1256
+ return p(s);
1257
+ }
1258
+ function x(s) {
1259
+ typeof Deno < "u" && typeof Deno.unrefTimer < "u" ? Deno.unrefTimer(s) : s && typeof s.unref < "u" && s.unref();
1260
+ }
1261
+ var W = 30 * 1e3, w = [], E = class {
1262
+ name;
1263
+ options;
1264
+ _states;
1265
+ fn;
1266
+ getTz() {
1267
+ return this.options.timezone || this.options.utcOffset;
1268
+ }
1269
+ applyDayOffset(e) {
1270
+ if (this.options.dayOffset !== void 0 && this.options.dayOffset !== 0) {
1271
+ let t = this.options.dayOffset * 24 * 60 * 60 * 1e3;
1272
+ return new Date(e.getTime() + t);
1273
+ }
1274
+ return e;
1275
+ }
1276
+ constructor(e, t, r) {
1277
+ let n, i;
1278
+ if (p(t)) i = t;
1279
+ else if (typeof t == "object") n = t;
1280
+ else if (t !== void 0) throw new Error("Cron: Invalid argument passed for optionsIn. Should be one of function, or object (options).");
1281
+ if (p(r)) i = r;
1282
+ else if (typeof r == "object") n = r;
1283
+ else if (r !== void 0) throw new Error("Cron: Invalid argument passed for funcIn. Should be one of function, or object (options).");
1284
+ if (this.name = n?.name, this.options = R(n), this._states = {
1285
+ kill: !1,
1286
+ blocking: !1,
1287
+ previousRun: void 0,
1288
+ currentRun: void 0,
1289
+ once: void 0,
1290
+ currentTimeout: void 0,
1291
+ maxRuns: n ? n.maxRuns : void 0,
1292
+ paused: n ? n.paused : !1,
1293
+ pattern: new C("* * * * *", void 0, { mode: "auto" })
1294
+ }, e && (e instanceof Date || typeof e == "string" && e.indexOf(":") > 0) ? this._states.once = new m(e, this.getTz()) : this._states.pattern = new C(e, this.options.timezone, {
1295
+ mode: this.options.mode,
1296
+ alternativeWeekdays: this.options.alternativeWeekdays,
1297
+ sloppyRanges: this.options.sloppyRanges
1298
+ }), this.name) {
1299
+ if (w.find((o) => o.name === this.name)) throw new Error("Cron: Tried to initialize new named job '" + this.name + "', but name already taken.");
1300
+ w.push(this);
1301
+ }
1302
+ return i !== void 0 && _(i) && (this.fn = i, this.schedule()), this;
1303
+ }
1304
+ nextRun(e) {
1305
+ let t = this._next(e);
1306
+ return t ? this.applyDayOffset(t.getDate(!1)) : null;
1307
+ }
1308
+ nextRuns(e, t) {
1309
+ this._states.maxRuns !== void 0 && e > this._states.maxRuns && (e = this._states.maxRuns);
1310
+ let r = t || this._states.currentRun || void 0;
1311
+ return this._enumerateRuns(e, r, "next");
1312
+ }
1313
+ previousRuns(e, t) {
1314
+ return this._enumerateRuns(e, t || void 0, "previous");
1315
+ }
1316
+ _enumerateRuns(e, t, r) {
1317
+ let n = [], i = t ? new m(t, this.getTz()) : null, a = r === "next" ? this._next : this._previous;
1318
+ for (; e--;) {
1319
+ let o = a.call(this, i);
1320
+ if (!o) break;
1321
+ let h = o.getDate(!1);
1322
+ n.push(this.applyDayOffset(h)), i = o;
1323
+ }
1324
+ return n;
1325
+ }
1326
+ match(e) {
1327
+ if (this._states.once) {
1328
+ let r = new m(e, this.getTz());
1329
+ r.ms = 0;
1330
+ let n = new m(this._states.once, this.getTz());
1331
+ return n.ms = 0, r.getTime() === n.getTime();
1332
+ }
1333
+ let t = new m(e, this.getTz());
1334
+ return t.ms = 0, t.match(this._states.pattern, this.options);
1335
+ }
1336
+ getPattern() {
1337
+ if (!this._states.once) return this._states.pattern ? this._states.pattern.pattern : void 0;
1338
+ }
1339
+ getOnce() {
1340
+ return this._states.once ? this._states.once.getDate() : null;
1341
+ }
1342
+ isRunning() {
1343
+ let e = this.nextRun(this._states.currentRun), t = !this._states.paused, r = this.fn !== void 0, n = !this._states.kill;
1344
+ return t && r && n && e !== null;
1345
+ }
1346
+ isStopped() {
1347
+ return this._states.kill;
1348
+ }
1349
+ isBusy() {
1350
+ return this._states.blocking;
1351
+ }
1352
+ currentRun() {
1353
+ return this._states.currentRun ? this._states.currentRun.getDate() : null;
1354
+ }
1355
+ previousRun() {
1356
+ return this._states.previousRun ? this._states.previousRun.getDate() : null;
1357
+ }
1358
+ msToNext(e) {
1359
+ let t = this._next(e);
1360
+ return t ? e instanceof m || e instanceof Date ? t.getTime() - e.getTime() : t.getTime() - new m(e).getTime() : null;
1361
+ }
1362
+ stop() {
1363
+ this._states.kill = !0, this._states.currentTimeout && clearTimeout(this._states.currentTimeout);
1364
+ let e = w.indexOf(this);
1365
+ e >= 0 && w.splice(e, 1);
1366
+ }
1367
+ pause() {
1368
+ return this._states.paused = !0, !this._states.kill;
1369
+ }
1370
+ resume() {
1371
+ return this._states.paused = !1, !this._states.kill;
1372
+ }
1373
+ schedule(e) {
1374
+ if (e && this.fn) throw new Error("Cron: It is not allowed to schedule two functions using the same Croner instance.");
1375
+ e && (this.fn = e);
1376
+ let t = this.msToNext(), r = this.nextRun(this._states.currentRun);
1377
+ return t == null || isNaN(t) || r === null ? this : (t > W && (t = W), this._states.currentTimeout = setTimeout(() => this._checkTrigger(r), t), this._states.currentTimeout && this.options.unref && x(this._states.currentTimeout), this);
1378
+ }
1379
+ async _trigger(e) {
1380
+ this._states.blocking = !0, this._states.currentRun = new m(void 0, this.getTz());
1381
+ try {
1382
+ if (this.options.catch) try {
1383
+ this.fn !== void 0 && await this.fn(this, this.options.context);
1384
+ } catch (t) {
1385
+ if (p(this.options.catch)) try {
1386
+ this.options.catch(t, this);
1387
+ } catch {}
1388
+ }
1389
+ else this.fn !== void 0 && await this.fn(this, this.options.context);
1390
+ } finally {
1391
+ this._states.previousRun = new m(e, this.getTz()), this._states.blocking = !1;
1392
+ }
1393
+ }
1394
+ async trigger() {
1395
+ await this._trigger();
1396
+ }
1397
+ runsLeft() {
1398
+ return this._states.maxRuns;
1399
+ }
1400
+ _checkTrigger(e) {
1401
+ let t = /* @__PURE__ */ new Date(), r = !this._states.paused && t.getTime() >= e.getTime(), n = this._states.blocking && this.options.protect;
1402
+ r && !n ? (this._states.maxRuns !== void 0 && this._states.maxRuns--, this._trigger()) : r && n && p(this.options.protect) && setTimeout(() => this.options.protect(this), 0), this.schedule();
1403
+ }
1404
+ _next(e) {
1405
+ let t = !!(e || this._states.currentRun), r = !1;
1406
+ !e && this.options.startAt && this.options.interval && ([e, t] = this._calculatePreviousRun(e, t), r = !e), e = new m(e, this.getTz()), this.options.startAt && e && e.getTime() < this.options.startAt.getTime() && (e = this.options.startAt);
1407
+ let n = this._states.once || new m(e, this.getTz());
1408
+ return !r && n !== this._states.once && (n = n.increment(this._states.pattern, this.options, t)), this._states.once && this._states.once.getTime() <= e.getTime() || n === null || this._states.maxRuns !== void 0 && this._states.maxRuns <= 0 || this._states.kill || this.options.stopAt && n.getTime() >= this.options.stopAt.getTime() ? null : n;
1409
+ }
1410
+ _previous(e) {
1411
+ let t = new m(e, this.getTz());
1412
+ this.options.stopAt && t.getTime() > this.options.stopAt.getTime() && (t = this.options.stopAt);
1413
+ let r = new m(t, this.getTz());
1414
+ return this._states.once ? this._states.once.getTime() < t.getTime() ? this._states.once : null : (r = r.decrement(this._states.pattern, this.options), r === null || this.options.startAt && r.getTime() < this.options.startAt.getTime() ? null : r);
1415
+ }
1416
+ _calculatePreviousRun(e, t) {
1417
+ let r = new m(void 0, this.getTz()), n = e;
1418
+ if (this.options.startAt.getTime() <= r.getTime()) {
1419
+ n = this.options.startAt;
1420
+ let i = n.getTime() + this.options.interval * 1e3;
1421
+ for (; i <= r.getTime();) n = new m(n, this.getTz()).increment(this._states.pattern, this.options, !0), i = n.getTime() + this.options.interval * 1e3;
1422
+ t = !0;
1423
+ }
1424
+ return n === null && (n = void 0), [n, t];
1425
+ }
1426
+ };
1427
+ //#endregion
1428
+ //#region src/builtins/backup-orchestrator/cron-helpers.ts
1429
+ /**
1430
+ * Cron helpers — wraps `croner` so the orchestrator + the `previewNextRuns`
1431
+ * cap method share one validation + next-run-computation surface.
1432
+ *
1433
+ * `croner` is the opinionated node cron library we standardize on
1434
+ * server-side: pure JS, native TypeScript types, supports the full
1435
+ * 5-field POSIX cron grammar plus seconds (6-field), DST-aware, and
1436
+ * exposes `nextRun()` / `nextRuns(N)` for peeking without scheduling.
1437
+ * The orchestrator uses the peeking API (it has its own polling
1438
+ * loop); future surfaces (e.g. event-driven retention sweeps) can
1439
+ * register an actual scheduled `Cron(pattern, fn)` and let croner
1440
+ * own the timer.
1441
+ */
1442
+ /**
1443
+ * Validate a cron expression and return its next firing time strictly
1444
+ * after `after`, expressed as ms-epoch. Returns `null` when the
1445
+ * expression has no future runs (croner returns null e.g. for
1446
+ * fully-bounded patterns that have already elapsed). Throws when the
1447
+ * expression is syntactically invalid — callers are expected to
1448
+ * surface that to the operator.
1449
+ */
1450
+ function computeNextDue(cron, after) {
1451
+ const next = new E(cron, { paused: true }).nextRun(new Date(after));
1452
+ return next ? next.getTime() : null;
1453
+ }
1454
+ /**
1455
+ * Validate without computing — returns true when `cron` parses, false
1456
+ * otherwise. Used by the cap-router input zod schema's `.refine`
1457
+ * callback to reject malformed expressions at the boundary instead
1458
+ * of at scheduler-tick time.
1459
+ */
1460
+ function isValidCron(cron) {
1461
+ try {
1462
+ new E(cron, { paused: true });
1463
+ return true;
1464
+ } catch {
1465
+ return false;
1466
+ }
1467
+ }
1468
+ /**
1469
+ * Peek the next N firing times after `after` (default: now). Used by
1470
+ * the admin UI's CronEditor preview pane — operators see the next
1471
+ * 3–5 runs as they edit the expression so a wrong pattern is
1472
+ * obvious before save.
1473
+ */
1474
+ function previewNextRuns(cron, count, after) {
1475
+ const job = new E(cron, { paused: true });
1476
+ const baseline = after !== void 0 ? new Date(after) : /* @__PURE__ */ new Date();
1477
+ const out = [];
1478
+ let cursor = baseline;
1479
+ for (let i = 0; i < count; i++) {
1480
+ cursor = job.nextRun(cursor);
1481
+ if (!cursor) break;
1482
+ out.push(cursor.getTime());
1483
+ }
1484
+ return out;
1485
+ }
1486
+ //#endregion
1487
+ //#region src/builtins/backup-orchestrator/backup-orchestrator.addon.ts
1488
+ /**
1489
+ * `backup-orchestrator` builtin — singleton owner of the `backup` cap.
1490
+ *
1491
+ * Responsibilities:
1492
+ * - Schedule (cron-like timer at top of hour, daily/weekly cadence)
1493
+ * - Per-destination policy (enable/disable, retention count, label)
1494
+ * persisted in `backup_destination_policies` (this builtin owns
1495
+ * the table).
1496
+ * - Archive build (one tar.gz with embedded manifest, fanned out to
1497
+ * every selected `backups`-typed `StorageLocation` via the
1498
+ * consumer-facing `storage` cap's chunked upload protocol).
1499
+ * - Aggregate listing/restore/delete across destinations — operates
1500
+ * on the per-location `manifests.json` index file, not the
1501
+ * archives directly.
1502
+ *
1503
+ * Storage routing: destinations are
1504
+ * `api.storage.listLocations({ type: 'backups' })` — every operator-
1505
+ * visible `backups` location IS a destination. The orchestrator joins
1506
+ * that list with the `backup_destination_policies` table to surface
1507
+ * `enabled` / `retentionCount` / `label`. The actual archive bytes
1508
+ * travel through `api.storage.beginUpload` / `writeChunk` /
1509
+ * `finalizeUpload`, with the staging archive built into the `cache`
1510
+ * location.
1511
+ */
1512
+ /**
1513
+ * Hardcoded seed for newly-discovered `backups` locations. Operators
1514
+ * tune retention per-destination after creation via the destinations
1515
+ * table. Promoted from a configurable global to a constant when the
1516
+ * orchestrator's settings panel was retired (per-destination cron +
1517
+ * inline retention input on the destinations row replaces every knob
1518
+ * the panel used to expose).
1519
+ */
1520
+ var DEFAULT_RETENTION_COUNT = 7;
1521
+ /**
1522
+ * Default cron stamped on a freshly-discovered `backups` location.
1523
+ * Operators can edit it from the destination row; setting empty in
1524
+ * the editor flips the destination back to manual-only. Daily 03:00
1525
+ * server-local mirrors the previous global default.
1526
+ */
1527
+ var DEFAULT_CRON = "0 3 * * *";
1528
+ var SCHEDULE_TICK_MS = 6e4;
1529
+ /**
1530
+ * Build the human-facing archive label. Every archive always carries
1531
+ * the destination id and the creation date — useful in restore wizards
1532
+ * + cross-destination listings where UUIDs are unreadable.
1533
+ *
1534
+ * Format: `${destinationId} · ${YYYY-MM-DD HH:mm}[${suffix}]`. Suffix is
1535
+ * an optional operator-provided tag (e.g. "before-migration") appended
1536
+ * after another middot. UTC is intentional — labels are also written
1537
+ * into the per-location manifest which travels across timezones with
1538
+ * the backup.
1539
+ */
1540
+ function formatArchiveLabel(destinationId, createdAt, suffix) {
1541
+ const d = new Date(createdAt);
1542
+ const pad = (n) => String(n).padStart(2, "0");
1543
+ const stamp = `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
1544
+ const trimmed = suffix?.trim();
1545
+ return trimmed ? `${destinationId} · ${stamp} · ${trimmed}` : `${destinationId} · ${stamp}`;
1546
+ }
1547
+ /**
1548
+ * Chunk size for the read-stream that feeds the chunked upload pipe.
1549
+ * 8 MiB is well under the WS frame limit (16 MiB on tRPC) with headroom.
1550
+ */
1551
+ var UPLOAD_CHUNK_BYTES = 8 * 1024 * 1024;
1552
+ /** Manifest id this addon registers under (must match `package.json`). */
1553
+ var ORCHESTRATOR_ADDON_ID = "backup-orchestrator";
1554
+ var BackupOrchestratorAddon = class extends _camstack_types.BaseAddon {
1555
+ systemBackup = null;
1556
+ policies = null;
1557
+ dataDir = "";
1558
+ scheduleTimer = null;
1559
+ /**
1560
+ * Baseline used as the "after" anchor when a policy has a cron but
1561
+ * has never run yet (no `lastRunAt`). Set on orchestrator boot so a
1562
+ * pattern that fires every minute doesn't immediately replay every
1563
+ * historical "missed" run from epoch 0.
1564
+ */
1565
+ scheduleBaselineAt = Date.now();
1566
+ manifestLock = new LocationManifestLock();
1567
+ constructor() {
1568
+ super({});
1569
+ }
1570
+ async onInitialize() {
1571
+ const envDataPath = process.env["CAMSTACK_DATA"];
1572
+ const fallbackLocation = await this.ctx.api.storage.resolve.query({
1573
+ location: "data",
1574
+ relativePath: ""
1575
+ }).catch(() => null);
1576
+ this.dataDir = envDataPath ?? (fallbackLocation ? parentDir(fallbackLocation) : "camstack-data");
1577
+ this.systemBackup = new SystemBackupService(this.dataDir, this.ctx.logger);
1578
+ this.policies = this.resolvePolicyService();
1579
+ if (this.policies) try {
1580
+ await this.policies.initialize();
1581
+ } catch (err) {
1582
+ this.ctx.logger.error("backup-orchestrator: policy table init failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
1583
+ this.policies = null;
1584
+ }
1585
+ const provider = {
1586
+ listDestinations: () => this.listDestinations(),
1587
+ list: () => this.aggregatedList(),
1588
+ listLocations: () => this.systemBackup.statLocations(),
1589
+ trigger: (input) => this.triggerBackup(input),
1590
+ getEntries: (input) => this.getEntries(input),
1591
+ restore: (input) => this.scheduleRestore(input),
1592
+ delete: (input) => this.deleteArchive(input),
1593
+ listArchives: (input) => this.listArchives(input),
1594
+ upsertDestinationPolicy: (input) => this.upsertDestinationPolicy(input),
1595
+ previewSchedule: (input) => this.previewSchedule(input)
1596
+ };
1597
+ this.startScheduleTimer();
1598
+ this.ctx.logger.info("Backup orchestrator initialized", { meta: {
1599
+ dataDir: this.dataDir,
1600
+ policies: this.policies !== null
1601
+ } });
1602
+ return [{
1603
+ capability: _camstack_types.backupCapability,
1604
+ provider
1605
+ }];
1606
+ }
1607
+ async onShutdown() {
1608
+ this.stopScheduleTimer();
1609
+ this.systemBackup = null;
1610
+ this.policies = null;
1611
+ }
1612
+ /**
1613
+ * Aggregate destinations from `api.storage.listLocations({ type: 'backups' })`
1614
+ * joined with the per-destination policy table. New locations get an
1615
+ * `ensureDefault` policy seeded transparently (enabled=true,
1616
+ * retentionCount=defaultRetentionCount). Pre-existing operator-edited
1617
+ * policies are preserved by the `ensureDefault` idempotency.
1618
+ */
1619
+ async listDestinations() {
1620
+ const locations = await this.fetchBackupLocations();
1621
+ const policies = await this.loadPoliciesIndexed(locations.map((l) => l.id));
1622
+ const lastSuccess = await Promise.all(locations.map(async (loc) => {
1623
+ try {
1624
+ const manifest = await readLocationManifest(this.storageApi(), loc.id);
1625
+ return [loc.id, latestArchiveOf(manifest)];
1626
+ } catch {
1627
+ return [loc.id, void 0];
1628
+ }
1629
+ }));
1630
+ const lastSuccessByLoc = new Map(lastSuccess);
1631
+ return locations.map((loc) => this.toDestinationWire(loc, policies.get(loc.id), lastSuccessByLoc.get(loc.id)));
1632
+ }
1633
+ async fetchBackupLocations() {
1634
+ try {
1635
+ return await this.ctx.api.storage.listLocations.query({ type: "backups" });
1636
+ } catch (err) {
1637
+ this.ctx.logger.error("backup-orchestrator: listLocations failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
1638
+ return [];
1639
+ }
1640
+ }
1641
+ /**
1642
+ * Load every persisted policy and key it by `locationId`, while
1643
+ * lazily seeding a default for any `locationId` we haven't seen
1644
+ * before. Idempotent — `ensureDefault` no-ops on already-persisted
1645
+ * rows. Falls back to the in-memory default policy when the policy
1646
+ * service isn't wired (no settings-store backend available).
1647
+ */
1648
+ async loadPoliciesIndexed(locationIds) {
1649
+ const out = /* @__PURE__ */ new Map();
1650
+ if (!this.policies) {
1651
+ for (const id of locationIds) out.set(id, this.fallbackPolicy(id));
1652
+ return out;
1653
+ }
1654
+ const persisted = await this.policies.list().catch((err) => {
1655
+ this.ctx.logger.warn("backup-orchestrator: policy list failed — using defaults", { meta: { error: err instanceof Error ? err.message : String(err) } });
1656
+ return [];
1657
+ });
1658
+ for (const p of persisted) out.set(p.locationId, p);
1659
+ for (const id of locationIds) {
1660
+ if (out.has(id)) continue;
1661
+ try {
1662
+ const fresh = await this.policies.ensureDefault(id, {
1663
+ retentionCount: DEFAULT_RETENTION_COUNT,
1664
+ cron: DEFAULT_CRON
1665
+ });
1666
+ out.set(id, fresh);
1667
+ } catch (err) {
1668
+ this.ctx.logger.warn("backup-orchestrator: ensureDefault failed", { meta: {
1669
+ locationId: id,
1670
+ error: err instanceof Error ? err.message : String(err)
1671
+ } });
1672
+ out.set(id, this.fallbackPolicy(id));
1673
+ }
1674
+ }
1675
+ return out;
1676
+ }
1677
+ fallbackPolicy(locationId) {
1678
+ return {
1679
+ locationId,
1680
+ enabled: true,
1681
+ retentionCount: DEFAULT_RETENTION_COUNT,
1682
+ cron: DEFAULT_CRON
1683
+ };
1684
+ }
1685
+ toDestinationWire(loc, policy, latest) {
1686
+ const effective = policy ?? this.fallbackPolicy(loc.id);
1687
+ const subId = extractSlug(loc.id);
1688
+ const displayName = effective.label && effective.label.length > 0 ? effective.label : loc.displayName;
1689
+ return {
1690
+ id: loc.id,
1691
+ addonId: ORCHESTRATOR_ADDON_ID,
1692
+ subId,
1693
+ displayName,
1694
+ kind: kindFromProviderId(loc.providerId),
1695
+ triggerSupported: effective.enabled,
1696
+ restoreSupported: true,
1697
+ enabled: effective.enabled,
1698
+ retentionCount: effective.retentionCount,
1699
+ ...effective.label !== void 0 ? { label: effective.label } : {},
1700
+ ...latest !== void 0 ? { lastSuccessAt: latest.createdAt } : {},
1701
+ ...latest !== void 0 ? { lastSuccessSizeBytes: latest.sizeBytes } : {},
1702
+ ...effective.cron !== void 0 ? { cron: effective.cron } : {},
1703
+ ...effective.lastRunAt !== void 0 ? { lastRunAt: effective.lastRunAt } : {},
1704
+ ...effective.cron !== void 0 && effective.cron.trim().length > 0 ? { nextRunAt: this.safeNextDue(effective.cron, effective.lastRunAt) } : {}
1705
+ };
1706
+ }
1707
+ /**
1708
+ * Compute next-due ms-epoch for a cron, or `undefined` on parse error.
1709
+ * Catches so a single malformed cron doesn't blank out the whole
1710
+ * destinations list at the cap surface.
1711
+ */
1712
+ safeNextDue(cron, after) {
1713
+ try {
1714
+ return computeNextDue(cron, after ?? this.scheduleBaselineAt) ?? void 0;
1715
+ } catch {
1716
+ return;
1717
+ }
1718
+ }
1719
+ /**
1720
+ * Aggregate listing — read every `backups` location's `manifests.json`
1721
+ * and return one wire entry per archive, tagged with `destinationId`.
1722
+ * Failures on individual locations are logged and skipped so a single
1723
+ * unreachable destination doesn't blank out the whole UI.
1724
+ */
1725
+ async aggregatedList() {
1726
+ const locations = await this.fetchBackupLocations();
1727
+ const out = [];
1728
+ for (const loc of locations) {
1729
+ let manifest;
1730
+ try {
1731
+ manifest = await readLocationManifest(this.storageApi(), loc.id);
1732
+ } catch (err) {
1733
+ this.ctx.logger.warn("backup list: manifest read failed", { meta: {
1734
+ locationId: loc.id,
1735
+ error: err instanceof Error ? err.message : String(err)
1736
+ } });
1737
+ continue;
1738
+ }
1739
+ for (const a of manifest.archives) {
1740
+ const entry = {
1741
+ id: a.id,
1742
+ destinationId: loc.id,
1743
+ createdAt: a.createdAt,
1744
+ sizeBytes: a.sizeBytes,
1745
+ ...a.label !== void 0 ? { label: a.label } : {},
1746
+ locations: [...a.manifest.locations]
1747
+ };
1748
+ out.push(entry);
1749
+ }
1750
+ }
1751
+ return out;
1752
+ }
1753
+ /**
1754
+ * Build a single archive in the `cache` storage location, then push
1755
+ * it to every selected destination via the storage cap's chunked
1756
+ * upload protocol. Per-destination errors are logged and counted
1757
+ * via allSettled semantics — one bad sink doesn't kill the run. The
1758
+ * cache file is deleted in `finally` so a crash mid-fanout doesn't
1759
+ * leak.
1760
+ */
1761
+ async triggerBackup(input) {
1762
+ if (!this.systemBackup) throw new Error("orchestrator not initialized");
1763
+ const targets = await this.resolveTargets(input?.destinations);
1764
+ if (targets.length === 0) {
1765
+ this.ctx.logger.warn("backup trigger: no destinations selected");
1766
+ return [];
1767
+ }
1768
+ const policies = await this.loadPoliciesIndexed(targets.map((t) => t.id));
1769
+ const archiveId = (0, node_crypto.randomUUID)();
1770
+ const archiveFilename = `${archiveId}.tar.gz`;
1771
+ const cacheRelativePath = `${archiveId}.tar.gz`;
1772
+ const cachePath = await this.ctx.api.storage.resolve.query({
1773
+ location: "cache",
1774
+ relativePath: cacheRelativePath
1775
+ });
1776
+ const result = await this.systemBackup.createArchive({
1777
+ archivePath: cachePath,
1778
+ ...input?.locations ? { locations: input.locations } : {}
1779
+ });
1780
+ try {
1781
+ const entries = [];
1782
+ const createdAt = Date.now();
1783
+ for (const dest of targets) try {
1784
+ const policy = policies.get(dest.id) ?? this.fallbackPolicy(dest.id);
1785
+ const stored = await this.uploadAndIndex({
1786
+ locationId: dest.id,
1787
+ archiveId,
1788
+ archiveFilename,
1789
+ cachePath,
1790
+ sizeBytes: result.sizeBytes,
1791
+ archiveManifest: result.manifest,
1792
+ retentionCount: policy.retentionCount,
1793
+ createdAt,
1794
+ label: formatArchiveLabel(dest.id, createdAt, input?.label)
1795
+ });
1796
+ entries.push(stored);
1797
+ this.ctx.eventBus.emit({
1798
+ id: (0, node_crypto.randomUUID)(),
1799
+ timestamp: /* @__PURE__ */ new Date(),
1800
+ source: {
1801
+ type: "addon",
1802
+ id: "backup-orchestrator"
1803
+ },
1804
+ category: _camstack_types.EventCategory.BackupCompleted,
1805
+ data: {
1806
+ destinationId: dest.id,
1807
+ backupId: stored.id,
1808
+ sizeMB: Math.round((stored.sizeBytes ?? 0) / (1024 * 1024))
1809
+ }
1810
+ });
1811
+ } catch (err) {
1812
+ this.ctx.logger.error("backup trigger: destination failed", { meta: {
1813
+ destinationId: dest.id,
1814
+ error: err instanceof Error ? err.message : String(err)
1815
+ } });
1816
+ }
1817
+ return entries;
1818
+ } finally {
1819
+ try {
1820
+ await node_fs_promises.rm(cachePath, { force: true });
1821
+ } catch {}
1822
+ }
1823
+ }
1824
+ /**
1825
+ * Push the staged archive to a single destination via chunked
1826
+ * upload, then update the per-location manifest. Applies the
1827
+ * destination's retention policy as part of the same lock-protected
1828
+ * manifest mutation so the deletes never race with a concurrent
1829
+ * append.
1830
+ *
1831
+ * Throws on upload / manifest failure — caller logs and continues to
1832
+ * the next destination so a single bad sink doesn't fail the run.
1833
+ */
1834
+ async uploadAndIndex(input) {
1835
+ const { uploadId } = await this.ctx.api.storage.beginUpload.mutate({
1836
+ location: input.locationId,
1837
+ relativePath: input.archiveFilename,
1838
+ sizeBytes: input.sizeBytes
1839
+ });
1840
+ try {
1841
+ let offset = 0;
1842
+ const stream = node_fs.createReadStream(input.cachePath, { highWaterMark: UPLOAD_CHUNK_BYTES });
1843
+ for await (const rawChunk of stream) {
1844
+ const chunk = toBufferChunk(rawChunk);
1845
+ const data = new Uint8Array(chunk.byteLength);
1846
+ data.set(chunk);
1847
+ await this.ctx.api.storage.writeChunk.mutate({
1848
+ uploadId,
1849
+ offset,
1850
+ data
1851
+ });
1852
+ offset += data.byteLength;
1853
+ }
1854
+ await this.ctx.api.storage.finalizeUpload.mutate({ uploadId });
1855
+ } catch (err) {
1856
+ try {
1857
+ await this.ctx.api.storage.abortUpload.mutate({ uploadId });
1858
+ } catch {}
1859
+ throw err;
1860
+ }
1861
+ const entry = buildArchiveEntry({
1862
+ id: input.archiveId,
1863
+ filename: input.archiveFilename,
1864
+ createdAt: input.createdAt,
1865
+ sizeBytes: input.sizeBytes,
1866
+ label: input.label,
1867
+ manifest: input.archiveManifest
1868
+ });
1869
+ await this.manifestLock.run(input.locationId, async () => {
1870
+ const { keep, drop } = applyRetention([...(await readLocationManifest(this.storageApi(), input.locationId)).archives, entry], input.retentionCount);
1871
+ for (const stale of drop) try {
1872
+ await this.ctx.api.storage.delete.mutate({
1873
+ location: input.locationId,
1874
+ relativePath: stale.filename
1875
+ });
1876
+ } catch (err) {
1877
+ this.ctx.logger.warn("backup retention: storage delete failed (continuing)", { meta: {
1878
+ destinationId: input.locationId,
1879
+ backupId: stale.id,
1880
+ filename: stale.filename,
1881
+ error: err instanceof Error ? err.message : String(err)
1882
+ } });
1883
+ }
1884
+ const next = {
1885
+ version: 1,
1886
+ archives: keep
1887
+ };
1888
+ await writeLocationManifest(this.storageApi(), input.locationId, next);
1889
+ });
1890
+ return {
1891
+ id: input.archiveId,
1892
+ destinationId: input.locationId,
1893
+ createdAt: input.createdAt,
1894
+ sizeBytes: input.sizeBytes,
1895
+ label: input.label,
1896
+ locations: [...input.archiveManifest.locations]
1897
+ };
1898
+ }
1899
+ /**
1900
+ * List archives at a single destination — newest-first. Reads only
1901
+ * the per-location `manifests.json` (no per-archive tarball read);
1902
+ * the orchestrator already snapshots each archive's locations into
1903
+ * the manifest at upload time.
1904
+ */
1905
+ async listArchives(input) {
1906
+ let manifest;
1907
+ try {
1908
+ manifest = await readLocationManifest(this.storageApi(), input.destinationId);
1909
+ } catch (err) {
1910
+ this.ctx.logger.warn("backup listArchives: manifest read failed", { meta: {
1911
+ destinationId: input.destinationId,
1912
+ error: err instanceof Error ? err.message : String(err)
1913
+ } });
1914
+ return [];
1915
+ }
1916
+ return [...manifest.archives].toSorted((a, b) => b.createdAt - a.createdAt).map((a) => ({
1917
+ id: a.id,
1918
+ filename: a.filename,
1919
+ createdAt: a.createdAt,
1920
+ sizeBytes: a.sizeBytes,
1921
+ ...a.label !== void 0 ? { label: a.label } : {},
1922
+ locations: [...a.manifest.locations]
1923
+ }));
1924
+ }
1925
+ /**
1926
+ * Upsert the per-destination policy row. Used by the admin-UI
1927
+ * destinations table — toggle enabled / change retention / set
1928
+ * label / set cron schedule. No-ops with a logged warning if the
1929
+ * settings backend lacks structured-table support (no persistence
1930
+ * available).
1931
+ *
1932
+ * Validates `cron` server-side: a malformed expression rejects the
1933
+ * upsert with an actionable error so the operator sees the problem
1934
+ * at save time instead of at scheduler-tick time. Empty / undefined
1935
+ * `cron` clears any prior schedule (manual-only).
1936
+ */
1937
+ async upsertDestinationPolicy(input) {
1938
+ if (!this.policies) {
1939
+ this.ctx.logger.warn("backup upsertDestinationPolicy: no settings-store backend — change will not persist", { meta: { locationId: input.locationId } });
1940
+ return;
1941
+ }
1942
+ const cron = typeof input.cron === "string" ? input.cron.trim() : "";
1943
+ if (cron.length > 0 && !isValidCron(cron)) throw new Error(`Invalid cron expression: "${cron}"`);
1944
+ const existing = await this.policies.get(input.locationId).catch(() => null);
1945
+ await this.policies.upsert({
1946
+ locationId: input.locationId,
1947
+ enabled: input.enabled,
1948
+ retentionCount: input.retentionCount,
1949
+ ...input.label !== void 0 ? { label: input.label } : {},
1950
+ ...cron.length > 0 ? { cron } : {},
1951
+ ...existing?.lastRunAt !== void 0 ? { lastRunAt: existing.lastRunAt } : {}
1952
+ });
1953
+ }
1954
+ /**
1955
+ * Validate a cron expression and peek the next N firing times. The
1956
+ * admin UI's CronEditor calls this on every keystroke (debounced)
1957
+ * to render a live "next-run" preview — invalid expressions surface
1958
+ * the parse error inline instead of waiting for save.
1959
+ */
1960
+ async previewSchedule(input) {
1961
+ const trimmed = input.cron.trim();
1962
+ if (trimmed.length === 0) return {
1963
+ ok: false,
1964
+ error: "Cron expression is empty",
1965
+ nextRuns: []
1966
+ };
1967
+ try {
1968
+ return {
1969
+ ok: true,
1970
+ nextRuns: previewNextRuns(trimmed, input.count ?? 5)
1971
+ };
1972
+ } catch (err) {
1973
+ return {
1974
+ ok: false,
1975
+ error: err instanceof Error ? err.message : "Invalid cron expression",
1976
+ nextRuns: []
1977
+ };
1978
+ }
1979
+ }
1980
+ async getEntries(input) {
1981
+ try {
1982
+ return (await readLocationManifest(this.storageApi(), input.destinationId)).archives.find((a) => a.id === input.backupId)?.manifest ?? null;
1983
+ } catch (err) {
1984
+ this.ctx.logger.warn("backup getEntries: manifest read failed", { meta: {
1985
+ destinationId: input.destinationId,
1986
+ backupId: input.backupId,
1987
+ error: err instanceof Error ? err.message : String(err)
1988
+ } });
1989
+ return null;
1990
+ }
1991
+ }
1992
+ async deleteArchive(input) {
1993
+ await this.manifestLock.run(input.destinationId, async () => {
1994
+ const manifest = await readLocationManifest(this.storageApi(), input.destinationId);
1995
+ const target = manifest.archives.find((a) => a.id === input.backupId);
1996
+ if (!target) throw new Error(`backup delete: backup "${input.backupId}" not found at "${input.destinationId}"`);
1997
+ try {
1998
+ await this.ctx.api.storage.delete.mutate({
1999
+ location: input.destinationId,
2000
+ relativePath: target.filename
2001
+ });
2002
+ } catch (err) {
2003
+ this.ctx.logger.warn("backup delete: storage delete failed (continuing manifest cleanup)", { meta: {
2004
+ destinationId: input.destinationId,
2005
+ filename: target.filename,
2006
+ error: err instanceof Error ? err.message : String(err)
2007
+ } });
2008
+ }
2009
+ const next = {
2010
+ version: 1,
2011
+ archives: manifest.archives.filter((a) => a.id !== input.backupId)
2012
+ };
2013
+ await writeLocationManifest(this.storageApi(), input.destinationId, next);
2014
+ });
2015
+ }
2016
+ /**
2017
+ * Schedule a restore for the next boot. Pulls the archive bytes
2018
+ * down from the destination's storage location via the chunked-
2019
+ * download protocol into the `cache` location, then writes the
2020
+ * usual restore marker against the cached path so the boot-time
2021
+ * apply hook (in server-backend launcher) extracts before any
2022
+ * addon starts.
2023
+ *
2024
+ * `locations` (optional) is propagated into the marker as a
2025
+ * whitelist — only those top-level locations get extracted.
2026
+ */
2027
+ async scheduleRestore(input) {
2028
+ if (!this.systemBackup) throw new Error("orchestrator not initialized");
2029
+ const entry = (await readLocationManifest(this.storageApi(), input.destinationId)).archives.find((a) => a.id === input.backupId);
2030
+ if (!entry) throw new Error(`backup restore: backup "${input.backupId}" not found at location "${input.destinationId}"`);
2031
+ const cachePath = await this.ctx.api.storage.resolve.query({
2032
+ location: "cache",
2033
+ relativePath: `restore-${input.backupId}.tar.gz`
2034
+ });
2035
+ await downloadArchiveToCache({
2036
+ api: this.ctx.api.storage,
2037
+ locationId: input.destinationId,
2038
+ filename: entry.filename,
2039
+ cachePath,
2040
+ chunkBytes: UPLOAD_CHUNK_BYTES,
2041
+ logger: this.ctx.logger
2042
+ });
2043
+ this.systemBackup.scheduleRestoreMarker({
2044
+ archivePath: cachePath,
2045
+ source: input.backupId,
2046
+ ...input.locations ? { locations: input.locations } : {}
2047
+ });
2048
+ this.ctx.eventBus.emit({
2049
+ id: (0, node_crypto.randomUUID)(),
2050
+ timestamp: /* @__PURE__ */ new Date(),
2051
+ source: {
2052
+ type: "addon",
2053
+ id: "backup-orchestrator"
2054
+ },
2055
+ category: _camstack_types.EventCategory.BackupRestored,
2056
+ data: {
2057
+ backupId: input.backupId,
2058
+ destinationId: input.destinationId
2059
+ }
2060
+ });
2061
+ }
2062
+ /**
2063
+ * Resolve a target destination set: caller-provided list wins;
2064
+ * otherwise fall back to every destination flagged as enabled in
2065
+ * its policy row (`triggerSupported !== false` after the policy
2066
+ * join). The old "global enabledDestinations override" was retired
2067
+ * along with the orchestrator settings panel — per-destination
2068
+ * `enabled` toggles in the destinations table are now the only
2069
+ * routing knob.
2070
+ */
2071
+ async resolveTargets(requested) {
2072
+ const all = await this.listDestinations();
2073
+ if (requested && requested.length > 0) return all.filter((d) => requested.includes(d.id));
2074
+ return all.filter((d) => d.triggerSupported !== false);
2075
+ }
2076
+ startScheduleTimer() {
2077
+ if (this.scheduleTimer) return;
2078
+ this.scheduleTimer = setInterval(() => {
2079
+ this.onScheduleTick();
2080
+ }, SCHEDULE_TICK_MS);
2081
+ }
2082
+ stopScheduleTimer() {
2083
+ if (!this.scheduleTimer) return;
2084
+ clearInterval(this.scheduleTimer);
2085
+ this.scheduleTimer = null;
2086
+ }
2087
+ /**
2088
+ * Per-destination cron-based scheduling. Walks every enabled
2089
+ * destination policy that carries a `cron` expression, computes the
2090
+ * next due time after `lastRunAt` (or the orchestrator's first-tick
2091
+ * baseline if never run), and fires a backup targeting that single
2092
+ * destination when due. Each destination keeps its own cadence —
2093
+ * an offsite SFTP can run weekly while local stays nightly without
2094
+ * any global setting.
2095
+ *
2096
+ * Errors per destination are isolated; one misconfigured cron
2097
+ * doesn't block the others. The policy's `lastRunAt` is updated
2098
+ * BEFORE the trigger to dedupe (the tick runs every
2099
+ * SCHEDULE_TICK_MS — without the up-front update a long-running
2100
+ * upload could overlap with the next tick's same-due check).
2101
+ */
2102
+ async onScheduleTick() {
2103
+ if (!this.policies) return;
2104
+ const policies = await this.policies.list();
2105
+ const now = Date.now();
2106
+ for (const p of policies) {
2107
+ if (!p.enabled || !p.cron || p.cron.trim().length === 0) continue;
2108
+ let due;
2109
+ try {
2110
+ due = computeNextDue(p.cron, p.lastRunAt ?? this.scheduleBaselineAt);
2111
+ } catch (err) {
2112
+ this.ctx.logger.warn("Invalid cron — skipping destination", { meta: {
2113
+ locationId: p.locationId,
2114
+ cron: p.cron,
2115
+ error: err instanceof Error ? err.message : String(err)
2116
+ } });
2117
+ continue;
2118
+ }
2119
+ if (due === null || due > now) continue;
2120
+ const stamped = {
2121
+ ...p,
2122
+ lastRunAt: now
2123
+ };
2124
+ try {
2125
+ await this.policies.upsert(stamped);
2126
+ } catch (err) {
2127
+ this.ctx.logger.warn("Failed to persist scheduled-run timestamp", { meta: {
2128
+ locationId: p.locationId,
2129
+ error: err instanceof Error ? err.message : String(err)
2130
+ } });
2131
+ }
2132
+ try {
2133
+ this.ctx.logger.info("Scheduled backup firing for destination", { meta: {
2134
+ locationId: p.locationId,
2135
+ cron: p.cron,
2136
+ dueAt: due
2137
+ } });
2138
+ await this.triggerBackup({ destinations: [p.locationId] });
2139
+ } catch (err) {
2140
+ this.ctx.logger.error("Scheduled backup failed", { meta: {
2141
+ locationId: p.locationId,
2142
+ error: err instanceof Error ? err.message : String(err)
2143
+ } });
2144
+ }
2145
+ }
2146
+ }
2147
+ resolvePolicyService() {
2148
+ const reg = this.ctx.kernel.capabilityRegistry;
2149
+ if (!reg) return null;
2150
+ const backend = reg.getSingleton("settings-store");
2151
+ if (!backend) {
2152
+ this.ctx.logger.warn("backup-orchestrator: no settings-store provider — destination policies will not persist");
2153
+ return null;
2154
+ }
2155
+ if (!backend.ensureTable || !backend.tableInsert) {
2156
+ this.ctx.logger.warn("backup-orchestrator: settings backend lacks structured-table support — destination policies will not persist");
2157
+ return null;
2158
+ }
2159
+ return new BackupDestinationPolicyService(backend);
2160
+ }
2161
+ /**
2162
+ * Narrow `ctx.api.storage` to the subset the manifest helpers need.
2163
+ * Lets the helpers be unit-tested with a Map-backed fake without
2164
+ * pulling in the full `AddonApi` shape.
2165
+ */
2166
+ storageApi() {
2167
+ return this.ctx.api.storage;
2168
+ }
2169
+ };
2170
+ /**
2171
+ * Map a `storage-provider` providerId to an operator-friendly `kind`
2172
+ * the admin UI uses for destination cards. Falls back to the raw
2173
+ * provider id so unknown providers still surface (operator sees
2174
+ * `webdav-storage` instead of a blank slot).
2175
+ */
2176
+ function kindFromProviderId(providerId) {
2177
+ switch (providerId) {
2178
+ case "filesystem-storage": return "local";
2179
+ case "s3-storage": return "s3";
2180
+ case "sftp-storage": return "sftp";
2181
+ case "webdav-storage": return "webdav";
2182
+ default: return providerId;
2183
+ }
2184
+ }
2185
+ /**
2186
+ * Extract the slug part of a `<type>:<slug>` storage-location id.
2187
+ * Defensive: if the id doesn't contain a colon (shouldn't happen given
2188
+ * the regex on `StorageLocationSchema.id`) we return the whole string.
2189
+ */
2190
+ function extractSlug(id) {
2191
+ const colon = id.indexOf(":");
2192
+ return colon < 0 ? id : id.slice(colon + 1);
2193
+ }
2194
+ /**
2195
+ * Narrow a `for await` chunk from `fs.createReadStream` to a
2196
+ * `Buffer`. The stream yields `string | Buffer` per its public type
2197
+ * even though the no-encoding overload always emits `Buffer`. The
2198
+ * runtime check + clear error keeps eslint's no-unsafe-* rules happy
2199
+ * without resorting to `as Buffer`.
2200
+ */
2201
+ function toBufferChunk(value) {
2202
+ if (Buffer.isBuffer(value)) return value;
2203
+ if (value instanceof Uint8Array) return value;
2204
+ throw new Error("backup upload: unexpected non-binary chunk from read stream");
2205
+ }
2206
+ /**
2207
+ * Return the most-recent archive in a location manifest, or undefined
2208
+ * when the location has no archives yet. Pure transform — no I/O.
2209
+ */
2210
+ function latestArchiveOf(manifest) {
2211
+ if (manifest.archives.length === 0) return void 0;
2212
+ let newest = manifest.archives[0];
2213
+ for (const a of manifest.archives) if (a.createdAt > newest.createdAt) newest = a;
2214
+ return newest;
2215
+ }
2216
+ /**
2217
+ * Tiny `path.dirname`-equivalent that only depends on `/` and `\`.
2218
+ * Avoids a `node:path` import in this module purely so the
2219
+ * destination-policy.ts and manifest-store.ts neighbours stay node-
2220
+ * builtin-free at the import surface.
2221
+ */
2222
+ function parentDir(p) {
2223
+ const sep = p.includes("/") ? "/" : "\\";
2224
+ const i = p.lastIndexOf(sep);
2225
+ return i <= 0 ? "" : p.slice(0, i);
2226
+ }
2227
+ //#endregion
2228
+ exports.BackupOrchestratorAddon = BackupOrchestratorAddon;
2229
+ exports.default = BackupOrchestratorAddon;