This covers the full API surface shared by all runtimes (Node.js, Deno, Tauri). Import paths differ; everything else is identical.
import { createNode } from "@momics/iroh-http-node"; // or -deno, -tauriconst node = await createNode();
console.log(node.publicKey.toString()); // base32 node ID, this is your addressWith options:
const node = await createNode({
key: savedKey, // Uint8Array, restores a stable identity
relay: {
mode: "default", // or "staging" / "disabled"
urls: ["https://my-relay.example.com"], // optional custom relay list
},
discovery: {
dns: true,
mdns: { serviceName: "my-app" },
},
});const res = await node.fetch("httpi://<peer-public-key>/api/data");
console.log(res.status, await res.json());httpi:// is the URL scheme. The host is a base32 public key, not a hostname. The response is a standard Response object.
node.serve({}, (req) => {
const peerId = req.headers.get("Peer-Id"); // authenticated caller identity
return new Response("ok");
});The handler receives a standard Request and returns a Response, same contract as Deno.serve. The Peer-Id header is injected by the QUIC layer and cannot be spoofed.
Graceful shutdown via AbortSignal:
const ac = new AbortController();
node.serve({ signal: ac.signal }, handler);
ac.abort(); // stop accepting new connectionsRaw QUIC connections with bidirectional streams, unidirectional streams, and datagrams. The API follows WebTransport.
const session = await node.dial("<peer-public-key>");
await session.ready;const { readable, writable } = await session.createBidirectionalStream();
const writer = writable.getWriter();
await writer.write(new TextEncoder().encode("hello"));
await writer.close();
const reader = readable.getReader();
const { value } = await reader.read();
console.log(new TextDecoder().decode(value));const ac = new AbortController();
for await (const session of node.incoming({ signal: ac.signal })) {
console.log("peer:", session.remoteId.toString());
for await (const { readable, writable } of session.incomingBidirectionalStreams) {
// handle stream
}
}Unreliable, low-latency, no ordering guarantees.
await session.datagrams.writable.getWriter()
.write(new TextEncoder().encode("ping"));
const { value } = await session.datagrams.readable.getReader().read();// Send-only:
const writable = await session.createUnidirectionalStream();
await writable.getWriter().write(data);
// Receive-only:
const reader = session.incomingUnidirectionalStreams.getReader();
const { value: inStream } = await reader.read();Find peers on the local network. Desktop uses the Rust DNS-SD transport; the
Tauri adapter uses Android NsdManager and Apple Network/Bonjour APIs on mobile.
All platforms feed the same Rust peer projection and endpoint lookup.
await node.advertisePeer({ serviceName: "my-app" });const ac = new AbortController();
for await (const event of node.browsePeers({ serviceName: "my-app", signal: ac.signal })) {
if (event.isActive) {
console.log("found:", event.nodeId, event.addrs);
const res = await node.fetch(`httpi://${event.nodeId}/api`);
} else {
console.log("gone:", event.nodeId);
}
}Every node has an Ed25519 keypair. Signing and verification work both as standalone functions and as methods on a live node.
import { generateSecretKey, secretKeySign, publicKeyVerify } from "@momics/iroh-http-node";
const sk = generateSecretKey(); // 32-byte Ed25519 seed
const sig = secretKeySign(sk, data); // 64-byte signature
const ok = publicKeyVerify(publicKey, data, sig); // booleanSigning via node.secretKey requires a node created with a supplied key
(otherwise secretKey is undefined — the identity is native-held):
const key = generateSecretKey();
const node = await createNode({ key });
const sig = await node.secretKey.sign(data);
const ok = await node.publicKey.verify(data, sig);Generate a key, pass it to createNode, and store its bytes for next launch:
const key = generateSecretKey(); // Uint8Array (32 bytes)
const node = await createNode({ key }); // node.secretKey === key
// persist `key` (or node.secretKey.toBytes()) in secure storage
const restored = await createNode({ key }); // same identityconst pk = PublicKey.fromString(nodeIdString); // parse base32 node ID
pk.toString(); // base32 string
pk.bytes; // Uint8Array(32)
pk.equals(otherKey); // identity comparisonIroh QUIC authenticates peer identity cryptographically. You always know who connected. But it does not handle authorization. That's your job.
The Peer-Id header on incoming requests is injected by the transport layer and stripped if a client tries to set it. It's safe to trust.
const ALLOWED = new Set(["<peer-key-1>", "<peer-key-2>"]);
node.serve({}, (req) => {
if (!ALLOWED.has(req.headers.get("Peer-Id")))
return new Response("Forbidden", { status: 403 });
return new Response("ok");
});
for await (const session of node.incoming()) {
if (!ALLOWED.has(session.remoteId.toString())) {
session.close(403, "Forbidden");
continue;
}
}const node = await createNode();
// ... use the node ...
await node.close(); // graceful: drains in-flight requests, then shuts downOr with using (TypeScript 5.2+):
await using node = await createNode();
// automatically closed when scope exits