@claw-link/gateway-host 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/native/harness-core.darwin-arm64.node +0 -0
- package/native/index.d.ts +23 -8
- package/package.json +1 -1
- package/scripts/p2p-test.js +5 -4
- package/src/transport/p2p.js +42 -38
- package/src/worker.js +3 -2
|
Binary file
|
package/native/index.d.ts
CHANGED
|
@@ -34,20 +34,35 @@ export declare function assemble(parts: Array<Buffer>, expectedCid: string): Buf
|
|
|
34
34
|
/** Native-core version (so the host can log which acceleration tier is active). */
|
|
35
35
|
export declare function coreVersion(): string
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* An **iroh** peer-to-peer Capsule-byte transport (AHP H2H data plane).
|
|
38
38
|
*
|
|
39
39
|
* Bytes are AEAD-encrypted, content-addressed (`b3_<blake3>`), stored locally, and fetched from a
|
|
40
|
-
* peer's
|
|
40
|
+
* peer over iroh's mutually-authenticated, NAT-traversing QUIC (hole-punch + relay fallback). Peers
|
|
41
|
+
* are addressed by their full `EndpointAddr` (NodeId + relay + direct addrs), handed over your own
|
|
42
|
+
* signaling — NOT via public discovery. See `transport.rs` for the full security posture.
|
|
41
43
|
*/
|
|
42
44
|
export declare class Transport {
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Build the iroh endpoint and start serving.
|
|
47
|
+
*
|
|
48
|
+
* * `secretKeyHex` — 32-byte Ed25519 secret key as hex (the node's persistent identity; its
|
|
49
|
+
* public key is the iroh NodeId). If omitted, a fresh key is generated — read it back via
|
|
50
|
+
* `secretKeyHex()` and persist it for a stable identity across restarts.
|
|
51
|
+
* * `relayUrl` — optional relay base URL for connectivity / hole-punch. The relay is UNTRUSTED
|
|
52
|
+
* (it only ever relays E2E-encrypted QUIC). Omit to use iroh's standard n0 relays.
|
|
53
|
+
*/
|
|
54
|
+
static create(secretKeyHex?: string | undefined | null, relayUrl?: string | undefined | null): Promise<Transport>
|
|
55
|
+
/**
|
|
56
|
+
* This node's full reachability as a JSON string (NodeId + relay url + known direct socket
|
|
57
|
+
* addrs). Store it and hand it to a peer so it can dial us — no public discovery involved.
|
|
58
|
+
*/
|
|
59
|
+
nodeAddr(): Promise<string>
|
|
60
|
+
/** The node's Ed25519 secret key as hex (so the host can persist a stable identity). */
|
|
61
|
+
secretKeyHex(): string
|
|
47
62
|
/** AEAD-encrypt `bytes` with the 32-byte `key`, store the ciphertext, and return its cid. */
|
|
48
63
|
add(bytes: Buffer, key: Buffer): string
|
|
49
|
-
/** Fetch `cid` from `
|
|
50
|
-
fetch(cid: string,
|
|
64
|
+
/** Fetch `cid` from `peerNodeAddr` over iroh, verify integrity, and AEAD-decrypt with `key`. */
|
|
65
|
+
fetch(cid: string, peerNodeAddr: string, key: Buffer): Promise<Buffer>
|
|
51
66
|
/** Does the local store hold this cid? */
|
|
52
67
|
has(cid: string): boolean
|
|
53
68
|
/** Close the endpoint (stops serving; in-flight transfers drain). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claw-link/gateway-host",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "ClawLink Host Gateway — a secure, outbound-only worker that bridges a local agent CLI (OpenClaw, Hermes, Claude, Codex, Cursor) to your ClawLink agents. No inbound ports; authenticated per-agent by a Host Token.",
|
|
5
5
|
"homepage": "https://claw-link.co",
|
|
6
6
|
"bin": {
|
package/scripts/p2p-test.js
CHANGED
|
@@ -12,16 +12,17 @@ async function run() {
|
|
|
12
12
|
console.log('To enable acceleration, install the @claw-link/harness-core binary for your platform.');
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
|
-
console.log(`Native H2H core v${core.version} — running QUIC P2P self-test (loopback)…\n`);
|
|
15
|
+
console.log(`Native H2H core v${core.version} — running iroh QUIC P2P self-test (loopback)…\n`);
|
|
16
16
|
const a = await core.Transport.create();
|
|
17
17
|
const b = await core.Transport.create();
|
|
18
|
+
const aAddr = await a.nodeAddr();
|
|
18
19
|
try {
|
|
19
20
|
const key = crypto.randomBytes(32);
|
|
20
21
|
for (const n of [4 * 1024, 1024 * 1024, 8 * 1024 * 1024]) {
|
|
21
22
|
const data = crypto.randomBytes(n);
|
|
22
23
|
const cid = a.add(data, key);
|
|
23
24
|
const t0 = Date.now();
|
|
24
|
-
const got = await b.fetch(cid,
|
|
25
|
+
const got = await b.fetch(cid, aAddr, key);
|
|
25
26
|
const ms = Date.now() - t0;
|
|
26
27
|
if (Buffer.compare(got, data) !== 0) throw new Error(`integrity mismatch at ${n} bytes`);
|
|
27
28
|
const mib = (n / (1024 * 1024));
|
|
@@ -29,9 +30,9 @@ async function run() {
|
|
|
29
30
|
}
|
|
30
31
|
const cid = a.add(Buffer.from('secret'), key);
|
|
31
32
|
let wrongKey = false;
|
|
32
|
-
try { await b.fetch(cid,
|
|
33
|
+
try { await b.fetch(cid, aAddr, crypto.randomBytes(32)); } catch { wrongKey = true; }
|
|
33
34
|
let unknown = false;
|
|
34
|
-
try { await b.fetch('b3_0000',
|
|
35
|
+
try { await b.fetch('b3_0000', aAddr, key); } catch { unknown = true; }
|
|
35
36
|
console.log(`\n AEAD: wrong key rejected ${wrongKey ? 'OK' : 'FAIL'}`);
|
|
36
37
|
console.log(` integrity: unknown cid rejected ${unknown ? 'OK' : 'FAIL'}`);
|
|
37
38
|
if (!wrongKey || !unknown) throw new Error('security checks failed');
|
package/src/transport/p2p.js
CHANGED
|
@@ -1,61 +1,65 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
// Host-side H2H transport orchestration
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Host-side H2H transport orchestration over the native iroh core: secure, NAT-traversing QUIC.
|
|
3
|
+
// Peers are addressed by their iroh NodeAddr (NodeId = Ed25519 public key → mutual auth, no MITM;
|
|
4
|
+
// hole-punch + relay fallback handle NAT). Discovery rides our authenticated host-bridge signaling
|
|
5
|
+
// (transport_announce/resolve), NOT iroh's public DNS. ACCELERATION, NEVER A DEPENDENCY — no core,
|
|
6
|
+
// or a fetch that can't connect, falls back to the central relay (the caller handles that).
|
|
5
7
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// consumer: buf = await p2p.fetch(bridge, cid, key) // resolve a peer + P2P fetch, else null
|
|
8
|
+
// producer: cid = await p2p.offer(bridge, plaintext, key) // store + announce our NodeAddr
|
|
9
|
+
// consumer: buf = await p2p.fetch(bridge, cid, key) // resolve peer NodeAddr + fetch, else null
|
|
9
10
|
//
|
|
10
|
-
// `key` is
|
|
11
|
-
const
|
|
11
|
+
// `key` is the 32-byte per-blob AEAD key (the authorization layer on top of the authenticated link).
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
12
14
|
const core = require('./core');
|
|
15
|
+
const config = require('../config');
|
|
13
16
|
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
// (Cross-network/NAT hole-punching is a later rung; a peer that can't be reached falls back to relay.)
|
|
17
|
-
function lanIpv4() {
|
|
18
|
-
for (const list of Object.values(os.networkInterfaces())) {
|
|
19
|
-
for (const i of list || []) {
|
|
20
|
-
if (i && i.family === 'IPv4' && !i.internal) return i.address;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
17
|
+
// Stable node identity (Ed25519 secret key) so a host keeps the same NodeId across restarts.
|
|
18
|
+
const KEY_PATH = path.join(config.HOME_DIR, 'transport.key');
|
|
25
19
|
|
|
26
20
|
class P2P {
|
|
27
21
|
constructor(logger) {
|
|
28
22
|
this.logger = logger || { info() {}, warn() {} };
|
|
29
23
|
this.node = null;
|
|
30
24
|
this.starting = null;
|
|
31
|
-
this.
|
|
25
|
+
this.addr = null; // our NodeAddr (JSON string) to hand to peers
|
|
32
26
|
}
|
|
33
27
|
|
|
34
28
|
available() {
|
|
35
29
|
return !!(core.available && core.Transport);
|
|
36
30
|
}
|
|
37
31
|
|
|
38
|
-
|
|
32
|
+
_loadKey() {
|
|
33
|
+
try { const k = fs.readFileSync(KEY_PATH, 'utf8').trim(); if (/^[0-9a-f]{64}$/i.test(k)) return k; } catch { /* none */ }
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
_saveKey(hex) {
|
|
37
|
+
try { if (config.ensureHomeDir) config.ensureHomeDir(); fs.writeFileSync(KEY_PATH, hex, { mode: 0o600 }); }
|
|
38
|
+
catch (e) { this.logger.warn('could not persist transport identity:', e.message); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Start the iroh node once (idempotent). Returns our NodeAddr string, or null. */
|
|
39
42
|
async start() {
|
|
40
43
|
if (!this.available()) return null;
|
|
41
|
-
if (this.
|
|
44
|
+
if (this.addr) return this.addr;
|
|
42
45
|
if (!this.starting) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
})
|
|
52
|
-
|
|
46
|
+
const relayUrl = process.env.CLAWHOST_RELAY_URL || undefined; // undefined → default relays
|
|
47
|
+
this.starting = (async () => {
|
|
48
|
+
const keyHex = this._loadKey() || undefined;
|
|
49
|
+
const node = await core.Transport.create(keyHex, relayUrl);
|
|
50
|
+
this.node = node;
|
|
51
|
+
if (!keyHex) this._saveKey(node.secretKeyHex()); // persist a freshly-generated identity
|
|
52
|
+
this.addr = await node.nodeAddr();
|
|
53
|
+
let id = '?'; try { id = JSON.parse(this.addr).id.slice(0, 12); } catch { /* keep ? */ }
|
|
54
|
+
this.logger.info(`p2p transport up (core ${core.version}) node ${id}…${relayUrl ? ` relay ${relayUrl}` : ' (default relays)'}`);
|
|
55
|
+
return node;
|
|
56
|
+
})().catch((e) => { this.logger.warn('p2p transport start failed (using central relay):', e.message); this.node = null; this.addr = null; return null; });
|
|
53
57
|
}
|
|
54
58
|
await this.starting;
|
|
55
|
-
return this.
|
|
59
|
+
return this.addr;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
/** Offer a blob for P2P fetch: AEAD-encrypt + store locally, announce cid→
|
|
62
|
+
/** Offer a blob for P2P fetch: AEAD-encrypt + store locally, announce cid→our NodeAddr. Returns cid|null. */
|
|
59
63
|
async offer(bridge, plaintext, key) {
|
|
60
64
|
const addr = await this.start();
|
|
61
65
|
if (!addr || !this.node) return null;
|
|
@@ -65,8 +69,8 @@ class P2P {
|
|
|
65
69
|
return cid;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
/** Fetch a blob by cid
|
|
69
|
-
* signal the caller should fall back to the central relay. */
|
|
72
|
+
/** Fetch a blob by cid: resolve a peer NodeAddr + fetch over iroh. Returns plaintext Buffer, or null
|
|
73
|
+
* to signal the caller should fall back to the central relay. */
|
|
70
74
|
async fetch(bridge, cid, key) {
|
|
71
75
|
if (!this.available()) return null;
|
|
72
76
|
await this.start();
|
|
@@ -77,8 +81,7 @@ class P2P {
|
|
|
77
81
|
try {
|
|
78
82
|
const t0 = Date.now();
|
|
79
83
|
const buf = await this.node.fetch(cid, peer.addr, key);
|
|
80
|
-
|
|
81
|
-
this.logger.info(`p2p fetch ${cid.slice(0, 14)}… ${buf.length}B from ${peer.addr} in ${ms}ms`);
|
|
84
|
+
this.logger.info(`p2p fetch ${cid.slice(0, 14)}… ${buf.length}B in ${Date.now() - t0}ms`);
|
|
82
85
|
bridge.transportSession(cid, 'p2p', buf.length).catch(() => {});
|
|
83
86
|
return buf;
|
|
84
87
|
} catch (e) {
|
|
@@ -90,6 +93,7 @@ class P2P {
|
|
|
90
93
|
close() {
|
|
91
94
|
if (this.node) { try { this.node.close(); } catch { /* ignore */ } this.node = null; }
|
|
92
95
|
this.starting = null;
|
|
96
|
+
this.addr = null;
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
99
|
|
package/src/worker.js
CHANGED
|
@@ -19,8 +19,9 @@ const VERSION = (() => { try { return require('../package.json').version; } catc
|
|
|
19
19
|
// Generous (matches the server's 15-min reaper) so legitimate long generations,
|
|
20
20
|
// which keep producing output, are never the ones that hit it.
|
|
21
21
|
const JOB_MAX_MS = Number(process.env.CLAWHOST_JOB_MAX_MS) || 15 * 60 * 1000;
|
|
22
|
-
// Offer outputs larger than this over P2P (
|
|
23
|
-
|
|
22
|
+
// Offer outputs larger than this over P2P (must match the engine's TRANSPORT_MIN_BYTES).
|
|
23
|
+
// Lowered to 1 KiB for validation so a normal reply triggers P2P; raise for production.
|
|
24
|
+
const TRANSPORT_MIN_BYTES = Number(process.env.CLAWHOST_TRANSPORT_MIN) || 1024;
|
|
24
25
|
|
|
25
26
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
26
27
|
let stopping = false;
|