diff --git a/.github/scripts/install-playwright.sh b/.github/scripts/install-playwright.sh
new file mode 100644
index 00000000..3537c960
--- /dev/null
+++ b/.github/scripts/install-playwright.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# The Azure Ubuntu mirror can spend the entire job budget downloading WebKit
+# dependencies. Use Ubuntu's archive while retaining apt's signature checks.
+if [[ "${RUNNER_OS:-}" == "Linux" ]]; then
+ # Hosted images also use mirror+file URIs. Updating only .sources leaves
+ # that indirection free to select Azure again.
+ if [[ -f /etc/apt/apt-mirrors.txt ]]; then
+ printf '%s\n' 'https://archive.ubuntu.com/ubuntu/' |
+ sudo tee /etc/apt/apt-mirrors.txt >/dev/null
+ fi
+ for sources in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
+ if [[ -f "$sources" ]]; then
+ sudo sed -i \
+ 's|http://azure.archive.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g; s|https://azure.archive.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' \
+ "$sources"
+ fi
+ done
+fi
+
+npx playwright install --with-deps "$@"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5cd5ee35..e7b1840b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -102,6 +102,8 @@ jobs:
- suite: workers
commands: |
npm run test:node-inventory
+ npm run test:events
+ npm run test:node-expansion
npm run test:workers -- --allowOnly=false
npm run test:browser
- suite: runtime
@@ -219,7 +221,7 @@ jobs:
working-directory: demo
- run: npm run typecheck
working-directory: demo
- - run: npx playwright install --with-deps chromium webkit
+ - run: bash ../.github/scripts/install-playwright.sh chromium webkit
working-directory: demo
- name: Run ${{ matrix.suite }} contracts
run: ${{ matrix.commands }}
@@ -307,7 +309,7 @@ jobs:
working-directory: demo
- run: npm run typecheck
working-directory: demo
- - run: npx playwright install --with-deps chromium webkit
+ - run: bash ../.github/scripts/install-playwright.sh chromium webkit
working-directory: demo
- run: npm run test:d1-migration
working-directory: demo
@@ -342,7 +344,7 @@ jobs:
working-directory: demo
- run: npm run typecheck
working-directory: demo
- - run: npx playwright install --with-deps chromium webkit
+ - run: bash ../.github/scripts/install-playwright.sh chromium webkit
working-directory: demo
- run: npm run test:runtime:fallback
working-directory: demo
@@ -365,7 +367,7 @@ jobs:
cache-dependency-path: package.json
- run: npm install
- run: npm run build
- - run: npx playwright install --with-deps chromium
+ - run: bash .github/scripts/install-playwright.sh chromium
- run: npm run test:package
- name: Verify packed native plugin discovery
run: node demo/tests/scripts/prepare-native-package.mjs
@@ -396,7 +398,7 @@ jobs:
- name: Check the original Worker in workerd
run: npm run test:upstream:workerd -- --reporter=default --reporter=json --outputFile=artifacts/upstream/workerd-results.json
working-directory: demo
- - run: npx playwright install --with-deps chromium webkit
+ - run: bash ../.github/scripts/install-playwright.sh chromium webkit
working-directory: demo
- name: Build and test the browser adapter against the same expectations
run: npm run test:upstream:browser
diff --git a/.prettierignore b/.prettierignore
index da69bf4a..4422bef0 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -28,3 +28,6 @@ demo/tests/native-embedding/android/app/src/main/assets/
demo/tests/native-embedding/android/build/
demo/tests/native-embedding/android/app/build/
demo/tests/native-embedding/android/.gradle/
+
+# Generated pinned stream implementation.
+src/compat/stream-web/workers-ponyfill.js
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 751d842e..2a48a003 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -69,6 +69,20 @@ not ship it behind an experimental flag as a substitute for finishing it. Design
notes and reproducible experiments can be reviewed independently without adding
unfinished production behavior.
+### Explicit WebView events profile
+
+The approved events work targets the current WebView architecture with documented
+engine boundaries, rather than replacing the engine to seek unrestricted Workers
+equivalence. The events adapter and its process event/warning/nextTick integration
+may be accepted against the [WebView events profile](src/compat/events/README.md).
+This is a scoped exception to unrestricted whole-module equivalence above, not a
+blanket exception for new incomplete Node modules. Keep the existing process and
+async-hooks limitations visible. Require the shared behavioral contracts, public
+Vite/esbuild imports, an independently installed tarball, and iOS/Android page and
+Dedicated Worker evidence for this profile. Do not label the complete Node
+compatibility inventory or unrestricted Workers behavior **Complete** as a result.
+Publishing and release authorization remain separate.
+
### 0.1.0 Preview exception
0.1.0 Preview may include existing compatibility implementations with the limitations documented in the [compatibility inventory](docs/compatibility.md). Their inclusion does not establish complete module compatibility or certify untested behavior. Keep missing exports, unsupported operations and verification boundaries visible; a passing application scenario does not make its dependencies complete.
diff --git a/demo/package.json b/demo/package.json
index 379f3534..1788075a 100644
--- a/demo/package.json
+++ b/demo/package.json
@@ -40,6 +40,7 @@
"test:runtime:fallback:dev": "playwright test --config tests/playwright.stream-fallback-dev.config.ts",
"test:service-bindings": "node --test tests/experiments/services/integration.test.mjs",
"test:cloudflare-bindings": "node --test tests/experiments/cloudflare/integration.test.mjs",
+ "test:node-completion": "npm run test:node-inventory && node tests/experiments/node-compat/completion-report.mjs",
"test:node-boundaries": "node --test tests/experiments/node-compat/*.probe.mjs",
"test:opennext": "node --test tests/opennext/integration.test.mjs && vite build --config tests/opennext/local/vite.config.ts && node tests/opennext/local/esbuild.mjs && node --test tests/opennext/local.test.mjs",
"test:opennext:split": "vite build --config tests/opennext/split/vite.config.ts && node tests/opennext/split/esbuild.mjs && node --test --test-concurrency=1 tests/opennext/split/*.test.mjs",
@@ -47,7 +48,12 @@
"test:void:routes": "npm --prefix void run build && node --test tests/void/routes/acceptance.test.mjs",
"test:upstream:opennext:prepare": "node tests/upstream/opennext/prepare.mjs",
"test:upstream:opennext": "node --test tests/upstream/opennext/acceptance.test.mjs",
- "test:upstream:opennext:native": "node --test tests/upstream/opennext/native.test.mjs"
+ "test:upstream:opennext:native": "node --test tests/upstream/opennext/native.test.mjs",
+ "test:events": "node --test tests/experiments/node-compat/events-baseline.probe.mjs tests/experiments/node-compat/events-surface.probe.mjs tests/experiments/node-compat/events-contracts.probe.mjs",
+ "test:events:native:prepare": "node tests/experiments/node-compat/prepare-events-consumer.mjs",
+ "test:events:native": "node tests/experiments/node-compat/events-native.mjs",
+ "test:node-expansion": "node --test tests/experiments/node-compat/expansion-contracts.probe.mjs",
+ "test:node-expansion:native": "node tests/experiments/node-compat/expansion-native.mjs"
},
"dependencies": {
"@capacitor/core": "^8.5.3",
diff --git a/demo/tests/experiments/node-compat/completion-report.mjs b/demo/tests/experiments/node-compat/completion-report.mjs
new file mode 100644
index 00000000..56131e0c
--- /dev/null
+++ b/demo/tests/experiments/node-compat/completion-report.mjs
@@ -0,0 +1,270 @@
+import assert from "node:assert/strict";
+import { readFile, writeFile } from "node:fs/promises";
+import { fileURLToPath } from "node:url";
+import path from "node:path";
+
+const assessments = [
+ {
+ modules: ["events"],
+ priority: 1,
+ category: "implementation-and-host-integration",
+ next: "Public events imports implement the explicitly scoped WebView profile, including process warning delivery and nextTick scheduling. Keep native async and V8 inspection boundaries explicit; this is not unrestricted Workers or process-module certification. See the profile and generated acceptance evidence.",
+ evidence: [
+ "surface-gaps.json",
+ "events-baseline.probe.mjs",
+ "events-surface.probe.mjs",
+ "events-contracts.probe.mjs",
+ "events-native.mjs",
+ "../../../../src/compat/events/README.md",
+ "../../../../src/compat/async-hooks/README.md",
+ ],
+ },
+ {
+ modules: ["crypto", "zlib"],
+ priority: 2,
+ category: "implementation-gap",
+ next: "Inventory algorithms, key/codec classes, synchronous and streaming behavior. Evaluate JS/Wasm implementations against all pinned operations; random/hash/HKDF/scrypt, owned secret/asymmetric keys, RSA/EC/Ed25519 signatures, prime generation/checking X509Certificate parsing/fields/name checks/signature checks and Brotli/Zstd profiles now supplement dependency polyfills. Pinned Workers X509 key-check restrictions are documented. Additional key-generation options, remaining algorithms and complete codec contracts remain open. No technical-impossibility claim has been established.",
+ evidence: [
+ "surface-gaps.json",
+ "expansion-contracts.probe.mjs",
+ "expansion-native.mjs",
+ "../../../../src/compat/expansion/README.md",
+ ],
+ },
+ {
+ modules: ["fs", "fs/promises"],
+ priority: 2,
+ category: "implementation-gap",
+ next: "Compare the Workers virtual filesystem, descriptors, FileHandle, writes, permissions and watch behavior. Request-local memory-backed /tmp now supports writes, descriptors and FileHandle; permission no-ops, statfs and embedded-file copying are tested; compare remaining cross-volume operations, descriptors and stream semantics.",
+ evidence: [
+ "../../../../src/compat/fs/index.ts",
+ "../../../../src/compat/fs/promises/index.ts",
+ ],
+ },
+ {
+ modules: ["process", "console"],
+ priority: 2,
+ category: "implementation-and-host-integration",
+ next: "Classify each export as functional or a pinned Workers stub; verify scheduling, streams, events and host-specific introspection independently. Do not substitute standalone Node behavior.",
+ evidence: ["surface-gaps.json"],
+ },
+ {
+ modules: ["util", "util/types", "sys", "assert", "assert/strict"],
+ priority: 3,
+ category: "engine-boundary",
+ next: "Separate ordinary JS algorithms and sys alias routing from unobservable proxy/promise state, call sites and host brands. Closing the export list will not complete these modules or assertion diagnostics.",
+ evidence: [
+ "engine-boundaries.probe.mjs",
+ "../../../../src/compat/util/README.md",
+ ],
+ },
+ {
+ modules: ["async_hooks", "diagnostics_channel"],
+ priority: 3,
+ category: "engine-boundary",
+ next: "Test native async continuation and concurrent request isolation. Build-time lowering supports a bounded profile but changes observable Promise behavior; do not classify arbitrary async code as complete.",
+ evidence: ["engine-boundaries.probe.mjs", "async-transform.probe.mjs"],
+ },
+ {
+ modules: ["worker_threads"],
+ priority: 3,
+ category: "engine-boundary",
+ next: "Reconcile MessagePort scheduling, trusted event branding and serialization. Native and synthetic ports have different observable behavior; the current adapter does not supply Node threads.",
+ evidence: ["message-port-boundaries.probe.mjs"],
+ },
+ {
+ modules: ["stream/web"],
+ priority: 2,
+ category: "behavior-gap",
+ next: "The owned stream implementation now matches tested Workers tee cancellation reasons and pending pipeTo abort timing. Complete the remaining operation/state inventory and native integration acceptance; raw host boundary probes describe host forwarding, not this adapter.",
+ evidence: ["stream-boundaries.probe.mjs"],
+ },
+ {
+ modules: ["module"],
+ priority: 3,
+ category: "implementation-and-host-integration",
+ next: "Audit Workers module resolution and loader/stub behavior. VFS JSON loading and bundling are the current limits; separately evaluate executable resolution and engine compile-cache APIs.",
+ evidence: ["../../../../src/compat/module/index.ts", "surface-gaps.json"],
+ },
+ {
+ modules: [
+ "tls",
+ "_tls_wrap",
+ "_stream_wrap",
+ "_http_common",
+ "_http_outgoing",
+ "_http_server",
+ "http",
+ "https",
+ "net",
+ "_tls_common",
+ "_http_agent",
+ "_http_client",
+ "_http_incoming",
+ ],
+ priority: 2,
+ category: "implementation-and-native-transport",
+ next: "Direct TLS and same-connection STARTTLS exchange/hostname rejection are tested in page and Dedicated Worker on both OSes. TLS 1.2/1.3 and repeated write-half-close are covered on both OSes; custom Duplex wrapping remains unsupported. Audit remaining class/prototype/state/event contracts; these scenarios do not certify the whole TLS/server API.",
+ evidence: ["surface-gaps.json", "../../../../docs/compatibility.md"],
+ },
+ {
+ modules: ["dns", "dns/promises"],
+ priority: 2,
+ category: "behavior-gap",
+ next: "Verify Workers synthetic Service Binding/Hyperdrive addresses as well as DNS-over-HTTPS and errors; public DNS success does not cover these overrides.",
+ evidence: ["../../../../docs/compatibility.md"],
+ },
+ {
+ modules: [
+ "child_process",
+ "cluster",
+ "dgram",
+ "vm",
+ "v8",
+ "wasi",
+ "sqlite",
+ "inspector",
+ "inspector/promises",
+ "repl",
+ "tty",
+ "trace_events",
+ ],
+ priority: 3,
+ category: "workers-stub-profile",
+ next: "Audit every operation against the pinned profile, preserving actual rejection/no-op behavior. A module in this group is not necessarily wholly nonfunctional; retained functional methods and validation still require acceptance.",
+ evidence: [
+ "../../../../src/compat/README.md",
+ "engine-boundaries.probe.mjs",
+ ],
+ },
+];
+
+export function completionReport(baseline, reports) {
+ const names = Object.keys(baseline.modules).sort();
+ const covered = new Map();
+ for (const item of assessments)
+ for (const name of item.modules) {
+ assert.ok(names.includes(name), `Unknown assessment module: ${name}`);
+ assert.ok(!covered.has(name), `Duplicate assessment: ${name}`);
+ covered.set(name, item);
+ }
+ for (const [engine, report] of Object.entries(reports)) {
+ assert.deepEqual(
+ report.profile,
+ baseline.profile,
+ `${engine}: mismatched Workers profile`,
+ );
+ assert.deepEqual(
+ Object.keys(report.modules).sort(),
+ names,
+ `${engine}: incomplete inventory`,
+ );
+ }
+ assert.deepEqual(Object.keys(reports).sort(), ["chromium", "webkit"]);
+ const modules = Object.fromEntries(
+ names.map((name) => {
+ const engines = Object.fromEntries(
+ Object.entries(reports).map(([engine, report]) => {
+ const value = report.modules[name];
+ if (!value.buildError && !value.evaluationError) {
+ assert.ok(
+ value.surface &&
+ Array.isArray(value.missing) &&
+ Array.isArray(value.extra),
+ `${engine}/${name}: missing surface evidence`,
+ );
+ assert.ok(
+ value.typeMismatches && typeof value.typeMismatches === "object",
+ `${engine}/${name}: missing type audit`,
+ );
+ }
+ return [
+ engine,
+ {
+ missing: value.missing ?? [],
+ extra: value.extra ?? [],
+ typeMismatches: value.typeMismatches ?? {},
+ ...(value.buildError ? { buildError: value.buildError } : {}),
+ ...(value.evaluationError
+ ? { evaluationError: value.evaluationError }
+ : {}),
+ },
+ ];
+ }),
+ );
+ const gaps = Object.values(engines).some(
+ (value) =>
+ value.buildError ||
+ value.evaluationError ||
+ value.missing.length ||
+ value.extra.length ||
+ Object.keys(value.typeMismatches).length,
+ );
+ const { modules: ignored, ...assessment } = covered.get(name) ?? {
+ priority: 2,
+ category: "verification-gap",
+ next: "Existing contracts and matching exports do not certify a whole module. Finish operation/prototype inventory, invalid inputs, state, cleanup, build integration and iOS/Android acceptance before promotion.",
+ evidence: ["../../../../src/compat/README.md"],
+ };
+ return [
+ name,
+ {
+ status: gaps ? "surface-gaps" : "surface-aligned-not-certified",
+ ...assessment,
+ engines,
+ },
+ ];
+ }),
+ );
+ return {
+ profile: baseline.profile,
+ scope:
+ "Import/export and type audit plus implementation triage. Not whole-module behavioral or native acceptance. Priority is an investigation order, not a feasibility guarantee.",
+ summary: {
+ modules: names.length,
+ withSurfaceGaps: Object.values(modules).filter(
+ (m) => m.status === "surface-gaps",
+ ).length,
+ surfaceAligned: Object.values(modules).filter(
+ (m) => m.status === "surface-aligned-not-certified",
+ ).length,
+ certifiedCompleteByThisAudit: 0,
+ },
+ modules,
+ };
+}
+
+if (
+ process.argv[1] &&
+ path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
+) {
+ const read = async (url) => JSON.parse(await readFile(url, "utf8"));
+ const artifacts = new URL(
+ "../../../artifacts/node-inventory/",
+ import.meta.url,
+ );
+ const baseline = await read(new URL("exports.json", import.meta.url));
+ const reports = Object.fromEntries(
+ await Promise.all(
+ ["chromium", "webkit"].map(async (engine) => [
+ engine,
+ await read(new URL(`${engine}-surface.json`, artifacts)),
+ ]),
+ ),
+ );
+ const report = completionReport(baseline, reports);
+ await writeFile(
+ new URL("completion.json", artifacts),
+ JSON.stringify(report, null, 2) + "\n",
+ );
+ console.log(JSON.stringify(report.summary, null, 2));
+ console.log(
+ "Report: demo/artifacts/node-inventory/completion.json; successful generation is not compatibility acceptance.",
+ );
+ if (process.argv.includes("--require-complete")) {
+ console.error(
+ "No whole-module certification is supplied by an import/export audit.",
+ );
+ process.exitCode = 1;
+ }
+}
diff --git a/demo/tests/experiments/node-compat/completion-report.test.mjs b/demo/tests/experiments/node-compat/completion-report.test.mjs
new file mode 100644
index 00000000..0f757c59
--- /dev/null
+++ b/demo/tests/experiments/node-compat/completion-report.test.mjs
@@ -0,0 +1,78 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { readFile } from "node:fs/promises";
+import { completionReport } from "./completion-report.mjs";
+const baseline = JSON.parse(
+ await readFile(new URL("exports.json", import.meta.url), "utf8"),
+);
+function reports() {
+ return Object.fromEntries(
+ ["chromium", "webkit"].map((engine) => [
+ engine,
+ {
+ profile: structuredClone(baseline.profile),
+ modules: Object.fromEntries(
+ Object.keys(baseline.modules).map((name) => [
+ name,
+ { surface: {}, missing: [], extra: [], typeMismatches: {} },
+ ]),
+ ),
+ },
+ ]),
+ );
+}
+test("even exact export alignment cannot certify behavioral/native compatibility", () => {
+ const result = completionReport(baseline, reports());
+ assert.equal(
+ result.summary.surfaceAligned,
+ Object.keys(baseline.modules).length,
+ );
+ assert.equal(result.summary.certifiedCompleteByThisAudit, 0);
+ assert.ok(
+ Object.values(result.modules).every(
+ (module) =>
+ module.status === "surface-aligned-not-certified" &&
+ module.next &&
+ module.evidence.length,
+ ),
+ );
+ assert.equal(result.modules["fs"].category, "implementation-gap");
+ assert.equal(result.modules["stream/web"].category, "behavior-gap");
+ assert.equal(result.modules["util/types"].category, "engine-boundary");
+});
+test("one engine's missing export, import error or wrong type remains debt", () => {
+ for (const change of [
+ { missing: ["once"] },
+ { buildError: ["unresolved"] },
+ { evaluationError: { name: "TypeError", message: "broken" } },
+ { typeMismatches: { once: { expected: "function", actual: "object" } } },
+ ]) {
+ const input = reports();
+ Object.assign(input.webkit.modules.events, change);
+ const result = completionReport(baseline, input);
+ assert.equal(result.summary.withSurfaceGaps, 1);
+ assert.equal(result.modules.events.status, "surface-gaps");
+ }
+});
+test("missing engines, modules and stale profiles fail rather than disappear", () => {
+ let input = reports();
+ delete input.webkit;
+ assert.throws(() => completionReport(baseline, input));
+ input = reports();
+ delete input.webkit.modules.crypto;
+ assert.throws(
+ () => completionReport(baseline, input),
+ /incomplete inventory/,
+ );
+ input = reports();
+ input.chromium.profile.workerd = "different";
+ assert.throws(
+ () => completionReport(baseline, input),
+ /mismatched Workers profile/,
+ );
+});
+test("old reports without the type audit cannot be promoted as current evidence", () => {
+ const input = reports();
+ delete input.chromium.modules.events.typeMismatches;
+ assert.throws(() => completionReport(baseline, input), /missing type audit/);
+});
diff --git a/demo/tests/experiments/node-compat/events-baseline.probe.mjs b/demo/tests/experiments/node-compat/events-baseline.probe.mjs
new file mode 100644
index 00000000..0d06c27a
--- /dev/null
+++ b/demo/tests/experiments/node-compat/events-baseline.probe.mjs
@@ -0,0 +1,56 @@
+import {
+ observeEvents,
+ expectedEvents,
+} from "../../../../src/compat/events/observations.spec.mjs";
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { mkdir, readFile, writeFile } from "node:fs/promises";
+import { createRequire } from "node:module";
+import { Miniflare, convertV4MiniflareOptions } from "miniflare";
+
+// Pinned Workers observations for the scoped WebView events adapter.
+// In particular, do not mistake Node's native EventTarget introspection for the
+// behavior of the pinned Workers profile.
+test("pin events behavior before implementing missing exports", async () => {
+ const require = createRequire(import.meta.url);
+ const mfRequire = createRequire(require.resolve("miniflare/package.json"));
+ const version = JSON.parse(
+ await readFile(mfRequire.resolve("workerd/package.json"), "utf8"),
+ ).version;
+ assert.equal(version, "1.20261012.1");
+ const runtime = new Miniflare(
+ convertV4MiniflareOptions({
+ modules: true,
+ compatibilityDate: "2026-10-17",
+ script: `import * as events from 'node:events';
+import { AsyncLocalStorage } from 'node:async_hooks';
+export default { async fetch() { return Response.json(await (${observeEvents.toString()})(events, AsyncLocalStorage)); }};`,
+ }),
+ );
+ try {
+ const response = await runtime.dispatchFetch(
+ "https://events-baseline.invalid/",
+ );
+ assert.equal(response.status, 200);
+ const result = await response.json();
+ assert.deepEqual(result, expectedEvents);
+ const output = new URL(
+ "../../../artifacts/node-inventory/",
+ import.meta.url,
+ );
+ await mkdir(output, { recursive: true });
+ await writeFile(
+ new URL("events-baseline.json", output),
+ JSON.stringify(
+ {
+ profile: { workerd: version, compatibilityDate: "2026-10-17" },
+ result,
+ },
+ null,
+ 2,
+ ) + "\n",
+ );
+ } finally {
+ await runtime.dispose();
+ }
+});
diff --git a/demo/tests/experiments/node-compat/events-contracts.probe.mjs b/demo/tests/experiments/node-compat/events-contracts.probe.mjs
new file mode 100644
index 00000000..75fd16fb
--- /dev/null
+++ b/demo/tests/experiments/node-compat/events-contracts.probe.mjs
@@ -0,0 +1,218 @@
+import { build as viteBuild } from "vite";
+import { workersCapacitorPlugin as viteWorkers } from "../../../../dist/tooling/vite.js";
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { readFile, mkdir, writeFile } from "node:fs/promises";
+import { createRequire } from "node:module";
+import { fileURLToPath } from "node:url";
+import { build } from "esbuild";
+import { chromium, webkit } from "@playwright/test";
+import { Miniflare, convertV4MiniflareOptions } from "miniflare";
+import { workersCapacitorPlugin } from "../../../../dist/tooling/esbuild.js";
+import { eventCases } from "../../../../src/compat/events/index.spec.mjs";
+
+const root = fileURLToPath(new URL("../../../..", import.meta.url));
+const profile = JSON.parse(
+ await readFile(new URL("exports.json", import.meta.url), "utf8"),
+).profile;
+const require = createRequire(import.meta.url);
+const mfRequire = createRequire(require.resolve("miniflare/package.json"));
+assert.equal(
+ JSON.parse(await readFile(mfRequire.resolve("workerd/package.json"), "utf8"))
+ .version,
+ profile.workerd,
+);
+const runtime = new Miniflare(
+ convertV4MiniflareOptions({
+ modules: true,
+ compatibilityDate: profile.compatibilityDate,
+ script: `import * as events from 'node:events';
+import { AsyncLocalStorage } from 'node:async_hooks';
+import process from 'node:process';
+const cases = {${Object.entries(eventCases)
+ .map(([name, fn]) => `${JSON.stringify(name)}: (${fn.toString()})`)
+ .join(",")}};
+export default {async fetch(request) {
+ const name = new URL(request.url).pathname.slice(1);
+ return Response.json(await cases[name]({events, AsyncLocalStorage, process}));
+}};`,
+ }),
+);
+const baseline = {};
+try {
+ for (const name of Object.keys(eventCases)) {
+ const response = await runtime.dispatchFetch(
+ `https://events.invalid/${name}`,
+ );
+ assert.equal(response.status, 200, name);
+ baseline[name] = await response.json();
+ }
+} finally {
+ await runtime.dispose();
+}
+const output = new URL("../../../artifacts/node-inventory/", import.meta.url);
+await mkdir(output, { recursive: true });
+await writeFile(
+ new URL("events-contracts-workerd.json", output),
+ JSON.stringify({ profile, results: baseline }, null, 2) + "\n",
+);
+const compiled = await build({
+ absWorkingDir: root,
+ stdin: {
+ resolveDir: root,
+ contents: `import * as events from 'node:events';
+import { eventCases } from './src/compat/events/index.spec.mjs';
+import { AsyncLocalStorage } from 'node:async_hooks';
+import process from 'node:process';
+export const run = name => eventCases[name]({events, AsyncLocalStorage, process});`,
+ },
+ bundle: true,
+ platform: "browser",
+ format: "esm",
+ write: false,
+ outfile: "demo/artifacts/node-inventory/events-contracts.js",
+ plugins: [
+ workersCapacitorPlugin({
+ execution: "local",
+ worker: "demo/src/workers/hono/standard.ts",
+ }),
+ ],
+});
+const entry = fileURLToPath(new URL("events-vite-entry.mjs", output));
+await writeFile(
+ entry,
+ `import * as events from ${JSON.stringify("node:events")};
+import { eventCases } from ${JSON.stringify(fileURLToPath(new URL("../../../../src/compat/events/index.spec.mjs", import.meta.url)))};
+import { AsyncLocalStorage } from 'node:async_hooks';
+import process from 'node:process';
+export const run = name => eventCases[name]({events, AsyncLocalStorage, process});`,
+);
+const viteResult = await viteBuild({
+ configFile: false,
+ root,
+ logLevel: "error",
+ plugins: [
+ viteWorkers({
+ execution: "local",
+ worker: "demo/src/workers/hono/standard.ts",
+ }),
+ ],
+ build: {
+ write: false,
+ minify: false,
+ rollupOptions: {
+ input: entry,
+ preserveEntrySignatures: "strict",
+ output: { inlineDynamicImports: true },
+ },
+ },
+});
+const viteChunk = viteResult.output.find(
+ (item) => item.type === "chunk" && item.isEntry,
+);
+assert.ok(viteChunk);
+for (const [builder, source] of Object.entries({
+ esbuild: compiled.outputFiles[0].text,
+ vite: viteChunk.code,
+})) {
+ for (const [engine, launcher] of Object.entries({ chromium, webkit })) {
+ for (const mode of ["page", "worker"]) {
+ test(`${builder}/${engine}/${mode}: events contracts through public imports`, async (t) => {
+ const browser = await launcher.launch();
+ const results = {};
+ try {
+ for (const name of Object.keys(eventCases)) {
+ await t.test(name, { timeout: 15000 }, async () => {
+ const page = await browser.newPage();
+ await page.route("http://events.test/", (route) =>
+ route.fulfill({
+ contentType: "text/html",
+ body: "
Events contracts",
+ }),
+ );
+ await page.goto("http://events.test/");
+ try {
+ const result = await page.evaluate(
+ async ({ source, name, mode }) => {
+ const url = URL.createObjectURL(
+ new Blob([source], { type: "text/javascript" }),
+ );
+ try {
+ if (mode === "page") {
+ const api = await import(url);
+ return JSON.parse(JSON.stringify(await api.run(name)));
+ }
+ const workerUrl = URL.createObjectURL(
+ new Blob(
+ [
+ `import { run } from ${JSON.stringify(url)};\nself.onmessage = async ({data}) => { try { self.postMessage({value: JSON.parse(JSON.stringify(await run(data)))}); } catch (error) { self.postMessage({error: String(error)}); } };`,
+ ],
+ { type: "text/javascript" },
+ ),
+ );
+ const worker = new Worker(workerUrl, { type: "module" });
+ try {
+ return await new Promise((resolve, reject) => {
+ const timer = setTimeout(
+ () =>
+ reject(new Error("Worker operation timed out")),
+ 8000,
+ );
+ worker.onerror = (event) => {
+ clearTimeout(timer);
+ reject(new Error(event.message));
+ };
+ worker.onmessage = ({ data }) => {
+ clearTimeout(timer);
+ data.error
+ ? reject(new Error(data.error))
+ : resolve(data.value);
+ };
+ worker.postMessage(name);
+ });
+ } finally {
+ worker.terminate();
+ URL.revokeObjectURL(workerUrl);
+ }
+ } finally {
+ URL.revokeObjectURL(url);
+ }
+ },
+ { source, name, mode },
+ );
+ results[name] = result;
+ assert.deepEqual(result, baseline[name]);
+ } finally {
+ await page.close();
+ }
+ });
+ }
+ } finally {
+ await browser.close();
+ await writeFile(
+ new URL(
+ `events-contracts-${builder}-${engine}-${mode}.json`,
+ output,
+ ),
+ JSON.stringify(
+ {
+ profile,
+ builder,
+ engine,
+ mode,
+ status: "webview-profile-operation-contracts",
+ knownGaps: {
+ process:
+ "existing partial process profile; native stack inspection remains engine-dependent",
+ },
+ results,
+ },
+ null,
+ 2,
+ ) + "\n",
+ );
+ }
+ });
+ }
+ }
+}
diff --git a/demo/tests/experiments/node-compat/events-native.mjs b/demo/tests/experiments/node-compat/events-native.mjs
new file mode 100644
index 00000000..d4b0079a
--- /dev/null
+++ b/demo/tests/experiments/node-compat/events-native.mjs
@@ -0,0 +1,254 @@
+import { execFileSync } from "node:child_process";
+import assert from "node:assert/strict";
+import { readFile, mkdir, writeFile } from "node:fs/promises";
+import { fileURLToPath, pathToFileURL } from "node:url";
+import { createHash } from "node:crypto";
+import { build } from "esbuild";
+
+import { eventCases } from "../../../../src/compat/events/index.spec.mjs";
+import { nativeBackend } from "../../native/backend.mjs";
+import { runNativeAcceptance } from "../../native/acceptance.mjs";
+const [platform, device] = process.argv.slice(2);
+assert.ok(
+ ["ios", "android"].includes(platform) && device,
+ "Usage: node events-native.mjs ios|android ",
+);
+const root = fileURLToPath(new URL("../../../..", import.meta.url));
+const baseline = JSON.parse(
+ await readFile(
+ new URL(
+ "../../../artifacts/node-inventory/events-contracts-workerd.json",
+ import.meta.url,
+ ),
+ "utf8",
+ ),
+);
+const profile = JSON.parse(
+ await readFile(new URL("exports.json", import.meta.url), "utf8"),
+).profile;
+assert.deepEqual(baseline.profile, profile);
+assert.deepEqual(Object.keys(baseline.results), Object.keys(eventCases));
+const artifacts = fileURLToPath(
+ new URL(
+ `../../../artifacts/node-inventory/native-events/${platform}/`,
+ import.meta.url,
+ ),
+);
+await mkdir(artifacts, { recursive: true });
+await mkdir(`${artifacts}/public`, { recursive: true });
+await writeFile(
+ `${artifacts}/public/index.html`,
+ "Events native fixture",
+);
+await writeFile(
+ `${artifacts}/worker.ts`,
+ 'export default {fetch() {return new Response("Events fixture");}};',
+);
+await writeFile(
+ `${artifacts}/wrangler.json`,
+ JSON.stringify({
+ name: "events-native-fixture",
+ main: "worker.ts",
+ compatibility_date: profile.compatibilityDate,
+ assets: { directory: "./public", binding: "ASSETS" },
+ }),
+);
+await mkdir(`${artifacts}/local`, { recursive: true });
+await writeFile(
+ `${artifacts}/local/worker.ts`,
+ 'export default {fetch() {return new Response("Events local fixture");}};',
+);
+await writeFile(
+ `${artifacts}/local/wrangler.json`,
+ JSON.stringify({
+ name: "events-local-fixture",
+ main: "worker.ts",
+ compatibility_date: profile.compatibilityDate,
+ }),
+);
+const { consumer, tarball, tarballSha256 } = JSON.parse(
+ await readFile(
+ new URL(
+ "../../../artifacts/node-inventory/events-consumer.json",
+ import.meta.url,
+ ),
+ "utf8",
+ ),
+);
+const { workersCapacitorPlugin } = await import(
+ pathToFileURL(
+ `${consumer}/node_modules/cf-workers-native/dist/tooling/esbuild.js`,
+ ).href
+);
+const compiled = await build({
+ absWorkingDir: consumer,
+ stdin: {
+ resolveDir: consumer,
+ contents: `import * as events from 'node:events';
+import { eventCases } from ${JSON.stringify(root + "/src/compat/events/index.spec.mjs")};
+import { AsyncLocalStorage } from 'node:async_hooks';
+import process from 'node:process';
+export const run = name => eventCases[name]({events, AsyncLocalStorage, process});`,
+ },
+ bundle: true,
+ platform: "browser",
+ format: "esm",
+ write: false,
+ outfile: `${artifacts}/candidate.js`,
+ plugins: [
+ workersCapacitorPlugin({
+ execution: "local",
+ worker: `${artifacts}/local/worker.ts`,
+ workerConfig: `${artifacts}/local/wrangler.json`,
+ }),
+ ],
+});
+const source = compiled.outputFiles[0].text;
+const hash = createHash("sha256").update(source).digest("hex");
+await writeFile(`${artifacts}/candidate.js`, source);
+await writeFile(
+ `${artifacts}/inputs.json`,
+ JSON.stringify(
+ {
+ profile,
+ hash,
+ platform,
+ device,
+ tarball,
+ tarballSha256,
+ cases: Object.keys(eventCases),
+ },
+ null,
+ 2,
+ ),
+);
+// Allow emulator clock drift without disabling TLS verification.
+const tlsDirectory = `${artifacts}/tls`;
+await mkdir(tlsDirectory, { recursive: true });
+await writeFile(`${tlsDirectory}/index`, "");
+await writeFile(`${tlsDirectory}/serial`, "01\n");
+await writeFile(
+ `${tlsDirectory}/ca.cnf`,
+ `[ca]
+default_ca=local
+[local]
+database=${tlsDirectory}/index
+new_certs_dir=${tlsDirectory}
+serial=${tlsDirectory}/serial
+private_key=${tlsDirectory}/key.pem
+default_md=sha256
+policy=policy
+x509_extensions=extensions
+[policy]
+commonName=supplied
+[extensions]
+subjectAltName=DNS:localhost,IP:127.0.0.1
+basicConstraints=critical,CA:TRUE
+`,
+);
+execFileSync(
+ "openssl",
+ [
+ "req",
+ "-new",
+ "-newkey",
+ "rsa:2048",
+ "-nodes",
+ "-keyout",
+ `${tlsDirectory}/key.pem`,
+ "-out",
+ `${tlsDirectory}/request.pem`,
+ "-subj",
+ "/CN=Events native acceptance",
+ ],
+ { stdio: "pipe" },
+);
+const certificateTime = (time) =>
+ new Date(time)
+ .toISOString()
+ .replace(/[-:]/g, "")
+ .replace(/T/, "")
+ .slice(0, 14) + "Z";
+execFileSync(
+ "openssl",
+ [
+ "ca",
+ "-batch",
+ "-selfsign",
+ "-config",
+ `${tlsDirectory}/ca.cnf`,
+ "-in",
+ `${tlsDirectory}/request.pem`,
+ "-out",
+ `${tlsDirectory}/ca.pem`,
+ "-startdate",
+ certificateTime(Date.now() - 86400000),
+ "-enddate",
+ certificateTime(Date.now() + 172800000),
+ ],
+ { stdio: "pipe" },
+);
+const backend = await nativeBackend({
+ tlsDirectory,
+ dispatch: async () => new Response("Not found", { status: 404 }),
+ driver: "",
+ dispose: async () => {},
+ instrument: () => false,
+});
+const bootstrapCode = `
+async function main() {
+const source = ${JSON.stringify(source)};
+const names = ${JSON.stringify(Object.keys(eventCases))};
+const report = async value => {await fetch(${JSON.stringify(backend.url.origin + "/__acceptance/report")}, {method:'POST', mode:'no-cors', headers:{'content-type':'text/plain'}, body:JSON.stringify(value)});};
+const results = {};
+try {
+ for (const mode of ['page', 'worker']) {
+ results[mode] = {};
+ for (const name of names) {
+ const url = URL.createObjectURL(new Blob([source], {type:'text/javascript'}));
+ try {
+ if (mode === 'page') { const api = await import(/* @vite-ignore */ url); results[mode][name] = JSON.parse(JSON.stringify(await api.run(name))); }
+ else {
+ const workerURL = URL.createObjectURL(new Blob(['import {run} from ' + JSON.stringify(url) + '; self.onmessage = async ({data}) => {try {self.postMessage({value:JSON.parse(JSON.stringify(await run(data)))});} catch(error) {self.postMessage({error:String(error)});}};'], {type:'text/javascript'}));
+ const worker = new Worker(workerURL, {type:'module'});
+ try {
+ results[mode][name] = await new Promise((resolve,reject) => {
+ const timer = setTimeout(() => reject(new Error('Worker timeout: '+name)), 8000);
+ worker.onmessage = ({data}) => {clearTimeout(timer); data.error ? reject(new Error(data.error)) : resolve(data.value);};
+ worker.onerror = event => {clearTimeout(timer);reject(new Error(event.message || 'Worker error'));};
+ worker.postMessage(name);
+ });
+ } finally {worker.terminate(); URL.revokeObjectURL(workerURL);}
+ }
+ } finally {URL.revokeObjectURL(url);}
+ }
+ }
+ await report({stage:'complete', hash:${JSON.stringify(hash)}, results});
+ document.body.textContent = 'Events contracts complete';
+} catch(error) {await report({stage:'failure', message:String(error), stack:error.stack});document.body.textContent=String(error);}
+}
+main();
+`;
+await runNativeAcceptance({
+ backend,
+ artifacts,
+ platform,
+ device,
+ bootstrapCode,
+ shell: consumer,
+ appId: "jp.rdlabo.workerscapacitor.events",
+ worker: `${artifacts}/worker.ts`,
+ workerConfig: `${artifacts}/wrangler.json`,
+ validate: async (backend) => {
+ const completed = backend.reports.find(
+ (report) => report.stage === "complete",
+ );
+ assert.equal(completed.hash, hash);
+ assert.deepEqual(Object.keys(completed.results).sort(), ["page", "worker"]);
+ for (const mode of ["page", "worker"])
+ assert.deepEqual(completed.results[mode], baseline.results);
+ },
+});
+console.log(
+ `${platform}: ${Object.keys(eventCases).length} operation groups passed in page and Dedicated Worker (${hash})`,
+);
diff --git a/demo/tests/experiments/node-compat/events-surface.probe.mjs b/demo/tests/experiments/node-compat/events-surface.probe.mjs
new file mode 100644
index 00000000..6617314e
--- /dev/null
+++ b/demo/tests/experiments/node-compat/events-surface.probe.mjs
@@ -0,0 +1,115 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { readFile, mkdir, writeFile } from "node:fs/promises";
+import { fileURLToPath } from "node:url";
+import { build } from "esbuild";
+import { chromium, webkit } from "@playwright/test";
+import { workersCapacitorPlugin } from "../../../../dist/tooling/esbuild.js";
+import { lowerAsync } from "../../../../dist/tooling/async.js";
+import { expectedEvents } from "../../../../src/compat/events/observations.spec.mjs";
+
+// Public imports and selected observations for the scoped WebView profile.
+// Native acceptance and broader operation contracts have separate runners.
+const root = fileURLToPath(new URL("../../../..", import.meta.url));
+const expectedTypes = JSON.parse(
+ await readFile(new URL("export-types.json", import.meta.url), "utf8"),
+).modules.events;
+const compiled = await build({
+ absWorkingDir: root,
+ stdin: {
+ contents: `import * as events from 'node:events';
+import { observeEvents } from './src/compat/events/observations.spec.mjs';
+import { AsyncLocalStorage } from 'node:async_hooks';
+export const surface = Object.fromEntries(Object.entries(events).map(([key, value]) => [key, typeof value]));
+export const observe = () => observeEvents(events, AsyncLocalStorage);`,
+ resolveDir: root,
+ },
+ bundle: true,
+ platform: "browser",
+ format: "esm",
+ write: false,
+ outfile: "demo/artifacts/node-inventory/events-surface.js",
+ plugins: [
+ workersCapacitorPlugin({
+ execution: "local",
+ worker: "demo/src/workers/hono/standard.ts",
+ }),
+ ],
+});
+for (const [engine, launcher] of Object.entries({ chromium, webkit })) {
+ test(`${engine}: public events matches selected pinned Workers observations`, async () => {
+ const browser = await launcher.launch();
+ try {
+ const page = await browser.newPage();
+ const result = await page.evaluate(async (source) => {
+ const url = URL.createObjectURL(
+ new Blob([source], { type: "text/javascript" }),
+ );
+ try {
+ const api = await import(url);
+ return { surface: api.surface, observations: await api.observe() };
+ } finally {
+ URL.revokeObjectURL(url);
+ }
+ }, compiled.outputFiles[0].text);
+ assert.deepEqual(result.surface, expectedTypes);
+ assert.deepEqual(result.observations, expectedEvents);
+ const output = new URL(
+ "../../../artifacts/node-inventory/",
+ import.meta.url,
+ );
+ await mkdir(output, { recursive: true });
+ await writeFile(
+ new URL(`events-surface-${engine}.json`, output),
+ JSON.stringify(
+ { engine, status: "selected-observations-only", ...result },
+ null,
+ 2,
+ ) + "\n",
+ );
+ } finally {
+ await browser.close();
+ }
+ });
+}
+
+// Ordinary build lowering must preserve the native intrinsic without exemptions.
+const intrinsicSource = await readFile(
+ new URL("../../../../src/compat/events/intrinsics.ts", import.meta.url),
+ "utf8",
+);
+const loweredIntrinsic = await lowerAsync(
+ intrinsicSource,
+ "intrinsics.mjs",
+ "js",
+);
+for (const [engine, launcher] of Object.entries({ chromium, webkit })) {
+ test(`${engine}: ordinary async lowering preserves native iterator intrinsic discovery`, async () => {
+ const browser = await launcher.launch();
+ try {
+ const page = await browser.newPage();
+ const observe = (source) =>
+ page.evaluate(async (code) => {
+ const url = URL.createObjectURL(
+ new Blob([code], { type: "text/javascript" }),
+ );
+ try {
+ const api = await import(url);
+ return {
+ type: typeof api.AsyncIteratorPrototype[Symbol.asyncIterator],
+ };
+ } catch (error) {
+ return { error: error.name };
+ } finally {
+ URL.revokeObjectURL(url);
+ }
+ }, source);
+ assert.deepEqual(await observe(intrinsicSource), { type: "function" });
+ assert.deepEqual(await observe(loweredIntrinsic.code), {
+ type: "function",
+ });
+ } finally {
+ await browser.close();
+ }
+ });
+}
diff --git a/demo/tests/experiments/node-compat/expansion-contracts.probe.mjs b/demo/tests/experiments/node-compat/expansion-contracts.probe.mjs
new file mode 100644
index 00000000..66b0dbdf
--- /dev/null
+++ b/demo/tests/experiments/node-compat/expansion-contracts.probe.mjs
@@ -0,0 +1,223 @@
+import { build as viteBuild } from "vite";
+import { workersCapacitorPlugin as viteWorkers } from "../../../../dist/tooling/vite.js";
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { readFile, mkdir, writeFile } from "node:fs/promises";
+import { createRequire } from "node:module";
+import { fileURLToPath } from "node:url";
+import { build } from "esbuild";
+import { chromium, webkit } from "@playwright/test";
+import { Miniflare, convertV4MiniflareOptions } from "miniflare";
+import { workersCapacitorPlugin } from "../../../../dist/tooling/esbuild.js";
+import { eventCases } from "../../../../src/compat/expansion/index.spec.mjs";
+
+const root = fileURLToPath(new URL("../../../..", import.meta.url));
+const profile = JSON.parse(
+ await readFile(new URL("exports.json", import.meta.url), "utf8"),
+).profile;
+const require = createRequire(import.meta.url);
+const mfRequire = createRequire(require.resolve("miniflare/package.json"));
+assert.equal(
+ JSON.parse(await readFile(mfRequire.resolve("workerd/package.json"), "utf8"))
+ .version,
+ profile.workerd,
+);
+const runtime = new Miniflare(
+ convertV4MiniflareOptions({
+ modules: true,
+ compatibilityDate: profile.compatibilityDate,
+ script: `import * as tls from 'node:tls'; import * as streams from 'node:stream/web'; import * as module from 'node:module'; import * as httpCommon from 'node:_http_common'; import * as httpOutgoing from 'node:_http_outgoing'; import * as httpServer from 'node:_http_server'; import * as http from 'node:http'; import * as fs from 'node:fs'; import * as zlib from 'node:zlib'; import * as sys from 'node:sys'; import * as util from 'node:util'; import * as types from 'node:util/types'; import * as console from 'node:console'; import * as crypto from 'node:crypto';
+import { AsyncLocalStorage } from 'node:async_hooks';
+import process from 'node:process';
+const cases = {${Object.entries(eventCases)
+ .map(([name, fn]) => `${JSON.stringify(name)}: (${fn.toString()})`)
+ .join(",")}};
+export default {async fetch(request) {
+ const name = new URL(request.url).pathname.slice(1);
+ return Response.json(await cases[name]({process, tls, streams, sys, util, types, console, crypto, fs, zlib, module, httpCommon, httpOutgoing, httpServer, http}));
+}};`,
+ }),
+);
+const baseline = {};
+try {
+ for (const name of Object.keys(eventCases)) {
+ const response = await runtime.dispatchFetch(
+ `https://events.invalid/${name}`,
+ );
+ assert.equal(
+ response.status,
+ 200,
+ response.status === 200 ? name : `${name}: ${await response.text()}`,
+ );
+ baseline[name] = await response.json();
+ }
+} finally {
+ await runtime.dispose();
+}
+const output = new URL("../../../artifacts/node-inventory/", import.meta.url);
+await mkdir(output, { recursive: true });
+await writeFile(
+ new URL("expansion-contracts-workerd.json", output),
+ JSON.stringify({ profile, results: baseline }, null, 2) + "\n",
+);
+const compiled = await build({
+ absWorkingDir: root,
+ stdin: {
+ resolveDir: root,
+ contents: `import * as tls from 'node:tls'; import * as streams from 'node:stream/web'; import * as module from 'node:module'; import * as httpCommon from 'node:_http_common'; import * as httpOutgoing from 'node:_http_outgoing'; import * as httpServer from 'node:_http_server'; import * as http from 'node:http'; import * as fs from 'node:fs'; import * as zlib from 'node:zlib'; import * as sys from 'node:sys'; import * as util from 'node:util'; import * as types from 'node:util/types'; import * as console from 'node:console'; import * as crypto from 'node:crypto';
+import { eventCases } from './src/compat/expansion/index.spec.mjs';
+import { AsyncLocalStorage } from 'node:async_hooks';
+import process from 'node:process';
+export const run = name => eventCases[name]({process, tls, streams, sys, util, types, console, crypto, fs, zlib, module, httpCommon, httpOutgoing, httpServer, http});`,
+ },
+ bundle: true,
+ platform: "browser",
+ format: "esm",
+ write: false,
+ outfile: "demo/artifacts/node-inventory/expansion-contracts.js",
+ plugins: [
+ workersCapacitorPlugin({
+ execution: "local",
+ worker: "demo/src/workers/hono/standard.ts",
+ }),
+ ],
+});
+const entry = fileURLToPath(new URL("expansion-vite-entry.mjs", output));
+await writeFile(
+ entry,
+ `import * as tls from 'node:tls'; import * as streams from 'node:stream/web'; import * as module from 'node:module'; import * as httpCommon from 'node:_http_common'; import * as httpOutgoing from 'node:_http_outgoing'; import * as httpServer from 'node:_http_server'; import * as http from 'node:http'; import * as fs from 'node:fs'; import * as zlib from 'node:zlib'; import * as sys from 'node:sys'; import * as util from 'node:util'; import * as types from 'node:util/types'; import * as console from 'node:console'; import * as crypto from 'node:crypto';
+import { eventCases } from ${JSON.stringify(fileURLToPath(new URL("../../../../src/compat/expansion/index.spec.mjs", import.meta.url)))};
+import { AsyncLocalStorage } from 'node:async_hooks';
+import process from 'node:process';
+export const run = name => eventCases[name]({process, tls, streams, sys, util, types, console, crypto, fs, zlib, module, httpCommon, httpOutgoing, httpServer, http});`,
+);
+const viteResult = await viteBuild({
+ configFile: false,
+ root,
+ logLevel: "error",
+ plugins: [
+ viteWorkers({
+ execution: "local",
+ worker: "demo/src/workers/hono/standard.ts",
+ }),
+ ],
+ build: {
+ write: false,
+ minify: false,
+ rollupOptions: {
+ input: entry,
+ preserveEntrySignatures: "strict",
+ output: { inlineDynamicImports: true },
+ },
+ },
+});
+const viteChunk = viteResult.output.find(
+ (item) => item.type === "chunk" && item.isEntry,
+);
+assert.ok(viteChunk);
+await writeFile(new URL("expansion-vite.js", output), viteChunk.code);
+for (const [builder, source] of Object.entries({
+ esbuild: compiled.outputFiles[0].text,
+ vite: viteChunk.code,
+})) {
+ for (const [engine, launcher] of Object.entries({ chromium, webkit })) {
+ for (const mode of ["page", "worker"]) {
+ test(`${builder}/${engine}/${mode}: events contracts through public imports`, async (t) => {
+ const browser = await launcher.launch();
+ const results = {};
+ try {
+ for (const name of Object.keys(eventCases)) {
+ await t.test(name, { timeout: 15000 }, async () => {
+ const page = await browser.newPage();
+ await page.route("http://localhost/", (route) =>
+ route.fulfill({
+ contentType: "text/html",
+ body: "Events contracts",
+ }),
+ );
+ await page.goto("http://localhost/");
+ try {
+ const result = await page.evaluate(
+ async ({ source, name, mode }) => {
+ const url = URL.createObjectURL(
+ new Blob([source], { type: "text/javascript" }),
+ );
+ try {
+ if (mode === "page") {
+ const api = await import(url);
+ return JSON.parse(JSON.stringify(await api.run(name)));
+ }
+ const workerUrl = URL.createObjectURL(
+ new Blob(
+ [
+ `import { run } from ${JSON.stringify(url)};\nself.onmessage = async ({data}) => { try { self.postMessage({value: JSON.parse(JSON.stringify(await run(data)))}); } catch (error) { self.postMessage({error: String(error)}); } };`,
+ ],
+ { type: "text/javascript" },
+ ),
+ );
+ const worker = new Worker(workerUrl, { type: "module" });
+ try {
+ return await new Promise((resolve, reject) => {
+ const timer = setTimeout(
+ () =>
+ reject(new Error("Worker operation timed out")),
+ 8000,
+ );
+ worker.onerror = (event) => {
+ clearTimeout(timer);
+ reject(new Error(event.message));
+ };
+ worker.onmessage = ({ data }) => {
+ clearTimeout(timer);
+ data.error
+ ? reject(new Error(data.error))
+ : resolve(data.value);
+ };
+ worker.postMessage(name);
+ });
+ } finally {
+ worker.terminate();
+ URL.revokeObjectURL(workerUrl);
+ }
+ } finally {
+ URL.revokeObjectURL(url);
+ }
+ },
+ { source, name, mode },
+ );
+ results[name] = result;
+ assert.deepEqual(result, baseline[name]);
+ } finally {
+ await page.close();
+ }
+ });
+ }
+ } finally {
+ await browser.close();
+ await writeFile(
+ new URL(
+ `expansion-contracts-${builder}-${engine}-${mode}.json`,
+ output,
+ ),
+ JSON.stringify(
+ {
+ profile,
+ builder,
+ engine,
+ mode,
+ status: "webview-profile-operation-contracts",
+ knownGaps: {
+ process:
+ "existing partial process profile; native stack inspection remains engine-dependent",
+ },
+ results,
+ },
+ null,
+ 2,
+ ) + "\n",
+ );
+ }
+ });
+ }
+ }
+}
diff --git a/demo/tests/experiments/node-compat/expansion-native.mjs b/demo/tests/experiments/node-compat/expansion-native.mjs
new file mode 100644
index 00000000..49921493
--- /dev/null
+++ b/demo/tests/experiments/node-compat/expansion-native.mjs
@@ -0,0 +1,469 @@
+import { createServer as tcpServer } from "node:net";
+import { TLSSocket, createSecureContext } from "node:tls";
+import { execFileSync } from "node:child_process";
+import assert from "node:assert/strict";
+import { readFile, mkdir, writeFile } from "node:fs/promises";
+import { fileURLToPath, pathToFileURL } from "node:url";
+import { createHash } from "node:crypto";
+import { build } from "esbuild";
+
+import { eventCases } from "../../../../src/compat/expansion/index.spec.mjs";
+import { nativeBackend } from "../../native/backend.mjs";
+import { runNativeAcceptance } from "../../native/acceptance.mjs";
+const [platform, device] = process.argv.slice(2);
+assert.ok(
+ ["ios", "android"].includes(platform) && device,
+ "Usage: node expansion-native.mjs ios|android ",
+);
+const root = fileURLToPath(new URL("../../../..", import.meta.url));
+const baseline = JSON.parse(
+ await readFile(
+ new URL(
+ "../../../artifacts/node-inventory/expansion-contracts-workerd.json",
+ import.meta.url,
+ ),
+ "utf8",
+ ),
+);
+const profile = JSON.parse(
+ await readFile(new URL("exports.json", import.meta.url), "utf8"),
+).profile;
+assert.deepEqual(baseline.profile, profile);
+assert.deepEqual(Object.keys(baseline.results), Object.keys(eventCases));
+const artifacts = fileURLToPath(
+ new URL(
+ `../../../artifacts/node-inventory/native-expansion/${platform}/`,
+ import.meta.url,
+ ),
+);
+await mkdir(artifacts, { recursive: true });
+await mkdir(`${artifacts}/public`, { recursive: true });
+await writeFile(
+ `${artifacts}/public/index.html`,
+ "Compatibility expansion fixture",
+);
+await writeFile(
+ `${artifacts}/worker.ts`,
+ 'export default {fetch() {return new Response("Compatibility fixture");}};',
+);
+await writeFile(
+ `${artifacts}/wrangler.json`,
+ JSON.stringify({
+ name: "events-native-fixture",
+ main: "worker.ts",
+ compatibility_date: profile.compatibilityDate,
+ assets: { directory: "./public", binding: "ASSETS" },
+ }),
+);
+await mkdir(`${artifacts}/local`, { recursive: true });
+await writeFile(
+ `${artifacts}/local/worker.ts`,
+ 'export default {fetch() {return new Response("Events local fixture");}};',
+);
+await writeFile(
+ `${artifacts}/local/wrangler.json`,
+ JSON.stringify({
+ name: "events-local-fixture",
+ main: "worker.ts",
+ compatibility_date: profile.compatibilityDate,
+ }),
+);
+const { consumer, tarball, tarballSha256 } = JSON.parse(
+ await readFile(
+ new URL(
+ "../../../artifacts/node-inventory/events-consumer.json",
+ import.meta.url,
+ ),
+ "utf8",
+ ),
+);
+const { workersCapacitorPlugin } = await import(
+ pathToFileURL(
+ `${consumer}/node_modules/cf-workers-native/dist/tooling/esbuild.js`,
+ ).href
+);
+const compiled = await build({
+ absWorkingDir: consumer,
+ stdin: {
+ resolveDir: consumer,
+ contents: `import * as net from 'node:net'; import * as tls from 'node:tls'; import * as streams from 'node:stream/web'; import * as module from 'node:module'; import * as httpCommon from 'node:_http_common'; import * as httpOutgoing from 'node:_http_outgoing'; import * as httpServer from 'node:_http_server'; import * as http from 'node:http'; import * as fs from 'node:fs'; import * as zlib from 'node:zlib'; import * as sys from 'node:sys'; import * as util from 'node:util'; import * as types from 'node:util/types'; import * as console from 'node:console'; import * as crypto from 'node:crypto';
+import { eventCases } from ${JSON.stringify(root + "/src/compat/expansion/index.spec.mjs")};
+import { AsyncLocalStorage } from 'node:async_hooks';
+import process from 'node:process';
+export const run = name => eventCases[name]({process, tls, streams, sys, util, types, console, crypto, fs, zlib, module, httpCommon, httpOutgoing, httpServer, http});
+export {createNetworkRelay, initializeWorkerNetwork} from ${JSON.stringify(consumer + "/node_modules/cf-workers-native/dist/browser/fetch/relay.js")};
+export const runTls = async ({port,upgradePort}) => {
+ const attempt = (servername,halfClose=false) => new Promise((resolve,reject)=> {
+ let accepted=false, body='';
+ const socket=tls.connect({host:'127.0.0.1',port,servername});
+ const timer=setTimeout(()=>{socket.destroy();reject(new Error('TLS timeout: '+servername));},8000);
+ socket.on('secureConnect',()=>{accepted=socket.authorized && socket.encrypted;socket[halfClose?'end':'write']('GET /tls-native HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n');});
+ socket.on('data',chunk=>{body+=chunk.toString();});
+ socket.on('error',()=>{clearTimeout(timer);resolve({accepted,received:false});});
+ socket.on('end',()=>{clearTimeout(timer);resolve({accepted,received:body.includes('404') && body.includes('Not found')});});
+ });
+ const valid=await attempt('localhost'), directEnd=await attempt('localhost',true), invalid=await attempt('wrong.invalid');
+
+ const upgrade = (servername,halfClose=false,legacy=false) => new Promise((resolve,reject)=>{
+ const plain=net.connect({host:'127.0.0.1',port:upgradePort});
+ let socket=plain, accepted=false, body='', greeting='';
+ const timer=setTimeout(()=>{socket.destroy();plain.destroy();reject(new Error('Upgrade timeout: '+servername));},8000);
+ plain.on('error',()=>{});
+ plain.on('connect',()=>plain.write(legacy==='untrusted'?'UNTRUSTED\\r\\n':legacy?'UPGRADE12\\r\\n':'UPGRADE\\r\\n'));
+ const onGreeting=chunk=>{
+ greeting+=chunk.toString();if(!greeting.endsWith('\\r\\n'))return;
+ plain.off('data',onGreeting);
+ if(greeting!=='READY\\r\\n'){clearTimeout(timer);plain.destroy();reject(new Error('Invalid upgrade greeting'));return;}
+ socket=tls.connect({socket:plain,servername});
+ socket.on('secureConnect',()=>{accepted=socket.authorized&&socket.encrypted;});
+ socket.on('data',chunk=>{body+=chunk.toString();});
+ socket.on('end',()=>{clearTimeout(timer);socket.destroy();plain.destroy();resolve({accepted,received:body==='encrypted-response'});});
+ socket.on('error',()=>{clearTimeout(timer);plain.destroy();resolve({accepted,received:false});});
+ if(halfClose)socket.end('encrypted-request');else socket.write('encrypted-request');
+ };
+ plain.on('data',onGreeting);
+ });
+ const upgradeEnd=[];for(let i=0;i<3;i++)upgradeEnd.push(await upgrade('localhost',true));
+ return {valid,directEnd,invalid,upgrade:await upgrade('localhost'),upgradeEnd,upgradeTls12:await upgrade('localhost',true,true),upgradeUntrusted:await upgrade('localhost',false,'untrusted'),upgradeInvalid:await upgrade('wrong.invalid')};
+};`,
+ },
+ bundle: true,
+ platform: "browser",
+ format: "esm",
+ write: false,
+ outfile: `${artifacts}/candidate.js`,
+ plugins: [
+ workersCapacitorPlugin({
+ execution: "local",
+ worker: `${artifacts}/local/worker.ts`,
+ workerConfig: `${artifacts}/local/wrangler.json`,
+ }),
+ ],
+});
+const source = compiled.outputFiles[0].text;
+const hash = createHash("sha256").update(source).digest("hex");
+await writeFile(`${artifacts}/candidate.js`, source);
+await writeFile(
+ `${artifacts}/inputs.json`,
+ JSON.stringify(
+ {
+ profile,
+ hash,
+ platform,
+ device,
+ tarball,
+ tarballSha256,
+ cases: Object.keys(eventCases),
+ },
+ null,
+ 2,
+ ),
+);
+// Allow emulator clock drift without disabling TLS verification.
+const tlsDirectory = `${artifacts}/tls`;
+await mkdir(tlsDirectory, { recursive: true });
+await writeFile(`${tlsDirectory}/index`, "");
+await writeFile(`${tlsDirectory}/serial`, "01\n");
+await writeFile(
+ `${tlsDirectory}/ca.cnf`,
+ `[ca]
+default_ca=local
+[local]
+database=${tlsDirectory}/index
+new_certs_dir=${tlsDirectory}
+serial=${tlsDirectory}/serial
+private_key=${tlsDirectory}/key.pem
+default_md=sha256
+policy=policy
+x509_extensions=extensions
+[policy]
+commonName=supplied
+[extensions]
+subjectAltName=DNS:localhost,IP:127.0.0.1
+basicConstraints=critical,CA:TRUE
+`,
+);
+execFileSync(
+ "openssl",
+ [
+ "req",
+ "-new",
+ "-newkey",
+ "rsa:2048",
+ "-nodes",
+ "-keyout",
+ `${tlsDirectory}/key.pem`,
+ "-out",
+ `${tlsDirectory}/request.pem`,
+ "-subj",
+ "/CN=Compatibility expansion acceptance",
+ ],
+ { stdio: "pipe" },
+);
+const certificateTime = (time) =>
+ new Date(time)
+ .toISOString()
+ .replace(/[-:]/g, "")
+ .replace(/T/, "")
+ .slice(0, 14) + "Z";
+execFileSync(
+ "openssl",
+ [
+ "ca",
+ "-batch",
+ "-selfsign",
+ "-config",
+ `${tlsDirectory}/ca.cnf`,
+ "-in",
+ `${tlsDirectory}/request.pem`,
+ "-out",
+ `${tlsDirectory}/ca.pem`,
+ "-startdate",
+ certificateTime(Date.now() - 86400000),
+ "-enddate",
+ certificateTime(Date.now() + 172800000),
+ ],
+ { stdio: "pipe" },
+);
+// A separate, currently valid self-signed chain is deliberately not installed
+// in either OS trust store. Hostname-only validation must not accept it.
+await writeFile(
+ `${tlsDirectory}/untrusted.cnf`,
+ (await readFile(`${tlsDirectory}/ca.cnf`, "utf8")).replace(
+ `${tlsDirectory}/key.pem`,
+ `${tlsDirectory}/untrusted-key.pem`,
+ ),
+);
+execFileSync(
+ "openssl",
+ [
+ "req",
+ "-new",
+ "-newkey",
+ "rsa:2048",
+ "-nodes",
+ "-keyout",
+ `${tlsDirectory}/untrusted-key.pem`,
+ "-out",
+ `${tlsDirectory}/untrusted-request.pem`,
+ "-subj",
+ "/CN=Untrusted compatibility fixture",
+ ],
+ { stdio: "pipe" },
+);
+execFileSync(
+ "openssl",
+ [
+ "ca",
+ "-batch",
+ "-selfsign",
+ "-config",
+ `${tlsDirectory}/untrusted.cnf`,
+ "-in",
+ `${tlsDirectory}/untrusted-request.pem`,
+ "-out",
+ `${tlsDirectory}/untrusted.pem`,
+ "-startdate",
+ certificateTime(Date.now() - 86400000),
+ "-enddate",
+ certificateTime(Date.now() + 172800000),
+ ],
+ { stdio: "pipe" },
+);
+const untrustedContext = createSecureContext({
+ key: await readFile(`${tlsDirectory}/untrusted-key.pem`),
+ cert: await readFile(`${tlsDirectory}/untrusted.pem`),
+});
+const upgradeStats = { connections: 0, requests: 0, unexpected: 0 };
+const upgradeProtocols = [];
+const upgradeSockets = new Set();
+const context = createSecureContext({
+ key: await readFile(`${tlsDirectory}/key.pem`),
+ cert: await readFile(`${tlsDirectory}/ca.pem`),
+});
+const context12 = createSecureContext({
+ key: await readFile(`${tlsDirectory}/key.pem`),
+ cert: await readFile(`${tlsDirectory}/ca.pem`),
+ maxVersion: "TLSv1.2",
+});
+const upgradeServer = tcpServer((socket) => {
+ upgradeStats.connections++;
+ upgradeSockets.add(socket);
+ socket.on("close", () => upgradeSockets.delete(socket));
+ socket.on("error", () => {});
+ let greeting = "";
+ const begin = (chunk) => {
+ greeting += chunk.toString();
+ if (!greeting.endsWith("\r\n")) return;
+ socket.off("data", begin);
+ if (!["UPGRADE\r\n", "UPGRADE12\r\n", "UNTRUSTED\r\n"].includes(greeting)) {
+ upgradeStats.unexpected++;
+ socket.destroy();
+ return;
+ }
+ socket.write("READY\r\n", () => {
+ const secure = new TLSSocket(socket, {
+ isServer: true,
+ secureContext:
+ greeting === "UNTRUSTED\r\n"
+ ? untrustedContext
+ : greeting === "UPGRADE12\r\n"
+ ? context12
+ : context,
+ });
+ secure.on("error", () => {});
+ let body = "";
+ secure.on("data", (chunk) => {
+ body += chunk.toString();
+ if (body === "encrypted-request") {
+ upgradeStats.requests++;
+ upgradeProtocols.push(secure.getProtocol());
+ secure.end("encrypted-response");
+ } else if (!"encrypted-request".startsWith(body)) {
+ upgradeStats.unexpected++;
+ secure.destroy();
+ }
+ });
+ });
+ };
+ socket.on("data", begin);
+});
+await new Promise((resolve) => upgradeServer.listen(0, "127.0.0.1", resolve));
+const tlsPorts = { port: 0, upgradePort: upgradeServer.address().port };
+if (platform === "android")
+ execFileSync("adb", [
+ "-s",
+ device,
+ "reverse",
+ `tcp:${tlsPorts.upgradePort}`,
+ `tcp:${tlsPorts.upgradePort}`,
+ ]);
+const backend = await nativeBackend({
+ tlsDirectory,
+ dispatch: async () => new Response("Not found", { status: 404 }),
+ driver: "",
+ dispose: async () => {
+ if (platform === "android")
+ execFileSync("adb", [
+ "-s",
+ device,
+ "reverse",
+ "--remove",
+ `tcp:${tlsPorts.upgradePort}`,
+ ]);
+ for (const socket of upgradeSockets) socket.destroy();
+ await new Promise((resolve) => upgradeServer.close(resolve));
+ },
+ instrument: () => false,
+});
+tlsPorts.port = Number(backend.url.port);
+const bootstrapCode = `
+async function main() {
+const source = ${JSON.stringify(source)};
+const names = ${JSON.stringify(Object.keys(eventCases))};
+const report = async value => {await fetch(${JSON.stringify(backend.url.origin + "/__acceptance/report")}, {method:'POST', mode:'no-cors', headers:{'content-type':'text/plain'}, body:JSON.stringify(value)});};
+const results = {};
+try {
+ for (const mode of ['page', 'worker']) {
+ results[mode] = {};
+ for (const name of names) {
+ const url = URL.createObjectURL(new Blob([source], {type:'text/javascript'}));
+ try {
+ if (mode === 'page') { const api = await import(/* @vite-ignore */ url); results[mode][name] = JSON.parse(JSON.stringify(await api.run(name))); }
+ else {
+ const workerURL = URL.createObjectURL(new Blob(['import {run} from ' + JSON.stringify(url) + '; self.onmessage = async ({data}) => {try {self.postMessage({value:JSON.parse(JSON.stringify(await run(data)))});} catch(error) {self.postMessage({error:String(error)});}};'], {type:'text/javascript'}));
+ const worker = new Worker(workerURL, {type:'module'});
+ try {
+ results[mode][name] = await new Promise((resolve,reject) => {
+ const timer = setTimeout(() => reject(new Error('Worker timeout: '+name)), 8000);
+ worker.onmessage = ({data}) => {clearTimeout(timer); data.error ? reject(new Error(data.error)) : resolve(data.value);};
+ worker.onerror = event => {clearTimeout(timer);reject(new Error(event.message || 'Worker error'));};
+ worker.postMessage(name);
+ });
+ } finally {worker.terminate(); URL.revokeObjectURL(workerURL);}
+ }
+ } finally {URL.revokeObjectURL(url);}
+ }
+ }
+ const tlsURL=URL.createObjectURL(new Blob([source],{type:'text/javascript'}));
+ const tlsApi=await import(/* @vite-ignore */ tlsURL);
+ const nativeTls={page:await tlsApi.runTls(${JSON.stringify(tlsPorts)})};
+ const workerURL=URL.createObjectURL(new Blob(['import {runTls, initializeWorkerNetwork} from '+JSON.stringify(tlsURL)+'; self.onmessage=async ({data})=>{try {await initializeWorkerNetwork();self.postMessage({tls:await runTls(data)});}catch(e){self.postMessage({error:String(e)});}};'],{type:'text/javascript'}));
+ const tlsWorker=new Worker(workerURL,{type:'module'});
+ const relay=tlsApi.createNetworkRelay(tlsWorker);
+ tlsWorker.addEventListener('message',relay.receive);
+ try {nativeTls.worker=await new Promise((resolve,reject)=>{
+ const timer=setTimeout(()=>reject(new Error('Worker TLS timeout')),20000);
+ tlsWorker.onmessage=({data})=>{if(data.tls || data.error){clearTimeout(timer);data.error?reject(new Error(data.error)):resolve(data.tls);}};
+ tlsWorker.postMessage(${JSON.stringify(tlsPorts)});
+ });} finally {tlsWorker.terminate();await relay.dispose();URL.revokeObjectURL(workerURL);URL.revokeObjectURL(tlsURL);}
+ await report({stage:'complete', nativeTls, hash:${JSON.stringify(hash)}, results});
+ document.body.textContent = 'Compatibility contracts complete';
+} catch(error) {await report({stage:'failure', message:String(error), stack:error.stack});document.body.textContent=String(error);}
+}
+main();
+`;
+await runNativeAcceptance({
+ backend,
+ artifacts,
+ platform,
+ device,
+ bootstrapCode,
+ shell: consumer,
+ appId: "jp.rdlabo.workerscapacitor.events",
+ worker: `${artifacts}/worker.ts`,
+ workerConfig: `${artifacts}/wrangler.json`,
+ validate: async (backend) => {
+ const completed = backend.reports.find(
+ (report) => report.stage === "complete",
+ );
+ assert.equal(completed.hash, hash);
+ for (const mode of ["page", "worker"])
+ assert.deepEqual(completed.nativeTls[mode], {
+ valid: { accepted: true, received: true },
+ directEnd: { accepted: true, received: true },
+ invalid: { accepted: false, received: false },
+ upgrade: { accepted: true, received: true },
+ upgradeEnd: Array.from({ length: 3 }, () => ({
+ accepted: true,
+ received: true,
+ })),
+ upgradeTls12: { accepted: true, received: true },
+ upgradeUntrusted: { accepted: false, received: false },
+ upgradeInvalid: { accepted: false, received: false },
+ });
+ assert.deepEqual(upgradeStats, {
+ connections: 14,
+ requests: 10,
+ unexpected: 0,
+ });
+ assert.deepEqual(Object.keys(completed.results).sort(), ["page", "worker"]);
+ for (const mode of ["page", "worker"])
+ assert.deepEqual(completed.results[mode], baseline.results);
+ assert.equal(
+ upgradeProtocols.filter((protocol) => protocol === "TLSv1.3").length,
+ 8,
+ );
+ assert.equal(
+ upgradeProtocols.filter((protocol) => protocol === "TLSv1.2").length,
+ 2,
+ );
+ await writeFile(
+ `${artifacts}/tls-upgrade-evidence.json`,
+ JSON.stringify(
+ {
+ hash,
+ platform,
+ stats: upgradeStats,
+ protocols: upgradeProtocols,
+ results: completed.nativeTls,
+ },
+ null,
+ 2,
+ ) + "\n",
+ );
+ },
+});
+console.log(
+ `${platform}: ${Object.keys(eventCases).length} operation groups passed in page and Dedicated Worker (${hash})`,
+);
diff --git a/demo/tests/experiments/node-compat/export-types.json b/demo/tests/experiments/node-compat/export-types.json
new file mode 100644
index 00000000..185a3440
--- /dev/null
+++ b/demo/tests/experiments/node-compat/export-types.json
@@ -0,0 +1,1746 @@
+{
+ "profile": {
+ "compatibilityDate": "2026-10-17",
+ "workerd": "1.20261012.1"
+ },
+ "sourceRevision": "5358e3d337336eaf696a48bc1563d808b10feba5",
+ "modules": {
+ "_http_agent": {
+ "Agent": "function",
+ "default": "object",
+ "globalAgent": "object"
+ },
+ "_http_client": {
+ "ClientRequest": "function",
+ "default": "object"
+ },
+ "_http_common": {
+ "CRLF": "string",
+ "_checkInvalidHeaderChar": "function",
+ "_checkIsHttpToken": "function",
+ "chunkExpression": "object",
+ "continueExpression": "object",
+ "default": "object",
+ "kIncomingMessage": "symbol",
+ "methods": "object",
+ "parsers": "object"
+ },
+ "_http_incoming": {
+ "IncomingMessage": "function",
+ "default": "object"
+ },
+ "_http_outgoing": {
+ "OutgoingMessage": "function",
+ "default": "object",
+ "kHighWaterMark": "symbol",
+ "kUniqueHeaders": "symbol",
+ "parseUniqueHeadersOption": "function",
+ "validateHeaderName": "function",
+ "validateHeaderValue": "function"
+ },
+ "_http_server": {
+ "STATUS_CODES": "object",
+ "Server": "function",
+ "ServerResponse": "function",
+ "_connectionListener": "function",
+ "default": "object",
+ "httpServerPreClose": "function",
+ "kConnectionsCheckingInterval": "symbol",
+ "kServerResponse": "symbol",
+ "setupConnectionsTracking": "function",
+ "storeHTTPOptions": "function"
+ },
+ "_stream_duplex": {
+ "Duplex": "function",
+ "default": "function",
+ "from": "function",
+ "fromWeb": "function",
+ "toWeb": "function"
+ },
+ "_stream_passthrough": {
+ "PassThrough": "function",
+ "default": "function"
+ },
+ "_stream_readable": {
+ "Readable": "function",
+ "ReadableState": "function",
+ "default": "function",
+ "from": "function",
+ "fromWeb": "function",
+ "toWeb": "function",
+ "wrap": "function"
+ },
+ "_stream_transform": {
+ "Transform": "function",
+ "default": "function"
+ },
+ "_stream_wrap": {
+ "default": "function"
+ },
+ "_stream_writable": {
+ "Writable": "function",
+ "WritableState": "function",
+ "default": "function",
+ "fromWeb": "function",
+ "toWeb": "function"
+ },
+ "_tls_common": {
+ "SecureContext": "function",
+ "createSecureContext": "function",
+ "default": "object",
+ "translatePeerCertificate": "function"
+ },
+ "_tls_wrap": {
+ "TLSSocket": "function",
+ "connect": "function",
+ "default": "object"
+ },
+ "assert": {
+ "AssertionError": "function",
+ "deepEqual": "function",
+ "deepStrictEqual": "function",
+ "default": "function",
+ "doesNotMatch": "function",
+ "doesNotReject": "function",
+ "doesNotThrow": "function",
+ "equal": "function",
+ "fail": "function",
+ "ifError": "function",
+ "match": "function",
+ "notDeepEqual": "function",
+ "notDeepStrictEqual": "function",
+ "notEqual": "function",
+ "notStrictEqual": "function",
+ "ok": "function",
+ "partialDeepStrictEqual": "function",
+ "rejects": "function",
+ "strict": "function",
+ "strictEqual": "function",
+ "throws": "function"
+ },
+ "assert/strict": {
+ "AssertionError": "function",
+ "deepEqual": "function",
+ "deepStrictEqual": "function",
+ "default": "function",
+ "doesNotMatch": "function",
+ "doesNotReject": "function",
+ "doesNotThrow": "function",
+ "equal": "function",
+ "fail": "function",
+ "ifError": "function",
+ "match": "function",
+ "notDeepEqual": "function",
+ "notDeepStrictEqual": "function",
+ "notEqual": "function",
+ "notStrictEqual": "function",
+ "ok": "function",
+ "partialDeepStrictEqual": "function",
+ "rejects": "function",
+ "strict": "function",
+ "strictEqual": "function",
+ "throws": "function"
+ },
+ "async_hooks": {
+ "AsyncLocalStorage": "function",
+ "AsyncResource": "function",
+ "asyncWrapProviders": "object",
+ "createHook": "function",
+ "default": "object",
+ "executionAsyncId": "function",
+ "executionAsyncResource": "function",
+ "triggerAsyncId": "function"
+ },
+ "buffer": {
+ "Blob": "function",
+ "Buffer": "function",
+ "File": "function",
+ "INSPECT_MAX_BYTES": "number",
+ "SlowBuffer": "function",
+ "atob": "function",
+ "btoa": "function",
+ "constants": "object",
+ "default": "object",
+ "isAscii": "function",
+ "isUtf8": "function",
+ "kMaxLength": "number",
+ "kStringMaxLength": "number",
+ "resolveObjectURL": "function",
+ "transcode": "function"
+ },
+ "child_process": {
+ "ChildProcess": "function",
+ "_forkChild": "function",
+ "default": "object",
+ "exec": "function",
+ "execFile": "function",
+ "execFileSync": "function",
+ "execSync": "function",
+ "fork": "function",
+ "spawn": "function",
+ "spawnSync": "function"
+ },
+ "cluster": {
+ "Cluster": "function",
+ "SCHED_NONE": "number",
+ "SCHED_RR": "number",
+ "Worker": "function",
+ "_events": "object",
+ "_eventsCount": "number",
+ "_maxListeners": "number",
+ "default": "object",
+ "disconnect": "function",
+ "fork": "function",
+ "isMaster": "boolean",
+ "isPrimary": "boolean",
+ "isWorker": "boolean",
+ "schedulingPolicy": "number",
+ "settings": "object",
+ "setupMaster": "function",
+ "setupPrimary": "function",
+ "workers": "object"
+ },
+ "console": {
+ "Console": "function",
+ "assert": "function",
+ "clear": "function",
+ "context": "function",
+ "count": "function",
+ "countReset": "function",
+ "createTask": "function",
+ "debug": "function",
+ "default": "object",
+ "dir": "function",
+ "dirxml": "function",
+ "error": "function",
+ "group": "function",
+ "groupCollapsed": "function",
+ "groupEnd": "function",
+ "info": "function",
+ "log": "function",
+ "profile": "function",
+ "profileEnd": "function",
+ "table": "function",
+ "time": "function",
+ "timeEnd": "function",
+ "timeLog": "function",
+ "timeStamp": "function",
+ "trace": "function",
+ "warn": "function"
+ },
+ "constants": {
+ "COPYFILE_EXCL": "number",
+ "COPYFILE_FICLONE": "number",
+ "COPYFILE_FICLONE_FORCE": "number",
+ "DH_CHECK_P_NOT_PRIME": "number",
+ "DH_CHECK_P_NOT_SAFE_PRIME": "number",
+ "DH_NOT_SUITABLE_GENERATOR": "number",
+ "DH_UNABLE_TO_CHECK_GENERATOR": "number",
+ "E2BIG": "number",
+ "EACCES": "number",
+ "EADDRINUSE": "number",
+ "EADDRNOTAVAIL": "number",
+ "EAFNOSUPPORT": "number",
+ "EAGAIN": "number",
+ "EALREADY": "number",
+ "EBADF": "number",
+ "EBADMSG": "number",
+ "EBUSY": "number",
+ "ECANCELED": "number",
+ "ECHILD": "number",
+ "ECONNABORTED": "number",
+ "ECONNREFUSED": "number",
+ "ECONNRESET": "number",
+ "EDEADLK": "number",
+ "EDESTADDRREQ": "number",
+ "EDOM": "number",
+ "EDQUOT": "number",
+ "EEXIST": "number",
+ "EFAULT": "number",
+ "EFBIG": "number",
+ "EHOSTUNREACH": "number",
+ "EIDRM": "number",
+ "EILSEQ": "number",
+ "EINPROGRESS": "number",
+ "EINTR": "number",
+ "EINVAL": "number",
+ "EIO": "number",
+ "EISCONN": "number",
+ "EISDIR": "number",
+ "ELOOP": "number",
+ "EMFILE": "number",
+ "EMLINK": "number",
+ "EMSGSIZE": "number",
+ "EMULTIHOP": "number",
+ "ENAMETOOLONG": "number",
+ "ENETDOWN": "number",
+ "ENETRESET": "number",
+ "ENETUNREACH": "number",
+ "ENFILE": "number",
+ "ENGINE_METHOD_ALL": "number",
+ "ENGINE_METHOD_CIPHERS": "number",
+ "ENGINE_METHOD_DH": "number",
+ "ENGINE_METHOD_DIGESTS": "number",
+ "ENGINE_METHOD_DSA": "number",
+ "ENGINE_METHOD_EC": "number",
+ "ENGINE_METHOD_NONE": "number",
+ "ENGINE_METHOD_PKEY_ASN1_METHS": "number",
+ "ENGINE_METHOD_PKEY_METHS": "number",
+ "ENGINE_METHOD_RAND": "number",
+ "ENGINE_METHOD_RSA": "number",
+ "ENOBUFS": "number",
+ "ENODATA": "number",
+ "ENODEV": "number",
+ "ENOENT": "number",
+ "ENOEXEC": "number",
+ "ENOLCK": "number",
+ "ENOLINK": "number",
+ "ENOMEM": "number",
+ "ENOMSG": "number",
+ "ENOPROTOOPT": "number",
+ "ENOSPC": "number",
+ "ENOSR": "number",
+ "ENOSTR": "number",
+ "ENOSYS": "number",
+ "ENOTCONN": "number",
+ "ENOTDIR": "number",
+ "ENOTEMPTY": "number",
+ "ENOTSOCK": "number",
+ "ENOTSUP": "number",
+ "ENOTTY": "number",
+ "ENXIO": "number",
+ "EOPNOTSUPP": "number",
+ "EOVERFLOW": "number",
+ "EPERM": "number",
+ "EPIPE": "number",
+ "EPROTO": "number",
+ "EPROTONOSUPPORT": "number",
+ "EPROTOTYPE": "number",
+ "ERANGE": "number",
+ "EROFS": "number",
+ "ESPIPE": "number",
+ "ESRCH": "number",
+ "ESTALE": "number",
+ "ETIME": "number",
+ "ETIMEDOUT": "number",
+ "ETXTBSY": "number",
+ "EWOULDBLOCK": "number",
+ "EXDEV": "number",
+ "EXTENSIONLESS_FORMAT_JAVASCRIPT": "number",
+ "EXTENSIONLESS_FORMAT_WASM": "number",
+ "F_OK": "number",
+ "OPENSSL_VERSION_NUMBER": "number",
+ "O_APPEND": "number",
+ "O_CREAT": "number",
+ "O_DIRECT": "number",
+ "O_DIRECTORY": "number",
+ "O_DSYNC": "number",
+ "O_EXCL": "number",
+ "O_NOATIME": "number",
+ "O_NOCTTY": "number",
+ "O_NOFOLLOW": "number",
+ "O_NONBLOCK": "number",
+ "O_RDONLY": "number",
+ "O_RDWR": "number",
+ "O_SYMLINK": "number",
+ "O_SYNC": "number",
+ "O_TRUNC": "number",
+ "O_WRONLY": "number",
+ "POINT_CONVERSION_COMPRESSED": "number",
+ "POINT_CONVERSION_HYBRID": "number",
+ "POINT_CONVERSION_UNCOMPRESSED": "number",
+ "PRIORITY_ABOVE_NORMAL": "number",
+ "PRIORITY_BELOW_NORMAL": "number",
+ "PRIORITY_HIGH": "number",
+ "PRIORITY_HIGHEST": "number",
+ "PRIORITY_LOW": "number",
+ "PRIORITY_NORMAL": "number",
+ "RSA_NO_PADDING": "number",
+ "RSA_PKCS1_OAEP_PADDING": "number",
+ "RSA_PKCS1_PADDING": "number",
+ "RSA_PKCS1_PSS_PADDING": "number",
+ "RSA_PSS_SALTLEN_AUTO": "number",
+ "RSA_PSS_SALTLEN_DIGEST": "number",
+ "RSA_PSS_SALTLEN_MAX_SIGN": "number",
+ "RSA_X931_PADDING": "number",
+ "RTLD_DEEPBIND": "number",
+ "RTLD_GLOBAL": "number",
+ "RTLD_LAZY": "number",
+ "RTLD_LOCAL": "number",
+ "RTLD_NOW": "number",
+ "R_OK": "number",
+ "SIGABRT": "number",
+ "SIGALRM": "number",
+ "SIGBUS": "number",
+ "SIGCHLD": "number",
+ "SIGCONT": "number",
+ "SIGFPE": "number",
+ "SIGHUP": "number",
+ "SIGILL": "number",
+ "SIGINFO": "number",
+ "SIGINT": "number",
+ "SIGIO": "number",
+ "SIGIOT": "number",
+ "SIGKILL": "number",
+ "SIGPIPE": "number",
+ "SIGPOLL": "number",
+ "SIGPROF": "number",
+ "SIGPWR": "number",
+ "SIGQUIT": "number",
+ "SIGSEGV": "number",
+ "SIGSTKFLT": "number",
+ "SIGSTOP": "number",
+ "SIGSYS": "number",
+ "SIGTERM": "number",
+ "SIGTRAP": "number",
+ "SIGTSTP": "number",
+ "SIGTTIN": "number",
+ "SIGTTOU": "number",
+ "SIGURG": "number",
+ "SIGUSR1": "number",
+ "SIGUSR2": "number",
+ "SIGVTALRM": "number",
+ "SIGWINCH": "number",
+ "SIGXCPU": "number",
+ "SIGXFSZ": "number",
+ "SSL_OP_ALL": "number",
+ "SSL_OP_ALLOW_NO_DHE_KEX": "number",
+ "SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION": "number",
+ "SSL_OP_CIPHER_SERVER_PREFERENCE": "number",
+ "SSL_OP_CISCO_ANYCONNECT": "number",
+ "SSL_OP_COOKIE_EXCHANGE": "number",
+ "SSL_OP_CRYPTOPRO_TLSEXT_BUG": "number",
+ "SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS": "number",
+ "SSL_OP_LEGACY_SERVER_CONNECT": "number",
+ "SSL_OP_NO_COMPRESSION": "number",
+ "SSL_OP_NO_ENCRYPT_THEN_MAC": "number",
+ "SSL_OP_NO_QUERY_MTU": "number",
+ "SSL_OP_NO_RENEGOTIATION": "number",
+ "SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION": "number",
+ "SSL_OP_NO_SSLv2": "number",
+ "SSL_OP_NO_SSLv3": "number",
+ "SSL_OP_NO_TICKET": "number",
+ "SSL_OP_NO_TLSv1": "number",
+ "SSL_OP_NO_TLSv1_1": "number",
+ "SSL_OP_NO_TLSv1_2": "number",
+ "SSL_OP_NO_TLSv1_3": "number",
+ "SSL_OP_PRIORITIZE_CHACHA": "number",
+ "SSL_OP_TLS_ROLLBACK_BUG": "number",
+ "S_IFBLK": "number",
+ "S_IFCHR": "number",
+ "S_IFDIR": "number",
+ "S_IFIFO": "number",
+ "S_IFLNK": "number",
+ "S_IFMT": "number",
+ "S_IFREG": "number",
+ "S_IFSOCK": "number",
+ "S_IRGRP": "number",
+ "S_IROTH": "number",
+ "S_IRUSR": "number",
+ "S_IRWXG": "number",
+ "S_IRWXO": "number",
+ "S_IRWXU": "number",
+ "S_IWGRP": "number",
+ "S_IWOTH": "number",
+ "S_IWUSR": "number",
+ "S_IXGRP": "number",
+ "S_IXOTH": "number",
+ "S_IXUSR": "number",
+ "TLS1_1_VERSION": "number",
+ "TLS1_2_VERSION": "number",
+ "TLS1_3_VERSION": "number",
+ "TLS1_VERSION": "number",
+ "UV_DIRENT_BLOCK": "number",
+ "UV_DIRENT_CHAR": "number",
+ "UV_DIRENT_DIR": "number",
+ "UV_DIRENT_FIFO": "number",
+ "UV_DIRENT_FILE": "number",
+ "UV_DIRENT_LINK": "number",
+ "UV_DIRENT_SOCKET": "number",
+ "UV_DIRENT_UNKNOWN": "number",
+ "UV_FS_COPYFILE_EXCL": "number",
+ "UV_FS_COPYFILE_FICLONE": "number",
+ "UV_FS_COPYFILE_FICLONE_FORCE": "number",
+ "UV_FS_O_FILEMAP": "number",
+ "UV_FS_SYMLINK_DIR": "number",
+ "UV_FS_SYMLINK_JUNCTION": "number",
+ "W_OK": "number",
+ "X_OK": "number",
+ "default": "object",
+ "defaultCipherList": "string",
+ "defaultCoreCipherList": "string"
+ },
+ "crypto": {
+ "Certificate": "function",
+ "Cipher": "function",
+ "Cipheriv": "function",
+ "CryptoKey": "function",
+ "Decipher": "function",
+ "Decipheriv": "function",
+ "DiffieHellman": "function",
+ "DiffieHellmanGroup": "function",
+ "ECDH": "function",
+ "Hash": "function",
+ "Hmac": "function",
+ "KeyObject": "function",
+ "PrivateKeyObject": "function",
+ "PublicKeyObject": "function",
+ "SecretKeyObject": "function",
+ "Sign": "function",
+ "Verify": "function",
+ "X509Certificate": "function",
+ "checkPrime": "function",
+ "checkPrimeSync": "function",
+ "constants": "object",
+ "createCipher": "function",
+ "createCipheriv": "function",
+ "createDecipher": "function",
+ "createDecipheriv": "function",
+ "createDiffieHellman": "function",
+ "createDiffieHellmanGroup": "function",
+ "createECDH": "function",
+ "createHash": "function",
+ "createHmac": "function",
+ "createPrivateKey": "function",
+ "createPublicKey": "function",
+ "createSecretKey": "function",
+ "createSign": "function",
+ "createVerify": "function",
+ "default": "object",
+ "diffieHellman": "function",
+ "fips": "boolean",
+ "generateKey": "function",
+ "generateKeyPair": "function",
+ "generateKeyPairSync": "function",
+ "generateKeySync": "function",
+ "generatePrime": "function",
+ "generatePrimeSync": "function",
+ "getCipherInfo": "function",
+ "getCiphers": "function",
+ "getCurves": "function",
+ "getDiffieHellman": "function",
+ "getFips": "function",
+ "getHashes": "function",
+ "getRandomValues": "function",
+ "hash": "function",
+ "hkdf": "function",
+ "hkdfSync": "function",
+ "pbkdf2": "function",
+ "pbkdf2Sync": "function",
+ "privateDecrypt": "function",
+ "privateEncrypt": "function",
+ "pseudoRandomBytes": "function",
+ "publicDecrypt": "function",
+ "publicEncrypt": "function",
+ "randomBytes": "function",
+ "randomFill": "function",
+ "randomFillSync": "function",
+ "randomInt": "function",
+ "randomUUID": "function",
+ "scrypt": "function",
+ "scryptSync": "function",
+ "secureHeapUsed": "function",
+ "setEngine": "function",
+ "setFips": "function",
+ "sign": "function",
+ "subtle": "object",
+ "timingSafeEqual": "function",
+ "verify": "function",
+ "webcrypto": "object"
+ },
+ "dgram": {
+ "Socket": "function",
+ "createSocket": "function",
+ "default": "object"
+ },
+ "diagnostics_channel": {
+ "Channel": "function",
+ "TracingChannel": "function",
+ "channel": "function",
+ "default": "object",
+ "hasSubscribers": "function",
+ "subscribe": "function",
+ "tracingChannel": "function",
+ "unsubscribe": "function"
+ },
+ "dns": {
+ "ADDRCONFIG": "number",
+ "ADDRGETNETWORKPARAMS": "string",
+ "ALL": "number",
+ "BADFAMILY": "string",
+ "BADFLAGS": "string",
+ "BADHINTS": "string",
+ "BADNAME": "string",
+ "BADQUERY": "string",
+ "BADRESP": "string",
+ "BADSTR": "string",
+ "CANCELLED": "string",
+ "CONNREFUSED": "string",
+ "DESTRUCTION": "string",
+ "EOF": "string",
+ "FILE": "string",
+ "FORMERR": "string",
+ "LOADIPHLPAPI": "string",
+ "NODATA": "string",
+ "NOMEM": "string",
+ "NONAME": "string",
+ "NOTFOUND": "string",
+ "NOTIMP": "string",
+ "NOTINITIALIZED": "string",
+ "REFUSED": "string",
+ "Resolver": "function",
+ "SERVFAIL": "string",
+ "TIMEOUT": "string",
+ "V4MAPPED": "number",
+ "default": "object",
+ "getDefaultResultOrder": "function",
+ "getServers": "function",
+ "lookup": "function",
+ "lookupService": "function",
+ "promises": "object",
+ "resolve": "function",
+ "resolve4": "function",
+ "resolve6": "function",
+ "resolveAny": "function",
+ "resolveCaa": "function",
+ "resolveCname": "function",
+ "resolveMx": "function",
+ "resolveNaptr": "function",
+ "resolveNs": "function",
+ "resolvePtr": "function",
+ "resolveSoa": "function",
+ "resolveSrv": "function",
+ "resolveTxt": "function",
+ "reverse": "function",
+ "setDefaultResultOrder": "function",
+ "setServers": "function"
+ },
+ "dns/promises": {
+ "ADDRCONFIG": "number",
+ "ADDRGETNETWORKPARAMS": "string",
+ "ALL": "number",
+ "BADFAMILY": "string",
+ "BADFLAGS": "string",
+ "BADHINTS": "string",
+ "BADNAME": "string",
+ "BADQUERY": "string",
+ "BADRESP": "string",
+ "BADSTR": "string",
+ "CANCELLED": "string",
+ "CONNREFUSED": "string",
+ "DESTRUCTION": "string",
+ "EOF": "string",
+ "FILE": "string",
+ "FORMERR": "string",
+ "LOADIPHLPAPI": "string",
+ "NODATA": "string",
+ "NOMEM": "string",
+ "NONAME": "string",
+ "NOTFOUND": "string",
+ "NOTIMP": "string",
+ "NOTINITIALIZED": "string",
+ "REFUSED": "string",
+ "Resolver": "function",
+ "SERVFAIL": "string",
+ "TIMEOUT": "string",
+ "V4MAPPED": "number",
+ "default": "object",
+ "getDefaultResultOrder": "function",
+ "getServers": "function",
+ "lookup": "function",
+ "lookupService": "function",
+ "resolve": "function",
+ "resolve4": "function",
+ "resolve6": "function",
+ "resolveAny": "function",
+ "resolveCaa": "function",
+ "resolveCname": "function",
+ "resolveMx": "function",
+ "resolveNaptr": "function",
+ "resolveNs": "function",
+ "resolvePtr": "function",
+ "resolveSoa": "function",
+ "resolveSrv": "function",
+ "resolveTxt": "function",
+ "reverse": "function",
+ "setDefaultResultOrder": "function",
+ "setServers": "function"
+ },
+ "domain": {
+ "Domain": "function",
+ "active": "object",
+ "create": "object",
+ "createDomain": "function",
+ "default": "object"
+ },
+ "events": {
+ "EventEmitter": "function",
+ "EventEmitterAsyncResource": "function",
+ "addAbortListener": "function",
+ "captureRejectionSymbol": "symbol",
+ "captureRejections": "boolean",
+ "default": "function",
+ "defaultMaxListeners": "number",
+ "errorMonitor": "symbol",
+ "getEventListeners": "function",
+ "getMaxListeners": "function",
+ "listenerCount": "function",
+ "on": "function",
+ "once": "function",
+ "setMaxListeners": "function",
+ "usingDomains": "boolean"
+ },
+ "fs": {
+ "Dir": "function",
+ "Dirent": "function",
+ "F_OK": "number",
+ "FileReadStream": "function",
+ "FileWriteStream": "function",
+ "R_OK": "number",
+ "ReadStream": "function",
+ "Stats": "function",
+ "W_OK": "number",
+ "WriteStream": "function",
+ "X_OK": "number",
+ "access": "function",
+ "accessSync": "function",
+ "appendFile": "function",
+ "appendFileSync": "function",
+ "chmod": "function",
+ "chmodSync": "function",
+ "chown": "function",
+ "chownSync": "function",
+ "close": "function",
+ "closeSync": "function",
+ "constants": "object",
+ "copyFile": "function",
+ "copyFileSync": "function",
+ "cp": "function",
+ "cpSync": "function",
+ "createReadStream": "function",
+ "createWriteStream": "function",
+ "default": "object",
+ "exists": "function",
+ "existsSync": "function",
+ "fchmod": "function",
+ "fchmodSync": "function",
+ "fchown": "function",
+ "fchownSync": "function",
+ "fdatasync": "function",
+ "fdatasyncSync": "function",
+ "fstat": "function",
+ "fstatSync": "function",
+ "fsync": "function",
+ "fsyncSync": "function",
+ "ftruncate": "function",
+ "ftruncateSync": "function",
+ "futimes": "function",
+ "futimesSync": "function",
+ "glob": "function",
+ "globSync": "function",
+ "lchmod": "function",
+ "lchmodSync": "function",
+ "lchown": "function",
+ "lchownSync": "function",
+ "link": "function",
+ "linkSync": "function",
+ "lstat": "function",
+ "lstatSync": "function",
+ "lutimes": "function",
+ "lutimesSync": "function",
+ "mkdir": "function",
+ "mkdirSync": "function",
+ "mkdtemp": "function",
+ "mkdtempSync": "function",
+ "open": "function",
+ "openAsBlob": "function",
+ "openSync": "function",
+ "opendir": "function",
+ "opendirSync": "function",
+ "promises": "object",
+ "read": "function",
+ "readFile": "function",
+ "readFileSync": "function",
+ "readSync": "function",
+ "readdir": "function",
+ "readdirSync": "function",
+ "readlink": "function",
+ "readlinkSync": "function",
+ "readv": "function",
+ "readvSync": "function",
+ "realpath": "function",
+ "realpathSync": "function",
+ "rename": "function",
+ "renameSync": "function",
+ "rm": "function",
+ "rmSync": "function",
+ "rmdir": "function",
+ "rmdirSync": "function",
+ "stat": "function",
+ "statSync": "function",
+ "statfs": "function",
+ "statfsSync": "function",
+ "symlink": "function",
+ "symlinkSync": "function",
+ "truncate": "function",
+ "truncateSync": "function",
+ "unlink": "function",
+ "unlinkSync": "function",
+ "unwatchFile": "function",
+ "utimes": "function",
+ "utimesSync": "function",
+ "watch": "function",
+ "watchFile": "function",
+ "write": "function",
+ "writeFile": "function",
+ "writeFileSync": "function",
+ "writeSync": "function",
+ "writev": "function",
+ "writevSync": "function"
+ },
+ "fs/promises": {
+ "FileHandle": "function",
+ "access": "function",
+ "appendFile": "function",
+ "chmod": "function",
+ "chown": "function",
+ "constants": "object",
+ "copyFile": "function",
+ "cp": "function",
+ "default": "object",
+ "glob": "function",
+ "lchmod": "function",
+ "lchown": "function",
+ "link": "function",
+ "lstat": "function",
+ "lutimes": "function",
+ "mkdir": "function",
+ "mkdtemp": "function",
+ "open": "function",
+ "opendir": "function",
+ "readFile": "function",
+ "readdir": "function",
+ "readlink": "function",
+ "realpath": "function",
+ "rename": "function",
+ "rm": "function",
+ "rmdir": "function",
+ "stat": "function",
+ "statfs": "function",
+ "symlink": "function",
+ "truncate": "function",
+ "unlink": "function",
+ "utimes": "function",
+ "watch": "function",
+ "writeFile": "function"
+ },
+ "http": {
+ "Agent": "function",
+ "ClientRequest": "function",
+ "CloseEvent": "function",
+ "IncomingMessage": "function",
+ "METHODS": "object",
+ "MessageEvent": "function",
+ "OutgoingMessage": "function",
+ "STATUS_CODES": "object",
+ "Server": "function",
+ "ServerResponse": "function",
+ "WebSocket": "function",
+ "_connectionListener": "function",
+ "createServer": "function",
+ "default": "object",
+ "get": "function",
+ "globalAgent": "object",
+ "maxHeaderSize": "number",
+ "request": "function",
+ "setMaxIdleHTTPParsers": "function",
+ "validateHeaderName": "function",
+ "validateHeaderValue": "function"
+ },
+ "http2": {
+ "Http2ServerRequest": "function",
+ "Http2ServerResponse": "function",
+ "connect": "function",
+ "constants": "object",
+ "createSecureServer": "function",
+ "createServer": "function",
+ "default": "object",
+ "getDefaultSettings": "function",
+ "getPackedSettings": "function",
+ "getUnpackedSettings": "function",
+ "performServerHandshake": "function",
+ "sensitiveHeaders": "symbol"
+ },
+ "https": {
+ "Agent": "function",
+ "Server": "function",
+ "createServer": "function",
+ "default": "object",
+ "get": "function",
+ "globalAgent": "object",
+ "request": "function"
+ },
+ "inspector": {
+ "Network": "object",
+ "Session": "function",
+ "close": "function",
+ "console": "object",
+ "default": "object",
+ "open": "function",
+ "url": "function",
+ "waitForDebugger": "function"
+ },
+ "inspector/promises": {
+ "Network": "object",
+ "Session": "function",
+ "close": "function",
+ "console": "object",
+ "default": "object",
+ "open": "function",
+ "url": "function",
+ "waitForDebugger": "function"
+ },
+ "module": {
+ "Module": "function",
+ "SourceMap": "function",
+ "_cache": "object",
+ "_debug": "function",
+ "_extensions": "object",
+ "_findPath": "function",
+ "_initPaths": "function",
+ "_load": "function",
+ "_nodeModulePaths": "function",
+ "_pathCache": "object",
+ "_preloadModules": "function",
+ "_resolveFilename": "function",
+ "_resolveLookupPaths": "function",
+ "builtinModules": "object",
+ "constants": "object",
+ "createRequire": "function",
+ "default": "function",
+ "enableCompileCache": "function",
+ "findPackageJSON": "function",
+ "findSourceMap": "function",
+ "flushCompileCache": "function",
+ "getCompileCacheDir": "function",
+ "getSourceMapsSupport": "function",
+ "globalPaths": "object",
+ "isBuiltin": "function",
+ "register": "function",
+ "registerHooks": "function",
+ "runMain": "function",
+ "setSourceMapsSupport": "function",
+ "stripTypeScriptTypes": "function",
+ "syncBuiltinESMExports": "function",
+ "wrap": "function"
+ },
+ "net": {
+ "BlockList": "function",
+ "BoundSocket": "function",
+ "Server": "function",
+ "Socket": "function",
+ "SocketAddress": "function",
+ "Stream": "function",
+ "_normalizeArgs": "function",
+ "connect": "function",
+ "createConnection": "function",
+ "createServer": "function",
+ "default": "object",
+ "getDefaultAutoSelectFamily": "function",
+ "getDefaultAutoSelectFamilyAttemptTimeout": "function",
+ "isIP": "function",
+ "isIPv4": "function",
+ "isIPv6": "function",
+ "setDefaultAutoSelectFamily": "function",
+ "setDefaultAutoSelectFamilyAttemptTimeout": "function"
+ },
+ "os": {
+ "EOL": "string",
+ "arch": "function",
+ "availableParallelism": "function",
+ "constants": "object",
+ "cpus": "function",
+ "default": "object",
+ "devNull": "string",
+ "endianness": "function",
+ "freemem": "function",
+ "getPriority": "function",
+ "homedir": "function",
+ "hostname": "function",
+ "loadavg": "function",
+ "machine": "function",
+ "networkInterfaces": "function",
+ "platform": "function",
+ "release": "function",
+ "setPriority": "function",
+ "tmpdir": "function",
+ "totalmem": "function",
+ "type": "function",
+ "uptime": "function",
+ "userInfo": "function",
+ "version": "function"
+ },
+ "path": {
+ "basename": "function",
+ "default": "object",
+ "delimiter": "string",
+ "dirname": "function",
+ "extname": "function",
+ "format": "function",
+ "isAbsolute": "function",
+ "join": "function",
+ "matchesGlob": "function",
+ "normalize": "function",
+ "parse": "function",
+ "posix": "object",
+ "relative": "function",
+ "resolve": "function",
+ "sep": "string",
+ "toNamespacedPath": "function",
+ "win32": "object"
+ },
+ "path/posix": {
+ "basename": "function",
+ "default": "object",
+ "delimiter": "string",
+ "dirname": "function",
+ "extname": "function",
+ "format": "function",
+ "isAbsolute": "function",
+ "join": "function",
+ "matchesGlob": "function",
+ "normalize": "function",
+ "parse": "function",
+ "posix": "object",
+ "relative": "function",
+ "resolve": "function",
+ "sep": "string",
+ "toNamespacedPath": "function",
+ "win32": "object"
+ },
+ "path/win32": {
+ "basename": "function",
+ "default": "object",
+ "delimiter": "string",
+ "dirname": "function",
+ "extname": "function",
+ "format": "function",
+ "isAbsolute": "function",
+ "join": "function",
+ "matchesGlob": "function",
+ "normalize": "function",
+ "parse": "function",
+ "posix": "object",
+ "relative": "function",
+ "resolve": "function",
+ "sep": "string",
+ "toNamespacedPath": "function",
+ "win32": "object"
+ },
+ "perf_hooks": {
+ "Performance": "function",
+ "PerformanceEntry": "function",
+ "PerformanceMark": "function",
+ "PerformanceMeasure": "function",
+ "PerformanceObserver": "function",
+ "PerformanceObserverEntryList": "function",
+ "PerformanceResourceTiming": "function",
+ "constants": "object",
+ "createHistogram": "function",
+ "default": "object",
+ "eventLoopUtilization": "function",
+ "monitorEventLoopDelay": "function",
+ "performance": "object",
+ "timerify": "function"
+ },
+ "process": {
+ "_channel": "undefined",
+ "_debugEnd": "function",
+ "_debugProcess": "function",
+ "_disconnect": "undefined",
+ "_events": "object",
+ "_eventsCount": "number",
+ "_exiting": "boolean",
+ "_fatalException": "function",
+ "_getActiveHandles": "function",
+ "_getActiveRequests": "function",
+ "_handleQueue": "undefined",
+ "_kill": "function",
+ "_linkedBinding": "function",
+ "_maxListeners": "undefined",
+ "_pendingMessage": "undefined",
+ "_preload_modules": "object",
+ "_rawDebug": "function",
+ "_send": "undefined",
+ "_startProfilerIdleNotifier": "function",
+ "_stopProfilerIdleNotifier": "function",
+ "_tickCallback": "function",
+ "abort": "function",
+ "allowedNodeEnvironmentFlags": "object",
+ "arch": "string",
+ "argv": "object",
+ "argv0": "string",
+ "assert": "function",
+ "availableMemory": "function",
+ "binding": "function",
+ "channel": "object",
+ "chdir": "function",
+ "config": "object",
+ "connected": "boolean",
+ "constrainedMemory": "function",
+ "cpuUsage": "function",
+ "cwd": "function",
+ "debugPort": "number",
+ "default": "object",
+ "dlopen": "function",
+ "domain": "object",
+ "emitWarning": "function",
+ "env": "object",
+ "execArgv": "object",
+ "execPath": "string",
+ "execve": "function",
+ "exit": "function",
+ "exitCode": "undefined",
+ "features": "object",
+ "finalization": "object",
+ "getActiveResourcesInfo": "function",
+ "getBuiltinModule": "function",
+ "getSourceMapsSupport": "function",
+ "getegid": "function",
+ "geteuid": "function",
+ "getgid": "function",
+ "getgroups": "function",
+ "getuid": "function",
+ "hasUncaughtExceptionCaptureCallback": "function",
+ "hrtime": "function",
+ "initgroups": "function",
+ "kill": "function",
+ "loadEnvFile": "function",
+ "memoryUsage": "function",
+ "moduleLoadList": "object",
+ "nextTick": "function",
+ "noDeprecation": "boolean",
+ "openStdin": "function",
+ "permission": "object",
+ "pid": "number",
+ "platform": "string",
+ "ppid": "number",
+ "reallyExit": "function",
+ "ref": "function",
+ "release": "object",
+ "report": "object",
+ "resourceUsage": "function",
+ "send": "function",
+ "setSourceMapsEnabled": "function",
+ "setUncaughtExceptionCaptureCallback": "function",
+ "setegid": "function",
+ "seteuid": "function",
+ "setgid": "function",
+ "setgroups": "function",
+ "setuid": "function",
+ "sourceMapsEnabled": "boolean",
+ "stderr": "object",
+ "stdin": "object",
+ "stdout": "object",
+ "threadCpuUsage": "function",
+ "throwDeprecation": "boolean",
+ "title": "string",
+ "traceDeprecation": "boolean",
+ "umask": "function",
+ "unref": "function",
+ "uptime": "function",
+ "version": "string",
+ "versions": "object"
+ },
+ "punycode": {
+ "decode": "function",
+ "default": "object",
+ "encode": "function",
+ "toASCII": "function",
+ "toUnicode": "function",
+ "ucs2": "object",
+ "version": "string"
+ },
+ "querystring": {
+ "decode": "function",
+ "default": "object",
+ "encode": "function",
+ "escape": "function",
+ "parse": "function",
+ "stringify": "function",
+ "unescape": "function",
+ "unescapeBuffer": "function"
+ },
+ "readline": {
+ "Interface": "function",
+ "clearLine": "function",
+ "clearScreenDown": "function",
+ "createInterface": "function",
+ "cursorTo": "function",
+ "default": "object",
+ "emitKeypressEvents": "function",
+ "moveCursor": "function",
+ "promises": "object"
+ },
+ "readline/promises": {
+ "Interface": "function",
+ "Readline": "function",
+ "createInterface": "function",
+ "default": "object"
+ },
+ "repl": {
+ "REPLServer": "function",
+ "REPL_MODE_SLOPPY": "symbol",
+ "REPL_MODE_STRICT": "symbol",
+ "Recoverable": "function",
+ "_builtinLibs": "object",
+ "builtinModules": "object",
+ "default": "object",
+ "start": "function",
+ "writer": "function"
+ },
+ "sqlite": {
+ "DatabaseSync": "function",
+ "StatementSync": "function",
+ "backup": "function",
+ "constants": "object",
+ "default": "object"
+ },
+ "stream": {
+ "Duplex": "function",
+ "PassThrough": "function",
+ "Readable": "function",
+ "Stream": "function",
+ "Transform": "function",
+ "Writable": "function",
+ "_isArrayBufferView": "function",
+ "_isUint8Array": "function",
+ "_uint8ArrayToBuffer": "function",
+ "addAbortSignal": "function",
+ "compose": "function",
+ "default": "function",
+ "destroy": "function",
+ "duplexPair": "function",
+ "finished": "function",
+ "getDefaultHighWaterMark": "function",
+ "isDestroyed": "function",
+ "isDisturbed": "function",
+ "isErrored": "function",
+ "isReadable": "function",
+ "isWritable": "function",
+ "pipeline": "function",
+ "promises": "object",
+ "setDefaultHighWaterMark": "function"
+ },
+ "stream/consumers": {
+ "arrayBuffer": "function",
+ "blob": "function",
+ "buffer": "function",
+ "default": "object",
+ "json": "function",
+ "text": "function"
+ },
+ "stream/promises": {
+ "default": "object",
+ "finished": "function",
+ "pipeline": "function",
+ "promises": "object"
+ },
+ "stream/web": {
+ "ByteLengthQueuingStrategy": "function",
+ "CompressionStream": "function",
+ "CountQueuingStrategy": "function",
+ "DecompressionStream": "function",
+ "ReadableByteStreamController": "function",
+ "ReadableStream": "function",
+ "ReadableStreamBYOBReader": "function",
+ "ReadableStreamBYOBRequest": "function",
+ "ReadableStreamDefaultController": "function",
+ "ReadableStreamDefaultReader": "function",
+ "TextDecoderStream": "function",
+ "TextEncoderStream": "function",
+ "TransformStream": "function",
+ "TransformStreamDefaultController": "function",
+ "WritableStream": "function",
+ "WritableStreamDefaultController": "function",
+ "WritableStreamDefaultWriter": "function",
+ "default": "object"
+ },
+ "string_decoder": {
+ "StringDecoder": "function",
+ "default": "object"
+ },
+ "sys": {
+ "MIMEParams": "function",
+ "MIMEType": "function",
+ "TextDecoder": "function",
+ "TextEncoder": "function",
+ "_errnoException": "function",
+ "_exceptionWithHostPort": "function",
+ "_extend": "function",
+ "aborted": "function",
+ "callbackify": "function",
+ "debug": "function",
+ "debuglog": "function",
+ "default": "object",
+ "deprecate": "function",
+ "format": "function",
+ "formatWithOptions": "function",
+ "getCallSite": "function",
+ "getCallSites": "function",
+ "getSystemErrorMap": "function",
+ "getSystemErrorMessage": "function",
+ "getSystemErrorName": "function",
+ "inherits": "function",
+ "inspect": "function",
+ "isArray": "function",
+ "isBoolean": "function",
+ "isBuffer": "function",
+ "isDate": "function",
+ "isDeepStrictEqual": "function",
+ "isError": "function",
+ "isFunction": "function",
+ "isNull": "function",
+ "isNullOrUndefined": "function",
+ "isNumber": "function",
+ "isObject": "function",
+ "isPrimitive": "function",
+ "isRegExp": "function",
+ "isString": "function",
+ "isSymbol": "function",
+ "isUndefined": "function",
+ "log": "function",
+ "parseArgs": "function",
+ "parseEnv": "function",
+ "promisify": "function",
+ "stripVTControlCharacters": "function",
+ "styleText": "function",
+ "toUSVString": "function",
+ "transferableAbortController": "function",
+ "transferableAbortSignal": "function",
+ "types": "object"
+ },
+ "test": {
+ "MockFunctionContext": "function",
+ "MockTracker": "function",
+ "default": "object",
+ "mock": "object"
+ },
+ "timers": {
+ "Timeout": "function",
+ "active": "function",
+ "clearImmediate": "function",
+ "clearInterval": "function",
+ "clearTimeout": "function",
+ "default": "object",
+ "enroll": "function",
+ "promises": "object",
+ "setImmediate": "function",
+ "setInterval": "function",
+ "setTimeout": "function",
+ "unenroll": "function"
+ },
+ "timers/promises": {
+ "default": "object",
+ "scheduler": "object",
+ "setImmediate": "function",
+ "setInterval": "function",
+ "setTimeout": "function"
+ },
+ "tls": {
+ "CLIENT_RENEG_LIMIT": "number",
+ "CLIENT_RENEG_WINDOW": "number",
+ "DEFAULT_CIPHERS": "string",
+ "DEFAULT_ECDH_CURVE": "string",
+ "DEFAULT_MAX_VERSION": "string",
+ "DEFAULT_MIN_VERSION": "string",
+ "SecureContext": "function",
+ "Server": "function",
+ "TLSSocket": "function",
+ "checkServerIdentity": "function",
+ "connect": "function",
+ "convertALPNProtocols": "function",
+ "createSecureContext": "function",
+ "createSecurePair": "function",
+ "createServer": "function",
+ "default": "object",
+ "getCiphers": "function",
+ "rootCertificates": "object"
+ },
+ "trace_events": {
+ "createTracing": "function",
+ "default": "object",
+ "getEnabledCategories": "function"
+ },
+ "tty": {
+ "ReadStream": "function",
+ "WriteStream": "function",
+ "default": "object",
+ "isatty": "function"
+ },
+ "url": {
+ "URL": "function",
+ "URLSearchParams": "function",
+ "Url": "function",
+ "default": "object",
+ "domainToASCII": "function",
+ "domainToUnicode": "function",
+ "fileURLToPath": "function",
+ "format": "function",
+ "parse": "function",
+ "pathToFileURL": "function",
+ "resolve": "function",
+ "resolveObject": "function",
+ "toPathIfFileURL": "function",
+ "urlToHttpOptions": "function"
+ },
+ "util": {
+ "MIMEParams": "function",
+ "MIMEType": "function",
+ "TextDecoder": "function",
+ "TextEncoder": "function",
+ "_errnoException": "function",
+ "_exceptionWithHostPort": "function",
+ "_extend": "function",
+ "aborted": "function",
+ "callbackify": "function",
+ "debug": "function",
+ "debuglog": "function",
+ "default": "object",
+ "deprecate": "function",
+ "format": "function",
+ "formatWithOptions": "function",
+ "getCallSite": "function",
+ "getCallSites": "function",
+ "getSystemErrorMap": "function",
+ "getSystemErrorMessage": "function",
+ "getSystemErrorName": "function",
+ "inherits": "function",
+ "inspect": "function",
+ "isArray": "function",
+ "isBoolean": "function",
+ "isBuffer": "function",
+ "isDate": "function",
+ "isDeepStrictEqual": "function",
+ "isError": "function",
+ "isFunction": "function",
+ "isNull": "function",
+ "isNullOrUndefined": "function",
+ "isNumber": "function",
+ "isObject": "function",
+ "isPrimitive": "function",
+ "isRegExp": "function",
+ "isString": "function",
+ "isSymbol": "function",
+ "isUndefined": "function",
+ "log": "function",
+ "parseArgs": "function",
+ "parseEnv": "function",
+ "promisify": "function",
+ "stripVTControlCharacters": "function",
+ "styleText": "function",
+ "toUSVString": "function",
+ "transferableAbortController": "function",
+ "transferableAbortSignal": "function",
+ "types": "object"
+ },
+ "util/types": {
+ "default": "object",
+ "isAnyArrayBuffer": "function",
+ "isArgumentsObject": "function",
+ "isArrayBuffer": "function",
+ "isArrayBufferView": "function",
+ "isAsyncFunction": "function",
+ "isBigInt64Array": "function",
+ "isBigIntObject": "function",
+ "isBigUint64Array": "function",
+ "isBooleanObject": "function",
+ "isBoxedPrimitive": "function",
+ "isCryptoKey": "function",
+ "isDataView": "function",
+ "isDate": "function",
+ "isExternal": "function",
+ "isFloat16Array": "function",
+ "isFloat32Array": "function",
+ "isFloat64Array": "function",
+ "isGeneratorFunction": "function",
+ "isGeneratorObject": "function",
+ "isInt16Array": "function",
+ "isInt32Array": "function",
+ "isInt8Array": "function",
+ "isKeyObject": "function",
+ "isMap": "function",
+ "isMapIterator": "function",
+ "isModuleNamespaceObject": "function",
+ "isNativeError": "function",
+ "isNumberObject": "function",
+ "isPromise": "function",
+ "isProxy": "function",
+ "isRegExp": "function",
+ "isSet": "function",
+ "isSetIterator": "function",
+ "isSharedArrayBuffer": "function",
+ "isStringObject": "function",
+ "isSymbolObject": "function",
+ "isTypedArray": "function",
+ "isUint16Array": "function",
+ "isUint32Array": "function",
+ "isUint8Array": "function",
+ "isUint8ClampedArray": "function",
+ "isWeakMap": "function",
+ "isWeakSet": "function"
+ },
+ "v8": {
+ "DefaultDeserializer": "function",
+ "DefaultSerializer": "function",
+ "Deserializer": "function",
+ "GCProfiler": "function",
+ "Serializer": "function",
+ "cachedDataVersionTag": "function",
+ "default": "object",
+ "deserialize": "function",
+ "getCppHeapStatistics": "function",
+ "getHeapCodeStatistics": "function",
+ "getHeapSnapshot": "function",
+ "getHeapSpaceStatistics": "function",
+ "getHeapStatistics": "function",
+ "isStringOneByteRepresentation": "function",
+ "promiseHooks": "object",
+ "queryObjects": "function",
+ "serialize": "function",
+ "setFlagsFromString": "function",
+ "setHeapSnapshotNearHeapLimit": "function",
+ "startupSnapshot": "object",
+ "stopCoverage": "function",
+ "takeCoverage": "function",
+ "writeHeapSnapshot": "function"
+ },
+ "vm": {
+ "Script": "function",
+ "compileFunction": "function",
+ "constants": "object",
+ "createContext": "function",
+ "createScript": "function",
+ "default": "object",
+ "isContext": "function",
+ "measureMemory": "function",
+ "runInContext": "function",
+ "runInNewContext": "function",
+ "runInThisContext": "function"
+ },
+ "wasi": {
+ "WASI": "function",
+ "default": "object"
+ },
+ "worker_threads": {
+ "BroadcastChannel": "function",
+ "MessageChannel": "function",
+ "MessagePort": "function",
+ "SHARE_ENV": "symbol",
+ "Worker": "function",
+ "default": "object",
+ "getEnvironmentData": "function",
+ "isInternalThread": "boolean",
+ "isMainThread": "boolean",
+ "isMarkedAsUntransferable": "function",
+ "markAsUncloneable": "function",
+ "markAsUntransferable": "function",
+ "moveMessagePortToContext": "function",
+ "parentPort": "object",
+ "postMessageToThread": "function",
+ "receiveMessageOnPort": "function",
+ "resourceLimits": "object",
+ "setEnvironmentData": "function",
+ "threadId": "number",
+ "workerData": "object"
+ },
+ "zlib": {
+ "BROTLI_DECODE": "number",
+ "BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES": "number",
+ "BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP": "number",
+ "BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES": "number",
+ "BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1": "number",
+ "BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2": "number",
+ "BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS": "number",
+ "BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_CL_SPACE": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_DICTIONARY": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_DISTANCE": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_PADDING_1": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_PADDING_2": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_RESERVED": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_TRANSFORM": "number",
+ "BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS": "number",
+ "BROTLI_DECODER_ERROR_INVALID_ARGUMENTS": "number",
+ "BROTLI_DECODER_ERROR_UNREACHABLE": "number",
+ "BROTLI_DECODER_NEEDS_MORE_INPUT": "number",
+ "BROTLI_DECODER_NEEDS_MORE_OUTPUT": "number",
+ "BROTLI_DECODER_NO_ERROR": "number",
+ "BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION": "number",
+ "BROTLI_DECODER_PARAM_LARGE_WINDOW": "number",
+ "BROTLI_DECODER_RESULT_ERROR": "number",
+ "BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT": "number",
+ "BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT": "number",
+ "BROTLI_DECODER_RESULT_SUCCESS": "number",
+ "BROTLI_DECODER_SUCCESS": "number",
+ "BROTLI_DEFAULT_MODE": "number",
+ "BROTLI_DEFAULT_QUALITY": "number",
+ "BROTLI_DEFAULT_WINDOW": "number",
+ "BROTLI_ENCODE": "number",
+ "BROTLI_LARGE_MAX_WINDOW_BITS": "number",
+ "BROTLI_MAX_INPUT_BLOCK_BITS": "number",
+ "BROTLI_MAX_QUALITY": "number",
+ "BROTLI_MAX_WINDOW_BITS": "number",
+ "BROTLI_MIN_INPUT_BLOCK_BITS": "number",
+ "BROTLI_MIN_QUALITY": "number",
+ "BROTLI_MIN_WINDOW_BITS": "number",
+ "BROTLI_MODE_FONT": "number",
+ "BROTLI_MODE_GENERIC": "number",
+ "BROTLI_MODE_TEXT": "number",
+ "BROTLI_OPERATION_EMIT_METADATA": "number",
+ "BROTLI_OPERATION_FINISH": "number",
+ "BROTLI_OPERATION_FLUSH": "number",
+ "BROTLI_OPERATION_PROCESS": "number",
+ "BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING": "number",
+ "BROTLI_PARAM_LARGE_WINDOW": "number",
+ "BROTLI_PARAM_LGBLOCK": "number",
+ "BROTLI_PARAM_LGWIN": "number",
+ "BROTLI_PARAM_MODE": "number",
+ "BROTLI_PARAM_NDIRECT": "number",
+ "BROTLI_PARAM_NPOSTFIX": "number",
+ "BROTLI_PARAM_QUALITY": "number",
+ "BrotliCompress": "function",
+ "BrotliDecompress": "function",
+ "DEFLATE": "number",
+ "DEFLATERAW": "number",
+ "Deflate": "function",
+ "DeflateRaw": "function",
+ "GUNZIP": "number",
+ "GZIP": "number",
+ "Gunzip": "function",
+ "Gzip": "function",
+ "INFLATE": "number",
+ "INFLATERAW": "number",
+ "Inflate": "function",
+ "InflateRaw": "function",
+ "UNZIP": "number",
+ "Unzip": "function",
+ "ZLIB_VERNUM": "number",
+ "ZSTD_COMPRESS": "number",
+ "ZSTD_DECODE": "number",
+ "ZSTD_DECOMPRESS": "number",
+ "ZSTD_ENCODE": "number",
+ "ZSTD_error_GENERIC": "number",
+ "ZSTD_error_checksum_wrong": "number",
+ "ZSTD_error_corruption_detected": "number",
+ "ZSTD_error_dictionaryCreation_failed": "number",
+ "ZSTD_error_dictionary_corrupted": "number",
+ "ZSTD_error_dictionary_wrong": "number",
+ "ZSTD_error_dstBuffer_null": "number",
+ "ZSTD_error_dstSize_tooSmall": "number",
+ "ZSTD_error_frameParameter_unsupported": "number",
+ "ZSTD_error_frameParameter_windowTooLarge": "number",
+ "ZSTD_error_init_missing": "number",
+ "ZSTD_error_literals_headerWrong": "number",
+ "ZSTD_error_maxSymbolValue_tooLarge": "number",
+ "ZSTD_error_maxSymbolValue_tooSmall": "number",
+ "ZSTD_error_memory_allocation": "number",
+ "ZSTD_error_noForwardProgress_destFull": "number",
+ "ZSTD_error_noForwardProgress_inputEmpty": "number",
+ "ZSTD_error_no_error": "number",
+ "ZSTD_error_parameter_combination_unsupported": "number",
+ "ZSTD_error_parameter_outOfBound": "number",
+ "ZSTD_error_parameter_unsupported": "number",
+ "ZSTD_error_prefix_unknown": "number",
+ "ZSTD_error_srcSize_wrong": "number",
+ "ZSTD_error_stabilityCondition_notRespected": "number",
+ "ZSTD_error_stage_wrong": "number",
+ "ZSTD_error_tableLog_tooLarge": "number",
+ "ZSTD_error_version_unsupported": "number",
+ "ZSTD_error_workSpace_tooSmall": "number",
+ "Z_BEST_COMPRESSION": "number",
+ "Z_BEST_SPEED": "number",
+ "Z_BLOCK": "number",
+ "Z_BUF_ERROR": "number",
+ "Z_DATA_ERROR": "number",
+ "Z_DEFAULT_CHUNK": "number",
+ "Z_DEFAULT_COMPRESSION": "number",
+ "Z_DEFAULT_LEVEL": "number",
+ "Z_DEFAULT_MEMLEVEL": "number",
+ "Z_DEFAULT_STRATEGY": "number",
+ "Z_DEFAULT_WINDOWBITS": "number",
+ "Z_ERRNO": "number",
+ "Z_FILTERED": "number",
+ "Z_FINISH": "number",
+ "Z_FIXED": "number",
+ "Z_FULL_FLUSH": "number",
+ "Z_HUFFMAN_ONLY": "number",
+ "Z_MAX_CHUNK": "number",
+ "Z_MAX_LEVEL": "number",
+ "Z_MAX_MEMLEVEL": "number",
+ "Z_MAX_WINDOWBITS": "number",
+ "Z_MEM_ERROR": "number",
+ "Z_MIN_CHUNK": "number",
+ "Z_MIN_LEVEL": "number",
+ "Z_MIN_MEMLEVEL": "number",
+ "Z_MIN_WINDOWBITS": "number",
+ "Z_NEED_DICT": "number",
+ "Z_NO_COMPRESSION": "number",
+ "Z_NO_FLUSH": "number",
+ "Z_OK": "number",
+ "Z_PARTIAL_FLUSH": "number",
+ "Z_RLE": "number",
+ "Z_STREAM_END": "number",
+ "Z_STREAM_ERROR": "number",
+ "Z_SYNC_FLUSH": "number",
+ "Z_VERSION_ERROR": "number",
+ "ZstdCompress": "function",
+ "ZstdDecompress": "function",
+ "brotliCompress": "function",
+ "brotliCompressSync": "function",
+ "brotliDecompress": "function",
+ "brotliDecompressSync": "function",
+ "codes": "object",
+ "constants": "object",
+ "crc32": "function",
+ "createBrotliCompress": "function",
+ "createBrotliDecompress": "function",
+ "createDeflate": "function",
+ "createDeflateRaw": "function",
+ "createGunzip": "function",
+ "createGzip": "function",
+ "createInflate": "function",
+ "createInflateRaw": "function",
+ "createUnzip": "function",
+ "createZstdCompress": "function",
+ "createZstdDecompress": "function",
+ "default": "object",
+ "deflate": "function",
+ "deflateRaw": "function",
+ "deflateRawSync": "function",
+ "deflateSync": "function",
+ "gunzip": "function",
+ "gunzipSync": "function",
+ "gzip": "function",
+ "gzipSync": "function",
+ "inflate": "function",
+ "inflateRaw": "function",
+ "inflateRawSync": "function",
+ "inflateSync": "function",
+ "unzip": "function",
+ "unzipSync": "function",
+ "zstdCompress": "function",
+ "zstdCompressSync": "function",
+ "zstdDecompress": "function",
+ "zstdDecompressSync": "function"
+ }
+ }
+}
diff --git a/demo/tests/experiments/node-compat/inventory.test.mjs b/demo/tests/experiments/node-compat/inventory.test.mjs
index 382d8d1c..01915941 100644
--- a/demo/tests/experiments/node-compat/inventory.test.mjs
+++ b/demo/tests/experiments/node-compat/inventory.test.mjs
@@ -73,6 +73,25 @@ export default { async fetch() {
),
expected.modules,
);
+ const expectedTypes = JSON.parse(
+ await readFile(new URL("export-types.json", import.meta.url), "utf8"),
+ );
+ assert.deepEqual(expectedTypes.profile, profile);
+ assert.equal(expectedTypes.sourceRevision, expected.sourceRevision);
+ assert.deepEqual(
+ Object.fromEntries(
+ Object.entries(result.modules).map(([name, value]) => [
+ name,
+ Object.fromEntries(
+ Object.entries(value.exports ?? {}).map(([key, descriptor]) => [
+ key,
+ descriptor.type,
+ ]),
+ ),
+ ]),
+ ),
+ expectedTypes.modules,
+ );
await mkdir(artifacts, { recursive: true });
await writeFile(
new URL("workerd.json", artifacts),
diff --git a/demo/tests/experiments/node-compat/local-surface.test.mjs b/demo/tests/experiments/node-compat/local-surface.test.mjs
index 47e69609..9cc72127 100644
--- a/demo/tests/experiments/node-compat/local-surface.test.mjs
+++ b/demo/tests/experiments/node-compat/local-surface.test.mjs
@@ -12,6 +12,14 @@ import { workersCapacitorPlugin } from "../../../../dist/tooling/esbuild.js";
const baseline = JSON.parse(
await readFile(new URL("exports.json", import.meta.url), "utf8"),
);
+const baselineTypes = JSON.parse(
+ await readFile(new URL("export-types.json", import.meta.url), "utf8"),
+);
+assert.deepEqual(baselineTypes.profile, baseline.profile);
+assert.deepEqual(
+ Object.keys(baselineTypes.modules).sort(),
+ Object.keys(baseline.modules).sort(),
+);
const root = fileURLToPath(new URL("../../../..", import.meta.url));
const artifacts = new URL(
"../../../artifacts/node-inventory/",
@@ -65,9 +73,15 @@ for (const [engine, browserType] of Object.entries({ chromium, webkit })) {
// Each module gets a fresh realm: import-time side effects cannot make
// a later module appear supported or mask its initialization failure.
const page = await browser.newPage();
+ // Capacitor runs in a secure context; Web Crypto is unavailable on
+ // opaque about:blank origins in the browser harness.
+ await page.route("http://localhost/", (route) =>
+ route.fulfill({ contentType: "text/html", body: "" }),
+ );
+ await page.goto("http://localhost/");
try {
modules[name] = await page.evaluate(
- async ({ source, expected }) => {
+ async ({ source, expected, expectedTypes }) => {
const url = URL.createObjectURL(
new Blob([source], { type: "text/javascript" }),
);
@@ -76,6 +90,17 @@ for (const [engine, browserType] of Object.entries({ chromium, webkit })) {
const names = Object.keys(surface).sort();
return {
surface,
+ typeMismatches: Object.fromEntries(
+ Object.entries(surface)
+ .filter(
+ ([key, value]) =>
+ key in expectedTypes && value !== expectedTypes[key],
+ )
+ .map(([key, actual]) => [
+ key,
+ { expected: expectedTypes[key], actual },
+ ]),
+ ),
missing: expected.filter((name) => !names.includes(name)),
extra: names.filter((name) => !expected.includes(name)),
};
@@ -87,7 +112,11 @@ for (const [engine, browserType] of Object.entries({ chromium, webkit })) {
URL.revokeObjectURL(url);
}
},
- { source: compiled.source, expected: baseline.modules[name] },
+ {
+ source: compiled.source,
+ expected: baseline.modules[name],
+ expectedTypes: baselineTypes.modules[name],
+ },
);
} finally {
await page.close();
@@ -110,7 +139,8 @@ for (const [engine, browserType] of Object.entries({ chromium, webkit })) {
result.buildError ||
result.evaluationError ||
result.missing?.length ||
- result.extra?.length,
+ result.extra?.length ||
+ Object.keys(result.typeMismatches ?? {}).length,
);
for (const [name, result] of gaps) {
assertNoSurfaceRegression(
@@ -124,7 +154,7 @@ for (const [engine, browserType] of Object.entries({ chromium, webkit })) {
);
for (const [name, result] of gaps)
console.log(
- `${name}: ${JSON.stringify(result.buildError ?? result.evaluationError ?? { missing: result.missing, extra: result.extra })}`,
+ `${name}: ${JSON.stringify(result.buildError ?? result.evaluationError ?? { missing: result.missing, extra: result.extra, typeMismatches: result.typeMismatches })}`,
);
});
}
diff --git a/demo/tests/experiments/node-compat/prepare-events-consumer.mjs b/demo/tests/experiments/node-compat/prepare-events-consumer.mjs
new file mode 100644
index 00000000..4b686b42
--- /dev/null
+++ b/demo/tests/experiments/node-compat/prepare-events-consumer.mjs
@@ -0,0 +1,68 @@
+import { execFileSync } from "node:child_process";
+import { mkdir, mkdtemp, readFile, readdir, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { createHash } from "node:crypto";
+import assert from "node:assert/strict";
+const root = fileURLToPath(new URL("../../../..", import.meta.url));
+const output = new URL("../../../artifacts/node-inventory/", import.meta.url);
+await mkdir(output, { recursive: true });
+await mkdir(join(root, "artifacts/npm"), { recursive: true });
+const packed = await mkdtemp(join(root, "artifacts/npm/events-"));
+execFileSync("npm", ["pack", "--pack-destination", packed], {
+ cwd: root,
+ stdio: "inherit",
+});
+const archives = (await readdir(packed)).filter((name) =>
+ name.endsWith(".tgz"),
+);
+assert.equal(archives.length, 1);
+const tarball = join(packed, archives[0]);
+const tarballSha256 = createHash("sha256")
+ .update(await readFile(tarball))
+ .digest("hex");
+// A real installed package prevents Xcode recursively traversing the checkout
+// through a local npm link and also tests the distributable's runtime files.
+const consumer = await mkdtemp(join(tmpdir(), "cf-events-consumer-"));
+const dependencies = { "cf-workers-native": "file:" + tarball };
+for (const name of [
+ "@capacitor/core",
+ "@capacitor/ios",
+ "@capacitor/android",
+ "@capacitor/cli",
+ "typescript",
+]) {
+ dependencies[name] = JSON.parse(
+ await readFile(
+ join(root, "demo/node_modules", name, "package.json"),
+ "utf8",
+ ),
+ ).version;
+}
+await writeFile(
+ join(consumer, "package.json"),
+ JSON.stringify(
+ { name: "events-acceptance", private: true, type: "module", dependencies },
+ null,
+ 2,
+ ) + "\n",
+);
+await writeFile(
+ join(consumer, "capacitor.config.ts"),
+ 'export default {appId:"jp.rdlabo.workerscapacitor.events",appName:"Events acceptance",webDir:"www"};\n',
+);
+await mkdir(join(consumer, "www"));
+await writeFile(
+ join(consumer, "www/index.html"),
+ "Events acceptance",
+);
+execFileSync("npm", ["install", "--no-audit", "--no-fund"], {
+ cwd: consumer,
+ stdio: "inherit",
+});
+await writeFile(
+ new URL("events-consumer.json", output),
+ JSON.stringify({ consumer, tarball, tarballSha256 }, null, 2) + "\n",
+);
+console.log(`Prepared independent events consumer: ${consumer}`);
diff --git a/demo/tests/experiments/node-compat/surface-gaps.json b/demo/tests/experiments/node-compat/surface-gaps.json
index ef479926..a3436104 100644
--- a/demo/tests/experiments/node-compat/surface-gaps.json
+++ b/demo/tests/experiments/node-compat/surface-gaps.json
@@ -1,319 +1,33 @@
{
"common": {
- "_http_common": {
- "buildError": ["Could not resolve \"node:_http_common\""]
- },
- "_http_outgoing": {
- "buildError": ["Could not resolve \"node:_http_outgoing\""]
- },
- "_http_server": {
- "buildError": ["Could not resolve \"node:_http_server\""]
- },
- "_stream_wrap": {
- "buildError": ["Could not resolve \"node:_stream_wrap\""]
- },
- "_tls_wrap": {
- "buildError": ["Could not resolve \"node:_tls_wrap\""]
- },
"crypto": {
- "missing": [
- "Certificate",
- "CryptoKey",
- "ECDH",
- "KeyObject",
- "PrivateKeyObject",
- "PublicKeyObject",
- "SecretKeyObject",
- "X509Certificate",
- "checkPrime",
- "checkPrimeSync",
- "createPrivateKey",
- "createPublicKey",
- "createSecretKey",
- "diffieHellman",
- "fips",
- "generateKey",
- "generateKeyPair",
- "generateKeyPairSync",
- "generateKeySync",
- "generatePrime",
- "generatePrimeSync",
- "getCipherInfo",
- "getCurves",
- "getFips",
- "getRandomValues",
- "hash",
- "hkdf",
- "hkdfSync",
- "randomInt",
- "randomUUID",
- "scrypt",
- "scryptSync",
- "secureHeapUsed",
- "setEngine",
- "setFips",
- "sign",
- "subtle",
- "timingSafeEqual",
- "verify",
- "webcrypto"
- ],
+ "missing": ["fips", "getCipherInfo", "getFips"],
"extra": ["createCredentials", "listCiphers", "prng", "rng"]
},
- "events": {
- "missing": [
- "EventEmitterAsyncResource",
- "addAbortListener",
- "captureRejectionSymbol",
- "captureRejections",
- "errorMonitor",
- "getEventListeners",
- "getMaxListeners",
- "on",
- "setMaxListeners",
- "usingDomains"
- ],
- "extra": ["init"]
- },
"fs": {
- "missing": [
- "Dir",
- "Dirent",
- "F_OK",
- "FileReadStream",
- "FileWriteStream",
- "R_OK",
- "ReadStream",
- "W_OK",
- "WriteStream",
- "X_OK",
- "access",
- "accessSync",
- "appendFile",
- "appendFileSync",
- "chmod",
- "chmodSync",
- "chown",
- "chownSync",
- "close",
- "closeSync",
- "copyFile",
- "copyFileSync",
- "cp",
- "cpSync",
- "createReadStream",
- "createWriteStream",
- "exists",
- "fchmod",
- "fchmodSync",
- "fchown",
- "fchownSync",
- "fdatasync",
- "fdatasyncSync",
- "fstat",
- "fstatSync",
- "fsync",
- "fsyncSync",
- "ftruncate",
- "ftruncateSync",
- "futimes",
- "futimesSync",
- "glob",
- "globSync",
- "lchmod",
- "lchmodSync",
- "lchown",
- "lchownSync",
- "link",
- "linkSync",
- "lstat",
- "lutimes",
- "lutimesSync",
- "mkdir",
- "mkdirSync",
- "mkdtemp",
- "mkdtempSync",
- "open",
- "openAsBlob",
- "openSync",
- "opendir",
- "opendirSync",
- "read",
- "readSync",
- "readlink",
- "readlinkSync",
- "readv",
- "readvSync",
- "realpath",
- "rename",
- "renameSync",
- "rm",
- "rmSync",
- "rmdir",
- "rmdirSync",
- "statfs",
- "statfsSync",
- "symlink",
- "symlinkSync",
- "truncate",
- "truncateSync",
- "unlink",
- "unlinkSync",
- "unwatchFile",
- "utimes",
- "utimesSync",
- "watch",
- "watchFile",
- "write",
- "writeFile",
- "writeFileSync",
- "writeSync",
- "writev",
- "writevSync"
- ],
"extra": ["clearVirtualFiles", "registerVirtualFiles"]
},
- "fs/promises": {
- "missing": [
- "FileHandle",
- "appendFile",
- "chmod",
- "chown",
- "constants",
- "copyFile",
- "cp",
- "glob",
- "lchmod",
- "lchown",
- "link",
- "lutimes",
- "mkdir",
- "mkdtemp",
- "open",
- "opendir",
- "readlink",
- "rename",
- "rm",
- "rmdir",
- "statfs",
- "symlink",
- "truncate",
- "unlink",
- "utimes",
- "watch",
- "writeFile"
- ],
- "extra": []
- },
- "module": {
- "missing": [
- "Module",
- "SourceMap",
- "_cache",
- "_debug",
- "_extensions",
- "_findPath",
- "_initPaths",
- "_load",
- "_nodeModulePaths",
- "_pathCache",
- "_preloadModules",
- "_resolveFilename",
- "_resolveLookupPaths",
- "constants",
- "enableCompileCache",
- "findPackageJSON",
- "findSourceMap",
- "flushCompileCache",
- "getCompileCacheDir",
- "getSourceMapsSupport",
- "globalPaths",
- "register",
- "registerHooks",
- "runMain",
- "setSourceMapsSupport",
- "stripTypeScriptTypes",
- "syncBuiltinESMExports",
- "wrap"
- ],
- "extra": []
- },
"process": {
"missing": [
"_channel",
- "_debugEnd",
- "_debugProcess",
"_disconnect",
"_events",
"_eventsCount",
- "_exiting",
- "_fatalException",
- "_getActiveHandles",
- "_getActiveRequests",
"_handleQueue",
- "_kill",
- "_linkedBinding",
"_maxListeners",
"_pendingMessage",
- "_preload_modules",
- "_rawDebug",
"_send",
- "_startProfilerIdleNotifier",
- "_stopProfilerIdleNotifier",
- "_tickCallback",
"abort",
- "allowedNodeEnvironmentFlags",
"argv0",
"assert",
- "availableMemory",
- "channel",
"config",
- "connected",
- "constrainedMemory",
- "cpuUsage",
- "debugPort",
- "domain",
- "execve",
- "exitCode",
- "finalization",
- "getActiveResourcesInfo",
"getBuiltinModule",
- "getSourceMapsSupport",
- "getegid",
- "geteuid",
- "getgid",
- "getgroups",
- "getuid",
- "hasUncaughtExceptionCaptureCallback",
- "hrtime",
- "initgroups",
"loadEnvFile",
- "moduleLoadList",
- "noDeprecation",
"openStdin",
- "permission",
- "ppid",
"reallyExit",
- "ref",
- "release",
- "report",
- "resourceUsage",
- "send",
- "setSourceMapsEnabled",
- "setUncaughtExceptionCaptureCallback",
- "setegid",
- "seteuid",
- "setgid",
- "setgroups",
- "setuid",
- "sourceMapsEnabled",
"stderr",
"stdin",
- "stdout",
- "threadCpuUsage",
- "throwDeprecation",
- "traceDeprecation",
- "unref"
+ "stdout"
],
"extra": [
"addListener",
@@ -331,206 +45,15 @@
]
},
"sys": {
- "missing": [
- "MIMEParams",
- "MIMEType",
- "TextDecoder",
- "TextEncoder",
- "_errnoException",
- "_exceptionWithHostPort",
- "aborted",
- "debug",
- "formatWithOptions",
- "getCallSite",
- "getCallSites",
- "getSystemErrorMap",
- "getSystemErrorMessage",
- "getSystemErrorName",
- "isDeepStrictEqual",
- "parseArgs",
- "parseEnv",
- "stripVTControlCharacters",
- "styleText",
- "toUSVString",
- "transferableAbortController",
- "transferableAbortSignal"
- ],
- "extra": []
- },
- "tls": {
- "missing": [
- "CLIENT_RENEG_LIMIT",
- "CLIENT_RENEG_WINDOW",
- "DEFAULT_CIPHERS",
- "DEFAULT_ECDH_CURVE",
- "DEFAULT_MAX_VERSION",
- "DEFAULT_MIN_VERSION",
- "SecureContext",
- "Server",
- "TLSSocket",
- "checkServerIdentity",
- "connect",
- "convertALPNProtocols",
- "createSecureContext",
- "createSecurePair",
- "createServer",
- "getCiphers",
- "rootCertificates"
- ],
- "extra": []
+ "missing": ["getCallSite", "getCallSites"]
},
"util": {
- "missing": ["getCallSite", "getCallSites"],
- "extra": []
- },
- "util/types": {
- "buildError": ["Could not resolve \"node:util/types\""]
+ "missing": ["getCallSite", "getCallSites"]
},
"zlib": {
- "missing": [
- "BROTLI_DECODE",
- "BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES",
- "BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP",
- "BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES",
- "BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1",
- "BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2",
- "BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS",
- "BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET",
- "BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1",
- "BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2",
- "BROTLI_DECODER_ERROR_FORMAT_CL_SPACE",
- "BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT",
- "BROTLI_DECODER_ERROR_FORMAT_DICTIONARY",
- "BROTLI_DECODER_ERROR_FORMAT_DISTANCE",
- "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE",
- "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE",
- "BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE",
- "BROTLI_DECODER_ERROR_FORMAT_PADDING_1",
- "BROTLI_DECODER_ERROR_FORMAT_PADDING_2",
- "BROTLI_DECODER_ERROR_FORMAT_RESERVED",
- "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET",
- "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME",
- "BROTLI_DECODER_ERROR_FORMAT_TRANSFORM",
- "BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS",
- "BROTLI_DECODER_ERROR_INVALID_ARGUMENTS",
- "BROTLI_DECODER_ERROR_UNREACHABLE",
- "BROTLI_DECODER_NEEDS_MORE_INPUT",
- "BROTLI_DECODER_NEEDS_MORE_OUTPUT",
- "BROTLI_DECODER_NO_ERROR",
- "BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION",
- "BROTLI_DECODER_PARAM_LARGE_WINDOW",
- "BROTLI_DECODER_RESULT_ERROR",
- "BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT",
- "BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT",
- "BROTLI_DECODER_RESULT_SUCCESS",
- "BROTLI_DECODER_SUCCESS",
- "BROTLI_DEFAULT_MODE",
- "BROTLI_DEFAULT_QUALITY",
- "BROTLI_DEFAULT_WINDOW",
- "BROTLI_ENCODE",
- "BROTLI_LARGE_MAX_WINDOW_BITS",
- "BROTLI_MAX_INPUT_BLOCK_BITS",
- "BROTLI_MAX_QUALITY",
- "BROTLI_MAX_WINDOW_BITS",
- "BROTLI_MIN_INPUT_BLOCK_BITS",
- "BROTLI_MIN_QUALITY",
- "BROTLI_MIN_WINDOW_BITS",
- "BROTLI_MODE_FONT",
- "BROTLI_MODE_GENERIC",
- "BROTLI_MODE_TEXT",
- "BROTLI_OPERATION_EMIT_METADATA",
- "BROTLI_OPERATION_FINISH",
- "BROTLI_OPERATION_FLUSH",
- "BROTLI_OPERATION_PROCESS",
- "BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING",
- "BROTLI_PARAM_LARGE_WINDOW",
- "BROTLI_PARAM_LGBLOCK",
- "BROTLI_PARAM_LGWIN",
- "BROTLI_PARAM_MODE",
- "BROTLI_PARAM_NDIRECT",
- "BROTLI_PARAM_NPOSTFIX",
- "BROTLI_PARAM_QUALITY",
- "BrotliCompress",
- "BrotliDecompress",
- "DEFLATE",
- "DEFLATERAW",
- "GUNZIP",
- "GZIP",
- "INFLATE",
- "INFLATERAW",
- "UNZIP",
- "ZLIB_VERNUM",
- "ZSTD_COMPRESS",
- "ZSTD_DECODE",
- "ZSTD_DECOMPRESS",
- "ZSTD_ENCODE",
- "ZSTD_error_GENERIC",
- "ZSTD_error_checksum_wrong",
- "ZSTD_error_corruption_detected",
- "ZSTD_error_dictionaryCreation_failed",
- "ZSTD_error_dictionary_corrupted",
- "ZSTD_error_dictionary_wrong",
- "ZSTD_error_dstBuffer_null",
- "ZSTD_error_dstSize_tooSmall",
- "ZSTD_error_frameParameter_unsupported",
- "ZSTD_error_frameParameter_windowTooLarge",
- "ZSTD_error_init_missing",
- "ZSTD_error_literals_headerWrong",
- "ZSTD_error_maxSymbolValue_tooLarge",
- "ZSTD_error_maxSymbolValue_tooSmall",
- "ZSTD_error_memory_allocation",
- "ZSTD_error_noForwardProgress_destFull",
- "ZSTD_error_noForwardProgress_inputEmpty",
- "ZSTD_error_no_error",
- "ZSTD_error_parameter_combination_unsupported",
- "ZSTD_error_parameter_outOfBound",
- "ZSTD_error_parameter_unsupported",
- "ZSTD_error_prefix_unknown",
- "ZSTD_error_srcSize_wrong",
- "ZSTD_error_stabilityCondition_notRespected",
- "ZSTD_error_stage_wrong",
- "ZSTD_error_tableLog_tooLarge",
- "ZSTD_error_version_unsupported",
- "ZSTD_error_workSpace_tooSmall",
- "Z_MEM_ERROR",
- "Z_VERSION_ERROR",
- "ZstdCompress",
- "ZstdDecompress",
- "brotliCompress",
- "brotliCompressSync",
- "brotliDecompress",
- "brotliDecompressSync",
- "constants",
- "crc32",
- "createBrotliCompress",
- "createBrotliDecompress",
- "createZstdCompress",
- "createZstdDecompress",
- "zstdCompress",
- "zstdCompressSync",
- "zstdDecompress",
- "zstdDecompressSync"
- ],
- "extra": [
- "Z_BINARY",
- "Z_DEFLATED",
- "Z_TEXT",
- "Z_TREES",
- "Z_UNKNOWN",
- "Zlib"
- ]
- }
- },
- "chromium": {
- "console": {
- "missing": ["Console"],
- "extra": ["memory"]
+ "extra": ["Zlib"]
}
},
- "webkit": {
- "console": {
- "missing": ["Console", "context", "createTask"],
- "extra": ["record", "recordEnd", "screenshot", "takeHeapSnapshot"]
- }
- }
+ "chromium": {},
+ "webkit": {}
}
diff --git a/demo/tests/experiments/node-compat/surface-regressions.mjs b/demo/tests/experiments/node-compat/surface-regressions.mjs
index 8edc7ef1..2681d4ae 100644
--- a/demo/tests/experiments/node-compat/surface-regressions.mjs
+++ b/demo/tests/experiments/node-compat/surface-regressions.mjs
@@ -7,6 +7,11 @@ export function assertNoSurfaceRegression(name, actual, known = {}) {
if (actual[stage])
assert.deepEqual(actual[stage], known[stage], `${name}: new ${stage}`);
}
+ assert.deepEqual(
+ actual.typeMismatches ?? {},
+ known.typeMismatches ?? {},
+ `${name}: changed export types`,
+ );
// Previously unimportable modules may become importable incrementally.
if (known.buildError || known.evaluationError) return;
for (const kind of ["missing", "extra"]) {
diff --git a/demo/tests/experiments/node-compat/surface-regressions.test.mjs b/demo/tests/experiments/node-compat/surface-regressions.test.mjs
index 734a56f3..3c70e12f 100644
--- a/demo/tests/experiments/node-compat/surface-regressions.test.mjs
+++ b/demo/tests/experiments/node-compat/surface-regressions.test.mjs
@@ -24,3 +24,17 @@ test("import failure debt permits recovery but not a different failure", () => {
check("module", { evaluationError: { name: "TypeError" } }, debt),
);
});
+
+test("an export retaining its name but losing callability is a regression", () => {
+ const broken = {
+ missing: [],
+ extra: [],
+ typeMismatches: { once: { expected: "function", actual: "object" } },
+ };
+ assert.throws(() => check("events", broken), /changed export types/);
+ assert.throws(
+ () => check("events", broken, { buildError: ["unresolved"] }),
+ /changed export types/,
+ );
+ check("events", { missing: [], extra: [], typeMismatches: {} });
+});
diff --git a/demo/tests/fixtures/workers/compatibility/node.ts b/demo/tests/fixtures/workers/compatibility/node.ts
index 84f25597..d95c443d 100644
--- a/demo/tests/fixtures/workers/compatibility/node.ts
+++ b/demo/tests/fixtures/workers/compatibility/node.ts
@@ -1,4 +1,4 @@
-import { runNodeContracts } from "../../../validation/node-modules";
+import { runNodeContracts } from "../../../validation/node-modules-core";
// Real workerd resolves the node:* imports in node-modules to its builtins;
// the same contract suites then run as in the browser profiles.
diff --git a/demo/tests/validation/node-modules-core.ts b/demo/tests/validation/node-modules-core.ts
new file mode 100644
index 00000000..df6c0e44
--- /dev/null
+++ b/demo/tests/validation/node-modules-core.ts
@@ -0,0 +1,293 @@
+import {
+ TlsCommonImplementation,
+ tlsCommonContract,
+} from "../../../src/compat/tls-common/index.spec";
+import {
+ AssertImplementation,
+ assertContract,
+} from "../../../src/compat/assert/index.spec";
+import {
+ PerfHooksImplementation,
+ perfHooksContract,
+} from "../../../src/compat/perf-hooks/index.spec";
+import {
+ TraceEventsImplementation,
+ traceEventsContract,
+} from "../../../src/compat/trace-events/index.spec";
+import {
+ DnsImplementation,
+ dnsContract,
+} from "../../../src/compat/dns/index.spec";
+import {
+ StreamConsumersImplementation,
+ streamConsumersContract,
+} from "../../../src/compat/stream-consumers/index.spec";
+import {
+ HttpsImplementation,
+ httpsContract,
+} from "../../../src/compat/https/index.spec";
+import {
+ WorkerThreadsImplementation,
+ workerThreadsContract,
+} from "../../../src/compat/worker-threads/index.spec";
+import {
+ HttpAgentImplementation,
+ httpAgentContract,
+} from "../../../src/compat/http-agent/index.spec";
+import {
+ HttpClientImplementation,
+ httpClientContract,
+} from "../../../src/compat/http-client/index.spec";
+import {
+ HttpIncomingImplementation,
+ httpIncomingContract,
+} from "../../../src/compat/http-incoming/index.spec";
+import {
+ TestImplementation,
+ testContract,
+} from "../../../src/compat/test/index.spec";
+import {
+ SqliteImplementation,
+ sqliteContract,
+} from "../../../src/compat/sqlite/index.spec";
+import {
+ ReplImplementation,
+ replContract,
+} from "../../../src/compat/repl/index.spec";
+import {
+ DomainImplementation,
+ domainContract,
+} from "../../../src/compat/domain/index.spec";
+import {
+ ReadlineImplementation,
+ readlineContract,
+} from "../../../src/compat/readline/index.spec";
+import {
+ InspectorImplementation,
+ inspectorContract,
+} from "../../../src/compat/inspector/index.spec";
+import {
+ ChildProcessImplementation,
+ childProcessContract,
+} from "../../../src/compat/child-process/index.spec";
+import {
+ ClusterImplementation,
+ clusterContract,
+} from "../../../src/compat/cluster/index.spec";
+import {
+ DgramImplementation,
+ dgramContract,
+} from "../../../src/compat/dgram/index.spec";
+import {
+ VmImplementation,
+ vmContract,
+} from "../../../src/compat/vm/index.spec";
+import {
+ Http2Implementation,
+ http2Contract,
+} from "../../../src/compat/http2/index.spec";
+import {
+ ConstantsImplementation,
+ constantsContract,
+} from "../../../src/compat/constants/index.spec";
+import {
+ V8Implementation,
+ v8Contract,
+} from "../../../src/compat/v8/index.spec";
+import {
+ PunycodeImplementation,
+ punycodeContract,
+} from "../../../src/compat/punycode/index.spec";
+import {
+ WasiImplementation,
+ wasiContract,
+} from "../../../src/compat/wasi/index.spec";
+import {
+ OsImplementation,
+ osContract,
+} from "../../../src/compat/os/index.spec";
+import {
+ TtyImplementation,
+ ttyContract,
+} from "../../../src/compat/tty/index.spec";
+import {
+ UrlImplementation,
+ urlContract,
+} from "../../../src/compat/url/index.spec";
+import {
+ AsyncHooksImplementation,
+ asyncHooksContract,
+ NodeAsyncHooks,
+} from "../../../src/compat/async-hooks/index.spec";
+import {
+ TimersImplementation,
+ timersContract,
+} from "../../../src/compat/timers/index.spec";
+// Environment runners share module-owned contracts. The node:* imports in
+// their implementations resolve to workerd builtins or the bundler aliases.
+import {
+ runContract,
+ type ContractResult,
+} from "cf-workers-native/shared/contracts/testing";
+import {
+ nodePathContract,
+ BundledPath,
+ WorkersPath,
+} from "../../../src/compat/path/index.spec";
+import {
+ nodeQuerystringContract,
+ NodeQuerystring,
+} from "../../../src/compat/querystring/index.spec";
+import {
+ nodeStringDecoderContract,
+ NodeStringDecoder,
+} from "../../../src/compat/string-decoder/index.spec";
+
+import {
+ nodeHttpContract,
+ NodeHttp,
+} from "../../../src/compat/http/index.spec";
+
+import {
+ NetImplementation,
+ netContract,
+} from "../../../src/compat/net/index.spec";
+
+import {
+ BufferImplementation,
+ bufferContract,
+} from "../../../src/compat/buffer/index.spec";
+
+import {
+ StreamWebImplementation,
+ streamWebContract,
+} from "../../../src/compat/stream-web/index.spec";
+import {
+ UtilImplementation,
+ utilContract,
+} from "../../../src/compat/util/index.spec";
+
+import {
+ DiagnosticsImplementation,
+ diagnosticsContract,
+} from "../../../src/compat/diagnostics-channel/index.spec";
+
+import {
+ StreamImplementation,
+ streamContract,
+} from "../../../src/compat/stream/index.spec";
+
+import {
+ StreamReadableImplementation,
+ streamReadableContract,
+} from "../../../src/compat/stream-readable/index.spec";
+import {
+ StreamWritableImplementation,
+ streamWritableContract,
+} from "../../../src/compat/stream-writable/index.spec";
+import {
+ StreamDuplexImplementation,
+ streamDuplexContract,
+} from "../../../src/compat/stream-duplex/index.spec";
+import {
+ StreamTransformImplementation,
+ streamTransformContract,
+} from "../../../src/compat/stream-transform/index.spec";
+import {
+ StreamPassthroughImplementation,
+ streamPassthroughContract,
+} from "../../../src/compat/stream-passthrough/index.spec";
+import {
+ StreamPromisesImplementation,
+ streamPromisesContract,
+} from "../../../src/compat/stream-promises/index.spec";
+
+export type NodeProfile = "bundled" | "workers";
+
+export async function runNodeContracts(
+ profile: NodeProfile,
+ localSetup: () => Promise = async () => [],
+): Promise {
+ const reports = [
+ await runContract(
+ profile === "workers" ? new WorkersPath() : new BundledPath(),
+ nodePathContract,
+ ),
+ await runContract(new NodeQuerystring(), nodeQuerystringContract),
+ await runContract(new NodeStringDecoder(), nodeStringDecoderContract),
+ await runContract(new NodeHttp(), nodeHttpContract),
+ await runContract(new NetImplementation(), netContract),
+ await runContract(new BufferImplementation(), bufferContract),
+ await runContract(new TimersImplementation(), timersContract),
+ await runContract(new UrlImplementation(), urlContract),
+ await runContract(new OsImplementation(), osContract),
+ await runContract(new TtyImplementation(), ttyContract),
+ await runContract(new WasiImplementation(), wasiContract),
+ await runContract(new PunycodeImplementation(), punycodeContract),
+ await runContract(new ConstantsImplementation(), constantsContract),
+ await runContract(new V8Implementation(), v8Contract),
+ await runContract(new Http2Implementation(), http2Contract),
+ await runContract(new VmImplementation(), vmContract),
+ await runContract(new ChildProcessImplementation(), childProcessContract),
+ await runContract(new ClusterImplementation(), clusterContract),
+ await runContract(new DgramImplementation(), dgramContract),
+ await runContract(new InspectorImplementation(), inspectorContract),
+ await runContract(new DomainImplementation(), domainContract),
+ await runContract(new ReadlineImplementation(), readlineContract),
+ await runContract(new ReplImplementation(), replContract),
+ await runContract(new SqliteImplementation(), sqliteContract),
+ await runContract(new TestImplementation(), testContract),
+ await runContract(
+ new StreamConsumersImplementation(),
+ streamConsumersContract,
+ ),
+ await runContract(new AssertImplementation(), assertContract),
+ await runContract(new PerfHooksImplementation(), perfHooksContract),
+ await runContract(new TraceEventsImplementation(), traceEventsContract),
+ await runContract(new DnsImplementation(), dnsContract),
+ await runContract(new HttpsImplementation(), httpsContract),
+ await runContract(new WorkerThreadsImplementation(), workerThreadsContract),
+ await runContract(new HttpAgentImplementation(), httpAgentContract),
+ await runContract(new HttpClientImplementation(), httpClientContract),
+ await runContract(new HttpIncomingImplementation(), httpIncomingContract),
+ await runContract(new TlsCommonImplementation(), tlsCommonContract),
+ ];
+ reports.push(
+ await runContract(
+ profile === "workers"
+ ? new NodeAsyncHooks()
+ : new AsyncHooksImplementation(),
+ asyncHooksContract,
+ ),
+ // These legacy VFS/module suites exercise local setup and do not yet
+ // supply equivalent workerd fixtures. Keep them as local smoke tests;
+ // never report them as successful comparisons with Workers builtins.
+ ...(profile === "workers" ? [] : await localSetup()),
+ await runContract(new StreamWebImplementation(), streamWebContract),
+ await runContract(new UtilImplementation(), utilContract),
+ await runContract(new DiagnosticsImplementation(), diagnosticsContract),
+ await runContract(new StreamImplementation(), streamContract),
+ await runContract(
+ new StreamReadableImplementation(),
+ streamReadableContract,
+ ),
+ await runContract(
+ new StreamWritableImplementation(),
+ streamWritableContract,
+ ),
+ await runContract(new StreamDuplexImplementation(), streamDuplexContract),
+ await runContract(
+ new StreamTransformImplementation(),
+ streamTransformContract,
+ ),
+ await runContract(
+ new StreamPassthroughImplementation(),
+ streamPassthroughContract,
+ ),
+ await runContract(
+ new StreamPromisesImplementation(),
+ streamPromisesContract,
+ ),
+ );
+ return reports;
+}
diff --git a/demo/tests/validation/node-modules.ts b/demo/tests/validation/node-modules.ts
index 296e2af9..cdeae954 100644
--- a/demo/tests/validation/node-modules.ts
+++ b/demo/tests/validation/node-modules.ts
@@ -1,165 +1,9 @@
-import {
- TlsCommonImplementation,
- tlsCommonContract,
-} from "../../../src/compat/tls-common/index.spec";
-import {
- AssertImplementation,
- assertContract,
-} from "../../../src/compat/assert/index.spec";
-import {
- PerfHooksImplementation,
- perfHooksContract,
-} from "../../../src/compat/perf-hooks/index.spec";
-import {
- TraceEventsImplementation,
- traceEventsContract,
-} from "../../../src/compat/trace-events/index.spec";
-import {
- DnsImplementation,
- dnsContract,
-} from "../../../src/compat/dns/index.spec";
-import {
- StreamConsumersImplementation,
- streamConsumersContract,
-} from "../../../src/compat/stream-consumers/index.spec";
-import {
- HttpsImplementation,
- httpsContract,
-} from "../../../src/compat/https/index.spec";
-import {
- WorkerThreadsImplementation,
- workerThreadsContract,
-} from "../../../src/compat/worker-threads/index.spec";
-import {
- HttpAgentImplementation,
- httpAgentContract,
-} from "../../../src/compat/http-agent/index.spec";
-import {
- HttpClientImplementation,
- httpClientContract,
-} from "../../../src/compat/http-client/index.spec";
-import {
- HttpIncomingImplementation,
- httpIncomingContract,
-} from "../../../src/compat/http-incoming/index.spec";
-import {
- TestImplementation,
- testContract,
-} from "../../../src/compat/test/index.spec";
-import {
- SqliteImplementation,
- sqliteContract,
-} from "../../../src/compat/sqlite/index.spec";
-import {
- ReplImplementation,
- replContract,
-} from "../../../src/compat/repl/index.spec";
-import {
- DomainImplementation,
- domainContract,
-} from "../../../src/compat/domain/index.spec";
-import {
- ReadlineImplementation,
- readlineContract,
-} from "../../../src/compat/readline/index.spec";
-import {
- InspectorImplementation,
- inspectorContract,
-} from "../../../src/compat/inspector/index.spec";
-import {
- ChildProcessImplementation,
- childProcessContract,
-} from "../../../src/compat/child-process/index.spec";
-import {
- ClusterImplementation,
- clusterContract,
-} from "../../../src/compat/cluster/index.spec";
-import {
- DgramImplementation,
- dgramContract,
-} from "../../../src/compat/dgram/index.spec";
-import {
- VmImplementation,
- vmContract,
-} from "../../../src/compat/vm/index.spec";
-import {
- Http2Implementation,
- http2Contract,
-} from "../../../src/compat/http2/index.spec";
-import {
- ConstantsImplementation,
- constantsContract,
-} from "../../../src/compat/constants/index.spec";
-import {
- V8Implementation,
- v8Contract,
-} from "../../../src/compat/v8/index.spec";
-import {
- PunycodeImplementation,
- punycodeContract,
-} from "../../../src/compat/punycode/index.spec";
-import {
- WasiImplementation,
- wasiContract,
-} from "../../../src/compat/wasi/index.spec";
-import {
- OsImplementation,
- osContract,
-} from "../../../src/compat/os/index.spec";
-import {
- TtyImplementation,
- ttyContract,
-} from "../../../src/compat/tty/index.spec";
-import {
- UrlImplementation,
- urlContract,
-} from "../../../src/compat/url/index.spec";
-import {
- AsyncHooksImplementation,
- asyncHooksContract,
- NodeAsyncHooks,
-} from "../../../src/compat/async-hooks/index.spec";
import { installNodeGlobals } from "../../../src/shared/globals/index";
-import {
- TimersImplementation,
- timersContract,
-} from "../../../src/compat/timers/index.spec";
import { workersCapacitor } from "cf-workers-native/capacitor";
-// Environment runners share module-owned contracts. The node:* imports in
-// their implementations resolve to workerd builtins or the bundler aliases.
import {
runContract,
type ContractResult,
} from "cf-workers-native/shared/contracts/testing";
-import {
- nodePathContract,
- BundledPath,
- WorkersPath,
-} from "../../../src/compat/path/index.spec";
-import {
- nodeQuerystringContract,
- NodeQuerystring,
-} from "../../../src/compat/querystring/index.spec";
-import {
- nodeStringDecoderContract,
- NodeStringDecoder,
-} from "../../../src/compat/string-decoder/index.spec";
-
-import {
- nodeHttpContract,
- NodeHttp,
-} from "../../../src/compat/http/index.spec";
-
-import {
- NetImplementation,
- netContract,
-} from "../../../src/compat/net/index.spec";
-
-import {
- BufferImplementation,
- bufferContract,
-} from "../../../src/compat/buffer/index.spec";
-
import {
FsImplementation,
fsContract,
@@ -169,141 +13,19 @@ import {
moduleContract,
} from "../../../src/compat/module/index.spec";
import {
- StreamWebImplementation,
- streamWebContract,
-} from "../../../src/compat/stream-web/index.spec";
-import {
- UtilImplementation,
- utilContract,
-} from "../../../src/compat/util/index.spec";
-
-import {
- DiagnosticsImplementation,
- diagnosticsContract,
-} from "../../../src/compat/diagnostics-channel/index.spec";
-
-import {
- StreamImplementation,
- streamContract,
-} from "../../../src/compat/stream/index.spec";
-
-import {
- StreamReadableImplementation,
- streamReadableContract,
-} from "../../../src/compat/stream-readable/index.spec";
-import {
- StreamWritableImplementation,
- streamWritableContract,
-} from "../../../src/compat/stream-writable/index.spec";
-import {
- StreamDuplexImplementation,
- streamDuplexContract,
-} from "../../../src/compat/stream-duplex/index.spec";
-import {
- StreamTransformImplementation,
- streamTransformContract,
-} from "../../../src/compat/stream-transform/index.spec";
-import {
- StreamPassthroughImplementation,
- streamPassthroughContract,
-} from "../../../src/compat/stream-passthrough/index.spec";
-import {
- StreamPromisesImplementation,
- streamPromisesContract,
-} from "../../../src/compat/stream-promises/index.spec";
-
-export type NodeProfile = "bundled" | "workers";
+ runNodeContracts as runSharedNodeContracts,
+ type NodeProfile,
+} from "./node-modules-core";
+// Keep local setup statically loaded for WebView bootstrap, but outside the
+// real-workerd entry's dependency graph.
export async function runNodeContracts(
profile: NodeProfile = "bundled",
): Promise {
- const reports = [
- await runContract(
- profile === "workers" ? new WorkersPath() : new BundledPath(),
- nodePathContract,
- ),
- await runContract(new NodeQuerystring(), nodeQuerystringContract),
- await runContract(new NodeStringDecoder(), nodeStringDecoderContract),
- await runContract(new NodeHttp(), nodeHttpContract),
- await runContract(new NetImplementation(), netContract),
- await runContract(new BufferImplementation(), bufferContract),
- await runContract(new TimersImplementation(), timersContract),
- await runContract(new UrlImplementation(), urlContract),
- await runContract(new OsImplementation(), osContract),
- await runContract(new TtyImplementation(), ttyContract),
- await runContract(new WasiImplementation(), wasiContract),
- await runContract(new PunycodeImplementation(), punycodeContract),
- await runContract(new ConstantsImplementation(), constantsContract),
- await runContract(new V8Implementation(), v8Contract),
- await runContract(new Http2Implementation(), http2Contract),
- await runContract(new VmImplementation(), vmContract),
- await runContract(new ChildProcessImplementation(), childProcessContract),
- await runContract(new ClusterImplementation(), clusterContract),
- await runContract(new DgramImplementation(), dgramContract),
- await runContract(new InspectorImplementation(), inspectorContract),
- await runContract(new DomainImplementation(), domainContract),
- await runContract(new ReadlineImplementation(), readlineContract),
- await runContract(new ReplImplementation(), replContract),
- await runContract(new SqliteImplementation(), sqliteContract),
- await runContract(new TestImplementation(), testContract),
- await runContract(
- new StreamConsumersImplementation(),
- streamConsumersContract,
- ),
- await runContract(new AssertImplementation(), assertContract),
- await runContract(new PerfHooksImplementation(), perfHooksContract),
- await runContract(new TraceEventsImplementation(), traceEventsContract),
- await runContract(new DnsImplementation(), dnsContract),
- await runContract(new HttpsImplementation(), httpsContract),
- await runContract(new WorkerThreadsImplementation(), workerThreadsContract),
- await runContract(new HttpAgentImplementation(), httpAgentContract),
- await runContract(new HttpClientImplementation(), httpClientContract),
- await runContract(new HttpIncomingImplementation(), httpIncomingContract),
- await runContract(new TlsCommonImplementation(), tlsCommonContract),
- ];
- reports.push(
- await runContract(
- profile === "workers"
- ? new NodeAsyncHooks()
- : new AsyncHooksImplementation(),
- asyncHooksContract,
- ),
- // These legacy VFS/module suites exercise local setup and do not yet
- // supply equivalent workerd fixtures. Keep them as local smoke tests;
- // never report them as successful comparisons with Workers builtins.
- ...(profile === "workers"
- ? []
- : [
- await runContract(new FsImplementation(), fsContract),
- await runContract(new ModuleImplementation(), moduleContract),
- ]),
- await runContract(new StreamWebImplementation(), streamWebContract),
- await runContract(new UtilImplementation(), utilContract),
- await runContract(new DiagnosticsImplementation(), diagnosticsContract),
- await runContract(new StreamImplementation(), streamContract),
- await runContract(
- new StreamReadableImplementation(),
- streamReadableContract,
- ),
- await runContract(
- new StreamWritableImplementation(),
- streamWritableContract,
- ),
- await runContract(new StreamDuplexImplementation(), streamDuplexContract),
- await runContract(
- new StreamTransformImplementation(),
- streamTransformContract,
- ),
- await runContract(
- new StreamPassthroughImplementation(),
- streamPassthroughContract,
- ),
- await runContract(
- new StreamPromisesImplementation(),
- streamPromisesContract,
- ),
- );
- return reports;
+ return runSharedNodeContracts(profile, async () => [
+ await runContract(new FsImplementation(), fsContract),
+ await runContract(new ModuleImplementation(), moduleContract),
+ ]);
}
/** Standalone host validation owns a direct runtime, even when the app prefers a Worker. */
diff --git a/docs/compatibility.md b/docs/compatibility.md
index bbb910de..1c45e204 100644
--- a/docs/compatibility.md
+++ b/docs/compatibility.md
@@ -27,21 +27,22 @@ and [verification scope](native-embedding.md#verification) for host-specific evi
Whole-Node compatibility is incomplete. Importing a module successfully does not
establish equivalent methods, validation, state, scheduling or engine behavior.
-| Area | Behavior and limitations |
-| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| path, querystring, string_decoder, buffer, timers | Implement the pinned Workers profile. Workers stubs remain stubs; for example, matchesGlob reproduces the pinned rejection. |
-| HTTP and net | In-memory server dispatch; no OS listening server. Outbound TCP/TLS requires the native transport. |
-| fs / fs/promises | Read-only embedded virtual files. Writes, FileHandle, watch and full filesystem semantics are unsupported. |
-| module | Limited builtin registry; createRequire loads VFS JSON only. Executable modules must be bundled; arbitrary dynamic resolution is unsupported. |
-| process, events, console, util / util/types / sys | Incomplete exports or behavior. Engine introspection, call sites and host-object detection are not equivalent to Workers. |
-| crypto and zlib | Dependency implementations do not cover all Workers exports, algorithms, keys or codecs. Do not assume cryptographic or compression parity. |
-| TLS and HTTP internals | node:tls is incomplete. \_http_common, \_http_outgoing, \_http_server, \_tls_wrap and \_stream_wrap fail the import audit. Native TLS transport does not imply Node TLS API compatibility. |
-| Streams | A packaged fallback supplies byte streams when the host lacks them. Workers tee cancellation reasons and pipeTo abort timing are not fully reproduced. |
-| async_hooks | Build-transformed async functions retain scoped stores. Untransformed native async callbacks reject; arbitrary dynamic code and request-origin isolation are not fully supported. |
-| diagnostics_channel | Synchronous channel/store contracts are implemented; async propagation depends on the async-hooks limitations. |
-| worker_threads | Workers stubs and local MessagePort behavior, not Node threads. Trusted-event branding, host-object serialization and global identity differ. |
-| DNS | DNS-over-HTTPS implementation; synthetic Service Binding/Hyperdrive address overrides are not implemented. |
-| Other modules | Implementations and shared contracts exist, but whole-module acceptance remains outstanding. Namespace equality alone is insufficient. |
+| Area | Behavior and limitations |
+| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| path, querystring, string_decoder, buffer, timers | Implement the pinned Workers profile. Workers stubs remain stubs; for example, matchesGlob reproduces the pinned rejection. |
+| HTTP and net | In-memory server dispatch; no OS listening server. Outbound TCP/TLS requires the native transport. |
+| fs / fs/promises | Read-only embedded files plus request-local in-memory `/tmp`, with writes, descriptors and FileHandle operations. Watch is unsupported, as in Workers. Full filesystem semantics remain under audit. |
+| module | Limited builtin registry; createRequire loads VFS JSON only. Executable modules must be bundled; arbitrary dynamic resolution is unsupported. |
+| process, console, util / util/types / sys | Incomplete exports or behavior. Engine introspection, call sites and host-object detection are not equivalent to Workers. |
+| crypto and zlib | Selected profiles cover random/hash/HKDF/scrypt, secret/asymmetric keys, signatures, primes and Brotli/Zstd streaming. X509Certificate parsing, fields and checks are implemented with the pinned Workers limits; additional algorithms and full crypto/codec parity remain gaps. |
+| TLS and HTTP internals | TLS and HTTP internal imports resolve. Direct Node TLS exchange and hostname rejection are tested on native transports. Existing native sockets upgrade with authenticated TLS before application writes. TLS 1.2/1.3 upgrades are tested on both OSes; custom Duplex wrapping and complete class/state contracts remain gaps. |
+| Streams | An owned implementation supplies byte streams and the tested Workers tee cancellation and pipeTo abort behavior on every host. Whole-module acceptance remains open. |
+| async_hooks | Build-transformed async functions retain scoped stores. Untransformed native async callbacks reject; arbitrary dynamic code and request-origin isolation are not fully supported. |
+| diagnostics_channel | Synchronous channel/store contracts are implemented; async propagation depends on the async-hooks limitations. |
+| worker_threads | Workers stubs and local MessagePort behavior, not Node threads. Trusted-event branding, host-object serialization and global identity differ. |
+| DNS | DNS-over-HTTPS implementation; synthetic Service Binding/Hyperdrive address overrides are not implemented. |
+| events | [WebView events profile](../src/compat/events/README.md): EventEmitter, abort/iteration helpers, transformed AsyncResource propagation and process warning delivery. Native async and V8 inspection remain bounded. |
+| Other modules | Implementations and shared contracts exist, but whole-module acceptance remains outstanding. Namespace equality alone is insufficient. |
The exact missing/extra export baseline is maintained in
[surface-gaps.json](../demo/tests/experiments/node-compat/surface-gaps.json), against
@@ -50,6 +51,57 @@ Those files are the source of truth; this guide does not duplicate the export li
Run `npm run build`, then `npm run test:node-inventory --prefix demo` to check it.
Behavioral contracts live beside each implementation in `src/compat`.
+### Completion assessment
+
+The import audit checks export names and their JavaScript types against the pinned
+Workers profile. A passing audit means no new surface regression; it does not
+establish whole-module compatibility. In particular, a function replaced with an
+object is now a regression even if the export name is unchanged.
+
+After `npm run build`, run `npm run test:node-completion --prefix demo` to regenerate
+`demo/artifacts/node-inventory/completion.json`. The report covers every pinned
+module in both Chromium and WebKit, distinguishes surface gaps from aligned but
+uncertified modules, and records the next investigation with evidence references.
+Missing engines, modules, type-audit data or a mismatched profile fail generation.
+The optional report flag `--require-complete` rejects surface-only evidence; it
+must not be used to turn report generation into a compatibility certificate.
+
+Residual work has different causes:
+
+- **Implementation gaps:** `crypto`, `zlib` and remaining virtual filesystem semantics
+ still need operation-level implementation and verification. Their absence is
+ not evidence of technical impossibility.
+- **Behavior gaps:** Remaining stream lifecycle and synthetic DNS behavior require
+ specific adapters and contracts, beyond exporting the right names.
+- **Engine boundaries:** proxy/promise introspection, native async continuation
+ hooks and trusted MessagePort branding have observable differences. Build-time
+ transforms or native bridges require explicit scope and separate verification.
+- **Workers stubs:** preserve the pinned runtime's actual no-op/rejection behavior.
+ Do not implement desktop Node capabilities merely because Node exposes them.
+- **Verification gaps:** even an aligned export surface needs whole-module
+ operation/prototype coverage and native acceptance before it is called complete.
+
+Run `npm run test:node-boundaries --prefix demo` for the counterexamples and
+`npm run test:events --prefix demo` for the Workers events baseline and implementation. `node:events` is routed through the
+[WebView events profile](../src/compat/events/README.md); process warning delivery
+and microtask scheduling are shared with that adapter. Tests compare ordinary
+Vite/esbuild imports in-page and in Dedicated Workers. Engine inspection and
+untransformed native-async boundaries remain explicit; matching this profile does
+not certify all Workers/Node modules.
+
+The [additional compatibility profiles](../src/compat/expansion/README.md) cover
+console stubs, sys/util aliases, selected crypto operations, request-local `/tmp`,
+secret KeyObjects, Brotli/Zstd/CRC32, filesystem metadata, stream cancellation,
+module metadata and HTTP helpers. Run
+`npm run test:node-expansion --prefix demo` for live differential tests against
+the pinned Workers version. CI runs these checks alongside the existing events
+suite. For native verification, prepare an installed consumer with
+`npm run test:events:native:prepare --prefix demo`, then run
+`npm run test:node-expansion:native --prefix demo -- ios ` or
+`npm run test:node-expansion:native --prefix demo -- android `.
+Native reports include the candidate hash and installed tarball identity under
+`demo/artifacts/node-inventory/native-expansion/`.
+
Workers non-functional modules do not gain device capabilities: VM isolates,
process spawning, V8 heap access, UDP sockets and an OS terminal are not supplied.
`node:test` exposes the Workers mock API, not Node's complete test runner.
diff --git a/native/android/src/main/java/jp/rdlabo/workerscapacitor/WorkerSockets.java b/native/android/src/main/java/jp/rdlabo/workerscapacitor/WorkerSockets.java
index 28df436f..82c73cdd 100644
--- a/native/android/src/main/java/jp/rdlabo/workerscapacitor/WorkerSockets.java
+++ b/native/android/src/main/java/jp/rdlabo/workerscapacitor/WorkerSockets.java
@@ -33,13 +33,15 @@ private final class Connection extends SimpleChannelInboundHandler {
final Result opening;
final ArrayDeque chunks = new ArrayDeque<>();
SocketChannel channel;
- Result reading, watching;
+ Result reading, watching, upgrading;
+ String host; int port;
int count, offset;
boolean ready, closed, eof, ended, writing, reset;
Connection(String id, Result opening) { this.id=id; this.opening=opening; }
void fail(String message) {
if (closed) return;
closed=true;
+ if(upgrading!=null) { Result pending=upgrading; upgrading=null; pending.reject(message); }
if (!ready) { ready=true; opening.reject(message); }
if (reading!=null) {
Result pending=reading; reading=null;
@@ -85,7 +87,8 @@ void drain() {
fail("Socket failed: "+error.getMessage());
}
}
- synchronized void open(String id,String host,int port,boolean tls,Result result) {
+ synchronized void open(String id,String host,int port,boolean tls,Result result) { open(id,host,port,tls,null,result); }
+ synchronized void open(String id,String host,int port,boolean tls,String servername,Result result) {
if(stopped || id==null || id.isEmpty() || connections.containsKey(id) || host==null || host.isEmpty() || port<1 || port>65535) { result.reject("Invalid socket open"); return; }
try {
final SSLEngine engine;
@@ -96,11 +99,11 @@ synchronized void open(String id,String host,int port,boolean tls,Result result)
SSLContext context=SSLContext.getInstance("TLS");
context.init(null,null,null); tlsContext=context;
}
- engine=tlsContext.createSSLEngine(host,port); engine.setUseClientMode(true);
+ engine=tlsContext.createSSLEngine(servername==null || servername.isEmpty() ? host : servername,port); engine.setUseClientMode(true);
SSLParameters parameters=engine.getSSLParameters(); parameters.setEndpointIdentificationAlgorithm("HTTPS"); engine.setSSLParameters(parameters);
} else engine=null;
if(group==null) group=new NioEventLoopGroup(1);
- Connection connection=new Connection(id,result); connections.put(id,connection);
+ Connection connection=new Connection(id,result); connection.host=host; connection.port=port; connections.put(id,connection);
ChannelFuture future=new Bootstrap().group(group).channel(NioSocketChannel.class)
.option(ChannelOption.AUTO_READ,false).option(ChannelOption.ALLOW_HALF_CLOSURE,true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,30000)
@@ -130,6 +133,37 @@ synchronized void open(String id,String host,int port,boolean tls,Result result)
});
} catch(Exception error) { Connection connection=connections.remove(id); if(connection!=null) connection.fail(error.toString()); else result.reject(error.toString()); }
}
+ void startTls(String id,String servername,Result result) {
+ perform(id,result,connection -> {
+ if(connection.ended || connection.eof || connection.writing || connection.upgrading!=null || connection.channel.pipeline().get(SslHandler.class)!=null) { result.reject("Socket cannot be upgraded"); return; }
+ try {
+ final SSLEngine engine;
+ synchronized(this) {
+ if(tlsContext==null) { tlsContext=SSLContext.getInstance("TLS"); tlsContext.init(null,null,null); }
+ engine=tlsContext.createSSLEngine(servername==null || servername.isEmpty()?connection.host:servername,connection.port);
+ }
+ engine.setUseClientMode(true);
+ SSLParameters parameters=engine.getSSLParameters(); parameters.setEndpointIdentificationAlgorithm("HTTPS"); engine.setSSLParameters(parameters);
+ connection.upgrading=result;
+ JSObject moved=new JSObject(); moved.put("upgraded",true);
+ if(connection.reading!=null) { Result pending=connection.reading; connection.reading=null; pending.resolve(moved); }
+ if(connection.watching!=null) { Result pending=connection.watching; connection.watching=null; pending.resolve(moved); }
+ ArrayDeque buffered=new ArrayDeque<>(connection.chunks); connection.chunks.clear();
+ int firstOffset=connection.offset; connection.offset=0;
+ SslHandler handler=new SslHandler(engine);
+ connection.channel.pipeline().addFirst(handler);
+ for(byte[] chunk:buffered) {
+ connection.channel.pipeline().fireChannelRead(Unpooled.wrappedBuffer(chunk,firstOffset,chunk.length-firstOffset)); firstOffset=0;
+ }
+ handler.handshakeFuture().addListener(handshake -> {
+ if(connection.closed) return;
+ if(!handshake.isSuccess()) { connection.fail("TLS failed: "+handshake.cause()); return; }
+ Result pending=connection.upgrading; connection.upgrading=null; pending.resolve(null); connection.pump();
+ });
+ connection.channel.read();
+ } catch(Exception error) { result.reject(error.toString()); connection.fail(error.toString()); }
+ });
+ }
private static String authority(java.net.InetSocketAddress address) {
String host=address.getAddress().getHostAddress(); return (host.contains(":")?"["+host+"]":host)+":"+address.getPort();
}
@@ -163,7 +197,7 @@ void write(String id,String encoded,Result result) {
try { chunk=Base64.decode(encoded,Base64.DEFAULT); } catch(Exception error) { result.reject("Invalid socket chunk"); return; }
if(chunk.length>65536) { result.reject("Socket chunk exceeds limit"); return; }
perform(id,result,connection -> {
- if(connection.ended || connection.writing) { result.reject("Socket write unavailable"); return; }
+ if(connection.ended || connection.writing || connection.upgrading!=null) { result.reject("Socket write unavailable"); return; }
connection.writing=true;
connection.channel.writeAndFlush(Unpooled.wrappedBuffer(chunk)).addListener(future -> {
connection.writing=false;
@@ -174,11 +208,25 @@ void write(String id,String encoded,Result result) {
void end(String id,Result result) {
perform(id,result,true,connection -> {
if(connection.reset) { result.resolve(null); return; }
- if(connection.writing) { result.reject("Socket write still pending"); return; }
+ if(connection.writing || connection.upgrading!=null) { result.reject("Socket write still pending"); return; }
if(connection.ended) { result.resolve(null); return; }
connection.ended=true;
- connection.channel.shutdownOutput().addListener(future -> {
- if(future.isSuccess()) result.resolve(null); else { result.reject("Socket end failed"); connection.fail("Socket end failed"); }
+ Runnable shutdownOutput=() -> {
+ if(connection.eof && !connection.channel.isActive()) { result.resolve(null); return; }
+ connection.channel.shutdownOutput().addListener(future -> {
+ if(future.isSuccess() || connection.eof) result.resolve(null);
+ else { result.reject("Socket end failed"); connection.fail("Socket end failed"); }
+ });
+ };
+ SslHandler tls=connection.channel.pipeline().get(SslHandler.class);
+ if(tls==null) shutdownOutput.run();
+ else tls.closeOutbound().addListener(future -> {
+ // TLS close_notify half-closes the TLS write side. Do not send
+ // TCP FIN here: it can close a peer's raw transport before its
+ // encrypted response has flushed. The read side owns final close.
+ if(future.isSuccess()) result.resolve(null);
+ else if(connection.eof) result.resolve(null);
+ else { result.reject("TLS end failed"); connection.fail("TLS end failed"); }
});
});
}
diff --git a/native/android/src/main/java/jp/rdlabo/workerscapacitor/WorkersCapacitorPlugin.java b/native/android/src/main/java/jp/rdlabo/workerscapacitor/WorkersCapacitorPlugin.java
index a6ee7536..c2ebc86d 100644
--- a/native/android/src/main/java/jp/rdlabo/workerscapacitor/WorkersCapacitorPlugin.java
+++ b/native/android/src/main/java/jp/rdlabo/workerscapacitor/WorkersCapacitorPlugin.java
@@ -110,9 +110,10 @@ private WorkerSockets.Result result(PluginCall call) {
};
}
+ @PluginMethod public void socketStartTls(PluginCall call) { network.startTls(call.getString("id"),call.getString("servername"),result(call)); }
@PluginMethod public void socketWatch(PluginCall call) { network.watch(call.getString("id"),result(call)); }
@PluginMethod public void socketOpen(PluginCall call) {
- network.open(call.getString("id"),call.getString("host"),call.getInt("port",0),call.getBoolean("tls",false),result(call));
+ network.open(call.getString("id"),call.getString("host"),call.getInt("port",0),call.getBoolean("tls",false),call.getString("servername"),result(call));
}
@PluginMethod public void socketWrite(PluginCall call) { network.write(call.getString("id"),call.getString("chunk"),result(call)); }
@PluginMethod public void socketEnd(PluginCall call) { network.end(call.getString("id"),result(call)); }
@@ -246,8 +247,9 @@ private class HostBridge {
}
@JavascriptInterface public void cookiesGet(String callbackId,String url) { WorkersCapacitorPlugin.this.cookiesGet(url,hostResult(owner,callbackId)); }
@JavascriptInterface public void cookiesSet(String callbackId,String url,String cookies) { WorkersCapacitorPlugin.this.cookiesSet(url,cookies,hostResult(owner,callbackId)); }
+ @JavascriptInterface public void socketStartTls(String callbackId,String id,String servername) { network.startTls(id,servername,hostResult(owner,callbackId)); }
@JavascriptInterface public void socketWatch(String callbackId,String id) { network.watch(id,hostResult(owner,callbackId)); }
- @JavascriptInterface public void socketOpen(String callbackId,String id,String host,int port,boolean tls) { network.open(id,host,port,tls,hostResult(owner,callbackId)); }
+ @JavascriptInterface public void socketOpen(String callbackId,String id,String host,int port,boolean tls,String servername) { network.open(id,host,port,tls,servername,hostResult(owner,callbackId)); }
@JavascriptInterface public void socketWrite(String callbackId,String id,String chunk) { network.write(id,chunk,hostResult(owner,callbackId)); }
@JavascriptInterface public void socketEnd(String callbackId,String id) { network.end(id,hostResult(owner,callbackId)); }
@JavascriptInterface public void socketRead(String callbackId,String id,int count) { network.read(id,count,hostResult(owner,callbackId)); }
diff --git a/native/ios/NetworkBridge.swift b/native/ios/NetworkBridge.swift
index 64cbc841..11f51aa0 100644
--- a/native/ios/NetworkBridge.swift
+++ b/native/ios/NetworkBridge.swift
@@ -22,13 +22,23 @@ final class NetworkBridge {
}
}
}
+ func socketVerifyTls(_ call: CAPPluginCall) {
+ guard let id = call.getString("id"), let encoded = call.getArray("certificates", String.self), encoded.count <= 32 else { reject(call, "Invalid certificate chain"); return }
+ let certificates = encoded.compactMap { $0.utf8.count <= 1048576 ? Data(base64Encoded: $0) : nil }
+ guard certificates.count == encoded.count else { reject(call, "Invalid certificate chain"); return }
+ sockets.verifyTls(id: id, certificates: certificates, completion: socketResult(call))
+ }
+ func socketStartTls(_ call: CAPPluginCall) {
+ guard let id = call.getString("id") else { reject(call, "Missing socket"); return }
+ sockets.startTls(id: id, servername: call.getString("servername"), completion: socketResult(call))
+ }
func socketWatch(_ call: CAPPluginCall) {
guard let id = call.getString("id") else { reject(call, "Missing socket"); return }
sockets.watch(id: id, completion: socketResult(call))
}
func socketOpen(_ call: CAPPluginCall) {
guard let id = call.getString("id"), let host = call.getString("host"), let port = call.getInt("port") else { reject(call, "Invalid socket open"); return }
- sockets.open(id: id, host: host, port: port, tls: call.getBool("tls") ?? false, completion: socketResult(call))
+ sockets.open(id: id, host: host, port: port, tls: call.getBool("tls") ?? false, servername: call.getString("servername"), completion: socketResult(call))
}
func socketWrite(_ call: CAPPluginCall) {
guard let id = call.getString("id"), let encoded = call.getString("chunk"), encoded.utf8.count <= 87384, let data = Data(base64Encoded: encoded) else { reject(call, "Invalid socket write"); return }
diff --git a/native/ios/SocketBridge.swift b/native/ios/SocketBridge.swift
index 41d4035a..f28c41ea 100644
--- a/native/ios/SocketBridge.swift
+++ b/native/ios/SocketBridge.swift
@@ -1,5 +1,6 @@
import Foundation
import Network
+import Security
import Capacitor
/// TCP byte transport. All state and completions belong to one serial queue.
@@ -7,6 +8,8 @@ final class SocketBridge: @unchecked Sendable {
private final class Connection: @unchecked Sendable {
let id: String
let transport: NWConnection
+ var encrypted = false
+ var host = ""
var opening: ((Result) -> Void)?
var reading: ((Result) -> Void)?
var writing: ((Result) -> Void)?
@@ -51,12 +54,19 @@ final class SocketBridge: @unchecked Sendable {
let waiters = value.closeWaiters; value.closeWaiters.removeAll()
for waiter in waiters { waiter() }
}
- func open(id: String, host: String, port: Int, tls: Bool, completion: @escaping Completion) {
+ func open(id: String, host: String, port: Int, tls: Bool, servername: String? = nil, completion: @escaping Completion) {
queue.async {
guard !self.stopped, !id.isEmpty, !host.isEmpty, port > 0, port <= 65535,
self.values[id] == nil, let endpointPort = NWEndpoint.Port(rawValue: UInt16(port)) else { completion(.failure(Failure.invalid)); return }
- let transport = NWConnection(host: NWEndpoint.Host(host), port: endpointPort, using: tls ? .tls : .tcp)
+ let parameters: NWParameters
+ if tls {
+ let options = NWProtocolTLS.Options()
+ sec_protocol_options_set_tls_server_name(options.securityProtocolOptions, servername ?? host)
+ parameters = NWParameters(tls: options, tcp: NWProtocolTCP.Options())
+ } else { parameters = .tcp }
+ let transport = NWConnection(host: NWEndpoint.Host(host), port: endpointPort, using: parameters)
let value = Connection(id, transport)
+ value.host = host; value.encrypted = tls
value.opening = completion
self.values[id] = value
transport.stateUpdateHandler = { [weak self, weak value] state in
@@ -89,6 +99,31 @@ final class SocketBridge: @unchecked Sendable {
transport.start(queue: self.queue)
}
}
+ func startTls(id: String, servername: String?, completion: @escaping Completion) {
+ queue.async {
+ guard let value = self.values[id], value.ready, !value.closed, !value.eof, !value.ended, !value.encrypted, value.writing == nil,
+ let hostname = servername, !hostname.isEmpty, !hostname.utf8.contains(0) else { completion(.failure(Failure.invalid)); return }
+ value.encrypted = true; value.host = hostname
+ value.reading?(.success(["upgraded": true])); value.reading = nil
+ value.watching?(.success(["upgraded": true])); value.watching = nil
+ // JS encrypts records with OpenSSL memory BIOs on this same TCP stream.
+ // The successful JS open is withheld until verifyTls authenticates it.
+ completion(.success(["tlsEngine": "openssl-wasm"]))
+ }
+ }
+ func verifyTls(id: String, certificates: [Data], completion: @escaping Completion) {
+ queue.async {
+ guard let value = self.values[id], !value.closed, value.encrypted, !certificates.isEmpty else { completion(.failure(Failure.invalid)); return }
+ let chain = certificates.compactMap { SecCertificateCreateWithData(nil, $0 as CFData) }
+ guard chain.count == certificates.count else { completion(.failure(Failure.invalid)); return }
+ var trust: SecTrust?
+ let policy = SecPolicyCreateSSL(true, value.host as CFString)
+ guard SecTrustCreateWithCertificates(chain as CFArray, policy, &trust) == errSecSuccess, let trust else { completion(.failure(Failure.invalid)); return }
+ var error: CFError?
+ guard SecTrustEvaluateWithError(trust, &error) else { completion(.failure(error as Error? ?? Failure.invalid)); return }
+ completion(.success([:]))
+ }
+ }
func watch(id: String, completion: @escaping Completion) {
queue.async {
if let value = self.values[id], value.reset { completion(.success(["reset": true])); return }
diff --git a/native/ios/WorkersCapacitorPlugin.swift b/native/ios/WorkersCapacitorPlugin.swift
index 3a00fb38..9b8e471a 100644
--- a/native/ios/WorkersCapacitorPlugin.swift
+++ b/native/ios/WorkersCapacitorPlugin.swift
@@ -13,6 +13,8 @@ public final class WorkersCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
CAPPluginMethod(name: "start", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "stop", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "socketWatch", returnType: CAPPluginReturnPromise),
+ CAPPluginMethod(name: "socketVerifyTls", returnType: CAPPluginReturnPromise),
+ CAPPluginMethod(name: "socketStartTls", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "socketOpen", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "socketWrite", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "socketEnd", returnType: CAPPluginReturnPromise),
@@ -69,6 +71,8 @@ public final class WorkersCapacitorPlugin: CAPPlugin, CAPBridgedPlugin {
Task { await owner.closeAll(); call.resolve() }
}
}
+ @objc func socketVerifyTls(_ call: CAPPluginCall) { network.socketVerifyTls(call) }
+ @objc func socketStartTls(_ call: CAPPluginCall) { network.socketStartTls(call) }
@objc func socketWatch(_ call: CAPPluginCall) { network.socketWatch(call) }
@objc func socketOpen(_ call: CAPPluginCall) { network.socketOpen(call) }
@objc func socketWrite(_ call: CAPPluginCall) { network.socketWrite(call) }
diff --git a/package.json b/package.json
index b97d15e8..390ab8e1 100644
--- a/package.json
+++ b/package.json
@@ -111,12 +111,14 @@
"brotli-dec-wasm": "2.3.2",
"esbuild": "^0.28.2",
"jsonc-parser": "^3.3.1",
+ "memfs": "4.80.0",
"node-stdlib-browser": "^1.3.1",
"parse-srcset": "^1.0.2",
"parse5": "^8.0.1",
"punycode": "2.3.1",
"readable-stream": "4.8.0",
"rollup": "^4.64.9",
+ "scrypt-js": "3.0.1",
"smol-toml": "^1.9.1",
"string_decoder": "^1.3.0",
"vite-plugin-node-polyfills": "^0.28.0",
@@ -126,12 +128,12 @@
"@playwright/test": "^1.64.1",
"@types/node": "^26.6.5",
"hono": "^4.13.13",
+ "np": "^12.1.1",
"prettier": "3.8.4",
"typescript": "^7.0.2",
"vite": "^7.3.7",
"vitest": "^5.0.5",
- "whatwg-url": "17.3.0",
- "np": "^12.1.1"
+ "whatwg-url": "17.3.0"
},
"peerDependencies": {
"@capacitor/core": "^8.0.0",
diff --git a/scripts/build-codecs.mjs b/scripts/build-codecs.mjs
index 0636b6da..fa1a645f 100644
--- a/scripts/build-codecs.mjs
+++ b/scripts/build-codecs.mjs
@@ -1,4 +1,39 @@
import { build } from "esbuild";
+import { readFile } from "node:fs/promises";
+import { createHash } from "node:crypto";
+
+// Fail before packaging if a generated implementation and its provenance drift.
+for (const [asset, manifest] of [
+ [
+ "src/compat/crypto/native-crypto.wasm",
+ "src/compat/crypto/native-crypto.build.json",
+ ],
+ [
+ "src/compat/zlib/native-codecs.wasm",
+ "src/compat/zlib/native-codecs.build.json",
+ ],
+ [
+ "src/compat/stream-web/workers-ponyfill.js",
+ "src/compat/stream-web/workers-ponyfill.build.json",
+ ],
+]) {
+ const provenance = JSON.parse(await readFile(manifest, "utf8"));
+ const expected = provenance.sha256;
+ if (provenance.bindingSha256) {
+ const binding = asset.replace(/\.wasm$/, ".c");
+ if (
+ createHash("sha256")
+ .update(await readFile(binding))
+ .digest("hex") !== provenance.bindingSha256
+ )
+ throw new Error(`Native binding changed without rebuilding: ${binding}`);
+ }
+ const actual = createHash("sha256")
+ .update(await readFile(asset))
+ .digest("hex");
+ if (actual !== expected)
+ throw new Error(`Generated artifact hash mismatch: ${asset}`);
+}
// The package owns its codec asset. Consumers need no WASM loader or extra URL.
await build({
entryPoints: ["src/shared/brotli/index.ts"],
@@ -17,3 +52,21 @@ await build({
format: "esm",
target: "es2022",
});
+
+await build({
+ entryPoints: ["src/compat/zlib/native-codecs.ts"],
+ outfile: "dist/compat/zlib/native-codecs.js",
+ bundle: true,
+ format: "esm",
+ target: "es2022",
+ loader: { ".wasm": "binary" },
+});
+
+await build({
+ entryPoints: ["src/compat/crypto/native-crypto.ts"],
+ outfile: "dist/compat/crypto/native-crypto.js",
+ bundle: true,
+ format: "esm",
+ target: "es2022",
+ loader: { ".wasm": "binary" },
+});
diff --git a/scripts/build-native-codecs.mjs b/scripts/build-native-codecs.mjs
new file mode 100644
index 00000000..a5909f54
--- /dev/null
+++ b/scripts/build-native-codecs.mjs
@@ -0,0 +1,76 @@
+import { execFileSync } from "node:child_process";
+import { readdirSync, readFileSync, writeFileSync } from "node:fs";
+import { join, resolve } from "node:path";
+import { createHash } from "node:crypto";
+// One-time/reproducibility build. Normal npm builds embed the checked-in Wasm
+// and do not download a compiler or compile native sources.
+const sdk = resolve(process.env.CODEC_EMSDK ?? "artifacts/toolchains/emsdk");
+const zstd = resolve("artifacts/upstream/zstd");
+const brotli = resolve("artifacts/upstream/brotli");
+const revision = (path) =>
+ execFileSync("git", ["rev-parse", "HEAD"], {
+ cwd: path,
+ encoding: "utf8",
+ }).trim();
+const expected = {
+ emsdk: "e4fe26ef59168ff44f4c23c466e497bf60b3411e",
+ zstd: "f8745da6ff1ad1e7bab384bd1f9d742439278e99",
+ brotli: "ed738e842d2fbdf2d6459e39267a633c4a9b2f5d",
+};
+const revisions = {
+ emsdk: revision(sdk),
+ zstd: revision(zstd),
+ brotli: revision(brotli),
+};
+for (const name of Object.keys(expected))
+ if (revisions[name] !== expected[name])
+ throw new Error(`Unexpected ${name} revision: ${revisions[name]}`);
+const sources = [
+ ...["common", "compress", "decompress"].flatMap((dir) =>
+ readdirSync(join(zstd, "lib", dir))
+ .filter((n) => n.endsWith(".c"))
+ .sort()
+ .map((n) => join(zstd, "lib", dir, n)),
+ ),
+ ...["common", "enc", "dec"].flatMap((dir) =>
+ readdirSync(join(brotli, "c", dir))
+ .filter((n) => n.endsWith(".c"))
+ .sort()
+ .map((n) => join(brotli, "c", dir, n)),
+ ),
+];
+const output = "src/compat/zlib/native-codecs.wasm";
+execFileSync(
+ join(sdk, "upstream/emscripten/emcc"),
+ [
+ "src/compat/zlib/native-codecs.c",
+ ...sources,
+ "-I" + join(zstd, "lib"),
+ "-I" + join(brotli, "c/include"),
+ "-O3",
+ "-DNDEBUG",
+ "-DZSTD_DISABLE_ASM",
+ "-DBROTLI_BUILD_PORTABLE",
+ "-sSTANDALONE_WASM=1",
+ "-sALLOW_MEMORY_GROWTH=1",
+ "-sFILESYSTEM=0",
+ "-sASSERTIONS=0",
+ "-sMALLOC=emmalloc",
+ "--no-entry",
+ '-sEXPORTED_FUNCTIONS=["_malloc","_free"]',
+ "-o",
+ output,
+ ],
+ { stdio: "inherit" },
+);
+writeFileSync(
+ "src/compat/zlib/native-codecs.build.json",
+ JSON.stringify(
+ {
+ revisions,
+ sha256: createHash("sha256").update(readFileSync(output)).digest("hex"),
+ },
+ null,
+ 2,
+ ) + "\n",
+);
diff --git a/scripts/build-native-crypto.mjs b/scripts/build-native-crypto.mjs
new file mode 100644
index 00000000..24df1878
--- /dev/null
+++ b/scripts/build-native-crypto.mjs
@@ -0,0 +1,98 @@
+import { execFileSync } from "node:child_process";
+import { readFileSync, writeFileSync } from "node:fs";
+import { resolve, join } from "node:path";
+import { createHash } from "node:crypto";
+import assert from "node:assert/strict";
+import { format, resolveConfig } from "prettier";
+const sdk = resolve(process.env.CODEC_EMSDK ?? "artifacts/toolchains/emsdk");
+const source = resolve("artifacts/upstream/openssl");
+const revision = (directory) =>
+ execFileSync("git", ["rev-parse", "HEAD"], {
+ cwd: directory,
+ encoding: "utf8",
+ }).trim();
+const revisions = { emsdk: revision(sdk), openssl: revision(source) };
+assert.equal(revisions.emsdk, "e4fe26ef59168ff44f4c23c466e497bf60b3411e");
+assert.equal(revisions.openssl, "45e844fa2a14ec92d146bd8f5778ac130b6625fb");
+assert.equal(
+ execFileSync("git", ["diff", "--name-only"], {
+ cwd: source,
+ encoding: "utf8",
+ }).trim(),
+ "",
+ "OpenSSL source must match the pinned revision",
+);
+const compiler = join(sdk, "upstream/emscripten");
+const configure = [
+ "linux-generic32",
+ "--cross-compile-prefix=",
+ "no-asm",
+ "no-shared",
+ "no-threads",
+ "no-tests",
+ "no-dso",
+ "no-module",
+ "no-apps",
+ "no-docs",
+ "no-sock",
+ "no-ui-console",
+ "no-autoload-config",
+ "no-engine",
+ "no-afalgeng",
+ "--with-rand-seed=getrandom",
+ "-O3",
+];
+if (!process.argv.includes("--link-only")) {
+ execFileSync(join(compiler, "emconfigure"), ["./Configure", ...configure], {
+ cwd: source,
+ stdio: "inherit",
+ });
+ execFileSync(join(compiler, "emmake"), ["make", "-j4", "build_libs"], {
+ cwd: source,
+ stdio: "inherit",
+ });
+}
+const binding = "src/compat/crypto/native-crypto.c";
+const output = "src/compat/crypto/native-crypto.wasm";
+const flags = [
+ "-O3",
+ "-Wno-deprecated-declarations",
+ "-sSTANDALONE_WASM=1",
+ "-sALLOW_MEMORY_GROWTH=1",
+ "-sFILESYSTEM=0",
+ "-sASSERTIONS=0",
+ "-sMALLOC=emmalloc",
+ "-sABORTING_MALLOC=0",
+ "--no-entry",
+ '-sEXPORTED_FUNCTIONS=["_malloc","_free"]',
+];
+execFileSync(
+ join(compiler, "emcc"),
+ [
+ binding,
+ "-I" + join(source, "include"),
+ join(source, "libssl.a"),
+ join(source, "libcrypto.a"),
+ ...flags,
+ "-o",
+ output,
+ ],
+ { stdio: "inherit" },
+);
+const sha = (path) =>
+ createHash("sha256").update(readFileSync(path)).digest("hex");
+const manifest = "src/compat/crypto/native-crypto.build.json";
+writeFileSync(
+ manifest,
+ await format(
+ JSON.stringify({
+ revisions,
+ configure,
+ flags,
+ libraries: ["libssl.a", "libcrypto.a"],
+ bindingSha256: sha(binding),
+ sha256: sha(output),
+ }),
+ { ...(await resolveConfig(manifest)), parser: "json" },
+ ),
+);
diff --git a/scripts/build-workers-streams.mjs b/scripts/build-workers-streams.mjs
new file mode 100644
index 00000000..80a27ddd
--- /dev/null
+++ b/scripts/build-workers-streams.mjs
@@ -0,0 +1,107 @@
+import { build } from "esbuild";
+import { readFile, writeFile } from "node:fs/promises";
+import { execFileSync } from "node:child_process";
+import assert from "node:assert/strict";
+import { createHash } from "node:crypto";
+const root = "artifacts/upstream/web-streams-polyfill";
+const revision = execFileSync("git", ["rev-parse", "HEAD"], {
+ cwd: root,
+ encoding: "utf8",
+}).trim();
+assert.equal(revision, "12b4fedec6c01503a137d837204ca0b85992461e");
+const changes = {};
+await build({
+ entryPoints: [root + "/src/ponyfill.ts"],
+ outfile: "src/compat/stream-web/workers-ponyfill.js",
+ bundle: true,
+ format: "esm",
+ target: "es2022",
+ minify: true,
+ keepNames: true,
+ define: { DEBUG: "false" },
+ banner: {
+ js: "// Generated from web-streams-polyfill 4.3.0 (MIT), with pinned Workers cancellation semantics.\n// Rebuild using scripts/build-workers-streams.mjs; see LICENSE.",
+ },
+ plugins: [
+ {
+ name: "workers-stream-behavior",
+ setup(build) {
+ build.onLoad(
+ { filter: /readable-stream\/(tee|pipe)\.ts$/ },
+ async ({ path }) => {
+ let source = await readFile(path, "utf8");
+ if (path.endsWith("/tee.ts")) {
+ const pattern =
+ "const compositeReason = CreateArrayFromList([reason1, reason2]);";
+ assert.equal(source.split(pattern).length - 1, 4);
+ source = source.replaceAll(
+ pattern,
+ "const compositeReason = reason;",
+ );
+ } else {
+ const replace = (from, to) => {
+ assert.equal(source.split(from).length - 1, 1, from);
+ source = source.replace(from, to);
+ };
+ replace(
+ "actions.map(action => action())",
+ "actions.reverse().map(action => action())",
+ );
+ replace(
+ "shutdownWithAction(() => Promise.all(actions.reverse().map(action => action())), true, error);",
+ "shutdownWithAction(() => Promise.all(actions.reverse().map(action => action())), true, error, true);",
+ );
+ replace(
+ "const request = new PipeReadRequest(state);\n ReadableStreamDefaultReaderRead(reader, request);",
+ "const request = new PipeReadRequest(state);\n state._currentRead = request._promise;\n ReadableStreamDefaultReaderRead(reader, request);",
+ );
+ replace(
+ "originalError?: any) {",
+ "originalError?: any, waitForRead = false) {",
+ );
+ replace(
+ "if (dest._state === 'writable' && !WritableStreamCloseQueuedOrInFlight(dest)) {\n uponFulfillment(state._waitForWritesToFinish(), doTheRest);\n } else {\n doTheRest();\n }",
+ "const read = waitForRead ? state._currentRead : undefined;\n uponFulfillment(read ? PerformPromiseThen(read, noop, noop) : promiseResolvedWith(undefined), () => {\n if (dest._state === 'writable' && !WritableStreamCloseQueuedOrInFlight(dest)) uponFulfillment(state._waitForWritesToFinish(), doTheRest);\n else doTheRest();\n });",
+ );
+ replace(
+ "class PipeState {\n _shuttingDown = false;",
+ "class PipeState {\n _shuttingDown = false;\n _currentRead: Promise | undefined;",
+ );
+ // Two read request classes share this prefix; patch only PipeReadRequest.
+ assert.equal(
+ source.split(
+ "_chunkSteps(chunk: T) {\n this._state._currentWrite",
+ ).length - 1,
+ 2,
+ );
+ source = source.replace(
+ "_chunkSteps(chunk: T) {\n this._state._currentWrite",
+ "_chunkSteps(chunk: T) {\n if (this._state._shuttingDown) {this._resolvePromise(false); return;}\n this._state._currentWrite",
+ );
+ }
+ changes[path.endsWith("/tee.ts") ? "tee" : "pipe"] = createHash(
+ "sha256",
+ )
+ .update(source)
+ .digest("hex");
+ return { contents: source, loader: "ts" };
+ },
+ );
+ },
+ },
+ ],
+});
+await writeFile(
+ "src/compat/stream-web/workers-ponyfill.build.json",
+ JSON.stringify(
+ {
+ revision,
+ changes,
+ sha256: createHash("sha256")
+ .update(await readFile("src/compat/stream-web/workers-ponyfill.js"))
+ .digest("hex"),
+ },
+ null,
+ 2,
+ ) + "\n",
+);
diff --git a/scripts/test-package.mjs b/scripts/test-package.mjs
index 831f7369..9e49ade0 100644
--- a/scripts/test-package.mjs
+++ b/scripts/test-package.mjs
@@ -104,6 +104,22 @@ for (const module of [
throw new Error(`Missing upstream license notice in tarball: ${notice}`);
}
}
+for (const notice of [
+ "dist/compat/stream-web/LICENSE",
+ ...["openssl", "emscripten", "musl"].map(
+ (name) => `dist/compat/crypto/LICENSE.${name}`,
+ ),
+ "dist/compat/crypto/LICENSE-APACHE",
+ "dist/compat/crypto/LICENSE-3RD-PARTY.md",
+ ...["brotli", "zstd", "emscripten", "musl"].map(
+ (name) => `dist/compat/zlib/LICENSE.${name}`,
+ ),
+]) {
+ assert.ok(
+ pack.files.some((file) => file.path === notice),
+ `Missing bundled implementation license: ${notice}`,
+ );
+}
for (const name of ["NOTICE.md", "LICENSE-APACHE"]) {
const notice = `dist/compat/async-hooks/${name}`;
if (!pack.files.some((file) => file.path === notice))
diff --git a/src/browser/fetch/host-call.ts b/src/browser/fetch/host-call.ts
index 5be7ddbd..f0e84d22 100644
--- a/src/browser/fetch/host-call.ts
+++ b/src/browser/fetch/host-call.ts
@@ -3,6 +3,7 @@ import type { NativeCall } from "./native-network.js";
type Host = {
cookiesGet(callback: string, url: string): void;
cookiesSet(callback: string, url: string, cookies: string): void;
+ socketStartTls(callback: string, id: string, servername: string): void;
socketWatch(callback: string, id: string): void;
socketOpen(
callback: string,
@@ -10,6 +11,7 @@ type Host = {
host: string,
port: number,
tls: boolean,
+ servername: string,
): void;
socketWrite(callback: string, id: string, chunk: string): void;
socketEnd(callback: string, id: string): void;
@@ -66,6 +68,9 @@ export function getHostCall(): NativeCall | undefined {
case "cookiesSet":
host.cookiesSet(id, String(args.url), JSON.stringify(args.cookies));
break;
+ case "socketStartTls":
+ host.socketStartTls(id, handle, String(args.servername ?? ""));
+ break;
case "socketWatch":
host.socketWatch(id, handle);
break;
@@ -76,6 +81,9 @@ export function getHostCall(): NativeCall | undefined {
String(args.host),
Number(args.port),
args.tls === true,
+ typeof args.servername === "string"
+ ? args.servername
+ : String(args.host),
);
break;
case "socketWrite":
diff --git a/src/browser/fetch/native-network.ts b/src/browser/fetch/native-network.ts
index 569adde6..b4c501bf 100644
--- a/src/browser/fetch/native-network.ts
+++ b/src/browser/fetch/native-network.ts
@@ -7,6 +7,8 @@ export type NativeNetworkMethod =
| "cookiesSet"
| "socketWatch"
| "socketOpen"
+ | "socketStartTls"
+ | "socketVerifyTls"
| "socketWrite"
| "socketEnd"
| "socketRead"
diff --git a/src/browser/fetch/relay.ts b/src/browser/fetch/relay.ts
index 86c8c02a..39c0f390 100644
--- a/src/browser/fetch/relay.ts
+++ b/src/browser/fetch/relay.ts
@@ -9,6 +9,8 @@ import {
const methods = new Set([
"socketWatch",
"socketOpen",
+ "socketStartTls",
+ "socketVerifyTls",
"socketWrite",
"socketEnd",
"socketRead",
diff --git a/src/browser/isolated/transport.ts b/src/browser/isolated/transport.ts
index 5139dcb2..326747b7 100644
--- a/src/browser/isolated/transport.ts
+++ b/src/browser/isolated/transport.ts
@@ -1,3 +1,4 @@
+import { runWithRequestScope } from "../../shared/request-scope.js";
import { setWorkerEnv } from "../../cloudflare/apis/cloudflare-workers/index.js";
import { isResponse } from "../fetch/interop.js";
import { Request as WorkerRequest } from "../request/index.js";
@@ -115,21 +116,23 @@ export function serve(worker: WorkerHandler) {
body,
signal: controller.signal,
});
- const response = await worker.fetch(request as unknown as Request, env, {
- props: {},
- passThroughOnException() {
- throw new Error(
- "passThroughOnException is unsupported for local execution.",
- );
- },
- waitUntil(promise) {
- pending.push(
- promise.catch((cause) => {
- errors.push(String(cause));
- }),
- );
- },
- });
+ const response = await runWithRequestScope(() =>
+ worker.fetch(request as unknown as Request, env, {
+ props: {},
+ passThroughOnException() {
+ throw new Error(
+ "passThroughOnException is unsupported for local execution.",
+ );
+ },
+ waitUntil(promise) {
+ pending.push(
+ promise.catch((cause) => {
+ errors.push(String(cause));
+ }),
+ );
+ },
+ }),
+ );
if (!isResponse(response) || response.status === 0)
throw new TypeError("worker.fetch must return a non-error Response.");
if (response.bodyUsed || response.body?.locked)
diff --git a/src/browser/request/index.ts b/src/browser/request/index.ts
index 0f4c98a3..1063f1a2 100644
--- a/src/browser/request/index.ts
+++ b/src/browser/request/index.ts
@@ -1,4 +1,5 @@
-import { platform } from "../../shared/globals/platform.js";
+import { readable } from "../../compat/stream-web/interop.js";
+import { nativeStreams, platform } from "../../shared/globals/platform.js";
import { hasBody, inheritBodySource } from "../body/index.js";
import {
Body,
@@ -161,7 +162,12 @@ export class Request extends Body {
"Request with a GET or HEAD method cannot have a body.",
);
// Transfer only after all initializer validation succeeds.
- if (nativeSource?.body) body = new platform.Request(nativeSource).body;
+ if (nativeSource?.body)
+ body = readable(
+ nativeSource.body.pipeThrough(
+ new nativeStreams.TransformStream(),
+ ),
+ );
else if (original && init.body === undefined) body = moveBody(original);
super(body, headers);
if (original && init.body === undefined) inheritBodySource(this, original);
diff --git a/src/cloudflare/workers/worker-runtime.ts b/src/cloudflare/workers/worker-runtime.ts
index af9136f3..bfe6cba1 100644
--- a/src/cloudflare/workers/worker-runtime.ts
+++ b/src/cloudflare/workers/worker-runtime.ts
@@ -1,3 +1,4 @@
+import { runWithRequestScope } from "../../shared/request-scope.js";
import { installWebGlobals } from "../../shared/globals/web.js";
import { createOutboundFetch } from "../../browser/fetch/outbound.js";
import { browserFetch, isRequest } from "../../browser/fetch/interop.js";
@@ -61,7 +62,7 @@ export class DirectFetchRuntime extends FetchRuntime {
this.restoreWeb();
}
protected dispatch(request: Request, env: Env, ctx: WorkerContext) {
- return this.worker.fetch(request, env, ctx);
+ return runWithRequestScope(() => this.worker.fetch(request, env, ctx));
}
}
diff --git a/src/compat/console/index.ts b/src/compat/console/index.ts
new file mode 100644
index 00000000..8e56ff00
--- /dev/null
+++ b/src/compat/console/index.ts
@@ -0,0 +1,112 @@
+// Copyright (c) 2017-2022 Cloudflare, Inc.
+// Licensed under the Apache 2.0 license found in the LICENSE file or at:
+// https://opensource.org/licenses/Apache-2.0
+// Copyright Joyent and Node contributors. All rights reserved. MIT license.
+
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+
+class ERR_METHOD_NOT_IMPLEMENTED extends Error {
+ readonly code = "ERR_METHOD_NOT_IMPLEMENTED";
+ constructor(method: string) {
+ super(`The ${method} method is not implemented`);
+ }
+}
+import processImpl from "node:process";
+import type { ConsoleOptions } from "node:console";
+
+const noop: VoidFunction = () => {};
+
+// TODO(soon): This class is Node.js compatible but globalThis.console is not.
+// This is because globalThis.console is provided by the v8 runtime.
+// We need to find a way to patch and add createTask and context methods whenever
+// `enable_nodejs_console_module` flag is enabled.
+const globalConsole = globalThis.console as typeof globalThis.console & {
+ _times: Map;
+ _ignoreErrors: boolean;
+ _stderrErrorHandler: () => void;
+ _stdoutErrorHandler: () => void;
+ _stderr: NodeJS.WritableStream;
+ _stdout: NodeJS.WritableStream;
+};
+
+globalConsole._times = new Map();
+globalConsole._ignoreErrors = true;
+globalConsole._stderrErrorHandler = noop;
+globalConsole._stdoutErrorHandler = noop;
+
+// We have this assertion because node-internal:public_process has undefined as the type
+globalConsole._stderr = processImpl.stderr as unknown as NodeJS.WritableStream;
+globalConsole._stdout = processImpl.stdout as unknown as NodeJS.WritableStream;
+
+export function Console(_options: ConsoleOptions): never {
+ throw new ERR_METHOD_NOT_IMPLEMENTED("Console");
+}
+// We want to make sure the following works:
+// ```js
+// import mod from 'node:console`;
+// new mod.Console(...)
+// ```
+// @ts-expect-error TS2322 This is intentional.
+globalConsole.Console = Console;
+
+export const log = console.log.bind(console);
+Console.prototype.log = log;
+export const info = console.info.bind(console);
+Console.prototype.info = info;
+export const debug = console.debug.bind(console);
+Console.prototype.debug = debug;
+export const warn = console.warn.bind(console);
+Console.prototype.warn = warn;
+export const error = console.error.bind(console);
+Console.prototype.error = error;
+export const dir = console.dir.bind(console);
+Console.prototype.dir = dir;
+export const time = console.time.bind(console);
+Console.prototype.time = time;
+export const timeEnd = console.timeEnd.bind(console);
+Console.prototype.timeEnd = timeEnd;
+export const timeLog = console.timeLog.bind(console);
+Console.prototype.timeLog = timeLog;
+export const trace = console.trace.bind(console);
+Console.prototype.trace = trace;
+export const assert = console.assert.bind(console);
+Console.prototype.assert = assert;
+export const clear = console.clear.bind(console);
+Console.prototype.clear = clear;
+export const count = console.count.bind(console);
+Console.prototype.count = count;
+export const countReset = console.countReset.bind(console);
+Console.prototype.countReset = countReset;
+export const group = console.group.bind(console);
+Console.prototype.group = group;
+export const groupEnd = console.groupEnd.bind(console);
+Console.prototype.groupEnd = groupEnd;
+export const table = console.table.bind(console);
+Console.prototype.table = table;
+
+export const dirxml = console.dirxml.bind(console);
+Console.prototype.dirxml = dirxml;
+export const groupCollapsed = console.groupCollapsed.bind(console);
+Console.prototype.groupCollapsed = groupCollapsed;
+export const profile = console.profile.bind(console);
+Console.prototype.profile = profile;
+export const profileEnd = console.profileEnd.bind(console);
+Console.prototype.profileEnd = profileEnd;
+export const timeStamp = console.timeStamp.bind(console);
+Console.prototype.timeStamp = timeStamp;
+
+export function context(): void {
+ throw new ERR_METHOD_NOT_IMPLEMENTED("Console.context");
+}
+// @ts-expect-error TS2339 This is intentional.
+globalConsole.context = context;
+Console.prototype.context = context;
+
+export function createTask(): void {
+ throw new ERR_METHOD_NOT_IMPLEMENTED("Console.createTask");
+}
+// @ts-expect-error TS2339 This is intentional.
+globalConsole.createTask = createTask;
+Console.prototype.createTask = createTask;
+
+export default globalConsole;
diff --git a/src/compat/crypto/LICENSE-3RD-PARTY.md b/src/compat/crypto/LICENSE-3RD-PARTY.md
new file mode 100644
index 00000000..7530a8e2
--- /dev/null
+++ b/src/compat/crypto/LICENSE-3RD-PARTY.md
@@ -0,0 +1,19 @@
+# Upstream notices
+
+The net facade, internal implementation, logical port table, error classes and
+validators are adapted from Cloudflare workerd tag `v1.20261012.1`:
+
+- `src/node/net.ts`
+- `src/node/internal/internal_net.ts`
+- `src/node/internal/internal_errors.ts` (required declarations only)
+- `src/node/internal/validators.ts` (required declarations only)
+- `src/cloudflare/internal/http.ts`
+
+Copyright Cloudflare, Inc.; Apache License 2.0. Node.js/Joyent and Deno MIT
+notices are retained in the corresponding source files. Host transport and
+module imports are adapted for Capacitor. Public exports remain those of the
+pinned workerd module; internal helpers are not added to the public facade.
+
+X509 certificate wrappers and safe name rendering are adapted from
+`src/node/internal/crypto_x509.ts` and `src/workerd/api/crypto/x509.c++` at
+workerd commit `5358e3d337336eaf696a48bc1563d808b10feba5` (Apache-2.0).
diff --git a/src/compat/crypto/LICENSE-APACHE b/src/compat/crypto/LICENSE-APACHE
new file mode 100644
index 00000000..f433b1a5
--- /dev/null
+++ b/src/compat/crypto/LICENSE-APACHE
@@ -0,0 +1,177 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/src/compat/crypto/LICENSE.emscripten b/src/compat/crypto/LICENSE.emscripten
new file mode 100644
index 00000000..70cabe38
--- /dev/null
+++ b/src/compat/crypto/LICENSE.emscripten
@@ -0,0 +1,102 @@
+Emscripten is available under 2 licenses, the MIT license and the
+University of Illinois/NCSA Open Source License.
+
+Both are permissive open source licenses, with little if any
+practical difference between them.
+
+The reason for offering both is that (1) the MIT license is
+well-known, while (2) the University of Illinois/NCSA Open Source
+License allows Emscripten's code to be integrated upstream into
+LLVM, which uses that license, should the opportunity arise.
+
+The full text of both licenses follows.
+
+==============================================================================
+
+Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+==============================================================================
+
+Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file.
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal with the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+ Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimers.
+
+ Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following disclaimers
+ in the documentation and/or other materials provided with the
+ distribution.
+
+ Neither the names of Mozilla,
+ nor the names of its contributors may be used to endorse
+ or promote products derived from this Software without specific prior
+ written permission.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE.
+
+==============================================================================
+
+This program uses portions of Node.js source code located in src/library_path.js,
+in accordance with the terms of the MIT license. Node's license follows:
+
+ """
+ Copyright Joyent, Inc. and other Node contributors. All rights reserved.
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+ """
+
+The musl libc project is bundled in this repo, and it has the MIT license, see
+system/lib/libc/musl/COPYRIGHT
+
+The third_party/ subdirectory contains code with other licenses. None of it is
+used by default, but certain options use it (e.g., the optional closure compiler
+flag will run closure compiler from third_party/).
+
diff --git a/src/compat/crypto/LICENSE.musl b/src/compat/crypto/LICENSE.musl
new file mode 100644
index 00000000..c1628e9a
--- /dev/null
+++ b/src/compat/crypto/LICENSE.musl
@@ -0,0 +1,193 @@
+musl as a whole is licensed under the following standard MIT license:
+
+----------------------------------------------------------------------
+Copyright © 2005-2020 Rich Felker, et al.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+----------------------------------------------------------------------
+
+Authors/contributors include:
+
+A. Wilcox
+Ada Worcester
+Alex Dowad
+Alex Suykov
+Alexander Monakov
+Andre McCurdy
+Andrew Kelley
+Anthony G. Basile
+Aric Belsito
+Arvid Picciani
+Bartosz Brachaczek
+Benjamin Peterson
+Bobby Bingham
+Boris Brezillon
+Brent Cook
+Chris Spiegel
+Clément Vasseur
+Daniel Micay
+Daniel Sabogal
+Daurnimator
+David Carlier
+David Edelsohn
+Denys Vlasenko
+Dmitry Ivanov
+Dmitry V. Levin
+Drew DeVault
+Emil Renner Berthing
+Fangrui Song
+Felix Fietkau
+Felix Janda
+Gianluca Anzolin
+Hauke Mehrtens
+He X
+Hiltjo Posthuma
+Isaac Dunham
+Jaydeep Patil
+Jens Gustedt
+Jeremy Huntwork
+Jo-Philipp Wich
+Joakim Sindholt
+John Spencer
+Julien Ramseier
+Justin Cormack
+Kaarle Ritvanen
+Khem Raj
+Kylie McClain
+Leah Neukirchen
+Luca Barbato
+Luka Perkov
+M Farkas-Dyck (Strake)
+Mahesh Bodapati
+Markus Wichmann
+Masanori Ogino
+Michael Clark
+Michael Forney
+Mikhail Kremnyov
+Natanael Copa
+Nicholas J. Kain
+orc
+Pascal Cuoq
+Patrick Oppenlander
+Petr Hosek
+Petr Skocik
+Pierre Carrier
+Reini Urban
+Rich Felker
+Richard Pennington
+Ryan Fairfax
+Samuel Holland
+Segev Finer
+Shiz
+sin
+Solar Designer
+Stefan Kristiansson
+Stefan O'Rear
+Szabolcs Nagy
+Timo Teräs
+Trutz Behn
+Valentin Ochs
+Will Dietz
+William Haddon
+William Pitcock
+
+Portions of this software are derived from third-party works licensed
+under terms compatible with the above MIT license:
+
+The TRE regular expression implementation (src/regex/reg* and
+src/regex/tre*) is Copyright © 2001-2008 Ville Laurikari and licensed
+under a 2-clause BSD license (license text in the source files). The
+included version has been heavily modified by Rich Felker in 2012, in
+the interests of size, simplicity, and namespace cleanliness.
+
+Much of the math library code (src/math/* and src/complex/*) is
+Copyright © 1993,2004 Sun Microsystems or
+Copyright © 2003-2011 David Schultz or
+Copyright © 2003-2009 Steven G. Kargl or
+Copyright © 2003-2009 Bruce D. Evans or
+Copyright © 2008 Stephen L. Moshier or
+Copyright © 2017-2018 Arm Limited
+and labelled as such in comments in the individual source files. All
+have been licensed under extremely permissive terms.
+
+The ARM memcpy code (src/string/arm/memcpy.S) is Copyright © 2008
+The Android Open Source Project and is licensed under a two-clause BSD
+license. It was taken from Bionic libc, used on Android.
+
+The AArch64 memcpy and memset code (src/string/aarch64/*) are
+Copyright © 1999-2019, Arm Limited.
+
+The implementation of DES for crypt (src/crypt/crypt_des.c) is
+Copyright © 1994 David Burren. It is licensed under a BSD license.
+
+The implementation of blowfish crypt (src/crypt/crypt_blowfish.c) was
+originally written by Solar Designer and placed into the public
+domain. The code also comes with a fallback permissive license for use
+in jurisdictions that may not recognize the public domain.
+
+The smoothsort implementation (src/stdlib/qsort.c) is Copyright © 2011
+Valentin Ochs and is licensed under an MIT-style license.
+
+The x86_64 port was written by Nicholas J. Kain and is licensed under
+the standard MIT terms.
+
+The mips and microblaze ports were originally written by Richard
+Pennington for use in the ellcc project. The original code was adapted
+by Rich Felker for build system and code conventions during upstream
+integration. It is licensed under the standard MIT terms.
+
+The mips64 port was contributed by Imagination Technologies and is
+licensed under the standard MIT terms.
+
+The powerpc port was also originally written by Richard Pennington,
+and later supplemented and integrated by John Spencer. It is licensed
+under the standard MIT terms.
+
+All other files which have no copyright comments are original works
+produced specifically for use as part of this library, written either
+by Rich Felker, the main author of the library, or by one or more
+contibutors listed above. Details on authorship of individual files
+can be found in the git version control history of the project. The
+omission of copyright and license comments in each file is in the
+interest of source tree size.
+
+In addition, permission is hereby granted for all public header files
+(include/* and arch/*/bits/*) and crt files intended to be linked into
+applications (crt/*, ldso/dlstart.c, and arch/*/crt_arch.h) to omit
+the copyright notice and permission notice otherwise required by the
+license, and to use these files without any requirement of
+attribution. These files include substantial contributions from:
+
+Bobby Bingham
+John Spencer
+Nicholas J. Kain
+Rich Felker
+Richard Pennington
+Stefan Kristiansson
+Szabolcs Nagy
+
+all of whom have explicitly granted such permission.
+
+This file previously contained text expressing a belief that most of
+the files covered by the above exception were sufficiently trivial not
+to be subject to copyright, resulting in confusion over whether it
+negated the permissions granted in the license. In the spirit of
+permissive licensing, and of not having licensing issues being an
+obstacle to adoption, that text has been removed.
diff --git a/src/compat/crypto/LICENSE.openssl b/src/compat/crypto/LICENSE.openssl
new file mode 100644
index 00000000..49cc83d2
--- /dev/null
+++ b/src/compat/crypto/LICENSE.openssl
@@ -0,0 +1,177 @@
+
+ Apache License
+ Version 2.0, January 2004
+ https://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/src/compat/crypto/asymmetric.ts b/src/compat/crypto/asymmetric.ts
new file mode 100644
index 00000000..bcb99fb3
--- /dev/null
+++ b/src/compat/crypto/asymmetric.ts
@@ -0,0 +1,332 @@
+import { Buffer } from "node:buffer";
+import { NativeKey, nativeTimingEqual } from "./native-crypto.js";
+import { importJwk } from "./key-formats.js";
+import {
+ create,
+ KeyObject,
+ PrivateKeyObject,
+ PublicKeyObject,
+} from "./keys.js";
+import { keyStates } from "./key-state.js";
+import { bytes, validateInteger } from "./validation.js";
+import {
+ validateObject,
+ validateString,
+ validateOneOf,
+ validateFunction,
+ validateUint32,
+} from "../net/validators.js";
+import { ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE } from "../net/errors.js";
+function handle(key: KeyObject): NativeKey {
+ const value = keyStates.get(key)?.asymmetric;
+ if (!value)
+ throw Object.assign(
+ new Error(
+ "Synchronous access to opaque host key material is unavailable",
+ ),
+ { code: "ERR_METHOD_NOT_IMPLEMENTED" },
+ );
+ return value;
+}
+function input(value: any, privateKey: boolean): NativeKey {
+ if (value == null)
+ throw new ERR_INVALID_ARG_TYPE(
+ "key",
+ ["ArrayBuffer", "Buffer", "TypedArray", "DataView", "string", "object"],
+ value,
+ );
+ const options =
+ typeof value === "string" ||
+ value instanceof ArrayBuffer ||
+ ArrayBuffer.isView(value)
+ ? { key: value }
+ : value;
+ const data = options.key;
+ if (
+ data == null ||
+ data instanceof KeyObject ||
+ data instanceof globalThis.CryptoKey
+ )
+ throw new ERR_INVALID_ARG_TYPE(
+ "options.key",
+ ["ArrayBuffer", "Buffer", "TypedArray", "DataView", "string", "object"],
+ data,
+ );
+ if (
+ typeof data === "object" &&
+ !(data instanceof ArrayBuffer) &&
+ !ArrayBuffer.isView(data)
+ )
+ return importJwk(data, privateKey);
+ const format = options.format ?? "pem";
+ validateOneOf(format, "format", ["pem", "der"]);
+ if (options.type !== undefined)
+ validateOneOf(
+ options.type,
+ "type",
+ privateKey ? ["pkcs1", "pkcs8", "sec1"] : ["pkcs1", "spki"],
+ );
+ const raw =
+ typeof data === "string"
+ ? Buffer.from(data, options.encoding ?? "utf8")
+ : bytes(data, "key");
+ const password =
+ options.passphrase === undefined
+ ? undefined
+ : typeof options.passphrase === "string"
+ ? Buffer.from(options.passphrase, options.encoding ?? "utf8")
+ : bytes(options.passphrase, "passphrase");
+ if (privateKey) return NativeKey.parse(raw, format === "pem", true, password);
+ try {
+ return NativeKey.parse(raw, format === "pem", false, password);
+ } catch (error) {
+ // createPublicKey also accepts a serialized private key, deriving public material.
+ let key: NativeKey;
+ try {
+ key = NativeKey.parse(raw, format === "pem", true, password);
+ } catch {
+ throw error;
+ }
+ try {
+ return NativeKey.parse(key.export(false, false, "spki"), false, false);
+ } finally {
+ key.dispose();
+ }
+ }
+}
+export function createPrivateKey(value: unknown): PrivateKeyObject {
+ return create("private", {
+ asymmetric: input(value, true),
+ }) as PrivateKeyObject;
+}
+export function createPublicKey(value: any): PublicKeyObject {
+ if (keyStates.has(value)) {
+ if (value.type !== "private")
+ throw new ERR_INVALID_ARG_TYPE("key", "PrivateKeyObject", value);
+ return create("public", {
+ asymmetric: NativeKey.parse(
+ handle(value).export(false, false, "spki"),
+ false,
+ false,
+ ),
+ }) as PublicKeyObject;
+ }
+ return create("public", {
+ asymmetric: input(value, false),
+ }) as PublicKeyObject;
+}
+export function generateKeyPairSync(type: string, options: any = {}) {
+ validateOneOf(type, "type", ["rsa", "ec", "ed25519", "x25519", "dh"]);
+ validateObject(options, "options");
+ let key: NativeKey;
+ if (type === "rsa") {
+ validateUint32(options.modulusLength, "options.modulusLength");
+ validateUint32(options.publicExponent ?? 65537, "options.publicExponent");
+ key = NativeKey.generate(
+ "RSA",
+ options.modulusLength,
+ undefined,
+ options.publicExponent ?? 65537,
+ );
+ } else if (type === "ec") {
+ validateString(options.namedCurve, "options.namedCurve");
+ validateOneOf(options.paramEncoding ?? "named", "options.paramEncoding", [
+ "named",
+ "explicit",
+ ]);
+ key = NativeKey.generate("EC", 0, options.namedCurve);
+ } else if (type === "dh") {
+ const groups: Record = {
+ modp1: "modp_768",
+ modp2: "modp_1024",
+ modp5: "modp_1536",
+ modp14: "modp_2048",
+ modp15: "modp_3072",
+ modp16: "modp_4096",
+ modp17: "modp_6144",
+ modp18: "modp_8192",
+ };
+ const group = options.group ?? options.groupName;
+ validateString(group, "options.group");
+ key = NativeKey.generate("DH", 0, groups[group] ?? group);
+ } else key = NativeKey.generate(type.toUpperCase());
+ const privateKey = create("private", { asymmetric: key }) as PrivateKeyObject;
+ const publicKey = createPublicKey(privateKey);
+ return {
+ publicKey: options.publicKeyEncoding
+ ? publicKey.export(options.publicKeyEncoding)
+ : publicKey,
+ privateKey: options.privateKeyEncoding
+ ? privateKey.export(options.privateKeyEncoding)
+ : privateKey,
+ };
+}
+export function generateKeyPair(
+ type: string,
+ options: any,
+ callback: any,
+): void {
+ if (typeof options === "function") {
+ callback = options;
+ options = {};
+ }
+ validateFunction(callback, "callback");
+ let result: ReturnType | undefined,
+ failure: unknown;
+ try {
+ result = generateKeyPairSync(type, options);
+ } catch (error) {
+ failure = error;
+ }
+ queueMicrotask(() =>
+ failure
+ ? callback(failure)
+ : callback(null, result!.publicKey, result!.privateKey),
+ );
+}
+function optionsOf(value: any) {
+ return value &&
+ typeof value === "object" &&
+ !keyStates.has(value) &&
+ "key" in value
+ ? value
+ : { key: value };
+}
+function operationKey(value: any, privateKey: boolean): KeyObject {
+ const key = optionsOf(value).key;
+ if (keyStates.has(key)) {
+ if (key.type === "secret" || (privateKey && key.type !== "private"))
+ throw new TypeError("Invalid key type");
+ return key;
+ }
+ return privateKey ? createPrivateKey(value) : createPublicKey(value);
+}
+function digestName(algorithm: any): string | null {
+ if (algorithm == null) return null;
+ validateString(algorithm, "algorithm");
+ return algorithm;
+}
+export function sign(
+ algorithm: any,
+ data: unknown,
+ key: any,
+ callback?: any,
+): any {
+ if (callback !== undefined) validateFunction(callback, "callback");
+ const digest = digestName(algorithm),
+ raw = bytes(data, "data");
+ const execute = () => {
+ const options = optionsOf(key);
+ return Buffer.from(
+ handle(operationKey(key, true)).sign(
+ digest,
+ raw,
+ options.padding ?? 0,
+ options.saltLength ?? -1,
+ options.dsaEncoding === "ieee-p1363",
+ ),
+ );
+ };
+ if (callback === undefined) return execute();
+ let result: Buffer | undefined, failure: unknown;
+ try {
+ result = execute();
+ } catch (error) {
+ failure = error;
+ }
+ queueMicrotask(() => callback(failure ?? null, result));
+}
+export function verify(
+ algorithm: any,
+ data: unknown,
+ key: any,
+ signature: unknown,
+ callback?: any,
+): any {
+ if (callback !== undefined) validateFunction(callback, "callback");
+ const digest = digestName(algorithm),
+ raw = bytes(data, "data"),
+ sig = bytes(signature, "signature");
+ const execute = () => {
+ const options = optionsOf(key);
+ return handle(operationKey(key, false)).verify(
+ digest,
+ raw,
+ sig,
+ options.padding ?? 0,
+ options.saltLength ?? -1,
+ options.dsaEncoding === "ieee-p1363",
+ );
+ };
+ if (callback === undefined) return execute();
+ let result: boolean | undefined, failure: unknown;
+ try {
+ result = execute();
+ } catch (error) {
+ failure = error;
+ }
+ queueMicrotask(() => callback(failure ?? null, result));
+}
+export function diffieHellman(options: any): Buffer {
+ validateObject(options, "options");
+ const { publicKey, privateKey } = options;
+ if (!keyStates.has(publicKey))
+ throw new ERR_INVALID_ARG_TYPE("options.publicKey", "KeyObject", publicKey);
+ if (!keyStates.has(privateKey))
+ throw new ERR_INVALID_ARG_TYPE(
+ "options.privateKey",
+ "KeyObject",
+ privateKey,
+ );
+ if (publicKey.type !== "public")
+ throw new ERR_INVALID_ARG_TYPE(
+ "options.publicKey",
+ "public key",
+ publicKey,
+ );
+ if (privateKey.type !== "private")
+ throw new ERR_INVALID_ARG_TYPE(
+ "options.privateKey",
+ "private key",
+ privateKey,
+ );
+ if (publicKey.asymmetricKeyType !== privateKey.asymmetricKeyType)
+ throw new ERR_INVALID_ARG_VALUE(
+ "options.publicKey.asymmetricKeyType",
+ publicKey.asymmetricKeyType,
+ "must equal privateKey.asymmetricKeyType",
+ );
+ const publicHandle = handle(publicKey),
+ privateHandle = handle(privateKey);
+
+ // The pinned workerd crypto-keys.c++ passes the public key as the first
+ // DHPointer::stateless argument. Preserve its observable failure rather than
+ // silently substituting standalone Node behavior.
+ try {
+ return Buffer.from(publicHandle.derive(privateHandle));
+ } catch {
+ throw new Error("Failed to derive shared diffie-hellman secret");
+ }
+}
+export function timingSafeEqual(a: unknown, b: unknown): boolean {
+ if (!(a instanceof ArrayBuffer) && !ArrayBuffer.isView(a))
+ throw new ERR_INVALID_ARG_TYPE(
+ "buf1",
+ ["ArrayBuffer", "Buffer", "TypedArray", "DataView"],
+ a,
+ );
+ if (!(b instanceof ArrayBuffer) && !ArrayBuffer.isView(b))
+ throw new ERR_INVALID_ARG_TYPE(
+ "buf2",
+ ["ArrayBuffer", "Buffer", "TypedArray", "DataView"],
+ b,
+ );
+ const left = bytes(a, "buf1"),
+ right = bytes(b, "buf2");
+ if (left.length !== right.length)
+ throw Object.assign(
+ new RangeError("Input buffers must have the same byte length"),
+ { code: "ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH" },
+ );
+ return nativeTimingEqual(left, right);
+}
diff --git a/src/compat/crypto/brorand.cjs b/src/compat/crypto/brorand.cjs
new file mode 100644
index 00000000..e407ebe7
--- /dev/null
+++ b/src/compat/crypto/brorand.cjs
@@ -0,0 +1,76 @@
+/* Browser-only adaptation of brorand 1.1.0.
+
+This software is licensed under the MIT License.
+
+Copyright Fedor Indutny, 2014.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*/
+var r;
+
+module.exports = function rand(len) {
+ if (!r) r = new Rand(null);
+
+ return r.generate(len);
+};
+
+function Rand(rand) {
+ this.rand = rand;
+}
+module.exports.Rand = Rand;
+
+Rand.prototype.generate = function generate(len) {
+ return this._rand(len);
+};
+
+// Emulate crypto API using randy
+Rand.prototype._rand = function _rand(n) {
+ if (this.rand.getBytes) return this.rand.getBytes(n);
+
+ var res = new Uint8Array(n);
+ for (var i = 0; i < res.length; i++) res[i] = this.rand.getByte();
+ return res;
+};
+
+if (typeof self === "object") {
+ if (self.crypto && self.crypto.getRandomValues) {
+ // Modern browsers
+ Rand.prototype._rand = function _rand(n) {
+ var arr = new Uint8Array(n);
+ self.crypto.getRandomValues(arr);
+ return arr;
+ };
+ } else if (self.msCrypto && self.msCrypto.getRandomValues) {
+ // IE
+ Rand.prototype._rand = function _rand(n) {
+ var arr = new Uint8Array(n);
+ self.msCrypto.getRandomValues(arr);
+ return arr;
+ };
+
+ // Safari's WebWorkers do not have `crypto`
+ } else if (typeof window === "object") {
+ // Old junk
+ Rand.prototype._rand = function () {
+ throw new Error("Not implemented yet");
+ };
+ }
+}
diff --git a/src/compat/crypto/certificate.ts b/src/compat/crypto/certificate.ts
new file mode 100644
index 00000000..4a389fba
--- /dev/null
+++ b/src/compat/crypto/certificate.ts
@@ -0,0 +1,102 @@
+// @ts-nocheck
+// Copyright (c) 2017-2022 Cloudflare, Inc.
+// Licensed under the Apache 2.0 license found in the LICENSE file or at:
+// https://opensource.org/licenses/Apache-2.0
+//
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { nativeSpkac } from "./native-crypto.js";
+import { bytes } from "./validation.js";
+import { isAnyArrayBuffer } from "../util/types/index.js";
+const cryptoImpl = {
+ verifySpkac: (data) => nativeSpkac(data, 0),
+ exportPublicKey: (data) => nativeSpkac(data, 1),
+ exportChallenge: (data) => nativeSpkac(data, 2),
+};
+import { Buffer } from "node:buffer";
+
+function getArrayBufferOrView(value, name, encoding) {
+ if (typeof value === "string")
+ return Buffer.from(value, encoding === "buffer" ? undefined : encoding);
+ if (isAnyArrayBuffer(value)) return new Uint8Array(value);
+ return bytes(value, name);
+}
+
+export function verifySpkac(
+ spkac: Buffer | ArrayBuffer | ArrayBufferView | string,
+ encoding?: string,
+): boolean {
+ return cryptoImpl.verifySpkac(getArrayBufferOrView(spkac, "spkac", encoding));
+}
+
+export function exportPublicKey(
+ spkac: Buffer | ArrayBuffer | ArrayBufferView | string,
+ encoding?: string,
+): Buffer {
+ const ret = cryptoImpl.exportPublicKey(
+ getArrayBufferOrView(spkac, "spkac", encoding),
+ );
+ return ret ? Buffer.from(ret) : Buffer.alloc(0);
+}
+
+export function exportChallenge(
+ spkac: Buffer | ArrayBuffer | ArrayBufferView | string,
+ encoding?: string,
+): Buffer {
+ const ret = cryptoImpl.exportChallenge(
+ getArrayBufferOrView(spkac, "spkac", encoding),
+ );
+ return ret ? Buffer.from(ret) : Buffer.alloc(0);
+}
+
+// The legacy implementation of this exposed the Certificate
+// object and required that users create an instance before
+// calling the member methods. This API pattern has been
+// deprecated, however, as the method implementations do not
+// rely on any object state.
+
+export declare class Certificate {
+ constructor();
+ verifySpkac: typeof verifySpkac;
+ exportPublicKey: typeof exportPublicKey;
+ exportChallenge: typeof exportChallenge;
+ static verifySpkac: typeof verifySpkac;
+ static exportPublicKey: typeof exportPublicKey;
+ static exportChallenge: typeof exportChallenge;
+}
+
+// For backwards compatibility reasons, this cannot be converted into a
+// ES6 Class.
+export function Certificate(this: unknown): Certificate {
+ if (!(this instanceof Certificate)) {
+ return new Certificate();
+ }
+ return this;
+}
+
+Certificate.prototype.verifySpkac = verifySpkac;
+Certificate.prototype.exportPublicKey = exportPublicKey;
+Certificate.prototype.exportChallenge = exportChallenge;
+
+Certificate.exportChallenge = exportChallenge;
+Certificate.exportPublicKey = exportPublicKey;
+Certificate.verifySpkac = verifySpkac;
diff --git a/src/compat/crypto/ecdh.ts b/src/compat/crypto/ecdh.ts
new file mode 100644
index 00000000..a27f1ff5
--- /dev/null
+++ b/src/compat/crypto/ecdh.ts
@@ -0,0 +1,172 @@
+// @ts-nocheck
+// Ported from pinned workerd crypto_dh.ts; see LICENSE-APACHE and LICENSE-3RD-PARTY.md.
+import { Buffer } from "node:buffer";
+import { NativeECDH } from "./native-crypto.js";
+import { validateString, validateOneOf } from "../net/validators.js";
+import { ERR_INVALID_ARG_TYPE } from "../net/errors.js";
+import { isArrayBufferView } from "../util/types/index.js";
+const cryptoImpl = { ECDHHandle: NativeECDH };
+const kHandle = Symbol("kHandle");
+export interface ECDH {
+ [kHandle]: cryptoImpl.ECDHHandle;
+ computeSecret(
+ otherPublicKey: string | ArrayBufferView | ArrayBuffer,
+ inputEncoding?: string,
+ outputEncoding?: string,
+ ): Buffer | string;
+ generateKeys(encoding?: string, format?: string): Buffer | string;
+ getPrivateKey(encoding?: string): Buffer | string;
+ getPublicKey(encoding?: string, format?: string): Buffer | string;
+ setPrivateKey(
+ key: string | ArrayBufferView | ArrayBuffer,
+ encoding?: string,
+ ): void;
+}
+
+export const ECDH = function (this: ECDH, curveName: string) {
+ if (!(this instanceof ECDH)) {
+ return new ECDH(curveName);
+ }
+ validateString(curveName, "curveName");
+ this[kHandle] = new cryptoImpl.ECDHHandle(curveName);
+ return this;
+} as unknown as {
+ new (curveName: string): ECDH;
+};
+
+// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ECDH.prototype.computeSecret = function (
+ this: ECDH,
+ otherPublicKey: string | ArrayBufferView | ArrayBuffer,
+ inputEncoding?: string,
+ outputEncoding?: string,
+): Buffer | string {
+ if (typeof otherPublicKey === "string") {
+ otherPublicKey = Buffer.from(otherPublicKey, inputEncoding);
+ }
+ if (!isArrayBufferView(otherPublicKey)) {
+ throw new ERR_INVALID_ARG_TYPE(
+ "otherPublicKey",
+ ["string", "Buffer", "TypedArray", "DataView"],
+ otherPublicKey,
+ );
+ }
+ if (inputEncoding != null) {
+ validateString(inputEncoding, "inputEncoding");
+ }
+ if (outputEncoding != null) {
+ validateString(outputEncoding, "outputEncoding");
+ }
+ const ret = this[kHandle].computeSecret(otherPublicKey);
+ if (typeof outputEncoding === "string") {
+ return Buffer.from(ret).toString(outputEncoding);
+ }
+ return Buffer.from(ret);
+};
+
+// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ECDH.prototype.generateKeys = function (
+ this: ECDH,
+ encoding?: string,
+ format?: string,
+): Buffer | string {
+ if (encoding != null) {
+ validateString(encoding, "encoding");
+ }
+ if (format != null) {
+ validateOneOf(format, "format", ["compressed", "uncompressed", "hybrid"]);
+ }
+ this[kHandle].generateKeys();
+ return this.getPublicKey(encoding, format);
+};
+
+// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ECDH.prototype.getPrivateKey = function (
+ this: ECDH,
+ encoding?: string,
+): Buffer | string {
+ if (encoding != null) {
+ validateString(encoding, "encoding");
+ }
+ const pvt = this[kHandle].getPrivateKey();
+ if (typeof encoding === "string") {
+ return Buffer.from(pvt).toString(encoding);
+ }
+ return Buffer.from(pvt);
+};
+
+// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ECDH.prototype.getPublicKey = function (
+ this: ECDH,
+ encoding?: string,
+ format: ECDHFormat = "uncompressed",
+): Buffer | string {
+ if (encoding != null) {
+ validateString(encoding, "encoding");
+ }
+ validateOneOf(format, "format", ["compressed", "uncompressed", "hybrid"]);
+ const pub = this[kHandle].getPublicKey(format);
+ if (typeof encoding === "string") {
+ return Buffer.from(pub).toString(encoding);
+ }
+ return Buffer.from(pub);
+};
+
+// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ECDH.prototype.setPrivateKey = function (
+ this: ECDH,
+ key: string | ArrayBufferView | ArrayBuffer,
+ encoding?: string,
+): void {
+ if (encoding != null) {
+ validateString(encoding, "encoding");
+ }
+ if (typeof key === "string") {
+ key = Buffer.from(key, encoding);
+ }
+ if (!isArrayBufferView(key)) {
+ throw new ERR_INVALID_ARG_TYPE(
+ "key",
+ ["string", "Buffer", "TypedArray", "DataView"],
+ key,
+ );
+ }
+ this[kHandle].setPrivateKey(key);
+};
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access
+(ECDH as any).convertKey = function (
+ key: string | ArrayBufferView | ArrayBuffer,
+ curve: string,
+ inputEncoding?: string,
+ outputEncoding?: string,
+ format: ECDHFormat = "uncompressed",
+): Buffer | string {
+ if (typeof key === "string") {
+ key = Buffer.from(key, inputEncoding);
+ }
+ if (!isArrayBufferView(key)) {
+ throw new ERR_INVALID_ARG_TYPE(
+ "key",
+ ["string", "Buffer", "TypedArray", "DataView"],
+ key,
+ );
+ }
+ validateString(curve, "curve");
+ if (inputEncoding != null) {
+ validateString(inputEncoding, "inputEncoding");
+ }
+ if (outputEncoding != null) {
+ validateString(outputEncoding, "outputEncoding");
+ }
+ validateOneOf(format, "format", ["compressed", "uncompressed", "hybrid"]);
+ const ret = cryptoImpl.ECDHHandle.convertKey(key, curve, format);
+ if (typeof outputEncoding === "string") {
+ return Buffer.from(ret).toString(outputEncoding);
+ }
+ return Buffer.from(ret);
+};
+
+export function createECDH(curveName: string): ECDH {
+ return new ECDH(curveName);
+}
diff --git a/src/compat/crypto/hkdf.ts b/src/compat/crypto/hkdf.ts
new file mode 100644
index 00000000..5f7ed0c4
--- /dev/null
+++ b/src/compat/crypto/hkdf.ts
@@ -0,0 +1,85 @@
+import { secretMaterial } from "./keys.js";
+// RFC 5869 extract-and-expand, using the existing HMAC implementation.
+import { Buffer } from "node:buffer";
+// @ts-expect-error dependency has no declarations
+import original from "crypto-browserify/index.js";
+import { bytes, validateInteger } from "./validation.js";
+import { validateString, validateFunction } from "../net/validators.js";
+import { ERR_OUT_OF_RANGE } from "../net/errors.js";
+function parameters(
+ digest: string,
+ key: unknown,
+ salt: unknown,
+ info: unknown,
+ length: number,
+) {
+ validateString(digest, "digest");
+ const ikm = bytes(secretMaterial(key), "ikm"),
+ s = bytes(salt, "salt"),
+ i = bytes(info, "info");
+ validateInteger(length, "length", 0, 2147483647);
+ if (i.byteLength > 1024)
+ throw new ERR_OUT_OF_RANGE(
+ "info",
+ "must not contain more than 1024 bytes",
+ i.byteLength,
+ );
+ return { digest, ikm, s, i, length };
+}
+function derive({
+ digest,
+ ikm,
+ s,
+ i,
+ length,
+}: ReturnType): ArrayBuffer {
+ const prk = original.createHmac(digest, s).update(ikm).digest();
+ if (length > 255 * prk.length)
+ throw Object.assign(new RangeError("Invalid key length"), {
+ code: "ERR_CRYPTO_INVALID_KEYLEN",
+ });
+ const output = Buffer.alloc(length);
+ let previous = Buffer.alloc(0);
+ try {
+ for (let offset = 0, counter = 1; offset < length; counter++) {
+ previous = original
+ .createHmac(digest, prk)
+ .update(previous)
+ .update(i)
+ .update(Buffer.from([counter]))
+ .digest();
+ const size = Math.min(previous.length, length - offset);
+ output.set(previous.subarray(0, size), offset);
+ offset += size;
+ }
+ return new Uint8Array(output).buffer;
+ } finally {
+ prk.fill(0);
+ previous.fill(0);
+ }
+}
+export function hkdfSync(
+ digest: string,
+ key: unknown,
+ salt: unknown,
+ info: unknown,
+ length: number,
+) {
+ return derive(parameters(digest, key, salt, info, length));
+}
+export function hkdf(
+ digest: string,
+ key: unknown,
+ salt: unknown,
+ info: unknown,
+ length: number,
+ callback: (error: unknown, value?: ArrayBuffer) => void,
+) {
+ const args = parameters(digest, key, salt, info, length);
+ validateFunction(callback, "callback");
+ const result = new Promise((resolve) => resolve(derive(args)));
+ result.then(
+ (value) => callback(null, value),
+ (error) => callback(error),
+ );
+}
diff --git a/src/compat/crypto/index.cjs b/src/compat/crypto/index.cjs
new file mode 100644
index 00000000..7c72726b
--- /dev/null
+++ b/src/compat/crypto/index.cjs
@@ -0,0 +1 @@
+module.exports = require("./index.js").default;
diff --git a/src/compat/crypto/index.ts b/src/compat/crypto/index.ts
new file mode 100644
index 00000000..942e2dad
--- /dev/null
+++ b/src/compat/crypto/index.ts
@@ -0,0 +1,146 @@
+import { X509Certificate } from "./x509.js";
+export { X509Certificate };
+import { Certificate } from "./certificate.js";
+export { Certificate };
+import { ECDH, createECDH } from "./ecdh.js";
+export { ECDH, createECDH };
+import * as signatures from "./signatures.js";
+export * from "./signatures.js";
+import * as prime from "./prime.js";
+export * from "./prime.js";
+import * as asymmetric from "./asymmetric.js";
+export {
+ createPrivateKey,
+ createPublicKey,
+ generateKeyPair,
+ generateKeyPairSync,
+ diffieHellman,
+ timingSafeEqual,
+} from "./asymmetric.js";
+import * as keys from "./keys.js";
+export {
+ KeyObject,
+ SecretKeyObject,
+ PublicKeyObject,
+ PrivateKeyObject,
+ createSecretKey,
+ generateKey,
+ generateKeySync,
+} from "./keys.js";
+import * as metadata from "./metadata.js";
+export * from "./metadata.js";
+// Existing algorithms retain the dependency implementation. Additional APIs
+// below are separately verified against the pinned Workers profile.
+// @ts-expect-error crypto-browserify has no declarations
+import original from "crypto-browserify/index.js";
+import { hkdf, hkdfSync } from "./hkdf.js";
+import { randomInt, randomUUID } from "./random.js";
+import { scrypt, scryptSync } from "./scrypt.js";
+import { validateString } from "../net/validators.js";
+import { bytes } from "./validation.js";
+export { hkdf, hkdfSync, randomInt, randomUUID, scrypt, scryptSync };
+export const {
+ prng,
+ pseudoRandomBytes,
+ rng,
+ randomBytes,
+ Hash,
+ createHash,
+ getHashes,
+ pbkdf2,
+ pbkdf2Sync,
+ Cipher,
+ createCipher,
+ Decipher,
+ createDecipher,
+ getCiphers,
+ listCiphers,
+ DiffieHellmanGroup,
+ createDiffieHellmanGroup,
+ getDiffieHellman,
+ createDiffieHellman,
+ DiffieHellman,
+ randomFill,
+ randomFillSync,
+ createCredentials,
+ constants,
+} = original;
+import {
+ Hmac,
+ Cipheriv,
+ Decipheriv,
+ createHmac,
+ createCipheriv,
+ createDecipheriv,
+} from "./symmetric.js";
+export {
+ Hmac,
+ Cipheriv,
+ Decipheriv,
+ createHmac,
+ createCipheriv,
+ createDecipheriv,
+};
+import {
+ publicEncrypt,
+ privateEncrypt,
+ publicDecrypt,
+ privateDecrypt,
+} from "./rsa.js";
+export { publicEncrypt, privateEncrypt, publicDecrypt, privateDecrypt };
+export const webcrypto = globalThis.crypto;
+export const subtle = webcrypto.subtle;
+export const getRandomValues = webcrypto.getRandomValues.bind(webcrypto);
+export const CryptoKey = globalThis.CryptoKey;
+export function hash(
+ algorithm: string,
+ data: unknown,
+ outputEncoding: string = "hex",
+) {
+ validateString(algorithm, "algorithm");
+ validateString(outputEncoding, "outputEncoding");
+ const result = original
+ .createHash(algorithm)
+ .update(bytes(data, "data"))
+ .digest();
+ return outputEncoding === "buffer" ? result : result.toString(outputEncoding);
+}
+export default {
+ X509Certificate,
+ Certificate,
+ ...original,
+ ...metadata,
+ ...asymmetric,
+ ...signatures,
+ ...prime,
+ publicEncrypt,
+ privateEncrypt,
+ publicDecrypt,
+ privateDecrypt,
+ ECDH,
+ createECDH,
+ KeyObject: keys.KeyObject,
+ SecretKeyObject: keys.SecretKeyObject,
+ PublicKeyObject: keys.PublicKeyObject,
+ PrivateKeyObject: keys.PrivateKeyObject,
+ createSecretKey: keys.createSecretKey,
+ generateKey: keys.generateKey,
+ generateKeySync: keys.generateKeySync,
+ Hmac,
+ createHmac,
+ Cipheriv,
+ createCipheriv,
+ Decipheriv,
+ createDecipheriv,
+ hkdf,
+ hkdfSync,
+ webcrypto,
+ subtle,
+ getRandomValues,
+ CryptoKey,
+ randomInt,
+ randomUUID,
+ hash,
+ scrypt,
+ scryptSync,
+};
diff --git a/src/compat/crypto/key-formats.ts b/src/compat/crypto/key-formats.ts
new file mode 100644
index 00000000..1c524092
--- /dev/null
+++ b/src/compat/crypto/key-formats.ts
@@ -0,0 +1,143 @@
+import { Buffer } from "node:buffer";
+import { NativeKey } from "./native-crypto.js";
+// DER encodings follow RFC 5208/5915/8410; JWK members follow RFC 7518/8037.
+const curves: Record = {
+ "P-256": { oid: "2a8648ce3d030107", size: 32, native: "prime256v1" },
+ "P-384": { oid: "2b81040022", size: 48, native: "secp384r1" },
+ "P-521": { oid: "2b81040023", size: 66, native: "secp521r1" },
+ secp256k1: { oid: "2b8104000a", size: 32, native: "secp256k1" },
+};
+export function der(tag: number, ...parts: Uint8Array[]): Buffer {
+ const data = Buffer.concat(parts);
+ let length: number[] = [];
+ if (data.length < 128) length = [data.length];
+ else {
+ let size = data.length;
+ while (size) {
+ length.unshift(size & 255);
+ size = Math.floor(size / 256);
+ }
+ length.unshift(128 + length.length);
+ }
+ return Buffer.concat([Buffer.from([tag, ...length]), data]);
+}
+export function integer(bytes: Uint8Array): Buffer {
+ let offset = 0;
+ while (offset < bytes.length - 1 && bytes[offset] === 0) offset++;
+ const data = bytes.subarray(offset);
+ return der(2, ...(data[0] & 128 ? [Buffer.from([0]), data] : [data]));
+}
+function field(jwk: any, name: string, size?: number): Buffer {
+ if (typeof jwk[name] !== "string")
+ throw new TypeError(`Missing JWK member: ${name}`);
+ const bytes = Buffer.from(jwk[name], "base64url");
+ if (size !== undefined && bytes.length !== size)
+ throw new TypeError(`Invalid JWK member length: ${name}`);
+ if (!bytes.length) throw new TypeError(`Empty JWK member: ${name}`);
+ return bytes;
+}
+export function importJwk(jwk: any, privateKey: boolean): NativeKey {
+ if (jwk?.kty === "RSA") {
+ const names = privateKey
+ ? ["n", "e", "d", "p", "q", "dp", "dq", "qi"]
+ : ["n", "e"];
+ return NativeKey.parse(
+ der(
+ 48,
+ ...(privateKey ? [integer(Buffer.from([0]))] : []),
+ ...names.map((name) => integer(field(jwk, name))),
+ ),
+ false,
+ privateKey,
+ );
+ }
+ if (jwk?.kty === "EC") {
+ const curve = curves[jwk.crv];
+ if (!curve) throw new TypeError("Unsupported JWK curve");
+ const oid = der(6, Buffer.from(curve.oid, "hex"));
+ const point = Buffer.concat([
+ Buffer.from([4]),
+ field(jwk, "x", curve.size),
+ field(jwk, "y", curve.size),
+ ]);
+ const bits = der(3, Buffer.from([0]), point);
+ const data = privateKey
+ ? der(
+ 48,
+ integer(Buffer.from([1])),
+ der(4, field(jwk, "d", curve.size)),
+ der(160, oid),
+ der(161, bits),
+ )
+ : der(
+ 48,
+ der(48, der(6, Buffer.from("2a8648ce3d0201", "hex")), oid),
+ bits,
+ );
+ return NativeKey.parse(data, false, privateKey);
+ }
+ if (jwk?.kty === "OKP" && ["Ed25519", "X25519"].includes(jwk.crv)) {
+ const algorithm = der(
+ 48,
+ der(6, Buffer.from([43, 101, jwk.crv === "Ed25519" ? 112 : 110])),
+ );
+ const data = privateKey
+ ? der(
+ 48,
+ integer(Buffer.from([0])),
+ algorithm,
+ der(4, der(4, field(jwk, "d", 32))),
+ )
+ : der(48, algorithm, der(3, Buffer.from([0]), field(jwk, "x", 32)));
+ return NativeKey.parse(data, false, privateKey);
+ }
+ throw new TypeError("Unsupported JWK key type");
+}
+function encode(bytes: Uint8Array, size?: number): string {
+ if (size !== undefined && bytes.length < size)
+ bytes = Buffer.concat([Buffer.alloc(size - bytes.length), bytes]);
+ return Buffer.from(bytes).toString("base64url");
+}
+export function exportJwk(key: NativeKey, privateKey: boolean): any {
+ if (key.type === "rsa") {
+ const result: any = {
+ kty: "RSA",
+ n: encode(key.parameter("n")),
+ e: encode(key.parameter("e")),
+ };
+ if (privateKey)
+ for (const [name, param] of [
+ ["d", "d"],
+ ["p", "rsa-factor1"],
+ ["q", "rsa-factor2"],
+ ["dp", "rsa-exponent1"],
+ ["dq", "rsa-exponent2"],
+ ["qi", "rsa-coefficient1"],
+ ])
+ result[name] = encode(key.parameter(param));
+ return result;
+ }
+ if (key.type === "ec") {
+ const found = Object.entries(curves).find(
+ ([, value]) => value.native === key.group,
+ );
+ if (!found) throw new TypeError("Unsupported JWK curve");
+ const [crv, curve] = found;
+ return {
+ kty: "EC",
+ crv,
+ x: encode(key.parameter("qx"), curve.size),
+ y: encode(key.parameter("qy"), curve.size),
+ ...(privateKey ? { d: encode(key.parameter("priv"), curve.size) } : {}),
+ };
+ }
+ if (["ed25519", "x25519"].includes(key.type))
+ return {
+ kty: "OKP",
+ ...(key.type === "ed25519" ? { alg: "EdDSA" } : {}),
+ crv: key.type === "ed25519" ? "Ed25519" : "X25519",
+ x: encode(key.raw(false)),
+ ...(privateKey ? { d: encode(key.raw(true)) } : {}),
+ };
+ throw new TypeError("Unsupported JWK key type");
+}
diff --git a/src/compat/crypto/key-state.ts b/src/compat/crypto/key-state.ts
new file mode 100644
index 00000000..3e771d07
--- /dev/null
+++ b/src/compat/crypto/key-state.ts
@@ -0,0 +1,12 @@
+import type { NativeKey } from "./native-crypto.js";
+// Shared branding without importing crypto's algorithm dependency graph.
+export interface KeyState {
+ type: "secret" | "private" | "public";
+ bytes?: Uint8Array;
+ asymmetric?: NativeKey;
+ native?: CryptoKey;
+}
+export const keyStates = new WeakMap