@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
@@ -510,6 +510,82 @@ function testNoUnresolvedMarkers() {
510
510
  matches);
511
511
  }
512
512
 
513
+ // ---- Pattern: overdue defers (promised landing version already shipped) ----
514
+ //
515
+ // A comment/string that promises a feature "lands in vX" / "deferred to vX" /
516
+ // "not supported in vX" is fine while vX is in the future. Once the package
517
+ // version reaches vX, that promise is OVERDUE: either the feature shipped (and
518
+ // the comment is stale and should be corrected) or it never shipped (a real
519
+ // gap to close or an explicit defer-with-condition to record here). This gate
520
+ // surfaces the iabTcf-class "advertised-but-missing / stale-landing" shape on
521
+ // every release rather than letting promised-landing comments rot silently.
522
+ //
523
+ // STALE_DEFER_ALLOWLIST entries are acknowledged overdue mentions: each is
524
+ // either a deliberate defer-with-condition (no operator demand + escape hatch)
525
+ // or an item on the gap backlog being worked down. The key is the file; the
526
+ // value is a list of distinctive content substrings to permit. Remove an entry
527
+ // when the comment is corrected or the gap is closed — that is the backlog.
528
+ var STALE_DEFER_ALLOWLIST = {
529
+ // Deliberate defer-with-condition: needs an envelope-semantics decision
530
+ // (per-tenant KEM keypair vs symmetric); explicit escape hatch is passing
531
+ // { publicKey, ecPublicKey } directly. Tracked for design, not overdue work.
532
+ "lib/archive-wrap.js": [
533
+ // quote-free phrases (source has escaped \" so avoid quote chars here)
534
+ "deferred to v0.12.11",
535
+ "lands in v0.12.11",
536
+ ],
537
+ // Deliberate: Sieve extension refused per RFC 5228 §3.2 — script-declared
538
+ // capability gating, defer-with-condition (operator demand).
539
+ "lib/safe-sieve.js": ["not implemented in v0.9.55 — script refused"],
540
+ // Conditional on a future vendoring decision (no bundled EXIF/IPTC reader);
541
+ // operator-feeds-metadata escape hatch. Defer-with-condition.
542
+ "lib/ai-content-detect.js": ["IPTC PhotoMetadata reader lands in v0.10.9"],
543
+ // GAP BACKLOG (being worked down — these are real overdue items):
544
+ // archive-read ZIP64 read + fromTrustedStream (promised v0.12.8) — building.
545
+ "lib/archive-read.js": [
546
+ "not supported in v0.12.7. Will land",
547
+ "switch to tar — lands v0.12.8",
548
+ "carries ZIP64 sentinel sizes (not supported in v0.12.7)",
549
+ "deferred to v0.12.8 alongside the tar reader",
550
+ "fromTrustedStream.inspect() is deferred to v0.12.8",
551
+ "fromTrustedStream.entries() is deferred to v0.12.8",
552
+ "fromTrustedStream.extract() is deferred to v0.12.8",
553
+ ],
554
+ "lib/safe-archive.js": [
555
+ "tar lands v0.12.8, gz v0.12.9",
556
+ "fromTrustedStream` is deferred to v0.12.8",
557
+ ],
558
+ // STALE comments (feature shipped; phrased as future) — correcting to past
559
+ // tense as the backlog is worked; allowlisted until then.
560
+ "lib/break-glass.js": ["passkey lands in v0.5.2"],
561
+ };
562
+
563
+ function testNoStaleDefers() {
564
+ var path = require("node:path");
565
+ var pkgVersion = require(path.resolve(__dirname, "..", "..", "package.json")).version.split(".").map(Number);
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
+ // Promised-landing phrasings only ("X lands in vN" / "deferred to vN" / "not
568
+ // supported in vN"). NOT "deferred FROM vN" (that is an origin, not a deadline).
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;
572
+ var matches = _scan(PROMISE, { skipComments: false });
573
+ var overdue = [];
574
+ matches.forEach(function (m) {
575
+ var mm = m.content.match(PROMISE);
576
+ if (!mm) return;
577
+ // Only a STRICTLY-FUTURE promise is exempt. A promise for the current
578
+ // version is due in the release being cut now — if the feature isn't here,
579
+ // the comment is overdue and must be fixed in this release, not a later one.
580
+ if (cmp(mm[1].split(".").map(Number), pkgVersion) > 0) return;
581
+ var allow = STALE_DEFER_ALLOWLIST[m.file] || [];
582
+ if (allow.some(function (sub) { return m.content.indexOf(sub) !== -1; })) return;
583
+ overdue.push({ file: m.file, line: m.line, content: "overdue defer (promised v" + mm[1] + ", now v" + pkgVersion.join(".") + "): " + m.content.slice(0, 100) });
584
+ });
585
+ _report("no overdue defers in lib/ (promised-landing version already shipped — close the gap, fix the stale comment, or record it in STALE_DEFER_ALLOWLIST)",
586
+ overdue);
587
+ }
588
+
513
589
  // ---- Pattern: literal NUL bytes (0x00) in source files ----
