@camstack/addon-remote-storage 1.2.76 → 1.2.77

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.
package/dist/s3.addon.js CHANGED
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-DlUg7bET.js");
5
+ const require_shared = require("./shared-DW6PfRB-.js");
6
6
  let node_stream = require("node:stream");
7
7
  let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
8
8
  let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
package/dist/s3.addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { c as BaseAddon, i as rearmIdleAbort, n as getOptionalBasePath, o as scheduleIdleAbort, s as storageProviderCapability, t as createSessionId } from "./shared-Ch1dJcLf.mjs";
1
+ import { c as BaseAddon, i as rearmIdleAbort, n as getOptionalBasePath, o as scheduleIdleAbort, s as storageProviderCapability, t as createSessionId } from "./shared-NUBj0w5N.mjs";
2
2
  import { PassThrough } from "node:stream";
3
3
  import { DeleteObjectCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
4
4
  import { Upload } from "@aws-sdk/lib-storage";
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-DlUg7bET.js");
5
+ const require_shared = require("./shared-DW6PfRB-.js");
6
6
  let ssh2 = require("ssh2");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_shared.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-Ch1dJcLf.mjs";
1
+ import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-NUBj0w5N.mjs";
2
2
  import { Client } from "ssh2";
3
3
  import * as path from "node:path";
4
4
  //#region src/providers/sftp/sftp-config-schema.ts
@@ -10557,12 +10557,30 @@ var BackupDestinationInfoSchema = object({
10557
10557
  lastSuccessAt: number().optional(),
10558
10558
  /** Newest-archive size from `manifests.json`, or undefined. */
10559
10559
  lastSuccessSizeBytes: number().optional(),
10560
- /** Per-destination cron expression. Empty = manual-only (no schedule). */
10560
+ /**
10561
+ * Cron cadence(s) of the ENABLED schedules that fan out to this
10562
+ * destination, comma-joined. Absent when no enabled schedule targets it
10563
+ * — a destination nothing is scheduled to write to must not advertise a
10564
+ * cadence (D384). This is never the `backup_destination_policies.cron`
10565
+ * column: that per-location cron has scheduled nothing since 2026-07-28
10566
+ * and reading it made a destination with a DISABLED schedule claim a
10567
+ * nightly run.
10568
+ */
10561
10569
  cron: string().optional(),
10562
- /** ms-epoch of next computed firing for this destination's cron, if any. */
10570
+ /** ms-epoch of the next firing across those schedules (earliest), if any. */
10563
10571
  nextRunAt: number().optional(),
10564
- /** ms-epoch of last successful scheduled run (mirrors policy.lastRunAt). */
10565
- lastRunAt: number().optional()
10572
+ /**
10573
+ * ms-epoch of the last time a run ATTEMPTED to write here — success or
10574
+ * failure. Never a success stamp: pair it with `lastSuccessAt` (the
10575
+ * newest archive that actually landed) and `lastError`.
10576
+ */
10577
+ lastAttemptAt: number().optional(),
10578
+ /**
10579
+ * Why the last attempt failed, verbatim. Absent when the last attempt
10580
+ * landed the archive. A destination that has never been written to has
10581
+ * neither this nor `lastAttemptAt`.
10582
+ */
10583
+ lastError: string().optional()
10566
10584
  });
10567
10585
  /**
10568
10586
  * Per-archive entry returned by `backup.listArchives({ destinationId })`.
@@ -10725,8 +10743,16 @@ var BackupScheduleSchema = object({
10725
10743
  retentionCount: number().int().min(1).max(1e3),
10726
10744
  /** Optional subset of source locations to include; omitted = all. */
10727
10745
  dataSources: array(string()).readonly().optional(),
10728
- /** ms-epoch of last successful run. */
10729
- lastRunAt: number().optional(),
10746
+ /**
10747
+ * ms-epoch of the last tick that FIRED this schedule. Stamped before the
10748
+ * archive runs (it is the dedupe anchor), so it says "attempted", never
10749
+ * "succeeded" — a run refused by every destination stamps it too.
10750
+ */
10751
+ lastAttemptAt: number().optional(),
10752
+ /** ms-epoch of the last run of this schedule that landed at ≥1 destination. */
10753
+ lastSuccessAt: number().optional(),
10754
+ /** Why the last fired run failed, verbatim. Absent when it succeeded. */
10755
+ lastError: string().optional(),
10730
10756
  /** ms-epoch of next computed firing (read-only, filled on list). */
10731
10757
  nextRunAt: number().optional()
10732
10758
  });
