@blamejs/blamejs-shop 0.1.38 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +4 -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 +8 -0
  6. package/lib/vendor/blamejs/README.md +1 -1
  7. package/lib/vendor/blamejs/SECURITY.md +1 -0
  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-jwt.js +3 -2
  24. package/lib/vendor/blamejs/lib/guard-smtp-command.js +2 -2
  25. package/lib/vendor/blamejs/lib/mail-auth.js +2 -2
  26. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +1 -1
  27. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +7 -7
  28. package/lib/vendor/blamejs/lib/mail-crypto.js +1 -1
  29. package/lib/vendor/blamejs/lib/mail-dav.js +5 -4
  30. package/lib/vendor/blamejs/lib/mail-deploy.js +3 -2
  31. package/lib/vendor/blamejs/lib/mail-server-imap.js +1 -1
  32. package/lib/vendor/blamejs/lib/mail-server-jmap.js +1 -1
  33. package/lib/vendor/blamejs/lib/mail-server-managesieve.js +1 -1
  34. package/lib/vendor/blamejs/lib/mail-server-mx.js +142 -47
  35. package/lib/vendor/blamejs/lib/mail-server-submission.js +3 -3
  36. package/lib/vendor/blamejs/lib/mail-store.js +2 -2
  37. package/lib/vendor/blamejs/lib/mail.js +2 -2
  38. package/lib/vendor/blamejs/lib/network-tls.js +10 -7
  39. package/lib/vendor/blamejs/lib/safe-decompress.js +8 -6
  40. package/lib/vendor/blamejs/lib/safe-ical.js +12 -12
  41. package/lib/vendor/blamejs/lib/safe-mime.js +6 -6
  42. package/lib/vendor/blamejs/lib/safe-sieve.js +1 -1
  43. package/lib/vendor/blamejs/lib/safe-smtp.js +1 -1
  44. package/lib/vendor/blamejs/package.json +1 -1
  45. package/lib/vendor/blamejs/release-notes/v0.13.10.json +44 -0
  46. package/lib/vendor/blamejs/release-notes/v0.13.11.json +27 -0
  47. package/lib/vendor/blamejs/release-notes/v0.13.12.json +36 -0
  48. package/lib/vendor/blamejs/release-notes/v0.13.9.json +27 -0
  49. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +32 -0
  50. package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +4 -4
  51. package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +31 -0
  52. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +100 -7
  53. package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +26 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +5 -5
  55. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +1 -1
  56. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +166 -1
  57. package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-cluster.test.js +25 -18
  58. package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +2 -2
  59. package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +12 -12
  60. package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-exactly-once.test.js +15 -9
  61. package/lib/vendor/blamejs/test/layer-0-primitives/websocket-channels.test.js +0 -6
  62. 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,16 +25,23 @@ var teardownTestDb = helpers.teardownTestDb;
25
25
  var _mockReq = helpers._mockReq;
26
26
  var _mockRes = helpers._mockRes;
27
27
 
