@blamejs/core 0.15.13 → 0.15.14

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 (203) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/lib/a2a-tasks.js +38 -6
  3. package/lib/agent-event-bus.js +13 -0
  4. package/lib/agent-idempotency.js +5 -1
  5. package/lib/agent-snapshot.js +32 -2
  6. package/lib/ai-aedt-bias-audit.js +2 -1
  7. package/lib/ai-content-detect.js +1 -3
  8. package/lib/ai-frontier-protocol.js +1 -1
  9. package/lib/ai-model-manifest.js +1 -1
  10. package/lib/ai-output.js +16 -7
  11. package/lib/api-snapshot.js +4 -1
  12. package/lib/app-shutdown.js +7 -1
  13. package/lib/archive-gz.js +9 -0
  14. package/lib/archive-read.js +9 -7
  15. package/lib/archive-tar-read.js +51 -8
  16. package/lib/archive.js +4 -2
  17. package/lib/asn1-der.js +70 -22
  18. package/lib/atomic-file.js +204 -2
  19. package/lib/audit-chain.js +54 -5
  20. package/lib/audit-daily-review.js +12 -2
  21. package/lib/audit-sign.js +2 -1
  22. package/lib/audit-tools.js +108 -23
  23. package/lib/audit.js +7 -2
  24. package/lib/auth/access-lock.js +2 -2
  25. package/lib/auth/bot-challenge.js +1 -1
  26. package/lib/auth/ciba.js +43 -4
  27. package/lib/auth/dpop.js +6 -1
  28. package/lib/auth/fido-mds3.js +5 -1
  29. package/lib/auth/jwt.js +2 -2
  30. package/lib/auth/lockout.js +18 -2
  31. package/lib/auth/passkey.js +1 -1
  32. package/lib/auth/password.js +1 -1
  33. package/lib/auth/saml.js +33 -13
  34. package/lib/auth/sd-jwt-vc.js +24 -4
  35. package/lib/auth/status-list.js +14 -2
  36. package/lib/auth/step-up.js +9 -1
  37. package/lib/auth-bot-challenge.js +21 -2
  38. package/lib/backup/bundle.js +7 -2
  39. package/lib/backup/crypto.js +23 -8
  40. package/lib/backup/index.js +41 -20
  41. package/lib/backup/manifest.js +7 -1
  42. package/lib/break-glass.js +41 -22
  43. package/lib/cbor.js +34 -11
  44. package/lib/cdn-cache-control.js +7 -3
  45. package/lib/cert.js +5 -3
  46. package/lib/cli.js +5 -1
  47. package/lib/cloud-events.js +3 -2
  48. package/lib/cluster-storage.js +7 -3
  49. package/lib/codepoint-class.js +17 -0
  50. package/lib/compliance-eaa.js +1 -1
  51. package/lib/compliance-sanctions.js +9 -7
  52. package/lib/compliance.js +1 -1
  53. package/lib/config-drift.js +22 -8
  54. package/lib/content-credentials.js +11 -8
  55. package/lib/content-digest.js +11 -4
  56. package/lib/cookies.js +10 -2
  57. package/lib/cose.js +20 -0
  58. package/lib/crdt.js +2 -1
  59. package/lib/crypto-field.js +29 -23
  60. package/lib/crypto.js +18 -4
  61. package/lib/csp.js +4 -0
  62. package/lib/daemon.js +4 -1
  63. package/lib/data-act.js +27 -4
  64. package/lib/db-file-lifecycle.js +14 -6
  65. package/lib/db-query.js +69 -9
  66. package/lib/db.js +32 -15
  67. package/lib/dora.js +43 -13
  68. package/lib/dr-runbook.js +1 -1
  69. package/lib/dsa.js +2 -1
  70. package/lib/dsr.js +22 -8
  71. package/lib/early-hints.js +19 -0
  72. package/lib/eat.js +5 -1
  73. package/lib/external-db.js +60 -4
  74. package/lib/fda-21cfr11.js +30 -5
  75. package/lib/flag-providers.js +6 -2
  76. package/lib/forms.js +1 -1
  77. package/lib/gate-contract.js +46 -5
  78. package/lib/gdpr-ropa.js +18 -9
  79. package/lib/graphql-federation.js +17 -4
  80. package/lib/guard-all.js +2 -2
  81. package/lib/guard-dsn.js +1 -1
  82. package/lib/guard-envelope.js +1 -1
  83. package/lib/guard-html.js +9 -11
  84. package/lib/guard-imap-command.js +1 -1
  85. package/lib/guard-jmap.js +1 -1
  86. package/lib/guard-json.js +14 -6
  87. package/lib/guard-mail-move.js +1 -1
  88. package/lib/guard-managesieve-command.js +1 -1
  89. package/lib/guard-pop3-command.js +1 -1
  90. package/lib/guard-smtp-command.js +1 -1
  91. package/lib/guard-svg.js +8 -9
  92. package/lib/html-balance.js +7 -3
  93. package/lib/http-client-cookie-jar.js +33 -12
  94. package/lib/http-client.js +225 -53
  95. package/lib/iab-tcf.js +3 -2
  96. package/lib/importmap-integrity.js +41 -1
  97. package/lib/incident-report.js +9 -6
  98. package/lib/json-patch.js +1 -1
  99. package/lib/json-path.js +24 -3
  100. package/lib/jtd.js +2 -2
  101. package/lib/legal-hold.js +24 -8
  102. package/lib/log.js +2 -2
  103. package/lib/mail-agent.js +2 -2
  104. package/lib/mail-arf.js +1 -1
  105. package/lib/mail-auth.js +3 -3
  106. package/lib/mail-bimi.js +16 -16
  107. package/lib/mail-bounce.js +3 -3
  108. package/lib/mail-crypto-smime.js +71 -6
  109. package/lib/mail-deploy.js +9 -5
  110. package/lib/mail-greylist.js +2 -4
  111. package/lib/mail-helo.js +2 -4
  112. package/lib/mail-journal.js +11 -8
  113. package/lib/mail-mdn.js +8 -4
  114. package/lib/mail-rbl.js +2 -4
  115. package/lib/mail-scan.js +3 -5
  116. package/lib/mail-server-jmap.js +4 -1
  117. package/lib/mail-server-registry.js +1 -1
  118. package/lib/mail-server-tls.js +9 -2
  119. package/lib/mail-spam-score.js +2 -4
  120. package/lib/mail-store-fts.js +1 -1
  121. package/lib/mail.js +22 -2
  122. package/lib/markup-tokenizer.js +24 -0
  123. package/lib/mcp.js +6 -4
  124. package/lib/mdoc.js +26 -3
  125. package/lib/metrics.js +14 -3
  126. package/lib/middleware/api-encrypt.js +2 -2
  127. package/lib/middleware/body-parser.js +10 -4
  128. package/lib/middleware/bot-guard.js +26 -18
  129. package/lib/middleware/clear-site-data.js +5 -1
  130. package/lib/middleware/compression.js +9 -0
  131. package/lib/middleware/cors.js +32 -23
  132. package/lib/middleware/csrf-protect.js +60 -21
  133. package/lib/middleware/daily-byte-quota.js +6 -4
  134. package/lib/middleware/fetch-metadata.js +28 -4
  135. package/lib/middleware/network-allowlist.js +61 -30
  136. package/lib/middleware/rate-limit.js +25 -16
  137. package/lib/middleware/scim-server.js +2 -1
  138. package/lib/middleware/security-headers.js +24 -6
  139. package/lib/middleware/speculation-rules.js +6 -3
  140. package/lib/middleware/tus-upload.js +2 -2
  141. package/lib/money.js +1 -1
  142. package/lib/mtls-ca.js +10 -6
  143. package/lib/network-dns-resolver.js +1 -1
  144. package/lib/network-dns.js +9 -2
  145. package/lib/network-dnssec.js +2 -1
  146. package/lib/network-smtp-policy.js +23 -5
  147. package/lib/network-tls.js +27 -5
  148. package/lib/network-tsig.js +2 -2
  149. package/lib/nis2-report.js +1 -1
  150. package/lib/nist-crosswalk.js +1 -1
  151. package/lib/ntp-check.js +28 -0
  152. package/lib/numeric-bounds.js +9 -0
  153. package/lib/object-store/azure-blob.js +1 -2
  154. package/lib/object-store/gcs-bucket-ops.js +4 -2
  155. package/lib/object-store/gcs.js +6 -4
  156. package/lib/object-store/http-put.js +1 -2
  157. package/lib/object-store/http-request.js +30 -1
  158. package/lib/object-store/local.js +37 -17
  159. package/lib/object-store/sigv4.js +1 -2
  160. package/lib/observability-otlp-exporter.js +20 -4
  161. package/lib/outbox.js +11 -4
  162. package/lib/parsers/safe-xml.js +1 -1
  163. package/lib/parsers/safe-yaml.js +21 -3
  164. package/lib/queue-local.js +10 -3
  165. package/lib/redact.js +7 -3
  166. package/lib/request-helpers.js +201 -23
  167. package/lib/resource-access-lock.js +3 -3
  168. package/lib/restore-bundle.js +46 -18
  169. package/lib/restore-rollback.js +10 -4
  170. package/lib/restore.js +19 -0
  171. package/lib/retention.js +20 -4
  172. package/lib/router.js +17 -4
  173. package/lib/safe-ical.js +2 -2
  174. package/lib/safe-icap.js +1 -1
  175. package/lib/safe-json.js +44 -0
  176. package/lib/safe-sieve.js +1 -1
  177. package/lib/safe-vcard.js +1 -1
  178. package/lib/sandbox-worker.js +6 -0
  179. package/lib/sandbox.js +1 -1
  180. package/lib/scheduler.js +17 -1
  181. package/lib/self-update-standalone-verifier.js +16 -0
  182. package/lib/session.js +27 -3
  183. package/lib/sql.js +3 -3
  184. package/lib/static.js +65 -13
  185. package/lib/template.js +7 -5
  186. package/lib/tenant-quota.js +52 -19
  187. package/lib/tsa.js +5 -2
  188. package/lib/vault/index.js +5 -0
  189. package/lib/vault/passphrase-ops.js +22 -26
  190. package/lib/vault/passphrase-source.js +8 -3
  191. package/lib/vault/rotate.js +13 -18
  192. package/lib/vault/seal-pem-file.js +4 -1
  193. package/lib/vc.js +1 -1
  194. package/lib/vendor/MANIFEST.json +10 -10
  195. package/lib/vendor/public-suffix-list.dat +23 -10
  196. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  197. package/lib/webhook.js +16 -1
  198. package/lib/websocket.js +1 -1
  199. package/lib/worm.js +1 -1
  200. package/lib/ws-client.js +57 -46
  201. package/lib/x509-chain.js +44 -0
  202. package/package.json +1 -1
  203. package/sbom.cdx.json +6 -6
