@blamejs/core 0.8.89 → 0.9.0

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.
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.mail.requireTls
4
+ * @nav Mail
5
+ * @title REQUIRETLS — RFC 8689
6
+ * @order 460
7
+ *
8
+ * @intro
9
+ * RFC 8689 SMTP REQUIRETLS — per-message TLS-requirement signaling
10
+ * between sender and receiver MTAs. The sender advertises that the
11
+ * message MUST NOT be relayed over a cleartext (non-TLS) hop; if
12
+ * no downstream MTA can deliver under TLS, the message bounces
13
+ * instead of falling back to cleartext. Complements MTA-STS / DANE
14
+ * (which are policy-side, domain-scoped) with a per-message
15
+ * knob that overrides the policy when the operator wants
16
+ * stricter-than-policy delivery.
17
+ *
18
+ * Wire surface (RFC 8689 §3):
19
+ *
20
+ * EHLO peer advertises: 250 REQUIRETLS
21
+ * Client sends: MAIL FROM:<sender> REQUIRETLS
22
+ * Server replies: 250 OK (or 550 if it can't honor)
23
+ *
24
+ * Header surface (RFC 8689 §5):
25
+ *
26
+ * TLS-Required: No Explicit operator override; sender
27
+ * requests REQUIRETLS-style behavior be
28
+ * DISABLED for this message even if the
29
+ * policy infrastructure (MTA-STS / DANE)
30
+ * says otherwise. Use sparingly — primary
31
+ * use case is delivery to legacy peers
32
+ * during a controlled migration.
33
+ *
34
+ * This module ships:
35
+ *
36
+ * b.mail.requireTls.peerSupports(ehloLines) → boolean
37
+ * Walks EHLO response lines and returns true when the peer
38
+ * advertised the REQUIRETLS keyword.
39
+ *
40
+ * b.mail.requireTls.mailFromExtension({ requireTls }) → string
41
+ * Returns the trailing " REQUIRETLS" token (or empty string)
42
+ * to append to a MAIL FROM line.
43
+ *
44
+ * b.mail.requireTls.parseTlsRequiredHeader(headerValue) → "yes" | "no" | null
45
+ * Parses the TLS-Required header field per §5. Returns "no"
46
+ * only when the value is the literal token "no" (case-
47
+ * insensitive); any other value returns "yes" (the conservative
48
+ * default — operators must opt OUT explicitly, never default to
49
+ * fall-back-to-cleartext). null when the header is absent.
50
+ *
51
+ * @card
52
+ * RFC 8689 REQUIRETLS — per-message TLS-requirement signaling between MTAs (EHLO keyword + MAIL FROM extension + TLS-Required header parser).
53
+ */
54
+
55
+ var structuredFields = require("./structured-fields");
56
+ var validateOpts = require("./validate-opts");
57
+ var { defineClass } = require("./framework-error");
58
+
59
+ var RequireTlsError = defineClass("RequireTlsError", { alwaysPermanent: true });
60
+
61
+ var REQUIRETLS_TOKEN = "REQUIRETLS";
62
+
63
+ /**
64
+ * @primitive b.mail.requireTls.peerSupports
65
+ * @signature b.mail.requireTls.peerSupports(ehloLines)
66
+ * @since 0.8.90
67
+ * @status stable
68
+ *
69
+ * Walk a parsed EHLO response and return `true` when the peer
70
+ * advertised the `REQUIRETLS` keyword. `ehloLines` is the array of
71
+ * post-greeting capability lines returned by the SMTP transport
72
+ * (each entry is the capability token, e.g. `"SIZE 10485760"`,
73
+ * `"PIPELINING"`, `"REQUIRETLS"`). Case-insensitive match per RFC
74
+ * 5321 §2.4 (EHLO keywords are uppercase by convention but
75
+ * comparison is case-insensitive).
76
+ *
77
+ * Returns `false` for empty / non-array input — operators who can't
78
+ * parse the EHLO get a definitive "not supported" verdict rather
79
+ * than a throw, matching the "defensive request-shape reader"
80
+ * convention used elsewhere.
81
+ *
82
+ * @example
83
+ * var ehlo = ["mail.example.com", "PIPELINING", "SIZE 10485760", "REQUIRETLS", "STARTTLS"];
84
+ * b.mail.requireTls.peerSupports(ehlo); // → true
85
+ *
86
+ * b.mail.requireTls.peerSupports(["PIPELINING", "SIZE 10485760"]); // → false
87
+ */
88
+ function peerSupports(ehloLines) {
89
+ if (!Array.isArray(ehloLines)) return false;
90
+ for (var i = 0; i < ehloLines.length; i += 1) {
91
+ var line = ehloLines[i];
92
+ if (typeof line !== "string") continue;
93
+ // Keyword is everything up to the first space (RFC 5321 §4.1.1.1).
94
+ var sp = line.indexOf(" ");
95
+ var keyword = sp === -1 ? line : line.slice(0, sp);
96
+ if (keyword.toUpperCase() === REQUIRETLS_TOKEN) return true;
97
+ }
98
+ return false;
99
+ }
100
+
101
+ /**
102
+ * @primitive b.mail.requireTls.mailFromExtension
103
+ * @signature b.mail.requireTls.mailFromExtension(opts)
104
+ * @since 0.8.90
105
+ * @status stable
106
+ *
107
+ * Build the trailing SMTP MAIL FROM extension token for REQUIRETLS.
108
+ * Returns `" REQUIRETLS"` (with a leading space, ready to append)
109
+ * when `opts.requireTls === true`; empty string otherwise. The
110
+ * primitive does NOT validate the operator's address — that's the
111
+ * SMTP transport's job. This only emits the standard-defined token
112
+ * suffix.
113
+ *
114
+ * Refuses non-object opts. `requireTls` must be a boolean when
115
+ * provided (any other type throws `mail-require-tls/bad-flag`) so
116
+ * a truthy-but-wrong-shape value (e.g. `"yes"`) doesn't silently
117
+ * succeed.
118
+ *
119
+ * @opts
120
+ * requireTls: boolean, // true to emit " REQUIRETLS"; falsy/absent → ""
121
+ *
122
+ * @example
123
+ * var line = "MAIL FROM:<alice@example.com>" +
124
+ * b.mail.requireTls.mailFromExtension({ requireTls: true });
125
+ * // → "MAIL FROM:<alice@example.com> REQUIRETLS"
126
+ */
127
+ function mailFromExtension(opts) {
128
+ if (!opts || typeof opts !== "object" || Array.isArray(opts)) {
129
+ throw new RequireTlsError("mail-require-tls/bad-opts",
130
+ "mailFromExtension: opts must be a non-null object", true);
131
+ }
132
+ if (opts.requireTls === undefined || opts.requireTls === false) return "";
133
+ if (opts.requireTls !== true) {
134
+ throw new RequireTlsError("mail-require-tls/bad-flag",
135
+ "mailFromExtension: requireTls must be a boolean (got " + typeof opts.requireTls + ")");
136
+ }
137
+ return " " + REQUIRETLS_TOKEN;
138
+ }
139
+
140
+ /**
141
+ * @primitive b.mail.requireTls.parseTlsRequiredHeader
142
+ * @signature b.mail.requireTls.parseTlsRequiredHeader(headerValue)
143
+ * @since 0.8.90
144
+ * @status stable
145
+ *
146
+ * Parse the RFC 8689 §5 `TLS-Required` header field. Returns:
147
+ *
148
+ * - `"no"` when the value is the literal token `no` (case-
149
+ * insensitive, ignoring surrounding whitespace) — the sender
150
+ * EXPLICITLY opts out of REQUIRETLS-style behavior for this
151
+ * message.
152
+ * - `"yes"` for any other non-empty value — conservative default
153
+ * so an operator who set a typo / malformed value still gets
154
+ * the strict path (RFC 8689 §5: "if a recipient receives a
155
+ * message containing a TLS-Required field with any value other
156
+ * than 'No', it MUST be treated as if the field had been
157
+ * absent").
158
+ * - `null` when the header is absent / empty / not a string —
159
+ * operator code branches on null vs "yes" / "no".
160
+ *
161
+ * Refuses CR / LF / NUL in the value (header-injection-shape inputs
162
+ * shouldn't reach a parser that's downstream of header splitters
163
+ * anyway, but a defensive check here catches operator-side mistakes).
164
+ *
165
+ * @example
166
+ * b.mail.requireTls.parseTlsRequiredHeader("No"); // → "no"
167
+ * b.mail.requireTls.parseTlsRequiredHeader("no"); // → "no"
168
+ * b.mail.requireTls.parseTlsRequiredHeader(" no "); // → "no"
169
+ * b.mail.requireTls.parseTlsRequiredHeader("yes"); // → "yes"
170
+ * b.mail.requireTls.parseTlsRequiredHeader("anything"); // → "yes" (RFC 8689 §5 default)
171
+ * b.mail.requireTls.parseTlsRequiredHeader(""); // → null
172
+ * b.mail.requireTls.parseTlsRequiredHeader(undefined); // → null
173
+ */
174
+ function parseTlsRequiredHeader(headerValue) {
175
+ if (typeof headerValue !== "string") return null;
176
+ structuredFields.refuseControlBytes(headerValue, {
177
+ ErrorClass: RequireTlsError,
178
+ code: "mail-require-tls/bad-header-value",
179
+ label: "parseTlsRequiredHeader",
180
+ });
181
+ var trimmed = headerValue.trim();
182
+ if (trimmed.length === 0) return null;
183
+ if (trimmed.toLowerCase() === "no") return "no";
184
+ // RFC 8689 §5 — any other value treated as if absent (strict path).
185
+ return "yes";
186
+ }
187
+
188
+ module.exports = {
189
+ peerSupports: peerSupports,
190
+ mailFromExtension: mailFromExtension,
191
+ parseTlsRequiredHeader: parseTlsRequiredHeader,
192
+ REQUIRETLS_TOKEN: REQUIRETLS_TOKEN,
193
+ RequireTlsError: RequireTlsError,
194
+ };
195
+
196
+ // Reserved for future field validation paths; kept in canonical
197
+ // require ordering.
198
+ void validateOpts;
package/lib/mail.js CHANGED
@@ -1818,11 +1818,13 @@ function feedbackId(opts) {
1818
1818
  return parts.join(":");
1819
1819
  }
1820
1820
 
1821
- var mailSrs = require("./mail-srs");
1821
+ var mailRequireTls = require("./mail-require-tls");
1822
+ var mailSrs = require("./mail-srs");
1822
1823
 
1823
1824
  module.exports = {
1824
1825
  create: create,
1825
1826
  feedbackId: feedbackId,
1827
+ requireTls: mailRequireTls,
1826
1828
  srs: mailSrs,
1827
1829
  MailError: MailError,
1828
1830
  unsubscribe: mailUnsubscribe,
@@ -112,13 +112,14 @@ var fs = require("fs");
112
112
  var os = require("os");
113
113
  var path = require("path");
114
114
  var nodeCrypto = require("node:crypto");
115
- var atomicFile = require("../atomic-file");
116
- var crypto = require("../crypto");
117
- var lazyRequire = require("../lazy-require");
118
- var requestHelpers = require("../request-helpers");
119
- var safeBuffer = require("../safe-buffer");
120
- var safeJson = require("../safe-json");
121
- var validateOpts = require("../validate-opts");
115
+ var atomicFile = require("../atomic-file");
116
+ var crypto = require("../crypto");
117
+ var lazyRequire = require("../lazy-require");
118
+ var requestHelpers = require("../request-helpers");
119
+ var safeBuffer = require("../safe-buffer");
120
+ var safeJson = require("../safe-json");
121
+ var structuredFields = require("../structured-fields");
122
+ var validateOpts = require("../validate-opts");
122
123
  var C = require("../constants");
123
124
  var { defineClass } = require("../framework-error");
124
125
 
@@ -214,14 +215,20 @@ function _contentType(req) {
214
215
  var params = {};
215
216
  if (idx !== -1) {
216
217
  var rest = ct.slice(idx + 1);
217
- var parts = rest.split(";");
218
+ // RFC 9110 §8.3 + §5.6.6 — parameter values may be quoted-string
219
+ // (e.g. `boundary="foo;bar"`, `charset="x;y"`). Bare `.split(";")`
220
+ // would slice through quoted commas/semicolons and corrupt the
221
+ // multipart boundary. Use the shared quote-aware splitter that
222
+ // tracks RFC 8941 §3.3.3 quoted-string state with backslash-escape.
223
+ var parts = structuredFields.splitTopLevel(rest, ";");
218
224
  for (var i = 0; i < parts.length; i++) {
219
225
  var p = parts[i].trim();
220
226
  var eq = p.indexOf("=");
221
227
  if (eq === -1) continue;
222
228
  var k = p.slice(0, eq).trim().toLowerCase();
223
229
  var v = p.slice(eq + 1).trim();
224
- if (v.length >= 2 && v[0] === '"' && v[v.length - 1] === '"') v = v.slice(1, -1);
230
+ var _unq = structuredFields.unquoteSfString(v);
231
+ if (_unq !== null) v = _unq;
225
232
  params[k] = v;
226
233
  }
227
234
  }
@@ -305,7 +312,7 @@ function _detectSmuggling(req) {
305
312
  // (RFC 9112 §6.1). Anything else is a smuggling vector or
306
313
  // server-side decode error.
307
314
  if (typeof te === "string" && te.length > 0) {
308
- var tokens = te.toLowerCase().split(",").map(function (t) { return t.trim(); });
315
+ var tokens = te.toLowerCase().split(",").map(function (t) { return t.trim(); }); // allow:bare-split-on-quoted-header — RFC 9112 §6.1 Transfer-Encoding values (chunked / gzip / deflate / identity) are token-only; no quoted-string in the grammar
309
316
  var last = tokens[tokens.length - 1];
310
317
  if (last !== "chunked") {
311
318
  return {
@@ -621,7 +628,11 @@ function _parseHeaderParams(headerValue) {
621
628
  // `filename` so downstream consumers don't need parser-aware code.
622
629
  var out = { _value: "" };
623
630
  if (!headerValue) return out;
624
- var parts = headerValue.split(";");
631
+ // RFC 6266 §4.1 + RFC 9110 §5.6.6 — parameter values may be
632
+ // quoted-string (e.g. `filename="weird;name.txt"`). Bare
633
+ // `.split(";")` would slice through the quoted semicolon and
634
+ // corrupt the filename. Quote-aware shared splitter.
635
+ var parts = structuredFields.splitTopLevel(headerValue, ";");
625
636
  out._value = parts[0].trim().toLowerCase();
626
637
  var extName = null;
627
638
  for (var i = 1; i < parts.length; i++) {
@@ -630,7 +641,8 @@ function _parseHeaderParams(headerValue) {
630
641
  if (eq === -1) continue;
631
642
  var k = p.slice(0, eq).trim().toLowerCase();
632
643
  var v = p.slice(eq + 1).trim();
633
- if (v.length >= 2 && v[0] === '"' && v[v.length - 1] === '"') v = v.slice(1, -1);
644
+ var _unq = structuredFields.unquoteSfString(v);
645
+ if (_unq !== null) v = _unq;
634
646
  if (k.charAt(k.length - 1) === "*") {
635
647
  var decoded = _decodeRfc5987(v);
636
648
  if (decoded !== null) {
@@ -177,8 +177,8 @@ async function _dispatch(req, res, basePath, bearer, opts, maxPageSize) {
177
177
  count: pageSize,
178
178
  sortBy: query.sortBy || null,
179
179
  sortOrder: query.sortOrder || null,
180
- attributes: query.attributes ? query.attributes.split(",") : null,
181
- excludedAttributes: query.excludedAttributes ? query.excludedAttributes.split(",") : null,
180
+ attributes: query.attributes ? query.attributes.split(",") : null, // allow:bare-split-on-quoted-header — RFC 7644 §3.9 attributes/excludedAttributes are SCIM attribute paths (URN-ish identifiers); grammar excludes DQUOTE
181
+ excludedAttributes: query.excludedAttributes ? query.excludedAttributes.split(",") : null, // allow:bare-split-on-quoted-header — same SCIM attribute-name grammar
182
182
  }, ctx);
183
183
  _writeJson(res, H.OK, {
184
184
  schemas: [SCIM_MESSAGE_LIST],
@@ -40,13 +40,14 @@
40
40
  * cannot satisfy.
41
41
  */
42
42
 
43
- var nodeCrypto = require("crypto"); // for createHash() in checksum extension
44
- var C = require("../constants");
45
- var bCrypto = require("../crypto");
46
- var lazyRequire = require("../lazy-require");
47
- var safeAsync = require("../safe-async");
48
- var safeBuffer = require("../safe-buffer");
49
- var validateOpts = require("../validate-opts");
43
+ var nodeCrypto = require("crypto"); // for createHash() in checksum extension
44
+ var C = require("../constants");
45
+ var bCrypto = require("../crypto");
46
+ var lazyRequire = require("../lazy-require");
47
+ var safeAsync = require("../safe-async");
48
+ var safeBuffer = require("../safe-buffer");
49
+ var structuredFields = require("../structured-fields");
50
+ var validateOpts = require("../validate-opts");
50
51
  var { defineClass } = require("../framework-error");
51
52
 
52
53
  // Observability metric prefix for the TUS middleware. The framework
@@ -145,6 +146,10 @@ function _serializeMetadata(metaObj) {
145
146
  function _parseChecksumHeader(headerValue, allowedSet) {
146
147
  // tus.io 1.0.0 §3.5: `Upload-Checksum: <algo> <base64-digest>`.
147
148
  if (typeof headerValue !== "string") return null;
149
+ // The tus.io grammar implicitly excludes C0 / DEL (token + base64
150
+ // alphabet); refuse those on the RAW value BEFORE the slice/trim
151
+ // normalisation (same v0.8.90 trim-before-validate bug class).
152
+ if (structuredFields.containsControlBytes(headerValue)) return { error: "malformed" };
148
153
  var sp = headerValue.indexOf(" ");
149
154
  if (sp === -1) return { error: "malformed" };
150
155
  var algo = headerValue.slice(0, sp).trim().toLowerCase();
@@ -1708,9 +1708,187 @@ function isNullMx(mxRecords) {
1708
1708
  return only.exchange === "" || only.exchange === ".";
1709
1709
  }
1710
1710
 
1711
+ // RFC 9905 — Deprecating DNSSEC SHA-1 Usage. The IANA DNSSEC Algorithm
1712
+ // Numbers registry classifies SHA-1-based DNSKEY algorithms (5
1713
+ // RSASHA1, 7 RSASHA1-NSEC3-SHA1, 10 RSASHA512-using-SHA1-NSEC3) and
1714
+ // SHA-1 DS digest type 1 as "MUST NOT be used" / "MUST NOT be
1715
+ // supported". Operators auditing inbound DNSSEC chain-of-trust data
1716
+ // classify a record's algorithm number to decide whether to refuse
1717
+ // the validation as deprecated.
1718
+ //
1719
+ // Returns the classification verdict object:
1720
+ // {
1721
+ // deprecated: boolean, // true when SHA-1 family per RFC 9905 §3-§4
1722
+ // algorithm: number, // echo of input
1723
+ // name: string, // human-readable label
1724
+ // reason: string, // citation
1725
+ // }
1726
+ // for any IANA DNSKEY algorithm number, or null for unknown / non-
1727
+ // numeric input. Defensive request-shape reader — never throws.
1728
+
1729
+ /**
1730
+ * @primitive b.network.dns.classifyDnskeyAlgorithm
1731
+ * @signature b.network.dns.classifyDnskeyAlgorithm(algorithm)
1732
+ * @since 0.8.91
1733
+ * @status stable
1734
+ * @related b.network.dns.classifyDsDigestType, b.network.dns.isNullMx
1735
+ *
1736
+ * Classify a DNSKEY / RRSIG algorithm number against the IANA DNS
1737
+ * Security Algorithm Numbers registry, flagging SHA-1-based and
1738
+ * other deprecated algorithms per RFC 9905 (Deprecating DNSSEC
1739
+ * SHA-1 Usage), RFC 8624 (Algorithm Implementation Requirements),
1740
+ * and RFC 6944 / RFC 6725 (RSAMD5 deprecation).
1741
+ *
1742
+ * Returns `{ algorithm, name, deprecated, reason, known }` for any
1743
+ * IANA-assigned number; `known: false` for unassigned numbers
1744
+ * (operators decide whether unassigned == deprecated for their
1745
+ * threat model). Returns `null` for non-integer / non-finite input.
1746
+ *
1747
+ * Operators auditing inbound DNSSEC chain-of-trust evidence call
1748
+ * this on each link's algorithm number and refuse the validation
1749
+ * when `deprecated === true`. Defensive request-shape reader —
1750
+ * never throws.
1751
+ *
1752
+ * @example
1753
+ * var v = b.network.dns.classifyDnskeyAlgorithm(5);
1754
+ * // → { algorithm: 5, name: "RSASHA1", deprecated: true,
1755
+ * // reason: "SHA-1 deprecated (RFC 9905 §3)", known: true }
1756
+ * if (v && v.deprecated) throw new Error("refuse DNSSEC algo " + v.name);
1757
+ *
1758
+ * b.network.dns.classifyDnskeyAlgorithm(13);
1759
+ * // → { algorithm: 13, name: "ECDSAP256SHA256", deprecated: false, ... }
1760
+ */
1761
+
1762
+ // Canonical DNSKEY algorithm vocabulary (IANA DNS Security Algorithm
1763
+ // Numbers registry — https://www.iana.org/assignments/dns-sec-alg-numbers).
1764
+ // Operators looking up the human-readable label or computing whether
1765
+ // the framework's own DNSSEC paths use a deprecated algorithm walk
1766
+ // this table. Every IANA-assigned number gets an entry (including
1767
+ // Reserved / Private-use values) so `classifyDnskeyAlgorithm()`
1768
+ // returns `known: true` for the full assigned space; the "Unassigned"
1769
+ // range (17-122, 124-251) is the only set that surfaces as
1770
+ // `known: false`. Marked-deprecated entries cite the controlling
1771
+ // RFC; Reserved / Private-use entries are flagged so operators
1772
+ // auditing DNSSEC chain-of-trust evidence know they cannot validate
1773
+ // the entry against a public algorithm registry.
1774
+ var DNSKEY_ALGORITHMS = Object.freeze({
1775
+ 1: { name: "RSAMD5", deprecated: true, reason: "MD5 broken (RFC 6944 §2.1, RFC 6725)" },
1776
+ 2: { name: "DH", deprecated: true, reason: "Diffie-Hellman key (RFC 2539) — never widely deployed; superseded by signature algorithms" },
1777
+ 3: { name: "DSA", deprecated: true, reason: "DSA deprecated (RFC 8624 §3.1)" },
1778
+ 4: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 4034 §A.1) — not for production use" },
1779
+ 5: { name: "RSASHA1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §3)" },
1780
+ 6: { name: "DSA-NSEC3-SHA1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §3); DSA deprecated (RFC 8624 §3.1)" },
1781
+ 7: { name: "RSASHA1-NSEC3-SHA1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §3)" },
1782
+ 8: { name: "RSASHA256", deprecated: false, reason: "current — RFC 5702" }, // allow:raw-byte-literal — IANA DNSKEY algorithm number
1783
+ 9: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 5155) — not for production use" },
1784
+ 10: { name: "RSASHA512", deprecated: false, reason: "current — RFC 5702" },
1785
+ 11: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 5155) — not for production use" },
1786
+ 12: { name: "ECC-GOST", deprecated: true, reason: "deprecated (RFC 8624 §3.1)" },
1787
+ 13: { name: "ECDSAP256SHA256", deprecated: false, reason: "current — RFC 6605" },
1788
+ 14: { name: "ECDSAP384SHA384", deprecated: false, reason: "current — RFC 6605" },
1789
+ 15: { name: "ED25519", deprecated: false, reason: "current — RFC 8080" },
1790
+ 16: { name: "ED448", deprecated: false, reason: "current — RFC 8080" }, // allow:raw-byte-literal — IANA DNSKEY algorithm number
1791
+ // 17-122: Unassigned per IANA. Operators that see one of these
1792
+ // get known: false from classifyDnskeyAlgorithm() — the entry
1793
+ // is not a typo against the framework table, it's a value the
1794
+ // registry hasn't allocated yet.
1795
+ // 123-251: Reserved per IANA.
1796
+ 252: { name: "INDIRECT", deprecated: true, reason: "Reserved indirect-keys placeholder (RFC 4034 §A.1) — not usable for signing/verification" }, // allow:raw-byte-literal — IANA DNSKEY algorithm number
1797
+ 253: { name: "PRIVATEDNS", deprecated: false, reason: "Private algorithm identified by domain name (RFC 4034 §A.1.1) — operators using this assume the private algorithm itself is acceptable" },
1798
+ 254: { name: "PRIVATEOID", deprecated: false, reason: "Private algorithm identified by OID (RFC 4034 §A.1.2) — operators using this assume the private algorithm itself is acceptable" },
1799
+ 255: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 4034 §A.1) — not for production use" },
1800
+ });
1801
+
1802
+ /**
1803
+ * @primitive b.network.dns.classifyDsDigestType
1804
+ * @signature b.network.dns.classifyDsDigestType(digestType)
1805
+ * @since 0.8.91
1806
+ * @status stable
1807
+ * @related b.network.dns.classifyDnskeyAlgorithm, b.network.dns.isNullMx
1808
+ *
1809
+ * Classify a DS-record digest type against the IANA DNSSEC Delegation
1810
+ * Signer (DS) Resource Record (RR) Type Digest Algorithms registry,
1811
+ * flagging SHA-1 (digest type 1) as deprecated per RFC 9905 §4.
1812
+ *
1813
+ * Returns `{ digestType, name, deprecated, reason, known }` for any
1814
+ * IANA-assigned number; `null` for non-integer input.
1815
+ *
1816
+ * @example
1817
+ * var v = b.network.dns.classifyDsDigestType(1);
1818
+ * // → { digestType: 1, name: "SHA-1", deprecated: true,
1819
+ * // reason: "SHA-1 deprecated (RFC 9905 §4)", known: true }
1820
+ *
1821
+ * b.network.dns.classifyDsDigestType(2);
1822
+ * // → { digestType: 2, name: "SHA-256", deprecated: false, ... }
1823
+ */
1824
+
1825
+ // DS digest-type vocabulary (RFC 4034 §5.1 + RFC 6605 §6 + RFC 8624
1826
+ // §3.2 + RFC 9558). Digest type 1 = SHA-1 is deprecated per RFC 9905
1827
+ // §4. Digest types 5 (GOST R 34.11-2012) and 6 (SM3) added by RFC
1828
+ // 9558. Reserved value 0 surfaced for completeness.
1829
+ var DS_DIGEST_TYPES = Object.freeze({
1830
+ 0: { name: "Reserved", deprecated: true, reason: "Reserved (RFC 3658) — not for production use" },
1831
+ 1: { name: "SHA-1", deprecated: true, reason: "SHA-1 deprecated (RFC 9905 §4)" },
1832
+ 2: { name: "SHA-256", deprecated: false, reason: "current — RFC 4509" },
1833
+ 3: { name: "GOST R 34.11-94", deprecated: true, reason: "deprecated (RFC 8624 §3.2; superseded by GOST 2012 in RFC 9558)" },
1834
+ 4: { name: "SHA-384", deprecated: false, reason: "current — RFC 6605 §6" },
1835
+ 5: { name: "GOST R 34.11-2012", deprecated: false, reason: "current — RFC 9558 §3" },
1836
+ 6: { name: "SM3", deprecated: false, reason: "current — RFC 9558 §3 (Chinese national standard)" },
1837
+ });
1838
+
1839
+ function classifyDnskeyAlgorithm(algorithm) {
1840
+ if (typeof algorithm !== "number" || !isFinite(algorithm) || Math.floor(algorithm) !== algorithm) {
1841
+ return null;
1842
+ }
1843
+ var row = DNSKEY_ALGORITHMS[algorithm];
1844
+ if (!row) {
1845
+ return {
1846
+ algorithm: algorithm,
1847
+ name: "unassigned",
1848
+ deprecated: false,
1849
+ reason: "no IANA assignment for algorithm " + algorithm,
1850
+ known: false,
1851
+ };
1852
+ }
1853
+ return {
1854
+ algorithm: algorithm,
1855
+ name: row.name,
1856
+ deprecated: row.deprecated,
1857
+ reason: row.reason,
1858
+ known: true,
1859
+ };
1860
+ }
1861
+
1862
+ function classifyDsDigestType(digestType) {
1863
+ if (typeof digestType !== "number" || !isFinite(digestType) || Math.floor(digestType) !== digestType) {
1864
+ return null;
1865
+ }
1866
+ var row = DS_DIGEST_TYPES[digestType];
1867
+ if (!row) {
1868
+ return {
1869
+ digestType: digestType,
1870
+ name: "unassigned",
1871
+ deprecated: false,
1872
+ reason: "no IANA assignment for digest type " + digestType,
1873
+ known: false,
1874
+ };
1875
+ }
1876
+ return {
1877
+ digestType: digestType,
1878
+ name: row.name,
1879
+ deprecated: row.deprecated,
1880
+ reason: row.reason,
1881
+ known: true,
1882
+ };
1883
+ }
1884
+
1711
1885
  module.exports = {
1712
1886
  setServers: setServers,
1713
1887
  isNullMx: isNullMx,
1888
+ classifyDnskeyAlgorithm: classifyDnskeyAlgorithm,
1889
+ classifyDsDigestType: classifyDsDigestType,
1890
+ DNSKEY_ALGORITHMS: DNSKEY_ALGORITHMS,
1891
+ DS_DIGEST_TYPES: DS_DIGEST_TYPES,
1714
1892
  getServers: getServers,
1715
1893
  setResultOrder: setResultOrder,
1716
1894
  setFamily: setFamily,
@@ -598,7 +598,7 @@ async function tlsRptFetchPolicy(domain, opts) {
598
598
  if (/^v=TLSRPTv1\b/i.test(s)) { joined = s; break; }
599
599
  }
600
600
  if (joined.length === 0) return null;
601
- var parts = joined.split(";");
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
602
602
  var rua = [];
603
603
  for (var p = 0; p < parts.length; p += 1) {
604
604
  var t = parts[p].trim();
@@ -607,7 +607,7 @@ async function tlsRptFetchPolicy(domain, opts) {
607
607
  var k = t.slice(0, eq).trim().toLowerCase();
608
608
  var v = t.slice(eq + 1).trim();
609
609
  if (k === "rua") {
610
- var uris = v.split(",");
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
611
611
  for (var u = 0; u < uris.length; u += 1) {
612
612
  var uri = uris[u].trim();
613
613
  if (uri.length > 0) rua.push(uri);
@@ -41,6 +41,8 @@
41
41
  // values (RFC 9110), not byte sizes. Names are RFC 9110 reason phrases;
42
42
  // every consumer reads HTTP_STATUS.<NAME> rather than the underlying
43
43
  // integer, so the hex form is purely an internal storage detail.
44
+ var structuredFields = require("./structured-fields");
45
+
44
46
  var HTTP_STATUS = Object.freeze({
45
47
  OK: 0xC8,
46
48
  PARTIAL_CONTENT: 0xCE,
@@ -354,6 +356,19 @@ function parseListHeader(value, opts) {
354
356
  opts = opts || {};
355
357
  var s = typeof value === "string" ? value : String(value);
356
358
  if (s.length === 0) return [];
359
+ if (opts.strictToken) {
360
+ // RFC 9110 §5.6.2 token grammar excludes C0 / DEL. Scan the RAW
361
+ // value BEFORE the comma split + trim so a leading/trailing
362
+ // `\r\n\t` byte can't slip through (the trim() below would strip
363
+ // it before RFC_9110_TOKEN_RE saw it, matching the v0.8.90
364
+ // `parseTlsRequiredHeader` bug class).
365
+ structuredFields.refuseControlBytes(s, {
366
+ ErrorClass: TypeError,
367
+ code: "parseListHeader/control-character",
368
+ label: "parseListHeader",
369
+ useNativeError: true,
370
+ });
371
+ }
357
372
  var parts = s.split(",");
358
373
  var out = [];
359
374
  for (var i = 0; i < parts.length; i++) {
@@ -202,7 +202,29 @@ async function assertProduction(opts) {
202
202
  var want = opts.minTlsVersion;
203
203
  // Compare TLSv1.3 > TLSv1.2 > TLSv1.1 > TLSv1.0 by string.
204
204
  var order = ["TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"];
205
- if (order.indexOf(got) < order.indexOf(want)) {
205
+ // Validate BOTH the operator-supplied required version AND the
206
+ // currently-active version against the known-vocabulary BEFORE
207
+ // the rank compare. A typo'd `want` (e.g. "TLS1.3" or "TLSv1.4")
208
+ // maps to indexOf === -1; without this check, the comparison
209
+ // `3 < -1 === false` silently passes even though the operator
210
+ // asked for a version the framework doesn't recognize.
211
+ // Throw at config time — this is a production-posture entry
212
+ // point and operator typos must be loud at boot, not deferred
213
+ // to per-request audit.
214
+ if (order.indexOf(want) === -1) {
215
+ throw new TypeError(
216
+ "assertProductionPosture: opts.minTlsVersion '" + want +
217
+ "' is not one of " + order.join(" / "));
218
+ }
219
+ if (order.indexOf(got) === -1) {
220
+ // Node's DEFAULT_MIN_VERSION shouldn't ever drift outside the
221
+ // canonical 4-value vocabulary, but if it does (future Node
222
+ // version, monkey-patched runtime), surface the failure
223
+ // rather than silently treating it as "below required".
224
+ failures.push({ ok: false, code: "security/tls-min-version",
225
+ message: "Node TLS DEFAULT_MIN_VERSION is an unrecognized value '" + got +
226
+ "' (expected one of " + order.join(" / ") + "); required '" + want + "'" });
227
+ } else if (order.indexOf(got) < order.indexOf(want)) {
206
228
  failures.push({ ok: false, code: "security/tls-min-version",
207
229
  message: "Node TLS DEFAULT_MIN_VERSION is '" + got + "', required '" + want + "'" });
208
230
  }