@blamejs/blamejs-shop 0.4.74 → 0.4.75

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 (48) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/vendor/MANIFEST.json +48 -44
  4. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  5. package/lib/vendor/blamejs/api-snapshot.json +23 -2
  6. package/lib/vendor/blamejs/examples/wiki/lib/build-app.js +5 -1
  7. package/lib/vendor/blamejs/examples/wiki/lib/harvest-vendored-deps.js +6 -1
  8. package/lib/vendor/blamejs/examples/wiki/test/codebase-patterns.test.js +8 -4
  9. package/lib/vendor/blamejs/index.js +2 -0
  10. package/lib/vendor/blamejs/lib/auth/ciba.js +32 -8
  11. package/lib/vendor/blamejs/lib/auth/dpop.js +9 -0
  12. package/lib/vendor/blamejs/lib/auth/fido-mds3.js +25 -12
  13. package/lib/vendor/blamejs/lib/auth/jwt.js +19 -3
  14. package/lib/vendor/blamejs/lib/auth/oauth.js +8 -2
  15. package/lib/vendor/blamejs/lib/auth/saml.js +19 -9
  16. package/lib/vendor/blamejs/lib/crypto-field.js +19 -1
  17. package/lib/vendor/blamejs/lib/csp.js +9 -0
  18. package/lib/vendor/blamejs/lib/db-query.js +33 -2
  19. package/lib/vendor/blamejs/lib/mail-auth.js +24 -1
  20. package/lib/vendor/blamejs/lib/mail-dkim.js +20 -7
  21. package/lib/vendor/blamejs/lib/middleware/compose-pipeline.js +39 -5
  22. package/lib/vendor/blamejs/lib/pipl-cn.js +11 -8
  23. package/lib/vendor/blamejs/lib/request-helpers.js +146 -13
  24. package/lib/vendor/blamejs/lib/safe-json.js +26 -0
  25. package/lib/vendor/blamejs/lib/session.js +35 -117
  26. package/lib/vendor/blamejs/lib/sql.js +22 -0
  27. package/lib/vendor/blamejs/lib/ws-client.js +26 -0
  28. package/lib/vendor/blamejs/lib/x509-chain.js +71 -24
  29. package/lib/vendor/blamejs/package.json +1 -1
  30. package/lib/vendor/blamejs/release-notes/v0.15.15.json +73 -0
  31. package/lib/vendor/blamejs/test/00-primitives.js +54 -0
  32. package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +22 -0
  33. package/lib/vendor/blamejs/test/layer-0-primitives/ciba-authreqid-binding.test.js +130 -0
  34. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +37 -0
  35. package/lib/vendor/blamejs/test/layer-0-primitives/csp-builder.test.js +21 -0
  36. package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +22 -0
  37. package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3.test.js +40 -1
  38. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +29 -0
  39. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim.test.js +46 -0
  40. package/lib/vendor/blamejs/test/layer-0-primitives/middleware-compose-pipeline.test.js +74 -0
  41. package/lib/vendor/blamejs/test/layer-0-primitives/pipl-cn.test.js +12 -0
  42. package/lib/vendor/blamejs/test/layer-0-primitives/request-helpers.test.js +46 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js +77 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +74 -0
  45. package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +19 -0
  46. package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +32 -0
  47. package/lib/vendor/blamejs/test/layer-0-primitives/x509-chain-ca-enforcement.test.js +11 -0
  48. package/package.json +1 -1
@@ -384,6 +384,134 @@ function trustedClientIp(opts) {
384
384
  };
385
385
  }
386
386
 
