@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.
- package/CHANGELOG.md +4 -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 +8 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/SECURITY.md +1 -0
- 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-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.9.json +27 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +32 -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 +100 -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 +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
|
@@ -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")));
|
|
@@ -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