@blamejs/core 0.15.35 → 0.15.37

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,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.15.x
10
10
 
11
+ - v0.15.37 (2026-06-27) — **Several numeric options that silently accepted a non-finite value — disabling a clock-skew / freshness check or a resource cap — now reject it, so an `Infinity` skew or limit can no longer turn off the protection it configures.** A number of configuration options validated a numeric value with a bare `typeof === "number" && value >= 0` check, which accepts `Infinity`. Where the value is a clock-skew tolerance or a resource cap, an `Infinity` (or `NaN`) silently disabled the very check it tunes: a CWT / OCSP-staple / ARC clock-skew of `Infinity` made the expiry, freshness, and expiration comparisons unsatisfiable (an expired token / a replayed pre-revocation "good" response / an expired ARC seal would be accepted); a WebSocket-client `maxMessageBytes` / `maxFrameBytes` / `handshakeTimeoutMs` of `Infinity` disabled the inbound-OOM and stalled-handshake defenses; and inbox / flag-cache / audit-chain size and count caps of `Infinity` admitted unbounded data. These options now route through the finite-bounds validator: a present non-finite value is refused at the entry point (or falls back to the safe default on the result-returning paths). Options where an unbounded value is a deliberate intent — reconnect "retry indefinitely", inbox "retain indefinitely" — continue to accept `Infinity`. **Security:** *A non-finite clock-skew no longer disables CWT / OCSP / ARC time checks* — b.cwt.verify, the OCSP-staple freshness check in b.network.tls, and b.mail.arc.verify each took an operator clock-skew tolerance validated as `typeof === "number" && >= 0`, which accepts `Infinity`. Because each check is of the form `now > deadline + skew`, a skew of `Infinity` made it unsatisfiable and silently turned the check off: an expired CWT verified, a stale (post-nextUpdate) OCSP "good" response — the exact reply an attacker replays after the certificate is revoked — was accepted, and an expired ARC seal passed. A present clock-skew that is not a non-negative finite integer is now refused (b.cwt.verify throws cwt/bad-clock-skew; the OCSP and ARC paths fall back to their safe default). Regression tests assert an expired token / stale staple / expired ARC seal is still rejected when the skew is `Infinity`. · *WebSocket-client inbound caps can no longer be disabled by an Infinity value* — b.wsClient.connect validated maxMessageBytes, maxFrameBytes, and handshakeTimeoutMs (and the ping/pong keepalive intervals) with a bare numeric check that accepted `Infinity`, which disabled the inbound-message and frame size limits — the defenses against a malicious server sending an unbounded message — and the handshake timeout. A present non-finite value for these is now refused at connect time. The reconnect maxAttempts still accepts `Infinity` (a deliberate "reconnect indefinitely" intent). · *Inbox, flag-cache, and audit-chain caps reject a non-finite limit* — The inbox maxPayloadBytes / messageIdMaxLen / sourceMaxLen caps, the flag-cache ttlMs / maxEntries, and the audit-chain partition fan-out cap each accepted `Infinity`, disabling the cap (unbounded stored payloads, a never-expiring or unbounded cache, unbounded fan-out). These now require a positive finite integer — refused at create time, or clamped to the bounded default on the result-returning verify path. The inbox retentionDays still accepts `Infinity` (retain indefinitely).
12
+
13
+ - v0.15.36 (2026-06-27) — **Internal test-suite hardening only — the published library's runtime behavior and public API are unchanged (source-comment marker text aside).** The codebase-patterns guard class that allows a bare comma/semicolon split on token-only RFC header grammars was re-verified from scratch and renamed to a descriptive token, with its old name recorded as retired. Each live marked site (RRULE, RFC 9421 component identifiers, TLS-RPT rua, SCIM attribute paths) was re-read and confirmed to split a grammar with no quoted-string members. Five marker comments that suppressed nothing were removed or turned into plain explanatory comments. No runtime code, public API, or wire format changed. **Detectors:** *Bare token-only header-split suppression class re-verified, renamed, and pruned of inert markers* — Each marked bare `.split(",")` / `.split(";")` on an RFC header value was re-read and confirmed to operate on a token-only grammar (no quoted-string members, so a quote-aware splitter is unnecessary): RFC 5545 RRULE, RFC 9421 signature component identifiers, RFC 8460 TLS-RPT rua, and RFC 7644 SCIM attribute paths. The guard class was renamed to a descriptive token and its old name added to the retired-token set. Five marker comments that the detector never actually evaluated (two sat on a date-normalizing `.replace`, three in header parsers the guard intentionally does not scan) were removed or converted to plain comments. This is test-suite tooling plus source-comment text; no shipped framework behavior changed.
14
+
11
15
  - v0.15.35 (2026-06-26) — **`b.metrics` shadow registry no longer lets a comma in a label value forge extra Prometheus label pairs — a label-injection that downstream tenant-scoping or authorization selectors could be tricked by.** The shadow registry serialized a metric's label set into a `name=value,name=value` string key and re-split it on `,` to build the Prometheus exposition. A label VALUE containing a comma therefore split into multiple label pairs, so a single operator-set label could forge additional label pairs in the scrape output (for example turning one `route` label into a `route` plus an attacker-named label), which downstream tenant-scoping filters, authorization selectors, recording rules, and alerting trust as a boundary; two distinct label sets could also collide into one cardinality bucket. The label key now uses canonical-JSON of the string-coerced label set (the same approach the main registry already used) and the render path emits the structured label set kept alongside that key rather than splitting or re-parsing, so a `,` or `=` inside a label value stays inside the value and a label NAMED `constructor`, `prototype`, or `__proto__` (all valid Prometheus label names) is preserved instead of being dropped. Note: the cardinality keys exposed by `shadowRegistry().snapshot()` are now canonical-JSON strings (e.g. `{"route":"/api"}`) rather than `route=/api`. **Changed:** *shadowRegistry().snapshot() cardinality keys are canonical-JSON* — As a consequence of the label-injection fix, the per-series cardinality keys in a shadow-registry snapshot are now canonical-JSON strings (e.g. `{"tenant":"a"}`) instead of the previous `tenant=a` form. Code that reads those keys directly from snapshot() should parse them as JSON; the Prometheus render output is unchanged for conforming label values. **Security:** *Label values can no longer forge extra Prometheus label pairs in the shadow registry* — b.metrics shadowRegistry built a metric's cardinality key by joining `name=value` pairs with commas, then split that string on commas when rendering the Prometheus exposition. A comma (and `=`) in a label value therefore broke one value into several forged label pairs in the scrape output, and two different label sets could collide into the same cardinality bucket. The key is now canonical-JSON of the string-coerced label set and the render emits the structured label set kept alongside that key rather than splitting or re-parsing, so a label value is emitted as a single quoted value with its commas/equals intact, distinct label sets get distinct keys, and a label NAMED `constructor`, `prototype`, or `__proto__` (each a valid Prometheus label name) is preserved rather than dropped. Deterministic regression tests assert a comma-bearing label value produces exactly one label pair and that the reserved-name labels survive render.
