npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@perryts/iroh

v0.3.0

Published

Iroh P2P framework bindings (direct peer-to-peer QUIC + hole punching + relay fallback) for the Perry TypeScript-to-native compiler.

Readme

@perryts/iroh

Native bindings for Iroh — direct peer-to-peer QUIC connections with hole punching + relay fallback — for the Perry TypeScript-to-native compiler.

Closes PerryTS/perry#425.

What this is

A Perry "native library" package: a Rust crate exporting extern "C" symbols that the Perry compiler links into your TypeScript program. From your TypeScript code you import iroh like any npm package; under the hood every method call resolves to a direct call into the bundled staticlib — no Node addon, no IPC, no JSON marshalling.

This package contains:

  • src/lib.rs — the Rust crate that wraps iroh and exposes js_iroh_* extern "C" symbols
  • src/index.d.ts — the TypeScript surface (iroh module declaration) Perry resolves at compile time
  • Cargo.toml — staticlib build config consumed by the Perry linker
  • package.json — includes the perry.nativeLibrary manifest block

Install

bun add @perryts/iroh
# or
npm install @perryts/iroh

The package's package.json declares a perry.nativeLibrary block (see the manifest spec) which Perry's compiler reads at link time to discover the staticlib + extern "C" symbols. No post-install build step — Perry compiles the Rust crate as part of your project's build.

Quick start

A request/response peer-to-peer round trip. The server binds, prints its node id (share that out-of-band), waits for one peer, reads a message, and echoes it back. The client binds its own endpoint, dials the server's node id, sends a message, and reads the reply.

Server

import * as iroh from "iroh";

const ep = await iroh.bind();
const myId = await iroh.nodeId(ep);
console.log("share this with the peer:", myId);

const conn = await iroh.acceptOne(ep);
const stream = await iroh.acceptBi(conn);
const msg = await iroh.streamReadToEnd(stream, 65_536);
await iroh.streamWrite(stream, `echo: ${msg}`);
await iroh.streamFinish(stream);
await iroh.connClose(conn);
await iroh.close(ep);

Client

import * as iroh from "iroh";

const ep = await iroh.bind();
const conn = await iroh.connect(ep, "<server-node-id-from-server-stdout>");
const stream = await iroh.openBi(conn);
await iroh.streamWrite(stream, "hello, peer!");
await iroh.streamFinish(stream);
const reply = await iroh.streamReadToEnd(stream, 65_536);
console.log(reply);
await iroh.connClose(conn);
await iroh.close(ep);

The handshake flow

Iroh uses three layers of object — endpoint, connection, bi-directional stream — and you call them in roughly mirrored sequences on each side. The server side reads, the client side writes; whichever side calls streamFinish first signals "no more bytes from me" so the other side's streamReadToEnd resolves.

| Step | Server | Client | |---|---|---| | 1 | bind() → endpoint | bind() → endpoint | | 2 | nodeId(ep) → publish out-of-band | connect(ep, nodeId) → connection | | 3 | acceptOne(ep) → connection | openBi(conn) → stream | | 4 | acceptBi(conn) → stream | streamWrite + streamFinish | | 5 | streamReadToEnd | streamReadToEnd | | 6 | streamWrite + streamFinish | connClose + close | | 7 | connClose + close | — |

bind uses Iroh's N0 preset: discovery via the n0 number-DNS, n0 relay servers for hole-punch fallback. v0.3 adds optional secretKey (stable identity across restarts) and mdns (LAN peer discovery) knobs — see bind(options?) below.

API reference

bind(options?)

interface BindOptions {
  secretKey?: string;  // hex- or base32-encoded SecretKey for stable identity
  mdns?: boolean;      // enable LAN peer discovery via mDNS-like swarm discovery
}

function bind(options?: BindOptions): Promise<EndpointHandle>

Bind a fresh QUIC endpoint. Registers the v0 ALPN (perry-iroh/0) so this same endpoint can both dial peers and accept incoming connections from clients running this library.

// No options — fresh random identity, n0 relay/DNS only.
const ep = await iroh.bind();

// Stable identity across restarts.
const sk = process.env.IROH_SECRET ?? iroh.generateSecretKey();
// (persist `sk` somewhere, e.g. write it to .env)
const ep2 = await iroh.bind({ secretKey: sk });

// LAN discovery on top of the relay.
const ep3 = await iroh.bind({ secretKey: sk, mdns: true });

nodeId(endpoint)

function nodeId(endpoint: EndpointHandle): Promise<string>