514
590
  //
515
591
  // The Edit / Write tooling decodes JSON `\u0000` escape sequences into
@@ -2195,6 +2271,20 @@ async function testNoDuplicateCodeBlocks() {
2195
2271
  ],
2196
2272
  reason: "v0.12.7 + v0.12.8 — Per-module `_emitAudit(opts, action, outcome, metadata)` shape repeats across primitives that drop-silently emit to opts.audit.safeEmit if present. Each module's audit events carry a primitive-specific `action:` namespace (archive.read.*, archive.zip.*, archive.read.tar.*, http-client.*) + per-primitive metadata fields; consolidating would lose the namespace + force every consumer to import the same audit helper. Four-file repetition is the expected shape per `feedback_audit_safeEmit_per_module_emitAudit_shape`. archive-tar.js (write) does NOT carry _emitAudit — the read side lives in sibling archive-tar-read.js so the @primitive validator can pair both `b.archive.tar` (write) and `b.archive.read.tar` (read) cleanly.",
2197
2273
  },
2274
+ {
2275
+ mode: "family-subset",
2276
+ files: [
2277
+ "lib/archive-read.js:_assertGuardMetadata",
2278
+ "lib/archive-tar-read.js:_assertGuardMetadata",
2279
+ "lib/auth/ciba.js:_registerInitialInterval",
2280
+ "lib/auth/oauth.js:exchangeToken",
2281
+ "lib/auth/oauth.js:pollDeviceCode",
2282
+ "lib/auth/oid4vci.js:createCredentialOffer",
2283
+ "lib/auth/oid4vci.js:exchangePreAuthorizedCode",
2284
+ "lib/restore-rollback.js:swap",
2285
+ ],
2286
+ reason: "v0.13.8 — the shared shingle is the framework's emit-audit-then-throw-typed-error idiom (validate/poll/guard → emit a namespaced audit row → throw a primitive-specific FrameworkError), not behaviour. archive-read/archive-tar-read `_assertGuardMetadata` run the b.guardArchive metadata cascade and throw ArchiveReadError/TarError (factored so disk `extract` + in-memory `extractEntries` share one refusal path); ciba/oauth/oid4vci are OAuth/OIDC device-code + credential-offer polling/exchange; restore-rollback.swap is the backup restore swap. Each body is domain-divergent (different inputs, error classes, audit namespaces); consolidating would couple unrelated subsystems to one helper.",
2287
+ },
2198
2288
  {
2199
2289
  mode: "family-subset",
2200
2290
  files: [
@@ -5700,7 +5790,7 @@ var KNOWN_ANTIPATTERNS = [
5700
5790
  {
5701
5791
  // v0.10.15 — `zlib.gunzipSync` / `zlib.createGunzip` /
5702
5792
  // `zlib.brotliDecompress` without an output-size cap is the
5703
- // CVE-2025-0725 / CVE-2024-zlib decompression-amplification
5793
+ // CVE-2025-0725 / CWE-409 decompression-amplification
5704
5794
  // class. Attackers craft a kilobyte of compressed input that
5705
5795
  // explodes to gigabytes of output, exhausting memory before the
5706
5796
  // request handler sees the bytes. The defense is either the
@@ -5743,7 +5833,67 @@ var KNOWN_ANTIPATTERNS = [
5743
5833
  requires: /\bmaxOutputLength\b/,
5744
5834
  skipCommentLines: true,
5745
5835
  allowlist: [],
5746
- 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.",
5836
+ 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.",
5837
+ },
5838
+ {
5839
+ // Citation hygiene — a CVE identifier is always
5840
+ // CVE-<4-digit-year>-<sequence>, where the sequence is purely
5841
+ // numeric (CVE numbering spec). Two malformed shapes ship past
5842
+ // review: a non-numeric sequence (`CVE-2024-zlib` — a library
5843
+ // name dropped in as a placeholder), and a real id with a
5844
+ // hyphen-attached word (`CVE-2024-39687-class` — reads as if
5845
+ // `-class` is part of the id). The first is a fabricated
5846
+ // reference; the second is a parse hazard for any tool that
5847
+ // extracts CVE tokens. The annotation convention is a SPACE
5848
+ // before the descriptor (`CVE-2024-39687 class`), which leaves
5849
+ // the id token well-formed. This detector refuses both shapes:
5850
+ // a letter immediately after the year separator, OR a
5851
+ // hyphen-then-letter after the numeric sequence. It cannot
5852
+ // verify that a well-formed id is real or correctly attributed —
5853
+ // that stays a reviewer responsibility — but it makes the
5854
+ // structurally-invalid class impossible to ship. Real CVE
5855
+ // ranges (`CVE-2023-51764 / -51765`) and id-then-space-descriptor
5856
+ // forms pass unchanged.
5857
+ id: "malformed-cve-identifier",
5858
+ 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",
5859
+ regex: /CVE-[0-9]{4}-(?:[0-9]*[A-Za-z]|[0-9]+-[A-Za-z])/,
5860
+ allowlist: [],
5861
+ 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).",
5862
+ },
5863
+
5864
+ {
5865
+ // Codex P1 on v0.13.12 PR #234 — the MX listener's command pump was
5866
+ // made async (gates do DNS / store lookups), and the plaintext
5867
+ // socket.on("data") path was routed through a per-connection
5868
+ // serialization chain (`_feedChunk`). But the post-STARTTLS TLSSocket
5869
+ // fed `_ingestBytes` directly from a sync onData callback that ignored
5870
+ // the returned promise — so on the upgraded socket (where the default
5871
+ // strict/balanced profiles actually run the gates) async gate awaits
5872
+ // could overlap later TLS chunks and gate rejections went unhandled
5873
+ // instead of producing the 421 path. The invariant: the async command
5874
+ // pump (`_ingestBytes`) is fed ONLY through `_feedChunk` (the
5875
+ // `return _ingestBytes(...)` form), so every transport — plaintext and
5876
+ // TLS — shares the one serialized chain. A bare `_ingestBytes(` call
5877
+ // anywhere else is a second, un-serialized feed path. The lookbehind
5878
+ // exempts the function definition (`function _ingestBytes`) and the
5879
+ // single legitimate caller (`return _ingestBytes`); `_ingestBytes` is
5880
+ // unique to lib/mail-server-mx.js so this is effectively file-scoped.
5881
+ id: "mx-ingest-bytes-bypasses-feed-pump",
5882
+ 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",
5883
+ regex: /(?<!function )(?<!return )\b_ingestBytes\s*\(/,
5884
+ allowlist: [
5885
+ // The submission listener has its OWN _ingestBytes, and it is
5886
+ // SYNCHRONOUS — its only async work (SASL AUTH) is handled via
5887
+ // internal .then() chains inside the command handler, and RFC 4954
5888
+ // §4 forbids clients pipelining commands across AUTH until the
5889
+ // response is received, so a sync pump is spec-acceptable there.
5890
+ // There is no async-promise to drop, so the bare `_ingestBytes(`
5891
+ // call is safe. This invariant guards the MX listener's async pump
5892
+ // specifically; if the submission pump is ever made async, route it
5893
+ // through a _feedChunk equivalent and drop this allowlist entry.
5894
+ "lib/mail-server-submission.js",
5895
+ ],
5896
+ 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.",
5747
5897
  },
5748
5898
 
5749
5899
  {
@@ -6356,9 +6506,10 @@ var KNOWN_ANTIPATTERNS = [
6356
6506
  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.",
6357
6507
  },
6358
6508
  {
6359
- // CVE-2026-23993 accepting unknown JOSE alg values via a
6360
- // `switch (alg) { default: ... }` permissive default-branch is
6361
- // the canonical shape. Verifiers MUST throw in the default
6509
+ // Alg-allowlist gate (CWE-347 improper-sig-verification /
6510
+ // CWE-757 algorithm-downgrade) accepting unknown JOSE alg
6511
+ // values via a `switch (alg) { default: ... }` permissive
6512
+ // default-branch is the canonical shape. Verifiers MUST throw in the default
6362
6513
  // branch (no fall-through to a permissive "any signature"
6363
6514
  // path). The detector catches `switch (...alg)` (case-
6364
6515
  // insensitive) where the default branch returns/falls through
@@ -6374,7 +6525,7 @@ var KNOWN_ANTIPATTERNS = [
6374
6525
  // branch, so it doesn't match. Other auth files use
6375
6526
  // explicit if-cascades that throw, also not matched.
6376
6527
  ],
6377
- 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.",
6528
+ 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.",
6378
6529
  },
6379
6530
  {
6380
6531
  id: "inline-codepoint-class-table",
@@ -7263,6 +7414,36 @@ var KNOWN_ANTIPATTERNS = [
7263
7414
  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.",
7264
7415
  },
7265
7416
 
7417
+ {
7418
+ // Sibling to test-promise-settimeout-sleep, for the timer the
7419
+ // setTimeout regex misses: a COUNTED DRAIN-LOOP that reassigns a
7420
+ // promise to its own `.then()` over and over to "flush N
7421
+ // microtasks/ticks" before asserting — the `_waitMicrotasks(n)`
7422
+ // helper shape (`var p = Promise.resolve(); for (...) p = p.then(
7423
+ // () => new Promise(r => setImmediate(r)));`). Like the fixed
7424
+ // setTimeout sleep it guesses a budget; under SMOKE_PARALLEL=64
7425
+ // contention the awaited async work (a cluster-backend DB take,
7426
+ // a scheduler tick-claim) hasn't resolved within the tick count,
7427
+ // so the next assertion reads stale state. This was the recurring
7428
+ // rate-limit-cluster "4th blocked with 429" flake and the
7429
+ // scheduler-exactly-once tick-claim race. A single `await new
7430
+ // Promise(r => setImmediate(r))` event-loop yield is legitimate
7431
+ // and is NOT matched — only the self-reassigning `<x> = <x>.then(`
7432
+ // drain idiom paired with a timer is. Poll the observable
7433
+ // condition with helpers.waitUntil instead.
7434
+ id: "test-microtask-drain-loop-sleep",
7435
+ 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",
7436
+ scanScope: "test",
7437
+ regex: /\b(\w+)\s*=\s*\1\.then\([\s\S]{0,80}?set(?:Immediate|Timeout)\s*\(/,
7438
+ skipCommentLines: true,
7439
+ allowlist: [
7440
+ // The catalog itself carries this pattern as a regex literal +
7441
+ // in this entry's own prose/reason describing the antipattern.
7442
+ "test/layer-0-primitives/codebase-patterns.test.js",
7443
+ ],
7444
+ 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.",
7445
+ },
7446
+
7266
7447
  {
7267
7448
  // v0.10.13 PR #102 macOS hang — stream-throttle.test.js used
7268
7449
  // `setTimeout`-based rate enforcement plus `node:stream.pipeline`
@@ -10012,6 +10193,7 @@ async function run() {
10012
10193
  testHttp2TeardownPaired();
10013
10194
  testNoStrayConsoleCalls();
10014
10195
  testNoUnresolvedMarkers();
10196
+ testNoStaleDefers();
10015
10197
  testNoLiteralNulBytesInSource();
10016
10198
  testParserPrimitivesHaveFuzzHarness();
10017
10199
  testSafeGuardWiredInIndex();
@@ -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";
@@ -3,7 +3,7 @@
3
3
  * b.mail.server.mx — inbound SMTP / MX listener.
4
4
  *
5
5
  * Tests cover the wire-protocol state machine, SMTP-smuggling defense
6
- * (CVE-2023-51764 / CVE-2024-32178 — bare-LF dot-terminator), open-
6
+ * (CVE-2023-51764 / -51765 / -51766 — bare-LF dot-terminator), open-
7
7
  * relay refusal by default, STARTTLS-stripping defense, and the
8
8
  * helper byte-scan primitives (_detectSmugglingShape /
9
9
  * _findDotTerminator / _dotUnstuff).
@@ -263,6 +263,169 @@ async function testStrictProfileRequiresStartTls() {
263
263
  }
264
264
  }
265
265
 
266
+ // Connection-level gates (helo / rbl / greylist) wired into the live
267
+ // state machine. Each gate is an operator-supplied object; we drive the
268
+ // real wire protocol with mock gates and assert the SMTP verdict.
269
+ async function testConnectionGates() {
270
+ var ctx;
271
+ try { ctx = await _makeTestTlsContext(); }
272
+ catch (_e) {
273
+ check("connection gates (skipped — test cert fixture unavailable)", true);
274
+ return;
275
+ }
276
+
277
+ async function _connect(srv) {
278
+ var info = await srv.listen({ port: 0, address: "127.0.0.1" });
279
+ var socket = nodeNet.connect(info.port, "127.0.0.1");
280
+ await new Promise(function (r) { socket.once("connect", r); });
281
+ await _readGreeting(socket);
282
+ return socket;
283
+ }
284
+
285
+ // ---- greylist defer → 450 tempfail at RCPT ----
286
+ var greySrv = b.mail.server.mx.create({
287
+ tlsContext: ctx, profile: "permissive", localDomains: ["example.com"],
288
+ greylist: { check: async function () { return { action: "defer", reason: "first-seen" }; } },
289
+ });
290
+ var grerr = null, greySock;
291
+ try {
292
+ greySock = await _connect(greySrv);
293
+ await _sendCommand(greySock, "EHLO sender.example.com");
294
+ await _sendCommand(greySock, "MAIL FROM:<s@external.com>");
295
+ var greyRcpt = await _sendCommand(greySock, "RCPT TO:<alice@example.com>");
296
+ check("greylist defer → 450 tempfail", /^450 4\.7\.1/.test(greyRcpt));
297
+ greySock.destroy();
298
+ } catch (e) { grerr = e; } finally { await greySrv.close({ timeoutMs: 1000 }); } // allow:raw-time-literal — test-only short drain
299
+ check("greylist gate ran without error", grerr === null);
300
+
301
+ // ---- RBL listed → 554 at RCPT ----
302
+ var rblSrv = b.mail.server.mx.create({
303
+ tlsContext: ctx, profile: "permissive", localDomains: ["example.com"],
304
+ rbl: { query: async function () {
305
+ return { listed: [{ zone: "zen.spamhaus.org" }], allowed: [], neutral: [], errors: [] };
306
+ } },
307
+ });
308
+ try {
309
+ var rblSock = await _connect(rblSrv);
310
+ await _sendCommand(rblSock, "EHLO sender.example.com");
311
+ await _sendCommand(rblSock, "MAIL FROM:<s@external.com>");
312
+ var rblRcpt = await _sendCommand(rblSock, "RCPT TO:<alice@example.com>");
313
+ check("RBL-listed IP → 554 at RCPT", /^554 5\.7\.1/.test(rblRcpt));
314
+ rblSock.destroy();
315
+ } finally { await rblSrv.close({ timeoutMs: 1000 }); } // allow:raw-time-literal — test-only short drain
316
+
317
+ // ---- helo hard-reject → 550 at EHLO ----
318
+ var heloSrv = b.mail.server.mx.create({
319
+ tlsContext: ctx, profile: "permissive", localDomains: ["example.com"],
320
+ helo: { evaluate: async function () { return { action: "reject-shape" }; } },
321
+ });
322
+ try {
323
+ var heloSock = await _connect(heloSrv);
324
+ // A syntactically-valid domain (passes guardDomain) so the refusal
325
+ // comes from the helo GATE (reject-shape), not domain hardening.
326
+ var heloReply = await _sendCommand(heloSock, "EHLO sender.example.com");
327
+ check("helo hard-reject → 550 at EHLO", /^550 5\.7\.1/.test(heloReply));
328
+ heloSock.destroy();
329
+ } finally { await heloSrv.close({ timeoutMs: 1000 }); } // allow:raw-time-literal — test-only short drain
330
+
331
+ // ---- gates that accept → normal flow (gate ran + passed) ----
332
+ var passSrv = b.mail.server.mx.create({
333
+ tlsContext: ctx, profile: "permissive", localDomains: ["example.com"],
334
+ helo: { evaluate: async function () { return { action: "accept" }; } },
335
+ rbl: { query: async function () { return { listed: [], allowed: [], neutral: [], errors: [] }; } },
336
+ greylist: { check: async function () { return { action: "accept", reason: "known" }; } },
337
+ });
338
+ try {
339
+ var passSock = await _connect(passSrv);
340
+ await _sendCommand(passSock, "EHLO sender.example.com");
341
+ await _sendCommand(passSock, "MAIL FROM:<s@external.com>");
342
+ var passRcpt = await _sendCommand(passSock, "RCPT TO:<alice@example.com>");
343
+ check("accepting gates → RCPT 250", /^250 /.test(passRcpt));
344
+ passSock.destroy();
345
+ } finally { await passSrv.close({ timeoutMs: 1000 }); } // allow:raw-time-literal — test-only short drain
346
+
347
+ // ---- async-serial pump: pipelined commands (RFC 2920) keep ordering
348
+ // even though the greylist gate awaits between RCPTs. Send EHLO + MAIL
349
+ // + RCPT in a single write; the deferred RCPT must still answer 450
350
+ // and replies must arrive in order. ----
351
+ var slowCount = 0;
352
+ var pipeSrv = b.mail.server.mx.create({
353
+ tlsContext: ctx, profile: "permissive", localDomains: ["example.com"],
354
+ greylist: { check: async function () {
355
+ slowCount += 1;
356
+ await helpers.waitUntil(function () { return true; }, { timeoutMs: 100, label: "gate async yield" });
357
+ return { action: "defer", reason: "first-seen" };
358
+ } },
359
+ });
360
+ try {
361
+ var pipeInfo = await pipeSrv.listen({ port: 0, address: "127.0.0.1" });
362
+ var pipeSock = nodeNet.connect(pipeInfo.port, "127.0.0.1");
363
+ await new Promise(function (r) { pipeSock.once("connect", r); });
364
+ await _readGreeting(pipeSock);
365
+ // Pipeline EHLO + MAIL + RCPT in one TCP write.
366
+ var combined = await new Promise(function (resolve, reject) {
367
+ var buf = "";
368
+ function onData(chunk) {
369
+ buf += chunk.toString("utf8");
370
+ if (/^450 /m.test(buf)) { pipeSock.removeListener("data", onData); resolve(buf); }
371
+ }
372
+ pipeSock.on("data", onData);
373
+ pipeSock.once("error", reject);
374
+ pipeSock.write("EHLO sender.example.com\r\nMAIL FROM:<s@external.com>\r\nRCPT TO:<alice@example.com>\r\n");
375
+ });
376
+ var idx250ehlo = combined.indexOf("250");
377
+ var idx450 = combined.indexOf("450");
378
+ check("pipelined commands answered in order (250… before 450)",
379
+ idx250ehlo !== -1 && idx450 !== -1 && idx250ehlo < idx450);
380
+ check("greylist gate ran exactly once for the pipelined RCPT", slowCount === 1);
381
+ pipeSock.destroy();
382
+ } finally { await pipeSrv.close({ timeoutMs: 1000 }); } // allow:raw-time-literal — test-only short drain
383
+ }
384
+
385
+ // Gates must run + serialize on the POST-STARTTLS path too — the default
386
+ // strict/balanced profiles require STARTTLS before MAIL, so that's where
387
+ // the gates actually fire. Mint a CA so the client can trust the upgraded
388
+ // connection (no rejectUnauthorized bypass), do a real STARTTLS handshake,
389
+ // and assert the greylist gate produces 450 over TLS.
390
+ async function testGateOverStartTls() {
391
+ var ca, leaf;
392
+ try {
393
+ ca = await b.mtlsEngine.generateCa({ name: "mx-starttls-test-ca" });
394
+ leaf = await b.mtlsEngine.signClientCert({
395
+ cn: "localhost", caCertPem: ca.caCertPem, caKeyPem: ca.caKeyPem,
396
+ usage: "server", sans: ["DNS:localhost", "IP:127.0.0.1"], validityDays: 1,
397
+ });
398
+ } catch (_e) {
399
+ check("gate over STARTTLS (skipped — cert fixture unavailable)", true);
400
+ return;
401
+ }
402
+ var ctx = nodeTls.createSecureContext({ key: leaf.key, cert: leaf.cert });
403
+ var srv = b.mail.server.mx.create({
404
+ tlsContext: ctx, profile: "strict", localDomains: ["example.com"],
405
+ greylist: { check: async function () { return { action: "defer", reason: "first-seen" }; } },
406
+ });
407
+ var info = await srv.listen({ port: 0, address: "127.0.0.1" });
408
+ var plain, tlsSock;
409
+ try {
410
+ plain = nodeNet.connect(info.port, "127.0.0.1");
411
+ await new Promise(function (r) { plain.once("connect", r); });
412
+ await _readGreeting(plain);
413
+ await _sendCommand(plain, "EHLO sender.example.com");
414
+ var stReply = await _sendCommand(plain, "STARTTLS");
415
+ check("STARTTLS → 220 ready", /^220 /.test(stReply));
416
+ tlsSock = nodeTls.connect({ socket: plain, ca: [ca.caCertPem], servername: "localhost" });
417
+ await new Promise(function (r, j) {
418
+ tlsSock.once("secureConnect", r); tlsSock.once("error", j);
419
+ });
420
+ await _sendCommand(tlsSock, "EHLO sender.example.com"); // re-issue per RFC 3207 §4.2
421
+ await _sendCommand(tlsSock, "MAIL FROM:<s@external.com>");
422
+ var rcpt = await _sendCommand(tlsSock, "RCPT TO:<alice@example.com>");
423
+ check("greylist gate runs on the post-STARTTLS serialized pump → 450",
424
+ /^450 4\.7\.1/.test(rcpt));
425
+ tlsSock.destroy();
426
+ } finally { await srv.close({ timeoutMs: 1000 }); } // allow:raw-time-literal — test-only short drain
427
+ }
428
+
266
429
  async function run() {
267
430
  testSurface();
268
431
  testCreateRequiresTlsContext();
@@ -273,6 +436,8 @@ async function run() {
273
436
  await testEhloFlow();
274
437
  await testRelayRefused();
275
438
  await testStrictProfileRequiresStartTls();
439
+ await testConnectionGates();
440
+ await testGateOverStartTls();
276
441
  }
277
442
 
278
443
  module.exports = { run: run };
@@ -25,16 +25,23 @@ var teardownTestDb = helpers.teardownTestDb;
25
25
  var _mockReq = helpers._mockReq;
26
26
  var _mockRes = helpers._mockRes;
27
27
 
28
- function _waitMicrotasks(n) {
29
- // Default to 20 ticks (was 5). CI runners under SMOKE_PARALLEL=64
30
- // contention need many more setImmediate ticks before the rate-limit
31
- // middleware's async take() against the cluster-backend DB has
32
- // finished bumping the counter without enough ticks the next
33
- // fire() reads a stale count and the 4th-request-blocked assertion
34
- // misfires.
35
- var p = Promise.resolve();
36
- for (var i = 0; i < (n || 20); i++) p = p.then(function () { return new Promise(function (r) { setImmediate(r); }); });
37
- return p;
28
+ // Poll until the rate-limit middleware has resolved a request — either
29
+ // next() ran (the okGetter flips true) or a response was captured (a
30
+ // status was set on the mock res). Replaces a fixed setImmediate-tick
31
+ // drain that flaked under SMOKE_PARALLEL=64 contention: the async take()
32
+ // against the cluster-backend DB sometimes hadn't bumped the counter
33
+ // within the tick budget, so the next fire() read a stale count and the
34
+ // "4th-request-blocked" assertion misfired (§11b — poll the observable
35
+ // condition, never guess a tick/time budget).
36
+ function _settle(res, okGetter) {
37
+ return helpers.waitUntil(function () {
38
+ // `ended` is the unambiguous "a response was written" signal —
39
+ // it flips true only when the mock's end() runs. (status starts
40
+ // null, so a `status != null` check would read true before any
41
+ // response and defeat the poll.) A passed request sets ok via
42
+ // next(); a blocked one ends the response.
43
+ return okGetter() || res._captured().ended === true;
44
+ }, { timeoutMs: 5000, label: "rate-limit-cluster: request settled (next ran or response ended)" });
38
45
  }
39
46
 
40
47
  async function testClusterBackendBasicLimit() {
@@ -53,7 +60,7 @@ async function testClusterBackendBasicLimit() {
53
60
  var res = _mockRes();
54
61
  var nextCalled = false;
55
62
  mw(req, res, function () { nextCalled = true; });
56
- await _waitMicrotasks(20);
63
+ await _settle(res, function () { return nextCalled; });
57
64
  return { passed: nextCalled, status: res._captured().status };
58
65
  }
59
66
 
@@ -92,7 +99,7 @@ async function testClusterBackendIndependentKeys() {
92
99
  var res = _mockRes();
93
100
  var ok = false;
94
101
  mw(req, res, function () { ok = true; });
95
- await _waitMicrotasks(20);
102
+ await _settle(res, function () { return ok; });
96
103
  return ok;
97
104
  }
98
105
 
@@ -128,7 +135,7 @@ async function testClusterBackendWindowRollover() {
128
135
  var res = _mockRes();
129
136
  var ok = false;
130
137
  mw(req, res, function () { ok = true; });
131
- await _waitMicrotasks(20);
138
+ await _settle(res, function () { return ok; });
132
139
  return ok;
133
140
  }
134
141
 
@@ -170,7 +177,7 @@ async function testClusterBackendAuditEmit() {
170
177
  var res = _mockRes();
171
178
  var ok = false;
172
179
  mw(req, res, function () { ok = true; });
173
- await _waitMicrotasks(20);
180
+ await _settle(res, function () { return ok; });
174
181
  return ok;
175
182
  }
176
183
  await fire(); // pass
@@ -208,7 +215,7 @@ async function testCustomBackendObject() {
208
215
  var res = _mockRes();
209
216
  var ok = false;
210
217
  mw(req, res, function () { ok = true; });
211
- await _waitMicrotasks(20);
218
+ await _settle(res, function () { return ok; });
212
219
  return ok;
213
220
  }
214
221
  check("custom backend: 1st passes", await fire());
@@ -239,7 +246,7 @@ async function testFailOpenOnBackendError() {
239
246
  var res = _mockRes();
240
247
  var ok = false;
241
248
  mw(req, res, function () { ok = true; });
242
- await _waitMicrotasks(20);
249
+ await _settle(res, function () { return ok; });
243
250
  check("backend error → middleware fails open", ok === true);
244
251
  }
245
252
 
@@ -259,7 +266,7 @@ async function testMemoryFixedWindowBasicLimit() {
259
266
  var res = _mockRes();
260
267
  var ok = false;
261
268
  mw(req, res, function () { ok = true; });
262
- await _waitMicrotasks(20);
269
+ await _settle(res, function () { return ok; });
263
270
  return { passed: ok, status: res._captured().status };
264
271
  }
265
272
 
@@ -286,7 +293,7 @@ async function testMemoryFixedWindowIndependentKeys() {
286
293
  var res = _mockRes();
287
294
  var ok = false;
288
295
  mw(req, res, function () { ok = true; });
289
- await _waitMicrotasks(20);
296
+ await _settle(res, function () { return ok; });
290
297
  return ok;
291
298
  }
292
299
 
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  /**
3
3
  * b.safeIcal — bounded RFC 5545 iCalendar parser. Tests the AST
4
- * surface, line unfolding, RRULE caps (CVE-2024-39687 defense),
4
+ * surface, line unfolding, RRULE caps (calendar-bomb defense),
5
5
  * control-char refusal, property allowlist, profile / posture
6
6
  * cascades.
7
7
  */
@@ -131,7 +131,7 @@ function testRefusesOversizeBytes() {
131
131
  }
132
132
 
133
133
  function testRefusesRecursiveRrule() {
134
- // CVE-2024-39687: RRULE COUNT > 10000 refused regardless of profile.
134
+ // Calendar-bomb defense: RRULE COUNT > 10000 refused regardless of profile.
135
135
  expectRefused("refuses RRULE COUNT > 10000",
136
136
  function () {
137
137
  safeIcal.parse(_ical(_event("RRULE:FREQ=DAILY;COUNT=999999\r\n")));