@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/log.js CHANGED
@@ -117,7 +117,7 @@ function _normalizeLevel(level) {
117
117
  return level;
118
118
  }
119
119
  if (typeof level === "string") {
120
- if (LEVELS[level] === undefined) {
120
+ if (!Object.prototype.hasOwnProperty.call(LEVELS, level)) {
121
121
  throw new LogError("log/bad-level",
122
122
  "level must be one of " + LEVEL_NAMES.join(", "));
123
123
  }
@@ -587,7 +587,7 @@ function _escapeC0Controls(s) {
587
587
  function _bootMinLevel() {
588
588
  // allow:raw-process-env — see header comment above
589
589
  var raw = process.env.BLAMEJS_BOOT_LOG_LEVEL || process.env.LOG_LEVEL || "info";
590
- return LEVELS[raw] != null ? LEVELS[raw] : LEVELS.info;
590
+ return Object.prototype.hasOwnProperty.call(LEVELS, raw) ? LEVELS[raw] : LEVELS.info;
591
591
  }
592
592
 
593
593
  /**
package/lib/mail-agent.js CHANGED
@@ -353,7 +353,7 @@ async function _dispatchOrLocal(ctx, method, args, localFn) {
353
353
  var mode = ctx.dispatch.mode;
354
354
  if (mode === "local") return localFn(ctx, args);
355
355
  if (mode === "auto") {
356
- if (HEAVY_METHODS[method] && ctx.dispatch.queue) return _enqueueMethod(ctx, method, args);
356
+ if (Object.prototype.hasOwnProperty.call(HEAVY_METHODS, method) && ctx.dispatch.queue) return _enqueueMethod(ctx, method, args);
357
357
  return localFn(ctx, args);
358
358
  }
359
359
  // mode === "queue" — explicit queue dispatch.
@@ -362,7 +362,7 @@ async function _dispatchOrLocal(ctx, method, args, localFn) {
362
362
  "agent." + method + ": dispatch.mode='queue' requires opts.dispatch.queue");
363
363
  }
364
364
  // Sync-result methods refuse until orchestrator's result-bus lands.
365
- if (!HEAVY_METHODS[method]) {
365
+ if (!Object.prototype.hasOwnProperty.call(HEAVY_METHODS, method)) {
366
366
  throw new MailAgentError("mail-agent/queue-result-bus-deferred",
367
367
  "agent." + method + ": queue mode for sync-result methods wires at v0.9.21 " +
368
368
  "(b.agent.orchestrator). Use mode='local' or mode='auto' until then.");
package/lib/mail-arf.js CHANGED
@@ -274,7 +274,7 @@ function parse(rawMessage, opts) {
274
274
  // Surface non-normalized fields under extraFields for operator
275
275
  // visibility (X-* tags ESPs add for routing diagnostics, etc.).
276
276
  Object.keys(fieldMap).forEach(function (k) {
277
- if (!KNOWN_FIELDS[k]) extraFields[k] = fieldMap[k];
277
+ if (!Object.prototype.hasOwnProperty.call(KNOWN_FIELDS, k)) extraFields[k] = fieldMap[k];
278
278
  });
279
279
 
280
280
  var event = {
package/lib/mail-auth.js CHANGED
@@ -1122,7 +1122,7 @@ function _parseDmarcRecord(text) {
1122
1122
  else if (key === "aspf") policy.aspf = val.toLowerCase();
1123
1123
  else if (key === "np") {
1124
1124
  var npVal = val.toLowerCase();
1125
- if (!DMARCBIS_VALID_NP[npVal]) {
1125
+ if (!Object.prototype.hasOwnProperty.call(DMARCBIS_VALID_NP, npVal)) {
1126
1126
  throw new MailAuthError("mail-auth/dmarcbis-bad-tag",
1127
1127
  "DMARC np= must be one of none|quarantine|reject, got " + JSON.stringify(val));
1128
1128
  }
@@ -1130,7 +1130,7 @@ function _parseDmarcRecord(text) {
1130
1130
  }
1131
1131
  else if (key === "psd") {
1132
1132
  var psdVal = val.toLowerCase();
1133
- if (!DMARCBIS_VALID_PSD[psdVal]) {
1133
+ if (!Object.prototype.hasOwnProperty.call(DMARCBIS_VALID_PSD, psdVal)) {
1134
1134
  throw new MailAuthError("mail-auth/dmarcbis-bad-tag",
1135
1135
  "DMARC psd= must be one of y|n|u, got " + JSON.stringify(val));
1136
1136
  }
@@ -2017,7 +2017,7 @@ function authResultsEmit(opts) {
2017
2017
  }
2018
2018
  var method = String(r.method || "").toLowerCase();
2019
2019
  var result = String(r.result || "").toLowerCase();
2020
- if (!AR_VALID_METHODS[method]) {
2020
+ if (!Object.prototype.hasOwnProperty.call(AR_VALID_METHODS, method)) {
2021
2021
  throw new MailAuthError("mail-auth/ar-bad-method",
2022
2022
  "authResults.emit: unknown method '" + r.method + "'");
2023
2023
  }
@@ -2173,15 +2173,38 @@ function _extractFromHeaders(headerBlock) {
2173
2173
  // one of several authors (the §6.6.1 forbidden move), so the
2174
2174
  // field is treated as unparsable instead.
2175
2175
  address = value.trim();
2176
- if (/[\s,]/.test(address)) address = null;
2176
+ // An RFC 5322 addr-spec cannot contain whitespace, commas, or the
2177
+ // group / route markers ; : < > — their presence means a list, a
2178
+ // display-name, or RFC 5322 group syntax (`display-name: mailbox ;`),
2179
+ // not a single bare addr-spec.
2180
+ if (/[\s,;:<>]/.test(address)) address = null;
2177
2181
  }
2178
2182
  var at = address ? address.lastIndexOf("@") : -1;
2179
2183
  var domain = (at > 0 && address && at < address.length - 1)
2180
2184
  ? address.slice(at + 1).toLowerCase()
2181
2185
  : null;
2186
+ // The domain drives the DMARC policy lookup + alignment check. A value that
2187
+ // is not a syntactically valid DNS hostname (a trailing ';', group-syntax
2188
+ // soup, or other bytes that slipped the filters) would silently miss the
2189
+ // policy record and defeat DMARC (CWE-290). Require a real hostname;
2190
+ // otherwise treat From as unparsable — the caller fails closed (permerror →
2191
+ // reject).
2192
+ if (domain && !_isValidFromHostname(domain)) domain = null;
2182
2193
  return { count: count, address: address || null, domain: domain };
2183
2194
  }
2184
2195
 
2196
+ // Strict, bounded DNS-hostname check (ASCII / A-label form, as DMARC lookups
2197
+ // use). Anchored with a length lookahead and tempered labels — linear time, no
2198
+ // catastrophic backtracking. Requires at least two labels (a dot).
2199
+ var _FROM_HOSTNAME_RE =
2200
+ /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
2201
+ function _isValidFromHostname(host) {
2202
+ // Bound the length before the regex test (max DNS name is 253 octets) — the
2203
+ // pattern is already anchored + tempered, but bounding untrusted input before
2204
+ // any .test() is the framework convention.
2205
+ return typeof host === "string" && host.length <= 253 && _FROM_HOSTNAME_RE.test(host);
2206
+ }
2207
+
2185
2208
  async function inboundVerify(opts) {
2186
2209
  validateOpts.requireObject(opts, "inbound.verify", MailAuthError, "mail-auth/inbound-bad-input");
2187
2210
  validateOpts(opts, ["ip", "helo", "mailFrom", "message", "dnsLookup", "domainExists",
package/lib/mail-bimi.js CHANGED
@@ -57,6 +57,7 @@ var lazyRequire = require("./lazy-require");
57
57
  var networkDnsResolver = lazyRequire(function () { return require("./network-dns-resolver"); });
58
58
  var safeBuffer = require("./safe-buffer");
59
59
  var markupTokenizer = require("./markup-tokenizer");
60
+ var x509Chain = require("./x509-chain");
60
61
  var structuredFields = require("./structured-fields");
61
62
  var safeUrl = require("./safe-url");
62
63
  var validateOpts = require("./validate-opts");
@@ -399,7 +400,7 @@ function validateTinyPsSvg(svgBytes) {
399
400
  }
400
401
  // Any element name starting with "animate" is animation (covers
401
402
  // future SMIL extensions not in the static list above).
402
- if (name.indexOf("animate") === 0 && !TINY_PS_FORBIDDEN_TAGS[name]) {
403
+ if (name.indexOf("animate") === 0 && !Object.prototype.hasOwnProperty.call(TINY_PS_FORBIDDEN_TAGS, name)) {
403
404
  _vio("element-forbidden",
404
405
  "<" + name + "> animation element is forbidden in Tiny-PS");
405
406
  }
@@ -492,10 +493,14 @@ function _tokenizeTinyPsSvg(s) {
492
493
  if (lt > pos) tokens.push({ type: "text", raw: s.slice(pos, lt) });
493
494
 
494
495
  if (s.startsWith("<!--", lt)) {
495
- var endC = s.indexOf("-->", lt + 4);
496
+ // WHATWG comment end ("--!>" + abrupt "<!-->" / "<!--->"), matching a
497
+ // browser parsing inline SVG — so an element smuggled past an early
498
+ // terminator can't slip the Tiny-PS validation (mXSS differential).
499
+ // Fails closed on an unterminated comment.
500
+ var endC = markupTokenizer.htmlCommentEnd(s, lt);
496
501
  if (endC === -1) throw new Error("unterminated comment"); // allow:bare-error-throw — caught by outer try/catch and re-thrown as MailBimiError("bimi/svg-tiny-ps-violation")
497
- tokens.push({ type: "comment", raw: s.slice(lt, endC + 3) });
498
- pos = endC + 3;
502
+ tokens.push({ type: "comment", raw: s.slice(lt, endC) });
503
+ pos = endC;
499
504
  continue;
500
505
  }
501
506
  if (s.startsWith("<![CDATA[", lt)) {
@@ -823,11 +828,10 @@ function _verifyCertChain(leaf, intermediates, anchors) {
823
828
 
824
829
  for (var ai = 0; ai < anchors.length; ai += 1) {
825
830
  var anchor = anchors[ai];
826
- if (current.checkIssued(anchor)) {
827
- try {
828
- if (current.verify(anchor.publicKey)) return { ok: true };
829
- } catch (_e) { /* fall through to next anchor */ }
830
- }
831
+ // issuerValidlyIssued enforces basicConstraints cA:TRUE on the anchor
832
+ // in addition to the issuance + signature checks, so a non-CA cert
833
+ // cannot mint a VMC (basicConstraints bypass, CVE-2002-0862 class).
834
+ if (x509Chain.issuerValidlyIssued(anchor, current)) return { ok: true };
831
835
  }
