@sproutboat/runtime 0.36.0 → 0.38.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.
@@ -0,0 +1,114 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { ensurePorffor } from "../../toolchain/src/acquire";
6
+ import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
7
+
8
+ // DOMException in a compiled handler, compared with Bun. Porffor ships a stub global that
9
+ // cannot be constructed; the prelude replaces it (found while testing third-party packages).
10
+ const CASES = [
11
+ `(() => { const e = new DOMException("m", "AbortError"); return [e.name, e.message, e.code, e instanceof Error, e instanceof DOMException].join(); })()`,
12
+ `(() => { const e = new DOMException(); return [e.name, JSON.stringify(e.message), e.code].join(); })()`,
13
+ `(() => { const e = new DOMException("m", { name: "TimeoutError", cause: "c" }); return [e.name, e.code, e.cause].join(); })()`,
14
+ `(() => { const e = new DOMException("x", "CustomName"); return [e.name, e.code].join(); })()`,
15
+ `[DOMException.ABORT_ERR, DOMException.TIMEOUT_ERR, DOMException.DATA_CLONE_ERR, DOMException.NOT_SUPPORTED_ERR, DOMException.INVALID_STATE_ERR].join()`,
16
+ `(() => { const r = AbortSignal.abort().reason; return [r instanceof DOMException, r.name, r.code].join(); })()`,
17
+ `(() => { const c = new AbortController(); c.abort(); try { c.signal.throwIfAborted(); return "no throw"; } catch (e) { return [e instanceof DOMException, e.name].join(); } })()`,
18
+ `(() => { try { throw new DOMException("boom", "InvalidStateError"); } catch (e) { return String(e); } })()`,
19
+ ];
20
+ const handler = `export default {async fetch() {
21
+ const cases = [
22
+ ${CASES.map((c) => ` () => (${c}),`).join("\n")}
23
+ ];
24
+ const out = [];
25
+ for (const run of cases) {
26
+ try { out.push(String(await run())); } catch (e) { out.push("THROWS " + (e && e.message)); }
27
+ }
28
+ return new Response(JSON.stringify(out));
29
+ }};`;
30
+ const want = await Promise.all(
31
+ CASES.map(async (c) => {
32
+ try {
33
+ return String(await new Function("return " + c)());
34
+ } catch (e) {
35
+ return "THROWS " + (e instanceof Error ? e.message : String(e));
36
+ }
37
+ }),
38
+ );
39
+
40
+ function freePort(): number {
41
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
42
+ const port = listener.port;
43
+ listener.stop(true);
44
+ return port;
45
+ }
46
+
47
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-domexception-"));
48
+ let server: ReturnType<typeof Bun.spawn> | undefined;
49
+ try {
50
+ const porffor = await ensurePorffor();
51
+ const node = Bun.which("node");
52
+ if (!node) throw new Error("native streams test needs node on PATH");
53
+
54
+ const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
55
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
56
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
57
+ const generated = join(workdir, "domexception.generated.js");
58
+ const binary = join(workdir, "domexception-native");
59
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
60
+ await rm(binary, { force: true });
61
+
62
+ const compile = Bun.spawn(
63
+ [node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
64
+ {
65
+ cwd: workdir,
66
+ env: process.env,
67
+ stdout: "pipe",
68
+ stderr: "pipe",
69
+ },
70
+ );
71
+ const [exitCode, stdout, stderr] = await Promise.all([
72
+ compile.exited,
73
+ new Response(compile.stdout).text(),
74
+ new Response(compile.stderr).text(),
75
+ ]);
76
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
77
+
78
+ const port = freePort();
79
+ server = Bun.spawn([binary], {
80
+ cwd: workdir,
81
+ env: { ...process.env, PORT: String(port) },
82
+ stdout: "ignore",
83
+ stderr: "inherit",
84
+ });
85
+ let ready = false;
86
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
87
+ try {
88
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
89
+ ready = true;
90
+ } catch {
91
+ if (server.exitCode !== null) break;
92
+ await Bun.sleep(100);
93
+ }
94
+ }
95
+ assert(ready, "compiled handler did not start");
96
+ // SAFETY: the handler above is the only producer of this body: a JSON array with one
97
+ // string per case; the comparison below reports any entry that differs.
98
+ const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
99
+ const diffs = CASES.flatMap((c, i) =>
100
+ got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`],
101
+ );
102
+ assert.deepEqual(
103
+ diffs,
104
+ [],
105
+ `DOMException results differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`,
106
+ );
107
+ console.log(`Native DOMException matches Bun in all ${CASES.length} cases`);
108
+ } finally {
109
+ if (server) {
110
+ server.kill(9);
111
+ await server.exited;
112
+ }
113
+ await rm(workdir, { recursive: true, force: true });
114
+ }
@@ -0,0 +1,117 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { ensurePorffor } from "../../toolchain/src/acquire";
6
+ import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
7
+
8
+ // crypto.getRandomValues on wider integer views, in a compiled handler, compared with Bun. It filled only the
9
+ // low byte of each element (found while testing third-party packages).
10
+ const CASES = [
11
+ `(() => { const b = new Uint32Array(256); crypto.getRandomValues(b); return [b.some((x) => x > 65535), b.some((x) => x > 16777215), b.length].join(); })()`,
12
+ `(() => { const b = new Uint16Array(512); crypto.getRandomValues(b); return b.some((x) => x > 255); })()`,
13
+ `(() => { const b = new BigUint64Array(64); crypto.getRandomValues(b); return b.some((x) => x > 4294967295n); })()`,
14
+ `(() => { const b = new Int32Array(256); crypto.getRandomValues(b); return b.some((x) => x < 0); })()`,
15
+ `(() => { const c = new Array(37).fill(0); const b = new Uint32Array(1); for (let i = 0; i < 3700; i++) { crypto.getRandomValues(b); c[Math.floor((b[0] / 2 ** 32) * 37)]++; } return c.filter((n) => n > 0).length > 30; })()`,
16
+ `(() => { const buf = new Uint8Array(16); const view = new Uint32Array(buf.buffer, 4, 2); crypto.getRandomValues(view); return [buf.slice(0, 4).every((x) => x === 0), buf.slice(12).every((x) => x === 0), buf.slice(4, 12).some((x) => x > 0)].join(); })()`,
17
+ `(() => { const b = new Uint8Array(8); return crypto.getRandomValues(b) === b; })()`,
18
+ `(() => { try { crypto.getRandomValues(new Float32Array(2)); return "no throw"; } catch (e) { return e.name; } })()`,
19
+ `(() => { try { crypto.getRandomValues(new DataView(new ArrayBuffer(4))); return "no throw"; } catch (e) { return e.name; } })()`,
20
+ `(() => { try { crypto.getRandomValues([1, 2]); return "no throw"; } catch (e) { return e.name; } })()`,
21
+ `(() => { const b = new Uint8Array(0); return crypto.getRandomValues(b).length; })()`,
22
+ ];
23
+ const handler = `export default {async fetch() {
24
+ const cases = [
25
+ ${CASES.map((c) => ` () => (${c}),`).join("\n")}
26
+ ];
27
+ const out = [];
28
+ for (const run of cases) {
29
+ try { out.push(String(await run())); } catch (e) { out.push("THROWS " + (e && e.message)); }
30
+ }
31
+ return new Response(JSON.stringify(out));
32
+ }};`;
33
+ const want = await Promise.all(
34
+ CASES.map(async (c) => {
35
+ try {
36
+ return String(await new Function("return " + c)());
37
+ } catch (e) {
38
+ return "THROWS " + (e instanceof Error ? e.message : String(e));
39
+ }
40
+ }),
41
+ );
42
+
43
+ function freePort(): number {
44
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
45
+ const port = listener.port;
46
+ listener.stop(true);
47
+ return port;
48
+ }
49
+
50
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-getrandomvalues-"));
51
+ let server: ReturnType<typeof Bun.spawn> | undefined;
52
+ try {
53
+ const porffor = await ensurePorffor();
54
+ const node = Bun.which("node");
55
+ if (!node) throw new Error("native streams test needs node on PATH");
56
+
57
+ const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
58
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
59
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
60
+ const generated = join(workdir, "getrandomvalues.generated.js");
61
+ const binary = join(workdir, "getrandomvalues-native");
62
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
63
+ await rm(binary, { force: true });
64
+
65
+ const compile = Bun.spawn(
66
+ [node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
67
+ {
68
+ cwd: workdir,
69
+ env: process.env,
70
+ stdout: "pipe",
71
+ stderr: "pipe",
72
+ },
73
+ );
74
+ const [exitCode, stdout, stderr] = await Promise.all([
75
+ compile.exited,
76
+ new Response(compile.stdout).text(),
77
+ new Response(compile.stderr).text(),
78
+ ]);
79
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
80
+
81
+ const port = freePort();
82
+ server = Bun.spawn([binary], {
83
+ cwd: workdir,
84
+ env: { ...process.env, PORT: String(port) },
85
+ stdout: "ignore",
86
+ stderr: "inherit",
87
+ });
88
+ let ready = false;
89
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
90
+ try {
91
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
92
+ ready = true;
93
+ } catch {
94
+ if (server.exitCode !== null) break;
95
+ await Bun.sleep(100);
96
+ }
97
+ }
98
+ assert(ready, "compiled handler did not start");
99
+ // SAFETY: the handler above is the only producer of this body: a JSON array with one
100
+ // string per case; the comparison below reports any entry that differs.
101
+ const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
102
+ const diffs = CASES.flatMap((c, i) =>
103
+ got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`],
104
+ );
105
+ assert.deepEqual(
106
+ diffs,
107
+ [],
108
+ `getRandomValues results differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`,
109
+ );
110
+ console.log(`Native getRandomValues matches Bun in all ${CASES.length} cases`);
111
+ } finally {
112
+ if (server) {
113
+ server.kill(9);
114
+ await server.exited;
115
+ }
116
+ await rm(workdir, { recursive: true, force: true });
117
+ }
@@ -4,6 +4,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join, resolve } from "node:path";
6
6
  import { ensurePorffor } from "../../toolchain/src/acquire";
