@blamejs/blamejs-shop 0.5.21 → 0.5.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +1 -1
  3. package/SECURITY.md +7 -1
  4. package/lib/addresses.js +16 -2
  5. package/lib/affiliates.js +3 -3
  6. package/lib/announcement-bar.js +2 -2
  7. package/lib/api-keys.js +2 -2
  8. package/lib/asset-manifest.json +1 -1
  9. package/lib/blog-articles.js +2 -2
  10. package/lib/carrier-accounts.js +2 -2
  11. package/lib/catalog-drafts.js +1 -1
  12. package/lib/category-navigation.js +6 -3
  13. package/lib/compliance-export.js +8 -1
  14. package/lib/customer-notes.js +1 -1
  15. package/lib/customer-roles.js +1 -1
  16. package/lib/customer-surveys.js +2 -2
  17. package/lib/customers.js +1 -1
  18. package/lib/dispute-resolution.js +3 -3
  19. package/lib/dropship-forwarding.js +1 -1
  20. package/lib/dunning.js +1 -1
  21. package/lib/email-ab-tests.js +1 -1
  22. package/lib/email-campaigns.js +15 -2
  23. package/lib/email-warmup.js +1 -1
  24. package/lib/externaldb-d1.js +0 -8
  25. package/lib/gift-options.js +3 -3
  26. package/lib/knowledge-base.js +3 -3
  27. package/lib/line-gift-wrap.js +6 -2
  28. package/lib/live-chat.js +1 -1
  29. package/lib/notifications.js +1 -1
  30. package/lib/operator-accounts.js +1 -1
  31. package/lib/operator-help-center.js +3 -3
  32. package/lib/operator-inbox.js +2 -2
  33. package/lib/operator-roles.js +1 -1
  34. package/lib/order-notes.js +2 -2
  35. package/lib/order-ratings.js +3 -3
  36. package/lib/payment-methods.js +1 -1
  37. package/lib/payment-retries.js +1 -1
  38. package/lib/pixel-events.js +14 -6
  39. package/lib/plan-changes.js +1 -1
  40. package/lib/product-qa.js +1 -1
  41. package/lib/push-notifications.js +2 -2
  42. package/lib/reviews.js +3 -3
  43. package/lib/seller-signup.js +4 -4
  44. package/lib/sidebar-widgets.js +1 -1
  45. package/lib/sms-dispatcher.js +3 -3
  46. package/lib/stock-receipts.js +1 -1
  47. package/lib/storefront-dashboards.js +1 -1
  48. package/lib/storefront-forms.js +1 -1
  49. package/lib/storefront-pages.js +2 -2
  50. package/lib/subscription-billing.js +1 -1
  51. package/lib/subscription-controls.js +1 -1
  52. package/lib/suggestion-box.js +4 -4
  53. package/lib/support-tickets.js +3 -3
  54. package/lib/tenants.js +1 -1
  55. package/lib/text-guard.js +44 -2
  56. package/lib/theme-assets.js +1 -1
  57. package/lib/vendor/MANIFEST.json +12 -12
  58. package/lib/vendor/blamejs/CHANGELOG.md +32 -0
  59. package/lib/vendor/blamejs/NOTICE +1 -1
  60. package/lib/vendor/blamejs/README.md +1 -1
  61. package/lib/vendor/blamejs/lib/network-tls.js +98 -13
  62. package/lib/vendor/blamejs/lib/vendor/MANIFEST.json +12 -12
  63. package/lib/vendor/blamejs/lib/vendor/blamejs-pki.cjs +568 -304
  64. package/lib/vendor/blamejs/package.json +1 -1
  65. package/lib/vendor/blamejs/sbom.cdx.json +6 -6
  66. package/lib/vendors.js +3 -3
  67. package/lib/webhook-receiver.js +1 -1
  68. package/lib/webhook-subscriptions.js +1 -1
  69. package/lib/wishlist-sharing.js +1 -1
  70. package/package.json +1 -1
