@blamejs/blamejs-shop 0.4.73 → 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.
- package/CHANGELOG.md +4 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/customers.js +5 -2
- package/lib/storefront.js +41 -8
- package/lib/vendor/MANIFEST.json +48 -44
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +23 -2
- package/lib/vendor/blamejs/examples/wiki/lib/build-app.js +5 -1
- package/lib/vendor/blamejs/examples/wiki/lib/harvest-vendored-deps.js +6 -1
- package/lib/vendor/blamejs/examples/wiki/test/codebase-patterns.test.js +8 -4
- package/lib/vendor/blamejs/index.js +2 -0
- package/lib/vendor/blamejs/lib/auth/ciba.js +32 -8
- package/lib/vendor/blamejs/lib/auth/dpop.js +9 -0
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +25 -12
- package/lib/vendor/blamejs/lib/auth/jwt.js +19 -3
- package/lib/vendor/blamejs/lib/auth/oauth.js +8 -2
- package/lib/vendor/blamejs/lib/auth/saml.js +19 -9
- package/lib/vendor/blamejs/lib/crypto-field.js +19 -1
- package/lib/vendor/blamejs/lib/csp.js +9 -0
- package/lib/vendor/blamejs/lib/db-query.js +33 -2
- package/lib/vendor/blamejs/lib/mail-auth.js +24 -1
- package/lib/vendor/blamejs/lib/mail-dkim.js +20 -7
- package/lib/vendor/blamejs/lib/middleware/compose-pipeline.js +39 -5
- package/lib/vendor/blamejs/lib/pipl-cn.js +11 -8
- package/lib/vendor/blamejs/lib/request-helpers.js +146 -13
- package/lib/vendor/blamejs/lib/safe-json.js +26 -0
- package/lib/vendor/blamejs/lib/session.js +35 -117
- package/lib/vendor/blamejs/lib/sql.js +22 -0
- package/lib/vendor/blamejs/lib/ws-client.js +26 -0
- package/lib/vendor/blamejs/lib/x509-chain.js +71 -24
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.15.json +73 -0
- package/lib/vendor/blamejs/test/00-primitives.js +54 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ciba-authreqid-binding.test.js +130 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +37 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csp-builder.test.js +21 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3.test.js +40 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +29 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim.test.js +46 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/middleware-compose-pipeline.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pipl-cn.test.js +12 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-helpers.test.js +46 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js +77 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +19 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +32 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/x509-chain-ca-enforcement.test.js +11 -0
- package/package.json +1 -1
|
@@ -287,6 +287,28 @@ function composePipeline(entries, opts) {
|
|
|
287
287
|
catch (finalErr) { return reject(finalErr); }
|
|
288
288
|
resolve();
|
|
289
289
|
}
|
|
290
|
+
// A middleware (or error handler) that ENDS THE RESPONSE without calling
|
|
291
|
+
// next has halted the chain: it handled the request itself. Settle the
|
|
292
|
+
// outer promise so the awaiting router is released — a never-settled
|
|
293
|
+
// promise pins its req/res closure forever — but do NOT call finalNext:
|
|
294
|
+
// the router's next-flag stays false, so it won't run the route handler
|
|
295
|
+
// on top of an already-sent response.
|
|
296
|
+
function _resolveOnce() {
|
|
297
|
+
if (finished) return;
|
|
298
|
+
finished = true;
|
|
299
|
+
resolve();
|
|
300
|
+
}
|
|
301
|
+
// Settle when the response actually finishes. This is response-driven,
|
|
302
|
+
// not return-driven: a callback-style middleware that calls next() LATER
|
|
303
|
+
// (from a timer, stream, or legacy callback) returns before next() runs,
|
|
304
|
+
// so we must NOT treat a bare return as a halt — only an ended response.
|
|
305
|
+
// The synchronous _responseEnded() check below covers a middleware that
|
|
306
|
+
// ended the response inline (and a mock res without an event emitter);
|
|
307
|
+
// this listener covers one that ends it from a deferred callback.
|
|
308
|
+
if (res && typeof res.once === "function") {
|
|
309
|
+
res.once("finish", _resolveOnce);
|
|
310
|
+
res.once("close", _resolveOnce);
|
|
311
|
+
}
|
|
290
312
|
async function dispatch(err) {
|
|
291
313
|
if (finished) return;
|
|
292
314
|
if (idx >= resolved.length) return _finishOnce(err);
|
|
@@ -313,14 +335,19 @@ function composePipeline(entries, opts) {
|
|
|
313
335
|
}
|
|
314
336
|
try {
|
|
315
337
|
if (err) {
|
|
316
|
-
// Error handler: (err, req, res, next)
|
|
338
|
+
// Error handler: (err, req, res, next). Express convention — a
|
|
339
|
+
// 4-arg handler that returns without calling next has HANDLED the
|
|
340
|
+
// error, so the chain ends cleanly; settle (without finalNext).
|
|
317
341
|
await entry.mw(err, req, res, _next);
|
|
318
|
-
if (!advanced)
|
|
342
|
+
if (!advanced) _resolveOnce();
|
|
319
343
|
} else {
|
|
320
|
-
// Regular middleware: (req, res, next)
|
|
344
|
+
// Regular middleware: (req, res, next). Settle only if it ENDED the
|
|
345
|
+
// response (a halt). A bare return without next is NOT treated as a
|
|
346
|
+
// halt — it may be a callback-style middleware that calls next()
|
|
347
|
+
// later (timer/stream); the finish/close listener covers a deferred
|
|
348
|
+
// response end, and a deferred next() continues the chain.
|
|
321
349
|
await entry.mw(req, res, _next);
|
|
322
|
-
|
|
323
|
-
// The middleware presumably wrote the response itself.
|
|
350
|
+
if (!advanced && _responseEnded(res)) _resolveOnce();
|
|
324
351
|
}
|
|
325
352
|
} catch (syncErr) {
|
|
326
353
|
// Synchronous throw OR rejected promise — route through
|
|
@@ -334,6 +361,13 @@ function composePipeline(entries, opts) {
|
|
|
334
361
|
};
|
|
335
362
|
}
|
|
336
363
|
|
|
364
|
+
// True once the response has been committed/ended — the reliable "this
|
|
365
|
+
// middleware handled the request" signal (vs. the function merely returning,
|
|
366
|
+
// which a callback-style middleware does before its deferred next()).
|
|
367
|
+
function _responseEnded(res) {
|
|
368
|
+
return !!(res && (res.writableEnded || res.finished || res.headersSent));
|
|
369
|
+
}
|
|
370
|
+
|
|
337
371
|
composePipeline.CANONICAL_POSITIONS = CANONICAL_POSITIONS;
|
|
338
372
|
composePipeline.ComposePipelineError = ComposePipelineError;
|
|
339
373
|
|
|
@@ -108,14 +108,17 @@ function _requireRecordedAt(value, label) {
|
|
|
108
108
|
* and determine the lawful mechanism the transfer requires. PIPL Art. 38(1)
|
|
109
109
|
* permits three bases for moving personal information out of the PRC — the
|
|
110
110
|
* CAC standard contract (SCC), a CAC security assessment (Art. 40), or
|
|
111
|
-
* certification by a CAC-accredited body
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* operator (CIIO),
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* individuals
|
|
111
|
+
* certification by a CAC-accredited body. Under the CAC Provisions on
|
|
112
|
+
* Promoting and Regulating Cross-Border Data Flows (effective 2024, which
|
|
113
|
+
* relaxed the 2022 thresholds) the security assessment is MANDATORY (the
|
|
114
|
+
* operator may NOT self-select the standard contract or certification) when
|
|
115
|
+
* the exporter is a critical-information-infrastructure operator (CIIO),
|
|
116
|
+
* exports "important data", or — counting cumulatively since 1 January of
|
|
117
|
+
* the current year — transfers the personal information of more than
|
|
118
|
+
* 1,000,000 individuals (non-sensitive) or the sensitive personal
|
|
119
|
+
* information of more than 10,000 individuals. The 100,000–1,000,000
|
|
120
|
+
* non-sensitive band is the standard-contract / certification tier, NOT a
|
|
121
|
+
* security-assessment trigger.
|
|
119
122
|
*
|
|
120
123
|
* The builder validates the operator-supplied facts, computes
|
|
121
124
|
* `securityAssessmentRequired` against those thresholds, resolves the
|
|
@@ -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
|
-
|
|
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
|
|
840
|
-
var
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
var
|
|
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
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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 =
|
|
297
|
+
inputs.clientIp = resolveIp(req) || "";
|
|
380
298
|
} else if (f === "clientIpPrefix") {
|
|
381
|
-
// /24 v4 + /64 v6 — see
|
|
382
|
-
inputs.clientIpPrefix =
|
|
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
|