Skip to content

Repository files navigation

Cap'n Web over Iroh

Iroh 1.0 transports for Cap'n Web. The packages let Node.js and browser peers run Cap'n Web RPC sessions over direct, encrypted Iroh QUIC connections.

Packages

Package Purpose
@cbj/iroh-capnweb-js-core Shared ALPN and Cap'n Web session helper
@cbj/iroh-capnweb-js-node Transport for native @number0/iroh connections
@cbj/iroh-capnweb-js-browser Browser endpoint and transport backed by WebAssembly

The Node and browser packages re-export every API from core, including ALPN and createIrohRpcSession. The packages are ESM-only and use capnweb 0.10 from npm. The Node transport requires Node.js 20.3 or newer; repository development uses Node.js 22.18 or newer.

Install

Choose the transport for each peer:

npx jsr add @cbj/iroh-capnweb-js-node
npx jsr add @cbj/iroh-capnweb-js-browser

With Deno or package managers that support JSR natively, use deno add jsr:@cbj/iroh-capnweb-js-browser, pnpm add jsr:@cbj/iroh-capnweb-js-browser, or the equivalent. The Node transport uses the npm package @number0/iroh 1.0; declare it directly when your application imports Iroh classes, as the examples below do.

Node.js

Connect to an endpoint ticket and create a typed remote stub:

import { createIrohRpcSession, IrohTransport } from "@cbj/iroh-capnweb-js-node";
import { ALPN } from "@cbj/iroh-capnweb-js-node";
import { Endpoint, EndpointTicket } from "@number0/iroh";

const endpoint = await Endpoint.bind({ alpns: [ALPN] });
const address = EndpointTicket.fromString(ticket).endpointAddr();
const connection = await endpoint.connect(address, ALPN);

using remote = createIrohRpcSession<RemoteApi>(new IrohTransport(connection));
await remote.ping();

await endpoint.close();

The same transport handles an accepted server connection. listen() owns the endpoint accept loop and begins accepting when its first connection listener is attached:

import { ALPN, createIrohRpcSession, IrohTransport, listen } from "@cbj/iroh-capnweb-js-node";
import { Endpoint } from "@number0/iroh";

const endpoint = await Endpoint.bind({ alpns: [ALPN] });
await using server = listen(endpoint);

server.addEventListener("connection", ({ connection }) => {
  createIrohRpcSession(new IrohTransport(connection), new Api());
});

Disposing the server stops its accept loop and closes the endpoint and active connections.

Browser

The browser package creates its own Iroh endpoint. WebAssembly initialization is lazy and happens on the first API call:

import {
  createCapnWebEndpoint,
  createIrohRpcSession,
  getAddressFromString,
  IrohTransport,
} from "@cbj/iroh-capnweb-js-browser";

const endpoint = await createCapnWebEndpoint();
const address = await getAddressFromString(ticket);
const connection = await endpoint.connect(address);

using remote = createIrohRpcSession<RemoteApi>(new IrohTransport(connection));
await remote.ping();

await endpoint.close();

The generated ESM resolves its .wasm file with import.meta.url; modern bundlers should copy it as an asset automatically. init() is available for applications that want to preload the module.

createCapnWebEndpoint() uses Iroh's production relay configuration by default. It also accepts staging, custom relay URLs, and a stable 32-byte secret key:

await createCapnWebEndpoint({ relay: "staging" });
await createCapnWebEndpoint({ relay: { urls: ["https://relay.example.com"] } });
await createCapnWebEndpoint({ secretKey });

Browser endpoints can call accept() directly or use the endpoint-owning listen() API shown in the Node example. endpoint.ticket() returns a dialable endpoint ticket.

Callback capabilities

Pass an RpcTarget explicitly when the remote API needs to call back:

using camera = createIrohRpcSession<CameraApi>(new IrohTransport(connection));
const viewer = new ViewerTarget("local-viewer");

await camera.takePhoto("hello", viewer);

Cap'n Web converts the target into a remote stub for that call. Disposing the remote main stub shuts down the session and aborts the transport.

Protocol

  • ALPN: capnweb/iroh/1
  • One bidirectional QUIC stream per RPC session
  • UTF-8 Cap'n Web JSON messages
  • Four-byte, unsigned, big-endian payload length
  • Maximum message size: 32 MiB

Both peers must use the same ALPN and framing version.

Development

The browser build requires the wasm32-unknown-unknown Rust target, wasm-pack 0.15.0, and Clang with a WebAssembly backend. On macOS, brew install llvm supplies a compatible compiler.

vp install
rustup target add wasm32-unknown-unknown
cargo install wasm-pack --version 0.15.0 --locked
vp exec playwright install chromium
vp run ready

vp pack owns the browser package build: its prepare hook runs wasm-pack, then copies the generated .wasm asset into dist. The end-to-end suite uses real Iroh endpoints and relay networking for Node-to-Node, Browser-to-Browser, and Browser-to-Node coverage.

See RELEASING.md for the JSR release checklist.

License

Licensed under either the Apache License 2.0 or the MIT License, at your option.

About

Cap'n Web transports for Iroh on Node.js and browsers

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages