JS SDK

JS SDK

Use the rstream JavaScript SDKs for Control plane APIs, Engine APIs, tunnel runtimes, tokens, TURN, and watch streams.


The JavaScript SDKs cover several different integration layers. Use the API packages for platform and engine operations, the runtime package when a Node.js process needs to speak the tunnel protocol, and the browser/React packages when you are building an operator UI.

github.com/rstreamlabs/rstream-jsJavaScript and TypeScript packages for managed APIs, engine APIs, tunnel runtime, WebTTY, TURN, watch streams, and React.

This page focuses on the SDK surface. For raw HTTP routes, see APIs. For real-time event semantics, see Signaling.

Package map

PackageUse it for
@rstreamlabs/rstreamHosted Control plane API plus shared SDK contracts: auth tokens, resource boundaries, tunnel objects, TURN credentials, project objects, and helpers.
@rstreamlabs/tunnelsEngine HTTP API: live tunnel and client inventory, event watching, short-lived tunnel tokens, local TURN derivation.
@rstreamlabs/runtimeNode.js tunnel runtime: create bytestream tunnels, accept inbound streams, dial private bytestream tunnels, attach tunnels to Node HTTP servers.
@rstreamlabs/webttyBrowser-side WebTTY protocol client, command execution helper, and WebDAV filesystem sidecar client.
@rstreamlabs/reactReact hooks, providers, and the WebTTY terminal component built on the lower-level packages.

Install

Install only the packages your integration needs:

npm install @rstreamlabs/rstream @rstreamlabs/tunnels @rstreamlabs/runtime @rstreamlabs/webtty @rstreamlabs/react

Shared contracts

Shared schemas and TypeScript types are exported from @rstreamlabs/rstream. Use those exports when application code needs to validate or type payloads that are also consumed by @rstreamlabs/tunnels, @rstreamlabs/runtime, or @rstreamlabs/react.

import { authTokenResourcesSchema } from "@rstreamlabs/rstream/auth-token";
import { createAuthTokenResponseSchema } from "@rstreamlabs/rstream/auth-token";
import { createTurnCredentialsParamsSchema } from "@rstreamlabs/rstream/turn";
import { tunnelSchema } from "@rstreamlabs/rstream/tunnel";
import { turnCredentialsSchema } from "@rstreamlabs/rstream/turn";
import type { Tunnel } from "@rstreamlabs/rstream/tunnel";

@rstreamlabs/tunnels remains the Engine client package. It does not own the shared token, tunnel, TURN, project, or schema-helper contracts.

Control plane API

@rstreamlabs/rstream supports both bearer tokens and application credentials for Control plane API requests.

import { RstreamClient } from "@rstreamlabs/rstream";
 
const client = new RstreamClient({
  credentials: { token: process.env.RSTREAM_AUTHENTICATION_TOKEN! },
});
 
const whoami = await client.whoami();
const projects = await client.tunnels.projects.list();
const project =
  await client.tunnels.projects.resolveByEndpoint("project-endpoint");

Application credentials work as well:

import { RstreamClient } from "@rstreamlabs/rstream";
 
const client = new RstreamClient({
  credentials: {
    clientId: process.env.RSTREAM_CLIENT_ID!,
    clientSecret: process.env.RSTREAM_CLIENT_SECRET!,
  },
});
 
const whoami = await client.whoami();
const projects = await client.tunnels.projects.list();

Managed TURN issuance also lives on the Control plane API:

import { RstreamClient } from "@rstreamlabs/rstream";
 
const control = new RstreamClient({
  credentials: { token: process.env.RSTREAM_AUTHENTICATION_TOKEN! },
});
 
const turn = await control.tunnels.projects.createTurnCredentialsByEndpoint(
  "project-endpoint",
  { ttlSeconds: 600 },
);

Managed and locally derived TURN credentials are short-lived. ttlSeconds is optional and must be between 1 and 3600 seconds; the TURN runtime rejects usernames whose remaining lifetime is greater than one hour.

Engine API helpers

