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