@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.
Files changed (27) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/lib/analytics.js +1 -1
  3. package/lib/vendor/MANIFEST.json +2 -2
  4. package/lib/vendor/blamejs/CHANGELOG.md +16 -0
  5. package/lib/vendor/blamejs/api-snapshot.json +6 -2
  6. package/lib/vendor/blamejs/lib/archive-wrap.js +58 -0
  7. package/lib/vendor/blamejs/lib/archive.js +1 -0
  8. package/lib/vendor/blamejs/lib/backup/index.js +585 -10
  9. package/lib/vendor/blamejs/lib/safe-archive.js +112 -3
  10. package/lib/vendor/blamejs/package.json +1 -1
  11. package/lib/vendor/blamejs/release-notes/v0.12.13.json +31 -0
  12. package/lib/vendor/blamejs/release-notes/v0.12.14.json +18 -0
  13. package/lib/vendor/blamejs/release-notes/v0.12.15.json +27 -0
  14. package/lib/vendor/blamejs/release-notes/v0.12.16.json +18 -0
  15. package/lib/vendor/blamejs/release-notes/v0.12.17.json +22 -0
  16. package/lib/vendor/blamejs/release-notes/v0.12.18.json +22 -0
  17. package/lib/vendor/blamejs/release-notes/v0.12.19.json +22 -0
  18. package/lib/vendor/blamejs/release-notes/v0.12.20.json +18 -0
  19. package/lib/vendor/blamejs/test/layer-0-primitives/archive-sniff-envelope.test.js +118 -0
  20. package/lib/vendor/blamejs/test/layer-0-primitives/backup-bundle-info.test.js +279 -0
  21. package/lib/vendor/blamejs/test/layer-0-primitives/backup-object-store-adapter.test.js +167 -0
  22. package/lib/vendor/blamejs/test/layer-0-primitives/backup-verify-all-bundles.test.js +0 -0
  23. package/lib/vendor/blamejs/test/layer-0-primitives/backup-verify-bundle.test.js +186 -0
  24. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +28 -0
  25. package/lib/vendor/blamejs/test/layer-0-primitives/safe-archive-auto-unwrap.test.js +116 -0
  26. package/lib/vendor/blamejs/test/layer-0-primitives/safe-archive-inspect-unwrap.test.js +89 -0
  27. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.0.x
10
10
 
