sproutboat 0.50.0 → 0.51.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
@@ -10,6 +10,34 @@ maintained going forward by the `release` skill.
10
10
 
11
11
  ## Unreleased
12
12
 
13
+ ## [0.51.1] - 2026-11-05
14
+
15
+ Built on `@sproutboat/runtime` 0.42.2, `@sproutboat/toolchain` 0.23.1 and `@sproutboat/wire` 0.22.2. Nothing an app can observe changes except latency, the refused address ranges below and the `startTls()` write order, so there is no new compatibility date.
16
+
17
+ ### Fixed
18
+
19
+ - **Bytes written right after `socket.startTls()` no longer go out unencrypted.** On a `connect()` socket, the I/O thread took the start-TLS request and the app's queued writes in the same pass, and flushed all of them in plaintext before switching to TLS. Any write queued before the thread picked up the upgrade could leave in the clear. Only the bytes written before `startTls()` now go out in plaintext. Everything after waits until the TLS engine owns the socket, and new writes on the old plaintext socket reject, as in workerd. A failed plaintext flush is now a connection error. The egress tunnel also no longer stays paused on the discarded plain socket after an upgrade under backpressure.
20
+
21
+ - The BearSSL source ships in the package (`vendor/bearssl`, 867 KB) and is read first, hash-verified, so builds no longer depend on bearssl.org being up. bearssl.org stays as the fallback, with bounded retry on 5xx. Every source, vendored or downloaded, is checked against the pinned sha256, so a bad file cannot change what gets built.
22
+ - **A malformed `connect()` tunnel opening frame no longer ends the egress process.** A `tcp.open` head whose JSON did not parse used to throw out of the egress service's socket handler before the token was checked, which took egress down for every app. It now gets an `expected tcp.open` error frame and only that connection closes; the broker answers it as a bad request, and a refused head stops buffering whatever the client keeps sending.
23
+
24
+ ### Changed
25
+
26
+ - **`fetch()` and `connect()` refuse more reserved address 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`). This applies to the broker, the egress service and a standalone binary alike, and `SB_EGRESS_ALLOW` still overrides it.
27
+
28
+ ### Performance
29
+
30
+ - **Outbound TCP sockets set `TCP_NODELAY`.** Nagle's algorithm and the peer's delayed ACK stalled every fresh HTTPS `fetch()` by about 44 ms on Linux, because each small TLS handshake flight waited for an ACK; a fresh fetch now takes about 1.4 ms. This covers `fetch()`, outbound WebSockets and `connect()` on a standalone binary, including sockets handed over by `SB_EGRESS_FD`, and the egress tunnel's connections to origins.
31
+
32
+ ## [0.51.0] - 2026-11-03
33
+
34
+ Built on `@sproutboat/runtime` 0.42.0, `@sproutboat/toolchain` 0.23.0 and `@sproutboat/wire` 0.22.0. The change below starts at a new compatibility date; earlier dates answer as before.
35
+
36
+ ### Added
37
+
38
+ - **`connect()` from `cloudflare:sockets` from `2026-11-17`**: TCP sockets with `readable`, `writable`, `opened`, `closed`, `close()` and `startTls()`, on a standalone binary, under `sproutboat dev` and on a platform deploy. The same unmodified Postgres (`pg`, `postgres`) and Redis driver code that runs on workerd runs here, with no differences from workerd in the driver suites. Outbound policy is the one `fetch()` uses. Before the date, importing `cloudflare:sockets` fails the build with `cloudflare:sockets needs compatibility_date 2026-11-17 or later`.
39
+ - **From the same date a driver bundles as it does for workerd**: the `workerd` export condition (`pg-cloudflare`, `postgres/cf`), a bundled `Buffer`, `process` and `global`, and shims for `net`, `tls`, `dns`, `fs` and `crypto` so the Node branch of a driver names `cloudflare:sockets` instead of failing the build. A named function expression that shadows a global builtin function is renamed, which sproutd admission needs.
40
+
13
41
  ## [0.50.0] - 2026-11-02
14
42
 
15
43
  Built on `@sproutboat/runtime` 0.41.0, `@sproutboat/toolchain` 0.22.0 and `@sproutboat/wire` 0.21.4. Each change below that an existing app could observe starts at a new compatibility date; earlier dates answer as before.
package/SURFACE.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > Generated by `src/surface.test.ts` from `src/surface.ts` + the pinned
4
4
  > toolchain constants. Do not edit by hand — run `UPDATE_SURFACE=1 bun test`.
5
5
 
6
- **Package:** `sproutboat` 0.50.0
6
+ **Package:** `sproutboat` 0.51.1
7
7
 
8
8
  ## Commands
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutboat",
3
- "version": "0.50.0",
3
+ "version": "0.51.1",
4
4
  "description": "Wrangler-shaped CLI for Sproutboat. Deploys workers to any control plane via --api-url / SPROUTBOAT_API_URL.",
5
5
  "keywords": [
6
6
  "cli",
@@ -64,9 +64,9 @@
64
64
  "@sproutboat/artifact": "^0.3.0",
65
65
  "@sproutboat/assets": "^0.3.0",
66
66
  "@sproutboat/config": "^0.6.0",
67
- "@sproutboat/runtime": "^0.41.0",
68
- "@sproutboat/toolchain": "^0.22.0",
69
- "@sproutboat/wire": "^0.21.4"
67
+ "@sproutboat/runtime": "^0.42.2",
68
+ "@sproutboat/toolchain": "^0.23.1",
69
+ "@sproutboat/wire": "^0.22.2"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@oxlint/plugins": "1.85.0",
@@ -78,10 +78,10 @@
78
78
  "typescript": "7.0.2"
79
79
  },
80
80
  "optionalDependencies": {
81
- "@sproutboat/cli-darwin-arm64": "0.50.0",
82
- "@sproutboat/cli-darwin-x64": "0.50.0",
83
- "@sproutboat/cli-linux-arm64": "0.50.0",
84
- "@sproutboat/cli-linux-x64": "0.50.0"
81
+ "@sproutboat/cli-darwin-arm64": "0.51.1",
82
+ "@sproutboat/cli-darwin-x64": "0.51.1",
83
+ "@sproutboat/cli-linux-arm64": "0.51.1",
84
+ "@sproutboat/cli-linux-x64": "0.51.1"
85
85
  },
86
86
  "engines": {
87
87
  "node": ">=18"
package/src/bearssl.ts CHANGED
@@ -19,6 +19,9 @@ import { existsSync } from "node:fs";
19
19
  import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
20
20
  import { homedir } from "node:os";
21
21
  import { resolve } from "node:path";
22
+ // @ts-expect-error Bun's file loader returns the embedded asset path in a compiled executable.
23
+ import embeddedBearsslArchive from "../vendor/bearssl/bearssl-7bea48e.tar.gz" with { type: "file" };
24
+ import { fetchPinned, type FetchPinnedOptions } from "./fetch-pinned";
22
25
  import { LINUX_TARGETS, type BuildTarget } from "./targets";
23
26
  import { resolveHostCompiler } from "./toolchain";
24
27
 
@@ -112,12 +115,22 @@ const run = (
112
115
  child.on("close", (code) => done({ code: code ?? 1, stderr }));
113
116
  });
114
117
 
118
+ /**
119
+ * The pinned BearSSL source: the vendored copy (ships in the npm package, embedded in standalone
120
+ * binaries) first, bearssl.org only as a fallback. bearssl.org returned 502 for hours on 2026-11-04.
121
+ */
122
+ export const bearsslVendorArchive = (): string => embeddedBearsslArchive;
123
+
124
+ export const fetchBearsslTarball = (overrides: Partial<FetchPinnedOptions> = {}): Promise<Buffer> =>
125
+ fetchPinned({
126
+ localPath: bearsslVendorArchive(),
127
+ urls: [BEARSSL_URL],
128
+ sha256: BEARSSL_SHA256,
129
+ ...overrides,
130
+ });
131
+
115
132
  async function download(url: string, expected: string, to: string): Promise<Buffer> {
116
- const response = await fetch(url);
117
- if (!response.ok) throw new Error(`could not download ${url}: HTTP ${response.status}`);
118
- const bytes = Buffer.from(await response.arrayBuffer());
119
- const digest = createHash("sha256").update(bytes).digest("hex");
120
- if (digest !== expected) throw new Error(`checksum mismatch for ${url}: expected ${expected}, got ${digest}`);
133
+ const bytes = await fetchPinned({ urls: [url], sha256: expected });
121
134
  await writeFile(to, bytes);
122
135
  return bytes;
123
136
  }
@@ -143,7 +156,7 @@ async function sourceTree(): Promise<string> {
143
156
  await rm(tree, { recursive: true, force: true });
144
157
  if (!existsSync(tarball) || (await digest(tarball)) !== BEARSSL_SHA256) {
145
158
  await rm(tarball, { force: true });
146
- await download(BEARSSL_URL, BEARSSL_SHA256, tarball);
159
+ await writeFile(tarball, await fetchBearsslTarball());
147
160
  }
148
161
  const stage = resolve(dir, `.bearssl-stage-${process.pid}-${crypto.randomUUID()}`);
149
162
  await mkdir(stage, { recursive: true });
package/src/bundle.ts CHANGED
@@ -12,7 +12,8 @@
12
12
  * reaching for a Node API fails exactly as user code would.
13
13
  */
14
14
  import { relative } from "node:path";
15
- import { NODE_SHIMS } from "./node-shims";
15
+ import { SOCKETS_DATE } from "@sproutboat/runtime";
16
+ import { NODE_SHIMS, SOCKET_NODE_SHIMS } from "./node-shims";
16
17
 
17
18
  export type BundleResult = {
18
19
  /** One self-contained ESM module: what gets validated, hashed, and compiled. */
@@ -54,6 +55,86 @@ const nodeShimsPlugin: import("bun").BunPlugin = {
54
55
  },
55
56
  };
56
57
 
58
+ /**
59
+ * Porffor lists the global builtin functions on the global object of every unit, and a function
60
+ * named like one of them (postgres: `export const escapeIdentifier = function escape(str) {}`)
61
+ * drops the entry from that unit alone, so a sproutd app that does it no longer matches the
62
+ * runtime's common code and is rejected at admission. A named function expression's name is only
63
+ * visible inside itself, so renaming it is safe unless the body calls itself by that name.
64
+ */
65
+ const SHADOWED_BUILTIN_FUNCTIONS =
66
+ /([=(:,?&|]\s*|\breturn\s+)function\s+(escape|unescape|eval|isNaN|isFinite|parseInt|parseFloat|encodeURI|encodeURIComponent|decodeURI|decodeURIComponent)\s*\(/g;
67
+
68
+ /** What a driver reads off `process`: an empty environment and a microtask for nextTick. */
69
+ const PROCESS_STUB = `
70
+ const env = {};
71
+ function nextTick(fn, ...args) { Promise.resolve().then(() => fn(...args)); }
72
+ const process = { env, platform: "linux", arch: "wasm32", version: "v20.0.0", versions: {}, argv: [], browser: true, nextTick, emitWarning() {}, cwd() { return "/"; }, stderr: { write() {} }, stdout: { write() {} }, on() {}, off() {}, once() {}, emit() {} };
73
+ export default process;
74
+ `;
75
+
76
+ /**
77
+ * Node libraries use `Buffer` and `process` as free globals. Bun's browser target does not
78
+ * inject them for a CommonJS file, so a dependency that never imports Buffer would throw a
79
+ * ReferenceError at runtime. This gives every dependency file that uses one a local binding to the
80
+ * bundled implementation (the `buffer` package, which Porffor compiles; see the toolchain notes on
81
+ * typed array method dispatch). Applied only to node_modules, never to the handler's own code.
82
+ */
83
+ const nodeGlobalsPlugin: import("bun").BunPlugin = {
84
+ name: "sproutboat-node-globals",
85
+ setup(build) {
86
+ build.onResolve({ filter: /^sb-node-process$/ }, () => ({ path: "sb-node-process", namespace: "sb-node-global" }));
87
+ build.onLoad({ filter: /.*/, namespace: "sb-node-global" }, () => ({ contents: PROCESS_STUB, loader: "js" }));
88
+ build.onLoad({ filter: /node_modules\/.*\.(?:c?js|mjs)$/ }, async (args) => {
89
+ const original = await Bun.file(args.path).text();
90
+ const text = original.replace(
91
+ SHADOWED_BUILTIN_FUNCTIONS,
92
+ (_m, lead: string, name: string) => `${lead}function ${name}_sb(`,
93
+ );
94
+ const usesBuffer =
95
+ /(?:^|[^.\w$])Buffer\s*[.(]/m.test(text) &&
96
+ !/\b(?:import|require)\b[^;\n]*\bBuffer\b/.test(text) &&
97
+ !/(?:const|let|var)\s+\{?\s*Buffer\b/.test(text);
98
+ const usesProcess =
99
+ /(?:^|[^.\w$])process\s*\./m.test(text) &&
100
+ !/\b(?:import|require)\b[^;\n]*\bprocess\b/.test(text) &&
101
+ !/(?:const|let|var|function)\s+\{?\s*process\b/.test(text);
102
+ const usesGlobal =
103
+ /(?:^|[^.\w$])global\s*\./m.test(text) && !/(?:const|let|var|function)\s+\{?\s*global\b/.test(text);
104
+ if (!usesBuffer && !usesProcess && !usesGlobal)
105
+ return text === original ? undefined : { contents: text, loader: "js" };
106
+ const esm = /^\s*(?:import|export)\s/m.test(text);
107
+ let prefix = "";
108
+ if (usesBuffer) prefix += esm ? `import { Buffer } from "buffer";\n` : `var { Buffer } = require("buffer");\n`;
109
+ if (usesProcess)
110
+ prefix += esm
111
+ ? `import process from "sb-node-process";\n`
112
+ : `var process = require("sb-node-process").default;\n`;
113
+ if (usesGlobal) prefix += "var global = globalThis;\n";
114
+ return { contents: prefix + text, loader: "js" };
115
+ });
116
+ },
117
+ };
118
+
119
+ const socketShimSpecifiers = new RegExp(
120
+ `^(${Object.keys(SOCKET_NODE_SHIMS)
121
+ .map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
122
+ .join("|")})$`,
123
+ );
124
+
125
+ /** The Node built-ins a driver names, replaced from the sockets date: see SOCKET_NODE_SHIMS. */
126
+ const socketShimsPlugin: import("bun").BunPlugin = {
127
+ name: "sproutboat-socket-shims",
128
+ setup(build) {
129
+ build.onResolve({ filter: socketShimSpecifiers }, (args) => ({ path: args.path, namespace: "sb-socket-shim" }));
130
+ build.onLoad({ filter: /.*/, namespace: "sb-socket-shim" }, (args) => ({
131
+ // SAFETY: onResolve above only routes specifiers built from SOCKET_NODE_SHIMS's own keys here.
132
+ contents: (SOCKET_NODE_SHIMS as Record<string, string>)[args.path],
133
+ loader: "js",
134
+ }));
135
+ },
136
+ };
137
+
57
138
  /** Bun reports resolution failures on `AggregateError.errors`; its own message is just "Bundle failed". */
58
139
  function bundleDetail(cause: unknown): string {
59
140
  const errors = cause instanceof AggregateError ? cause.errors : [];
@@ -63,7 +144,15 @@ function bundleDetail(cause: unknown): string {
63
144
  }
64
145
 
65
146
  /** Bundle `entryPath` into a single module, resolving imports from `projectDir`. */
66
- export async function bundleHandler(entryPath: string, projectDir: string): Promise<BundleResult> {
147
+ export async function bundleHandler(
148
+ entryPath: string,
149
+ projectDir: string,
150
+ options: { compatibilityDate?: string } = {},
151
+ ): Promise<BundleResult> {
152
+ // From the sockets date a driver bundles as it does for workerd: its "workerd" export condition,
153
+ // Node's free `Buffer`/`process`/`global` bound to bundled code, and net/tls/dns/fs/crypto shims
154
+ // that name cloudflare:sockets when the driver takes its Node path.
155
+ const sockets = options.compatibilityDate !== undefined && options.compatibilityDate >= SOCKETS_DATE;
67
156
  let built: Awaited<ReturnType<typeof Bun.build>>;
68
157
  try {
69
158
  built = await Bun.build({
@@ -78,10 +167,11 @@ export async function bundleHandler(entryPath: string, projectDir: string): Prom
78
167
  minify: false,
79
168
  splitting: false,
80
169
  sourcemap: "none",
81
- plugins: [nodeShimsPlugin],
170
+ conditions: sockets ? ["workerd", "worker"] : undefined,
171
+ plugins: sockets ? [nodeShimsPlugin, socketShimsPlugin, nodeGlobalsPlugin] : [nodeShimsPlugin],
82
172
  // WorkerEntrypoint, DurableObject, RpcTarget and env are runtime globals; the
83
173
  // compiler turns this import into them, so it stays in the bundle as written.
84
- external: ["cloudflare:workers"],
174
+ external: ["cloudflare:workers", "cloudflare:sockets"],
85
175
  });
86
176
  } catch (cause) {
87
177
  // An unresolvable specifier arrives as an AggregateError whose `errors`
@@ -0,0 +1,80 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
+ import { dirname } from "node:path";
5
+
6
+ type Fetcher = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
7
+
8
+ export type FetchPinnedOptions = {
9
+ /** A vendored copy read from disk before any network source. Used only if its hash matches. */
10
+ localPath?: string;
11
+ /** Ordered download sources, tried after the vendored copy. Every one must serve the same bytes. */
12
+ urls: readonly string[];
13
+ /** Lowercase hex sha256 of the exact bytes. A source that serves anything else is rejected. */
14
+ sha256: string;
15
+ /** A verified copy is kept here (written atomically) and reused while its hash matches. */
16
+ cachePath?: string;
17
+ fetcher?: Fetcher;
18
+ /** Attempts per source for a 5xx or a network error. 4xx and a hash mismatch are never retried. */
19
+ attempts?: number;
20
+ /** First backoff; doubles each retry. */
21
+ backoffMs?: number;
22
+ timeoutMs?: number;
23
+ sleep?: (ms: number) => Promise<void>;
24
+ };
25
+
26
+ const sha256Hex = (bytes: Uint8Array): string => createHash("sha256").update(bytes).digest("hex");
27
+
28
+ /** A failure that will not change on retry: a 4xx, or bytes that fail the pin. */
29
+ class Permanent extends Error {}
30
+
31
+ /**
32
+ * Return a build-time dependency's bytes, only if they hash to the pinned sha256: first the vendored
33
+ * file (`localPath`), then the ordered download list. The pin is what makes every source safe: one
34
+ * that has different bytes (a tampered vendored file included) is skipped, so the list can only
35
+ * improve availability, never change what gets built. A 5xx or a network error is retried with exponential backoff (bounded); a 4xx or a
36
+ * hash mismatch moves straight on to the next source.
37
+ */
38
+ export async function fetchPinned(options: FetchPinnedOptions): Promise<Buffer> {
39
+ const sha256 = options.sha256.toLowerCase();
40
+ const fetcher: Fetcher = options.fetcher ?? fetch;
41
+ const attempts = Math.max(1, options.attempts ?? 3);
42
+ const sleep = options.sleep ?? ((ms: number) => Bun.sleep(ms));
43
+ if (options.localPath && existsSync(options.localPath)) {
44
+ const vendored = await readFile(options.localPath);
45
+ if (sha256Hex(vendored) === sha256) return vendored;
46
+ console.warn(
47
+ `fetchPinned: vendored ${options.localPath} does not match the pinned sha256, falling back to download`,
48
+ );
49
+ }
50
+ if (options.cachePath && existsSync(options.cachePath)) {
51
+ const cached = await readFile(options.cachePath);
52
+ if (sha256Hex(cached) === sha256) return cached;
53
+ await rm(options.cachePath, { force: true });
54
+ }
55
+ const failures: string[] = [];
56
+ for (const url of options.urls) {
57
+ for (let attempt = 0; attempt < attempts; attempt++) {
58
+ if (attempt > 0) await sleep((options.backoffMs ?? 500) * 2 ** (attempt - 1));
59
+ try {
60
+ const response = await fetcher(url, { signal: AbortSignal.timeout(options.timeoutMs ?? 60_000) });
61
+ if (response.status >= 500) throw new Error(`HTTP ${response.status}`);
62
+ if (!response.ok) throw new Permanent(`HTTP ${response.status}`);
63
+ const bytes = Buffer.from(await response.arrayBuffer());
64
+ const got = sha256Hex(bytes);
65
+ if (got !== sha256) throw new Permanent(`sha256 mismatch (expected ${sha256}, got ${got})`);
66
+ if (options.cachePath) {
67
+ await mkdir(dirname(options.cachePath), { recursive: true });
68
+ const stage = `${options.cachePath}.${process.pid}.tmp`;
69
+ await writeFile(stage, bytes);
70
+ await rename(stage, options.cachePath);
71
+ }
72
+ return bytes;
73
+ } catch (error) {
74
+ failures.push(`${url}: ${error instanceof Error ? error.message : String(error)}`);
75
+ if (error instanceof Permanent) break;
76
+ }
77
+ }
78
+ }
79
+ throw new Error(`could not download a verified copy (sha256 ${sha256}) from any source:\n ${failures.join("\n ")}`);
80
+ }
package/src/main.ts CHANGED
@@ -234,7 +234,9 @@ async function readProject(directory = process.cwd()) {
234
234
  // smuggle in a Node API the handler is not allowed to touch.
235
235
  let bundle: BundleResult;
236
236
  try {
237
- bundle = await bundleHandler(sourcePath, project.directory);
237
+ bundle = await bundleHandler(sourcePath, project.directory, {
238
+ compatibilityDate: project.config.compatibility_date,
239
+ });
238
240
  } catch (cause) {
239
241
  fail(cause instanceof BundleError ? cause.message : String(cause));
240
242
  }
package/src/node-shims.ts CHANGED
@@ -386,3 +386,85 @@ export const NODE_SHIMS = {
386
386
  "node:querystring": NODE_QUERYSTRING_SHIM,
387
387
  "node:url": NODE_URL_SHIM,
388
388
  } satisfies Record<string, string>;
389
+
390
+ // From the sockets date (2026-11-17), for drivers that carry a Node code path next to their Workers one.
391
+ /**
392
+ * node:net, node:tls, node:dns, node:fs and node:util/types for drivers that carry a Node code path
393
+ * next to their Workers one (pg, mysql2). The Workers path never calls them: every function throws
394
+ * a message that names cloudflare:sockets, so a driver that does take the Node path says why.
395
+ */
396
+ const unavailable = (what: string) =>
397
+ `function unavailable() { throw new Error("${what} is not available in a compiled handler; use cloudflare:sockets (connect())"); }\n`;
398
+ const NODE_NET_SHIM =
399
+ unavailable("the net module") +
400
+ `class Socket { constructor() { unavailable(); } }
401
+ const ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/;
402
+ function isIP(x) { return ipv4.test(String(x)) ? 4 : String(x).indexOf(":") >= 0 && /^[0-9a-fA-F:.]+$/.test(String(x)) ? 6 : 0; }
403
+ function connect() { unavailable(); }
404
+ export { Socket, isIP, connect };
405
+ export default { Socket, isIP, connect, createConnection: connect };
406
+ `;
407
+ const NODE_TLS_SHIM =
408
+ unavailable("the tls module") +
409
+ `function connect() { unavailable(); }
410
+ export { connect };
411
+ export default { connect };
412
+ `;
413
+ const NODE_DNS_SHIM =
414
+ unavailable("the dns module") +
415
+ `function lookup() { unavailable(); }
416
+ export { lookup };
417
+ export default { lookup, promises: { lookup } };
418
+ `;
419
+ const NODE_FS_SHIM =
420
+ unavailable("the fs module") +
421
+ `function readFileSync() { unavailable(); }
422
+ function existsSync() { return false; }
423
+ function statSync() { unavailable(); }
424
+ function createReadStream() { unavailable(); }
425
+ export { readFileSync, existsSync, statSync, createReadStream };
426
+ export default { readFileSync, existsSync, statSync, createReadStream };
427
+ `;
428
+ const NODE_UTIL_TYPES_SHIM = `
429
+ const isDate = (v) => v instanceof Date;
430
+ const isTypedArray = (v) => ArrayBuffer.isView(v) && !(v instanceof DataView);
431
+ const isUint8Array = (v) => v instanceof Uint8Array;
432
+ const isArrayBuffer = (v) => v instanceof ArrayBuffer;
433
+ const isPromise = (v) => v instanceof Promise;
434
+ const isRegExp = (v) => v instanceof RegExp;
435
+ export { isDate, isTypedArray, isUint8Array, isArrayBuffer, isPromise, isRegExp };
436
+ export default { isDate, isTypedArray, isUint8Array, isArrayBuffer, isPromise, isRegExp };
437
+ `;
438
+
439
+ /**
440
+ * node:crypto as a Workers handler has it: the Web Crypto global, not the Node API. pg asks for
441
+ * `crypto.webcrypto || globalThis.crypto` and for createHash("md5") inside a try, so a createHash
442
+ * that throws sends it to crypto.subtle. Bun's default polyfill (crypto-browserify) is 20k lines,
443
+ * and its elliptic-curve code does not compile under Porffor.
444
+ */
445
+ const NODE_CRYPTO_SHIM = `
446
+ function unavailable() { throw new Error("this crypto function is not available; use crypto.subtle"); }
447
+ const webcrypto = globalThis.crypto;
448
+ function randomBytes(n) { return webcrypto.getRandomValues(new Uint8Array(n)); }
449
+ function randomUUID() { return webcrypto.randomUUID(); }
450
+ function createHash() { unavailable(); }
451
+ function createHmac() { unavailable(); }
452
+ export { webcrypto, randomBytes, randomUUID, createHash, createHmac };
453
+ export default { webcrypto, randomBytes, randomUUID, createHash, createHmac, getRandomValues: (a) => webcrypto.getRandomValues(a), subtle: webcrypto.subtle };
454
+ `;
455
+
456
+ /** Replaces Bun's browser polyfills, in a bundle at the sockets date or later (see bundle.ts). */
457
+ export const SOCKET_NODE_SHIMS = {
458
+ "node:crypto": NODE_CRYPTO_SHIM,
459
+ crypto: NODE_CRYPTO_SHIM,
460
+ "node:net": NODE_NET_SHIM,
461
+ net: NODE_NET_SHIM,
462
+ "node:tls": NODE_TLS_SHIM,
463
+ tls: NODE_TLS_SHIM,
464
+ "node:dns": NODE_DNS_SHIM,
465
+ dns: NODE_DNS_SHIM,
466
+ "node:fs": NODE_FS_SHIM,
467
+ fs: NODE_FS_SHIM,
468
+ "node:util/types": NODE_UTIL_TYPES_SHIM,
469
+ "util/types": NODE_UTIL_TYPES_SHIM,
470
+ } satisfies Record<string, string>;
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2016 Thomas Pornin <pornin@bolet.org>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
18
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
19
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,14 @@
1
+ # Vendored BearSSL source
2
+
3
+ `bearssl-7bea48e.tar.gz` is the BearSSL source snapshot the CLI builds embedded sprouts against. It ships in the
4
+ npm package (and is embedded in standalone binaries), so a build needs no network access to bearssl.org, which
5
+ returned HTTP 502 for hours on 2026-11-04.
6
+
7
+ - Origin: https://bearssl.org/gitweb/?p=BearSSL;a=snapshot;h=7bea48e5e850ab4cafbe68d3765cdaba13a86d6f;sf=tgz
8
+ - Commit: `7bea48e5e850ab4cafbe68d3765cdaba13a86d6f` (`7bea48e5`; `git get-tar-commit-id` agrees)
9
+ - sha256: `ed397832b1613582737d9c1ce01c1af74ad6a9e9c52a341e24433987cda0f372`
10
+ - License: MIT, see `LICENSE.txt` (copied from the tarball)
11
+
12
+ `src/bearssl.ts` pins the sha256 and `fetchPinned` (`src/fetch-pinned.ts`) rejects this file if it does not match,
13
+ then falls back to bearssl.org. To update: replace the tarball, then change `BEARSSL_COMMIT`, `BEARSSL_SHA256` and
14
+ the import path in `src/bearssl.ts`.