Onion Routing with TorJS

tor-js lets you make HTTP requests through the Tor network from JavaScript. Not through a proxy. Not through a server you control. A real Tor client — circuit construction, relay encryption, directory consensus — running entirely in your browser or Node.js process.

The core is Arti, the Tor Project's official Rust implementation, compiled to WebAssembly. The TypeScript wrapper provides a clean fetch()-like API on top.

Quick Start

import { TorClient } from 'tor-js';

const client = new TorClient({
  gateway: 'https://tor-js-gateway.example.com',
});

const response = await client.fetch('https://check.torproject.org/api/ip');
console.log(await response.json()); // { IsTor: true, IP: "..." }

Architecture

Here's how the pieces fit together when running in a browser:

Your Application client.fetch("https://example.com")
TypeScript Wrapper TorClient • ArtiSocketProvider • Log • Storage
WASM (Arti) Tor circuits • directory • TLS • crypto
ArtiSocketProvider Browser: WebRTC → Gateway → Guard    Node.js: TCP → Guard
Tor Circuit Guard → Middle → Exit → example.com
3 layers of onion encryption — each relay peels one

The Layers

1. Arti → WebAssembly

Arti is ~200k lines of Rust across dozens of crates. Compiling it to WASM required replacing every platform assumption: std::time::Instant (no monotonic clock in browsers), async_trait with Send bounds (WASM is single-threaded), SQLite (no filesystem), native TLS (no OpenSSL). Two new crates handle the cross-platform abstractions: tor-time for time and tor-async-compat for conditional Send bounds.

2. WasmRuntime

Arti is runtime-agnostic — it works with Tokio, async-std, or anything implementing its Runtime trait. WasmRuntime implements this trait for browsers: setTimeout for sleep, wasm-bindgen-futures for task spawning, and a JS callback for opening socket connections. TLS uses rustls with rustls-rustcrypto (pure Rust, no C dependencies) — the same TLS library, just with a WASM-compatible crypto backend.

3. ArtiSocketProvider

Arti needs TCP connections to Tor relays. Browsers can't open raw TCP sockets. ArtiSocketProvider bridges this gap with a strategy pattern:

StrategyTransportEnvironment
directNative TCP (node:net / Deno.connect)Node.js, Deno
webrtcMultiplexed data channels via gatewayBrowsers with WebRTC
websocketOne WebSocket per relay via gatewayAny browser

In Node.js and Deno, no gateway is needed — connections go directly to Tor relays. In browsers, a gateway server proxies the bytes between WebSocket/WebRTC and the real TCP relay. The gateway is a byte pipe; all Tor protocol — encryption, circuit management, cell formatting — happens in the browser.

4. Storage

Tor clients need to persist directory state (consensus documents, microdescriptors, guard lists) to avoid re-downloading everything on each page load. The KeyValueStore trait provides a single interface that splits into state and directory storage internally. In browsers it's backed by IndexedDB, in Node.js by the filesystem. A preloading cache (CachedJsStorage) loads everything into a HashMap at startup so Arti's synchronous storage reads don't block the main thread.

5. Fast Bootstrap

A cold Tor bootstrap normally downloads consensus + microdescriptors + authority certs from relays through Tor circuits — slow when you don't have circuits yet. Fast bootstrap short-circuits this: the gateway server pre-packages these documents into a single bootstrap.zip.br file. The client fetches it over plain HTTPS (not through Tor), seeds storage, and then Arti validates the cached data normally.

The speed comes from brotli compression, handled transparently by the browser's Content-Encoding support. The full bundle compresses from ~40 MB down to ~3 MB, cutting first-connect time from minutes to seconds.

Trust model: The fast bootstrap bundle is fetched from the gateway, but this doesn't require trusting the gateway. Arti verifies every document using cryptographic signatures from Tor's directory authorities. A malicious gateway can't inject fake relays or forge directory data.

What Happens When You Call fetch()

Step 1: Bootstrap

On new TorClient(), Arti downloads (or loads from cache) a network consensus — the list of all ~10,000 Tor relays and their capabilities, signed by the directory authorities. It selects guard relays, builds circuits, and signals readiness.

Step 2: Circuit Construction

Arti builds a 3-hop circuit: Guard → Middle → Exit. Each hop involves an ntor handshake — a one-round-trip authenticated key exchange based on Curve25519. The client combines its ephemeral key with the relay's long-term identity key to derive shared secret material, proving the relay's identity and establishing forward-secret session keys.

The result is layered encryption — the onion in "onion routing." The client encrypts data three times: first with the exit relay's key, then the middle's, then the guard's. Each relay peels off one layer and forwards to the next. No single relay ever knows both who you are and what you're accessing.

Step 3: TLS & HTTP

tor-js builds a raw HTTP request, wraps it in a TLS connection to the destination (through the exit relay), and streams it through the circuit. The response flows back through the same circuit, and the client returns a standard Response object.

Note on TLS: There are two TLS layers. The inner TLS protects your HTTP request end-to-end (browser ↔ destination, using WebPKI certificates). The outer TLS protects the Tor protocol between relays (using Tor's own certificate scheme). Both use rustls.

The Gateway

The gateway server is built with Arti (native, not WASM) and serves two roles:

  1. Relay proxying: Accept a WebSocket or WebRTC connection, open a TCP connection to the target Tor relay, and pipe bytes bidirectionally. The gateway tracks the Tor consensus and only allows connections to addresses listed as Tor relays — preventing abuse as a general-purpose proxy.
  2. Fast bootstrap: Serve a pre-built bootstrap.zip.br with brotli Content-Encoding, so browsers decompress it natively.

The gateway never sees your traffic. All data is encrypted under Tor's layered encryption before it reaches the gateway. And the bootstrap data is verified by Arti using directory authority signatures.

In Node.js, no gateway is needed at all. ArtiSocketProvider uses the direct strategy and connects to Tor relays without any intermediary.

Bundle Size

ComponentSize (gzip)Notes
WASM binary~1.7 MBAll of Arti + rustls + crypto
JS wrapper~15 kBTypeScript compiled to ESM
CDN entry point~15 kBFetches WASM on first use
Base64 entry point~2.3 MBWASM embedded in JS (single file)

The CDN entry point (import from 'tor-js') is recommended for production. The WASM binary's SHA-256 hash is baked into the wrapper at build time — the CDN cannot serve a tampered binary.

Try It

# From Node.js:
npm install tor-js

# tor-fetch.js
import { TorClient } from 'tor-js';

const client = new TorClient();
const res = await client.fetch('https://check.torproject.org/api/ip');
console.log(await res.json());
client.close();

Source: github.com/voltrevo/artiGateway: github.com/voltrevo/tor-fast-bootstrap