@@ -356,9 +356,26 @@ function firstControlCharOffset(s, opts) {
356
356
  return -1;
357
357
  }
358
358
 
359
+ // Decode HTML numeric character references (hex &#x..; and decimal &#..;) just
360
+ // enough to expose a scheme hidden behind entity-encoding. The trailing
361
+ // semicolon is OPTIONAL — a browser decodes `&#106avascript:` (no semicolon)
362
+ // the same as `javascript:`, so a semicolon-required decoder lets the
363
+ // no-semicolon form bypass a scheme allowlist. Shared so guard-html / guard-svg
364
+ // / guard-markdown cannot drift on this (the bug class that shipped one buggy
365
+ // + one correct copy).
366
+ var NUMERIC_ENTITY_RE_G = /&#(?:x([0-9a-f]+)|(\d+));?/gi;
367
+ function decodeNumericEntities(s) {
368
+ return String(s == null ? "" : s).replace(NUMERIC_ENTITY_RE_G, function (m, hex, dec) {
369
+ var cp = hex !== undefined ? parseInt(hex, 16) : parseInt(dec, 10);
370
+ if (!isFinite(cp) || cp < 0 || cp > 0x10FFFF) return m;
371
+ try { return String.fromCodePoint(cp); } catch (_e) { return m; }
372
+ });
373
+ }
374
+
359
375
  module.exports = {
360
376
  isForbiddenControlChar: isForbiddenControlChar,
361
377
  firstControlCharOffset: firstControlCharOffset,
378
+ decodeNumericEntities: decodeNumericEntities,
362
379
  isAsciiAlnum: isAsciiAlnum,
363
380
  isUnreserved: isUnreserved,
364
381
  hex4: hex4,
@@ -82,7 +82,7 @@ function create(opts) {
82
82
  throw new ComplianceEaaError("compliance-eaa/bad-decl",
83
83
  "compliance.eaa.declareCriterion: decl must be an object with { conformance, note? }");
84
84
  }
85
- if (!VALID_CONFORMANCE[decl.conformance]) {
85
+ if (!Object.prototype.hasOwnProperty.call(VALID_CONFORMANCE, decl.conformance)) {
86
86
  throw new ComplianceEaaError("compliance-eaa/bad-conformance",
87
87
  "compliance.eaa.declareCriterion: conformance must be one of " + Object.keys(VALID_CONFORMANCE).join(", "));
88
88
  }
@@ -368,13 +368,14 @@ function create(opts) {
368
368
 
369
369
  for (var c = 0; c < index.length; c++) {
370
370
  var candidate = index[c];
371
- // Type filter: when input.type is set, skip candidates of
372
- // the wrong type unless candidate is an entity (entities can
373
- // be matched regardless to catch operator-side type errors).
374
- if (input.type && candidate.type !== input.type &&
375
- candidate.type !== "entity") {
376
- continue;
377
- }
371
+ // A sanctions screen MUST over-match on the legally-operative NAME. The
372
+ // operator-asserted input.type is an unverified counterparty
373
+ // self-classification, NOT data-driven non-match confidence using it to
374
+ // EXCLUDE a name hit inverts the screen to under-match (a false negative
375
+ // that processes a payment which should have been blocked). So never skip
376
+ // a candidate on type; the name match runs against every record and the
377
+ // type mismatch is surfaced as a non-dispositive `typeMatch` signal on
378
+ // each hit for operator triage.
378
379
 
379
380
  var bestForCandidate = { score: 0, name: "" };
380
381
  for (var qi = 0; qi < queryNames.length; qi++) {
@@ -404,6 +405,7 @@ function create(opts) {
404
405
  listed: candidate.listedAt,
405
406
  programs: candidate.programs,
406
407
  type: candidate.type,
408
+ typeMatch: input.type ? candidate.type === input.type : null,
407
409
  country: candidate.country,
408
410
  });
409
411
  }
package/lib/compliance.js CHANGED
@@ -953,7 +953,7 @@ var REGIME_MAP = Object.freeze({
953
953
  * b.compliance.describe("not-a-real-posture"); // → null
954
954
  */
955
955
  function describe(posture) {
956
- return REGIME_MAP[posture] || null;
956
+ return Object.prototype.hasOwnProperty.call(REGIME_MAP, posture) ? REGIME_MAP[posture] : null;
957
957
  }
958
958
 
959
959
  // POSTURE_DEFAULTS — per-posture configuration knobs that primitives
@@ -35,7 +35,6 @@
35
35
  * @card
36
36
  * Monitor + alert when runtime config diverges from a declared baseline.
37
37
  */
38
- var nodeFs = require("node:fs");
39
38
  var nodePath = require("node:path");
40
39
  var auditSign = require("./audit-sign");
41
40
  var canonicalJson = require("./canonical-json");
@@ -45,6 +44,7 @@ var safeJson = require("./safe-json");
45
44
  var validateOpts = require("./validate-opts");
46
45
  var { defineClass } = require("./framework-error");
47
46
  var atomicFile = require("./atomic-file");
47
+ var C = require("./constants");
48
48
 
49
49
  var audit = lazyRequire(function () { return require("./audit"); });
50
50
  var auditEmit = require("./audit-emit");
@@ -163,9 +163,13 @@ function create(opts) {
163
163
  var _emit = auditEmit.gatedReasonEmitter({ audit: auditOn, sink: auditInstance });
164
164
 
165
165
  function _readSidecar() {
166
- if (!nodeFs.existsSync(sidecarPath)) return null;
166
+ // Capped fd-bound read (no existsSync check-then-read window): the signed
167
+ // config-baseline sidecar is parsed + verified, so a tampered multi-GB file
168
+ // would OOM the reader before signature verify. refuseSymlink stays OFF: the
169
+ // data dir may be a symlink-mounted volume (k8s PVC). Any read failure
170
+ // (missing / too-large) → null, the existing "no baseline yet" behavior.
167
171
  var raw;
168
- try { raw = nodeFs.readFileSync(sidecarPath, "utf8"); }
172
+ try { raw = atomicFile.fdSafeReadSync(sidecarPath, { maxBytes: C.BYTES.mib(1), encoding: "utf8" }); }
169
173
  catch (_e) { return null; }
170
174
  var parsed;
171
175
  try { parsed = safeJson.parse(raw); }
@@ -190,9 +194,12 @@ function create(opts) {
190
194
  publicKeyPem: auditSign.getPublicKey(),
191
195
  snapshot: snapshot,
192
196
  };
193
- var tmp = sidecarPath + ".tmp";
194
- nodeFs.writeFileSync(tmp, JSON.stringify(payload, null, 2));
195
- atomicFile.renameWithRetry(tmp, sidecarPath);
197
+ // Atomic, symlink-refusing write. The previous hand-rolled form staged
198
+ // into a PREDICTABLE temp name (`sidecarPath + ".tmp"`) via a plain
199
+ // writeFileSync, so an attacker could pre-plant a symlink at that exact
200
+ // path and have the signed sidecar written through it (CWE-59 / CWE-377).
201
+ // writeSync uses a CSPRNG temp name opened O_EXCL | O_NOFOLLOW.
202
+ atomicFile.writeSync(sidecarPath, JSON.stringify(payload, null, 2), { fileMode: 0o600 });
196
203
  }
197
204
 
198
205
  function _verifySidecar(parsed) {
@@ -367,7 +374,10 @@ function verifyVendorIntegrity(opts) {
367
374
  var libVendorDir = opts.libVendorDir || nodePath.join(__dirname, "vendor");
368
375
  var manifestPath = opts.manifestPath || nodePath.join(libVendorDir, "MANIFEST.json");
369
376
  var raw;
370
- try { raw = nodeFs.readFileSync(manifestPath, "utf8"); }
377
+ // Capped fd-bound read of the vendor MANIFEST.json (operator-bundled, install-
378
+ // time). refuseSymlink OFF — the vendored tree ships read-only and an install
379
+ // may symlink lib/. The cap precedes the alloc.
380
+ try { raw = atomicFile.fdSafeReadSync(manifestPath, { maxBytes: C.BYTES.mib(4), encoding: "utf8" }); }
371
381
  catch (_e) {
372
382
  throw _err("VENDOR_MANIFEST_MISSING",
373
383
  "vendor MANIFEST.json missing at " + manifestPath, true);
@@ -397,7 +407,11 @@ function verifyVendorIntegrity(opts) {
397
407
  var abs = nodePath.isAbsolute(rel) ? rel : nodePath.join(libVendorDir, relInVendor);
398
408
  var actual;
399
409
  try {
400
- var bytes = nodeFs.readFileSync(abs);
410
+ // Capped fd-bound read (raw bytes hashing, no encoding). Sanity ceiling
411
+ // so a corrupted/huge vendored file is a read-failed mismatch (caught
412
+ // below) rather than an OOM in the boot integrity loop. NO refuseSymlink
413
+ // (vendored tree ships read-only; installs may symlink lib/).
414
+ var bytes = atomicFile.fdSafeReadSync(abs, { maxBytes: C.BYTES.mib(64) });
401
415
  actual = "sha256:" + require("node:crypto")
402
416
  .createHash("sha256").update(bytes).digest("hex");
403
417
  } catch (_e) {
@@ -31,6 +31,7 @@
31
31
  var nodeCrypto = require("node:crypto");
32
32
  var C = require("./constants");
33
33
  var bCrypto = require("./crypto");
34
+ var x509Chain = require("./x509-chain");
34
35
  var canonicalJson = require("./canonical-json");
35
36
  var safeJson = require("./safe-json");
36
37
  var validateOpts = require("./validate-opts");
@@ -1103,7 +1104,7 @@ function attachIdentityAssertion(opts) {
1103
1104
  validateOpts.requireObject(opts, "contentCredentials.attachIdentityAssertion", ContentCredentialsError);
1104
1105
  validateOpts(opts, ["binding", "subject", "referencedAssertions", "privateKeyPem", "audit"],
1105
1106
  "contentCredentials.attachIdentityAssertion");
1106
- if (typeof opts.binding !== "string" || !IDENTITY_BINDINGS[opts.binding]) {
1107
+ if (typeof opts.binding !== "string" || !Object.prototype.hasOwnProperty.call(IDENTITY_BINDINGS, opts.binding)) {
1107
1108
  throw ContentCredentialsError.factory("content-credentials/bad-identity-binding",
1108
1109
  "attachIdentityAssertion: binding must be one of " + Object.keys(IDENTITY_BINDINGS).join(" / "));
1109
1110
  }
@@ -1217,7 +1218,7 @@ function verifyIdentityAssertion(assertion, publicKeyPem, opts) {
1217
1218
  return _fail("public-key-required");
1218
1219
  }
1219
1220
  var sp = assertion.signer_payload;
1220
- if (!sp || typeof sp !== "object" || typeof sp.binding !== "string" || !IDENTITY_BINDINGS[sp.binding] ||
1221
+ if (!sp || typeof sp !== "object" || typeof sp.binding !== "string" || !Object.prototype.hasOwnProperty.call(IDENTITY_BINDINGS, sp.binding) ||
1221
1222
  !Array.isArray(sp.referenced_assertions)) {
1222
1223
  return _fail("signer-payload-shape");
1223
1224
  }
@@ -1334,9 +1335,10 @@ function _verifyIdentityX509Chain(certChainPem, trustAnchorsPem) {
1334
1335
  // only direct-root / self-signed leaves.
1335
1336
  for (var li = 0; li < certs.length - 1; li += 1) {
1336
1337
  var child = certs[li], parent = certs[li + 1];
1337
- var linked = false;
1338
- try { linked = child.checkIssued(parent) && child.verify(parent.publicKey); }
1339
- catch (_eLink) { linked = false; }
1338
+ // issuerValidlyIssued enforces basicConstraints cA:TRUE on the parent in
1339
+ // addition to the issuance + signature linkage — a non-CA cert cannot be
1340
+ // an intermediate issuer (basicConstraints bypass, CVE-2002-0862 class).
1341
+ var linked = x509Chain.issuerValidlyIssued(parent, child);
1340
1342
  if (!linked) { return { ok: false, reason: "broken-chain" }; }
1341
1343
  }
1342
1344
  // The top of the presented chain must chain to (or BE) a trust anchor.
@@ -1347,8 +1349,9 @@ function _verifyIdentityX509Chain(certChainPem, trustAnchorsPem) {
1347
1349
  if (top.fingerprint256 === anchor.fingerprint256) {
1348
1350
  chained = true; // top of the chain IS the anchor (root-in-chain or self-signed leaf == anchor)
1349
1351
  } else {
1350
- try { chained = top.checkIssued(anchor) && top.verify(anchor.publicKey); }
1351
- catch (_eChk) { chained = false; }
1352
+ // issuerValidlyIssued enforces basicConstraints cA:TRUE on the anchor
1353
+ // in addition to issuance + signature (basicConstraints bypass class).
1354
+ chained = x509Chain.issuerValidlyIssued(anchor, top);
1352
1355
  }
1353
1356
  if (chained) {
1354
1357
  if (now < anchor.validFromDate.getTime() || now > anchor.validToDate.getTime()) {
@@ -1427,7 +1430,7 @@ function cacImplicitLabel(opts) {
1427
1430
  throw new ContentCredentialsError("cac-implicit-label/bad-content-id",
1428
1431
  "cacImplicitLabel: contentId must match [A-Za-z0-9._:/-]");
1429
1432
  }
1430
- if (typeof opts.contentKind !== "string" || !CAC_KIND_ENUM[opts.contentKind]) {
1433
+ if (typeof opts.contentKind !== "string" || !Object.prototype.hasOwnProperty.call(CAC_KIND_ENUM, opts.contentKind)) {
1431
1434
  throw new ContentCredentialsError("cac-implicit-label/bad-content-kind",
1432
1435
  "cacImplicitLabel: contentKind must be one of " +
1433
1436
  Object.keys(CAC_KIND_ENUM).join("/"));
@@ -99,9 +99,12 @@ function create(body, opts) {
99
99
  if (!Array.isArray(algos) || algos.length === 0) throw new ContentDigestError("content-digest/bad-arg", "contentDigest.create: opts.algorithms must be a non-empty array");
100
100
  var members = algos.map(function (a) {
101
101
  var name = String(a).toLowerCase();
102
- var nodeAlg = ACTIVE[name];
102
+ // hasOwnProperty: the algorithm name is operator/caller input; a bracket
103
+ // lookup lets "constructor"/"toString" inherit a truthy value off the
104
+ // prototype and pass the support check (proto shadowing).
105
+ var nodeAlg = Object.prototype.hasOwnProperty.call(ACTIVE, name) ? ACTIVE[name] : undefined;
103
106
  if (!nodeAlg) {
104
- if (DEPRECATED[name]) throw new ContentDigestError("content-digest/insecure-algorithm", "contentDigest.create: '" + name + "' is a deprecated/insecure digest algorithm (RFC 9530 §6); use sha-256 or sha-512");
107
+ if (Object.prototype.hasOwnProperty.call(DEPRECATED, name)) throw new ContentDigestError("content-digest/insecure-algorithm", "contentDigest.create: '" + name + "' is a deprecated/insecure digest algorithm (RFC 9530 §6); use sha-256 or sha-512");
105
108
  throw new ContentDigestError("content-digest/unsupported-algorithm", "contentDigest.create: unsupported digest algorithm '" + name + "'");
106
109
  }
107
110
  var digest = nodeCrypto.createHash(nodeAlg).update(bytes).digest("base64");
@@ -154,8 +157,12 @@ function verify(fieldValue, body, opts) {
154
157
  var kvp = structuredFields.parseKeyValuePiece(m);
155
158
  var name = kvp.key;
156
159
  var raw = kvp.value.trim();
160
+ // hasOwnProperty: `name` is the algorithm token from the untrusted inbound
161
+ // Content-Digest header; a bracket lookup lets "constructor"/"__proto__"
162
+ // inherit a truthy value off the prototype and reach createHash (proto
163
+ // shadowing).
164
+ if (!Object.prototype.hasOwnProperty.call(ACTIVE, name)) continue; // ignore legacy / unknown entries
157
165
  var nodeAlg = ACTIVE[name];
158
- if (!nodeAlg) continue; // ignore legacy / unknown entries
159
166
  if (raw.length < 2 || raw.charAt(0) !== ":" || raw.charAt(raw.length - 1) !== ":") {
160
167
  throw new ContentDigestError("content-digest/bad-field", "contentDigest.verify: '" + name + "' value is not an RFC 8941 byte sequence (:base64:)");
161
168
  }
@@ -172,7 +179,7 @@ function verify(fieldValue, body, opts) {
172
179
  if (!Array.isArray(opts.required)) throw new ContentDigestError("content-digest/bad-arg", "contentDigest.verify: opts.required must be an array");
173
180
  for (var r = 0; r < opts.required.length; r++) {
174
181
  var req = String(opts.required[r]).toLowerCase();
175
- if (!ACTIVE[req]) throw new ContentDigestError("content-digest/unsupported-algorithm", "contentDigest.verify: required algorithm '" + req + "' is not a modern digest");
182
+ if (!Object.prototype.hasOwnProperty.call(ACTIVE, req)) throw new ContentDigestError("content-digest/unsupported-algorithm", "contentDigest.verify: required algorithm '" + req + "' is not a modern digest");
176
183
  if (!seen[req]) throw new ContentDigestError("content-digest/missing-algorithm", "contentDigest.verify: required digest '" + req + "' is not present");
177
184
  }
178
185
  }
package/lib/cookies.js CHANGED
@@ -96,7 +96,11 @@ function _validateValue(value) {
96
96
  // Length cap before the regex test (defense in depth — the regex
97
97
  // here is a simple character class, but the same discipline that
98
98
  // bounds longer regexes elsewhere applies).
99
- if (value.length > MAX_VALUE_LENGTH || FORBIDDEN_VALUE_RE.test(value)) {
99
+ // Byte length, not value.length (UTF-16 code units): the cap bounds the
100
+ // on-the-wire Set-Cookie size, and a multibyte value is 2-4x larger in bytes
101
+ // than chars, so a char-count check under-enforces and a 4096-char value can
102
+ // emit a ~12 KiB+ header.
103
+ if (Buffer.byteLength(value, "utf8") > MAX_VALUE_LENGTH || FORBIDDEN_VALUE_RE.test(value)) {
100
104
  throw new CookieError("cookies/invalid-value",
101
105
  "cookie value is too long or contains forbidden control character (CRLF/NUL/;/,)");
102
106
  }
@@ -108,7 +112,11 @@ function _validateValue(value) {
108
112
  // header — never trust unscrubbed values reach the wire.
109
113
  function _scrubAttr(s) {
110
114
  if (typeof s !== "string") return s;
111
- return s.replace(/[\r\n\0]/g, ""); // allow:duplicate-regex CR/LF/NUL header-injection rejection appears in cookies / mail / security-headers; each is the boundary primitive for its domain
115
+ // `;` joins Set-Cookie attributes, so an unscrubbed `;` in Path/SameSite is
116
+ // attribute injection (e.g. Path=/x; HttpOnly). Domain is regex-validated
117
+ // upstream; Path/SameSite only flow through here, so strip `;` alongside the
118
+ // CR/LF/NUL header-injection set.
119
+ return s.replace(/[\r\n\0;]/g, ""); // allow:duplicate-regex — CR/LF/NUL(+;) header-injection rejection appears in cookies / mail / security-headers; each is the boundary primitive for its domain
112
120
 
113
121
  }
114
122
 
package/lib/cose.js CHANGED
@@ -110,6 +110,24 @@ function _algParamsFor(algId) {
110
110
  }
111
111
  }
112
112
 
113
+ // RFC 9053 §2 binds each ECDSA alg to ONE curve. node:crypto will happily
114
+ // verify a sha512 (ES512-shape) signature against a P-256 key as
115
+ // self-consistent, so without this check a COSE_Sign1 declaring ES512 but
116
+ // carrying a P-256 key passes when ES512 is allowlisted — an alg/curve
117
+ // confusion (the COSE sibling of jwt-external._assertAlgKtyMatch). EdDSA /
118
+ // ML-DSA carry their parameters in the KeyObject and need no binding.
119
+ var _ES_ALG_CURVE = { ES256: "prime256v1", ES384: "secp384r1", ES512: "secp521r1" };
120
+ function _assertEcAlgCurve(algName, key) {
121
+ var want = _ES_ALG_CURVE[algName];
122
+ if (want === undefined) return; // not an ECDSA alg — nothing to bind
123
+ var got = (key && key.asymmetricKeyDetails && key.asymmetricKeyDetails.namedCurve) || null;
124
+ if (key.asymmetricKeyType !== "ec" || got !== want) {
125
+ throw new CoseError("cose/alg-curve-mismatch",
126
+ "cose: alg '" + algName + "' requires an EC key on " + want +
127
+ ", got " + (key.asymmetricKeyType === "ec" ? got : key.asymmetricKeyType));
128
+ }
129
+ }
130
+
113
131
  function _bstr(x) {
114
132
  return safeBuffer.toBuffer(x, {
115
133
  errorFactory: function (code, msg) { return new CoseError(code, msg); },
@@ -169,6 +187,7 @@ async function sign(payload, opts) {
169
187
  var algId = ALG_NAME_TO_ID[opts.alg];
170
188
  var params = _algParamsFor(algId);
171
189
  var key = _toKeyObject(opts.privateKey, "private");
190
+ _assertEcAlgCurve(opts.alg, key); // RFC 9053 alg↔curve binding — refuse e.g. ES512 over a P-256 key
172
191
 
173
192
  var protMap = new Map();
174
193
  protMap.set(HDR_ALG, algId);
@@ -352,6 +371,7 @@ async function verify(coseSign1, opts) {
352
371
  var key = opts.publicKey
353
372
  ? _toKeyObject(opts.publicKey, "public")
354
373
  : _toKeyObject(opts.keyResolver(protMap, unprotected), "public");
374
+ _assertEcAlgCurve(algName, key); // RFC 9053 alg↔curve binding
355
375
 
356
376
  var externalAad = opts.externalAad == null ? Buffer.alloc(0) : _bstr(opts.externalAad);
357
377
  var toBeSigned = _toBeSigned(protectedBstr, externalAad, payload);
package/lib/crdt.js CHANGED
@@ -41,6 +41,7 @@
41
41
 
42
42
  var bCrypto = require("./crypto");
43
43
  var safeJson = require("./safe-json");
44
+ var numericBounds = require("./numeric-bounds");
44
45
  var { defineClass } = require("./framework-error");
45
46
 
46
47
  var CrdtError = defineClass("CrdtError", { alwaysPermanent: true });
@@ -52,7 +53,7 @@ function _replicaId(opts) {
52
53
  return id;
53
54
  }
54
55
  function _posInt(n, label) {
55
- if (typeof n !== "number" || !isFinite(n) || n < 0 || Math.floor(n) !== n) throw new CrdtError("crdt/bad-value", "crdt: " + label + " must be a non-negative integer");
56
+ if (!numericBounds.isNonNegativeFiniteInt(n)) throw new CrdtError("crdt/bad-value", "crdt: " + label + " must be a non-negative integer");
56
57
  return n;
57
58
  }
58
59
  function _maxMerge(a, b) {
@@ -62,7 +62,6 @@ var vaultAad = require("./vault-aad");
62
62
  var validateOpts = require("./validate-opts");
63
63
  var numericBounds = require("./numeric-bounds");
64
64
  var safeJson = require("./safe-json");
65
- var frameworkSchema = require("./framework-schema");
66
65
  var sql = require("./sql");
67
66
  var { defineClass } = require("./framework-error");
68
67
  var { sha3Hash, kdf, generateBytes, encryptPacked, decryptPacked, generateToken } = require("./crypto");
@@ -226,14 +225,19 @@ var PER_ROW_KEYS_TABLE = "_blamejs_per_row_keys"; // allow:hand-rolled-sql
226
225
  var PER_ROW_KEYS_COLUMN = "wrappedKey";
227
226
  var PER_ROW_KEYS_SCHEMA_VERSION = "1";
228
227
 
229
- // The per-row-key registry is read/written against the LOCAL db() / dbHandle
230
- // handle directly (not clusterStorage), so SQL composed for it uses the
231
- // RESOLVED name (prefix-aware via frameworkSchema.tableName) and quoteName so
232
- // b.sql emits the quoted identifier the single-node path expects the same
233
- // shape db-query.js's _sqlOpts and db.js's own local-handle b.sql calls use.
228
+ // The per-row-key registry ALWAYS lives in the LOCAL sqlite reconcile creates
229
+ // it under its RAW schema name, and per-row keys are never dispatched to an
230
+ // external backend (see _kRowOnce's db() fallback). It is read/written against
231
+ // the local db() / dbHandle directly, NOT through clusterStorage, so it must use
232
+ // the RAW table name. frameworkSchema.tableName() resolves to the cluster-mode
233
+ // EXTERNAL prefixed name (the prefix applies only to the external backend, via
234
+ // resolveTables — local DDL stays raw); under a custom tablePrefix that names a
235
+ // table that does not exist locally, which SILENTLY breaks crypto-shred:
236
+ // destroyPerRowKey deletes 0 rows and sealed cells stay decryptable after
237
+ // eraseHard. quoteName so b.sql emits the quoted identifier the local path expects.
234
238
  var _PER_ROW_SQL_OPTS = { dialect: "sqlite", quoteName: true };
235
239
  function _perRowKeysTableName() {
236
- return frameworkSchema.tableName(PER_ROW_KEYS_TABLE);
240
+ return PER_ROW_KEYS_TABLE;
237
241
  }
238
242
 
239
243
  // Build the canonical AAD parts for a row-secret wrap in
@@ -1271,19 +1275,19 @@ function unsealRow(table, row, actor, dbHandle) {
1271
1275
  unsealed = out[field];
1272
1276
  }
1273
1277
  } catch (e) {
1274
- // A DB-write attacker who can write `vault:<crafted>` /
1275
- // `vault.aad:<crafted>` payloads to sealed columns can force
1276
- // KEM decapsulation / AEAD verify on attacker-controlled
1277
- // bytes via this read path. Surface the failure as a chain
1278
- // row so operators alert on burst patterns; null the field
1279
- // so downstream code sees "no value" instead of crashing the
1280
- // request. AAD-shape failures additionally indicate cross-
1281
- // row copy attempts the audit metadata flags the shape so
1282
- // operators can write alert rules.
1278
+ // A crypto-shredded (or never-materialized) per-row key is an EXPECTED
1279
+ // absence, not a decryption-oracle attack: the wrapped secret is gone,
1280
+ // so there is no oracle to brute-force. Reading such a row must read as
1281
+ // "no value" WITHOUT counting against the rate cap — otherwise a bulk
1282
+ // read over a table with many erased rows (GDPR eraseHard) trips the
1283
+ // cap and DoS's the live rows (self-DoS, CWE-307 mis-applied). It is
1284
+ // audited under a distinct, non-failure action so operators don't alert
1285
+ // on routine post-erasure reads as forged-ciphertext bursts.
1286
+ var _shredded = e && e.code === "crypto-field/row-key-unavailable";
1283
1287
  try {
1284
1288
  audit().safeEmit({
1285
- action: "system.crypto.unseal_failed",
1286
- outcome: "failure",
1289
+ action: _shredded ? "system.crypto.shredded_read" : "system.crypto.unseal_failed",
1290
+ outcome: _shredded ? "success" : "failure",
1287
1291
  metadata: {
1288
1292
  table: table,
1289
1293
  field: field,
@@ -1293,11 +1297,13 @@ function unsealRow(table, row, actor, dbHandle) {
1293
1297
  },
1294
1298
  });
1295
1299
  } catch (_e) { /* drop-silent */ }
1296
- // Default-on rate cap: account this failure against the (actor,
1297
- // table, column) tuple. When it trips the threshold, arm the
1298
- // cooldown + emit the distinct rate-exceeded audit once on the
1299
- // transition. No-op when the cap is disabled.
1300
- if (_rateNoteFailure(capActor, table, field)) {
1300
+ // Default-on rate cap: account a genuine decryption / AEAD-verify /
1301
+ // AAD-downgrade failure (a possible forged-ciphertext attack) against
1302
+ // the (actor, table, column) tuple. A shredded-key read is exempt (see
1303
+ // above). When the cap trips the threshold, arm the cooldown + emit the
1304
+ // distinct rate-exceeded audit once on the transition. No-op when the
1305
+ // cap is disabled.
1306
+ if (!_shredded && _rateNoteFailure(capActor, table, field)) {
1301
1307
  _emitRateAudit({
1302
1308
  table: table, field: field, actor: capActor, shape: shape,
1303
1309
  threshold: _rateCap.threshold, windowMs: _rateCap.windowMs, cooldownMs: _rateCap.cooldownMs,
package/lib/crypto.js CHANGED
@@ -963,7 +963,7 @@ var SRI_ALGORITHMS = { "sha256": "sha256", "sha384": "sha384", "sha512": "sha512
963
963
  function sri(content, opts) {
964
964
  opts = opts || {};
965
965
  var algorithm = (opts.algorithm || "sha384").toLowerCase();
966
- if (!SRI_ALGORITHMS[algorithm]) {
966
+ if (!Object.prototype.hasOwnProperty.call(SRI_ALGORITHMS, algorithm)) {
967
967
  throw new Error("crypto.sri: unsupported algorithm '" + algorithm +
968
968
  "' (W3C SRI 1.0 §3.2 supports sha256/sha384/sha512)");
969
969
  }
@@ -1356,7 +1356,21 @@ function decrypt(ciphertext, privateKeys, opts) {
1356
1356
  opts && opts.raw === true ? { raw: true } : undefined);
1357
1357
  }
1358
1358
 
1359
+ // Bounds-checked 2-byte length read — a bare _envU16(packed, pos) on a
1360
+ // truncated envelope throws a raw Node RangeError that escapes the documented
1361
+ // "Invalid envelope: ..." error contract (untrusted ciphertext must fail with
1362
+ // the typed envelope error, not a stack-trace-leaking RangeError).
1363
+ function _envU16(buf, at) {
1364
+ if (at + 2 > buf.length) {
1365
+ throw new Error("Invalid envelope: truncated (expected a 2-byte length at offset " + at + ")");
1366
+ }
1367
+ return buf.readUInt16BE(at);
1368
+ }
1369
+
1359
1370
  function decryptEnvelope(packed, privateKeys, internalOpts) {
1371
+ if (!Buffer.isBuffer(packed) || packed.length < 4) {
1372
+ throw new Error("Invalid envelope: too short (need at least the 4-byte suite header)");
1373
+ }
1360
1374
  var kemId = packed[1], cipherId = packed[2], kdfId = packed[3], pos = 4;
1361
1375
 
1362
1376
  // The legacy 0xE1 envelope predates the FixedInfo / suite-binding
@@ -1372,7 +1386,7 @@ function decryptEnvelope(packed, privateKeys, internalOpts) {
1372
1386
  throw new Error("Invalid envelope: unsupported KDF (only SHAKE256 supported)");
1373
1387
  }
1374
1388
 
1375
- var kemCtLen = packed.readUInt16BE(pos); pos += 2;
1389
+ var kemCtLen = _envU16(packed, pos); pos += 2;
1376
1390
  var kemCt = packed.subarray(pos, pos + kemCtLen); pos += kemCtLen;
1377
1391
 
1378
1392
  var mlkemPriv = nodeCrypto.createPrivateKey(
@@ -1383,7 +1397,7 @@ function decryptEnvelope(packed, privateKeys, internalOpts) {
1383
1397
  var fixedInfo = omitFixedInfo ? Buffer.alloc(0) : _suiteFixedInfo(kemId, cipherId, kdfId);
1384
1398
 
1385
1399
  if (kemId === C.KEM_IDS.ML_KEM_1024_P384) {
1386
- var ecEphLen = packed.readUInt16BE(pos); pos += 2;
1400
+ var ecEphLen = _envU16(packed, pos); pos += 2;
1387
1401
  var ecEphDer = packed.subarray(pos, pos + ecEphLen); pos += ecEphLen;
1388
1402
  var ecPrivPem = typeof privateKeys === "string" ? null : privateKeys.ecPrivateKey;
1389
1403
  if (!ecPrivPem) throw new Error("Hybrid KEM requires EC private key");
@@ -1400,7 +1414,7 @@ function decryptEnvelope(packed, privateKeys, internalOpts) {
1400
1414
  // the correct keypair via privateKeys when the envelope was sealed
1401
1415
  // with this algorithm. Same length-prefixed shape as the P-384
1402
1416
  // hybrid: 2-byte ec-eph-len + DER X25519 pubkey + nonce + ct.
1403
- var x25519EphLen = packed.readUInt16BE(pos); pos += 2;
1417
+ var x25519EphLen = _envU16(packed, pos); pos += 2;
1404
1418
  var x25519EphDer = packed.subarray(pos, pos + x25519EphLen); pos += x25519EphLen;
1405
1419
  var x25519PrivPem = typeof privateKeys === "string" ? null : privateKeys.x25519PrivateKey;
1406
1420
  if (!x25519PrivPem) throw new Error("ML-KEM-768 + X25519 hybrid envelope requires x25519PrivateKey");
package/lib/csp.js CHANGED
@@ -416,6 +416,10 @@ function mergeDirectives(base, additions, opts) {
416
416
  ? directives[name].slice()
417
417
  : (directives["default-src"] ? directives["default-src"].slice() : ["'self'"]);
418
418
  var added = additions[name];
419
+ // A directive value of 'none' MUST stand alone (CSP3 §2.3.1) — appending a
420
+ // host to it emits the malformed "'none' https://x". When merging real
421
+ // sources in, the added sources supersede 'none', so drop it first.
422
+ if (added.length && existing.length === 1 && existing[0] === "'none'") existing = [];
419
423
  for (var si = 0; si < added.length; si += 1) {
420
424
  if (existing.indexOf(added[si]) === -1) existing.push(added[si]);
421
425
  }
package/lib/daemon.js CHANGED
@@ -73,7 +73,10 @@ function _isLivePid(pid) {
73
73
 
74
74
  function _readPidFile(pidFile) {
75
75
  try {
76
- var raw = nodeFs.readFileSync(pidFile, "utf8");
76
+ // Same fd-safe + capped + symlink-refusing read as app-shutdown's lockfile
77
+ // reader (one shape): a PID file is never a legit symlink mount, so
78
+ // refuseSymlink is safe; any throw → null ("nothing live there").
79
+ var raw = atomicFile.fdSafeReadSync(pidFile, { maxBytes: C.BYTES.kib(1), refuseSymlink: true, encoding: "utf8" });
77
80
  var pid = parseInt(String(raw).trim(), 10);
78
81
  return isFinite(pid) && pid > 0 ? pid : null;
79
82
  } catch (_e) { return null; }
package/lib/data-act.js CHANGED
@@ -89,6 +89,32 @@ var DESIGNATED_GATEKEEPERS = Object.freeze([
89
89
  "booking",
90
90
  ]);
91
91
 
92
+ // Is `recipient` a designated DMA gatekeeper? Match the gatekeeper token as a
93
+ // DNS LABEL of the recipient's host authority — any position, including the
94
+ // FINAL label. A substring `indexOf(g + ".")` test missed a gatekeeper that is
95
+ // the last label (e.g. "blog.google" / "drive.google" on the real ".google"
96
+ // TLD, or "data.amazon"), letting an Art 32 §1 third-party share through — and
97
+ // also false-flagged a non-gatekeeper whose name merely contained the token
98
+ // ("notgoogle.com"). Label matching closes the bypass and drops the false
99
+ // positive. A bare name ("Google") still matches exactly. URL / userinfo / port
100
+ // forms are normalized to the host authority first.
101
+ function _isGatekeeperRecipient(recipient) {
102
+ var s = String(recipient).toLowerCase().trim();
103
+ var scheme = s.indexOf("://");
104
+ if (scheme !== -1) s = s.slice(scheme + 3);
105
+ s = s.split("/")[0].split("?")[0].split("#")[0]; // drop path / query / fragment
106
+ var at = s.lastIndexOf("@");
107
+ if (at !== -1) s = s.slice(at + 1); // drop userinfo
108
+ s = s.split(":")[0]; // drop port
109
+ if (s.length === 0) return false;
110
+ var labels = s.split(".");
111
+ return DESIGNATED_GATEKEEPERS.some(function (g) {
112
+ if (s === g) return true;
113
+ for (var i = 0; i < labels.length; i++) { if (labels[i] === g) return true; }
114
+ return false;
115
+ });
116
+ }
117
+
92
118
  /**
93
119
  * @primitive b.dataAct.declareProduct
94
120
  * @signature b.dataAct.declareProduct(opts)
@@ -229,10 +255,7 @@ function shareWithThirdParty(opts) {
229
255
  acceptGatekeeper: "optional-plain-object",
230
256
  }, "dataAct.shareWithThirdParty", DataActError, "dataact/bad-opts");
231
257
 
232
- var recipientLower = opts.recipient.toLowerCase();
233
- var isGatekeeper = DESIGNATED_GATEKEEPERS.some(function (g) {
234
- return recipientLower === g || recipientLower.indexOf(g + ".") !== -1;
235
- });
258
+ var isGatekeeper = _isGatekeeperRecipient(opts.recipient);
236
259
  if (isGatekeeper) {
237
260
  if (!opts.acceptGatekeeper || typeof opts.acceptGatekeeper !== "object" ||
238
261
  typeof opts.acceptGatekeeper.reason !== "string" ||
@@ -172,8 +172,11 @@ function fileLifecycle(opts) {
172
172
  : nodePath.join(opts.dataDir, frameworkFiles.fileName("dbKeyEnc"));
173
173
  var flushIntervalMs = opts.flushIntervalMs || DEFAULT_FLUSH_INTERVAL_MS;
174
174
  var tmpDir = _resolveTmpDir(opts.tmpDir, opts.allowDiskFallback === true);
175
- if (!nodeFs.existsSync(tmpDir)) nodeFs.mkdirSync(tmpDir, { recursive: true });
176
- if (!nodeFs.existsSync(opts.dataDir)) nodeFs.mkdirSync(opts.dataDir, { recursive: true });
175
+ // 0o700 these hold the DB encryption tmpfs scratch + the data dir
176
+ // (db.key.enc, the decrypted DB while open); a default-mode mkdir leaves
177
+ // them group/other-traversable.
178
+ if (!nodeFs.existsSync(tmpDir)) nodeFs.mkdirSync(tmpDir, { recursive: true, mode: 0o700 });
179
+ if (!nodeFs.existsSync(opts.dataDir)) nodeFs.mkdirSync(opts.dataDir, { recursive: true, mode: 0o700 });
177
180
 
178
181
  var dbPath = null;
179
182
  var encKey = null;
@@ -183,7 +186,9 @@ function fileLifecycle(opts) {
183
186
  function _loadOrGenerateKey() {
184
187
  if (encKey) return encKey;
185
188
  if (nodeFs.existsSync(keyPath)) {
186
- var sealedKey = nodeFs.readFileSync(keyPath, "utf8");
189
+ // Cap + fd-bound. NO refuseSymlink: dbKeyPath may be an operator-absolute
190
+ // KMS-fronted volume that legitimately symlinks the sealed key.
191
+ var sealedKey = atomicFile.fdSafeReadSync(keyPath, { maxBytes: C.BYTES.kib(64), encoding: "utf8" });
187
192
  var keyB64;
188
193
  try { keyB64 = opts.vault.unseal(sealedKey); }
189
194
  catch (e) {
@@ -212,7 +217,10 @@ function fileLifecycle(opts) {
212
217
  dbPath = nodePath.join(tmpDir, "blamejs-fl-" + label + "-" +
213
218
  generateToken(TMP_NAME_BYTES) + ".db");
214
219
  if (nodeFs.existsSync(encPath)) {
215
- var packed = nodeFs.readFileSync(encPath);
220
+ // Cap + fd-bound (deployment-tunable ceiling). NO refuseSymlink: the data
221
+ // dir may be a symlink-mounted volume (k8s PVC). db.enc is AEAD-checked
222
+ // downstream, so the cap is the OOM-before-cap defense.
223
+ var packed = atomicFile.fdSafeReadSync(encPath, { maxBytes: (opts.maxDbBytes) || C.BYTES.gib(2) });
216
224
  if (packed.length < 26) { // minimum envelope length
217
225
  throw new DbFileLifecycleError("db-file-lifecycle/short-envelope",
218
226
  "fileLifecycle: " + encPath + " too short to be a valid envelope (" + packed.length + " bytes)");
@@ -248,7 +256,7 @@ function fileLifecycle(opts) {
248
256
  catch (_e) { /* best-effort — operators on read-only handles or pre-init still flush */ }
249
257
  }
250
258
  if (!nodeFs.existsSync(dbPath)) return null;
251
- var plain = nodeFs.readFileSync(dbPath);
259
+ var plain = atomicFile.fdSafeReadSync(dbPath, { maxBytes: (opts.maxDbBytes) || C.BYTES.gib(2) });
252
260
  var packed = encryptPacked(plain, encKey, _aad(opts.dataDir, label));
253
261
  atomicFile.writeSync(encPath, packed);
254
262
  _emitAudit("flushed", "success", { label: label, bytes: plain.length });
@@ -269,7 +277,7 @@ function fileLifecycle(opts) {
269
277
  throw new DbFileLifecycleError("db-file-lifecycle/no-source",
270
278
  "fileLifecycle.snapshot: " + dbPath + " is missing");
271
279
  }
272
- var plain = nodeFs.readFileSync(dbPath);
280
+ var plain = atomicFile.fdSafeReadSync(dbPath, { maxBytes: (opts.maxDbBytes) || C.BYTES.gib(2) });
273
281
  return encryptPacked(plain, encKey, _aad(opts.dataDir, label));
274
282
  }
275
283