@blamejs/blamejs-shop 0.4.90 → 0.4.92
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/gift-card-ledger.js +8 -2
- package/lib/order.js +1 -0
- package/lib/vendor/MANIFEST.json +49 -41
- package/lib/vendor/blamejs/CHANGELOG.md +6 -0
- package/lib/vendor/blamejs/SECURITY.md +1 -0
- package/lib/vendor/blamejs/api-snapshot.json +208 -2
- package/lib/vendor/blamejs/examples/wiki/test/e2e.js +7 -4
- package/lib/vendor/blamejs/examples/wiki/test/integration.js +15 -12
- package/lib/vendor/blamejs/index.js +2 -0
- package/lib/vendor/blamejs/lib/audit-sign.js +34 -1
- package/lib/vendor/blamejs/lib/backup/manifest.js +191 -44
- package/lib/vendor/blamejs/lib/codepoint-class.js +284 -77
- package/lib/vendor/blamejs/lib/framework-error.js +14 -0
- package/lib/vendor/blamejs/lib/fsm.js +80 -24
- package/lib/vendor/blamejs/lib/log.js +32 -0
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +18 -2
- package/lib/vendor/blamejs/lib/middleware/request-id.js +24 -4
- package/lib/vendor/blamejs/lib/request-helpers.js +50 -0
- package/lib/vendor/blamejs/lib/safe-path.js +24 -10
- package/lib/vendor/blamejs/lib/sql.js +133 -0
- package/lib/vendor/blamejs/lib/totp.js +98 -33
- package/lib/vendor/blamejs/lib/ws-client.js +39 -28
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.21.json +51 -0
- package/lib/vendor/blamejs/release-notes/v0.15.22.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.15.23.json +22 -0
- package/lib/vendor/blamejs/test/00-primitives.js +80 -0
- package/lib/vendor/blamejs/test/_smoke-worker.js +81 -0
- package/lib/vendor/blamejs/test/integration/federation-auth.test.js +7 -4
- package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +7 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-manifest-signature.test.js +91 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +65 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codepoint-class.test.js +58 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-default-gate-posture-caps.test.js +5 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-replaystore-required.test.js +9 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/fsm.test.js +99 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/money.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +9 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-xff-spoofing.test.js +36 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-helpers.test.js +33 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-id-async-context.test.js +117 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-path.test.js +64 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +96 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +55 -0
- package/lib/vendor/blamejs/test/smoke.js +93 -10
- package/package.json +1 -1
|
@@ -360,6 +360,9 @@ function create(opts) {
|
|
|
360
360
|
var store = { requestId: id || null, _extra: {} };
|
|
361
361
|
return _als.run(store, fn);
|
|
362
362
|
}
|
|
363
|
+
function enterRequestId(id) {
|
|
364
|
+
_als.enterWith({ requestId: id || null, _extra: {} });
|
|
365
|
+
}
|
|
363
366
|
function runWithContext(ctx, fn) {
|
|
364
367
|
var existing = _getStore();
|
|
365
368
|
var rid = (ctx && ctx.requestId) || (existing && existing.requestId) || null;
|
|
@@ -417,6 +420,7 @@ function create(opts) {
|
|
|
417
420
|
getLevel: getLevel,
|
|
418
421
|
isLevelEnabled: level_in,
|
|
419
422
|
runWithRequestId: runWithRequestId,
|
|
423
|
+
enterRequestId: enterRequestId,
|
|
420
424
|
runWithContext: runWithContext,
|
|
421
425
|
getRequestId: getRequestId,
|
|
422
426
|
middleware: middleware,
|
|
@@ -639,6 +643,33 @@ function runWithRequestId(id, fn) {
|
|
|
639
643
|
return _als.run({ requestId: id || null, _extra: {} }, fn);
|
|
640
644
|
}
|
|
641
645
|
|
|
646
|
+
/**
|
|
647
|
+
* @primitive b.log.enterRequestId
|
|
648
|
+
* @signature b.log.enterRequestId(id)
|
|
649
|
+
* @since 0.15.21
|
|
650
|
+
* @status stable
|
|
651
|
+
* @related b.log.runWithRequestId, b.log.getRequestId
|
|
652
|
+
*
|
|
653
|
+
* Bind `id` into the AsyncLocalStorage scope for the REMAINDER of the
|
|
654
|
+
* current async execution — without nesting a callback. Where
|
|
655
|
+
* `runWithRequestId(id, fn)` wraps a function (and the binding closes when
|
|
656
|
+
* `fn` returns), this uses `AsyncLocalStorage.enterWith` so the id survives a
|
|
657
|
+
* dispatch model that hands control back to its caller before the awaited
|
|
658
|
+
* work runs — a boolean-`next` middleware chain (`b.router`), where the route
|
|
659
|
+
* handler executes after the middleware returns. Call it once per request,
|
|
660
|
+
* inside the per-request async context, so each request stays isolated. The
|
|
661
|
+
* companion to `b.middleware.requestId({ asyncContext: true })`, which calls
|
|
662
|
+
* it for you.
|
|
663
|
+
*
|
|
664
|
+
* @example
|
|
665
|
+
* // inside a per-request middleware, before next():
|
|
666
|
+
* b.log.enterRequestId(req.requestId);
|
|
667
|
+
* // any awaited handler downstream now sees b.log.getRequestId() === req.requestId
|
|
668
|
+
*/
|
|
669
|
+
function enterRequestId(id) {
|
|
670
|
+
_als.enterWith({ requestId: id || null, _extra: {} });
|
|
671
|
+
}
|
|
672
|
+
|
|
642
673
|
module.exports = {
|
|
643
674
|
create: create,
|
|
644
675
|
boot: boot,
|
|
@@ -649,4 +680,5 @@ module.exports = {
|
|
|
649
680
|
// instance handy but still need to read ALS state.
|
|
650
681
|
getRequestId: getRequestId,
|
|
651
682
|
runWithRequestId: runWithRequestId,
|
|
683
|
+
enterRequestId: enterRequestId,
|
|
652
684
|
};
|
|
@@ -448,6 +448,7 @@ function _resolveBackend(opts) {
|
|
|
448
448
|
* pruneIntervalMs: number,
|
|
449
449
|
* trustedProxies: string|string[], // CIDRs of your reverse proxies — peer-gates X-Forwarded-For for the IP key
|
|
450
450
|
* clientIpResolver: function(req): string|null, // own the rate-limit key's client IP
|
|
451
|
+
* ipKeyMode: "exact"|"prefix64", // default "exact"; "prefix64" keys IPv6 by its /64 (IPv4 stays exact) so one end-site can't rotate the low 64 bits to evade the limit
|
|
451
452
|
* trustProxy: boolean|number, // legacy; refused with the default IP key (spoofable) — use trustedProxies
|
|
452
453
|
* }
|
|
453
454
|
*
|
|
@@ -465,7 +466,7 @@ function create(opts) {
|
|
|
465
466
|
opts = opts || {};
|
|
466
467
|
validateOpts(opts, [
|
|
467
468
|
"keyFn", "statusOnLimit", "bodyOnLimit", "onDeny", "problemDetails",
|
|
468
|
-
"header", "headerPrefix", "skipPaths", "scope",
|
|
469
|
+
"header", "headerPrefix", "skipPaths", "scope", "ipKeyMode",
|
|
469
470
|
"backend", "trustProxy", "trustedProxies", "clientIpResolver", "algorithm",
|
|
470
471
|
// memory backend (token-bucket)
|
|
471
472
|
"burst", "refillPerSecond",
|
|
@@ -493,7 +494,22 @@ function create(opts) {
|
|
|
493
494
|
"or set your own keyFn.");
|
|
494
495
|
}
|
|
495
496
|
var _clientIp = function (req) { return _ipResolver.resolve(req) || "unknown"; };
|
|
496
|
-
|
|
497
|
+
// ipKeyMode "prefix64" collapses an IPv6 client to its routing-significant
|
|
498
|
+
// /64 for the DEFAULT key, so one end-site can't rotate the low 64 bits to
|
|
499
|
+
// mint unlimited fresh buckets and walk its per-IP limit (RFC 6177); IPv4
|
|
500
|
+
// stays exact. The audit actor still records the full resolved IP via
|
|
501
|
+
// _clientIp — only the rate-limit key is bucketed.
|
|
502
|
+
if (opts.ipKeyMode !== undefined && opts.ipKeyMode !== "exact" && opts.ipKeyMode !== "prefix64") {
|
|
503
|
+
throw new Error("middleware.rateLimit: ipKeyMode must be \"exact\" (default) or \"prefix64\"");
|
|
504
|
+
}
|
|
505
|
+
var _defaultKey = (opts.ipKeyMode === "prefix64")
|
|
506
|
+
? function (req) {
|
|
507
|
+
var ip = _ipResolver.resolve(req);
|
|
508
|
+
if (!ip) return "unknown";
|
|
509
|
+
return requestHelpers.ipKey(ip, { ipv6Bits: 64 }) || ip;
|
|
510
|
+
}
|
|
511
|
+
: _clientIp;
|
|
512
|
+
var keyFn = opts.keyFn || _defaultKey;
|
|
497
513
|
var statusOnLimit = opts.statusOnLimit || 429;
|
|
498
514
|
var bodyOnLimit = opts.bodyOnLimit !== undefined ? opts.bodyOnLimit : "Too Many Requests";
|
|
499
515
|
var onDeny = typeof opts.onDeny === "function" ? opts.onDeny : null;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
var C = require("../constants");
|
|
9
9
|
var { generateToken } = require("../crypto");
|
|
10
10
|
var validateOpts = require("../validate-opts");
|
|
11
|
+
var log = require("../log");
|
|
11
12
|
|
|
12
13
|
var DEFAULT_FORMAT = /^[A-Za-z0-9._-]{8,128}$/;
|
|
13
14
|
// Hard cap on inbound header length. The DEFAULT_FORMAT regex caps at
|
|
@@ -33,30 +34,43 @@ var MAX_INBOUND_LEN = C.BYTES.bytes(256);
|
|
|
33
34
|
* the chain — every later primitive expects `req.requestId` to
|
|
34
35
|
* be present for log lines and audit-record metadata.
|
|
35
36
|
*
|
|
37
|
+
* Pass `asyncContext: true` to additionally bind the id into the framework's
|
|
38
|
+
* AsyncLocalStorage scope so `b.log.getRequestId()` (and every
|
|
39
|
+
* `b.log.create`-built logger) returns it inside awaited route-handler code,
|
|
40
|
+
* not just on `req.requestId`. The `b.router` dispatch model is boolean-`next`
|
|
41
|
+
* — the route handler runs after this middleware returns — so the binding uses
|
|
42
|
+
* `AsyncLocalStorage.enterWith` (it persists forward through the awaited
|
|
43
|
+
* chain) rather than a callback wrap (which would close before the handler
|
|
44
|
+
* runs). Each request runs in its own async context, so the binding is
|
|
45
|
+
* request-scoped.
|
|
46
|
+
*
|
|
36
47
|
* @opts
|
|
37
48
|
* {
|
|
38
49
|
* headerName: string, // default "X-Request-Id"
|
|
39
50
|
* trustUpstream: boolean, // default true; false → always re-mint
|
|
40
51
|
* formatRegex: RegExp, // default /^[A-Za-z0-9._-]{8,128}$/
|
|
52
|
+
* asyncContext: boolean, // default false; true → bind into b.log ALS for awaited handler code
|
|
41
53
|
* }
|
|
42
54
|
*
|
|
43
55
|
* @example
|
|
44
56
|
* var b = require("@blamejs/core");
|
|
45
57
|
* var app = b.router.create();
|
|
46
|
-
* app.use(b.middleware.requestId({
|
|
47
|
-
* app.get("/health", function (req, res) {
|
|
48
|
-
*
|
|
58
|
+
* app.use(b.middleware.requestId({ asyncContext: true }));
|
|
59
|
+
* app.get("/health", async function (req, res) {
|
|
60
|
+
* await somethingAsync();
|
|
61
|
+
* res.end(b.log.getRequestId()); // → the request's id, even after await
|
|
49
62
|
* });
|
|
50
63
|
*/
|
|
51
64
|
function create(opts) {
|
|
52
65
|
opts = opts || {};
|
|
53
66
|
validateOpts(opts, [
|
|
54
|
-
"headerName", "trustUpstream", "formatRegex",
|
|
67
|
+
"headerName", "trustUpstream", "formatRegex", "asyncContext",
|
|
55
68
|
], "middleware.requestId");
|
|
56
69
|
var headerName = (opts.headerName || "X-Request-Id");
|
|
57
70
|
var headerNameLower = headerName.toLowerCase();
|
|
58
71
|
var trustUpstream = opts.trustUpstream !== false;
|
|
59
72
|
var format = opts.formatRegex || DEFAULT_FORMAT;
|
|
73
|
+
var asyncContext = opts.asyncContext === true;
|
|
60
74
|
|
|
61
75
|
return function requestId(req, res, next) {
|
|
62
76
|
var inbound = req.headers && req.headers[headerNameLower];
|
|
@@ -72,6 +86,12 @@ function create(opts) {
|
|
|
72
86
|
if (typeof res.setHeader === "function") {
|
|
73
87
|
res.setHeader(headerName, id);
|
|
74
88
|
}
|
|
89
|
+
// Bind into the log ALS so awaited handler code reads the id via
|
|
90
|
+
// b.log.getRequestId(). enterWith (not run-with-callback) because the
|
|
91
|
+
// boolean-next dispatcher runs the handler after this returns.
|
|
92
|
+
if (asyncContext) {
|
|
93
|
+
log.enterRequestId(id);
|
|
94
|
+
}
|
|
75
95
|
next();
|
|
76
96
|
};
|
|
77
97
|
}
|
|
@@ -531,6 +531,55 @@ function ipPrefix(ip, opts) {
|
|
|
531
531
|
return "";
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
+
/**
|
|
535
|
+
* @primitive b.requestHelpers.ipKey
|
|
536
|
+
* @signature b.requestHelpers.ipKey(ip, opts?)
|
|
537
|
+
* @since 0.15.21
|
|
538
|
+
* @related b.requestHelpers.ipPrefix, b.requestHelpers.clientIp
|
|
539
|
+
*
|
|
540
|
+
* Derive a stable rate-limit / blocklist key from a client IP: the IPv4
|
|
541
|
+
* address <strong>verbatim</strong> (one IPv4 is one host) but the IPv6
|
|
542
|
+
* address collapsed to its routing-significant <code>/64</code> prefix. A
|
|
543
|
+
* single IPv6 end-site is allocated a whole <code>/64</code> (RFC 6177 / RFC
|
|
544
|
+
* 4291 §2.5.4) and freely rotates the low 64 bits, so keying on the full
|
|
545
|
+
* 128-bit address lets one site mint unlimited fresh keys — defeating a
|
|
546
|
+
* per-IP throttle and an exact-address block. Keying on the <code>/64</code>
|
|
547
|
+
* closes that while still distinguishing real end-sites. Unlike
|
|
548
|
+
* <code>ipPrefix</code> (which masks IPv4 to a <code>/24</code> pool), this
|
|
549
|
+
* keeps IPv4 exact — a rate limiter wants per-host IPv4 granularity.
|
|
550
|
+
*
|
|
551
|
+
* Returns the canonical key string, or <code>""</code> for a non-string /
|
|
552
|
+
* empty / unparseable input (caller falls back to its own bucket). An
|
|
553
|
+
* IPv4-mapped IPv6 address (<code>::ffff:1.2.3.4</code>) folds to its dotted
|
|
554
|
+
* IPv4 form so a client keys the same however a proxy reported it. Pass
|
|
555
|
+
* <code>opts.ipv6Bits</code> to override the IPv6 mask width (default 64).
|
|
556
|
+
*
|
|
557
|
+
* @opts
|
|
558
|
+
* ipv6Bits: number, // IPv6 mask width in bits (default 64; valid 0..128)
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* b.requestHelpers.ipKey("203.0.113.47"); // → "203.0.113.47" (exact)
|
|
562
|
+
* b.requestHelpers.ipKey("2001:db8:1:2:dead:beef:0:1"); // → "2001:db8:1:2:0:0:0:0/64"
|
|
563
|
+
*/
|
|
564
|
+
function ipKey(ip, opts) {
|
|
565
|
+
if (typeof ip !== "string" || ip.length === 0) return "";
|
|
566
|
+
opts = opts || {};
|
|
567
|
+
var v6 = _resolvePrefixBits(opts.ipv6Bits, IPV6_DEFAULT_PREFIX, 128); // IPv6 max prefix length in bits
|
|
568
|
+
var lower = ip.toLowerCase();
|
|
569
|
+
// IPv4-mapped IPv6 (::ffff:1.2.3.4) → its exact dotted host, so a v4 client
|
|
570
|
+
// keys identically whether the proxy reported 1.2.3.4 or ::ffff:1.2.3.4.
|
|
571
|
+
if (lower.indexOf(V4_MAPPED_V6_PREFIX) === 0 && lower.indexOf(".") !== -1) {
|
|
572
|
+
var m4 = _maskIpv4(lower.substring(V4_MAPPED_V6_PREFIX.length), 32); // /32 = exact host
|
|
573
|
+
return m4 ? m4.replace(/\/32$/, "") : "";
|
|
574
|
+
}
|
|
575
|
+
if (ip.indexOf(":") !== -1) return _maskIpv6(ip, v6) || ""; // IPv6 → /ipv6Bits prefix
|
|
576
|
+
if (ip.indexOf(".") !== -1) { // IPv4 → exact host, verbatim
|
|
577
|
+
var m = _maskIpv4(ip, 32);
|
|
578
|
+
return m ? m.replace(/\/32$/, "") : "";
|
|
579
|
+
}
|
|
580
|
+
return "";
|
|
581
|
+
}
|
|
582
|
+
|
|
534
583
|
/**
|
|
535
584
|
* @primitive b.requestHelpers.trustedProtocol
|
|
536
585
|
* @signature b.requestHelpers.trustedProtocol(opts?)
|
|
@@ -1307,6 +1356,7 @@ module.exports = {
|
|
|
1307
1356
|
clientIp: clientIp,
|
|
1308
1357
|
trustedClientIp: trustedClientIp,
|
|
1309
1358
|
ipPrefix: ipPrefix,
|
|
1359
|
+
ipKey: ipKey,
|
|
1310
1360
|
requestProtocol: requestProtocol,
|
|
1311
1361
|
trustedProtocol: trustedProtocol,
|
|
1312
1362
|
requestHost: requestHost,
|
|
@@ -206,19 +206,33 @@ function _resolveCore(base, rel, opts) {
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
// Lexical resolve
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
209
|
+
// Lexical resolve + containment using the TARGET platform's path semantics
|
|
210
|
+
// (nodePath.win32 / nodePath.posix), NOT the runtime's. The runtime nodePath
|
|
211
|
+
// would treat the OTHER platform's separator as an ordinary filename
|
|
212
|
+
// character — so a Windows-target validation on a POSIX host would NOT collapse
|
|
213
|
+
// `ok\..\..\outside` and would wrongly accept a path that escapes the base
|
|
214
|
+
// when later interpreted with Windows path rules (the cross-platform
|
|
215
|
+
// backslash-traversal hole). The target module collapses the target's
|
|
216
|
+
// separators + `..` and its sep matches the resolved output, which both closes
|
|
217
|
+
// that hole AND stops the inverse false-refusal of legitimate in-base paths.
|
|
218
|
+
var pathMod = isWin ? nodePath.win32 : nodePath.posix;
|
|
219
|
+
var baseResolved = pathMod.resolve(base);
|
|
220
|
+
var joined = pathMod.resolve(baseResolved, rel);
|
|
221
|
+
var sepChar = pathMod.sep;
|
|
215
222
|
if (joined !== baseResolved && joined.slice(0, baseResolved.length + 1) !== baseResolved + sepChar) {
|
|
216
223
|
_refuse("safe-path/escapes-base",
|
|
217
224
|
"b.safePath.resolve: rel resolves outside base ('" + joined + "' not inside '" + baseResolved + "')");
|
|
218
225
|
}
|
|
219
226
|
if (opts.realpath === true) {
|
|
227
|
+
// realpath resolves symlinks on the RUNTIME filesystem, so it must use the
|
|
228
|
+
// runtime path module and runtime-resolved paths (a foreign-platform path
|
|
229
|
+
// can't be symlink-resolved on this host). The lexical check above already
|
|
230
|
+
// refused a cross-platform escape; this adds the on-disk symlink check.
|
|
231
|
+
var rtBaseResolved = nodePath.resolve(base);
|
|
232
|
+
var rtJoined = nodePath.resolve(rtBaseResolved, rel);
|
|
233
|
+
var rtSep = nodePath.sep;
|
|
220
234
|
var baseRealpath;
|
|
221
|
-
try { baseRealpath = nodeFs.realpathSync.native(
|
|
235
|
+
try { baseRealpath = nodeFs.realpathSync.native(rtBaseResolved); }
|
|
222
236
|
catch (e) {
|
|
223
237
|
_refuse("safe-path/realpath-base-unresolvable",
|
|
224
238
|
"b.safePath.resolve: opts.realpath set but base realpath failed: " + (e && e.message));
|
|
@@ -227,12 +241,12 @@ function _resolveCore(base, rel, opts) {
|
|
|
227
241
|
// ancestor that exists, and check its realpath. Operators want
|
|
228
242
|
// refusal when ANY ancestor symlink escapes — nodeFs.realpathSync on a
|
|
229
243
|
// non-existent path would throw.
|
|
230
|
-
var ancestor =
|
|
231
|
-
while (ancestor.length >
|
|
244
|
+
var ancestor = rtJoined;
|
|
245
|
+
while (ancestor.length > rtBaseResolved.length) {
|
|
232
246
|
try {
|
|
233
247
|
var ancRealpath = nodeFs.realpathSync.native(ancestor);
|
|
234
248
|
if (ancRealpath !== baseRealpath &&
|
|
235
|
-
ancRealpath.slice(0, baseRealpath.length + 1) !== baseRealpath +
|
|
249
|
+
ancRealpath.slice(0, baseRealpath.length + 1) !== baseRealpath + rtSep) {
|
|
236
250
|
_refuse("safe-path/realpath-escapes-base",
|
|
237
251
|
"b.safePath.resolve: symlink resolution at '" + ancestor +
|
|
238
252
|
"' escapes base realpath '" + baseRealpath + "'");
|
|
@@ -2041,6 +2041,45 @@ class UpdateBuilder extends Builder {
|
|
|
2041
2041
|
this._where = new Predicate(this, "AND");
|
|
2042
2042
|
this._returning = null;
|
|
2043
2043
|
this._allowNoWhere = false;
|
|
2044
|
+
// guardedUpdate() flips _requireGuard so _render refuses to emit a CAS
|
|
2045
|
+
// statement that has no compare-and-swap fence (which would silently be a
|
|
2046
|
+
// plain unconditional-on-state update). _guardCount tracks guardWhere calls.
|
|
2047
|
+
this._requireGuard = false;
|
|
2048
|
+
this._guardCount = 0;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
// guardWhere(col, expected) - the compare-and-swap fence. ANDs
|
|
2052
|
+
// `col = <expected>` (a bound ?) into the WHERE so the UPDATE lands ONLY if
|
|
2053
|
+
// the row is STILL in the expected value - the cross-instance atomic claim
|
|
2054
|
+
// (the transaction substitute on autocommit-only substrates: D1 over an HTTP
|
|
2055
|
+
// bridge, any adapter without interactive transactions). An EXPLICIT
|
|
2056
|
+
// `null` becomes `col IS NULL` (since `col = NULL` is never true) so a
|
|
2057
|
+
// null-state fence works; `undefined` is REFUSED rather than silently
|
|
2058
|
+
// collapsing to `IS NULL`, because an omitted/unset expected value would turn
|
|
2059
|
+
// a CAS into "match the NULL-state rows" and update the wrong rows. The
|
|
2060
|
+
// won/lost result is read from rowCount via b.sql.casWon.
|
|
2061
|
+
guardWhere(col, expected) {
|
|
2062
|
+
if (expected === undefined) {
|
|
2063
|
+
throw _err("guardWhere expected value is undefined - pass an explicit null for an " +
|
|
2064
|
+
"IS NULL fence, or a value; refusing to silently match NULL-state rows",
|
|
2065
|
+
"sql-builder/bad-guard-value");
|
|
2066
|
+
}
|
|
2067
|
+
if (expected === null) {
|
|
2068
|
+
this._where.whereNull(col);
|
|
2069
|
+
} else {
|
|
2070
|
+
this._where.whereOp(col, "=", expected);
|
|
2071
|
+
}
|
|
2072
|
+
this._guardCount += 1;
|
|
2073
|
+
return this;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// guardWhereOp(col, op, expected) - a non-equality CAS fence (e.g. an
|
|
2077
|
+
// optimistic-version `>=`, or a balance `>= amount` debit guard). Routes the
|
|
2078
|
+
// operator through the same whereOp allowlist every other predicate uses.
|
|
2079
|
+
guardWhereOp(col, op, expected) {
|
|
2080
|
+
this._where.whereOp(col, op, expected);
|
|
2081
|
+
this._guardCount += 1;
|
|
2082
|
+
return this;
|
|
2044
2083
|
}
|
|
2045
2084
|
|
|
2046
2085
|
// set(obj) - column->value assignments. set(col, value) - single
|
|
@@ -2112,6 +2151,11 @@ class UpdateBuilder extends Builder {
|
|
|
2112
2151
|
|
|
2113
2152
|
_render() {
|
|
2114
2153
|
if (this._set.length === 0) throw _err("update requires a set(...) call", "sql-builder/empty-set");
|
|
2154
|
+
if (this._requireGuard && this._guardCount === 0) {
|
|
2155
|
+
throw _err("guardedUpdate requires at least one guardWhere(...) / guardWhereOp(...) " +
|
|
2156
|
+
"compare-and-swap fence - without it this is a plain update; use b.sql.update for that",
|
|
2157
|
+
"sql-builder/no-guard");
|
|
2158
|
+
}
|
|
2115
2159
|
if (this._where.length === 0 && !this._allowNoWhere) {
|
|
2116
2160
|
throw _err("refusing unconditional update - call where(...) first or allowNoWhere()",
|
|
2117
2161
|
"sql-builder/no-where");
|
|
@@ -3896,6 +3940,93 @@ function insertSelectWhere(tableNameOrRef, opts) { return new InsertSelectWhereB
|
|
|
3896
3940
|
*/
|
|
3897
3941
|
function update(tableNameOrRef, opts) { return new UpdateBuilder(tableNameOrRef, opts); }
|
|
3898
3942
|
|
|
3943
|
+
/**
|
|
3944
|
+
* @primitive b.sql.guardedUpdate
|
|
3945
|
+
* @signature b.sql.guardedUpdate(table, opts?)
|
|
3946
|
+
* @since 0.15.21
|
|
3947
|
+
* @status stable
|
|
3948
|
+
* @related b.sql.update, b.sql.insertSelectWhere, b.sql.casWon
|
|
3949
|
+
*
|
|
3950
|
+
* Start a compare-and-swap `UPDATE` builder - the cross-instance-safe way to
|
|
3951
|
+
* advance a status / version on a single-statement-per-request backend (D1
|
|
3952
|
+
* over an HTTP bridge, or any autocommit-only adapter without interactive
|
|
3953
|
+
* transactions). It is `b.sql.update` plus a required `guardWhere(col,
|
|
3954
|
+
* expected)` fence: the statement lands ONLY when the row is STILL in the
|
|
3955
|
+
* expected value, so two racing transitions cannot both win. Refuses to
|
|
3956
|
+
* render without at least one `guardWhere(...)` / `guardWhereOp(...)` - an
|
|
3957
|
+
* unfenced one would just be a plain update.
|
|
3958
|
+
*
|
|
3959
|
+
* Read the winner from the result's `rowCount` with `b.sql.casWon(result)`:
|
|
3960
|
+
* exactly one row matched (`won: true`) means this caller made the
|
|
3961
|
+
* transition; zero (`won: false`) means it lost the race and must no-op /
|
|
3962
|
+
* refuse. The sibling of `b.sql.insertSelectWhere` (the conditional-INSERT
|
|
3963
|
+
* debit) for the conditional-UPDATE case, and the b.fsm composition partner
|
|
3964
|
+
* (resolve the destination side-effect-free with `instance.target(event)`,
|
|
3965
|
+
* then guard on the from-state here).
|
|
3966
|
+
*
|
|
3967
|
+
* @opts
|
|
3968
|
+
* dialect: string, // postgres | sqlite | mysql (default sqlite)
|
|
3969
|
+
* schema: string, // schema qualifier
|
|
3970
|
+
* prefix: string, // operator app-table namespace prefix
|
|
3971
|
+
* allowedColumns: array, // column-membership gate set
|
|
3972
|
+
*
|
|
3973
|
+
* @example
|
|
3974
|
+
* var b = require("@blamejs/core");
|
|
3975
|
+
* // advance order id=7 from "paid" -> "shipped" iff still "paid"
|
|
3976
|
+
* var q = b.sql.guardedUpdate("orders")
|
|
3977
|
+
* .set({ status: "shipped" })
|
|
3978
|
+
* .where("id", 7)
|
|
3979
|
+
* .guardWhere("status", "paid")
|
|
3980
|
+
* .toSql();
|
|
3981
|
+
* // -> { sql: 'UPDATE orders SET "status" = ? WHERE "id" = ? AND "status" = ?',
|
|
3982
|
+
* // params: ["shipped", 7, "paid"] }
|
|
3983
|
+
* // var res = await b.db.raw(q.sql, q.params);
|
|
3984
|
+
* // if (!b.sql.casWon(res).won) { return refuse(); } // lost the race
|
|
3985
|
+
*/
|
|
3986
|
+
function guardedUpdate(tableNameOrRef, opts) {
|
|
3987
|
+
var builder = new UpdateBuilder(tableNameOrRef, opts);
|
|
3988
|
+
builder._requireGuard = true;
|
|
3989
|
+
return builder;
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3992
|
+
/**
|
|
3993
|
+
* @primitive b.sql.casWon
|
|
3994
|
+
* @signature b.sql.casWon(result)
|
|
3995
|
+
* @since 0.15.21
|
|
3996
|
+
* @status stable
|
|
3997
|
+
* @related b.sql.guardedUpdate, b.sql.insertSelectWhere
|
|
3998
|
+
*
|
|
3999
|
+
* Interpret a compare-and-swap result's affected-row count into a won/lost
|
|
4000
|
+
* verdict, owning the `Number(rowCount) === 1` check and the cross-adapter
|
|
4001
|
+
* field-name divergence (`b.db` / `b.externalDb` normalize to `rowCount`; raw
|
|
4002
|
+
* sqlite reports `changes`, raw mysql `affectedRows` / `rowsAffected`).
|
|
4003
|
+
* Returns `{ won, rowCount }` where `won` is true only when exactly one row
|
|
4004
|
+
* was affected. Throws when the result carries no recognizable numeric
|
|
4005
|
+
* row-count field - an indeterminate result must surface, never be silently
|
|
4006
|
+
* read as a win (a phantom win on a CAS is a double-spend).
|
|
4007
|
+
*
|
|
4008
|
+
* @example
|
|
4009
|
+
* var v = b.sql.casWon(await b.db.raw(q.sql, q.params));
|
|
4010
|
+
* if (v.won) { applyTransition(); } else { refuseLostRace(v.rowCount); }
|
|
4011
|
+
*/
|
|
4012
|
+
function casWon(result) {
|
|
4013
|
+
if (!result || typeof result !== "object") {
|
|
4014
|
+
throw _err("casWon: result must be the object returned by the query runner",
|
|
4015
|
+
"sql-builder/bad-cas-result");
|
|
4016
|
+
}
|
|
4017
|
+
var count = null;
|
|
4018
|
+
var fields = ["rowCount", "changes", "affectedRows", "rowsAffected"];
|
|
4019
|
+
for (var i = 0; i < fields.length; i += 1) {
|
|
4020
|
+
var v = result[fields[i]];
|
|
4021
|
+
if (typeof v === "number" && isFinite(v)) { count = v; break; }
|
|
4022
|
+
}
|
|
4023
|
+
if (count === null) {
|
|
4024
|
+
throw _err("casWon: result has no numeric rowCount / changes / affectedRows field - " +
|
|
4025
|
+
"cannot determine the compare-and-swap outcome", "sql-builder/no-row-count");
|
|
4026
|
+
}
|
|
4027
|
+
return { won: count === 1, rowCount: count };
|
|
4028
|
+
}
|
|
4029
|
+
|
|
3899
4030
|
/**
|
|
3900
4031
|
* @primitive b.sql.delete
|
|
3901
4032
|
* @signature b.sql.delete(table, opts?)
|
|
@@ -3976,6 +4107,8 @@ module.exports = {
|
|
|
3976
4107
|
insert: insert,
|
|
3977
4108
|
insertSelectWhere: insertSelectWhere,
|
|
3978
4109
|
update: update,
|
|
4110
|
+
guardedUpdate: guardedUpdate,
|
|
4111
|
+
casWon: casWon,
|
|
3979
4112
|
delete: del,
|
|
3980
4113
|
upsert: upsert,
|
|
3981
4114
|
// Table reference
|
|
@@ -7,16 +7,28 @@
|
|
|
7
7
|
* posture. SHA-256 is supported as backward-compatible opt-in for
|
|
8
8
|
* deployments whose authenticator app only goes that high.
|
|
9
9
|
*
|
|
10
|
-
* SHA-1 is NOT supported — explicitly rejected at
|
|
11
|
-
*
|
|
12
|
-
* what RFC 6238 prescribes as the
|
|
13
|
-
* (Authy, 1Password, Bitwarden,
|
|
14
|
-
* support SHA-512 when the otpauth
|
|
15
|
-
* `algorithm` parameter. Operators selecting
|
|
16
|
-
* should verify SHA-512 support — Google
|
|
17
|
-
* versions and minimal hardware tokens may not. A
|
|
18
|
-
* and surface" stance is preferable to a silent
|
|
19
|
-
* undermines the framework's algorithm posture.
|
|
10
|
+
* SHA-1 is NOT supported for generation — explicitly rejected at
|
|
11
|
+
* compute() / generate() / uri(). Most authenticator apps still default
|
|
12
|
+
* to SHA-1 for legacy reasons (it's what RFC 6238 prescribes as the
|
|
13
|
+
* default), but the major modern apps (Authy, 1Password, Bitwarden,
|
|
14
|
+
* Microsoft Authenticator, Aegis) all support SHA-512 when the otpauth
|
|
15
|
+
* URI declares it via the `algorithm` parameter. Operators selecting
|
|
16
|
+
* authenticator apps should verify SHA-512 support — Google
|
|
17
|
+
* Authenticator's older versions and minimal hardware tokens may not. A
|
|
18
|
+
* clear "reject SHA-1 and surface" stance is preferable to a silent
|
|
19
|
+
* SHA-1 default that undermines the framework's algorithm posture.
|
|
20
|
+
*
|
|
21
|
+
* One verify-only exception exists for migration: a consumer holding
|
|
22
|
+
* pre-existing RFC-6238-default secrets (SHA-1) can verify a single
|
|
23
|
+
* legacy code during a re-enrollment flow with
|
|
24
|
+
* `verify(secret, code, { algorithm: "sha1", verifyOnly: true })`. This
|
|
25
|
+
* widens nothing on the generation side — compute()/generate()/uri()
|
|
26
|
+
* still refuse SHA-1, so new-enrollment posture stays SHA-512-only — it
|
|
27
|
+
* only lets the maintained verifier (separator stripping, 64-bit
|
|
28
|
+
* counter, drift/replay semantics) authenticate the final legacy login
|
|
29
|
+
* instead of forcing the consumer to hand-roll a parallel HOTP. Each
|
|
30
|
+
* such verification emits an `auth.totp.legacy_sha1_verify` audit signal
|
|
31
|
+
* so a compliance dashboard sees the migration traffic.
|
|
20
32
|
*
|
|
21
33
|
* Public API:
|
|
22
34
|
*
|
|
@@ -24,6 +36,9 @@
|
|
|
24
36
|
* totp.generate(secret, opts?) → string (current code)
|
|
25
37
|
* totp.compute(secret, timeStep, opts?) → string (code at specific step)
|
|
26
38
|
* totp.verify(secret, code, opts?) → step | false
|
|
39
|
+
* (opts.verifyOnly + opts.algorithm
|
|
40
|
+
* "sha1" accepts a legacy secret;
|
|
41
|
+
* see the SHA-1 note above)
|
|
27
42
|
* totp.uri(secret, account, opts) → string (otpauth://…)
|
|
28
43
|
* totp.generateBackupCodes(opts?) → string[]
|
|
29
44
|
*
|
|
@@ -77,6 +92,13 @@ var DEFAULT_DRIFT_STEPS = 1;
|
|
|
77
92
|
// docstring for the rationale.
|
|
78
93
|
var DEFAULT_ALGORITHM = "sha512";
|
|
79
94
|
var SUPPORTED_ALGORITHMS = Object.freeze(["sha256", "sha512"]);
|
|
95
|
+
// Algorithms accepted ONLY on the verify() path, and only when the caller
|
|
96
|
+
// explicitly passes { verifyOnly: true } — never by compute()/generate()/
|
|
97
|
+
// uri(). SHA-1 is RFC 6238's default and the algorithm pre-existing secrets
|
|
98
|
+
// were provisioned with; this lets a consumer authenticate a final legacy
|
|
99
|
+
// code during re-enrollment without hand-rolling a parallel HOTP, while
|
|
100
|
+
// keeping new-enrollment generation SHA-512-only.
|
|
101
|
+
var VERIFY_ONLY_ALGORITHMS = Object.freeze(["sha1"]);
|
|
80
102
|
// Default secret length matches the HMAC-SHA512 block size (1024 bits).
|
|
81
103
|
// MIN_SECRET_BYTES is RFC 4226 §4's hard floor — operators can opt down
|
|
82
104
|
// to it for QR provisioning into apps that balk at long secrets, but no
|
|
@@ -106,28 +128,51 @@ function _base32Decode(str) {
|
|
|
106
128
|
|
|
107
129
|
// ---- Core HOTP (RFC 4226 §5.3) ----
|
|
108
130
|
|
|
109
|
-
function
|
|
131
|
+
function _emitTotpAudit(action, alg) {
|
|
132
|
+
setImmediate(function () {
|
|
133
|
+
try {
|
|
134
|
+
var auditMod = require("./audit"); // allow:inline-require — circular-load defense
|
|
135
|
+
auditMod.safeEmit({
|
|
136
|
+
action: action,
|
|
137
|
+
outcome: "success",
|
|
138
|
+
metadata: { algorithm: alg, frameworkDefault: DEFAULT_ALGORITHM },
|
|
139
|
+
});
|
|
140
|
+
} catch (_e) { /* drop-silent */ }
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ctx.allowVerifyOnly is set ONLY by verify() — it is the single gate that
|
|
145
|
+
// lets a verify-only legacy algorithm (SHA-1) through, and even then only
|
|
146
|
+
// when the caller also passes opts.verifyOnly:true. compute()/generate()/
|
|
147
|
+
// uri() call _resolveOpts with no ctx, so SHA-1 can never reach the
|
|
148
|
+
// generation path regardless of opts.
|
|
149
|
+
function _resolveOpts(opts, ctx) {
|
|
110
150
|
opts = opts || {};
|
|
151
|
+
var allowVerifyOnly = !!(ctx && ctx.allowVerifyOnly);
|
|
111
152
|
var alg = (opts.algorithm || DEFAULT_ALGORITHM).toLowerCase();
|
|
153
|
+
var verifyOnly = false;
|
|
112
154
|
if (SUPPORTED_ALGORITHMS.indexOf(alg) === -1) {
|
|
113
|
-
|
|
114
|
-
|
|
155
|
+
if (allowVerifyOnly && opts.verifyOnly === true && VERIFY_ONLY_ALGORITHMS.indexOf(alg) !== -1) {
|
|
156
|
+
// Legacy-secret verification path — accepted, but flagged so the
|
|
157
|
+
// HOTP core knows this is a one-final-login migration verify and the
|
|
158
|
+
// audit trail records it.
|
|
159
|
+
verifyOnly = true;
|
|
160
|
+
} else {
|
|
161
|
+
throw new AuthError("auth-totp/bad-alg",
|
|
162
|
+
"algorithm must be one of " + SUPPORTED_ALGORITHMS.join(", ") + " (got: " + alg + ")" +
|
|
163
|
+
(VERIFY_ONLY_ALGORITHMS.indexOf(alg) !== -1
|
|
164
|
+
? "; " + alg + " is accepted only by verify() with { verifyOnly: true }"
|
|
165
|
+
: ""));
|
|
166
|
+
}
|
|
115
167
|
}
|
|
116
168
|
// SHA-256 is supported for back-compat with authenticator apps that
|
|
117
|
-
// don't yet honor SHA-512. Emit an audit signal
|
|
118
|
-
//
|
|
119
|
-
//
|
|
169
|
+
// don't yet honor SHA-512. Emit an audit signal when it's selected so
|
|
170
|
+
// operator compliance dashboards see which accounts run on the weaker
|
|
171
|
+
// hash and can plan the migration; SHA-1 verify-only gets its own signal.
|
|
120
172
|
if (alg === "sha256") {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
auditMod.safeEmit({
|
|
125
|
-
action: "auth.totp.algorithm_downgraded",
|
|
126
|
-
outcome: "success",
|
|
127
|
-
metadata: { algorithm: alg, frameworkDefault: DEFAULT_ALGORITHM },
|
|
128
|
-
});
|
|
129
|
-
} catch (_e) { /* drop-silent */ }
|
|
130
|
-
});
|
|
173
|
+
_emitTotpAudit("auth.totp.algorithm_downgraded", alg);
|
|
174
|
+
} else if (verifyOnly) {
|
|
175
|
+
_emitTotpAudit("auth.totp.legacy_sha1_verify", alg);
|
|
131
176
|
}
|
|
132
177
|
var digits = opts.digits != null ? opts.digits : DEFAULT_DIGITS;
|
|
133
178
|
if (typeof digits !== "number" || digits < 6 || digits > 10) {
|
|
@@ -141,7 +186,10 @@ function _resolveOpts(opts) {
|
|
|
141
186
|
if (typeof driftSteps !== "number" || driftSteps < 0) {
|
|
142
187
|
throw new AuthError("auth-totp/bad-drift", "driftSteps must be >= 0 (got: " + driftSteps + ")");
|
|
143
188
|
}
|
|
144
|
-
return {
|
|
189
|
+
return {
|
|
190
|
+
algorithm: alg, digits: digits, stepSeconds: stepSeconds,
|
|
191
|
+
driftSteps: driftSteps, verifyOnly: verifyOnly,
|
|
192
|
+
};
|
|
145
193
|
}
|
|
146
194
|
|
|
147
195
|
function _validateSecret(secret) {
|
|
@@ -150,10 +198,11 @@ function _validateSecret(secret) {
|
|
|
150
198
|
}
|
|
151
199
|
}
|
|
152
200
|
|
|
153
|
-
// HOTP truncation per RFC 4226 §5.3 — produces digit-string code
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
201
|
+
// HOTP truncation per RFC 4226 §5.3 — produces a digit-string code from an
|
|
202
|
+
// ALREADY-RESOLVED opts object. Split out from compute() so verify() can run
|
|
203
|
+
// it with a verify-resolved algorithm (including the verify-only SHA-1 path)
|
|
204
|
+
// without going back through compute()'s resolver, which always refuses SHA-1.
|
|
205
|
+
function _hotp(secret, timeStep, resolved) {
|
|
157
206
|
var key = _base32Decode(secret);
|
|
158
207
|
if (key.length === 0) {
|
|
159
208
|
throw new AuthError("auth-totp/bad-secret", "secret decoded to zero bytes");
|
|
@@ -177,6 +226,13 @@ function compute(secret, timeStep, opts) {
|
|
|
177
226
|
return String(binCode % modulus).padStart(resolved.digits, "0");
|
|
178
227
|
}
|
|
179
228
|
|
|
229
|
+
// Public generation path — always refuses SHA-1 (no verify-only ctx).
|
|
230
|
+
function compute(secret, timeStep, opts) {
|
|
231
|
+
_validateSecret(secret);
|
|
232
|
+
var resolved = _resolveOpts(opts);
|
|
233
|
+
return _hotp(secret, timeStep, resolved);
|
|
234
|
+
}
|
|
235
|
+
|
|
180
236
|
// ---- Public API ----
|
|
181
237
|
|
|
182
238
|
function generateSecret(opts) {
|
|
@@ -203,7 +259,11 @@ function verify(secret, code, opts) {
|
|
|
203
259
|
// catching exceptions per call.
|
|
204
260
|
if (typeof secret !== "string" || secret.length === 0) return false;
|
|
205
261
|
if (code == null) return false;
|
|
206
|
-
|
|
262
|
+
// verify() is the single path that may accept a verify-only legacy
|
|
263
|
+
// algorithm (SHA-1) — gated on opts.verifyOnly inside _resolveOpts. A
|
|
264
|
+
// genuinely-bad algorithm/digits/step/drift still throws (a misconfigured
|
|
265
|
+
// verifier must surface, not silently read as "didn't match").
|
|
266
|
+
var resolved = _resolveOpts(opts, { allowVerifyOnly: true });
|
|
207
267
|
var nowMs = (opts && opts.now) || Date.now();
|
|
208
268
|
var currentStep = Math.floor(nowMs / 1000 / resolved.stepSeconds);
|
|
209
269
|
var lastUsedStep = (opts && typeof opts.lastUsedStep === "number") ? opts.lastUsedStep : null;
|
|
@@ -224,7 +284,11 @@ function verify(secret, code, opts) {
|
|
|
224
284
|
var step = currentStep + d;
|
|
225
285
|
if (lastUsedStep !== null && step <= lastUsedStep) continue; // reject replays at-or-below the last accepted step
|
|
226
286
|
var expected;
|
|
227
|
-
|
|
287
|
+
// Run the HOTP core with the verify-resolved algorithm directly — going
|
|
288
|
+
// back through compute() would re-resolve and refuse the verify-only
|
|
289
|
+
// SHA-1 path. Resolution already happened once above (no per-step audit
|
|
290
|
+
// spam, and the legacy-verify audit fires exactly once per verify()).
|
|
291
|
+
try { expected = _hotp(secret, step, resolved); }
|
|
228
292
|
catch (_e) { return false; }
|
|
229
293
|
var expectedBuf = Buffer.from(expected);
|
|
230
294
|
if (timingSafeEqual(expectedBuf, userBuf)) {
|
|
@@ -294,4 +358,5 @@ module.exports = {
|
|
|
294
358
|
DEFAULT_SECRET_BYTES: DEFAULT_SECRET_BYTES,
|
|
295
359
|
MIN_SECRET_BYTES: MIN_SECRET_BYTES,
|
|
296
360
|
SUPPORTED_ALGORITHMS: SUPPORTED_ALGORITHMS,
|
|
361
|
+
VERIFY_ONLY_ALGORITHMS: VERIFY_ONLY_ALGORITHMS,
|
|
297
362
|
};
|