@blamejs/core 0.18.44 → 0.18.46

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 CHANGED
@@ -8,6 +8,66 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.18.x
10
10
 
11
+ - v0.18.46 (2026-08-22) — **A failing test reported the handle it leaked instead of the check that failed.** Thirty test files ran their assertions inside a `try` whose `finally` drained open handles. A throw from a `finally` replaces the error the body raised, so when a test failed and its skipped teardown left a socket behind, what surfaced was "a handle leaked" — the consequence — while the check that actually failed was discarded. That is the shape of failure that reads as an unexplained flake, and it was reachable in every suite that opens a server. **Fixed:** *A drain no longer replaces the failure it followed* — `try { ...tests... } finally { await drain() }` reads as correct. It is not. When the body throws, every teardown after the throw is skipped, so the drain finds the servers those teardowns would have closed and throws too — and a throw from a `finally` replaces the body's error. The run reports a leaked handle, which is true and useless: the check that failed is gone, and the leak looks like the bug.
12
+
13
+ The drain now runs through one helper that keeps the body's error and appends the others to it, so a run that failed for a reason reports that reason, and a run that only leaked still reports the leak. Thirty files moved onto it.
14
+
15
+ Six of those thirty had their own cleanup — destroying a keep-alive agent, closing websocket clients and the sockets an upgrade detached from the server, resetting module state — sitting in the same `finally`. They were the worse instance rather than the exception: more throwable work in the place that replaces the failure. The helper took an optional teardown so their work runs inside the structure that preserves the error, instead of those six taking an exemption.
16
+
17
+ One file drained fifty-six times, once per section. A drain is a leak VERDICT rather than cleanup, so that was fifty-six places a real assertion failure could surface as a leak. Its sections now clean up and the verdict is taken once, at the end, where nothing is left to mask. The trade is that a leak there is reported against the file rather than the section; what the drain names is the surviving handles with their addresses and states, which is the attribution that matters. · *The helper that makes that promise is now tested* — Thirty files rely on "the body's error wins", and nothing checked it. A helper can stop doing the one thing it exists for while every suite using it still passes, because the suites only exercise it on the path where nothing goes wrong.
18
+
19
+ It is now driven directly: a body that throws, a teardown that throws after it, both throwing at once, a teardown that throws when the body passed, and a body that throws a string rather than an Error — which used to lose the appended note entirely, since assigning to a property of a primitive is a silent no-op outside strict mode.
20
+
21
+ The suite was confirmed to fail against the behaviour it forbids before it was kept. · *Two ways the failure-preserving helper could still lose the failure* — A thrown `null` was not a failure. Both helpers decided whether something had failed by asking whether the value it threw was truthy, so `throw null`, `throw ""` and `throw 0` read as passes: the drain helper returned normally, and the polling helper dropped "last predicate threw" from its timeout message, leaving "the condition never came true" where the truth was "the condition could not be evaluated". Both now track whether a throw happened as its own flag, separate from what was thrown.
22
+
23
+ And annotating an error could destroy it, or be invisible. When a run fails and its teardown fails after, the second failure is appended to the first — by mutating the error's message. An error can refuse to be mutated: a frozen one, or a `message` defined non-writable. That assignment throws, and the throw escapes from the annotator and replaces the failure it was called to annotate. Appending is now attempted and, when refused, the same information is carried in a new error that keeps the original as its cause.
24
+
25
+ The note also has to reach the place anybody reads. A runner prints `.stack`, not `.message`, and a stack string is formatted once on first read and then cached — so an error that some assertion or logger had already touched carried a stack headed by the original message, and the appended note never appeared. The note now goes on the stack as well, guarded on whether it is already there rather than on whether the stack was cached, so it lands exactly once.
26
+
27
+ All of it is the swallowing this release is about, one level further in: the helper that exists to stop a failure being lost had several ways of its own to lose one. · *A polling timeout blamed an exception the predicate had stopped raising* — The polling helper names the last exception in its timeout message, which is the difference between "the condition never came true" and "the condition could not be evaluated". It reported whatever had most recently been thrown, including from a poll many rounds earlier — so a predicate that raised while the thing it watches was still settling, and then answered cleanly, blamed the timeout on an exception it had stopped raising.
28
+
29
+ A throw during polling is not the verdict. The predicate is being asked repeatedly precisely because the state is still settling, so the loop now treats a throw as "not yet" and keeps nothing from it. The diagnostic speaks for one attempt — the one at the deadline — and answers the question worth asking: was the predicate still failing to evaluate when the wait gave up?
30
+
31
+ The timeout diagnostic now has its own tests, which is what surfaced this: whether the helper eventually returns is exercised by every suite that polls, and the message it produces when it does not was exercised by none of them. · *Two test files said they were outside the smoke run* — Both carried a header stating they were not part of the smoke surface. The runner takes every `*.test.js` under the layer directory, and the only opt-out is a `STANDALONE_ONLY` marker in the first two kilobytes, which neither file carries — so both have been running all along. A comment that says a failure here will not block is the kind that gets believed. **Detectors:** *A handle drain may not sit in a `finally`* — The shape is refused wherever it reappears. It matches both spellings — a direct call and a local wrapper — because a word boundary alone misses the underscore-prefixed form, which is exactly how six of the thirty files went uncounted when the population was first measured.
32
+
33
+ Anchored on the `finally` and the drain together and tempered so it cannot cross a function-closing brace; the quantifier is a backstop far above any real `finally` body rather than the precision mechanism. It was confirmed firing on all thirty before any of them were converted, and silent after the last one.
34
+
35
+ - v0.18.45 (2026-08-21) — **`b.publicSuffix` returned names that cannot exist — a 64-octet label, and `ex*ample.com`.** `canonicalDomain` is where the framework decides what a domain IS, and two kinds of non-domain got through it. A label longer than the 63 octets RFC 1035 allows came back as an organizational domain, and 17 of 31 non-LDH characters survived because `domainToASCII` maps a name without validating it as a host. Both answers then travelled: into DMARC alignment, cookie-domain comparisons and residency routing, where a name no resolver will ever return is compared against one that exists. **Changed:** *Vendored `@blamejs/pki` 0.5.23 to 0.5.25* — The toolkit behind `b.mtlsCa` and `b.auth.passkey`. Two releases land together.
36
+
37
+ 0.5.24 binds an ACME certificate download to the order that asked for it. The end-entity certificate that comes back must carry the public key the order's CSR asked to have certified, and its identifier set must equal the order's — its dNSName and iPAddress subject alternative names, with the subject common name read only where it asserts none. `downloadCertificate` now requires `expectedSpki`, `identifiers`, or both, and refuses a call supplying neither rather than returning a certificate nothing looked at. An identifier that maps to no certificate name is refused rather than dropped from the comparison, which is what let an order for a name plus one other identifier be satisfied by a certificate covering only the name.
38
+
39
+ The same release gives case-insensitive name comparison one definition that folds with ASCII rules, so U+212A KELVIN SIGN is no longer read as the `k` its Unicode lowercase produces — the mapping that made two separately registrable domains compare equal. And every guard module freezes its exports: a boundary reads its check off the module object at the moment of the call, and the module registry hands every caller the same object, so a single property assignment would have replaced a constant-time comparison or a size cap in every module at once.
40
+
41
+ 0.5.25 gives a key that cannot sign a way to prove possession. `pki.crmf.build` emits a `POPOPrivKey` proof under `keyEncipherment` or `keyAgreement`, so an ML-KEM certificate request has a proof to carry instead of no option at all, and a request whose proof requires it announces cmp2021(3) rather than a version its own reader refuses.
42
+
43
+ The framework calls `pki.crl.sign`, `pki.key.export`, `pki.webcrypto.subtle` and the WebAuthn verification surface. None of those changed shape, so nothing changes for callers going through `b.mtlsCa` or `b.auth.passkey`. The ACME requirement is a breaking change for code calling the vendored toolkit directly; the toolkit's own `MIGRATING.md` carries the route. **Fixed:** *A label longer than 63 octets is no longer a domain* — RFC 1035 §2.3.4 bounds a label at 63 octets and the whole name at 253. `canonicalDomain` enforced the second and not the first, so `<64 a's>.com` came back as a canonical domain and `b.publicSuffix.organizationalDomain` handed it to the caller — a name that cannot be put on the wire, and one the caller cannot tell apart from a name that can.
44
+
45
+ That is the argument the 253-octet check was already making. Its own comment describes the failure as handing back a name that "cannot be put on the wire, and a caller cannot tell", which is exactly as true of a label. The rest of the framework — `b.guardDomain`, `b.guardEmail`, `b.mailAuth` — enforces both bounds; this was the one place that enforced one and not the other.
46
+
47
+ Measured on the CONVERTED form for the same reason the total is: a 30-character internationalized label is well inside 63 as typed and can exceed it as an `xn--` A-label.
48
+
49
+ `b.mailAuth` had been compensating with its own per-label loop, and the comment there argued a label cap was a DNS wire rule rather than a naming one. That did not survive the observation that the 253 cap is equally a wire rule and was enforced anyway. The loop is gone; the bound now applies wherever a domain is canonicalized rather than at the one caller that remembered. · *A character that cannot appear in a host is no longer a domain* — `domainToASCII` is a UTS #46 MAPPER, not a hostname validator. It passes through characters that no DNS name may contain, so `ex*ample.com`, `ex(ample.com`, `ex'ample.com` and `ex"ample.com` all came back from `canonicalDomain` looking canonical — 17 of the 31 non-LDH characters tested. A caller then compared one against a cookie domain, an SPF record or a residency table.
50
+
51
+ A canonical domain is now letters, digits, hyphens and dots, checked on the converted form where the question has one answer: an internationalized label has already become its `xn--` spelling by then.
52
+
53
+ Underscore is deliberately still accepted. `_dmarc`, `_domainkey`, `_acme-challenge` and `_sip._tcp` are real DNS names, and whether to accept a service label is a policy question the framework already answers elsewhere — `b.guardDomain`'s `underscorePolicy` refuses it at strict and allows it at permissive. `canonicalDomain` normalizes a name; it does not judge one.
54
+
55
+ The check composes `b.codepointClass.isRunOf` rather than hand-rolling a character loop, so the set reads as the rule it enforces and the range table is built once. **Detectors:** *The gates that read source with comments removed now share one lexer, and it reads regular expressions* — Checks that assert a construct is PRESENT read the source with comments stripped, because a check that finds its construct in a commented-out line concludes it is there and stays silent — the state it exists to catch. Two gates did that stripping, each its own way, and each lost source the other kept.
56
+
57
+ One removed comments with two pattern replaces. Those cannot tell a comment from the same characters inside a string: an `Accept` header of `*/*` contains the two characters `/*`, which opened a block comment that ran to the next `*/` anywhere in the file and deleted an audit emission from the scan. That gate checks every emitted namespace is registered, so anything it cannot see is a namespace that goes unregistered with nothing reporting it.
58
+
59
+ The other tracked strings but not regular expressions, so a literal holding `//` read as a line comment. Across `lib/` that changed what the gates read for 101 files, in both directions: `/^curl\//i` in a blocked-agent list took the rest of its line with it, and elsewhere a quote inside a pattern desynchronized the scan far enough that whole comment blocks survived into what the gates then read as code.
60
+
61
+ Both now call one implementation, beside the tokenizer that resolves the same ambiguity for the detector matchers. · *Stripping comments may not change whether a file parses* — Deciding whether a slash opens a pattern or divides means naming what may precede one, and that list is open-ended — the regex-literal gate was rewritten for exactly that reason, and the same list rebuilt here was missing a different entry each time it was checked: a postfix increment, a control-flow header, a statement block, an object literal, `break`, a non-ASCII identifier, a function expression, a trailing-dot number.
62
+
63
+ So the list is no longer what the gates rest on. Removing comments cannot change whether source parses, and anything deleted beyond a comment takes syntax with it. Every file under `lib/` and `test/` is now compiled twice — as written, and with comments removed — and the two must agree. Alongside it, three generated sweeps ask the same question of the language rather than of this codebase: every reserved word, and every block-bearing construct, must survive a pattern written after it — and every word must be read in the role its POSITION gives it rather than the one it spells. That last one matters because a word is not always the keyword it looks like: after a dot it is a property name, so `o.default / 2` divides, and after `break` or `continue` it is a label that ends the statement, so a pattern on the next line opens rather than divides.
64
+
65
+ What that covers is exact. It catches a slash read as opening a pattern where it divided, which deletes code and stops the file parsing. The opposite mistake leaves a real comment behind and still parses, so it is pinned by cases instead, and it is bounded — a pattern scan stops at the newline, where a phantom block comment runs to the next `*/` or to the end of the file.
66
+
67
+ It is not a check that passes by looking nowhere: the stripper as it stood before it lexed patterns breaks parsing in ten `lib/` files, and the check reports if its own walk reaches fewer files than the tree holds, if a generated fixture stops being valid source, or if a case no longer contains the text it searches for. · *A removed comment leaves a separator only where the tokens would fuse* — A block comment can be the only thing between two tokens. Deleting it outright joins them: `key/* note */in obj` becomes `keyin obj`, and `a +/* note */+b` becomes `a ++b` — different programs, and the second is not one at all.
68
+
69
+ A separator now goes in exactly where the pair would fuse: two words, or two punctuation characters that spell one of the language's multi-character operators. Everywhere else nothing is inserted, because the checks match source shapes and a space between a name and its parenthesis makes a pattern stop matching — which is a miss, and misses are silent. **References:** [RFC 1035 §2.3.4 — size limits](https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4) · [UTS #46 — Unicode IDNA Compatibility Processing](https://www.unicode.org/reports/tr46/)
70
+
11
71
  - v0.18.44 (2026-08-20) — **Four things every consumer was writing itself, and one of them was a wrong timestamp in the audit chain.** A Markdown renderer, a country-code check, a shared-secret header gate and a monotonic clock. Each was being hand-rolled downstream, and in each case the copies got the same detail wrong: author text reaching HTML unescaped, `UK` accepted as a country code, a length check running after a constant-time compare that throws on a length mismatch, and a timestamp that moves backwards when NTP corrects the clock. The last one was not only a downstream problem — `b.chainWriter` had it too, and it made a compliance export refuse itself. **Added:** *`b.time.monotonicNow()` and `b.time.monotonicClock(opts?)`* — A clock whose `now()` never returns a value less than or equal to the one before it. `monotonicNow()` is the shared process-wide sequence; `monotonicClock(opts)` builds an isolated one.
12
72
 
13
73
  The four-line version of this is easy to write and leaves out three things. `opts.source` makes the clock testable, which a closure over `Date.now` is not. `observeFloor(ms)` seeds the guarantee from a value read back out of storage, so it survives a restart or failover — precisely when a fresh node is syncing NTP and a backwards step is most likely, and precisely when a process-memory floor has just reset to zero. And `maxDriftMs` (default one second) bounds how far ahead of the underlying source a burst may walk the returned value.
package/NOTICE CHANGED
@@ -68,7 +68,7 @@ Used for: FIPS 203 ML-KEM (ml_kem_512 / ml_kem_768 / ml_kem_1024),
68
68
  reference implementation.
69
69
  --------------------------------------------------------------------------------
70
70
  Component: @blamejs/pki
71
- Version: 0.5.23
71
+ Version: 0.5.25
72
72
  Source: https://github.com/blamejs/pki
73
73
  License: Apache-2.0
74
74
  Copyright: Copyright (c) blamejs contributors
package/README.md CHANGED
@@ -323,7 +323,7 @@ All runtime dependencies are committed to the repo — no transitive npm install
323
323
  | [`@noble/hashes`](https://github.com/paulmillr/noble-hashes) | 2.3.0 | [Paul Miller](https://github.com/paulmillr) | Browser (ESM) build only — SHAKE256 / SHA-3 / SHA-2 / HMAC / HKDF for the client half of a hybrid exchange. The server side reaches all of these through `node:crypto`, so there is no server bundle |
324
324
  | [`@noble/curves`](https://github.com/paulmillr/noble-curves) | 2.3.0 (bundles @noble/hashes 2.3.0) | [Paul Miller](https://github.com/paulmillr) | RFC 9497 Oblivious Pseudo-Random Function (OPRF / VOPRF / POPRF) over ristretto255 / P-256 / P-384 / P-521, behind `b.crypto.oprf` |
325
325
  | [`@noble/post-quantum`](https://github.com/paulmillr/noble-post-quantum) | 0.7.0 (bundles @noble/hashes, @noble/curves, @noble/ciphers 2.3.0) | [Paul Miller](https://github.com/paulmillr) | Pure-JS FIPS 203 ML-KEM (`ml_kem_512` / `ml_kem_768` / `ml_kem_1024`), FIPS 204 ML-DSA (`ml_dsa_44/65/87`), FIPS 205 SLH-DSA (`slh_dsa_*`). First-class on both server-side and client-side via `b.pqcSoftware` — security-first defaults pin to the highest cat-5 levels (ML-KEM-1024, ML-DSA-87, SLH-DSA-SHAKE-256f); interoperable with Node's built-in WebCrypto ML-KEM that `b.crypto.encrypt` / `b.middleware.apiEncrypt` use. A browser (ESM) build ships beside it carrying the KEM suites only — a client half encapsulates and does not sign |
326
- | [`@blamejs/pki`](https://github.com/blamejs/pki) | 0.5.23 | [blamejs](https://github.com/blamejs) | Zero-dependency pure-JS X.509 / CRL / PKCS#12 / CSR / CMS toolkit backing `b.mtlsCa` — ML-DSA-87 (FIPS 204) post-quantum + ECDSA-P384 cert signing, PBMAC1 PKCS#12 packaging, chain validation (no openssl CLI) — and the WebAuthn attestation / assertion verification behind `b.auth.passkey` |
326
+ | [`@blamejs/pki`](https://github.com/blamejs/pki) | 0.5.25 | [blamejs](https://github.com/blamejs) | Zero-dependency pure-JS X.509 / CRL / PKCS#12 / CSR / CMS toolkit backing `b.mtlsCa` — ML-DSA-87 (FIPS 204) post-quantum + ECDSA-P384 cert signing, PBMAC1 PKCS#12 packaging, chain validation (no openssl CLI) — and the WebAuthn attestation / assertion verification behind `b.auth.passkey` |
327
327
  | [`SecLists` 10k-most-common.txt](https://github.com/danielmiessler/SecLists/blob/master/Passwords/Common-Credentials/10k-most-common.txt) | master snapshot | [Daniel Miessler / SecLists contributors](https://github.com/danielmiessler/SecLists) (CC-BY-3.0) | Top-10000 common-password dictionary read by `b.auth.password.policy()` for the NIST 800-63B §5.1.1.2 "previously breached" check |
328
328
  | [`prismjs`](https://prismjs.com/) | 1.30.0 | [Lea Verou + contributors](https://github.com/PrismJS/prism) | Syntax highlighting in the example wiki's code blocks (browser-side) |
329
329
 
package/lib/mail-auth.js CHANGED
@@ -1184,9 +1184,10 @@ async function _fetchDmarcRecord(domain, dnsLookup) {
1184
1184
  // eight queries however long it is. That is a denial-of-service guard on the
1185
1185
  // RECEIVER, not a nicety — the sender picks the domain.
1186
1186
  // RFC 1035 §2.3.4 — the wire form of a name is at most 253 octets, and a single
1187
- // label at most 63.
1187
+ // label at most 63. The per-label bound is enforced by canonicalDomain, so only
1188
+ // the qname total is measured here: `_dmarc.` is PREPENDED after
1189
+ // canonicalization and can push a name that was inside 253 past it.
1188
1190
  var DMARC_MAX_QNAME_OCTETS = 253;
1189
- var DNS_MAX_LABEL_OCTETS = 63;
1190
1191
  var DMARC_TREE_WALK_MAX_LABELS = 8;
1191
1192
  var DMARC_TREE_WALK_LABEL_FLOOR = 7;
1192
1193
 
@@ -1224,18 +1225,15 @@ function _dmarcAuthorDomainLabels(domain) {
1224
1225
  // both sides end up in one canonical form.
1225
1226
  var d = publicSuffix.canonicalDomain(String(domain));
1226
1227
  if (!d) return null;
1227
- // RFC 1035 §2.3.4 bounds a LABEL at 1..63 octets as well as the whole name at
1228
- // 253, and `canonicalDomain` enforces only the second it is the framework's
1229
- // definition of a domain NAME, and a label cap is a DNS wire rule rather than
1230
- // a naming one. Checking it here keeps the answer the same whichever resolver
1231
- // is wired in: the default one refuses an over-long label as `dns/bad-host`
1232
- // and the evaluation temperrors, while an operator's own `dnsLookup` would
1233
- // answer for it and let a policy apply to a name that cannot exist.
1234
- var labels = d.split(".");
1235
- for (var i = 0; i < labels.length; i += 1) {
1236
- if (labels[i].length === 0 || labels[i].length > DNS_MAX_LABEL_OCTETS) return null;
1237
- }
1238
- return labels;
1228
+ // Both RFC 1035 §2.3.4 bounds 63 octets per label and 253 for the whole
1229
+ // name are enforced by `canonicalDomain` itself, so an over-long or empty
1230
+ // label has already returned "" above. This function used to re-check the
1231
+ // label bound because canonicalDomain enforced only the total; the comment
1232
+ // here argued a label cap was a DNS wire rule rather than a naming one, which
1233
+ // did not survive the observation that the 253 cap is equally a wire rule and
1234
+ // was enforced there anyway. Fixing it at the definition means every caller
1235
+ // gets it, not just this one.
1236
+ return d.split(".");
1239
1237
  }
1240
1238
 
1241
1239
  // The ordered list of names the walk queries, starting with the domain itself.
@@ -59,6 +59,7 @@
59
59
  */
60
60
 
61
61
  var nodeUrl = require("node:url");
62
+ var codepointClass = require("./codepoint-class");
62
63
  var vendorData = require("./vendor-data");
63
64
  var pslDataModule = require("./vendor/public-suffix-list.data");
64
65
  var { PublicSuffixError } = require("./framework-error");
@@ -99,6 +100,32 @@ function _firstNonHostCharacter(name) {
99
100
  return -1;
100
101
  }
101
102
 
103
+ // The first character of an already-CONVERTED name that a domain may not
104
+ // contain, or -1.
105
+ //
106
+ // Run after `domainToASCII`, where the question has one answer: an
107
+ // internationalized label has become its `xn--` form by then, so a real name is
108
+ // letters, digits, hyphens and dots. `domainToASCII` is a UTS #46 MAPPER, not a
109
+ // hostname validator, and passes through 17 of the 31 non-LDH characters
110
+ // tested — `ex*ample.com`, `ex(ample.com` and `ex"ample.com` all came back as
111
+ // canonical domains, and a caller then compared one against a cookie domain, an
112
+ // SPF record or a residency table.
113
+ //
114
+ // Underscore is permitted. `_dmarc`, `_domainkey`, `_acme-challenge` and
115
+ // `_sip._tcp` are real DNS names, and whether to accept a service label is a
116
+ // POLICY question the framework already answers elsewhere — guardDomain's
117
+ // `underscorePolicy` refuses it at strict and allows it at permissive. This
118
+ // function normalizes a name; it does not judge one.
119
+ // Composed rather than hand-rolled: codepointClass owns the framework's
120
+ // character-set membership, caches the range table per distinct set, and reads
121
+ // as the rule it enforces. A charCodeAt loop here would be a fourth copy of a
122
+ // shape three files already have.
123
+ var _DOMAIN_CHARS = codepointClass.ASCII_ALNUM + "-._";
124
+
125
+ function _isLdhDomain(ascii) {
126
+ return codepointClass.isRunOf(ascii, _DOMAIN_CHARS, 1, ascii.length);
127
+ }
128
+
102
129
  // The characters UTS #46 treats as a label separator, and therefore the ones
103
130
  // that can mark the root of an absolute name. Scanned against Node's own
104
131
  // mapping and found to be exactly this set across the BMP and the SMP.
@@ -172,6 +199,14 @@ function _normalizeInput(domain) {
172
199
  throw _err("public-suffix/invalid-domain",
173
200
  "publicSuffix: domain contains empty label");
174
201
  }
202
+ // LDH, checked on the CONVERTED form — see _firstNonLdhCharacter for why the
203
+ // question only has one answer here.
204
+ if (!_isLdhDomain(ascii)) {
205
+ throw _err("public-suffix/invalid-domain",
206
+ "publicSuffix: domain contains a character that is not a letter, digit, " +
207
+ "hyphen, dot or underscore; domainToASCII MAPS such a name without " +
208
+ "validating it as a host, so `ex*ample.com` arrives here looking canonical");
209
+ }
175
210
  // RFC 1035 §2.3.4 — 253 octets max for the wire form (255 minus the leading
176
211
  // length byte and the root's null). This is the AUTHORITATIVE check, and it
177
212
  // has to run on the converted name: an internationalized label grows into its
@@ -190,6 +225,28 @@ function _normalizeInput(domain) {
190
225
  "publicSuffix: domain exceeds 253-octet RFC 1035 limit once converted " +
191
226
  "to A-labels (" + withoutRoot + " octets)");
192
227
  }
228
+ // RFC 1035 §2.3.4 bounds a LABEL at 63 octets as well as the whole name at
229
+ // 253, and only the second was checked. The argument is the one written above
230
+ // for the total: a 64-octet label cannot be put on the wire, and a caller
231
+ // handed one back cannot tell it apart from a name that can — this function
232
+ // returned `<64 a's>.com` as an organizational domain, and every resolver
233
+ // would refuse it. The rest of the framework already enforces both bounds
234
+ // (b.guardDomain, b.guardEmail, b.mailAuth); this was the one place that
235
+ // enforced one and not the other.
236
+ //
237
+ // Measured on the CONVERTED form for the same reason the total is: a 30-
238
+ // character internationalized label is well under 63 going in and can exceed
239
+ // it as an `xn--` A-label. The root marker, if still present, is not a label.
240
+ var labelSpan = ascii.slice(0, withoutRoot);
241
+ var labels = labelSpan.split(".");
242
+ for (var li = 0; li < labels.length; li += 1) {
243
+ if (labels[li].length > 63) {
244
+ throw _err("public-suffix/invalid-domain",
245
+ "publicSuffix: label " + JSON.stringify(labels[li].slice(0, 16) + "...") +
246
+ " is " + labels[li].length + " octets once converted to an A-label, over " +
247
+ "the 63-octet RFC 1035 limit");
248
+ }
249
+ }
193
250
  // The root marker is stripped here when it was not an ASCII dot on the way
194
251
  // in. UTS #46 maps U+3002, U+FF0E and U+FF61 to ".", so `münchen.example。`
195
252
  // arrives with no trailing dot and leaves the conversion with one. Returning
@@ -21,7 +21,7 @@
21
21
  "server": "sha256:f3325f480cb8eb814fcb0baaa19336cbbf2b993f48624c6aa9600ffd69d0be5e",
22
22
  "browser": "sha256:0ffd91540bcb586a29b56e52ee1c29df69097b50776beb4036a07558f7a4e12e"
23
23
  },
24
- "refreshedAt": "2026-08-21T09:57:09.089Z"
24
+ "refreshedAt": "2026-08-22T03:19:31.188Z"
25
25
  },
26
26
  "@noble/hashes": {
27
27
  "version": "2.3.0",
@@ -48,7 +48,7 @@
48
48
  "hashes": {
49
49
  "browser": "sha256:dfe4b7ae3c9880e388c8da4b68f44742b229b53afacd1e674179527e33da62b0"
50
50
  },
51
- "refreshedAt": "2026-08-21T09:57:09.089Z"
51
+ "refreshedAt": "2026-08-22T03:19:31.188Z"
52
52
  },
53
53
  "@noble/curves": {
54
54
  "version": "2.3.0",
@@ -65,12 +65,12 @@
65
65
  "server": "lib/vendor/noble-curves.cjs"
66
66
  },
67
67
  "bundler": "esbuild --format=cjs --platform=node",
68
- "bundledAt": "2026-08-06T00:00:00Z",
68
+ "bundledAt": "2026-08-21T00:00:00Z",
69
69
  "cpe": "cpe:2.3:a:paulmillr:noble-curves:2.3.0:*:*:*:*:node.js:*:*",
70
70
  "hashes": {
71
71
  "server": "sha256:b5fe88d1ea780d0581dee6145d666f89d46fc9531b5db35db2e5b16627840890"
72
72
  },
73
- "refreshedAt": "2026-08-21T09:57:09.089Z",
73
+ "refreshedAt": "2026-08-22T03:19:31.188Z",
74
74
  "components": {
75
75
  "@noble/hashes": {
76
76
  "url": "https://github.com/paulmillr/noble-hashes",
@@ -114,7 +114,7 @@
114
114
  "server": "sha256:fab7ebe5737793862c473444f4ee5912f79dd1edec86683acbb4eecbca0f5892",
115
115
  "browser": "sha256:cae1d5bbdc7184b202b6ca68df6e1db7b0d0f668c77809ded189ca7f271accc9"
116
116
  },
117
- "refreshedAt": "2026-08-21T09:57:09.089Z",
117
+ "refreshedAt": "2026-08-22T03:19:31.188Z",
118
118
  "components": {
119
119
  "@noble/hashes": {
120
120
  "url": "https://github.com/paulmillr/noble-hashes",
@@ -148,7 +148,7 @@
148
148
  },
149
149
  "runtime_artifact": "lib/vendor/common-passwords-top-10000.data.js",
150
150
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
151
- "refreshedAt": "2026-08-21T09:57:09.089Z"
151
+ "refreshedAt": "2026-08-22T03:19:31.188Z"
152
152
  },
153
153
  "bimi-trust-anchors": {
154
154
  "version": "operator-managed",
@@ -173,7 +173,7 @@
173
173
  },
174
174
  "runtime_artifact": "lib/vendor/bimi-trust-anchors.data.js",
175
175
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
176
- "refreshedAt": "2026-08-21T09:57:09.089Z"
176
+ "refreshedAt": "2026-08-22T03:19:31.188Z"
177
177
  },
178
178
  "publicsuffix-list": {
179
179
  "version": "master",
@@ -193,10 +193,10 @@
193
193
  },
194
194
  "runtime_artifact": "lib/vendor/public-suffix-list.data.js",
195
195
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
196
- "refreshedAt": "2026-08-21T09:57:09.089Z"
196
+ "refreshedAt": "2026-08-22T03:19:31.188Z"
197
197
  },
198
198
  "@blamejs/pki": {
199
- "version": "0.5.23",
199
+ "version": "0.5.25",
200
200
  "license": "Apache-2.0",
201
201
  "author": "blamejs",
202
202
  "source": "https://github.com/blamejs/pki",
@@ -217,11 +217,11 @@
217
217
  },
218
218
  "bundler": "esbuild --format=cjs --platform=node --external:crypto --external:node:crypto",
219
219
  "bundledAt": "2026-08-21T00:00:00Z",
220
- "cpe": "cpe:2.3:a:blamejs:pki:0.5.23:*:*:*:*:node.js:*:*",
220
+ "cpe": "cpe:2.3:a:blamejs:pki:0.5.25:*:*:*:*:node.js:*:*",
221
221
  "hashes": {
222
- "server": "sha256:3e365ca7e4cd0a7d80b20690cb8901c31d1a9e841d95f73674f97e011466f7ed"
222
+ "server": "sha256:4e64654dfddb16742f615f24a4e8892286495d94ae29597cdc3e988122f3ef4a"
223
223
  },
224
- "refreshedAt": "2026-08-21T09:57:09.089Z"
224
+ "refreshedAt": "2026-08-22T03:19:31.188Z"
225
225
  }
226
226
  }
227
227
  }