@blamejs/blamejs-shop 0.1.37 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/admin.js +205 -42
  3. package/lib/asset-manifest.json +5 -5
  4. package/lib/storefront.js +428 -51
  5. package/lib/vendor/MANIFEST.json +2 -2
  6. package/lib/vendor/blamejs/CHANGELOG.md +12 -0
  7. package/lib/vendor/blamejs/README.md +2 -2
  8. package/lib/vendor/blamejs/SECURITY.md +1 -0
  9. package/lib/vendor/blamejs/api-snapshot.json +2 -2
  10. package/lib/vendor/blamejs/lib/ai-disclosure.js +2 -3
  11. package/lib/vendor/blamejs/lib/archive-gz.js +5 -3
  12. package/lib/vendor/blamejs/lib/archive-read.js +101 -35
  13. package/lib/vendor/blamejs/lib/archive-tar-read.js +86 -31
  14. package/lib/vendor/blamejs/lib/archive-tar.js +2 -3
  15. package/lib/vendor/blamejs/lib/auth/fal.js +12 -0
  16. package/lib/vendor/blamejs/lib/auth/jwt-external.js +15 -11
  17. package/lib/vendor/blamejs/lib/auth/jwt.js +2 -2
  18. package/lib/vendor/blamejs/lib/auth/oauth.js +7 -6
  19. package/lib/vendor/blamejs/lib/auth/oid4vci.js +3 -3
  20. package/lib/vendor/blamejs/lib/auth/saml.js +15 -12
  21. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -3
  22. package/lib/vendor/blamejs/lib/backup/index.js +2 -3
  23. package/lib/vendor/blamejs/lib/calendar.js +9 -2
  24. package/lib/vendor/blamejs/lib/circuit-breaker.js +5 -4
  25. package/lib/vendor/blamejs/lib/cose.js +4 -3
  26. package/lib/vendor/blamejs/lib/crypto-hpke.js +1 -1
  27. package/lib/vendor/blamejs/lib/crypto-oprf.js +2 -2
  28. package/lib/vendor/blamejs/lib/crypto.js +2 -2
  29. package/lib/vendor/blamejs/lib/framework-error.js +2 -1
  30. package/lib/vendor/blamejs/lib/guard-jwt.js +3 -2
  31. package/lib/vendor/blamejs/lib/guard-smtp-command.js +2 -2
  32. package/lib/vendor/blamejs/lib/mail-auth.js +2 -2
  33. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +1 -1
  34. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +7 -7
  35. package/lib/vendor/blamejs/lib/mail-crypto.js +1 -1
  36. package/lib/vendor/blamejs/lib/mail-dav.js +5 -4
  37. package/lib/vendor/blamejs/lib/mail-deploy.js +3 -2
  38. package/lib/vendor/blamejs/lib/mail-server-imap.js +1 -1
  39. package/lib/vendor/blamejs/lib/mail-server-jmap.js +10 -10
  40. package/lib/vendor/blamejs/lib/mail-server-managesieve.js +1 -1
  41. package/lib/vendor/blamejs/lib/mail-server-mx.js +142 -47
  42. package/lib/vendor/blamejs/lib/mail-server-submission.js +6 -5
  43. package/lib/vendor/blamejs/lib/mail-store.js +2 -2
  44. package/lib/vendor/blamejs/lib/mail.js +2 -2
  45. package/lib/vendor/blamejs/lib/mdoc.js +14 -14
  46. package/lib/vendor/blamejs/lib/network-dnssec.js +10 -8
  47. package/lib/vendor/blamejs/lib/network-tls.js +10 -7
  48. package/lib/vendor/blamejs/lib/safe-decompress.js +8 -6
  49. package/lib/vendor/blamejs/lib/safe-ical.js +12 -12
  50. package/lib/vendor/blamejs/lib/safe-mime.js +6 -6
  51. package/lib/vendor/blamejs/lib/safe-sieve.js +1 -1
  52. package/lib/vendor/blamejs/lib/safe-smtp.js +1 -1
  53. package/lib/vendor/blamejs/package.json +1 -1
  54. package/lib/vendor/blamejs/release-notes/v0.13.10.json +44 -0
  55. package/lib/vendor/blamejs/release-notes/v0.13.11.json +27 -0
  56. package/lib/vendor/blamejs/release-notes/v0.13.12.json +36 -0
  57. package/lib/vendor/blamejs/release-notes/v0.13.7.json +27 -0
  58. package/lib/vendor/blamejs/release-notes/v0.13.8.json +27 -0
  59. package/lib/vendor/blamejs/release-notes/v0.13.9.json +27 -0
  60. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +82 -0
  61. package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +4 -4
  62. package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +31 -0
  63. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +188 -6
  64. package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +26 -0
  65. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +5 -5
  66. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +1 -1
  67. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +166 -1
  68. package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-cluster.test.js +25 -18
  69. package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +2 -2
  70. package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +12 -12
  71. package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-exactly-once.test.js +15 -9
  72. package/lib/vendor/blamejs/test/layer-0-primitives/websocket-channels.test.js +0 -6
  73. package/package.json +1 -1
