@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/asset-manifest.json +3 -3
  3. package/lib/storefront.js +94 -21
  4. package/lib/vendor/MANIFEST.json +2 -2
  5. package/lib/vendor/blamejs/CHANGELOG.md +10 -0
  6. package/lib/vendor/blamejs/README.md +1 -1
  7. package/lib/vendor/blamejs/SECURITY.md +2 -1
  8. package/lib/vendor/blamejs/api-snapshot.json +2 -2
  9. package/lib/vendor/blamejs/lib/archive-read.js +17 -0
  10. package/lib/vendor/blamejs/lib/auth/fal.js +12 -0
  11. package/lib/vendor/blamejs/lib/auth/jwt-external.js +15 -11
  12. package/lib/vendor/blamejs/lib/auth/jwt.js +2 -2
  13. package/lib/vendor/blamejs/lib/auth/oauth.js +7 -6
  14. package/lib/vendor/blamejs/lib/auth/oid4vci.js +3 -3
  15. package/lib/vendor/blamejs/lib/auth/saml.js +15 -12
  16. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -3
  17. package/lib/vendor/blamejs/lib/calendar.js +9 -2
  18. package/lib/vendor/blamejs/lib/circuit-breaker.js +5 -4
  19. package/lib/vendor/blamejs/lib/crypto-hpke.js +1 -1
  20. package/lib/vendor/blamejs/lib/crypto-oprf.js +2 -2
  21. package/lib/vendor/blamejs/lib/crypto.js +2 -2
  22. package/lib/vendor/blamejs/lib/framework-error.js +2 -1
  23. package/lib/vendor/blamejs/lib/guard-filename.js +90 -5
  24. package/lib/vendor/blamejs/lib/guard-jwt.js +3 -2
  25. package/lib/vendor/blamejs/lib/guard-smtp-command.js +2 -2
  26. package/lib/vendor/blamejs/lib/mail-auth.js +2 -2
  27. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +1 -1
  28. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +7 -7
  29. package/lib/vendor/blamejs/lib/mail-crypto.js +1 -1
  30. package/lib/vendor/blamejs/lib/mail-dav.js +5 -4
  31. package/lib/vendor/blamejs/lib/mail-deploy.js +3 -2
  32. package/lib/vendor/blamejs/lib/mail-server-imap.js +1 -1
  33. package/lib/vendor/blamejs/lib/mail-server-jmap.js +1 -1
  34. package/lib/vendor/blamejs/lib/mail-server-managesieve.js +1 -1
  35. package/lib/vendor/blamejs/lib/mail-server-mx.js +142 -47
  36. package/lib/vendor/blamejs/lib/mail-server-submission.js +3 -3
  37. package/lib/vendor/blamejs/lib/mail-store.js +2 -2
  38. package/lib/vendor/blamejs/lib/mail.js +2 -2
  39. package/lib/vendor/blamejs/lib/network-tls.js +10 -7
  40. package/lib/vendor/blamejs/lib/safe-decompress.js +8 -6
  41. package/lib/vendor/blamejs/lib/safe-ical.js +12 -12
  42. package/lib/vendor/blamejs/lib/safe-mime.js +6 -6
  43. package/lib/vendor/blamejs/lib/safe-sieve.js +1 -1
  44. package/lib/vendor/blamejs/lib/safe-smtp.js +1 -1
  45. package/lib/vendor/blamejs/package.json +1 -1
  46. package/lib/vendor/blamejs/release-notes/v0.13.10.json +44 -0
  47. package/lib/vendor/blamejs/release-notes/v0.13.11.json +27 -0
  48. package/lib/vendor/blamejs/release-notes/v0.13.12.json +36 -0
  49. package/lib/vendor/blamejs/release-notes/v0.13.13.json +18 -0
  50. package/lib/vendor/blamejs/release-notes/v0.13.9.json +27 -0
  51. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +76 -0
  52. package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +4 -4
  53. package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +31 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +104 -7
  55. package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +26 -0
  56. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +5 -5
  57. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +1 -1
  58. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +166 -1
  59. package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-cluster.test.js +45 -27
  60. package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +2 -2
  61. package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +12 -12
  62. package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-exactly-once.test.js +15 -9
  63. package/lib/vendor/blamejs/test/layer-0-primitives/websocket-channels.test.js +0 -6
  64. 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 / 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,27 +25,43 @@ 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() {
41
- // limit=3 / windowMs=10s → first 3 requests pass, 4th blocked.
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: 10000,
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 _waitMicrotasks(20);
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: 10000,
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 _waitMicrotasks(20);
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: 10000,
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 _waitMicrotasks(20);
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 past.
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() - 60000, 99);
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: 10000,
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 _waitMicrotasks(20);
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 _waitMicrotasks(20);
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 _waitMicrotasks(20);
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: 10000,
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 _waitMicrotasks(20);
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: 10000,
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 _waitMicrotasks(20);
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 (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")));
@@ -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: 5000,
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: 5000,
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: 5000,
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: 5000 });
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: 5000 });
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: 5000 });
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: 5000 });
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: 5000,
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: 5000 });
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: 5000,
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: 5000,
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: 5000,
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 _waitMicrotasks(5);
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 _waitMicrotasks(10);
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
- await _waitMicrotasks(10);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.1.38",
3
+ "version": "0.2.1",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {