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

@midwess/unb

v2.0.3

Published

Target-routed duplex messaging for TypeScript on Node.js and browsers

Readme

unb

Target-routed duplex messaging for TypeScript. Calls use canonical /node/subject paths. Node.js uses the native Rust node and server binding. Browsers use the same full Rust Node and NodeBuilder surface inside a dedicated WebAssembly worker. Rust owns framing, sessions, routing, endpoint fallback, cancellation, discovery, and AI projection; TypeScript adapts application values and platform transports.

Install

npm install @midwess/unb

The package is ESM-only and requires Node.js 20 or newer. Version 2.0.3 supports native Node.js execution on Apple silicon macOS (darwin-arm64) and worker-backed execution in browsers with WebAssembly and module-worker support. Browser execution does not require cross-origin isolation. CommonJS, Bun, Deno, Linux native addons, Intel macOS native addons, and Windows native addons are unsupported.

Use explicit entry points in application code:

import { Node } from '@midwess/unb/node'
import { BrowserClient, Node as BrowserNode } from '@midwess/unb/browser'

The package root uses conditional exports for bundlers that select node or browser conditions.

Node.js Quick Start

import {
  Endpoint,
  EndpointSet,
  Node,
  type StandardSchemaV1,
} from '@midwess/unb/node'

type WeatherInput = { city: string }
type WeatherOutput = { city: string; tempC: number }

const weatherInput: StandardSchemaV1.Schema<WeatherInput> = {
  '~standard': {
    version: 1,
    vendor: 'weather-example',
    validate(value) {
      if (typeof value === 'object' && value !== null && typeof (value as WeatherInput).city === 'string') return { value: value as WeatherInput }
      return { issues: [{ message: 'city must be a string' }] }
    },
  },
}

const weatherOutput: StandardSchemaV1.Schema<WeatherOutput> = {
  '~standard': {
    version: 1,
    vendor: 'weather-example',
    validate(value) { return { value: value as WeatherOutput } },
  },
}

const service = Node.builder('weather-service')
  .insecureAcceptDeclaredPeerIdentities()
  .unary('weather.now', {
    input: weatherInput,
    output: weatherOutput,
    discovery: {
      input: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
      output: { type: 'object', properties: { city: { type: 'string' }, tempC: { type: 'number' } } },
    },
  }, request => ({ city: request.payload.city, tempC: 21 }))
  .build()

const caller = Node.builder('caller').insecureAcceptDeclaredPeerIdentities().build()
const hosting = await service.host({ address: '127.0.0.1:0', websocket: {} }).start()

try {
  if (!hosting.tcpAddress) throw new Error('WebSocket hosting did not start')
  const connection = await caller.connect(new EndpointSet([Endpoint.websocket(`ws://${hosting.tcpAddress}`)]))
  console.log(connection.peer, connection.status())
  const reply = await caller.fetchJson<WeatherOutput>('/weather-service/weather.now', { body: { city: 'Hanoi' } })
  console.log(reply.body)
} finally {
  hosting.close()
  caller.close()
  service.close()
}

For in-process nodes, use await caller.link(service) instead of creating a network endpoint. See the repository examples/ directory for request/response, linking, catalogs, errors, multi-hop routing, runtime mutation, AI tools, WebTransport fallback, node-to-node relay, and raw byte payload examples.

Peer connection lifecycle

Node-to-node connect() remains eager: it verifies the peer and completes the initial route synchronization before returning a shared PeerConnection. After unintentional transport loss, the shared handle publishes connecting and owns one automatic maintenance supervisor. It immediately performs an ordered endpoint sweep, then retries indefinitely with capped jittered backoff.

const connection = await caller.connect(endpoints)
console.log(connection.status())

// Optional observation only; callers never poll or trigger recovery.
await connection.changed()

// Terminal for this logical connection and all of its clones.
connection.disconnect()

New operations that need a recovering route may wait only within their original deadline and resolve the authoritative route again when woken. Operations already admitted to the failed session fail with PEER_UNREACHABLE and are not replayed. A later deliberate connect() creates a new logical handle; it never revives the disconnected clones. Unix parent links use this same maintenance path.

Peer admission

Every node must select an explicit peer-admission policy before build(). Node.js and browser builders expose the same asynchronous peerLayer(fn) contract. The callback receives the local and remote declared identities, including their opaque proof, and a consume-once downstream continuation:

import { Node, type PeerLayerFn } from '@midwess/unb/browser'

const verifyPeer: PeerLayerFn = async (_local, remote, next) => {
  if (remote.proof !== 'expected-application-proof') throw new Error('peer proof rejected')
  await next()
}

const node = Node.builder('browser-shell')
  .identityProof('expected-shell-proof')
  .peerLayer(verifyPeer)
  .build()

await next() marks the declared identity verified and runs the remaining layers in registration order. A throw, rejected promise, return without next(), repeated next(), or downstream rejection fails closed. Node, connection, or session retirement drains UNB's pending admission wait; a user JavaScript promise may continue running, but its late completion cannot revive the retired session. UNB's existing five-second establishment timeout remains authoritative and no browser-only timeout is added.

