@blamejs/blamejs-shop 0.4.84 → 0.4.86

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 (61) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/inventory-locations.js +86 -0
  4. package/lib/order.js +4 -2
  5. package/lib/security-middleware.js +1 -0
  6. package/lib/stock-transfers.js +21 -14
  7. package/lib/vendor/MANIFEST.json +71 -39
  8. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  9. package/lib/vendor/blamejs/api-snapshot.json +11 -3
  10. package/lib/vendor/blamejs/lib/atomic-file.js +34 -0
  11. package/lib/vendor/blamejs/lib/auth/fido-mds3.js +10 -0
  12. package/lib/vendor/blamejs/lib/auth/password.js +1 -0
  13. package/lib/vendor/blamejs/lib/auth/saml.js +11 -3
  14. package/lib/vendor/blamejs/lib/daemon.js +4 -1
  15. package/lib/vendor/blamejs/lib/external-db.js +131 -0
  16. package/lib/vendor/blamejs/lib/graphql-federation.js +25 -15
  17. package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +1 -0
  18. package/lib/vendor/blamejs/lib/log-stream-local.js +14 -1
  19. package/lib/vendor/blamejs/lib/log-stream-otlp.js +1 -0
  20. package/lib/vendor/blamejs/lib/log-stream-webhook.js +1 -0
  21. package/lib/vendor/blamejs/lib/mail-auth.js +69 -14
  22. package/lib/vendor/blamejs/lib/mail-bimi.js +6 -0
  23. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +10 -0
  24. package/lib/vendor/blamejs/lib/mail-dkim.js +68 -15
  25. package/lib/vendor/blamejs/lib/mail.js +39 -0
  26. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +6 -2
  27. package/lib/vendor/blamejs/lib/network-dns-resolver.js +61 -11
  28. package/lib/vendor/blamejs/lib/network-dns.js +47 -2
  29. package/lib/vendor/blamejs/lib/network-nts.js +16 -0
  30. package/lib/vendor/blamejs/lib/network-proxy.js +55 -2
  31. package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +1 -0
  32. package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +1 -0
  33. package/lib/vendor/blamejs/lib/object-store/http-request.js +4 -0
  34. package/lib/vendor/blamejs/lib/outbox.js +29 -0
  35. package/lib/vendor/blamejs/lib/queue-sqs.js +1 -0
  36. package/lib/vendor/blamejs/lib/request-helpers.js +25 -6
  37. package/lib/vendor/blamejs/lib/session-device-binding.js +46 -24
  38. package/lib/vendor/blamejs/lib/session.js +85 -28
  39. package/lib/vendor/blamejs/package.json +1 -1
  40. package/lib/vendor/blamejs/release-notes/v0.15.16.json +94 -0
  41. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +51 -0
  42. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-append-nofollow.test.js +87 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +48 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-non-atomic-backend.test.js +200 -0
  45. package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3-cert-bad-validity.test.js +159 -0
  46. package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +49 -2
  47. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-dmarc-policy-failclosed.test.js +139 -0
  48. package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi-cert-validity-unparseable.test.js +137 -0
  49. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime-bad-validity.test.js +134 -0
  50. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim-numericdate-failclosed.test.js +155 -0
  51. package/lib/vendor/blamejs/test/layer-0-primitives/mail-proxy-framing-bounds.test.js +263 -0
  52. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-lookup-timeout-default.test.js +116 -0
  53. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver-timeout.test.js +126 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/network-nts-handshake-byte-cap.test.js +127 -0
  55. package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notbefore.test.js +238 -0
  56. package/lib/vendor/blamejs/test/layer-0-primitives/session-destroy-all-store-backed.test.js +128 -0
  57. package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding-ipv6-canonical-and-no-store.test.js +202 -0
  58. package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +18 -9
  59. package/lib/vendor/blamejs/test/layer-0-primitives/webhook-verify-nonce-atomic.test.js +169 -0
  60. package/lib/webhooks.js +38 -9
  61. package/package.json +1 -1