@@ -15,7 +15,7 @@
15
15
  * delivery-time iTIP processing, and the scheduling primitives that
16
16
  * compose against ical bytes.
17
17
  *
18
- * Defends `CVE-2024-39687` (ical4j RRULE recursion / "Outlook
18
+ * Defends the ical4j RRULE-recursion expansion-DoS class ("Outlook
19
19
  * calendar bomb" — a hostile RRULE with unbounded COUNT and
20
20
  * recursive BYxxx expansion can pin a CalDAV server's CPU at 100%
21
21
  * until the request times out). Caps:
@@ -36,8 +36,8 @@
36
36
  * instances than this cap.
37
37
  * - RRULE BYDAY / BYMONTH / BYMONTHDAY / BYHOUR / BYMINUTE /
38
38
  * BYSECOND / BYSETPOS / BYWEEKNO / BYYEARDAY list-length cap
39
- * (24 entries) — refused regardless of profile. CVE-2024-39687
40
- * achieves expansion blow-up by stacking long BYxxx lists.
39
+ * (24 entries) — refused regardless of profile. The recursion
40
+ * DoS achieves expansion blow-up by stacking long BYxxx lists.
41
41
  *
42
42
  * Header-injection / control-char defense: refuses NUL, C0 control
43
43
  * bytes (other than TAB inside QUOTED-PRINTABLE-shaped values), and
@@ -75,8 +75,8 @@
75
75
  * @card
76
76
  * Bounded RFC 5545 iCalendar parser — caps total bytes, nesting
77
77
  * depth, RRULE COUNT and BYxxx list-lengths; refuses NUL / C0 / DEL
78
- * inside property values; allowlists property names; defends
79
- * CVE-2024-39687 (ical4j RRULE recursion / Outlook calendar-bomb).
78
+ * inside property values; allowlists property names; defends the
79
+ * ical4j RRULE-recursion expansion-DoS class (Outlook calendar-bomb).
80
80
  */
81
81
 
82
82
  var C = require("./constants");
@@ -84,8 +84,8 @@ var { defineClass } = require("./framework-error");
84
84
 
85
85
  var SafeIcalError = defineClass("SafeIcalError", { alwaysPermanent: true });
86
86
 
87
- // RRULE caps are enforced regardless of profile — CVE-2024-39687 has
88
- // no safe permissive posture.
87
+ // RRULE caps are enforced regardless of profile — the recursion-DoS
88
+ // class has no safe permissive posture.
89
89
  var RRULE_MAX_COUNT = 10000; // allow:raw-byte-literal — RFC 5545 §3.3.10 recurrence-count cap
90
90
  var RRULE_MAX_BY_ENTRIES = 24; // allow:raw-byte-literal — BYxxx list-length cap
91
91
 
@@ -223,7 +223,7 @@ function parse(text, opts) {
223
223
  if (byteLen > caps.maxBytes) {
224
224
  throw new SafeIcalError("safe-ical/oversize-bytes",
225
225
  "safeIcal.parse: input " + byteLen + " bytes exceeds maxBytes=" + caps.maxBytes +
226
- " (CVE-2024-39687-class defense)");
226
+ " (calendar-bomb defense)");
227
227
  }
228
228
 
229
229
  var lines = _unfold(s, caps);
@@ -466,7 +466,7 @@ function _parseComponent(lines, startIdx, ctx, depth) {
466
466
  if (depth > ctx.caps.maxNestingDepth) {
467
467
  throw new SafeIcalError("safe-ical/oversize-nesting",
468
468
  "safeIcal.parse: nesting depth exceeds maxNestingDepth=" +
469
- ctx.caps.maxNestingDepth + " (CVE-2024-39687-class defense)");
469
+ ctx.caps.maxNestingDepth + " (calendar-bomb defense)");
470
470
  }
471
471
  ctx.componentCount += 1;
472
472
  if (ctx.componentCount > ctx.caps.maxComponents) {
@@ -516,7 +516,7 @@ function _parseComponent(lines, startIdx, ctx, depth) {
516
516
  "safeIcal.parse: unknown property '" + pn +
517
517
  "' (extend via opts.extraProperties or use X- prefix)");
518
518
  }
519
- // RRULE caps — CVE-2024-39687 defense.
519
+ // RRULE caps — recursion-DoS / calendar-bomb defense.
520
520
  if (pn === "RRULE" || pn === "EXRULE") {
521
521
  _validateRrule(ln.value);
522
522
  }
@@ -558,7 +558,7 @@ function _validateRrule(value) {
558
558
  if (!isFinite(n) || n < 0 || n > RRULE_MAX_COUNT) {
559
559
  throw new SafeIcalError("safe-ical/oversize-rrule-count",
560
560
  "safeIcal.parse: RRULE COUNT=" + val + " exceeds cap=" +
561
- RRULE_MAX_COUNT + " (CVE-2024-39687 defense)");
561
+ RRULE_MAX_COUNT + " (calendar-bomb defense)");
562
562
  }
563
563
  } else if (key === "BYDAY" || key === "BYMONTH" || key === "BYMONTHDAY" ||
564
564
  key === "BYHOUR" || key === "BYMINUTE" || key === "BYSECOND" ||
@@ -567,7 +567,7 @@ function _validateRrule(value) {
567
567
  if (entries.length > RRULE_MAX_BY_ENTRIES) {
568
568
  throw new SafeIcalError("safe-ical/oversize-rrule-by",
569
569
  "safeIcal.parse: RRULE " + key + " list length " + entries.length +
570
- " exceeds cap=" + RRULE_MAX_BY_ENTRIES + " (CVE-2024-39687 defense)");
570
+ " exceeds cap=" + RRULE_MAX_BY_ENTRIES + " (calendar-bomb defense)");
571
571
  }
572
572
  }
