@blamejs/blamejs-shop 0.1.38 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/lib/asset-manifest.json +3 -3
- package/lib/storefront.js +94 -21
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +10 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/SECURITY.md +2 -1
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/archive-read.js +17 -0
- 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/calendar.js +9 -2
- package/lib/vendor/blamejs/lib/circuit-breaker.js +5 -4
- 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-filename.js +90 -5
- 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 +1 -1
- 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 +3 -3
- package/lib/vendor/blamejs/lib/mail-store.js +2 -2
- package/lib/vendor/blamejs/lib/mail.js +2 -2
- 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.13.json +18 -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 +76 -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 +104 -7
- 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 +45 -27
- 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
|
@@ -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,27 +25,43 @@ 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() {
|
|
41
|
-
// limit=3
|
|
48
|
+
// limit=3 → first 3 requests pass, 4th blocked.
|
|
49
|
+
// windowMs is a deliberately-large 1h: the fixed-window counter resets
|
|
50
|
+
// at each `floor(now/windowMs)*windowMs` boundary, so a small window
|
|
51
|
+
// (e.g. 10s) let the test's sequential awaited calls straddle a window
|
|
52
|
+
// boundary under SMOKE_PARALLEL=64 CPU contention — the counter reset
|
|
53
|
+
// mid-test and a "blocked" assertion saw a fresh count (the recurring
|
|
54
|
+
// rate-limit-cluster flake). A 1h window makes wall-clock progress
|
|
55
|
+
// during the sub-second test negligible vs the window, so all calls
|
|
56
|
+
// land in one window. (The rollover test below keeps a small window
|
|
57
|
+
// on purpose — it needs an aged row to fall into a prior window.)
|
|
42
58
|
var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-rl-"));
|
|
43
59
|
try {
|
|
44
60
|
await setupTestDb(tmpDir);
|
|
45
61
|
var mw = b.middleware.rateLimit({
|
|
46
62
|
backend: "cluster",
|
|
47
63
|
limit: 3,
|
|
48
|
-
windowMs:
|
|
64
|
+
windowMs: 3600000,
|
|
49
65
|
});
|
|
50
66
|
|
|
51
67
|
async function fire() {
|
|
@@ -53,7 +69,7 @@ async function testClusterBackendBasicLimit() {
|
|
|
53
69
|
var res = _mockRes();
|
|
54
70
|
var nextCalled = false;
|
|
55
71
|
mw(req, res, function () { nextCalled = true; });
|
|
56
|
-
await
|
|
72
|
+
await _settle(res, function () { return nextCalled; });
|
|
57
73
|
return { passed: nextCalled, status: res._captured().status };
|
|
58
74
|
}
|
|
59
75
|
|
|
@@ -83,7 +99,7 @@ async function testClusterBackendIndependentKeys() {
|
|
|
83
99
|
var mw = b.middleware.rateLimit({
|
|
84
100
|
backend: "cluster",
|
|
85
101
|
limit: 2,
|
|
86
|
-
windowMs:
|
|
102
|
+
windowMs: 3600000,
|
|
87
103
|
keyFn: function (req) { return req.headers["x-key"] || "default"; },
|
|
88
104
|
});
|
|
89
105
|
|
|
@@ -92,7 +108,7 @@ async function testClusterBackendIndependentKeys() {
|
|
|
92
108
|
var res = _mockRes();
|
|
93
109
|
var ok = false;
|
|
94
110
|
mw(req, res, function () { ok = true; });
|
|
95
|
-
await
|
|
111
|
+
await _settle(res, function () { return ok; });
|
|
96
112
|
return ok;
|
|
97
113
|
}
|
|
98
114
|
|
|
@@ -120,7 +136,7 @@ async function testClusterBackendWindowRollover() {
|
|
|
120
136
|
var mw = b.middleware.rateLimit({
|
|
121
137
|
backend: "cluster",
|
|
122
138
|
limit: 2,
|
|
123
|
-
windowMs:
|
|
139
|
+
windowMs: 3600000, // 1h — same straddle-immunity as the other tests
|
|
124
140
|
});
|
|
125
141
|
|
|
126
142
|
async function fire() {
|
|
@@ -128,7 +144,7 @@ async function testClusterBackendWindowRollover() {
|
|
|
128
144
|
var res = _mockRes();
|
|
129
145
|
var ok = false;
|
|
130
146
|
mw(req, res, function () { ok = true; });
|
|
131
|
-
await
|
|
147
|
+
await _settle(res, function () { return ok; });
|
|
132
148
|
return ok;
|
|
133
149
|
}
|
|
134
150
|
|
|
@@ -136,10 +152,12 @@ async function testClusterBackendWindowRollover() {
|
|
|
136
152
|
await fire(); await fire();
|
|
137
153
|
check("rollover: 3rd in same window blocked", !(await fire()));
|
|
138
154
|
|
|
139
|
-
// Fast-forward: rewrite the row so its windowStart is well in the
|
|
155
|
+
// Fast-forward: rewrite the row so its windowStart is well in the
|
|
156
|
+
// past — > one window (1h) back so the next take sees it as a prior
|
|
157
|
+
// window and rolls the count over.
|
|
140
158
|
b.db.prepare(
|
|
141
159
|
"UPDATE _blamejs_rate_limit_counters SET windowStart = ?, count = ?"
|
|
142
|
-
).run(Date.now() -
|
|
160
|
+
).run(Date.now() - 7200000, 99);
|
|
143
161
|
|
|
144
162
|
// The next take's INSERT...ON CONFLICT sees an older windowStart
|
|
145
163
|
// and rolls count back to 1 → request passes.
|
|
@@ -162,7 +180,7 @@ async function testClusterBackendAuditEmit() {
|
|
|
162
180
|
var mw = b.middleware.rateLimit({
|
|
163
181
|
backend: "cluster",
|
|
164
182
|
limit: 1,
|
|
165
|
-
windowMs:
|
|
183
|
+
windowMs: 3600000,
|
|
166
184
|
});
|
|
167
185
|
|
|
168
186
|
async function fire() {
|
|
@@ -170,7 +188,7 @@ async function testClusterBackendAuditEmit() {
|
|
|
170
188
|
var res = _mockRes();
|
|
171
189
|
var ok = false;
|
|
172
190
|
mw(req, res, function () { ok = true; });
|
|
173
|
-
await
|
|
191
|
+
await _settle(res, function () { return ok; });
|
|
174
192
|
return ok;
|
|
175
193
|
}
|
|
176
194
|
await fire(); // pass
|
|
@@ -208,7 +226,7 @@ async function testCustomBackendObject() {
|
|
|
208
226
|
var res = _mockRes();
|
|
209
227
|
var ok = false;
|
|
210
228
|
mw(req, res, function () { ok = true; });
|
|
211
|
-
await
|
|
229
|
+
await _settle(res, function () { return ok; });
|
|
212
230
|
return ok;
|
|
213
231
|
}
|
|
214
232
|
check("custom backend: 1st passes", await fire());
|
|
@@ -239,7 +257,7 @@ async function testFailOpenOnBackendError() {
|
|
|
239
257
|
var res = _mockRes();
|
|
240
258
|
var ok = false;
|
|
241
259
|
mw(req, res, function () { ok = true; });
|
|
242
|
-
await
|
|
260
|
+
await _settle(res, function () { return ok; });
|
|
243
261
|
check("backend error → middleware fails open", ok === true);
|
|
244
262
|
}
|
|
245
263
|
|
|
@@ -251,7 +269,7 @@ async function testMemoryFixedWindowBasicLimit() {
|
|
|
251
269
|
backend: "memory",
|
|
252
270
|
algorithm: "fixed-window",
|
|
253
271
|
max: 3,
|
|
254
|
-
windowMs:
|
|
272
|
+
windowMs: 3600000,
|
|
255
273
|
});
|
|
256
274
|
|
|
257
275
|
async function fire() {
|
|
@@ -259,7 +277,7 @@ async function testMemoryFixedWindowBasicLimit() {
|
|
|
259
277
|
var res = _mockRes();
|
|
260
278
|
var ok = false;
|
|
261
279
|
mw(req, res, function () { ok = true; });
|
|
262
|
-
await
|
|
280
|
+
await _settle(res, function () { return ok; });
|
|
263
281
|
return { passed: ok, status: res._captured().status };
|
|
264
282
|
}
|
|
265
283
|
|
|
@@ -277,7 +295,7 @@ async function testMemoryFixedWindowIndependentKeys() {
|
|
|
277
295
|
backend: "memory",
|
|
278
296
|
algorithm: "fixed-window",
|
|
279
297
|
max: 2,
|
|
280
|
-
windowMs:
|
|
298
|
+
windowMs: 3600000,
|
|
281
299
|
keyFn: function (req) { return req.headers["x-key"] || "default"; },
|
|
282
300
|
});
|
|
283
301
|
|
|
@@ -286,7 +304,7 @@ async function testMemoryFixedWindowIndependentKeys() {
|
|
|
286
304
|
var res = _mockRes();
|
|
287
305
|
var ok = false;
|
|
288
306
|
mw(req, res, function () { ok = true; });
|
|
289
|
-
await
|
|
307
|
+
await _settle(res, function () { return ok; });
|
|
290
308
|
return ok;
|
|
291
309
|
}
|
|
292
310
|
|
|
@@ -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")));
|
|
@@ -26,7 +26,7 @@ async function testHappyPath() {
|
|
|
26
26
|
var r = await b.sandbox.run({
|
|
27
27
|
source: "return { upper: input.name.toUpperCase(), len: input.name.length };",
|
|
28
28
|
input: { name: "alice" },
|
|
29
|
-
timeoutMs:
|
|
29
|
+
timeoutMs: 10000,
|
|
30
30
|
});
|
|
31
31
|
check("happy path returns wrapped result", typeof r === "object");
|
|
32
32
|
check("happy path produces result.result.upper", r.result && r.result.upper === "ALICE");
|
|
@@ -40,7 +40,7 @@ async function testAllowedBuiltins() {
|
|
|
40
40
|
source: "return { ts: Date.now() > 0, sqrt: Math.sqrt(16) };",
|
|
41
41
|
input: {},
|
|
42
42
|
allowed: ["Date", "Math"],
|
|
43
|
-
timeoutMs:
|
|
43
|
+
timeoutMs: 10000,
|
|
44
44
|
});
|
|
45
45
|
check("allowed Date works", r.result.ts === true);
|
|
46
46
|
check("allowed Math works", r.result.sqrt === 4);
|
|
@@ -51,7 +51,7 @@ async function testBadAllowedRejected() {
|
|
|
51
51
|
await b.sandbox.run({
|
|
52
52
|
source: "return null;",
|
|
53
53
|
allowed: ["process"],
|
|
54
|
-
timeoutMs:
|
|
54
|
+
timeoutMs: 10000,
|
|
55
55
|
});
|
|
56
56
|
check("bad-allowed should have refused", false);
|
|
57
57
|
} catch (e) {
|
|
@@ -62,13 +62,13 @@ async function testBadAllowedRejected() {
|
|
|
62
62
|
|
|
63
63
|
async function testBadSourceRejected() {
|
|
64
64
|
try {
|
|
65
|
-
await b.sandbox.run({ source: "", timeoutMs:
|
|
65
|
+
await b.sandbox.run({ source: "", timeoutMs: 10000 });
|
|
66
66
|
check("empty source should refuse", false);
|
|
67
67
|
} catch (e) {
|
|
68
68
|
check("empty source refused", e && e.code === "sandbox/bad-source");
|
|
69
69
|
}
|
|
70
70
|
try {
|
|
71
|
-
await b.sandbox.run({ source: 42, timeoutMs:
|
|
71
|
+
await b.sandbox.run({ source: 42, timeoutMs: 10000 });
|
|
72
72
|
check("non-string source should refuse", false);
|
|
73
73
|
} catch (e) {
|
|
74
74
|
check("non-string source refused", e && e.code === "sandbox/bad-source");
|
|
@@ -92,7 +92,7 @@ async function testBadTimeoutRejected() {
|
|
|
92
92
|
|
|
93
93
|
async function testBadMaxBytesRejected() {
|
|
94
94
|
try {
|
|
95
|
-
await b.sandbox.run({ source: "return 1;", maxBytes: 100, timeoutMs:
|
|
95
|
+
await b.sandbox.run({ source: "return 1;", maxBytes: 100, timeoutMs: 10000 });
|
|
96
96
|
check("under-floor maxBytes should refuse", false);
|
|
97
97
|
} catch (e) {
|
|
98
98
|
check("under-floor maxBytes refused", e && e.code === "sandbox/bad-max-bytes");
|
|
@@ -113,7 +113,7 @@ async function testTimeoutEnforced() {
|
|
|
113
113
|
|
|
114
114
|
async function testParseError() {
|
|
115
115
|
try {
|
|
116
|
-
await b.sandbox.run({ source: "this is not valid javascript ((", timeoutMs:
|
|
116
|
+
await b.sandbox.run({ source: "this is not valid javascript ((", timeoutMs: 10000 });
|
|
117
117
|
check("malformed source should refuse", false);
|
|
118
118
|
} catch (e) {
|
|
119
119
|
check("parse-error returned", e && e.code === "sandbox/parse-error");
|
|
@@ -124,7 +124,7 @@ async function testRuntimeError() {
|
|
|
124
124
|
try {
|
|
125
125
|
await b.sandbox.run({
|
|
126
126
|
source: "throw new Error('boom');",
|
|
127
|
-
timeoutMs:
|
|
127
|
+
timeoutMs: 10000,
|
|
128
128
|
});
|
|
129
129
|
check("throwing source should reject", false);
|
|
130
130
|
} catch (e) {
|
|
@@ -138,7 +138,7 @@ async function testBadInput() {
|
|
|
138
138
|
var circular = {};
|
|
139
139
|
circular.self = circular;
|
|
140
140
|
try {
|
|
141
|
-
await b.sandbox.run({ source: "return 1;", input: circular, timeoutMs:
|
|
141
|
+
await b.sandbox.run({ source: "return 1;", input: circular, timeoutMs: 10000 });
|
|
142
142
|
check("circular input should refuse", false);
|
|
143
143
|
} catch (e) {
|
|
144
144
|
check("bad-input refused", e && e.code === "sandbox/bad-input");
|
|
@@ -151,7 +151,7 @@ async function testContainment() {
|
|
|
151
151
|
try {
|
|
152
152
|
await b.sandbox.run({
|
|
153
153
|
source: "var x = require; return typeof x;",
|
|
154
|
-
timeoutMs:
|
|
154
|
+
timeoutMs: 10000,
|
|
155
155
|
});
|
|
156
156
|
check("require should not be reachable", false);
|
|
157
157
|
} catch (e) {
|
|
@@ -163,7 +163,7 @@ async function testProcessUnreachable() {
|
|
|
163
163
|
try {
|
|
164
164
|
await b.sandbox.run({
|
|
165
165
|
source: "return process.env.HOME;",
|
|
166
|
-
timeoutMs:
|
|
166
|
+
timeoutMs: 10000,
|
|
167
167
|
});
|
|
168
168
|
check("process should not be reachable", false);
|
|
169
169
|
} catch (e) {
|
|
@@ -176,7 +176,7 @@ async function testNoNetworkAccess() {
|
|
|
176
176
|
try {
|
|
177
177
|
await b.sandbox.run({
|
|
178
178
|
source: "var http = require('http'); return 1;",
|
|
179
|
-
timeoutMs:
|
|
179
|
+
timeoutMs: 10000,
|
|
180
180
|
});
|
|
181
181
|
check("network access should refuse", false);
|
|
182
182
|
} catch (e) {
|
|
@@ -21,12 +21,6 @@ var check = helpers.check;
|
|
|
21
21
|
var setupTestDb = helpers.setupTestDb;
|
|
22
22
|
var teardownTestDb = helpers.teardownTestDb;
|
|
23
23
|
|
|
24
|
-
function _waitMicrotasks(n) {
|
|
25
|
-
var p = Promise.resolve();
|
|
26
|
-
for (var i = 0; i < (n || 5); i++) p = p.then(function () { return new Promise(function (r) { setImmediate(r); }); });
|
|
27
|
-
return p;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
24
|
async function testSingleNodeNoTickClaim() {
|
|
31
25
|
// No opts.cluster wired → tick-claim path skipped entirely. fires
|
|
32
26
|
// increments normally, tickClaimLost stays 0, no rows in the ticks
|
|
@@ -38,7 +32,10 @@ async function testSingleNodeNoTickClaim() {
|
|
|
38
32
|
var sched = b.scheduler.create({ audit: false });
|
|
39
33
|
sched.schedule({ name: "single", every: 60000, run: async function () { fired++; } });
|
|
40
34
|
sched._fireOnce("single");
|
|
41
|
-
await
|
|
35
|
+
await helpers.waitUntil(function () {
|
|
36
|
+
var t = sched.list()[0];
|
|
37
|
+
return t && t.fires === 1;
|
|
38
|
+
}, { timeoutMs: 5000, label: "scheduler single-node: fire callback completed" });
|
|
42
39
|
var listed = sched.list()[0];
|
|
43
40
|
check("single-node: fires increments", listed.fires === 1);
|
|
44
41
|
check("single-node: tickClaimLost stays 0", listed.tickClaimLost === 0);
|
|
@@ -69,7 +66,10 @@ async function testClusterWinnerInsertsTickRow() {
|
|
|
69
66
|
var nominalRun = task.nextRun;
|
|
70
67
|
|
|
71
68
|
sched._fireOnce("winner");
|
|
72
|
-
await
|
|
69
|
+
await helpers.waitUntil(function () {
|
|
70
|
+
var t = sched.list()[0];
|
|
71
|
+
return t && t.fires === 1;
|
|
72
|
+
}, { timeoutMs: 5000, label: "scheduler winner: tick-claim won + fire completed" });
|
|
73
73
|
|
|
74
74
|
var listed = sched.list()[0];
|
|
75
75
|
check("winner: fires increments", listed.fires === 1);
|
|
@@ -116,7 +116,13 @@ async function testClusterLoserSkipsAndCounts() {
|
|
|
116
116
|
);
|
|
117
117
|
|
|
118
118
|
sched._fireOnce("loser");
|
|
119
|
-
|
|
119
|
+
// The lost tick-claim is observable as tickClaimLost incrementing —
|
|
120
|
+
// poll on that rather than a fixed tick budget. Once it lands, the
|
|
121
|
+
// skipped run (fires stays 0) is guaranteed.
|
|
122
|
+
await helpers.waitUntil(function () {
|
|
123
|
+
var t = sched.list()[0];
|
|
124
|
+
return t && t.tickClaimLost === 1;
|
|
125
|
+
}, { timeoutMs: 5000, label: "scheduler loser: tick-claim lost recorded" });
|
|
120
126
|
|
|
121
127
|
var listed = sched.list()[0];
|
|
122
128
|
check("loser: fires stays 0", listed.fires === 0);
|
|
@@ -16,12 +16,6 @@ var check = helpers.check;
|
|
|
16
16
|
var setupTestDb = helpers.setupTestDb;
|
|
17
17
|
var teardownTestDb = helpers.teardownTestDb;
|
|
18
18
|
|
|
19
|
-
function _waitMicrotasks(n) {
|
|
20
|
-
var p = Promise.resolve();
|
|
21
|
-
for (var i = 0; i < (n || 5); i++) p = p.then(function () { return new Promise(function (r) { setImmediate(r); }); });
|
|
22
|
-
return p;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
19
|
// Minimal connection mock — EventEmitter with .send() that captures
|
|
26
20
|
// what was sent. Mirrors the WebSocketConnection surface that
|
|
27
21
|
// websocket-channels actually touches (.send + .on('close')).
|
package/package.json
CHANGED