first commit

This commit is contained in:
Raven Scott
2026-07-22 19:25:27 -04:00
commit 75918f94e7
221 changed files with 27314 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# Compare Node compact-encoding vectors with Dart encodeBytes output.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
NODE_OUT="$(node "$ROOT/tool/interop_node/compact_encoding_vectors.js")"
echo "$NODE_OUT" | dart run "$ROOT/tool/interop_node/check_vectors.dart"
+67
View File
@@ -0,0 +1,67 @@
// ignore_for_file: avoid_print
import 'dart:convert' as convert;
import 'dart:io';
import 'dart:typed_data';
import 'package:compact_encoding/compact_encoding.dart';
Uint8List _hexDecode(String hex) {
final out = Uint8List(hex.length ~/ 2);
for (var i = 0; i < out.length; i++) {
out[i] = int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16);
}
return out;
}
String _hexEncode(Uint8List bytes) =>
bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
void main() async {
final input = await stdin.transform(convert.utf8.decoder).join();
final Map<String, dynamic> vectors =
convert.jsonDecode(input) as Map<String, dynamic>;
final checks = <String, Uint8List>{
'uint_42': encodeBytes(uint, 42),
'uint_4200': encodeBytes(uint, 4200),
'uint_max_safe': encodeBytes(uint, maxSafeInteger),
'uint_0x10000': encodeBytes(uint, 0x10000),
'int_42': encodeBytes(intZigZag, 42),
'int_neg_4200': encodeBytes(intZigZag, -4200),
'buffer_hi': encodeBytes(buffer, Uint8List.fromList('hi'.codeUnits)),
'string_wheat': encodeBytes(utf8, '🌾'),
'frame_uint_4200': encodeBytes(frame(uint), 4200),
'ipv4': encodeBytes(ipv4, '1.2.3.4'),
'ipv6_abbrev': encodeBytes(ipv6, '1:2::7:8'),
'float64_162': encodeBytes(float64, 162.2377294),
};
var failed = 0;
for (final entry in checks.entries) {
final expected = _hexDecode(vectors[entry.key] as String);
final actual = entry.value;
if (!_eq(expected, actual)) {
stderr.writeln(
'MISMATCH ${entry.key}: node=${vectors[entry.key]} dart=${_hexEncode(actual)}',
);
failed++;
} else {
print('OK ${entry.key}');
}
}
if (failed > 0) {
stderr.writeln('FAILED $failed vectors');
exit(1);
}
print('INTEROP_OK ${checks.length} vectors');
}
bool _eq(Uint8List a, Uint8List b) {
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
@@ -0,0 +1,39 @@
/**
* Encode compact-encoding fixtures in Node for Dart cross-checks.
*
* Usage (from holepunch clone or with compact-encoding installed):
* node tool/interop_node/compact_encoding_vectors.js
*
* Or with NODE_PATH pointing at holepunch repos.
*/
const path = require('path')
const holepunch =
process.env.HOLEPUNCH_REPOS ||
path.join(
process.env.HOME || '',
'dev/pearcli/holepunch-repos/holepunchto_repos'
)
const enc = require(path.join(holepunch, 'compact-encoding'))
function hex(buf) {
return Buffer.from(buf).toString('hex')
}
const vectors = {
uint_42: hex(enc.encode(enc.uint, 42)),
uint_4200: hex(enc.encode(enc.uint, 4200)),
uint_max_safe: hex(enc.encode(enc.uint, Number.MAX_SAFE_INTEGER)),
uint_0x10000: hex(enc.encode(enc.uint, 0x10000)),
int_42: hex(enc.encode(enc.int, 42)),
int_neg_4200: hex(enc.encode(enc.int, -4200)),
buffer_hi: hex(enc.encode(enc.buffer, Buffer.from('hi'))),
string_wheat: hex(enc.encode(enc.string, '🌾')),
frame_uint_4200: hex(enc.encode(enc.frame(enc.uint), 4200)),
ipv4: hex(enc.encode(enc.ipv4, '1.2.3.4')),
ipv6_abbrev: hex(enc.encode(enc.ipv6, '1:2::7:8')),
float64_162: hex(enc.encode(enc.float64, 162.2377294))
}
console.log(JSON.stringify(vectors, null, 2))
@@ -0,0 +1,60 @@
#!/usr/bin/env node
/**
* Connect to a Dart HyperDHT server (OPEN LAN).
* Args: <bootstrap host:port> <serverPublicKeyHex>
*/
const HyperDHT = require('/Users/raven/dev/pearcli/holepunch-repos/holepunchto_repos/hyperdht')
const b4a = require('b4a')
async function main() {
const bootstrap = process.argv[2]
const pkHex = process.argv[3]
if (!bootstrap || !pkHex) {
console.error('usage: node hyperdht_connect_client.cjs <host:port> <pkHex>')
process.exit(2)
}
const dht = new HyperDHT({
bootstrap: [bootstrap],
ephemeral: false,
firewalled: false,
})
await dht.ready()
const pk = b4a.from(pkHex, 'hex')
const socket = dht.connect(pk)
await new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('open timeout')), 15000)
socket.on('open', () => {
clearTimeout(t)
resolve()
})
socket.on('error', reject)
})
console.log('node client open')
socket.write(b4a.from('hello from node'))
const reply = await new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('data timeout')), 10000)
socket.once('data', (data) => {
clearTimeout(t)
resolve(data)
})
})
console.log('node got', b4a.toString(reply))
if (b4a.toString(reply) !== 'pong from dart') {
throw new Error('unexpected reply')
}
socket.destroy()
await dht.destroy()
console.log('HYPERDHT_NODE_CLIENT_OK')
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
@@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* Diagnose Node HyperDHT / dht-rpc against a Dart bootstrap.
* Args: <bootstrap host:port> [serverPublicKeyHex]
*/
const HyperDHT = require('/Users/raven/dev/pearcli/holepunch-repos/holepunchto_repos/hyperdht')
const b4a = require('b4a')
async function main() {
const bootstrap = process.argv[2]
const pkHex = process.argv[3]
if (!bootstrap) {
console.error('usage: node hyperdht_diag_client.cjs <host:port> [pkHex]')
process.exit(2)
}
const dht = new HyperDHT({
bootstrap: [bootstrap],
ephemeral: false,
firewalled: false,
})
dht.on('persistent', () => console.log('node persistent', dht.host, dht.port))
dht.on('nat-update', (host, port) => console.log('node nat', host, port))
try {
await dht.ready()
console.log('node ready id=', dht.id && b4a.toString(dht.id, 'hex').slice(0, 16))
console.log('node table size', dht.table.toArray().length)
for (const n of dht.table.toArray()) {
console.log(
' peer',
n.host + ':' + n.port,
'id=',
n.id && b4a.toString(n.id, 'hex').slice(0, 16)
)
}
} catch (err) {
console.error('ready failed', err)
}
if (pkHex) {
const target = HyperDHT.hash(b4a.from(pkHex, 'hex'))
console.log('findPeer target', b4a.toString(target, 'hex').slice(0, 16))
let hits = 0
try {
for await (const data of dht.findPeer(b4a.from(pkHex, 'hex'))) {
hits++
console.log(
'findPeer hit from',
data.from.host + ':' + data.from.port,
'id=',
data.from.id && b4a.toString(data.from.id, 'hex').slice(0, 16),
'peer=',
data.peer && b4a.toString(data.peer.publicKey, 'hex').slice(0, 16),
'relays=',
data.peer && data.peer.relayAddresses && data.peer.relayAddresses.length
)
}
} catch (err) {
console.error('findPeer err', err)
}
console.log('findPeer hits', hits)
// Also try connect with explicit relay = bootstrap
const [host, port] = bootstrap.split(':')
const socket = dht.connect(b4a.from(pkHex, 'hex'), {
nodes: [{ host, port: Number(port) }],
})
try {
await new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('open timeout')), 10000)
socket.on('open', () => {
clearTimeout(t)
resolve()
})
socket.on('error', reject)
})
console.log('CONNECT_OK')
socket.destroy()
} catch (err) {
console.error('connect failed', err && err.message)
socket.destroy()
}
}
await dht.destroy()
console.log('DIAG_DONE')
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
@@ -0,0 +1,87 @@
#!/usr/bin/env node
/**
* Firewalled Node HyperDHT client — forces holepunch (no LAN shortcut).
* Args: <bootstrap host:port> <serverPublicKeyHex>
*
* Prints HYPERDHT_NODE_HOLEPUNCH_CLIENT_OK on success.
*/
const HyperDHT = require('/Users/raven/dev/pearcli/holepunch-repos/holepunchto_repos/hyperdht')
const b4a = require('b4a')
async function main() {
const bootstrap = process.argv[2]
const pkHex = process.argv[3]
if (!bootstrap || !pkHex) {
console.error('usage: node hyperdht_holepunch_client.cjs <host:port> <pkHex>')
process.exit(2)
}
// firewalled: true → handshake firewall UNKNOWN (not OPEN) → server punches.
// localConnection: false → skip LAN ping shortcut; exercise real holepunch.
const dht = new HyperDHT({
bootstrap: [bootstrap],
ephemeral: true,
firewalled: true,
host: '127.0.0.1',
})
await dht.ready()
await dht.fullyBootstrapped()
console.log('node client ready nodes=', dht.nodes.length, 'fw=', dht.firewalled)
const pk = b4a.from(pkHex, 'hex')
let usedHolepunch = false
const socket = dht.connect(pk, {
localConnection: false,
holepunch(remoteFirewall, localFirewall) {
usedHolepunch = true
console.log(
'node client holepunch gate remoteFw=',
remoteFirewall,
'localFw=',
localFirewall
)
return true
},
})
await new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('open timeout')), 25000)
socket.on('open', () => {
clearTimeout(t)
resolve()
})
socket.on('error', reject)
})
console.log('node client open usedHolepunch=', usedHolepunch)
// Probe-phase initiator connect often finishes before holepunch() gate.
// OPEN path increments punches.open — reject that.
if (dht.stats.punches.open > 0) {
throw new Error('OPEN punch counter incremented')
}
console.log('node punches=', dht.stats.punches)
socket.write(b4a.from('hello punch from node'))
const reply = await new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('data timeout')), 10000)
socket.once('data', (data) => {
clearTimeout(t)
resolve(data)
})
})
console.log('node got', b4a.toString(reply))
if (b4a.toString(reply) !== 'pong punch from dart') {
throw new Error('unexpected reply')
}
socket.destroy()
await dht.destroy()
console.log('HYPERDHT_NODE_HOLEPUNCH_CLIENT_OK')
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
@@ -0,0 +1,54 @@
#!/usr/bin/env node
/**
* Firewalled Node HyperDHT server for Dart holepunch client.
* Args: <bootstrap host:port>
* Prints: READY <pkHex>
*/
const HyperDHT = require('/Users/raven/dev/pearcli/holepunch-repos/holepunchto_repos/hyperdht')
const b4a = require('b4a')
async function main() {
const bootstrap = process.argv[2]
if (!bootstrap) {
console.error('usage: node hyperdht_holepunch_server.cjs <host:port>')
process.exit(2)
}
// firewalled → advertise holepunch relays (remoteAddress() is null).
const dht = new HyperDHT({
bootstrap: [bootstrap],
ephemeral: true,
firewalled: true,
host: '127.0.0.1',
})
await dht.ready()
await dht.fullyBootstrapped()
console.log('node server ready nodes=', dht.nodes.length)
const server = dht.createServer({ shareLocalAddress: false }, (socket) => {
console.log('node server got connection')
socket.once('data', (data) => {
console.log('node got', b4a.toString(data))
socket.write(b4a.from('pong punch from node'))
setTimeout(() => process.exit(0), 400)
})
socket.on('error', () => {})
})
await server.listen()
console.log('READY', b4a.toString(server.publicKey, 'hex'))
await new Promise((resolve) => {
process.on('SIGTERM', resolve)
process.on('SIGINT', resolve)
setTimeout(resolve, 45000)
})
await server.close()
await dht.destroy()
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
@@ -0,0 +1,85 @@
#!/usr/bin/env node
/**
* Encode HyperDHT message fixtures with Node hyperdht codecs.
* Prints JSON lines: { "name", "hex" }
*/
const c = require('compact-encoding')
const b4a = require('b4a')
const m = require('/Users/raven/dev/pearcli/holepunch-repos/holepunchto_repos/hyperdht/lib/messages.js')
function enc(codec, value) {
const state = { start: 0, end: 0, buffer: null }
codec.preencode(state, value)
state.buffer = b4a.allocUnsafe(state.end)
codec.encode(state, value)
return b4a.toString(state.buffer, 'hex')
}
const pk = b4a.alloc(32, 9)
const sig = b4a.alloc(64, 7)
const token = b4a.alloc(32)
token[31] = 1
const vectors = [
[
'handshake',
enc(m.handshake, {
mode: 1,
noise: b4a.from([0xaa, 0xbb, 0xcc]),
peerAddress: { host: '1.2.3.4', port: 1234 },
relayAddress: { host: '5.6.7.8', port: 5678 },
}),
],
[
'noisePayload',
enc(m.noisePayload, {
version: 1,
error: 0,
firewall: 2,
holepunch: null,
addresses4: [{ host: '10.0.0.1', port: 49737 }],
addresses6: [],
udx: { version: 1, reusableSocket: true, id: 42, seq: 7 },
secretStream: {},
relayThrough: null,
relayAddresses: null,
}),
],
[
'announce',
enc(m.announce, {
peer: {
publicKey: pk,
relayAddresses: [{ host: '8.8.8.8', port: 53 }],
},
refresh: null,
signature: null,
bump: 3,
}),
],
[
'holepunchPayload',
enc(m.holepunchPayload, {
error: 0,
firewall: 1,
round: 2,
connected: true,
punching: true,
addresses: [{ host: '1.1.1.1', port: 80 }],
remoteAddress: null,
token,
remoteToken: null,
}),
],
[
'peer',
enc(m.peer, {
publicKey: pk,
relayAddresses: [{ host: '127.0.0.1', port: 49737 }],
}),
],
]
for (const [name, hex] of vectors) {
console.log(JSON.stringify({ name, hex }))
}
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env node
/**
* Node HyperDHT client → Dart app via Dart blind-relay (force-relay).
*
* Args: <bootstrap> <appPkHex> <relayPkHex>
*
* Handshake must be DHT-relayed (do NOT pass relayAddresses to the app).
* Combined with Dart advertising holepunch relays + UNKNOWN firewall, Node
* skips the direct-connect shortcuts and waits on blind-relay after aborting
* holepunch.
*/
const HyperDHT = require('/Users/raven/dev/pearcli/holepunch-repos/holepunchto_repos/hyperdht')
const b4a = require('b4a')
async function main() {
const bootstrap = process.argv[2]
const appPkHex = process.argv[3]
const relayPkHex = process.argv[4]
if (!bootstrap || !appPkHex || !relayPkHex) {
console.error(
'usage: node hyperdht_relay_client.cjs <bootstrap> <appPk> <relayPk>'
)
process.exit(2)
}
const dht = new HyperDHT({
bootstrap: [bootstrap],
ephemeral: true,
firewalled: true,
})
await dht.ready()
const appPk = b4a.from(appPkHex, 'hex')
const relayPk = b4a.from(relayPkHex, 'hex')
const [bootHost, bootPortStr] = bootstrap.split(':')
const bootPort = Number(bootPortStr)
console.error('node connecting…')
const socket = dht.connect(appPk, {
relayThrough: relayPk,
holepunch: () => false,
// Force DHT-relayed handshake via bootstrap — if Node findPeer hits the
// app itself, !relayed triggers an immediate direct onsocket and races
// the blind-relay path.
relayAddresses: [{ host: bootHost, port: bootPort }],
localConnection: false,
})
socket.on('error', (err) => console.error('node socket error', err.message))
socket.on('close', () => console.error('node socket close'))
await new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('open timeout')), 45000)
socket.on('open', () => {
clearTimeout(t)
console.error(
'node open remoteId=',
socket.rawStream?.remoteId,
'remote=',
socket.rawStream?.remoteHost + ':' + socket.rawStream?.remotePort
)
resolve()
})
socket.on('error', reject)
})
const chunks = []
const reply = new Promise((resolve, reject) => {
const t = setTimeout(
() =>
reject(
new Error(
'data timeout got=' + chunks.map((c) => b4a.toString(c)).join('|')
)
),
20000
)
socket.on('data', (data) => {
chunks.push(data)
console.error('node data', b4a.toString(data))
if (b4a.toString(data).startsWith('pong:')) {
clearTimeout(t)
resolve(data)
}
})
})
await new Promise((r) => setTimeout(r, 200))
console.error('node writing hello')
socket.write(b4a.from('hello from node'))
const data = await reply
console.error('node got', b4a.toString(data))
if (b4a.toString(data) !== 'pong:hello from node') {
throw new Error('unexpected reply: ' + b4a.toString(data))
}
socket.destroy()
await dht.destroy()
console.log('HYPERDHT_NODE_RELAY_CLIENT_OK')
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env node
/**
* Node blind-relay only (app + client are Dart).
* Args: <bootstrap host:port>
* Prints: READY <relayPkHex>
*/
const HyperDHT = require('/Users/raven/dev/pearcli/holepunch-repos/holepunchto_repos/hyperdht')
const RelayServer = require('/Users/raven/dev/pearcli/holepunch-repos/holepunchto_repos/blind-relay').Server
const b4a = require('b4a')
async function main() {
const bootstrap = process.argv[2]
if (!bootstrap) {
console.error('usage: node hyperdht_relay_only.cjs <host:port>')
process.exit(2)
}
const dht = new HyperDHT({
bootstrap: [bootstrap],
ephemeral: false,
firewalled: false,
})
await dht.ready()
const relay = new RelayServer({
createStream(opts) {
return dht.createRawStream({ ...opts, framed: true })
},
})
const server = dht.createServer(function (socket) {
console.error('node relay accepted peer')
const session = relay.accept(socket, { id: socket.remotePublicKey })
session.on('error', (err) => console.error('relay session', err.message))
})
await server.listen()
console.log('READY', b4a.toString(server.publicKey, 'hex'))
// Stay alive until stdin closes or 60s.
await new Promise((resolve) => {
const t = setTimeout(resolve, 60000)
process.stdin.on('end', () => {
clearTimeout(t)
resolve()
})
process.stdin.resume()
})
await relay.close()
await dht.destroy()
console.log('HYPERDHT_NODE_RELAY_ONLY_OK')
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env node
/**
* Node HyperDHT server for Dart client interop.
* Args: <bootstrap host:port>
* Prints: READY <pkHex>
* Expects one connection, replies to first payload, then exits.
*/
const HyperDHT = require('/Users/raven/dev/pearcli/holepunch-repos/holepunchto_repos/hyperdht')
const b4a = require('b4a')
async function main() {
const bootstrap = process.argv[2]
if (!bootstrap) {
console.error('usage: node hyperdht_server.cjs <host:port>')
process.exit(2)
}
const dht = new HyperDHT({
bootstrap: [bootstrap],
ephemeral: false,
firewalled: false,
})
await dht.ready()
const server = dht.createServer((socket) => {
console.log('node server got connection')
socket.once('data', (data) => {
console.log('node got', b4a.toString(data))
socket.write(b4a.from('pong from node'))
// Allow write to flush, then exit cleanly (parent may also SIGTERM).
setTimeout(() => process.exit(0), 250)
})
socket.on('error', () => {})
})
await server.listen()
console.log('READY', b4a.toString(server.publicKey, 'hex'))
// Keep alive until parent kills or we get a signal.
await new Promise((resolve) => {
process.on('SIGTERM', resolve)
process.on('SIGINT', resolve)
setTimeout(resolve, 30000)
})
await server.close()
await dht.destroy()
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env node
/**
* Node Hyperswarm client — joins topic (client-only) against Dart bootstrap.
* Args: <bootstrap host:port> <topicHex>
*/
const Hyperswarm = require('/Users/raven/dev/pearcli/holepunch-repos/holepunchto_repos/hyperswarm')
const b4a = require('b4a')
async function main() {
const bootstrap = process.argv[2]
const topicHex = process.argv[3]
if (!bootstrap || !topicHex) {
console.error('usage: node hyperswarm_client.cjs <host:port> <topicHex>')
process.exit(2)
}
const swarm = new Hyperswarm({
bootstrap: [bootstrap],
ephemeral: false,
firewalled: false,
})
const done = new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('connection timeout')), 25000)
swarm.on('connection', (conn) => {
console.log('node client connection')
conn.on('error', () => {})
conn.write(b4a.from('hello swarm from node'))
conn.once('data', (data) => {
clearTimeout(t)
console.log('node got', b4a.toString(data))
if (b4a.toString(data) !== 'pong swarm from dart') {
reject(new Error('unexpected reply'))
return
}
conn.end()
resolve()
})
})
})
const topic = b4a.from(topicHex, 'hex')
swarm.join(topic, { server: false, client: true })
await swarm.flush()
console.log('node client flushed')
await done
await swarm.destroy()
console.log('HYPERSWARM_NODE_CLIENT_OK')
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
@@ -0,0 +1,108 @@
#!/usr/bin/env node
/**
* Node Hyperswarm client → Dart Hyperswarm app via Dart blind-relay.
*
* Args: <bootstrap host:port> <topicHex> <relayPkHex>
*
* Monkey-patches dht.connect with the same force-relay-friendly opts as
* hyperdht_relay_client.cjs (DHT-relayed handshake, holepunch aborted,
* localConnection: false) so Node does not race a direct onsocket against
* Dart's forceRelay wait.
*/
const Hyperswarm = require('/Users/raven/dev/pearcli/holepunch-repos/holepunchto_repos/hyperswarm')
const b4a = require('b4a')
async function main() {
const bootstrap = process.argv[2]
const topicHex = process.argv[3]
const relayPkHex = process.argv[4]
if (!bootstrap || !topicHex || !relayPkHex) {
console.error(
'usage: node hyperswarm_relay_client.cjs <host:port> <topicHex> <relayPk>'
)
process.exit(2)
}
const [bootHost, bootPortStr] = bootstrap.split(':')
const bootPort = Number(bootPortStr)
const relayPk = b4a.from(relayPkHex, 'hex')
const topic = b4a.from(topicHex, 'hex')
const swarm = new Hyperswarm({
bootstrap: [bootstrap],
ephemeral: true,
firewalled: true,
// Always advertise relayThrough (bypass randomized/force gate).
relayThrough: () => relayPk,
})
const origConnect = swarm.dht.connect.bind(swarm.dht)
swarm.dht.connect = (publicKey, opts = {}) =>
origConnect(publicKey, {
...opts,
relayThrough: relayPk,
holepunch: () => false,
localConnection: false,
// Force DHT-relayed handshake via bootstrap — same as HyperDHT relay client.
relayAddresses: [{ host: bootHost, port: bootPort }],
})
const done = new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('connection timeout')), 45000)
swarm.on('connection', (conn) => {
console.error('node swarm connection')
conn.on('error', (err) => console.error('node conn error', err.message))
const chunks = []
const reply = new Promise((res, rej) => {
const dt = setTimeout(
() =>
rej(
new Error(
'data timeout got=' + chunks.map((c) => b4a.toString(c)).join('|')
)
),
20000
)
conn.on('data', (data) => {
chunks.push(data)
console.error('node data', b4a.toString(data))
if (b4a.toString(data).startsWith('pong:')) {
clearTimeout(dt)
res(data)
}
})
})
setTimeout(() => {
console.error('node writing hello')
conn.write(b4a.from('hello swarm from node'))
}, 200)
reply
.then((data) => {
clearTimeout(t)
if (b4a.toString(data) !== 'pong:hello swarm from node') {
reject(new Error('unexpected reply: ' + b4a.toString(data)))
return
}
conn.end()
resolve()
})
.catch(reject)
})
})
swarm.join(topic, { server: false, client: true })
await swarm.flush()
console.error('node client flushed')
await done
await swarm.destroy()
console.log('HYPERSWARM_NODE_RELAY_CLIENT_OK')
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env node
/**
* Node Hyperswarm server — joins topic (server-only) on Dart bootstrap.
* Args: <bootstrap host:port> <topicHex>
* Prints: READY
*/
const Hyperswarm = require('/Users/raven/dev/pearcli/holepunch-repos/holepunchto_repos/hyperswarm')
const b4a = require('b4a')
async function main() {
const bootstrap = process.argv[2]
const topicHex = process.argv[3]
if (!bootstrap || !topicHex) {
console.error('usage: node hyperswarm_server.cjs <host:port> <topicHex>')
process.exit(2)
}
const swarm = new Hyperswarm({
bootstrap: [bootstrap],
ephemeral: false,
firewalled: false,
})
swarm.on('connection', (conn) => {
console.log('node server connection')
conn.on('error', () => {})
conn.once('data', (data) => {
console.log('node got', b4a.toString(data))
conn.write(b4a.from('pong swarm from node'))
setTimeout(() => process.exit(0), 400)
})
})
const topic = b4a.from(topicHex, 'hex')
await swarm.join(topic, { server: true, client: false }).flushed()
console.log('READY')
await new Promise((resolve) => {
process.on('SIGTERM', resolve)
process.on('SIGINT', resolve)
setTimeout(resolve, 45000)
})
await swarm.destroy()
}
main().catch((err) => {
console.error(err)
process.exit(1)
})
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env node
/**
* Deterministic XX + Ed25519 vectors for Dart interop.
* Run from noise-curve-ed directory (npm install there first).
*
* Note: noise-handshake zeros ephemeral key buffers in place on final();
* capture hex strings immediately after each message.
*/
const path = require('path')
const ROOT = '/Users/raven/dev/pearcli/holepunch-repos/holepunchto_repos'
const Noise = require(path.join(ROOT, 'noise-handshake'))
const curve = require(path.join(ROOT, 'noise-curve-ed'))
function hex(b) {
return Buffer.from(b).toString('hex')
}
function copy(b) {
return Buffer.from(hex(b), 'hex')
}
const seeds = {
initiatorStatic: Buffer.from('01'.repeat(32), 'hex'),
responderStatic: Buffer.from('02'.repeat(32), 'hex'),
initiatorEph: Buffer.from('03'.repeat(32), 'hex'),
responderEph: Buffer.from('04'.repeat(32), 'hex'),
}
const is = curve.generateKeyPair(seeds.initiatorStatic)
const rs = curve.generateKeyPair(seeds.responderStatic)
const ie = curve.generateKeyPair(seeds.initiatorEph)
const re = curve.generateKeyPair(seeds.responderEph)
const initiator = new Noise('XX', true, is, { curve })
const responder = new Noise('XX', false, rs, { curve })
initiator.initialise(Buffer.alloc(0))
responder.initialise(Buffer.alloc(0))
initiator.e = ie
const m1Hex = hex(initiator.send())
responder.recv(copy(Buffer.from(m1Hex, 'hex')))
responder.e = re
const m2Hex = hex(responder.send())
initiator.recv(copy(Buffer.from(m2Hex, 'hex')))
const m3Hex = hex(initiator.send())
responder.recv(copy(Buffer.from(m3Hex, 'hex')))
const out = {
m1: m1Hex,
m2: m2Hex,
m3: m3Hex,
initiatorRx: hex(initiator.rx),
initiatorTx: hex(initiator.tx),
responderRx: hex(responder.rx),
responderTx: hex(responder.tx),
}
console.log(JSON.stringify(out))
+11
View File
@@ -0,0 +1,11 @@
{
"name": "hyperdart-interop-node",
"private": true,
"dependencies": {
"b4a": "^1.8.1",
"compact-encoding": "^2.19.2",
"protomux": "^3.11.0",
"streamx": "^2.28.0",
"udx-native": "1.20.7"
}
}
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env node
'use strict'
/**
* Protomux peer over newline-hex framed duplex (stdin ↔ stdout).
* Pairs protocol `interop` and echoes string messages with an `echo:` prefix.
*/
const Protomux = require('protomux')
const c = require('compact-encoding')
const { Duplex } = require('streamx')
const readline = require('readline')
const stream = new Duplex({
write (data, cb) {
process.stdout.write(Buffer.from(data).toString('hex') + '\n')
cb(null)
}
})
const rl = readline.createInterface({ input: process.stdin })
rl.on('line', (line) => {
const s = line.trim()
if (!s) return
stream.push(Buffer.from(s, 'hex'))
})
rl.on('close', () => {
stream.push(null)
})
const mux = new Protomux(stream)
mux.pair({ protocol: 'interop' }, () => {
const ch = mux.createChannel({ protocol: 'interop' })
const echo = ch.addMessage({
encoding: c.string,
onmessage (m) {
echo.send('echo:' + m)
}
})
ch.open()
})
process.stdout.write('READY\n')
+10
View File
@@ -0,0 +1,10 @@
name: interop_check_vectors
description: Temporary runner for Node↔Dart compact-encoding vector check.
publish_to: none
environment:
sdk: ^3.5.0
dependencies:
compact_encoding:
path: ../../packages/compact_encoding
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
UDX_PKG="$ROOT/packages/udx"
INTEROP="$ROOT/tool/interop_node"
PREFIX="${TMPDIR:-/tmp}/hyperdart_udx_$$"
# Ensure native lib built
if [[ ! -f "$UDX_PKG/native/build/libhyper_udx.dylib" && ! -f "$UDX_PKG/native/build/libhyper_udx.so" ]]; then
bash "$UDX_PKG/tool/build_native.sh"
fi
# Ensure Node udx-native available
if [[ ! -d "$INTEROP/node_modules/udx-native" ]]; then
(cd "$INTEROP" && npm install [email protected] --no-save 2>/dev/null || npm install [email protected])
fi
cleanup() {
rm -f "$PREFIX.node" "$PREFIX.dart"
kill ${NODE_PID:-} 2>/dev/null || true
}
trap cleanup EXIT
echo "=== Dart client → Node echo-server ==="
rm -f "$PREFIX.node" "$PREFIX.dart"
node "$INTEROP/udx_node_peer.js" echo-server "$PREFIX" &
NODE_PID=$!
(cd "$UDX_PKG" && dart run bin/node_interop.dart echo-client "$PREFIX")
wait $NODE_PID
NODE_PID=
echo "=== Node client → Dart echo-server ==="
rm -f "$PREFIX.node" "$PREFIX.dart"
node "$INTEROP/udx_node_peer.js" echo-client "$PREFIX" &
NODE_PID=$!
(cd "$UDX_PKG" && dart run bin/node_interop.dart echo-server "$PREFIX")
wait $NODE_PID
NODE_PID=
echo "UDX_NODE_INTEROP_OK"
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env node
/**
* TCP responder for Dart↔Node secret-stream interop.
* Usage: node secret_stream_server.js
* Prints: PORT <n>
* Expects one encrypted message "ping-from-dart", replies "pong-from-node", exits.
*/
const net = require('net')
const path = require('path')
const ROOT = '/Users/raven/dev/pearcli/holepunch-repos/holepunchto_repos'
const NoiseSecretStream = require(path.join(ROOT, 'hyperswarm-secret-stream'))
const server = net.createServer((socket) => {
const s = new NoiseSecretStream(false, socket)
s.on('data', (data) => {
const msg = Buffer.from(data).toString()
if (msg === 'ping-from-dart') {
s.write(Buffer.from('pong-from-node'))
}
})
s.on('error', (err) => {
console.error('stream error', err)
process.exit(1)
})
})
server.listen(0, '127.0.0.1', () => {
const { port } = server.address()
console.log('PORT', port)
})
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env node
/**
* Node udx-native peer for Dart interop.
*
* Coord files (prefix from argv[2]):
* <prefix>.node — written by this process: "<port>"
* <prefix>.dart — written by Dart: "<port>"
*
* Stream ids: Node=2, Dart=1 (matches udx-native makeTwoStreams style).
*
* Mode argv[1]:
* echo-server — wait for Dart connect, echo one message, exit
* echo-client — connect to Dart, send ping, expect pong, exit
*/
const fs = require('fs')
const path = require('path')
const UDX = require('udx-native')
const mode = process.argv[2] || 'echo-server'
const prefix = process.argv[3]
if (!prefix) {
console.error('usage: node udx_node_peer.js <echo-server|echo-client> <coord-prefix>')
process.exit(2)
}
const NODE_ID = 2
const DART_ID = 1
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms))
}
async function waitFile(file, timeoutMs = 10000) {
const start = Date.now()
while (!fs.existsSync(file)) {
if (Date.now() - start > timeoutMs) throw new Error('timeout waiting for ' + file)
await sleep(20)
}
return fs.readFileSync(file, 'utf8').trim()
}
async function main() {
const udx = new UDX()
const socket = udx.createSocket()
socket.bind(0, '127.0.0.1')
const nodePort = socket.address().port
fs.writeFileSync(prefix + '.node', String(nodePort))
console.log('NODE_PORT', nodePort)
const dartPort = parseInt(await waitFile(prefix + '.dart'), 10)
console.log('DART_PORT', dartPort)
const stream = udx.createStream(NODE_ID)
stream.connect(socket, DART_ID, dartPort, '127.0.0.1')
if (mode === 'echo-server') {
await new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('echo-server timeout')), 10000)
stream.once('data', (data) => {
clearTimeout(t)
const msg = Buffer.from(data).toString()
console.log('RECV', msg)
stream.write(Buffer.from('echo: ' + msg))
stream.end()
resolve()
})
})
await new Promise((r) => stream.once('close', r))
console.log('NODE_ECHO_SERVER_OK')
} else if (mode === 'echo-client') {
await new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('echo-client timeout')), 10000)
stream.once('data', (data) => {
clearTimeout(t)
const msg = Buffer.from(data).toString()
console.log('RECV', msg)
if (msg !== 'pong-from-dart') {
reject(new Error('unexpected: ' + msg))
return
}
resolve()
})
stream.write(Buffer.from('ping-from-node'))
})
stream.end()
await new Promise((r) => stream.once('close', r))
console.log('NODE_ECHO_CLIENT_OK')
} else {
throw new Error('unknown mode ' + mode)
}
socket.close()
// Give uv a tick to settle
await sleep(50)
process.exit(0)
}
main().catch((err) => {
console.error(err)
process.exit(1)
})