OverviewFilesCommitsIssuesDocsReleasesPeers

hypermosh-shell commits / 80aad7b

hypermosh-shell v0.0.1 — mosh over hyperdht
Tunnel mosh's UDP over a hole-punched, identity-keyed hyperdht connection so resilient/roaming terminal sessions work against NAT'd, Doze-prone P2P nodes (celltron) that a plain reliable-stream shell can't survive. Proven: relay core (UDP<->hyperdht, framed, auto-reconnect) + a real mosh session end-to-end over the tunnel (loopback, all hops + session output). CLI (id/serve/connect) uses the hyperident device identity + firewall. Roadmap: one-command wrapper, celltron deploy, flap-resilience test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
80aad7b9f37f651444592e1d38c2710e7b02a44e
by plankatron <plankatron@users.noreply.github.com> · 2026-07-14T19:53:16.000Z

Changed files (9) +1263 -0

.gitignore +3 -0
……@@ -0,0 +1,3 @@
1node_modules/
2*.log
3.DS_Store
README.md +132 -0
……@@ -0,0 +1,132 @@
1# hypermosh-shell
2
3**Resilient, roaming terminal sessions to identity-keyed P2P nodes** — real
4[mosh](https://mosh.org) tunneled over a hole-punched
5[hyperdht](https://github.com/holepunchto/hyperdht) connection.
6
7Built for **celltron-class nodes**: NAT'd, Doze-prone phones with no reachable
8UDP port, where a plain reliable-stream shell (like `hyperident-shell`) **dies on
9every network flap** — Doze, wifi handoff, cellular↔wifi roaming, a DHT hiccup.
10mosh was designed for exactly that world. hypermosh-shell gives mosh a P2P
11datagram path so its resilience works against a peer you reach *by public key*,
12not by `host:port`.
13
14## The idea
15
16```
17tabertron (client) celltron (server, in proot)
18 mosh-client ─UDP→ [hypermosh connect] ═hyperdht═ [hypermosh serve] ─UDP→ mosh-server
19 ←UDP─ (local 127.0.0.1 identity-keyed, (bridge to ←UDP─
20 udp port) firewall'd, 127.0.0.1:port)
21 auto-reconnect)
22```
23
24- mosh's **SSP** (State Synchronization Protocol) already survives packet loss,
25 latency, and IP roaming — it just needs *a* datagram path to the server.
26- celltron has no public UDP port, so we carry mosh's UDP datagrams over a
27 **hyperdht connection**: hole-punched, encrypted, addressed by the device's
28 hyperident **public key**, and firewalled by an allow-list — the same identity
29 model as `hyperident-shell`.
30- When the phone's network flaps, the hyperdht stream drops and the relay
31 **auto-reconnects** (re-looking-up the pubkey — which *is* roaming, P2P-style).
32 mosh sees a brief gap in datagrams and **resyncs the session**. Nothing lost.
33
34The relay itself is deliberately dumb: a UDP↔stream bridge with framing and
35reconnect. **mosh provides all the resilience**; hypermosh only provides the
36pipe.
37
38## Status
39
40| Piece | State |
41|---|---|
42| UDP↔hyperdht relay core (`lib/relay.mjs`) | ✅ **proven** (loopback round-trip) |
43| Real mosh session end-to-end over the tunnel | ✅ **proven** (loopback, all hops + session output) |
44| CLI (`hypermosh id/serve/connect`) | ✅ usable (manual bootstrap) |
45| One-command wrapper (`hypermosh <key>` auto-bootstraps mosh-server) | 🔜 roadmap |
46| celltron deploy (mosh-server in proot via extract-from-deb) | 🔜 roadmap |
47| Flap/roaming resilience test (kill+restore tunnel mid-session) | 🔜 roadmap |
48
49## Install
50
51```sh
52npm install # hyperdht, b4a, hypercore-id-encoding
53# requires `mosh` on both ends; on celltron, mosh-server goes in the proot rootfs
54```
55
56Dual-runtime: runs on **Node** (client side / testing) and **Bare** (celltron)
57via the `#node:dgram` import map (`dgram` on node, `bare-dgram` on bare).
58
59## Usage (manual bootstrap — mosh-style)
60
61mosh normally uses `ssh` to start `mosh-server` and hand back a one-time key.
62hypermosh uses **`hyperident-shell`** as that bootstrap channel.
63
64**1. Start `mosh-server` on the remote (via hyperident-shell), grab its port+key:**
65```sh
66hyperident-shell <celltron-key> -- mosh-server new -i 127.0.0.1 -- bash -l
67# -> MOSH CONNECT 60001 <base64-key>
68```
69
70**2. On the remote (celltron), bridge the tunnel to that port:**
71```sh
72hypermosh serve --udp 60001 --firewall ~/.hyperident-shell/firewall.txt
73# -> hypermosh serving as <celltron-key>
74```
75
76**3. On the client, open the tunnel and point mosh-client at it:**
77```sh
78hypermosh connect <celltron-key> --udp-listen 6000
79# -> point mosh-client at: 127.0.0.1 6000
80MOSH_KEY=<base64-key> mosh-client 127.0.0.1 6000
81```
82
83You now have a mosh session on celltron that **survives Doze, roaming, and DHT
84flaps** — the failure mode that makes a plain stream shell unusable on a phone.
85
86> The one-command `hypermosh <celltron-key>` (which does steps 1–3 for you and
87> launches mosh-client) is the next milestone — it just wires these primitives
88> together over a small control channel.
89
90## CLI
91
92```
93hypermosh id # print this device's pubkey (hyperident vault)
94hypermosh serve --udp <port> [--firewall <f> | --disable-firewall]
95hypermosh connect <server-pubkey> [--udp-listen <port>]
96```
97
98Identity comes from the hyperident vault (`~/.hyperident/device.key` + `.pub`) —
99the same identity as `hyperident-shell`, so the same firewall allow-list governs
100both.
101
102## Design notes
103
104- **Framing.** UDP datagrams are `[uint16 BE len][payload]` over the reliable
105 UDX byte stream, so they reassemble cleanly.
106- **No `writable` guard.** The client writes datagrams to the hyperdht stream
107 *without* checking `socket.writable` — UDX buffers pre-connection writes, so
108 mosh's earliest packets survive the ~seconds of DHT connect instead of being
109 dropped. (Dropping them was a real bug: mosh reported "Nothing received from
110 server" while the connection was still establishing.)
111- **Auto-reconnect = roaming.** On stream `close`, the client redials the
112 server pubkey. That reconnect is what turns a phone's network change into
113 mosh's roaming.
114
115## Tests
116
117```sh
118npm test
119# [1] loopback: UDP echo through hyperdht tunnel ✅
120# [2] real mosh: session through the hyperdht tunnel ✅ (skipped if mosh absent)
121```
122
123## Related
124
125- `hyperident-shell` — the identity-keyed P2P shell this complements (and uses to
126 bootstrap `mosh-server`). Stream-based, so fragile on flaky links — which is
127 the gap hypermosh fills.
128- `celltron` — the retired-phone P2P node this is built for.
129
130## License
131
132Apache-2.0
bin/hypermosh.mjs +73 -0
……@@ -0,0 +1,73 @@
1#!/usr/bin/env node
2// hypermosh — tunnel mosh's UDP over an identity-keyed hyperdht connection.
3//
4// hypermosh id
5// hypermosh serve --udp <mosh-server-port> [--firewall <file> | --disable-firewall]
6// hypermosh connect <server-pubkey> [--udp-listen <port>]
7//
8// Bootstrap flow (mosh-style, using hyperident-shell as the "ssh"):
9// 1) on the client, over hyperident-shell: mosh-server new -i 127.0.0.1
10// -> prints MOSH CONNECT <port> <key>
11// 2) on the server (celltron): hypermosh serve --udp <port>
12// 3) on the client: hypermosh connect <server-pubkey> --udp-listen 6000
13// 4) on the client: MOSH_KEY=<key> mosh-client 127.0.0.1 6000
14// See README for the details and the roadmap toward a one-command wrapper.
15import DHT from 'hyperdht'
16import process from '#node:process'
17import { serve, connect } from '../lib/relay.mjs'
18import { loadDeviceKeyPair, readAllowed, HypercoreId } from '../lib/identity.mjs'
19
20function flags (argv) {
21 const pos = []
22 const f = {}
23 for (let i = 0; i < argv.length; i++) {
24 const a = argv[i]
25 if (a.startsWith('--')) { const k = a.slice(2); const v = (argv[i + 1] && !argv[i + 1].startsWith('--')) ? argv[++i] : true; f[k] = v }
26 else pos.push(a)
27 }
28 return { pos, f }
29}
30
31function usage () {
32 console.error('usage:\n hypermosh id\n hypermosh serve --udp <port> [--firewall <file> | --disable-firewall]\n hypermosh connect <server-pubkey> [--udp-listen <port>]')
33 process.exit(1)
34}
35
36async function main () {
37 const [cmd, ...rest] = process.argv.slice(2)
38 const { pos, f } = flags(rest)
39
40 if (cmd === 'id') {
41 console.log(loadDeviceKeyPair().pub)
42 return
43 }
44
45 if (cmd === 'serve') {
46 if (!f.udp) usage()
47 const keyPair = loadDeviceKeyPair()
48 const allow = f['disable-firewall'] ? true : readAllowed(f.firewall)
49 const s = await serve({ keyPair, udpPort: Number(f.udp), allow })
50 console.log('hypermosh serving as ' + HypercoreId.encode(s.publicKey))
51 console.log(' bridging hyperdht <-> 127.0.0.1:' + f.udp + ' (mosh-server)')
52 console.log(' firewall: ' + (allow === true ? 'DISABLED (any peer)' : (allow.length + ' allowed key(s)')))
53 process.on('SIGINT', async () => { await s.close(); process.exit(0) })
54 return
55 }
56
57 if (cmd === 'connect') {
58 if (!pos[0]) usage()
59 const remoteKey = HypercoreId.decode(pos[0])
60 const keyPair = loadDeviceKeyPair() // present our device identity to the server firewall
61 const localPort = f['udp-listen'] ? Number(f['udp-listen']) : 0
62 const c = await connect({ remoteKey, keyPair, localPort })
63 console.log('hypermosh tunnel up.')
64 console.log(' point mosh-client at: 127.0.0.1 ' + c.localPort)
65 console.log(' e.g.: MOSH_KEY=<key> mosh-client 127.0.0.1 ' + c.localPort)
66 process.on('SIGINT', async () => { await c.close(); process.exit(0) })
67 return
68 }
69
70 usage()
71}
72
73main().catch((e) => { console.error('hypermosh: ' + e.message); process.exit(1) })
lib/identity.mjs +41 -0
……@@ -0,0 +1,41 @@
1// identity — load the hyperident device keyPair (same vault as hyperident-shell)
2// so a hypermosh tunnel is addressed by the device pubkey and firewalled by the
3// same allow-list. Reads only via `#node:fs` (bare-fs on bare).
4import fs from '#node:fs'
5import os from '#node:os'
6import path from '#node:path'
7import b4a from 'b4a'
8import HypercoreId from 'hypercore-id-encoding'
9
10function homedir () { return os.homedir ? os.homedir() : (process.env.HOME || '/root') }
11const DEFAULT_VAULT = () => path.join(homedir(), '.hyperident')
12
13// device.key = 64-byte libsodium secret (hex), device.pub = 32-byte public (hex).
14// The 64-byte secret is itself a valid hyperdht keyPair — pass straight to
15// server.listen / node.connect.
16export function loadDeviceKeyPair (vault = DEFAULT_VAULT()) {
17 let secretHex, publicHex
18 try {
19 secretHex = fs.readFileSync(path.join(vault, 'device.key'), 'utf8').trim()
20 publicHex = fs.readFileSync(path.join(vault, 'device.pub'), 'utf8').trim()
21 } catch (e) {
22 if (e.code === 'ENOENT') throw new Error('no hyperident device identity at ' + vault + ' — run `pgh init` first')
23 throw e
24 }
25 const secretKey = b4a.from(secretHex, 'hex')
26 const publicKey = b4a.from(publicHex, 'hex')
27 if (secretKey.length !== 64 || publicKey.length !== 32) {
28 throw new Error('unexpected device key sizes (secret=' + secretKey.length + ' public=' + publicKey.length + ')')
29 }
30 return { publicKey, secretKey, pub: HypercoreId.encode(publicKey) }
31}
32
33// Read an allow-list firewall file (one z-base32/hex key per line, '#' comments).
34export function readAllowed (firewallPath) {
35 if (!firewallPath) return []
36 let txt
37 try { txt = fs.readFileSync(firewallPath, 'utf8') } catch (e) { if (e.code === 'ENOENT') return []; throw e }
38 return txt.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#')).map((k) => HypercoreId.decode(k))
39}
40
41export { HypercoreId }
lib/relay.mjs +103 -0
……@@ -0,0 +1,103 @@
1// relay — tunnel UDP datagrams over a hyperdht connection.
2//
3// The whole point: mosh's SSP already survives packet loss, latency, and
4// roaming — it just needs *a* datagram path to the server. celltron (a NAT'd,
5// Doze-prone phone) has no reachable UDP port, so we carry mosh's UDP over a
6// hole-punched, identity-keyed hyperdht connection. mosh provides the
7// resilience; this relay is a dumb UDP<->stream bridge with auto-reconnect
8// (which becomes mosh's roaming when the phone's network flaps).
9//
10// Dual-runtime (node + bare) via the `#node:dgram` import map.
11
12import DHT from 'hyperdht'
13import dgram from '#node:dgram'
14import b4a from 'b4a'
15
16// Datagrams are length-prefixed ([uint16 BE len][payload]) so they reassemble
17// cleanly out of the reliable UDX byte stream.
18export function makeFramer (onDatagram) {
19 let buf = b4a.alloc(0)
20 return (chunk) => {
21 buf = b4a.concat([buf, chunk])
22 while (buf.length >= 2) {
23 const len = buf.readUInt16BE(0)
24 if (buf.length < 2 + len) break
25 onDatagram(buf.subarray(2, 2 + len))
26 buf = buf.subarray(2 + len)
27 }
28 }
29}
30
31export function frame (dg) {
32 const h = b4a.alloc(2)
33 h.writeUInt16BE(dg.length, 0)
34 return b4a.concat([h, dg])
35}
36
37// SERVER side (runs next to mosh-server, e.g. on celltron): accept identity-
38// firewalled hyperdht connections; bridge each to the local mosh-server UDP
39// port. `allow` is `true` (any) or an array of allowed public keys.
40export async function serve ({ keyPair, udpHost = '127.0.0.1', udpPort, allow = true, node = null } = {}) {
41 if (!udpPort) throw new Error('serve requires udpPort (the local mosh-server UDP port)')
42 const dht = node || new DHT()
43
44 const firewall = (remotePublicKey) =>
45 !(allow === true || allow.some((k) => b4a.equals(remotePublicKey, k))) // true = DENY
46
47 const server = dht.createServer({ firewall }, (socket) => {
48 const udp = dgram.createSocket('udp4')
49 // mosh-server -> tunnel
50 udp.on('message', (msg) => { try { socket.write(frame(msg)) } catch {} })
51 // tunnel -> mosh-server
52 socket.on('data', makeFramer((dg) => { try { udp.send(dg, udpPort, udpHost) } catch {} }))
53 socket.on('error', () => {})
54 socket.on('close', () => { try { udp.close() } catch {} })
55 })
56
57 await server.listen(keyPair)
58 return {
59 dht,
60 server,
61 publicKey: keyPair.publicKey,
62 async close () { await server.close(); if (!node) await dht.destroy() }
63 }
64}
65
66// CLIENT side (runs next to mosh-client): expose a local UDP port that
67// mosh-client targets; bridge it to the remote server over hyperdht. The peer
68// (mosh-client) UDP address is learned from its first packet. On stream close
69// we redial — that reconnect IS mosh's roaming across network changes.
70//
71// NOTE: writes are NOT guarded on socket.writable — UDX buffers pre-connection
72// writes, so mosh's earliest packets survive the ~seconds of DHT connect
73// instead of being dropped (the bug that made an early prototype time out).
74export async function connect ({ remoteKey, keyPair = null, localHost = '127.0.0.1', localPort = 0, node = null, reconnectMs = 500 } = {}) {
75 const dht = node || new DHT()
76 const udp = dgram.createSocket('udp4')
77 let peer = null
78 let socket = null
79 let closed = false
80
81 const dial = () => {
82 if (closed) return
83 socket = dht.connect(remoteKey, keyPair ? { keyPair } : undefined)
84 socket.on('data', makeFramer((dg) => { if (peer) { try { udp.send(dg, peer.port, peer.address) } catch {} } }))
85 socket.on('error', () => {})
86 socket.on('close', () => { socket = null; if (!closed) setTimeout(dial, reconnectMs) })
87 }
88
89 udp.on('message', (msg, rinfo) => {
90 peer = rinfo
91 if (socket) { try { socket.write(frame(msg)) } catch {} } // buffers until connected
92 })
93
94 await new Promise((resolve) => udp.bind(localPort, localHost, resolve))
95 dial()
96
97 return {
98 dht,
99 udp,
100 localPort: udp.address().port,
101 async close () { closed = true; try { udp.close() } catch {}; if (socket) socket.destroy(); if (!node) await dht.destroy() }
102 }
103}
package-lock.json +774 -0
……@@ -0,0 +1,774 @@
1{
2 "name": "hypermosh-shell",
3 "version": "0.0.1",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "hypermosh-shell",
9 "version": "0.0.1",
10 "license": "Apache-2.0",
11 "dependencies": {
12 "b4a": "^1.6.0",
13 "hypercore-id-encoding": "^1.2.0",
14 "hyperdht": "^6.6.0"
15 },
16 "bin": {
17 "hypermosh": "bin/hypermosh.mjs"
18 },
19 "optionalDependencies": {
20 "bare-dgram": "^1.0.0",
21 "bare-fs": "^4.0.0",
22 "bare-os": "^3.0.0",
23 "bare-path": "^3.0.0",
24 "bare-process": "^4.0.0"
25 }
26 },
27 "node_modules/@hyperswarm/secret-stream": {
28 "version": "6.9.1",
29 "resolved": "https://registry.npmjs.org/@hyperswarm/secret-stream/-/secret-stream-6.9.1.tgz",
30 "integrity": "sha512-xb0S5y3YJwBakD77JOGBHlBxdp63mHClZoXBYoLv+9wH8e054ESKlmQptWqjJK5dv5VMUIVYOJB4MaOpB0JdGw==",
31 "license": "Apache-2.0",
32 "dependencies": {
33 "b4a": "^1.1.0",
34 "hypercore-crypto": "^3.3.1",
35 "noise-curve-ed": "^2.0.1",
36 "noise-handshake": "^4.0.0",
37 "sodium-secretstream": "^1.1.0",
38 "sodium-universal": "^5.0.0",
39 "streamx": "^2.14.0",
40 "timeout-refresh": "^2.0.0",
41 "unslab": "^1.3.0"
42 }
43 },
44 "node_modules/adaptive-timeout": {
45 "version": "1.0.1",
46 "resolved": "https://registry.npmjs.org/adaptive-timeout/-/adaptive-timeout-1.0.1.tgz",
47 "integrity": "sha512-+seGiBtHqbnyZ0Quq/ZGxxwP66153WBABawa07/QeyuPFzbduPBjiGQAyCiriiLDzt3dkbMBskSlfqtRwblK8Q==",
48 "license": "Apache-2.0",
49 "dependencies": {
50 "xache": "^1.2.1"
51 }
52 },
53 "node_modules/b4a": {
54 "version": "1.8.1",
55 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
56 "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
57 "license": "Apache-2.0",
58 "peerDependencies": {
59 "react-native-b4a": "*"
60 },
61 "peerDependenciesMeta": {
62 "react-native-b4a": {
63 "optional": true
64 }
65 }
66 },
67 "node_modules/bare-abort": {
68 "version": "2.0.13",
69 "resolved": "https://registry.npmjs.org/bare-abort/-/bare-abort-2.0.13.tgz",
70 "integrity": "sha512-zdc8l88eB11Jsz5rDd6sCAgv2kUFXgdrZWoMlgU6JMkfAi1/uuGFC3IEHswKbIRQTk5H3T5CMuechsXYxiaHlQ==",
71 "license": "Apache-2.0",
72 "optional": true
73 },
74 "node_modules/bare-addon-resolve": {
75 "version": "1.10.0",
76 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
77 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
78 "license": "Apache-2.0",
79 "dependencies": {
80 "bare-module-resolve": "^1.10.0",
81 "bare-semver": "^1.0.0"
82 },
83 "peerDependencies": {
84 "bare-url": "*"
85 },
86 "peerDependenciesMeta": {
87 "bare-url": {
88 "optional": true
89 }
90 }
91 },
92 "node_modules/bare-ansi-escapes": {
93 "version": "2.2.3",
94 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
95 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
96 "license": "Apache-2.0",
97 "dependencies": {
98 "bare-stream": "^2.6.5"
99 },
100 "peerDependencies": {
101 "bare-buffer": "*"
102 },
103 "peerDependenciesMeta": {
104 "bare-buffer": {
105 "optional": true
106 }
107 }
108 },
109 "node_modules/bare-assert": {
110 "version": "1.2.0",
111 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
112 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
113 "license": "Apache-2.0",
114 "dependencies": {
115 "bare-inspect": "^3.1.2"
116 }
117 },
118 "node_modules/bare-dgram": {
119 "version": "1.0.1",
120 "resolved": "https://registry.npmjs.org/bare-dgram/-/bare-dgram-1.0.1.tgz",
121 "integrity": "sha512-EdsyRErrkWgN8fENdrDdXFEE9HAuJ/m6ehXz13fVj9JhdCaLWIA+L8o5aYNRLt66x08RlyG2vbrRAZoxGfcdlg==",
122 "license": "Apache-2.0",
123 "optional": true,
124 "dependencies": {
125 "bare-events": "^2.5.0",
126 "udx-native": "^1.11.2"
127 }
128 },
129 "node_modules/bare-env": {
130 "version": "3.0.1",
131 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.1.tgz",
132 "integrity": "sha512-BdoLvnzaWR0YyyJreEx4zG2Od0AC/s9JSZ+EXyMZKunpn8zEgR/dGv37hdFUaY1bjDkcRKvcKkFyJIwlJ0CaYA==",
133 "license": "Apache-2.0",
134 "optional": true,
135 "dependencies": {
136 "bare-os": "^3.0.1"
137 }
138 },
139 "node_modules/bare-events": {
140 "version": "2.9.1",
141 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz",
142 "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==",
143 "license": "Apache-2.0",
144 "peerDependencies": {
145 "bare-abort-controller": "*"
146 },
147 "peerDependenciesMeta": {
148 "bare-abort-controller": {
149 "optional": true
150 }
151 }
152 },
153 "node_modules/bare-fs": {
154 "version": "4.7.4",
155 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz",
156 "integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==",
157 "license": "Apache-2.0",
158 "dependencies": {
159 "bare-events": "^2.5.4",
160 "bare-path": "^3.0.0",
161 "bare-stream": "^2.6.4",
162 "bare-url": "^2.2.2",
163 "fast-fifo": "^1.3.2"
164 },
165 "engines": {
166 "bare": ">=1.16.0"
167 },
168 "peerDependencies": {
169 "bare-buffer": "*"
170 },
171 "peerDependenciesMeta": {
172 "bare-buffer": {
173 "optional": true
174 }
175 }
176 },
177 "node_modules/bare-hrtime": {
178 "version": "2.1.1",
179 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
180 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
181 "license": "Apache-2.0",
182 "optional": true
183 },
184 "node_modules/bare-inspect": {
185 "version": "3.1.4",
186 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
187 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
188 "license": "Apache-2.0",
189 "dependencies": {
190 "bare-ansi-escapes": "^2.1.0",
191 "bare-type": "^1.0.0"
192 },
193 "engines": {
194 "bare": ">=1.18.0"
195 }
196 },
197 "node_modules/bare-module-resolve": {
198 "version": "1.12.2",
199 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.2.tgz",
200 "integrity": "sha512-j+hiD5k99qec4KjJvYsI67q5AOBifmy9JG3oeMVxTmvrhn2sIdp8StrUvZu4YNgwTpO+NhniQG16N1ETDe1k5w==",
201 "license": "Apache-2.0",
202 "dependencies": {
203 "bare-semver": "^1.0.0"
204 },
205 "peerDependencies": {
206 "bare-url": "*"
207 },
208 "peerDependenciesMeta": {
209 "bare-url": {
210 "optional": true
211 }
212 }
213 },
214 "node_modules/bare-os": {
215 "version": "3.9.3",
216 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.3.tgz",
217 "integrity": "sha512-fF4Q7QsyKVF5Rj0qvI8BgUNjqzC2JvQlpTaPLjVJVxYVUX5Zr9un+y3w1HmA4nNKdFmRBT8z/WmrjvXzXVerKQ==",
218 "license": "Apache-2.0",
219 "optional": true,
220 "engines": {
221 "bare": ">=1.14.0"
222 }
223 },
224 "node_modules/bare-path": {
225 "version": "3.1.1",
226 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz",
227 "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==",
228 "license": "Apache-2.0"
229 },
230 "node_modules/bare-pipe": {
231 "version": "4.2.2",
232 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.2.2.tgz",
233 "integrity": "sha512-Wkf6//CmKS8inHrkgi3X/xNMeaGfs2I0ayLM0B8cbPB4DxYHcoes6Ng+muCBfXNIJ9qmEjwDDJjAUz7nRxxjgQ==",
234 "license": "Apache-2.0",
235 "optional": true,
236 "dependencies": {
237 "bare-events": "^2.0.0",
238 "bare-stream": "^2.0.0"
239 },
240 "engines": {
241 "bare": ">=1.16.0"
242 }
243 },
244 "node_modules/bare-posix": {
245 "version": "1.0.1",
246 "resolved": "https://registry.npmjs.org/bare-posix/-/bare-posix-1.0.1.tgz",
247 "integrity": "sha512-b4quA8dyRVvX0aFIAIiev5zFATHwIUGr42Gmt+CqiBNg/bdsNkrj4bbkeotMOMiljJS4DCME5kywJO/TFwKo1A==",
248 "license": "Apache-2.0",
249 "optional": true
250 },
251 "node_modules/bare-process": {
252 "version": "4.5.1",
253 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.5.1.tgz",
254 "integrity": "sha512-CaAvy1trputD49mtwfJ6G75vydhnirLrW/F3Sznp4H556e1uZn8YMo9ELicBTrGYy7RBNUgPl9bpB/ERzRCiDw==",
255 "license": "Apache-2.0",
256 "optional": true,
257 "dependencies": {
258 "bare-abort": "^2.0.13",
259 "bare-env": "^3.0.0",
260 "bare-events": "^2.3.1",
261 "bare-hrtime": "^2.0.0",
262 "bare-os": "^3.7.1",
263 "bare-posix": "^1.0.1",
264 "bare-signals": "^5.0.0",
265 "bare-stdio": "^1.0.1"
266 }
267 },
268 "node_modules/bare-semver": {
269 "version": "1.1.0",
270 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.1.0.tgz",
271 "integrity": "sha512-1Hw5qJ7hXdVt3uPUqjeFTuxyvBUJauvz5A1I2jk8gzjZMHp04n//6nV9MDbG9CMw78JHY2lGV0w6s//LrASm2w==",
272 "license": "Apache-2.0"
273 },
274 "node_modules/bare-signals": {
275 "version": "5.0.0",
276 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-5.0.0.tgz",
277 "integrity": "sha512-8Gn8bBFUh2AUCJ9wWWFjSGWIo1HIUSIQnXRJGSm/f7GCDMqsuJhRmR1dT+HtDaDBkeu2l8Koxt2JLurfJ5yHVw==",
278 "license": "Apache-2.0",
279 "optional": true,
280 "dependencies": {
281 "bare-events": "^2.5.3"
282 },
283 "engines": {
284 "bare": ">=1.7.0"
285 }
286 },
287 "node_modules/bare-stdio": {
288 "version": "1.0.3",
289 "resolved": "https://registry.npmjs.org/bare-stdio/-/bare-stdio-1.0.3.tgz",
290 "integrity": "sha512-8BRMx7RWMWCbBmKhyHgBco62J+Pgss7+EPI21L3R9+w0UpwDYWUUcbE0YTOesGyt7M6N7VtoshYbzwrP0pI52Q==",
291 "license": "Apache-2.0",
292 "optional": true,
293 "dependencies": {
294 "bare-fs": "^4.5.2",
295 "bare-pipe": "^4.1.5",
296 "bare-tty": "^5.0.3"
297 }
298 },
299 "node_modules/bare-stream": {
300 "version": "2.13.3",
301 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz",
302 "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==",
303 "license": "Apache-2.0",
304 "dependencies": {
305 "b4a": "^1.8.1",
306 "streamx": "^2.25.0",
307 "teex": "^1.0.1"
308 },
309 "peerDependencies": {
310 "bare-abort-controller": "*",
311 "bare-buffer": "*",
312 "bare-events": "*"
313 },
314 "peerDependenciesMeta": {
315 "bare-abort-controller": {
316 "optional": true
317 },
318 "bare-buffer": {
319 "optional": true
320 },
321 "bare-events": {
322 "optional": true
323 }
324 }
325 },
326 "node_modules/bare-tty": {
327 "version": "5.1.2",
328 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.1.2.tgz",
329 "integrity": "sha512-wDHPU/yVQtrDCUrKVdIVOLmfIw7dFPETz30UayuFlROvgz8Juv2SmISSMhqxYQVO+fVnG/1ZoLcbGh/35eAjDA==",
330 "license": "Apache-2.0",
331 "optional": true,
332 "dependencies": {
333 "bare-events": "^2.2.0",
334 "bare-signals": "^5.0.0",
335 "bare-stream": "^2.0.0"
336 },
337 "engines": {
338 "bare": ">=1.16.0"
339 }
340 },
341 "node_modules/bare-type": {
342 "version": "1.1.0",
343 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
344 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
345 "license": "Apache-2.0",
346 "engines": {
347 "bare": ">=1.2.0"
348 }
349 },
350 "node_modules/bare-url": {
351 "version": "2.4.5",
352 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz",
353 "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==",
354 "license": "Apache-2.0",
355 "peer": true,
356 "dependencies": {
357 "bare-path": "^3.0.0"
358 }
359 },
360 "node_modules/bits-to-bytes": {
361 "version": "1.3.0",
362 "resolved": "https://registry.npmjs.org/bits-to-bytes/-/bits-to-bytes-1.3.0.tgz",
363 "integrity": "sha512-OJoHTpFXS9bXHBCekGTByf3MqM8CGblBDIduKQeeVVeiU9dDWywSSirXIBYGgg3d1zbVuvnMa1vD4r6PA0kOKg==",
364 "license": "ISC",
365 "dependencies": {
366 "b4a": "^1.5.0"
367 }
368 },
369 "node_modules/blind-relay": {
370 "version": "1.6.1",
371 "resolved": "https://registry.npmjs.org/blind-relay/-/blind-relay-1.6.1.tgz",
372 "integrity": "sha512-38YmKCl/m9NcTkwueBbcGIt9Zx3IT/Q9Nsluu6C5Gur6PqfE0zWPhPwCzia1hri/g0ICYxrqkgLtEaoWD5WeSQ==",
373 "license": "Apache-2.0",
374 "dependencies": {
375 "b4a": "^1.6.4",
376 "bare-events": "^2.2.0",
377 "bits-to-bytes": "^1.3.0",
378 "compact-encoding": "^3.0.0",
379 "compact-encoding-bitfield": "^1.0.0",
380 "protomux": "^3.5.1",
381 "sodium-universal": "^5.0.0",
382 "streamx": "^2.15.1"
383 }
384 },
385 "node_modules/bogon": {
386 "version": "1.3.0",
387 "resolved": "https://registry.npmjs.org/bogon/-/bogon-1.3.0.tgz",
388 "integrity": "sha512-04tu0G/v0f2HPuQlSfhO635WwHDBCaITJ490rhxH9ttUlgPqOirZVKV02YB6NvPf5VkmbqJCm3bnZwrPk/eDSg==",
389 "license": "MIT",
390 "dependencies": {
391 "compact-encoding": "^3.0.0",
392 "compact-encoding-net": "^1.2.0"
393 }
394 },
395 "node_modules/compact-encoding": {
396 "version": "3.3.0",
397 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-3.3.0.tgz",
398 "integrity": "sha512-e64XyzlBvTRJ3iScuU/U2w25Dglkm7fhrRbrGaHIwuTlNnzjRKMaQBCVl7mJRvZg7wI5a9wX1IVTXCuaAANNkw==",
399 "license": "Apache-2.0",
400 "dependencies": {
401 "b4a": "^1.3.0"
402 }
403 },
404 "node_modules/compact-encoding-bitfield": {
405 "version": "1.1.0",
406 "resolved": "https://registry.npmjs.org/compact-encoding-bitfield/-/compact-encoding-bitfield-1.1.0.tgz",
407 "integrity": "sha512-F6wliSKHi50BsBStmnsRJAzJgYSIYzdfSe3M0oHj4uQ1RcctJK/NQVD7wF51bj/DBMne2i5gUxrGUFkNZQns5w==",
408 "license": "Apache-2.0",
409 "dependencies": {
410 "compact-encoding": "^3.0.0"
411 }
412 },
413 "node_modules/compact-encoding-net": {
414 "version": "1.3.0",
415 "resolved": "https://registry.npmjs.org/compact-encoding-net/-/compact-encoding-net-1.3.0.tgz",
416 "integrity": "sha512-HIYjL3aMiSENApR691aMLngXt6rkTHb9HUkEukcx3YwIZpoMUVXHXyjBzoo9kVwC7KakooBA1RRWb46Cu6qtAQ==",
417 "license": "Apache-2.0",
418 "dependencies": {
419 "compact-encoding": "^3.0.0"
420 }
421 },
422 "node_modules/dht-rpc": {
423 "version": "6.27.0",
424 "resolved": "https://registry.npmjs.org/dht-rpc/-/dht-rpc-6.27.0.tgz",
425 "integrity": "sha512-NsfgRlFDQnkA2+H+hJXMPLmBOn/TSIIc0cRMV8q42M4xc1uAXWtpvIIQsONF5tYcYC6px0bslrUGideN1h4S4A==",
426 "license": "MIT",
427 "dependencies": {
428 "adaptive-timeout": "^1.0.1",
429 "b4a": "^1.6.1",
430 "bare-events": "^2.2.0",
431 "compact-encoding": "^3.0.0",
432 "compact-encoding-net": "^1.2.0",
433 "fast-fifo": "^1.1.0",
434 "kademlia-routing-table": "^1.0.1",
435 "nat-sampler": "^1.0.1",
436 "sodium-universal": "^5.0.0",
437 "streamx": "^2.13.2",
438 "time-ordered-set": "^2.0.0",
439 "udx-native": "^1.5.3"
440 }
441 },
442 "node_modules/events-universal": {
443 "version": "1.0.1",
444 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
445 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
446 "license": "Apache-2.0",
447 "dependencies": {
448 "bare-events": "^2.7.0"
449 }
450 },
451 "node_modules/fast-fifo": {
452 "version": "1.3.2",
453 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
454 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
455 "license": "MIT"
456 },
457 "node_modules/generate-object-property": {
458 "version": "2.0.0",
459 "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-2.0.0.tgz",
460 "integrity": "sha512-KwuURPyqn2Mz8DdV29pJwQu0Y7tcsbkULr82eeOcY/ZllFK6I9Wm8dsRByIu7CKWlFi9BdW1b3mcXMp/kQBQsw==",
461 "license": "MIT",
462 "dependencies": {
463 "is-property": "^1.0.0"
464 }
465 },
466 "node_modules/generate-string": {
467 "version": "1.0.1",
468 "resolved": "https://registry.npmjs.org/generate-string/-/generate-string-1.0.1.tgz",
469 "integrity": "sha512-IfTY0dKZM43ACyGvXkbG7De7WY7MxTS5VO6Juhe8oJKpCmrYYXoqp/cJMskkpi0k9H8wuXq0H+eI898/BCqvXg==",
470 "license": "MIT"
471 },
472 "node_modules/hypercore-crypto": {
473 "version": "3.7.0",
474 "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.7.0.tgz",
475 "integrity": "sha512-SWxobptQf2V/+ZLCCDRTXqFzGfTPIkNIuDyLKXpEMfcpJ1/ec94N9aWgylHcWOHmRt14ng/KR7z0BugebWHK9Q==",
476 "license": "MIT",
477 "dependencies": {
478 "b4a": "^1.6.6",
479 "compact-encoding": "^3.0.0",
480 "sodium-universal": "^5.0.0"
481 }
482 },
483 "node_modules/hypercore-id-encoding": {
484 "version": "1.3.0",
485 "resolved": "https://registry.npmjs.org/hypercore-id-encoding/-/hypercore-id-encoding-1.3.0.tgz",
486 "integrity": "sha512-W6sHdGo5h7LXEsoWfKf/KfuROZmZRQDlGqJF2EPHW+noCK66Vvr0+zE6cL0vqQi18s0kQPeN7Sq3QyR0Ytc2VQ==",
487 "license": "Apache-2.0",
488 "dependencies": {
489 "b4a": "^1.5.3",
490 "z32": "^1.0.0"
491 }
492 },
493 "node_modules/hyperdht": {
494 "version": "6.33.0",
495 "resolved": "https://registry.npmjs.org/hyperdht/-/hyperdht-6.33.0.tgz",
496 "integrity": "sha512-veSvVKptjPTu2di3q5IWI62ZDFzEmTj1QSTAkiXW/cg0+lgMUlwfVWB4dX+WI14xNb54vogVIjA/Ph3KiVuRJQ==",
497 "license": "MIT",
498 "dependencies": {
499 "@hyperswarm/secret-stream": "^6.6.2",
500 "b4a": "^1.3.1",
501 "bare-events": "^2.2.0",
502 "blind-relay": "^1.3.0",
503 "bogon": "^1.0.0",
504 "compact-encoding": "^3.0.0",
505 "dht-rpc": "^6.15.1",
506 "hypercore-crypto": "^3.3.0",
507 "hypercore-id-encoding": "^1.2.0",
508 "hyperdht-address": "^1.0.1",
509 "noise-curve-ed": "^2.0.0",
510 "noise-handshake": "^4.0.0",
511 "record-cache": "^1.1.1",
512 "safety-catch": "^1.0.1",
513 "signal-promise": "^1.0.3",
514 "sodium-universal": "^5.0.1",
515 "streamx": "^2.16.1",
516 "unslab": "^1.3.0",
517 "xache": "^1.1.0"
518 },
519 "bin": {
520 "hyperdht": "bin.js"
521 }
522 },
523 "node_modules/hyperdht-address": {
524 "version": "1.1.1",
525 "resolved": "https://registry.npmjs.org/hyperdht-address/-/hyperdht-address-1.1.1.tgz",
526 "integrity": "sha512-Mu/+7SW2cwvHxMXswa5mOrhrS5BJyntViAuB25k8d8wNtA8eAPs4LaIq6TwN1SS/KQY02h1r+gM8cQeN/k360w==",
527 "license": "Apache-2.0",
528 "dependencies": {
529 "compact-encoding": "^3.0.0",
530 "hyperschema": "^1.20.1"
531 }
532 },
533 "node_modules/hyperschema": {
534 "version": "1.21.0",
535 "resolved": "https://registry.npmjs.org/hyperschema/-/hyperschema-1.21.0.tgz",
536 "integrity": "sha512-lEnIbLTUQf7w6FU6X+R3yUJhBwCzF4ewRnAaYOrPdcVWe1rbOf94PzMiXb5pBKxroCXzlrPLxVujxAsTrrHc8g==",
537 "license": "Apache-2.0",
538 "dependencies": {
539 "bare-fs": "^4.0.1",
540 "compact-encoding": "^3.0.0",
541 "generate-object-property": "^2.0.0",
542 "generate-string": "^1.0.1"
543 }
544 },
545 "node_modules/is-property": {
546 "version": "1.0.2",
547 "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
548 "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
549 "license": "MIT"
550 },
551 "node_modules/kademlia-routing-table": {
552 "version": "1.0.6",
553 "resolved": "https://registry.npmjs.org/kademlia-routing-table/-/kademlia-routing-table-1.0.6.tgz",
554 "integrity": "sha512-Ve6jwIlUCYvUzBnXnzVRHDZCFgXURW9gmF3r7n05kZs/2rNbLHXwGdcq0qIaSwdmJCvtosgR4JensnVU65hzNQ==",
555 "license": "MIT",
556 "dependencies": {
557 "bare-events": "^2.2.0"
558 }
559 },
560 "node_modules/nanoassert": {
561 "version": "2.0.0",
562 "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz",
563 "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==",
564 "license": "ISC"
565 },
566 "node_modules/nat-sampler": {
567 "version": "1.0.1",
568 "resolved": "https://registry.npmjs.org/nat-sampler/-/nat-sampler-1.0.1.tgz",
569 "integrity": "sha512-yQvyNN7xbqR8crTKk3U8gRgpcV1Az+vfCEijiHu9oHHsnIl8n3x+yXNHl42M6L3czGynAVoOT9TqBfS87gDdcw==",
570 "license": "MIT"
571 },
572 "node_modules/noise-curve-ed": {
573 "version": "2.1.0",
574 "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz",
575 "integrity": "sha512-zAzJx+VwZM3w6EA1hTmDhJfvAnCeBQn/1FAeZ0LtGxCcCtlAK/uJXQVF/eDVUOaAZ286lHlx77WJ+qj9SmsRRg==",
576 "license": "ISC",
577 "dependencies": {
578 "b4a": "^1.1.0",
579 "nanoassert": "^2.0.0",
580 "sodium-universal": "^5.0.0"
581 }
582 },
583 "node_modules/noise-handshake": {
584 "version": "4.2.0",
585 "resolved": "https://registry.npmjs.org/noise-handshake/-/noise-handshake-4.2.0.tgz",
586 "integrity": "sha512-9O/VTNX/E2/AToyMTTDU0J/4WhaXMTdqc2DHs9vf+snoZ0cenSBq0dNYTVV1snYYEkmo6QeRrYMxtqtoYnY+LA==",
587 "license": "Apache-2.0",
588 "dependencies": {
589 "b4a": "^1.1.0",
590 "nanoassert": "^2.0.0",
591 "sodium-universal": "^5.0.0"
592 }
593 },
594 "node_modules/protomux": {
595 "version": "3.11.0",
596 "resolved": "https://registry.npmjs.org/protomux/-/protomux-3.11.0.tgz",
597 "integrity": "sha512-Ze43XaNHbL6zQ/zJwVYvue7Aj8pDB1HNnISsrFhAur3291Y+Iu2fMhOpySD73J/32BoBIUYrhl07lJsBR/fUVA==",
598 "license": "MIT",
599 "dependencies": {
600 "b4a": "^1.3.1",
601 "compact-encoding": "^3.0.0",
602 "queue-tick": "^1.0.0",
603 "safety-catch": "^1.0.1",
604 "unslab": "^1.3.0"
605 }
606 },
607 "node_modules/queue-tick": {
608 "version": "1.0.1",
609 "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
610 "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
611 "license": "MIT"
612 },
613 "node_modules/record-cache": {
614 "version": "1.2.0",
615 "resolved": "https://registry.npmjs.org/record-cache/-/record-cache-1.2.0.tgz",
616 "integrity": "sha512-kyy3HWCez2WrotaL3O4fTn0rsIdfRKOdQQcEJ9KpvmKmbffKVvwsloX063EgRUlpJIXHiDQFhJcTbZequ2uTZw==",
617 "license": "MIT",
618 "dependencies": {
619 "b4a": "^1.3.1"
620 }
621 },
622 "node_modules/require-addon": {
623 "version": "1.2.0",
624 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
625 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
626 "license": "Apache-2.0",
627 "dependencies": {
628 "bare-addon-resolve": "^1.3.0"
629 },
630 "engines": {
631 "bare": ">=1.10.0"
632 }
633 },
634 "node_modules/safety-catch": {
635 "version": "1.0.3",
636 "resolved": "https://registry.npmjs.org/safety-catch/-/safety-catch-1.0.3.tgz",
637 "integrity": "sha512-Zq+J1TefpoEq/HTUabo0YXX5MNvttjWYODGohgPBO2jfko8Wqx3JYMgE823szDFVamdH5PlpByvfiWScTdSYDA==",
638 "license": "MIT"
639 },
640 "node_modules/signal-promise": {
641 "version": "1.0.3",
642 "resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz",
643 "integrity": "sha512-WBgv0UnIq2C+Aeh0/n+IRpP6967eIx9WpynTUoiW3isPpfe1zu2LJzyfXdo9Tgef8yR/sGjcMvoUXD7EYdiz+g==",
644 "license": "MIT"
645 },
646 "node_modules/sodium-native": {
647 "version": "5.1.0",
648 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz",
649 "integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==",
650 "license": "MIT",
651 "dependencies": {
652 "bare-assert": "^1.2.0",
653 "require-addon": "^1.1.0",
654 "which-runtime": "^1.2.1"
655 },
656 "engines": {
657 "bare": ">=1.16.0"
658 }
659 },
660 "node_modules/sodium-secretstream": {
661 "version": "1.2.0",
662 "resolved": "https://registry.npmjs.org/sodium-secretstream/-/sodium-secretstream-1.2.0.tgz",
663 "integrity": "sha512-q/DbraNFXm1KfCiiZvapmz5UC3OlpirYFIvBK2MhGaOFSb3gRyk8OXTi17UI9SGfshQNCpsVvlopogbzZNyW6Q==",
664 "license": "MIT",
665 "dependencies": {
666 "b4a": "^1.1.1",
667 "sodium-universal": "^5.0.0"
668 }
669 },
670 "node_modules/sodium-universal": {
671 "version": "5.0.1",
672 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
673 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
674 "license": "MIT",
675 "dependencies": {
676 "sodium-native": "^5.0.1"
677 },
678 "peerDependencies": {
679 "sodium-javascript": "~0.8.0"
680 },
681 "peerDependenciesMeta": {
682 "sodium-javascript": {
683 "optional": true
684 }
685 }
686 },
687 "node_modules/streamx": {
688 "version": "2.28.0",
689 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz",
690 "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==",
691 "license": "MIT",
692 "dependencies": {
693 "events-universal": "^1.0.0",
694 "fast-fifo": "^1.3.2",
695 "text-decoder": "^1.1.0"
696 }
697 },
698 "node_modules/teex": {
699 "version": "1.0.1",
700 "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
701 "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
702 "license": "MIT",
703 "dependencies": {
704 "streamx": "^2.12.5"
705 }
706 },
707 "node_modules/text-decoder": {
708 "version": "1.2.7",
709 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
710 "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
711 "license": "Apache-2.0",
712 "dependencies": {
713 "b4a": "^1.6.4"
714 }
715 },
716 "node_modules/time-ordered-set": {
717 "version": "2.0.1",
718 "resolved": "https://registry.npmjs.org/time-ordered-set/-/time-ordered-set-2.0.1.tgz",
719 "integrity": "sha512-VJEKmgSN2UiOLB8BpN8Sh2b9LGMHTP5OPrQRpnKjvOheOyzk0mufbjzjKTIG2gO4A+Y+vDJ+0TcLbpUmMLsg8A==",
720 "license": "MIT"
721 },
722 "node_modules/timeout-refresh": {
723 "version": "2.0.1",
724 "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-2.0.1.tgz",
725 "integrity": "sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==",
726 "license": "MIT"
727 },
728 "node_modules/udx-native": {
729 "version": "1.20.7",
730 "resolved": "https://registry.npmjs.org/udx-native/-/udx-native-1.20.7.tgz",
731 "integrity": "sha512-FNG0QpnSt0us2xbLP5mmG5Q0UKdLYjKHuh2eR9VrJSFmoZMzeszYLLhK12+iq5x3eq/rgoGG/lLwPn7IDAr6pg==",
732 "license": "Apache-2.0",
733 "dependencies": {
734 "b4a": "^1.5.0",
735 "bare-events": "^2.2.0",
736 "require-addon": "^1.1.0",
737 "streamx": "^2.22.0"
738 },
739 "engines": {
740 "bare": ">=1.17.4"
741 }
742 },
743 "node_modules/unslab": {
744 "version": "1.3.0",
745 "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
746 "integrity": "sha512-YATkfKAFj47kTzmiQrWXMyRvaVrHsW6MEALa4bm+FhiA2YG4oira+Z3DXN6LrYOYn2Y8eO94Lwl9DOHjs1FpoQ==",
747 "license": "Apache-2.0",
748 "dependencies": {
749 "b4a": "^1.6.6"
750 }
751 },
752 "node_modules/which-runtime": {
753 "version": "1.4.0",
754 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.4.0.tgz",
755 "integrity": "sha512-0ugbP4CJW4e2D20jvEcC4973dCgIaHI4Rw1PT+26U9zEve7FyYdWAIwUnoeOYvoCfn+wXHoHTKb1KhkYlb60Pw==",
756 "license": "Apache-2.0"
757 },
758 "node_modules/xache": {
759 "version": "1.2.1",
760 "resolved": "https://registry.npmjs.org/xache/-/xache-1.2.1.tgz",
761 "integrity": "sha512-igRS6jPreJ54ABdzhh4mCDXcz+XMaWO2q1ABRV2yWYuk29jlp8VT7UBdCqNkX7rpYBbXsebVVKkwIuYZjyZNqA==",
762 "license": "MIT"
763 },
764 "node_modules/z32": {
765 "version": "1.1.0",
766 "resolved": "https://registry.npmjs.org/z32/-/z32-1.1.0.tgz",
767 "integrity": "sha512-1WUHy+VS6d0HPNspDxvLssBbeQjXMjSnpv0vH82vRAUfg847NmX3OXozp/hRP5jPhxBbrVzrgvAt+UsGNzRFQQ==",
768 "license": "MIT",
769 "dependencies": {
770 "b4a": "^1.5.3"
771 }
772 }
773 }
774}
package.json +30 -0
……@@ -0,0 +1,30 @@
1{
2 "name": "hypermosh-shell",
3 "version": "0.0.1",
4 "description": "Resilient, roaming terminal sessions to identity-keyed P2P nodes — mosh tunneled over a hole-punched hyperdht connection. Built for celltron-class nodes (NAT'd, Doze-prone phones) where a plain stream shell dies on every network flap.",
5 "type": "module",
6 "bin": { "hypermosh": "./bin/hypermosh.mjs" },
7 "exports": { ".": "./lib/relay.mjs", "./identity": "./lib/identity.mjs" },
8 "imports": {
9 "#node:dgram": { "bare": "bare-dgram", "default": "dgram" },
10 "#node:fs": { "bare": "bare-fs", "default": "fs" },
11 "#node:os": { "bare": "bare-os", "default": "os" },
12 "#node:path": { "bare": "bare-path", "default": "path" },
13 "#node:process": { "bare": "bare-process", "default": "process" }
14 },
15 "scripts": { "test": "node test/tunnel.test.mjs" },
16 "dependencies": {
17 "b4a": "^1.6.0",
18 "hypercore-id-encoding": "^1.2.0",
19 "hyperdht": "^6.6.0"
20 },
21 "optionalDependencies": {
22 "bare-dgram": "^1.0.0",
23 "bare-fs": "^4.0.0",
24 "bare-os": "^3.0.0",
25 "bare-path": "^3.0.0",
26 "bare-process": "^4.0.0"
27 },
28 "license": "Apache-2.0",
29 "author": "hyper-fleeky"
30}
test/pty-mosh.py +28 -0
……@@ -0,0 +1,28 @@
1#!/usr/bin/env python3
2# Drive mosh-client in a PTY with a real window size (mosh asserts s_height>0).
3# Usage: pty-mosh.py <MOSH_KEY> <host> <port> <seconds> (prints captured output)
4import os, pty, sys, select, struct, fcntl, termios, time
5
6key, host, port, secs = sys.argv[1], sys.argv[2], sys.argv[3], float(sys.argv[4])
7mfd, sfd = pty.openpty()
8fcntl.ioctl(sfd, termios.TIOCSWINSZ, struct.pack('HHHH', 40, 120, 0, 0))
9pid = os.fork()
10if pid == 0:
11 os.setsid()
12 os.dup2(sfd, 0); os.dup2(sfd, 1); os.dup2(sfd, 2)
13 try: fcntl.ioctl(sfd, termios.TIOCSCTTY, 0)
14 except OSError: pass
15 os.environ.update(MOSH_KEY=key, LC_ALL='C.UTF-8', LANG='C.UTF-8', TERM='xterm')
16 os.execvp('mosh-client', ['mosh-client', host, port]); os._exit(127)
17os.close(sfd)
18buf = b''; end = time.time() + secs
19while time.time() < end:
20 r, _, _ = select.select([mfd], [], [], 0.5)
21 if r:
22 try: d = os.read(mfd, 8192)
23 except OSError: break
24 if not d: break
25 buf += d
26try: os.kill(pid, 15)
27except OSError: pass
28sys.stdout.buffer.write(buf); sys.stdout.flush()
test/tunnel.test.mjs +79 -0
……@@ -0,0 +1,79 @@
1// hypermosh tunnel tests.
2// 1) loopback: UDP echo round-trips through the hyperdht tunnel (no mosh needed)
3// 2) real mosh: a mosh session runs end-to-end through the tunnel (if mosh present)
4import DHT from 'hyperdht'
5import dgram from 'dgram'
6import b4a from 'b4a'
7import { spawn, execSync } from 'child_process'
8import { fileURLToPath } from 'url'
9import path from 'path'
10import { serve, connect } from '../lib/relay.mjs'
11
12const __dirname = path.dirname(fileURLToPath(import.meta.url))
13let failures = 0
14
15async function testLoopback () {
16 console.log('\n[1] loopback: UDP echo through hyperdht tunnel')
17 const echo = dgram.createSocket('udp4')
18 echo.on('message', (msg, r) => echo.send(b4a.concat([b4a.from('echo:'), msg]), r.port, r.address))
19 await new Promise((res) => echo.bind(0, '127.0.0.1', res))
20
21 const keyPair = DHT.keyPair()
22 const s = await serve({ keyPair, udpPort: echo.address().port })
23 const c = await connect({ remoteKey: keyPair.publicKey })
24
25 const probe = dgram.createSocket('udp4')
26 const got = new Promise((resolve, reject) => {
27 const t = setTimeout(() => reject(new Error('timeout')), 20000)
28 probe.on('message', (m) => { clearTimeout(t); resolve(b4a.toString(m)) })
29 })
30 await new Promise((res) => probe.bind(0, '127.0.0.1', res))
31 const payload = 'hello-' + Math.floor(process.uptime() * 1000)
32 probe.send(b4a.from(payload), c.localPort, '127.0.0.1')
33
34 try {
35 const reply = await got
36 if (reply === 'echo:' + payload) console.log(' ✅ PASS — round-trip ok')
37 else { console.log(' ❌ FAIL — got', reply); failures++ }
38 } catch (e) { console.log(' ❌ FAIL —', e.message); failures++ }
39 finally { probe.close(); echo.close(); await c.close(); await s.close() }
40}
41
42async function testRealMosh () {
43 let hasMosh = true
44 try { execSync('command -v mosh-server && command -v mosh-client && command -v python3', { stdio: 'ignore' }) } catch { hasMosh = false }
45 if (!hasMosh) { console.log('\n[2] real mosh: SKIPPED (mosh/python3 not installed)'); return }
46
47 console.log('\n[2] real mosh: session through the hyperdht tunnel')
48 const ms = spawn('mosh-server', ['new', '-i', '127.0.0.1', '--', 'bash', '-lc', 'echo HYPERMOSH_OK; sleep 8'],
49 { env: { ...process.env, LC_ALL: 'C.UTF-8', LANG: 'C.UTF-8' } })
50 let sout = ''
51 let conn
52 try {
53 conn = await new Promise((resolve, reject) => {
54 const t = setTimeout(() => reject(new Error('no MOSH CONNECT')), 10000)
55 const scan = (d) => { sout += d; const m = sout.match(/MOSH CONNECT (\d+) (\S+)/); if (m) { clearTimeout(t); resolve({ port: +m[1], key: m[2] }) } }
56 ms.stdout.on('data', scan); ms.stderr.on('data', scan)
57 })
58 } catch (e) { console.log(' ❌ FAIL —', e.message); failures++; try { ms.kill() } catch {}; return }
59
60 const keyPair = DHT.keyPair()
61 const s = await serve({ keyPair, udpPort: conn.port })
62 const c = await connect({ remoteKey: keyPair.publicKey })
63
64 const drv = spawn('python3', [path.join(__dirname, 'pty-mosh.py'), conn.key, '127.0.0.1', String(c.localPort), '12'], { env: process.env })
65 let cap = ''
66 drv.stdout.on('data', (d) => { cap += d })
67 await new Promise((res) => { const t = setTimeout(() => { try { drv.kill() } catch {}; res() }, 20000); drv.on('exit', () => { clearTimeout(t); res() }) })
68
69 const clean = cap.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '')
70 if (clean.includes('HYPERMOSH_OK')) console.log(' ✅ PASS — mosh session traversed the tunnel')
71 else { console.log(' ❌ FAIL — marker not seen'); failures++ }
72
73 await c.close(); await s.close(); try { ms.kill() } catch {}
74}
75
76await testLoopback()
77await testRealMosh()
78console.log(failures === 0 ? '\n✅ all tests passed' : '\n❌ ' + failures + ' test(s) failed')
79process.exit(failures === 0 ? 0 : 1)