Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24f64c9689 | |||
| 08dc353e82 | |||
| 495f020730 | |||
| c7780d2d34 | |||
| 4ad00dba3d |
@@ -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
+21
-32
@@ -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"
|
||||
@@ -11571,7 +11545,7 @@ dependencies = [
|
||||
"nym-sdk",
|
||||
"reqwest 0.13.1",
|
||||
"rustls 0.23.37",
|
||||
"smoltcp",
|
||||
"smoltcp 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.2",
|
||||
@@ -11583,7 +11557,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smolmix-wasm"
|
||||
version = "1.21.0"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-tungstenite",
|
||||
"bytes",
|
||||
@@ -11609,7 +11583,7 @@ dependencies = [
|
||||
"semver 1.0.27",
|
||||
"serde",
|
||||
"serde-wasm-bindgen 0.6.5",
|
||||
"smoltcp",
|
||||
"smoltcp 0.12.0 (git+https://github.com/nymtech/smoltcp?rev=62ac5b8b3287d4773694f19a3b55e4c004354a0b)",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tsify",
|
||||
@@ -11636,6 +11610,21 @@ dependencies = [
|
||||
"managed",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smoltcp"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/nymtech/smoltcp?rev=62ac5b8b3287d4773694f19a3b55e4c004354a0b#62ac5b8b3287d4773694f19a3b55e4c004354a0b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"byteorder",
|
||||
"cfg-if",
|
||||
"defmt 0.3.100",
|
||||
"heapless",
|
||||
"libc",
|
||||
"log",
|
||||
"managed",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "snafu"
|
||||
version = "0.7.5"
|
||||
@@ -12642,7 +12631,7 @@ dependencies = [
|
||||
"futures",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"smoltcp",
|
||||
"smoltcp 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
@@ -15000,9 +14989,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.2"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -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,21 +1,4 @@
|
||||
---
|
||||
ansible_ssh_private_key_file: ~/.ssh/<SSH_KEY>
|
||||
|
||||
cli_url: "https://github.com/nymtech/nym/releases/download/nym-binaries-{{ nym_version }}/nym-cli"
|
||||
tunnel_manager_url: "https://github.com/nymtech/nym/raw/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh"
|
||||
quic_bridge_deployment_url: "https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/quic_bridge_deployment.sh"
|
||||
|
||||
###############################################################################
|
||||
## GLOBAL VARS
|
||||
## These values will be used globally unless overwritten per node in inventory/all
|
||||
###############################################################################
|
||||
|
||||
ansible_user: root # used for ssh, like `ssh root@nym-exit.ch-1.mynodes.net`
|
||||
email: "<EMAIL>" # used in certbot, description.toml and landing page
|
||||
website: "<WEBSITE>" # it is used in the description.toml
|
||||
description: "<NODE_PUBLIC_DESCRIPTION>" # or define per node in inventory/all
|
||||
# operator_name: "<OPERATOR_NAME>" # used in landing page if provided
|
||||
|
||||
###############################################################################
|
||||
## GLOBAL VARS
|
||||
## These values will be used globally unless overwritten per node in inventory/all
|
||||
@@ -23,16 +6,41 @@ description: "<NODE_PUBLIC_DESCRIPTION>" # or define per node in inventory/all
|
||||
## Per node changes in inventory/all will overwrite these global vars
|
||||
###############################################################################
|
||||
|
||||
# moniker: "<MONIKER>" # if not setup here not in inventory/all it get's derived from the hostname
|
||||
# mode: <MODE> # entry-gateway/exit-gateway/mixnode
|
||||
# wireguard_enabled: <WIREGUARD_ENABLED> # true/false
|
||||
hostname: "" # this is a fallback, keep it and setup hostname per node in inventory/all
|
||||
## MANDATORY - uncomment & define
|
||||
|
||||
## --SSH--
|
||||
#ansible_user: root # used for ssh, like `ssh root@nym-exit.ch-1.mynodes.net`
|
||||
# ansible_ssh_private_key_file: ~/.ssh/<SSH_KEY>
|
||||
|
||||
## --Operator info--
|
||||
# email: "<EMAIL>" # used in certbot, description.toml and landing page
|
||||
# website: "<WEBSITE>" # it is used in the description.toml
|
||||
# description: "<NODE_PUBLIC_DESCRIPTION>" # or define per node in inventory/all
|
||||
# moniker: "<MONIKER>"
|
||||
|
||||
## --Node defaults (can override per node in inventory/all)--
|
||||
# accept_operator_terms: true # controls --accept-operator-terms-and-conditions, read: https://nym.com/docs/operators/nodes/nym-node/setup#terms--conditions
|
||||
# mode: exit-gateway # entry-gateway/exit-gateway/mixnode
|
||||
# wireguard_enabled: true # true/false
|
||||
hostname: "" # keep this fallback, keep it and setup hostname per node in inventory/all
|
||||
|
||||
## OPTIONAL - uncomment & define
|
||||
|
||||
# operator_name: "<OPERATOR_NAME>" # used in landing page if provided
|
||||
# nym_version: "nym-binaries-v2026.7-tola" # to use particular version instead of Latest, provide in such form:
|
||||
|
||||
## alternative SSH key var setting, instead of a hardcoded path
|
||||
## useful if the playbook is shared in a repo by more admins with each having own local key
|
||||
# ansible_ssh_private_key_file: "{{ lookup('env', '<YOUR_ANSIBLE SSH_KEY_ENV_VAR>') }}"
|
||||
|
||||
###############################################################################
|
||||
## GLOBAL PACKAGES
|
||||
## GLOBAL PACKAGES & URLs
|
||||
## These will be installed during deployment
|
||||
###############################################################################
|
||||
|
||||
nym_cli_url: "https://github.com/nymtech/nym/releases/download/{{ nym_version }}/nym-cli"
|
||||
tunnel_manager_url: "https://github.com/nymtech/nym/raw/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh"
|
||||
quic_bridge_deployment_url: "https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/quic_bridge_deployment.sh"
|
||||
|
||||
packages:
|
||||
- tmux
|
||||
@@ -50,24 +58,6 @@ packages:
|
||||
- ufw
|
||||
|
||||
|
||||
###############################################################################
|
||||
## OPTIONAL OVERRIDES
|
||||
## All values below already have defaults in the playbook/roles
|
||||
## Uncomment only if you want to override them
|
||||
###############################################################################
|
||||
|
||||
###############################################################################
|
||||
## SYSTEM MAINTENANCE PLAYBOOK KNOBS
|
||||
###############################################################################
|
||||
|
||||
# To use particular version instead of Latest, provide in such form:
|
||||
# nym_version: "nym-binaries-v2026.7-tola"
|
||||
|
||||
## NOTE:
|
||||
## if you want to pin Nym to a specific version instead of using the
|
||||
## latest release from GitHub in /tasks/main.yml then
|
||||
## uncomment the line above and set the tag
|
||||
|
||||
###############################################################################
|
||||
## SYSTEM MAINTENANCE PLAYBOOK KNOBS
|
||||
###############################################################################
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
[nym_nodes]
|
||||
# READ CONFIGURATION GUIDE:
|
||||
# https://nym.com//docs/operators/orchestration/ansible#configuration
|
||||
## READ CONFIGURATION GUIDE:
|
||||
## https://nym.com/docs/operators/orchestration/ansible#configuration
|
||||
|
||||
# VARIABLES INFO
|
||||
# required vars to set values per node:
|
||||
# `ansible_host`, `hostname`, `location`
|
||||
##############
|
||||
## TEMPLATE ##
|
||||
##############
|
||||
## uncomment and exchange the <VARIABLES> with your real values for each node without the <> brackets
|
||||
|
||||
# global vars can be set in the group_vars/all.yml, for example:
|
||||
# `email`, `ansible_user`, `moniker`, `description`, `mode`, `wireguard_enabled`
|
||||
# othersise they must be set per node!
|
||||
|
||||
############
|
||||
# TEMPLATE #
|
||||
############
|
||||
# node1 ansible_host=<YOUR_SERVER_IP> ansible_user=<USER> hostname=<HOSTNAME> location=<LOCATION> email=<EMAIL> mode=<MODE> wireguard_enabled=<true/false> moniker=<MONIKER> description=<DESCRIPTION>
|
||||
|
||||
# remove all comments and exchange the <VARIABLES> with your real values for each node
|
||||
# without <> brackets
|
||||
####################
|
||||
## VARIABLES INFO ##
|
||||
####################
|
||||
|
||||
# PRIORITY ORDER
|
||||
# anything setup globaly can be overwritten in this file per node
|
||||
# if provided here, it takes priority over the global setting
|
||||
## --REQUIRED VARS--
|
||||
## required per node:
|
||||
## ansible_host, hostname, location
|
||||
|
||||
# EXAMPLES
|
||||
# exit + wireguard gateway:
|
||||
## --OPTIONAL VARS--
|
||||
## can be set in the group_vars/all.yml or per node here:
|
||||
## email, ansible_user, moniker, description, mode, wireguard_enabled
|
||||
|
||||
## --PRIORITY ORDER--
|
||||
## anything setup globaly can be overwritten in this file per node
|
||||
## if provided here, it takes priority over the global setting
|
||||
|
||||
## --EXAMPLES--
|
||||
## exit + wireguard gateway:
|
||||
# node2 ansible_host=11.12.13.14 hostname=nym-exit.ch-1.mydomain.net mode=exit-gateway location=CH wireguard_enabled=true
|
||||
|
||||
# entry gateway, no wireguard:
|
||||
## entry gateway, no wireguard:
|
||||
# node3 ansible_host=12.13.14.15 hostname=nym-entry.ch-2.mydomain.net mode=entry-gateway location=CH wireguard_enabled=false
|
||||
|
||||
# NOTE:
|
||||
# all examples above don't have defined user, email nor description as we use the definition from group_vars/main.yml without an attempt of overwriting it
|
||||
# all examples above don't have moniker defined as there is a function in /templates/description.toml.j2 deriving it from the hostname
|
||||
## mixnode (comment out tunnel+quic roles in deploy.yml for these)
|
||||
# mix-de-1 ansible_host=13.14.15.16 hostname=nym-mix.de-1.example.net location=DE mode=mixnode wireguard_enabled=false
|
||||
|
||||
## NOTE:
|
||||
## all examples above don't have defined user, email nor description as we use global vars from playbooks/group_vars/all.yml
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
loop:
|
||||
- "/etc/nginx/sites-enabled/{{ hostname }}-ssl"
|
||||
- "/etc/nginx/sites-enabled/nym-wss-config"
|
||||
when: not le_cert.stat.exists
|
||||
notify: Restart nginx
|
||||
|
||||
- name: Ensure nginx is enabled and running (needed for ACME http-01)
|
||||
@@ -111,18 +110,13 @@
|
||||
- name: Obtain/renew certificate
|
||||
command:
|
||||
cmd: >-
|
||||
{% if le_cert.stat.exists %}
|
||||
certbot certonly --webroot
|
||||
-w /var/www/{{ hostname }}
|
||||
certbot certonly --nginx
|
||||
--non-interactive --agree-tos --keep-until-expiring
|
||||
-m {{ email }} -d {{ hostname }}
|
||||
{% else %}
|
||||
certbot --nginx
|
||||
--non-interactive --agree-tos --redirect
|
||||
-m {{ email }} -d {{ hostname }}
|
||||
{% endif %}
|
||||
register: certbot_result
|
||||
failed_when: false
|
||||
failed_when: false
|
||||
|
||||
|
||||
|
||||
# re-check cert after certbot attempt
|
||||
- name: Re-check whether certificate exists after certbot
|
||||
@@ -170,4 +164,4 @@
|
||||
changed_when: false
|
||||
|
||||
- name: Flush handlers (apply restart after successful tests)
|
||||
meta: flush_handlers
|
||||
meta: flush_handlers
|
||||
@@ -10,7 +10,7 @@ mixnet_bind_address: "0.0.0.0:1789" # maps to --mixnet-bind-address
|
||||
landing_page_assets_base_dir: "/var/www"
|
||||
|
||||
# Flag toggles
|
||||
# accept_operator_terms: true # controls --accept-operator-terms-and-conditions
|
||||
accept_operator_terms: false # override in group_vars or inventory
|
||||
nym_write_flag: true # controls -w
|
||||
nym_init_only_flag: true # controls --init-only
|
||||
wss_port: 9001 # controlls --announce-wss-port
|
||||
@@ -18,7 +18,7 @@ wss_port: 9001 # controlls --announce-wss-port
|
||||
# Optional: extra flags if you want to append more later
|
||||
nym_extra_flags: ""
|
||||
|
||||
# CLI URL (nym_version can be set elsewhere / via GitHub API)
|
||||
# CLI URL
|
||||
nym_cli_url: "https://github.com/nymtech/nym/releases/download/{{ nym_version }}/nym-cli"
|
||||
|
||||
# UFW
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"mixmining_reserve": {
|
||||
"denom": "unym",
|
||||
"amount": "166613455567357"
|
||||
"amount": "164623226345363"
|
||||
},
|
||||
"vesting_tokens": {
|
||||
"denom": "unym",
|
||||
@@ -13,6 +13,6 @@
|
||||
},
|
||||
"circulating_supply": {
|
||||
"denom": "unym",
|
||||
"amount": "833386544432643"
|
||||
"amount": "835376773654637"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"nodes": 682,
|
||||
"locations": 78,
|
||||
"mixnodes": 242,
|
||||
"exit_gateways": 432
|
||||
"nodes": 677,
|
||||
"locations": 77,
|
||||
"mixnodes": 240,
|
||||
"exit_gateways": 429
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
833_386_544
|
||||
835_376_773
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4_628
|
||||
4_572
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
254_977
|
||||
255_586
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
61_194_673
|
||||
61_340_814
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
61_194_672
|
||||
61_340_813
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
| **Item** | **Description** | **Amount in NYM** |
|
||||
|:-------------------|:------------------------------------------------------|--------------------:|
|
||||
| Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 |
|
||||
| Mixmining Reserve | Tokens releasing for operators rewards | 166_613_455 |
|
||||
| Mixmining Reserve | Tokens releasing for operators rewards | 164_623_226 |
|
||||
| Vesting Tokens | Tokens locked outside of circulation for future claim | 0 |
|
||||
| Circulating Supply | Amount of unlocked tokens | 833_386_544 |
|
||||
| Stake Saturation | Optimal size of node self-bond + delegation | 254_977 |
|
||||
| Circulating Supply | Amount of unlocked tokens | 835_376_773 |
|
||||
| Stake Saturation | Optimal size of node self-bond + delegation | 255_586 |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"interval": {
|
||||
"reward_pool": "166613455567357.391753168447944888",
|
||||
"staking_supply": "61194672938727.325886595653401629",
|
||||
"reward_pool": "164623226345363.285226429762152046",
|
||||
"staking_supply": "61340813321050.793375219026221616",
|
||||
"staking_supply_scale_factor": "0.07342892",
|
||||
"epoch_reward_budget": "4628151543.537705326476901331",
|
||||
"stake_saturation_point": "254977803911.363857860815222506",
|
||||
"epoch_reward_budget": "4572867398.482313478511937837",
|
||||
"stake_saturation_point": "255586722171.04497239674594259",
|
||||
"sybil_resistance": "0.3",
|
||||
"active_set_work_factor": "10",
|
||||
"interval_pool_emission": "0.02"
|
||||
|
||||
@@ -1 +1 @@
|
||||
Wednesday, May 27th 2026, 11:42:58 UTC
|
||||
Thursday, June 4th 2026, 11:40:35 UTC
|
||||
|
||||
@@ -3,17 +3,23 @@
|
||||
"changelog": "Changelog",
|
||||
"release-cycle": "Release Cycle",
|
||||
"variables": "Variables & Parameters",
|
||||
"sandbox": "Sandbox Testnet",
|
||||
"binaries": "Binaries",
|
||||
"---1": {
|
||||
"type": "separator"
|
||||
},
|
||||
"nodes": "Nodes & Validators Guides",
|
||||
"binaries": "Binaries",
|
||||
"orchestration" : "Orchestration",
|
||||
"performance-and-testing": "Performance Measurement",
|
||||
"tools": "Tools",
|
||||
"troubleshooting": "Troubleshooting",
|
||||
"---2": {
|
||||
"type": "separator"
|
||||
},
|
||||
"sandbox": "Sandbox Testnet",
|
||||
"tokenomics": "Tokenomics",
|
||||
"faq": "FAQ",
|
||||
"community-counsel": "Community Counsel",
|
||||
"---": {
|
||||
"---3": {
|
||||
"type": "separator"
|
||||
},
|
||||
"archive": "Archive",
|
||||
|
||||
@@ -291,17 +291,26 @@ KVM network is gone.
|
||||
|
||||
##### 3. Setup KVM public bridge for new VMs
|
||||
|
||||
To create a KVM network bridge on Ubuntu, edit a config file located in `/etc/netplan/` either called `00-installer.yaml` or `00-installer-config.yaml` and add the bridge details.
|
||||
To create a KVM network bridge on Ubuntu, edit a config file located in `/etc/netplan/` often called `50-cloud-init.yaml` or `00-installer.yaml` or `00-installer-config.yaml` and add the bridge details.
|
||||
|
||||
- Before you edit the file, make a backup to stay on the save side:
|
||||
- Before you edit the file, make a backup to stay on the save side, the file should be named something like this:
|
||||
```bash
|
||||
cp /etc/netplan/00-installer-config.yaml /etc/netplan/00-installer-config.yaml.bak
|
||||
cp /etc/netplan/50-cloud-init.yaml /etc/netplan/50-cloud-init.yaml.bak
|
||||
# or
|
||||
cp /etc/netplan/00-installer.yaml /etc/netplan/00-installer.yaml.bak
|
||||
# or
|
||||
cp /etc/netplan/00-installer-config.yaml /etc/netplan/00-installer-config.yaml.bak
|
||||
```
|
||||
|
||||
- Open `00-installer-config.yaml` or `00-installer.yaml.`config in a text editor:
|
||||
- If none of these files existed, simply check the name of your config by this command and apply the same backup logic:
|
||||
```sh
|
||||
ls /etc/netplan/
|
||||
```
|
||||
|
||||
- Open the original config in a text editor:
|
||||
```bash
|
||||
nano /etc/netplan/50-cloud-init.yaml
|
||||
# or
|
||||
nano /etc/netplan/00-installer.yaml
|
||||
# or
|
||||
nano /etc/netplan/00-installer-config.yaml
|
||||
@@ -380,12 +389,13 @@ netplan --debug apply
|
||||
```bash
|
||||
apt install yamllint -y
|
||||
|
||||
yamllint /etc/netplan/50-cloud-init.yaml
|
||||
# or
|
||||
yamllint /etc/netplan/00-installer.yaml
|
||||
# or
|
||||
yamllint /etc/netplan/00-installer-config.yaml
|
||||
|
||||
|
||||
```
|
||||
|
||||
- Apply correct permissions:
|
||||
```bash
|
||||
chmod 600 /etc/netplan/00-installer.yaml
|
||||
@@ -410,9 +420,12 @@ systemctl enable --now systemd-networkd
|
||||
|
||||
- If things went wrong, you can always revert from the backed up file:
|
||||
```bash
|
||||
cp /etc/netplan/50-cloud-init.yaml.bak /etc/netplan/50-cloud-init.yaml
|
||||
# or
|
||||
cp /etc/netplan/00-installer-config.yaml.bak /etc/netplan/00-installer-config.yaml
|
||||
# or
|
||||
cp /etc/netplan/00-installer.yaml.bak /etc/netplan/00-installer.yaml
|
||||
|
||||
# and
|
||||
netplan apply
|
||||
```
|
||||
@@ -535,19 +548,50 @@ Using the scripts is a two-step process. First, initialisation part is done from
|
||||
<Steps>
|
||||
##### 1. Initialise VM from the host machine
|
||||
- Log in to your host as `root`
|
||||
- Run this block and follow the prompts carefully:
|
||||
- Get the script:
|
||||
```bash
|
||||
wget "https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/kvm-setup/initialise-vm.sh"
|
||||
chmod +x ./initialise-vm.sh
|
||||
./initialise-vm.sh
|
||||
```
|
||||
|
||||
- Run this block and follow the prompts carefully or provide with arguments:
|
||||
```sh
|
||||
# interactive CLI
|
||||
./initialise-vm.sh
|
||||
|
||||
# arguments - see --help menu
|
||||
./initialise-vm.sh --help
|
||||
|
||||
# example
|
||||
# ./initialise-vm.sh --name ubuntu1 --password topsecretrootpassword --cpus 4 --ram 8192 --size 60
|
||||
```
|
||||
|
||||
|
||||
##### 2. Configure VM from within
|
||||
- After logging into your VM run this block and follow the prompts carefully:
|
||||
- After logging into your VM, using `login: root` and `password: <YOUR_PASSWORD>`, get second script to configure the VM from within:
|
||||
```bash
|
||||
wget "https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/kvm-setup/configure-vm.sh"
|
||||
chmod +x ./configure-vm.sh
|
||||
```
|
||||
|
||||
- Likely your connection won't work and therefore you need to create the script manually, open a text editor:
|
||||
```sh
|
||||
nano configure-vm.sh
|
||||
```
|
||||
|
||||
- Paste there [this content](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/kvm-setup/configure-vm.sh)
|
||||
- Save and exit
|
||||
- Run this block and follow the prompts carefully, or provide with arguments:
|
||||
|
||||
```sh
|
||||
# interactive CLI
|
||||
./configure-vm.sh
|
||||
|
||||
# arguments - see --help menu
|
||||
./configure-vm.sh --help
|
||||
|
||||
# example
|
||||
# ./configure-vm.sh --interface enp1s0 --ipv4 192.168.1.100 --gateway4 192.168.1.1 --ipv6 2001:db8::1 --gateway6 2001:db8::fffe
|
||||
```
|
||||
</ Steps>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Tabs } from 'nextra/components';
|
||||
import { Steps } from 'nextra/components';
|
||||
import { RunTabs } from 'components/operators/nodes/node-run-command-tabs';
|
||||
import { VarInfo } from 'components/variable-info.tsx';
|
||||
import { AccordionTemplate } from 'components/accordion-template.tsx';
|
||||
|
||||
# Orchestrating Nym Nodes with Ansible
|
||||
|
||||
@@ -95,14 +96,13 @@ Before starting Ansible, ensure that your `A` and `AAAA` records are pointed to
|
||||
<Steps>
|
||||
###### 1. Configure global variables:
|
||||
- Open `playbooks/group_vars/all.yml`
|
||||
- Setup any variables which you want to have propagated on all your nodes globally
|
||||
- Define all values under the section labeled as `## MANDATORY - uncomment & define` - these values will be propagated on all your nodes globally
|
||||
- Optionally define values of your choice under the section `## OPTIONAL - uncomment & define`
|
||||
- Note that in the next step we will be setting up a node inventory, where each of the variable can be configured per node, taking priority over the global ones.
|
||||
- Setup a correct path for your SSH kety to `ansible_ssh_private_key_file:`
|
||||
- Use these variables or comment them out with `#`:
|
||||
- `ansible_user`
|
||||
- `email`
|
||||
- `website`
|
||||
- `description`
|
||||
- Setup a correct path for your SSH kety to `ansible_ssh_private_key_file:`, alternatively export your SSH key as an env var and use this:
|
||||
```sh
|
||||
ansible_ssh_private_key_file: "{{ lookup('env', '<YOUR_ANSIBLE SSH_KEY_ENV_VAR>') }}"
|
||||
```
|
||||
- Keep `hostname=""` as a fallback for nodes without a hostname
|
||||
|
||||
###### 2. Create node inventory:
|
||||
@@ -111,11 +111,11 @@ Before starting Ansible, ensure that your `A` and `AAAA` records are pointed to
|
||||
```sh
|
||||
node1 ansible_host=<YOUR_SERVER_IP> ansible_user=<USER> hostname=<HOSTNAME> location=<LOCATION> email=<EMAIL> mode=<MODE> wireguard_enabled=<true/false> moniker=<MONIKER> description=<DESCRIPTION>
|
||||
```
|
||||
- These are mandatory values specific for each node - must be defined in the inventory:
|
||||
- Mandatory values specific for each node - must be defined in the inventory:
|
||||
- `ansible_host`: IPv4 host address
|
||||
- `hostname`: node domain, otherwise fallbacks to `""` for nodes without domain
|
||||
- `location`: node server location
|
||||
- These are mandatory values which can be setup per node or in `group_vars/all` globally:
|
||||
- Mandatory values which can be setup per node or in `group_vars/all` globally:
|
||||
- `ansible_user`
|
||||
- `email`
|
||||
- `website`
|
||||
@@ -123,6 +123,7 @@ node1 ansible_host=<YOUR_SERVER_IP> ansible_user=<USER> hostname=<HOSTNAME> loca
|
||||
- `description`
|
||||
- `mode`
|
||||
- `wireguard_enabled`
|
||||
- `accept_operator_terms` - **Make sure to read [Nym Operators Terms & Conditions](/operators/nodes/nym-node/setup#terms--conditions) first!**
|
||||
|
||||
###### 3. Test your setup
|
||||
Run this command to check if everything is configured correctly in your inventory:
|
||||
@@ -131,17 +132,12 @@ cd playbooks
|
||||
ansible-inventory --graph
|
||||
```
|
||||
|
||||
###### 4. Configure `nym-node run` command arguments
|
||||
###### 4. Optional: remove some of `nym-node run` command arguments
|
||||
These variables are read by the main task for `nym-node` installation: `roles/nym/tasks/config.yaml`
|
||||
|
||||
Open `roles/nym/defaults/main.yml` and have a look on the variables used:
|
||||
|
||||
- If you agree with [Terms and conditions](/operators/nodes/nym-node/setup#terms--conditions) uncomment the line: `accept_operator_terms: true` without which your node can never take part in Nym Network.
|
||||
- The rest is up to your configuration but generally these flags workflows
|
||||
|
||||
These variables are read by the main task for `nym-node` installation: `roles/nym/tasks/config.yaml`
|
||||
- Open that yaml and have a look on the flags
|
||||
- In case of not needing some of the, delete them (ie when running `--mode mixnode` you can delete everything from `--hostname` to `--announce-wss-port`)
|
||||
|
||||
###### 5. Configure `deploy.yml` playbook
|
||||
###### 5. Optional: Configure `deploy.yml` playbook
|
||||
Open `playbooks/deploy.yml` and comment out `tunnel` and `quic` roles in case of running your playbook for nodes in a mode `mixnode`.
|
||||
|
||||
Save all the files and test with:
|
||||
@@ -159,11 +155,13 @@ This chapter describes fundamental commands for using Ansible playbooks in relat
|
||||
|
||||
### Logic
|
||||
|
||||
<AccordionTemplate name="See Ansible logic description">
|
||||
|
||||
The main logic of the playbook flow when running with a basic command and playbook like this:
|
||||
```sh
|
||||
ansible-playbook <PLAYBOOK>.yml
|
||||
```
|
||||
<Steps>
|
||||
<Steps>
|
||||
###### 1. Read inventory
|
||||
Ansible parses `inventory/all` and performs the playbook on all entries in it, unless specified otherwise
|
||||
|
||||
@@ -175,7 +173,8 @@ Ansible parses `group_vars/all.yml` and asigns global variables to all inventory
|
||||
###### 3. Follow roles in the playbook
|
||||
Ansible reads the roles defined in `<PLAYBOOK>.yml` passed with the command and executes the tasks defined under each role
|
||||
|
||||
</ Steps>
|
||||
</ Steps>
|
||||
</AccordionTemplate>
|
||||
|
||||
### Usage
|
||||
|
||||
@@ -237,6 +236,17 @@ cd playbooks
|
||||
ansible-playbook system-maintenance.yml
|
||||
```
|
||||
|
||||
###### 5. Mitigate kernel CVE
|
||||
|
||||
This playbook is to mitigate some of the Kernel issues found in April and May 2026.
|
||||
|
||||
This playbook will run roles on all the inventory entries in parallel by default.
|
||||
|
||||
```sh
|
||||
cd playbooks
|
||||
ansible-playbook mitigate_kernel_CVE.yml
|
||||
```
|
||||
|
||||
</ Steps>
|
||||
|
||||
|
||||
@@ -248,7 +258,7 @@ ansible-playbook system-maintenance.yml
|
||||
|
||||
<Steps>
|
||||
|
||||
###### One node at a time
|
||||
###### Node limit
|
||||
To test new configuration, it's advised to try it on one server at first. Of course you can comment out any entries in the inventory, but there are easier ways to do it.
|
||||
|
||||
- Provide flag `-l` followed by inventory entry and Ansible will change state only of that entry:
|
||||
@@ -261,11 +271,14 @@ ansible-playbook upgrade.yml -l node1
|
||||
# point to multiple entries
|
||||
ansible-playbook upgrade.yml -l "node1,node2"
|
||||
|
||||
# use regex
|
||||
# use regex - ie all exit nodes
|
||||
ansible-playbook upgrade.yml -l "*exit*"
|
||||
|
||||
# use group in inventory labeled as [group]
|
||||
ansible-playbook deploy.yml -l new_nodes
|
||||
```
|
||||
|
||||
###### Role limit
|
||||
###### Tag selection
|
||||
|
||||
<Callout>
|
||||
To update your exit policy by using the newest [NTM](https://github.com/nymtech/nym/blob/develop/scripts/nym-node-setup/network-tunnel-manager.sh) via Ansible, just run:
|
||||
|
||||
@@ -537,3 +537,15 @@ html:not(.dark) .landing-card {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Bold pages/operators/nodes sidebar item */
|
||||
html.dark .nextra-sidebar-container a[href="/docs/operators/nodes"] {
|
||||
color: var(--textPrimary) !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
html:not(.dark) .nextra-sidebar-container a[href="/docs/operators/nodes"] {
|
||||
color: #111 !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
+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:
|
||||
|
||||
@@ -1,36 +1,101 @@
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
# detect active network interface
|
||||
INTERFACE=$(ip -o link show | awk -F': ' '{print $2}' | grep -v lo | head -n 1)
|
||||
usage() {
|
||||
local code="${1:-0}"
|
||||
cat <<EOF
|
||||
Usage: $0 [OPTIONS]
|
||||
|
||||
echo "Detected active network interface: $INTERFACE"
|
||||
read -p "Is this correct? (y/n): " CONFIRM
|
||||
if [[ "$CONFIRM" != "y" ]]; then
|
||||
echo "Exiting. Please manually specify the correct network interface."
|
||||
Options:
|
||||
-i, --interface Network interface (optional; auto-detected if omitted)
|
||||
-4, --ipv4 IPv4 address for the VM (optional)
|
||||
-6, --ipv6 IPv6 address for the VM (optional)
|
||||
-g, --gateway4 IPv4 gateway of the host server (optional)
|
||||
-G, --gateway6 IPv6 gateway of the host server (optional)
|
||||
-y, --yes Skip all confirmation prompts (auto-confirm)
|
||||
-h, --help Show this help message
|
||||
|
||||
Example:
|
||||
$0 --ipv4 192.168.1.100 --gateway4 192.168.1.1 --ipv6 2001:db8::1 --gateway6 2001:db8::fffe
|
||||
$0 --ipv4 192.168.1.100 --gateway4 192.168.1.1 --yes
|
||||
EOF
|
||||
exit "$code"
|
||||
}
|
||||
|
||||
# --- parse flags ---
|
||||
INTERFACE=""
|
||||
IPv4_VM=""
|
||||
IPv6_VM=""
|
||||
IPv4_GATEWAY_HOST_SERVER=""
|
||||
IPv6_GATEWAY_HOST_SERVER=""
|
||||
AUTO_YES=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-i|--interface)
|
||||
[[ -n "${2:-}" && "${2:0:1}" != "-" ]] || { echo "Error: --interface requires a value."; exit 1; }
|
||||
INTERFACE="$2"; shift 2 ;;
|
||||
-4|--ipv4)
|
||||
[[ -n "${2:-}" && "${2:0:1}" != "-" ]] || { echo "Error: --ipv4 requires a value."; exit 1; }
|
||||
IPv4_VM="$2"; shift 2 ;;
|
||||
-6|--ipv6)
|
||||
[[ -n "${2:-}" && "${2:0:1}" != "-" ]] || { echo "Error: --ipv6 requires a value."; exit 1; }
|
||||
IPv6_VM="$2"; shift 2 ;;
|
||||
-g|--gateway4)
|
||||
[[ -n "${2:-}" && "${2:0:1}" != "-" ]] || { echo "Error: --gateway4 requires a value."; exit 1; }
|
||||
IPv4_GATEWAY_HOST_SERVER="$2"; shift 2 ;;
|
||||
-G|--gateway6)
|
||||
[[ -n "${2:-}" && "${2:0:1}" != "-" ]] || { echo "Error: --gateway6 requires a value."; exit 1; }
|
||||
IPv6_GATEWAY_HOST_SERVER="$2"; shift 2 ;;
|
||||
-y|--yes) AUTO_YES=true; shift ;;
|
||||
-h|--help) usage ;;
|
||||
*)
|
||||
echo "Error: Unknown option: $1"
|
||||
usage 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# at least one of IPv4 or IPv6 must be provided
|
||||
if [[ -z "$IPv4_VM" && -z "$IPv6_VM" ]]; then
|
||||
echo "Error: At least one of --ipv4 or --ipv6 must be provided."
|
||||
echo "Run '$0 --help' for usage."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# prompt for network settings
|
||||
read -p "Enter IPv4 address for VM (leave blank if not used): " IPv4_VM
|
||||
read -p "Enter IPv6 address for VM (leave blank if not used): " IPv6_VM
|
||||
read -p "Enter IPv4 gateway (host server, leave blank if not used): " IPv4_GATEWAY_HOST_SERVER
|
||||
read -p "Enter IPv6 gateway (host server, leave blank if not used): " IPv6_GATEWAY_HOST_SERVER
|
||||
confirm() {
|
||||
local prompt="$1"
|
||||
if $AUTO_YES; then
|
||||
echo "${prompt} [Y/n] (auto-confirmed)"
|
||||
return 0
|
||||
fi
|
||||
read -p "${prompt} [Y/n]: " REPLY
|
||||
[[ -z "$REPLY" || "$REPLY" == "y" || "$REPLY" == "Y" ]]
|
||||
}
|
||||
|
||||
# resize partition
|
||||
# --- detect or validate interface ---
|
||||
if [[ -z "$INTERFACE" ]]; then
|
||||
INTERFACE=$(ip -o link show | awk -F': ' '{print $2}' | grep -v lo | head -n 1)
|
||||
echo "Detected active network interface: $INTERFACE"
|
||||
if ! confirm "Is this correct?"; then
|
||||
echo "Exiting. Re-run with --interface <name> to specify one manually."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Using network interface: $INTERFACE"
|
||||
fi
|
||||
|
||||
# --- resize partition ---
|
||||
echo "Expanding partition and resizing filesystem..."
|
||||
growpart /dev/vda 1
|
||||
resize2fs /dev/vda1
|
||||
df -h
|
||||
|
||||
# ask before continuing
|
||||
read -p "Continue with network configuration? (y/n): " CONTINUE
|
||||
if [[ "$CONTINUE" != "y" ]]; then
|
||||
if ! confirm "Continue with network configuration?"; then
|
||||
echo "Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# generate Netplan config
|
||||
# --- generate Netplan config ---
|
||||
NETPLAN_CONFIG="/etc/netplan/01-network-config.yaml"
|
||||
echo "Creating Netplan configuration at $NETPLAN_CONFIG..."
|
||||
|
||||
@@ -45,28 +110,19 @@ network:
|
||||
addresses:
|
||||
EOF
|
||||
|
||||
# append IPv4 address if provided
|
||||
if [[ -n "$IPv4_VM" ]]; then
|
||||
echo " - $IPv4_VM/24" >> $NETPLAN_CONFIG
|
||||
fi
|
||||
[[ -n "$IPv4_VM" ]] && echo " - $IPv4_VM/24" >> $NETPLAN_CONFIG
|
||||
[[ -n "$IPv6_VM" ]] && echo " - $IPv6_VM/64" >> $NETPLAN_CONFIG
|
||||
|
||||
# append IPv6 address if provided
|
||||
if [[ -n "$IPv6_VM" ]]; then
|
||||
echo " - $IPv6_VM/64" >> $NETPLAN_CONFIG
|
||||
fi
|
||||
|
||||
echo " routes:" >> $NETPLAN_CONFIG
|
||||
|
||||
# append IPv4 route if provided
|
||||
if [[ -n "$IPv4_GATEWAY_HOST_SERVER" ]]; then
|
||||
echo " - to: default" >> $NETPLAN_CONFIG
|
||||
echo " via: $IPv4_GATEWAY_HOST_SERVER" >> $NETPLAN_CONFIG
|
||||
fi
|
||||
|
||||
# append IPv6 route if provided
|
||||
if [[ -n "$IPv6_GATEWAY_HOST_SERVER" ]]; then
|
||||
echo " - to: default" >> $NETPLAN_CONFIG
|
||||
echo " via: $IPv6_GATEWAY_HOST_SERVER" >> $NETPLAN_CONFIG
|
||||
if [[ -n "$IPv4_GATEWAY_HOST_SERVER" || -n "$IPv6_GATEWAY_HOST_SERVER" ]]; then
|
||||
echo " routes:" >> $NETPLAN_CONFIG
|
||||
if [[ -n "$IPv4_GATEWAY_HOST_SERVER" ]]; then
|
||||
echo " - to: default" >> $NETPLAN_CONFIG
|
||||
echo " via: $IPv4_GATEWAY_HOST_SERVER" >> $NETPLAN_CONFIG
|
||||
fi
|
||||
if [[ -n "$IPv6_GATEWAY_HOST_SERVER" ]]; then
|
||||
echo " - to: default" >> $NETPLAN_CONFIG
|
||||
echo " via: $IPv6_GATEWAY_HOST_SERVER" >> $NETPLAN_CONFIG
|
||||
fi
|
||||
fi
|
||||
|
||||
cat <<EOF >> $NETPLAN_CONFIG
|
||||
@@ -78,58 +134,43 @@ cat <<EOF >> $NETPLAN_CONFIG
|
||||
- 2001:4860:4860::8888 # Google IPv6 DNS
|
||||
EOF
|
||||
|
||||
# secure Netplan config
|
||||
chmod 600 $NETPLAN_CONFIG
|
||||
|
||||
# generate Netplan configuration
|
||||
netplan generate
|
||||
|
||||
# ask before applying Netplan
|
||||
read -p "Apply Netplan changes? (y/n): " CONTINUE
|
||||
if [[ "$CONTINUE" != "y" ]]; then
|
||||
if ! confirm "Apply Netplan changes?"; then
|
||||
echo "Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# apply Netplan and verify settings
|
||||
netplan --debug apply
|
||||
|
||||
# show IP configurations
|
||||
ip -4 a
|
||||
ip -6 a
|
||||
ip -4 r
|
||||
ip -6 r
|
||||
|
||||
# test network connectivity
|
||||
echo "Testing IPv4 connectivity for 10 seconds..."
|
||||
timeout 10 ping -4 google.com
|
||||
|
||||
echo "Testing IPv6 connectivity for 10 seconds..."
|
||||
timeout 10 ping -6 google.com
|
||||
|
||||
# ask before updating system
|
||||
read -p "Proceed with system update and upgrade? (y/n): " CONTINUE
|
||||
if [[ "$CONTINUE" != "y" ]]; then
|
||||
echo "Skipping updates."
|
||||
else
|
||||
if confirm "Proceed with system update and upgrade?"; then
|
||||
apt update && apt upgrade -y
|
||||
else
|
||||
echo "Skipping updates."
|
||||
fi
|
||||
|
||||
# generate SSH host keys without password
|
||||
# --- SSH setup ---
|
||||
echo "Generating SSH host keys..."
|
||||
ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N ""
|
||||
ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key -N ""
|
||||
ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key -N ""
|
||||
ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N ""
|
||||
ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key -N ""
|
||||
ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key -N ""
|
||||
ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""
|
||||
|
||||
# restart SSH service
|
||||
systemctl restart ssh.service
|
||||
|
||||
# ensure ~/.ssh directory exists
|
||||
mkdir -p ~/.ssh
|
||||
|
||||
# Open authorized_keys file for user input
|
||||
echo "# Add your admin SSH keys here, save and exit!" > ~/.ssh/authorized_keys
|
||||
nano ~/.ssh/authorized_keys
|
||||
|
||||
echo "Setup complete! Try to ping and ssh from the outside before killing this console"
|
||||
echo "Setup complete! Try to ping and ssh from the outside before killing this console"
|
||||
@@ -1,6 +1,86 @@
|
||||
#!/bin/bash
|
||||
|
||||
# check if noble-server-cloudimg-amd64.img is in working dir - if not, wget it
|
||||
usage() {
|
||||
local code="${1:-0}"
|
||||
cat <<EOF
|
||||
Usage: $0 [OPTIONS]
|
||||
|
||||
Options:
|
||||
-n, --name VM name (required)
|
||||
-p, --password Root password (required)
|
||||
-c, --cpus Number of vCPUs (required)
|
||||
-r, --ram RAM in MB (required); e.g. 2048, 4096, 8192
|
||||
-s, --size Disk size to add in GB (required); e.g. 50, 100, 200
|
||||
-h, --help Show this help message
|
||||
|
||||
Example:
|
||||
$0 --name myvm --password secret --cpus 4 --ram 8192 --size 100
|
||||
EOF
|
||||
exit "$code"
|
||||
}
|
||||
|
||||
# --- parse flags ---
|
||||
VM_NAME=""
|
||||
PASSWORD=""
|
||||
VCPUS=""
|
||||
RAM=""
|
||||
SIZE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-n|--name)
|
||||
[[ -n "${2:-}" && "${2:0:1}" != "-" ]] || { echo "Error: --name requires a value."; exit 1; }
|
||||
VM_NAME="$2"; shift 2 ;;
|
||||
-p|--password)
|
||||
[[ -n "${2:-}" && "${2:0:1}" != "-" ]] || { echo "Error: --password requires a value."; exit 1; }
|
||||
PASSWORD="$2"; shift 2 ;;
|
||||
-c|--cpus)
|
||||
[[ -n "${2:-}" && "${2:0:1}" != "-" ]] || { echo "Error: --cpus requires a value."; exit 1; }
|
||||
VCPUS="$2"; shift 2 ;;
|
||||
-r|--ram)
|
||||
[[ -n "${2:-}" && "${2:0:1}" != "-" ]] || { echo "Error: --ram requires a value."; exit 1; }
|
||||
RAM="$2"; shift 2 ;;
|
||||
-s|--size)
|
||||
[[ -n "${2:-}" && "${2:0:1}" != "-" ]] || { echo "Error: --size requires a value."; exit 1; }
|
||||
SIZE="$2"; shift 2 ;;
|
||||
-h|--help) usage ;;
|
||||
*)
|
||||
echo "Error: Unknown option: $1"
|
||||
usage 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- validate required flags ---
|
||||
MISSING=()
|
||||
[[ -z "$VM_NAME" ]] && MISSING+=("--name")
|
||||
[[ -z "$PASSWORD" ]] && MISSING+=("--password")
|
||||
[[ -z "$VCPUS" ]] && MISSING+=("--cpus")
|
||||
[[ -z "$RAM" ]] && MISSING+=("--ram")
|
||||
[[ -z "$SIZE" ]] && MISSING+=("--size")
|
||||
|
||||
if [[ ${#MISSING[@]} -gt 0 ]]; then
|
||||
echo "Error: Missing required flags: ${MISSING[*]}"
|
||||
echo "Run '$0 --help' for usage."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! "$VCPUS" =~ ^[0-9]+$ || "$VCPUS" -lt 1 ]]; then
|
||||
echo "Error: --cpus must be a positive integer."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! "$RAM" =~ ^[0-9]+$ || "$RAM" -lt 256 ]]; then
|
||||
echo "Error: --ram must be a positive integer (minimum 256 MB)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! "$SIZE" =~ ^[0-9]+$ || "$SIZE" -lt 1 ]]; then
|
||||
echo "Error: --size must be a positive integer in GB."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- check / download base image ---
|
||||
if [[ ! -f noble-server-cloudimg-amd64.img ]]; then
|
||||
echo "Base image not found. Downloading noble-server-cloudimg-amd64.img..."
|
||||
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
|
||||
@@ -10,99 +90,40 @@ if [[ ! -f noble-server-cloudimg-amd64.img ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# prompt for VM_NAME
|
||||
read -p "Enter VM name: " VM_NAME
|
||||
if [[ -z "$VM_NAME" ]]; then
|
||||
echo "Error: VM_NAME cannot be empty. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# prompt for PASSWORD w silent input
|
||||
read -s -p "Enter password for the VM: " PASSWORD
|
||||
echo
|
||||
if [[ -z "$PASSWORD" ]]; then
|
||||
echo "Error: PASSWORD cannot be empty. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# prompt for number of vCPUs
|
||||
read -p "Enter the number of vCPUs for the VM: " VCPUS
|
||||
if [[ -z "$VCPUS" || ! "$VCPUS" =~ ^[0-9]+$ ]]; then
|
||||
echo "Error: Invalid number of vCPUs. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# prompt for RAM size with suggestions
|
||||
DEFAULT_RAM=4096
|
||||
HALF_RAM=$((DEFAULT_RAM / 2))
|
||||
DOUBLE_RAM=$((DEFAULT_RAM * 2))
|
||||
TRIPLE_RAM=$((DEFAULT_RAM * 3))
|
||||
FOUR_TIMES_RAM=$((DEFAULT_RAM * 4))
|
||||
SIX_TIMES_RAM=$((DEFAULT_RAM * 6))
|
||||
EIGHT_TIMES_RAM=$((DEFAULT_RAM * 8))
|
||||
|
||||
echo "Choose the amount of RAM for the VM:"
|
||||
echo "1) $HALF_RAM MB"
|
||||
echo "2) $DEFAULT_RAM MB (recommended)"
|
||||
echo "3) $DOUBLE_RAM MB"
|
||||
echo "4) $TRIPLE_RAM MB"
|
||||
echo "5) $FOUR_TIMES_RAM MB"
|
||||
echo "6) $SIX_TIMES_RAM MB"
|
||||
echo "7) $EIGHT_TIMES_RAM MB"
|
||||
read -p "Enter your choice (1-7) or specify a custom amount in MB: " RAM_CHOICE
|
||||
|
||||
case $RAM_CHOICE in
|
||||
1) RAM=$HALF_RAM ;;
|
||||
2) RAM=$DEFAULT_RAM ;;
|
||||
3) RAM=$DOUBLE_RAM ;;
|
||||
4) RAM=$TRIPLE_RAM ;;
|
||||
5) RAM=$FOUR_TIMES_RAM ;;
|
||||
6) RAM=$SIX_TIMES_RAM ;;
|
||||
7) RAM=$EIGHT_TIMES_RAM ;;
|
||||
*)
|
||||
if [[ "$RAM_CHOICE" =~ ^[0-9]+$ ]]; then
|
||||
RAM=$RAM_CHOICE
|
||||
else
|
||||
echo "Invalid choice. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# define image path
|
||||
IMAGE_PATH="/var/lib/libvirt/images/${VM_NAME}.img"
|
||||
|
||||
# copy the base image
|
||||
|
||||
if [[ -e "$IMAGE_PATH" ]]; then
|
||||
echo "Error: $IMAGE_PATH already exists. Choose a different --name or remove the old image first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Copying the base image to $IMAGE_PATH..."
|
||||
cp noble-server-cloudimg-amd64.img "$IMAGE_PATH"
|
||||
|
||||
# install guestfs-tools if missing
|
||||
echo "Checking and installing guestfs-tools if needed..."
|
||||
if ! dpkg -l | grep -q guestfs-tools; then
|
||||
sudo apt update && sudo apt install guestfs-tools -y
|
||||
fi
|
||||
|
||||
# set root password inside the image
|
||||
echo "Setting root password inside the VM image..."
|
||||
virt-customize -a "$IMAGE_PATH" --root-password password:"$PASSWORD"
|
||||
|
||||
# resize the image
|
||||
echo "Resizing the image by +100G..."
|
||||
qemu-img resize "$IMAGE_PATH" +100G
|
||||
echo "Resizing the image by +${SIZE}G..."
|
||||
qemu-img resize "$IMAGE_PATH" +${SIZE}G
|
||||
|
||||
# install the VM and run log in prompt
|
||||
echo "Starting VM installation..."
|
||||
virt-install \
|
||||
--name "$VM_NAME" \
|
||||
--ram="$RAM" \
|
||||
--vcpus="$VCPUS" \
|
||||
--cpu host \
|
||||
--hvm \
|
||||
--disk bus=virtio,path="$IMAGE_PATH" \
|
||||
--network bridge=br0 \
|
||||
--graphics none \
|
||||
--console pty,target_type=serial \
|
||||
--osinfo ubuntunoble \
|
||||
--import
|
||||
--name "$VM_NAME" \
|
||||
--ram="$RAM" \
|
||||
--vcpus="$VCPUS" \
|
||||
--cpu host \
|
||||
--hvm \
|
||||
--disk bus=virtio,path="$IMAGE_PATH" \
|
||||
--network bridge=br0 \
|
||||
--graphics none \
|
||||
--console pty,target_type=serial \
|
||||
--osinfo ubuntunoble \
|
||||
--import
|
||||
|
||||
echo "VM $VM_NAME has been successfully installed!"
|
||||
echo "VM $VM_NAME has been successfully installed!"
|
||||
@@ -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',
|
||||
}),
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user