@@ -8,6 +8,38 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.18.x
10
10
 
11
+ - v0.18.24 (2026-08-11) — **A scheduler test waited for the wrong task, so a loaded machine could fail the release pipeline on a race in the test rather than a defect in the framework.** The suite covering scheduled-task success and failure registered two tasks — one that returns and one that throws — then waited for the aggregate counters to show a fire and an error before asserting that the succeeding task had actually run. Both counters are totals across tasks, so the throwing task alone satisfied the wait. On an unloaded machine the two first-fires land together and it never mattered; on a loaded CI runner they separate far enough that the wait returned while the succeeding task had not run yet, and the assertion failed.
12
+
13
+ The wait now uses the same counter the assertion does. Nothing in the shipped framework changes; this is the release pipeline being made to fail only for real reasons. **Fixed:** *The scheduler suite waits for the task it asserts on* — `aggregate.totalFires` and `aggregate.withErrors` both count across every registered task, so a wait for "at least one fire and at least one error" is satisfied by a single failing task. The assertion that followed checked a counter incremented inside the succeeding task's body, which that wait says nothing about.
14
+
15
+ This is the general shape worth naming: a poll-until-ready is only as good as the condition it polls, and a condition that is merely correlated with the assertion is a race waiting for a slow machine. The wait now polls the success counter itself, alongside the aggregate error count, so it cannot return before the thing being asserted is true.
16
+
17
+ - v0.18.23 (2026-08-10) — **A Certificate Transparency consistency proof was checked against the new tree but not against the tree you pinned — unless your pinned size happened to be a power of two.** `b.network.tls.ct.verifyConsistency` answers one question: is the tree I pinned earlier still a prefix of the tree this log is showing me now? RFC 9162 §2.1.4.2 answers it by rebuilding BOTH roots from the proof — the first tree's and the second's — and requiring both to match what the operator holds. The implementation rebuilt only the second. The pinned root reached the computation solely as a seed in the branch taken when the first tree is a complete subtree, so for a pinned size of 4, 8 or 16 it was checked by accident, and for 3, 5, 6, 7, 9 and every other size it was not looked at at all. A proof describing an entirely different first tree of the same size verified.
18
+
19
+ That is the whole premise of pinning, and it was being taken on trust. Both roots are now rebuilt and both are checked; a proof that does not connect to the pinned tree is refused with `first-root-mismatch`.
20
+
21
+ Two smaller defects came out of the same work. A tree is consistent with itself and RFC 9162 says so with an empty proof, which the walk refused for any size that was not a power of two — so an operator re-checking an unchanged STH was told their log was inconsistent. And a proof carrying entries beyond the root was accepted with the remainder ignored, where the sibling inclusion-proof walk refuses exactly that. **Changed:** *Vendored `@blamejs/pki` 0.4.13 → 0.4.14* — The PKI toolkit behind `b.mtlsCa` now wipes every key-establishment secret it allocates once that secret stops being needed, classical as well as post-quantum, and reaches raw key material through a single path that clears the copy it hands out. A password supplied as a string or `Uint8Array` is encoded into a buffer the toolkit allocated — a credential copy — and that copy is cleared once the key derivation has consumed it; a caller-supplied `Buffer` is borrowed and left untouched. Public API behaviour is unchanged. **Fixed:** *A tree is consistent with itself* — RFC 9162 §2.1.4 gives the proof for two equal tree sizes as the empty list. The verification walk in §2.1.4.2 is written for a first tree strictly smaller than the second, and running it on equal sizes made it look for a sibling the definition says is not there — so it refused. Not always: for a power-of-two size the index bits shift down to zero on their own and the walk short-circuits, so the failure appeared at 3, 5, 6, 7, 9 and not at 4, 8, 16.
22
+
23
+ An operator polling a log that has not grown since their last check got told the log was inconsistent, intermittently by tree size. A false alarm on a security check is worse than a quiet one, because it is the alarm that teaches you to ignore the alarm. Equal sizes now verify on an empty proof and refuse a non-empty one. · *A consistency proof must be fully consumed* — Entries past the point the walk stopped were ignored rather than refused, while the inclusion-proof walk beside it refuses an audit path with entries beyond the root. Nothing was forgeable through it — the computed root still had to match — but a proof carrying material the verifier never looked at is not the proof the operator believes was checked, and the two walkers disagreeing about what well-formed means is its own defect. Both now refuse a tail. **Security:** *The consistency proof must connect to the pinned first root* — A consistency check has two halves: the proof rebuilds the new tree's root, which must match the STH the log just served, and it rebuilds the old tree's root, which must match the STH the operator pinned. Only the first half was enforced.
24
+
25
+ The pinned root was used as the walk's seed when the first tree happened to be a complete subtree — true exactly when the pinned size is a power of two — and in that case the final root comparison carried it along. At every other size the walk seeded from the proof itself and the supplied root was never read. A log could serve a proof for some other first tree of the same size and it verified; so did a pinned root of arbitrary bytes.
26
+
27
+ The exposure is not that a log can forge a tree, but that pinning stopped detecting the thing it exists to detect: a log that quietly rewrote history before the point you pinned would still have passed, at 11 of the 16 first-sizes below 17. Both roots are now rebuilt per RFC 9162 §2.1.4.2 and both are checked, with a distinct `first-root-mismatch` reason so a proof that is well-formed but describes the wrong tree does not read as a malformed one. **Detectors:** *Merkle proofs are tested against real trees* — All three defects were reachable because the tests built their fixtures by hand: four-leaf trees with hand-picked sibling values. A four-leaf tree is a perfect binary tree, so it never takes the uneven split the algorithm turns on, and hand-picked values can be chosen to make the answer come out right without describing any tree at all — one fixture asserted the incomplete-subtree branch with a first root of thirty-two identical bytes, which is exactly the input the missing check would have caught.
28
+
29
+ The suite now transcribes RFC 9162's recursive definitions separately from the framework's iterative walk, builds real trees, derives real proofs, and feeds every first-size/second-size pair up to seventeen leaves through the verifier before breaking them one way at a time. The two implementations share no structure, so agreement means something. A detector fires on a re-hand-rolled tree hash.
30
+
31
+ - v0.18.22 (2026-08-10) — **An OCSP response that declared one signature algorithm and carried another was accepted — the field steering the digest was never checked against the key doing the verifying.** `b.network.tls.ocsp.evaluate` read the responder's `signatureAlgorithm` OID to pick a digest, then handed the bytes to `node:crypto`, which selects RSA or ECDSA from the KEY rather than from that field. A response declaring `sha256WithRSAEncryption` while carrying an ECDSA signature therefore verified, and so did the reverse.
32
+
33
+ This is not a forgery path and was never exploitable: the issuer's private key is required either way, and a response signed by any other key is still refused. What it meant is that the declared algorithm and the algorithm actually verified were allowed to differ, with nothing in the result saying so. RFC 6960 §4.2.1 gives that field one job; the verifier now holds it to it, and a mismatch is refused with `tls/ocsp-sig-alg-key-mismatch`.
34
+
35
+ It surfaced while covering the refusal paths of the revocation checker — the half of a security primitive that only matters when something is wrong, and the half that had the least coverage. **Fixed:** *The revocation checker's refusal paths are covered* — A revocation check earns its cost only by saying NO for every reason it should, and the accept path is the part that gets exercised. Each refusal now has a test driving the real `evaluate` call with a response that is well-formed in every respect except the one under test: `certStatus` revoked (with and without a CRLReason) and unknown; a `responseStatus` that is not successful; a ResponseData carrying no responses at all; a nonce that does not echo and one absent when required; a signature that does not verify and one made by the wrong key; a CertID under an unrecognised hash algorithm and one naming a different issuer than the certificate under validation; a SingleResponse with no thisUpdate.
36
+
37
+ Both encodings of the nonce extension are covered — the RFC 8954 raw form and the RFC 6960 double-wrapped one — because a verifier that reads only one scores a live responder's echo as a mismatch and refuses a good response. **Security:** *The OCSP signatureAlgorithm must agree with the issuer key* — The verifier mapped the response's `signatureAlgorithm` OID to a digest — `sha256WithRSAEncryption` to SHA-256, `ecdsa-with-SHA384` to SHA-384, and so on — and then called `node:crypto.verify(digest, tbs, issuerKey, signature)`. Node decides RSA versus ECDSA from the key object, so the OID's algorithm half was read and discarded. An EC-signed response declaring RSA verified; an RSA-signed response declaring ECDSA verified.
38
+
39
+ Nothing was forgeable through it. Producing a signature that verifies under the issuer key still requires that private key, and the release ships a test asserting that a response signed by a different key is refused. The defect is that a field the verifier depends on to choose the digest was never validated against the key it verified with, so what a response claimed and what was actually checked could differ silently.
40
+
41
+ `evaluate` now requires the OID family and the issuer key type to agree, and refuses with `tls/ocsp-sig-alg-key-mismatch` when they do not. Responders that declare their own algorithm — every conformant one — are unaffected. **Detectors:** *one OCSP response builder for the test suite* — An OCSP response is layers of nested DER, so each suite that needed one grew its own assembler — three of them, each reaching only the shapes that suite had thought of. Between them they covered the accept path, staleness and the CertID issuer binding, and could not express the signature-algorithm disagreement above at all: no builder had a knob for it. A shared builder makes a new refusal shape an argument rather than a fourth copy, and a detector fires on a re-hand-rolled one.
42
+
11
43
  - v0.18.21 (2026-08-10) — **An NTS or NTP query that cannot build its request left the datagram socket open — one leaked handle per failed query, for the life of the process.** `b.network.ntp.nts.querySingle` and `b.ntpCheck.querySingle` opened their UDP socket before building the request packet. Building it can fail — an AEAD suite the encoder does not recognise is the reachable case, and refusing it is the point — and the socket opened first had nobody left to close it once the exception left the Promise executor. The caller saw the right error either way, which is why this stayed invisible: nothing was wrong with the rejection, only with what it left behind. A scheduler polling a misconfigured time source accumulated one dead handle per attempt and never released any of them.
