@blamejs/core 0.12.22 → 0.12.23
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 +74 -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.23 (2026-05-24) — **`bundleAdapterStorage.cloneBundle(src, dst, opts?)` — same-storage byte-verbatim bundle clone for pre-rotation snapshots.** `storage.cloneBundle(srcBundleId, dstBundleId, opts?)` copies a bundle's adapter payload (bundle.tar / bundle.tar.gz / every directory key) from src to dst WITHOUT touching the envelope or inner archive. Encrypted bundles are cloned byte-verbatim — the new bundleId carries the same envelope under the same recipient/passphrase. Operators preserving a known-good snapshot before a destructive operation (rewrap, key rotation, schema migration, manual operator-side editing) get a single-call atomic clone instead of a manual readBundle → writeBundle cycle (which would re-encode through the envelope and adapter contracts, breaking byte-identity). **Added:** *`storage.cloneBundle(src, dst, opts?)` — byte-verbatim payload clone* — Reads the source bundle's storage keys + writes them under the destination bundleId without invoking the wrap layer, gunzip path, or tar walker. Encrypted bundles produce byte-identical clones (a tar.gz wrap-recipient envelope cloned via cloneBundle has bit-for-bit equal bytes to the source). Returns `{ srcBundleId, dstBundleId, format, keysCopied, bytesCopied }`. `opts.overwrite` (default false) gates whether to refuse if dstBundleId already exists. Same-id clones refused upfront with `backup/clone-same-id`.
|
|
12
|
+
|
|
11
13
|
- 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
14
|
|
|
13
15
|
- 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.
|
package/lib/backup/index.js
CHANGED
|
@@ -1541,6 +1541,80 @@ function bundleAdapterStorage(opts) {
|
|
|
1541
1541
|
// bytesRewritten }`. Refuses cross-kind rotation (recipient ↔
|
|
1542
1542
|
// passphrase) — that's a separate migration the operator
|
|
1543
1543
|
// configures explicitly.
|
|
1544
|
+
// cloneBundle(srcBundleId, dstBundleId, opts?) — v0.12.23
|
|
1545
|
+
// same-storage bundle clone. Copies the bundle's adapter
|
|
1546
|
+
// payload (bundle.tar / bundle.tar.gz / every directory key)
|
|
1547
|
+
// from src to dst WITHOUT touching the envelope or inner
|
|
1548
|
+
// archive. Useful before destructive operations (rewrap, key
|
|
1549
|
+
// rotation, schema migration) to preserve a known-good
|
|
1550
|
+
// snapshot under a distinct bundleId. opts.overwrite (default
|
|
1551
|
+
// false) gates whether to refuse if dstBundleId already
|
|
1552
|
+
// exists in storage.
|
|
1553
|
+
//
|
|
1554
|
+
// Returns `{ srcBundleId, dstBundleId, format, keysCopied,
|
|
1555
|
+
// bytesCopied }`.
|
|
1556
|
+
async cloneBundle(srcBundleId, dstBundleId, cloneOpts) {
|
|
1557
|
+
cloneOpts = cloneOpts || {};
|
|
1558
|
+
_ensureBundleId(srcBundleId);
|
|
1559
|
+
_ensureBundleId(dstBundleId);
|
|
1560
|
+
if (srcBundleId === dstBundleId) {
|
|
1561
|
+
throw new BackupError("backup/clone-same-id",
|
|
1562
|
+
"cloneBundle: srcBundleId === dstBundleId — refusing same-id clone");
|
|
1563
|
+
}
|
|
1564
|
+
var info = await this.bundleInfo(srcBundleId);
|
|
1565
|
+
var dstAlreadyExists = await this.hasBundle(dstBundleId);
|
|
1566
|
+
if (cloneOpts.overwrite !== true && dstAlreadyExists) {
|
|
1567
|
+
throw new BackupError("backup/clone-dst-exists",
|
|
1568
|
+
"cloneBundle: dstBundleId '" + dstBundleId + "' already exists; " +
|
|
1569
|
+
"pass opts.overwrite=true to replace");
|
|
1570
|
+
}
|
|
1571
|
+
// Codex P1 on v0.12.23 PR #174 — when overwrite=true, the
|
|
1572
|
+
// existence guard was the only protection but it didn't
|
|
1573
|
+
// delete the destination's existing keys before writing
|
|
1574
|
+
// the source keys. Stale-format bundles (dst=tar, src=
|
|
1575
|
+
// directory) ended up with BOTH the old tar key and the
|
|
1576
|
+
// new directory keys present — bundleInfo / readBundle
|
|
1577
|
+
// would resolve to the (still-existing) tar payload,
|
|
1578
|
+
// disagreeing with the reported clone result. Same applies
|
|
1579
|
+
// for directory→directory clones where the destination
|
|
1580
|
+
// had extra files not in source.
|
|
1581
|
+
//
|
|
1582
|
+
// Fix: when overwrite is enabled + the destination
|
|
1583
|
+
// already exists, call deleteBundle(dst) first to purge
|
|
1584
|
+
// every key under the destination's prefix so clone
|
|
1585
|
+
// semantics are actually replace-bytes.
|
|
1586
|
+
if (dstAlreadyExists && cloneOpts.overwrite === true) {
|
|
1587
|
+
await this.deleteBundle(dstBundleId);
|
|
1588
|
+
}
|
|
1589
|
+
var keysCopied = 0;
|
|
1590
|
+
var bytesCopied = 0;
|
|
1591
|
+
if (info.format === "tar" || info.format === "tar.gz") {
|
|
1592
|
+
var suffix = info.format === "tar.gz" ? TAR_GZ_KEY_SUFFIX : TAR_KEY_SUFFIX;
|
|
1593
|
+
var payload = await adapter.readFile(srcBundleId + suffix);
|
|
1594
|
+
await adapter.writeFile(dstBundleId + suffix, payload);
|
|
1595
|
+
keysCopied += 1;
|
|
1596
|
+
bytesCopied += payload.length;
|
|
1597
|
+
} else {
|
|
1598
|
+
// Directory format — walk every key under the source
|
|
1599
|
+
// bundleId + replicate under the destination prefix.
|
|
1600
|
+
var srcKeys = await adapter.listKeys(srcBundleId + "/");
|
|
1601
|
+
for (var ki = 0; ki < srcKeys.length; ki += 1) {
|
|
1602
|
+
var srcKey = srcKeys[ki];
|
|
1603
|
+
var rel = srcKey.slice((srcBundleId + "/").length);
|
|
1604
|
+
var keyBytes = await adapter.readFile(srcKey);
|
|
1605
|
+
await adapter.writeFile(dstBundleId + "/" + rel, keyBytes);
|
|
1606
|
+
keysCopied += 1;
|
|
1607
|
+
bytesCopied += keyBytes.length;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return {
|
|
1611
|
+
srcBundleId: srcBundleId,
|
|
1612
|
+
dstBundleId: dstBundleId,
|
|
1613
|
+
format: info.format,
|
|
1614
|
+
keysCopied: keysCopied,
|
|
1615
|
+
bytesCopied: bytesCopied,
|
|
1616
|
+
};
|
|
1617
|
+
},
|
|
1544
1618
|
async rewrapBundle(bundleId, rwOpts) {
|
|
1545
1619
|
rwOpts = rwOpts || {};
|
|
1546
1620
|
_ensureBundleId(bundleId);
|
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:913ec1d4-c11d-49fe-922b-3738c93f2aad",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-24T08:
|
|
8
|
+
"timestamp": "2026-05-24T08:56:20.519Z",
|
|
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.23",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.23",
|
|
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.23",
|
|
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.23",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|