832
836
  if (current.checkIssued(current)) {
833
837
  return { ok: false, reason: "self-signed root not in trust-anchor bundle" };
@@ -837,13 +841,9 @@ function _verifyCertChain(leaf, intermediates, anchors) {
837
841
  for (var ii = 0; ii < intermediates.length; ii += 1) {
838
842
  var cand = intermediates[ii];
839
843
  if (cand === current) continue;
840
- if (current.checkIssued(cand)) {
841
- try {
842
- if (current.verify(cand.publicKey)) {
843
- nextIssuer = cand;
844
- break;
845
- }
846
- } catch (_e) { /* fall through */ }
844
+ if (x509Chain.issuerValidlyIssued(cand, current)) {
845
+ nextIssuer = cand;
846
+ break;
847
847
  }
848
848
  }
849
849
  if (nextIssuer === null) {
@@ -448,7 +448,7 @@ function handler(opts) {
448
448
  opts = opts || {};
449
449
  var vendor = opts.vendor;
450
450
  var customParser = typeof opts.parser === "function" ? opts.parser : null;
451
- if (!customParser && (typeof vendor !== "string" || !VENDORS[vendor])) {
451
+ if (!customParser && (typeof vendor !== "string" || !Object.prototype.hasOwnProperty.call(VENDORS, vendor))) {
452
452
  throw _err("handler/bad-config",
453
453
  "mailBounce.handler requires { vendor } (one of " +
454
454
  Object.keys(VENDORS).join(", ") + ") or { parser }");
@@ -761,7 +761,7 @@ function _parseDsn(rawMessage) {
761
761
  throw _err("bounce/dsn-malformed",
762
762
  "mailBounce.dsn.parse: per-recipient group missing Final-Recipient");
763
763
  }
764
- if (action && !DSN_ACTIONS[action]) {
764
+ if (action && !Object.prototype.hasOwnProperty.call(DSN_ACTIONS, action)) {
765
765
  throw _err("bounce/dsn-malformed",
766
766
  "mailBounce.dsn.parse: Action token '" + action + "' is not RFC 3464 §2.3.3");
767
767
  }
@@ -825,7 +825,7 @@ function _buildDsn(opts) {
825
825
  validateOpts.requireNonEmptyString(opts.finalRecipient,
826
826
  "mailBounce.dsn.build: opts.finalRecipient", MailBounceError, "bounce/dsn-malformed");
827
827
  var action = String(opts.action || "failed").toLowerCase();
828
- if (!DSN_ACTIONS[action]) {
828
+ if (!Object.prototype.hasOwnProperty.call(DSN_ACTIONS, action)) {
829
829
  throw _err("bounce/dsn-malformed",
830
830
  "mailBounce.dsn.build: opts.action must be one of " +
831
831
  Object.keys(DSN_ACTIONS).join(" / ") + "; got '" + action + "'");
@@ -98,6 +98,7 @@ var lazyRequire = require("./lazy-require");
98
98
  var audit = lazyRequire(function () { return require("./audit"); });
99
99
  var nodeCrypto = require("node:crypto");
100
100
  var validateOpts = require("./validate-opts");
101
+ var x509Chain = require("./x509-chain");
101
102
  var cms = require("./cms-codec");
102
103
  var asn1 = require("./asn1-der");
103
104
  var pqcSoftware = require("./pqc-software");
@@ -297,7 +298,8 @@ function verify(opts) {
297
298
  // verification failure (bad alg, missing signed-attrs, message-
298
299
  // digest mismatch, signature mismatch); reaching the next line
299
300
  // means the per-signer verify succeeded.
300
- var siResult = _verifySignerInfo(sd.signerInfos[0], msgBytes, opts.signerPublicKey, opts.audit);
301
+ var siResult = _verifySignerInfo(sd.signerInfos[0], msgBytes, opts.signerPublicKey, opts.audit,
302
+ sd.encapContent && sd.encapContent.eContentType);
301
303
  // Trust-anchor chain validation when operator supplies roots. The
302
304
  // signer cert is in sd.certificates (its serialNumber matches the
303
305
  // sid's serialNumber); intermediate certs are also in
@@ -336,7 +338,7 @@ function verify(opts) {
336
338
  // signer's signature, masking real multi-signer envelopes as a single
337
339
  // false-failure). verifyAll now iterates `sd.signerInfos` directly and
338
340
  // calls this helper per index with the matching per-signer key.
339
- function _verifySignerInfo(si, msgBytes, signerPublicKey, auditHandle) {
341
+ function _verifySignerInfo(si, msgBytes, signerPublicKey, auditHandle, eContentType) {
340
342
  var sigAlg = _oidToSigAlg(si.sigAlgOid);
341
343
  if (!sigAlg) {
342
344
  throw new MailCryptoError("mail-crypto/smime/bad-sig-alg",
@@ -365,6 +367,26 @@ function _verifySignerInfo(si, msgBytes, signerPublicKey, auditHandle) {
365
367
  "smime.verify: recomputed message digest does not match signedAttrs.messageDigest " +
366
368
  "(message was tampered or signed-attrs were swapped)");
367
369
  }
370
+ // RFC 5652 §11.1: when signedAttrs are present the contentType attribute
371
+ // MUST be present and MUST equal the SignedData eContentType. The TSA
372
+ // sibling enforces this; S/MIME omitted it, so a SignerInfo with a missing
373
+ // or mismatched contentType still verified — a cross-protocol content-type
374
+ // binding gap. The check runs over signedAttrs, which the signature below
375
+ // covers, so a tampered attribute also fails the signature.
376
+ if (eContentType) {
377
+ var ctOid = _extractContentTypeOid(si.signedAttrsRaw);
378
+ if (!ctOid) {
379
+ _audit(auditHandle, "mail.crypto.smime.verify_fail", "denied", { reason: "no-content-type-attr" });
380
+ throw new MailCryptoError("mail-crypto/smime/no-content-type-attr",
381
+ "smime.verify: signedAttrs missing the contentType attribute (RFC 5652 §11.1)");
382
+ }
383
+ if (ctOid !== eContentType) {
384
+ _audit(auditHandle, "mail.crypto.smime.verify_fail", "denied", { reason: "content-type-mismatch" });
385
+ throw new MailCryptoError("mail-crypto/smime/content-type-mismatch",
386
+ "smime.verify: signedAttrs.contentType (" + ctOid + ") does not match eContentType (" +
387
+ eContentType + ")");
388
+ }
389
+ }
368
390
  var ok;
369
391
  try {
370
392
  ok = sigAlg.pqc.verify(
@@ -471,7 +493,10 @@ function _verifyTrustChain(sd, trustAnchorCertsPem, signerPublicKey, auditHandle
471
493
  // Stop when we've reached a trust anchor.
472
494
  for (var ri = 0; ri < roots.length; ri += 1) {
473
495
  var r = roots[ri];
474
- if (current.issuer === r.subject) {
496
+ // Enforce basicConstraints cA:TRUE on the issuing root in addition to
497
+ // the DN match + signature — a non-CA cert can never be a chain issuer
498
+ // (basicConstraints bypass, CVE-2002-0862 class).
499
+ if (current.issuer === r.subject && x509Chain.isCaCert(r)) {
475
500
  try {
476
501
  if (current.verify(r.publicKey)) {
477
502
  void auditHandle;
@@ -480,12 +505,12 @@ function _verifyTrustChain(sd, trustAnchorCertsPem, signerPublicKey, auditHandle
480
505
  } catch (_e) { /* fall through to next root */ }
481
506
  }
482
507
  }
483
- // Find an intermediate whose subject == current.issuer.
508
+ // Find an intermediate whose subject == current.issuer (and that is a CA).
484
509
  var found = null;
485
510
  for (var hi = 0; hi < chain.length; hi += 1) {
486
511
  var h = chain[hi];
487
512
  if (h === current) continue;
488
- if (h.subject === current.issuer) {
513
+ if (h.subject === current.issuer && x509Chain.isCaCert(h)) {
489
514
  try {
490
515
  if (current.verify(h.publicKey)) { found = h; break; }
491
516
  } catch (_e) { /* try next */ }
@@ -574,7 +599,8 @@ function verifyAll(opts) {
574
599
  throw new MailCryptoError("mail-crypto/smime/missing-key",
575
600
  "verifyAll: no public key supplied for SignerInfo serial " + serialHex);
576
601
  }
577
- var siRes = _verifySignerInfo(si, msgBytes, pub, opts.audit);
602
+ var siRes = _verifySignerInfo(si, msgBytes, pub, opts.audit,
603
+ sd.encapContent && sd.encapContent.eContentType);
578
604
  results.push({
579
605
  serialHex: serialHex,
580
606
  sigAlgOid: si.sigAlgOid,
@@ -621,6 +647,42 @@ function _extractSerialHex(sidBytes) {
621
647
 
622
648
  // RFC 5652 §11.2 messageDigest OID.
623
649
  var OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4";
650
+ // RFC 5652 §11.1 contentType signed-attribute OID.
651
+ var OID_CONTENT_TYPE_ATTR = "1.2.840.113549.1.9.3";
652
+
653
+ // Extract the contentType signed-attribute's OID value from a signedAttrs
654
+ // SET, or null if absent / malformed. Mirrors _extractMessageDigest's walk
655
+ // but the attribute value is an OID (the eContentType), not an OCTET STRING.
656
+ function _extractContentTypeOid(signedAttrsRaw) {
657
+ var node;
658
+ try { node = asn1.readNode(signedAttrsRaw); }
659
+ catch (_e) { return null; }
660
+ if (node.tag !== asn1.TAG.SET) return null;
661
+ var attrs;
662
+ try { attrs = asn1.readSequence(node.value); }
663
+ catch (_e) { return null; }
664
+ for (var i = 0; i < attrs.length; i += 1) {
665
+ var attr = attrs[i];
666
+ if (attr.tag !== asn1.TAG.SEQUENCE) continue;
667
+ var children;
668
+ try { children = asn1.readSequence(attr.value); }
669
+ catch (_e) { continue; }
670
+ if (children.length < 2) continue;
671
+ var oid;
672
+ try { oid = asn1.readOid(children[0]); }
673
+ catch (_e) { continue; }
674
+ if (oid !== OID_CONTENT_TYPE_ATTR) continue;
675
+ var valuesSet = children[1];
676
+ if (valuesSet.tag !== asn1.TAG.SET) continue;
677
+ var valueChildren;
678
+ try { valueChildren = asn1.readSequence(valuesSet.value); }
679
+ catch (_e) { continue; }
680
+ if (valueChildren.length === 0) continue;
681
+ try { return asn1.readOid(valueChildren[0]); }
682
+ catch (_e) { return null; }
683
+ }
684
+ return null;
685
+ }
624
686
 
625
687
  function _extractMessageDigest(signedAttrsRaw) {
626
688
  // signedAttrsRaw is `31 LL VV...` — the universal SET-tagged blob
@@ -821,4 +883,7 @@ module.exports = {
821
883
  // validation (a cert matches the verified signer key iff its SPKI public
822
884
  // key equals, or has as a suffix, the raw signer key bytes).
823
885
  _certKeyMatches: _certKeyMatches,
886
+ // Exposed for tests — RFC 5652 §11.1 contentType signed-attr extraction
887
+ // (verify binds it to the eContentType).
888
+ _extractContentTypeOid: _extractContentTypeOid,
824
889
  };
@@ -123,7 +123,7 @@ function mtaStsPublish(opts) {
123
123
  throw new MailDeployError("mail-deploy/bad-domain",
124
124
  "mtaStsPublish: opts.domain must be a valid hostname");
125
125
  }
126
- if (!STS_MODES[opts.mode]) {
126
+ if (!Object.prototype.hasOwnProperty.call(STS_MODES, opts.mode)) {
127
127
  throw new MailDeployError("mail-deploy/bad-mode",
128
128
  "mtaStsPublish: opts.mode must be 'enforce' | 'testing' | 'none'");
129
129
  }
@@ -730,14 +730,14 @@ function _validateTlsRptReport(raw, ctx) {
730
730
  var sRaw = summary["total-successful-session-count"];
731
731
  var fRaw = summary["total-failure-session-count"];
732
732
  if (sRaw !== undefined) {
733
- if (typeof sRaw !== "number" || !isFinite(sRaw) || sRaw < 0 || Math.floor(sRaw) !== sRaw) {
733
+ if (!numericBounds.isNonNegativeFiniteInt(sRaw)) {
734
734
  throw new TlsRptParseError("mail-tlsrpt/bad-summary",
735
735
  "parseTlsRptReport: policies[" + i + "].summary.total-successful-session-count must be a finite non-negative integer");
736
736
  }
737
737
  totalSuccess += sRaw;
738
738
  }
739
739
  if (fRaw !== undefined) {
740
- if (typeof fRaw !== "number" || !isFinite(fRaw) || fRaw < 0 || Math.floor(fRaw) !== fRaw) {
740
+ if (!numericBounds.isNonNegativeFiniteInt(fRaw)) {
741
741
  throw new TlsRptParseError("mail-tlsrpt/bad-summary",
742
742
  "parseTlsRptReport: policies[" + i + "].summary.total-failure-session-count must be a finite non-negative integer");
743
743
  }
@@ -776,7 +776,11 @@ function _validatePolicy(p, idx) {
776
776
  "parseTlsRptReport: policies[" + idx + "].policy missing");
777
777
  }
778
778
  var pType = policy["policy-type"];
779
- if (!TLSRPT_POLICY_TYPES[pType]) {
779
+ // hasOwnProperty, not `TLSRPT_POLICY_TYPES[pType]`: a remote MTA's report JSON
780
+ // controls pType, and a bracket lookup on the plain-object allowlist resolves
781
+ // inherited members ("constructor"/"__proto__"/"toString") to truthy, passing
782
+ // an out-of-vocabulary policy-type (prototype shadowing).
783
+ if (typeof pType !== "string" || !Object.prototype.hasOwnProperty.call(TLSRPT_POLICY_TYPES, pType)) {
780
784
  throw new TlsRptParseError("mail-tlsrpt/bad-policy",
781
785
  "parseTlsRptReport: policies[" + idx + "].policy.policy-type '" + pType +
782
786
  "' not in {sts, tlsa, no-policy-found}");
@@ -805,7 +809,7 @@ function _validatePolicy(p, idx) {
805
809
  throw new TlsRptParseError("mail-tlsrpt/bad-failure-detail",
806
810
  "parseTlsRptReport: policies[" + idx + "].failure-details[" + k + "] must be an object");
807
811
  }
808
- if (typeof fd["result-type"] === "string" && !TLSRPT_RESULT_TYPES[fd["result-type"]]) {
812
+ if (typeof fd["result-type"] === "string" && !Object.prototype.hasOwnProperty.call(TLSRPT_RESULT_TYPES, fd["result-type"])) {
809
813
  // Unknown result-type — surface as audit metadata but don't
810
814
  // refuse; RFC 8460 §4.4 result-type registry can grow over
811
815
  // time and we shouldn't break on new IANA entries.
package/lib/mail-dkim.js CHANGED
@@ -167,13 +167,24 @@ function _parseHeaders(rawHeaders) {
167
167
 
168
168
  // ---- Hashing + signing ----
169
169
 
170
- function _bodyHashB64(body, algorithm, canonBody) {
170
+ function _bodyHashB64(body, algorithm, canonBody, lcap) {
171
171
  var canonicalized = canonBody === "simple"
172
172
  ? _canonBodySimple(body)
173
173
  : _canonBodyRelaxed(body);
174
174
  var hashName = "sha256"; // both rsa-sha256 and ed25519-sha256 hash with sha256
175
- return nodeCrypto.createHash(hashName)
176
- .update(canonicalized).digest("base64");
175
+ var hash = nodeCrypto.createHash(hashName);
176
+ // RFC 6376 §3.4.5 — the l= body-length count is octets of the body AFTER
177
+ // canonicalization. Canonicalize first, then truncate the canonicalized
178
+ // octet stream to lcap (slicing the raw body before canonicalizing diverges
179
+ // whenever relaxed canon changes the byte count within the first lcap
180
+ // octets — WSP-run collapse, trailing-WSP strip, CRLF normalization).
181
+ if (typeof lcap === "number" && isFinite(lcap) && lcap >= 0) {
182
+ var buf = Buffer.from(canonicalized, "utf8");
183
+ hash.update(lcap < buf.length ? buf.subarray(0, lcap) : buf);
184
+ } else {
185
+ hash.update(canonicalized);
186
+ }
187
+ return hash.digest("base64");
177
188
  }
178
189
 
179
190
  function _signString(strToSign, privateKey, algorithm) {
@@ -674,15 +685,17 @@ function _verifySingleSignature(rfc822, parsedHeaders, sigHeader, keyTags, sigTa
674
685
 
675
686
  var split = _splitHeadersBody(rfc822);
676
687
  var body = split.body;
688
+ var lcap;
677
689
  if (sigTags.l !== undefined) {
678
690
  // The framework refuses l= at SIGN-time per the M3AAWG / Gmail /
679
691
  // Microsoft 365 guidance (v0.7.18). On VERIFY, an `l=` tag on an
680
692
  // inbound signature signals append-after-signature exposure —
681
693
  // operators decide acceptance. Honor the cap for the body hash so
682
694
  // the signature still validates against legitimate senders that
683
- // use l=, but flag in the result.
684
- var lcap = parseInt(sigTags.l, 10);
685
- if (isFinite(lcap) && lcap >= 0) body = body.slice(0, lcap);
695
+ // use l=, but flag in the result. The cap is octets of the
696
+ // CANONICALIZED body (RFC 6376 §3.4.5), applied inside _bodyHashB64.
697
+ var parsedL = parseInt(sigTags.l, 10);
698
+ if (isFinite(parsedL) && parsedL >= 0) lcap = parsedL;
686
699
  }
687
700
 
688
701
  // 1. Body-hash check.
@@ -690,7 +703,7 @@ function _verifySingleSignature(rfc822, parsedHeaders, sigHeader, keyTags, sigTa
690
703
  if (typeof expectedBh !== "string") {
691
704
  return { result: "permerror", errors: ["DKIM-Signature missing bh="] };
692
705
  }
693
- var actualBh = _bodyHashB64(body, algorithm, canonBody);
706
+ var actualBh = _bodyHashB64(body, algorithm, canonBody, lcap);
694
707
  if (actualBh !== expectedBh) {
695
708
  return { result: "fail", errors: ["body hash mismatch"] };
696
709
  }
@@ -162,7 +162,7 @@ var COMPLIANCE_POSTURES = gateContract.ALL_STRICT_POSTURES;
162
162
  function create(opts) {
163
163
  opts = opts || {};
164
164
  var profile = gateContract.resolveProfileName(opts, COMPLIANCE_POSTURES, DEFAULT_PROFILE);
165
- if (!PROFILES[profile]) {
165
+ if (!Object.prototype.hasOwnProperty.call(PROFILES, profile)) {
166
166
  throw new MailGreylistError("mail-greylist/bad-profile",
167
167
  "create: unknown profile '" + profile + "'");
168
168
  }
@@ -323,9 +323,7 @@ function create(opts) {
323
323
  * @example
324
324
  * b.mail.greylist.compliancePosture("hipaa"); // → "strict"
325
325
  */
326
- function compliancePosture(posture) {
327
- return COMPLIANCE_POSTURES[posture] || null;
328
- }
326
+ var compliancePosture = gateContract.makePostureAccessor(COMPLIANCE_POSTURES);
329
327
 
330
328
  function _hashFingerprint(cidr, mailFrom, rcptTo) {
331
329
  // Namespace-hash so the on-disk key is unlinkable to the PII
package/lib/mail-helo.js CHANGED
@@ -201,7 +201,7 @@ var LOCALHOST_REFUSED = Object.freeze({
201
201
  async function evaluate(ctx, opts) {
202
202
  opts = opts || {};
203
203
  var profile = gateContract.resolveProfileName(opts, COMPLIANCE_POSTURES, DEFAULT_PROFILE);
204
- if (!PROFILES[profile]) {
204
+ if (!Object.prototype.hasOwnProperty.call(PROFILES, profile)) {
205
205
  throw new MailHeloError("mail-helo/bad-profile",
206
206
  "evaluate: unknown profile '" + profile + "'");
207
207
  }
@@ -453,9 +453,7 @@ function _emitAudit(auditImpl, action, metadata) {
453
453
  * @example
454
454
  * b.mail.helo.compliancePosture("hipaa"); // → "strict"
455
455
  */
456
- function compliancePosture(posture) {
457
- return COMPLIANCE_POSTURES[posture] || null;
458
- }
456
+ var compliancePosture = gateContract.makePostureAccessor(COMPLIANCE_POSTURES);
459
457
 
460
458
  module.exports = {
461
459
  evaluate: evaluate,
@@ -180,9 +180,9 @@ function create(opts) {
180
180
  throw new MailJournalError("mail-journal/bad-vault",
181
181
  "b.mail.journal.create: opts.vault must be a b.vault handle");
182
182
  }
183
- if (!opts.legalHold || typeof opts.legalHold.isOnHold !== "function") {
183
+ if (!opts.legalHold || typeof opts.legalHold.isHeld !== "function") {
184
184
  throw new MailJournalError("mail-journal/bad-legal-hold",
185
- "b.mail.journal.create: opts.legalHold must be a b.legalHold handle (must expose isOnHold)");
185
+ "b.mail.journal.create: opts.legalHold must be a b.legalHold handle (must expose isHeld)");
186
186
  }
187
187
  if (!opts.db || typeof opts.db.runSql !== "function") {
188
188
  throw new MailJournalError("mail-journal/bad-db",
@@ -252,7 +252,7 @@ function create(opts) {
252
252
  async function record(req) {
253
253
  validateOpts.requireObject(req, "mail.journal.record",
254
254
  MailJournalError, "mail-journal/bad-record");
255
- if (!ALLOWED_DIRECTIONS[req.direction]) {
255
+ if (!Object.prototype.hasOwnProperty.call(ALLOWED_DIRECTIONS, req.direction)) {
256
256
  throw new MailJournalError("mail-journal/bad-direction",
257
257
  "mail.journal.record: opts.direction must be 'inbound' | 'outbound' | 'internal'");
258
258
  }
@@ -300,11 +300,13 @@ function create(opts) {
300
300
 
301
301
  var regimesJson = JSON.stringify(opts.regimes);
302
302
  var floorUntil = archivedAt + floorMs;
303
- // legal_hold is omitted from the INSERT so the column's
304
- // `NOT NULL DEFAULT 0` applies (the prior inline `0` SQL literal) b.sql
305
- // binds every value it is given, so leaving the column out keeps the row
306
- // unsealed-at-rest default without binding a redundant constant. b.sql
307
- // quotes every column + binds every remaining value as a placeholder.
303
+ // Auto-derive the legal-hold flag from the registry: an entry whose actor is
304
+ // under an active legal hold is born held, exempt from retention expiry even
305
+ // after the floor passes (the doc's "every entry carries a legalHold flag").
306
+ // setLegalHold remains the manual override. isHeld already treats a lapsed
307
+ // (retainUntil-expired) hold as not-held, so a stale hold won't over-flag.
308
+ var actorHeld = !!opts.legalHold.isHeld(req.actorId);
309
+ // b.sql quotes every column + binds every value as a placeholder.
308
310
  var insBuilt = _t("insert").values({
309
311
  journal_id: journalId,
310
312
  direction: req.direction,
@@ -314,6 +316,7 @@ function create(opts) {
314
316
  size_bytes: sizeBytes,
315
317
  regimes: regimesJson,
316
318
  floor_until: floorUntil,
319
+ legal_hold: actorHeld ? 1 : 0,
317
320
  storage_key: storageKey,
318
321
  sealed_payload: sealedBlob,
319
322
  }).toSql();
package/lib/mail-mdn.js CHANGED
@@ -162,14 +162,14 @@ function build(opts) {
162
162
  validateOpts.requireNonEmptyString(opts.finalRecipient,
163
163
  "mailMdn.build: opts.finalRecipient", MailMdnError, "mdn/missing-required-field");
164
164
  var disposition = String(opts.disposition || "").toLowerCase();
165
- if (!DISPOSITION_TYPES[disposition]) {
165
+ if (!Object.prototype.hasOwnProperty.call(DISPOSITION_TYPES, disposition)) {
166
166
  throw _err("mdn/missing-required-field",
167
167
  "mailMdn.build: opts.disposition must be one of " +
168
168
  Object.keys(DISPOSITION_TYPES).join(" / ") +
169
169
  "; got '" + String(opts.disposition) + "'");
170
170
  }
171
171
  var actionMode = String(opts.actionMode || "manual-action").toLowerCase();
172
- if (!ACTION_MODES[actionMode]) {
172
+ if (!Object.prototype.hasOwnProperty.call(ACTION_MODES, actionMode)) {
173
173
  throw _err("mdn/missing-required-field",
174
174
  "mailMdn.build: opts.actionMode must be one of " +
175
175
  Object.keys(ACTION_MODES).join(" / ") +
@@ -179,7 +179,7 @@ function build(opts) {
179
179
  // Accept the canonical mixed-case form too — RFC 3798 §3.2.6.2 uses
180
180
  // `MDN-sent-manually` / `MDN-sent-automatically`. Compare lower-case
181
181
  // for robustness; emit canonical mixed-case in the output.
182
- if (!SENDING_MODES[sendingMode]) {
182
+ if (!Object.prototype.hasOwnProperty.call(SENDING_MODES, sendingMode)) {
183
183
  throw _err("mdn/missing-required-field",
184
184
  "mailMdn.build: opts.sendingMode must be one of " +
185
185
  "MDN-sent-manually / MDN-sent-automatically; got '" +
@@ -395,7 +395,11 @@ function parse(rawMessage) {
395
395
  "mailMdn.parse: message/disposition-notification missing Disposition");
396
396
  }
397
397
  var disposition = _parseDisposition(dispositionField);
398
- if (!disposition || !DISPOSITION_TYPES[disposition.type]) {
398
+ // hasOwnProperty: the Disposition token comes from an untrusted inbound MDN, so
399
+ // a bracket lookup lets "constructor"/"__proto__" pass as a valid type (proto
400
+ // shadowing).
401
+ if (!disposition || typeof disposition.type !== "string" ||
402
+ !Object.prototype.hasOwnProperty.call(DISPOSITION_TYPES, disposition.type)) {
399
403
  throw _err("mdn/parse-failed",
400
404
  "mailMdn.parse: Disposition type token not in RFC 3798 §3.2.6 vocabulary; got '" +
401
405
  (disposition && disposition.type) + "'");
package/lib/mail-rbl.js CHANGED
@@ -140,7 +140,7 @@ function create(opts) {
140
140
  "create: opts.resolver must be a b.network.dns.resolver.create() instance");
141
141
  }
142
142
  var profile = gateContract.resolveProfileName(opts, COMPLIANCE_POSTURES, DEFAULT_PROFILE);
143
- if (!PROFILES[profile]) {
143
+ if (!Object.prototype.hasOwnProperty.call(PROFILES, profile)) {
144
144
  throw new MailRblError("mail-rbl/bad-profile",
145
145
  "create: unknown profile '" + profile + "'");
146
146
  }
@@ -314,9 +314,7 @@ function reverseIp(ip) {
314
314
  * @example
315
315
  * b.mail.rbl.compliancePosture("hipaa"); // → "strict"
316
316
  */
317
- function compliancePosture(posture) {
318
- return COMPLIANCE_POSTURES[posture] || null;
319
- }
317
+ var compliancePosture = gateContract.makePostureAccessor(COMPLIANCE_POSTURES);
320
318
 
321
319
  function _validateZoneNames(zones) {
322
320
  for (var i = 0; i < zones.length; i += 1) {
package/lib/mail-scan.js CHANGED
@@ -164,7 +164,7 @@ function create(opts) {
164
164
  numericBounds.requirePositiveFiniteInt(opts.port, "mail.scan.create.port",
165
165
  MailScanError, "mail-scan/bad-port", { max: 65535 }); // TCP port-number range cap
166
166
  var protocol = opts.protocol || DEFAULT_PROTOCOL;
167
- if (!ALLOWED_PROTOCOLS[protocol]) {
167
+ if (!Object.prototype.hasOwnProperty.call(ALLOWED_PROTOCOLS, protocol)) {
168
168
  throw new MailScanError("mail-scan/bad-protocol",
169
169
  "mail.scan.create.protocol must be 'icap' or 'clamav-instream'; got '" + protocol + "'");
170
170
  }
@@ -174,7 +174,7 @@ function create(opts) {
174
174
  MailScanError, "mail-scan/bad-service");
175
175
  }
176
176
  var profile = gateContract.resolveProfileName(opts, COMPLIANCE_POSTURES, DEFAULT_PROFILE);
177
- if (!PROFILES[profile]) {
177
+ if (!Object.prototype.hasOwnProperty.call(PROFILES, profile)) {
178
178
  throw new MailScanError("mail-scan/bad-profile",
179
179
  "mail.scan.create.profile: unknown '" + profile + "' (valid: strict / balanced / permissive)");
180
180
  }
@@ -448,9 +448,7 @@ function create(opts) {
448
448
  * @example
449
449
  * b.mail.scan.compliancePosture("hipaa"); // → "strict"
450
450
  */
451
- function compliancePosture(posture) {
452
- return COMPLIANCE_POSTURES[posture] || null;
453
- }
451
+ var compliancePosture = gateContract.makePostureAccessor(COMPLIANCE_POSTURES);
454
452
 
455
453
  function _emitScanResult(auditImpl, rv) {
456
454
  if (rv.verdict === "clean") {