573
573
  }
@@ -27,7 +27,7 @@
27
27
  * crypto.
28
28
  *
29
29
  * Defends `CVE-2024-39929` (Exim MIME multipart parser) and
30
- * `CVE-2025-30258` (gnumail truncated-MIME-tree class) by capping
30
+ * `CVE-2026-26312` (Stalwart nested `message/rfc822` MIME OOM) by capping
31
31
  * total parts, nesting depth, boundary length, header bytes,
32
32
  * header-line bytes, decoded body bytes, message bytes — plus
33
33
  * charset + transfer-encoding allowlists.
@@ -41,7 +41,7 @@
41
41
  * incoming message above a threshold.
42
42
  *
43
43
  * @card
44
- * Bounded MIME parser — walks RFC 5322 + 2045 / 2046 / 2047 + EAI message structure into a part tree with hard caps on depth, part count, body size, header bytes, and charset / transfer-encoding allowlists. Defends CVE-2024-39929 + CVE-2025-30258.
44
+ * Bounded MIME parser — walks RFC 5322 + 2045 / 2046 / 2047 + EAI message structure into a part tree with hard caps on depth, part count, body size, header bytes, and charset / transfer-encoding allowlists. Defends CVE-2024-39929 + CVE-2026-26312.
45
45
  */
46
46
 
47
47
  var C = require("./constants");
@@ -332,13 +332,13 @@ function _parsePart(buf, ctx, depth) {
332
332
  if (depth > ctx.maxNestingDepth) {
333
333
  throw new SafeMimeError("safe-mime/oversize-nesting",
334
334
  "safeMime.parse: nesting depth exceeded maxNestingDepth=" + ctx.maxNestingDepth +
335
- " (CVE-2024-39929-class defense)");
335
+ " (CVE-2024-39929 class defense)");
336
336
  }
337
337
  ctx.partCount += 1;
