@blamejs/core 0.18.12 → 0.18.13

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.18.x
10
10
 
11
+ - v0.18.13 (2026-08-06) — **`b.db.collection` no longer corrupts a BLOB overflow column (its bytes were spread as numeric keys, destroying the JSON payload); broader offline test coverage across db-collection, TLS, external-db residency, notify, and BIMI.** A schemaless collection with a BLOB overflow column read that column back as a byte array. Because a byte array is a JavaScript object, the collection treated it as an already-parsed overflow object and spread its bytes as numeric keys on read, on update, and on insert — silently destroying the stored JSON payload. The overflow value is now decoded as JSON at every one of those points. The release also closes offline test-coverage gaps and adds an explicit test that the external-database residency boundary is enforced. **Changed:** *Broader offline test coverage and an explicit external-database residency-enforcement test* — b.db.collection (BLOB overflow round-trips, introspection-error wrapping, overflow-as-jsonColumn), b.network.tls.connectWithEch (the connect success, timeout, unreachable-peer, and synchronous-throw paths driven against local servers), b.externalDb (the boot-time data-residency check now has an explicit test that a personal-data backend outside the declared region is refused and compliant backends are admitted), b.notify (the default HTTP client is bound at send time), and b.mail.bimi (a certificate chain deeper than the maximum is rejected) now have tests for these boundary paths. **Fixed:** *b.db.collection no longer corrupts a BLOB overflow column* — A collection configured with an overflow column (unknown fields folded into a JSON column) reads that column back as a byte array when the column is declared BLOB. A byte array's typeof is "object", so the collection took it to be an already-parsed overflow object and spread its raw bytes as numeric-index keys — on read (surfacing { "0": …, "1": … } onto the row), on a $set/$unset update (rewriting the column to the byte spread and destroying the original keys), and on an insert that supplied a Buffer overflow value. The overflow value is now decoded from its bytes as JSON at all three points, so the stored payload survives; a BLOB holding non-JSON bytes falls back to an empty object instead of corrupting, matching the existing non-JSON-string behaviour.
12
+
11
13
  - v0.18.12 (2026-08-05) — **Certificate Transparency SCT verification now works for real certificates (it previously rejected every embedded SCT); b.network.tls.ct.verifyScts / requireScts now require the issuing CA; broader Redis-client and TLS-verification test coverage.** b.network.tls.ct.verifyScts reconstructed the wrong RFC 6962 signed input for the SCTs embedded in a certificate — it built an x509_entry over the bare TBSCertificate instead of the precert_entry (issuer_key_hash + TBSCertificate) the log actually signed, and separately read the SCT's signature algorithm from a field the parser does not emit. Either alone made verification impossible: verifyScts and requireScts rejected every real embedded SCT. Verification now builds the RFC 6962 §3.2 precert_entry and requires the issuing CA so it can bind issuer_key_hash. This is a fail-closed fix — nothing invalid was ever trusted — but SCT enforcement now needs the issuer supplied. **Changed:** *b.network.tls.ct.verifyScts and requireScts now require the issuing CA* — Because an embedded SCT binds the issuing CA's key, verifying one needs that key. Pass the issuer to verifyScts as issuerCertDer (the issuer certificate DER), issuerSpkiDer (its SubjectPublicKeyInfo DER), or issuerKeyHash (the precomputed 32-byte SHA-256). Without it, verifyScts returns { ok: false, reason: "issuer-key-required" } and does not verify. requireScts derives the issuer from the peer's certificate chain automatically and accepts the same options; supply one explicitly only when the issuer is not present in the chain. verifyInclusion takes the same issuer options for embedded (precert_entry) leaves. · *Vendored @blamejs/pki refreshed to 0.3.32* — The vendored @blamejs/pki bundle behind the mutual-TLS CA engine is refreshed to 0.3.32, which adds C509 (CBOR-encoded certificate) extended-key-usage and extension registries (RFC 7633 TLS Feature, RFC 4556 PKINIT, and SSH/CMC/Bundle-Security purposes). No blamejs public API changes. · *Error- and edge-path tests for the Redis client and TLS-verification primitives* — b.redisClient (RESP framing and protocol-error edges, split replies, AUTH / SELECT on connect, push-message demux, connect/command timeouts, socket-error and close teardown) and b.network.tls (OCSP and Certificate-Transparency parse and verification branches, ECH config framing, and PKIX host/SAN matching) now have explicit tests for their boundary branches, driven through local fake servers and crafted certificates. **Fixed:** *Certificate Transparency SCT verification now works for certificate-embedded SCTs* — For an SCT embedded in a certificate's X.509v3 extension, the log computed its signature over the RFC 6962 §3.2 precert_entry — issuer_key_hash (SHA-256 of the issuing CA's SubjectPublicKeyInfo) followed by the TBSCertificate with the SCT extension removed. b.network.tls.ct.verifyScts instead reconstructed an x509_entry over just the stripped TBSCertificate, with no issuer binding, so the reconstructed input never matched what the log signed and no real embedded SCT could verify. It additionally read the SCT's signature-algorithm from a field name the parser does not emit, so the algorithm cross-check failed before the signature was even reached. verifyScts now builds the precert_entry and reads the parsed algorithm field, so a valid SCT verifies and a genuine mismatch is still refused. b.network.tls.ct.verifyInclusion builds the RFC 9162 §4.6 leaf the same way. The prior behaviour failed closed (it rejected rather than accepted), so no invalid certificate was ever trusted. **Security:** *SCT verification now binds each log key to the SCT's log ID* — An SCT's log ID is the SHA-256 of the log's public key (RFC 6962 §3.2). b.network.tls.ct.verifyScts now recomputes that hash from the configured key and refuses the SCT (reason "log-id-key-mismatch") when it does not equal the log ID the SCT names, before checking the signature. This prevents a misconfigured logKeys map from letting a key impersonate an approved log. · *SCT verification counts distinct logs, refuses future timestamps, and validates the minimum* — verifyScts now counts SCTs from distinct logs, so duplicate SCTs from a single log can no longer satisfy a "minimum 2 SCTs" diversity policy (verifiedCount reflects distinct logs). It refuses an SCT whose timestamp is in the future (RFC 6962 §5.2; opts.now / opts.clockSkewMs override the clock and the default five-minute skew). A minScts that is not a positive integer is rejected at the entry point rather than silently accepting an unverified certificate. Parsing an SCT extension from a peer certificate now fails closed on malformed DER — it returns no SCT instead of throwing, so a hostile certificate cannot raise an uncaught error out of verifyScts, parseScts, or the requireScts predicate.
12
14
 
13
15
  - v0.18.11 (2026-08-03) — **A static file that is a symlink at the final path component now fails closed with a clean 404 instead of an uncaught error; broader DNS, field-crypto, and static-server coverage.** The static server refuses a symlink at the final path component (its O_NOFOLLOW TOCTOU defense) — but that refusal now falls back to a clean 404 / NOT_FOUND rather than propagating an uncaught error that hung the request or surfaced a raw ELOOP from staticServe.integrity(). Alongside the fix, error and edge branches of b.network.dns, b.cryptoField, and b.staticServe come under explicit test. **Changed:** *Error- and edge-path tests for the DNS, field-crypto, and static-server primitives* — b.network.dns (DoH oversized-body handling, DoT mid-query connection reset and socket reuse, cache expiry, system-resolver error paths), b.cryptoField (tampered / relocated / downgraded cell fail-closed nulling, key-row AAD and rowId resolution, unseal rate-cap configuration, on-read upgrade branches), and b.staticServe (integrity and confinement edge cases, idle-timeout teardown, the final-component symlink refusal above) now have explicit tests for their boundary branches. An unused internal date-formatting helper was removed. **Fixed:** *A final-component symlink (or a mid-hash read error) on a static file fails closed with a clean 404* — When computing a served file's SRI and ETag, the static server opens it with O_NOFOLLOW so a symlink swapped in at the final path component after the in-root check cannot be followed (a TOCTOU / CWE-22 / CWE-367 defense). On POSIX that open raises ELOOP for a symlinked target — the common atomic-deploy pattern where a stable name links to a hashed file (latest.js pointing at app.<hash>.js). That error, and any read error mid-hash, previously propagated uncaught: the middleware request hung with no response, and staticServe.integrity() threw a raw ELOOP instead of its documented NOT_FOUND. It now falls back exactly as a read error does — the middleware returns 404 (falls through) and integrity() throws NOT_FOUND. The symlink is still not served; the refusal is just clean. Windows is unaffected (O_NOFOLLOW is a no-op there).
@@ -216,6 +216,22 @@ function _splitUpdateOperators(update) {
216
216
  * users.find({ dept: "eng" });
217
217
  * // → JSON_EXTRACT(data, '$.dept') = 'eng'
218
218
  */
219
+
220
+ // Decode a BLOB overflow value as JSON. A BLOB column reads back as a
221
+ // Uint8Array/Buffer (typeof "object"); its bytes are JSON text and must be
222
+ // parsed — Object.assign over the raw view would spread the bytes as numeric
223
+ // keys and destroy the payload. Returns the parsed plain object, or null when
224
+ // the bytes are not a JSON object (unparseable / a non-object / an array) —
225
+ // mirroring the string branches: the read path leaves an un-decodable value
226
+ // on the row as-is, write/update coalesce a null to {}. Callers reach this
227
+ // only after an ArrayBuffer.isView guard.
228
+ function _blobToObject(view) {
229
+ try {
230
+ var parsed = safeJson.parse(Buffer.from(view.buffer, view.byteOffset, view.byteLength).toString("utf8"));
231
+ return (parsed && typeof parsed === "object" && !Array.isArray(parsed)) ? parsed : null;
232
+ } catch (_e) { return null; }
233
+ }
234
+
219
235
  function collection(name, opts) {
220
236
  validateOpts.requireNonEmptyString(name, "collection(name): name", TypeError, "db-collection/bad-name");
221
237
  opts = opts || {};
@@ -297,10 +313,14 @@ function collection(name, opts) {
297
313
  });
298
314
  if (hasExtras) {
299
315
  var existing = null;
300
- if (typeof writeDoc[overflow] === "object" && writeDoc[overflow] !== null && !Array.isArray(writeDoc[overflow])) {
316
+ if (ArrayBuffer.isView(writeDoc[overflow])) { // BLOB/Buffer overflow value -> decode as JSON, don't spread bytes
317
+ existing = _blobToObject(writeDoc[overflow]);
318
+ } else if (typeof writeDoc[overflow] === "object" && writeDoc[overflow] !== null && !Array.isArray(writeDoc[overflow])) {
301
319
  existing = writeDoc[overflow];
302
320
  }
303
321
  writeDoc[overflow] = JSON.stringify(Object.assign({}, existing || {}, extras));
322
+ } else if (writeDoc[overflow] !== undefined && writeDoc[overflow] !== null && ArrayBuffer.isView(writeDoc[overflow])) {
323
+ writeDoc[overflow] = JSON.stringify(_blobToObject(writeDoc[overflow]) || {});
304
324
  } else if (writeDoc[overflow] !== undefined && writeDoc[overflow] !== null && typeof writeDoc[overflow] === "object") {
305
325
  writeDoc[overflow] = JSON.stringify(writeDoc[overflow]);
306
326
  }
@@ -321,6 +341,8 @@ function collection(name, opts) {
321
341
  var extra = null;
322
342
  if (typeof out[overflow] === "string" && out[overflow].length > 0) {
323
343
  try { extra = safeJson.parse(out[overflow]); } catch (_e) { extra = null; }
344
+ } else if (ArrayBuffer.isView(out[overflow])) { // BLOB/Buffer overflow -> decode JSON, don't spread bytes onto the row
345
+ extra = _blobToObject(out[overflow]);
324
346
  } else if (typeof out[overflow] === "object" && !Array.isArray(out[overflow])) {
325
347
  extra = out[overflow];
326
348
  }
@@ -537,6 +559,8 @@ function collection(name, opts) {
537
559
  if (rows[ri][overflow] !== undefined && rows[ri][overflow] !== null) {
538
560
  if (typeof rows[ri][overflow] === "string" && rows[ri][overflow].length > 0) {
539
561
  try { existing = safeJson.parse(rows[ri][overflow]) || {}; } catch (_e) { existing = {}; }
562
+ } else if (ArrayBuffer.isView(rows[ri][overflow])) { // BLOB/Buffer overflow -> decode JSON before the read-modify-write
563
+ existing = _blobToObject(rows[ri][overflow]) || {};
540
564
  } else if (typeof rows[ri][overflow] === "object") {
541
565
  existing = rows[ri][overflow] || {};
542
566
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.18.12",
3
+ "version": "0.18.13",
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:f252dbc4-dd1d-490a-bb8e-8b2310f8b127",
5
+ "serialNumber": "urn:uuid:bf5b4eb4-4331-43ae-99ee-da98a007aedd",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-06T00:29:00.492Z",
8
+ "timestamp": "2026-08-06T04:24:19.246Z",
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.18.12",
22
+ "bom-ref": "@blamejs/core@0.18.13",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.18.12",
25
+ "version": "0.18.13",
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.18.12",
29
+ "purl": "pkg:npm/%40blamejs/core@0.18.13",
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.18.12",
57
+ "ref": "@blamejs/core@0.18.13",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]