TS SDK docs (#6840)
* First sweep packages + some minor tweaking * Second sweep * Regenerate lockfile + package.json mods * Regenerate lockfile again * Fix CI * Fix CI again * All building properly * unblock * Tweak examples * Comments + readme + fix rotten unit test * First pass docs * Big pass * Massive pass on new docs * Update integrations.md w mobile * Partial overhaul review * new playground + big pass * new fix lychee err * IPR notice tweak
This commit is contained in:
@@ -83,5 +83,6 @@ test-tutorials/
|
||||
# pnpm
|
||||
.pnpm-store/
|
||||
|
||||
tmp/
|
||||
# operator tools
|
||||
scripts/nym-node-setup/auto-bond/nodes.csv
|
||||
@@ -310,6 +310,24 @@ const sdks = [
|
||||
"fetch()-compatible API that routes HTTP(S) requests through the Mixnet. Browsers and Node.js.",
|
||||
href: "/developers/mix-fetch",
|
||||
},
|
||||
{
|
||||
name: "mix-tunnel",
|
||||
description:
|
||||
"Owns the shared Mixnet tunnel that mix-fetch, mix-dns, and mix-websocket ride on. One IPR connection and userspace TCP/IP stack for all three.",
|
||||
href: "/developers/mix-tunnel",
|
||||
},
|
||||
{
|
||||
name: "mix-dns",
|
||||
description:
|
||||
"Resolves hostnames to IPs through the Mixnet. UDP DNS via an IPR exit, no TCP or TLS.",
|
||||
href: "/developers/mix-dns",
|
||||
},
|
||||
{
|
||||
name: "mix-websocket",
|
||||
description:
|
||||
"WebSocket-compatible class for ws and wss traffic routed through the Mixnet via an IPR exit.",
|
||||
href: "/developers/mix-websocket",
|
||||
},
|
||||
];
|
||||
|
||||
export const LandingPage = () => {
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Button from "@mui/material/Button";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import { mixFetch, createMixFetch } from "@nymproject/mix-fetch-full-fat";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import type { SetupMixFetchOps } from "@nymproject/mix-fetch-full-fat";
|
||||
|
||||
const defaultUrl =
|
||||
"https://nymtech.net/.wellknown/network-requester/exit-policy.txt";
|
||||
const args = { mode: "unsafe-ignore-cors" };
|
||||
const mixFetchOptions: SetupMixFetchOps = {
|
||||
clientId: "docs-mixfetch-demo", // explicit ID
|
||||
preferredGateway: "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1",
|
||||
mixFetchOverride: {
|
||||
requestTimeoutMs: 60_000,
|
||||
},
|
||||
forceTls: true, // force WSS
|
||||
};
|
||||
|
||||
// Log entry type for the visible log panel
|
||||
type LogLevel = "info" | "error" | "send" | "receive";
|
||||
type LogEntry = { timestamp: string; message: string; level: LogLevel };
|
||||
|
||||
// Color map for log levels
|
||||
const logColors: Record<LogLevel, string> = {
|
||||
info: "gray",
|
||||
error: "red",
|
||||
send: "blue",
|
||||
receive: "green",
|
||||
};
|
||||
|
||||
// Label map for log levels
|
||||
const logLabels: Record<LogLevel, string> = {
|
||||
info: "INFO",
|
||||
error: "ERROR",
|
||||
send: "SEND",
|
||||
receive: "RECV",
|
||||
};
|
||||
|
||||
export const MixFetch = () => {
|
||||
// MixFetch initialization state
|
||||
const [status, setStatus] = useState<"idle" | "starting" | "ready" | "error">(
|
||||
"idle"
|
||||
);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
|
||||
// Log panel state
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
|
||||
// Single fetch state
|
||||
const [url, setUrl] = useState<string>(defaultUrl);
|
||||
const [html, setHtml] = useState<string>();
|
||||
const [busy, setBusy] = useState<boolean>(false);
|
||||
|
||||
// Concurrent fetch state
|
||||
const [concurrentResults, setConcurrentResults] = useState<string[]>([]);
|
||||
const [concurrentBusy, setConcurrentBusy] = useState<boolean>(false);
|
||||
|
||||
// Auto-scroll within the log panel when new entries are added (without scrolling the page)
|
||||
const logContainerRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (logContainerRef.current) {
|
||||
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
|
||||
}
|
||||
}, [logs]);
|
||||
|
||||
// Helper to add a timestamped log entry
|
||||
const addLog = (message: string, level: LogLevel) => {
|
||||
const timestamp = new Date().toISOString().substring(11, 23); // HH:MM:SS.mmm
|
||||
setLogs((prev) => [...prev, { timestamp, message, level }]);
|
||||
};
|
||||
|
||||
// Initialize MixFetch explicitly via createMixFetch
|
||||
const handleStart = async () => {
|
||||
try {
|
||||
setStatus("starting");
|
||||
setErrorMsg(null);
|
||||
addLog("Starting MixFetch...", "info");
|
||||
await createMixFetch(mixFetchOptions);
|
||||
setStatus("ready");
|
||||
addLog("MixFetch is ready!", "info");
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
setStatus("error");
|
||||
setErrorMsg(msg);
|
||||
addLog(`Error: ${msg}`, "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Single URL fetch — mixFetch reuses the existing singleton
|
||||
const handleFetch = async () => {
|
||||
try {
|
||||
setBusy(true);
|
||||
setHtml(undefined);
|
||||
addLog(`Sending request to ${url}...`, "send");
|
||||
const response = await mixFetch(url, args, mixFetchOptions);
|
||||
const resHtml = await response.text();
|
||||
setHtml(resHtml);
|
||||
addLog(`Response received (${resHtml.length} bytes)`, "receive");
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
addLog(`Fetch error: ${msg}`, "error");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Send 5 concurrent requests to different URLs on the same domain
|
||||
const handleConcurrentFetch = async () => {
|
||||
const baseUrl = "https://jsonplaceholder.typicode.com/posts/";
|
||||
const count = 5;
|
||||
try {
|
||||
setConcurrentBusy(true);
|
||||
setConcurrentResults([]);
|
||||
addLog(
|
||||
`Starting ${count} concurrent requests to ${baseUrl}1-${count}...`,
|
||||
"send"
|
||||
);
|
||||
// Fire off all requests concurrently using Promise.all
|
||||
const requests = Array.from({ length: count }, (_, i) => {
|
||||
const targetUrl = `${baseUrl}${i + 1}`;
|
||||
return mixFetch(targetUrl, args, mixFetchOptions)
|
||||
.then((res) => res.json())
|
||||
.then((json: { id: number; title: string }) => {
|
||||
const entry = `[${json.id}] ${json.title}`;
|
||||
addLog(entry, "receive");
|
||||
return entry;
|
||||
});
|
||||
});
|
||||
const results = await Promise.all(requests);
|
||||
setConcurrentResults(results);
|
||||
addLog(`All ${count} concurrent requests completed!`, "info");
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
addLog(`Concurrent fetch error: ${msg}`, "error");
|
||||
} finally {
|
||||
setConcurrentBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Are fetch controls enabled?
|
||||
const isReady = status === "ready";
|
||||
|
||||
// Status text + color for the startup indicator
|
||||
const statusText: Record<typeof status, string> = {
|
||||
idle: "Not started",
|
||||
starting: "Starting...",
|
||||
ready: "Ready",
|
||||
error: `Error: ${errorMsg}`,
|
||||
};
|
||||
const statusColor: Record<typeof status, string> = {
|
||||
idle: "#9e9e9e",
|
||||
starting: "orange",
|
||||
ready: "#85E89D",
|
||||
error: "#ff6b6b",
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Stack spacing={3}>
|
||||
{/* --- Start MixFetch Section --- */}
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={status === "starting" || status === "ready"}
|
||||
onClick={handleStart}
|
||||
>
|
||||
Start MixFetch
|
||||
</Button>
|
||||
{status === "starting" && <CircularProgress size={20} />}
|
||||
<Typography
|
||||
fontFamily="monospace"
|
||||
fontSize="small"
|
||||
sx={{ color: statusColor[status] }}
|
||||
>
|
||||
{statusText[status]}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{/* --- Fetch Controls (disabled until ready) --- */}
|
||||
<Box
|
||||
sx={{
|
||||
opacity: isReady ? 1 : 0.5,
|
||||
pointerEvents: isReady ? "auto" : "none",
|
||||
}}
|
||||
>
|
||||
{/* Single fetch */}
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TextField
|
||||
disabled={busy}
|
||||
fullWidth
|
||||
label="URL"
|
||||
type="text"
|
||||
variant="outlined"
|
||||
defaultValue={defaultUrl}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
size="small"
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={busy}
|
||||
onClick={handleFetch}
|
||||
>
|
||||
Fetch
|
||||
</Button>
|
||||
</Stack>
|
||||
{busy && (
|
||||
<Box mt={2}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{html && (
|
||||
<>
|
||||
<Box mt={2}>
|
||||
<strong>Response</strong>
|
||||
</Box>
|
||||
<Paper sx={{ p: 2, mt: 1 }} elevation={4}>
|
||||
<Typography fontFamily="monospace" fontSize="small">
|
||||
{html}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Concurrent fetch demo */}
|
||||
<Box mt={3}>
|
||||
<strong>Concurrent Requests</strong>
|
||||
<Box mt={1}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={concurrentBusy}
|
||||
onClick={handleConcurrentFetch}
|
||||
>
|
||||
Send 5 Concurrent Requests (posts/1-5)
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
{concurrentBusy && (
|
||||
<Box mt={2}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{concurrentResults.length > 0 && (
|
||||
<Paper sx={{ p: 2, mt: 2 }} elevation={4}>
|
||||
{concurrentResults.map((result, i) => (
|
||||
<Typography key={i} fontFamily="monospace" fontSize="small">
|
||||
{result}
|
||||
</Typography>
|
||||
))}
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* --- Log Panel --- */}
|
||||
{logs.length > 0 && (
|
||||
<Paper
|
||||
ref={logContainerRef}
|
||||
sx={{ p: 2, mt: 2, maxHeight: 200, overflow: "auto" }}
|
||||
>
|
||||
<strong>Log</strong>
|
||||
{logs.map((entry, i) => (
|
||||
<Typography
|
||||
key={i}
|
||||
fontFamily="monospace"
|
||||
fontSize="small"
|
||||
sx={{ color: logColors[entry.level] }}
|
||||
>
|
||||
{entry.timestamp} [{logLabels[entry.level]}] {entry.message}
|
||||
</Typography>
|
||||
))}
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"nodes": 652,
|
||||
"nodes": 659,
|
||||
"locations": 75,
|
||||
"mixnodes": 239,
|
||||
"exit_gateways": 405
|
||||
"mixnodes": 238,
|
||||
"exit_gateways": 413
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
Monday, June 8th 2026, 11:52:06 UTC
|
||||
Tuesday, June 9th 2026, 08:23:52 UTC
|
||||
|
||||
@@ -0,0 +1,749 @@
|
||||
// Single interactive playground for the mix-* TypeScript SDK, modelled on
|
||||
// wasm/smolmix/internal-dev but driving the published @nymproject/mix-* packages
|
||||
// and trimmed/adapted for a docs audience. One shared tunnel, several sections
|
||||
// (DNS, GET, WebSocket, stress, download), each with a verbose timeline log and
|
||||
// and, where it teaches something, a tunnel-vs-clearnet comparison.
|
||||
//
|
||||
// The package import is dynamic (see ./lib.ts loadModules) so the multi-MB wasm
|
||||
// loads only when the visitor clicks Setup, not on page render.
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
loadModules,
|
||||
randomClientId,
|
||||
clampSurbs,
|
||||
formatSize,
|
||||
formatRate,
|
||||
hexPreview,
|
||||
sha256hex,
|
||||
saveFile,
|
||||
dohResolve,
|
||||
generateRequests,
|
||||
type PlaygroundMods,
|
||||
type MixWebSocketLike,
|
||||
type SetupOpts,
|
||||
} from './lib';
|
||||
import {
|
||||
useLogs,
|
||||
LogPanel,
|
||||
StatusText,
|
||||
Spinner,
|
||||
Button,
|
||||
box,
|
||||
row,
|
||||
input,
|
||||
num,
|
||||
legend,
|
||||
sub,
|
||||
type Status,
|
||||
} from './ui';
|
||||
|
||||
const VERIFY_TEXT_URL = 'https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-demo.txt';
|
||||
|
||||
// Default IPR exit for the playground. Users can switch to auto-discovery
|
||||
// with the "Use random IPR" toggle.
|
||||
const DEFAULT_IPR =
|
||||
'6B6iuWX4bQP4GVA4Yq7XmZencaaGw6BaPY6xJWYSwsbF.6g6LRx1fgU2Q2A4ZPKonYHtfBARh1GPMe1LtXk6vpRR8@q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1';
|
||||
|
||||
function eqBytes(a: Uint8Array, b: Uint8Array): boolean {
|
||||
if (a.byteLength !== b.byteLength) return false;
|
||||
for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function MixPlayground() {
|
||||
const { log, lines } = useLogs();
|
||||
const [mods, setMods] = useState<PlaygroundMods | null>(null);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [busy, setBusy] = useState(false); // setup/disconnect in flight
|
||||
const [tunnelStatus, setTunnelStatus] = useState<Status>({ text: 'Not started', colour: 'gray' });
|
||||
|
||||
// Connection form.
|
||||
const [useRandomIpr, setUseRandomIpr] = useState(false);
|
||||
const [iprAddress, setIprAddress] = useState(DEFAULT_IPR);
|
||||
const [clientId, setClientId] = useState('');
|
||||
const [forceTls, setForceTls] = useState(true);
|
||||
const [disablePoisson, setDisablePoisson] = useState(false);
|
||||
const [disableCover, setDisableCover] = useState(false);
|
||||
const [openSurbs, setOpenSurbs] = useState(10); // matches SurbsConfig::default (ipr.rs)
|
||||
const [dataSurbs, setDataSurbs] = useState(2); // matches SurbsConfig::default (ipr.rs)
|
||||
const [primaryDns, setPrimaryDns] = useState('');
|
||||
const [fallbackDns, setFallbackDns] = useState('');
|
||||
const [debug, setDebug] = useState(true);
|
||||
|
||||
// Section inputs.
|
||||
const [dnsHost, setDnsHost] = useState('example.com');
|
||||
const [getUrl, setGetUrl] = useState('https://httpbin.org/get');
|
||||
const [wsUrl, setWsUrl] = useState('wss://echo.websocket.org');
|
||||
const [wsMessage, setWsMessage] = useState('Hello from the mixnet!');
|
||||
const [wsStatus, setWsStatus] = useState<Status>({ text: 'Not connected', colour: 'gray' });
|
||||
const [wsConnected, setWsConnected] = useState(false);
|
||||
const [burstCount, setBurstCount] = useState(10);
|
||||
const [burstMin, setBurstMin] = useState(64);
|
||||
const [burstMax, setBurstMax] = useState(1024);
|
||||
const [burstBusy, setBurstBusy] = useState(false);
|
||||
const [stressCount, setStressCount] = useState(10);
|
||||
const [stressMode, setStressMode] = useState<'uniform' | 'mixed' | 'drip'>('mixed');
|
||||
const [stressUrl, setStressUrl] = useState('https://jsonplaceholder.typicode.com/posts/');
|
||||
const [stressTimeout, setStressTimeout] = useState(60);
|
||||
const [stressBusy, setStressBusy] = useState(false);
|
||||
const [stressStatus, setStressStatus] = useState<Status>({ text: '' });
|
||||
const [downloadUrl, setDownloadUrl] = useState(
|
||||
'https://web.cs.ucdavis.edu/~rogaway/papers/moral-fn.pdf',
|
||||
);
|
||||
const [textBusy, setTextBusy] = useState(false);
|
||||
const [textStatus, setTextStatus] = useState<Status>({ text: '' });
|
||||
const [textOutput, setTextOutput] = useState<string | null>(null);
|
||||
const [pdfBusy, setPdfBusy] = useState(false);
|
||||
const [pdfStatus, setPdfStatus] = useState<Status>({ text: '' });
|
||||
const [pdfInfo, setPdfInfo] = useState<{ size: number; hash: string } | null>(null);
|
||||
const [filePreview, setFilePreview] = useState<{ url: string; isImage: boolean } | null>(null);
|
||||
const [bothStatus, setBothStatus] = useState<Status>({ text: '' });
|
||||
|
||||
const wsRef = useRef<MixWebSocketLike | null>(null);
|
||||
const wsSendQueue = useRef<number[]>([]);
|
||||
const burstRef = useRef<{
|
||||
payloads: Uint8Array[];
|
||||
received: number;
|
||||
verified: number;
|
||||
mismatches: number;
|
||||
rtts: number[];
|
||||
expected: number;
|
||||
resolve: () => void;
|
||||
} | null>(null);
|
||||
const cachedPdf = useRef<ArrayBuffer | null>(null);
|
||||
|
||||
// Generate the client id after mount (not at render) to keep SSG and client
|
||||
// hydration in agreement; see randomClientId in ./lib.
|
||||
useEffect(() => {
|
||||
setClientId((c) => c || randomClientId());
|
||||
}, []);
|
||||
|
||||
// Revoke the previous object URL when the download changes or on unmount.
|
||||
useEffect(() => () => { if (filePreview) URL.revokeObjectURL(filePreview.url); }, [filePreview]);
|
||||
|
||||
// Connection -------------------------------------------------------------
|
||||
|
||||
async function setup() {
|
||||
setBusy(true);
|
||||
const cid = clientId || randomClientId();
|
||||
if (cid !== clientId) setClientId(cid);
|
||||
setTunnelStatus({ text: 'Loading wasm...', colour: 'orange' });
|
||||
let m = mods;
|
||||
try {
|
||||
if (!m) {
|
||||
const t0 = performance.now();
|
||||
m = await loadModules();
|
||||
setMods(m);
|
||||
log('master', `Modules loaded (${(performance.now() - t0).toFixed(0)} ms)`);
|
||||
}
|
||||
} catch (e) {
|
||||
setTunnelStatus({ text: 'Failed to load wasm', colour: 'red' });
|
||||
log('master', `module load failed: ${e}`, 'red');
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!useRandomIpr && !iprAddress.trim()) {
|
||||
setTunnelStatus({ text: "IPR address required (or check 'random')", colour: 'red' });
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const opts: SetupOpts = {
|
||||
...(useRandomIpr ? {} : { preferredIpr: iprAddress.trim() }),
|
||||
clientId: cid,
|
||||
forceTls,
|
||||
disablePoissonTraffic: disablePoisson,
|
||||
disableCoverTraffic: disableCover,
|
||||
openReplySurbs: clampSurbs(openSurbs),
|
||||
dataReplySurbs: clampSurbs(dataSurbs),
|
||||
primaryDns: primaryDns.trim() || undefined,
|
||||
fallbackDns: fallbackDns.trim() || undefined,
|
||||
debug,
|
||||
};
|
||||
log(
|
||||
'master',
|
||||
`setupMixTunnel (clientId=${cid}, IPR: ${
|
||||
useRandomIpr ? 'auto-discover' : iprAddress.trim().slice(0, 28) + '...'
|
||||
})`,
|
||||
);
|
||||
setTunnelStatus({ text: 'Connecting to mixnet...', colour: 'orange' });
|
||||
// The gateway/IPR/smoltcp detail is printed by the Rust client straight to
|
||||
// the worker's console; it can't be forwarded to this panel. Point the user
|
||||
// there rather than silently dropping it.
|
||||
log(
|
||||
'master',
|
||||
debug
|
||||
? 'Connecting... (gateway, IPR discovery and smoltcp logs are in the browser console)'
|
||||
: 'Connecting... (tick "Verbose transport logs" for the gateway/IPR detail in the console)',
|
||||
'gray',
|
||||
);
|
||||
|
||||
try {
|
||||
const t0 = performance.now();
|
||||
const st = await m.getTunnelState();
|
||||
if (st.state === 'ready') {
|
||||
log('master', 'Tunnel already up; reusing it.', 'green');
|
||||
} else {
|
||||
await m.setupMixTunnel(opts);
|
||||
log('master', `setupMixTunnel OK: tunnel ready in ${((performance.now() - t0) / 1000).toFixed(1)}s`, 'green');
|
||||
}
|
||||
const final = await m.getTunnelState();
|
||||
log('master', `tunnel state: ${final.state}${final.reason ? ` (${final.reason})` : ''}`);
|
||||
setConnected(true);
|
||||
setTunnelStatus({ text: 'Connected', colour: 'green' });
|
||||
} catch (e) {
|
||||
const msg = String(e);
|
||||
if (/already initialised/.test(msg)) {
|
||||
setTunnelStatus({ text: 'Tunnel spent; reload the page to reconnect', colour: 'red' });
|
||||
log('master', 'tunnel already initialised but not ready; reload the page', 'red');
|
||||
} else {
|
||||
setTunnelStatus({ text: `Failed: ${msg}`, colour: 'red' });
|
||||
log('master', `setupMixTunnel failed: ${msg}`, 'red');
|
||||
}
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
if (!mods) return;
|
||||
setBusy(true);
|
||||
log('master', 'Disconnecting...');
|
||||
try {
|
||||
await mods.disconnectMixTunnel();
|
||||
log('master', 'Disconnected. Reload the page to reconnect (the wasm tunnel is one-shot).', 'green');
|
||||
setConnected(false);
|
||||
setTunnelStatus({ text: 'Disconnected', colour: 'gray' });
|
||||
} catch (e) {
|
||||
log('master', `disconnect failed: ${e}`, 'red');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
// DNS --------------------------------------------------------------------
|
||||
|
||||
async function dnsTunnel() {
|
||||
if (!mods) return;
|
||||
const h = dnsHost.trim();
|
||||
if (!h) return log('dns', 'Hostname is required', 'red');
|
||||
log('dns', `tunnel resolve ${h}`);
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const ip = await mods.mixDNS(h);
|
||||
log('dns', `tunnel ${h} => ${ip} (${(performance.now() - t0).toFixed(0)} ms)`, 'green');
|
||||
} catch (e) {
|
||||
log('dns', `tunnel resolve failed: ${e}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
async function dnsClearnet() {
|
||||
const h = dnsHost.trim();
|
||||
if (!h) return log('dns', 'Hostname is required', 'red');
|
||||
log('dns', `clearnet DoH resolve ${h}`);
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const ip = await dohResolve(h);
|
||||
log(
|
||||
'dns',
|
||||
`clearnet ${h} => ${ip} (${(performance.now() - t0).toFixed(0)} ms); visible in DevTools Network`,
|
||||
'green',
|
||||
);
|
||||
} catch (e) {
|
||||
log('dns', `clearnet DoH failed: ${e}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
// GET --------------------------------------------------------------------
|
||||
|
||||
async function getTunnel() {
|
||||
if (!mods) return;
|
||||
const u = getUrl.trim();
|
||||
if (!u) return log('get', 'URL is required', 'red');
|
||||
log('get', `tunnel GET ${u}`);
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const resp = await mods.mixFetch(u, {});
|
||||
log('get', `tunnel ${resp.status} ${resp.statusText} (${(performance.now() - t0).toFixed(0)} ms)`, 'green');
|
||||
} catch (e) {
|
||||
log('get', `tunnel GET failed: ${e}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
async function getClearnet() {
|
||||
const u = getUrl.trim();
|
||||
if (!u) return log('get', 'URL is required', 'red');
|
||||
log('get', `clearnet GET ${u}`);
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const resp = await window.fetch(u, { mode: 'cors' });
|
||||
log(
|
||||
'get',
|
||||
`clearnet ${resp.status} ${resp.statusText} (${(performance.now() - t0).toFixed(0)} ms); visible in DevTools Network`,
|
||||
'green',
|
||||
);
|
||||
} catch (e) {
|
||||
log('get', `clearnet fetch failed: ${e}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket --------------------------------------------------------------
|
||||
|
||||
const onWsMessage = useCallback(
|
||||
(ev: Event) => {
|
||||
const e = ev as MessageEvent;
|
||||
let rtt: number | null = null;
|
||||
if (wsSendQueue.current.length) rtt = performance.now() - (wsSendQueue.current.shift() as number);
|
||||
|
||||
const b = burstRef.current;
|
||||
if (b) {
|
||||
if (rtt != null) b.rtts.push(rtt);
|
||||
const recvBuf = new Uint8Array(e.data as ArrayBuffer);
|
||||
const sent = b.payloads[b.received];
|
||||
if (sent && eqBytes(recvBuf, sent)) b.verified++;
|
||||
else b.mismatches++;
|
||||
b.received++;
|
||||
if (b.received >= b.expected) b.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = e.data;
|
||||
let preview: string;
|
||||
if (typeof data === 'string') preview = data.length <= 200 ? data : data.slice(0, 200) + '...';
|
||||
else if (data instanceof ArrayBuffer) preview = `[binary ${data.byteLength} bytes] ${hexPreview(data)}`;
|
||||
else preview = `[${typeof data}]`;
|
||||
log('ws', rtt != null ? `recv (${rtt.toFixed(0)} ms RTT): ${preview}` : `recv: ${preview}`, 'green');
|
||||
},
|
||||
[log],
|
||||
);
|
||||
|
||||
async function wsConnect() {
|
||||
if (!mods) return;
|
||||
const url = wsUrl.trim();
|
||||
if (!url) return log('ws', 'WebSocket URL is required', 'red');
|
||||
if (wsRef.current && wsRef.current.readyState !== 3) await wsRef.current.close().catch(() => {});
|
||||
|
||||
setWsStatus({ text: 'Connecting...', colour: 'orange' });
|
||||
wsSendQueue.current = [];
|
||||
log('ws', `connecting to ${url}`);
|
||||
const t0 = performance.now();
|
||||
|
||||
const ws = new mods.MixWebSocket(url);
|
||||
ws.addEventListener('message', onWsMessage);
|
||||
ws.addEventListener('close', (ev) => {
|
||||
const e = ev as CloseEvent;
|
||||
log('ws', `closed: ${e.code} ${e.reason || ''}${e.wasClean ? '' : ' (unclean)'}`, 'orange');
|
||||
setWsStatus({ text: 'Closed', colour: 'gray' });
|
||||
setWsConnected(false);
|
||||
wsRef.current = null;
|
||||
});
|
||||
ws.addEventListener('error', () => {
|
||||
log('ws', 'error', 'red');
|
||||
setWsStatus({ text: 'Error', colour: 'red' });
|
||||
});
|
||||
|
||||
try {
|
||||
await ws.opened();
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
log('ws', `connected in ${ms} ms (protocols=${ws.protocols.join(',') || 'none'})`, 'green');
|
||||
setWsStatus({ text: `Connected (${ms} ms)`, colour: 'green' });
|
||||
setWsConnected(true);
|
||||
wsRef.current = ws;
|
||||
} catch (e) {
|
||||
log('ws', `connect failed: ${e}`, 'red');
|
||||
setWsStatus({ text: 'Error', colour: 'red' });
|
||||
}
|
||||
}
|
||||
|
||||
async function wsSend() {
|
||||
const ws = wsRef.current;
|
||||
if (!ws || ws.readyState !== 1) return;
|
||||
wsSendQueue.current.push(performance.now());
|
||||
await ws.send(wsMessage);
|
||||
log('ws', `send: ${wsMessage}`);
|
||||
}
|
||||
|
||||
async function wsClose() {
|
||||
const ws = wsRef.current;
|
||||
if (!ws) return;
|
||||
log('ws', 'closing...');
|
||||
await ws.close(1000, 'user requested');
|
||||
}
|
||||
|
||||
async function wsBurst() {
|
||||
const ws = wsRef.current;
|
||||
if (!ws || ws.readyState !== 1) return;
|
||||
if (burstCount < 1 || burstCount > 500) return log('ws', 'burst count must be 1-500', 'red');
|
||||
if (burstMin < 1 || burstMax < burstMin) return log('ws', 'invalid size range', 'red');
|
||||
|
||||
const payloads: Uint8Array[] = [];
|
||||
let totalBytes = 0;
|
||||
for (let i = 0; i < burstCount; i++) {
|
||||
const size = burstMin === burstMax ? burstMin : burstMin + Math.floor(Math.random() * (burstMax - burstMin + 1));
|
||||
const buf = new Uint8Array(size);
|
||||
crypto.getRandomValues(buf);
|
||||
payloads.push(buf);
|
||||
totalBytes += size;
|
||||
}
|
||||
log('ws', `echo burst: ${burstCount} msgs, ${formatSize(burstMin)}-${formatSize(burstMax)} (${formatSize(totalBytes)} total)`);
|
||||
setBurstBusy(true);
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
burstRef.current = { payloads, received: 0, verified: 0, mismatches: 0, rtts: [], expected: burstCount, resolve };
|
||||
});
|
||||
const t0 = performance.now();
|
||||
for (let i = 0; i < burstCount; i++) {
|
||||
wsSendQueue.current.push(performance.now());
|
||||
ws.send(payloads[i]); // fire in order; Comlink preserves FIFO to the worker
|
||||
}
|
||||
await done;
|
||||
const totalMs = performance.now() - t0;
|
||||
const b = burstRef.current!;
|
||||
burstRef.current = null;
|
||||
|
||||
const rtts = b.rtts.slice().sort((a, c) => a - c);
|
||||
const pick = (q: number) => (rtts.length ? rtts[Math.min(rtts.length - 1, Math.floor(rtts.length * q))].toFixed(0) : 'n/a');
|
||||
const avg = rtts.length ? (rtts.reduce((a, c) => a + c, 0) / rtts.length).toFixed(0) : 'n/a';
|
||||
const msgPerSec = (burstCount / (totalMs / 1000)).toFixed(1);
|
||||
|
||||
log('ws', `burst done: ${burstCount} msgs in ${(totalMs / 1000).toFixed(2)}s (${msgPerSec} msg/s, ${formatRate(totalBytes, totalMs)})`, 'green');
|
||||
log('ws', `verify: ${b.verified}/${burstCount} OK${b.mismatches ? `, ${b.mismatches} MISMATCH` : ''}`, b.mismatches === 0 ? 'green' : 'red');
|
||||
log('ws', `RTT: min=${pick(0)} avg=${avg} p50=${pick(0.5)} p95=${pick(0.95)} max=${pick(1)} ms`);
|
||||
setBurstBusy(false);
|
||||
}
|
||||
|
||||
// Stress -----------------------------------------------------------------
|
||||
|
||||
async function oneStress(req: { id: number; url: string; label: string }) {
|
||||
const start = performance.now();
|
||||
try {
|
||||
const resp = await mods!.mixFetch(req.url, {});
|
||||
const body = await resp.text();
|
||||
const el = ((performance.now() - start) / 1000).toFixed(2);
|
||||
log('stress', `[#${req.id} ${req.label}] ${resp.status} OK ${el}s (${body.length}B)`, 'green');
|
||||
return { ok: true, id: req.id, label: req.label };
|
||||
} catch (e) {
|
||||
const el = ((performance.now() - start) / 1000).toFixed(2);
|
||||
log('stress', `[#${req.id} ${req.label}] FAIL ${el}s: ${e}`, 'red');
|
||||
return { ok: false, id: req.id, label: req.label };
|
||||
}
|
||||
}
|
||||
|
||||
async function runStress() {
|
||||
if (!mods) return;
|
||||
setStressBusy(true);
|
||||
setStressStatus({ text: 'Running...', colour: 'orange' });
|
||||
const reqs = generateRequests(stressCount, stressMode, stressTimeout, stressUrl.trim());
|
||||
if (stressMode !== 'uniform') {
|
||||
const bd: Record<string, number> = {};
|
||||
reqs.forEach((r) => (bd[r.label] = (bd[r.label] || 0) + 1));
|
||||
log('stress', `${stressCount} requests, ${stressMode} mode, profiles: ${JSON.stringify(bd)}`);
|
||||
} else {
|
||||
log('stress', `${stressCount} requests, uniform mode`);
|
||||
}
|
||||
const t0 = performance.now();
|
||||
const settled = await Promise.allSettled(reqs.map((r) => oneStress(r)));
|
||||
const totalSec = ((performance.now() - t0) / 1000).toFixed(2);
|
||||
const ok = settled.filter((s) => s.status === 'fulfilled' && s.value.ok).length;
|
||||
const fail = stressCount - ok;
|
||||
log('stress', `done: ${ok}/${stressCount} OK, ${fail} failed (${totalSec}s total)`, fail === 0 ? 'green' : 'red');
|
||||
setStressStatus({ text: `Done: ${ok}/${stressCount} OK, ${fail} failed (${totalSec}s)`, colour: fail === 0 ? 'green' : 'red' });
|
||||
setStressBusy(false);
|
||||
}
|
||||
|
||||
// Download ---------------------------------------------------------------
|
||||
|
||||
async function verifyText() {
|
||||
if (!mods) return;
|
||||
setTextBusy(true);
|
||||
setTextStatus({ text: 'Fetching...', colour: 'orange' });
|
||||
log('download', `GET ${VERIFY_TEXT_URL} over the tunnel... (live transport logs in the browser console)`, 'orange');
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const resp = await mods.mixFetch(VERIFY_TEXT_URL, {});
|
||||
const text = await resp.text();
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
setTextStatus({ text: `${formatSize(text.length)} in ${ms} ms`, colour: 'green' });
|
||||
setTextOutput(text);
|
||||
log('download', `UTF-8 demo: ${formatSize(text.length)} in ${ms} ms`, 'green');
|
||||
} catch (e) {
|
||||
setTextStatus({ text: `Failed: ${e}`, colour: 'red' });
|
||||
log('download', `UTF-8 demo FAILED: ${e}`, 'red');
|
||||
} finally {
|
||||
setTextBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchFile() {
|
||||
if (!mods) return;
|
||||
const url = downloadUrl.trim();
|
||||
if (!url) return log('download', 'Download URL is required', 'red');
|
||||
setPdfBusy(true);
|
||||
cachedPdf.current = null;
|
||||
setPdfInfo(null);
|
||||
setFilePreview(null);
|
||||
setPdfStatus({ text: 'Fetching...', colour: 'orange' });
|
||||
log('download', `GET ${url} over the tunnel... (live transport logs in the browser console)`, 'orange');
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const resp = await mods.mixFetch(url, {});
|
||||
const buf = await resp.arrayBuffer();
|
||||
const ms = performance.now() - t0;
|
||||
const hash = await sha256hex(buf);
|
||||
cachedPdf.current = buf;
|
||||
setPdfInfo({ size: buf.byteLength, hash });
|
||||
const contentType = resp.headers.get('content-type') || '';
|
||||
const isImage = contentType.startsWith('image/') || /\.(jpe?g|png|gif|webp|avif|svg)(\?|$)/i.test(url);
|
||||
const objectUrl = URL.createObjectURL(new Blob([buf], contentType ? { type: contentType } : undefined));
|
||||
setFilePreview({ url: objectUrl, isImage });
|
||||
setPdfStatus({ text: `${formatSize(buf.byteLength)} in ${(ms / 1000).toFixed(1)}s`, colour: 'green' });
|
||||
log(
|
||||
'download',
|
||||
`${formatSize(buf.byteLength)} in ${(ms / 1000).toFixed(1)}s (${formatRate(buf.byteLength, ms)}); SHA-256: ${hash.slice(0, 16)}...`,
|
||||
'green',
|
||||
);
|
||||
} catch (e) {
|
||||
setPdfStatus({ text: `Failed: ${e}`, colour: 'red' });
|
||||
log('download', `FAILED: ${e}`, 'red');
|
||||
} finally {
|
||||
setPdfBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function savePdf() {
|
||||
const buf = cachedPdf.current;
|
||||
if (!buf) return;
|
||||
const filename = decodeURIComponent(downloadUrl.trim().split('/').pop()?.split('?')[0] || 'download');
|
||||
saveFile(buf, filename, 'application/octet-stream');
|
||||
}
|
||||
|
||||
async function runBoth() {
|
||||
setBothStatus({ text: 'Running...', colour: 'orange' });
|
||||
const t0 = performance.now();
|
||||
await Promise.allSettled([verifyText(), fetchFile()]);
|
||||
setBothStatus({ text: `Done in ${((performance.now() - t0) / 1000).toFixed(1)}s`, colour: 'green' });
|
||||
}
|
||||
|
||||
// Render -----------------------------------------------------------------
|
||||
|
||||
return (
|
||||
<div style={{ margin: '1.5rem 0' }}>
|
||||
{/* Connection */}
|
||||
<div style={box}>
|
||||
<div style={legend}>Connection</div>
|
||||
<div style={row}>
|
||||
<label style={{ ...sub, display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<input type="checkbox" checked={useRandomIpr} onChange={(e) => setUseRandomIpr(e.target.checked)} />
|
||||
Use random IPR
|
||||
</label>
|
||||
<input
|
||||
style={input}
|
||||
value={iprAddress}
|
||||
onChange={(e) => setIprAddress(e.target.value)}
|
||||
placeholder="<nym-address of IPR exit node>"
|
||||
disabled={useRandomIpr}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<details style={{ margin: '0.5rem 0' }}>
|
||||
<summary style={{ cursor: 'pointer', ...sub }}>Advanced options</summary>
|
||||
<div style={{ padding: '0.6rem 0' }}>
|
||||
<div style={row}>
|
||||
<label style={sub}>
|
||||
<input type="checkbox" checked={forceTls} onChange={(e) => setForceTls(e.target.checked)} /> Force TLS
|
||||
</label>
|
||||
<label style={sub}>
|
||||
<input type="checkbox" checked={disablePoisson} onChange={(e) => setDisablePoisson(e.target.checked)} /> Disable Poisson traffic
|
||||
</label>
|
||||
<label style={sub}>
|
||||
<input type="checkbox" checked={disableCover} onChange={(e) => setDisableCover(e.target.checked)} /> Disable cover traffic
|
||||
</label>
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>Client ID</label>
|
||||
<input style={input} value={clientId} onChange={(e) => setClientId(e.target.value)} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>Open SURBs</label>
|
||||
<input style={num} type="number" min={0} max={50} value={openSurbs} onChange={(e) => setOpenSurbs(+e.target.value)} />
|
||||
<label style={sub}>Data SURBs</label>
|
||||
<input style={num} type="number" min={0} max={50} value={dataSurbs} onChange={(e) => setDataSurbs(+e.target.value)} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>Primary DNS</label>
|
||||
<input style={input} value={primaryDns} onChange={(e) => setPrimaryDns(e.target.value)} placeholder="8.8.8.8:53" />
|
||||
<label style={sub}>Fallback DNS</label>
|
||||
<input style={input} value={fallbackDns} onChange={(e) => setFallbackDns(e.target.value)} placeholder="1.1.1.1:53" />
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div style={row}>
|
||||
<Button onClick={setup} disabled={busy || connected}>
|
||||
{busy ? 'Working...' : 'setupMixTunnel'}
|
||||
</Button>
|
||||
<Button onClick={disconnect} disabled={busy || !connected}>
|
||||
disconnectMixTunnel
|
||||
</Button>
|
||||
<label
|
||||
style={sub}
|
||||
title="Routes the Rust client's deep [smolmix] logs (gateway, IPR discovery, smoltcp) to the browser console. Set before connecting."
|
||||
>
|
||||
<input type="checkbox" checked={debug} onChange={(e) => setDebug(e.target.checked)} /> Verbose transport logs → console
|
||||
</label>
|
||||
<StatusText status={tunnelStatus} />
|
||||
</div>
|
||||
<LogPanel lines={lines('master')} placeholder="Press setupMixTunnel to bring up the tunnel." />
|
||||
<div style={{ ...sub, marginTop: '0.5rem' }}>
|
||||
One-shot per page: after <code>disconnectMixTunnel</code> you must reload to reconnect, and each load uses a fresh client identity.
|
||||
</div>
|
||||
<div style={{ ...sub, marginTop: '0.35rem' }}>
|
||||
This timeline shows the API-level events your code sees; the Rust client's deep transport logs (gateway, IPR discovery, smoltcp) go to the browser console behind <strong>Verbose transport logs</strong>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DNS */}
|
||||
<div style={box}>
|
||||
<div style={legend}>DNS resolve: tunnel vs clearnet</div>
|
||||
<div style={row}>
|
||||
<input style={input} value={dnsHost} onChange={(e) => setDnsHost(e.target.value)} placeholder="example.com" />
|
||||
<Button onClick={dnsTunnel} disabled={!connected}>via tunnel (IPR)</Button>
|
||||
<Button onClick={dnsClearnet}>via DoH (clearnet)</Button>
|
||||
</div>
|
||||
<div style={sub}>The clearnet DoH query appears in DevTools Network; the tunnel resolution does not.</div>
|
||||
<div style={sub}>Resolve the same hostname twice: the second answer comes from the in-wasm DNS cache, served locally with no mixnet round-trip.</div>
|
||||
<LogPanel lines={lines('dns')} />
|
||||
</div>
|
||||
|
||||
{/* GET */}
|
||||
<div style={box}>
|
||||
<div style={legend}>GET: tunnel vs clearnet</div>
|
||||
<div style={row}>
|
||||
<input style={input} value={getUrl} onChange={(e) => setGetUrl(e.target.value)} placeholder="https://..." />
|
||||
<Button onClick={getTunnel} disabled={!connected}>via tunnel</Button>
|
||||
<Button onClick={getClearnet}>via window.fetch</Button>
|
||||
</div>
|
||||
<div style={sub}>Both buttons request the same URL, but the clearnet one reaches the server from your own IP and the tunnel one from the IPR's exit gateway.</div>
|
||||
<div style={sub}>The clearnet button is a normal browser request, so some hosts block it with CORS while the tunnel request to the same URL succeeds; the defaults here are CORS-permissive.</div>
|
||||
<div style={sub}>The first tunnel request to a host runs a full TCP + TLS handshake (visible in the browser console with debug logging on). The HTTPS connection is then pooled, so a second request to the same host skips the handshake; the log timings show the difference.</div>
|
||||
<LogPanel lines={lines('get')} />
|
||||
</div>
|
||||
|
||||
{/* WebSocket */}
|
||||
<div style={box}>
|
||||
<div style={legend}>WebSocket</div>
|
||||
<div style={row}>
|
||||
<input style={input} value={wsUrl} onChange={(e) => setWsUrl(e.target.value)} placeholder="wss://..." />
|
||||
<Button onClick={wsConnect} disabled={!connected || wsConnected}>Connect</Button>
|
||||
<Button onClick={wsClose} disabled={!wsConnected}>Close</Button>
|
||||
<StatusText status={wsStatus} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<input style={input} value={wsMessage} onChange={(e) => setWsMessage(e.target.value)} />
|
||||
<Button onClick={wsSend} disabled={!wsConnected || burstBusy}>Send</Button>
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>Echo burst</label>
|
||||
<input style={num} type="number" min={1} max={500} value={burstCount} onChange={(e) => setBurstCount(+e.target.value)} />
|
||||
<label style={sub}>size</label>
|
||||
<input style={num} type="number" min={1} value={burstMin} onChange={(e) => setBurstMin(+e.target.value)} />
|
||||
<span style={sub}>–</span>
|
||||
<input style={num} type="number" min={1} value={burstMax} onChange={(e) => setBurstMax(+e.target.value)} />
|
||||
<span style={sub}>bytes</span>
|
||||
<Button onClick={wsBurst} disabled={!wsConnected || burstBusy}>{burstBusy ? 'Bursting...' : 'Send burst'}</Button>
|
||||
</div>
|
||||
<div style={sub}>Connecting runs a TCP handshake (plus a TLS handshake for wss://) inside the worker, visible in the browser console with debug logging on.</div>
|
||||
<LogPanel lines={lines('ws')} />
|
||||
</div>
|
||||
|
||||
{/* Stress */}
|
||||
<div style={box}>
|
||||
<div style={legend}>Stress test</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>Requests</label>
|
||||
<input style={num} type="number" min={1} max={200} value={stressCount} onChange={(e) => setStressCount(+e.target.value)} />
|
||||
<label style={sub}>Mode</label>
|
||||
<select style={{ ...input, flex: '0 0 9rem' }} value={stressMode} onChange={(e) => setStressMode(e.target.value as typeof stressMode)}>
|
||||
<option value="uniform">Uniform</option>
|
||||
<option value="mixed">Mixed sizes</option>
|
||||
<option value="drip">Slow drip</option>
|
||||
</select>
|
||||
<Button onClick={runStress} disabled={!connected || stressBusy}>{stressBusy ? 'Running...' : 'Run stress test'}</Button>
|
||||
<StatusText status={stressStatus} />
|
||||
</div>
|
||||
{stressMode === 'uniform' && (
|
||||
<div style={row}>
|
||||
<label style={sub}>Base URL</label>
|
||||
<input style={input} value={stressUrl} onChange={(e) => setStressUrl(e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
{stressMode === 'mixed' && <div style={sub}>Random mix of 128 B / 1 KB / 10 KB / 100 KB / 1 MB responses (httpbin.org/bytes).</div>}
|
||||
{stressMode === 'drip' && (
|
||||
<div style={row}>
|
||||
<label style={sub}>Timeout (s)</label>
|
||||
<input style={num} type="number" min={5} max={300} value={stressTimeout} onChange={(e) => setStressTimeout(+e.target.value)} />
|
||||
<span style={sub}>safe / boundary / over / slow-start, relative to this timeout (httpbin.org/drip).</span>
|
||||
</div>
|
||||
)}
|
||||
<div style={sub}>Requests to the same host share one pooled TCP + TLS connection, so only the first pays the handshake cost.</div>
|
||||
<LogPanel lines={lines('stress')} />
|
||||
</div>
|
||||
|
||||
{/* Download */}
|
||||
<div style={box}>
|
||||
<div style={legend}>File download</div>
|
||||
<div style={row}>
|
||||
<Button onClick={verifyText} disabled={!connected || textBusy}>Fetch UTF-8 text</Button>
|
||||
{textBusy ? <Spinner label="downloading... see the browser console for progress" /> : <StatusText status={textStatus} />}
|
||||
</div>
|
||||
{textOutput != null && (
|
||||
<pre
|
||||
style={{
|
||||
maxHeight: 180,
|
||||
overflowY: 'auto',
|
||||
fontSize: 12,
|
||||
whiteSpace: 'pre-wrap',
|
||||
background: 'rgba(127,127,127,0.06)',
|
||||
border: '1px solid rgba(127,127,127,0.2)',
|
||||
borderRadius: 6,
|
||||
padding: '0.5rem',
|
||||
}}
|
||||
>
|
||||
{textOutput}
|
||||
</pre>
|
||||
)}
|
||||
<div style={row}>
|
||||
<input style={input} value={downloadUrl} onChange={(e) => setDownloadUrl(e.target.value)} />
|
||||
<Button onClick={fetchFile} disabled={!connected || pdfBusy}>Fetch file</Button>
|
||||
<Button onClick={savePdf} disabled={!pdfInfo}>Save</Button>
|
||||
<Button onClick={() => filePreview && window.open(filePreview.url, '_blank')} disabled={!filePreview}>Open in new tab</Button>
|
||||
<Button onClick={runBoth} disabled={!connected}>Run both</Button>
|
||||
{pdfBusy ? <Spinner label="downloading... see the browser console for progress" /> : <StatusText status={bothStatus} />}
|
||||
</div>
|
||||
{pdfInfo && (
|
||||
<div style={sub}>
|
||||
Size: {pdfInfo.size.toLocaleString()} bytes · SHA-256: <code>{pdfInfo.hash}</code>
|
||||
</div>
|
||||
)}
|
||||
{filePreview?.isImage && (
|
||||
<img
|
||||
src={filePreview.url}
|
||||
alt="File downloaded over the mixnet"
|
||||
style={{
|
||||
maxHeight: 240,
|
||||
maxWidth: '100%',
|
||||
marginTop: '0.5rem',
|
||||
borderRadius: 6,
|
||||
border: '1px solid rgba(127,127,127,0.25)',
|
||||
display: 'block',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div style={sub}>Fetches a real file over the tunnel and reports its size and SHA-256. Fetch it twice and the second download reuses the pooled HTTPS connection, skipping the handshake.</div>
|
||||
<LogPanel lines={lines('download')} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// Pure helpers + package loader for the mix playground. No React here: this is
|
||||
// the logic ported from `wasm/smolmix/internal-dev/index.js`, adapted to drive
|
||||
// the *published* @nymproject/mix-* packages instead of internal-dev's own
|
||||
// worker. The differences that matter:
|
||||
// - published `mixFetch` returns a real `Response` (internal-dev returned a
|
||||
// raw `{body,status,statusText,headers}` and wrapped it);
|
||||
// - there is no live `setDebugLogging`; debug is a `setupMixTunnel` opt;
|
||||
// - the WebSocket is the EventTarget-based `MixWebSocket` (async send/close,
|
||||
// `opened()`, binaryType fixed to arraybuffer).
|
||||
|
||||
// Local mirror of the published `SetupMixTunnelOpts` (subset we surface).
|
||||
export interface SetupOpts {
|
||||
preferredIpr?: string;
|
||||
clientId?: string;
|
||||
forceTls?: boolean;
|
||||
disablePoissonTraffic?: boolean;
|
||||
disableCoverTraffic?: boolean;
|
||||
openReplySurbs?: number;
|
||||
dataReplySurbs?: number;
|
||||
primaryDns?: string;
|
||||
fallbackDns?: string;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
export interface TunnelState {
|
||||
state: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
// Mirror of the published `MixWebSocket` runtime surface.
|
||||
export interface MixWebSocketLike extends EventTarget {
|
||||
send(data: string | ArrayBuffer | Uint8Array): Promise<void>;
|
||||
close(code?: number, reason?: string): Promise<void>;
|
||||
opened(): Promise<void>;
|
||||
readonly readyState: number;
|
||||
readonly protocols: string[];
|
||||
}
|
||||
|
||||
// The slice of the three packages the playground uses. They share one tunnel
|
||||
// (mix-tunnel is deduped), so setup/disconnect/state are taken from mix-fetch.
|
||||
export interface PlaygroundMods {
|
||||
setupMixTunnel(opts?: SetupOpts): Promise<void>;
|
||||
disconnectMixTunnel(): Promise<void>;
|
||||
getTunnelState(): Promise<TunnelState>;
|
||||
mixFetch(url: string, init?: RequestInit): Promise<Response>;
|
||||
mixDNS(hostname: string): Promise<string>;
|
||||
MixWebSocket: new (url: string, protocols?: string | string[]) => MixWebSocketLike;
|
||||
}
|
||||
|
||||
// Lazy-load the three facades. Literal specifiers keep webpack code-splitting
|
||||
// the wasm into async chunks (loaded only when the user clicks Setup). The
|
||||
// `@ts-ignore`s are harmless once installed; they keep a fresh checkout
|
||||
// type-checking before `pnpm install`.
|
||||
export async function loadModules(): Promise<PlaygroundMods> {
|
||||
const [f, d, w] = await Promise.all([
|
||||
// @ts-ignore -- @nymproject/mix-fetch resolves at runtime; lazy wasm chunk
|
||||
import('@nymproject/mix-fetch'),
|
||||
// @ts-ignore -- @nymproject/mix-dns resolves at runtime; lazy wasm chunk
|
||||
import('@nymproject/mix-dns'),
|
||||
// @ts-ignore -- @nymproject/mix-websocket resolves at runtime; lazy wasm chunk
|
||||
import('@nymproject/mix-websocket'),
|
||||
]);
|
||||
return {
|
||||
setupMixTunnel: f.setupMixTunnel,
|
||||
disconnectMixTunnel: f.disconnectMixTunnel,
|
||||
getTunnelState: f.getTunnelState,
|
||||
mixFetch: f.mixFetch,
|
||||
mixDNS: d.mixDNS,
|
||||
MixWebSocket: w.MixWebSocket,
|
||||
} as unknown as PlaygroundMods;
|
||||
}
|
||||
|
||||
// Fresh client-storage id per page load so a reload gets a clean identity and
|
||||
// doesn't collide with the gateway connection from the previous load (which
|
||||
// lingers in the gateway's post-disconnect grace window). See tunnel.rs:
|
||||
// "Randomise per session to get a clean client".
|
||||
//
|
||||
// Called from a post-mount effect, never at module/render time: Math.random at
|
||||
// render would differ between SSG and the client and trip React hydration.
|
||||
export const randomClientId = () => `smolmix-playground-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
export const clampSurbs = (n: number) => Math.min(50, Math.max(0, n));
|
||||
|
||||
export function formatSize(bytes: number): string {
|
||||
if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + ' MB';
|
||||
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return bytes + ' B';
|
||||
}
|
||||
|
||||
export function formatRate(bytes: number, ms: number): string {
|
||||
return (bytes / 1024 / (ms / 1000)).toFixed(1) + ' KB/s';
|
||||
}
|
||||
|
||||
export function hexPreview(data: Uint8Array | ArrayBuffer, maxBytes = 64): string {
|
||||
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
||||
const len = Math.min(bytes.length, maxBytes);
|
||||
const hex = Array.from(bytes.slice(0, len), (b) => b.toString(16).padStart(2, '0')).join(' ');
|
||||
return bytes.length > maxBytes ? `${hex} ...` : hex;
|
||||
}
|
||||
|
||||
export async function sha256hex(buf: ArrayBuffer): Promise<string> {
|
||||
const hash = await crypto.subtle.digest('SHA-256', buf);
|
||||
return Array.from(new Uint8Array(hash), (b) => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
export function saveFile(buf: ArrayBuffer, filename: string, mimeType: string): void {
|
||||
const blob = new Blob([buf], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// The browser exposes no raw DNS API; "clearnet DNS" from JS is DoH (HTTPS) to
|
||||
// a public resolver. Google's JSON API is CORS-friendly and returns
|
||||
// { Status, Answer: [{ name, type, TTL, data }] } where type=1 is an A record.
|
||||
// The request shows up in DevTools Network as a plain HTTPS fetch.
|
||||
export async function dohResolve(hostname: string): Promise<string> {
|
||||
const resp = await window.fetch(
|
||||
`https://dns.google/resolve?name=${encodeURIComponent(hostname)}&type=A`,
|
||||
{ mode: 'cors' },
|
||||
);
|
||||
const json = await resp.json();
|
||||
if (json.Status !== 0) throw new Error(`DoH status=${json.Status}`);
|
||||
const a = (json.Answer as Array<{ type: number; data: string }> | undefined)?.find(
|
||||
(x) => x.type === 1,
|
||||
);
|
||||
if (!a) throw new Error('no A record');
|
||||
return a.data;
|
||||
}
|
||||
|
||||
// Stress-test request generation (uniform / mixed / drip), ported verbatim.
|
||||
export interface StressRequest {
|
||||
id: number;
|
||||
url: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const SIZE_PROFILES = [
|
||||
{ label: 'tiny', bytes: 128 },
|
||||
{ label: 'small', bytes: 1024 },
|
||||
{ label: 'medium', bytes: 10240 },
|
||||
{ label: 'large', bytes: 102400 },
|
||||
{ label: 'xlarge', bytes: 1048576 },
|
||||
];
|
||||
|
||||
function buildDripProfiles(timeoutSec: number) {
|
||||
return [
|
||||
{ label: 'safe', duration: Math.round(timeoutSec * 0.5), delay: 0, bytes: 100 },
|
||||
{ label: 'boundary', duration: Math.round(timeoutSec * 0.92), delay: 0, bytes: 100 },
|
||||
{ label: 'over', duration: Math.round(timeoutSec * 1.08), delay: 0, bytes: 100 },
|
||||
{
|
||||
label: 'slow-start',
|
||||
duration: Math.round(timeoutSec * 0.83),
|
||||
delay: Math.round(timeoutSec * 0.17),
|
||||
bytes: 100,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function generateRequests(
|
||||
count: number,
|
||||
mode: 'uniform' | 'mixed' | 'drip',
|
||||
timeoutSec: number,
|
||||
baseUrl: string,
|
||||
): StressRequest[] {
|
||||
const requests: StressRequest[] = [];
|
||||
if (mode === 'uniform') {
|
||||
for (let i = 1; i <= count; i++) requests.push({ id: i, url: `${baseUrl}${i}`, label: 'uniform' });
|
||||
} else if (mode === 'mixed') {
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const p = SIZE_PROFILES[Math.floor(Math.random() * SIZE_PROFILES.length)];
|
||||
requests.push({ id: i, url: `https://httpbin.org/bytes/${p.bytes}`, label: p.label });
|
||||
}
|
||||
} else {
|
||||
const profiles = buildDripProfiles(timeoutSec);
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const p = profiles[Math.floor(Math.random() * profiles.length)];
|
||||
requests.push({
|
||||
id: i,
|
||||
url: `https://httpbin.org/drip?duration=${p.duration}&numbytes=${p.bytes}&delay=${p.delay}&code=200`,
|
||||
label: p.label,
|
||||
});
|
||||
}
|
||||
}
|
||||
return requests;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { box, row, legend, sub, input, Button, LogPanel, StatusText, useLogs, type Status } from './ui';
|
||||
|
||||
// Raw mixnet messaging demo, styled to match the mix-* playground sections.
|
||||
// Uses @nymproject/sdk-full-fat (a separate wasm client from the smolmix tunnel)
|
||||
// to create a Nym client, send a message to a Nym address, and receive it.
|
||||
//
|
||||
// The SDK is imported dynamically on Connect, not at module scope: that keeps
|
||||
// the page SSR/SSG-safe and means the second wasm runtime + gateway connection
|
||||
// only load when the visitor opts in.
|
||||
|
||||
const nymApiUrl = 'https://validator.nymtech.net/api';
|
||||
const preferredGateway = 'q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1';
|
||||
|
||||
// Minimal shape of the bits of NymMixnetClient we use, to type the `any` from
|
||||
// the dynamic import.
|
||||
interface MessagingClient {
|
||||
client: {
|
||||
start(opts: {
|
||||
clientId: string;
|
||||
nymApiUrl: string;
|
||||
forceTls?: boolean;
|
||||
preferredGateway?: string;
|
||||
}): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
send(args: { payload: { message: string; mimeType: string }; recipient: string }): Promise<void>;
|
||||
};
|
||||
events: {
|
||||
subscribeToLoaded(cb: (e: { args: unknown }) => void): void;
|
||||
subscribeToConnected(cb: (e: { args: { address: string } }) => void): void;
|
||||
subscribeToTextMessageReceivedEvent(cb: (e: { args: { payload: string } }) => void): void;
|
||||
};
|
||||
}
|
||||
|
||||
export function MessagingDemo() {
|
||||
const { log, lines } = useLogs();
|
||||
const [status, setStatus] = useState<Status>({ text: 'Not connected', colour: 'gray' });
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [selfAddress, setSelfAddress] = useState('');
|
||||
const [recipient, setRecipient] = useState('');
|
||||
const [message, setMessage] = useState('hello through the mixnet');
|
||||
const clientRef = useRef<MessagingClient | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clientRef.current?.client.stop().catch(() => {});
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function connect() {
|
||||
setBusy(true);
|
||||
setStatus({ text: 'Loading SDK...', colour: 'orange' });
|
||||
log('msg', 'Loading @nymproject/sdk-full-fat (wasm)...');
|
||||
try {
|
||||
// @ts-ignore -- published separately; dynamic import keeps the wasm off SSR and lazy
|
||||
const mod = await import('@nymproject/sdk-full-fat');
|
||||
const nym = (await mod.createNymMixnetClient()) as unknown as MessagingClient;
|
||||
clientRef.current = nym;
|
||||
|
||||
nym.events.subscribeToLoaded(() => log('msg', 'client wasm loaded', 'green'));
|
||||
nym.events.subscribeToConnected((e) => {
|
||||
const addr = e.args.address;
|
||||
setSelfAddress(addr);
|
||||
setRecipient((r) => r || addr); // default to sending to yourself
|
||||
setConnected(true);
|
||||
setStatus({ text: 'Connected', colour: 'green' });
|
||||
log('msg', `connected; self address: ${addr}`, 'green');
|
||||
});
|
||||
nym.events.subscribeToTextMessageReceivedEvent((e) => {
|
||||
log('msg', `received: ${e.args.payload}`, 'green');
|
||||
});
|
||||
|
||||
log('msg', 'Starting client and connecting to a gateway...');
|
||||
setStatus({ text: 'Connecting to mixnet...', colour: 'orange' });
|
||||
await nym.client.start({ clientId: crypto.randomUUID(), nymApiUrl, forceTls: true, preferredGateway });
|
||||
} catch (err) {
|
||||
setStatus({ text: 'Failed', colour: 'red' });
|
||||
log('msg', `error: ${err instanceof Error ? err.message : String(err)}`, 'red');
|
||||
setBusy(false);
|
||||
}
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
async function send() {
|
||||
const nym = clientRef.current;
|
||||
if (!nym || !recipient || !message) return;
|
||||
try {
|
||||
await nym.client.send({ payload: { message, mimeType: 'text/plain' }, recipient });
|
||||
log('msg', `sent: ${message}`);
|
||||
} catch (err) {
|
||||
log('msg', `send failed: ${err instanceof Error ? err.message : String(err)}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={box}>
|
||||
<div style={legend}>Raw mixnet messaging</div>
|
||||
<div style={sub}>
|
||||
Creates a client with <code>@nymproject/sdk-full-fat</code> and sends a message to a Nym
|
||||
address through the mixnet (defaults to your own address). This loads a separate wasm runtime
|
||||
and connects its own client, so it is opt-in.
|
||||
</div>
|
||||
<div style={{ ...row, marginTop: '0.75rem' }}>
|
||||
<Button onClick={connect} disabled={busy || connected}>
|
||||
{busy ? 'Connecting...' : 'Connect'}
|
||||
</Button>
|
||||
<StatusText status={status} />
|
||||
</div>
|
||||
{selfAddress && (
|
||||
<div style={{ ...sub, wordBreak: 'break-all', margin: '0 0 0.5rem' }}>
|
||||
Your address: <code>{selfAddress}</code>
|
||||
</div>
|
||||
)}
|
||||
<div style={row}>
|
||||
<input
|
||||
style={input}
|
||||
value={recipient}
|
||||
onChange={(e) => setRecipient(e.target.value)}
|
||||
placeholder="recipient Nym address"
|
||||
disabled={!connected}
|
||||
/>
|
||||
</div>
|
||||
<div style={row}>
|
||||
<input
|
||||
style={input}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="message"
|
||||
disabled={!connected}
|
||||
/>
|
||||
<Button onClick={send} disabled={!connected}>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
<LogPanel lines={lines('msg')} placeholder="Press Connect to create a mixnet client." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Shared presentational primitives for the playground sections (MixPlayground
|
||||
// and the raw-messaging demo) so they share one look. Theme-neutral inline
|
||||
// styles (rgba greys read on light and dark Nextra themes), a per-section log
|
||||
// store, and an autoscrolling log panel.
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export type Colour = 'green' | 'red' | 'orange' | 'gray' | undefined;
|
||||
export const COLOURS: Record<string, string> = {
|
||||
green: '#16a34a',
|
||||
red: '#dc2626',
|
||||
orange: '#d97706',
|
||||
gray: '#9ca3af',
|
||||
};
|
||||
|
||||
export interface LogEntry {
|
||||
ts: string;
|
||||
msg: string;
|
||||
colour?: Colour;
|
||||
}
|
||||
|
||||
// One append-only buffer per section, keyed by section name. Red entries mirror
|
||||
// to console.error so they sit alongside the Rust-side `[smolmix] ...` logs.
|
||||
export function useLogs() {
|
||||
const [store, setStore] = useState<Record<string, LogEntry[]>>({});
|
||||
const log = useCallback((section: string, msg: string, colour?: Colour) => {
|
||||
const ts = new Date().toISOString().slice(11, 23);
|
||||
if (colour === 'red') console.error(`[smolmix-playground:${section}]`, msg);
|
||||
setStore((s) => ({ ...s, [section]: [...(s[section] ?? []), { ts, msg, colour }] }));
|
||||
}, []);
|
||||
const lines = useCallback((section: string) => store[section] ?? [], [store]);
|
||||
return { log, lines };
|
||||
}
|
||||
|
||||
export const box: React.CSSProperties = {
|
||||
border: '1px solid rgba(127,127,127,0.3)',
|
||||
borderRadius: 8,
|
||||
padding: '1rem',
|
||||
margin: '1rem 0',
|
||||
};
|
||||
export const row: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
marginBottom: '0.5rem',
|
||||
};
|
||||
export const btn: React.CSSProperties = {
|
||||
padding: '0.35rem 0.8rem',
|
||||
borderRadius: 6,
|
||||
border: '1px solid rgba(127,127,127,0.4)',
|
||||
background: 'transparent',
|
||||
cursor: 'pointer',
|
||||
fontSize: 14,
|
||||
};
|
||||
export const input: React.CSSProperties = {
|
||||
padding: '0.35rem 0.6rem',
|
||||
borderRadius: 6,
|
||||
border: '1px solid rgba(127,127,127,0.4)',
|
||||
background: 'transparent',
|
||||
fontSize: 14,
|
||||
flex: '1 1 14rem',
|
||||
minWidth: 0,
|
||||
};
|
||||
export const num: React.CSSProperties = { ...input, flex: '0 0 5rem' };
|
||||
export const legend: React.CSSProperties = { fontWeight: 600, marginBottom: '0.6rem' };
|
||||
export const sub: React.CSSProperties = { fontSize: 12, opacity: 0.65 };
|
||||
|
||||
export function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
style={{ ...btn, ...(props.disabled ? { opacity: 0.4, cursor: 'not-allowed' } : {}) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function LogPanel({ lines, placeholder }: { lines: LogEntry[]; placeholder?: string }) {
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
useEffect(() => {
|
||||
if (ref.current) ref.current.scrollTop = ref.current.scrollHeight;
|
||||
}, [lines]);
|
||||
return (
|
||||
<pre
|
||||
ref={ref}
|
||||
style={{
|
||||
maxHeight: 220,
|
||||
overflowY: 'auto',
|
||||
padding: '0.6rem',
|
||||
borderRadius: 6,
|
||||
background: 'rgba(127,127,127,0.08)',
|
||||
border: '1px solid rgba(127,127,127,0.25)',
|
||||
fontSize: 12.5,
|
||||
lineHeight: 1.5,
|
||||
margin: '0.5rem 0 0',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{lines.length === 0
|
||||
? placeholder ?? 'Idle.'
|
||||
: lines.map((l, i) => (
|
||||
<div key={i} style={l.colour ? { color: COLOURS[l.colour] } : undefined}>
|
||||
[{l.ts}] {l.msg}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
text: string;
|
||||
colour?: Colour;
|
||||
}
|
||||
|
||||
export function StatusText({ status }: { status: Status }) {
|
||||
return (
|
||||
<span style={{ ...sub, color: status.colour ? COLOURS[status.colour] : undefined }}>
|
||||
{status.text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// A small CSS spinner with optional label. Used while a tunnel request is in
|
||||
// flight, since mixFetch buffers the whole body and exposes no byte progress;
|
||||
// the live transport detail goes to the browser console instead.
|
||||
export function Spinner({ label }: { label?: string }) {
|
||||
return (
|
||||
<span style={{ ...sub, display: 'inline-flex', alignItems: 'center', gap: 6 }}>
|
||||
<style>{'@keyframes mixspin{to{transform:rotate(360deg)}}'}</style>
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
width: 11,
|
||||
height: 11,
|
||||
border: '2px solid rgba(127,127,127,0.35)',
|
||||
borderTopColor: COLOURS.orange,
|
||||
borderRadius: '50%',
|
||||
display: 'inline-block',
|
||||
animation: 'mixspin 0.7s linear infinite',
|
||||
}}
|
||||
/>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -22,10 +22,18 @@
|
||||
*/
|
||||
|
||||
// nym-sdk / nym-bin-common / nym-network-defaults (Rust SDK crates)
|
||||
export const NYM_SDK_VERSION = "1.21.0";
|
||||
export const NYM_SDK_VERSION = "1.21.1";
|
||||
|
||||
// smolmix standalone crate
|
||||
export const SMOLMIX_VERSION = "1.21.0";
|
||||
export const SMOLMIX_VERSION = "1.21.1";
|
||||
|
||||
// TypeScript SDK packages (published to npm). mix-fetch is on its own 2.x track
|
||||
// after the v1 to v2 break; the tunnel + mix-dns + mix-websocket facades share
|
||||
// a 0.x line for now. Bump these to match the published npm versions.
|
||||
export const MIX_FETCH_VERSION = "2.0.0";
|
||||
export const MIX_TUNNEL_VERSION = "0.1.0";
|
||||
export const MIX_DNS_VERSION = "0.1.0";
|
||||
export const MIX_WEBSOCKET_VERSION = "0.1.0";
|
||||
|
||||
// Minimum supported Rust version (matches workspace rust-version in root Cargo.toml)
|
||||
export const RUST_MSRV = "1.87";
|
||||
|
||||
@@ -62,6 +62,107 @@ const config = {
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
|
||||
// TS SDK reorg 2026-05: the per-package /developers/typescript/examples/*
|
||||
// and /developers/typescript/playground/* pages were consolidated into
|
||||
// top-level package pages (mix-fetch, mix-dns, mix-websocket) and
|
||||
// typescript/quick-start, typescript/smart-contracts, typescript/cosmos-kit.
|
||||
{
|
||||
source: "/docs/developers/typescript/examples/mix-fetch",
|
||||
destination: "/docs/developers/mix-fetch",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/examples/mixnet",
|
||||
destination: "/docs/developers/typescript/quick-start",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/examples/nym-smart-contracts",
|
||||
destination: "/docs/developers/typescript/smart-contracts",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/examples/cosmos-kit",
|
||||
destination: "/docs/developers/typescript/cosmos-kit",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/examples",
|
||||
destination: "/docs/developers/typescript",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/playground/mixfetch",
|
||||
destination: "/docs/developers/mix-fetch",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/playground/traffic",
|
||||
destination: "/docs/developers/typescript/quick-start",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/playground/mixnodes",
|
||||
destination: "/docs/developers/typescript/smart-contracts",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/playground/wallet",
|
||||
destination: "/docs/developers/typescript/cosmos-kit",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/playground/cosmos-kit",
|
||||
destination: "/docs/developers/typescript/cosmos-kit",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/playground",
|
||||
destination: "/docs/developers/typescript",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
|
||||
// The per-package typedoc dirs moved out of typescript/api/<pkg>/ to
|
||||
// <pkg>/api/ so each package's API reference nests under its own sidebar
|
||||
// entry (matches the rust/<module>/ pattern). @nymproject/sdk's typedoc
|
||||
// stays at typescript/api/sdk/ since its landing is typescript.mdx.
|
||||
{
|
||||
source: "/docs/developers/typescript/api/mix-tunnel/:path*",
|
||||
destination: "/docs/developers/mix-tunnel/api/:path*",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/api/mix-fetch/:path*",
|
||||
destination: "/docs/developers/mix-fetch/api/:path*",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/api/mix-dns/:path*",
|
||||
destination: "/docs/developers/mix-dns/api/:path*",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/api/mix-websocket/:path*",
|
||||
destination: "/docs/developers/mix-websocket/api/:path*",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
|
||||
{
|
||||
source: "/docs/architecture/nym-vs-others.html",
|
||||
destination: "/docs/network/overview/comparisons",
|
||||
@@ -368,13 +469,13 @@ const config = {
|
||||
},
|
||||
{
|
||||
source: "/developers/integrations/integration-options.html",
|
||||
destination: "/docs/developers/integrations",
|
||||
destination: "/docs/developers",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/developers/faq/integrations-faq.html",
|
||||
destination: "/docs/developers/integrations",
|
||||
destination: "/docs/developers",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
@@ -434,7 +535,7 @@ const config = {
|
||||
},
|
||||
{
|
||||
source: "/developers/integrations",
|
||||
destination: "/docs/developers/integrations/payment-integration.html",
|
||||
destination: "/docs/developers",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
@@ -794,7 +895,7 @@ const config = {
|
||||
},
|
||||
{
|
||||
source: "/developers/faq/integrations-faq.html",
|
||||
destination: "/docs/developers/integrations",
|
||||
destination: "/docs/developers",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
@@ -884,7 +985,7 @@ const config = {
|
||||
},
|
||||
{
|
||||
source: "/developers/faq/integrations-faq.html",
|
||||
destination: "/docs/developers/integrations",
|
||||
destination: "/docs/developers",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
@@ -1179,16 +1280,39 @@ const config = {
|
||||
},
|
||||
|
||||
// Docs reorg: language-based sidebar
|
||||
// Deleted routing pages → merged into integrations
|
||||
// Deleted routing pages → merged into the developer overview
|
||||
{
|
||||
source: "/docs/developers/native",
|
||||
destination: "/docs/developers/integrations",
|
||||
destination: "/docs/developers",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/browsers",
|
||||
destination: "/docs/developers/integrations",
|
||||
destination: "/docs/developers",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
|
||||
// --- Developers reorg (2026-06): single front door + collapsed mix-* pages ---
|
||||
// integrations.mdx merged into the overview (index)
|
||||
{
|
||||
source: "/docs/developers/integrations",
|
||||
destination: "/docs/developers",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
// mix-dns concepts merged into its reference page
|
||||
{
|
||||
source: "/docs/developers/mix-dns/concepts",
|
||||
destination: "/docs/developers/mix-dns/guides",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
// mix-tunnel concepts removed; security lives in exit-security, architecture on its own page
|
||||
{
|
||||
source: "/docs/developers/mix-tunnel/concepts",
|
||||
destination: "/docs/developers/mix-tunnel",
|
||||
permanent: true,
|
||||
basePath: false,
|
||||
},
|
||||
@@ -1208,7 +1332,7 @@ const config = {
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/api/mix-fetch",
|
||||
destination: "/docs/developers/typescript/api/mix-fetch/globals",
|
||||
destination: "/docs/developers/mix-fetch/api/globals",
|
||||
permanent: false,
|
||||
basePath: false,
|
||||
},
|
||||
@@ -1218,18 +1342,6 @@ const config = {
|
||||
permanent: false,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/examples",
|
||||
destination: "/docs/developers/typescript/examples/mix-fetch",
|
||||
permanent: false,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/playground",
|
||||
destination: "/docs/developers/typescript/playground/mixfetch",
|
||||
permanent: false,
|
||||
basePath: false,
|
||||
},
|
||||
{
|
||||
source: "/docs/developers/typescript/api",
|
||||
destination: "/docs/developers/typescript/api/sdk",
|
||||
|
||||
@@ -39,7 +39,10 @@
|
||||
"@nextui-org/accordion": "^2.0.40",
|
||||
"@nextui-org/react": "^2.4.8",
|
||||
"@nymproject/contract-clients": ">=1.2.4-rc.2 || ^1",
|
||||
"@nymproject/mix-fetch-full-fat": "^1.4.3",
|
||||
"@nymproject/mix-dns": "^0.1.0",
|
||||
"@nymproject/mix-fetch": "^2.0.0",
|
||||
"@nymproject/mix-tunnel": "^0.1.0",
|
||||
"@nymproject/mix-websocket": "^0.1.0",
|
||||
"@nymproject/sdk-full-fat": ">=1.5.1-rc.0 || ^1.4.1",
|
||||
"@redocly/cli": "^1.25.15",
|
||||
"@types/mdx": "^2.0.13",
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
{
|
||||
"index": "Overview",
|
||||
"integrations": "Choosing an Approach",
|
||||
"concepts": "Key Concepts",
|
||||
|
||||
"sep-intro": {
|
||||
"type": "separator"
|
||||
},
|
||||
"--": {
|
||||
"type": "separator",
|
||||
"title": "Rust"
|
||||
},
|
||||
"smolmix": "smolmix",
|
||||
"smolmix": "smolmix (TCP/UDP tunnel)",
|
||||
"rust": "nym-sdk",
|
||||
|
||||
"-": {
|
||||
"type": "separator",
|
||||
"title": "TypeScript"
|
||||
},
|
||||
"mix-fetch": "mix-fetch",
|
||||
"typescript": "TypeScript SDK",
|
||||
"playground": "Playground (embedded clients)",
|
||||
"mix-tunnel": "mix-tunnel (shared tunnel)",
|
||||
"mix-fetch": "mix-fetch (HTTPS requests)",
|
||||
"mix-dns": "mix-dns (DNS resolution)",
|
||||
"mix-websocket": "mix-websocket (ws / wss)",
|
||||
"mix-architecture": "mix-* Family Architecture",
|
||||
"typescript": "Raw Messaging SDK",
|
||||
|
||||
"sep-extras": {
|
||||
"type": "separator"
|
||||
},
|
||||
"---": {
|
||||
"type": "separator",
|
||||
"title": "Extras"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"nym-connect": "Nym Connect"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: "Interacting with the Nyx Blockchain"
|
||||
description: "Query and transact against Nyx, the Cosmos-SDK chain underpinning Nym: the nyxd CLI wallet, Ledger, the Cosmos chain registry, and running an RPC node."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-06"
|
||||
---
|
||||
|
||||
# Interacting with the Nyx blockchain
|
||||
|
||||
Nyx is the Cosmos-SDK blockchain that underpins Nym. It holds the NYM token and the mixnet smart contracts (node bonding, rewarding, and the directory). This section covers the ways to query it and submit transactions.
|
||||
|
||||
For smart-contract access from TypeScript, see [`@nymproject/contract-clients`](https://www.npmjs.com/package/@nymproject/contract-clients), covered under [Smart Contracts](/developers/typescript/smart-contracts) in the TypeScript SDK.
|
||||
|
||||
## In this section
|
||||
|
||||
- [CLI Wallet](/developers/chain/cli-wallet): use the `nyxd` binary to create keypairs and to sign and broadcast transactions from the command line.
|
||||
- [Ledger Live](/developers/chain/ledger-live): use a Ledger hardware wallet with the Nyx chain.
|
||||
- [Cosmos Registry](/developers/chain/cosmos-registry): Nyx's entry in the Cosmos chain registry (chain info and RPC endpoints).
|
||||
- [RPC Nodes](/developers/chain/rpc-node): run a node that holds a copy of the chain for querying and broadcasting, without taking part in consensus.
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"cli-wallet": "CLI Wallet",
|
||||
"ledger-live": "Ledger Live",
|
||||
"cosmos-registry": "Cosmos Registry",
|
||||
"rpc-node": "RPC Nodes"
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"socks5": "SOCKS Proxy",
|
||||
"websocket": "Websocket"
|
||||
"websocket": "Websocket",
|
||||
"webassembly-client": "WebAssembly Client"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"exit-security": "Exit Security",
|
||||
"message-queue": "Message Queue & Cover Traffic"
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: "Exit Security: What the Mixnet Protects and What It Doesn't"
|
||||
description: "The canonical security model for traffic that leaves the Nym mixnet at an IPR exit gateway. Applies to smolmix, mix-tunnel, mix-fetch, mix-dns, and mix-websocket alike."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-03"
|
||||
---
|
||||
|
||||
# Exit security
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
Every tool that reaches an external service through the Nym mixnet shares the same security model, whether it's the Rust [`smolmix`](/developers/smolmix) crate or the mix-* packages built on [`mix-tunnel`](/developers/mix-tunnel) ([`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), [`mix-websocket`](/developers/mix-websocket)). They all exit the mixnet at an [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) gateway, so they inherit the same properties and the same single caveat. This page is the canonical statement of that model; the package pages link here rather than restating it.
|
||||
|
||||
## The one-sentence version
|
||||
|
||||
The mixnet hides **who** you are from the destination and **where** you're going from the network, but the exit gateway sees your **destination** and any payload you didn't encrypt yourself.
|
||||
|
||||
## Proxy mode or end-to-end?
|
||||
|
||||
This page is about **proxy mode**: your traffic leaves the mixnet at an IPR exit and continues to a third-party server over clearnet, where the security trade-offs apply.
|
||||
|
||||
If both ends run a Nym client (**end-to-end**), traffic never exits the mixnet. It stays Sphinx-encrypted from your client to the other client, there is no IPR, and the [encrypt-your-own-payload](#encrypt-your-own-payload) concern below does not arise: the mixnet is the encrypted channel. What still applies to end-to-end traffic is everything that is not exit-specific: the [trust boundaries](#trust-boundaries) (unlinkability is statistical, not absolute) and [what the mixnet does not protect](#what-the-mixnet-does-not-protect) (application identity, fingerprinting, traffic analysis). For the end-to-end wiring itself, see [`nym-sdk`](/developers/rust) and the [TypeScript SDK](/developers/typescript).
|
||||
|
||||
## What each hop sees
|
||||
|
||||
```text
|
||||
you
|
||||
│ Sphinx
|
||||
▼
|
||||
entry gateway
|
||||
│ Sphinx
|
||||
▼
|
||||
3 mix layers
|
||||
│ Sphinx
|
||||
▼
|
||||
IPR exit gateway
|
||||
│ plain IP (Sphinx removed here)
|
||||
▼
|
||||
destination
|
||||
```
|
||||
|
||||
| Segment | Mixnet encryption | What's visible |
|
||||
|---|---|---|
|
||||
| Your machine → mixnet entry | Sphinx (layered) | Entry gateway sees your IP but not the destination |
|
||||
| Inside the mixnet (entry gateway + 3 mix layers) | Sphinx (layered) | Each node only knows its previous and next hop |
|
||||
| Exit gateway (IPR) | Sphinx removed, raw IP packet exposed | IPR sees destination IP + port. Payload depends on your application layer (see below). |
|
||||
| IPR → remote host | None (Sphinx is mixnet-only; your own TLS, if any, still applies) | Remote host sees the IPR's IP, not yours |
|
||||
|
||||
The Sphinx encryption is the **mixnet transport layer**: it protects packets as they traverse the mix nodes. At the exit gateway the Sphinx layers are stripped and the original IP packet is forwarded to the destination. This is analogous to how a Tor exit node or VPN endpoint unwraps its tunnel.
|
||||
|
||||
## Encrypt your own payload
|
||||
|
||||
Because the IPR removes the Sphinx layers, whatever is inside that IP packet is visible to the exit unless you encrypted it yourself.
|
||||
|
||||
- **Application-layer encryption closes the gap.** TLS, the Noise Protocol, or any authenticated encryption keeps the payload as ciphertext to the IPR. It still sees the destination IP and port, but not the content. Over TLS the IPR only ever handles ciphertext bound for that destination; the bytes inside stay opaque to it.
|
||||
- **Unencrypted payloads are fully visible.** Plain HTTP, unencrypted WebSocket (`ws://`), and plain UDP DNS are readable in full at the exit. The mixnet still hides your identity, so the exit reads the content without being able to attribute it to you.
|
||||
|
||||
## Trust boundaries
|
||||
|
||||
- You trust the mixnet to provide unlinkability between sender and receiver. Sphinx provides this cryptographically at the per-packet level: a node cannot read addressing beyond its own hop. Unlinkability of your *traffic pattern* over time is weaker, and statistical rather than absolute. It comes from mixing and cover traffic, and degrades with low network traffic, with cover traffic or Poisson timing disabled, and against an adversary that can observe a large fraction of the network.
|
||||
- You trust the IPR exit gateway in the same way you trust a VPN exit or Tor exit node: it can inspect your raw IP packets. The difference is that the IPR doesn't know who is sending the traffic (the mixnet hides your identity).
|
||||
|
||||
<Callout type="warning">
|
||||
Treat the IPR exactly as you would a VPN exit or a Tor exit node: it can inspect your raw IP packets. The difference Nym adds is that the IPR doesn't know who's sending the traffic. Protect the payload with TLS or equivalent, and pin a trusted exit (`preferredIpr`) if the exit operator matters to you.
|
||||
</Callout>
|
||||
|
||||
## What the mixnet does not protect
|
||||
|
||||
The mixnet operates at the **network layer**: it hides your IP and unlinks sender from receiver. It does nothing at the **application layer**, so anything you reveal in the content of your traffic is yours to manage:
|
||||
|
||||
- **Application identity.** If you log in, send a cookie, or include an API token, the destination knows who you are regardless of the network path. The mixnet anonymises the pipe, not what you put through it.
|
||||
- **Fingerprinting.** A stable request pattern, a distinctive TLS or HTTP fingerprint, or a recognisable account correlates your traffic across sessions. `mix-fetch`'s [default headers](/developers/mix-fetch/guides#default-request-headers) reduce trivial fingerprinting but do not make you indistinguishable from a real browser.
|
||||
|
||||
Separately, the network-layer guarantee itself is not absolute:
|
||||
|
||||
- **Statistical traffic analysis.** Unlinkability is probabilistic, not a guarantee. It is strong by default but weakens with low network traffic, with cover traffic and Poisson timing turned off, and against an adversary observing a large fraction of the network.
|
||||
|
||||
If you need anonymity at the application layer too, design for it explicitly: fresh identities, no cross-session correlators, and no logged-in accounts you also use over clearnet.
|
||||
|
||||
## Comparison with other privacy tools
|
||||
|
||||
| | Nym (mixnet) | Tor | VPN |
|
||||
|---|---|---|---|
|
||||
| **Exit node sees traffic?** | Yes (encrypt it) | Yes (encrypt it) | Yes (encrypt it) |
|
||||
| **Exit node knows sender?** | No (mixnet hides identity) | No (onion routing) | Yes (VPN provider knows) |
|
||||
| **Timing analysis resistance** | Strong with defaults (mixing, cover traffic) | Weak (low-latency) | None |
|
||||
| **UDP support** | Yes | No (TCP only) | Yes |
|
||||
|
||||
The timing-analysis rating assumes the defaults. Cover traffic and Poisson timing can be turned off to trade that resistance for latency and bandwidth, moving Nym's row toward Tor's. The named switches for this (`disableCoverTraffic` / `disablePoissonTraffic`) are specific to the browser/wasm packages ([`mix-tunnel`](/developers/mix-tunnel/guides#configuration) and the feature packages built on it); the native `smolmix` crate does not expose them by those names. The UDP row reflects a design difference, not a ranking: Tor is TCP-only by design, while the Nym IPR routes raw IP.
|
||||
|
||||
## Read more
|
||||
|
||||
The package pages add the parts specific to their transport (where TLS terminates, what the resolver sees, WSS vs `ws://`):
|
||||
|
||||
- [Exit Gateway Services](/network/infrastructure/exit-services#ip-packet-router): how the IPR allocates addresses and routes raw IP packets, and how it differs from the SOCKS-based Network Requester.
|
||||
- The per-package "Security model" section on [mix-fetch](/developers/mix-fetch/concepts#security-model), [mix-dns](/developers/mix-dns/guides#security-model), and [mix-websocket](/developers/mix-websocket/concepts#security-model) for the transport-specific exposure.
|
||||
@@ -82,7 +82,7 @@ sequenceDiagram
|
||||
|
||||
## What does `send()` do then?
|
||||
|
||||
When passing a message to a client (however you do it, either piping messages from an app to a standalone client or via one of the `send` functions exposed by the SDKs), you are **putting that message into the queue** to be source encrypted and sent in the future, in order to ensure that traffic leaving the client does so in a manner that to an external observer is uniform / does not create any 'burst' or change in traffic timings that could aid traffic analysis.
|
||||
When passing a message to a client (however you do it, either piping messages from an app to a standalone client or via one of the `send` functions exposed by the SDKs), you are **putting that message into the queue** to be source-encrypted and sent later, so that traffic leaving the client stays uniform to an external observer and creates no burst or timing change that could aid traffic analysis.
|
||||
|
||||
## Note on Client Shutdown
|
||||
Accidentally dropping a client before your message has been sent is possible and should be avoided (see the [troubleshooting guide](/developers/rust/mixnet/troubleshooting) for more on this). To avoid it:
|
||||
|
||||
@@ -1,30 +1,78 @@
|
||||
---
|
||||
title: "Overview"
|
||||
description: "Developer documentation index for the Nym mixnet: Rust and TypeScript SDKs, smolmix, mix-fetch, chain interaction, and CLI tools."
|
||||
description: "Choose a Nym integration path by runtime and approach, then find the crate or package: nym-sdk, smolmix, mix-tunnel, mix-fetch, mix-dns, mix-websocket, and the TypeScript SDK."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-05-12"
|
||||
lastUpdated: "2026-06-09"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
# Overview
|
||||
|
||||
This section covers the SDKs, standalone crates, blockchain interaction, and developer tools for building on the Nym mixnet.
|
||||
Every Nym integration sends its traffic through the mixnet via a Nym client. Which crate or package you use comes down to two questions:
|
||||
|
||||
## Start here
|
||||
1. **Runtime**: where does your code run?
|
||||
2. **Approach**: do you control both sides of the connection (**end-to-end**), or are you reaching a third-party service through the mixnet (**proxy**)?
|
||||
|
||||
If you're new, read **[Choosing an Approach](/developers/integrations)** first. It maps your runtime (native vs browser vs mobile) and your architecture (end-to-end vs proxy) onto the right crate/library.
|
||||
The table below maps those two answers to a package.
|
||||
|
||||
## Crates/Libraries
|
||||
## Choosing a package
|
||||
|
||||
| Crate/library | Language | Use it for |
|
||||
| Runtime | End-to-end (both sides run Nym) | Proxy (exit to clearnet) |
|
||||
|---|---|---|
|
||||
| [`nym-sdk`](/developers/rust) | Rust | E2E messaging, `AsyncRead`/`AsyncWrite` streams, client pooling. Start with the [Tour](/developers/rust/tour). |
|
||||
| [`smolmix`](/developers/smolmix) | Rust | `TcpStream` and `UdpSocket` over the Mixnet via a userspace IP stack. Compatible with `tokio-rustls`, `hyper`, `tungstenite`. |
|
||||
| [`mix-fetch`](/developers/mix-fetch) | TypeScript | `fetch()`-compatible API for browser HTTP(S) requests over the Mixnet. |
|
||||
| [TypeScript SDK](/developers/typescript) | TypeScript | Browser-side Mixnet Client (raw messaging) and Nyx Smart Contracts. |
|
||||
| [Standalone Clients](/developers/clients) | Language-agnostic | SOCKS5 and WebSocket binaries for piping traffic through the Mixnet without an SDK. |
|
||||
| **Native Rust** (desktop / CLI / server) | [`nym-sdk`](/developers/rust): Mixnet, Stream, Client Pool | [`smolmix`](/developers/smolmix): `TcpStream` / `UdpSocket` · [`nym-sdk` SOCKS](/developers/rust) |
|
||||
| **Browser / WebView** (JS + WASM) | [TypeScript SDK](/developers/typescript): `@nymproject/sdk` raw messaging | [`mix-fetch`](/developers/mix-fetch) HTTP/S · [`mix-dns`](/developers/mix-dns) DNS · [`mix-websocket`](/developers/mix-websocket) WS/WSS |
|
||||
|
||||
## Other sections
|
||||
<Callout type="info">
|
||||
**Mobile is a host, not a runtime.** The same phone can run either row. Compile the Rust SDK to a native library (`uniffi` plus [`cargo-swift`](https://github.com/antoniusnaumann/cargo-swift) for an iOS XCFramework, or [`cargo-ndk`](https://github.com/bbqsrc/cargo-ndk) for Android `jniLibs/`), or load the WASM packages inside a WebView (Capacitor, Cordova, Ionic, WKWebView, Android WebView). The SDK ships [FFI bindings](/developers/rust/ffi) for Go and C/C++ only; for Swift or Kotlin you generate your own from the [`sdk/ffi/shared`](https://github.com/nymtech/nym/tree/develop/sdk/ffi) uniffi crate. The WebView path needs no Nym-specific native code. On Android the native path has a [TLS bootstrap gotcha](/developers/rust/mixnet/troubleshooting#android-mixnet-bootstrap-fails-with-a-certificate-revoked--ocsp-error).
|
||||
</Callout>
|
||||
|
||||
- **[Chain interaction](/developers/chain)**: query Nyx state, submit transactions, and call Nym smart contracts.
|
||||
- **[APIs](/apis/introduction)**: auto-generated reference for Nym infrastructure HTTP endpoints.
|
||||
## End-to-end or proxy
|
||||
|
||||
The runtime axis is about where your code runs: a native process has raw sockets and a filesystem, so it runs the full Rust client; a browser or WebView has neither (only WebSockets and `fetch`, under mixed-content rules), so it runs a WASM client inside a Web Worker. The approach axis is about who runs Nym at the other end.
|
||||
|
||||
**End-to-end**: both sides run a Nym client. Traffic stays Sphinx-encrypted the whole way ([what this protects](/developers/concepts/exit-security#proxy-mode-or-end-to-end)). Use it for peer-to-peer setups or anywhere you control both endpoints.
|
||||
|
||||

|
||||
|
||||
**Proxy**: only your side runs Nym. Traffic exits the mixnet at an Exit Gateway and continues to the destination over the public internet. The mixnet anonymises the sender; protecting the payload (TLS, Noise) is your application's job, exactly as on a direct connection. Use it for third-party services such as blockchain RPCs or external APIs.
|
||||
|
||||

|
||||
|
||||
<Callout type="warning">
|
||||
Past the Exit Gateway, traffic travels the public internet like any other connection. The mixnet anonymises the sender but does not encrypt the payload beyond the gateway. Use TLS or another application-layer cipher. See [Exit security](/developers/concepts/exit-security) for what the exit can and cannot observe.
|
||||
</Callout>
|
||||
|
||||
In a browser or WebView, your app talks to that WASM client through JS bindings rather than direct calls. The mixnet behaviour is identical in both modes, only the integration shape differs. See [mix-* architecture](/developers/mix-architecture) for the full picture.
|
||||
|
||||

|
||||
|
||||
## Packages
|
||||
|
||||
### Rust
|
||||
|
||||
| Crate | Use it for |
|
||||
|---|---|
|
||||
| [`nym-sdk`](/developers/rust) | End-to-end mixnet messaging, `AsyncRead`/`AsyncWrite` byte streams, client pooling. Start with the [Tour](/developers/rust/tour). |
|
||||
| [`smolmix`](/developers/smolmix) | `TcpStream` and `UdpSocket` over the mixnet via a userspace IP stack. Compatible with `tokio-rustls`, `hyper`, `tokio-tungstenite`, and the rest of the async Rust ecosystem. |
|
||||
|
||||
### TypeScript
|
||||
|
||||
The four mix-* packages share one tunnel ([`mix-tunnel`](/developers/mix-tunnel)) and one WASM instance; install only what you need. See [mix-* architecture](/developers/mix-architecture) for how they're wired.
|
||||
|
||||
| Package | Use it for |
|
||||
|---|---|
|
||||
| [`mix-tunnel`](/developers/mix-tunnel) | The shared tunnel the three feature packages build on. Most apps don't import it directly. |
|
||||
| [`mix-fetch`](/developers/mix-fetch) | Drop-in `fetch()` for HTTP and HTTPS through the mixnet. |
|
||||
| [`mix-dns`](/developers/mix-dns) | Hostname-to-IP resolution through the mixnet. UDP DNS via the IPR. |
|
||||
| [`mix-websocket`](/developers/mix-websocket) | WebSocket-like class for WS and WSS through the mixnet. |
|
||||
| [TypeScript SDK](/developers/typescript) | `@nymproject/sdk`: end-to-end raw messaging when you control both ends. Smart contracts via `@nymproject/contract-clients`. |
|
||||
|
||||
### Standalone and other
|
||||
|
||||
| Resource | Use it for |
|
||||
|---|---|
|
||||
| [SOCKS5 / WebSocket clients](/developers/clients) | Language-agnostic binaries for piping traffic through the mixnet without an SDK. |
|
||||
| [Chain interaction](/developers/chain) | Query Nyx state, submit transactions, call Nym smart contracts. |
|
||||
| [APIs](/apis/introduction) | Auto-generated reference for Nym infrastructure HTTP endpoints. |
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
title: "Choosing an Approach"
|
||||
description: "Decide which Nym integration path fits your project. Compare nym-sdk, smolmix, mix-fetch, and the TypeScript SDK by runtime environment and architecture."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-05-12"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
# Choosing an Approach
|
||||
|
||||
Any application that integrates with Nym sends its traffic through the Mixnet via a Nym client. The right product depends on two factors: your **environment** (where your code runs) and your **architecture** (whether you control both sides of the communication).
|
||||
|
||||
## At a glance
|
||||
|
||||
| | **End-to-end** (both sides run Nym) | **Proxy mode** (Nym → clearnet exit) |
|
||||
|---|---|---|
|
||||
| **Rust** (native / desktop / CLI) | [`nym-sdk`](/developers/rust) (Stream, Mixnet, Client Pool) | [`smolmix`](/developers/smolmix) (TCP / UDP) · [`nym-sdk`](/developers/rust) SOCKS client |
|
||||
| **TypeScript** (browser) | [TypeScript SDK](/developers/typescript) (WASM Mixnet Client, messaging only) | [`mix-fetch`](/developers/mix-fetch) (HTTP) |
|
||||
| **Mobile** (iOS / Android) | via [`nym-vpn-client`](#mobile) (uniffi + cargo-swift / cargo-ndk) | via [`nym-vpn-client`](#mobile) (uniffi + cargo-swift / cargo-ndk) |
|
||||
|
||||
## Environment
|
||||
|
||||
Different runtimes have different transport constraints: a browser cannot open raw sockets or access the filesystem, while a desktop app can.
|
||||
|
||||
- **Native / Desktop / CLI**: full access to system networking and persistent storage. Use [`nym-sdk`](/developers/rust) (the Rust SDK) for E2E messaging or byte streams, or [`smolmix`](/developers/smolmix) for TCP/UDP socket-shaped access in proxy mode.
|
||||
- **Browser**: restricted to WebSockets, Web Transport, and `fetch`; HTTPS-only mixed-content rules; no filesystem access. Use [`mix-fetch`](/developers/mix-fetch) for HTTP(S) requests, or the [TypeScript SDK](/developers/typescript)'s WASM Mixnet Client for raw message passing.
|
||||
|
||||
### Mobile
|
||||
|
||||
There is no first-party mobile SDK, but [`nym-vpn-client`](https://github.com/nymtech/nym-vpn-client) ships production iOS and Android apps built around the Nym stack and is the reference we'd point you at. The relevant pieces are `nym-vpn-core/crates/nym-vpn-lib-uniffi` ([`uniffi`](https://mozilla.github.io/uniffi-rs/) FFI wrapper), `nym-vpn-core/iOS.mk` ([`cargo-swift`](https://github.com/antoniusnaumann/cargo-swift) → XCFramework + SwiftPM), and `nym-vpn-core/Android.mk` ([`cargo-ndk`](https://github.com/bbqsrc/cargo-ndk) → `jniLibs/`, driven from Gradle).
|
||||
|
||||
|
||||
If you try this and hit (or solve) blockers, drop a note in the [Nym dev channel on Matrix](https://matrix.to/#/#dev:nymtech.chat) or open an issue on [GitHub](https://github.com/nymtech/nym).
|
||||
|
||||
## Architecture
|
||||
|
||||
The second factor is whether you control both sides of the communication.
|
||||
|
||||
**End-to-end (E2E)**: both sides run Nym clients. All traffic stays Sphinx-encrypted the entire way. Appropriate for peer-to-peer setups or any case where you control both endpoints.
|
||||
|
||||

|
||||
|
||||
**Proxy**: only the client side runs Nym. Traffic exits the Mixnet at an Exit Gateway and continues to the destination over the public internet. The mixnet anonymises the sender; payload protection (TLS, Noise, etc.) is your application's job, as on a direct connection. Appropriate when connecting to third-party services such as blockchain RPCs or external APIs.
|
||||
|
||||

|
||||
|
||||
<Callout type="warning">
|
||||
Once traffic leaves the Exit Gateway, it travels over the public internet to the destination, exactly like any other server-initiated connection. The mixnet anonymises the sender but does not encrypt the payload past the gateway. Use TLS or another application-layer cipher as you would on a direct connection. See [Exit Gateway Services](/network/infrastructure/exit-services) for what the exit can and cannot observe.
|
||||
</Callout>
|
||||
|
||||
**Browser apps**: both proxy and E2E modes work slightly differently in a browser setting. The Nym client runs as a WASM blob inside a Web Worker, and your application communicates with it via JS bindings rather than direct function calls. The mixnet behaviour is identical; the integration shape differs.
|
||||
|
||||

|
||||
|
||||
## Where to go next
|
||||
|
||||
- **Rust, E2E messaging or byte streams**: [`nym-sdk`](/developers/rust)
|
||||
- **Rust, TCP/UDP socket replacements**: [`smolmix`](/developers/smolmix)
|
||||
- **Browser, HTTP(S) requests**: [`mix-fetch`](/developers/mix-fetch)
|
||||
- **Browser, raw mixnet messaging or Nyx smart contracts**: [TypeScript SDK](/developers/typescript)
|
||||
- **Background on Sphinx, gateways, and the mixnet itself**: [Key Concepts](/developers/concepts)
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: "mix-* Architecture: How the Browser Mixnet Packages Are Wired"
|
||||
description: "The shared browser architecture behind mix-tunnel, mix-fetch, mix-dns, and mix-websocket: one Web Worker, one WASM instance, a Comlink boundary, and a smoltcp + rustls stack reaching the mixnet via an IPR."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-05"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
# Architecture
|
||||
|
||||
The four mix-* packages ([`mix-tunnel`](/developers/mix-tunnel), [`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), and [`mix-websocket`](/developers/mix-websocket)) are not four independent clients. They are thin facades over a single shared tunnel, which runs in a Web Worker. This page explains that shared machinery once, so the per-package pages can stay focused on their own API.
|
||||
|
||||
```text
|
||||
Main thread (your app)
|
||||
mix-fetch, mix-dns, mix-websocket
|
||||
│ each re-exports mix-tunnel's controls and calls into it
|
||||
▼
|
||||
mix-tunnel → Comlink proxy
|
||||
│
|
||||
▼ postMessage (structured clone), into the worker
|
||||
Web Worker (one per page)
|
||||
smolmix-wasm
|
||||
├─ IPR client → WebSocket (WSS) to entry gateway
|
||||
├─ smoltcp (userspace TCP/IP stack)
|
||||
└─ rustls (TLS, Mozilla CA bundle compiled in)
|
||||
│
|
||||
▼
|
||||
Nym mixnet: entry → 3 mix layers → IPR exit → internet
|
||||
```
|
||||
|
||||
## The package family
|
||||
|
||||
Only `mix-tunnel` owns the tunnel. The three feature packages each depend on it and **re-export** its controls (`setupMixTunnel`, `disconnectMixTunnel`, and `getTunnelState`) alongside their own operation:
|
||||
|
||||
| Package | Adds | Re-exports from mix-tunnel |
|
||||
|---|---|---|
|
||||
| `mix-tunnel` | the tunnel itself | (owns them) |
|
||||
| `mix-fetch` | `mixFetch`, `createMixFetch` | setup / disconnect / state |
|
||||
| `mix-dns` | `mixDNS` | setup / disconnect / state |
|
||||
| `mix-websocket` | `MixWebSocket` | setup / disconnect / state |
|
||||
|
||||
So `import { setupMixTunnel, mixFetch } from '@nymproject/mix-fetch'` and `import { setupMixTunnel } from '@nymproject/mix-tunnel'` reach the **same** `setupMixTunnel`. You rarely import `mix-tunnel` directly; you get it transitively through whichever feature package you use.
|
||||
|
||||
## One tunnel, one WASM instance
|
||||
|
||||
The bundler deduplicates [`@nymproject/mix-tunnel`](https://www.npmjs.com/package/@nymproject/mix-tunnel) to a single module, so no matter how many feature packages a page imports, there is exactly one Web Worker and one `smolmix-wasm` instance. Any feature package reaches that same instance through `getMixTunnel`. Bringing the tunnel up with `setupMixTunnel`, though, is a one-time operation: the first call succeeds and a second rejects with `tunnel already initialised`, so call it once.
|
||||
|
||||
This is why the tunnel is configured once, at the first `setupMixTunnel`, and why options passed to a later call have no effect until teardown. It is also why a single connection to the entry gateway, a single IPR exit, and a single DNS cache are shared across all your `mixFetch` / `mixDNS` / `MixWebSocket` traffic.
|
||||
|
||||
## The worker boundary
|
||||
|
||||
The mixnet work (Sphinx packet construction, cover traffic, Poisson send timing, the smoltcp poll loop) is CPU-bound and must not block the UI thread. So `mix-tunnel` runs all of it in a Web Worker and talks to it over [Comlink](https://github.com/GoogleChromeLabs/comlink), which wraps `postMessage` in an async RPC. The main thread holds a `Comlink.Remote` proxy; every call (`mixFetch`, `mixDNS`, `ws.send`) is an `await` that hops the worker boundary. That boundary is the reason `MixWebSocket.send()` and `.close()` return promises where the browser `WebSocket` returns `void`.
|
||||
|
||||
<Callout type="info">
|
||||
**The `proxy` re-export.** `mix-websocket` needs to pass a message callback *into* the worker. Comlink marks a value as "transfer this by proxy, not by clone" using a `Symbol` that is created per module instance. If `mix-websocket` bundled its own copy of Comlink, that symbol would not match the one the worker-owning module's serialiser checks for, and the callback would fall through to structured clone, which cannot clone functions, so it throws. To avoid that, `mix-tunnel` re-exports `proxy`, and `mix-websocket` imports it from there, so both sides share one Comlink instance and the marker symbol matches.
|
||||
</Callout>
|
||||
|
||||
## Inside the worker
|
||||
|
||||
The worker hosts `smolmix-wasm`, the WebAssembly build of the Rust [`smolmix`](/developers/smolmix) crate. Three pieces do the work:
|
||||
|
||||
- **IPR client**: opens a WebSocket (WSS by default, via `forceTls`) to a Nym entry gateway and speaks the mixnet protocol, exiting at an [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router).
|
||||
- **`smoltcp`**: a userspace TCP/IP stack. Because the browser exposes no raw sockets, smolmix runs its own TCP and UDP over the mixnet's IP transport. A reactor polls smoltcp and wakes the relevant tasks when data arrives.
|
||||
- **`rustls`**: terminates TLS for `https://` and `wss://` end-to-end with the destination, with the Mozilla CA bundle compiled into the WASM. The IPR sees only ciphertext for encrypted targets.
|
||||
|
||||
The tunnel is **one-shot per WASM instance**: `setupMixTunnel` can initialise it once. After `disconnectMixTunnel`, the instance is spent and the page must reload to build a new tunnel.
|
||||
|
||||
## Tunnel lifecycle
|
||||
|
||||
```text
|
||||
(no tunnel) ──setupMixTunnel()──▶ connecting ──▶ ready ──┐
|
||||
│ mixFetch / mixDNS / MixWebSocket
|
||||
▼
|
||||
shutdown ◀──disconnectMixTunnel()── (still ready)
|
||||
```
|
||||
|
||||
`getTunnelState()` reflects this as `connecting | ready | shutting_down | shutdown | failed` (see [tunnel state](/developers/mix-tunnel/guides#tunnel-state); the diagram shows the happy path, `shutting_down` is the transient during teardown and `failed` carries a `reason`). The transitions are coarse; the fine-grained gateway, IPR-discovery, and smoltcp events are logged to the browser console when the tunnel is brought up with `debug: true`.
|
||||
|
||||
## Going deeper
|
||||
|
||||
- The native crate and its design: [`smolmix`](/developers/smolmix).
|
||||
- The package sources, including bundler/WASM-inlining specifics and the worker plumbing: [`sdk/typescript/packages`](https://github.com/nymtech/nym/tree/develop/sdk/typescript/packages) and the WASM crate under [`wasm/smolmix`](https://github.com/nymtech/nym/tree/develop/wasm/smolmix).
|
||||
- See it run: the [mixnet playground](/developers/playground).
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: "mix-dns: Hostname Resolution Over the Nym Mixnet"
|
||||
description: "TypeScript package that resolves hostnames through the Nym mixnet as UDP DNS via an IPR exit gateway."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-05"
|
||||
---
|
||||
|
||||
# mix-dns
|
||||
|
||||
[`@nymproject/mix-dns`](https://www.npmjs.com/package/@nymproject/mix-dns) resolves hostnames to IPs through the Nym mixnet. The query travels as a UDP datagram to a public resolver via the [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) exit gateway, not through the browser or OS resolver.
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Your browser app │
|
||||
│ └─ mixDNS('example.com') │
|
||||
│ └─ mix-tunnel (smolmix-wasm, Web Worker) │
|
||||
│ └─ UDP datagram via the IPR │
|
||||
│ └─ public resolver (default 8.8.8.8:53) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The resolver sees a query from the IPR's IP, not yours, and the browser's own resolver path (the OS stub resolver, any local DoH) is bypassed entirely.
|
||||
|
||||
## When to use it
|
||||
|
||||
`mix-dns` is for cases where you need the resolved IP itself, not a connection that uses it. [`mix-fetch`](/developers/mix-fetch) and [`mix-websocket`](/developers/mix-websocket) already resolve via the mixnet internally; you don't need to call `mixDNS` before either.
|
||||
|
||||
Direct uses:
|
||||
|
||||
- Validate that a hostname resolves to an expected IP range before connecting through any path.
|
||||
- Build IP-based allow / deny lists for an app that performs the connection itself.
|
||||
- Probe whether a hostname is reachable from the IPR exit's perspective without opening a connection.
|
||||
|
||||
## In this section
|
||||
|
||||
- [Get started](/developers/mix-dns/get-started): install and resolve your first hostname.
|
||||
- [Reference & security](/developers/mix-dns/guides): configure the resolver, and what it sees.
|
||||
- [TypeDoc reference](/developers/mix-dns/api/globals): generated from the source.
|
||||
- [Browser example](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-dns/browser): a runnable example app.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"get-started": "Get started",
|
||||
"guides": "Reference & security",
|
||||
"api": "TypeDoc Reference"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"globals": "API Index",
|
||||
"functions": "Functions",
|
||||
"interfaces": "Interfaces"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"disconnectMixTunnel": "disconnectMixTunnel",
|
||||
"getTunnelState": "getTunnelState",
|
||||
"mixDNS": "mixDNS",
|
||||
"setupMixTunnel": "setupMixTunnel"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[**@nymproject/mix-dns**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-dns](../globals.md) / disconnectMixTunnel
|
||||
|
||||
# Function: disconnectMixTunnel()
|
||||
|
||||
> **disconnectMixTunnel**(): `Promise`\<`void`\>
|
||||
|
||||
Tear the tunnel down. After this, the WASM is unusable until page reload.
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`void`\>
|
||||
|
||||
## Source
|
||||
|
||||
mix-tunnel/dist/esm/index.d.ts:16
|
||||
@@ -0,0 +1,19 @@
|
||||
[**@nymproject/mix-dns**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-dns](../globals.md) / getTunnelState
|
||||
|
||||
# Function: getTunnelState()
|
||||
|
||||
> **getTunnelState**(): `Promise`\<`TunnelState`\>
|
||||
|
||||
Inspect the current tunnel state. Pre-setup reads as `connecting`.
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`TunnelState`\>
|
||||
|
||||
## Source
|
||||
|
||||
mix-tunnel/dist/esm/index.d.ts:18
|
||||
@@ -0,0 +1,26 @@
|
||||
[**@nymproject/mix-dns**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-dns](../globals.md) / mixDNS
|
||||
|
||||
# Function: mixDNS()
|
||||
|
||||
> **mixDNS**(`hostname`): `Promise`\<`string`\>
|
||||
|
||||
Resolve a hostname through the mixnet. Returns the IP as a string
|
||||
(e.g. `"93.184.216.34"`).
|
||||
|
||||
The tunnel must already be set up via `setupMixTunnel()`.
|
||||
|
||||
## Parameters
|
||||
|
||||
• **hostname**: `string`
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`string`\>
|
||||
|
||||
## Source
|
||||
|
||||
[mix-dns/src/index.ts:23](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-dns/src/index.ts#L23)
|
||||
@@ -0,0 +1,23 @@
|
||||
[**@nymproject/mix-dns**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-dns](../globals.md) / setupMixTunnel
|
||||
|
||||
# Function: setupMixTunnel()
|
||||
|
||||
> **setupMixTunnel**(`opts`?): `Promise`\<`void`\>
|
||||
|
||||
Initialise the mixnet tunnel. Idempotent; safe to call from multiple feature packages.
|
||||
|
||||
## Parameters
|
||||
|
||||
• **opts?**: [`SetupMixTunnelOpts`](../interfaces/SetupMixTunnelOpts.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`void`\>
|
||||
|
||||
## Source
|
||||
|
||||
mix-tunnel/dist/esm/index.d.ts:14
|
||||
@@ -0,0 +1,16 @@
|
||||
**@nymproject/mix-dns** • [**Docs**](globals.md)
|
||||
|
||||
***
|
||||
|
||||
# @nymproject/mix-dns
|
||||
|
||||
## Interfaces
|
||||
|
||||
- [SetupMixTunnelOpts](interfaces/SetupMixTunnelOpts.md)
|
||||
|
||||
## Functions
|
||||
|
||||
- [mixDNS](functions/mixDNS.md)
|
||||
- [setupMixTunnel](functions/setupMixTunnel.md)
|
||||
- [disconnectMixTunnel](functions/disconnectMixTunnel.md)
|
||||
- [getTunnelState](functions/getTunnelState.md)
|
||||
@@ -0,0 +1,167 @@
|
||||
[**@nymproject/mix-dns**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-dns](../globals.md) / SetupMixTunnelOpts
|
||||
|
||||
# Interface: SetupMixTunnelOpts
|
||||
|
||||
## Properties
|
||||
|
||||
### preferredIpr?
|
||||
|
||||
> `optional` **preferredIpr**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:2
|
||||
|
||||
***
|
||||
|
||||
### clientId?
|
||||
|
||||
> `optional` **clientId**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:3
|
||||
|
||||
***
|
||||
|
||||
### forceTls?
|
||||
|
||||
> `optional` **forceTls**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:4
|
||||
|
||||
***
|
||||
|
||||
### disablePoissonTraffic?
|
||||
|
||||
> `optional` **disablePoissonTraffic**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:5
|
||||
|
||||
***
|
||||
|
||||
### disableCoverTraffic?
|
||||
|
||||
> `optional` **disableCoverTraffic**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:6
|
||||
|
||||
***
|
||||
|
||||
### openReplySurbs?
|
||||
|
||||
> `optional` **openReplySurbs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:7
|
||||
|
||||
***
|
||||
|
||||
### dataReplySurbs?
|
||||
|
||||
> `optional` **dataReplySurbs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:8
|
||||
|
||||
***
|
||||
|
||||
### primaryDns?
|
||||
|
||||
> `optional` **primaryDns**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:9
|
||||
|
||||
***
|
||||
|
||||
### fallbackDns?
|
||||
|
||||
> `optional` **fallbackDns**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:10
|
||||
|
||||
***
|
||||
|
||||
### storagePassphrase?
|
||||
|
||||
> `optional` **storagePassphrase**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:11
|
||||
|
||||
***
|
||||
|
||||
### connectTimeoutMs?
|
||||
|
||||
> `optional` **connectTimeoutMs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:12
|
||||
|
||||
***
|
||||
|
||||
### dnsTimeoutMs?
|
||||
|
||||
> `optional` **dnsTimeoutMs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:13
|
||||
|
||||
***
|
||||
|
||||
### tcpKeepaliveMs?
|
||||
|
||||
> `optional` **tcpKeepaliveMs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:14
|
||||
|
||||
***
|
||||
|
||||
### tcpBufferSize?
|
||||
|
||||
> `optional` **tcpBufferSize**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:15
|
||||
|
||||
***
|
||||
|
||||
### maxRedirects?
|
||||
|
||||
> `optional` **maxRedirects**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:16
|
||||
|
||||
***
|
||||
|
||||
### debug?
|
||||
|
||||
> `optional` **debug**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:17
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"SetupMixTunnelOpts": "SetupMixTunnelOpts"
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: "Get started with mix-dns"
|
||||
description: "Install @nymproject/mix-dns and resolve a hostname through the Nym mixnet."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-05"
|
||||
---
|
||||
|
||||
# Get started
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @nymproject/mix-dns
|
||||
```
|
||||
|
||||
ESM only, with the worker and WASM inlined via [`mix-tunnel`](/developers/mix-tunnel/get-started#installation); no bundler config needed.
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { setupMixTunnel, mixDNS, disconnectMixTunnel } from '@nymproject/mix-dns';
|
||||
|
||||
await setupMixTunnel();
|
||||
|
||||
const ip = await mixDNS('example.com');
|
||||
console.log(ip); // e.g. an IPv4 address string
|
||||
|
||||
await disconnectMixTunnel();
|
||||
```
|
||||
|
||||
`mixDNS` returns the first resolved address as a string: an IPv4 A record when available, otherwise IPv6. It rejects if the hostname cannot be resolved. To resolve and immediately use the result via `mixFetch`, the simpler path is to skip `mixDNS` entirely and call `mixFetch('https://example.com')`, which handles resolution itself.
|
||||
|
||||
Resolve hostnames live in the [mixnet playground](/developers/playground), with a tunnel-vs-clearnet (DoH) comparison.
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: "mix-dns reference & security"
|
||||
description: "Configure the DNS resolver used by mix-dns, and what the resolver sees through the IPR exit."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-09"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
# Reference & security
|
||||
|
||||
## Configuration
|
||||
|
||||
The DNS resolver is configured at tunnel setup, not per-call. Pass the resolver in `setupMixTunnel`:
|
||||
|
||||
```ts
|
||||
await setupMixTunnel({
|
||||
// Set the resolver explicitly. Defaults are 8.8.8.8:53 primary and
|
||||
// 1.1.1.1:53 fallback. Both fields take a `host:port` socket address;
|
||||
// fallbackDns is used if the primary fails to respond.
|
||||
primaryDns: '8.8.8.8:53',
|
||||
fallbackDns: '1.1.1.1:53',
|
||||
});
|
||||
```
|
||||
|
||||
The full options surface is documented under [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts).
|
||||
|
||||
## Security model
|
||||
|
||||
`mix-dns` follows the shared [mixnet exit security model](/developers/concepts/exit-security). The transport-specific exposure: at the IPR exit the query leaves as a plain UDP DNS request to the resolver, so the resolver sees the queried hostname and the IPR's IP, never yours. There is no TLS to terminate; the query and response are plaintext on the IPR-to-resolver leg.
|
||||
|
||||
<Callout type="warning">
|
||||
At the resolver the query is plaintext UDP. The resolver can read the hostname you are looking up, while the mixnet keeps it from learning who you are. Choosing `8.8.8.8` vs `1.1.1.1` only changes which third party sees the queries; both see them coming from the IPR. To remove the resolver from your trust set, pick one you already trust, or layer DNS-over-HTTPS via `mixFetch` to a DoH endpoint instead of `mixDNS`.
|
||||
</Callout>
|
||||
@@ -1,130 +1,34 @@
|
||||
---
|
||||
title: "mix-fetch: fetch() Over the Nym Mixnet"
|
||||
description: "Package providing a fetch()-compatible API that routes HTTP(S) requests through the Nym mixnet via a Network Requester. Available for browsers and Node.js."
|
||||
description: "Drop-in fetch() replacement that routes HTTP and HTTPS requests through the Nym mixnet via an IPR exit gateway."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-05-12"
|
||||
lastUpdated: "2026-06-05"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
# mix-fetch
|
||||
|
||||
`mix-fetch` is a replacement for [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) that routes HTTP(S) requests through the Nym mixnet. The call signature is identical; underneath, the request is tunnelled through a WASM Nym client to a Network Requester (a Nym service provider, typically operated by an Exit Gateway), which decodes a SOCKS5-shaped connect request and opens a TCP connection to the destination. TLS runs end-to-end between the WASM bundle and the destination server.
|
||||
|
||||
Available for browsers and Node.js, with the WASM core shared between both.
|
||||
[`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch) routes HTTP and HTTPS through the Nym mixnet behind the browser [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) signature: `mixFetch(url, init)` returns the same `Response` you would get from `fetch(url, init)`. The request travels mixnet hops first, exits at an [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) gateway, and reaches the destination with the IPR's IP, not yours. It is not a perfect substitute for `fetch`: no cookies or credentials, no HTTP cache, no `AbortController`, and HTTPS-only in practice (plain HTTP is fully visible at the exit; see [drop-in caveats](/developers/mix-fetch/guides#drop-in-caveats)).
|
||||
|
||||
```text
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ Your app (browser or Node.js) │
|
||||
│ └─ mixFetch('https://...') (fetch() replacement) │
|
||||
│ └─ Go-WASM HTTP/TLS client (embedded Mozilla CA bundle) │
|
||||
│ └─ Rust-WASM SOCKS5 framing + Nym mixnet transport │
|
||||
│ └─ WebSocket → entry gateway │
|
||||
│ └─ mixnet (Sphinx, 3 mix hops by default) │
|
||||
│ └─ Network Requester decodes SOCKS5 │
|
||||
│ request, opens TCP to dest │
|
||||
│ └─ destination server │
|
||||
│ (TLS handshake here) │
|
||||
│ Your browser app │
|
||||
│ └─ mixFetch('https://...') │
|
||||
│ └─ mix-tunnel (shared singleton, Web Worker, smolmix-wasm) │
|
||||
│ └─ smoltcp userspace TCP/IP + rustls TLS │
|
||||
│ └─ WebSocket to entry gateway │
|
||||
│ └─ Nym mixnet (3 mix layers) │
|
||||
│ └─ IPR exit gateway → destination │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Two WASM modules sit in the bundle: a Go module that handles HTTP and TLS (Go's `crypto/tls` compiled to WASM, with an embedded Mozilla root CA list), and a Rust module that handles SOCKS5 request framing and the Nym mixnet client itself. They communicate via JS bindings.
|
||||
TLS terminates end-to-end between the WASM bundle and the destination server. The IPR sees destination IP and port; for HTTPS targets, payload is TLS ciphertext.
|
||||
|
||||
Because TLS terminates at the destination (not at the Network Requester or any node before it), every hop from the entry gateway onwards only sees TLS ciphertext for HTTPS targets. This is the same trust model as a normal HTTPS request through a SOCKS proxy.
|
||||
## In this section
|
||||
|
||||
<Callout type="info">
|
||||
The "SOCKS5" framing here is Nym's binary `Socks5Request` format wrapped in Sphinx packets, not RFC 1928 SOCKS5 over a TCP socket. The Network Requester decodes it on the mixnet side and proxies onwards as regular TCP.
|
||||
</Callout>
|
||||
|
||||
## Runtime and platform support
|
||||
|
||||
### Browser
|
||||
|
||||
The WASM core runs in a Web Worker and needs:
|
||||
- WebSocket support, for the entry-gateway connection
|
||||
- WebAssembly
|
||||
- A CSP that permits `wss://` connections and `worker-src 'self'` (or `blob:` for the `*-full-fat` variants, which load workers as inline blobs)
|
||||
|
||||
Mixed-content rules apply: target URLs must be HTTPS.
|
||||
|
||||
### Node.js
|
||||
|
||||
The same WASM core runs in a `worker_threads` worker. The `ws` package polyfills `WebSocket`, and a Node-flavoured `comlink` adapter (`mix-fetch-node/src/node-adapter.ts`) bridges `worker_threads` to the same Worker-like API surface.
|
||||
|
||||
## Installation
|
||||
|
||||
### Browser variants
|
||||
|
||||
| Variant | Package | When to use |
|
||||
|---|---|---|
|
||||
| ESM | `@nymproject/mix-fetch` | Modern project, you can configure your bundler |
|
||||
| ESM full-fat | `@nymproject/mix-fetch-full-fat` | Modern project, can't configure your bundler |
|
||||
| CommonJS | `@nymproject/mix-fetch-commonjs` | Legacy project, you can configure your bundler |
|
||||
| CommonJS full-fat | `@nymproject/mix-fetch-commonjs-full-fat` | Legacy project, can't configure your bundler |
|
||||
|
||||
### Node.js variant
|
||||
|
||||
| Variant | Package | When to use |
|
||||
|---|---|---|
|
||||
| CommonJS | `@nymproject/mix-fetch-node-commonjs` | Node.js (currently the only published Node variant) |
|
||||
|
||||
The standard browser variants need your bundler to handle WASM and web workers (see [Bundling](/developers/typescript/bundling)). The `*-full-fat` variants inline both as Base64 so no bundler configuration is needed.
|
||||
|
||||
<Callout type="warning">
|
||||
The `*-full-fat` variants are large (~18 MB), since they inline ~10 MB of WASM (Go runtime + Rust core) and the web-worker source as Base64. Prefer a standard variant if bundle size matters.
|
||||
</Callout>
|
||||
|
||||
```bash
|
||||
# Browser
|
||||
npm install @nymproject/mix-fetch-full-fat
|
||||
|
||||
# Node.js
|
||||
npm install @nymproject/mix-fetch-node-commonjs
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
`mixFetch` caps concurrent connections at **10 per destination host** (Go `http.Transport`'s `MaxConnsPerHost`, see `wasm/mix-fetch/go-mix-conn/internal/mixfetch/mixfetch.go:214`). Keep-alive is disabled, so each request opens a fresh TCP connection through the mixnet; extra concurrent requests to the same host queue until a slot frees. Different hosts are independent.
|
||||
</Callout>
|
||||
|
||||
## Playground and examples
|
||||
|
||||
See the [interactive playground](/developers/typescript/playground/mixfetch) for a working `mixFetch` example you can run in the browser.
|
||||
|
||||
The first call bootstraps the WASM Nym client (gateway handshake, key generation, cover traffic). Subsequent calls reuse the active client; the Rust side holds it in a `OnceLock` singleton, so there is one client per page (or per Node process).
|
||||
|
||||
## When to use mix-fetch
|
||||
|
||||
| | mix-fetch | WASM Mixnet Client | smolmix | Plain fetch (no mixnet) |
|
||||
|---|---|---|---|---|
|
||||
| **Runtime** | Browser, Node.js | Browser | Native (Rust) | Anywhere |
|
||||
| **Architecture** | Proxy (Network Requester → destination) | E2E (both sides Nym) | Proxy | Direct |
|
||||
| **API shape** | `fetch()` replacement | Send/recv text or binary messages | `TcpStream` / `UdpSocket` | `fetch()` |
|
||||
| **HTTP support** | Yes | No | Yes (via `hyper` over `TcpStream`) | Yes |
|
||||
| **Sender unlinkability** | Strong (mixnet) | Strong (mixnet) | Strong (mixnet) | None |
|
||||
| **Concurrency** | 10 per host | Unlimited | Unlimited | Unlimited |
|
||||
|
||||
## Security model
|
||||
|
||||
<Callout type="warning">
|
||||
Use HTTPS targets. Plaintext HTTP requests are visible to the Network Requester and to any router between it and the destination.
|
||||
</Callout>
|
||||
|
||||
### What's protected
|
||||
|
||||
| Segment | Mixnet encryption | What's visible |
|
||||
|---|---|---|
|
||||
| App → entry gateway | Sphinx (layered) over a WebSocket | Entry gateway sees your IP, not the destination |
|
||||
| Inside the mixnet | Sphinx (layered) | Each node only knows previous / next hop |
|
||||
| Network Requester | Sphinx removed; SOCKS5 connect request decoded | The Requester sees destination hostname + port; payload is application-layer TLS |
|
||||
| Network Requester → destination | None (regular TCP) | TLS handshake + ciphertext (with HTTPS targets); cleartext (with HTTP targets) |
|
||||
|
||||
### Why mix-fetch ships its own CA store
|
||||
|
||||
The browser's TLS stack and CA store aren't accessible from JavaScript or from a WASM SOCKS client; on Node, the TLS stack lives outside the Web Worker that hosts the mixnet client. `mix-fetch` therefore performs TLS itself, inside the WASM bundle, against the destination server. The bundle ships with an embedded Mozilla root CA list (refreshed from [curl.se's bundle](https://curl.se/docs/caextract.html), verified by SHA-256 in `wasm/mix-fetch/go-mix-conn/scripts/update-root-certs.sh`) and an in-WASM TLS implementation (Go's `crypto/tls`, configured at `wasm/mix-fetch/go-mix-conn/internal/sslhelpers/ssl_helper.go`). The mixnet path sees encrypted TLS ciphertext, not plaintext.
|
||||
|
||||
`mix-fetch` handles the TLS layer for you: HTTPS targets are protected end-to-end between the WASM bundle and the destination, as if a browser had initiated the TLS handshake directly. Plaintext HTTP targets remain visible to the Network Requester and to any router beyond it. See [Exit Gateway Services](/network/infrastructure/exit-services) for what the exit can and cannot observe.
|
||||
|
||||
## API reference
|
||||
|
||||
Generated reference: [typedoc output](/developers/typescript/api/mix-fetch).
|
||||
- [Get started](/developers/mix-fetch/get-started): install and make your first mixnet request.
|
||||
- [Reference](/developers/mix-fetch/guides): request shape, default headers, drop-in caveats, configuration.
|
||||
- [Concepts & security](/developers/mix-fetch/concepts): what the IPR exit sees.
|
||||
- [Migrating from v1.x](/developers/mix-fetch/migration): the v1 to v2 clean break.
|
||||
- [TypeDoc reference](/developers/mix-fetch/api/globals): generated from the source.
|
||||
- [Browser example](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-fetch/browser): a runnable example app.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"get-started": "Get started",
|
||||
"guides": "Reference",
|
||||
"concepts": "Concepts & security",
|
||||
"migration": "Migrating from v1.x",
|
||||
"api": "TypeDoc Reference"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"globals": "API Index",
|
||||
"functions": "Functions",
|
||||
"interfaces": "Interfaces"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"createMixFetch": "createMixFetch",
|
||||
"disconnectMixTunnel": "disconnectMixTunnel",
|
||||
"getTunnelState": "getTunnelState",
|
||||
"mixFetch": "mixFetch",
|
||||
"setupMixTunnel": "setupMixTunnel"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
[**@nymproject/mix-fetch**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-fetch](../globals.md) / createMixFetch
|
||||
|
||||
# Function: createMixFetch()
|
||||
|
||||
> **createMixFetch**(`opts`?): `Promise`\<(`url`, `init`?) => `Promise`\<`Response`\>\>
|
||||
|
||||
Convenience: set up the tunnel and return a fetch-bound function. Equivalent
|
||||
to `await setupMixTunnel(opts); return mixFetch;`. Safe to call multiple
|
||||
times; the underlying tunnel is a singleton.
|
||||
|
||||
## Parameters
|
||||
|
||||
• **opts?**: [`SetupMixTunnelOpts`](../interfaces/SetupMixTunnelOpts.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<(`url`, `init`?) => `Promise`\<`Response`\>\>
|
||||
|
||||
## Source
|
||||
|
||||
[mix-fetch/src/index.ts:62](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-fetch/src/index.ts#L62)
|
||||
@@ -0,0 +1,19 @@
|
||||
[**@nymproject/mix-fetch**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-fetch](../globals.md) / disconnectMixTunnel
|
||||
|
||||
# Function: disconnectMixTunnel()
|
||||
|
||||
> **disconnectMixTunnel**(): `Promise`\<`void`\>
|
||||
|
||||
Tear the tunnel down. After this, the WASM is unusable until page reload.
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`void`\>
|
||||
|
||||
## Source
|
||||
|
||||
mix-tunnel/dist/esm/index.d.ts:16
|
||||
@@ -0,0 +1,19 @@
|
||||
[**@nymproject/mix-fetch**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-fetch](../globals.md) / getTunnelState
|
||||
|
||||
# Function: getTunnelState()
|
||||
|
||||
> **getTunnelState**(): `Promise`\<`TunnelState`\>
|
||||
|
||||
Inspect the current tunnel state. Pre-setup reads as `connecting`.
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`TunnelState`\>
|
||||
|
||||
## Source
|
||||
|
||||
mix-tunnel/dist/esm/index.d.ts:18
|
||||
@@ -0,0 +1,28 @@
|
||||
[**@nymproject/mix-fetch**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-fetch](../globals.md) / mixFetch
|
||||
|
||||
# Function: mixFetch()
|
||||
|
||||
> **mixFetch**(`url`, `init`?): `Promise`\<`Response`\>
|
||||
|
||||
Fetch over the mixnet. Drop-in replacement for the browser `fetch()`.
|
||||
|
||||
Requires the tunnel to be up: call `setupMixTunnel(opts)` first, or use
|
||||
`createMixFetch(opts)` to combine setup + fetch.
|
||||
|
||||
## Parameters
|
||||
|
||||
• **url**: `string`
|
||||
|
||||
• **init?**: `RequestInit`
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`Response`\>
|
||||
|
||||
## Source
|
||||
|
||||
[mix-fetch/src/index.ts:39](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-fetch/src/index.ts#L39)
|
||||
@@ -0,0 +1,23 @@
|
||||
[**@nymproject/mix-fetch**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-fetch](../globals.md) / setupMixTunnel
|
||||
|
||||
# Function: setupMixTunnel()
|
||||
|
||||
> **setupMixTunnel**(`opts`?): `Promise`\<`void`\>
|
||||
|
||||
Initialise the mixnet tunnel. Idempotent; safe to call from multiple feature packages.
|
||||
|
||||
## Parameters
|
||||
|
||||
• **opts?**: [`SetupMixTunnelOpts`](../interfaces/SetupMixTunnelOpts.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`void`\>
|
||||
|
||||
## Source
|
||||
|
||||
mix-tunnel/dist/esm/index.d.ts:14
|
||||
@@ -0,0 +1,17 @@
|
||||
**@nymproject/mix-fetch** • [**Docs**](globals.md)
|
||||
|
||||
***
|
||||
|
||||
# @nymproject/mix-fetch
|
||||
|
||||
## Interfaces
|
||||
|
||||
- [SetupMixTunnelOpts](interfaces/SetupMixTunnelOpts.md)
|
||||
|
||||
## Functions
|
||||
|
||||
- [mixFetch](functions/mixFetch.md)
|
||||
- [createMixFetch](functions/createMixFetch.md)
|
||||
- [setupMixTunnel](functions/setupMixTunnel.md)
|
||||
- [disconnectMixTunnel](functions/disconnectMixTunnel.md)
|
||||
- [getTunnelState](functions/getTunnelState.md)
|
||||
@@ -0,0 +1,167 @@
|
||||
[**@nymproject/mix-fetch**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-fetch](../globals.md) / SetupMixTunnelOpts
|
||||
|
||||
# Interface: SetupMixTunnelOpts
|
||||
|
||||
## Properties
|
||||
|
||||
### preferredIpr?
|
||||
|
||||
> `optional` **preferredIpr**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:2
|
||||
|
||||
***
|
||||
|
||||
### clientId?
|
||||
|
||||
> `optional` **clientId**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:3
|
||||
|
||||
***
|
||||
|
||||
### forceTls?
|
||||
|
||||
> `optional` **forceTls**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:4
|
||||
|
||||
***
|
||||
|
||||
### disablePoissonTraffic?
|
||||
|
||||
> `optional` **disablePoissonTraffic**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:5
|
||||
|
||||
***
|
||||
|
||||
### disableCoverTraffic?
|
||||
|
||||
> `optional` **disableCoverTraffic**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:6
|
||||
|
||||
***
|
||||
|
||||
### openReplySurbs?
|
||||
|
||||
> `optional` **openReplySurbs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:7
|
||||
|
||||
***
|
||||
|
||||
### dataReplySurbs?
|
||||
|
||||
> `optional` **dataReplySurbs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:8
|
||||
|
||||
***
|
||||
|
||||
### primaryDns?
|
||||
|
||||
> `optional` **primaryDns**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:9
|
||||
|
||||
***
|
||||
|
||||
### fallbackDns?
|
||||
|
||||
> `optional` **fallbackDns**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:10
|
||||
|
||||
***
|
||||
|
||||
### storagePassphrase?
|
||||
|
||||
> `optional` **storagePassphrase**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:11
|
||||
|
||||
***
|
||||
|
||||
### connectTimeoutMs?
|
||||
|
||||
> `optional` **connectTimeoutMs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:12
|
||||
|
||||
***
|
||||
|
||||
### dnsTimeoutMs?
|
||||
|
||||
> `optional` **dnsTimeoutMs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:13
|
||||
|
||||
***
|
||||
|
||||
### tcpKeepaliveMs?
|
||||
|
||||
> `optional` **tcpKeepaliveMs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:14
|
||||
|
||||
***
|
||||
|
||||
### tcpBufferSize?
|
||||
|
||||
> `optional` **tcpBufferSize**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:15
|
||||
|
||||
***
|
||||
|
||||
### maxRedirects?
|
||||
|
||||
> `optional` **maxRedirects**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:16
|
||||
|
||||
***
|
||||
|
||||
### debug?
|
||||
|
||||
> `optional` **debug**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
mix-tunnel/dist/esm/types.d.ts:17
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"SetupMixTunnelOpts": "SetupMixTunnelOpts"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: "mix-fetch concepts & security"
|
||||
description: "What the IPR exit sees when you route HTTP through mix-fetch, and what TLS keeps private."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-05"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
# Concepts & security
|
||||
|
||||
## Security model
|
||||
|
||||
`mix-fetch` follows the shared [mixnet exit security model](/developers/concepts/exit-security): the IPR exit sees your destination, and you rely on TLS to keep the payload as ciphertext to it. What that means specifically for HTTP/S:
|
||||
|
||||
| At the IPR exit | What's visible |
|
||||
|---|---|
|
||||
| HTTPS (`https://`) | Destination IP and port. Payload is TLS ciphertext, terminating at the destination rather than the IPR. |
|
||||
| HTTP (`http://`) | Destination IP and port, plus the full request and response in plaintext. |
|
||||
|
||||
<Callout type="warning">
|
||||
TLS terminates inside the WASM instance (via [`rustls`](https://docs.rs/rustls) in smolmix-wasm), not in the browser. The Mozilla CA bundle is compiled into the WASM. Mixed content rules still apply at the page level, so serve your app over HTTPS.
|
||||
</Callout>
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: "Get started with mix-fetch"
|
||||
description: "Install @nymproject/mix-fetch and make your first HTTP request through the Nym mixnet."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-05"
|
||||
---
|
||||
|
||||
# Get started
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @nymproject/mix-fetch
|
||||
```
|
||||
|
||||
ESM only, with the worker and WASM inlined via [`mix-tunnel`](/developers/mix-tunnel/get-started#installation); no bundler config needed.
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { setupMixTunnel, mixFetch, disconnectMixTunnel } from '@nymproject/mix-fetch';
|
||||
|
||||
// Bring the shared mixnet tunnel up. Same call works from mix-dns and mix-websocket.
|
||||
await setupMixTunnel();
|
||||
|
||||
// Drop-in fetch. The Response is the real DOM Response, not a wrapper.
|
||||
const res = await mixFetch('https://example.com');
|
||||
console.log(res.status, await res.text());
|
||||
|
||||
// Tear down. The WASM is unusable after this until page reload.
|
||||
await disconnectMixTunnel();
|
||||
```
|
||||
|
||||
For one-shot use without an explicit setup step, the `createMixFetch` helper combines setup and fetch:
|
||||
|
||||
```ts
|
||||
import { createMixFetch } from '@nymproject/mix-fetch';
|
||||
|
||||
const mixFetch = await createMixFetch({ disableCoverTraffic: true });
|
||||
const res = await mixFetch('https://example.com');
|
||||
```
|
||||
|
||||
Call `createMixFetch` once and reuse the function it returns. It calls `setupMixTunnel` internally, so calling `createMixFetch` a second time rejects with `tunnel already initialised`; the tunnel is [one-shot per page](/developers/mix-tunnel/get-started).
|
||||
|
||||
Run `mixFetch` live in the [mixnet playground](/developers/playground), with a tunnel-vs-clearnet comparison.
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
title: "mix-fetch guides"
|
||||
description: "Request shape, default headers, drop-in caveats, and tunnel configuration for mix-fetch."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-05"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
# Reference
|
||||
|
||||
## Request shape
|
||||
|
||||
The `init` argument is the standard [`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit). Headers, method, and body all work. `AbortController` (`signal`) is not supported: an in-flight request cannot be cancelled.
|
||||
|
||||
```ts
|
||||
const res = await mixFetch('https://httpbin.org/post', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ hello: 'mixnet' }),
|
||||
});
|
||||
console.log(await res.json());
|
||||
```
|
||||
|
||||
Binary responses come back via the standard `Response.arrayBuffer()` / `Response.blob()` methods:
|
||||
|
||||
```ts
|
||||
const res = await mixFetch('https://example.com/image.png');
|
||||
const blob = await res.blob();
|
||||
```
|
||||
|
||||
Repeated headers (`Set-Cookie`, `Vary`, `Link`, `WWW-Authenticate`) are preserved. The wasm side returns headers as a `[name, value]` pair sequence, which `Headers` reconstructs verbatim.
|
||||
|
||||
## Default request headers
|
||||
|
||||
When the caller doesn't set them, `mixFetch` injects four browser-shape headers before the request leaves the tunnel. The shim exists because many CDNs (cloudflare's bot management) and host policies (wikimedia's User-Agent policy) reject requests that look unlike a real browser. Caller-supplied values always win.
|
||||
|
||||
| Header | Injected default |
|
||||
|--------|------------------|
|
||||
| `User-Agent` | `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36` |
|
||||
| `Accept` | `text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8` |
|
||||
| `Accept-Language` | `en-US,en;q=0.9` |
|
||||
| `Accept-Encoding` | `identity` |
|
||||
|
||||
`Accept-Encoding` is forced to `identity` rather than `gzip, deflate, br` because the wasm build has no decompressor. Advertising compression would let the server return a compressed body the wasm build cannot decode, so `Response.text()` or `.json()` would see raw compressed bytes. Responses therefore arrive uncompressed, so large text or JSON bodies transfer more bytes over the slower mixnet path.
|
||||
|
||||
To override any of these, set the header in the `init.headers` bag like normal:
|
||||
|
||||
```ts
|
||||
const res = await mixFetch('https://example.com', {
|
||||
headers: { 'User-Agent': 'my-app/1.0' },
|
||||
});
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
The shim does not attempt full browser impersonation. TLS fingerprint (JA3), HTTP/2, and header ordering are still distinguishable from a real Chrome request. If you need stronger blend-in, you'll need to handle that at the application or destination layer.
|
||||
</Callout>
|
||||
|
||||
## Drop-in caveats
|
||||
|
||||
`mixFetch` matches the `fetch()` call signature but is not a perfect substitute. The differences are intentional and follow from running outside the browser's networking stack:
|
||||
|
||||
| Difference | What it means | What to do |
|
||||
|---|---|---|
|
||||
| **No same-origin restriction** | Requests aren't subject to browser CORS preflight. The IPR honours its exit policy regardless of `Origin`. | Don't rely on CORS as an access-control mechanism for `mixFetch` requests; treat them as you would server-to-server calls. |
|
||||
| **No cookies / credentials** | The browser cookie jar is not shared with the WASM instance. `credentials: 'include'` has no effect. | Pass auth tokens via `Authorization` or other explicit headers. |
|
||||
| **No HTTP cache** | The browser HTTP cache is not consulted. Every call hits the network. | Cache responses at the application layer if needed. |
|
||||
| **No service-worker interception** | Requests don't pass through any `fetch` event handlers registered by service workers. | n/a |
|
||||
| **HTTPS only in practice** | The IPR sees plaintext HTTP in full. | Always target `https://` URLs. |
|
||||
|
||||
## Errors
|
||||
|
||||
`mixFetch` follows `fetch` semantics for HTTP status: a 4xx or 5xx response **resolves** with a `Response` carrying that status, so check `response.ok` or `response.status` yourself. The promise **rejects** only on a transport-level failure: a connection or TLS failure, a DNS failure, or the IPR refusing the destination under its exit policy. A rejection is a plain `Error` whose message describes the cause; there is no typed error class, so match on the message if you need to branch.
|
||||
|
||||
## Timeouts and cancellation
|
||||
|
||||
There is no per-request timeout, and `AbortController` / `signal` is ignored: an in-flight `mixFetch` cannot be cancelled. To bound how long you wait, race it against a timer. This stops you waiting but does not cancel the underlying request:
|
||||
|
||||
```ts
|
||||
const res = await Promise.race([
|
||||
mixFetch(url),
|
||||
new Promise<Response>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('mixFetch timeout')), 30_000),
|
||||
),
|
||||
]);
|
||||
```
|
||||
|
||||
Connection and DNS timeouts at the tunnel level are set once via `connectTimeoutMs` and `dnsTimeoutMs` in [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts).
|
||||
|
||||
## Configuration
|
||||
|
||||
`setupMixTunnel(opts)` (and `createMixFetch(opts)`) accept the shared tunnel options from [`@nymproject/mix-tunnel`](/developers/mix-tunnel/guides#configuration). The most commonly touched are:
|
||||
|
||||
```ts
|
||||
await setupMixTunnel({
|
||||
// Pin a specific IPR (otherwise auto-discovered from the topology).
|
||||
preferredIpr: 'D1rrUqJY9pesL3pTaMaxLnpZGGYQ4ZpZwpQXCqaeBXTW.6PpFkRvF...',
|
||||
|
||||
// Lower latency and bandwidth at the cost of traffic-analysis resistance.
|
||||
disableCoverTraffic: true,
|
||||
disablePoissonTraffic: true,
|
||||
});
|
||||
```
|
||||
|
||||
The full option surface is documented under [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts).
|
||||
|
||||
<Callout type="info">
|
||||
The first `mixFetch` call after `setupMixTunnel()` may take a few seconds: gateway handshake, IPR discovery, and the first DNS resolution all happen on demand. Subsequent calls reuse the tunnel and complete in roughly the time of a normal HTTPS request plus mixnet latency.
|
||||
</Callout>
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: "mix-fetch: Migrating from v1.x to v2"
|
||||
description: "What changed between mix-fetch v1 and v2: removed packages, the new setup options, dropped request-init flags, and the underlying architecture change to a single WASM module exiting via an IPR."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-05-29"
|
||||
---
|
||||
|
||||
# Migrating from v1.x
|
||||
|
||||
v2 is a clean break. The package no longer ships a Go-WASM HTTP/TLS client or a SOCKS5-shaped Network Requester request; it routes through the shared [`mix-tunnel`](/developers/mix-tunnel) and exits at an IPR. If you're starting fresh, you don't need this page; see [mix-fetch](/developers/mix-fetch).
|
||||
|
||||
## Removed packages
|
||||
|
||||
The five-variant publish (`@nymproject/mix-fetch`, `mix-fetch-full-fat`, `mix-fetch-commonjs`, `mix-fetch-commonjs-full-fat`, `mix-fetch-node-commonjs`) is gone. There is one package, ESM only:
|
||||
|
||||
| v1.x | v2.0 |
|
||||
|---|---|
|
||||
| `@nymproject/mix-fetch` (ESM) | `@nymproject/mix-fetch` |
|
||||
| `@nymproject/mix-fetch-full-fat` | `@nymproject/mix-fetch` (always inlined) |
|
||||
| `@nymproject/mix-fetch-commonjs` | (no CJS build) |
|
||||
| `@nymproject/mix-fetch-commonjs-full-fat` | (no CJS build) |
|
||||
| `@nymproject/mix-fetch-node-commonjs` | (no Node build) |
|
||||
|
||||
The single v2 package is ESM-only and always inlines its wasm (the v1 `full-fat` behaviour is now the default), so there is no separate bundler-config step. CommonJS and Node builds are not shipped. The Node build in particular is not just a packaging variant: the mixnet transport runs on browser globals (`WebSocket`, Web Worker) inside the wasm, so a Node target would need a runtime-compatibility layer, not a CommonJS rebuild. If you need a CJS or Node variant, open an issue describing the use case.
|
||||
|
||||
## Removed options
|
||||
|
||||
The v1 setup options bag covered Network Requester selection and gateway tuning. v2 replaces it with the smolmix [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts) surface:
|
||||
|
||||
| v1 option | v2 equivalent |
|
||||
|---|---|
|
||||
| `clientId` | retained as `clientId` in `SetupMixTunnelOpts` (defaults to `smolmix-wasm` if unset) |
|
||||
| `preferredGateway` | (replaced by `preferredIpr`, the exit-side pin) |
|
||||
| `preferredNetworkRequester` | (no replacement: v2 exits via IPR, not Network Requester) |
|
||||
| `mixFetchOverride.requestTimeoutMs` | (no replacement at TS layer: surface via smolmix-wasm config if needed) |
|
||||
| `forceTls: true` | retained as `forceTls` in `SetupMixTunnelOpts` (defaults to WSS; you rarely need to set it) |
|
||||
| `extra.hiddenGateways` | (no replacement) |
|
||||
|
||||
## Removed request-init flags
|
||||
|
||||
The v1 `mode: 'unsafe-ignore-cors'` flag is gone. v2 doesn't perform browser-side CORS checks at all (see [Drop-in caveats](/developers/mix-fetch/guides#drop-in-caveats)), so the flag has no meaning.
|
||||
|
||||
## New setup
|
||||
|
||||
```ts
|
||||
// v1
|
||||
import { createMixFetch } from '@nymproject/mix-fetch-full-fat';
|
||||
const mixFetch = await createMixFetch({
|
||||
clientId: 'my-app',
|
||||
preferredGateway: 'q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1',
|
||||
mixFetchOverride: { requestTimeoutMs: 60_000 },
|
||||
forceTls: true,
|
||||
});
|
||||
|
||||
// v2
|
||||
import { createMixFetch } from '@nymproject/mix-fetch';
|
||||
const mixFetch = await createMixFetch({
|
||||
// Optional: pin an exit IPR instead of an entry gateway.
|
||||
// preferredIpr: '...',
|
||||
});
|
||||
```
|
||||
|
||||
## Architectural change
|
||||
|
||||
v1 ran two WASM modules: a Go module with `crypto/tls` + Mozilla CA bundle, and a Rust module handling Nym's `Socks5Request` framing to a Network Requester exit. v2 runs one WASM module ([smolmix-wasm](https://github.com/nymtech/nym/tree/develop/wasm/smolmix)) with a userspace TCP/IP stack ([smoltcp](https://docs.rs/smoltcp)) and `rustls` for TLS. The exit is an IPR, not a Network Requester.
|
||||
|
||||
Consequences:
|
||||
|
||||
- **One WASM module, smaller bundle.** v1's Go runtime accounted for ~6 MB of the full-fat bundle; v2 drops it.
|
||||
- **Shared infrastructure with `mix-dns` and `mix-websocket`.** The same tunnel handles all three.
|
||||
- **IPR exit policies apply.** What was allowed by your previous Network Requester may not be allowed by your default IPR, which applies the current [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt).
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: "mix-tunnel: Shared Mixnet Tunnel for the Browser"
|
||||
description: "TypeScript package that owns the shared Nym mixnet tunnel in the browser. The base layer for mix-fetch, mix-dns, and mix-websocket."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-05"
|
||||
---
|
||||
|
||||
# mix-tunnel
|
||||
|
||||
[`@nymproject/mix-tunnel`](https://www.npmjs.com/package/@nymproject/mix-tunnel) owns a single mixnet tunnel in the browser and exposes it to the feature packages built on it: [`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), and [`mix-websocket`](/developers/mix-websocket). All three call into the same tunnel, so they share one IPR connection, one userspace TCP/IP stack ([`smoltcp`](https://docs.rs/smoltcp)), and one DNS cache.
|
||||
|
||||
The tunnel lives in a Web Worker and is built on [smolmix-wasm](https://github.com/nymtech/nym/tree/develop/wasm/smolmix), the WebAssembly build of the [Rust `smolmix` crate](/developers/smolmix). One WASM instance per page, regardless of how many feature packages you import. See [mix-* architecture](/developers/mix-architecture) for how the worker, the Comlink boundary, and the smoltcp + rustls stack fit together.
|
||||
|
||||
## When to use it directly
|
||||
|
||||
Most apps don't import `mix-tunnel` directly. The feature packages re-export `setupMixTunnel`, `disconnectMixTunnel`, and `getTunnelState`, so calling `setupMixTunnel()` from `mix-fetch` brings the tunnel up for all three.
|
||||
|
||||
Use `mix-tunnel` directly when you want to:
|
||||
|
||||
- Configure the tunnel once at app startup, before any feature package is loaded.
|
||||
- Inspect tunnel state from UI code that isn't tied to a specific feature package.
|
||||
- Tear down the tunnel explicitly, for example before a page unload.
|
||||
|
||||
## In this section
|
||||
|
||||
- [Get started](/developers/mix-tunnel/get-started): install and bring the tunnel up.
|
||||
- [Reference](/developers/mix-tunnel/guides): configuration and reading tunnel state.
|
||||
- [Architecture](/developers/mix-architecture): how the shared tunnel and Web Worker are wired.
|
||||
- [Exit security](/developers/concepts/exit-security): what the IPR exit can see.
|
||||
- [TypeDoc reference](/developers/mix-tunnel/api/globals): generated from the source.
|
||||
- [Examples](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples): the mix-fetch, mix-dns, and mix-websocket example apps all bring the tunnel up through this package.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"get-started": "Get started",
|
||||
"guides": "Reference",
|
||||
"api": "TypeDoc Reference"
|
||||
}
|
||||
+1
-2
@@ -3,6 +3,5 @@
|
||||
"functions": "Functions",
|
||||
"interfaces": "Interfaces",
|
||||
"enumerations": "Enumerations",
|
||||
"type-aliases": "Type Aliases",
|
||||
"variables": "Variables"
|
||||
"type-aliases": "Type Aliases"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[**@nymproject/mix-tunnel**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-tunnel](../globals.md) / EventKinds
|
||||
|
||||
# Enumeration: EventKinds
|
||||
|
||||
## Enumeration Members
|
||||
|
||||
### Loaded
|
||||
|
||||
> **Loaded**: `"Loaded"`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:72](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L72)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"disconnectMixTunnel": "disconnectMixTunnel",
|
||||
"getMixTunnel": "getMixTunnel",
|
||||
"getTunnelState": "getTunnelState",
|
||||
"proxy": "proxy",
|
||||
"setupMixTunnel": "setupMixTunnel"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[**@nymproject/mix-tunnel**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-tunnel](../globals.md) / disconnectMixTunnel
|
||||
|
||||
# Function: disconnectMixTunnel()
|
||||
|
||||
> **disconnectMixTunnel**(): `Promise`\<`void`\>
|
||||
|
||||
Tear the tunnel down. After this, the WASM is unusable until page reload.
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`void`\>
|
||||
|
||||
## Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/index.ts:61](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/index.ts#L61)
|
||||
@@ -0,0 +1,24 @@
|
||||
[**@nymproject/mix-tunnel**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-tunnel](../globals.md) / getMixTunnel
|
||||
|
||||
# Function: getMixTunnel()
|
||||
|
||||
> **getMixTunnel**(): `Promise`\<`Remote`\<[`IMixTunnelWorker`](../interfaces/IMixTunnelWorker.md)\>\>
|
||||
|
||||
Get the singleton tunnel worker handle. The first call spawns the worker
|
||||
and loads smolmix-wasm; subsequent calls return the same handle.
|
||||
|
||||
Note: this does NOT call `setupMixTunnel` automatically. Call it on the
|
||||
returned handle (or use the top-level `setupMixTunnel` helper) before
|
||||
issuing fetch/dns/websocket requests.
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`Remote`\<[`IMixTunnelWorker`](../interfaces/IMixTunnelWorker.md)\>\>
|
||||
|
||||
## Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/index.ts:47](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/index.ts#L47)
|
||||
@@ -0,0 +1,19 @@
|
||||
[**@nymproject/mix-tunnel**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-tunnel](../globals.md) / getTunnelState
|
||||
|
||||
# Function: getTunnelState()
|
||||
|
||||
> **getTunnelState**(): `Promise`\<[`TunnelState`](../interfaces/TunnelState.md)\>
|
||||
|
||||
Inspect the current tunnel state. Pre-setup reads as `connecting`.
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<[`TunnelState`](../interfaces/TunnelState.md)\>
|
||||
|
||||
## Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/index.ts:68](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/index.ts#L68)
|
||||
@@ -0,0 +1,25 @@
|
||||
[**@nymproject/mix-tunnel**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-tunnel](../globals.md) / proxy
|
||||
|
||||
# Function: proxy()
|
||||
|
||||
> **proxy**\<`T`\>(`obj`): `T` & `ProxyMarked`
|
||||
|
||||
## Type parameters
|
||||
|
||||
• **T** *extends* `object`
|
||||
|
||||
## Parameters
|
||||
|
||||
• **obj**: `T`
|
||||
|
||||
## Returns
|
||||
|
||||
`T` & `ProxyMarked`
|
||||
|
||||
## Source
|
||||
|
||||
node\_modules/.pnpm/comlink@4.4.2/node\_modules/comlink/dist/umd/comlink.d.ts:154
|
||||
@@ -0,0 +1,23 @@
|
||||
[**@nymproject/mix-tunnel**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-tunnel](../globals.md) / setupMixTunnel
|
||||
|
||||
# Function: setupMixTunnel()
|
||||
|
||||
> **setupMixTunnel**(`opts`?): `Promise`\<`void`\>
|
||||
|
||||
Initialise the mixnet tunnel. Idempotent; safe to call from multiple feature packages.
|
||||
|
||||
## Parameters
|
||||
|
||||
• **opts?**: [`SetupMixTunnelOpts`](../interfaces/SetupMixTunnelOpts.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`void`\>
|
||||
|
||||
## Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/index.ts:55](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/index.ts#L55)
|
||||
@@ -0,0 +1,31 @@
|
||||
**@nymproject/mix-tunnel** • [**Docs**](globals.md)
|
||||
|
||||
***
|
||||
|
||||
# @nymproject/mix-tunnel
|
||||
|
||||
## Enumerations
|
||||
|
||||
- [EventKinds](enumerations/EventKinds.md)
|
||||
|
||||
## Interfaces
|
||||
|
||||
- [SetupMixTunnelOpts](interfaces/SetupMixTunnelOpts.md)
|
||||
- [TunnelState](interfaces/TunnelState.md)
|
||||
- [MixFetchResponseInit](interfaces/MixFetchResponseInit.md)
|
||||
- [IMixTunnelWorker](interfaces/IMixTunnelWorker.md)
|
||||
- [LoadedEvent](interfaces/LoadedEvent.md)
|
||||
|
||||
## Type Aliases
|
||||
|
||||
- [TunnelStateName](type-aliases/TunnelStateName.md)
|
||||
- [WsEventType](type-aliases/WsEventType.md)
|
||||
- [WsEventCallback](type-aliases/WsEventCallback.md)
|
||||
|
||||
## Functions
|
||||
|
||||
- [getMixTunnel](functions/getMixTunnel.md)
|
||||
- [setupMixTunnel](functions/setupMixTunnel.md)
|
||||
- [disconnectMixTunnel](functions/disconnectMixTunnel.md)
|
||||
- [getTunnelState](functions/getTunnelState.md)
|
||||
- [proxy](functions/proxy.md)
|
||||
@@ -0,0 +1,155 @@
|
||||
[**@nymproject/mix-tunnel**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-tunnel](../globals.md) / IMixTunnelWorker
|
||||
|
||||
# Interface: IMixTunnelWorker
|
||||
|
||||
## Methods
|
||||
|
||||
### setupMixTunnel()
|
||||
|
||||
> **setupMixTunnel**(`opts`?): `Promise`\<`void`\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
• **opts?**: [`SetupMixTunnelOpts`](SetupMixTunnelOpts.md)
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`\<`void`\>
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:61](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L61)
|
||||
|
||||
***
|
||||
|
||||
### disconnectMixTunnel()
|
||||
|
||||
> **disconnectMixTunnel**(): `Promise`\<`void`\>
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`\<`void`\>
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:62](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L62)
|
||||
|
||||
***
|
||||
|
||||
### getTunnelState()
|
||||
|
||||
> **getTunnelState**(): `Promise`\<[`TunnelState`](TunnelState.md)\>
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`\<[`TunnelState`](TunnelState.md)\>
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:63](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L63)
|
||||
|
||||
***
|
||||
|
||||
### mixFetch()
|
||||
|
||||
> **mixFetch**(`url`, `init`): `Promise`\<[`MixFetchResponseInit`](MixFetchResponseInit.md)\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
• **url**: `string`
|
||||
|
||||
• **init**: `unknown`
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`\<[`MixFetchResponseInit`](MixFetchResponseInit.md)\>
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:64](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L64)
|
||||
|
||||
***
|
||||
|
||||
### mixDNS()
|
||||
|
||||
> **mixDNS**(`hostname`): `Promise`\<`string`\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
• **hostname**: `string`
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`\<`string`\>
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:65](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L65)
|
||||
|
||||
***
|
||||
|
||||
### mixWebSocket()
|
||||
|
||||
> **mixWebSocket**(`url`, `protocols`, `onEvent`): `Promise`\<`number`\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
• **url**: `string`
|
||||
|
||||
• **protocols**: `undefined` \| `string`[]
|
||||
|
||||
• **onEvent**: [`WsEventCallback`](../type-aliases/WsEventCallback.md)
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`\<`number`\>
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:66](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L66)
|
||||
|
||||
***
|
||||
|
||||
### wsSend()
|
||||
|
||||
> **wsSend**(`handleId`, `data`): `Promise`\<`void`\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
• **handleId**: `number`
|
||||
|
||||
• **data**: `string` \| `Uint8Array` \| `ArrayBuffer`
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`\<`void`\>
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:67](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L67)
|
||||
|
||||
***
|
||||
|
||||
### wsClose()
|
||||
|
||||
> **wsClose**(`handleId`, `code`, `reason`): `Promise`\<`void`\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
• **handleId**: `number`
|
||||
|
||||
• **code**: `number`
|
||||
|
||||
• **reason**: `string`
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`\<`void`\>
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:68](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L68)
|
||||
@@ -0,0 +1,31 @@
|
||||
[**@nymproject/mix-tunnel**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-tunnel](../globals.md) / LoadedEvent
|
||||
|
||||
# Interface: LoadedEvent
|
||||
|
||||
## Properties
|
||||
|
||||
### kind
|
||||
|
||||
> **kind**: [`Loaded`](../enumerations/EventKinds.md#loaded)
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:76](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L76)
|
||||
|
||||
***
|
||||
|
||||
### args
|
||||
|
||||
> **args**: `object`
|
||||
|
||||
#### loaded
|
||||
|
||||
> **loaded**: `true`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:77](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L77)
|
||||
@@ -0,0 +1,60 @@
|
||||
[**@nymproject/mix-tunnel**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-tunnel](../globals.md) / MixFetchResponseInit
|
||||
|
||||
# Interface: MixFetchResponseInit
|
||||
|
||||
Pre-serialised response shape produced by `smolmix-wasm::mixFetch`. Designed
|
||||
for Comlink transfer (Uint8Array + primitive arrays survive structured clone).
|
||||
|
||||
`headers` is a sequence of `[name, value]` pairs rather than a record so that
|
||||
repeated names like `Set-Cookie`, `Vary`, `Link`, `WWW-Authenticate` survive.
|
||||
The TS facade reconstructs a real `Response` via:
|
||||
|
||||
new Response(raw.body, {
|
||||
status: raw.status,
|
||||
statusText: raw.statusText,
|
||||
headers: new Headers(raw.headers),
|
||||
})
|
||||
|
||||
## Properties
|
||||
|
||||
### body
|
||||
|
||||
> **body**: `Uint8Array`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:51](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L51)
|
||||
|
||||
***
|
||||
|
||||
### status
|
||||
|
||||
> **status**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:52](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L52)
|
||||
|
||||
***
|
||||
|
||||
### statusText
|
||||
|
||||
> **statusText**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:53](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L53)
|
||||
|
||||
***
|
||||
|
||||
### headers
|
||||
|
||||
> **headers**: [`string`, `string`][]
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:54](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L54)
|
||||
@@ -0,0 +1,167 @@
|
||||
[**@nymproject/mix-tunnel**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-tunnel](../globals.md) / SetupMixTunnelOpts
|
||||
|
||||
# Interface: SetupMixTunnelOpts
|
||||
|
||||
## Properties
|
||||
|
||||
### preferredIpr?
|
||||
|
||||
> `optional` **preferredIpr**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:11](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L11)
|
||||
|
||||
***
|
||||
|
||||
### clientId?
|
||||
|
||||
> `optional` **clientId**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:12](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L12)
|
||||
|
||||
***
|
||||
|
||||
### forceTls?
|
||||
|
||||
> `optional` **forceTls**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:13](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L13)
|
||||
|
||||
***
|
||||
|
||||
### disablePoissonTraffic?
|
||||
|
||||
> `optional` **disablePoissonTraffic**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:14](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L14)
|
||||
|
||||
***
|
||||
|
||||
### disableCoverTraffic?
|
||||
|
||||
> `optional` **disableCoverTraffic**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:15](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L15)
|
||||
|
||||
***
|
||||
|
||||
### openReplySurbs?
|
||||
|
||||
> `optional` **openReplySurbs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:16](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L16)
|
||||
|
||||
***
|
||||
|
||||
### dataReplySurbs?
|
||||
|
||||
> `optional` **dataReplySurbs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:17](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L17)
|
||||
|
||||
***
|
||||
|
||||
### primaryDns?
|
||||
|
||||
> `optional` **primaryDns**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:18](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L18)
|
||||
|
||||
***
|
||||
|
||||
### fallbackDns?
|
||||
|
||||
> `optional` **fallbackDns**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:19](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L19)
|
||||
|
||||
***
|
||||
|
||||
### storagePassphrase?
|
||||
|
||||
> `optional` **storagePassphrase**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:20](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L20)
|
||||
|
||||
***
|
||||
|
||||
### connectTimeoutMs?
|
||||
|
||||
> `optional` **connectTimeoutMs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:21](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L21)
|
||||
|
||||
***
|
||||
|
||||
### dnsTimeoutMs?
|
||||
|
||||
> `optional` **dnsTimeoutMs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:22](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L22)
|
||||
|
||||
***
|
||||
|
||||
### tcpKeepaliveMs?
|
||||
|
||||
> `optional` **tcpKeepaliveMs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:23](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L23)
|
||||
|
||||
***
|
||||
|
||||
### tcpBufferSize?
|
||||
|
||||
> `optional` **tcpBufferSize**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:24](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L24)
|
||||
|
||||
***
|
||||
|
||||
### maxRedirects?
|
||||
|
||||
> `optional` **maxRedirects**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:25](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L25)
|
||||
|
||||
***
|
||||
|
||||
### debug?
|
||||
|
||||
> `optional` **debug**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:26](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L26)
|
||||
@@ -0,0 +1,27 @@
|
||||
[**@nymproject/mix-tunnel**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-tunnel](../globals.md) / TunnelState
|
||||
|
||||
# Interface: TunnelState
|
||||
|
||||
## Properties
|
||||
|
||||
### state
|
||||
|
||||
> **state**: [`TunnelStateName`](../type-aliases/TunnelStateName.md)
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:32](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L32)
|
||||
|
||||
***
|
||||
|
||||
### reason?
|
||||
|
||||
> `optional` **reason**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:33](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L33)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"IMixTunnelWorker": "IMixTunnelWorker",
|
||||
"LoadedEvent": "LoadedEvent",
|
||||
"MixFetchResponseInit": "MixFetchResponseInit",
|
||||
"SetupMixTunnelOpts": "SetupMixTunnelOpts",
|
||||
"TunnelState": "TunnelState"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[**@nymproject/mix-tunnel**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-tunnel](../globals.md) / TunnelStateName
|
||||
|
||||
# Type alias: TunnelStateName
|
||||
|
||||
> **TunnelStateName**: `"connecting"` \| `"ready"` \| `"disconnecting"` \| `"disconnected"` \| `"failed"`
|
||||
|
||||
## Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:29](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L29)
|
||||
@@ -0,0 +1,25 @@
|
||||
[**@nymproject/mix-tunnel**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-tunnel](../globals.md) / WsEventCallback
|
||||
|
||||
# Type alias: WsEventCallback()
|
||||
|
||||
> **WsEventCallback**: (`handleId`, `type`, `data`) => `void`
|
||||
|
||||
## Parameters
|
||||
|
||||
• **handleId**: `number`
|
||||
|
||||
• **type**: [`WsEventType`](WsEventType.md)
|
||||
|
||||
• **data**: `unknown`
|
||||
|
||||
## Returns
|
||||
|
||||
`void`
|
||||
|
||||
## Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:58](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L58)
|
||||
@@ -0,0 +1,13 @@
|
||||
[**@nymproject/mix-tunnel**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-tunnel](../globals.md) / WsEventType
|
||||
|
||||
# Type alias: WsEventType
|
||||
|
||||
> **WsEventType**: `"open"` \| `"text"` \| `"binary"` \| `"close"` \| `"error"`
|
||||
|
||||
## Source
|
||||
|
||||
[sdk/typescript/packages/mix-tunnel/src/types.ts:57](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-tunnel/src/types.ts#L57)
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"TunnelStateName": "TunnelStateName",
|
||||
"WsEventCallback": "WsEventCallback",
|
||||
"WsEventType": "WsEventType"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: "Get started with mix-tunnel"
|
||||
description: "Install @nymproject/mix-tunnel and bring up the shared mixnet tunnel."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-05"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
# Get started
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @nymproject/mix-tunnel
|
||||
```
|
||||
|
||||
The package ships ESM only. The bundled Web Worker and smolmix-wasm are inlined, so no separate WASM-loading config is needed in Webpack, Vite, or esbuild.
|
||||
|
||||
<Callout type="info">
|
||||
The smolmix-family packages (`mix-tunnel`, `mix-fetch`, `mix-dns`, `mix-websocket`) are new and version together. The API is still settling: pin exact versions and expect breaking changes between releases until the family reaches a stable line. `mix-fetch` already went through a [v1 to v2 clean break](/developers/mix-fetch/migration).
|
||||
</Callout>
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { setupMixTunnel, getTunnelState, disconnectMixTunnel } from '@nymproject/mix-tunnel';
|
||||
|
||||
// Brings the tunnel up. Call once per page; a second call rejects.
|
||||
await setupMixTunnel();
|
||||
|
||||
// { state: 'ready' } once the IPR handshake completes.
|
||||
console.log(await getTunnelState());
|
||||
|
||||
// Tear down before page unload. The WASM is unusable after this until reload.
|
||||
window.addEventListener('beforeunload', () => { disconnectMixTunnel(); });
|
||||
```
|
||||
|
||||
After `setupMixTunnel()` resolves, [`mixFetch()`](/developers/mix-fetch), [`mixDNS()`](/developers/mix-dns), and `new MixWebSocket()` (from [`mix-websocket`](/developers/mix-websocket)) all become usable.
|
||||
|
||||
Bring the tunnel up live in the [mixnet playground](/developers/playground) and inspect its state over the live mixnet.
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: "mix-tunnel guides"
|
||||
description: "Configure the shared mixnet tunnel and read its connection state."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-05"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
# Reference
|
||||
|
||||
## Configuration
|
||||
|
||||
`setupMixTunnel(opts)` accepts the full smolmix-wasm `SetupOpts` surface plus one TS-layer addition (`debug`). Pass any subset; every field has a default (listed in [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts)).
|
||||
|
||||
```ts
|
||||
await setupMixTunnel({
|
||||
// Pin a specific IPR (otherwise auto-discovered from the topology).
|
||||
preferredIpr: 'D1rrUqJY9pesL3pTaMaxLnpZGGYQ4ZpZwpQXCqaeBXTW.6PpFkRvF...',
|
||||
|
||||
// Cover traffic and Poisson timing are ON by default. Setting these disables
|
||||
// them for lower latency and bandwidth, at the cost of traffic-analysis
|
||||
// resistance (it drops timing-correlation resistance at the entry and exit).
|
||||
disableCoverTraffic: true,
|
||||
disablePoissonTraffic: true,
|
||||
|
||||
// Verbose console tracing from smolmix-wasm. Useful while integrating.
|
||||
debug: true,
|
||||
});
|
||||
```
|
||||
|
||||
The complete option surface (IPR pinning, SURB budgets, DNS server overrides, TCP/connect timeouts, gateway selection) is documented in the [`SetupMixTunnelOpts` type reference](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts).
|
||||
|
||||
<Callout type="info">
|
||||
Call `setupMixTunnel` once. The WASM tunnel is one-shot per page: the first call brings it up, and a second call rejects with `tunnel already initialised`. Because the feature packages share that one tunnel, calling it from `mix-fetch` is enough for `mix-dns` and `mix-websocket` too. If more than one call site might invoke it, guard with `getTunnelState()` and skip setup when the state is already `ready`.
|
||||
</Callout>
|
||||
|
||||
## Tunnel state
|
||||
|
||||
`getTunnelState()` returns a tagged state for surfacing connection status in the UI. The five states are:
|
||||
|
||||
| State | Meaning |
|
||||
|---|---|
|
||||
| `connecting` | Default before `setupMixTunnel()` completes. |
|
||||
| `ready` | Tunnel is up. Feature packages can issue requests. |
|
||||
| `shutting_down` | `disconnectMixTunnel()` in progress. |
|
||||
| `shutdown` | Cleanly torn down. The WASM is no longer usable. |
|
||||
| `failed` | Setup or runtime error. The `reason` field carries a human-readable cause. |
|
||||
|
||||
```ts
|
||||
const { state, reason } = await getTunnelState();
|
||||
if (state === 'failed') {
|
||||
console.error('Tunnel failed:', reason);
|
||||
}
|
||||
```
|
||||
|
||||
`await setupMixTunnel()` resolves only once the tunnel is `ready`, so most apps never need to read the state. Poll `getTunnelState()` only to drive a separate status indicator. The transitions are coarse (no progress percentage during `connecting`); the underlying connection events are visible via `debug: true` in the console.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: "mix-websocket: WebSocket Over the Nym Mixnet"
|
||||
description: "TypeScript package that exposes a WebSocket-like class for WS and WSS traffic routed through the Nym mixnet."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-05"
|
||||
---
|
||||
|
||||
# mix-websocket
|
||||
|
||||
[`@nymproject/mix-websocket`](https://www.npmjs.com/package/@nymproject/mix-websocket) exposes `MixWebSocket`, a class that mirrors the browser [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) API for traffic that needs to travel through the Nym mixnet. WS and WSS endpoints are reached via the [IPR (Internet Packet Router)](/network/infrastructure/exit-services#ip-packet-router) exit gateway; the destination server sees the connection coming from the IPR's IP, not yours.
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Your browser app │
|
||||
│ └─ new MixWebSocket('wss://...') │
|
||||
│ └─ mix-tunnel (smolmix-wasm, Web Worker) │
|
||||
│ └─ smoltcp userspace TCP/IP + rustls TLS │
|
||||
│ └─ Nym mixnet → IPR exit gateway │
|
||||
│ └─ destination WS server │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The TLS handshake (for `wss://` targets) terminates inside the WASM bundle, end-to-end with the destination. The IPR sees TCP frames addressed to the destination's IP and port; for WSS, the payload is TLS ciphertext.
|
||||
|
||||
## In this section
|
||||
|
||||
- [Get started](/developers/mix-websocket/get-started): install and open your first mixnet WebSocket.
|
||||
- [Reference](/developers/mix-websocket/guides): sending and receiving, error handling, configuration.
|
||||
- [Concepts & security](/developers/mix-websocket/concepts): how it differs from the browser WebSocket, and the security model.
|
||||
- [TypeDoc reference](/developers/mix-websocket/api/globals): generated from the source.
|
||||
- [Browser example](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-websocket/browser): a runnable example app.
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"get-started": "Get started",
|
||||
"guides": "Reference",
|
||||
"concepts": "Concepts & security",
|
||||
"api": "TypeDoc Reference"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"globals": "API Index",
|
||||
"classes": "Classes",
|
||||
"functions": "Functions",
|
||||
"interfaces": "Interfaces",
|
||||
"type-aliases": "Type Aliases"
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
[**@nymproject/mix-websocket**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-websocket](../globals.md) / MixWebSocket
|
||||
|
||||
# Class: MixWebSocket
|
||||
|
||||
WebSocket-like channel over the Nym mixnet. The tunnel must already be
|
||||
set up (`setupMixTunnel()`) before constructing one.
|
||||
|
||||
Differences from the browser `WebSocket`:
|
||||
- Constructor resolves asynchronously; use `await ws.opened()` if you
|
||||
need to block until the upgrade completes.
|
||||
- `binaryType` is fixed to `arraybuffer` (no Blob support).
|
||||
- No `bufferedAmount`; the tunnel queues writes through the worker.
|
||||
|
||||
## Extends
|
||||
|
||||
- `EventTarget`
|
||||
|
||||
## Constructors
|
||||
|
||||
### new MixWebSocket()
|
||||
|
||||
> **new MixWebSocket**(`url`, `protocols`?): [`MixWebSocket`](MixWebSocket.md)
|
||||
|
||||
#### Parameters
|
||||
|
||||
• **url**: `string`
|
||||
|
||||
• **protocols?**: `string` \| `string`[]
|
||||
|
||||
#### Returns
|
||||
|
||||
[`MixWebSocket`](MixWebSocket.md)
|
||||
|
||||
#### Overrides
|
||||
|
||||
`EventTarget.constructor`
|
||||
|
||||
#### Source
|
||||
|
||||
[dev/work/nym/sdk/typescript/packages/mix-websocket/src/index.ts:44](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-websocket/src/index.ts#L44)
|
||||
|
||||
## Properties
|
||||
|
||||
### url
|
||||
|
||||
> `readonly` **url**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
[dev/work/nym/sdk/typescript/packages/mix-websocket/src/index.ts:38](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-websocket/src/index.ts#L38)
|
||||
|
||||
***
|
||||
|
||||
### protocols
|
||||
|
||||
> `readonly` **protocols**: `string`[]
|
||||
|
||||
#### Source
|
||||
|
||||
[dev/work/nym/sdk/typescript/packages/mix-websocket/src/index.ts:39](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-websocket/src/index.ts#L39)
|
||||
|
||||
***
|
||||
|
||||
### handleIdPromise
|
||||
|
||||
> `private` **handleIdPromise**: `Promise`\<`number`\>
|
||||
|
||||
#### Source
|
||||
|
||||
[dev/work/nym/sdk/typescript/packages/mix-websocket/src/index.ts:41](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-websocket/src/index.ts#L41)
|
||||
|
||||
***
|
||||
|
||||
### state
|
||||
|
||||
> `private` **state**: [`MixWebSocketReadyState`](../type-aliases/MixWebSocketReadyState.md) = `CONNECTING`
|
||||
|
||||
#### Source
|
||||
|
||||
[dev/work/nym/sdk/typescript/packages/mix-websocket/src/index.ts:42](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-websocket/src/index.ts#L42)
|
||||
|
||||
## Accessors
|
||||
|
||||
### readyState
|
||||
|
||||
> `get` **readyState**(): [`MixWebSocketReadyState`](../type-aliases/MixWebSocketReadyState.md)
|
||||
|
||||
#### Returns
|
||||
|
||||
[`MixWebSocketReadyState`](../type-aliases/MixWebSocketReadyState.md)
|
||||
|
||||
#### Source
|
||||
|
||||
[dev/work/nym/sdk/typescript/packages/mix-websocket/src/index.ts:76](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-websocket/src/index.ts#L76)
|
||||
|
||||
## Methods
|
||||
|
||||
### opened()
|
||||
|
||||
> **opened**(): `Promise`\<`void`\>
|
||||
|
||||
Block until the WebSocket transitions out of `CONNECTING`. Resolves when
|
||||
`open` fires (or when the connection fails before opening).
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`\<`void`\>
|
||||
|
||||
#### Source
|
||||
|
||||
[dev/work/nym/sdk/typescript/packages/mix-websocket/src/index.ts:84](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-websocket/src/index.ts#L84)
|
||||
|
||||
***
|
||||
|
||||
### send()
|
||||
|
||||
> **send**(`data`): `Promise`\<`void`\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
• **data**: `string` \| `ArrayBuffer` \| `Uint8Array`
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`\<`void`\>
|
||||
|
||||
#### Source
|
||||
|
||||
[dev/work/nym/sdk/typescript/packages/mix-websocket/src/index.ts:97](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-websocket/src/index.ts#L97)
|
||||
|
||||
***
|
||||
|
||||
### close()
|
||||
|
||||
> **close**(`code`, `reason`): `Promise`\<`void`\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
• **code**: `number`= `1000`
|
||||
|
||||
• **reason**: `string`= `''`
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`\<`void`\>
|
||||
|
||||
#### Source
|
||||
|
||||
[dev/work/nym/sdk/typescript/packages/mix-websocket/src/index.ts:106](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-websocket/src/index.ts#L106)
|
||||
|
||||
***
|
||||
|
||||
### handleEvent()
|
||||
|
||||
> `private` **handleEvent**(`type`, `data`): `void`
|
||||
|
||||
#### Parameters
|
||||
|
||||
• **type**: `WsEventType`
|
||||
|
||||
• **data**: `unknown`
|
||||
|
||||
#### Returns
|
||||
|
||||
`void`
|
||||
|
||||
#### Source
|
||||
|
||||
[dev/work/nym/sdk/typescript/packages/mix-websocket/src/index.ts:114](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-websocket/src/index.ts#L114)
|
||||
|
||||
***
|
||||
|
||||
### addEventListener()
|
||||
|
||||
> **addEventListener**(`type`, `callback`, `options`?): `void`
|
||||
|
||||
Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched.
|
||||
|
||||
The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture.
|
||||
|
||||
When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET.
|
||||
|
||||
When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in § 2.8 Observing event listeners.
|
||||
|
||||
When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed.
|
||||
|
||||
If an AbortSignal is passed for options's signal, then the event listener will be removed when signal is aborted.
|
||||
|
||||
The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture.
|
||||
|
||||
[MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)
|
||||
|
||||
#### Parameters
|
||||
|
||||
• **type**: `string`
|
||||
|
||||
• **callback**: `null` \| `EventListenerOrEventListenerObject`
|
||||
|
||||
• **options?**: `boolean` \| `AddEventListenerOptions`
|
||||
|
||||
#### Returns
|
||||
|
||||
`void`
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`EventTarget.addEventListener`
|
||||
|
||||
#### Source
|
||||
|
||||
.local/share/pnpm/global/5/.pnpm/typescript@5.4.5/node\_modules/typescript/lib/lib.dom.d.ts:8256
|
||||
|
||||
***
|
||||
|
||||
### dispatchEvent()
|
||||
|
||||
> **dispatchEvent**(`event`): `boolean`
|
||||
|
||||
Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise.
|
||||
|
||||
[MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent)
|
||||
|
||||
#### Parameters
|
||||
|
||||
• **event**: `Event`
|
||||
|
||||
#### Returns
|
||||
|
||||
`boolean`
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`EventTarget.dispatchEvent`
|
||||
|
||||
#### Source
|
||||
|
||||
.local/share/pnpm/global/5/.pnpm/typescript@5.4.5/node\_modules/typescript/lib/lib.dom.d.ts:8262
|
||||
|
||||
***
|
||||
|
||||
### removeEventListener()
|
||||
|
||||
> **removeEventListener**(`type`, `callback`, `options`?): `void`
|
||||
|
||||
Removes the event listener in target's event listener list with the same type, callback, and options.
|
||||
|
||||
[MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener)
|
||||
|
||||
#### Parameters
|
||||
|
||||
• **type**: `string`
|
||||
|
||||
• **callback**: `null` \| `EventListenerOrEventListenerObject`
|
||||
|
||||
• **options?**: `boolean` \| `EventListenerOptions`
|
||||
|
||||
#### Returns
|
||||
|
||||
`void`
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`EventTarget.removeEventListener`
|
||||
|
||||
#### Source
|
||||
|
||||
.local/share/pnpm/global/5/.pnpm/typescript@5.4.5/node\_modules/typescript/lib/lib.dom.d.ts:8268
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"MixWebSocket": "MixWebSocket"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"disconnectMixTunnel": "disconnectMixTunnel",
|
||||
"getTunnelState": "getTunnelState",
|
||||
"setupMixTunnel": "setupMixTunnel"
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
[**@nymproject/mix-websocket**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-websocket](../globals.md) / disconnectMixTunnel
|
||||
|
||||
# Function: disconnectMixTunnel()
|
||||
|
||||
> **disconnectMixTunnel**(): `Promise`\<`void`\>
|
||||
|
||||
Tear the tunnel down. After this, the WASM is unusable until page reload.
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`void`\>
|
||||
|
||||
## Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/index.d.ts:16
|
||||
@@ -0,0 +1,19 @@
|
||||
[**@nymproject/mix-websocket**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-websocket](../globals.md) / getTunnelState
|
||||
|
||||
# Function: getTunnelState()
|
||||
|
||||
> **getTunnelState**(): `Promise`\<`TunnelState`\>
|
||||
|
||||
Inspect the current tunnel state. Pre-setup reads as `connecting`.
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`TunnelState`\>
|
||||
|
||||
## Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/index.d.ts:18
|
||||
@@ -0,0 +1,23 @@
|
||||
[**@nymproject/mix-websocket**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-websocket](../globals.md) / setupMixTunnel
|
||||
|
||||
# Function: setupMixTunnel()
|
||||
|
||||
> **setupMixTunnel**(`opts`?): `Promise`\<`void`\>
|
||||
|
||||
Initialise the mixnet tunnel. Idempotent; safe to call from multiple feature packages.
|
||||
|
||||
## Parameters
|
||||
|
||||
• **opts?**: [`SetupMixTunnelOpts`](../interfaces/SetupMixTunnelOpts.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`Promise`\<`void`\>
|
||||
|
||||
## Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/index.d.ts:14
|
||||
@@ -0,0 +1,23 @@
|
||||
**@nymproject/mix-websocket** • [**Docs**](globals.md)
|
||||
|
||||
***
|
||||
|
||||
# @nymproject/mix-websocket
|
||||
|
||||
## Classes
|
||||
|
||||
- [MixWebSocket](classes/MixWebSocket.md)
|
||||
|
||||
## Interfaces
|
||||
|
||||
- [SetupMixTunnelOpts](interfaces/SetupMixTunnelOpts.md)
|
||||
|
||||
## Type Aliases
|
||||
|
||||
- [MixWebSocketReadyState](type-aliases/MixWebSocketReadyState.md)
|
||||
|
||||
## Functions
|
||||
|
||||
- [setupMixTunnel](functions/setupMixTunnel.md)
|
||||
- [disconnectMixTunnel](functions/disconnectMixTunnel.md)
|
||||
- [getTunnelState](functions/getTunnelState.md)
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
[**@nymproject/mix-websocket**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-websocket](../globals.md) / SetupMixTunnelOpts
|
||||
|
||||
# Interface: SetupMixTunnelOpts
|
||||
|
||||
## Properties
|
||||
|
||||
### preferredIpr?
|
||||
|
||||
> `optional` **preferredIpr**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:2
|
||||
|
||||
***
|
||||
|
||||
### clientId?
|
||||
|
||||
> `optional` **clientId**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:3
|
||||
|
||||
***
|
||||
|
||||
### forceTls?
|
||||
|
||||
> `optional` **forceTls**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:4
|
||||
|
||||
***
|
||||
|
||||
### disablePoissonTraffic?
|
||||
|
||||
> `optional` **disablePoissonTraffic**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:5
|
||||
|
||||
***
|
||||
|
||||
### disableCoverTraffic?
|
||||
|
||||
> `optional` **disableCoverTraffic**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:6
|
||||
|
||||
***
|
||||
|
||||
### openReplySurbs?
|
||||
|
||||
> `optional` **openReplySurbs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:7
|
||||
|
||||
***
|
||||
|
||||
### dataReplySurbs?
|
||||
|
||||
> `optional` **dataReplySurbs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:8
|
||||
|
||||
***
|
||||
|
||||
### primaryDns?
|
||||
|
||||
> `optional` **primaryDns**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:9
|
||||
|
||||
***
|
||||
|
||||
### fallbackDns?
|
||||
|
||||
> `optional` **fallbackDns**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:10
|
||||
|
||||
***
|
||||
|
||||
### storagePassphrase?
|
||||
|
||||
> `optional` **storagePassphrase**: `string`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:11
|
||||
|
||||
***
|
||||
|
||||
### connectTimeoutMs?
|
||||
|
||||
> `optional` **connectTimeoutMs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:12
|
||||
|
||||
***
|
||||
|
||||
### dnsTimeoutMs?
|
||||
|
||||
> `optional` **dnsTimeoutMs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:13
|
||||
|
||||
***
|
||||
|
||||
### tcpKeepaliveMs?
|
||||
|
||||
> `optional` **tcpKeepaliveMs**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:14
|
||||
|
||||
***
|
||||
|
||||
### tcpBufferSize?
|
||||
|
||||
> `optional` **tcpBufferSize**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:15
|
||||
|
||||
***
|
||||
|
||||
### maxRedirects?
|
||||
|
||||
> `optional` **maxRedirects**: `number`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:16
|
||||
|
||||
***
|
||||
|
||||
### debug?
|
||||
|
||||
> `optional` **debug**: `boolean`
|
||||
|
||||
#### Source
|
||||
|
||||
dev/work/nym/sdk/typescript/packages/mix-tunnel/dist/esm/types.d.ts:17
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"SetupMixTunnelOpts": "SetupMixTunnelOpts"
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
[**@nymproject/mix-websocket**](../globals.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@nymproject/mix-websocket](../globals.md) / MixWebSocketReadyState
|
||||
|
||||
# Type alias: MixWebSocketReadyState
|
||||
|
||||
> **MixWebSocketReadyState**: `0` \| `1` \| `2` \| `3`
|
||||
|
||||
## Source
|
||||
|
||||
[dev/work/nym/sdk/typescript/packages/mix-websocket/src/index.ts:20](https://github.com/nymtech/nym/blob/8ea9a230a7d5819511b34aec8d6705e038511ad3/sdk/typescript/packages/mix-websocket/src/index.ts#L20)
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"MixWebSocketReadyState": "MixWebSocketReadyState"
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: "mix-websocket concepts & security"
|
||||
description: "How MixWebSocket differs from the browser WebSocket, and what the IPR exit sees."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-05"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
|
||||
# Concepts & security
|
||||
|
||||
## Differences from the browser WebSocket
|
||||
|
||||
`MixWebSocket` extends `EventTarget` and mirrors the standard WebSocket surface where it makes sense. Differences are intentional and follow from the underlying WASM transport:
|
||||
|
||||
| Browser `WebSocket` | `MixWebSocket` | Why |
|
||||
|---|---|---|
|
||||
| Synchronous constructor; readiness via the `open` event | Asynchronous constructor; readiness via `await ws.opened()` or the `open` event | The mixnet connect, smoltcp socket open, and TLS handshake all happen in the worker. |
|
||||
| `binaryType: 'blob' \| 'arraybuffer'` (default Blob) | `binaryType` is fixed to `'arraybuffer'` | Blob construction in a Web Worker requires a transferable; ArrayBuffer is the lowest common denominator. |
|
||||
| `bufferedAmount` | Not exposed | Writes queue inside the worker; no main-thread byte counter. |
|
||||
| `send()` returns `void` synchronously | `send()` returns `Promise<void>` | The send hops the worker boundary. |
|
||||
| `close(code, reason)` returns `void` | `close(code, reason)` returns `Promise<void>` | Same reason. |
|
||||
| `protocols` argument supports string or string[] | Same | (no difference) |
|
||||
|
||||
The standard `open`, `message`, `close`, and `error` events fire as you would expect. `MessageEvent.data` is `string` for text frames and `ArrayBuffer` for binary frames.
|
||||
|
||||
## Security model
|
||||
|
||||
`mix-websocket` follows the shared [mixnet exit security model](/developers/concepts/exit-security). What that means specifically for WS/WSS:
|
||||
|
||||
| At the IPR exit | What's visible |
|
||||
|---|---|
|
||||
| Secure (`wss://`) | Destination IP and port. Frames are TLS ciphertext, terminating at the destination. |
|
||||
| Plain (`ws://`) | Destination IP and port, plus every frame in plaintext. |
|
||||
|
||||
<Callout type="warning">
|
||||
TLS terminates inside the WASM bundle (via [`rustls`](https://docs.rs/rustls) in smolmix-wasm), not in the browser. Mozilla's CA bundle is compiled into the WASM. Use `wss://` for any non-public traffic; `ws://` is visible to the IPR in full.
|
||||
</Callout>
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: "Get started with mix-websocket"
|
||||
description: "Install @nymproject/mix-websocket and open a WebSocket through the Nym mixnet."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-05"
|
||||
---
|
||||
|
||||
# Get started
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @nymproject/mix-websocket
|
||||
```
|
||||
|
||||
ESM only, with the worker and WASM inlined via [`mix-tunnel`](/developers/mix-tunnel/get-started#installation); no bundler config needed.
|
||||
|
||||
## Quick start
|
||||
|
||||
Echo against `wss://echo.websocket.org`, the same endpoint the smolmix dev tool uses:
|
||||
|
||||
```ts
|
||||
import { setupMixTunnel, MixWebSocket } from '@nymproject/mix-websocket';
|
||||
|
||||
await setupMixTunnel();
|
||||
|
||||
const ws = new MixWebSocket('wss://echo.websocket.org');
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
console.log('connected');
|
||||
ws.send('hello mixnet');
|
||||
});
|
||||
|
||||
ws.addEventListener('message', (e) => {
|
||||
console.log('received:', e.data);
|
||||
ws.close();
|
||||
});
|
||||
|
||||
ws.addEventListener('close', () => console.log('closed'));
|
||||
ws.addEventListener('error', (e) => console.error('error:', e));
|
||||
```
|
||||
|
||||
Or `await` on the upgrade instead of subscribing to the `open` event:
|
||||
|
||||
```ts
|
||||
const ws = new MixWebSocket('wss://echo.websocket.org');
|
||||
await ws.opened();
|
||||
ws.send('hello mixnet');
|
||||
```
|
||||
|
||||
Open a `MixWebSocket` live in the [mixnet playground](/developers/playground), which echoes messages and runs an echo burst over the live mixnet.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: "mix-websocket guides"
|
||||
description: "Send and receive frames, handle errors, and configure the tunnel for mix-websocket."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-05"
|
||||
---
|
||||
|
||||
# Reference
|
||||
|
||||
## Sending and receiving
|
||||
|
||||
```ts
|
||||
ws.addEventListener('message', (e) => {
|
||||
if (typeof e.data === 'string') {
|
||||
console.log('text frame:', e.data);
|
||||
} else {
|
||||
// ArrayBuffer for binary frames
|
||||
console.log('binary frame:', e.data.byteLength, 'bytes');
|
||||
}
|
||||
});
|
||||
|
||||
// Send a text frame
|
||||
await ws.send('hello');
|
||||
|
||||
// Send a binary frame
|
||||
await ws.send(new Uint8Array([0x01, 0x02, 0x03]));
|
||||
```
|
||||
|
||||
`send()` rejects if called when `readyState` is not `OPEN`. Use `await ws.opened()` to gate the first send, and check `ws.readyState` if you keep references around for later use.
|
||||
|
||||
## Error handling
|
||||
|
||||
The `error` event carries a non-standard `.message` field with the underlying cause (the standard `Event` carries no payload). Use it for diagnostics:
|
||||
|
||||
```ts
|
||||
ws.addEventListener('error', (e) => {
|
||||
const msg = (e as Event & { message?: string }).message;
|
||||
console.error('mix-websocket failure:', msg);
|
||||
});
|
||||
```
|
||||
|
||||
If the upgrade fails before `open` fires, `MixWebSocket` transitions to `CLOSED` and dispatches `error`. `opened()` resolves either on `open` or on the failure event, so callers don't hang.
|
||||
|
||||
## Configuration
|
||||
|
||||
`MixWebSocket` has no per-instance configuration beyond `url` and `protocols`. Tunnel-level options live on `setupMixTunnel`, the same shared call that mix-fetch and mix-dns use:
|
||||
|
||||
```ts
|
||||
await setupMixTunnel({
|
||||
preferredIpr: '...',
|
||||
disableCoverTraffic: true,
|
||||
});
|
||||
```
|
||||
|
||||
See [`SetupMixTunnelOpts`](/developers/mix-tunnel/api/interfaces/SetupMixTunnelOpts) for the full surface.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: "Mixnet Playground"
|
||||
description: "Interactive browser playground for Nym's TypeScript packages: drive fetch, DNS, WebSocket and download traffic through a mixnet tunnel, and send end-to-end messages with the raw messaging SDK."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-09"
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components'
|
||||
import { MixPlayground } from '../../components/playground/MixPlayground'
|
||||
import { MessagingDemo } from '../../components/playground/messaging-section'
|
||||
|
||||
# Mixnet playground
|
||||
|
||||
This playground runs Nym's browser TypeScript packages against the live mixnet. It covers both integration models:
|
||||
|
||||
- **Proxy**, via the [mix-* family](/developers/mix-tunnel): bring the shared tunnel up once, then drive `fetch`, DNS, WebSocket, stress and file-download traffic through it to clearnet destinations.
|
||||
- **End-to-end**, via the [raw messaging SDK](/developers/typescript): Sphinx-encrypted messages between two Nym clients, with nothing exiting to the clearnet.
|
||||
|
||||
Some sections send the same request over the tunnel and over the clearnet, so you can compare the two.
|
||||
|
||||
## HTTPS / DNS / WebSockets
|
||||
|
||||
<Callout type="info">
|
||||
**Watch the Network tab.** Open DevTools → Network before you connect. Once
|
||||
`setupMixTunnel` reports ready, every tunnel operation here (`mixFetch`,
|
||||
`mixDNS`, `MixWebSocket`) adds **no new request** to that tab: it is multiplexed
|
||||
inside the single WebSocket to the entry gateway. Only the *clearnet* comparison
|
||||
buttons add rows. (Setup also fetches the network topology over HTTPS and
|
||||
refreshes it periodically, so those nym-api calls and the gateway WebSocket are
|
||||
the only clearnet requests you will see.) Your real traffic never leaves the
|
||||
browser as an identifiable, per-destination request.
|
||||
</Callout>
|
||||
|
||||
<Callout type="info">
|
||||
Everything here runs client-side over the live Nym mixnet. The first
|
||||
`setupMixTunnel` is slow (a few seconds): it loads the WebAssembly client,
|
||||
registers a fresh client identity with a gateway, and discovers an IPR exit.
|
||||
Later calls reuse the tunnel.
|
||||
</Callout>
|
||||
|
||||
<MixPlayground />
|
||||
|
||||
## Raw mixnet messaging
|
||||
|
||||
The sections above share one smolmix tunnel and exit to the clearnet through an IPR. The [Messaging SDK](/developers/typescript) (`@nymproject/sdk`) is the other model: end-to-end mixnet messages between two Nym clients, where you control both ends and nothing exits to the clearnet. It runs a separate wasm client, so it loads on demand:
|
||||
|
||||
<MessagingDemo />
|
||||
|
||||
## Source and examples
|
||||
|
||||
- [Playground source](https://github.com/nymtech/nym/tree/develop/documentation/docs/components/playground): the React component behind this page (`MixPlayground.tsx` and `lib.ts`).
|
||||
- [SDK examples](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples): standalone runnable apps, including a browser example per package ([mix-fetch](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-fetch/browser), [mix-dns](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-dns/browser), [mix-websocket](https://github.com/nymtech/nym/tree/develop/sdk/typescript/examples/mix-websocket/browser)).
|
||||
|
||||
## Per-package docs
|
||||
|
||||
For the API of each package, see
|
||||
[mix-tunnel](/developers/mix-tunnel), [mix-fetch](/developers/mix-fetch),
|
||||
[mix-dns](/developers/mix-dns), and [mix-websocket](/developers/mix-websocket).
|
||||
@@ -40,9 +40,9 @@ Add dependencies to `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
nym-sdk = "1.21.0"
|
||||
nym-network-defaults = "1.21.0"
|
||||
nym-bin-common = { version = "1.21.0", features = ["basic_tracing"] }
|
||||
nym-sdk = "1.21.1"
|
||||
nym-network-defaults = "1.21.1"
|
||||
nym-bin-common = { version = "1.21.1", features = ["basic_tracing"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
```
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { RUST_MSRV } from '../../../components/versions'
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
nym-sdk = "1.21.0"
|
||||
nym-sdk = "1.21.1"
|
||||
```
|
||||
|
||||
**Minimum Rust version:** {RUST_MSRV}+
|
||||
|
||||
@@ -76,3 +76,9 @@ client.disconnect().await;
|
||||
## Lots of `duplicate fragment received` messages
|
||||
|
||||
`WARN` level logs about duplicate fragments are caused by Mixnet-level packet retransmission: the original and the retransmitted copy both arrive. This is not a bug in your client logic.
|
||||
|
||||
## Android: mixnet bootstrap fails with a certificate `Revoked` / OCSP error
|
||||
|
||||
When you compile the SDK for Android (via `uniffi` + `cargo-ndk`), the client can fail to bootstrap the mixnet with a hard `Revoked` or "Certificate does not specify OCSP responder" error on certain Validator endpoints. The cause is `rustls-platform-verifier` routing certificate validation through Java's `CertPathValidator`, which enforces an OCSP check the endpoint does not satisfy. iOS and the desktop targets are unaffected; they tolerate or skip the check.
|
||||
|
||||
Configure the client to use preconfigured TLS roots with `webpki_roots::TLS_SERVER_ROOTS` instead of the platform verifier to get around it.
|
||||
|
||||
@@ -29,8 +29,8 @@ Add dependencies to `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
nym-sdk = "1.21.0"
|
||||
nym-bin-common = { version = "1.21.0", features = ["basic_tracing"] }
|
||||
nym-sdk = "1.21.1"
|
||||
nym-bin-common = { version = "1.21.1", features = ["basic_tracing"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
```
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ Add dependencies to `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
nym-sdk = "1.21.0"
|
||||
nym-bin-common = { version = "1.21.0", features = ["basic_tracing"] }
|
||||
nym-sdk = "1.21.1"
|
||||
nym-bin-common = { version = "1.21.1", features = ["basic_tracing"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
rand = "0.8"
|
||||
```
|
||||
|
||||
@@ -32,8 +32,10 @@ Traffic exits the mixnet at an [IPR (Internet Packet Router)](/network/infrastru
|
||||
|
||||
This means `smolmix` is not directly compatible with alternative async runtimes like [`smol`](https://docs.rs/smol) or [`async-std`](https://docs.rs/async-std). If you need to use `smolmix` from another runtime, the [`async-compat`](https://docs.rs/async-compat) crate can bridge the gap.
|
||||
|
||||
### Non-native `smolmix`
|
||||
A WASM version of `smolmix` is planned for a future release.
|
||||
### The WebAssembly build (browsers and WebViews)
|
||||
The WebAssembly build of `smolmix` powers the mix-* packages [`mix-tunnel`](/developers/mix-tunnel), [`mix-fetch`](/developers/mix-fetch), [`mix-dns`](/developers/mix-dns), and [`mix-websocket`](/developers/mix-websocket). The wasm is inlined into `mix-tunnel`, so apps install one of those packages rather than building the crate themselves.
|
||||
|
||||
It runs in any JavaScript runtime that provides `WebSocket`, Web Workers, and WebAssembly. That means desktop browsers and mobile WebViews alike, so hybrid app shells (Capacitor, Ionic, Cordova, `react-native-webview`) are in scope, not just web pages. It does not run in a bare Node.js or React Native Hermes/JSC runtime, which lack Web Workers (and, for Hermes, WebAssembly). For native mobile or server targets, compile the Rust crate itself rather than using the wasm packages.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -41,8 +43,8 @@ Add `smolmix` to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
smolmix = "1.21.0"
|
||||
nym-bin-common = { version = "1.21.0", features = ["basic_tracing"] }
|
||||
smolmix = "1.21.1"
|
||||
nym-bin-common = { version = "1.21.1", features = ["basic_tracing"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
```
|
||||
|
||||
@@ -52,6 +54,8 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
**Minimum Rust version:** {RUST_MSRV}+
|
||||
|
||||
`smolmix`'s API may still change between minor releases. Pin a version rather than tracking the latest, and read the changelog before bumping.
|
||||
|
||||
### From Git
|
||||
|
||||
For unreleased changes, import directly from the repository:
|
||||
@@ -76,33 +80,9 @@ smolmix = { git = "https://github.com/nymtech/nym", branch = "develop" }
|
||||
Traffic is Sphinx-encrypted inside the mixnet. Past the Exit Gateway, it travels over the public internet to the destination, the same as any other server-initiated connection. Protect the payload at the application layer with TLS ([`rustls`](https://docs.rs/rustls)), Noise Protocol ([`snow`](https://docs.rs/snow)), or equivalent, as you would on a direct connection.
|
||||
</Callout>
|
||||
|
||||
### What's protected
|
||||
The mixnet hides your identity from the destination and your destination from the network, but the IPR exit gateway sees the destination IP and port, and sees your payload only if you didn't encrypt it. If you connect with TLS (as in the [TCP example](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/tcp.rs)), the IPR only sees ciphertext addressed to the destination, not its contents. Plaintext HTTP is fully readable at the exit.
|
||||
|
||||
| Segment | Mixnet encryption | What's visible |
|
||||
|---|---|---|
|
||||
| Your machine → mixnet entry | Sphinx (layered) | Entry gateway sees your IP but not the destination |
|
||||
| Inside the mixnet (entry + 3 mix layers + exit) | Sphinx (layered) | Each node only knows prev/next hop |
|
||||
| Exit gateway (IPR) | Sphinx removed, raw IP packet exposed | IPR sees destination IP + port. Payload depends on your application layer (see below). |
|
||||
| IPR → remote host | None (Sphinx is mixnet-only) | Remote host sees IPR's IP, not yours |
|
||||
|
||||
The Sphinx encryption is the **mixnet transport layer**: it protects packets as they traverse the mix nodes. At the exit gateway, the Sphinx layers are stripped and the original IP packet is forwarded to the destination. This is analogous to how a Tor exit node or VPN endpoint unwraps its tunnel.
|
||||
|
||||
What's inside that IP packet is up to you. If you connect with TLS (as in the [TCP example](https://github.com/nymtech/nym/blob/develop/smolmix/core/examples/tcp.rs)), the IPR sees encrypted TLS ciphertext going to a destination IP: it knows where but not what. If you send plaintext HTTP, the IPR can read the full request and response.
|
||||
|
||||
### Trust boundaries
|
||||
|
||||
- You trust the mixnet to provide unlinkability between sender and receiver. This is enforced cryptographically by the Sphinx packet format and mixing.
|
||||
- You trust the IPR exit gateway in the same way you trust a VPN exit or Tor exit node: it can inspect your raw IP packets. The difference is that the IPR doesn't know who is sending the traffic (the mixnet hides your identity).
|
||||
- **Application-layer encryption closes the gap.** TLS, Noise Protocol, or any authenticated encryption keeps the payload as ciphertext to the IPR. It can see destination IP and port, but not payload content.
|
||||
|
||||
### Comparison with other privacy tools
|
||||
|
||||
| | smolmix | Tor | VPN |
|
||||
|---|---|---|---|
|
||||
| **Exit node sees traffic?** | Yes (encrypt it) | Yes (encrypt it) | Yes (encrypt it) |
|
||||
| **Exit node knows sender?** | No (mixnet hides identity) | No (onion routing) | Yes (VPN provider knows) |
|
||||
| **Timing analysis resistance** | Strong (mixing, cover traffic) | Weak (low-latency) | None |
|
||||
| **UDP support** | Yes | No (TCP only) | Yes |
|
||||
The full model (what each hop sees, trust boundaries, and a comparison with Tor and VPNs) is on [Exit security](/developers/concepts/exit-security).
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user