Network
Framework network helpers — DNS-over-HTTPS dispatch, TLS configuration, OCSP/CT validation, NTP/NTS-KE bootstrap.
b.network is the umbrella facade over the framework's outbound- network surface: DNS (default DoH on, optional DoT, lookup cache with TTL bound), TLS trust store (CA bundle / system trust / ignored-cert opt-in), proxy resolution from HTTP_PROXY / HTTPS_PROXY / NO_PROXY, NTP / NTS-KE drift checks, SMTP policy (MTA-STS / DANE / TLS-RPT), heartbeat watchdog, byte quota, SSRF allowlist, and socket-level defaults (TCP_NODELAY / SO_KEEPALIVE).
bootFromEnv reads BLAMEJS_* environment variables once at startup and applies the union to the live facade — operators wire it from a process-supervisor's env without touching code. snapshot returns a redacted view of the current configuration for the operations dashboard. applyToSocket is the per-socket tuning hook for primitives building their own server (tls, wsServer, etc.).
b.network.dns.dane.matchCertificate(opts) #
{
{
tlsa: [ { usage, selector, matchingType, data: Buffer|hex } ], // the TLSA RRset
certificate: Buffer, // leaf certificate (DER)
chain?: Buffer[], // intermediate / CA certs (DER), for TA usages
}
}
Match a server certificate against a set of (DNSSEC-verified) TLSA records (RFC 6698 / 7671). For each record the selected data — the full certificate DER (selector 0) or its subjectPublicKeyInfo (selector 1) — is hashed per the matching type (exact / SHA-256 / SHA-512) and compared, constant-time, to the record's association data. End-entity usages (PKIX-EE 1, DANE-EE 3) are matched against the leaf certificate; trust-anchor usages (PKIX-TA 0, DANE-TA 2) are matched against the leaf and any supplied chain.
Returns the matching record plus what the caller must still do: a DANE-EE match is self-sufficient (the TLSA pins the key); a DANE-TA match still needs chain-to-anchor verification; PKIX usages still need full PKIX validation. Throws dane/no-match if nothing matches. Verify the TLSA RRset with b.network.dns.dnssec before trusting the records — an unauthenticated TLSA proves nothing.
var r = b.network.dns.dane.matchCertificate({ tlsa: records, certificate: leafDer });
// → { ok: true, matched: { usage: 3, selector: 1, matchingType: 1 }, daneAuthenticated: true, pkixRequired: false }
b.network.dns.resolver.create(opts?) #
{
profile: "strict" | "balanced" | "permissive",
posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
maxTtlMs: number, // cap any TTL from upstream; default 24h
minTtlMs: number, // floor short-TTL records; default 60s
serveStale: number | false, // ms to retain expired entries; default 6h
timeoutMs: number, // per-query upstream deadline; default 10s
transport: { lookup(name, qtype) → Promise }, // operator override
audit: b.audit namespace,
}
Build a resolver instance with the given options. Returns an instance with .query(name, type, opts) → Promise<{ rrs, ttl, fromCache, stale, validated, response }> plus per-type shortcuts.
var resolver = b.network.dns.resolver.create({ profile: "strict" });
var r = await resolver.queryTxt("_dmarc.example.com");
console.log(r.rrs.map(function (rr) { return rr.decoded.join(""); }));
b.network.dns.resolver.followCnames(name, type, opts?) #
{
validate: boolean, // per-call: refuse if upstream AD=0
}
Walk CNAME redirections until the target record arrives or the chain depth cap from b.safeDns trips. Returns the same shape as query() plus chain: [name, name, ...] listing each hop.
var r = await resolver.followCnames("alias.example.com", "A");
console.log(r.chain, r.rrs.map(function (rr) { return rr.decoded; }));
b.network.dns.resolver.resolveTxt(qname, dnsLookup?, resolver?) #
Resolve TXT records for qname via the operator's dnsLookup(qname, "TXT") override when supplied, otherwise the framework's shared validating (DoH / DoT / system, DNSSEC-checked) resolver — normalized to the string[][] shape dnsPromises.resolveTxt returns. Throws an ENODATA Error when no TXT records exist.
This is the secure DNS path for every email-auth policy lookup (DMARC / DKIM / ARC / BIMI / MTA-STS / TLS-RPT). Plaintext system DNS (dnsPromises.resolveTxt) is spoofable — DNS cache-poisoning / Kaminsky-class — and must NOT be used for records a downgrade or redirect would exploit.
Pass resolver (a b.network.dns.resolver.create() instance) to reshape TXT records off a caller-owned resolver instead of the shared one — the mail-auth and mail-dkim modules use this so their TXT lookups share the same resolver (and cache) they use for A / MX / PTR, instead of each re-rolling the reshape.
// requires: outbound DNS (DoH/DoT) to resolve the name below
var rrs = await b.network.dns.resolver.resolveTxt("_dmarc.example.com");
// → [ ["v=DMARC1; p=reject"] ]
b.network.dns.resolver.safeResolveTxt(qname, opts?) #
{
dnsLookup: function, // operator override (qname, "TXT") => string[][]
errorFactory: function, // (code, message) => Error, thrown on non-absence failures
code: string, // error code passed to errorFactory
}
resolveTxt wrapped with the email-auth policy-fetch convention: a domain with no record (ENOTFOUND / ENODATA) returns null (absence is not an error); any other failure throws the caller's typed error via errorFactory(code, message) so each protocol keeps its own error class. The same secure resolver path as resolveTxt — never plaintext system DNS.
// requires: outbound DNS (DoH/DoT) to resolve the name below
var rrs = await b.network.dns.resolver.safeResolveTxt("_mta-sts.example.com", {
errorFactory: function (code, msg) { return new SmtpPolicyError(code, msg); },
code: "smtp/mta-sts-txt-lookup-failed",
});
if (rrs === null) return null; // no MTA-STS record published
b.network.dns.querySvcb(name, opts?) #
{
{
transport: "doh" | "dot" | "system",
}
}
Query SVCB records (RFC 9460 §2) for name. Returns an array of { priority, target, params } records sorted by priority. AliasMode records (priority === 0) carry a target and empty params — the caller chases the alias by re-querying the target. ServiceMode records (priority > 0) carry SvcParams: alpn / port / ipv4hint / ipv6hint / ech / mandatory / dohpath. Unknown SvcParamKeys surface under params.unknown[key] as raw bytes — operators implementing forward-compat can still read them. Malformed rdata throws DnsError with code dns/svcb-malformed.
Resolving the query raises every refusal that step can make: dns/bad-host for a name that is not host-shaped, dns/bad-transport for a transport outside the three above, dns/transport-unavailable when the chosen transport is not configured, dns/no-system-resolvers when the system has none, dns/bad-reply for a reply it cannot parse, and dns/no-result when the name resolves to nothing. dns/unsupported-type, dns/wrong-family and dns/resolve-failed are raised by b.network.dns.resolve and by the protocol resolver, neither of which this query goes through.
The transport rejects rather than throwing, so these arrive as a rejected promise: dns/doh-failed for a DoH request that errors, dns/doh-http for a non-2xx DoH reply, dns/doh-too-large for a DoH body over 256 KiB, dns/dot-handshake-failed and dns/dot-failed for a DoT session that cannot start or that errors, dns/system-failed for a system-resolver query that errors, and dns/lookup-timeout when the lookup exceeds its budget.
// requires: outbound DNS to resolve the name below
var b = require("@blamejs/core");
var rrs = await b.network.dns.querySvcb("_443._wss.example.com");
b.network.dns.queryHttps(name, opts?) #
{
{
transport: "doh" | "dot" | "system",
}
}
Query HTTPS records (RFC 9460 §9). Identical to querySvcb except the QTYPE is HTTPS (65) — the user-agent-facing variant of SVCB for https:// origins. Browsers query this for ECH discovery and h3 advertisement; servers can call it to validate their own published HTTPS RRset. Returns the same shape as querySvcb.
// requires: outbound DNS to resolve the name below
var b = require("@blamejs/core");
var rrs = await b.network.dns.queryHttps("example.com");
b.network.dns.discoverEncrypted(opts?) #
{
{
name: string,
insecureSystemResolverOnly: boolean,
}
}
RFC 9462 Discovery of Designated Resolvers. Queries _dns.resolver.arpa for SVCB records that advertise encrypted DNS alternatives (DoH / DoT) hosted by the network's currently-configured Do53 resolver. Returns a list of resolver descriptors with { transport, alpn, target, port, dohpath, ipv4hint, ipv6hint, priority }.
The discovery query goes through the system resolver by default (RFC 9462 §4 — DDR validation requires the response to come from the Do53 resolver whose IP we compare). Callers that already have a trusted DoH / DoT transport configured can pass { insecureSystemResolverOnly: false } to allow DDR via the encrypted transport too.
Throws DnsError with code dns/ddr-not-discovered when the resolver does not publish DDR records, dns/bad-host when name is not a non-empty host-shaped string, and dns/bad-opt when insecureSystemResolverOnly is not a boolean, so a caller that meant to widen the discovery is refused rather than kept on the narrow path.
The underlying SVCB query raises its own refusals through this call: dns/bad-transport, dns/transport-unavailable, dns/no-system-resolvers, dns/bad-reply and dns/svcb-malformed. Its dns/no-result does NOT reach the caller: a resolver that publishes nothing is the dns/ddr-not-discovered case, and this primitive translates it. dns/unsupported-type, dns/wrong-family and dns/resolve-failed are raised by b.network.dns.resolve and by the protocol resolver, neither of which this call goes through.
The transport's own failures arrive as a rejected promise here too: dns/doh-failed, dns/doh-http, dns/doh-too-large, dns/dot-handshake-failed, dns/dot-failed, dns/system-failed and dns/lookup-timeout, each as described on b.network.dns.querySvcb.
var b = require("@blamejs/core");
var resolvers = await b.network.dns.discoverEncrypted();
b.network.dns.useDesignatedResolvers(list) #
RFC 9463 Discovery of Network-designated Resolvers. The framework doesn't run a DHCP / IPv6 RA client itself; an operator-side agent (or the output of discoverEncrypted()) supplies the resolver list and the framework swaps its transport over to the lowest-priority entry. Items are tried in order: the first one that successfully configures (DoH useDnsOverHttps, DoT useDnsOverTls) wins.
Each entry shape:
{ transport: "doh" | "dot", url: string, host: string, port: number, servername: string, alpn: Array
Throws DnsError with code dns/dnr-no-resolvers if list is empty, and dns/dnr-malformed if an entry is missing its required transport-specific fields.
Each accepted entry is then configured as a resolver, which validates it: dns/bad-doh-url, dns/bad-doh-method, dns/bad-doh-provider and dns/bad-doh-ca for a DoH entry, and dns/bad-dot-host, dns/bad-dot-port and dns/bad-dot-ca for a DoT entry. A list that is well-formed but whose entries do not configure is refused rather than partially applied.
var b = require("@blamejs/core");
var found = await b.network.dns.discoverEncrypted();
b.network.dns.useDesignatedResolvers(found.map(function (r) {
return r.transport === "doh"
? { transport: "doh", url: "https://" + r.target + (r.dohpath || "/dns-query") }
: { transport: "dot", host: r.target, port: r.port, servername: r.target };
}));
b.network.dns.isNullMx(mxRecords) #
RFC 7505 Null-MX check — returns true when the supplied MX records signal "this domain does not accept email" (a single MX record with priority 0 and exchange .). Operators sending mail call this before delivery to skip domains that have explicitly opted out of email. Returns false for any other shape (zero records, multiple records, non-zero priority, non-. exchange).
MX records are expected in the { priority, exchange } shape returned by node:dns.resolveMx (or b.network.dns.resolve(host, "MX")). Operator supplies the records; this is a pure classifier, no network call.
var node = require("node:dns/promises");
var mx;
try { mx = await node.resolveMx("example.com"); }
catch (e) { mx = []; }
var acceptsMail = !b.network.dns.isNullMx(mx);
// → false when the domain publishes Null-MX (RFC 7505): refuse to send
// rather than queue mail that can never be delivered
b.network.dns.classifyDnskeyAlgorithm(algorithm) #
Classify a DNSKEY / RRSIG algorithm number against the IANA DNS Security Algorithm Numbers registry, flagging SHA-1-based and other deprecated algorithms per RFC 9905 (Deprecating DNSSEC SHA-1 Usage), RFC 9904 (Algorithm Implementation Requirements, which obsoletes RFC 8624), and RFC 6725 (RSAMD5 deprecation).
deprecated answers "should this still be accepted", so it follows the VALIDATION requirement. Where a spelling differs between signing and validation — RSASHA512 is NOT RECOMMENDED to sign with but MUST still validate — the reason names both, since one boolean cannot.
Returns { algorithm, name, deprecated, reason, known } for any IANA-assigned number; known: false for unassigned numbers (operators decide whether unassigned == deprecated for their threat model). Returns null for non-integer / non-finite input.
Operators auditing inbound DNSSEC chain-of-trust evidence call this on each link's algorithm number and refuse the validation when deprecated === true. Defensive request-shape reader — never throws.
var v = b.network.dns.classifyDnskeyAlgorithm(5);
// → { algorithm: 5, name: "RSASHA1", deprecated: true,
// reason: "SHA-1 deprecated (RFC 9905 §3)", known: true }
var refused = v && v.deprecated ? "refuse DNSSEC algo " + v.name : null;
// → "refuse DNSSEC algo RSASHA1" — the classifier reports, the caller decides
b.network.dns.classifyDnskeyAlgorithm(13);
// → { algorithm: 13, name: "ECDSAP256SHA256", deprecated: false, ... }
b.network.dns.classifyDsDigestType(digestType) #
Classify a DS-record digest type against the IANA DNSSEC Delegation Signer (DS) Resource Record (RR) Type Digest Algorithms registry, flagging SHA-1 (digest type 1) as deprecated per RFC 9905 §4.
Returns { digestType, name, deprecated, reason, known } for any IANA-assigned number; null for non-integer input.
var v = b.network.dns.classifyDsDigestType(1);
// → { digestType: 1, name: "SHA-1", deprecated: true,
// reason: "SHA-1 deprecated (RFC 9905 §4)", known: true }
b.network.dns.classifyDsDigestType(2);
// → { digestType: 2, name: "SHA-256", deprecated: false, ... }
b.network.dns.dnssec.keyTag(dnskeyRdata) #
Compute the RFC 4034 Appendix B key tag of a DNSKEY from its full RDATA (flags || protocol || algorithm || public key) — the 16-bit identifier an RRSIG / DS references to select the signing key.
var tag = b.network.dns.dnssec.keyTag(dnskeyRdata);
b.network.dns.dnssec.verifyDs(opts) #
{
{
ownerName: string, // the child zone name (the DNSKEY owner)
dnskeyRdata: Buffer, // full DNSKEY RDATA (flags||protocol||alg||publicKey)
ds: { keyTag, algorithm, digestType, digest: Buffer }, // the parent DS
}
}
Verify a DS (Delegation Signer) record against a child DNSKEY — the link that lets a parent zone vouch for a child's key. The DS digest (SHA-256 / SHA-384) is recomputed over the owner name plus the DNSKEY RDATA and compared to the DS, with the key tag and algorithm checked.
b.network.dns.dnssec.verifyDs({ ownerName: "example.com", dnskeyRdata: ksk, ds: parentDs });
b.network.dns.dnssec.verifyRrset(opts) #
{
{
name: string, // owner name of the RRset
type: string|number, // RR type (e.g. "DNSKEY", "A")
class?: number, // default 1 (IN)
rdatas: Buffer[], // each record's wire-format RDATA
rrsig: { // the RRSIG covering the RRset
algorithm, labels, originalTtl, expiration, inception, keyTag,
signerName: string, signature: Buffer,
},
dnskey: { algorithm, publicKey: Buffer }, // the signing DNSKEY (publicKey = bytes after flags/proto/alg)
at?: Date, // validity instant (default now); must be a valid Date
}
}
Verify an RRSIG over an RRset against a DNSKEY (RFC 4035 §5.3). The signed data is reconstructed in canonical form — the RRSIG RDATA without the signature, then the RRset's records ordered by canonical RDATA with the RRSIG Original TTL — and the signature is verified with the DNSKEY (RSA/SHA-256, ECDSA P-256/384, Ed25519). The signature's inception / expiration window is enforced against opts.at. RR types carrying embedded domain names are refused (dnssec/uncanonicalizable-type) rather than mis-validated.
b.network.dns.dnssec.verifyRrset({ name: "example.com", type: "DNSKEY", rdatas: keys, rrsig: sig, dnskey: ksk });
b.network.dns.dnssec.nsec3Hash(name, opts) #
{
{
salt: Buffer, // zone NSEC3 salt (may be empty)
iterations: number, // additional hash iterations (>= 0)
}
}
Compute the RFC 5155 §5 NSEC3 hash of a name — iterated SHA-1 over the canonical (lowercased, root-terminated) wire form with the zone's salt. The result is the unencoded hash; the NSEC3 owner label is its base32hex encoding. SHA-1 is the only hash IANA registers for NSEC3, so this is a wire-protocol constant, not a cryptographic default.
var h = b.network.dns.dnssec.nsec3Hash("a.example.com", { salt: salt, iterations: 0 });
b.network.dns.dnssec.verifyDenial(opts) #
{
{
qname: string, // the queried name
qtype: string|number, // queried type (required for proof "nodata")
proof: string, // "nxdomain" | "nodata"
zone: string, // the signer zone apex (a suffix of qname)
nsec3?: [ { owner: string, rdata: Buffer } ], // NSEC3 records (owner = base32hex-label.zone)
nsec?: [ { owner: string, rdata: Buffer } ], // NSEC records
maxIterations?: number, // NSEC3 iteration cap (default 500)
allowOptOut?: boolean, // accept an Opt-Out NXDOMAIN proof (default false)
}
}
Prove that a name does not exist (NXDOMAIN) or that a name has no records of a given type (NODATA) from the signed NSEC (RFC 4034 §4) or NSEC3 (RFC 5155) records in a response's Authority section. This is the other half of "verify the answer yourself": verifyRrset proves a positive answer, verifyDenial proves a negative.
The records MUST already be verified with verifyRrset — this checks the denial RELATION (closest-encloser, covering ranges, type-bitmap absence), not the signatures. For NSEC3, the iterated-hash count is capped (opts.maxIterations, default 500) to bound the SHA-1 work an attacker can force. An NXDOMAIN proof that relies on an Opt-Out NSEC3 (RFC 5155 §6) is refused unless opts.allowOptOut — opt-out only proves "no signed records", not non-existence.
b.network.dns.dnssec.verifyDenial({
qname: "nope.example.com", proof: "nxdomain", zone: "example.com", nsec3: records,
});
b.network.dns.dnssec.verifyChain(opts) #
{
{
links: [ { // ordered root-first
zone: string,
dnskeys: Buffer[], // the zone's DNSKEY RRset RDATAs
dnskeyRrsig: { algorithm, labels, originalTtl, expiration, inception, keyTag, signerName, signature },
dsRdatas?: Buffer[], // DS RRset for this zone (served by parent; omit for root)
dsRrsig?: { ... }, // RRSIG over the DS RRset (signed by parent; omit for root)
} ],
trustAnchors?: [ { keyTag, algorithm, digestType, digest: Buffer } ], // default IANA root
at?: Date, // validity instant (default now)
}
}
Validate a DNSSEC delegation chain from the root down to a zone, against a pinned trust anchor (RFC 4035 §5). For each link, the zone's DNSKEY RRset must be self-signed by one of its keys; that signing key must be vouched for either by a pinned anchor (root) or by a DS record served by the already-trusted parent. The DS RRset itself is verified against the parent's keys, so trust flows root → TLD → zone with no gap. The default anchors are the IANA root KSKs; override with opts.trustAnchors.
This composes verifyRrset + verifyDs + the key tag; it returns the leaf zone's trusted DNSKEY set, which the caller then passes to verifyRrset / verifyDenial for the actual answer.
KeyTrap (CVE-2023-50387) amplification is bounded with non-configurable caps: at most 4 same-tag DNSKEY candidates are tried per RRSIG, at most 64 DNSKEYs per zone link and 16 DS records per delegation are accepted, the chain is at most 128 links deep, and the whole response is held to a signature-validation budget that scales with chain depth (so a legitimate deep delegation always fits while bounded collisions stay bounded). A hostile zone publishing many colliding keys / signatures is refused with dnssec/too-many-colliding-keys / dnssec/too-many-dnskeys / dnssec/too-many-ds / dnssec/too-many-links / dnssec/validation-budget-exceeded rather than driving O(keys x sigs) verifications. (NSEC3 iteration counts are separately capped at 150 per RFC 9276 / the CVE-2023-50868 fix.)
var trusted = b.network.dns.dnssec.verifyChain({ links: [rootLink, orgLink] });
// → { ok: true, zone: "org.", keys: [ ...trusted org DNSKEY rdatas ] }
b.network.tls.certificateCompressionAlgorithms() #
The RFC 8879 certificate-compression algorithms this runtime can decompress, newest-runtime order, as a fresh array you own.
Certificate compression shrinks the TLS Certificate message — on a post-quantum deployment the largest thing on the wire during a handshake, since an ML-DSA-87 signature alone runs about 4.6 KB before any public key or chain. Every outbound path the framework ships already advertises this list, and b.router advertises it inbound; call this directly only when you are assembling tls.connect options yourself and want to narrow the list or check what the runtime supports.
Compressing the certificate is not the record-layer compression that CRIME attacked: the Certificate message is public, fixed, and not attacker-influenced, so its compressed length reveals nothing about a secret and no attacker-chosen plaintext shares a compression context with one.
Returns an empty array on a runtime that does not implement the extension, which reads naturally as "advertise nothing".
b.network.tls.certificateCompressionAlgorithms();
// -> ["zlib", "brotli", "zstd"]
// Narrow what an outbound connection will accept.
var opts = b.network.tls.buildOptions({ certificateCompression: ["brotli"] });
b.network.tls.outboundPosture() #
The framework's outbound TLS posture as a fresh options object, ready to merge into any tls.connect, https.request or https.Agent options:
var connectOpts = Object.assign({ host: h, port: p }, b.network.tls.outboundPosture());
Every protocol client the framework ships — DNS-over-HTTPS and DNS-over-TLS, NTS-KE, Redis, syslog, WebSocket, proxy tunnels, the HTTP client, the ECH and OCSP paths — merges this rather than listing the keys itself, so raising the posture is one edit that cannot reach some outbound paths and miss others.
It reads the LIVE key-share preference, so b.network.tls.preferredGroups.set(...) (or the b.network.tls.pqc alias) takes effect on the next dial across every one of those clients. Call it per connection rather than caching the result, or an operator's later narrowing will not reach the wire.
// Restrict every outbound handshake to the NIST-curve hybrids.
b.network.tls.preferredGroups.set(["SecP256r1MLKEM768", "SecP384r1MLKEM1024"]);
b.network.tls.outboundPosture();
// → { minVersion: "TLSv1.3",
// ecdhCurve: "SecP256r1MLKEM768:SecP384r1MLKEM1024",
// certificateCompression: ["zlib", "brotli", "zstd"] }
b.network.tls.explainOutboundFailure(err, ctx?) #
{
host: string, // peer named in the message; default: omitted
port: number, // appended to host when both are present
tlsOpts: object, // the options the dial used; each setting below is read from it
minVersion: string, // the floor that was applied; default: the live posture
maxVersion: string, // the ceiling that was applied, when a caller capped the dial below TLS 1.3
secureProtocol: string, // an OpenSSL method name, when a caller pinned one version that way
ecdhCurve: string, // the group list that was offered; default: the live posture
}
Turn a refused outbound handshake into a sentence naming the likely cause.
OpenSSL reports a rejected ClientHello as a bare alert — tlsv1 alert protocol version — which names neither the peer nor anything an operator can act on, and reads as a problem with the request rather than with the posture the client applied to it. Two of those alerts are routinely the posture doing its job:
A protocol_version alert against a client pinning minVersion: "TLSv1.3" means the peer offers no TLS 1.3. That is a version refusal and has nothing to do with the key-exchange groups, which is worth stating plainly: the post-quantum posture is the framework's most visible property, so it draws the blame for handshake failures it did not cause.
A handshake_failure alert against a narrowed group list means the peer shares none of the offered groups. Under the shipped preference this cannot happen — the list ends in classical X25519, so a peer with no post-quantum hybrid negotiates that — so it points at a list an operator has narrowed past what the peer can do.
Neither alert is exclusive to these causes, so the wording is hedged and the original error text is carried through rather than replaced. Returns null when the error is not one this can speak to, which callers use to fall back to the original message.
Pass tlsOpts — the options object the dial was made with — rather than copying settings out of it. Handing the object over names every setting it could carry, so the explanation stays right as the diagnosis learns to read settings a caller was not thinking about when the error handler was written.
var why = b.network.tls.explainOutboundFailure(err, { host: "registry.example.com", port: 443 });
// -> "TLS handshake refused by registry.example.com:443 - the peer most
// likely does not offer TLS 1.3, ..."
b.network.tls.annotateOutboundFailure(err, ctx?) #
{
host: string, // peer named in the message
port: number, // appended to host when both are present
tlsOpts: object, // the options the dial used; see explainOutboundFailure for the settings read from it
}
Rewrite a refused handshake's message in place to name the likely cause, and hand the same error back.
The error object is kept rather than replaced so err.code, the error class and anything a caller already branches on survive untouched; only the message changes, and only when there is something to say. The stack's leading line is refreshed alongside it, since it embeds a copy of the message captured at construction.
Diagnosis never fails a request: any error raised while working out the explanation is swallowed and the original message stands.
socket.on("error", function (err) {
b.network.tls.annotateOutboundFailure(err, { host: host, port: port });
handle(err);
});
b.network.tls.parseEchConfigList(raw) #
Parse a draft-ietf-tls-esni-22 ECHConfigList byte string (the value of the ech= SvcParam in an SVCB or HTTPS DNS record per RFC 9460 paragraph 7.4.2). Accepts a Buffer or a strict-base64 string. Returns { rawLength, configs: [{ version, length, keyConfig, ... }] }.
For each ECHConfig at the published draft-22 version (0xfe0d) the decoded keyConfig carries configId, kemId, publicKey (Buffer), and cipherSuites (each { kdfId, aeadId }); the entry also exposes maximumNameLength, publicName, and extensions. Unknown future ECH versions surface their raw body Buffer so the caller can forward them to a Node build that supports them.
Throws NetworkTlsError("tls/ech-config-malformed") on any framing violation (truncated length prefix, vector overflow, bad cipher_suites stride, etc.).
var b = require("@blamejs/core");
var rrs = await b.network.dns.queryHttps("example.com");
// `ech` is optional in an HTTPS record, and a domain can drop or rotate it
// at any time — handle its absence rather than indexing into the match.
var rec = rrs.find(function (r) { return r.params && r.params.ech; });
if (rec) {
var parsed = b.network.tls.parseEchConfigList(rec.params.ech);
// parsed.configs[0].keyConfig.kemId === 0x0020 (X25519)
}
b.network.tls.connectWithEch(opts) #
{
{
host: string,
port: number,
alpn: string[],
ipFamily: 4 | 6,
timeoutMs: number,
servername: string,
ca: string|Buffer|Array,
checkServerIdentity: function,
echOverride: Buffer|string,
rejectUnauthorized: boolean,
}
}
b.network.tls.checkServerIdentity9525
Open a TLS-1.3 outbound connection with Encrypted Client Hello (ECH, draft-ietf-tls-esni-22) when the destination publishes an ech= SvcParam via SVCB/HTTPS records (RFC 9460 paragraph 2.4 / paragraph 9). The flow:
1. b.network.dns.queryHttps(host) to discover ECH config. 2. If any record carries ech=, the parsed ECHConfigList is attached to tls.connect({ ech }) so the outer ClientHello uses the published public_name SNI and the inner ClientHello (real SNI, ALPN, etc.) is HPKE-encrypted under the published public key. 3. If no record carries ech=, or DNS fails, the function falls back to a normal TLS connect (still TLSv1.3-floor + framework PQC group preference). Operators get an observability.event so the degradation is visible. 4. If the running Node build does not support the ech connect option, the function emits a one-shot warn and connects without ECH — never throws on missing Node-side support.
Returns the connected tls.TLSSocket once secureConnect fires. b.httpClient will compose this in a follow-up release; this primitive is the operator escape hatch for raw outbound TLS over ECH (custom protocol clients, mTLS testing, ECH validation tools).
// requires: outbound DNS for the ECH config, and a reachable peer
var b = require("@blamejs/core");
var sock = await b.network.tls.connectWithEch({
host: "ech-target.example.com",
alpn: ["h2", "http/1.1"],
});
sock.write("GET / HTTP/1.1\r\nHost: ech-target.example.com\r\n\r\n");
b.network.tls.checkServerIdentity9525(host, cert) #
Drop-in replacement for Node's tls.checkServerIdentity that implements RFC 9525 paragraph 6 strictly. Operators pass it to tls.connect({ checkServerIdentity }) (or to any framework primitive that exposes pkixStrict: true).
Differences vs Node's default matcher:
- SAN-required when present is mandatory: a peer cert lacking subjectAltName refuses with tls/pkix-san-required (RFC 9525 paragraph 6.4.4 forbids Common Name fallback). - CN-only legacy certs surface a distinct tls/pkix-cn-fallback-refused code so audit logs distinguish "missing SAN" from "ancient CN-only cert still shipping". - Wildcard matching is restricted to the entire leftmost label. *.example.com matches foo.example.com but NOT foo.bar.example.com and NOT example.com. Partial wildcards like f*o.example.com and middle wildcards like foo.*.example.com refuse. - IP literals match iPAddress SAN entries only — never DNS entries, never wildcards. IPv6 comparison is byte-equal after canonicalization (zone-id stripped, :: expanded).
Every other refusal carries tls/pkix-hostname-mismatch: a name no SAN entry matches, a wildcard that does not cover the host, a host that is not a non-empty string, a missing peer cert, and an IP literal with no matching iPAddress entry.
Returns Error | undefined — the Error shape Node expects; when undefined, the connection is permitted to proceed. The codes above are read off that returned Error, which this function returns rather than throws.
Reading the peer's subjectaltname is the one step that THROWS rather than returning: a malformed quoted entry raises tls/altname-format, because a SAN list this cannot split is not a list it can check a name against.
var tls = require("node:tls");
var b = require("@blamejs/core");
var sock = tls.connect({
host: "internal.example.com",
port: 443,
checkServerIdentity: b.network.tls.checkServerIdentity9525,
});
b.network.dns.tsig.sign(message, opts) #
{
keyName: string, // REQUIRED — the shared-key name
secret: string | Buffer, // REQUIRED — base64 string or raw bytes
algorithm: string, // default: "hmac-sha256"
fudge: number, // default: 300 (seconds)
time: number, // default: now (Unix seconds)
originalId: number, // default: the message's own ID
requestMac: Buffer, // when signing a response (§5.4.1)
error: number, // default: 0 (NOERROR)
otherData: Buffer, // default: empty
allowLegacy: boolean, // permit HMAC-MD5 / HMAC-SHA-1
}
Append a TSIG resource record to a DNS message (a Buffer of wire bytes) and return the signed wire Buffer. The MAC is the HMAC over the message plus the RFC 8945 §4.3.3 TSIG variables. Returns { wire, mac } — wire is the message with the TSIG RR appended and ARCOUNT incremented, and mac is the raw HMAC (keep it to verify the matching response).
var signed = b.network.dns.tsig.sign(queryWire, {
keyName: "update.key.", secret: "",
});
socket.send(signed.wire);
b.network.dns.tsig.verify(message, opts) #
{
keys: object, // { "": { secret, algorithm } }
keyName: string, // single-key form (with secret)
secret: string | Buffer, // single-key form
algorithm: string, // expected algorithm (single-key form)
now: number, // default: now (Unix seconds)
requestMac: Buffer, // when verifying a response (§5.4.1)
allowLegacy: boolean,
}
Verify the TSIG record on a DNS message: locate the trailing TSIG RR, recompute the HMAC over the RFC 8945 §4.3.3 digest, compare it in constant time, and check that now is within fudge seconds of timeSigned. Returns { valid, keyName, algorithm, timeSigned, fudge, error, macValid, timeValid, reason }; valid is true only when the MAC matches, the time window holds, and the embedded error is NOERROR. Never throws for an authentication failure — only for a malformed message or unknown key shape.
var r = b.network.dns.tsig.verify(received, {
keys: { "update.key.": { secret: "" } },
});
if (!r.valid) refuse(r.reason);
b.network.socket.applyToSocket(socket) #
Apply the framework's socket defaults (TCP_NODELAY, SO_KEEPALIVE + initial-delay) to a freshly-created net.Socket / tls.TLSSocket. Best-effort: a socket that has already errored, lacks the setter methods, or rejects the call is left as-is. Returns the same socket. Used by primitives that build their own server (b.tls, b.wsServer, b.smtp) so every socket on the wire follows the same tuning.
var net = require("net");
var s = new net.Socket();
var ret = b.network.socket.applyToSocket(s);
ret === s;
// → true
s.destroy();
b.network.bootFromEnv(opts) #
{
env: object, // default process.env — pass a fixture object in tests
audit: boolean, // default true — emit `network.boot.from_env`
}
Read BLAMEJS_* environment variables once and apply the union to the live network facade. Recognized keys cover NTP servers / timeout / drift thresholds, DNS servers / result-order / family / lookup-timeout / cache-TTL / DoH URL or provider / DoT host+port, HTTP_PROXY / HTTPS_PROXY / NO_PROXY, extra-CA file or directory, BLAMEJS_USE_SYSTEM_TRUST, and the socket TCP_NODELAY / SO_KEEPALIVE defaults. Returns an applied report — exactly which keys took effect. Audits network.boot.from_env unless opts.audit:false.
// requires: outbound NTP (udp/123) to the servers named below
var applied = b.network.bootFromEnv({
env: { BLAMEJS_NTP_SERVERS: "time.cloudflare.com,time.google.com" },
audit: false,
});
applied.ntp.servers;
// → 2
b.network.snapshot() #
Return a redacted snapshot of the network facade's current configuration: NTP servers + drift thresholds, DNS state (servers, result-order, family, DoH/DoT, cache TTL), proxy resolution, TLS trust-store size + system-trust flag, heartbeat statuses, and socket defaults. Cheap; safe to call from a /healthz or operations endpoint. No secrets are returned.
var snap = b.network.snapshot();
typeof snap.tls.caCount;
// → "number"