npm.io
0.25.0 • Published 1 week ago

@bytecodealliance/preview2-shim

Licence
(Apache-2.0 WITH LLVM-exception)
Version
0.25.0
Deps
1
Size
579 kB
Vulns
0
Weekly
0
Stars
1.0K

Preview2 Shim

WASI Preview2 implementations for Node.js & browsers.

Node.js support is fully tested and conformant against the Wasmtime test suite.

Browser support is available with the platform limitations documented below.

The Node.js implementation owns its worker artifact. Direct package use and supported downstream bundlers should resolve it through the public shim imports; applications do not need to import or copy files from dist/io.

Browser support matrix

Browser defaults are capability-safe: clocks and secure randomness use Web APIs, stdout and stderr write to the console, stdin is closed, outbound HTTP uses fetch, filesystem preopens must be configured explicitly, and raw sockets are unavailable unless an embedding supplies an adapter.

WASI area Browser status Default capability
CLI environment and arguments Configurable per WASIShim; compatibility setters are global Empty snapshots unless configured
CLI stdin Adapter-backed Closed stream
CLI stdout and stderr Web API Console-backed, preserving split UTF-8 writes until flush/newline
CLI terminals Adapter-backed No terminal resource
Clocks Web API performance.now, Date.now, and timer-backed pollables
Random Web API crypto.getRandomValues, including requests larger than 64 KiB
I/O streams and poll Implemented browser resources Non-blocking streams depend on their injected handlers
Filesystem Adapter-backed; opt-in in-memory compatibility implementation No persistent storage is selected implicitly
Outbound HTTP Web API Delegates to fetch
Incoming HTTP Adapter-backed; opt-in in-memory client Browsers cannot listen for arbitrary inbound HTTP
TCP and UDP Adapter-backed; opt-in in-memory implementations Raw sockets are not exposed by standard browsers
DNS Host adapter required DNS is not exposed independently by standard browsers
WASIShim instantiation Implemented Interface namespaces can be overridden per instance

An operation is not considered supported merely because its interface shape exists. Adapter-backed rows require the embedding application to provide that capability; unavailable operations fail with a WASI-domain error instead of logging or returning a placeholder resource.

Detailed browser capabilities

The following table describes the built-in browser implementation. An application-provided namespace can replace any row through WASIShim.

Interface Implemented Host adapter required Unsupported by browser implementation
wasi:cli environment, arguments, initial cwd, exit, stream and terminal accessors stdin/stdout/stderr handlers and terminal resources
wasi:clocks wall clock, monotonic clock, timer subscriptions timezone APIs (not part of Preview 2)
wasi:random secure and insecure bytes, insecure seed
wasi:io errors, input/output streams, poll and pollables readiness and I/O behavior for injected stream handlers synchronous blocking of the browser event loop
wasi:filesystem descriptors, files, directories, links, metadata, streams, preopens through the ephemeral adapter persistent storage, permissions, and external file handles symbolic-link creation and reading
wasi:http/outgoing-handler Fetch-backed requests and buffered request bodies Fetch implementation and network permission request/response trailers; streaming uploads
wasi:http/incoming-handler request/response translation, injectable handler, and in-memory client HTTP server, service worker, or other request source direct browser listening
wasi:sockets/ip-name-lookup interface shape only complete interface replacement built-in DNS lookup
wasi:sockets/tcp* opt-in in-memory server/client implementation adapter for external connectivity built-in raw TCP
wasi:sockets/udp* opt-in in-memory server/client implementation adapter for external connectivity built-in raw UDP

Outbound HTTP buffers a requested body until outgoing-body.finish before calling fetch. This preserves complete-body semantics across browsers but does not provide streaming upload or upload backpressure. Incoming Fetch bodies retain their asynchronous stream behavior. HTTP trailers are not implemented.

Chromium-based browsers can opt into Fetch request streaming. This setting uses a ReadableStream request body with duplex: "half"; unsupported browsers reject the request, so applications should enable it only after applying their own browser support policy or feature detection:

import { http } from "@bytecodealliance/preview2-shim";

http._setRequestStreaming(true);

The setting affects subsequent requests made through the browser HTTP shim. Call http._setRequestStreaming(false) to restore portable completion buffering.

Browser applications select storage explicitly. The bundled file-data adapter is ephemeral and must be opted into:

import { filesystem } from "@bytecodealliance/preview2-shim";
import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";

const shim = new WASIShim({
    environment: { MODE: "browser" },
    arguments: ["component"],
    stdout: { write: (bytes) => terminal.write(bytes) },
    browserFilesystem: {
        adapter: new filesystem.InMemoryFilesystemAdapter(),
        preopens: { "/data": { dir: {} } },
    },
    sandbox: { enableNetwork: false },
});

The browser shim does not request File System Access permissions or choose IndexedDB/OPFS on an application's behalf. Applications that need another storage model implement the generated wasi:filesystem/types and wasi:filesystem/preopens namespaces and inject them through the filesystem option:

const shim = new WASIShim({
    filesystem: {
        types: applicationFilesystemTypes,
        preopens: applicationFilesystemPreopens,
    },
});

This keeps permission prompts, handle acquisition, persistence, and synchronization policy in application code. Raw TCP, UDP, and DNS are denied by default; outbound HTTP remains a separate fetch capability.

Deterministic in-memory transports are available for components that act as servers and for tests:

import { http, sockets } from "@bytecodealliance/preview2-shim";

