@blamejs/core 0.15.11 → 0.15.13

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 (305) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +7 -23
  4. package/lib/acme.js +13 -16
  5. package/lib/agent-event-bus.js +5 -4
  6. package/lib/agent-idempotency.js +2 -5
  7. package/lib/agent-orchestrator.js +2 -5
  8. package/lib/agent-saga.js +3 -5
  9. package/lib/agent-tenant.js +2 -5
  10. package/lib/ai-adverse-decision.js +2 -15
  11. package/lib/ai-capability.js +1 -6
  12. package/lib/ai-dp.js +1 -5
  13. package/lib/ai-input.js +2 -2
  14. package/lib/ai-pref.js +3 -8
  15. package/lib/ai-quota.js +3 -14
  16. package/lib/api-key.js +37 -28
  17. package/lib/archive-adapters.js +2 -4
  18. package/lib/archive-entry-policy.js +32 -0
  19. package/lib/archive-read.js +5 -17
  20. package/lib/archive-tar-read.js +5 -16
  21. package/lib/archive.js +2 -10
  22. package/lib/arg-parser.js +7 -6
  23. package/lib/asyncapi-traits.js +2 -6
  24. package/lib/atomic-file.js +108 -31
  25. package/lib/audit-chain.js +133 -53
  26. package/lib/audit-daily-review.js +24 -14
  27. package/lib/audit-emit.js +82 -0
  28. package/lib/audit-sign.js +257 -0
  29. package/lib/audit.js +84 -0
  30. package/lib/auth/access-lock.js +5 -27
  31. package/lib/auth/dpop.js +23 -35
  32. package/lib/auth/fido-mds3.js +9 -15
  33. package/lib/auth/jwt-external.js +26 -8
  34. package/lib/auth/jwt.js +13 -15
  35. package/lib/auth/lockout.js +6 -25
  36. package/lib/auth/oauth.js +67 -45
  37. package/lib/auth/oid4vci.js +55 -32
  38. package/lib/auth/oid4vp.js +3 -2
  39. package/lib/auth/openid-federation.js +6 -6
  40. package/lib/auth/passkey.js +3 -3
  41. package/lib/auth/password.js +7 -1
  42. package/lib/auth/saml.js +37 -27
  43. package/lib/auth/sd-jwt-vc-holder.js +2 -14
  44. package/lib/auth/sd-jwt-vc-issuer.js +2 -14
  45. package/lib/auth/sd-jwt-vc.js +1 -1
  46. package/lib/auth/status-list.js +7 -7
  47. package/lib/auth/step-up.js +6 -12
  48. package/lib/auth-bot-challenge.js +6 -37
  49. package/lib/backup/bundle.js +11 -18
  50. package/lib/backup/index.js +14 -47
  51. package/lib/bounded-map.js +112 -1
  52. package/lib/breach-deadline.js +1 -11
  53. package/lib/cache.js +7 -18
  54. package/lib/cbor.js +2 -1
  55. package/lib/cdn-cache-control.js +8 -9
  56. package/lib/cert.js +3 -10
  57. package/lib/chain-writer.js +162 -47
  58. package/lib/cli.js +5 -1
  59. package/lib/client-hints.js +10 -10
  60. package/lib/cloud-events.js +40 -31
  61. package/lib/cluster-storage.js +2 -38
  62. package/lib/cluster.js +4 -2
  63. package/lib/cms-codec.js +2 -1
  64. package/lib/codepoint-class.js +67 -1
  65. package/lib/compliance-ai-act-logging.js +1 -6
  66. package/lib/compliance-ai-act-transparency.js +2 -4
  67. package/lib/compliance-eaa.js +1 -11
  68. package/lib/compliance-sanctions-fetcher.js +21 -28
  69. package/lib/compliance-sanctions.js +2 -14
  70. package/lib/compliance.js +2 -9
  71. package/lib/config-drift.js +2 -12
  72. package/lib/content-digest.js +10 -8
  73. package/lib/cookies.js +5 -11
  74. package/lib/cose.js +19 -11
  75. package/lib/cra-report.js +1 -11
  76. package/lib/crypto-field.js +5 -10
  77. package/lib/crypto.js +120 -3
  78. package/lib/csp.js +235 -3
  79. package/lib/daemon.js +42 -41
  80. package/lib/data-act.js +19 -9
  81. package/lib/db-query.js +6 -40
  82. package/lib/db.js +34 -12
  83. package/lib/dbsc.js +5 -6
  84. package/lib/ddl-change-control.js +18 -14
  85. package/lib/deprecate.js +4 -5
  86. package/lib/did.js +6 -2
  87. package/lib/dsr.js +8 -12
  88. package/lib/external-db-migrate.js +21 -22
  89. package/lib/external-db.js +14 -26
  90. package/lib/fda-21cfr11.js +12 -8
  91. package/lib/fdx.js +22 -18
  92. package/lib/file-upload.js +80 -66
  93. package/lib/flag-evaluation-context.js +5 -8
  94. package/lib/framework-error.js +12 -0
  95. package/lib/framework-schema.js +19 -0
  96. package/lib/fsm.js +7 -12
  97. package/lib/gate-contract.js +869 -38
  98. package/lib/gdpr-ropa.js +4 -13
  99. package/lib/graphql-federation.js +1 -1
  100. package/lib/guard-agent-registry.js +2 -1
  101. package/lib/guard-all.js +1 -0
  102. package/lib/guard-archive.js +9 -30
  103. package/lib/guard-auth.js +23 -80
  104. package/lib/guard-cidr.js +20 -96
  105. package/lib/guard-csv.js +135 -196
  106. package/lib/guard-domain.js +23 -106
  107. package/lib/guard-dsn.js +16 -13
  108. package/lib/guard-email.js +46 -53
  109. package/lib/guard-envelope.js +1 -1
  110. package/lib/guard-event-bus-topic.js +2 -1
  111. package/lib/guard-filename.js +18 -62
  112. package/lib/guard-graphql.js +28 -75
  113. package/lib/guard-html-wcag.js +15 -2
  114. package/lib/guard-html.js +65 -117
  115. package/lib/guard-idempotency-key.js +2 -1
  116. package/lib/guard-image.js +280 -77
  117. package/lib/guard-imap-command.js +8 -9
  118. package/lib/guard-json.js +87 -103
  119. package/lib/guard-jsonpath.js +20 -88
  120. package/lib/guard-jwt.js +32 -114
  121. package/lib/guard-list-id.js +2 -7
  122. package/lib/guard-list-unsubscribe.js +2 -7
  123. package/lib/guard-mail-compose.js +5 -6
  124. package/lib/guard-mail-move.js +2 -1
  125. package/lib/guard-mail-query.js +5 -3
  126. package/lib/guard-mail-sieve.js +6 -7
  127. package/lib/guard-managesieve-command.js +5 -4
  128. package/lib/guard-markdown.js +76 -110
  129. package/lib/guard-message-id.js +5 -6
  130. package/lib/guard-mime.js +20 -99
  131. package/lib/guard-oauth.js +19 -73
  132. package/lib/guard-pdf.js +65 -72
  133. package/lib/guard-pop3-command.js +12 -13
  134. package/lib/guard-posture-chain.js +2 -1
  135. package/lib/guard-regex.js +24 -99
  136. package/lib/guard-saga-config.js +2 -1
  137. package/lib/guard-shell.js +22 -87
  138. package/lib/guard-smtp-command.js +8 -11
  139. package/lib/guard-sql.js +15 -13
  140. package/lib/guard-stream-args.js +2 -1
  141. package/lib/guard-svg.js +95 -140
  142. package/lib/guard-template.js +23 -80
  143. package/lib/guard-tenant-id.js +2 -1
  144. package/lib/guard-text.js +592 -0
  145. package/lib/guard-time.js +27 -95
  146. package/lib/guard-uuid.js +21 -93
  147. package/lib/guard-xml.js +76 -106
  148. package/lib/guard-yaml.js +24 -60
  149. package/lib/http-client-cache.js +8 -13
  150. package/lib/http-client-cookie-jar.js +2 -4
  151. package/lib/http-client.js +8 -21
  152. package/lib/http-message-signature.js +26 -8
  153. package/lib/i18n-messageformat.js +5 -7
  154. package/lib/i18n.js +83 -26
  155. package/lib/inbox.js +8 -8
  156. package/lib/incident-report.js +3 -21
  157. package/lib/ip-utils.js +49 -6
  158. package/lib/jobs.js +3 -2
  159. package/lib/keychain.js +6 -18
  160. package/lib/legal-hold.js +6 -15
  161. package/lib/log-stream-cloudwatch.js +44 -112
  162. package/lib/log-stream-otlp-grpc.js +28 -14
  163. package/lib/log-stream-otlp.js +16 -80
  164. package/lib/log-stream-syslog.js +6 -0
  165. package/lib/log-stream-webhook.js +20 -92
  166. package/lib/log.js +24 -2
  167. package/lib/mail-arc-sign.js +8 -3
  168. package/lib/mail-arf.js +3 -2
  169. package/lib/mail-auth.js +40 -66
  170. package/lib/mail-bimi.js +19 -39
  171. package/lib/mail-crypto-pgp.js +3 -2
  172. package/lib/mail-dav.js +8 -35
  173. package/lib/mail-deploy.js +6 -9
  174. package/lib/mail-dkim.js +19 -38
  175. package/lib/mail-greylist.js +15 -26
  176. package/lib/mail-helo.js +2 -3
  177. package/lib/mail-journal.js +2 -1
  178. package/lib/mail-mdn.js +4 -3
  179. package/lib/mail-rbl.js +5 -8
  180. package/lib/mail-scan.js +2 -2
  181. package/lib/mail-send-deliver.js +33 -20
  182. package/lib/mail-server-imap.js +32 -87
  183. package/lib/mail-server-jmap.js +35 -51
  184. package/lib/mail-server-managesieve.js +29 -83
  185. package/lib/mail-server-mx.js +33 -74
  186. package/lib/mail-server-net.js +177 -0
  187. package/lib/mail-server-pop3.js +30 -83
  188. package/lib/mail-server-rate-limit.js +30 -6
  189. package/lib/mail-server-submission.js +34 -73
  190. package/lib/mail-server-tls.js +89 -0
  191. package/lib/mail-spam-score.js +7 -8
  192. package/lib/mail-store.js +3 -2
  193. package/lib/mail.js +11 -11
  194. package/lib/markup-escape.js +31 -0
  195. package/lib/markup-tokenizer.js +54 -0
  196. package/lib/mcp-tool-registry.js +26 -23
  197. package/lib/mcp.js +1 -1
  198. package/lib/mdoc.js +11 -12
  199. package/lib/metrics.js +32 -30
  200. package/lib/middleware/age-gate.js +3 -23
  201. package/lib/middleware/api-encrypt.js +11 -22
  202. package/lib/middleware/assetlinks.js +2 -7
  203. package/lib/middleware/bearer-auth.js +2 -1
  204. package/lib/middleware/body-parser.js +79 -54
  205. package/lib/middleware/bot-disclose.js +7 -10
  206. package/lib/middleware/bot-guard.js +2 -10
  207. package/lib/middleware/csrf-protect.js +13 -2
  208. package/lib/middleware/daily-byte-quota.js +6 -26
  209. package/lib/middleware/deny-response.js +19 -1
  210. package/lib/middleware/fetch-metadata.js +7 -0
  211. package/lib/middleware/idempotency-key.js +4 -20
  212. package/lib/middleware/rate-limit.js +20 -28
  213. package/lib/middleware/scim-server.js +3 -2
  214. package/lib/middleware/security-headers.js +9 -1
  215. package/lib/middleware/security-txt.js +2 -6
  216. package/lib/middleware/tus-upload.js +12 -27
  217. package/lib/middleware/web-app-manifest.js +2 -7
  218. package/lib/migrations.js +4 -13
  219. package/lib/mime-parse.js +34 -17
  220. package/lib/module-loader.js +44 -0
  221. package/lib/money.js +105 -0
  222. package/lib/mtls-ca.js +116 -36
  223. package/lib/network-byte-quota.js +4 -18
  224. package/lib/network-dane.js +8 -6
  225. package/lib/network-dns-resolver.js +97 -6
  226. package/lib/network-dns.js +22 -26
  227. package/lib/network-dnssec.js +16 -16
  228. package/lib/network-heartbeat.js +6 -5
  229. package/lib/network-proxy.js +3 -7
  230. package/lib/network-smtp-policy.js +29 -41
  231. package/lib/network-tls.js +74 -55
  232. package/lib/network.js +2 -6
  233. package/lib/nis2-report.js +1 -11
  234. package/lib/nonce-store.js +81 -3
  235. package/lib/notify.js +7 -12
  236. package/lib/numeric-bounds.js +22 -8
  237. package/lib/object-store/azure-blob-bucket-ops.js +2 -6
  238. package/lib/object-store/azure-blob.js +5 -45
  239. package/lib/object-store/gcs.js +8 -71
  240. package/lib/object-store/http-put.js +1 -5
  241. package/lib/object-store/http-request.js +128 -0
  242. package/lib/object-store/sigv4-bucket-ops.js +2 -1
  243. package/lib/object-store/sigv4.js +9 -77
  244. package/lib/observability-otlp-exporter.js +9 -25
  245. package/lib/observability.js +95 -0
  246. package/lib/openapi-paths-builder.js +7 -3
  247. package/lib/otel-export.js +7 -10
  248. package/lib/outbox.js +43 -37
  249. package/lib/pagination.js +11 -15
  250. package/lib/parsers/safe-env.js +5 -4
  251. package/lib/parsers/safe-ini.js +10 -4
  252. package/lib/parsers/safe-toml.js +11 -9
  253. package/lib/parsers/safe-xml.js +2 -2
  254. package/lib/parsers/safe-yaml.js +7 -6
  255. package/lib/pick.js +63 -5
  256. package/lib/pipl-cn.js +69 -59
  257. package/lib/privacy-pass.js +8 -6
  258. package/lib/problem-details.js +2 -5
  259. package/lib/pubsub.js +4 -8
  260. package/lib/redact.js +2 -1
  261. package/lib/render.js +4 -2
  262. package/lib/request-helpers.js +133 -6
  263. package/lib/restore.js +5 -22
  264. package/lib/retention.js +3 -5
  265. package/lib/safe-async.js +457 -0
  266. package/lib/safe-buffer.js +137 -6
  267. package/lib/safe-decompress.js +2 -1
  268. package/lib/safe-dns.js +2 -1
  269. package/lib/safe-ical.js +12 -26
  270. package/lib/safe-json.js +7 -8
  271. package/lib/safe-jsonpath.js +6 -5
  272. package/lib/safe-mime.js +25 -29
  273. package/lib/safe-redirect.js +2 -5
  274. package/lib/safe-schema.js +7 -6
  275. package/lib/safe-sql.js +126 -0
  276. package/lib/safe-vcard.js +12 -26
  277. package/lib/sandbox-worker.js +9 -2
  278. package/lib/sandbox.js +3 -2
  279. package/lib/scheduler.js +5 -12
  280. package/lib/sec-cyber.js +82 -37
  281. package/lib/seeders.js +9 -21
  282. package/lib/self-update.js +92 -69
  283. package/lib/session-device-binding.js +112 -66
  284. package/lib/session.js +194 -6
  285. package/lib/sql.js +225 -78
  286. package/lib/sse.js +6 -10
  287. package/lib/static.js +136 -98
  288. package/lib/storage.js +4 -2
  289. package/lib/structured-fields.js +313 -17
  290. package/lib/tenant-quota.js +2 -20
  291. package/lib/tsa.js +11 -15
  292. package/lib/validate-opts.js +154 -8
  293. package/lib/vault/seal-pem-file.js +30 -48
  294. package/lib/vault-aad.js +3 -2
  295. package/lib/vc.js +2 -7
  296. package/lib/vex.js +35 -13
  297. package/lib/web-push-vapid.js +23 -20
  298. package/lib/webhook-dispatcher.js +706 -0
  299. package/lib/webhook.js +43 -18
  300. package/lib/websocket-channels.js +9 -7
  301. package/lib/websocket.js +7 -3
  302. package/lib/worm.js +2 -3
  303. package/lib/ws-client.js +8 -10
  304. package/package.json +1 -1
  305. package/sbom.cdx.json +6 -6
