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