@@ -10775,14 +10801,7 @@ method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }
10775
10801
  locationId: string(),
10776
10802
  enabled: boolean(),
10777
10803
  retentionCount: number().int().min(1).max(1e3),
10778
- label: string().optional(),
10779
- /**
10780
- * Per-destination cron expression. Empty string clears the
10781
- * schedule (manual-only). Validated server-side via croner;
10782
- * malformed expressions reject the upsert with an actionable
10783
- * message.
10784
- */
10785
- cron: string().optional()
10804
+ label: string().optional()
10786
10805
  }), _void(), {
10787
10806
  kind: "mutation",
10788
10807
  auth: "admin"
@@ -10534,12 +10534,30 @@ var BackupDestinationInfoSchema = object({
10534
10534
  lastSuccessAt: number().optional(),
10535
10535
  /** Newest-archive size from `manifests.json`, or undefined. */
10536
10536
  lastSuccessSizeBytes: number().optional(),
10537
- /** Per-destination cron expression. Empty = manual-only (no schedule). */
10537
+ /**
10538
+ * Cron cadence(s) of the ENABLED schedules that fan out to this
10539
+ * destination, comma-joined. Absent when no enabled schedule targets it
10540
+ * — a destination nothing is scheduled to write to must not advertise a
10541
+ * cadence (D384). This is never the `backup_destination_policies.cron`
10542
+ * column: that per-location cron has scheduled nothing since 2026-07-28
10543
+ * and reading it made a destination with a DISABLED schedule claim a
10544
+ * nightly run.
10545
+ */
10538
10546
  cron: string().optional(),
10539
- /** ms-epoch of next computed firing for this destination's cron, if any. */
10547
+ /** ms-epoch of the next firing across those schedules (earliest), if any. */
10540
10548
  nextRunAt: number().optional(),
10541
- /** ms-epoch of last successful scheduled run (mirrors policy.lastRunAt). */
10542
- lastRunAt: number().optional()
10549
+ /**
10550
+ * ms-epoch of the last time a run ATTEMPTED to write here — success or
10551
+ * failure. Never a success stamp: pair it with `lastSuccessAt` (the
10552
+ * newest archive that actually landed) and `lastError`.
10553
+ */
10554
+ lastAttemptAt: number().optional(),
10555
+ /**
10556
+ * Why the last attempt failed, verbatim. Absent when the last attempt
10557
+ * landed the archive. A destination that has never been written to has
10558
+ * neither this nor `lastAttemptAt`.
10559
+ */
10560
+ lastError: string().optional()
10543
10561
  });
10544
10562
  /**
10545
10563
  * Per-archive entry returned by `backup.listArchives({ destinationId })`.
@@ -10702,8 +10720,16 @@ var BackupScheduleSchema = object({
10702
10720
  retentionCount: number().int().min(1).max(1e3),
10703
10721
  /** Optional subset of source locations to include; omitted = all. */
10704
10722
  dataSources: array(string()).readonly().optional(),
10705
- /** ms-epoch of last successful run. */
10706
- lastRunAt: number().optional(),
10723
+ /**
10724
+ * ms-epoch of the last tick that FIRED this schedule. Stamped before the
10725
+ * archive runs (it is the dedupe anchor), so it says "attempted", never
10726
+ * "succeeded" — a run refused by every destination stamps it too.
10727
+ */
10728
+ lastAttemptAt: number().optional(),
10729
+ /** ms-epoch of the last run of this schedule that landed at ≥1 destination. */
10730
+ lastSuccessAt: number().optional(),
10731
+ /** Why the last fired run failed, verbatim. Absent when it succeeded. */
10732
+ lastError: string().optional(),
10707
10733
  /** ms-epoch of next computed firing (read-only, filled on list). */
10708
10734
  nextRunAt: number().optional()
10709
10735
  });
@@ -10752,14 +10778,7 @@ method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }
10752
10778
  locationId: string(),
10753
10779
  enabled: boolean(),
10754
10780
  retentionCount: number().int().min(1).max(1e3),
