@camstack/system 1.2.20 → 1.2.22

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 (48) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +26 -13
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1366 -1098
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1366 -1098
  10. package/dist/builtins/backup-orchestrator/schedule-store.d.ts +33 -0
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  14. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  15. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  17. package/dist/builtins/hub-forwarder/index.js +1 -1
  18. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  19. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  20. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  21. package/dist/builtins/local-network/local-network.addon.js +1 -1
  22. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  23. package/dist/builtins/loki-logging/index.js +1 -1
  24. package/dist/builtins/loki-logging/index.mjs +1 -1
  25. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  26. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  27. package/dist/builtins/platform-probe/index.js +1 -1
  28. package/dist/builtins/platform-probe/index.mjs +1 -1
  29. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  30. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  31. package/dist/builtins/snapshot/index.js +1 -1
  32. package/dist/builtins/snapshot/index.mjs +1 -1
  33. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  34. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  35. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  36. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  37. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +5 -3
  38. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +5 -3
  39. package/dist/builtins/storage-orchestrator/storage-orchestrator.service.d.ts +6 -0
  40. package/dist/builtins/system-config/system-config.addon.js +1 -1
  41. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  42. package/dist/builtins/winston-logging/index.js +1 -1
  43. package/dist/builtins/winston-logging/index.mjs +1 -1
  44. package/dist/{dist-DmQ6thmF.mjs → dist-C9_XdXl5.mjs} +100 -4
  45. package/dist/{dist-OQ1yU92D.js → dist-j49ewCPY.js} +100 -4
  46. package/dist/index.js +1 -1
  47. package/dist/index.mjs +1 -1
  48. package/package.json +3 -2
@@ -1,4 +1,4 @@
1
- import { at as BaseAddon, jt as EventCategory, y as backupCapability } from "../../dist-DmQ6thmF.mjs";
1
+ import { at as BaseAddon, jt as EventCategory, y as backupCapability } from "../../dist-C9_XdXl5.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import Kt from "node:fs";
4
4
  import * as path$1 from "node:path";