7
+ import { bearsslEnv } from "./bearssl";
7
8
  import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
8
9
 
9
10
  // The pieces of a JWT check (base64url decode, TextEncoder, HMAC verify) against values that do
@@ -290,7 +291,7 @@ try {
290
291
  [node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
291
292
  {
292
293
  cwd: workdir,
293
- env: process.env,
294
+ env: { ...process.env, ...(await bearsslEnv()) },
294
295
  stdout: "pipe",
295
296
  stderr: "pipe",
296
297
  },
@@ -0,0 +1,182 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { bearsslEnv } from "./bearssl";
6
+ import { ensurePorffor } from "../../toolchain/src/acquire";
7
+ import { jsonObject, parseJsonValue, type JsonObject, type JsonValue } from "../../wire/src/json";
8
+ import { SUBTLE_DATE } from "../src/subtle-date";
9
+ import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
10
+
11
+ // The date gate and the parts of crypto.subtle that need no native half (the broker transport has
12
+ // none): HMAC, digest, JWK for symmetric keys, and every error's DOMException name, against what
13
+ // workerd answers for the same calls. The RSA/ECDSA/Ed25519/AES paths run in the embedded
14
+ // transport and are covered by src/subtle/subtle.test.ts (C against BoringSSL) and the
15
+ // conformance run in the PR description.
16
+ const handler = `
17
+ const hex = (b) => [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, "0")).join("");
18
+ async function t(f) {
19
+ try { return await f(); } catch (e) { return "ERR " + e.name + (e instanceof DOMException ? " dom" : ""); }
20
+ }
21
+ const raw = new Uint8Array(32).fill(7);
22
+ const hmac = { name: "HMAC", hash: "SHA-256" };
23
+ export default {
24
+ async fetch() {
25
+ const s = crypto.subtle;
26
+ const out = {};
27
+ out.jwkImport = await t(async () => {
28
+ const k = await s.importKey("jwk", { kty: "oct", k: "AAECAwQFBgc", alg: "HS256", ext: true }, hmac, true, ["sign"]);
29
+ return [k.algorithm.length, k.type, k.usages, hex(await s.exportKey("raw", k))];
30
+ });
31
+ out.jwkExport = await t(async () => {
32
+ const k = await s.importKey("raw", raw, hmac, true, ["sign", "verify"]);
33
+ const j = await s.exportKey("jwk", k);
34
+ return [j.kty, j.alg, j.k, j.key_ops, j.ext];
35
+ });
36
+ out.badFormat = await t(() => s.importKey("foo", raw, hmac, true, ["sign"]));
37
+ out.badUsage = await t(() => s.importKey("raw", raw, hmac, true, ["encrypt"]));
38
+ out.empty = await t(() => s.importKey("raw", new Uint8Array(0), hmac, true, ["sign"]));
39
+ out.noHash = await t(() => s.importKey("raw", raw, { name: "HMAC" }, true, ["sign"]));
40
+ out.unknown = await t(() => s.digest("FOO", raw));
41
+ out.nonExtractable = await t(async () => s.exportKey("raw", await s.importKey("raw", raw, hmac, false, ["sign"])));
42
+ out.wrongUsage = await t(async () => s.sign("HMAC", await s.importKey("raw", raw, hmac, true, ["verify"]), raw));
43
+ out.wrongAlgorithm = await t(async () => s.sign("ECDSA", await s.importKey("raw", raw, hmac, true, ["sign"]), raw));
44
+ out.stringData = await t(async () => s.sign("HMAC", await s.importKey("raw", raw, hmac, true, ["sign"]), "abc"));
45
+ out.badJwkAlg = await t(() => s.importKey("jwk", { kty: "oct", k: "AAECAwQFBgc", alg: "HS384" }, hmac, true, ["sign"]));
46
+ out.badJwkKty = await t(() => s.importKey("jwk", { kty: "RSA", k: "AAECAwQFBgc" }, hmac, true, ["sign"]));
47
+ out.badJwkB64 = await t(() => s.importKey("jwk", { kty: "oct", k: "!!!" }, hmac, true, ["sign"]));
48
+ out.extFalse = await t(() => s.importKey("jwk", { kty: "oct", k: "AAECAwQFBgc", ext: false }, hmac, true, ["sign"]));
49
+ out.signVerify = await t(async () => {
50
+ const k = await s.importKey("raw", raw, { name: "HMAC", hash: "SHA-512" }, false, ["sign", "verify"]);
51
+ const sig = await s.sign("HMAC", k, raw);
52
+ return [sig.byteLength, await s.verify("HMAC", k, sig, raw), await s.verify("HMAC", k, sig, new Uint8Array(3))];
53
+ });
54
+ out.digests = await t(async () => [hex(await s.digest("SHA-1", raw)), hex(await s.digest({ name: "sha-256" }, raw)), hex(await s.digest("MD5", raw))]);
55
+ out.tse = await t(() => [s.timingSafeEqual(raw, raw), s.timingSafeEqual(raw, new Uint8Array(32))]);
56
+ out.tseLength = await t(() => s.timingSafeEqual(raw, new Uint8Array(3)));
57
+ out.isKey = await t(async () => { const k = await s.importKey("raw", raw, hmac, true, ["sign"]); return [k instanceof CryptoKey, Object.keys(k)]; });
58
+ // The native half is in the prelude on every transport.
59
+ out.needsNative = await t(async () => { const k = await s.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]); const sig = await s.sign({ name: "ECDSA", hash: "SHA-256" }, k.privateKey, raw); return await s.verify({ name: "ECDSA", hash: "SHA-256" }, k.publicKey, sig, raw); });
60
+ out.unsupportedCurve = await t(() => s.generateKey({ name: "ECDSA", namedCurve: "secp256k1" }, true, ["sign"]));
61
+ return Response.json(out);
62
+ },
63
+ };`;
64
+
65
+ const dom = " dom";
66
+ const expectedNew: JsonObject = {
67
+ jwkImport: [64, "secret", ["sign"], "0001020304050607"],
68
+ jwkExport: ["oct", "HS256", "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc", ["sign", "verify"], true],
69
+ badFormat: "ERR NotSupportedError" + dom,
70
+ badUsage: "ERR SyntaxError" + dom,
71
+ empty: "ERR DataError" + dom,
72
+ noHash: "ERR TypeError",
73
+ unknown: "ERR NotSupportedError" + dom,
74
+ nonExtractable: "ERR InvalidAccessError" + dom,
75
+ wrongUsage: "ERR InvalidAccessError" + dom,
76
+ wrongAlgorithm: "ERR InvalidAccessError" + dom,
77
+ stringData: "ERR TypeError",
78
+ badJwkAlg: "ERR DataError" + dom,
79
+ badJwkKty: "ERR DataError" + dom,
80
+ badJwkB64: "ERR DataError" + dom,
81
+ extFalse: "ERR DataError" + dom,
82
+ signVerify: [64, true, false],
83
+ digests: [],
84
+ tse: [true, false],
85
+ tseLength: "ERR TypeError",
86
+ isKey: [true, ["usages", "algorithm", "extractable", "type"]],
87
+ needsNative: true,
88
+ unsupportedCurve: "ERR NotSupportedError" + dom,
89
+ };
90
+ // The digests are checked against Bun, not typed in.
91
+ const bunHex = async (algo: string, data: Uint8Array) =>
92
+ Buffer.from(await crypto.subtle.digest(algo, data)).toString("hex");
93
+ const rawBytes = new Uint8Array(32).fill(7);
94
+ const digests: JsonValue[] = [
95
+ await bunHex("SHA-1", rawBytes),
96
+ await bunHex("SHA-256", rawBytes),
97
+ Buffer.from(new Bun.CryptoHasher("md5").update(rawBytes).digest()).toString("hex"),
98
+ ];
99
+ expectedNew.digests = digests;
100
+
101
+ function freePort(): number {
102
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
103
+ const port = listener.port;
104
+ listener.stop(true);
105
+ return port;
106
+ }
107
+
108
+ async function run(date: string): Promise<JsonObject> {
109
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-subtle-"));
110
+ let server: ReturnType<typeof Bun.spawn> | undefined;
111
+ try {
112
+ const porffor = await ensurePorffor();
113
+ const node = Bun.which("node");
114
+ if (!node) throw new Error("native subtle test needs node on PATH");
115
+ const [core, broker] = await Promise.all([
116
+ readFile(preludePath, "utf8"),
117
+ readFile(transportPath("broker"), "utf8"),
118
+ ]);
119
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
120
+ const generated = join(workdir, "subtle.generated.js");
121
+ const binary = join(workdir, "subtle-native");
122
+ await writeFile(
123
+ generated,
124
+ wrapNativeFetchHandler(handler, core.replace(TRANSPORT_MARKER, broker), undefined, undefined, undefined, date),
125
+ );
126
+ const compile = Bun.spawn(
127
+ [node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
128
+ {
129
+ cwd: workdir,
130
+ env: { ...process.env, ...(await bearsslEnv()) },
131
+ stdout: "pipe",
132
+ stderr: "pipe",
133
+ },
134
+ );
135
+ const [exitCode, stdout, stderr] = await Promise.all([
136
+ compile.exited,
137
+ new Response(compile.stdout).text(),
138
+ new Response(compile.stderr).text(),
139
+ ]);
140
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
141
+ const port = freePort();
142
+ server = Bun.spawn([binary], {
143
+ cwd: workdir,
144
+ env: { ...process.env, PORT: String(port) },
145
+ stdout: "ignore",
146
+ stderr: "inherit",
147
+ });
148
+ for (let attempt = 0; attempt < 50; attempt++) {
149
+ try {
150
+ const response = await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
151
+ assert.equal(response.status, 200);
152
+ const body = jsonObject(parseJsonValue(await response.text()));
153
+ assert(body, "the handler returns a JSON object");
154
+ return body;
155
+ } catch (error) {
156
+ if (error instanceof assert.AssertionError) throw error;
157
+ if (server.exitCode !== null) break;
158
+ await Bun.sleep(100);
159
+ }
160
+ }
161
+ throw new Error(`compiled handler did not start (${date})`);
162
+ } finally {
163
+ if (server) {
164
+ server.kill(9);
165
+ await server.exited;
166
+ }
167
+ await rm(workdir, { recursive: true, force: true });
168
+ }
169
+ }
170
+
171
+ const prelude = await readFile(preludePath, "utf8");
172
+ assert(prelude.includes(`const __SB_SUBTLE_DATE = "${SUBTLE_DATE}";`), "SUBTLE_DATE and __SB_SUBTLE_DATE differ");
173
+ const [before, after] = await Promise.all([
174
+ run(new Date(Date.parse(SUBTLE_DATE) - 86_400_000).toISOString().slice(0, 10)),
175
+ run(SUBTLE_DATE),
176
+ ]);
177
+ assert.deepEqual(after, expectedNew, "from the new date crypto.subtle answers as workerd does");
178
+ // Before the date nothing changed: the old subset, its plain-object keys and its errors.
179
+ assert.equal(before.jwkImport, "ERR NotSupportedError dom", "before the date, jwk import is not supported");
180
+ assert.equal(before.needsNative, "ERR NotSupportedError dom", "before the date, ECDSA is not supported");
181
+ assert.deepEqual(before.signVerify, [64, true, false]);
182
+ console.log("crypto.subtle: the date gate and the transport-independent surface match workerd");
@@ -0,0 +1,123 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { ensurePorffor } from "../../toolchain/src/acquire";
6
+ import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
7
+
8
+ // TextDecoder encodings (UTF-16LE/BE, windows-1252), Blob.stream and Response.url in a compiled handler, compared with Bun
9
+ // (found while testing third-party packages).
10
+ const CASES = [
11
+ `new TextDecoder("utf-16le").decode(new Uint8Array([104, 0, 105, 0, 0x3d, 0xd8, 0x00, 0xde]))`,
12
+ `new TextDecoder("utf-16be").decode(new Uint8Array([0, 104, 0, 105]))`,
13
+ `JSON.stringify(new TextDecoder("utf-16le").decode(new Uint8Array([0xff, 0xfe, 65, 0])))`,
14
+ `JSON.stringify(new TextDecoder("utf-16le", { ignoreBOM: true }).decode(new Uint8Array([0xff, 0xfe, 65, 0])))`,
15
+ `JSON.stringify(new TextDecoder("utf-16le").decode(new Uint8Array([0x00, 0xd8, 65, 0])))`,
16
+ `JSON.stringify(new TextDecoder("utf-16le").decode(new Uint8Array([65, 0, 66])))`,
17
+ `(() => { try { new TextDecoder("utf-16le", { fatal: true }).decode(new Uint8Array([0x00, 0xd8])); return "no throw"; } catch (e) { return e.name; } })()`,
18
+ `(() => { const d = new TextDecoder("utf-16le"); return [d.decode(new Uint8Array([104]), { stream: true }), d.decode(new Uint8Array([0, 105]), { stream: true }), d.decode(new Uint8Array([0x3d, 0xd8]), { stream: true }), d.decode(new Uint8Array([0x00, 0xde]))].join("|"); })()`,
19
+ `new TextDecoder("windows-1252").decode(new Uint8Array([0x80, 0x41, 0xe9, 0x9f, 0xff, 0x7f]))`,
20
+ `JSON.stringify(new TextDecoder("windows-1252").decode(new Uint8Array([0x81, 0x8d, 0x8f, 0x90, 0x9d])))`,
21
+ `[new TextDecoder("latin1").encoding, new TextDecoder("ISO-8859-1").encoding, new TextDecoder(" ascii ").encoding, new TextDecoder("utf-16").encoding, new TextDecoder("UTF8").encoding, new TextDecoder("unicodefffe").encoding].join()`,
22
+ `(() => { try { new TextDecoder("not-an-encoding"); return "no throw"; } catch (e) { return e.name; } })()`,
23
+ `new TextDecoder("utf-8").decode(new Uint8Array([0xe2, 0x82, 0xac]))`,
24
+ `new Response(new Blob(["abc", new Uint8Array([100, 101])]).stream()).text()`,
25
+ `(async () => { const r = new Blob(["hello"]).stream().getReader(); const a = await r.read(); const b = await r.read(); return [a.done, a.value.length, b.done].join(); })()`,
26
+ `JSON.stringify([new Response("x").url, new Response("x").redirected, new Response(null, { status: 429 }).url + "|"])`,
27
+ `(async () => { const r = new Blob([]).stream().getReader(); const a = await r.read(); return a.done; })()`,
28
+ ];
29
+ const handler = `export default {async fetch() {
30
+ const cases = [
31
+ ${CASES.map((c) => ` () => (${c}),`).join("\n")}
32
+ ];
33
+ const out = [];
34
+ for (const run of cases) {
35
+ try { out.push(String(await run())); } catch (e) { out.push("THROWS " + (e && e.message)); }
36
+ }
37
+ return new Response(JSON.stringify(out));
38
+ }};`;
39
+ const want = await Promise.all(
40
+ CASES.map(async (c) => {
41
+ try {
42
+ return String(await new Function("return " + c)());
43
+ } catch (e) {
44
+ return "THROWS " + (e instanceof Error ? e.message : String(e));
45
+ }
46
+ }),
47
+ );
48
+
49
+ function freePort(): number {
50
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
51
+ const port = listener.port;
52
+ listener.stop(true);
53
+ return port;
54
+ }
55
+
56
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-textdecoder-encodings-"));
57
+ let server: ReturnType<typeof Bun.spawn> | undefined;
58
+ try {
59
+ const porffor = await ensurePorffor();
60
+ const node = Bun.which("node");
61
+ if (!node) throw new Error("native streams test needs node on PATH");
62
+
63
+ const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
64
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
65
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
66
+ const generated = join(workdir, "textdecoder-encodings.generated.js");
67
+ const binary = join(workdir, "textdecoder-encodings-native");
68
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
69
+ await rm(binary, { force: true });
70
+
71
+ const compile = Bun.spawn(
72
+ [node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
73
+ {
74
+ cwd: workdir,
75
+ env: process.env,
76
+ stdout: "pipe",
77
+ stderr: "pipe",
78
+ },
79
+ );
80
+ const [exitCode, stdout, stderr] = await Promise.all([
81
+ compile.exited,
82
+ new Response(compile.stdout).text(),
83
+ new Response(compile.stderr).text(),
84
+ ]);
85
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
86
+
87
+ const port = freePort();
88
+ server = Bun.spawn([binary], {
89
+ cwd: workdir,
90
+ env: { ...process.env, PORT: String(port) },
91
+ stdout: "ignore",
92
+ stderr: "inherit",
93
+ });
94
+ let ready = false;
95
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
96
+ try {
97
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
98
+ ready = true;
99
+ } catch {
100
+ if (server.exitCode !== null) break;
101
+ await Bun.sleep(100);
102
+ }
103
+ }
104
+ assert(ready, "compiled handler did not start");
105
+ // SAFETY: the handler above is the only producer of this body: a JSON array with one
106
+ // string per case; the comparison below reports any entry that differs.
107
+ const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
108
+ const diffs = CASES.flatMap((c, i) =>
109
+ got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`],
110
+ );
111
+ assert.deepEqual(
112
+ diffs,
113
+ [],
114
+ `TextDecoder, Blob.stream and Response.url results differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`,
115
+ );
116
+ console.log(`Native TextDecoder encodings, Blob.stream and Response.url match Bun in all ${CASES.length} cases`);
117
+ } finally {
118
+ if (server) {
119
+ server.kill(9);
120
+ await server.exited;
121
+ }
122
+ await rm(workdir, { recursive: true, force: true });
123
+ }
@@ -3,6 +3,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
5
  import { ensurePorffor } from "../../toolchain/src/acquire";
6
+ import { bearsslEnv } from "./bearssl";
6
7
  import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
7
8
 
8
9
  // Web API behaviour the W2 conformance suite found wrong, compared with Bun: Headers
@@ -25,7 +26,8 @@ const CASES = [
25
26
  `new URL("http://münchen.test/x").host`,
26
27
  `crypto.subtle.digest("SHA-1", new TextEncoder().encode("abc")).then((b) => Array.from(new Uint8Array(b), (x) => x.toString(16).padStart(2, "0")).join(""))`,
27
28
  `crypto.subtle.importKey("raw", new TextEncoder().encode("key"), { name: "HMAC", hash: "SHA-1" }, true, ["sign"]).then(async (k) => [k.algorithm.hash.name, k.algorithm.length, Array.from(new Uint8Array(await crypto.subtle.sign("HMAC", k, new TextEncoder().encode("msg"))), (x) => x.toString(16).padStart(2, "0")).join(""), new Uint8Array(await crypto.subtle.exportKey("raw", k)).length].join())`,
28
- `crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-512" }, true, ["sign"]).then(async (k) => [k.type, k.algorithm.length, (await crypto.subtle.exportKey("raw", k)).byteLength].join())`, `(async () => { const collect = async (rs) => { const r = rs.getReader(); const out = []; for (;;) { const { value, done } = await r.read(); if (done) return out; out.push(value); } }; const bytes = new ReadableStream({ start(c) { c.enqueue(new Uint8Array([0x61, 0xe2])); c.enqueue(new Uint8Array([0x82, 0xac, 0xf0, 0x9f])); c.enqueue(new Uint8Array([0x98, 0x80])); c.close(); } }); const text = new ReadableStream({ start(c) { c.enqueue("a\\uD83D"); c.enqueue("\\uDE00b"); c.close(); } }); return (await collect(bytes.pipeThrough(new TextDecoderStream()))).join("|") + ";" + (await collect(text.pipeThrough(new TextEncoderStream()))).map((u) => Array.from(u).join(",")).join("|"); })()`,
29
+ `crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-512" }, true, ["sign"]).then(async (k) => [k.type, k.algorithm.length, (await crypto.subtle.exportKey("raw", k)).byteLength].join())`,
30
+ `(async () => { const collect = async (rs) => { const r = rs.getReader(); const out = []; for (;;) { const { value, done } = await r.read(); if (done) return out; out.push(value); } }; const bytes = new ReadableStream({ start(c) { c.enqueue(new Uint8Array([0x61, 0xe2])); c.enqueue(new Uint8Array([0x82, 0xac, 0xf0, 0x9f])); c.enqueue(new Uint8Array([0x98, 0x80])); c.close(); } }); const text = new ReadableStream({ start(c) { c.enqueue("a\\uD83D"); c.enqueue("\\uDE00b"); c.close(); } }); return (await collect(bytes.pipeThrough(new TextDecoderStream()))).join("|") + ";" + (await collect(text.pipeThrough(new TextEncoderStream()))).map((u) => Array.from(u).join(",")).join("|"); })()`,
29
31
  ];
30
32
  const handler = `export default {async fetch() {
31
33
  const cases = [
@@ -37,7 +39,15 @@ ${CASES.map((c) => ` () => (${c}),`).join("\n")}
37
39
  }
38
40
  return new Response(JSON.stringify(out));
39
41
  }};`;
40
- const want = await Promise.all(CASES.map(async (c) => { try { return String(await new Function("return " + c)()); } catch (e) { return "THROWS " + (e instanceof Error ? e.message : String(e)); } }));
42
+ const want = await Promise.all(
43
+ CASES.map(async (c) => {
44
+ try {
45
+ return String(await new Function("return " + c)());
46
+ } catch (e) {
47
+ return "THROWS " + (e instanceof Error ? e.message : String(e));
48
+ }
49
+ }),
50
+ );
41
51
 
42
52
  function freePort(): number {
43
53
  const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
@@ -53,10 +63,7 @@ try {
53
63
  const node = Bun.which("node");
54
64
  if (!node) throw new Error("native streams test needs node on PATH");
55
65
 
56
- const [core, broker] = await Promise.all([
57
- readFile(preludePath, "utf8"),
58
- readFile(transportPath("broker"), "utf8"),
59
- ]);
66
+ const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
60
67
  assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
61
68
  const prelude = core.replace(TRANSPORT_MARKER, broker);
62
69
  const generated = join(workdir, "web-apis.generated.js");
@@ -64,12 +71,15 @@ try {
64
71
  await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
65
72
  await rm(binary, { force: true });
66
73
 
67
- const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
68
- cwd: workdir,
69
- env: process.env,
70
- stdout: "pipe",
71
- stderr: "pipe",
72
- });
74
+ const compile = Bun.spawn(
75
+ [node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
76
+ {
77
+ cwd: workdir,
78
+ env: { ...process.env, ...(await bearsslEnv()) },
79
+ stdout: "pipe",
80
+ stderr: "pipe",
81
+ },
82
+ );
73
83
  const [exitCode, stdout, stderr] = await Promise.all([
74
84
  compile.exited,
75
85
  new Response(compile.stdout).text(),
@@ -98,8 +108,14 @@ try {
98
108
  // SAFETY: the handler above is the only producer of this body: a JSON array with one
99
109
  // string per case; the comparison below reports any entry that differs.
100
110
  const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
101
- const diffs = CASES.flatMap((c, i) => (got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`]));
102
- assert.deepEqual(diffs, [], `Web API results differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`);
111
+ const diffs = CASES.flatMap((c, i) =>
112
+ got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`],
113
+ );
114
+ assert.deepEqual(
115
+ diffs,
116
+ [],
117
+ `Web API results differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`,
118
+ );
103
119
  console.log(`Native Web APIs match Bun in all ${CASES.length} cases`);
104
120
  } finally {
105
121
  if (server) {