@blamejs/core 0.8.17 → 0.8.18

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.8.x
10
10
 
11
+ - **0.8.18** (2026-05-08) — Database query-builder hardening on `b.db.from(...).where|whereRaw` and `b.safeSql`. **`Query.where(field, "LIKE", value)`** — now escapes SQL `%` and `_` wildcard metacharacters in operator-supplied values and emits `LIKE ? ESCAPE '\\'`. Closes the column-disclosure class where `q=%@%` enumerated the entire table. Operators who deliberately want LIKE wildcards bypass via `whereRaw()`. **`Query.where(field, "IN", [...])`** — now expands an array to `IN (?, ?, ?)` and binds each value separately. Pre-v0.8.18 the array bound to a single placeholder and silently matched zero rows (node:sqlite `?` doesn't support array-binding). Refuses non-array / empty-array inputs. **`Query.whereRaw(sql, params)`** — placeholder-counting now skips `?` characters inside SQL string literals (single + double quoted, including `''`-doubled escapes), line comments (`-- ...`), and block comments (`/* ... */`). Pre-v0.8.18 the naive regex counted literal-`?` and either threw on count mismatch OR let through fragments with masked missing real placeholders. **`b.safeSql.BANNED_IDENTIFIERS`** — extended to refuse `pragma` / `attach` / `detach` / `analyze` / `vacuum` / `reindex` as bare identifiers. Closes the SQLite-specific escape-the-parameterized-model surface (PRAGMAs disable security-relevant protections; ATTACH mounts external databases). **`b.db.declareTable`** — refuses any app schema name that begins with the framework's `_blamejs_` prefix (was an exact-match Set; an app schema entry like `_blamejs_audit_log_archive` slipped past the gate and could shadow / look-alike framework tables).
12
+
11
13
  - **0.8.17** (2026-05-08) — Email auth + DANE / TLS-RPT spec-conformance fixes (ARC / DMARC / SPF / A-R / DANE / TLS-RPT). Ten more RFC-cited gaps closed. **ARC (`b.mail.arc`)** — signer's AMS canonicalization now includes the current hop's `ARC-Authentication-Results` per RFC 8617 §5.1.1 (auto-prepends to `h=` unless operator passes `excludeAarFromAms: true`). Microsoft + Google receivers DO include AAR in `h=`; the prior framework default produced chains those receivers couldn't verify. Verifier counterpart: when reconstructing the AMS canonical input, only the CURRENT hop's AAR is kept (prior-hop AARs stripped). Verifier now enforces `t=` (signing time) + `x=` (expiration) time windows per RFC 8617 §5.2 with operator-tunable `clockSkewMs` (default 5 min) — pre-v0.8.17 the verifier parsed but never enforced. **DMARC (`b.mail.dmarc`)** — multiple `v=DMARC1` records on a domain now treated as having no DMARC record per RFC 7489 §6.6.3 (`_fetchDmarcRecord` returns null on multi-match). Subdomain `sp=` fallback wired: when `_dmarc.<from-domain>` returns no record, the verifier walks one label up to the (heuristic) organizational domain and applies its `sp=` (subdomain policy) — falls back to `p=` when `sp=` absent. Result includes `policyOriginDomain` + `orgDomainPolicyApplied: true` so operators can audit which record drove the verdict. (Heuristic only — operators with PSL needs supply their own `dnsLookup` for full Public Suffix List walk.) **SPF (`b.mail.spf`)** — initial query for the sender's SPF record no longer counts toward the 10-lookup limit per RFC 7208 §4.6.4. Pre-v0.8.17 was off-by-one — senders at the spec ceiling got false `permerror`. **A-R (`b.mail.authResults`)** — result vocabulary is now METHOD-SPECIFIC per RFC 8601 §2.7 (per-method `AR_RESULTS_BY_METHOD` map). The flat `AR_VALID_RESULTS` table previously accepted `hardfail` for DKIM (only valid for DMARC §2.7.4) and accepted `temperror` / `permerror` for methods that don't recognize them. **DANE (`b.network.smtp.dane`)** — `daneTlsa()` now REFUSES to return records unless the caller passes `opts.dnssecValidated: true` per RFC 7672 §1.3 (TLSA records that are not DNSSEC-validated MUST NOT be used). Pre-v0.8.17 silently used un-validated records — silent escalation class. `daneVerifyChain` enforces RFC 6698 §2.1.4 + RFC 7672 §3.1.1 chain-order: a DANE-TA match at chain position `i` is accepted only when its DER Subject equals the DER Issuer of cert at position `i-1` (i.e. it's actually the parent in the chain, not a random non-leaf cert that happens to hash-match). Chain-order is best-effort — synthetic test fixtures without ASN.1-parseable DER fall through with `chainOrderUnverified: true` flagged on the match. **TLS-RPT (`b.network.smtp.tlsRpt.fetchPolicy`)** — record without `rua=` now returns `null` (record ignored) per RFC 8460 §3. Pre-v0.8.17 returned `{ version: "TLSRPTv1", rua: [] }` which operators incorrectly treated as a valid record with no destinations. Bug fix only — no new operator-facing primitives.
12
14
 
13
15
  - **0.8.16** (2026-05-08) — Email auth + transport spec-conformance fixes (DKIM / SPF / MTA-STS / OCSP). Ten RFC-cited gaps closed against shipped primitives. **DKIM (`b.mail.dkim`)** — verifier now refuses any signature whose `h=` tag does not include `from` (RFC 6376 §3.5 cornerstone bypass — without From-coverage the signature does not bind to the visible sender; receivers were treating these as valid); refuses unrecognized `v=` tag values per RFC 6376 §3.5 (only `v=1` accepted); enforces empty `p=` as explicit key revocation per RFC 6376 §3.6.1 (verdict `fail`, not `permerror` — well-formed signature against a withdrawn key); enforces `k=` algorithm-family tag agreement with the signature's `a=` family (e.g. `k=rsa` paired with `a=ed25519-sha256` permerrors per RFC 6376 §3.6.1); selector validator now accepts multi-label selectors per RFC 6376 §3.1 ABNF (`sub-domain *("." sub-domain)` — common for time-rotated keys like `2024.s1`). **SPF (`b.mail.spf`)** — refuses domains that publish multiple `v=spf1` TXT records with `permerror` per RFC 7208 §4.5 (most operators don't realize multi-record SPF was always invalid; this surfaces the misconfig instead of silently picking the first); `include:` mechanism now permerrors when the included domain has no SPF record per RFC 7208 §5.2 (closes the silent-authorization class where `include:gone-domain.example` followed by `+all` would silently allow). **MTA-STS (`b.smtpPolicy.mtaSts.fetch`)** — now requires the `_mta-sts.<domain>` TXT precondition record per RFC 8461 §3.1 before fetching the HTTPS policy (closes the silent-escalation class where the framework would fetch policies for domains that never opted in, AND defeats operator-side rotation when the `id=` in the TXT changes); cache TTL is now bounded by the policy's `max_age` value per RFC 8461 §3.2 (clamped between 1 hour floor and 1 year ceiling) instead of the framework's hardcoded 60-min default. **OCSP (`b.network.tls.ocsp.evaluate`)** — `evaluateOcspResponse` now enforces the `thisUpdate` / `nextUpdate` time window per RFC 6960 §4.2.2.1, rejecting responses whose validity window has expired or hasn't started yet (with operator-tunable `clockSkewMs`, default 5 min). Pre-v0.8.16 a captured "good" response could replay forever even after the cert was revoked; this defeats `requireGood` posture. Bug fix only — no new operator-facing primitives, no surface change beyond the additional refusals.
package/lib/db-query.js CHANGED
@@ -117,6 +117,33 @@ class Query {
117
117
  value = lookup.value;
118
118
  }
119
119
  cryptoField && _validateField(field);
120
+ if (op === "IN") {
121
+ // node:sqlite ? does not support array-binding. Pre-v0.8.18
122
+ // `where(field, "IN", [1,2,3])` silently bound the entire
123
+ // array to a single placeholder and matched zero rows.
124
+ // Expand to (?, ?, ?) and push each value separately.
125
+ if (!Array.isArray(value) || value.length === 0) {
126
+ throw new Error("where IN requires a non-empty array of values");
127
+ }
128
+ var placeholders = value.map(function () { return "?"; }).join(", ");
129
+ this._where.push('"' + field + '" IN (' + placeholders + ")");
130
+ for (var i = 0; i < value.length; i += 1) this._whereParams.push(value[i]);
131
+ return this;
132
+ }
133
+ if (op === "LIKE" && typeof value === "string") {
134
+ // Escape SQL LIKE metacharacters % and _ in operator-supplied
135
+ // input. Without this, a single `%` in untrusted input becomes
136
+ // a wildcard that matches everything — a column-disclosure
137
+ // class (`q=%@%` enumerates entire table). Use a backslash as
138
+ // the escape character (uniform across SQLite + Postgres) and
139
+ // emit the corresponding ESCAPE clause so the engine treats it
140
+ // as the escape token. Operators who deliberately want LIKE
141
+ // wildcards in their value bypass via whereRaw().
142
+ var escaped = value.replace(/[\\%_]/g, "\\$&");
143
+ this._where.push('"' + field + '" LIKE ? ESCAPE ' + "'\\\\'");
144
+ this._whereParams.push(escaped);
145
+ return this;
146
+ }
120
147
  this._where.push('"' + field + '" ' + op + " ?");
121
148
  this._whereParams.push(value);
122
149
  return this;
@@ -152,7 +179,14 @@ class Query {
152
179
  throw new Error("whereRaw: sql must be a non-empty string");
153
180
  }
154
181
  var p = Array.isArray(params) ? params : (params == null ? [] : [params]);
155
- var holders = (sql.match(/\?/g) || []).length;
182
+ // Count `?` placeholders, but skip occurrences inside string
183
+ // literals ('...' or "..."), line comments (-- to EOL), and
184
+ // block comments (/* ... */). Pre-v0.8.18 the naive regex
185
+ // counted `?` inside literals (e.g. `WHERE name = 'a?b' AND id
186
+ // = ?`) which caused mismatched-count errors OR — worse — let
187
+ // through fragments where the literal-`?` placebo masked a
188
+ // missed real placeholder.
189
+ var holders = _countPlaceholders(sql);
156
190
  if (holders !== p.length) {
157
191
  throw new Error("whereRaw: " + holders + " placeholder(s) in sql but " +
158
192
  p.length + " param(s) supplied");
@@ -392,6 +426,46 @@ class Query {
392
426
  }
393
427
  }
394
428
 
429
+ // Count `?` placeholders outside string literals + comments.
430
+ // Tracks SQL single-quoted, double-quoted, line-comment, and block-
431
+ // comment state to avoid counting `?` characters that are part of
432
+ // literal text the SQL engine never interprets as a binding marker.
433
+ function _countPlaceholders(sql) {
434
+ var count = 0;
435
+ var i = 0;
436
+ var len = sql.length;
437
+ while (i < len) {
438
+ var ch = sql.charAt(i);
439
+ var next = i + 1 < len ? sql.charAt(i + 1) : "";
440
+ if (ch === "'" || ch === '"') {
441
+ var quote = ch;
442
+ i += 1;
443
+ while (i < len) {
444
+ if (sql.charAt(i) === quote) {
445
+ // SQL doubles the quote char to escape it within a literal.
446
+ if (sql.charAt(i + 1) === quote) { i += 2; continue; }
447
+ i += 1; break;
448
+ }
449
+ i += 1;
450
+ }
451
+ continue;
452
+ }
453
+ if (ch === "-" && next === "-") {
454
+ while (i < len && sql.charAt(i) !== "\n") i += 1;
455
+ continue;
456
+ }
457
+ if (ch === "/" && next === "*") {
458
+ i += 2;
459
+ while (i < len && !(sql.charAt(i) === "*" && sql.charAt(i + 1) === "/")) i += 1;
460
+ i += 2;
461
+ continue;
462
+ }
463
+ if (ch === "?") count += 1;
464
+ i += 1;
465
+ }
466
+ return count;
467
+ }
468
+
395
469
  function _validateField(field) {
396
470
  if (typeof field !== "string" ||
397
471
  field.length === 0 ||
package/lib/db.js CHANGED
@@ -744,13 +744,20 @@ async function init(opts) {
744
744
  }
745
745
  }
746
746
 
747
- // Refuse app schema entries that collide with framework-reserved names
747
+ // Refuse app schema entries that collide with framework-reserved names.
748
+ // Pre-v0.8.18 this was an exact-match Set; an app could ship
749
+ // `_blamejs_audit_log_archive` (or similar prefix-collision) and the
750
+ // framework would silently provision it next to the reserved
751
+ // namespace, allowing a row-by-row look-alike attack against audit
752
+ // archive tooling.
748
753
  for (var ri = 0; ri < opts.schema.length; ri++) {
749
- if (RESERVED_TABLE_NAMES.has(opts.schema[ri].name)) {
754
+ var appName = opts.schema[ri].name;
755
+ if (RESERVED_TABLE_NAMES.has(appName) ||
756
+ (typeof appName === "string" && appName.indexOf("_blamejs_") === 0)) {
750
757
  throw new DbError("db/reserved-table-name",
751
- "table name '" + opts.schema[ri].name + "' is reserved by the framework. " +
758
+ "table name '" + appName + "' is reserved by the framework. " +
752
759
  "Pick a different name (the framework provisions audit_log, consent_log, " +
753
- "and _blamejs_* tables automatically).");
760
+ "and any '_blamejs_*'-prefixed tables automatically).");
754
761
  }
755
762
  }
756
763
 
package/lib/safe-sql.js CHANGED
@@ -58,6 +58,12 @@ var BANNED_IDENTIFIERS = new Set([
58
58
  "where", "from", "join", "into", "values", "table", "database",
59
59
  "schema", "index", "view", "trigger", "procedure", "function",
60
60
  "begin", "commit", "rollback", "savepoint",
61
+ // SQLite-specific commands that escape the parameterized-query
62
+ // model. attach/detach mount external databases; pragma changes
63
+ // PRAGMAs (foreign_keys / cell_size_check / trusted_schema /
64
+ // journal_mode etc.) which can disable security-relevant
65
+ // protections; analyze / vacuum drop or rewrite indexes.
66
+ "pragma", "attach", "detach", "analyze", "vacuum", "reindex",
61
67
  ]);
62
68
 
63
69
  // Default identifier shape — Postgres NAMEDATALEN (63 chars) is the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.17",
3
+ "version": "0.8.18",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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:d988e730-a00b-4cd0-990a-763f21f74dd3",
5
+ "serialNumber": "urn:uuid:1751cf74-fcc9-495e-a159-bac5eb2565b3",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-07T07:32:59.512Z",
8
+ "timestamp": "2026-05-07T07:40:58.458Z",
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.8.17",
22
+ "bom-ref": "@blamejs/core@0.8.18",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.17",
25
+ "version": "0.8.18",
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.8.17",
29
+ "purl": "pkg:npm/%40blamejs/core@0.8.18",
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.8.17",
57
+ "ref": "@blamejs/core@0.8.18",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]