insecureAcceptDeclaredPeerIdentities() is an explicit development escape hatch that trusts a self-declared identity without validating its proof. It is not a replacement for a production peer layer.

Hosting And TLS

node.host(options) takes a declarative options object. Transports are opt-in: a transport you do not configure is disabled, and a host with no transport fails at start. There is no boolean named tls — WebSocket TLS and WebTransport TLS are configured separately.

Reverse-proxy termination (recommended production default for HTTP/WebSocket — unb loads no certificates):

const hosting = await node.host({
  address: '127.0.0.1:8080',
  websocket: {},
}).start()

Direct HTTPS/WSS termination with file-based PEM credentials — the same TLS listener serves HTTPS POST ingress and WSS upgrades over HTTP/1.1:

const hosting = await node.host({
  address: '0.0.0.0:443',
  websocket: {
    tls: { certFile: '/run/secrets/fullchain.pem', keyFile: '/run/secrets/privkey.pem' },
  },
  webtransport: {
    identity: { certFile: '/run/secrets/fullchain.pem', keyFile: '/run/secrets/privkey.pem' },
  },
}).start()

Development WebTransport requires explicit intent; the certificate hash is exposed only for this path and can be handed to Endpoint.webtransport(url, hosting.developmentCertHash):

const hosting = await node.host({
  address: '127.0.0.1:0',
  websocket: {},
  webtransport: { developmentSelfSigned: ['localhost'] },
}).start()

Credentials are loaded and validated when host() is called, before any socket binds. Certificate reload is deployment-owned: for TCP TLS supply a Rust-side rustls config with a dynamic certificate resolver (Rust API escape hatch); for WebTransport, replace the hosting (restart the QUIC endpoint) when the certificate rotates. Production WebTransport always requires a real identity — there is no implicit self-signed certificate.

Handlers And Middleware

NodeBuilder.unary() infers the validated input and checked output types. NodeBuilder.streaming() accepts an async iterable and validates each event.

const node = Node.builder('clock')
  .insecureAcceptDeclaredPeerIdentities()
  .layer(async (request, next) => next.run({
    context: { ...request.context, traced: true },
  }))
  .streaming('clock.ticks', {
    input: weatherInput,
    event: weatherOutput,
  }, async function* (request) {
    yield { city: request.payload.city, tempC: 21 }
    yield { city: request.payload.city, tempC: 22 }
  })
  .build()

Middleware runs in registration order. It may continue with next.run(), replace the local payload or context passed to later middleware and handlers, or reject by throwing a structured error.

Handlers can also be added at runtime with node.unary() and node.streaming(), then removed with removeSubject() or removeOperation().

Requests And Subscriptions

fetch and subscribe are the raw primitives: request bodies, response bodies, and stream events are Uint8Array, with status and headers passed through untouched. fetchJson<T> and subscribeJson<T> are the explicit JSON convenience APIs — they encode the request body, decode the reply, and optionally validate it with a Standard Schema passed as schema.

const stream = await node.subscribeJson<WeatherOutput>('/clock/clock/ticks', { body: { city: 'Hanoi' } })
try {
  for await (const event of stream) {
    console.log(event)
    break
  }
} finally {
  await stream.return()
}

EventStream is an async iterator. Normal producer completion ends iteration. return(), close(), and async disposal propagate cancellation to Rust. Always close a stream when leaving before normal completion.

Worker-backed browser Nodes also accept timeoutMs on fetch, fetchHead, and fetchJson. The value must be a positive 32-bit integer and replaces the default 30-second call deadline for that request; it does not change connection-establishment or subscription deadlines.

Raw Bytes

Raw handlers opt in with raw: true and exchange Uint8Array payloads without JSON or schema involvement:

const files = Node.builder('files')
  .insecureAcceptDeclaredPeerIdentities()
  .unary('files.blob', { raw: true }, request => request.payload)
  .streaming('files.chunks', { raw: true }, async function* (request) { yield request.payload })
  .build()

const response = await files.fetch('/files/files/blob', { body: new Uint8Array([0, 255, 128]) })
console.log(response.status, response.body instanceof Uint8Array)

An empty body decodes to null under fetchJson/subscribeJson; a non-empty body that is not valid JSON throws JsonError, which carries the raw bytes and the parse cause so callers can fall back to byte handling.

Discovery And Schemas

Standard Schema validators and discovery JSON Schema have separate purposes:

  • input, output, and event Standard Schema values validate runtime application data.
  • discovery.input, discovery.output, discovery.event, and discovery.error describe contracts to remote tools and catalogs.
  • Runtime validators are never converted into JSON Schema automatically.

Node applications can inspect localCatalog(false) for the local index, localCatalog(true) for complete local contracts, catalog() for reachable catalog data, and discoverTree() for the reachable node graph. Browser clients use discover() and discoverTree().