@rstreamlabs/tunnels talks to the Engine HTTP API. Use it when you need operational state, watch streams, token minting, or self-hosted engine access without opening the tunnel transport yourself.

When the engine address is already known, connect directly with @rstreamlabs/tunnels.

import { RstreamTunnelsClient } from "@rstreamlabs/tunnels";
 
const client = new RstreamTunnelsClient({
  credentials: { token: process.env.RSTREAM_AUTHENTICATION_TOKEN! },
  engine: process.env.RSTREAM_ENGINE!,
});
 
const tunnels = await client.tunnels.list();
const clients = await client.clients.list();

This is the usual path for self-hosted deployments where no Control plane API is present.

For hosted projects, the SDK can resolve a managed project and mint short-lived engine tokens from application credentials.

import { RstreamTunnelsClient } from "@rstreamlabs/tunnels";
 
const client = new RstreamTunnelsClient({
  credentials: {
    clientId: process.env.RSTREAM_CLIENT_ID!,
    clientSecret: process.env.RSTREAM_CLIENT_SECRET!,
  },
  projectEndpoint: "project-endpoint",
});
 
const engine = await client.getEngine();
const tunnels = await client.tunnels.list();

Short-lived auth tokens are useful when a backend needs to delegate a narrow capability to another service, a browser-facing bootstrap endpoint, or a device.

import { RstreamTunnelsClient } from "@rstreamlabs/tunnels";
 
const admin = new RstreamTunnelsClient({
  credentials: {
    clientId: process.env.RSTREAM_CLIENT_ID!,
    clientSecret: process.env.RSTREAM_CLIENT_SECRET!,
  },
  projectEndpoint: "project-endpoint",
});
 
const { token } = await admin.auth.createAuthToken({
  expires_in: 60,
  resources: {
    tunnels: {
      scopes: {
        tunnels: {
          list: true,
        },
      },
    },
  },
});

Fine-grained resource boundaries can scope discovery, creation, and connection permissions down to tunnel properties and request paths. Public payloads always use resources.tunnels. If the client is configured with projectEndpoint or projectId, scope-only token minting and scope-only resource leaves are project-scoped by default. Pass projectScoped: false only when you intentionally need a global scope-only boundary.

import { RstreamTunnelsClient } from "@rstreamlabs/tunnels";
 
const admin = new RstreamTunnelsClient({
  credentials: {
    clientId: process.env.RSTREAM_CLIENT_ID!,
    clientSecret: process.env.RSTREAM_CLIENT_SECRET!,
  },
  projectEndpoint: "project-endpoint",
});
 
const { token } = await admin.auth.createAuthToken({
  expires_in: 60,
  resources: {
    tunnels: {
      scopes: {
        tunnels: {
          create: {
            filters: {
              protocol: { oneof: ["http"] },
              labels: {
                env: "prod",
              },
            },
          },
          connect: {
            params: {
              path: { regex: "^/api" },
            },
          },
          list: {
            select: {
              id: true,
              name: true,
              protocol: true,
            },
          },
        },
      },
    },
  },
});

See Fine-Grained Tokens for the full resource-boundary model and explicit AND / OR operators.

The watch helper connects to the engine over SSE or WebSocket and validates incoming events.

import { Watch } from "@rstreamlabs/tunnels";
 
const watch = new Watch(
  {
    auth: process.env.RSTREAM_AUTHENTICATION_TOKEN!,
    engine: process.env.RSTREAM_ENGINE!,
    transport: "sse",
  },
  {
    onEvent: (event) => {
      console.log(event.type);
    },
  },
);
 
await watch.connect();

When the watch stream runs in a browser, pass an auth function instead of a cached token string. The function should call your backend and return a fresh short-lived watch token for each connection attempt.

