@blamejs/core 0.12.17 → 0.12.19

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/CHANGELOG.md CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.12.x
10
10
 
11
+ - v0.12.19 (2026-05-24) — **`bundleAdapterStorage.verifyBundle(bundleId)` — bundle integrity check without restore + `b.safeArchive.inspect` tar.gz support.** `storage.verifyBundle(bundleId, opts?)` walks a bundle without restoring it: confirms the payload exists, the envelope (if any) decrypts under the supplied key, and the inner tar / tar.gz walker enumerates every entry without writing to disk. Returns `{ ok, format, envelopeKind, entryCount, errors }` — operators wanting periodic health-check of a backup repository call verifyBundle across `listBundles()` and aggregate `ok === false` results. Composes the bundle's known format directly through the unwrap → gunzip → tar pipeline (skips safeArchive's auto-sniff because the bundle's format is already known from bundleInfo). `b.safeArchive.inspect` separately gains `format: "tar.gz"` dispatch — operators with a known tar.gz payload now get entry enumeration without extracting. **Added:** *`storage.verifyBundle(bundleId, opts?)` — integrity check without restore* — Composes bundleInfo for format/envelope detection + the unwrap → gunzip → tar walker chain directly. opts.recipient / opts.passphrase override the storage's configured keys (useful for verifying a bundle under a different key set than the storage was opened with — e.g. verifying that a key rotation candidate can still read a bundle). Wrong key / corrupted payload returns `ok: false` with a typed error code in the errors array rather than throwing. Directory format reports ok=true based on manifest existence + readability (the inspect walker doesn't apply). · *`b.safeArchive.inspect` accepts `format: "tar.gz"`* — Mirrors the v0.12.9 extract surface: operators handed a `.tar.gz` payload can now enumerate entries without extracting. Composes `b.archive.read.gz` + `.asTar()` internally so the same bomb caps apply (1 GiB output / 100× ratio default) to inspect calls.
12
+
13
+ - v0.12.18 (2026-05-24) — **`bundleAdapterStorage.listBundles({ withStats })` + `bundleInfo.createdAt` — opt-in mtime + size from `statKey`.** Two additions on `b.backup.bundleAdapterStorage`. `listBundles({ withStats: true })` fans out `statKey` per bundle and populates `createdAt` (ISO string from mtimeMs) + `size` (bytes) on every entry. Without the opt the default fast path stays a single listKeys call — operators rendering a bundle picker UI choose between O(1) listings and O(N) stat-enriched listings explicitly. `bundleInfo(bundleId)` gains the same `createdAt` field for parity. fsAdapter + objectStoreAdapter both expose `statKey`; legacy adapters without the capability leave the fields null. **Added:** *`storage.listBundles({ withStats: true })` — opt-in per-bundle stat fan-out* — When the adapter exposes `statKey`, populates `createdAt` (ISO string from mtimeMs) + `size` (bytes) per entry. Stat fan-out is O(N) round-trips so the opt is OFF by default — operators wanting cheap one-shot listings stay on `listBundles()`. Format precedence (tar.gz > tar > directory) carries through to which payload key gets stat'd. · *`storage.bundleInfo(bundleId)` returns `createdAt`* — The bundle introspection primitive now returns `{ bundleId, format, envelopeKind, sizeBytes, createdAt }`. `createdAt` is the ISO string derived from `statKey.mtimeMs` (when the adapter exposes it) — null otherwise. Matches the listBundles+withStats shape so operators can use bundleInfo for single-bundle drill-downs without re-mapping field names.
14
+
11
15
  - v0.12.17 (2026-05-23) — **`bundleAdapterStorage.bundleInfo` + `listBundles.format` — per-bundle introspection for envelope kind + format without restore.** Two introspection additions on `b.backup.bundleAdapterStorage`. `listBundles()` now returns the inferred `format` (`"tar"` / `"tar.gz"` / `"directory"`) per bundle from the storage key suffix — no byte read. `storage.bundleInfo(bundleId)` returns `{ bundleId, format, envelopeKind, sizeBytes }` where `envelopeKind` is the result of a 5-byte magic probe (`"recipient"` / `"passphrase"` / `"none"`). Operators administering a multi-strategy backup repository can now filter bundles by encryption posture or by format without a full restore cycle. **Added:** *`listBundles()` carries inferred format per bundle* — Each entry now includes `format` alongside `bundleId` / `createdAt` / `size`. Inference is from the storage key suffix the writeBundle path produced — `<bid>/bundle.tar` → tar, `<bid>/bundle.tar.gz` → tar.gz, anything else → directory. Cheap: no byte read, no per-key stat call. Operators rendering a bundle picker UI now sort + filter by format from a single list call. · *`storage.bundleInfo(bundleId)` — per-bundle introspection* — Returns `{ bundleId, format, envelopeKind, sizeBytes }`. `format` from the storage layout (no byte read). `envelopeKind` from `b.archive.sniffEnvelope` over the bundle payload — `"recipient"` (BAWRP / v0.12.10 hybrid PQC), `"passphrase"` (BAWPP / v0.12.11 Argon2id), `"none"` (plaintext or directory format). `sizeBytes` is the payload byte count for tar / tar.gz; null for directory format (operator's per-file walk applies if exact size matters). Nonexistent bundles refused with `backup/bundle-not-found`.
12
16
 
13
17
  - v0.12.16 (2026-05-23) — **`b.safeArchive.inspect` auto-unwraps wrap envelopes (parallel to the v0.12.15 extract path).** Mirrors the v0.12.15 auto-unwrap support into `b.safeArchive.inspect`. Operators enumerating entries of a sealed archive get a single inspect() call regardless of envelope shape — pass `opts.recipient` or `opts.passphrase` alongside `source` and the orchestrator unwraps inline before walking the inner format. Missing-key opt surfaces a structured `safe-archive/no-recipient-for-wrap` / `safe-archive/no-passphrase-for-wrap` refusal upfront. Carries the v0.12.15 P1 + P2 fixes (close original source before replacing + forward opts.signal to inner buffer adapter) into the inspect path so the same descriptor-leak + abort-propagation contracts hold. **Added:** *`b.safeArchive.inspect` auto-unwraps `BAWRP` + `BAWPP` envelopes* — The orchestrator's `format: "auto"` sniffer recognises the wrap magics and routes through `b.archive.unwrap` / `b.archive.unwrapWithPassphrase` inline. After unwrap, the inner bytes are wrapped in a buffer adapter + re-sniffed; the resulting summary carries the INNER `format` (`"tar"` / `"zip"` / etc.) — operators querying `summary.format` see the carrier format, not `"wrap-recipient"`. Entry enumeration walks the inner archive after a single key-derivation pass; no temporary file lands on disk.
@@ -1365,8 +1365,10 @@ function bundleAdapterStorage(opts) {
1365
1365
  nodeFs.writeFileSync(destPath, bytes, { flag: "wx", mode: 0o600 });
1366
1366
  }
1367
1367
  },
1368
- async listBundles() {
1368
+ async listBundles(listOpts) {
1369
1369
  // Get every key, partition by bundleId prefix, return sorted.
1370
+ listOpts = listOpts || {};
1371
+ var withStats = listOpts.withStats === true;
1370
1372
  // v0.12.17 — each bundle now carries the inferred format
1371
1373
  // (tar / tar.gz / directory) so operators picking which
1372
1374
  // bundle to restore can filter by format without touching
@@ -1408,16 +1410,128 @@ function bundleAdapterStorage(opts) {
1408
1410
  if (statsJ.hasTarGz) fmtJ = "tar.gz"; // matches readBundle precedence
1409
1411
  else if (statsJ.hasTar) fmtJ = "tar";
1410
1412
  else fmtJ = "directory";
1411
- out.push({
1413
+ var entry = {
1412
1414
  bundleId: bidJ,
1413
1415
  format: fmtJ,
1414
1416
  createdAt: null, // adapter may not expose mtime
1415
1417
  size: null, // best-effort; operators with stat-fast adapters call bundleInfo
1416
- });
1418
+ };
1419
+ // v0.12.18 — when opts.withStats is true AND the adapter
1420
+ // exposes statKey, fan-out a stat call per bundle's
1421
+ // payload key. O(N) round-trips so this is opt-in;
1422
+ // listBundles() with no opts stays cheap (single listKeys
1423
+ // call). Operators wanting per-bundle stats but not the
1424
+ // full bundleInfo envelope probe pick this middle ground.
1425
+ if (withStats && typeof adapter.statKey === "function") {
1426
+ var statKey;
1427
+ if (statsJ.hasTarGz) statKey = bidJ + TAR_GZ_KEY_SUFFIX;
1428
+ else if (statsJ.hasTar) statKey = bidJ + TAR_KEY_SUFFIX;
1429
+ else statKey = bidJ + "/manifest.json";
1430
+ var sk = await adapter.statKey(statKey);
1431
+ if (sk && typeof sk.size === "number") entry.size = sk.size;
1432
+ if (sk && typeof sk.mtimeMs === "number") entry.createdAt = new Date(sk.mtimeMs).toISOString();
1433
+ }
1434
+ out.push(entry);
1417
1435
  }
1418
1436
  out.sort(function (a, b) { return a.bundleId < b.bundleId ? 1 : -1; });
1419
1437
  return out;
1420
1438
  },
1439
+ // verifyBundle(bundleId, opts?) — v0.12.19 integrity check.
1440
+ // Walks the bundle without restoring: confirms the payload
1441
+ // exists, the envelope (if any) decrypts under the supplied
1442
+ // key, and the inner archive structure is well-formed (every
1443
+ // entry enumerable). Returns `{ ok, format, envelopeKind,
1444
+ // entryCount, errors }`. opts.recipient / opts.passphrase
1445
+ // forwarded when the bundle is wrap-wrapped; omit them for
1446
+ // plaintext bundles. Composes safeArchive.inspect which gates
1447
+ // entries through bomb-policy + entry-type-policy walkers, so
1448
+ // a malformed archive surfaces with a typed error rather than
1449
+ // crashing the verify call.
1450
+ async verifyBundle(bundleId, vOpts) {
1451
+ vOpts = vOpts || {};
1452
+ _ensureBundleId(bundleId);
1453
+ var info;
1454
+ try { info = await this.bundleInfo(bundleId); }
1455
+ catch (e) {
1456
+ return {
1457
+ ok: false,
1458
+ format: null,
1459
+ envelopeKind: null,
1460
+ entryCount: 0,
1461
+ errors: [e.code || e.message || String(e)],
1462
+ };
1463
+ }
1464
+ if (info.format === "directory") {
1465
+ // Directory format isn't archive-shaped — manifest.json
1466
+ // existence + readability via bundleInfo IS the
1467
+ // verification. No inspect walk applies.
1468
+ return {
1469
+ ok: true,
1470
+ format: info.format,
1471
+ envelopeKind: info.envelopeKind,
1472
+ entryCount: null,
1473
+ errors: [],
1474
+ };
1475
+ }
1476
+ // Compose the reader chain directly so we don't depend on
1477
+ // safeArchive's auto-sniff dispatch (which is conservative
1478
+ // about inferring "tar.gz" from gzip-magic-only inner bytes).
1479
+ // We already know the bundle's format + envelopeKind from
1480
+ // bundleInfo — apply the layers in order: unwrap (if any)
1481
+ // → gunzip (if tar.gz) → tar walker.
1482
+ var keySuffix = info.format === "tar.gz" ? TAR_GZ_KEY_SUFFIX : TAR_KEY_SUFFIX;
1483
+ var payload = await adapter.readFile(bundleId + keySuffix);
1484
+ try {
1485
+ // Layer 1 — unwrap envelope if present.
1486
+ if (info.envelopeKind === "recipient") {
1487
+ var rcp = vOpts.recipient !== undefined ? vOpts.recipient : recipient;
1488
+ if (!rcp) {
1489
+ return {
1490
+ ok: false, format: info.format, envelopeKind: info.envelopeKind,
1491
+ entryCount: 0, errors: ["backup/no-recipient-for-verify"],
1492
+ };
1493
+ }
1494
+ payload = archiveLazy().unwrap(payload, { recipient: rcp });
1495
+ } else if (info.envelopeKind === "passphrase") {
1496
+ var pp = vOpts.passphrase !== undefined ? vOpts.passphrase : passphrase;
1497
+ if (typeof pp !== "string" && !Buffer.isBuffer(pp)) {
1498
+ return {
1499
+ ok: false, format: info.format, envelopeKind: info.envelopeKind,
1500
+ entryCount: 0, errors: ["backup/no-passphrase-for-verify"],
1501
+ };
1502
+ }
1503
+ payload = await archiveLazy().unwrapWithPassphrase(payload, { passphrase: pp });
1504
+ }
1505
+ // Layer 2 — gunzip if tar.gz.
1506
+ var tarReader;
1507
+ if (info.format === "tar.gz") {
1508
+ var gzReader = archiveLazy().read.gz(archiveAdaptersLazy().buffer(payload), {
1509
+ maxDecompressedBytes: maxBundleBytes,
1510
+ maxExpansionRatio: 0,
1511
+ });
1512
+ tarReader = gzReader.asTar();
1513
+ } else {
1514
+ tarReader = archiveLazy().read.tar(archiveAdaptersLazy().buffer(payload));
1515
+ }
1516
+ // Layer 3 — walk the tar entries (inspect doesn't extract).
1517
+ var entries = await tarReader.inspect();
1518
+ return {
1519
+ ok: true,
1520
+ format: info.format,
1521
+ envelopeKind: info.envelopeKind,
1522
+ entryCount: entries.length,
1523
+ errors: [],
1524
+ };
1525
+ } catch (e) {
1526
+ return {
1527
+ ok: false,
1528
+ format: info.format,
1529
+ envelopeKind: info.envelopeKind,
1530
+ entryCount: 0,
1531
+ errors: [e.code || e.message || String(e)],
1532
+ };
1533
+ }
1534
+ },
1421
1535
  // bundleInfo(bundleId) — v0.12.17 per-bundle introspection.
1422
1536
  // Returns `{ bundleId, format, envelopeKind, sizeBytes }`.
1423
1537
  // `format` is one of `"tar"` / `"tar.gz"` / `"directory"`
@@ -1450,6 +1564,28 @@ function bundleAdapterStorage(opts) {
1450
1564
  }
1451
1565
  var envelopeKind = "none";
1452
1566
  var sizeBytes = null;
1567
+ var createdAt = null;
1568
+ // Codex P2 on v0.12.18 PR #169 — directory-format bundles
1569
+ // leave payloadKey null but DO have a manifest.json that
1570
+ // statKey can read. For createdAt parity with
1571
+ // listBundles({ withStats }), stat the manifest in the
1572
+ // directory case so the bundleInfo return shape is
1573
+ // populated identically across formats.
1574
+ if (payloadKey === null && fmt === "directory" &&
1575
+ typeof adapter.statKey === "function") {
1576
+ // Stat the manifest.json so directory-format bundles
1577
+ // populate createdAt + sizeBytes identically to how
1578
+ // listBundles({ withStats }) reports them. NOTE: sizeBytes
1579
+ // is the manifest's size here, not the total file-tree
1580
+ // payload (operators wanting the true total walk
1581
+ // per-file keys themselves) — same convention as
1582
+ // listBundles({ withStats }) for parity.
1583
+ var dirSt = await adapter.statKey(manifestKey);
1584
+ if (dirSt && typeof dirSt.size === "number") sizeBytes = dirSt.size;
1585
+ if (dirSt && typeof dirSt.mtimeMs === "number") {
1586
+ createdAt = new Date(dirSt.mtimeMs).toISOString();
1587
+ }
1588
+ }
1453
1589
  if (payloadKey !== null) {
1454
1590
  // Codex P1 on v0.12.17 PR #168 — claim was a 5-byte magic
1455
1591
  // probe; the implementation was reading the entire bundle
@@ -1478,6 +1614,7 @@ function bundleAdapterStorage(opts) {
1478
1614
  if (typeof adapter.statKey === "function") {
1479
1615
  var st = await adapter.statKey(payloadKey);
1480
1616
  if (st && typeof st.size === "number") sizeBytes = st.size;
1617
+ if (st && typeof st.mtimeMs === "number") createdAt = new Date(st.mtimeMs).toISOString();
1481
1618
  }
1482
1619
  }
1483
1620
  return {
@@ -1485,6 +1622,7 @@ function bundleAdapterStorage(opts) {
1485
1622
  format: fmt,
1486
1623
  envelopeKind: envelopeKind,
1487
1624
  sizeBytes: sizeBytes,
1625
+ createdAt: createdAt,
1488
1626
  };
1489
1627
  },
1490
1628
  async deleteBundle(bundleId) {
@@ -373,9 +373,21 @@ async function inspect(opts) {
373
373
  bombPolicy: opts.bombPolicy,
374
374
  audit: opts.audit,
375
375
  });
376
+ } else if (format === "tar.gz") {
377
+ // v0.12.19 — inspect parity with extract for tar.gz format.
378
+ // gz envelope auto-decompresses + the inner tar walker
379
+ // enumerates entries without writing to disk.
380
+ reader = archiveGz().read.gz(source, {
381
+ maxDecompressedBytes: opts.maxDecompressedBytes,
382
+ maxExpansionRatio: opts.maxExpansionRatio,
383
+ audit: opts.audit,
384
+ }).asTar({
385
+ bombPolicy: opts.bombPolicy,
386
+ audit: opts.audit,
387
+ });
376
388
  } else {
377
389
  throw new SafeArchiveError("safe-archive/format-unsupported",
378
- "inspect: format=" + JSON.stringify(format) + " — v0.12.8 ships ZIP + tar; v0.12.16 auto-unwraps wrap envelopes");
390
+ "inspect: format=" + JSON.stringify(format) + " — v0.12.19 ships ZIP + tar + tar.gz; auto-unwraps wrap envelopes");
379
391
  }
380
392
  var entries = await reader.inspect();
381
393
  var totalCompressed = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.12.17",
3
+ "version": "0.12.19",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:5b052d03-601b-4eb1-93d8-60aaaf81b2f7",
5
+ "serialNumber": "urn:uuid:4b94a721-bd76-436d-8d96-e205956b6d90",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-24T04:43:58.102Z",
8
+ "timestamp": "2026-05-24T05:39:53.422Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.12.17",
22
+ "bom-ref": "@blamejs/core@0.12.19",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.12.17",
25
+ "version": "0.12.19",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.12.17",
29
+ "purl": "pkg:npm/%40blamejs/core@0.12.19",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.12.17",
57
+ "ref": "@blamejs/core@0.12.19",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]