@sproutboat/runtime 0.42.0 → 0.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @sproutboat/runtime
2
2
 
3
+ ## 0.42.1
4
+
5
+ ### Patch Changes
6
+
7
+ - dbba349: fetch() and connect() now also refuse reserved space that carries no public service: AS112 and the old IPv4 anycast blocks (`192.31.196.0/24`, `192.52.193.0/24`, `192.88.99.0/24`, `192.175.48.0/24`), SIIT (`::ffff:0:0:0/96`), IETF protocol assignments including Teredo (`2001::/23`), `3fff::/20` and SRv6 (`5f00::/16`). Applies to the broker, the egress service and the standalone client alike; `SB_EGRESS_ALLOW` still overrides.
8
+ - cf519dd: Outbound TCP sockets now set `TCP_NODELAY`. Without it, Nagle's algorithm and the peer's delayed ACK stalled every fresh HTTPS `fetch()` by about 44 ms on Linux (each small TLS handshake flight waited for an ACK). This covers `fetch()`, outbound WebSockets and `connect()` from `cloudflare:sockets` on the embedded transport (standalone binaries, including sockets handed over by `SB_EGRESS_FD`), and the egress tunnel's connections to origins in `wire`. No compatibility date: only latency changes, no result, header or error does.
9
+
3
10
  ## 0.42.0
4
11
 
5
12
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/runtime",
3
- "version": "0.42.0",
3
+ "version": "0.42.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -2,13 +2,7 @@ import { expect, test } from "bun:test";
2
2
  import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import {
6
- blockedRange,
7
- egressAllowList,
8
- egressRefusal,
9
- parseAddress,
10
- spliceEgressTable,
11
- } from "./egress";
5
+ import { blockedRange, egressAllowList, egressRefusal, parseAddress, spliceEgressTable } from "./egress";
12
6
  import { validateHttpSyncSource } from "./source";
13
7
  import { wrapNativeFetchHandler } from "./wrap";
14
8
 
@@ -41,6 +35,17 @@ const BLOCKED = [
41
35
  "fd12::1",
42
36
  "fe80::1",
43
37
  "ff02::1",
38
+ "192.31.196.1",
39
+ "192.52.193.1",
40
+ "192.88.99.1",
41
+ "192.175.48.1",
42
+ "::ffff:0:7f00:1",
43
+ "2001::1",
44
+ "2001:0:4136:e378:8000:63bf:3fff:fdd2",
45
+ "2001:1ff::1",
46
+ "3fff::1",
47
+ "3fff:fff:ffff::1",
48
+ "5f00::1",
44
49
  ];
