@blamejs/core 0.15.13 → 0.15.15

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 (208) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +38 -6
  4. package/lib/agent-event-bus.js +13 -0
  5. package/lib/agent-idempotency.js +5 -1
  6. package/lib/agent-snapshot.js +32 -2
  7. package/lib/ai-aedt-bias-audit.js +2 -1
  8. package/lib/ai-content-detect.js +1 -3
  9. package/lib/ai-frontier-protocol.js +1 -1
  10. package/lib/ai-model-manifest.js +1 -1
  11. package/lib/ai-output.js +16 -7
  12. package/lib/api-snapshot.js +4 -1
  13. package/lib/app-shutdown.js +7 -1
  14. package/lib/archive-gz.js +9 -0
  15. package/lib/archive-read.js +9 -7
  16. package/lib/archive-tar-read.js +51 -8
  17. package/lib/archive.js +4 -2
  18. package/lib/asn1-der.js +70 -22
  19. package/lib/atomic-file.js +204 -2
  20. package/lib/audit-chain.js +54 -5
  21. package/lib/audit-daily-review.js +12 -2
  22. package/lib/audit-sign.js +2 -1
  23. package/lib/audit-tools.js +108 -23
  24. package/lib/audit.js +7 -2
  25. package/lib/auth/access-lock.js +2 -2
  26. package/lib/auth/bot-challenge.js +1 -1
  27. package/lib/auth/ciba.js +71 -8
  28. package/lib/auth/dpop.js +15 -1
  29. package/lib/auth/fido-mds3.js +30 -13
  30. package/lib/auth/jwt.js +21 -5
  31. package/lib/auth/lockout.js +18 -2
  32. package/lib/auth/oauth.js +8 -2
  33. package/lib/auth/passkey.js +1 -1
  34. package/lib/auth/password.js +1 -1
  35. package/lib/auth/saml.js +42 -12
  36. package/lib/auth/sd-jwt-vc.js +24 -4
  37. package/lib/auth/status-list.js +14 -2
  38. package/lib/auth/step-up.js +9 -1
  39. package/lib/auth-bot-challenge.js +21 -2
  40. package/lib/backup/bundle.js +7 -2
  41. package/lib/backup/crypto.js +23 -8
  42. package/lib/backup/index.js +41 -20
  43. package/lib/backup/manifest.js +7 -1
  44. package/lib/break-glass.js +41 -22
  45. package/lib/cbor.js +34 -11
  46. package/lib/cdn-cache-control.js +7 -3
  47. package/lib/cert.js +5 -3
  48. package/lib/cli.js +5 -1
  49. package/lib/cloud-events.js +3 -2
  50. package/lib/cluster-storage.js +7 -3
  51. package/lib/codepoint-class.js +17 -0
  52. package/lib/compliance-eaa.js +1 -1
  53. package/lib/compliance-sanctions.js +9 -7
  54. package/lib/compliance.js +1 -1
  55. package/lib/config-drift.js +22 -8
  56. package/lib/content-credentials.js +11 -8
  57. package/lib/content-digest.js +11 -4
  58. package/lib/cookies.js +10 -2
  59. package/lib/cose.js +20 -0
  60. package/lib/crdt.js +2 -1
  61. package/lib/crypto-field.js +48 -24
  62. package/lib/crypto.js +18 -4
  63. package/lib/csp.js +13 -0
  64. package/lib/daemon.js +4 -1
  65. package/lib/data-act.js +27 -4
  66. package/lib/db-file-lifecycle.js +14 -6
  67. package/lib/db-query.js +102 -11
  68. package/lib/db.js +32 -15
  69. package/lib/dora.js +43 -13
  70. package/lib/dr-runbook.js +1 -1
  71. package/lib/dsa.js +2 -1
  72. package/lib/dsr.js +22 -8
  73. package/lib/early-hints.js +19 -0
  74. package/lib/eat.js +5 -1
  75. package/lib/external-db.js +60 -4
  76. package/lib/fda-21cfr11.js +30 -5
  77. package/lib/flag-providers.js +6 -2
  78. package/lib/forms.js +1 -1
  79. package/lib/gate-contract.js +46 -5
  80. package/lib/gdpr-ropa.js +18 -9
  81. package/lib/graphql-federation.js +17 -4
  82. package/lib/guard-all.js +2 -2
  83. package/lib/guard-dsn.js +1 -1
  84. package/lib/guard-envelope.js +1 -1
  85. package/lib/guard-html.js +9 -11
  86. package/lib/guard-imap-command.js +1 -1
  87. package/lib/guard-jmap.js +1 -1
  88. package/lib/guard-json.js +14 -6
  89. package/lib/guard-mail-move.js +1 -1
  90. package/lib/guard-managesieve-command.js +1 -1
  91. package/lib/guard-pop3-command.js +1 -1
  92. package/lib/guard-smtp-command.js +1 -1
  93. package/lib/guard-svg.js +8 -9
  94. package/lib/html-balance.js +7 -3
  95. package/lib/http-client-cookie-jar.js +33 -12
  96. package/lib/http-client.js +225 -53
  97. package/lib/iab-tcf.js +3 -2
  98. package/lib/importmap-integrity.js +41 -1
  99. package/lib/incident-report.js +9 -6
  100. package/lib/json-patch.js +1 -1
  101. package/lib/json-path.js +24 -3
  102. package/lib/jtd.js +2 -2
  103. package/lib/legal-hold.js +24 -8
  104. package/lib/log.js +2 -2
  105. package/lib/mail-agent.js +2 -2
  106. package/lib/mail-arf.js +1 -1
  107. package/lib/mail-auth.js +27 -4
  108. package/lib/mail-bimi.js +16 -16
  109. package/lib/mail-bounce.js +3 -3
  110. package/lib/mail-crypto-smime.js +71 -6
  111. package/lib/mail-deploy.js +9 -5
  112. package/lib/mail-dkim.js +20 -7
  113. package/lib/mail-greylist.js +2 -4
  114. package/lib/mail-helo.js +2 -4
  115. package/lib/mail-journal.js +11 -8
  116. package/lib/mail-mdn.js +8 -4
  117. package/lib/mail-rbl.js +2 -4
  118. package/lib/mail-scan.js +3 -5
  119. package/lib/mail-server-jmap.js +4 -1
  120. package/lib/mail-server-registry.js +1 -1
  121. package/lib/mail-server-tls.js +9 -2
  122. package/lib/mail-spam-score.js +2 -4
  123. package/lib/mail-store-fts.js +1 -1
  124. package/lib/mail.js +22 -2
  125. package/lib/markup-tokenizer.js +24 -0
  126. package/lib/mcp.js +6 -4
  127. package/lib/mdoc.js +26 -3
  128. package/lib/metrics.js +14 -3
  129. package/lib/middleware/api-encrypt.js +2 -2
  130. package/lib/middleware/body-parser.js +10 -4
  131. package/lib/middleware/bot-guard.js +26 -18
  132. package/lib/middleware/clear-site-data.js +5 -1
  133. package/lib/middleware/compose-pipeline.js +39 -5
  134. package/lib/middleware/compression.js +9 -0
  135. package/lib/middleware/cors.js +32 -23
  136. package/lib/middleware/csrf-protect.js +60 -21
  137. package/lib/middleware/daily-byte-quota.js +6 -4
  138. package/lib/middleware/fetch-metadata.js +28 -4
  139. package/lib/middleware/network-allowlist.js +61 -30
  140. package/lib/middleware/rate-limit.js +25 -16
  141. package/lib/middleware/scim-server.js +2 -1
  142. package/lib/middleware/security-headers.js +24 -6
  143. package/lib/middleware/speculation-rules.js +6 -3
  144. package/lib/middleware/tus-upload.js +2 -2
  145. package/lib/money.js +1 -1
  146. package/lib/mtls-ca.js +10 -6
  147. package/lib/network-dns-resolver.js +1 -1
  148. package/lib/network-dns.js +9 -2
  149. package/lib/network-dnssec.js +2 -1
  150. package/lib/network-smtp-policy.js +23 -5
  151. package/lib/network-tls.js +27 -5
  152. package/lib/network-tsig.js +2 -2
  153. package/lib/nis2-report.js +1 -1
  154. package/lib/nist-crosswalk.js +1 -1
  155. package/lib/ntp-check.js +28 -0
  156. package/lib/numeric-bounds.js +9 -0
  157. package/lib/object-store/azure-blob.js +1 -2
  158. package/lib/object-store/gcs-bucket-ops.js +4 -2
  159. package/lib/object-store/gcs.js +6 -4
  160. package/lib/object-store/http-put.js +1 -2
  161. package/lib/object-store/http-request.js +30 -1
  162. package/lib/object-store/local.js +37 -17
  163. package/lib/object-store/sigv4.js +1 -2
  164. package/lib/observability-otlp-exporter.js +20 -4
  165. package/lib/outbox.js +11 -4
  166. package/lib/parsers/safe-xml.js +1 -1
  167. package/lib/parsers/safe-yaml.js +21 -3
  168. package/lib/pipl-cn.js +11 -8
  169. package/lib/queue-local.js +10 -3
  170. package/lib/redact.js +7 -3
  171. package/lib/request-helpers.js +347 -36
  172. package/lib/resource-access-lock.js +3 -3
  173. package/lib/restore-bundle.js +46 -18
  174. package/lib/restore-rollback.js +10 -4
  175. package/lib/restore.js +19 -0
  176. package/lib/retention.js +20 -4
  177. package/lib/router.js +17 -4
  178. package/lib/safe-ical.js +2 -2
  179. package/lib/safe-icap.js +1 -1
  180. package/lib/safe-json.js +70 -0
  181. package/lib/safe-sieve.js +1 -1
  182. package/lib/safe-vcard.js +1 -1
  183. package/lib/sandbox-worker.js +6 -0
  184. package/lib/sandbox.js +1 -1
  185. package/lib/scheduler.js +17 -1
  186. package/lib/self-update-standalone-verifier.js +16 -0
  187. package/lib/session.js +62 -120
  188. package/lib/sql.js +25 -3
  189. package/lib/static.js +65 -13
  190. package/lib/template.js +7 -5
  191. package/lib/tenant-quota.js +52 -19
  192. package/lib/tsa.js +5 -2
  193. package/lib/vault/index.js +5 -0
  194. package/lib/vault/passphrase-ops.js +22 -26
  195. package/lib/vault/passphrase-source.js +8 -3
  196. package/lib/vault/rotate.js +13 -18
  197. package/lib/vault/seal-pem-file.js +4 -1
  198. package/lib/vc.js +1 -1
  199. package/lib/vendor/MANIFEST.json +10 -10
  200. package/lib/vendor/public-suffix-list.dat +23 -10
  201. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  202. package/lib/webhook.js +16 -1
  203. package/lib/websocket.js +1 -1
  204. package/lib/worm.js +1 -1
  205. package/lib/ws-client.js +83 -46
  206. package/lib/x509-chain.js +91 -0
  207. package/package.json +1 -1
  208. package/sbom.cdx.json +6 -6