const tcpSockets = new sockets.InMemoryTcpSockets();
const tcpClient = tcpSockets.connect(serverAddress);
const udpSockets = new sockets.InMemoryUdpSockets();
const udpClient = udpSockets.createClient(clientAddress);
const httpClient = new http.InMemoryHttpClient(component.incomingHandler);

const shim = new WASIShim({ tcpSockets, udpSockets });

These implementations route bytes only within the current JavaScript realm; they do not grant raw browser network access. InMemoryHttpClient.fetch(request) returns a standard Web Response.

For a small application-owned implementation, see the Map-backed browser filesystem test shim. It keeps named roots in an in-memory Map, implements createPreopens, and is intentionally example code rather than a published or supported filesystem package. The example is exercised through the reusable filesystem implementation test suite, which can also be pointed at other implementations.

Browser filesystem adapters own the capabilities passed in preopens and the roots returned from getRoot. A root may be shared by multiple descriptors and preopen names; the adapter is therefore responsible for persistence and synchronization of shared mutations. Calling dispose on the namespace returned by createFilesystem calls the adapter's optional dispose method once and invalidates further preopen access. WASIShim does not currently cascade disposal, so embeddings using external handles must retain and dispose their application-owned filesystem namespace or adapter themselves. The bundled in-memory adapter keeps all state in memory and shares mutations for the same file-data object.

Features

WASI Shim object for easy instantiation

An default instantiation object can be used via the WASIShim class in @bytecodealliance/preview2-shim/instantiation:

import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";
import type {
    VersionedWASIImportObject,
    WASIImportObject,
} from "@bytecodealliance/preview2-shim/instantiation";

const shim = new WASIShim();

const unversioned: WASIImportObject = shim.getImportObject();
// console.log('unversioned', unversioned);
unversioned satisfies WASIImportObject;
unversioned satisfies VersionedWASIImportObject<"">;

const versioned: VersionedWASIImportObject<"0.2.3"> = shim.getImportObject({
    asVersion: "0.2.3",
});
//console.log('versioned', versioned);
versioned satisfies VersionedWASIImportObject<"0.2.3">;

The import object generated by getImportObject can be easily used in instantiate() calls produced by jco transpile (with --instantiation=async):

import { WASIShim } from '@bytecodealliance/preview2-shim/instantiation';

// The code below assumes that you have output your transpiled WebAssembly component to `dist/transpiled`
import { instantiate } from './dist/transpiled/component.js';

const loader = async (path: string) => {
    const buf = await readFile(`./dist/transpiled/${path}`);
    return await WebAssembly.compile(buf.buffer as ArrayBuffer);
};
const component = await instantiate(loader, new WASIShim().getImportObject());

// TODO: Code that uses your component's exports goes here.

Sandboxing

On Node.js, the preview2-shim provides host filesystem, environment, and network access by default, matching the usual behavior of Node.js libraries. Browser defaults expose no filesystem preopens or raw sockets. Both platforms can configure which capabilities a guest receives.

Using WASIShim for sandboxing

The WASIShim class accepts a sandbox configuration option to control access:

import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";

// Fully sandboxed - no filesystem, network, or env access
const sandboxedShim = new WASIShim({
    sandbox: {
        preopens: {}, // No filesystem access
        env: {}, // No environment variables
        args: ["arg1"], // Custom arguments
        enableNetwork: false, // Disable network access
    },
});

// Node.js only: map virtual paths to host paths
const limitedShim = new WASIShim({
    sandbox: {
        preopens: {
            "/data": "/tmp/guest-data", // Guest sees /data, maps to /tmp/guest-data
            "/config": "/etc/app", // Guest sees /config, maps to /etc/app
        },
        env: { ENV1: "42" }, // Only expose specific env vars
    },
});

const component = await instantiate(loader, sandboxedShim.getImportObject());
Notes on sandboxing
  • By default (when no options are passed), the shim is providing full access to match typical Node.js library behavior. In browsers, filesystem preopens remain empty until the application explicitly injects filesystem namespaces or selects the ephemeral file-data adapter.
  • sandbox.preopens maps guest paths to Node.js host paths on Node.js. With browserFilesystem, the same option maps guest paths to capabilities understood by its adapter and overrides browserFilesystem.preopens. A custom filesystem can implement createPreopens(preopens) to interpret the provided properties and return its own wasi:filesystem/preopens namespace. The shim passes those properties through unchanged.
  • Each WASIShim instance has its own isolated preopens, environment variables, and arguments. Multiple instances with different configurations will not affect each other.
  • The direct preopen functions (_setPreopens, _clearPreopens, etc.) modify global state and affect all components not using WASIShim with explicit configuration. For isolation, prefer using WASIShim with the sandbox option containing preopens and env.
  • When sandbox.enableNetwork: false, Node.js socket operations receive an instance-local denied network capability. Outbound HTTP is a separate Fetch capability; replace or omit the HTTP namespace when the embedding must deny it as well.

License

This project is licensed under the Apache 2.0 license with the LLVM exception. See LICENSE for more details.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.

Host IO extensions

Opt-in providers can use the Node-only @bytecodealliance/preview2-shim/io-worker entry point to operate on existing streams in the shim's IO worker. Host-selected modules load lazily and share the worker's stream, future, and poll ownership rules. Guest code cannot select extension modules.

The Node wasi:tls provider and its TLS policy live in jco-std, which wraps the supplied TCP streams without opening a replacement connection.