@rookdaemon/agora 0.3.0 → 0.4.1
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/README.md +36 -0
- package/dist/chunk-2U4PZINT.js +453 -0
- package/dist/chunk-2U4PZINT.js.map +1 -0
- package/dist/chunk-D7Y66GFC.js +572 -0
- package/dist/chunk-D7Y66GFC.js.map +1 -0
- package/dist/{chunk-JUOGKXFN.js → chunk-IOHECZYT.js} +77 -557
- package/dist/chunk-IOHECZYT.js.map +1 -0
- package/dist/cli.js +41 -11
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +101 -14
- package/dist/index.js +105 -423
- package/dist/index.js.map +1 -1
- package/dist/relay/relay-server.d.ts +2 -0
- package/dist/relay/relay-server.js +27 -0
- package/dist/relay/relay-server.js.map +1 -0
- package/package.json +3 -1
- package/dist/chunk-JUOGKXFN.js.map +0 -1
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
// src/identity/keypair.ts
|
|
2
|
+
import { sign, verify, generateKeyPairSync } from "crypto";
|
|
3
|
+
function generateKeyPair() {
|
|
4
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
5
|
+
return {
|
|
6
|
+
publicKey: publicKey.export({ type: "spki", format: "der" }).toString("hex"),
|
|
7
|
+
privateKey: privateKey.export({ type: "pkcs8", format: "der" }).toString("hex")
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function signMessage(message, privateKeyHex) {
|
|
11
|
+
const messageBuffer = typeof message === "string" ? Buffer.from(message) : message;
|
|
12
|
+
const privateKey = Buffer.from(privateKeyHex, "hex");
|
|
13
|
+
const signature = sign(null, messageBuffer, {
|
|
14
|
+
key: privateKey,
|
|
15
|
+
format: "der",
|
|
16
|
+
type: "pkcs8"
|
|
17
|
+
});
|
|
18
|
+
return signature.toString("hex");
|
|
19
|
+
}
|
|
20
|
+
function verifySignature(message, signatureHex, publicKeyHex) {
|
|
21
|
+
const messageBuffer = typeof message === "string" ? Buffer.from(message) : message;
|
|
22
|
+
const signature = Buffer.from(signatureHex, "hex");
|
|
23
|
+
const publicKey = Buffer.from(publicKeyHex, "hex");
|
|
24
|
+
try {
|
|
25
|
+
return verify(null, messageBuffer, {
|
|
26
|
+
key: publicKey,
|
|
27
|
+
format: "der",
|
|
28
|
+
type: "spki"
|
|
29
|
+
}, signature);
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function exportKeyPair(keyPair) {
|
|
35
|
+
return {
|
|
36
|
+
publicKey: keyPair.publicKey,
|
|
37
|
+
privateKey: keyPair.privateKey
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function importKeyPair(publicKeyHex, privateKeyHex) {
|
|
41
|
+
const hexPattern = /^[0-9a-f]+$/i;
|
|
42
|
+
if (!hexPattern.test(publicKeyHex)) {
|
|
43
|
+
throw new Error("Invalid public key: must be a hex string");
|
|
44
|
+
}
|
|
45
|
+
if (!hexPattern.test(privateKeyHex)) {
|
|
46
|
+
throw new Error("Invalid private key: must be a hex string");
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
publicKey: publicKeyHex,
|
|
50
|
+
privateKey: privateKeyHex
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/message/envelope.ts
|
|
55
|
+
import { createHash } from "crypto";
|
|
56
|
+
function stableStringify(value) {
|
|
57
|
+
if (value === null || value === void 0) return JSON.stringify(value);
|
|
58
|
+
if (typeof value !== "object") return JSON.stringify(value);
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
return "[" + value.map(stableStringify).join(",") + "]";
|
|
61
|
+
}
|
|
62
|
+
const keys = Object.keys(value).sort();
|
|
63
|
+
const pairs = keys.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k]));
|
|
64
|
+
return "{" + pairs.join(",") + "}";
|
|
65
|
+
}
|
|
66
|
+
function canonicalize(type, sender, timestamp, payload, inReplyTo) {
|
|
67
|
+
const obj = { payload, sender, timestamp, type };
|
|
68
|
+
if (inReplyTo !== void 0) {
|
|
69
|
+
obj.inReplyTo = inReplyTo;
|
|
70
|
+
}
|
|
71
|
+
return stableStringify(obj);
|
|
72
|
+
}
|
|
73
|
+
function computeId(canonical) {
|
|
74
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
75
|
+
}
|
|
76
|
+
function createEnvelope(type, sender, privateKey, payload, timestamp = Date.now(), inReplyTo) {
|
|
77
|
+
const canonical = canonicalize(type, sender, timestamp, payload, inReplyTo);
|
|
78
|
+
const id = computeId(canonical);
|
|
79
|
+
const signature = signMessage(canonical, privateKey);
|
|
80
|
+
return {
|
|
81
|
+
id,
|
|
82
|
+
type,
|
|
83
|
+
sender,
|
|
84
|
+
timestamp,
|
|
85
|
+
...inReplyTo !== void 0 ? { inReplyTo } : {},
|
|
86
|
+
payload,
|
|
87
|
+
signature
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function verifyEnvelope(envelope) {
|
|
91
|
+
const { id, type, sender, timestamp, payload, signature, inReplyTo } = envelope;
|
|
92
|
+
const canonical = canonicalize(type, sender, timestamp, payload, inReplyTo);
|
|
93
|
+
const expectedId = computeId(canonical);
|
|
94
|
+
if (id !== expectedId) {
|
|
95
|
+
return { valid: false, reason: "id_mismatch" };
|
|
96
|
+
}
|
|
97
|
+
const sigValid = verifySignature(canonical, signature, sender);
|
|
98
|
+
if (!sigValid) {
|
|
99
|
+
return { valid: false, reason: "signature_invalid" };
|
|
100
|
+
}
|
|
101
|
+
return { valid: true };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/relay/store.ts
|
|
105
|
+
import * as fs from "fs";
|
|
106
|
+
import * as path from "path";
|
|
107
|
+
var MessageStore = class {
|
|
108
|
+
storageDir;
|
|
109
|
+
constructor(storageDir) {
|
|
110
|
+
this.storageDir = storageDir;
|
|
111
|
+
fs.mkdirSync(storageDir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
recipientDir(publicKey) {
|
|
114
|
+
const safe = publicKey.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
115
|
+
return path.join(this.storageDir, safe);
|
|
116
|
+
}
|
|
117
|
+
save(recipientKey, message) {
|
|
118
|
+
const dir = this.recipientDir(recipientKey);
|
|
119
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
120
|
+
const filename = `${Date.now()}-${crypto.randomUUID()}.json`;
|
|
121
|
+
fs.writeFileSync(path.join(dir, filename), JSON.stringify(message));
|
|
122
|
+
}
|
|
123
|
+
load(recipientKey) {
|
|
124
|
+
const dir = this.recipientDir(recipientKey);
|
|
125
|
+
if (!fs.existsSync(dir)) return [];
|
|
126
|
+
const files = fs.readdirSync(dir).sort();
|
|
127
|
+
const messages = [];
|
|
128
|
+
for (const file of files) {
|
|
129
|
+
if (!file.endsWith(".json")) continue;
|
|
130
|
+
try {
|
|
131
|
+
const data = fs.readFileSync(path.join(dir, file), "utf8");
|
|
132
|
+
messages.push(JSON.parse(data));
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return messages;
|
|
137
|
+
}
|
|
138
|
+
clear(recipientKey) {
|
|
139
|
+
const dir = this.recipientDir(recipientKey);
|
|
140
|
+
if (!fs.existsSync(dir)) return;
|
|
141
|
+
const files = fs.readdirSync(dir);
|
|
142
|
+
for (const file of files) {
|
|
143
|
+
if (file.endsWith(".json")) {
|
|
144
|
+
fs.unlinkSync(path.join(dir, file));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// src/relay/server.ts
|
|
151
|
+
import { EventEmitter } from "events";
|
|
152
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
153
|
+
var RelayServer = class extends EventEmitter {
|
|
154
|
+
wss = null;
|
|
155
|
+
/** publicKey -> sessionId -> ConnectedAgent (multiple sessions per key) */
|
|
156
|
+
sessions = /* @__PURE__ */ new Map();
|
|
157
|
+
identity;
|
|
158
|
+
storagePeers = [];
|
|
159
|
+
store = null;
|
|
160
|
+
maxPeers = 100;
|
|
161
|
+
constructor(options) {
|
|
162
|
+
super();
|
|
163
|
+
if (options) {
|
|
164
|
+
if ("identity" in options && options.identity) {
|
|
165
|
+
this.identity = options.identity;
|
|
166
|
+
} else if ("publicKey" in options && "privateKey" in options) {
|
|
167
|
+
this.identity = { publicKey: options.publicKey, privateKey: options.privateKey };
|
|
168
|
+
}
|
|
169
|
+
const opts = options;
|
|
170
|
+
if (opts.storagePeers?.length && opts.storageDir) {
|
|
171
|
+
this.storagePeers = opts.storagePeers;
|
|
172
|
+
this.store = new MessageStore(opts.storageDir);
|
|
173
|
+
}
|
|
174
|
+
if (opts.maxPeers !== void 0) {
|
|
175
|
+
this.maxPeers = opts.maxPeers;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Start the relay server
|
|
181
|
+
* @param port - Port to listen on
|
|
182
|
+
* @param host - Optional host (default: all interfaces)
|
|
183
|
+
*/
|
|
184
|
+
start(port, host) {
|
|
185
|
+
return new Promise((resolve, reject) => {
|
|
186
|
+
try {
|
|
187
|
+
this.wss = new WebSocketServer({ port, host: host ?? "0.0.0.0" });
|
|
188
|
+
let resolved = false;
|
|
189
|
+
this.wss.on("error", (error) => {
|
|
190
|
+
this.emit("error", error);
|
|
191
|
+
if (!resolved) {
|
|
192
|
+
resolved = true;
|
|
193
|
+
reject(error);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
this.wss.on("listening", () => {
|
|
197
|
+
if (!resolved) {
|
|
198
|
+
resolved = true;
|
|
199
|
+
resolve();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
this.wss.on("connection", (socket) => {
|
|
203
|
+
this.handleConnection(socket);
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
reject(error);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Stop the relay server
|
|
212
|
+
*/
|
|
213
|
+
async stop() {
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
if (!this.wss) {
|
|
216
|
+
resolve();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
for (const sessionMap of this.sessions.values()) {
|
|
220
|
+
for (const agent of sessionMap.values()) {
|
|
221
|
+
agent.socket.close();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
this.sessions.clear();
|
|
225
|
+
this.wss.close((err) => {
|
|
226
|
+
if (err) {
|
|
227
|
+
reject(err);
|
|
228
|
+
} else {
|
|
229
|
+
this.wss = null;
|
|
230
|
+
resolve();
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get one connected agent per public key (first session). For backward compatibility.
|
|
237
|
+
*/
|
|
238
|
+
getAgents() {
|
|
239
|
+
const out = /* @__PURE__ */ new Map();
|
|
240
|
+
for (const [key, sessionMap] of this.sessions) {
|
|
241
|
+
const first = sessionMap.values().next().value;
|
|
242
|
+
if (first) out.set(key, first);
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Handle incoming connection
|
|
248
|
+
*/
|
|
249
|
+
handleConnection(socket) {
|
|
250
|
+
let agentPublicKey = null;
|
|
251
|
+
let sessionId = null;
|
|
252
|
+
socket.on("message", (data) => {
|
|
253
|
+
try {
|
|
254
|
+
const msg = JSON.parse(data.toString());
|
|
255
|
+
if (msg.type === "register" && !agentPublicKey) {
|
|
256
|
+
if (!msg.publicKey || typeof msg.publicKey !== "string") {
|
|
257
|
+
this.sendError(socket, "Invalid registration: missing or invalid publicKey");
|
|
258
|
+
socket.close();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const publicKey = msg.publicKey;
|
|
262
|
+
const name = msg.name;
|
|
263
|
+
agentPublicKey = publicKey;
|
|
264
|
+
sessionId = crypto.randomUUID();
|
|
265
|
+
if (!this.sessions.has(publicKey) && this.sessions.size >= this.maxPeers) {
|
|
266
|
+
this.sendError(socket, `Relay is at capacity (max ${this.maxPeers} peers)`);
|
|
267
|
+
socket.close();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const agent = {
|
|
271
|
+
publicKey,
|
|
272
|
+
name,
|
|
273
|
+
socket,
|
|
274
|
+
lastSeen: Date.now()
|
|
275
|
+
};
|
|
276
|
+
if (!this.sessions.has(publicKey)) {
|
|
277
|
+
this.sessions.set(publicKey, /* @__PURE__ */ new Map());
|
|
278
|
+
}
|
|
279
|
+
this.sessions.get(publicKey).set(sessionId, agent);
|
|
280
|
+
const isFirstSession = this.sessions.get(publicKey).size === 1;
|
|
281
|
+
this.emit("agent-registered", publicKey);
|
|
282
|
+
const peers = [];
|
|
283
|
+
for (const [key, sessionMap] of this.sessions) {
|
|
284
|
+
if (key === publicKey) continue;
|
|
285
|
+
const firstAgent = sessionMap.values().next().value;
|
|
286
|
+
peers.push({ publicKey: key, name: firstAgent?.name });
|
|
287
|
+
}
|
|
288
|
+
for (const storagePeer of this.storagePeers) {
|
|
289
|
+
if (storagePeer !== publicKey && !this.sessions.has(storagePeer)) {
|
|
290
|
+
peers.push({ publicKey: storagePeer, name: void 0 });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
socket.send(JSON.stringify({
|
|
294
|
+
type: "registered",
|
|
295
|
+
publicKey,
|
|
296
|
+
sessionId,
|
|
297
|
+
peers
|
|
298
|
+
}));
|
|
299
|
+
if (isFirstSession) {
|
|
300
|
+
this.broadcastPeerEvent("peer_online", publicKey, name);
|
|
301
|
+
}
|
|
302
|
+
if (this.store && this.storagePeers.includes(publicKey)) {
|
|
303
|
+
const queued = this.store.load(publicKey);
|
|
304
|
+
for (const stored of queued) {
|
|
305
|
+
socket.send(JSON.stringify({
|
|
306
|
+
type: "message",
|
|
307
|
+
from: stored.from,
|
|
308
|
+
name: stored.name,
|
|
309
|
+
envelope: stored.envelope
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
this.store.clear(publicKey);
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (!agentPublicKey) {
|
|
317
|
+
this.sendError(socket, "Not registered: send registration message first");
|
|
318
|
+
socket.close();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (msg.type === "message") {
|
|
322
|
+
if (!msg.to || typeof msg.to !== "string") {
|
|
323
|
+
this.sendError(socket, 'Invalid message: missing or invalid "to" field');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!msg.envelope || typeof msg.envelope !== "object") {
|
|
327
|
+
this.sendError(socket, 'Invalid message: missing or invalid "envelope" field');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const envelope = msg.envelope;
|
|
331
|
+
const verification = verifyEnvelope(envelope);
|
|
332
|
+
if (!verification.valid) {
|
|
333
|
+
this.sendError(socket, `Invalid envelope: ${verification.reason || "verification failed"}`);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (envelope.sender !== agentPublicKey) {
|
|
337
|
+
this.sendError(socket, "Envelope sender does not match registered public key");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const senderSessionMap = this.sessions.get(agentPublicKey);
|
|
341
|
+
if (senderSessionMap) {
|
|
342
|
+
for (const a of senderSessionMap.values()) {
|
|
343
|
+
a.lastSeen = Date.now();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (envelope.type === "peer_list_request" && this.identity && msg.to === this.identity.publicKey) {
|
|
347
|
+
this.handlePeerListRequest(envelope, socket, agentPublicKey);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const recipientSessionMap = this.sessions.get(msg.to);
|
|
351
|
+
const openRecipients = recipientSessionMap ? Array.from(recipientSessionMap.values()).filter((a) => a.socket.readyState === WebSocket.OPEN) : [];
|
|
352
|
+
if (openRecipients.length === 0) {
|
|
353
|
+
if (this.store && this.storagePeers.includes(msg.to)) {
|
|
354
|
+
const senderSessionMap2 = this.sessions.get(agentPublicKey);
|
|
355
|
+
const senderAgent = senderSessionMap2?.values().next().value;
|
|
356
|
+
this.store.save(msg.to, {
|
|
357
|
+
from: agentPublicKey,
|
|
358
|
+
name: senderAgent?.name,
|
|
359
|
+
envelope
|
|
360
|
+
});
|
|
361
|
+
this.emit("message-relayed", agentPublicKey, msg.to, envelope);
|
|
362
|
+
} else {
|
|
363
|
+
this.sendError(socket, "Recipient not connected", "unknown_recipient");
|
|
364
|
+
}
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
const senderSessionMap2 = this.sessions.get(agentPublicKey);
|
|
369
|
+
const senderAgent = senderSessionMap2?.values().next().value;
|
|
370
|
+
const relayMessage = {
|
|
371
|
+
type: "message",
|
|
372
|
+
from: agentPublicKey,
|
|
373
|
+
name: senderAgent?.name,
|
|
374
|
+
envelope
|
|
375
|
+
};
|
|
376
|
+
const messageStr = JSON.stringify(relayMessage);
|
|
377
|
+
for (const recipient of openRecipients) {
|
|
378
|
+
recipient.socket.send(messageStr);
|
|
379
|
+
}
|
|
380
|
+
this.emit("message-relayed", agentPublicKey, msg.to, envelope);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
this.sendError(socket, "Failed to relay message");
|
|
383
|
+
this.emit("error", err);
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (msg.type === "broadcast") {
|
|
388
|
+
if (!msg.envelope || typeof msg.envelope !== "object") {
|
|
389
|
+
this.sendError(socket, 'Invalid broadcast: missing or invalid "envelope" field');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const envelope = msg.envelope;
|
|
393
|
+
const verification = verifyEnvelope(envelope);
|
|
394
|
+
if (!verification.valid) {
|
|
395
|
+
this.sendError(socket, `Invalid envelope: ${verification.reason || "verification failed"}`);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (envelope.sender !== agentPublicKey) {
|
|
399
|
+
this.sendError(socket, "Envelope sender does not match registered public key");
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const senderSessionMap = this.sessions.get(agentPublicKey);
|
|
403
|
+
if (senderSessionMap) {
|
|
404
|
+
for (const a of senderSessionMap.values()) {
|
|
405
|
+
a.lastSeen = Date.now();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const senderSessionMapForName = this.sessions.get(agentPublicKey);
|
|
409
|
+
const senderAgent = senderSessionMapForName?.values().next().value;
|
|
410
|
+
const relayMessage = {
|
|
411
|
+
type: "message",
|
|
412
|
+
from: agentPublicKey,
|
|
413
|
+
name: senderAgent?.name,
|
|
414
|
+
envelope
|
|
415
|
+
};
|
|
416
|
+
const messageStr = JSON.stringify(relayMessage);
|
|
417
|
+
for (const [key, sessionMap] of this.sessions) {
|
|
418
|
+
if (key === agentPublicKey) continue;
|
|
419
|
+
for (const agent of sessionMap.values()) {
|
|
420
|
+
if (agent.socket.readyState === WebSocket.OPEN) {
|
|
421
|
+
try {
|
|
422
|
+
agent.socket.send(messageStr);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
this.emit("error", err);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (msg.type === "ping") {
|
|
432
|
+
socket.send(JSON.stringify({ type: "pong" }));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
this.sendError(socket, `Unknown message type: ${msg.type}`);
|
|
436
|
+
} catch (err) {
|
|
437
|
+
this.emit("error", new Error(`Message parsing failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
438
|
+
this.sendError(socket, "Invalid message format");
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
socket.on("close", () => {
|
|
442
|
+
if (agentPublicKey && sessionId) {
|
|
443
|
+
const sessionMap = this.sessions.get(agentPublicKey);
|
|
444
|
+
if (sessionMap) {
|
|
445
|
+
const agent = sessionMap.get(sessionId);
|
|
446
|
+
const agentName = agent?.name;
|
|
447
|
+
sessionMap.delete(sessionId);
|
|
448
|
+
if (sessionMap.size === 0) {
|
|
449
|
+
this.sessions.delete(agentPublicKey);
|
|
450
|
+
this.emit("agent-disconnected", agentPublicKey);
|
|
451
|
+
this.emit("disconnection", agentPublicKey);
|
|
452
|
+
if (!this.storagePeers.includes(agentPublicKey)) {
|
|
453
|
+
this.broadcastPeerEvent("peer_offline", agentPublicKey, agentName);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
socket.on("error", (error) => {
|
|
460
|
+
this.emit("error", error);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Send an error message to a client
|
|
465
|
+
*/
|
|
466
|
+
sendError(socket, message, code) {
|
|
467
|
+
try {
|
|
468
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
469
|
+
const payload = { type: "error", message };
|
|
470
|
+
if (code) payload.code = code;
|
|
471
|
+
socket.send(JSON.stringify(payload));
|
|
472
|
+
}
|
|
473
|
+
} catch (err) {
|
|
474
|
+
this.emit("error", new Error(`Failed to send error message: ${err instanceof Error ? err.message : String(err)}`));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Broadcast a peer event to all connected agents (all sessions except the one for publicKey)
|
|
479
|
+
*/
|
|
480
|
+
broadcastPeerEvent(eventType, publicKey, name) {
|
|
481
|
+
const message = {
|
|
482
|
+
type: eventType,
|
|
483
|
+
publicKey,
|
|
484
|
+
name
|
|
485
|
+
};
|
|
486
|
+
const messageStr = JSON.stringify(message);
|
|
487
|
+
for (const [key, sessionMap] of this.sessions) {
|
|
488
|
+
if (key === publicKey) continue;
|
|
489
|
+
for (const agent of sessionMap.values()) {
|
|
490
|
+
if (agent.socket.readyState === WebSocket.OPEN) {
|
|
491
|
+
try {
|
|
492
|
+
agent.socket.send(messageStr);
|
|
493
|
+
} catch (err) {
|
|
494
|
+
this.emit("error", new Error(`Failed to send ${eventType} event: ${err instanceof Error ? err.message : String(err)}`));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Handle peer list request from an agent
|
|
502
|
+
*/
|
|
503
|
+
handlePeerListRequest(envelope, socket, requesterPublicKey) {
|
|
504
|
+
if (!this.identity) {
|
|
505
|
+
this.sendError(socket, "Relay does not support peer discovery (no identity configured)");
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const { filters } = envelope.payload;
|
|
509
|
+
const now = Date.now();
|
|
510
|
+
const peersList = [];
|
|
511
|
+
for (const [key, sessionMap] of this.sessions) {
|
|
512
|
+
if (key === requesterPublicKey) continue;
|
|
513
|
+
const first = sessionMap.values().next().value;
|
|
514
|
+
if (first) peersList.push(first);
|
|
515
|
+
}
|
|
516
|
+
let peers = peersList;
|
|
517
|
+
if (filters?.activeWithin) {
|
|
518
|
+
peers = peers.filter((p) => now - p.lastSeen < filters.activeWithin);
|
|
519
|
+
}
|
|
520
|
+
if (filters?.limit && filters.limit > 0) {
|
|
521
|
+
peers = peers.slice(0, filters.limit);
|
|
522
|
+
}
|
|
523
|
+
const response = {
|
|
524
|
+
peers: peers.map((p) => ({
|
|
525
|
+
publicKey: p.publicKey,
|
|
526
|
+
metadata: p.name || p.metadata ? {
|
|
527
|
+
name: p.name,
|
|
528
|
+
version: p.metadata?.version,
|
|
529
|
+
capabilities: p.metadata?.capabilities
|
|
530
|
+
} : void 0,
|
|
531
|
+
lastSeen: p.lastSeen
|
|
532
|
+
})),
|
|
533
|
+
totalPeers: this.sessions.size - (this.sessions.has(requesterPublicKey) ? 1 : 0),
|
|
534
|
+
relayPublicKey: this.identity.publicKey
|
|
535
|
+
};
|
|
536
|
+
const responseEnvelope = createEnvelope(
|
|
537
|
+
"peer_list_response",
|
|
538
|
+
this.identity.publicKey,
|
|
539
|
+
this.identity.privateKey,
|
|
540
|
+
response,
|
|
541
|
+
Date.now(),
|
|
542
|
+
envelope.id
|
|
543
|
+
// Reply to the request
|
|
544
|
+
);
|
|
545
|
+
const relayMessage = {
|
|
546
|
+
type: "message",
|
|
547
|
+
from: this.identity.publicKey,
|
|
548
|
+
name: "relay",
|
|
549
|
+
envelope: responseEnvelope
|
|
550
|
+
};
|
|
551
|
+
try {
|
|
552
|
+
socket.send(JSON.stringify(relayMessage));
|
|
553
|
+
} catch (err) {
|
|
554
|
+
this.emit("error", new Error(`Failed to send peer list response: ${err instanceof Error ? err.message : String(err)}`));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
export {
|
|
560
|
+
generateKeyPair,
|
|
561
|
+
signMessage,
|
|
562
|
+
verifySignature,
|
|
563
|
+
exportKeyPair,
|
|
564
|
+
importKeyPair,
|
|
565
|
+
canonicalize,
|
|
566
|
+
computeId,
|
|
567
|
+
createEnvelope,
|
|
568
|
+
verifyEnvelope,
|
|
569
|
+
MessageStore,
|
|
570
|
+
RelayServer
|
|
571
|
+
};
|
|
572
|
+
//# sourceMappingURL=chunk-D7Y66GFC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/identity/keypair.ts","../src/message/envelope.ts","../src/relay/store.ts","../src/relay/server.ts"],"sourcesContent":["import { sign, verify, generateKeyPairSync } from 'node:crypto';\n\n/**\n * Represents an ed25519 key pair for agent identity\n */\nexport interface KeyPair {\n publicKey: string; // hex-encoded\n privateKey: string; // hex-encoded\n}\n\n/**\n * Generates a new ed25519 key pair\n * @returns KeyPair with hex-encoded public and private keys\n */\nexport function generateKeyPair(): KeyPair {\n const { publicKey, privateKey } = generateKeyPairSync('ed25519');\n \n return {\n publicKey: publicKey.export({ type: 'spki', format: 'der' }).toString('hex'),\n privateKey: privateKey.export({ type: 'pkcs8', format: 'der' }).toString('hex'),\n };\n}\n\n/**\n * Signs a message with the private key\n * @param message - The message to sign (string or Buffer)\n * @param privateKeyHex - The private key in hex format\n * @returns Signature as hex string\n */\nexport function signMessage(message: string | Buffer, privateKeyHex: string): string {\n const messageBuffer = typeof message === 'string' ? Buffer.from(message) : message;\n const privateKey = Buffer.from(privateKeyHex, 'hex');\n \n const signature = sign(null, messageBuffer, {\n key: privateKey,\n format: 'der',\n type: 'pkcs8',\n });\n \n return signature.toString('hex');\n}\n\n/**\n * Verifies a signature with the public key\n * @param message - The original message (string or Buffer)\n * @param signatureHex - The signature in hex format\n * @param publicKeyHex - The public key in hex format\n * @returns true if signature is valid, false otherwise\n */\nexport function verifySignature(\n message: string | Buffer,\n signatureHex: string,\n publicKeyHex: string\n): boolean {\n const messageBuffer = typeof message === 'string' ? Buffer.from(message) : message;\n const signature = Buffer.from(signatureHex, 'hex');\n const publicKey = Buffer.from(publicKeyHex, 'hex');\n \n try {\n return verify(null, messageBuffer, {\n key: publicKey,\n format: 'der',\n type: 'spki',\n }, signature);\n } catch {\n return false;\n }\n}\n\n/**\n * Exports a key pair to a JSON-serializable format\n * @param keyPair - The key pair to export\n * @returns KeyPair object with hex-encoded keys\n */\nexport function exportKeyPair(keyPair: KeyPair): KeyPair {\n return {\n publicKey: keyPair.publicKey,\n privateKey: keyPair.privateKey,\n };\n}\n\n/**\n * Imports a key pair from hex strings\n * @param publicKeyHex - The public key in hex format\n * @param privateKeyHex - The private key in hex format\n * @returns KeyPair object\n * @throws Error if keys are not valid hex strings\n */\nexport function importKeyPair(publicKeyHex: string, privateKeyHex: string): KeyPair {\n // Validate that keys are valid hex strings\n const hexPattern = /^[0-9a-f]+$/i;\n if (!hexPattern.test(publicKeyHex)) {\n throw new Error('Invalid public key: must be a hex string');\n }\n if (!hexPattern.test(privateKeyHex)) {\n throw new Error('Invalid private key: must be a hex string');\n }\n \n return {\n publicKey: publicKeyHex,\n privateKey: privateKeyHex,\n };\n}\n","import { createHash } from 'node:crypto';\nimport { signMessage, verifySignature } from '../identity/keypair';\n\n/**\n * Message types on the Agora network.\n * Every piece of data flowing between agents is wrapped in an envelope.\n */\nexport type MessageType =\n | 'announce' // Agent publishes capabilities/state\n | 'discover' // Agent requests peer discovery\n | 'request' // Agent requests a service\n | 'response' // Agent responds to a request\n | 'publish' // Agent publishes knowledge/state\n | 'subscribe' // Agent subscribes to a topic/domain\n | 'verify' // Agent verifies another agent's claim\n | 'ack' // Acknowledgement\n | 'error' // Error response\n | 'paper_discovery' // Agent publishes a discovered academic paper\n | 'peer_list_request' // Request peer list from relay\n | 'peer_list_response' // Relay responds with connected peers\n | 'peer_referral' // Agent recommends another agent\n | 'capability_announce' // Agent publishes capabilities to network\n | 'capability_query' // Agent queries for capabilities\n | 'capability_response' // Response with matching peers\n | 'commit' // Agent commits to a prediction (commit-reveal pattern)\n | 'reveal' // Agent reveals prediction and outcome\n | 'verification' // Agent verifies another agent's output\n | 'revocation' // Agent revokes a prior verification\n | 'reputation_query' // Agent queries for reputation data\n | 'reputation_response'; // Response to reputation query\n\n/**\n * The signed envelope that wraps every message on the network.\n * Content-addressed: the ID is the hash of the canonical payload.\n * Signed: every envelope carries a signature from the sender's private key.\n */\nexport interface Envelope<T = unknown> {\n /** Content-addressed ID: SHA-256 hash of canonical payload */\n id: string;\n /** Message type */\n type: MessageType;\n /** Sender's public key (hex-encoded ed25519) */\n sender: string;\n /** Unix timestamp (ms) when the message was created */\n timestamp: number;\n /** Optional: ID of the message this is responding to */\n inReplyTo?: string;\n /** The actual payload */\n payload: T;\n /** ed25519 signature over the canonical form (hex-encoded) */\n signature: string;\n}\n\n/**\n * Deterministic JSON serialization with recursively sorted keys.\n */\nfunction stableStringify(value: unknown): string {\n if (value === null || value === undefined) return JSON.stringify(value);\n if (typeof value !== 'object') return JSON.stringify(value);\n if (Array.isArray(value)) {\n return '[' + value.map(stableStringify).join(',') + ']';\n }\n const keys = Object.keys(value as Record<string, unknown>).sort();\n const pairs = keys.map(k => JSON.stringify(k) + ':' + stableStringify((value as Record<string, unknown>)[k]));\n return '{' + pairs.join(',') + '}';\n}\n\n/**\n * Canonical form of an envelope for signing/hashing.\n * Deterministic JSON serialization: recursively sorted keys, no whitespace.\n */\nexport function canonicalize(type: MessageType, sender: string, timestamp: number, payload: unknown, inReplyTo?: string): string {\n const obj: Record<string, unknown> = { payload, sender, timestamp, type };\n if (inReplyTo !== undefined) {\n obj.inReplyTo = inReplyTo;\n }\n return stableStringify(obj);\n}\n\n/**\n * Compute the content-addressed ID for a message.\n */\nexport function computeId(canonical: string): string {\n return createHash('sha256').update(canonical).digest('hex');\n}\n\n/**\n * Create a signed envelope.\n * @param type - Message type\n * @param sender - Sender's public key (hex)\n * @param privateKey - Sender's private key (hex) for signing\n * @param payload - The message payload\n * @param timestamp - Timestamp for the envelope (ms), defaults to Date.now()\n * @param inReplyTo - Optional ID of the message being replied to\n * @returns A signed Envelope\n */\nexport function createEnvelope<T>(\n type: MessageType,\n sender: string,\n privateKey: string,\n payload: T,\n timestamp: number = Date.now(),\n inReplyTo?: string,\n): Envelope<T> {\n const canonical = canonicalize(type, sender, timestamp, payload, inReplyTo);\n const id = computeId(canonical);\n const signature = signMessage(canonical, privateKey);\n\n return {\n id,\n type,\n sender,\n timestamp,\n ...(inReplyTo !== undefined ? { inReplyTo } : {}),\n payload,\n signature,\n };\n}\n\n/**\n * Verify an envelope's integrity and authenticity.\n * Checks:\n * 1. Canonical form matches the ID (content-addressing)\n * 2. Signature is valid for the sender's public key\n * \n * @returns Object with `valid` boolean and optional `reason` for failure\n */\nexport function verifyEnvelope(envelope: Envelope): { valid: boolean; reason?: string } {\n const { id, type, sender, timestamp, payload, signature, inReplyTo } = envelope;\n\n // Reconstruct canonical form\n const canonical = canonicalize(type, sender, timestamp, payload, inReplyTo);\n\n // Check content-addressed ID\n const expectedId = computeId(canonical);\n if (id !== expectedId) {\n return { valid: false, reason: 'id_mismatch' };\n }\n\n // Check signature\n const sigValid = verifySignature(canonical, signature, sender);\n if (!sigValid) {\n return { valid: false, reason: 'signature_invalid' };\n }\n\n return { valid: true };\n}\n","/**\n * store.ts — File-based message store for offline peers.\n * When the relay has storage enabled for certain public keys, messages\n * for offline recipients are persisted and delivered when they connect.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nexport interface StoredMessage {\n from: string;\n name?: string;\n envelope: object;\n}\n\nexport class MessageStore {\n private storageDir: string;\n\n constructor(storageDir: string) {\n this.storageDir = storageDir;\n fs.mkdirSync(storageDir, { recursive: true });\n }\n\n private recipientDir(publicKey: string): string {\n const safe = publicKey.replace(/[^a-zA-Z0-9_-]/g, '_');\n return path.join(this.storageDir, safe);\n }\n\n save(recipientKey: string, message: StoredMessage): void {\n const dir = this.recipientDir(recipientKey);\n fs.mkdirSync(dir, { recursive: true });\n const filename = `${Date.now()}-${crypto.randomUUID()}.json`;\n fs.writeFileSync(path.join(dir, filename), JSON.stringify(message));\n }\n\n load(recipientKey: string): StoredMessage[] {\n const dir = this.recipientDir(recipientKey);\n if (!fs.existsSync(dir)) return [];\n const files = fs.readdirSync(dir).sort();\n const messages: StoredMessage[] = [];\n for (const file of files) {\n if (!file.endsWith('.json')) continue;\n try {\n const data = fs.readFileSync(path.join(dir, file), 'utf8');\n messages.push(JSON.parse(data) as StoredMessage);\n } catch {\n // Skip files that cannot be read or parsed\n }\n }\n return messages;\n }\n\n clear(recipientKey: string): void {\n const dir = this.recipientDir(recipientKey);\n if (!fs.existsSync(dir)) return;\n const files = fs.readdirSync(dir);\n for (const file of files) {\n if (file.endsWith('.json')) {\n fs.unlinkSync(path.join(dir, file));\n }\n }\n }\n}\n","import { EventEmitter } from 'node:events';\nimport { WebSocketServer, WebSocket } from 'ws';\nimport { verifyEnvelope, createEnvelope, type Envelope } from '../message/envelope';\nimport type { PeerListRequestPayload, PeerListResponsePayload } from '../message/types/peer-discovery';\nimport { MessageStore } from './store';\n\n/**\n * Represents a connected agent in the relay\n */\ninterface ConnectedAgent {\n /** Agent's public key */\n publicKey: string;\n /** Optional agent name */\n name?: string;\n /** WebSocket connection */\n socket: WebSocket;\n /** Last seen timestamp (ms) */\n lastSeen: number;\n /** Optional metadata */\n metadata?: {\n version?: string;\n capabilities?: string[];\n };\n}\n\n/**\n * Events emitted by RelayServer\n */\nexport interface RelayServerEvents {\n 'agent-registered': (publicKey: string) => void;\n 'agent-disconnected': (publicKey: string) => void;\n /** Emitted when a session disconnects (same as agent-disconnected for compatibility) */\n 'disconnection': (publicKey: string) => void;\n 'message-relayed': (from: string, to: string, envelope: Envelope) => void;\n 'error': (error: Error) => void;\n}\n\n/**\n * WebSocket relay server for routing messages between agents.\n * \n * Agents connect to the relay and register with their public key.\n * Messages are routed to recipients based on the 'to' field.\n * All envelopes are verified before being forwarded.\n */\nexport interface RelayServerOptions {\n /** Optional relay identity for peer_list_request handling */\n identity?: { publicKey: string; privateKey: string };\n /** Public keys that should have messages stored when offline */\n storagePeers?: string[];\n /** Directory for persisting messages for storage peers */\n storageDir?: string;\n /** Maximum number of concurrent registered peers (default: 100) */\n maxPeers?: number;\n}\n\nexport class RelayServer extends EventEmitter {\n private wss: WebSocketServer | null = null;\n /** publicKey -> sessionId -> ConnectedAgent (multiple sessions per key) */\n private sessions = new Map<string, Map<string, ConnectedAgent>>();\n private identity?: { publicKey: string; privateKey: string };\n private storagePeers: string[] = [];\n private store: MessageStore | null = null;\n private maxPeers: number = 100;\n\n constructor(options?: { publicKey: string; privateKey: string } | RelayServerOptions) {\n super();\n if (options) {\n if ('identity' in options && options.identity) {\n this.identity = options.identity;\n } else if ('publicKey' in options && 'privateKey' in options) {\n this.identity = { publicKey: options.publicKey, privateKey: options.privateKey };\n }\n const opts = options as RelayServerOptions;\n if (opts.storagePeers?.length && opts.storageDir) {\n this.storagePeers = opts.storagePeers;\n this.store = new MessageStore(opts.storageDir);\n }\n if (opts.maxPeers !== undefined) {\n this.maxPeers = opts.maxPeers;\n }\n }\n }\n\n /**\n * Start the relay server\n * @param port - Port to listen on\n * @param host - Optional host (default: all interfaces)\n */\n start(port: number, host?: string): Promise<void> {\n return new Promise((resolve, reject) => {\n try {\n this.wss = new WebSocketServer({ port, host: host ?? '0.0.0.0' });\n let resolved = false;\n\n this.wss.on('error', (error) => {\n this.emit('error', error);\n if (!resolved) {\n resolved = true;\n reject(error);\n }\n });\n\n this.wss.on('listening', () => {\n if (!resolved) {\n resolved = true;\n resolve();\n }\n });\n\n this.wss.on('connection', (socket: WebSocket) => {\n this.handleConnection(socket);\n });\n } catch (error) {\n reject(error);\n }\n });\n }\n\n /**\n * Stop the relay server\n */\n async stop(): Promise<void> {\n return new Promise((resolve, reject) => {\n if (!this.wss) {\n resolve();\n return;\n }\n\n // Close all agent connections (all sessions)\n for (const sessionMap of this.sessions.values()) {\n for (const agent of sessionMap.values()) {\n agent.socket.close();\n }\n }\n this.sessions.clear();\n\n this.wss.close((err) => {\n if (err) {\n reject(err);\n } else {\n this.wss = null;\n resolve();\n }\n });\n });\n }\n\n /**\n * Get one connected agent per public key (first session). For backward compatibility.\n */\n getAgents(): Map<string, ConnectedAgent> {\n const out = new Map<string, ConnectedAgent>();\n for (const [key, sessionMap] of this.sessions) {\n const first = sessionMap.values().next().value;\n if (first) out.set(key, first);\n }\n return out;\n }\n\n /**\n * Handle incoming connection\n */\n private handleConnection(socket: WebSocket): void {\n let agentPublicKey: string | null = null;\n let sessionId: string | null = null;\n\n socket.on('message', (data: Buffer) => {\n try {\n const msg = JSON.parse(data.toString());\n\n // Handle registration\n if (msg.type === 'register' && !agentPublicKey) {\n if (!msg.publicKey || typeof msg.publicKey !== 'string') {\n this.sendError(socket, 'Invalid registration: missing or invalid publicKey');\n socket.close();\n return;\n }\n\n const publicKey = msg.publicKey;\n const name = msg.name;\n agentPublicKey = publicKey;\n sessionId = crypto.randomUUID();\n\n // Allow multiple sessions per publicKey; only enforce max unique peers\n if (!this.sessions.has(publicKey) && this.sessions.size >= this.maxPeers) {\n this.sendError(socket, `Relay is at capacity (max ${this.maxPeers} peers)`);\n socket.close();\n return;\n }\n\n const agent: ConnectedAgent = {\n publicKey,\n name,\n socket,\n lastSeen: Date.now(),\n };\n\n if (!this.sessions.has(publicKey)) {\n this.sessions.set(publicKey, new Map());\n }\n this.sessions.get(publicKey)!.set(sessionId, agent);\n const isFirstSession = this.sessions.get(publicKey)!.size === 1;\n\n this.emit('agent-registered', publicKey);\n\n // Build peers list: one entry per connected publicKey + storage peers\n const peers: Array<{ publicKey: string; name?: string }> = [];\n for (const [key, sessionMap] of this.sessions) {\n if (key === publicKey) continue;\n const firstAgent = sessionMap.values().next().value;\n peers.push({ publicKey: key, name: firstAgent?.name });\n }\n for (const storagePeer of this.storagePeers) {\n if (storagePeer !== publicKey && !this.sessions.has(storagePeer)) {\n peers.push({ publicKey: storagePeer, name: undefined });\n }\n }\n\n socket.send(JSON.stringify({\n type: 'registered',\n publicKey,\n sessionId,\n peers,\n }));\n\n // Notify other agents only when this is the first session for this peer\n if (isFirstSession) {\n this.broadcastPeerEvent('peer_online', publicKey, name);\n }\n\n // Deliver any stored messages for this peer\n if (this.store && this.storagePeers.includes(publicKey)) {\n const queued = this.store.load(publicKey);\n for (const stored of queued) {\n socket.send(JSON.stringify({\n type: 'message',\n from: stored.from,\n name: stored.name,\n envelope: stored.envelope,\n }));\n }\n this.store.clear(publicKey);\n }\n return;\n }\n\n // Require registration before processing messages\n if (!agentPublicKey) {\n this.sendError(socket, 'Not registered: send registration message first');\n socket.close();\n return;\n }\n\n // Handle message relay\n if (msg.type === 'message') {\n if (!msg.to || typeof msg.to !== 'string') {\n this.sendError(socket, 'Invalid message: missing or invalid \"to\" field');\n return;\n }\n\n if (!msg.envelope || typeof msg.envelope !== 'object') {\n this.sendError(socket, 'Invalid message: missing or invalid \"envelope\" field');\n return;\n }\n\n const envelope = msg.envelope as Envelope;\n\n // Verify envelope signature\n const verification = verifyEnvelope(envelope);\n if (!verification.valid) {\n this.sendError(socket, `Invalid envelope: ${verification.reason || 'verification failed'}`);\n return;\n }\n\n // Verify sender matches registered agent\n if (envelope.sender !== agentPublicKey) {\n this.sendError(socket, 'Envelope sender does not match registered public key');\n return;\n }\n\n // Update lastSeen for any session of sender\n const senderSessionMap = this.sessions.get(agentPublicKey);\n if (senderSessionMap) {\n for (const a of senderSessionMap.values()) {\n a.lastSeen = Date.now();\n }\n }\n\n // Handle peer_list_request directed at relay\n if (envelope.type === 'peer_list_request' && this.identity && msg.to === this.identity.publicKey) {\n this.handlePeerListRequest(envelope as Envelope<PeerListRequestPayload>, socket, agentPublicKey);\n return;\n }\n\n // Find all recipient sessions\n const recipientSessionMap = this.sessions.get(msg.to);\n const openRecipients = recipientSessionMap\n ? Array.from(recipientSessionMap.values()).filter(a => a.socket.readyState === WebSocket.OPEN)\n : [];\n if (openRecipients.length === 0) {\n // If recipient is a storage peer, queue the message\n if (this.store && this.storagePeers.includes(msg.to)) {\n const senderSessionMap = this.sessions.get(agentPublicKey);\n const senderAgent = senderSessionMap?.values().next().value;\n this.store.save(msg.to, {\n from: agentPublicKey,\n name: senderAgent?.name,\n envelope,\n });\n this.emit('message-relayed', agentPublicKey, msg.to, envelope);\n } else {\n this.sendError(socket, 'Recipient not connected', 'unknown_recipient');\n }\n return;\n }\n\n // Forward envelope to all sessions of the recipient\n try {\n const senderSessionMap = this.sessions.get(agentPublicKey);\n const senderAgent = senderSessionMap?.values().next().value;\n const relayMessage = {\n type: 'message',\n from: agentPublicKey,\n name: senderAgent?.name,\n envelope,\n };\n const messageStr = JSON.stringify(relayMessage);\n for (const recipient of openRecipients) {\n recipient.socket.send(messageStr);\n }\n this.emit('message-relayed', agentPublicKey, msg.to, envelope);\n } catch (err) {\n this.sendError(socket, 'Failed to relay message');\n this.emit('error', err as Error);\n }\n return;\n }\n\n // Handle broadcast: same validation as message, then forward to all other agents\n if (msg.type === 'broadcast') {\n if (!msg.envelope || typeof msg.envelope !== 'object') {\n this.sendError(socket, 'Invalid broadcast: missing or invalid \"envelope\" field');\n return;\n }\n\n const envelope = msg.envelope as Envelope;\n\n const verification = verifyEnvelope(envelope);\n if (!verification.valid) {\n this.sendError(socket, `Invalid envelope: ${verification.reason || 'verification failed'}`);\n return;\n }\n\n if (envelope.sender !== agentPublicKey) {\n this.sendError(socket, 'Envelope sender does not match registered public key');\n return;\n }\n\n // Update lastSeen for any session of sender\n const senderSessionMap = this.sessions.get(agentPublicKey);\n if (senderSessionMap) {\n for (const a of senderSessionMap.values()) {\n a.lastSeen = Date.now();\n }\n }\n\n const senderSessionMapForName = this.sessions.get(agentPublicKey);\n const senderAgent = senderSessionMapForName?.values().next().value;\n const relayMessage = {\n type: 'message' as const,\n from: agentPublicKey,\n name: senderAgent?.name,\n envelope,\n };\n const messageStr = JSON.stringify(relayMessage);\n\n for (const [key, sessionMap] of this.sessions) {\n if (key === agentPublicKey) continue;\n for (const agent of sessionMap.values()) {\n if (agent.socket.readyState === WebSocket.OPEN) {\n try {\n agent.socket.send(messageStr);\n } catch (err) {\n this.emit('error', err as Error);\n }\n }\n }\n }\n return;\n }\n\n // Handle ping\n if (msg.type === 'ping') {\n socket.send(JSON.stringify({ type: 'pong' }));\n return;\n }\n\n // Unknown message type\n this.sendError(socket, `Unknown message type: ${msg.type}`);\n } catch (err) {\n // Invalid JSON or other parsing errors\n this.emit('error', new Error(`Message parsing failed: ${err instanceof Error ? err.message : String(err)}`));\n this.sendError(socket, 'Invalid message format');\n }\n });\n\n socket.on('close', () => {\n if (agentPublicKey && sessionId) {\n const sessionMap = this.sessions.get(agentPublicKey);\n if (sessionMap) {\n const agent = sessionMap.get(sessionId);\n const agentName = agent?.name;\n sessionMap.delete(sessionId);\n if (sessionMap.size === 0) {\n this.sessions.delete(agentPublicKey);\n this.emit('agent-disconnected', agentPublicKey);\n this.emit('disconnection', agentPublicKey);\n // Storage-enabled peers are always considered connected; skip peer_offline for them\n if (!this.storagePeers.includes(agentPublicKey)) {\n this.broadcastPeerEvent('peer_offline', agentPublicKey, agentName);\n }\n }\n }\n }\n });\n\n socket.on('error', (error) => {\n this.emit('error', error);\n });\n }\n\n /**\n * Send an error message to a client\n */\n private sendError(socket: WebSocket, message: string, code?: string): void {\n try {\n if (socket.readyState === WebSocket.OPEN) {\n const payload: { type: 'error'; message: string; code?: string } = { type: 'error', message };\n if (code) payload.code = code;\n socket.send(JSON.stringify(payload));\n }\n } catch (err) {\n // Log errors when sending error messages, but don't propagate to avoid cascading failures\n this.emit('error', new Error(`Failed to send error message: ${err instanceof Error ? err.message : String(err)}`));\n }\n }\n\n /**\n * Broadcast a peer event to all connected agents (all sessions except the one for publicKey)\n */\n private broadcastPeerEvent(eventType: 'peer_online' | 'peer_offline', publicKey: string, name?: string): void {\n const message = {\n type: eventType,\n publicKey,\n name,\n };\n const messageStr = JSON.stringify(message);\n\n for (const [key, sessionMap] of this.sessions) {\n if (key === publicKey) continue;\n for (const agent of sessionMap.values()) {\n if (agent.socket.readyState === WebSocket.OPEN) {\n try {\n agent.socket.send(messageStr);\n } catch (err) {\n this.emit('error', new Error(`Failed to send ${eventType} event: ${err instanceof Error ? err.message : String(err)}`));\n }\n }\n }\n }\n }\n\n /**\n * Handle peer list request from an agent\n */\n private handlePeerListRequest(envelope: Envelope<PeerListRequestPayload>, socket: WebSocket, requesterPublicKey: string): void {\n if (!this.identity) {\n this.sendError(socket, 'Relay does not support peer discovery (no identity configured)');\n return;\n }\n\n const { filters } = envelope.payload;\n const now = Date.now();\n\n // One entry per publicKey (first session for lastSeen/metadata)\n const peersList: ConnectedAgent[] = [];\n for (const [key, sessionMap] of this.sessions) {\n if (key === requesterPublicKey) continue;\n const first = sessionMap.values().next().value;\n if (first) peersList.push(first);\n }\n\n let peers = peersList;\n\n // Apply filters\n if (filters?.activeWithin) {\n peers = peers.filter(p => (now - p.lastSeen) < filters.activeWithin!);\n }\n\n if (filters?.limit && filters.limit > 0) {\n peers = peers.slice(0, filters.limit);\n }\n\n // Build response payload\n const response: PeerListResponsePayload = {\n peers: peers.map(p => ({\n publicKey: p.publicKey,\n metadata: p.name || p.metadata ? {\n name: p.name,\n version: p.metadata?.version,\n capabilities: p.metadata?.capabilities,\n } : undefined,\n lastSeen: p.lastSeen,\n })),\n totalPeers: this.sessions.size - (this.sessions.has(requesterPublicKey) ? 1 : 0),\n relayPublicKey: this.identity.publicKey,\n };\n\n // Create signed envelope\n const responseEnvelope = createEnvelope(\n 'peer_list_response',\n this.identity.publicKey,\n this.identity.privateKey,\n response,\n Date.now(),\n envelope.id // Reply to the request\n );\n\n // Send response\n const relayMessage = {\n type: 'message',\n from: this.identity.publicKey,\n name: 'relay',\n envelope: responseEnvelope,\n };\n\n try {\n socket.send(JSON.stringify(relayMessage));\n } catch (err) {\n this.emit('error', new Error(`Failed to send peer list response: ${err instanceof Error ? err.message : String(err)}`));\n }\n }\n}\n"],"mappings":"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","names":["senderSessionMap"]}
|