338
338
  if (ctx.partCount > ctx.maxParts) {
339
339
  throw new SafeMimeError("safe-mime/oversize-part-count",
340
340
  "safeMime.parse: total parts exceeded maxParts=" + ctx.maxParts +
341
- " (CVE-2024-39929-class defense)");
341
+ " (CVE-2024-39929 class defense)");
342
342
  }
343
343
 
344
344
  var sep = _findHeaderBodySep(buf);
@@ -668,7 +668,7 @@ function _decodeRfc2047Words(value) {
668
668
  raw = Buffer.from(text.replace(/_/g, " ").replace(/=([0-9A-Fa-f]{2})/g,
669
669
  function (__, hex) { return String.fromCharCode(parseInt(hex, 16)); }), "binary"); // allow:raw-byte-literal — parseInt radix 16, not bytes
670
670
  }
671
- // RFC 2047 §5 / CVE-2020-7244 header-injection defense — after
671
+ // RFC 2047 §5 encoded-word header-injection defense — after
672
672
  // base64 / Q-encoded decode, check the DECODED bytes for header
673
673
  // separators (CR, LF, NUL). A sender that base64-encodes
674
674
  // `\r\nBcc: attacker@x.com` would otherwise reach the consumer's
@@ -680,7 +680,7 @@ function _decodeRfc2047Words(value) {
680
680
  if (b === 0x0d /* CR */ || b === 0x0a /* LF */ || b === 0x00 /* NUL */) {
681
681
  throw new SafeMimeError("safe-mime/rfc2047-header-injection",
682
682
  "RFC 2047 encoded-word decoded to bytes containing CR/LF/NUL " +
683
- "(byte index " + bi + "); refusing per RFC 2047 §5 / CVE-2020-7244 class");
683
+ "(byte index " + bi + "); refusing per RFC 2047 §5 (encoded-word header injection)");
684
684
  }
685
685
  }
686
686
  return _decodeBufferAs(raw, charset);
