@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
|
@@ -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 /
|
|
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) +
|
|
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
|
-
//
|
|
6360
|
-
//
|
|
6361
|
-
//
|
|
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: "
|
|
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 (
|
|
16
|
-
*
|
|
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
|
|
207
|
-
threw && /
|
|
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
|
|
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 —
|
|
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 /
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
//
|
|
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")));
|