@blamejs/blamejs-shop 0.1.38 → 0.2.1

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 (64) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/asset-manifest.json +3 -3
  3. package/lib/storefront.js +94 -21
  4. package/lib/vendor/MANIFEST.json +2 -2
  5. package/lib/vendor/blamejs/CHANGELOG.md +10 -0
  6. package/lib/vendor/blamejs/README.md +1 -1
  7. package/lib/vendor/blamejs/SECURITY.md +2 -1
  8. package/lib/vendor/blamejs/api-snapshot.json +2 -2
  9. package/lib/vendor/blamejs/lib/archive-read.js +17 -0
  10. package/lib/vendor/blamejs/lib/auth/fal.js +12 -0
  11. package/lib/vendor/blamejs/lib/auth/jwt-external.js +15 -11
  12. package/lib/vendor/blamejs/lib/auth/jwt.js +2 -2
  13. package/lib/vendor/blamejs/lib/auth/oauth.js +7 -6
  14. package/lib/vendor/blamejs/lib/auth/oid4vci.js +3 -3
  15. package/lib/vendor/blamejs/lib/auth/saml.js +15 -12
  16. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -3
  17. package/lib/vendor/blamejs/lib/calendar.js +9 -2
  18. package/lib/vendor/blamejs/lib/circuit-breaker.js +5 -4
  19. package/lib/vendor/blamejs/lib/crypto-hpke.js +1 -1
  20. package/lib/vendor/blamejs/lib/crypto-oprf.js +2 -2
  21. package/lib/vendor/blamejs/lib/crypto.js +2 -2
  22. package/lib/vendor/blamejs/lib/framework-error.js +2 -1
  23. package/lib/vendor/blamejs/lib/guard-filename.js +90 -5
  24. package/lib/vendor/blamejs/lib/guard-jwt.js +3 -2
  25. package/lib/vendor/blamejs/lib/guard-smtp-command.js +2 -2
  26. package/lib/vendor/blamejs/lib/mail-auth.js +2 -2
  27. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +1 -1
  28. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +7 -7
  29. package/lib/vendor/blamejs/lib/mail-crypto.js +1 -1
  30. package/lib/vendor/blamejs/lib/mail-dav.js +5 -4
  31. package/lib/vendor/blamejs/lib/mail-deploy.js +3 -2
  32. package/lib/vendor/blamejs/lib/mail-server-imap.js +1 -1
  33. package/lib/vendor/blamejs/lib/mail-server-jmap.js +1 -1
  34. package/lib/vendor/blamejs/lib/mail-server-managesieve.js +1 -1
  35. package/lib/vendor/blamejs/lib/mail-server-mx.js +142 -47
  36. package/lib/vendor/blamejs/lib/mail-server-submission.js +3 -3
  37. package/lib/vendor/blamejs/lib/mail-store.js +2 -2
  38. package/lib/vendor/blamejs/lib/mail.js +2 -2
  39. package/lib/vendor/blamejs/lib/network-tls.js +10 -7
  40. package/lib/vendor/blamejs/lib/safe-decompress.js +8 -6
  41. package/lib/vendor/blamejs/lib/safe-ical.js +12 -12
  42. package/lib/vendor/blamejs/lib/safe-mime.js +6 -6
  43. package/lib/vendor/blamejs/lib/safe-sieve.js +1 -1
  44. package/lib/vendor/blamejs/lib/safe-smtp.js +1 -1
  45. package/lib/vendor/blamejs/package.json +1 -1
  46. package/lib/vendor/blamejs/release-notes/v0.13.10.json +44 -0
  47. package/lib/vendor/blamejs/release-notes/v0.13.11.json +27 -0
  48. package/lib/vendor/blamejs/release-notes/v0.13.12.json +36 -0
  49. package/lib/vendor/blamejs/release-notes/v0.13.13.json +18 -0
  50. package/lib/vendor/blamejs/release-notes/v0.13.9.json +27 -0
  51. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +76 -0
  52. package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +4 -4
  53. package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +31 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +104 -7
  55. package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +26 -0
  56. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +5 -5
  57. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +1 -1
  58. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +166 -1
  59. package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-cluster.test.js +45 -27
  60. package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +2 -2
  61. package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +12 -12
  62. package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-exactly-once.test.js +15 -9
  63. package/lib/vendor/blamejs/test/layer-0-primitives/websocket-channels.test.js +0 -6
  64. package/package.json +1 -1
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.10",
4
+ "date": "2026-05-27",
5
+ "headline": "Documented-but-inert options wired up, a non-existent CVE reference removed, and a silent iCalendar cap-bypass fixed",
6
+ "summary": "A sweep for places where a documented option or citation did not match what the code does. The most operator-relevant fix: b.calendar.fromIcal documented a safeIcalOpts option that forwards parser caps (byte size, RRULE limits, nesting depth) to b.safeIcal.parse, but the value was never forwarded — so an operator who set tight caps through it got the default profile instead, silently. That is corrected; the nested options now reach the parser. b.archive.read.zip documented an AbortSignal option that was never honored; it now aborts the read at the entry boundary. b.auth.fal documented a bearerOnly alias that had no effect; it now forces the no-proof-of-possession path and refuses the contradictory combination of bearerOnly:true with a holder-of-key binding. Separately, the auth verification paths cited CVE-2026-23993 (13 places) for the \"reject an unknown alg before key lookup\" guard — that CVE id does not exist (the registry has no record of it); the citation is replaced with the weakness class (CWE-347 / CWE-757) and the real, verifiable neighboring CVEs. The circuit-breaker error-code note that promised a rename \"in v0.10\" is corrected to the actual plan (v1.0), and the build gate that catches overdue version promises now also catches two-part version numbers.",
7
+ "sections": [
8
+ {
9
+ "heading": "Fixed",
10
+ "items": [
11
+ {
12
+ "title": "`b.calendar.fromIcal` now forwards `safeIcalOpts` to the parser",
13
+ "body": "The documented `safeIcalOpts` option (parser caps: max bytes, RRULE COUNT/BYxxx limits, nesting depth) was not being passed to `b.safeIcal.parse` — when supplied under the documented nested key it was silently ignored and the parser ran with its default profile. Both forms now reach the parser: the documented nested `{ safeIcalOpts: { ... } }` and the top-level `{ profile, ... }` that earlier releases accepted, with the nested form winning on conflict. No caller regresses."
14
+ },
15
+ {
16
+ "title": "`b.archive.read.zip` honors the documented `signal` (AbortSignal)",
17
+ "body": "The `signal` option was documented but never read. A large or slow archive read can now be aborted cooperatively — the reader checks the signal at each entry boundary (`inspect`, `entries`, `extractEntries`, `extract`) and rejects with an `archive-read/aborted` error."
18
+ },
19
+ {
20
+ "title": "Removed a non-existent CVE reference from the JWT/JWE verification paths",
21
+ "body": "The \"reject an unknown/unsupported `alg` before any key lookup\" guard in `b.auth.jwt.verifyExternal`, `b.auth.oauth.verifyIdToken`, `b.auth.oid4vci`, and `b.auth.sd-jwt-vc` cited a CVE id that the registry has no record of. The behaviour is unchanged; the citation is now the weakness class it defends (CWE-347 improper signature verification / CWE-757 algorithm downgrade) alongside the real, verifiable alg-confusion / JWE-bypass CVEs already cited beside it."
22
+ }
23
+ ]
24
+ },
25
+ {
26
+ "heading": "Changed",
27
+ "items": [
28
+ {
29
+ "title": "`b.auth.fal` `bearerOnly` is now a real alias and refuses contradictions",
30
+ "body": "`bearerOnly: true` now forces the no-proof-of-possession path (equivalent to `hokBinding: null`), as documented. Passing `bearerOnly: true` together with a non-null `hokBinding` is a contradictory assurance request and is now refused at the call rather than silently resolved one way."
31
+ }
32
+ ]
33
+ },
34
+ {
35
+ "heading": "Detectors",
36
+ "items": [
37
+ {
38
+ "title": "Overdue-version-promise gate now catches two-part version numbers",
39
+ "body": "The build gate that flags a deferral whose promised landing version has already shipped previously matched only three-part versions (`vN.N.N`); a two-part promise (`vN.N`) slipped past it. It now matches both. The `b.circuitBreaker` `CIRCUIT_OPEN` error-code note that pointed at a passed version is corrected to its actual plan (rename at v1.0, with a deprecation warning a minor ahead)."
40
+ }
41
+ ]
42
+ }
43
+ ]
44
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.11",
4
+ "date": "2026-05-27",
5
+ "headline": "Test-suite reliability: replaced fixed-delay waits in the rate-limiter and scheduler suites with condition polling",
6
+ "summary": "No runtime behaviour changes. The rate-limiter, scheduler, and websocket-channel test suites waited for asynchronous work to settle by draining a fixed number of event-loop ticks before asserting. Under heavily parallel CI that budget was occasionally too short, so an assertion read state before the async work (a cluster-backend counter update, a scheduler tick-claim) had landed — an intermittent failure unrelated to the code under test. Those waits now poll the observable condition (helpers.waitUntil) and exit as soon as it holds, with a generous upper bound, so they pass quickly on fast machines and reliably under load. A build gate is added so the fixed-tick-drain shape cannot be reintroduced.",
7
+ "sections": [
8
+ {
9
+ "heading": "Fixed",
10
+ "items": [
11
+ {
12
+ "title": "Flaky fixed-budget waits in the rate-limiter / scheduler / sandbox test suites made contention-tolerant",
13
+ "body": "The rate-limit-cluster and scheduler-exactly-once suites drained a fixed count of event-loop ticks before asserting on asynchronously-updated state; under contended CI the budget could expire before the work settled, producing intermittent failures. They now wait on the actual observable condition (a written response, a settled counter). The sandbox suite's success-path cases gave the worker a 5 s execution budget that cold worker-thread startup under heavily parallel Windows CI could just exceed; those are raised to the framework's 10 s ceiling. Affects test code only — no change to shipped framework behaviour. The unused tick-drain helper in the websocket-channel suite was removed."
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "heading": "Detectors",
19
+ "items": [
20
+ {
21
+ "title": "Build gate rejects the fixed-tick-drain wait shape in tests",
22
+ "body": "A new test-suite lint rule flags the counted microtask/tick-drain idiom (reassigning a promise to its own `.then()` in a loop to wait a fixed number of ticks), the sibling of the existing fixed-`setTimeout`-sleep rule. A single event-loop yield is unaffected; only the drain-as-wait shape is rejected, directing the wait to condition polling instead."
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.12",
4
+ "date": "2026-05-27",
5
+ "headline": "Inbound MX listener now runs the connection-level gate cascade it documented — HELO identity, DNS blocklist, and greylisting",
6
+ "summary": "b.mail.server.mx.create documented helo / rbl / greylist gate options, but the listener never invoked them — an operator who wired them got silent acceptance of mail those gates would have rejected. They are now wired into the live SMTP state machine: the HELO-identity gate evaluates at HELO/EHLO and refuses a spoofed or malformed identity with 550; the DNS-blocklist gate evaluates the connecting IP once per connection and refuses a listed source with 554; the greylisting gate defers a first-seen (ip, sender, recipient) tuple with a 450 tempfail so legitimate senders retry and pass. Each gate is skipped when the operator doesn't wire it. Because these gates do DNS and store lookups, the per-connection command pump was reworked to process commands asynchronously and strictly in arrival order, so pipelined commands (RFC 2920) cannot overtake a gate still resolving and the existing SMTP-smuggling and STARTTLS-stripping defenses are unchanged. The message-authentication gate (SPF/DKIM/DMARC alignment via b.guardEnvelope) needs the inbound SPF + DKIM verification results as inputs; that inbound-auth pipeline lands as a follow-up, and the documentation no longer implies that gate is active today.",
7
+ "sections": [
8
+ {
9
+ "heading": "Added",
10
+ "items": [
11
+ {
12
+ "title": "HELO-identity / RBL / greylist gates wired into `b.mail.server.mx`",
13
+ "body": "When wired, `opts.helo` (FCrDNS / HELO-shape / self-name checks) refuses a bad HELO identity at HELO/EHLO with 550; `opts.rbl` refuses a connecting IP found on a DNS blocklist with 554 (evaluated once per connection); `opts.greylist` defers a first-seen (ip, sender, recipient) tuple with 450 4.7.1. Their verdicts surface on the `rcpt_to` event (`rblListed`, `greylist`) and the `helo` event (`heloVerdict`), with dedicated `helo_gate_refused` / `rbl_refused` / `greylist_deferred` audit events. A gate the operator doesn't supply is skipped, never synthesized."
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "heading": "Changed",
19
+ "items": [
20
+ {
21
+ "title": "MX command pump processes commands asynchronously and in arrival order",
22
+ "body": "Gate evaluation involves DNS and store lookups, so the per-connection command pump now awaits each command before the next. Pipelined commands are serialized so a gate resolving cannot let a later command answer ahead of an earlier one; reply ordering, the bare-LF SMTP-smuggling refusal, and the STARTTLS-stripping defense are unchanged. No change to the listener's external behaviour when no gates are wired."
23
+ }
24
+ ]
25
+ },
26
+ {
27
+ "heading": "Deprecated",
28
+ "items": [
29
+ {
30
+ "title": "SPF/DKIM/DMARC-alignment gate documentation corrected to match what is active",
31
+ "body": "The `envelope` (SPF/DKIM/DMARC alignment) and `dmarc` gate options were documented as wireable but require inbound SPF + DKIM verification results the listener does not yet produce. They are removed from the documented option set until the inbound-authentication pipeline (composing `b.mail.spf` + `b.mail.dmarc` + DKIM verification) lands; run those checks on the delivered message via the agent handoff in the meantime."
32
+ }
33
+ ]
34
+ }
35
+ ]
36
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.13",
4
+ "date": "2026-05-27",
5
+ "headline": "Archive extraction-path verification now refuses Windows reserved names, NTFS data streams, and trailing-dot/space per segment",
6
+ "summary": "b.guardFilename.verifyExtractionPath (the per-entry gate b.archive.read.zip.extract / b.safeArchive run on every extracted file) checked traversal, absolute paths, drive-letter and UNC prefixes, null bytes, PATH_MAX overflow, and realpath containment — but not the per-segment Windows write-target hazards the disk validate / sanitize paths already reject. An archive entry named CON, NUL.txt, subdir/LPT1, file.txt:hidden, or secret.txt. stayed inside the extraction root, so the containment and realpath checks passed it, yet on Windows it would resolve to a device, write a hidden NTFS stream, or (after Windows strips the trailing dot/space) overwrite a sibling file. These are now refused: any path segment that collides with a Windows reserved device name, uses NTFS alternate-data-stream syntax (name:stream), or carries a trailing dot or leading/trailing whitespace. The checks are platform-unconditional — a verifier running on Linux still refuses names that are only dangerous on the Windows host that ultimately extracts the archive — with a per-check opt-out (reservedNamePolicy / adsPolicy / leadingTrailingPolicy: \"allow\") for Linux-only targets.",
7
+ "sections": [
8
+ {
9
+ "heading": "Security",
10
+ "items": [
11
+ {
12
+ "title": "`verifyExtractionPath` refuses per-segment Windows extraction hazards (reserved names / NTFS ADS / trailing dot-space)",
13
+ "body": "Closes a within-root write-target-redirection gap: an extracted entry could stay inside the destination yet, on Windows, resolve to a device (`CON` / `NUL` / `COM1` / `LPT1`), write a hidden alternate data stream (`file.txt:payload`), or overwrite a sibling after Windows strips a trailing dot/space (`config.`). The verification gate now rejects all three per path segment. Refusal is platform-unconditional (the verifier may run on a different OS than the extractor); set `reservedNamePolicy` / `adsPolicy` / `leadingTrailingPolicy` to `\"allow\"` to opt a check out on a Linux-only target. Single-entry, name-only residuals — 8.3 short-name aliasing, case-insensitive cross-entry collisions, and archive symlink/hardlink entry-target validation — remain the extract orchestrator's responsibility (it owns the case-folded seen-set and the link-target gate)."
14
+ }
15
+ ]
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.9",
4
+ "date": "2026-05-26",
5
+ "headline": "Corrected CVE citations in source threat annotations + a build gate that refuses malformed CVE identifiers",
6
+ "summary": "Several source-comment threat annotations cited CVE identifiers that were rejected by the numbering authority (never assigned to a real issue), attributed to the wrong product, or structurally malformed (a placeholder with a non-numeric sequence). The annotated defenses are unchanged — every cap, refusal, and constant-time comparison behaves exactly as before; only the reference labels were corrected, each to a verifiable CVE or to the underlying weakness class (CWE / RFC) where no single CVE fits. Notable corrections: the S/MIME SHA-1 / MD5 certificate-signature refusal now cites the SHAttered collision and RFC 8551 §2.5 instead of a rejected candidate id; decompression-output caps cite CWE-409 and CVE-2025-0725 instead of a fabricated placeholder; the iCalendar RRULE / nesting / byte caps describe the calendar-bomb recursion-DoS class instead of an unrelated SSRF advisory; and the SAML signature-wrapping (XSW) defense now cites the actively-exploited CVE-2024-45409 (ruby-saml, CVSS 10.0) and CVE-2025-25291 / -25292 that the duplicate-element refusal defeats. A new build-time detector refuses any CVE token whose sequence number is not all-numeric, so a placeholder identifier can never reach a release again.",
7
+ "sections": [
8
+ {
9
+ "heading": "Fixed",
10
+ "items": [
11
+ {
12
+ "title": "Corrected rejected / misattributed / malformed CVE references in source threat annotations",
13
+ "body": "Threat-annotation comments across the mail, crypto, auth, guard, and safe modules carried CVE identifiers that were rejected by the CVE numbering authority, attributed to the wrong product, or written as non-numeric placeholders. Each was corrected to a verifiable CVE or to the weakness class (CWE / RFC) it defends. No runtime behaviour changed — the defenses these comments describe are unchanged. The S/MIME certificate check's SHA-1 / MD5 refusal message now names the SHAttered collision and RFC 8551 §2.5; the SAML XSW defense now names CVE-2024-45409 and CVE-2025-25291 / -25292."
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "heading": "Detectors",
19
+ "items": [
20
+ {
21
+ "title": "`malformed-cve-identifier` — refuses structurally-invalid CVE tokens at build time",
22
+ "body": "A CVE identifier's sequence number is always numeric (`CVE-<year>-<digits>`). The new detector refuses any CVE token whose post-year segment contains a letter — the placeholder shape that lets a fabricated reference slip past review. It cannot verify that a well-formed id is real or correctly attributed (that stays a review responsibility), but it makes the structurally-invalid class impossible to ship."
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ }
@@ -125,6 +125,49 @@ function testVerifyExtractionPathRefusals() {
125
125
  check("verifyExtractionPath: 4 refusals", refusals === 4);
126
126
  }
127
127
 
128
+ // Per-segment Windows-extraction hazards — refused even though they stay
129
+ // inside the extraction root (within-root write-target redirection /
130
+ // collision that the containment + realpath checks can't see). Platform-
131
+ // unconditional, with per-check opt-outs for Linux-only targets.
132
+ function testVerifyExtractionPathWindowsHazards() {
133
+ function expectCode(name, code, opts) {
134
+ var e = null;
135
+ try { b.guardFilename.verifyExtractionPath(name, "/tmp", opts); }
136
+ catch (err) { e = err; }
137
+ check("verifyExtractionPath refuses " + JSON.stringify(name) + " (" + code + ")",
138
+ e && (e.code || "").indexOf(code) !== -1);
139
+ }
140
+ // Windows reserved device names (bare + with extension + nested segment).
141
+ expectCode("CON", "filename.extraction-reserved-name");
142
+ expectCode("aux.txt", "filename.extraction-reserved-name");
143
+ expectCode("subdir/NUL", "filename.extraction-reserved-name");
144
+ expectCode("logs/COM1.log", "filename.extraction-reserved-name");
145
+ // Superscript-digit COM/LPT spoof (U+00B9/B2/B3 — Windows folds to 1/2/3).
146
+ // Built from codepoints so the test source stays pure-ASCII.
147
+ expectCode("COM" + String.fromCharCode(0xB9), "filename.extraction-reserved-name");
148
+ expectCode("sub/LPT" + String.fromCharCode(0xB3), "filename.extraction-reserved-name");
149
+ // NTFS alternate data streams.
150
+ expectCode("file.txt:evil.exe", "filename.extraction-ntfs-ads");
151
+ expectCode("dir/data.bin:$DATA", "filename.extraction-ntfs-ads");
152
+ // Trailing dot / leading-or-trailing whitespace (Windows strips → collision).
153
+ expectCode("secret.txt.", "filename.extraction-leading-trailing");
154
+ expectCode("name with trailing space ", "filename.extraction-leading-trailing");
155
+
156
+ // Opt-outs accept the name (then pass the realpath leg into a real root).
157
+ function expectAccept(name, opts) {
158
+ var dest = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-vx-"));
159
+ var ok = false, code = null;
160
+ try { ok = b.guardFilename.verifyExtractionPath(name, dest, opts).indexOf(dest) === 0; }
161
+ catch (e) { code = e && e.code; }
162
+ finally { fs.rmSync(dest, { recursive: true, force: true }); }
163
+ check("verifyExtractionPath accepts " + JSON.stringify(name) + " with opt-out" +
164
+ (code ? " (threw " + code + ")" : ""), ok === true);
165
+ }
166
+ expectAccept("CON", { reservedNamePolicy: "allow" });
167
+ expectAccept("file.txt:evil.exe", { adsPolicy: "allow" });
168
+ expectAccept("secret.txt.", { leadingTrailingPolicy: "allow" });
169
+ }
170
+
128
171
  async function testExtractRefusesOverwrite() {
129
172
  // Codex P1 on v0.12.7 PR #158 — the catch-block cleanup deleted
130
173
  // PRE-EXISTING destination files on abort because the rename-onto-
@@ -272,15 +315,48 @@ async function testExtractEntriesInMemory() {
272
315
  check("tar extractEntries: binary matches", tcollected["sub/y.dat"].equals(Buffer.from([9, 8, 7])));
273
316
  }
274
317
 
318
+ // opts.signal (AbortSignal) is documented on b.archive.read.zip — verify
319
+ // it actually aborts the read at the entry boundary rather than being a
320
+ // dead doc opt.
321
+ async function testSignalAbort() {
322
+ var z = b.archive.zip();
323
+ z.addFile("a.txt", "one\n");
324
+ z.addFile("b.txt", "two\n");
325
+ var bytes = z.toBuffer();
326
+
327
+ var ac = new AbortController();
328
+ ac.abort(); // already aborted before any read
329
+ var reader = b.archive.read.zip(b.archive.adapters.buffer(bytes), { signal: ac.signal });
330
+
331
+ var inspectThrew = null;
332
+ try { await reader.inspect(); } catch (e) { inspectThrew = e; }
333
+ check("zip inspect honors an aborted signal",
334
+ inspectThrew && (inspectThrew.code || "").indexOf("archive-read/aborted") !== -1);
335
+
336
+ var extractThrew = null;
337
+ try { for await (var _e of reader.extractEntries()) { void _e; } }
338
+ catch (e) { extractThrew = e; }
339
+ check("zip extractEntries honors an aborted signal",
340
+ extractThrew && (extractThrew.code || "").indexOf("archive-read/aborted") !== -1);
341
+
342
+ // Sanity: with no signal the same reader works.
343
+ var ok = b.archive.read.zip(b.archive.adapters.buffer(bytes));
344
+ var n = 0;
345
+ for await (var e2 of ok.extractEntries()) { void e2; n += 1; }
346
+ check("zip extractEntries with no signal still yields entries", n === 2);
347
+ }
348
+
275
349
  async function run() {
276
350
  await testRoundTripExtract();
277
351
  await testExtractEntriesInMemory();
352
+ await testSignalAbort();
278
353
  testSafeArchiveErrorClass();
279
354
  await testSafeArchiveInspect();
280
355
  await testZipBombPolicy();
281
356
  testEntryTypePolicy();
282
357
  testVerifyExtractionPathHappy();
283
358
  testVerifyExtractionPathRefusals();
359
+ testVerifyExtractionPathWindowsHazards();
284
360
  await testExtractRefusesOverwrite();
285
361
  await testSafeArchiveRefusesTrustedStreamSource();
286
362
  await testGuardArchiveInspect();
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * CVE-2026-22817 — alg/kty confusion (RS256→HS256, ES256↔RSA, etc.)
6
6
  * CVE-2026-23552 — cross-realm JWT acceptance (constant-time iss)
7
- * CVE-2026-23993 — unknown-alg paths skipping verify
7
+ * Alg-allowlist gate (CWE-347 / CWE-757) — unknown-alg paths skipping verify
8
8
  *
9
9
  * Plus the larger AUTH-2…AUTH-36 findings closed in the same batch:
10
10
  *
@@ -137,7 +137,7 @@ async function testCrossRealmIssRefused() {
137
137
  threw && /iss-mismatch/.test(threw.code || ""));
138
138
  }
139
139
 
140
- // -------- CVE-2026-23993 — unknown alg refused before key lookup --------
140
+ // -------- Alg-allowlist gate — unknown alg refused before key lookup --------
141
141
 
142
142
  async function testUnknownAlgRefusedBeforeLookup() {
143
143
  // Caller's allowlist is ES256 only; token declares HS256. Verifier
@@ -156,8 +156,8 @@ async function testUnknownAlgRefusedBeforeLookup() {
156
156
  } catch (e) { threw = e; }
157
157
  // HS256 is in REFUSED_ALGS so the operator-allowlist refusal fires
158
158
  // FIRST; but the underlying mechanic — token alg refused before key
159
- // resolution — is what CVE-2026-23993 closes.
160
- check("CVE-2026-23993 — token alg refused before key lookup",
159
+ // resolution — is what the alg-allowlist gate (CWE-347 / CWE-757) closes.
160
+ check("alg-allowlist gate — token alg refused before key lookup",
161
161
  threw && /alg-not-allowed|refused-alg/.test(threw.code || ""));
162
162
  }
163
163
 
@@ -83,6 +83,36 @@ function testFromIcalRoundTrip() {
83
83
  check("toIcal preserves DTSTART", /DTSTART:20260522T090000/.test(back));
84
84
  }
85
85
 
86
+ function testFromIcalSafeIcalOptsForwarded() {
87
+ // @opts documents safeIcalOpts as forwarded to b.safeIcal.parse.
88
+ // Proof it actually reaches the parser: an unknown profile inside
89
+ // safeIcalOpts must surface safeIcal's bad-opt refusal. If the opts
90
+ // were dropped (or the whole outer opts passed instead), safeIcal
91
+ // would default to "strict" and never throw.
92
+ var ical =
93
+ "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//test//EN\r\n" +
94
+ "BEGIN:VEVENT\r\nUID:a@b\r\nDTSTAMP:20260521T100000Z\r\n" +
95
+ "DTSTART:20260522T090000\r\nDURATION:PT1H\r\nSUMMARY:x\r\n" +
96
+ "END:VEVENT\r\nEND:VCALENDAR\r\n";
97
+ var threw = null;
98
+ try { b.calendar.fromIcal(ical, { safeIcalOpts: { profile: "bogus" } }); }
99
+ catch (e) { threw = e; }
100
+ check("fromIcal forwards safeIcalOpts to safeIcal.parse (bad profile refused)",
101
+ threw && (threw.code || "").indexOf("safe-ical/bad-opt") !== -1);
102
+ // And a VALID nested profile parses cleanly (no accidental refusal).
103
+ var ev = b.calendar.fromIcal(ical, { safeIcalOpts: { profile: "balanced" } });
104
+ check("fromIcal honors a valid nested safeIcalOpts profile", ev && ev["@type"] === "Event");
105
+ // Backward-compat: the historically-working TOP-LEVEL parser-options
106
+ // form must keep working (no patch-release regression).
107
+ var threwTop = null;
108
+ try { b.calendar.fromIcal(ical, { profile: "bogus" }); }
109
+ catch (e) { threwTop = e; }
110
+ check("fromIcal still honors a top-level profile (no regression)",
111
+ threwTop && (threwTop.code || "").indexOf("safe-ical/bad-opt") !== -1);
112
+ var evTop = b.calendar.fromIcal(ical, { profile: "balanced" });
113
+ check("fromIcal parses with a valid top-level profile", evTop && evTop["@type"] === "Event");
114
+ }
115
+
86
116
  function testFromIcalNoVevent() {
87
117
  var threw = null;
88
118
  try { b.calendar.fromIcal("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//x//EN\r\nEND:VCALENDAR\r\n"); }
@@ -840,6 +870,7 @@ function run() {
840
870
  testValidateHappyPath();
841
871
  testValidateRefusalCases();
842
872
  testFromIcalRoundTrip();
873
+ testFromIcalSafeIcalOptsForwarded();
843
874
  testFromIcalNoVevent();
844
875
  testRrulePreserved();
845
876
  testExpandRecurrenceDaily();
@@ -566,7 +566,9 @@ function testNoStaleDefers() {
566
566
  function cmp(a, b) { for (var i = 0; i < 3; i += 1) { if ((a[i] || 0) !== (b[i] || 0)) return (a[i] || 0) - (b[i] || 0); } return 0; }
567
567
  // Promised-landing phrasings only ("X lands in vN" / "deferred to vN" / "not
568
568
  // supported in vN"). NOT "deferred FROM vN" (that is an origin, not a deadline).
569
- var PROMISE = /(?:deferred to|lands(?: in)?|will land(?: in)?|not supported in)\s+v?(\d+\.\d+\.\d+)/i;
569
+ // Accept 2-part (vN.N) AND 3-part (vN.N.N) promised-landing versions —
570
+ // a 2-part "v0.10" promise slipped past the old 3-part-only pattern.
571
+ var PROMISE = /(?:deferred to|lands(?: in)?|will land(?: in)?|not supported in)\s+v?(\d+\.\d+(?:\.\d+)?)/i;
570
572
  var matches = _scan(PROMISE, { skipComments: false });
571
573
  var overdue = [];
572
574
  matches.forEach(function (m) {
@@ -2259,6 +2261,10 @@ async function testNoDuplicateCodeBlocks() {
2259
2261
  files: ["lib/api-key.js:issue", "lib/db-query.js:<top>", "lib/session.js:create"],
2260
2262
  reason: "Generic JS array helper / lambda shape — Object.keys(...).map(fn) + similar functional idioms appearing in any code that walks a column-or-key list.",
2261
2263
  },
2264
+ {
2265
+ files: ["lib/guard-filename.js:verifyExtractionPath", "lib/hal.js:resource", "lib/vault-aad.js:_canonicalize"],
2266
+ reason: "v0.13.13 — coincidental token shingle of the generic split-then-walk-segments idiom (`x.split(sep); for (...) { var seg = ...; if (...) throw/continue }`). guard-filename verifyExtractionPath walks path components refusing per-segment Windows-extraction hazards (reserved names / NTFS-ADS / trailing-dot); hal.js:resource builds a HAL resource by walking link/embedded keys; vault-aad.js:_canonicalize canonicalizes AAD key-value segments. Three unrelated domains (path safety / hypermedia link assembly / crypto AAD canonicalization) — no shared behaviour to extract; the only commonality is the universal split-and-loop control-flow shape.",
2267
+ },
2262
2268
  {
2263
2269
  mode: "family-subset",
2264
2270
  files: [
@@ -5788,7 +5794,7 @@ var KNOWN_ANTIPATTERNS = [
5788
5794
  {
5789
5795
  // v0.10.15 — `zlib.gunzipSync` / `zlib.createGunzip` /
5790
5796
  // `zlib.brotliDecompress` without an output-size cap is the
5791
- // CVE-2025-0725 / CVE-2024-zlib decompression-amplification
5797
+ // CVE-2025-0725 / CWE-409 decompression-amplification
5792
5798
  // class. Attackers craft a kilobyte of compressed input that
5793
5799
  // explodes to gigabytes of output, exhausting memory before the
5794
5800
  // request handler sees the bytes. The defense is either the
@@ -5831,7 +5837,67 @@ var KNOWN_ANTIPATTERNS = [
5831
5837
  requires: /\bmaxOutputLength\b/,
5832
5838
  skipCommentLines: true,
5833
5839
  allowlist: [],
5834
- reason: "CVE-2025-0725 (libcurl + zlib decompression amplification) + CVE-2024-zlib bomb class. Every gunzip / brotli decompress on operator-supplied bytes MUST bound the output. Use `zlib.gunzipSync(buf, { maxOutputLength: <C.BYTES.* constant> })` so the operator sees the cap at config time; refusal becomes a typed error before the bomb reaches memory.",
5840
+ reason: "CVE-2025-0725 (libcurl + zlib decompression amplification) + CWE-409 (uncontrolled-resource decompression bomb) class. Every gunzip / brotli decompress on operator-supplied bytes MUST bound the output. Use `zlib.gunzipSync(buf, { maxOutputLength: <C.BYTES.* constant> })` so the operator sees the cap at config time; refusal becomes a typed error before the bomb reaches memory.",
5841
+ },
5842
+ {
5843
+ // Citation hygiene — a CVE identifier is always
5844
+ // CVE-<4-digit-year>-<sequence>, where the sequence is purely
5845
+ // numeric (CVE numbering spec). Two malformed shapes ship past
5846
+ // review: a non-numeric sequence (`CVE-2024-zlib` — a library
5847
+ // name dropped in as a placeholder), and a real id with a
5848
+ // hyphen-attached word (`CVE-2024-39687-class` — reads as if
5849
+ // `-class` is part of the id). The first is a fabricated
5850
+ // reference; the second is a parse hazard for any tool that
5851
+ // extracts CVE tokens. The annotation convention is a SPACE
5852
+ // before the descriptor (`CVE-2024-39687 class`), which leaves
5853
+ // the id token well-formed. This detector refuses both shapes:
5854
+ // a letter immediately after the year separator, OR a
5855
+ // hyphen-then-letter after the numeric sequence. It cannot
5856
+ // verify that a well-formed id is real or correctly attributed —
5857
+ // that stays a reviewer responsibility — but it makes the
5858
+ // structurally-invalid class impossible to ship. Real CVE
5859
+ // ranges (`CVE-2023-51764 / -51765`) and id-then-space-descriptor
5860
+ // forms pass unchanged.
5861
+ id: "malformed-cve-identifier",
5862
+ primitive: "cite a real CVE as CVE-<year>-<digits> (all-numeric sequence) then a SPACE before any descriptor — never a non-numeric sequence or a hyphen-attached word",
5863
+ regex: /CVE-[0-9]{4}-(?:[0-9]*[A-Za-z]|[0-9]+-[A-Za-z])/,
5864
+ allowlist: [],
5865
+ reason: "A CVE identifier's sequence number is always numeric and the token ends at the sequence (CVE-<year>-<digits>). A non-numeric sequence (CVE-2024-zlib) is a fabricated placeholder; a hyphen-attached word (CVE-2024-39687-class) makes the id un-parseable. Cite a verifiable CVE followed by a space before any class/descriptor word, or name the weakness class (CWE / RFC).",
5866
+ },
5867
+
5868
+ {
5869
+ // Codex P1 on v0.13.12 PR #234 — the MX listener's command pump was
5870
+ // made async (gates do DNS / store lookups), and the plaintext
5871
+ // socket.on("data") path was routed through a per-connection
5872
+ // serialization chain (`_feedChunk`). But the post-STARTTLS TLSSocket
5873
+ // fed `_ingestBytes` directly from a sync onData callback that ignored
5874
+ // the returned promise — so on the upgraded socket (where the default
5875
+ // strict/balanced profiles actually run the gates) async gate awaits
5876
+ // could overlap later TLS chunks and gate rejections went unhandled
5877
+ // instead of producing the 421 path. The invariant: the async command
5878
+ // pump (`_ingestBytes`) is fed ONLY through `_feedChunk` (the
5879
+ // `return _ingestBytes(...)` form), so every transport — plaintext and
5880
+ // TLS — shares the one serialized chain. A bare `_ingestBytes(` call
5881
+ // anywhere else is a second, un-serialized feed path. The lookbehind
5882
+ // exempts the function definition (`function _ingestBytes`) and the
5883
+ // single legitimate caller (`return _ingestBytes`); `_ingestBytes` is
5884
+ // unique to lib/mail-server-mx.js so this is effectively file-scoped.
5885
+ id: "mx-ingest-bytes-bypasses-feed-pump",
5886
+ primitive: "feed the MX command pump only via _feedChunk (`return _ingestBytes(...)`); never call _ingestBytes directly from a sync callback — it drops the async pump's promise and breaks command serialization + the 421 error path",
5887
+ regex: /(?<!function )(?<!return )\b_ingestBytes\s*\(/,
5888
+ allowlist: [
5889
+ // The submission listener has its OWN _ingestBytes, and it is
5890
+ // SYNCHRONOUS — its only async work (SASL AUTH) is handled via
5891
+ // internal .then() chains inside the command handler, and RFC 4954
5892
+ // §4 forbids clients pipelining commands across AUTH until the
5893
+ // response is received, so a sync pump is spec-acceptable there.
5894
+ // There is no async-promise to drop, so the bare `_ingestBytes(`
5895
+ // call is safe. This invariant guards the MX listener's async pump
5896
+ // specifically; if the submission pump is ever made async, route it
5897
+ // through a _feedChunk equivalent and drop this allowlist entry.
5898
+ "lib/mail-server-submission.js",
5899
+ ],
5900
+ reason: "Codex P1 on PR #234: the async MX command pump must be fed through the single per-connection serialization chain (_feedChunk → `return _ingestBytes`). A bare `_ingestBytes(` call from a sync callback (the original post-STARTTLS onData) ignores the returned promise, letting async HELO/RBL/greylist gate awaits overlap later chunks and turning gate rejections into unhandled rejections instead of the 421 path. Route every transport (plaintext + TLS) through _feedChunk.",
5835
5901
  },
5836
5902
 
5837
5903
  {
@@ -6444,9 +6510,10 @@ var KNOWN_ANTIPATTERNS = [
6444
6510
  reason: "CVE-2026-23552 — JWT iss comparisons against attacker-controlled payload values leak prefix-timing via `!==`. Every JWT verifier in the framework (oauth.verifyIdToken / jwt-external.verifyExternal / oauth.parseFrontchannelLogoutRequest / sd-jwt-vc.verify) routes through jwtExternal._issuerMatches for constant-time comparison. Detection is precise: `payload.iss !== ...` / `claims.iss !== ...` / `token.iss !== ...` is the JWT-verify-side shape. Non-JWT iss checks (e.g. discovery-document self-consistency where iss came from the same TLS-fetched body) are not in scope and don't match the regex.",
6445
6511
  },
6446
6512
  {
6447
- // CVE-2026-23993 accepting unknown JOSE alg values via a
6448
- // `switch (alg) { default: ... }` permissive default-branch is
6449
- // the canonical shape. Verifiers MUST throw in the default
6513
+ // Alg-allowlist gate (CWE-347 improper-sig-verification /
6514
+ // CWE-757 algorithm-downgrade) accepting unknown JOSE alg
6515
+ // values via a `switch (alg) { default: ... }` permissive
6516
+ // default-branch is the canonical shape. Verifiers MUST throw in the default
6450
6517
  // branch (no fall-through to a permissive "any signature"
6451
6518
  // path). The detector catches `switch (...alg)` (case-
6452
6519
  // insensitive) where the default branch returns/falls through
@@ -6462,7 +6529,7 @@ var KNOWN_ANTIPATTERNS = [
6462
6529
  // branch, so it doesn't match. Other auth files use
6463
6530
  // explicit if-cascades that throw, also not matched.
6464
6531
  ],
6465
- reason: "CVE-2026-23993 — JWT verifiers that accept unknown alg values via a permissive switch-default branch are the canonical bypass class. Every alg-dispatch primitive in the framework throws in the default branch (`throw new AuthError('.../unsupported-alg', ...)`) so an unrecognized alg can never reach a signature-verify call. The detector specifically flags `switch (alg)` (or `switch (header.alg)` / `switch (sigAlgo)`) whose default-branch returns / breaks rather than throwing. New alg-dispatch code throws in the default — no exceptions.",
6532
+ reason: "Alg-allowlist gate (CWE-347 / CWE-757) — JWT verifiers that accept unknown alg values via a permissive switch-default branch are the canonical bypass class. Every alg-dispatch primitive in the framework throws in the default branch (`throw new AuthError('.../unsupported-alg', ...)`) so an unrecognized alg can never reach a signature-verify call. The detector specifically flags `switch (alg)` (or `switch (header.alg)` / `switch (sigAlgo)`) whose default-branch returns / breaks rather than throwing. New alg-dispatch code throws in the default — no exceptions.",
6466
6533
  },
6467
6534
  {
6468
6535
  id: "inline-codepoint-class-table",
@@ -7351,6 +7418,36 @@ var KNOWN_ANTIPATTERNS = [
7351
7418
  reason: "Every 'test passes alone, fails under SMOKE_PARALLEL=64' flake (macOS watcher, log-stream-otlp, safe-async-loops, rate-limit-cluster, sandbox flake) is the same root cause: a fixed-budget setTimeout sleep that's too short for runner-contention reality. `helpers.waitUntil(predicate, opts?)` polls the actual condition every 25ms up to a 5000ms cap, exiting early when the predicate returns truthy. Fast platforms finish in milliseconds; contended platforms get the full budget. `helpers.passiveObserve(ms, label)` is the sibling primitive for the rare case of verifying ABSENCE of an event over a window (work simulators, TTL-elapse before assertion). The allowlist's structural FPs are permanent; the migration-backlog files drain to zero in subsequent patches.",
7352
7419
  },
7353
7420
 
7421
+ {
7422
+ // Sibling to test-promise-settimeout-sleep, for the timer the
7423
+ // setTimeout regex misses: a COUNTED DRAIN-LOOP that reassigns a
7424
+ // promise to its own `.then()` over and over to "flush N
7425
+ // microtasks/ticks" before asserting — the `_waitMicrotasks(n)`
7426
+ // helper shape (`var p = Promise.resolve(); for (...) p = p.then(
7427
+ // () => new Promise(r => setImmediate(r)));`). Like the fixed
7428
+ // setTimeout sleep it guesses a budget; under SMOKE_PARALLEL=64
7429
+ // contention the awaited async work (a cluster-backend DB take,
7430
+ // a scheduler tick-claim) hasn't resolved within the tick count,
7431
+ // so the next assertion reads stale state. This was the recurring
7432
+ // rate-limit-cluster "4th blocked with 429" flake and the
7433
+ // scheduler-exactly-once tick-claim race. A single `await new
7434
+ // Promise(r => setImmediate(r))` event-loop yield is legitimate
7435
+ // and is NOT matched — only the self-reassigning `<x> = <x>.then(`
7436
+ // drain idiom paired with a timer is. Poll the observable
7437
+ // condition with helpers.waitUntil instead.
7438
+ id: "test-microtask-drain-loop-sleep",
7439
+ primitive: "helpers.waitUntil(predicate, { timeoutMs, label }) — poll the observable condition; never drain a fixed count of microtasks/ticks by reassigning a promise to its own .then() in a loop",
7440
+ scanScope: "test",
7441
+ regex: /\b(\w+)\s*=\s*\1\.then\([\s\S]{0,80}?set(?:Immediate|Timeout)\s*\(/,
7442
+ skipCommentLines: true,
7443
+ allowlist: [
7444
+ // The catalog itself carries this pattern as a regex literal +
7445
+ // in this entry's own prose/reason describing the antipattern.
7446
+ "test/layer-0-primitives/codebase-patterns.test.js",
7447
+ ],
7448
+ reason: "A for-loop that reassigns a promise to its own `.then(() => new Promise(r => setImmediate(r)))` to drain a fixed number of microtask ticks is the same fixed-budget anti-pattern as a setTimeout sleep, just timed in event-loop turns instead of milliseconds — and it flakes the same way: when the async work under test hasn't resolved within the tick count (cluster DB take, scheduler tick-claim) the following assertion reads stale state. Poll the observable condition with helpers.waitUntil(predicate, { timeoutMs, label }); a lone `await new Promise(r => setImmediate(r))` yield is fine and isn't matched.",
7449
+ },
7450
+
7354
7451
  {
7355
7452
  // v0.10.13 PR #102 macOS hang — stream-throttle.test.js used
7356
7453
  // `setTimeout`-based rate enforcement plus `node:stream.pipeline`
@@ -58,6 +58,31 @@ function testFromAssertion() {
58
58
  b.auth.fal.fromAssertion({ channel: "back" }) === "FAL1");
59
59
  }
60
60
 
61
+ function testBearerOnlyAlias() {
62
+ // bearerOnly:true is the documented alias for hokBinding === null —
63
+ // it forces the bearer path. With replay + injection-protection on a
64
+ // back-channel it still classifies as FAL2 (not FAL3), proving no
65
+ // proof-of-possession binding is applied.
66
+ check("bearerOnly:true → no HoK (back + replay + bcAuth = FAL2)",
67
+ b.auth.fal.fromAssertion({
68
+ channel: "back", replayProtected: true,
69
+ backChannelAuthenticated: true, bearerOnly: true,
70
+ }) === "FAL2");
71
+ // bearerOnly:true with mTLS even + replay must NOT reach FAL3 —
72
+ // bearerOnly wins is a contradiction, so it's refused (config-time).
73
+ var threw = null;
74
+ try {
75
+ b.auth.fal.fromAssertion({ channel: "back", hokBinding: "mtls",
76
+ replayProtected: true, bearerOnly: true });
77
+ } catch (e) { threw = e; }
78
+ check("bearerOnly:true + hokBinding refused",
79
+ threw && (threw.code || "").indexOf("auth/bad-fal-opts") !== -1);
80
+ // bearerOnly:false is a no-op (hokBinding still honored → FAL3).
81
+ check("bearerOnly:false is inert (mTLS + replay = FAL3)",
82
+ b.auth.fal.fromAssertion({ channel: "back", hokBinding: "mtls",
83
+ replayProtected: true, bearerOnly: false }) === "FAL3");
84
+ }
85
+
61
86
  function testFromAssertionRefusesBadShape() {
62
87
  function expectCode(label, fn, code) {
63
88
  var threw = null;
@@ -105,6 +130,7 @@ function testRequireFal() {
105
130
  async function run() {
106
131
  testSurface();
107
132
  testFromAssertion();
133
+ testBearerOnlyAlias();
108
134
  testFromAssertionRefusesBadShape();
109
135
  testMeets();
110
136
  testRequireFal();
@@ -12,8 +12,8 @@
12
12
  * - the doc-block contains the required RFC citations + CVE
13
13
  * references + deferral conditions + escape hatch (per the
14
14
  * project's defer-with-condition rule),
15
- * - checkCert() refuses SHA-1 / MD5 cert signatures (CVE-2017-9006
16
- * class) and < 2048-bit RSA (RFC 8301 §3.1).
15
+ * - checkCert() refuses SHA-1 / MD5 cert signatures (SHAttered SHA-1
16
+ * collision; RFC 8551 §2.5) and < 2048-bit RSA (RFC 8301 §3.1).
17
17
  *
18
18
  * Run standalone: `node test/layer-0-primitives/mail-crypto-smime.test.js`
19
19
  * Or via smoke: `node test/smoke.js`
@@ -203,8 +203,8 @@ function testSmimeCheckCertRefusesSha1() {
203
203
  try { smime.checkCert({ certPem: certPem }); } catch (e) { threw = e; }
204
204
  check("checkCert refuses SHA-1 cert signature",
205
205
  threw && threw.code === "mail-crypto/smime/refused-hash");
206
- check("checkCert SHA-1 refusal names CVE class",
207
- threw && /CVE-2017-9006/.test(threw.message));
206
+ check("checkCert SHA-1 refusal names the weakness class",
207
+ threw && /SHAttered|RFC 8551/.test(threw.message));
208
208
  }
209
209
 
210
210
  // ---- checkCert: RSA < 2048 refusal ----
@@ -260,7 +260,7 @@ function testSmimeDocBlockCitations() {
260
260
  check("smime doc block names RFC 8550", src.indexOf("RFC 8550") !== -1);
261
261
  check("smime doc block names RFC 8301", src.indexOf("RFC 8301") !== -1);
262
262
  check("smime doc block names CVE-2017-17688", src.indexOf("CVE-2017-17688") !== -1);
263
- check("smime doc block names CVE-2017-9006", src.indexOf("CVE-2017-9006") !== -1);
263
+ check("smime doc block names SHAttered SHA-1 class", src.indexOf("SHAttered") !== -1);
264
264
  // v0.10.16 — sign/verify went live; deferred-conditions language
265
265
  // was replaced with "LIVE on b.cms substrate".
266
266
  check("smime doc block names live status",
@@ -276,7 +276,7 @@ async function testCaldavPutValidIcal() {
276
276
  async function testCaldavPutRefusesInvalidIcal() {
277
277
  var dav = mailDav.create({ storage: _makeStorage() });
278
278
  var res = _makeRes();
279
- // RRULE with COUNT > 10000 — CVE-2024-39687 defense.
279
+ // RRULE with COUNT > 10000 — calendar-bomb / recursion-DoS defense.
280
280
  var bad = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//B//\r\n" +
281
281
  "BEGIN:VEVENT\r\nUID:b@x\r\nDTSTAMP:20260101T120000Z\r\nDTSTART:20260101T130000Z\r\n" +
282
282
  "RRULE:FREQ=DAILY;COUNT=999999\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";