Return the local node's stable identifier (a hex-encoded Ed25519 public key). Share this with peers so they can dial you. Awaits the endpoint coming online before reading the address — safe to call right after bind().

const myId = await iroh.nodeId(ep);
// e.g. "f49a76...c1b4e2"

connect(endpoint, nodeId)

function connect(endpoint: EndpointHandle, nodeId: string): Promise<ConnHandle>

Open an outgoing connection to a peer addressed by its node id (the value of the peer's nodeId(ep)). Resolves once the QUIC handshake completes. Uses the hardcoded v0 ALPN, so the peer must also be running @perryts/iroh.

const conn = await iroh.connect(ep, serverNodeId);

acceptOne(endpoint)

function acceptOne(endpoint: EndpointHandle): Promise<ConnHandle>

Wait for the next incoming peer connection on this endpoint and finish the handshake. Rejects if the endpoint is closed before a peer arrives. Each call yields one connection — for a server that handles many peers, call it in a loop.

const conn = await iroh.acceptOne(ep);

openBi(conn)

function openBi(conn: ConnHandle): Promise<BiStreamHandle>

Open a bi-directional stream from the local end of the connection. The peer must call acceptBi to pick it up.

const stream = await iroh.openBi(conn);

acceptBi(conn)

function acceptBi(conn: ConnHandle): Promise<BiStreamHandle>

Accept the next bi-directional stream the peer opens. Mirrors openBi.

const stream = await iroh.acceptBi(conn);

streamWrite(stream, data)

function streamWrite(stream: BiStreamHandle, data: string): Promise<void>

Write a UTF-8 string to the send half of the stream. v0 is text-only — encode binary as base64/hex if needed. Multiple writes are concatenated; the peer sees the bytes once you call streamFinish (or in chunks as they arrive over the wire, terminated by streamFinish).

await iroh.streamWrite(stream, "hello, peer!");

streamFinish(stream)

function streamFinish(stream: BiStreamHandle): Promise<void>

Close the send half of the stream. The peer's pending streamReadToEnd resolves once the in-flight bytes drain. Without this call, the peer's read would hang.

await iroh.streamFinish(stream);

streamReadToEnd(stream, maxBytes)

function streamReadToEnd(stream: BiStreamHandle, maxBytes: number): Promise<string>

Drain the recv half of the stream and resolve with the bytes as a UTF-8 string. Rejects if the peer's payload exceeds maxBytes (back-pressure cap to prevent unbounded buffering) or if the bytes aren't valid UTF-8.

const reply = await iroh.streamReadToEnd(stream, 65_536);

connClose(conn)

function connClose(conn: ConnHandle): Promise<void>

Close a peer connection with a clean QUIC shutdown frame and wait for the close to propagate. Idempotent — closing an already-dropped handle resolves successfully.

await iroh.connClose(conn);

close(endpoint)

function close(endpoint: EndpointHandle): Promise<void>

Close the endpoint gracefully. Drops any remaining handle state. Idempotent.

await iroh.close(ep);

v0.3.0 — bind options, deterministic keys, status snapshots

generateSecretKey() and secretKeyFromSeed(seed)

function generateSecretKey(): string;
function secretKeyFromSeed(seed: Uint8Array | Buffer): string;

Synchronous helpers for producing the secretKey you pass into bind. generateSecretKey returns a fresh random key as a 64-char hex string; secretKeyFromSeed takes exactly 32 bytes (e.g. a SHA-256 of a passphrase) and returns the deterministic key derived from those bytes — same seed in, same key out, same nodeId out. Returns "" on a malformed (wrong-length) seed.

import { createHash } from "node:crypto";

// Same passphrase → same nodeId every run, no env file needed.
const seed = createHash("sha256").update("my-app:dev-fixture").digest();
const sk = iroh.secretKeyFromSeed(seed);
const ep = await iroh.bind({ secretKey: sk });

nodeStatus(endpoint)

interface NodeStatus {
  nodeId: string;
  online: boolean;
  homeRelay: string;       // empty string if none yet
  directAddrs: string[];   // observed "ip:port" entries
}

function nodeStatus(endpoint: EndpointHandle): Promise<NodeStatus>;

One-shot snapshot of the endpoint's current network reachability — useful for healthchecks and human-readable status pages. Subscribing to changes would need a callback/event surface; that's a deferred followup tracked alongside the connection-event work in lib.rs.

const status = await iroh.nodeStatus(ep);
console.log(status);
// {
//   nodeId: "f49a76...c1b4e2",
//   online: true,
//   homeRelay: "https://use1-1.relay.iroh.network./",
//   directAddrs: ["192.168.1.42:51820", "[2601:...]:51820"]
// }

v0.2.0 — multi-peer + binary streams

endpointConnections(endpoint) and connNodeId(conn)

function endpointConnections(endpoint: EndpointHandle): ConnHandle[];
function connNodeId(conn: ConnHandle): string;

Synchronous accessors for fan-out / broadcast. endpointConnections returns every active peer connection handle that was registered via connect or acceptOne (and not yet closed via connClose). connNodeId looks up the remote node id without round-tripping a Promise.

// Server: broadcast a message to every connected client
const conns = iroh.endpointConnections(ep);
for (const c of conns) {
  console.log("sending to", iroh.connNodeId(c));
  const stream = await iroh.openBi(c);
  await iroh.streamWrite(stream, "broadcast: hello");
  await iroh.streamFinish(stream);
}

streamWriteBuffer(stream, buffer) and streamReadToEndBuffer(stream, maxBytes)

function streamWriteBuffer(stream: BiStreamHandle, buffer: Uint8Array | Buffer): Promise<void>;
function streamReadToEndBuffer(stream: BiStreamHandle, maxBytes: number): Promise<Uint8Array>;

Binary-safe variants of streamWrite / streamReadToEnd. Use these for file transfer, encrypted payloads, or anything that isn't valid UTF-8.

// Client: send a PNG over QUIC, peer reads it back as a Buffer
import { readFile } from 'node:fs/promises';

const png = await readFile('hero.png');
const stream = await iroh.openBi(conn);
await iroh.streamWriteBuffer(stream, png);
await iroh.streamFinish(stream);

// Peer side
const bytes = await iroh.streamReadToEndBuffer(serverStream, 16 * 1024 * 1024);
console.log("received", bytes.length, "bytes");

Types

Exported from the iroh module declaration in src/index.d.ts:

type EndpointHandle = number & { readonly __irohEndpoint: unique symbol };
type ConnHandle     = number & { readonly __irohConn:     unique symbol };
type BiStreamHandle = number & { readonly __irohStream:   unique symbol };

These are opaque branded numbers — you should never inspect or arithmetic on them. The brand prevents you from passing an EndpointHandle where a ConnHandle is expected.

ALPN

Hardcoded to "perry-iroh/0" for v0 — every server registers it at bind() time, every client connects with it. This means client and server must both be on @perryts/iroh (or another implementation that opts into the same ALPN). Per-call ALPN strings are a v1 followup.

Error handling

Every async function rejects with an Error whose message is prefixed by the operation, e.g. iroh connect: bad node id: invalid character. Common rejection reasons:

  • Invalid handle (iroh <op>: invalid <kind> handle) — you passed a handle that was never returned by this library, or one that was already consumed by close / connClose.
  • Bad node id (iroh connect: bad node id: ...) — the string passed to connect is not a parseable Iroh EndpointId.
  • Endpoint closed before peer connected (iroh acceptOne: ...) — close was called while acceptOne was pending.
  • Payload too large (iroh streamReadToEnd: ...) — the peer wrote more than maxBytes before calling streamFinish.
  • Non-UTF-8 payload (iroh streamReadToEnd: payload was not valid UTF-8: ...) — the peer wrote raw binary; v0 is text-only.

Status & roadmap

What's there:

  • bind(options?) / nodeId / close / nodeStatus (snapshot)
  • generateSecretKey / secretKeyFromSeed for stable identities
  • connect / acceptOne / connClose
  • endpointConnections / connNodeId for fan-out
  • openBi / acceptBi / streamWrite / streamFinish / streamReadToEnd
  • streamWriteBuffer / streamReadToEndBuffer (binary)
  • bind({ mdns: true }) for LAN peer discovery

Known gaps, tracked in PerryTS/perry:

  • Per-call ALPN strings (still hardcodes perry-iroh/0)
  • Streaming subscriptions to node-status / discovery events — nodeStatus returns a one-shot snapshot today; subscribing to changes needs a callback/event surface that's a separate followup (also blocks endpoint.on('connection', cb)-style)
  • Multiple bi-streams per connection in idiomatic JS (today: open one stream and use it)
  • Unidirectional streams + datagram surface

Versioning

Pre-1.0. The perry.nativeLibrary.abiVersion (currently 0.5) is a hard pin against Perry's perry-ffi ABI — bump it in lockstep with the Perry release that the bindings target.

License

MIT — see LICENSE.