@blamejs/core 0.12.14 → 0.12.15

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 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.15 (2026-05-23) — **`b.safeArchive.extract` auto-unwraps v0.12.10 recipient and v0.12.11 passphrase envelopes inline.** 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. **Added:** *`b.safeArchive.extract` auto-unwraps wrap envelopes* — 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. **Security:** *Missing-key opt refused upfront with structured error* — 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.
12
+
11
13
  - v0.12.14 (2026-05-23) — **`b.archive.sniffEnvelope(bytes)` — identify recipient vs passphrase vs raw payload without attempting decryption.** 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. **Added:** *`b.archive.sniffEnvelope(bytes)` — magic-byte envelope identifier* — 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.
12
14
 
13
15
  - v0.12.13 (2026-05-23) — **`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.** `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. **Added:** *`b.backup.bundleAdapterStorage.objectStoreAdapter(client, opts?)` — object-store-backed bundle storage* — 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. · *`opts.prefix` — per-deployment key namespacing inside the bucket* — 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. **Security:** *Key path validation — traversal + NUL byte refusal at every adapter call* — 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.
@@ -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
- "(encryptPacked-wrap lands v0.12.10)");
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.12.14",
3
+ "version": "0.12.15",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:e3025640-8314-45f6-9dc8-7e1f45fb1063",
5
+ "serialNumber": "urn:uuid:73e48489-0fa1-477f-b6f3-eec74e9ba65f",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-24T02:31:34.805Z",
8
+ "timestamp": "2026-05-24T03:18:46.285Z",
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.14",
22
+ "bom-ref": "@blamejs/core@0.12.15",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.12.14",
25
+ "version": "0.12.15",
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.14",
29
+ "purl": "pkg:npm/%40blamejs/core@0.12.15",
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.14",
57
+ "ref": "@blamejs/core@0.12.15",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]