387
+ // IP-prefix masking constants — named so the bit-arithmetic stays readable.
388
+ // /24 IPv4 is the original IP-geolocation bucket and matches the legacy
389
+ // carrier-NAT pool stride; /64 IPv6 is the customer LAN every RIR allocates
390
+ // (RFC 4291 §2.5.4), so tightening below it punishes IPv6 privacy-extension
391
+ // address rotation.
392
+ var IP_BITS_PER_BYTE = 8; // bits per byte; protocol constant, not a byte size
393
+ var IPV4_OCTET_COUNT = 4;
394
+ var IPV4_OCTET_RANGE = 256; // 0..255 inclusive; v4 octet domain
395
+ var IPV4_TOTAL_BITS = 32; // IPv4 address width in bits
396
+ var IPV4_DEFAULT_PREFIX = 24; // /24 carrier-NAT pool stride
397
+ var IPV6_GROUP_COUNT = 8; // 8 16-bit groups in v6
398
+ var IPV6_BYTE_COUNT = 16; // 16 bytes in v6
399
+ var IPV6_DEFAULT_PREFIX = 64; // /64 customer LAN per RFC 4291 §2.5.4
400
+ var IP_BYTE_MASK = 0xff;
401
+ var IP_HEX_RADIX = 16; // base-16 radix
402
+ var V4_MAPPED_V6_PREFIX = "::ffff:";
403
+
404
+ function _maskIpv4(ip, prefix) {
405
+ // ip = "a.b.c.d"; prefix is bits to keep (1..32).
406
+ var parts = String(ip).split(".");
407
+ if (parts.length !== IPV4_OCTET_COUNT) return null;
408
+ var n = 0;
409
+ for (var i = 0; i < IPV4_OCTET_COUNT; i++) {
410
+ var oct = parseInt(parts[i], 10);
411
+ if (!Number.isInteger(oct) || oct < 0 || oct >= IPV4_OCTET_RANGE) return null;
412
+ n = (n * IPV4_OCTET_RANGE) + oct;
413
+ }
414
+ // Apply prefix mask.
415
+ var mask = prefix === 0 ? 0 : (-1 >>> (IPV4_TOTAL_BITS - prefix)) << (IPV4_TOTAL_BITS - prefix);
416
+ // Bitwise on 32-bit unsigned. JS coerces to 32-bit signed, so use
417
+ // unsigned right shift to recover.
418
+ var masked = (n & mask) >>> 0;
419
+ return ((masked >>> IP_BITS_PER_BYTE * 3) & IP_BYTE_MASK) + "." +
420
+ ((masked >>> IP_BITS_PER_BYTE * 2) & IP_BYTE_MASK) + "." +
421
+ ((masked >>> IP_BITS_PER_BYTE) & IP_BYTE_MASK) + "." +
422
+ (masked & IP_BYTE_MASK) + "/" + prefix;
423
+ }
424
+
425
+ function _maskIpv6(ip, prefix) {
426
+ // Expand to 8 16-bit groups. Accept :: shorthand. Reject if invalid.
427
+ var raw = String(ip).toLowerCase();
428
+ // Strip an embedded zone id (fe80::1%eth0); not part of the address.
429
+ var pct = raw.indexOf("%");
430
+ if (pct !== -1) raw = raw.substring(0, pct);
431
+ var doubleColonAt = raw.indexOf("::");
432
+ var groups;
433
+ if (doubleColonAt === -1) {
434
+ groups = raw.split(":");
435
+ if (groups.length !== IPV6_GROUP_COUNT) return null;
436
+ } else {
437
+ var left = raw.substring(0, doubleColonAt).split(":");
438
+ var right = raw.substring(doubleColonAt + 2).split(":");
439
+ if (left.length === 1 && left[0] === "") left = [];
440
+ if (right.length === 1 && right[0] === "") right = [];
441
+ var fillCount = IPV6_GROUP_COUNT - left.length - right.length;
442
+ if (fillCount < 0) return null;
443
+ var middle = [];
444
+ for (var fi = 0; fi < fillCount; fi++) middle.push("0");
445
+ groups = left.concat(middle).concat(right);
446
+ }
447
+ // Each group is 1–4 hex chars.
448
+ var bytes = [];
449
+ for (var gi = 0; gi < IPV6_GROUP_COUNT; gi++) {
450
+ var g = groups[gi];
451
+ if (typeof g !== "string" || g.length === 0 || g.length > 4 || /[^0-9a-f]/.test(g)) return null;
452
+ var v = parseInt(g, IP_HEX_RADIX);
453
+ if (!Number.isInteger(v) || v < 0 || v > 0xffff) return null;
454
+ bytes.push((v >> IP_BITS_PER_BYTE) & IP_BYTE_MASK);
455
+ bytes.push(v & IP_BYTE_MASK);
456
+ }
457
+ // Apply prefix in bits.
458
+ var keepBytes = Math.floor(prefix / IP_BITS_PER_BYTE);
459
+ var keepBits = prefix % IP_BITS_PER_BYTE;
460
+ for (var bi = 0; bi < IPV6_BYTE_COUNT; bi++) {
461
+ if (bi < keepBytes) continue;
462
+ if (bi === keepBytes && keepBits > 0) {
463
+ var m = (IP_BYTE_MASK << (IP_BITS_PER_BYTE - keepBits)) & IP_BYTE_MASK;
464
+ bytes[bi] = bytes[bi] & m;
465
+ } else {
466
+ bytes[bi] = 0;
467
+ }
468
+ }
469
+ // Re-emit as colon-hex (no compression — deterministic for hashing).
470
+ var out = [];
471
+ for (var oi = 0; oi < IPV6_BYTE_COUNT; oi += 2) {
472
+ out.push(((bytes[oi] << IP_BITS_PER_BYTE) | bytes[oi + 1]).toString(IP_HEX_RADIX));
473
+ }
474
+ return out.join(":") + "/" + prefix;
475
+ }
476
+
477
+ /**
478
+ * @primitive b.requestHelpers.ipPrefix
479
+ * @signature b.requestHelpers.ipPrefix(ip)
480
+ * @since 0.15.15
481
+ * @related b.requestHelpers.clientIp, b.requestHelpers.trustedClientIp
482
+ *
483
+ * Mask a client IP to its subnet bucket: a <code>/24</code> for IPv4 (the
484
+ * carrier-NAT pool stride) and a <code>/64</code> for IPv6 (the customer-LAN
485
+ * prefix RIRs allocate, RFC 4291 §2.5.4). Returns the canonical
486
+ * <code>"network/prefix"</code> string, or <code>""</code> for a non-string /
487
+ * empty / unparseable input. An IPv4-mapped IPv6 address
488
+ * (<code>::ffff:1.2.3.4</code>) folds to its dotted form so it buckets the
489
+ * same regardless of how a proxy reported it.
490
+ *
491
+ * This is the masking the session device-fingerprint's built-in
492
+ * <code>clientIpPrefix</code> field hashes (so roaming carriers that flip the
493
+ * public IP within a subnet don't log a user out). Exposed so an operator who
494
+ * drops to a function-form fingerprint field — for a custom mask width, or to
495
+ * combine the prefix with other signals — reuses this exact algorithm instead
496
+ * of re-deriving the /24 + /64 masking (and silently diverging).
497
+ *
498
+ * @example
499
+ * b.requestHelpers.ipPrefix("203.0.113.47"); // → "203.0.113.0/24"
500
+ * b.requestHelpers.ipPrefix("2001:db8::1"); // → "2001:db8:0:0/64"
501
+ */
502
+ function ipPrefix(ip) {
503
+ if (typeof ip !== "string" || ip.length === 0) return "";
504
+ // IPv4-mapped IPv6 (::ffff:1.2.3.4) — strip the wrapper so the v4 mask
505
+ // applies. Same bucket regardless of how the proxy reported it.
506
+ var lower = ip.toLowerCase();
507
+ if (lower.indexOf(V4_MAPPED_V6_PREFIX) === 0 && lower.indexOf(".") !== -1) {
508
+ return _maskIpv4(lower.substring(V4_MAPPED_V6_PREFIX.length), IPV4_DEFAULT_PREFIX) || "";
509
+ }
510
+ if (ip.indexOf(":") !== -1) return _maskIpv6(ip, IPV6_DEFAULT_PREFIX) || "";
511
+ if (ip.indexOf(".") !== -1) return _maskIpv4(ip, IPV4_DEFAULT_PREFIX) || "";
512
+ return "";
513
+ }
514
+
387
515
  /**
388
516
  * @primitive b.requestHelpers.trustedProtocol
389
517
  * @signature b.requestHelpers.trustedProtocol(opts?)
@@ -787,8 +915,6 @@ function captureResponseStatus(res, onEnd) {
787
915
  return origEnd;
788
916
  }
789
917
 
790
- var Q_VALUE_RE = /(?:^|;|\s)q\s*=\s*([0-9]*\.?[0-9]+)/i;
791
-
792
918
  /**
793
919
  * @primitive b.requestHelpers.parseQualityList
794
920
  * @signature b.requestHelpers.parseQualityList(headerValue, opts?)
@@ -831,24 +957,30 @@ function parseQualityList(headerValue, opts) {
831
957
  if (typeof headerValue !== "string" || headerValue.length === 0) return [];
832
958
  opts = opts || {};
833
959
  var caseSensitive = opts.caseSensitive === true;
834
- var parts = headerValue.split(",");
960
+ // Quote-aware split: a parameter value may be a quoted-string containing ','
961
+ // or ';' or 'q=' (RFC 7231 §5.3.1 / RFC 9110), which must not split an element
962
+ // or be mis-read as the q-value. splitUnquoted respects double-quoted runs.
963
+ var parts = structuredFields.splitUnquoted(headerValue, ",");
835
964
  var out = [];
836
965
  for (var i = 0; i < parts.length; i++) {
837
966
  var p = parts[i].trim();
838
967
  if (p.length === 0) continue;
839
- var semi = p.indexOf(";");
840
- var value, q;
841
- if (semi === -1) {
842
- value = caseSensitive ? p : p.toLowerCase();
843
- q = 1;
844
- } else {
845
- var head = p.slice(0, semi).trim();
846
- value = caseSensitive ? head : head.toLowerCase();
847
- var rest = p.slice(semi + 1).trim();
848
- var qm = rest.match(Q_VALUE_RE);
968
+ var segs = structuredFields.splitUnquoted(p, ";");
969
+ var head = segs[0].trim();
970
+ var value = caseSensitive ? head : head.toLowerCase();
971
+ var q = 1;
972
+ // The q parameter is the named token `q` separating media-type params from
973
+ // accept-ext; the FIRST `q` parameter is the quality. Only a parameter
974
+ // literally named `q` counts — never a `q=`-shaped substring of another
975
+ // parameter's name or quoted value.
976
+ for (var s = 1; s < segs.length; s++) {
977
+ var kv = structuredFields.parseKeyValuePiece(segs[s], "=", true);
978
+ if (kv.key !== "q") continue;
979
+ var qm = String(kv.value).trim().match(/^([0-9]*\.?[0-9]+)/);
849
980
  q = qm ? parseFloat(qm[1]) : 1;
850
981
  if (isNaN(q) || q < 0) q = 0;
851
982
  if (q > 1) q = 1;
983
+ break;
852
984
  }
853
985
  out.push({ value: value, q: q });
854
986
  }
@@ -1067,6 +1199,7 @@ module.exports = {
1067
1199
  // proxy-trust primitives (default refuses forwarded headers)
1068
1200
  clientIp: clientIp,
1069
1201
  trustedClientIp: trustedClientIp,
1202
+ ipPrefix: ipPrefix,
1070
1203
  requestProtocol: requestProtocol,
1071
1204
  trustedProtocol: trustedProtocol,
1072
1205
  appendVary: appendVary,
@@ -969,9 +969,35 @@ function _capInt(value, defaultValue, ceiling) {
969
969
  * });
970
970
  */
