@blamejs/core 0.16.33 → 0.16.34

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.16.x
10
10
 
11
+ - v0.16.34 (2026-07-16) — **The DPoP middleware returns the correct multiple-proof rejection when a request carries a repeated DPoP header, instead of mislabeling it as a missing proof.** RFC 9449 §4.1 permits only one DPoP header value per request. b.middleware.dpop rejected a request that carried the header as an array -- repeated DPoP: lines a custom server or proxy did not collapse -- but its array-shape check sat after the missing-header guard, and an array is not a string, so the missing-header guard always ran first. A duplicated DPoP proof was therefore rejected as a missing proof (and, when a DPoP nonce was required, answered with use_dpop_nonce, prompting the client into a pointless nonce-retry loop) rather than with the invalid_dpop_proof / multiple-DPoP-headers rejection the specification calls for. Both paths already refused the request, so this was never a fail-open -- only an incorrect diagnostic and a wasted round trip. The array-shape check now runs before the missing-header guard, so a repeated DPoP header is rejected with the correct error. **Fixed:** *DPoP middleware rejects a repeated DPoP header with the correct multiple-proof error* — b.middleware.dpop enforces the RFC 9449 §4.1 single-value rule, but its Array.isArray check for a repeated header (when a server or proxy delivered the DPoP header as an array rather than a comma-joined string) sat after the non-string / empty guard. Because an array fails the non-string check first, the dedicated multiple-DPoP-headers branch never ran: a duplicated proof was reported as a missing DPoP header, and under a required-nonce policy it returned use_dpop_nonce, driving the client into a fruitless nonce-retry loop. The array-shape check now runs first, so a repeated DPoP header is rejected with invalid_dpop_proof and a multiple-DPoP-headers message. This changes only the error code and message for that malformed-request case; both orderings already refused the request, so no valid request is affected.
12
+
11
13
  - v0.16.33 (2026-07-16) — **Agent-snapshot restore now authenticates a sealed snapshot's tenant and capture-time against its signature, closing a cross-tenant restore path, alongside fail-closed and never-throw hardening across the DKIM/ARC, crypto-envelope, and image/PDF verifiers.** A sealed agent snapshot's authenticated envelope binds its table, snapshot id, and schema version, but the decorative wrapper fields a hostile or compromised storage backend can rewrite -- the tenant id that loadLatest filters on and the capture time it sorts on -- were trusted without being checked against the signed body. b.agent.snapshot restore now cross-checks both wrapper fields against the signature-covered values and refuses a mismatch, so a relabelled row can no longer surface one tenant's authentic snapshot to another (cross-tenant restore) or misrepresent when the restored state was captured. The release also hardens several verifiers that a valid-but-unusual or hostile input could push off their documented contract: the DKIM, inbound-authentication, and ARC verifiers now accept a bare-LF (Unix line-ending) message and return an authentication verdict instead of throwing; the crypto envelope and packed-secret decoders reject a truncated ciphertext with their typed error instead of leaking a raw cipher exception; the image guard routes a byte-order-mark-prefixed SVG to refusal at every profile instead of serving it as unknown content; and the image and PDF guards no longer throw on a hostile metadata bag whose bytes field is an array-like object, honoring their never-throw inspection contract. **Fixed:** *DKIM, inbound-authentication, and ARC verifiers accept bare-LF messages instead of throwing* — b.mail.dkim.verify, b.mail.inbound.verify, and b.mail.arc.verify canonicalize over CRLF and split the header block on a CRLF-CRLF separator. A message read from a Unix file or mbox, or passed through operator tooling that stripped carriage returns, arrives with bare-LF line endings and previously raised an uncaught error out of the verifier rather than returning an authentication verdict. The header/body split now normalizes bare-LF to canonical CRLF before locating the separator (a no-op on a proper CRLF message), so a bare-LF message produces a verdict -- and a message signed on the CRLF wire but transported bare-LF now verifies correctly rather than failing. inbound.verify and arc.verify additionally treat a message with no separator at all as headers-only, returning a verdict in keeping with their always-return-a-verdict contract. · *Crypto envelope and packed-secret decoders reject truncated ciphertext with a typed error* — b.crypto.decryptEnvelope and b.crypto.decryptPacked verify that each declared component of an untrusted ciphertext -- the length-prefixed KEM ciphertext and hybrid ephemeral public key, and the trailing nonce and authentication tag -- fits within the envelope before handing it to the cipher or the key-agreement step. A ciphertext truncated inside any of those components previously reached Node crypto as an under-length value and surfaced as a raw exception (a cipher RangeError on the nonce, or a Failed to perform decapsulation / key-parse error on the KEM ciphertext or ephemeral key), escaping the documented Invalid envelope error contract and leaking implementation detail. Both decoders now reject a truncated input with their typed Invalid envelope / Invalid packed format error, while a truncation inside the ciphertext body still surfaces as the genuine authentication-tag failure; a well-formed input is never affected. · *Image guard routes a BOM-prefixed SVG to refusal at every profile* — b.guardImage detects SVG by its leading markup so it can route it to the SVG guard or refuse it. A UTF-8 byte-order-mark before the markup previously defeated the offset-anchored signature scan, so a BOM-prefixed SVG fell through as unknown content -- served rather than refused under the balanced and permissive profiles. The magic-byte scanner now skips a leading BOM when matching the SVG and XML signatures, so a BOM-prefixed SVG is detected and refused at every profile. The BOM skip applies only to those text-family signatures: a binary raster's magic must sit at its real offset, so a BOM-prefixed PNG or JPEG is still refused as unknown content rather than accepted as a valid raster. · *Image and PDF guards no longer throw on a hostile array-like metadata bag* — b.guardImage.validate and b.guardPdf.validate document pure inspection that never throws on hostile metadata. Their byte-size cap measured any value carrying a numeric length, but the measurement primitive accepts only strings, Buffers, and Uint8Arrays and threw on a plain Array or array-like object -- crashing a direct validate or sanitize caller (the gate path already fails closed). The cap now measures only those byte-carrying types and passes an unmeasurable array-like through to magic detection, which reads only the leading bytes and refuses unrecognized content, so validate returns a refusal instead of throwing. **Security:** *Agent-snapshot restore binds the requested tenant and capture-time to the sealed snapshot's signature* — b.agent.snapshot seals each snapshot under an authenticated envelope whose AAD binds the table, snapshot id, and schema version. The metadata a backend stores alongside the sealed blob -- the tenant id that loadLatest({ tenantId }) filters on and the takenAt it sorts on to pick the latest -- is not covered by that AAD, and a hostile or compromised backend can return independently tampered list() and get() results. A backend could therefore relabel tenant A's list() entry as tenant B (leaving A's get() row honest) so that loadLatest({ tenantId: 'tenant-b' }) selected and returned tenant A's authentic snapshot -- a cross-tenant restore of in-flight sagas, streams, and idempotency state -- or inflate a row's list() age to serve an older snapshot as the latest. loadLatest now binds the requested selection criteria to the loaded snapshot's signature-covered values: the authenticated tenant id must equal the requested tenant id, and the list() sort key that selected a row must equal that row's authenticated capture time; a divergence is refused (agent-snapshot/tenant-id-mismatch, agent-snapshot/taken-at-mismatch), and the load path additionally cross-checks the get() wrapper against the signed body. The fix is at load time and does not change the seal format, so previously persisted snapshots remain restorable. A hostile backend can still withhold snapshots it never reveals, but every snapshot returned is authentic and bound to the requested tenant. **Detectors:** *A byte-size cap must vet its input type before measuring it* — A codebase-patterns gate refuses a guard that measures a metadata bag's byte length gated only on a numeric length property -- the shape that let an array-like bytes field crash the image and PDF guards. A byte-size cap over untrusted metadata must confirm the value is a string, Buffer, or Uint8Array before measuring it, so a future guard cannot reintroduce the never-throw-contract violation.
12
14
 
13
15
  - v0.16.32 (2026-07-15) — **The vendored Public Suffix List is refreshed to the current upstream snapshot, and vendor-update.sh gains the --refresh-data mode its file headers and verifier messages have always pointed at.** The vendored Mozilla Public Suffix List is updated to the latest upstream build, so organizational-domain derivation for DMARC alignment, BIMI issuer scoping, cookie-scope confinement, and same-site policy reflects the current registry delegations; the data module is regenerated and re-signed (SHA-256 + SHA3-512 + SLH-DSA), the manifest hashes refreshed, and the NOTICE attribution date updated. The refresh itself now runs through scripts/vendor-update.sh --refresh-data — the maintenance command that every vendored data file's header and lib/vendor-data.js's tamper-error messages reference — which fetches the upstream where one exists, sanity-checks the body before anything reaches the signer, re-appends the in-payload integrity canary, regenerates and re-signs the .data.js carrier, updates the manifest and NOTICE dates, and verifies all four integrity layers. A codebase-patterns gate now refuses any script flag referenced from error messages, file headers, or operator docs that the target script does not implement. **Added:** *scripts/vendor-update.sh --refresh-data — fetch, canary, re-sign, and verify the vendored data files in one command* — The mode that vendored data-file headers and lib/vendor-data.js tamper-error messages direct operators to now exists. `vendor-update.sh --refresh-data [entry]` refreshes the Public Suffix List and the SecLists common-password corpus from their upstreams (refusing a truncated or error body before it can reach the signer), re-appends each file's in-payload integrity canary, regenerates and re-signs the .data.js carrier with the operator-local SLH-DSA key whenever the raw file changed OR the carrier fails four-layer verification (so a corrupted signature block or a stripped provenance header is repairable through the documented path), updates the MANIFEST bundledAt and NOTICE attribution dates, refreshes the manifest hashes, and finishes by running the four-layer verifier (SHA-256 + SHA3-512 + SLH-DSA signature + canary) over every vendored data file. The operator-managed BIMI trust-anchor bundle is never fetched — it is re-signed only when the local .pem was edited per its file-header procedure. A Public Suffix List fetch whose VERSION timestamp is older than the vendored one is refused rather than signed (the list is CDN-served, and a lagging edge can return an older snapshot than the one already vendored). Entries whose upstream is unchanged are left byte-identical, so a no-op refresh produces a clean working tree; in-flight downloads live in gitignored *.refresh-tmp files that are removed on exit. **Changed:** *Vendored Public Suffix List refreshed to the current upstream snapshot* — The vendored Mozilla Public Suffix List is updated to the latest upstream build (2026-07-15), so organizational-domain derivation for DMARC alignment, BIMI issuer scoping, cookie-scope confinement, and same-site policy reflects the current registry delegations. The data module is regenerated and re-signed (SHA-256 + SHA3-512 + SLH-DSA), the manifest hashes refreshed, and the NOTICE attribution date updated. **Detectors:** *Script flags referenced from error messages, file headers, and operator docs must exist in the target script* — A codebase-patterns gate resolves every `<script>.sh --flag` / `<script>.js --flag` reference in lib/, scripts/, and the operator docs against the scripts/ directory and refuses any flag with no whole-token occurrence outside comment lines in the target script — a usage-header comment alone does not count as an implementation. A maintenance command recommended by a verification-failure message or a stale-data gate must work when the operator reaches for it.
@@ -311,16 +311,21 @@ function create(opts) {
311
311
 
312
312
  var middleware = async function dpopMiddleware(req, res, next) {
313
313
  var proofHeader = req.headers && req.headers.dpop;
314
+ // RFC 9449 §4.1 — only ONE DPoP header value per request. Check the
315
+ // array shape (repeated `DPoP:` header lines a custom server/proxy did
316
+ // not collapse) BEFORE the non-string guard: an array is not a string,
317
+ // so the guard below would otherwise shadow this branch and mislabel a
318
+ // duplicated proof as a missing one, letting the multiple-proof
319
+ // rejection never run.
320
+ if (Array.isArray(proofHeader)) {
321
+ return _writeUnauthorized(req, res, "invalid_dpop_proof",
322
+ "multiple DPoP headers are not allowed", null, onDeny, problemMode);
323
+ }
314
324
  if (typeof proofHeader !== "string" || proofHeader.length === 0) {
315
325
  return _writeUnauthorized(req, res,
316
326
  nonceMgr ? "use_dpop_nonce" : "invalid_dpop_proof",
317
327
  "DPoP header required", _freshNonce(), onDeny, problemMode);
318
328
  }
319
- // RFC 9449 §4.1 — only ONE DPoP header value per request.
320
- if (Array.isArray(proofHeader)) {
321
- return _writeUnauthorized(req, res, "invalid_dpop_proof",
322
- "multiple DPoP headers are not allowed", null, onDeny, problemMode);
323
- }
324
329
  // RFC 9449 §4.1 single-value invariant. node:http
325
330
  // collapses repeated headers into a comma-joined string when the
326
331
  // client ships `DPoP: proof1, DPoP: proof2`; the Array.isArray
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.16.33",
3
+ "version": "0.16.34",
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:28a4f186-23d8-487b-bead-ee0129c21413",
5
+ "serialNumber": "urn:uuid:cb89f635-0d20-4d7a-b200-e38dc8bed885",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-16T10:43:55.252Z",
8
+ "timestamp": "2026-07-16T12:36:01.015Z",
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.16.33",
22
+ "bom-ref": "@blamejs/core@0.16.34",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.16.33",
25
+ "version": "0.16.34",
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.16.33",
29
+ "purl": "pkg:npm/%40blamejs/core@0.16.34",
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.16.33",
57
+ "ref": "@blamejs/core@0.16.34",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]