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

@opentunnel/client

v0.5.1

Published

OpenTunnel SDK for Bun: expose local services through a blind TLS tunnel

Readme

OpenTunnel Client

@opentunnel/client is a TypeScript SDK that creates tunnels and forwards them to local services from inside your process. It ships compiled JavaScript with type declarations and runs on Bun, Node, and Deno. effect is a peer dependency, so an app that already uses Effect shares one copy with the SDK. TLS terminates in your process, so the relay never sees plaintext or your private key.

It implements the same protocol and on-disk layout as the Rust client and CLI (see docs/protocol.md), so a tunnel created by the CLI can be used here and vice versa.

Quick start

import { create } from "@opentunnel/client"

const client = create()
const connection = await client.tunnel.connect({
  routes: { api: "127.0.0.1:3000" },
})
console.log(`https://api.${connection.tunnel.hostname}`)

for await (const event of connection.events) console.log(event)

connect creates the profile's tunnel if it has none, resolves once the bridge first attaches, and reconnects with backoff until you call close(). It rejects on fatal errors such as an invalid token.

The Effect interface exposes the same capabilities, with scoped connections and events as a Stream:

import { OpenTunnelClient } from "@opentunnel/client/effect"

Routes

Routes map a name to a host:port target. A name is a subdomain label, or @ for the tunnel hostname itself. Path routing is not supported.

await connection.setRoutes({ api: "127.0.0.1:4000", "@": "127.0.0.1:8080" })

Changing only targets applies to new connections immediately. Adding or removing names re-attaches the bridge.

A route can also be an object with options:

await connection.setRoutes({
  web: "127.0.0.1:3000",
  api: { target: "127.0.0.1:4000", proxyProtocol: "v2" },
})

proxyProtocol ("v1" or "v2") writes a PROXY protocol header with the visitor's address and port, and (v2) the requested hostname, before each connection's data, then forwards the data unchanged. Enable it only for a target that expects the header, listening on loopback. Unknown keys and values are rejected. Routes are not stored; they are the ones you pass to connect and setRoutes.

A target receives the decrypted bytes unchanged from a loopback connection, so it cannot tell a tunneled request from a local one by peer address or Host. The visitor's address is the peer field of each connection-opened event, and is delivered to the target only with proxyProtocol. See docs/trust-boundary.md in the repository for what a target can trust.

API

interface Client {
  profile: { list(): Promise<string[]> }
  tunnel: {
    list(): Promise<StoredTunnel[]>
    get(options?: { profile?: string }): Promise<Identity | undefined>
    pending(options?): Promise<{ id: string; hostname: string } | undefined>
    create(options?: { profile?: string; onProgress?(stage): void }): Promise<Identity>
    resume(options?): Promise<Identity | undefined>
    ensure(options?: { profile?: string }): Promise<Identity>
    remove(options?: { profile?: string }): Promise<void>
    connect(options: { profile?: string; routes: Routes; signal?: AbortSignal }): Promise<Connection>
  }
  dispose(): Promise<void>
}

interface Connection {
  tunnel: Identity
  events: AsyncIterable<ClientEvent>
  status(): Status
  setRoutes(routes: Routes): Promise<void>
  closed: Promise<void>
  close(): Promise<void>
}

Events are connecting, connected, disconnected, reconnecting, connection-opened, connection-closed, and stopped.

Storage

create() stores identities under $XDG_DATA_HOME/opentunnel/<profile>/, the same files the CLI uses. Pass a store to isolate or own persistence:

import { create, OpenTunnelStorage } from "@opentunnel/client"

const client = create({ store: OpenTunnelStorage.memory() })

A memory store loses the tunnel's token when the process exits, and tunnels do not expire, so a tunnel it created can no longer be deleted. Call client.tunnel.remove() before exiting, or use the default store for tunnels that should outlive the process.

Backpressure

The SDK stops reading from a local socket while more than 1 MiB is queued on the bridge WebSocket, and resets a connection whose local side stops reading for long enough to buffer 8 MiB. The protocol has no per-connection flow control yet, so one slow public reader can delay others on the same bridge.