openclaw-core 1.0.0

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.
@@ -0,0 +1,118 @@
1
+ import crypto from "node:crypto";
2
+ import { EventEmitter } from "node:events";
3
+ export class SignalManager extends EventEmitter {
4
+ agentDid;
5
+ privateKey = null;
6
+ trustedPeers = new Map();
7
+ groups = new Map();
8
+ db = null;
9
+ memory = null;
10
+ constructor(agentDid, memory, db) {
11
+ super();
12
+ this.setMaxListeners(100);
13
+ this.agentDid = agentDid;
14
+ this.memory = memory ?? null;
15
+ this.db = db ?? null;
16
+ }
17
+ setAgentId(did) { this.agentDid = did; }
18
+ setPrivateKey(pk) { this.privateKey = pk; }
19
+ sign(payload) {
20
+ if (this.privateKey) {
21
+ try {
22
+ const sign = crypto.createSign("ed25519");
23
+ sign.update(payload);
24
+ const keyBuf = Buffer.from(this.privateKey, "hex");
25
+ const privKey = crypto.createPrivateKey({ key: keyBuf, format: "der", type: "pkcs8" });
26
+ return sign.sign(privKey).toString("hex");
27
+ }
28
+ catch {
29
+ }
30
+ }
31
+ const secret = process.env.SIGNAL_HMAC_SECRET || "lobster-default-hmac-key";
32
+ return crypto.createHmac("sha256", secret).update(payload).digest("hex");
33
+ }
34
+ verify(payload, signature, publicKey) {
35
+ if (publicKey) {
36
+ try {
37
+ const verify = crypto.createVerify("ed25519");
38
+ verify.update(payload);
39
+ const pubKey = crypto.createPublicKey({ key: Buffer.from(publicKey, "hex"), format: "der", type: "spki" });
40
+ return verify.verify(pubKey, Buffer.from(signature, "hex"));
41
+ }
42
+ catch { }
43
+ }
44
+ const expected = this.sign(payload);
45
+ return crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(signature, "hex"));
46
+ }
47
+ async sendTo(targetDid, type, payload) {
48
+ const canonical = JSON.stringify({ from: this.agentDid, to: targetDid, type, payload, ts: Date.now() });
49
+ const signal = {
50
+ id: crypto.randomUUID(),
51
+ from: this.agentDid,
52
+ to: targetDid,
53
+ type,
54
+ payload,
55
+ signature: this.sign(canonical),
56
+ timestamp: Date.now(),
57
+ };
58
+ this.emit(type, signal);
59
+ this.emit("*", signal);
60
+ if (this.db) {
61
+ await this.db.query("INSERT INTO signals { id: $id, from: $from, to: $to, type: $type, payload: $payload, signature: $sig, timestamp: time::now() }", { id: signal.id, from: signal.from, to: signal.to, type: signal.type, payload: signal.payload, sig: signal.signature }).catch((e) => console.error("[SignalManager] DB persist failed:", e));
62
+ }
63
+ return signal;
64
+ }
65
+ async sendSecure(targetDid, content) {
66
+ const peer = this.trustedPeers.get(targetDid);
67
+ let encryptedContent = content;
68
+ let encrypted = false;
69
+ if (peer?.publicKey) {
70
+ try {
71
+ const sharedKey = crypto.createHash("sha256")
72
+ .update(`${this.agentDid}:${targetDid}`)
73
+ .digest();
74
+ const iv = crypto.randomBytes(12);
75
+ const cipher = crypto.createCipheriv("aes-256-gcm", sharedKey, iv);
76
+ const enc = Buffer.concat([cipher.update(content, "utf8"), cipher.final()]);
77
+ const tag = cipher.getAuthTag();
78
+ encryptedContent = JSON.stringify({
79
+ iv: iv.toString("hex"),
80
+ data: enc.toString("hex"),
81
+ tag: tag.toString("hex"),
82
+ });
83
+ encrypted = true;
84
+ }
85
+ catch (e) {
86
+ console.warn("[SignalManager] E2EE encryption failed, sending plaintext.", e);
87
+ }
88
+ }
89
+ return this.sendTo(targetDid, "SECURE_MESSAGE", { content: encryptedContent, encrypted });
90
+ }
91
+ async addTrustedPeer(did, publicKey) {
92
+ this.trustedPeers.set(did, { did, publicKey, addedAt: Date.now() });
93
+ if (this.db) {
94
+ await this.db.query("UPSERT trusted_peers SET did=$did, public_key=$pk, added_at=time::now()", { did, pk: publicKey }).catch(() => { });
95
+ }
96
+ }
97
+ removePeer(did) { this.trustedPeers.delete(did); }
98
+ async joinGroup(groupId) {
99
+ if (!this.groups.has(groupId))
100
+ this.groups.set(groupId, new Set());
101
+ this.groups.get(groupId).add(this.agentDid);
102
+ }
103
+ async sync(since) {
104
+ if (!this.db)
105
+ return [];
106
+ const result = await this.db.query("SELECT * FROM signals WHERE timestamp > $since ORDER BY timestamp ASC", { since });
107
+ if (Array.isArray(result) && result[0] && typeof result[0] === 'object' && 'result' in result[0]) {
108
+ return result[0].result;
109
+ }
110
+ return (Array.isArray(result) ? result : []);
111
+ }
112
+ async queryAuditLogs(limit = 50) {
113
+ if (!this.db)
114
+ return [];
115
+ const result = await this.db.query("SELECT * FROM signals ORDER BY timestamp DESC LIMIT $limit", { limit });
116
+ return result[0]?.result || result[0] || [];
117
+ }
118
+ }
@@ -0,0 +1,47 @@
1
+ export interface DiscoveredNode {
2
+ did: string;
3
+ url: string;
4
+ publicKey: string;
5
+ source: 'MDNS' | 'DHT' | 'MANUAL' | 'GOSSIP';
6
+ discoveredAt: Date;
7
+ }
8
+ export interface IPeerRegistry {
9
+ get(did: string): {
10
+ status: string;
11
+ } | undefined;
12
+ connect(url: string, isLocal: boolean): Promise<void>;
13
+ }
14
+ export declare class PeerDiscovery {
15
+ private localDid;
16
+ private localUrl;
17
+ private localPublicKey;
18
+ private peerRegistry;
19
+ private db;
20
+ private mdnsSocket;
21
+ private discoveryTimer;
22
+ private announceTimer;
23
+ private dhtNodes;
24
+ private knownDids;
25
+ private running;
26
+ private dhtRoutingTable;
27
+ private dhtPort;
28
+ private dhtSocket;
29
+ constructor(localDid: string, localUrl: string, localPublicKey: string);
30
+ updateIdentity(did: string, url: string, publicKey: string): void;
31
+ init(peerRegistry: IPeerRegistry, db?: {
32
+ query: (q: string, p?: Record<string, unknown>) => Promise<unknown>;
33
+ }): Promise<void>;
34
+ private startMDNS;
35
+ private handleMDNSMessage;
36
+ private announceMDNS;
37
+ private startDHT;
38
+ private bootstrapDHT;
39
+ private handleDHTMessage;
40
+ private respondToFindNode;
41
+ private announceDHT;
42
+ private onDiscovery;
43
+ announce(): void;
44
+ scan(): Promise<void>;
45
+ getDiscovered(): DiscoveredNode[];
46
+ destroy(): void;
47
+ }
@@ -0,0 +1,217 @@
1
+ import dgram from "node:dgram";
2
+ const MDNS_MULTICAST_ADDR = '224.0.0.251';
3
+ const MDNS_PORT = 5353;
4
+ const MDNS_SERVICE_NAME = '_lobster._tcp.local';
5
+ const DHT_BOOTSTRAP_NODES = [
6
+ 'router.bittorrent.com:6881',
7
+ 'dht.transmissionbt.com:6881',
8
+ 'router.utorrent.com:6881',
9
+ ];
10
+ const DISCOVERY_INTERVAL = 60_000;
11
+ const ANNOUNCE_INTERVAL = 30_000;
12
+ export class PeerDiscovery {
13
+ localDid;
14
+ localUrl;
15
+ localPublicKey;
16
+ peerRegistry = null;
17
+ db = null;
18
+ mdnsSocket = null;
19
+ discoveryTimer = null;
20
+ announceTimer = null;
21
+ dhtNodes = new Map();
22
+ knownDids = new Set();
23
+ running = false;
24
+ dhtRoutingTable = new Map();
25
+ dhtPort;
26
+ dhtSocket = null;
27
+ constructor(localDid, localUrl, localPublicKey) {
28
+ this.localDid = localDid;
29
+ this.localUrl = localUrl;
30
+ this.localPublicKey = localPublicKey;
31
+ this.knownDids.add(localDid);
32
+ this.dhtPort = 6881 + Math.floor(Math.random() * 1000);
33
+ }
34
+ updateIdentity(did, url, publicKey) {
35
+ this.localDid = did;
36
+ this.localUrl = url;
37
+ this.localPublicKey = publicKey;
38
+ this.knownDids.add(did);
39
+ }
40
+ async init(peerRegistry, db) {
41
+ this.peerRegistry = peerRegistry;
42
+ this.db = db ?? null;
43
+ this.running = true;
44
+ this.startMDNS();
45
+ this.startDHT();
46
+ this.discoveryTimer = setInterval(() => { this.scan(); }, DISCOVERY_INTERVAL);
47
+ this.announceTimer = setInterval(() => { this.announce(); }, ANNOUNCE_INTERVAL);
48
+ setTimeout(() => this.announce(), 2000);
49
+ }
50
+ startMDNS() {
51
+ try {
52
+ this.mdnsSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
53
+ this.mdnsSocket.on('message', (msg, rinfo) => { this.handleMDNSMessage(msg, rinfo); });
54
+ this.mdnsSocket.on('error', () => { });
55
+ this.mdnsSocket.bind(MDNS_PORT, () => {
56
+ try {
57
+ this.mdnsSocket.addMembership(MDNS_MULTICAST_ADDR);
58
+ }
59
+ catch {
60
+ }
61
+ });
62
+ }
63
+ catch {
64
+ }
65
+ }
66
+ handleMDNSMessage(msg, _rinfo) {
67
+ try {
68
+ const payload = JSON.parse(msg.toString());
69
+ if (payload.service !== MDNS_SERVICE_NAME)
70
+ return;
71
+ if (payload.did === this.localDid)
72
+ return;
73
+ this.onDiscovery({ did: payload.did, url: payload.url, publicKey: payload.publicKey, source: 'MDNS', discoveredAt: new Date() });
74
+ }
75
+ catch { }
76
+ }
77
+ announceMDNS() {
78
+ if (!this.mdnsSocket)
79
+ return;
80
+ const announcement = JSON.stringify({
81
+ service: MDNS_SERVICE_NAME, did: this.localDid, url: this.localUrl,
82
+ publicKey: this.localPublicKey, timestamp: Date.now(),
83
+ });
84
+ try {
85
+ this.mdnsSocket.send(announcement, MDNS_PORT, MDNS_MULTICAST_ADDR, () => { });
86
+ }
87
+ catch { }
88
+ }
89
+ startDHT() {
90
+ try {
91
+ this.dhtSocket = dgram.createSocket('udp4');
92
+ this.dhtSocket.on('message', (msg, rinfo) => { this.handleDHTMessage(msg, rinfo); });
93
+ this.dhtSocket.on('error', () => { });
94
+ this.dhtSocket.bind(this.dhtPort, () => { this.bootstrapDHT(); });
95
+ }
96
+ catch { }
97
+ }
98
+ bootstrapDHT() {
99
+ if (this.db) {
100
+ this.db.query("SELECT did, url, public_key FROM peer_nodes WHERE status != 'OFFLINE' LIMIT 20")
101
+ .then((result) => {
102
+ const rows = result[0]?.result || result[0] || [];
103
+ for (const row of rows) {
104
+ if (row.did !== this.localDid) {
105
+ this.dhtNodes.set(row.did, { url: row.url, did: row.did, pubKey: row.public_key || '' });
106
+ }
107
+ }
108
+ }).catch(() => { });
109
+ }
110
+ }
111
+ handleDHTMessage(msg, rinfo) {
112
+ try {
113
+ const payload = JSON.parse(msg.toString());
114
+ switch (payload.type) {
115
+ case 'ANNOUNCE':
116
+ if (payload.did === this.localDid)
117
+ return;
118
+ this.dhtNodes.set(payload.did, { url: payload.url, did: payload.did, pubKey: payload.publicKey });
119
+ this.dhtRoutingTable.set(payload.did, { host: rinfo.address, port: rinfo.port, lastSeen: new Date() });
120
+ this.onDiscovery({ did: payload.did, url: payload.url, publicKey: payload.publicKey, source: 'DHT', discoveredAt: new Date() });
121
+ break;
122
+ case 'FIND_NODE':
123
+ this.respondToFindNode(payload, rinfo);
124
+ break;
125
+ case 'NODES':
126
+ if (Array.isArray(payload.nodes)) {
127
+ for (const node of payload.nodes) {
128
+ if (node.did !== this.localDid && !this.knownDids.has(node.did)) {
129
+ this.onDiscovery({ did: node.did, url: node.url, publicKey: node.publicKey || '', source: 'DHT', discoveredAt: new Date() });
130
+ }
131
+ }
132
+ }
133
+ break;
134
+ }
135
+ }
136
+ catch { }
137
+ }
138
+ respondToFindNode(_request, rinfo) {
139
+ const knownNodes = Array.from(this.dhtNodes.values()).slice(0, 10)
140
+ .map(n => ({ did: n.did, url: n.url, publicKey: n.pubKey }));
141
+ const response = JSON.stringify({ type: 'NODES', from: this.localDid, nodes: knownNodes, timestamp: Date.now() });
142
+ this.dhtSocket?.send(response, rinfo.port, rinfo.address, () => { });
143
+ }
144
+ announceDHT() {
145
+ if (!this.dhtSocket)
146
+ return;
147
+ const announcement = JSON.stringify({ type: 'ANNOUNCE', did: this.localDid, url: this.localUrl, publicKey: this.localPublicKey, timestamp: Date.now() });
148
+ for (const [, entry] of this.dhtRoutingTable) {
149
+ try {
150
+ this.dhtSocket.send(announcement, entry.port, entry.host, () => { });
151
+ }
152
+ catch { }
153
+ }
154
+ for (const bootstrap of DHT_BOOTSTRAP_NODES) {
155
+ const [host, portStr] = bootstrap.split(':');
156
+ try {
157
+ this.dhtSocket.send(announcement, parseInt(portStr), host, () => { });
158
+ }
159
+ catch { }
160
+ }
161
+ }
162
+ async onDiscovery(node) {
163
+ if (this.knownDids.has(node.did))
164
+ return;
165
+ this.knownDids.add(node.did);
166
+ if (this.db) {
167
+ this.db.query(`INSERT INTO agent_logs {
168
+ agent_id: $localDid, type: 'PEER_DISCOVERY', sub_type: $source,
169
+ payload: { did: $did, url: $url }, timestamp: time::now()
170
+ }`, { localDid: this.localDid, source: node.source, did: node.did, url: node.url }).catch(() => { });
171
+ }
172
+ if (this.peerRegistry) {
173
+ const existing = this.peerRegistry.get(node.did);
174
+ if (!existing || existing.status === 'OFFLINE') {
175
+ await this.peerRegistry.connect(node.url, node.source === 'MDNS').catch(() => { });
176
+ }
177
+ }
178
+ }
179
+ announce() {
180
+ this.announceMDNS();
181
+ this.announceDHT();
182
+ }
183
+ async scan() {
184
+ if (!this.running)
185
+ return;
186
+ this.announceMDNS();
187
+ if (this.dhtSocket) {
188
+ const findRequest = JSON.stringify({ type: 'FIND_NODE', from: this.localDid, target: '*', timestamp: Date.now() });
189
+ for (const [, entry] of this.dhtRoutingTable) {
190
+ try {
191
+ this.dhtSocket.send(findRequest, entry.port, entry.host, () => { });
192
+ }
193
+ catch { }
194
+ }
195
+ }
196
+ }
197
+ getDiscovered() {
198
+ return Array.from(this.dhtNodes.values()).map(n => ({
199
+ did: n.did, url: n.url, publicKey: n.pubKey, source: 'DHT', discoveredAt: new Date(),
200
+ }));
201
+ }
202
+ destroy() {
203
+ this.running = false;
204
+ if (this.discoveryTimer)
205
+ clearInterval(this.discoveryTimer);
206
+ if (this.announceTimer)
207
+ clearInterval(this.announceTimer);
208
+ try {
209
+ this.mdnsSocket?.close();
210
+ }
211
+ catch { }
212
+ try {
213
+ this.dhtSocket?.close();
214
+ }
215
+ catch { }
216
+ }
217
+ }
@@ -0,0 +1,102 @@
1
+ export interface PeerNode {
2
+ did: string;
3
+ url: string;
4
+ publicKey: string;
5
+ status: 'ONLINE' | 'OFFLINE' | 'CONNECTING';
6
+ rtt: number;
7
+ syncCursor: string;
8
+ lastHeartbeat: Date;
9
+ retryCount: number;
10
+ location?: {
11
+ lat: number;
12
+ lon: number;
13
+ };
14
+ trustScore: number;
15
+ machineFingerprint?: string;
16
+ isLocal?: boolean;
17
+ isLAN?: boolean;
18
+ jwt?: string;
19
+ liveQueryId?: string;
20
+ }
21
+ export interface OutboxMessage {
22
+ id: string;
23
+ target_did: string;
24
+ signal_type: string;
25
+ payload: Record<string, any>;
26
+ created_at: Date;
27
+ expire_at: Date;
28
+ attempts: number;
29
+ last_error?: string;
30
+ }
31
+ export interface HandshakePayload {
32
+ did: string;
33
+ publicKey: string;
34
+ url: string;
35
+ machineFingerprint?: string;
36
+ nodeInfo: {
37
+ version: string;
38
+ location?: {
39
+ lat: number;
40
+ lon: number;
41
+ };
42
+ capabilities: string[];
43
+ };
44
+ signature: string;
45
+ timestamp: number;
46
+ }
47
+ export interface HandshakeResponse {
48
+ accepted: boolean;
49
+ did: string;
50
+ publicKey: string;
51
+ url: string;
52
+ machineFingerprint?: string;
53
+ jwt: string;
54
+ changefeedCursor: string;
55
+ location?: {
56
+ lat: number;
57
+ lon: number;
58
+ };
59
+ }
60
+ export declare class PeerRegistry {
61
+ private peers;
62
+ private heartbeatTimers;
63
+ private reconnectTimers;
64
+ private db;
65
+ private signalManager;
66
+ private localDid;
67
+ private localUrl;
68
+ private localPublicKey;
69
+ private localMachineFingerprint;
70
+ private privateKey;
71
+ constructor(localDid: string, localUrl: string, localPublicKey: string);
72
+ init(db: any, signalManager: any): Promise<void>;
73
+ setPrivateKey(key: string): void;
74
+ setMachineFingerprint(fingerprint: string): void;
75
+ updateIdentity(did: string, url: string, publicKey: string): void;
76
+ private bootstrapSchema;
77
+ private restoreFromDB;
78
+ private validateUrl;
79
+ connect(remoteUrl: string, isLAN?: boolean): Promise<boolean>;
80
+ acceptHandshake(payload: HandshakePayload): Promise<HandshakeResponse | null>;
81
+ register(peer: PeerNode): Promise<void>;
82
+ remove(did: string): Promise<void>;
83
+ list(): PeerNode[];
84
+ listOnline(): PeerNode[];
85
+ get(did: string): PeerNode | undefined;
86
+ listByDistance(lat: number, lon: number, maxKm?: number): PeerNode[];
87
+ private haversine;
88
+ private startHeartbeat;
89
+ private stopHeartbeat;
90
+ private sendHeartbeat;
91
+ private handleHeartbeatFailure;
92
+ private markOffline;
93
+ private startReconnect;
94
+ private stopReconnect;
95
+ enqueueOutbox(targetDid: string, signalType: string, payload: Record<string, any>): Promise<void>;
96
+ flushOutbox(targetDid: string): Promise<void>;
97
+ syncChangeFeed(targetDid: string): Promise<void>;
98
+ queryTrustChain(targetDid: string, maxDepth?: number): Promise<any[]>;
99
+ private signPayload;
100
+ private signJWT;
101
+ destroy(): void;
102
+ }