12
16
 
13
17
  - v0.15.34 (2026-06-26) — **`b.mail.crypto.pgp.verify` now accepts every valid RSA OpenPGP signature — one whose value happened to have a high zero byte (about 1 in 256) was being rejected.** An OpenPGP RSA signature is an integer modulo the key's modulus, and ~1 in 256 signatures have a value that begins with a zero byte. The MPI encoding strips those leading zero bytes (RFC 9580 §3.2), but b.mail.crypto.pgp.verify passed the stripped value straight to the RSA verification, which requires a signature exactly the modulus byte length — so a perfectly valid signature was intermittently reported invalid. The signature is now left-padded back to the modulus width before verification, exactly as the Ed25519 path already pads its components. Verification of every valid RSA signature is now reliable, including signatures produced by other OpenPGP implementations. Also includes internal test-tooling: two more guard-suite suppression classes were re-verified and their tokens retired. **Fixed:** *RSA OpenPGP signatures with a high zero byte now verify reliably* — b.mail.crypto.pgp.verify read the RSA signature MPI (whose leading zero bytes the OpenPGP wire format strips) and handed it to the RSA verification without restoring the stripped bytes. Node's RSA verify requires the signature to be exactly the modulus byte length, so a signature whose value had one or more high zero bytes — about 1 in 256 of all signatures, for any key — was rejected as invalid even though it was correct. The signature is now left-padded to the modulus width before verification (the same correction the Ed25519 verification path already applied to its R/S components). This affects signatures the framework produces and signatures from other OpenPGP implementations alike. A deterministic regression test searches for a short-MPI signature and asserts it verifies. **Detectors:** *Two more suppression-marker classes re-verified and their tokens retired* — The raw-hash-compare and seal-without-aad guard classes were re-read and confirmed (a data-residency region tag compared with === — not a secret; and two intentional non-AEAD-bound seals — a non-regulated plain-mode column whose AAD is enforced by the posture seal-envelope floor where it matters, and a throwaway vault-readiness probe), then renamed to descriptive tokens with their old names added to the retired-token set. Test-suite tooling only.
