Skip to content

dialback

@johnhenry/dialback is a library for creating a reverse proxy over WebSockets: an Agent dials out (through a NAT/firewall it’s behind), and the Server dials back through that same connection to reach it — the name is the mechanism, not just a label.

Request/Response <-HTTP-> [Server] <-WS-> [Agent]

Provenance: previously developed as leproxy (itself a rename of the original proxy-socks), but never actually published to npm under either name. Renamed and adopted into the @johnhenry scope, starting fresh at 0.0.0.

A Server instance is Fetch-shaped (server.fetch(request)), which is what makes it easy to sit behind another package in this family: @johnhenry/hostable can forward gateway traffic straight to an agent behind a firewall with <Upstream app={dialbackServer} />, no adapter needed — see hostable’s own API, “Ecosystem integration.”

Terminal window
npm install @johnhenry/dialback

Latest on npm: @johnhenry/[email protected].

Node only, no CDN build. The root imports node:http, node:crypto and ws's server. ./browsermesh imports node:events, so it loads raw only in Node; esm.sh's events polyfill gets it to import.

dialback 0.0.5 requires Node 26 or later ("engines": { "node": ">=26.0.0" }) and ships its own TypeScript declarations for Server and Agent. Its runtime dependencies are ws and @johnhenry/webwire.

// Server (Node.js)
import { Server } from "@johnhenry/dialback";
const server = new Server(() => new Response("no responder", { status: 500 }), {
secret: "shared-secret",
});
await server.listen(8082);

Server#listen(port) starts an HTTP server and a WebSocket server on the same port: plain HTTP requests go to server.fetch(), and WebSocket connections are registered as agents (and unregistered when they close). Converting Node’s req/res to and from Request/Response is done by @johnhenry/webwire, which dialback 0.0.5 depends on — so repeated response headers such as Set-Cookie reach the client as separate header lines instead of being collapsed to the last one. X-Forwarded-Host is read before Host, so the agent sees the host the original client asked for rather than the proxy’s own. If you want to run your own HTTP server instead, use server.fetch() and server.addConnection() directly, as in the Deno example.

// Agent
import { Agent } from "@johnhenry/dialback";
const { serve } = new Agent("ws://localhost:8082", { reconnect: 1000, secret: "shared-secret" });
serve(async (request, { id }) => {
return new Response("Hello there!", {
status: 200,
headers: { "content-type": "text/plain", etag: id },
});
});

See API for the full Server/Agent reference, including Deno usage and the authentication model, and the dialback/browsermesh transport for a real per-agent-identity alternative to the shared-secret model above.

  • API — Server, Agent, utility functions, and a note on authentication
  • The dialback/browsermesh transport — optional per-agent Ed25519 identity in place of a shared secret, the handshake, the @johnhenry/dialback/handshake subpath, and its honestly-documented limitations

Contributions are welcome — see the repo for details.

This project is licensed under the MIT License.

github.com/johnhenry/dialback