Errors

All package errors extend UnbError:

  • ProtocolError exposes a stable SCREAMING_SNAKE_CASE code, such as UNKNOWN_SUBJECT, INVALID_INPUT, BUSY, or CANCELLED.
  • ValidationError exposes Standard Schema issues.
  • JsonError reports a non-JSON body under fetchJson/subscribeJson and carries the raw bytes.
  • DialError reports endpoint selection and connection failures.
  • BindingError reports native or WASI binding failures.
  • AiError exposes AI operation codes such as unknown_tool or invalid_input.
  • UnsupportedPlatformError identifies a capability unavailable on the current runtime.

Raw fetch resolves protocol error frames as responses: the mapped status, the exact code in the unb-code header, and the error body as bytes. fetchJson throws ProtocolError instead.

import { decodeJsonBody, ProtocolError } from '@midwess/unb/node'

const response = await node.fetch('/weather/weather.missing')
if (response.status >= 400) console.error(response.headers['unb-code'], decodeJsonBody(response.body))

try {
  await node.fetchJson('/weather/weather.missing')
} catch (error) {
  if (error instanceof ProtocolError) console.error(error.code, error.message)
  else throw error
}

Use error classes and codes as machine-readable behavior. Human-oriented message suggestions are not a stable data format.

Browser Client

import { BrowserClient, Endpoint, EndpointSet } from '@midwess/unb/browser'

const endpoints = new EndpointSet([
  Endpoint.webtransport('https://api.example.com/wire', 'CERT_HASH_HEX'),
  Endpoint.websocket('wss://api.example.com/wire'),
])

const client = await BrowserClient.connect(endpoints, { peer: 'api' })
try {
  const reply = await client.fetchJson('/api/weather.now', {
    body: { city: 'Hanoi' },
    headers: { trace: 'browser-example' },
  })
  console.log(reply.body)

  const stream = await client.subscribeJson('/api/clock.ticks')
  try {
    console.log(await stream.next())
  } finally {
    await stream.return()
  }

  await client.discover('/api', { detail: 'index', scope: 'reachable' })
  await client.discoverTree({ root: 'api', maxDepth: 4 })
} finally {
  await client.close()
}

The endpoint list expresses preference and fallback. Rust prefers WebTransport when supported, then tries WebSocket after connection failure or timeout. The public API does not expose the selected transport, so application code should not infer it from successful connection alone.

Browsers support Node construction, linking, target-routed requests, subscriptions, discovery, dynamic unary and streaming handlers, and peer-admission layers. Listener hosting, request-continuation layer() middleware, state registration, and native/provider capabilities remain unsupported and throw UnsupportedPlatformError where exposed.

Browser Deployment

The browser runtime requires workers only. It does not require SharedArrayBuffer, atomics, WebAssembly threads, or crossOriginIsolated, so no Cross-Origin-Opener-Policy or Cross-Origin-Embedder-Policy header is needed on the host document. The runtime therefore loads in third-party iframes, alongside scripts served without Cross-Origin-Resource-Policy, and in embeds.

Cross-origin WASM, worker, and transport resources must still permit loading through suitable CORS headers. The package emits its WASM, loader, and worker under dist/browser-runtime/ and resolves them through package-relative URLs. Bundlers and static servers must preserve and serve those relationships.

The worker executes trusted package-native WebAssembly. Treat the package, worker, WASM, loader, and their hosting origin as trusted executable dependencies, and apply dependency-integrity and Content Security Policy controls.

AI Projection

discoverTree(), await listTools(), and callTool() expose Rust-backed discovery and tool projection. listTools reads the subject's full discovery contract: a described unary subject yields a schema-bearing tool (complete: true), and callTool validates input against that schema before anything is sent — a validation failure returns an invalid_input error carrying the schema so the caller can correct itself. A subject without a usable contract keeps the generic request fallback with complete: false; inspect ToolList.warnings rather than assuming every subject is schema-described.

Cleanup

Close resources deterministically:

  • await stream.return() or await stream.close() for subscriptions.
  • await client.close() for BrowserClient.
  • hosting.close() for listeners.
  • node.close() for native nodes.

Finalizers are best-effort safeguards, not the primary lifecycle strategy. Use try/finally around owned resources.

Runtime Matrix

| Capability | Node.js 20+ on macOS arm64 | Qualified browser | |---|---:|---:| | Conditional package root | Native binding | Browser binding through bundler | | Requests, subscriptions, discovery | Yes | Yes | | WebSocket and WebTransport dialing | Yes | Yes, with capability detection and fallback | | Build and link a node | Yes | Yes | | Listener hosting | Yes | No | | JavaScript unary and streaming handlers | Yes | Yes | | Peer-admission peerLayer() | Yes | Yes | | Request-continuation layer() | Yes | No | | AI tree and tool projection | Limited facade | Client discovery only |

There is no JavaScript protocol fallback and no public raw native-binding export.