12
44
 
13
45
  Both now build the request first and open the socket only once every step that can throw has succeeded, so there is nothing to leak rather than something each new failure path has to remember to undo. A synchronous refusal from `send()` routes through the same close-then-settle path.
@@ -77,7 +77,7 @@ Used for: WebAuthn / passkey registration + authentication response
77
77
  server.cjs).
78
78
  --------------------------------------------------------------------------------
79
79
  Component: @blamejs/pki
80
- Version: 0.4.13
80
+ Version: 0.4.14
81
81
  Source: https://github.com/blamejs/pki
82
82
  License: Apache-2.0
83
83
  Copyright: Copyright (c) blamejs contributors
@@ -321,7 +321,7 @@ All runtime dependencies are committed to the repo — no transitive npm install
321
321
  | [`@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` |
322
322
  | [`@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 |
323
323
  | [`@simplewebauthn/server`](https://github.com/MasterKale/SimpleWebAuthn) | 13.3.2 | [Matthew Miller](https://github.com/MasterKale) | WebAuthn / passkey verification |
324
- | [`@blamejs/pki`](https://github.com/blamejs/pki) | 0.4.13 | [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) |
324
+ | [`@blamejs/pki`](https://github.com/blamejs/pki) | 0.4.14 | [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) |
325
325
  | [`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 |
326
326
  | [`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) |
327
327
 
@@ -1610,6 +1610,23 @@ function _verifyOcspSignature(parsed, issuerPem) {
1610
1610
  throw new TlsTrustError("tls/ocsp-bad-issuer-key",
1611
1611
  "issuer public key parse failed: " + ((e && e.message) || String(e)));
1612
1612
  }
1613
+ // RFC 6960 §4.2.1 — signatureAlgorithm identifies the algorithm the
1614
+ // responder used, and the digest above is derived from it. node:crypto picks
1615
+ // RSA or ECDSA from the KEY, not from that field, so a response declaring
1616
+ // one and carrying the other verifies without anyone noticing the
1617
+ // disagreement. That is not a forgery path — the issuer's private key is
1618
+ // required either way — but it means the field steering the digest was never
1619
+ // checked against the key doing the verifying. Require them to agree, so
1620
+ // what the response claims and what was actually verified are the same
1621
+ // thing.
1622
+ var wantKeyType = (algOid === OID_RSA_SHA256 || algOid === OID_RSA_SHA384 ||
1623
+ algOid === OID_RSA_SHA512) ? "rsa" : "ec";
1624
+ if (keyObj.asymmetricKeyType !== wantKeyType) {
1625
+ throw new TlsTrustError("tls/ocsp-sig-alg-key-mismatch",
1626
+ "OCSP signatureAlgorithm OID '" + algOid + "' declares a " + wantKeyType.toUpperCase() +
1627
+ " signature but the issuer key is " + String(keyObj.asymmetricKeyType).toUpperCase() +
1628
+ " (RFC 6960 §4.2.1 — the declared algorithm does not match the issuer key)");
1629
+ }
1613
1630
  // ECDSA OCSP signatures use DER-encoded ECDSA-Sig-Value (the ASN.1
1614
1631
  // shape that node:crypto.verify accepts by default — no dsaEncoding
1615
1632
  // option needed).
@@ -2831,13 +2848,19 @@ function _ctVerifyInclusionPath(leafHash, leafIndex, treeSize, auditPath) {
2831
2848
  // prefix. Returns the computed roots (oldRoot, newRoot) so the caller
2832
2849
  // can compare against the operator-supplied STHs.
2833
2850
  function _ctVerifyConsistencyPath(m, n, consistencyProof, firstHash) {
2834
- if (typeof m !== "number" || m < 1 || Math.floor(m) !== m) {
2851
+ // isFinite carries the weight here: Math.floor(Infinity) === Infinity, so an
2852
+ // integer check alone accepts a tree of infinite size. The walk used to
2853
+ // reject it downstream by running out of proof, but that was an accident of
2854
+ // the loop rather than a decision, and the equal-sizes shortcut below has no
2855
+ // loop to run out of. A tree size that is not a finite positive integer is
2856
+ // refused here, once, for every path through this function.
2857
+ if (typeof m !== "number" || !isFinite(m) || m < 1 || Math.floor(m) !== m) {
2835
2858
  throw new TlsTrustError("tls/ct-bad-first-size",
2836
- "ct.verifyConsistency: m (first tree size) must be a positive integer");
2859
+ "ct.verifyConsistency: m (first tree size) must be a finite positive integer");
2837
2860
  }
2838
- if (typeof n !== "number" || n < m || Math.floor(n) !== n) {
2861
+ if (typeof n !== "number" || !isFinite(n) || n < m || Math.floor(n) !== n) {
2839
2862
  throw new TlsTrustError("tls/ct-bad-second-size",
2840
- "ct.verifyConsistency: n (second tree size) must be an integer >= m");
2863
+ "ct.verifyConsistency: n (second tree size) must be a finite integer >= m");
2841
2864
  }
2842
2865
  if (!Buffer.isBuffer(firstHash) || firstHash.length !== 32) { // RFC 9162 SHA-256 digest length
2843
2866
  throw new TlsTrustError("tls/ct-bad-first-hash",
@@ -2847,11 +2870,34 @@ function _ctVerifyConsistencyPath(m, n, consistencyProof, firstHash) {
2847
2870
  throw new TlsTrustError("tls/ct-bad-consistency-proof",
2848
2871
  "ct.verifyConsistency: consistencyProof must be an array of Buffers");
2849
2872
  }
2850
- // RFC 9162 §2.1.4.2 algorithm is the same as the inclusion-proof
2851
- // walk, with the leaf-index seeded at the first-tree size minus 1 and
2852
- // the special case for m being a complete subtree.
2873
+ // A tree is consistent with itself, and RFC 9162 §2.1.4 says so with an
2874
+ // EMPTY proof: PROOF(m, D[m]) = SUBPROOF(m, D[m], true) = {}. The walk below
2875
+ // is §2.1.4.2, which is written for a first tree strictly smaller than the
2876
+ // second — run on m === n it looks for a sibling that the definition says is
2877
+ // not there. That refused a conformant log's answer whenever the size was
2878
+ // not a power of two (for a power of two the index bits happen to shift down
2879
+ // to zero and the walk short-circuits), so an operator re-checking a pinned
2880
+ // STH against an unchanged one was told the log was inconsistent. A false
2881
+ // alarm on this check is worse than a quiet one: it is the alarm that
2882
+ // teaches an operator to ignore it.
2883
+ if (m === n) {
2884
+ if (consistencyProof.length !== 0) {
2885
+ throw new TlsTrustError("tls/ct-consistency-not-empty",
2886
+ "ct.verifyConsistency: the proof for two equal tree sizes must be empty " +
2887
+ "(RFC 9162 §2.1.4), got " + consistencyProof.length + " entries");
2888
+ }
2889
+ return firstHash;
2890
+ }
2891
+ // RFC 9162 §2.1.4.2 — the walk rebuilds BOTH roots from the same proof: the
2892
+ // first tree's root (fr) and the second's (sr). Both must be checked. Only
2893
+ // sr was, and firstHash reached the computation solely as the seed in the
2894
+ // complete-subtree branch below — so whenever the pinned size was NOT a
2895
+ // power of two, the operator's pinned root was never looked at, and a proof
2896
+ // for some OTHER first tree of the same size verified. That is the whole
2897
+ // premise of pinning an STH ("the tree I pinned is a prefix of the tree you
2898
+ // are showing me"), and it was being taken on trust.
2853
2899
  var path = consistencyProof.slice();
2854
- var node;
2900
+ var firstNode, secondNode;
2855
2901
  var fn = m - 1;
2856
2902
  var sn = n - 1;
2857
2903
  // Walk past the right-side bits — the consistency proof omits the
@@ -2860,14 +2906,15 @@ function _ctVerifyConsistencyPath(m, n, consistencyProof, firstHash) {
2860
2906
 
2861
2907
  if (fn === 0) {
2862
2908
  // m was a complete subtree — its root is the firstHash itself.
2863
- node = firstHash;
2909
+ firstNode = firstHash;
2864
2910
  } else {
2865
2911
  if (path.length === 0) {
2866
2912
  throw new TlsTrustError("tls/ct-consistency-empty",
2867
2913
  "ct.verifyConsistency: consistency proof empty but first tree is not a complete subtree");
2868
2914
  }
2869
- node = path.shift();
2915
+ firstNode = path.shift();
2870
2916
  }
2917
+ secondNode = firstNode;
2871
2918
  while (sn > 0) {
2872
2919
  if (path.length === 0) {
2873
2920
  throw new TlsTrustError("tls/ct-consistency-short",
@@ -2879,15 +2926,34 @@ function _ctVerifyConsistencyPath(m, n, consistencyProof, firstHash) {
2879
2926
  "ct.verifyConsistency: consistency-proof entry is not a 32-byte Buffer");
2880
2927
  }
2881
2928
  if ((fn & 1) === 1 || fn === sn) {
2882
- node = _ctInnerHashFinal(sibling, node);
2929
+ firstNode = _ctInnerHashFinal(sibling, firstNode);
2930
+ secondNode = _ctInnerHashFinal(sibling, secondNode);
2883
2931
  while ((fn & 1) === 0 && fn !== 0) { fn >>>= 1; sn >>>= 1; }
2884
2932
  } else {
2885
- node = _ctInnerHashFinal(node, sibling);
2933
+ secondNode = _ctInnerHashFinal(secondNode, sibling);
2886
2934
  }
2887
2935
  fn >>>= 1;
2888
2936
  sn >>>= 1;
2889
2937
  }
2890
- return node;
2938
+ // The rebuilt first root must be the one the operator pinned, or the proof
2939
+ // connects their second root to a first tree they never saw.
2940
+ if (firstNode.length !== firstHash.length ||
2941
+ !bCrypto.timingSafeEqual(firstNode, firstHash)) {
2942
+ throw new TlsTrustError("tls/ct-first-root-mismatch",
2943
+ "ct.verifyConsistency: the proof rebuilds a first-tree root that is not the one supplied " +
2944
+ "(RFC 9162 §2.1.4.2 — the proof does not connect the pinned tree to the new one)");
2945
+ }
2946
+ // Everything the peer sent must have been consumed. Its sibling walk above
2947
+ // refuses an audit path with entries beyond the root for the same reason:
2948
+ // a proof carrying material the verifier never looked at is not the proof
2949
+ // the operator believes was checked, and silently ignoring the remainder
2950
+ // means the two walkers disagree about what a well-formed proof is.
2951
+ if (path.length !== 0) {
2952
+ throw new TlsTrustError("tls/ct-consistency-long",
2953
+ "ct.verifyConsistency: consistency proof has " + path.length +
2954
+ " trailing entries beyond the second-tree root");
2955
+ }
2956
+ return secondNode;
2891
2957
  }
2892
2958
 
2893
2959
  function _findSctOid(rawDer) {
@@ -3087,6 +3153,15 @@ var ct = Object.freeze({
3087
3153
  consistency: consistencyResult };
3088
3154
  }
3089
3155
  } catch (e) {
3156
+ // A proof that rebuilds the wrong first tree is a different answer
3157
+ // from a malformed one, and it is the answer that matters most here:
3158
+ // it says the log's history before the pinned point does not match
3159
+ // what was pinned. Composing the consistency check inside an
3160
+ // inclusion check must not flatten that into "malformed proof".
3161
+ if (e && e.code === "tls/ct-first-root-mismatch") {
3162
+ return { valid: false, reason: "first-root-mismatch",
3163
+ error: e.message || String(e) };
3164
+ }
3090
3165
  return { valid: false, reason: "consistency-walk-failed",
3091
3166
  error: (e && e.message) || String(e) };
3092
3167
  }
@@ -3128,6 +3203,16 @@ var ct = Object.freeze({
3128
3203
  computed = _ctVerifyConsistencyPath(opts.firstSize, opts.secondSize,
3129
3204
  opts.proof || [], firstRoot);
3130
3205
  } catch (e) {
3206
+ // A first root that does not rebuild is a distinct answer from a
3207
+ // malformed proof: the proof is well-formed, it just describes a
3208
+ // different tree than the one the operator pinned.
3209
+ // `e` is already proven truthy by the guard on this branch, so the
3210
+ // usual (e && e.message) shape would be dead here; the fallback below
3211
+ // keeps it because a throw of a falsy value still reaches that one.
3212
+ if (e && e.code === "tls/ct-first-root-mismatch") {
3213
+ return { valid: false, reason: "first-root-mismatch",
3214
+ error: e.message || String(e) };
3215
+ }
3131
3216
  return { valid: false, reason: "consistency-walk-failed",
3132
3217
  error: (e && e.message) || String(e) };
3133
3218
  }
@@ -21,7 +21,7 @@
21
21
  "server": "sha256:f3325f480cb8eb814fcb0baaa19336cbbf2b993f48624c6aa9600ffd69d0be5e",
22
22
  "browser": "sha256:0ffd91540bcb586a29b56e52ee1c29df69097b50776beb4036a07558f7a4e12e"
23
23
  },
24
- "refreshedAt": "2026-08-10T18:39:35.028Z"
24
+ "refreshedAt": "2026-08-11T00:05:07.579Z"
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-10T18:39:35.028Z"
51
+ "refreshedAt": "2026-08-11T00:05:07.579Z"
52
52
  },
53
53
  "@noble/curves": {
54
54
  "version": "2.3.0",
@@ -70,7 +70,7 @@
70
70
  "hashes": {
71
71
  "server": "sha256:b5fe88d1ea780d0581dee6145d666f89d46fc9531b5db35db2e5b16627840890"
72
72
  },
73
- "refreshedAt": "2026-08-10T18:39:35.028Z",
73
+ "refreshedAt": "2026-08-11T00:05:07.579Z",
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-10T18:39:35.028Z",
117
+ "refreshedAt": "2026-08-11T00:05:07.579Z",
118
118
  "components": {
119
119
  "@noble/hashes": {
120
120
  "url": "https://github.com/paulmillr/noble-hashes",
@@ -151,7 +151,7 @@
151
151
  "hashes": {
152
152
  "server": "sha256:e83195dc9f189385da9c856ef38843f4466f93ea8f3d7fc2efcb1e1b18da6f20"
153
153
  },
154
- "refreshedAt": "2026-08-10T18:39:35.028Z"
154
+ "refreshedAt": "2026-08-11T00:05:07.579Z"
155
155
  },
156
156
  "SecLists-common-passwords-top-10000": {
157
157
  "version": "10k-most-common (master)",
@@ -171,7 +171,7 @@
171
171
  },
172
172
  "runtime_artifact": "lib/vendor/common-passwords-top-10000.data.js",
173
173
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
174
- "refreshedAt": "2026-08-10T18:39:35.028Z"
174
+ "refreshedAt": "2026-08-11T00:05:07.579Z"
175
175
  },
176
176
  "bimi-trust-anchors": {
177
177
  "version": "operator-managed",
@@ -196,7 +196,7 @@
196
196
  },
197
197
  "runtime_artifact": "lib/vendor/bimi-trust-anchors.data.js",
198
198
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
199
- "refreshedAt": "2026-08-10T18:39:35.028Z"
199
+ "refreshedAt": "2026-08-11T00:05:07.579Z"
200
200
  },
201
201
  "publicsuffix-list": {
202
202
  "version": "master",
@@ -216,10 +216,10 @@
216
216
  },
217
217
  "runtime_artifact": "lib/vendor/public-suffix-list.data.js",
218
218
  "integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
219
- "refreshedAt": "2026-08-10T18:39:35.028Z"
219
+ "refreshedAt": "2026-08-11T00:05:07.579Z"
220
220
  },
221
221
  "@blamejs/pki": {
222
- "version": "0.4.13",
222
+ "version": "0.4.14",
223
223
  "license": "Apache-2.0",
224
224
  "author": "blamejs",
225
225
  "source": "https://github.com/blamejs/pki",
@@ -240,11 +240,11 @@
240
240
  },
241
241
  "bundler": "esbuild --format=cjs --platform=node --external:crypto --external:node:crypto",
242
242
  "bundledAt": "2026-08-10T00:00:00Z",
243
- "cpe": "cpe:2.3:a:blamejs:pki:0.4.13:*:*:*:*:node.js:*:*",
243
+ "cpe": "cpe:2.3:a:blamejs:pki:0.4.14:*:*:*:*:node.js:*:*",
244
244
  "hashes": {
245
- "server": "sha256:067ac05899ef04f7851a8d010dc4289e7a01706d5fd2cdc1ce59860631bba284"
245
+ "server": "sha256:96a873b733c95b53b8e2ced065a82192b6acabcede312cf68a45a128857cc3bc"
246
246
  },
247
- "refreshedAt": "2026-08-10T18:39:35.028Z"
247
+ "refreshedAt": "2026-08-11T00:05:07.579Z"
248
248
  }
249
249
  }
250
250
  }