@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.
- package/CHANGELOG.md +4 -0
- package/index.js +2 -0
- package/lib/a2a-tasks.js +7 -23
- package/lib/acme.js +13 -16
- package/lib/agent-event-bus.js +5 -4
- package/lib/agent-idempotency.js +2 -5
- package/lib/agent-orchestrator.js +2 -5
- package/lib/agent-saga.js +3 -5
- package/lib/agent-tenant.js +2 -5
- package/lib/ai-adverse-decision.js +2 -15
- package/lib/ai-capability.js +1 -6
- package/lib/ai-dp.js +1 -5
- package/lib/ai-input.js +2 -2
- package/lib/ai-pref.js +3 -8
- package/lib/ai-quota.js +3 -14
- package/lib/api-key.js +37 -28
- package/lib/archive-adapters.js +2 -4
- package/lib/archive-entry-policy.js +32 -0
- package/lib/archive-read.js +5 -17
- package/lib/archive-tar-read.js +5 -16
- package/lib/archive.js +2 -10
- package/lib/arg-parser.js +7 -6
- package/lib/asyncapi-traits.js +2 -6
- package/lib/atomic-file.js +108 -31
- package/lib/audit-chain.js +133 -53
- package/lib/audit-daily-review.js +24 -14
- package/lib/audit-emit.js +82 -0
- package/lib/audit-sign.js +257 -0
- package/lib/audit.js +84 -0
- package/lib/auth/access-lock.js +5 -27
- package/lib/auth/dpop.js +23 -35
- package/lib/auth/fido-mds3.js +9 -15
- package/lib/auth/jwt-external.js +26 -8
- package/lib/auth/jwt.js +13 -15
- package/lib/auth/lockout.js +6 -25
- package/lib/auth/oauth.js +67 -45
- package/lib/auth/oid4vci.js +55 -32
- package/lib/auth/oid4vp.js +3 -2
- package/lib/auth/openid-federation.js +6 -6
- package/lib/auth/passkey.js +3 -3
- package/lib/auth/password.js +7 -1
- package/lib/auth/saml.js +37 -27
- package/lib/auth/sd-jwt-vc-holder.js +2 -14
- package/lib/auth/sd-jwt-vc-issuer.js +2 -14
- package/lib/auth/sd-jwt-vc.js +1 -1
- package/lib/auth/status-list.js +7 -7
- package/lib/auth/step-up.js +6 -12
- package/lib/auth-bot-challenge.js +6 -37
- package/lib/backup/bundle.js +11 -18
- package/lib/backup/index.js +14 -47
- package/lib/bounded-map.js +112 -1
- package/lib/breach-deadline.js +1 -11
- package/lib/cache.js +7 -18
- package/lib/cbor.js +2 -1
- package/lib/cdn-cache-control.js +8 -9
- package/lib/cert.js +3 -10
- package/lib/chain-writer.js +162 -47
- package/lib/cli.js +5 -1
- package/lib/client-hints.js +10 -10
- package/lib/cloud-events.js +40 -31
- package/lib/cluster-storage.js +2 -38
- package/lib/cluster.js +4 -2
- package/lib/cms-codec.js +2 -1
- package/lib/codepoint-class.js +67 -1
- package/lib/compliance-ai-act-logging.js +1 -6
- package/lib/compliance-ai-act-transparency.js +2 -4
- package/lib/compliance-eaa.js +1 -11
- package/lib/compliance-sanctions-fetcher.js +21 -28
- package/lib/compliance-sanctions.js +2 -14
- package/lib/compliance.js +2 -9
- package/lib/config-drift.js +2 -12
- package/lib/content-digest.js +10 -8
- package/lib/cookies.js +5 -11
- package/lib/cose.js +19 -11
- package/lib/cra-report.js +1 -11
- package/lib/crypto-field.js +5 -10
- package/lib/crypto.js +120 -3
- package/lib/csp.js +235 -3
- package/lib/daemon.js +42 -41
- package/lib/data-act.js +19 -9
- package/lib/db-query.js +6 -40
- package/lib/db.js +34 -12
- package/lib/dbsc.js +5 -6
- package/lib/ddl-change-control.js +18 -14
- package/lib/deprecate.js +4 -5
- package/lib/did.js +6 -2
- package/lib/dsr.js +8 -12
- package/lib/external-db-migrate.js +21 -22
- package/lib/external-db.js +14 -26
- package/lib/fda-21cfr11.js +12 -8
- package/lib/fdx.js +22 -18
- package/lib/file-upload.js +80 -66
- package/lib/flag-evaluation-context.js +5 -8
- package/lib/framework-error.js +12 -0
- package/lib/framework-schema.js +19 -0
- package/lib/fsm.js +7 -12
- package/lib/gate-contract.js +869 -38
- package/lib/gdpr-ropa.js +4 -13
- package/lib/graphql-federation.js +1 -1
- package/lib/guard-agent-registry.js +2 -1
- package/lib/guard-all.js +1 -0
- package/lib/guard-archive.js +9 -30
- package/lib/guard-auth.js +23 -80
- package/lib/guard-cidr.js +20 -96
- package/lib/guard-csv.js +135 -196
- package/lib/guard-domain.js +23 -106
- package/lib/guard-dsn.js +16 -13
- package/lib/guard-email.js +46 -53
- package/lib/guard-envelope.js +1 -1
- package/lib/guard-event-bus-topic.js +2 -1
- package/lib/guard-filename.js +18 -62
- package/lib/guard-graphql.js +28 -75
- package/lib/guard-html-wcag.js +15 -2
- package/lib/guard-html.js +65 -117
- package/lib/guard-idempotency-key.js +2 -1
- package/lib/guard-image.js +280 -77
- package/lib/guard-imap-command.js +8 -9
- package/lib/guard-json.js +87 -103
- package/lib/guard-jsonpath.js +20 -88
- package/lib/guard-jwt.js +32 -114
- package/lib/guard-list-id.js +2 -7
- package/lib/guard-list-unsubscribe.js +2 -7
- package/lib/guard-mail-compose.js +5 -6
- package/lib/guard-mail-move.js +2 -1
- package/lib/guard-mail-query.js +5 -3
- package/lib/guard-mail-sieve.js +6 -7
- package/lib/guard-managesieve-command.js +5 -4
- package/lib/guard-markdown.js +76 -110
- package/lib/guard-message-id.js +5 -6
- package/lib/guard-mime.js +20 -99
- package/lib/guard-oauth.js +19 -73
- package/lib/guard-pdf.js +65 -72
- package/lib/guard-pop3-command.js +12 -13
- package/lib/guard-posture-chain.js +2 -1
- package/lib/guard-regex.js +24 -99
- package/lib/guard-saga-config.js +2 -1
- package/lib/guard-shell.js +22 -87
- package/lib/guard-smtp-command.js +8 -11
- package/lib/guard-sql.js +15 -13
- package/lib/guard-stream-args.js +2 -1
- package/lib/guard-svg.js +95 -140
- package/lib/guard-template.js +23 -80
- package/lib/guard-tenant-id.js +2 -1
- package/lib/guard-text.js +592 -0
- package/lib/guard-time.js +27 -95
- package/lib/guard-uuid.js +21 -93
- package/lib/guard-xml.js +76 -106
- package/lib/guard-yaml.js +24 -60
- package/lib/http-client-cache.js +8 -13
- package/lib/http-client-cookie-jar.js +2 -4
- package/lib/http-client.js +8 -21
- package/lib/http-message-signature.js +26 -8
- package/lib/i18n-messageformat.js +5 -7
- package/lib/i18n.js +83 -26
- package/lib/inbox.js +8 -8
- package/lib/incident-report.js +3 -21
- package/lib/ip-utils.js +49 -6
- package/lib/jobs.js +3 -2
- package/lib/keychain.js +6 -18
- package/lib/legal-hold.js +6 -15
- package/lib/log-stream-cloudwatch.js +44 -112
- package/lib/log-stream-otlp-grpc.js +28 -14
- package/lib/log-stream-otlp.js +16 -80
- package/lib/log-stream-syslog.js +6 -0
- package/lib/log-stream-webhook.js +20 -92
- package/lib/log.js +24 -2
- package/lib/mail-arc-sign.js +8 -3
- package/lib/mail-arf.js +3 -2
- package/lib/mail-auth.js +40 -66
- package/lib/mail-bimi.js +19 -39
- package/lib/mail-crypto-pgp.js +3 -2
- package/lib/mail-dav.js +8 -35
- package/lib/mail-deploy.js +6 -9
- package/lib/mail-dkim.js +19 -38
- package/lib/mail-greylist.js +15 -26
- package/lib/mail-helo.js +2 -3
- package/lib/mail-journal.js +2 -1
- package/lib/mail-mdn.js +4 -3
- package/lib/mail-rbl.js +5 -8
- package/lib/mail-scan.js +2 -2
- package/lib/mail-send-deliver.js +33 -20
- package/lib/mail-server-imap.js +32 -87
- package/lib/mail-server-jmap.js +35 -51
- package/lib/mail-server-managesieve.js +29 -83
- package/lib/mail-server-mx.js +33 -74
- package/lib/mail-server-net.js +177 -0
- package/lib/mail-server-pop3.js +30 -83
- package/lib/mail-server-rate-limit.js +30 -6
- package/lib/mail-server-submission.js +34 -73
- package/lib/mail-server-tls.js +89 -0
- package/lib/mail-spam-score.js +7 -8
- package/lib/mail-store.js +3 -2
- package/lib/mail.js +11 -11
- package/lib/markup-escape.js +31 -0
- package/lib/markup-tokenizer.js +54 -0
- package/lib/mcp-tool-registry.js +26 -23
- package/lib/mcp.js +1 -1
- package/lib/mdoc.js +11 -12
- package/lib/metrics.js +32 -30
- package/lib/middleware/age-gate.js +3 -23
- package/lib/middleware/api-encrypt.js +11 -22
- package/lib/middleware/assetlinks.js +2 -7
- package/lib/middleware/bearer-auth.js +2 -1
- package/lib/middleware/body-parser.js +79 -54
- package/lib/middleware/bot-disclose.js +7 -10
- package/lib/middleware/bot-guard.js +2 -10
- package/lib/middleware/csrf-protect.js +13 -2
- package/lib/middleware/daily-byte-quota.js +6 -26
- package/lib/middleware/deny-response.js +19 -1
- package/lib/middleware/fetch-metadata.js +7 -0
- package/lib/middleware/idempotency-key.js +4 -20
- package/lib/middleware/rate-limit.js +20 -28
- package/lib/middleware/scim-server.js +3 -2
- package/lib/middleware/security-headers.js +9 -1
- package/lib/middleware/security-txt.js +2 -6
- package/lib/middleware/tus-upload.js +12 -27
- package/lib/middleware/web-app-manifest.js +2 -7
- package/lib/migrations.js +4 -13
- package/lib/mime-parse.js +34 -17
- package/lib/module-loader.js +44 -0
- package/lib/money.js +105 -0
- package/lib/mtls-ca.js +116 -36
- package/lib/network-byte-quota.js +4 -18
- package/lib/network-dane.js +8 -6
- package/lib/network-dns-resolver.js +97 -6
- package/lib/network-dns.js +22 -26
- package/lib/network-dnssec.js +16 -16
- package/lib/network-heartbeat.js +6 -5
- package/lib/network-proxy.js +3 -7
- package/lib/network-smtp-policy.js +29 -41
- package/lib/network-tls.js +74 -55
- package/lib/network.js +2 -6
- package/lib/nis2-report.js +1 -11
- package/lib/nonce-store.js +81 -3
- package/lib/notify.js +7 -12
- package/lib/numeric-bounds.js +22 -8
- package/lib/object-store/azure-blob-bucket-ops.js +2 -6
- package/lib/object-store/azure-blob.js +5 -45
- package/lib/object-store/gcs.js +8 -71
- package/lib/object-store/http-put.js +1 -5
- package/lib/object-store/http-request.js +128 -0
- package/lib/object-store/sigv4-bucket-ops.js +2 -1
- package/lib/object-store/sigv4.js +9 -77
- package/lib/observability-otlp-exporter.js +9 -25
- package/lib/observability.js +95 -0
- package/lib/openapi-paths-builder.js +7 -3
- package/lib/otel-export.js +7 -10
- package/lib/outbox.js +43 -37
- package/lib/pagination.js +11 -15
- package/lib/parsers/safe-env.js +5 -4
- package/lib/parsers/safe-ini.js +10 -4
- package/lib/parsers/safe-toml.js +11 -9
- package/lib/parsers/safe-xml.js +2 -2
- package/lib/parsers/safe-yaml.js +7 -6
- package/lib/pick.js +63 -5
- package/lib/pipl-cn.js +69 -59
- package/lib/privacy-pass.js +8 -6
- package/lib/problem-details.js +2 -5
- package/lib/pubsub.js +4 -8
- package/lib/redact.js +2 -1
- package/lib/render.js +4 -2
- package/lib/request-helpers.js +133 -6
- package/lib/restore.js +5 -22
- package/lib/retention.js +3 -5
- package/lib/safe-async.js +457 -0
- package/lib/safe-buffer.js +137 -6
- package/lib/safe-decompress.js +2 -1
- package/lib/safe-dns.js +2 -1
- package/lib/safe-ical.js +12 -26
- package/lib/safe-json.js +7 -8
- package/lib/safe-jsonpath.js +6 -5
- package/lib/safe-mime.js +25 -29
- package/lib/safe-redirect.js +2 -5
- package/lib/safe-schema.js +7 -6
- package/lib/safe-sql.js +126 -0
- package/lib/safe-vcard.js +12 -26
- package/lib/sandbox-worker.js +9 -2
- package/lib/sandbox.js +3 -2
- package/lib/scheduler.js +5 -12
- package/lib/sec-cyber.js +82 -37
- package/lib/seeders.js +9 -21
- package/lib/self-update.js +92 -69
- package/lib/session-device-binding.js +112 -66
- package/lib/session.js +194 -6
- package/lib/sql.js +225 -78
- package/lib/sse.js +6 -10
- package/lib/static.js +136 -98
- package/lib/storage.js +4 -2
- package/lib/structured-fields.js +313 -17
- package/lib/tenant-quota.js +2 -20
- package/lib/tsa.js +11 -15
- package/lib/validate-opts.js +154 -8
- package/lib/vault/seal-pem-file.js +30 -48
- package/lib/vault-aad.js +3 -2
- package/lib/vc.js +2 -7
- package/lib/vex.js +35 -13
- package/lib/web-push-vapid.js +23 -20
- package/lib/webhook-dispatcher.js +706 -0
- package/lib/webhook.js +43 -18
- package/lib/websocket-channels.js +9 -7
- package/lib/websocket.js +7 -3
- package/lib/worm.js +2 -3
- package/lib/ws-client.js +8 -10
- package/package.json +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 }
|
package/lib/network-dane.js
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/lib/network-dns.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/lib/network-dnssec.js
CHANGED
|
@@ -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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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");
|
package/lib/network-heartbeat.js
CHANGED
|
@@ -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
|
-
|
|
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().
|
|
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().
|
|
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().
|
|
394
|
+
observability().safeEvent("network.heartbeat.passive.timeout", 1, {
|
|
394
395
|
pongCount: state.pongCount,
|
|
395
396
|
lastPongMs: state.lastPongMs,
|
|
396
397
|
timeoutMs: state.timeoutMs,
|
package/lib/network-proxy.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
var
|
|
99
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
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 <
|
|
604
|
-
|
|
605
|
-
|
|
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);
|