@@ -0,0 +1,263 @@
1
+ "use strict";
2
+ /**
3
+ * SMTP client (b.mail.transports.smtp) + proxy CONNECT tunnel
4
+ * (b.network.proxy.agentFor) framing-buffer + wall-clock bounds.
5
+ *
6
+ * Both paths accumulate bytes off a peer socket until a delimiter
7
+ * (CRLF / CRLFCRLF). Without a cap, a peer that streams bytes and never
8
+ * sends the delimiter grows the accumulator without limit (OOM). Without
9
+ * an absolute deadline, a slow-trickle peer resets the per-socket idle
10
+ * timer forever and the operation hangs.
11
+ *
12
+ * These drive the real shipped consumer surface:
13
+ * - b.mail.transports.smtp(...).send(msg) against a hostile net.Server
14
+ * - b.network.proxy.agentFor(url).createConnection(...) against a
15
+ * hostile CONNECT proxy
16
+ *
17
+ * RED (pre-fix): unbounded `buffer += data` / `Buffer.concat` → the send
18
+ * never settles (the server streams forever); the test times out / OOMs.
19
+ * GREEN (post-fix): the send rejects promptly with a bounded-error code.
20
+ */
21
+
22
+ var net = require("node:net");
23
+
24
+ var helpers = require("../helpers");
25
+ var b = helpers.b;
26
+ var check = helpers.check;
27
+
28
+ function listen(server) {
29
+ return new Promise(function (resolve, reject) {
30
+ server.on("error", reject);
31
+ server.listen(0, "127.0.0.1", function () {
32
+ resolve(server.address().port);
33
+ });
34
+ });
35
+ }
36
+
37
+ function closeServer(server) {
38
+ return new Promise(function (resolve) {
39
+ try { server.close(function () { resolve(); }); }
40
+ catch (_e) { resolve(); }
41
+ });
42
+ }
43
+
44
+ // ---- (1) SMTP: framing buffer is bounded against a never-CRLF peer ----
45
+
46
+ async function testSmtpResponseTooLargeBounded() {
47
+ var streamTimer = null;
48
+ // A server that completes the TCP handshake, sends a 220 greeting (so
49
+ // the client moves past connect), then on the first client command
50
+ // streams non-CRLF bytes forever.
51
+ var server = net.createServer(function (sock) {
52
+ // Client destroys mid-blast at the cap; swallow the resulting ECONNRESET.
53
+ sock.on("error", function () { if (streamTimer) { clearInterval(streamTimer); streamTimer = null; } });
54
+ sock.write("220 evil.example ESMTP\r\n");
55
+ sock.on("data", function () {
56
+ // Client just sent EHLO; respond with an unbounded non-CRLF blast.
57
+ var blast = Buffer.alloc(16 * 1024, 0x41); // 16 KiB of 'A', no CRLF
58
+ streamTimer = setInterval(function () {
59
+ if (!sock.writable) { clearInterval(streamTimer); return; }
60
+ try { sock.write(blast); } catch (_e) { clearInterval(streamTimer); }
61
+ }, 1);
62
+ });
63
+ });
64
+ var port = await listen(server);
65
+
66
+ var transport = b.mail.transports.smtp({
67
+ host: "127.0.0.1",
68
+ port: port,
69
+ timeoutMs: 60000, // idle timer kept high so it can't mask the cap
70
+ });
71
+
72
+ var err = null;
73
+ try {
74
+ await transport.send({
75
+ from: "ops@example.com",
76
+ to: ["alice@dest.example"],
77
+ subject: "hi",
78
+ text: "body",
79
+ });
80
+ } catch (e) { err = e; }
81
+
82
+ if (streamTimer) clearInterval(streamTimer);
83
+ await closeServer(server);
84
+
85
+ check("smtp: never-CRLF peer rejects (does not hang/OOM)", err !== null);
86
+ check("smtp: rejects with response-too-large",
87
+ err && typeof err.message === "string" &&
88
+ err.message.indexOf("response-too-large") !== -1);
89
+ check("smtp: error is a MailError", err && err.isMailError === true);
90
+ }
91
+
92
+ // ---- (2) SMTP: absolute transaction deadline fires on a trickle peer ----
93
+
94
+ async function testSmtpTransactionDeadlineBounded() {
95
+ var trickleTimer = null;
96
+ // A server that greets, then for each command emits a SINGLE
97
+ // continuation byte-pair periodically — never a final response. With a
98
+ // generous idle timeout but a tiny maxTransactionMs the send must still
99
+ // fail on the wall-clock deadline.
100
+ var server = net.createServer(function (sock) {
101
+ // Client destroys at the transaction deadline; swallow the ECONNRESET.
102
+ sock.on("error", function () { if (trickleTimer) { clearInterval(trickleTimer); trickleTimer = null; } });
103
+ sock.write("220 slow.example ESMTP\r\n");
104
+ sock.on("data", function () {
105
+ // Keep the socket "alive" with periodic continuation lines so the
106
+ // idle timer never trips, but never complete the EHLO response.
107
+ if (trickleTimer) return;
108
+ trickleTimer = setInterval(function () {
109
+ if (!sock.writable) { clearInterval(trickleTimer); return; }
110
+ try { sock.write("250-keepalive\r\n"); } catch (_e) { clearInterval(trickleTimer); }
111
+ }, 20);
112
+ });
113
+ });
114
+ var port = await listen(server);
115
+
116
+ var transport = b.mail.transports.smtp({
117
+ host: "127.0.0.1",
118
+ port: port,
119
+ timeoutMs: 60000, // idle timer never trips (trickle keeps it warm)
120
+ maxTransactionMs: 300, // absolute deadline — must fire
121
+ });
122
+
123
+ var started = Date.now();
124
+ var err = null;
125
+ try {
126
+ await transport.send({
127
+ from: "ops@example.com",
128
+ to: ["alice@dest.example"],
129
+ subject: "hi",
130
+ text: "body",
131
+ });
132
+ } catch (e) { err = e; }
133
+ var elapsed = Date.now() - started;
134
+
135
+ if (trickleTimer) clearInterval(trickleTimer);
136
+ await closeServer(server);
137
+
138
+ check("smtp: trickle peer rejects (does not hang)", err !== null);
139
+ check("smtp: rejects with transaction-timeout",
140
+ err && typeof err.message === "string" &&
141
+ err.message.indexOf("transaction-timeout") !== -1);
142
+ check("smtp: deadline fired near maxTransactionMs (well under idle)", elapsed < 5000);
143
+ }
144
+
145
+ // ---- (3) maxTransactionMs is validated at config time ----
146
+
147
+ function testMaxTransactionMsValidated() {
148
+ var threw = null;
149
+ try {
150
+ b.mail.transports.smtp({ host: "127.0.0.1", maxTransactionMs: -5 });
151
+ } catch (e) { threw = e; }
152
+ check("smtp: negative maxTransactionMs throws MailError",
153
+ threw && threw.isMailError === true && threw.code === "mail/smtp-misconfigured");
154
+ }
155
+
156
+ // ---- (4) Proxy CONNECT: framing buffer bounded against a never-CRLFCRLF proxy ----
157
+
158
+ async function testProxyConnectHeadersTooLargeBounded() {
159
+ var streamTimer = null;
160
+ // A fake proxy that accepts the CONNECT, then streams bytes that never
161
+ // contain the CRLFCRLF header terminator.
162
+ var server = net.createServer(function (sock) {
163
+ // Client tears down once the cap trips; swallow the resulting ECONNRESET.
164
+ sock.on("error", function () { if (streamTimer) { clearInterval(streamTimer); streamTimer = null; } });
165
+ sock.on("data", function () {
166
+ if (streamTimer) return;
167
+ var blast = Buffer.alloc(16 * 1024, 0x42); // 16 KiB of 'B', no CRLFCRLF
168
+ streamTimer = setInterval(function () {
169
+ if (!sock.writable) { clearInterval(streamTimer); return; }
170
+ try { sock.write(blast); } catch (_e) { clearInterval(streamTimer); }
171
+ }, 1);
172
+ });
173
+ });
174
+ var port = await listen(server);
175
+
176
+ b.network.proxy.set({ http: "http://127.0.0.1:" + port });
177
+ var agent = b.network.proxy.agentFor("http://target.example/");
178
+
179
+ var err = null;
180
+ await new Promise(function (resolve) {
181
+ agent.createConnection({ host: "target.example", port: 80 }, function (e, sock) {
182
+ err = e;
183
+ if (sock) { try { sock.destroy(); } catch (_e) { /* ignore */ } }
184
+ resolve();
185
+ });
186
+ });
187
+
188
+ if (streamTimer) clearInterval(streamTimer);
189
+ b.network.proxy._resetForTest();
190
+ await closeServer(server);
191
+
192
+ check("proxy: never-CRLFCRLF reply rejects (does not hang/OOM)", err !== null);
193
+ check("proxy: rejects with connect-headers-too-large",
194
+ err && err.code === "proxy/connect-headers-too-large");
195
+ }
196
+
197
+ // ---- (5) Proxy CONNECT: ABSOLUTE wall-clock deadline (Codex P2, #362) ----
198
+ // A proxy that trickles a byte just inside every idle window but never sends
199
+ // CRLFCRLF would reset an idle socket.setTimeout forever and never trip the
200
+ // 64 KiB cap. The connect must still fail via an ABSOLUTE deadline. RED when
201
+ // the bound is socket.setTimeout (idle): the connect hangs past the deadline.
202
+ async function testProxyConnectAbsoluteDeadlineBounded() {
203
+ var trickle = null;
204
+ var server = net.createServer(function (sock) {
205
+ // The client tears the socket down at the deadline; writing to it after
206
+ // emits ECONNRESET on the server side — expected, swallow it so the test
207
+ // process doesn't crash on an unhandled 'error'.
208
+ sock.on("error", function () { if (trickle) { clearInterval(trickle); trickle = null; } });
209
+ sock.on("data", function () {
210
+ if (trickle) return;
211
+ // One byte every 50ms — keeps an idle timer warm, stays far under the
212
+ // 64 KiB header cap, never sends the CRLFCRLF terminator.
213
+ trickle = setInterval(function () {
214
+ if (!sock.writable) { clearInterval(trickle); return; }
215
+ try { sock.write("x"); } catch (_e) { clearInterval(trickle); }
216
+ }, 50);
217
+ });
218
+ });
219
+ var port = await listen(server);
220
+
221
+ b.network.proxy._setConnectTimeoutForTest(300); // small absolute deadline
222
+ b.network.proxy.set({ http: "http://127.0.0.1:" + port });
223
+ var agent = b.network.proxy.agentFor("http://target.example/");
224
+
225
+ var started = Date.now();
226
+ var err = null;
227
+ await new Promise(function (resolve) {
228
+ agent.createConnection({ host: "target.example", port: 80 }, function (e, sock) {
229
+ err = e;
230
+ if (sock) { try { sock.destroy(); } catch (_e) { /* ignore */ } }
231
+ resolve();
232
+ });
233
+ });
234
+ var elapsed = Date.now() - started;
235
+
236
+ if (trickle) clearInterval(trickle);
237
+ b.network.proxy._resetForTest();
238
+ await closeServer(server);
239
+
240
+ check("proxy: trickling CONNECT reply still fails (absolute deadline)", err !== null);
241
+ check("proxy: rejects with connect-timeout", err && err.code === "proxy/connect-timeout");
242
+ check("proxy: bounded by the absolute deadline (~timeout, not held open by trickle)",
243
+ elapsed < 3000);
244
+ }
245
+
246
+ // ---- Run ----
247
+
248
+ async function run() {
249
+ await testSmtpResponseTooLargeBounded();
250
+ await testSmtpTransactionDeadlineBounded();
251
+ testMaxTransactionMsValidated();
252
+ await testProxyConnectHeadersTooLargeBounded();
253
+ await testProxyConnectAbsoluteDeadlineBounded();
254
+ }
255
+
256
+ module.exports = { run: run };
257
+
258
+ if (require.main === module) {
259
+ run().then(
260
+ function () { console.log("[mail-proxy-framing-bounds] OK — " + helpers.getChecks() + " checks passed"); },
261
+ function (e) { console.error("FAIL:", (e && e.stack) || e); process.exit(1); }
262
+ );
263
+ }
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ /**
3
+ * network-dns: every lookup has an effective wall-clock deadline out of
4
+ * the box, and a stalled upstream tears the socket down.
5
+ *
6
+ * Regression guard for the CWE-400 class: `lookupTimeoutMs` defaulted to
7
+ * 0 (no deadline) and `_withTimeout` was a no-op at 0, so a
8
+ * header-then-stall / accept-then-never-reply upstream hung the request
9
+ * forever. The raw transport sockets also had no req.setTimeout /
10
+ * socket.setTimeout, so even a rejected promise leaked the fd.
11
+ *
12
+ * Drives the real `b.network.dns.resolve(host, "SVCB", {transport:
13
+ * "system"})` consumer path against a TCP black-hole responder — the
14
+ * same raw-query code path (`_systemRawQuery`) an operator runs — and
15
+ * confirms a stalled upstream is torn down within the deadline rather
16
+ * than hanging forever.
17
+ */
18
+
19
+ var net = require("node:net");
20
+
21
+ var helpers = require("../helpers");
22
+ var check = helpers.check;
23
+ var b = helpers.b;
24
+
25
+ var C = require("../../lib/constants");
26
+
27
+ var dnsModule = b.network.dns;
28
+
29
+ // A TCP server that accepts the connection but never sends a reply (and
30
+ // never closes) — models a stalled upstream resolver. Returns the live
31
+ // sockets it accepted so the test can assert teardown.
32
+ function _startBlackHoleResponder() {
33
+ return new Promise(function (resolve) {
34
+ var accepted = [];
35
+ var srv = net.createServer(function (sock) {
36
+ accepted.push(sock);
37
+ // Swallow the query; deliberately never reply, never end.
38
+ sock.on("data", function () { /* black hole */ });
39
+ sock.on("error", function () { /* fixture best-effort */ });
40
+ });
41
+ srv.unref();
42
+ srv.listen(0, "127.0.0.1", function () {
43
+ resolve({ srv: srv, port: srv.address().port, accepted: accepted });
44
+ });
45
+ });
46
+ }
47
+
48
+ async function _throwsAsync(fn, expectedCodeSubstr) {
49
+ try { await fn(); return false; }
50
+ catch (e) {
51
+ if (!expectedCodeSubstr) return true;
52
+ var hay = (e.code || "") + " " + (e.message || "");
53
+ return hay.indexOf(expectedCodeSubstr) !== -1;
54
+ }
55
+ }
56
+
57
+ async function run() {
58
+ dnsModule._resetForTest();
59
+
60
+ // ----------------------------------------------------------------
61
+ // The default deadline is applied (not 0 = no-timeout).
62
+ // ----------------------------------------------------------------
63
+ check("default lookupTimeoutMs is a non-zero wall-clock deadline (10s)",
64
+ dnsModule._stateForTest().lookupTimeoutMs === C.TIME.seconds(10));
65
+
66
+ // ----------------------------------------------------------------
67
+ // A stalled upstream tears the socket down within the deadline AND
68
+ // rejects the consumer call — without any operator setLookupTimeoutMs.
69
+ // We shorten the deadline to keep the test fast; the *mechanism* under
70
+ // test (setTimeout teardown + non-zero default _withTimeout) is the
71
+ // same one the 10s default arms. On the buggy tree this hangs forever.
72
+ // ----------------------------------------------------------------
73
+ var hole = await _startBlackHoleResponder();
74
+ dnsModule.useSystemResolver();
75
+ dnsModule.setServers(["127.0.0.1:" + hole.port]);
76
+ dnsModule.setLookupTimeoutMs(500);
77
+
78
+ var rejected = await _throwsAsync(function () {
79
+ return dnsModule.resolve("stall.example.com", "SVCB", { transport: "system" });
80
+ }, "dns/lookup-timeout");
81
+ check("resolve(...,'SVCB',system) against a black-hole upstream rejects with dns/lookup-timeout (does not hang)",
82
+ rejected);
83
+
84
+ // The accepted socket(s) must be destroyed — the deadline teardown,
85
+ // not just a promise-level reject leaving the fd alive.
86
+ await helpers.waitUntil(function () {
87
+ return hole.accepted.length > 0 &&
88
+ hole.accepted.every(function (s) { return s.destroyed; });
89
+ }, {
90
+ timeoutMs: 5000,
91
+ label: "network-dns timeout: stalled upstream socket torn down",
92
+ });
93
+ check("stalled upstream socket is torn down (fd not leaked)",
94
+ hole.accepted.length > 0 && hole.accepted.every(function (s) { return s.destroyed; }));
95
+
96
+ hole.srv.close();
97
+ dnsModule._resetForTest();
98
+
99
+ // ----------------------------------------------------------------
100
+ // Operator opt-out (0) still disables the deadline — the override path
101
+ // survives the default change.
102
+ // ----------------------------------------------------------------
103
+ dnsModule.setLookupTimeoutMs(0);
104
+ check("operator can disable the deadline (lookupTimeoutMs = 0)",
105
+ dnsModule._stateForTest().lookupTimeoutMs === 0);
106
+ dnsModule._resetForTest();
107
+ }
108
+
109
+ module.exports = { run: run };
110
+
111
+ if (require.main === module) {
112
+ run().then(
113
+ function () { console.log("[network-dns-lookup-timeout-default] OK — " + helpers.getChecks() + " checks passed"); },
114
+ function (e) { console.error(e && e.stack || e); process.exit(1); }
115
+ );
116
+ }
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ /**
3
+ * b.network.dns.resolver — per-query upstream timeout.
4
+ *
5
+ * A non-responsive / slow / stalling DoH endpoint (or a transport whose
6
+ * lookup never resolves — the wire-level analogue of a server that sends
7
+ * 200 headers then never ends the body) must not hold the await pending
8
+ * forever. Every query() and every followCnames hop is bounded by the
9
+ * create({ timeoutMs }) wall-clock deadline. RED on the pre-fix tree:
10
+ * queryA() never settles and the test hangs until the harness kills it.
11
+ */
12
+
13
+ var helpers = require("../helpers");
14
+ var b = helpers.b;
15
+ var check = helpers.check;
16
+
17
+ // A transport whose lookup never resolves — models a DoH endpoint that
18
+ // accepts the connection, sends headers, then stalls the body forever.
19
+ function _hangingTransport() {
20
+ return {
21
+ lookupCalls: 0,
22
+ lookup: function () {
23
+ this.lookupCalls += 1;
24
+ return new Promise(function () { /* never settles */ });
25
+ },
26
+ };
27
+ }
28
+
29
+ // Bound a promise so a regressed (hanging) tree fails as a timeout instead of
30
+ // stalling the whole suite. Polls via helpers.waitUntil (not a fixed
31
+ // setTimeout sleep) for the promise to settle, then rethrows its rejection —
32
+ // a genuine hang surfaces as a waitUntil timeout rather than hanging forever.
33
+ function _within(promise, ms, label) {
34
+ var state = { done: false, value: undefined, error: undefined };
35
+ promise.then(
36
+ function (v) { state.done = true; state.value = v; },
37
+ function (e) { state.done = true; state.error = e; }
38
+ );
39
+ return helpers.waitUntil(function () { return state.done; }, { timeoutMs: ms, label: label })
40
+ .then(function () { if (state.error) throw state.error; return state.value; });
41
+ }
42
+
43
+ async function testQueryTimesOutOnHangingTransport() {
44
+ var transport = _hangingTransport();
45
+ var r = b.network.dns.resolver.create({ transport: transport, timeoutMs: 80 });
46
+
47
+ var threw = null;
48
+ try {
49
+ await _within(r.queryA("example.com"), 4000,
50
+ "queryA against a hanging transport");
51
+ } catch (e) {
52
+ threw = e;
53
+ }
54
+ check("queryA rejects on a hanging transport (does not hang)", threw !== null);
55
+ var msg = threw ? String(threw.message) : "";
56
+ check("rejection is the resolver deadline, not the test backstop",
57
+ /timed out/i.test(msg) && !/TEST-DEADLINE/.test(msg));
58
+ check("transport.lookup was actually invoked", transport.lookupCalls === 1);
59
+ }
60
+
61
+ async function testFollowCnamesHopTimesOut() {
62
+ // followCnames loops query() per hop; the first hop's transport stall
63
+ // must bound the whole walk, not hang it.
64
+ var transport = _hangingTransport();
65
+ var r = b.network.dns.resolver.create({ transport: transport, timeoutMs: 80 });
66
+
67
+ var threw = null;
68
+ try {
69
+ await _within(r.followCnames("alias.example.com", "A"), 4000,
70
+ "followCnames against a hanging transport");
71
+ } catch (e) {
72
+ threw = e;
73
+ }
74
+ check("followCnames rejects when a hop's transport stalls", threw !== null);
75
+ var msg2 = threw ? String(threw.message) : "";
76
+ check("followCnames rejection is the resolver deadline",
77
+ /timed out/i.test(msg2) && !/TEST-DEADLINE/.test(msg2));
78
+ }
79
+
80
+ async function testBadTimeoutRejectedAtCreate() {
81
+ var cases = [0, -1, NaN, Infinity];
82
+ for (var i = 0; i < cases.length; i += 1) {
83
+ var threw = null;
84
+ try {
85
+ b.network.dns.resolver.create({ timeoutMs: cases[i] });
86
+ } catch (e) {
87
+ threw = e;
88
+ }
89
+ check("create rejects timeoutMs=" + cases[i],
90
+ threw !== null && threw.code === "resolver/bad-input");
91
+ }
92
+ }
93
+
94
+ async function testDefaultTimeoutAccepted() {
95
+ // No timeoutMs supplied — create() must still build (uses the default)
96
+ // and a fast fake transport resolves normally.
97
+ var r = b.network.dns.resolver.create({
98
+ transport: { lookup: function () { return Promise.reject(new Error("boom")); } },
99
+ });
100
+ var threw = null;
101
+ try {
102
+ await r.queryA("example.com");
103
+ } catch (e) {
104
+ threw = e;
105
+ }
106
+ // A transport error (not a timeout) surfaces as upstream-failed — proves
107
+ // the default-timeout path doesn't swallow real fast failures.
108
+ check("default timeout path surfaces fast transport errors",
109
+ threw !== null && threw.code === "resolver/upstream-failed");
110
+ }
111
+
112
+ async function run() {
113
+ await testQueryTimesOutOnHangingTransport();
114
+ await testFollowCnamesHopTimesOut();
115
+ await testBadTimeoutRejectedAtCreate();
116
+ await testDefaultTimeoutAccepted();
117
+ }
118
+
119
+ module.exports = { run: run };
120
+
121
+ if (require.main === module) run().then(function () {
122
+ process.stdout.write("PASS: network-dns-resolver-timeout\n");
123
+ }).catch(function (e) {
124
+ process.stderr.write("FAIL: " + (e && e.stack || e) + "\n");
125
+ process.exit(1);
126
+ });
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ /**
3
+ * b.* NTS-KE handshake byte-cap.
4
+ *
5
+ * The NTS-KE (RFC 8915) key-establishment handshake reader accumulates
6
+ * received bytes until a REC_END record terminates the exchange. A
7
+ * wall-clock timer bounds the handshake by time, but NOT by memory: a
8
+ * server that streams non-END records fast enough OOMs the process
9
+ * before the timer fires. The reader must cap the accumulated buffer at
10
+ * a sane ceiling (64 KiB) and fail closed with an nts/* typed error.
11
+ *
12
+ * This drives the real consumer path — network-nts.performKeHandshake
13
+ * against a live loopback TLS server negotiating ALPN "ntske/1" that
14
+ * streams > 64 KiB of non-END records.
15
+ */
16
+
17
+ var helpers = require("../helpers");
18
+ var check = helpers.check;
19
+ var nts = require("../../lib/network-nts");
20
+ var nodeTls = require("node:tls");
21
+
22
+ var REC_NEW_COOKIE = 5;
23
+
24
+ // Encode an NTS-KE record: u16 type (top bit = critical) || u16 length
25
+ // || body. We emit non-critical NEW_COOKIE records that never include a
26
+ // REC_END (type 0), so a conformant reader keeps accumulating.
27
+ function _encodeRecord(type, body) {
28
+ var hdr = Buffer.alloc(4);
29
+ hdr.writeUInt16BE(type & 0x7fff, 0);
30
+ hdr.writeUInt16BE(body.length, 2);
31
+ return Buffer.concat([hdr, body]);
32
+ }
33
+
34
+ function testHandshakeByteCapFailsClosed() {
35
+ return new Promise(function (resolve, reject) {
36
+ (async function () {
37
+ var server = null;
38
+ try {
39
+ var ca = await helpers.b.mtlsEngine.generateCa({ name: "nts-test-ca" });
40
+ var leaf = await helpers.b.mtlsEngine.signClientCert({
41
+ cn: "localhost",
42
+ caCertPem: ca.caCertPem,
43
+ caKeyPem: ca.caKeyPem,
44
+ usage: "server",
45
+ sans: ["DNS:localhost", "IP:127.0.0.1"],
46
+ validityDays: 1,
47
+ });
48
+
49
+ // One ~1 KiB non-END record; streamed enough times to exceed the
50
+ // 64 KiB ceiling without ever sending REC_END.
51
+ var oneRecord = _encodeRecord(REC_NEW_COOKIE, Buffer.alloc(1020, 0x41));
52
+ var TOTAL_BYTES = 128 * 1024; // 2x the 64 KiB cap
53
+
54
+ server = nodeTls.createServer({
55
+ key: leaf.key,
56
+ cert: leaf.cert,
57
+ minVersion: "TLSv1.3",
58
+ maxVersion: "TLSv1.3",
59
+ ALPNProtocols: ["ntske/1"],
60
+ }, function (sock) {
61
+ sock.on("error", function () { /* client tears down mid-stream */ });
62
+ var sent = 0;
63
+ function pump() {
64
+ while (sent < TOTAL_BYTES) {
65
+ sent += oneRecord.length;
66
+ if (!sock.write(oneRecord)) {
67
+ sock.once("drain", pump);
68
+ return;
69
+ }
70
+ }
71
+ }
72
+ pump();
73
+ });
74
+
75
+ server.listen(0, "127.0.0.1", function () {
76
+ var port = server.address().port;
77
+ var startedAt = Date.now();
78
+ nts.performKeHandshake({
79
+ host: "127.0.0.1",
80
+ port: port,
81
+ servername: "localhost",
82
+ ca: ca.caCertPem,
83
+ // Generous timeout: a passing run must reject on the byte cap
84
+ // FAST (well under this), proving the cap — not the timer —
85
+ // bounded the read.
86
+ timeoutMs: 30000,
87
+ }).then(function () {
88
+ server.close();
89
+ reject(new Error("handshake resolved despite > 64 KiB of non-END records"));
90
+ }).catch(function (e) {
91
+ var elapsed = Date.now() - startedAt;
92
+ try { server.close(); } catch (_e) { /* best-effort */ }
93
+ try {
94
+ check("handshake rejects on oversized stream",
95
+ e && e.code === "nts/ke-too-large");
96
+ check("rejection is NtsError typed",
97
+ e instanceof nts.NtsError);
98
+ check("rejection is bounded, not the wall-clock timeout",
99
+ e.code !== "nts/ke-timeout");
100
+ check("bounded fast (well under the 30s timer)",
101
+ elapsed < 15000);
102
+ resolve();
103
+ } catch (assertErr) {
104
+ reject(assertErr);
105
+ }
106
+ });
107
+ });
108
+
109
+ server.on("error", function (e) { reject(e); });
110
+ } catch (e) {
111
+ if (server) { try { server.close(); } catch (_e) { /* best-effort */ } }
112
+ reject(e);
113
+ }
114
+ })();
115
+ });
116
+ }
117
+
118
+ async function run() {
119
+ await testHandshakeByteCapFailsClosed();
120
+ }
121
+
122
+ module.exports = { run: run };
123
+
124
+ if (require.main === module) {
125
+ run().then(function () { console.log("OK"); })
126
+ .catch(function (e) { console.error(e.stack || e); process.exit(1); });
127
+ }