New TS SDK packages (#6839)
* 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
This commit is contained in:
@@ -7,7 +7,10 @@ on:
|
||||
paths:
|
||||
- "documentation/docs/**"
|
||||
- "sdk/typescript/packages/sdk/src/**"
|
||||
- "sdk/typescript/packages/mix-tunnel/src/**"
|
||||
- "sdk/typescript/packages/mix-fetch/src/**"
|
||||
- "sdk/typescript/packages/mix-dns/src/**"
|
||||
- "sdk/typescript/packages/mix-websocket/src/**"
|
||||
- ".github/workflows/ci-docs.yml"
|
||||
|
||||
jobs:
|
||||
@@ -47,7 +50,7 @@ jobs:
|
||||
- name: Check if TypeScript SDK source changed
|
||||
id: check-ts-sdk
|
||||
run: |
|
||||
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -qE '^sdk/typescript/packages/(sdk|mix-fetch)/src/'; then
|
||||
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -qE '^sdk/typescript/packages/(sdk|mix-tunnel|mix-fetch|mix-dns|mix-websocket)/src/'; then
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
@@ -58,8 +61,11 @@ jobs:
|
||||
if: steps.check-ts-sdk.outputs.changed == 'true'
|
||||
run: |
|
||||
npm install -g typedoc@0.25.13 typedoc-plugin-markdown@4.0.3
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/mix-tunnel && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/mix-dns && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/mix-websocket && typedoc --skipErrorChecking
|
||||
|
||||
- name: Verify doc versions
|
||||
run: ${{ github.workspace }}/documentation/scripts/verify-doc-versions.sh
|
||||
|
||||
@@ -40,10 +40,12 @@ jobs:
|
||||
- name: Install wasm-opt
|
||||
run: cargo install wasm-opt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24.6"
|
||||
# Produce wasm/smolmix/pkg/package.json before any pnpm step. The
|
||||
# `pnpm dev:on` in `prebuild:ci` adds wasm/smolmix/pkg to the dynamic
|
||||
# workspace; mix-tunnel's `workspace:*` lookup against @nymproject/
|
||||
# smolmix-wasm needs the package.json to be present.
|
||||
- name: Build smolmix wasm
|
||||
run: make -C wasm/smolmix
|
||||
|
||||
- name: Install
|
||||
run: pnpm i
|
||||
|
||||
@@ -30,11 +30,6 @@ jobs:
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24.6"
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
|
||||
@@ -33,14 +33,6 @@ jobs:
|
||||
- name: Install wasm-opt
|
||||
run: cargo install wasm-opt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24.6"
|
||||
|
||||
- name: Update root CA certificate bundle
|
||||
run: ./wasm/mix-fetch/go-mix-conn/scripts/update-root-certs.sh
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
|
||||
|
||||
Generated
+1
-27
@@ -5414,32 +5414,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mix-fetch-wasm"
|
||||
version = "1.4.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures",
|
||||
"js-sys",
|
||||
"nym-bin-common",
|
||||
"nym-http-api-client",
|
||||
"nym-ordered-buffer",
|
||||
"nym-service-providers-common",
|
||||
"nym-socks5-requests",
|
||||
"nym-validator-client",
|
||||
"nym-wasm-client-core",
|
||||
"nym-wasm-utils",
|
||||
"rand 0.8.6",
|
||||
"serde",
|
||||
"serde-wasm-bindgen 0.6.5",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tsify",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mixnet-connectivity-check"
|
||||
version = "0.1.0"
|
||||
@@ -11583,7 +11557,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smolmix-wasm"
|
||||
version = "1.21.0"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-tungstenite",
|
||||
"bytes",
|
||||
|
||||
@@ -174,7 +174,6 @@ members = [
|
||||
"tools/nymvisor",
|
||||
"tools/ts-rs-cli",
|
||||
"wasm/client",
|
||||
"wasm/mix-fetch",
|
||||
"wasm/smolmix",
|
||||
"wasm/zknym-lib",
|
||||
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
|
||||
@@ -605,10 +604,6 @@ opt-level = 3
|
||||
# lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
[profile.release.package.mix-fetch-wasm]
|
||||
# lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
[profile.release.package.smolmix-wasm]
|
||||
# lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
@@ -105,14 +105,17 @@ sdk-wasm: sdk-wasm-build sdk-wasm-test sdk-wasm-lint
|
||||
|
||||
sdk-wasm-build:
|
||||
$(MAKE) -C wasm/client
|
||||
$(MAKE) -C wasm/mix-fetch
|
||||
$(MAKE) -C wasm/smolmix
|
||||
# $(MAKE) -C wasm/zknym-lib
|
||||
|
||||
# run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo
|
||||
#
|
||||
# `mix-tunnel` must build before the three feature packages — they import it
|
||||
# via `workspace:*` and the lerna topological sort will respect that as long
|
||||
# as we keep them in the same `--scope` invocation.
|
||||
sdk-typescript-build:
|
||||
npx lerna run --scope @nymproject/sdk build --stream
|
||||
npx lerna run --scope @nymproject/mix-fetch build --stream
|
||||
npx lerna run --scope '{@nymproject/mix-tunnel,@nymproject/mix-fetch,@nymproject/mix-dns,@nymproject/mix-websocket}' build --stream
|
||||
pnpm --pwd sdk/typescript/codegen/contract-clients build
|
||||
|
||||
# NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown)
|
||||
@@ -124,7 +127,6 @@ sdk-wasm-test:
|
||||
|
||||
sdk-wasm-lint:
|
||||
RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo clippy $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
|
||||
$(MAKE) -C wasm/mix-fetch check-fmt
|
||||
$(MAKE) -C wasm/smolmix check-fmt
|
||||
|
||||
# Add to top-level targets
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
"audit:fix": "pnpm audit --fix",
|
||||
"build": "run-s build:types build:packages",
|
||||
"build:ci": "run-s build:types build:packages build:wasm build:ci:sdk",
|
||||
"build:ci:sdk": "lerna run --scope '{@nymproject/sdk,@nymproject/sdk-react,@nymproject/mix-fetch,@nymproject/nodejs-client,@nymproject/mix-fetch-node}' build --stream",
|
||||
"build:ci:sdk": "lerna run --scope '{@nymproject/mix-tunnel,@nymproject/mix-fetch,@nymproject/mix-dns,@nymproject/mix-websocket}' build --stream",
|
||||
"build:ci:storybook": "pnpm build && pnpm dev:on && run-p build:playground && pnpm build:ci:storybook:collect-artifacts ",
|
||||
"build:ci:storybook:collect-artifacts": "mkdir -p ts-packages/dist && mv sdk/typescript/packages/react-components/storybook-static ts-packages/dist/storybook ",
|
||||
"build:packages": "run-s build:packages:theme build:packages:react",
|
||||
|
||||
Generated
+565
-993
File diff suppressed because it is too large
Load Diff
+13
-3
@@ -1,9 +1,15 @@
|
||||
# The static workspace below is intentionally minimal. It contains only what
|
||||
# CI workflows without a Rust toolchain (e.g. ci-build-ts, storybook) need to
|
||||
# resolve. Anything that depends on `@nymproject/smolmix-wasm` (mix-tunnel +
|
||||
# mix-fetch + mix-dns + mix-websocket + internal-dev) or the wasm-pack output
|
||||
# dir itself lives in `dev-mode-add.mjs` and is only injected when `pnpm
|
||||
# dev:on` runs. Build the wasm first if dev:on will be called:
|
||||
# `make -C wasm/smolmix` populates wasm/smolmix/pkg/.
|
||||
packages:
|
||||
- 'ts-packages/*'
|
||||
- 'nym-wallet'
|
||||
- 'explorer-v2'
|
||||
- 'types'
|
||||
- 'sdk/typescript/packages/mix-fetch/internal-dev'
|
||||
- 'sdk/typescript/packages/react-components'
|
||||
- 'sdk/typescript/packages/mui-theme'
|
||||
|
||||
@@ -21,8 +27,12 @@ allowBuilds:
|
||||
msgpackr-extract: true
|
||||
nx: true
|
||||
protobufjs: true
|
||||
sharp: true
|
||||
tiny-secp256k1: true
|
||||
# Native C++ bindings; both fail to compile against Node 24's node-addon-api.
|
||||
# Skipping the build leaves the JS shells in place, which is all our SDK
|
||||
# packages need (only nym-wallet / explorer-v2 image stack actually uses them
|
||||
# at runtime). Mirrors the fix on `max/fix-pnpm-ci` for documentation/docs/.
|
||||
sharp: false
|
||||
tiny-secp256k1: false
|
||||
unrs-resolver: true
|
||||
|
||||
catalog:
|
||||
|
||||
@@ -10,8 +10,10 @@ const os = require('os');
|
||||
/**
|
||||
* Creates the default Webpack config
|
||||
* @param baseDir The base directory path, e.g. pass `__dirname` of the webpack config file using this method
|
||||
* @param htmlPath Path or array of HtmlWebpackPlugin opts
|
||||
* @param opts Optional flags: `{ skipFavicon: true }` to omit the WebpackFavicons plugin
|
||||
*/
|
||||
const webpackCommon = (baseDir, htmlPath) => ({
|
||||
const webpackCommon = (baseDir, htmlPath, opts = {}) => ({
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@@ -80,9 +82,13 @@ const webpackCommon = (baseDir, htmlPath) => ({
|
||||
},
|
||||
}),
|
||||
|
||||
new WebpackFavicons({
|
||||
src: path.resolve(__dirname, '../../assets/favicon/favicon.png'), // the asset directory is relative to THIS file
|
||||
}),
|
||||
...(opts.skipFavicon
|
||||
? []
|
||||
: [
|
||||
new WebpackFavicons({
|
||||
src: path.resolve(__dirname, '../../../../assets/favicon/favicon.png'), // repo-root /assets/favicon/favicon.png
|
||||
}),
|
||||
]),
|
||||
|
||||
new Dotenv(),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# mixDNS Usage Example
|
||||
|
||||
Shows how to resolve hostnames over the Nym mixnet using
|
||||
`@nymproject/mix-dns`. Resolution travels the IPR's DNS path (UDP, no TCP/TLS).
|
||||
|
||||
```ts
|
||||
import { setupMixTunnel, mixDNS } from '@nymproject/mix-dns';
|
||||
|
||||
await setupMixTunnel();
|
||||
const ip = await mixDNS('nymtech.net');
|
||||
```
|
||||
|
||||
## Running the example
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run start
|
||||
```
|
||||
|
||||
Open http://localhost:1234. The example resolves three hostnames and prints
|
||||
the round-trip time for each.
|
||||
|
||||
## Sharing the tunnel
|
||||
|
||||
`setupMixTunnel()` is a no-op after the first call across all three smolmix
|
||||
SDKs (`mix-fetch`, `mix-dns`, `mix-websocket`). If you already imported one
|
||||
of the others, you can skip the setup line.
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@nymproject/mix-dns-example-parcel",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"build": "parcel build --no-cache --no-content-hash",
|
||||
"serve": "serve dist",
|
||||
"start": "parcel --no-cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/mix-dns": "workspace:*",
|
||||
"parcel": "^2.9.3"
|
||||
},
|
||||
"private": true,
|
||||
"source": "src/index.html"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>mixDNS</title>
|
||||
<script type="module" src="./index.ts"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
padding: 2rem;
|
||||
}
|
||||
pre { background: #f4f4f4; padding: 0.5rem; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>mixDNS</h1>
|
||||
<p>Resolves a handful of hostnames over the Nym mixnet using the IPR's DNS path.</p>
|
||||
<pre id="output"></pre>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,54 @@
|
||||
import { setupMixTunnel, mixDNS, type SetupMixTunnelOpts } from '@nymproject/mix-dns';
|
||||
|
||||
function append(line: string) {
|
||||
const el = document.getElementById('output') as HTMLPreElement;
|
||||
el.appendChild(document.createTextNode(`${line}\n`));
|
||||
}
|
||||
|
||||
// Tunnel configuration. Every field is optional.
|
||||
//
|
||||
// `debug: true` turns on smolmix-wasm's verbose tracing so you can see the
|
||||
// UDP DNS query and response in DevTools. Leave it off in production.
|
||||
const setupOpts: SetupMixTunnelOpts = {
|
||||
debug: true,
|
||||
|
||||
// Pin a specific exit IPR. Otherwise auto-discovered from the topology.
|
||||
// preferredIpr: 'D1rrUqJY9pesL3pTaMaxLnpZGGYQ4ZpZwpQXCqaeBXTW.6PpFkRvF...',
|
||||
|
||||
// DNS resolver overrides. Defaults: 1.1.1.1 (primary) / 8.8.8.8 (fallback).
|
||||
// Set these to test against a specific resolver, e.g. Quad9 for filtered DNS.
|
||||
// primaryDns: '9.9.9.9',
|
||||
// fallbackDns: '149.112.112.112',
|
||||
|
||||
// Per-query timeout. Default: 30s.
|
||||
// dnsTimeoutMs: 5_000,
|
||||
};
|
||||
|
||||
// Hostnames cover a mix of cases: the Nym site itself, the de facto smoke
|
||||
// test (example.com), a major CDN (cloudflare), and a host that takes
|
||||
// several A records.
|
||||
const hostnames = ['nymtech.net', 'example.com', 'cloudflare.com', 'github.com'];
|
||||
|
||||
async function main() {
|
||||
append('Setting up mixnet tunnel...');
|
||||
await setupMixTunnel(setupOpts);
|
||||
append('Tunnel ready.\n');
|
||||
|
||||
for (const host of hostnames) {
|
||||
const start = performance.now();
|
||||
try {
|
||||
const ip = await mixDNS(host);
|
||||
const ms = Math.round(performance.now() - start);
|
||||
append(`${host} -> ${ip} (${ms} ms)`);
|
||||
} catch (err) {
|
||||
append(`${host} -> error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
append(`Error: ${err}`);
|
||||
});
|
||||
});
|
||||
@@ -8,9 +8,9 @@
|
||||
"start": "parcel --no-cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/mix-fetch": ">=1.4.2-rc.0 || ^1",
|
||||
"@nymproject/mix-fetch": "workspace:*",
|
||||
"parcel": "^2.9.3"
|
||||
},
|
||||
"private": false,
|
||||
"private": true,
|
||||
"source": "src/index.html"
|
||||
}
|
||||
@@ -1,46 +1,71 @@
|
||||
import { mixFetch } from '@nymproject/mix-fetch';
|
||||
import { setupMixTunnel, mixFetch, type SetupMixTunnelOpts } from '@nymproject/mix-fetch';
|
||||
import { appendOutput, appendImageOutput } from './utils';
|
||||
|
||||
// Tunnel configuration. Every field is optional; uncomment to tweak.
|
||||
//
|
||||
// `debug: true` turns on smolmix-wasm's verbose tracing so you can watch the
|
||||
// IPR handshake, DNS lookups, and per-request lifecycle in DevTools. Leave
|
||||
// it off in production.
|
||||
const setupOpts: SetupMixTunnelOpts = {
|
||||
debug: true,
|
||||
|
||||
// Pin a specific exit IPR. Otherwise auto-discovered from the topology.
|
||||
// preferredIpr: 'D1rrUqJY9pesL3pTaMaxLnpZGGYQ4ZpZwpQXCqaeBXTW.6PpFkRvF...',
|
||||
|
||||
// Anonymity / performance trade-off. Cover traffic + Poisson padding
|
||||
// smear timing patterns at the cost of bandwidth. Default: both on.
|
||||
// disableCoverTraffic: true,
|
||||
// disablePoissonTraffic: true,
|
||||
|
||||
// Custom DNS resolvers (over UDP through the IPR). Default: 1.1.1.1 / 8.8.8.8.
|
||||
// primaryDns: '9.9.9.9',
|
||||
// fallbackDns: '149.112.112.112',
|
||||
|
||||
// Connect / DNS budgets. Defaults: 60s / 30s respectively.
|
||||
// connectTimeoutMs: 30_000,
|
||||
// dnsTimeoutMs: 15_000,
|
||||
|
||||
// mixFetch redirect chain depth. Default: 5.
|
||||
// maxRedirects: 10,
|
||||
};
|
||||
|
||||
async function main() {
|
||||
// options for mixFetch (you can also set these with the `createMixFetch` function
|
||||
const mixFetchOptions = {
|
||||
preferredGateway: '6Gb7ftQdKveMjPyrxDXeAtfYAX7Zg5mVZHtnRC5MmZ1B', // with WSS
|
||||
preferredNetworkRequester:
|
||||
'8rRGWy54oC8drFL9DepMegBt2DLrsqQwCoHMXt9nsnTo.2XjCPVbb4FpQ9hNRcXwb9mTzEAVVk1zf1tcch3wdtNEA@6Gb7ftQdKveMjPyrxDXeAtfYAX7Zg5mVZHtnRC5MmZ1B',
|
||||
mixFetchOverride: {
|
||||
requestTimeoutMs: 60_000,
|
||||
},
|
||||
};
|
||||
appendOutput('Setting up mixnet tunnel...');
|
||||
await setupMixTunnel(setupOpts);
|
||||
appendOutput('Tunnel ready.\n');
|
||||
|
||||
// disable CORS (in your app, you probably don't want to disable CORS, it is a good thing to leave it enabled)
|
||||
const args = { mode: 'unsafe-ignore-cors' };
|
||||
|
||||
// this is the URL of standard list of allow hosts the you can request data from with mixFetch and the Nym SOCKS5
|
||||
// client - you can request to have more hosts added by getting in touch on Discord or Telegram
|
||||
// Standard allowlist for the Nym network-requester. The IPR enforces its own
|
||||
// exit policy, so the URL must pass that policy regardless of the source.
|
||||
let url = 'https://nymtech.net/.wellknown/network-requester/standard-allowed-list.txt';
|
||||
|
||||
appendOutput('Get a text file:');
|
||||
appendOutput(`Downloading ${url}...\n`);
|
||||
let resp = await mixFetch(url, args, mixFetchOptions); // NB: you only need to pass options to the 1st call
|
||||
console.log({ resp });
|
||||
|
||||
let resp = await mixFetch(url);
|
||||
const text = await resp.text();
|
||||
appendOutput(text);
|
||||
|
||||
// get an image
|
||||
appendOutput('\nGet an image:\n');
|
||||
url = 'https://nymtech.net/favicon.svg';
|
||||
resp = await mixFetch(url, args);
|
||||
console.log({ resp });
|
||||
|
||||
url = 'https://httpbin.org/image/png';
|
||||
resp = await mixFetch(url);
|
||||
const buffer = await resp.arrayBuffer();
|
||||
const type = resp.headers.get('Content-Type') || 'image/svg';
|
||||
const type = resp.headers.get('Content-Type') || 'image/png';
|
||||
const blobUrl = URL.createObjectURL(new Blob([buffer], { type }));
|
||||
appendImageOutput(blobUrl);
|
||||
|
||||
// Per-request header override. smolmix-wasm ships a browser-shape header
|
||||
// shim (User-Agent + Accept + Accept-Language + Accept-Encoding); anything
|
||||
// you pass in `init.headers` wins over the shim defaults.
|
||||
appendOutput('\nOverride User-Agent for one request:\n');
|
||||
url = 'https://httpbin.org/headers';
|
||||
resp = await mixFetch(url, {
|
||||
headers: { 'User-Agent': 'mix-fetch-example/0.1' },
|
||||
});
|
||||
appendOutput(await resp.text());
|
||||
}
|
||||
|
||||
// wait for the html to load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
// let's do this!
|
||||
main();
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
appendOutput(`Error: ${err}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
const { createMixFetch, disconnectMixFetch } = require('@nymproject/mix-fetch-node-commonjs');
|
||||
|
||||
/**
|
||||
* The main entry point
|
||||
*/
|
||||
(async () => {
|
||||
console.log('Tester is starting up...');
|
||||
|
||||
const addr =
|
||||
'D274yd1h3L3pNJzdxE5VgJ7izAsAVMsDrQtFSkKUegfk.8J67cGbcwvrJKF3Kb16HVWWc9AnrFnEibNCm9zCkuVFu@Emswx6KXyjRfq1c2k4d4uD2e6nBSbH1biorCZUei8UNS';
|
||||
|
||||
console.log('About to set up mixFetch...');
|
||||
const { mixFetch } = await createMixFetch({
|
||||
preferredNetworkRequester: addr,
|
||||
clientId: 'node-client1',
|
||||
clientOverride: {
|
||||
coverTraffic: { disableLoopCoverTrafficStream: true },
|
||||
traffic: { disableMainPoissonPacketDistribution: true },
|
||||
},
|
||||
mixFetchOverride: { requestTimeoutMs: 60000 },
|
||||
responseBodyConfigMap: {},
|
||||
extra: {},
|
||||
});
|
||||
|
||||
globalThis.mixFetch = mixFetch;
|
||||
|
||||
if (!globalThis.mixFetch) {
|
||||
console.error('Oh no! Could not create mixFetch');
|
||||
} else {
|
||||
console.log('Ready!');
|
||||
}
|
||||
|
||||
let url = 'https://nymtech.net/.wellknown/network-requester/standard-allowed-list.txt';
|
||||
console.log(`Using mixFetch to get ${url}...`);
|
||||
const args = { mode: 'unsafe-ignore-cors' };
|
||||
|
||||
let resp = await mixFetch(url, args);
|
||||
console.log({ resp });
|
||||
const text = await resp.text();
|
||||
|
||||
console.log('disconnecting');
|
||||
await disconnectMixFetch();
|
||||
console.log('disconnected! all further usages should fail');
|
||||
|
||||
// get an image
|
||||
url = 'https://nymtech.net/favicon.svg';
|
||||
resp = await mixFetch(url, args);
|
||||
console.log({ resp });
|
||||
const buffer = await resp.arrayBuffer();
|
||||
const type = resp.headers.get('Content-Type') || 'image/svg';
|
||||
const blobUrl = URL.createObjectURL(new Blob([buffer], { type }));
|
||||
console.log(JSON.stringify({ bufferBytes: buffer.byteLength, blobUrl }, null, 2));
|
||||
console.log(blobUrl);
|
||||
})();
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "@nymproject/mix-fetch-node-js-example",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"start:server": "node server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/mix-fetch-node-commonjs": "^1.2.1-rc.2"
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
const express = require('express');
|
||||
const { mixFetch } = require('@nymproject/mix-fetch-node-commonjs');
|
||||
|
||||
const app = express();
|
||||
app.use(express.static('public'));
|
||||
|
||||
app.get('/nym-fetch', async (req, res) => {
|
||||
try {
|
||||
const args = {
|
||||
mode: 'unsafe-ignore-cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const url = req.query.url;
|
||||
|
||||
if (!url) {
|
||||
return res.status(400).send('input a valid url');
|
||||
}
|
||||
|
||||
const extra = {
|
||||
hiddenGateways: [
|
||||
{
|
||||
owner: 'n1ns3v70ul9gnl9l9fkyz8cyxfq75vjcmx8el0t3',
|
||||
host: 'sandbox-gateway1.nymtech.net',
|
||||
explicitIp: '35.158.238.80',
|
||||
identityKey: 'HjNEDJuotWV8VD4ufeA1jeheTnfNJ7Jorevp57hgaZua',
|
||||
sphinxKey: 'BoXeUD7ERGmzRauMjJD3itVNnQiH42ncUb6kcVLrb3dy',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mixFetchOptions = {
|
||||
nymApiUrl: 'https://sandbox-nym-api1.nymtech.net/api',
|
||||
preferredGateway: 'HjNEDJuotWV8VD4ufeA1jeheTnfNJ7Jorevp57hgaZua',
|
||||
preferredNetworkRequester:
|
||||
'AzGdJ4MU78Ex22NEWfeycbN7bt3PFZr1MtKstAdhfELG.GSxnKnvKPjjQm3FdtsgG5KyhP6adGbPHRmFWDH4XfUpP@HjNEDJuotWV8VD4ufeA1jeheTnfNJ7Jorevp57hgaZua',
|
||||
mixFetchOverride: {
|
||||
requestTimeoutMs: 60_000,
|
||||
},
|
||||
forceTls: false,
|
||||
extra,
|
||||
};
|
||||
|
||||
const response = await mixFetch(url, args, mixFetchOptions);
|
||||
const json = await response.json();
|
||||
res.send(json);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500).send(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(3000, () => console.log('Server running on port 3000'));
|
||||
@@ -0,0 +1,37 @@
|
||||
# mixWebSocket Usage Example
|
||||
|
||||
Shows how to open a WSS connection over the Nym mixnet using
|
||||
`@nymproject/mix-websocket`. The `MixWebSocket` class mirrors the browser
|
||||
`WebSocket` API where it makes sense.
|
||||
|
||||
```ts
|
||||
import { setupMixTunnel, MixWebSocket } from '@nymproject/mix-websocket';
|
||||
|
||||
await setupMixTunnel();
|
||||
const ws = new MixWebSocket('wss://echo.websocket.events');
|
||||
await ws.opened();
|
||||
ws.addEventListener('message', (e) => console.log(e.data));
|
||||
await ws.send('hello');
|
||||
```
|
||||
|
||||
## Running the example
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run start
|
||||
```
|
||||
|
||||
Open http://localhost:1234. The example echoes whatever you send via
|
||||
`echo.websocket.events`.
|
||||
|
||||
## Differences from the browser WebSocket
|
||||
|
||||
- `await ws.opened()` blocks until the upgrade completes.
|
||||
- `binaryType` is fixed to `arraybuffer`.
|
||||
- No `bufferedAmount`; writes queue through the tunnel worker.
|
||||
|
||||
## Sharing the tunnel
|
||||
|
||||
`setupMixTunnel()` is shared across `mix-fetch`, `mix-dns`, and
|
||||
`mix-websocket`. If another of those is already initialised, you can skip
|
||||
the setup line.
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@nymproject/mix-websocket-example-parcel",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"build": "parcel build --no-cache --no-content-hash",
|
||||
"serve": "serve dist",
|
||||
"start": "parcel --no-cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/mix-websocket": "workspace:*",
|
||||
"parcel": "^2.9.3"
|
||||
},
|
||||
"private": true,
|
||||
"source": "src/index.html"
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>mixWebSocket</title>
|
||||
<script type="module" src="./index.ts"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
padding: 2rem;
|
||||
}
|
||||
pre { background: #f4f4f4; padding: 0.5rem; border-radius: 4px; max-height: 320px; overflow: auto; }
|
||||
.row { display: flex; gap: 0.5rem; margin: 0.5rem 0; }
|
||||
input { flex: 1; padding: 0.4rem; font-family: monospace; }
|
||||
button { padding: 0.4rem 0.8rem; cursor: pointer; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>mixWebSocket</h1>
|
||||
<p>Opens a WSS connection to <code>wss://echo.websocket.events</code> through the Nym mixnet.</p>
|
||||
<div class="row">
|
||||
<input id="message" type="text" value="hello mixnet" placeholder="message">
|
||||
<button id="send" disabled>send</button>
|
||||
<button id="close" disabled>close</button>
|
||||
</div>
|
||||
<pre id="output"></pre>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,90 @@
|
||||
import { setupMixTunnel, MixWebSocket, type SetupMixTunnelOpts } from '@nymproject/mix-websocket';
|
||||
|
||||
function log(line: string) {
|
||||
const el = document.getElementById('output') as HTMLPreElement;
|
||||
el.appendChild(document.createTextNode(`${line}\n`));
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
|
||||
// Tunnel configuration. Every field is optional.
|
||||
//
|
||||
// `debug: true` turns on smolmix-wasm's verbose tracing so you can watch
|
||||
// the TLS handshake and WebSocket frame exchange in DevTools. Leave it off
|
||||
// in production.
|
||||
const setupOpts: SetupMixTunnelOpts = {
|
||||
debug: true,
|
||||
|
||||
// Pin a specific exit IPR. Otherwise auto-discovered from the topology.
|
||||
// preferredIpr: 'D1rrUqJY9pesL3pTaMaxLnpZGGYQ4ZpZwpQXCqaeBXTW.6PpFkRvF...',
|
||||
|
||||
// Anonymity / performance trade-off. Cover traffic + Poisson padding
|
||||
// smear timing patterns at the cost of bandwidth. Default: both on.
|
||||
// disableCoverTraffic: true,
|
||||
// disablePoissonTraffic: true,
|
||||
|
||||
// TCP keepalive cadence for the underlying smoltcp socket. Default: 10s.
|
||||
// Lower it if you need quicker dead-peer detection on idle WebSockets.
|
||||
// tcpKeepaliveMs: 5_000,
|
||||
|
||||
// Connect budget for the TCP + TLS + WS handshake. Default: 60s.
|
||||
// connectTimeoutMs: 30_000,
|
||||
};
|
||||
|
||||
// Public echo server. Sends each frame back to the client.
|
||||
const WS_URL = 'wss://echo.websocket.org';
|
||||
|
||||
async function main() {
|
||||
log('Setting up mixnet tunnel...');
|
||||
await setupMixTunnel(setupOpts);
|
||||
log('Tunnel ready.');
|
||||
|
||||
log(`Connecting to ${WS_URL}...`);
|
||||
const ws = new MixWebSocket(WS_URL);
|
||||
|
||||
ws.addEventListener('open', () => log('< open'));
|
||||
ws.addEventListener('message', (e) => {
|
||||
const data = (e as MessageEvent).data;
|
||||
if (typeof data === 'string') {
|
||||
log(`< text: ${data}`);
|
||||
} else if (data instanceof ArrayBuffer) {
|
||||
log(`< binary: ${data.byteLength} bytes`);
|
||||
}
|
||||
});
|
||||
ws.addEventListener('close', (e) => {
|
||||
const ce = e as CloseEvent;
|
||||
log(`< close: code=${ce.code} reason=${ce.reason || '(empty)'}`);
|
||||
});
|
||||
ws.addEventListener('error', (e) => {
|
||||
const evt = e as Event & { message?: string };
|
||||
log(`< error: ${evt.message ?? '(no detail)'}`);
|
||||
});
|
||||
|
||||
await ws.opened();
|
||||
|
||||
const sendBtn = document.getElementById('send') as HTMLButtonElement;
|
||||
const closeBtn = document.getElementById('close') as HTMLButtonElement;
|
||||
const input = document.getElementById('message') as HTMLInputElement;
|
||||
|
||||
sendBtn.disabled = false;
|
||||
closeBtn.disabled = false;
|
||||
|
||||
sendBtn.addEventListener('click', async () => {
|
||||
const value = input.value;
|
||||
if (!value) return;
|
||||
log(`> ${value}`);
|
||||
await ws.send(value);
|
||||
});
|
||||
|
||||
closeBtn.addEventListener('click', async () => {
|
||||
sendBtn.disabled = true;
|
||||
closeBtn.disabled = true;
|
||||
await ws.close(1000, 'user requested');
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
log(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
# smolmix SDK playground (internal-dev)
|
||||
|
||||
Internal-only browser playground that exercises the smolmix-based TS SDK
|
||||
family end-to-end against a live mixnet:
|
||||
|
||||
- `@nymproject/mix-tunnel` — tunnel lifecycle (setup, state, disconnect)
|
||||
- `@nymproject/mix-fetch` — HTTP/HTTPS through the mixnet
|
||||
- `@nymproject/mix-dns` — hostname resolution via the IPR
|
||||
- `@nymproject/mix-websocket` — WS/WSS through the mixnet
|
||||
|
||||
Use at your own risk; this is dev scaffolding, not a polished demo.
|
||||
|
||||
## Getting started
|
||||
|
||||
From the repo root:
|
||||
|
||||
```
|
||||
make sdk-wasm-build # build smolmix-wasm pkg/
|
||||
pnpm i # install workspace deps (resolves workspace:* refs)
|
||||
```
|
||||
|
||||
Then build the four TS SDK packages so the playground can resolve them:
|
||||
|
||||
```
|
||||
pnpm build:ci:sdk
|
||||
```
|
||||
|
||||
Then start the dev server (webpack):
|
||||
|
||||
```
|
||||
cd sdk/typescript/packages/internal-dev
|
||||
pnpm start
|
||||
```
|
||||
|
||||
Open <http://localhost:3000/>. The page has four sections (Tunnel, Fetch,
|
||||
DNS, WebSocket); start with **Setup tunnel** and wait for the state to flip
|
||||
green. The other three sections are then live.
|
||||
|
||||
Alternative: parcel-based playground in `parcel/` — same source, different
|
||||
bundler.
|
||||
|
||||
## Iterating on the SDKs
|
||||
|
||||
If you edit a TS SDK package (e.g. `mix-fetch`) the playground won't see the
|
||||
change until the package is rebuilt. From the repo root:
|
||||
|
||||
```
|
||||
pnpm --filter @nymproject/mix-fetch build
|
||||
```
|
||||
|
||||
webpack-dev-server picks up the new `dist/` via the workspace symlink and
|
||||
hot-reloads.
|
||||
|
||||
If you edit `mix-tunnel`'s rollup config or anything touching the inlined
|
||||
worker bundle, run the package's own `pnpm build` so the rollup chain
|
||||
re-runs:
|
||||
|
||||
```
|
||||
pnpm --filter @nymproject/mix-tunnel build
|
||||
```
|
||||
|
||||
If you edit `wasm/smolmix` itself, run `make -C wasm/smolmix build-debug`
|
||||
to regenerate `pkg/` — then `pnpm --filter @nymproject/mix-tunnel build`
|
||||
to pick up the new wasm bytes.
|
||||
|
||||
## Stuck?
|
||||
|
||||
```
|
||||
rm -rf node_modules && pnpm i && pnpm start
|
||||
```
|
||||
|
||||
Often resets pnpm-link state if the workspace package symlinks have drifted.
|
||||
+9
-5
@@ -1,14 +1,19 @@
|
||||
{
|
||||
"name": "@nymproject/mix-fetch-tester-webpack",
|
||||
"version": "1.0.6",
|
||||
"name": "@nymproject/smolmix-internal-dev",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "Internal playground exercising the smolmix-based TS SDK family (mix-tunnel, mix-fetch, mix-dns, mix-websocket).",
|
||||
"scripts": {
|
||||
"build": "webpack build --progress --config webpack.prod.js",
|
||||
"serve": "npx serve dist",
|
||||
"start": "webpack serve --progress --port 3000"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/mix-fetch": ">=1.4.2-rc.0 || ^1"
|
||||
"@nymproject/mix-tunnel": "workspace:*",
|
||||
"@nymproject/mix-fetch": "workspace:*",
|
||||
"@nymproject/mix-dns": "workspace:*",
|
||||
"@nymproject/mix-websocket": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "catalog:",
|
||||
@@ -53,6 +58,5 @@
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "catalog:",
|
||||
"webpack-merge": "catalog:"
|
||||
},
|
||||
"private": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@nymproject/smolmix-internal-dev-parcel",
|
||||
"version": "0.0.0",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"build": "npx parcel build --no-cache --no-content-hash",
|
||||
"serve": "npx serve dist",
|
||||
"start": "npx parcel --no-cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/mix-tunnel": "workspace:*",
|
||||
"@nymproject/mix-fetch": "workspace:*",
|
||||
"@nymproject/mix-dns": "workspace:*",
|
||||
"@nymproject/mix-websocket": "workspace:*"
|
||||
},
|
||||
"private": true,
|
||||
"source": "../src/index.html"
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>smolmix TS SDK dev</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6' fill='%23333'/%3E%3Ccircle cx='8' cy='8' r='2' fill='%23c8f7c5'/%3E%3C/svg%3E">
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; max-width: 1000px; margin: 16px auto; padding: 0 12px; }
|
||||
fieldset { margin-bottom: 12px; }
|
||||
legend { font-weight: bold; }
|
||||
.row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
||||
.status { font-size: 0.85em; color: gray; }
|
||||
.local-log {
|
||||
background: #f5f5f5; border: 1px solid #ddd; padding: 6px;
|
||||
margin: 8px 0 0; max-height: 140px; overflow-y: auto;
|
||||
font-size: 0.82em; white-space: pre-wrap;
|
||||
font-family: ui-monospace, monospace;
|
||||
}
|
||||
</style>
|
||||
<!-- HtmlWebpackPlugin injects <script src="index.js"> at build time. -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>smolmix TS SDK dev</h1>
|
||||
<p class="status">
|
||||
Drives <code>@nymproject/mix-tunnel</code>, <code>mix-fetch</code>,
|
||||
<code>mix-dns</code>, <code>mix-websocket</code> against a live mixnet.
|
||||
Mirrors <code>wasm/smolmix/internal-dev</code> so you can A/B the SDK
|
||||
layer against the raw WASM.
|
||||
</p>
|
||||
|
||||
<!-- Connection -->
|
||||
<fieldset id="startup-controls">
|
||||
<legend>Connection</legend>
|
||||
<div>
|
||||
<label>IPR address: </label>
|
||||
<input type="text" size="80" id="ipr-address" disabled
|
||||
value="HEpAyUwqTeYQ6sxrL8Pbp6UNVPG6ta1CcbzP9q7vBggp.2byzbUSC6J5t9gpFFXPPnB7tZKW3c6cX1QSfmzYRq3wp@FQBbq1crAkCrjVBnEN85VqgZgGRMLJV65NJk8bPADdw"
|
||||
placeholder="<nym-address of IPR exit node>" />
|
||||
<label style="margin-left: 8px">
|
||||
<input type="checkbox" id="opt-random-ipr" checked /> Use random IPR
|
||||
</label>
|
||||
</div>
|
||||
<details style="margin-top: 8px" open>
|
||||
<summary style="cursor: pointer; font-size: 0.9em; color: #555">Advanced Options</summary>
|
||||
<div style="margin-top: 6px; padding: 8px; background: #f9f9f9; font-size: 0.9em">
|
||||
<div>
|
||||
<label><input type="checkbox" id="opt-force-tls" checked /> Force TLS</label>
|
||||
</div>
|
||||
<div style="margin-top: 4px">
|
||||
<label>Client ID: <input type="text" id="opt-client-id" size="20" /></label>
|
||||
<span class="status">(randomised on load for clean state)</span>
|
||||
</div>
|
||||
<div style="margin-top: 4px">
|
||||
<label><input type="checkbox" id="opt-disable-poisson" /> Disable Poisson traffic</label>
|
||||
</div>
|
||||
<div style="margin-top: 4px">
|
||||
<label><input type="checkbox" id="opt-disable-cover" /> Disable cover traffic</label>
|
||||
</div>
|
||||
<div style="margin-top: 4px">
|
||||
<label>Open reply SURBs:
|
||||
<input type="number" id="opt-open-surbs" value="10" min="1" max="50" style="width: 60px" />
|
||||
</label>
|
||||
<label style="margin-left: 8px">Data reply SURBs:
|
||||
<input type="number" id="opt-data-surbs" value="10" min="0" max="50" style="width: 60px" />
|
||||
</label>
|
||||
</div>
|
||||
<div style="margin-top: 4px">
|
||||
<label>Primary DNS:
|
||||
<input type="text" id="opt-primary-dns" placeholder="8.8.8.8:53" size="18" />
|
||||
</label>
|
||||
<label style="margin-left: 8px">Fallback DNS:
|
||||
<input type="text" id="opt-fallback-dns" placeholder="1.1.1.1:53" size="18" />
|
||||
</label>
|
||||
</div>
|
||||
<div style="margin-top: 4px">
|
||||
<label>User-Agent override:
|
||||
<input type="text" id="opt-user-agent" size="40" placeholder="(empty → wasm-shim default)" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<div style="margin-top: 8px">
|
||||
<button id="btn-setup">setupMixTunnel</button>
|
||||
<button id="btn-disconnect" disabled>disconnectMixTunnel</button>
|
||||
<label style="margin-left: 16px">
|
||||
<input type="checkbox" id="opt-debug-logging" checked /> Debug logging
|
||||
</label>
|
||||
<span id="tunnel-status" class="status" style="margin-left: 10px">Not started</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- DNS Resolve: tunnel (mixDNS) vs clearnet (DoH JSON) -->
|
||||
<fieldset id="dns-controls">
|
||||
<legend>DNS Resolve</legend>
|
||||
<div class="row">
|
||||
<input type="text" size="40" id="dns-host" value="example.com" />
|
||||
<button id="btn-dns-tunnel" disabled>via tunnel</button>
|
||||
<button id="btn-dns-clearnet">via DoH (clearnet)</button>
|
||||
</div>
|
||||
<pre class="local-log" id="dns-log"></pre>
|
||||
</fieldset>
|
||||
|
||||
<!-- GET: tunnel vs clearnet, same URL. Preset buttons fill the URL field. -->
|
||||
<fieldset id="get-controls">
|
||||
<legend>GET</legend>
|
||||
<div class="row">
|
||||
<input type="text" size="60" id="get-url" value="https://nymtech.net/.wellknown/network-requester/standard-allowed-list.txt" />
|
||||
<button id="btn-get-tunnel" disabled>via tunnel</button>
|
||||
<button id="btn-get-clearnet">via window.fetch (clearnet)</button>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 4px">
|
||||
<span class="status">presets:</span>
|
||||
<button class="preset" data-url="https://upload.wikimedia.org/wikipedia/commons/8/80/Wikipedia-logo-v2.svg" data-render="image">Wikimedia logo (image)</button>
|
||||
<button class="preset" data-url="https://www.cloudflare.com/cdn-cgi/trace">Cloudflare trace</button>
|
||||
<button class="preset" data-url="https://httpbin.org/get">httpbin /get</button>
|
||||
</div>
|
||||
<pre class="local-log" id="get-log"></pre>
|
||||
<div id="get-image-output"></div>
|
||||
</fieldset>
|
||||
|
||||
<!-- WebSocket -->
|
||||
<fieldset id="ws-controls" disabled>
|
||||
<legend>WebSocket</legend>
|
||||
<div class="row">
|
||||
<input type="text" size="60" id="ws-url" value="wss://echo.websocket.org" />
|
||||
<button id="btn-ws-connect">Connect</button>
|
||||
<button id="btn-ws-close" disabled>Close</button>
|
||||
<span id="ws-status" class="status">Not connected</span>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 4px">
|
||||
<input type="text" size="50" id="ws-message" value="Hello from mix-websocket!" />
|
||||
<button id="btn-ws-send" disabled>Send</button>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 4px">
|
||||
<label>Echo burst:</label>
|
||||
<input type="number" id="ws-burst-count" value="10" min="1" max="500" style="width: 60px" />
|
||||
<label>Size:</label>
|
||||
<input type="number" id="ws-burst-min" value="64" min="1" max="1048576" style="width: 80px" />
|
||||
<span>–</span>
|
||||
<input type="number" id="ws-burst-max" value="1024" min="1" max="1048576" style="width: 80px" />
|
||||
<span class="status">bytes</span>
|
||||
<button id="btn-ws-burst" disabled>Send Burst</button>
|
||||
</div>
|
||||
<pre class="local-log" id="ws-log"></pre>
|
||||
</fieldset>
|
||||
|
||||
<!-- Stress Test -->
|
||||
<fieldset id="stress-controls" disabled>
|
||||
<legend>Stress Test</legend>
|
||||
<div class="row">
|
||||
<label>Requests:</label>
|
||||
<input type="number" id="stress-count" value="10" min="1" max="200" style="width: 60px" />
|
||||
<label>Mode:</label>
|
||||
<select id="stress-mode">
|
||||
<option value="uniform">Uniform</option>
|
||||
<option value="mixed" selected>Mixed sizes</option>
|
||||
<option value="drip">Slow drip</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="stress-uniform-opts" style="display: none; margin-top: 8px; padding: 8px; background: #f9f9f9">
|
||||
<label>Base URL:</label>
|
||||
<input type="text" size="50" id="stress-url" value="https://jsonplaceholder.typicode.com/posts/" />
|
||||
</div>
|
||||
|
||||
<div id="stress-mixed-opts" style="margin-top: 8px; padding: 8px; background: #f9f9f9">
|
||||
<table style="font-size: 0.9em; border-collapse: collapse">
|
||||
<tr><td style="padding: 1px 10px 1px 0"><b>tiny</b></td><td>128 B</td></tr>
|
||||
<tr><td style="padding: 1px 10px 1px 0"><b>small</b></td><td>1 KB</td></tr>
|
||||
<tr><td style="padding: 1px 10px 1px 0"><b>medium</b></td><td>10 KB</td></tr>
|
||||
<tr><td style="padding: 1px 10px 1px 0"><b>large</b></td><td>100 KB</td></tr>
|
||||
<tr><td style="padding: 1px 10px 1px 0"><b>xlarge</b></td><td>1 MB</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="stress-drip-opts" style="display: none; margin-top: 8px; padding: 8px; background: #f9f9f9">
|
||||
<table style="font-size: 0.9em; border-collapse: collapse">
|
||||
<tr><td style="padding: 1px 10px 1px 0"><b>safe</b></td><td>~50% of timeout</td></tr>
|
||||
<tr><td style="padding: 1px 10px 1px 0"><b>boundary</b></td><td>~92% of timeout</td></tr>
|
||||
<tr><td style="padding: 1px 10px 1px 0"><b>over</b></td><td>~108% of timeout</td></tr>
|
||||
<tr><td style="padding: 1px 10px 1px 0"><b>slow-start</b></td><td>~17% delay + ~83% drip</td></tr>
|
||||
</table>
|
||||
<div style="margin-top: 6px">
|
||||
<label>Request timeout (s):</label>
|
||||
<input type="number" id="stress-timeout" value="60" min="5" max="300" style="width: 60px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 8px">
|
||||
<button id="btn-stress">Run Stress Test</button>
|
||||
<span id="stress-status" class="status"></span>
|
||||
</div>
|
||||
<pre class="local-log" id="stress-log"></pre>
|
||||
</fieldset>
|
||||
|
||||
<!-- File Download -->
|
||||
<fieldset id="download-controls" disabled>
|
||||
<legend>File Download</legend>
|
||||
<div style="display: flex; gap: 12px; flex-wrap: wrap">
|
||||
<div style="flex: 1; min-width: 220px; padding: 8px; background: #f9f9f9; border: 1px solid #ddd">
|
||||
<b>UTF-8 Demo</b>
|
||||
<div class="status">Unicode text (Cambridge CS)</div>
|
||||
<button id="btn-verify-text">Fetch</button>
|
||||
<span id="verify-text-status" class="status"></span>
|
||||
<pre id="verify-text-output" style="margin-top: 6px; max-height: 200px; overflow-y: auto; font-size: 0.8em; white-space: pre-wrap; display: none; background: #fff; padding: 6px; border: 1px solid #eee"></pre>
|
||||
</div>
|
||||
<div style="flex: 1; min-width: 220px; padding: 8px; background: #f9f9f9; border: 1px solid #ddd">
|
||||
<b>File Download</b>
|
||||
<div style="margin: 4px 0 6px">
|
||||
<input type="text" size="50" id="download-url"
|
||||
value="https://nymtech.net/uploads/Nym_WFP_Paper_5_58a1105679.pdf" />
|
||||
</div>
|
||||
<button id="btn-verify-pdf">Fetch</button>
|
||||
<button id="btn-save-pdf" disabled>Save</button>
|
||||
<span id="verify-pdf-status" class="status"></span>
|
||||
<div id="verify-pdf-output" style="margin-top: 6px; font-size: 0.8em; display: none">
|
||||
<div>Size: <code id="verify-pdf-size"></code></div>
|
||||
<div>SHA-256: <code id="verify-pdf-sha"></code></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="local-log" id="download-log"></pre>
|
||||
</fieldset>
|
||||
|
||||
<!-- Master timeline -->
|
||||
<fieldset>
|
||||
<legend>Output (master timeline)</legend>
|
||||
<pre id="output" style="background: #f5f5f5; padding: 8px; max-height: 300px; overflow-y: auto; font-size: 0.85em; white-space: pre-wrap"></pre>
|
||||
</fieldset>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,680 @@
|
||||
// smolmix TS SDK dev — mirrors wasm/smolmix/internal-dev so the two
|
||||
// playgrounds run the same scenarios; the SDK layer adds (mostly) nothing
|
||||
// the raw WASM doesn't already do, so observed behaviour should match.
|
||||
|
||||
import {
|
||||
setupMixTunnel,
|
||||
disconnectMixTunnel,
|
||||
SetupMixTunnelOpts,
|
||||
} from '@nymproject/mix-tunnel';
|
||||
import { mixFetch } from '@nymproject/mix-fetch';
|
||||
import { mixDNS } from '@nymproject/mix-dns';
|
||||
import { MixWebSocket } from '@nymproject/mix-websocket';
|
||||
|
||||
// Helpers ============================================================
|
||||
|
||||
const $ = <T extends HTMLElement = HTMLElement>(id: string): T => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) throw new Error(`#${id} not found`);
|
||||
return el as T;
|
||||
};
|
||||
|
||||
type LogColour = 'green' | 'red' | 'orange' | 'gray' | undefined;
|
||||
|
||||
function display(msg: string, colour?: LogColour) {
|
||||
const ts = new Date().toISOString().slice(11, 23);
|
||||
const line = document.createElement('div');
|
||||
if (colour) line.style.color = colour;
|
||||
line.textContent = `[${ts}] ${msg}`;
|
||||
const out = $('output');
|
||||
out.appendChild(line);
|
||||
out.scrollTop = out.scrollHeight;
|
||||
if (colour === 'red') console.error('[sdk-dev]', msg);
|
||||
}
|
||||
|
||||
function logTo(targetId: string, msg: string, colour?: LogColour) {
|
||||
const target = document.getElementById(targetId);
|
||||
if (!target) return;
|
||||
const ts = new Date().toISOString().slice(11, 23);
|
||||
const line = document.createElement('div');
|
||||
if (colour) line.style.color = colour;
|
||||
line.textContent = `[${ts}] ${msg}`;
|
||||
target.appendChild(line);
|
||||
target.scrollTop = target.scrollHeight;
|
||||
if (colour === 'red') console.error(`[sdk-dev:${targetId}]`, msg);
|
||||
}
|
||||
|
||||
const 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`;
|
||||
};
|
||||
|
||||
const formatRate = (bytes: number, ms: number): string =>
|
||||
`${(bytes / 1024 / (ms / 1000)).toFixed(1)} KB/s`;
|
||||
|
||||
const 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;
|
||||
};
|
||||
|
||||
async function sha256hex(bytes: BufferSource): Promise<string> {
|
||||
const hash = await crypto.subtle.digest('SHA-256', bytes);
|
||||
return Array.from(new Uint8Array(hash), (b) => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
function saveFile(buf: BlobPart, filename: string, mimeType: string) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Tunnel-gated UI bits. GET + DNS fieldsets stay enabled so their clearnet
|
||||
// buttons work without a tunnel; only the per-button gates are toggled.
|
||||
const GATED_FIELDSETS = ['ws-controls', 'stress-controls', 'download-controls'];
|
||||
const GATED_BUTTONS = ['btn-get-tunnel', 'btn-dns-tunnel'];
|
||||
|
||||
function setTunnelButtonsEnabled(enabled: boolean) {
|
||||
for (const id of GATED_FIELDSETS) ($(id) as HTMLFieldSetElement).disabled = !enabled;
|
||||
for (const id of GATED_BUTTONS) ($(id) as HTMLButtonElement).disabled = !enabled;
|
||||
}
|
||||
|
||||
const strField = (id: string): string | undefined => {
|
||||
const v = ($(id) as HTMLInputElement).value.trim();
|
||||
return v.length === 0 ? undefined : v;
|
||||
};
|
||||
const numField = (id: string): number | undefined => {
|
||||
const v = ($(id) as HTMLInputElement).value.trim();
|
||||
if (!v) return undefined;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
};
|
||||
|
||||
// Override the default User-Agent header injected by the wasm-shim. Kept in
|
||||
// module scope so each fetch can reach it without re-reading the input.
|
||||
let userAgentOverride: string | undefined;
|
||||
const fetchInit = (): RequestInit | undefined =>
|
||||
userAgentOverride ? { headers: { 'User-Agent': userAgentOverride } } : undefined;
|
||||
|
||||
// Connection =========================================================
|
||||
|
||||
$('opt-random-ipr').addEventListener('change', (e) => {
|
||||
$('ipr-address').toggleAttribute('disabled', (e.target as HTMLInputElement).checked);
|
||||
});
|
||||
|
||||
$('btn-setup').addEventListener('click', async () => {
|
||||
const useRandom = ($('opt-random-ipr') as HTMLInputElement).checked;
|
||||
const ipr = useRandom ? undefined : ($('ipr-address') as HTMLInputElement).value.trim();
|
||||
if (!useRandom && !ipr) {
|
||||
display('IPR address is required (or check "Use random IPR")', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
const statusEl = $('tunnel-status');
|
||||
($('btn-setup') as HTMLButtonElement).disabled = true;
|
||||
statusEl.textContent = 'Connecting...';
|
||||
statusEl.style.color = 'orange';
|
||||
|
||||
userAgentOverride = strField('opt-user-agent');
|
||||
|
||||
const opts: SetupMixTunnelOpts = {
|
||||
...(ipr ? { preferredIpr: ipr } : {}),
|
||||
clientId: strField('opt-client-id'),
|
||||
forceTls: ($('opt-force-tls') as HTMLInputElement).checked,
|
||||
disablePoissonTraffic: ($('opt-disable-poisson') as HTMLInputElement).checked,
|
||||
disableCoverTraffic: ($('opt-disable-cover') as HTMLInputElement).checked,
|
||||
openReplySurbs: numField('opt-open-surbs'),
|
||||
dataReplySurbs: numField('opt-data-surbs'),
|
||||
primaryDns: strField('opt-primary-dns'),
|
||||
fallbackDns: strField('opt-fallback-dns'),
|
||||
debug: ($('opt-debug-logging') as HTMLInputElement).checked,
|
||||
};
|
||||
|
||||
display(`setupMixTunnel (clientId=${opts.clientId}, IPR: ${ipr ? `${ipr.slice(0, 30)}...` : 'auto-discover'})`);
|
||||
|
||||
try {
|
||||
const t0 = performance.now();
|
||||
await setupMixTunnel(opts);
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
display(`tunnel ready in ${ms} ms`, 'green');
|
||||
statusEl.textContent = `Connected (${ms} ms)`;
|
||||
statusEl.style.color = 'green';
|
||||
setTunnelButtonsEnabled(true);
|
||||
($('btn-disconnect') as HTMLButtonElement).disabled = false;
|
||||
} catch (e) {
|
||||
const msg = String(e);
|
||||
display(`setupMixTunnel failed: ${msg}`, 'red');
|
||||
statusEl.textContent = `Failed: ${msg}`;
|
||||
statusEl.style.color = 'red';
|
||||
statusEl.title = msg;
|
||||
($('btn-setup') as HTMLButtonElement).disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
$('btn-disconnect').addEventListener('click', async () => {
|
||||
display('Disconnecting...');
|
||||
try {
|
||||
await disconnectMixTunnel();
|
||||
display('Disconnected', 'green');
|
||||
$('tunnel-status').textContent = 'Disconnected';
|
||||
$('tunnel-status').style.color = 'gray';
|
||||
setTunnelButtonsEnabled(false);
|
||||
($('btn-disconnect') as HTMLButtonElement).disabled = true;
|
||||
// Tunnel uses OnceLock semantics — no re-setup without page reload.
|
||||
($('btn-setup') as HTMLButtonElement).disabled = true;
|
||||
} catch (e) {
|
||||
display(`Disconnect failed: ${e}`, 'red');
|
||||
}
|
||||
});
|
||||
|
||||
// DNS Resolve ========================================================
|
||||
|
||||
$('btn-dns-tunnel').addEventListener('click', async () => {
|
||||
const host = ($('dns-host') as HTMLInputElement).value.trim();
|
||||
if (!host) { logTo('dns-log', 'Hostname is required', 'red'); return; }
|
||||
|
||||
const btn = $('btn-dns-tunnel') as HTMLButtonElement;
|
||||
btn.disabled = true;
|
||||
logTo('dns-log', `tunnel resolve ${host}`);
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const ip = await mixDNS(host);
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
logTo('dns-log', `tunnel ${host} => ${ip} (${ms} ms)`, 'green');
|
||||
} catch (e) {
|
||||
logTo('dns-log', `tunnel resolve failed: ${e}`, 'red');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Browsers expose no raw DNS API; the closest analogue from JS is DoH via
|
||||
// HTTPS to a public resolver. Google's JSON API is CORS-friendly.
|
||||
$('btn-dns-clearnet').addEventListener('click', async () => {
|
||||
const host = ($('dns-host') as HTMLInputElement).value.trim();
|
||||
if (!host) { logTo('dns-log', 'Hostname is required', 'red'); return; }
|
||||
|
||||
logTo('dns-log', `clearnet DoH resolve ${host}`);
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const resp = await window.fetch(
|
||||
`https://dns.google/resolve?name=${encodeURIComponent(host)}&type=A`,
|
||||
{ mode: 'cors' },
|
||||
);
|
||||
const json = (await resp.json()) as { Status: number; Answer?: Array<{ type: number; data: string }> };
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
|
||||
if (json.Status !== 0) {
|
||||
logTo('dns-log', `clearnet DoH error: status=${json.Status} (${ms} ms)`, 'red');
|
||||
return;
|
||||
}
|
||||
const a = json.Answer?.find((r) => r.type === 1);
|
||||
if (!a) { logTo('dns-log', `clearnet DoH: no A record (${ms} ms)`, 'orange'); return; }
|
||||
logTo('dns-log', `clearnet ${host} => ${a.data} (${ms} ms); visible in DevTools Network`, 'green');
|
||||
} catch (e) {
|
||||
logTo('dns-log', `clearnet DoH failed: ${e}`, 'red');
|
||||
}
|
||||
});
|
||||
|
||||
// GET ===============================================================
|
||||
|
||||
// `asImage` is a per-call argument rather than module state because GET
|
||||
// requests interleave: a preset-fired image fetch may still be awaiting its
|
||||
// body when a second preset click arrives. Closing over the flag in the
|
||||
// caller's scope keeps each invocation's intent isolated.
|
||||
async function getViaTunnel(url: string, asImage: boolean) {
|
||||
logTo('get-log', `tunnel GET ${url}`);
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const resp = await mixFetch(url, fetchInit());
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
const ct = resp.headers.get('content-type') ?? '?';
|
||||
logTo('get-log', `tunnel ${resp.status} ${resp.statusText} (${ms} ms, ${ct})`, 'green');
|
||||
|
||||
if (!resp.ok) {
|
||||
const errText = await resp.text();
|
||||
logTo('get-log', errText.length > 600 ? `${errText.slice(0, 600)}\n... (${errText.length} bytes total)` : errText);
|
||||
return;
|
||||
}
|
||||
|
||||
if (asImage) {
|
||||
const buf = await resp.arrayBuffer();
|
||||
const type = resp.headers.get('content-type') ?? 'image/svg+xml';
|
||||
const blobUrl = URL.createObjectURL(new Blob([buf], { type }));
|
||||
const img = document.createElement('img');
|
||||
img.src = blobUrl;
|
||||
img.style.maxWidth = '120px';
|
||||
img.style.marginRight = '4px';
|
||||
img.title = url;
|
||||
$('get-image-output').appendChild(img);
|
||||
} else {
|
||||
const text = await resp.text();
|
||||
logTo('get-log', text.length > 400 ? `${text.slice(0, 400)}\n... (${text.length} bytes total)` : text);
|
||||
}
|
||||
} catch (e) {
|
||||
logTo('get-log', `tunnel GET failed: ${e}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
$('btn-get-tunnel').addEventListener('click', () => {
|
||||
const url = ($('get-url') as HTMLInputElement).value.trim();
|
||||
if (!url) { logTo('get-log', 'URL is required', 'red'); return; }
|
||||
getViaTunnel(url, false);
|
||||
});
|
||||
|
||||
$('btn-get-clearnet').addEventListener('click', async () => {
|
||||
const url = ($('get-url') as HTMLInputElement).value.trim();
|
||||
if (!url) { logTo('get-log', 'URL is required', 'red'); return; }
|
||||
logTo('get-log', `clearnet GET ${url}`);
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const resp = await window.fetch(url, { mode: 'cors' });
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
logTo('get-log', `clearnet ${resp.status} ${resp.statusText} (${ms} ms); visible in DevTools Network`, 'green');
|
||||
} catch (e) {
|
||||
logTo('get-log', `clearnet fetch failed: ${e}`, 'red');
|
||||
}
|
||||
});
|
||||
|
||||
// Preset buttons fill the URL field and immediately tunnel-fetch. The
|
||||
// `data-render="image"` hint asks the GET handler to render the body as an
|
||||
// inline <img> rather than logging the response text.
|
||||
for (const btn of Array.from(document.querySelectorAll<HTMLButtonElement>('button.preset'))) {
|
||||
btn.addEventListener('click', () => {
|
||||
const url = btn.dataset.url ?? '';
|
||||
if (!url) return;
|
||||
($('get-url') as HTMLInputElement).value = url;
|
||||
getViaTunnel(url, btn.dataset.render === 'image');
|
||||
});
|
||||
}
|
||||
|
||||
// WebSocket =========================================================
|
||||
|
||||
let activeWs: MixWebSocket | undefined;
|
||||
let wsConnectT0 = 0;
|
||||
const wsSendQueue: number[] = [];
|
||||
|
||||
// Burst-mode state: collected silently to avoid log spam during 500-msg runs.
|
||||
let wsBurstActive = false;
|
||||
let wsBurstRtts: number[] = [];
|
||||
let wsBurstExpected = 0;
|
||||
let wsBurstResolve: (() => void) | null = null;
|
||||
let wsBurstHashes: string[] = [];
|
||||
let wsBurstVerified = 0;
|
||||
let wsBurstMismatches = 0;
|
||||
|
||||
function setWsButtonState(state: 'connected' | 'connecting' | 'disconnected') {
|
||||
const connected = state === 'connected';
|
||||
const connecting = state === 'connecting';
|
||||
($('btn-ws-connect') as HTMLButtonElement).disabled = connected || connecting;
|
||||
($('btn-ws-send') as HTMLButtonElement).disabled = !connected;
|
||||
($('btn-ws-close') as HTMLButtonElement).disabled = !connected;
|
||||
($('btn-ws-burst') as HTMLButtonElement).disabled = !connected;
|
||||
}
|
||||
|
||||
$('btn-ws-connect').addEventListener('click', () => {
|
||||
const url = ($('ws-url') as HTMLInputElement).value.trim();
|
||||
if (!url) { logTo('ws-log', 'WebSocket URL is required', 'red'); return; }
|
||||
|
||||
// Tear down any prior connection so a rapid double-click doesn't leak it.
|
||||
if (activeWs && activeWs.readyState !== 3 /* CLOSED */) activeWs.close();
|
||||
|
||||
const statusEl = $('ws-status');
|
||||
statusEl.textContent = 'Connecting...';
|
||||
statusEl.style.color = 'orange';
|
||||
setWsButtonState('connecting');
|
||||
wsSendQueue.length = 0;
|
||||
|
||||
logTo('ws-log', `connecting to ${url}`);
|
||||
wsConnectT0 = performance.now();
|
||||
|
||||
const ws = new MixWebSocket(url);
|
||||
activeWs = ws;
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
const ms = (performance.now() - wsConnectT0).toFixed(0);
|
||||
logTo('ws-log', `connected in ${ms} ms`, 'green');
|
||||
statusEl.textContent = `Connected (${ms} ms)`;
|
||||
statusEl.style.color = 'green';
|
||||
setWsButtonState('connected');
|
||||
});
|
||||
|
||||
ws.addEventListener('message', async (ev) => {
|
||||
const data = (ev as MessageEvent).data;
|
||||
let preview: string;
|
||||
let bytes: Uint8Array<ArrayBuffer> | undefined;
|
||||
if (typeof data === 'string') {
|
||||
preview = data.length <= 200 ? data : `${data.slice(0, 200)}...`;
|
||||
} else if (data instanceof ArrayBuffer) {
|
||||
bytes = new Uint8Array(data as ArrayBuffer);
|
||||
preview = `[binary ${bytes.length} bytes] ${hexPreview(bytes)}`;
|
||||
} else {
|
||||
preview = `[unknown ${typeof data}]`;
|
||||
}
|
||||
|
||||
const rttMs = wsSendQueue.length > 0 ? performance.now() - (wsSendQueue.shift() as number) : null;
|
||||
|
||||
if (wsBurstActive) {
|
||||
if (rttMs !== null) wsBurstRtts.push(rttMs);
|
||||
// Verify echo content against the recorded send hash.
|
||||
if (bytes) {
|
||||
const hash = await sha256hex(bytes);
|
||||
if (hash === wsBurstHashes[wsBurstVerified + wsBurstMismatches]) wsBurstVerified += 1;
|
||||
else wsBurstMismatches += 1;
|
||||
}
|
||||
if (wsBurstRtts.length >= wsBurstExpected && wsBurstResolve) wsBurstResolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (rttMs !== null) logTo('ws-log', `recv (${rttMs.toFixed(0)} ms RTT): ${preview}`, 'green');
|
||||
else logTo('ws-log', `recv: ${preview}`, 'green');
|
||||
});
|
||||
|
||||
ws.addEventListener('close', (ev) => {
|
||||
const ce = ev as CloseEvent;
|
||||
logTo('ws-log', `closed: ${ce.code} ${ce.reason}`, 'orange');
|
||||
statusEl.textContent = 'Closed';
|
||||
statusEl.style.color = 'gray';
|
||||
setWsButtonState('disconnected');
|
||||
activeWs = undefined;
|
||||
});
|
||||
|
||||
ws.addEventListener('error', (ev) => {
|
||||
// MixWebSocket attaches a non-standard `.message` to its error events so
|
||||
// the playground can surface the underlying cause.
|
||||
const msg = (ev as Event & { message?: string }).message ?? '(no detail)';
|
||||
logTo('ws-log', `error: ${msg}`, 'red');
|
||||
statusEl.textContent = 'Error';
|
||||
statusEl.style.color = 'red';
|
||||
});
|
||||
});
|
||||
|
||||
$('btn-ws-send').addEventListener('click', async () => {
|
||||
if (!activeWs || activeWs.readyState !== 1 /* OPEN */) return;
|
||||
const msg = ($('ws-message') as HTMLInputElement).value;
|
||||
wsSendQueue.push(performance.now());
|
||||
try {
|
||||
await activeWs.send(msg);
|
||||
logTo('ws-log', `send: ${msg}`);
|
||||
} catch (e) {
|
||||
logTo('ws-log', `send failed: ${e}`, 'red');
|
||||
}
|
||||
});
|
||||
|
||||
$('btn-ws-close').addEventListener('click', async () => {
|
||||
if (!activeWs) return;
|
||||
logTo('ws-log', 'closing...');
|
||||
try {
|
||||
await activeWs.close(1000, 'done');
|
||||
} catch (e) {
|
||||
logTo('ws-log', `close failed: ${e}`, 'red');
|
||||
}
|
||||
});
|
||||
|
||||
$('btn-ws-burst').addEventListener('click', async () => {
|
||||
if (!activeWs || activeWs.readyState !== 1) return;
|
||||
const count = parseInt(($('ws-burst-count') as HTMLInputElement).value, 10);
|
||||
const minSize = parseInt(($('ws-burst-min') as HTMLInputElement).value, 10);
|
||||
const maxSize = parseInt(($('ws-burst-max') as HTMLInputElement).value, 10);
|
||||
|
||||
if (count < 1 || count > 500) { logTo('ws-log', 'burst count must be 1-500', 'red'); return; }
|
||||
if (minSize < 1 || maxSize < minSize) { logTo('ws-log', 'invalid size range', 'red'); return; }
|
||||
|
||||
($('btn-ws-burst') as HTMLButtonElement).disabled = true;
|
||||
($('btn-ws-send') as HTMLButtonElement).disabled = true;
|
||||
|
||||
// Generate random payloads + pre-compute their hashes for echo verification.
|
||||
const payloads: Uint8Array[] = [];
|
||||
wsBurstHashes = [];
|
||||
let totalBytes = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const size = minSize + Math.floor(Math.random() * (maxSize - minSize + 1));
|
||||
const buf = new Uint8Array(size);
|
||||
crypto.getRandomValues(buf);
|
||||
payloads.push(buf);
|
||||
totalBytes += size;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
wsBurstHashes.push(await sha256hex(buf));
|
||||
}
|
||||
|
||||
wsBurstActive = true;
|
||||
wsBurstRtts = [];
|
||||
wsBurstExpected = count;
|
||||
wsBurstVerified = 0;
|
||||
wsBurstMismatches = 0;
|
||||
wsSendQueue.length = 0;
|
||||
|
||||
logTo('ws-log', `burst: ${count} msgs, ${formatSize(totalBytes)} total`);
|
||||
|
||||
const burstDone = new Promise<void>((resolve) => {
|
||||
wsBurstResolve = resolve;
|
||||
});
|
||||
|
||||
const t0 = performance.now();
|
||||
for (const payload of payloads) {
|
||||
wsSendQueue.push(performance.now());
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await activeWs.send(payload);
|
||||
}
|
||||
|
||||
await burstDone;
|
||||
const totalMs = performance.now() - t0;
|
||||
|
||||
wsBurstActive = false;
|
||||
wsBurstResolve = null;
|
||||
|
||||
const rtts = wsBurstRtts.slice().sort((a, b) => a - b);
|
||||
const rttMin = rtts[0]?.toFixed(0) ?? '?';
|
||||
const rttMax = rtts[rtts.length - 1]?.toFixed(0) ?? '?';
|
||||
const rttAvg = rtts.length ? (rtts.reduce((a, b) => a + b, 0) / rtts.length).toFixed(0) : '?';
|
||||
const p50 = rtts[Math.floor(rtts.length * 0.5)]?.toFixed(0) ?? '?';
|
||||
const p95 = rtts[Math.floor(rtts.length * 0.95)]?.toFixed(0) ?? '?';
|
||||
const msgPerSec = (count / (totalMs / 1000)).toFixed(1);
|
||||
|
||||
logTo('ws-log', `burst done: ${count} msgs in ${(totalMs / 1000).toFixed(2)}s (${msgPerSec} msg/s, ${formatRate(totalBytes, totalMs)})`, 'green');
|
||||
logTo('ws-log', `verify: ${wsBurstVerified}/${count} OK${wsBurstMismatches > 0 ? `, ${wsBurstMismatches} MISMATCH` : ''}`, wsBurstMismatches === 0 ? 'green' : 'red');
|
||||
logTo('ws-log', `RTT: min=${rttMin} avg=${rttAvg} p50=${p50} p95=${p95} max=${rttMax} ms`);
|
||||
|
||||
($('btn-ws-burst') as HTMLButtonElement).disabled = false;
|
||||
($('btn-ws-send') as HTMLButtonElement).disabled = false;
|
||||
});
|
||||
|
||||
// Stress Test =======================================================
|
||||
|
||||
interface SizeProfile { label: string; bytes: number; }
|
||||
const SIZE_PROFILES: SizeProfile[] = [
|
||||
{ label: 'tiny', bytes: 128 },
|
||||
{ label: 'small', bytes: 1024 },
|
||||
{ label: 'medium', bytes: 10240 },
|
||||
{ label: 'large', bytes: 102400 },
|
||||
{ label: 'xlarge', bytes: 1048576 },
|
||||
];
|
||||
|
||||
interface DripProfile { label: string; duration: number; delay: number; bytes: number; }
|
||||
const buildDripProfiles = (timeoutSec: number): DripProfile[] => [
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
interface StressRequest { id: number; url: string; label: string; }
|
||||
|
||||
function generateRequests(count: number, mode: string, timeoutSec: number): StressRequest[] {
|
||||
const requests: StressRequest[] = [];
|
||||
if (mode === 'uniform') {
|
||||
const baseUrl = ($('stress-url') as HTMLInputElement).value.trim();
|
||||
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 if (mode === 'drip') {
|
||||
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;
|
||||
}
|
||||
|
||||
interface StressResult { id: number; label: string; ok: boolean; elapsed: string; status?: number; textLength?: number; error?: string; }
|
||||
|
||||
async function runOneStressRequest(req: StressRequest): Promise<StressResult> {
|
||||
const tag = `#${req.id} ${req.label}`;
|
||||
const start = performance.now();
|
||||
try {
|
||||
const resp = await mixFetch(req.url, fetchInit());
|
||||
const body = await resp.text();
|
||||
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
|
||||
logTo('stress-log', `[${tag}] ${resp.status} OK ${elapsed}s (${body.length}B)`, 'green');
|
||||
return { id: req.id, label: req.label, ok: true, elapsed, status: resp.status, textLength: body.length };
|
||||
} catch (e) {
|
||||
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
|
||||
logTo('stress-log', `[${tag}] FAIL ${elapsed}s: ${e}`, 'red');
|
||||
return { id: req.id, label: req.label, ok: false, elapsed, error: String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
$('stress-mode').addEventListener('change', (ev) => {
|
||||
const mode = (ev.target as HTMLSelectElement).value;
|
||||
$('stress-uniform-opts').style.display = mode === 'uniform' ? 'block' : 'none';
|
||||
$('stress-mixed-opts').style.display = mode === 'mixed' ? 'block' : 'none';
|
||||
$('stress-drip-opts').style.display = mode === 'drip' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
$('btn-stress').addEventListener('click', async () => {
|
||||
const count = parseInt(($('stress-count') as HTMLInputElement).value, 10);
|
||||
const mode = ($('stress-mode') as HTMLSelectElement).value;
|
||||
const timeoutSec = parseInt(($('stress-timeout') as HTMLInputElement)?.value || '60', 10);
|
||||
|
||||
const statusEl = $('stress-status');
|
||||
($('btn-stress') as HTMLButtonElement).disabled = true;
|
||||
statusEl.textContent = 'Running...';
|
||||
|
||||
const requests = generateRequests(count, mode, timeoutSec);
|
||||
|
||||
if (mode === 'mixed' || mode === 'drip') {
|
||||
const breakdown: Record<string, number> = {};
|
||||
for (const r of requests) breakdown[r.label] = (breakdown[r.label] || 0) + 1;
|
||||
logTo('stress-log', `${count} requests, ${mode} mode, profiles: ${JSON.stringify(breakdown)}`);
|
||||
} else {
|
||||
logTo('stress-log', `${count} requests, ${mode} mode`);
|
||||
}
|
||||
|
||||
const t0 = performance.now();
|
||||
const settled = await Promise.allSettled(requests.map((r) => runOneStressRequest(r)));
|
||||
const totalSec = ((performance.now() - t0) / 1000).toFixed(2);
|
||||
|
||||
const results: StressResult[] = settled.map((s) =>
|
||||
s.status === 'fulfilled' ? s.value : ({ id: -1, label: '?', ok: false, elapsed: '?', error: String(s.reason) } as StressResult),
|
||||
);
|
||||
const ok = results.filter((r) => r.ok).length;
|
||||
const fail = results.filter((r) => !r.ok).length;
|
||||
|
||||
logTo('stress-log', `done: ${ok}/${count} OK, ${fail} failed (${totalSec}s total)`, fail === 0 ? 'green' : 'red');
|
||||
if (fail > 0) {
|
||||
for (const r of results.filter((r) => !r.ok)) {
|
||||
logTo('stress-log', ` FAIL #${r.id} ${r.label} (${r.elapsed}s): ${r.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
statusEl.textContent = `Done: ${ok}/${count} OK, ${fail} failed (${totalSec}s)`;
|
||||
($('btn-stress') as HTMLButtonElement).disabled = false;
|
||||
});
|
||||
|
||||
// File Download =====================================================
|
||||
|
||||
// UCS Cambridge UTF-8 demo file — small, public, character-rich. Good for
|
||||
// confirming byte-for-byte preservation across the tunnel.
|
||||
const VERIFY_TEXT_URL = 'https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-demo.txt';
|
||||
|
||||
let cachedPdf: ArrayBuffer | null = null;
|
||||
|
||||
$('btn-verify-text').addEventListener('click', async () => {
|
||||
const statusEl = $('verify-text-status');
|
||||
const outputEl = $('verify-text-output') as HTMLPreElement;
|
||||
($('btn-verify-text') as HTMLButtonElement).disabled = true;
|
||||
statusEl.textContent = 'Fetching...';
|
||||
statusEl.style.color = 'orange';
|
||||
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const resp = await mixFetch(VERIFY_TEXT_URL, fetchInit());
|
||||
const text = await resp.text();
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
statusEl.textContent = `${resp.status} OK (${ms} ms, ${text.length} chars)`;
|
||||
statusEl.style.color = 'green';
|
||||
outputEl.textContent = text;
|
||||
outputEl.style.display = 'block';
|
||||
logTo('download-log', `UTF-8 demo: ${text.length} chars (${ms} ms)`, 'green');
|
||||
} catch (e) {
|
||||
statusEl.textContent = `Failed: ${e}`;
|
||||
statusEl.style.color = 'red';
|
||||
logTo('download-log', `UTF-8 demo failed: ${e}`, 'red');
|
||||
} finally {
|
||||
($('btn-verify-text') as HTMLButtonElement).disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
$('btn-verify-pdf').addEventListener('click', async () => {
|
||||
const url = ($('download-url') as HTMLInputElement).value.trim();
|
||||
if (!url) { logTo('download-log', 'PDF URL is required', 'red'); return; }
|
||||
|
||||
const statusEl = $('verify-pdf-status');
|
||||
const outputEl = $('verify-pdf-output');
|
||||
($('btn-verify-pdf') as HTMLButtonElement).disabled = true;
|
||||
statusEl.textContent = 'Fetching...';
|
||||
statusEl.style.color = 'orange';
|
||||
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const resp = await mixFetch(url, fetchInit());
|
||||
const buf = await resp.arrayBuffer();
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
const hash = await sha256hex(buf);
|
||||
|
||||
cachedPdf = buf;
|
||||
$('verify-pdf-size').textContent = `${formatSize(buf.byteLength)} (${buf.byteLength} bytes)`;
|
||||
$('verify-pdf-sha').textContent = hash;
|
||||
outputEl.style.display = 'block';
|
||||
($('btn-save-pdf') as HTMLButtonElement).disabled = false;
|
||||
|
||||
statusEl.textContent = `${resp.status} OK (${ms} ms)`;
|
||||
statusEl.style.color = 'green';
|
||||
logTo('download-log', `PDF: ${formatSize(buf.byteLength)} (${ms} ms), sha256=${hash.slice(0, 16)}...`, 'green');
|
||||
} catch (e) {
|
||||
statusEl.textContent = `Failed: ${e}`;
|
||||
statusEl.style.color = 'red';
|
||||
logTo('download-log', `PDF failed: ${e}`, 'red');
|
||||
} finally {
|
||||
($('btn-verify-pdf') as HTMLButtonElement).disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
$('btn-save-pdf').addEventListener('click', () => {
|
||||
if (!cachedPdf) return;
|
||||
const url = ($('download-url') as HTMLInputElement).value.trim();
|
||||
const name = url.split('/').pop() || 'download.pdf';
|
||||
saveFile(cachedPdf, name, 'application/pdf');
|
||||
});
|
||||
|
||||
// Page load ==========================================================
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
// Pre-populate clientId so each page load uses a fresh keystore slot.
|
||||
($('opt-client-id') as HTMLInputElement).value = `sdk-${Math.random().toString(36).slice(2, 8)}`;
|
||||
display('SDK dev ready. Click setupMixTunnel to connect.');
|
||||
});
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"outDir": "./dist"
|
||||
@@ -0,0 +1,38 @@
|
||||
const path = require('path');
|
||||
const { mergeWithRules } = require('webpack-merge');
|
||||
const { webpackCommon } = require('../../examples/.webpack/webpack.base');
|
||||
|
||||
// smolmix-wasm is base64-inlined into the @nymproject/mix-tunnel worker bundle
|
||||
// (see mix-tunnel/rollup/worker.mjs `maxFileSize`), which is itself base64-inlined
|
||||
// into mix-tunnel/dist/esm/index.js. No sibling .wasm asset to copy.
|
||||
|
||||
module.exports = mergeWithRules({
|
||||
module: {
|
||||
rules: {
|
||||
test: 'match',
|
||||
use: 'replace',
|
||||
},
|
||||
},
|
||||
})(
|
||||
webpackCommon(
|
||||
__dirname,
|
||||
[
|
||||
{
|
||||
inject: true,
|
||||
filename: 'index.html',
|
||||
template: path.resolve(__dirname, 'src/index.html'),
|
||||
chunks: ['index'],
|
||||
},
|
||||
],
|
||||
{ skipFavicon: true },
|
||||
),
|
||||
{
|
||||
entry: {
|
||||
index: path.resolve(__dirname, 'src/index.ts'),
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
publicPath: '/',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
# @nymproject/mix-dns
|
||||
|
||||
Hostname-to-IP resolution over the Nym mixnet. Uses the IP Packet Router's
|
||||
DNS path (UDP), so no TCP socket or TLS handshake is set up.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { setupMixTunnel, mixDNS } from '@nymproject/mix-dns';
|
||||
|
||||
await setupMixTunnel();
|
||||
|
||||
const ip = await mixDNS('example.com');
|
||||
console.log(ip); // "93.184.216.34"
|
||||
```
|
||||
|
||||
The tunnel is shared with `@nymproject/mix-fetch` and
|
||||
`@nymproject/mix-websocket` via `@nymproject/mix-tunnel`; calling
|
||||
`setupMixTunnel` once is enough for all three.
|
||||
|
||||
## Consumer build requirements
|
||||
|
||||
Ships as raw ESM with a bare `import` of `@nymproject/mix-tunnel`. Use a
|
||||
bundler that follows package imports (webpack, rollup, parcel, vite,
|
||||
esbuild).
|
||||
|
||||
Runs in any environment exposing `Worker`, `WebAssembly`, `Blob`, and
|
||||
`URL.createObjectURL`. That covers modern browsers, Electron renderers,
|
||||
and mobile WebViews (Capacitor, Cordova, Ionic, iOS WKWebView, Android
|
||||
WebView). A Node-direct entry point is not yet ported from v1.
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "@nymproject/mix-dns",
|
||||
"version": "0.1.0",
|
||||
"description": "Hostname-to-IP resolution over the Nym mixnet (DNS over UDP/IPR, no TCP/TLS).",
|
||||
"license": "Apache-2.0",
|
||||
"author": "Nym Technologies SA",
|
||||
"homepage": "https://nym.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nymtech/nym",
|
||||
"directory": "sdk/typescript/packages/mix-dns"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/nymtech/nym/issues"
|
||||
},
|
||||
"keywords": ["nym", "mixnet", "privacy", "dns"],
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"browser": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rimraf dist",
|
||||
"docs:generate": "typedoc",
|
||||
"docs:generate:prod": "typedoc --basePath ./docs/tsdoc/nymproject/mix-dns/",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"start": "tsc -w",
|
||||
"tsc": "tsc --noEmit true"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/mix-tunnel": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nymproject/eslint-config-react-typescript": "workspace:*",
|
||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||
"@typescript-eslint/parser": "catalog:",
|
||||
"eslint": "^8.10.0",
|
||||
"rimraf": "catalog:",
|
||||
"tslib": "catalog:",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"private": false,
|
||||
"type": "module",
|
||||
"types": "./dist/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// @nymproject/mix-dns
|
||||
//
|
||||
// Hostname-to-IP resolution over the Nym mixnet. Travels the IPR's DNS
|
||||
// path (UDP) without setting up a TCP or TLS connection.
|
||||
|
||||
import {
|
||||
getMixTunnel,
|
||||
setupMixTunnel,
|
||||
disconnectMixTunnel,
|
||||
getTunnelState,
|
||||
SetupMixTunnelOpts,
|
||||
} from '@nymproject/mix-tunnel';
|
||||
|
||||
export { setupMixTunnel, disconnectMixTunnel, getTunnelState };
|
||||
export type { SetupMixTunnelOpts };
|
||||
|
||||
/**
|
||||
* 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()`.
|
||||
*/
|
||||
export const mixDNS = async (hostname: string): Promise<string> => {
|
||||
const tunnel = await getMixTunnel();
|
||||
return tunnel.mixDNS(hostname);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"lib": ["es2021", "dom", "esnext"],
|
||||
"outDir": "./dist/",
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"allowJs": false,
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "**/node_modules", "dist", "**/dist"]
|
||||
}
|
||||
+3
-2
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"sort": ["kind"],
|
||||
"entryPoints": ["./src/index.ts"],
|
||||
"out": "./docs",
|
||||
"exclude": ["./src/worker/**"],
|
||||
"out": "../../../../documentation/docs/pages/developers/mix-dns/api",
|
||||
"plugin": ["typedoc-plugin-markdown"],
|
||||
"entryFileName": "globals",
|
||||
"kindSortOrder": [
|
||||
"Function",
|
||||
"Interface",
|
||||
@@ -1,2 +0,0 @@
|
||||
src/worker/*.js
|
||||
docs
|
||||
@@ -1,14 +0,0 @@
|
||||
# Nym MixFetch
|
||||
|
||||
This package is a drop-in replacement for `fetch` in NodeJS to send HTTP requests over the Nym Mixnet.
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const { mixFetch } = require('@nymproject/mix-fetch-node-commonjs');
|
||||
|
||||
...
|
||||
|
||||
const response = await mixFetch('https://nymtech.net');
|
||||
const html = await response.text();
|
||||
```
|
||||
@@ -1,54 +0,0 @@
|
||||
const { createMixFetch, disconnectMixFetch } = require('../dist/cjs/index.js');
|
||||
|
||||
/**
|
||||
* The main entry point
|
||||
*/
|
||||
(async () => {
|
||||
console.log('Tester is starting up...');
|
||||
|
||||
const addr =
|
||||
'D274yd1h3L3pNJzdxE5VgJ7izAsAVMsDrQtFSkKUegfk.8J67cGbcwvrJKF3Kb16HVWWc9AnrFnEibNCm9zCkuVFu@Emswx6KXyjRfq1c2k4d4uD2e6nBSbH1biorCZUei8UNS';
|
||||
|
||||
console.log('About to set up mixFetch...');
|
||||
const { mixFetch } = await createMixFetch({
|
||||
preferredNetworkRequester: addr,
|
||||
clientId: 'node-client1',
|
||||
clientOverride: {
|
||||
coverTraffic: { disableLoopCoverTrafficStream: true },
|
||||
traffic: { disableMainPoissonPacketDistribution: true },
|
||||
},
|
||||
mixFetchOverride: { requestTimeoutMs: 60000 },
|
||||
responseBodyConfigMap: {},
|
||||
extra: {},
|
||||
});
|
||||
|
||||
globalThis.mixFetch = mixFetch;
|
||||
|
||||
if (!globalThis.mixFetch) {
|
||||
console.error('Oh no! Could not create mixFetch');
|
||||
} else {
|
||||
console.log('Ready!');
|
||||
}
|
||||
|
||||
let url = 'https://nymtech.net/.wellknown/network-requester/standard-allowed-list.txt';
|
||||
console.log(`Using mixFetch to get ${url}...`);
|
||||
const args = { mode: 'unsafe-ignore-cors' };
|
||||
|
||||
let resp = await mixFetch(url, args);
|
||||
console.log({ resp });
|
||||
const text = await resp.text();
|
||||
|
||||
console.log('disconnecting');
|
||||
await disconnectMixFetch();
|
||||
console.log('disconnected! all further usages should fail');
|
||||
|
||||
// get an image
|
||||
url = 'https://nymtech.net/favicon.svg';
|
||||
resp = await mixFetch(url, args);
|
||||
console.log({ resp });
|
||||
const buffer = await resp.arrayBuffer();
|
||||
const type = resp.headers.get('Content-Type') || 'image/svg';
|
||||
const blobUrl = URL.createObjectURL(new Blob([buffer], { type }));
|
||||
console.log(JSON.stringify({ bufferBytes: buffer.byteLength, blobUrl }, null, 2));
|
||||
console.log(blobUrl);
|
||||
})();
|
||||
@@ -1,16 +0,0 @@
|
||||
import preset from 'ts-jest/presets/index.js';
|
||||
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
export default {
|
||||
...preset.defaults,
|
||||
verbose: true,
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': [
|
||||
'ts-jest',
|
||||
{
|
||||
tsconfig: 'tsconfig.jest.json',
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"name": "@nymproject/mix-fetch-node",
|
||||
"version": "1.4.3",
|
||||
"description": "This package is a drop-in replacement for `fetch` in NodeJS to send HTTP requests over the Nym Mixnet.",
|
||||
"license": "Apache-2.0",
|
||||
"author": "Nym Technologies SA",
|
||||
"files": [
|
||||
"dist/cjs/worker.js",
|
||||
"dist/**/*"
|
||||
],
|
||||
"main": "dist/cjs/index.js",
|
||||
"scripts": {
|
||||
"build": "scripts/build-prod.sh",
|
||||
"build:dev": "scripts/build.sh",
|
||||
"build:worker": "rollup -c rollup-worker.config.mjs",
|
||||
"clean": "rimraf dist",
|
||||
"docs:dev": "run-p docs:watch docs:serve ",
|
||||
"docs:generate": "typedoc",
|
||||
"docs:generate:prod": "typedoc --basePath ./docs/tsdoc/nymproject/sdk/",
|
||||
"docs:prod:build": "scripts/build-prod-docs-collect.sh",
|
||||
"docs:serve": "reload -b -d ./docs -p 3000",
|
||||
"docs:watch": "nodemon --ext ts --watch './src/**/*' --watch './typedoc.json' --exec \"pnpm docs:generate\"",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"start": "tsc -w",
|
||||
"start:dev": "nodemon --watch src -e ts,json --exec 'pnpm build:dev:esm'",
|
||||
"test": "node --experimental-fetch --experimental-vm-modules node_modules/jest/bin/jest.js -c=jest.config.mjs --no-cache",
|
||||
"tsc": "tsc --noEmit true"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/mix-fetch-wasm-node": ">=1.4.2-rc.0 || ^1",
|
||||
"comlink": "^4.3.1",
|
||||
"fake-indexeddb": "^5.0.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"ws": "^8.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^24.0.1",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-replace": "^5.0.2",
|
||||
"@rollup/plugin-typescript": "^10.0.1",
|
||||
"@rollup/plugin-wasm": "^6.1.1",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/ws": "catalog:",
|
||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||
"@typescript-eslint/parser": "catalog:",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-airbnb": "catalog:",
|
||||
"eslint-config-airbnb-typescript": "catalog:",
|
||||
"eslint-config-prettier": "catalog:",
|
||||
"eslint-import-resolver-root-import": "catalog:",
|
||||
"eslint-plugin-import": "catalog:",
|
||||
"eslint-plugin-jest": "catalog:",
|
||||
"eslint-plugin-jsx-a11y": "catalog:",
|
||||
"eslint-plugin-prettier": "catalog:",
|
||||
"jest": "^29.5.0",
|
||||
"nodemon": "^2.0.21",
|
||||
"reload": "^3.2.1",
|
||||
"rimraf": "catalog:",
|
||||
"rollup": "^3.9.1",
|
||||
"rollup-plugin-base64": "^1.0.1",
|
||||
"rollup-plugin-modify": "^3.0.0",
|
||||
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "catalog:",
|
||||
"tslib": "catalog:",
|
||||
"typedoc": "^0.24.8",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"private": false,
|
||||
"types": "./dist/cjs/index.d.ts"
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import webWorkerLoader from 'rollup-plugin-web-worker-loader';
|
||||
import { wasm } from '@rollup/plugin-wasm';
|
||||
|
||||
export default {
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
dir: 'dist/cjs',
|
||||
format: 'cjs',
|
||||
},
|
||||
plugins: [
|
||||
webWorkerLoader({ targetPlatform: 'node', inline: false }),
|
||||
replace({
|
||||
values: {
|
||||
"createURLWorkerFactory('web-worker-0.js')":
|
||||
"createURLWorkerFactory(require('path').resolve(__dirname, 'web-worker-0.js'))",
|
||||
},
|
||||
delimiters: ['', ''],
|
||||
preventAssignment: true,
|
||||
}),
|
||||
resolve({ browser: false, extensions: ['.js', '.ts'] }),
|
||||
wasm({ targetEnv: 'node', maxFileSize: 0 }),
|
||||
typescript({
|
||||
compilerOptions: { outDir: 'dist/cjs', target: 'es5' },
|
||||
exclude: ['src/worker.ts'],
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import modify from 'rollup-plugin-modify';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import { wasm } from '@rollup/plugin-wasm';
|
||||
|
||||
export default {
|
||||
input: 'src/worker/index.ts',
|
||||
output: {
|
||||
dir: 'dist/cjs',
|
||||
format: 'cjs',
|
||||
},
|
||||
external: ['util', 'fake-indexeddb'],
|
||||
plugins: [
|
||||
resolve({
|
||||
browser: false,
|
||||
preferBuiltins: true,
|
||||
extensions: ['.js', '.ts'],
|
||||
}),
|
||||
commonjs(),
|
||||
modify({
|
||||
find: 'const ret = new WebSocket(getStringFromWasm0(arg0, arg1));',
|
||||
replace: 'const ws = require("ws"); const ret = new ws.WebSocket(getStringFromWasm0(arg0, arg1));',
|
||||
}),
|
||||
// TODO: `getObject(...).require` seems to generate a warning on Webpack but with Rollup we get a panic since it can't require.
|
||||
// By hard coding the require here, we can workaround that.
|
||||
// Reference: https://github.com/rust-random/getrandom/issues/224
|
||||
modify({ find: 'getObject(arg0).require(getStringFromWasm0(arg1, arg2));', replace: 'require("crypto");' }),
|
||||
modify({
|
||||
find: 'getObject(arg0).getRandomValues(getObject(arg1));',
|
||||
replace: 'require("crypto").getRandomValues(getObject(arg1));',
|
||||
}),
|
||||
wasm({ targetEnv: 'node', maxFileSize: 0, fileName: '[name].wasm' }),
|
||||
typescript({
|
||||
compilerOptions: {
|
||||
outDir: 'dist/cjs',
|
||||
declaration: false,
|
||||
target: 'es5',
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
rm -rf ../../../../dist/ts/docs/tsdoc/nymproject/mix-fetch-node || true
|
||||
|
||||
# run the build
|
||||
yarn docs:generate:prod
|
||||
|
||||
# move the output outside of the yarn/npm workspaces
|
||||
mkdir -p ../../../../dist/ts/docs/tsdoc/nymproject
|
||||
mv docs ../../../../dist/ts/docs/tsdoc/nymproject/mix-fetch-node
|
||||
|
||||
echo "Output can be found in:"
|
||||
realpath ../../../../dist/ts/docs/tsdoc/nymproject/mix-fetch-node
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
rm -rf dist || true
|
||||
rm -rf ../../../../dist/ts/sdk/mix-fetch-node || true
|
||||
|
||||
# run the build
|
||||
scripts/build.sh
|
||||
node scripts/buildPackageJson.mjs
|
||||
|
||||
# move the output outside of the yarn/npm workspaces
|
||||
mkdir -p ../../../../dist/ts/sdk
|
||||
mv dist ../../../../dist/ts/sdk
|
||||
mv ../../../../dist/ts/sdk/dist ../../../../dist/ts/sdk/mix-fetch-node
|
||||
|
||||
echo "Output can be found in:"
|
||||
realpath ../../../../dist/ts/sdk/mix-fetch-node
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
rm -rf dist || true
|
||||
|
||||
#-------------------------------------------------------
|
||||
# WEB WORKER (mix-fetch WASM)
|
||||
#-------------------------------------------------------
|
||||
# The web worker needs to be bundled because the WASM bundle needs to be loaded synchronously and all dependencies
|
||||
# must be included in the worker script (because it is not loaded as an ES Module)
|
||||
|
||||
# build the worker
|
||||
rollup -c rollup-worker.config.mjs
|
||||
|
||||
# move it next to the Typescript `src/index.ts` so it can be inlined by rollup
|
||||
rm -f src/worker/*.js
|
||||
rm -f src/worker/*.wasm
|
||||
mv dist/cjs/index.js src/worker/worker.js
|
||||
|
||||
# move WASM files out of build area
|
||||
mkdir -p dist/worker
|
||||
mv dist/cjs/*.wasm dist/worker
|
||||
|
||||
#-------------------------------------------------------
|
||||
# COMMON JS
|
||||
#-------------------------------------------------------
|
||||
# Some old build systems cannot fully handle ESM or ES2021, so build
|
||||
# a CommonJS bundle targeting ES5
|
||||
|
||||
# build the SDK as a CommonJS bundle
|
||||
rollup -c rollup-cjs.config.mjs
|
||||
|
||||
# move WASM files into place
|
||||
cp dist/worker/*.wasm dist/cjs
|
||||
|
||||
#-------------------------------------------------------
|
||||
# CLEAN UP
|
||||
#-------------------------------------------------------
|
||||
|
||||
rm -rf dist/cjs/worker
|
||||
|
||||
# copy README
|
||||
cp README.md dist/cjs/README.md
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
|
||||
// parse the package.json from the SDK, so we can keep fields like the name and version
|
||||
const json = JSON.parse(fs.readFileSync('package.json').toString());
|
||||
|
||||
// defaults (NB: these are in the output file locations)
|
||||
const browser = 'index.js';
|
||||
const main = 'index.js';
|
||||
const types = 'index.d.ts';
|
||||
|
||||
const getPackageJson = (type, suffix) => ({
|
||||
name: `${json.name}${suffix ? `-${suffix}` : ''}`,
|
||||
version: json.version,
|
||||
license: json.license,
|
||||
author: json.author,
|
||||
type,
|
||||
browser,
|
||||
main,
|
||||
types,
|
||||
});
|
||||
|
||||
fs.writeFileSync('dist/cjs/package.json', JSON.stringify(getPackageJson('commonjs', 'commonjs'), null, 2));
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
rm -rf dist || true
|
||||
rm -rf ../../../../dist || true
|
||||
|
||||
yarn
|
||||
yarn build
|
||||
cd ../../../../dist/sdk
|
||||
|
||||
cd cjs
|
||||
echo "Publishing CommonJS package to NPM.."
|
||||
npm publish --access=public
|
||||
cd ..
|
||||
@@ -1,77 +0,0 @@
|
||||
/* eslint-disable-next-line no-console */
|
||||
|
||||
import * as Comlink from 'comlink';
|
||||
import InlineWasmWebWorker from 'web-worker:./worker/worker';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
|
||||
import nodeEndpoint from './node-adapter';
|
||||
import type { IMixFetchWebWorker } from './types';
|
||||
import { EventKinds, IMixFetch } from './types';
|
||||
|
||||
const createWorker = async () =>
|
||||
new Promise<Worker>((resolve, reject) => {
|
||||
// rollup will inline the built worker script, so that when the SDK is used in
|
||||
// other projects, they will not need to mess around trying to bundle it
|
||||
// however, it will make this SDK bundle bigger because of Base64 inline data
|
||||
const worker = new InlineWasmWebWorker();
|
||||
|
||||
worker.addListener('error', reject);
|
||||
worker.addListener('message', (msg: any) => {
|
||||
worker.removeListener('error', reject);
|
||||
if (msg.kind === EventKinds.Loaded) {
|
||||
resolve(worker);
|
||||
} else {
|
||||
reject(msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const convertHeaders = (headers: any): Headers => {
|
||||
const out = new Headers();
|
||||
Object.keys(headers).forEach((key) => {
|
||||
out.append(key, headers[key]);
|
||||
});
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* Use this method to initialise `mixFetch`.
|
||||
*
|
||||
* @returns An instance of `mixFetch` that you can use to make your requests using the same interface as `fetch`.
|
||||
*/
|
||||
export const createMixFetch = async (): Promise<IMixFetch> => {
|
||||
// start the worker
|
||||
const worker = await createWorker();
|
||||
|
||||
// bind with Comlink
|
||||
const wrappedWorker = Comlink.wrap<IMixFetchWebWorker>(nodeEndpoint(worker));
|
||||
|
||||
// handle the responses
|
||||
const mixFetchWebWorker: IMixFetch = {
|
||||
setupMixFetch: wrappedWorker.setupMixFetch,
|
||||
mixFetch: async (url: string, args: any) => {
|
||||
const workerResponse = await wrappedWorker.mixFetch(url, args);
|
||||
if (!workerResponse) {
|
||||
throw new Error('No response received');
|
||||
}
|
||||
const { headers: headersRaw, status, statusText } = workerResponse;
|
||||
|
||||
// reconstruct the Headers object instance from a plain object
|
||||
const headers = convertHeaders(headersRaw);
|
||||
|
||||
// handle blobs
|
||||
if (workerResponse.body.blobUrl) {
|
||||
const blob = await (await fetch(workerResponse.body.blobUrl)).blob();
|
||||
const body = await blob.arrayBuffer();
|
||||
return new Response(body, { headers, status, statusText });
|
||||
}
|
||||
|
||||
// handle everything else
|
||||
const body = Object.values(workerResponse.body)[0]; // we are expecting only one value to be set in `.body`
|
||||
return new Response(body, { headers, status, statusText });
|
||||
},
|
||||
disconnectMixFetch: wrappedWorker.disconnectMixFetch,
|
||||
};
|
||||
|
||||
return mixFetchWebWorker;
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import type { SetupMixFetchOps, IMixFetchFn } from './types';
|
||||
import { createMixFetch as createMixFetchInternal } from './create-mix-fetch';
|
||||
|
||||
// this is the default timeout for getting a response
|
||||
const REQUEST_TIMEOUT_MILLISECONDS = 60_000;
|
||||
|
||||
export * from './types';
|
||||
|
||||
/**
|
||||
* Create a global mixFetch instance and optionally configure settings.
|
||||
*
|
||||
* @param opts Optional settings
|
||||
*/
|
||||
export const createMixFetch = async (opts?: SetupMixFetchOps) => {
|
||||
if (!(globalThis as any).__mixFetchGlobal) {
|
||||
// load the worker and set up mixFetch with defaults
|
||||
(globalThis as any).__mixFetchGlobal = await createMixFetchInternal();
|
||||
await (globalThis as any).__mixFetchGlobal.setupMixFetch(opts);
|
||||
}
|
||||
|
||||
return (globalThis as any).__mixFetchGlobal;
|
||||
};
|
||||
|
||||
/**
|
||||
* mixFetch is a drop-in replacement for the standard `fetch` interface.
|
||||
*
|
||||
* @param url The URL to fetch from.
|
||||
* @param args Fetch options.
|
||||
* @param opts Optionally configure mixFetch when it gets created. This only happens once, the first time it gets used.
|
||||
*/
|
||||
export const mixFetch: IMixFetchFn = async (url, args, opts?: SetupMixFetchOps) => {
|
||||
// ensure mixFetch instance exists
|
||||
const instance = await createMixFetch({
|
||||
mixFetchOverride: {
|
||||
requestTimeoutMs: REQUEST_TIMEOUT_MILLISECONDS,
|
||||
},
|
||||
...opts,
|
||||
});
|
||||
|
||||
// execute user request
|
||||
return instance.mixFetch(url, args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops the usage of mixFetch and disconnect the client from the mixnet.
|
||||
*/
|
||||
export const disconnectMixFetch = async (): Promise<void> => {
|
||||
// JS: I'm ignoring this lint (no-else-return) because I want to explicitly state
|
||||
// that `__mixFetchGlobal` is definitely not null in the else branch.
|
||||
if (!(globalThis as any).__mixFetchGlobal) {
|
||||
throw new Error("mixFetch hasn't been setup");
|
||||
} else {
|
||||
return (globalThis as any).__mixFetchGlobal.disconnectMixFetch();
|
||||
}
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Borrowed from https://github.com/GoogleChromeLabs/comlink/blob/main/src/node-adapter.ts
|
||||
|
||||
import { Endpoint } from 'comlink';
|
||||
|
||||
export interface NodeEndpoint {
|
||||
postMessage(message: any, transfer?: any[]): void;
|
||||
on(type: string, listener: EventListenerOrEventListenerObject, options?: {}): void;
|
||||
off(type: string, listener: EventListenerOrEventListenerObject, options?: {}): void;
|
||||
start?: () => void;
|
||||
}
|
||||
|
||||
export default function nodeEndpoint(nep: NodeEndpoint): Endpoint {
|
||||
const listeners = new WeakMap();
|
||||
return {
|
||||
postMessage: nep.postMessage.bind(nep),
|
||||
addEventListener: (_, eh) => {
|
||||
const l = (data: any) => {
|
||||
if ('handleEvent' in eh) {
|
||||
eh.handleEvent({ data } as MessageEvent);
|
||||
} else {
|
||||
eh({ data } as MessageEvent);
|
||||
}
|
||||
};
|
||||
nep.on('message', l);
|
||||
listeners.set(eh, l);
|
||||
},
|
||||
removeEventListener: (_, eh) => {
|
||||
const l = listeners.get(eh);
|
||||
if (!l) {
|
||||
return;
|
||||
}
|
||||
nep.off('message', l);
|
||||
listeners.delete(eh);
|
||||
},
|
||||
start: nep.start && nep.start.bind(nep),
|
||||
};
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import type { MixFetchOpts } from '@nymproject/mix-fetch-wasm-node';
|
||||
|
||||
type IMixFetchWorkerFn = (url: string, args: any) => Promise<MixFetchWebWorkerResponse>;
|
||||
|
||||
// export type IMixFetchFn = typeof fetch;
|
||||
export type IMixFetchFn = (url: string, args: any, opts?: SetupMixFetchOps) => Promise<Response>;
|
||||
|
||||
export type SetupMixFetchOps = MixFetchOpts & {
|
||||
responseBodyConfigMap?: ResponseBodyConfigMap;
|
||||
};
|
||||
|
||||
export interface IMixFetchWebWorker {
|
||||
mixFetch: IMixFetchWorkerFn;
|
||||
setupMixFetch: (opts?: SetupMixFetchOps) => Promise<void>;
|
||||
disconnectMixFetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface IMixFetch {
|
||||
mixFetch: IMixFetchFn;
|
||||
setupMixFetch: (opts?: SetupMixFetchOps) => Promise<void>;
|
||||
disconnectMixFetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
export enum EventKinds {
|
||||
Loaded = 'Loaded',
|
||||
}
|
||||
|
||||
export interface LoadedEvent {
|
||||
kind: EventKinds.Loaded;
|
||||
args: {
|
||||
loaded: true;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
uint8array?: Uint8Array;
|
||||
json?: any;
|
||||
text?: string;
|
||||
formData?: any;
|
||||
blobUrl?: string;
|
||||
}
|
||||
|
||||
export type ResponseBodyMethod = 'uint8array' | 'json' | 'text' | 'formData' | 'blob';
|
||||
|
||||
export interface ResponseBodyConfigMap {
|
||||
/**
|
||||
* Set the response `Content-Type`s to decode as uint8array.
|
||||
*/
|
||||
uint8array?: Array<RegExp | string>;
|
||||
|
||||
/**
|
||||
* Set the response `Content-Type`s to decode with the `json()` response body method.
|
||||
*/
|
||||
json?: Array<RegExp | string>;
|
||||
|
||||
/**
|
||||
* Set the response `Content-Type`s to decode with the `text()` response body method.
|
||||
*/
|
||||
text?: Array<RegExp | string>;
|
||||
|
||||
/**
|
||||
* Set the response `Content-Type`s to decode with the `formData()` response body method.
|
||||
*/
|
||||
formData?: Array<RegExp | string>;
|
||||
|
||||
/**
|
||||
* Set the response `Content-Type`s to decode with the `blob()` response body method.
|
||||
*/
|
||||
blob?: Array<RegExp | string>;
|
||||
/**
|
||||
* Set this to the default fallback method. Set to `undefined` if you want to ignore unknown types.
|
||||
*/
|
||||
|
||||
fallback?: ResponseBodyMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default values for the handling of response bodies.
|
||||
*/
|
||||
export const ResponseBodyConfigMapDefaults: ResponseBodyConfigMap = {
|
||||
uint8array: ['application/octet-stream'],
|
||||
json: ['application/json', 'text/json', /application\/json.*/, /text\/json\+.*/],
|
||||
text: ['text/plain', /text\/plain.*/, 'text/html', /text\/html.*/],
|
||||
formData: ['application/x-www-form-urlencoded', 'multipart/form-data'],
|
||||
blob: [/image\/.*/, /video\/.*/],
|
||||
fallback: 'blob',
|
||||
};
|
||||
|
||||
export interface MixFetchWebWorkerResponse {
|
||||
body: ResponseBody;
|
||||
url: string;
|
||||
headers: any;
|
||||
status: number;
|
||||
statusText: string;
|
||||
type: string;
|
||||
ok: boolean;
|
||||
redirected: boolean;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { handleResponseMimeTypes } from './handle-response-mime-types';
|
||||
|
||||
describe('handleResponseMimeTypes', () => {
|
||||
test('gracefully handles empty values', async () => {
|
||||
const resp = await handleResponseMimeTypes(new Response());
|
||||
expect(Object.values(resp)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('handles text', async () => {
|
||||
const TEXT = 'This is text';
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(TEXT, { headers: new Headers([['Content-Type', 'text/plain']]) }),
|
||||
);
|
||||
expect(resp.text).toBe(TEXT);
|
||||
});
|
||||
test('handles text (charset=utf-8)', async () => {
|
||||
const TEXT = 'This is text';
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(TEXT, { headers: new Headers([['Content-Type', 'text/plain; charset=utf-8']]) }),
|
||||
);
|
||||
expect(resp.text).toBe(TEXT);
|
||||
});
|
||||
test('handles html', async () => {
|
||||
const TEXT = 'This is html';
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(TEXT, { headers: new Headers([['Content-Type', 'text/html']]) }),
|
||||
);
|
||||
expect(resp.text).toBe(TEXT);
|
||||
});
|
||||
test('handles html (charset=utf-8)', async () => {
|
||||
const TEXT = 'This is html';
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(TEXT, { headers: new Headers([['Content-Type', 'text/html; charset=utf-8']]) }),
|
||||
);
|
||||
expect(resp.text).toBe(TEXT);
|
||||
});
|
||||
test('handles images', async () => {
|
||||
const DATA = Buffer.from(new Uint8Array([0, 1, 2, 3]));
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(DATA, { headers: new Headers([['Content-Type', 'image/jpeg']]) }),
|
||||
);
|
||||
expect(resp.blobUrl).toBeDefined();
|
||||
});
|
||||
test('handles videos', async () => {
|
||||
const DATA = Buffer.from(new Uint8Array([0, 1, 2, 3]));
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(DATA, { headers: new Headers([['Content-Type', 'video/mpeg4']]) }),
|
||||
);
|
||||
expect(resp.blobUrl).toBeDefined();
|
||||
});
|
||||
test('handles form data when URL encoded', async () => {
|
||||
const formData = 'foo=bar&baz=42';
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(formData, { headers: new Headers([['Content-Type', 'application/x-www-form-urlencoded']]) }),
|
||||
);
|
||||
expect(resp.formData.foo).toBe('bar');
|
||||
expect(resp.formData.baz).toBe('42');
|
||||
});
|
||||
test('handles JSON data', async () => {
|
||||
const json = '{ "foo": "bar", "baz": 42 }';
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(json, { headers: new Headers([['Content-Type', 'application/json']]) }),
|
||||
);
|
||||
expect(resp.text).toBe(json);
|
||||
});
|
||||
test('handles JSON data (charset=utf-8)', async () => {
|
||||
const json = '{ "foo": "bar", "baz": 42 }';
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(json, { headers: new Headers([['Content-Type', 'application/json; charset=utf-8']]) }),
|
||||
);
|
||||
expect(resp.text).toBe(json);
|
||||
});
|
||||
});
|
||||
@@ -1,111 +0,0 @@
|
||||
import type { ResponseBody, ResponseBodyConfigMap, ResponseBodyMethod } from '../types';
|
||||
import { ResponseBodyConfigMapDefaults } from '../types';
|
||||
|
||||
const getContentType = (response?: Response) => {
|
||||
if (!response) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// this is what should be returned in the headers
|
||||
if (response.headers.has('Content-Type')) {
|
||||
return response.headers.get('Content-Type') as string;
|
||||
}
|
||||
|
||||
// handle weird servers that use lowercase headers
|
||||
if (response.headers.has('content-type')) {
|
||||
return response.headers.get('content-type') as string;
|
||||
}
|
||||
|
||||
// the Content-Type/content-type header is not part of the response
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const doHandleResponseMethod = async (response: Response, method?: ResponseBodyMethod): Promise<ResponseBody> => {
|
||||
switch (method) {
|
||||
case 'uint8array':
|
||||
return {
|
||||
uint8array: new Uint8Array(await response.arrayBuffer()),
|
||||
};
|
||||
case 'json':
|
||||
case 'text':
|
||||
return { text: await response.text() };
|
||||
case 'blob': {
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
return { blobUrl };
|
||||
}
|
||||
case 'formData': {
|
||||
const formData: any = {};
|
||||
const data = await response.formData();
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const pair of data.entries()) {
|
||||
const [key, value] = pair;
|
||||
formData[key] = value;
|
||||
}
|
||||
return { formData };
|
||||
}
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const testIfIncluded = (value?: string, tests?: Array<string | RegExp>): boolean => {
|
||||
if (!tests) {
|
||||
return false;
|
||||
}
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < tests.length; i += 1) {
|
||||
const test = tests[i];
|
||||
if (typeof test === 'string' && value === test) {
|
||||
return true;
|
||||
}
|
||||
if ((test as RegExp).test && (test as RegExp).test(value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// default return is false, because nothing above matched
|
||||
return false;
|
||||
};
|
||||
|
||||
export const handleResponseMimeTypes = async (
|
||||
response: Response,
|
||||
config?: ResponseBodyConfigMap,
|
||||
): Promise<ResponseBody> => {
|
||||
// combine the user supplied config with the default
|
||||
const finalConfig: ResponseBodyConfigMap = { ...ResponseBodyConfigMapDefaults, ...config };
|
||||
|
||||
const contentType = getContentType(response);
|
||||
|
||||
// check if the headers say what the content type are, otherwise return the bytes of the response as a blob
|
||||
if (!contentType) {
|
||||
// no content type, or body, so the response is only the status, e.g. GET
|
||||
if (!response.body) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// handle fallback method
|
||||
return doHandleResponseMethod(response, config?.fallback || 'blob');
|
||||
}
|
||||
|
||||
if (testIfIncluded(contentType, finalConfig.uint8array)) {
|
||||
return doHandleResponseMethod(response, 'uint8array');
|
||||
}
|
||||
if (testIfIncluded(contentType, finalConfig.json)) {
|
||||
return doHandleResponseMethod(response, 'json');
|
||||
}
|
||||
if (testIfIncluded(contentType, finalConfig.text)) {
|
||||
return doHandleResponseMethod(response, 'text');
|
||||
}
|
||||
if (testIfIncluded(contentType, finalConfig.formData)) {
|
||||
return doHandleResponseMethod(response, 'formData');
|
||||
}
|
||||
if (testIfIncluded(contentType, finalConfig.blob)) {
|
||||
return doHandleResponseMethod(response, 'blob');
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import './polyfill';
|
||||
|
||||
import { loadWasm } from './wasm-loading';
|
||||
import { run } from './main';
|
||||
|
||||
(async function main() {
|
||||
await loadWasm();
|
||||
await run();
|
||||
})().catch((e: any) => console.error('Unhandled exception in mixFetch worker', e));
|
||||
@@ -1,74 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import * as Comlink from 'comlink';
|
||||
import { parentPort } from 'node:worker_threads';
|
||||
import { setupMixFetch, disconnectMixFetch } from '@nymproject/mix-fetch-wasm-node';
|
||||
import type { IMixFetchWebWorker, LoadedEvent } from '../types';
|
||||
|
||||
import nodeEndpoint from '../node-adapter';
|
||||
import { EventKinds, ResponseBodyConfigMap, ResponseBodyConfigMapDefaults } from '../types';
|
||||
import { handleResponseMimeTypes } from './handle-response-mime-types';
|
||||
|
||||
/**
|
||||
* Helper method to send typed messages.
|
||||
* @param event The strongly typed message to send back to the calling thread.
|
||||
*/
|
||||
const postMessageWithType = <E>(event: E) => parentPort?.postMessage(event);
|
||||
|
||||
export async function run() {
|
||||
const { mixFetch } = globalThis as any;
|
||||
let responseBodyConfigMap: ResponseBodyConfigMap = ResponseBodyConfigMapDefaults;
|
||||
|
||||
const mixFetchWebWorker: IMixFetchWebWorker = {
|
||||
mixFetch: async (url, args) => {
|
||||
console.log('[Worker] --- mixFetch ---', { url, args });
|
||||
|
||||
const response: Response = await mixFetch(url, args);
|
||||
console.log('[Worker]', { response, json: JSON.stringify(response, null, 2) });
|
||||
|
||||
const bodyResponse = await handleResponseMimeTypes(response, responseBodyConfigMap);
|
||||
console.log('[Worker]', { bodyResponse });
|
||||
|
||||
const headers: any = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
const output = {
|
||||
body: bodyResponse,
|
||||
url: response.url,
|
||||
headers,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
type: response.type,
|
||||
ok: response.ok,
|
||||
redirected: response.redirected,
|
||||
};
|
||||
|
||||
console.log('[Worker]', { output });
|
||||
|
||||
return output;
|
||||
},
|
||||
setupMixFetch: async (opts) => {
|
||||
console.log('[Worker] --- setupMixFetch ---', { opts });
|
||||
if (opts?.responseBodyConfigMap) {
|
||||
responseBodyConfigMap = opts.responseBodyConfigMap;
|
||||
}
|
||||
await setupMixFetch(opts || {});
|
||||
},
|
||||
disconnectMixFetch: async () => {
|
||||
console.log('[Worker] --- disconnectMixFetch ---');
|
||||
|
||||
await disconnectMixFetch();
|
||||
},
|
||||
};
|
||||
|
||||
// start comlink listening for messages and handle them above
|
||||
if (parentPort) {
|
||||
Comlink.expose(mixFetchWebWorker, nodeEndpoint(parentPort));
|
||||
}
|
||||
|
||||
// notify any listeners that the web worker has loaded - HOWEVER, mixFetch hasn't been setup and the client started
|
||||
// call `setupMixFetch` from the main thread to start the Nym client
|
||||
postMessageWithType<LoadedEvent>({ kind: EventKinds.Loaded, args: { loaded: true } });
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { TextDecoder, TextEncoder } from 'node:util';
|
||||
import * as crypto from 'node:crypto';
|
||||
import * as fs from 'node:fs';
|
||||
import WebSocket from 'ws';
|
||||
import fetch, { Headers, Request, Response } from 'node-fetch';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
import { indexedDB } from 'fake-indexeddb';
|
||||
|
||||
(globalThis as any).performance = {
|
||||
now() {
|
||||
const [sec, nsec] = process.hrtime();
|
||||
return sec * 1000 + nsec / 1000000;
|
||||
},
|
||||
};
|
||||
|
||||
(globalThis as any).TextDecoder = TextDecoder;
|
||||
(globalThis as any).fetch = fetch;
|
||||
(globalThis as any).Headers = Headers;
|
||||
(globalThis as any).Request = Request;
|
||||
(globalThis as any).Response = Response;
|
||||
(globalThis as any).fs = fs;
|
||||
(globalThis as any).crypto = crypto;
|
||||
(globalThis as any).WebSocket = WebSocket;
|
||||
(globalThis as any).Worker = Worker;
|
||||
|
||||
globalThis.process = process;
|
||||
globalThis.TextEncoder = TextEncoder;
|
||||
globalThis.Reflect = Reflect;
|
||||
globalThis.Proxy = Proxy;
|
||||
globalThis.Error = Error;
|
||||
globalThis.Promise = Promise;
|
||||
globalThis.Object = Object;
|
||||
globalThis.indexedDB = indexedDB;
|
||||
|
||||
// has to be loaded after all the polyfill action
|
||||
// eslint-disable-next-line import/extensions, import/no-extraneous-dependencies
|
||||
import('@nymproject/mix-fetch-wasm-node/wasm_exec');
|
||||
@@ -1,69 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
/// <reference types="@nymproject/mix-fetch-wasm-node" />
|
||||
|
||||
// Copyright 2020-2023 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import '@nymproject/mix-fetch-wasm-node/mix_fetch_wasm_bg.wasm';
|
||||
|
||||
// @ts-ignore
|
||||
import getGoConnectionWasmBytes from '@nymproject/mix-fetch-wasm-node/go_conn.wasm';
|
||||
|
||||
import {
|
||||
send_client_data,
|
||||
start_new_mixnet_connection,
|
||||
mix_fetch_initialised,
|
||||
finish_mixnet_connection,
|
||||
set_panic_hook,
|
||||
} from '@nymproject/mix-fetch-wasm-node';
|
||||
|
||||
export async function loadGoWasm() {
|
||||
// rollup will provide a function to get the Go connection WASM bytes here
|
||||
const bytes = await getGoConnectionWasmBytes();
|
||||
|
||||
const go = new Go(); // Defined in wasm_exec.js
|
||||
|
||||
// the WebAssembly runtime will parse the bytes and then start the Go runtime
|
||||
const wasmObj = await WebAssembly.instantiate(bytes, go.importObject);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Loaded GO WASM');
|
||||
|
||||
go.run(wasmObj);
|
||||
}
|
||||
|
||||
function setupRsGoBridge() {
|
||||
const rsGoBridge = {
|
||||
send_client_data,
|
||||
start_new_mixnet_connection,
|
||||
mix_fetch_initialised,
|
||||
finish_mixnet_connection,
|
||||
};
|
||||
|
||||
// and to discourage users from trying to call those methods directly)
|
||||
// @ts-expect-error globalThis has index signature of any
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
globalThis.__rs_go_bridge__ = rsGoBridge;
|
||||
}
|
||||
|
||||
export async function loadWasm() {
|
||||
// load go WASM package
|
||||
await loadGoWasm();
|
||||
|
||||
console.log('Loaded GO WASM');
|
||||
|
||||
// sets up better stack traces in case of in-rust panics
|
||||
set_panic_hook();
|
||||
|
||||
setupRsGoBridge();
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"lib": ["es2021", "dom", "dom.iterable", "esnext", "webworker"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"declaration": true,
|
||||
"baseUrl": ".",
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": [
|
||||
"jest.*",
|
||||
"webpack.config.js",
|
||||
"webpack.prod.js",
|
||||
"webpack.common.js",
|
||||
"node_modules",
|
||||
"**/node_modules",
|
||||
"dist",
|
||||
"**/dist",
|
||||
"scripts",
|
||||
"jest",
|
||||
"__tests__",
|
||||
"**/__tests__",
|
||||
"__jest__",
|
||||
"**/__jest__",
|
||||
"config/*"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
declare module 'web-worker:*' {
|
||||
import { Worker } from 'node:worker_threads';
|
||||
|
||||
const WorkerFactory: new () => Worker;
|
||||
export default WorkerFactory;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
declare module '@nymproject/mix-fetch-wasm-node/wasm_exec' {
|
||||
export declare global {
|
||||
class Go {
|
||||
constructor();
|
||||
|
||||
importObject: any;
|
||||
|
||||
run(goWasm: any);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"presets": ["@babel/env"]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
# Nym MixFetch
|
||||
|
||||
This package is a drop-in replacement for `fetch` to send HTTP requests over the Nym Mixnet.
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const { mixFetch } = require('@nymproject/mix-fetch');
|
||||
|
||||
...
|
||||
|
||||
const response = await mixFetch('https://nymtech.net');
|
||||
const html = await response.text();
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
# Nym MixFetch
|
||||
|
||||
This package is a drop-in replacement for `fetch` to send HTTP requests over the Nym Mixnet.
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const { mixFetch } = require('@nymproject/mix-fetch');
|
||||
|
||||
...
|
||||
|
||||
const response = await mixFetch('https://nymtech.net');
|
||||
const html = await response.text();
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
# Nym MixFetch
|
||||
|
||||
This package is a drop-in replacement for `fetch` to send HTTP requests over the Nym Mixnet.
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import { mixFetch } from '@nymproject/mix-fetch';
|
||||
|
||||
...
|
||||
|
||||
const response = await mixFetch('https://nymtech.net');
|
||||
const html = await response.text();
|
||||
```
|
||||
@@ -1,17 +1,128 @@
|
||||
# Nym MixFetch
|
||||
# @nymproject/mix-fetch
|
||||
|
||||
This package is a drop-in replacement for `fetch` to send HTTP requests over the Nym Mixnet.
|
||||
Drop-in replacement for `fetch()` that routes HTTP/HTTPS through the Nym
|
||||
mixnet.
|
||||
|
||||
## Usage
|
||||
|
||||
Use `mixFetch` in your own project with:
|
||||
```ts
|
||||
import { setupMixTunnel, mixFetch } from '@nymproject/mix-fetch';
|
||||
|
||||
```js
|
||||
import { mixFetch } from '@nymproject/mix-fetch';
|
||||
await setupMixTunnel();
|
||||
|
||||
...
|
||||
|
||||
const response = await mixFetch('https://nymtech.net');
|
||||
const html = await response.text();
|
||||
const res = await mixFetch('https://example.com/');
|
||||
const html = await res.text();
|
||||
```
|
||||
|
||||
The `setupMixTunnel` call accepts the full `SetupMixTunnelOpts` surface: IPR
|
||||
pinning, cover-traffic toggles, SURB budgets, DNS overrides, TCP/connect
|
||||
timeouts, etc. See `@nymproject/mix-tunnel`'s typings for the complete list.
|
||||
|
||||
### Convenience: setup + fetch in one call
|
||||
|
||||
```ts
|
||||
import { createMixFetch } from '@nymproject/mix-fetch';
|
||||
|
||||
const mixFetch = await createMixFetch({ preferredIpr: '...' });
|
||||
const res = await mixFetch('https://example.com/');
|
||||
```
|
||||
|
||||
### Shared tunnel
|
||||
|
||||
Setting up the tunnel once unlocks all three smolmix SDKs simultaneously:
|
||||
|
||||
```ts
|
||||
import { setupMixTunnel, mixFetch } from '@nymproject/mix-fetch';
|
||||
import { mixDNS } from '@nymproject/mix-dns';
|
||||
import { MixWebSocket } from '@nymproject/mix-websocket';
|
||||
|
||||
await setupMixTunnel();
|
||||
|
||||
await mixFetch('https://example.com/');
|
||||
await mixDNS('example.com');
|
||||
const ws = new MixWebSocket('wss://echo.websocket.events');
|
||||
```
|
||||
|
||||
All three packages delegate to `@nymproject/mix-tunnel`, which owns the single
|
||||
Web Worker hosting `@nymproject/smolmix-wasm`.
|
||||
|
||||
## Default request headers
|
||||
|
||||
`mixFetch` ships a small browser-shape header shim. If the caller doesn't set
|
||||
these headers, smolmix-wasm fills them in before the request leaves the
|
||||
tunnel. 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` (the wasm build has no decompressor) |
|
||||
|
||||
Rationale: many CDNs (cloudflare bot management) and host policies (wikimedia)
|
||||
reject requests that lack browser-canonical headers. The shim is a floor:
|
||||
it does not attempt TLS-fingerprint or HTTP/2 impersonation, just the
|
||||
header-shaped tells. See the smolmix-wasm README "Browser-shape header
|
||||
shim" section for the full story and the JA3 caveats.
|
||||
|
||||
Override per-request like any other header:
|
||||
|
||||
```ts
|
||||
const res = await mixFetch('https://example.com', {
|
||||
headers: { 'User-Agent': 'my-app/1.0' },
|
||||
});
|
||||
```
|
||||
|
||||
## Migrating from v1.x
|
||||
|
||||
The legacy v1.x mix-fetch was a thin wrapper around a Go-based wasm network
|
||||
stack. v2.x is a thin wrapper around the smolmix-wasm Rust stack. The API
|
||||
surface is **not** identical; if your v1 code looks like the left column,
|
||||
update it to look like the right:
|
||||
|
||||
| v1.x | v2.x |
|
||||
|---|---|
|
||||
| `await createMixFetch({ preferredNetworkRequester, clientId, mixFetchOverride, responseBodyConfigMap })` | `await setupMixTunnel({ preferredIpr, clientId, connectTimeoutMs, ... })` |
|
||||
| `mixFetch(url, args, opts)` (3-arg) | `mixFetch(url, args)` (2-arg) + `setupMixTunnel(opts)` separately |
|
||||
| `args.mode = 'unsafe-ignore-cors'` | not needed; the IPR enforces its own egress policy, browser CORS doesn't apply |
|
||||
| `disconnectMixFetch()` | `disconnectMixTunnel()` |
|
||||
|
||||
Notable differences:
|
||||
|
||||
- **Gateway routing**: v1's `preferredGateway` and `preferredNetworkRequester`
|
||||
are gone. v2 uses smolmix's IPR auto-discovery by default; pin one with
|
||||
`preferredIpr` if needed.
|
||||
- **Response body handling**: v1's `responseBodyConfigMap` (used to opt
|
||||
particular MIME types into specific body parsers) is gone. v2 returns a
|
||||
real `Response` object; call `.text()`, `.arrayBuffer()`, `.json()`,
|
||||
`.blob()` as usual.
|
||||
- **Cover traffic**: v1's `clientOverride.coverTraffic` is now flat opts
|
||||
(`disableCoverTraffic`, `disablePoissonTraffic`).
|
||||
- **Bundle size**: v2 inlines the wasm + worker into a single ESM module.
|
||||
No sibling assets to ship, at the cost of a large single chunk. Plan
|
||||
code-splitting around it (dynamic `import('@nymproject/mix-fetch')` is
|
||||
the usual move).
|
||||
- **Runtime target**: v2 ships a single ESM bundle that runs in any environment
|
||||
exposing `Worker`, `WebAssembly`, `Blob`, and `URL.createObjectURL`. That
|
||||
covers modern browsers, Electron renderers, and mobile WebViews (Capacitor,
|
||||
Cordova, Ionic, iOS WKWebView, Android WebView). The v1
|
||||
`@nymproject/mix-fetch-node` companion for Node is not yet ported to the
|
||||
smolmix backend.
|
||||
|
||||
See `@nymproject/mix-tunnel`'s `SetupMixTunnelOpts` for the full v2 options
|
||||
surface.
|
||||
|
||||
## Consumer build requirements
|
||||
|
||||
Ships as raw ESM with a bare `import` of `@nymproject/mix-tunnel`. Use a
|
||||
bundler that follows package imports (webpack, rollup, parcel, vite,
|
||||
esbuild).
|
||||
|
||||
Runs in any environment exposing `Worker`, `WebAssembly`, `Blob`, and
|
||||
`URL.createObjectURL`. That covers modern browsers, Electron renderers,
|
||||
and mobile WebViews (Capacitor, Cordova, Ionic, iOS WKWebView, Android
|
||||
WebView). A Node-direct entry point is not yet ported from v1.
|
||||
|
||||
The wasm payload lives inside `@nymproject/mix-tunnel`, so your bundler
|
||||
will surface a single large chunk. Plan code-splitting around it
|
||||
(dynamic `import('@nymproject/mix-fetch')` is the usual move).
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# MixFetch Internal Tester
|
||||
|
||||
This project is for use by Nym developers only. Use at your own risk!
|
||||
|
||||
## Getting started
|
||||
|
||||
From the root of this repository run:
|
||||
|
||||
```
|
||||
pnpm i
|
||||
make sdk-wasm-build
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```
|
||||
cd sdk/typescript/packages/mix-fetch
|
||||
```
|
||||
|
||||
You can run in watch mode:
|
||||
|
||||
```
|
||||
pnpm start
|
||||
```
|
||||
|
||||
Or do a single build:
|
||||
|
||||
```
|
||||
pnpm build:dev:esm-no-inline
|
||||
```
|
||||
|
||||
Then, in another terminal:
|
||||
|
||||
```
|
||||
cd sdk/typescript/packages/mix-fetch/internal-dev/parcel
|
||||
pnpm i && pnpm start
|
||||
```
|
||||
|
||||
If you have trouble with changes not propagating:
|
||||
|
||||
```
|
||||
rm -rf node_modules && pnpm i && pnpm start
|
||||
```
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "@nymproject/mix-fetch-tester-parcel",
|
||||
"version": "1.0.6",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"build": "npx parcel build --no-cache --no-content-hash",
|
||||
"serve": "npx serve dist",
|
||||
"start": "npx parcel --no-cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/mix-fetch": ">=1.4.2-rc.0 || ^1"
|
||||
},
|
||||
"private": false,
|
||||
"source": "../src/index.html"
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Internal Tester</title>
|
||||
<script type="module" src="./index.ts"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Internal tester</h1>
|
||||
<p>Open dev tools to see more output and errors</p>
|
||||
<pre id="output"></pre>
|
||||
<div id="outputImage"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,77 +0,0 @@
|
||||
import { createMixFetch, disconnectMixFetch } from '@nymproject/mix-fetch';
|
||||
|
||||
function appendOutput(value: string) {
|
||||
const el = document.getElementById('output') as HTMLPreElement;
|
||||
const text = document.createTextNode(`${value}\n`);
|
||||
el.appendChild(text);
|
||||
}
|
||||
|
||||
function appendImageOutput(url: string) {
|
||||
const el = document.getElementById('outputImage') as HTMLPreElement;
|
||||
const imgNode = document.createElement('img');
|
||||
imgNode.src = url;
|
||||
el.appendChild(imgNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* The main entry point
|
||||
*/
|
||||
async function main() {
|
||||
appendOutput('Tester is starting up...');
|
||||
|
||||
// const addr =
|
||||
// 'EVdJ66jqpoVzmktVecy5UJxsTCEWo5gMn5zDZR7Hm8jy.GXNpoX7RcYcxKvBkV3dSHqC78WaPuWieweRPWzYqNhh5@GAjhJcrd6f1edaqUkfWCff6zdHoqo756qYrc2TfPuCXJ';
|
||||
// const addr = '7Y9eyF1p1JmzHnd7TVZufnQHkh93ASc9sRBCFY57ZGr8.F8KPyVMVqFQ5yJC3LqeP2ZC7fukzj9a1T426rjo432yT@q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1';
|
||||
const addr = undefined;
|
||||
|
||||
appendOutput('About to set up mixFetch...');
|
||||
const { mixFetch } = await createMixFetch({
|
||||
preferredNetworkRequester: addr,
|
||||
clientId: 'my-new-client-16',
|
||||
clientOverride: {
|
||||
coverTraffic: { disableLoopCoverTrafficStream: true },
|
||||
traffic: { disableMainPoissonPacketDistribution: true },
|
||||
},
|
||||
mixFetchOverride: { requestTimeoutMs: 60000 },
|
||||
responseBodyConfigMap: {},
|
||||
});
|
||||
(window as any).mixFetch = mixFetch;
|
||||
|
||||
if (!(window as any).mixFetch) {
|
||||
console.error('Oh no! Could not create mixFetch');
|
||||
appendOutput('Oh no! Could not create mixFetch');
|
||||
} else {
|
||||
appendOutput('Ready!');
|
||||
}
|
||||
|
||||
let url = 'https://nymtech.net/.wellknown/network-requester/standard-allowed-list.txt';
|
||||
appendOutput(`Using mixFetch to get ${url}...`);
|
||||
const args = { mode: 'unsafe-ignore-cors' };
|
||||
|
||||
let resp = await mixFetch(url, args);
|
||||
console.log({ resp });
|
||||
const text = await resp.text();
|
||||
|
||||
appendOutput(JSON.stringify(resp, null, 2));
|
||||
appendOutput(JSON.stringify({ text }, null, 2));
|
||||
|
||||
// console.log('disconnecting');
|
||||
// await disconnectMixFetch();
|
||||
// console.log('disconnected! all further usages should fail');
|
||||
|
||||
// get an image
|
||||
url = 'https://matrix.org/assets/frontpage/github-mark.svg';
|
||||
resp = await mixFetch(url, args);
|
||||
console.log({ resp });
|
||||
const buffer = await resp.arrayBuffer();
|
||||
const type = resp.headers.get('Content-Type') || 'image/svg';
|
||||
const blobUrl = URL.createObjectURL(new Blob([buffer], { type }));
|
||||
appendOutput(JSON.stringify({ bufferBytes: buffer.byteLength, blobUrl }, null, 2));
|
||||
appendImageOutput(blobUrl);
|
||||
}
|
||||
|
||||
// wait for the html to load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
// let's do this!
|
||||
main();
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
const path = require('path');
|
||||
const { mergeWithRules } = require('webpack-merge');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
const { webpackCommon } = require('../../../examples/.webpack/webpack.base');
|
||||
|
||||
console.log('mix-fetch package path is: ', path.dirname(require.resolve('@nymproject/mix-fetch/package.json')));
|
||||
|
||||
module.exports = mergeWithRules({
|
||||
module: {
|
||||
rules: {
|
||||
test: 'match',
|
||||
use: 'replace',
|
||||
},
|
||||
},
|
||||
})(
|
||||
webpackCommon(__dirname, [
|
||||
{
|
||||
inject: true,
|
||||
filename: 'index.html',
|
||||
template: path.resolve(__dirname, 'src/index.html'),
|
||||
chunks: ['index'],
|
||||
},
|
||||
]),
|
||||
{
|
||||
entry: {
|
||||
index: path.resolve(__dirname, 'src/index.ts'),
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
publicPath: '/',
|
||||
},
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
// copy the WASM files, because webpack doesn't do this automatically even though there are
|
||||
// `new URL(..., import.meta.url)` statements in the web worker
|
||||
// from: path.resolve(path.dirname(require.resolve('@nymproject/mix-fetch/package.json')), 'dist/esm/*.wasm'),
|
||||
from: path.resolve(path.dirname(require.resolve('@nymproject/mix-fetch/package.json')), '*.wasm'),
|
||||
to: '[name][ext]',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -1,15 +0,0 @@
|
||||
import preset from 'ts-jest/presets/index.js'
|
||||
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
export default {
|
||||
...preset.defaults,
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': [
|
||||
'ts-jest',
|
||||
{
|
||||
tsconfig: 'tsconfig.jest.json',
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -1,86 +1,52 @@
|
||||
{
|
||||
"name": "@nymproject/mix-fetch",
|
||||
"version": "1.4.3",
|
||||
"description": "This package is a drop-in replacement for `fetch` to send HTTP requests over the Nym Mixnet.",
|
||||
"version": "2.0.0",
|
||||
"description": "Drop-in `fetch` replacement that routes HTTP/HTTPS requests through the Nym mixnet.",
|
||||
"license": "Apache-2.0",
|
||||
"author": "Nym Technologies SA",
|
||||
"homepage": "https://nym.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nymtech/nym",
|
||||
"directory": "sdk/typescript/packages/mix-fetch"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/nymtech/nym/issues"
|
||||
},
|
||||
"keywords": ["nym", "mixnet", "privacy", "fetch", "http"],
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist/esm/worker.js",
|
||||
"dist/cjs/worker.js",
|
||||
"dist/**/*"
|
||||
],
|
||||
"main": "dist/cjs/index.js",
|
||||
"browser": "dist/esm/index.js",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"browser": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "scripts/build-prod.sh",
|
||||
"build:dev": "scripts/build.sh",
|
||||
"build:dev:esm": "MIX_FETCH_DEV_MODE=true scripts/build-dev-esm.sh",
|
||||
"build:dev:esm-no-inline": "scripts/build-dev-esm.sh",
|
||||
"build:worker": "rollup -c rollup-worker.config.mjs",
|
||||
"build:worker:full-fat": "rollup -c rollup-worker-full-fat.config.mjs",
|
||||
"build": "tsc",
|
||||
"clean": "rimraf dist",
|
||||
"docs:dev": "run-p docs:watch docs:serve ",
|
||||
"docs:generate": "typedoc",
|
||||
"docs:generate:prod": "typedoc --basePath ./docs/tsdoc/nymproject/sdk/",
|
||||
"docs:prod:build": "scripts/build-prod-docs-collect.sh",
|
||||
"docs:serve": "reload -b -d ./docs -p 3000",
|
||||
"docs:watch": "nodemon --ext ts --watch './src/**/*' --watch './typedoc.json' --exec \"pnpm docs:generate\"",
|
||||
"docs:generate:prod": "typedoc --basePath ./docs/tsdoc/nymproject/mix-fetch/",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"prebuild": "node scripts/showDependencyLocation.cjs",
|
||||
"start": "tsc -w",
|
||||
"start:dev": "nodemon --watch src -e ts,json --exec 'pnpm build:dev:esm'",
|
||||
"test": "node --experimental-vm-modules --no-warnings node_modules/jest/bin/jest.js -c=jest.config.mjs --no-cache",
|
||||
"tsc": "tsc --noEmit true"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nymproject/mix-fetch-wasm": ">=1.4.2-rc.0 || ^1",
|
||||
"comlink": "^4.3.1"
|
||||
"@nymproject/mix-tunnel": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "catalog:",
|
||||
"@babel/plugin-transform-async-to-generator": "catalog:",
|
||||
"@babel/preset-env": "catalog:",
|
||||
"@babel/preset-react": "catalog:",
|
||||
"@babel/preset-typescript": "catalog:",
|
||||
"@nymproject/eslint-config-react-typescript": "workspace:*",
|
||||
"@rollup/plugin-commonjs": "^24.0.1",
|
||||
"@rollup/plugin-inject": "^5.0.3",
|
||||
"@rollup/plugin-json": "^6.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-replace": "^5.0.2",
|
||||
"@rollup/plugin-terser": "^0.2.1",
|
||||
"@rollup/plugin-typescript": "^10.0.1",
|
||||
"@rollup/plugin-wasm": "^6.1.1",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^16.7.13",
|
||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||
"@typescript-eslint/parser": "catalog:",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-airbnb": "catalog:",
|
||||
"eslint-config-airbnb-typescript": "catalog:",
|
||||
"eslint-config-prettier": "catalog:",
|
||||
"eslint-import-resolver-root-import": "catalog:",
|
||||
"eslint-plugin-import": "catalog:",
|
||||
"eslint-plugin-jest": "catalog:",
|
||||
"eslint-plugin-jsx-a11y": "catalog:",
|
||||
"eslint-plugin-prettier": "catalog:",
|
||||
"eslint-plugin-react": "catalog:",
|
||||
"eslint-plugin-react-hooks": "catalog:",
|
||||
"jest": "^29.5.0",
|
||||
"nodemon": "^2.0.21",
|
||||
"reload": "^3.2.1",
|
||||
"rimraf": "catalog:",
|
||||
"rollup": "^3.9.1",
|
||||
"rollup-plugin-base64": "^1.0.1",
|
||||
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "catalog:",
|
||||
"tslib": "catalog:",
|
||||
"typedoc": "^0.24.8",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"private": false,
|
||||
"type": "module",
|
||||
"types": "./dist/esm/index.d.ts"
|
||||
"types": "./dist/index.d.ts",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { getConfig } from './rollup/cjs.mjs';
|
||||
|
||||
export default {
|
||||
...getConfig({
|
||||
inline: true,
|
||||
outputDir: 'dist/cjs-full-fat',
|
||||
}),
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import { getConfig } from './rollup/cjs.mjs';
|
||||
|
||||
export default {
|
||||
...getConfig({ inline: false }),
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { getConfig } from './rollup/esm.mjs';
|
||||
|
||||
export default {
|
||||
...getConfig({
|
||||
inline: true,
|
||||
outputDir: 'dist/esm-full-fat',
|
||||
}),
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { getConfig } from './rollup/esm.mjs';
|
||||
|
||||
export default {
|
||||
...getConfig({
|
||||
// by default, the web worker will not be inlined, in local development mode it will be
|
||||
inline: process.env.MIX_FETCH_DEV_MODE === 'true',
|
||||
}),
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { getConfig } from './rollup/worker.mjs';
|
||||
|
||||
export default {
|
||||
...getConfig({
|
||||
inlineWasm: true,
|
||||
format: 'cjs',
|
||||
}),
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import { wasm } from '@rollup/plugin-wasm';
|
||||
import webWorkerLoader from 'rollup-plugin-web-worker-loader';
|
||||
|
||||
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
|
||||
|
||||
/**
|
||||
* Gets the config for bundling the package as a CommonJS module.
|
||||
*
|
||||
* @param opts Options:
|
||||
* `{ inline: boolean }` - set inline to true to inline the web worker in the main bundle
|
||||
* `{ outputDir: string }` - override the destination
|
||||
*/
|
||||
export const getConfig = (opts) => ({
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
dir: opts.outputDir || 'dist/cjs',
|
||||
format: 'cjs',
|
||||
},
|
||||
plugins: [
|
||||
webWorkerLoader({ targetPlatform: 'browser', inline: opts.inline }), // the inline param is used here
|
||||
resolve({ extensions }),
|
||||
wasm({ maxFileSize: 10000000, targetEnv: 'browser' }),
|
||||
typescript({
|
||||
compilerOptions: { outDir: opts.outputDir || 'dist/cjs', target: 'es5' },
|
||||
exclude: ['worker/*'],
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import webWorkerLoader from 'rollup-plugin-web-worker-loader';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
|
||||
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
|
||||
|
||||
/**
|
||||
* Gets the config for bundling the package as an ES Module.
|
||||
*
|
||||
* @param opts Options:
|
||||
* `{ inline: boolean }` - set inline to true to inline the web worker in the main bundle
|
||||
* `{ outputDir: string }` - override the destination *
|
||||
*/
|
||||
export const getConfig = (opts) => ({
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
dir: opts.outputDir || 'dist/esm',
|
||||
format: 'es',
|
||||
},
|
||||
plugins: [
|
||||
webWorkerLoader({ targetPlatform: 'browser', inline: opts.inline }), // the inline param is used here
|
||||
replace({
|
||||
// when loading the web worker as a full ES module, tell pass `new Worker({ type: 'module'})` to tell
|
||||
// the browser to load and allow imports inside the worker code. Also load as a URL so relative paths work.
|
||||
// values: opts.inline
|
||||
// ? undefined
|
||||
// : {
|
||||
// "createURLWorkerFactory('web-worker-0.js')":
|
||||
// "createURLWorkerFactory(new URL('web-worker-0.js', import.meta.url))",
|
||||
// },
|
||||
values: {
|
||||
"createURLWorkerFactory('web-worker-0.js')":
|
||||
"createURLWorkerFactory(new URL('web-worker-0.js', import.meta.url))",
|
||||
},
|
||||
delimiters: ['', ''],
|
||||
preventAssignment: true,
|
||||
}),
|
||||
resolve({ extensions }),
|
||||
typescript({
|
||||
exclude: ['worker/*'],
|
||||
compilerOptions: { outDir: opts.outputDir || 'dist/esm' },
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import { wasm } from '@rollup/plugin-wasm';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
|
||||
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
|
||||
|
||||
/**
|
||||
* Configure worker output
|
||||
*
|
||||
* @param opts
|
||||
* `format`: `es` or `cjs`,
|
||||
* `inlineWasm`: true or false,
|
||||
* `tsTarget`: `es5` or `es6`
|
||||
*/
|
||||
export const getConfig = (opts) => ({
|
||||
input: 'src/worker/index.ts',
|
||||
output: {
|
||||
dir: 'dist',
|
||||
format: opts?.format || 'es',
|
||||
},
|
||||
plugins: [
|
||||
resolve({ extensions }),
|
||||
// this is some nasty monkey patching that removes the WASM URL (because it is handled by the `wasm` plugin)
|
||||
replace({
|
||||
values: { "input = new URL('mix_fetch_wasm_bg.wasm', import.meta.url);": 'input = undefined;' },
|
||||
delimiters: ['', ''],
|
||||
preventAssignment: true,
|
||||
}),
|
||||
opts?.inlineWasm === true
|
||||
? wasm({ maxFileSize: 10_000_000, targetEnv: 'browser' }) // force the wasm plugin to embed the wasm bundle - this means no downstream bundlers have to worry about handling it
|
||||
: wasm({
|
||||
targetEnv: 'browser',
|
||||
fileName: '[name].wasm',
|
||||
}),
|
||||
typescript({
|
||||
compilerOptions: {
|
||||
declaration: false,
|
||||
target: opts?.tsTarget || 'es6',
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
#-------------------------------------------------------
|
||||
# WEB WORKER (mix-fetch WASM)
|
||||
#-------------------------------------------------------
|
||||
# The web worker needs to be bundled because the WASM bundle needs to be loaded synchronously and all dependencies
|
||||
# must be included in the worker script (because it is not loaded as an ES Module)
|
||||
|
||||
# build the worker
|
||||
rollup -c rollup-worker.config.mjs
|
||||
|
||||
# move it next to the Typescript `src/index.ts` so it can be inlined by rollup
|
||||
rm -f src/worker/*.js
|
||||
rm -f src/worker/*.wasm
|
||||
mv dist/index.js src/worker/worker.js
|
||||
|
||||
# move WASM files out of build area
|
||||
mkdir -p dist/worker
|
||||
mv dist/*.wasm dist/worker
|
||||
|
||||
#-------------------------------------------------------
|
||||
# ESM
|
||||
#-------------------------------------------------------
|
||||
|
||||
# build the SDK as a ESM bundle
|
||||
rollup -c rollup-esm.config.mjs
|
||||
|
||||
# move WASM files into place
|
||||
cp dist/worker/*.wasm dist/esm
|
||||
node scripts/postBuildReplace.mjs dist
|
||||
|
||||
#-------------------------------------------------------
|
||||
# CLEAN UP
|
||||
#-------------------------------------------------------
|
||||
|
||||
# remove typings that aren't needed
|
||||
rm -rf dist/esm/worker
|
||||
|
||||
# clear staging area
|
||||
rm -rf dist/worker
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
rm -rf ../../../../dist/ts/docs/tsdoc/nymproject/mix-fetch || true
|
||||
|
||||
# run the build
|
||||
pnpm docs:generate:prod
|
||||
|
||||
# move the output outside of the yarn/npm workspaces
|
||||
mkdir -p ../../../../dist/ts/docs/tsdoc/nymproject
|
||||
mv docs ../../../../dist/ts/docs/tsdoc/nymproject/mix-fetch
|
||||
|
||||
echo "Output can be found in:"
|
||||
realpath ../../../../dist/ts/docs/tsdoc/nymproject/mix-fetch
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
rm -rf dist || true
|
||||
rm -rf ../../../../dist/ts/sdk/mix-fetch || true
|
||||
|
||||
# run the build
|
||||
scripts/build.sh
|
||||
node scripts/buildPackageJson.mjs
|
||||
|
||||
# move the output outside of the yarn/npm workspaces
|
||||
mkdir -p ../../../../dist/ts/sdk
|
||||
mv dist ../../../../dist/ts/sdk
|
||||
mv ../../../../dist/ts/sdk/dist ../../../../dist/ts/sdk/mix-fetch
|
||||
|
||||
echo "Output can be found in:"
|
||||
realpath ../../../../dist/ts/sdk/mix-fetch
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
rm -rf dist || true
|
||||
|
||||
#-------------------------------------------------------
|
||||
# WEB WORKER (mix-fetch WASM)
|
||||
#-------------------------------------------------------
|
||||
# The web worker needs to be bundled because the WASM bundle needs to be loaded synchronously and all dependencies
|
||||
# must be included in the worker script (because it is not loaded as an ES Module)
|
||||
|
||||
# build the worker
|
||||
rollup -c rollup-worker.config.mjs
|
||||
|
||||
# move it next to the Typescript `src/index.ts` so it can be inlined by rollup
|
||||
rm -f src/worker/*.js
|
||||
rm -f src/worker/*.wasm
|
||||
mv dist/index.js src/worker/worker.js
|
||||
|
||||
# move WASM files out of build area
|
||||
mkdir -p dist/worker
|
||||
mv dist/*.wasm dist/worker
|
||||
|
||||
#-------------------------------------------------------
|
||||
# ESM
|
||||
#-------------------------------------------------------
|
||||
|
||||
# build the SDK as a ESM bundle
|
||||
rollup -c rollup-esm.config.mjs
|
||||
|
||||
# move WASM files into place
|
||||
cp dist/worker/*.wasm dist/esm
|
||||
|
||||
#-------------------------------------------------------
|
||||
# COMMON JS
|
||||
#-------------------------------------------------------
|
||||
# Some old build systems cannot fully handle ESM or ES2021, so build
|
||||
# a CommonJS bundle targeting ES5
|
||||
|
||||
# build the SDK as a CommonJS bundle
|
||||
rollup -c rollup-cjs.config.mjs
|
||||
|
||||
# move WASM files into place
|
||||
cp dist/worker/*.wasm dist/cjs
|
||||
node scripts/postBuildReplace.mjs dist
|
||||
|
||||
#-------------------------------------------------------
|
||||
# WEB WORKER (full-fat)
|
||||
#-------------------------------------------------------
|
||||
|
||||
# build the worker
|
||||
rollup -c rollup-worker-full-fat.config.mjs
|
||||
|
||||
# move it next to the Typescript `src/index.ts` so it can be inlined by rollup
|
||||
rm -f src/worker/*.js
|
||||
rm -f src/worker/*.wasm
|
||||
mv dist/index.js src/worker/worker.js
|
||||
|
||||
# move WASM files out of build area
|
||||
mkdir -p dist/worker
|
||||
|
||||
#-------------------------------------------------------
|
||||
# ESM (full-fat)
|
||||
#-------------------------------------------------------
|
||||
|
||||
# build the SDK as a ESM bundle (with worker inlined as a blob)
|
||||
rollup -c rollup-esm-full-fat.config.mjs
|
||||
|
||||
#-------------------------------------------------------
|
||||
# COMMON JS (full-fat)
|
||||
#-------------------------------------------------------
|
||||
# Some old build systems cannot fully handle ESM or ES2021, so build
|
||||
# a CommonJS bundle targeting ES5
|
||||
|
||||
# build the SDK as a CommonJS bundle (with worker inlined as a blob)
|
||||
rollup -c rollup-cjs-full-fat.config.mjs
|
||||
|
||||
#-------------------------------------------------------
|
||||
# CLEAN UP
|
||||
#-------------------------------------------------------
|
||||
|
||||
rm -rf dist/cjs/worker
|
||||
rm -rf dist/esm/worker
|
||||
rm -rf dist/cjs-full-fat/worker
|
||||
rm -rf dist/esm-full-fat/worker
|
||||
|
||||
# copy README
|
||||
cp README.md dist/esm
|
||||
cp README-CommonJS.md dist/cjs/README.md
|
||||
cp README-CommonJS-full-fat.md dist/cjs-full-fat/README.md
|
||||
cp README-full-fat.md dist/esm-full-fat/README.md
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
|
||||
// parse the package.json from the SDK, so we can keep fields like the name and version
|
||||
const json = JSON.parse(fs.readFileSync('package.json').toString());
|
||||
|
||||
// defaults (NB: these are in the output file locations)
|
||||
const browser = 'index.js';
|
||||
const main = 'index.js';
|
||||
const types = 'index.d.ts';
|
||||
|
||||
const getPackageJson = (type, suffix) => ({
|
||||
name: `${json.name}${suffix ? `-${suffix}` : ''}`,
|
||||
version: json.version,
|
||||
license: json.license,
|
||||
author: json.author,
|
||||
type,
|
||||
browser,
|
||||
main,
|
||||
types,
|
||||
});
|
||||
|
||||
fs.writeFileSync('dist/cjs/package.json', JSON.stringify(getPackageJson('commonjs', 'commonjs'), null, 2));
|
||||
fs.writeFileSync('dist/esm/package.json', JSON.stringify(getPackageJson('module'), null, 2));
|
||||
|
||||
fs.writeFileSync(
|
||||
'dist/cjs-full-fat/package.json',
|
||||
JSON.stringify(getPackageJson('commonjs', 'commonjs-full-fat'), null, 2),
|
||||
);
|
||||
fs.writeFileSync('dist/esm-full-fat/package.json', JSON.stringify(getPackageJson('module', 'full-fat'), null, 2));
|
||||
@@ -1,45 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// first level is a list of file names to process, then for each copying the @rollup/replace plugin syntax here:
|
||||
// key is what to search for, value is what to replace it with
|
||||
const replaceConfig = {
|
||||
'esm/web-worker-0.js': {
|
||||
// add `import.meta.url` to make WASM file relative to the worker JS file
|
||||
"_loadWasmModule(0, 'mix_fetch_wasm_bg.wasm',":
|
||||
"_loadWasmModule(0, new URL('mix_fetch_wasm_bg.wasm', import.meta.url),",
|
||||
"_loadWasmModule(0, 'go_conn.wasm',": "_loadWasmModule(0, new URL('go_conn.wasm', import.meta.url),",
|
||||
},
|
||||
'esm/index.js': {
|
||||
// tell the browser the worker is a module, so it should provide `import.meta.url` needed above
|
||||
'const worker = new WorkerFactory();': "const worker = new WorkerFactory({ type: 'module' });",
|
||||
},
|
||||
};
|
||||
|
||||
const basePathToFindFilesIn = process.argv[2];
|
||||
|
||||
console.log(`Replacing files in "${path.resolve(basePathToFindFilesIn)}"...`);
|
||||
|
||||
Object.keys(replaceConfig).forEach((filename) => {
|
||||
const absFilename = path.resolve(basePathToFindFilesIn, filename);
|
||||
|
||||
if (!fs.existsSync(absFilename)) {
|
||||
console.log(`Skipping replacing ${filename} as does not exist`);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(absFilename).toString();
|
||||
|
||||
console.log(`Replacing values in "${absFilename}"...`);
|
||||
|
||||
const replacementMap = replaceConfig[filename];
|
||||
|
||||
let newContent = content;
|
||||
Object.keys(replacementMap).forEach((toFind) => {
|
||||
const toReplace = replacementMap[toFind];
|
||||
newContent = newContent.replaceAll(toFind, toReplace);
|
||||
fs.writeFileSync(absFilename, newContent);
|
||||
});
|
||||
|
||||
console.log('Done');
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
rm -rf dist || true
|
||||
rm -rf ../../../../dist || true
|
||||
|
||||
yarn
|
||||
yarn build
|
||||
cd ../../../../dist/sdk
|
||||
|
||||
cd cjs
|
||||
echo "Publishing CommonJS package to NPM.."
|
||||
npm publish --access=public
|
||||
cd ..
|
||||
|
||||
cd esm
|
||||
echo "Publishing ESM package to NPM.."
|
||||
npm publish --access=public
|
||||
cd ..
|
||||
@@ -1,8 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const packageName = '@nymproject/mix-fetch-wasm';
|
||||
const packageJsonPath = require.resolve(packageName + '/package.json');
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString());
|
||||
|
||||
console.log(`🟢🟢🟢 ${packageName} is at ${packageJsonPath} is version ${packageJson.version}`);
|
||||
@@ -1,77 +0,0 @@
|
||||
import InlineWasmWebWorker from 'web-worker:./worker/worker';
|
||||
import * as Comlink from 'comlink';
|
||||
import type { IMixFetchWebWorker } from './types';
|
||||
import { EventKinds, IMixFetch } from './types';
|
||||
|
||||
const createWorker = async () =>
|
||||
new Promise<Worker>((resolve, reject) => {
|
||||
// rollup will inline the built worker script, so that when the SDK is used in
|
||||
// other projects, they will not need to mess around trying to bundle it
|
||||
// however, it will make this SDK bundle bigger because of Base64 inline data
|
||||
const worker = new InlineWasmWebWorker();
|
||||
|
||||
worker.addEventListener('error', reject);
|
||||
worker.addEventListener(
|
||||
'message',
|
||||
(msg) => {
|
||||
worker.removeEventListener('error', reject);
|
||||
if (msg.data?.kind === EventKinds.Loaded) {
|
||||
resolve(worker);
|
||||
} else {
|
||||
reject(msg);
|
||||
}
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
|
||||
const convertHeaders = (headers: any): Headers => {
|
||||
const out = new Headers();
|
||||
Object.keys(headers).forEach((key) => {
|
||||
out.append(key, headers[key]);
|
||||
});
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* Use this method to initialise `mixFetch`.
|
||||
*
|
||||
* @returns An instance of `mixFetch` that you can use to make your requests using the same interface as `fetch`.
|
||||
*/
|
||||
export const createMixFetch = async (): Promise<IMixFetch> => {
|
||||
// start the worker
|
||||
const worker = await createWorker();
|
||||
|
||||
// bind with Comlink
|
||||
const wrappedWorker = Comlink.wrap<IMixFetchWebWorker>(worker);
|
||||
|
||||
// handle the responses
|
||||
const mixFetchWebWorker: IMixFetch = {
|
||||
setupMixFetch: wrappedWorker.setupMixFetch,
|
||||
mixFetch: async (url: string, args: any) => {
|
||||
const workerResponse = await wrappedWorker.mixFetch(url, args);
|
||||
if (!workerResponse) {
|
||||
throw new Error('No response received');
|
||||
}
|
||||
console.log({ workerResponse });
|
||||
const { headers: headersRaw, status, statusText } = workerResponse;
|
||||
|
||||
// reconstruct the Headers object instance from a plain object
|
||||
const headers = convertHeaders(headersRaw);
|
||||
|
||||
// handle blobs
|
||||
if (workerResponse.body.blobUrl) {
|
||||
const blob = await (await fetch(workerResponse.body.blobUrl)).blob();
|
||||
const body = await blob.arrayBuffer();
|
||||
return new Response(body, { headers, status, statusText });
|
||||
}
|
||||
|
||||
// handle everything else
|
||||
const body = Object.values(workerResponse.body)[0]; // we are expecting only one value to be set in `.body`
|
||||
return new Response(body, { headers, status, statusText });
|
||||
},
|
||||
disconnectMixFetch: wrappedWorker.disconnectMixFetch,
|
||||
};
|
||||
|
||||
return mixFetchWebWorker;
|
||||
};
|
||||
@@ -1,79 +1,65 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import type { SetupMixFetchOps, IMixFetchFn, IMixFetch } from './types';
|
||||
import { createMixFetch as createMixFetchInternal } from './create-mix-fetch';
|
||||
// @nymproject/mix-fetch
|
||||
//
|
||||
// Drop-in `fetch()` replacement that routes HTTP/HTTPS through the Nym mixnet.
|
||||
// Shares a single mixnet tunnel with @nymproject/mix-dns and
|
||||
// @nymproject/mix-websocket via @nymproject/mix-tunnel.
|
||||
//
|
||||
// v2 surface (clean, no legacy shim):
|
||||
//
|
||||
// import { setupMixTunnel, mixFetch, disconnectMixTunnel } from '@nymproject/mix-fetch';
|
||||
// await setupMixTunnel({ preferredIpr, disableCoverTraffic, ... });
|
||||
// const res = await mixFetch('https://example.com');
|
||||
// await disconnectMixTunnel();
|
||||
//
|
||||
// Or, for the "setup + fetch" convenience:
|
||||
//
|
||||
// const mixFetch = await createMixFetch({ ...opts });
|
||||
// const res = await mixFetch('https://example.com');
|
||||
//
|
||||
// See `SetupMixTunnelOpts` (re-exported from @nymproject/mix-tunnel) for the
|
||||
// full options surface — mirrors smolmix-wasm's SetupOpts.
|
||||
|
||||
// this is the default timeout for getting a response
|
||||
const REQUEST_TIMEOUT_MILLISECONDS = 60_000;
|
||||
import {
|
||||
getMixTunnel,
|
||||
setupMixTunnel,
|
||||
disconnectMixTunnel,
|
||||
getTunnelState,
|
||||
SetupMixTunnelOpts,
|
||||
} from '@nymproject/mix-tunnel';
|
||||
|
||||
export * from './types';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/**
|
||||
* Keep a singleton of the mixFetch interface on the `window` object.
|
||||
*/
|
||||
__mixFetchGlobal?: IMixFetch;
|
||||
}
|
||||
}
|
||||
export { setupMixTunnel, disconnectMixTunnel, getTunnelState };
|
||||
export type { SetupMixTunnelOpts };
|
||||
|
||||
/**
|
||||
* Create a global mixFetch instance and optionally configure settings.
|
||||
* Fetch over the mixnet. Drop-in replacement for the browser `fetch()`.
|
||||
*
|
||||
* @param opts Optional settings
|
||||
* Requires the tunnel to be up — call `setupMixTunnel(opts)` first, or use
|
||||
* `createMixFetch(opts)` to combine setup + fetch.
|
||||
*/
|
||||
export const createMixFetch = async (opts?: SetupMixFetchOps) => {
|
||||
if (!window) {
|
||||
throw new Error('`window` is not defined');
|
||||
}
|
||||
export const mixFetch = async (url: string, init?: RequestInit): Promise<Response> => {
|
||||
const tunnel = await getMixTunnel();
|
||||
// The wasm-side returns a `{body: Uint8Array, status, statusText,
|
||||
// headers: [[k,v]...]}` object (see smolmix `serialise_response`).
|
||||
// The `Headers` constructor accepts the [[k,v]] pair shape directly so
|
||||
// repeated names like `Set-Cookie` survive.
|
||||
const raw = await tunnel.mixFetch(url, init ?? {});
|
||||
|
||||
if (!window.__mixFetchGlobal) {
|
||||
// load the worker and set up mixFetch with defaults
|
||||
window.__mixFetchGlobal = await createMixFetchInternal();
|
||||
await window.__mixFetchGlobal.setupMixFetch(opts);
|
||||
|
||||
window.onunload = async () => {
|
||||
if (window.__mixFetchGlobal) {
|
||||
await window.__mixFetchGlobal.disconnectMixFetch();
|
||||
}
|
||||
};
|
||||
}
|
||||
return window.__mixFetchGlobal;
|
||||
};
|
||||
|
||||
/**
|
||||
* mixFetch is a drop-in replacement for the standard `fetch` interface.
|
||||
*
|
||||
* @param url The URL to fetch from.
|
||||
* @param args Fetch options.
|
||||
* @param opts Optionally configure mixFetch when it gets created. This only happens once, the first time it gets used.
|
||||
*/
|
||||
export const mixFetch: IMixFetchFn = async (url, args, opts?: SetupMixFetchOps) => {
|
||||
// ensure mixFetch instance exists
|
||||
const instance = await createMixFetch({
|
||||
mixFetchOverride: {
|
||||
requestTimeoutMs: REQUEST_TIMEOUT_MILLISECONDS,
|
||||
},
|
||||
...opts,
|
||||
// `raw.body` is `Uint8Array<ArrayBufferLike>` — the ArrayBufferLike side
|
||||
// includes SharedArrayBuffer, which the Response constructor's BodyInit
|
||||
// doesn't accept. The runtime value is always a non-shared array; cast.
|
||||
return new Response(raw.body as BodyInit, {
|
||||
status: raw.status,
|
||||
statusText: raw.statusText,
|
||||
headers: new Headers(raw.headers),
|
||||
});
|
||||
|
||||
// execute user request
|
||||
return instance.mixFetch(url, args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops the usage of mixFetch and disconnect the client from the mixnet.
|
||||
* 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.
|
||||
*/
|
||||
export const disconnectMixFetch = async (): Promise<void> => {
|
||||
if (!window) {
|
||||
throw new Error('`window` is not defined');
|
||||
}
|
||||
|
||||
// JS: I'm ignoring this lint (no-else-return) because I want to explicitly state
|
||||
// that `__mixFetchGlobal` is definitely not null in the else branch.
|
||||
if (!window.__mixFetchGlobal) {
|
||||
throw new Error("mixFetch hasn't been setup");
|
||||
// eslint-disable-next-line no-else-return
|
||||
} else {
|
||||
return window.__mixFetchGlobal.disconnectMixFetch();
|
||||
}
|
||||
export const createMixFetch = async (opts?: SetupMixTunnelOpts): Promise<typeof mixFetch> => {
|
||||
await setupMixTunnel(opts);
|
||||
return mixFetch;
|
||||
};
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import type { MixFetchOpts } from '@nymproject/mix-fetch-wasm';
|
||||
|
||||
type IMixFetchWorkerFn = (url: string, args: any) => Promise<MixFetchWebWorkerResponse>;
|
||||
|
||||
// export type IMixFetchFn = typeof fetch;
|
||||
export type IMixFetchFn = (url: string, args: any, opts?: SetupMixFetchOps) => Promise<Response>;
|
||||
|
||||
export type SetupMixFetchOps = MixFetchOpts & {
|
||||
responseBodyConfigMap?: ResponseBodyConfigMap;
|
||||
};
|
||||
|
||||
export interface IMixFetchWebWorker {
|
||||
mixFetch: IMixFetchWorkerFn;
|
||||
setupMixFetch: (opts?: SetupMixFetchOps) => Promise<void>;
|
||||
disconnectMixFetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface IMixFetch {
|
||||
mixFetch: IMixFetchFn;
|
||||
setupMixFetch: (opts?: SetupMixFetchOps) => Promise<void>;
|
||||
disconnectMixFetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
export enum EventKinds {
|
||||
Loaded = 'Loaded',
|
||||
}
|
||||
|
||||
export interface LoadedEvent {
|
||||
kind: EventKinds.Loaded;
|
||||
args: {
|
||||
loaded: true;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResponseBody {
|
||||
uint8array?: Uint8Array;
|
||||
json?: any;
|
||||
text?: string;
|
||||
formData?: any;
|
||||
blobUrl?: string;
|
||||
}
|
||||
|
||||
export type ResponseBodyMethod = 'uint8array' | 'json' | 'text' | 'formData' | 'blob';
|
||||
|
||||
export interface ResponseBodyConfigMap {
|
||||
/**
|
||||
* Set the response `Content-Type`s to decode as uint8array.
|
||||
*/
|
||||
uint8array?: Array<RegExp | string>;
|
||||
|
||||
/**
|
||||
* Set the response `Content-Type`s to decode with the `json()` response body method.
|
||||
*/
|
||||
json?: Array<RegExp | string>;
|
||||
|
||||
/**
|
||||
* Set the response `Content-Type`s to decode with the `text()` response body method.
|
||||
*/
|
||||
text?: Array<RegExp | string>;
|
||||
|
||||
/**
|
||||
* Set the response `Content-Type`s to decode with the `formData()` response body method.
|
||||
*/
|
||||
formData?: Array<RegExp | string>;
|
||||
|
||||
/**
|
||||
* Set the response `Content-Type`s to decode with the `blob()` response body method.
|
||||
*/
|
||||
blob?: Array<RegExp | string>;
|
||||
/**
|
||||
* Set this to the default fallback method. Set to `undefined` if you want to ignore unknown types.
|
||||
*/
|
||||
|
||||
fallback?: ResponseBodyMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default values for the handling of response bodies.
|
||||
*/
|
||||
export const ResponseBodyConfigMapDefaults: ResponseBodyConfigMap = {
|
||||
uint8array: ['application/octet-stream'],
|
||||
json: ['application/json', 'text/json', /application\/json.*/, /text\/json\+.*/],
|
||||
text: ['text/plain', /text\/plain.*/, 'text/html', /text\/html.*/],
|
||||
formData: ['application/x-www-form-urlencoded', 'multipart/form-data'],
|
||||
blob: [/image\/.*/, /video\/.*/],
|
||||
fallback: 'blob',
|
||||
};
|
||||
|
||||
export interface MixFetchWebWorkerResponse {
|
||||
body: ResponseBody;
|
||||
url: string;
|
||||
headers: any;
|
||||
status: number;
|
||||
statusText: string;
|
||||
type: string;
|
||||
ok: boolean;
|
||||
redirected: boolean;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { handleResponseMimeTypes } from './handle-response-mime-types';
|
||||
|
||||
describe('handleResponseMimeTypes', () => {
|
||||
test('gracefully handles empty values', async () => {
|
||||
const resp = await handleResponseMimeTypes(new Response());
|
||||
expect(Object.values(resp)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('handles text', async () => {
|
||||
const TEXT = 'This is text';
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(TEXT, { headers: new Headers([['Content-Type', 'text/plain']]) }),
|
||||
);
|
||||
expect(resp.text).toBe(TEXT);
|
||||
});
|
||||
test('handles text (charset=utf-8)', async () => {
|
||||
const TEXT = 'This is text';
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(TEXT, { headers: new Headers([['Content-Type', 'text/plain; charset=utf-8']]) }),
|
||||
);
|
||||
expect(resp.text).toBe(TEXT);
|
||||
});
|
||||
test('handles html', async () => {
|
||||
const TEXT = 'This is html';
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(TEXT, { headers: new Headers([['Content-Type', 'text/html']]) }),
|
||||
);
|
||||
expect(resp.text).toBe(TEXT);
|
||||
});
|
||||
test('handles html (charset=utf-8)', async () => {
|
||||
const TEXT = 'This is html';
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(TEXT, { headers: new Headers([['Content-Type', 'text/html; charset=utf-8']]) }),
|
||||
);
|
||||
expect(resp.text).toBe(TEXT);
|
||||
});
|
||||
test('handles images', async () => {
|
||||
const DATA = new Uint8Array([0, 1, 2, 3]).buffer;
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(DATA, { headers: new Headers([['Content-Type', 'image/jpeg']]) }),
|
||||
);
|
||||
expect(resp.blobUrl).toBeDefined();
|
||||
});
|
||||
test('handles videos', async () => {
|
||||
const DATA = new Uint8Array([0, 1, 2, 3]).buffer;
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(DATA, { headers: new Headers([['Content-Type', 'video/mpeg4']]) }),
|
||||
);
|
||||
expect(resp.blobUrl).toBeDefined();
|
||||
});
|
||||
test('handles form data when URL encoded', async () => {
|
||||
const formData = 'foo=bar&baz=42';
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(formData, { headers: new Headers([['Content-Type', 'application/x-www-form-urlencoded']]) }),
|
||||
);
|
||||
expect(resp.formData.foo).toBe('bar');
|
||||
expect(resp.formData.baz).toBe('42');
|
||||
});
|
||||
test('handles JSON data', async () => {
|
||||
const json = '{ "foo": "bar", "baz": 42 }';
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(json, { headers: new Headers([['Content-Type', 'application/json']]) }),
|
||||
);
|
||||
expect(resp.text).toBe(json);
|
||||
});
|
||||
test('handles JSON data (charset=utf-8)', async () => {
|
||||
const json = '{ "foo": "bar", "baz": 42 }';
|
||||
const resp = await handleResponseMimeTypes(
|
||||
new Response(json, { headers: new Headers([['Content-Type', 'application/json; charset=utf-8']]) }),
|
||||
);
|
||||
expect(resp.text).toBe(json);
|
||||
});
|
||||
});
|
||||
@@ -1,111 +0,0 @@
|
||||
import type { ResponseBody, ResponseBodyConfigMap, ResponseBodyMethod } from '../types';
|
||||
import { ResponseBodyConfigMapDefaults } from '../types';
|
||||
|
||||
const getContentType = (response?: Response) => {
|
||||
if (!response) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// this is what should be returned in the headers
|
||||
if (response.headers.has('Content-Type')) {
|
||||
return response.headers.get('Content-Type') as string;
|
||||
}
|
||||
|
||||
// handle weird servers that use lowercase headers
|
||||
if (response.headers.has('content-type')) {
|
||||
return response.headers.get('content-type') as string;
|
||||
}
|
||||
|
||||
// the Content-Type/content-type header is not part of the response
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const doHandleResponseMethod = async (response: Response, method?: ResponseBodyMethod): Promise<ResponseBody> => {
|
||||
switch (method) {
|
||||
case 'uint8array':
|
||||
return {
|
||||
uint8array: new Uint8Array(await response.arrayBuffer()),
|
||||
};
|
||||
case 'json':
|
||||
case 'text':
|
||||
return { text: await response.text() };
|
||||
case 'blob': {
|
||||
const blob = await response.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
return { blobUrl };
|
||||
}
|
||||
case 'formData': {
|
||||
const formData: any = {};
|
||||
const data = await response.formData();
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const pair of data.entries()) {
|
||||
const [key, value] = pair;
|
||||
formData[key] = value;
|
||||
}
|
||||
return { formData };
|
||||
}
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const testIfIncluded = (value?: string, tests?: Array<string | RegExp>): boolean => {
|
||||
if (!tests) {
|
||||
return false;
|
||||
}
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < tests.length; i += 1) {
|
||||
const test = tests[i];
|
||||
if (typeof test === 'string' && value === test) {
|
||||
return true;
|
||||
}
|
||||
if ((test as RegExp).test && (test as RegExp).test(value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// default return is false, because nothing above matched
|
||||
return false;
|
||||
};
|
||||
|
||||
export const handleResponseMimeTypes = async (
|
||||
response: Response,
|
||||
config?: ResponseBodyConfigMap,
|
||||
): Promise<ResponseBody> => {
|
||||
// combine the user supplied config with the default
|
||||
const finalConfig: ResponseBodyConfigMap = { ...ResponseBodyConfigMapDefaults, ...config };
|
||||
|
||||
const contentType = getContentType(response);
|
||||
|
||||
// check if the headers say what the content type are, otherwise return the bytes of the response as a blob
|
||||
if (!contentType) {
|
||||
// no content type, or body, so the response is only the status, e.g. GET
|
||||
if (!response.body) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// handle fallback method
|
||||
return doHandleResponseMethod(response, config?.fallback || 'blob');
|
||||
}
|
||||
|
||||
if (testIfIncluded(contentType, finalConfig.uint8array)) {
|
||||
return doHandleResponseMethod(response, 'uint8array');
|
||||
}
|
||||
if (testIfIncluded(contentType, finalConfig.json)) {
|
||||
return doHandleResponseMethod(response, 'json');
|
||||
}
|
||||
if (testIfIncluded(contentType, finalConfig.text)) {
|
||||
return doHandleResponseMethod(response, 'text');
|
||||
}
|
||||
if (testIfIncluded(contentType, finalConfig.formData)) {
|
||||
return doHandleResponseMethod(response, 'formData');
|
||||
}
|
||||
if (testIfIncluded(contentType, finalConfig.blob)) {
|
||||
return doHandleResponseMethod(response, 'blob');
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { loadWasm } from './wasm-loading';
|
||||
import { run } from './main';
|
||||
|
||||
async function main() {
|
||||
await loadWasm();
|
||||
await run();
|
||||
}
|
||||
|
||||
main().catch((e: any) => console.error('Unhandled exception in mixFetch worker', e));
|
||||
@@ -1,69 +0,0 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
import { setupMixFetch, disconnectMixFetch } from '@nymproject/mix-fetch-wasm';
|
||||
import * as Comlink from 'comlink';
|
||||
import type { IMixFetchWebWorker, LoadedEvent } from '../types';
|
||||
import { EventKinds, ResponseBodyConfigMap, ResponseBodyConfigMapDefaults } from '../types';
|
||||
import { handleResponseMimeTypes } from './handle-response-mime-types';
|
||||
|
||||
/**
|
||||
* Helper method to send typed messages.
|
||||
* @param event The strongly typed message to send back to the calling thread.
|
||||
*/
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
const postMessageWithType = <E>(event: E) => self.postMessage(event);
|
||||
export async function run() {
|
||||
const { mixFetch } = self as any;
|
||||
let responseBodyConfigMap: ResponseBodyConfigMap = ResponseBodyConfigMapDefaults;
|
||||
|
||||
const mixFetchWebWorker: IMixFetchWebWorker = {
|
||||
mixFetch: async (url, args) => {
|
||||
console.log('[Worker] --- mixFetch ---', { url, args });
|
||||
|
||||
const response: Response = await mixFetch(url, args);
|
||||
|
||||
console.log('[Worker]', { response, json: JSON.stringify(response, null, 2) });
|
||||
|
||||
const bodyResponse = await handleResponseMimeTypes(response, responseBodyConfigMap);
|
||||
console.log('[Worker]', { bodyResponse });
|
||||
|
||||
const headers: any = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
const output = {
|
||||
body: bodyResponse,
|
||||
url: response.url,
|
||||
headers,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
type: response.type,
|
||||
ok: response.ok,
|
||||
redirected: response.redirected,
|
||||
};
|
||||
|
||||
console.log('[Worker]', { output });
|
||||
|
||||
return output;
|
||||
},
|
||||
setupMixFetch: async (opts) => {
|
||||
console.log('[Worker] --- setupMixFetch ---', { opts });
|
||||
if (opts?.responseBodyConfigMap) {
|
||||
responseBodyConfigMap = opts.responseBodyConfigMap;
|
||||
}
|
||||
await setupMixFetch(opts || {});
|
||||
},
|
||||
disconnectMixFetch: async () => {
|
||||
console.log('[Worker] --- disconnectMixFetch ---');
|
||||
|
||||
await disconnectMixFetch();
|
||||
},
|
||||
};
|
||||
|
||||
// start comlink listening for messages and handle them above
|
||||
Comlink.expose(mixFetchWebWorker);
|
||||
|
||||
// notify any listeners that the web worker has loaded - HOWEVER, mixFetch hasn't been setup and the client started
|
||||
// call `setupMixFetch` from the main thread to start the Nym client
|
||||
postMessageWithType<LoadedEvent>({ kind: EventKinds.Loaded, args: { loaded: true } });
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention,no-restricted-globals */
|
||||
/// <reference types="@nymproject/mix-fetch-wasm" />
|
||||
|
||||
// Copyright 2020-2023 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//
|
||||
// Rollup will replace wasmBytes with a function that loads the WASM bundle from a base64 string embedded in the output.
|
||||
//
|
||||
// Doing it this way, saves having to support a large variety of bundlers and their quirks.
|
||||
//
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import getMixFetchWasmBytes from '@nymproject/mix-fetch-wasm/mix_fetch_wasm_bg.wasm';
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import getGoConnectionWasmBytes from '@nymproject/mix-fetch-wasm/go_conn.wasm';
|
||||
|
||||
// wasm_bindgen creates a global variable (with the exports attached) that is in scope after `importScripts`
|
||||
import init, {
|
||||
set_panic_hook,
|
||||
send_client_data,
|
||||
start_new_mixnet_connection,
|
||||
mix_fetch_initialised,
|
||||
finish_mixnet_connection,
|
||||
} from '@nymproject/mix-fetch-wasm';
|
||||
|
||||
// see `typings/wasm_exec.d.ts` for the defintion of the `class Go` in global scope
|
||||
import '@nymproject/mix-fetch-wasm/wasm_exec';
|
||||
|
||||
async function loadGoWasm() {
|
||||
// rollup will provide a function to get the Go connection WASM bytes here
|
||||
const bytes = await getGoConnectionWasmBytes();
|
||||
|
||||
// @ts-ignore
|
||||
const go = new Go(); // Defined in wasm_exec.js
|
||||
|
||||
// the WebAssembly runtime will parse the bytes and then start the Go runtime
|
||||
const wasmObj = await WebAssembly.instantiate(bytes, go.importObject);
|
||||
go.run(wasmObj);
|
||||
}
|
||||
|
||||
function setupRsGoBridge() {
|
||||
const rsGoBridge = {
|
||||
send_client_data,
|
||||
start_new_mixnet_connection,
|
||||
mix_fetch_initialised,
|
||||
finish_mixnet_connection,
|
||||
};
|
||||
|
||||
// (note: reason for intermediate `__rs_go_bridge__` object is to decrease global scope bloat
|
||||
// and to discourage users from trying to call those methods directly)
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
(self as any).__rs_go_bridge__ = rsGoBridge;
|
||||
}
|
||||
|
||||
export async function loadWasm() {
|
||||
// rollup with provide a function to get the mixFetch WASM bytes
|
||||
const bytes = await getMixFetchWasmBytes();
|
||||
|
||||
// load rust WASM package
|
||||
await init(bytes);
|
||||
console.log('Loaded RUST WASM');
|
||||
|
||||
// load go WASM package
|
||||
await loadGoWasm();
|
||||
console.log('Loaded GO WASM');
|
||||
|
||||
// sets up better stack traces in case of in-rust panics
|
||||
set_panic_hook();
|
||||
|
||||
setupRsGoBridge();
|
||||
|
||||
// goWasmSetLogging('trace');
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user