libp2p-mesh 2026.5.13 → 2026.5.14

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.
Files changed (44) hide show
  1. package/README.md +61 -23
  2. package/api.ts +1 -1
  3. package/dist/api.d.ts +1 -1
  4. package/dist/index.d.ts +0 -1
  5. package/dist/index.js +60 -24
  6. package/dist/runtime-setter-api.d.ts +4 -0
  7. package/dist/runtime-setter-api.js +19 -0
  8. package/dist/src/agent-tools.d.ts +63 -24
  9. package/dist/src/agent-tools.js +69 -34
  10. package/dist/src/channel.d.ts +1 -0
  11. package/dist/src/channel.js +20 -4
  12. package/dist/src/dht-registry.d.ts +38 -0
  13. package/dist/src/dht-registry.js +80 -0
  14. package/dist/src/inbound.d.ts +0 -3
  15. package/dist/src/inbound.js +29 -14
  16. package/dist/src/instance-id.d.ts +53 -0
  17. package/dist/src/instance-id.js +156 -0
  18. package/dist/src/mesh.js +310 -23
  19. package/dist/src/plugin.d.ts +1 -2
  20. package/dist/src/plugin.js +18 -30
  21. package/dist/src/types.d.ts +87 -0
  22. package/index.ts +60 -24
  23. package/openclaw.plugin.json +72 -33
  24. package/package.json +19 -7
  25. package/src/agent-tools.ts +69 -35
  26. package/src/channel.ts +25 -4
  27. package/src/dht-registry.ts +105 -0
  28. package/src/inbound.ts +35 -18
  29. package/src/instance-id.ts +221 -0
  30. package/src/mesh.ts +368 -27
  31. package/src/plugin.ts +25 -36
  32. package/src/types.ts +95 -0
  33. package/src/agent-tools-feishu.test.ts +0 -68
  34. package/src/config-schema.test.ts +0 -63
  35. package/src/feishu-channel.test.ts +0 -191
  36. package/src/feishu-channel.ts +0 -253
  37. package/src/feishu-client.test.ts +0 -303
  38. package/src/feishu-client.ts +0 -178
  39. package/src/feishu-e2e.test.ts +0 -90
  40. package/src/feishu-types.test.ts +0 -125
  41. package/src/feishu-types.ts +0 -51
  42. package/src/inbound-feishu.test.ts +0 -91
  43. package/src/index.ts +0 -1
  44. package/src/plugin-registration.test.ts +0 -60
package/src/inbound.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { P2PMessage } from "./types.js";
2
- import type { FeishuApiClient } from "./feishu-client.js";
2
+ import { verifyInstanceSignature } from "./instance-id.js";
3
3
 