971
971
 
972
+ /**
973
+ * @primitive b.safeJson.isJsonObject
974
+ * @signature b.safeJson.isJsonObject(value)
975
+ * @since 0.15.14
976
+ * @status stable
977
+ * @related b.safeJson.parse
978
+ *
979
+ * True iff <code>value</code> is a plain JSON object — not <code>null</code>,
980
+ * not an array, not a scalar. <code>safeJson.parse</code> accepts the literal
981
+ * <code>null</code> and scalars / arrays (all valid JSON documents), so a
982
+ * parsed JWS header, claims set, or document must be re-checked before its
983
+ * fields are dereferenced. This is that check, shared so the
984
+ * <code>!x || typeof x !== "object" || Array.isArray(x)</code> idiom isn't
985
+ * re-rolled (and silently varied) at every call site.
986
+ *
987
+ * @example
988
+ * var b = require("blamejs");
989
+ * b.safeJson.isJsonObject(b.safeJson.parse('{"a":1}')); // → true
990
+ * b.safeJson.isJsonObject(b.safeJson.parse("null")); // → false
991
+ * b.safeJson.isJsonObject(b.safeJson.parse("[1,2]")); // → false
992
+ */
993
+ function isJsonObject(value) {
994
+ return value !== null && typeof value === "object" && !Array.isArray(value);
995
+ }
996
+
972
997
  module.exports = {
973
998
  parse: parse,
974
999
  parseOrDefault: parseOrDefault,
1000
+ isJsonObject: isJsonObject,
975
1001
  stringify: stringify,
976
1002
  stringifyForScript: stringifyForScript,
977
1003
  canonical: canonical,
@@ -256,130 +256,48 @@ function _sealForInsert(row) {
256
256
  var DEFAULT_FINGERPRINT_FIELDS = ["clientIp", "userAgent", "acceptLanguage"];
257
257
 
258
258
  // Subnet binding: roaming carriers (T-Mobile / Verizon / etc.) flip the
259
- // public client IP every few requests as the device hops cells, so a
260
- // strict full-IP fingerprint logs out healthy mobile users. The
261
- // "clientIpPrefix" field hashes a /24 mask for IPv4 (256-address bucket
262
- // same Class C-shaped neighborhood) and a /64 mask for IPv6 (the IPv6
263
- // "site" prefix the RIRs allocate to every ISP customer). Drift across
264
- // /24 OR /64 is meaningfully suspicious; drift within is not.
265
- //
266
- // Per the IPv6 addressing architecture (RFC 4291 §2.5.4) every customer
267
- // LAN is assigned at least a /64; tightening below /64 punishes IPv6
268
- // privacy-extension address rotation. /24 IPv4 is the original
269
- // IP-geolocation bucket size and matches the legacy carrier-NAT pool
270
- // stride. Operators with stricter needs pass a function-form
271
- // fingerprint field for custom mask widths.
272
- //
273
- // Protocol constants named so the bit-arithmetic stays readable.
274
- var IP_BITS_PER_BYTE = 8; // bits per byte; protocol constant, not a byte size
275
- var IPV4_OCTET_COUNT = 4;
276
- var IPV4_OCTET_RANGE = 256; // 0..255 inclusive; v4 octet domain
277
- var IPV4_TOTAL_BITS = 32; // IPv4 address width in bits
278
- var IPV4_DEFAULT_PREFIX = 24; // /24 carrier-NAT pool stride
279
- var IPV6_GROUP_COUNT = 8; // 8 16-bit groups in v6
280
- var IPV6_BYTE_COUNT = 16; // 16 bytes in v6
281
- var IPV6_DEFAULT_PREFIX = 64; // /64 customer LAN per RFC 4291 §2.5.4
282
- var BYTE_MASK = 0xff;
283
- var HEX_RADIX = 16; // base-16 radix
284
- var V4_MAPPED_V6_PREFIX = "::ffff:";
285
-
286
- function _maskIpv4(ip, prefix) {
287
- // ip = "a.b.c.d"; prefix is bits to keep (1..32).
288
- var parts = String(ip).split(".");
289
- if (parts.length !== IPV4_OCTET_COUNT) return null;
290
- var n = 0;
291
- for (var i = 0; i < IPV4_OCTET_COUNT; i++) {
292
- var oct = parseInt(parts[i], 10);
293
- if (!Number.isInteger(oct) || oct < 0 || oct >= IPV4_OCTET_RANGE) return null;
294
- n = (n * IPV4_OCTET_RANGE) + oct;
295
- }
296
- // Apply prefix mask.
297
- var mask = prefix === 0 ? 0 : (-1 >>> (IPV4_TOTAL_BITS - prefix)) << (IPV4_TOTAL_BITS - prefix);
298
- // Bitwise on 32-bit unsigned. JS coerces to 32-bit signed, so use
299
- // unsigned right shift to recover.
300
- var masked = (n & mask) >>> 0;
301
- return ((masked >>> IP_BITS_PER_BYTE * 3) & BYTE_MASK) + "." +
302
- ((masked >>> IP_BITS_PER_BYTE * 2) & BYTE_MASK) + "." +
303
- ((masked >>> IP_BITS_PER_BYTE) & BYTE_MASK) + "." +
304
- (masked & BYTE_MASK) + "/" + prefix;
305
- }
306
-
307
- function _maskIpv6(ip, prefix) {
308
- // Expand to 8 16-bit groups. Accept :: shorthand. Reject if invalid.
309
- var raw = String(ip).toLowerCase();
310
- // Strip an embedded zone id (fe80::1%eth0); not part of the address.
311
- var pct = raw.indexOf("%");
312
- if (pct !== -1) raw = raw.substring(0, pct);
313
- var doubleColonAt = raw.indexOf("::");
314
- var groups;
315
- if (doubleColonAt === -1) {
316
- groups = raw.split(":");
317
- if (groups.length !== IPV6_GROUP_COUNT) return null;
318
- } else {
319
- var left = raw.substring(0, doubleColonAt).split(":");
320
- var right = raw.substring(doubleColonAt + 2).split(":");
321
- if (left.length === 1 && left[0] === "") left = [];
322
- if (right.length === 1 && right[0] === "") right = [];
323
- var fillCount = IPV6_GROUP_COUNT - left.length - right.length;
324
- if (fillCount < 0) return null;
325
- var middle = [];
326
- for (var fi = 0; fi < fillCount; fi++) middle.push("0");
327
- groups = left.concat(middle).concat(right);
328
- }
329
- // Each group is 1–4 hex chars.
330
- var bytes = [];
331
- for (var gi = 0; gi < IPV6_GROUP_COUNT; gi++) {
332
- var g = groups[gi];
333
- if (typeof g !== "string" || g.length === 0 || g.length > 4 || /[^0-9a-f]/.test(g)) return null;
334
- var v = parseInt(g, HEX_RADIX);
335
- if (!Number.isInteger(v) || v < 0 || v > 0xffff) return null;
336
- bytes.push((v >> IP_BITS_PER_BYTE) & BYTE_MASK);
337
- bytes.push(v & BYTE_MASK);
338
- }
339
- // Apply prefix in bits.
340
- var keepBytes = Math.floor(prefix / IP_BITS_PER_BYTE);
341
- var keepBits = prefix % IP_BITS_PER_BYTE;
342
- for (var bi = 0; bi < IPV6_BYTE_COUNT; bi++) {
343
- if (bi < keepBytes) continue;
344
- if (bi === keepBytes && keepBits > 0) {
345
- var m = (BYTE_MASK << (IP_BITS_PER_BYTE - keepBits)) & BYTE_MASK;
346
- bytes[bi] = bytes[bi] & m;
347
- } else {
348
- bytes[bi] = 0;
349
- }
350
- }
351
- // Re-emit as colon-hex (no compression — deterministic for hashing).
352
- var out = [];
353
- for (var oi = 0; oi < IPV6_BYTE_COUNT; oi += 2) {
354
- out.push(((bytes[oi] << IP_BITS_PER_BYTE) | bytes[oi + 1]).toString(HEX_RADIX));
355
- }
356
- return out.join(":") + "/" + prefix;
357
- }
358
-
359
- function _ipPrefix(ip) {
360
- if (typeof ip !== "string" || ip.length === 0) return "";
361
- // IPv4-mapped IPv6 (::ffff:1.2.3.4) — strip the wrapper so the v4
362
- // mask applies. Same bucket regardless of how the proxy reported it.
363
- var lower = ip.toLowerCase();
364
- if (lower.indexOf(V4_MAPPED_V6_PREFIX) === 0 && lower.indexOf(".") !== -1) {
365
- return _maskIpv4(lower.substring(V4_MAPPED_V6_PREFIX.length), IPV4_DEFAULT_PREFIX) || "";
259
+ // public client IP every few requests as the device hops cells, so a strict
260
+ // full-IP fingerprint logs out healthy mobile users. The "clientIpPrefix"
261
+ // field hashes the /24 (IPv4) + /64 (IPv6) subnet bucket instead — drift
262
+ // across the bucket is meaningfully suspicious, drift within is not. The
263
+ // masking lives in requestHelpers.ipPrefix (the IP-utilities home, next to
264
+ // clientIp / trustedClientIp); operators with stricter needs pass a
265
+ // function-form fingerprint field and reuse requestHelpers.ipPrefix for a
266
+ // custom mask width.
267
+
268
+ // Resolve the per-call client-IP function for the clientIp / clientIpPrefix
269
+ // fingerprint fields. With { trustedProxies } (an array/string of CIDRs) or a
270
+ // custom { clientIpResolver }, the IP is peer-gated through
271
+ // requestHelpers.trustedClientIp so a deployment behind a trusted proxy binds
272
+ // the session to the real client and not the proxy address (which silently
273
+ // defeats the IP component of the fingerprint). With neither, it falls back to
274
+ // the bare-socket peer the historical default, preserved so existing
275
+ // fingerprints don't change and log users out. The SAME option must be passed
276
+ // to create / verify / rotate (exactly like fingerprintFields) or the
277
+ // fingerprint won't match across the session lifecycle. An invalid CIDR throws
278
+ // at the call (config-time entry-point validation).
279
+ function _clientIpResolver(opts) {
280
+ if (opts && (opts.trustedProxies != null || typeof opts.clientIpResolver === "function")) {
281
+ return requestHelpers.trustedClientIp({
282
+ trustedProxies: opts.trustedProxies,
283
+ clientIpResolver: opts.clientIpResolver,
284
+ }).resolve;
366
285
  }
367
- if (ip.indexOf(":") !== -1) return _maskIpv6(ip, IPV6_DEFAULT_PREFIX) || "";
368
- if (ip.indexOf(".") !== -1) return _maskIpv4(ip, IPV4_DEFAULT_PREFIX) || "";
369
- return "";
286
+ return requestHelpers.clientIp;
370
287
  }
371
288
 
372
- function _buildFingerprintInputs(req, fields) {
289
+ function _buildFingerprintInputs(req, fields, resolveIp) {
373
290
  if (!req) return null;
291
+ resolveIp = resolveIp || requestHelpers.clientIp;
374
292
  var headers = req.headers || {};
375
293
  var inputs = {};
376
294
  for (var i = 0; i < fields.length; i++) {
377
295
  var f = fields[i];
378
296
  if (f === "clientIp") {
379
- inputs.clientIp = requestHelpers.clientIp(req) || "";
297
+ inputs.clientIp = resolveIp(req) || "";
380
298
  } else if (f === "clientIpPrefix") {
381
- // /24 v4 + /64 v6 — see _ipPrefix() commentary.
382
- inputs.clientIpPrefix = _ipPrefix(requestHelpers.clientIp(req) || "");
299
+ // /24 v4 + /64 v6 — see requestHelpers.ipPrefix commentary.
300
+ inputs.clientIpPrefix = requestHelpers.ipPrefix(resolveIp(req) || "");
383
301
  } else if (f === "userAgent") {
384
302
  inputs.userAgent = String(headers["user-agent"] || "");
385
303
  } else if (f === "acceptLanguage") {
@@ -482,7 +400,7 @@ async function create(opts) {
482
400
  var dataObj = opts.data ? Object.assign({}, opts.data) : null;
483
401
  var fpFields = Array.isArray(opts.fingerprintFields) && opts.fingerprintFields.length > 0
484
402
  ? opts.fingerprintFields : DEFAULT_FINGERPRINT_FIELDS;
485
- var fpInputs = _buildFingerprintInputs(opts.req, fpFields);
403
+ var fpInputs = _buildFingerprintInputs(opts.req, fpFields, _clientIpResolver(opts));
486
404
  if (fpInputs) {
487
405
  if (!dataObj) dataObj = {};
488
406
  dataObj.__bj_fingerprint = _hashFingerprint(sid, fpInputs);
@@ -660,7 +578,7 @@ async function verify(token, verifyOpts) {
660
578
  if (storedFingerprint && verifyOpts.req) {
661
579
  var fpFields = Array.isArray(verifyOpts.fingerprintFields) && verifyOpts.fingerprintFields.length > 0
662
580
  ? verifyOpts.fingerprintFields : DEFAULT_FINGERPRINT_FIELDS;
663
- var currentInputs = _buildFingerprintInputs(verifyOpts.req, fpFields);
581
+ var currentInputs = _buildFingerprintInputs(verifyOpts.req, fpFields, _clientIpResolver(verifyOpts));
664
582
  var currentHash = _hashFingerprint(sid, currentInputs);
665
583
  if (currentHash !== storedFingerprint) {
666
584
  fingerprintDrift = true;
@@ -1058,7 +976,7 @@ async function rotate(oldToken, opts) {
1058
976
  "so the device binding can be re-keyed to the new session id", true);
1059
977
  }
1060
978
  if (!newDataObj) newDataObj = {};
1061
- newDataObj.__bj_fingerprint = _hashFingerprint(newSid, _buildFingerprintInputs(opts.req, fpFields));
979
+ newDataObj.__bj_fingerprint = _hashFingerprint(newSid, _buildFingerprintInputs(opts.req, fpFields, _clientIpResolver(opts)));
1062
980
  }
1063
981
 
1064
982
  var dataJson = newDataObj ? JSON.stringify(newDataObj) : null;
@@ -798,6 +798,17 @@ class Predicate {
798
798
  return this._add(joiner, qc + " " + op + " NULL", []);
799
799
  }
800
800
 
801
+ // `col = NULL` / `col != NULL` is UNKNOWN in SQL — never true. Emitting it
802
+ // (e.g. from where({ col: null })) silently matches zero rows; worse, a null
803
+ // accidentally passed where a real value was expected (where({ ownerId }))
804
+ // would, if rewritten to `IS NULL`, return orphan rows — an authorization
805
+ // footgun. Refuse it and direct the caller to the explicit NULL predicates.
806
+ if (value === null && (op === "=" || op === "!=" || op === "<>")) {
807
+ throw _err("where(" + JSON.stringify(col) + ", '" + op + "', null) is never true in SQL " +
808
+ "(col " + op + " NULL is UNKNOWN); use whereNull(col) / whereNotNull(col) to test for NULL",
809
+ "sql-builder/null-equality");
810
+ }
811
+
801
812
  if ((op === "LIKE" || op === "NOT LIKE") && typeof value === "string") {
802
813
  return this._add(joiner, qc + " " + op + " ? ESCAPE '~'", [_escapeLike(value)]);
803
814
  }
@@ -948,6 +959,17 @@ class Predicate {
948
959
  if (!Array.isArray(values) || values.length === 0) {
949
960
  throw _err("whereInArray requires a non-empty array of values", "sql-builder/empty-in");
950
961
  }
962
+ // Validate each element is a bindable parameter. On the non-Postgres IN-list
963
+ // path every element is its own `?`, so the driver rejects an undefined at
964
+ // execute; the Postgres `= ANY(?)` path binds the WHOLE array as one param,
965
+ // where an undefined is silently coerced to NULL — diverging per dialect.
966
+ // Reject undefined here so every backend fails the same way, at build.
967
+ for (var vi = 0; vi < values.length; vi += 1) {
968
+ if (values[vi] === undefined) {
969
+ throw _err("whereInArray value[" + vi + "] is undefined (not a bindable parameter)",
970
+ "sql-builder/bad-in-value");
971
+ }
972
+ }
951
973
  this._gate(col);
952
974
  var qc = _qualifiedColumn(col, this._dialect());
953
975
  if (this._dialect() === "postgres") {
@@ -585,6 +585,7 @@ class WsClient extends EventEmitter {
585
585
  this._reconnectAttempt = 0;
586
586
  this._fragmentChunks = [];
587
587
  this._fragmentOpcode = null;
588
+ this._fragmentBytes = 0;
588
589
 
589
590
  this._startHeartbeat();
590
591
  if (this._opts.auditOn) {
@@ -623,6 +624,14 @@ class WsClient extends EventEmitter {
623
624
  }
624
625
 
625
626
  _handleFrame(frame) {
627
+ // Once the connection has been torn down (e.g. a prior frame in the same
628
+ // parsed batch tripped maxMessageBytes and called _teardown, which sets
629
+ // _closed synchronously), drop any remaining buffered frames — processing
630
+ // them would emit a spurious cascade of protocol errors (a stray
631
+ // continuation after the fragment state reset). A graceful close keeps
632
+ // _closed false until the handshake completes, so the peer's CLOSE frame
633
+ // is still processed and the normal close code surfaces.
634
+ if (this._closed) return;
626
635
  // RFC 6455 §5.5: control frames MUST be <= 125 bytes AND non-fragmented.
627
636
  var isControl = frame.opcode === OPCODE_PING ||
628
637
  frame.opcode === OPCODE_PONG ||
@@ -692,6 +701,7 @@ class WsClient extends EventEmitter {
692
701
  this._fragmentOpcode = frame.opcode;
693
702
  this._fragmentRsv1 = frame.rsv1 === true;
694
703
  this._fragmentChunks = [frame.payload];
704
+ this._fragmentBytes = safeBuffer.byteLengthOf(frame.payload);
695
705
  } else if (frame.opcode === OPCODE_CONT) {
696
706
  if (this._fragmentOpcode == null) {
697
707
  this._handleSocketError(new WsClientError("ws-client/protocol-error",
@@ -699,6 +709,21 @@ class WsClient extends EventEmitter {
699
709
  return;
700
710
  }
701
711
  this._fragmentChunks.push(frame.payload);
712
+ this._fragmentBytes += safeBuffer.byteLengthOf(frame.payload);
713
+ }
714
+ // Enforce maxMessageBytes on the RUNNING fragment total, not only at FIN:
715
+ // a peer that streams continuation frames and never sets FIN would
716
+ // otherwise grow _fragmentChunks without bound, one maxFrameBytes-sized
717
+ // frame at a time (CWE-770 / CWE-400). The per-frame parser cap bounds a
718
+ // single frame, never the sum.
719
+ if (this._fragmentOpcode != null && this._fragmentBytes > this._opts.maxMessageBytes) {
720
+ this._fragmentChunks = [];
721
+ this._fragmentOpcode = null;
722
+ this._fragmentRsv1 = false;
723
+ this._fragmentBytes = 0;
724
+ this._handleSocketError(new WsClientError("ws-client/message-too-big",
725
+ "incoming message exceeds maxMessageBytes (" + this._opts.maxMessageBytes + ")"));
726
+ return;
702
727
  }
703
728
  if (frame.fin) {
704
729
  var fullPayload = Buffer.concat(this._fragmentChunks); // allow:handrolled-buffer-collect — bounded by maxMessageBytes below
@@ -712,6 +737,7 @@ class WsClient extends EventEmitter {
712
737
  this._fragmentChunks = [];
713
738
  this._fragmentOpcode = null;
714
739
  this._fragmentRsv1 = false;
740
+ this._fragmentBytes = 0;
715
741
  if (this._negotiatedDeflate && firstFrameRsv1) {
716
742
  try {
717
743
  var zlib = require("node:zlib"); // allow:inline-require — zlib only on deflate-negotiated path
@@ -1,33 +1,80 @@
1
1
  "use strict";
2
+ /**
3
+ * @module b.x509Chain
4
+ * @nav Crypto
5
+ * @title X.509 chain (CA-bit issuer test)
6
+ *
7
+ * @intro
8
+ * The basicConstraints-enforcing issuer test the framework's own
9
+ * certificate-chain walkers route through (<code>b.tsa.verifyToken</code>,
10
+ * <code>b.mail.bimi</code> VMC/CMC, <code>b.mail.crypto.smime</code>,
11
+ * <code>b.mdoc</code>, <code>b.contentCredentials</code>,
12
+ * <code>b.auth.fido</code>). It exists because node:crypto's
13
+ * <code>X509Certificate.checkIssued()</code> validates the issuer/subject
14
+ * DN match, the AKI/SKI linkage, and — only when a keyUsage extension is
15
+ * present — keyCertSign, but it does <strong>not</strong> enforce
16
+ * basicConstraints cA:TRUE. A leaf / end-entity certificate (cA:FALSE)
17
+ * that omits keyUsage is therefore wrongly accepted as a signing CA for
18
+ * the next certificate in the chain — the classic basicConstraints bypass
19
+ * (CVE-2002-0862 class). Every in-tree walker routes its issuer test
20
+ * through these helpers so the cA enforcement can never be forgotten in
21
+ * one walker but present in another.
22
+ *
23
+ * Exposed so a consumer validating an X.509 chain <em>outside</em> a TLS
24
+ * handshake — an operator-uploaded CA bundle, a non-handshake PQ-signed
25
+ * certificate — has the same hardened, fail-closed test instead of being
26
+ * pushed toward the raw <code>checkIssued()</code> path this module
27
+ * exists to prevent. Both helpers fail closed: any malformed input or
28
+ * unsupported key type returns false rather than throwing.
29
+ *
30
+ * @card
31
+ * basicConstraints cA:TRUE-enforcing X.509 issuer test, fail-closed —
32
+ * the hardened alternative to node's checkIssued() for chains built
33
+ * outside a TLS handshake.
34
+ */
2
35
 
3
- // Internal X.509 path-validation helpers shared by the framework's
4
- // certificate-chain walkers (b.tsa.verifyToken, b.mail.bimi VMC/CMC,
5
- // b.mail.crypto.smime.verify). They exist because node:crypto's
6
- // X509Certificate.checkIssued() validates the issuer/subject DN match,
7
- // the AKI/SKI linkage, and — when a keyUsage extension is present —
8
- // keyCertSign, but it does NOT enforce basicConstraints cA:TRUE. A
9
- // leaf / end-entity certificate (cA:FALSE) that omits keyUsage is
10
- // therefore wrongly accepted as a signing CA for the next cert in the
11
- // chain the classic basicConstraints bypass (CVE-2002-0862 class).
12
- // Every chain walker routes its issuer test through these helpers so the
13
- // cA enforcement can never be forgotten in one walker but present in
14
- // another.
15
-
16
- // True only when `cert` asserts basicConstraints cA:TRUE. node's
17
- // X509Certificate exposes `.ca` (a boolean); a cert with no
18
- // basicConstraints extension or with cA:FALSE returns false. A missing
19
- // cert or a non-boolean `.ca` (parse failure / unsupported runtime)
20
- // fails closed.
36
+ /**
37
+ * @primitive b.x509Chain.isCaCert
38
+ * @signature b.x509Chain.isCaCert(cert)
39
+ * @since 0.15.15
40
+ * @status stable
41
+ * @related b.x509Chain.issuerValidlyIssued
42
+ *
43
+ * True only when <code>cert</code> asserts basicConstraints cA:TRUE.
44
+ * node's <code>X509Certificate</code> exposes <code>.ca</code> (a boolean);
45
+ * a certificate with no basicConstraints extension or with cA:FALSE
46
+ * returns false. A missing cert or a non-boolean <code>.ca</code> (parse
47
+ * failure / unsupported runtime) fails closed to false.
48
+ *
49
+ * @example
50
+ * var crypto = require("crypto");
51
+ * var ca = new crypto.X509Certificate(caPem);
52
+ * b.x509Chain.isCaCert(ca); // true only if basicConstraints cA:TRUE
53
+ */
21
54
  function isCaCert(cert) {
22
55
  return !!cert && cert.ca === true;
23
56
  }
24
57
 
25
- // True when `issuer` validly issued `subject` AND is itself a CA: the
26
- // DN / AKI-SKI / keyUsage linkage (checkIssued), the cryptographic
27
- // signature (verify), and basicConstraints cA:TRUE (isCaCert). The cA
28
- // check runs first so a non-CA cert is rejected before the expensive
29
- // signature verification. Any exception (malformed cert, unsupported
30
- // key type) fails closed to false.
58
+ /**
59
+ * @primitive b.x509Chain.issuerValidlyIssued
60
+ * @signature b.x509Chain.issuerValidlyIssued(issuer, subject)
61
+ * @since 0.15.15
62
+ * @status stable
63
+ * @related b.x509Chain.isCaCert
64
+ *
65
+ * True when <code>issuer</code> validly issued <code>subject</code> AND is
66
+ * itself a CA: the DN / AKI-SKI / keyUsage linkage (checkIssued), the
67
+ * cryptographic signature (verify), and basicConstraints cA:TRUE
68
+ * (isCaCert). The cA check runs first so a non-CA certificate is rejected
69
+ * before the expensive signature verification. Any exception (malformed
70
+ * cert, unsupported key type) fails closed to false.
71
+ *
72
+ * @example
73
+ * var crypto = require("crypto");
74
+ * var issuer = new crypto.X509Certificate(issuerPem);
75
+ * var subject = new crypto.X509Certificate(leafPem);
76
+ * b.x509Chain.issuerValidlyIssued(issuer, subject); // → boolean
77
+ */
31
78
  function issuerValidlyIssued(issuer, subject) {
32
79
  try {
33
80
  return isCaCert(issuer) &&
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.15.14",
3
+ "version": "0.15.15",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",