Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

node:tls

ImportsImplementation
node:tls@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/tls

node:tls targets Node 24.20.0 and exposes its complete module export list. An injected jco:node/tls@0.1.0 provider supplies encrypted sockets, listeners, secure contexts, certificate inspection, cipher/CA queries, and TLS controls. The guest socket uses the portable classic Duplex implementation for pipes and backpressure. Importing the module grants no capabilities; the default provider throws ERR_JCO_TLS_ADAPTER_REQUIRED when used.

Jco replaces ordinary node:tls imports and adds the jco:node/tls@0.1.0 capability plus the tls-callbacks guest export. Cryptography, certificate parsing, and the TLS protocol run in the injected provider. Importing the module does not open sockets or read the host’s trust store.

Shared TLS provider

node:tls, direct HTTPS, and secure direct HTTP/2 use jco:node/tls. HTTP protocols pass one-use TLS configuration handles to their transport host, so their WIT interfaces no longer duplicate certificate and cipher options. Bind those HTTP hosts to the same TLS provider instance. The native provider consumes the handle before starting the HTTP operation; handles from another provider, or already consumed handles, are rejected.

HTTPS over WASI sockets calls jco:node/tls.start-tls. createWasiTlsBridge delegates that operation to a supplied wasi:tls/types@0.2.0-draft provider and reuses its future, connection, and WASI IO resources. The draft only supports verified client stream upgrades. It cannot supply Node servers, per-connection trust options, certificate inspection, or cipher controls; the bridge denies those operations. A native provider can also accept { wasiTls } to provide both paths. The older /tls/host exports remain WASI providers for existing embedders; they are not the primary Node capability.

HTTPS via wasi-http is unsupported: outgoing-handler cannot accept the TLS provider or its configuration. Select direct or wasi-sockets. HTTP/2 over WASI sockets remains h2c-only.

Binding the native provider

Use one factory per component. With --instantiation, the import keys are the WIT interface names without their versions; Jco’s deny-by-default module map applies only to ESM output:

import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";
import { createTlsHost } from "@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/tls/node-host/node";
import * as wasiTlsTypes from "@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/tls/host";
import { createHttpHost } from "@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/http/host/node";
import { createHttp2Host } from "@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/http2/host/node";
import { instantiate } from "./component.js";

const tls = createTlsHost({ onCallbackError: (error) => console.error(error) });
let instance;
instance = await instantiate(undefined, {
  ...new WASIShim().getImportObject(),
  "jco:node/tls": tls,
  "wasi:tls/types": wasiTlsTypes,
  "jco:node/http": createHttpHost(() => instance.httpCallbacks, tls),
  "jco:node/http2": createHttp2Host(() => instance.http2Callbacks, tls),
});
if (instance.tlsCallbacks) tls.attachCallbacks(instance.tlsCallbacks);
// Call component exports. When the component is finished:
// tls.dispose();

Transpile with JSPI and promising exports. For direct HTTP, also select async imports jco:node/http@0.1.0#request, #[method]server.listen, #[method]server.close, and #[method]server.get-connections. Direct HTTP/2 has the existing async session/stream selectors. Explicit --map selections for the HTTP providers add those selectors automatically. Supplying import objects at instantiation does not retroactively change the generated binding mode.

The native provider grants native network listeners/connections and trust-store queries. setDefaultCACertificates changes only that provider’s trust policy, including its subsequent HTTPS and HTTP/2 configurations. It does not change the embedding process’s default CA store. Explicit SecureContext objects retain their original trust settings.

dispose() destroys sockets, including incomplete handshakes, closes listeners, and releases contexts. Guest callback traps dispose the provider and are passed to onCallbackError. Standalone contexts and closed server objects remain provider-owned until disposal, allowing normal inspection and server reuse.

Supplying your own implementation

Import TlsHost and TlsCallbacks from /tls/core. Start with the deny provider and replace only the operations your component needs:

import denied from "@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/tls/node-host";
import type { TlsHost } from "@bytecodealliance/jco-std/wasi/0.2.x/node/24.x.x/tls/core";

const host: TlsHost = {
  ...denied,
  // Implement permitted operations; every other operation still fails explicitly.
};

For a WASI provider, createWasiTlsBridge(provider) already supplies such an object. No alias mapping is necessary when supplying the object directly under the binding’s existing import key. A custom HTTP transport must understand its TLS provider’s configuration handles. TlsConfigurationProvider.takeContextOptions is the native HTTP adapters’ local integration contract, not an additional WIT import. It is exported as a type from /tls/core; HTTP hosts only require this method from their TLS provider.

The interface groups operations as follows:

OperationsResponsibility
query, set-default-caCipher/CA/identity queries and provider-local trust changes.
create-context, release-contextValidate TLS options and manage opaque configuration handles.
connect, create-serverCreate a transport for the supplied guest identifier.
socket-operation, server-operationClosed enums of inspection and control operations.
write, end, releaseStream writes, completion acknowledgements, and socket cleanup.
is-available, start-tlsOptional upgrades of owned WASI streams using the WASI TLS contract.

Dispatch events after the initiating import returns. target is the guest socket/server ID. Accepted sockets use a separate positive 31-bit identifier range. The guest installs an accepted socket when it receives secureConnection. write events carry { token, error? } and complete exactly one pending write or end callback. Pause native reads after delivering a data chunk; the guest’s resume operation signals available buffer capacity. core.ts and host-node.ts define the event payloads and closed operation argument lists together.

Options and inspection values use wire.ts’s graph JSON format, { root, nodes }. Primitive values are inline; { ref: index } references bytes, array, or object nodes. Undefined and non-finite numbers have explicit tagged values. Object nodes contain key/value pairs and may carry an error name. This preserves binary fields, error codes, and self-signed certificate issuer cycles. Stream data itself is list<u8>, not JSON. Functions and native handles never cross this boundary. WIT result errors carry name, message, and optional code.

Compatibility and engine limits

The full Node 24.20 module export list is present. Local tests cover mutual TLS, hostname/trust rejection, custom identity checks, ALPN, certificate graphs, keying material, early end(), and backpressure. The component fixture adapts the documentation’s echo client/server to use PEM arguments and finite input instead of filesystem reads and process.stdin. It also exercises HTTPS in both directions, and the HTTP/2 component fixture exercises the shared provider.

Explicit unsupported operations include wrapping an arbitrary guest socket with new TLSSocket, native X509Certificate return values, PSK/SNI/ALPN/lookup callbacks, OpenSSL engine options, and server newSession, resumeSession, OCSPRequest, keylog, and raw TCP connection events. Use connect, getPeerCertificate, getCertificate, and Server.addContext where applicable. Socket session, keylog, and OCSPResponse events are delivered. Renegotiation limits are host policy; changing CLIENT_RENEG_LIMIT or CLIENT_RENEG_WINDOW throws. The socket is a portable Duplex; it is not an instance of the separate node:net shim’s Socket class.

StarlingMonkey runs the component fixture. QuickJS is explicitly skipped with TODO(unskip) because componentize-qjs cannot link the shared WASI TLS resource types. Export a synchronous starter for long-lived event-driven work: a guest export cannot await a promise resolved solely by a future independent host callback. The fixture’s starter/status exports demonstrate this engine boundary.