@@ -3115,13 +3115,20 @@ var SystemBackupService = class {
3115
3115
  try {
3116
3116
  const stagedInDataDir = path$1.join(this.dataDir, ARCHIVE_MANIFEST_NAME);
3117
3117
  fs.copyFileSync(manifestStagePath, stagedInDataDir);
3118
+ const stagingRel = path$1.relative(this.dataDir, path$1.dirname(opts.archivePath));
3119
+ const excludePrefix = stagingRel && !stagingRel.startsWith("..") && !path$1.isAbsolute(stagingRel) ? stagingRel : null;
3118
3120
  try {
3119
3121
  await Kn({
3120
3122
  file: opts.archivePath,
3121
3123
  gzip: true,
3122
3124
  cwd: this.dataDir,
3123
3125
  follow: true,
3124
- portable: true
3126
+ portable: true,
3127
+ filter: (entryPath) => {
3128
+ if (!excludePrefix) return true;
3129
+ const norm = entryPath.replace(/^\.\//, "").replace(/\/+$/, "");
3130
+ return norm !== excludePrefix && !norm.startsWith(`${excludePrefix}/`);
3131
+ }
3125
3132
  }, [ARCHIVE_MANIFEST_NAME, ...present]);
3126
3133
  } finally {
3127
3134
  try {
@@ -3388,1166 +3395,1302 @@ function entryMatchesLocation(entryPath, prefix) {
3388
3395
  return entryPath === prefix || entryPath.startsWith(`${prefix}/`);
3389
3396
  }
3390
3397
  //#endregion
3391
- //#region src/builtins/backup-orchestrator/destination-policy.ts
3392
- var TABLE_NAME = "backup_destination_policies";
3393
- var BACKUP_DESTINATION_POLICIES_SCHEMA = { columns: [
3394
- {
3395
- name: "location_id",
3396
- type: "TEXT",
3397
- primaryKey: true
3398
- },
3399
- {
3400
- name: "enabled",
3401
- type: "INTEGER",
3402
- notNull: true,
3403
- defaultValue: 1
3404
- },
3405
- {
3406
- name: "retention_count",
3407
- type: "INTEGER",
3408
- notNull: true,
3409
- defaultValue: 7
3410
- },
3411
- {
3412
- name: "label",
3413
- type: "TEXT"
3414
- },
3415
- {
3416
- name: "cron",
3417
- type: "TEXT"
3418
- },
3419
- {
3420
- name: "last_run_at",
3421
- type: "INTEGER"
3422
- }
3423
- ] };
3424
- var BackupDestinationPolicyService = class {
3425
- backend;
3426
- tableEnsured = false;
3427
- constructor(backend) {
3428
- this.backend = backend;
3398
+ //#region ../../node_modules/croner/dist/croner.js
3399
+ function T(s) {
3400
+ return Date.UTC(s.y, s.m - 1, s.d, s.h, s.i, s.s);
3401
+ }
3402
+ function D(s, e) {
3403
+ 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;
3404
+ }
3405
+ function A(s, e) {
3406
+ let t = new Date(Date.parse(s));
3407
+ if (isNaN(t)) throw new Error("Invalid ISO8601 passed to timezone parser.");
3408
+ let r = s.substring(9);
3409
+ 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);
3410
+ }
3411
+ function v(s, e, t) {
3412
+ return k(A(s, e), t);
3413
+ }
3414
+ function k(s, e) {
3415
+ 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);
3416
+ if (D(h, s)) {
3417
+ let u = /* @__PURE__ */ new Date(o.getTime() - 36e5);
3418
+ return D(g(u, s.tz), s) ? u : o;
3429
3419
  }
3430
- /**
3431
- * Lazy `ensureTable` called from every read/write path. The
3432
- * settings backend's own `ensureTable` is idempotent (additive
3433
- * migration), so repeated calls collapse to a no-op after the first.
3434
- * This keeps the constructor synchronous and avoids a separate
3435
- * `initialize()` step that callers could forget.
3436
- */
3437
- async initialize() {
3438
- await this.ensureTable();
3420
+ let l = new Date(o.getTime() + T(s) - T(h));
3421
+ if (D(g(l, s.tz), s)) return l;
3422
+ if (e) throw new Error("Invalid date passed to fromTZ()");
3423
+ return o.getTime() > l.getTime() ? o : l;
3424
+ }
3425
+ function g(s, e) {
3426
+ let t, r;
3427
+ try {
3428
+ t = new Intl.DateTimeFormat("en-US", {
3429
+ timeZone: e,
3430
+ year: "numeric",
3431
+ month: "numeric",
3432
+ day: "numeric",
3433
+ hour: "numeric",
3434
+ minute: "numeric",
3435
+ second: "numeric",
3436
+ hour12: !1
3437
+ }), r = t.formatToParts(s);
3438
+ } catch (i) {
3439
+ let a = i instanceof Error ? i.message : String(i);
3440
+ 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}`);
3439
3441
  }
3440
- async ensureTable() {
3441
- if (this.tableEnsured) return;
3442
- 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");
3443
- await this.backend.ensureTable(TABLE_NAME, BACKUP_DESTINATION_POLICIES_SCHEMA);
3444
- this.tableEnsured = true;
3442
+ let n = {
3443
+ year: 0,
3444
+ month: 0,
3445
+ day: 0,
3446
+ hour: 0,
3447
+ minute: 0,
3448
+ second: 0
3449
+ };
3450
+ 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));
3451
+ 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)}`);
3452
+ return n.hour === 24 && (n.hour = 0), {
3453
+ y: n.year,
3454
+ m: n.month,
3455
+ d: n.day,
3456
+ h: n.hour,
3457
+ i: n.minute,
3458
+ s: n.second,
3459
+ tz: e
3460
+ };
3461
+ }
3462
+ function b(s, e, t, r, n, i, a) {
3463
+ return {
3464
+ y: s,
3465
+ m: e,
3466
+ d: t,
3467
+ h: r,
3468
+ i: n,
3469
+ s: i,
3470
+ tz: a
3471
+ };
3472
+ }
3473
+ var O = [
3474
+ 1,
3475
+ 2,
3476
+ 4,
3477
+ 8,
3478
+ 16
3479
+ ], C = class {
3480
+ pattern;
3481
+ timezone;
3482
+ mode;
3483
+ alternativeWeekdays;
3484
+ sloppyRanges;
3485
+ second;
3486
+ minute;
3487
+ hour;
3488
+ day;
3489
+ month;
3490
+ dayOfWeek;
3491
+ year;
3492
+ lastDayOfMonth;
3493
+ lastWeekday;
3494
+ nearestWeekdays;
3495
+ starDOM;
3496
+ starDOW;
3497
+ starYear;
3498
+ useAndLogic;
3499
+ constructor(e, t, r) {
3500
+ 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();
3445
3501
  }
3446
- async list() {
3447
- await this.ensureTable();
3448
- if (!this.backend.tableQuery) return [];
3449
- const rows = await this.backend.tableQuery(TABLE_NAME, { orderBy: {
3450
- field: "location_id",
3451
- direction: "asc"
3452
- } });
3453
- const out = [];
3454
- for (const row of rows) {
3455
- const mapped = mapRowToPolicy(row);
3456
- if (mapped) out.push(mapped);
3502
+ parse() {
3503
+ if (!(typeof this.pattern == "string" || this.pattern instanceof String)) throw new TypeError("CronPattern: Pattern has to be of type string.");
3504
+ this.pattern.indexOf("@") >= 0 && (this.pattern = this.handleNicknames(this.pattern).trim());
3505
+ let e = this.pattern.match(/\S+/g) || [""], t = e.length;
3506
+ 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.");
3507
+ if (this.mode !== "auto") {
3508
+ let n;
3509
+ switch (this.mode) {
3510
+ case "5-part":
3511
+ n = 5;
3512
+ break;
3513
+ case "6-part":
3514
+ n = 6;
3515
+ break;
3516
+ case "7-part":
3517
+ n = 7;
3518
+ break;
3519
+ case "5-or-6-parts":
3520
+ n = [5, 6];
3521
+ break;
3522
+ case "6-or-7-parts":
3523
+ n = [6, 7];
3524
+ break;
3525
+ default: n = 0;
3526
+ }
3527
+ if (!(Array.isArray(n) ? n.includes(t) : t === n)) {
3528
+ let a = Array.isArray(n) ? n.join(" or ") : n.toString();
3529
+ throw new TypeError(`CronPattern: mode '${this.mode}' requires exactly ${a} parts, but pattern '${this.pattern}' has ${t} parts.`);
3530
+ }
3457
3531
  }
3458
- return out;
3532
+ 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.");
3533
+ 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) {
3534
+ case "5-part":
3535
+ e[0] = "0", e[6] = "*";
3536
+ break;
3537
+ case "6-part":
3538
+ e[6] = "*";
3539
+ break;
3540
+ case "5-or-6-parts":
3541
+ e[6] = "*";
3542
+ break;
3543
+ case "6-or-7-parts": break;
3544
+ case "7-part":
3545
+ case "auto": break;
3546
+ }
3547
+ 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);
3548
+ let r = this.alternativeWeekdays ? -1 : 0;
3549
+ 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]);
3459
3550
  }
3460
- async get(locationId) {
3461
- await this.ensureTable();
3462
- if (!this.backend.tableGet) return null;
3463
- const row = await this.backend.tableGet(TABLE_NAME, { location_id: locationId });
3464
- return row ? mapRowToPolicy(row) : null;
3551
+ partToArray(e, t, r, n) {
3552
+ let i = this[e], a = e === "day" && this.lastDayOfMonth, o = e === "day" && this.lastWeekday;
3553
+ if (t === "" && !a && !o) throw new TypeError("CronPattern: configuration entry " + e + " (" + t + ") is empty, check for trailing spaces.");
3554
+ if (t === "*") return i.fill(n);
3555
+ let h = t.split(",");
3556
+ if (h.length > 1) for (let l = 0; l < h.length; l++) this.partToArray(e, h[l], r, n);
3557
+ 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);
3465
3558
  }
3466
- /**
3467
- * Replace-or-insert. Implemented as `delete` + `insert` (mirrors the
3468
- * `SqliteLocationStore` contract) so backends that don't surface
3469
- * `tableUpdate` still work.
3470
- */
3471
- async upsert(p) {
3472
- await this.ensureTable();
3473
- if (!this.backend.tableDelete || !this.backend.tableInsert) throw new Error("BackupDestinationPolicyService: backend missing tableDelete/tableInsert");
3474
- await this.backend.tableDelete(TABLE_NAME, { location_id: p.locationId });
3475
- await this.backend.tableInsert(TABLE_NAME, mapPolicyToRow(p));
3559
+ throwAtIllegalCharacters(e) {
3560
+ 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.");
3476
3561
  }
3477
- async delete(locationId) {
3478
- await this.ensureTable();
3479
- if (!this.backend.tableDelete) throw new Error("BackupDestinationPolicyService: backend missing tableDelete");
3480
- await this.backend.tableDelete(TABLE_NAME, { location_id: locationId });
3562
+ handleNumber(e, t, r, n) {
3563
+ let i = this.extractNth(e, t), a = e.toUpperCase().includes("W");
3564
+ if (t !== "day" && a) throw new TypeError("CronPattern: Nearest weekday modifier (W) only allowed in day-of-month.");
3565
+ a && (t = "nearestWeekdays");
3566
+ let o = parseInt(i[0], 10) + r;
3567
+ if (isNaN(o)) throw new TypeError("CronPattern: " + t + " is not a number: '" + e + "'");
3568
+ this.setPart(t, o, i[1] || n);
3481
3569
  }
3482
- /**
3483
- * Auto-create a default policy for a `backups` location seen for the
3484
- * first time (returns the existing policy unchanged when present).
3485
- * The orchestrator calls this lazily on the first `listDestinations`
3486
- * pass so newly-added storage locations are surfaced as enabled
3487
- * destinations without an explicit configuration step.
3488
- *
3489
- * Defaults are passed by the orchestrator (currently:
3490
- * `retentionCount=7` + `cron='0 3 * * *'`) so a fresh destination
3491
- * starts running daily at 03:00 with 7 archives kept. Operator can
3492
- * tune both inline on the destinations table.
3493
- */
3494
- async ensureDefault(locationId, defaults) {
3495
- const existing = await this.get(locationId);
3496
- if (existing) return existing;
3497
- const fresh = {
3498
- locationId,
3499
- enabled: true,
3500
- retentionCount: defaults.retentionCount,
3501
- ...defaults.cron !== void 0 ? { cron: defaults.cron } : {}
3502
- };
3503
- await this.upsert(fresh);
3504
- return fresh;
3570
+ setPart(e, t, r) {
3571
+ if (!Object.prototype.hasOwnProperty.call(this, e)) throw new TypeError("CronPattern: Invalid part specified: " + e);
3572
+ if (e === "dayOfWeek") {
3573
+ if (t === 7 && (t = 0), t < 0 || t > 6) throw new RangeError("CronPattern: Invalid value for dayOfWeek: " + t);
3574
+ this.setNthWeekdayOfMonth(t, r);
3575
+ return;
3576
+ }
3577
+ if (e === "second" || e === "minute") {
3578
+ if (t < 0 || t >= 60) throw new RangeError("CronPattern: Invalid value for " + e + ": " + t);
3579
+ } else if (e === "hour") {
3580
+ if (t < 0 || t >= 24) throw new RangeError("CronPattern: Invalid value for " + e + ": " + t);
3581
+ } else if (e === "day" || e === "nearestWeekdays") {
3582
+ if (t < 0 || t >= 31) throw new RangeError("CronPattern: Invalid value for " + e + ": " + t);
3583
+ } else if (e === "month") {
3584
+ if (t < 0 || t >= 12) throw new RangeError("CronPattern: Invalid value for " + e + ": " + t);
3585
+ } else if (e === "year" && (t < 1 || t >= 1e4)) throw new RangeError("CronPattern: Invalid value for " + e + ": " + t + " (supported range: 1-9999)");
3586
+ this[e][t] = r;
3505
3587
  }
3506
- };
3507
- function mapPolicyToRow(p) {
3508
- return {
3509
- location_id: p.locationId,
3510
- enabled: p.enabled ? 1 : 0,
3511
- retention_count: p.retentionCount,
3512
- label: p.label ?? null,
3513
- cron: p.cron ?? null,
3514
- last_run_at: p.lastRunAt ?? null
3515
- };
3516
- }
3517
- /**
3518
- * Decode a row pulled from the structured table. Defensive against
3519
- * schema drift / partial migrations: returns `null` on missing
3520
- * `location_id` so corrupt rows skip the result list rather than
3521
- * throwing the whole orchestrator down at boot.
3522
- */
3523
- function mapRowToPolicy(row) {
3524
- const locationId = String(row["location_id"] ?? "");
3525
- if (!locationId) return null;
3526
- const labelRaw = row["label"];
3527
- const label = typeof labelRaw === "string" && labelRaw.length > 0 ? labelRaw : void 0;
3528
- const cronRaw = row["cron"];
3529
- const cron = typeof cronRaw === "string" && cronRaw.length > 0 ? cronRaw : void 0;
3530
- const lastRunRaw = row["last_run_at"];
3531
- const lastRunAt = typeof lastRunRaw === "number" && lastRunRaw > 0 ? lastRunRaw : void 0;
3532
- return {
3533
- locationId,
3534
- enabled: row["enabled"] === 1 || row["enabled"] === true,
3535
- retentionCount: Number(row["retention_count"] ?? 0),
3536
- ...label !== void 0 ? { label } : {},
3537
- ...cron !== void 0 ? { cron } : {},
3538
- ...lastRunAt !== void 0 ? { lastRunAt } : {}
3539
- };
3540
- }
3541
- //#endregion
3542
- //#region src/builtins/backup-orchestrator/manifest-store.ts
3543
- /** File path (relative to the location root) where the index lives. */
3544
- var MANIFESTS_FILENAME = "manifests.json";
3545
- /**
3546
- * Read the per-location manifest. Returns an empty manifest on first
3547
- * use (when `manifests.json` doesn't exist) or when the file is
3548
- * unreadable / corrupt — operators see destination state degrade
3549
- * gracefully rather than the orchestrator crashing.
3550
- */
3551
- async function readLocationManifest(api, locationId) {
3552
- if (!await api.exists.query({
3553
- location: locationId,
3554
- relativePath: "manifests.json"
3555
- }).catch(() => false)) return EMPTY_MANIFEST;
3556
- let bytes;
3557
- try {
3558
- bytes = await api.read.query({
3559
- location: locationId,
3560
- relativePath: MANIFESTS_FILENAME
3561
- });
3562
- } catch {
3563
- return EMPTY_MANIFEST;
3588
+ validateNotNaN(e, t) {
3589
+ if (isNaN(e)) throw new TypeError(t);
3564
3590
  }
3565
- return parseManifestBytes(bytes);
3566
- }
3567
- async function writeLocationManifest(api, locationId, manifest) {
3568
- const json = JSON.stringify(manifest, null, 2);
3569
- const bytes = new TextEncoder().encode(json);
3570
- await api.write.mutate({
3571
- location: locationId,
3572
- relativePath: MANIFESTS_FILENAME,
3573
- data: bytes
3574
- });
3575
- }
3576
- /**
3577
- * Decode the raw bytes of `manifests.json`. Falls back to an empty
3578
- * manifest on JSON-parse / shape-validation failure — same defensive
3579
- * stance as the legacy `LocalBackupService.ensureManifestsLoaded`.
3580
- */
3581
- function parseManifestBytes(bytes) {
3582
- try {
3583
- const text = new TextDecoder().decode(bytes);
3584
- if (text.trim().length === 0) return EMPTY_MANIFEST;
3585
- return normalizeManifest(JSON.parse(text));
3586
- } catch {
3587
- return EMPTY_MANIFEST;
3591
+ validateRange(e, t, r, n, i) {
3592
+ if (e > t) throw new TypeError("CronPattern: From value is larger than to value: '" + i + "'");
3593
+ if (r !== void 0) {
3594
+ if (r === 0) throw new TypeError("CronPattern: Syntax error, illegal stepping: 0");
3595
+ if (r > this[n].length) throw new TypeError("CronPattern: Syntax error, steps cannot be greater than maximum value of part (" + this[n].length + ")");
3596
+ }
3588
3597
  }
3589
- }
3590
- /**
3591
- * Coerce an arbitrary parsed JSON shape into the canonical manifest
3592
- * type. Bogus archive entries are silently dropped (a corrupt row
3593
- * shouldn't lock the whole location out of operator visibility).
3594
- */
3595
- function normalizeManifest(raw) {
3596
- if (typeof raw !== "object" || raw == null) return EMPTY_MANIFEST;
3597
- const obj = raw;
3598
- const archives = [];
3599
- if (Array.isArray(obj.archives)) for (const a of obj.archives) {
3600
- if (typeof a !== "object" || a == null) continue;
3601
- const cand = a;
3602
- if (typeof cand["id"] !== "string" || typeof cand["filename"] !== "string") continue;
3603
- if (typeof cand["createdAt"] !== "number" || typeof cand["sizeBytes"] !== "number") continue;
3604
- const manifest = cand["manifest"];
3605
- if (typeof manifest !== "object" || manifest == null) continue;
3606
- const entry = {
3607
- id: String(cand["id"]),
3608
- filename: String(cand["filename"]),
3609
- createdAt: Number(cand["createdAt"]),
3610
- sizeBytes: Number(cand["sizeBytes"]),
3611
- ...typeof cand["label"] === "string" ? { label: cand["label"] } : {},
3612
- manifest
3613
- };
3614
- archives.push(entry);
3598
+ handleRangeWithStepping(e, t, r, n) {
3599
+ if (e.toUpperCase().includes("W")) throw new TypeError("CronPattern: Syntax error, W is not allowed in ranges with stepping.");
3600
+ let i = this.extractNth(e, t), a = i[0].match(/^(\d+)-(\d+)\/(\d+)$/);
3601
+ if (a === null) throw new TypeError("CronPattern: Syntax error, illegal range with stepping: '" + e + "'");
3602
+ let [, o, h, l] = a, y = parseInt(o, 10) + r, u = parseInt(h, 10) + r, d = parseInt(l, 10);
3603
+ 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);
3604
+ for (let c = y; c <= u; c += d) this.setPart(t, c, i[1] || n);
3615
3605
  }
3616
- return {
3617
- version: 1,
3618
- archives
3619
- };
3620
- }
3621
- var EMPTY_MANIFEST = {
3622
- version: 1,
3623
- archives: []
3624
- };
3625
- /**
3626
- * Build a `ManifestArchiveEntry` from the raw inputs collected during
3627
- * a successful upload. Pure transform — no I/O — kept here so the
3628
- * triggerBackup path stays terse.
3629
- */
3630
- function buildArchiveEntry(input) {
3631
- return {
3632
- id: input.id,
3633
- filename: input.filename,
3634
- createdAt: input.createdAt,
3635
- sizeBytes: input.sizeBytes,
3636
- ...input.label !== void 0 ? { label: input.label } : {},
3637
- manifest: input.manifest
3638
- };
3639
- }
3640
- /**
3641
- * Pure retention split: given an archive list and the operator's
3642
- * `retentionCount`, return `{ keep, drop }` where `keep` is the most-
3643
- * recent N archives (newest first) and `drop` is the older overflow.
3644
- *
3645
- * `retentionCount <= 0` is treated as "no pruning" — defensive against
3646
- * a config value that would otherwise wipe every archive on the next
3647
- * write.
3648
- */
3649
- function applyRetention(archives, retentionCount) {
3650
- if (retentionCount <= 0) return {
3651
- keep: archives,
3652
- drop: []
3653
- };
3654
- const sorted = [...archives].toSorted((a, b) => b.createdAt - a.createdAt);
3655
- if (sorted.length <= retentionCount) return {
3656
- keep: sorted,
3657
- drop: []
3658
- };
3659
- return {
3660
- keep: sorted.slice(0, retentionCount),
3661
- drop: sorted.slice(retentionCount)
3662
- };
3663
- }
3664
- /**
3665
- * Per-location async mutex. The orchestrator owns one instance and
3666
- * funnels every read-modify-write of `manifests.json` through it so
3667
- * parallel destination uploads (within the same location) commit in
3668
- * arrival order. Across locations the operations run in parallel —
3669
- * each location keys its own promise chain.
3670
- */
3671
- var LocationManifestLock = class {
3672
- chains = /* @__PURE__ */ new Map();
3673
- async run(locationId, fn) {
3674
- const previous = this.chains.get(locationId) ?? Promise.resolve();
3675
- let resolveNext;
3676
- const next = new Promise((res) => {
3677
- resolveNext = res;
3678
- });
3679
- this.chains.set(locationId, previous.then(() => next));
3680
- try {
3681
- await previous;
3682
- return await fn();
3683
- } finally {
3684
- resolveNext();
3685
- const tail = this.chains.get(locationId);
3686
- if (tail) Promise.resolve(tail).then(() => {
3687
- if (this.chains.get(locationId) === tail) this.chains.delete(locationId);
3688
- }).catch(() => {});
3606
+ extractNth(e, t) {
3607
+ let r = e, n;
3608
+ if (r.includes("#")) {
3609
+ if (t !== "dayOfWeek") throw new Error("CronPattern: nth (#) only allowed in day-of-week field");
3610
+ n = r.split("#")[1], r = r.split("#")[0];
3611
+ } else if (r.toUpperCase().endsWith("L")) {
3612
+ if (t !== "dayOfWeek") throw new Error("CronPattern: L modifier only allowed in day-of-week field (use L alone for day-of-month)");
3613
+ n = "L", r = r.slice(0, -1);
3689
3614
  }
3615
+ return [r, n];
3690
3616
  }
3691
- };
3692
- //#endregion
3693
- //#region src/builtins/backup-orchestrator/download-helpers.ts
3694
- /**
3695
- * Chunked-download helper for the backup orchestrator (Task 12 of
3696
- * the storage-unification refactor).
3697
- *
3698
- * Streams an archive from a destination `StorageLocation` into a
3699
- * local filesystem path via the consumer-facing `storage` cap. Pulled
3700
- * out of `backup-orchestrator.addon.ts` so it can be unit-tested with
3701
- * an in-memory fake `AddonApi['storage']` slice the addon is hard
3702
- * to bootstrap in vitest because of the BaseAddon ceremony.
3703
- *
3704
- * Cleanup contract:
3705
- * - Successful run leaves the cache file in place + calls
3706
- * `endDownload`.
3707
- * - Mid-stream failure destroys the writeStream, removes the
3708
- * partial cache file, and best-effort calls `endDownload` so the
3709
- * storage provider's session bookkeeping doesn't leak.
3710
- */
3711
- async function downloadArchiveToCache(input) {
3712
- const { downloadId, sizeBytes } = await input.api.beginDownload.mutate({
3713
- location: input.locationId,
3714
- relativePath: input.filename
3715
- });
3716
- const writeStream = fs.createWriteStream(input.cachePath);
3717
- let offset = 0;
3718
- try {
3719
- while (offset < sizeBytes) {
3720
- const remaining = sizeBytes - offset;
3721
- const length = Math.min(input.chunkBytes, remaining);
3722
- const chunk = await input.api.readChunk.query({
3723
- downloadId,
3724
- offset,
3725
- length
3726
- });
3727
- if (chunk.byteLength === 0) throw new Error(`backup restore: empty chunk at offset ${offset}/${sizeBytes} from "${input.locationId}/${input.filename}"`);
3728
- writeStream.write(chunk);
3729
- offset += chunk.byteLength;
3730
- }
3731
- await new Promise((resolve, reject) => {
3732
- writeStream.end((err) => {
3733
- if (err) reject(err);
3734
- else resolve();
3735
- });
3736
- });
3737
- try {
3738
- await input.api.endDownload.mutate({ downloadId });
3739
- } catch (err) {
3740
- input.logger.warn("backup restore: endDownload failed (ignoring)", { meta: {
3741
- downloadId,
3742
- error: err instanceof Error ? err.message : String(err)
3743
- } });
3617
+ handleRange(e, t, r, n) {
3618
+ if (e.toUpperCase().includes("W")) throw new TypeError("CronPattern: Syntax error, W is not allowed in a range.");
3619
+ let i = this.extractNth(e, t), a = i[0].split("-");
3620
+ if (a.length !== 2) throw new TypeError("CronPattern: Syntax error, illegal range: '" + e + "'");
3621
+ let o = parseInt(a[0], 10) + r, h = parseInt(a[1], 10) + r;
3622
+ 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);
3623
+ for (let l = o; l <= h; l++) this.setPart(t, l, i[1] || n);
3624
+ }
3625
+ handleStepping(e, t, r, n) {
3626
+ if (e.toUpperCase().includes("W")) throw new TypeError("CronPattern: Syntax error, W is not allowed in parts with stepping.");
3627
+ let i = this.extractNth(e, t), a = i[0].split("/");
3628
+ if (a.length !== 2) throw new TypeError("CronPattern: Syntax error, illegal stepping: '" + e + "'");
3629
+ if (this.sloppyRanges) a[0] === "" && (a[0] = "*");
3630
+ else {
3631
+ 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.");
3632
+ 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.");
3744
3633
  }
3745
- } catch (err) {
3746
- await new Promise((resolve) => {
3747
- writeStream.once("close", () => resolve());
3748
- writeStream.destroy();
3749
- });
3750
- try {
3751
- await fsp.rm(input.cachePath, { force: true });
3752
- } catch {}
3753
- try {
3754
- await input.api.endDownload.mutate({ downloadId });
3755
- } catch {}
3756
- throw err;
3634
+ let o = 0;
3635
+ a[0] !== "*" && (o = parseInt(a[0], 10) + r);
3636
+ let h = parseInt(a[1], 10);
3637
+ this.validateNotNaN(h, "CronPattern: Syntax error, illegal stepping: (NaN)"), this.validateRange(0, this[t].length - 1, h, t, e);
3638
+ for (let l = o; l < this[t].length; l += h) this.setPart(t, l, i[1] || n);
3757
3639
  }
3758
- }
3759
- //#endregion
3760
- //#region ../../node_modules/croner/dist/croner.js
3761
- function T(s) {
3762
- return Date.UTC(s.y, s.m - 1, s.d, s.h, s.i, s.s);
3763
- }
3764
- function D(s, e) {
3765
- 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;
3766
- }
3767
- function A(s, e) {
3768
- let t = new Date(Date.parse(s));
3769
- if (isNaN(t)) throw new Error("Invalid ISO8601 passed to timezone parser.");
3770
- let r = s.substring(9);
3771
- 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);
3772
- }
3773
- function v(s, e, t) {
3774
- return k(A(s, e), t);
3775
- }
3776
- function k(s, e) {
3777
- 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);
3778
- if (D(h, s)) {
3779
- let u = /* @__PURE__ */ new Date(o.getTime() - 36e5);
3780
- return D(g(u, s.tz), s) ? u : o;
3640
+ replaceAlphaDays(e) {
3641
+ 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");
3781
3642
  }
3782
- let l = new Date(o.getTime() + T(s) - T(h));
3783
- if (D(g(l, s.tz), s)) return l;
3784
- if (e) throw new Error("Invalid date passed to fromTZ()");
3785
- return o.getTime() > l.getTime() ? o : l;
3786
- }
3787
- function g(s, e) {
3788
- let t, r;
3789
- try {
3790
- t = new Intl.DateTimeFormat("en-US", {
3791
- timeZone: e,
3792
- year: "numeric",
3793
- month: "numeric",
3794
- day: "numeric",
3795
- hour: "numeric",
3796
- minute: "numeric",
3797
- second: "numeric",
3798
- hour12: !1
3799
- }), r = t.formatToParts(s);
3800
- } catch (i) {
3801
- let a = i instanceof Error ? i.message : String(i);
3802
- 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}`);
3643
+ replaceAlphaDaysQuartz(e) {
3644
+ 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");
3803
3645
  }
3804
- let n = {
3805
- year: 0,
3806
- month: 0,
3807
- day: 0,
3808
- hour: 0,
3809
- minute: 0,
3810
- second: 0
3811
- };
3812
- 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));
3813
- 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)}`);
3814
- return n.hour === 24 && (n.hour = 0), {
3815
- y: n.year,
3816
- m: n.month,
3817
- d: n.day,
3818
- h: n.hour,
3819
- i: n.minute,
3820
- s: n.second,
3821
- tz: e
3822
- };
3823
- }
3824
- function b(s, e, t, r, n, i, a) {
3825
- return {
3826
- y: s,
3827
- m: e,
3828
- d: t,
3829
- h: r,
3830
- i: n,
3831
- s: i,
3832
- tz: a
3833
- };
3834
- }
3835
- var O = [
3836
- 1,
3837
- 2,
3838
- 4,
3839
- 8,
3840
- 16
3841
- ], C = class {
3842
- pattern;
3843
- timezone;
3844
- mode;
3845
- alternativeWeekdays;
3846
- sloppyRanges;
3646
+ replaceAlphaMonths(e) {
3647
+ 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");
3648
+ }
3649
+ handleNicknames(e) {
3650
+ let t = e.trim().toLowerCase();
3651
+ if (t === "@yearly" || t === "@annually") return "0 0 1 1 *";
3652
+ if (t === "@monthly") return "0 0 1 * *";
3653
+ if (t === "@weekly") return "0 0 * * 0";
3654
+ if (t === "@daily" || t === "@midnight") return "0 0 * * *";
3655
+ if (t === "@hourly") return "0 * * * *";
3656
+ 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.");
3657
+ return e;
3658
+ }
3659
+ setNthWeekdayOfMonth(e, t) {
3660
+ if (typeof t != "number" && t.toUpperCase() === "L") this.dayOfWeek[e] = this.dayOfWeek[e] | 32;
3661
+ else if (t === 63) this.dayOfWeek[e] = 63;
3662
+ else if (t < 6 && t > 0) this.dayOfWeek[e] = this.dayOfWeek[e] | O[t - 1];
3663
+ else throw new TypeError(`CronPattern: nth weekday out of range, should be 1-5 or L. Value: ${t}, Type: ${typeof t}`);
3664
+ }
3665
+ };
3666
+ var P = [
3667
+ 31,
3668
+ 28,
3669
+ 31,
3670
+ 30,
3671
+ 31,
3672
+ 30,
3673
+ 31,
3674
+ 31,
3675
+ 30,
3676
+ 31,
3677
+ 30,
3678
+ 31
3679
+ ], f = [
3680
+ [
3681
+ "month",
3682
+ "year",
3683
+ 0
3684
+ ],
3685
+ [
3686
+ "day",
3687
+ "month",
3688
+ -1
3689
+ ],
3690
+ [
3691
+ "hour",
3692
+ "day",
3693
+ 0
3694
+ ],
3695
+ [
3696
+ "minute",
3697
+ "hour",
3698
+ 0
3699
+ ],
3700
+ [
3701
+ "second",
3702
+ "minute",
3703
+ 0
3704
+ ]
3705
+ ], m = class s {
3706
+ tz;
3707
+ ms;
3847
3708
  second;
3848
3709
  minute;
3849
3710
  hour;
3850
3711
  day;
3851
3712
  month;
3852
- dayOfWeek;
3853
3713
  year;
3854
- lastDayOfMonth;
3855
- lastWeekday;
3856
- nearestWeekdays;
3857
- starDOM;
3858
- starDOW;
3859
- starYear;
3860
- useAndLogic;
3861
- constructor(e, t, r) {
3862
- 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();
3714
+ constructor(e, t) {
3715
+ if (this.tz = t, e && e instanceof Date) if (!isNaN(e)) this.fromDate(e);
3716
+ else throw new TypeError("CronDate: Invalid date passed to CronDate constructor");
3717
+ else if (e == null) this.fromDate(/* @__PURE__ */ new Date());
3718
+ else if (e && typeof e == "string") this.fromString(e);
3719
+ else if (e instanceof s) this.fromCronDate(e);
3720
+ else throw new TypeError("CronDate: Invalid type (" + typeof e + ") passed to CronDate constructor");
3863
3721
  }
3864
- parse() {
3865
- if (!(typeof this.pattern == "string" || this.pattern instanceof String)) throw new TypeError("CronPattern: Pattern has to be of type string.");
3866
- this.pattern.indexOf("@") >= 0 && (this.pattern = this.handleNicknames(this.pattern).trim());
3867
- let e = this.pattern.match(/\S+/g) || [""], t = e.length;
3868
- 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.");
3869
- if (this.mode !== "auto") {
3870
- let n;
3871
- switch (this.mode) {
3872
- case "5-part":
3873
- n = 5;
3874
- break;
3875
- case "6-part":
3876
- n = 6;
3877
- break;
3878
- case "7-part":
3879
- n = 7;
3880
- break;
3881
- case "5-or-6-parts":
3882
- n = [5, 6];
3883
- break;
3884
- case "6-or-7-parts":
3885
- n = [6, 7];
3886
- break;
3887
- default: n = 0;
3722
+ getLastDayOfMonth(e, t) {
3723
+ return t !== 1 ? P[t] : new Date(Date.UTC(e, t + 1, 0)).getUTCDate();
3724
+ }
3725
+ getLastWeekday(e, t) {
3726
+ let r = this.getLastDayOfMonth(e, t), i = new Date(Date.UTC(e, t, r)).getUTCDay();
3727
+ return i === 0 ? r - 2 : i === 6 ? r - 1 : r;
3728
+ }
3729
+ getNearestWeekday(e, t, r) {
3730
+ let n = this.getLastDayOfMonth(e, t);
3731
+ if (r > n) return -1;
3732
+ let a = new Date(Date.UTC(e, t, r)).getUTCDay();
3733
+ return a === 0 ? r === n ? r - 2 : r + 1 : a === 6 ? r === 1 ? r + 2 : r - 1 : r;
3734
+ }
3735
+ isNthWeekdayOfMonth(e, t, r, n) {
3736
+ let a = new Date(Date.UTC(e, t, r)).getUTCDay(), o = 0;
3737
+ for (let h = 1; h <= r; h++) new Date(Date.UTC(e, t, h)).getUTCDay() === a && o++;
3738
+ if (n & 63 && O[o - 1] & n) return !0;
3739
+ if (n & 32) {
3740
+ let h = this.getLastDayOfMonth(e, t);
3741
+ for (let l = r + 1; l <= h; l++) if (new Date(Date.UTC(e, t, l)).getUTCDay() === a) return !1;
3742
+ return !0;
3743
+ }
3744
+ return !1;
3745
+ }
3746
+ fromDate(e) {
3747
+ 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();
3748
+ else try {
3749
+ let t = g(e, this.tz);
3750
+ 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;
3751
+ } catch (t) {
3752
+ let r = t instanceof Error ? t.message : String(t);
3753
+ throw new TypeError(`CronDate: Failed to convert date to timezone '${this.tz}'. This may happen with invalid timezone names or dates. Original error: ${r}`);
3754
+ }
3755
+ 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();
3756
+ }
3757
+ fromCronDate(e) {
3758
+ 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;
3759
+ }
3760
+ apply() {
3761
+ 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) {
3762
+ let e = new Date(Date.UTC(this.year, this.month, this.day, this.hour, this.minute, this.second, this.ms));
3763
+ 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;
3764
+ } else return !1;
3765
+ }
3766
+ fromString(e) {
3767
+ if (typeof this.tz == "number") {
3768
+ let t = v(e);
3769
+ 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();
3770
+ } else return this.fromDate(v(e, this.tz));
3771
+ }
3772
+ findNext(e, t, r, n) {
3773
+ return this._findMatch(e, t, r, n, 1);
3774
+ }
3775
+ _findMatch(e, t, r, n, i) {
3776
+ let a = this[t], o;
3777
+ r.lastDayOfMonth && (o = this.getLastDayOfMonth(this.year, this.month));
3778
+ 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;
3779
+ for (let u = l; y(u); u += i) {
3780
+ let d = r[t][u];
3781
+ if (t === "day" && !d) {
3782
+ for (let c = 0; c < r.nearestWeekdays.length; c++) if (r.nearestWeekdays[c]) {
3783
+ let M = this.getNearestWeekday(this.year, this.month, c - n);
3784
+ if (M === -1) continue;
3785
+ if (M === u - n) {
3786
+ d = 1;
3787
+ break;
3788
+ }
3789
+ }
3888
3790
  }
3889
- if (!(Array.isArray(n) ? n.includes(t) : t === n)) {
3890
- let a = Array.isArray(n) ? n.join(" or ") : n.toString();
3891
- throw new TypeError(`CronPattern: mode '${this.mode}' requires exactly ${a} parts, but pattern '${this.pattern}' has ${t} parts.`);
3791
+ if (t === "day" && r.lastWeekday) {
3792
+ let c = this.getLastWeekday(this.year, this.month);
3793
+ u - n === c && (d = 1);
3892
3794
  }
3795
+ if (t === "day" && r.lastDayOfMonth && u - n == o && (d = 1), t === "day" && !r.starDOW) {
3796
+ let c = r.dayOfWeek[(h + (u - n - 1)) % 7];
3797
+ if (c && c & 63) c = this.isNthWeekdayOfMonth(this.year, this.month, u - n, c) ? 1 : 0;
3798
+ else if (c) throw new Error(`CronDate: Invalid value for dayOfWeek encountered. ${c}`);
3799
+ r.useAndLogic ? d = d && c : !e.domAndDow && !r.starDOM ? d = d || c : d = d && c;
3800
+ }
3801
+ if (d) return this[t] = u - n, a !== this[t] ? 2 : 1;
3893
3802
  }
3894
- 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.");
3895
- 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) {
3896
- case "5-part":
3897
- e[0] = "0", e[6] = "*";
3898
- break;
3899
- case "6-part":
3900
- e[6] = "*";
3901
- break;
3902
- case "5-or-6-parts":
3903
- e[6] = "*";
3904
- break;
3905
- case "6-or-7-parts": break;
3906
- case "7-part":
3907
- case "auto": break;
3908
- }
3909
- 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);
3910
- let r = this.alternativeWeekdays ? -1 : 0;
3911
- 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]);
3803
+ return 3;
3912
3804
  }
3913
- partToArray(e, t, r, n) {
3914
- let i = this[e], a = e === "day" && this.lastDayOfMonth, o = e === "day" && this.lastWeekday;
3915
- if (t === "" && !a && !o) throw new TypeError("CronPattern: configuration entry " + e + " (" + t + ") is empty, check for trailing spaces.");
3916
- if (t === "*") return i.fill(n);
3917
- let h = t.split(",");
3918
- if (h.length > 1) for (let l = 0; l < h.length; l++) this.partToArray(e, h[l], r, n);
3919
- 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);
3805
+ recurse(e, t, r) {
3806
+ if (r === 0 && !e.starYear) {
3807
+ if (this.year >= 0 && this.year < e.year.length && e.year[this.year] === 0) {
3808
+ let i = -1;
3809
+ for (let a = this.year + 1; a < e.year.length && a < 1e4; a++) if (e.year[a] === 1) {
3810
+ i = a;
3811
+ break;
3812
+ }
3813
+ if (i === -1) return null;
3814
+ this.year = i, this.month = 0, this.day = 1, this.hour = 0, this.minute = 0, this.second = 0, this.ms = 0;
3815
+ }
3816
+ if (this.year >= 1e4) return null;
3817
+ }
3818
+ let n = this.findNext(t, f[r][0], e, f[r][2]);
3819
+ if (n > 1) {
3820
+ let i = r + 1;
3821
+ for (; i < f.length;) this[f[i][0]] = -f[i][2], i++;
3822
+ if (n === 3) {
3823
+ if (this[f[r][1]]++, this[f[r][0]] = -f[r][2], this.apply(), r === 0 && !e.starYear) {
3824
+ for (; this.year >= 0 && this.year < e.year.length && e.year[this.year] === 0 && this.year < 1e4;) this.year++;
3825
+ if (this.year >= 1e4 || this.year >= e.year.length) return null;
3826
+ }
3827
+ return this.recurse(e, t, 0);
3828
+ } else if (this.apply()) return this.recurse(e, t, r - 1);
3829
+ }
3830
+ return r += 1, r >= f.length ? this : (e.starYear ? this.year >= 3e3 : this.year >= 1e4) ? null : this.recurse(e, t, r);
3920
3831
  }
3921
- throwAtIllegalCharacters(e) {
3922
- 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.");
3832
+ increment(e, t, r) {
3833
+ return this.second += t.interval !== void 0 && t.interval > 1 && r ? t.interval : 1, this.ms = 0, this.apply(), this.recurse(e, t, 0);
3923
3834
  }
3924
- handleNumber(e, t, r, n) {
3925
- let i = this.extractNth(e, t), a = e.toUpperCase().includes("W");
3926
- if (t !== "day" && a) throw new TypeError("CronPattern: Nearest weekday modifier (W) only allowed in day-of-month.");
3927
- a && (t = "nearestWeekdays");
3928
- let o = parseInt(i[0], 10) + r;
3929
- if (isNaN(o)) throw new TypeError("CronPattern: " + t + " is not a number: '" + e + "'");
3930
- this.setPart(t, o, i[1] || n);
3835
+ decrement(e, t) {
3836
+ return this.second -= t.interval !== void 0 && t.interval > 1 ? t.interval : 1, this.ms = 0, this.apply(), this.recurseBackward(e, t, 0, 0);
3931
3837
  }
3932
- setPart(e, t, r) {
3933
- if (!Object.prototype.hasOwnProperty.call(this, e)) throw new TypeError("CronPattern: Invalid part specified: " + e);
3934
- if (e === "dayOfWeek") {
3935
- if (t === 7 && (t = 0), t < 0 || t > 6) throw new RangeError("CronPattern: Invalid value for dayOfWeek: " + t);
3936
- this.setNthWeekdayOfMonth(t, r);
3937
- return;
3838
+ recurseBackward(e, t, r, n = 0) {
3839
+ if (n > 1e4) return null;
3840
+ if (r === 0 && !e.starYear) {
3841
+ if (this.year >= 0 && this.year < e.year.length && e.year[this.year] === 0) {
3842
+ let a = -1;
3843
+ for (let o = this.year - 1; o >= 0; o--) if (e.year[o] === 1) {
3844
+ a = o;
3845
+ break;
3846
+ }
3847
+ if (a === -1) return null;
3848
+ this.year = a, this.month = 11, this.day = 31, this.hour = 23, this.minute = 59, this.second = 59, this.ms = 0;
3849
+ }
3850
+ if (this.year < 0) return null;
3851
+ }
3852
+ let i = this.findPrevious(t, f[r][0], e, f[r][2]);
3853
+ if (i > 1) {
3854
+ let a = r + 1;
3855
+ for (; a < f.length;) {
3856
+ let o = f[a][0], h = f[a][2], l = this.getMaxPatternValue(o, e, h);
3857
+ this[o] = l, a++;
3858
+ }
3859
+ if (i === 3) {
3860
+ if (this[f[r][1]]--, r === 0) {
3861
+ let y = this.getLastDayOfMonth(this.year, this.month);
3862
+ this.day > y && (this.day = y);
3863
+ }
3864
+ if (r === 1) if (this.day <= 0) this.day = 1;
3865
+ else {
3866
+ let y = this.year, u = this.month;
3867
+ for (; u < 0;) u += 12, y--;
3868
+ for (; u > 11;) u -= 12, y++;
3869
+ let d = u !== 1 ? P[u] : new Date(Date.UTC(y, u + 1, 0)).getUTCDate();
3870
+ this.day > d && (this.day = d);
3871
+ }
3872
+ this.apply();
3873
+ let o = f[r][0], h = f[r][2], l = this.getMaxPatternValue(o, e, h);
3874
+ if (o === "day") {
3875
+ let y = this.getLastDayOfMonth(this.year, this.month);
3876
+ this[o] = Math.min(l, y);
3877
+ } else this[o] = l;
3878
+ if (this.apply(), r === 0) {
3879
+ let y = f[1][2], u = this.getMaxPatternValue("day", e, y), d = this.getLastDayOfMonth(this.year, this.month), c = Math.min(u, d);
3880
+ 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]));
3881
+ }
3882
+ if (r === 0 && !e.starYear) {
3883
+ for (; this.year >= 0 && this.year < e.year.length && e.year[this.year] === 0;) this.year--;
3884
+ if (this.year < 0) return null;
3885
+ }
3886
+ return this.recurseBackward(e, t, 0, n + 1);
3887
+ } else if (this.apply()) return this.recurseBackward(e, t, r - 1, n + 1);
3938
3888
  }
3939
- if (e === "second" || e === "minute") {
3940
- if (t < 0 || t >= 60) throw new RangeError("CronPattern: Invalid value for " + e + ": " + t);
3941
- } else if (e === "hour") {
3942
- if (t < 0 || t >= 24) throw new RangeError("CronPattern: Invalid value for " + e + ": " + t);
3943
- } else if (e === "day" || e === "nearestWeekdays") {
3944
- if (t < 0 || t >= 31) throw new RangeError("CronPattern: Invalid value for " + e + ": " + t);
3945
- } else if (e === "month") {
3946
- if (t < 0 || t >= 12) throw new RangeError("CronPattern: Invalid value for " + e + ": " + t);
3947
- } else if (e === "year" && (t < 1 || t >= 1e4)) throw new RangeError("CronPattern: Invalid value for " + e + ": " + t + " (supported range: 1-9999)");
3948
- this[e][t] = r;
3949
- }
3950
- validateNotNaN(e, t) {
3951
- if (isNaN(e)) throw new TypeError(t);
3889
+ return r += 1, r >= f.length ? this : this.year < 0 ? null : this.recurseBackward(e, t, r, n + 1);
3952
3890
  }
3953
- validateRange(e, t, r, n, i) {
3954
- if (e > t) throw new TypeError("CronPattern: From value is larger than to value: '" + i + "'");
3955
- if (r !== void 0) {
3956
- if (r === 0) throw new TypeError("CronPattern: Syntax error, illegal stepping: 0");
3957
- if (r > this[n].length) throw new TypeError("CronPattern: Syntax error, steps cannot be greater than maximum value of part (" + this[n].length + ")");
3958
- }
3891
+ getMaxPatternValue(e, t, r) {
3892
+ if (e === "day" && t.lastDayOfMonth) return this.getLastDayOfMonth(this.year, this.month);
3893
+ if (e === "day" && !t.starDOW) return this.getLastDayOfMonth(this.year, this.month);
3894
+ for (let n = t[e].length - 1; n >= 0; n--) if (t[e][n]) return n - r;
3895
+ return t[e].length - 1 - r;
3959
3896
  }
3960
- handleRangeWithStepping(e, t, r, n) {
3961
- if (e.toUpperCase().includes("W")) throw new TypeError("CronPattern: Syntax error, W is not allowed in ranges with stepping.");
3962
- let i = this.extractNth(e, t), a = i[0].match(/^(\d+)-(\d+)\/(\d+)$/);
3963
- if (a === null) throw new TypeError("CronPattern: Syntax error, illegal range with stepping: '" + e + "'");
3964
- let [, o, h, l] = a, y = parseInt(o, 10) + r, u = parseInt(h, 10) + r, d = parseInt(l, 10);
3965
- 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);
3966
- for (let c = y; c <= u; c += d) this.setPart(t, c, i[1] || n);
3897
+ findPrevious(e, t, r, n) {
3898
+ return this._findMatch(e, t, r, n, -1);
3967
3899
  }
3968
- extractNth(e, t) {
3969
- let r = e, n;
3970
- if (r.includes("#")) {
3971
- if (t !== "dayOfWeek") throw new Error("CronPattern: nth (#) only allowed in day-of-week field");
3972
- n = r.split("#")[1], r = r.split("#")[0];
3973
- } else if (r.toUpperCase().endsWith("L")) {
3974
- if (t !== "dayOfWeek") throw new Error("CronPattern: L modifier only allowed in day-of-week field (use L alone for day-of-month)");
3975
- n = "L", r = r.slice(0, -1);
3976
- }
3977
- return [r, n];
3900
+ getDate(e) {
3901
+ 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);
3978
3902
  }
3979
- handleRange(e, t, r, n) {
3980
- if (e.toUpperCase().includes("W")) throw new TypeError("CronPattern: Syntax error, W is not allowed in a range.");
3981
- let i = this.extractNth(e, t), a = i[0].split("-");
3982
- if (a.length !== 2) throw new TypeError("CronPattern: Syntax error, illegal range: '" + e + "'");
3983
- let o = parseInt(a[0], 10) + r, h = parseInt(a[1], 10) + r;
3984
- 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);
3985
- for (let l = o; l <= h; l++) this.setPart(t, l, i[1] || n);
3903
+ getTime() {
3904
+ return this.getDate(!1).getTime();
3986
3905
  }
3987
- handleStepping(e, t, r, n) {
3988
- if (e.toUpperCase().includes("W")) throw new TypeError("CronPattern: Syntax error, W is not allowed in parts with stepping.");
3989
- let i = this.extractNth(e, t), a = i[0].split("/");
3990
- if (a.length !== 2) throw new TypeError("CronPattern: Syntax error, illegal stepping: '" + e + "'");
3991
- if (this.sloppyRanges) a[0] === "" && (a[0] = "*");
3992
- else {
3993
- 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.");
3994
- 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.");
3906
+ match(e, t) {
3907
+ if (!e.starYear && (this.year < 0 || this.year >= e.year.length || e.year[this.year] === 0)) return !1;
3908
+ for (let r = 0; r < f.length; r++) {
3909
+ let n = f[r][0], i = f[r][2], a = this[n];
3910
+ if (a + i < 0 || a + i >= e[n].length) return !1;
3911
+ let o = e[n][a + i];
3912
+ if (n === "day") {
3913
+ if (!o) {
3914
+ for (let h = 0; h < e.nearestWeekdays.length; h++) if (e.nearestWeekdays[h]) {
3915
+ let l = this.getNearestWeekday(this.year, this.month, h - i);
3916
+ if (l !== -1 && l === a) {
3917
+ o = 1;
3918
+ break;
3919
+ }
3920
+ }
3921
+ }
3922
+ if (e.lastWeekday) a === this.getLastWeekday(this.year, this.month) && (o = 1);
3923
+ if (e.lastDayOfMonth) a === this.getLastDayOfMonth(this.year, this.month) && (o = 1);
3924
+ if (!e.starDOW) {
3925
+ let h = new Date(Date.UTC(this.year, this.month, 1, 0, 0, 0, 0)).getUTCDay(), l = e.dayOfWeek[(h + (a - 1)) % 7];
3926
+ 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;
3927
+ }
3928
+ }
3929
+ if (!o) return !1;
3995
3930
  }
3996
- let o = 0;
3997
- a[0] !== "*" && (o = parseInt(a[0], 10) + r);
3998
- let h = parseInt(a[1], 10);
3999
- this.validateNotNaN(h, "CronPattern: Syntax error, illegal stepping: (NaN)"), this.validateRange(0, this[t].length - 1, h, t, e);
4000
- for (let l = o; l < this[t].length; l += h) this.setPart(t, l, i[1] || n);
3931
+ return !0;
4001
3932
  }
4002
- replaceAlphaDays(e) {
4003
- 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");
3933
+ };
3934
+ function R(s) {
3935
+ 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, ![
3936
+ "auto",
3937
+ "5-part",
3938
+ "6-part",
3939
+ "7-part",
3940
+ "5-or-6-parts",
3941
+ "6-or-7-parts"
3942
+ ].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'.");
3943
+ if (s.startAt && (s.startAt = new m(s.startAt, s.timezone)), s.stopAt && (s.stopAt = new m(s.stopAt, s.timezone)), s.interval !== null) {
3944
+ if (isNaN(s.interval)) throw new Error("CronOptions: Supplied value for interval is not a number");
3945
+ if (s.interval < 0) throw new Error("CronOptions: Supplied value for interval can not be negative");
4004
3946
  }
4005
- replaceAlphaDaysQuartz(e) {
4006
- 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");
3947
+ if (s.utcOffset !== void 0) {
3948
+ if (isNaN(s.utcOffset)) throw new Error("CronOptions: Invalid value passed for utcOffset, should be number representing minutes offset from UTC.");
3949
+ if (s.utcOffset < -870 || s.utcOffset > 870) throw new Error("CronOptions: utcOffset out of bounds.");
3950
+ if (s.utcOffset !== void 0 && s.timezone) throw new Error("CronOptions: Combining 'utcOffset' with 'timezone' is not allowed.");
4007
3951
  }
4008
- replaceAlphaMonths(e) {
4009
- 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");
3952
+ if (s.unref !== !0 && s.unref !== !1) throw new Error("CronOptions: Unref should be either true, false or undefined(false).");
3953
+ 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.");
3954
+ return s;
3955
+ }
3956
+ function p(s) {
3957
+ return Object.prototype.toString.call(s) === "[object Function]" || typeof s == "function" || s instanceof Function;
3958
+ }
3959
+ function _(s) {
3960
+ return p(s);
3961
+ }
3962
+ function x(s) {
3963
+ typeof Deno < "u" && typeof Deno.unrefTimer < "u" ? Deno.unrefTimer(s) : s && typeof s.unref < "u" && s.unref();
3964
+ }
3965
+ var W = 30 * 1e3, w = [], E = class {
3966
+ name;
3967
+ options;
3968
+ _states;
3969
+ fn;
3970
+ getTz() {
3971
+ return this.options.timezone || this.options.utcOffset;
4010
3972
  }
4011
- handleNicknames(e) {
4012
- let t = e.trim().toLowerCase();
4013
- if (t === "@yearly" || t === "@annually") return "0 0 1 1 *";
4014
- if (t === "@monthly") return "0 0 1 * *";
4015
- if (t === "@weekly") return "0 0 * * 0";
4016
- if (t === "@daily" || t === "@midnight") return "0 0 * * *";
4017
- if (t === "@hourly") return "0 * * * *";
4018
- 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.");
3973
+ applyDayOffset(e) {
3974
+ if (this.options.dayOffset !== void 0 && this.options.dayOffset !== 0) {
3975
+ let t = this.options.dayOffset * 24 * 60 * 60 * 1e3;
3976
+ return new Date(e.getTime() + t);
3977
+ }
4019
3978
  return e;
4020
3979
  }
4021
- setNthWeekdayOfMonth(e, t) {
4022
- if (typeof t != "number" && t.toUpperCase() === "L") this.dayOfWeek[e] = this.dayOfWeek[e] | 32;
4023
- else if (t === 63) this.dayOfWeek[e] = 63;
4024
- else if (t < 6 && t > 0) this.dayOfWeek[e] = this.dayOfWeek[e] | O[t - 1];
4025
- else throw new TypeError(`CronPattern: nth weekday out of range, should be 1-5 or L. Value: ${t}, Type: ${typeof t}`);
3980
+ constructor(e, t, r) {
3981
+ let n, i;
3982
+ if (p(t)) i = t;
3983
+ else if (typeof t == "object") n = t;
3984
+ else if (t !== void 0) throw new Error("Cron: Invalid argument passed for optionsIn. Should be one of function, or object (options).");
3985
+ if (p(r)) i = r;
3986
+ else if (typeof r == "object") n = r;
3987
+ else if (r !== void 0) throw new Error("Cron: Invalid argument passed for funcIn. Should be one of function, or object (options).");
3988
+ if (this.name = n?.name, this.options = R(n), this._states = {
3989
+ kill: !1,
3990
+ blocking: !1,
3991
+ previousRun: void 0,
3992
+ currentRun: void 0,
3993
+ once: void 0,
3994
+ currentTimeout: void 0,
3995
+ maxRuns: n ? n.maxRuns : void 0,
3996
+ paused: n ? n.paused : !1,
3997
+ pattern: new C("* * * * *", void 0, { mode: "auto" })
3998
+ }, 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, {
3999
+ mode: this.options.mode,
4000
+ alternativeWeekdays: this.options.alternativeWeekdays,
4001
+ sloppyRanges: this.options.sloppyRanges
4002
+ }), this.name) {
4003
+ if (w.find((o) => o.name === this.name)) throw new Error("Cron: Tried to initialize new named job '" + this.name + "', but name already taken.");
4004
+ w.push(this);
4005
+ }
4006
+ return i !== void 0 && _(i) && (this.fn = i, this.schedule()), this;
4026
4007
  }
4027
- };
4028
- var P = [
4029
- 31,
4030
- 28,
4031
- 31,
4032
- 30,
4033
- 31,
4034
- 30,
4035
- 31,
4036
- 31,
4037
- 30,
4038
- 31,
4039
- 30,
4040
- 31
4041
- ], f = [
4042
- [
4043
- "month",
4044
- "year",
4045
- 0
4046
- ],
4047
- [
4048
- "day",
4049
- "month",
4050
- -1
4051
- ],
4052
- [
4053
- "hour",
4054
- "day",
4055
- 0
4056
- ],
4057
- [
4058
- "minute",
4059
- "hour",
4060
- 0
4061
- ],
4062
- [
4063
- "second",
4064
- "minute",
4065
- 0
4066
- ]
4067
- ], m = class s {
4068
- tz;
4069
- ms;
4070
- second;
4071
- minute;
4072
- hour;
4073
- day;
4074
- month;
4075
- year;
4076
- constructor(e, t) {
4077
- if (this.tz = t, e && e instanceof Date) if (!isNaN(e)) this.fromDate(e);
4078
- else throw new TypeError("CronDate: Invalid date passed to CronDate constructor");
4079
- else if (e == null) this.fromDate(/* @__PURE__ */ new Date());
4080
- else if (e && typeof e == "string") this.fromString(e);
4081
- else if (e instanceof s) this.fromCronDate(e);
4082
- else throw new TypeError("CronDate: Invalid type (" + typeof e + ") passed to CronDate constructor");
4008
+ nextRun(e) {
4009
+ let t = this._next(e);
4010
+ return t ? this.applyDayOffset(t.getDate(!1)) : null;
4011
+ }
4012
+ nextRuns(e, t) {
4013
+ this._states.maxRuns !== void 0 && e > this._states.maxRuns && (e = this._states.maxRuns);
4014
+ let r = t || this._states.currentRun || void 0;
4015
+ return this._enumerateRuns(e, r, "next");
4016
+ }
4017
+ previousRuns(e, t) {
4018
+ return this._enumerateRuns(e, t || void 0, "previous");
4019
+ }
4020
+ _enumerateRuns(e, t, r) {
4021
+ let n = [], i = t ? new m(t, this.getTz()) : null, a = r === "next" ? this._next : this._previous;
4022
+ for (; e--;) {
4023
+ let o = a.call(this, i);
4024
+ if (!o) break;
4025
+ let h = o.getDate(!1);
4026
+ n.push(this.applyDayOffset(h)), i = o;
4027
+ }
4028
+ return n;
4083
4029
  }
4084
- getLastDayOfMonth(e, t) {
4085
- return t !== 1 ? P[t] : new Date(Date.UTC(e, t + 1, 0)).getUTCDate();
4030
+ match(e) {
4031
+ if (this._states.once) {
4032
+ let r = new m(e, this.getTz());
4033
+ r.ms = 0;
4034
+ let n = new m(this._states.once, this.getTz());
4035
+ return n.ms = 0, r.getTime() === n.getTime();
4036
+ }
4037
+ let t = new m(e, this.getTz());
4038
+ return t.ms = 0, t.match(this._states.pattern, this.options);
4086
4039
  }
4087
- getLastWeekday(e, t) {
4088
- let r = this.getLastDayOfMonth(e, t), i = new Date(Date.UTC(e, t, r)).getUTCDay();
4089
- return i === 0 ? r - 2 : i === 6 ? r - 1 : r;
4040
+ getPattern() {
4041
+ if (!this._states.once) return this._states.pattern ? this._states.pattern.pattern : void 0;
4090
4042
  }
4091
- getNearestWeekday(e, t, r) {
4092
- let n = this.getLastDayOfMonth(e, t);
4093
- if (r > n) return -1;
4094
- let a = new Date(Date.UTC(e, t, r)).getUTCDay();
4095
- return a === 0 ? r === n ? r - 2 : r + 1 : a === 6 ? r === 1 ? r + 2 : r - 1 : r;
4043
+ getOnce() {
4044
+ return this._states.once ? this._states.once.getDate() : null;
4096
4045
  }
4097
- isNthWeekdayOfMonth(e, t, r, n) {
4098
- let a = new Date(Date.UTC(e, t, r)).getUTCDay(), o = 0;
4099
- for (let h = 1; h <= r; h++) new Date(Date.UTC(e, t, h)).getUTCDay() === a && o++;
4100
- if (n & 63 && O[o - 1] & n) return !0;
4101
- if (n & 32) {
4102
- let h = this.getLastDayOfMonth(e, t);
4103
- for (let l = r + 1; l <= h; l++) if (new Date(Date.UTC(e, t, l)).getUTCDay() === a) return !1;
4104
- return !0;
4105
- }
4106
- return !1;
4046
+ isRunning() {
4047
+ let e = this.nextRun(this._states.currentRun), t = !this._states.paused, r = this.fn !== void 0, n = !this._states.kill;
4048
+ return t && r && n && e !== null;
4107
4049
  }
4108
- fromDate(e) {
4109
- 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();
4110
- else try {
4111
- let t = g(e, this.tz);
4112
- 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;
4113
- } catch (t) {
4114
- let r = t instanceof Error ? t.message : String(t);
4115
- throw new TypeError(`CronDate: Failed to convert date to timezone '${this.tz}'. This may happen with invalid timezone names or dates. Original error: ${r}`);
4116
- }
4117
- 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();
4050
+ isStopped() {
4051
+ return this._states.kill;
4118
4052
  }
4119
- fromCronDate(e) {
4120
- 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;
4053
+ isBusy() {
4054
+ return this._states.blocking;
4121
4055
  }
4122
- apply() {
4123
- 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) {
4124
- let e = new Date(Date.UTC(this.year, this.month, this.day, this.hour, this.minute, this.second, this.ms));
4125
- 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;
4126
- } else return !1;
4056
+ currentRun() {
4057
+ return this._states.currentRun ? this._states.currentRun.getDate() : null;
4127
4058
  }
4128
- fromString(e) {
4129
- if (typeof this.tz == "number") {
4130
- let t = v(e);
4131
- 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();
4132
- } else return this.fromDate(v(e, this.tz));
4059
+ previousRun() {
4060
+ return this._states.previousRun ? this._states.previousRun.getDate() : null;
4133
4061
  }
4134
- findNext(e, t, r, n) {
4135
- return this._findMatch(e, t, r, n, 1);
4062
+ msToNext(e) {
4063
+ let t = this._next(e);
4064
+ return t ? e instanceof m || e instanceof Date ? t.getTime() - e.getTime() : t.getTime() - new m(e).getTime() : null;
4136
4065
  }
4137
- _findMatch(e, t, r, n, i) {
4138
- let a = this[t], o;
4139
- r.lastDayOfMonth && (o = this.getLastDayOfMonth(this.year, this.month));
4140
- 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;
4141
- for (let u = l; y(u); u += i) {
4142
- let d = r[t][u];
4143
- if (t === "day" && !d) {
4144
- for (let c = 0; c < r.nearestWeekdays.length; c++) if (r.nearestWeekdays[c]) {
4145
- let M = this.getNearestWeekday(this.year, this.month, c - n);
4146
- if (M === -1) continue;
4147
- if (M === u - n) {
4148
- d = 1;
4149
- break;
4150
- }
4151
- }
4152
- }
4153
- if (t === "day" && r.lastWeekday) {
4154
- let c = this.getLastWeekday(this.year, this.month);
4155
- u - n === c && (d = 1);
4156
- }
4157
- if (t === "day" && r.lastDayOfMonth && u - n == o && (d = 1), t === "day" && !r.starDOW) {
4158
- let c = r.dayOfWeek[(h + (u - n - 1)) % 7];
4159
- if (c && c & 63) c = this.isNthWeekdayOfMonth(this.year, this.month, u - n, c) ? 1 : 0;
4160
- else if (c) throw new Error(`CronDate: Invalid value for dayOfWeek encountered. ${c}`);
4161
- r.useAndLogic ? d = d && c : !e.domAndDow && !r.starDOM ? d = d || c : d = d && c;
4066
+ stop() {
4067
+ this._states.kill = !0, this._states.currentTimeout && clearTimeout(this._states.currentTimeout);
4068
+ let e = w.indexOf(this);
4069
+ e >= 0 && w.splice(e, 1);
4070
+ }
4071
+ pause() {
4072
+ return this._states.paused = !0, !this._states.kill;
4073
+ }
4074
+ resume() {
4075
+ return this._states.paused = !1, !this._states.kill;
4076
+ }
4077
+ schedule(e) {
4078
+ if (e && this.fn) throw new Error("Cron: It is not allowed to schedule two functions using the same Croner instance.");
4079
+ e && (this.fn = e);
4080
+ let t = this.msToNext(), r = this.nextRun(this._states.currentRun);
4081
+ 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);
4082
+ }
4083
+ async _trigger(e) {
4084
+ this._states.blocking = !0, this._states.currentRun = new m(void 0, this.getTz());
4085
+ try {
4086
+ if (this.options.catch) try {
4087
+ this.fn !== void 0 && await this.fn(this, this.options.context);
4088
+ } catch (t) {
4089
+ if (p(this.options.catch)) try {
4090
+ this.options.catch(t, this);
4091
+ } catch {}
4162
4092
  }
4163
- if (d) return this[t] = u - n, a !== this[t] ? 2 : 1;
4093
+ else this.fn !== void 0 && await this.fn(this, this.options.context);
4094
+ } finally {
4095
+ this._states.previousRun = new m(e, this.getTz()), this._states.blocking = !1;
4164
4096
  }
4165
- return 3;
4166
4097
  }
4167
- recurse(e, t, r) {
4168
- if (r === 0 && !e.starYear) {
4169
- if (this.year >= 0 && this.year < e.year.length && e.year[this.year] === 0) {
4170
- let i = -1;
4171
- for (let a = this.year + 1; a < e.year.length && a < 1e4; a++) if (e.year[a] === 1) {
4172
- i = a;
4173
- break;
4174
- }
4175
- if (i === -1) return null;
4176
- this.year = i, this.month = 0, this.day = 1, this.hour = 0, this.minute = 0, this.second = 0, this.ms = 0;
4177
- }
4178
- if (this.year >= 1e4) return null;
4179
- }
4180
- let n = this.findNext(t, f[r][0], e, f[r][2]);
4181
- if (n > 1) {
4182
- let i = r + 1;
4183
- for (; i < f.length;) this[f[i][0]] = -f[i][2], i++;
4184
- if (n === 3) {
4185
- if (this[f[r][1]]++, this[f[r][0]] = -f[r][2], this.apply(), r === 0 && !e.starYear) {
4186
- for (; this.year >= 0 && this.year < e.year.length && e.year[this.year] === 0 && this.year < 1e4;) this.year++;
4187
- if (this.year >= 1e4 || this.year >= e.year.length) return null;
4188
- }
4189
- return this.recurse(e, t, 0);
4190
- } else if (this.apply()) return this.recurse(e, t, r - 1);
4191
- }
4192
- return r += 1, r >= f.length ? this : (e.starYear ? this.year >= 3e3 : this.year >= 1e4) ? null : this.recurse(e, t, r);
4098
+ async trigger() {
4099
+ await this._trigger();
4100
+ }
4101
+ runsLeft() {
4102
+ return this._states.maxRuns;
4103
+ }
4104
+ _checkTrigger(e) {
4105
+ let t = /* @__PURE__ */ new Date(), r = !this._states.paused && t.getTime() >= e.getTime(), n = this._states.blocking && this.options.protect;
4106
+ 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();
4107
+ }
4108
+ _next(e) {
4109
+ let t = !!(e || this._states.currentRun), r = !1;
4110
+ !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);
4111
+ let n = this._states.once || new m(e, this.getTz());
4112
+ 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;
4113
+ }
4114
+ _previous(e) {
4115
+ let t = new m(e, this.getTz());
4116
+ this.options.stopAt && t.getTime() > this.options.stopAt.getTime() && (t = this.options.stopAt);
4117
+ let r = new m(t, this.getTz());
4118
+ 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);
4119
+ }
4120
+ _calculatePreviousRun(e, t) {
4121
+ let r = new m(void 0, this.getTz()), n = e;
4122
+ if (this.options.startAt.getTime() <= r.getTime()) {
4123
+ n = this.options.startAt;
4124
+ let i = n.getTime() + this.options.interval * 1e3;
4125
+ for (; i <= r.getTime();) n = new m(n, this.getTz()).increment(this._states.pattern, this.options, !0), i = n.getTime() + this.options.interval * 1e3;
4126
+ t = !0;
4127
+ }
4128
+ return n === null && (n = void 0), [n, t];
4129
+ }
4130
+ };
4131
+ //#endregion
4132
+ //#region src/builtins/backup-orchestrator/cron-helpers.ts
4133
+ /**
4134
+ * Cron helpers — wraps `croner` so the orchestrator + the `previewNextRuns`
4135
+ * cap method share one validation + next-run-computation surface.
4136
+ *
4137
+ * `croner` is the opinionated node cron library we standardize on
4138
+ * server-side: pure JS, native TypeScript types, supports the full
4139
+ * 5-field POSIX cron grammar plus seconds (6-field), DST-aware, and
4140
+ * exposes `nextRun()` / `nextRuns(N)` for peeking without scheduling.
4141
+ * The orchestrator uses the peeking API (it has its own polling
4142
+ * loop); future surfaces (e.g. event-driven retention sweeps) can
4143
+ * register an actual scheduled `Cron(pattern, fn)` and let croner
4144
+ * own the timer.
4145
+ */
4146
+ /**
4147
+ * Validate a cron expression and return its next firing time strictly
4148
+ * after `after`, expressed as ms-epoch. Returns `null` when the
4149
+ * expression has no future runs (croner returns null e.g. for
4150
+ * fully-bounded patterns that have already elapsed). Throws when the
4151
+ * expression is syntactically invalid — callers are expected to
4152
+ * surface that to the operator.
4153
+ */
4154
+ function computeNextDue(cron, after) {
4155
+ const next = new E(cron, { paused: true }).nextRun(new Date(after));
4156
+ return next ? next.getTime() : null;
4157
+ }
4158
+ /**
4159
+ * Validate without computing — returns true when `cron` parses, false
4160
+ * otherwise. Used by the cap-router input zod schema's `.refine`
4161
+ * callback to reject malformed expressions at the boundary instead
4162
+ * of at scheduler-tick time.
4163
+ */
4164
+ function isValidCron(cron) {
4165
+ try {
4166
+ new E(cron, { paused: true });
4167
+ return true;
4168
+ } catch {
4169
+ return false;
4170
+ }
4171
+ }
4172
+ /**
4173
+ * Peek the next N firing times after `after` (default: now). Used by
4174
+ * the admin UI's CronEditor preview pane — operators see the next
4175
+ * 3–5 runs as they edit the expression so a wrong pattern is
4176
+ * obvious before save.
4177
+ */
4178
+ function previewNextRuns(cron, count, after) {
4179
+ const job = new E(cron, { paused: true });
4180
+ const baseline = after !== void 0 ? new Date(after) : /* @__PURE__ */ new Date();
4181
+ const out = [];
4182
+ let cursor = baseline;
4183
+ for (let i = 0; i < count; i++) {
4184
+ cursor = job.nextRun(cursor);
4185
+ if (!cursor) break;
4186
+ out.push(cursor.getTime());
4187
+ }
4188
+ return out;
4189
+ }
4190
+ //#endregion
4191
+ //#region src/builtins/backup-orchestrator/destination-policy.ts
4192
+ var TABLE_NAME$1 = "backup_destination_policies";
4193
+ var BACKUP_DESTINATION_POLICIES_SCHEMA = { columns: [
4194
+ {
4195
+ name: "location_id",
4196
+ type: "TEXT",
4197
+ primaryKey: true
4198
+ },
4199
+ {
4200
+ name: "enabled",
4201
+ type: "INTEGER",
4202
+ notNull: true,
4203
+ defaultValue: 1
4204
+ },
4205
+ {
4206
+ name: "retention_count",
4207
+ type: "INTEGER",
4208
+ notNull: true,
4209
+ defaultValue: 7
4210
+ },
4211
+ {
4212
+ name: "label",
4213
+ type: "TEXT"
4214
+ },
4215
+ {
4216
+ name: "cron",
4217
+ type: "TEXT"
4218
+ },
4219
+ {
4220
+ name: "last_run_at",
4221
+ type: "INTEGER"
4222
+ }
4223
+ ] };
4224
+ var BackupDestinationPolicyService = class {
4225
+ backend;
4226
+ tableEnsured = false;
4227
+ constructor(backend) {
4228
+ this.backend = backend;
4193
4229
  }
4194
- increment(e, t, r) {
4195
- return this.second += t.interval !== void 0 && t.interval > 1 && r ? t.interval : 1, this.ms = 0, this.apply(), this.recurse(e, t, 0);
4230
+ /**
4231
+ * Lazy `ensureTable` called from every read/write path. The
4232
+ * settings backend's own `ensureTable` is idempotent (additive
4233
+ * migration), so repeated calls collapse to a no-op after the first.
4234
+ * This keeps the constructor synchronous and avoids a separate
4235
+ * `initialize()` step that callers could forget.
4236
+ */
4237
+ async initialize() {
4238
+ await this.ensureTable();
4196
4239
  }
4197
- decrement(e, t) {
4198
- return this.second -= t.interval !== void 0 && t.interval > 1 ? t.interval : 1, this.ms = 0, this.apply(), this.recurseBackward(e, t, 0, 0);
4240
+ async ensureTable() {
4241
+ if (this.tableEnsured) return;
4242
+ 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");
4243
+ await this.backend.ensureTable(TABLE_NAME$1, BACKUP_DESTINATION_POLICIES_SCHEMA);
4244
+ this.tableEnsured = true;
4199
4245
  }
4200
- recurseBackward(e, t, r, n = 0) {
4201
- if (n > 1e4) return null;
4202
- if (r === 0 && !e.starYear) {
4203
- if (this.year >= 0 && this.year < e.year.length && e.year[this.year] === 0) {
4204
- let a = -1;
4205
- for (let o = this.year - 1; o >= 0; o--) if (e.year[o] === 1) {
4206
- a = o;
4207
- break;
4208
- }
4209
- if (a === -1) return null;
4210
- this.year = a, this.month = 11, this.day = 31, this.hour = 23, this.minute = 59, this.second = 59, this.ms = 0;
4211
- }
4212
- if (this.year < 0) return null;
4213
- }
4214
- let i = this.findPrevious(t, f[r][0], e, f[r][2]);
4215
- if (i > 1) {
4216
- let a = r + 1;
4217
- for (; a < f.length;) {
4218
- let o = f[a][0], h = f[a][2], l = this.getMaxPatternValue(o, e, h);
4219
- this[o] = l, a++;
4220
- }
4221
- if (i === 3) {
4222
- if (this[f[r][1]]--, r === 0) {
4223
- let y = this.getLastDayOfMonth(this.year, this.month);
4224
- this.day > y && (this.day = y);
4225
- }
4226
- if (r === 1) if (this.day <= 0) this.day = 1;
4227
- else {
4228
- let y = this.year, u = this.month;
4229
- for (; u < 0;) u += 12, y--;
4230
- for (; u > 11;) u -= 12, y++;
4231
- let d = u !== 1 ? P[u] : new Date(Date.UTC(y, u + 1, 0)).getUTCDate();
4232
- this.day > d && (this.day = d);
4233
- }
4234
- this.apply();
4235
- let o = f[r][0], h = f[r][2], l = this.getMaxPatternValue(o, e, h);
4236
- if (o === "day") {
4237
- let y = this.getLastDayOfMonth(this.year, this.month);
4238
- this[o] = Math.min(l, y);
4239
- } else this[o] = l;
4240
- if (this.apply(), r === 0) {
4241
- let y = f[1][2], u = this.getMaxPatternValue("day", e, y), d = this.getLastDayOfMonth(this.year, this.month), c = Math.min(u, d);
4242
- 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]));
4243
- }
4244
- if (r === 0 && !e.starYear) {
4245
- for (; this.year >= 0 && this.year < e.year.length && e.year[this.year] === 0;) this.year--;
4246
- if (this.year < 0) return null;
4247
- }
4248
- return this.recurseBackward(e, t, 0, n + 1);
4249
- } else if (this.apply()) return this.recurseBackward(e, t, r - 1, n + 1);
4246
+ async list() {
4247
+ await this.ensureTable();
4248
+ if (!this.backend.tableQuery) return [];
4249
+ const rows = await this.backend.tableQuery(TABLE_NAME$1, { orderBy: {
4250
+ field: "location_id",
4251
+ direction: "asc"
4252
+ } });
4253
+ const out = [];
4254
+ for (const row of rows) {
4255
+ const mapped = mapRowToPolicy(row);
4256
+ if (mapped) out.push(mapped);
4250
4257
  }
4251
- return r += 1, r >= f.length ? this : this.year < 0 ? null : this.recurseBackward(e, t, r, n + 1);
4258
+ return out;
4252
4259
  }
4253
- getMaxPatternValue(e, t, r) {
4254
- if (e === "day" && t.lastDayOfMonth) return this.getLastDayOfMonth(this.year, this.month);
4255
- if (e === "day" && !t.starDOW) return this.getLastDayOfMonth(this.year, this.month);
4256
- for (let n = t[e].length - 1; n >= 0; n--) if (t[e][n]) return n - r;
4257
- return t[e].length - 1 - r;
4260
+ async get(locationId) {
4261
+ await this.ensureTable();
4262
+ if (!this.backend.tableGet) return null;
4263
+ const row = await this.backend.tableGet(TABLE_NAME$1, { location_id: locationId });
4264
+ return row ? mapRowToPolicy(row) : null;
4258
4265
  }
4259
- findPrevious(e, t, r, n) {
4260
- return this._findMatch(e, t, r, n, -1);
4266
+ /**
4267
+ * Replace-or-insert. Implemented as `delete` + `insert` (mirrors the
4268
+ * `SqliteLocationStore` contract) so backends that don't surface
4269
+ * `tableUpdate` still work.
4270
+ */
4271
+ async upsert(p) {
4272
+ await this.ensureTable();
4273
+ if (!this.backend.tableDelete || !this.backend.tableInsert) throw new Error("BackupDestinationPolicyService: backend missing tableDelete/tableInsert");
4274
+ await this.backend.tableDelete(TABLE_NAME$1, { location_id: p.locationId });
4275
+ await this.backend.tableInsert(TABLE_NAME$1, mapPolicyToRow(p));
4261
4276
  }
4262
- getDate(e) {
4263
- 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);
4277
+ async delete(locationId) {
4278
+ await this.ensureTable();
4279
+ if (!this.backend.tableDelete) throw new Error("BackupDestinationPolicyService: backend missing tableDelete");
4280
+ await this.backend.tableDelete(TABLE_NAME$1, { location_id: locationId });
4264
4281
  }
4265
- getTime() {
4266
- return this.getDate(!1).getTime();
4282
+ /**
4283
+ * Auto-create a default policy for a `backups` location seen for the
4284
+ * first time (returns the existing policy unchanged when present).
4285
+ * The orchestrator calls this lazily on the first `listDestinations`
4286
+ * pass so newly-added storage locations are surfaced as enabled
4287
+ * destinations without an explicit configuration step.
4288
+ *
4289
+ * Defaults are passed by the orchestrator (currently:
4290
+ * `retentionCount=7` + `cron='0 3 * * *'`) so a fresh destination
4291
+ * starts running daily at 03:00 with 7 archives kept. Operator can
4292
+ * tune both inline on the destinations table.
4293
+ */
4294
+ async ensureDefault(locationId, defaults) {
4295
+ const existing = await this.get(locationId);
4296
+ if (existing) return existing;
4297
+ const fresh = {
4298
+ locationId,
4299
+ enabled: true,
4300
+ retentionCount: defaults.retentionCount,
4301
+ ...defaults.cron !== void 0 ? { cron: defaults.cron } : {}
4302
+ };
4303
+ await this.upsert(fresh);
4304
+ return fresh;
4267
4305
  }
4268
- match(e, t) {
4269
- if (!e.starYear && (this.year < 0 || this.year >= e.year.length || e.year[this.year] === 0)) return !1;
4270
- for (let r = 0; r < f.length; r++) {
4271
- let n = f[r][0], i = f[r][2], a = this[n];
4272
- if (a + i < 0 || a + i >= e[n].length) return !1;
4273
- let o = e[n][a + i];
4274
- if (n === "day") {
4275
- if (!o) {
4276
- for (let h = 0; h < e.nearestWeekdays.length; h++) if (e.nearestWeekdays[h]) {
4277
- let l = this.getNearestWeekday(this.year, this.month, h - i);
4278
- if (l !== -1 && l === a) {
4279
- o = 1;
4280
- break;
4281
- }
4282
- }
4283
- }
4284
- if (e.lastWeekday) a === this.getLastWeekday(this.year, this.month) && (o = 1);
4285
- if (e.lastDayOfMonth) a === this.getLastDayOfMonth(this.year, this.month) && (o = 1);
4286
- if (!e.starDOW) {
4287
- let h = new Date(Date.UTC(this.year, this.month, 1, 0, 0, 0, 0)).getUTCDay(), l = e.dayOfWeek[(h + (a - 1)) % 7];
4288
- 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;
4289
- }
4290
- }
4291
- if (!o) return !1;
4306
+ };
4307
+ function mapPolicyToRow(p) {
4308
+ return {
4309
+ location_id: p.locationId,
4310
+ enabled: p.enabled ? 1 : 0,
4311
+ retention_count: p.retentionCount,
4312
+ label: p.label ?? null,
4313
+ cron: p.cron ?? null,
4314
+ last_run_at: p.lastRunAt ?? null
4315
+ };
4316
+ }
4317
+ /**
4318
+ * Decode a row pulled from the structured table. Defensive against
4319
+ * schema drift / partial migrations: returns `null` on missing
4320
+ * `location_id` so corrupt rows skip the result list rather than
4321
+ * throwing the whole orchestrator down at boot.
4322
+ */
4323
+ function mapRowToPolicy(row) {
4324
+ const locationId = String(row["location_id"] ?? "");
4325
+ if (!locationId) return null;
4326
+ const labelRaw = row["label"];
4327
+ const label = typeof labelRaw === "string" && labelRaw.length > 0 ? labelRaw : void 0;
4328
+ const cronRaw = row["cron"];
4329
+ const cron = typeof cronRaw === "string" && cronRaw.length > 0 ? cronRaw : void 0;
4330
+ const lastRunRaw = row["last_run_at"];
4331
+ const lastRunAt = typeof lastRunRaw === "number" && lastRunRaw > 0 ? lastRunRaw : void 0;
4332
+ return {
4333
+ locationId,
4334
+ enabled: row["enabled"] === 1 || row["enabled"] === true,
4335
+ retentionCount: Number(row["retention_count"] ?? 0),
4336
+ ...label !== void 0 ? { label } : {},
4337
+ ...cron !== void 0 ? { cron } : {},
4338
+ ...lastRunAt !== void 0 ? { lastRunAt } : {}
4339
+ };
4340
+ }
4341
+ //#endregion
4342
+ //#region src/builtins/backup-orchestrator/download-helpers.ts
4343
+ /**
4344
+ * Chunked-download helper for the backup orchestrator (Task 12 of
4345
+ * the storage-unification refactor).
4346
+ *
4347
+ * Streams an archive from a destination `StorageLocation` into a
4348
+ * local filesystem path via the consumer-facing `storage` cap. Pulled
4349
+ * out of `backup-orchestrator.addon.ts` so it can be unit-tested with
4350
+ * an in-memory fake `AddonApi['storage']` slice — the addon is hard
4351
+ * to bootstrap in vitest because of the BaseAddon ceremony.
4352
+ *
4353
+ * Cleanup contract:
4354
+ * - Successful run leaves the cache file in place + calls
4355
+ * `endDownload`.
4356
+ * - Mid-stream failure destroys the writeStream, removes the
4357
+ * partial cache file, and best-effort calls `endDownload` so the
4358
+ * storage provider's session bookkeeping doesn't leak.
4359
+ */
4360
+ async function downloadArchiveToCache(input) {
4361
+ const { downloadId, sizeBytes } = await input.api.beginDownload.mutate({
4362
+ location: input.locationId,
4363
+ relativePath: input.filename
4364
+ });
4365
+ const writeStream = fs.createWriteStream(input.cachePath);
4366
+ let offset = 0;
4367
+ try {
4368
+ while (offset < sizeBytes) {
4369
+ const remaining = sizeBytes - offset;
4370
+ const length = Math.min(input.chunkBytes, remaining);
4371
+ const chunk = await input.api.readChunk.query({
4372
+ downloadId,
4373
+ offset,
4374
+ length
4375
+ });
4376
+ if (chunk.byteLength === 0) throw new Error(`backup restore: empty chunk at offset ${offset}/${sizeBytes} from "${input.locationId}/${input.filename}"`);
4377
+ writeStream.write(chunk);
4378
+ offset += chunk.byteLength;
4292
4379
  }
4293
- return !0;
4380
+ await new Promise((resolve, reject) => {
4381
+ writeStream.end((err) => {
4382
+ if (err) reject(err);
4383
+ else resolve();
4384
+ });
4385
+ });
4386
+ try {
4387
+ await input.api.endDownload.mutate({ downloadId });
4388
+ } catch (err) {
4389
+ input.logger.warn("backup restore: endDownload failed (ignoring)", { meta: {
4390
+ downloadId,
4391
+ error: err instanceof Error ? err.message : String(err)
4392
+ } });
4393
+ }
4394
+ } catch (err) {
4395
+ await new Promise((resolve) => {
4396
+ writeStream.once("close", () => resolve());
4397
+ writeStream.destroy();
4398
+ });
4399
+ try {
4400
+ await fsp.rm(input.cachePath, { force: true });
4401
+ } catch {}
4402
+ try {
4403
+ await input.api.endDownload.mutate({ downloadId });
4404
+ } catch {}
4405
+ throw err;
4294
4406
  }
4295
- };
4296
- function R(s) {
4297
- 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, ![
4298
- "auto",
4299
- "5-part",
4300
- "6-part",
4301
- "7-part",
4302
- "5-or-6-parts",
4303
- "6-or-7-parts"
4304
- ].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'.");
4305
- if (s.startAt && (s.startAt = new m(s.startAt, s.timezone)), s.stopAt && (s.stopAt = new m(s.stopAt, s.timezone)), s.interval !== null) {
4306
- if (isNaN(s.interval)) throw new Error("CronOptions: Supplied value for interval is not a number");
4307
- if (s.interval < 0) throw new Error("CronOptions: Supplied value for interval can not be negative");
4407
+ }
4408
+ //#endregion
4409
+ //#region src/builtins/backup-orchestrator/manifest-store.ts
4410
+ /** File path (relative to the location root) where the index lives. */
4411
+ var MANIFESTS_FILENAME = "manifests.json";
4412
+ /**
4413
+ * Read the per-location manifest. Returns an empty manifest on first
4414
+ * use (when `manifests.json` doesn't exist) or when the file is
4415
+ * unreadable / corrupt — operators see destination state degrade
4416
+ * gracefully rather than the orchestrator crashing.
4417
+ */
4418
+ async function readLocationManifest(api, locationId) {
4419
+ if (!await api.exists.query({
4420
+ location: locationId,
4421
+ relativePath: "manifests.json"
4422
+ }).catch(() => false)) return EMPTY_MANIFEST;
4423
+ let bytes;
4424
+ try {
4425
+ bytes = await api.read.query({
4426
+ location: locationId,
4427
+ relativePath: MANIFESTS_FILENAME
4428
+ });
4429
+ } catch {
4430
+ return EMPTY_MANIFEST;
4308
4431
  }
4309
- if (s.utcOffset !== void 0) {
4310
- if (isNaN(s.utcOffset)) throw new Error("CronOptions: Invalid value passed for utcOffset, should be number representing minutes offset from UTC.");
4311
- if (s.utcOffset < -870 || s.utcOffset > 870) throw new Error("CronOptions: utcOffset out of bounds.");
4312
- if (s.utcOffset !== void 0 && s.timezone) throw new Error("CronOptions: Combining 'utcOffset' with 'timezone' is not allowed.");
4432
+ return parseManifestBytes(bytes);
4433
+ }
4434
+ async function writeLocationManifest(api, locationId, manifest) {
4435
+ const json = JSON.stringify(manifest, null, 2);
4436
+ const bytes = new TextEncoder().encode(json);
4437
+ await api.write.mutate({
4438
+ location: locationId,
4439
+ relativePath: MANIFESTS_FILENAME,
4440
+ data: bytes
4441
+ });
4442
+ }
4443
+ /**
4444
+ * Decode the raw bytes of `manifests.json`. Falls back to an empty
4445
+ * manifest on JSON-parse / shape-validation failure — same defensive
4446
+ * stance as the legacy `LocalBackupService.ensureManifestsLoaded`.
4447
+ */
4448
+ function parseManifestBytes(bytes) {
4449
+ try {
4450
+ const text = new TextDecoder().decode(bytes);
4451
+ if (text.trim().length === 0) return EMPTY_MANIFEST;
4452
+ return normalizeManifest(JSON.parse(text));
4453
+ } catch {
4454
+ return EMPTY_MANIFEST;
4313
4455
  }
4314
- if (s.unref !== !0 && s.unref !== !1) throw new Error("CronOptions: Unref should be either true, false or undefined(false).");
4315
- 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.");
4316
- return s;
4317
4456
  }
4318
- function p(s) {
4319
- return Object.prototype.toString.call(s) === "[object Function]" || typeof s == "function" || s instanceof Function;
4457
+ /**
4458
+ * Coerce an arbitrary parsed JSON shape into the canonical manifest
4459
+ * type. Bogus archive entries are silently dropped (a corrupt row
4460
+ * shouldn't lock the whole location out of operator visibility).
4461
+ */
4462
+ function normalizeManifest(raw) {
4463
+ if (typeof raw !== "object" || raw == null) return EMPTY_MANIFEST;
4464
+ const obj = raw;
4465
+ const archives = [];
4466
+ if (Array.isArray(obj.archives)) for (const a of obj.archives) {
4467
+ if (typeof a !== "object" || a == null) continue;
4468
+ const cand = a;
4469
+ if (typeof cand["id"] !== "string" || typeof cand["filename"] !== "string") continue;
4470
+ if (typeof cand["createdAt"] !== "number" || typeof cand["sizeBytes"] !== "number") continue;
4471
+ const manifest = cand["manifest"];
4472
+ if (typeof manifest !== "object" || manifest == null) continue;
4473
+ const entry = {
4474
+ id: String(cand["id"]),
4475
+ filename: String(cand["filename"]),
4476
+ createdAt: Number(cand["createdAt"]),
4477
+ sizeBytes: Number(cand["sizeBytes"]),
4478
+ ...typeof cand["label"] === "string" ? { label: cand["label"] } : {},
4479
+ manifest
4480
+ };
4481
+ archives.push(entry);
4482
+ }
4483
+ return {
4484
+ version: 1,
4485
+ archives
4486
+ };
4320
4487
  }
4321
- function _(s) {
4322
- return p(s);
4488
+ var EMPTY_MANIFEST = {
4489
+ version: 1,
4490
+ archives: []
4491
+ };
4492
+ /**
4493
+ * Build a `ManifestArchiveEntry` from the raw inputs collected during
4494
+ * a successful upload. Pure transform — no I/O — kept here so the
4495
+ * triggerBackup path stays terse.
4496
+ */
4497
+ function buildArchiveEntry(input) {
4498
+ return {
4499
+ id: input.id,
4500
+ filename: input.filename,
4501
+ createdAt: input.createdAt,
4502
+ sizeBytes: input.sizeBytes,
4503
+ ...input.label !== void 0 ? { label: input.label } : {},
4504
+ manifest: input.manifest
4505
+ };
4323
4506
  }
4324
- function x(s) {
4325
- typeof Deno < "u" && typeof Deno.unrefTimer < "u" ? Deno.unrefTimer(s) : s && typeof s.unref < "u" && s.unref();
4507
+ /**
4508
+ * Pure retention split: given an archive list and the operator's
4509
+ * `retentionCount`, return `{ keep, drop }` where `keep` is the most-
4510
+ * recent N archives (newest first) and `drop` is the older overflow.
4511
+ *
4512
+ * `retentionCount <= 0` is treated as "no pruning" — defensive against
4513
+ * a config value that would otherwise wipe every archive on the next
4514
+ * write.
4515
+ */
4516
+ function applyRetention(archives, retentionCount) {
4517
+ if (retentionCount <= 0) return {
4518
+ keep: archives,
4519
+ drop: []
4520
+ };
4521
+ const sorted = [...archives].toSorted((a, b) => b.createdAt - a.createdAt);
4522
+ if (sorted.length <= retentionCount) return {
4523
+ keep: sorted,
4524
+ drop: []
4525
+ };
4526
+ return {
4527
+ keep: sorted.slice(0, retentionCount),
4528
+ drop: sorted.slice(retentionCount)
4529
+ };
4326
4530
  }
4327
- var W = 30 * 1e3, w = [], E = class {
4328
- name;
4329
- options;
4330
- _states;
4331
- fn;
4332
- getTz() {
4333
- return this.options.timezone || this.options.utcOffset;
4334
- }
4335
- applyDayOffset(e) {
4336
- if (this.options.dayOffset !== void 0 && this.options.dayOffset !== 0) {
4337
- let t = this.options.dayOffset * 24 * 60 * 60 * 1e3;
4338
- return new Date(e.getTime() + t);
4339
- }
4340
- return e;
4341
- }
4342
- constructor(e, t, r) {
4343
- let n, i;
4344
- if (p(t)) i = t;
4345
- else if (typeof t == "object") n = t;
4346
- else if (t !== void 0) throw new Error("Cron: Invalid argument passed for optionsIn. Should be one of function, or object (options).");
4347
- if (p(r)) i = r;
4348
- else if (typeof r == "object") n = r;
4349
- else if (r !== void 0) throw new Error("Cron: Invalid argument passed for funcIn. Should be one of function, or object (options).");
4350
- if (this.name = n?.name, this.options = R(n), this._states = {
4351
- kill: !1,
4352
- blocking: !1,
4353
- previousRun: void 0,
4354
- currentRun: void 0,
4355
- once: void 0,
4356
- currentTimeout: void 0,
4357
- maxRuns: n ? n.maxRuns : void 0,
4358
- paused: n ? n.paused : !1,
4359
- pattern: new C("* * * * *", void 0, { mode: "auto" })
4360
- }, 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, {
4361
- mode: this.options.mode,
4362
- alternativeWeekdays: this.options.alternativeWeekdays,
4363
- sloppyRanges: this.options.sloppyRanges
4364
- }), this.name) {
4365
- if (w.find((o) => o.name === this.name)) throw new Error("Cron: Tried to initialize new named job '" + this.name + "', but name already taken.");
4366
- w.push(this);
4367
- }
4368
- return i !== void 0 && _(i) && (this.fn = i, this.schedule()), this;
4369
- }
4370
- nextRun(e) {
4371
- let t = this._next(e);
4372
- return t ? this.applyDayOffset(t.getDate(!1)) : null;
4373
- }
4374
- nextRuns(e, t) {
4375
- this._states.maxRuns !== void 0 && e > this._states.maxRuns && (e = this._states.maxRuns);
4376
- let r = t || this._states.currentRun || void 0;
4377
- return this._enumerateRuns(e, r, "next");
4378
- }
4379
- previousRuns(e, t) {
4380
- return this._enumerateRuns(e, t || void 0, "previous");
4381
- }
4382
- _enumerateRuns(e, t, r) {
4383
- let n = [], i = t ? new m(t, this.getTz()) : null, a = r === "next" ? this._next : this._previous;
4384
- for (; e--;) {
4385
- let o = a.call(this, i);
4386
- if (!o) break;
4387
- let h = o.getDate(!1);
4388
- n.push(this.applyDayOffset(h)), i = o;
4389
- }
4390
- return n;
4391
- }
4392
- match(e) {
4393
- if (this._states.once) {
4394
- let r = new m(e, this.getTz());
4395
- r.ms = 0;
4396
- let n = new m(this._states.once, this.getTz());
4397
- return n.ms = 0, r.getTime() === n.getTime();
4531
+ /**
4532
+ * Per-location async mutex. The orchestrator owns one instance and
4533
+ * funnels every read-modify-write of `manifests.json` through it so
4534
+ * parallel destination uploads (within the same location) commit in
4535
+ * arrival order. Across locations the operations run in parallel —
4536
+ * each location keys its own promise chain.
4537
+ */
4538
+ var LocationManifestLock = class {
4539
+ chains = /* @__PURE__ */ new Map();
4540
+ async run(locationId, fn) {
4541
+ const previous = this.chains.get(locationId) ?? Promise.resolve();
4542
+ let resolveNext;
4543
+ const next = new Promise((res) => {
4544
+ resolveNext = res;
4545
+ });
4546
+ this.chains.set(locationId, previous.then(() => next));
4547
+ try {
4548
+ await previous;
4549
+ return await fn();
4550
+ } finally {
4551
+ resolveNext();
4552
+ const tail = this.chains.get(locationId);
4553
+ if (tail) Promise.resolve(tail).then(() => {
4554
+ if (this.chains.get(locationId) === tail) this.chains.delete(locationId);
4555
+ }).catch(() => {});
4398
4556
  }
4399
- let t = new m(e, this.getTz());
4400
- return t.ms = 0, t.match(this._states.pattern, this.options);
4401
- }
4402
- getPattern() {
4403
- if (!this._states.once) return this._states.pattern ? this._states.pattern.pattern : void 0;
4404
- }
4405
- getOnce() {
4406
- return this._states.once ? this._states.once.getDate() : null;
4407
- }
4408
- isRunning() {
4409
- let e = this.nextRun(this._states.currentRun), t = !this._states.paused, r = this.fn !== void 0, n = !this._states.kill;
4410
- return t && r && n && e !== null;
4411
- }
4412
- isStopped() {
4413
- return this._states.kill;
4414
- }
4415
- isBusy() {
4416
- return this._states.blocking;
4417
- }
4418
- currentRun() {
4419
- return this._states.currentRun ? this._states.currentRun.getDate() : null;
4420
- }
4421
- previousRun() {
4422
- return this._states.previousRun ? this._states.previousRun.getDate() : null;
4423
- }
4424
- msToNext(e) {
4425
- let t = this._next(e);
4426
- return t ? e instanceof m || e instanceof Date ? t.getTime() - e.getTime() : t.getTime() - new m(e).getTime() : null;
4427
4557
  }
4428
- stop() {
4429
- this._states.kill = !0, this._states.currentTimeout && clearTimeout(this._states.currentTimeout);
4430
- let e = w.indexOf(this);
4431
- e >= 0 && w.splice(e, 1);
4558
+ };
4559
+ //#endregion
4560
+ //#region src/builtins/backup-orchestrator/schedule-store.ts
4561
+ var TABLE_NAME = "backup_schedules";
4562
+ var BACKUP_SCHEDULES_SCHEMA = { columns: [
4563
+ {
4564
+ name: "id",
4565
+ type: "TEXT",
4566
+ primaryKey: true
4567
+ },
4568
+ {
4569
+ name: "label",
4570
+ type: "TEXT",
4571
+ notNull: true,
4572
+ defaultValue: ""
4573
+ },
4574
+ {
4575
+ name: "cron",
4576
+ type: "TEXT",
4577
+ notNull: true,
4578
+ defaultValue: ""
4579
+ },
4580
+ {
4581
+ name: "enabled",
4582
+ type: "INTEGER",
4583
+ notNull: true,
4584
+ defaultValue: 1
4585
+ },
4586
+ {
4587
+ name: "location_ids",
4588
+ type: "TEXT",
4589
+ notNull: true,
4590
+ defaultValue: "[]"
4591
+ },
4592
+ {
4593
+ name: "retention_count",
4594
+ type: "INTEGER",
4595
+ notNull: true,
4596
+ defaultValue: 7
4597
+ },
4598
+ {
4599
+ name: "data_sources",
4600
+ type: "TEXT"
4601
+ },
4602
+ {
4603
+ name: "last_run_at",
4604
+ type: "INTEGER"
4432
4605
  }
4433
- pause() {
4434
- return this._states.paused = !0, !this._states.kill;
4606
+ ] };
4607
+ var BackupScheduleService = class {
4608
+ backend;
4609
+ tableEnsured = false;
4610
+ constructor(backend) {
4611
+ this.backend = backend;
4435
4612
  }
4436
- resume() {
4437
- return this._states.paused = !1, !this._states.kill;
4613
+ async initialize() {
4614
+ await this.ensureTable();
4438
4615
  }
4439
- schedule(e) {
4440
- if (e && this.fn) throw new Error("Cron: It is not allowed to schedule two functions using the same Croner instance.");
4441
- e && (this.fn = e);
4442
- let t = this.msToNext(), r = this.nextRun(this._states.currentRun);
4443
- 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);
4616
+ async ensureTable() {
4617
+ if (this.tableEnsured) return;
4618
+ if (!this.backend.ensureTable) throw new Error("BackupScheduleService: settings backend does not implement ensureTable — expected the SQLite settings backend, got a backend without structured-table support");
4619
+ await this.backend.ensureTable(TABLE_NAME, BACKUP_SCHEDULES_SCHEMA);
4620
+ this.tableEnsured = true;
4444
4621
  }
4445
- async _trigger(e) {
4446
- this._states.blocking = !0, this._states.currentRun = new m(void 0, this.getTz());
4447
- try {
4448
- if (this.options.catch) try {
4449
- this.fn !== void 0 && await this.fn(this, this.options.context);
4450
- } catch (t) {
4451
- if (p(this.options.catch)) try {
4452
- this.options.catch(t, this);
4453
- } catch {}
4454
- }
4455
- else this.fn !== void 0 && await this.fn(this, this.options.context);
4456
- } finally {
4457
- this._states.previousRun = new m(e, this.getTz()), this._states.blocking = !1;
4622
+ async list() {
4623
+ await this.ensureTable();
4624
+ if (!this.backend.tableQuery) return [];
4625
+ const rows = await this.backend.tableQuery(TABLE_NAME, { orderBy: {
4626
+ field: "id",
4627
+ direction: "asc"
4628
+ } });
4629
+ const out = [];
4630
+ for (const row of rows) {
4631
+ const mapped = mapRowToSchedule(row);
4632
+ if (mapped) out.push(mapped);
4458
4633
  }
4634
+ return out;
4459
4635
  }
4460
- async trigger() {
4461
- await this._trigger();
4462
- }
4463
- runsLeft() {
4464
- return this._states.maxRuns;
4465
- }
4466
- _checkTrigger(e) {
4467
- let t = /* @__PURE__ */ new Date(), r = !this._states.paused && t.getTime() >= e.getTime(), n = this._states.blocking && this.options.protect;
4468
- 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();
4469
- }
4470
- _next(e) {
4471
- let t = !!(e || this._states.currentRun), r = !1;
4472
- !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);
4473
- let n = this._states.once || new m(e, this.getTz());
4474
- 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;
4636
+ async get(id) {
4637
+ await this.ensureTable();
4638
+ if (!this.backend.tableGet) return null;
4639
+ const row = await this.backend.tableGet(TABLE_NAME, { id });
4640
+ return row ? mapRowToSchedule(row) : null;
4475
4641
  }
4476
- _previous(e) {
4477
- let t = new m(e, this.getTz());
4478
- this.options.stopAt && t.getTime() > this.options.stopAt.getTime() && (t = this.options.stopAt);
4479
- let r = new m(t, this.getTz());
4480
- 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);
4642
+ /** Replace-or-insert (delete + insert — mirrors the policy store). */
4643
+ async upsert(s) {
4644
+ await this.ensureTable();
4645
+ if (!this.backend.tableDelete || !this.backend.tableInsert) throw new Error("BackupScheduleService: backend missing tableDelete/tableInsert");
4646
+ await this.backend.tableDelete(TABLE_NAME, { id: s.id });
4647
+ await this.backend.tableInsert(TABLE_NAME, mapScheduleToRow(s));
4481
4648
  }
4482
- _calculatePreviousRun(e, t) {
4483
- let r = new m(void 0, this.getTz()), n = e;
4484
- if (this.options.startAt.getTime() <= r.getTime()) {
4485
- n = this.options.startAt;
4486
- let i = n.getTime() + this.options.interval * 1e3;
4487
- for (; i <= r.getTime();) n = new m(n, this.getTz()).increment(this._states.pattern, this.options, !0), i = n.getTime() + this.options.interval * 1e3;
4488
- t = !0;
4489
- }
4490
- return n === null && (n = void 0), [n, t];
4649
+ async delete(id) {
4650
+ await this.ensureTable();
4651
+ if (!this.backend.tableDelete) throw new Error("BackupScheduleService: backend missing tableDelete");
4652
+ await this.backend.tableDelete(TABLE_NAME, { id });
4491
4653
  }
4492
4654
  };
4493
- //#endregion
4494
- //#region src/builtins/backup-orchestrator/cron-helpers.ts
4495
- /**
4496
- * Cron helpers — wraps `croner` so the orchestrator + the `previewNextRuns`
4497
- * cap method share one validation + next-run-computation surface.
4498
- *
4499
- * `croner` is the opinionated node cron library we standardize on
4500
- * server-side: pure JS, native TypeScript types, supports the full
4501
- * 5-field POSIX cron grammar plus seconds (6-field), DST-aware, and
4502
- * exposes `nextRun()` / `nextRuns(N)` for peeking without scheduling.
4503
- * The orchestrator uses the peeking API (it has its own polling
4504
- * loop); future surfaces (e.g. event-driven retention sweeps) can
4505
- * register an actual scheduled `Cron(pattern, fn)` and let croner
4506
- * own the timer.
4507
- */
4508
- /**
4509
- * Validate a cron expression and return its next firing time strictly
4510
- * after `after`, expressed as ms-epoch. Returns `null` when the
4511
- * expression has no future runs (croner returns null e.g. for
4512
- * fully-bounded patterns that have already elapsed). Throws when the
4513
- * expression is syntactically invalid — callers are expected to
4514
- * surface that to the operator.
4515
- */
4516
- function computeNextDue(cron, after) {
4517
- const next = new E(cron, { paused: true }).nextRun(new Date(after));
4518
- return next ? next.getTime() : null;
4655
+ function mapScheduleToRow(s) {
4656
+ return {
4657
+ id: s.id,
4658
+ label: s.label,
4659
+ cron: s.cron,
4660
+ enabled: s.enabled ? 1 : 0,
4661
+ location_ids: JSON.stringify([...s.locationIds]),
4662
+ retention_count: s.retentionCount,
4663
+ data_sources: s.dataSources ? JSON.stringify([...s.dataSources]) : null,
4664
+ last_run_at: s.lastRunAt ?? null
4665
+ };
4519
4666
  }
4520
- /**
4521
- * Validate without computing returns true when `cron` parses, false
4522
- * otherwise. Used by the cap-router input zod schema's `.refine`
4523
- * callback to reject malformed expressions at the boundary instead
4524
- * of at scheduler-tick time.
4525
- */
4526
- function isValidCron(cron) {
4667
+ function parseStringArray(raw) {
4668
+ if (typeof raw !== "string" || raw.length === 0) return [];
4527
4669
  try {
4528
- new E(cron, { paused: true });
4529
- return true;
4670
+ const parsed = JSON.parse(raw);
4671
+ if (!Array.isArray(parsed)) return [];
4672
+ return parsed.filter((v) => typeof v === "string");
4530
4673
  } catch {
4531
- return false;
4674
+ return [];
4532
4675
  }
4533
4676
  }
4534
- /**
4535
- * Peek the next N firing times after `after` (default: now). Used by
4536
- * the admin UI's CronEditor preview pane — operators see the next
4537
- * 3–5 runs as they edit the expression so a wrong pattern is
4538
- * obvious before save.
4539
- */
4540
- function previewNextRuns(cron, count, after) {
4541
- const job = new E(cron, { paused: true });
4542
- const baseline = after !== void 0 ? new Date(after) : /* @__PURE__ */ new Date();
4543
- const out = [];
4544
- let cursor = baseline;
4545
- for (let i = 0; i < count; i++) {
4546
- cursor = job.nextRun(cursor);
4547
- if (!cursor) break;
4548
- out.push(cursor.getTime());
4549
- }
4550
- return out;
4677
+ function mapRowToSchedule(row) {
4678
+ const id = String(row["id"] ?? "");
4679
+ if (!id) return null;
4680
+ const dataSourcesRaw = row["data_sources"];
4681
+ const dataSources = typeof dataSourcesRaw === "string" && dataSourcesRaw.length > 0 ? parseStringArray(dataSourcesRaw) : void 0;
4682
+ const lastRunRaw = row["last_run_at"];
4683
+ const lastRunAt = typeof lastRunRaw === "number" && lastRunRaw > 0 ? lastRunRaw : void 0;
4684
+ return {
4685
+ id,
4686
+ label: typeof row["label"] === "string" ? String(row["label"]) : "",
4687
+ cron: typeof row["cron"] === "string" ? String(row["cron"]) : "",
4688
+ enabled: row["enabled"] === 1 || row["enabled"] === true,
4689
+ locationIds: parseStringArray(row["location_ids"]),
4690
+ retentionCount: Number(row["retention_count"] ?? 7),
4691
+ ...dataSources !== void 0 ? { dataSources } : {},
4692
+ ...lastRunAt !== void 0 ? { lastRunAt } : {}
4693
+ };
4551
4694
  }
4552
4695
  //#endregion
4553
4696
  //#region src/builtins/backup-orchestrator/backup-orchestrator.addon.ts
@@ -4620,6 +4763,7 @@ var ORCHESTRATOR_ADDON_ID = "backup-orchestrator";
4620
4763
  var BackupOrchestratorAddon = class extends BaseAddon {
4621
4764
  systemBackup = null;
4622
4765
  policies = null;
4766
+ schedules = null;
4623
4767
  dataDir = "";
4624
4768
  /**
4625
4769
  * Transient build buffer for archive fan-out. The tar is built here
@@ -4653,6 +4797,14 @@ var BackupOrchestratorAddon = class extends BaseAddon {
4653
4797
  this.ctx.logger.error("backup-orchestrator: policy table init failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
4654
4798
  this.policies = null;
4655
4799
  }
4800
+ this.schedules = this.resolveScheduleService();
4801
+ if (this.schedules) try {
4802
+ await this.schedules.initialize();
4803
+ await this.migrateLegacyCronToSchedules();
4804
+ } catch (err) {
4805
+ this.ctx.logger.error("backup-orchestrator: schedule table init failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
4806
+ this.schedules = null;
4807
+ }
4656
4808
  const provider = {
4657
4809
  listDestinations: () => this.listDestinations(),
4658
4810
  list: () => this.aggregatedList(),
@@ -4663,7 +4815,10 @@ var BackupOrchestratorAddon = class extends BaseAddon {
4663
4815
  delete: (input) => this.deleteArchive(input),
4664
4816
  listArchives: (input) => this.listArchives(input),
4665
4817
  upsertDestinationPolicy: (input) => this.upsertDestinationPolicy(input),
4666
- previewSchedule: (input) => this.previewSchedule(input)
4818
+ previewSchedule: (input) => this.previewSchedule(input),
4819
+ listSchedules: () => this.listSchedules(),
4820
+ upsertSchedule: (input) => this.upsertSchedule(input),
4821
+ deleteSchedule: (input) => this.deleteSchedule(input)
4667
4822
  };
4668
4823
  this.startScheduleTimer();
4669
4824
  this.ctx.logger.info("Backup orchestrator initialized", { meta: {
@@ -4679,6 +4834,7 @@ var BackupOrchestratorAddon = class extends BaseAddon {
4679
4834
  this.stopScheduleTimer();
4680
4835
  this.systemBackup = null;
4681
4836
  this.policies = null;
4837
+ this.schedules = null;
4682
4838
  }
4683
4839
  /**
4684
4840
  * Aggregate destinations from `api.storage.listLocations({ type: 'backups' })`
@@ -4860,7 +5016,7 @@ var BackupOrchestratorAddon = class extends BaseAddon {
4860
5016
  cachePath,
4861
5017
  sizeBytes: result.sizeBytes,
4862
5018
  archiveManifest: result.manifest,
4863
- retentionCount: policy.retentionCount,
5019
+ retentionCount: input?.retentionCount ?? policy.retentionCount,
4864
5020
  createdAt,
4865
5021
  label: formatArchiveLabel(dest.id, createdAt, input?.label)
4866
5022
  });
@@ -5153,65 +5309,177 @@ var BackupOrchestratorAddon = class extends BaseAddon {
5153
5309
  this.scheduleTimer = null;
5154
5310
  }
5155
5311
  /**
5156
- * Per-destination cron-based scheduling. Walks every enabled
5157
- * destination policy that carries a `cron` expression, computes the
5158
- * next due time after `lastRunAt` (or the orchestrator's first-tick
5159
- * baseline if never run), and fires a backup targeting that single
5160
- * destination when due. Each destination keeps its own cadence —
5161
- * an offsite SFTP can run weekly while local stays nightly without
5162
- * any global setting.
5312
+ * Schedule-driven backup scheduling. Walks every enabled backup
5313
+ * SCHEDULE (the N:M entry), computes the next due time after
5314
+ * `lastRunAt` (or the orchestrator's first-tick baseline if never
5315
+ * run), and fires a SINGLE archive fanned out to ALL of the
5316
+ * schedule's `locationIds` when due with the schedule's per-entry
5317
+ * retention applied to each. Each schedule keeps its own cadence.
5163
5318
  *
5164
- * Errors per destination are isolated; one misconfigured cron
5165
- * doesn't block the others. The policy's `lastRunAt` is updated
5166
- * BEFORE the trigger to dedupe (the tick runs every
5167
- * SCHEDULE_TICK_MS — without the up-front update a long-running
5168
- * upload could overlap with the next tick's same-due check).
5319
+ * Errors per schedule are isolated; one misconfigured cron doesn't
5320
+ * block the others. The schedule's `lastRunAt` is updated BEFORE the
5321
+ * trigger to dedupe (the tick runs every SCHEDULE_TICK_MS — without
5322
+ * the up-front update a long-running upload could overlap with the
5323
+ * next tick's same-due check).
5169
5324
  */
5170
5325
  async onScheduleTick() {
5171
- if (!this.policies) return;
5172
- const policies = await this.policies.list();
5326
+ if (!this.schedules) return;
5327
+ const schedules = await this.schedules.list();
5173
5328
  const now = Date.now();
5174
- for (const p of policies) {
5175
- if (!p.enabled || !p.cron || p.cron.trim().length === 0) continue;
5329
+ for (const s of schedules) {
5330
+ if (!s.enabled || !s.cron || s.cron.trim().length === 0) continue;
5331
+ if (s.locationIds.length === 0) continue;
5176
5332
  let due;
5177
5333
  try {
5178
- due = computeNextDue(p.cron, p.lastRunAt ?? this.scheduleBaselineAt);
5334
+ due = computeNextDue(s.cron, s.lastRunAt ?? this.scheduleBaselineAt);
5179
5335
  } catch (err) {
5180
- this.ctx.logger.warn("Invalid cron — skipping destination", { meta: {
5181
- locationId: p.locationId,
5182
- cron: p.cron,
5336
+ this.ctx.logger.warn("Invalid cron — skipping schedule", { meta: {
5337
+ scheduleId: s.id,
5338
+ cron: s.cron,
5183
5339
  error: err instanceof Error ? err.message : String(err)
5184
5340
  } });
5185
5341
  continue;
5186
5342
  }
5187
5343
  if (due === null || due > now) continue;
5188
5344
  const stamped = {
5189
- ...p,
5345
+ ...s,
5190
5346
  lastRunAt: now
5191
5347
  };
5192
5348
  try {
5193
- await this.policies.upsert(stamped);
5349
+ await this.schedules.upsert(stamped);
5194
5350
  } catch (err) {
5195
5351
  this.ctx.logger.warn("Failed to persist scheduled-run timestamp", { meta: {
5196
- locationId: p.locationId,
5352
+ scheduleId: s.id,
5197
5353
  error: err instanceof Error ? err.message : String(err)
5198
5354
  } });
5199
5355
  }
5200
5356
  try {
5201
- this.ctx.logger.info("Scheduled backup firing for destination", { meta: {
5202
- locationId: p.locationId,
5203
- cron: p.cron,
5357
+ this.ctx.logger.info("Scheduled backup firing", { meta: {
5358
+ scheduleId: s.id,
5359
+ label: s.label,
5360
+ locationIds: s.locationIds,
5361
+ cron: s.cron,
5204
5362
  dueAt: due
5205
5363
  } });
5206
- await this.triggerBackup({ destinations: [p.locationId] });
5364
+ await this.triggerBackup({
5365
+ destinations: [...s.locationIds],
5366
+ ...s.dataSources && s.dataSources.length > 0 ? { locations: [...s.dataSources] } : {},
5367
+ label: s.label,
5368
+ retentionCount: s.retentionCount
5369
+ });
5207
5370
  } catch (err) {
5208
5371
  this.ctx.logger.error("Scheduled backup failed", { meta: {
5372
+ scheduleId: s.id,
5373
+ error: err instanceof Error ? err.message : String(err)
5374
+ } });
5375
+ }
5376
+ }
5377
+ }
5378
+ async listSchedules() {
5379
+ if (!this.schedules) return [];
5380
+ return (await this.schedules.list()).map((s) => {
5381
+ let nextRunAt;
5382
+ if (s.enabled && s.cron.trim().length > 0) try {
5383
+ const next = computeNextDue(s.cron, Date.now());
5384
+ if (next !== null) nextRunAt = next;
5385
+ } catch {}
5386
+ return {
5387
+ id: s.id,
5388
+ label: s.label,
5389
+ cron: s.cron,
5390
+ enabled: s.enabled,
5391
+ locationIds: [...s.locationIds],
5392
+ retentionCount: s.retentionCount,
5393
+ ...s.dataSources !== void 0 ? { dataSources: [...s.dataSources] } : {},
5394
+ ...s.lastRunAt !== void 0 ? { lastRunAt: s.lastRunAt } : {},
5395
+ ...nextRunAt !== void 0 ? { nextRunAt } : {}
5396
+ };
5397
+ });
5398
+ }
5399
+ async upsertSchedule(input) {
5400
+ if (!this.schedules) throw new Error("backup upsertSchedule: no settings-store backend — schedule cannot be persisted");
5401
+ const cron = input.cron.trim();
5402
+ if (cron.length > 0 && !isValidCron(cron)) throw new Error(`Invalid cron expression: "${cron}"`);
5403
+ const id = input.id && input.id.length > 0 ? input.id : randomUUID();
5404
+ const existing = await this.schedules.get(id).catch(() => null);
5405
+ const next = {
5406
+ id,
5407
+ label: input.label,
5408
+ cron,
5409
+ enabled: input.enabled,
5410
+ locationIds: [...input.locationIds],
5411
+ retentionCount: input.retentionCount,
5412
+ ...input.dataSources && input.dataSources.length > 0 ? { dataSources: [...input.dataSources] } : {},
5413
+ ...existing?.lastRunAt !== void 0 ? { lastRunAt: existing.lastRunAt } : {}
5414
+ };
5415
+ await this.schedules.upsert(next);
5416
+ let nextRunAt;
5417
+ if (next.enabled && cron.length > 0) try {
5418
+ const due = computeNextDue(cron, Date.now());
5419
+ if (due !== null) nextRunAt = due;
5420
+ } catch {}
5421
+ return {
5422
+ id: next.id,
5423
+ label: next.label,
5424
+ cron: next.cron,
5425
+ enabled: next.enabled,
5426
+ locationIds: [...next.locationIds],
5427
+ retentionCount: next.retentionCount,
5428
+ ...next.dataSources !== void 0 ? { dataSources: [...next.dataSources] } : {},
5429
+ ...next.lastRunAt !== void 0 ? { lastRunAt: next.lastRunAt } : {},
5430
+ ...nextRunAt !== void 0 ? { nextRunAt } : {}
5431
+ };
5432
+ }
5433
+ async deleteSchedule(input) {
5434
+ if (!this.schedules) return;
5435
+ await this.schedules.delete(input.id);
5436
+ }
5437
+ /**
5438
+ * One-shot migration: pre-schedule installs carried the cron on each
5439
+ * `BackupDestinationPolicy`. When the schedule table is empty but
5440
+ * policies with crons exist, mint one schedule per cron'd destination
5441
+ * so existing scheduled backups keep firing after the redesign. The
5442
+ * legacy per-location cron is left intact but the scheduler no longer
5443
+ * reads it, so no double-fire.
5444
+ */
5445
+ async migrateLegacyCronToSchedules() {
5446
+ if (!this.schedules || !this.policies) return;
5447
+ if ((await this.schedules.list()).length > 0) return;
5448
+ const cronned = (await this.policies.list()).filter((p) => p.cron && p.cron.trim().length > 0);
5449
+ if (cronned.length === 0) return;
5450
+ for (const p of cronned) {
5451
+ const schedule = {
5452
+ id: randomUUID(),
5453
+ label: p.label ?? p.locationId,
5454
+ cron: p.cron ?? "",
5455
+ enabled: p.enabled,
5456
+ locationIds: [p.locationId],
5457
+ retentionCount: p.retentionCount,
5458
+ ...p.lastRunAt !== void 0 ? { lastRunAt: p.lastRunAt } : {}
5459
+ };
5460
+ try {
5461
+ await this.schedules.upsert(schedule);
5462
+ this.ctx.logger.info("Migrated legacy per-location cron to schedule", { meta: {
5463
+ locationId: p.locationId,
5464
+ scheduleId: schedule.id,
5465
+ cron: schedule.cron
5466
+ } });
5467
+ } catch (err) {
5468
+ this.ctx.logger.warn("Legacy cron migration failed for destination", { meta: {
5209
5469
  locationId: p.locationId,
5210
5470
  error: err instanceof Error ? err.message : String(err)
5211
5471
  } });
5212
5472
  }
5213
5473
  }
5214
5474
  }
5475
+ resolveScheduleService() {
5476
+ const reg = this.ctx.kernel.capabilityRegistry;
5477
+ if (!reg) return null;
5478
+ const backend = reg.getSingleton("settings-store");
5479
+ if (!backend) return null;
5480
+ if (!backend.ensureTable || !backend.tableInsert) return null;
5481
+ return new BackupScheduleService(backend);
5482
+ }
5215
5483
  resolvePolicyService() {
5216
5484
  const reg = this.ctx.kernel.capabilityRegistry;
5217
5485
  if (!reg) return null;