45
50
  const PUBLIC = [
46
51
  "1.1.1.1",
@@ -54,23 +59,25 @@ const PUBLIC = [
54
59
  "2002:808:808::1",
55
60
  "2001:4860:4860::8888",
56
61
  "2606:4700::1111",
62
+ "192.31.195.255",
63
+ "192.88.100.1",
64
+ "2001:200::1",
65
+ "4000::1",
66
+ "3ffe::1",
67
+ "5f01::1",
57
68
  ];
58
69
  const ALLOW = "8.8.4.4, 10.9.9.9";
59
70
 
60
71
  test("private and reserved addresses are blocked, public ones are not", () => {
61
- for (const address of BLOCKED)
62
- expect([address, blockedRange(address)]).not.toEqual([address, null]);
63
- for (const address of PUBLIC)
64
- expect([address, blockedRange(address)]).toEqual([address, null]);
72
+ for (const address of BLOCKED) expect([address, blockedRange(address)]).not.toEqual([address, null]);
73
+ for (const address of PUBLIC) expect([address, blockedRange(address)]).toEqual([address, null]);
65
74
  expect(blockedRange("::ffff:127.0.0.1")).toBe("127.0.0.0/8");
66
75
  expect(blockedRange("64:ff9b::a00:1")).toBe("10.0.0.0/8");
67
76
  });
68
77
 
69
78
  test("the operator allow list lets an exact address or everything through", () => {
70
79
  expect(egressRefusal("db", "10.9.9.9", egressAllowList(ALLOW))).toBeNull();
71
- expect(egressRefusal("db", "10.9.9.8", egressAllowList(ALLOW))).toContain(
72
- "SB_EGRESS_ALLOW=10.9.9.8",
73
- );
80
+ expect(egressRefusal("db", "10.9.9.8", egressAllowList(ALLOW))).toContain("SB_EGRESS_ALLOW=10.9.9.8");
74
81
  expect(egressRefusal("db", "127.0.0.1", ["*"])).toBeNull();
75
82
  expect(egressRefusal("x", "169.254.169.254", [])).toBe(
76
83
  "fetch() refused: x resolves to 169.254.169.254, a private or reserved address (169.254.0.0/16). An operator can allow it with SB_EGRESS_ALLOW=169.254.169.254",
@@ -78,15 +85,7 @@ test("the operator allow list lets an exact address or everything through", () =
78
85
  });
79
86
 
80
87
  test("parseAddress rejects what is not an address", () => {
81
- for (const bad of [
82
- "",
83
- "1.2.3",
84
- "256.1.1.1",
85
- "example.com",
86
- "1::2::3",
87
- "1:2:3:4:5:6:7:8:9",
88
- "12345::",
89
- ])
88
+ for (const bad of ["", "1.2.3", "256.1.1.1", "example.com", "1::2::3", "1:2:3:4:5:6:7:8:9", "12345::"])
90
89
  expect([bad, parseAddress(bad)]).toEqual([bad, null]);
91
90
  });
92
91
 
@@ -99,10 +98,7 @@ test("the C table in transport-embedded.js is generated from this one (bun run g
99
98
 
100
99
  test("the C check gives the same answer and message as the TS one", () => {
101
100
  const source = readFileSync(transportFile, "utf8");
102
- const block = source.slice(
103
- source.indexOf("// EGRESS:BEGIN"),
104
- source.indexOf("// EGRESS:END"),
105
- );
101
+ const block = source.slice(source.indexOf("// EGRESS:BEGIN"), source.indexOf("// EGRESS:END"));
106
102
  const dir = mkdtempSync(join(tmpdir(), "sb-egress-"));
107
103
  try {
108
104
  writeFileSync(
@@ -130,23 +126,14 @@ int main(int argc, char** argv) {
130
126
  }
131
127
  `,
132
128
  );
133
- const cc = Bun.spawnSync([
134
- "cc",
135
- "-Wall",
136
- "-Werror",
137
- "-o",
138
- join(dir, "check"),
139
- join(dir, "check.c"),
140
- ]);
129
+ const cc = Bun.spawnSync(["cc", "-Wall", "-Werror", "-o", join(dir, "check"), join(dir, "check.c")]);
141
130
  expect(cc.stderr.toString()).toBe("");
142
131
  const addresses = [...BLOCKED, ...PUBLIC, "10.9.9.9"];
143
132
  const run = Bun.spawnSync([join(dir, "check"), ...addresses], {
144
133
  env: { ...process.env, SB_EGRESS_ALLOW: ALLOW },
145
134
  });
146
135
  const fromC = run.stdout.toString().trimEnd().split("\n");
147
- const fromTs = addresses.map(
148
- (a) => egressRefusal("h", a, egressAllowList(ALLOW)) ?? "ok",
149
- );
136
+ const fromTs = addresses.map((a) => egressRefusal("h", a, egressAllowList(ALLOW)) ?? "ok");
150
137
  expect(fromC).toEqual(fromTs);
151
138
  } finally {
152
139
  rmSync(dir, { recursive: true, force: true });
@@ -156,21 +143,13 @@ int main(int argc, char** argv) {
156
143
  test("#174: fetch() needs no config, so every sprout gets it", () => {
157
144
  const handler = `export default { async fetch() { return await fetch("https://api.example.com/"); } };`;
158
145
  expect(validateHttpSyncSource(handler)).toEqual({ ok: true });
159
- const prelude = readFileSync(
160
- new URL("./native-fetch-prelude.js", import.meta.url),
161
- "utf8",
162
- );
163
- expect(wrapNativeFetchHandler(handler, prelude)).toContain(
164
- "__sbInstallFetch();",
165
- );
146
+ const prelude = readFileSync(new URL("./native-fetch-prelude.js", import.meta.url), "utf8");
147
+ expect(wrapNativeFetchHandler(handler, prelude)).toContain("__sbInstallFetch();");
166
148
  });
167
149
 
168
150
  test("SB_EGRESS_FD: the host connects, the sprout gets the socket", () => {
169
151
  const source = readFileSync(transportFile, "utf8");
170
- const block = source.slice(
171
- source.indexOf("// EGRESS-FD:BEGIN"),
172
- source.indexOf("// EGRESS-FD:END"),
173
- );
152
+ const block = source.slice(source.indexOf("// EGRESS-FD:BEGIN"), source.indexOf("// EGRESS-FD:END"));
174
153
  const dir = mkdtempSync(join(tmpdir(), "sb-egress-fd-"));
175
154
  try {
176
155
  // A fake host on one end of a socketpair: "up" gets a socket whose peer says
@@ -184,6 +163,8 @@ test("SB_EGRESS_FD: the host connects, the sprout gets the socket", () => {
184
163
  #include <string.h>
185
164
  #include <unistd.h>
186
165
  #include <sys/socket.h>
166
+ #include <netinet/in.h>
167
+ #include <netinet/tcp.h>
187
168
  #include <sys/time.h>
188
169
  #include <sys/wait.h>
189
170
  ${block}
@@ -261,18 +242,76 @@ int main(void) {
261
242
  }
262
243
  });
263
244
 
245
+ test("sb_tcp_connect sets TCP_NODELAY on the socket it returns", () => {
246
+ // Nagle plus the peer's delayed ACK stalled every fresh HTTPS fetch about 44 ms on Linux (a TLS
247
+ // handshake writes small flights). Timing is flaky on CI and loopback on macOS does not show
248
+ // it, so ask the kernel: connect to a local listener and read the option back.
249
+ const source = readFileSync(transportFile, "utf8");
250
+ const between = (from: string, to: string) => source.slice(source.indexOf(from), source.indexOf(to));
251
+ const dir = mkdtempSync(join(tmpdir(), "sb-nodelay-"));
252
+ try {
253
+ writeFileSync(
254
+ join(dir, "check.c"),
255
+ `#include <errno.h>
256
+ #include <pthread.h>
257
+ #include <netdb.h>
258
+ #include <stdio.h>
259
+ #include <stdlib.h>
260
+ #include <string.h>
261
+ #include <unistd.h>
262
+ #include <sys/socket.h>
263
+ #include <sys/time.h>
264
+ #include <sys/uio.h>
265
+ #include <netinet/in.h>
266
+ #include <netinet/tcp.h>
267
+ #include <arpa/inet.h>
268
+ ${between("// EGRESS:BEGIN", "// EGRESS:END")}
269
+ ${between("// EGRESS-FD:BEGIN", "// EGRESS-FD:END")}
270
+ ${between("// TCP-CONNECT:BEGIN", "// TCP-CONNECT:END")}
271
+ int main(void) {
272
+ int listener = socket(AF_INET, SOCK_STREAM, 0);
273
+ struct sockaddr_in addr;
274
+ memset(&addr, 0, sizeof(addr));
275
+ addr.sin_family = AF_INET;
276
+ addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
277
+ if (bind(listener, (struct sockaddr*)&addr, sizeof(addr)) || listen(listener, 1)) return 2;
278
+ socklen_t len = sizeof(addr);
279
+ getsockname(listener, (struct sockaddr*)&addr, &len);
280
+ char refused[512] = "";
281
+ int fd = sb_tcp_connect("127.0.0.1", ntohs(addr.sin_port), refused, sizeof(refused));
282
+ int value = -1;
283
+ socklen_t vlen = sizeof(value);
284
+ if (fd >= 0) getsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &value, &vlen);
285
+ printf("fd %s nodelay %s\\n", fd >= 0 ? "yes" : "no", value > 0 ? "on" : "off");
286
+ return 0;
287
+ }
288
+ `,
289
+ );
290
+ const cc = Bun.spawnSync([
291
+ "cc",
292
+ "-Wall",
293
+ "-Werror",
294
+ "-Wno-unused-function",
295
+ "-Wno-unused-result",
296
+ "-o",
297
+ join(dir, "check"),
298
+ join(dir, "check.c"),
299
+ ]);
300
+ expect(cc.stderr.toString()).toBe("");
301
+ const run = Bun.spawnSync([join(dir, "check")], {
302
+ env: { ...process.env, SB_EGRESS_ALLOW: "127.0.0.1", SB_EGRESS_FD: "" },
303
+ });
304
+ expect(run.stdout.toString().trim()).toBe("fd yes nodelay on");
305
+ } finally {
306
+ rmSync(dir, { recursive: true, force: true });
307
+ }
308
+ });
309
+
264
310
  test("the Host header carries a non-default port", () => {
265
311
  const source = readFileSync(transportFile, "utf8");
266
- const between = (from: string, to: string) =>
267
- source.slice(source.indexOf(from), source.indexOf(to));
268
- const buffer = between(
269
- "typedef struct { char* p; size_t len; size_t cap; } sb_buf;",
270
- "static void sb_putjson(",
271
- );
272
- const builder = between(
273
- "// body_len, not strlen(body)",
274
- "// Turn a whole raw response",
275
- );
312
+ const between = (from: string, to: string) => source.slice(source.indexOf(from), source.indexOf(to));
313
+ const buffer = between("typedef struct { char* p; size_t len; size_t cap; } sb_buf;", "static void sb_putjson(");
314
+ const builder = between("// body_len, not strlen(body)", "// Turn a whole raw response");
276
315
  const dir = mkdtempSync(join(tmpdir(), "sb-host-"));
277
316
  try {
278
317
  writeFileSync(
@@ -318,10 +357,7 @@ int main(void) { show(80, 80); show(8080, 80); show(443, 443); show(8443, 443);
318
357
 
319
358
  test("async fetch: requests run on threads and come back through the host", () => {
320
359
  const source = readFileSync(transportFile, "utf8");
321
- const block = source.slice(
322
- source.indexOf("// ASYNC:BEGIN"),
323
- source.indexOf("// ASYNC:END"),
324
- );
360
+ const block = source.slice(source.indexOf("// ASYNC:BEGIN"), source.indexOf("// ASYNC:END"));
325
361
  const dir = mkdtempSync(join(tmpdir(), "sb-async-"));
326
362
  try {
327
363
  // Stand-ins: a client that takes 200 ms, and a host that records which timers fire.
@@ -385,13 +421,8 @@ int main(void) {
385
421
  .trimEnd()
386
422
  .split("\n");
387
423
  // 16 requests of 200 ms on 8 threads: two rounds, not sixteen.
388
- expect(lines.slice(0, 16).sort()).toEqual(
389
- Array.from({ length: 16 }, (_, i) => `${i + 1} h/${i + 1} body`).sort(),
390
- );
391
- expect(lines.slice(16)).toEqual([
392
- "elapsed overlapped",
393
- "outside a callback nothing",
394
- ]);
424
+ expect(lines.slice(0, 16).sort()).toEqual(Array.from({ length: 16 }, (_, i) => `${i + 1} h/${i + 1} body`).sort());
425
+ expect(lines.slice(16)).toEqual(["elapsed overlapped", "outside a callback nothing"]);
395
426
  } finally {
396
427
  rmSync(dir, { recursive: true, force: true });
397
428
  }
package/src/egress.ts CHANGED
@@ -41,6 +41,18 @@ export const BLOCKED_RANGES = [
41
41
  "fe80::/10",
42
42
  "fec0::/10",
43
43
  "ff00::/8",
44
+ // Reserved space that carries no public service: AS112 and the IETF's old anycast
45
+ // blocks, SIIT (`::ffff:0:0:0/96`, which embeds an IPv4 address the mapped rule
46
+ // above does not see), IETF protocol assignments (includes Teredo, `2001::/32`),
47
+ // the second documentation block and SRv6 SIDs.
48
+ "192.31.196.0/24",
49
+ "192.52.193.0/24",
50
+ "192.88.99.0/24",
51
+ "192.175.48.0/24",
52
+ "::ffff:0:0:0/96",
53
+ "2001::/23",
54
+ "3fff::/20",
55
+ "5f00::/16",
44
56
  ] as const;
45
57
 
46
58
  /** Parse an IPv4 or IPv6 literal to its bytes; `null` when it is neither. */
@@ -123,11 +135,7 @@ export function egressAllowList(raw: string | undefined): string[] {
123
135
 
124
136
  /** Why `fetch()` may not connect to `address`, or `null` when it may. The
125
137
  * message is identical in the C client; keep the two in step. */
126
- export function egressRefusal(
127
- host: string,
128
- address: string,
129
- allow: readonly string[],
130
- ): string | null {
138
+ export function egressRefusal(host: string, address: string, allow: readonly string[]): string | null {
131
139
  if (allow.includes("*") || allow.includes(address)) return null;
132
140
  const range = blockedRange(address);
133
141
  if (range === null) return null;
@@ -149,8 +157,5 @@ const TABLE_BEGIN = /( *\/\/ EGRESS-TABLE:BEGIN[^\n]*\n)[\s\S]*?( *\/\/ EGRESS-T
149
157
  /** `source` with the generated table between its EGRESS-TABLE markers replaced. */
150
158
  export function spliceEgressTable(source: string): string {
151
159
  if (!TABLE_BEGIN.test(source)) throw new Error("EGRESS-TABLE markers not found");
152
- return source.replace(
153
- TABLE_BEGIN,
154
- (_, begin: string, end: string) => `${begin}${egressRangesC()}\n${end}`,
155
- );
160
+ return source.replace(TABLE_BEGIN, (_, begin: string, end: string) => `${begin}${egressRangesC()}\n${end}`);
156
161
  }
@@ -20,6 +20,8 @@ Porffor.c`
20
20
  #include <stdint.h>
21
21
  #include <sys/stat.h>
22
22
  #include <sys/socket.h>
23
+ #include <netinet/in.h>
24
+ #include <netinet/tcp.h>
23
25
  #include <netdb.h>
24
26
  #include <arpa/inet.h>
25
27
  #include <string.h>
@@ -2594,6 +2596,14 @@ static const char* sb_egress_blocked(const unsigned char* a, int len) {
2594
2596
  { 16, 10, { 254, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, "fe80::/10" },
2595
2597
  { 16, 10, { 254, 192, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, "fec0::/10" },
2596
2598
  { 16, 8, { 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, "ff00::/8" },
2599
+ { 4, 24, { 192, 31, 196, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, "192.31.196.0/24" },
2600
+ { 4, 24, { 192, 52, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, "192.52.193.0/24" },
2601
+ { 4, 24, { 192, 88, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, "192.88.99.0/24" },
2602
+ { 4, 24, { 192, 175, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, "192.175.48.0/24" },
2603
+ { 16, 96, { 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0 }, "::ffff:0:0:0/96" },
2604
+ { 16, 23, { 32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, "2001::/23" },
2605
+ { 16, 20, { 63, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, "3fff::/20" },
2606
+ { 16, 16, { 95, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, "5f00::/16" },
2597
2607
  };
2598
2608
  // EGRESS-TABLE:END
2599
2609
  static const unsigned char mapped[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff };
@@ -2653,6 +2663,14 @@ static int sb_egress_refused(const char* host, const struct sockaddr* sa, char*
2653
2663
  // EGRESS:END
2654
2664
 
2655
2665
  // EGRESS-FD:BEGIN
2666
+ // TCP_NODELAY on an outbound socket. Without it Nagle holds a small write until the peer ACKs the
2667
+ // last one, and the peer delays that ACK: about 44 ms per TLS handshake flight on Linux. A unix
2668
+ // socket (an fd handed over by sproutd) refuses the option, which is fine: only TCP needs it.
2669
+ static void sb_tcp_nodelay(int fd) {
2670
+ int one = 1;
2671
+ setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
2672
+ }
2673
+
2656
2674
  // SB_EGRESS_FD=N: a sandboxed host (sproutd) opens outbound connections for the
2657
2675
  // sprout, which may not open sockets itself. Ask on unix socket N with
2658
2676
  // "HOST PORT\n"; the host resolves, applies its policy and answers "ok\n" with the
@@ -2699,6 +2717,7 @@ static int sb_egress_fd_ask(int via, const char* host, int port, char* refused,
2699
2717
  if (!strcmp(answer, "ok") && fd >= 0) {
2700
2718
  setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
2701
2719
  setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
2720
+ sb_tcp_nodelay(fd);
2702
2721
  return fd;
2703
2722
  }
2704
2723
  if (fd >= 0) close(fd);
@@ -2707,6 +2726,7 @@ static int sb_egress_fd_ask(int via, const char* host, int port, char* refused,
2707
2726
  }
2708
2727
  // EGRESS-FD:END
2709
2728
 
2729
+ // TCP-CONNECT:BEGIN
2710
2730
  // refused gets the first refusal; a caller reports it when nothing connected.
2711
2731
  static int sb_tcp_connect(const char* host, int port, char* refused, size_t refused_len) {
2712
2732
  const char* via = getenv("SB_EGRESS_FD");
@@ -2739,13 +2759,14 @@ static int sb_tcp_connect(const char* host, int port, char* refused, size_t refu
2739
2759
  struct timeval tv; tv.tv_sec = 30; tv.tv_usec = 0;
2740
2760
  setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
2741
2761
  setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
2742
- if (connect(fd, it->ai_addr, it->ai_addrlen) == 0) break;
2762
+ if (connect(fd, it->ai_addr, it->ai_addrlen) == 0) { sb_tcp_nodelay(fd); break; }
2743
2763
  close(fd);
2744
2764
  fd = -1;
2745
2765
  }
2746
2766
  freeaddrinfo(res);
2747
2767
  return fd;
2748
2768
  }
2769
+ // TCP-CONNECT:END
2749
2770
 
2750
2771
  // body_len, not strlen(body): a binary body can contain NUL bytes (#232).
2751
2772
  // Host carries the port unless it is the scheme's default (RFC 9110 7.2).
package/test/bearssl.ts CHANGED
@@ -1,16 +1,14 @@
1
- import { createHash } from "node:crypto";
2
1
  import { existsSync } from "node:fs";
3
2
  import { mkdir, readdir, rename, rm, writeFile } from "node:fs/promises";
4
3
  import { homedir } from "node:os";
5
- import { join, resolve } from "node:path";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { BEARSSL_SHA256, BEARSSL_URLS, BEARSSL_VENDORED_PATH, BEARSSL_VERSION } from "../../toolchain/src/deps";
6
+ import { fetchPinned } from "../../toolchain/src/fetch-pinned";
6
7
 
7
8
  // The prelude carries crypto.subtle's native half on every transport, so a test that compiles a
8
9
  // handler using crypto.subtle needs BearSSL on the include path and link line, as the CLI arranges
9
10
  // for a build. Same pinned source as the CLI's src/bearssl.ts; built once into the toolchain cache.
10
- const COMMIT = "7bea48e5e850ab4cafbe68d3765cdaba13a86d6f";
11
- const VERSION = COMMIT.slice(0, 7);
12
- const URL_ = `https://bearssl.org/gitweb/?p=BearSSL;a=snapshot;h=${COMMIT};sf=tgz`;
13
- const SHA256 = "ed397832b1613582737d9c1ce01c1af74ad6a9e9c52a341e24433987cda0f372";
11
+ const VERSION = BEARSSL_VERSION;
14
12
 
15
13
  const walk = async (dir: string): Promise<string[]> =>
16
14
  (
@@ -34,10 +32,14 @@ export async function bearsslEnv(): Promise<{ SB_EXTRA_CFLAGS: string; SB_EXTRA_
34
32
  const marker = join(objects, ".complete");
35
33
  if (!existsSync(marker)) {
36
34
  await mkdir(root, { recursive: true });
37
- const response = await fetch(URL_);
38
- if (!response.ok) throw new Error(`could not download ${URL_}: HTTP ${response.status}`);
39
- const bytes = Buffer.from(await response.arrayBuffer());
40
- if (createHash("sha256").update(bytes).digest("hex") !== SHA256) throw new Error("BearSSL checksum mismatch");
35
+ // Vendored tarball first (vendor/bearssl), then bearssl.org with bounded retry; both sha256-pinned: see
36
+ // toolchain/src/deps.ts. A downloaded copy is cached so a later run needs no network.
37
+ const bytes = await fetchPinned({
38
+ localPath: BEARSSL_VENDORED_PATH,
39
+ urls: BEARSSL_URLS,
40
+ sha256: BEARSSL_SHA256,
41
+ cachePath: join(dirname(root), "deps", `bearssl-src-${BEARSSL_SHA256}.tar.gz`),
42
+ });
41
43
  await writeFile(join(root, "bearssl.tar.gz"), bytes);
42
44
  await rm(tree, { recursive: true, force: true });
43
45
  const untar = Bun.spawnSync(["tar", "xzf", join(root, "bearssl.tar.gz"), "-C", root]);