@@ -619,7 +619,7 @@ function parse(script, opts) {
619
619
  * Parse-only validation — returns `{ ok, requiredCaps, issues }`
620
620
  * shape mirroring the rest of the guard family. Operator-facing
621
621
  * primitives that want a JMAP-style `SieveScript/validate` response
622
- * (RFC 9404) compose this and surface `issues` directly.
622
+ * (RFC 9661 — JMAP for Sieve Scripts) compose this and surface `issues` directly.
623
623
  *
624
624
  * @opts
625
625
  * profile: "strict" | "balanced" | "permissive",
@@ -23,7 +23,7 @@
23
23
  * - RFC 5321 §2.3.8 — line termination MUST be CRLF
24
24
  * - RFC 5321 §4.5.2 — dot-stuffing on the SMTP body
25
25
  * - RFC 5321 §4.1.1.4 — DATA command terminates with `<CRLF>.<CRLF>`
26
- * - CVE-2023-51764 / -51765 / -51766 / 2024-32178 — SMTP
26
+ * - CVE-2023-51764 / -51765 / -51766 — SMTP
27
27
  * smuggling (parsers that accept bare-LF dot-terminators).
28
28
  * The guard primitive `b.guardSmtpCommand.detectBodySmuggling`
29
29
  * owns smuggling detection; the safe-* terminator scanner
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.13.6",
3
+ "version": "0.13.12",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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,27 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.7",
4
+ "date": "2026-05-26",
5
+ "headline": "Documentation accuracy — several primitives described shipped features as deferred",
6
+ "summary": "A documentation sweep corrected primitive descriptions that still called features deferred after they shipped, so the wiki and inline docs now match the code. b.mdoc documents that device authentication (the ISO 18013-5 §9.1.3 signature variant, verifyDeviceAuth) is verified, not deferred — only the COSE_Mac0 device-auth variant remains refused. b.network.dns.dnssec documents that the root-to-zone chain walk against the IANA trust anchors (verifyChain) and NSEC / NSEC3 denial of existence (verifyDenial / nsec3Hash) ship, where the card previously said they were deferred. b.cose lists COSE_Mac0 and COSE_Encrypt0 among what it ships. The JMAP server documents its push channel, blob upload/download, and EmailSubmission handlers as present, and the submission server documents CHUNKING / BDAT as supported. A new test detector keeps this class of drift from recurring: it fails the build when a comment promises a feature lands in a version that has already shipped.",
7
+ "sections": [
8
+ {
9
+ "heading": "Fixed",
10
+ "items": [
11
+ {
12
+ "title": "Corrected `deferred`/`does-not-ship` docs for features that have shipped",
13
+ "body": "`b.mdoc` (device authentication, §9.1.3), `b.network.dns.dnssec` (chain walk + NSEC/NSEC3), `b.cose` (COSE_Mac0 + COSE_Encrypt0), the JMAP server (push, blob, EmailSubmission), and the submission server (BDAT/CHUNKING) all carried `@card`/`@intro` text describing shipped capabilities as deferred or not-shipped. The descriptions now match the implemented surface; genuinely-deferred items (the mdoc MAC variant, DNSSEC in-RDATA name canonicalization, COSE multi-signer/multi-recipient) remain documented as such."
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "heading": "Detectors",
19
+ "items": [
20
+ {
21
+ "title": "Overdue-defer detector in the codebase-pattern gate",
22
+ "body": "A new check fails the build when a comment promises a feature \"lands in\" / is \"deferred to\" / is \"not supported in\" a version that the package has already reached — catching stale deferral notes (a feature that shipped but whose comment still says otherwise, or a missed deadline) before they reach a release. An allowlist records the deliberate defer-with-condition exceptions."
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.8",
4
+ "date": "2026-05-26",
5
+ "headline": "In-memory archive extraction for read-only / serverless filesystems",
6
+ "summary": "Archive readers gain an in-memory extraction path so an uploaded archive can be opened and its contents read without writing anything to disk — the case a read-only or ephemeral serverless filesystem requires. b.archive.read.zip(...).extractEntries() and b.archive.read.tar(...).extractEntries() are async generators that yield each regular file entry as { name, bytes, size }, applying the same bomb-policy caps, b.guardArchive metadata cascade (which refuses a Zip-Slip / traversal archive wholesale), and entry-type-policy refusals as the disk extract() — only the disk realpath agreement check is omitted, since nothing is written and the caller owns where the returned bytes land. Directory and link entries carry no content and are not yielded. The guard cascade is factored into one shared path so disk and in-memory extraction refuse identically. Also a documentation fix: b.archive.gz no longer claims a b.archive.zip().toGzip() convenience exists — a ZIP is already DEFLATE-compressed per entry, so gzip-wrapping it gains nothing; gzip the uncompressed tar stream (the canonical .tar.gz) instead.",
7
+ "sections": [
8
+ {
9
+ "heading": "Added",
10
+ "items": [
11
+ {
12
+ "title": "`extractEntries()` — in-memory archive extraction (ZIP + tar)",
13
+ "body": "`b.archive.read.zip(source).extractEntries(opts?)` and `b.archive.read.tar(source).extractEntries(opts?)` are async generators yielding `{ name, bytes, size }` per regular file entry, never touching disk — for serverless / read-only filesystems where the disk `extract({ destination })` path cannot run. Same bomb-policy, guard-archive cascade, and entry-type-policy refusals as disk extraction; the bytes are byte-identical to what `extract()` writes."
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "heading": "Fixed",
19
+ "items": [
20
+ {
21
+ "title": "Removed the inaccurate `b.archive.zip().toGzip()` doc claim",
22
+ "body": "The `b.archive.gz` documentation described a `b.archive.zip().toGzip()` convenience method that does not (and should not) exist: a ZIP is already DEFLATE-compressed per entry, so gzip-wrapping it would compress already-compressed data for no benefit. `b.archive.tar().toGzip()` (the real `.tar.gz`) is unchanged."
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ }
@@ -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
+ }
@@ -223,8 +223,90 @@ async function testBundleAdapterStorageRoundTrip() {
223
223
  }
224
224
  }
225
225
 
226
+ // In-memory extraction (serverless / read-only FS): reader.extractEntries()
227
+ // yields decompressed bytes without ever writing to disk, and those bytes are
228
+ // byte-identical to what disk extract() produces.
229
+ async function testExtractEntriesInMemory() {
230
+ var z = b.archive.zip();
231
+ z.addFile("readme.txt", "Hello, in-memory!\n");
232
+ z.addFile("data/nums.csv", "n,sq\n2,4\n");
233
+ z.addFile("docs/deep.bin", Buffer.from([0, 1, 2, 3, 255]));
234
+ var bytes = z.toBuffer();
235
+
236
+ // Spy: no fs write may happen during the in-memory path.
237
+ var wrote = false;
238
+ var origWrite = fs.writeFileSync;
239
+ fs.writeFileSync = function () { wrote = true; return origWrite.apply(fs, arguments); };
240
+ var collected = {};
241
+ try {
242
+ var reader = b.archive.read.zip(b.archive.adapters.buffer(bytes));
243
+ for await (var e of reader.extractEntries()) { collected[e.name] = e.bytes; }
244
+ } finally {
245
+ fs.writeFileSync = origWrite;
246
+ }
247
+ check("zip extractEntries: no disk write", wrote === false);
248
+ check("zip extractEntries: 3 file entries (dir skipped)", Object.keys(collected).length === 3);
249
+ check("zip extractEntries: text bytes match", collected["readme.txt"].toString("utf8") === "Hello, in-memory!\n");
250
+ check("zip extractEntries: binary bytes match", collected["docs/deep.bin"].equals(Buffer.from([0, 1, 2, 3, 255])));
251
+
252
+ // Byte-equality with disk extract().
253
+ var dest = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-archive-mem-"));
254
+ try {
255
+ await b.safeArchive.extract({ source: bytes, destination: dest, guardProfile: "balanced" });
256
+ var diskBin = fs.readFileSync(path.join(dest, "docs/deep.bin"));
257
+ check("zip extractEntries bytes == disk extract bytes", collected["docs/deep.bin"].equals(diskBin));
258
+ } finally {
259
+ fs.rmSync(dest, { recursive: true, force: true });
260
+ }
261
+
262
+ // tar in-memory.
263
+ var t = b.archive.tar();
264
+ t.addFile("x.txt", "world\n");
265
+ t.addFile("sub/y.dat", Buffer.from([9, 8, 7]));
266
+ var tbytes = t.toBuffer();
267
+ var tcollected = {};
268
+ var tr = b.archive.read.tar(b.archive.adapters.buffer(tbytes));
269
+ for await (var te of tr.extractEntries()) { tcollected[te.name] = te.bytes; }
270
+ check("tar extractEntries: 2 entries", Object.keys(tcollected).length === 2);
271
+ check("tar extractEntries: text matches", tcollected["x.txt"].toString("utf8") === "world\n");
272
+ check("tar extractEntries: binary matches", tcollected["sub/y.dat"].equals(Buffer.from([9, 8, 7])));
273
+ }
274
+
275
+ // opts.signal (AbortSignal) is documented on b.archive.read.zip — verify
276
+ // it actually aborts the read at the entry boundary rather than being a
277
+ // dead doc opt.
278
+ async function testSignalAbort() {
279
+ var z = b.archive.zip();
280
+ z.addFile("a.txt", "one\n");
281
+ z.addFile("b.txt", "two\n");
282
+ var bytes = z.toBuffer();
283
+
284
+ var ac = new AbortController();
285
+ ac.abort(); // already aborted before any read
286
+ var reader = b.archive.read.zip(b.archive.adapters.buffer(bytes), { signal: ac.signal });
287
+
288
+ var inspectThrew = null;
289
+ try { await reader.inspect(); } catch (e) { inspectThrew = e; }
290
+ check("zip inspect honors an aborted signal",
291
+ inspectThrew && (inspectThrew.code || "").indexOf("archive-read/aborted") !== -1);
292
+
293
+ var extractThrew = null;
294
+ try { for await (var _e of reader.extractEntries()) { void _e; } }
295
+ catch (e) { extractThrew = e; }
296
+ check("zip extractEntries honors an aborted signal",
297
+ extractThrew && (extractThrew.code || "").indexOf("archive-read/aborted") !== -1);
298
+
299
+ // Sanity: with no signal the same reader works.
300
+ var ok = b.archive.read.zip(b.archive.adapters.buffer(bytes));
301
+ var n = 0;
302
+ for await (var e2 of ok.extractEntries()) { void e2; n += 1; }
303
+ check("zip extractEntries with no signal still yields entries", n === 2);
304
+ }
305
+
226
306
  async function run() {
227
307
  await testRoundTripExtract();
308
+ await testExtractEntriesInMemory();
309
+ await testSignalAbort();
228
310
  testSafeArchiveErrorClass();
229
311
  await testSafeArchiveInspect();
230
312
  await testZipBombPolicy();
@@ -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();