package/lib/session.js CHANGED
@@ -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;
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;
295
285
  }
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;
286
+ return requestHelpers.clientIp;
305
287
  }
306
288
 
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) || "";
366
- }
367
- if (ip.indexOf(":") !== -1) return _maskIpv6(ip, IPV6_DEFAULT_PREFIX) || "";
368
- if (ip.indexOf(".") !== -1) return _maskIpv4(ip, IPV4_DEFAULT_PREFIX) || "";
369
- return "";
370
- }
371
-
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;
@@ -843,8 +761,16 @@ async function destroyAllForUser(userId) {
843
761
  // userId is sealed; look up via derived userIdHash.
844
762
  var lookup = cryptoField.lookupHash(SESSION_TABLE, "userId", userId);
845
763
  if (!lookup) {
764
+ // The session table's userIdHash derived-hash schema is registered during
765
+ // b.db.init(). A pluggable-store consumer (b.session.useStore) who never
766
+ // called b.db.init() lands here first — surface that, not just an opaque
767
+ // "framework misconfigured", since destroyAllForUser still needs b.db for
768
+ // the userIdHash index + the stateless valid-from boundary.
846
769
  throw _err("MISCONFIGURED",
847
- "the session table schema is missing the userIdHash derived hash framework misconfigured",
770
+ "session.destroyAllForUser: the session table's userIdHash derived-hash schema is " +
771
+ "not registered. It is registered during b.db.init() — call b.db.init() at boot even " +
772
+ "when session data lives in a pluggable store (b.session.useStore). If b.db is already " +
773
+ "initialized, the session table schema is misconfigured.",
848
774
  true);
849
775
  }
850
776
  // Dual-read across the keyed-MAC flip: a pre-v0.15.0 session row carries
@@ -860,8 +786,24 @@ async function destroyAllForUser(userId) {
860
786
  var result = await _currentStore().execute(built.sql, built.params);
861
787
  // Also raise the stateless valid-from boundary so a "logout everywhere"
862
788
  // revokes the operator's stateless tokens (sealed cookies / JWTs checked via
863
- // b.session.check) too, not only the store-backed rows just deleted.
864
- await bump(userId);
789
+ // b.session.check) too, not only the store-backed rows just deleted. This
790
+ // bump writes to the FRAMEWORK db (clusterStorage); a pluggable-store consumer
791
+ // (b.session.useStore) who never ran b.db.init() would otherwise surface the
792
+ // opaque "db/not-initialized" here, after the store delete already succeeded.
793
+ // Rethrow it as a session-specific, actionable error.
794
+ try {
795
+ await bump(userId);
796
+ } catch (e) {
797
+ if (e && e.code === "db/not-initialized") {
798
+ throw _err("MISCONFIGURED",
799
+ "session.destroyAllForUser raises the stateless valid-from boundary (so a " +
800
+ "logout-everywhere also revokes sealed-cookie / JWT sessions), which requires " +
801
+ "b.db.init() — call it at boot even when session data lives in a pluggable " +
802
+ "store (b.session.useStore). The store-backed rows were already deleted; rerun " +
803
+ "after b.db.init() to also raise the stateless boundary.", true);
804
+ }
805
+ throw e;
806
+ }
865
807
  return result.rowCount || 0;
866
808
  }
867
809
 
@@ -1034,7 +976,7 @@ async function rotate(oldToken, opts) {
1034
976
  "so the device binding can be re-keyed to the new session id", true);
1035
977
  }
