# 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 ```ts 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`: ```ts 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. ```ts 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: ```ts 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 ```ts interface Client { profile: { list(): Promise } tunnel: { list(): Promise get(options?: { profile?: string }): Promise pending(options?): Promise<{ id: string; hostname: string } | undefined> create(options?: { profile?: string; onProgress?(stage): void }): Promise resume(options?): Promise ensure(options?: { profile?: string }): Promise remove(options?: { profile?: string }): Promise connect(options: { profile?: string; routes: Routes; signal?: AbortSignal }): Promise } dispose(): Promise } interface Connection { tunnel: Identity events: AsyncIterable status(): Status setRoutes(routes: Routes): Promise closed: Promise close(): Promise } ``` Events are `connecting`, `connected`, `disconnected`, `reconnecting`, `connection-opened`, `connection-closed`, and `stopped`. ## Storage `create()` stores identities under `$XDG_DATA_HOME/opentunnel//`, the same files the CLI uses. Pass a store to isolate or own persistence: ```ts 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.