@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
|
@@ -47,6 +47,7 @@ var archiveRead = lazyRequire(function () { return require("./archive-read"); })
|
|
|
47
47
|
var archiveAdapters = lazyRequire(function () { return require("./archive-adapters"); });
|
|
48
48
|
var archiveTarRead = lazyRequire(function () { return require("./archive-tar-read"); });
|
|
49
49
|
var archiveGz = lazyRequire(function () { return require("./archive-gz"); });
|
|
50
|
+
var archiveWrap = lazyRequire(function () { return require("./archive-wrap"); });
|
|
50
51
|
|
|
51
52
|
// ---- Format sniffing ----------------------------------------------------
|
|
52
53
|
|
|
@@ -93,6 +94,10 @@ async function _sniffMagic(adapter) {
|
|
|
93
94
|
if (head.length >= 5) {
|
|
94
95
|
var prefix = head.slice(0, 5).toString("utf8");
|
|
95
96
|
if (prefix === MAGIC_ENCPACKED) return { format: "encryptPacked" };
|
|
97
|
+
// v0.12.15 — archive-wrap recipient envelope (v0.12.10 / BAWRP).
|
|
98
|
+
if (prefix === "BAWRP") return { format: "wrap-recipient" };
|
|
99
|
+
// v0.12.15 — archive-wrap passphrase envelope (v0.12.11 / BAWPP).
|
|
100
|
+
if (prefix === "BAWPP") return { format: "wrap-passphrase" };
|
|
96
101
|
}
|
|
97
102
|
// tar — "ustar" at offset 257 within the first 512-byte header.
|
|
98
103
|
if (head.length >= 263) {
|
|
@@ -102,6 +107,22 @@ async function _sniffMagic(adapter) {
|
|
|
102
107
|
return { format: "unknown" };
|
|
103
108
|
}
|
|
104
109
|
|
|
110
|
+
// Collect a random-access adapter's bytes into a Buffer. Used by
|
|
111
|
+
// the v0.12.15 auto-unwrap path so the envelope can be decrypted
|
|
112
|
+
// inline + the inner bytes re-fed to a buffer adapter for format
|
|
113
|
+
// re-sniffing. Adapters expose `size` + `range(offset, length)`.
|
|
114
|
+
async function _collectSourceBytes(source) {
|
|
115
|
+
var size = source.size;
|
|
116
|
+
if (size == null && typeof source.resolveSize === "function") {
|
|
117
|
+
size = await source.resolveSize();
|
|
118
|
+
}
|
|
119
|
+
if (typeof size !== "number" || size < 0) {
|
|
120
|
+
throw new SafeArchiveError("safe-archive/bad-source",
|
|
121
|
+
"_collectSourceBytes: source adapter did not report a numeric size");
|
|
122
|
+
}
|
|
123
|
+
return source.range(0, size);
|
|
124
|
+
}
|
|
125
|
+
|
|
105
126
|
// ---- Public extract orchestrator ----------------------------------------
|
|
106
127
|
|
|
107
128
|
/**
|
|
@@ -180,6 +201,50 @@ async function extract(opts) {
|
|
|
180
201
|
var sniff = await _sniffMagic(source);
|
|
181
202
|
format = sniff.format;
|
|
182
203
|
}
|
|
204
|
+
// v0.12.15 — auto-unwrap path. When the sniffer identifies a
|
|
205
|
+
// wrap envelope, unwrap inline + re-sniff the inner bytes so
|
|
206
|
+
// operators get a single extract() call regardless of envelope
|
|
207
|
+
// shape. Operator must supply opts.recipient or opts.passphrase
|
|
208
|
+
// matching the envelope kind.
|
|
209
|
+
if (format === "wrap-recipient" || format === "wrap-passphrase") {
|
|
210
|
+
var sealedBytes = await _collectSourceBytes(source);
|
|
211
|
+
var inner;
|
|
212
|
+
if (format === "wrap-recipient") {
|
|
213
|
+
if (!opts.recipient) {
|
|
214
|
+
throw new SafeArchiveError("safe-archive/no-recipient-for-wrap",
|
|
215
|
+
"extract: source is a wrap-recipient envelope (BAWRP) but opts.recipient was not supplied. " +
|
|
216
|
+
"Pass `{ recipient: { privateKey, ecPrivateKey } }` (or peer-cert form) to unwrap inline.");
|
|
217
|
+
}
|
|
218
|
+
inner = archiveWrap().unwrap(sealedBytes, { recipient: opts.recipient });
|
|
219
|
+
} else {
|
|
220
|
+
if (typeof opts.passphrase !== "string" && !Buffer.isBuffer(opts.passphrase)) {
|
|
221
|
+
throw new SafeArchiveError("safe-archive/no-passphrase-for-wrap",
|
|
222
|
+
"extract: source is a wrap-passphrase envelope (BAWPP) but opts.passphrase was not supplied. " +
|
|
223
|
+
"Pass `{ passphrase: <string|Buffer> }` to unwrap inline.");
|
|
224
|
+
}
|
|
225
|
+
inner = await archiveWrap().unwrapWithPassphrase(sealedBytes, { passphrase: opts.passphrase });
|
|
226
|
+
}
|
|
227
|
+
// Codex P1 on v0.12.15 PR #166 — close the original source
|
|
228
|
+
// adapter BEFORE replacing it. When opts.source was a string
|
|
229
|
+
// path, the fs adapter opened a file descriptor; overwriting
|
|
230
|
+
// `source` loses the close reference and the descriptor
|
|
231
|
+
// leaks across repeated extract() calls (eventually EMFILE
|
|
232
|
+
// under load). The outer finally still closes whatever
|
|
233
|
+
// `source` points at, but the original handle needs explicit
|
|
234
|
+
// release here.
|
|
235
|
+
if (typeof source.close === "function" && typeof opts.source === "string") {
|
|
236
|
+
try { source.close(); } catch (_e) { /* drop-silent */ }
|
|
237
|
+
}
|
|
238
|
+
// Codex P2 on v0.12.15 PR #166 — forward opts.signal to the
|
|
239
|
+
// inner buffer adapter so abort propagation stays intact
|
|
240
|
+
// across the unwrap boundary. Without it, an abort raised
|
|
241
|
+
// after unwrapping would no longer cancel inner range()
|
|
242
|
+
// calls, breaking the documented signal contract for
|
|
243
|
+
// large wrapped archives.
|
|
244
|
+
source = archiveAdapters().buffer(inner, { signal: opts.signal });
|
|
245
|
+
var innerSniff = await _sniffMagic(source);
|
|
246
|
+
format = innerSniff.format;
|
|
247
|
+
}
|
|
183
248
|
var reader;
|
|
184
249
|
if (format === "zip") {
|
|
185
250
|
reader = archiveRead().zip(source, {
|
|
@@ -210,8 +275,8 @@ async function extract(opts) {
|
|
|
210
275
|
});
|
|
211
276
|
} else {
|
|
212
277
|
throw new SafeArchiveError("safe-archive/format-unsupported",
|
|
213
|
-
"extract: format=" + JSON.stringify(format) + " — v0.12.9 ships ZIP + tar + tar.gz " +
|
|
214
|
-
"
|
|
278
|
+
"extract: format=" + JSON.stringify(format) + " — v0.12.9 ships ZIP + tar + tar.gz; " +
|
|
279
|
+
"v0.12.10/v0.12.11 added wrap-recipient + wrap-passphrase envelopes (auto-unwrap as of v0.12.15)");
|
|
215
280
|
}
|
|
216
281
|
var result = await reader.extract({
|
|
217
282
|
destination: opts.destination,
|
|
@@ -265,6 +330,38 @@ async function inspect(opts) {
|
|
|
265
330
|
var sniff = await _sniffMagic(source);
|
|
266
331
|
format = sniff.format;
|
|
267
332
|
}
|
|
333
|
+
// v0.12.16 — auto-unwrap path for inspect, parallel to the
|
|
334
|
+
// v0.12.15 extract path. Wrap envelopes (BAWRP / BAWPP) are
|
|
335
|
+
// unwrapped inline + re-sniffed so operators can enumerate
|
|
336
|
+
// entries of a sealed archive in a single inspect() call.
|
|
337
|
+
if (format === "wrap-recipient" || format === "wrap-passphrase") {
|
|
338
|
+
var sealedBytes = await _collectSourceBytes(source);
|
|
339
|
+
var inner;
|
|
340
|
+
if (format === "wrap-recipient") {
|
|
341
|
+
if (!opts.recipient) {
|
|
342
|
+
throw new SafeArchiveError("safe-archive/no-recipient-for-wrap",
|
|
343
|
+
"inspect: source is a wrap-recipient envelope (BAWRP) but opts.recipient was not supplied. " +
|
|
344
|
+
"Pass `{ recipient: { privateKey, ecPrivateKey } }` (or peer-cert form) to unwrap inline.");
|
|
345
|
+
}
|
|
346
|
+
inner = archiveWrap().unwrap(sealedBytes, { recipient: opts.recipient });
|
|
347
|
+
} else {
|
|
348
|
+
if (typeof opts.passphrase !== "string" && !Buffer.isBuffer(opts.passphrase)) {
|
|
349
|
+
throw new SafeArchiveError("safe-archive/no-passphrase-for-wrap",
|
|
350
|
+
"inspect: source is a wrap-passphrase envelope (BAWPP) but opts.passphrase was not supplied. " +
|
|
351
|
+
"Pass `{ passphrase: <string|Buffer> }` to unwrap inline.");
|
|
352
|
+
}
|
|
353
|
+
inner = await archiveWrap().unwrapWithPassphrase(sealedBytes, { passphrase: opts.passphrase });
|
|
354
|
+
}
|
|
355
|
+
// v0.12.15 P1 — close the original fs adapter (if string-
|
|
356
|
+
// backed) BEFORE replacing the source reference. v0.12.15 P2
|
|
357
|
+
// — forward opts.signal to the inner buffer adapter.
|
|
358
|
+
if (typeof source.close === "function" && typeof opts.source === "string") {
|
|
359
|
+
try { source.close(); } catch (_e) { /* drop-silent */ }
|
|
360
|
+
}
|
|
361
|
+
source = archiveAdapters().buffer(inner, { signal: opts.signal });
|
|
362
|
+
var innerSniff = await _sniffMagic(source);
|
|
363
|
+
format = innerSniff.format;
|
|
364
|
+
}
|
|
268
365
|
var reader;
|
|
269
366
|
if (format === "zip") {
|
|
270
367
|
reader = archiveRead().zip(source, {
|
|
@@ -276,9 +373,21 @@ async function inspect(opts) {
|
|
|
276
373
|
bombPolicy: opts.bombPolicy,
|
|
277
374
|
audit: opts.audit,
|
|
278
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
|
+
});
|
|
279
388
|
} else {
|
|
280
389
|
throw new SafeArchiveError("safe-archive/format-unsupported",
|
|
281
|
-
"inspect: format=" + JSON.stringify(format) + " — v0.12.
|
|
390
|
+
"inspect: format=" + JSON.stringify(format) + " — v0.12.19 ships ZIP + tar + tar.gz; auto-unwraps wrap envelopes");
|
|
282
391
|
}
|
|
283
392
|
var entries = await reader.inspect();
|
|
284
393
|
var totalCompressed = 0;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.13",
|
|
4
|
+
"date": "2026-05-23",
|
|
5
|
+
"headline": "`b.backup.bundleAdapterStorage.objectStoreAdapter` — wraps any `b.objectStore` backend (local / SigV4 / GCS / Azure-Blob) into the backup-adapter contract; closes the v0.11.2 \"any custom backend\" promise",
|
|
6
|
+
"summary": "`b.backup.bundleAdapterStorage.objectStoreAdapter(client, opts)` adapts a `b.objectStore`-shaped client into the `{ writeFile, readFile, listKeys, deleteKey, hasKey }` adapter contract that `bundleAdapterStorage` consumes. Operators wire any of the four shipped object-store backends (`protocol: \"local\"` / `\"sigv4\"` for S3+MinIO / `\"gcs\"` / `\"azure-blob\"`) through the same recipient / passphrase wrap layers shipped in v0.12.10 and v0.12.11 — the bundle bytes hit the object-store `put` as an opaque envelope. `opts.prefix` namespaces every key under a fixed root inside the bucket so operators sharing a bucket across deployments keep listings scoped. Closes the deferral surfaced in v0.11.2 JSDoc and the v0.12.10 release-notes follow-up list: \"S3 or any custom backend\" is now wired with no operator-supplied adapter glue.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.backup.bundleAdapterStorage.objectStoreAdapter(client, opts?)` — object-store-backed bundle storage",
|
|
13
|
+
"body": "Adapts any `b.objectStore.buildBackend({ protocol })` client (local / sigv4 / gcs / azure-blob) into the `{ writeFile, readFile, listKeys, deleteKey, hasKey }` adapter contract. NOT_FOUND errors from the underlying client translate to `backup/no-key` for the readFile path and to idempotent return-without-throw for deleteKey (matching the fsAdapter contract). hasKey routes through `client.head(key)` — NOT_FOUND → false; any other error propagates so operators can distinguish network failure from missing-key. Composes transparently with v0.12.10 `cryptoStrategy: \"recipient\"` and v0.12.11 `cryptoStrategy: \"passphrase\"` — the wrap envelope is the bytes hitting the object-store put."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "`opts.prefix` — per-deployment key namespacing inside the bucket",
|
|
17
|
+
"body": "Operators sharing one bucket across multiple deployments (per-environment / per-tenant / per-region) pass distinct prefixes so listings stay scoped. The prefix gets a trailing slash inserted automatically; traversal segments (`..`) and NUL bytes refused upfront with `backup/bad-arg` so a misconfigured prefix can't escape the operator's intended scope. listKeys strips the prefix on return so the adapter surface looks identical to the fsAdapter — operators switching backends don't see key-shape drift."
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"heading": "Security",
|
|
23
|
+
"items": [
|
|
24
|
+
{
|
|
25
|
+
"title": "Key path validation — traversal + NUL byte refusal at every adapter call",
|
|
26
|
+
"body": "Every `_scopedKey(key)` invocation refuses keys containing `..` traversal segments or NUL bytes upfront with `backup/bad-key` so a misconfigured bundleId or an attacker-controlled value never reaches the underlying `client.put(...)` / `client.get(...)`. Matches the same defensive posture the fsAdapter carries against operator-supplied key shapes."
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.14",
|
|
4
|
+
"date": "2026-05-23",
|
|
5
|
+
"headline": "`b.archive.sniffEnvelope(bytes)` — identify recipient vs passphrase vs raw payload without attempting decryption",
|
|
6
|
+
"summary": "Small helper closing a gap in the archive-wrap surface. `b.archive.sniffEnvelope(bytes)` reads the first 5 bytes of a buffer and returns one of `\"recipient\"` (v0.12.10 BAWRP envelope), `\"passphrase\"` (v0.12.11 BAWPP envelope), or `\"none\"` (raw payload or unrelated bytes). The sniff does NO cryptographic work — no Argon2id round, no decapsulation, no allocation beyond a 5-byte ASCII compare — so it's safe to call on adversarial input. Operators dispatching between unwrap paths get a clean predicate instead of trial-decrypting under multiple key candidates.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.archive.sniffEnvelope(bytes)` — magic-byte envelope identifier",
|
|
13
|
+
"body": "Returns `\"recipient\"` (BAWRP / v0.12.10 hybrid PQC envelope), `\"passphrase\"` (BAWPP / v0.12.11 Argon2id + XChaCha20 envelope), or `\"none\"` (raw archive bytes / unrelated payload). Accepts Buffer + Uint8Array; non-buffer / null / undefined / empty-buffer inputs return `\"none\"` upfront. Operators wire the result into a switch that dispatches to the matching unwrap primitive — no trial decryption, no per-key candidate attempts."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.15",
|
|
4
|
+
"date": "2026-05-23",
|
|
5
|
+
"headline": "`b.safeArchive.extract` auto-unwraps v0.12.10 recipient and v0.12.11 passphrase envelopes inline",
|
|
6
|
+
"summary": "The safeArchive orchestrator's `format: \"auto\"` sniffer recognises `BAWRP` (v0.12.10 recipient) and `BAWPP` (v0.12.11 passphrase) envelope magics and routes through `b.archive.unwrap` / `b.archive.unwrapWithPassphrase` inline before re-sniffing the inner format. Operators pass `opts.recipient` (or `opts.passphrase`) alongside `source` + `destination` and get a single extract() call regardless of envelope shape. Missing the matching key opt surfaces a structured `safe-archive/no-recipient-for-wrap` / `safe-archive/no-passphrase-for-wrap` refusal upfront rather than a downstream crypto error.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.safeArchive.extract` auto-unwraps wrap envelopes",
|
|
13
|
+
"body": "The sniffer at byte 0-4 recognises `BAWRP` (returns `format: \"wrap-recipient\"`) and `BAWPP` (returns `format: \"wrap-passphrase\"`). The extract path collects the sealed adapter into a Buffer, routes through `b.archive.unwrap` (recipient) or `b.archive.unwrapWithPassphrase` (passphrase), wraps the inner bytes in a buffer adapter, re-sniffs the inner format, and dispatches to the appropriate `b.archive.read.*` reader. A wrap-around-tar.gz envelope round-trips through wrap → unwrap → gunzip → untar with no operator intervention beyond passing the key opt."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"heading": "Security",
|
|
19
|
+
"items": [
|
|
20
|
+
{
|
|
21
|
+
"title": "Missing-key opt refused upfront with structured error",
|
|
22
|
+
"body": "When the sniffer identifies a wrap envelope but the operator hasn't supplied `opts.recipient` (BAWRP) or `opts.passphrase` (BAWPP), extract refuses with `safe-archive/no-recipient-for-wrap` / `safe-archive/no-passphrase-for-wrap` BEFORE any decryption attempt. Operators wiring extract behind an HTTP boundary get a typed refusal instead of a leaked crypto-level error."
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.16",
|
|
4
|
+
"date": "2026-05-23",
|
|
5
|
+
"headline": "`b.safeArchive.inspect` auto-unwraps wrap envelopes (parallel to the v0.12.15 extract path)",
|
|
6
|
+
"summary": "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.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.safeArchive.inspect` auto-unwraps `BAWRP` + `BAWPP` envelopes",
|
|
13
|
+
"body": "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."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.17",
|
|
4
|
+
"date": "2026-05-23",
|
|
5
|
+
"headline": "`bundleAdapterStorage.bundleInfo` + `listBundles.format` — per-bundle introspection for envelope kind + format without restore",
|
|
6
|
+
"summary": "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.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`listBundles()` carries inferred format per bundle",
|
|
13
|
+
"body": "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."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "`storage.bundleInfo(bundleId)` — per-bundle introspection",
|
|
17
|
+
"body": "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`."
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.18",
|
|
4
|
+
"date": "2026-05-24",
|
|
5
|
+
"headline": "`bundleAdapterStorage.listBundles({ withStats })` + `bundleInfo.createdAt` — opt-in mtime + size from `statKey`",
|
|
6
|
+
"summary": "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.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`storage.listBundles({ withStats: true })` — opt-in per-bundle stat fan-out",
|
|
13
|
+
"body": "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."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "`storage.bundleInfo(bundleId)` returns `createdAt`",
|
|
17
|
+
"body": "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."
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.19",
|
|
4
|
+
"date": "2026-05-24",
|
|
5
|
+
"headline": "`bundleAdapterStorage.verifyBundle(bundleId)` — bundle integrity check without restore + `b.safeArchive.inspect` tar.gz support",
|
|
6
|
+
"summary": "`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.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`storage.verifyBundle(bundleId, opts?)` — integrity check without restore",
|
|
13
|
+
"body": "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)."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "`b.safeArchive.inspect` accepts `format: \"tar.gz\"`",
|
|
17
|
+
"body": "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."
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.20",
|
|
4
|
+
"date": "2026-05-24",
|
|
5
|
+
"headline": "`bundleAdapterStorage.verifyAllBundles(opts)` — bounded-parallel batch integrity walk with stopOnFirstFailure short-circuit",
|
|
6
|
+
"summary": "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.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`storage.verifyAllBundles(opts?)` — batch integrity walk",
|
|
13
|
+
"body": "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."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 0 — b.archive.sniffEnvelope — magic-byte identification
|
|
4
|
+
* of recipient vs passphrase envelopes vs raw payload.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
var b = require("../../index");
|
|
8
|
+
var helpers = require("../helpers");
|
|
9
|
+
var check = helpers.check;
|
|
10
|
+
|
|
11
|
+
async function testSniffRecipient() {
|
|
12
|
+
var pair = b.crypto.generateEncryptionKeyPair();
|
|
13
|
+
var sealed = b.archive.wrap(Buffer.from("PHI"), { recipient: pair });
|
|
14
|
+
check("sniffEnvelope: BAWRP buffer returns \"recipient\"",
|
|
15
|
+
b.archive.sniffEnvelope(sealed) === "recipient");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function testSniffPassphrase() {
|
|
19
|
+
var sealed = await b.archive.wrapWithPassphrase(Buffer.from("PHI"), {
|
|
20
|
+
passphrase: "aLongCorrectHorseBatteryStaple9876!Phrase",
|
|
21
|
+
});
|
|
22
|
+
check("sniffEnvelope: BAWPP buffer returns \"passphrase\"",
|
|
23
|
+
b.archive.sniffEnvelope(sealed) === "passphrase");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function testSniffRawBytes() {
|
|
27
|
+
check("sniffEnvelope: plain bytes return \"none\"",
|
|
28
|
+
b.archive.sniffEnvelope(Buffer.from("hello world")) === "none");
|
|
29
|
+
check("sniffEnvelope: gzip bytes return \"none\" (gzip is not a wrap envelope)",
|
|
30
|
+
b.archive.sniffEnvelope(Buffer.from([0x1f, 0x8b, 0x08, 0x00])) === "none");
|
|
31
|
+
check("sniffEnvelope: tar header bytes return \"none\"",
|
|
32
|
+
b.archive.sniffEnvelope(Buffer.alloc(512)) === "none");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function testSniffEmpty() {
|
|
36
|
+
check("sniffEnvelope: empty buffer returns \"none\"",
|
|
37
|
+
b.archive.sniffEnvelope(Buffer.alloc(0)) === "none");
|
|
38
|
+
check("sniffEnvelope: 1-byte buffer returns \"none\"",
|
|
39
|
+
b.archive.sniffEnvelope(Buffer.from([0x42])) === "none");
|
|
40
|
+
check("sniffEnvelope: 4-byte buffer (below magic) returns \"none\"",
|
|
41
|
+
b.archive.sniffEnvelope(Buffer.from("BAWR")) === "none");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function testSniffTruncatedEnvelope() {
|
|
45
|
+
// Codex P2B on v0.12.14 PR #165 — a 5-byte BAWRP / BAWPP buffer is
|
|
46
|
+
// a TRUNCATED envelope, not raw bytes. Sniff must classify by the
|
|
47
|
+
// magic alone so dispatch routes to unwrap, which surfaces the
|
|
48
|
+
// truncation error.
|
|
49
|
+
check("sniffEnvelope: 5-byte BAWRP returns \"recipient\" (truncated envelope, not raw)",
|
|
50
|
+
b.archive.sniffEnvelope(Buffer.from("BAWRP")) === "recipient");
|
|
51
|
+
check("sniffEnvelope: 5-byte BAWPP returns \"passphrase\" (truncated envelope, not raw)",
|
|
52
|
+
b.archive.sniffEnvelope(Buffer.from("BAWPP")) === "passphrase");
|
|
53
|
+
// Operator dispatch path: truncated envelopes routed to unwrap
|
|
54
|
+
// surface a structured wrap error rather than silent
|
|
55
|
+
// misclassification.
|
|
56
|
+
var refused = null;
|
|
57
|
+
try {
|
|
58
|
+
b.archive.unwrap(Buffer.from("BAWRP"), {
|
|
59
|
+
recipient: b.crypto.generateEncryptionKeyPair(),
|
|
60
|
+
});
|
|
61
|
+
} catch (e) { refused = e; }
|
|
62
|
+
check("sniffEnvelope → unwrap on truncated BAWRP: structured error surfaced",
|
|
63
|
+
refused instanceof b.archive.ArchiveWrapError);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function testSniffZeroCopyView() {
|
|
67
|
+
// Codex P2A on v0.12.14 PR #165 — sniff must NOT copy the input
|
|
68
|
+
// when given a Uint8Array. Construct a large Uint8Array, check
|
|
69
|
+
// the sniff returns quickly without allocating a Buffer of the
|
|
70
|
+
// full input length. We can't directly measure allocation here,
|
|
71
|
+
// but we can verify the result is correct + the byte at the
|
|
72
|
+
// typed-array's byteOffset is read (not byte 0 of the underlying
|
|
73
|
+
// ArrayBuffer).
|
|
74
|
+
var underlying = new ArrayBuffer(1024);
|
|
75
|
+
var view = new Uint8Array(underlying, 100, 50); // offset 100, length 50
|
|
76
|
+
// Write BAWRP into the view starting at view[0] (which is
|
|
77
|
+
// underlying[100]).
|
|
78
|
+
view[0] = 0x42; view[1] = 0x41; view[2] = 0x57; view[3] = 0x52; view[4] = 0x50;
|
|
79
|
+
check("sniffEnvelope: zero-copy view honours byteOffset",
|
|
80
|
+
b.archive.sniffEnvelope(view) === "recipient");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function testSniffNonBuffer() {
|
|
84
|
+
check("sniffEnvelope: string input returns \"none\" (non-Buffer)",
|
|
85
|
+
b.archive.sniffEnvelope("BAWRP") === "none");
|
|
86
|
+
check("sniffEnvelope: null returns \"none\"",
|
|
87
|
+
b.archive.sniffEnvelope(null) === "none");
|
|
88
|
+
check("sniffEnvelope: undefined returns \"none\"",
|
|
89
|
+
b.archive.sniffEnvelope(undefined) === "none");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function testSniffUint8Array() {
|
|
93
|
+
var pair = b.crypto.generateEncryptionKeyPair();
|
|
94
|
+
var sealed = b.archive.wrap(Buffer.from("X"), { recipient: pair });
|
|
95
|
+
var u8 = new Uint8Array(sealed);
|
|
96
|
+
check("sniffEnvelope: Uint8Array carrying BAWRP returns \"recipient\"",
|
|
97
|
+
b.archive.sniffEnvelope(u8) === "recipient");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function run() {
|
|
101
|
+
await testSniffRecipient();
|
|
102
|
+
await testSniffPassphrase();
|
|
103
|
+
await testSniffRawBytes();
|
|
104
|
+
await testSniffEmpty();
|
|
105
|
+
await testSniffTruncatedEnvelope();
|
|
106
|
+
await testSniffZeroCopyView();
|
|
107
|
+
await testSniffNonBuffer();
|
|
108
|
+
await testSniffUint8Array();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { run: run };
|
|
112
|
+
|
|
113
|
+
if (require.main === module) {
|
|
114
|
+
run().then(
|
|
115
|
+
function () { console.log("[archive-sniff-envelope] OK — " + helpers.getChecks() + " checks passed"); },
|
|
116
|
+
function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }
|
|
117
|
+
);
|
|
118
|
+
}
|