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(); +export function isKeyObject(value: unknown): boolean { + return typeof value === "object" && value !== null && keyStates.has(value); +} diff --git a/src/compat/crypto/keys.ts b/src/compat/crypto/keys.ts new file mode 100644 index 00000000..4b70f2ee --- /dev/null +++ b/src/compat/crypto/keys.ts @@ -0,0 +1,248 @@ +import { exportJwk } from "./key-formats.js"; +// API behavior follows workerd@5358e3d337336eaf696a48bc1563d808b10feba5 +// crypto_keys.ts (Apache-2.0 and Node MIT). Host CryptoKey export is an explicit +// synchronous-engine boundary; adapter-created secret keys own copied bytes. +import { Buffer } from "node:buffer"; +import { keyStates, type KeyState } from "./key-state.js"; +import { bytes, validateInteger } from "./validation.js"; +import { + validateObject, + validateString, + validateOneOf, +} from "../net/validators.js"; +import { ERR_INVALID_ARG_TYPE } from "../net/errors.js"; +function state(value: object): KeyState { + const result = keyStates.get(value); + if (!result) throw new ERR_INVALID_ARG_TYPE("key", "KeyObject", value); + return result; +} +function boundary(): never { + throw Object.assign( + new Error( + "Synchronous export of a host CryptoKey is unavailable in this WebView runtime", + ), + { code: "ERR_METHOD_NOT_IMPLEMENTED" }, + ); +} +export function create( + type: KeyState["type"], + value: Omit, +): KeyObject { + const Class = + type === "secret" + ? SecretKeyObject + : type === "public" + ? PublicKeyObject + : PrivateKeyObject; + const result = Object.create(Class.prototype) as KeyObject; + keyStates.set(result, { type, ...value }); + return result; +} +export abstract class KeyObject { + constructor() { + throw new Error("Illegal constructor"); + } + static from(key: CryptoKey): KeyObject { + if (!(key instanceof globalThis.CryptoKey)) + throw new ERR_INVALID_ARG_TYPE("key", "CryptoKey", key); + return create(key.type, { native: key }); + } + abstract get type(): KeyState["type"]; + get [Symbol.toStringTag]() { + return "KeyObject"; + } + export(options: any = {}): any { + validateObject(options, "options"); + if (options.format === undefined) options.format = "buffer"; + validateString(options.format, "options.format"); + if (options.type !== undefined) + validateString(options.type, "options.type"); + const current = state(this); + if (current.asymmetric) { + if (options.format === "jwk") + return exportJwk(current.asymmetric, current.type === "private"); + validateOneOf(options.format, "options.format", ["pem", "der"]); + validateOneOf( + options.type, + "options.type", + current.type === "private" + ? ["pkcs1", "pkcs8", "sec1"] + : ["pkcs1", "spki"], + ); + if (options.cipher !== undefined) + validateString(options.cipher, "options.cipher"); + const passphrase = + options.passphrase === undefined + ? undefined + : bytes(options.passphrase, "options.passphrase"); + const result = current.asymmetric.export( + current.type === "private", + options.format === "pem", + options.type, + options.cipher, + passphrase, + ); + return options.format === "pem" + ? new TextDecoder().decode(result) + : Buffer.from(result); + } + const data = current.bytes; + if (!data) return boundary(); + if (options.format === "buffer") return Buffer.from(data); + if (options.format === "jwk") + return { + kty: "oct", + ext: true, + k: Buffer.from(data) + .toString("base64") + .replace(/=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_"), + }; + throw new TypeError( + `Invalid format for secret key export: ${options.format}`, + ); + } + equals(other: KeyObject): boolean { + if (other == null) + throw new TypeError("Cannot read properties of null or undefined"); + const a = state(this), + b = keyStates.get(other); + if (this === other || (a.native && a.native === b?.native)) return true; + if (a.type !== other?.type) return false; + if (!b) + throw new ERR_INVALID_ARG_TYPE("otherKeyObject", "KeyObject", other); + if (a.asymmetric && b.asymmetric) return a.asymmetric.equals(b.asymmetric); + if (!a.bytes || !b.bytes) return boundary(); + if (a.bytes.length !== b.bytes.length) return false; + let difference = 0; + for (let i = 0; i < a.bytes.length; i++) + difference |= a.bytes[i] ^ b.bytes[i]; + return difference === 0; + } +} +export class SecretKeyObject extends KeyObject { + get type(): "secret" { + return "secret"; + } + override export(options?: any) { + return super.export(options); + } + get symmetricKeySize() { + const key = state(this); + return ( + key.bytes?.length ?? ((key.native?.algorithm as any)?.length ?? 0) | 0 + ); + } + [Symbol.for("nodejs.util.inspect.custom")]() { + return `SecretKeyObject { size: ${this.symmetricKeySize} }`; + } +} +abstract class AsymmetricKeyObject extends KeyObject { + get asymmetricKeyType() { + const key = state(this).asymmetric; + if (!key) return boundary(); + return key.type; + } + get asymmetricKeyDetails() { + const key = state(this).asymmetric; + if (!key) return boundary(); + if (key.type === "rsa") + return { + modulusLength: key.bits, + publicExponent: BigInt( + "0x" + Buffer.from(key.parameter("e")).toString("hex"), + ), + }; + if (key.type === "ec") return { namedCurve: key.group }; + return {}; + } + toCryptoKey() { + throw Object.assign( + new Error("The toCryptoKey method is not implemented"), + { code: "ERR_METHOD_NOT_IMPLEMENTED" }, + ); + } +} +export class PublicKeyObject extends AsymmetricKeyObject { + get type(): "public" { + return "public"; + } + override export(options?: any) { + return super.export(options); + } +} +export class PrivateKeyObject extends AsymmetricKeyObject { + get type(): "private" { + return "private"; + } + override export(options?: any) { + return super.export(options); + } +} +export function createSecretKey( + value: unknown, + encoding?: BufferEncoding, +): SecretKeyObject { + if ( + typeof value !== "string" && + !(value instanceof ArrayBuffer) && + !ArrayBuffer.isView(value) && + !( + typeof SharedArrayBuffer !== "undefined" && + value instanceof SharedArrayBuffer + ) + ) + throw new ERR_INVALID_ARG_TYPE( + "key", + ["string", "ArrayBuffer", "TypedArray", "DataView"], + value, + ); + const data = + typeof value === "string" + ? Buffer.from(value, encoding) + : typeof SharedArrayBuffer !== "undefined" && + value instanceof SharedArrayBuffer + ? new Uint8Array(value) + : bytes(value, "key"); + return create("secret", { bytes: new Uint8Array(data) }) as SecretKeyObject; +} +export function secretMaterial(value: unknown): any { + if (typeof value !== "object" || value === null || !keyStates.has(value)) + return value; + const key = state(value); + if (key.type !== "secret") throw new TypeError("Expected a secret key"); + if (!key.bytes) return boundary(); + return Buffer.from(key.bytes); +} +export function generateKeySync( + type: string, + options: { length: number }, +): SecretKeyObject { + validateOneOf(type, "type", ["hmac", "aes"]); + validateObject(options, "options"); + if (type === "hmac") + validateInteger(options.length, "options.length", 8, 65536); + else validateOneOf(options.length, "options.length", [128, 192, 256]); + const material = new Uint8Array(Math.floor(options.length / 8)); + globalThis.crypto.getRandomValues(material); + return createSecretKey(material); +} +export function generateKey( + type: string, + options: { length: number }, + callback: (error: unknown, key?: SecretKeyObject) => void, +): void { + let key: SecretKeyObject | undefined, failure: unknown; + try { + key = generateKeySync(type, options); + } catch (error) { + failure = error; + } + queueMicrotask(() => callback(failure ?? null, key)); +} + +export { isKeyObject } from "./key-state.js"; +export function getKeyObjectHandle(key: KeyObject) { + return key; +} diff --git a/src/compat/crypto/metadata.ts b/src/compat/crypto/metadata.ts new file mode 100644 index 00000000..0a574ee8 --- /dev/null +++ b/src/compat/crypto/metadata.ts @@ -0,0 +1,22 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under Apache-2.0: https://opensource.org/licenses/Apache-2.0 +// Adapted from workerd@5358e3d337336eaf696a48bc1563d808b10feba5. +export function getCurves(): string[] { + return ["secp224r1", "prime256v1", "secp384r1", "secp521r1"]; +} +export function secureHeapUsed(): Record { + return { total: 0, used: 0, utilization: 0, min: 0 }; +} +function unavailable(method: string): never { + throw Object.assign(new Error(`The ${method} method is not implemented`), { + code: "ERR_METHOD_NOT_IMPLEMENTED", + }); +} +export function setEngine(_engine: string, _flags?: number): never { + return unavailable("setEngine"); +} +export function setFips(_enabled: boolean): never { + return unavailable("setFips"); +} +// Do not copy Workers' fips=true: this adapter uses JavaScript algorithms, +// not Workers' FIPS backend. That capability remains an explicit boundary. diff --git a/src/compat/crypto/native-crypto.build.json b/src/compat/crypto/native-crypto.build.json new file mode 100644 index 00000000..fdd5bb62 --- /dev/null +++ b/src/compat/crypto/native-crypto.build.json @@ -0,0 +1,40 @@ +{ + "revisions": { + "emsdk": "e4fe26ef59168ff44f4c23c466e497bf60b3411e", + "openssl": "45e844fa2a14ec92d146bd8f5778ac130b6625fb" + }, + "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" + ], + "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\"]" + ], + "libraries": ["libssl.a", "libcrypto.a"], + "bindingSha256": "819bf16122da0df3c5ce7b6158b23700b8d42778fcb0e14f3a9e054d5f68831f", + "sha256": "b25197493fce8f3b40615654f877b3dec064d90e3cd1a34dc86695dd3bb68c50" +} diff --git a/src/compat/crypto/native-crypto.c b/src/compat/crypto/native-crypto.c new file mode 100644 index 00000000..fd193da5 --- /dev/null +++ b/src/compat/crypto/native-crypto.c @@ -0,0 +1,538 @@ +// OpenSSL bridge owned by cf-workers-native (MIT). +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define API EMSCRIPTEN_KEEPALIVE +static unsigned char *output; +static size_t output_length; +static void clear_output(void) { OPENSSL_clear_free(output, output_length); output=NULL; output_length=0; } +static int save_bio(BIO *bio) { + char *data; long size=BIO_get_mem_data(bio,&data); + if(size<0) return 0; + clear_output(); output=OPENSSL_memdup(data,(size_t)size);output_length=(size_t)size; + return output != NULL; +} +API int wk_init(void) { return OPENSSL_init_crypto(OPENSSL_INIT_NO_LOAD_CONFIG,NULL); } +API void wk_output_clear(void) { clear_output(); } +API void *wk_output(void) { return output; } +API size_t wk_output_length(void) { return output_length; } +API unsigned long wk_error(void) { return ERR_get_error(); } +API const char *wk_error_reason(unsigned long error) { return ERR_reason_error_string(error); } +API int wk_equal(const void *a,const void *b,size_t size) { return CRYPTO_memcmp(a,b,size)==0; } +struct password_input {const unsigned char *bytes;int length;}; +static int password_callback(char *buffer,int size,int writing,void *opaque) { + (void)writing;struct password_input *password=opaque; + if(!password || password->length>size)return -1; + if(password->length)memcpy(buffer,password->bytes,password->length); + return password->length; +} +API EVP_PKEY *wk_key_parse(const unsigned char *data,int size,int pem,int private_key,const unsigned char *password,int password_length) { + ERR_clear_error(); BIO *bio=BIO_new_mem_buf(data,size); if(!bio)return NULL; + struct password_input pass={password,password_length}; + EVP_PKEY *key=NULL; + if(pem) { + key=private_key?PEM_read_bio_PrivateKey(bio,NULL,password_callback,&pass):PEM_read_bio_PUBKEY(bio,NULL,NULL,NULL); + } else { + const unsigned char *cursor=data; + key=private_key?d2i_AutoPrivateKey(NULL,&cursor,size):d2i_PUBKEY(NULL,&cursor,size); + if(!key && private_key) { ERR_clear_error();BIO_reset(bio);key=d2i_PKCS8PrivateKey_bio(bio,NULL,password_callback,&pass); } + if(!key && !private_key) { cursor=data; RSA *rsa=d2i_RSAPublicKey(NULL,&cursor,size);if(rsa){key=EVP_PKEY_new();if(!key || !EVP_PKEY_assign_RSA(key,rsa)){RSA_free(rsa);EVP_PKEY_free(key);key=NULL;}} } + } + BIO_free(bio);return key; +} +API void wk_key_free(EVP_PKEY *key) { EVP_PKEY_free(key); } +API const char *wk_key_type(EVP_PKEY *key) { return EVP_PKEY_get0_type_name(key); } +API int wk_key_bits(EVP_PKEY *key) { return EVP_PKEY_get_bits(key); } +API int wk_key_equal(EVP_PKEY *a,EVP_PKEY *b) { return EVP_PKEY_eq(a,b); } +API int wk_key_export(EVP_PKEY *key,int private_key,int pem,int type,const char *cipher_name,const char *password,int password_length) { + ERR_clear_error();BIO *bio=BIO_new(BIO_s_mem());if(!bio)return 0; + const EVP_CIPHER *cipher=cipher_name && *cipher_name?EVP_get_cipherbyname(cipher_name):NULL; + if(cipher_name && *cipher_name && !cipher){BIO_free(bio);return 0;} + int ok=0; + if(private_key) { + if(type==0) ok=pem?PEM_write_bio_PKCS8PrivateKey(bio,key,cipher,(char *)password,password_length,NULL,NULL):i2d_PKCS8PrivateKey_bio(bio,key,cipher,(char *)password,password_length,NULL,NULL); + else if(type==1) {RSA *rsa=EVP_PKEY_get1_RSA(key);if(rsa){ok=pem?PEM_write_bio_RSAPrivateKey(bio,rsa,cipher,(const unsigned char *)password,password_length,NULL,NULL):i2d_RSAPrivateKey_bio(bio,rsa);RSA_free(rsa);}} + else if(type==2) {EC_KEY *ec=EVP_PKEY_get1_EC_KEY(key);if(ec){ok=pem?PEM_write_bio_ECPrivateKey(bio,ec,cipher,(const unsigned char *)password,password_length,NULL,NULL):i2d_ECPrivateKey_bio(bio,ec);EC_KEY_free(ec);}} + } else { + if(type==1) {RSA *rsa=EVP_PKEY_get1_RSA(key);if(rsa){ok=pem?PEM_write_bio_RSAPublicKey(bio,rsa):i2d_RSAPublicKey_bio(bio,rsa);RSA_free(rsa);}} + else ok=pem?PEM_write_bio_PUBKEY(bio,key):i2d_PUBKEY_bio(bio,key); + } + if(ok)ok=save_bio(bio);BIO_free(bio);return ok; +} +API EVP_PKEY *wk_key_generate(const char *type,int bits,const char *curve,unsigned long exponent) { + ERR_clear_error();EVP_PKEY_CTX *ctx=EVP_PKEY_CTX_new_from_name(NULL,type,NULL);if(!ctx)return NULL; + EVP_PKEY *key=NULL;int ok=EVP_PKEY_keygen_init(ctx)>0; + if(ok && (!strcmp(type,"RSA") || !strcmp(type,"RSA-PSS")))ok=EVP_PKEY_CTX_set_rsa_keygen_bits(ctx,bits)>0; + if(ok && (!strcmp(type,"RSA") || !strcmp(type,"RSA-PSS"))) { BIGNUM *e=BN_new();ok=e && BN_set_word(e,exponent) && EVP_PKEY_CTX_set1_rsa_keygen_pubexp(ctx,e)>0;BN_free(e); } + if(ok && (!strcmp(type,"EC") || !strcmp(type,"DH")))ok=EVP_PKEY_CTX_set_group_name(ctx,curve)>0; + if(ok)ok=EVP_PKEY_generate(ctx,&key)>0; + EVP_PKEY_CTX_free(ctx);if(!ok){EVP_PKEY_free(key);return NULL;}return key; +} +static int signature_width(EVP_PKEY *key) { + if(EVP_PKEY_is_a(key,"EC"))return (EVP_PKEY_get_bits(key)+7)/8; + if(EVP_PKEY_is_a(key,"DSA")){BIGNUM *q=NULL;if(!EVP_PKEY_get_bn_param(key,OSSL_PKEY_PARAM_FFC_Q,&q))return 0;int size=BN_num_bytes(q);BN_free(q);return size;} + return 0; +} +API int wk_sign(EVP_PKEY *key,const char *digest,const unsigned char *data,size_t length,int padding,int saltlen,int p1363) { + ERR_clear_error();EVP_MD_CTX *ctx=EVP_MD_CTX_new();if(!ctx)return 0;EVP_PKEY_CTX *pctx=NULL; + int ok=EVP_DigestSignInit_ex(ctx,&pctx,digest && *digest?digest:NULL,NULL,NULL,key,NULL)>0; + if(ok && padding)ok=EVP_PKEY_CTX_set_rsa_padding(pctx,padding)>0; + if(ok && padding==RSA_PKCS1_PSS_PADDING)ok=EVP_PKEY_CTX_set_rsa_pss_saltlen(pctx,saltlen)>0; + size_t size=0;if(ok)ok=EVP_DigestSign(ctx,NULL,&size,data,length)>0; + clear_output();if(ok){output=OPENSSL_malloc(size);output_length=size;ok=output && EVP_DigestSign(ctx,output,&output_length,data,length)>0;} + if(ok && p1363 && signature_width(key)) { + int width=signature_width(key);const unsigned char *cursor=output;ECDSA_SIG *signature=d2i_ECDSA_SIG(NULL,&cursor,output_length); + unsigned char *raw=OPENSSL_malloc(2*width);const BIGNUM *r,*s; + if(!signature || !raw)ok=0; + else {ECDSA_SIG_get0(signature,&r,&s);ok=BN_bn2binpad(r,raw,width)==width && BN_bn2binpad(s,raw+width,width)==width;} + ECDSA_SIG_free(signature);clear_output();if(ok){output=raw;output_length=2*width;}else OPENSSL_clear_free(raw,2*width); + } + EVP_MD_CTX_free(ctx);return ok; +} +API int wk_verify(EVP_PKEY *key,const char *digest,const unsigned char *data,size_t length,const unsigned char *signature,size_t signature_length,int padding,int saltlen,int p1363) { + ERR_clear_error();EVP_MD_CTX *ctx=EVP_MD_CTX_new();if(!ctx)return -1;EVP_PKEY_CTX *pctx=NULL; + int ok=EVP_DigestVerifyInit_ex(ctx,&pctx,digest && *digest?digest:NULL,NULL,NULL,key,NULL)>0; + if(ok && padding)ok=EVP_PKEY_CTX_set_rsa_padding(pctx,padding)>0; + if(ok && padding==RSA_PKCS1_PSS_PADDING)ok=EVP_PKEY_CTX_set_rsa_pss_saltlen(pctx,saltlen)>0; + unsigned char *converted=NULL; + if(ok && p1363 && signature_width(key)) { + int width=signature_width(key); + if(signature_length!=(size_t)(2*width)){EVP_MD_CTX_free(ctx);return 0;} + ECDSA_SIG *value=ECDSA_SIG_new();BIGNUM *r=BN_bin2bn(signature,width,NULL),*s=BN_bin2bn(signature+width,width,NULL); + if(!value || !r || !s || !ECDSA_SIG_set0(value,r,s)){ECDSA_SIG_free(value);BN_free(r);BN_free(s);EVP_MD_CTX_free(ctx);return -1;} + int length=i2d_ECDSA_SIG(value,&converted);ECDSA_SIG_free(value);if(length<=0){EVP_MD_CTX_free(ctx);return -1;} + signature=converted;signature_length=(size_t)length; + } + int result=ok?EVP_DigestVerify(ctx,signature,signature_length,data,length):-1;OPENSSL_free(converted);EVP_MD_CTX_free(ctx);return result; +} + +API int wk_derive(EVP_PKEY *key,EVP_PKEY *peer) { + ERR_clear_error();EVP_PKEY_CTX *ctx=EVP_PKEY_CTX_new(key,NULL);if(!ctx)return 0; + size_t size=0;int ok=EVP_PKEY_derive_init(ctx)>0 && EVP_PKEY_derive_set_peer(ctx,peer)>0 && EVP_PKEY_derive(ctx,NULL,&size)>0; + clear_output();if(ok){output=OPENSSL_malloc(size);output_length=size;ok=output && EVP_PKEY_derive(ctx,output,&output_length)>0;} + EVP_PKEY_CTX_free(ctx);return ok; +} +API int wk_key_parameter(EVP_PKEY *key,const char *name) { + ERR_clear_error();BIGNUM *bn=NULL;if(!EVP_PKEY_get_bn_param(key,name,&bn))return 0; + clear_output();output_length=BN_num_bytes(bn);output=OPENSSL_malloc(output_length?output_length:1); + int ok=output!=NULL;if(ok)BN_bn2bin(bn,output);BN_clear_free(bn);return ok; +} +API int wk_key_group(EVP_PKEY *key) { + char name[100];size_t size=0;if(!EVP_PKEY_get_utf8_string_param(key,OSSL_PKEY_PARAM_GROUP_NAME,name,sizeof(name),&size))return 0; + clear_output();output=(unsigned char *)OPENSSL_strdup(name);output_length=size;return output!=NULL; +} +API int wk_key_raw(EVP_PKEY *key,int private_key) { + size_t size=0;int ok=private_key?EVP_PKEY_get_raw_private_key(key,NULL,&size):EVP_PKEY_get_raw_public_key(key,NULL,&size); + clear_output();if(ok){output=OPENSSL_malloc(size);output_length=size;ok=output && (private_key?EVP_PKEY_get_raw_private_key(key,output,&output_length):EVP_PKEY_get_raw_public_key(key,output,&output_length));}return ok; +} +API int wk_prime_check(const unsigned char *data,int length,int checks) { + ERR_clear_error();BIGNUM *value=BN_bin2bn(data,length,NULL);BN_CTX *ctx=BN_CTX_new(); + int result=value && ctx?BN_is_prime_ex(value,checks,ctx,NULL):-1;BN_clear_free(value);BN_CTX_free(ctx);return result; +} +API int wk_prime_generate(int bits,int safe,const unsigned char *add,int add_length,const unsigned char *rem,int rem_length) { + ERR_clear_error();BIGNUM *a=add?BN_bin2bn(add,add_length,NULL):NULL,*r=rem?BN_bin2bn(rem,rem_length,NULL):NULL,*prime=BN_new();BN_CTX *ctx=BN_CTX_new(); + int ok=prime && ctx && (!add || a) && (!rem || r) && BN_generate_prime_ex2(prime,bits,safe,a,r,NULL,ctx); + clear_output();if(ok){output_length=BN_num_bytes(prime);output=OPENSSL_malloc(output_length?output_length:1);ok=output!=NULL;if(ok)BN_bn2bin(prime,output);} + BN_clear_free(a);BN_clear_free(r);BN_clear_free(prime);BN_CTX_free(ctx);return ok; +} +API EVP_MD_CTX *wk_digest_new(const char *algorithm) { + ERR_clear_error();const EVP_MD *md=EVP_get_digestbyname(algorithm);EVP_MD_CTX *ctx=EVP_MD_CTX_new(); + if(!md || !ctx || EVP_DigestInit_ex(ctx,md,NULL)!=1){EVP_MD_CTX_free(ctx);return NULL;}return ctx; +} +API void wk_digest_free(EVP_MD_CTX *ctx) {EVP_MD_CTX_free(ctx);} +API int wk_digest_update(EVP_MD_CTX *ctx,const unsigned char *data,size_t length) {return EVP_DigestUpdate(ctx,data,length);} +API int wk_digest_final(EVP_MD_CTX *ctx) { + unsigned int length=EVP_MAX_MD_SIZE;clear_output();output=OPENSSL_malloc(length);if(!output)return 0; + output_length=length;if(EVP_DigestFinal_ex(ctx,output,&length)!=1){clear_output();return 0;}output_length=length;return 1; +} +API int wk_sign_hash(EVP_PKEY *key,const char *digest,const unsigned char *data,size_t length,int padding,int saltlen) { + ERR_clear_error();EVP_PKEY_CTX *ctx=EVP_PKEY_CTX_new(key,NULL);if(!ctx)return 0; + int ok=EVP_PKEY_sign_init(ctx)>0 && EVP_PKEY_CTX_set_signature_md(ctx,EVP_get_digestbyname(digest))>0; + if(ok && padding)ok=EVP_PKEY_CTX_set_rsa_padding(ctx,padding)>0; + if(ok && padding==RSA_PKCS1_PSS_PADDING)ok=EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx,saltlen)>0; + size_t size=0;if(ok)ok=EVP_PKEY_sign(ctx,NULL,&size,data,length)>0; + clear_output();if(ok){output=OPENSSL_malloc(size);output_length=size;ok=output && EVP_PKEY_sign(ctx,output,&output_length,data,length)>0;}EVP_PKEY_CTX_free(ctx);return ok; +} +API int wk_verify_hash(EVP_PKEY *key,const char *digest,const unsigned char *data,size_t length,const unsigned char *signature,size_t signature_length,int padding,int saltlen) { + ERR_clear_error();EVP_PKEY_CTX *ctx=EVP_PKEY_CTX_new(key,NULL);if(!ctx)return -1; + int ok=EVP_PKEY_verify_init(ctx)>0 && EVP_PKEY_CTX_set_signature_md(ctx,EVP_get_digestbyname(digest))>0; + if(ok && padding)ok=EVP_PKEY_CTX_set_rsa_padding(ctx,padding)>0; + if(ok && padding==RSA_PKCS1_PSS_PADDING)ok=EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx,saltlen)>0; + int result=ok?EVP_PKEY_verify(ctx,signature,signature_length,data,length):-1;EVP_PKEY_CTX_free(ctx);return result; +} +API EC_KEY *wk_ec_new(const char *curve) {return EC_KEY_new_by_curve_name(OBJ_sn2nid(curve));} +API void wk_ec_free(EC_KEY *key) {EC_KEY_free(key);} +API int wk_ec_generate(EC_KEY *key) {return EC_KEY_generate_key(key);} +API int wk_ec_set_private(EC_KEY *key,const unsigned char *data,int length) { + const EC_GROUP *group=EC_KEY_get0_group(key);BIGNUM *private=BN_bin2bn(data,length,NULL),*order=BN_new();EC_POINT *point=EC_POINT_new(group); + int ok=private && order && point && EC_GROUP_get_order(group,order,NULL) && !BN_is_zero(private) && BN_cmp(private,order)<0; + if(ok)ok=EC_POINT_mul(group,point,private,NULL,NULL,NULL) && EC_KEY_set_private_key(key,private) && EC_KEY_set_public_key(key,point); + BN_clear_free(private);BN_free(order);EC_POINT_free(point);return ok; +} +static int save_point(const EC_GROUP *group,const EC_POINT *point,int format) { + clear_output();size_t size=EC_POINT_point2oct(group,point,format,NULL,0,NULL);if(!size)return 0; + output=OPENSSL_malloc(size);output_length=size;return output && EC_POINT_point2oct(group,point,format,output,size,NULL)==size; +} +API int wk_ec_public(EC_KEY *key,int format) {const EC_POINT *point=EC_KEY_get0_public_key(key);return point && save_point(EC_KEY_get0_group(key),point,format);} +API int wk_ec_private(EC_KEY *key) { + const BIGNUM *private=EC_KEY_get0_private_key(key);if(!private)return 0;clear_output();output_length=BN_num_bytes(private);output=OPENSSL_malloc(output_length);return output && BN_bn2bin(private,output)==output_length; +} +API int wk_ec_secret(EC_KEY *key,const unsigned char *data,size_t length) { + if(EC_KEY_check_key(key)!=1)return -2;const EC_GROUP *group=EC_KEY_get0_group(key);EC_POINT *point=EC_POINT_new(group); + if(!point || !EC_POINT_oct2point(group,point,data,length,NULL) || EC_POINT_is_at_infinity(group,point) || EC_POINT_is_on_curve(group,point,NULL)!=1){EC_POINT_free(point);return -3;} + clear_output();output_length=(EC_GROUP_get_degree(group)+7)/8;output=OPENSSL_malloc(output_length);int ok=output && ECDH_compute_key(output,output_length,point,key,NULL)>0;EC_POINT_free(point);return ok; +} +API int wk_ec_convert(EC_KEY *key,const unsigned char *data,size_t length,int format) { + const EC_GROUP *group=EC_KEY_get0_group(key);EC_POINT *point=EC_POINT_new(group); + int ok=point && EC_POINT_oct2point(group,point,data,length,NULL) && save_point(group,point,format);EC_POINT_free(point);return ok; +} +API int wk_rsa(EVP_PKEY *key,int operation,const unsigned char *data,size_t length,int padding,const char *hash,const unsigned char *label,size_t label_length,int has_label) { + ERR_clear_error();EVP_PKEY_CTX *ctx=EVP_PKEY_CTX_new(key,NULL);if(!ctx)return -1; + int initialized=operation==0?EVP_PKEY_encrypt_init(ctx):operation==1?EVP_PKEY_decrypt_init(ctx):operation==2?EVP_PKEY_sign_init(ctx):EVP_PKEY_verify_recover_init(ctx); + int status=-1;if(initialized<=0)goto done; + const EVP_MD *digest=NULL; + if(operation<2){digest=EVP_get_digestbyname(hash);if(!digest){status=-2;goto done;}} + if(EVP_PKEY_CTX_set_rsa_padding(ctx,padding)<=0){status=-3;goto done;} + if(digest && padding==RSA_PKCS1_OAEP_PADDING){ + if(EVP_PKEY_CTX_set_rsa_oaep_md(ctx,digest)!=1){status=-4;goto done;} + if(EVP_PKEY_CTX_set_rsa_mgf1_md(ctx,digest)!=1){status=-5;goto done;} + } + if(operation<2 && has_label){if(padding!=RSA_PKCS1_OAEP_PADDING){status=-6;goto done;}unsigned char *copy=OPENSSL_malloc(label_length?label_length:1);if(!copy){status=-6;goto done;}if(label_length)memcpy(copy,label,label_length); + if(EVP_PKEY_CTX_set0_rsa_oaep_label(ctx,copy,label_length)!=1){OPENSSL_free(copy);status=-6;goto done;} + } + int (*cipher)(EVP_PKEY_CTX*,unsigned char*,size_t*,const unsigned char*,size_t)=operation==0?EVP_PKEY_encrypt:operation==1?EVP_PKEY_decrypt:operation==2?EVP_PKEY_sign:EVP_PKEY_verify_recover; + size_t size=0;if(cipher(ctx,NULL,&size,data,length)!=1){status=-7;goto done;} + clear_output();output=OPENSSL_malloc(size?size:1);output_length=size; + if(!output || cipher(ctx,output,&output_length,data,length)!=1){clear_output();status=-8;goto done;} + status=1; + done: EVP_PKEY_CTX_free(ctx);return status; +} +API int wk_spkac(const unsigned char *data,int length,int operation) { + while(length>0 && (data[length-1]==' ' || data[length-1]=='\n' || data[length-1]=='\r' || data[length-1]=='\t'))length--; + if(!length)return 0; + NETSCAPE_SPKI *spki=NETSCAPE_SPKI_b64_decode((const char*)data,length);if(!spki)return 0; + int ok=0;EVP_PKEY *key=NULL;BIO *bio=NULL; + if(operation==2){ + const ASN1_IA5STRING *challenge=spki->spkac->challenge;clear_output();output_length=ASN1_STRING_length(challenge);output=OPENSSL_malloc(output_length?output_length:1); + if(output){memcpy(output,ASN1_STRING_get0_data(challenge),output_length);ok=1;} + } else { + key=NETSCAPE_SPKI_get_pubkey(spki); + if(key && operation==1){bio=BIO_new(BIO_s_mem());ok=bio && PEM_write_bio_PUBKEY(bio,key) && save_bio(bio);} + // Workers' FIPS backend rejects MD5 signatures. This is an API behavior + // restriction, not a claim that the bundled OpenSSL build is FIPS certified. + else if(key && OBJ_obj2nid(spki->sig_algor.algorithm)!=NID_md5WithRSAEncryption)ok=NETSCAPE_SPKI_verify(spki,key)==1; + } + BIO_free(bio);EVP_PKEY_free(key);NETSCAPE_SPKI_free(spki);return ok; +} + +// Name rendering adapted from workerd crypto/x509.c++ (Apache-2.0). +#include +#include +static const int kX509NameFlagsRFC2253WithinUtf8JSON = XN_FLAG_RFC2253 & ~ASN1_STRFLGS_ESC_MSB & ~ASN1_STRFLGS_ESC_CTRL; +int isSafeAltName(const char* name, size_t length, int utf8) { + for (size_t i = 0; i < length; i++) { + char c = name[i]; + switch (c) { + case '"': + case '\\': + // These mess with encoding rules. + // Fall through. + case ',': + // Commas make it impossible to split the list of subject alternative + // names unambiguously, which is why we have to escape. + // Fall through. + case '\'': + // Single quotes are unlikely to appear in any legitimate values, but they + // could be used to make a value look like it was escaped (i.e., enclosed + // in single/double quotes). + return 0; + default: + if (utf8) { + // In UTF8 strings, we require escaping for any ASCII control character, + // but NOT for non-ASCII characters. Note that all bytes of any code + // point that consists of more than a single byte have their MSB set. + if (((unsigned char)c) < ' ' || c == '\x7f') { + return 0; + } + } else { + // Check if the char is a control character or non-ASCII character. Note + // that char may or may not be a signed type. Regardless, non-ASCII + // values will always be outside of this range. + if (c < ' ' || c > '~') { + return 0; + } + } + } + } + return 1; +} + +void printAltName(BIO* out, const char* name, size_t length, int utf8, const char* safe_prefix) { + if (isSafeAltName(name, length, utf8)) { + // For backward-compatibility, append "safe" names without any + // modifications. + if (safe_prefix != NULL) { + BIO_printf(out, "%s:", safe_prefix); + } + BIO_write(out, name, length); + } else { + // If a name is not "safe", we cannot embed it without special + // encoding. This does not usually happen, but we don't want to hide + // it from the user either. We use JSON compatible escaping here. + BIO_write(out, "\"", 1); + if (safe_prefix != NULL) { + BIO_printf(out, "%s:", safe_prefix); + } + for (size_t j = 0; j < length; j++) { + char c = name[j]; + if (c == '\\') { + BIO_write(out, "\\\\", 2); + } else if (c == '"') { + BIO_write(out, "\\\"", 2); + } else if ((c >= ' ' && c != ',' && c <= '~') || (utf8 && (c & 0x80))) { + // Note that the above condition explicitly excludes commas, which means + // that those are encoded as Unicode escape sequences in the "else" + // block. That is not strictly necessary, and Node.js itself would parse + // it correctly either way. We only do this to account for third-party + // code that might be splitting the string at commas (as Node.js itself + // used to do). + BIO_write(out, &c, 1); + } else { + // Control character or non-ASCII character. We treat everything as + // Latin-1, which corresponds to the first 255 Unicode code points. + const char hex[] = "0123456789abcdef"; + char u[] = {'\\', 'u', '0', '0', hex[(c & 0xf0) >> 4], hex[c & 0x0f]}; + BIO_write(out, u, sizeof(u)); + } + } + BIO_write(out, "\"", 1); + } +} + +void printLatin1AltName(BIO* out, const ASN1_IA5STRING* name, const char* safe_prefix) { + printAltName(out, (const char*)name->data, name->length, 0, safe_prefix); +} + +void printUtf8AltName(BIO* out, const ASN1_UTF8STRING* name, const char* safe_prefix) { + printAltName(out, (const char*)name->data, name->length, 1, safe_prefix); +} + +int printGeneralName(BIO* out, const GENERAL_NAME* gen) { + if (gen->type == GEN_DNS) { + ASN1_IA5STRING* name = gen->d.dNSName; + BIO_write(out, "DNS:", 4); + // Note that the preferred name syntax (see RFCs 5280 and 1034) with + // wildcards is a subset of what we consider "safe", so spec-compliant DNS + // names will never need to be escaped. + printLatin1AltName(out, name, NULL); + } else if (gen->type == GEN_EMAIL) { + ASN1_IA5STRING* name = gen->d.rfc822Name; + BIO_write(out, "email:", 6); + printLatin1AltName(out, name, NULL); + } else if (gen->type == GEN_URI) { + ASN1_IA5STRING* name = gen->d.uniformResourceIdentifier; + BIO_write(out, "URI:", 4); + // The set of "safe" names was designed to include just about any URI, + // with a few exceptions, most notably URIs that contains commas (see + // RFC 2396). In other words, most legitimate URIs will not require + // escaping. + printLatin1AltName(out, name, NULL); + } else if (gen->type == GEN_DIRNAME) { + // Earlier versions of Node.js used X509_NAME_oneline to print the X509_NAME + // object. The format was non standard and should be avoided. The use of + // X509_NAME_oneline is discouraged by OpenSSL but was required for backward + // compatibility. Conveniently, X509_NAME_oneline produced ASCII and the + // output was unlikely to contains commas or other characters that would + // require escaping. However, it SHOULD NOT produce ASCII output since an + // RFC5280 AttributeValue may be a UTF8String. + // Newer versions of Node.js have since switched to X509_NAME_print_ex to + // produce a better format at the cost of backward compatibility. The new + // format may contain Unicode characters and it is likely to contain commas, + // which require escaping. Fortunately, the recently safeguarded function + // PrintAltName handles all of that safely. + BIO_printf(out, "DirName:"); + BIO *tmp = BIO_new(BIO_s_mem()); if (!tmp) return 0; + if (X509_NAME_print_ex(tmp, gen->d.dirn, 0, kX509NameFlagsRFC2253WithinUtf8JSON) < 0) { + BIO_free(tmp); return 0; + } + char* oline = NULL; + long n_bytes = BIO_get_mem_data(tmp, &oline); // NOLINT(runtime/int) + + if (n_bytes < 0 || (n_bytes > 0 && oline == NULL)) { BIO_free(tmp); return 0; } + + printAltName(out, oline, (size_t)n_bytes, 1, NULL); BIO_free(tmp); + } else if (gen->type == GEN_IPADD) { + BIO_printf(out, "IP Address:"); + const ASN1_OCTET_STRING* ip = gen->d.ip; + const unsigned char* b = ip->data; + if (ip->length == 4) { + BIO_printf(out, "%d.%d.%d.%d", b[0], b[1], b[2], b[3]); + } else if (ip->length == 16) { + for (unsigned int j = 0; j < 8; j++) { + uint16_t pair = (b[2 * j] << 8) | b[2 * j + 1]; + BIO_printf(out, (j == 0) ? "%X" : ":%X", pair); + } + } else { + BIO_printf(out, ""); + } + } else if (gen->type == GEN_RID) { + // Unlike OpenSSL's default implementation, never print the OID as text and + // instead always print its numeric representation. + char oline[256] = {0}; + OBJ_obj2txt(oline, sizeof(oline), gen->d.rid, 1); + BIO_printf(out, "Registered ID:%s", oline); + } else if (gen->type == GEN_OTHERNAME) { + // The pinned Workers BoringSSL build uses the pre-OpenSSL-3 rendering. + BIO_printf(out, "othername:"); + } else if (gen->type == GEN_X400) { + // TODO(tniessen): this is what OpenSSL does, implement properly instead + BIO_printf(out, "X400Name:"); + } else if (gen->type == GEN_EDIPARTY) { + // TODO(tniessen): this is what OpenSSL does, implement properly instead + BIO_printf(out, "EdiPartyName:"); + } else { + // This is safe because X509V3_EXT_d2i would have returned NULL in this + // case already. + return 0; + } + + return 1; +} + +int safeX509SubjectAltNamePrint(BIO* out, X509_EXTENSION* ext) { + + + GENERAL_NAMES* names = (GENERAL_NAMES*)X509V3_EXT_d2i(ext); + if (names == NULL) return 0; + + int ok = 1; + + for (int i = 0; i < sk_GENERAL_NAME_num(names); i++) { + GENERAL_NAME* gen = sk_GENERAL_NAME_value(names, i); + + if (i != 0) BIO_write(out, ", ", 2); + + if (!(ok = printGeneralName(out, gen))) { + break; + } + } + sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free); + + return ok; +} + +int safeX509InfoAccessPrint(BIO* out, X509_EXTENSION* ext) { + + + AUTHORITY_INFO_ACCESS* descs = (AUTHORITY_INFO_ACCESS*)X509V3_EXT_d2i(ext); + if (descs == NULL) return 0; + + int ok = 1; + + for (int i = 0; i < sk_ACCESS_DESCRIPTION_num(descs); i++) { + ACCESS_DESCRIPTION* desc = sk_ACCESS_DESCRIPTION_value(descs, i); + + if (i != 0) BIO_write(out, "\n", 1); + + char objtmp[80] = {0}; + i2t_ASN1_OBJECT(objtmp, sizeof(objtmp), desc->method); + BIO_printf(out, "%s - ", objtmp); + if (!(ok = printGeneralName(out, desc->location))) { + break; + } + } + sk_ACCESS_DESCRIPTION_pop_free(descs, ACCESS_DESCRIPTION_free); + + // BoringSSL follows the pre-OpenSSL-3 infoAccess representation. + BIO_write(out, "\n", 1); + + return ok; +} + + +API X509 *wk_x509_parse(const unsigned char *data, int length) { + ERR_clear_error(); BIO *bio=BIO_new_mem_buf(data,length); if(!bio)return NULL; + X509 *cert=PEM_read_bio_X509_AUX(bio,NULL,NULL,NULL); BIO_free(bio); + if(!cert){ERR_clear_error();const unsigned char *p=data;cert=d2i_X509(NULL,&p,length);} + ERR_clear_error();return cert; +} +API void wk_x509_free(X509 *cert) {X509_free(cert);} +API EVP_PKEY *wk_x509_key(X509 *cert) {return X509_get_pubkey(cert);} +API int wk_x509_ca(X509 *cert) {return X509_check_ca(cert)==1;} +API int wk_x509_compare(X509 *cert,void *other,int operation) { + ERR_clear_error(); int ok=operation==0?X509_check_issued(other,cert)==X509_V_OK:operation==1?X509_check_private_key(cert,other)==1:X509_verify(cert,other)==1; + ERR_clear_error();return ok; +} +API int wk_x509_check(X509 *cert,const char *name,int length,int flags,int operation) { + char *peer=NULL;ERR_clear_error();clear_output(); + int ok=operation==0?X509_check_host(cert,name,length,flags,&peer):operation==1?X509_check_email(cert,name,length,flags):X509_check_ip_asc(cert,name,flags); + if(ok==1){output_length=peer?strlen(peer):(size_t)length;output=OPENSSL_memdup(peer?peer:name,output_length);if(!output)ok=-1;} + OPENSSL_free(peer);ERR_clear_error();return ok; +} +static void x509_name_part(BIO *bio,const void *data,int length) { + unsigned char size[4]={(unsigned int)length>>24,(unsigned int)length>>16,(unsigned int)length>>8,(unsigned int)length}; + BIO_write(bio,size,4);BIO_write(bio,data,length); +} +API int wk_x509_field(X509 *cert,int field) { + ERR_clear_error();clear_output(); BIO *bio=BIO_new(BIO_s_mem());if(!bio)return 0;int ok=1; + switch(field) { + case 0: case 1: ok=X509_NAME_print_ex(bio,field==0?X509_get_subject_name(cert):X509_get_issuer_name(cert),0,ASN1_STRFLGS_ESC_2253|ASN1_STRFLGS_ESC_CTRL|ASN1_STRFLGS_UTF8_CONVERT|XN_FLAG_SEP_MULTILINE|XN_FLAG_FN_SN)>0;break; + case 2: case 3: {int index=X509_get_ext_by_NID(cert,field==2?NID_subject_alt_name:NID_info_access,-1);ok=index>=0;if(ok)ok=field==2?safeX509SubjectAltNamePrint(bio,X509_get_ext(cert,index)):safeX509InfoAccessPrint(bio,X509_get_ext(cert,index));break;} + case 4: case 5: ok=ASN1_TIME_print(bio,field==4?X509_get0_notBefore(cert):X509_get0_notAfter(cert));break; + case 6: case 7: case 8: {unsigned char digest[EVP_MAX_MD_SIZE];unsigned int size=0;ok=X509_digest(cert,field==6?EVP_sha1():field==7?EVP_sha256():EVP_sha512(),digest,&size);if(ok)for(unsigned int i=0;i0;if(ok)BIO_write(bio,data,length);OPENSSL_free(data);EC_KEY_free(ec);EVP_PKEY_free(key);break;} + default:ok=0; + } + if(ok)ok=save_bio(bio);BIO_free(bio);ERR_clear_error();return ok; +} + +// Memory-BIO TLS for platforms whose connected transport cannot add TLS. +// The host MUST authenticate the peer chain and hostname before exposing data. +#include +API SSL *wk_tls_new(const char *host) { + ERR_clear_error(); SSL_CTX *ctx=SSL_CTX_new(TLS_client_method());if(!ctx)return NULL; + SSL_CTX_set_min_proto_version(ctx,TLS1_2_VERSION); + // Trust is evaluated by the OS (SecTrust), not a separate bundled CA store. + SSL_CTX_set_verify(ctx,SSL_VERIFY_NONE,NULL); + SSL *ssl=SSL_new(ctx);SSL_CTX_free(ctx);if(!ssl)return NULL; + BIO *read=BIO_new(BIO_s_mem()),*write=BIO_new(BIO_s_mem()); + if(!read || !write){BIO_free(read);BIO_free(write);SSL_free(ssl);return NULL;} + BIO_set_mem_eof_return(read,-1);BIO_set_mem_eof_return(write,-1); + SSL_set_bio(ssl,read,write);SSL_set_connect_state(ssl); + SSL_set_mode(ssl,SSL_MODE_ENABLE_PARTIAL_WRITE|SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + if(host && *host && !SSL_set_tlsext_host_name(ssl,host)){SSL_free(ssl);return NULL;} + return ssl; +} +API void wk_tls_free(SSL *ssl){SSL_free(ssl);} +API int wk_tls_feed(SSL *ssl,const unsigned char *data,int length){return BIO_write(SSL_get_rbio(ssl),data,length)==length;} +API int wk_tls_handshake(SSL *ssl){ERR_clear_error();int r=SSL_do_handshake(ssl);return r==1?1:-SSL_get_error(ssl,r);} +API int wk_tls_read(SSL *ssl){ + ERR_clear_error();clear_output();output_length=65536;output=OPENSSL_malloc(output_length);if(!output)return -1; + int r=SSL_read(ssl,output,output_length);if(r>0){output_length=r;return r;}int error=SSL_get_error(ssl,r);clear_output();return -error; +} +API int wk_tls_write(SSL *ssl,const unsigned char *data,int length){ERR_clear_error();int r=SSL_write(ssl,data,length);return r>0?r:-SSL_get_error(ssl,r);} +API int wk_tls_drain(SSL *ssl){ + clear_output();size_t size=BIO_ctrl_pending(SSL_get_wbio(ssl));if(!size)return 0;if(size>65536)size=65536; + output=OPENSSL_malloc(size);output_length=size;if(!output)return -1; + int r=BIO_read(SSL_get_wbio(ssl),output,size);if(r>0){output_length=r;return r;}clear_output();return -1; +} +API int wk_tls_shutdown(SSL *ssl){ERR_clear_error();int r=SSL_shutdown(ssl);return r>=0?r:-SSL_get_error(ssl,r);} +API int wk_tls_certificate(SSL *ssl,int index){ + STACK_OF(X509) *chain=SSL_get_peer_cert_chain(ssl);if(!chain || index<0 || index>=sk_X509_num(chain))return 0; + BIO *bio=BIO_new(BIO_s_mem());if(!bio)return 0;int ok=i2d_X509_bio(bio,sk_X509_value(chain,index));if(ok)ok=save_bio(bio);BIO_free(bio);return ok; +} diff --git a/src/compat/crypto/native-crypto.ts b/src/compat/crypto/native-crypto.ts new file mode 100644 index 00000000..ce2beb36 --- /dev/null +++ b/src/compat/crypto/native-crypto.ts @@ -0,0 +1,615 @@ +import binary from "./native-crypto.wasm"; +type API = Record any> & { + memory: WebAssembly.Memory; +}; +let instance: API | undefined; +const encoder = new TextEncoder(), + decoder = new TextDecoder(); +function library(): API { + if (instance) return instance; + let api: API; + const wasi = { + random_get(pointer: number, length: number) { + const bytes = new Uint8Array(api.memory.buffer, pointer, length); + for (let offset = 0; offset < length; offset += 65536) + globalThis.crypto.getRandomValues( + bytes.subarray(offset, Math.min(length, offset + 65536)), + ); + return 0; + }, + clock_time_get(clock: number, _precision: bigint, pointer: number) { + const milliseconds = clock === 1 ? performance.now() : Date.now(); + new DataView(api.memory.buffer).setBigUint64( + pointer, + BigInt(Math.floor(milliseconds * 1e6)), + true, + ); + return 0; + }, + environ_sizes_get(count: number, size: number) { + const view = new DataView(api.memory.buffer); + view.setUint32(count, 0, true); + view.setUint32(size, 0, true); + return 0; + }, + environ_get() { + return 0; + }, + fd_read() { + return 8; + }, + fd_write() { + return 8; + }, + fd_seek() { + return 8; + }, + fd_close() { + return 8; + }, + }; + api = new WebAssembly.Instance( + new WebAssembly.Module(binary as BufferSource), + { + env: { + emscripten_notify_memory_growth() {}, + __syscall_getdents64() { + return -52; + }, + }, + wasi_snapshot_preview1: wasi, + }, + ).exports as API; + api._initialize(); + if (!api.wk_init()) throw new Error("Crypto backend initialization failed"); + instance = api; + return api; +} +function text(pointer: number): string { + const data = new Uint8Array(library().memory.buffer); + let end = pointer; + while (data[end]) end++; + return decoder.decode(data.subarray(pointer, end)); +} +function error(): Error { + const api = library(), + code = api.wk_error(); + return Object.assign( + new Error( + code ? text(api.wk_error_reason(code)) : "Cryptographic operation failed", + ), + { code: "ERR_CRYPTO_OPERATION_FAILED" }, + ); +} +function inputs( + values: (string | Uint8Array | null | undefined)[], + operation: (api: API, pointers: number[]) => T, +): T { + const api = library(), + allocated: { pointer: number; length: number }[] = []; + try { + const pointers = values.map((value) => { + if (value == null) return 0; + const data = + typeof value === "string" + ? encoder.encode(value + "\0") + : new Uint8Array(value.buffer, value.byteOffset, value.byteLength); + const pointer = api.malloc(Math.max(1, data.length)); + if (!pointer) throw new RangeError("Memory limit exceeded"); + allocated.push({ pointer, length: data.length }); + new Uint8Array(api.memory.buffer).set(data, pointer); + return pointer; + }); + return operation(api, pointers); + } finally { + for (const { pointer, length } of allocated) { + new Uint8Array(api.memory.buffer, pointer, length).fill(0); + api.free(pointer); + } + } +} +function result(api: API): Uint8Array { + try { + return new Uint8Array( + api.memory.buffer, + api.wk_output(), + api.wk_output_length(), + ).slice(); + } finally { + api.wk_output_clear(); + } +} +const released = new FinalizationRegistry((pointer) => + library().wk_key_free(pointer), +); +export class NativeKey { + checkCertificate(cert: number, operation: number) { + return library().wk_x509_compare(cert, this.handle(), operation) === 1; + } + rsa(operation: number, data: Uint8Array, options: any): Uint8Array { + if (this.type !== "rsa") + throw new Error("publicEncrypt() currently only supports RSA keys"); + const label = options.oaepLabel; + return inputs( + [ + data, + options.oaepHash, + label instanceof ArrayBuffer ? new Uint8Array(label) : label, + ], + (api, [p, h, l]) => { + const status = api.wk_rsa( + this.pointer, + operation, + p, + data.byteLength, + options.padding, + h, + l, + label?.byteLength ?? 0, + +(label !== undefined), + ); + if (status !== 1) + throw new Error( + ( + { + [-1]: + operation === 0 || operation === 2 + ? "Failed to init for encryption" + : "Failed to init for decryption", + [-2]: "Unsupported hash digest", + [-3]: "Failed to set the padding", + [-4]: "Failed to set the digest", + [-5]: "Failed to set the mgf1 digest", + [-6]: "Failed to set the OAEP label", + [-7]: "Failed to determine output size", + [-8]: "Failed to cipher/decipher", + } as Record + )[status], + ); + return result(api); + }, + ); + } + constructor(private pointer: number) { + if (!pointer) throw error(); + released.register(this, pointer, this); + } + dispose() { + if (this.pointer) { + released.unregister(this); + library().wk_key_free(this.pointer); + this.pointer = 0; + } + } + private handle() { + if (!this.pointer) throw new Error("Key has been disposed"); + return this.pointer; + } + static parse( + data: Uint8Array, + pem: boolean, + privateKey: boolean, + password?: Uint8Array, + ) { + return inputs( + [data, password], + (api, [p, pass]) => + new NativeKey( + api.wk_key_parse( + p, + data.length, + +pem, + +privateKey, + pass, + password?.length ?? 0, + ), + ), + ); + } + static generate(type: string, bits = 0, curve?: string, exponent = 65537) { + return inputs( + [type, curve], + (api, [t, c]) => new NativeKey(api.wk_key_generate(t, bits, c, exponent)), + ); + } + get type() { + return text(library().wk_key_type(this.handle())).toLowerCase(); + } + get bits() { + return library().wk_key_bits(this.handle()) as number; + } + equals(other: NativeKey) { + return library().wk_key_equal(this.handle(), other.handle()) === 1; + } + export( + privateKey: boolean, + pem: boolean, + type: "pkcs8" | "pkcs1" | "sec1" | "spki", + cipher?: string, + password?: Uint8Array, + ) { + return inputs([cipher, password], (api, [c, p]) => { + if ( + !api.wk_key_export( + this.handle(), + +privateKey, + +pem, + type === "pkcs1" ? 1 : type === "sec1" ? 2 : 0, + c, + p, + password?.length ?? 0, + ) + ) + throw error(); + return result(api); + }); + } + parameter(name: string) { + return inputs([name], (api, [p]) => { + if (!api.wk_key_parameter(this.handle(), p)) throw error(); + return result(api); + }); + } + raw(privateKey: boolean) { + const api = library(); + if (!api.wk_key_raw(this.handle(), +privateKey)) throw error(); + return result(api); + } + get group() { + const api = library(); + if (!api.wk_key_group(this.handle())) throw error(); + return decoder.decode(result(api)); + } + sign( + digest: string | null, + data: Uint8Array, + padding = 0, + saltLength = -1, + p1363 = false, + ) { + return inputs([digest, data], (api, [d, p]) => { + if ( + !api.wk_sign( + this.handle(), + d, + p, + data.length, + padding, + saltLength, + +p1363, + ) + ) + throw error(); + return result(api); + }); + } + verify( + digest: string | null, + data: Uint8Array, + signature: Uint8Array, + padding = 0, + saltLength = -1, + p1363 = false, + ) { + return inputs([digest, data, signature], (api, [d, p, s]) => { + const value = api.wk_verify( + this.handle(), + d, + p, + data.length, + s, + signature.length, + padding, + saltLength, + +p1363, + ); + if (value < 0) throw error(); + return value === 1; + }); + } + signHash(digest: string, data: Uint8Array, padding = 0, saltLength = -1) { + return inputs([digest, data], (api, [d, p]) => { + if ( + !api.wk_sign_hash(this.handle(), d, p, data.length, padding, saltLength) + ) + throw error(); + return result(api); + }); + } + verifyHash( + digest: string, + data: Uint8Array, + signature: Uint8Array, + padding = 0, + saltLength = -1, + ) { + return inputs([digest, data, signature], (api, [d, p, s]) => { + const valid = api.wk_verify_hash( + this.handle(), + d, + p, + data.length, + s, + signature.length, + padding, + saltLength, + ); + if (valid < 0) throw error(); + return valid === 1; + }); + } + derive(peer: NativeKey) { + const api = library(); + if (!api.wk_derive(this.handle(), peer.handle())) throw error(); + return result(api); + } +} +export function nativeTimingEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) + throw new RangeError("Input buffers must have the same byte length"); + return inputs( + [a, b], + (api, [ap, bp]) => api.wk_equal(ap, bp, a.length) === 1, + ); +} +export function nativePrimeCheck(data: Uint8Array, checks: number): boolean { + return inputs([data], (api, [p]) => { + const result = api.wk_prime_check(p, data.length, checks); + if (result < 0) throw new Error("Error while checking prime"); + return result === 1; + }); +} +export function nativePrimeGenerate( + bits: number, + safe: boolean, + add?: Uint8Array, + rem?: Uint8Array, +): Uint8Array { + return inputs([add, rem], (api, [a, r]) => { + if ( + !api.wk_prime_generate( + bits, + +safe, + a, + add?.length ?? 0, + r, + rem?.length ?? 0, + ) + ) + throw new Error("Error while generating prime"); + return result(api); + }); +} + +const digestReleased = new FinalizationRegistry((pointer) => + library().wk_digest_free(pointer), +); +export class NativeDigest { + private pointer: number; + constructor( + algorithm: string, + private context = "Signing", + ) { + this.pointer = inputs([algorithm], (api, [p]) => api.wk_digest_new(p)); + if (!this.pointer) throw new Error(`Unknown digest: ${algorithm}`); + digestReleased.register(this, this.pointer, this); + } + update(data: Uint8Array) { + if (!this.pointer) + throw new Error(`${this.context} context has already been finalized`); + inputs([data], (api, [p]) => { + if (!api.wk_digest_update(this.pointer, p, data.length)) throw error(); + }); + } + final() { + if (!this.pointer) + throw new Error(`${this.context} context has already been finalized`); + const api = library(); + try { + if (!api.wk_digest_final(this.pointer)) throw error(); + return result(api); + } finally { + api.wk_digest_free(this.pointer); + this.pointer = 0; + digestReleased.unregister(this); + } + } +} + +const ecReleased = new FinalizationRegistry((pointer) => + library().wk_ec_free(pointer), +); +export class NativeECDH { + private pointer: number; + constructor(curve: string) { + if (!["secp224r1", "prime256v1", "secp384r1", "secp521r1"].includes(curve)) + throw new Error("Invalid curve"); + this.pointer = inputs([curve], (api, [p]) => api.wk_ec_new(p)); + if (!this.pointer) + throw new Error("Failed to create key using named curve"); + ecReleased.register(this, this.pointer, this); + } + generateKeys() { + if (!library().wk_ec_generate(this.pointer)) + throw new Error("Failed to generate keys"); + } + getPrivateKey() { + const api = library(); + if (!api.wk_ec_private(this.pointer)) + throw new Error("Failed to get ECDH private key"); + return result(api); + } + getPublicKey(format: string) { + const api = library(); + if (!api.wk_ec_public(this.pointer, ecFormat(format))) + throw new Error("Failed to get ECDH public key"); + return result(api); + } + setPrivateKey(data: Uint8Array) { + inputs([data], (api, [p]) => { + if (!api.wk_ec_set_private(this.pointer, p, data.byteLength)) + throw new Error("Private key is not valid for specified curve."); + }); + } + computeSecret(data: Uint8Array) { + return inputs([data], (api, [p]) => { + const status = api.wk_ec_secret(this.pointer, p, data.byteLength); + if (status !== 1) + throw new Error( + status === -2 + ? "Invalid keypair" + : status === -3 + ? "Failed to set point" + : "Failed to compute ECDH key", + ); + return result(api); + }); + } + static convertKey(data: Uint8Array, curve: string, format: string) { + if (!data.byteLength) return new Uint8Array(); + const key = new NativeECDH(curve); + try { + return inputs([data], (api, [p]) => { + if ( + !api.wk_ec_convert(key.pointer, p, data.byteLength, ecFormat(format)) + ) + throw new Error("Failed to set point"); + return result(api); + }); + } finally { + library().wk_ec_free(key.pointer); + ecReleased.unregister(key); + } + } +} +function ecFormat(format: string) { + if (format === "hybrid") throw new Error("Failed to get public key length"); + return format === "compressed" ? 2 : 4; +} + +export function nativeSpkac( + data: Uint8Array, + operation: number, +): boolean | Uint8Array { + return inputs([data], (api, [p]) => { + const ok = api.wk_spkac(p, data.byteLength, operation); + return operation === 0 ? !!ok : ok ? result(api) : new Uint8Array(); + }); +} + +const x509Released = new FinalizationRegistry((pointer) => + library().wk_x509_free(pointer), +); +export class NativeX509 { + private constructor(private pointer: number) { + x509Released.register(this, pointer); + } + static parse(data: Uint8Array) { + return inputs([data], (api, [p]) => { + const pointer = api.wk_x509_parse(p, data.byteLength); + return pointer ? new NativeX509(pointer) : undefined; + }); + } + field(field: number): Uint8Array | undefined { + const api = library(); + return api.wk_x509_field(this.pointer, field) ? result(api) : undefined; + } + string(field: number): string | undefined { + const value = this.field(field); + return value === undefined ? undefined : decoder.decode(value); + } + get isCA() { + return library().wk_x509_ca(this.pointer) === 1; + } + get publicKey() { + const pointer = library().wk_x509_key(this.pointer); + return pointer ? new NativeKey(pointer) : undefined; + } + check(name: string, flags: number, operation: number) { + return inputs([name], (api, [p]) => { + const status = api.wk_x509_check( + this.pointer, + p, + encoder.encode(name).length, + flags, + operation, + ); + if (status < 0) + throw new Error( + status === -2 + ? operation === 2 + ? "Invalid IP" + : "Invalid name" + : "Operation failed", + ); + return status === 1 ? decoder.decode(result(api)) : undefined; + }); + } + checkIssued(other: NativeX509) { + return library().wk_x509_compare(this.pointer, other.pointer, 0) === 1; + } + checkKey(key: NativeKey, operation: number) { + return key.checkCertificate(this.pointer, operation); + } +} + +const tlsReleased = new FinalizationRegistry((pointer) => + library().wk_tls_free(pointer), +); +export class NativeTLS { + private pointer: number; + constructor(host: string) { + this.pointer = inputs([host], (api, [p]) => api.wk_tls_new(p)); + if (!this.pointer) throw error(); + tlsReleased.register(this, this.pointer, this); + } + private handle() { + if (!this.pointer) throw new Error("TLS session closed"); + return this.pointer; + } + dispose() { + if (this.pointer) { + library().wk_tls_free(this.pointer); + this.pointer = 0; + tlsReleased.unregister(this); + } + } + feed(data: Uint8Array) { + inputs([data], (api, [p]) => { + if (!api.wk_tls_feed(this.handle(), p, data.byteLength)) throw error(); + }); + } + handshake(): number { + return library().wk_tls_handshake(this.handle()); + } + read(): number | Uint8Array { + const api = library(), + status = api.wk_tls_read(this.handle()); + return status > 0 ? result(api) : status; + } + write(data: Uint8Array): number { + return inputs([data], (api, [p]) => + api.wk_tls_write(this.handle(), p, data.byteLength), + ); + } + shutdown(): number { + return library().wk_tls_shutdown(this.handle()); + } + drain(): Uint8Array[] { + const api = library(), + chunks: Uint8Array[] = []; + for (;;) { + const status = api.wk_tls_drain(this.handle()); + if (status === 0) return chunks; + if (status < 0) throw error(); + chunks.push(result(api)); + } + } + certificates(): Uint8Array[] { + const api = library(), + certs: Uint8Array[] = []; + for (let i = 0; api.wk_tls_certificate(this.handle(), i); i++) + certs.push(result(api)); + return certs; + } +} diff --git a/src/compat/crypto/native-crypto.wasm b/src/compat/crypto/native-crypto.wasm new file mode 100755 index 00000000..3bb0cca0 Binary files /dev/null and b/src/compat/crypto/native-crypto.wasm differ diff --git a/src/compat/crypto/native-crypto.wasm.d.ts b/src/compat/crypto/native-crypto.wasm.d.ts new file mode 100644 index 00000000..7fd58709 --- /dev/null +++ b/src/compat/crypto/native-crypto.wasm.d.ts @@ -0,0 +1,2 @@ +declare const bytes: Uint8Array; +export default bytes; diff --git a/src/compat/crypto/prime.ts b/src/compat/crypto/prime.ts new file mode 100644 index 00000000..d02bfaa7 --- /dev/null +++ b/src/compat/crypto/prime.ts @@ -0,0 +1,250 @@ +// Adapted from pinned workerd crypto_random.ts (Apache-2.0 / Node MIT). +import { Buffer } from "node:buffer"; +import { + validateObject, + validateFunction, + validateBoolean, + validateInt32, +} from "../net/validators.js"; +import { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE } from "../net/errors.js"; +import { isAnyArrayBuffer, isArrayBufferView } from "../util/types/index.js"; +import { nativePrimeCheck, nativePrimeGenerate } from "./native-crypto.js"; +function view(value: any): Uint8Array { + return ArrayBuffer.isView(value) + ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) + : new Uint8Array(value); +} +function arrayBufferToUnsignedBigInt(value: any): bigint { + const hex = Buffer.from(view(value)).toString("hex"); + return hex ? BigInt("0x" + hex) : 0n; +} +const cryptoImpl = { + checkPrimeSync(candidate: ArrayBufferView, checks: number) { + const data = view(candidate); + if (checks > 64) throw new RangeError("Invalid number of checks"); + if (data.length > 1024) + throw new RangeError("checkPrime candidate exceeds maximum size"); + return nativePrimeCheck(data, checks); + }, + randomPrime( + bits: number, + safe: boolean, + add?: ArrayBufferView, + rem?: ArrayBufferView, + ): ArrayBuffer { + if (bits > 8192) + throw new RangeError("generatePrime size exceeds maximum (8192 bits)"); + if (add !== undefined) { + const a = arrayBufferToUnsignedBigInt(add), + r = rem === undefined ? undefined : arrayBufferToUnsignedBigInt(rem); + if ( + !( + (a === 12n && r === 11n) || + (a === 24n && r === 23n) || + (a === 60n && r === 59n) + ) + ) + throw new RangeError("Invalid values for add and rem"); + if (a.toString(2).length > bits) + throw new RangeError( + "options.add must not be bigger than size of the requested prime", + ); + } + return nativePrimeGenerate( + bits, + safe, + add === undefined ? undefined : view(add), + rem === undefined ? undefined : view(rem), + ).buffer as ArrayBuffer; + }, +}; +export type PrimeNum = ArrayBuffer | ArrayBufferView | Buffer | bigint; +export interface GeneratePrimeOptions { + add?: PrimeNum; + rem?: PrimeNum; + safe?: boolean; + bigint?: boolean; +} + +export interface CheckPrimeOptions { + checks?: number; +} + +export type GeneratePrimeCallback = ( + err: Error | null, + prime?: bigint | ArrayBuffer, +) => void; +export type CheckPrimeCallback = (err: Error | null, prime?: boolean) => void; + +function processGeneratePrimeOptions(options: GeneratePrimeOptions): { + add: ArrayBufferView; + rem: ArrayBufferView; + safe: boolean; + bigint: boolean; +} { + validateObject(options, "options"); + const { safe = false, bigint = false } = options; + let { add, rem } = options; + validateBoolean(safe, "options.safe"); + validateBoolean(bigint, "options.bigint"); + + if (add !== undefined) { + if (typeof add === "bigint") { + add = unsignedBigIntToBuffer(add, "options.add"); + } else if (!isAnyArrayBuffer(add) && !isArrayBufferView(add)) { + throw new ERR_INVALID_ARG_TYPE( + "options.add", + ["ArrayBuffer", "TypedArray", "Buffer", "DataView", "bigint"], + add, + ); + } + } + + if (rem !== undefined) { + if (typeof rem === "bigint") { + rem = unsignedBigIntToBuffer(rem, "options.rem"); + } else if (!isAnyArrayBuffer(rem) && !isArrayBufferView(rem)) { + throw new ERR_INVALID_ARG_TYPE( + "options.rem", + ["ArrayBuffer", "TypedArray", "Buffer", "DataView", "bigint"], + rem, + ); + } + } + + return { + safe, + bigint, + add: add as ArrayBufferView, + rem: rem as ArrayBufferView, + }; +} + +export function generatePrimeSync( + size: number, + options: GeneratePrimeOptions = {}, +): bigint | ArrayBuffer { + validateInt32(size, "size", 1); + const { safe, bigint, add, rem } = processGeneratePrimeOptions(options); + + const primeBuf = cryptoImpl.randomPrime(size, safe, add, rem); + return bigint ? arrayBufferToUnsignedBigInt(primeBuf) : primeBuf; +} + +export function generatePrime( + size: number, + options: GeneratePrimeOptions, + callback: GeneratePrimeCallback, +): void; +export function generatePrime( + size: number, + callback: GeneratePrimeCallback, +): void; +export function generatePrime( + size: number, + options: GeneratePrimeOptions | GeneratePrimeCallback, + callback?: GeneratePrimeCallback, +): void { + validateInt32(size, "size", 1); + if (typeof options === "function") { + callback = options; + options = {}; + } + validateFunction(callback, "callback"); + + const { safe, bigint, add, rem } = processGeneratePrimeOptions(options); + + new Promise((res, rej) => { + try { + const primeBuf = cryptoImpl.randomPrime(size, safe, add, rem); + res(bigint ? arrayBufferToUnsignedBigInt(primeBuf) : primeBuf); + } catch (err) { + rej(err as Error); + } + }).then( + (val: bigint | ArrayBuffer): void => { + callback(null, val); + }, + (err: unknown): void => { + callback(err as Error); + }, + ); +} + +function unsignedBigIntToBuffer(bigint: bigint, name: string): Buffer { + if (bigint < 0) { + throw new ERR_OUT_OF_RANGE(name, ">= 0", bigint); + } + + const hex = bigint.toString(16); + const padded = hex.padStart(hex.length + (hex.length % 2), "0"); + return Buffer.from(padded, "hex"); +} + +function validateCandidate(candidate: PrimeNum): Buffer { + if (typeof candidate === "bigint") + candidate = unsignedBigIntToBuffer(candidate, "candidate"); + if (!isAnyArrayBuffer(candidate) && !isArrayBufferView(candidate)) { + throw new ERR_INVALID_ARG_TYPE( + "candidate", + ["ArrayBuffer", "TypedArray", "Buffer", "DataView", "bigint"], + candidate, + ); + } + return candidate as Buffer; +} + +function validateChecks(options: CheckPrimeOptions): number { + const { checks = 0 } = options; + // The checks option is unsigned but must fit into a signed 32-bit integer for OpenSSL. + validateInt32(checks, "options.checks", 0); + return checks; +} + +export function checkPrimeSync( + candidate: PrimeNum, + options: CheckPrimeOptions = {}, +): boolean { + candidate = validateCandidate(candidate); + validateObject(options, "options"); + const checks = validateChecks(options); + return cryptoImpl.checkPrimeSync(candidate as ArrayBufferView, checks); +} + +export function checkPrime( + candidate: PrimeNum, + options: CheckPrimeOptions, + callback: CheckPrimeCallback, +): void; +export function checkPrime( + candidate: PrimeNum, + callback: CheckPrimeCallback, +): void; +export function checkPrime( + candidate: PrimeNum, + options: CheckPrimeOptions | CheckPrimeCallback, + callback?: CheckPrimeCallback, +): void { + candidate = validateCandidate(candidate); + if (typeof options === "function") { + callback = options; + options = {}; + } + validateObject(options, "options"); + validateFunction(callback, "callback"); + const checks = validateChecks(options); + new Promise((res, rej) => { + try { + res(cryptoImpl.checkPrimeSync(candidate as ArrayBufferView, checks)); + } catch (err) { + rej(err as Error); + } + }).then( + (val: boolean): void => { + callback(null, val); + }, + (err: unknown): void => { + callback(err); + }, + ); +} diff --git a/src/compat/crypto/random.ts b/src/compat/crypto/random.ts new file mode 100644 index 00000000..4b8cfa1f --- /dev/null +++ b/src/compat/crypto/random.ts @@ -0,0 +1,154 @@ +// 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. + +// Adapted from workerd@5358e3d337336eaf696a48bc1563d808b10feba5. +import { Buffer } from "node:buffer"; +import type { RandomUUIDOptions } from "node:crypto"; +import { validateInteger } from "./validation.js"; +import { validateBoolean, validateObject } from "../net/validators.js"; +import { ERR_OUT_OF_RANGE } from "../net/errors.js"; +// Cache size stays below the Web Crypto per-call limit. +function randomFillSync(buffer: Uint8Array) { + return globalThis.crypto.getRandomValues(buffer as Uint8Array); +} +const RAND_MAX = 0xffff_ffff_ffff; +// Cache random data to use in randomInt. The cache size must be evenly +// divisible by 6 because each attempt to obtain a random int uses 6 bytes. +const randomCache = Buffer.alloc(6 * 1024); +let randomCacheOffset = 0; +let initialized = false; + +function getRandomInt(min: number, max: number): number { + if (!initialized) { + randomFillSync(randomCache); + initialized = true; + } + // First we generate a random int between [0..range) + const range = max - min; + + if (!(range <= RAND_MAX)) { + throw new ERR_OUT_OF_RANGE( + `max${max ? "" : " - min"}`, + `<= ${RAND_MAX}`, + range, + ); + } + + // For (x % range) to produce an unbiased value greater than or equal to 0 and + // less than range, x must be drawn randomly from the set of integers greater + // than or equal to 0 and less than randLimit. + const randLimit = RAND_MAX - (RAND_MAX % range); + + // If we don't have a callback, or if there is still data in the cache, we can + // do this synchronously, which is super fast. + while (randomCacheOffset <= randomCache.length) { + if (randomCacheOffset === randomCache.length) { + // This might block the thread for a bit, but we are in sync mode. + randomFillSync(randomCache); + randomCacheOffset = 0; + } + + const x = randomCache.readUIntBE(randomCacheOffset, 6); + randomCacheOffset += 6; + if (x < randLimit) { + return (x % range) + min; + } + } + return 0; // Should be unreachable. +} + +export type RandomIntCallback = (err: Error | null, n?: number) => void; +export function randomInt(max: number): number; +export function randomInt(min: number, max: number): number; +export function randomInt(max: number, callback: RandomIntCallback): void; +export function randomInt( + min: number, + max: number, + callback: RandomIntCallback, +): void; +export function randomInt( + minOrMax: number, + maxOrCallback?: number | RandomIntCallback, + callback?: RandomIntCallback, +): number | undefined { + let min = 0; + let max = 0; + if (typeof callback === "function") { + validateInteger(minOrMax, "min"); + validateInteger(maxOrCallback, "max"); + min = minOrMax; + max = maxOrCallback; + } else if (typeof maxOrCallback === "function") { + min = 0; + validateInteger(minOrMax, "max"); + max = minOrMax; + callback = maxOrCallback; + } else if (arguments.length === 2) { + validateInteger(minOrMax, "min"); + validateInteger(maxOrCallback, "max"); + min = minOrMax; + max = maxOrCallback; + } else { + min = 0; + validateInteger(minOrMax, "max"); + max = minOrMax; + } + + if (min >= max) { + throw new ERR_OUT_OF_RANGE("min", "min < max", min); + } + + if (callback != null) { + new Promise((res) => { + res(getRandomInt(min, max)); + }).then( + (n: number): void => { + callback(null, n); + }, + (err: unknown): void => { + callback(err as Error); + }, + ); + } else { + return getRandomInt(min, max); + } + + return undefined; +} + +export function randomUUID(options?: RandomUUIDOptions): string { + // While we do not actually use the entropy cache, we go ahead and validate + // the input parameters as Node.js does. + if (options !== undefined) { + validateObject(options, "options"); + if (options.disableEntropyCache !== undefined) { + validateBoolean( + options.disableEntropyCache, + "options.disableEntropyCache", + ); + } + } + return crypto.randomUUID(); +} diff --git a/src/compat/crypto/rsa.ts b/src/compat/crypto/rsa.ts new file mode 100644 index 00000000..4c244037 --- /dev/null +++ b/src/compat/crypto/rsa.ts @@ -0,0 +1,235 @@ +// @ts-nocheck +// Pinned workerd crypto_cipher.ts; LICENSE-APACHE and LICENSE-3RD-PARTY.md. +import { Buffer } from "node:buffer"; +import { validateNumber, validateString } from "../net/validators.js"; +import { ERR_INVALID_ARG_TYPE, ERR_MISSING_ARGS } from "../net/errors.js"; +import { ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE } from "./sign-errors.js"; +import { isAnyArrayBuffer, isArrayBufferView } from "../util/types/index.js"; +import { isKeyObject, getKeyObjectHandle } from "./keys.js"; +import { createPrivateKey, createPublicKey } from "./asymmetric.js"; +import { keyStates } from "./key-state.js"; +const kRsaPkcs1Padding = 1; +function operate(operation, key, data, options) { + const handle = keyStates.get(key)?.asymmetric; + if (!handle) + throw Object.assign( + new Error( + "Synchronous access to opaque host key material is unavailable", + ), + { code: "ERR_METHOD_NOT_IMPLEMENTED" }, + ); + if (data instanceof ArrayBuffer) data = new Uint8Array(data); + return handle.rsa(operation, data, options); +} +const cryptoImpl = { + publicEncrypt: (...args) => operate(0, ...args), + privateDecrypt: (...args) => operate(1, ...args), + privateEncrypt: (...args) => operate(2, ...args), + publicDecrypt: (...args) => operate(3, ...args), +}; +function getPaddingAndHash(options: HashOptions): PublicPrivateCipherOptions { + const { + padding = kRsaPkcs1Padding, + oaepHash = "sha256", + oaepLabel, + encoding = "utf8", + } = options; + + validateNumber(padding, "options.padding"); + validateString(oaepHash, "options.oaepHash"); + + let label: ArrayBufferView | ArrayBuffer | undefined; + if (oaepLabel !== undefined) { + if (typeof oaepLabel === "string") { + label = Buffer.from(oaepLabel, encoding); + } else if (isAnyArrayBuffer(oaepLabel) || isArrayBufferView(oaepLabel)) { + label = oaepLabel; + } else { + throw new ERR_INVALID_ARG_TYPE( + "options.oaepLabel", + ["string", "Buffer", "TypedArray", "DataView", "ArrayBuffer"], + oaepLabel, + ); + } + } + + return { padding, oaepHash, oaepLabel: label }; +} + +export function privateEncrypt( + privateKey: CreateAsymmetricKeyOptions | KeyObject | CryptoKey | undefined, + buffer: string | ArrayBufferView | ArrayBuffer | undefined, +): Buffer { + if (privateKey === undefined) { + throw new ERR_MISSING_ARGS("privateKey"); + } + if (buffer === undefined) { + throw new ERR_MISSING_ARGS("buffer"); + } + const key = ((): CryptoKey => { + if (privateKey instanceof CryptoKey) { + if (privateKey.type !== "private") { + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE( + privateKey.type, + "private", + ); + } + return privateKey; + } else if (isKeyObject(privateKey)) { + if (privateKey.type !== "private") { + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE( + privateKey.type, + "private", + ); + } + return getKeyObjectHandle(privateKey); + } else { + const pvtKey = createPrivateKey(privateKey); + return getKeyObjectHandle(pvtKey); + } + })(); + if (typeof buffer === "string") { + const { encoding = "utf8" } = privateKey as { encoding?: string }; + buffer = Buffer.from(buffer, encoding); + } + + return Buffer.from( + cryptoImpl.privateEncrypt( + key, + buffer, + getPaddingAndHash(privateKey as HashOptions), + ), + ); +} + +export function privateDecrypt( + privateKey: CreateAsymmetricKeyOptions | KeyData, + buffer: string | ArrayBufferView | ArrayBuffer, +): Buffer { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (privateKey === undefined) { + throw new ERR_MISSING_ARGS("privateKey"); + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (buffer === undefined) { + throw new ERR_MISSING_ARGS("buffer"); + } + const key = ((): CryptoKey => { + if (privateKey instanceof CryptoKey) { + if (privateKey.type !== "private") { + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE( + privateKey.type, + "private", + ); + } + return privateKey; + } else if (isKeyObject(privateKey)) { + if (privateKey.type !== "private") { + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE( + privateKey.type, + "private", + ); + } + return getKeyObjectHandle(privateKey); + } else { + const pvtKey = createPrivateKey(privateKey as CreateAsymmetricKeyOptions); + return getKeyObjectHandle(pvtKey); + } + })(); + if (typeof buffer === "string") { + const { encoding = "utf8" } = privateKey as { encoding?: string }; + buffer = Buffer.from(buffer, encoding); + } + + return Buffer.from( + cryptoImpl.privateDecrypt( + key, + buffer, + getPaddingAndHash(privateKey as HashOptions), + ), + ); +} + +export function publicEncrypt( + key: CreateAsymmetricKeyOptions | KeyData, + buffer: string | ArrayBufferView | ArrayBuffer, +): Buffer { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (key === undefined) { + throw new ERR_MISSING_ARGS("key"); + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (buffer === undefined) { + throw new ERR_MISSING_ARGS("buffer"); + } + const pkey = ((): CryptoKey => { + if (key instanceof CryptoKey) { + if (key.type !== "public") { + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, "public"); + } + return key; + } else if (isKeyObject(key)) { + if (key.type !== "public") { + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, "public"); + } + return getKeyObjectHandle(key); + } else { + const pubKey = createPublicKey(key as CreateAsymmetricKeyOptions); + return getKeyObjectHandle(pubKey); + } + })(); + if (typeof buffer === "string") { + const { encoding = "utf8" } = key as { encoding?: string }; + buffer = Buffer.from(buffer, encoding); + } + + return Buffer.from( + cryptoImpl.publicEncrypt( + pkey, + buffer, + getPaddingAndHash(key as HashOptions), + ), + ); +} + +export function publicDecrypt( + key: CreateAsymmetricKeyOptions | KeyData, + buffer: string | ArrayBufferView | ArrayBuffer, +): Buffer { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (key === undefined) { + throw new ERR_MISSING_ARGS("key"); + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (buffer === undefined) { + throw new ERR_MISSING_ARGS("buffer"); + } + const pkey = ((): CryptoKey => { + if (key instanceof CryptoKey) { + if (key.type !== "public") { + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, "public"); + } + return key; + } else if (isKeyObject(key)) { + if (key.type !== "public") { + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, "public"); + } + return getKeyObjectHandle(key); + } else { + const pubKey = createPublicKey(key as CreateAsymmetricKeyOptions); + return getKeyObjectHandle(pubKey); + } + })(); + if (typeof buffer === "string") { + const { encoding = "utf8" } = key as { encoding?: string }; + buffer = Buffer.from(buffer, encoding); + } + + return Buffer.from( + cryptoImpl.publicDecrypt( + pkey, + buffer, + getPaddingAndHash(key as HashOptions), + ), + ); +} diff --git a/src/compat/crypto/scrypt.ts b/src/compat/crypto/scrypt.ts new file mode 100644 index 00000000..3a73fce2 --- /dev/null +++ b/src/compat/crypto/scrypt.ts @@ -0,0 +1,227 @@ +// Copyright (c) 2017-2023 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. + +// Validation/callback behavior from pinned workerd; algorithm from scrypt-js. +import { syncScrypt } from "scrypt-js"; +import { Buffer } from "node:buffer"; +import { + validateInteger, + bytes as getArrayBufferOrView, +} from "./validation.js"; +import { validateFunction, validateUint32 } from "../net/validators.js"; +import { ERR_INVALID_ARG_VALUE } from "../net/errors.js"; +type ArrayLike = any; +const kMaxLength = 2147483647; +const cryptoImpl = { + getScrypt( + password: Uint8Array, + salt: Uint8Array, + N: number, + r: number, + p: number, + maxmem: number, + length: number, + ) { + if ( + N < 2 || + (N & (N - 1)) !== 0 || + r < 1 || + p < 1 || + 128 * N * r + 128 * r * p + 256 * r > maxmem + ) { + throw new Error("Invalid scrypt params"); + } + return new Uint8Array(syncScrypt(password, salt, N, r, p, length)).buffer; + }, +}; +export interface ValidatedScryptOptions { + password: ArrayLike; + salt: ArrayLike; + keylen: number; + N: number; + r: number; + p: number; + maxmem: number; +} + +export interface ScryptOptions { + N?: number; + r?: number; + p?: number; + maxmem?: number; + cost?: number; + blockSize?: number; + parallelization?: number; +} + +const defaults: ScryptOptions = { + N: 16384, + r: 8, + p: 1, + maxmem: 32 << 20, +}; + +function validateParameters( + password: ArrayLike, + salt: ArrayLike, + keylen: number, + options?: ScryptOptions, +): ValidatedScryptOptions { + // TODO(soon): Add support for KeyObject input. + password = getArrayBufferOrView(password, "password"); + salt = getArrayBufferOrView(salt, "salt"); + + if (password.byteLength >= kMaxLength) { + throw new ERR_INVALID_ARG_VALUE("password", password, "is too big"); + } + if (salt.byteLength >= kMaxLength) { + throw new ERR_INVALID_ARG_VALUE("salt", salt, "is too big"); + } + + validateInteger(keylen, "keylen", 0, kMaxLength); + + let { N, r, p, maxmem } = defaults; + if (options && options !== defaults) { + const has_N = options.N !== undefined; + if (has_N) { + N = options.N; + validateUint32(N, "N"); + } + if (options.cost !== undefined) { + if (has_N) throw new ERR_INVALID_ARG_VALUE("cost", options.cost); + N = options.cost; + validateUint32(N, "cost"); + } + const has_r = options.r !== undefined; + if (has_r) { + r = options.r; + validateUint32(r, "r"); + } + if (options.blockSize !== undefined) { + if (has_r) + throw new ERR_INVALID_ARG_VALUE("blockSize", options.blockSize); + r = options.blockSize; + validateUint32(r, "blockSize"); + } + const has_p = options.p !== undefined; + if (has_p) { + p = options.p; + validateUint32(p, "p"); + } + if (options.parallelization !== undefined) { + if (has_p) + throw new ERR_INVALID_ARG_VALUE( + "parallelization", + options.parallelization, + ); + p = options.parallelization; + validateUint32(p, "parallelization"); + } + if (options.maxmem !== undefined) { + maxmem = options.maxmem; + validateInteger(maxmem, "maxmem", 0); + } + if (N === 0) N = defaults.N; + if (r === 0) r = defaults.r; + if (p === 0) p = defaults.p; + if (maxmem === 0) maxmem = defaults.maxmem; + } + + return { + password, + salt, + keylen, + N: N as number, + r: r as number, + p: p as number, + maxmem: maxmem as number, + }; +} + +type Callback = (err: Error | null, derivedKey?: Buffer) => void; +type OptionsOrCallback = ScryptOptions | Callback; + +export function scrypt( + password: ArrayLike, + salt: ArrayLike, + keylen: number, + options: OptionsOrCallback, + callback: OptionsOrCallback = defaults, +): void { + if (callback === defaults) { + callback = options; + options = defaults; + } + + let N: number; + let r: number; + let p: number; + let maxmem: number; + ({ password, salt, keylen, N, r, p, maxmem } = validateParameters( + password, + salt, + keylen, + options as ScryptOptions, + )); + + validateFunction(callback, "callback"); + + new Promise((res, rej) => { + try { + res(cryptoImpl.getScrypt(password, salt, N, r, p, maxmem, keylen)); + } catch (err) { + rej(err as Error); + } + }).then( + (val: ArrayBuffer) => { + (callback as Callback)(null, Buffer.from(val)); + }, + (err: unknown) => { + (callback as Callback)(err as Error); + }, + ); +} + +export function scryptSync( + password: ArrayLike, + salt: ArrayLike, + keylen: number, + options?: ScryptOptions, +): Buffer { + let N: number; + let r: number; + let p: number; + let maxmem: number; + ({ password, salt, keylen, N, r, p, maxmem } = validateParameters( + password, + salt, + keylen, + options, + )); + + return Buffer.from( + cryptoImpl.getScrypt(password, salt, N, r, p, maxmem, keylen), + ); +} diff --git a/src/compat/crypto/sign-errors.ts b/src/compat/crypto/sign-errors.ts new file mode 100644 index 00000000..e7f7b413 --- /dev/null +++ b/src/compat/crypto/sign-errors.ts @@ -0,0 +1,17 @@ +// Adapted from workerd internal_errors.ts (Apache-2.0 / Node MIT). +import { NodeError } from "../net/error-base.js"; +export { ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE } from "../net/errors.js"; +export class ERR_CRYPTO_SIGN_KEY_REQUIRED extends NodeError { + constructor() { + super("ERR_CRYPTO_SIGN_KEY_REQUIRED", "No key provided to sign"); + } +} + +export class ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE extends NodeError { + constructor(actual: string, expected: string) { + super( + "ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE", + `Invalid key object type ${actual}, expected ${expected}.`, + ); + } +} diff --git a/src/compat/crypto/sign-handle.ts b/src/compat/crypto/sign-handle.ts new file mode 100644 index 00000000..5d1bbfe3 --- /dev/null +++ b/src/compat/crypto/sign-handle.ts @@ -0,0 +1,149 @@ +import { Buffer } from "node:buffer"; +import { NativeDigest } from "./native-crypto.js"; +import { keyStates } from "./key-state.js"; +import { KeyObject } from "./keys.js"; +import { sign, verify } from "./asymmetric.js"; +import { der, integer } from "./key-formats.js"; +function key(value: any) { + if (value instanceof globalThis.CryptoKey) value = KeyObject.from(value); + const result = keyStates.get(value)?.asymmetric; + if (!result) + throw Object.assign( + new Error( + "Synchronous access to opaque host key material is unavailable", + ), + { code: "ERR_METHOD_NOT_IMPLEMENTED" }, + ); + return result; +} +function rawSignature(data: Uint8Array, width: number): Buffer { + let offset = 0; + const item = (tag: number) => { + if (data[offset++] !== tag) throw new Error("Invalid DER signature"); + let length = data[offset++]; + if (length & 128) { + const count = length & 127; + length = 0; + for (let i = 0; i < count; i++) length = length * 256 + data[offset++]; + } + if (offset + length > data.length) throw new Error("Invalid DER signature"); + return length; + }; + const length = item(48); + if (offset + length !== data.length) throw new Error("Invalid DER signature"); + const output = Buffer.alloc(width * 2); + for (let part = 0; part < 2; part++) { + let length = item(2); + while (length > width && data[offset] === 0) { + offset++; + length--; + } + if (length > width) throw new Error("Invalid DER signature"); + output.set( + data.subarray(offset, offset + length), + part * width + width - length, + ); + offset += length; + } + return output; +} +class SignHandle { + private hash: NativeDigest; + constructor(private algorithm: string) { + this.hash = new NativeDigest(algorithm); + } + update(data: Uint8Array) { + this.hash.update( + Buffer.from(data.buffer, data.byteOffset, data.byteLength), + ); + } + sign(value: any, padding?: number, saltLength?: number, encoding?: number) { + const native = key(value), + signed = native.signHash( + this.algorithm, + this.hash.final(), + padding, + saltLength, + ); + return encoding === 1 && native.type === "ec" + ? rawSignature(signed, Math.ceil(native.bits / 8)) + : signed; + } +} +class VerifyHandle { + private hash: NativeDigest; + constructor(private algorithm: string) { + this.hash = new NativeDigest(algorithm, "Verification"); + } + update(data: Uint8Array) { + this.hash.update( + Buffer.from(data.buffer, data.byteOffset, data.byteLength), + ); + } + verify( + value: any, + signature: Uint8Array, + padding?: number, + saltLength?: number, + encoding?: number, + ) { + const native = key(value), + hash = this.hash.final(); + if (encoding === 1 && native.type === "ec") { + const width = Math.ceil(native.bits / 8); + if (signature.length !== width * 2) return false; + signature = der( + 48, + integer(signature.subarray(0, width)), + integer(signature.subarray(width)), + ); + } + return native.verifyHash( + this.algorithm, + hash, + signature, + padding, + saltLength, + ); + } +} +export default { + SignHandle, + VerifyHandle, + signOneShot( + key: any, + algorithm: any, + data: any, + padding?: number, + saltLength?: number, + encoding?: number, + ) { + return sign(algorithm, data, { + key, + padding, + saltLength, + dsaEncoding: encoding === 1 ? "ieee-p1363" : "der", + }); + }, + verifyOneShot( + key: any, + algorithm: any, + data: any, + signature: any, + padding?: number, + saltLength?: number, + encoding?: number, + ) { + return verify( + algorithm, + data, + { + key, + padding, + saltLength, + dsaEncoding: encoding === 1 ? "ieee-p1363" : "der", + }, + signature, + ); + }, +}; diff --git a/src/compat/crypto/signatures.ts b/src/compat/crypto/signatures.ts new file mode 100644 index 00000000..631268b1 --- /dev/null +++ b/src/compat/crypto/signatures.ts @@ -0,0 +1,436 @@ +// @ts-nocheck +import { createPrivateKey, createPublicKey } from "./asymmetric.js"; +// Copyright (c) 2017-2023 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. + +/* TODO: the following is adopted code, enabling linting one day */ +/* eslint-disable */ + +import { + default as cryptoImpl, + type SignHandle, + type VerifyHandle, + type CreateAsymmetricKeyOptions, +} from "./sign-handle.js"; + +import { validateString } from "../net/validators.js"; + +import { Buffer } from "node:buffer"; + +import { KeyObject, isKeyObject, getKeyObjectHandle } from "./keys.js"; + +import { + ERR_CRYPTO_SIGN_KEY_REQUIRED, + ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, +} from "./sign-errors.js"; + +import { Writable } from "../stream/writable.js"; +import { isArrayBufferView } from "../util/types/index.js"; + +export interface SignOptions {} + +export interface Sign extends Writable { + [kHandle]: SignHandle; + update(data: any): void; + sign(privateKey: any): any; +} +export interface Verify extends Writable { + [kHandle]: VerifyHandle; + update(data: any): void; + verify(publicKey: any, signature: any): any; +} + +const kHandle = Symbol("kHandle"); + +// Uses old-style class syntax for Node.js compatibility +export const Sign = function (this: Sign, algorithm: string, options: any) { + if (!(this instanceof Sign)) return new Sign(algorithm, options); + validateString(algorithm, "algorithm"); + this[kHandle] = new cryptoImpl.SignHandle(algorithm); + Writable.call(this, options); + return this; +} as any as { + new (algorithm: string, options?: SignOptions): Sign; +}; +Object.setPrototypeOf(Sign.prototype, Writable.prototype); +Object.setPrototypeOf(Sign, Writable); + +Sign.prototype._write = function _write( + chunk: string | ArrayBufferView, + encoding: string | undefined | null, + callback: (err?: any) => void, +) { + this.update(chunk, encoding); + callback(); +}; + +Sign.prototype.update = function ( + this: Sign, + data: string | ArrayBufferView, + encoding?: string, +) { + if (typeof data === "string") { + data = Buffer.from(data, encoding); + } + if (!isArrayBufferView(data)) { + throw new ERR_INVALID_ARG_TYPE( + "data", + ["string", "Buffer", "TypedArray", "DataView"], + data, + ); + } + this[kHandle].update(data); + return this; +}; + +function getIntOption(name: string, options: any): number | undefined { + const value = options[name]; + if (value !== undefined) { + if (value === value >> 0) { + return value; + } + throw new ERR_INVALID_ARG_VALUE(`options.${name}`, value); + } + return undefined; +} + +function getDSASignatureEncoding(options: any): number { + if (typeof options === "object") { + const { dsaEncoding = "der" } = options; + if (dsaEncoding === "der") + return 0; // kSigEncDER; + else if (dsaEncoding === "ieee-p1363") return 1; // kSigEncP1363 + throw new ERR_INVALID_ARG_VALUE("options.dsaEncoding", dsaEncoding); + } + + return 0; +} + +function getPrivateKey(options: any): CryptoKey { + if (options instanceof CryptoKey) { + return options as CryptoKey; + } else if (isKeyObject(options)) { + const keyObject = options as KeyObject; + if (keyObject.type === "secret") { + throw new ERR_INVALID_ARG_TYPE( + "options", + ["PublicKeyObject", "PrivateKeyObject", "CryptoKey", "object"], + keyObject.type, + ); + } + if (keyObject.type === "public") { + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE("public", "private"); + } + return getKeyObjectHandle(keyObject); + } else { + return getKeyObjectHandle( + createPrivateKey(options as CreateAsymmetricKeyOptions), + ); + } +} + +Sign.prototype.sign = function ( + this: Sign, + options: any, + encoding?: string, +): Buffer | string { + if (!options) { + throw new ERR_CRYPTO_SIGN_KEY_REQUIRED(); + } + + const key = getPrivateKey(options); + + // Options specific to RSA + const rsaPadding = getIntOption("padding", options); + const pssSaltLength = getIntOption("saltLength", options); + + // Options specific to (EC)DSA + const dsaSigEnc = getDSASignatureEncoding(options); + + const u8 = this[kHandle].sign(key, rsaPadding, pssSaltLength, dsaSigEnc); + + const res = Buffer.from(u8.buffer, u8.byteOffset, u8.byteLength); + + if (encoding && encoding !== "buffer") { + return res.toString(encoding); + } + + return res; +}; + +// Uses old-style class syntax for Node.js compatibility +export const Verify = function (this: Verify, algorithm: string, options: any) { + if (!(this instanceof Verify)) return new Verify(algorithm, options); + validateString(algorithm, "algorithm"); + this[kHandle] = new cryptoImpl.VerifyHandle(algorithm); + Writable.call(this, options); + return this; +} as any as { + new (algorithm: string, options?: SignOptions): Verify; +}; +Object.setPrototypeOf(Verify.prototype, Writable.prototype); +Object.setPrototypeOf(Verify, Writable); + +Verify.prototype._write = function _write( + chunk: string | ArrayBufferView, + encoding: string | undefined | null, + callback: (err?: unknown) => void, +) { + this.update(chunk, encoding); + callback(); +}; + +Verify.prototype.update = function ( + this: Verify, + data: string | ArrayBufferView, + encoding?: string, +) { + if (typeof data === "string") { + data = Buffer.from(data, encoding); + } + if (!isArrayBufferView(data)) { + throw new ERR_INVALID_ARG_TYPE( + "data", + ["string", "Buffer", "TypedArray", "DataView"], + data, + ); + } + this[kHandle].update(data); + return this; +}; + +Verify.prototype.verify = function ( + this: Verify, + options: any, + signature: ArrayBufferView | string, + encoding: string = "utf8", +) { + if (!options) { + throw new ERR_CRYPTO_SIGN_KEY_REQUIRED(); + } + + let key: CryptoKey; + if (options instanceof CryptoKey) { + key = options as CryptoKey; + } else if (isKeyObject(options)) { + key = getKeyObjectHandle(options as KeyObject); + } else { + key = getKeyObjectHandle( + createPublicKey(options as CreateAsymmetricKeyOptions), + ); + } + if (!(key instanceof CryptoKey) && !isKeyObject(key)) { + throw new ERR_INVALID_ARG_TYPE( + "options", + ["KeyObject", "CryptoKey", "object"], + key, + ); + } + + // Options specific to RSA + const rsaPadding = getIntOption("padding", options); + const pssSaltLength = getIntOption("saltLength", options); + + // Options specific to (EC)DSA + const dsaSigEnc = getDSASignatureEncoding(options); + + if (typeof signature === "string") { + signature = Buffer.from(signature, encoding); + } + + return this[kHandle].verify( + key, + signature, + rsaPadding, + pssSaltLength, + dsaSigEnc, + ); +}; + +export function createSign(algorithm: string, options: any) { + return new Sign(algorithm, options); +} + +export function createVerify(algorithm: string, options: any) { + return new Verify(algorithm, options); +} + +type SignCallback = (err: any, signature?: Buffer) => void; +type VerifyCallback = (err: any, valid?: boolean) => void; + +export function sign( + algorithm: string | null | undefined, + data: BufferSource, + options: any, + callback?: SignCallback, +): Buffer | void { + if (algorithm != null) { + validateString(algorithm, "algorithm"); + } else { + algorithm = undefined; + } + if (!isArrayBufferView(data)) { + throw new ERR_INVALID_ARG_TYPE( + "data", + ["Buffer", "TypedArray", "DataView"], + data, + ); + } + + if (!options) { + throw new ERR_CRYPTO_SIGN_KEY_REQUIRED(); + } + + const key = getPrivateKey(options); + + // Options specific to RSA + const rsaPadding = getIntOption("padding", options); + const pssSaltLength = getIntOption("saltLength", options); + + // Options specific to (EC)DSA + const dsaSigEnc = getDSASignatureEncoding(options); + + if (callback === undefined) { + return Buffer.from( + cryptoImpl.signOneShot( + key, + algorithm, + data, + rsaPadding, + pssSaltLength, + dsaSigEnc, + ), + ); + } + + try { + const signature = Buffer.from( + cryptoImpl.signOneShot( + key, + algorithm, + data, + rsaPadding, + pssSaltLength, + dsaSigEnc, + ), + ); + queueMicrotask(() => callback(null, signature)); + } catch (err) { + queueMicrotask(() => callback(err)); + } +} + +export function verify( + algorithm: string | null | undefined, + data: BufferSource, + options: any, + signature: BufferSource, + callback?: VerifyCallback, +): boolean | undefined { + // Node.js allows the algorithm to be either undefined or null, in which + // case we just normalize it to undefined. + if (algorithm != null) { + validateString(algorithm, "algorithm"); + } else { + algorithm = undefined; + } + + if (!isArrayBufferView(data)) { + throw new ERR_INVALID_ARG_TYPE( + "data", + ["Buffer", "TypedArray", "DataView"], + data, + ); + } + + if (!isArrayBufferView(signature)) { + throw new ERR_INVALID_ARG_TYPE( + "signature", + ["Buffer", "TypedArray", "DataView"], + signature, + ); + } + + if (!options) { + throw new ERR_CRYPTO_SIGN_KEY_REQUIRED(); + } + + let key: CryptoKey; + if (options instanceof CryptoKey) { + key = options; + } else if (isKeyObject(options)) { + key = getKeyObjectHandle(options as KeyObject); + } else { + key = getKeyObjectHandle( + createPublicKey(options as CreateAsymmetricKeyOptions), + ); + } + if (!(key instanceof CryptoKey) && !isKeyObject(key)) { + throw new ERR_INVALID_ARG_TYPE( + "options", + ["KeyObject", "CryptoKey", "object"], + key, + ); + } + + // Options specific to RSA + const rsaPadding = getIntOption("padding", options); + const pssSaltLength = getIntOption("saltLength", options); + + // Options specific to (EC)DSA + const dsaSigEnc = getDSASignatureEncoding(options); + + if (callback === undefined) { + return cryptoImpl.verifyOneShot( + key, + algorithm, + data, + signature, + rsaPadding, + pssSaltLength, + dsaSigEnc, + ); + } + + try { + callback( + null, + cryptoImpl.verifyOneShot( + key, + algorithm, + data, + signature, + rsaPadding, + pssSaltLength, + dsaSigEnc, + ), + ); + } catch (err) { + queueMicrotask(() => callback(err)); + } + return; // explicit return is necessary to squelch typescript warning. +} diff --git a/src/compat/crypto/symmetric.ts b/src/compat/crypto/symmetric.ts new file mode 100644 index 00000000..8cb36a21 --- /dev/null +++ b/src/compat/crypto/symmetric.ts @@ -0,0 +1,131 @@ +import { Buffer } from "node:buffer"; +import { Transform } from "../stream/transform.js"; +// @ts-expect-error dependency has no declarations +import original from "crypto-browserify/index.js"; +import { secretMaterial } from "./keys.js"; +const BaseTransform = + Transform as unknown as typeof import("node:stream").Transform; +const handles = new WeakMap(); +export class Hmac extends BaseTransform { + constructor(algorithm: string, key: unknown, options?: any) { + super(options); + handles.set( + this, + original.createHmac( + algorithm, + typeof key === "string" + ? Buffer.from(key, options?.encoding) + : secretMaterial(key), + ), + ); + } + update(data: unknown, encoding?: string): this { + handles.get(this).update(data, encoding); + return this; + } + digest(encoding?: string): any { + return handles.get(this).digest(encoding); + } + override _transform(chunk: any, encoding: any, callback: any) { + try { + this.update(chunk, encoding); + callback(); + } catch (error) { + callback(error); + } + } + override _flush(callback: any) { + try { + this.push(this.digest()); + callback(); + } catch (error) { + callback(error); + } + } +} +class CipherTransform extends BaseTransform { + constructor( + decrypt: boolean, + algorithm: string, + key: unknown, + iv: unknown, + options?: any, + ) { + super(options); + handles.set( + this, + (decrypt ? original.createDecipheriv : original.createCipheriv)( + algorithm, + secretMaterial(key), + iv, + options, + ), + ); + } + update(data: unknown, inputEncoding?: string, outputEncoding?: string): any { + return handles.get(this).update(data, inputEncoding, outputEncoding); + } + final(encoding?: string): any { + return handles.get(this).final(encoding); + } + setAutoPadding(value?: boolean): this { + handles.get(this).setAutoPadding(value); + return this; + } + setAAD(data: unknown, options?: any): this { + handles.get(this).setAAD(data, options); + return this; + } + getAuthTag(): any { + return handles.get(this).getAuthTag(); + } + setAuthTag(data: unknown): this { + handles.get(this).setAuthTag(data); + return this; + } + override _transform(chunk: any, encoding: any, callback: any) { + try { + this.push(this.update(chunk, encoding)); + callback(); + } catch (error) { + callback(error); + } + } + override _flush(callback: any) { + try { + this.push(this.final()); + callback(); + } catch (error) { + callback(error); + } + } +} +export class Cipheriv extends CipherTransform { + constructor(algorithm: string, key: unknown, iv: unknown, options?: any) { + super(false, algorithm, key, iv, options); + } +} +export class Decipheriv extends CipherTransform { + constructor(algorithm: string, key: unknown, iv: unknown, options?: any) { + super(true, algorithm, key, iv, options); + } +} +export function createHmac(algorithm: string, key: unknown, options?: any) { + return new Hmac(algorithm, key, options); +} +export function createCipheriv( + algorithm: string, + key: unknown, + iv: unknown, + options?: any, +) { + return new Cipheriv(algorithm, key, iv, options); +} +export function createDecipheriv( + algorithm: string, + key: unknown, + iv: unknown, + options?: any, +) { + return new Decipheriv(algorithm, key, iv, options); +} diff --git a/src/compat/crypto/validation.ts b/src/compat/crypto/validation.ts new file mode 100644 index 00000000..47c6c31d --- /dev/null +++ b/src/compat/crypto/validation.ts @@ -0,0 +1,26 @@ +import { Buffer } from "node:buffer"; +import { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE } from "../net/errors.js"; +export function validateInteger( + value: unknown, + name: string, + min = Number.MIN_SAFE_INTEGER, + max = Number.MAX_SAFE_INTEGER, +): asserts value is number { + if (typeof value !== "number") + throw new ERR_INVALID_ARG_TYPE(name, "number", value); + if (!Number.isInteger(value)) + throw new ERR_OUT_OF_RANGE(name, "an integer", value); + if (value < min || value > max) + throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value); +} +export function bytes(value: unknown, name: string): Buffer { + if (typeof value === "string") return Buffer.from(value); + if (value instanceof ArrayBuffer) return Buffer.from(value); + if (ArrayBuffer.isView(value)) + return Buffer.from(value.buffer, value.byteOffset, value.byteLength); + throw new ERR_INVALID_ARG_TYPE( + name, + ["string", "ArrayBuffer", "Buffer", "TypedArray", "DataView"], + value, + ); +} diff --git a/src/compat/crypto/x509.ts b/src/compat/crypto/x509.ts new file mode 100644 index 00000000..c2775d74 --- /dev/null +++ b/src/compat/crypto/x509.ts @@ -0,0 +1,463 @@ +// 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. + +/* TODO: the following is adopted code, enabling linting one day */ +/* eslint-disable */ + +import { NativeX509 } from "./native-crypto.js"; +import { + validateString, + validateObject, + validateBoolean, +} from "../net/validators.js"; +import { Buffer } from "buffer"; +import { ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE } from "../net/errors.js"; +import { PublicKeyObject, PrivateKeyObject, create } from "./keys.js"; +import { keyStates } from "./key-state.js"; +const isArrayBufferView = ArrayBuffer.isView; +const certificateKeys = new WeakSet(); +function flags(options: any) { + // BoringSSL always forbids partial-label wildcards. The ALWAYS_CHECK_SUBJECT, + // MULTI_LABEL_WILDCARDS and SINGLE_LABEL_SUBDOMAINS flags are zero there. + // https://boringssl.googlesource.com/boringssl/+/refs/heads/fips-20240407/include/openssl/x509.h + let flags = 4; + if (options == null) return flags; + if (options.wildcards === false) flags |= 2; + if (options.subject === "never") flags |= 32; + else if ( + options.subject !== undefined && + options.subject !== "default" && + options.subject !== "always" + ) + throw new Error("Invalid subject option"); + return flags; +} + +function translatePeerCertificate(c: any) { + if (!c) return null; + + if (c.issuerCertificate != null && c.issuerCertificate !== c) { + c.issuerCertificate = translatePeerCertificate(c.issuerCertificate); + } + if (c.infoAccess != null) { + const info = c.infoAccess; + c.infoAccess = {}; + + // XXX: More key validation? + const regex = /([^\n:]*):([^\n]*)(?:\n|$)/g; + regex[Symbol.replace](info, (_: any, key: any, val: any): any => { + if (val.charCodeAt(0) === 0x22) { + // The translatePeerCertificate function is only + // used on internally created legacy certificate + // objects, and any value that contains a quote + // will always be a valid JSON string literal, + // so this should never throw. + val = JSON.parse(val); + } + if (key in c.infoAccess) c.infoAccess[key].push(val); + else c.infoAccess[key] = [val]; + }); + } + return c; +} + +function checkOptions(options?: any) { + if (options == null) return; + validateObject(options, "options"); + if (options.multiLabelWildcards !== undefined) + validateBoolean(options.multiLabelWildcards, "options.multiLabelWildcards"); + if (options.partialWildcards !== undefined) + validateBoolean(options.partialWildcards, "options.partialWildcards"); + if (options.singleLabelSubdomains !== undefined) + validateBoolean( + options.singleLabelSubdomains, + "options.singleLabelSubdomains", + ); + if (options.wildcards !== undefined) + validateBoolean(options.wildcards, "options.wildcards"); + if (options.subject !== undefined) + validateString(options.subject, "options.subject"); +} + +export class X509Certificate { + #handle: NativeX509; + #state = new Map(); + + constructor(buffer: ArrayBufferView | ArrayBuffer | NativeX509 | string) { + if (buffer instanceof NativeX509) { + this.#handle = buffer; + return; + } + if (typeof buffer === "string") { + buffer = Buffer.from(buffer); + } + if (!isArrayBufferView(buffer)) { + throw new ERR_INVALID_ARG_TYPE( + "buffer", + ["string", "Buffer", "TypedArray", "DataView"], + buffer, + ); + } + const handle = NativeX509.parse( + new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength), + ); + if (handle == null) { + throw new ERR_INVALID_ARG_VALUE( + "buffer", + buffer, + "is not a valid certificate", + ); + } + this.#handle = handle; + } + + get subject() { + let value = this.#state.get("subject"); + if (value === undefined) { + value = this.#handle.string(0); + this.#state.set("subject", value); + } + return value ?? undefined; + } + + get subjectAltName() { + let value = this.#state.get("subjectAltName"); + if (value === undefined) { + value = this.#handle.string(2); + this.#state.set("subjectAltName", value); + } + return value ?? undefined; + } + + get issuer() { + let value = this.#state.get("issuer"); + if (value === undefined) { + value = this.#handle.string(1); + this.#state.set("issuer", value); + } + return value ?? undefined; + } + + get issuerCertificate() { + let value = this.#state.get("issuerCertificate"); + if (value === undefined) { + this.#state.set("issuerCertificate", value); + } + return value ?? undefined; + } + + get infoAccess() { + let value = this.#state.get("infoAccess"); + if (value === undefined) { + value = this.#handle.string(3); + this.#state.set("infoAccess", value); + } + return value ?? undefined; + } + + get validFrom() { + let value = this.#state.get("validFrom"); + if (value === undefined) { + value = this.#handle.string(4); + this.#state.set("validFrom", value); + } + return value ?? undefined; + } + + get validTo() { + let value = this.#state.get("validTo"); + if (value === undefined) { + value = this.#handle.string(5); + this.#state.set("validTo", value); + } + return value ?? undefined; + } + + get fingerprint() { + let value = this.#state.get("fingerprint"); + if (value === undefined) { + value = this.#handle.string(6); + this.#state.set("fingerprint", value); + } + return value ?? undefined; + } + + get fingerprint256() { + let value = this.#state.get("fingerprint256"); + if (value === undefined) { + value = this.#handle.string(7); + this.#state.set("fingerprint256", value); + } + return value ?? undefined; + } + + get fingerprint512() { + let value = this.#state.get("fingerprint512"); + if (value === undefined) { + value = this.#handle.string(8); + this.#state.set("fingerprint512", value); + } + return value ?? undefined; + } + + get keyUsage() { + let value = this.#state.get("keyUsage"); + if (value === undefined) { + value = this.#handle.string(12)?.split("\n"); + this.#state.set("keyUsage", value); + } + return value ?? undefined; + } + + get serialNumber() { + let value = this.#state.get("serialNumber"); + if (value === undefined) { + value = this.#handle.string(9); + if (value != null) value = value.toUpperCase(); + this.#state.set("serialNumber", value); + } + return value ?? undefined; + } + + get raw() { + let value = this.#state.get("raw"); + if (value === undefined) { + value = this.#handle.field(10); + if (value != null) value = Buffer.from(value); + this.#state.set("raw", value); + } + return value ?? undefined; + } + + get publicKey() { + let value = this.#state.get("publicKey"); + if (value === undefined) { + const inner = this.#handle!.publicKey; + if (inner !== undefined) { + value = create("public", { asymmetric: inner }); + certificateKeys.add(value); + this.#state.set("publicKey", value); + } + } + return value ?? undefined; + } + + toString() { + let value = this.#state.get("pem"); + if (value === undefined) { + value = this.#handle.string(11); + this.#state.set("pem", value); + } + return value ?? undefined; + } + + // There's no standardized JSON encoding for X509 certs so we + // fallback to providing the PEM encoding as a string. + toJSON() { + return this.toString(); + } + + get ca() { + let value = this.#state.get("ca"); + if (value === undefined) { + value = this.#handle!.isCA; + this.#state.set("ca", value); + } + return value ?? false; + } + + checkHost(name: string, options?: any) { + validateString(name, "name"); + checkOptions(options); + return this.#handle.check(name, flags(options), 0) ?? undefined; + } + + checkEmail(email: string, options?: any) { + validateString(email, "email"); + checkOptions(options); + return this.#handle.check(email, flags(options), 1) ?? undefined; + } + + checkIP(ip: string, options?: any) { + validateString(ip, "ip"); + checkOptions(options); + // The options argument is currently undocumented since none of the options + // have any effect on the behavior of this function. However, we still parse + // the options argument in case OpenSSL adds flags in the future that do + // affect the behavior of X509_check_ip. This ensures that no invalid values + // are passed as the second argument in the meantime. + return this.#handle.check(ip, flags(options), 2) ?? undefined; + } + + checkIssued(otherCert: X509Certificate) { + if (!(otherCert instanceof X509Certificate)) + throw new ERR_INVALID_ARG_TYPE("otherCert", "X509Certificate", otherCert); + return this.#handle!.checkIssued(otherCert.#handle!) ?? undefined; + } + + checkPrivateKey(pkey: PrivateKeyObject) { + if (!(pkey instanceof PrivateKeyObject)) + throw new ERR_INVALID_ARG_TYPE("pkey", "KeyObject", pkey); + if (pkey.type !== "private") throw new ERR_INVALID_ARG_VALUE("pkey", pkey); + // Pinned workerd returns false for imported/generated private KeyObjects, + // even when their public material matches (RSA and EC comparison fixtures). + return false; + } + + verify(pkey: PublicKeyObject) { + if (!(pkey instanceof PublicKeyObject)) + throw new ERR_INVALID_ARG_TYPE("pkey", "KeyObject", pkey); + if (pkey.type !== "public") throw new ERR_INVALID_ARG_VALUE("pkey", pkey); + // Keys exposed by X509 retain the certificate backend in the pinned runtime. + return ( + certificateKeys.has(pkey) && + this.#handle.checkKey(keyStates.get(pkey)!.asymmetric!, 2) + ); + } + + #legacy() { + const name = (field: number) => { + const data = this.#handle.field(field) ?? new Uint8Array(); + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + const decoder = new TextDecoder(); + let offset = 0; + const next = () => { + const size = view.getUint32(offset); + offset += 4; + const value = decoder.decode(data.subarray(offset, offset + size)); + offset += size; + return value; + }; + const out: Record = {}; + while (offset < data.byteLength) { + const key = next(), + value = next(); + if (Object.hasOwn(out, key)) { + const old = out[key]!; + out[key] = Array.isArray(old) ? [...old, value] : [old, value]; + } else + Object.defineProperty(out, key, { + value, + writable: true, + enumerable: true, + configurable: true, + }); + } + return out; + }; + const key = keyStates.get(this.publicKey)?.asymmetric; + const out: any = { + subject: name(13), + issuer: name(14), + subjectAltName: this.subjectAltName ?? "", + infoAccess: this.infoAccess ?? "", + ca: this.ca, + valid_from: this.validFrom, + valid_to: this.validTo, + fingerprint: this.fingerprint, + fingerprint256: this.fingerprint256, + fingerprint512: this.fingerprint512, + serialNumber: this.serialNumber, + ext_key_usage: this.keyUsage, + raw: this.raw, + }; + if (key?.type === "rsa") { + out.bits = key.bits; + out.modulus = Buffer.from(key.parameter("n")) + .toString("hex") + .replace(/^0+/, "") + .toUpperCase(); + out.exponent = + "0x" + + BigInt("0x" + Buffer.from(key.parameter("e")).toString("hex")).toString( + 16, + ); + out.pubkey = key.export(false, false, "spki"); + } else if (key?.type === "ec") { + out.bits = key.bits; + out.pubkey = this.#handle.field(17); + out.asn1Curve = this.#handle.string(15); + out.nistCurve = this.#handle.string(16); + } + return out; + } + + toLegacyObject() { + let value = this.#state.get("legacy"); + if (value === undefined) { + let { + subject, + subjectAltName, + infoAccess, + issuer, + ca, + modulus, + bits, + exponent, + pubkey, + asn1Curve, + nistCurve, + valid_from, + valid_to, + fingerprint, + fingerprint256, + fingerprint512, + serialNumber, + ext_key_usage, + raw, + } = this.#legacy() as any; + if (raw != null) raw = Buffer.from(raw); + if (pubkey != null) pubkey = Buffer.from(pubkey); + if (modulus != null) modulus = modulus.toUpperCase(); + if (fingerprint != null) fingerprint = fingerprint.toUpperCase(); + if (fingerprint256 != null) fingerprint256 = fingerprint256.toUpperCase(); + if (fingerprint512 != null) fingerprint512 = fingerprint512.toUpperCase(); + if (serialNumber != null) serialNumber = serialNumber.toUpperCase(); + value = translatePeerCertificate({ + subject, + subjectAltName, + infoAccess, + issuer, + ca, + modulus, + bits, + exponent, + pubkey, + asn1Curve, + nistCurve, + valid_from, + valid_to, + fingerprint, + fingerprint256, + fingerprint512, + serialNumber, + ext_key_usage, + raw, + }); + this.#state.set("legacy", value); + } + return value; + } +} diff --git a/src/compat/events/README.md b/src/compat/events/README.md new file mode 100644 index 00000000..ab251246 --- /dev/null +++ b/src/compat/events/README.md @@ -0,0 +1,57 @@ +# Events in the WebView profile + +This adapter targets workerd `1.20261012.1`, compatibility date `2026-10-17`, +through the cf-workers-native Vite and esbuild plugins. It implements the pinned +`events` exports for page and Dedicated Worker execution on Capacitor WebViews. +The WebView profile is an explicitly scoped target; it is not a claim of complete +Workers engine equivalence or complete `process`/`async_hooks` compatibility. + +The adapter includes EventEmitter listener ordering, mutation and removal, +listener limits, error monitoring, rejection capture, `once`, async iteration, +abort/disposal helpers and EventEmitterAsyncResource. It preserves Workers' +behavior where that differs from desktop Node: EventTarget listener introspection +returns an empty list, and the pinned EventEmitterAsyncResource `emit` returns +true even when no listener exists. Options unsupported by the pinned Workers +implementation do not acquire desktop Node behavior here. + +`process` event delivery, `emitWarning` and `nextTick` are shared with the adapter. +A listener-limit warning reaches `process.on("warning")` asynchronously, with the +original Error object. `nextTick` uses queueMicrotask as in the pinned workerd +implementation. Other process exports retain the existing dependency polyfill; +this change does not implement a Node operating-system process. + +## Profile boundaries + +- Application async functions must pass through the supplied build plugins. + Untransformed native async callbacks do not have Workers' engine context hooks. + AsyncResource propagation is covered for transformed concurrent listeners. +- Native error stacks, V8 call sites and inspection of hidden Proxy/Promise state + are host-dependent. WebKit does not implement V8's Error.captureStackTrace. + Warning delivery is supported; V8 stack inspection is not promised. +- The existing partial process and async-hooks profiles remain visible in the + repository compatibility inventory. Their missing exports are not hidden by + events surface alignment. + +## Reproduction + +Run `npm run build`, then: + +```sh +node --test demo/tests/experiments/node-compat/events-baseline.probe.mjs \ + demo/tests/experiments/node-compat/events-surface.probe.mjs \ + demo/tests/experiments/node-compat/events-contracts.probe.mjs +``` + +The same cases in `index.spec.mjs` run against the pinned workerd and through both +public build plugins in Chromium and WebKit, in page and Dedicated Worker modes. +`observations.spec.mjs` additionally covers abort-listener disposal and Workers +export/default-property behavior. Assertions cover actual behavior, not merely +export presence. Generated evidence belongs under `demo/artifacts/node-inventory`. + +The `events-native.mjs` runner uses an independently installed npm tarball, +a Capacitor shell and a local HTTPS report endpoint. It runs the same operation +cases in both execution modes on a simulator/emulator. It verifies every returned +case against workerd and records the compiled source hash and platform. Run it +with `ios ` or `android ` after running `npm run test:events:native:prepare --prefix demo`. +Then use `npm run test:events:native --prefix demo -- `. +The preparation step builds and installs a local tarball; it does not publish it. diff --git a/src/compat/events/errors.ts b/src/compat/events/errors.ts new file mode 100644 index 00000000..0f4cc6c3 --- /dev/null +++ b/src/compat/events/errors.ts @@ -0,0 +1,43 @@ +// 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 +// +// Adapted from Deno and Node.js: +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. +// +// Adapted from Node.js. 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. +export { + AbortError, + ERR_INVALID_ARG_TYPE, + ERR_OUT_OF_RANGE, +} from "../net/errors.js"; +import { NodeError, NodeTypeError } from "../net/error-base.js"; +export class ERR_UNHANDLED_ERROR extends NodeError { + constructor(x: string) { + super("ERR_UNHANDLED_ERROR", `Unhandled error. (${x})`); + } +} +export class ERR_INVALID_THIS extends NodeTypeError { + constructor(x: string) { + super("ERR_INVALID_THIS", `Value of "this" must be of type ${x}`); + } +} diff --git a/src/compat/events/index.cjs b/src/compat/events/index.cjs new file mode 100644 index 00000000..7c72726b --- /dev/null +++ b/src/compat/events/index.cjs @@ -0,0 +1 @@ +module.exports = require("./index.js").default; diff --git a/src/compat/events/index.spec.mjs b/src/compat/events/index.spec.mjs new file mode 100644 index 00000000..d6a52f7b --- /dev/null +++ b/src/compat/events/index.spec.mjs @@ -0,0 +1,426 @@ +// Differential operations against the pinned Workers profile. These cases do +// not certify async_hooks, warnings/process integration or native acceptance. +export const eventCases = { + warningDelivery: async ({ process }) => { + const warnings = []; + const order = []; + const original = Object.assign(new Error("original"), { marker: true }); + const listener = function (value) { + order.push("warning"); + warnings.push({ + receiver: this === process, + original: value === original, + name: value.name, + message: value.message, + code: value.code ?? null, + detail: value.detail ?? null, + }); + }; + process.on("warning", listener); + try { + process.emitWarning(original); + process.emitWarning("text", { + type: "CustomWarning", + code: "CUSTOM", + detail: "detail", + }); + order.push("sync"); + queueMicrotask(() => order.push("microtask")); + await new Promise((resolve) => setTimeout(resolve, 0)); + const errors = []; + for (const run of [ + () => process.emitWarning(1), + () => process.emitWarning("text", 1), + () => process.emitWarning("text", "Warning", 1), + ]) { + try { + run(); + } catch (error) { + errors.push({ name: error.name, code: error.code }); + } + } + return { warnings, order, errors }; + } finally { + process.removeListener("warning", listener); + } + }, + nextTickOrdering: async ({ process }) => { + const order = []; + process.nextTick((a, b) => order.push(a + b), 1, 2); + Promise.resolve().then(() => order.push("promise")); + queueMicrotask(() => order.push("microtask")); + order.push("sync"); + await new Promise((resolve) => setTimeout(resolve, 0)); + return order; + }, + warnings: async ({ events: e, process }) => { + const warnings = []; + const listener = (value) => + warnings.push({ + name: value.name, + type: String(value.type), + count: value.count, + }); + process.on("warning", listener); + const logs = []; + const oldLog = console.log; + const oldWarn = console.warn; + const capture = (...args) => + logs.push( + args.map((value) => + typeof value === "string" + ? value + : { + name: value.name, + message: value.message, + type: String(value.type), + count: value.count, + }, + ), + ); + console.log = capture; + console.warn = capture; + try { + const emitter = new e.EventEmitter(); + emitter.setMaxListeners(1); + emitter.on("item", () => {}); + emitter.on("item", () => {}); + emitter.on("item", () => {}); + await new Promise((resolve) => setTimeout(resolve, 0)); + return { logs, warnings, count: emitter.listenerCount("item") }; + } finally { + console.log = oldLog; + console.warn = oldWarn; + process.removeListener("warning", listener); + } + }, + listeners: async ({ events: e }) => { + const emitter = new e.EventEmitter(); + const calls = []; + function listener(...args) { + calls.push([this === emitter, ...args]); + } + const symbol = Symbol("item"); + const chain = [ + emitter.on(symbol, listener), + emitter.addListener(symbol, listener), + emitter.prependListener(symbol, listener), + emitter.once("once", listener), + emitter.prependOnceListener("once", listener), + ].every((x) => x === emitter); + const before = [ + emitter.listenerCount(symbol), + e.listenerCount(emitter, symbol), + emitter.listeners("once").every((x) => x === listener), + emitter.rawListeners("once").every((x) => x.listener === listener), + emitter.eventNames().map(String), + ]; + const emitted = [ + emitter.emit(symbol, 1), + emitter.emit("once", 2), + emitter.emit("once", 3), + ]; + emitter.off(symbol, listener); + emitter.removeListener(symbol, listener); + const after = emitter.listenerCount(symbol); + const removed = emitter.removeAllListeners() === emitter; + return { + chain, + before, + emitted, + calls, + after, + removed, + names: emitter.eventNames(), + }; + }, + listenerMutation: async ({ events: e }) => { + const emitter = new e.EventEmitter(); + const log = []; + const second = () => log.push("second"); + emitter.on("newListener", (name) => log.push(`add:${String(name)}`)); + emitter.on("removeListener", (name) => log.push(`remove:${String(name)}`)); + emitter.on("item", () => { + log.push("first"); + emitter.removeListener("item", second); + }); + emitter.on("item", second); + emitter.emit("item"); + emitter.emit("item"); + emitter.removeAllListeners("item"); + emitter.removeAllListeners(); + return { log, count: emitter.eventNames().length }; + }, + defaults: async ({ events: e }) => { + const original = e.default.defaultMaxListeners; + const capture = e.default.captureRejections; + try { + e.setMaxListeners(7); + const a = new e.EventEmitter(); + const b = new e.EventEmitter(); + const inherited = [a.getMaxListeners(), e.defaultMaxListeners]; + e.setMaxListeners(3, a, b); + const configured = [ + e.getMaxListeners(a), + b.getMaxListeners(), + a.setMaxListeners(0) === a, + a.getMaxListeners(), + ]; + const target = new EventTarget(); + e.setMaxListeners(2, target); + e.default.captureRejections = true; + return { + inherited, + configured, + targetMax: e.getMaxListeners(target), + targetListeners: e.getEventListeners(target, "x"), + capture: [e.captureRejections, e.default.captureRejections], + domains: e.usingDomains, + rejectionKey: Symbol.keyFor(e.captureRejectionSymbol), + }; + } finally { + e.default.defaultMaxListeners = original; + e.default.captureRejections = capture; + } + }, + invalidInputs: async ({ events: e }) => { + const emitter = new e.EventEmitter(); + const errors = []; + const operations = [ + () => emitter.on("x", 1), + () => emitter.once("x", null), + () => emitter.prependListener("x", false), + () => emitter.prependOnceListener("x", {}), + () => emitter.removeListener("x", 4), + () => emitter.setMaxListeners(-1), + () => e.setMaxListeners(NaN), + () => { + e.default.defaultMaxListeners = -1; + }, + () => { + e.default.captureRejections = 1; + }, + () => new e.EventEmitter({ captureRejections: "yes" }), + () => e.getEventListeners({}, "x"), + () => e.setMaxListeners(1, {}), + () => e.addAbortListener(undefined, () => {}), + () => e.addAbortListener(new AbortController().signal, 1), + () => e.once(emitter, "x", 1), + () => e.on(emitter, "x", { signal: 1 }), + ]; + for (const run of operations) { + try { + await run(); + errors.push(null); + } catch (error) { + errors.push({ + name: error.name, + code: error.code, + message: error.message, + }); + } + } + return errors; + }, + errorMonitor: async ({ events: e }) => { + const emitter = new e.EventEmitter(); + const log = []; + const error = new Error("failure"); + emitter.on(e.errorMonitor, (value) => + log.push(["monitor", value === error]), + ); + let thrown; + try { + emitter.emit("error", error); + } catch (value) { + thrown = value === error; + } + emitter.on("error", (value) => log.push(["handler", value === error])); + const emitted = emitter.emit("error", error); + return { log, thrown, emitted }; + }, + once: async ({ events: e }) => { + const emitter = new e.EventEmitter(); + const first = e.once(emitter, "item"); + emitter.emit("item", 1, 2); + const value = await first; + const second = e.once(emitter, "item"); + emitter.emit("error", new Error("rejected")); + let error; + try { + await second; + } catch (value) { + error = value.message; + } + const target = new EventTarget(); + const third = e.once(target, "item"); + target.dispatchEvent(new Event("item")); + const targetValue = (await third).map((value) => value.type); + return { value, error, targetValue, listeners: emitter.eventNames() }; + }, + onceAbort: async ({ events: e }) => { + const results = []; + for (const alreadyAborted of [true, false]) { + const emitter = new e.EventEmitter(); + const controller = new AbortController(); + if (alreadyAborted) controller.abort("reason"); + const promise = e.once(emitter, "item", { signal: controller.signal }); + if (!alreadyAborted) controller.abort("reason"); + try { + await promise; + } catch (error) { + results.push({ + name: error.name, + code: error.code, + cause: error.cause ?? null, + listeners: emitter.eventNames(), + }); + } + } + return results; + }, + iteratorQueue: async ({ events: e }) => { + const emitter = new e.EventEmitter(); + const iterator = e.on(emitter, "item"); + const identity = iterator[Symbol.asyncIterator]() === iterator; + emitter.emit("item", 1); + emitter.emit("item", 2, 3); + const values = [await iterator.next(), await iterator.next()]; + const pending = [iterator.next(), iterator.next()]; + const returned = await iterator.return(); + return { + identity, + values, + returned, + pending: await Promise.all(pending), + next: await iterator.next(), + listeners: emitter.eventNames(), + }; + }, + iteratorError: async ({ events: e }) => { + const result = []; + for (const pending of [true, false]) { + const emitter = new e.EventEmitter(); + const iterator = e.on(emitter, "item"); + const next = pending ? iterator.next() : null; + emitter.emit("error", new Error("failure")); + let message; + try { + await (next ?? iterator.next()); + } catch (error) { + message = error.message; + } + result.push({ + message, + next: await iterator.next(), + listeners: emitter.eventNames(), + }); + } + return result; + }, + iteratorThrow: async ({ events: e }) => { + const emitter = new e.EventEmitter(); + const iterator = e.on(emitter, "item"); + let invalid; + try { + iterator.throw("bad"); + } catch (error) { + invalid = error.code; + } + const returned = iterator.throw(new Error("thrown")); + let message; + try { + await iterator.next(); + } catch (error) { + message = error.message; + } + await iterator.return(); + return { invalid, returned, message, listeners: emitter.eventNames() }; + }, + iteratorAbort: async ({ events: e }) => { + const emitter = new e.EventEmitter(); + const controller = new AbortController(); + const iterator = e.on(emitter, "item", { signal: controller.signal }); + const pending = iterator.next(); + controller.abort(); + let code; + try { + await pending; + } catch (error) { + code = error.code; + } + let already; + try { + e.on(emitter, "item", { signal: controller.signal }); + } catch (error) { + already = error.code; + } + return { + code, + already, + done: await iterator.next(), + listeners: emitter.eventNames(), + }; + }, + rejectionHook: async ({ events: e }) => { + const emitter = new e.EventEmitter({ captureRejections: true }); + const captured = new Promise((resolve) => { + emitter[e.captureRejectionSymbol] = function (error, event, ...args) { + resolve({ + receiver: this === emitter, + message: error.message, + event, + args, + }); + }; + }); + emitter.on("item", async () => { + throw new Error("rejection"); + }); + emitter.emit("item", 1, 2); + return captured; + }, + resourceConcurrency: async ({ events: e, AsyncLocalStorage }) => { + const als = new AsyncLocalStorage(); + const resources = ["a", "b"].map((value) => + als.run(value, () => new e.EventEmitterAsyncResource()), + ); + const completions = resources.map( + (emitter, i) => + new Promise((resolve) => { + emitter.on("item", async () => { + const before = als.getStore(); + await Promise.resolve(); + const afterPromise = als.getStore(); + await new Promise((done) => setTimeout(done, i)); + resolve([before, afterPromise, als.getStore()]); + }); + }), + ); + als.run("caller", () => { + for (const resource of resources) resource.emit("item"); + }); + return { + values: await Promise.all(completions), + restored: als.getStore() ?? null, + }; + }, + resource: async ({ events: e, AsyncLocalStorage }) => { + const als = new AsyncLocalStorage(); + const emitter = als.run("created", () => new e.EventEmitterAsyncResource()); + const seen = []; + emitter.on("item", function (value) { + seen.push([als.getStore(), this === emitter, value]); + }); + const emitted = als.run("caller", () => emitter.emit("item", 4)); + return { + seen, + emitted, + empty: emitter.emit("absent"), + owner: emitter.asyncResource.eventEmitter === emitter, + id: emitter.asyncResource.asyncId(), + trigger: emitter.asyncResource.triggerAsyncId(), + restored: als.getStore() ?? null, + }; + }, +}; diff --git a/src/compat/events/index.ts b/src/compat/events/index.ts new file mode 100644 index 00000000..0fa29891 --- /dev/null +++ b/src/compat/events/index.ts @@ -0,0 +1,1049 @@ +// 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 +// +// Adapted from Deno and Node.js: +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. +// +// Adapted from Node.js. 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. +/* eslint-disable */ +// Workers events profile for the transformed WebView execution target. +// Source: cloudflare/workerd@5358e3d337336eaf696a48bc1563d808b10feba5 +// src/node/internal/events.ts. Adaptations: local validators/errors, host +// process/async_hooks/util dependencies, spliceOne, pinned process-v2 flag. +// See README.md for the supported execution profile and engine boundaries. + +import { + AbortError, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_THIS, + ERR_OUT_OF_RANGE, + ERR_UNHANDLED_ERROR, +} from "./errors.js"; +import type { + EventEmitterAsyncResource as _EventEmitterAsyncResource, + EventEmitter as _EventEmitter, +} from "node:events"; +import { + validateAbortSignal, + validateBoolean, + validateFunction, + validateObject, +} from "../net/validators.js"; +function spliceOne(list: unknown[], index: number) { + for (; index + 1 < list.length; index++) list[index] = list[index + 1]; + list.pop(); +} +import { nextTick, emitWarning } from "../process/index.js"; +import { default as async_hooks } from "node:async_hooks"; +const { AsyncResource } = async_hooks; + +import { inspect } from "node:util"; + +const enableNodejsProcessV2 = true; + +const kRejection = Symbol.for("nodejs.rejection"); +const kCapture = Symbol("kCapture"); +const kErrorMonitor = Symbol("events.errorMonitor"); +const kMaxEventTargetListeners = Symbol("events.maxEventTargetListeners"); +const kMaxEventTargetListenersWarned = Symbol( + "events.maxEventTargetListenersWarned", +); + +export interface EventEmitterOptions { + captureRejections?: boolean; +} + +export type EventName = string | symbol | number; +export type EventCallback = ((...args: any[]) => unknown) & { + listener?: EventCallback; +}; +// Upstream ambient declaration removed for Babel parsing; runtime unchanged. +interface EventEmitter extends _EventEmitter { + _events: Record | undefined; + _eventsCount: number; +} + +type AsyncResource = typeof AsyncResource; + +declare var EventTarget: Function; + +export function EventEmitter( + this: EventEmitter, + opts?: EventEmitterOptions, +): EventEmitter { + EventEmitter.init.call(this, opts); + return this; +} + +class EventEmitterReferencingAsyncResource extends AsyncResource { + #eventEmitter: EventEmitter; + constructor(emitter: EventEmitter) { + super(""); + this.#eventEmitter = emitter; + } + + get eventEmitter() { + if (this.#eventEmitter === undefined) + throw new ERR_INVALID_THIS("EventEmitterReferencingAsyncResource"); + return this.#eventEmitter; + } +} + +const EventEmitterConstructor = EventEmitter as unknown as typeof _EventEmitter; + +export class EventEmitterAsyncResource + extends EventEmitterConstructor + implements _EventEmitterAsyncResource +{ + #asyncResource: EventEmitterReferencingAsyncResource; + + constructor(options?: EventEmitterOptions) { + super(options); + this.#asyncResource = new EventEmitterReferencingAsyncResource( + this as unknown as EventEmitter, + ); + } + + // @ts-expect-error TS2416 Not assignable to base type + get asyncResource(): AsyncResource { + if (this.#asyncResource === undefined) + throw new ERR_INVALID_THIS("EventEmitterAsyncResource"); + // @ts-expect-error TS2741 Prototype is missing from type. + return this.#asyncResource; + } + + override emit(event: string | symbol, ...args: any[]): boolean { + if (this.#asyncResource === undefined) + throw new ERR_INVALID_THIS("EventEmitterAsyncResource"); + args.unshift(super.emit, this, event); + Reflect.apply( + this.#asyncResource.runInAsyncScope, + this.#asyncResource, + args, + ); + return true; + } +} + +export function addAbortListener( + signal: AbortSignal | undefined, + listener: any, +) { + if (signal === undefined) { + throw new ERR_INVALID_ARG_TYPE("signal", "AbortSignal", signal); + } + validateAbortSignal(signal, "signal"); + validateFunction(listener, "listener"); + + let removeEventListener: Function; + if (signal.aborted) { + queueMicrotask(() => listener()); + } else { + signal.addEventListener("abort", listener, { once: true }); + removeEventListener = () => { + signal.removeEventListener("abort", listener); + }; + } + return { + __proto__: null, + [Symbol.dispose]() { + removeEventListener?.(); + }, + }; +} + +export default EventEmitter; + +EventEmitter.on = on; +EventEmitter.once = once; +EventEmitter.getEventListeners = getEventListeners; +EventEmitter.getMaxListeners = _getMaxListeners; +EventEmitter.setMaxListeners = setMaxListeners; +EventEmitter.listenerCount = listenerCount; +EventEmitter.EventEmitter = EventEmitter; +EventEmitter.usingDomains = false; +EventEmitter.captureRejectionSymbol = kRejection; +EventEmitter.errorMonitor = kErrorMonitor; +EventEmitter.EventEmitterAsyncResource = EventEmitterAsyncResource; + +export const captureRejectionSymbol = EventEmitter.captureRejectionSymbol; +export const captureRejections = false; +export const errorMonitor = EventEmitter.errorMonitor; +export const getMaxListeners = _getMaxListeners; +export const usingDomains = EventEmitter.usingDomains; +export let defaultMaxListeners = 10; + +Object.defineProperties(EventEmitter, { + captureRejections: { + get(this: EventEmitter) { + return EventEmitter.prototype[kCapture]; + }, + set(this: EventEmitter, value: unknown): void { + validateBoolean(value, "EventEmitter.captureRejections"); + + EventEmitter.prototype[kCapture] = value; + }, + enumerable: true, + }, + defaultMaxListeners: { + enumerable: true, + get: function (this: EventEmitter): number { + return defaultMaxListeners; + }, + set: function (this: EventEmitter, arg: unknown): void { + if (typeof arg !== "number" || arg < 0 || Number.isNaN(arg)) { + throw new ERR_OUT_OF_RANGE( + "defaultMaxListeners", + "a non-negative number", + arg, + ); + } + defaultMaxListeners = arg; + }, + }, + kMaxEventTargetListeners: { + value: kMaxEventTargetListeners, + enumerable: false, + configurable: false, + writable: false, + }, + kMaxEventTargetListenersWarned: { + value: kMaxEventTargetListenersWarned, + enumerable: false, + configurable: false, + writable: false, + }, +}); + +// The default for captureRejections is false +Object.defineProperty(EventEmitter.prototype, kCapture, { + value: false, + writable: true, + enumerable: false, +}); + +EventEmitter.init = function (this: EventEmitter, opts?: EventEmitterOptions) { + if ( + this._events === undefined || + this._events === Object.getPrototypeOf(this)._events + ) { + this._events = Object.create(null); + this._eventsCount = 0; + } + + (this as any)._maxListeners ??= undefined; + (this as any).domain = null; + + if (opts?.captureRejections) { + validateBoolean(opts.captureRejections, "options.captureRejections"); + (this as any)[kCapture] = Boolean(opts.captureRejections); + } else { + // Assigning the kCapture property directly saves an expensive + // prototype lookup in a very sensitive hot path. + (this as any)[kCapture] = EventEmitter.prototype[kCapture]; + } +}; + +export function setMaxListeners( + n = defaultMaxListeners, + ...eventTargets: any[] +) { + if (typeof n !== "number" || n < 0 || Number.isNaN(n)) { + throw new ERR_OUT_OF_RANGE("n", "a non-negative number", n); + } + if (eventTargets.length === 0) { + defaultMaxListeners = n; + } else { + for (let i = 0; i < eventTargets.length; i++) { + const target = eventTargets[i]; + if (target instanceof EventTarget) { + (target as any)[kMaxEventTargetListeners] = n; + (target as any)[kMaxEventTargetListenersWarned] = false; + } else if (typeof target.setMaxListeners === "function") { + target.setMaxListeners(n); + } else { + throw new ERR_INVALID_ARG_TYPE( + "eventTargets", + ["EventEmitter", "EventTarget"], + target, + ); + } + } + } +} + +EventEmitter.prototype._events = undefined; +EventEmitter.prototype._eventsCount = 0; +EventEmitter.prototype._maxListeners = undefined; +EventEmitter.addAbortListener = addAbortListener; + +function addCatch( + that: any, + promise: Promise, + type: string | symbol, + args: any[], +) { + if (!that[kCapture]) { + return; + } + + // Handle Promises/A+ spec, then could be a getter + // that throws on second use. + try { + const then = promise.then; + + if (typeof then === "function") { + then.call(promise, undefined, function (err) { + // The callback is called with nextTick to avoid a follow-up + // rejection from this promise. + nextTick(emitUnhandledRejectionOrErr, that, err, type, args); + }); + } + } catch (err) { + that.emit("error", err); + } +} + +function emitUnhandledRejectionOrErr( + ee: any, + err: any, + type: string | symbol, + args: any[], +) { + if (typeof ee[kRejection] === "function") { + ee[kRejection](err, type, ...args); + } else { + // We have to disable the capture rejections mechanism, otherwise + // we might end up in an infinite loop. + const prev = ee[kCapture]; + + // If the error handler throws, it is not catchable and it + // will end up in 'uncaughtException'. We restore the previous + // value of kCapture in case the uncaughtException is present + // and the exception is handled. + try { + ee[kCapture] = false; + ee.emit("error", err); + } finally { + ee[kCapture] = prev; + } + } +} + +EventEmitter.prototype.setMaxListeners = function setMaxListeners(n: number) { + if (typeof n !== "number" || n < 0 || Number.isNaN(n)) { + throw new ERR_OUT_OF_RANGE("n", "a non-negative number", n); + } + this._maxListeners = n; + return this; +}; + +function _getMaxListeners(that: any) { + if (that._maxListeners === undefined) { + return (EventEmitter as any).defaultMaxListeners; + } + return that._maxListeners; +} + +EventEmitter.prototype.getMaxListeners = function getMaxListeners( + this: EventEmitter, +): number { + return _getMaxListeners(this); +}; + +EventEmitter.prototype.emit = function emit( + this: EventEmitter, + type: string | symbol, + ...args: any[] +): boolean { + let doError = type === "error"; + + const events = this._events; + if (events !== undefined) { + if (doError && events[kErrorMonitor] !== undefined) { + this.emit(kErrorMonitor, ...args); + } + doError = doError && events.error === undefined; + } else if (!doError) { + return false; + } + + // If there is no 'error' event listener then throw. + if (doError) { + let er; + if (args.length > 0) { + er = args[0]; + } + if (er instanceof Error) { + try { + const capture = {}; + (Error as any).captureStackTrace(capture, EventEmitter.prototype.emit); + } catch { + // pass + } + + // Note: The comments on the `throw` lines are intentional, they show + // up in Node's output if this results in an unhandled exception. + throw er; // Unhandled 'error' event + } + + let stringifiedEr; + try { + stringifiedEr = inspect(er); + } catch { + stringifiedEr = er; + } + + // At least give some kind of context to the user + const err = new ERR_UNHANDLED_ERROR(stringifiedEr); + (err as any).context = er; + throw err; // Unhandled 'error' event + } + + const handler = events?.[type]; + + if (handler === undefined) { + return false; + } + + if (typeof handler === "function") { + const result = (handler as Function).apply(this, args); + + // We check if result is undefined first because that + // is the most common case so we do not pay any perf + // penalty + if (result !== undefined && result !== null) { + addCatch(this, result, type, args); + } + } else { + const len = handler.length; + const listeners = arrayClone(handler); + for (let i = 0; i < len; ++i) { + const result = listeners[i].apply(this, args); + + // We check if result is undefined first because that + // is the most common case so we do not pay any perf + // penalty. + // This code is duplicated because extracting it away + // would make it non-inlineable. + if (result !== undefined && result !== null) { + addCatch(this, result, type, args); + } + } + } + + return true; +}; + +function _addListener( + target: any, + type: string | symbol, + listener: unknown, + prepend: boolean, +) { + let m; + let events; + let existing; + + validateFunction(listener, "listener"); + + events = target._events; + if (events === undefined) { + events = target._events = Object.create(null); + target._eventsCount = 0; + } else { + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (events.newListener !== undefined) { + target.emit("newListener", type, (listener as any).listener ?? listener); + + // Re-assign `events` because a newListener handler could have caused the + // this._events to be assigned to a new object + events = target._events; + } + existing = events[type]; + } + + if (existing === undefined) { + // Optimize the case of one listener. Don't need the extra array object. + events[type] = listener; + ++target._eventsCount; + } else { + if (typeof existing === "function") { + // Adding the second element, need to change to array. + existing = events[type] = prepend + ? [listener, existing] + : [existing, listener]; + // If we've already got an array, just append. + } else if (prepend) { + existing.unshift(listener); + } else { + existing.push(listener); + } + + // Check for listener leak + m = _getMaxListeners(target); + if (m > 0 && existing.length > m && !existing.warned) { + existing.warned = true; + console.log( + "Possible EventEmitter memory leak detected. " + + `${existing.length} ${String(type)} listeners ` + + `added to an EventEmitter. Use ` + + "emitter.setMaxListeners() to increase limit", + ); + const w: EventEmitterError = Object.assign( + new Error( + "Possible EventEmitter memory leak detected. " + + `${existing.length} ${String(type)} listeners ` + + `added to ${inspect(target, { depth: -1 })}. Use ` + + "emitter.setMaxListeners() to increase limit", + ), + { + name: "MaxListenersExceededWarning", + emitter: target, + type: type, + count: existing.length, + }, + ); + // Only the newer process version compat adds process.emitWarning support. + if (enableNodejsProcessV2) emitWarning(w); + } + } + + return target; +} + +interface EventEmitterError extends Error { + name: string; + emitter: unknown; + type: string | symbol; + count: number; +} + +EventEmitter.prototype.addListener = function addListener( + this: EventEmitter, + type: string | symbol, + listener: unknown, +) { + return _addListener(this, type, listener, false); +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.prependListener = function prependListener( + type: string | symbol, + listener: unknown, +) { + return _addListener(this, type, listener, true); +}; + +function onceWrapper(this: any) { + if (!this.fired) { + this.target.removeListener(this.type, this.wrapFn); + this.fired = true; + if (arguments.length === 0) { + return this.listener.call(this.target); + } + return this.listener.apply(this.target, arguments); + } +} + +function _onceWrap(target: any, type: string | symbol, listener: unknown) { + const state = { fired: false, wrapFn: undefined, target, type, listener }; + const wrapped = onceWrapper.bind(state); + (wrapped as any).listener = listener; + (state as any).wrapFn = wrapped; + return wrapped; +} + +EventEmitter.prototype.once = function once( + type: string | symbol, + listener: unknown, +) { + validateFunction(listener, "listener"); + + this.on(type, _onceWrap(this, type, listener)); + return this; +}; + +EventEmitter.prototype.prependOnceListener = function prependOnceListener( + type: string | symbol, + listener: unknown, +) { + validateFunction(listener, "listener"); + + this.prependListener(type, _onceWrap(this, type, listener)); + return this; +}; + +EventEmitter.prototype.removeListener = function removeListener( + type: string | symbol, + listener: unknown, +) { + validateFunction(listener, "listener"); + + const events = this._events; + if (events === undefined) { + return this; + } + + const list = events[type] as EventCallback | EventCallback[] | undefined; + if (list === undefined) { + return this; + } + + if (list === listener || ("listener" in list && list.listener === listener)) { + if (--this._eventsCount === 0) { + this._events = Object.create(null); + } else { + delete events[type]; + if (events.removeListener) { + this.emit("removeListener", type, list.listener || listener); + } + } + } else if (typeof list !== "function") { + let position = -1; + + for (let i = list.length - 1; i >= 0; i--) { + if ( + list[i] === listener || + (list[i] as EventCallback).listener === listener + ) { + position = i; + break; + } + } + + if (position < 0) { + return this; + } + + if (position === 0) { + list.shift(); + } else { + spliceOne(list, position); + } + + if (list.length === 1) { + events[type] = list.at(0) as unknown as EventCallback[]; + } + + if (events.removeListener !== undefined) { + this.emit("removeListener", type, listener); + } + } + + return this; +}; + +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; + +EventEmitter.prototype.removeAllListeners = function removeAllListeners( + type: string | symbol, +) { + const events = this._events; + if (events === undefined) { + return this; + } + + // Not listening for removeListener, no need to emit + if (events.removeListener === undefined) { + if (arguments.length === 0) { + this._events = Object.create(null); + this._eventsCount = 0; + } else if (events[type] !== undefined) { + if (--this._eventsCount === 0) { + this._events = Object.create(null); + } else { + delete events[type]; + } + } + return this; + } + + // Emit removeListener for all listeners on all events + if (arguments.length === 0) { + for (const key of Reflect.ownKeys(events)) { + if (key === "removeListener") continue; + this.removeAllListeners(key); + } + this.removeAllListeners("removeListener"); + this._events = Object.create(null); + this._eventsCount = 0; + return this; + } + + const listeners = events[type]; + + if (typeof listeners === "function") { + this.removeListener(type, listeners); + } else if (listeners !== undefined) { + // LIFO order + for (let i = listeners.length - 1; i >= 0; i--) { + this.removeListener(type, listeners[i] as EventListener); + } + } + + return this; +}; + +function _listeners(target: any, type: string | symbol, unwrap: boolean) { + const events = target._events; + + if (events === undefined) { + return []; + } + + const evlistener = events[type]; + if (evlistener === undefined) { + return []; + } + + if (typeof evlistener === "function") { + return unwrap ? [evlistener.listener || evlistener] : [evlistener]; + } + + return unwrap ? unwrapListeners(evlistener) : arrayClone(evlistener); +} + +EventEmitter.prototype.listeners = function listeners(type: string | symbol) { + return _listeners(this, type, true); +}; + +EventEmitter.prototype.rawListeners = function rawListeners( + type: string | symbol, +) { + return _listeners(this, type, false); +}; + +const _listenerCount = function listenerCount( + this: any, + type: string | symbol, +) { + const events = this._events; + + if (events !== undefined) { + const evlistener = events[type]; + + if (typeof evlistener === "function") { + return 1; + } else if (evlistener !== undefined) { + return evlistener.length; + } + } + + return 0; +}; + +EventEmitter.prototype.listenerCount = _listenerCount; + +export function listenerCount(emitter: any, type: string | symbol) { + if (typeof emitter.listenerCount === "function") { + return emitter.listenerCount(type); + } + return _listenerCount.call(emitter, type); +} + +EventEmitter.prototype.eventNames = function eventNames() { + return this._eventsCount > 0 ? Reflect.ownKeys(this._events || {}) : []; +}; + +function arrayClone(arr: any[]) { + // At least since V8 8.3, this implementation is faster than the previous + // which always used a simple for-loop + switch (arr.length) { + case 2: + return [arr[0], arr[1]]; + case 3: + return [arr[0], arr[1], arr[2]]; + case 4: + return [arr[0], arr[1], arr[2], arr[3]]; + case 5: + return [arr[0], arr[1], arr[2], arr[3], arr[4]]; + case 6: + return [arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]]; + } + return arr.slice(); +} + +function unwrapListeners(arr: any[]) { + const ret = arrayClone(arr); + for (let i = 0; i < ret.length; ++i) { + const orig = ret[i].listener; + if (typeof orig === "function") { + ret[i] = orig; + } + } + return ret; +} + +export function getEventListeners(emitterOrTarget: any, type: string | symbol) { + // First check if EventEmitter + if (typeof emitterOrTarget.listeners === "function") { + return emitterOrTarget.listeners(type); + } + if (emitterOrTarget instanceof EventTarget) { + // Workers does not implement the ability to get the event listeners on an + // EventTarget the way that Node.js does. We simply return empty here. + return []; + } + throw new ERR_INVALID_ARG_TYPE( + "emitter", + ["EventEmitter", "EventTarget"], + emitterOrTarget, + ); +} + +export interface OnceOptions { + signal?: AbortSignal; +} + +export async function once( + emitter: any, + name: string | symbol, + options: OnceOptions = {}, +) { + validateObject(options, "options"); + const { signal } = options; + validateAbortSignal(signal, "options.signal"); + if (signal?.aborted) { + throw new AbortError(undefined, { cause: signal.reason }); + } + return new Promise((resolve, reject) => { + const errorListener = (err: any) => { + emitter.removeListener(name, resolver); + if (signal != null) { + eventTargetAgnosticRemoveListener(signal, "abort", abortListener); + } + reject(err); + }; + const resolver = (...args: any[]) => { + if (typeof emitter.removeListener === "function") { + emitter.removeListener("error", errorListener); + } + if (signal != null) { + eventTargetAgnosticRemoveListener(signal, "abort", abortListener); + } + resolve(args); + }; + eventTargetAgnosticAddListener(emitter, name, resolver, { once: true }); + if (name !== "error" && typeof emitter.once === "function") { + // EventTarget does not have `error` event semantics like Node + // EventEmitters, we listen to `error` events only on EventEmitters. + emitter.once("error", errorListener); + } + function abortListener() { + eventTargetAgnosticRemoveListener(emitter, name, resolver); + eventTargetAgnosticRemoveListener(emitter, "error", errorListener); + reject(new AbortError()); + } + if (signal != null) { + eventTargetAgnosticAddListener(signal, "abort", abortListener, { + once: true, + }); + } + }); +} + +import { AsyncIteratorPrototype } from "./intrinsics.js"; + +function createIterResult(value: any, done: boolean) { + return { value, done }; +} + +function eventTargetAgnosticRemoveListener( + emitter: any, + name: string | symbol, + listener: unknown, + flags: unknown = undefined, +) { + if (typeof emitter.removeListener === "function") { + emitter.removeListener(name, listener); + } else if (typeof emitter.removeEventListener === "function") { + emitter.removeEventListener(name, listener, flags); + } else { + throw new ERR_INVALID_ARG_TYPE("emitter", "EventEmitter", emitter); + } +} + +interface AddListenerFlags { + once?: boolean; +} + +function eventTargetAgnosticAddListener( + emitter: any, + name: string | symbol, + listener: unknown, + flags: AddListenerFlags = {}, +) { + if (typeof emitter.on === "function") { + if (flags?.once) { + emitter.once(name, listener); + } else { + emitter.on(name, listener); + } + } else if (typeof emitter.addEventListener === "function") { + // EventTarget does not have `error` event semantics like Node + // EventEmitters, we do not listen to `error` events here. + emitter.addEventListener( + name, + (arg: unknown) => { + (listener as any)(arg); + }, + flags, + ); + } else { + throw new ERR_INVALID_ARG_TYPE("emitter", "EventEmitter", emitter); + } +} + +interface OnOptions { + signal?: AbortSignal; +} + +export function on( + emitter: any, + event: string | symbol, + options: OnOptions = {}, +) { + const signal = options?.signal; + validateAbortSignal(signal, "options.signal"); + if (signal?.aborted) { + throw new AbortError(); + } + + const unconsumedEvents: any[] = []; + const unconsumedPromises: any[] = []; + let error: any = null; + let finished = false; + + const iterator = Object.setPrototypeOf( + { + next() { + // First, we consume all unread events + const value = unconsumedEvents.shift(); + if (value) { + return Promise.resolve(createIterResult(value, false)); + } + + // Then we error, if an error happened + // This happens one time if at all, because after 'error' + // we stop listening + if (error) { + const p = Promise.reject(error); + // Only the first element errors + error = null; + return p; + } + + // If the iterator is finished, resolve to done + if (finished) { + return Promise.resolve(createIterResult(undefined, true)); + } + + // Wait until an event happens + return new Promise(function (resolve, reject) { + unconsumedPromises.push({ resolve, reject }); + }); + }, + + return() { + eventTargetAgnosticRemoveListener(emitter, event, eventHandler); + eventTargetAgnosticRemoveListener(emitter, "error", errorHandler); + + if (signal) { + eventTargetAgnosticRemoveListener(signal, "abort", abortListener, { + once: true, + }); + } + + finished = true; + + for (const promise of unconsumedPromises) { + promise.resolve(createIterResult(undefined, true)); + } + + return Promise.resolve(createIterResult(undefined, true)); + }, + + throw(err: any) { + if (!err || !(err instanceof Error)) { + throw new ERR_INVALID_ARG_TYPE( + "EventEmitter.AsyncIterator", + "Error", + err, + ); + } + error = err; + eventTargetAgnosticRemoveListener(emitter, event, eventHandler); + eventTargetAgnosticRemoveListener(emitter, "error", errorHandler); + }, + + [Symbol.asyncIterator]() { + return this; + }, + }, + AsyncIteratorPrototype, + ); + + eventTargetAgnosticAddListener(emitter, event, eventHandler); + if (event !== "error" && typeof emitter.on === "function") { + emitter.on("error", errorHandler); + } + + if (signal) { + eventTargetAgnosticAddListener(signal, "abort", abortListener, { + once: true, + }); + } + + return iterator; + + function abortListener() { + errorHandler(new AbortError()); + } + + function eventHandler(...args: any[]) { + const promise = unconsumedPromises.shift(); + if (promise) { + promise.resolve(createIterResult(args, false)); + } else { + unconsumedEvents.push(args); + } + } + + function errorHandler(err: any) { + finished = true; + + const toError = unconsumedPromises.shift(); + + if (toError) { + toError.reject(err); + } else { + // The next time we call next() + error = err; + } + + iterator.return(); + } +} diff --git a/src/compat/events/intrinsics.ts b/src/compat/events/intrinsics.ts new file mode 100644 index 00000000..47ff2482 --- /dev/null +++ b/src/compat/events/intrinsics.ts @@ -0,0 +1,4 @@ +// Native intrinsic discovery must survive the ordinary async lowering pipeline. +export const AsyncIteratorPrototype = Object.getPrototypeOf( + Object.getPrototypeOf(async function* () {}).prototype, +); diff --git a/src/compat/events/observations.spec.mjs b/src/compat/events/observations.spec.mjs new file mode 100644 index 00000000..300d0c68 --- /dev/null +++ b/src/compat/events/observations.spec.mjs @@ -0,0 +1,167 @@ +// Shared feasibility observations, not whole-module acceptance. +export async function observeEvents(events, AsyncLocalStorage) { + const emitter = new events.EventEmitter(); + const handler = () => {}; + emitter.on("item", handler); + const listed = events.getEventListeners(emitter, "item"); + listed.length = 0; + const target = new EventTarget(); + target.addEventListener("item", handler); + const targetListeners = events.getEventListeners(target, "item").length; + events.setMaxListeners(2, target); + const targetMax = events.getMaxListeners(target); + const aborted = new AbortController(); + aborted.abort("cause"); + const order = []; + const disposed = events.addAbortListener(aborted.signal, () => + order.push("callback"), + ); + disposed[Symbol.dispose](); + order.push("sync"); + await Promise.resolve(); + const pending = new AbortController(); + let pendingCalls = 0; + const disposable = events.addAbortListener( + pending.signal, + () => pendingCalls++, + ); + disposable[Symbol.dispose](); + pending.abort(); + const stopped = new AbortController(); + let stoppedCalls = 0; + stopped.signal.addEventListener("abort", (event) => + event.stopImmediatePropagation(), + ); + events.addAbortListener(stopped.signal, () => stoppedCalls++); + stopped.abort(); + const als = new AsyncLocalStorage(); + const resource = als.run( + "creation", + () => new events.EventEmitterAsyncResource(), + ); + let store; + resource.on("item", () => { + store = als.getStore(); + }); + const emitResult = als.run("dispatch", () => resource.emit("item")); + const emptyEmitResult = resource.emit("absent"); + const iterator = events.on(emitter, "data"); + emitter.emit("data", 1, 2); + const first = await iterator.next(); + await iterator.return(); + const iteratorCleaned = + emitter.listenerCount("data") === 0 && emitter.listenerCount("error") === 0; + const captureBefore = events.captureRejections; + events.default.captureRejections = true; + const captureAfter = events.captureRejections; + const captureDefault = events.default.captureRejections; + events.default.captureRejections = false; + const sequence = []; + const ordered = new events.EventEmitter(); + const late = () => sequence.push("late"); + ordered.on("tick", () => { + sequence.push("first"); + ordered.removeListener("tick", late); + }); + ordered.on("tick", late); + ordered.prependOnceListener("tick", () => sequence.push("once")); + ordered.emit("tick"); + ordered.emit("tick"); + const promiseEmitter = new events.EventEmitter(); + const pendingOnce = events.once(promiseEmitter, "ready"); + promiseEmitter.emit("ready", 3, 4); + const onceValue = await pendingOnce; + const onceCleaned = + promiseEmitter.listenerCount("ready") === 0 && + promiseEmitter.listenerCount("error") === 0; + const abort = new AbortController(); + const abortOnce = events.once(promiseEmitter, "ready", { + signal: abort.signal, + }); + abort.abort("cancel"); + let abortError; + try { + await abortOnce; + } catch (error) { + abortError = { + name: error.name, + code: error.code, + cause: error.cause ?? null, + }; + } + const abortCleaned = + promiseEmitter.listenerCount("ready") === 0 && + promiseEmitter.listenerCount("error") === 0; + const rejectionEmitter = new events.EventEmitter({ captureRejections: true }); + const captured = new Promise((resolve) => + rejectionEmitter.once("error", (error) => resolve(error.message)), + ); + rejectionEmitter.on("reject", async () => { + throw new Error("captured"); + }); + rejectionEmitter.emit("reject"); + const capturedMessage = await captured; + const errors = []; + for (const operation of [ + () => emitter.on("x", 1), + () => events.setMaxListeners(-1), + () => emitter.emit("error", "failure"), + ]) { + try { + operation(); + } catch (error) { + errors.push({ name: error.name, code: error.code }); + } + } + return { + sequence, + onceValue, + onceCleaned, + abortError, + abortCleaned, + capturedMessage, + errors, + emitterListeners: emitter.listenerCount("item"), + targetListeners, + targetMax, + order, + pendingCalls, + stoppedCalls, + store, + emitResult, + emptyEmitResult, + first, + iteratorCleaned, + captureBefore, + captureAfter, + captureDefault, + }; +} + +export const expectedEvents = { + sequence: ["once", "first", "late", "first"], + onceValue: [3, 4], + onceCleaned: true, + abortError: { name: "AbortError", code: "ABORT_ERR", cause: null }, + abortCleaned: true, + capturedMessage: "captured", + errors: [ + { name: "TypeError", code: "ERR_INVALID_ARG_TYPE" }, + { name: "RangeError", code: "ERR_OUT_OF_RANGE" }, + { name: "Error", code: "ERR_UNHANDLED_ERROR" }, + ], + emitterListeners: 1, + targetListeners: 0, + targetMax: 10, + order: ["sync", "callback"], + pendingCalls: 0, + stoppedCalls: 0, + store: "creation", + emitResult: true, + emptyEmitResult: true, + first: { value: [1, 2], done: false }, + iteratorCleaned: true, + captureBefore: false, + captureAfter: false, + captureDefault: true, +}; diff --git a/src/compat/expansion/README.md b/src/compat/expansion/README.md new file mode 100644 index 00000000..2f9ccd22 --- /dev/null +++ b/src/compat/expansion/README.md @@ -0,0 +1,76 @@ +# Additional compatibility profiles + +These adapters target workerd 1.20261012.1, compatibility date 2026-10-17. +`demo/tests/experiments/node-compat/expansion-contracts.probe.mjs` compares the +same operations in workerd, Chromium and WebKit, with both bundlers and both +page and Dedicated Worker execution. The native runner uses an installed npm +package and the same workerd expectations. These operation tests do not certify +whole modules. + +Implemented profiles include console's Workers stubs, sys aliasing, public +util/types predicates, crypto random integers/UUIDs/Web Crypto exports, +hash/HKDF/scrypt, the four Workers ECDH curves, crypto engine/secure-heap stubs, +writable memory-backed /tmp, module metadata, HTTP internal +helpers, adapter-owned secret and asymmetric KeyObjects, prime generation and +checking, incremental signatures, Certificate SPKAC and X509Certificate operations, +Brotli/Zstd codecs and CRC32. +Additional profiles cover process resource/permission/report stubs, its pinned +wall-clock hrtime behavior, and TLS hostname/ALPN/options contracts. Native runs +also test real TLS exchange and hostname mismatch rejection. + +The SDK starts a fresh /tmp volume for each local fetch dispatch. Handles opened +inside that request retain their volume; embedded bundle assets stay read-only. +Standalone imports outside SDK dispatch share a fallback volume. Copying embedded files to /tmp preserves immutable embedded bytes. Permission +changes are validated no-ops and statfs returns the pinned Workers zero values. +These checks do not cover every descriptor or cross-volume operation. Watching files is unsupported in Workers too. + +Brotli 1.1.0 and Zstd 1.5.7 use bundled, synchronously initialized WebAssembly. +The profile covers parameters, synchronous/callback operations, incremental +streams, flush/reset, byte accounting and output limits for synchronous +processing. Streaming output is not cumulatively capped by maxOutputLength. +The pinned source revisions and artifact hashes accompany the codecs; rebuild +with scripts/build-native-codecs.mjs and the pinned Emscripten SDK. Remaining +codec edge cases and inherited gzip/deflate behavior still require acceptance. +Scrypt uses a synchronous JavaScript backend, including for callback completion, +so large parameters can block the WebView. + +Adapter-created secret keys support copied raw/JWK export, equality, generation, +HMAC, HKDF and symmetric ciphers. KeyObject.from can retain a host CryptoKey, but +synchronous extraction of its opaque key material is unavailable. RSA, EC, Ed25519 and X25519 key generation and PEM/DER/JWK conversion use +bundled OpenSSL 3.5.9 WebAssembly; RSA/EC streaming and one-shot signatures +are compared with Workers and independent test signatures. This covers selected +algorithms and operations, not the whole crypto module. Additional key-generation +options and remaining error contracts still need work. +X509Certificate parses PEM/DER, exposes certificate fields and legacy objects, +extracts RSA/EC public keys, checks names and issuers, and verifies signatures +with certificate public keys. The pinned Workers runtime returns false for +matching imported/generated private keys and public keys used by these X509 +checks; the adapter preserves this conservative result. It also preserves the +pinned runtime's refusal to match partial or multiple DNS labels with a wildcard, +and its ignored subject-always/single-label-subdomain options. These are Workers profile limits, not Node +semantics or a claim that the keys do not match. +Certificate SPKAC operations preserve the pinned Workers rejection of MD5 +signatures while verifying SHA-256 signatures and extracting public keys and +challenges. Invalid SPKAC data returns false or empty buffers, as in Workers. +The pinned revisions and hashes are in crypto/native-crypto.build.json; rebuild +with scripts/build-native-crypto.mjs and the pinned Emscripten SDK. Entropy comes +from the host crypto.getRandomValues; there is no noncryptographic fallback. +Direct Node TLS is implemented and tested through the native transport. +Existing native sockets can upgrade on the same TCP connection. Application +writes wait for authenticated TLS; hostname mismatch or cancellation closes +the connection without sending queued application bytes. Native acceptance +exchanges a plaintext upgrade command followed by TLS, checks connection counts +and tests page and Dedicated Worker modes on both OSes. Custom Duplex wrapping +is unsupported. iOS upgrades use the existing OpenSSL Wasm backend with memory +BIOs on the original TCP connection; SecTrust must authenticate the peer chain +and hostname before the connection opens or application data is exposed. Android +uses the existing Netty channel with the platform SSLEngine. Native tests cover +TLS 1.2 and 1.3, including repeated write-half-close followed by response reads. +Full TLS class and +HTTP internal state contracts remain open. +Consult the generated completion report for remaining work. + +Workers reports `fips=true` because of its own crypto backend. The WebView's +JavaScript implementations do not establish FIPS compliance, so that flag is +not copied merely to match the export inventory. `setFips` and `setEngine` +retain Workers' unsupported-operation behavior. diff --git a/src/compat/expansion/index.spec.mjs b/src/compat/expansion/index.spec.mjs new file mode 100644 index 00000000..e3bd0a1f --- /dev/null +++ b/src/compat/expansion/index.spec.mjs @@ -0,0 +1,1623 @@ +export const eventCases = { + x509: function ({ crypto: c }) { + const pem = + "-----BEGIN CERTIFICATE-----\nMIIDhDCCAmygAwIBAgIUdN1my4bhw3mAm+rrvYEZBzi4nuAwDQYJKoZIhvcNAQEL\nBQAwJjESMBAGA1UEAwwJbG9jYWxob3N0MRAwDgYDVQQKDAdXb3JrZXJzMB4XDTI2\nMTAyMjIzMTEyNVoXDTI2MTAyNDIzMTEyNVowJjESMBAGA1UEAwwJbG9jYWxob3N0\nMRAwDgYDVQQKDAdXb3JrZXJzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEAmdA4QcozdOsYQJXG5oDzOJEKHIoyM1HDVXgH2ZWTL0Qm9yJJ02uV55IhW2AY\nshAs1ZROFCae5891g4ohl1H4sFDE5HYuD4BLkh9RY8QcZ7170nA1fnxNfO0K0GZs\nJyCOerlt5fLJjcEQiiyjbs8/2yukxHJraHQIZ1cO/PbWJx9O7iZ5Ru7u2ompDrsz\nzTbVgjI/u4g+sHEf+11me+7WW13Lj4FvpDHZp1K9zoMtOj/ruURVs/5wO3gygNnO\nVVrtNc/0nOU0Qqlqyr0QC9ZkHpn374hL2m0KC9BNixbLdQ+Oy4O406UhQXUGOSPg\ndfN3MBgPJrs1yZ1Io6pL41LWXwIDAQABo4GpMIGmMB0GA1UdDgQWBBSlprvE05R3\nZRC3gulykkeMzbOIZzAPBgNVHRMBAf8EBTADAQH/MFUGA1UdEQROMEyCCWxvY2Fs\naG9zdIIOKi5leGFtcGxlLnRlc3SHBH8AAAGBEXRlc3RAZXhhbXBsZS50ZXN0hhZo\ndHRwczovL2V4YW1wbGUudGVzdC9hMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF\nBQcDAjANBgkqhkiG9w0BAQsFAAOCAQEAWO4DydLOPOBNyKYw50dBT/UgF9l7zvk+\nl2C7v1dMVCtpKW4egHUjINf+tOHC7PLjMMSKDvM/NGszAePPRSAJzfvoIEF5nahz\nLAzcM3zIdp7vviP1yeSSgJH5QRvo4pMV13K4FHARR5L/ShZqEKvKbVwDCrzymkND\nV5SGa11p5xf0Fm6061vfgBQ85vNFANXByqznGhn9tBRf5pJGBl4cjTJ9WCl8zVCs\n++24wsetgXwxL9O1p+NvfM8SdTVGBSevAA/OAc2Fjoye4oJHH4n3sCsIxoEeRnb4\nZYhC9Q4UwNucHDFJASAMWC5AfX7P+HnaguZXGHsfBrw/39sZFdncwg==\n-----END CERTIFICATE-----\n"; + const privatePem = + "-----BEGIN PRIVATE KEY-----\nMIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQCZ0DhByjN06xhA\nlcbmgPM4kQocijIzUcNVeAfZlZMvRCb3IknTa5XnkiFbYBiyECzVlE4UJp7nz3WD\niiGXUfiwUMTkdi4PgEuSH1FjxBxnvXvScDV+fE187QrQZmwnII56uW3l8smNwRCK\nLKNuzz/bK6TEcmtodAhnVw789tYnH07uJnlG7u7aiakOuzPNNtWCMj+7iD6wcR/7\nXWZ77tZbXcuPgW+kMdmnUr3Ogy06P+u5RFWz/nA7eDKA2c5VWu01z/Sc5TRCqWrK\nvRAL1mQemffviEvabQoL0E2LFst1D47Lg7jTpSFBdQY5I+B183cwGA8muzXJnUij\nqkvjUtZfAgMBAAECgf8siPZrptQSyRG8lmJJGJoUo5EbjUEG5SHuRhshJdain2Fi\nXTQDAUbTk9qsyLx0Cc9R6i3D0LFeaXIvmqvgIprr+2vAYYKfT7S28Yb2hq6Y1WPW\nUQO5H3/zZ1IraUiG4uR0ZTEVoKP98mieEAgvlMr81UtF0V0uFwZ1kBpkjAE2nGj4\nISzH0MsafINRIYWsJ4cDGJikBY9e0DG2DhG1w4qNmJq+2cWM8+kLVII8V7u97ef1\nWGnSTa3OkmU5wlmUbS3Ju8bAoTkkJ+a49KT3OlKITdF7UKYGG2rseWgZULd5ZSek\nlcnuBAuqD7lJRBbVjI5AFr8fSQU0FzMwrvVAOpECgYEAy4ERAoBZfToz1ILH5u4K\n8N4uZmAafRlVtyPISP1KkSNDQI1gCxuuCsT03lgWOrXKE8gtqICSryMygycBSGni\n3+QUQU61+QnTuslIwTfIZUQG6zTdwEpAmLVJ3KsaaS4rpn3fpq+umPLmZ59h9eJp\nuevxhFv/muWjzRsxxTPsMfECgYEAwX2woht7j6NF9ilSrtSGoESPwCNci431mKJd\no3MOKtjtedYWfycJKUQ+B+do0xd7Y43dTrLcH52ZpvDMsyYg3bP9uWKnvqc4cm8q\ndn4AyYx0W1P4jf8iyLm6yEqif6J6UuLyyoMjsO3b8FBqAHRNdGFIg6f183MYcu4b\ndwUZPU8CgYA68hSiO9kv+7bdnFAY5OSIZmLcRoQFrlv0o0m1UqDRr3wga5LhRNBB\n0FD5QlyV/ugcmMQY5b0XO8O614Z4miscHO19CTaOfzk0Bm+rf+Ps6Spz4KlGtHtH\ngGN3ppwM0VHVdOpeVs7lAJ6ab0Epvr5PGzem1hsQm+m5VGIzM9n7kQKBgFYqY/t8\n46SOoDxEILcMm7tJbP6ygKcBG+rR+4MrXJbGLdMA/Nm5B4JiCoG4ktzWazNlXLWM\nPyyxIVjPiUkKZL52RjpFbdAgZanISY+lAzK+Ip79IQYTJWlgFIbXfc/7CiBZQqfe\niIllE3nMRbhYEpoOLJ28sXGtwqiD0qVJFvlPAoGAaf740rwvulTjEuaMfyaUf95/\n7fiRyD39Ct9GqDLvOiJAyWrVLaRYdV8warqWwwu0KHczAVs+eWe4WhTV7jZvPBLg\nLJcIZuJytVWIYFfSkHgdxk6sFYKCvETi6zgYBREnYkFPYzMdiOX57c2Di8SzAt/T\nUV3iEhCZhSw7+VFVgu8=\n-----END PRIVATE KEY-----\n"; + const cert = new c.X509Certificate(pem), + key = c.createPrivateKey(privatePem); + const attempt = (fn) => { + try { + return fn() ?? null; + } catch (error) { + return { name: error.name, code: error.code, message: error.message }; + } + }; + const fields = [ + "subject", + "issuer", + "subjectAltName", + "infoAccess", + "validFrom", + "validTo", + "fingerprint", + "fingerprint256", + "fingerprint512", + "keyUsage", + "serialNumber", + "ca", + "issuerCertificate", + ]; + const padded = new Uint8Array(cert.raw.length + 4); + padded.set(cert.raw, 2); + return { + fields: Object.fromEntries(fields.map((k) => [k, cert[k] ?? null])), + raw: Array.from(cert.raw), + pem: cert.toString(), + json: cert.toJSON(), + legacy: cert.toLegacyObject(), + cached: + cert.raw === cert.raw && + cert.publicKey === cert.publicKey && + cert.toLegacyObject() === cert.toLegacyObject(), + publicKey: cert.publicKey.export({ format: "pem", type: "spki" }), + der: new c.X509Certificate( + new DataView(padded.buffer, 2, cert.raw.length), + ).fingerprint, + issued: cert.checkIssued(cert), + private: cert.checkPrivateKey(key), + verified: cert.verify(cert.publicKey), + wrongKey: cert.checkPrivateKey( + c.generateKeyPairSync("rsa", { modulusLength: 1024 }).privateKey, + ), + hosts: [ + "localhost", + "x.example.test", + "x.y.example.test", + "wrong.invalid", + "localhost\0wrong", + ].map((n) => attempt(() => cert.checkHost(n))), + options: [ + { wildcards: false }, + { partialWildcards: false }, + { multiLabelWildcards: true }, + { subject: "never" }, + { subject: "invalid" }, + { wildcards: 0 }, + false, + ].map((o) => attempt(() => cert.checkHost("x.y.example.test", o))), + emails: ["test@example.test", "other@example.test"].map((n) => + attempt(() => cert.checkEmail(n)), + ), + ips: ["127.0.0.1", "127.0.0.2", "::1", "not-ip"].map((n) => + attempt(() => cert.checkIP(n)), + ), + invalid: [ + () => new c.X509Certificate("bad"), + () => new c.X509Certificate(new ArrayBuffer(4)), + () => cert.checkIssued({}), + () => cert.checkPrivateKey(cert.publicKey), + () => cert.verify(key), + () => cert.checkHost(1), + ].map(attempt), + }; + }, + + x509Ec: function ({ crypto: c }) { + const pem = + "-----BEGIN CERTIFICATE-----\nMIIBaDCCAQ6gAwIBAgIUR0JYwBRWD1OQflPk9iq6c8s/90swCgYIKoZIzj0EAwIw\nGjEYMBYGA1UEAwwPZWMuZXhhbXBsZS50ZXN0MB4XDTI2MTAyMzAwNTEyN1oXDTI2\nMTAyNTAwNTEyN1owGjEYMBYGA1UEAwwPZWMuZXhhbXBsZS50ZXN0MFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAE91VvPzxvJu7z28yu3162tL9f1pYoEAEsCbb0dYdG\nUGcUv3zuHz7fhcfqvuzuDnV2Jh8Y8qp+MtGlWxh8fOhYzKMyMDAwHQYDVR0OBBYE\nFAaHxuLK5SXnLdzXW7X0SclrQrvvMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0E\nAwIDSAAwRQIhAOZXfipKh5gsRprMHc/SO3TcjNqBBUSKHwOxr2nqpa1UAiB3ZSBJ\nuNDgK4eNIasqXlmgXmC8M3MD7LdTk0qwUc+fPQ==\n-----END CERTIFICATE-----\n", + keyPem = + "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg3MCUdaaqmJh19iLT\ny72VTuXH0U44yXi/D5VN89PXA6WhRANCAAT3VW8/PG8m7vPbzK7fXra0v1/WligQ\nASwJtvR1h0ZQZxS/fO4fPt+Fx+q+7O4OdXYmHxjyqn4y0aVbGHx86FjM\n-----END PRIVATE KEY-----\n"; + const cert = new c.X509Certificate(pem), + raw = new Uint8Array(cert.raw); + raw[raw.length - 1] ^= 1; + const damaged = new c.X509Certificate(raw); + const key = c.createPrivateKey(keyPem); + return { + legacy: cert.toLegacyObject(), + publicKey: cert.publicKey.export({ format: "pem", type: "spki" }), + usage: cert.keyUsage ?? null, + subject: cert.subject, + issued: cert.checkIssued(cert), + private: cert.checkPrivateKey(key), + verified: cert.verify(cert.publicKey), + imported: cert.verify(c.createPublicKey(key)), + damaged: damaged.verify(cert.publicKey), + cached: cert.toLegacyObject() === cert.toLegacyObject(), + }; + }, + x509Names: function ({ crypto: c }) { + const cert = new c.X509Certificate( + "-----BEGIN CERTIFICATE-----\nMIICdjCCAhygAwIBAgIUQsn9ZMGrGViSPljhZDsSPq54huQwCgYIKoZIzj0EAwIw\nODEbMBkGA1UEAwwSY2Fmw6kuZXhhbXBsZS50ZXN0MRkwFwYDVQQKDBBXb3JrZXJz\nICsgTmF0aXZlMB4XDTI2MTAyMzAxMDEzOVoXDTI2MTAyNTAxMDEzOVowODEbMBkG\nA1UEAwwSY2Fmw6kuZXhhbXBsZS50ZXN0MRkwFwYDVQQKDBBXb3JrZXJzICsgTmF0\naXZlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBT9EJkWZClxIzvyp7SNufuJF\nEAYD8CzONWw47BsuaZCJElS8JEbVTNC5aWNy45S7IIRj6d4PS/GU6DT1M1sLoaOC\nAQIwgf8wCQYDVR0TBAIwADCBsQYDVR0RBIGpMIGmghJ2YWxpZC5leGFtcGxlLnRl\nc3SCF2NvbW1hLG5hbWUuZXhhbXBsZS50ZXN0hhhodHRwczovL2V4YW1wbGUudGVz\ndC9hLGKHECABDbgAAAAAAAAAAAAAAAGBEXRlc3RAZXhhbXBsZS50ZXN0oCEGCisG\nAQQBgjcUAgOgEwwRdXNlckBleGFtcGxlLnRlc3SkFTATMREwDwYDVQQDDAhEaXIs\nTmFtZTA+BggrBgEFBQcBAQQyMDAwLgYIKwYBBQUHMAGGImh0dHBzOi8vb2NzcC5l\neGFtcGxlLnRlc3QvdmFsaWRhdGUwCgYIKoZIzj0EAwIDSAAwRQIgfppXDfKn85vj\nDm1QS9agAk9W5P8Md841oJ+8nVaAg4ECIQDeXmgVDWiRqusAivE68OEKJB5arucO\nyLzk3hNzHXSQKg==\n-----END CERTIFICATE-----\n", + ); + const raw = new Uint8Array(cert.raw), + marker = new TextEncoder().encode("Workers + Native"); + for (let i = 0; i <= raw.length - marker.length; i++) + if (marker.every((byte, j) => raw[i + j] === byte)) raw[i] = 0; + const nulNames = new c.X509Certificate(raw).toLegacyObject(); + const attempt = (fn) => { + try { + return fn() ?? null; + } catch (e) { + return { name: e.name, message: e.message, code: e.code }; + } + }; + return { + nulNames, + subject: cert.subject, + san: cert.subjectAltName, + info: cert.infoAccess, + ca: cert.ca, + legacy: cert.toLegacyObject(), + ip: cert.checkIP("2001:db8::1"), + names: [{}, { subject: "always" }, { subject: "never" }].map((o) => + attempt(() => cert.checkHost("café.example.test", o)), + ), + invalid: [ + () => cert.checkEmail("a\0b"), + () => cert.checkIP("127.0.0.1\0evil"), + () => cert.checkHost("valid.example.test", { partialWildcards: 1 }), + () => + cert.checkHost("valid.example.test", { singleLabelSubdomains: 1 }), + ].map(attempt), + }; + }, + certificates: function ({ crypto: c }) { + const signatures = [ + "MIIBVDCBvjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzEBFZlRokWwqjZlV3uIZCb4XHlCRHi11xrIKzd3+zjmeqJdb3UMkZCQH52zU7DO+mfYwD1+pB9QHh0NsLlkUqkc+Ko5k2KHs0EuWL4NoOMf++wiRD39b2XpEyBdI7NYlsfZHhrXihsyVZsNrfXlWv6+gqGvFjh4nXEzZ2+kxfvECAwEAARYacG9ydGFibGUtd29ya2Vycy1jaGFsbGVuZ2UwDQYJKoZIhvcNAQEEBQADgYEAdBV72QBrvJdRpeG7kVjGpPlW8BRVVBO7arGOQjYs6q7W03dkMPj4uzVyGYZLj59eScSYGA7PnyiOGwy5loRIMVPciLK3YKz74auiRz+XUHFJysOK28Y6QPuRr48CRFu8H7e081ZOHkeyhcwRhd7wjEI4ngHoJ3dhqMy0syA5Vi0=", + "MIIBVDCBvjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzEBFZlRokWwqjZlV3uIZCb4XHlCRHi11xrIKzd3+zjmeqJdb3UMkZCQH52zU7DO+mfYwD1+pB9QHh0NsLlkUqkc+Ko5k2KHs0EuWL4NoOMf++wiRD39b2XpEyBdI7NYlsfZHhrXihsyVZsNrfXlWv6+gqGvFjh4nXEzZ2+kxfvECAwEAARYacG9ydGFibGUtd29ya2Vycy1jaGFsbGVuZ2UwDQYJKoZIhvcNAQELBQADgYEAyzRKqH1NI8+WT1sXzMIDzxnPl+GiD9aEsGjRPMZek+NYOxb63DpXBaZ4PcAc7no1nymhoysWxibrj/gJjG2hOUTRkzNamyF4vdRij5FsAtyWMUaV0uDZFkuasiL3R/Q/oh5XEP+uWqlysfeGG+z/JKoWU5nL/Ac2kT4evqx5pMU=", + ]; + const api = c.Certificate; + const instance = new api(), + called = api(); + const describe = (value) => ({ + verified: api.verifySpkac(value), + publicKey: api.exportPublicKey(value).toString("utf8"), + challenge: api.exportChallenge(value).toString("hex"), + }); + const cases = signatures.flatMap((value) => { + const data = new TextEncoder().encode(value), + padded = new Uint8Array(data.length + 4); + padded.set(data, 2); + const view = new DataView(padded.buffer, 2, data.length); + const damaged = value.slice(0, -8) + "AAAAAAAA"; + return [ + describe(value), + describe(data.buffer), + describe(view), + describe(value + " \n\r\t"), + describe(damaged), + { + hex: api + .exportChallenge( + Array.from(data, (b) => b.toString(16).padStart(2, "0")).join(""), + "hex", + ) + .toString("hex"), + legacy: instance + .exportPublicKey(data) + .equals(api.exportPublicKey(data)), + called: called + .exportChallenge(data) + .equals(api.exportChallenge(data)), + bufferEncoding: api.exportChallenge(value, "buffer").toString("hex"), + }, + ]; + }); + const invalid = ["", " \n\t", "not-base64", "SPKAC=" + signatures[0]].map( + describe, + ); + const errors = []; + for (const name of ["verifySpkac", "exportPublicKey", "exportChallenge"]) + for (const value of [undefined, null, 42, {}]) { + try { + api[name](value); + errors.push(null); + } catch (e) { + errors.push({ name: e.name, code: e.code, message: e.message }); + } + } + return { + cases, + invalid, + errors, + instance: instance instanceof api, + called: called instanceof api, + methods: ["verifySpkac", "exportPublicKey", "exportChallenge"].map( + (n) => instance[n] === api[n], + ), + }; + }, + + primeNumbers: async function ({ crypto }) { + const capture = (fn) => { + try { + return { value: fn() ?? null }; + } catch (e) { + return { name: e.name, code: e.code ?? null, message: e.message }; + } + }; + const candidates = [ + 0n, + 1n, + 2n, + 3n, + 4n, + 17n, + 561n, + 1105n, + 18446744073709551557n, + ]; + const checked = candidates.map((value) => crypto.checkPrimeSync(value)); + const safe = crypto.generatePrimeSync(32, { safe: true, bigint: true }); + const constrained = crypto.generatePrimeSync(32, { + add: 12n, + rem: 11n, + bigint: true, + }); + const buffer = crypto.generatePrimeSync(32); + const log = []; + const async = await new Promise((resolve, reject) => { + crypto.checkPrime(17n, (error, value) => { + log.push("callback"); + error ? reject(error) : resolve(value); + }); + log.push("after"); + }); + const generated = await new Promise((resolve, reject) => + crypto.generatePrime(16, { bigint: true }, (error, value) => + error ? reject(error) : resolve(crypto.checkPrimeSync(value)), + ), + ); + const errors = [ + () => crypto.checkPrimeSync(-1n), + () => crypto.checkPrimeSync("17"), + () => crypto.checkPrimeSync(17n, { checks: 65 }), + () => crypto.checkPrimeSync(17n, { checks: -1 }), + () => crypto.checkPrimeSync(new Uint8Array(1025)), + () => crypto.generatePrimeSync(0), + () => crypto.generatePrimeSync(8193), + () => crypto.generatePrimeSync(16, { safe: 1 }), + () => crypto.generatePrimeSync(16, { add: 4n, rem: 3n }), + () => crypto.generatePrimeSync(2, { add: 12n, rem: 11n }), + ].map(capture); + return { + checked, + safe: + crypto.checkPrimeSync(safe) && crypto.checkPrimeSync((safe - 1n) / 2n), + safeBits: safe.toString(2).length, + remainder: String(constrained % 12n), + constrainedBits: constrained.toString(2).length, + buffer: { + brand: buffer instanceof ArrayBuffer, + bytes: buffer.byteLength, + prime: crypto.checkPrimeSync(buffer), + }, + async, + generated, + log, + errors, + }; + }, + asymmetricKeys: async function ({ crypto }) { + // Disposable fixture keys generated by independent Node crypto, never credentials. + const fixtures = { + rsa: { + privateKey: + "-----BEGIN PRIVATE KEY-----\nMIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAKmqearRpQ+76PnZ\nQ8r2aFHbGae4GwWqLQDF15XxwtcDUUTytnIBmpxw8I5cs9YezIh2hZc/lYcW9Gmb\nXBTOaHtDgkR+hXxq1o0awFarEJMFW80w3j+68/syK11IvRhsbcGBQvw15SGoeaHQ\nhvWSN+IiTDKQYeJ328e7yjFh3Ct5AgMBAAECgYBhwVdYlHIGHr6dNhFZX5+7igG+\nINVw7hv/1f3g5urb941brHxyM3phqLDxVXhIZnZyKdfOzVPaAd/Q+TKUylhnvJQz\nf3b2YBdtOz4AIsMPWnTilDRi1Kwu5mvcF5gzlbBsleJMTjRRDWwG1Sxg69Yze/Rh\nnZ8kJTEye7nMaZPAAQJBANo0MndLMjNLLpin925wiDmLO5Vct6GnPGY1gfNbzVXl\n6uUs4oc5XolZDjsW97bYsn+Rip52hX5rqkeNYBncOJECQQDHDfYgGx48qFmb1gRU\nd8ikvDIWk9DurQvnjGcGfWvAzvtyw9ybk0CBpFghrF7izutj+j0VfnFWlNX9e612\nNXhpAkAy1nhvjMh8ZObNUEz4oSVL6uGN8hUih5kt4weM0HWtrqykimmYwcgBzgrf\nv4ElS6WCzUDznH+Lrb/1fb3HKKTRAkAc+QBBDhFpnMtmLlF1BfZTW/0iYiOACfn4\nmuwH+strRfU9cBq1Hkq+U/Br/k3VT27dn3eyoDPY+VLoUU8fLIs5AkAl7rPmRk0W\naVESr8259cVOXJZNfff+NDbTDO88/et+bARWUPPFopAOQhbRaHzoQN05WsAnDSl0\n3R6Dwk53bF0r\n-----END PRIVATE KEY-----\n", + publicKey: + "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCpqnmq0aUPu+j52UPK9mhR2xmn\nuBsFqi0AxdeV8cLXA1FE8rZyAZqccPCOXLPWHsyIdoWXP5WHFvRpm1wUzmh7Q4JE\nfoV8ataNGsBWqxCTBVvNMN4/uvP7MitdSL0YbG3BgUL8NeUhqHmh0Ib1kjfiIkwy\nkGHid9vHu8oxYdwreQIDAQAB\n-----END PUBLIC KEY-----\n", + jwk: { + kty: "RSA", + n: "qap5qtGlD7vo-dlDyvZoUdsZp7gbBaotAMXXlfHC1wNRRPK2cgGanHDwjlyz1h7MiHaFlz-Vhxb0aZtcFM5oe0OCRH6FfGrWjRrAVqsQkwVbzTDeP7rz-zIrXUi9GGxtwYFC_DXlIah5odCG9ZI34iJMMpBh4nfbx7vKMWHcK3k", + e: "AQAB", + d: "YcFXWJRyBh6-nTYRWV-fu4oBviDVcO4b_9X94Obq2_eNW6x8cjN6Yaiw8VV4SGZ2cinXzs1T2gHf0PkylMpYZ7yUM3929mAXbTs-ACLDD1p04pQ0YtSsLuZr3BeYM5WwbJXiTE40UQ1sBtUsYOvWM3v0YZ2fJCUxMnu5zGmTwAE", + p: "2jQyd0syM0sumKf3bnCIOYs7lVy3oac8ZjWB81vNVeXq5SzihzleiVkOOxb3ttiyf5GKnnaFfmuqR41gGdw4kQ", + q: "xw32IBsePKhZm9YEVHfIpLwyFpPQ7q0L54xnBn1rwM77csPcm5NAgaRYIaxe4s7rY_o9FX5xVpTV_XutdjV4aQ", + dp: "MtZ4b4zIfGTmzVBM-KElS-rhjfIVIoeZLeMHjNB1ra6spIppmMHIAc4K37-BJUulgs1A85x_i62_9X29xyik0Q", + dq: "HPkAQQ4RaZzLZi5RdQX2U1v9ImIjgAn5-JrsB_rLa0X1PXAatR5KvlPwa_5N1U9u3Z93sqAz2PlS6FFPHyyLOQ", + qi: "Je6z5kZNFmlREq_NufXFTlyWTX33_jQ20wzvPP3rfmwEVlDzxaKQDkIW0Wh86EDdOVrAJw0pdN0eg8JOd2xdKw", + }, + signature: + "VZCB1w22hBa1DrzurqCnmUdaMXCFMWFCPmOg2IKfQGW48EsZCod4GpE07mJATLLjFVgdTppygeOUqvQK85/SD6jo3HNA270/ZANk4exXpj72lQziYslAY7wERfO9xozZwzAd2Mwg/XrUtlT4flGodizI0wCmGsOZ9ToqcSSjFKM=", + }, + ec: { + privateKey: + "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkzAWEEq6Qf3Q3Zj8\njZzVPzjXi9v1UE4fUvSPCYDzIQ+hRANCAATUyxPEFq8wbhMtrw73FeZ4pAGrhRIV\n33P91jSkmz3i4hqV4Q4p8i4jxCmRxXjS8pmjSzVVtV1haS6lqKB3PqTW\n-----END PRIVATE KEY-----\n", + publicKey: + "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1MsTxBavMG4TLa8O9xXmeKQBq4US\nFd9z/dY0pJs94uIaleEOKfIuI8QpkcV40vKZo0s1VbVdYWkupaigdz6k1g==\n-----END PUBLIC KEY-----\n", + jwk: { + kty: "EC", + x: "1MsTxBavMG4TLa8O9xXmeKQBq4USFd9z_dY0pJs94uI", + y: "GpXhDinyLiPEKZHFeNLymaNLNVW1XWFpLqWooHc-pNY", + crv: "P-256", + d: "kzAWEEq6Qf3Q3Zj8jZzVPzjXi9v1UE4fUvSPCYDzIQ8", + }, + signature: + "MEYCIQD6Z6h2C9Ph77WTRPSzZiegB6hwdYGLY6piOyWM8h9iAQIhALPJTymfz9UQrYqJTuwvuFk3iM0laFYjEcueJYXPX67X", + }, + ed25519: { + privateKey: + "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIOzFGVeuGidC3g/gfu6BLZHV0nkI1cOdcgJSZndBdLk5\n-----END PRIVATE KEY-----\n", + publicKey: + "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAZ9S/SKhncpbOeH+ppPVjBQnZl6SBN+2UCWsZPmqVc7U=\n-----END PUBLIC KEY-----\n", + jwk: { + crv: "Ed25519", + d: "7MUZV64aJ0LeD-B-7oEtkdXSeQjVw51yAlJmd0F0uTk", + x: "Z9S_SKhncpbOeH-ppPVjBQnZl6SBN-2UCWsZPmqVc7U", + kty: "OKP", + }, + signature: + "2Atk1x63ifZkxT1hOgEN32mT3U4Toiy3+659GbAgKFsMZi6Z2j0ZKTk8DsnXCllDzGB0LJKxnxnQEp0I/tAcCQ==", + }, + x25519: { + privateKey: + "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VuBCIEIFgmCj/A/WL5EEgvvjyCUV0/+K/sxR62Z6WlK/Vz1ydG\n-----END PRIVATE KEY-----\n", + publicKey: + "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VuAyEACs62/8koQj99zkBorRDHAvE3uptEtZIjbmL6ogtsRwU=\n-----END PUBLIC KEY-----\n", + jwk: { + crv: "X25519", + d: "WCYKP8D9YvkQSC--PIJRXT_4r-zFHrZnpaUr9XPXJ0Y", + x: "Cs62_8koQj99zkBorRDHAvE3uptEtZIjbmL6ogtsRwU", + kty: "OKP", + }, + signature: null, + peer: "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VuAyEA3lvtx86rEZf58Uf9X9OIEYtpWglahuEwoK3lzb9kxC0=\n-----END PUBLIC KEY-----\n", + secret: + "c47b7be86dc7effc5514183876c5377825417f783b1ae4b8f5edd6afcf0c4922", + }, + }; + const decode = (value) => + Uint8Array.from(atob(value), (c) => c.charCodeAt(0)); + const data = new TextEncoder().encode("Workers portable key"); + const result = {}; + for (const [type, fixture] of Object.entries(fixtures)) { + const privateKey = crypto.createPrivateKey(fixture.privateKey); + const publicKey = crypto.createPublicKey(privateKey); + const imported = crypto.createPublicKey(fixture.publicKey); + const jwk = crypto.createPrivateKey({ key: fixture.jwk, format: "jwk" }); + const der = privateKey.export({ format: "der", type: "pkcs8" }); + const roundtrip = crypto.createPrivateKey({ + key: der, + format: "der", + type: "pkcs8", + }); + const details = privateKey.asymmetricKeyDetails; + const entry = { + type: privateKey.type, + algorithm: privateKey.asymmetricKeyType, + details: JSON.parse( + JSON.stringify(details, (_, value) => + typeof value === "bigint" ? value.toString() : value, + ), + ), + publicPem: publicKey.export({ format: "pem", type: "spki" }), + publicEqual: publicKey.equals(imported), + jwkEqual: privateKey.equals(jwk), + derEqual: privateKey.equals(roundtrip), + publicJwk: publicKey.export({ format: "jwk" }), + privateJwk: privateKey.export({ format: "jwk" }), + }; + entry.dhValidation = [ + {}, + { publicKey }, + { publicKey: {}, privateKey }, + { publicKey: privateKey, privateKey }, + { publicKey, privateKey: publicKey }, + { publicKey: fixture.publicKey, privateKey }, + { publicKey, privateKey: fixture.privateKey }, + { + publicKey, + privateKey: crypto.createPrivateKey( + fixtures[type === "rsa" ? "ec" : "rsa"].privateKey, + ), + }, + ].map((options) => { + try { + crypto.diffieHellman(options); + return null; + } catch (e) { + return { name: e.name, code: e.code ?? null }; + } + }); + if (type === "x25519") { + try { + entry.secret = crypto + .diffieHellman({ + privateKey, + publicKey: crypto.createPublicKey(fixture.peer), + }) + .toString("hex"); + } catch (e) { + entry.error = { name: e.name, message: e.message }; + } + } else { + const algorithm = type === "ed25519" ? null : "sha256"; + const signature = crypto.sign(algorithm, data, privateKey); + entry.verifies = crypto.verify(algorithm, data, publicKey, signature); + entry.independent = crypto.verify( + algorithm, + data, + publicKey, + decode(fixture.signature), + ); + entry.tampered = crypto.verify( + algorithm, + new Uint8Array([0]), + publicKey, + signature, + ); + if (type !== "ed25519") { + const signer = crypto.createSign("sha256"); + signer.update(data.subarray(0, 3)); + signer.update(data.subarray(3)); + const streamed = signer.sign(privateKey); + const verifier = crypto.createVerify("sha256"); + verifier.update(data.subarray(0, 5)); + verifier.update(data.subarray(5)); + entry.streaming = { + valid: verifier.verify(publicKey, streamed), + independent: crypto + .createVerify("sha256") + .update(data) + .verify(publicKey, decode(fixture.signature)), + cross: crypto.verify("sha256", data, publicKey, streamed), + constructor: signer instanceof crypto.Sign, + }; + if (type === "rsa") + entry.streaming.signature = streamed.toString("base64"); + const failure = (run) => { + try { + run(); + return null; + } catch (error) { + return { + name: error.name, + message: error.message, + code: error.code ?? null, + }; + } + }; + entry.streaming.errors = [ + failure(() => signer.update(data)), + failure(() => signer.sign(privateKey)), + failure(() => verifier.update(data)), + failure(() => verifier.verify(publicKey, streamed)), + failure(() => crypto.createSign("not-a-digest")), + failure(() => crypto.createVerify("not-a-digest")), + ]; + const encoded = crypto + .createSign("sha256") + .update(data) + .sign({ + key: fixture.privateKey, + ...(type === "ec" + ? { dsaEncoding: "ieee-p1363" } + : { padding: 6, saltLength: 0 }), + }); + entry.streaming.options = crypto + .createVerify("sha256") + .update(data) + .verify( + { + key: fixture.publicKey, + ...(type === "ec" + ? { dsaEncoding: "ieee-p1363" } + : { padding: 6, saltLength: 0 }), + }, + encoded, + ); + } + if (type === "ec") { + const encoded = crypto.sign("sha256", data, { + key: fixture.privateKey, + dsaEncoding: "ieee-p1363", + }); + entry.p1363 = { + length: encoded.length, + valid: crypto.verify( + "sha256", + data, + { key: fixture.publicKey, dsaEncoding: "ieee-p1363" }, + encoded, + ), + }; + } + if (type === "rsa") { + entry.rsaComposition = [false, true].map((wrapped) => { + try { + const pub = wrapped ? { key: publicKey } : publicKey; + const priv = wrapped ? { key: privateKey } : privateKey; + const encrypted = crypto.publicEncrypt(pub, data); + const signed = crypto.privateEncrypt(priv, data); + return [ + crypto.privateDecrypt(priv, encrypted).toString("hex"), + crypto.publicDecrypt(pub, signed).toString("hex"), + ]; + } catch (e) { + return { name: e.name, code: e.code ?? null }; + } + }); + entry.rsaOptions = [ + { oaepLabel: new Uint8Array([1]) }, + { padding: 4, oaepLabel: new Uint8Array([1]), oaepHash: "sha256" }, + { padding: 4, oaepLabel: new Uint8Array([1]), oaepHash: "sha1" }, + { oaepLabel: 42 }, + ].map((options) => { + try { + const encrypted = crypto.publicEncrypt( + { key: fixture.publicKey, ...options }, + data, + ); + const decoded = crypto.privateDecrypt( + { key: fixture.privateKey, ...options }, + encrypted, + ); + let mismatch; + try { + crypto.privateDecrypt( + { + key: fixture.privateKey, + ...options, + oaepLabel: new Uint8Array([2]), + }, + encrypted, + ); + mismatch = null; + } catch (e) { + mismatch = { name: e.name, message: e.message }; + } + return { decoded: decoded.toString("hex"), mismatch }; + } catch (e) { + return { name: e.name, code: e.code ?? null, message: e.message }; + } + }); + const signed = crypto.sign("sha256", data, { + key: fixture.privateKey, + padding: 6, + saltLength: 0, + }); + entry.pss = crypto.verify( + "sha256", + data, + { key: fixture.publicKey, padding: 6, saltLength: 0 }, + signed, + ); + const passphrase = new Uint8Array([112, 0, 97]); + entry.encrypted = ["pem", "der"].map((format) => { + const encrypted = privateKey.export({ + type: "pkcs8", + format, + cipher: "aes-256-cbc", + passphrase, + }); + return crypto + .createPrivateKey({ + key: encrypted, + type: "pkcs8", + format, + passphrase, + }) + .equals(privateKey); + }); + } + + if (type !== "ec") entry.signature = signature.toString("base64"); + entry.async = await new Promise((resolve, reject) => + crypto.sign(algorithm, data, privateKey, (error, signed) => + error + ? reject(error) + : crypto.verify( + algorithm, + data, + publicKey, + signed, + (err, valid) => (err ? reject(err) : resolve(valid)), + ), + ), + ); + } + result[type] = entry; + } + result.timing = [ + crypto.timingSafeEqual(data, new Uint8Array(data)), + crypto.timingSafeEqual(new Uint8Array([1]), new Uint8Array([2])), + ]; + return result; + }, + asymmetricGeneration: function ({ crypto }) { + return ["rsa", "ec", "ed25519", "x25519"].map((type) => { + const { privateKey, publicKey } = crypto.generateKeyPairSync( + type, + type === "rsa" + ? { modulusLength: 1024 } + : type === "ec" + ? { namedCurve: "prime256v1" } + : {}, + ); + const exported = privateKey.export({ format: "pem", type: "pkcs8" }); + return { + type, + privateType: privateKey.type, + publicType: publicKey.type, + equal: crypto.createPrivateKey(exported).equals(privateKey), + publicEqual: crypto.createPublicKey(privateKey).equals(publicKey), + }; + }); + }, + + processClock: function ({ process }) { + const now = Date.now; + try { + Date.now = () => 1234567; + const invalid = [null, 12, [], [1], [1, 2, 3]].map((value) => { + try { + return process.hrtime(value); + } catch (e) { + return { name: e.name, code: e.code, message: e.message }; + } + }); + return { + absolute: process.hrtime(), + bigint: String(process.hrtime.bigint()), + delta: process.hrtime([1233, 900000000]), + invalid, + }; + } finally { + Date.now = now; + } + }, + processStubs: function ({ process }) { + const call = (name, args = []) => { + try { + return { value: process[name](...args) ?? null }; + } catch (e) { + return { + name: e.name, + code: e.code ?? null, + message: e.message, + syscall: e.syscall ?? null, + errno: e.errno ?? null, + }; + } + }; + const simple = [ + "ref", + "unref", + "setSourceMapsEnabled", + "getSourceMapsSupport", + "setUncaughtExceptionCaptureCallback", + "hasUncaughtExceptionCaptureCallback", + "kill", + "binding", + "dlopen", + "getActiveResourcesInfo", + "memoryUsage", + "resourceUsage", + "threadCpuUsage", + "cpuUsage", + "_linkedBinding", + "_getActiveRequests", + "_getActiveHandles", + "_kill", + "constrainedMemory", + "availableMemory", + "execve", + "_fatalException", + "_tickCallback", + "_debugProcess", + "_debugEnd", + "_startProfilerIdleNotifier", + "_stopProfilerIdleNotifier", + "send", + "uptime", + "getegid", + "getgid", + "geteuid", + "getuid", + "getgroups", + ].map((name) => [name, call(name)]); + const permissions = [ + "setegid", + "setgid", + "seteuid", + "setuid", + "setgroups", + "initgroups", + ].map((name) => [ + name, + [0, 1, "0", null, undefined].map((value) => call(name, [value])), + ]); + const nested = [ + "permission.has", + "report.getReport", + "report.writeReport", + "finalization.register", + "finalization.registerBeforeExit", + "finalization.unregister", + ].map((path) => { + const [a, b] = path.split("."); + try { + return [path, { value: process[a][b]() ?? null }]; + } catch (e) { + return [ + path, + { name: e.name, code: e.code ?? null, message: e.message }, + ]; + } + }); + const properties = Object.fromEntries( + [ + "arch", + "pid", + "ppid", + "exitCode", + "channel", + "connected", + "debugPort", + "noDeprecation", + "traceDeprecation", + "throwDeprecation", + "sourceMapsEnabled", + "execPath", + "release", + "moduleLoadList", + "_preload_modules", + "domain", + "_exiting", + ].map((name) => [name, process[name] ?? null]), + ); + return { + simple, + permissions, + nested, + properties, + flags: Array.from(process.allowedNodeEnvironmentFlags), + }; + }, + tlsHelpers: function ({ tls }) { + const capture = (fn) => { + try { + const result = fn(); + return result === undefined ? null : result; + } catch (e) { + return { name: e.name, code: e.code ?? null, message: e.message }; + } + }; + const certificates = [ + ["api.example.com", { subjectaltname: "DNS:*.example.com" }], + ["nested.api.example.com", { subjectaltname: "DNS:*.example.com" }], + ["example.com", { subject: { CN: "example.com" } }], + ["EXAMPLE.COM.", { subjectaltname: "DNS:example.com" }], + [ + "example.com", + { subjectaltname: "DNS:other.com", subject: { CN: "example.com" } }, + ], + ["127.0.0.1", { subjectaltname: "IP Address:127.0.0.1" }], + ["2001:db8::1", { subjectaltname: "IP Address:2001:0db8:0:0:0:0:0:1" }], + ["::ffff:192.0.2.1", { subjectaltname: "IP Address:::ffff:c000:201" }], + ["example.com", { subjectaltname: 'DNS:"bad' }], + ["example.com", {}], + ].map(([host, cert]) => + capture(() => { + const e = tls.checkServerIdentity(host, cert); + return e + ? { + name: e.name, + code: e.code, + message: e.message, + reason: e.reason, + host: e.host, + sameCert: e.cert === cert, + } + : null; + }), + ); + const protocols = [ + ["h2", "http/1.1"], + ["é"], + new Uint8Array([2, 104, 50]), + new DataView(new Uint8Array([0, 2, 104, 50, 0]).buffer, 1, 3), + ["x".repeat(256)], + null, + ].map((value) => + capture(() => { + const out = {}; + tls.convertALPNProtocols(value, out); + return out.ALPNProtocols ? Array.from(out.ALPNProtocols) : null; + }), + ); + const unsupported = [ + tls.createServer, + tls.Server, + tls.getCiphers, + tls.createSecurePair, + ].map((fn) => capture(() => fn())); + const validations = [ + { enableTrace: true }, + { isServer: true }, + { server: {} }, + { requestCert: true }, + { rejectUnauthorized: false }, + { ALPNProtocols: [] }, + { SNICallback: () => {} }, + { requestOCSP: true }, + { secureContext: {} }, + { pskCallback: () => {} }, + { checkServerIdentity: 123 }, + { checkServerIdentity: () => {} }, + ].map((opts) => + capture(() => { + const socket = new tls.TLSSocket(undefined, opts); + socket.destroy(); + return null; + }), + ); + const ctx = tls.createSecureContext(); + return { + certificates, + protocols, + unsupported, + validations, + context: ctx instanceof tls.SecureContext, + contextValue: ctx.context ?? null, + constants: [ + tls.CLIENT_RENEG_LIMIT, + tls.CLIENT_RENEG_WINDOW, + tls.DEFAULT_CIPHERS, + tls.DEFAULT_ECDH_CURVE, + tls.DEFAULT_MIN_VERSION, + tls.DEFAULT_MAX_VERSION, + tls.rootCertificates, + ], + }; + }, + aliases: function ({ sys, util, types }) { + return { + same: sys.default === util.default, + format: sys.format("%s:%d", "value", 3), + typesSame: types.default === util.types, + predicates: [ + types.isMap(new Map()), + types.isMap({}), + types.isUint8Array(new Uint8Array()), + types.isDataView(new DataView(new ArrayBuffer(1))), + types.isDate(new Date(0)), + types.isBoxedPrimitive(Object(1n)), + types.isArrayBufferView(new Uint16Array()), + types.isArrayBuffer(new ArrayBuffer(1)), + ], + }; + }, + consoleStubs: function ({ console: api }) { + const errors = []; + for (const name of ["Console", "context", "createTask"]) { + try { + api[name](); + errors.push(null); + } catch (e) { + errors.push({ name: e.name, code: e.code, message: e.message }); + } + } + return { + errors, + defaultIdentity: api.default === globalThis.console, + prototypes: [ + "log", + "warn", + "error", + "table", + "context", + "createTask", + ].map((n) => api.Console.prototype[n] === api[n]), + }; + }, + cryptoMetadata: function ({ crypto: c }) { + const errors = {}; + for (const name of ["setEngine", "setFips"]) { + try { + c[name](); + } catch (error) { + errors[name] = { + name: error.name, + code: error.code, + message: error.message, + }; + } + } + const curves = c.getCurves(); + const exchanges = curves.map((curve) => { + const alice = c.createECDH(curve), + bob = c.createECDH(curve); + alice.setPrivateKey( + c.createHash("sha256").update("fixture alice").digest().subarray(0, 16), + ); + bob.setPrivateKey( + c.createHash("sha256").update("fixture bob").digest().subarray(0, 16), + ); + const a = alice.getPublicKey(), + b = bob.getPublicKey(); + const left = alice.computeSecret(b), + right = bob.computeSecret(a); + const converted = ["compressed", "uncompressed", "hybrid"].map( + (format) => { + try { + const encoded = c.ECDH.convertKey( + a, + curve, + undefined, + undefined, + format, + ); + return [ + encoded.toString("hex"), + bob.computeSecret(encoded).equals(right), + ]; + } catch (e) { + return { name: e.name, message: e.message }; + } + }, + ); + const copy = new c.ECDH(curve); + const privateBytes = alice.getPrivateKey(); + const setResult = copy.setPrivateKey( + new DataView( + privateBytes.buffer, + privateBytes.byteOffset, + privateBytes.byteLength, + ), + ); + const generated = c.ECDH(curve); + generated.generateKeys(); + const failures = []; + for (const run of [ + () => new c.ECDH("not-a-curve"), + () => new c.ECDH(curve).getPrivateKey(), + () => new c.ECDH(curve).getPublicKey(), + () => new c.ECDH(curve).computeSecret(a), + () => copy.setPrivateKey(new Uint8Array([0])), + () => copy.computeSecret(new Uint8Array([1])), + () => c.ECDH.convertKey(new Uint8Array([1]), curve), + ]) { + try { + run(); + failures.push(null); + } catch (e) { + failures.push({ name: e.name, message: e.message }); + } + } + return { + curve, + converted, + failures, + instance: copy instanceof c.ECDH, + setReturnsUndefined: setResult === undefined, + copied: copy.getPublicKey().equals(a), + generated: generated + .computeSecret(a) + .equals(alice.computeSecret(generated.getPublicKey())), + empty: c.ECDH.convertKey(new Uint8Array(), "not-a-curve").length, + bytes: left.length, + equal: left.equals(right), + secret: left.toString("hex"), + publicKey: a.toString("hex"), + }; + }); + return { curves, exchanges, heap: c.secureHeapUsed(), errors }; + }, + secretKeys: async function ({ crypto: c, types }) { + const input = new Uint8Array([1, 2, 3, 4]); + const key = c.createSecretKey(input); + input.fill(0); + const raw = key.export(); + raw.fill(0); + const options = {}; + key.export(options); + const hmac = c.createHmac("sha256", key).update("fixture").digest("hex"); + const brands = []; + const streamResults = []; + const cipherKey = c.createSecretKey( + c.createHash("sha256").update("cipher key").digest(), + ); + const iv = c.createHash("sha256").update("iv").digest().subarray(0, 16); + const cipher = c.createCipheriv("aes-256-cbc", cipherKey, iv); + for (const value of [ + new c.Hmac("sha256", key), + c.createHmac("md5", key), + c.createCipheriv("aes-256-cbc", cipherKey, iv), + new c.Decipheriv("aes-256-cbc", cipherKey, iv), + ]) { + const Class = value.constructor; + brands.push([ + Class.name, + Object.getPrototypeOf(value) === Class.prototype, + value instanceof c.Hmac, + value instanceof c.Cipheriv, + value instanceof c.Decipheriv, + ]); + } + for (const algorithm of ["sha256", "sha384", "sha512"]) { + const instance = c.createHmac(algorithm, key); + streamResults.push( + await new Promise((resolve, reject) => { + instance.on("data", (data) => resolve(data.toString("hex"))); + instance.on("error", reject); + instance.write("fix"); + instance.end("ture"); + }), + ); + } + const streamCipher = c.createCipheriv("aes-256-cbc", cipherKey, iv); + const streamDecipher = c.createDecipheriv("aes-256-cbc", cipherKey, iv); + const streamed = new Promise((resolve, reject) => { + let text = ""; + streamDecipher.setEncoding("utf8"); + streamDecipher.on("data", (chunk) => { + text += chunk; + }); + streamDecipher.on("end", () => resolve(text)); + streamDecipher.on("error", reject); + streamCipher.on("error", reject); + streamCipher.pipe(streamDecipher); + }); + streamCipher.write("mes"); + streamCipher.end("sage"); + const encrypted = + cipher.update("message", "utf8", "hex") + cipher.final("hex"); + const generated = await new Promise((resolve, reject) => + c.generateKey("hmac", { length: 9 }, (error, value) => + error ? reject(error) : resolve(value.symmetricKeySize), + ), + ); + const constructors = []; + for (const Class of [ + c.KeyObject, + c.SecretKeyObject, + c.PublicKeyObject, + c.PrivateKeyObject, + ]) { + try { + new Class(); + constructors.push(null); + } catch (e) { + constructors.push({ name: e.name, message: e.message }); + } + } + return { + type: key.type, + size: key.symmetricKeySize, + constructor: key.constructor.name, + tag: Object.prototype.toString.call(key), + export: key.export().toString("hex"), + jwk: key.export({ format: "jwk" }), + equal: key.equals(c.createSecretKey(new Uint8Array([1, 2, 3, 4]))), + unequal: key.equals(c.createSecretKey("different")), + branded: types.isKeyObject(key), + fake: types.isKeyObject({}), + options, + hmac, + encodedHmac: c + .createHmac("sha256", "aa", { encoding: "hex" }) + .update("message") + .digest("hex"), + brands, + streamResults, + streamed: await streamed, + encrypted, + generated, + aes: c.generateKeySync("aes", { length: 192 }).symmetricKeySize, + constructors, + }; + }, + + cryptoRandom: async function ({ crypto: c }) { + const values = Array.from({ length: 200 }, () => c.randomInt(-3, 9)); + const asynchronous = await new Promise((resolve, reject) => + c.randomInt(1, (err, n) => (err ? reject(err) : resolve(n))), + ); + const errors = []; + for (const args of [[0], [3, 3], [1.5], ["x"], [0, 2 ** 48]]) { + try { + c.randomInt(...args); + errors.push(null); + } catch (e) { + errors.push({ name: e.name, code: e.code }); + } + } + return { + range: values.every((n) => Number.isInteger(n) && n >= -3 && n < 9), + asynchronous, + uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test( + c.randomUUID(), + ), + webcrypto: c.webcrypto === globalThis.crypto, + subtle: c.subtle === globalThis.crypto.subtle, + errors, + }; + }, + cryptoHashKdf: async function ({ crypto: c }) { + const hex = (value) => + Array.from(new Uint8Array(value)) + .map((n) => n.toString(16).padStart(2, "0")) + .join(""); + const result = { + hash: c.hash("sha256", "abc"), + bytes: hex(c.hash("sha256", new Uint8Array([97, 98, 99]), "buffer")), + hkdf: hex(c.hkdfSync("sha256", "key", "salt", "info", 42)), + empty: hex(c.hkdfSync("sha256", "", "", "", 0)), + }; + result.hkdfAsync = await new Promise((resolve, reject) => + c.hkdf("sha256", "key", "salt", "info", 42, (e, b) => + e ? reject(e) : resolve(hex(b)), + ), + ); + result.scrypt = hex( + c.scryptSync("password", "salt", 32, { N: 16, r: 1, p: 1 }), + ); + result.scryptAsync = await new Promise((resolve, reject) => + c.scrypt( + "password", + "salt", + 32, + { cost: 16, blockSize: 1, parallelization: 1 }, + (e, b) => (e ? reject(e) : resolve(hex(b))), + ), + ); + return result; + }, + cryptoInvalid: function ({ crypto: c }) { + const errors = []; + const calls = [ + () => c.hash(3, "x"), + () => c.hash("sha256", null), + () => c.hkdfSync("sha256", "a", "b", "c", -1), + () => c.hkdfSync("sha256", "a", "b", new Uint8Array(1025), 1), + () => c.scryptSync("a", "b", 16, { N: 3 }), + () => c.scryptSync("a", "b", 16, { N: 16, cost: 16 }), + () => c.scryptSync("a", "b", 16, { N: 16, r: 1, maxmem: 1 }), + () => c.randomUUID({ disableEntropyCache: 1 }), + ]; + for (const call of calls) { + try { + call(); + errors.push(null); + } catch (e) { + errors.push({ name: e.name, code: e.code }); + } + } + return errors; + }, + + fsWrites: function ({ fs }) { + const dir = "/tmp/cf-writes"; + fs.mkdirSync(dir, { recursive: true }); + try { + const wrote = fs.writeFileSync(dir + "/a", "hello"); + const appended = fs.appendFileSync(dir + "/a", " world"); + const fd = fs.openSync(dir + "/a", "r+"); + const bytes = new Uint8Array(5); + const read = fs.readSync(fd, bytes, 0, 5, 0); + const written = fs.writeSync(fd, new Uint8Array([72]), 0, 1, 0); + fs.ftruncateSync(fd, 5); + const size = fs.fstatSync(fd).size; + fs.closeSync(fd); + return { + wrote, + appended, + read, + written, + size, + data: fs.readFileSync(dir + "/a", "utf8"), + bytes: Array.from(bytes), + }; + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }, + fsDirectories: function ({ fs }) { + const dir = "/tmp/cf-directories"; + fs.mkdirSync(dir, { recursive: true }); + try { + fs.writeFileSync(dir + "/source", "value"); + fs.copyFileSync(dir + "/source", dir + "/copy"); + fs.renameSync(dir + "/copy", dir + "/renamed"); + fs.symlinkSync(dir + "/source", dir + "/link"); + return { + entries: fs.readdirSync(dir).sort(), + link: fs.lstatSync(dir + "/link").isSymbolicLink(), + target: fs.readlinkSync(dir + "/link"), + resolved: fs.realpathSync(dir + "/link"), + data: fs.readFileSync(dir + "/link", "utf8"), + size: fs.statSync(dir + "/renamed").size, + }; + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }, + fsPromises: async function ({ fs }) { + const p = fs.promises, + dir = "/tmp/cf-promises"; + await p.mkdir(dir, { recursive: true }); + try { + const handle = await p.open(dir + "/file", "w+"); + try { + await handle.writeFile("abc"); + const buffer = new Uint8Array(3); + const read = await handle.read(buffer, 0, 3, 0); + return { + read: read.bytesRead, + buffer: Array.from(buffer), + size: (await handle.stat()).size, + }; + } finally { + await handle.close(); + } + } finally { + await p.rm(dir, { recursive: true, force: true }); + } + }, + fsCallbacks: async function ({ fs }) { + const dir = "/tmp/cf-callbacks"; + fs.mkdirSync(dir, { recursive: true }); + try { + await new Promise((resolve, reject) => + fs.writeFile(dir + "/file", "abc", (error) => + error ? reject(error) : resolve(), + ), + ); + const data = await new Promise((resolve, reject) => + fs.readFile(dir + "/file", "utf8", (error, data) => + error ? reject(error) : resolve(data), + ), + ); + const errors = []; + for (const fn of [ + () => fs.watch(dir), + () => fs.watchFile(dir), + () => fs.unwatchFile(dir), + () => fs.readFileSync(dir + "/missing"), + ]) { + try { + fn(); + errors.push(null); + } catch (e) { + errors.push(e.code); + } + } + return { data, errors }; + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }, + brotli: async function ({ zlib: z }) { + const text = "hello world hello world hello world"; + const input = new TextEncoder().encode(text); + const compressed = z.brotliCompressSync(input, { + params: { [z.constants.BROTLI_PARAM_QUALITY]: 4 }, + }); + const decoded = new TextDecoder().decode( + z.brotliDecompressSync(compressed), + ); + const asyncDecoded = await new Promise((resolve, reject) => + z.brotliDecompress(compressed, (e, b) => + e ? reject(e) : resolve(new TextDecoder().decode(b)), + ), + ); + const stream = z.createBrotliCompress({ + params: { [z.constants.BROTLI_PARAM_QUALITY]: 4 }, + }); + const parts = []; + const done = new Promise((resolve, reject) => { + stream.on("data", (b) => parts.push(b)); + stream.on("error", reject); + stream.on("end", resolve); + }); + stream.write(input.subarray(0, 4)); + stream.end(input.subarray(4)); + await done; + const joined = new Uint8Array(parts.reduce((n, b) => n + b.length, 0)); + let offset = 0; + for (const b of parts) { + joined.set(b, offset); + offset += b.length; + } + return { + decoded, + asyncDecoded, + stream: new TextDecoder().decode(z.brotliDecompressSync(joined)), + crc: z.crc32("hello"), + incremental: z.crc32("lo", z.crc32("hel")), + }; + }, + + brotliErrors: function ({ zlib: z }) { + const outcomes = []; + const compressed = z.brotliCompressSync(new Uint8Array(100)); + for (const run of [ + () => z.brotliDecompressSync(new Uint8Array()), + () => z.brotliDecompressSync(new Uint8Array([255, 255, 255])), + () => z.brotliDecompressSync(compressed, { maxOutputLength: 10 }), + ]) { + try { + run(); + outcomes.push("success"); + } catch (error) { + outcomes.push({ + name: error.name, + message: error.message, + code: error.code, + }); + } + } + return outcomes; + }, + + codecProfiles: async function ({ zlib: z }) { + const input = new TextEncoder().encode("streaming fixture ".repeat(2000)); + const result = {}; + for (const name of ["brotli", "zstd"]) { + const prefix = name === "brotli" ? "Brotli" : "Zstd"; + const options = { + chunkSize: 256, + params: name === "brotli" ? { 0: 1, 1: 4, 2: 20 } : { 100: 5, 201: 1 }, + }; + const compressed = z[name + "CompressSync"](input, options); + const decoded = z[name + "DecompressSync"](compressed); + const info = z[name + "CompressSync"](input, { ...options, info: true }); + const stream = z["create" + prefix + "Compress"](options); + stream.reset(); + const parts = []; + stream.on("data", (chunk) => parts.push(chunk)); + const done = new Promise((resolve, reject) => { + stream.on("end", resolve); + stream.on("error", reject); + }); + stream.write(input.subarray(0, 17000)); + await new Promise((resolve, reject) => { + stream.once("error", reject); + stream.flush(resolve); + }); + const flushed = parts.length > 0; + stream.end(input.subarray(17000)); + await done; + const joined = new Uint8Array(parts.reduce((n, b) => n + b.length, 0)); + let offset = 0; + for (const part of parts) { + joined.set(part, offset); + offset += part.length; + } + const decoder = z["create" + prefix + "Decompress"]({ chunkSize: 128 }); + let decodedLength = 0, + decodedCrc = 0; + decoder.on("data", (b) => { + decodedLength += b.length; + decodedCrc = z.crc32(b, decodedCrc); + }); + const decoderDone = new Promise((resolve, reject) => { + decoder.on("end", resolve); + decoder.on("error", reject); + }); + for (let i = 0; i < joined.length; i += 7) + decoder.write(joined.subarray(i, i + 7)); + decoder.end(); + await decoderDone; + result[name] = { + length: decoded.length, + crc: z.crc32(decoded), + flushed, + decodedLength, + decodedCrc, + bytesWritten: stream.bytesWritten, + info: { + type: info.engine.constructor.name, + bytesWritten: info.engine.bytesWritten, + roundTrip: z[name + "DecompressSync"](info.buffer).length, + }, + }; + } + return result; + }, + + codecErrors: function ({ zlib: z }) { + const out = {}; + for (const name of ["Brotli", "Zstd"]) { + const values = []; + for (const options of [ + { chunkSize: 1 }, + { params: { 999: 1 } }, + { params: { 1: "bad" } }, + { flush: 99 }, + { finishFlush: -1 }, + { maxOutputLength: 0 }, + ]) { + try { + const engine = new z[name + "Compress"](options); + engine.destroy(); + values.push(null); + } catch (error) { + values.push({ + name: error.name, + code: error.code, + message: error.message, + }); + } + } + out[name] = values; + } + return out; + }, + + fsMetadata: async function ({ fs }) { + const path = "/tmp/metadata"; + fs.writeFileSync(path, "data", { mode: 0o600 }); + const before = fs.statSync(path); + fs.chmodSync(path, 0); + await fs.promises.chown(path, 123, 456); + const handle = await fs.promises.open(path, "r+"); + await handle.chmod(0); + await handle.chown(123, 456); + const after = await handle.stat(); + await handle.close(); + fs.mkdirSync("/tmp/metadata-dir", { mode: 0o700 }); + const mode = fs.statSync("/tmp/metadata-dir").mode; + fs.rmSync(path); + fs.rmdirSync("/tmp/metadata-dir"); + return { + before: [before.mode, before.uid, before.gid], + after: [after.mode, after.uid, after.gid], + directoryMode: mode, + statfs: fs.statfsSync("/missing"), + bigint: Object.values( + await fs.promises.statfs("/missing", { bigint: true }), + ).map(String), + }; + }, + + streamCancellation: async function ({ + streams: { ReadableStream, WritableStream }, + }) { + const tee = []; + for (const bytes of [false, true]) + for (const reverse of [false, true]) { + let reason; + const source = new ReadableStream({ + ...(bytes ? { type: "bytes" } : {}), + cancel(value) { + reason = value; + }, + }); + const pair = source.tee(); + const first = reverse ? 1 : 0, + second = 1 - first; + const a = pair[first].getReader().cancel("first"); + const b = pair[second].cancel("last"); + await Promise.all([a, b]); + tee.push(reason); + } + const pipes = []; + for (const options of [ + {}, + { preventCancel: true }, + { preventAbort: true }, + { preventCancel: true, preventAbort: true }, + ]) { + const events = []; + let controller, + settled = false; + const source = new ReadableStream({ + start(c) { + controller = c; + }, + cancel(reason) { + events.push(["cancel", reason]); + }, + }); + const sink = new WritableStream({ + write(value) { + events.push(["write", value]); + }, + abort(reason) { + events.push(["abort", reason]); + }, + }); + const signal = new AbortController(); + const done = source + .pipeTo(sink, { ...options, signal: signal.signal }) + .then( + () => { + settled = true; + }, + (reason) => { + settled = true; + events.push(["reject", reason]); + }, + ); + signal.abort("stop"); + await new Promise((resolve) => setTimeout(resolve, 10)); + const before = { settled, events: events.slice() }; + controller.enqueue(1); + await done; + pipes.push({ + before, + events, + sourceLocked: source.locked, + sinkLocked: sink.locked, + }); + } + return { tee, pipes }; + }, + + moduleMetadata: function ({ module: m }) { + const errors = {}; + for (const name of [ + "register", + "registerHooks", + "runMain", + "syncBuiltinESMExports", + "wrap", + "_debug", + "_findPath", + "_initPaths", + "_load", + "_resolveFilename", + "_resolveLookupPaths", + "_nodeModulePaths", + "findSourceMap", + "findPackageJSON", + "setSourceMapsSupport", + "stripTypeScriptTypes", + "SourceMap", + ]) { + try { + m[name](); + errors[name] = null; + } catch (e) { + errors[name] = { name: e.name, code: e.code, message: e.message }; + } + } + return { + errors, + compile: m.enableCompileCache() === undefined, + dir: m.getCompileCacheDir() === undefined, + flush: m.flushCompileCache() === undefined, + maps: m.getSourceMapsSupport(), + builtin: [ + "crypto", + "node:crypto", + "fs/promises", + "node:test", + "test", + "unknown", + ].map((n) => m.isBuiltin(n)), + }; + }, + httpInternals: function ({ + httpCommon: c, + httpOutgoing: o, + httpServer: s, + http, + }) { + const target = {}; + s.storeHTTPOptions.call(target, { + requestTimeout: 12000, + headersTimeout: 5000, + requireHostHeader: false, + }); + const errors = []; + for (const fn of [ + () => s.setupConnectionsTracking(), + () => s._connectionListener(), + () => + s.storeHTTPOptions.call({}, { requestTimeout: 1, headersTimeout: 2 }), + ]) { + try { + fn(); + errors.push(null); + } catch (e) { + errors.push({ name: e.name, code: e.code }); + } + } + const closed = []; + s.httpServerPreClose({ + closeIdleConnections() { + closed.push(true); + }, + }); + return { + tokens: ["X-Test", "bad space", ""].map((n) => c._checkIsHttpToken(n)), + headers: ["ok", "bad\r\nvalue"].map((n) => c._checkInvalidHeaderChar(n)), + methods: c.methods === http.METHODS, + classes: + o.OutgoingMessage === http.OutgoingMessage && s.Server === http.Server, + unique: [...o.parseUniqueHeadersOption(["X-A", ["x-a", "X-B"]])], + none: o.parseUniqueHeadersOption(null), + target: { + requestTimeout: target.requestTimeout, + headersTimeout: target.headersTimeout, + requireHostHeader: target.requireHostHeader, + }, + errors, + closed, + }; + }, +}; diff --git a/src/compat/fs/embedded.ts b/src/compat/fs/embedded.ts new file mode 100644 index 00000000..d8136190 --- /dev/null +++ b/src/compat/fs/embedded.ts @@ -0,0 +1,298 @@ +// Read-only in-memory filesystem for bundled worker assets. Build +// integrations register file contents at startup; bundled server code such +// as OpenNext's traced manifests then reads them through the node:fs +// surface. Mutating calls and real device filesystem access are not +// provided. + +const encoder = new TextEncoder(); +const files = new Map(); + +function normalize(path: unknown): string { + if (typeof path !== "string") throw new TypeError("path must be a string"); + const raw = path.startsWith("file://") ? new URL(path).pathname : path; + const parts: string[] = []; + for (const segment of raw.split("/")) { + if (segment === "" || segment === ".") continue; + if (segment === "..") parts.pop(); + else parts.push(segment); + } + return "/" + parts.join("/"); +} + +/** Called by generated bundle code; consumers do not invoke this directly. */ +export function registerVirtualFiles( + entries: Record, +): void { + for (const [path, contents] of Object.entries(entries)) { + const key = normalize(path); + files.set( + key, + typeof contents === "string" + ? encoder.encode(contents) + : contents instanceof Uint8Array + ? new Uint8Array(contents) + : Uint8Array.from(atob(contents.base64), (c) => c.charCodeAt(0)), + ); + } +} +/** Test/reset helper. */ +export function clearVirtualFiles(): void { + files.clear(); +} + +function fileEntry(path: string): Uint8Array | undefined { + const direct = files.get(path); + if (direct !== undefined) return direct; + // Bundled runtimes resolve relative to an empty dirname; treat relative + // and absolute lookups uniformly, then fall back to a suffix match so a + // build-absolute path resolves against a registered relative one. + for (const [key, contents] of files) + if (key === path || key.endsWith(path) || path.endsWith(key)) + return contents; + return undefined; +} + +function dirEntries(path: string): string[] { + const prefix = path.endsWith("/") ? path : path + "/"; + const names = new Set(); + for (const key of files.keys()) { + if (!key.startsWith(prefix)) continue; + const rest = key.slice(prefix.length); + names.add(rest.split("/")[0]); + } + return [...names].sort(); +} + +function exists(path: string): boolean { + return fileEntry(path) !== undefined || dirEntries(path).length > 0; +} + +function enoent(syscall: string, path: string): Error { + const error = new Error( + `ENOENT: no such file or directory, ${syscall} '${path}'`, + ) as NodeJS.ErrnoException; + error.code = "ENOENT"; + error.errno = -2; + error.syscall = syscall; + error.path = path; + return error; +} + +export class Stats { + readonly #size: number; + readonly #directory: boolean; + readonly #mtime: Date; + constructor(size: number, directory: boolean, mtime: Date) { + this.#size = size; + this.#directory = directory; + this.#mtime = mtime; + } + isFile() { + return !this.#directory; + } + isDirectory() { + return this.#directory; + } + isSymbolicLink() { + return false; + } + isBlockDevice() { + return false; + } + isCharacterDevice() { + return false; + } + isFIFO() { + return false; + } + isSocket() { + return false; + } + get size() { + return this.#size; + } + get mtime() { + return this.#mtime; + } + get mtimeMs() { + return this.#mtime.getTime(); + } + get birthtime() { + return this.#mtime; + } + get birthtimeMs() { + return this.#mtime.getTime(); + } + get ctime() { + return this.#mtime; + } + get ctimeMs() { + return this.#mtime.getTime(); + } + get atime() { + return this.#mtime; + } + get atimeMs() { + return this.#mtime.getTime(); + } + get mode() { + return this.#directory ? 0o40755 : 0o100644; + } + get dev() { + return 0; + } + get ino() { + return 0; + } + get nlink() { + return 1; + } + get uid() { + return 0; + } + get gid() { + return 0; + } + get rdev() { + return 0; + } + get blksize() { + return 4096; + } + get blocks() { + return Math.ceil(this.#size / 512); + } +} + +function statFor(path: string, syscall: string): Stats { + const data = fileEntry(path); + if (data !== undefined) return new Stats(data.length, false, new Date(0)); + if (dirEntries(path).length > 0 || path === "/") + return new Stats(0, true, new Date(0)); + throw enoent(syscall, path); +} + +function toPath(input: unknown): string { + if (input instanceof URL) return normalize(input.href); + if (typeof input !== "string" && !(input instanceof String)) + throw new TypeError("path must be a string or URL"); + return normalize(String(input)); +} + +function decode(data: Uint8Array, encoding?: string | null): string { + if (encoding === "utf8" || encoding === "utf-8" || encoding == null) + return new TextDecoder().decode(data); + throw new Error(`Unsupported encoding '${encoding}'`); +} + +export function readFileSync(path: unknown, options?: unknown): unknown { + const p = toPath(path); + const data = fileEntry(p); + if (data === undefined) throw enoent("open", p); + const encoding = + typeof options === "string" + ? options + : (options as { encoding?: string } | undefined)?.encoding; + return encoding ? decode(data, encoding) : new Uint8Array(data); +} + +export function readFile( + path: unknown, + options: unknown, + callback?: (error: Error | null, data?: unknown) => void, +): void { + const cb = (typeof options === "function" ? options : callback) as + | ((error: Error | null, data?: unknown) => void) + | undefined; + const promise = Promise.resolve().then(() => readFileSync(path, options)); + if (cb) + promise.then( + (data) => cb(null, data), + (error) => cb(error), + ); +} + +export function readdirSync(path: unknown): string[] { + const p = toPath(path); + const entries = dirEntries(p); + if (entries.length === 0 && fileEntry(p) === undefined && p !== "/") + throw enoent("scandir", p); + return entries; +} + +export function readdir( + path: unknown, + callback: (error: Error | null, entries?: string[]) => void, +): void { + Promise.resolve() + .then(() => readdirSync(path)) + .then( + (entries) => callback(null, entries), + (error) => callback(error), + ); +} + +export function existsSync(path: unknown): boolean { + return exists(toPath(path)); +} + +export function statSync(path: unknown): Stats { + return statFor(toPath(path), "stat"); +} + +export function lstatSync(path: unknown): Stats { + return statSync(path); +} + +export function stat( + path: unknown, + callback: (error: Error | null, stats?: Stats) => void, +): void { + Promise.resolve() + .then(() => statSync(path)) + .then( + (stats) => callback(null, stats), + (error) => callback(error), + ); +} + +export function realpathSync(path: unknown): string { + const p = toPath(path); + if (!exists(p)) throw enoent("realpath", p); + return p; +} + +export const promises = { + readFile: (path: unknown, options?: unknown) => + Promise.resolve().then(() => readFileSync(path, options)), + readdir: (path: unknown) => Promise.resolve().then(() => readdirSync(path)), + stat: (path: unknown) => Promise.resolve().then(() => statSync(path)), + lstat: (path: unknown) => Promise.resolve().then(() => lstatSync(path)), + realpath: (path: unknown) => Promise.resolve().then(() => realpathSync(path)), + access: (path: unknown) => + Promise.resolve().then(() => { + if (!exists(toPath(path))) throw enoent("access", toPath(path)); + }), +}; + +export const constants = { + F_OK: 0, + R_OK: 4, + W_OK: 2, + X_OK: 1, +}; + +export default { + readFileSync, + readFile, + readdirSync, + readdir, + existsSync, + statSync, + lstatSync, + stat, + realpathSync, + promises, + constants, + Stats, +}; diff --git a/src/compat/fs/index.ts b/src/compat/fs/index.ts index bb99614a..0b02148a 100644 --- a/src/compat/fs/index.ts +++ b/src/compat/fs/index.ts @@ -1,298 +1,357 @@ -// Read-only in-memory filesystem for bundled worker assets. Build -// integrations register file contents at startup; bundled server code such -// as OpenNext's traced manifests then reads them through the node:fs -// surface. Mutating calls and real device filesystem access are not -// provided. - -const encoder = new TextEncoder(); -const files = new Map(); - -function normalize(path: unknown): string { - if (typeof path !== "string") throw new TypeError("path must be a string"); - const raw = path.startsWith("file://") ? new URL(path).pathname : path; - const parts: string[] = []; - for (const segment of raw.split("/")) { - if (segment === "" || segment === ".") continue; - if (segment === "..") parts.pop(); - else parts.push(segment); - } - return "/" + parts.join("/"); +import { fileMode } from "./mode.js"; +import { validateInteger } from "../crypto/validation.js"; +import * as embedded from "./embedded.js"; +import { + temporary, + temporaryPath, + clearTemporaryFiles, + writablePath, + pathOf, +} from "./writable.js"; +import { Buffer } from "node:buffer"; +import { + validateFunction, + validateObject, + validateBoolean, +} from "../net/validators.js"; +export const registerVirtualFiles = embedded.registerVirtualFiles; +export function clearVirtualFiles() { + embedded.clearVirtualFiles(); + clearTemporaryFiles(); } - -/** Called by generated bundle code; consumers do not invoke this directly. */ -export function registerVirtualFiles( - entries: Record, -): void { - for (const [path, contents] of Object.entries(entries)) { - const key = normalize(path); - files.set( - key, - typeof contents === "string" - ? encoder.encode(contents) - : contents instanceof Uint8Array - ? contents - : Uint8Array.from(atob(contents.base64), (c) => c.charCodeAt(0)), - ); - } +const reads = new Set([ + "readFileSync", + "readFile", + "readdirSync", + "readdir", + "statSync", + "stat", + "lstatSync", + "realpathSync", + "existsSync", +]); +const api: any = { ...temporary }; +for (const name of Object.keys(api)) + if (typeof api[name] === "function" && /^[a-z]/.test(name)) + api[name] = (...args: any[]) => temporary[name](...args); +for (const name of [ + "ReadStream", + "WriteStream", + "FileReadStream", + "FileWriteStream", +]) { + api[name] = new Proxy(api[name], { + construct(_target, args) { + args[0] = writablePath(args[0], name); + return Reflect.construct(temporary[name], args); + }, + }); } -/** Test/reset helper. */ -export function clearVirtualFiles(): void { - files.clear(); +for (const name of reads) { + const old = (embedded as any)[name]; + api[name] = (...args: any[]) => + temporaryPath(args[0]) ? temporary[name](...args) : old(...args); } - -function fileEntry(path: string): Uint8Array | undefined { - const direct = files.get(path); - if (direct !== undefined) return direct; - // Bundled runtimes resolve relative to an empty dirname; treat relative - // and absolute lookups uniformly, then fall back to a suffix match so a - // build-absolute path resolves against a registered relative one. - for (const [key, contents] of files) - if (key === path || key.endsWith(path) || path.endsWith(key)) - return contents; - return undefined; +for (const name of ["watch", "watchFile", "unwatchFile"]) + api[name] = () => { + throw Object.assign(new Error("The operation is not supported"), { + code: "ERR_UNSUPPORTED_OPERATION", + }); + }; +// Workers reports the byte count from these synchronous operations. +for (const name of ["writeFileSync", "appendFileSync"]) + api[name] = (...args: any[]) => { + temporary[name](...args); + const encoding = typeof args[2] === "string" ? args[2] : args[2]?.encoding; + return typeof args[1] === "string" + ? Buffer.byteLength(args[1], encoding) + : args[1].byteLength; + }; +const promises: any = { ...temporary.promises }; +for (const name of Object.keys(promises)) + if (typeof promises[name] === "function" && /^[a-z]/.test(name)) + promises[name] = (...args: any[]) => temporary.promises[name](...args); +for (const name of [ + "readFile", + "readdir", + "stat", + "lstat", + "realpath", + "access", +]) { + const old = (embedded.promises as any)[name]; + promises[name] = (...args: any[]) => + temporaryPath(args[0]) ? temporary.promises[name](...args) : old(...args); } - -function dirEntries(path: string): string[] { - const prefix = path.endsWith("/") ? path : path + "/"; - const names = new Set(); - for (const key of files.keys()) { - if (!key.startsWith(prefix)) continue; - const rest = key.slice(prefix.length); - names.add(rest.split("/")[0]); - } - return [...names].sort(); +// Keep all writes inside the memory volume, including FileHandle operations. +for (const name of [ + "appendFile", + "writeFile", + "mkdir", + "mkdtemp", + "rm", + "rmdir", + "unlink", + "truncate", + "utimes", + "lutimes", + "chmod", + "chown", + "lchmod", + "lchown", + "open", + "rename", + "copyFile", + "cp", + "link", + "symlink", +]) { + promises[name] = async (...args: any[]) => { + if (name === "symlink") { + return new Promise((resolve, reject) => + temporary.symlink(...args, (error: unknown) => + error ? reject(error) : resolve(undefined), + ), + ); + } + const indexes = ["rename", "copyFile", "cp", "link"].includes(name) + ? [0, 1] + : [0]; + for (const i of indexes) + if ( + typeof args[i] !== "object" || + args[i] instanceof URL || + Buffer.isBuffer(args[i]) + ) { + if (!temporaryPath(args[i])) + throw Object.assign(new Error("EROFS: read-only file system"), { + code: "EROFS", + }); + } + return temporary.promises[name](...args); + }; } - -function exists(path: string): boolean { - return fileEntry(path) !== undefined || dirEntries(path).length > 0; -} - -function enoent(syscall: string, path: string): Error { - const error = new Error( - `ENOENT: no such file or directory, ${syscall} '${path}'`, - ) as NodeJS.ErrnoException; - error.code = "ENOENT"; - error.errno = -2; - error.syscall = syscall; - error.path = path; - return error; -} - -export class Stats { - readonly #size: number; - readonly #directory: boolean; - readonly #mtime: Date; - constructor(size: number, directory: boolean, mtime: Date) { - this.#size = size; - this.#directory = directory; - this.#mtime = mtime; - } - isFile() { - return !this.#directory; - } - isDirectory() { - return this.#directory; - } - isSymbolicLink() { - return false; - } - isBlockDevice() { - return false; - } - isCharacterDevice() { - return false; - } - isFIFO() { - return false; - } - isSocket() { - return false; - } - get size() { - return this.#size; - } - get mtime() { - return this.#mtime; - } - get mtimeMs() { - return this.#mtime.getTime(); - } - get birthtime() { - return this.#mtime; - } - get birthtimeMs() { - return this.#mtime.getTime(); - } - get ctime() { - return this.#mtime; - } - get ctimeMs() { - return this.#mtime.getTime(); - } - get atime() { - return this.#mtime; - } - get atimeMs() { - return this.#mtime.getTime(); - } - get mode() { - return this.#directory ? 0o40755 : 0o100644; - } - get dev() { - return 0; - } - get ino() { - return 0; - } - get nlink() { - return 1; - } - get uid() { - return 0; - } - get gid() { - return 0; - } - get rdev() { - return 0; - } - get blksize() { - return 4096; - } - get blocks() { - return Math.ceil(this.#size / 512); - } -} - -function statFor(path: string, syscall: string): Stats { - const data = fileEntry(path); - if (data !== undefined) return new Stats(data.length, false, new Date(0)); - if (dirEntries(path).length > 0 || path === "/") - return new Stats(0, true, new Date(0)); - throw enoent(syscall, path); -} - -function toPath(input: unknown): string { - if (input instanceof URL) return normalize(input.href); - if (typeof input !== "string" && !(input instanceof String)) - throw new TypeError("path must be a string or URL"); - return normalize(String(input)); -} - -function decode(data: Uint8Array, encoding?: string | null): string { - if (encoding === "utf8" || encoding === "utf-8" || encoding == null) - return new TextDecoder().decode(data); - throw new Error(`Unsupported encoding '${encoding}'`); -} - -export function readFileSync(path: unknown, options?: unknown): unknown { - const p = toPath(path); - const data = fileEntry(p); - if (data === undefined) throw enoent("open", p); - const encoding = - typeof options === "string" - ? options - : (options as { encoding?: string } | undefined)?.encoding; - return encoding ? decode(data, encoding) : data; -} - -export function readFile( - path: unknown, - options: unknown, - callback?: (error: Error | null, data?: unknown) => void, -): void { - const cb = (typeof options === "function" ? options : callback) as - | ((error: Error | null, data?: unknown) => void) - | undefined; - const promise = Promise.resolve().then(() => readFileSync(path, options)); - if (cb) - promise.then( - (data) => cb(null, data), - (error) => cb(error), - ); -} - -export function readdirSync(path: unknown): string[] { - const p = toPath(path); - const entries = dirEntries(p); - if (entries.length === 0 && fileEntry(p) === undefined && p !== "/") - throw enoent("scandir", p); - return entries; -} - -export function readdir( - path: unknown, - callback: (error: Error | null, entries?: string[]) => void, -): void { - Promise.resolve() - .then(() => readdirSync(path)) - .then( - (entries) => callback(null, entries), - (error) => callback(error), - ); -} - -export function existsSync(path: unknown): boolean { - return exists(toPath(path)); -} - -export function statSync(path: unknown): Stats { - return statFor(toPath(path), "stat"); -} - -export function lstatSync(path: unknown): Stats { - return statSync(path); +promises.watch = async function* () { + api.watch(); +}; +// Workers validates ownership/permission requests but has no mutable Unix +// ownership or permissions. These operations must not change the memory VFS. +function installOperation( + name: string, + handler: (...args: any[]) => any, + promise = true, +) { + api[name + "Sync"] = handler; + api[name] = (...args: any[]) => { + const callback = args.pop(); + validateFunction(callback, "callback"); + Promise.resolve() + .then(() => handler(...args)) + .then( + (value) => callback(null, value), + (error) => callback(error), + ); + }; + if (promise) promises[name] = async (...args: any[]) => handler(...args); } - -export function stat( - path: unknown, - callback: (error: Error | null, stats?: Stats) => void, -): void { - Promise.resolve() - .then(() => statSync(path)) - .then( - (stats) => callback(null, stats), - (error) => callback(error), - ); +function temporaryStats(stat: any) { + if (stat == null) return stat; + const bigint = typeof stat.mode === "bigint"; + const mode = (Number(stat.mode) & ~0o777) | 0o666; + stat.mode = bigint ? BigInt(mode) : mode; + stat.uid = stat.gid = bigint ? 0n : 0; + return stat; } - -export function realpathSync(path: unknown): string { - const p = toPath(path); - if (!exists(p)) throw enoent("realpath", p); - return p; +for (const name of ["stat", "lstat"]) + installOperation(name, (path: any, options?: any) => + temporaryPath(path) + ? temporaryStats(temporary[name + "Sync"](path, options)) + : embedded.statSync(path), + ); +installOperation( + "fstat", + (...args: any[]) => temporaryStats(temporary.fstatSync(...args)), + false, +); +for (const name of ["chmod", "lchmod", "fchmod", "chown", "lchown", "fchown"]) { + installOperation( + name, + (path: any, value: any, gid: any) => { + if (name.endsWith("chmod")) fileMode(value); + else { + validateInteger(value, "uid", -1, 0xffffffff); + validateInteger(gid, "gid", -1, 0xffffffff); + } + if (name.startsWith("f")) { + validateInteger(path, "fd", 0, 2147483647); + temporary.fstatSync(path); + } else if (name.startsWith("l") && temporaryPath(path)) + temporary.lstatSync(path); + else api.statSync(path); + }, + !name.startsWith("f"), + ); } - -export const promises = { - readFile: (path: unknown, options?: unknown) => - Promise.resolve().then(() => readFileSync(path, options)), - readdir: (path: unknown) => Promise.resolve().then(() => readdirSync(path)), - stat: (path: unknown) => Promise.resolve().then(() => statSync(path)), - lstat: (path: unknown) => Promise.resolve().then(() => lstatSync(path)), - realpath: (path: unknown) => Promise.resolve().then(() => realpathSync(path)), - access: (path: unknown) => - Promise.resolve().then(() => { - if (!exists(toPath(path))) throw enoent("access", toPath(path)); - }), -}; - -export const constants = { - F_OK: 0, - R_OK: 4, - W_OK: 2, - X_OK: 1, +installOperation("statfs", (path: any, options: any = {}) => { + pathOf(path); + validateObject(options, "options"); + const bigint = options.bigint ?? false; + validateBoolean(bigint, "options.bigint"); + const value = bigint ? 0n : 0; + return { + type: value, + bsize: value, + blocks: value, + bfree: value, + bavail: value, + files: value, + ffree: value, + }; +}); +const openPromise = promises.open; +promises.open = async (...args: any[]) => { + fileMode(args[2], 0o666); + args[2] = 0o666; + const handle = await openPromise(...args); + const stat = handle.stat.bind(handle); + handle.stat = async (...options: any[]) => + temporaryStats(await stat(...options)); + handle.chmod = async (mode: unknown) => { + fileMode(mode); + await stat(); + }; + handle.chown = async (uid: number, gid: number) => { + validateInteger(uid, "uid", -1, 0xffffffff); + validateInteger(gid, "gid", -1, 0xffffffff); + await stat(); + }; + return handle; }; - -export default { - readFileSync, - readFile, - readdirSync, - readdir, +// Copy an embedded bundle file into request-local /tmp without granting any +// write access to its source volume. +const temporaryCopyFile = api.copyFileSync; +installOperation("copyFile", (source: any, destination: any, mode = 0) => { + if (![0, 1, 2, 4].includes(mode)) + throw Object.assign(new TypeError("Invalid copy mode"), { + code: "ERR_INVALID_ARG_VALUE", + }); + if (mode === 4) + throw Object.assign(new Error("The operation is not supported"), { + code: "ERR_UNSUPPORTED_OPERATION", + }); + if (temporaryPath(source)) + return temporaryCopyFile(source, destination, mode); + writablePath(destination, "copyFile"); + if (mode === 1 && api.existsSync(destination)) + throw Object.assign(new Error("File already exists"), { code: "EEXIST" }); + temporary.writeFileSync(destination, embedded.readFileSync(pathOf(source))); +}); +api.promises = promises; +export { promises }; +export const { + Dir, + Dirent, + F_OK, + FileReadStream, + FileWriteStream, + R_OK, + ReadStream, + Stats, + W_OK, + WriteStream, + X_OK, + access, + accessSync, + appendFile, + appendFileSync, + chmod, + chmodSync, + chown, + chownSync, + close, + closeSync, + constants, + copyFile, + copyFileSync, + cp, + cpSync, + createReadStream, + createWriteStream, + exists, existsSync, - statSync, + fchmod, + fchmodSync, + fchown, + fchownSync, + fdatasync, + fdatasyncSync, + fstat, + fstatSync, + fsync, + fsyncSync, + ftruncate, + ftruncateSync, + futimes, + futimesSync, + glob, + globSync, + lchmod, + lchmodSync, + lchown, + lchownSync, + link, + linkSync, + lstat, lstatSync, - stat, + lutimes, + lutimesSync, + mkdir, + mkdirSync, + mkdtemp, + mkdtempSync, + open, + openAsBlob, + openSync, + opendir, + opendirSync, + read, + readFile, + readFileSync, + readSync, + readdir, + readdirSync, + readlink, + readlinkSync, + readv, + readvSync, + realpath, realpathSync, - promises, - constants, - Stats, -}; + rename, + renameSync, + rm, + rmSync, + rmdir, + rmdirSync, + stat, + statSync, + statfs, + statfsSync, + symlink, + symlinkSync, + truncate, + truncateSync, + unlink, + unlinkSync, + unwatchFile, + utimes, + utimesSync, + watch, + watchFile, + write, + writeFile, + writeFileSync, + writeSync, + writev, + writevSync, +} = api; +export default api; diff --git a/src/compat/fs/mode.ts b/src/compat/fs/mode.ts new file mode 100644 index 00000000..193ae03d --- /dev/null +++ b/src/compat/fs/mode.ts @@ -0,0 +1,17 @@ +// Pinned Workers mode validation (node/internal/validators.ts). +import { validateUint32 } from "../net/validators.js"; +import { ERR_INVALID_ARG_VALUE } from "../net/errors.js"; +export function fileMode(value: unknown, fallback?: number): number { + value ??= fallback; + if (typeof value === "string") { + if (!/^[0-7]+$/.test(value)) + throw new ERR_INVALID_ARG_VALUE( + "mode", + value, + "must be a 32-bit unsigned integer or an octal string", + ); + value = Number.parseInt(value, 8); + } + validateUint32(value, "mode"); + return value as number; +} diff --git a/src/compat/fs/promises/index.ts b/src/compat/fs/promises/index.ts index 8671d6b7..5e428b3b 100644 --- a/src/compat/fs/promises/index.ts +++ b/src/compat/fs/promises/index.ts @@ -1,3 +1,37 @@ import { promises } from "../index.js"; -export const { readFile, readdir, stat, lstat, realpath, access } = promises; +export const { + FileHandle, + access, + appendFile, + chmod, + chown, + constants, + copyFile, + cp, + glob, + lchmod, + lchown, + link, + lstat, + lutimes, + mkdir, + mkdtemp, + open, + opendir, + readFile, + readdir, + readlink, + realpath, + rename, + rm, + rmdir, + stat, + statfs, + symlink, + truncate, + unlink, + utimes, + watch, + writeFile, +} = promises; export default promises; diff --git a/src/compat/fs/writable.spec.ts b/src/compat/fs/writable.spec.ts new file mode 100644 index 00000000..a582c4dd --- /dev/null +++ b/src/compat/fs/writable.spec.ts @@ -0,0 +1,129 @@ +import { expect, test } from "vitest"; +import * as fs from "./index.js"; +import { runWithTemporaryFiles } from "./writable.js"; + +test("temporary files are isolated between requests and from the fallback volume", () => { + fs.clearVirtualFiles(); + runWithTemporaryFiles(() => { + fs.mkdirSync("/tmp/request"); + fs.writeFileSync("/tmp/request/data", "one"); + expect(fs.readFileSync("/tmp/request/data", "utf8")).toBe("one"); + runWithTemporaryFiles(() => { + expect(fs.existsSync("/tmp/request/data")).toBe(false); + fs.writeFileSync("/tmp/data", "two"); + }); + expect(fs.existsSync("/tmp/data")).toBe(false); + expect(fs.readFileSync("/tmp/request/data", "utf8")).toBe("one"); + }); + expect(fs.existsSync("/tmp/request/data")).toBe(false); +}); + +test("concurrent asynchronous requests retain separate temporary files", () => { + const operation = (value: string) => + runWithTemporaryFiles(() => { + fs.writeFileSync("/tmp/data", value); + return fs.promises.readFile("/tmp/data", "utf8").then((read: string) => { + expect(read).toBe(value); + expect(fs.readFileSync("/tmp/data", "utf8")).toBe(value); + }); + }); + return Promise.all([operation("first"), operation("second")]); +}); + +test("a temporary symlink cannot mutate embedded assets", () => { + fs.registerVirtualFiles({ "/bundle/secret": "original" }); + expect(() => fs.symlinkSync("/bundle/secret", "/tmp/link")).toThrow(); + expect(() => fs.writeFileSync("/bundle/secret", "changed")).toThrow(); + expect(fs.readFileSync("/bundle/secret", "utf8")).toBe("original"); +}); + +test("relative temporary symlinks resolve against their parent directory", async () => { + await runWithTemporaryFiles(async () => { + fs.mkdirSync("/tmp/nested"); + fs.writeFileSync("/tmp/nested/data", "local"); + fs.symlinkSync("data", "/tmp/nested/link"); + expect(fs.readFileSync("/tmp/nested/link", "utf8")).toBe("local"); + await fs.promises.symlink("../nested/data", "/tmp/nested/async-link"); + expect(await fs.promises.readFile("/tmp/nested/async-link", "utf8")).toBe( + "local", + ); + expect(() => + fs.symlinkSync("../../bundle/secret", "/tmp/nested/escape"), + ).toThrow(); + }); +}); + +test("callback writes report read-only paths asynchronously", async () => { + let returned = false; + const done = new Promise((resolve, reject) => { + fs.writeFile("/bundle/secret", "changed", (error: any) => { + try { + expect(returned).toBe(true); + expect(error.code).toBe("EROFS"); + resolve(); + } catch (failure) { + reject(failure); + } + }); + }); + returned = true; + await done; +}); + +test("direct stream constructors use the active request volume", async () => { + fs.clearVirtualFiles(); + const read = (value: string) => + runWithTemporaryFiles(() => { + fs.writeFileSync("/tmp/stream", value); + const stream = new fs.ReadStream("/tmp/stream"); + return new Promise((resolve, reject) => { + let text = ""; + stream.on("data", (chunk: any) => { + text += chunk.toString(); + }); + stream.on("end", () => resolve(text)); + stream.on("error", reject); + }); + }); + expect(await Promise.all([read("first"), read("second")])).toEqual([ + "first", + "second", + ]); + expect(fs.existsSync("/tmp/stream")).toBe(false); +}); + +test("embedded files can be copied into temporary files without exposing mutable storage", async () => { + fs.registerVirtualFiles({ "/bundle/copy-source": "original" }); + const bytes = fs.readFileSync("/bundle/copy-source") as Uint8Array; + bytes.fill(0); + expect(fs.readFileSync("/bundle/copy-source", "utf8")).toBe("original"); + await runWithTemporaryFiles(async () => { + await fs.promises.copyFile("/bundle/copy-source", "/tmp/copy"); + expect(fs.readFileSync("/tmp/copy", "utf8")).toBe("original"); + fs.writeFileSync("/tmp/copy", "changed"); + expect(fs.readFileSync("/bundle/copy-source", "utf8")).toBe("original"); + }); +}); + +test("permission operations preserve file metadata and statfs is a Workers stub", async () => { + await runWithTemporaryFiles(async () => { + fs.writeFileSync("/tmp/data", "content"); + const before = fs.statSync("/tmp/data"); + fs.chmodSync("/tmp/data", 0); + await fs.promises.chown("/tmp/data", 123, 456); + const handle = await fs.promises.open("/tmp/data", "r+"); + await handle.chmod(0); + await handle.chown(123, 456); + await handle.close(); + const after = fs.statSync("/tmp/data"); + expect([after.mode, after.uid, after.gid]).toEqual([ + before.mode, + before.uid, + before.gid, + ]); + expect(fs.readFileSync("/tmp/data", "utf8")).toBe("content"); + expect( + Object.values(await fs.promises.statfs("/missing", { bigint: true })), + ).toEqual(Array(7).fill(0n)); + }); +}); diff --git a/src/compat/fs/writable.ts b/src/compat/fs/writable.ts new file mode 100644 index 00000000..a0db6b0f --- /dev/null +++ b/src/compat/fs/writable.ts @@ -0,0 +1,153 @@ +import { fileMode } from "./mode.js"; +import { registerRequestScope } from "../../shared/request-scope.js"; +// Writable, memory-only /tmp. Embedded bundle files remain read-only. +// This adapter does not expose the device filesystem. +import { Volume, createFsFromVolume } from "memfs"; +import { Buffer } from "node:buffer"; +import { posix } from "node:path"; +import { ERR_INVALID_ARG_TYPE } from "../net/errors.js"; +import { AsyncLocalStorage } from "../async-hooks/index.js"; +export function pathOf(value: any): string { + if (value instanceof URL) value = decodeURIComponent(value.pathname); + if (Buffer.isBuffer(value)) value = value.toString(); + if (typeof value !== "string") + throw new ERR_INVALID_ARG_TYPE("path", ["string", "Buffer", "URL"], value); + return posix.resolve("/bundle", value); +} +export function temporaryPath(value: any): boolean { + if (typeof value === "number") return true; + const path = pathOf(value); + return path === "/tmp" || path.startsWith("/tmp/"); +} +export function writablePath(value: any, syscall: string) { + if (typeof value === "number") return value; + const path = pathOf(value); + if (!(path === "/tmp" || path.startsWith("/tmp/"))) + throw Object.assign( + new Error(`EROFS: read-only file system, ${syscall} '${path}'`), + { code: "EROFS", errno: -30, syscall, path }, + ); + return path; +} + +function createTemporary() { + const volume = new Volume(); + volume.mkdirSync("/tmp"); + const fs = createFsFromVolume(volume) as any; + const mutating = new Set([ + "appendFile", + "writeFile", + "mkdir", + "mkdtemp", + "rm", + "rmdir", + "unlink", + "truncate", + "utimes", + "lutimes", + "chmod", + "chown", + "lchmod", + "lchown", + ]); + const api: any = {}; + for (const name of Object.keys(fs)) { + if (typeof fs[name] !== "function") { + api[name] = fs[name]; + continue; + } + if (/^[A-Z]/.test(name)) { + api[name] = fs[name]; + continue; + } + const base = name.replace(/Sync$/, ""); + api[name] = (...args: any[]) => { + try { + if ( + ["writeFile", "appendFile"].includes(base) && + typeof args[0] !== "number" + ) { + const opts = args[2]; + if (typeof opts === "object" && opts !== null) { + fileMode(opts.mode, 0o666); + args[2] = { ...opts, mode: 0o666 }; + } else if (typeof opts === "function") + args.splice(2, 0, { mode: 0o666 }); + else if (opts === undefined || typeof opts === "string") + args[2] = { + mode: 0o666, + ...(typeof opts === "string" ? { encoding: opts } : {}), + }; + } + if (base === "symlink") { + args[1] = writablePath(args[1], base); + const target = Buffer.isBuffer(args[0]) + ? args[0].toString() + : args[0]; + if (typeof target !== "string") + throw new ERR_INVALID_ARG_TYPE( + "target", + ["string", "Buffer"], + target, + ); + writablePath(posix.resolve(posix.dirname(args[1]), target), base); + } else if (["rename", "copyFile", "cp", "link"].includes(base)) { + args[0] = writablePath(args[0], base); + args[1] = writablePath(args[1], base); + } else if ( + mutating.has(base) || + base === "open" || + base === "createWriteStream" + ) + args[0] = writablePath(args[0], base); + else if ( + typeof args[0] === "string" || + args[0] instanceof URL || + Buffer.isBuffer(args[0]) + ) + args[0] = pathOf(args[0]); + } catch (error) { + const callback = args[args.length - 1]; + if (!name.endsWith("Sync") && typeof callback === "function") { + queueMicrotask(() => callback(error)); + return; + } + throw error; + } + return fs[name](...args); + }; + } + return { + api, + clear() { + for (const name of fs.readdirSync("/tmp")) + fs.rmSync(`/tmp/${name}`, { recursive: true, force: true }); + }, + }; +} + +const contexts = new AsyncLocalStorage>(); +const fallback = createTemporary(); +const active = () => contexts.getStore() ?? fallback; +export const temporary: any = new Proxy( + {}, + { + get(_target, name) { + return active().api[name]; + }, + ownKeys() { + return Reflect.ownKeys(fallback.api); + }, + getOwnPropertyDescriptor() { + return { enumerable: true, configurable: true }; + }, + }, +); +export function clearTemporaryFiles() { + active().clear(); +} +export function runWithTemporaryFiles(callback: () => T): T { + return contexts.run(createTemporary(), callback); +} + +registerRequestScope(runWithTemporaryFiles); diff --git a/src/compat/http-common/index.ts b/src/compat/http-common/index.ts new file mode 100644 index 00000000..8d210733 --- /dev/null +++ b/src/compat/http-common/index.ts @@ -0,0 +1,48 @@ +// 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. + +import { METHODS as methods } from "../http/index.js"; +import { kIncomingMessage } from "../http/symbols.js"; +export { methods, kIncomingMessage }; +const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/; +/** + * Verifies that the given val is a valid HTTP token + * per the rules defined in RFC 7230 + * See https://tools.ietf.org/html/rfc7230#section-3.2.6 + */ +export function _checkIsHttpToken(val: string): boolean { + return tokenRegExp.test(val); +} + +const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; +/** + * True if val contains an invalid field-vchar + * field-value = *( field-content / obs-fold ) + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + */ +export function _checkInvalidHeaderChar( + val: number | string | ReadonlyArray, +): boolean { + if (Array.isArray(val)) { + return val.some((v: string) => headerCharRegex.test(v)); + } + return headerCharRegex.test(`${val}`); +} + +export const CRLF = "\r\n"; +export const chunkExpression = /(?:^|\W)chunked(?:$|\W)/i; +export const continueExpression = /(?:^|\W)100-continue(?:$|\W)/i; +export const parsers = { name: "parsers", max: 1000 }; +export default { + _checkIsHttpToken, + _checkInvalidHeaderChar, + chunkExpression, + kIncomingMessage, + CRLF, + continueExpression, + methods, + parsers, +}; diff --git a/src/compat/http-outgoing/index.ts b/src/compat/http-outgoing/index.ts new file mode 100644 index 00000000..65232d96 --- /dev/null +++ b/src/compat/http-outgoing/index.ts @@ -0,0 +1,46 @@ +// 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. + +import { + OutgoingMessage, + validateHeaderName, + validateHeaderValue, +} from "../http/index.js"; +import { kUniqueHeaders, kHighWaterMark } from "../http/symbols.js"; +export { + OutgoingMessage, + validateHeaderName, + validateHeaderValue, + kUniqueHeaders, + kHighWaterMark, +}; +export function parseUniqueHeadersOption( + headers?: (string | string[])[], +): Set | null { + if (!Array.isArray(headers)) { + return null; + } + + const unique = new Set(); + for (const header of headers) { + if (Array.isArray(header)) { + for (const h of header) { + unique.add(h.toLowerCase()); + } + } else { + unique.add(header.toLowerCase()); + } + } + return unique; +} + +export default { + OutgoingMessage, + validateHeaderName, + validateHeaderValue, + kUniqueHeaders, + kHighWaterMark, + parseUniqueHeadersOption, +}; diff --git a/src/compat/http-server/index.ts b/src/compat/http-server/index.ts new file mode 100644 index 00000000..1321f3cf --- /dev/null +++ b/src/compat/http-server/index.ts @@ -0,0 +1,149 @@ +// 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. + +import { + Server, + ServerResponse, + IncomingMessage, + STATUS_CODES, + _connectionListener, +} from "../http/index.js"; +import { + kIncomingMessage, + kServerResponse, + kConnectionsCheckingInterval, +} from "../http/symbols.js"; +import { validateInteger } from "../crypto/validation.js"; +import { validateBoolean } from "../net/validators.js"; +import { ERR_OUT_OF_RANGE } from "../net/errors.js"; +class ERR_OPTION_NOT_IMPLEMENTED extends Error { + code = "ERR_OPTION_NOT_IMPLEMENTED"; + constructor(option: string) { + super(`The ${option} option is not implemented`); + } +} +export { + Server, + ServerResponse, + STATUS_CODES, + kServerResponse, + kConnectionsCheckingInterval, + _connectionListener, +}; +export function setupConnectionsTracking(): never { + throw Object.assign( + new Error("The setupConnectionsTracking method is not implemented"), + { code: "ERR_METHOD_NOT_IMPLEMENTED" }, + ); +} +export function storeHTTPOptions(this: any, options: any): void { + this[kIncomingMessage] = options.IncomingMessage || IncomingMessage; + this[kServerResponse] = options.ServerResponse || ServerResponse; + + const maxHeaderSize = options.maxHeaderSize; + if (maxHeaderSize !== undefined) { + validateInteger(maxHeaderSize, "maxHeaderSize", 0); + throw new ERR_OPTION_NOT_IMPLEMENTED("maxHeaderSize"); + } + + const insecureHTTPParser = options.insecureHTTPParser; + if (insecureHTTPParser !== undefined) { + // If enabled it will use a HTTP parser with leniency flags enabled. + // Since our implementation does not use any http parser, and uses "fetch" API, + // it doesn't make sense to support this option. + validateBoolean(insecureHTTPParser, "options.insecureHTTPParser"); + } + + const requestTimeout = options.requestTimeout; + if (requestTimeout !== undefined) { + validateInteger(requestTimeout, "requestTimeout", 0); + this.requestTimeout = requestTimeout; + } else { + this.requestTimeout = 300_000; // 5 minutes + } + + const headersTimeout = options.headersTimeout; + if (headersTimeout !== undefined) { + validateInteger(headersTimeout, "headersTimeout", 0); + this.headersTimeout = headersTimeout; + } else { + this.headersTimeout = Math.min(60_000, this.requestTimeout); // Minimum between 60 seconds or requestTimeout + } + + if ( + this.requestTimeout > 0 && + this.headersTimeout > 0 && + this.headersTimeout > this.requestTimeout + ) { + throw new ERR_OUT_OF_RANGE( + "headersTimeout", + "<= requestTimeout", + headersTimeout, + ); + } + + const keepAliveTimeout = options.keepAliveTimeout; + if (keepAliveTimeout !== undefined) { + validateInteger(keepAliveTimeout, "keepAliveTimeout", 0); + throw new ERR_OPTION_NOT_IMPLEMENTED("keepAliveTimeout"); + } + + const keepAliveTimeoutBuffer = options.keepAliveTimeoutBuffer; + if (keepAliveTimeoutBuffer !== undefined) { + validateInteger(keepAliveTimeoutBuffer, "keepAliveTimeoutBuffer"); + throw new ERR_OPTION_NOT_IMPLEMENTED("keepAliveTimeoutBuffer"); + } + + const connectionsCheckingInterval = options.connectionsCheckingInterval; + if (connectionsCheckingInterval !== undefined) { + validateInteger( + connectionsCheckingInterval, + "connectionsCheckingInterval", + 0, + ); + throw new ERR_OPTION_NOT_IMPLEMENTED("connectionsCheckingInterval"); + } + + const requireHostHeader = options.requireHostHeader; + if (requireHostHeader !== undefined) { + validateBoolean(requireHostHeader, "options.requireHostHeader"); + this.requireHostHeader = requireHostHeader; + } else { + this.requireHostHeader = true; + } + + const joinDuplicateHeaders = options.joinDuplicateHeaders; + if (joinDuplicateHeaders !== undefined) { + validateBoolean(joinDuplicateHeaders, "options.joinDuplicateHeaders"); + } + this.joinDuplicateHeaders = joinDuplicateHeaders ?? false; + + const rejectNonStandardBodyWrites = options.rejectNonStandardBodyWrites; + if (rejectNonStandardBodyWrites !== undefined) { + validateBoolean( + rejectNonStandardBodyWrites, + "options.rejectNonStandardBodyWrites", + ); + this.rejectNonStandardBodyWrites = rejectNonStandardBodyWrites; + } else { + this.rejectNonStandardBodyWrites = false; + } +} + +export function httpServerPreClose(server: any) { + server.closeIdleConnections(); + clearInterval(server[kConnectionsCheckingInterval]); +} +export default { + Server, + ServerResponse, + STATUS_CODES, + kServerResponse, + kConnectionsCheckingInterval, + _connectionListener, + setupConnectionsTracking, + storeHTTPOptions, + httpServerPreClose, +}; diff --git a/src/compat/http/symbols.ts b/src/compat/http/symbols.ts new file mode 100644 index 00000000..182f7531 --- /dev/null +++ b/src/compat/http/symbols.ts @@ -0,0 +1,12 @@ +// 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. + +export const kIncomingMessage = Symbol("IncomingMessage"); +export const kServerResponse = Symbol("ServerResponse"); +export const kUniqueHeaders = Symbol("kUniqueHeaders"); +export const kHighWaterMark = Symbol("kHighWaterMark"); +export const kConnectionsCheckingInterval = Symbol( + "http.server.connectionsCheckingInterval", +); diff --git a/src/compat/module/builtins.ts b/src/compat/module/builtins.ts new file mode 100644 index 00000000..f90e4dbf --- /dev/null +++ b/src/compat/module/builtins.ts @@ -0,0 +1,102 @@ +// 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. + +// Intentionally does not include modules with mandatory 'node:' +// prefix like `node:test`. +// See: See https://nodejs.org/docs/latest/api/modules.html#built-in-modules-with-mandatory-node-prefix +// TODO(later): This list duplicates the list that is in +// workerd/jsg/modules.c++. Later we should source these +// from the same place so we don't have to maintain two lists. +export const builtinModules = [ + "_http_agent", + "_http_client", + "_http_common", + "_http_incoming", + "_http_outgoing", + "_http_server", + "_stream_duplex", + "_stream_passthrough", + "_stream_readable", + "_stream_transform", + "_stream_wrap", + "_stream_writable", + "_tls_common", + "_tls_wrap", + "assert", + "assert/strict", + "async_hooks", + "buffer", + "child_process", + "cluster", + "console", + "constants", + "crypto", + "dgram", + "diagnostics_channel", + "dns", + "dns/promises", + "domain", + "events", + "fs", + "fs/promises", + "http", + "http2", + "https", + "inspector", + "inspector/promises", + "module", + "net", + "os", + "path", + "path/posix", + "path/win32", + "perf_hooks", + "process", + "punycode", + "querystring", + "readline", + "readline/promises", + "repl", + "stream", + "stream/consumers", + "stream/promises", + "stream/web", + "string_decoder", + "sys", + "timers", + "timers/promises", + "tls", + "trace_events", + "tty", + "url", + "util", + "util/types", + "v8", + "vm", + "wasi", + "worker_threads", + "zlib", +]; +Object.freeze(builtinModules); diff --git a/src/compat/module/index.ts b/src/compat/module/index.ts index 588acc95..9770976a 100644 --- a/src/compat/module/index.ts +++ b/src/compat/module/index.ts @@ -2,29 +2,36 @@ // the registered virtual filesystem; executable modules cannot be loaded in // a bundled WebView runtime and throw explicitly. import { readFileSync } from "../fs/index.js"; -export const builtinModules = [ - "async_hooks", - "buffer", - "fs", - "fs/promises", - "http", - "module", - "net", - "path", - "querystring", - "stream", - "stream/web", - "string_decoder", - "util", -]; +import { builtinModules } from "./builtins.js"; +export { builtinModules }; +import * as metadata from "./metadata.js"; +export * from "./metadata.js"; +import { posix } from "node:path"; +import { ERR_INVALID_ARG_VALUE } from "../net/errors.js"; export function isBuiltin(specifier: string): boolean { + if (typeof specifier !== "string") return false; + if (["node:test", "node:test/reporters", "node:sqlite"].includes(specifier)) + return true; const name = specifier.startsWith("node:") ? specifier.slice(5) : specifier; return builtinModules.includes(name); } -export function createRequire(_filename: string | URL) { +export function createRequire(filename: string | URL) { + const value = String(filename); + if (!value.startsWith("/") && !value.startsWith("file:")) + throw new ERR_INVALID_ARG_VALUE( + "path", + value, + "The argument must be a file URL object, a file URL string, or an absolute path string.", + ); + const pathname = value.startsWith("file:") + ? decodeURIComponent(new URL(value).pathname) + : value; + const directory = posix.dirname(pathname); return function require(id: string): unknown { if (id.endsWith(".json")) { - const data = readFileSync(id, { encoding: "utf8" }); + const data = readFileSync(posix.resolve(directory, id), { + encoding: "utf8", + }); return JSON.parse(data as string); } throw new Error( @@ -43,4 +50,5 @@ export class Module { return createRequire("/")(id); } } +Object.assign(Module, metadata); export default Module; diff --git a/src/compat/module/metadata.ts b/src/compat/module/metadata.ts new file mode 100644 index 00000000..a9e4d61c --- /dev/null +++ b/src/compat/module/metadata.ts @@ -0,0 +1,154 @@ +// 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 + +// Pinned Workers module metadata and explicit stubs. +class ERR_METHOD_NOT_IMPLEMENTED extends Error { + readonly code = "ERR_METHOD_NOT_IMPLEMENTED"; + constructor(method: string) { + super(`The ${method} method is not implemented`); + } +} +export function register(): void { + // TODO(soon): We might support this in the future. + throw new ERR_METHOD_NOT_IMPLEMENTED("module.register"); +} + +export function runMain(): void { + // We don't plan to support this in the future. + throw new ERR_METHOD_NOT_IMPLEMENTED("module.runMain"); +} + +export function syncBuiltinESMExports(): void { + // We are unlikely to ever support this. + throw new ERR_METHOD_NOT_IMPLEMENTED("module.syncBuiltinESMExports"); +} + +export function wrap(): void { + // TODO(soon): Implement this feature. + throw new ERR_METHOD_NOT_IMPLEMENTED("module.wrap"); +} + +export const globalPaths: string[] = []; + +const compileCacheStatus = Object.freeze({ + __proto__: null, + FAILED: 0, + ENABLED: 1, + ALREADY_ENABLED: 2, + DISABLED: 3, +}); + +export const constants = Object.freeze({ + __proto__: null, + compileCacheStatus, +}); + +export const _cache = { __proto__: null }; +export const _pathCache = { __proto__: null }; + +export function _debug(): void { + // This is deprecated and will be removed in the future. + throw new ERR_METHOD_NOT_IMPLEMENTED("module._debug"); +} + +export function _findPath(): void { + // It doesn't make sense to support this. + throw new ERR_METHOD_NOT_IMPLEMENTED("module._findPath"); +} + +export function _initPaths(): void { + // It doesn't make sense to support this. + throw new ERR_METHOD_NOT_IMPLEMENTED("module._initPaths"); +} + +export function _load(): void { + // TODO(soon): Investigate the possibility of supporting this in the future. + throw new ERR_METHOD_NOT_IMPLEMENTED("module._load"); +} + +export function _preloadModules(): void { + // It doesn't make sense to support this. + throw new ERR_METHOD_NOT_IMPLEMENTED("module._preloadModules"); +} + +export function _resolveFilename(): void { + // TODO(soon): Investigate the possibility of supporting this in the future. + throw new ERR_METHOD_NOT_IMPLEMENTED("module._resolveFilename"); +} + +export function _resolveLookupPaths(): void { + // TODO(soon): Investigate the possibility of supporting this in the future. + throw new ERR_METHOD_NOT_IMPLEMENTED("module._resolveLookupPaths"); +} + +export function _nodeModulePaths(): void { + // It doesn't make sense to support this. + throw new ERR_METHOD_NOT_IMPLEMENTED("module._nodeModulePaths"); +} + +export function findSourceMap(): void { + // TODO(soon): Investigate the possibility of supporting this in the future. + throw new ERR_METHOD_NOT_IMPLEMENTED("module.findSourceMap"); +} + +export function findPackageJSON(): void { + // It doesn't make sense to support this. + throw new ERR_METHOD_NOT_IMPLEMENTED("module.findPackageJSON"); +} + +export function flushCompileCache(): void { + // We don't implement compile cache. This acts as a no-op. +} + +export function getSourceMapsSupport(): Record { + return Object.freeze({ + __proto__: null, + enabled: false, + nodeModules: false, + generatedCode: false, + }); +} + +export function setSourceMapsSupport(): void { + // We don't implement source maps support. + throw new ERR_METHOD_NOT_IMPLEMENTED("module.setSourceMapsSupport"); +} + +export function stripTypeScriptTypes(): void { + // We don't implement stripping TypeScript types. + throw new ERR_METHOD_NOT_IMPLEMENTED("module.stripTypeScriptTypes"); +} + +export function registerHooks(): void { + // We don't implement hooks registration. + throw new ERR_METHOD_NOT_IMPLEMENTED("module.registerHooks"); +} + +export function SourceMap(): void { + // We don't support source maps. + throw new ERR_METHOD_NOT_IMPLEMENTED("module.SourceMap"); +} + +export function enableCompileCache(): void { + // We don't plan to support this in the future. + // Since, this method shouldn't throw an error, we just act like a no-op. +} + +export function getCompileCacheDir(): undefined { + // We don't plan to support this in the future. + // Since, compile cache acts as a no-op, we just return undefined. + return undefined; +} + +export const _extensions = { + ".js": (): never => { + throw new ERR_METHOD_NOT_IMPLEMENTED("module._extensions.js"); + }, + ".json": (): never => { + throw new ERR_METHOD_NOT_IMPLEMENTED("module._extensions.json"); + }, + ".node": (): never => { + throw new ERR_METHOD_NOT_IMPLEMENTED("module._extensions.node"); + }, +}; diff --git a/src/compat/net/internal-net.ts b/src/compat/net/internal-net.ts index d8fee760..5a110a1f 100644 --- a/src/compat/net/internal-net.ts +++ b/src/compat/net/internal-net.ts @@ -1832,6 +1832,10 @@ function initializeConnection( allowHalfOpen: true, // A Node.js socket is always capable of being upgraded to the TLS socket. secureTransport: socket.encrypted ? "on" : "starttls", + expectedServerHostname: socket.encrypted + ? (socket as Socket & { servername?: string }).servername || + socket._host + : undefined, // We are not going to pass the high water-mark here. The outer Node.js // stream will implement the appropriate backpressure for us. }); diff --git a/src/compat/net/sockets.spec.ts b/src/compat/net/sockets.spec.ts index ea1063ec..fee471f1 100644 --- a/src/compat/net/sockets.spec.ts +++ b/src/compat/net/sockets.spec.ts @@ -146,3 +146,60 @@ it("does not announce native EOF until the reader consumes buffered bytes", asyn call.mock.calls.filter((args) => args[1] === "socketRead"), ).toHaveLength(1); }); + +it("preserves the TLS verification name when connecting to a resolved address", async () => { + const call = vi.fn(async (_, method) => + method === "socketWatch" ? new Promise(() => {}) : {}, + ); + restore = setNativeCall(call); + const socket = connect("127.0.0.1:443", { + secureTransport: "on", + expectedServerHostname: "example.com", + }); + await socket.opened; + expect( + call.mock.calls.find((args) => args[1] === "socketOpen")?.[2], + ).toMatchObject({ + host: "127.0.0.1", + port: 443, + tls: true, + servername: "example.com", + }); + await socket.close(); +}); + +it("flushes an active plain write before TLS handoff and invalidates the old writer", async () => { + let finish!: () => void; + const call = vi.fn(async (_, method) => { + if (method === "socketWrite") + return new Promise((resolve) => { + finish = resolve; + }); + if (method === "socketRead" || method === "socketWatch") + return new Promise(() => {}); + return {}; + }); + restore = setNativeCall(call); + const plain = connect("localhost:1234", { secureTransport: "starttls" }); + await plain.opened; + const writer = plain.writable.getWriter(); + void writer.closed.catch(() => {}); + const writing = writer.write(new Uint8Array([1])); + await Promise.resolve(); + await Promise.resolve(); + const tls = plain.startTls({ expectedServerHostname: "localhost" }); + await Promise.resolve(); + expect(call.mock.calls.some(([, m]) => m === "socketStartTls")).toBe(false); + finish(); + await writing; + await tls.opened; + await expect(writer.write(new Uint8Array([2]))).rejects.toThrow("upgraded"); + expect(call.mock.calls.filter(([, m]) => m === "socketOpen")).toHaveLength(1); + expect(call.mock.calls.filter(([, m]) => m === "socketWrite")).toHaveLength( + 1, + ); + await plain.close(); + expect(call.mock.calls.some(([, m]) => m === "socketClose")).toBe(false); + await tls.close(); + expect(call.mock.calls.some(([, m]) => m === "socketClose")).toBe(true); +}); diff --git a/src/compat/net/sockets.ts b/src/compat/net/sockets.ts index 38fff192..7b858af1 100644 --- a/src/compat/net/sockets.ts +++ b/src/compat/net/sockets.ts @@ -1,5 +1,9 @@ +import { nativeTlsSession } from "../tls/native-session.js"; import { observeConsumption } from "./reader.js"; -import { getNativeCall } from "../../browser/fetch/native-network.js"; +import { + getNativeCall, + type NativeCall, +} from "../../browser/fetch/native-network.js"; export interface SocketInfo { localAddress?: string | null; @@ -7,6 +11,8 @@ export interface SocketInfo { } export interface PlatformSocket { upgraded?: boolean; + secureTransport: "on" | "off" | "starttls"; + startTls(options?: { expectedServerHostname?: string }): PlatformSocket; opened: Promise; closed: Promise; readable: ReadableStream; @@ -26,6 +32,7 @@ function encoded(value: Uint8Array) { export function connect( address: string, options: { + expectedServerHostname?: string; allowHalfOpen?: boolean; secureTransport?: "on" | "off" | "starttls"; } = {}, @@ -51,6 +58,28 @@ export function connect( ) throw new TypeError("Invalid TCP address"); const id = crypto.randomUUID(); + const host = url.hostname.replace(/^\[|\]$/g, ""); + return transport(call, id, host, options.secureTransport ?? "off", () => + call("WorkersCapacitor", "socketOpen", { + id, + host, + port, + tls: options.secureTransport === "on", + ...(options.expectedServerHostname + ? { servername: options.expectedServerHostname } + : {}), + }), + ); +} +function transport( + call: NativeCall, + id: string, + host: string, + secureTransport: "on" | "off" | "starttls", + open: () => Promise, +): PlatformSocket { + let upgraded = false; + let activeWrite: Promise | undefined; let readableController!: ReadableStreamDefaultController; let writableController!: WritableStreamDefaultController; let readEnded = false, @@ -66,7 +95,8 @@ export function connect( readPending = false, pendingBytes = 0; const notifyTerminal = () => { - if (!terminal || stopped || readPending || pendingBytes !== 0) return; + if (upgraded || !terminal || stopped || readPending || pendingBytes !== 0) + return; if (!readEnded) { readEnded = true; readableController.close(); @@ -75,6 +105,7 @@ export function connect( }; let closing: Promise | undefined; const close = () => { + if (upgraded) return Promise.resolve(); if (!closing) { stopped = true; closing = call("WorkersCapacitor", "socketClose", { id }).then( @@ -98,7 +129,7 @@ export function connect( return closing; }; const fail = (error: unknown) => { - if (!stopped) { + if (!stopped && !upgraded) { readableController.error(error); writableController.error(error); rejectClosed(error); @@ -111,12 +142,7 @@ export function connect( const opened = Promise.resolve() .then(() => { if (stopped) throw new Error("Socket closed before connection"); - return call("WorkersCapacitor", "socketOpen", { - id, - host: url.hostname.replace(/^\[|\]$/g, ""), - port, - tls: options.secureTransport === "on", - }); + return open(); }) .then(async (value) => { if (stopped) { @@ -127,7 +153,7 @@ export function connect( }); void opened .then(async () => { - if (stopped) return; + if (stopped || upgraded) return; await call("WorkersCapacitor", "socketWatch", { id }); terminal = true; notifyTerminal(); @@ -144,12 +170,13 @@ export function connect( genericController as ReadableStreamDefaultController; try { await opened; + if (upgraded) return; readPending = true; const result = (await call("WorkersCapacitor", "socketRead", { id, maxBytes: 65536, })) as { chunk?: string; eof?: boolean }; - if (stopped) return; + if (stopped || upgraded) return; if (result.eof) { if (!readEnded) { readEnded = true; @@ -180,19 +207,27 @@ export function connect( writableController = controller; }, async write(chunk) { - try { + if (upgraded) throw new Error("Socket has been upgraded"); + const writing = (async () => { await opened; for (let offset = 0; offset < chunk.byteLength; offset += 65536) await call("WorkersCapacitor", "socketWrite", { id, chunk: encoded(chunk.subarray(offset, offset + 65536)), }); + })(); + activeWrite = writing; + try { + await writing; } catch (error) { fail(error); throw error; + } finally { + if (activeWrite === writing) activeWrite = undefined; } }, async close() { + if (upgraded) throw new Error("Socket has been upgraded"); try { await opened; await call("WorkersCapacitor", "socketEnd", { id }); @@ -212,6 +247,49 @@ export function connect( pendingBytes -= count; notifyTerminal(); }); - return { opened, closed, readable, writable, close }; + const socket: PlatformSocket = { + opened, + closed, + readable, + writable, + close, + secureTransport, + startTls(options = {}) { + if ( + upgraded || + stopped || + secureTransport !== "starttls" || + readEnded || + writeEnded + ) + throw new Error("Socket cannot be upgraded"); + upgraded = true; + socket.upgraded = true; + const error = new Error("Socket has been upgraded"); + readableController.error(error); + writableController.error(error); + resolveClosed(); + let secureCall = call; + return transport( + (...args) => secureCall(...args), + id, + host, + "on", + async () => { + const info = await opened; + await activeWrite; + const hostname = options.expectedServerHostname ?? host; + const result = (await call("WorkersCapacitor", "socketStartTls", { + id, + servername: hostname, + })) as { tlsEngine?: string } | undefined; + if (result?.tlsEngine === "openssl-wasm") + secureCall = await nativeTlsSession(call, id, hostname); + return info; + }, + ); + }, + }; + return socket; } export default { connect }; diff --git a/src/compat/process/base.d.ts b/src/compat/process/base.d.ts new file mode 100644 index 00000000..2c519113 --- /dev/null +++ b/src/compat/process/base.d.ts @@ -0,0 +1,38 @@ +declare module "cf-workers-native:process-base" { + const process: Record; + export default process; + export const addListener: any; + export const arch: any; + export const argv: any; + export const binding: any; + export const browser: any; + export const chdir: any; + export const cwd: any; + export const dlopen: any; + export const emit: any; + export const emitWarning: any; + export const env: any; + export const execArgv: any; + export const execPath: any; + export const exit: any; + export const features: any; + export const kill: any; + export const listeners: any; + export const memoryUsage: any; + export const nextTick: any; + export const off: any; + export const on: any; + export const once: any; + export const pid: any; + export const platform: any; + export const prependListener: any; + export const prependOnceListener: any; + export const removeAllListeners: any; + export const removeListener: any; + export const title: any; + export const umask: any; + export const uptime: any; + export const uvCounters: any; + export const version: any; + export const versions: any; +} diff --git a/src/compat/process/index.cjs b/src/compat/process/index.cjs new file mode 100644 index 00000000..7c72726b --- /dev/null +++ b/src/compat/process/index.cjs @@ -0,0 +1 @@ +module.exports = require("./index.js").default; diff --git a/src/compat/process/index.ts b/src/compat/process/index.ts new file mode 100644 index 00000000..63afd2e9 --- /dev/null +++ b/src/compat/process/index.ts @@ -0,0 +1,203 @@ +import * as metadata from "./metadata.js"; +export { + arch, + pid, + ppid, + allowedNodeEnvironmentFlags, + setSourceMapsEnabled, + getSourceMapsSupport, + ref, + unref, + setUncaughtExceptionCaptureCallback, + hasUncaughtExceptionCaptureCallback, + kill, + binding, + dlopen, + exitCode, + getActiveResourcesInfo, + memoryUsage, + resourceUsage, + threadCpuUsage, + cpuUsage, + channel, + connected, + debugPort, + noDeprecation, + traceDeprecation, + throwDeprecation, + sourceMapsEnabled, + execPath, + permission, + release, + report, + finalization, + _rawDebug, + moduleLoadList, + _preload_modules, + _linkedBinding, + domain, + _exiting, + _getActiveRequests, + _getActiveHandles, + _kill, + constrainedMemory, + availableMemory, + execve, + _fatalException, + _tickCallback, + _debugProcess, + _debugEnd, + _startProfilerIdleNotifier, + _stopProfilerIdleNotifier, + send, + uptime, + getegid, + getgid, + getgroups, + geteuid, + getuid, + setegid, + setgid, + seteuid, + setuid, + setgroups, + initgroups, +} from "./metadata.js"; +// Event delivery, warning and nextTick integration for the WebView profile. +// Other process exports retain the existing polyfill; see ../events/README.md. +export * from "cf-workers-native:process-base"; +import process from "./state.js"; +export { default } from "./state.js"; +import { EventEmitter } from "../events/index.js"; +import { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE } from "../net/errors.js"; +let initialized = false; +function invoke(method: string, args: any[]): any { + if (!initialized) { + EventEmitter.init.call(process as any); + initialized = true; + } + return Reflect.apply(EventEmitter.prototype[method], process, args); +} +export const on = (...args: any[]) => invoke("on", args); +export const addListener = on; +export const once = (...args: any[]) => invoke("once", args); +export const prependListener = (...args: any[]) => + invoke("prependListener", args); +export const prependOnceListener = (...args: any[]) => + invoke("prependOnceListener", args); +export const off = (...args: any[]) => invoke("off", args); +export const removeListener = off; +export const removeAllListeners = (...args: any[]) => + invoke("removeAllListeners", args); +export const emit = (...args: any[]) => invoke("emit", args); +export const listeners = (...args: any[]) => invoke("listeners", args); +const rawListeners = (...args: any[]) => invoke("rawListeners", args); +const eventNames = (...args: any[]) => invoke("eventNames", args); +const listenerCount = (...args: any[]) => invoke("listenerCount", args); +const getMaxListeners = (...args: any[]) => invoke("getMaxListeners", args); +const setMaxListeners = (...args: any[]) => invoke("setMaxListeners", args); +// Pinned workerd also implements nextTick with queueMicrotask. +export function nextTick(callback: (...args: any[]) => void, ...args: any[]) { + queueMicrotask(() => callback(...args)); +} +// Adapted from cloudflare/workerd@5358e3d337336eaf696a48bc1563d808b10feba5, +// src/node/internal/internal_process.ts, Apache-2.0. +// Copyright (c) 2017-2022 Cloudflare, Inc. +// https://opensource.org/licenses/Apache-2.0 +export function emitWarning( + warning: string | Error, + options?: any, + codeOrCtor?: any, + maybeCtor?: any, +) { + let err: Error & { code?: string; detail?: string }; + let name = "Warning", + detail, + code, + ctor; + if (typeof options === "object" && !Array.isArray(options)) { + if (options.type) name = options.type; + if (options.code) code = options.code; + if (options.detail) detail = options.detail; + ctor = options.ctor; + } else if (typeof options === "string") { + name = options; + if (typeof codeOrCtor === "string") { + code = codeOrCtor; + if (typeof maybeCtor === "function") ctor = maybeCtor; + else if (maybeCtor !== undefined) + throw new ERR_INVALID_ARG_TYPE("ctor", "function", maybeCtor); + } else if (typeof codeOrCtor === "function") ctor = codeOrCtor; + else if (codeOrCtor !== undefined) + throw new ERR_INVALID_ARG_TYPE("ctor", "function", codeOrCtor); + } else if (typeof options === "function") ctor = options; + else if (options !== undefined) + throw new ERR_INVALID_ARG_TYPE("options", "object", options); + if (typeof warning === "string") { + err = new (ctor || Error)(warning); + err.name = name; + } else if (warning instanceof Error) { + err = warning; + if (name && name !== "Warning") err.name = name; + } else throw new ERR_INVALID_ARG_TYPE("warning", "string or Error", warning); + if (code) err.code = code; + if (detail && typeof detail === "string") err.detail = detail; + // WebKit lacks V8's captureStackTrace. Exact stack inspection remains a + // process/engine acceptance gap even when warning delivery matches. + Error.captureStackTrace?.(err, ctor || emitWarning); + queueMicrotask(() => process.emit("warning", err)); +} +export const hrtime = Object.assign( + function hrtime(time?: [number, number]): [number, number] { + if (time !== undefined) { + if (!Array.isArray(time)) + throw new ERR_INVALID_ARG_TYPE("time", "tuple of integers", time); + else if ((time as number[]).length !== 2) + throw new ERR_OUT_OF_RANGE("time", "2", time); + } + const nanoTime = BigInt(Date.now()) * 1_000_000n; + const nanosBigint = nanoTime % 1_000_000_000n; + let seconds = Number((nanoTime - nanosBigint) / 1_000_000_000n); + let nanos = Number(nanosBigint); + if (time) { + const [prevSeconds, prevNanos] = time; + seconds -= prevSeconds; + nanos -= prevNanos; + // We don't have nanosecond-level precision, but if we + // did (say with the introduction of randomness), this + // is how we would handle nanosecond underflow + if (nanos < 0) { + seconds -= 1; + nanos += 1e9; + } + } + return [seconds, nanos]; + }, + { + bigint: function (): bigint { + return BigInt(Date.now()) * 1_000_000n; + }, + }, +); + +Object.assign(process, { + hrtime, + ...metadata, + on, + addListener, + once, + prependListener, + prependOnceListener, + off, + removeListener, + removeAllListeners, + emit, + listeners, + rawListeners, + eventNames, + listenerCount, + getMaxListeners, + setMaxListeners, + nextTick, + emitWarning, +}); diff --git a/src/compat/process/metadata.ts b/src/compat/process/metadata.ts new file mode 100644 index 00000000..8d3b1b72 --- /dev/null +++ b/src/compat/process/metadata.ts @@ -0,0 +1,357 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. Apache-2.0. +// Adapted from pinned workerd public_process.ts; these are Workers stubs, not host OS introspection. +import { NodeError } from "../net/error-base.js"; +class ERR_METHOD_NOT_IMPLEMENTED extends NodeError { + constructor(name: string) { + super( + "ERR_METHOD_NOT_IMPLEMENTED", + `The ${name} method is not implemented`, + ); + } +} +export class ERR_EPERM extends NodeError { + path?: string; + syscall: string; + errno: number; + constructor(options: { syscall: string; errno?: number; path?: string }) { + const message = options.path + ? `operation not permitted, ${options.syscall} '${options.path}'` + : "operation not permitted"; + super("EPERM", message); + this.syscall = options.syscall; + if (options.path !== undefined) { + this.path = options.path; + } + this.errno = options.errno ?? 1; // EPERM + } +} + +export const arch = "x64"; + +// For simplicity and polyfill expectations, we assign pid 1 to the process and pid 0 to the "parent". +export const pid = 1; +export const ppid = 0; + +export const allowedNodeEnvironmentFlags = new Set(); + +export function setSourceMapsEnabled( + _enabled: boolean, + _options?: { nodeModules: boolean; generatedCode: boolean }, +): void { + // no-op since we do not support disabling source maps +} + +export function getSourceMapsSupport(): { + enabled: boolean; + nodeModules: boolean; + generatedCode: boolean; +} { + return { + enabled: false, + nodeModules: false, + generatedCode: false, + }; +} + +export function ref(): void { + // no-op +} + +export function unref(): void { + // no-op +} + +export function setUncaughtExceptionCaptureCallback( + _fn?: (...args: unknown[]) => void, +): void { + throw new ERR_METHOD_NOT_IMPLEMENTED( + "process.setUncaughtExceptionCaptureCallback", + ); +} + +export function hasUncaughtExceptionCaptureCallback(): boolean { + throw new ERR_METHOD_NOT_IMPLEMENTED( + "process.hasUncaughtExceptionCaptureCallback", + ); +} + +export function kill(_pid: number, _signal?: string | number): boolean { + throw new ERR_METHOD_NOT_IMPLEMENTED("process.kill"); +} + +export function binding(_name: string): unknown { + throw new ERR_METHOD_NOT_IMPLEMENTED("process.binding"); +} + +export function dlopen( + _module: unknown, + _filename: string, + _flags?: number, +): void { + throw new ERR_METHOD_NOT_IMPLEMENTED("process.dlopen"); +} + +export const exitCode: number | undefined = undefined; + +export function getActiveResourcesInfo(): string[] { + throw new ERR_METHOD_NOT_IMPLEMENTED("process.getActiveResourcesInfo"); +} + +export function memoryUsage(): { + rss: number; + heapTotal: number; + heapUsed: number; + external: number; + arrayBuffers: number; +} { + return { + rss: 0, + heapTotal: 0, + heapUsed: 0, + external: 0, + arrayBuffers: 0, + }; +} + +export function resourceUsage(): { + userCPUTime: number; + systemCPUTime: number; + maxRSS: number; + sharedMemorySize: number; + unsharedDataSize: number; + unsharedStackSize: number; + minorPageFault: number; + majorPageFault: number; + swappedOut: number; + fsRead: number; + fsWrite: number; + ipcSent: number; + ipcReceived: number; + signalsCount: number; + voluntaryContextSwitches: number; + involuntaryContextSwitches: number; +} { + return { + userCPUTime: 0, + systemCPUTime: 0, + maxRSS: 0, + sharedMemorySize: 0, + unsharedDataSize: 0, + unsharedStackSize: 0, + minorPageFault: 0, + majorPageFault: 0, + swappedOut: 0, + fsRead: 0, + fsWrite: 0, + ipcSent: 0, + ipcReceived: 0, + signalsCount: 0, + voluntaryContextSwitches: 0, + involuntaryContextSwitches: 0, + }; +} + +export function threadCpuUsage(): { + user: number; + system: number; +} { + throw new ERR_METHOD_NOT_IMPLEMENTED("process.threadCpuUsage"); +} + +export function cpuUsage(_previousValue?: { user: number; system: number }): { + user: number; + system: number; +} { + throw new ERR_METHOD_NOT_IMPLEMENTED("process.cpuUsage"); +} + +// Properties and constants +export const channel = null; +export const connected = false; +export const debugPort = 0; // may be implemented to align with inspector in future +export const noDeprecation = false; +export const traceDeprecation = false; +export const throwDeprecation = false; +export const sourceMapsEnabled = false; +export const execPath = ""; + +export const permission = { + has: (): boolean => { + throw new ERR_METHOD_NOT_IMPLEMENTED("process.permission.has"); + }, +}; + +export const release = { + name: "node", + lts: true, + sourceUrl: "", + headersUrl: "", +}; + +export const report = { + compact: false, + directory: "", + filename: "", + getReport: (): Record => { + // We do not intend to implement process.report in workerd, but there + // are some modules in the ecosystem that call it expecting it to exist and + // not throw. Returning an empty object for now to satisfy that use case. + return {}; + }, + reportOnFatalError: false, + reportOnSignal: false, + reportOnUncaughtException: false, + excludeNetwork: false, + excludeEnv: false, + signal: "SIGUSR2", + writeReport: (): string => { + // In this case, there's an expectation that the function will produce + // a report on disk and return the filename. Since we do not intend + // to implement this API in workerd, we throw to indicate that. + throw new ERR_METHOD_NOT_IMPLEMENTED("process.report.writeReport"); + }, +}; + +export const finalization = { + register: (): void => { + throw new ERR_METHOD_NOT_IMPLEMENTED("process.finalization.register"); + }, + registerBeforeExit: (): void => { + throw new ERR_METHOD_NOT_IMPLEMENTED( + "process.finalization.registerBeforeExit", + ); + }, + unregister: (): void => { + throw new ERR_METHOD_NOT_IMPLEMENTED("process.finalization.unregister"); + }, +}; + +// Additional undocumented APIs +export function _rawDebug(...args: unknown[]): void { + console.log(...args); +} + +export const moduleLoadList: string[] = []; +export const _preload_modules: string[] = []; + +export function _linkedBinding(_name: string): unknown { + throw new ERR_METHOD_NOT_IMPLEMENTED("process._linkedBinding"); +} + +export const domain = null; +export const _exiting = false; + +export function _getActiveRequests(): unknown[] { + return []; +} + +export function _getActiveHandles(): unknown[] { + return []; +} + +export function _kill(_pid: number, _signal: number): boolean { + throw new ERR_METHOD_NOT_IMPLEMENTED("process._kill"); +} + +export function constrainedMemory(): number { + return 0; +} + +export function availableMemory(): number { + // This may be implemented in future, for now this matches unenv + return 0; +} + +export function execve( + _file: string, + _args: string[], + _env: Record, +): never { + throw new ERR_METHOD_NOT_IMPLEMENTED("process.execve"); +} + +export function _fatalException(_err: Error): boolean { + return false; +} + +export function _tickCallback(): void { + // no-op +} + +export function _debugProcess(_pid: number): void { + // no-op +} + +export function _debugEnd(): void { + // no-op +} + +export function _startProfilerIdleNotifier(): void { + // no-op +} + +export function _stopProfilerIdleNotifier(): void { + // no-op +} + +export function send(_message: unknown, _sendHandle?: unknown): boolean { + return false; +} + +export function uptime(): number { + return 0; +} + +// On the virtual filesystem, we only support group 0 +export function getegid(): number { + return 0; +} + +export function getgid(): number { + return 0; +} + +export function getgroups(): number[] { + return [0]; +} + +// On the virtual filesystem, we only support user 0 +export function geteuid(): number { + return 0; +} + +export function getuid(): number { + return 0; +} + +// We support group 0 but no others, with the virtual filesystem. +export function setegid(id: number | string): void { + if (id !== 0) throw new ERR_EPERM({ syscall: "setegid" }); +} + +// We support group 0 but no others, with the virtual filesystem. +export function setgid(id: number | string): void { + if (id !== 0) throw new ERR_EPERM({ syscall: "setgid" }); +} + +// We support group 0 but no others, with the virtual filesystem. +export function seteuid(id: number | string): void { + if (id !== 0) throw new ERR_EPERM({ syscall: "seteuid" }); +} + +// We support group 0 but no others, with the virtual filesystem. +export function setuid(id: number | string): void { + if (id !== 0) throw new ERR_EPERM({ syscall: "setuid" }); +} + +// We don't support setting or initializing arbitrary groups on the virtual filesystem. +export function setgroups(_groups: number[]): void { + throw new ERR_EPERM({ syscall: "setgroups" }); +} + +export function initgroups( + _user: number | string, + _extractGroup: number | string, +): void { + throw new ERR_EPERM({ syscall: "initgroups" }); +} diff --git a/src/compat/process/state.ts b/src/compat/process/state.ts new file mode 100644 index 00000000..04139831 --- /dev/null +++ b/src/compat/process/state.ts @@ -0,0 +1,4 @@ +import original from "cf-workers-native:process-base"; +// A dependency leaf keeps process.env available during events/util cycles. +const process = { ...original }; +export default process; diff --git a/src/compat/stream-web/LICENSE b/src/compat/stream-web/LICENSE new file mode 100644 index 00000000..7adbcf57 --- /dev/null +++ b/src/compat/stream-web/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2026 Mattias Buelens +Copyright (c) 2016 Diwank Singh Tomer + +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. diff --git a/src/compat/stream-web/README.md b/src/compat/stream-web/README.md index 31c33607..82f79236 100644 --- a/src/compat/stream-web/README.md +++ b/src/compat/stream-web/README.md @@ -4,19 +4,25 @@ The public module and installed globals share their stream constructors. Stream callbacks capture the registered async context at construction; starting AsyncLocalStorage does not replace those constructor identities. -On hosts without readable byte streams (including iOS 26.1), the adapter uses -web-streams-polyfill 4.3.0 for the core stream family. Native compression and -encoding codecs are bridged with backpressure. Request/Response keep body -ownership in the JS stream, using the native body codecs after consumption. -Blob streams use the same family. The fallback is JavaScript only and is -installed/restored through the existing global ownership mechanism. +Every host uses the packaged web-streams-polyfill 4.3.0 implementation with +pinned Workers cancellation patches. Tee cancellation forwards the last branch's +reason. Abort during a pending pipeTo read waits for that read to settle, discards +the arriving chunk, and cancels the source before aborting the destination. +The differential profile covers default/byte tee and preventCancel/preventAbort. + +Native compression and encoding codecs are bridged with backpressure. +Request/Response keep body ownership in the JS stream, using native body codecs +after consumption. Pre-installation native Request bodies transfer through a +captured native TransformStream before bridging. Blob streams use the same +owned family. Global installation/restoration retains the ownership mechanism. The dependency is pinned because the body adapter reads its disturbance state. A missing or changed state fails explicitly. The fallback browser fixture -removes the host byte-stream APIs before module initialization in both the -page and Worker, and runs the regular runtime contracts unchanged. +removes the host byte-stream APIs before initialization in page and Worker, +and runs the regular runtime contracts unchanged. -Whole-module acceptance remains open. The pinned Workers tee cancellation -reason and pending pipeTo abort timing still differ from standard host/ponyfill -streams. Those are implementable differences recorded by the stream-boundary -experiment, not reasons to declare the module complete or technically impossible. +Rebuild the checked-in implementation with scripts/build-workers-streams.mjs. +The adjacent build manifest records the upstream revision, patched source hashes +and generated artifact hash. Raw-host boundary probes remain useful counterexamples +to host forwarding; they are not the adapter's acceptance tests. Whole-module +acceptance remains open beyond these verified operations. diff --git a/src/compat/stream-web/ponyfill.ts b/src/compat/stream-web/ponyfill.ts index c9d9c840..75bf72a0 100644 --- a/src/compat/stream-web/ponyfill.ts +++ b/src/compat/stream-web/ponyfill.ts @@ -1,3 +1,3 @@ // The build bundles this dependency into an owned ESM asset so raw browser // consumers do not need a package resolver or an import map. -export * from "web-streams-polyfill"; +export * from "./workers-ponyfill.js"; diff --git a/src/compat/stream-web/runtime.ts b/src/compat/stream-web/runtime.ts index b61913d8..d142481a 100644 --- a/src/compat/stream-web/runtime.ts +++ b/src/compat/stream-web/runtime.ts @@ -1,9 +1,9 @@ import { contextConstructor } from "./context.js"; import * as ponyfill from "./ponyfill.js"; import { nativeStreams } from "../../shared/globals/platform.js"; -// iOS 26.1 has default streams but no readable byte streams or BYOB readers. -export const usesPonyfill = - typeof nativeStreams.ReadableByteStreamController !== "function"; +// Use owned Streams on every host: native Streams differ from Workers +// in tee cancellation and pending pipeTo abort behavior. +export const usesPonyfill = true; export const ReadableStream: typeof globalThis.ReadableStream = contextConstructor( (usesPonyfill diff --git a/src/compat/stream-web/workers-ponyfill.build.json b/src/compat/stream-web/workers-ponyfill.build.json new file mode 100644 index 00000000..0e815e6f --- /dev/null +++ b/src/compat/stream-web/workers-ponyfill.build.json @@ -0,0 +1,8 @@ +{ + "revision": "12b4fedec6c01503a137d837204ca0b85992461e", + "changes": { + "pipe": "a8afacd41619c08bcd9237a57840473cd945a896c14bb5ac25f3bf57fb801944", + "tee": "1df4fe916895d4331ce3d9e35ba39e418616565e4bceae4ad1a8730f7c61cfe6" + }, + "sha256": "087a485b462ed99adf68d36ae114eacdc2a15f6fd28fb30ab9d95f52c3ca7a17" +} diff --git a/src/compat/stream-web/workers-ponyfill.d.ts b/src/compat/stream-web/workers-ponyfill.d.ts new file mode 100644 index 00000000..76253a5f --- /dev/null +++ b/src/compat/stream-web/workers-ponyfill.d.ts @@ -0,0 +1 @@ +export * from "web-streams-polyfill"; diff --git a/src/compat/stream-web/workers-ponyfill.js b/src/compat/stream-web/workers-ponyfill.js new file mode 100644 index 00000000..89eabab9 --- /dev/null +++ b/src/compat/stream-web/workers-ponyfill.js @@ -0,0 +1,3 @@ +// Generated from web-streams-polyfill 4.3.0 (MIT), with pinned Workers cancellation semantics. +// Rebuild using scripts/build-workers-streams.mjs; see LICENSE. +var yn=Object.defineProperty;var a=(e,r)=>yn(e,"name",{value:r,configurable:!0});function q(){}a(q,"noop");var pn=q,i=pn,Kr=q;function R(e){return typeof e=="object"&&e!==null||typeof e=="function"}a(R,"typeIsObject");var Xr=q;function y(e,r){try{Object.defineProperty(e,"name",{value:r,configurable:!0})}catch{}}a(y,"setFunctionName");var Zr=Promise,Sn=Promise.resolve.bind(Zr),Rn=Promise.prototype.then,hn=Promise.reject.bind(Zr),Re=Sn;function _(e){return new Zr(e)}a(_,"newPromise");function m(e){return _(r=>r(e))}a(m,"promiseResolvedWith");function u(e){return hn(e)}a(u,"promiseRejectedWith");function j(e,r,t){return Rn.call(e,r,t)}a(j,"PerformPromiseThen");function w(e,r,t){j(j(e,r,t),void 0,Xr)}a(w,"uponPromise");function Xe(e,r){w(e,r)}a(Xe,"uponFulfillment");function Ze(e,r){w(e,void 0,r)}a(Ze,"uponRejection");function L(e,r,t){return j(e,r,t)}a(L,"transformPromiseWith");function X(e){j(e,void 0,Xr)}a(X,"setPromiseIsHandledToTrue");var ye=a(e=>{if(typeof queueMicrotask=="function")ye=queueMicrotask;else{let r=m(void 0);ye=a(t=>j(r,t),"_queueMicrotask")}return ye(e)},"_queueMicrotask");function Z(e,r,t){if(typeof e!="function")throw new TypeError("Argument is not a function");return Function.prototype.apply.call(e,r,t)}a(Z,"reflectCall");function Q(e,r,t){i(typeof e=="function"),i(r!==void 0),i(Array.isArray(t));try{return m(Z(e,r,t))}catch(n){return u(n)}}a(Q,"promiseCall");var cr=16384,P=class{constructor(){this._cursor=0;this._size=0;this._front={_elements:[],_next:void 0},this._back=this._front,this._cursor=0,this._size=0}static{a(this,"SimpleQueue")}get length(){return this._size}push(r){let t=this._back,n=t;i(t._next===void 0),t._elements.length===cr-1&&(n={_elements:[],_next:void 0}),t._elements.push(r),n!==t&&(this._back=n,t._next=n),++this._size}shift(){i(this._size>0);let r=this._front,t=r,n=this._cursor,o=n+1,l=r._elements,s=l[n];return o===cr&&(i(l.length===cr),i(r._next!==void 0),t=r._next,o=0),--this._size,this._cursor=o,r!==t&&(this._front=t),l[n]=void 0,s}forEach(r){let t=this._cursor,n=this._front,o=n._elements;for(;(t!==o.length||n._next!==void 0)&&!(t===o.length&&(i(n._next!==void 0),i(t===cr),n=n._next,o=n._elements,t=0,o.length===0));)r(o[t]),++t}peek(){i(this._size>0);let r=this._front,t=this._cursor;return r._elements[t]}};var Jr=Symbol("[[AbortSteps]]"),et=Symbol("[[ErrorSteps]]"),je=Symbol("[[CancelSteps]]"),Fe=Symbol("[[PullSteps]]"),ze=Symbol("[[CanPullSyncSteps]]"),Ue=Symbol("[[ReleaseSteps]]");function br(e,r){e._ownerReadableStream=r,r._reader=e,r._state==="readable"?rt(e):r._state==="closed"?_n(e):(i(r._state==="errored"),Kt(e,r._storedError))}a(br,"ReadableStreamReaderGenericInitialize");function Le(e,r){let t=e._ownerReadableStream;return i(t!==void 0),z(t,r)}a(Le,"ReadableStreamReaderGenericCancel");function F(e){let r=e._ownerReadableStream;i(r!==void 0),i(r._reader===e),r._state==="readable"?yr(e,new TypeError("Reader was released and can no longer be used to monitor the stream's closedness")):gn(e,new TypeError("Reader was released and can no longer be used to monitor the stream's closedness")),r._readableStreamController[Ue](),r._reader=void 0,e._ownerReadableStream=void 0}a(F,"ReadableStreamReaderGenericRelease");function Qe(e){return new TypeError("Cannot "+e+" a stream using a released reader")}a(Qe,"readerLockException");function rt(e){e._closedPromise=_((r,t)=>{e._closedPromise_resolve=r,e._closedPromise_reject=t})}a(rt,"defaultReaderClosedPromiseInitialize");function Kt(e,r){rt(e),yr(e,r)}a(Kt,"defaultReaderClosedPromiseInitializeAsRejected");function _n(e){rt(e),tt(e)}a(_n,"defaultReaderClosedPromiseInitializeAsResolved");function yr(e,r){e._closedPromise_reject!==void 0&&(X(e._closedPromise),e._closedPromise_reject(r),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0)}a(yr,"defaultReaderClosedPromiseReject");function gn(e,r){i(e._closedPromise_resolve===void 0),i(e._closedPromise_reject===void 0),Kt(e,r)}a(gn,"defaultReaderClosedPromiseResetToRejected");function tt(e){e._closedPromise_resolve!==void 0&&(e._closedPromise_resolve(void 0),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0)}a(tt,"defaultReaderClosedPromiseResolve");var Cn=Number.isFinite||function(e){return typeof e=="number"&&isFinite(e)},at=Cn;var Tn=Math.trunc||function(e){return e<0?Math.ceil(e):Math.floor(e)},Xt=Tn;function Pn(e){return typeof e=="object"||typeof e=="function"}a(Pn,"isDictionary");function v(e,r){if(e!==void 0&&!Pn(e))throw new TypeError(`${r} is not an object.`)}a(v,"assertDictionary");function k(e,r){if(typeof e!="function")throw new TypeError(`${r} is not a function.`)}a(k,"assertFunction");function wn(e){return typeof e=="object"&&e!==null||typeof e=="function"}a(wn,"isObject");function pr(e,r){if(!wn(e))throw new TypeError(`${r} is not an object.`)}a(pr,"assertObject");function I(e,r,t){if(e===void 0)throw new TypeError(`Parameter ${r} is required in '${t}'.`)}a(I,"assertRequiredArgument");function Je(e,r,t){if(e===void 0)throw new TypeError(`${r} is required in '${t}'.`)}a(Je,"assertRequiredField");function er(e){return Number(e)}a(er,"convertUnrestrictedDouble");function Zt(e){return e===0?0:e}a(Zt,"censorNegativeZero");function vn(e){return Zt(Xt(e))}a(vn,"integerPart");function Ve(e,r){let n=Number.MAX_SAFE_INTEGER,o=Number(e);if(o=Zt(o),!at(o))throw new TypeError(`${r} is not a finite number`);if(o=vn(o),o<0||o>n)throw new TypeError(`${r} is outside the accepted range of 0 to ${n}, inclusive`);return!at(o)||o===0?0:o}a(Ve,"convertUnsignedLongLongWithEnforceRange");function Me(e,r){if(!W(e))throw new TypeError(`${r} is not a ReadableStream.`)}a(Me,"assertReadableStream");function se(e){return new J(e)}a(se,"AcquireReadableStreamDefaultReader");function Rr(e,r){i(V(e._reader)),i(e._state==="readable"),e._reader._readRequests.push(r)}a(Rr,"ReadableStreamAddReadRequest");function rr(e,r,t){let n=e._reader;i(n._readRequests.length>0);let o=n._readRequests.shift();t?o._closeSteps():o._chunkSteps(r)}a(rr,"ReadableStreamFulfillReadRequest");function Te(e){return e._reader._readRequests.length}a(Te,"ReadableStreamGetNumReadRequests");function hr(e){let r=e._reader;return!(r===void 0||!V(r))}a(hr,"ReadableStreamHasDefaultReader");var J=class{static{a(this,"ReadableStreamDefaultReader")}constructor(r){if(I(r,1,"ReadableStreamDefaultReader"),Me(r,"First parameter"),D(r))throw new TypeError("This stream has already been locked for exclusive reading by another reader");br(this,r),this._readRequests=new P}get closed(){return V(this)?this._closedPromise:u(Sr("closed"))}cancel(r=void 0){return V(this)?this._ownerReadableStream===void 0?u(Qe("cancel")):Le(this,r):u(Sr("cancel"))}read(){if(!V(this))return u(Sr("read"));if(this._ownerReadableStream===void 0)return u(Qe("read from"));let r=it(this)?new ot:new nt;return pe(this,r),i(r._promise!==void 0),r._promise}releaseLock(){if(!V(this))throw Sr("releaseLock");this._ownerReadableStream!==void 0&&Bn(this)}};Object.defineProperties(J.prototype,{cancel:{enumerable:!0},read:{enumerable:!0},releaseLock:{enumerable:!0},closed:{enumerable:!0}});y(J.prototype.cancel,"cancel");y(J.prototype.read,"read");y(J.prototype.releaseLock,"releaseLock");typeof Symbol.toStringTag=="symbol"&&Object.defineProperty(J.prototype,Symbol.toStringTag,{value:"ReadableStreamDefaultReader",configurable:!0});var nt=class{static{a(this,"DefaultReadRequest")}constructor(){this._promise=_((r,t)=>{this._resolvePromise=r,this._rejectPromise=t})}_chunkSteps(r){this._resolvePromise({value:r,done:!1})}_closeSteps(){this._resolvePromise({value:void 0,done:!0})}_errorSteps(r){this._rejectPromise(r)}},ot=class{constructor(){this._promise=void 0}static{a(this,"SyncDefaultReadRequest")}_chunkSteps(r){i(this._promise===void 0),this._promise=Re({value:r,done:!1})}_closeSteps(){i(this._promise===void 0),this._promise=Re({value:void 0,done:!0})}_errorSteps(r){i(this._promise===void 0),this._promise=u(r)}};function V(e){return!R(e)||!Object.prototype.hasOwnProperty.call(e,"_readRequests")?!1:e instanceof J}a(V,"IsReadableStreamDefaultReader");function pe(e,r){let t=e._ownerReadableStream;i(t!==void 0),t._disturbed=!0,t._state==="closed"?r._closeSteps():t._state==="errored"?r._errorSteps(t._storedError):(i(t._state==="readable"),t._readableStreamController[Fe](r))}a(pe,"ReadableStreamDefaultReaderRead");function it(e){let r=e._ownerReadableStream;return i(r!==void 0),r._state==="closed"||r._state==="errored"?!0:(i(r._state==="readable"),r._readableStreamController[ze]())}a(it,"ReadableStreamDefaultReaderCanReadSync");function Bn(e){F(e);let r=new TypeError("Reader was released");lt(e,r)}a(Bn,"ReadableStreamDefaultReaderRelease");function lt(e,r){let t=e._readRequests;e._readRequests=new P,t.forEach(n=>{n._errorSteps(r)})}a(lt,"ReadableStreamDefaultReaderErrorReadRequests");function Sr(e){return new TypeError(`ReadableStreamDefaultReader.prototype.${e} can only be used on a ReadableStreamDefaultReader`)}a(Sr,"defaultReaderBrandCheckException");function ea(e){return e.slice()}a(ea,"CreateArrayFromList");function ra(e,r,t,n,o){return e!==t&&!O(e)&&!O(t)&&r+o<=e.byteLength&&n+o<=t.byteLength}a(ra,"CanCopyDataBlockBytes");function st(e,r,t,n,o){new Uint8Array(e).set(new Uint8Array(t,n,o),r)}a(st,"CopyDataBlockBytes");var ue=a(e=>(typeof e.transfer=="function"?ue=a(r=>r.transfer(),"TransferArrayBuffer"):typeof structuredClone=="function"?ue=a(r=>structuredClone(r,{transfer:[r]}),"TransferArrayBuffer"):ue=a(r=>r,"TransferArrayBuffer"),ue(e)),"TransferArrayBuffer");function ta(e){return!O(e)}a(ta,"CanTransferArrayBuffer");var O=a(e=>(typeof e.detached=="boolean"?O=a(r=>r.detached,"IsDetachedBuffer"):O=a(r=>r.byteLength===0,"IsDetachedBuffer"),O(e)),"IsDetachedBuffer");function _r(e,r,t){if(e.slice)return e.slice(r,t);let n=t-r,o=new ArrayBuffer(n);return st(o,0,e,r,n),o}a(_r,"ArrayBufferSlice");function Ne(e,r){let t=e[r];if(t!=null){if(typeof t!="function")throw new TypeError(`${String(r)} is not a function`);return t}}a(Ne,"GetMethod");function An(e){let r={next(){let n;try{n=dt(e)}catch(o){return u(o)}return Jt(n)},return(n){let o;try{let l=Ne(e.iterator,"return");if(l===void 0)return m({done:!0,value:n});o=Z(l,e.iterator,[n])}catch(l){return u(l)}return R(o)?Jt(o):u(new TypeError("The iterator.return() method must return an object"))}},t=r.next;return{iterator:r,nextMethod:t,done:!1}}a(An,"CreateAsyncFromSyncIterator");function Jt(e){try{let r=e.done,t=e.value,n=Re(t);return j(n,o=>({done:r,value:o}))}catch(r){return u(r)}}a(Jt,"AsyncFromSyncIteratorContinuation");var Pe=Symbol.asyncIterator??Symbol.for?.("Symbol.asyncIterator")??"@@asyncIterator";function ut(e,r="sync",t){if(i(r==="sync"||r==="async"),t===void 0)if(r==="async"){if(t=Ne(e,Pe),t===void 0){let l=Ne(e,Symbol.iterator),s=ut(e,"sync",l);return An(s)}}else t=Ne(e,Symbol.iterator);if(t===void 0)throw new TypeError("The object is not iterable");let n=Z(t,e,[]);if(!R(n))throw new TypeError("The iterator method must return an object");let o=n.next;return{iterator:n,nextMethod:o,done:!1}}a(ut,"GetIterator");function dt(e){let r=Z(e.nextMethod,e.iterator,[]);if(!R(r))throw new TypeError("The iterator.next() method must return an object");return r}a(dt,"IteratorNext");var gr=class{constructor(r,t){this._ongoingPromise=void 0;this._isFinished=!1;this._reader=r,this._preventCancel=t}static{a(this,"ReadableStreamAsyncIteratorImpl")}next(){let r=a(()=>this._nextSteps(),"nextSteps");return this._ongoingPromise=this._ongoingPromise?L(this._ongoingPromise,r,r):r(),this._ongoingPromise}return(r){let t=a(()=>this._returnSteps(r),"returnSteps");return this._ongoingPromise=this._ongoingPromise?L(this._ongoingPromise,t,t):t(),this._ongoingPromise}_nextSteps(){if(this._isFinished)return Promise.resolve({value:void 0,done:!0});let r=this._reader;i(r._ownerReadableStream!==void 0);let t=new ft(this);return pe(r,t),t._promise}_returnSteps(r){if(this._isFinished)return Promise.resolve({value:r,done:!0});this._isFinished=!0;let t=this._reader;if(i(t._ownerReadableStream!==void 0),i(t._readRequests.length===0),!this._preventCancel){let n=Le(t,r);return F(t),L(n,()=>({value:r,done:!0}))}return F(t),m({value:r,done:!0})}},ft=class{static{a(this,"AsyncIteratorReadRequest")}constructor(r){this._iterator=r,this._promise=_((t,n)=>{this._resolvePromise=t,this._rejectPromise=n})}_chunkSteps(r){let t=this._iterator;t._ongoingPromise=void 0,ye(()=>this._resolvePromise({value:r,done:!1}))}_closeSteps(){let r=this._iterator;r._ongoingPromise=void 0,r._isFinished=!0,F(r._reader),this._resolvePromise({value:void 0,done:!0})}_errorSteps(r){let t=this._iterator;t._ongoingPromise=void 0,t._isFinished=!0,F(t._reader),this._rejectPromise(r)}},oa={next(){return aa(this)?this._asyncIteratorImpl.next():u(na("next"))},return(e){return aa(this)?this._asyncIteratorImpl.return(e):u(na("return"))},[Pe](){return this}};Object.defineProperty(oa,Pe,{enumerable:!1});function ia(e,r){let t=se(e),n=new gr(t,r),o=Object.create(oa);return o._asyncIteratorImpl=n,o}a(ia,"AcquireReadableStreamAsyncIterator");function aa(e){if(!R(e)||!Object.prototype.hasOwnProperty.call(e,"_asyncIteratorImpl"))return!1;try{return e._asyncIteratorImpl instanceof gr}catch{return!1}}a(aa,"IsReadableStreamAsyncIterator");function na(e){return new TypeError(`ReadableStreamAsyncIterator.${e} can only be used on a ReadableSteamAsyncIterator`)}a(na,"streamAsyncIteratorBrandCheckException");var Wn=Number.isNaN||function(e){return e!==e},Cr=Wn;function we(e){return!(typeof e!="number"||Cr(e)||e<0)}a(we,"IsNonNegativeNumber");function mt(e){let r=_r(e.buffer,e.byteOffset,e.byteOffset+e.byteLength);return new Uint8Array(r)}a(mt,"CloneAsUint8Array");function tr(e){i("_queue"in e&&"_queueTotalSize"in e),i(e._queue.length>0);let r=e._queue.shift();return e._queueTotalSize-=r.size,e._queueTotalSize<0&&(e._queueTotalSize=0),r.value}a(tr,"DequeueValue");function ar(e,r,t){if(i("_queue"in e&&"_queueTotalSize"in e),!we(t)||t===1/0)throw new RangeError("Size must be a finite, non-NaN, non-negative number.");e._queue.push({value:r,size:t}),e._queueTotalSize+=t}a(ar,"EnqueueValueWithSize");function la(e){return i("_queue"in e&&"_queueTotalSize"in e),i(e._queue.length>0),e._queue.peek().value}a(la,"PeekQueueValue");function ee(e){i("_queue"in e&&"_queueTotalSize"in e),e._queue=new P,e._queueTotalSize=0}a(ee,"ResetQueue");var kn=Number.isInteger||function(e){return typeof e=="number"&&isFinite(e)&&Math.floor(e)===e},sa=kn;function ua(e){return e===DataView}a(ua,"isDataViewConstructor");function da(e){return ua(e.constructor)}a(da,"isDataView");function ct(e){return ua(e)?1:e.BYTES_PER_ELEMENT}a(ct,"arrayBufferViewElementSize");var de=class{static{a(this,"ReadableStreamBYOBRequest")}constructor(){throw new TypeError("Illegal constructor")}get view(){if(!bt(this))throw yt("view");return this._view}respond(r){if(!bt(this))throw yt("respond");if(I(r,1,"respond"),r=Ve(r,"First parameter"),this._associatedReadableByteStreamController===void 0)throw new TypeError("This BYOB request has been invalidated");if(O(this._view.buffer))throw new TypeError("The BYOB request's buffer has been detached and so cannot be used as a response");i(this._view.byteLength>0),i(this._view.buffer.byteLength>0),ir(this._associatedReadableByteStreamController,r)}respondWithNewView(r){if(!bt(this))throw yt("respondWithNewView");if(I(r,1,"respondWithNewView"),!ArrayBuffer.isView(r))throw new TypeError("You can only respond with array buffer views");if(this._associatedReadableByteStreamController===void 0)throw new TypeError("This BYOB request has been invalidated");if(O(r.buffer))throw new TypeError("The given view's buffer has been detached and so cannot be used as a response");lr(this._associatedReadableByteStreamController,r)}};Object.defineProperties(de.prototype,{respond:{enumerable:!0},respondWithNewView:{enumerable:!0},view:{enumerable:!0}});y(de.prototype.respond,"respond");y(de.prototype.respondWithNewView,"respondWithNewView");typeof Symbol.toStringTag=="symbol"&&Object.defineProperty(de.prototype,Symbol.toStringTag,{value:"ReadableStreamBYOBRequest",configurable:!0});var Y=class{static{a(this,"ReadableByteStreamController")}constructor(){throw new TypeError("Illegal constructor")}get byobRequest(){if(!re(this))throw nr("byobRequest");return vr(this)}get desiredSize(){if(!re(this))throw nr("desiredSize");return Ta(this)}close(){if(!re(this))throw nr("close");if(this._closeRequested)throw new TypeError("The stream has already been closed; do not close it again!");let r=this._controlledReadableByteStream._state;if(r!=="readable")throw new TypeError(`The stream (in ${r} state) is not in the readable state and cannot be closed`);$e(this)}enqueue(r){if(!re(this))throw nr("enqueue");if(I(r,1,"enqueue"),!ArrayBuffer.isView(r))throw new TypeError("chunk must be an array buffer view");if(r.byteLength===0)throw new TypeError("chunk must have non-zero byteLength");if(r.buffer.byteLength===0)throw new TypeError("chunk's buffer must have non-zero byteLength");if(this._closeRequested)throw new TypeError("stream is closed or draining");let t=this._controlledReadableByteStream._state;if(t!=="readable")throw new TypeError(`The stream (in ${t} state) is not in the readable state and cannot be enqueued to`);or(this,r)}error(r=void 0){if(!re(this))throw nr("error");M(this,r)}[je](r){fa(this),ee(this);let t=this._cancelAlgorithm(r);return wr(this),t}[Fe](r){let t=this._controlledReadableByteStream;if(i(hr(t)),this._queueTotalSize>0){i(Te(t)===0),Ca(this,r);return}let n=this._autoAllocateChunkSize;if(n!==void 0){let o;try{o=new ArrayBuffer(n)}catch(s){r._errorSteps(s);return}let l={buffer:o,bufferByteLength:n,byteOffset:0,byteLength:n,bytesFilled:0,minimumFill:1,elementSize:1,viewConstructor:Uint8Array,readerType:"default"};this._pendingPullIntos.push(l)}Rr(t,r),ve(this)}[ze](){return this._queueTotalSize>0}[Ue](){if(this._pendingPullIntos.length>0){let r=this._pendingPullIntos.peek();r.readerType="none",this._pendingPullIntos=new P,this._pendingPullIntos.push(r)}}};Object.defineProperties(Y.prototype,{close:{enumerable:!0},enqueue:{enumerable:!0},error:{enumerable:!0},byobRequest:{enumerable:!0},desiredSize:{enumerable:!0}});y(Y.prototype.close,"close");y(Y.prototype.enqueue,"enqueue");y(Y.prototype.error,"error");typeof Symbol.toStringTag=="symbol"&&Object.defineProperty(Y.prototype,Symbol.toStringTag,{value:"ReadableByteStreamController",configurable:!0});function re(e){return!R(e)||!Object.prototype.hasOwnProperty.call(e,"_controlledReadableByteStream")?!1:e instanceof Y}a(re,"IsReadableByteStreamController");function bt(e){return!R(e)||!Object.prototype.hasOwnProperty.call(e,"_associatedReadableByteStreamController")?!1:e instanceof de}a(bt,"IsReadableStreamBYOBRequest");function ve(e){if(!Dn(e))return;if(e._pulling){e._pullAgain=!0;return}i(!e._pullAgain),e._pulling=!0;let t=e._pullAlgorithm();w(t,()=>(e._pulling=!1,e._pullAgain&&(e._pullAgain=!1,ve(e)),null),n=>(M(e,n),null))}a(ve,"ReadableByteStreamControllerCallPullIfNeeded");function fa(e){Rt(e),e._pendingPullIntos=new P}a(fa,"ReadableByteStreamControllerClearPendingPullIntos");function ma(e,r){i(e._state!=="errored"),i(r.readerType!=="none");let t=!1;e._state==="closed"&&(i(r.bytesFilled%r.elementSize===0),t=!0);let n=ca(r);r.readerType==="default"?rr(e,n,t):(i(r.readerType==="byob"),wa(e,n,t))}a(ma,"ReadableByteStreamControllerCommitPullIntoDescriptor");function Pr(e,r){for(let t=0;t0&&ba(e,r.buffer,r.byteOffset,r.bytesFilled),Ye(e)}a(ya,"ReadableByteStreamControllerEnqueueDetachedPullIntoToQueue");function pa(e,r){let t=Math.min(e._queueTotalSize,r.byteLength-r.bytesFilled),n=r.bytesFilled+t,o=t,l=!1;i(!O(r.buffer)),i(r.bytesFilled=r.minimumFill&&(o=d-r.bytesFilled,l=!0);let f=e._queue;for(;o>0;){let c=f.peek(),b=Math.min(o,c.byteLength),S=r.byteOffset+r.bytesFilled,h=r.buffer,E=c.buffer,oe=c.byteOffset;i(ra(h,S,E,oe,b)),st(h,S,E,oe,b),c.byteLength===b?f.shift():(c.byteOffset+=b,c.byteLength-=b),e._queueTotalSize-=b,Sa(e,b,r),o-=b}return l||(i(e._queueTotalSize===0),i(r.bytesFilled>0),i(r.bytesFilled0&&e._queueTotalSize!==0;){let t=e._pendingPullIntos.peek();i(t.readerType!=="none"),pa(e,t)&&(Ye(e),r.push(t))}return r}a(pt,"ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue");function In(e){let r=e._controlledReadableByteStream._reader;for(i(V(r));r._readRequests.length>0;){if(e._queueTotalSize===0)return;let t=r._readRequests.shift();Ca(e,t)}}a(In,"ReadableByteStreamControllerProcessReadRequestsUsingQueue");function ha(e,r,t,n){let o=e._controlledReadableByteStream,l=r.constructor,s=ct(l),{byteOffset:d,byteLength:f}=r,c=t*s;i(c>=s&&c<=f),i(c%s===0);let b;try{b=ue(r.buffer)}catch(h){n._errorSteps(h);return}let S={buffer:b,bufferByteLength:b.byteLength,byteOffset:d,byteLength:f,bytesFilled:0,minimumFill:c,elementSize:s,viewConstructor:l,readerType:"byob"};if(e._pendingPullIntos.length>0){e._pendingPullIntos.push(S),St(o,n);return}if(o._state==="closed"){let h=new l(S.buffer,S.byteOffset,0);n._closeSteps(h);return}if(e._queueTotalSize>0){if(pa(e,S)){let h=ca(S);Ra(e),n._chunkSteps(h);return}if(e._closeRequested){let h=new TypeError("Insufficient bytes to fill elements in the given buffer");M(e,h),n._errorSteps(h);return}}e._pendingPullIntos.push(S),St(o,n),ve(e)}a(ha,"ReadableByteStreamControllerPullInto");function _a(e,r,t){let n=e._controlledReadableByteStream,o=r.constructor,l=ct(o),{byteLength:s}=r,d=t*l;return i(d>=l&&d<=s),i(d%l===0),e._pendingPullIntos.length>0?!1:n._state==="closed"?!0:e._queueTotalSize>=d}a(_a,"ReadableByteStreamControllerCanPullIntoSync");function On(e,r){i(r.bytesFilled%r.elementSize===0),r.readerType==="none"&&Ye(e);let t=e._controlledReadableByteStream;if(Br(t)){let n=[];for(;n.length<_t(t);)n.push(Ye(e));Pr(t,n)}}a(On,"ReadableByteStreamControllerRespondInClosedState");function qn(e,r,t){if(i(t.bytesFilled+r<=t.byteLength),Sa(e,r,t),t.readerType==="none"){ya(e,t);let l=pt(e);Pr(e._controlledReadableByteStream,l);return}if(t.bytesFilled0){let l=t.byteOffset+t.bytesFilled;ba(e,t.buffer,l-n,n)}t.bytesFilled-=n;let o=pt(e);ma(e._controlledReadableByteStream,t),Pr(e._controlledReadableByteStream,o)}a(qn,"ReadableByteStreamControllerRespondInReadableState");function ga(e,r){let t=e._pendingPullIntos.peek();i(ta(t.buffer)),Rt(e);let n=e._controlledReadableByteStream._state;n==="closed"?(i(r===0),On(e,t)):(i(n==="readable"),i(r>0),qn(e,r,t)),ve(e)}a(ga,"ReadableByteStreamControllerRespondInternal");function Ye(e){return i(e._byobRequest===null),e._pendingPullIntos.shift()}a(Ye,"ReadableByteStreamControllerShiftPendingPullInto");function Dn(e){let r=e._controlledReadableByteStream;if(r._state!=="readable"||e._closeRequested||!e._started)return!1;if(hr(r)&&Te(r)>0||Br(r)&&_t(r)>0)return!0;let t=Ta(e);return i(t!==null),t>0}a(Dn,"ReadableByteStreamControllerShouldCallPull");function wr(e){e._pullAlgorithm=void 0,e._cancelAlgorithm=void 0}a(wr,"ReadableByteStreamControllerClearAlgorithms");function $e(e){let r=e._controlledReadableByteStream;if(!(e._closeRequested||r._state!=="readable")){if(e._queueTotalSize>0){e._closeRequested=!0;return}if(e._pendingPullIntos.length>0){let t=e._pendingPullIntos.peek();if(t.bytesFilled%t.elementSize!==0){let n=new TypeError("Insufficient bytes to fill elements in the given buffer");throw M(e,n),n}}wr(e),Be(r)}}a($e,"ReadableByteStreamControllerClose");function or(e,r){let t=e._controlledReadableByteStream;if(e._closeRequested||t._state!=="readable")return;let{buffer:n,byteOffset:o,byteLength:l}=r;if(O(n))throw new TypeError("chunk's buffer is detached and so cannot be enqueued");let s=ue(n);if(e._pendingPullIntos.length>0){let d=e._pendingPullIntos.peek();if(O(d.buffer))throw new TypeError("The BYOB request's buffer has been detached and so cannot be filled with an enqueued chunk");Rt(e),d.buffer=ue(d.buffer),d.readerType==="none"&&ya(e,d)}if(hr(t))if(In(e),Te(t)===0)i(e._pendingPullIntos.length===0),Tr(e,s,o,l);else{i(e._queue.length===0),e._pendingPullIntos.length>0&&(i(e._pendingPullIntos.peek().readerType==="default"),Ye(e));let d=new Uint8Array(s,o,l);rr(t,d,!1)}else if(Br(t)){Tr(e,s,o,l);let d=pt(e);Pr(t,d)}else i(!D(t)),Tr(e,s,o,l);ve(e)}a(or,"ReadableByteStreamControllerEnqueue");function M(e,r){let t=e._controlledReadableByteStream;t._state==="readable"&&(fa(e),ee(e),wr(e),Ar(t,r))}a(M,"ReadableByteStreamControllerError");function Ca(e,r){i(e._queueTotalSize>0);let t=e._queue.shift();e._queueTotalSize-=t.byteLength,Ra(e);let n=new Uint8Array(t.buffer,t.byteOffset,t.byteLength);r._chunkSteps(n)}a(Ca,"ReadableByteStreamControllerFillReadRequestFromQueue");function vr(e){if(e._byobRequest===null&&e._pendingPullIntos.length>0){let r=e._pendingPullIntos.peek(),t=new Uint8Array(r.buffer,r.byteOffset+r.bytesFilled,r.byteLength-r.bytesFilled),n=Object.create(de.prototype);En(n,e,t),e._byobRequest=n}return e._byobRequest}a(vr,"ReadableByteStreamControllerGetBYOBRequest");function Ta(e){let r=e._controlledReadableByteStream._state;return r==="errored"?null:r==="closed"?0:e._strategyHWM-e._queueTotalSize}a(Ta,"ReadableByteStreamControllerGetDesiredSize");function ir(e,r){i(e._pendingPullIntos.length>0);let t=e._pendingPullIntos.peek(),n=e._controlledReadableByteStream._state;if(n==="closed"){if(r!==0)throw new TypeError("bytesWritten must be 0 when calling respond() on a closed stream")}else{if(i(n==="readable"),r===0)throw new TypeError("bytesWritten must be greater than 0 when calling respond() on a readable stream");if(t.bytesFilled+r>t.byteLength)throw new RangeError("bytesWritten out of range")}t.buffer=ue(t.buffer),ga(e,r)}a(ir,"ReadableByteStreamControllerRespond");function lr(e,r){i(e._pendingPullIntos.length>0),i(!O(r.buffer));let t=e._pendingPullIntos.peek(),n=e._controlledReadableByteStream._state;if(n==="closed"){if(r.byteLength!==0)throw new TypeError("The view's length must be 0 when calling respondWithNewView() on a closed stream")}else if(i(n==="readable"),r.byteLength===0)throw new TypeError("The view's length must be greater than 0 when calling respondWithNewView() on a readable stream");if(t.byteOffset+t.bytesFilled!==r.byteOffset)throw new RangeError("The region specified by view does not match byobRequest");if(t.bufferByteLength!==r.buffer.byteLength)throw new RangeError("The buffer of view has different capacity than byobRequest");if(t.bytesFilled+r.byteLength>t.byteLength)throw new RangeError("The region specified by view is larger than byobRequest");let o=r.byteLength;t.buffer=ue(r.buffer),ga(e,o)}a(lr,"ReadableByteStreamControllerRespondWithNewView");function ht(e,r,t,n,o,l,s){i(e._readableStreamController===void 0),s!==void 0&&(i(sa(s)),i(s>0)),r._controlledReadableByteStream=e,r._pullAgain=!1,r._pulling=!1,r._byobRequest=null,r._queue=r._queueTotalSize=void 0,ee(r),r._closeRequested=!1,r._started=!1,r._strategyHWM=l,r._pullAlgorithm=n,r._cancelAlgorithm=o,r._autoAllocateChunkSize=s,r._pendingPullIntos=new P,e._readableStreamController=r;let d=t();w(m(d),()=>(r._started=!0,i(!r._pulling),i(!r._pullAgain),ve(r),null),f=>(M(r,f),null))}a(ht,"SetUpReadableByteStreamController");function Pa(e,r,t){let n=Object.create(Y.prototype),o,l,s;r.start!==void 0?o=a(()=>r.start(n),"startAlgorithm"):o=a(()=>{},"startAlgorithm"),r.pull!==void 0?l=a(()=>r.pull(n),"pullAlgorithm"):l=a(()=>m(void 0),"pullAlgorithm"),r.cancel!==void 0?s=a(f=>r.cancel(f),"cancelAlgorithm"):s=a(()=>m(void 0),"cancelAlgorithm");let d=r.autoAllocateChunkSize;if(d===0)throw new TypeError("autoAllocateChunkSize must be greater than 0");ht(e,n,o,l,s,t,d)}a(Pa,"SetUpReadableByteStreamControllerFromUnderlyingSource");function En(e,r,t){i(re(r)),i(typeof t=="object"),i(ArrayBuffer.isView(t)),i(!O(t.buffer)),e._associatedReadableByteStreamController=r,e._view=t}a(En,"SetUpReadableStreamBYOBRequest");function yt(e){return new TypeError(`ReadableStreamBYOBRequest.prototype.${e} can only be used on a ReadableStreamBYOBRequest`)}a(yt,"byobRequestBrandCheckException");function nr(e){return new TypeError(`ReadableByteStreamController.prototype.${e} can only be used on a ReadableByteStreamController`)}a(nr,"byteStreamControllerBrandCheckException");function va(e,r){v(e,r);let t=e?.mode;return{mode:t===void 0?void 0:xn(t,`${r} has member 'mode' that`)}}a(va,"convertReaderOptions");function xn(e,r){if(e=`${e}`,e!=="byob")throw new TypeError(`${r} '${e}' is not a valid enumeration value for ReadableStreamReaderMode`);return e}a(xn,"convertReadableStreamReaderMode");function Ba(e,r){v(e,r);let t=e?.min??1;return{min:Ve(t,`${r} has member 'min' that`)}}a(Ba,"convertByobReadOptions");function kr(e){return new ae(e)}a(kr,"AcquireReadableStreamBYOBReader");function St(e,r){i(te(e._reader)),i(e._state==="readable"||e._state==="closed"),e._reader._readIntoRequests.push(r)}a(St,"ReadableStreamAddReadIntoRequest");function wa(e,r,t){let n=e._reader;i(n._readIntoRequests.length>0);let o=n._readIntoRequests.shift();t?o._closeSteps(r):o._chunkSteps(r)}a(wa,"ReadableStreamFulfillReadIntoRequest");function _t(e){return e._reader._readIntoRequests.length}a(_t,"ReadableStreamGetNumReadIntoRequests");function Br(e){let r=e._reader;return!(r===void 0||!te(r))}a(Br,"ReadableStreamHasBYOBReader");var ae=class{static{a(this,"ReadableStreamBYOBReader")}constructor(r){if(I(r,1,"ReadableStreamBYOBReader"),Me(r,"First parameter"),D(r))throw new TypeError("This stream has already been locked for exclusive reading by another reader");if(!re(r._readableStreamController))throw new TypeError("Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte source");br(this,r),this._readIntoRequests=new P}get closed(){return te(this)?this._closedPromise:u(Wr("closed"))}cancel(r=void 0){return te(this)?this._ownerReadableStream===void 0?u(Qe("cancel")):Le(this,r):u(Wr("cancel"))}read(r,t={}){if(!te(this))return u(Wr("read"));if(!ArrayBuffer.isView(r))return u(new TypeError("view must be an array buffer view"));if(r.byteLength===0)return u(new TypeError("view must have non-zero byteLength"));if(r.buffer.byteLength===0)return u(new TypeError("view's buffer must have non-zero byteLength"));if(O(r.buffer))return u(new TypeError("view's buffer has been detached"));let n;try{n=Ba(t,"options")}catch(s){return u(s)}let o=n.min;if(o===0)return u(new TypeError("options.min must be greater than 0"));if(da(r)){if(o>r.byteLength)return u(new RangeError("options.min must be less than or equal to view's byteLength"))}else if(o>r.length)return u(new RangeError("options.min must be less than or equal to view's length"));if(this._ownerReadableStream===void 0)return u(Qe("read from"));let l=jn(this,r,o)?new Ct:new gt;return Tt(this,r,o,l),i(l._promise!==void 0),l._promise}releaseLock(){if(!te(this))throw Wr("releaseLock");this._ownerReadableStream!==void 0&&Fn(this)}};Object.defineProperties(ae.prototype,{cancel:{enumerable:!0},read:{enumerable:!0},releaseLock:{enumerable:!0},closed:{enumerable:!0}});y(ae.prototype.cancel,"cancel");y(ae.prototype.read,"read");y(ae.prototype.releaseLock,"releaseLock");typeof Symbol.toStringTag=="symbol"&&Object.defineProperty(ae.prototype,Symbol.toStringTag,{value:"ReadableStreamBYOBReader",configurable:!0});var gt=class{static{a(this,"ByobReadIntoRequest")}constructor(){this._promise=_((r,t)=>{this._resolvePromise=r,this._rejectPromise=t})}_chunkSteps(r){this._resolvePromise({value:r,done:!1})}_closeSteps(r){this._resolvePromise({value:r,done:!0})}_errorSteps(r){this._rejectPromise(r)}},Ct=class{constructor(){this._promise=void 0}static{a(this,"SyncByobReadIntoRequest")}_chunkSteps(r){i(this._promise===void 0),this._promise=Re({value:r,done:!1})}_closeSteps(r){i(this._promise===void 0),this._promise=Re({value:r,done:!0})}_errorSteps(r){i(this._promise===void 0),this._promise=u(r)}};function te(e){return!R(e)||!Object.prototype.hasOwnProperty.call(e,"_readIntoRequests")?!1:e instanceof ae}a(te,"IsReadableStreamBYOBReader");function Tt(e,r,t,n){let o=e._ownerReadableStream;i(o!==void 0),o._disturbed=!0,o._state==="errored"?n._errorSteps(o._storedError):ha(o._readableStreamController,r,t,n)}a(Tt,"ReadableStreamBYOBReaderRead");function jn(e,r,t){let n=e._ownerReadableStream;return i(n!==void 0),n._state==="errored"?!0:_a(n._readableStreamController,r,t)}a(jn,"ReadableStreamBYOBReaderCanReadSync");function Fn(e){F(e);let r=new TypeError("Reader was released");Pt(e,r)}a(Fn,"ReadableStreamBYOBReaderRelease");function Pt(e,r){let t=e._readIntoRequests;e._readIntoRequests=new P,t.forEach(n=>{n._errorSteps(r)})}a(Pt,"ReadableStreamBYOBReaderErrorReadIntoRequests");function Wr(e){return new TypeError(`ReadableStreamBYOBReader.prototype.${e} can only be used on a ReadableStreamBYOBReader`)}a(Wr,"byobReaderBrandCheckException");function he(e,r){let{highWaterMark:t}=e;if(t===void 0)return r;if(Cr(t)||t<0)throw new RangeError("Invalid highWaterMark");return t}a(he,"ExtractHighWaterMark");function Ae(e){let{size:r}=e;return r||(()=>1)}a(Ae,"ExtractSizeAlgorithm");function We(e,r){v(e,r);let t=e?.highWaterMark,n=e?.size;return{highWaterMark:t===void 0?void 0:er(t),size:n===void 0?void 0:zn(n,`${r} has member 'size' that`)}}a(We,"convertQueuingStrategy");function zn(e,r){return k(e,r),t=>er(e(t))}a(zn,"convertQueuingStrategySize");function Aa(e,r){v(e,r);let t=e?.abort,n=e?.close,o=e?.start,l=e?.type,s=e?.write;return{abort:t===void 0?void 0:Un(t,e,`${r} has member 'abort' that`),close:n===void 0?void 0:Ln(n,e,`${r} has member 'close' that`),start:o===void 0?void 0:Qn(o,e,`${r} has member 'start' that`),write:s===void 0?void 0:Vn(s,e,`${r} has member 'write' that`),type:l}}a(Aa,"convertUnderlyingSink");function Un(e,r,t){return k(e,t),n=>Q(e,r,[n])}a(Un,"convertUnderlyingSinkAbortCallback");function Ln(e,r,t){return k(e,t),()=>Q(e,r,[])}a(Ln,"convertUnderlyingSinkCloseCallback");function Qn(e,r,t){return k(e,t),n=>Z(e,r,[n])}a(Qn,"convertUnderlyingSinkStartCallback");function Vn(e,r,t){return k(e,t),(n,o)=>Q(e,r,[n,o])}a(Vn,"convertUnderlyingSinkWriteCallback");function Ir(e,r){if(!$(e))throw new TypeError(`${r} is not a WritableStream.`)}a(Ir,"assertWritableStream");function Or(e){if(typeof e!="object"||e===null)return!1;try{return typeof e.aborted=="boolean"}catch{return!1}}a(Or,"isAbortSignal");function Wa(){if(typeof AbortController=="function")return new AbortController}a(Wa,"createAbortController");var me=class{static{a(this,"WritableStream")}constructor(r={},t={}){r===void 0?r=null:pr(r,"First parameter");let n=We(t,"Second parameter"),o=Aa(r,"First parameter");if(Oa(this),o.type!==void 0)throw new RangeError("Invalid type is specified");let s=Ae(n),d=he(n,1);eo(this,o,d,s)}get locked(){if(!$(this))throw Dr("locked");return fe(this)}abort(r=void 0){return $(this)?fe(this)?u(new TypeError("Cannot abort a stream that already has a writer")):ur(this,r):u(Dr("abort"))}close(){return $(this)?fe(this)?u(new TypeError("Cannot close a stream that already has a writer")):U(this)?u(new TypeError("Cannot close an already-closing stream")):qa(this):u(Dr("close"))}getWriter(){if(!$(this))throw Dr("getWriter");return At(this)}};Object.defineProperties(me.prototype,{abort:{enumerable:!0},close:{enumerable:!0},getWriter:{enumerable:!0},locked:{enumerable:!0}});y(me.prototype.abort,"abort");y(me.prototype.close,"close");y(me.prototype.getWriter,"getWriter");typeof Symbol.toStringTag=="symbol"&&Object.defineProperty(me.prototype,Symbol.toStringTag,{value:"WritableStream",configurable:!0});function At(e){return new ne(e)}a(At,"AcquireWritableStreamDefaultWriter");function Ia(e,r,t,n,o=1,l=()=>1){i(we(o));let s=Object.create(me.prototype);Oa(s);let d=Object.create(_e.prototype);return za(s,d,e,r,t,n,o,l),s}a(Ia,"CreateWritableStream");function Oa(e){e._state="writable",e._storedError=void 0,e._writer=void 0,e._writableStreamController=void 0,e._writeRequests=new P,e._inFlightWriteRequest=void 0,e._closeRequest=void 0,e._inFlightCloseRequest=void 0,e._pendingAbortRequest=void 0,e._backpressure=!1}a(Oa,"InitializeWritableStream");function $(e){return!R(e)||!Object.prototype.hasOwnProperty.call(e,"_writableStreamController")?!1:e instanceof me}a($,"IsWritableStream");function fe(e){return i($(e)),e._writer!==void 0}a(fe,"IsWritableStreamLocked");function ur(e,r){if(e._state==="closed"||e._state==="errored")return m(void 0);e._writableStreamController._abortReason=r,e._writableStreamController._abortController?.abort(r);let t=e._state;if(t==="closed"||t==="errored")return m(void 0);if(e._pendingAbortRequest!==void 0)return e._pendingAbortRequest._promise;i(t==="writable"||t==="erroring");let n=!1;t==="erroring"&&(n=!0,r=void 0);let o=_((l,s)=>{e._pendingAbortRequest={_promise:void 0,_resolve:l,_reject:s,_reason:r,_wasAlreadyErroring:n}});return e._pendingAbortRequest._promise=o,n||kt(e,r),o}a(ur,"WritableStreamAbort");function qa(e){let r=e._state;if(r==="closed"||r==="errored")return u(new TypeError(`The stream (in ${r} state) is not in the writable state and cannot be closed`));i(r==="writable"||r==="erroring"),i(!U(e));let t=_((o,l)=>{let s={_resolve:o,_reject:l};e._closeRequest=s}),n=e._writer;return n!==void 0&&e._backpressure&&r==="writable"&&xt(n),ro(e._writableStreamController),t}a(qa,"WritableStreamClose");function Mn(e){return i(fe(e)),i(e._state==="writable"),_((t,n)=>{let o={_resolve:t,_reject:n};e._writeRequests.push(o)})}a(Mn,"WritableStreamAddWriteRequest");function Wt(e,r){let t=e._state;if(t==="writable"){kt(e,r);return}i(t==="erroring"),It(e)}a(Wt,"WritableStreamDealWithRejection");function kt(e,r){i(e._storedError===void 0),i(e._state==="writable");let t=e._writableStreamController;i(t!==void 0),e._state="erroring",e._storedError=r;let n=e._writer;n!==void 0&&ja(n,r),!Da(e)&&t._started&&It(e)}a(kt,"WritableStreamStartErroring");function It(e){i(e._state==="erroring"),i(!Da(e)),e._state="errored",e._writableStreamController[et]();let r=e._storedError;if(e._writeRequests.forEach(o=>{o._reject(r)}),e._writeRequests=new P,e._pendingAbortRequest===void 0){qr(e);return}let t=e._pendingAbortRequest;if(e._pendingAbortRequest=void 0,t._wasAlreadyErroring){t._reject(r),qr(e);return}let n=e._writableStreamController[Jr](t._reason);w(n,()=>(t._resolve(),qr(e),null),o=>(t._reject(o),qr(e),null))}a(It,"WritableStreamFinishErroring");function Nn(e){i(e._inFlightWriteRequest!==void 0),e._inFlightWriteRequest._resolve(void 0),e._inFlightWriteRequest=void 0}a(Nn,"WritableStreamFinishInFlightWrite");function Yn(e,r){i(e._inFlightWriteRequest!==void 0),e._inFlightWriteRequest._reject(r),e._inFlightWriteRequest=void 0,i(e._state==="writable"||e._state==="erroring"),Wt(e,r)}a(Yn,"WritableStreamFinishInFlightWriteWithError");function $n(e){i(e._inFlightCloseRequest!==void 0),e._inFlightCloseRequest._resolve(void 0),e._inFlightCloseRequest=void 0;let r=e._state;i(r==="writable"||r==="erroring"),r==="erroring"&&(e._storedError=void 0,e._pendingAbortRequest!==void 0&&(e._pendingAbortRequest._resolve(),e._pendingAbortRequest=void 0)),e._state="closed";let t=e._writer;t!==void 0&&Va(t),i(e._pendingAbortRequest===void 0),i(e._storedError===void 0)}a($n,"WritableStreamFinishInFlightClose");function Gn(e,r){i(e._inFlightCloseRequest!==void 0),e._inFlightCloseRequest._reject(r),e._inFlightCloseRequest=void 0,i(e._state==="writable"||e._state==="erroring"),e._pendingAbortRequest!==void 0&&(e._pendingAbortRequest._reject(r),e._pendingAbortRequest=void 0),Wt(e,r)}a(Gn,"WritableStreamFinishInFlightCloseWithError");function U(e){return!(e._closeRequest===void 0&&e._inFlightCloseRequest===void 0)}a(U,"WritableStreamCloseQueuedOrInFlight");function Da(e){return!(e._inFlightWriteRequest===void 0&&e._inFlightCloseRequest===void 0)}a(Da,"WritableStreamHasOperationMarkedInFlight");function Hn(e){i(e._inFlightCloseRequest===void 0),i(e._closeRequest!==void 0),e._inFlightCloseRequest=e._closeRequest,e._closeRequest=void 0}a(Hn,"WritableStreamMarkCloseRequestInFlight");function Kn(e){i(e._inFlightWriteRequest===void 0),i(e._writeRequests.length!==0),e._inFlightWriteRequest=e._writeRequests.shift()}a(Kn,"WritableStreamMarkFirstWriteRequestInFlight");function qr(e){i(e._state==="errored"),e._closeRequest!==void 0&&(i(e._inFlightCloseRequest===void 0),e._closeRequest._reject(e._storedError),e._closeRequest=void 0);let r=e._writer;r!==void 0&&Et(r,e._storedError)}a(qr,"WritableStreamRejectCloseAndClosedPromiseIfNeeded");function Ot(e,r){i(e._state==="writable"),i(!U(e));let t=e._writer;t!==void 0&&r!==e._backpressure&&(r?so(t):(i(!r),xt(t))),e._backpressure=r}a(Ot,"WritableStreamUpdateBackpressure");var ne=class{static{a(this,"WritableStreamDefaultWriter")}constructor(r){if(I(r,1,"WritableStreamDefaultWriter"),Ir(r,"First parameter"),fe(r))throw new TypeError("This stream has already been locked for exclusive writing by another writer");this._ownerWritableStream=r,r._writer=this;let t=r._state;if(t==="writable")!U(r)&&r._backpressure?zr(this):ka(this),Er(this);else if(t==="erroring")Bt(this,r._storedError),Er(this);else if(t==="closed")ka(this),io(this);else{i(t==="errored");let n=r._storedError;Bt(this,n),Qa(this,n)}}get closed(){return ke(this)?this._closedPromise:u(Ie("closed"))}get desiredSize(){if(!ke(this))throw Ie("desiredSize");if(this._ownerWritableStream===void 0)throw sr("desiredSize");return Jn(this)}get ready(){return ke(this)?this._readyPromise:u(Ie("ready"))}abort(r=void 0){return ke(this)?this._ownerWritableStream===void 0?u(sr("abort")):Xn(this,r):u(Ie("abort"))}close(){if(!ke(this))return u(Ie("close"));let r=this._ownerWritableStream;return r===void 0?u(sr("close")):U(r)?u(new TypeError("Cannot close an already-closing stream")):Ea(this)}releaseLock(){if(!ke(this))throw Ie("releaseLock");let r=this._ownerWritableStream;r!==void 0&&(i(r._writer!==void 0),qt(this))}write(r=void 0){return ke(this)?this._ownerWritableStream===void 0?u(sr("write to")):xr(this,r):u(Ie("write"))}};Object.defineProperties(ne.prototype,{abort:{enumerable:!0},close:{enumerable:!0},releaseLock:{enumerable:!0},write:{enumerable:!0},closed:{enumerable:!0},desiredSize:{enumerable:!0},ready:{enumerable:!0}});y(ne.prototype.abort,"abort");y(ne.prototype.close,"close");y(ne.prototype.releaseLock,"releaseLock");y(ne.prototype.write,"write");typeof Symbol.toStringTag=="symbol"&&Object.defineProperty(ne.prototype,Symbol.toStringTag,{value:"WritableStreamDefaultWriter",configurable:!0});function ke(e){return!R(e)||!Object.prototype.hasOwnProperty.call(e,"_ownerWritableStream")?!1:e instanceof ne}a(ke,"IsWritableStreamDefaultWriter");function Xn(e,r){let t=e._ownerWritableStream;return i(t!==void 0),ur(t,r)}a(Xn,"WritableStreamDefaultWriterAbort");function Ea(e){let r=e._ownerWritableStream;return i(r!==void 0),qa(r)}a(Ea,"WritableStreamDefaultWriterClose");function xa(e){let r=e._ownerWritableStream;i(r!==void 0);let t=r._state;return U(r)||t==="closed"?m(void 0):t==="errored"?u(r._storedError):(i(t==="writable"||t==="erroring"),Ea(e))}a(xa,"WritableStreamDefaultWriterCloseWithErrorPropagation");function Zn(e,r){e._closedPromiseState==="pending"?Et(e,r):lo(e,r)}a(Zn,"WritableStreamDefaultWriterEnsureClosedPromiseRejected");function ja(e,r){e._readyPromiseState==="pending"?Ma(e,r):uo(e,r)}a(ja,"WritableStreamDefaultWriterEnsureReadyPromiseRejected");function Jn(e){let r=e._ownerWritableStream,t=r._state;return t==="errored"||t==="erroring"?null:t==="closed"?0:Ua(r._writableStreamController)}a(Jn,"WritableStreamDefaultWriterGetDesiredSize");function qt(e){let r=e._ownerWritableStream;i(r!==void 0),i(r._writer===e);let t=new TypeError("Writer was released and can no longer be used to monitor the stream's closedness");ja(e,t),Zn(e,t),r._writer=void 0,e._ownerWritableStream=void 0}a(qt,"WritableStreamDefaultWriterRelease");function xr(e,r){let t=e._ownerWritableStream;i(t!==void 0);let n=t._writableStreamController,o=to(n,r);if(t!==e._ownerWritableStream)return u(sr("write to"));let l=t._state;if(l==="errored")return u(t._storedError);if(U(t)||l==="closed")return u(new TypeError("The stream is closing or closed and cannot be written to"));if(l==="erroring")return u(t._storedError);i(l==="writable");let s=Mn(t);return ao(n,r,o),s}a(xr,"WritableStreamDefaultWriterWrite");var Fa={},_e=class{static{a(this,"WritableStreamDefaultController")}constructor(){throw new TypeError("Illegal constructor")}get abortReason(){if(!wt(this))throw vt("abortReason");return this._abortReason}get signal(){if(!wt(this))throw vt("signal");if(this._abortController===void 0)throw new TypeError("WritableStreamDefaultController.prototype.signal is not supported");return this._abortController.signal}error(r=void 0){if(!wt(this))throw vt("error");this._controlledWritableStream._state==="writable"&&La(this,r)}[Jr](r){let t=this._abortAlgorithm(r);return jr(this),t}[et](){ee(this)}};Object.defineProperties(_e.prototype,{abortReason:{enumerable:!0},signal:{enumerable:!0},error:{enumerable:!0}});typeof Symbol.toStringTag=="symbol"&&Object.defineProperty(_e.prototype,Symbol.toStringTag,{value:"WritableStreamDefaultController",configurable:!0});function wt(e){return!R(e)||!Object.prototype.hasOwnProperty.call(e,"_controlledWritableStream")?!1:e instanceof _e}a(wt,"IsWritableStreamDefaultController");function za(e,r,t,n,o,l,s,d){i($(e)),i(e._writableStreamController===void 0),r._controlledWritableStream=e,e._writableStreamController=r,r._queue=void 0,r._queueTotalSize=void 0,ee(r),r._abortReason=void 0,r._abortController=Wa(),r._started=!1,r._strategySizeAlgorithm=d,r._strategyHWM=s,r._writeAlgorithm=n,r._closeAlgorithm=o,r._abortAlgorithm=l;let f=Dt(r);Ot(e,f);let c=t(),b=m(c);w(b,()=>(i(e._state==="writable"||e._state==="erroring"),r._started=!0,Fr(r),null),S=>(i(e._state==="writable"||e._state==="erroring"),r._started=!0,Wt(e,S),null))}a(za,"SetUpWritableStreamDefaultController");function eo(e,r,t,n){let o=Object.create(_e.prototype),l,s,d,f;r.start!==void 0?l=a(()=>r.start(o),"startAlgorithm"):l=a(()=>{},"startAlgorithm"),r.write!==void 0?s=a(c=>r.write(c,o),"writeAlgorithm"):s=a(()=>m(void 0),"writeAlgorithm"),r.close!==void 0?d=a(()=>r.close(),"closeAlgorithm"):d=a(()=>m(void 0),"closeAlgorithm"),r.abort!==void 0?f=a(c=>r.abort(c),"abortAlgorithm"):f=a(()=>m(void 0),"abortAlgorithm"),za(e,o,l,s,d,f,t,n)}a(eo,"SetUpWritableStreamDefaultControllerFromUnderlyingSink");function jr(e){e._writeAlgorithm=void 0,e._closeAlgorithm=void 0,e._abortAlgorithm=void 0,e._strategySizeAlgorithm=void 0}a(jr,"WritableStreamDefaultControllerClearAlgorithms");function ro(e){ar(e,Fa,0),Fr(e)}a(ro,"WritableStreamDefaultControllerClose");function to(e,r){if(e._strategySizeAlgorithm===void 0)return i(e._controlledWritableStream._state!=="writable"),1;try{return e._strategySizeAlgorithm(r)}catch(t){return Ge(e,t),1}}a(to,"WritableStreamDefaultControllerGetChunkSize");function Ua(e){return e._strategyHWM-e._queueTotalSize}a(Ua,"WritableStreamDefaultControllerGetDesiredSize");function ao(e,r,t){try{ar(e,r,t)}catch(o){Ge(e,o);return}let n=e._controlledWritableStream;if(!U(n)&&n._state==="writable"){let o=Dt(e);Ot(n,o)}Fr(e)}a(ao,"WritableStreamDefaultControllerWrite");function Fr(e){let r=e._controlledWritableStream;if(!e._started||r._inFlightWriteRequest!==void 0)return;let t=r._state;if(i(t!=="closed"&&t!=="errored"),t==="erroring"){It(r);return}if(e._queue.length===0)return;let n=la(e);n===Fa?no(e):oo(e,n)}a(Fr,"WritableStreamDefaultControllerAdvanceQueueIfNeeded");function Ge(e,r){e._controlledWritableStream._state==="writable"&&La(e,r)}a(Ge,"WritableStreamDefaultControllerErrorIfNeeded");function no(e){let r=e._controlledWritableStream;Hn(r),tr(e),i(e._queue.length===0);let t=e._closeAlgorithm();jr(e),w(t,()=>($n(r),null),n=>(Gn(r,n),null))}a(no,"WritableStreamDefaultControllerProcessClose");function oo(e,r){let t=e._controlledWritableStream;Kn(t);let n=e._writeAlgorithm(r);w(n,()=>{Nn(t);let o=t._state;if(i(o==="writable"||o==="erroring"),tr(e),!U(t)&&o==="writable"){let l=Dt(e);Ot(t,l)}return Fr(e),null},o=>(t._state==="writable"&&jr(e),Yn(t,o),null))}a(oo,"WritableStreamDefaultControllerProcessWrite");function Dt(e){return Ua(e)<=0}a(Dt,"WritableStreamDefaultControllerGetBackpressure");function La(e,r){let t=e._controlledWritableStream;i(t._state==="writable"),jr(e),kt(t,r)}a(La,"WritableStreamDefaultControllerError");function Dr(e){return new TypeError(`WritableStream.prototype.${e} can only be used on a WritableStream`)}a(Dr,"streamBrandCheckException");function vt(e){return new TypeError(`WritableStreamDefaultController.prototype.${e} can only be used on a WritableStreamDefaultController`)}a(vt,"defaultControllerBrandCheckException");function Ie(e){return new TypeError(`WritableStreamDefaultWriter.prototype.${e} can only be used on a WritableStreamDefaultWriter`)}a(Ie,"defaultWriterBrandCheckException");function sr(e){return new TypeError("Cannot "+e+" a stream using a released writer")}a(sr,"defaultWriterLockException");function Er(e){e._closedPromise=_((r,t)=>{e._closedPromise_resolve=r,e._closedPromise_reject=t,e._closedPromiseState="pending"})}a(Er,"defaultWriterClosedPromiseInitialize");function Qa(e,r){Er(e),Et(e,r)}a(Qa,"defaultWriterClosedPromiseInitializeAsRejected");function io(e){Er(e),Va(e)}a(io,"defaultWriterClosedPromiseInitializeAsResolved");function Et(e,r){e._closedPromise_reject!==void 0&&(i(e._closedPromiseState==="pending"),X(e._closedPromise),e._closedPromise_reject(r),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0,e._closedPromiseState="rejected")}a(Et,"defaultWriterClosedPromiseReject");function lo(e,r){i(e._closedPromise_resolve===void 0),i(e._closedPromise_reject===void 0),i(e._closedPromiseState!=="pending"),Qa(e,r)}a(lo,"defaultWriterClosedPromiseResetToRejected");function Va(e){e._closedPromise_resolve!==void 0&&(i(e._closedPromiseState==="pending"),e._closedPromise_resolve(void 0),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0,e._closedPromiseState="resolved")}a(Va,"defaultWriterClosedPromiseResolve");function zr(e){e._readyPromise=_((r,t)=>{e._readyPromise_resolve=r,e._readyPromise_reject=t}),e._readyPromiseState="pending"}a(zr,"defaultWriterReadyPromiseInitialize");function Bt(e,r){zr(e),Ma(e,r)}a(Bt,"defaultWriterReadyPromiseInitializeAsRejected");function ka(e){zr(e),xt(e)}a(ka,"defaultWriterReadyPromiseInitializeAsResolved");function Ma(e,r){e._readyPromise_reject!==void 0&&(X(e._readyPromise),e._readyPromise_reject(r),e._readyPromise_resolve=void 0,e._readyPromise_reject=void 0,e._readyPromiseState="rejected")}a(Ma,"defaultWriterReadyPromiseReject");function so(e){i(e._readyPromise_resolve===void 0),i(e._readyPromise_reject===void 0),zr(e)}a(so,"defaultWriterReadyPromiseReset");function uo(e,r){i(e._readyPromise_resolve===void 0),i(e._readyPromise_reject===void 0),Bt(e,r)}a(uo,"defaultWriterReadyPromiseResetToRejected");function xt(e){e._readyPromise_resolve!==void 0&&(e._readyPromise_resolve(void 0),e._readyPromise_resolve=void 0,e._readyPromise_reject=void 0,e._readyPromiseState="fulfilled")}a(xt,"defaultWriterReadyPromiseResolve");function fo(){if(typeof globalThis<"u")return globalThis;if(typeof self<"u")return self;if(typeof global<"u")return global}a(fo,"getGlobals");var Na=fo();function mo(e){if(!(typeof e=="function"||typeof e=="object")||e.name!=="DOMException")return!1;try{return new e,!0}catch{return!1}}a(mo,"isDOMExceptionConstructor");function co(){let e=Na?.DOMException;return mo(e)?e:void 0}a(co,"getFromGlobal");function bo(){let e=a(function(t,n){this.message=t||"",this.name=n||"Error",Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)},"DOMException");return y(e,"DOMException"),e.prototype=Object.create(Error.prototype),Object.defineProperty(e.prototype,"constructor",{value:e,writable:!0,configurable:!0}),e}a(bo,"createPolyfill");var Ya=co()||bo();function Ut(e,r,t,n,o,l){i(W(e)),i($(r)),i(typeof t=="boolean"),i(typeof n=="boolean"),i(typeof o=="boolean"),i(l===void 0||Or(l)),i(!D(e)),i(!fe(r));let s=se(e),d=At(r);e._disturbed=!0;let f=new jt(d),c=new zt(f);return _((b,S)=>{let h;if(l!==void 0){if(h=a(()=>{let p=l.reason!==void 0?l.reason:new Ya("Aborted","AbortError"),T=[];n||T.push(()=>r._state==="writable"?ur(r,p):m(void 0)),o||T.push(()=>e._state==="readable"?z(e,p):m(void 0)),ie(()=>Promise.all(T.reverse().map(K=>K())),!0,p,!0)},"abortAlgorithm"),l.aborted){h();return}l.addEventListener("abort",h)}function E(){return _((p,T)=>{function K(g){g?p():j(oe(),K,T)}a(K,"next"),K(!1)})}a(E,"pipeLoop");function oe(){for(;!f._shuttingDown&&!r._backpressure&&r._state==="writable"&&!U(r)&&e._state==="readable"&&it(s);)pe(s,c);if(f._shuttingDown)return m(!0);if(r._backpressure)return j(d._readyPromise,oe);let p=new Ft(f);return f._currentRead=p._promise,pe(s,p),p._promise}if(a(oe,"pipeStep"),$a(e,s._closedPromise,p=>(n?H(!0,p):ie(()=>ur(r,p),!0,p),null)),$a(r,d._closedPromise,p=>(o?H(!0,p):ie(()=>z(e,p),!0,p),null)),yo(e,s._closedPromise,()=>(t?H():ie(()=>xa(d)),null)),U(r)||r._state==="closed"){let p=new TypeError("the destination writable stream closed before all data could be piped to it");o?H(!0,p):ie(()=>z(e,p),!0,p)}X(E());function ie(p,T,K,g=!1){if(f._shuttingDown)return;f._shuttingDown=!0;let B=g?f._currentRead:void 0;Xe(B?j(B,q,q):m(void 0),()=>{r._state==="writable"&&!U(r)?Xe(f._waitForWritesToFinish(),x):x()});function x(){return w(p(),()=>C(T,K),le=>C(!0,le)),null}a(x,"doTheRest")}a(ie,"shutdownWithAction");function H(p,T){f._shuttingDown||(f._shuttingDown=!0,r._state==="writable"&&!U(r)?Xe(f._waitForWritesToFinish(),()=>C(p,T)):C(p,T))}a(H,"shutdown");function C(p,T){return qt(d),F(s),l!==void 0&&l.removeEventListener("abort",h),p?S(T):b(void 0),null}a(C,"finalize")})}a(Ut,"ReadableStreamPipeTo");var jt=class{constructor(r){this._writer=r;this._shuttingDown=!1;this._currentWrite=m(void 0)}static{a(this,"PipeState")}_waitForWritesToFinish(){let r=this._currentWrite;return j(this._currentWrite,()=>r!==this._currentWrite?this._waitForWritesToFinish():void 0)}},Ft=class{static{a(this,"PipeReadRequest")}constructor(r){this._state=r,this._promise=_((t,n)=>{this._resolvePromise=t,this._rejectPromise=n})}_chunkSteps(r){if(this._state._shuttingDown){this._resolvePromise(!1);return}this._state._currentWrite=j(xr(this._state._writer,r),void 0,q),this._resolvePromise(!1)}_closeSteps(){this._resolvePromise(!0)}_errorSteps(r){this._rejectPromise(r)}},zt=class{constructor(r){this._state=r}static{a(this,"SyncPipeReadRequest")}_chunkSteps(r){this._state._currentWrite=j(xr(this._state._writer,r),void 0,q)}_closeSteps(){Kr()}_errorSteps(r){Kr()}};function $a(e,r,t){e._state==="errored"?t(e._storedError):Ze(r,t)}a($a,"isOrBecomesErrored");function yo(e,r,t){e._state==="closed"?t():Xe(r,t)}a(yo,"isOrBecomesClosed");var G=class{static{a(this,"ReadableStreamDefaultController")}constructor(){throw new TypeError("Illegal constructor")}get desiredSize(){if(!Ur(this))throw Lr("desiredSize");return Vr(this)}close(){if(!Ur(this))throw Lr("close");if(!Oe(this))throw new TypeError("The stream is not in a state that permits close");ce(this)}enqueue(r=void 0){if(!Ur(this))throw Lr("enqueue");if(!Oe(this))throw new TypeError("The stream is not in a state that permits enqueue");return Se(this,r)}error(r=void 0){if(!Ur(this))throw Lr("error");N(this,r)}[je](r){ee(this);let t=this._cancelAlgorithm(r);return Qr(this),t}[Fe](r){let t=this._controlledReadableStream;if(this._queue.length>0){let n=tr(this);this._closeRequested&&this._queue.length===0?(Qr(this),Be(t)):dr(this),r._chunkSteps(n)}else Rr(t,r),dr(this)}[ze](){return this._queue.length>0}[Ue](){}};Object.defineProperties(G.prototype,{close:{enumerable:!0},enqueue:{enumerable:!0},error:{enumerable:!0},desiredSize:{enumerable:!0}});y(G.prototype.close,"close");y(G.prototype.enqueue,"enqueue");y(G.prototype.error,"error");typeof Symbol.toStringTag=="symbol"&&Object.defineProperty(G.prototype,Symbol.toStringTag,{value:"ReadableStreamDefaultController",configurable:!0});function Ur(e){return!R(e)||!Object.prototype.hasOwnProperty.call(e,"_controlledReadableStream")?!1:e instanceof G}a(Ur,"IsReadableStreamDefaultController");function dr(e){if(!Ga(e))return;if(e._pulling){e._pullAgain=!0;return}i(!e._pullAgain),e._pulling=!0;let t=e._pullAlgorithm();w(t,()=>(e._pulling=!1,e._pullAgain&&(e._pullAgain=!1,dr(e)),null),n=>(N(e,n),null))}a(dr,"ReadableStreamDefaultControllerCallPullIfNeeded");function Ga(e){let r=e._controlledReadableStream;if(!Oe(e)||!e._started)return!1;if(D(r)&&Te(r)>0)return!0;let t=Vr(e);return i(t!==null),t>0}a(Ga,"ReadableStreamDefaultControllerShouldCallPull");function Qr(e){e._pullAlgorithm=void 0,e._cancelAlgorithm=void 0,e._strategySizeAlgorithm=void 0}a(Qr,"ReadableStreamDefaultControllerClearAlgorithms");function ce(e){if(!Oe(e))return;let r=e._controlledReadableStream;e._closeRequested=!0,e._queue.length===0&&(Qr(e),Be(r))}a(ce,"ReadableStreamDefaultControllerClose");function Se(e,r){if(!Oe(e))return;let t=e._controlledReadableStream;if(D(t)&&Te(t)>0)rr(t,r,!1);else{let n;try{n=e._strategySizeAlgorithm(r)}catch(o){throw N(e,o),o}try{ar(e,r,n)}catch(o){throw N(e,o),o}}dr(e)}a(Se,"ReadableStreamDefaultControllerEnqueue");function N(e,r){let t=e._controlledReadableStream;t._state==="readable"&&(ee(e),Qr(e),Ar(t,r))}a(N,"ReadableStreamDefaultControllerError");function Vr(e){let r=e._controlledReadableStream._state;return r==="errored"?null:r==="closed"?0:e._strategyHWM-e._queueTotalSize}a(Vr,"ReadableStreamDefaultControllerGetDesiredSize");function Ha(e){return!Ga(e)}a(Ha,"ReadableStreamDefaultControllerHasBackpressure");function Oe(e){let r=e._controlledReadableStream._state;return!e._closeRequested&&r==="readable"}a(Oe,"ReadableStreamDefaultControllerCanCloseOrEnqueue");function Lt(e,r,t,n,o,l,s){i(e._readableStreamController===void 0),r._controlledReadableStream=e,r._queue=void 0,r._queueTotalSize=void 0,ee(r),r._started=!1,r._closeRequested=!1,r._pullAgain=!1,r._pulling=!1,r._strategySizeAlgorithm=s,r._strategyHWM=l,r._pullAlgorithm=n,r._cancelAlgorithm=o,e._readableStreamController=r;let d=t();w(m(d),()=>(r._started=!0,i(!r._pulling),i(!r._pullAgain),dr(r),null),f=>(N(r,f),null))}a(Lt,"SetUpReadableStreamDefaultController");function Ka(e,r,t,n){let o=Object.create(G.prototype),l,s,d;r.start!==void 0?l=a(()=>r.start(o),"startAlgorithm"):l=a(()=>{},"startAlgorithm"),r.pull!==void 0?s=a(()=>r.pull(o),"pullAlgorithm"):s=a(()=>m(void 0),"pullAlgorithm"),r.cancel!==void 0?d=a(f=>r.cancel(f),"cancelAlgorithm"):d=a(()=>m(void 0),"cancelAlgorithm"),Lt(e,o,l,s,d,t,n)}a(Ka,"SetUpReadableStreamDefaultControllerFromUnderlyingSource");function Lr(e){return new TypeError(`ReadableStreamDefaultController.prototype.${e} can only be used on a ReadableStreamDefaultController`)}a(Lr,"defaultControllerBrandCheckException");function Xa(e,r){return i(W(e)),i(typeof r=="boolean"),re(e._readableStreamController)?So(e):po(e,r)}a(Xa,"ReadableStreamTee");function po(e,r){i(W(e)),i(typeof r=="boolean");let t=se(e),n=!1,o=!1,l=!1,s=!1,d,f,c,b,S,h=_(C=>{S=C});function E(){return n?(o=!0,m(void 0)):(n=!0,pe(t,{_chunkSteps:a(p=>{ye(()=>{o=!1;let T=p,K=p;l||Se(c._readableStreamController,T),s||Se(b._readableStreamController,K),n=!1,o&&E()})},"_chunkSteps"),_closeSteps:a(()=>{n=!1,l||ce(c._readableStreamController),s||ce(b._readableStreamController),(!l||!s)&&S(void 0)},"_closeSteps"),_errorSteps:a(()=>{n=!1},"_errorSteps")}),m(void 0))}a(E,"pullAlgorithm");function oe(C){if(l=!0,d=C,s){let T=z(e,C);S(T)}return h}a(oe,"cancel1Algorithm");function ie(C){if(s=!0,f=C,l){let T=z(e,C);S(T)}return h}a(ie,"cancel2Algorithm");function H(){}return a(H,"startAlgorithm"),c=ge(H,E,oe),b=ge(H,E,ie),Ze(t._closedPromise,C=>(N(c._readableStreamController,C),N(b._readableStreamController,C),(!l||!s)&&S(void 0),null)),[c,b]}a(po,"ReadableStreamDefaultTee");function So(e){i(W(e)),i(re(e._readableStreamController));let r=se(e),t=!1,n=!1,o=!1,l=!1,s=!1,d,f,c,b,S,h=_(g=>{S=g});function E(g){Ze(g._closedPromise,B=>(g!==r||(M(c._readableStreamController,B),M(b._readableStreamController,B),(!l||!s)&&S(void 0)),null))}a(E,"forwardReaderError");function oe(){te(r)&&(i(r._readIntoRequests.length===0),F(r),r=se(e),E(r)),pe(r,{_chunkSteps:a(B=>{ye(()=>{n=!1,o=!1;let x=B,le=B;if(!l&&!s)try{le=mt(B)}catch(fr){M(c._readableStreamController,fr),M(b._readableStreamController,fr),S(z(e,fr));return}l||or(c._readableStreamController,x),s||or(b._readableStreamController,le),t=!1,n?H():o&&C()})},"_chunkSteps"),_closeSteps:a(()=>{t=!1,l||$e(c._readableStreamController),s||$e(b._readableStreamController),c._readableStreamController._pendingPullIntos.length>0&&ir(c._readableStreamController,0),b._readableStreamController._pendingPullIntos.length>0&&ir(b._readableStreamController,0),(!l||!s)&&S(void 0)},"_closeSteps"),_errorSteps:a(()=>{t=!1},"_errorSteps")})}a(oe,"pullWithDefaultReader");function ie(g,B){V(r)&&(i(r._readRequests.length===0),F(r),r=kr(e),E(r));let x=B?b:c,le=B?c:b;Tt(r,g,1,{_chunkSteps:a(Ce=>{ye(()=>{n=!1,o=!1;let xe=B?s:l;if(B?l:s)xe||lr(x._readableStreamController,Ce);else{let Ht;try{Ht=mt(Ce)}catch(Hr){M(x._readableStreamController,Hr),M(le._readableStreamController,Hr),S(z(e,Hr));return}xe||lr(x._readableStreamController,Ce),or(le._readableStreamController,Ht)}t=!1,n?H():o&&C()})},"_chunkSteps"),_closeSteps:a(Ce=>{t=!1;let xe=B?s:l,mr=B?l:s;xe||$e(x._readableStreamController),mr||$e(le._readableStreamController),Ce!==void 0&&(i(Ce.byteLength===0),xe||lr(x._readableStreamController,Ce),!mr&&le._readableStreamController._pendingPullIntos.length>0&&ir(le._readableStreamController,0)),(!xe||!mr)&&S(void 0)},"_closeSteps"),_errorSteps:a(()=>{t=!1},"_errorSteps")})}a(ie,"pullWithBYOBReader");function H(){if(t)return n=!0,m(void 0);t=!0;let g=vr(c._readableStreamController);return g===null?oe():ie(g._view,!1),m(void 0)}a(H,"pull1Algorithm");function C(){if(t)return o=!0,m(void 0);t=!0;let g=vr(b._readableStreamController);return g===null?oe():ie(g._view,!0),m(void 0)}a(C,"pull2Algorithm");function p(g){if(l=!0,d=g,s){let x=z(e,g);S(x)}return h}a(p,"cancel1Algorithm");function T(g){if(s=!0,f=g,l){let x=z(e,g);S(x)}return h}a(T,"cancel2Algorithm");function K(){}return a(K,"startAlgorithm"),c=Qt(K,H,p),b=Qt(K,C,T),E(r),[c,b]}a(So,"ReadableByteStreamTee");function Za(e){return R(e)&&typeof e.getReader<"u"}a(Za,"isReadableStreamLike");function Ja(e){return Za(e)?ho(e.getReader()):Ro(e)}a(Ja,"ReadableStreamFrom");function Ro(e){let r,t=ut(e,"async"),n=q;function o(){let s;try{s=dt(t)}catch(f){return u(f)}let d=m(s);return L(d,f=>{if(!R(f))throw new TypeError("The promise returned by the iterator.next() method must fulfill with an object");if(f.done)ce(r._readableStreamController);else{let b=f.value;Se(r._readableStreamController,b)}})}a(o,"pullAlgorithm");function l(s){let d=t.iterator,f;try{f=Ne(d,"return")}catch(b){return u(b)}if(f===void 0)return m(void 0);let c=Q(f,d,[s]);return L(c,b=>{if(!R(b))throw new TypeError("The promise returned by the iterator.return() method must fulfill with an object")})}return a(l,"cancelAlgorithm"),r=ge(n,o,l,0),r}a(Ro,"ReadableStreamFromIterable");function ho(e){let r,t=q;function n(){let l;try{l=e.read()}catch(s){return u(s)}return L(l,s=>{if(!R(s))throw new TypeError("The promise returned by the reader.read() method must fulfill with an object");if(s.done)ce(r._readableStreamController);else{let d=s.value;Se(r._readableStreamController,d)}})}a(n,"pullAlgorithm");function o(l){try{return m(e.cancel(l))}catch(s){return u(s)}}return a(o,"cancelAlgorithm"),r=ge(t,n,o,0),r}a(ho,"ReadableStreamFromDefaultReader");function en(e,r){v(e,r);let t=e,n=t?.autoAllocateChunkSize,o=t?.cancel,l=t?.pull,s=t?.start,d=t?.type;return{autoAllocateChunkSize:n===void 0?void 0:Ve(n,`${r} has member 'autoAllocateChunkSize' that`),cancel:o===void 0?void 0:_o(o,t,`${r} has member 'cancel' that`),pull:l===void 0?void 0:go(l,t,`${r} has member 'pull' that`),start:s===void 0?void 0:Co(s,t,`${r} has member 'start' that`),type:d===void 0?void 0:To(d,`${r} has member 'type' that`)}}a(en,"convertUnderlyingDefaultOrByteSource");function _o(e,r,t){return k(e,t),n=>Q(e,r,[n])}a(_o,"convertUnderlyingSourceCancelCallback");function go(e,r,t){return k(e,t),n=>Q(e,r,[n])}a(go,"convertUnderlyingSourcePullCallback");function Co(e,r,t){return k(e,t),n=>Z(e,r,[n])}a(Co,"convertUnderlyingSourceStartCallback");function To(e,r){if(e=`${e}`,e!=="bytes")throw new TypeError(`${r} '${e}' is not a valid enumeration value for ReadableStreamType`);return e}a(To,"convertReadableStreamType");function rn(e,r){return v(e,r),{preventCancel:!!e?.preventCancel}}a(rn,"convertIteratorOptions");function Vt(e,r){v(e,r);let t=e?.preventAbort,n=e?.preventCancel,o=e?.preventClose,l=e?.signal;return l!==void 0&&Po(l,`${r} has member 'signal' that`),{preventAbort:!!t,preventCancel:!!n,preventClose:!!o,signal:l}}a(Vt,"convertPipeOptions");function Po(e,r){if(!Or(e))throw new TypeError(`${r} is not an AbortSignal.`)}a(Po,"assertAbortSignal");function tn(e,r){v(e,r);let t=e?.readable;Je(t,"readable","ReadableWritablePair"),Me(t,`${r} has member 'readable' that`);let n=e?.writable;return Je(n,"writable","ReadableWritablePair"),Ir(n,`${r} has member 'writable' that`),{readable:t,writable:n}}a(tn,"convertReadableWritablePair");var A=class{static{a(this,"ReadableStream")}constructor(r={},t={}){r===void 0?r=null:pr(r,"First parameter");let n=We(t,"Second parameter"),o=en(r,"First parameter");if(Mt(this),o.type==="bytes"){if(n.size!==void 0)throw new RangeError("The strategy for a byte stream cannot have a size function");let l=he(n,0);Pa(this,o,l)}else{i(o.type===void 0);let l=Ae(n),s=he(n,1);Ka(this,o,s,l)}}get locked(){if(!W(this))throw qe("locked");return D(this)}cancel(r=void 0){return W(this)?D(this)?u(new TypeError("Cannot cancel a stream that already has a reader")):z(this,r):u(qe("cancel"))}getReader(r=void 0){if(!W(this))throw qe("getReader");let t=va(r,"First parameter");return t.mode===void 0?se(this):(i(t.mode==="byob"),kr(this))}pipeThrough(r,t={}){if(!W(this))throw qe("pipeThrough");I(r,1,"pipeThrough");let n=tn(r,"First parameter"),o=Vt(t,"Second parameter");if(D(this))throw new TypeError("ReadableStream.prototype.pipeThrough cannot be used on a locked ReadableStream");if(fe(n.writable))throw new TypeError("ReadableStream.prototype.pipeThrough cannot be used on a locked WritableStream");let l=Ut(this,n.writable,o.preventClose,o.preventAbort,o.preventCancel,o.signal);return X(l),n.readable}pipeTo(r,t={}){if(!W(this))return u(qe("pipeTo"));if(r===void 0)return u("Parameter 1 is required in 'pipeTo'.");if(!$(r))return u(new TypeError("ReadableStream.prototype.pipeTo's first argument must be a WritableStream"));let n;try{n=Vt(t,"Second parameter")}catch(o){return u(o)}return D(this)?u(new TypeError("ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream")):fe(r)?u(new TypeError("ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream")):Ut(this,r,n.preventClose,n.preventAbort,n.preventCancel,n.signal)}tee(){if(!W(this))throw qe("tee");let r=Xa(this,!1);return ea(r)}values(r=void 0){if(!W(this))throw qe("values");let t=rn(r,"First parameter");return ia(this,t.preventCancel)}[Pe](r){return this.values(r)}static from(r){return Ja(r)}};Object.defineProperties(A,{from:{enumerable:!0}});Object.defineProperties(A.prototype,{cancel:{enumerable:!0},getReader:{enumerable:!0},pipeThrough:{enumerable:!0},pipeTo:{enumerable:!0},tee:{enumerable:!0},values:{enumerable:!0},locked:{enumerable:!0}});y(A.from,"from");y(A.prototype.cancel,"cancel");y(A.prototype.getReader,"getReader");y(A.prototype.pipeThrough,"pipeThrough");y(A.prototype.pipeTo,"pipeTo");y(A.prototype.tee,"tee");y(A.prototype.values,"values");typeof Symbol.toStringTag=="symbol"&&Object.defineProperty(A.prototype,Symbol.toStringTag,{value:"ReadableStream",configurable:!0});Object.defineProperty(A.prototype,Pe,{value:A.prototype.values,writable:!0,configurable:!0});function ge(e,r,t,n=1,o=()=>1){i(we(n));let l=Object.create(A.prototype);Mt(l);let s=Object.create(G.prototype);return Lt(l,s,e,r,t,n,o),l}a(ge,"CreateReadableStream");function Qt(e,r,t){let n=Object.create(A.prototype);Mt(n);let o=Object.create(Y.prototype);return ht(n,o,e,r,t,0,void 0),n}a(Qt,"CreateReadableByteStream");function Mt(e){e._state="readable",e._reader=void 0,e._storedError=void 0,e._disturbed=!1}a(Mt,"InitializeReadableStream");function W(e){return!R(e)||!Object.prototype.hasOwnProperty.call(e,"_readableStreamController")?!1:e instanceof A}a(W,"IsReadableStream");function D(e){return i(W(e)),e._reader!==void 0}a(D,"IsReadableStreamLocked");function z(e,r){if(e._disturbed=!0,e._state==="closed")return m(void 0);if(e._state==="errored")return u(e._storedError);Be(e);let t=e._reader;if(t!==void 0&&te(t)){let o=t._readIntoRequests;t._readIntoRequests=new P,o.forEach(l=>{l._closeSteps(void 0)})}let n=e._readableStreamController[je](r);return L(n,q)}a(z,"ReadableStreamCancel");function Be(e){i(e._state==="readable"),e._state="closed";let r=e._reader;if(r!==void 0&&(tt(r),V(r))){let t=r._readRequests;r._readRequests=new P,t.forEach(n=>{n._closeSteps()})}}a(Be,"ReadableStreamClose");function Ar(e,r){i(W(e)),i(e._state==="readable"),e._state="errored",e._storedError=r;let t=e._reader;t!==void 0&&(yr(t,r),V(t)?lt(t,r):(i(te(t)),Pt(t,r)))}a(Ar,"ReadableStreamError");function qe(e){return new TypeError(`ReadableStream.prototype.${e} can only be used on a ReadableStream`)}a(qe,"streamBrandCheckException");function Mr(e,r){v(e,r);let t=e?.highWaterMark;return Je(t,"highWaterMark","QueuingStrategyInit"),{highWaterMark:er(t)}}a(Mr,"convertQueuingStrategyInit");var on=a(e=>e.byteLength,"byteLengthSizeFunction");y(on,"size");var De=class{static{a(this,"ByteLengthQueuingStrategy")}constructor(r){I(r,1,"ByteLengthQueuingStrategy"),r=Mr(r,"First parameter"),this._byteLengthQueuingStrategyHighWaterMark=r.highWaterMark}get highWaterMark(){if(!nn(this))throw an("highWaterMark");return this._byteLengthQueuingStrategyHighWaterMark}get size(){if(!nn(this))throw an("size");return on}};Object.defineProperties(De.prototype,{highWaterMark:{enumerable:!0},size:{enumerable:!0}});typeof Symbol.toStringTag=="symbol"&&Object.defineProperty(De.prototype,Symbol.toStringTag,{value:"ByteLengthQueuingStrategy",configurable:!0});function an(e){return new TypeError(`ByteLengthQueuingStrategy.prototype.${e} can only be used on a ByteLengthQueuingStrategy`)}a(an,"byteLengthBrandCheckException");function nn(e){return!R(e)||!Object.prototype.hasOwnProperty.call(e,"_byteLengthQueuingStrategyHighWaterMark")?!1:e instanceof De}a(nn,"IsByteLengthQueuingStrategy");var un=a(()=>1,"countSizeFunction");y(un,"size");var Ee=class{static{a(this,"CountQueuingStrategy")}constructor(r){I(r,1,"CountQueuingStrategy"),r=Mr(r,"First parameter"),this._countQueuingStrategyHighWaterMark=r.highWaterMark}get highWaterMark(){if(!sn(this))throw ln("highWaterMark");return this._countQueuingStrategyHighWaterMark}get size(){if(!sn(this))throw ln("size");return un}};Object.defineProperties(Ee.prototype,{highWaterMark:{enumerable:!0},size:{enumerable:!0}});typeof Symbol.toStringTag=="symbol"&&Object.defineProperty(Ee.prototype,Symbol.toStringTag,{value:"CountQueuingStrategy",configurable:!0});function ln(e){return new TypeError(`CountQueuingStrategy.prototype.${e} can only be used on a CountQueuingStrategy`)}a(ln,"countBrandCheckException");function sn(e){return!R(e)||!Object.prototype.hasOwnProperty.call(e,"_countQueuingStrategyHighWaterMark")?!1:e instanceof Ee}a(sn,"IsCountQueuingStrategy");function dn(e,r){v(e,r);let t=e?.cancel,n=e?.flush,o=e?.readableType,l=e?.start,s=e?.transform,d=e?.writableType;return{cancel:t===void 0?void 0:Ao(t,e,`${r} has member 'cancel' that`),flush:n===void 0?void 0:wo(n,e,`${r} has member 'flush' that`),readableType:o,start:l===void 0?void 0:vo(l,e,`${r} has member 'start' that`),transform:s===void 0?void 0:Bo(s,e,`${r} has member 'transform' that`),writableType:d}}a(dn,"convertTransformer");function wo(e,r,t){return k(e,t),n=>Q(e,r,[n])}a(wo,"convertTransformerFlushCallback");function vo(e,r,t){return k(e,t),n=>Z(e,r,[n])}a(vo,"convertTransformerStartCallback");function Bo(e,r,t){return k(e,t),(n,o)=>Q(e,r,[n,o])}a(Bo,"convertTransformerTransformCallback");function Ao(e,r,t){return k(e,t),n=>Q(e,r,[n])}a(Ao,"convertTransformerCancelCallback");var He=class{static{a(this,"TransformStream")}constructor(r={},t={},n={}){r===void 0&&(r=null);let o=We(t,"Second parameter"),l=We(n,"Third parameter"),s=dn(r,"First parameter");if(s.readableType!==void 0)throw new RangeError("Invalid readableType specified");if(s.writableType!==void 0)throw new RangeError("Invalid writableType specified");let d=he(l,0),f=Ae(l),c=he(o,1),b=Ae(o),S,h=_(E=>{S=E});Wo(this,h,c,b,d,f),Io(this,s),s.start!==void 0?S(s.start(this._transformStreamController)):S(void 0)}get readable(){if(!Nt(this))throw mn("readable");return this._readable}get writable(){if(!Nt(this))throw mn("writable");return this._writable}};Object.defineProperties(He.prototype,{readable:{enumerable:!0},writable:{enumerable:!0}});typeof Symbol.toStringTag=="symbol"&&Object.defineProperty(He.prototype,Symbol.toStringTag,{value:"TransformStream",configurable:!0});function Wo(e,r,t,n,o,l){function s(){return r}a(s,"startAlgorithm");function d(h){return Do(e,h)}a(d,"writeAlgorithm");function f(h){return Eo(e,h)}a(f,"abortAlgorithm");function c(){return xo(e)}a(c,"closeAlgorithm"),e._writable=Ia(s,d,c,f,t,n);function b(){return jo(e)}a(b,"pullAlgorithm");function S(h){return Fo(e,h)}a(S,"cancelAlgorithm"),e._readable=ge(s,b,S,o,l),e._backpressure=void 0,e._backpressureChangePromise=void 0,e._backpressureChangePromise_resolve=void 0,$r(e,!0),e._transformStreamController=void 0}a(Wo,"InitializeTransformStream");function Nt(e){return!R(e)||!Object.prototype.hasOwnProperty.call(e,"_transformStreamController")?!1:e instanceof He}a(Nt,"IsTransformStream");function cn(e,r){N(e._readable._readableStreamController,r),$t(e,r)}a(cn,"TransformStreamError");function $t(e,r){Gr(e._transformStreamController),Ge(e._writable._writableStreamController,r),Yt(e)}a($t,"TransformStreamErrorWritableAndUnblockWrite");function Yt(e){e._backpressure&&$r(e,!1)}a(Yt,"TransformStreamUnblockWrite");function $r(e,r){i(e._backpressure!==r),e._backpressureChangePromise!==void 0&&e._backpressureChangePromise_resolve(),e._backpressureChangePromise=_(t=>{e._backpressureChangePromise_resolve=t}),e._backpressure=r}a($r,"TransformStreamSetBackpressure");var be=class{static{a(this,"TransformStreamDefaultController")}constructor(){throw new TypeError("Illegal constructor")}get desiredSize(){if(!Nr(this))throw Yr("desiredSize");let r=this._controlledTransformStream._readable._readableStreamController;return Vr(r)}enqueue(r=void 0){if(!Nr(this))throw Yr("enqueue");bn(this,r)}error(r=void 0){if(!Nr(this))throw Yr("error");Oo(this,r)}terminate(){if(!Nr(this))throw Yr("terminate");qo(this)}};Object.defineProperties(be.prototype,{enqueue:{enumerable:!0},error:{enumerable:!0},terminate:{enumerable:!0},desiredSize:{enumerable:!0}});y(be.prototype.enqueue,"enqueue");y(be.prototype.error,"error");y(be.prototype.terminate,"terminate");typeof Symbol.toStringTag=="symbol"&&Object.defineProperty(be.prototype,Symbol.toStringTag,{value:"TransformStreamDefaultController",configurable:!0});function Nr(e){return!R(e)||!Object.prototype.hasOwnProperty.call(e,"_controlledTransformStream")?!1:e instanceof be}a(Nr,"IsTransformStreamDefaultController");function ko(e,r,t,n,o){i(Nt(e)),i(e._transformStreamController===void 0),r._controlledTransformStream=e,e._transformStreamController=r,r._transformAlgorithm=t,r._flushAlgorithm=n,r._cancelAlgorithm=o,r._finishPromise=void 0,r._finishPromise_resolve=void 0,r._finishPromise_reject=void 0}a(ko,"SetUpTransformStreamDefaultController");function Io(e,r){let t=Object.create(be.prototype),n,o,l;r.transform!==void 0?n=a(s=>r.transform(s,t),"transformAlgorithm"):n=a(s=>{try{return bn(t,s),m(void 0)}catch(d){return u(d)}},"transformAlgorithm"),r.flush!==void 0?o=a(()=>r.flush(t),"flushAlgorithm"):o=a(()=>m(void 0),"flushAlgorithm"),r.cancel!==void 0?l=a(s=>r.cancel(s),"cancelAlgorithm"):l=a(()=>m(void 0),"cancelAlgorithm"),ko(e,t,n,o,l)}a(Io,"SetUpTransformStreamDefaultControllerFromTransformer");function Gr(e){e._transformAlgorithm=void 0,e._flushAlgorithm=void 0,e._cancelAlgorithm=void 0}a(Gr,"TransformStreamDefaultControllerClearAlgorithms");function bn(e,r){let t=e._controlledTransformStream,n=t._readable._readableStreamController;if(!Oe(n))throw new TypeError("Readable side is not in a state that permits enqueue");try{Se(n,r)}catch(l){throw $t(t,l),t._readable._storedError}let o=Ha(n);o!==t._backpressure&&(i(o),$r(t,!0))}a(bn,"TransformStreamDefaultControllerEnqueue");function Oo(e,r){cn(e._controlledTransformStream,r)}a(Oo,"TransformStreamDefaultControllerError");function fn(e,r){let t=e._transformAlgorithm(r);return L(t,void 0,n=>{throw cn(e._controlledTransformStream,n),n})}a(fn,"TransformStreamDefaultControllerPerformTransform");function qo(e){let r=e._controlledTransformStream,t=r._readable._readableStreamController;ce(t);let n=new TypeError("TransformStream terminated");$t(r,n)}a(qo,"TransformStreamDefaultControllerTerminate");function Do(e,r){i(e._writable._state==="writable");let t=e._transformStreamController;if(e._backpressure){let n=e._backpressureChangePromise;return i(n!==void 0),L(n,()=>{let o=e._writable,l=o._state;if(l==="erroring")throw o._storedError;return i(l==="writable"),fn(t,r)})}return fn(t,r)}a(Do,"TransformStreamDefaultSinkWriteAlgorithm");function Eo(e,r){let t=e._transformStreamController;if(t._finishPromise!==void 0)return t._finishPromise;let n=e._readable;t._finishPromise=_((l,s)=>{t._finishPromise_resolve=l,t._finishPromise_reject=s});let o=t._cancelAlgorithm(r);return Gr(t),w(o,()=>(n._state==="errored"?Ke(t,n._storedError):(N(n._readableStreamController,r),Gt(t)),null),l=>(N(n._readableStreamController,l),Ke(t,l),null)),t._finishPromise}a(Eo,"TransformStreamDefaultSinkAbortAlgorithm");function xo(e){let r=e._transformStreamController;if(r._finishPromise!==void 0)return r._finishPromise;let t=e._readable;r._finishPromise=_((o,l)=>{r._finishPromise_resolve=o,r._finishPromise_reject=l});let n=r._flushAlgorithm();return Gr(r),w(n,()=>(t._state==="errored"?Ke(r,t._storedError):(ce(t._readableStreamController),Gt(r)),null),o=>(N(t._readableStreamController,o),Ke(r,o),null)),r._finishPromise}a(xo,"TransformStreamDefaultSinkCloseAlgorithm");function jo(e){return i(e._backpressure),i(e._backpressureChangePromise!==void 0),$r(e,!1),e._backpressureChangePromise}a(jo,"TransformStreamDefaultSourcePullAlgorithm");function Fo(e,r){let t=e._transformStreamController;if(t._finishPromise!==void 0)return t._finishPromise;let n=e._writable;t._finishPromise=_((l,s)=>{t._finishPromise_resolve=l,t._finishPromise_reject=s});let o=t._cancelAlgorithm(r);return Gr(t),w(o,()=>(n._state==="errored"?Ke(t,n._storedError):(Ge(n._writableStreamController,r),Yt(e),Gt(t)),null),l=>(Ge(n._writableStreamController,l),Yt(e),Ke(t,l),null)),t._finishPromise}a(Fo,"TransformStreamDefaultSourceCancelAlgorithm");function Yr(e){return new TypeError(`TransformStreamDefaultController.prototype.${e} can only be used on a TransformStreamDefaultController`)}a(Yr,"defaultControllerBrandCheckException");function Gt(e){e._finishPromise_resolve!==void 0&&(e._finishPromise_resolve(),e._finishPromise_resolve=void 0,e._finishPromise_reject=void 0)}a(Gt,"defaultControllerFinishPromiseResolve");function Ke(e,r){e._finishPromise_reject!==void 0&&(X(e._finishPromise),e._finishPromise_reject(r),e._finishPromise_resolve=void 0,e._finishPromise_reject=void 0)}a(Ke,"defaultControllerFinishPromiseReject");function mn(e){return new TypeError(`TransformStream.prototype.${e} can only be used on a TransformStream`)}a(mn,"streamBrandCheckException");export{De as ByteLengthQueuingStrategy,Ee as CountQueuingStrategy,Y as ReadableByteStreamController,A as ReadableStream,ae as ReadableStreamBYOBReader,de as ReadableStreamBYOBRequest,G as ReadableStreamDefaultController,J as ReadableStreamDefaultReader,He as TransformStream,be as TransformStreamDefaultController,me as WritableStream,_e as WritableStreamDefaultController,ne as WritableStreamDefaultWriter}; diff --git a/src/compat/stream-wrap/LICENSE-3RD-PARTY.md b/src/compat/stream-wrap/LICENSE-3RD-PARTY.md new file mode 100644 index 00000000..5af46473 --- /dev/null +++ b/src/compat/stream-wrap/LICENSE-3RD-PARTY.md @@ -0,0 +1,11 @@ +# Upstream notices + +This module ports Cloudflare workerd tag `v1.20261012.1`, revision +`5358e3d337336eaf696a48bc1563d808b10feba5`: + +- `src/node/_tls_common.ts` +- `src/node/internal/internal_tls_common.ts` + +Copyright Cloudflare, Inc.; Apache License 2.0. Node.js/Joyent MIT notices +are preserved in the source files. Imports, resource-management symbols and +host constructor references are adapted for the Capacitor runtime. diff --git a/src/compat/stream-wrap/LICENSE-APACHE b/src/compat/stream-wrap/LICENSE-APACHE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/src/compat/stream-wrap/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/stream-wrap/index.ts b/src/compat/stream-wrap/index.ts new file mode 100644 index 00000000..c98434f3 --- /dev/null +++ b/src/compat/stream-wrap/index.ts @@ -0,0 +1 @@ +export { JSStreamSocket as default } from "../tls/jsstream.js"; diff --git a/src/compat/tls-wrap/LICENSE-3RD-PARTY.md b/src/compat/tls-wrap/LICENSE-3RD-PARTY.md new file mode 100644 index 00000000..5af46473 --- /dev/null +++ b/src/compat/tls-wrap/LICENSE-3RD-PARTY.md @@ -0,0 +1,11 @@ +# Upstream notices + +This module ports Cloudflare workerd tag `v1.20261012.1`, revision +`5358e3d337336eaf696a48bc1563d808b10feba5`: + +- `src/node/_tls_common.ts` +- `src/node/internal/internal_tls_common.ts` + +Copyright Cloudflare, Inc.; Apache License 2.0. Node.js/Joyent MIT notices +are preserved in the source files. Imports, resource-management symbols and +host constructor references are adapted for the Capacitor runtime. diff --git a/src/compat/tls-wrap/LICENSE-APACHE b/src/compat/tls-wrap/LICENSE-APACHE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/src/compat/tls-wrap/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/tls-wrap/index.ts b/src/compat/tls-wrap/index.ts new file mode 100644 index 00000000..4bc14888 --- /dev/null +++ b/src/compat/tls-wrap/index.ts @@ -0,0 +1,3 @@ +export { connect, TLSSocket } from "../tls/wrap.js"; +import { connect, TLSSocket } from "../tls/wrap.js"; +export default { connect, TLSSocket }; diff --git a/src/compat/tls/LICENSE-3RD-PARTY.md b/src/compat/tls/LICENSE-3RD-PARTY.md new file mode 100644 index 00000000..5af46473 --- /dev/null +++ b/src/compat/tls/LICENSE-3RD-PARTY.md @@ -0,0 +1,11 @@ +# Upstream notices + +This module ports Cloudflare workerd tag `v1.20261012.1`, revision +`5358e3d337336eaf696a48bc1563d808b10feba5`: + +- `src/node/_tls_common.ts` +- `src/node/internal/internal_tls_common.ts` + +Copyright Cloudflare, Inc.; Apache License 2.0. Node.js/Joyent MIT notices +are preserved in the source files. Imports, resource-management symbols and +host constructor references are adapted for the Capacitor runtime. diff --git a/src/compat/tls/LICENSE-APACHE b/src/compat/tls/LICENSE-APACHE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/src/compat/tls/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/tls/README.md b/src/compat/tls/README.md new file mode 100644 index 00000000..8f25abfd --- /dev/null +++ b/src/compat/tls/README.md @@ -0,0 +1,27 @@ +# Workers TLS adapter + +Adapted from workerd 5358e3d337336eaf696a48bc1563d808b10feba5. +The public tls, \_tls_wrap and \_stream_wrap imports share their constructors. +Certificate hostname checks, ALPN conversion, constants, options validation and +Workers stubs follow the pinned profile. Direct TLS connections use the existing +native socket transport and platform certificate verification. The original +hostname or explicit servername survives DNS resolution and reaches the native +TLS verifier and SNI configuration. + +The native expansion suite verifies successful TLS exchange and hostname mismatch +rejection in both page and Dedicated Worker on iOS and Android. Dedicated Workers +use the normal owned socket relay, including disposal. Trust overrides are confined +to the independent debug test consumer and simulator CA store. + +Existing native plaintext sockets upgrade on the same TCP connection, preserving +protocol state. iOS uses OpenSSL memory BIOs with mandatory SecTrust chain and +hostname validation; Android uses the existing Netty channel and platform +SSLEngine. Application reads and writes wait for authentication. The native suite +covers TLS 1.2 and 1.3, repeated write-half-close followed by response reads, and +rejection of hostname mismatch and an untrusted root in both execution modes. +Custom Duplex wrapping remains unsupported. + +Whole-module acceptance remains open. Further socket lifecycle/prototype and +error-path coverage is also required. Workers itself rejects server mode, custom +verification callbacks, ALPN negotiation options and several introspection APIs +in the pinned profile; retain those explicit failures. diff --git a/src/compat/tls/constants.ts b/src/compat/tls/constants.ts new file mode 100644 index 00000000..2333cc5c --- /dev/null +++ b/src/compat/tls/constants.ts @@ -0,0 +1,17 @@ +// @ts-nocheck +import { Buffer } from "node:buffer"; +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +// Allow {CLIENT_RENEG_LIMIT} client-initiated session renegotiations +// every {CLIENT_RENEG_WINDOW} seconds. An error event is emitted if more +// renegotiations are seen. The settings are applied to all remote client +// connections. +export const CLIENT_RENEG_LIMIT = 3; +export const CLIENT_RENEG_WINDOW = 600; +export const DEFAULT_CIPHERS = ""; +export const DEFAULT_ECDH_CURVE = "auto"; +export const DEFAULT_MIN_VERSION = "TLSv1.2"; +export const DEFAULT_MAX_VERSION = "TLSv1.3"; +export const rootCertificates = []; diff --git a/src/compat/tls/errors.ts b/src/compat/tls/errors.ts new file mode 100644 index 00000000..818b3407 --- /dev/null +++ b/src/compat/tls/errors.ts @@ -0,0 +1,93 @@ +// @ts-nocheck +// Adapted from pinned workerd internal_errors.ts (Apache-2.0 / Node MIT). +import { NodeError, NodeTypeError } from "../net/error-base.js"; +import type { PeerCertificate } from "node:tls"; +class NodeSyntaxError extends SyntaxError { + constructor(code, message) { + super(message); + this.code = code; + } +} +export { ERR_OUT_OF_RANGE, ERR_OPTION_NOT_IMPLEMENTED } from "../net/errors.js"; +export class ERR_TLS_HANDSHAKE_TIMEOUT extends NodeError { + constructor() { + super("ERR_TLS_HANDSHAKE_TIMEOUT", "TLS handshake timeout"); + } +} + +export class ERR_TLS_INVALID_CONTEXT extends NodeTypeError { + constructor(field: string) { + super("ERR_TLS_INVALID_CONTEXT", `${field} must be a SecureContext`); + } +} + +export class ERR_TLS_CERT_ALTNAME_INVALID extends NodeError { + reason: string; + host: string; + cert?: Partial | undefined; + + constructor(reason: string, host: string, cert?: Partial) { + super("ERR_TLS_CERT_ALTNAME_INVALID", reason); + this.reason = reason; + this.host = host; + this.cert = cert; + } +} + +export class ERR_TLS_CERT_ALTNAME_FORMAT extends NodeSyntaxError { + constructor() { + super( + "ERR_TLS_CERT_ALTNAME_FORMAT", + "Invalid subject alternative name string", + ); + } +} + +export class ConnResetException extends NodeError { + path?: string | undefined; + host?: string | undefined; + port?: number | undefined; + localAddress?: string | undefined; + + constructor(message: string) { + super("ECONNRESET", message); + } +} + +export function aggregateTwoErrors( + innerError: unknown, + outerError: Error | null | undefined, +): AggregateError { + if (innerError && outerError && innerError !== outerError) { + if ("errors" in outerError && Array.isArray(outerError.errors)) { + // If `outerError` is already an `AggregateError`. + outerError.errors.push(innerError); + return outerError as AggregateError; + } + const err = new AggregateError( + [outerError, innerError], + outerError.message, + ); + (err as unknown as NodeError).code = (outerError as NodeError).code; + return err; + } + return (innerError || outerError) as AggregateError; +} + +export class ERR_STREAM_WRAP extends NodeError { + constructor() { + super( + "ERR_STREAM_WRAP", + "Stream has StringDecoder set or is in objectMode", + ); + } +} + +export class ERR_METHOD_NOT_IMPLEMENTED extends NodeError { + constructor(name: string) { + super( + "ERR_METHOD_NOT_IMPLEMENTED", + `The ${name} method is not implemented`, + ); + } +} diff --git a/src/compat/tls/helpers.ts b/src/compat/tls/helpers.ts new file mode 100644 index 00000000..62910970 --- /dev/null +++ b/src/compat/tls/helpers.ts @@ -0,0 +1,277 @@ +// @ts-nocheck +import { Buffer } from "node:buffer"; +// 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 type { PeerCertificate } from "node:tls"; +import { isIP } from "../net/internal-net.js"; +const urlUtil = { + canonicalizeIp(ip: string) { + // URL's IPv6 serializer expands equivalent spellings to one canonical form. + if (isIP(ip) === 6) return new URL(`http://[${ip}]/`).hostname.slice(1, -1); + return ip; + }, +}; +import { + ERR_TLS_CERT_ALTNAME_INVALID, + ERR_TLS_CERT_ALTNAME_FORMAT, + ERR_OUT_OF_RANGE, +} from "./errors.js"; +import { isUint8Array, isArrayBufferView } from "../util/types/index.js"; + +// String#toLowerCase() is locale-sensitive so we use +// a conservative version that only lowercases A-Z. +function toLowerCase(c: string): string { + return String.fromCharCode(32 + c.charCodeAt(0)); +} + +function unfqdn(host: string): string { + return host.replace(/[.]$/, ""); +} + +function splitHost(host: string): string[] { + return unfqdn(host).replace(/[A-Z]/g, toLowerCase).split("."); +} + +function check( + hostParts: string[], + pattern: string | undefined | null, + wildcards: boolean, +): boolean { + // Empty strings, null, undefined, etc. never match. + if (!pattern) return false; + + const patternParts = splitHost(pattern); + + if (hostParts.length !== patternParts.length) return false; + + // Pattern has empty components, e.g. "bad..example.com". + if (patternParts.includes("")) return false; + + // RFC 6125 allows IDNA U-labels (Unicode) in names but we have no + // good way to detect their encoding or normalize them so we simply + // reject them. Control characters and blanks are rejected as well + // because nothing good can come from accepting them. + const isBad = (s: string): boolean => /[^\u0021-\u007F]/u.test(s); + if (patternParts.some(isBad)) return false; + + // Check host parts from right to left first. + for (let i = hostParts.length - 1; i > 0; i -= 1) { + if (hostParts[i] !== patternParts[i]) return false; + } + + const hostSubdomain = hostParts[0] as string; + const patternSubdomain = patternParts[0] as string; + const patternSubdomainParts = patternSubdomain.split("*", 3); + + // Short-circuit when the subdomain does not contain a wildcard. + // RFC 6125 does not allow wildcard substitution for components + // containing IDNA A-labels (Punycode) so match those verbatim. + if (patternSubdomainParts.length === 1 || patternSubdomain.includes("xn--")) + return hostSubdomain === patternSubdomain; + + if (!wildcards) return false; + + // More than one wildcard is always wrong. + if (patternSubdomainParts.length > 2) return false; + + // *.tld wildcards are not allowed. + if (patternParts.length <= 2) return false; + + const { 0: prefix, 1: suffix } = patternSubdomainParts as [string, string]; + + if (prefix.length + suffix.length > hostSubdomain.length) return false; + + if (!hostSubdomain.startsWith(prefix)) return false; + + if (!hostSubdomain.endsWith(suffix)) return false; + + return true; +} + +// This pattern is used to determine the length of escaped sequences within +// the subject alt names string. It allows any valid JSON string literal. +// This MUST match the JSON specification (ECMA-404 / RFC8259) exactly. +const jsonStringPattern = + // eslint-disable-next-line no-control-regex + /^"(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"/; + +function splitEscapedAltNames(altNames: string): string[] { + const result = []; + let currentToken = ""; + let offset = 0; + while (offset !== altNames.length) { + const nextSep = altNames.indexOf(",", offset); + const nextQuote = altNames.indexOf('"', offset); + if (nextQuote !== -1 && (nextSep === -1 || nextQuote < nextSep)) { + // There is a quote character and there is no separator before the quote. + currentToken += altNames.substring(offset, nextQuote); + const match = jsonStringPattern.exec(altNames.substring(nextQuote)); + if (!match) { + throw new ERR_TLS_CERT_ALTNAME_FORMAT(); + } + currentToken += JSON.parse(match[0]) as string; + offset = nextQuote + match[0].length; + } else if (nextSep !== -1) { + // There is a separator and no quote before it. + currentToken += altNames.substring(offset, nextSep); + result.push(currentToken); + currentToken = ""; + offset = nextSep + 2; + } else { + currentToken += altNames.substring(offset); + offset = altNames.length; + } + } + result.push(currentToken); + return result; +} + +export function checkServerIdentity( + hostname: string, + cert: Partial, +): Error | undefined { + const subject = cert.subject; + const altNames = cert.subjectaltname; + const dnsNames: string[] = []; + const ips: string[] = []; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion + hostname = "" + hostname; + + if (altNames) { + const splitAltNames = altNames.includes('"') + ? splitEscapedAltNames(altNames) + : altNames.split(", "); + splitAltNames.forEach((name) => { + if (name.startsWith("DNS:")) { + dnsNames.push(name.slice(4)); + } else if (name.startsWith("IP Address:")) { + ips.push(urlUtil.canonicalizeIp(name.slice(11))); + } + }); + } + + let valid = false; + let reason = "Unknown reason"; + + hostname = unfqdn(hostname); // Remove trailing dot for error messages. + + if (isIP(hostname)) { + valid = ips.includes(urlUtil.canonicalizeIp(hostname)); + if (!valid) + reason = `IP: ${hostname} is not in the cert's list: ` + ips.join(", "); + } else if (dnsNames.length > 0 || subject?.CN) { + const hostParts = splitHost(hostname); + const wildcard = (pattern: string): boolean => + check(hostParts, pattern, true); + + if (dnsNames.length > 0) { + valid = dnsNames.some(wildcard); + if (!valid) + reason = `Host: ${hostname}. is not in the cert's altnames: ${altNames}`; + } else { + // Match against Common Name only if no supported identifiers exist. + const cn = subject?.CN; + + if (Array.isArray(cn)) valid = cn.some(wildcard); + else if (cn) valid = wildcard(cn); + + if (!valid) reason = `Host: ${hostname}. is not cert's CN: ${cn}`; + } + } else { + reason = "Cert does not contain a DNS name"; + } + + if (!valid) { + return new ERR_TLS_CERT_ALTNAME_INVALID(reason, hostname, cert); + } + return undefined; +} + +// Convert protocols array into valid OpenSSL protocols list +// ("\x06spdy/2\x08http/1.1\x08http/1.0") +function convertProtocols(protocols: string[]): Buffer { + const lens = Array.from({ length: protocols.length }, () => 0); + const buff = Buffer.allocUnsafe( + protocols.reduce((p, c, i) => { + const len = Buffer.byteLength(c); + if (len > 255) { + throw new ERR_OUT_OF_RANGE( + "The byte length of the protocol at index " + + `${i} exceeds the maximum length.`, + "<= 255", + len, + true, + ); + } + lens[i] = len; + return p + 1 + len; + }, 0), + ); + + let offset = 0; + for (let i = 0, c = protocols.length; i < c; i++) { + buff[offset++] = lens[i] as number; + buff.write(protocols[i] as string, offset); + offset += lens[i] as number; + } + + return buff; +} + +export function convertALPNProtocols( + protocols: unknown, + out: { + ALPNProtocols: Buffer; + }, +): void { + // If protocols is Array - translate it into buffer + if (Array.isArray(protocols)) { + out.ALPNProtocols = convertProtocols(protocols as string[]); + } else if (isUint8Array(protocols)) { + // Copy new buffer not to be modified by user. + out.ALPNProtocols = Buffer.from(protocols); + } else if (isArrayBufferView(protocols)) { + out.ALPNProtocols = Buffer.from( + protocols.buffer.slice( + protocols.byteOffset, + protocols.byteOffset + protocols.byteLength, + ), + ); + } +} + +export function createServer(): void { + throw new Error("Not implemented"); +} + +export function Server(): void { + throw new Error("Not implemented"); +} + +export function getCiphers(): void { + throw new Error("Not implemented"); +} diff --git a/src/compat/tls/index.ts b/src/compat/tls/index.ts new file mode 100644 index 00000000..abe66449 --- /dev/null +++ b/src/compat/tls/index.ts @@ -0,0 +1,73 @@ +// @ts-nocheck +import { Buffer } from "node:buffer"; +// 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 { + checkServerIdentity, + convertALPNProtocols, + createServer, + Server, + getCiphers, +} from "./helpers.js"; +import { createSecureContext, SecureContext } from "../tls-common/index.js"; +import * as constants from "./constants.js"; +import { TLSSocket, connect } from "./wrap.js"; +import { ERR_METHOD_NOT_IMPLEMENTED } from "./errors.js"; + +let createSecurePair = undefined; + +if (true) { + createSecurePair = function createSecurePair(): void { + throw new ERR_METHOD_NOT_IMPLEMENTED("createSecurePair"); + }; +} + +export * from "./constants.js"; +export { + TLSSocket, + connect, + createSecureContext, + createServer, + checkServerIdentity, + SecureContext, + Server, + convertALPNProtocols, + getCiphers, + createSecurePair, +}; +export default { + SecureContext, + Server, + TLSSocket, + connect, + createSecureContext, + createServer, + checkServerIdentity, + convertALPNProtocols, + getCiphers, + createSecurePair, + ...constants, +}; diff --git a/src/compat/tls/jsstream.ts b/src/compat/tls/jsstream.ts new file mode 100644 index 00000000..752401b9 --- /dev/null +++ b/src/compat/tls/jsstream.ts @@ -0,0 +1,221 @@ +// @ts-nocheck +import { Buffer } from "node:buffer"; +// 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. + +/* eslint-disable @typescript-eslint/no-redundant-type-constituents */ + +import { notStrictEqual } from "../assert/index.js"; +import { Socket } from "../net/internal-net.js"; +import { ERR_STREAM_WRAP } from "./errors.js"; +import { Duplex, toBYOBWeb } from "../stream/duplex.js"; +import type { + SocketInfo, + Writer, + Socket as CloudflareSocket, +} from "node-internal:sockets"; + +const kCurrentWriteRequest = Symbol("kCurrentWriteRequest"); +const kCurrentShutdownRequest = Symbol("kCurrentShutdownRequest"); +const kPendingShutdownRequest = Symbol("kPendingShutdownRequest"); +const kPendingClose = Symbol("kPendingClose"); + +/* This class serves as a wrapper for when the C++ side of Node wants access + * to a standard JS stream. For example, TLS or HTTP do not operate on network + * resources conceptually, although that is the common case and what we are + * optimizing for; in theory, they are completely composable and can work with + * any stream resource they see. + * + * For the common case, i.e. a TLS socket wrapping around a net.Socket, we + * can skip going through the JS layer and let TLS access the raw C++ handle + * of a net.Socket. The flipside of this is that, to maintain composability, + * we need a way to create "fake" net.Socket instances that call back into a + * "real" JavaScript stream. JSStreamSocket is exactly this. + */ +export class JSStreamSocket extends Socket { + stream: Duplex; + [kCurrentWriteRequest]: null | unknown; + [kCurrentShutdownRequest]: null | unknown; + [kPendingShutdownRequest]: null | unknown; + [kPendingClose]: boolean; + + constructor(stream: Duplex) { + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + const closePromise = Promise.withResolvers(); + const openPromise = Promise.withResolvers(); + + const webStream = toBYOBWeb(stream); + Object.assign(webStream.writable, { + // eslint-disable-next-line @typescript-eslint/require-await + write: async (data: string | ArrayBufferView): Promise => { + stream.write(data); + }, + closed: closePromise.promise, + releaseLock: async (): Promise => {}, + }); + const handle: Socket["_handle"] = { + reading: true, + bytesRead: 0, + bytesWritten: 0, + socket: { + startTls(): CloudflareSocket { + throw new Error( + "startTls() should not be called for a duplex stream", + ); + }, + upgraded: false, + secureTransport: "off", + closed: closePromise.promise, + close: async (): Promise => { + queueMicrotask(() => { + closePromise.resolve(); + }); + return closePromise.promise; + }, + opened: openPromise.promise, + readable: webStream.readable, + writable: webStream.writable as unknown as Writer, + }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + reader: new ReadableStreamBYOBReader(webStream.readable), + writer: new WritableStreamDefaultWriter(webStream.writable), + options: { + host: "0.0.0.0", + port: 0, + addressType: 4, + }, + }; + + stream.pause(); + stream.on("error", (err) => this.emit("error", err)); + const ondata = (chunk: string | Buffer): void => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare + if (typeof chunk === "string" || stream.readableObjectMode === true) { + // Make sure that no further `data` events will happen. + stream.pause(); + stream.removeListener("data", ondata); + + this.emit("error", new ERR_STREAM_WRAP()); + return; + } + + // TODO(soon): We need to trigger read() result for _handle.reader.read(buf) call + // in node:net. + }; + stream.on("data", ondata); + stream.once("end", () => { + closePromise.resolve(); + }); + // Some `Stream` don't pass `hasError` parameters when closed. + stream.once("close", () => { + // Errors emitted from `stream` have also been emitted to this instance + // so that we don't pass errors to `destroy()` again. + this.destroy(); + }); + + super({ handle }); + this.stream = stream; + this[kCurrentWriteRequest] = null; + this[kCurrentShutdownRequest] = null; + this[kPendingShutdownRequest] = null; + this[kPendingClose] = false; + this.readable = stream.readable; + this.writable = stream.writable; + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + handle.socket.closed.then(this.doClose.bind(this)); + + openPromise.resolve({}); + + // Start reading. + this.read(0); + } + + isClosing(): boolean { + return !this.readable || !this.writable; + } + + readStart(): number { + this.stream.resume(); + return 0; + } + + readStop(): number { + this.stream.pause(); + return 0; + } + + doShutdown(req: unknown): number { + // TODO(addaleax): It might be nice if we could get into a state where + // DoShutdown() is not called on streams while a write is still pending. + // + // Currently, the only part of the code base where that happens is the + // TLS implementation, which calls both DoWrite() and DoShutdown() on the + // underlying network stream inside of its own DoShutdown() method. + // Working around that on the native side is not quite trivial (yet?), + // so for now that is supported here. + + if (this[kCurrentWriteRequest] !== null) { + this[kPendingShutdownRequest] = req; + return 0; + } + + this[kCurrentShutdownRequest] = req; + + if (this[kPendingClose]) { + // If doClose is pending, the stream & this._handle are gone. We can't do + // anything. doClose will call finishShutdown with ECANCELED for us shortly. + return 0; + } + + const handle = this._handle; + notStrictEqual(handle, null); + + queueMicrotask(() => { + // Ensure that write is dispatched asynchronously. + this.stream.end(); + }); + return 0; + } + + doClose(): void { + this[kPendingClose] = true; + + const handle = this._handle; + + // When sockets of the "net" module destroyed, they will call + // `this._handle.close()` which will also emit EOF if not emitted before. + // This feature makes sockets on the other side emit "end" and "close" + // even though we haven't called `end()`. As `stream` are likely to be + // instances of `net.Socket`, calling `stream.destroy()` manually will + // avoid issues that don't properly close wrapped connections. + this.stream.destroy(); + + queueMicrotask(() => { + notStrictEqual(handle, null); + this[kPendingClose] = false; + }); + } +} diff --git a/src/compat/tls/native-session.ts b/src/compat/tls/native-session.ts new file mode 100644 index 00000000..afcfa685 --- /dev/null +++ b/src/compat/tls/native-session.ts @@ -0,0 +1,181 @@ +import { NativeTLS } from "../crypto/native-crypto.js"; +import type { NativeCall } from "../../browser/fetch/native-network.js"; + +function decode(value: string) { + return Uint8Array.from(atob(value), (character) => character.charCodeAt(0)); +} +function encode(value: Uint8Array) { + let text = ""; + for (let i = 0; i < value.length; i += 8192) + text += String.fromCharCode(...value.subarray(i, i + 8192)); + return btoa(text); +} +/** Memory-BIO encryption on the original TCP stream, with mandatory OS trust. */ +export async function nativeTlsSession( + call: NativeCall, + id: string, + hostname: string, +): Promise { + const tls = new NativeTLS(hostname); + let stopped = false, + eof = false, + pumping = false; + let buffered: Uint8Array | undefined; + let incoming: Promise | undefined; + let outgoing = Promise.resolve(); + let reading: + | { + count: number; + resolve: (value: unknown) => void; + reject: (error: unknown) => void; + } + | undefined; + let resolveWatch!: (value: unknown) => void, + rejectWatch!: (error: unknown) => void; + const watched = new Promise((resolve, reject) => { + resolveWatch = resolve; + rejectWatch = reject; + }); + void watched.catch(() => {}); + const stop = (error?: unknown) => { + if (stopped) return; + stopped = true; + tls.dispose(); + buffered = undefined; + reading?.reject(error ?? new Error("TLS session closed")); + reading = undefined; + if (error) rejectWatch(error); + else resolveWatch({}); + }; + const flush = () => { + for (const data of tls.drain()) { + outgoing = outgoing.then(async () => { + if (stopped) throw new Error("TLS session closed"); + await call("WorkersCapacitor", "socketWrite", { + id, + chunk: encode(data), + }); + }); + } + return outgoing; + }; + const receive = (): Promise => + (incoming ??= (async () => { + const value = (await call("WorkersCapacitor", "socketRead", { + id, + maxBytes: 65536, + })) as { chunk?: string; eof?: boolean }; + if (stopped) throw new Error("TLS session closed"); + if (value.eof) + throw new Error("TLS connection closed without close_notify"); + if (typeof value.chunk !== "string") + throw new Error("Invalid TLS transport response"); + tls.feed(decode(value.chunk)); + })().finally(() => { + incoming = undefined; + })); + const retry = async (status: number) => { + await flush(); + if (status === -2) await receive(); + else if (status !== -3) throw new Error(`TLS operation failed (${status})`); + }; + const deliver = () => { + if (buffered && reading) { + const pending = reading; + reading = undefined; + const data = buffered.subarray(0, pending.count); + buffered = + buffered.byteLength > data.byteLength + ? buffered.subarray(data.byteLength) + : undefined; + pending.resolve({ chunk: encode(data) }); + } + if (eof && !buffered) { + reading?.resolve({ eof: true }); + reading = undefined; + resolveWatch({}); + } + }; + const pump = async () => { + if (pumping || stopped) return; + pumping = true; + try { + while (!stopped && !eof && !buffered) { + const result = tls.read(); + if (result instanceof Uint8Array) { + buffered = result; + deliver(); + } else if (result === -6) { + eof = true; + deliver(); + } else await retry(result); + } + } catch (error) { + stop(error); + } finally { + pumping = false; + } + }; + try { + for (;;) { + const status = tls.handshake(); + if (status === 1) break; + await retry(status); + } + await flush(); + const certificates = tls.certificates(); + if (!certificates.length) + throw new Error("TLS peer did not present a certificate"); + // No application reads/writes or successful open are exposed before this. + await call("WorkersCapacitor", "socketVerifyTls", { + id, + servername: hostname, + certificates: certificates.map(encode), + }); + } catch (error) { + stop(error); + throw error; + } + void pump(); + return async (plugin, method, args) => { + if (method === "socketClose") { + stop(); + return call(plugin, method, args); + } + if (method === "socketWatch") return watched; + if (stopped) throw new Error("TLS session closed"); + if (method === "socketRead") { + if (reading) throw new Error("TLS read already pending"); + return new Promise((resolve, reject) => { + reading = { count: Number(args.maxBytes ?? 65536), resolve, reject }; + deliver(); + void pump(); + }); + } + try { + if (method === "socketWrite") { + const data = decode(String(args.chunk)); + for (let offset = 0; offset < data.length; ) { + const status = tls.write(data.subarray(offset)); + if (status > 0) offset += status; + else await retry(status); + } + await flush(); + return {}; + } + if (method === "socketEnd") { + for (;;) { + const status = tls.shutdown(); + if (status >= 0) break; + await retry(status); + } + await flush(); + return {}; + } + return await call(plugin, method, args); + } catch (error) { + stop(error); + throw error; + } + }; +} diff --git a/src/compat/tls/upgrade.spec.ts b/src/compat/tls/upgrade.spec.ts new file mode 100644 index 00000000..8e61798c --- /dev/null +++ b/src/compat/tls/upgrade.spec.ts @@ -0,0 +1,110 @@ +import { afterEach, expect, it, vi } from "vitest"; +import { connect as tcp, Socket } from "../net/index.js"; +import { connect, TLSSocket } from "./index.js"; +import { + setNativeCall, + type NativeCall, +} from "../../browser/fetch/native-network.js"; +let restore: (() => void) | undefined; +afterEach(() => { + restore?.(); + restore = undefined; +}); +const tick = () => new Promise((resolve) => setTimeout(resolve, 10)); +function bridge() { + let resolve!: (value: unknown) => void, reject!: (error: Error) => void; + const handshake = new Promise((yes, no) => { + resolve = yes; + reject = no; + }); + const call = vi.fn(async (_, method) => { + if (method === "socketStartTls") return handshake; + if (method === "socketRead" || method === "socketWatch") + return new Promise(() => {}); + return {}; + }); + restore = setNativeCall(call); + return { call, resolve, reject }; +} +it("holds immediate application writes until the existing connection authenticates", async () => { + const { call, resolve } = bridge(); + const plain = tcp({ host: "localhost", port: 1234 }); + plain.on("error", () => {}); + await new Promise((yes) => plain.once("connect", yes)); + const tls = connect({ socket: plain, servername: "localhost" }); + tls.on("error", () => {}); + tls.write("secret"); + await tick(); + expect( + call.mock.calls.filter(([, m]) => m === "socketStartTls"), + ).toHaveLength(1); + expect(call.mock.calls.some(([, m]) => m === "socketWrite")).toBe(false); + resolve({}); + await tick(); + expect(tls.authorized).toBe(true); + expect(call.mock.calls.filter(([, m]) => m === "socketOpen")).toHaveLength(1); + expect(call.mock.calls.filter(([, m]) => m === "socketWrite")).toHaveLength( + 1, + ); + tls.destroy(); + plain.destroy(); +}); +it("never transmits queued application bytes when certificate verification fails", async () => { + const { call, reject } = bridge(); + const plain = tcp({ host: "localhost", port: 1234 }); + plain.on("error", () => {}); + await new Promise((yes) => plain.once("connect", yes)); + const tls = connect({ socket: plain, servername: "wrong.invalid" }); + const error = new Promise((yes) => tls.once("error", yes)); + tls.write("secret"); + await tick(); + reject(new Error("certificate mismatch")); + await error; + await tick(); + expect(call.mock.calls.some(([, m]) => m === "socketWrite")).toBe(false); + expect(call.mock.calls.some(([, m]) => m === "socketClose")).toBe(true); + plain.destroy(); +}); +it("upgrades a parent connected after TLS wrapping without opening another connection", async () => { + const { call, resolve } = bridge(); + const plain = new Socket(); + plain.on("error", () => {}); + const tls = connect({ socket: plain, servername: "localhost" }); + tls.on("error", () => {}); + tls.write("secret"); + await tick(); + expect(call.mock.calls).toHaveLength(0); + plain.connect({ host: "localhost", port: 1234 }); + await tick(); + expect( + call.mock.calls.filter(([, m]) => m === "socketStartTls"), + ).toHaveLength(1); + resolve({}); + await tick(); + expect(call.mock.calls.filter(([, m]) => m === "socketWrite")).toHaveLength( + 1, + ); + tls.destroy(); + plain.destroy(); +}); + +it("closes the connection when destroyed during authentication", async () => { + const { call, resolve } = bridge(); + const plain = tcp({ host: "localhost", port: 1234 }); + plain.on("error", () => {}); + await new Promise((yes) => plain.once("connect", yes)); + const tls = connect({ socket: plain, servername: "localhost" }); + tls.on("error", () => {}); + tls.write("secret"); + await tick(); + expect(() => new TLSSocket(plain, { port: 1234 })).toThrow( + /unavailable socket/, + ); + tls.destroy(); + await tick(); + resolve({}); + await tick(); + expect(call.mock.calls.some(([, m]) => m === "socketClose")).toBe(true); + expect(call.mock.calls.some(([, m]) => m === "socketWrite")).toBe(false); + plain.destroy(); +}); diff --git a/src/compat/tls/wrap.ts b/src/compat/tls/wrap.ts new file mode 100644 index 00000000..55c29924 --- /dev/null +++ b/src/compat/tls/wrap.ts @@ -0,0 +1,809 @@ +// @ts-nocheck +import { ERR_INVALID_ARG_TYPE } from "../net/errors.js"; +import { isArrayBufferView } from "../util/types/index.js"; +function validateBuffer(value: unknown, name: string) { + if (!isArrayBufferView(value)) + throw new ERR_INVALID_ARG_TYPE( + name, + ["Buffer", "TypedArray", "DataView"], + value, + ); +} +import { Buffer } from "node:buffer"; +import { socketReader } from "../net/reader.js"; +// 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 { + Socket, + type SocketOptions, + _normalizeArgs, + onConnectionOpened, + onConnectionClosed, + tryReadStart, +} from "../net/internal-net.js"; +import { JSStreamSocket } from "./jsstream.js"; +import type { + ConnectionOptions, + TlsOptions, + TLSSocket as TLSSocketType, +} from "node:tls"; +import type { Duplex } from "../stream/duplex.js"; +import type { OnReadOpts, TcpSocketConnectOpts } from "node:net"; +import { + validateString, + validateObject, + validateNumber, + validateFunction, + validateInt32, + validateUint32, +} from "../net/validators.js"; +import { + ConnResetException, + ERR_TLS_HANDSHAKE_TIMEOUT, + ERR_OPTION_NOT_IMPLEMENTED, + ERR_TLS_INVALID_CONTEXT, + ERR_METHOD_NOT_IMPLEMENTED, +} from "./errors.js"; +import { SecureContext } from "../tls-common/index.js"; +const processImpl = { shouldThrowOnNotImplementedTlsOption: () => true }; +import { ok } from "../assert/index.js"; + +const kConnectOptions = Symbol("connect-options"); +const kErrorEmitted = Symbol("error-emitted"); +const kRes = Symbol("res"); +const kPendingSession = Symbol("pendingSession"); +const kIsVerified = Symbol("verified"); + +// @ts-expect-error TS2323 Cannot redeclare error. +export declare class TLSSocket extends Socket { + _hadError: boolean; + _handle: Socket["_handle"]; + _init(): void; + _tlsOptions: TlsOptions & + ConnectionOptions & + SocketOptions & + TcpSocketConnectOpts & { + isServer?: boolean; + requestOCSP?: boolean; + server?: unknown; + onread?: OnReadOpts; + }; + _secureEstablished: boolean; + _securePending: boolean; + _newSessionPending: boolean; + _controlReleased: boolean; + authorized: boolean; + encrypted: boolean; + handle: ReturnType; + servername: null | string; + secureConnecting: boolean; + ssl: TLSSocket["_handle"]; + [kRes]: null | Socket["_handle"]; + [kIsVerified]: boolean; + [kPendingSession]: null | Buffer; + [kErrorEmitted]: boolean; + [kConnectOptions]?: NormalizedConnectionOptions; + + constructor( + socket: Socket | Duplex | undefined, + opts: TLSSocket["_tlsOptions"], + ); + prototype: TLSSocket; + + _destroySSL(): void; + _emitTLSError(error: Error): void; + _finishInit(): void; + _handleTimeout(): void; + _releaseControl(): boolean; + _start(): void; + _tlsError(error: Error): Error | null; + _wrapHandle( + wrap: null | Socket, + handle: Socket["_handle"] | null | undefined, + wrapHasActiveWriteFromPrevOwner: boolean, + ): unknown; + disableRenegotiation(): void; + getX509Certificate(): ReturnType; + setKeyCert(context: unknown): void; + setServername(name: string): void; + setSession(session: string | Buffer): void; + setMaxSendFragment(size: number): boolean; + getCertificate(): ReturnType; + getPeerX509Certificate(): ReturnType; + renegotiate( + options: { + rejectUnauthorized?: boolean | undefined; + requestCert?: boolean | undefined; + }, + callback?: (error: Error | null) => void, + ): boolean; + exportKeyingMaterial(length: number, label: string, context?: Buffer): Buffer; +} + +function onnewsessionclient( + this: TLSSocket, + _sessionId: string, + session: Buffer, +): void { + if (this[kIsVerified]) { + this.emit("session", session); + } else { + this[kPendingSession] = session; + } +} + +function onerror(this: TLSSocket, err: Error): void { + if (this._hadError) return; + + this._hadError = true; + + // Destroy socket if error happened before handshake's finish + if (!this._secureEstablished) { + // When handshake fails control is not yet released, + // so self._tlsError will return null instead of actual error + + // Set closing the socket after emitting an event since the socket needs to + // be accessible when the `tlsClientError` event is emitted. + this.destroy(err); + } else { + // Emit error + this._emitTLSError(err); + } +} + +// We are using old style function classes for node.js compat. +// @ts-expect-error TS2323 Cannot redeclare error. +export function TLSSocket( + this: TLSSocket, + socket: Socket | Duplex | undefined, + opts: TLSSocket["_tlsOptions"], +): void { + const tlsOptions = { ...opts }; + + if (tlsOptions.enableTrace) { + throw new ERR_OPTION_NOT_IMPLEMENTED("options.enableTrace"); + } + + if (tlsOptions.isServer) { + throw new ERR_OPTION_NOT_IMPLEMENTED("options.isServer"); + } + + if (tlsOptions.server) { + throw new ERR_OPTION_NOT_IMPLEMENTED("options.server"); + } + + if (tlsOptions.requestCert) { + // Servers will request certificate from clients. + // Does not apply to Cloudflare Workers. + throw new ERR_OPTION_NOT_IMPLEMENTED("options.requestCert"); + } + + if (tlsOptions.rejectUnauthorized === false) { + // TODO(soon): We don't support rejectUnauthorized=false + throw new ERR_OPTION_NOT_IMPLEMENTED("options.rejectUnauthorized"); + } + + if (tlsOptions.ALPNProtocols !== undefined) { + // Does not apply to Cloudflare Workers. + throw new ERR_OPTION_NOT_IMPLEMENTED("options.ALPNProtocols"); + } + + if (tlsOptions.SNICallback !== undefined) { + // Does not apply to Cloudflare Workers. + throw new ERR_OPTION_NOT_IMPLEMENTED("options.SNICallback"); + } + + if (tlsOptions.requestOCSP) { + // Not yet supported. Can be implemented in the future. + throw new ERR_OPTION_NOT_IMPLEMENTED("options.requestOCSP"); + } + + if ( + tlsOptions.secureContext !== undefined && + !(tlsOptions.secureContext instanceof SecureContext) + ) { + throw new ERR_TLS_INVALID_CONTEXT("context"); + } + + if (tlsOptions.pskCallback !== undefined) { + // Used for TLS-PSK negotiation. We do not support it. + throw new ERR_OPTION_NOT_IMPLEMENTED("options.pskCallback"); + } + + // checkServerIdentity requires access to the peer certificate via + // getPeerCertificate(), which is not yet implemented. When the + // throw_on_not_implemented_tls_options compat flag is enabled we throw; + // otherwise we silently continue so that existing workers are not broken. + if (tlsOptions.checkServerIdentity !== undefined) { + validateFunction( + tlsOptions.checkServerIdentity, + "options.checkServerIdentity", + ); + if (processImpl.shouldThrowOnNotImplementedTlsOption()) { + throw new ERR_OPTION_NOT_IMPLEMENTED("options.checkServerIdentity"); + } + } + + if (socket != null && !(socket instanceof Socket)) { + throw new ERR_METHOD_NOT_IMPLEMENTED("TLS upgrade of a custom Duplex"); + } + + if ( + socket instanceof Socket && + (socket.destroyed || + (socket._handle && + (socket._handle.handedOver || + socket._handle.socket.secureTransport !== "starttls"))) + ) { + throw new ERR_METHOD_NOT_IMPLEMENTED( + "TLS upgrade of an unavailable socket", + ); + } + this._tlsOptions = tlsOptions; + this._secureEstablished = false; + this._securePending = false; + this._newSessionPending = false; + this._controlReleased = false; + this.secureConnecting = true; + this.servername = tlsOptions.servername ?? null; + this.authorized = false; + this[kRes] = null; + this[kIsVerified] = false; + this[kPendingSession] = null; + this[kErrorEmitted] = false; + + let wrap: Socket | null = null; + let handle: Socket["_handle"] | null = null; + let wrapHasActiveWriteFromPrevOwner = false; + + if (socket) { + if (socket instanceof Socket) { + wrap = socket; + } else { + wrap = new JSStreamSocket(socket); + } + + handle = wrap._handle; + wrapHasActiveWriteFromPrevOwner = wrap.writableLength > 0; + } + + // Just a documented property to make secure sockets + // distinguishable from regular ones. + this.encrypted = true; + + Reflect.apply(Socket, this, [ + { + handle: this._wrapHandle(wrap, handle, wrapHasActiveWriteFromPrevOwner), + allowHalfOpen: socket ? socket.allowHalfOpen : tlsOptions.allowHalfOpen, + pauseOnCreate: tlsOptions.pauseOnConnect, + manualStart: true, + highWaterMark: tlsOptions.highWaterMark, + onread: !socket ? tlsOptions.onread : null, + signal: tlsOptions.signal, + lookup: tlsOptions.lookup, + } as SocketOptions, + ]); + + this._parent = handle; + this._parentWrap = wrap; + + // Proxy for API compatibility + this.ssl = this._handle; // C++ TLSWrap object + + this.on("error", this._tlsError.bind(this)); + + this._init(); + if (socket != null) { + this.connecting = true; + queueMicrotask(() => { + this.connecting = false; + this._start(); + }); + } +} +Object.setPrototypeOf(TLSSocket.prototype, Socket.prototype); +Object.setPrototypeOf(TLSSocket, Socket); + +TLSSocket.prototype.disableRenegotiation = function disableRenegotiation( + this: TLSSocket, +): void { + // Do nothing. +}; + +TLSSocket.prototype._wrapHandle = function _wrapHandle( + this: TLSSocket, + wrap: null | Socket, + handle: Socket["_handle"] | null | undefined, + _wrapHasActiveWriteFromPrevOwner: boolean, +): unknown { + if (!handle) { + return null; + } + this[kRes] = handle; + + // Guard against adding multiple listeners, as this method may be called + // repeatedly on the same socket by reinitializeHandle + if (this.listenerCount("close", onSocketCloseDestroySSL) === 0) { + this.once("close", onSocketCloseDestroySSL); + } + + wrap?.once("close", () => this.destroy()); + + return handle; +}; + +function onSocketCloseDestroySSL(this: TLSSocket): void { + // We call destroySSL directly because it is already an async process. + this._destroySSL(); + this[kRes] = null; +} + +TLSSocket.prototype._destroySSL = function _destroySSL(this: TLSSocket): void { + // We disable floating promises rule because we don't want to change the + // function signature and return Promise + // + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._handle?.socket.close().then(() => { + this[kPendingSession] = null; + this[kIsVerified] = false; + }); +}; + +TLSSocket.prototype._init = function _init(this: TLSSocket): void { + const options = this._tlsOptions; + + this.on("error", onerror.bind(this)); + + // This is emitted from net.Socket class. + this.on("connectionAttempt", onnewsessionclient.bind(this)); + + if (options.handshakeTimeout && options.handshakeTimeout > 0) + this.setTimeout(options.handshakeTimeout, this._handleTimeout.bind(this)); + + // TLSSocket can be initialized with 2 different handles. + // + // 1. Socket instance created by "node:net". In this scenario, we need + // to wait for 'connect' event to be emitted in order to trigger _finishInit(). + // 2. Duplex stream. Duplex streams are initialized through JSStreamSocket class. + // If that's the scenario, we can trigger _finishInit() immediately. Since, there + // is no async calls required to wait. + if (this._parentWrap != null && this._parentWrap instanceof JSStreamSocket) { + queueMicrotask(() => { + this._finishInit(); + ok(this._parentWrap instanceof JSStreamSocket); + this._parentWrap.readStart(); + tryReadStart(this); + }); + } else { + this.on("connect", () => { + this._finishInit(); + }); + } +}; + +TLSSocket.prototype.renegotiate = function ( + this: TLSSocket, + options: { + rejectUnauthorized?: boolean | undefined; + requestCert?: boolean | undefined; + }, + callback?: (error: Error | null) => void, +): boolean { + validateObject(options, "options"); + if (callback !== undefined) { + validateFunction(callback, "callback"); + } + // TLS renegotiation is not supported. + return false; +}; + +TLSSocket.prototype.exportKeyingMaterial = function exportKeyingMaterial( + this: TLSSocket, + length: number, + label: string, + context?: Buffer, +): Buffer { + validateUint32(length, "length", true); + validateString(label, "label"); + if (context !== undefined) { + validateBuffer(context, "context"); + } + + throw new Error("exportKeyingMaterial is not implemented"); +}; + +TLSSocket.prototype.setMaxSendFragment = function setMaxSendFragment( + this: TLSSocket, + size: number, +): boolean { + validateInt32(size, "size"); + // Setting maximum TLS fragment size is not supported. + return false; +}; + +TLSSocket.prototype._handleTimeout = function _handleTimeout( + this: TLSSocket, +): void { + this._emitTLSError(new ERR_TLS_HANDSHAKE_TIMEOUT()); +}; + +TLSSocket.prototype._emitTLSError = function _emitTLSError( + this: TLSSocket, + err: Error, +): void { + const e = this._tlsError(err); + if (e) this.emit("error", e); +}; + +TLSSocket.prototype._tlsError = function _tlsError( + this: TLSSocket, + err: Error, +): Error | null { + this.emit("_tlsError", err); + if (this._controlReleased) return err; + return null; +}; + +TLSSocket.prototype._releaseControl = function _releaseControl( + this: TLSSocket, +): boolean { + if (this._controlReleased) return false; + this._controlReleased = true; + this.removeListener("error", this._tlsError.bind(this)); + return true; +}; + +// This function is called from net.Socket onConnectionOpened() handler. +TLSSocket.prototype._finishInit = function _finishInit(this: TLSSocket): void { + // Guard against getting onhandshakedone() after .destroy(). + // * 1.2: If destroy() during onocspresponse(), then write of next handshake + // record fails, the handshake done info callbacks does not occur, and the + // socket closes. + // * 1.3: The OCSP response comes in the same record that finishes handshake, + // so even after .destroy(), the handshake done info callback occurs + // immediately after onocspresponse(). Ignore it. + if (!this._handle) return; + + this._secureEstablished = true; + if ( + this._tlsOptions.handshakeTimeout && + this._tlsOptions.handshakeTimeout > 0 + ) { + this.setTimeout(0, this._handleTimeout.bind(this)); + } + + this.emit("secure"); +}; + +TLSSocket.prototype._start = function _start(this: TLSSocket): void { + if (this.destroyed) return; + if (this.connecting) { + this.once("connect", this._start.bind(this)); + return; + } + + // If a user calls tls.connect({ socket }) with a socket that is not initialized + // and the socket is not connected, we need to wait for the socket to connect + // before we can complete the process. + // + // Take a look at the following test for this particular edge case: + // https://github.com/nodejs/node/blob/91d8a524ada001103a2d1c6825ca17b8393c183f/test/parallel/test-tls-on-empty-socket.js + if (this._parentWrap != null && this._parentWrap._handle == null) { + this._parentWrap.once("connect", () => { + // We need to update the Socket handle of this TLSSocket + // since it was created after TLSSocket is initialized. + if (this._parentWrap?._handle != null) { + this._handle = this._parentWrap._handle; + } + this._start(); + }); + return; + } + + // Guard against the following cases: + // - Socket was destroyed before the connection was established + // - TLSSocket can not be upgraded if the secureTransport does not support 'starttls' + if (this._handle?.socket.secureTransport !== "starttls") { + return; + } + + // The connection moves to a new handle: the read loop's pending read on + // this one is about to reject, which it must not take for a failure. + this._handle.handedOver = true; + this._handle.writer.releaseLock(); + this._handle.reader.releaseLock(); + + try { + const { host, port, addressType } = this._handle.options; + const tlsOpts = + this.servername != null + ? { expectedServerHostname: this.servername } + : undefined; + const socket = this._handle.socket.startTls(tlsOpts); + + this._handle = { + socket: socket, + writer: socket.writable.getWriter(), + reader: socketReader(socket.readable), + bytesRead: 0, + bytesWritten: 0, + reading: false, + options: this._handle.options, + }; + + // This is now an encrypted connection. + // There are cases where in node:net we have to distinguish between + // encrypted and unencrypted connections. + this.encrypted = true; + + this._handle.socket.opened.then( + onConnectionOpened.bind(this), + (err: unknown) => { + this.emit("connectionAttemptFailed", host, port, addressType, err); + this.destroy(err as Error); + }, + ); + + this._handle.socket.closed.then( + onConnectionClosed.bind(this), + (error: unknown): void => { + // Do not call this.destroy.bind(this) since user can override it. + this.destroy(error as Error); + }, + ); + } catch (error) { + this.destroy(error as Error); + } +}; + +TLSSocket.prototype.setServername = function setServername( + this: TLSSocket, + name: string, +): void { + validateString(name, "name"); + this.servername = name; +}; + +TLSSocket.prototype.setSession = function (_session: string | Buffer): void { + // Do nothing. We don't support setting session. +}; + +// @ts-expect-error TS2322 Inconsistencies between @types/node +TLSSocket.prototype.getPeerCertificate = function ( + _detailed?: boolean, +): ReturnType { + // Returns an object representing a peer certificate. + // This function is not supported. + throw new Error("getPeerCertificate is not implemented"); +}; + +TLSSocket.prototype.getCertificate = function ( + this: TLSSocket, +): ReturnType { + // Returns an object representing the local certificate. + // This function is not supported. + throw new Error("TLSSocket.getCertificate is not implemented"); +}; + +TLSSocket.prototype.getPeerX509Certificate = function ( + this: TLSSocket, + _detailed?: boolean, +): ReturnType { + // Returns the peer certificate as an X509 certificate. + // This function is not supported. + throw new Error("TLSSocket.getPeerX509Certificate is not implemented"); +}; + +TLSSocket.prototype.getX509Certificate = function ( + this: TLSSocket, +): ReturnType { + // Returns the local certificate as an X509 certificate. + // This function is not supported. + throw new Error("TLSSocket.getX509Certificate is not implemented"); +}; + +TLSSocket.prototype.setKeyCert = function ( + this: TLSSocket, + _context: unknown, +): void { + // Changing private key and certificate to be used with TCP connection + // is not supported due to the limitations of connect() api. + throw new Error("TLSSocket.setKeyCert is not implemented"); +}; + +// We have this syntax because of the original Node.js implementation. +// They are not supported by Cloudflare Workers but in Node.js +// they are all properties of this._handle[PROP_NAME] +[ + "getCipher", + "getSharedSigalgs", + "getEphemeralKeyInfo", + "getFinished", + "getPeerFinished", + "getProtocol", + "getSession", + "getTLSTicket", + "isSessionReused", + "enableTrace", +].forEach((method) => { + // @ts-expect-error TS7053 Omitting... + TLSSocket.prototype[method] = function (): null { + // None of these functions are supported by connect() api. + return null; + }; +}); + +type NormalizedConnectionOptions = ConnectionOptions & + SocketOptions & + TlsOptions & { + host?: string; + port: number; + }; +function normalizeConnectArgs( + listArgs: unknown[], +): [NormalizedConnectionOptions] | [NormalizedConnectionOptions, VoidFunction] { + const args = _normalizeArgs(listArgs); + const options = args[0] as NormalizedConnectionOptions; + const cb = args[1] as VoidFunction | undefined; + + // If args[0] was options, then normalize dealt with it. + // If args[0] is port, or args[0], args[1] is host, port, we need to + // find the options and merge them in, normalize's options has only + // the host/port/path args that it knows about, not the tls options. + // This means that options.host overrides a host arg. + if (listArgs[1] !== null && typeof listArgs[1] === "object") { + Object.assign(options, listArgs[1]); + } else if (listArgs[2] !== null && typeof listArgs[2] === "object") { + Object.assign(options, listArgs[2]); + } + + return cb ? [options, cb] : [options]; +} + +function onConnectSecure(this: TLSSocket): void { + this.authorized = true; + this.secureConnecting = false; + this.emit("secureConnect"); + + this[kIsVerified] = true; + const session = this[kPendingSession]; + this[kPendingSession] = null; + if (session) this.emit("session", session); + + this.removeListener("end", onConnectEnd); +} + +function onConnectEnd(this: TLSSocket): void { + // NOTE: This logic is shared with _http_client.js + if (!this._hadError) { + const options = this[kConnectOptions]; + this._hadError = true; + const error = new ConnResetException( + "Client network socket disconnected " + + "before secure TLS connection was " + + "established", + ); + error.path = options?.path; + error.host = options?.host; + error.port = options?.port; + // @ts-expect-error TS2339 Missing types + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + error.localAddress = options?.localAddress; + this.destroy(error); + } +} + +// Arguments: [port,] [host,] [options,] [cb] +export function connect(...args: unknown[]): TLSSocket { + args = normalizeConnectArgs(args); + const options = args[0] as NormalizedConnectionOptions; + const cb = args[1] as VoidFunction | undefined; + + if (options.minDHSize !== undefined) { + // We leave this validation for node.js compat. + validateNumber(options.minDHSize, "options.minDHSize", 1); + // Not supported. + throw new ERR_OPTION_NOT_IMPLEMENTED("options.minDHSize"); + } + + if (options.rejectUnauthorized === false) { + // TODO(soon): We don't support rejectUnauthorized=false + throw new ERR_OPTION_NOT_IMPLEMENTED("options.rejectUnauthorized"); + } + + if (options.pskCallback !== undefined) { + // Used for TLS-PSK negotiation. + // Does not make sense for Cloudflare Workers. + throw new ERR_OPTION_NOT_IMPLEMENTED("options.pskCallback"); + } + + if (options.checkServerIdentity !== undefined) { + validateFunction( + options.checkServerIdentity, + "options.checkServerIdentity", + ); + if (processImpl.shouldThrowOnNotImplementedTlsOption()) { + throw new ERR_OPTION_NOT_IMPLEMENTED("options.checkServerIdentity"); + } + } + + // @ts-expect-error TS2345 Type incompatibility between Node.js Duplex and internal Duplex + const tlssock = new TLSSocket(options.socket, { + allowHalfOpen: options.allowHalfOpen, + pipe: !!options.path, + ALPNProtocols: options.ALPNProtocols, + enableTrace: options.enableTrace, + highWaterMark: options.highWaterMark, + secureContext: options.secureContext, + onread: options.onread, + signal: options.signal, + lookup: options.lookup, + rejectUnauthorized: + options.rejectUnauthorized !== undefined + ? Boolean(options.rejectUnauthorized) // eslint-disable-line @typescript-eslint/no-unnecessary-type-conversion + : true, + }); + + tlssock[kConnectOptions] = options; + + // In Node.js, servername defaults to options.host when not explicitly set. + // Store it on the TLSSocket so _start() can pass it as expectedServerHostname + // to the native Socket.startTls() call for correct TLS certificate identity + // validation. + if (options.servername !== undefined) { + tlssock.servername = options.servername; + } + + if (cb) { + tlssock.once("secureConnect", cb); + } + + if (!options.socket) { + // If user provided the socket, it's their responsibility to manage its + // connectivity. If we created one internally, we connect it. + if (options.timeout) { + tlssock.setTimeout(options.timeout); + } + + tlssock.connect(options, tlssock._start.bind(tlssock)); + } + + tlssock._releaseControl(); + + if (options.session) { + tlssock.setSession(options.session); + } + + if (options.socket) { + tlssock._start(); + } + + // The 'secure' event is emitted by the SecurePair object once a secure connection has been established. + tlssock.on("secure", onConnectSecure); + tlssock.prependListener("end", onConnectEnd); + + return tlssock; +} diff --git a/src/compat/util/index.ts b/src/compat/util/index.ts index 8546f72b..fcbd2359 100644 --- a/src/compat/util/index.ts +++ b/src/compat/util/index.ts @@ -1,3 +1,4 @@ +import { isKeyObject } from "../crypto/key-state.js"; import { debuglog } from "./debuglog.js"; import { inspect, @@ -23,7 +24,49 @@ export * from "./core.js"; // @ts-expect-error the polyfill ships CJS without declarations import util from "util/util.js"; import { Buffer } from "node:buffer"; -export const types = util.types; +export const types = Object.assign(util.types, { + isKeyObject, + isDate(value: unknown) { + try { + Date.prototype.getTime.call(value); + return true; + } catch { + return false; + } + }, + isRegExp(value: unknown) { + try { + Object.getOwnPropertyDescriptor(RegExp.prototype, "source")!.get!.call( + value, + ); + return true; + } catch { + return false; + } + }, + isNativeError(value: unknown) { + return typeof (Error as any).isError === "function" + ? (Error as any).isError(value) + : value instanceof Error; + }, + isCryptoKey(value: unknown) { + try { + const ctor = globalThis.CryptoKey; + if (!ctor) return false; + Object.getOwnPropertyDescriptor(ctor.prototype, "type")!.get!.call(value); + return true; + } catch { + return false; + } + }, + isFloat16Array(value: unknown) { + const getTag = Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(Uint8Array.prototype), + Symbol.toStringTag, + )!.get!; + return getTag.call(value) === "Float16Array"; + }, +}); export const isArray = Array.isArray; export const isBoolean = (val: unknown): boolean => typeof val === "boolean"; export const isBuffer = (val: unknown): boolean => Buffer.isBuffer(val); diff --git a/src/compat/util/types/index.ts b/src/compat/util/types/index.ts new file mode 100644 index 00000000..f242f0ea --- /dev/null +++ b/src/compat/util/types/index.ts @@ -0,0 +1,49 @@ +// The same types object is shared by util and util/types. Engine-only +// predicates retain the explicit limitations of the dependency adapter. +import { types } from "../index.js"; +export default types; +export const { + isDate, + isKeyObject, + isRegExp, + isNativeError, + isCryptoKey, + isFloat16Array, + isArgumentsObject, + isGeneratorFunction, + isTypedArray, + isPromise, + isArrayBufferView, + isUint8Array, + isUint8ClampedArray, + isUint16Array, + isUint32Array, + isInt8Array, + isInt16Array, + isInt32Array, + isFloat32Array, + isFloat64Array, + isBigInt64Array, + isBigUint64Array, + isMap, + isSet, + isWeakMap, + isWeakSet, + isArrayBuffer, + isDataView, + isSharedArrayBuffer, + isAsyncFunction, + isMapIterator, + isSetIterator, + isGeneratorObject, + isNumberObject, + isStringObject, + isBooleanObject, + isBigIntObject, + isSymbolObject, + isBoxedPrimitive, + isAnyArrayBuffer, + isProxy, + isExternal, + isModuleNamespaceObject, +} = types; diff --git a/src/compat/zlib/LICENSE.brotli b/src/compat/zlib/LICENSE.brotli new file mode 100644 index 00000000..33b7cdd2 --- /dev/null +++ b/src/compat/zlib/LICENSE.brotli @@ -0,0 +1,19 @@ +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. + +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. diff --git a/src/compat/zlib/LICENSE.emscripten b/src/compat/zlib/LICENSE.emscripten new file mode 100644 index 00000000..70cabe38 --- /dev/null +++ b/src/compat/zlib/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/zlib/LICENSE.musl b/src/compat/zlib/LICENSE.musl new file mode 100644 index 00000000..c1628e9a --- /dev/null +++ b/src/compat/zlib/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/zlib/LICENSE.zstd b/src/compat/zlib/LICENSE.zstd new file mode 100644 index 00000000..75800288 --- /dev/null +++ b/src/compat/zlib/LICENSE.zstd @@ -0,0 +1,30 @@ +BSD License + +For Zstandard software + +Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook, nor Meta, nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/compat/zlib/compression.ts b/src/compat/zlib/compression.ts new file mode 100644 index 00000000..ae37e328 --- /dev/null +++ b/src/compat/zlib/compression.ts @@ -0,0 +1,334 @@ +import { Buffer } from "node:buffer"; +import { Transform } from "node:stream"; +import { NativeCodec, type Kind } from "./native-codecs.js"; +import { validateFunction, validateObject } from "../net/validators.js"; +import { validateInteger } from "../crypto/validation.js"; +import { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE } from "../net/errors.js"; +const flushMarker = Symbol("codec flush"); +function bytes(value: unknown): Uint8Array { + if (typeof value === "string") return Buffer.from(value); + if (ArrayBuffer.isView(value)) + return new Uint8Array(value.buffer, value.byteOffset, value.byteLength); + if (value instanceof ArrayBuffer) return new Uint8Array(value); + throw new ERR_INVALID_ARG_TYPE( + "buffer", + ["string", "Buffer", "TypedArray", "DataView", "ArrayBuffer"], + value, + ); +} +// Pinned workerd validators: NaN selects defaults; finite fractional values +// are accepted here and converted by the underlying buffer/codec operation. +function finite(value: unknown, name: string): value is number { + if (value === undefined || Number.isNaN(value)) return false; + if (typeof value !== "number") + throw new ERR_INVALID_ARG_TYPE(name, "number", value); + if (!Number.isFinite(value)) + throw new ERR_OUT_OF_RANGE(name, "a finite number", value); + return true; +} +function range( + value: unknown, + name: string, + low: number, + high: number, + fallback: number, +): number { + if (!finite(value, name)) return fallback; + if (value < low || value > high) + throw new ERR_OUT_OF_RANGE(name, `>= ${low} and <= ${high}`, value); + return value; +} +function settings(kind: Kind, encode: boolean, value: any = {}) { + if (value == null) value = {}; + validateObject(value, "options"); + const chunkSize = finite(value.chunkSize, "options.chunkSize") + ? value.chunkSize + : 16384; + if (chunkSize < 64) + throw new ERR_OUT_OF_RANGE("options.chunkSize", ">= 64", chunkSize); + const maxOutputLength = range( + value.maxOutputLength, + "options.maxOutputLength", + 1, + 2147483647, + 2147483647, + ); + const params = value.params ?? {}; + validateObject(params, "options.params"); + const maximum = kind === "brotli" ? 8 : encode ? 402 : 100; + const seen = new Set(); + for (const [key, val] of Object.entries(params)) { + const number = Number(key); + if ( + !Number.isInteger(number) || + number < 0 || + number > maximum || + seen.has(number) + ) + throw Object.assign( + new RangeError( + `${key} is not a valid ${kind === "brotli" ? "Brotli" : "Zstd"} parameter`, + ), + { + code: + kind === "brotli" + ? "ERR_BROTLI_INVALID_PARAM" + : "ERR_ZSTD_INVALID_PARAM", + }, + ); + seen.add(number); + if (typeof val !== "number" && typeof val !== "boolean") + throw new ERR_INVALID_ARG_TYPE("options.params[key]", "number", val); + } + const flush = range( + value.flush, + "options.flush", + 0, + kind === "brotli" ? 3 : 2, + 0, + ); + const finishFlush = range( + value.finishFlush, + "options.finishFlush", + 0, + kind === "brotli" ? 3 : 2, + 2, + ); + if (value.pledgedSrcSize !== undefined) + validateInteger( + value.pledgedSrcSize, + "options.pledgedSrcSize", + 0, + 0xffffffff, + ); + return { + chunkSize, + maxOutputLength, + params, + flush, + finishFlush, + pledged: value.pledgedSrcSize, + info: !!value.info, + }; +} +class CodecTransform extends Transform { + private codec: NativeCodec; + private readonly config: ReturnType; + bytesWritten = 0; + private readonly kind: Kind; + private readonly encode: boolean; + constructor(kind: Kind, encode: boolean, options?: any) { + const config = settings(kind, encode, options); + super(options ?? {}); + this.kind = kind; + this.encode = encode; + this.config = config; + this.codec = new NativeCodec( + kind, + encode, + config.chunkSize, + config.params, + config.pledged, + ); + } + _transform(input: any, _encoding: any, callback: any) { + try { + const operation = input[flushMarker] ?? this.config.flush; + this.codec.process(bytes(input), operation, (output) => + this.push(Buffer.from(output)), + ); + this.bytesWritten = this.codec.consumed; + callback(); + } catch (error) { + callback(error); + } + } + _flush(callback: any) { + try { + this.codec.process(new Uint8Array(), this.config.finishFlush, (output) => + this.push(Buffer.from(output)), + ); + this.bytesWritten = this.codec.consumed; + callback(); + } catch (error) { + callback(error); + } + } + _destroy(error: any, callback: any) { + this.codec.close(); + callback(error); + } + flush(kind?: number | (() => void), callback?: () => void) { + if (typeof kind === "function") { + callback = kind; + kind = 1; + } + kind ??= 1; + validateInteger(kind, "kind", 0, this.kind === "brotli" ? 3 : 2); + if (callback !== undefined) validateFunction(callback, "callback"); + if (this.writableFinished) { + if (callback) queueMicrotask(callback); + return; + } + const marker: any = Buffer.alloc(0); + marker[flushMarker] = kind; + this.write(marker, callback); + } + reset() { + if (this.destroyed) throw new Error("Codec is closed"); + this.codec.close(); + this.codec = new NativeCodec( + this.kind, + this.encode, + this.config.chunkSize, + this.config.params, + this.config.pledged, + ); + this.bytesWritten = 0; + } + close(callback?: () => void) { + if (callback) { + validateFunction(callback, "callback"); + this.once("close", callback); + } + this.destroy(); + } + _processChunk(input: unknown, operation = 2) { + const chunks: Uint8Array[] = []; + let length = 0; + try { + this.codec.process(bytes(input), operation, (output) => { + length += output.length; + if (length > this.config.maxOutputLength) + throw Object.assign( + new RangeError( + "Cannot create a Buffer larger than maxOutputLength", + ), + { code: "ERR_BUFFER_TOO_LARGE" }, + ); + chunks.push(output); + }); + this.bytesWritten = this.codec.consumed; + return Buffer.concat(chunks, length); + } finally { + this.codec.close(); + } + } +} +export class BrotliCompress extends CodecTransform { + constructor(options?: any) { + super("brotli", true, options); + } +} +export class BrotliDecompress extends CodecTransform { + constructor(options?: any) { + super("brotli", false, options); + } +} +export class ZstdCompress extends CodecTransform { + constructor(options?: any) { + super("zstd", true, options); + } +} +export class ZstdDecompress extends CodecTransform { + constructor(options?: any) { + super("zstd", false, options); + } +} +export const createBrotliCompress = (options?: any) => + new BrotliCompress(options); +export const createBrotliDecompress = (options?: any) => + new BrotliDecompress(options); +export const createZstdCompress = (options?: any) => new ZstdCompress(options); +export const createZstdDecompress = (options?: any) => + new ZstdDecompress(options); +function sync(kind: Kind, encode: boolean, input: unknown, options?: any) { + const data = bytes(input), + config = settings(kind, encode, options); + if (config.info) { + const Constructor = + kind === "brotli" + ? encode + ? BrotliCompress + : BrotliDecompress + : encode + ? ZstdCompress + : ZstdDecompress; + const engine = new Constructor(options); + return { buffer: engine._processChunk(data, config.finishFlush), engine }; + } + const codec = new NativeCodec( + kind, + encode, + config.chunkSize, + config.params, + config.pledged ?? (encode && kind === "zstd" ? data.length : undefined), + ); + const chunks: Uint8Array[] = []; + let length = 0; + try { + codec.process(data, config.finishFlush, (output) => { + length += output.length; + if (length > config.maxOutputLength) + throw new RangeError("Memory limit exceeded"); + chunks.push(output); + }); + return Buffer.concat(chunks, length); + } catch (error) { + if ( + error instanceof RangeError || + (error instanceof Error && error.message === "Unexpected end of file") + ) + throw error; + throw new Error("Compression failed"); + } finally { + codec.close(); + } +} +function asyncOperation( + kind: Kind, + encode: boolean, + input: unknown, + options: any, + callback?: any, +) { + if (typeof options === "function") { + callback = options; + options = undefined; + } + validateFunction(callback, "callback"); + const data = bytes(input); + settings(kind, encode, options); + new Promise((resolve) => resolve(sync(kind, encode, data, options))).then( + (result) => callback(null, result), + (error) => callback(error), + ); +} +export function brotliCompressSync(input: unknown, options?: any) { + return sync("brotli", true, input, options); +} +export function brotliDecompressSync(input: unknown, options?: any) { + return sync("brotli", false, input, options); +} +export function zstdCompressSync(input: unknown, options?: any) { + return sync("zstd", true, input, options); +} +export function zstdDecompressSync(input: unknown, options?: any) { + return sync("zstd", false, input, options); +} +export function brotliCompress(input: unknown, options?: any, callback?: any) { + asyncOperation("brotli", true, input, options, callback); +} +export function brotliDecompress( + input: unknown, + options?: any, + callback?: any, +) { + asyncOperation("brotli", false, input, options, callback); +} +export function zstdCompress(input: unknown, options?: any, callback?: any) { + asyncOperation("zstd", true, input, options, callback); +} +export function zstdDecompress(input: unknown, options?: any, callback?: any) { + asyncOperation("zstd", false, input, options, callback); +} diff --git a/src/compat/zlib/constants.ts b/src/compat/zlib/constants.ts new file mode 100644 index 00000000..e7e21fe4 --- /dev/null +++ b/src/compat/zlib/constants.ts @@ -0,0 +1,173 @@ +// Constants observed in workerd 1.20261012.1 (2026-10-17 profile). +export const Z_NO_FLUSH = 0; +export const Z_PARTIAL_FLUSH = 1; +export const Z_SYNC_FLUSH = 2; +export const Z_FULL_FLUSH = 3; +export const Z_FINISH = 4; +export const Z_BLOCK = 5; +export const Z_OK = 0; +export const Z_STREAM_END = 1; +export const Z_NEED_DICT = 2; +export const Z_ERRNO = -1; +export const Z_STREAM_ERROR = -2; +export const Z_DATA_ERROR = -3; +export const Z_MEM_ERROR = -4; +export const Z_BUF_ERROR = -5; +export const Z_VERSION_ERROR = -6; +export const Z_NO_COMPRESSION = 0; +export const Z_BEST_SPEED = 1; +export const Z_BEST_COMPRESSION = 9; +export const Z_DEFAULT_COMPRESSION = -1; +export const Z_FILTERED = 1; +export const Z_HUFFMAN_ONLY = 2; +export const Z_RLE = 3; +export const Z_FIXED = 4; +export const Z_DEFAULT_STRATEGY = 0; +export const ZLIB_VERNUM = 4897; +export const DEFLATE = 1; +export const INFLATE = 2; +export const GZIP = 3; +export const GUNZIP = 4; +export const DEFLATERAW = 5; +export const INFLATERAW = 6; +export const UNZIP = 7; +export const BROTLI_DECODE = 8; +export const BROTLI_ENCODE = 9; +export const Z_MIN_WINDOWBITS = 8; +export const Z_MAX_WINDOWBITS = 15; +export const Z_DEFAULT_WINDOWBITS = 15; +export const Z_MIN_CHUNK = 64; +export const Z_MAX_CHUNK = 134217728; +export const Z_DEFAULT_CHUNK = 16384; +export const Z_MIN_MEMLEVEL = 1; +export const Z_MAX_MEMLEVEL = 9; +export const Z_DEFAULT_MEMLEVEL = 8; +export const Z_MIN_LEVEL = -1; +export const Z_MAX_LEVEL = 9; +export const Z_DEFAULT_LEVEL = -1; +export const BROTLI_OPERATION_PROCESS = 0; +export const BROTLI_OPERATION_FLUSH = 1; +export const BROTLI_OPERATION_FINISH = 2; +export const BROTLI_OPERATION_EMIT_METADATA = 3; +export const BROTLI_PARAM_MODE = 0; +export const BROTLI_MODE_GENERIC = 0; +export const BROTLI_MODE_TEXT = 1; +export const BROTLI_MODE_FONT = 2; +export const BROTLI_DEFAULT_MODE = 0; +export const BROTLI_PARAM_QUALITY = 1; +export const BROTLI_MIN_QUALITY = 0; +export const BROTLI_MAX_QUALITY = 11; +export const BROTLI_DEFAULT_QUALITY = 11; +export const BROTLI_PARAM_LGWIN = 2; +export const BROTLI_MIN_WINDOW_BITS = 10; +export const BROTLI_MAX_WINDOW_BITS = 24; +export const BROTLI_LARGE_MAX_WINDOW_BITS = 30; +export const BROTLI_DEFAULT_WINDOW = 22; +export const BROTLI_PARAM_LGBLOCK = 3; +export const BROTLI_MIN_INPUT_BLOCK_BITS = 16; +export const BROTLI_MAX_INPUT_BLOCK_BITS = 24; +export const BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING = 4; +export const BROTLI_PARAM_SIZE_HINT = 5; +export const BROTLI_PARAM_LARGE_WINDOW = 6; +export const BROTLI_PARAM_NPOSTFIX = 7; +export const BROTLI_PARAM_NDIRECT = 8; +export const BROTLI_DECODER_RESULT_ERROR = 0; +export const BROTLI_DECODER_RESULT_SUCCESS = 1; +export const BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT = 2; +export const BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT = 3; +export const BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION = 0; +export const BROTLI_DECODER_PARAM_LARGE_WINDOW = 1; +export const BROTLI_DECODER_NO_ERROR = 0; +export const BROTLI_DECODER_SUCCESS = 1; +export const BROTLI_DECODER_NEEDS_MORE_INPUT = 2; +export const BROTLI_DECODER_NEEDS_MORE_OUTPUT = 3; +export const BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE = -1; +export const BROTLI_DECODER_ERROR_FORMAT_RESERVED = -2; +export const BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE = -3; +export const BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET = -4; +export const BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME = -5; +export const BROTLI_DECODER_ERROR_FORMAT_CL_SPACE = -6; +export const BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE = -7; +export const BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT = -8; +export const BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1 = -9; +export const BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2 = -10; +export const BROTLI_DECODER_ERROR_FORMAT_TRANSFORM = -11; +export const BROTLI_DECODER_ERROR_FORMAT_DICTIONARY = -12; +export const BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS = -13; +export const BROTLI_DECODER_ERROR_FORMAT_PADDING_1 = -14; +export const BROTLI_DECODER_ERROR_FORMAT_PADDING_2 = -15; +export const BROTLI_DECODER_ERROR_FORMAT_DISTANCE = -16; +export const BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET = -19; +export const BROTLI_DECODER_ERROR_INVALID_ARGUMENTS = -20; +export const BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES = -21; +export const BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS = -22; +export const BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP = -25; +export const BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1 = -26; +export const BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2 = -27; +export const BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES = -30; +export const BROTLI_DECODER_ERROR_UNREACHABLE = -31; +export const ZSTD_ENCODE = 10; +export const ZSTD_DECODE = 11; +export const ZSTD_COMPRESS = 10; +export const ZSTD_DECOMPRESS = 11; +export const ZSTD_e_continue = 0; +export const ZSTD_e_flush = 1; +export const ZSTD_e_end = 2; +export const ZSTD_c_compressionLevel = 100; +export const ZSTD_c_windowLog = 101; +export const ZSTD_c_hashLog = 102; +export const ZSTD_c_chainLog = 103; +export const ZSTD_c_searchLog = 104; +export const ZSTD_c_minMatch = 105; +export const ZSTD_c_targetLength = 106; +export const ZSTD_c_strategy = 107; +export const ZSTD_c_enableLongDistanceMatching = 160; +export const ZSTD_c_ldmHashLog = 161; +export const ZSTD_c_ldmMinMatch = 162; +export const ZSTD_c_ldmBucketSizeLog = 163; +export const ZSTD_c_ldmHashRateLog = 164; +export const ZSTD_c_contentSizeFlag = 200; +export const ZSTD_c_checksumFlag = 201; +export const ZSTD_c_dictIDFlag = 202; +export const ZSTD_c_nbWorkers = 400; +export const ZSTD_c_jobSize = 401; +export const ZSTD_c_overlapLog = 402; +export const ZSTD_d_windowLogMax = 100; +export const ZSTD_fast = 1; +export const ZSTD_dfast = 2; +export const ZSTD_greedy = 3; +export const ZSTD_lazy = 4; +export const ZSTD_lazy2 = 5; +export const ZSTD_btlazy2 = 6; +export const ZSTD_btopt = 7; +export const ZSTD_btultra = 8; +export const ZSTD_btultra2 = 9; +export const ZSTD_CLEVEL_DEFAULT = 3; +export const ZSTD_error_no_error = 0; +export const ZSTD_error_GENERIC = 1; +export const ZSTD_error_prefix_unknown = 10; +export const ZSTD_error_version_unsupported = 12; +export const ZSTD_error_frameParameter_unsupported = 14; +export const ZSTD_error_frameParameter_windowTooLarge = 16; +export const ZSTD_error_corruption_detected = 20; +export const ZSTD_error_checksum_wrong = 22; +export const ZSTD_error_literals_headerWrong = 24; +export const ZSTD_error_dictionary_corrupted = 30; +export const ZSTD_error_dictionary_wrong = 32; +export const ZSTD_error_dictionaryCreation_failed = 34; +export const ZSTD_error_parameter_unsupported = 40; +export const ZSTD_error_parameter_combination_unsupported = 41; +export const ZSTD_error_parameter_outOfBound = 42; +export const ZSTD_error_tableLog_tooLarge = 44; +export const ZSTD_error_maxSymbolValue_tooLarge = 46; +export const ZSTD_error_maxSymbolValue_tooSmall = 48; +export const ZSTD_error_stabilityCondition_notRespected = 50; +export const ZSTD_error_stage_wrong = 60; +export const ZSTD_error_init_missing = 62; +export const ZSTD_error_memory_allocation = 64; +export const ZSTD_error_workSpace_tooSmall = 66; +export const ZSTD_error_dstSize_tooSmall = 70; +export const ZSTD_error_srcSize_wrong = 72; +export const ZSTD_error_dstBuffer_null = 74; +export const ZSTD_error_noForwardProgress_destFull = 80; +export const ZSTD_error_noForwardProgress_inputEmpty = 82; diff --git a/src/compat/zlib/index.ts b/src/compat/zlib/index.ts new file mode 100644 index 00000000..28e3c0ec --- /dev/null +++ b/src/compat/zlib/index.ts @@ -0,0 +1,398 @@ +import { Buffer } from "node:buffer"; +// @ts-expect-error browserify-zlib has no declarations +import original from "browserify-zlib/lib/index.js"; +import { validateInteger } from "../crypto/validation.js"; +import { ERR_INVALID_ARG_TYPE } from "../net/errors.js"; +import * as compression from "./compression.js"; +export * from "./compression.js"; +export const { + Zlib, + codes, + Deflate, + Inflate, + Gzip, + Gunzip, + DeflateRaw, + InflateRaw, + Unzip, + createDeflate, + createInflate, + createDeflateRaw, + createInflateRaw, + createGzip, + createGunzip, + createUnzip, + deflate, + deflateSync, + gzip, + gzipSync, + deflateRaw, + deflateRawSync, + unzip, + unzipSync, + inflate, + inflateSync, + gunzip, + gunzipSync, + inflateRaw, + inflateRawSync, +} = original; +import * as pinnedConstants from "./constants.js"; +const topLevelConstants = { + Z_NO_FLUSH: pinnedConstants.Z_NO_FLUSH, + Z_PARTIAL_FLUSH: pinnedConstants.Z_PARTIAL_FLUSH, + Z_SYNC_FLUSH: pinnedConstants.Z_SYNC_FLUSH, + Z_FULL_FLUSH: pinnedConstants.Z_FULL_FLUSH, + Z_FINISH: pinnedConstants.Z_FINISH, + Z_BLOCK: pinnedConstants.Z_BLOCK, + Z_OK: pinnedConstants.Z_OK, + Z_STREAM_END: pinnedConstants.Z_STREAM_END, + Z_NEED_DICT: pinnedConstants.Z_NEED_DICT, + Z_ERRNO: pinnedConstants.Z_ERRNO, + Z_STREAM_ERROR: pinnedConstants.Z_STREAM_ERROR, + Z_DATA_ERROR: pinnedConstants.Z_DATA_ERROR, + Z_MEM_ERROR: pinnedConstants.Z_MEM_ERROR, + Z_BUF_ERROR: pinnedConstants.Z_BUF_ERROR, + Z_VERSION_ERROR: pinnedConstants.Z_VERSION_ERROR, + Z_NO_COMPRESSION: pinnedConstants.Z_NO_COMPRESSION, + Z_BEST_SPEED: pinnedConstants.Z_BEST_SPEED, + Z_BEST_COMPRESSION: pinnedConstants.Z_BEST_COMPRESSION, + Z_DEFAULT_COMPRESSION: pinnedConstants.Z_DEFAULT_COMPRESSION, + Z_FILTERED: pinnedConstants.Z_FILTERED, + Z_HUFFMAN_ONLY: pinnedConstants.Z_HUFFMAN_ONLY, + Z_RLE: pinnedConstants.Z_RLE, + Z_FIXED: pinnedConstants.Z_FIXED, + Z_DEFAULT_STRATEGY: pinnedConstants.Z_DEFAULT_STRATEGY, + ZLIB_VERNUM: pinnedConstants.ZLIB_VERNUM, + DEFLATE: pinnedConstants.DEFLATE, + INFLATE: pinnedConstants.INFLATE, + GZIP: pinnedConstants.GZIP, + GUNZIP: pinnedConstants.GUNZIP, + DEFLATERAW: pinnedConstants.DEFLATERAW, + INFLATERAW: pinnedConstants.INFLATERAW, + UNZIP: pinnedConstants.UNZIP, + BROTLI_DECODE: pinnedConstants.BROTLI_DECODE, + BROTLI_ENCODE: pinnedConstants.BROTLI_ENCODE, + Z_MIN_WINDOWBITS: pinnedConstants.Z_MIN_WINDOWBITS, + Z_MAX_WINDOWBITS: pinnedConstants.Z_MAX_WINDOWBITS, + Z_DEFAULT_WINDOWBITS: pinnedConstants.Z_DEFAULT_WINDOWBITS, + Z_MIN_CHUNK: pinnedConstants.Z_MIN_CHUNK, + Z_MAX_CHUNK: pinnedConstants.Z_MAX_CHUNK, + Z_DEFAULT_CHUNK: pinnedConstants.Z_DEFAULT_CHUNK, + Z_MIN_MEMLEVEL: pinnedConstants.Z_MIN_MEMLEVEL, + Z_MAX_MEMLEVEL: pinnedConstants.Z_MAX_MEMLEVEL, + Z_DEFAULT_MEMLEVEL: pinnedConstants.Z_DEFAULT_MEMLEVEL, + Z_MIN_LEVEL: pinnedConstants.Z_MIN_LEVEL, + Z_MAX_LEVEL: pinnedConstants.Z_MAX_LEVEL, + Z_DEFAULT_LEVEL: pinnedConstants.Z_DEFAULT_LEVEL, + BROTLI_OPERATION_PROCESS: pinnedConstants.BROTLI_OPERATION_PROCESS, + BROTLI_OPERATION_FLUSH: pinnedConstants.BROTLI_OPERATION_FLUSH, + BROTLI_OPERATION_FINISH: pinnedConstants.BROTLI_OPERATION_FINISH, + BROTLI_OPERATION_EMIT_METADATA: + pinnedConstants.BROTLI_OPERATION_EMIT_METADATA, + BROTLI_PARAM_MODE: pinnedConstants.BROTLI_PARAM_MODE, + BROTLI_MODE_GENERIC: pinnedConstants.BROTLI_MODE_GENERIC, + BROTLI_MODE_TEXT: pinnedConstants.BROTLI_MODE_TEXT, + BROTLI_MODE_FONT: pinnedConstants.BROTLI_MODE_FONT, + BROTLI_DEFAULT_MODE: pinnedConstants.BROTLI_DEFAULT_MODE, + BROTLI_PARAM_QUALITY: pinnedConstants.BROTLI_PARAM_QUALITY, + BROTLI_MIN_QUALITY: pinnedConstants.BROTLI_MIN_QUALITY, + BROTLI_MAX_QUALITY: pinnedConstants.BROTLI_MAX_QUALITY, + BROTLI_DEFAULT_QUALITY: pinnedConstants.BROTLI_DEFAULT_QUALITY, + BROTLI_PARAM_LGWIN: pinnedConstants.BROTLI_PARAM_LGWIN, + BROTLI_MIN_WINDOW_BITS: pinnedConstants.BROTLI_MIN_WINDOW_BITS, + BROTLI_MAX_WINDOW_BITS: pinnedConstants.BROTLI_MAX_WINDOW_BITS, + BROTLI_LARGE_MAX_WINDOW_BITS: pinnedConstants.BROTLI_LARGE_MAX_WINDOW_BITS, + BROTLI_DEFAULT_WINDOW: pinnedConstants.BROTLI_DEFAULT_WINDOW, + BROTLI_PARAM_LGBLOCK: pinnedConstants.BROTLI_PARAM_LGBLOCK, + BROTLI_MIN_INPUT_BLOCK_BITS: pinnedConstants.BROTLI_MIN_INPUT_BLOCK_BITS, + BROTLI_MAX_INPUT_BLOCK_BITS: pinnedConstants.BROTLI_MAX_INPUT_BLOCK_BITS, + BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING: + pinnedConstants.BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING, + BROTLI_PARAM_LARGE_WINDOW: pinnedConstants.BROTLI_PARAM_LARGE_WINDOW, + BROTLI_PARAM_NPOSTFIX: pinnedConstants.BROTLI_PARAM_NPOSTFIX, + BROTLI_PARAM_NDIRECT: pinnedConstants.BROTLI_PARAM_NDIRECT, + BROTLI_DECODER_RESULT_ERROR: pinnedConstants.BROTLI_DECODER_RESULT_ERROR, + BROTLI_DECODER_RESULT_SUCCESS: pinnedConstants.BROTLI_DECODER_RESULT_SUCCESS, + BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT: + pinnedConstants.BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT, + BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT: + pinnedConstants.BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT, + BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION: + pinnedConstants.BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION, + BROTLI_DECODER_PARAM_LARGE_WINDOW: + pinnedConstants.BROTLI_DECODER_PARAM_LARGE_WINDOW, + BROTLI_DECODER_NO_ERROR: pinnedConstants.BROTLI_DECODER_NO_ERROR, + BROTLI_DECODER_SUCCESS: pinnedConstants.BROTLI_DECODER_SUCCESS, + BROTLI_DECODER_NEEDS_MORE_INPUT: + pinnedConstants.BROTLI_DECODER_NEEDS_MORE_INPUT, + BROTLI_DECODER_NEEDS_MORE_OUTPUT: + pinnedConstants.BROTLI_DECODER_NEEDS_MORE_OUTPUT, + BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE, + BROTLI_DECODER_ERROR_FORMAT_RESERVED: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_RESERVED, + BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE, + BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET, + BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME, + BROTLI_DECODER_ERROR_FORMAT_CL_SPACE: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_CL_SPACE, + BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE, + BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT, + BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1, + BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2, + BROTLI_DECODER_ERROR_FORMAT_TRANSFORM: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_TRANSFORM, + BROTLI_DECODER_ERROR_FORMAT_DICTIONARY: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_DICTIONARY, + BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS, + BROTLI_DECODER_ERROR_FORMAT_PADDING_1: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_PADDING_1, + BROTLI_DECODER_ERROR_FORMAT_PADDING_2: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_PADDING_2, + BROTLI_DECODER_ERROR_FORMAT_DISTANCE: + pinnedConstants.BROTLI_DECODER_ERROR_FORMAT_DISTANCE, + BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET: + pinnedConstants.BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET, + BROTLI_DECODER_ERROR_INVALID_ARGUMENTS: + pinnedConstants.BROTLI_DECODER_ERROR_INVALID_ARGUMENTS, + BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES: + pinnedConstants.BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES, + BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS: + pinnedConstants.BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS, + BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP: + pinnedConstants.BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP, + BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1: + pinnedConstants.BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1, + BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2: + pinnedConstants.BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2, + BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES: + pinnedConstants.BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES, + BROTLI_DECODER_ERROR_UNREACHABLE: + pinnedConstants.BROTLI_DECODER_ERROR_UNREACHABLE, + ZSTD_ENCODE: pinnedConstants.ZSTD_ENCODE, + ZSTD_DECODE: pinnedConstants.ZSTD_DECODE, + ZSTD_COMPRESS: pinnedConstants.ZSTD_COMPRESS, + ZSTD_DECOMPRESS: pinnedConstants.ZSTD_DECOMPRESS, + ZSTD_error_no_error: pinnedConstants.ZSTD_error_no_error, + ZSTD_error_GENERIC: pinnedConstants.ZSTD_error_GENERIC, + ZSTD_error_prefix_unknown: pinnedConstants.ZSTD_error_prefix_unknown, + ZSTD_error_version_unsupported: + pinnedConstants.ZSTD_error_version_unsupported, + ZSTD_error_frameParameter_unsupported: + pinnedConstants.ZSTD_error_frameParameter_unsupported, + ZSTD_error_frameParameter_windowTooLarge: + pinnedConstants.ZSTD_error_frameParameter_windowTooLarge, + ZSTD_error_corruption_detected: + pinnedConstants.ZSTD_error_corruption_detected, + ZSTD_error_checksum_wrong: pinnedConstants.ZSTD_error_checksum_wrong, + ZSTD_error_literals_headerWrong: + pinnedConstants.ZSTD_error_literals_headerWrong, + ZSTD_error_dictionary_corrupted: + pinnedConstants.ZSTD_error_dictionary_corrupted, + ZSTD_error_dictionary_wrong: pinnedConstants.ZSTD_error_dictionary_wrong, + ZSTD_error_dictionaryCreation_failed: + pinnedConstants.ZSTD_error_dictionaryCreation_failed, + ZSTD_error_parameter_unsupported: + pinnedConstants.ZSTD_error_parameter_unsupported, + ZSTD_error_parameter_combination_unsupported: + pinnedConstants.ZSTD_error_parameter_combination_unsupported, + ZSTD_error_parameter_outOfBound: + pinnedConstants.ZSTD_error_parameter_outOfBound, + ZSTD_error_tableLog_tooLarge: pinnedConstants.ZSTD_error_tableLog_tooLarge, + ZSTD_error_maxSymbolValue_tooLarge: + pinnedConstants.ZSTD_error_maxSymbolValue_tooLarge, + ZSTD_error_maxSymbolValue_tooSmall: + pinnedConstants.ZSTD_error_maxSymbolValue_tooSmall, + ZSTD_error_stabilityCondition_notRespected: + pinnedConstants.ZSTD_error_stabilityCondition_notRespected, + ZSTD_error_stage_wrong: pinnedConstants.ZSTD_error_stage_wrong, + ZSTD_error_init_missing: pinnedConstants.ZSTD_error_init_missing, + ZSTD_error_memory_allocation: pinnedConstants.ZSTD_error_memory_allocation, + ZSTD_error_workSpace_tooSmall: pinnedConstants.ZSTD_error_workSpace_tooSmall, + ZSTD_error_dstSize_tooSmall: pinnedConstants.ZSTD_error_dstSize_tooSmall, + ZSTD_error_srcSize_wrong: pinnedConstants.ZSTD_error_srcSize_wrong, + ZSTD_error_dstBuffer_null: pinnedConstants.ZSTD_error_dstBuffer_null, + ZSTD_error_noForwardProgress_destFull: + pinnedConstants.ZSTD_error_noForwardProgress_destFull, + ZSTD_error_noForwardProgress_inputEmpty: + pinnedConstants.ZSTD_error_noForwardProgress_inputEmpty, +}; +export const { + Z_NO_FLUSH, + Z_PARTIAL_FLUSH, + Z_SYNC_FLUSH, + Z_FULL_FLUSH, + Z_FINISH, + Z_BLOCK, + Z_OK, + Z_STREAM_END, + Z_NEED_DICT, + Z_ERRNO, + Z_STREAM_ERROR, + Z_DATA_ERROR, + Z_MEM_ERROR, + Z_BUF_ERROR, + Z_VERSION_ERROR, + Z_NO_COMPRESSION, + Z_BEST_SPEED, + Z_BEST_COMPRESSION, + Z_DEFAULT_COMPRESSION, + Z_FILTERED, + Z_HUFFMAN_ONLY, + Z_RLE, + Z_FIXED, + Z_DEFAULT_STRATEGY, + ZLIB_VERNUM, + DEFLATE, + INFLATE, + GZIP, + GUNZIP, + DEFLATERAW, + INFLATERAW, + UNZIP, + BROTLI_DECODE, + BROTLI_ENCODE, + Z_MIN_WINDOWBITS, + Z_MAX_WINDOWBITS, + Z_DEFAULT_WINDOWBITS, + Z_MIN_CHUNK, + Z_MAX_CHUNK, + Z_DEFAULT_CHUNK, + Z_MIN_MEMLEVEL, + Z_MAX_MEMLEVEL, + Z_DEFAULT_MEMLEVEL, + Z_MIN_LEVEL, + Z_MAX_LEVEL, + Z_DEFAULT_LEVEL, + BROTLI_OPERATION_PROCESS, + BROTLI_OPERATION_FLUSH, + BROTLI_OPERATION_FINISH, + BROTLI_OPERATION_EMIT_METADATA, + BROTLI_PARAM_MODE, + BROTLI_MODE_GENERIC, + BROTLI_MODE_TEXT, + BROTLI_MODE_FONT, + BROTLI_DEFAULT_MODE, + BROTLI_PARAM_QUALITY, + BROTLI_MIN_QUALITY, + BROTLI_MAX_QUALITY, + BROTLI_DEFAULT_QUALITY, + BROTLI_PARAM_LGWIN, + BROTLI_MIN_WINDOW_BITS, + BROTLI_MAX_WINDOW_BITS, + BROTLI_LARGE_MAX_WINDOW_BITS, + BROTLI_DEFAULT_WINDOW, + BROTLI_PARAM_LGBLOCK, + BROTLI_MIN_INPUT_BLOCK_BITS, + BROTLI_MAX_INPUT_BLOCK_BITS, + BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING, + BROTLI_PARAM_LARGE_WINDOW, + BROTLI_PARAM_NPOSTFIX, + BROTLI_PARAM_NDIRECT, + BROTLI_DECODER_RESULT_ERROR, + BROTLI_DECODER_RESULT_SUCCESS, + BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT, + BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT, + BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION, + BROTLI_DECODER_PARAM_LARGE_WINDOW, + BROTLI_DECODER_NO_ERROR, + BROTLI_DECODER_SUCCESS, + BROTLI_DECODER_NEEDS_MORE_INPUT, + BROTLI_DECODER_NEEDS_MORE_OUTPUT, + BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE, + BROTLI_DECODER_ERROR_FORMAT_RESERVED, + BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE, + BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET, + BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME, + BROTLI_DECODER_ERROR_FORMAT_CL_SPACE, + BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE, + BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT, + BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1, + BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2, + BROTLI_DECODER_ERROR_FORMAT_TRANSFORM, + BROTLI_DECODER_ERROR_FORMAT_DICTIONARY, + BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS, + BROTLI_DECODER_ERROR_FORMAT_PADDING_1, + BROTLI_DECODER_ERROR_FORMAT_PADDING_2, + BROTLI_DECODER_ERROR_FORMAT_DISTANCE, + BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET, + BROTLI_DECODER_ERROR_INVALID_ARGUMENTS, + BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES, + BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS, + BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP, + BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1, + BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2, + BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES, + BROTLI_DECODER_ERROR_UNREACHABLE, + ZSTD_ENCODE, + ZSTD_DECODE, + ZSTD_COMPRESS, + ZSTD_DECOMPRESS, + ZSTD_error_no_error, + ZSTD_error_GENERIC, + ZSTD_error_prefix_unknown, + ZSTD_error_version_unsupported, + ZSTD_error_frameParameter_unsupported, + ZSTD_error_frameParameter_windowTooLarge, + ZSTD_error_corruption_detected, + ZSTD_error_checksum_wrong, + ZSTD_error_literals_headerWrong, + ZSTD_error_dictionary_corrupted, + ZSTD_error_dictionary_wrong, + ZSTD_error_dictionaryCreation_failed, + ZSTD_error_parameter_unsupported, + ZSTD_error_parameter_combination_unsupported, + ZSTD_error_parameter_outOfBound, + ZSTD_error_tableLog_tooLarge, + ZSTD_error_maxSymbolValue_tooLarge, + ZSTD_error_maxSymbolValue_tooSmall, + ZSTD_error_stabilityCondition_notRespected, + ZSTD_error_stage_wrong, + ZSTD_error_init_missing, + ZSTD_error_memory_allocation, + ZSTD_error_workSpace_tooSmall, + ZSTD_error_dstSize_tooSmall, + ZSTD_error_srcSize_wrong, + ZSTD_error_dstBuffer_null, + ZSTD_error_noForwardProgress_destFull, + ZSTD_error_noForwardProgress_inputEmpty, +} = topLevelConstants; +export const constants = Object.freeze({ ...pinnedConstants }); + +function data(input: unknown): Uint8Array { + if (typeof input === "string") return Buffer.from(input); + if (ArrayBuffer.isView(input)) + return new Uint8Array(input.buffer, input.byteOffset, input.byteLength); + if (input instanceof ArrayBuffer) return new Uint8Array(input); + throw new ERR_INVALID_ARG_TYPE( + "buffer", + ["string", "Buffer", "TypedArray", "DataView", "ArrayBuffer"], + input, + ); +} +export function crc32(input: unknown, value = 0): number { + validateInteger(value, "value", 0, 0xffffffff); + let crc = value ^ -1; + for (const byte of data(input)) { + crc ^= byte; + for (let bit = 0; bit < 8; bit++) + crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0); + } + return (crc ^ -1) >>> 0; +} +export default { + ...original, + ...topLevelConstants, + constants, + ...compression, + crc32, +}; diff --git a/src/compat/zlib/native-codecs.build.json b/src/compat/zlib/native-codecs.build.json new file mode 100644 index 00000000..2d4d5d2d --- /dev/null +++ b/src/compat/zlib/native-codecs.build.json @@ -0,0 +1,8 @@ +{ + "revisions": { + "emsdk": "e4fe26ef59168ff44f4c23c466e497bf60b3411e", + "zstd": "f8745da6ff1ad1e7bab384bd1f9d742439278e99", + "brotli": "ed738e842d2fbdf2d6459e39267a633c4a9b2f5d" + }, + "sha256": "52f2d0edcc0193e5289a0e9b945e2b1a2de8b6ea3efadf11ebd60b88c53d11a4" +} diff --git a/src/compat/zlib/native-codecs.c b/src/compat/zlib/native-codecs.c new file mode 100644 index 00000000..eb9bc829 --- /dev/null +++ b/src/compat/zlib/native-codecs.c @@ -0,0 +1,45 @@ +// Thin bindings to the pinned upstream codecs; see build-native-codecs.mjs. +#include +#include +#include +#include +#include +#include +#define API EMSCRIPTEN_KEEPALIVE + +API void* z_create(int encode) { return encode ? (void*)ZSTD_createCCtx() : (void*)ZSTD_createDCtx(); } +API void z_free(void* ctx, int encode) { if (encode) ZSTD_freeCCtx(ctx); else ZSTD_freeDCtx(ctx); } +API size_t z_param(void* ctx, int encode, int id, int value) { + return encode ? ZSTD_CCtx_setParameter(ctx, (ZSTD_cParameter)id, value) : ZSTD_DCtx_setParameter(ctx, (ZSTD_dParameter)id, value); +} +API size_t z_reset(void* ctx, int encode) { + return encode ? ZSTD_CCtx_reset(ctx, ZSTD_reset_session_only) : ZSTD_DCtx_reset(ctx, ZSTD_reset_session_only); +} +API size_t z_process(void* ctx, int encode, const void* src, size_t len, void* dst, size_t capacity, int operation, uint32_t* state) { + ZSTD_inBuffer in = { src, len, 0 }; + ZSTD_outBuffer out = { dst, capacity, 0 }; + size_t result = encode ? ZSTD_compressStream2(ctx, &out, &in, (ZSTD_EndDirective)operation) : ZSTD_decompressStream(ctx, &out, &in); + state[0] = in.pos; state[1] = out.pos; + return result; +} +API int z_is_error(size_t code) { return ZSTD_isError(code); } +API const char* z_error(size_t code) { return ZSTD_getErrorName(code); } +API size_t z_size(void* ctx, uint32_t length) { return ZSTD_CCtx_setPledgedSrcSize(ctx, length); } + +API void* b_create(int encode) { return encode ? (void*)BrotliEncoderCreateInstance(NULL, NULL, NULL) : (void*)BrotliDecoderCreateInstance(NULL, NULL, NULL); } +API void b_free(void* ctx, int encode) { if (encode) BrotliEncoderDestroyInstance(ctx); else BrotliDecoderDestroyInstance(ctx); } +API int b_param(void* ctx, int encode, int id, uint32_t value) { + return encode ? BrotliEncoderSetParameter(ctx, (BrotliEncoderParameter)id, value) : BrotliDecoderSetParameter(ctx, (BrotliDecoderParameter)id, value); +} +API int b_process(void* ctx, int encode, const uint8_t* src, size_t len, uint8_t* dst, size_t capacity, int operation, uint32_t* state) { + size_t available_in = len, available_out = capacity; + int result; + if (encode) { + result = BrotliEncoderCompressStream(ctx, (BrotliEncoderOperation)operation, &available_in, &src, &available_out, &dst, NULL); + if (result) result = BrotliEncoderIsFinished(ctx) ? 1 : BrotliEncoderHasMoreOutput(ctx) ? 3 : 2; + } else result = BrotliDecoderDecompressStream(ctx, &available_in, &src, &available_out, &dst, NULL); + state[0] = len - available_in; state[1] = capacity - available_out; + return result; +} +API int b_error(void* ctx) { return BrotliDecoderGetErrorCode(ctx); } +API const char* b_error_string(int code) { return BrotliDecoderErrorString((BrotliDecoderErrorCode)code); } diff --git a/src/compat/zlib/native-codecs.ts b/src/compat/zlib/native-codecs.ts new file mode 100644 index 00000000..f4b2acff --- /dev/null +++ b/src/compat/zlib/native-codecs.ts @@ -0,0 +1,163 @@ +import binary from "./native-codecs.wasm"; +type API = Record number> & { + memory: WebAssembly.Memory; +}; +let api: API | undefined; +function library(): API { + if (!api) { + const module = new WebAssembly.Module(binary as BufferSource); + api = new WebAssembly.Instance(module, { + env: { emscripten_notify_memory_growth() {} }, + wasi_snapshot_preview1: { + proc_exit(code: number) { + throw new Error(`Codec exited: ${code}`); + }, + fd_write() { + return 8; + }, + }, + }).exports as unknown as API; + api._initialize(); + } + return api; +} +function string(api: API, pointer: number) { + const bytes = new Uint8Array(api.memory.buffer); + let end = pointer; + while (bytes[end]) end++; + return new TextDecoder().decode(bytes.subarray(pointer, end)); +} +export type Kind = "brotli" | "zstd"; +export class NativeCodec { + private api = library(); + private ctx: number; + private state: number; + private output: number; + private closed = false; + finished = false; + consumed = 0; + constructor( + readonly kind: Kind, + readonly encode: boolean, + readonly chunkSize: number, + params: Record, + pledged?: number, + ) { + const api = this.api; + this.ctx = (kind === "brotli" ? api.b_create : api.z_create)(+encode); + this.state = api.malloc(8); + this.output = api.malloc(chunkSize); + if (!this.ctx || !this.state || !this.output) { + this.close(); + throw new RangeError("Memory limit exceeded"); + } + try { + for (const [key, value] of Object.entries(params)) { + const result = (kind === "brotli" ? api.b_param : api.z_param)( + this.ctx, + +encode, + +key, + Number(value), + ); + if (kind === "brotli" ? !result : api.z_is_error(result)) + throw Object.assign(new Error("Initialization failed"), { + code: "ERR_ZLIB_INITIALIZATION_FAILED", + }); + } + if (pledged !== undefined && kind === "zstd" && encode) + this.zcheck(api.z_size(this.ctx, pledged)); + } catch (error) { + this.close(); + throw error; + } + } + private zcheck(result: number) { + if (this.api.z_is_error(result)) { + const message = string(this.api, this.api.z_error(result)); + throw Object.assign(new Error(message), { + code: "ZSTD_ERROR", + errno: result | 0, + }); + } + } + process( + input: Uint8Array, + operation: number, + emit: (bytes: Uint8Array) => void, + ): void { + if (this.closed) throw new Error("Codec is closed"); + if ( + this.finished && + (this.kind === "brotli" || (!this.encode && input.length === 0)) + ) + return; + const api = this.api; + const pointer = api.malloc(Math.max(1, input.byteLength)); + if (!pointer) throw new RangeError("Memory limit exceeded"); + new Uint8Array(api.memory.buffer).set(input, pointer); + let offset = 0; + try { + for (;;) { + const result = (this.kind === "brotli" ? api.b_process : api.z_process)( + this.ctx, + +this.encode, + pointer + offset, + input.length - offset, + this.output, + this.chunkSize, + operation, + this.state, + ); + const state = new Uint32Array(api.memory.buffer, this.state, 2); + const consumed = state[0], + produced = state[1]; + offset += consumed; + this.consumed += consumed; + if (this.kind === "zstd") this.zcheck(result); + else if (!result) { + const errno = this.encode ? 0 : api.b_error(this.ctx); + const code = this.encode + ? "ERR_BROTLI_COMPRESSION_FAILED" + : "ERR_" + string(api, api.b_error_string(errno)); + throw Object.assign(new Error("Compression failed"), { code, errno }); + } + if (produced) + emit( + new Uint8Array(api.memory.buffer, this.output, produced).slice(), + ); + this.finished = this.kind === "brotli" ? result === 1 : result === 0; + if ( + !this.encode && + this.finished && + (this.kind === "brotli" || offset === input.length) + ) + break; + if (this.encode && operation !== 0 && this.finished) break; + if (offset === input.length && produced < this.chunkSize) { + if (this.encode && operation === 2 && !this.finished) { + if (!consumed && !produced) + throw new Error("Codec made no progress"); + continue; + } + if (!this.encode && operation === 2 && !this.finished) + throw new Error("Unexpected end of file"); + break; + } + if (!consumed && !produced) throw new Error("Codec made no progress"); + } + } finally { + api.free(pointer); + } + } + close(): void { + if (this.closed) return; + this.closed = true; + if (this.ctx) + (this.kind === "brotli" ? this.api.b_free : this.api.z_free)( + this.ctx, + +this.encode, + ); + if (this.state) this.api.free(this.state); + if (this.output) this.api.free(this.output); + } +} diff --git a/src/compat/zlib/native-codecs.wasm b/src/compat/zlib/native-codecs.wasm new file mode 100755 index 00000000..a3cdd9c7 Binary files /dev/null and b/src/compat/zlib/native-codecs.wasm differ diff --git a/src/compat/zlib/wasm.d.ts b/src/compat/zlib/wasm.d.ts new file mode 100644 index 00000000..112d4290 --- /dev/null +++ b/src/compat/zlib/wasm.d.ts @@ -0,0 +1,4 @@ +declare module "*.wasm" { + const bytes: Uint8Array; + export default bytes; +} diff --git a/src/shared/globals/inject.ts b/src/shared/globals/inject.ts index e5dfddf2..f64b26ca 100644 --- a/src/shared/globals/inject.ts +++ b/src/shared/globals/inject.ts @@ -1,3 +1,4 @@ // esbuild injects these imports only where a Node global is referenced. -export { process, global, setImmediate, clearImmediate } from "./runtime.js"; +export { global, setImmediate, clearImmediate } from "./runtime.js"; +export { default as process } from "node:process"; export { Buffer } from "node:buffer"; diff --git a/src/shared/globals/runtime.ts b/src/shared/globals/runtime.ts index c6323078..20da128f 100644 --- a/src/shared/globals/runtime.ts +++ b/src/shared/globals/runtime.ts @@ -1,5 +1,4 @@ // Keep non-Buffer globals independent of Buffer's own dependencies. -import process from "node:process"; import { setImmediate as localSetImmediate, clearImmediate as localClearImmediate, @@ -7,7 +6,7 @@ import { const nodeSetImmediate = globalThis.setImmediate ?? localSetImmediate; const nodeClearImmediate = globalThis.clearImmediate ?? localClearImmediate; const workerGlobal = globalThis; -export { process, workerGlobal as global }; +export { workerGlobal as global }; export { nodeSetImmediate as setImmediate, nodeClearImmediate as clearImmediate, diff --git a/src/shared/request-scope.ts b/src/shared/request-scope.ts new file mode 100644 index 00000000..d91cc841 --- /dev/null +++ b/src/shared/request-scope.ts @@ -0,0 +1,15 @@ +// Optional compatibility modules register request-local state here. Keeping +// this leaf dependency-free preserves the raw browser ESM entry point. +type Scope = (callback: () => T) => T; +const scopes = new Set(); +export function registerRequestScope(scope: Scope): void { + scopes.add(scope); +} +export function runWithRequestScope(callback: () => T): T { + const entries = [...scopes]; + const run = (index: number): T => + index === entries.length + ? callback() + : entries[index](() => run(index + 1)); + return run(0); +} diff --git a/src/tooling/async.ts b/src/tooling/async.ts index 7e91152e..5bc91181 100644 --- a/src/tooling/async.ts +++ b/src/tooling/async.ts @@ -35,21 +35,44 @@ export async function lowerAsync( ], }, plugins: [ - () => ({ - visitor: { - Program(path) { - // Type-only `this` parameters must not contribute to the wrapper - // arity computed by Babel before esbuild strips TypeScript syntax. - path.traverse({ - Function(path) { - const first = path.node.params[0]; - if (first?.type === "Identifier" && first.name === "this") - path.node.params.shift(); - }, - }); + () => { + const intrinsicFunctions: Array<{ async: boolean }> = []; + return { + visitor: { + Program(path) { + // Type-only `this` parameters must not contribute to the wrapper + // arity computed by Babel before esbuild strips TypeScript syntax. + path.traverse({ + Function(path) { + const first = path.node.params[0]; + if (first?.type === "Identifier" && first.name === "this") + path.node.params.shift(); + // Empty anonymous async generators are used to discover native + // iterator intrinsics (including by workerd's events module). + // They have no continuation to instrument. Hide only this + // exact form from lowering, then restore its native syntax. + const node = path.node; + if ( + node.type === "FunctionExpression" && + !node.id && + node.async && + node.generator && + node.params.length === 0 && + node.body.body.length === 0 && + node.body.directives.length === 0 + ) { + intrinsicFunctions.push(node); + node.async = false; + } + }, + }); + }, }, - }, - }), + post() { + for (const node of intrinsicFunctions) node.async = true; + }, + }; + }, asyncGenerators, asyncFunctions, ], diff --git a/src/tooling/shared.ts b/src/tooling/shared.ts index b36fca1c..5b13c344 100644 --- a/src/tooling/shared.ts +++ b/src/tooling/shared.ts @@ -1,5 +1,6 @@ import stdlib from "node-stdlib-browser"; import { fileURLToPath } from "node:url"; +import { createRequire } from "node:module"; export const configuredPath = fileURLToPath( new URL("../configured.js", import.meta.url), @@ -19,6 +20,9 @@ export const globalsInstallPath = fileURLToPath( export const nodeGlobalsInstallPath = fileURLToPath( new URL("../shared/globals/node-install.js", import.meta.url), ); +export const processStatePath = fileURLToPath( + new URL("../compat/process/state.js", import.meta.url), +); export const nodeCompatPath = (name: string) => fileURLToPath(new URL(`../compat/${name}/index.js`, import.meta.url)); const workersModulePath = (name: string) => @@ -33,6 +37,16 @@ const polyfill = (specifier: string) => // One entry per builtin; subpaths precede parent aliases when expanded. // The optional third path is the CommonJS constructor entry. const builtins: readonly (readonly [string, string, string?])[] = [ + ["_http_common", "http-common/index.js"], + ["_http_outgoing", "http-outgoing/index.js"], + ["_http_server", "http-server/index.js"], + ["zlib", "zlib/index.js"], + ["crypto", "crypto/index.js", "crypto/index.cjs"], + ["console", "console/index.js"], + ["sys", "util/index.js"], + ["util/types", "util/types/index.js"], + ["events", "events/index.js", "events/index.cjs"], + ["process", "process/index.js", "process/index.cjs"], ["punycode", "punycode/index.js"], ["child_process", "child-process/index.js"], ["cluster", "cluster/index.js"], @@ -64,6 +78,9 @@ const builtins: readonly (readonly [string, string, string?])[] = [ ["tty", "tty/index.js"], ["url", "url/index.js"], ["diagnostics_channel", "diagnostics-channel/index.js"], + ["tls", "tls/index.js"], + ["_tls_wrap", "tls-wrap/index.js"], + ["_stream_wrap", "stream-wrap/index.js"], ["_tls_common", "tls-common/index.js"], ["async_hooks", "async-hooks/index.js"], ["timers/promises", "timers/promises.js", "timers/promises.cjs"], @@ -117,7 +134,14 @@ function builtinAliases(commonJS = false): Record { export function compatibilityAliases(): Record { return { - "process/": stdlib.process, + "process/": nodeCompatPath("process"), + // Preserve brorand's browser crypto:false branch across Vite aliases. + brorand: fileURLToPath( + new URL("../compat/crypto/brorand.cjs", import.meta.url), + ), + "cf-workers-native:process-base": createRequire(import.meta.url).resolve( + stdlib.process, + ), "cloudflare:node": workersModulePath("cloudflare-node"), "cloudflare:workers": workersModulePath("cloudflare-workers"), "punycode/": polyfill("punycode/punycode.js"), @@ -135,6 +159,7 @@ export function commonJSCompatibilityAliases(): Record { export function compatibilityDependencies(): string[] { return [ + processStatePath, "cloudflare:node", "cloudflare:workers", ...Object.keys(builtinAliases()), diff --git a/src/tooling/vite.ts b/src/tooling/vite.ts index 4e2fcc59..231c6a68 100644 --- a/src/tooling/vite.ts +++ b/src/tooling/vite.ts @@ -20,6 +20,7 @@ import { configuredPath, compatibilityAliases, compatibilityDependencies, + processStatePath, commonJSCompatibilityAliases, globalsInstallPath, nodeGlobalsInstallPath, @@ -168,8 +169,9 @@ function compatibilityPlugins( ...inject({ Buffer: [globalsInjectPath, "Buffer"], // util reads process during Buffer initialization. Keep this import - // independent of the injector that also imports Buffer. - process: [aliases["process/"], "default"], + // on the shared state leaf: importing the full process adapter here + // would cycle through events/util in optimized development chunks. + process: [processStatePath, "default"], global: [globalsRuntimePath, "global"], setImmediate: [globalsRuntimePath, "setImmediate"], clearImmediate: [globalsRuntimePath, "clearImmediate"], diff --git a/tests/unit/async-build.test.ts b/tests/unit/async-build.test.ts index b86e74d1..da6a2709 100644 --- a/tests/unit/async-build.test.ts +++ b/tests/unit/async-build.test.ts @@ -21,3 +21,49 @@ it("preserves runtime arity and receivers when lowering TypeScript async functio expect(original.length).toBe(1); expect(await original.call({ value: 4 }, 3)).toBe(7); }); + +it("preserves native async iterator identity through repeated build passes", async () => { + const source = `export const intrinsic = Object.getPrototypeOf( + Object.getPrototypeOf(async function* () {}).prototype + ); + export const empty = async function* () {};`; + const first = await lowerAsync(source, "worker.js", "js"); + // Vite transforms source modules and subsequently rendered chunks. + const second = await lowerAsync(first.code, "chunk.js", "js"); + const api = await import( + `data:text/javascript;base64,${Buffer.from(second.code).toString("base64")}` + ); + const native = Object.getPrototypeOf( + Object.getPrototypeOf(async function* () {}).prototype, + ); + expect(api.intrinsic).toBe(native); + const iterator = api.empty(); + expect(iterator[Symbol.asyncIterator]()).toBe(iterator); + expect(await iterator.next()).toEqual({ value: undefined, done: true }); +}); + +it("still lowers generators with continuations and preserves cancellation cleanup", async () => { + const source = `export const sequence = async function* () { + try { yield await Promise.resolve(1); yield 2; } + finally { globalThis.__asyncBuildCleanup = true; } + };`; + const lowered = await lowerAsync(source, "worker.js", "js"); + expect(lowered.code).not.toMatch(/async\s+function\s*\*/); + const { sequence } = await import( + `data:text/javascript;base64,${Buffer.from(lowered.code).toString("base64")}` + ); + const state = globalThis as typeof globalThis & { + __asyncBuildCleanup?: boolean; + }; + try { + const iterator = sequence(); + expect(await iterator.next()).toEqual({ value: 1, done: false }); + expect(await iterator.return("cancel")).toEqual({ + value: "cancel", + done: true, + }); + expect(state.__asyncBuildCleanup).toBe(true); + } finally { + delete state.__asyncBuildCleanup; + } +}); diff --git a/tests/unit/opennext-compat.test.ts b/tests/unit/opennext-compat.test.ts index 963f348e..54264ee0 100644 --- a/tests/unit/opennext-compat.test.ts +++ b/tests/unit/opennext-compat.test.ts @@ -28,13 +28,7 @@ it("module contract", async () => { expect(report.cases.filter((c) => c.status !== "passed")).toEqual([]); }); it("stream-web contract", async () => { - const nativeFile = Object.getOwnPropertyDescriptor(globalThis, "File")!; const release = installWebGlobals(); - const installedFile = Object.getOwnPropertyDescriptor(globalThis, "File")!; - // Undici's multipart parser constructs global File and then validates its - // private native brand. Keep that Node-only codec dependency for this test; - // the browser and workerd contracts exercise the installed File constructor. - Object.defineProperty(globalThis, "File", nativeFile); try { const report = await runContract( new StreamWebImplementation(localStreams), @@ -43,7 +37,6 @@ it("stream-web contract", async () => { expect(report.errors).toEqual([]); expect(report.cases.filter((c) => c.status !== "passed")).toEqual([]); } finally { - Object.defineProperty(globalThis, "File", installedFile); release(); } }); diff --git a/vitest.config.ts b/vitest.config.ts index e9be9ac3..e43e97b5 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -11,9 +11,9 @@ export default defineConfig({ }, }, { - name: "test-brotli-binary", + name: "test-wasm-binary", async load(id) { - if (!id.endsWith("/brotli_dec_wasm_bg.wasm")) return; + if (!/\/(?:brotli_dec_wasm_bg|native-crypto)\.wasm$/.test(id)) return; const data = (await readFile(id)).toString("base64"); return `export default Uint8Array.from(atob(${JSON.stringify(data)}), c => c.charCodeAt(0));`; }, @@ -46,6 +46,8 @@ export default defineConfig({ "src/browser/fetch/native-network.spec.ts", "src/browser/fetch/scope.spec.ts", "src/compat/net/sockets.spec.ts", + "src/compat/tls/upgrade.spec.ts", + "src/compat/fs/writable.spec.ts", "src/shared/http-wire/index.spec.ts", ], },