Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 167a30478f | |||
| 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
|
||||
|
||||
|
||||
@@ -4,94 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2026.11-xynomizithra] (2026-06-08)
|
||||
|
||||
- bugfix: allow re-inviting expired members ([#6863])
|
||||
- feat: disable Nagle's algorithm for LP between nym-nodes ([#6857])
|
||||
- Keep peer in wg table when updating psk ([#6856])
|
||||
- chore: minor nym-node improvements ([#6850])
|
||||
- chore: LP registration adjustments ([#6845])
|
||||
- crates release: bump version to 1.21.1 ([#6844])
|
||||
- fix gateways being penalised for no stress testing ([#6843])
|
||||
- fix score inflation for throttled nodes ([#6842])
|
||||
- Bugfix/cherry pick/waterloo stres testing floats ([#6841])
|
||||
- bugfix: NMv3 race condition ([#6837])
|
||||
- feat: implement UpdateFamily for the node families contract ([#6834])
|
||||
- Bugfix/cherry pick/waterloo ns api ([#6833])
|
||||
- experiment: attempt to retroactively generate specs for node families and ecash contracts ([#6813])
|
||||
- moving lp packets in lp-data crate ([#6810])
|
||||
- upgrade axum to 0.8.9 (and side deps) ([#6808])
|
||||
- chore: expose admin method for migrating vesting delegations/mixnodes ([#6795])
|
||||
- [chore] fix clippy 1.95 lints for future version update ([#6794])
|
||||
- Handle Rate Limit Challenge Response ([#6786])
|
||||
- NYM-583: Avoid corrupted database on Windows. ([#6785])
|
||||
- Max/smolmix wasm ([#6784])
|
||||
- Chore/bugfixes ([#6783])
|
||||
- Switch from yarn to pnpm ([#6779])
|
||||
- feat: Node Families: expose stake information inside DVpnGateway ([#6778])
|
||||
- feat: Node Families: expose family information for NS API consumers ([#6777])
|
||||
- feat: Node Families: cache and expose family data within nym API ([#6774])
|
||||
- Re-order default API urls for network details ([#6767])
|
||||
- add ci for NM agent binary ([#6764])
|
||||
- feat/refactor: introduce shared contract caches within Nym API ([#6760])
|
||||
- chore: removed dead code for redundant mixnet-vesting integration tests ([#6759])
|
||||
- feat: Node Families: remove nodes upon unbonding ([#6752])
|
||||
- feat: Node Families: contract transactions ([#6750])
|
||||
- feat: Node Families: contract queries ([#6731])
|
||||
- feat: Node Families: initial contract storage ([#6717])
|
||||
- start node families topic branch ([#6715])
|
||||
- Bump rand from 0.8.5 to 0.8.6 in /contracts ([#6702])
|
||||
- Testing port checks in NS Agents ([#6694])
|
||||
- build(deps): bump microsoft/setup-msbuild from 2 to 3 ([#6602])
|
||||
- build(deps): bump tar from 0.4.44 to 0.4.45 ([#6595])
|
||||
- build(deps): bump quinn-proto from 0.11.12 to 0.11.14 ([#6549])
|
||||
- build(deps): bump docker/login-action from 3 to 4 ([#6518])
|
||||
- build(deps): bump actions/download-artifact from 7 to 8 ([#6497])
|
||||
- build(deps): bump actions/upload-artifact from 6 to 7 ([#6496])
|
||||
|
||||
[#6863]: https://github.com/nymtech/nym/pull/6863
|
||||
[#6857]: https://github.com/nymtech/nym/pull/6857
|
||||
[#6856]: https://github.com/nymtech/nym/pull/6856
|
||||
[#6850]: https://github.com/nymtech/nym/pull/6850
|
||||
[#6845]: https://github.com/nymtech/nym/pull/6845
|
||||
[#6844]: https://github.com/nymtech/nym/pull/6844
|
||||
[#6843]: https://github.com/nymtech/nym/pull/6843
|
||||
[#6842]: https://github.com/nymtech/nym/pull/6842
|
||||
[#6841]: https://github.com/nymtech/nym/pull/6841
|
||||
[#6837]: https://github.com/nymtech/nym/pull/6837
|
||||
[#6834]: https://github.com/nymtech/nym/pull/6834
|
||||
[#6833]: https://github.com/nymtech/nym/pull/6833
|
||||
[#6813]: https://github.com/nymtech/nym/pull/6813
|
||||
[#6810]: https://github.com/nymtech/nym/pull/6810
|
||||
[#6808]: https://github.com/nymtech/nym/pull/6808
|
||||
[#6795]: https://github.com/nymtech/nym/pull/6795
|
||||
[#6794]: https://github.com/nymtech/nym/pull/6794
|
||||
[#6786]: https://github.com/nymtech/nym/pull/6786
|
||||
[#6785]: https://github.com/nymtech/nym/pull/6785
|
||||
[#6784]: https://github.com/nymtech/nym/pull/6784
|
||||
[#6783]: https://github.com/nymtech/nym/pull/6783
|
||||
[#6779]: https://github.com/nymtech/nym/pull/6779
|
||||
[#6778]: https://github.com/nymtech/nym/pull/6778
|
||||
[#6777]: https://github.com/nymtech/nym/pull/6777
|
||||
[#6774]: https://github.com/nymtech/nym/pull/6774
|
||||
[#6767]: https://github.com/nymtech/nym/pull/6767
|
||||
[#6764]: https://github.com/nymtech/nym/pull/6764
|
||||
[#6760]: https://github.com/nymtech/nym/pull/6760
|
||||
[#6759]: https://github.com/nymtech/nym/pull/6759
|
||||
[#6752]: https://github.com/nymtech/nym/pull/6752
|
||||
[#6750]: https://github.com/nymtech/nym/pull/6750
|
||||
[#6731]: https://github.com/nymtech/nym/pull/6731
|
||||
[#6717]: https://github.com/nymtech/nym/pull/6717
|
||||
[#6715]: https://github.com/nymtech/nym/pull/6715
|
||||
[#6702]: https://github.com/nymtech/nym/pull/6702
|
||||
[#6694]: https://github.com/nymtech/nym/pull/6694
|
||||
[#6602]: https://github.com/nymtech/nym/pull/6602
|
||||
[#6595]: https://github.com/nymtech/nym/pull/6595
|
||||
[#6549]: https://github.com/nymtech/nym/pull/6549
|
||||
[#6518]: https://github.com/nymtech/nym/pull/6518
|
||||
[#6497]: https://github.com/nymtech/nym/pull/6497
|
||||
[#6496]: https://github.com/nymtech/nym/pull/6496
|
||||
|
||||
## [2026.10-waterloo] (2026-05-27)
|
||||
|
||||
- Re-order default API urls for network details - Waterloo release ([#6799])
|
||||
|
||||
Generated
+1424
-1375
File diff suppressed because it is too large
Load Diff
+110
-114
@@ -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",
|
||||
@@ -212,7 +211,7 @@ edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.87.0"
|
||||
readme = "README.md"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
addr = "0.15.6"
|
||||
@@ -407,6 +406,7 @@ utoipa-swagger-ui = "9.0.2"
|
||||
utoipauto = "0.2"
|
||||
uuid = "1.19.0"
|
||||
vergen = { version = "=8.3.1", default-features = false }
|
||||
vergen-gitcl = { version = "1.0.8", default-features = false }
|
||||
walkdir = "2"
|
||||
x25519-dalek = "2.0.0"
|
||||
zeroize = "1.7.0"
|
||||
@@ -429,115 +429,115 @@ libcrux-sha3 = "0.0.8"
|
||||
libcrux-traits = "0.0.6"
|
||||
|
||||
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
|
||||
nym-api-requests = { version = "1.21.1", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.21.1", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.21.1", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.21.1", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.21.1", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.21.1", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.21.1", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.21.1", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.21.1", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.21.1", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.21.1", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.21.1", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.21.1", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.21.1", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.21.1", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.21.1", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.21.1", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.21.1", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.21.1", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.21.1", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.21.1", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.21.1", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.21.1", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.21.1", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.21.1", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.21.1", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.21.1", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.21.1", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.21.1", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.21.1", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-gateway-probe = { version = "1.21.1", path = "nym-gateway-probe" }
|
||||
nym-gateway-requests = { version = "1.21.1", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.21.1", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.21.1", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.21.1", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.21.1", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.21.1", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.21.1", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.21.1", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.21.1", path = "common/ip-packet-requests" }
|
||||
nym-lp = { version = "1.21.1", path = "common/nym-lp" }
|
||||
nym-lp-data = { version = "1.21.1", path = "common/nym-lp-data" }
|
||||
nym-kkt = { version = "1.21.1", path = "common/nym-kkt" }
|
||||
nym-kkt-ciphersuite = { version = "1.21.1", path = "common/nym-kkt-ciphersuite" }
|
||||
nym-kkt-context = { version = "1.21.1", path = "common/nym-kkt-context" }
|
||||
nym-metrics = { version = "1.21.1", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.21.1", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.21.1", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.21.1", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.21.1", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.21.1", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.21.1", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.21.1", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.21.1", path = "nym-node/nym-node-metrics" }
|
||||
nym-node-families-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/node-families-contract" }
|
||||
nym-ordered-buffer = { version = "1.21.1", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.21.1", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.21.1", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.21.1", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.21.1", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.21.1", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.21.1", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.21.1", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.21.1", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.21.1", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.21.1", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.21.1", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.21.1", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.21.1", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.21.1", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.21.1", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.21.1", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.21.1", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.21.1", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.21.1", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.21.1", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.21.1", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.21.1", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.21.1", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.21.1", path = "common/task" }
|
||||
nym-tun = { version = "1.21.1", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.21.1", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.21.1", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.21.1", path = "common/topology" }
|
||||
nym-types = { version = "1.21.1", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.21.1", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.21.1", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-network-monitors-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/network-monitors-contract" }
|
||||
nym-verloc = { version = "1.21.1", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.21.1", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.21.1", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.21.1", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.21.1", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.21.1", path = "common/wireguard-private-metadata/server" }
|
||||
nym-sqlx-pool-guard = { version = "1.21.1", path = "nym-sqlx-pool-guard" }
|
||||
nym-wasm-client-core = { version = "1.21.1", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.21.1", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.21.1", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.21.1", path = "common/nyxd-scraper-shared" }
|
||||
nym-api-requests = { version = "1.21.0", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.21.0", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.21.0", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.21.0", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.21.0", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.21.0", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.21.0", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.21.0", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.21.0", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.21.0", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.21.0", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.21.0", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.21.0", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.21.0", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.21.0", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.21.0", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.21.0", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.21.0", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.21.0", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.21.0", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.21.0", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.21.0", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.21.0", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.21.0", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.21.0", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.21.0", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.21.0", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.21.0", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.21.0", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.21.0", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-gateway-probe = { version = "1.18.0", path = "nym-gateway-probe" }
|
||||
nym-gateway-requests = { version = "1.21.0", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.21.0", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.21.0", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.21.0", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.21.0", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.21.0", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.21.0", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.21.0", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.21.0", path = "common/ip-packet-requests" }
|
||||
nym-lp = { version = "1.21.0", path = "common/nym-lp" }
|
||||
nym-lp-data = { version = "1.21.0", path = "common/nym-lp-data" }
|
||||
nym-kkt = { version = "1.21.0", path = "common/nym-kkt" }
|
||||
nym-kkt-ciphersuite = { version = "1.21.0", path = "common/nym-kkt-ciphersuite" }
|
||||
nym-kkt-context = { version = "1.21.0", path = "common/nym-kkt-context" }
|
||||
nym-metrics = { version = "1.21.0", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.21.0", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.21.0", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.21.0", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.21.0", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.21.0", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.21.0", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.21.0", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.21.0", path = "nym-node/nym-node-metrics" }
|
||||
nym-node-families-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/node-families-contract" }
|
||||
nym-ordered-buffer = { version = "1.21.0", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.21.0", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.21.0", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.21.0", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.21.0", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.21.0", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.21.0", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.21.0", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.21.0", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.21.0", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.21.0", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.21.0", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.21.0", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.21.0", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.21.0", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.21.0", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.21.0", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.21.0", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.21.0", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.21.0", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.21.0", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.21.0", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.21.0", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.21.0", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.21.0", path = "common/task" }
|
||||
nym-tun = { version = "1.21.0", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.21.0", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.21.0", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.21.0", path = "common/topology" }
|
||||
nym-types = { version = "1.21.0", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.21.0", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.21.0", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-network-monitors-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/network-monitors-contract" }
|
||||
nym-verloc = { version = "1.21.0", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.21.0", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.21.0", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.21.0", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.21.0", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.21.0", path = "common/wireguard-private-metadata/server" }
|
||||
nym-sqlx-pool-guard = { version = "1.2.0", path = "nym-sqlx-pool-guard" }
|
||||
nym-wasm-client-core = { version = "1.21.0", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.21.0", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.21.0", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.21.0", path = "common/nyxd-scraper-shared" }
|
||||
|
||||
smolmix = { version = "1.21.1", path = "smolmix/core" }
|
||||
smolmix = { version = "1.21.0", path = "smolmix/core" }
|
||||
|
||||
# coconut/DKG related
|
||||
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
|
||||
@@ -604,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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
description = "Implementation of the Nym Client"
|
||||
version = "1.1.78"
|
||||
version = "1.1.77"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
version = "1.1.78"
|
||||
version = "1.1.77"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -218,11 +218,6 @@ impl ManagedConnection {
|
||||
"Managed to establish connection to {}", self.address
|
||||
);
|
||||
|
||||
// disable Nagle: mix packets are latency-sensitive and flushed one at a time.
|
||||
if let Err(err) = stream.set_nodelay(true) {
|
||||
warn!(peer = %address, error = %err, "failed to set TCP_NODELAY on outbound mixnet connection");
|
||||
}
|
||||
|
||||
// 3. perform noise handshake (if applicable)
|
||||
let noise_start = tokio::time::Instant::now();
|
||||
let noise_stream = match upgrade_noise_initiator(stream, &self.noise_config).await {
|
||||
@@ -251,42 +246,25 @@ impl ManagedConnection {
|
||||
noise_handshake_ms,
|
||||
"Noise initiator handshake completed for {:?}", address
|
||||
);
|
||||
let mut conn = Framed::new(noise_stream, NymCodec);
|
||||
// let the write buffer accumulate several packets before flushing (see run_io_loop)
|
||||
conn.set_backpressure_boundary(OUTBOUND_WRITE_BUFFER);
|
||||
let conn = Framed::new(noise_stream, NymCodec);
|
||||
|
||||
// 4. start handling the framed stream
|
||||
run_io_loop(conn, self.message_receiver, address).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Upper bound on how many already-queued packets we drain into a single flush.
|
||||
/// Bounds the per-batch allocation and how often we re-check the read side; the actual
|
||||
/// write coalescing is governed by the Framed backpressure boundary below.
|
||||
const OUTBOUND_FLUSH_BATCH: usize = 1024;
|
||||
|
||||
/// Write-buffer high-water mark for the egress `Framed`: packets are coalesced up to
|
||||
/// roughly this many bytes before a flush, trading a larger write burst for far fewer
|
||||
/// syscalls (and noise frames) under load. Kept under the ~64KiB noise frame ceiling so
|
||||
/// a flush is usually a single frame.
|
||||
const OUTBOUND_WRITE_BUFFER: usize = 32 * 1024;
|
||||
|
||||
// The connection is unidirectional (send-only); we read from it solely to
|
||||
// notice peer FIN/RST while idle so we can evict the cache entry before the
|
||||
// next outbound send finds it stale.
|
||||
async fn run_io_loop<T>(
|
||||
conn: Framed<T, NymCodec>,
|
||||
receiver: ReceiverStream<FramedNymPacket>,
|
||||
mut receiver: ReceiverStream<FramedNymPacket>,
|
||||
address: SocketAddr,
|
||||
) where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let (mut sink, mut stream) = conn.split();
|
||||
|
||||
// drain all currently-queued packets into one flush rather than flushing per packet,
|
||||
// which otherwise caps egress throughput and backs up the per-connection queue under load
|
||||
let mut receiver = receiver.ready_chunks(OUTBOUND_FLUSH_BATCH);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = stream.next() => {
|
||||
@@ -327,22 +305,14 @@ async fn run_io_loop<T>(
|
||||
);
|
||||
break;
|
||||
}
|
||||
Some(batch) => {
|
||||
// feed the whole ready batch, then flush once
|
||||
let res = async {
|
||||
for packet in batch {
|
||||
sink.feed(packet).await?;
|
||||
}
|
||||
sink.flush().await
|
||||
}
|
||||
.await;
|
||||
if let Err(err) = res {
|
||||
Some(packet) => {
|
||||
if let Err(err) = sink.send(packet).await {
|
||||
debug!(
|
||||
event = "connection.forward_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "forward_error",
|
||||
"failed to forward packet batch to {address}: {err}"
|
||||
"Failed to forward packet to {address}: {err}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -61,12 +61,9 @@ pub struct NodeFamily {
|
||||
|
||||
/// A pending invitation for a node to join a particular family.
|
||||
///
|
||||
/// Invitations are stored until they are accepted, rejected, or revoked. Once the
|
||||
/// chain advances past `expires_at` an invitation becomes inert but stays in storage
|
||||
/// — there is no background process clearing expired invitations. A timed-out
|
||||
/// invitation is cleared either when explicitly revoked/rejected, or when the family
|
||||
/// issues a fresh invitation for the same node, which archives the stale one as
|
||||
/// `Expired` and supersedes it.
|
||||
/// Invitations are stored until they are accepted, rejected, revoked, or until the
|
||||
/// chain advances past `expires_at` (in which case they remain in storage but are
|
||||
/// treated as inert — there is no background process clearing expired invitations).
|
||||
#[cw_serde]
|
||||
pub struct FamilyInvitation {
|
||||
/// The family that issued the invitation.
|
||||
@@ -110,10 +107,8 @@ pub struct PastFamilyMember {
|
||||
|
||||
/// Terminal status for an invitation that has been moved out of the pending set.
|
||||
///
|
||||
/// Note: an invitation that merely times out is **not** archived here on its own —
|
||||
/// it is left inert in the pending set (see `FamilyInvitation::expires_at`). It only
|
||||
/// reaches `Expired` if the family issues a fresh invitation for the same node, which
|
||||
/// supersedes and archives the stale one.
|
||||
/// Note: timed-out invitations are not represented here — they are simply left in
|
||||
/// the pending set (see `FamilyInvitation::expires_at`).
|
||||
#[cw_serde]
|
||||
pub enum FamilyInvitationStatus {
|
||||
/// Still awaiting a response. Recorded with a timestamp for completeness even
|
||||
@@ -126,16 +121,11 @@ pub enum FamilyInvitationStatus {
|
||||
/// The family revoked the invitation at the given timestamp before it could
|
||||
/// be accepted or rejected.
|
||||
Revoked { at: u64 },
|
||||
/// The invitation had already expired and was superseded by a fresh invitation
|
||||
/// for the same node from the same family, issued at the given timestamp. This is
|
||||
/// the only path that archives a timed-out invitation.
|
||||
Expired { at: u64 },
|
||||
}
|
||||
|
||||
/// Historical record of an invitation that has reached a terminal state
|
||||
/// (`Accepted`, `Rejected`, `Revoked`, or `Expired`). A timed-out invitation is
|
||||
/// archived here only when a fresh invitation for the same node supersedes it
|
||||
/// (status `Expired`); otherwise it stays in the pending map until explicitly cleared.
|
||||
/// (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not**
|
||||
/// archived here — they remain in the pending map until explicitly cleared.
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyInvitation {
|
||||
/// The original invitation as it was issued.
|
||||
|
||||
@@ -24,8 +24,10 @@ pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
|
||||
|
||||
pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str =
|
||||
"n1m3a2ltkjqud8mkmrpqvgllrtv2p4r6js6qwl7p8cqkzrq8jg6e2qwqgl8z";
|
||||
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str =
|
||||
"n1na0vys0z077hq3zrz6pfea85zgv8ks3t5zysdt6y38c87q045hnsyf2g5x";
|
||||
// \/ TODO: this has to be updated once the contract is deployed
|
||||
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
pub const ECASH_CONTRACT_ADDRESS: &str =
|
||||
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str =
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-kkt"
|
||||
description = "Key transport protocol for the Nym network"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
|
||||
edition = { workspace = true }
|
||||
license.workspace = true
|
||||
|
||||
@@ -14,7 +14,6 @@ use tokio::sync::mpsc::Receiver;
|
||||
#[derive(Hash, PartialOrd, PartialEq, Clone, Debug, Eq, Copy)]
|
||||
pub enum PeerControlRequestTypeV2 {
|
||||
AddPeer,
|
||||
UpdatePeerPsk,
|
||||
RemovePeer,
|
||||
QueryPeer,
|
||||
GetClientBandwidthByKey,
|
||||
@@ -27,7 +26,6 @@ impl From<&PeerControlRequest> for PeerControlRequestTypeV2 {
|
||||
fn from(req: &PeerControlRequest) -> Self {
|
||||
match req {
|
||||
PeerControlRequest::AddPeer { .. } => PeerControlRequestTypeV2::AddPeer,
|
||||
PeerControlRequest::UpdatePeerPsk { .. } => PeerControlRequestTypeV2::UpdatePeerPsk,
|
||||
PeerControlRequest::PreAllocateIpPair { .. } => PeerControlRequestTypeV2::AddPeer,
|
||||
PeerControlRequest::RemovePeer { .. } => PeerControlRequestTypeV2::RemovePeer,
|
||||
PeerControlRequest::QueryPeer { .. } => PeerControlRequestTypeV2::QueryPeer,
|
||||
@@ -117,15 +115,6 @@ impl MockPeerControllerV2 {
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
PeerControlRequest::UpdatePeerPsk { response_tx, .. } => {
|
||||
response_tx
|
||||
.send(
|
||||
*response
|
||||
.downcast()
|
||||
.expect("registered response has mismatched type"),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
PeerControlRequest::PreAllocateIpPair { response_tx, .. } => {
|
||||
response_tx
|
||||
.send(
|
||||
|
||||
@@ -71,7 +71,6 @@ impl From<&Key> for KeyWrapper {
|
||||
#[derive(Hash, PartialOrd, PartialEq, Clone, Debug, Eq)]
|
||||
pub enum PeerControlRequestType {
|
||||
AddPeer { public_key: KeyWrapper },
|
||||
UpdatePeerPsk { peer_key: KeyWrapper },
|
||||
AllocatePeerIpPair {},
|
||||
ReleaseIpPair { ip_pair: IpPair },
|
||||
RemovePeer { key: KeyWrapper },
|
||||
@@ -87,7 +86,6 @@ impl PeerControlRequestType {
|
||||
pub fn peer_key(&self) -> Option<KeyWrapper> {
|
||||
match self {
|
||||
PeerControlRequestType::AddPeer { public_key } => Some(public_key.clone()),
|
||||
PeerControlRequestType::UpdatePeerPsk { peer_key } => Some(peer_key.clone()),
|
||||
PeerControlRequestType::AllocatePeerIpPair {} => None,
|
||||
PeerControlRequestType::ReleaseIpPair { .. } => None,
|
||||
PeerControlRequestType::RemovePeer { key } => Some(key.clone()),
|
||||
@@ -111,11 +109,6 @@ impl From<&PeerControlRequest> for PeerControlRequestType {
|
||||
PeerControlRequest::AddPeer { peer, .. } => PeerControlRequestType::AddPeer {
|
||||
public_key: (&peer.public_key).into(),
|
||||
},
|
||||
PeerControlRequest::UpdatePeerPsk { peer_key, .. } => {
|
||||
PeerControlRequestType::UpdatePeerPsk {
|
||||
peer_key: peer_key.into(),
|
||||
}
|
||||
}
|
||||
PeerControlRequest::PreAllocateIpPair { .. } => {
|
||||
PeerControlRequestType::AllocatePeerIpPair {}
|
||||
}
|
||||
@@ -278,9 +271,6 @@ impl MockPeerController {
|
||||
}
|
||||
response_tx.send_downcasted(response.content)
|
||||
}
|
||||
PeerControlRequest::UpdatePeerPsk { response_tx, .. } => {
|
||||
response_tx.send_downcasted(response.content)
|
||||
}
|
||||
PeerControlRequest::PreAllocateIpPair { response_tx, .. } => {
|
||||
response_tx.send_downcasted(response.content)
|
||||
}
|
||||
|
||||
@@ -76,12 +76,6 @@ pub enum PeerControlRequest {
|
||||
peer: Peer,
|
||||
response_tx: oneshot::Sender<AddPeerControlResponse>,
|
||||
},
|
||||
/// Update PSK for an existing peer, without changing its IP allocation
|
||||
UpdatePeerPsk {
|
||||
peer_key: Key,
|
||||
psk: Key,
|
||||
response_tx: oneshot::Sender<UpdatePeerPskControlResponse>,
|
||||
},
|
||||
/// Attempt to allocate an IP pair from the pool
|
||||
PreAllocateIpPair {
|
||||
response_tx: oneshot::Sender<AllocatePeerControlResponse>,
|
||||
@@ -124,7 +118,6 @@ pub enum PeerControlRequest {
|
||||
}
|
||||
|
||||
pub type AddPeerControlResponse = Result<()>;
|
||||
pub type UpdatePeerPskControlResponse = Result<()>;
|
||||
pub type AllocatePeerControlResponse = Result<IpPair>;
|
||||
pub type ReleaseIpPairControlResponse = Result<()>;
|
||||
pub type RemovePeerControlResponse = Result<()>;
|
||||
@@ -324,50 +317,6 @@ impl PeerController {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_update_peer_psk_request(&mut self, peer_key: &Key, psk: Key) -> Result<()> {
|
||||
// observation will get automatically added once dropped
|
||||
let _metric_timer =
|
||||
PROMETHEUS_METRICS.start_timer(PrometheusMetric::WireguardDefguardPeerPskUpdate);
|
||||
|
||||
nym_metrics::inc!("wg_peer_update_psk_attempts");
|
||||
|
||||
let Ok(Some(mut peer)) = self.handle_query_peer_by_key(peer_key).await else {
|
||||
return Ok(());
|
||||
};
|
||||
let encoded_psk = psk.to_lower_hex();
|
||||
peer.preshared_key = Some(psk);
|
||||
|
||||
// Account for bandwidth used so far *before* reconfiguring: `configure_peer`
|
||||
// isn't guaranteed to preserve the kernel rx/tx counters, so fold the
|
||||
// accrued bytes into the metrics first to avoid losing them on a reset.
|
||||
if let Ok(host) = self.wg_api.read_interface_data() {
|
||||
self.update_metrics(&host).await;
|
||||
*self.host_information.write().await = host;
|
||||
}
|
||||
|
||||
// Try to update WireGuard peer
|
||||
if let Err(e) = self.wg_api.configure_peer(&peer) {
|
||||
nym_metrics::inc!("wg_peer_update_psk_failed");
|
||||
nym_metrics::inc!("wg_config_errors_total");
|
||||
return Err(e.into());
|
||||
};
|
||||
|
||||
// Persist the new PSK to disk so it survives a restart. Kernel-first: a
|
||||
// failure here leaves the live session working, only risking drift on restart.
|
||||
self.ecash_verifier
|
||||
.storage()
|
||||
.update_peer_psk(&peer_key.to_string(), Some(&encoded_psk))
|
||||
.await?;
|
||||
|
||||
// Refresh again so the cached host information reflects the post-update state
|
||||
if let Ok(host) = self.wg_api.read_interface_data() {
|
||||
*self.host_information.write().await = host;
|
||||
}
|
||||
|
||||
nym_metrics::inc!("wg_peer_update_psk_success");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Allocate IP pair from pool for a new peer registration
|
||||
///
|
||||
/// This only allocates IPs - the caller must handle database storage and
|
||||
@@ -564,15 +513,6 @@ impl PeerController {
|
||||
PeerControlRequest::AddPeer { peer, response_tx } => {
|
||||
response_tx.send(self.handle_add_request(&peer).await).ok();
|
||||
}
|
||||
PeerControlRequest::UpdatePeerPsk {
|
||||
peer_key,
|
||||
psk,
|
||||
response_tx,
|
||||
} => {
|
||||
response_tx
|
||||
.send(self.handle_update_peer_psk_request(&peer_key, psk).await)
|
||||
.ok();
|
||||
}
|
||||
PeerControlRequest::PreAllocateIpPair { response_tx } => {
|
||||
response_tx.send(self.handle_ip_allocation_request()).ok();
|
||||
}
|
||||
|
||||
Generated
+264
-277
File diff suppressed because it is too large
Load Diff
@@ -1188,7 +1188,7 @@
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, or revoked. Once the chain advances past `expires_at` an invitation becomes inert but stays in storage — there is no background process clearing expired invitations. A timed-out invitation is cleared either when explicitly revoked/rejected, or when the family issues a fresh invitation for the same node, which archives the stale one as `Expired` and supersedes it.",
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
@@ -1218,7 +1218,7 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
"FamilyInvitationStatus": {
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: an invitation that merely times out is **not** archived here on its own — it is left inert in the pending set (see `FamilyInvitation::expires_at`). It only reaches `Expired` if the family issues a fresh invitation for the same node, which supersedes and archives the stale one.",
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: timed-out invitations are not represented here — they are simply left in the pending set (see `FamilyInvitation::expires_at`).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Still awaiting a response. Recorded with a timestamp for completeness even though pending invitations live in a separate map.",
|
||||
@@ -1315,35 +1315,11 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The invitation had already expired and was superseded by a fresh invitation for the same node from the same family, issued at the given timestamp. This is the only path that archives a timed-out invitation.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expired"
|
||||
],
|
||||
"properties": {
|
||||
"expired": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"PastFamilyInvitation": {
|
||||
"description": "Historical record of an invitation that has reached a terminal state (`Accepted`, `Rejected`, `Revoked`, or `Expired`). A timed-out invitation is archived here only when a fresh invitation for the same node supersedes it (status `Expired`); otherwise it stays in the pending map until explicitly cleared.",
|
||||
"description": "Historical record of an invitation that has reached a terminal state (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not** archived here — they remain in the pending map until explicitly cleared.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitation",
|
||||
@@ -1412,7 +1388,7 @@
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, or revoked. Once the chain advances past `expires_at` an invitation becomes inert but stays in storage — there is no background process clearing expired invitations. A timed-out invitation is cleared either when explicitly revoked/rejected, or when the family issues a fresh invitation for the same node, which archives the stale one as `Expired` and supersedes it.",
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
@@ -2097,7 +2073,7 @@
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, or revoked. Once the chain advances past `expires_at` an invitation becomes inert but stays in storage — there is no background process clearing expired invitations. A timed-out invitation is cleared either when explicitly revoked/rejected, or when the family issues a fresh invitation for the same node, which archives the stale one as `Expired` and supersedes it.",
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
@@ -2127,7 +2103,7 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
"FamilyInvitationStatus": {
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: an invitation that merely times out is **not** archived here on its own — it is left inert in the pending set (see `FamilyInvitation::expires_at`). It only reaches `Expired` if the family issues a fresh invitation for the same node, which supersedes and archives the stale one.",
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: timed-out invitations are not represented here — they are simply left in the pending set (see `FamilyInvitation::expires_at`).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Still awaiting a response. Recorded with a timestamp for completeness even though pending invitations live in a separate map.",
|
||||
@@ -2224,35 +2200,11 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The invitation had already expired and was superseded by a fresh invitation for the same node from the same family, issued at the given timestamp. This is the only path that archives a timed-out invitation.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expired"
|
||||
],
|
||||
"properties": {
|
||||
"expired": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"PastFamilyInvitation": {
|
||||
"description": "Historical record of an invitation that has reached a terminal state (`Accepted`, `Rejected`, `Revoked`, or `Expired`). A timed-out invitation is archived here only when a fresh invitation for the same node supersedes it (status `Expired`); otherwise it stays in the pending map until explicitly cleared.",
|
||||
"description": "Historical record of an invitation that has reached a terminal state (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not** archived here — they remain in the pending map until explicitly cleared.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitation",
|
||||
@@ -2328,7 +2280,7 @@
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, or revoked. Once the chain advances past `expires_at` an invitation becomes inert but stays in storage — there is no background process clearing expired invitations. A timed-out invitation is cleared either when explicitly revoked/rejected, or when the family issues a fresh invitation for the same node, which archives the stale one as `Expired` and supersedes it.",
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
@@ -2358,7 +2310,7 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
"FamilyInvitationStatus": {
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: an invitation that merely times out is **not** archived here on its own — it is left inert in the pending set (see `FamilyInvitation::expires_at`). It only reaches `Expired` if the family issues a fresh invitation for the same node, which supersedes and archives the stale one.",
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: timed-out invitations are not represented here — they are simply left in the pending set (see `FamilyInvitation::expires_at`).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Still awaiting a response. Recorded with a timestamp for completeness even though pending invitations live in a separate map.",
|
||||
@@ -2455,35 +2407,11 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The invitation had already expired and was superseded by a fresh invitation for the same node from the same family, issued at the given timestamp. This is the only path that archives a timed-out invitation.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expired"
|
||||
],
|
||||
"properties": {
|
||||
"expired": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"PastFamilyInvitation": {
|
||||
"description": "Historical record of an invitation that has reached a terminal state (`Accepted`, `Rejected`, `Revoked`, or `Expired`). A timed-out invitation is archived here only when a fresh invitation for the same node supersedes it (status `Expired`); otherwise it stays in the pending map until explicitly cleared.",
|
||||
"description": "Historical record of an invitation that has reached a terminal state (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not** archived here — they remain in the pending map until explicitly cleared.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitation",
|
||||
@@ -2706,7 +2634,7 @@
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, or revoked. Once the chain advances past `expires_at` an invitation becomes inert but stays in storage — there is no background process clearing expired invitations. A timed-out invitation is cleared either when explicitly revoked/rejected, or when the family issues a fresh invitation for the same node, which archives the stale one as `Expired` and supersedes it.",
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
@@ -2796,7 +2724,7 @@
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, or revoked. Once the chain advances past `expires_at` an invitation becomes inert but stays in storage — there is no background process clearing expired invitations. A timed-out invitation is cleared either when explicitly revoked/rejected, or when the family issues a fresh invitation for the same node, which archives the stale one as `Expired` and supersedes it.",
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
@@ -2886,7 +2814,7 @@
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, or revoked. Once the chain advances past `expires_at` an invitation becomes inert but stays in storage — there is no background process clearing expired invitations. A timed-out invitation is cleared either when explicitly revoked/rejected, or when the family issues a fresh invitation for the same node, which archives the stale one as `Expired` and supersedes it.",
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, or revoked. Once the chain advances past `expires_at` an invitation becomes inert but stays in storage — there is no background process clearing expired invitations. A timed-out invitation is cleared either when explicitly revoked/rejected, or when the family issues a fresh invitation for the same node, which archives the stale one as `Expired` and supersedes it.",
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
@@ -81,7 +81,7 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
"FamilyInvitationStatus": {
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: an invitation that merely times out is **not** archived here on its own — it is left inert in the pending set (see `FamilyInvitation::expires_at`). It only reaches `Expired` if the family issues a fresh invitation for the same node, which supersedes and archives the stale one.",
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: timed-out invitations are not represented here — they are simply left in the pending set (see `FamilyInvitation::expires_at`).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Still awaiting a response. Recorded with a timestamp for completeness even though pending invitations live in a separate map.",
|
||||
@@ -178,35 +178,11 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The invitation had already expired and was superseded by a fresh invitation for the same node from the same family, issued at the given timestamp. This is the only path that archives a timed-out invitation.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expired"
|
||||
],
|
||||
"properties": {
|
||||
"expired": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"PastFamilyInvitation": {
|
||||
"description": "Historical record of an invitation that has reached a terminal state (`Accepted`, `Rejected`, `Revoked`, or `Expired`). A timed-out invitation is archived here only when a fresh invitation for the same node supersedes it (status `Expired`); otherwise it stays in the pending map until explicitly cleared.",
|
||||
"description": "Historical record of an invitation that has reached a terminal state (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not** archived here — they remain in the pending map until explicitly cleared.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitation",
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, or revoked. Once the chain advances past `expires_at` an invitation becomes inert but stays in storage — there is no background process clearing expired invitations. A timed-out invitation is cleared either when explicitly revoked/rejected, or when the family issues a fresh invitation for the same node, which archives the stale one as `Expired` and supersedes it.",
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
|
||||
+3
-27
@@ -46,7 +46,7 @@
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, or revoked. Once the chain advances past `expires_at` an invitation becomes inert but stays in storage — there is no background process clearing expired invitations. A timed-out invitation is cleared either when explicitly revoked/rejected, or when the family issues a fresh invitation for the same node, which archives the stale one as `Expired` and supersedes it.",
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
@@ -76,7 +76,7 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
"FamilyInvitationStatus": {
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: an invitation that merely times out is **not** archived here on its own — it is left inert in the pending set (see `FamilyInvitation::expires_at`). It only reaches `Expired` if the family issues a fresh invitation for the same node, which supersedes and archives the stale one.",
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: timed-out invitations are not represented here — they are simply left in the pending set (see `FamilyInvitation::expires_at`).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Still awaiting a response. Recorded with a timestamp for completeness even though pending invitations live in a separate map.",
|
||||
@@ -173,35 +173,11 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The invitation had already expired and was superseded by a fresh invitation for the same node from the same family, issued at the given timestamp. This is the only path that archives a timed-out invitation.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expired"
|
||||
],
|
||||
"properties": {
|
||||
"expired": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"PastFamilyInvitation": {
|
||||
"description": "Historical record of an invitation that has reached a terminal state (`Accepted`, `Rejected`, `Revoked`, or `Expired`). A timed-out invitation is archived here only when a fresh invitation for the same node supersedes it (status `Expired`); otherwise it stays in the pending map until explicitly cleared.",
|
||||
"description": "Historical record of an invitation that has reached a terminal state (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not** archived here — they remain in the pending map until explicitly cleared.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitation",
|
||||
|
||||
+3
-27
@@ -46,7 +46,7 @@
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, or revoked. Once the chain advances past `expires_at` an invitation becomes inert but stays in storage — there is no background process clearing expired invitations. A timed-out invitation is cleared either when explicitly revoked/rejected, or when the family issues a fresh invitation for the same node, which archives the stale one as `Expired` and supersedes it.",
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
@@ -76,7 +76,7 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
"FamilyInvitationStatus": {
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: an invitation that merely times out is **not** archived here on its own — it is left inert in the pending set (see `FamilyInvitation::expires_at`). It only reaches `Expired` if the family issues a fresh invitation for the same node, which supersedes and archives the stale one.",
|
||||
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: timed-out invitations are not represented here — they are simply left in the pending set (see `FamilyInvitation::expires_at`).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Still awaiting a response. Recorded with a timestamp for completeness even though pending invitations live in a separate map.",
|
||||
@@ -173,35 +173,11 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "The invitation had already expired and was superseded by a fresh invitation for the same node from the same family, issued at the given timestamp. This is the only path that archives a timed-out invitation.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expired"
|
||||
],
|
||||
"properties": {
|
||||
"expired": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"at"
|
||||
],
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"PastFamilyInvitation": {
|
||||
"description": "Historical record of an invitation that has reached a terminal state (`Accepted`, `Rejected`, `Revoked`, or `Expired`). A timed-out invitation is archived here only when a fresh invitation for the same node supersedes it (status `Expired`); otherwise it stays in the pending map until explicitly cleared.",
|
||||
"description": "Historical record of an invitation that has reached a terminal state (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not** archived here — they remain in the pending map until explicitly cleared.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"invitation",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, or revoked. Once the chain advances past `expires_at` an invitation becomes inert but stays in storage — there is no background process clearing expired invitations. A timed-out invitation is cleared either when explicitly revoked/rejected, or when the family issues a fresh invitation for the same node, which archives the stale one as `Expired` and supersedes it.",
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, or revoked. Once the chain advances past `expires_at` an invitation becomes inert but stays in storage — there is no background process clearing expired invitations. A timed-out invitation is cleared either when explicitly revoked/rejected, or when the family issues a fresh invitation for the same node, which archives the stale one as `Expired` and supersedes it.",
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"FamilyInvitation": {
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, or revoked. Once the chain advances past `expires_at` an invitation becomes inert but stays in storage — there is no background process clearing expired invitations. A timed-out invitation is cleared either when explicitly revoked/rejected, or when the family issues a fresh invitation for the same node, which archives the stale one as `Expired` and supersedes it.",
|
||||
"description": "A pending invitation for a node to join a particular family.\n\nInvitations are stored until they are accepted, rejected, revoked, or until the chain advances past `expires_at` (in which case they remain in storage but are treated as inert — there is no background process clearing expired invitations).",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"expires_at",
|
||||
|
||||
@@ -292,17 +292,11 @@ impl NodeFamiliesStorage<'_> {
|
||||
/// - ensuring `expires_at` is strictly in the future.
|
||||
///
|
||||
/// As defence-in-depth, this method errors with [`FamilyNotFound`] if
|
||||
/// `family_id` is unknown and with [`PendingInvitationAlreadyExists`] if a
|
||||
/// *still-valid* pending invitation for the same `(family, node)` pair is
|
||||
/// already stored — the underlying `IndexedMap` would otherwise silently
|
||||
/// `family_id` is unknown and with [`PendingInvitationAlreadyExists`] if
|
||||
/// a pending invitation for the same `(family, node)` pair is already
|
||||
/// stored — the underlying `IndexedMap` would otherwise silently
|
||||
/// overwrite it.
|
||||
///
|
||||
/// If a pending invitation for the pair exists but has already expired
|
||||
/// (`now >= expires_at`), it is archived in [`Self::past_family_invitations`]
|
||||
/// with status [`FamilyInvitationStatus::Expired`] and the fresh invitation
|
||||
/// supersedes it. Together with an explicit revoke/reject, this is the only
|
||||
/// path that clears a timed-out invitation out of the pending map.
|
||||
///
|
||||
/// Returns the freshly persisted [`FamilyInvitation`].
|
||||
///
|
||||
/// [`FamilyNotFound`]: NodeFamiliesContractError::FamilyNotFound
|
||||
@@ -310,37 +304,25 @@ impl NodeFamiliesStorage<'_> {
|
||||
pub(crate) fn add_pending_invitation(
|
||||
&self,
|
||||
store: &mut dyn Storage,
|
||||
env: &Env,
|
||||
family_id: NodeFamilyId,
|
||||
node_id: NodeId,
|
||||
expires_at: u64,
|
||||
) -> Result<FamilyInvitation, NodeFamiliesContractError> {
|
||||
let now = env.block.time.seconds();
|
||||
let key: FamilyMember = (family_id, node_id);
|
||||
|
||||
if !self.families.has(store, family_id) {
|
||||
return Err(NodeFamiliesContractError::FamilyNotFound { family_id });
|
||||
}
|
||||
|
||||
if let Some(existing) = self.pending_family_invitations.may_load(store, key)? {
|
||||
// a still-valid invitation blocks a duplicate; an expired one is
|
||||
// archived and superseded by the fresh invitation below.
|
||||
if now < existing.expires_at {
|
||||
return Err(NodeFamiliesContractError::PendingInvitationAlreadyExists {
|
||||
family_id,
|
||||
node_id,
|
||||
});
|
||||
}
|
||||
|
||||
let counter = self.next_past_invitation_counter(store, key)?;
|
||||
self.past_family_invitations.save(
|
||||
store,
|
||||
(key, counter),
|
||||
&PastFamilyInvitation {
|
||||
invitation: existing,
|
||||
status: FamilyInvitationStatus::Expired { at: now },
|
||||
},
|
||||
)?;
|
||||
if self
|
||||
.pending_family_invitations
|
||||
.may_load(store, key)?
|
||||
.is_some()
|
||||
{
|
||||
return Err(NodeFamiliesContractError::PendingInvitationAlreadyExists {
|
||||
family_id,
|
||||
node_id,
|
||||
});
|
||||
}
|
||||
|
||||
let invitation = FamilyInvitation {
|
||||
@@ -932,11 +914,10 @@ mod tests {
|
||||
let s = NodeFamiliesStorage::new();
|
||||
let alice = tester.addr_make("alice");
|
||||
let f = tester.make_family(&alice);
|
||||
let env = tester.env();
|
||||
let expires_at = env.block.time.seconds() + 100;
|
||||
let expires_at = tester.env().block.time.seconds() + 100;
|
||||
|
||||
let inv = s
|
||||
.add_pending_invitation(tester.storage_mut(), &env, f.id, 42, expires_at)
|
||||
.add_pending_invitation(tester.storage_mut(), f.id, 42, expires_at)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(inv.family_id, f.id);
|
||||
@@ -956,7 +937,7 @@ mod tests {
|
||||
let env = tester.env();
|
||||
let expires_at = env.block.time.seconds() + 100;
|
||||
|
||||
let res = s.add_pending_invitation(tester.storage_mut(), &env, 99, 42, expires_at);
|
||||
let res = s.add_pending_invitation(tester.storage_mut(), 99, 42, expires_at);
|
||||
assert_eq!(
|
||||
res.unwrap_err(),
|
||||
NodeFamiliesContractError::FamilyNotFound { family_id: 99 }
|
||||
@@ -974,7 +955,7 @@ mod tests {
|
||||
tester.invite_to_family(f.id, 42);
|
||||
|
||||
let expires_at = env.block.time.seconds() + 200;
|
||||
let res = s.add_pending_invitation(tester.storage_mut(), &env, f.id, 42, expires_at);
|
||||
let res = s.add_pending_invitation(tester.storage_mut(), f.id, 42, expires_at);
|
||||
assert_eq!(
|
||||
res.unwrap_err(),
|
||||
NodeFamiliesContractError::PendingInvitationAlreadyExists {
|
||||
@@ -984,47 +965,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_pending_invitation_supersedes_expired() {
|
||||
let mut tester = init_contract_tester();
|
||||
let s = NodeFamiliesStorage::new();
|
||||
let env = tester.env();
|
||||
let alice = tester.addr_make("alice");
|
||||
let f = tester.make_family(&alice);
|
||||
|
||||
// first invitation expires at exactly `now`, so it is immediately stale
|
||||
let stale_exp = env.block.time.seconds();
|
||||
s.add_pending_invitation(tester.storage_mut(), &env, f.id, 42, stale_exp)
|
||||
.unwrap();
|
||||
|
||||
// re-inviting the same node supersedes the expired invitation
|
||||
let fresh_exp = env.block.time.seconds() + 100;
|
||||
let fresh = s
|
||||
.add_pending_invitation(tester.storage_mut(), &env, f.id, 42, fresh_exp)
|
||||
.unwrap();
|
||||
assert_eq!(fresh.expires_at, fresh_exp);
|
||||
|
||||
// the fresh invitation is the one left pending
|
||||
let pending = s
|
||||
.pending_family_invitations
|
||||
.load(tester.storage(), (f.id, 42))
|
||||
.unwrap();
|
||||
assert_eq!(pending.expires_at, fresh_exp);
|
||||
|
||||
// the stale one is archived as Expired, stamped at `now`
|
||||
let past = s
|
||||
.past_family_invitations
|
||||
.load(tester.storage(), ((f.id, 42), 0))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
past.status,
|
||||
FamilyInvitationStatus::Expired {
|
||||
at: env.block.time.seconds()
|
||||
}
|
||||
);
|
||||
assert_eq!(past.invitation.expires_at, stale_exp);
|
||||
}
|
||||
|
||||
// ---- accept_invitation ----
|
||||
|
||||
#[test]
|
||||
@@ -1035,7 +975,7 @@ mod tests {
|
||||
let alice = tester.addr_make("alice");
|
||||
let f = tester.make_family(&alice);
|
||||
let expires_at = env.block.time.seconds() + 100;
|
||||
s.add_pending_invitation(tester.storage_mut(), &env, f.id, 42, expires_at)
|
||||
s.add_pending_invitation(tester.storage_mut(), f.id, 42, expires_at)
|
||||
.unwrap();
|
||||
|
||||
let updated = s
|
||||
@@ -1092,7 +1032,7 @@ mod tests {
|
||||
let f = tester.make_family(&alice);
|
||||
// expires at exactly `now` — `now >= expires_at` triggers
|
||||
let expires_at = tester.env().block.time.seconds();
|
||||
s.add_pending_invitation(tester.storage_mut(), &env, f.id, 42, expires_at)
|
||||
s.add_pending_invitation(tester.storage_mut(), f.id, 42, expires_at)
|
||||
.unwrap();
|
||||
|
||||
let res = s.accept_invitation(tester.storage_mut(), &env, f.id, 42);
|
||||
@@ -1147,7 +1087,7 @@ mod tests {
|
||||
let alice = tester.addr_make("alice");
|
||||
let f = tester.make_family(&alice);
|
||||
let expires_at = env.block.time.seconds();
|
||||
s.add_pending_invitation(tester.storage_mut(), &env, f.id, 42, expires_at)
|
||||
s.add_pending_invitation(tester.storage_mut(), f.id, 42, expires_at)
|
||||
.unwrap();
|
||||
|
||||
s.reject_pending_invitation(tester.storage_mut(), &env, f.id, 42)
|
||||
@@ -1265,7 +1205,7 @@ mod tests {
|
||||
|
||||
let expires_at = env.block.time.seconds() + 100;
|
||||
for _ in 0..2 {
|
||||
s.add_pending_invitation(tester.storage_mut(), &env, f.id, 42, expires_at)
|
||||
s.add_pending_invitation(tester.storage_mut(), f.id, 42, expires_at)
|
||||
.unwrap();
|
||||
s.accept_invitation(tester.storage_mut(), &env, f.id, 42)
|
||||
.unwrap();
|
||||
|
||||
@@ -168,9 +168,8 @@ pub trait NodeFamiliesContractTesterExt:
|
||||
node: NodeId,
|
||||
expiration: u64,
|
||||
) -> FamilyInvitation {
|
||||
let env = self.env();
|
||||
NodeFamiliesStorage::new()
|
||||
.add_pending_invitation(self, &env, family, node, expiration)
|
||||
.add_pending_invitation(self, family, node, expiration)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
|
||||
@@ -254,8 +254,7 @@ pub(crate) fn try_invite_to_family(
|
||||
ensure_node_not_in_family(&storage, deps.as_ref(), node_id)?;
|
||||
|
||||
let expires_at = env.block.time.seconds() + validity;
|
||||
let invitation =
|
||||
storage.add_pending_invitation(deps.storage, &env, owned.id, node_id, expires_at)?;
|
||||
let invitation = storage.add_pending_invitation(deps.storage, owned.id, node_id, expires_at)?;
|
||||
|
||||
Ok(Response::new().add_event(
|
||||
Event::new(events::FAMILY_INVITATION_EVENT_NAME)
|
||||
@@ -1312,8 +1311,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::testing::NodeFamiliesContractTesterExt;
|
||||
use mixnet_contract::testable_mixnet_contract::EmbeddedMixnetContractExt;
|
||||
use nym_contracts_common_testing::ChainOpts;
|
||||
use nym_node_families_contract_common::FamilyInvitationStatus;
|
||||
|
||||
#[test]
|
||||
fn happy_path_persists_pending_invitation() -> anyhow::Result<()> {
|
||||
@@ -1472,98 +1469,6 @@ mod tests {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_reinvite_once_previous_invitation_has_expired() -> anyhow::Result<()> {
|
||||
let mut tester = init_contract_tester();
|
||||
let alice = tester.addr_make("alice");
|
||||
let family = tester.make_family(&alice);
|
||||
let node_id = tester.bond_dummy_nymnode()?;
|
||||
|
||||
// first invitation with a short, explicit validity
|
||||
let first_env = tester.env();
|
||||
try_invite_to_family(
|
||||
tester.deps_mut(),
|
||||
first_env.clone(),
|
||||
message_info(&alice, &[]),
|
||||
node_id,
|
||||
Some(5),
|
||||
)?;
|
||||
let first_expires_at = first_env.block.time.seconds() + 5;
|
||||
|
||||
// let it lapse
|
||||
tester.advance_time_by(10);
|
||||
|
||||
// re-inviting the same node now succeeds and refreshes the expiry
|
||||
let second_env = tester.env();
|
||||
try_invite_to_family(
|
||||
tester.deps_mut(),
|
||||
second_env.clone(),
|
||||
message_info(&alice, &[]),
|
||||
node_id,
|
||||
Some(5),
|
||||
)?;
|
||||
|
||||
let storage = NodeFamiliesStorage::new();
|
||||
let pending = storage
|
||||
.pending_family_invitations
|
||||
.load(tester.deps().storage, (family.id, node_id))?;
|
||||
assert_eq!(pending.expires_at, second_env.block.time.seconds() + 5);
|
||||
|
||||
// the lapsed invitation was archived as Expired at the re-invite time
|
||||
let archived = storage
|
||||
.past_family_invitations
|
||||
.load(tester.deps().storage, ((family.id, node_id), 0))?;
|
||||
assert!(matches!(
|
||||
archived.status,
|
||||
FamilyInvitationStatus::Expired { at } if at == second_env.block.time.seconds()
|
||||
));
|
||||
assert_eq!(archived.invitation.expires_at, first_expires_at);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_reinvite_while_previous_invitation_is_still_valid() -> anyhow::Result<()> {
|
||||
let mut tester = init_contract_tester();
|
||||
let alice = tester.addr_make("alice");
|
||||
let family = tester.make_family(&alice);
|
||||
let node_id = tester.bond_dummy_nymnode()?;
|
||||
|
||||
let env = tester.env();
|
||||
try_invite_to_family(
|
||||
tester.deps_mut(),
|
||||
env,
|
||||
message_info(&alice, &[]),
|
||||
node_id,
|
||||
Some(100),
|
||||
)?;
|
||||
|
||||
// some time passes, but the invitation has not yet expired
|
||||
tester.advance_time_by(10);
|
||||
|
||||
let env = tester.env();
|
||||
let err = try_invite_to_family(
|
||||
tester.deps_mut(),
|
||||
env,
|
||||
message_info(&alice, &[]),
|
||||
node_id,
|
||||
Some(100),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
NodeFamiliesContractError::PendingInvitationAlreadyExists {
|
||||
family_id: family.id,
|
||||
node_id,
|
||||
}
|
||||
);
|
||||
// nothing was archived — the still-valid invitation stays pending
|
||||
assert!(NodeFamiliesStorage::new()
|
||||
.past_family_invitations
|
||||
.may_load(tester.deps().storage, ((family.id, node_id), 0))?
|
||||
.is_none());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
mod revoke_family_invitation {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -18,7 +18,6 @@ MULTISIG_CONTRACT_ADDRESS=n1tz0setr8vkh9udp8xyxgpqc89ns27k4d0jx2h942hr0ax63yjhmq
|
||||
COCONUT_DKG_CONTRACT_ADDRESS=n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865utqj5elvh
|
||||
ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9
|
||||
NETWORK_MONITORS_CONTRACT_ADDRESS=n1x5krtvyqklj360x38v62ze42g8s8trfsfqzlv8c9296chcpvqadssqnem5
|
||||
NODE_FAMILIES_CONTRACT_ADDRESS=n13clyapdqk5umyynp20kqwf59rxlwlp24yf2ltzasflhsdhrxq7fsahyr6z
|
||||
|
||||
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
|
||||
NYXD=https://rpc.sandbox.nymtech.net
|
||||
|
||||
@@ -15,14 +15,25 @@ use std::time::Instant;
|
||||
|
||||
impl PeerRegistrator {
|
||||
/// In the case of an already registered WG peer, update its PSK.
|
||||
///
|
||||
/// The peer controller keeps the active config and the on-disk PSK in sync.
|
||||
pub(super) async fn update_peer_psk(
|
||||
&self,
|
||||
peer: PeerPublicKey,
|
||||
psk: Key,
|
||||
) -> Result<(), GatewayWireguardError> {
|
||||
self.peer_manager.update_peer_psk(peer, psk).await
|
||||
// 1. check if the peer is currently being handled
|
||||
if self.peer_manager.check_active_peer(peer).await? {
|
||||
// 2. if so, force disconnect it (as we're handling new request from the same peer)
|
||||
self.peer_manager.remove_peer(peer).await?;
|
||||
}
|
||||
|
||||
// 3. update the on-disk PSK
|
||||
let encoded_psk = psk.to_lower_hex();
|
||||
self.ecash_verifier
|
||||
.storage()
|
||||
.update_peer_psk(&peer.to_string(), Some(&encoded_psk))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn lp_peer_to_final_response(
|
||||
|
||||
@@ -125,44 +125,6 @@ impl PeerManager {
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn update_peer_psk(
|
||||
&self,
|
||||
pub_key: PeerPublicKey,
|
||||
psk: Key,
|
||||
) -> Result<(), GatewayWireguardError> {
|
||||
let controller_start = Instant::now();
|
||||
let peer_key = Key::new(pub_key.to_bytes());
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
let msg = PeerControlRequest::UpdatePeerPsk {
|
||||
peer_key,
|
||||
psk,
|
||||
response_tx,
|
||||
};
|
||||
self.wireguard_gateway_data
|
||||
.peer_tx()
|
||||
.send(msg)
|
||||
.await
|
||||
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
|
||||
|
||||
let res = response_rx
|
||||
.await
|
||||
.map_err(|_| GatewayWireguardError::internal("no response for update peer psk"))?
|
||||
.map_err(|err| {
|
||||
GatewayWireguardError::InternalError(format!(
|
||||
"updating peer psk could not be performed: {err:?}"
|
||||
))
|
||||
});
|
||||
|
||||
let latency = controller_start.elapsed().as_secs_f64();
|
||||
add_histogram_obs!(
|
||||
"wg_peer_controller_channel_latency_seconds",
|
||||
latency,
|
||||
WG_CONTROLLER_LATENCY_BUCKETS
|
||||
);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn remove_peer(&self, pub_key: PeerPublicKey) -> Result<(), GatewayWireguardError> {
|
||||
let controller_start = Instant::now();
|
||||
let key = Key::new(pub_key.to_bytes());
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-api"
|
||||
version = "1.1.81"
|
||||
version = "1.1.80-no-gw-penalty"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
license = "GPL-3.0"
|
||||
|
||||
@@ -75,7 +75,11 @@ test-utils = []
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
vergen = { workspace = true, features = ["build", "git", "gitcl", "rustc", "cargo"] }
|
||||
vergen-gitcl = { workspace = true, default-features = false, features = [
|
||||
"build",
|
||||
"cargo",
|
||||
"rustc",
|
||||
] }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["vergen", "nym-http-api-client"]
|
||||
|
||||
@@ -5,21 +5,18 @@
|
||||
|
||||
use anyhow::{Context, bail};
|
||||
use std::{path::PathBuf, process::Command};
|
||||
use vergen::EmitBuilder;
|
||||
use vergen_gitcl::{BuildBuilder, CargoBuilder, Emitter, GitclBuilder, RustcBuilder};
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
build_go()?;
|
||||
generate_exit_policy_ports()?;
|
||||
|
||||
EmitBuilder::builder()
|
||||
.all_build()
|
||||
.all_git()
|
||||
.all_rustc()
|
||||
.all_cargo()
|
||||
Emitter::default()
|
||||
.add_instructions(&BuildBuilder::all_build()?)?
|
||||
.add_instructions(&CargoBuilder::all_cargo()?)?
|
||||
.add_instructions(&GitclBuilder::all_git()?)?
|
||||
.add_instructions(&RustcBuilder::all_rustc()?)?
|
||||
.emit()
|
||||
.context("failed to extract build metadata")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse PORT_MAPPINGS from network-tunnel-manager.sh and generate a sorted
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node"
|
||||
version = "1.33.0"
|
||||
version = "1.32.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0"
|
||||
|
||||
@@ -157,9 +157,6 @@ pub enum PrometheusMetric {
|
||||
#[strum(props(help = "The distribution of defguard peer creation time"))]
|
||||
WireguardDefguardPeerCreation,
|
||||
|
||||
#[strum(props(help = "The distribution of defguard peer psk update time"))]
|
||||
WireguardDefguardPeerPskUpdate,
|
||||
|
||||
#[strum(props(
|
||||
help = "The distribution of time it takes to verify a credential during peer registration"
|
||||
))]
|
||||
@@ -323,9 +320,6 @@ impl PrometheusMetric {
|
||||
PrometheusMetric::WireguardDefguardPeerCreation => {
|
||||
Metric::new_histogram(&name, help, Some(REG_LATENCY_BUCKETS))
|
||||
}
|
||||
PrometheusMetric::WireguardDefguardPeerPskUpdate => {
|
||||
Metric::new_histogram(&name, help, Some(REG_LATENCY_BUCKETS))
|
||||
}
|
||||
PrometheusMetric::DvpnAuthenticatorClientRegistrationMsg1 => {
|
||||
Metric::new_histogram(&name, help, Some(REG_LATENCY_BUCKETS))
|
||||
}
|
||||
@@ -458,7 +452,7 @@ mod tests {
|
||||
// a sanity check for anyone adding new metrics. if this test fails,
|
||||
// make sure any methods on `PrometheusMetric` enum don't need updating
|
||||
// or require custom Display impl
|
||||
assert_eq!(47, PrometheusMetric::COUNT)
|
||||
assert_eq!(46, PrometheusMetric::COUNT)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -693,12 +693,7 @@ pub struct MixnetDebug {
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub initial_connection_timeout: Duration,
|
||||
|
||||
/// Maximum number of packets buffered per egress connection awaiting a socket write.
|
||||
/// This is a short-term burst absorber, not a queue: buffer depth converts directly into
|
||||
/// added latency (roughly `depth / per-peer send rate`), so an oversized value is just
|
||||
/// bufferbloat. Once it fills, further packets for that peer are dropped rather than
|
||||
/// delayed, which is preferable in a mixnet where a packet held that long has already
|
||||
/// missed its usefulness window. Keep worst-case queuing well under the per-hop mix delay.
|
||||
/// Maximum number of packets that can be stored waiting to get sent to a particular connection.
|
||||
pub maximum_connection_buffer_size: usize,
|
||||
|
||||
/// Specify whether any framed packets between nodes should use the legacy format (v7)
|
||||
@@ -887,11 +882,9 @@ impl MixnetDebug {
|
||||
// which for all intents and purposes will never happen
|
||||
const DEFAULT_MAXIMUM_FORWARD_PACKET_DELAY: Duration = Duration::from_secs(10);
|
||||
const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: Duration = Duration::from_millis(10_000);
|
||||
const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = Duration::from_secs(16);
|
||||
const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = Duration::from_millis(300_000);
|
||||
const DEFAULT_INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(1_500);
|
||||
// small enough to keep worst-case egress queuing in the tens-of-ms range at a few thousand
|
||||
// pps per peer (vs. the old 2000, which was hundreds of ms of bufferbloat)
|
||||
const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 192;
|
||||
const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000;
|
||||
}
|
||||
|
||||
impl Default for MixnetDebug {
|
||||
|
||||
@@ -404,7 +404,7 @@ where
|
||||
};
|
||||
|
||||
// Connect to target gateway with timeout
|
||||
let mut stream = match timeout(Duration::from_secs(5), S::connect(target_addr)).await {
|
||||
let stream = match timeout(Duration::from_secs(5), S::connect(target_addr)).await {
|
||||
Ok(Ok(stream)) => stream,
|
||||
Ok(Err(e)) => {
|
||||
inc!("lp_forward_failed");
|
||||
@@ -422,16 +422,6 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
// Disable Nagle's algorithm: the forward stream carries small request/response
|
||||
// handshake packets, so we want them sent immediately rather than coalesced.
|
||||
if let Err(e) = stream.set_no_delay(true) {
|
||||
inc!("lp_forward_failed");
|
||||
return Err(LpHandlerError::ConnectionFailure {
|
||||
egress: target_addr,
|
||||
reason: format!("failed to set TCP_NODELAY: {e}"),
|
||||
});
|
||||
}
|
||||
|
||||
debug!("Opened persistent exit connection to {target_addr} for forwarding");
|
||||
self.exit_stream = Some((stream, target_addr));
|
||||
|
||||
|
||||
@@ -171,14 +171,6 @@ impl LpControlListener {
|
||||
}
|
||||
|
||||
fn handle_connection(&self, stream: tokio::net::TcpStream, remote_addr: SocketAddr) {
|
||||
// Disable Nagle's algorithm on the accepted socket so our responses are flushed
|
||||
// immediately rather than coalesced. This is the write side of every reply we send,
|
||||
// including handshake replies forwarded back to an entry gateway. Non-fatal: a valid
|
||||
// connection should still be served if the option can't be set.
|
||||
if let Err(e) = stream.set_nodelay(true) {
|
||||
warn!("failed to set TCP_NODELAY on accepted LP connection from {remote_addr}: {e}");
|
||||
}
|
||||
|
||||
if let Some(initiator_details) = self
|
||||
.nodes_handler_state
|
||||
.nodes
|
||||
|
||||
@@ -18,7 +18,6 @@ use nym_sphinx_types::{Delay, REPLAY_TAG_SIZE};
|
||||
use std::collections::HashMap;
|
||||
use std::mem;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::Instant;
|
||||
use tokio_util::codec::Framed;
|
||||
@@ -60,11 +59,6 @@ impl PendingReplayCheckPackets {
|
||||
self.packets.values().map(|v| v.len()).sum()
|
||||
}
|
||||
|
||||
/// Instant at which the currently-deferred batch must be flushed, or `None` if nothing is pending.
|
||||
fn flush_deadline(&self, deferral: Duration) -> Option<Instant> {
|
||||
(self.total_count() > 0).then(|| self.last_acquired_mutex + deferral)
|
||||
}
|
||||
|
||||
fn replay_tags(&self) -> HashMap<u32, Vec<&[u8; REPLAY_TAG_SIZE]>> {
|
||||
let mut replay_tags = HashMap::with_capacity(self.packets.len());
|
||||
'outer: for (rotation_id, packets) in &self.packets {
|
||||
@@ -295,7 +289,7 @@ impl ConnectionHandler {
|
||||
.processing_config
|
||||
.maximum_replay_detection_deferral;
|
||||
|
||||
let count_threshold = self.pending_packets.total_count()
|
||||
let count_threshold = self.pending_packets.packets.len()
|
||||
< self
|
||||
.shared
|
||||
.processing_config
|
||||
@@ -712,23 +706,13 @@ impl ConnectionHandler {
|
||||
) {
|
||||
let mut packets_processed: u64 = 0;
|
||||
loop {
|
||||
// make sure pending packets are not stuck in the queue if we don't get any more packets
|
||||
// from this sender
|
||||
let flush_deadline = self.pending_packets.flush_deadline(
|
||||
self.shared
|
||||
.processing_config
|
||||
.maximum_replay_detection_deferral,
|
||||
);
|
||||
|
||||
tokio::select! {
|
||||
biased;
|
||||
// 1. check for cancellation
|
||||
_ = self.shared.shutdown_token.cancelled() => {
|
||||
trace!("connection handler: received shutdown");
|
||||
Span::current().record("exit_reason", "shutdown");
|
||||
break
|
||||
}
|
||||
// 2. handle any incoming packet
|
||||
maybe_framed_nym_packet = mixnet_connection.next() => {
|
||||
match maybe_framed_nym_packet {
|
||||
Some(Ok(packet)) => {
|
||||
@@ -748,7 +732,7 @@ impl ConnectionHandler {
|
||||
);
|
||||
Span::current().record("exit_reason", "corrupted");
|
||||
Span::current().record("packets_processed", packets_processed);
|
||||
break
|
||||
return
|
||||
}
|
||||
None => {
|
||||
debug!(
|
||||
@@ -758,101 +742,14 @@ impl ConnectionHandler {
|
||||
);
|
||||
Span::current().record("exit_reason", "closed_by_remote");
|
||||
Span::current().record("packets_processed", packets_processed);
|
||||
break
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3. check for the deferred pending packets
|
||||
_ = async move {
|
||||
match flush_deadline {
|
||||
Some(d) => tokio::time::sleep_until(d).await,
|
||||
None => std::future::pending::<()>().await,
|
||||
}
|
||||
} => {
|
||||
self.handle_pending_packets_batch(Instant::now()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drain any packets still deferred for replay-checking so they are forwarded
|
||||
// rather than silently dropped when the connection closes, errors, or shuts down
|
||||
self.handle_pending_packets_batch(Instant::now()).await;
|
||||
|
||||
Span::current().record("packets_processed", packets_processed);
|
||||
debug!("exiting and closing connection");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use nym_sphinx_params::{PacketSize, PacketType};
|
||||
use nym_sphinx_types::{
|
||||
DESTINATION_ADDRESS_LENGTH, Destination, DestinationAddressBytes, IDENTIFIER_LENGTH,
|
||||
NODE_ADDRESS_LENGTH, Node, NodeAddressBytes, NymPacket, PrivateKey, PublicKey,
|
||||
};
|
||||
|
||||
fn random_pubkey() -> PublicKey {
|
||||
(&PrivateKey::random()).into()
|
||||
}
|
||||
|
||||
// Build a real sphinx packet whose first hop validates against `key`, then partially
|
||||
// unwrap it - enough to land one entry in the pending replay-check batch.
|
||||
fn pending_packet(key: &PrivateKey) -> PartialyUnwrappedPacketWithKeyRotation {
|
||||
let route = [
|
||||
Node::new(
|
||||
NodeAddressBytes::from_bytes([1u8; NODE_ADDRESS_LENGTH]),
|
||||
key.into(),
|
||||
),
|
||||
Node::new(
|
||||
NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]),
|
||||
random_pubkey(),
|
||||
),
|
||||
];
|
||||
let destination = Destination::new(
|
||||
DestinationAddressBytes::from_bytes([3u8; DESTINATION_ADDRESS_LENGTH]),
|
||||
[4u8; IDENTIFIER_LENGTH],
|
||||
);
|
||||
let delays: Vec<Delay> = std::iter::repeat_with(|| Delay::new_from_nanos(0))
|
||||
.take(route.len())
|
||||
.collect();
|
||||
let packet = NymPacket::sphinx_build(
|
||||
true,
|
||||
PacketSize::RegularPacket.payload_size(),
|
||||
b"x",
|
||||
&route,
|
||||
&destination,
|
||||
&delays,
|
||||
)
|
||||
.expect("failed to build test sphinx packet");
|
||||
let framed =
|
||||
FramedNymPacket::new(packet, PacketType::Mix, SphinxKeyRotation::Unknown, true);
|
||||
|
||||
PartiallyUnwrappedPacket::new(framed, key)
|
||||
.map_err(|(_, err)| err)
|
||||
.expect("failed to partially unwrap test packet")
|
||||
.with_key_rotation(0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_flush_deadline_when_nothing_pending() {
|
||||
let pending = PendingReplayCheckPackets::new();
|
||||
assert!(pending.flush_deadline(Duration::from_millis(50)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flush_deadline_is_batch_start_plus_deferral() {
|
||||
let key = PrivateKey::random();
|
||||
let mut pending = PendingReplayCheckPackets::new();
|
||||
|
||||
let batch_start = Instant::now();
|
||||
pending.push(batch_start, pending_packet(&key));
|
||||
|
||||
let deferral = Duration::from_millis(50);
|
||||
assert_eq!(
|
||||
pending.flush_deadline(deferral),
|
||||
Some(batch_start + deferral)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::Instant;
|
||||
use tracing::{debug, error, warn};
|
||||
use tracing::{debug, error};
|
||||
|
||||
pub(crate) mod final_hop;
|
||||
|
||||
@@ -192,12 +192,6 @@ impl SharedData {
|
||||
match accepted {
|
||||
Ok((socket, remote_addr)) => {
|
||||
debug!("accepted incoming mixnet connection from: {remote_addr}");
|
||||
// disable Nagle: mix packets are latency-sensitive and flushed one at a time.
|
||||
if let Err(err) = socket.set_nodelay(true) {
|
||||
warn!(
|
||||
"failed to set TCP_NODELAY on mixnet connection from {remote_addr}: {err}"
|
||||
);
|
||||
}
|
||||
let mut handler = ConnectionHandler::new(self, remote_addr);
|
||||
let join_handle =
|
||||
tokio::spawn(async move { handler.handle_connection(socket).await });
|
||||
|
||||
@@ -32,8 +32,7 @@ pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str =
|
||||
"n1x5krtvyqklj360x38v62ze42g8s8trfsfqzlv8c9296chcpvqadssqnem5";
|
||||
|
||||
// \/ TODO: this has to be updated once the contract is deployed
|
||||
pub(crate) const NODE_FAMILIES_CONTRACT_ADDRESS: &str =
|
||||
"n13clyapdqk5umyynp20kqwf59rxlwlp24yf2ltzasflhsdhrxq7fsahyr6z";
|
||||
pub(crate) const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
// -- Constructor functions --
|
||||
|
||||
@@ -214,10 +214,9 @@ The `family_members` map SHALL be keyed by `NodeId` alone; the value SHALL carry
|
||||
- reject `validity == 0` with `ZeroInvitationValidity`;
|
||||
- verify `node_id` refers to a currently-bonded, not-unbonding node in the mixnet contract via `MixnetContractQuerier::check_node_existence` (which returns `false` both when no bond exists and when the bond is in the unbonding state), failing with `NodeDoesntExist { node_id }` otherwise;
|
||||
- verify the node is not already in any family (see "A node belongs to at most one family");
|
||||
- reject `(family_id, node_id)` pairs that already have a **still-valid** pending invitation (`env.block.time.seconds() < existing.expires_at`) with `PendingInvitationAlreadyExists { family_id, node_id }`;
|
||||
- when a pending invitation for the pair exists but has already expired (`env.block.time.seconds() >= existing.expires_at`), archive it as `PastFamilyInvitation { invitation, status: Expired { at: env.block.time.seconds() } }` using the next free per-`(family, node)` archive slot, then let the fresh invitation supersede it;
|
||||
- persist a `FamilyInvitation` with `expires_at = env.block.time.seconds() + validity`;
|
||||
- emit a `family_invitation` event with `family_id`, `node_id`, `expires_at` (the same event whether or not it superseded an expired invitation).
|
||||
- reject `(family_id, node_id)` pairs that already have a pending invitation with `PendingInvitationAlreadyExists { family_id, node_id }`;
|
||||
- emit a `family_invitation` event with `family_id`, `node_id`, `expires_at`.
|
||||
|
||||
#### Scenario: Successful invitation persists with the computed expiry
|
||||
- **WHEN** family owner sends `InviteToFamily { node_id, validity_secs: Some(v) }` and all preconditions hold
|
||||
@@ -236,13 +235,9 @@ The `family_members` map SHALL be keyed by `NodeId` alone; the value SHALL carry
|
||||
- **WHEN** `InviteToFamily { node_id }` targets a `node_id` for which the mixnet contract's `check_node_existence` returns `false`
|
||||
- **THEN** the call fails with `NodeDoesntExist { node_id }`
|
||||
|
||||
#### Scenario: Duplicate still-valid pending invitation is rejected
|
||||
- **WHEN** family `F` already has a still-valid (not yet expired) pending invitation for node `n` and `InviteToFamily { node_id: n }` is sent again by `F`'s owner
|
||||
- **THEN** the call fails with `PendingInvitationAlreadyExists { family_id: F.id, node_id: n }` (the existing invitation is preserved and nothing is archived)
|
||||
|
||||
#### Scenario: Re-inviting after the previous invitation expired supersedes it
|
||||
- **WHEN** family `F` has a pending invitation for node `n` whose `expires_at <= env.block.time.seconds()` and `InviteToFamily { node_id: n }` is sent again by `F`'s owner
|
||||
- **THEN** the call succeeds: the stale invitation is archived under `past_family_invitations` with `status = Expired { at: env.block.time.seconds() }`, and a fresh `FamilyInvitation` for `(F.id, n)` is persisted with the newly computed `expires_at`
|
||||
#### Scenario: Duplicate pending invitation is rejected
|
||||
- **WHEN** family `F` already has a pending invitation for node `n` and `InviteToFamily { node_id: n }` is sent again by `F`'s owner
|
||||
- **THEN** the call fails with `PendingInvitationAlreadyExists { family_id: F.id, node_id: n }` (the existing invitation is preserved)
|
||||
|
||||
### Requirement: Acceptance and rejection of an invitation are gated on node control
|
||||
|
||||
@@ -293,7 +288,7 @@ A successful `AcceptFamilyInvitation` SHALL:
|
||||
- emit `family_invitation_rejected` or `family_invitation_revoked` respectively, with `family_id` and `node_id` attributes;
|
||||
- fail with `InvitationNotFound { family_id, node_id }` if no pending invitation exists.
|
||||
|
||||
The contract performs no background sweep of expired entries. Reject and revoke are the two *targeted* ways to clear a specific pending invitation; an expired one is additionally cleared if the family owner re-invites the same node (archiving the stale entry as `Expired { at: now }` before superseding it — see "Invitations require an existing family …") or if the family is disbanded.
|
||||
These two paths SHALL be the only ways to clear an expired invitation out of `pending_family_invitations` — the contract performs no background sweep of expired entries.
|
||||
|
||||
#### Scenario: Owner revokes a still-pending invitation
|
||||
- **WHEN** family owner sends `RevokeFamilyInvitation { node_id }` for a node currently in their pending invitations
|
||||
@@ -366,7 +361,7 @@ The auto-cleared invitations share the `Rejected` terminal state with invitation
|
||||
|
||||
### Requirement: Expired pending invitations remain in storage until explicitly cleared
|
||||
|
||||
The contract SHALL NOT run any background sweep of expired pending invitations. A `FamilyInvitation` whose `expires_at <= env.block.time.seconds()` SHALL remain in `pending_family_invitations` until either the family owner revokes it, the node controller rejects it, the family owner re-invites the same node (which archives the stale entry as `Expired { at: now }` and replaces it), or the family is disbanded. Accept attempts against such entries MUST fail with `InvitationExpired`. Read queries SHALL surface a boolean `expired` flag (`now >= invitation.expires_at`) on each returned `PendingFamilyInvitationDetails` so callers can filter without doing the comparison themselves.
|
||||
The contract SHALL NOT run any background sweep of expired pending invitations. A `FamilyInvitation` whose `expires_at <= env.block.time.seconds()` SHALL remain in `pending_family_invitations` until either the family owner revokes it, the node controller rejects it, or the family is disbanded. Accept attempts against such entries MUST fail with `InvitationExpired`. Read queries SHALL surface a boolean `expired` flag (`now >= invitation.expires_at`) on each returned `PendingFamilyInvitationDetails` so callers can filter without doing the comparison themselves.
|
||||
|
||||
#### Scenario: Expired invitation is still listed by pending queries
|
||||
- **WHEN** family `F` has a pending invitation for node `n` whose `expires_at` is in the past and `GetPendingInvitationsForFamilyPaged { family_id: F.id }` is queried
|
||||
|
||||
+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"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user