10755
- label: string().optional(),
10756
- /**
10757
- * Per-destination cron expression. Empty string clears the
10758
- * schedule (manual-only). Validated server-side via croner;
10759
- * malformed expressions reject the upsert with an actionable
10760
- * message.
10761
- */
10762
- cron: string().optional()
10781
+ label: string().optional()
10763
10782
  }), _void(), {
10764
10783
  kind: "mutation",
10765
10784
  auth: "admin"
package/dist/smb.addon.js CHANGED
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-DlUg7bET.js");
5
+ const require_shared = require("./shared-DW6PfRB-.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_shared.__toESM(node_path);
@@ -225,6 +225,28 @@ function shareIdentity(input) {
225
225
  function pathSegments(remotePath) {
226
226
  return remotePath.split("/").filter((s) => s.length > 0);
227
227
  }
228
+ /**
229
+ * Turn a directory path into the MASK `smbclient dir` needs to enumerate it.
230
+ *
231
+ * `samba-client`'s `list(x)` runs `smbclient -c 'dir x'`, and smbclient's
232
+ * `dir` argument is a wildcard mask evaluated against the current directory —
233
+ * it does NOT descend into a directory named by it. A bare directory path
234
+ * therefore returns that directory's own entry, and an empty string returns
235
+ * nothing at all, because an empty mask matches nothing.
236
+ *
237
+ * Measured on the hub, 2026-09-07: `backups:smb` held a 387 MB archive and a
238
+ * `manifests.json`; `storage.list` answered `[]` and `storage.exists`
239
+ * answered `false`, so every SMB upload was refused by its own post-transfer
240
+ * "did it land?" check even though the bytes were intact on the share. The
241
+ * unit suite was green because its fake client treated the argument as a
242
+ * directory — the fake supplied the semantics production assumed and
243
+ * smbclient does not have.
244
+ *
245
+ * @param dir `''` for the connection's basePath, else a relative directory.
246
+ */
247
+ function listMaskFor(dir) {
248
+ return dir === "" ? "*" : `${dir}/*`;
249
+ }
228
250
  //#endregion
229
251
  //#region src/providers/smb/smb-provider.ts
230
252
  /**
@@ -484,7 +506,7 @@ var SmbStorageProvider = class {
484
506
  const client = await this.clientForLocation(location);
485
507
  const dir = safeSmbRelativePath(prefix ?? "");
486
508
  try {
487
- return (await client.list(dir)).filter((e) => e.name !== "." && e.name !== "..").map((e) => dir === "" ? e.name : `${dir}/${e.name}`);
509
+ return (await client.list(listMaskFor(dir))).filter((e) => e.name !== "." && e.name !== "..").map((e) => dir === "" ? e.name : `${dir}/${e.name}`);
488
510
  } catch (err) {
489
511
  if (isNotFound(err)) return [];
490
512
  this.logger.warn("smb-storage: list failed", { meta: {
@@ -682,14 +704,19 @@ var SmbStorageProvider = class {
682
704
  ...cfg.basePath !== "" ? { directory: cfg.basePath } : {}
683
705
  });
684
706
  }
685
- /** One entry of a remote listing, or `null` when the path does not exist. */
707
+ /**
708
+ * One entry of a remote listing, or `null` when the path does not exist.
709
+ *
710
+ * Goes through {@link listMaskFor} — a bare directory path is NOT a
711
+ * listable argument for smbclient.
712
+ */
686
713
  async statRemote(location, relativePath) {
687
714
  const client = await this.clientForLocation(location);
688
715
  const target = safeSmbRelativePath(relativePath);
689
716
  const dir = target.includes("/") ? target.slice(0, target.lastIndexOf("/")) : "";
690
717
  const name = target.includes("/") ? target.slice(target.lastIndexOf("/") + 1) : target;
691
718
  try {
692
- return (await client.list(dir)).find((e) => e.name.toLowerCase() === name.toLowerCase()) ?? null;
719
+ return (await client.list(listMaskFor(dir))).find((e) => e.name.toLowerCase() === name.toLowerCase()) ?? null;
693
720
  } catch (err) {
694
721
  if (isNotFound(err)) return null;
695
722
  throw err;
@@ -1,4 +1,4 @@
1
- import { c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, s as storageProviderCapability } from "./shared-Ch1dJcLf.mjs";
1
+ import { c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, s as storageProviderCapability } from "./shared-NUBj0w5N.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as path from "node:path";
4
4
  import * as fsp from "node:fs/promises";
@@ -218,6 +218,28 @@ function shareIdentity(input) {
218
218
  function pathSegments(remotePath) {
219
219
  return remotePath.split("/").filter((s) => s.length > 0);
220
220
  }
221
+ /**
222
+ * Turn a directory path into the MASK `smbclient dir` needs to enumerate it.
223
+ *
224
+ * `samba-client`'s `list(x)` runs `smbclient -c 'dir x'`, and smbclient's
225
+ * `dir` argument is a wildcard mask evaluated against the current directory —
226
+ * it does NOT descend into a directory named by it. A bare directory path
227
+ * therefore returns that directory's own entry, and an empty string returns
228
+ * nothing at all, because an empty mask matches nothing.
229
+ *
230
+ * Measured on the hub, 2026-09-07: `backups:smb` held a 387 MB archive and a
231
+ * `manifests.json`; `storage.list` answered `[]` and `storage.exists`
232
+ * answered `false`, so every SMB upload was refused by its own post-transfer
233
+ * "did it land?" check even though the bytes were intact on the share. The
234
+ * unit suite was green because its fake client treated the argument as a
235
+ * directory — the fake supplied the semantics production assumed and
236
+ * smbclient does not have.
237
+ *
238
+ * @param dir `''` for the connection's basePath, else a relative directory.
239
+ */
240
+ function listMaskFor(dir) {
241
+ return dir === "" ? "*" : `${dir}/*`;
242
+ }
221
243
  //#endregion
222
244
  //#region src/providers/smb/smb-provider.ts
223
245
  /**
@@ -477,7 +499,7 @@ var SmbStorageProvider = class {
477
499
  const client = await this.clientForLocation(location);
478
500
  const dir = safeSmbRelativePath(prefix ?? "");
479
501
  try {
480
- return (await client.list(dir)).filter((e) => e.name !== "." && e.name !== "..").map((e) => dir === "" ? e.name : `${dir}/${e.name}`);
502
+ return (await client.list(listMaskFor(dir))).filter((e) => e.name !== "." && e.name !== "..").map((e) => dir === "" ? e.name : `${dir}/${e.name}`);
481
503
  } catch (err) {
482
504
  if (isNotFound(err)) return [];
483
505
  this.logger.warn("smb-storage: list failed", { meta: {
@@ -675,14 +697,19 @@ var SmbStorageProvider = class {
675
697
  ...cfg.basePath !== "" ? { directory: cfg.basePath } : {}
676
698
  });
677
699
  }
678
- /** One entry of a remote listing, or `null` when the path does not exist. */
700
+ /**
701
+ * One entry of a remote listing, or `null` when the path does not exist.
702
+ *
703
+ * Goes through {@link listMaskFor} — a bare directory path is NOT a
704
+ * listable argument for smbclient.
705
+ */
679
706
  async statRemote(location, relativePath) {
680
707
  const client = await this.clientForLocation(location);
681
708
  const target = safeSmbRelativePath(relativePath);
682
709
  const dir = target.includes("/") ? target.slice(0, target.lastIndexOf("/")) : "";
683
710
  const name = target.includes("/") ? target.slice(target.lastIndexOf("/") + 1) : target;
684
711
  try {
685
- return (await client.list(dir)).find((e) => e.name.toLowerCase() === name.toLowerCase()) ?? null;
712
+ return (await client.list(listMaskFor(dir))).find((e) => e.name.toLowerCase() === name.toLowerCase()) ?? null;
686
713
  } catch (err) {
687
714
  if (isNotFound(err)) return null;
688
715
  throw err;
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-DlUg7bET.js");
5
+ const require_shared = require("./shared-DW6PfRB-.js");
6
6
  let node_stream = require("node:stream");
7
7
  let webdav = require("webdav");
8
8
  //#region src/providers/webdav/webdav-config-schema.ts
@@ -1,4 +1,4 @@
1
- import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-Ch1dJcLf.mjs";
1
+ import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-NUBj0w5N.mjs";
2
2
  import { PassThrough } from "node:stream";
3
3
  import { AuthType, createClient } from "webdav";
4
4
  //#region src/providers/webdav/webdav-config-schema.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-remote-storage",
3
- "version": "1.2.76",
3
+ "version": "1.2.77",
4
4
  "description": "Remote storage providers (SFTP, S3, WebDAV, SMB) — unifies remote backends behind the storage-provider cap",
5
5
  "keywords": [
6
6
  "camstack",