@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.
- package/CHANGELOG.md +2 -0
- package/lib/analytics.js +1 -1
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +16 -0
- package/lib/vendor/blamejs/api-snapshot.json +6 -2
- package/lib/vendor/blamejs/lib/archive-wrap.js +58 -0
- package/lib/vendor/blamejs/lib/archive.js +1 -0
- package/lib/vendor/blamejs/lib/backup/index.js +585 -10
- package/lib/vendor/blamejs/lib/safe-archive.js +112 -3
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.13.json +31 -0
- package/lib/vendor/blamejs/release-notes/v0.12.14.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.15.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.12.16.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.17.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.12.18.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.12.19.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.12.20.json +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-sniff-envelope.test.js +118 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-bundle-info.test.js +279 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-object-store-adapter.test.js +167 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-verify-all-bundles.test.js +0 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-verify-bundle.test.js +186 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +28 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-archive-auto-unwrap.test.js +116 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-archive-inspect-unwrap.test.js +89 -0
- 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,
|
|
1395
|
+
stats = { count: 0, hasTar: false, hasTarGz: false, hasOther: false };
|
|
1381
1396
|
byBundle.set(bid, stats);
|
|
1382
1397
|
}
|
|
1383
1398
|
stats.count += 1;
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1396
|
-
|
|
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
|
|