4
4
  export type InboundHandlerDeps = {
5
5
  logger?: {
@@ -8,29 +8,46 @@ export type InboundHandlerDeps = {
8
8
  warn?: (msg: string) => void;
9
9
  error?: (msg: string) => void;
10
10
  };
11
- sendToChannel?: (channelId: string, target: string, text: string) => Promise<void>;
12
- feishuClient?: FeishuApiClient;
13
11
  };
14
12
 
15
13
  export function handleP2PInbound(msg: P2PMessage, deps: InboundHandlerDeps): void {
16
- const { logger, sendToChannel, feishuClient } = deps;
14
+ const { logger } = deps;
15
+ const instanceTag = msg.instanceId ? ` [instance: ${msg.instanceId}]` : "";
16
+ const signedTag = msg.signature ? " [signed]" : "";
17
17
 
18
- if (msg.type === "broadcast") {
19
- logger?.info?.(`[libp2p-mesh] Broadcast from ${msg.from} on topic ${msg.topic ?? "(none)"}: ${msg.payload}`);
20
- } else {
21
- logger?.info?.(`[libp2p-mesh] Direct message from ${msg.from}: ${msg.payload}`);
22
- }
23
-
24
- if (msg.type !== "broadcast" && sendToChannel && msg.payload) {
25
- const text = `[来自 ${msg.from}]\n${msg.payload}`;
26
- sendToChannel("libp2p-mesh", msg.from, text).catch((err) => {
27
- logger?.error?.(`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${err}`);
18
+ // Verify signature if present
19
+ if (msg.signature && msg.instanceId && msg.pubkey) {
20
+ const signedPayload = JSON.stringify({
21
+ id: msg.id,
22
+ type: msg.type,
23
+ from: msg.from,
24
+ to: msg.to,
25
+ topic: msg.topic,
26
+ payload: msg.payload,
27
+ timestamp: msg.timestamp,
28
+ instanceId: msg.instanceId,
28
29
  });
30
+ const valid = verifyInstanceSignature(
31
+ { id: msg.instanceId, name: "", pubkey: msg.pubkey, binding: "", bindingComponents: { username: "", hostname: "", platform: "" }, createdAt: 0 },
32
+ signedPayload,
33
+ msg.signature,
34
+ );
35
+ if (valid) {
36
+ logger?.info?.(`[libp2p-mesh] Verified signature from instance ${msg.instanceId}`);
37
+ } else {
38
+ logger?.warn?.(`[libp2p-mesh] Invalid signature from instance ${msg.instanceId}`);
39
+ }
40
+ } else if (msg.signature) {
41
+ logger?.warn?.(`[libp2p-mesh] Message has signature but no pubkey; cannot verify`);
29
42
  }
30
43
 
31
- if (feishuClient && msg.payload) {
32
- feishuClient.sendMessage(msg.from, msg.payload).catch(() => {
33
- logger?.warn?.("[libp2p-mesh] Failed to forward P2P message to Feishu");
34
- });
44
+ if (msg.type === "broadcast") {
45
+ logger?.info?.(
46
+ `[libp2p-mesh] Broadcast from ${msg.from}${instanceTag}${signedTag} on topic ${msg.topic ?? "(none)"}: ${msg.payload}`,
47
+ );
48
+ } else {
49
+ logger?.info?.(
50
+ `[libp2p-mesh] Direct message from ${msg.from}${instanceTag}${signedTag}: ${msg.payload}`,
51
+ );
35
52
  }
36
53
  }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Lightweight Instance Identity module inspired by BAID (Binding Agent ID).
3
+ *
4
+ * BAID core idea: bind multiple identity dimensions (name, code, profile, user)
5
+ * into a single cryptographic identity.
6
+ *
7
+ * Our lightweight adaptation:
8
+ * - Ed25519 keypair for self-sovereign identity (provable via signatures)
9
+ * - Multi-dimensional binding hash: username + hostname + platform
10
+ * - InstanceID format: name@<pubkey_b64url[0:12]>.<binding[0:8]>
11
+ * - Persistent storage in ~/.openclaw/libp2p/instance-id.json
12
+ */
13
+
14
+ import { createHash, generateKeyPairSync, sign, verify } from "node:crypto";
15
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
16
+ import { homedir, hostname, platform, userInfo } from "node:os";
17
+ import path from "node:path";
18
+
19
+ export interface InstanceIdentity {
20
+ /** Full InstanceID string, e.g. "alice-mac@AQIDBAUGBweI.7a3f9e2b" */
21
+ id: string;
22
+ /** Human-readable instance name */
23
+ name: string;
24
+ /** Base64url-encoded Ed25519 public key (SPKI/DER) */
25
+ pubkey: string;
26
+ /** Hex SHA-256 binding hash of environment dimensions */
27
+ binding: string;
28
+ /** Components that contributed to the binding hash */
29
+ bindingComponents: {
30
+ username: string;
31
+ hostname: string;
32
+ platform: string;
33
+ };
34
+ /** Timestamp when the identity was created */
35
+ createdAt: number;
36
+ }
37
+
38
+ interface PersistedIdentity extends InstanceIdentity {
39
+ /** Base64url-encoded Ed25519 private key (PKCS8/DER) — stored for signing */
40
+ privkey: string;
41
+ }
42
+
43
+ export interface InstanceIDOptions {
44
+ /** Custom instance name (defaults to "<username>-<hostname>") */
45
+ name?: string;
46
+ /** Custom storage path for the identity file */
47
+ customPath?: string;
48
+ }
49
+
50
+ function resolveInstanceIDPath(customPath?: string): string {
51
+ if (customPath) return customPath;
52
+ const stateDir = process.env.OPENCLAW_STATE_DIR;
53
+ if (stateDir) {
54
+ return path.join(stateDir, "libp2p", "instance-id.json");
55
+ }
56
+ return path.join(homedir(), ".openclaw", "libp2p", "instance-id.json");
57
+ }
58
+
59
+ function getBindingComponents(): InstanceIdentity["bindingComponents"] {
60
+ let username: string;
61
+ try {
62
+ username = userInfo().username;
63
+ } catch {
64
+ username = process.env.USER || process.env.USERNAME || "unknown";
65
+ }
66
+ return {
67
+ username,
68
+ hostname: hostname(),
69
+ platform: platform(),
70
+ };
71
+ }
72
+
73
+ function computeBindingHash(
74
+ components: InstanceIdentity["bindingComponents"],
75
+ ): string {
76
+ const data = `${components.username}::${components.hostname}::${components.platform}`;
77
+ return createHash("sha256").update(data).digest("hex");
78
+ }
79
+
80
+ function getDefaultName(): string {
81
+ const { username, hostname: h } = getBindingComponents();
82
+ const shortHost = h.split(".")[0];
83
+ return `${username}-${shortHost}`;
84
+ }
85
+
86
+ function generateEd25519KeyPair(): { publicKey: Buffer; privateKey: Buffer } {
87
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
88
+ publicKeyEncoding: { type: "spki", format: "der" },
89
+ privateKeyEncoding: { type: "pkcs8", format: "der" },
90
+ });
91
+ return { publicKey: Buffer.from(publicKey), privateKey: Buffer.from(privateKey) };
92
+ }
93
+
94
+ function pubkeyShort(pubkey: Buffer): string {
95
+ return pubkey.toString("base64url").slice(0, 12);
96
+ }
97
+
98
+ function bindingShort(binding: string): string {
99
+ return binding.slice(0, 8);
100
+ }
101
+
102
+ export function generateInstanceIdentity(options: InstanceIDOptions = {}): PersistedIdentity {
103
+ const name = options.name?.trim() || getDefaultName();
104
+ const { publicKey, privateKey } = generateEd25519KeyPair();
105
+ const bindingComponents = getBindingComponents();
106
+ const binding = computeBindingHash(bindingComponents);
107
+
108
+ const id = `${name}@${pubkeyShort(publicKey)}.${bindingShort(binding)}`;
109
+
110
+ return {
111
+ id,
112
+ name,
113
+ pubkey: publicKey.toString("base64url"),
114
+ privkey: privateKey.toString("base64url"),
115
+ binding,
116
+ bindingComponents,
117
+ createdAt: Date.now(),
118
+ };
119
+ }
120
+
121
+ export async function loadOrCreateInstanceIdentity(
122
+ options: InstanceIDOptions = {},
123
+ ): Promise<{ identity: InstanceIdentity; signMessage: (message: string) => string }> {
124
+ const filePath = resolveInstanceIDPath(options.customPath);
125
+
126
+ try {
127
+ const raw = await readFile(filePath, "utf8");
128
+ const persisted = JSON.parse(raw) as PersistedIdentity;
129
+
130
+ // Validate that the stored identity still matches this environment
131
+ const currentComponents = getBindingComponents();
132
+ const currentBinding = computeBindingHash(currentComponents);
133
+
134
+ if (persisted.binding !== currentBinding) {
135
+ // Environment changed (e.g. migrated to new machine) — regenerate
136
+ const fresh = generateInstanceIdentity(options);
137
+ await mkdir(path.dirname(filePath), { recursive: true });
138
+ await writeFile(filePath, JSON.stringify(fresh, null, 2));
139
+ return {
140
+ identity: stripPrivateKey(fresh),
141
+ signMessage: (msg) => signWithKey(Buffer.from(fresh.privkey, "base64url"), msg),
142
+ };
143
+ }
144
+
145
+ return {
146
+ identity: stripPrivateKey(persisted),
147
+ signMessage: (msg) => signWithKey(Buffer.from(persisted.privkey, "base64url"), msg),
148
+ };
149
+ } catch {
150
+ // File doesn't exist or is corrupt — create new identity
151
+ const fresh = generateInstanceIdentity(options);
152
+ await mkdir(path.dirname(filePath), { recursive: true });
153
+ await writeFile(filePath, JSON.stringify(fresh, null, 2));
154
+ return {
155
+ identity: stripPrivateKey(fresh),
156
+ signMessage: (msg) => signWithKey(Buffer.from(fresh.privkey, "base64url"), msg),
157
+ };
158
+ }
159
+ }
160
+
161
+ function stripPrivateKey(persisted: PersistedIdentity): InstanceIdentity {
162
+ const { privkey: _, ...identity } = persisted;
163
+ return identity;
164
+ }
165
+
166
+ function signWithKey(privateKey: Buffer, message: string): string {
167
+ const sig = sign(null, Buffer.from(message, "utf8"), {
168
+ key: privateKey,
169
+ format: "der",
170
+ type: "pkcs8",
171
+ });
172
+ return sig.toString("base64url");
173
+ }
174
+
175
+ export function verifyInstanceSignature(
176
+ identity: InstanceIdentity,
177
+ message: string,
178
+ signature: string,
179
+ ): boolean {
180
+ try {
181
+ const pubkeyBuffer = Buffer.from(identity.pubkey, "base64url");
182
+ return verify(
183
+ null,
184
+ Buffer.from(message, "utf8"),
185
+ { key: pubkeyBuffer, format: "der", type: "spki" },
186
+ Buffer.from(signature, "base64url"),
187
+ );
188
+ } catch {
189
+ return false;
190
+ }
191
+ }
192
+
193
+ export function verifyInstanceIDBinding(
194
+ identity: InstanceIdentity,
195
+ ): { valid: boolean; currentBinding: string; mismatch?: string } {
196
+ const currentComponents = getBindingComponents();
197
+ const currentBinding = computeBindingHash(currentComponents);
198
+
199
+ if (identity.binding !== currentBinding) {
200
+ return {
201
+ valid: false,
202
+ currentBinding,
203
+ mismatch: `Stored binding ${identity.binding.slice(0, 8)} does not match current environment ${currentBinding.slice(0, 8)}`,
204
+ };
205
+ }
206
+
207
+ return { valid: true, currentBinding };
208
+ }
209
+
210
+ export function formatInstanceIDForDisplay(identity: InstanceIdentity): string {
211
+ const { bindingComponents, createdAt } = identity;
212
+ const date = new Date(createdAt).toLocaleString();
213
+ return [
214
+ `Instance ID: ${identity.id}`,
215
+ `Name: ${identity.name}`,
216
+ `Pubkey: ${identity.pubkey.slice(0, 24)}...`,
217
+ `Binding: ${identity.binding.slice(0, 16)}...`,
218
+ `Bound to: ${bindingComponents.username}@${bindingComponents.hostname} (${bindingComponents.platform})`,
219
+ `Created: ${date}`,
220
+ ].join("\n");
221
+ }