@blamejs/blamejs-shop 0.0.113 → 0.0.114

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 (27) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/lib/analytics.js +1 -1
  3. package/lib/vendor/MANIFEST.json +2 -2
  4. package/lib/vendor/blamejs/CHANGELOG.md +16 -0
  5. package/lib/vendor/blamejs/api-snapshot.json +6 -2
  6. package/lib/vendor/blamejs/lib/archive-wrap.js +58 -0
  7. package/lib/vendor/blamejs/lib/archive.js +1 -0
  8. package/lib/vendor/blamejs/lib/backup/index.js +585 -10
  9. package/lib/vendor/blamejs/lib/safe-archive.js +112 -3
  10. package/lib/vendor/blamejs/package.json +1 -1
  11. package/lib/vendor/blamejs/release-notes/v0.12.13.json +31 -0
  12. package/lib/vendor/blamejs/release-notes/v0.12.14.json +18 -0
  13. package/lib/vendor/blamejs/release-notes/v0.12.15.json +27 -0
  14. package/lib/vendor/blamejs/release-notes/v0.12.16.json +18 -0
  15. package/lib/vendor/blamejs/release-notes/v0.12.17.json +22 -0
  16. package/lib/vendor/blamejs/release-notes/v0.12.18.json +22 -0
  17. package/lib/vendor/blamejs/release-notes/v0.12.19.json +22 -0
  18. package/lib/vendor/blamejs/release-notes/v0.12.20.json +18 -0
  19. package/lib/vendor/blamejs/test/layer-0-primitives/archive-sniff-envelope.test.js +118 -0
  20. package/lib/vendor/blamejs/test/layer-0-primitives/backup-bundle-info.test.js +279 -0
  21. package/lib/vendor/blamejs/test/layer-0-primitives/backup-object-store-adapter.test.js +167 -0
  22. package/lib/vendor/blamejs/test/layer-0-primitives/backup-verify-all-bundles.test.js +0 -0
  23. package/lib/vendor/blamejs/test/layer-0-primitives/backup-verify-bundle.test.js +186 -0
  24. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +28 -0
  25. package/lib/vendor/blamejs/test/layer-0-primitives/safe-archive-auto-unwrap.test.js +116 -0
  26. package/lib/vendor/blamejs/test/layer-0-primitives/safe-archive-inspect-unwrap.test.js +89 -0
  27. package/package.json +1 -1
@@ -1365,8 +1365,23 @@ 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;
1372
+ // v0.12.17 — each bundle now carries the inferred format
1373
+ // (tar / tar.gz / directory) so operators picking which
1374
+ // bundle to restore can filter by format without touching
1375
+ // bytes. Format is inferred from the key suffix the
1376
+ // writeBundle path produced (rule §2 — the format is part
1377
+ // of the storage layout, not behind a probe).
1378
+ //
1379
+ // Codex P2 on v0.12.17 PR #168 — track WHICH suffixes a
1380
+ // bundle carries (set of booleans) then apply explicit
1381
+ // precedence at the end: tar.gz > tar > directory. Matches
1382
+ // readBundle's preference (which checks hasTarGz first)
1383
+ // so listBundles' reported format aligns with restore
1384
+ // behavior regardless of adapter.listKeys() order.
1370
1385
  var allKeys = await adapter.listKeys("");
1371
1386
  var byBundle = new Map();
