@claw-link/gateway-host 0.3.3 → 0.3.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claw-link/gateway-host",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
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": {
@@ -6,6 +6,9 @@ const crypto = require('crypto');
6
6
  const core = require('../src/transport/core');
7
7
 
8
8
  async function run() {
9
+ // The self-test transfers over loopback, which the SSRF address-filter blocks by default — allow
10
+ // LAN/loopback just for this in-process test (does not affect the running service).
11
+ process.env.CLAWHOST_ALLOW_LAN = process.env.CLAWHOST_ALLOW_LAN || '1';
9
12
  if (!core.available || !core.Transport) {
10
13
  console.log('Native H2H core is NOT installed for this platform.');
11
14
  console.log('The host works fine — Capsule bytes route through the central relay (no P2P speedup).');
package/src/bridge.js CHANGED
@@ -46,7 +46,7 @@ class Bridge {
46
46
  capsuleGet(path) { return this._post('capsule_get', { path }); }
47
47
  // H2H peer discovery for the QUIC P2P transport (central relay is the fallback).
48
48
  transportAnnounce(cid, addr) { return this._post('transport_announce', { cid, addr }); }
49
- transportResolve(cid) { return this._post('transport_resolve', { cid }); }
49
+ transportResolve(cid, jobId) { return this._post('transport_resolve', { cid, job_id: jobId }); }
50
50
  transportSession(cid, mode, bytes) { return this._post('transport_session', { cid, mode, bytes }); }
51
51
  // DB floor for a transport ref: fetch the inline plaintext for a transport_cid this job may see.
52
52
  capsuleContent(cid, jobId) { return this._post('capsule_content', { cid, job_id: jobId }); }
@@ -30,7 +30,13 @@ class P2P {
30
30
  }
31
31
 
32
32
  _loadKey() {
33
- try { const k = fs.readFileSync(KEY_PATH, 'utf8').trim(); if (/^[0-9a-f]{64}$/i.test(k)) return k; } catch { /* none */ }
33
+ try {
34
+ const st = fs.statSync(KEY_PATH);
35
+ // The node secret is impersonation-grade — tighten perms if a backup/umask left it loose.
36
+ if (st.mode & 0o077) { try { fs.chmodSync(KEY_PATH, 0o600); this.logger.warn('tightened transport.key permissions to 600'); } catch { /* best effort */ } }
37
+ const k = fs.readFileSync(KEY_PATH, 'utf8').trim();
38
+ if (/^[0-9a-f]{64}$/i.test(k)) return k;
39
+ } catch { /* none */ }
34
40
  return null;
35
41
  }
36
42
  _saveKey(hex) {
@@ -59,25 +65,30 @@ class P2P {
59
65
  return this.addr;
60
66
  }
61
67
 
62
- /** Offer a blob for P2P fetch: AEAD-encrypt + store locally, announce cid→our NodeAddr. Returns cid|null. */
63
- async offer(bridge, plaintext, key) {
68
+ /** AEAD-encrypt + store a blob locally for P2P serving; returns its cid. Does NOT announce yet
69
+ * the capsule must be persisted first so the server can authorize the announce. null if unavailable. */
70
+ async addBlob(plaintext, key) {
64
71
  const addr = await this.start();
65
72
  if (!addr || !this.node) return null;
66
- let cid;
67
- try { cid = this.node.add(plaintext, key); } catch (e) { this.logger.warn('p2p add failed:', e.message); return null; }
68
- try { await bridge.transportAnnounce(cid, addr); } catch (e) { this.logger.warn('p2p announce failed:', e.message); }
69
- return cid;
73
+ try { return this.node.add(plaintext, key); } catch (e) { this.logger.warn('p2p add failed:', e.message); return null; }
74
+ }
75
+
76
+ /** Announce a previously-added cid to the directory (call AFTER the capsule is persisted, so the
77
+ * server can verify this host is the producer). */
78
+ async announce(bridge, cid) {
79
+ if (!this.addr || !cid) return;
80
+ try { await bridge.transportAnnounce(cid, this.addr); } catch (e) { this.logger.warn('p2p announce failed:', e.message); }
70
81
  }
71
82
 
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. */
74
- async fetch(bridge, cid, key) {
83
+ /** Fetch a blob by cid for a given job: resolve a peer NodeAddr (authorized via job.sees) + fetch
84
+ * over iroh. Returns plaintext Buffer, or null to fall back to the central relay. */
85
+ async fetch(bridge, cid, key, jobId) {
75
86
  if (!this.available()) return null;
76
87
  await this.start();
77
88
  if (!this.node) return null;
78
89
  let peer = null;
79
- try { const r = await bridge.transportResolve(cid); peer = r && r.peer; } catch { peer = null; }
80
- if (!peer || !peer.addr) return null; // no P2P source → relay
90
+ try { const r = await bridge.transportResolve(cid, jobId); peer = r && r.peer; } catch { peer = null; }
91
+ if (!peer || !peer.addr) return null; // no authorized P2P source → relay
81
92
  try {
82
93
  const t0 = Date.now();
83
94
  const buf = await this.node.fetch(cid, peer.addr, key);
package/src/worker.js CHANGED
@@ -37,7 +37,7 @@ async function resolveTransport(bridge, p2p, job) {
37
37
  let text = null;
38
38
  if (p2p && p2p.available()) {
39
39
  try {
40
- const buf = await p2p.fetch(bridge, ref.cid, Buffer.from(ref.key, 'hex'));
40
+ const buf = await p2p.fetch(bridge, ref.cid, Buffer.from(ref.key, 'hex'), job.id);
41
41
  if (buf) text = buf.toString('utf8');
42
42
  } catch (e) { logger.warn(`p2p resolve ${ref.cid.slice(0, 14)}… failed: ${e.message}`); }
43
43
  }
@@ -69,20 +69,23 @@ async function processJob(bridge, agentCfg, job, p2p) {
69
69
  try { await bridge.stream(job.id, seq++, evt.type, String(evt.content ?? '')); }
70
70
  catch (e) { logger.warn(`stream seq ${seq} failed: ${e.message}`); }
71
71
  }
72
- // H2H: offer a large result for P2P fetch by the next node's host (inline copy stays the floor).
73
- let transport;
72
+ // H2H: store a large result for P2P fetch by the next node's host (inline copy stays the floor).
73
+ // We add+ref it before complete(), but ANNOUNCE only AFTER complete() persists the capsule — so
74
+ // the directory can verify this host is the producer before listing it.
75
+ let transport, offerCid;
74
76
  try {
75
77
  const buf = Buffer.from(finalText, 'utf8');
76
78
  if (p2p && p2p.available() && job.harness && buf.length > TRANSPORT_MIN_BYTES) {
77
79
  const key = crypto.randomBytes(32);
78
- const cid = await p2p.offer(bridge, buf, key);
79
- if (cid) transport = { cid, key: key.toString('hex'), size: buf.length };
80
+ offerCid = await p2p.addBlob(buf, key);
81
+ if (offerCid) transport = { cid: offerCid, key: key.toString('hex'), size: buf.length };
80
82
  }
81
83
  } catch (e) { logger.warn(`p2p offer failed: ${e.message}`); }
82
84
  const meta = {};
83
85
  if (toolCalls.length) meta.tool_calls = toolCalls;
84
86
  if (transport) meta.transport = transport;
85
87
  await withRetry(() => bridge.complete(job.id, finalText, meta));
88
+ if (offerCid) await p2p.announce(bridge, offerCid);
86
89
  logger.info(`job ${job.id} done (${finalText.length} chars${transport ? `, P2P-offered ${transport.cid.slice(0, 14)}…` : ''})`);
87
90
  } catch (e) {
88
91
  logger.error(`job ${job.id} failed: ${e.message}`);