The returned token is sent as rstream.token on /api/sse or /api/websocket. The engine accepts query-string authentication only on those two streaming endpoints. The query token must be an auth or app token, not a pat; it must be unexpired; the engine requires its remaining TTL to be no more than one hour plus clock-skew allowance; and every token-level resources.tunnels branch must allow tunnel listing without create or connect scopes. The JavaScript Watch helper applies the stricter client-side rule that exp - iat is at most 3600 seconds. In practice, mint the token with tunnels.resources.read-only and a resources.tunnels.scopes.tunnels.list boundary scoped to the project or filters the browser should see.

async function createWatchToken(issuerToken: string) {
  const response = await fetch("/api/tokens", {
    method: "POST",
    credentials: "omit",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${issuerToken}`,
    },
    body: JSON.stringify({
      permissions: ["tunnels.resources.read-only"],
      resources: {
        tunnels: {
          scopes: { tunnels: { list: true } },
        },
      },
    }),
    cache: "no-store",
  });
  if (!response.ok) {
    throw new Error("Failed to create watch token.");
  }
  const { token } = await response.json();
  return token;
}
 
declare const issuerToken: string;
 
const watch = new Watch(
  {
    auth: () => createWatchToken(issuerToken),
    engine: "engine.example.com",
    transport: "websocket",
  },
  { onEvent: console.log },
);

The same package can derive TURN credentials locally from application credentials:

import { createTURNCredentials } from "@rstreamlabs/tunnels";
 
const turn = await createTURNCredentials({
  credentials: {
    clientId: process.env.RSTREAM_CLIENT_ID!,
    clientSecret: process.env.RSTREAM_CLIENT_SECRET!,
  },
  projectEndpoint: "project-endpoint",
  clusterDomain: "cluster.example.rstream.test",
  ttlSeconds: 600,
});

See STUN and TURN for the full TURN model.

Node.js tunnel runtime

@rstreamlabs/runtime is Node-only. It opens TLS/TCP connections to the engine using the rstream tunnel protocol and exposes bytestream tunnels as Node streams. Use this package when the JavaScript process is the agent that creates or serves tunnels.

For a complete Next.js App Router example that serves a custom Node HTTP server through this runtime, see Expose a Local Next.js App with rstream.

import http from "node:http";
import os from "node:os";
import { Client } from "@rstreamlabs/runtime";
 
const ctrl = await Client.fromEnv().connect();
const tunnel = await ctrl.createTunnel({
  protocol: "http",
  httpVersion: "http/1.1",
  publish: true,
});
 
const server = http.createServer((_req, res) => {
  res.writeHead(200, { "content-type": "text/plain" });
  res.end(os.hostname());
});
 
console.log("Server accessible at:", await tunnel.forwardingAddress());
await tunnel.serve(server);

The runtime uses the same configuration model as the Go and C++ SDKs. You can use RSTREAM_CONFIG and RSTREAM_CONTEXT, set RSTREAM_ENGINE and RSTREAM_AUTHENTICATION_TOKEN, configure mTLS with RSTREAM_MTLS_CERT_FILE and RSTREAM_MTLS_KEY_FILE, or pass all values directly in code:

import { Client } from "@rstreamlabs/runtime";
 
const client = new Client({
  engine: "engine.example.com:443",
  token: process.env.RSTREAM_AUTHENTICATION_TOKEN,
});

The JavaScript runtime supports token contexts and file-based mTLS contexts. It parses the shared credential storage schema, but macOS Keychain and PKCS#11 storage are not implemented in JavaScript and fail explicitly during configuration resolution.

For the TLS control-channel transport, the runtime supports the shared transport.proxy.http YAML shape and opens an HTTP CONNECT tunnel to the engine before starting TLS. HTTPS proxy verification can use transport.proxy.tls.caFile, transport.proxy.tls.serverName, and transport.proxy.tls.insecureSkipVerify when the proxy uses private PKI or needs an explicit verification name. transport.proxy.fromEnvironment: true makes the runtime read HTTPS_PROXY, HTTP_PROXY, ALL_PROXY, and NO_PROXY from the process environment. It does not read desktop proxy preference stores directly. SOCKS5 proxying and QUIC transport are not implemented in the JavaScript runtime; selected contexts that request transport.proxy.socks5 or transport.useQuic fail during configuration resolution.

Agent authentication and published tunnel authentication are separate. RSTREAM_MTLS_CERT_FILE and RSTREAM_MTLS_KEY_FILE authenticate the Node.js runtime when it opens the engine control channel. mtlsAuth: true protects public traffic entering a published tunnel URL and requires downstream clients to present a registered client certificate:

const tunnel = await ctrl.createTunnel({
  protocol: "http",
  httpVersion: "http/1.1",
  publish: true,
  mtlsAuth: true,
});

The runtime supports bytestream tunnels. Datagram tunnels, QUIC transport, HTTP/3, WebTransport, browser runtimes, and custom DNS resolver settings are not supported by the JavaScript runtime package.

Use Client.dial() to connect to a private bytestream tunnel by name or ID. The returned object is a regular Node Duplex stream.

import { Client } from "@rstreamlabs/runtime";
 
const stream = await Client.fromEnv().dial("private-api");
 
stream.write("ping\n");
stream.on("data", (chunk) => {
  process.stdout.write(chunk);
});

WebSocket works over an HTTP/1.1 bytestream tunnel because serve() passes accepted tunnel streams through Node's normal HTTP server connection path. Attach an upgrade handler or a WebSocket library to the same http.Server.

import http from "node:http";
import { Client } from "@rstreamlabs/runtime";
import { WebSocketServer } from "ws";
 
const ctrl = await Client.fromEnv().connect();
const tunnel = await ctrl.createTunnel({
  protocol: "http",
  httpVersion: "http/1.1",
  publish: true,
});
 
const server = http.createServer();
const wss = new WebSocketServer({ noServer: true });
 
server.on("upgrade", (req, socket, head) => {
  wss.handleUpgrade(req, socket, head, (ws) => {
    ws.send("connected through rstream");
  });
});
 
console.log(await tunnel.forwardingAddress());
await tunnel.serve(server);

WebTTY client

@rstreamlabs/webtty is the browser-side client for the rstream WebTTY protocol. It connects to a WebTTY WebSocket endpoint, opens a remote execution session, streams stdout/stderr, writes stdin, and sends terminal resize events.

import { WebTTY } from "@rstreamlabs/webtty";
 
const webtty = new WebTTY(
  {
    url: "wss://<published-host>?rstream.token=<token>",
  },
  {
    cmdArgs: ["uname", "-a"],
    interactive: false,
  },
  {
    onStdout: (chunk) => {
      console.log(new TextDecoder().decode(chunk));
    },
    onComplete: (exitCode) => {
      console.log("completed", exitCode);
    },
    onError: (message) => {
      console.error(message);
    },
  },
);
 
webtty.connect();

For non-interactive agent workflows, use runWebTTYCommand when you want a collected result, and openWebTTYCommand when you need streaming output or stdin control:

import { parseWebTTYServers } from "@rstreamlabs/tunnels";
import { runWebTTYCommand } from "@rstreamlabs/webtty";
 
const [server] = parseWebTTYServers(tunnels);
 
if (server?.exec_path) {
  const result = await runWebTTYCommand(
    { url: "wss://<published-host>?rstream.token=<token>" },
    "sh",
    ["-lc", "docker ps --format '{{.Names}}'"],
    {
      execPath: server.exec_path,
      timeoutMs: 30_000,
    },
  );
 
  console.log(result.exitCode, result.stdout, result.stderr);
}

When a WebTTY server advertises the fs capability, use the filesystem sidecar path discovered from inventory instead of assuming /fs:

import { parseWebTTYServers } from "@rstreamlabs/tunnels";
import { WebTTYFileSystem } from "@rstreamlabs/webtty";
 
const [server] = parseWebTTYServers(tunnels);
 
if (server?.capabilities.includes("fs") && server.fs_path) {
  const fs = new WebTTYFileSystem({
    fsPath: server.fs_path,
    url: "https://<published-host>?rstream.token=<token>",
  });
 
  const files = await fs.list("/");
  const readme = await fs.readText("/README.md");
}

The filesystem sidecar is a WebDAV boundary rooted by the server --fs-root; it is not a sandbox.

For operational setup and CLI workflows, see WebTTY and Access Remote Machines with rstream WebTTY.

React helpers

@rstreamlabs/react provides client-side helpers for rstream UIs. The useRstream hook and RstreamProvider wrap the watch model from @rstreamlabs/tunnels; WebTTYTerminal binds @rstreamlabs/webtty to an xterm.js terminal.

Use the hook directly in small components:

import { useRstream } from "@rstreamlabs/react/hooks";
 
export function TunnelInventory({
  createWatchToken,
}: {
  createWatchToken: () => Promise<string>;
}) {
  const { state, error, tunnels } = useRstream({
    auth: createWatchToken,
    engine: "engine.example.com",
    transport: "sse",
  });
 
  if (error) return <p>{error.message}</p>;
  return (
    <ul data-state={state}>
      {tunnels.map((tunnel) => (
        <li key={tunnel.id}>{tunnel.name ?? tunnel.id}</li>
      ))}
    </ul>
  );
}

Use the provider when several views share the same stream:

import { RstreamProvider } from "@rstreamlabs/react/providers";
import { useRstreamContext } from "@rstreamlabs/react/providers";
 
function TunnelsTable() {
  const { tunnels } = useRstreamContext();
  return <pre>{JSON.stringify(tunnels, null, 2)}</pre>;
}
 
export function Dashboard({
  createWatchToken,
}: {
  createWatchToken: () => Promise<string>;
}) {
  return (
    <RstreamProvider
      options={{ auth: createWatchToken, engine: "engine.example.com" }}
    >
      <TunnelsTable />
    </RstreamProvider>
  );
}

Embed a WebTTY terminal when a backend has issued a scoped WebTTY token:

import { WebTTYTerminal } from "@rstreamlabs/react/components";
 
export function Terminal({
  execPath,
  token,
}: {
  execPath?: string;
  token: string;
}) {
  return (
    <WebTTYTerminal
      execPath={execPath}
      url={`wss://<published-host>?rstream.token=${encodeURIComponent(token)}`}
      terminalOptions={{ cursorBlink: true }}
    />
  );
}

Environment variables

The Node.js tunnel runtime follows the same configuration model as the Go and C++ SDKs. When values are not provided explicitly, Client.fromEnv() reads:

VariablePurpose
RSTREAM_CONFIGPath to the rstream configuration file. If unset, the runtime falls back to ~/.rstream/config.yaml.
RSTREAM_CONTEXTName of the context to select during config-based resolution.
RSTREAM_ENGINEEngine endpoint used by Engine API helpers and by the Node.js tunnel runtime.
RSTREAM_AUTHENTICATION_TOKENAuthentication token used by API helpers and by token-authenticated runtime connections.
RSTREAM_MTLS_CERT_FILEClient certificate file for mTLS agent authentication.
RSTREAM_MTLS_KEY_FILEClient private key file for mTLS agent authentication.
RSTREAM_QUIC_TRANSPORTShared runtime transport flag. QUIC transport is not supported by the JavaScript runtime package, so RSTREAM_QUIC_TRANSPORT=1 fails during configuration resolution.
HTTPS_PROXY / HTTP_PROXY / ALL_PROXY / NO_PROXYProcess proxy variables read when the selected config sets transport.proxy.fromEnvironment: true.

For backend integrations based on application credentials, prefer passing clientId and clientSecret explicitly rather than relying on ambient process state. Browser-side React and WebTTY code should receive short-lived tokens from a backend endpoint, not long-lived account or application credentials.

Examples

The repository includes Control plane API examples through @rstreamlabs/rstream, Engine API examples through @rstreamlabs/tunnels, tunnel runtime examples through @rstreamlabs/runtime, WebTTY helpers through @rstreamlabs/webtty, and React hooks/components through @rstreamlabs/react.