1372
1387
  for (var i = 0; i < allKeys.length; i += 1) {
@@ -1377,28 +1392,334 @@ function bundleAdapterStorage(opts) {
1377
1392
  if (!_isValidBundleId(bid)) continue;
1378
1393
  var stats = byBundle.get(bid);
1379
1394
  if (!stats) {
1380
- stats = { count: 0, size: 0 };
1395
+ stats = { count: 0, hasTar: false, hasTarGz: false, hasOther: false };
1381
1396
  byBundle.set(bid, stats);
1382
1397
  }
1383
1398
  stats.count += 1;
1384
- // Note: size is approximate — we'd need a stat-per-key here,
1385
- // and many adapter implementations don't expose a fast stat
1386
- // primitive. listBundles is best-effort; operators wanting
1387
- // exact size do their own walk.
1399
+ var rest = key.slice(slash + 1);
1400
+ if (rest === "bundle.tar") stats.hasTar = true;
1401
+ else if (rest === "bundle.tar.gz") stats.hasTarGz = true;
1402
+ else stats.hasOther = true;
1388
1403
  }
1389
1404
  var out = [];
1390
1405
  var entries = Array.from(byBundle.entries());
1391
1406
  for (var j = 0; j < entries.length; j += 1) {
1392
1407
  var bidJ = entries[j][0];
1393
- out.push({
1408
+ var statsJ = entries[j][1];
1409
+ var fmtJ;
1410
+ if (statsJ.hasTarGz) fmtJ = "tar.gz"; // matches readBundle precedence
1411
+ else if (statsJ.hasTar) fmtJ = "tar";
1412
+ else fmtJ = "directory";
1413
+ var entry = {
1394
1414
  bundleId: bidJ,
1395
- createdAt: null, // adapter may not expose mtime
1396
- size: null, // best-effort
1397
- });
1415
+ format: fmtJ,
1416
+ createdAt: null, // adapter may not expose mtime
1417
+ size: null, // best-effort; operators with stat-fast adapters call bundleInfo
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);
1398
1435
  }
1399
1436
  out.sort(function (a, b) { return a.bundleId < b.bundleId ? 1 : -1; });
1400
1437
  return out;
1401
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
+ },
1535
+ // verifyAllBundles(opts?) — v0.12.20 batch integrity check.
1536
+ // Iterates listBundles() + calls verifyBundle on each. Returns
1537
+ // `{ total, ok, failed, results }` where `results` is an array
1538
+ // of per-bundle verifyBundle outputs. opts.concurrency caps the
1539
+ // parallelism (default 4 — gentle on the storage backend);
1540
+ // opts.stopOnFirstFailure short-circuits the walk when an
1541
+ // unhealthy bundle is found (default false — operators want
1542
+ // the full report). opts.recipient / opts.passphrase forwarded
1543
+ // to verifyBundle for each bundle.
1544
+ async verifyAllBundles(vOpts) {
1545
+ vOpts = vOpts || {};
1546
+ // Codex P1 on v0.12.20 PR #171 — clamp fractional + zero
1547
+ // floors so a stray `0.5` doesn't spawn zero workers + return
1548
+ // a silent ok=0/failed=0 report on non-empty storage. Default
1549
+ // 4; minimum 1; non-finite / non-positive falls back to
1550
+ // default.
1551
+ var concurrency = 4; // allow:raw-byte-literal — default fan-out, not byte count
1552
+ if (typeof vOpts.concurrency === "number" && Number.isFinite(vOpts.concurrency) &&
1553
+ vOpts.concurrency > 0) {
1554
+ concurrency = Math.max(1, Math.floor(vOpts.concurrency));
1555
+ }
1556
+ var stopOnFirst = vOpts.stopOnFirstFailure === true;
1557
+ var list = await this.listBundles();
1558
+ var self = this;
1559
+ var results = [];
1560
+ var failed = 0;
1561
+ var ok = 0;
1562
+ var pending = list.slice();
1563
+ var inflight = [];
1564
+ var aborted = false;
1565
+ // Sequential bounded-parallel walk. Bring up `concurrency`
1566
+ // workers; each pulls the next bundleId until the queue is
1567
+ // empty or stopOnFirstFailure trips.
1568
+ function _spawn() {
1569
+ if (aborted) return null;
1570
+ if (pending.length === 0) return null;
1571
+ var entry = pending.shift();
1572
+ // Codex P2 on v0.12.20 PR #171 — wrap each worker so any
1573
+ // verifyBundle rejection becomes a failed-result entry
1574
+ // rather than rejecting the whole batch. Without this, a
1575
+ // mid-walk failure (payload disappeared between listBundles
1576
+ // + readFile, network blip on object-store, etc.) would
1577
+ // throw out of Promise.race and abort verifyAllBundles
1578
+ // without returning the promised aggregate report.
1579
+ var promise = self.verifyBundle(entry.bundleId, {
1580
+ recipient: vOpts.recipient,
1581
+ passphrase: vOpts.passphrase,
1582
+ }).then(function (r) {
1583
+ results.push(Object.assign({ bundleId: entry.bundleId }, r));
1584
+ if (r.ok) ok += 1;
1585
+ else {
1586
+ failed += 1;
1587
+ if (stopOnFirst) aborted = true;
1588
+ }
1589
+ }, function (err) {
1590
+ // Rejection path — convert to a failed-result entry so
1591
+ // the aggregate stays consistent.
1592
+ results.push({
1593
+ bundleId: entry.bundleId,
1594
+ ok: false,
1595
+ format: null,
1596
+ envelopeKind: null,
1597
+ entryCount: 0,
1598
+ errors: [err && err.code ? err.code : ((err && err.message) || String(err))],
1599
+ });
1600
+ failed += 1;
1601
+ if (stopOnFirst) aborted = true;
1602
+ });
1603
+ inflight.push(promise);
1604
+ promise.finally(function () {
1605
+ var idx = inflight.indexOf(promise);
1606
+ if (idx !== -1) inflight.splice(idx, 1);
1607
+ });
1608
+ return promise;
1609
+ }
1610
+ // Warm-up: spawn up to concurrency workers.
1611
+ var i;
1612
+ for (i = 0; i < concurrency; i += 1) _spawn();
1613
+ // Drain: as each worker resolves, spawn its replacement.
1614
+ while (inflight.length > 0) {
1615
+ await Promise.race(inflight.slice());
1616
+ if (!aborted && pending.length > 0 && inflight.length < concurrency) {
1617
+ while (inflight.length < concurrency && pending.length > 0) _spawn();
1618
+ }
1619
+ }
1620
+ // Sort results back to listBundles order for operator-
1621
+ // friendly output (concurrency reorders inflight completion).
1622
+ results.sort(function (a, b) { return a.bundleId < b.bundleId ? 1 : -1; });
1623
+ return {
1624
+ total: list.length,
1625
+ ok: ok,
1626
+ failed: failed,
1627
+ results: results,
1628
+ };
1629
+ },
1630
+ // bundleInfo(bundleId) — v0.12.17 per-bundle introspection.
1631
+ // Returns `{ bundleId, format, envelopeKind, sizeBytes }`.
1632
+ // `format` is one of `"tar"` / `"tar.gz"` / `"directory"`
1633
+ // inferred from the storage layout (no byte read).
1634
+ // `envelopeKind` is the result of a 5-byte magic probe on the
1635
+ // bundle payload — `"recipient"` (BAWRP) / `"passphrase"`
1636
+ // (BAWPP) / `"none"` (plaintext). `sizeBytes` is the payload
1637
+ // byte count for tar / tar.gz; null for directory format
1638
+ // (operator's per-file walk if exact size matters).
1639
+ //
1640
+ // Instance method — wiki page documents this under the
1641
+ // bundleAdapterStorage primitive rather than as a top-level
1642
+ // b.X primitive.
1643
+ async bundleInfo(bundleId) {
1644
+ _ensureBundleId(bundleId);
1645
+ var tarKey = bundleId + TAR_KEY_SUFFIX;
1646
+ var tarGzKey = bundleId + TAR_GZ_KEY_SUFFIX;
1647
+ var manifestKey = bundleId + "/manifest.json";
1648
+ var fmt = null;
1649
+ var payloadKey = null;
1650
+ if (await adapter.hasKey(tarGzKey)) {
1651
+ fmt = "tar.gz"; payloadKey = tarGzKey;
1652
+ } else if (await adapter.hasKey(tarKey)) {
1653
+ fmt = "tar"; payloadKey = tarKey;
1654
+ } else if (await adapter.hasKey(manifestKey)) {
1655
+ fmt = "directory";
1656
+ } else {
1657
+ throw new BackupError("backup/bundle-not-found",
1658
+ "bundleInfo: '" + bundleId + "' not in storage");
1659
+ }
1660
+ var envelopeKind = "none";
1661
+ var sizeBytes = null;
1662
+ var createdAt = null;
1663
+ // Codex P2 on v0.12.18 PR #169 — directory-format bundles
1664
+ // leave payloadKey null but DO have a manifest.json that
1665
+ // statKey can read. For createdAt parity with
1666
+ // listBundles({ withStats }), stat the manifest in the
1667
+ // directory case so the bundleInfo return shape is
1668
+ // populated identically across formats.
1669
+ if (payloadKey === null && fmt === "directory" &&
1670
+ typeof adapter.statKey === "function") {
1671
+ // Stat the manifest.json so directory-format bundles
1672
+ // populate createdAt + sizeBytes identically to how
1673
+ // listBundles({ withStats }) reports them. NOTE: sizeBytes
1674
+ // is the manifest's size here, not the total file-tree
1675
+ // payload (operators wanting the true total walk
1676
+ // per-file keys themselves) — same convention as
1677
+ // listBundles({ withStats }) for parity.
1678
+ var dirSt = await adapter.statKey(manifestKey);
1679
+ if (dirSt && typeof dirSt.size === "number") sizeBytes = dirSt.size;
1680
+ if (dirSt && typeof dirSt.mtimeMs === "number") {
1681
+ createdAt = new Date(dirSt.mtimeMs).toISOString();
1682
+ }
1683
+ }
1684
+ if (payloadKey !== null) {
1685
+ // Codex P1 on v0.12.17 PR #168 — claim was a 5-byte magic
1686
+ // probe; the implementation was reading the entire bundle
1687
+ // into memory. For multi-GB bundles, an administrative
1688
+ // metadata call would allocate the whole payload and put
1689
+ // memory pressure on the host. Prefer the adapter's
1690
+ // optional `readPartial(key, length)` capability for the
1691
+ // probe. fsAdapter + objectStoreAdapter both expose it as
1692
+ // of v0.12.17; legacy adapters without it fall back to a
1693
+ // capped 16-byte readFile via the fallback path (still
1694
+ // bounded; better than full payload).
1695
+ if (typeof adapter.readPartial === "function") {
1696
+ var probe = await adapter.readPartial(payloadKey, 16); // allow:raw-byte-literal — 16-byte probe head, magic comparison
1697
+ envelopeKind = archiveLazy().sniffEnvelope(probe);
1698
+ } else {
1699
+ // Legacy adapter — readPartial missing. Operators using
1700
+ // a custom adapter without the capability get
1701
+ // envelopeKind: "unknown" rather than an OOM risk. They
1702
+ // can probe themselves by reading the first N bytes via
1703
+ // their own client.
1704
+ envelopeKind = "unknown";
1705
+ }
1706
+ // sizeBytes is reported via a stat-like path when the
1707
+ // adapter exposes one; otherwise stays null. fsAdapter +
1708
+ // objectStoreAdapter expose `statKey`.
1709
+ if (typeof adapter.statKey === "function") {
1710
+ var st = await adapter.statKey(payloadKey);
1711
+ if (st && typeof st.size === "number") sizeBytes = st.size;
1712
+ if (st && typeof st.mtimeMs === "number") createdAt = new Date(st.mtimeMs).toISOString();
1713
+ }
1714
+ }
1715
+ return {
1716
+ bundleId: bundleId,
1717
+ format: fmt,
1718
+ envelopeKind: envelopeKind,
1719
+ sizeBytes: sizeBytes,
1720
+ createdAt: createdAt,
1721
+ };
1722
+ },
1402
1723
  async deleteBundle(bundleId) {
1403
1724
  _ensureBundleId(bundleId);
1404
1725
  var keys = await adapter.listKeys(bundleId + "/");
@@ -1494,6 +1815,260 @@ bundleAdapterStorage.fsAdapter = function (fsOpts) {
1494
1815
  try { return nodeFs.existsSync(_keyPath(key)); }
1495
1816
  catch (_e) { return false; }
1496
1817
  },
1818
+ // v0.12.17 — optional capabilities consumed by bundleInfo.
1819
+ // readPartial: open + read up to `length` bytes from the start
1820
+ // of the file without materializing the whole payload.
1821
+ // Bundle-info's envelope probe needs at most 16 bytes — the
1822
+ // partial read keeps multi-GB bundle metadata cheap.
1823
+ async readPartial(key, length) {
1824
+ // CodeQL js/file-system-race + js/insecure-temporary-file —
1825
+ // drop the existsSync probe (TOCTOU) and the default mode on
1826
+ // open. Use openSync with explicit owner-only mode + handle
1827
+ // ENOENT atomically; the system call is itself the existence
1828
+ // check.
1829
+ var p = _keyPath(key);
1830
+ var fd;
1831
+ try {
1832
+ fd = nodeFs.openSync(p, "r", 0o600);
1833
+ } catch (e) {
1834
+ if (e && e.code === "ENOENT") {
1835
+ throw new BackupError("backup/no-key",
1836
+ "fsAdapter.readPartial: key not found: " + JSON.stringify(key));
1837
+ }
1838
+ throw e;
1839
+ }
1840
+ try {
1841
+ var buf = Buffer.alloc(length);
1842
+ var bytesRead = nodeFs.readSync(fd, buf, 0, length, 0);
1843
+ return buf.slice(0, bytesRead);
1844
+ } finally {
1845
+ try { nodeFs.closeSync(fd); } catch (_e) { /* drop-silent */ }
1846
+ }
1847
+ },
1848
+ async statKey(key) {
1849
+ var p = _keyPath(key);
1850
+ if (!nodeFs.existsSync(p)) return null;
1851
+ var st = nodeFs.statSync(p);
1852
+ return { size: st.size, mtimeMs: st.mtimeMs };
1853
+ },
1854
+ };
1855
+ };
1856
+
1857
+ // ---- v0.12.13: objectStoreAdapter ----------------------------------------
1858
+
1859
+ /**
1860
+ * @primitive b.backup.bundleAdapterStorage.objectStoreAdapter
1861
+ * @signature b.backup.bundleAdapterStorage.objectStoreAdapter(client, opts?)
1862
+ * @since 0.12.13
1863
+ * @status stable
1864
+ * @related b.backup.bundleAdapterStorage, b.objectStore
1865
+ *
1866
+ * Wraps a `b.objectStore`-shaped client into the
1867
+ * `{ writeFile, readFile, listKeys, deleteKey, hasKey }` adapter
1868
+ * contract that `bundleAdapterStorage` consumes. The client must
1869
+ * expose `put(key, body) → Promise<{ size }>`, `get(key) →
1870
+ * Promise<Buffer>`, `head(key) → Promise<{ size, ... }>`,
1871
+ * `delete(key) → Promise<boolean>`, and `list(prefix, opts?) →
1872
+ * Promise<{ items: [{ key, size, ... }], truncated }>` — the
1873
+ * shape produced by `b.objectStore.buildBackend({ protocol: ... })`
1874
+ * for the local / SigV4 / GCS / Azure-Blob backends.
1875
+ *
1876
+ * `opts.prefix` namespaces every key under a fixed root inside the
1877
+ * bucket — operators sharing a bucket across multiple deployments
1878
+ * pass distinct prefixes so listings stay scoped.
1879
+ *
1880
+ * `opts.list` is the operator-tunable `{ maxResults, ... }` pass-
1881
+ * through forwarded to the underlying `client.list` call (defaults
1882
+ * to whatever the backend's `list` defaults to — typically 1000).
1883
+ *
1884
+ * Closes the v0.12.10 deferral: "S3 / MinIO / Azure / GCS-backed
1885
+ * backups" promised since v0.11.2 JSDoc.
1886
+ *
1887
+ * @opts
1888
+ * prefix: string, // namespace every key under this prefix in the bucket
1889
+ * list: { maxResults: number }, // forwarded to client.list opts
1890
+ *
1891
+ * @example
1892
+ * var client = b.objectStore.buildBackend({
1893
+ * protocol: "local",
1894
+ * rootDir: "/var/backups",
1895
+ * });
1896
+ * var storage = b.backup.bundleAdapterStorage({
1897
+ * adapter: b.backup.bundleAdapterStorage.objectStoreAdapter(client),
1898
+ * format: "tar.gz",
1899
+ * cryptoStrategy: "recipient",
1900
+ * recipient: pair,
1901
+ * });
1902
+ * // bundle bytes hit the object-store backend's put(); restore
1903
+ * // path composes through unwrap + read.gz + read.tar.
1904
+ */
1905
+ bundleAdapterStorage.objectStoreAdapter = function (client, osOpts) {
1906
+ if (!client || typeof client !== "object") {
1907
+ throw new BackupError("backup/bad-adapter",
1908
+ "objectStoreAdapter: client is required (a b.objectStore-shaped object with put / get / head / delete / list)");
1909
+ }
1910
+ var required = ["put", "get", "head", "delete", "list"];
1911
+ for (var i = 0; i < required.length; i += 1) {
1912
+ if (typeof client[required[i]] !== "function") {
1913
+ throw new BackupError("backup/bad-adapter",
1914
+ "objectStoreAdapter: client missing method '" + required[i] + "'");
1915
+ }
1916
+ }
1917
+ osOpts = osOpts || {};
1918
+ var prefix = "";
1919
+ if (osOpts.prefix !== undefined && osOpts.prefix !== null) {
1920
+ if (typeof osOpts.prefix !== "string") {
1921
+ throw new BackupError("backup/bad-arg",
1922
+ "objectStoreAdapter: opts.prefix must be a string");
1923
+ }
1924
+ if (osOpts.prefix.indexOf("..") !== -1 || osOpts.prefix.indexOf("\u0000") !== -1) {
1925
+ throw new BackupError("backup/bad-arg",
1926
+ "objectStoreAdapter: opts.prefix contains traversal segment or NUL byte");
1927
+ }
1928
+ // Strip trailing slashes without a backtracking regex (CodeQL
1929
+ // js/polynomial-redos flagged `/\/+$/`, which is linear in
1930
+ // practice but flagged conservatively). Walk back from the end
1931
+ // and slice once.
1932
+ var endIdx = osOpts.prefix.length;
1933
+ while (endIdx > 0 && osOpts.prefix.charCodeAt(endIdx - 1) === 0x2f) endIdx -= 1; // 0x2f = "/"
1934
+ prefix = osOpts.prefix.slice(0, endIdx);
1935
+ if (prefix.length > 0) prefix += "/";
1936
+ }
1937
+ var listOpts = osOpts.list || {};
1938
+
1939
+ function _scopedKey(key) {
1940
+ if (typeof key !== "string" || key.length === 0) {
1941
+ throw new BackupError("backup/bad-key",
1942
+ "objectStoreAdapter: key must be a non-empty string");
1943
+ }
1944
+ if (key.indexOf("..") !== -1 || key.indexOf("\u0000") !== -1) {
1945
+ throw new BackupError("backup/bad-key",
1946
+ "objectStoreAdapter: key contains traversal segment or NUL byte");
1947
+ }
1948
+ return prefix + key;
1949
+ }
1950
+
1951
+ return {
1952
+ async writeFile(key, bytes) {
1953
+ if (!Buffer.isBuffer(bytes) && !(bytes instanceof Uint8Array)) {
1954
+ throw new BackupError("backup/bad-arg",
1955
+ "objectStoreAdapter.writeFile: bytes must be a Buffer or Uint8Array");
1956
+ }
1957
+ await client.put(_scopedKey(key), Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes));
1958
+ },
1959
+ async readFile(key) {
1960
+ var scoped = _scopedKey(key);
1961
+ try {
1962
+ var body = await client.get(scoped);
1963
+ return Buffer.isBuffer(body) ? body : Buffer.from(body);
1964
+ } catch (e) {
1965
+ // b.objectStore surfaces NOT_FOUND via the framework's
1966
+ // err.code === "NOT_FOUND" convention — translate to the
1967
+ // backup adapter contract's no-key error.
1968
+ if (e && (e.code === "NOT_FOUND" || /NOT_FOUND|not found/i.test(e.message || ""))) {
1969
+ throw new BackupError("backup/no-key",
1970
+ "objectStoreAdapter: key not found: " + JSON.stringify(key));
1971
+ }
1972
+ throw e;
1973
+ }
1974
+ },
1975
+ async listKeys(keyPrefix) {
1976
+ // _scopedKey rejects empty strings; for list(prefix) we want
1977
+ // to allow listing the whole bundle root. Compose the
1978
+ // prefix manually so `listKeys("")` enumerates everything
1979
+ // under the operator-supplied namespace.
1980
+ var realScoped = prefix + (keyPrefix || "");
1981
+ // Codex P1 on v0.12.13 PR #164 — object-store backends page
1982
+ // results (default 1000 keys). Without continuation, listKeys
1983
+ // silently dropped bundles past page 1 — listBundles missed
1984
+ // them, deleteBundle skipped them. Follow the
1985
+ // truncated / continuationToken contract until every page
1986
+ // is consumed. PAGINATION_CAP guards against a runaway
1987
+ // server returning truncated:true forever (defense-in-depth;
1988
+ // shipped backends honour the contract).
1989
+ var PAGINATION_CAP = 1000; // allow:raw-byte-literal — page count cap, not byte count
1990
+ var out = [];
1991
+ var token = null;
1992
+ var pages = 0;
1993
+ do {
1994
+ var pageOpts = Object.assign({}, listOpts);
1995
+ if (token) pageOpts.continuationToken = token;
1996
+ var result = await client.list(realScoped, pageOpts);
1997
+ var items = result && result.items ? result.items : (Array.isArray(result) ? result : []);
1998
+ for (var i = 0; i < items.length; i += 1) {
1999
+ var k = typeof items[i] === "string" ? items[i] : items[i].key;
2000
+ if (typeof k !== "string") continue;
2001
+ if (prefix.length > 0 && k.indexOf(prefix) === 0) {
2002
+ out.push(k.slice(prefix.length));
2003
+ } else {
2004
+ out.push(k);
2005
+ }
2006
+ }
2007
+ token = result && result.continuationToken ? result.continuationToken : null;
2008
+ if (!result || result.truncated !== true) break;
2009
+ if (!token) break; // truncated:true without continuationToken — stop to avoid spin
2010
+ pages += 1;
2011
+ if (pages > PAGINATION_CAP) {
2012
+ throw new BackupError("backup/list-pagination-runaway",
2013
+ "objectStoreAdapter.listKeys: backend returned >" + PAGINATION_CAP +
2014
+ " pages without exhausting; refusing to spin (operator should narrow the prefix or raise opts.list.maxResults)");
2015
+ }
2016
+ } while (true);
2017
+ return out;
2018
+ },
2019
+ async deleteKey(key) {
2020
+ try {
2021
+ await client.delete(_scopedKey(key));
2022
+ } catch (e) {
2023
+ // drop-silent on NOT_FOUND — adapter contract is idempotent
2024
+ // delete (fsAdapter same shape).
2025
+ if (e && (e.code === "NOT_FOUND" || /NOT_FOUND|not found/i.test(e.message || ""))) {
2026
+ return;
2027
+ }
2028
+ throw e;
2029
+ }
2030
+ },
2031
+ async hasKey(key) {
2032
+ try {
2033
+ await client.head(_scopedKey(key));
2034
+ return true;
2035
+ } catch (e) {
2036
+ if (e && (e.code === "NOT_FOUND" || /NOT_FOUND|not found/i.test(e.message || ""))) {
2037
+ return false;
2038
+ }
2039
+ throw e;
2040
+ }
2041
+ },
2042
+ // v0.12.17 — readPartial uses the b.objectStore client's range
2043
+ // capability (every shipped backend honours `{ range: [start,
2044
+ // end] }` per the client contract). bundleInfo's envelope probe
2045
+ // reads 16 bytes regardless of bundle size.
2046
+ async readPartial(key, length) {
2047
+ var scoped = _scopedKey(key);
2048
+ try {
2049
+ var body = await client.get(scoped, { range: [0, Math.max(0, length - 1)] });
2050
+ var buf = Buffer.isBuffer(body) ? body : Buffer.from(body);
2051
+ return buf.slice(0, length);
2052
+ } catch (e) {
2053
+ if (e && (e.code === "NOT_FOUND" || /NOT_FOUND|not found/i.test(e.message || ""))) {
2054
+ throw new BackupError("backup/no-key",
2055
+ "objectStoreAdapter.readPartial: key not found: " + JSON.stringify(key));
2056
+ }
2057
+ throw e;
2058
+ }
2059
+ },
2060
+ async statKey(key) {
2061
+ try {
2062
+ var meta = await client.head(_scopedKey(key));
2063
+ if (!meta || typeof meta.size !== "number") return null;
2064
+ return { size: meta.size, mtimeMs: meta.lastModified || null };
2065
+ } catch (e) {
2066
+ if (e && (e.code === "NOT_FOUND" || /NOT_FOUND|not found/i.test(e.message || ""))) {
2067
+ return null;
2068
+ }
2069
+ throw e;
2070
+ }
2071
+ },
1497
2072
  };
1498
2073
  };
1499
2074