@@ -39,6 +39,7 @@
39
39
  */
40
40
 
41
41
  var C = require("./constants");
42
+ var boundedMap = require("./bounded-map");
42
43
  var defineClass = require("./framework-error").defineClass;
43
44
  var lazyRequire = require("./lazy-require");
44
45
  var validateOpts = require("./validate-opts");
@@ -79,9 +80,7 @@ function _slideAndSum(entry, nowHour) {
79
80
  function _memoryBackend() {
80
81
  var store = new Map();
81
82
  function _get(key) {
82
- var entry = store.get(key);
83
- if (!entry) { entry = _newEntry(); store.set(key, entry); }
84
- return entry;
83
+ return boundedMap.getOrInsert(store, key, function () { return _newEntry(); });
85
84
  }
86
85
  return {
87
86
  async total(key, nowMs) {
@@ -172,27 +171,14 @@ function create(opts) {
172
171
  validateOpts(opts, ["bytesPerDay", "cache", "audit", "now"], "network.byteQuota");
173
172
  _requirePositiveBytes("bytesPerDay", opts.bytesPerDay);
174
173
  var bytesPerDay = opts.bytesPerDay;
175
- var auditOn = opts.audit !== false;
176
174
  var now = typeof opts.now === "function" ? opts.now : function () { return Date.now(); };
177
175
  var backend = opts.cache && typeof opts.cache.get === "function"
178
176
  ? _cacheBackend(opts.cache)
179
177
  : _memoryBackend();
180
178
 
181
- function _emitAudit(action, outcome, metadata) {
182
- if (!auditOn) return;
183
- try {
184
- audit().safeEmit({
185
- action: "network.byte_quota." + action,
186
- outcome: outcome,
187
- metadata: metadata || {},
188
- });
189
- } catch (_e) { /* drop-silent — audit is best-effort */ }
190
- }
179
+ var _emitAudit = audit().namespaced("network.byte_quota", opts.audit);
191
180
 
192
- function _emitMetric(verb, n, labels) {
193
- try { observability().safeEvent("network.byte_quota." + verb, n || 1, labels || {}); }
194
- catch (_e) { /* drop-silent */ }
195
- }
181
+ var _emitMetric = observability().namespaced("network.byte_quota");
196
182
 
197
183
  // check(key, bytes) — preflight without mutation. Returns
198
184
  // { allowed, total, remaining, quota, retryAfterSec, degraded }
@@ -31,6 +31,7 @@
31
31
 
32
32
  var nodeCrypto = require("node:crypto");
33
33
  var bCrypto = require("./crypto");
34
+ var safeBuffer = require("./safe-buffer");
34
35
  var validateOpts = require("./validate-opts");
35
36
  var { defineClass } = require("./framework-error");
36
37
 
@@ -44,12 +45,13 @@ var SELECTORS = { 0: "Cert", 1: "SPKI" };
44
45
  // refused rather than guessed.
45
46
  var MATCHING = { 0: null, 1: "sha256", 2: "sha512" };
46
47
 
47
- function _bytes(x, what) {
48
- if (Buffer.isBuffer(x)) return x;
49
- if (x instanceof Uint8Array) return Buffer.from(x);
50
- if (typeof x === "string") return Buffer.from(x, "hex");
51
- throw new DaneError("dane/bad-bytes", "dane: " + what + " must be a Buffer / Uint8Array / hex string");
52
- }
48
+ var _bytes = safeBuffer.makeByteCoercer({
49
+ errorClass: DaneError,
50
+ typeCode: "dane/bad-bytes",
51
+ messagePrefix: "dane: ",
52
+ messageSuffix: " must be a Buffer / Uint8Array / hex string",
53
+ encoding: "hex",
54
+ });
53
55
 
54
56
  function _selectedData(x509, selector) {
55
57
  if (selector === 0) return Buffer.from(x509.raw); // full certificate DER
@@ -188,7 +188,9 @@ function create(opts) {
188
188
  var serveStale = opts.serveStale === false ? 0 :
189
189
  typeof opts.serveStale === "number" ? opts.serveStale : DEFAULT_STALE_WINDOW;
190
190
  var transport = opts.transport || _defaultTransport();
191
- var auditImpl = opts.audit || audit();
191
+ // Audit goes to the operator-supplied sink (opts.audit) when present, else the
192
+ // framework chain — b.audit.namespaced no-ops if the sink has no safeEmit.
193
+ var _baseAudit = audit().namespaced("network.dns.resolver", { sink: opts.audit });
192
194
 
193
195
  if (typeof transport.lookup !== "function") {
194
196
  throw new ResolverError("resolver/bad-transport",
@@ -238,11 +240,7 @@ function create(opts) {
238
240
  }
239
241
 
240
242
  function _safeEmit(action, metadata) {
241
- try {
242
- if (auditImpl && typeof auditImpl.safeEmit === "function") {
243
- auditImpl.safeEmit({ action: "network.dns.resolver." + action, outcome: "success", metadata: metadata });
244
- }
245
- } catch (_e) { /* audit drop-silent per validation tier policy */ }
243
+ _baseAudit(action, "success", metadata);
246
244
  }
247
245
 
248
246
  async function query(name, type, qopts) {
@@ -526,8 +524,101 @@ function _minTtl(rrs) {
526
524
  return min === Infinity ? 0 : min;
527
525
  }
528
526
 
527
+ // _sharedTxtResolver — one process-wide validating resolver (with its TTL
528
+ // cache) shared by every email-auth policy TXT lookup, instead of each module
529
+ // constructing its own (or reaching for plaintext system DNS).
530
+ var _sharedResolver = null;
531
+ function _sharedTxtResolver() {
532
+ if (!_sharedResolver) _sharedResolver = create();
533
+ return _sharedResolver;
534
+ }
535
+
536
+ /**
537
+ * @primitive b.network.dns.resolver.resolveTxt
538
+ * @signature b.network.dns.resolver.resolveTxt(qname, dnsLookup?, resolver?)
539
+ * @since 0.15.13
540
+ * @status stable
541
+ * @related b.network.dns.resolver.safeResolveTxt, b.network.dns.resolver.create
542
+ *
543
+ * Resolve TXT records for `qname` via the operator's `dnsLookup(qname, "TXT")`
544
+ * override when supplied, otherwise the framework's shared validating
545
+ * (DoH / DoT / system, DNSSEC-checked) resolver — normalized to the
546
+ * `string[][]` shape `dnsPromises.resolveTxt` returns. Throws an `ENODATA`
547
+ * Error when no TXT records exist.
548
+ *
549
+ * This is the secure DNS path for every email-auth policy lookup (DMARC / DKIM /
550
+ * ARC / BIMI / MTA-STS / TLS-RPT). Plaintext system DNS (`dnsPromises.resolveTxt`)
551
+ * is spoofable — DNS cache-poisoning / Kaminsky-class — and must NOT be used for
552
+ * records a downgrade or redirect would exploit.
553
+ *
554
+ * Pass `resolver` (a `b.network.dns.resolver.create()` instance) to reshape TXT
555
+ * records off a caller-owned resolver instead of the shared one — the mail-auth
556
+ * and mail-dkim modules use this so their TXT lookups share the same resolver
557
+ * (and cache) they use for A / MX / PTR, instead of each re-rolling the reshape.
558
+ *
559
+ * @example
560
+ * var rrs = await b.network.dns.resolver.resolveTxt("_dmarc.example.com");
561
+ * // → [ ["v=DMARC1; p=reject"] ]
562
+ */
563
+ async function resolveTxt(qname, dnsLookup, resolver) {
564
+ if (dnsLookup) return dnsLookup(qname, "TXT");
565
+ var r = await (resolver || _sharedTxtResolver()).queryTxt(qname);
566
+ var out = [];
567
+ for (var i = 0; i < r.rrs.length; i += 1) {
568
+ var rr = r.rrs[i];
569
+ if (rr && rr.type === 16) { // IANA DNS qtype TXT
570
+ out.push(Array.isArray(rr.decoded) ? rr.decoded : [String(rr.decoded)]);
571
+ }
572
+ }
573
+ if (out.length === 0) {
574
+ var err = new Error("no TXT records for " + qname);
575
+ err.code = "ENODATA";
576
+ throw err;
577
+ }
578
+ return out;
579
+ }
580
+
581
+ /**
582
+ * @primitive b.network.dns.resolver.safeResolveTxt
583
+ * @signature b.network.dns.resolver.safeResolveTxt(qname, opts?)
584
+ * @since 0.15.13
585
+ * @status stable
586
+ * @related b.network.dns.resolver.resolveTxt
587
+ *
588
+ * `resolveTxt` wrapped with the email-auth policy-fetch convention: a domain
589
+ * with no record (ENOTFOUND / ENODATA) returns `null` (absence is not an
590
+ * error); any other failure throws the caller's typed error via
591
+ * `errorFactory(code, message)` so each protocol keeps its own error class.
592
+ * The same secure resolver path as `resolveTxt` — never plaintext system DNS.
593
+ *
594
+ * @opts
595
+ * dnsLookup: function, // operator override (qname, "TXT") => string[][]
596
+ * errorFactory: function, // (code, message) => Error, thrown on non-absence failures
597
+ * code: string, // error code passed to errorFactory
598
+ *
599
+ * @example
600
+ * var rrs = await b.network.dns.resolver.safeResolveTxt("_mta-sts.example.com", {
601
+ * errorFactory: function (code, msg) { return new SmtpPolicyError(code, msg); },
602
+ * code: "smtp/mta-sts-txt-lookup-failed",
603
+ * });
604
+ * if (rrs === null) return null; // no MTA-STS record published
605
+ */
606
+ async function safeResolveTxt(qname, opts) {
607
+ opts = opts || {};
608
+ try {
609
+ return await resolveTxt(qname, opts.dnsLookup);
610
+ } catch (e) {
611
+ if (e && (e.code === "ENOTFOUND" || e.code === "ENODATA")) return null;
612
+ var msg = "TXT lookup for " + qname + " failed: " + ((e && e.message) || String(e));
613
+ if (typeof opts.errorFactory === "function") throw opts.errorFactory(opts.code, msg);
614
+ throw e;
615
+ }
616
+ }
617
+
529
618
  module.exports = {
530
619
  create: create,
620
+ resolveTxt: resolveTxt,
621
+ safeResolveTxt: safeResolveTxt,
531
622
  ResolverError: ResolverError,
532
623
  QTYPE_BY_NAME: QTYPE_BY_NAME,
533
624
  };
@@ -149,7 +149,7 @@ function setServers(serverList) {
149
149
  throw new DnsError("dns/setservers-failed", "dns.setServers failed: " + e.message);
150
150
  }
151
151
  _clearCache();
152
- _emitObs("network.dns.servers.set", { count: serverList.length });
152
+ observability().safeEvent("network.dns.servers.set", 1, { count: serverList.length });
153
153
  }
154
154
 
155
155
  function getServers() {
@@ -169,7 +169,7 @@ function setResultOrder(order) {
169
169
  try { dns.setDefaultResultOrder(order); } catch (_e) { /* node may not support setter on this version — best-effort */ }
170
170
  }
171
171
  _clearCache();
172
- _emitObs("network.dns.result_order.set", { order: order });
172
+ observability().safeEvent("network.dns.result_order.set", 1, { order: order });
173
173
  }
174
174
 
175
175
  function setFamily(fam) {
@@ -215,7 +215,7 @@ function useSystemResolver() {
215
215
  STATE.systemResolver = true;
216
216
  _resetDotPool();
217
217
  _clearCache();
218
- _emitObs("network.dns.system_resolver.set", {});
218
+ observability().safeEvent("network.dns.system_resolver.set", 1, {});
219
219
  }
220
220
 
221
221
  function useDnsOverHttps(opts) {
@@ -246,7 +246,7 @@ function useDnsOverHttps(opts) {
246
246
  }
247
247
  STATE.doh = { url: url, method: method, ca: opts.ca || null };
248
248
  _clearCache();
249
- _emitObs("network.dns.doh.set", { url: url, method: method || "auto" });
249
+ observability().safeEvent("network.dns.doh.set", 1, { url: url, method: method || "auto" });
250
250
  }
251
251
 
252
252
  function useDnsOverTls(opts) {
@@ -267,7 +267,7 @@ function useDnsOverTls(opts) {
267
267
  };
268
268
  _resetDotPool();
269
269
  _clearCache();
270
- _emitObs("network.dns.dot.set", { host: STATE.dot.host, port: STATE.dot.port });
270
+ observability().safeEvent("network.dns.dot.set", 1, { host: STATE.dot.host, port: STATE.dot.port });
271
271
  }
272
272
 
273
273
  function _withTimeout(promise, ms, host) {
@@ -1178,13 +1178,13 @@ async function _querySvcbLike(host, qtype, opts) {
1178
1178
  throw new DnsError("dns/bad-transport",
1179
1179
  "dns.querySvcb: transport must be 'doh' | 'dot' | 'system' | undefined");
1180
1180
  }
1181
- _emitObs("network.dns.svcb.requested", { qtype: qtype, transport: opts.transport || "auto" });
1181
+ observability().safeEvent("network.dns.svcb.requested", 1, { qtype: qtype, transport: opts.transport || "auto" });
1182
1182
  var startMs = _now();
1183
1183
  var reply;
1184
1184
  try {
1185
1185
  reply = await _rawQuery(host, qtype, opts.transport);
1186
1186
  } catch (e) {
1187
- _emitObs("network.dns.svcb.failure", {
1187
+ observability().safeEvent("network.dns.svcb.failure", 1, {
1188
1188
  latencyMs: _now() - startMs,
1189
1189
  code: e.code || "unknown",
1190
1190
  });
@@ -1198,7 +1198,7 @@ async function _querySvcbLike(host, qtype, opts) {
1198
1198
  records.push(_parseSvcbRdata(decoded.msg, ans.rdataOff, ans.rdlen));
1199
1199
  }
1200
1200
  records.sort(function (a, b) { return a.priority - b.priority; });
1201
- _emitObs("network.dns.svcb.success", {
1201
+ observability().safeEvent("network.dns.svcb.success", 1, {
1202
1202
  latencyMs: _now() - startMs,
1203
1203
  count: records.length,
1204
1204
  qtype: qtype,
@@ -1322,7 +1322,7 @@ async function discoverEncrypted(opts) {
1322
1322
  try {
1323
1323
  records = await _querySvcbLike(name, DNS_QTYPE_SVCB, { transport: transport });
1324
1324
  } catch (e) {
1325
- _emitObs("network.dns.ddr.failure", {
1325
+ observability().safeEvent("network.dns.ddr.failure", 1, {
1326
1326
  latencyMs: _now() - startMs,
1327
1327
  code: e.code || "unknown",
1328
1328
  });
@@ -1333,7 +1333,7 @@ async function discoverEncrypted(opts) {
1333
1333
  throw e;
1334
1334
  }
1335
1335
  if (records.length === 0) {
1336
- _emitObs("network.dns.ddr.empty", { latencyMs: _now() - startMs });
1336
+ observability().safeEvent("network.dns.ddr.empty", 1, { latencyMs: _now() - startMs });
1337
1337
  throw new DnsError("dns/ddr-not-discovered",
1338
1338
  "dns.discoverEncrypted: resolver returned empty DDR record set at " + name);
1339
1339
  }
@@ -1364,7 +1364,7 @@ async function discoverEncrypted(opts) {
1364
1364
  throw new DnsError("dns/ddr-not-discovered",
1365
1365
  "dns.discoverEncrypted: DDR records present but none advertised a recognized transport (alpn=dot/h2/h3)");
1366
1366
  }
1367
- _emitObs("network.dns.ddr.success", {
1367
+ observability().safeEvent("network.dns.ddr.success", 1, {
1368
1368
  latencyMs: _now() - startMs,
1369
1369
  count: resolvers.length,
1370
1370
  });
@@ -1454,7 +1454,7 @@ function useDesignatedResolvers(list) {
1454
1454
  });
1455
1455
  }
1456
1456
  _designatedResolvers = validated.slice();
1457
- _emitObs("network.dns.dnr.set", {
1457
+ observability().safeEvent("network.dns.dnr.set", 1, {
1458
1458
  count: validated.length,
1459
1459
  active: j,
1460
1460
  transport: v.transport,
@@ -1462,7 +1462,7 @@ function useDesignatedResolvers(list) {
1462
1462
  return { active: j, count: validated.length };
1463
1463
  } catch (e) {
1464
1464
  lastErr = e;
1465
- _emitObs("network.dns.dnr.entry_failed", {
1465
+ observability().safeEvent("network.dns.dnr.entry_failed", 1, {
1466
1466
  index: j,
1467
1467
  transport: v.transport,
1468
1468
  code: e.code || "unknown",
@@ -1511,7 +1511,7 @@ async function lookup(host, opts) {
1511
1511
  if (cached.error) throw cached.error;
1512
1512
  return opts.all ? cached.value : cached.value[0];
1513
1513
  }
1514
- _emitObs("network.dns.lookup.requested", { family: cacheKey });
1514
+ observability().safeEvent("network.dns.lookup.requested", 1, { family: cacheKey });
1515
1515
  var startMs = _now();
1516
1516
  // Resolve secure-DNS default on first use. Idempotent.
1517
1517
  _ensureSecureDefault();
@@ -1546,11 +1546,11 @@ async function lookup(host, opts) {
1546
1546
  throw new DnsError("dns/no-result", "dns lookup of '" + host + "' returned no addresses");
1547
1547
  }
1548
1548
  _cachePutPositive(host, cacheKey, normalized);
1549
- _emitObs("network.dns.lookup.success", { latencyMs: _now() - startMs, count: normalized.length });
1549
+ observability().safeEvent("network.dns.lookup.success", 1, { latencyMs: _now() - startMs, count: normalized.length });
1550
1550
  return opts.all ? normalized : normalized[0];
1551
1551
  } catch (e) {
1552
1552
  _cachePutNegative(host, cacheKey, e);
1553
- _emitObs("network.dns.lookup.failure", { latencyMs: _now() - startMs, code: e.code || "unknown" });
1553
+ observability().safeEvent("network.dns.lookup.failure", 1, { latencyMs: _now() - startMs, code: e.code || "unknown" });
1554
1554
  throw e;
1555
1555
  }
1556
1556
  }
@@ -1571,7 +1571,7 @@ async function _resolveProtocol(host, family, opts) {
1571
1571
  throw new DnsError("dns/bad-transport",
1572
1572
  "dns.resolve" + family + ": transport must be 'doh' | 'dot' | 'system' | undefined");
1573
1573
  }
1574
- _emitObs("network.dns.resolve.requested", { family: family, transport: opts.transport || "auto" });
1574
+ observability().safeEvent("network.dns.resolve.requested", 1, { family: family, transport: opts.transport || "auto" });
1575
1575
  var startMs = _now();
1576
1576
  try {
1577
1577
  var addrs;
@@ -1597,10 +1597,10 @@ async function _resolveProtocol(host, family, opts) {
1597
1597
  if (normalized.length === 0) {
1598
1598
  throw new DnsError("dns/no-result", "dns.resolve" + family + " of '" + host + "' returned no addresses");
1599
1599
  }
1600
- _emitObs("network.dns.resolve.success", { family: family, latencyMs: _now() - startMs, count: normalized.length });
1600
+ observability().safeEvent("network.dns.resolve.success", 1, { family: family, latencyMs: _now() - startMs, count: normalized.length });
1601
1601
  return normalized;
1602
1602
  } catch (e) {
1603
- _emitObs("network.dns.resolve.failure", { family: family, latencyMs: _now() - startMs, code: e.code || "unknown" });
1603
+ observability().safeEvent("network.dns.resolve.failure", 1, { family: family, latencyMs: _now() - startMs, code: e.code || "unknown" });
1604
1604
  if (e instanceof DnsError) throw e;
1605
1605
  throw new DnsError("dns/resolve-failed",
1606
1606
  "dns.resolve" + family + " of '" + host + "' failed: " + (e.message || String(e)));
@@ -1643,16 +1643,16 @@ async function reverse(ip) {
1643
1643
  throw new DnsError("dns/bad-ip",
1644
1644
  "dns.reverse: '" + ip + "' is not a valid IPv4 or IPv6 address");
1645
1645
  }
1646
- _emitObs("network.dns.reverse.requested", { family: net.isIPv6(ip) ? 6 : 4 });
1646
+ observability().safeEvent("network.dns.reverse.requested", 1, { family: net.isIPv6(ip) ? 6 : 4 });
1647
1647
  var startMs = _now();
1648
1648
  try {
1649
1649
  var ptrs = await _withTimeout(dnsPromises.reverse(ip), STATE.lookupTimeoutMs, ip);
1650
- _emitObs("network.dns.reverse.success", {
1650
+ observability().safeEvent("network.dns.reverse.success", 1, {
1651
1651
  latencyMs: _now() - startMs, count: Array.isArray(ptrs) ? ptrs.length : 0,
1652
1652
  });
1653
1653
  return Array.isArray(ptrs) ? ptrs : [];
1654
1654
  } catch (e) {
1655
- _emitObs("network.dns.reverse.failure", {
1655
+ observability().safeEvent("network.dns.reverse.failure", 1, {
1656
1656
  latencyMs: _now() - startMs, code: e.code || "unknown",
1657
1657
  });
1658
1658
  if (e instanceof DnsError) throw e;
@@ -1674,10 +1674,6 @@ function nodeLookup(host, options, callback) {
1674
1674
  );
1675
1675
  }
1676
1676
 
1677
- function _emitObs(name, fields) {
1678
- try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
1679
- }
1680
-
1681
1677
  function _stateForTest() { return STATE; }
1682
1678
  function _resetForTest() {
1683
1679
  STATE.servers = null; STATE.resultOrder = null; STATE.family = 0;
@@ -46,6 +46,7 @@
46
46
 
47
47
  var nodeCrypto = require("node:crypto");
48
48
  var bCrypto = require("./crypto");
49
+ var safeBuffer = require("./safe-buffer");
49
50
  var validateOpts = require("./validate-opts");
50
51
  var { defineClass } = require("./framework-error");
51
52
 
@@ -79,11 +80,14 @@ var TYPE_NUM = {
79
80
  SMIMEA: 53, CDS: 59, CDNSKEY: 60, OPENPGPKEY: 61, CAA: 257, HINFO: 13,
80
81
  };
81
82
 
82
- function _bytes(x, what) {
83
- if (Buffer.isBuffer(x)) return x;
84
- if (x instanceof Uint8Array) return Buffer.from(x);
85
- throw new DnssecError("dnssec/bad-bytes", "dnssec: " + what + " must be a Buffer");
86
- }
83
+ // DNSSEC wire data is bytes, never text (allowString:false).
84
+ var _bytes = safeBuffer.makeByteCoercer({
85
+ errorClass: DnssecError,
86
+ typeCode: "dnssec/bad-bytes",
87
+ messagePrefix: "dnssec: ",
88
+ messageSuffix: " must be a Buffer",
89
+ allowString: false,
90
+ });
87
91
 
88
92
  // Canonical wire form of a domain name (RFC 4034 §6.2): each label
89
93
  // length-prefixed, ASCII lowercased, terminated by the root label.
@@ -157,8 +161,11 @@ function _dnskeyToKey(algId, publicKey) {
157
161
  return _jwkKey({ kty: "OKP", crv: "Ed25519", x: pk.toString("base64url") });
158
162
  }
159
163
  function _jwkKey(jwk) {
160
- try { return nodeCrypto.createPublicKey({ key: jwk, format: "jwk" }); }
161
- catch (e) { throw new DnssecError("dnssec/bad-key", "dnssec: could not import DNSKEY: " + ((e && e.message) || e)); }
164
+ return bCrypto.importPublicJwk(jwk, {
165
+ errorClass: DnssecError,
166
+ code: "dnssec/bad-key",
167
+ messagePrefix: "dnssec: could not import DNSKEY: ",
168
+ });
162
169
  }
163
170
 
164
171
  /**
@@ -284,15 +291,8 @@ function verifyRrset(opts) {
284
291
  }
285
292
 
286
293
  // Validity window (fail closed on a bad opts.at).
287
- var atMs;
288
- if (opts.at !== undefined && opts.at !== null) {
289
- if (!(opts.at instanceof Date) || !isFinite(opts.at.getTime())) {
290
- throw new DnssecError("dnssec/bad-at", "dnssec.verifyRrset: opts.at must be a valid Date");
291
- }
292
- atMs = opts.at.getTime();
293
- } else {
294
- atMs = Date.now();
295
- }
294
+ validateOpts.optionalDate(opts.at, "dnssec.verifyRrset: opts.at", DnssecError, "dnssec/bad-at");
295
+ var atMs = (opts.at !== undefined && opts.at !== null) ? opts.at.getTime() : Date.now();
296
296
  var nowSec = Math.floor(atMs / 1000);
297
297
  if (nowSec < (rrsig.inception >>> 0)) throw new DnssecError("dnssec/not-yet-valid", "dnssec.verifyRrset: RRSIG inception is in the future");
298
298
  if (nowSec > (rrsig.expiration >>> 0)) throw new DnssecError("dnssec/expired", "dnssec.verifyRrset: RRSIG has expired");
@@ -5,6 +5,7 @@ var net = require("node:net");
5
5
  var C = require("./constants");
6
6
  var validateOpts = require("./validate-opts");
7
7
  var lazyRequire = require("./lazy-require");
8
+ var boundedMap = require("./bounded-map");
8
9
  var { defineClass } = require("./framework-error");
9
10
 
10
11
  var HeartbeatError = defineClass("HeartbeatError", { alwaysPermanent: true });
@@ -198,9 +199,9 @@ function start(opts) {
198
199
  var started = [];
199
200
  for (var j = 0; j < opts.targets.length; j++) {
200
201
  var t = opts.targets[j];
201
- if (TARGETS.has(t.name)) {
202
+ boundedMap.requireAbsent(TARGETS, t.name, function () {
202
203
  throw new HeartbeatError("heartbeat/duplicate", "heartbeat target '" + t.name + "' already started");
203
- }
204
+ });
204
205
  var entry = {
205
206
  target: t,
206
207
  intervalMs: t.intervalMs || DEFAULT_INTERVAL_MS,
@@ -259,7 +260,7 @@ function statuses() {
259
260
 
260
261
  function _emitObsProbe(entry, result) {
261
262
  try {
262
- observability().emit("network.heartbeat.probe", {
263
+ observability().safeEvent("network.heartbeat.probe", 1, {
263
264
  name: entry.target.name,
264
265
  type: entry.target.type,
265
266
  ok: result.ok,
@@ -381,7 +382,7 @@ function passive(opts) {
381
382
 
382
383
  function _emitObsPong(state) {
383
384
  try {
384
- observability().emit("network.heartbeat.passive.pong", {
385
+ observability().safeEvent("network.heartbeat.passive.pong", 1, {
385
386
  pongCount: state.pongCount,
386
387
  timeoutMs: state.timeoutMs,
387
388
  });
@@ -390,7 +391,7 @@ function _emitObsPong(state) {
390
391
 
391
392
  function _emitObsTimeout(state) {
392
393
  try {
393
- observability().emit("network.heartbeat.passive.timeout", {
394
+ observability().safeEvent("network.heartbeat.passive.timeout", 1, {
394
395
  pongCount: state.pongCount,
395
396
  lastPongMs: state.lastPongMs,
396
397
  timeoutMs: state.timeoutMs,
@@ -70,7 +70,7 @@ function set(opts) {
70
70
  if (opts.no !== undefined) STATE.noProxy = _parseNoProxy(opts.no);
71
71
  if (opts.auth !== undefined) STATE.auth = opts.auth ? _parseAuth(opts.auth) : null;
72
72
  STATE.agentCache.clear();
73
- _emitObs("network.proxy.set", {
73
+ observability().safeEvent("network.proxy.set", 1, {
74
74
  httpSet: !!STATE.http,
75
75
  httpsSet: !!STATE.https,
76
76
  noProxyCount: STATE.noProxy.length,
@@ -93,7 +93,7 @@ function fromEnv(envObj) {
93
93
  if (authEnv) { STATE.auth = _parseAuth(authEnv); changed = true; }
94
94
  if (changed) {
95
95
  STATE.agentCache.clear();
96
- _emitObs("network.proxy.from_env", {
96
+ observability().safeEvent("network.proxy.from_env", 1, {
97
97
  httpSet: !!STATE.http,
98
98
  httpsSet: !!STATE.https,
99
99
  noProxyCount: STATE.noProxy.length,
@@ -255,7 +255,7 @@ function agentFor(targetUrl) {
255
255
  };
256
256
  }
257
257
  STATE.agentCache.set(key, agent);
258
- _emitObs("network.proxy.agent.created", { protocol: u.protocol });
258
+ observability().safeEvent("network.proxy.agent.created", 1, { protocol: u.protocol });
259
259
  return agent;
260
260
  }
261
261
 
@@ -268,10 +268,6 @@ function snapshot() {
268
268
  };
269
269
  }
270
270
 
271
- function _emitObs(name, fields) {
272
- try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
273
- }
274
-
275
271
  function _resetForTest() {
276
272
  STATE.http = null; STATE.https = null; STATE.noProxy = []; STATE.auth = null;
277
273
  STATE.agentCache.clear();
@@ -57,7 +57,9 @@ var nodeCrypto = require("node:crypto");
57
57
  var zlib = require("node:zlib");
58
58
  var asn1 = require("./asn1-der");
59
59
  var lazyRequire = require("./lazy-require");
60
+ var networkDnsResolver = lazyRequire(function () { return require("./network-dns-resolver"); });
60
61
  var validateOpts = require("./validate-opts");
62
+ var structuredFields = require("./structured-fields");
61
63
  var bCrypto = require("./crypto");
62
64
  var safeUrl = require("./safe-url");
63
65
  var safeJson = require("./safe-json");
@@ -91,14 +93,12 @@ function _parseStsPolicy(text) {
91
93
  "MTA-STS policy text is empty");
92
94
  }
93
95
  var policy = { version: null, mode: null, mx: [], max_age: null };
94
- var lines = text.split(/\r?\n/);
95
- for (var i = 0; i < lines.length; i += 1) {
96
- var line = lines[i].trim();
97
- if (line.length === 0) continue;
98
- var colonAt = line.indexOf(":");
99
- if (colonAt === -1) continue;
100
- var key = line.slice(0, colonAt).trim().toLowerCase();
101
- var val = line.slice(colonAt + 1).trim();
96
+ // RFC 8461 §3.2 — newline-separated `key: value` lines, no quoted-string;
97
+ // mx may repeat, so iterate pairs (a last-wins map would drop hosts).
98
+ var pairs = structuredFields.parseTagList(text, { sep: /\r?\n/, kvSep: ":" });
99
+ for (var i = 0; i < pairs.length; i += 1) {
100
+ var key = pairs[i][0];
101
+ var val = pairs[i][1];
102
102
  if (key === "version") policy.version = val;
103
103
  else if (key === "mode") policy.mode = val.toLowerCase();
104
104
  else if (key === "mx") policy.mx.push(val.toLowerCase());
@@ -123,17 +123,13 @@ function _parseStsPolicy(text) {
123
123
  // cached policy forever (defeating operator rotation), and would also
124
124
  // fetch policies from domains that don't publish one.
125
125
  async function _fetchStsTxt(domain, dnsLookup) {
126
- var records;
127
- try {
128
- records = dnsLookup
129
- ? await dnsLookup("_mta-sts." + domain, "TXT")
130
- : await dnsPromises.resolveTxt("_mta-sts." + domain);
131
- } catch (e) {
132
- if (e && (e.code === "ENOTFOUND" || e.code === "ENODATA")) return null;
133
- throw new SmtpPolicyError("smtp/mta-sts-txt-lookup-failed",
134
- "_mta-sts." + domain + " TXT lookup failed: " +
135
- ((e && e.message) || String(e)));
136
- }
126
+ // Secure DNS path — a spoofed _mta-sts TXT could downgrade mail TLS, so this
127
+ // must not use plaintext dnsPromises.
128
+ var records = await networkDnsResolver().safeResolveTxt("_mta-sts." + domain, {
129
+ dnsLookup: dnsLookup,
130
+ errorFactory: function (code, msg) { return new SmtpPolicyError(code, msg); },
131
+ code: "smtp/mta-sts-txt-lookup-failed",
132
+ });
137
133
  if (!Array.isArray(records)) return null;
138
134
  for (var i = 0; i < records.length; i += 1) {
139
135
  var rec = Array.isArray(records[i]) ? records[i].join("") : records[i];
@@ -577,19 +573,14 @@ async function tlsRptFetchPolicy(domain, opts) {
577
573
  }
578
574
  opts = opts || {};
579
575
  var qname = "_smtp._tls." + domain;
580
- var records;
581
- try {
582
- if (opts.dnsLookup) {
583
- records = await opts.dnsLookup(qname, "TXT");
584
- } else {
585
- records = await dnsPromises.resolveTxt(qname);
586
- }
587
- } catch (e) {
588
- if (e && (e.code === "ENOTFOUND" || e.code === "ENODATA")) return null;
589
- throw new SmtpPolicyError("smtp/tls-rpt-lookup-failed",
590
- "TLS-RPT TXT lookup for " + qname + " failed: " +
591
- ((e && e.message) || String(e)));
592
- }
576
+ // Secure DNS path — not plaintext dnsPromises (a spoofed TLS-RPT TXT could
577
+ // redirect failure reports).
578
+ var records = await networkDnsResolver().safeResolveTxt(qname, {
579
+ dnsLookup: opts.dnsLookup,
580
+ errorFactory: function (code, msg) { return new SmtpPolicyError(code, msg); },
581
+ code: "smtp/tls-rpt-lookup-failed",
582
+ });
583
+ if (records === null) return null;
593
584
  // Pick the first record that begins with v=TLSRPTv1 per RFC 8460 §3.
594
585
  var joined = "";
595
586
  for (var i = 0; i < (records || []).length; i += 1) {
@@ -598,16 +589,13 @@ async function tlsRptFetchPolicy(domain, opts) {
598
589
  if (/^v=TLSRPTv1\b/i.test(s)) { joined = s; break; }
599
590
  }
600
591
  if (joined.length === 0) return null;
601
- var parts = joined.split(";"); // allow:bare-split-on-quoted-header — allow:raw-time-literal — TLS-RPT record grammar (RFC 8460 §3): `tlsrpt-record = "v=TLSRPTv1;" *(WSP) tlsrpt-rua` with token-only values; no quoted-string
592
+ // TLS-RPT record grammar (RFC 8460 §3): `"v=TLSRPTv1;" *(WSP) tlsrpt-rua`
593
+ // with token-only values, no quoted-string — the naive split is correct.
594
+ var pairs = structuredFields.parseTagList(joined);
602
595
  var rua = [];
603
- for (var p = 0; p < parts.length; p += 1) {
604
- var t = parts[p].trim();
605
- var eq = t.indexOf("=");
606
- if (eq === -1) continue;
607
- var k = t.slice(0, eq).trim().toLowerCase();
608
- var v = t.slice(eq + 1).trim();
609
- if (k === "rua") {
610
- var uris = v.split(","); // allow:bare-split-on-quoted-header — allow:raw-time-literal — TLS-RPT rua grammar (RFC 8460 §3): rua = tlsrpt-uri *("," tlsrpt-uri); URIs percent-encode reserved chars, no quoted-string
596
+ for (var p = 0; p < pairs.length; p += 1) {
597
+ if (pairs[p][0] === "rua") {
598
+ var uris = pairs[p][1].split(","); // allow:bare-split-on-quoted-header — allow:raw-time-literal — TLS-RPT rua grammar (RFC 8460 §3): rua = tlsrpt-uri *("," tlsrpt-uri); URIs percent-encode reserved chars, no quoted-string
611
599
  for (var u = 0; u < uris.length; u += 1) {
612
600
  var uri = uris[u].trim();
613
601
  if (uri.length > 0) rua.push(uri);