1036
978
  if (!newDataObj) newDataObj = {};
1037
- newDataObj.__bj_fingerprint = _hashFingerprint(newSid, _buildFingerprintInputs(opts.req, fpFields));
979
+ newDataObj.__bj_fingerprint = _hashFingerprint(newSid, _buildFingerprintInputs(opts.req, fpFields, _clientIpResolver(opts)));
1038
980
  }
1039
981
 
1040
982
  var dataJson = newDataObj ? JSON.stringify(newDataObj) : null;
package/lib/sql.js CHANGED
@@ -439,7 +439,7 @@ class SqlFunction {
439
439
  throw _err("b.sql.fn(name): name must be a string", "sql-builder/bad-fn");
440
440
  }
441
441
  var key = name.toUpperCase();
442
- if (SQL_FUNCTIONS[key] === undefined) {
442
+ if (!Object.prototype.hasOwnProperty.call(SQL_FUNCTIONS, key)) {
443
443
  throw _err("b.sql.fn(name): '" + name + "' is not an allowlisted SQL function " +
444
444
  "(NOW / CURRENT_TIMESTAMP / CURRENT_DATE / CURRENT_TIME); a bound value uses a ? " +
445
445
  "placeholder, an arbitrary expression uses a guarded raw fragment", "sql-builder/bad-fn");
@@ -513,7 +513,7 @@ function _castType(type, dialect) {
513
513
  throw _err("cast type must be a non-empty string", "sql-builder/bad-cast");
514
514
  }
515
515
  var key = type.toLowerCase();
516
- if (CAST_TYPES[key] === undefined) {
516
+ if (!Object.prototype.hasOwnProperty.call(CAST_TYPES, key)) {
517
517
  throw _err("cast type '" + type + "' is not on the allowlist (jsonb / json / " +
518
518
  "interval / uuid / text / int / bigint / timestamptz / boolean)", "sql-builder/bad-cast");
519
519
  }
@@ -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") {
@@ -3519,7 +3541,7 @@ var catalog = Object.freeze({
3519
3541
  * b.sql.pragma("journal_mode").sql; // -> 'PRAGMA journal_mode' (read)
3520
3542
  */
3521
3543
  function pragma(verb, arg) {
3522
- if (typeof verb !== "string" || CATALOG_PRAGMA_VERBS[verb] === undefined) {
3544
+ if (typeof verb !== "string" || !Object.prototype.hasOwnProperty.call(CATALOG_PRAGMA_VERBS, verb)) {
3523
3545
  throw _err("pragma: verb '" + verb + "' is not on the allowlist (journal_mode / " +
3524
3546
  "synchronous / wal_checkpoint); a PRAGMA outside this set is refused by design",
3525
3547
  "sql-builder/bad-pragma");
package/lib/static.js CHANGED
@@ -42,6 +42,7 @@ var nodeFs = require("node:fs");
42
42
  var fsp = require("node:fs/promises");
43
43
  var nodeCrypto = require("node:crypto");
44
44
  var nodePath = require("node:path");
45
+ var atomicFile = require("./atomic-file");
45
46
  var C = require("./constants");
46
47
  var gateContract = require("./gate-contract");
47
48
  var lazyRequire = require("./lazy-require");
@@ -240,10 +241,16 @@ async function _readMeta(root, candidate) {
240
241
  var sri = nodeCrypto.createHash("sha384");
241
242
  var sha3 = nodeCrypto.createHash("sha3-512");
242
243
  await new Promise(function (resolve, reject) {
243
- // The path handed to createReadStream is the confined output of
244
- // `_assertInsideRoot(root, candidate)` above (lexical resolve +
245
- // root-prefix containment), not the request-derived candidate.
246
- var s = nodeFs.createReadStream(absPath);
244
+ // The path is the confined output of `_assertInsideRoot(root, candidate)`
245
+ // above (lexical resolve + root-prefix containment). Open it O_NOFOLLOW and
246
+ // stream from that fd: lexical confinement still leaves a window where a
247
+ // symlink swapped in at the final component after the check would be
248
+ // followed by a plain createReadStream (CWE-22 / CWE-367); O_NOFOLLOW refuses
249
+ // it. The open throws synchronously on a symlink (ELOOP) / missing file —
250
+ // reject the hash promise so the caller falls back exactly as on a read error.
251
+ var s;
252
+ try { s = nodeFs.createReadStream(absPath, { fd: atomicFile.openNoFollowSync(absPath) }); }
253
+ catch (e) { reject(e); return; }
247
254
  s.on("data", function (chunk) { sri.update(chunk); sha3.update(chunk); });
248
255
  s.on("end", resolve);
249
256
  s.on("error", reject);
@@ -399,7 +406,7 @@ function _shouldForceAttachment(contentType, ext, contentSafetyMap, allowSvgRend
399
406
  return true;
400
407
  }
401
408
  if (bare.indexOf("text/") === 0) return false;
402
- if (SAFE_RENDER_RASTER_MIMES[bare]) return false;
409
+ if (Object.prototype.hasOwnProperty.call(SAFE_RENDER_RASTER_MIMES, bare)) return false;
403
410
  if (bare === "image/svg+xml") {
404
411
  if (!allowSvgRender) return true;
405
412
  if (!contentSafetyMap || typeof contentSafetyMap !== "object") return true;
@@ -884,10 +891,21 @@ function create(opts) {
884
891
  // `_assertInsideRoot`, not the request-derived candidate.
885
892
  var confined = _assertInsideRoot(root, absPath);
886
893
  if (!confined) return { ok: false, reason: "read-failed" };
894
+ // Only the first 64 KiB is ever inspected, so read JUST that prefix from an
895
+ // O_NOFOLLOW fd: the previous fsp.readFile slurped the WHOLE file into memory
896
+ // just to sniff its header — a request-reachable OOM on a user-content mount
897
+ // — and followed a post-confinement symlink swap. A PREFIX read (not a
898
+ // maxBytes refusal) is required so a large but allowed file still serves.
887
899
  var sample;
888
- try { sample = await fsp.readFile(confined, { flag: "r" }); }
889
- catch (_e) { return { ok: false, reason: "read-failed" }; }
890
- var detected = fileType.detect(sample.slice(0, C.BYTES.kib(64))) || {};
900
+ try {
901
+ var sniffFd = atomicFile.openNoFollowSync(confined);
902
+ try {
903
+ var sniffBuf = Buffer.alloc(C.BYTES.kib(64));
904
+ var sniffN = nodeFs.readSync(sniffFd, sniffBuf, 0, sniffBuf.length, 0);
905
+ sample = sniffBuf.slice(0, sniffN);
906
+ } finally { nodeFs.closeSync(sniffFd); }
907
+ } catch (_e) { return { ok: false, reason: "read-failed" }; }
908
+ var detected = fileType.detect(sample) || {};
891
909
  if (!detected.mime) return { ok: false, reason: "indeterminate" };
892
910
  if (allowedFileTypes.indexOf(detected.mime) === -1) {
893
911
  return { ok: false, reason: "not-allowed", detected: detected.mime };
@@ -1051,6 +1069,23 @@ function create(opts) {
1051
1069
  // creation can ever ride this code path.
1052
1070
  gateHandle = await fsp.open(gateConfined, gateOpenFlags, 0o600);
1053
1071
  var gateStat = await gateHandle.stat();
1072
+ // Cap before Buffer.alloc(gateStat.size): the gate buffers the WHOLE
1073
+ // file to inspect it, so a multi-GiB file on a user-content mount is a
1074
+ // request-reachable OOM lever. A file too large for the gate to vet is
1075
+ // refused (the gate's own "refuse" disposition) rather than buffered.
1076
+ if (gateStat.size > C.BYTES.mib(16)) {
1077
+ stats.failures += 1;
1078
+ _emitObs("staticServe.content_safety_refused", 1, { route: urlPath });
1079
+ try { await gateHandle.close(); } catch (_ce) { /* close best-effort */ }
1080
+ if (auditFailures) {
1081
+ emitAudit("staticServe.serve.failure", Object.assign({
1082
+ outcome: "failure", reason: "content_safety_too_large",
1083
+ resource: urlPath, ext: ext, sizeBytes: gateStat.size,
1084
+ }, actorCtx));
1085
+ }
1086
+ return writeErr(res, HTTP.UNSUPPORTED_MEDIA_TYPE,
1087
+ "content_safety_refused", "Unsupported Media Type");
1088
+ }
1054
1089
  gateBuf = Buffer.alloc(gateStat.size);
1055
1090
  var gateRead = 0;
1056
1091
  while (gateRead < gateStat.size) {
@@ -1135,8 +1170,11 @@ function create(opts) {
1135
1170
  "precondition_failed", "Precondition Failed");
1136
1171
  }
1137
1172
 
1138
- // Conditional: If-Modified-Since (304)
1139
- var ifModSince = headersIn["if-modified-since"];
1173
+ // Conditional: If-Modified-Since (304) — RFC 7232 §6: evaluated ONLY when
1174
+ // If-None-Match is absent. Otherwise a changed-ETag resource rewritten
1175
+ // within the same wall-clock second (mtime compared at second granularity)
1176
+ // would be falsely reported 304 despite the content hash differing.
1177
+ var ifModSince = !ifNone && headersIn["if-modified-since"];
1140
1178
  if (ifModSince) {
1141
1179
  var ims = Date.parse(ifModSince);
1142
1180
  if (isFinite(ims) && Math.floor(meta.mtimeMs / C.TIME.seconds(1)) <= Math.floor(ims / C.TIME.seconds(1))) {
@@ -1151,8 +1189,9 @@ function create(opts) {
1151
1189
  }
1152
1190
  }
1153
1191
 
1154
- // Conditional: If-Unmodified-Since (412)
1155
- var ifUnmodSince = headersIn["if-unmodified-since"];
1192
+ // Conditional: If-Unmodified-Since (412) — RFC 7232 §6: evaluated ONLY
1193
+ // when If-Match is absent.
1194
+ var ifUnmodSince = !ifMatch && headersIn["if-unmodified-since"];
1156
1195
  if (ifUnmodSince) {
1157
1196
  var ius = Date.parse(ifUnmodSince);
1158
1197
  if (isFinite(ius) && Math.floor(meta.mtimeMs / C.TIME.seconds(1)) > Math.floor(ius / C.TIME.seconds(1))) {
@@ -1348,7 +1387,20 @@ function create(opts) {
1348
1387
  }
1349
1388
 
1350
1389
  var streamOpts = range ? { start: range.start, end: range.end } : {};
1351
- var fileStream = nodeFs.createReadStream(streamTarget, streamOpts);
1390
+ // Open streamTarget (the re-confined _assertInsideRoot output) O_NOFOLLOW and
1391
+ // stream from that fd, so a symlink swapped in at the final component after
1392
+ // confinement is refused (ELOOP) rather than followed (CWE-22 / CWE-367).
1393
+ // Headers are already committed here, so a sync refusal can't re-issue a 404
1394
+ // — abort the connection instead of serving the swapped target.
1395
+ var fileStream;
1396
+ try {
1397
+ fileStream = nodeFs.createReadStream(streamTarget,
1398
+ Object.assign({ fd: atomicFile.openNoFollowSync(streamTarget) }, streamOpts));
1399
+ } catch (_openErr) {
1400
+ releaseSlot();
1401
+ try { res.destroy(); } catch (_d) { /* already torn down */ }
1402
+ return;
1403
+ }
1352
1404
 
1353
1405
  // Idle timeout — close the connection if the client stalls. Pattern is
1354
1406
  // a deadline-style debounce (clearTimeout + setTimeout) tied directly
package/lib/template.js CHANGED
@@ -93,6 +93,7 @@ var nodeFs = require("node:fs");
93
93
  var nodePath = require("node:path");
94
94
  var lazyRequire = require("./lazy-require");
95
95
  var validateOpts = require("./validate-opts");
96
+ var markupEscape = require("./markup-escape").markupEscape;
96
97
 
97
98
  // Lazy because b.template can be loaded before b.sandbox (which pulls
98
99
  // in node:worker_threads). Operators not opting into sandboxed helpers
@@ -116,9 +117,6 @@ var DEFAULT_STRING_TEMPLATE_BYTES = require("./constants").BYTES.kib(256);
116
117
  // HTML escape (exported)
117
118
  // ============================================================
118
119
 
119
- var ESCAPE_MAP = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#x27;" };
120
- var ESCAPE_RE = /[&<>"']/g;
121
-
122
120
  /**
123
121
  * @primitive b.template.escapeHtml
124
122
  * @signature b.template.escapeHtml(value)
@@ -141,8 +139,12 @@ var ESCAPE_RE = /[&<>"']/g;
141
139
  */
142
140
  function escapeHtml(value) {
143
141
  if (value === null || value === undefined) return "";
144
- var s = typeof value === "string" ? value : String(value);
145
- return s.replace(ESCAPE_RE, function (c) { return ESCAPE_MAP[c]; });
142
+ // Delegate the actual escaping to the centralized markup escaper so the
143
+ // five-character HTML set (& < > " ') is defined in one place — a divergence
144
+ // between this and the shared escaper is an XSS / XML-injection surface. The
145
+ // apostrophe is escaped to its numeric form for safety in single-quoted
146
+ // attribute contexts (the historical behavior of this primitive).
147
+ return markupEscape(value, { apos: "&#x27;" });
146
148
  }
147
149
 
148
150
  // ============================================================
@@ -52,6 +52,7 @@
52
52
 
53
53
  var C = require("./constants");
54
54
  var lazyRequire = require("./lazy-require");
55
+ var boundedMap = require("./bounded-map");
55
56
  var validateOpts = require("./validate-opts");
56
57
  var { defineClass } = require("./framework-error");
57
58
 
@@ -354,18 +355,47 @@ function budget(opts) {
354
355
  }
355
356
  var auditOn = opts.audit !== false;
356
357
 
357
- // tenantId { windowStart, calls, rowsRead }
358
+ // TRUE sliding window (RFC-ish rolling counter, as advertised) — the prior
359
+ // first-call-pinned fixed window admitted ~2x the cap in a boundary burst.
360
+ // The window is split into BINS sub-bins; each observe lands in the current
361
+ // bin and the cap is enforced against the trailing-window SUM, so a burst
362
+ // straddling the reset can't double the rate. Mirrors network-byte-quota's
363
+ // _slideAndSum ring (here with two counters + a configurable bin width).
364
+ var BINS = 12;
365
+ var binMs = Math.max(1, Math.floor(windowMs / BINS));
366
+
367
+ // tenantId → { calls: number[BINS], rows: number[BINS], startBin }
358
368
  var counters = new Map();
359
369
 
360
- function _slot(tenantId, now) {
361
- var c = counters.get(tenantId);
362
- if (!c || (now - c.windowStart) >= windowMs) {
363
- c = { windowStart: now, calls: 0, rowsRead: 0 };
364
- counters.set(tenantId, c);
370
+ // Advance the ring so its BINS bins cover the trailing [nowBin-BINS+1 .. nowBin],
371
+ // zeroing bins that scrolled out of the window. Returns the tenant's ring.
372
+ function _slide(tenantId, now) {
373
+ var nowBin = Math.floor(now / binMs);
374
+ // A freshly-created ring is anchored at the current bin, so the advance
375
+ // below evaluates to 0 (no-op) on the create path — get-or-insert and the
376
+ // slide stay one code path.
377
+ var c = boundedMap.getOrInsert(counters, tenantId, function () {
378
+ return { calls: new Array(BINS).fill(0), rows: new Array(BINS).fill(0), startBin: nowBin - (BINS - 1) };
379
+ });
380
+ var advance = nowBin - (c.startBin + (BINS - 1));
381
+ if (advance > 0) {
382
+ if (advance >= BINS) {
383
+ c.calls.fill(0); c.rows.fill(0);
384
+ } else {
385
+ for (var i = 0; i < BINS - advance; i++) { c.calls[i] = c.calls[i + advance]; c.rows[i] = c.rows[i + advance]; }
386
+ for (var k = BINS - advance; k < BINS; k++) { c.calls[k] = 0; c.rows[k] = 0; }
387
+ }
388
+ c.startBin = nowBin - (BINS - 1);
365
389
  }
366
390
  return c;
367
391
  }
368
392
 
393
+ function _sum(c) {
394
+ var calls = 0, rows = 0;
395
+ for (var i = 0; i < BINS; i++) { calls += c.calls[i]; rows += c.rows[i]; }
396
+ return { calls: calls, rowsRead: rows };
397
+ }
398
+
369
399
  var _emitAudit = audit().namespaced(null, { audit: auditOn });
370
400
 
371
401
  function _emitMetric(name, n) {
@@ -378,16 +408,19 @@ function budget(opts) {
378
408
  "tenantQuota.budget.observe: tenantId", TenantQuotaError, "tenant-quota/bad-tenant");
379
409
  info = info || {};
380
410
  var rowsRead = (typeof info.rowsRead === "number" && info.rowsRead >= 0) ? info.rowsRead : 0;
381
- var now = Date.now();
382
- var c = _slot(tenantId, now);
383
- c.calls += 1;
384
- c.rowsRead += rowsRead;
411
+ // info.now lets a caller / test supply a deterministic clock (idiomatic —
412
+ // mirrors auth verifiers' opts.now); defaults to wall-clock.
413
+ var now = (typeof info.now === "number") ? info.now : Date.now();
414
+ var c = _slide(tenantId, now);
415
+ c.calls[BINS - 1] += 1;
416
+ c.rows[BINS - 1] += rowsRead;
417
+ var tot = _sum(c);
385
418
  var maxCalls = Math.max(1, Math.floor(qpsCap * (windowMs / C.TIME.seconds(1))));
386
- if (c.calls > maxCalls || c.rowsRead > rowsCap) {
419
+ if (tot.calls > maxCalls || tot.rowsRead > rowsCap) {
387
420
  _emitAudit("tenant.budget.exceeded", "denied", {
388
421
  tenantId: tenantId,
389
- calls: c.calls,
390
- rowsRead: c.rowsRead,
422
+ calls: tot.calls,
423
+ rowsRead: tot.rowsRead,
391
424
  qpsCap: qpsCap,
392
425
  rowsCap: rowsCap,
393
426
  windowMs: windowMs,
@@ -395,19 +428,19 @@ function budget(opts) {
395
428
  _emitMetric("tenant.budget.exceeded", 1);
396
429
  throw new TenantQuotaError("tenant-quota/budget-exceeded",
397
430
  "tenantQuota.budget: tenant '" + tenantId + "' exceeded budget " +
398
- "(calls=" + c.calls + "/" + maxCalls + ", rowsRead=" + c.rowsRead +
431
+ "(calls=" + tot.calls + "/" + maxCalls + ", rowsRead=" + tot.rowsRead +
399
432
  "/" + rowsCap + ", windowMs=" + windowMs + ")");
400
433
  }
401
- return { calls: c.calls, rowsRead: c.rowsRead, windowMs: windowMs };
434
+ return { calls: tot.calls, rowsRead: tot.rowsRead, windowMs: windowMs };
402
435
  }
403
436
 
404
437
  function snapshot(tenantId) {
405
- var now = Date.now();
406
- var c = counters.get(tenantId);
407
- if (!c || (now - c.windowStart) >= windowMs) {
438
+ if (!counters.has(tenantId)) {
408
439
  return { tenantId: tenantId, calls: 0, rowsRead: 0, windowMs: windowMs };
409
440
  }
410
- return { tenantId: tenantId, calls: c.calls, rowsRead: c.rowsRead, windowMs: windowMs };
441
+ var c = _slide(tenantId, Date.now());
442
+ var tot = _sum(c);
443
+ return { tenantId: tenantId, calls: tot.calls, rowsRead: tot.rowsRead, windowMs: windowMs };
411
444
  }
412
445
 
413
446
  function reset(tenantId) {
package/lib/tsa.js CHANGED
@@ -56,6 +56,7 @@ var bCrypto = require("./crypto");
56
56
  var safeBuffer = require("./safe-buffer");
57
57
  var asn1 = require("./asn1-der");
58
58
  var cms = require("./cms-codec");
59
+ var x509Chain = require("./x509-chain");
59
60
  var validateOpts = require("./validate-opts");
60
61
  var { defineClass } = require("./framework-error");
61
62
 
@@ -539,8 +540,10 @@ function _verifyChain(signerCertDer, tokenCerts, trustAnchorsPem, at) {
539
540
  throw new TsaError("tsa/chain-loop", "tsa.verifyToken: certificate chain did not terminate");
540
541
  }
541
542
  function _issued(issuer, subject) {
542
- try { return subject.checkIssued(issuer) && subject.verify(issuer.publicKey); }
543
- catch (_e) { return false; }
543
+ // Enforces basicConstraints cA:TRUE on the issuer in addition to the
544
+ // checkIssued + signature linkage — a non-CA cert can never be a chain
545
+ // issuer (basicConstraints bypass, CVE-2002-0862 class).
546
+ return x509Chain.issuerValidlyIssued(issuer, subject);
544
547
  }
545
548
  function _assertValidAt(cert, atMs) {
546
549
  if (atMs < cert.validFromDate.getTime() || atMs > cert.validToDate.getTime()) {
@@ -681,6 +681,11 @@ module.exports = {
681
681
  init: init,
682
682
  seal: seal,
683
683
  unseal: unseal,
684
+ // The default sealed-storage Store: a { seal, unseal } pair backed by the
685
+ // in-process vault key. b.cert.create (and other sealed-disk consumers)
686
+ // resolve `opts.vault || getDefaultStore()`; documented in their @opts.
687
+ getDefaultStore: function () { return { seal: seal, unseal: unseal }; },
688
+ Store: { seal: seal, unseal: unseal },
684
689
  getDerivedHashSalt: getDerivedHashSalt,
685
690
  getDerivedHashMacKey: getDerivedHashMacKey,
686
691
  _zeroizeAndReplace: _zeroizeAndReplace,