@@ -124,7 +124,7 @@ function create(opts) {
124
124
  var auditImpl = opts.audit || audit();
125
125
  var declaredRegimes = Object.create(null);
126
126
  for (var i = 0; i < BUILTIN_REGIMES.length; i += 1) declaredRegimes[BUILTIN_REGIMES[i]] = true;
127
- // allow:numeric-opt-Infinity — operator opt clamped to [1, DEFAULT_MAX_HOP_COUNT]; bad input falls back to default
127
+ // allow:numeric-opt-Infinity-intentional — operator opt clamped to [1, DEFAULT_MAX_HOP_COUNT] (the `<= DEFAULT_MAX_HOP_COUNT` upper bound rejects Infinity); bad input falls back to default
128
128
  var maxHopCount = typeof opts.maxHopCount === "number" && opts.maxHopCount > 0 &&
129
129
  opts.maxHopCount <= DEFAULT_MAX_HOP_COUNT
130
130
  ? Math.floor(opts.maxHopCount)
@@ -37,6 +37,7 @@ var canonicalJson = require("./canonical-json");
37
37
  var C = require("./constants");
38
38
  var clusterStorage = require("./cluster-storage");
39
39
  var frameworkSchema = require("./framework-schema");
40
+ var numericBounds = require("./numeric-bounds");
40
41
  var sql = require("./sql");
41
42
  var safeSql = require("./safe-sql");
42
43
  var safeBuffer = require("./safe-buffer");
@@ -305,7 +306,10 @@ async function verifyChain(queryAllAsync, tableName, opts) {
305
306
  // coerce so a Postgres INTEGER/BIGINT chainKey is type-stable in the
306
307
  // reported break-shape and the per-key WHERE bind, matching SQLite.
307
308
  var keyRows = frameworkSchema.coerceRows(await queryAllAsync(keysBuilt.sql, keysBuilt.params));
308
- var maxChains = (typeof opts.maxChains === "number" && opts.maxChains > 0) ? opts.maxChains : 100000; // allow:numeric-opt-Infinity — partition fan-out cap; non-number / <=0 falls back to the default
309
+ // Partition fan-out cap; a non-finite / <= 0 / non-integer value (Infinity
310
+ // would make the `keyRows.length > maxChains` cap unsatisfiable) falls back
311
+ // to the bounded default rather than disabling the cap.
312
+ var maxChains = numericBounds.isPositiveFiniteInt(opts.maxChains) ? opts.maxChains : 100000;
309
313
  if (keyRows.length > maxChains) {
310
314
  return {
311
315
  ok: false,
package/lib/calendar.js CHANGED
@@ -1119,7 +1119,7 @@ function _firstParamValue(prop, paramName) {
1119
1119
 
1120
1120
  function _icalRruleToJscal(rrule) {
1121
1121
  var out = { "@type": "RecurrenceRule", frequency: "daily" };
1122
- var parts = String(rrule).split(";"); // allow:bare-split-on-quoted-header — RFC 5545 RRULE grammar has no quoted-string members; values are token-only
1122
+ var parts = String(rrule).split(";"); // allow:bare-split-on-quoted-header-token-grammar — RFC 5545 RRULE grammar has no quoted-string members; values are token-only
1123
1123
  for (var i = 0; i < parts.length; i += 1) {
1124
1124
  var kv = parts[i].split("=");
1125
1125
  if (kv.length !== 2) continue;
@@ -1129,11 +1129,11 @@ function _icalRruleToJscal(rrule) {
1129
1129
  else if (key === "INTERVAL") out.interval = parseInt(val, 10);
1130
1130
  else if (key === "COUNT") out.count = parseInt(val, 10);
1131
1131
  else if (key === "UNTIL") out.until = _icalDateTimeToUtc(val);
1132
- else if (key === "BYDAY") out.byDay = val.split(",").map(function (d) { // allow:bare-split-on-quoted-header — RFC 5545 BYDAY values are token-only
1132
+ else if (key === "BYDAY") out.byDay = val.split(",").map(function (d) { // allow:bare-split-on-quoted-header-token-grammar — RFC 5545 BYDAY values are token-only
1133
1133
  return { "@type": "NDay", day: d.slice(-2).toLowerCase() };
1134
1134
  });
1135
- else if (key === "BYMONTH") out.byMonth = val.split(","); // allow:bare-split-on-quoted-header — RFC 5545 BYMONTH values are integer-only
1136
- else if (key === "BYMONTHDAY") out.byMonthDay = val.split(",").map(function (n) { return parseInt(n, 10); }); // allow:bare-split-on-quoted-header — RFC 5545 BYMONTHDAY values are integer-only
1135
+ else if (key === "BYMONTH") out.byMonth = val.split(","); // allow:bare-split-on-quoted-header-token-grammar — RFC 5545 BYMONTH values are integer-only
1136
+ else if (key === "BYMONTHDAY") out.byMonthDay = val.split(",").map(function (n) { return parseInt(n, 10); }); // allow:bare-split-on-quoted-header-token-grammar — RFC 5545 BYMONTHDAY values are integer-only
1137
1137
  }
1138
1138
  return out;
1139
1139
  }
@@ -1168,13 +1168,13 @@ function _utcDateTimeToIcal(s) {
1168
1168
  // JSCalendar UTCDateTime "2026-05-22T10:00:00.123Z" →
1169
1169
  // "20260522T100000Z" (RFC 5545 §3.3.5 form 2 has NO fractional
1170
1170
  // seconds; strict ICS consumers reject `T100000.123Z`).
1171
- return String(s).replace(/\.\d+/, "").replace(/[-:]/g, ""); // allow:bare-split-on-quoted-header — not a header split
1171
+ return String(s).replace(/\.\d+/, "").replace(/[-:]/g, "");
1172
1172
  }
1173
1173
 
1174
1174
  function _localDateTimeToIcal(s) {
1175
1175
  // JSCalendar LocalDateTime "2026-05-22T09:00:00.123" →
1176
1176
  // "20260522T090000" (same fractional-second strip as the UTC form).
1177
- return String(s).replace(/\.\d+/, "").replace(/[-:]/g, ""); // allow:bare-split-on-quoted-header — not a header split
1177
+ return String(s).replace(/\.\d+/, "").replace(/[-:]/g, "");
1178
1178
  }
1179
1179
 
1180
1180
  function _isUtcDateTime(s) {
package/lib/cwt.js CHANGED
@@ -39,6 +39,7 @@
39
39
  var cose = require("./cose");
40
40
  var cbor = require("./cbor");
41
41
  var C = require("./constants");
42
+ var numericBounds = require("./numeric-bounds");
42
43
  var validateOpts = require("./validate-opts");
43
44
  var { defineClass } = require("./framework-error");
44
45
 
@@ -191,7 +192,11 @@ async function verify(cwt, opts) {
191
192
  }
192
193
 
193
194
  // Time claims (NumericDate, seconds). Skew tolerance both directions.
194
- var skew = (typeof opts.clockSkewSec === "number" && opts.clockSkewSec >= 0) ? opts.clockSkewSec : 60; // allow:numeric-opt-Infinity — clamped non-negative, else default / allow:raw-time-literal clock-skew in seconds (NumericDate units), not a ms duration
195
+ // A present clockSkewSec must be a non-negative finite integeran
196
+ // Infinity / NaN / negative skew would otherwise make `now > exp + skew`
197
+ // unsatisfiable and silently disable the expiry / not-before checks.
198
+ numericBounds.requireNonNegativeFiniteIntIfPresent(opts.clockSkewSec, "cwt.verify: opts.clockSkewSec", CwtError, "cwt/bad-clock-skew");
199
+ var skew = (typeof opts.clockSkewSec === "number") ? opts.clockSkewSec : 60; // allow:raw-time-literal — clock-skew in seconds (NumericDate units), not a ms duration
195
200
  var now = _nowSec(opts);
196
201
  // A present exp / nbf MUST be a well-formed NumericDate — a non-numeric
197
202
  // value would otherwise bypass the time check entirely (a token could
@@ -1338,8 +1338,9 @@ async function transaction(fn, opts) {
1338
1338
  // (serialization_failure) are transient — retry with capped attempts
1339
1339
  // and a small jittered backoff. Operators tune retries via opts.deadlockRetries (default 3).
1340
1340
  // numeric-bounds doesn't have a non-negative-int helper; use a
1341
- // direct check with allow marker (zero is permitted to disable
1342
- // retries entirely).
1341
+ // Explicit isFinite/integer guard (zero is permitted to disable retries
1342
+ // entirely); a non-finite deadlockRetries is refused here, so Math.floor
1343
+ // below only ever sees a validated finite integer.
1343
1344
  if (opts.deadlockRetries !== undefined) {
1344
1345
  if (typeof opts.deadlockRetries !== "number" || !isFinite(opts.deadlockRetries) ||
1345
1346
  opts.deadlockRetries < 0 || (opts.deadlockRetries | 0) !== opts.deadlockRetries) {
@@ -1348,7 +1349,7 @@ async function transaction(fn, opts) {
1348
1349
  }
1349
1350
  }
1350
1351
  var maxRetries = (typeof opts.deadlockRetries === "number")
1351
- ? Math.floor(opts.deadlockRetries) : 3; // allow:numeric-opt-Infinity
1352
+ ? Math.floor(opts.deadlockRetries) : 3;
1352
1353
  // Validate the transaction-level residency tag shape at entry (the
1353
1354
  // sessionGucs / deadlockRetries discipline) so an empty-string tag
1354
1355
  // fails before BEGIN rather than at the first statement.
package/lib/flag-cache.js CHANGED
@@ -24,6 +24,7 @@
24
24
  var validateOpts = require("./validate-opts");
25
25
  var lazyRequire = require("./lazy-require");
26
26
  var C = require("./constants");
27
+ var numericBounds = require("./numeric-bounds");
27
28
  var { defineClass } = require("./framework-error");
28
29
  var FlagError = defineClass("FlagError", { alwaysPermanent: true });
29
30
 
@@ -36,18 +37,17 @@ function cache(downstream, opts) {
36
37
  throw new FlagError("flag/bad-cache",
37
38
  "cache: downstream provider must implement .evaluate()");
38
39
  }
39
- // allow:numeric-opt-Infinity defaults clamped + ttl-floor enforced below
40
- var ttlMs = (typeof opts.ttlMs === "number" && opts.ttlMs > 0)
41
- ? opts.ttlMs
42
- : C.TIME.seconds(30);
40
+ // A present ttlMs / maxEntries must be a positive finite integer — Infinity
41
+ // would pass a bare `typeof === "number" && > 0` check and give a
42
+ // never-expiring entry (ttlMs) or an unbounded cache (maxEntries).
43
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.ttlMs, "flag.cache: opts.ttlMs", FlagError, "flag/bad-cache");
44
+ var ttlMs = (typeof opts.ttlMs === "number") ? opts.ttlMs : C.TIME.seconds(30);
43
45
  if (ttlMs < C.TIME.seconds(1)) {
44
46
  throw new FlagError("flag/bad-cache",
45
47
  "cache: ttlMs must be >= 1000ms - got " + ttlMs);
46
48
  }
47
- // allow:numeric-opt-Infinity maxEntries default + Math.floor coerce; throws on bad type at config time
48
- var maxEntries = (typeof opts.maxEntries === "number" && opts.maxEntries > 0)
49
- ? Math.floor(opts.maxEntries)
50
- : 10000; // entry-count default
49
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.maxEntries, "flag.cache: opts.maxEntries", FlagError, "flag/bad-cache");
50
+ var maxEntries = (typeof opts.maxEntries === "number") ? opts.maxEntries : 10000; // entry-count default
51
51
  var auditOn = opts.audit === true; // off by default — too chatty
52
52
  var entries = new Map();
53
53
  var hits = 0;
@@ -212,7 +212,7 @@ function _buildCacheKey(method, url, varyHeaderValues) {
212
212
  // uncacheable.
213
213
  function _extractVaryValues(varyHeader, requestHeaders) {
214
214
  if (typeof varyHeader !== "string" || varyHeader.length === 0) return [];
215
- var names = varyHeader.split(",").map(function (s) { // allow:bare-split-on-quoted-header — RFC 9110 §12.5.5 Vary is a comma-list of field-names (token grammar); no quoted-string
215
+ var names = varyHeader.split(",").map(function (s) { // RFC 9110 §12.5.5 Vary is a comma-list of field-names (token grammar); no quoted-string, so a bare split is correct
216
216
  return s.trim().toLowerCase();
217
217
  }).filter(function (s) { return s.length > 0; });
218
218
  if (names.indexOf("*") !== -1) return null; // sentinel: "uncacheable"
@@ -262,7 +262,7 @@ function _evaluateStorage(method, statusCode, responseHeaders, sharedCache) {
262
262
 
263
263
  // Vary: * is uncacheable per RFC 9110 §12.5.5.
264
264
  if (typeof varyHeader === "string" && varyHeader.indexOf("*") !== -1) {
265
- var trimmed = varyHeader.split(",").map(function (s) { return s.trim(); }); // allow:bare-split-on-quoted-header — RFC 9110 §12.5.5 Vary field-names; token grammar only
265
+ var trimmed = varyHeader.split(",").map(function (s) { return s.trim(); }); // RFC 9110 §12.5.5 Vary field-names; token grammar only, so a bare split is correct
266
266
  if (trimmed.indexOf("*") !== -1) {
267
267
  return { cacheable: false, reason: "vary-star", freshnessMs: -1, directives: directives, varyHeader: varyHeader };
268
268
  }
@@ -319,7 +319,7 @@ function sign(msg, opts) {
319
319
  // header isn't already supplied. Operators wanting to use the
320
320
  // RFC 9530 "sha-512" identifier (SHA-512 instead of SHA3-512) supply
321
321
  // the header themselves; the framework emits SHA3-512.
322
- var coveredLower = opts.covered.map(function (c) { return c.split(";")[0].toLowerCase(); }); // allow:bare-split-on-quoted-header — opts.covered is operator-supplied component-id list (e.g. "content-digest;sf"); component identifiers are RFC 9421 §2.1 derived-field names with token-only grammar; no quoted-string
322
+ var coveredLower = opts.covered.map(function (c) { return c.split(";")[0].toLowerCase(); }); // allow:bare-split-on-quoted-header-token-grammar — opts.covered is operator-supplied component-id list (e.g. "content-digest;sf"); component identifiers are RFC 9421 §2.1 derived-field names with token-only grammar; no quoted-string
323
323
  if (coveredLower.indexOf("content-digest") !== -1 &&
324
324
  _resolveHeader(m.headers, "content-digest") === null) {
325
325
  if (m.body == null) {
@@ -451,7 +451,7 @@ function _parseSignature(headerValue, label) {
451
451
  if (headerValue.indexOf(prefix) !== 0) {
452
452
  // Multiple signature labels can appear; comma-separated. Find the
453
453
  // matching label.
454
- var parts = headerValue.split(","); // allow:bare-split-on-quoted-header — RFC 9421 §2.4 Signature header values are `label=:b64:` form; base64 alphabet excludes `,` and the label tokens are RFC 8941 §3.3.4 sf-token (no DQUOTE in practice)
454
+ var parts = headerValue.split(","); // allow:bare-split-on-quoted-header-token-grammar — RFC 9421 §2.4 Signature header values are `label=:b64:` form; base64 alphabet excludes `,` and the label tokens are RFC 8941 §3.3.4 sf-token (no DQUOTE in practice)
455
455
  for (var i = 0; i < parts.length; i++) {
456
456
  var p = parts[i].trim();
457
457
  if (p.indexOf(prefix) === 0) {
@@ -519,7 +519,7 @@ function verify(msg, opts) {
519
519
  // If content-digest is covered, recompute and compare. RFC 9421 §B.2.5
520
520
  // mandates that verifiers re-run the digest over the body — a stale
521
521
  // header from a proxy would otherwise verify trivially.
522
- var coveredLower = parsedInput.covered.map(function (c) { return c.split(";")[0].toLowerCase(); }); // allow:bare-split-on-quoted-header — same as sign() above: covered items are RFC 9421 §2.1 component-ids, token grammar
522
+ var coveredLower = parsedInput.covered.map(function (c) { return c.split(";")[0].toLowerCase(); }); // allow:bare-split-on-quoted-header-token-grammar — same as sign() above: covered items are RFC 9421 §2.1 component-ids, token grammar
523
523
  if (coveredLower.indexOf("content-digest") !== -1) {
524
524
  if (m.body == null) {
525
525
  return { valid: false, reason: "content-digest-no-body" };
package/lib/inbox.js CHANGED
@@ -44,6 +44,7 @@
44
44
 
45
45
  var C = require("./constants");
46
46
  var codepointClass = require("./codepoint-class");
47
+ var numericBounds = require("./numeric-bounds");
47
48
  var lazyRequire = require("./lazy-require");
48
49
  var safeJson = require("./safe-json");
49
50
  var safeSql = require("./safe-sql");
@@ -155,15 +156,17 @@ function create(opts) {
155
156
  // The table identifier reaches SQL through b.sql, which validates +
156
157
  // quotes it by construction on every emitted statement; _validateTableName
157
158
  // above fails fast at create() time on a bad name.
158
- var retentionDays = (typeof opts.retentionDays === "number" && opts.retentionDays > 0) // allow:numeric-opt-Infinity
159
+ var retentionDays = (typeof opts.retentionDays === "number" && opts.retentionDays > 0) // allow:numeric-opt-Infinity-intentional — Infinity = retain indefinitely, a supported intent
159
160
  ? opts.retentionDays : 30; // default retention days
160
161
  var auditOn = opts.audit !== false;
161
- var maxPayloadBytes = (typeof opts.maxPayloadBytes === "number" && opts.maxPayloadBytes > 0) // allow:numeric-opt-Infinity
162
- ? opts.maxPayloadBytes : C.BYTES.kib(64);
163
- var messageIdMaxLen = (typeof opts.messageIdMaxLen === "number" && opts.messageIdMaxLen > 0) // allow:numeric-opt-Infinity
164
- ? opts.messageIdMaxLen : 256; // message-id length cap
165
- var sourceMaxLen = (typeof opts.sourceMaxLen === "number" && opts.sourceMaxLen > 0) // allow:numeric-opt-Infinity
166
- ? opts.sourceMaxLen : 256; // source length cap
162
+ // The byte / length caps must be positive finite integers Infinity would
163
+ // disable the cap and admit unbounded stored payloads / identifiers.
164
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
165
+ ["maxPayloadBytes", "messageIdMaxLen", "sourceMaxLen"],
166
+ "inbox.create", InboxError, "inbox/bad-opt");
167
+ var maxPayloadBytes = (typeof opts.maxPayloadBytes === "number") ? opts.maxPayloadBytes : C.BYTES.kib(64);
168
+ var messageIdMaxLen = (typeof opts.messageIdMaxLen === "number") ? opts.messageIdMaxLen : 256; // message-id length cap
169
+ var sourceMaxLen = (typeof opts.sourceMaxLen === "number") ? opts.sourceMaxLen : 256; // source length cap
167
170
 
168
171
  function _emitAudit(action, outcome, metadata) {
169
172
  if (!auditOn) return;
@@ -346,6 +349,14 @@ function create(opts) {
346
349
  }
347
350
 
348
351
  async function sweep() {
352
+ // retentionDays: Infinity is the documented "retain indefinitely" intent —
353
+ // there is no horizon to age past, so sweep deletes nothing. (Computing a
354
+ // cutoff would otherwise throw: `new Date(Date.now() - Infinity)` is an
355
+ // Invalid Date, and "Infinity days" is not a valid Postgres interval.)
356
+ if (!isFinite(retentionDays)) {
357
+ _emitAudit("inbox.swept", "success", { deleted: 0, retentionDays: retentionDays });
358
+ return 0;
359
+ }
349
360
  var dialect = _sqlDialect(externalDb);
350
361
  var deleted = 0;
351
362
  await externalDb.transaction(async function (xdb) {
@@ -51,6 +51,7 @@
51
51
  var nodeCrypto = require("node:crypto");
52
52
  var lazyRequire = require("./lazy-require");
53
53
  var validateOpts = require("./validate-opts");
54
+ var numericBounds = require("./numeric-bounds");
54
55
  var safeBuffer = require("./safe-buffer");
55
56
  var dkim = require("./mail-dkim");
56
57
  var { defineClass } = require("./framework-error");
@@ -246,8 +247,11 @@ function sign(opts) {
246
247
  "sign: headersToSign[" + hi + "] must be a non-empty string");
247
248
  }
248
249
  }
249
- var timestamp = (typeof opts.timestamp === "number" && opts.timestamp > 0) // allow:numeric-opt-Infinity
250
- ? Math.floor(opts.timestamp) : Math.floor(Date.now() / 1000); // Unix epoch seconds divisor
250
+ // A present t= timestamp must be a positive finite integer (NumericDate
251
+ // seconds) an Infinity / NaN value would serialize into a malformed t= tag.
252
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.timestamp, "arc.sign: opts.timestamp", MailAuthError, "arc-sign/bad-timestamp");
253
+ var timestamp = (typeof opts.timestamp === "number")
254
+ ? opts.timestamp : Math.floor(Date.now() / 1000); // Unix epoch seconds divisor
251
255
  var auditOn = opts.audit !== false;
252
256
 
253
257
  var keyObject;
package/lib/mail-auth.js CHANGED
@@ -52,6 +52,7 @@ var structuredFields = require("./structured-fields");
52
52
  var markupEscape = require("./markup-escape").markupEscape;
53
53
  var bCrypto = require("./crypto");
54
54
  var C = require("./constants");
55
+ var numericBounds = require("./numeric-bounds");
55
56
  var dkim = require("./mail-dkim");
56
57
  var mimeParse = require("./mime-parse");
57
58
  var safeXml = require("./parsers/safe-xml");
@@ -1575,7 +1576,11 @@ async function arcVerify(rfc822, opts) {
1575
1576
  var anyFail = false;
1576
1577
  // RFC 8617 §5.2 — operator-tunable clock skew on t= (signing
1577
1578
  // timestamp) and x= (expiration) tags. Default 5 min.
1578
- var arcClockSkewMs = typeof opts.clockSkewMs === "number" && opts.clockSkewMs >= 0 // allow:numeric-opt-Infinity operator-supplied skew, default 5 min
1579
+ // A present clockSkewMs must be a non-negative finite integer; an Infinity /
1580
+ // NaN / negative skew makes `amsX + skewSec < nowSec` (and the t= future
1581
+ // check) unsatisfiable and silently disables the RFC 8617 §5.2 expiry /
1582
+ // future-timestamp gate. Non-finite falls to the default.
1583
+ var arcClockSkewMs = numericBounds.isNonNegativeFiniteInt(opts.clockSkewMs)
1579
1584
  ? opts.clockSkewMs : C.TIME.minutes(5);
1580
1585
  var nowSec = Math.floor(Date.now() / 1000); // Unix epoch seconds divisor
1581
1586
 
@@ -101,9 +101,9 @@ function create(opts) {
101
101
  "middleware.ageGate: opts.getAge must be a function (req) -> number | null");
102
102
  }
103
103
  var getAge = opts.getAge;
104
- var requireAge = (typeof opts.requireAge === "number" && opts.requireAge > 0) // allow:numeric-opt-Infinity — age is operator domain, not a bytes/time-shaped opt
104
+ var requireAge = (typeof opts.requireAge === "number" && opts.requireAge > 0) // allow:numeric-opt-Infinity-intentional — age threshold; an Infinity bound is fail-closed (denies everyone), never a bypass
105
105
  ? opts.requireAge : null;
106
- var consentRequired = (typeof opts.consentRequired === "number" && opts.consentRequired > 0) // allow:numeric-opt-Infinity — age threshold, not a bytes/time-shaped opt
106
+ var consentRequired = (typeof opts.consentRequired === "number" && opts.consentRequired > 0) // allow:numeric-opt-Infinity-intentional — age threshold; an Infinity bound is fail-closed (classifies everyone below-threshold), never a bypass
107
107
  ? opts.consentRequired : null;
108
108
  var hasParentalConsent = typeof opts.hasParentalConsent === "function" ? opts.hasParentalConsent : null;
109
109
  var skipPaths = Array.isArray(opts.skipPaths) ? opts.skipPaths.slice() : [];
@@ -351,7 +351,7 @@ function _detectSmuggling(req) {
351
351
  // (RFC 9112 §6.1). Anything else is a smuggling vector or
352
352
  // server-side decode error.
353
353
  if (typeof te === "string" && te.length > 0) {
354
- var tokens = te.toLowerCase().split(",").map(function (t) { return t.trim(); }); // allow:bare-split-on-quoted-header — RFC 9112 §6.1 Transfer-Encoding values (chunked / gzip / deflate / identity) are token-only; no quoted-string in the grammar
354
+ var tokens = te.toLowerCase().split(",").map(function (t) { return t.trim(); }); // RFC 9112 §6.1 Transfer-Encoding values (chunked / gzip / deflate / identity) are token-only; no quoted-string in the grammar, so a bare split is correct
355
355
  var last = tokens[tokens.length - 1];
356
356
  if (last !== "chunked") {
357
357
  return {
@@ -237,8 +237,8 @@ async function _dispatch(req, res, basePath, bearer, opts, maxPageSize, bulkCfg)
237
237
  count: pageSize,
238
238
  sortBy: query.sortBy || null,
239
239
  sortOrder: query.sortOrder || null,
240
- attributes: query.attributes ? query.attributes.split(",") : null, // allow:bare-split-on-quoted-header — RFC 7644 §3.9 attributes/excludedAttributes are SCIM attribute paths (URN-ish identifiers); grammar excludes DQUOTE
241
- excludedAttributes: query.excludedAttributes ? query.excludedAttributes.split(",") : null, // allow:bare-split-on-quoted-header — same SCIM attribute-name grammar
240
+ attributes: query.attributes ? query.attributes.split(",") : null, // allow:bare-split-on-quoted-header-token-grammar — RFC 7644 §3.9 attributes/excludedAttributes are SCIM attribute paths (URN-ish identifiers); grammar excludes DQUOTE
241
+ excludedAttributes: query.excludedAttributes ? query.excludedAttributes.split(",") : null, // allow:bare-split-on-quoted-header-token-grammar — same SCIM attribute-name grammar
242
242
  }, ctx);
243
243
  _writeJson(res, H.OK, {
244
244
  schemas: [SCIM_MESSAGE_LIST],
@@ -613,7 +613,7 @@ async function tlsRptFetchPolicy(domain, opts) {
613
613
  var rua = [];
614
614
  for (var p = 0; p < pairs.length; p += 1) {
615
615
  if (pairs[p][0] === "rua") {
616
- var uris = pairs[p][1].split(","); // allow:bare-split-on-quoted-header — allow:raw-time-literal — TLS-RPT rua grammar (RFC 8460 §3): rua = tlsrpt-uri *("," tlsrpt-uri); URIs percent-encode reserved chars, no quoted-string
616
+ var uris = pairs[p][1].split(","); // allow:bare-split-on-quoted-header-token-grammar — allow:raw-time-literal — TLS-RPT rua grammar (RFC 8460 §3): rua = tlsrpt-uri *("," tlsrpt-uri); URIs percent-encode reserved chars, no quoted-string
617
617
  for (var u = 0; u < uris.length; u += 1) {
618
618
  var uri = uris[u].trim();
619
619
  if (uri.length > 0) rua.push(uri);
@@ -1204,7 +1204,11 @@ function evaluateOcspResponse(ocspDer, opts) {
1204
1204
  // gets revoked, the attacker keeps presenting the cached "good" and
1205
1205
  // the framework keeps accepting it. requireGood postures depend on
1206
1206
  // freshness — reject expired or future-dated responses outright.
1207
- var clockSkewMs = typeof opts.clockSkewMs === "number" && opts.clockSkewMs >= 0 // allow:numeric-opt-Infinity operator-supplied skew, default 5 min if absent or invalid
1207
+ // A present clockSkewMs must be a non-negative finite integer; an Infinity /
1208
+ // NaN / negative skew would make the staleness check `now > nextUpdate + skew`
1209
+ // unsatisfiable and silently disable the freshness window — accepting a
1210
+ // replayed pre-revocation "good" response. Non-finite falls to the default.
1211
+ var clockSkewMs = numericBounds.isNonNegativeFiniteInt(opts.clockSkewMs)
1208
1212
  ? opts.clockSkewMs : C.TIME.minutes(5);
1209
1213
  var now = typeof opts.now === "number" ? opts.now : Date.now();
1210
1214
  // thisUpdate / nextUpdate are already unix-ms NUMBERS (parseOcspResponse →
package/lib/ws-client.js CHANGED
@@ -52,6 +52,7 @@ var { EventEmitter } = require("node:events");
52
52
 
53
53
  var lazyRequire = require("./lazy-require");
54
54
  var validateOpts = require("./validate-opts");
55
+ var numericBounds = require("./numeric-bounds");
55
56
  var safeAsync = require("./safe-async");
56
57
  var safeBuffer = require("./safe-buffer");
57
58
  var bCrypto = lazyRequire(function () { return require("./crypto"); });
@@ -217,16 +218,18 @@ function connect(target, opts) {
217
218
  "wsClient.connect: subprotocols[" + sp + "] must be a non-empty string");
218
219
  }
219
220
  }
220
- var pingMs = (typeof opts.pingMs === "number" && opts.pingMs > 0) // allow:numeric-opt-Infinity
221
- ? opts.pingMs : DEFAULT_PING_MS;
222
- var pongMs = (typeof opts.pongMs === "number" && opts.pongMs > 0) // allow:numeric-opt-Infinity
223
- ? opts.pongMs : DEFAULT_PONG_MS;
224
- var maxMessageBytes = (typeof opts.maxMessageBytes === "number" && opts.maxMessageBytes > 0) // allow:numeric-opt-Infinity
225
- ? opts.maxMessageBytes : DEFAULT_MAX_BYTES;
226
- var maxFrameBytes = (typeof opts.maxFrameBytes === "number" && opts.maxFrameBytes > 0) // allow:numeric-opt-Infinity
227
- ? opts.maxFrameBytes : DEFAULT_MAX_FRAME;
228
- var handshakeTimeoutMs = (typeof opts.handshakeTimeoutMs === "number" && opts.handshakeTimeoutMs > 0) // allow:numeric-opt-Infinity
229
- ? opts.handshakeTimeoutMs : DEFAULT_HANDSHAKE_TIMEOUT_MS;
221
+ // These are keepalive/timeout intervals and inbound-OOM caps; a present
222
+ // value must be a positive finite integer. A bare `typeof === "number" && > 0`
223
+ // check accepts Infinity, which silently disables the cap (a malicious server
224
+ // could then send an unbounded message/frame, or stall the handshake forever).
225
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
226
+ ["pingMs", "pongMs", "maxMessageBytes", "maxFrameBytes", "handshakeTimeoutMs"],
227
+ "wsClient.connect", WsClientError, "ws-client/bad-opt");
228
+ var pingMs = (typeof opts.pingMs === "number") ? opts.pingMs : DEFAULT_PING_MS;
229
+ var pongMs = (typeof opts.pongMs === "number") ? opts.pongMs : DEFAULT_PONG_MS;
230
+ var maxMessageBytes = (typeof opts.maxMessageBytes === "number") ? opts.maxMessageBytes : DEFAULT_MAX_BYTES;
231
+ var maxFrameBytes = (typeof opts.maxFrameBytes === "number") ? opts.maxFrameBytes : DEFAULT_MAX_FRAME;
232
+ var handshakeTimeoutMs = (typeof opts.handshakeTimeoutMs === "number") ? opts.handshakeTimeoutMs : DEFAULT_HANDSHAKE_TIMEOUT_MS;
230
233
 
231
234
  var reconnectOpts = _normaliseReconnect(opts.reconnect);
232
235
  var permessageDeflate = opts.permessageDeflate !== false;
@@ -281,14 +284,18 @@ function _normaliseReconnect(input) {
281
284
  "wsClient.connect: reconnect must be false / null / object");
282
285
  }
283
286
  validateOpts(input, ["maxAttempts", "baseMs", "maxMs", "enabled"], "wsClient.connect.reconnect");
287
+ // baseMs / maxMs are backoff durations; a present value must be a positive
288
+ // finite integer (an Infinity base would stall the first reconnect forever).
289
+ numericBounds.requireAllPositiveFiniteIntIfPresent(input, ["baseMs", "maxMs"],
290
+ "wsClient.connect.reconnect", WsClientError, "ws-client/bad-reconnect-opt");
284
291
  return {
285
292
  enabled: input.enabled !== false,
286
- maxAttempts: (typeof input.maxAttempts === "number" && input.maxAttempts >= 0) // allow:numeric-opt-Infinity
293
+ // maxAttempts: Infinity is a deliberate "reconnect indefinitely" intent, so
294
+ // a non-finite value is accepted here (unlike the bounded caps above).
295
+ maxAttempts: (typeof input.maxAttempts === "number" && input.maxAttempts >= 0) // allow:numeric-opt-Infinity-intentional — Infinity = reconnect indefinitely, a supported intent
287
296
  ? input.maxAttempts : DEFAULT_RECONNECT_MAX_ATTEMPTS,
288
- baseMs: (typeof input.baseMs === "number" && input.baseMs > 0) // allow:numeric-opt-Infinity
289
- ? input.baseMs : DEFAULT_RECONNECT_BASE_MS,
290
- maxMs: (typeof input.maxMs === "number" && input.maxMs > 0) // allow:numeric-opt-Infinity
291
- ? input.maxMs : DEFAULT_RECONNECT_MAX_MS,
297
+ baseMs: (typeof input.baseMs === "number") ? input.baseMs : DEFAULT_RECONNECT_BASE_MS,
298
+ maxMs: (typeof input.maxMs === "number") ? input.maxMs : DEFAULT_RECONNECT_MAX_MS,
292
299
  };
293
300
  }
294
301
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.15.35",
3
+ "version": "0.15.37",
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:ecbff4b1-ce1e-42ec-93f8-46f7c036e3e8",
5
+ "serialNumber": "urn:uuid:a1be0521-cb1d-4b8e-9ed6-b0053b5b9bfd",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-06-27T06:52:37.069Z",
8
+ "timestamp": "2026-06-27T09:36:13.300Z",
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.15.35",
22
+ "bom-ref": "@blamejs/core@0.15.37",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.15.35",
25
+ "version": "0.15.37",
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.15.35",
29
+ "purl": "pkg:npm/%40blamejs/core@0.15.37",
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.15.35",
57
+ "ref": "@blamejs/core@0.15.37",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]