28
- function _waitMicrotasks(n) {
29
- // Default to 20 ticks (was 5). CI runners under SMOKE_PARALLEL=64
30
- // contention need many more setImmediate ticks before the rate-limit
31
- // middleware's async take() against the cluster-backend DB has
32
- // finished bumping the counter without enough ticks the next
33
- // fire() reads a stale count and the 4th-request-blocked assertion
34
- // misfires.
35
- var p = Promise.resolve();
36
- for (var i = 0; i < (n || 20); i++) p = p.then(function () { return new Promise(function (r) { setImmediate(r); }); });
37
- return p;
28
+ // Poll until the rate-limit middleware has resolved a request — either
29
+ // next() ran (the okGetter flips true) or a response was captured (a
30
+ // status was set on the mock res). Replaces a fixed setImmediate-tick
31
+ // drain that flaked under SMOKE_PARALLEL=64 contention: the async take()
32
+ // against the cluster-backend DB sometimes hadn't bumped the counter
33
+ // within the tick budget, so the next fire() read a stale count and the
34
+ // "4th-request-blocked" assertion misfired (§11b — poll the observable
35
+ // condition, never guess a tick/time budget).
36
+ function _settle(res, okGetter) {
37
+ return helpers.waitUntil(function () {
38
+ // `ended` is the unambiguous "a response was written" signal —
39
+ // it flips true only when the mock's end() runs. (status starts
40
+ // null, so a `status != null` check would read true before any
41
+ // response and defeat the poll.) A passed request sets ok via
42
+ // next(); a blocked one ends the response.
43
+ return okGetter() || res._captured().ended === true;
44
+ }, { timeoutMs: 5000, label: "rate-limit-cluster: request settled (next ran or response ended)" });
38
45
  }
39
46
 
40
47
  async function testClusterBackendBasicLimit() {
@@ -53,7 +60,7 @@ async function testClusterBackendBasicLimit() {
53
60
  var res = _mockRes();
54
61
  var nextCalled = false;
55
62
  mw(req, res, function () { nextCalled = true; });
56
- await _waitMicrotasks(20);
63
+ await _settle(res, function () { return nextCalled; });
57
64
  return { passed: nextCalled, status: res._captured().status };
58
65
  }
59
66
 
@@ -92,7 +99,7 @@ async function testClusterBackendIndependentKeys() {
92
99
  var res = _mockRes();
93
100
  var ok = false;
94
101
  mw(req, res, function () { ok = true; });
95
- await _waitMicrotasks(20);
102
+ await _settle(res, function () { return ok; });
96
103
  return ok;
97
104
  }
98
105
 
@@ -128,7 +135,7 @@ async function testClusterBackendWindowRollover() {
128
135
  var res = _mockRes();
129
136
  var ok = false;
130
137
  mw(req, res, function () { ok = true; });
131
- await _waitMicrotasks(20);
138
+ await _settle(res, function () { return ok; });
132
139
  return ok;
133
140
  }
134
141
 
@@ -170,7 +177,7 @@ async function testClusterBackendAuditEmit() {
170
177
  var res = _mockRes();
171
178
  var ok = false;
172
179
  mw(req, res, function () { ok = true; });
173
- await _waitMicrotasks(20);
180
+ await _settle(res, function () { return ok; });
174
181
  return ok;
175
182
  }
176
183
  await fire(); // pass
@@ -208,7 +215,7 @@ async function testCustomBackendObject() {
208
215
  var res = _mockRes();
209
216
  var ok = false;
210
217
  mw(req, res, function () { ok = true; });
211
- await _waitMicrotasks(20);
218
+ await _settle(res, function () { return ok; });
212
219
  return ok;
213
220
  }
214
221
  check("custom backend: 1st passes", await fire());
@@ -239,7 +246,7 @@ async function testFailOpenOnBackendError() {
239
246
  var res = _mockRes();
240
247
  var ok = false;
241
248
  mw(req, res, function () { ok = true; });
242
- await _waitMicrotasks(20);
249
+ await _settle(res, function () { return ok; });
243
250
  check("backend error → middleware fails open", ok === true);
244
251
  }
245
252
 
@@ -259,7 +266,7 @@ async function testMemoryFixedWindowBasicLimit() {
259
266
  var res = _mockRes();
260
267
  var ok = false;
261
268
  mw(req, res, function () { ok = true; });
262
- await _waitMicrotasks(20);
269
+ await _settle(res, function () { return ok; });
263
270
  return { passed: ok, status: res._captured().status };
264
271
  }
265
272
 
@@ -286,7 +293,7 @@ async function testMemoryFixedWindowIndependentKeys() {
286
293
  var res = _mockRes();
287
294
  var ok = false;
288
295
  mw(req, res, function () { ok = true; });
289
- await _waitMicrotasks(20);
296
+ await _settle(res, function () { return ok; });
290
297
  return ok;
291
298
  }
292
299
 
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  /**
3
3
  * b.safeIcal — bounded RFC 5545 iCalendar parser. Tests the AST
4
- * surface, line unfolding, RRULE caps (CVE-2024-39687 defense),
4
+ * surface, line unfolding, RRULE caps (calendar-bomb defense),
5
5
  * control-char refusal, property allowlist, profile / posture
6
6
  * cascades.
7
7
  */
@@ -131,7 +131,7 @@ function testRefusesOversizeBytes() {
131
131
  }
132
132
 
133
133
  function testRefusesRecursiveRrule() {
134
- // CVE-2024-39687: RRULE COUNT > 10000 refused regardless of profile.
134
+ // Calendar-bomb defense: RRULE COUNT > 10000 refused regardless of profile.
135
135
  expectRefused("refuses RRULE COUNT > 10000",
136
136
  function () {
137
137
  safeIcal.parse(_ical(_event("RRULE:FREQ=DAILY;COUNT=999999\r\n")));
@@ -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.0",
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": {