@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.
- package/CHANGELOG.md +6 -0
- package/lib/admin.js +205 -42
- package/lib/asset-manifest.json +5 -5
- package/lib/storefront.js +428 -51
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +12 -0
- package/lib/vendor/blamejs/README.md +2 -2
- package/lib/vendor/blamejs/SECURITY.md +1 -0
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/ai-disclosure.js +2 -3
- package/lib/vendor/blamejs/lib/archive-gz.js +5 -3
- package/lib/vendor/blamejs/lib/archive-read.js +101 -35
- package/lib/vendor/blamejs/lib/archive-tar-read.js +86 -31
- package/lib/vendor/blamejs/lib/archive-tar.js +2 -3
- package/lib/vendor/blamejs/lib/auth/fal.js +12 -0
- package/lib/vendor/blamejs/lib/auth/jwt-external.js +15 -11
- package/lib/vendor/blamejs/lib/auth/jwt.js +2 -2
- package/lib/vendor/blamejs/lib/auth/oauth.js +7 -6
- package/lib/vendor/blamejs/lib/auth/oid4vci.js +3 -3
- package/lib/vendor/blamejs/lib/auth/saml.js +15 -12
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -3
- package/lib/vendor/blamejs/lib/backup/index.js +2 -3
- package/lib/vendor/blamejs/lib/calendar.js +9 -2
- package/lib/vendor/blamejs/lib/circuit-breaker.js +5 -4
- package/lib/vendor/blamejs/lib/cose.js +4 -3
- package/lib/vendor/blamejs/lib/crypto-hpke.js +1 -1
- package/lib/vendor/blamejs/lib/crypto-oprf.js +2 -2
- package/lib/vendor/blamejs/lib/crypto.js +2 -2
- package/lib/vendor/blamejs/lib/framework-error.js +2 -1
- package/lib/vendor/blamejs/lib/guard-jwt.js +3 -2
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +2 -2
- package/lib/vendor/blamejs/lib/mail-auth.js +2 -2
- package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +1 -1
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +7 -7
- package/lib/vendor/blamejs/lib/mail-crypto.js +1 -1
- package/lib/vendor/blamejs/lib/mail-dav.js +5 -4
- package/lib/vendor/blamejs/lib/mail-deploy.js +3 -2
- package/lib/vendor/blamejs/lib/mail-server-imap.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +10 -10
- package/lib/vendor/blamejs/lib/mail-server-managesieve.js +1 -1
- package/lib/vendor/blamejs/lib/mail-server-mx.js +142 -47
- package/lib/vendor/blamejs/lib/mail-server-submission.js +6 -5
- package/lib/vendor/blamejs/lib/mail-store.js +2 -2
- package/lib/vendor/blamejs/lib/mail.js +2 -2
- package/lib/vendor/blamejs/lib/mdoc.js +14 -14
- package/lib/vendor/blamejs/lib/network-dnssec.js +10 -8
- package/lib/vendor/blamejs/lib/network-tls.js +10 -7
- package/lib/vendor/blamejs/lib/safe-decompress.js +8 -6
- package/lib/vendor/blamejs/lib/safe-ical.js +12 -12
- package/lib/vendor/blamejs/lib/safe-mime.js +6 -6
- package/lib/vendor/blamejs/lib/safe-sieve.js +1 -1
- package/lib/vendor/blamejs/lib/safe-smtp.js +1 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.10.json +44 -0
- package/lib/vendor/blamejs/release-notes/v0.13.11.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.13.12.json +36 -0
- package/lib/vendor/blamejs/release-notes/v0.13.7.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.13.8.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.13.9.json +27 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +82 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +4 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +31 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +188 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +26 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +5 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +1 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +166 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-cluster.test.js +25 -18
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +2 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +12 -12
- package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-exactly-once.test.js +15 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/websocket-channels.test.js +0 -6
- 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
|
|
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.
|
|
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
|
-
*
|
|
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 —
|
|
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
|
-
" (
|
|
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 + " (
|
|
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 —
|
|
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 + " (
|
|
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 + " (
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
@@ -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
|
-
*
|
|
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
|
-
// --------
|
|
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
|
|
160
|
-
check("
|
|
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();
|