@rookdaemon/agora 0.2.7 → 0.2.9
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 +33 -0
- package/dist/chunk-JUOGKXFN.js +1645 -0
- package/dist/chunk-JUOGKXFN.js.map +1 -0
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +1163 -1137
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1613 -25
- package/dist/index.js +1135 -24
- package/dist/index.js.map +1 -1
- package/package.json +11 -2
- package/dist/cli.d.ts.map +0 -1
- package/dist/config.d.ts +0 -59
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -115
- package/dist/config.js.map +0 -1
- package/dist/discovery/bootstrap.d.ts +0 -32
- package/dist/discovery/bootstrap.d.ts.map +0 -1
- package/dist/discovery/bootstrap.js +0 -36
- package/dist/discovery/bootstrap.js.map +0 -1
- package/dist/discovery/peer-discovery.d.ts +0 -59
- package/dist/discovery/peer-discovery.d.ts.map +0 -1
- package/dist/discovery/peer-discovery.js +0 -108
- package/dist/discovery/peer-discovery.js.map +0 -1
- package/dist/identity/keypair.d.ts +0 -42
- package/dist/identity/keypair.d.ts.map +0 -1
- package/dist/identity/keypair.js +0 -83
- package/dist/identity/keypair.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/message/envelope.d.ts +0 -59
- package/dist/message/envelope.d.ts.map +0 -1
- package/dist/message/envelope.js +0 -83
- package/dist/message/envelope.js.map +0 -1
- package/dist/message/types/paper-discovery.d.ts +0 -28
- package/dist/message/types/paper-discovery.d.ts.map +0 -1
- package/dist/message/types/paper-discovery.js +0 -2
- package/dist/message/types/paper-discovery.js.map +0 -1
- package/dist/message/types/peer-discovery.d.ts +0 -78
- package/dist/message/types/peer-discovery.d.ts.map +0 -1
- package/dist/message/types/peer-discovery.js +0 -90
- package/dist/message/types/peer-discovery.js.map +0 -1
- package/dist/peer/client.d.ts +0 -50
- package/dist/peer/client.d.ts.map +0 -1
- package/dist/peer/client.js +0 -138
- package/dist/peer/client.js.map +0 -1
- package/dist/peer/manager.d.ts +0 -65
- package/dist/peer/manager.d.ts.map +0 -1
- package/dist/peer/manager.js +0 -153
- package/dist/peer/manager.js.map +0 -1
- package/dist/peer/server.d.ts +0 -65
- package/dist/peer/server.d.ts.map +0 -1
- package/dist/peer/server.js +0 -154
- package/dist/peer/server.js.map +0 -1
- package/dist/registry/capability.d.ts +0 -44
- package/dist/registry/capability.d.ts.map +0 -1
- package/dist/registry/capability.js +0 -94
- package/dist/registry/capability.js.map +0 -1
- package/dist/registry/discovery-service.d.ts +0 -64
- package/dist/registry/discovery-service.d.ts.map +0 -1
- package/dist/registry/discovery-service.js +0 -129
- package/dist/registry/discovery-service.js.map +0 -1
- package/dist/registry/messages.d.ts +0 -105
- package/dist/registry/messages.d.ts.map +0 -1
- package/dist/registry/messages.js +0 -2
- package/dist/registry/messages.js.map +0 -1
- package/dist/registry/peer-store.d.ts +0 -57
- package/dist/registry/peer-store.d.ts.map +0 -1
- package/dist/registry/peer-store.js +0 -92
- package/dist/registry/peer-store.js.map +0 -1
- package/dist/registry/peer.d.ts +0 -20
- package/dist/registry/peer.d.ts.map +0 -1
- package/dist/registry/peer.js +0 -2
- package/dist/registry/peer.js.map +0 -1
- package/dist/relay/client.d.ts +0 -112
- package/dist/relay/client.d.ts.map +0 -1
- package/dist/relay/client.js +0 -281
- package/dist/relay/client.js.map +0 -1
- package/dist/relay/server.d.ts +0 -76
- package/dist/relay/server.d.ts.map +0 -1
- package/dist/relay/server.js +0 -338
- package/dist/relay/server.js.map +0 -1
- package/dist/relay/types.d.ts +0 -35
- package/dist/relay/types.d.ts.map +0 -1
- package/dist/relay/types.js +0 -2
- package/dist/relay/types.js.map +0 -1
- package/dist/reputation/commit-reveal.d.ts +0 -45
- package/dist/reputation/commit-reveal.d.ts.map +0 -1
- package/dist/reputation/commit-reveal.js +0 -125
- package/dist/reputation/commit-reveal.js.map +0 -1
- package/dist/reputation/scoring.d.ts +0 -31
- package/dist/reputation/scoring.d.ts.map +0 -1
- package/dist/reputation/scoring.js +0 -105
- package/dist/reputation/scoring.js.map +0 -1
- package/dist/reputation/store.d.ts +0 -83
- package/dist/reputation/store.d.ts.map +0 -1
- package/dist/reputation/store.js +0 -202
- package/dist/reputation/store.js.map +0 -1
- package/dist/reputation/types.d.ts +0 -150
- package/dist/reputation/types.d.ts.map +0 -1
- package/dist/reputation/types.js +0 -113
- package/dist/reputation/types.js.map +0 -1
- package/dist/reputation/verification.d.ts +0 -28
- package/dist/reputation/verification.d.ts.map +0 -1
- package/dist/reputation/verification.js +0 -91
- package/dist/reputation/verification.js.map +0 -1
- package/dist/service.d.ts +0 -90
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js +0 -176
- package/dist/service.js.map +0 -1
- package/dist/transport/http.d.ts +0 -41
- package/dist/transport/http.d.ts.map +0 -1
- package/dist/transport/http.js +0 -103
- package/dist/transport/http.js.map +0 -1
- package/dist/transport/peer-config.d.ts +0 -38
- package/dist/transport/peer-config.d.ts.map +0 -1
- package/dist/transport/peer-config.js +0 -41
- package/dist/transport/peer-config.js.map +0 -1
- package/dist/transport/relay.d.ts +0 -30
- package/dist/transport/relay.d.ts.map +0 -1
- package/dist/transport/relay.js +0 -85
- package/dist/transport/relay.js.map +0 -1
- package/dist/utils.d.ts +0 -40
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -59
- package/dist/utils.js.map +0 -1
|
@@ -0,0 +1,1645 @@
|
|
|
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/transport/peer-config.ts
|
|
55
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
56
|
+
function loadPeerConfig(path2) {
|
|
57
|
+
const content = readFileSync(path2, "utf-8");
|
|
58
|
+
return JSON.parse(content);
|
|
59
|
+
}
|
|
60
|
+
function savePeerConfig(path2, config) {
|
|
61
|
+
const content = JSON.stringify(config, null, 2);
|
|
62
|
+
writeFileSync(path2, content, "utf-8");
|
|
63
|
+
}
|
|
64
|
+
function initPeerConfig(path2) {
|
|
65
|
+
if (existsSync(path2)) {
|
|
66
|
+
return loadPeerConfig(path2);
|
|
67
|
+
}
|
|
68
|
+
const identity = generateKeyPair();
|
|
69
|
+
const config = {
|
|
70
|
+
identity,
|
|
71
|
+
peers: {}
|
|
72
|
+
};
|
|
73
|
+
savePeerConfig(path2, config);
|
|
74
|
+
return config;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/message/envelope.ts
|
|
78
|
+
import { createHash } from "crypto";
|
|
79
|
+
function stableStringify(value) {
|
|
80
|
+
if (value === null || value === void 0) return JSON.stringify(value);
|
|
81
|
+
if (typeof value !== "object") return JSON.stringify(value);
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
return "[" + value.map(stableStringify).join(",") + "]";
|
|
84
|
+
}
|
|
85
|
+
const keys = Object.keys(value).sort();
|
|
86
|
+
const pairs = keys.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k]));
|
|
87
|
+
return "{" + pairs.join(",") + "}";
|
|
88
|
+
}
|
|
89
|
+
function canonicalize(type, sender, timestamp, payload, inReplyTo) {
|
|
90
|
+
const obj = { payload, sender, timestamp, type };
|
|
91
|
+
if (inReplyTo !== void 0) {
|
|
92
|
+
obj.inReplyTo = inReplyTo;
|
|
93
|
+
}
|
|
94
|
+
return stableStringify(obj);
|
|
95
|
+
}
|
|
96
|
+
function computeId(canonical) {
|
|
97
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
98
|
+
}
|
|
99
|
+
function createEnvelope(type, sender, privateKey, payload, timestamp = Date.now(), inReplyTo) {
|
|
100
|
+
const canonical = canonicalize(type, sender, timestamp, payload, inReplyTo);
|
|
101
|
+
const id = computeId(canonical);
|
|
102
|
+
const signature = signMessage(canonical, privateKey);
|
|
103
|
+
return {
|
|
104
|
+
id,
|
|
105
|
+
type,
|
|
106
|
+
sender,
|
|
107
|
+
timestamp,
|
|
108
|
+
...inReplyTo !== void 0 ? { inReplyTo } : {},
|
|
109
|
+
payload,
|
|
110
|
+
signature
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function verifyEnvelope(envelope) {
|
|
114
|
+
const { id, type, sender, timestamp, payload, signature, inReplyTo } = envelope;
|
|
115
|
+
const canonical = canonicalize(type, sender, timestamp, payload, inReplyTo);
|
|
116
|
+
const expectedId = computeId(canonical);
|
|
117
|
+
if (id !== expectedId) {
|
|
118
|
+
return { valid: false, reason: "id_mismatch" };
|
|
119
|
+
}
|
|
120
|
+
const sigValid = verifySignature(canonical, signature, sender);
|
|
121
|
+
if (!sigValid) {
|
|
122
|
+
return { valid: false, reason: "signature_invalid" };
|
|
123
|
+
}
|
|
124
|
+
return { valid: true };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/transport/http.ts
|
|
128
|
+
async function sendToPeer(config, peerPublicKey, type, payload, inReplyTo) {
|
|
129
|
+
const peer = config.peers.get(peerPublicKey);
|
|
130
|
+
if (!peer) {
|
|
131
|
+
return { ok: false, status: 0, error: "Unknown peer" };
|
|
132
|
+
}
|
|
133
|
+
if (!peer.url) {
|
|
134
|
+
return { ok: false, status: 0, error: "No webhook URL configured" };
|
|
135
|
+
}
|
|
136
|
+
const envelope = createEnvelope(
|
|
137
|
+
type,
|
|
138
|
+
config.identity.publicKey,
|
|
139
|
+
config.identity.privateKey,
|
|
140
|
+
payload,
|
|
141
|
+
Date.now(),
|
|
142
|
+
inReplyTo
|
|
143
|
+
);
|
|
144
|
+
const envelopeJson = JSON.stringify(envelope);
|
|
145
|
+
const envelopeBase64 = Buffer.from(envelopeJson).toString("base64url");
|
|
146
|
+
const webhookPayload = {
|
|
147
|
+
message: `[AGORA_ENVELOPE]${envelopeBase64}`,
|
|
148
|
+
name: "Agora",
|
|
149
|
+
sessionKey: `agora:${envelope.sender.substring(0, 16)}`,
|
|
150
|
+
deliver: false
|
|
151
|
+
};
|
|
152
|
+
try {
|
|
153
|
+
const response = await fetch(`${peer.url}/agent`, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: {
|
|
156
|
+
"Authorization": `Bearer ${peer.token}`,
|
|
157
|
+
"Content-Type": "application/json"
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify(webhookPayload)
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
ok: response.ok,
|
|
163
|
+
status: response.status,
|
|
164
|
+
error: response.ok ? void 0 : await response.text()
|
|
165
|
+
};
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
status: 0,
|
|
170
|
+
error: err instanceof Error ? err.message : String(err)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function decodeInboundEnvelope(message, knownPeers) {
|
|
175
|
+
const prefix = "[AGORA_ENVELOPE]";
|
|
176
|
+
if (!message.startsWith(prefix)) {
|
|
177
|
+
return { ok: false, reason: "not_agora_message" };
|
|
178
|
+
}
|
|
179
|
+
const base64Payload = message.substring(prefix.length);
|
|
180
|
+
if (!base64Payload) {
|
|
181
|
+
return { ok: false, reason: "invalid_base64" };
|
|
182
|
+
}
|
|
183
|
+
let envelopeJson;
|
|
184
|
+
try {
|
|
185
|
+
const decoded = Buffer.from(base64Payload, "base64url");
|
|
186
|
+
if (decoded.length === 0) {
|
|
187
|
+
return { ok: false, reason: "invalid_base64" };
|
|
188
|
+
}
|
|
189
|
+
envelopeJson = decoded.toString("utf-8");
|
|
190
|
+
} catch {
|
|
191
|
+
return { ok: false, reason: "invalid_base64" };
|
|
192
|
+
}
|
|
193
|
+
let envelope;
|
|
194
|
+
try {
|
|
195
|
+
envelope = JSON.parse(envelopeJson);
|
|
196
|
+
} catch {
|
|
197
|
+
return { ok: false, reason: "invalid_json" };
|
|
198
|
+
}
|
|
199
|
+
const verification = verifyEnvelope(envelope);
|
|
200
|
+
if (!verification.valid) {
|
|
201
|
+
return { ok: false, reason: verification.reason || "verification_failed" };
|
|
202
|
+
}
|
|
203
|
+
const senderKnown = knownPeers.has(envelope.sender);
|
|
204
|
+
if (!senderKnown) {
|
|
205
|
+
return { ok: false, reason: "unknown_sender" };
|
|
206
|
+
}
|
|
207
|
+
return { ok: true, envelope };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/transport/relay.ts
|
|
211
|
+
import WebSocket from "ws";
|
|
212
|
+
async function sendViaRelay(config, peerPublicKey, type, payload, inReplyTo) {
|
|
213
|
+
if (config.relayClient && config.relayClient.connected()) {
|
|
214
|
+
const envelope = createEnvelope(
|
|
215
|
+
type,
|
|
216
|
+
config.identity.publicKey,
|
|
217
|
+
config.identity.privateKey,
|
|
218
|
+
payload,
|
|
219
|
+
Date.now(),
|
|
220
|
+
inReplyTo
|
|
221
|
+
);
|
|
222
|
+
return config.relayClient.send(peerPublicKey, envelope);
|
|
223
|
+
}
|
|
224
|
+
return new Promise((resolve) => {
|
|
225
|
+
const ws = new WebSocket(config.relayUrl);
|
|
226
|
+
let registered = false;
|
|
227
|
+
let messageSent = false;
|
|
228
|
+
let resolved = false;
|
|
229
|
+
const resolveOnce = (result) => {
|
|
230
|
+
if (!resolved) {
|
|
231
|
+
resolved = true;
|
|
232
|
+
clearTimeout(timeout);
|
|
233
|
+
resolve(result);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const timeout = setTimeout(() => {
|
|
237
|
+
if (!messageSent) {
|
|
238
|
+
ws.close();
|
|
239
|
+
resolveOnce({ ok: false, error: "Relay connection timeout" });
|
|
240
|
+
}
|
|
241
|
+
}, 1e4);
|
|
242
|
+
ws.on("open", () => {
|
|
243
|
+
const registerMsg = {
|
|
244
|
+
type: "register",
|
|
245
|
+
publicKey: config.identity.publicKey
|
|
246
|
+
};
|
|
247
|
+
ws.send(JSON.stringify(registerMsg));
|
|
248
|
+
});
|
|
249
|
+
ws.on("message", (data) => {
|
|
250
|
+
try {
|
|
251
|
+
const msg = JSON.parse(data.toString());
|
|
252
|
+
if (msg.type === "registered" && !registered) {
|
|
253
|
+
registered = true;
|
|
254
|
+
const envelope = createEnvelope(
|
|
255
|
+
type,
|
|
256
|
+
config.identity.publicKey,
|
|
257
|
+
config.identity.privateKey,
|
|
258
|
+
payload,
|
|
259
|
+
Date.now(),
|
|
260
|
+
inReplyTo
|
|
261
|
+
);
|
|
262
|
+
const relayMsg = {
|
|
263
|
+
type: "message",
|
|
264
|
+
to: peerPublicKey,
|
|
265
|
+
envelope
|
|
266
|
+
};
|
|
267
|
+
ws.send(JSON.stringify(relayMsg));
|
|
268
|
+
messageSent = true;
|
|
269
|
+
setTimeout(() => {
|
|
270
|
+
ws.close();
|
|
271
|
+
resolveOnce({ ok: true });
|
|
272
|
+
}, 100);
|
|
273
|
+
} else if (msg.type === "error") {
|
|
274
|
+
ws.close();
|
|
275
|
+
resolveOnce({ ok: false, error: msg.message || "Relay server error" });
|
|
276
|
+
}
|
|
277
|
+
} catch (err) {
|
|
278
|
+
ws.close();
|
|
279
|
+
resolveOnce({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
ws.on("error", (err) => {
|
|
283
|
+
ws.close();
|
|
284
|
+
resolveOnce({ ok: false, error: err.message });
|
|
285
|
+
});
|
|
286
|
+
ws.on("close", () => {
|
|
287
|
+
if (!messageSent) {
|
|
288
|
+
resolveOnce({ ok: false, error: "Connection closed before message sent" });
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/relay/store.ts
|
|
295
|
+
import * as fs from "fs";
|
|
296
|
+
import * as path from "path";
|
|
297
|
+
var MessageStore = class {
|
|
298
|
+
storageDir;
|
|
299
|
+
constructor(storageDir) {
|
|
300
|
+
this.storageDir = storageDir;
|
|
301
|
+
fs.mkdirSync(storageDir, { recursive: true });
|
|
302
|
+
}
|
|
303
|
+
recipientDir(publicKey) {
|
|
304
|
+
const safe = publicKey.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
305
|
+
return path.join(this.storageDir, safe);
|
|
306
|
+
}
|
|
307
|
+
save(recipientKey, message) {
|
|
308
|
+
const dir = this.recipientDir(recipientKey);
|
|
309
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
310
|
+
const filename = `${Date.now()}-${crypto.randomUUID()}.json`;
|
|
311
|
+
fs.writeFileSync(path.join(dir, filename), JSON.stringify(message));
|
|
312
|
+
}
|
|
313
|
+
load(recipientKey) {
|
|
314
|
+
const dir = this.recipientDir(recipientKey);
|
|
315
|
+
if (!fs.existsSync(dir)) return [];
|
|
316
|
+
const files = fs.readdirSync(dir).sort();
|
|
317
|
+
const messages = [];
|
|
318
|
+
for (const file of files) {
|
|
319
|
+
if (!file.endsWith(".json")) continue;
|
|
320
|
+
try {
|
|
321
|
+
const data = fs.readFileSync(path.join(dir, file), "utf8");
|
|
322
|
+
messages.push(JSON.parse(data));
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return messages;
|
|
327
|
+
}
|
|
328
|
+
clear(recipientKey) {
|
|
329
|
+
const dir = this.recipientDir(recipientKey);
|
|
330
|
+
if (!fs.existsSync(dir)) return;
|
|
331
|
+
const files = fs.readdirSync(dir);
|
|
332
|
+
for (const file of files) {
|
|
333
|
+
if (file.endsWith(".json")) {
|
|
334
|
+
fs.unlinkSync(path.join(dir, file));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// src/relay/server.ts
|
|
341
|
+
import { EventEmitter } from "events";
|
|
342
|
+
import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
|
|
343
|
+
var RelayServer = class extends EventEmitter {
|
|
344
|
+
wss = null;
|
|
345
|
+
agents = /* @__PURE__ */ new Map();
|
|
346
|
+
identity;
|
|
347
|
+
storagePeers = [];
|
|
348
|
+
store = null;
|
|
349
|
+
constructor(options) {
|
|
350
|
+
super();
|
|
351
|
+
if (options) {
|
|
352
|
+
if ("identity" in options && options.identity) {
|
|
353
|
+
this.identity = options.identity;
|
|
354
|
+
} else if ("publicKey" in options && "privateKey" in options) {
|
|
355
|
+
this.identity = { publicKey: options.publicKey, privateKey: options.privateKey };
|
|
356
|
+
}
|
|
357
|
+
const opts = options;
|
|
358
|
+
if (opts.storagePeers?.length && opts.storageDir) {
|
|
359
|
+
this.storagePeers = opts.storagePeers;
|
|
360
|
+
this.store = new MessageStore(opts.storageDir);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Start the relay server
|
|
366
|
+
* @param port - Port to listen on
|
|
367
|
+
* @param host - Optional host (default: all interfaces)
|
|
368
|
+
*/
|
|
369
|
+
start(port, host) {
|
|
370
|
+
return new Promise((resolve, reject) => {
|
|
371
|
+
try {
|
|
372
|
+
this.wss = new WebSocketServer({ port, host: host ?? "0.0.0.0" });
|
|
373
|
+
let resolved = false;
|
|
374
|
+
this.wss.on("error", (error) => {
|
|
375
|
+
this.emit("error", error);
|
|
376
|
+
if (!resolved) {
|
|
377
|
+
resolved = true;
|
|
378
|
+
reject(error);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
this.wss.on("listening", () => {
|
|
382
|
+
if (!resolved) {
|
|
383
|
+
resolved = true;
|
|
384
|
+
resolve();
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
this.wss.on("connection", (socket) => {
|
|
388
|
+
this.handleConnection(socket);
|
|
389
|
+
});
|
|
390
|
+
} catch (error) {
|
|
391
|
+
reject(error);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Stop the relay server
|
|
397
|
+
*/
|
|
398
|
+
async stop() {
|
|
399
|
+
return new Promise((resolve, reject) => {
|
|
400
|
+
if (!this.wss) {
|
|
401
|
+
resolve();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
for (const agent of this.agents.values()) {
|
|
405
|
+
agent.socket.close();
|
|
406
|
+
}
|
|
407
|
+
this.agents.clear();
|
|
408
|
+
this.wss.close((err) => {
|
|
409
|
+
if (err) {
|
|
410
|
+
reject(err);
|
|
411
|
+
} else {
|
|
412
|
+
this.wss = null;
|
|
413
|
+
resolve();
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Get all connected agents
|
|
420
|
+
*/
|
|
421
|
+
getAgents() {
|
|
422
|
+
return new Map(this.agents);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Handle incoming connection
|
|
426
|
+
*/
|
|
427
|
+
handleConnection(socket) {
|
|
428
|
+
let agentPublicKey = null;
|
|
429
|
+
socket.on("message", (data) => {
|
|
430
|
+
try {
|
|
431
|
+
const msg = JSON.parse(data.toString());
|
|
432
|
+
if (msg.type === "register" && !agentPublicKey) {
|
|
433
|
+
if (!msg.publicKey || typeof msg.publicKey !== "string") {
|
|
434
|
+
this.sendError(socket, "Invalid registration: missing or invalid publicKey");
|
|
435
|
+
socket.close();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const publicKey = msg.publicKey;
|
|
439
|
+
const name = msg.name;
|
|
440
|
+
agentPublicKey = publicKey;
|
|
441
|
+
const existing = this.agents.get(publicKey);
|
|
442
|
+
if (existing) {
|
|
443
|
+
existing.socket.close();
|
|
444
|
+
}
|
|
445
|
+
const agent = {
|
|
446
|
+
publicKey,
|
|
447
|
+
name,
|
|
448
|
+
socket,
|
|
449
|
+
lastSeen: Date.now()
|
|
450
|
+
};
|
|
451
|
+
this.agents.set(publicKey, agent);
|
|
452
|
+
this.emit("agent-registered", publicKey);
|
|
453
|
+
let peers = Array.from(this.agents.values()).filter((a) => a.publicKey !== publicKey).map((a) => ({ publicKey: a.publicKey, name: a.name }));
|
|
454
|
+
for (const storagePeer of this.storagePeers) {
|
|
455
|
+
if (storagePeer !== publicKey && !this.agents.has(storagePeer)) {
|
|
456
|
+
peers.push({ publicKey: storagePeer, name: void 0 });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
socket.send(JSON.stringify({
|
|
460
|
+
type: "registered",
|
|
461
|
+
publicKey,
|
|
462
|
+
peers
|
|
463
|
+
}));
|
|
464
|
+
this.broadcastPeerEvent("peer_online", publicKey, name);
|
|
465
|
+
if (this.store && this.storagePeers.includes(publicKey)) {
|
|
466
|
+
const queued = this.store.load(publicKey);
|
|
467
|
+
for (const stored of queued) {
|
|
468
|
+
socket.send(JSON.stringify({
|
|
469
|
+
type: "message",
|
|
470
|
+
from: stored.from,
|
|
471
|
+
name: stored.name,
|
|
472
|
+
envelope: stored.envelope
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
475
|
+
this.store.clear(publicKey);
|
|
476
|
+
}
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (!agentPublicKey) {
|
|
480
|
+
this.sendError(socket, "Not registered: send registration message first");
|
|
481
|
+
socket.close();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (msg.type === "message") {
|
|
485
|
+
if (!msg.to || typeof msg.to !== "string") {
|
|
486
|
+
this.sendError(socket, 'Invalid message: missing or invalid "to" field');
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (!msg.envelope || typeof msg.envelope !== "object") {
|
|
490
|
+
this.sendError(socket, 'Invalid message: missing or invalid "envelope" field');
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const envelope = msg.envelope;
|
|
494
|
+
const verification = verifyEnvelope(envelope);
|
|
495
|
+
if (!verification.valid) {
|
|
496
|
+
this.sendError(socket, `Invalid envelope: ${verification.reason || "verification failed"}`);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (envelope.sender !== agentPublicKey) {
|
|
500
|
+
this.sendError(socket, "Envelope sender does not match registered public key");
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const senderAgent = this.agents.get(agentPublicKey);
|
|
504
|
+
if (senderAgent) {
|
|
505
|
+
senderAgent.lastSeen = Date.now();
|
|
506
|
+
}
|
|
507
|
+
if (envelope.type === "peer_list_request" && this.identity && msg.to === this.identity.publicKey) {
|
|
508
|
+
this.handlePeerListRequest(envelope, socket, agentPublicKey);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const recipient = this.agents.get(msg.to);
|
|
512
|
+
if (!recipient || recipient.socket.readyState !== WebSocket2.OPEN) {
|
|
513
|
+
if (this.store && this.storagePeers.includes(msg.to)) {
|
|
514
|
+
const senderAgent2 = this.agents.get(agentPublicKey);
|
|
515
|
+
this.store.save(msg.to, {
|
|
516
|
+
from: agentPublicKey,
|
|
517
|
+
name: senderAgent2?.name,
|
|
518
|
+
envelope
|
|
519
|
+
});
|
|
520
|
+
this.emit("message-relayed", agentPublicKey, msg.to, envelope);
|
|
521
|
+
} else {
|
|
522
|
+
this.sendError(socket, "Recipient not connected");
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
const senderAgent2 = this.agents.get(agentPublicKey);
|
|
528
|
+
const relayMessage = {
|
|
529
|
+
type: "message",
|
|
530
|
+
from: agentPublicKey,
|
|
531
|
+
name: senderAgent2?.name,
|
|
532
|
+
envelope
|
|
533
|
+
};
|
|
534
|
+
recipient.socket.send(JSON.stringify(relayMessage));
|
|
535
|
+
this.emit("message-relayed", agentPublicKey, msg.to, envelope);
|
|
536
|
+
} catch (err) {
|
|
537
|
+
this.sendError(socket, "Failed to relay message");
|
|
538
|
+
this.emit("error", err);
|
|
539
|
+
}
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (msg.type === "broadcast") {
|
|
543
|
+
if (!msg.envelope || typeof msg.envelope !== "object") {
|
|
544
|
+
this.sendError(socket, 'Invalid broadcast: missing or invalid "envelope" field');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const envelope = msg.envelope;
|
|
548
|
+
const verification = verifyEnvelope(envelope);
|
|
549
|
+
if (!verification.valid) {
|
|
550
|
+
this.sendError(socket, `Invalid envelope: ${verification.reason || "verification failed"}`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (envelope.sender !== agentPublicKey) {
|
|
554
|
+
this.sendError(socket, "Envelope sender does not match registered public key");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const senderAgentBroadcast = this.agents.get(agentPublicKey);
|
|
558
|
+
if (senderAgentBroadcast) {
|
|
559
|
+
senderAgentBroadcast.lastSeen = Date.now();
|
|
560
|
+
}
|
|
561
|
+
const senderAgent = this.agents.get(agentPublicKey);
|
|
562
|
+
const relayMessage = {
|
|
563
|
+
type: "message",
|
|
564
|
+
from: agentPublicKey,
|
|
565
|
+
name: senderAgent?.name,
|
|
566
|
+
envelope
|
|
567
|
+
};
|
|
568
|
+
const messageStr = JSON.stringify(relayMessage);
|
|
569
|
+
for (const agent of this.agents.values()) {
|
|
570
|
+
if (agent.publicKey !== agentPublicKey && agent.socket.readyState === WebSocket2.OPEN) {
|
|
571
|
+
try {
|
|
572
|
+
agent.socket.send(messageStr);
|
|
573
|
+
} catch (err) {
|
|
574
|
+
this.emit("error", err);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (msg.type === "ping") {
|
|
581
|
+
socket.send(JSON.stringify({ type: "pong" }));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
this.sendError(socket, `Unknown message type: ${msg.type}`);
|
|
585
|
+
} catch (err) {
|
|
586
|
+
this.emit("error", new Error(`Message parsing failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
587
|
+
this.sendError(socket, "Invalid message format");
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
socket.on("close", () => {
|
|
591
|
+
if (agentPublicKey) {
|
|
592
|
+
const agent = this.agents.get(agentPublicKey);
|
|
593
|
+
const agentName = agent?.name;
|
|
594
|
+
this.agents.delete(agentPublicKey);
|
|
595
|
+
this.emit("agent-disconnected", agentPublicKey);
|
|
596
|
+
if (!this.storagePeers.includes(agentPublicKey)) {
|
|
597
|
+
this.broadcastPeerEvent("peer_offline", agentPublicKey, agentName);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
socket.on("error", (error) => {
|
|
602
|
+
this.emit("error", error);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Send an error message to a client
|
|
607
|
+
*/
|
|
608
|
+
sendError(socket, message) {
|
|
609
|
+
try {
|
|
610
|
+
if (socket.readyState === WebSocket2.OPEN) {
|
|
611
|
+
socket.send(JSON.stringify({ type: "error", message }));
|
|
612
|
+
}
|
|
613
|
+
} catch (err) {
|
|
614
|
+
this.emit("error", new Error(`Failed to send error message: ${err instanceof Error ? err.message : String(err)}`));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Broadcast a peer event to all connected agents
|
|
619
|
+
*/
|
|
620
|
+
broadcastPeerEvent(eventType, publicKey, name) {
|
|
621
|
+
const message = {
|
|
622
|
+
type: eventType,
|
|
623
|
+
publicKey,
|
|
624
|
+
name
|
|
625
|
+
};
|
|
626
|
+
const messageStr = JSON.stringify(message);
|
|
627
|
+
for (const agent of this.agents.values()) {
|
|
628
|
+
if (agent.publicKey !== publicKey && agent.socket.readyState === WebSocket2.OPEN) {
|
|
629
|
+
try {
|
|
630
|
+
agent.socket.send(messageStr);
|
|
631
|
+
} catch (err) {
|
|
632
|
+
this.emit("error", new Error(`Failed to send ${eventType} event: ${err instanceof Error ? err.message : String(err)}`));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Handle peer list request from an agent
|
|
639
|
+
*/
|
|
640
|
+
handlePeerListRequest(envelope, socket, requesterPublicKey) {
|
|
641
|
+
if (!this.identity) {
|
|
642
|
+
this.sendError(socket, "Relay does not support peer discovery (no identity configured)");
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const { filters } = envelope.payload;
|
|
646
|
+
const now = Date.now();
|
|
647
|
+
let peers = Array.from(this.agents.values());
|
|
648
|
+
peers = peers.filter((p) => p.publicKey !== requesterPublicKey);
|
|
649
|
+
if (filters?.activeWithin) {
|
|
650
|
+
peers = peers.filter((p) => now - p.lastSeen < filters.activeWithin);
|
|
651
|
+
}
|
|
652
|
+
if (filters?.limit && filters.limit > 0) {
|
|
653
|
+
peers = peers.slice(0, filters.limit);
|
|
654
|
+
}
|
|
655
|
+
const response = {
|
|
656
|
+
peers: peers.map((p) => ({
|
|
657
|
+
publicKey: p.publicKey,
|
|
658
|
+
metadata: p.name || p.metadata ? {
|
|
659
|
+
name: p.name,
|
|
660
|
+
version: p.metadata?.version,
|
|
661
|
+
capabilities: p.metadata?.capabilities
|
|
662
|
+
} : void 0,
|
|
663
|
+
lastSeen: p.lastSeen
|
|
664
|
+
})),
|
|
665
|
+
totalPeers: this.agents.size - 1,
|
|
666
|
+
// Exclude requester from count
|
|
667
|
+
relayPublicKey: this.identity.publicKey
|
|
668
|
+
};
|
|
669
|
+
const responseEnvelope = createEnvelope(
|
|
670
|
+
"peer_list_response",
|
|
671
|
+
this.identity.publicKey,
|
|
672
|
+
this.identity.privateKey,
|
|
673
|
+
response,
|
|
674
|
+
Date.now(),
|
|
675
|
+
envelope.id
|
|
676
|
+
// Reply to the request
|
|
677
|
+
);
|
|
678
|
+
const relayMessage = {
|
|
679
|
+
type: "message",
|
|
680
|
+
from: this.identity.publicKey,
|
|
681
|
+
name: "relay",
|
|
682
|
+
envelope: responseEnvelope
|
|
683
|
+
};
|
|
684
|
+
try {
|
|
685
|
+
socket.send(JSON.stringify(relayMessage));
|
|
686
|
+
} catch (err) {
|
|
687
|
+
this.emit("error", new Error(`Failed to send peer list response: ${err instanceof Error ? err.message : String(err)}`));
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
// src/relay/client.ts
|
|
693
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
694
|
+
import WebSocket3 from "ws";
|
|
695
|
+
var RelayClient = class extends EventEmitter2 {
|
|
696
|
+
ws = null;
|
|
697
|
+
config;
|
|
698
|
+
reconnectAttempts = 0;
|
|
699
|
+
reconnectTimeout = null;
|
|
700
|
+
pingInterval = null;
|
|
701
|
+
isConnected = false;
|
|
702
|
+
isRegistered = false;
|
|
703
|
+
shouldReconnect = true;
|
|
704
|
+
onlinePeers = /* @__PURE__ */ new Map();
|
|
705
|
+
constructor(config) {
|
|
706
|
+
super();
|
|
707
|
+
this.config = {
|
|
708
|
+
pingInterval: 3e4,
|
|
709
|
+
maxReconnectDelay: 6e4,
|
|
710
|
+
...config
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Connect to the relay server
|
|
715
|
+
*/
|
|
716
|
+
async connect() {
|
|
717
|
+
if (this.ws && (this.ws.readyState === WebSocket3.CONNECTING || this.ws.readyState === WebSocket3.OPEN)) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
this.shouldReconnect = true;
|
|
721
|
+
return this.doConnect();
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Disconnect from the relay server
|
|
725
|
+
*/
|
|
726
|
+
disconnect() {
|
|
727
|
+
this.shouldReconnect = false;
|
|
728
|
+
this.cleanup();
|
|
729
|
+
if (this.ws) {
|
|
730
|
+
this.ws.close();
|
|
731
|
+
this.ws = null;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Check if currently connected and registered
|
|
736
|
+
*/
|
|
737
|
+
connected() {
|
|
738
|
+
return this.isConnected && this.isRegistered;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Send a message to a specific peer
|
|
742
|
+
*/
|
|
743
|
+
async send(to, envelope) {
|
|
744
|
+
if (!this.connected()) {
|
|
745
|
+
return { ok: false, error: "Not connected to relay" };
|
|
746
|
+
}
|
|
747
|
+
const message = {
|
|
748
|
+
type: "message",
|
|
749
|
+
to,
|
|
750
|
+
envelope
|
|
751
|
+
};
|
|
752
|
+
try {
|
|
753
|
+
this.ws.send(JSON.stringify(message));
|
|
754
|
+
return { ok: true };
|
|
755
|
+
} catch (err) {
|
|
756
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Broadcast a message to all connected peers
|
|
761
|
+
*/
|
|
762
|
+
async broadcast(envelope) {
|
|
763
|
+
if (!this.connected()) {
|
|
764
|
+
return { ok: false, error: "Not connected to relay" };
|
|
765
|
+
}
|
|
766
|
+
const message = {
|
|
767
|
+
type: "broadcast",
|
|
768
|
+
envelope
|
|
769
|
+
};
|
|
770
|
+
try {
|
|
771
|
+
this.ws.send(JSON.stringify(message));
|
|
772
|
+
return { ok: true };
|
|
773
|
+
} catch (err) {
|
|
774
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Get list of currently online peers
|
|
779
|
+
*/
|
|
780
|
+
getOnlinePeers() {
|
|
781
|
+
return Array.from(this.onlinePeers.values());
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Check if a specific peer is online
|
|
785
|
+
*/
|
|
786
|
+
isPeerOnline(publicKey) {
|
|
787
|
+
return this.onlinePeers.has(publicKey);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Internal: Perform connection
|
|
791
|
+
*/
|
|
792
|
+
async doConnect() {
|
|
793
|
+
return new Promise((resolve, reject) => {
|
|
794
|
+
try {
|
|
795
|
+
this.ws = new WebSocket3(this.config.relayUrl);
|
|
796
|
+
let resolved = false;
|
|
797
|
+
const resolveOnce = (callback) => {
|
|
798
|
+
if (!resolved) {
|
|
799
|
+
resolved = true;
|
|
800
|
+
callback();
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
this.ws.on("open", () => {
|
|
804
|
+
this.isConnected = true;
|
|
805
|
+
this.reconnectAttempts = 0;
|
|
806
|
+
this.startPingInterval();
|
|
807
|
+
const registerMsg = {
|
|
808
|
+
type: "register",
|
|
809
|
+
publicKey: this.config.publicKey,
|
|
810
|
+
name: this.config.name
|
|
811
|
+
};
|
|
812
|
+
this.ws.send(JSON.stringify(registerMsg));
|
|
813
|
+
});
|
|
814
|
+
this.ws.on("message", (data) => {
|
|
815
|
+
try {
|
|
816
|
+
const msg = JSON.parse(data.toString());
|
|
817
|
+
this.handleMessage(msg);
|
|
818
|
+
if (msg.type === "registered" && !resolved) {
|
|
819
|
+
resolveOnce(() => resolve());
|
|
820
|
+
}
|
|
821
|
+
} catch (err) {
|
|
822
|
+
this.emit("error", new Error(`Failed to parse message: ${err instanceof Error ? err.message : String(err)}`));
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
this.ws.on("close", () => {
|
|
826
|
+
this.isConnected = false;
|
|
827
|
+
this.isRegistered = false;
|
|
828
|
+
this.cleanup();
|
|
829
|
+
this.emit("disconnected");
|
|
830
|
+
if (this.shouldReconnect) {
|
|
831
|
+
this.scheduleReconnect();
|
|
832
|
+
}
|
|
833
|
+
if (!resolved) {
|
|
834
|
+
resolveOnce(() => reject(new Error("Connection closed before registration")));
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
this.ws.on("error", (err) => {
|
|
838
|
+
this.emit("error", err);
|
|
839
|
+
if (!resolved) {
|
|
840
|
+
resolveOnce(() => reject(err));
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
} catch (err) {
|
|
844
|
+
reject(err);
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Handle incoming message from relay
|
|
850
|
+
*/
|
|
851
|
+
handleMessage(msg) {
|
|
852
|
+
switch (msg.type) {
|
|
853
|
+
case "registered":
|
|
854
|
+
this.isRegistered = true;
|
|
855
|
+
if (msg.peers) {
|
|
856
|
+
for (const peer of msg.peers) {
|
|
857
|
+
this.onlinePeers.set(peer.publicKey, peer);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
this.emit("connected");
|
|
861
|
+
break;
|
|
862
|
+
case "message":
|
|
863
|
+
if (msg.envelope && msg.from) {
|
|
864
|
+
const verification = verifyEnvelope(msg.envelope);
|
|
865
|
+
if (!verification.valid) {
|
|
866
|
+
this.emit("error", new Error(`Invalid envelope signature: ${verification.reason}`));
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (msg.envelope.sender !== msg.from) {
|
|
870
|
+
this.emit("error", new Error("Envelope sender does not match relay from field"));
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
this.emit("message", msg.envelope, msg.from, msg.name);
|
|
874
|
+
}
|
|
875
|
+
break;
|
|
876
|
+
case "peer_online":
|
|
877
|
+
if (msg.publicKey) {
|
|
878
|
+
const peer = {
|
|
879
|
+
publicKey: msg.publicKey,
|
|
880
|
+
name: msg.name
|
|
881
|
+
};
|
|
882
|
+
this.onlinePeers.set(msg.publicKey, peer);
|
|
883
|
+
this.emit("peer_online", peer);
|
|
884
|
+
}
|
|
885
|
+
break;
|
|
886
|
+
case "peer_offline":
|
|
887
|
+
if (msg.publicKey) {
|
|
888
|
+
const peer = this.onlinePeers.get(msg.publicKey);
|
|
889
|
+
if (peer) {
|
|
890
|
+
this.onlinePeers.delete(msg.publicKey);
|
|
891
|
+
this.emit("peer_offline", peer);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
break;
|
|
895
|
+
case "error":
|
|
896
|
+
this.emit("error", new Error(`Relay error: ${msg.message || "Unknown error"}`));
|
|
897
|
+
break;
|
|
898
|
+
case "pong":
|
|
899
|
+
break;
|
|
900
|
+
default:
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Schedule reconnection with exponential backoff
|
|
906
|
+
*/
|
|
907
|
+
scheduleReconnect() {
|
|
908
|
+
if (this.reconnectTimeout) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const delay = Math.min(
|
|
912
|
+
1e3 * Math.pow(2, this.reconnectAttempts),
|
|
913
|
+
this.config.maxReconnectDelay
|
|
914
|
+
);
|
|
915
|
+
this.reconnectAttempts++;
|
|
916
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
917
|
+
this.reconnectTimeout = null;
|
|
918
|
+
if (this.shouldReconnect) {
|
|
919
|
+
this.doConnect().catch((err) => {
|
|
920
|
+
this.emit("error", err);
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}, delay);
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Start periodic ping messages
|
|
927
|
+
*/
|
|
928
|
+
startPingInterval() {
|
|
929
|
+
this.stopPingInterval();
|
|
930
|
+
this.pingInterval = setInterval(() => {
|
|
931
|
+
if (this.ws && this.ws.readyState === WebSocket3.OPEN) {
|
|
932
|
+
const ping = { type: "ping" };
|
|
933
|
+
this.ws.send(JSON.stringify(ping));
|
|
934
|
+
}
|
|
935
|
+
}, this.config.pingInterval);
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Stop ping interval
|
|
939
|
+
*/
|
|
940
|
+
stopPingInterval() {
|
|
941
|
+
if (this.pingInterval) {
|
|
942
|
+
clearInterval(this.pingInterval);
|
|
943
|
+
this.pingInterval = null;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Cleanup resources
|
|
948
|
+
*/
|
|
949
|
+
cleanup() {
|
|
950
|
+
this.stopPingInterval();
|
|
951
|
+
if (this.reconnectTimeout) {
|
|
952
|
+
clearTimeout(this.reconnectTimeout);
|
|
953
|
+
this.reconnectTimeout = null;
|
|
954
|
+
}
|
|
955
|
+
this.onlinePeers.clear();
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
// src/discovery/peer-discovery.ts
|
|
960
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
961
|
+
var PeerDiscoveryService = class extends EventEmitter3 {
|
|
962
|
+
config;
|
|
963
|
+
constructor(config) {
|
|
964
|
+
super();
|
|
965
|
+
this.config = config;
|
|
966
|
+
this.config.relayClient.on("message", (envelope, from) => {
|
|
967
|
+
if (envelope.type === "peer_list_response") {
|
|
968
|
+
this.handlePeerList(envelope);
|
|
969
|
+
} else if (envelope.type === "peer_referral") {
|
|
970
|
+
this.handleReferral(envelope, from);
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Request peer list from relay
|
|
976
|
+
*/
|
|
977
|
+
async discoverViaRelay(filters) {
|
|
978
|
+
if (!this.config.relayPublicKey) {
|
|
979
|
+
throw new Error("Relay public key not configured");
|
|
980
|
+
}
|
|
981
|
+
if (!this.config.relayClient.connected()) {
|
|
982
|
+
throw new Error("Not connected to relay");
|
|
983
|
+
}
|
|
984
|
+
const payload = filters ? { filters } : {};
|
|
985
|
+
const envelope = createEnvelope(
|
|
986
|
+
"peer_list_request",
|
|
987
|
+
this.config.publicKey,
|
|
988
|
+
this.config.privateKey,
|
|
989
|
+
payload
|
|
990
|
+
);
|
|
991
|
+
const result = await this.config.relayClient.send(this.config.relayPublicKey, envelope);
|
|
992
|
+
if (!result.ok) {
|
|
993
|
+
throw new Error(`Failed to send peer list request: ${result.error}`);
|
|
994
|
+
}
|
|
995
|
+
return new Promise((resolve, reject) => {
|
|
996
|
+
const timeout = setTimeout(() => {
|
|
997
|
+
cleanup();
|
|
998
|
+
reject(new Error("Peer list request timed out"));
|
|
999
|
+
}, 1e4);
|
|
1000
|
+
const messageHandler = (responseEnvelope, from) => {
|
|
1001
|
+
if (responseEnvelope.type === "peer_list_response" && responseEnvelope.inReplyTo === envelope.id && from === this.config.relayPublicKey) {
|
|
1002
|
+
cleanup();
|
|
1003
|
+
resolve(responseEnvelope.payload);
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
const cleanup = () => {
|
|
1007
|
+
clearTimeout(timeout);
|
|
1008
|
+
this.config.relayClient.off("message", messageHandler);
|
|
1009
|
+
};
|
|
1010
|
+
this.config.relayClient.on("message", messageHandler);
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Send peer referral to another agent
|
|
1015
|
+
*/
|
|
1016
|
+
async referPeer(recipientPublicKey, referredPublicKey, metadata) {
|
|
1017
|
+
if (!this.config.relayClient.connected()) {
|
|
1018
|
+
return { ok: false, error: "Not connected to relay" };
|
|
1019
|
+
}
|
|
1020
|
+
const payload = {
|
|
1021
|
+
publicKey: referredPublicKey,
|
|
1022
|
+
endpoint: metadata?.endpoint,
|
|
1023
|
+
metadata: metadata?.name ? { name: metadata.name } : void 0,
|
|
1024
|
+
comment: metadata?.comment,
|
|
1025
|
+
trustScore: metadata?.trustScore
|
|
1026
|
+
};
|
|
1027
|
+
const envelope = createEnvelope(
|
|
1028
|
+
"peer_referral",
|
|
1029
|
+
this.config.publicKey,
|
|
1030
|
+
this.config.privateKey,
|
|
1031
|
+
payload
|
|
1032
|
+
);
|
|
1033
|
+
return this.config.relayClient.send(recipientPublicKey, envelope);
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Handle incoming peer referral
|
|
1037
|
+
*/
|
|
1038
|
+
handleReferral(envelope, from) {
|
|
1039
|
+
const verification = verifyEnvelope(envelope);
|
|
1040
|
+
if (!verification.valid) {
|
|
1041
|
+
this.emit("error", new Error(`Invalid peer referral: ${verification.reason}`));
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
this.emit("peer-referral", envelope.payload, from);
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Handle incoming peer list from relay
|
|
1048
|
+
*/
|
|
1049
|
+
handlePeerList(envelope) {
|
|
1050
|
+
const verification = verifyEnvelope(envelope);
|
|
1051
|
+
if (!verification.valid) {
|
|
1052
|
+
this.emit("error", new Error(`Invalid peer list response: ${verification.reason}`));
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (envelope.sender !== this.config.relayPublicKey) {
|
|
1056
|
+
this.emit("error", new Error("Peer list response not from configured relay"));
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
this.emit("peers-discovered", envelope.payload.peers);
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
// src/discovery/bootstrap.ts
|
|
1064
|
+
var DEFAULT_BOOTSTRAP_RELAYS = [
|
|
1065
|
+
{
|
|
1066
|
+
url: "wss://agora-relay.lbsa71.net",
|
|
1067
|
+
name: "Primary Bootstrap Relay"
|
|
1068
|
+
// Note: Public key would need to be set when the relay is actually deployed
|
|
1069
|
+
// For now, this is a placeholder that would be configured when the relay is running
|
|
1070
|
+
}
|
|
1071
|
+
];
|
|
1072
|
+
function getDefaultBootstrapRelay() {
|
|
1073
|
+
return {
|
|
1074
|
+
relayUrl: DEFAULT_BOOTSTRAP_RELAYS[0].url,
|
|
1075
|
+
timeout: 1e4
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function parseBootstrapRelay(url, publicKey) {
|
|
1079
|
+
return {
|
|
1080
|
+
relayUrl: url,
|
|
1081
|
+
relayPublicKey: publicKey,
|
|
1082
|
+
timeout: 1e4
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/utils.ts
|
|
1087
|
+
function shortKey(publicKey) {
|
|
1088
|
+
return "..." + publicKey.slice(-8);
|
|
1089
|
+
}
|
|
1090
|
+
function resolveBroadcastName(config, cliName) {
|
|
1091
|
+
if (cliName) {
|
|
1092
|
+
return cliName;
|
|
1093
|
+
}
|
|
1094
|
+
if (config.relay) {
|
|
1095
|
+
if (typeof config.relay === "object" && config.relay.name) {
|
|
1096
|
+
return config.relay.name;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if (config.identity.name) {
|
|
1100
|
+
return config.identity.name;
|
|
1101
|
+
}
|
|
1102
|
+
return void 0;
|
|
1103
|
+
}
|
|
1104
|
+
function formatDisplayName(name, publicKey) {
|
|
1105
|
+
const shortId = shortKey(publicKey);
|
|
1106
|
+
if (!name || name.trim() === "" || name.startsWith("...")) {
|
|
1107
|
+
return shortId;
|
|
1108
|
+
}
|
|
1109
|
+
return `${name} (${shortId})`;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/reputation/types.ts
|
|
1113
|
+
function validateVerificationRecord(record) {
|
|
1114
|
+
const errors = [];
|
|
1115
|
+
if (typeof record !== "object" || record === null) {
|
|
1116
|
+
return { valid: false, errors: ["Record must be an object"] };
|
|
1117
|
+
}
|
|
1118
|
+
const r = record;
|
|
1119
|
+
if (typeof r.id !== "string" || r.id.length === 0) {
|
|
1120
|
+
errors.push("id must be a non-empty string");
|
|
1121
|
+
}
|
|
1122
|
+
if (typeof r.verifier !== "string" || r.verifier.length === 0) {
|
|
1123
|
+
errors.push("verifier must be a non-empty string");
|
|
1124
|
+
}
|
|
1125
|
+
if (typeof r.target !== "string" || r.target.length === 0) {
|
|
1126
|
+
errors.push("target must be a non-empty string");
|
|
1127
|
+
}
|
|
1128
|
+
if (typeof r.domain !== "string" || r.domain.length === 0) {
|
|
1129
|
+
errors.push("domain must be a non-empty string");
|
|
1130
|
+
}
|
|
1131
|
+
if (!["correct", "incorrect", "disputed"].includes(r.verdict)) {
|
|
1132
|
+
errors.push("verdict must be one of: correct, incorrect, disputed");
|
|
1133
|
+
}
|
|
1134
|
+
if (typeof r.confidence !== "number" || r.confidence < 0 || r.confidence > 1) {
|
|
1135
|
+
errors.push("confidence must be a number between 0 and 1");
|
|
1136
|
+
}
|
|
1137
|
+
if (r.evidence !== void 0 && typeof r.evidence !== "string") {
|
|
1138
|
+
errors.push("evidence must be a string if provided");
|
|
1139
|
+
}
|
|
1140
|
+
if (typeof r.timestamp !== "number" || r.timestamp <= 0) {
|
|
1141
|
+
errors.push("timestamp must be a positive number");
|
|
1142
|
+
}
|
|
1143
|
+
if (typeof r.signature !== "string" || r.signature.length === 0) {
|
|
1144
|
+
errors.push("signature must be a non-empty string");
|
|
1145
|
+
}
|
|
1146
|
+
return { valid: errors.length === 0, errors };
|
|
1147
|
+
}
|
|
1148
|
+
function validateCommitRecord(record) {
|
|
1149
|
+
const errors = [];
|
|
1150
|
+
if (typeof record !== "object" || record === null) {
|
|
1151
|
+
return { valid: false, errors: ["Record must be an object"] };
|
|
1152
|
+
}
|
|
1153
|
+
const r = record;
|
|
1154
|
+
if (typeof r.id !== "string" || r.id.length === 0) {
|
|
1155
|
+
errors.push("id must be a non-empty string");
|
|
1156
|
+
}
|
|
1157
|
+
if (typeof r.agent !== "string" || r.agent.length === 0) {
|
|
1158
|
+
errors.push("agent must be a non-empty string");
|
|
1159
|
+
}
|
|
1160
|
+
if (typeof r.domain !== "string" || r.domain.length === 0) {
|
|
1161
|
+
errors.push("domain must be a non-empty string");
|
|
1162
|
+
}
|
|
1163
|
+
if (typeof r.commitment !== "string" || r.commitment.length !== 64) {
|
|
1164
|
+
errors.push("commitment must be a 64-character hex string (SHA-256 hash)");
|
|
1165
|
+
}
|
|
1166
|
+
if (typeof r.timestamp !== "number" || r.timestamp <= 0) {
|
|
1167
|
+
errors.push("timestamp must be a positive number");
|
|
1168
|
+
}
|
|
1169
|
+
if (typeof r.expiry !== "number" || r.expiry <= 0) {
|
|
1170
|
+
errors.push("expiry must be a positive number");
|
|
1171
|
+
}
|
|
1172
|
+
if (typeof r.expiry === "number" && typeof r.timestamp === "number" && r.expiry <= r.timestamp) {
|
|
1173
|
+
errors.push("expiry must be after timestamp");
|
|
1174
|
+
}
|
|
1175
|
+
if (typeof r.signature !== "string" || r.signature.length === 0) {
|
|
1176
|
+
errors.push("signature must be a non-empty string");
|
|
1177
|
+
}
|
|
1178
|
+
return { valid: errors.length === 0, errors };
|
|
1179
|
+
}
|
|
1180
|
+
function validateRevealRecord(record) {
|
|
1181
|
+
const errors = [];
|
|
1182
|
+
if (typeof record !== "object" || record === null) {
|
|
1183
|
+
return { valid: false, errors: ["Record must be an object"] };
|
|
1184
|
+
}
|
|
1185
|
+
const r = record;
|
|
1186
|
+
if (typeof r.id !== "string" || r.id.length === 0) {
|
|
1187
|
+
errors.push("id must be a non-empty string");
|
|
1188
|
+
}
|
|
1189
|
+
if (typeof r.agent !== "string" || r.agent.length === 0) {
|
|
1190
|
+
errors.push("agent must be a non-empty string");
|
|
1191
|
+
}
|
|
1192
|
+
if (typeof r.commitmentId !== "string" || r.commitmentId.length === 0) {
|
|
1193
|
+
errors.push("commitmentId must be a non-empty string");
|
|
1194
|
+
}
|
|
1195
|
+
if (typeof r.prediction !== "string" || r.prediction.length === 0) {
|
|
1196
|
+
errors.push("prediction must be a non-empty string");
|
|
1197
|
+
}
|
|
1198
|
+
if (typeof r.outcome !== "string" || r.outcome.length === 0) {
|
|
1199
|
+
errors.push("outcome must be a non-empty string");
|
|
1200
|
+
}
|
|
1201
|
+
if (r.evidence !== void 0 && typeof r.evidence !== "string") {
|
|
1202
|
+
errors.push("evidence must be a string if provided");
|
|
1203
|
+
}
|
|
1204
|
+
if (typeof r.timestamp !== "number" || r.timestamp <= 0) {
|
|
1205
|
+
errors.push("timestamp must be a positive number");
|
|
1206
|
+
}
|
|
1207
|
+
if (typeof r.signature !== "string" || r.signature.length === 0) {
|
|
1208
|
+
errors.push("signature must be a non-empty string");
|
|
1209
|
+
}
|
|
1210
|
+
return { valid: errors.length === 0, errors };
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// src/reputation/store.ts
|
|
1214
|
+
import { promises as fs2 } from "fs";
|
|
1215
|
+
import { dirname } from "path";
|
|
1216
|
+
var ReputationStore = class {
|
|
1217
|
+
filePath;
|
|
1218
|
+
verifications = /* @__PURE__ */ new Map();
|
|
1219
|
+
commits = /* @__PURE__ */ new Map();
|
|
1220
|
+
reveals = /* @__PURE__ */ new Map();
|
|
1221
|
+
loaded = false;
|
|
1222
|
+
constructor(filePath) {
|
|
1223
|
+
this.filePath = filePath;
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Load records from JSONL file
|
|
1227
|
+
*/
|
|
1228
|
+
async load() {
|
|
1229
|
+
try {
|
|
1230
|
+
const content = await fs2.readFile(this.filePath, "utf-8");
|
|
1231
|
+
const lines = content.trim().split("\n").filter((line) => line.length > 0);
|
|
1232
|
+
for (const line of lines) {
|
|
1233
|
+
try {
|
|
1234
|
+
const record = JSON.parse(line);
|
|
1235
|
+
switch (record.type) {
|
|
1236
|
+
case "verification": {
|
|
1237
|
+
const { type: _type, ...verification } = record;
|
|
1238
|
+
const validation = validateVerificationRecord(verification);
|
|
1239
|
+
if (validation.valid) {
|
|
1240
|
+
this.verifications.set(verification.id, verification);
|
|
1241
|
+
}
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1244
|
+
case "commit": {
|
|
1245
|
+
const { type: _type, ...commit } = record;
|
|
1246
|
+
const validation = validateCommitRecord(commit);
|
|
1247
|
+
if (validation.valid) {
|
|
1248
|
+
this.commits.set(commit.id, commit);
|
|
1249
|
+
}
|
|
1250
|
+
break;
|
|
1251
|
+
}
|
|
1252
|
+
case "reveal": {
|
|
1253
|
+
const { type: _type, ...reveal } = record;
|
|
1254
|
+
const validation = validateRevealRecord(reveal);
|
|
1255
|
+
if (validation.valid) {
|
|
1256
|
+
this.reveals.set(reveal.id, reveal);
|
|
1257
|
+
}
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
} catch {
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
this.loaded = true;
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
if (error.code === "ENOENT") {
|
|
1268
|
+
this.loaded = true;
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
throw error;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Ensure the store is loaded
|
|
1276
|
+
*/
|
|
1277
|
+
async ensureLoaded() {
|
|
1278
|
+
if (!this.loaded) {
|
|
1279
|
+
await this.load();
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Append a record to the JSONL file
|
|
1284
|
+
*/
|
|
1285
|
+
async appendToFile(record) {
|
|
1286
|
+
await fs2.mkdir(dirname(this.filePath), { recursive: true });
|
|
1287
|
+
const line = JSON.stringify(record) + "\n";
|
|
1288
|
+
await fs2.appendFile(this.filePath, line, "utf-8");
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Add a verification record
|
|
1292
|
+
*/
|
|
1293
|
+
async addVerification(verification) {
|
|
1294
|
+
await this.ensureLoaded();
|
|
1295
|
+
const validation = validateVerificationRecord(verification);
|
|
1296
|
+
if (!validation.valid) {
|
|
1297
|
+
throw new Error(`Invalid verification: ${validation.errors.join(", ")}`);
|
|
1298
|
+
}
|
|
1299
|
+
this.verifications.set(verification.id, verification);
|
|
1300
|
+
await this.appendToFile({ type: "verification", ...verification });
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Add a commit record
|
|
1304
|
+
*/
|
|
1305
|
+
async addCommit(commit) {
|
|
1306
|
+
await this.ensureLoaded();
|
|
1307
|
+
const validation = validateCommitRecord(commit);
|
|
1308
|
+
if (!validation.valid) {
|
|
1309
|
+
throw new Error(`Invalid commit: ${validation.errors.join(", ")}`);
|
|
1310
|
+
}
|
|
1311
|
+
this.commits.set(commit.id, commit);
|
|
1312
|
+
await this.appendToFile({ type: "commit", ...commit });
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Add a reveal record
|
|
1316
|
+
*/
|
|
1317
|
+
async addReveal(reveal) {
|
|
1318
|
+
await this.ensureLoaded();
|
|
1319
|
+
const validation = validateRevealRecord(reveal);
|
|
1320
|
+
if (!validation.valid) {
|
|
1321
|
+
throw new Error(`Invalid reveal: ${validation.errors.join(", ")}`);
|
|
1322
|
+
}
|
|
1323
|
+
this.reveals.set(reveal.id, reveal);
|
|
1324
|
+
await this.appendToFile({ type: "reveal", ...reveal });
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Get all verifications
|
|
1328
|
+
*/
|
|
1329
|
+
async getVerifications() {
|
|
1330
|
+
await this.ensureLoaded();
|
|
1331
|
+
return Array.from(this.verifications.values());
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Get verifications for a specific target
|
|
1335
|
+
*/
|
|
1336
|
+
async getVerificationsByTarget(target) {
|
|
1337
|
+
await this.ensureLoaded();
|
|
1338
|
+
return Array.from(this.verifications.values()).filter((v) => v.target === target);
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Get verifications by domain
|
|
1342
|
+
*/
|
|
1343
|
+
async getVerificationsByDomain(domain) {
|
|
1344
|
+
await this.ensureLoaded();
|
|
1345
|
+
return Array.from(this.verifications.values()).filter((v) => v.domain === domain);
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Get verifications for an agent (where they are the target of verification)
|
|
1349
|
+
* This requires looking up the target message to find the agent
|
|
1350
|
+
* For now, we'll return all verifications and let the caller filter
|
|
1351
|
+
*/
|
|
1352
|
+
async getVerificationsByDomainForAgent(domain) {
|
|
1353
|
+
return this.getVerificationsByDomain(domain);
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Get all commits
|
|
1357
|
+
*/
|
|
1358
|
+
async getCommits() {
|
|
1359
|
+
await this.ensureLoaded();
|
|
1360
|
+
return Array.from(this.commits.values());
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Get commit by ID
|
|
1364
|
+
*/
|
|
1365
|
+
async getCommit(id) {
|
|
1366
|
+
await this.ensureLoaded();
|
|
1367
|
+
return this.commits.get(id) || null;
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Get commits by agent
|
|
1371
|
+
*/
|
|
1372
|
+
async getCommitsByAgent(agent) {
|
|
1373
|
+
await this.ensureLoaded();
|
|
1374
|
+
return Array.from(this.commits.values()).filter((c) => c.agent === agent);
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Get all reveals
|
|
1378
|
+
*/
|
|
1379
|
+
async getReveals() {
|
|
1380
|
+
await this.ensureLoaded();
|
|
1381
|
+
return Array.from(this.reveals.values());
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Get reveal by commitment ID
|
|
1385
|
+
*/
|
|
1386
|
+
async getRevealByCommitment(commitmentId) {
|
|
1387
|
+
await this.ensureLoaded();
|
|
1388
|
+
return Array.from(this.reveals.values()).find((r) => r.commitmentId === commitmentId) || null;
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Get reveals by agent
|
|
1392
|
+
*/
|
|
1393
|
+
async getRevealsByAgent(agent) {
|
|
1394
|
+
await this.ensureLoaded();
|
|
1395
|
+
return Array.from(this.reveals.values()).filter((r) => r.agent === agent);
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
// src/reputation/verification.ts
|
|
1400
|
+
function createVerification(verifier, privateKey, target, domain, verdict, confidence, timestamp, evidence) {
|
|
1401
|
+
if (confidence < 0 || confidence > 1) {
|
|
1402
|
+
throw new Error("confidence must be between 0 and 1");
|
|
1403
|
+
}
|
|
1404
|
+
const payload = {
|
|
1405
|
+
verifier,
|
|
1406
|
+
target,
|
|
1407
|
+
domain,
|
|
1408
|
+
verdict,
|
|
1409
|
+
confidence,
|
|
1410
|
+
timestamp
|
|
1411
|
+
};
|
|
1412
|
+
if (evidence !== void 0) {
|
|
1413
|
+
payload.evidence = evidence;
|
|
1414
|
+
}
|
|
1415
|
+
const envelope = createEnvelope("verification", verifier, privateKey, payload, timestamp);
|
|
1416
|
+
const record = {
|
|
1417
|
+
id: envelope.id,
|
|
1418
|
+
verifier,
|
|
1419
|
+
target,
|
|
1420
|
+
domain,
|
|
1421
|
+
verdict,
|
|
1422
|
+
confidence,
|
|
1423
|
+
timestamp,
|
|
1424
|
+
signature: envelope.signature
|
|
1425
|
+
};
|
|
1426
|
+
if (evidence !== void 0) {
|
|
1427
|
+
record.evidence = evidence;
|
|
1428
|
+
}
|
|
1429
|
+
return record;
|
|
1430
|
+
}
|
|
1431
|
+
function verifyVerificationSignature(record) {
|
|
1432
|
+
const structureValidation = validateVerificationRecord(record);
|
|
1433
|
+
if (!structureValidation.valid) {
|
|
1434
|
+
return {
|
|
1435
|
+
valid: false,
|
|
1436
|
+
reason: `Invalid structure: ${structureValidation.errors.join(", ")}`
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
const payload = {
|
|
1440
|
+
verifier: record.verifier,
|
|
1441
|
+
target: record.target,
|
|
1442
|
+
domain: record.domain,
|
|
1443
|
+
verdict: record.verdict,
|
|
1444
|
+
confidence: record.confidence,
|
|
1445
|
+
timestamp: record.timestamp
|
|
1446
|
+
};
|
|
1447
|
+
if (record.evidence !== void 0) {
|
|
1448
|
+
payload.evidence = record.evidence;
|
|
1449
|
+
}
|
|
1450
|
+
const envelope = {
|
|
1451
|
+
id: record.id,
|
|
1452
|
+
type: "verification",
|
|
1453
|
+
sender: record.verifier,
|
|
1454
|
+
timestamp: record.timestamp,
|
|
1455
|
+
payload,
|
|
1456
|
+
signature: record.signature
|
|
1457
|
+
};
|
|
1458
|
+
return verifyEnvelope(envelope);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/reputation/commit-reveal.ts
|
|
1462
|
+
import { createHash as createHash2 } from "crypto";
|
|
1463
|
+
function hashPrediction(prediction) {
|
|
1464
|
+
return createHash2("sha256").update(prediction).digest("hex");
|
|
1465
|
+
}
|
|
1466
|
+
function createCommit(agent, privateKey, domain, prediction, timestamp, expiryMs) {
|
|
1467
|
+
const commitment = hashPrediction(prediction);
|
|
1468
|
+
const expiry = timestamp + expiryMs;
|
|
1469
|
+
const payload = {
|
|
1470
|
+
agent,
|
|
1471
|
+
domain,
|
|
1472
|
+
commitment,
|
|
1473
|
+
timestamp,
|
|
1474
|
+
expiry
|
|
1475
|
+
};
|
|
1476
|
+
const envelope = createEnvelope("commit", agent, privateKey, payload, timestamp);
|
|
1477
|
+
return {
|
|
1478
|
+
id: envelope.id,
|
|
1479
|
+
agent,
|
|
1480
|
+
domain,
|
|
1481
|
+
commitment,
|
|
1482
|
+
timestamp,
|
|
1483
|
+
expiry,
|
|
1484
|
+
signature: envelope.signature
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
function createReveal(agent, privateKey, commitmentId, prediction, outcome, timestamp, evidence) {
|
|
1488
|
+
const payload = {
|
|
1489
|
+
agent,
|
|
1490
|
+
commitmentId,
|
|
1491
|
+
prediction,
|
|
1492
|
+
outcome,
|
|
1493
|
+
timestamp
|
|
1494
|
+
};
|
|
1495
|
+
if (evidence !== void 0) {
|
|
1496
|
+
payload.evidence = evidence;
|
|
1497
|
+
}
|
|
1498
|
+
const envelope = createEnvelope("reveal", agent, privateKey, payload, timestamp);
|
|
1499
|
+
const record = {
|
|
1500
|
+
id: envelope.id,
|
|
1501
|
+
agent,
|
|
1502
|
+
commitmentId,
|
|
1503
|
+
prediction,
|
|
1504
|
+
outcome,
|
|
1505
|
+
timestamp,
|
|
1506
|
+
signature: envelope.signature
|
|
1507
|
+
};
|
|
1508
|
+
if (evidence !== void 0) {
|
|
1509
|
+
record.evidence = evidence;
|
|
1510
|
+
}
|
|
1511
|
+
return record;
|
|
1512
|
+
}
|
|
1513
|
+
function verifyReveal(commit, reveal) {
|
|
1514
|
+
const commitValidation = validateCommitRecord(commit);
|
|
1515
|
+
if (!commitValidation.valid) {
|
|
1516
|
+
return { valid: false, reason: `Invalid commit: ${commitValidation.errors.join(", ")}` };
|
|
1517
|
+
}
|
|
1518
|
+
const revealValidation = validateRevealRecord(reveal);
|
|
1519
|
+
if (!revealValidation.valid) {
|
|
1520
|
+
return { valid: false, reason: `Invalid reveal: ${revealValidation.errors.join(", ")}` };
|
|
1521
|
+
}
|
|
1522
|
+
if (reveal.commitmentId !== commit.id) {
|
|
1523
|
+
return { valid: false, reason: "Reveal does not reference this commit" };
|
|
1524
|
+
}
|
|
1525
|
+
if (reveal.agent !== commit.agent) {
|
|
1526
|
+
return { valid: false, reason: "Reveal agent does not match commit agent" };
|
|
1527
|
+
}
|
|
1528
|
+
if (reveal.timestamp < commit.expiry) {
|
|
1529
|
+
return { valid: false, reason: "Reveal timestamp is before commit expiry" };
|
|
1530
|
+
}
|
|
1531
|
+
const predictedHash = hashPrediction(reveal.prediction);
|
|
1532
|
+
if (predictedHash !== commit.commitment) {
|
|
1533
|
+
return { valid: false, reason: "Prediction hash does not match commitment" };
|
|
1534
|
+
}
|
|
1535
|
+
return { valid: true };
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// src/reputation/scoring.ts
|
|
1539
|
+
function decay(deltaTimeMs, lambda = Math.log(2) / 70) {
|
|
1540
|
+
const deltaDays = deltaTimeMs / (1e3 * 60 * 60 * 24);
|
|
1541
|
+
return Math.exp(-lambda * deltaDays);
|
|
1542
|
+
}
|
|
1543
|
+
function verdictWeight(verdict) {
|
|
1544
|
+
switch (verdict) {
|
|
1545
|
+
case "correct":
|
|
1546
|
+
return 1;
|
|
1547
|
+
case "incorrect":
|
|
1548
|
+
return -1;
|
|
1549
|
+
case "disputed":
|
|
1550
|
+
return 0;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
function computeTrustScore(agent, domain, verifications, currentTime) {
|
|
1554
|
+
const relevantVerifications = verifications.filter(
|
|
1555
|
+
(v) => v.target === agent && v.domain === domain
|
|
1556
|
+
);
|
|
1557
|
+
if (relevantVerifications.length === 0) {
|
|
1558
|
+
return {
|
|
1559
|
+
agent,
|
|
1560
|
+
domain,
|
|
1561
|
+
score: 0,
|
|
1562
|
+
verificationCount: 0,
|
|
1563
|
+
lastVerified: 0,
|
|
1564
|
+
topVerifiers: []
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
let totalWeight = 0;
|
|
1568
|
+
const verifierWeights = /* @__PURE__ */ new Map();
|
|
1569
|
+
for (const verification of relevantVerifications) {
|
|
1570
|
+
const deltaTime = currentTime - verification.timestamp;
|
|
1571
|
+
const decayFactor = decay(deltaTime);
|
|
1572
|
+
const verdict = verdictWeight(verification.verdict);
|
|
1573
|
+
const weight = verdict * verification.confidence * decayFactor;
|
|
1574
|
+
totalWeight += weight;
|
|
1575
|
+
const currentVerifierWeight = verifierWeights.get(verification.verifier) || 0;
|
|
1576
|
+
verifierWeights.set(verification.verifier, currentVerifierWeight + Math.abs(weight));
|
|
1577
|
+
}
|
|
1578
|
+
const rawScore = totalWeight / Math.max(relevantVerifications.length, 1);
|
|
1579
|
+
const normalizedScore = Math.max(0, Math.min(1, (rawScore + 1) / 2));
|
|
1580
|
+
const lastVerified = Math.max(...relevantVerifications.map((v) => v.timestamp));
|
|
1581
|
+
const topVerifiers = Array.from(verifierWeights.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([verifier]) => verifier);
|
|
1582
|
+
return {
|
|
1583
|
+
agent,
|
|
1584
|
+
domain,
|
|
1585
|
+
score: normalizedScore,
|
|
1586
|
+
verificationCount: relevantVerifications.length,
|
|
1587
|
+
lastVerified,
|
|
1588
|
+
topVerifiers
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
function computeTrustScores(agent, verifications, currentTime) {
|
|
1592
|
+
const domains = new Set(
|
|
1593
|
+
verifications.filter((v) => v.target === agent).map((v) => v.domain)
|
|
1594
|
+
);
|
|
1595
|
+
const scores = /* @__PURE__ */ new Map();
|
|
1596
|
+
for (const domain of domains) {
|
|
1597
|
+
const score = computeTrustScore(agent, domain, verifications, currentTime);
|
|
1598
|
+
scores.set(domain, score);
|
|
1599
|
+
}
|
|
1600
|
+
return scores;
|
|
1601
|
+
}
|
|
1602
|
+
var computeAllTrustScores = computeTrustScores;
|
|
1603
|
+
|
|
1604
|
+
export {
|
|
1605
|
+
generateKeyPair,
|
|
1606
|
+
signMessage,
|
|
1607
|
+
verifySignature,
|
|
1608
|
+
exportKeyPair,
|
|
1609
|
+
importKeyPair,
|
|
1610
|
+
loadPeerConfig,
|
|
1611
|
+
savePeerConfig,
|
|
1612
|
+
initPeerConfig,
|
|
1613
|
+
canonicalize,
|
|
1614
|
+
computeId,
|
|
1615
|
+
createEnvelope,
|
|
1616
|
+
verifyEnvelope,
|
|
1617
|
+
sendToPeer,
|
|
1618
|
+
decodeInboundEnvelope,
|
|
1619
|
+
sendViaRelay,
|
|
1620
|
+
MessageStore,
|
|
1621
|
+
RelayServer,
|
|
1622
|
+
RelayClient,
|
|
1623
|
+
PeerDiscoveryService,
|
|
1624
|
+
DEFAULT_BOOTSTRAP_RELAYS,
|
|
1625
|
+
getDefaultBootstrapRelay,
|
|
1626
|
+
parseBootstrapRelay,
|
|
1627
|
+
shortKey,
|
|
1628
|
+
resolveBroadcastName,
|
|
1629
|
+
formatDisplayName,
|
|
1630
|
+
validateVerificationRecord,
|
|
1631
|
+
validateCommitRecord,
|
|
1632
|
+
validateRevealRecord,
|
|
1633
|
+
ReputationStore,
|
|
1634
|
+
createVerification,
|
|
1635
|
+
verifyVerificationSignature,
|
|
1636
|
+
hashPrediction,
|
|
1637
|
+
createCommit,
|
|
1638
|
+
createReveal,
|
|
1639
|
+
verifyReveal,
|
|
1640
|
+
decay,
|
|
1641
|
+
computeTrustScore,
|
|
1642
|
+
computeTrustScores,
|
|
1643
|
+
computeAllTrustScores
|
|
1644
|
+
};
|
|
1645
|
+
//# sourceMappingURL=chunk-JUOGKXFN.js.map
|