11
+ - v0.0.114 (2026-05-23) — **Catalog mirror — close out the last 7 detectors + `matchOn: "basename"` runner support.** Closes the codebase-patterns catalog gap against vendored blamejs. The seven detectors held back from the v0.0.113 bulk port — six because the splice tool couldn't roundtrip their regex literals (embedded double-quotes / complex character classes) and one (`release-named-test-file`) that needed `matchOn: "basename"` support in the runner — are now hand-ported. Shop's catalog grows from 114 to 121 detectors, with seven covering archive-wrap PQC hybrid contracts, SQL transaction-wrapper extraction, the two `Map.prototype.getOrInsertComputed` Node-26 migration variants, PQC AlgorithmIdentifier `NULL` parameters anti-pattern, release-named test-file refusal, and `safeDecompress` paired-cap discipline. One legitimate `map-has-then-set` site in `lib/analytics.js#byCurrency` surfaced through the port and is kept with an inline `allow:` marker documenting the Node-26 floor-bump migration target. **Added:** *Seven new codebase-patterns detectors complete the catalog mirror* — `archive-wrap-recipient-missing-ec-half` (hybrid PQC `recipient: { publicKey: ... }` must also carry `ecPublicKey:` — uses `requires:` companion-check to exempt files where the matching `ecPublicKey` lives elsewhere), `inline-sql-transaction-wrapper` (BEGIN/COMMIT/ROLLBACK try/catch boilerplate routes through `dbSchema.runInTransaction`), `map-get-or-insert-pre-node-26` + `map-has-then-set-pre-node-26` (both variants of the two-step Map insert-if-absent pattern that Node 26 collapses to `Map.prototype.getOrInsertComputed`), `pqc-algid-with-null-params` (RFC 9909/9881/9936 — PQC AlgorithmIdentifier parameters field is ABSENT, not `NULL`), `release-named-test-file` (refuses `v0-8-41-additions.test.js` / `slot-19-enhancements.test.js` / `batch-N.test.js` shapes — uses the new `matchOn: "basename"` runner feature), `safedecompress-omits-max-compressed-bytes` (every `safeDecompress({ maxOutputBytes })` call MUST also name `maxCompressedBytes` so the caps stay aligned with operator intent — uses `requires:` companion-check). · *`matchOn: "basename"` runner support* — `_scan(regex, scope, { matchOn: "basename" })` matches the regex against each file's basename rather than file contents. Used by detectors that police naming conventions (e.g. release-named-test-file). The feature mirrors the runner contract in vendored blamejs's catalog so future basename-policing detectors can be ported verbatim. **Changed:** *`lib/analytics.js#byCurrency` carries an inline `allow:map-has-then-set-pre-node-26` marker* — The `if (!byCurrency.has(cur)) { ...byCurrency.set(cur, {...}); }` insert-if-absent in the per-currency aggregate loop migrates cleanly to `Map.prototype.getOrInsertComputed` when the framework's `engines.node` floor bumps to Node 26 (eligible Oct 2026 per the LTS calendar). The detector now exists so a fresh occurrence post-this-patch trips pre-merge; the existing site is allowed with a reason that includes the migration target.
12
+
11
13
  - v0.0.113 (2026-05-23) — **Catalog mirror — 66 new codebase-patterns detectors ported from vendored blamejs.** The codebase-patterns detector catalog grows from 48 to 114 — 66 new detectors ported verbatim from `lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js`. The full vendored set covers archive-handling traps, framework-helper composition (the `inline-*` family), Node-version-conditional patterns, SBOM derivation traps, mail-server TLS upgrade hygiene, mountinfo bind-detection invariants, and dozens of other bug classes the framework has surfaced through audits. Every new detector scopes `shop` (lib + worker) so reinventions are caught anywhere in the application surface. Vendor allowlist entries referencing internal blamejs paths (e.g. `lib/crypto.js`) are kept verbatim — shop's `_walk` skips the vendor tree, so those entries are harmless no-ops and preserve the operator-readable reason text. Three pre-existing inline reinventions surfaced through the port and are kept with inline `allow:` markers documenting why the framework helper can't compose — the existing `TypeError` message contract requires the literal field name (`/timeoutMs/`, `/opts.version/`) which `validateOpts.*`'s `code`-first error shape doesn't carry. Eight detector blocks (six with regex literals containing literal `"` characters that the splice tool can't roundtrip, plus `release-named-test-file` whose `matchOn: "basename"` directive shop's runner doesn't honor yet, plus `inline-base64url-three-replace` already ported in 0.0.112) defer to a follow-up. **Added:** *Mirror 66 new codebase-patterns detectors from vendored blamejs* — Categories: archive-handling (extract-overwrite-without-refusal, gz-without-safedecompress, read-gz-without-self-authored-budget, tar-walker-without-truncation-check, wrap-recipient-missing-ec-half — except wrap-recipient which deferred to follow-up); the `inline-*` family of framework-helper composition reminders (40+ detectors covering aggregate-issues, assert-no-char-threats, audit-emit-wrapper, audit-shape-validation, bad-input-issue-result, batch-positive-int-validation, buffer-byte-equality-loop, build-guard-gate-forwarder, char-strip-policy-cascade, codepoint-class-table, compliance-posture-lookup, crlf-string-test, default-resolution-cascade, detect-char-threats, emit-event-wrapper, extract-bytes-as-text, flush-timer-scheduler, hex-string-validator, iso8601-millisecond-strip, issue-validator-entry, log-via-or-fallback, migration-filename-regex, numeric-bounds-cascade, object-store-http-request, observability-shape-validation, optional-* validators, profile-builder-forwarder, redis-client-opts-forwarding, require-* validators, resolve-profile-and-posture, rule-pack-loader, sql-identifier-regex, trailing-hspace-strip); SBOM derivation traps (toplevel-ref-by-slash-heuristic, bom-ref enum-rank-without-validation, etc.); compliance/posture coverage (compliance-posture-coverage-drift); cred-store + mailstore + mountinfo invariants; PQC + dot-stuff regex shapes; CONDSTORE + BDAT mail-server invariants. Full list visible by diffing `test/layer-0-primitives/codebase-patterns.test.js` against the v0.0.112 baseline. · *Worker `b` adapter exposes `b.validateOpts`* — `worker/b.js` now re-exports the `validate-opts` leaf module so future Worker primitives can compose `b.validateOpts.optionalPositiveFinite` / `b.validateOpts.optionalFunction` / `b.validateOpts.requireNonEmptyString` etc. against the same one-source-of-truth as lib-side primitives. Adds the leaf to the Worker bundle but doesn't bind a public namespace change. **Changed:** *Three inline-validation sites carry per-line `allow:` markers* — `lib/externaldb-d1.js#_validateOpts` (two lines: `timeoutMs`, `fetch`), `lib/r2-bridge.js#_validateOpts` (one line: `timeoutMs`), and `worker/render/search.js#renderSearch` (one line: `opts.version`) keep the literal `TypeError` shape with the labelled message because each is paired with a test contract that asserts the field name appears in the message (`assert.throws(..., /timeoutMs/)`). The framework's `validateOpts.*` helper produces a `code`-first `TypeError` (`TypeError: validate-opts/bad-positive-finite`) whose message doesn't carry the label; routing through it would silently strip the labelled-field contract from the test. Marker explains the constraint at the source.
12
14
 
13
15
  - v0.0.112 (2026-05-23) — **Compose `b.crypto.toBase64Url` instead of reinventing — nine primitives + new detector.** Nine framework primitives (`api-keys`, `carrier-accounts`, `customer-surveys`, `stock-alerts`, `stock-receipts`, `subscription-gifts`, `webhook-receiver`, `wishlist-sharing`, `storefront`'s server-side `_b64u`) shipped with hand-rolled base64url encoding — the canonical four-line `.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")` chain. The trailing-padding `.replace(/=+$/, "")` strip is polynomial-ReDoS-shaped per CodeQL js/polynomial-redos. The framework's `b.crypto.toBase64Url(buf)` helper routes through Node's built-in `base64url` encoding which is linear-time and produces the same RFC 4648 §5 output. Every server-side callsite now composes the framework primitive. Two browser-side helpers in `lib/storefront.js`'s WebAuthn UI string-template (no `b.crypto` available in the page runtime) carry inline `allow:inline-base64url-three-replace` markers — `window.btoa` plus the three-replace shim is the browser-built-in equivalent. New `inline-base64url-three-replace` codebase-patterns detector enforces the composition shop-wide. **Added:** *`inline-base64url-three-replace` codebase-patterns detector* — Flags `.replace(/=+$/, ...)` anywhere under `lib/` or `worker/`. Catches the canonical four-line base64url reinvention chain at the trailing-padding-strip line. Ported verbatim (with shop-scope) from blamejs's catalog. Browser-side string-template emissions in `lib/storefront.js` carry inline allow markers documenting that the browser has no `b.crypto` runtime — `window.btoa` plus the three-replace shim is the built-in. **Changed:** *Nine primitives compose `b.crypto.toBase64Url`* — `api-keys` `_generateToken`, `carrier-accounts` `_generateSecret`, `customer-surveys` `_generateToken`, `stock-alerts` `_mintToken` (inlined the redundant `_b64url` helper), `stock-receipts` `_generateToken`, `subscription-gifts` `_generateToken`, `webhook-receiver` `_generateSecret`, `wishlist-sharing` `_generateToken`, and `storefront`'s server-side `_b64u` helper all now route through `_b().crypto.toBase64Url(buf)`. Linear-time output, no polynomial-ReDoS surface, no Node-minor-version Buffer-flag-rename risk.
package/lib/analytics.js CHANGED
@@ -268,7 +268,7 @@ function create(opts) {
268
268
  for (var i = 0; i < r.rows.length; i += 1) {
269
269
  var row = r.rows[i];
270
270
  var cur = row.currency;
271
- if (!byCurrency.has(cur)) {
271
+ if (!byCurrency.has(cur)) { // allow:map-has-then-set-pre-node-26 — engines.node is `>=24` today; migrate to Map.prototype.getOrInsertComputed when the floor bumps to Node 26 (eligible Oct 2026 per LTS calendar)
272
272
  var byStatus = {};
273
273
  for (var j = 0; j < ALL_STATUSES.length; j += 1) byStatus[ALL_STATUSES[j]] = 0;
274
274
  byCurrency.set(cur, {
@@ -3,8 +3,8 @@
3
3
  "_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
4
4
  "packages": {
5
5
  "blamejs": {
6
- "version": "0.12.12",
7
- "tag": "v0.12.12",
6
+ "version": "0.12.20",
7
+ "tag": "v0.12.20",
8
8
  "license": "Apache-2.0",
9
9
  "author": "blamejs contributors",
10
10
  "source": "https://github.com/blamejs/blamejs",
@@ -8,6 +8,22 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.12.x
10
10
 
11
+ - 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.
12
+
13
+ - v0.12.19 (2026-05-24) — **`bundleAdapterStorage.verifyBundle(bundleId)` — bundle integrity check without restore + `b.safeArchive.inspect` tar.gz support.** `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. **Added:** *`storage.verifyBundle(bundleId, opts?)` — integrity check without restore* — 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). · *`b.safeArchive.inspect` accepts `format: "tar.gz"`* — 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.
14
+
15
+ - v0.12.18 (2026-05-24) — **`bundleAdapterStorage.listBundles({ withStats })` + `bundleInfo.createdAt` — opt-in mtime + size from `statKey`.** 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. **Added:** *`storage.listBundles({ withStats: true })` — opt-in per-bundle stat fan-out* — 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. · *`storage.bundleInfo(bundleId)` returns `createdAt`* — 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.
16
+
17
+ - v0.12.17 (2026-05-23) — **`bundleAdapterStorage.bundleInfo` + `listBundles.format` — per-bundle introspection for envelope kind + format without restore.** 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. **Added:** *`listBundles()` carries inferred format per bundle* — 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. · *`storage.bundleInfo(bundleId)` — per-bundle introspection* — 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
+ - v0.12.16 (2026-05-23) — **`b.safeArchive.inspect` auto-unwraps wrap envelopes (parallel to the v0.12.15 extract path).** 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. **Added:** *`b.safeArchive.inspect` auto-unwraps `BAWRP` + `BAWPP` envelopes* — 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.
20
+
21
+ - 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.
22
+
23
+ - 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.
24
+
25
+ - 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.
26
+
11
27
  - v0.12.12 (2026-05-23) — **`b.ai.disclosure.chatbot` + `b.ai.disclosure.deepfake` + `b.ai.disclosure.emotion` — EU AI Act Art. 50 transparency obligations (calendar-locked 2026-08-02) with US-CA AB-853 + China CAC GenAI cross-walk.** EU AI Act Art. 50 transparency primitives land ahead of the 2026-08-02 enforcement deadline. `b.ai.disclosure.chatbot(session, opts)` emits the Art. 50(1) first-contact "you are interacting with an AI system" disclosure with placement control (`first-message` / `always` / `on-request`). `b.ai.disclosure.deepfake(content, { contentType, placement, jurisdiction })` emits the Art. 50(4) synthetic-content label + machine-readable metadata payload for image / audio / video / text. `b.ai.disclosure.emotion({ systemType })` emits the Art. 50(3) emotion-recognition / biometric-categorisation notice. Each primitive emits a tamper-evident `ai-act/*-disclosure-applied` audit event so the compliance trail backs the user-facing notice. Cross-jurisdiction cross-walk lives in `opts.jurisdiction`: `"eu"` (default), `"us-ca"` adds AB-853 §22949.91 to the cross-walk array, `"cn"` adds CAC GenAI Measures Art. 12. The deepfake primitive returns a `schema: "c2pa-v1.4-ready"` metadata field that the v0.12.21 `b.contentCredentials` C2PA adapter will consume when it lands — this patch ships the label markup + schema; the C2PA manifest emission is the next composition. **Added:** *`b.ai.disclosure.chatbot(session, opts)` — Art. 50(1) first-contact disclosure* — Operators interacting with natural persons via an AI system get a primitive that emits the "you are interacting with an AI system" notice + audits the emission. `placement` opts: `"first-message"` (default — emit on first contact only, tracked via `session.aiDisclosureEmitted`), `"always"` (every response), `"on-request"` (operator wires their own trigger). Returns `{ text, language, jurisdiction, placement, shouldEmit, article, regulation }` — `shouldEmit` is the operator-consumable boolean for response-wire-up logic. · *`b.ai.disclosure.deepfake(content, opts)` — Art. 50(4) synthetic-content label* — Operators emitting model-generated or model-manipulated content get a primitive that returns both the visible label markup AND the machine-readable metadata payload. `contentType: "image" | "audio" | "video" | "text"` is required; `placement: "label" | "metadata" | "both"` (default `"both"`) controls what the primitive populates. The metadata payload includes `schema: "c2pa-v1.4-ready"` — the v0.12.21 `b.contentCredentials` C2PA adapter will consume this schema field when it lands. `crossWalk` array carries `["eu-ai-act/Art. 50(4)"]` plus the per-jurisdiction reference (AB-853 §22949.91 / CAC GenAI Art. 12). · *`b.ai.disclosure.emotion(opts)` — Art. 50(3) emotion-recognition / biometric-categorisation notice* — Operators deploying emotion-recognition or biometric-categorisation systems get the consent-flow notice primitive. `systemType: "emotion" | "biometric-categorisation"` (default `"emotion"`) selects which Art. 50(3) sub-obligation applies. Returns the notice payload + emits an `ai-act/emotion-disclosure-applied` audit event. · *Cross-jurisdiction cross-walk: EU + US-CA + China in a single primitive* — The `opts.jurisdiction` opt accepts `"eu"` (default — Regulation (EU) 2024/1689), `"us-ca"` (California AB-853 effective 2026), or `"cn"` (China CAC GenAI Measures). The chatbot + deepfake primitives both honour the cross-walk: the deepfake response's `crossWalk` array carries every jurisdiction-specific legal reference the same emission satisfies, so operators serving multi-region traffic emit one notice + audit one event + reference all applicable regimes. **Security:** *Drop-silent audit emission preserves the disclosure path under audit-bus failure* — If `opts.audit` is supplied but its `safeEmit` throws (network bus down, audit-sign chain malformed), the disclosure primitive still returns the user-facing notice payload. The Art. 50 obligation is the user-facing notice itself; the audit emission is a parallel best-effort chain-of-custody record. Refusing the disclosure to defend the audit chain would fail the wrong direction — the regulatory contract is satisfied by emitting the notice. Matches the framework's `audit.safeEmit` drop-silent contract for hot-path observability sinks. **Migration:** *C2PA manifest emission lands in v0.12.21* — The deepfake primitive's metadata payload includes a `schema: "c2pa-v1.4-ready"` field that the v0.12.21 `b.contentCredentials` adapter will consume. Operators emitting image / audio / video for v0.12.12-0.12.20 get the label markup + structured metadata; the actual C2PA manifest (signed JUMBF assertion chain) is the next composition layer.
12
28
 
13
29
  - v0.12.11 (2026-05-23) — **`b.archive.wrapWithPassphrase` + `b.archive.unwrapWithPassphrase` — Argon2id + XChaCha20-Poly1305 archive envelope + `b.backup` `cryptoStrategy: "passphrase"` with HIPAA / PCI-DSS 128-bit entropy floor.** Passphrase wrap lands as the second `b.archive` envelope strategy alongside v0.12.10's recipient wrap. `b.archive.wrapWithPassphrase(bytes, { passphrase, minEntropyBits })` produces a `BAWPP`-prefixed envelope under Argon2id (RFC 9106; framework-default 64 MiB / 3 iterations / 4 parallelism) key derivation with XChaCha20-Poly1305 AEAD; each envelope carries its own fresh salt in the wire format (5-byte magic + 1-byte version + 1-byte saltLen + salt + 24-byte nonce + ciphertext+tag) so KDF parameters can rotate in future minors without per-envelope version bumps. `b.archive.unwrapWithPassphrase(sealed, { passphrase })` verifies the `BAWPP` header before any Argon2id compute so non-envelope inputs fail with `archive-wrap/bad-magic` rather than burning the KDF on bad bytes. `b.backup.bundleAdapterStorage({ cryptoStrategy: "passphrase", passphrase })` composes the wrap layer transparently — bundle bytes hitting the adapter's `writeFile` are an opaque passphrase-derived envelope. Default `passphraseMinEntropyBits: 80` matches OWASP strong-password guidance; HIPAA + PCI-DSS postures raise the floor to 128 bits automatically (matching the framework's existing crypto-grade-password discipline for sealed-storage). The recipient strategy from v0.12.10 + passphrase strategy from v0.12.11 + plaintext strategy from v0.12.7 cover the operator's posture matrix: HIPAA / PCI-DSS pick recipient or passphrase; non-regulated deployments may stay on `"none"` when the storage layer is itself the protective boundary. **Added:** *`b.archive.wrapWithPassphrase(bytes, { passphrase, minEntropyBits })` — Argon2id-derived archive envelope* — Composes `b.backupCrypto.encryptWithFreshSalt(bytes, passphrase)` (Argon2id KDF + XChaCha20-Poly1305 AEAD, fresh per-envelope salt) and prepends a 7-byte `BAWPP` envelope header (5-byte magic + 1-byte version + 1-byte saltLen) so format sniffers can identify passphrase wrap output without trial KDF work. Entropy estimate uses observed-alphabet bit-count (the standard NIST/OWASP character-class approximation). `minEntropyBits` defaults to 80; the gate refuses upfront with `archive-wrap/weak-passphrase` when the estimate falls short. · *`b.archive.unwrapWithPassphrase(sealed, { passphrase })` — inverse with magic-check upfront* — Verifies the 7-byte `BAWPP` header (magic + version + saltLen) before any cryptographic work so non-envelope inputs (raw archives, recipient-wrap envelopes, truncated buffers) fail with `archive-wrap/bad-magic` / `archive-wrap/bad-version` / `archive-wrap/truncated-envelope` rather than wasting Argon2id compute. Routes through `b.backupCrypto.decryptWithPassphrase(encrypted, passphrase, saltHex)` so the framework's locked Argon2id parameters apply. · *`b.backup.bundleAdapterStorage({ cryptoStrategy: "passphrase", passphrase })` — Argon2id-keyed bundle storage* — Composes `b.archive.wrapWithPassphrase` transparently — every `writeBundle` payload is wrapped before `adapter.writeFile`; every `readBundle` payload is unwrapped after `adapter.readFile`. The `passphraseMinEntropyBits` opt defaults to 80 (OWASP strong-password floor); HIPAA + PCI-DSS postures raise the floor to 128 bits automatically. Passphrase + directory format combination refused upfront (same contract as recipient + directory). Wire-format envelope on disk is opaque ciphertext — no information leakage about archive contents through the storage adapter. · *HIPAA + PCI-DSS postures raise entropy floor to 128 bits under passphrase strategy* — `bundleAdapterStorage({ posture: "hipaa", cryptoStrategy: "passphrase", passphrase })` enforces `passphraseMinEntropyBits >= 128` regardless of the operator-supplied opt. The 128-bit floor matches the framework's existing crypto-grade-password discipline for sealed-storage cells. Operators sourcing passphrases from a CSPRNG (`b.crypto.generateBytes(16).toString("base64url")` → ~128 bits) pass without issue; operators typing dictionary phrases trip the gate. **Security:** *Magic-check before KDF work — non-envelope inputs can't burn Argon2id compute* — Adversarial inputs that look like passphrase envelopes but aren't (random bytes, recipient envelopes, raw archives) fail at byte 0-4 (magic check) rather than after a 64 MiB Argon2id round. Operators handing user-supplied bundles to readBundle on a server with concurrent load get bounded refusal latency rather than worst-case KDF compute under a chosen-bytes attack.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
- "frameworkVersion": "0.12.12",
4
- "createdAt": "2026-05-23T21:15:03.328Z",
3
+ "frameworkVersion": "0.12.20",
4
+ "createdAt": "2026-05-24T05:46:34.569Z",
5
5
  "exports": {
6
6
  "a2a": {
7
7
  "type": "object",
@@ -1855,6 +1855,10 @@
1855
1855
  }
1856
1856
  }
1857
1857
  },
1858
+ "sniffEnvelope": {
1859
+ "type": "function",
1860
+ "arity": 1
1861
+ },
1858
1862
  "tar": {
1859
1863
  "type": "function",
1860
1864
  "arity": 0
@@ -240,6 +240,63 @@ function _isPassphraseMagic(buf) {
240
240
  buf.slice(0, 5).toString("ascii") === ARCH_PASSPHRASE_MAGIC;
241
241
  }
242
242
 
243
+ /**
244
+ * @primitive b.archive.sniffEnvelope
245
+ * @signature b.archive.sniffEnvelope(bytes)
246
+ * @since 0.12.14
247
+ * @status stable
248
+ * @related b.archive.wrap, b.archive.unwrap, b.archive.wrapWithPassphrase, b.archive.unwrapWithPassphrase
249
+ *
250
+ * Identify the envelope shape carried by a buffer without attempting
251
+ * decryption. Returns one of:
252
+ * - `"recipient"` — `BAWRP` header (v0.12.10 hybrid PQC envelope).
253
+ * Operator routes through `b.archive.unwrap(bytes, { recipient })`.
254
+ * - `"passphrase"` — `BAWPP` header (v0.12.11 Argon2id + XChaCha20
255
+ * envelope). Operator routes through
256
+ * `b.archive.unwrapWithPassphrase(bytes, { passphrase })`.
257
+ * - `"none"` — no archive-wrap envelope magic. The bytes are
258
+ * either raw archive content (gz / tar / zip) or an unrelated
259
+ * payload; operator routes to the appropriate `b.archive.read.*`
260
+ * primitive (or refuses entirely).
261
+ *
262
+ * The sniff is byte 0-4 inspection ONLY — no cryptographic work,
263
+ * no allocation beyond a 5-byte ASCII compare. Safe to call on
264
+ * adversarial input.
265
+ *
266
+ * @example
267
+ * var kind = b.archive.sniffEnvelope(payloadBytes);
268
+ * switch (kind) {
269
+ * case "recipient": return b.archive.unwrap(payloadBytes, { recipient });
270
+ * case "passphrase": return b.archive.unwrapWithPassphrase(payloadBytes, { passphrase });
271
+ * case "none": return payloadBytes;
272
+ * }
273
+ */
274
+ function sniffEnvelope(bytes) {
275
+ if (!Buffer.isBuffer(bytes) && !(bytes instanceof Uint8Array)) {
276
+ return "none";
277
+ }
278
+ // Codex P2A on v0.12.14 PR #165 — `Buffer.from(uint8Array)` copies
279
+ // the entire input, turning a constant-time 5-byte probe into an
280
+ // O(n) allocation. Use the zero-copy view form so the sniff is
281
+ // truly cheap regardless of input size.
282
+ var buf = Buffer.isBuffer(bytes)
283
+ ? bytes
284
+ : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
285
+ if (buf.length < 5) return "none";
286
+ // Codex P2B on v0.12.14 PR #165 — match on the 5-byte ASCII magic
287
+ // alone, NOT on the full header (which requires version + saltLen
288
+ // bytes). A truncated envelope (`BAWRP` + nothing else) is still a
289
+ // recipient envelope; the unwrap call surfaces the truncation with
290
+ // a structured `archive-wrap/truncated-envelope` error. Returning
291
+ // "none" on truncated input would misclassify damaged envelopes as
292
+ // raw payload and the operator's dispatch switch would skip the
293
+ // wrap error path entirely.
294
+ var magic = buf.slice(0, 5).toString("ascii");
295
+ if (magic === ARCH_WRAP_MAGIC) return "recipient";
296
+ if (magic === ARCH_PASSPHRASE_MAGIC) return "passphrase";
297
+ return "none";
298
+ }
299
+
243
300
  /**
244
301
  * @primitive b.archive.wrapWithPassphrase
245
302
  * @signature b.archive.wrapWithPassphrase(bytes, opts)
@@ -438,6 +495,7 @@ module.exports = {
438
495
  unwrap: unwrap,
439
496
  wrapWithPassphrase: wrapWithPassphrase,
440
497
  unwrapWithPassphrase: unwrapWithPassphrase,
498
+ sniffEnvelope: sniffEnvelope,
441
499
  ArchiveWrapError: ArchiveWrapError,
442
500
  // Exposed for sibling modules + sniffer
443
501
  _isWrapMagic: _isWrapMagic,
@@ -556,6 +556,7 @@ module.exports = {
556
556
  unwrap: archiveWrap.unwrap,
557
557
  wrapWithPassphrase: archiveWrap.wrapWithPassphrase,
558
558
  unwrapWithPassphrase: archiveWrap.unwrapWithPassphrase,
559
+ sniffEnvelope: archiveWrap.sniffEnvelope,
559
560
  ArchiveError: ArchiveError,
560
561
  TarError: archiveTar.TarError,
561
562
  ArchiveGzError: archiveGz.ArchiveGzError,