@blamejs/core 0.12.21 → 0.12.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/backup/index.js +107 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.12.x
|
|
10
10
|
|
|
11
|
+
- v0.12.22 (2026-05-24) — **`bundleAdapterStorage.rewrapAllBundles(opts)` — bounded-parallel batch envelope rotation with mixed-storage skip semantics.** Batch wrapper over the v0.12.21 rewrapBundle primitive. `storage.rewrapAllBundles(opts?)` iterates `listBundles()` + rotates each bundle's wrap envelope through a bounded-parallel pool (default 4 workers). Plaintext bundles + directory-format bundles get skipped cleanly (recorded as `status: "skipped"` with a `reason` field); rewrap failures get bucketed into `status: "failed"`. Operators completing a key-rotation event across an entire backup repository now have a single call that handles mixed-strategy storage correctly. `opts.newRecipient` / `opts.newPassphrase` / `opts.oldRecipient` / `opts.oldPassphrase` / `opts.concurrency` / `opts.stopOnFirstFailure` mirror the verifyAllBundles + rewrapBundle surface. **Added:** *`storage.rewrapAllBundles(opts?)` — batch envelope rotation* — Iterates listBundles() + dispatches each bundle through rewrapBundle with the operator-supplied new key. Returns `{ total, rotated, skipped, failed, results }` where the per-bundle results carry `{ status: "rotated" | "skipped" | "failed", oldEnvelopeKind, newEnvelopeKind, reason }`. Bounded-parallel fan-out (default 4) keeps the storage backend under control; opts.stopOnFirstFailure short-circuits on the first rotation that throws an unexpected error (skips don't trip the short-circuit — they're expected for mixed-strategy storage). Plaintext + directory bundles skipped with `reason: "format-not-wrappable"` / `reason: "no-envelope"` rather than reported as failures.
|
|
12
|
+
|
|
11
13
|
- v0.12.21 (2026-05-24) — **`bundleAdapterStorage.rewrapBundle(bundleId, opts)` — key rotation without restore + rewrite of inner archive bytes.** `storage.rewrapBundle(bundleId, opts?)` rotates a bundle's wrap envelope under a new recipient keypair or passphrase WITHOUT touching the inner tar / tar.gz bytes. Operators rotating a compromised keypair, migrating to a new HSM, or refreshing passphrases on a HIPAA-posture repository previously had to `readBundle` → write to a stage dir → `writeBundle` under the new key — three byte-walks of the bundle payload, two filesystem touches, transient plaintext on disk. rewrapBundle does it as unwrap + rewrap in memory: zero disk plaintext, one round-trip through the wrap layer, the gzipped tar archive bytes inside the envelope are never inflated. Cross-kind rotation (recipient ↔ passphrase) is refused — that's a separate migration the operator configures with explicit cryptoStrategy switch. **Added:** *`storage.rewrapBundle(bundleId, opts?)` — in-place envelope rotation* — Unwraps the bundle under the old key (storage's configured recipient/passphrase OR `opts.oldRecipient` / `opts.oldPassphrase`), re-wraps under the new key (`opts.newRecipient` / `opts.newPassphrase`), writes the rewrapped bytes back to the same storage key. Returns `{ bundleId, oldEnvelopeKind, newEnvelopeKind, bytesRewritten }`. Plaintext bundles refused with `backup/no-envelope-to-rewrap`; cross-kind rotation refused with `backup/no-new-recipient` / `backup/no-new-passphrase`. The inner archive bytes (the gz-compressed tar payload) are never decompressed or re-encoded — rewrap is a wrap-layer-only operation. **Security:** *Zero plaintext on disk during rotation* — The inner tar bytes flow only through memory — old-envelope unwrap → new-envelope wrap → adapter writeFile. Operators previously rotating via readBundle + writeBundle wrote plaintext archive bytes to a temporary stage directory; rewrapBundle removes that exposure window entirely. Matches the operator-side discipline that backup payloads should never land on disk in plaintext form during steady-state operations.
|
|
12
14
|
|
|
13
15
|
- v0.12.20 (2026-05-24) — **`bundleAdapterStorage.verifyAllBundles(opts)` — bounded-parallel batch integrity walk with stopOnFirstFailure short-circuit.** Batch wrapper over the v0.12.19 verifyBundle primitive. `storage.verifyAllBundles(opts?)` iterates `listBundles()` + walks each bundle with a bounded-parallel pool. Returns `{ total, ok, failed, results }` where `results` carries every per-bundle verifyBundle output (including bundleId, format, envelopeKind, entryCount, errors). `opts.concurrency` defaults to 4 (gentle on the storage backend); `opts.stopOnFirstFailure` short-circuits the walk when an unhealthy bundle is found (default off — operators want the full health report). `opts.recipient` / `opts.passphrase` forwarded to every per-bundle verify call. Operators wiring a periodic cron job over a backup repository now have a single primitive to call instead of hand-rolling the listBundles loop. **Added:** *`storage.verifyAllBundles(opts?)` — batch integrity walk* — Iterates `listBundles()` + calls `verifyBundle(bundleId, opts)` on each. Bounded-parallel fan-out (default 4 workers; `opts.concurrency` raises or lowers); each worker pulls from a shared queue + the warm-up keeps `concurrency` workers in flight until the queue drains. Returns the aggregate `{ total, ok, failed, results }` with `results` sorted by bundleId so the report is stable across runs regardless of completion order. `opts.stopOnFirstFailure` short-circuits the walk when the first unhealthy bundle is found — useful for fast-fail CI gates that don't need to enumerate every failure.
|
package/lib/backup/index.js
CHANGED
|
@@ -1640,6 +1640,113 @@ function bundleAdapterStorage(opts) {
|
|
|
1640
1640
|
// unhealthy bundle is found (default false — operators want
|
|
1641
1641
|
// the full report). opts.recipient / opts.passphrase forwarded
|
|
1642
1642
|
// to verifyBundle for each bundle.
|
|
1643
|
+
// rewrapAllBundles(opts) — v0.12.22 batch wrapper over the
|
|
1644
|
+
// v0.12.21 rewrapBundle primitive. Iterates listBundles() +
|
|
1645
|
+
// rewraps each through a bounded-parallel pool, skipping
|
|
1646
|
+
// plaintext / directory bundles cleanly. Returns
|
|
1647
|
+
// `{ total, rotated, skipped, failed, results }` where the
|
|
1648
|
+
// results array carries per-bundle `{ status: "rotated" |
|
|
1649
|
+
// "skipped" | "failed", ... }`. opts.concurrency /
|
|
1650
|
+
// opts.stopOnFirstFailure mirror verifyAllBundles.
|
|
1651
|
+
// opts.newRecipient / opts.newPassphrase /
|
|
1652
|
+
// opts.oldRecipient / opts.oldPassphrase forwarded to each
|
|
1653
|
+
// per-bundle rewrap.
|
|
1654
|
+
async rewrapAllBundles(opts) {
|
|
1655
|
+
opts = opts || {};
|
|
1656
|
+
var concurrency = 4; // allow:raw-byte-literal — default fan-out, not byte count
|
|
1657
|
+
if (typeof opts.concurrency === "number" && Number.isFinite(opts.concurrency) &&
|
|
1658
|
+
opts.concurrency > 0) {
|
|
1659
|
+
concurrency = Math.max(1, Math.floor(opts.concurrency));
|
|
1660
|
+
}
|
|
1661
|
+
var stopOnFirst = opts.stopOnFirstFailure === true;
|
|
1662
|
+
var list = await this.listBundles();
|
|
1663
|
+
var self = this;
|
|
1664
|
+
var results = [];
|
|
1665
|
+
var rotated = 0;
|
|
1666
|
+
var skipped = 0;
|
|
1667
|
+
var failed = 0;
|
|
1668
|
+
var pending = list.slice();
|
|
1669
|
+
var inflight = [];
|
|
1670
|
+
var aborted = false;
|
|
1671
|
+
function _spawn() {
|
|
1672
|
+
// Codex P1 on v0.12.22 PR #173 — synchronously drain
|
|
1673
|
+
// non-wrappable entries inside _spawn until we hit one
|
|
1674
|
+
// that actually needs an async rewrap (or the pending
|
|
1675
|
+
// queue empties). The prior implementation returned
|
|
1676
|
+
// Promise.resolve() for skipped entries without adding
|
|
1677
|
+
// to inflight; if the first `concurrency` items were
|
|
1678
|
+
// all directory bundles, the warm-up drained pending
|
|
1679
|
+
// into the skipped bucket without spawning any inflight
|
|
1680
|
+
// workers + the drain loop exited immediately, leaving
|
|
1681
|
+
// the rest of the queue unprocessed.
|
|
1682
|
+
while (!aborted && pending.length > 0) {
|
|
1683
|
+
var entry = pending.shift();
|
|
1684
|
+
if (entry.format !== "tar" && entry.format !== "tar.gz") {
|
|
1685
|
+
results.push({
|
|
1686
|
+
bundleId: entry.bundleId,
|
|
1687
|
+
status: "skipped",
|
|
1688
|
+
reason: "format-not-wrappable",
|
|
1689
|
+
oldEnvelopeKind: null,
|
|
1690
|
+
newEnvelopeKind: null,
|
|
1691
|
+
});
|
|
1692
|
+
skipped += 1;
|
|
1693
|
+
continue; // try the next pending entry
|
|
1694
|
+
}
|
|
1695
|
+
return _spawnRewrap(entry);
|
|
1696
|
+
}
|
|
1697
|
+
return null;
|
|
1698
|
+
}
|
|
1699
|
+
function _spawnRewrap(entry) {
|
|
1700
|
+
var p = self.rewrapBundle(entry.bundleId, opts).then(function (r) {
|
|
1701
|
+
results.push(Object.assign({ status: "rotated" }, r));
|
|
1702
|
+
rotated += 1;
|
|
1703
|
+
}, function (err) {
|
|
1704
|
+
var code = (err && err.code) || (err && err.message) || String(err);
|
|
1705
|
+
if (/no-envelope-to-rewrap/.test(code)) {
|
|
1706
|
+
results.push({
|
|
1707
|
+
bundleId: entry.bundleId,
|
|
1708
|
+
status: "skipped",
|
|
1709
|
+
reason: "no-envelope",
|
|
1710
|
+
oldEnvelopeKind: null,
|
|
1711
|
+
newEnvelopeKind: null,
|
|
1712
|
+
});
|
|
1713
|
+
skipped += 1;
|
|
1714
|
+
} else {
|
|
1715
|
+
results.push({
|
|
1716
|
+
bundleId: entry.bundleId,
|
|
1717
|
+
status: "failed",
|
|
1718
|
+
reason: code,
|
|
1719
|
+
oldEnvelopeKind: null,
|
|
1720
|
+
newEnvelopeKind: null,
|
|
1721
|
+
});
|
|
1722
|
+
failed += 1;
|
|
1723
|
+
if (stopOnFirst) aborted = true;
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
inflight.push(p);
|
|
1727
|
+
p.finally(function () {
|
|
1728
|
+
var idx = inflight.indexOf(p);
|
|
1729
|
+
if (idx !== -1) inflight.splice(idx, 1);
|
|
1730
|
+
});
|
|
1731
|
+
return p;
|
|
1732
|
+
}
|
|
1733
|
+
var ri;
|
|
1734
|
+
for (ri = 0; ri < concurrency; ri += 1) _spawn();
|
|
1735
|
+
while (inflight.length > 0) {
|
|
1736
|
+
await Promise.race(inflight.slice());
|
|
1737
|
+
if (!aborted && pending.length > 0 && inflight.length < concurrency) {
|
|
1738
|
+
while (inflight.length < concurrency && pending.length > 0) _spawn();
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
results.sort(function (a, b) { return a.bundleId < b.bundleId ? 1 : -1; });
|
|
1742
|
+
return {
|
|
1743
|
+
total: list.length,
|
|
1744
|
+
rotated: rotated,
|
|
1745
|
+
skipped: skipped,
|
|
1746
|
+
failed: failed,
|
|
1747
|
+
results: results,
|
|
1748
|
+
};
|
|
1749
|
+
},
|
|
1643
1750
|
async verifyAllBundles(vOpts) {
|
|
1644
1751
|
vOpts = vOpts || {};
|
|
1645
1752
|
// Codex P1 on v0.12.20 PR #171 — clamp fractional + zero
|
package/package.json
CHANGED
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:
|
|
5
|
+
"serialNumber": "urn:uuid:aa0703c8-0954-482c-af7e-d813018096cf",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-24T08:01:37.093Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.12.22",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.22",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.12.22",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.12.22",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|