@rookdaemon/agora 0.2.8 → 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/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 -30
- package/dist/index.js +1135 -29
- package/dist/index.js.map +1 -1
- package/package.json +4 -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/jwt-auth.d.ts +0 -40
- package/dist/relay/jwt-auth.d.ts.map +0 -1
- package/dist/relay/jwt-auth.js +0 -109
- package/dist/relay/jwt-auth.js.map +0 -1
- package/dist/relay/message-buffer.d.ts +0 -41
- package/dist/relay/message-buffer.d.ts.map +0 -1
- package/dist/relay/message-buffer.js +0 -53
- package/dist/relay/message-buffer.js.map +0 -1
- package/dist/relay/rest-api.d.ts +0 -68
- package/dist/relay/rest-api.d.ts.map +0 -1
- package/dist/relay/rest-api.js +0 -225
- package/dist/relay/rest-api.js.map +0 -1
- package/dist/relay/run-relay.d.ts +0 -33
- package/dist/relay/run-relay.d.ts.map +0 -1
- package/dist/relay/run-relay.js +0 -57
- package/dist/relay/run-relay.js.map +0 -1
- package/dist/relay/server.d.ts +0 -91
- package/dist/relay/server.d.ts.map +0 -1
- package/dist/relay/server.js +0 -385
- package/dist/relay/server.js.map +0 -1
- package/dist/relay/store.d.ts +0 -19
- package/dist/relay/store.d.ts.map +0 -1
- package/dist/relay/store.js +0 -55
- package/dist/relay/store.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
package/dist/index.js
CHANGED
|
@@ -1,30 +1,1136 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_BOOTSTRAP_RELAYS,
|
|
3
|
+
MessageStore,
|
|
4
|
+
PeerDiscoveryService,
|
|
5
|
+
RelayClient,
|
|
6
|
+
RelayServer,
|
|
7
|
+
ReputationStore,
|
|
8
|
+
canonicalize,
|
|
9
|
+
computeAllTrustScores,
|
|
10
|
+
computeId,
|
|
11
|
+
computeTrustScore,
|
|
12
|
+
computeTrustScores,
|
|
13
|
+
createCommit,
|
|
14
|
+
createEnvelope,
|
|
15
|
+
createReveal,
|
|
16
|
+
createVerification,
|
|
17
|
+
decay,
|
|
18
|
+
decodeInboundEnvelope,
|
|
19
|
+
exportKeyPair,
|
|
20
|
+
formatDisplayName,
|
|
21
|
+
generateKeyPair,
|
|
22
|
+
getDefaultBootstrapRelay,
|
|
23
|
+
hashPrediction,
|
|
24
|
+
importKeyPair,
|
|
25
|
+
initPeerConfig,
|
|
26
|
+
loadPeerConfig,
|
|
27
|
+
parseBootstrapRelay,
|
|
28
|
+
resolveBroadcastName,
|
|
29
|
+
savePeerConfig,
|
|
30
|
+
sendToPeer,
|
|
31
|
+
sendViaRelay,
|
|
32
|
+
shortKey,
|
|
33
|
+
signMessage,
|
|
34
|
+
validateCommitRecord,
|
|
35
|
+
validateRevealRecord,
|
|
36
|
+
validateVerificationRecord,
|
|
37
|
+
verifyEnvelope,
|
|
38
|
+
verifyReveal,
|
|
39
|
+
verifySignature,
|
|
40
|
+
verifyVerificationSignature
|
|
41
|
+
} from "./chunk-JUOGKXFN.js";
|
|
42
|
+
|
|
43
|
+
// src/registry/capability.ts
|
|
44
|
+
import { createHash } from "crypto";
|
|
45
|
+
function stableStringify(value) {
|
|
46
|
+
if (value === null || value === void 0) return JSON.stringify(value);
|
|
47
|
+
if (typeof value !== "object") return JSON.stringify(value);
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
return "[" + value.map(stableStringify).join(",") + "]";
|
|
50
|
+
}
|
|
51
|
+
const keys = Object.keys(value).sort();
|
|
52
|
+
const pairs = keys.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k]));
|
|
53
|
+
return "{" + pairs.join(",") + "}";
|
|
54
|
+
}
|
|
55
|
+
function computeCapabilityId(name, version, inputSchema, outputSchema) {
|
|
56
|
+
const data = {
|
|
57
|
+
name,
|
|
58
|
+
version,
|
|
59
|
+
...inputSchema !== void 0 ? { inputSchema } : {},
|
|
60
|
+
...outputSchema !== void 0 ? { outputSchema } : {}
|
|
61
|
+
};
|
|
62
|
+
const canonical = stableStringify(data);
|
|
63
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
64
|
+
}
|
|
65
|
+
function createCapability(name, version, description, options = {}) {
|
|
66
|
+
const { inputSchema, outputSchema, tags = [] } = options;
|
|
67
|
+
const id = computeCapabilityId(name, version, inputSchema, outputSchema);
|
|
68
|
+
return {
|
|
69
|
+
id,
|
|
70
|
+
name,
|
|
71
|
+
version,
|
|
72
|
+
description,
|
|
73
|
+
...inputSchema !== void 0 ? { inputSchema } : {},
|
|
74
|
+
...outputSchema !== void 0 ? { outputSchema } : {},
|
|
75
|
+
tags
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function validateCapability(capability) {
|
|
79
|
+
const errors = [];
|
|
80
|
+
if (!capability || typeof capability !== "object") {
|
|
81
|
+
return { valid: false, errors: ["Capability must be an object"] };
|
|
82
|
+
}
|
|
83
|
+
const cap = capability;
|
|
84
|
+
if (!cap.id || typeof cap.id !== "string") {
|
|
85
|
+
errors.push("Missing or invalid field: id (must be a string)");
|
|
86
|
+
}
|
|
87
|
+
if (!cap.name || typeof cap.name !== "string") {
|
|
88
|
+
errors.push("Missing or invalid field: name (must be a string)");
|
|
89
|
+
}
|
|
90
|
+
if (!cap.version || typeof cap.version !== "string") {
|
|
91
|
+
errors.push("Missing or invalid field: version (must be a string)");
|
|
92
|
+
}
|
|
93
|
+
if (!cap.description || typeof cap.description !== "string") {
|
|
94
|
+
errors.push("Missing or invalid field: description (must be a string)");
|
|
95
|
+
}
|
|
96
|
+
if (!Array.isArray(cap.tags)) {
|
|
97
|
+
errors.push("Missing or invalid field: tags (must be an array)");
|
|
98
|
+
} else if (!cap.tags.every((tag) => typeof tag === "string")) {
|
|
99
|
+
errors.push("Invalid field: tags (all elements must be strings)");
|
|
100
|
+
}
|
|
101
|
+
if (cap.inputSchema !== void 0 && (typeof cap.inputSchema !== "object" || cap.inputSchema === null)) {
|
|
102
|
+
errors.push("Invalid field: inputSchema (must be an object)");
|
|
103
|
+
}
|
|
104
|
+
if (cap.outputSchema !== void 0 && (typeof cap.outputSchema !== "object" || cap.outputSchema === null)) {
|
|
105
|
+
errors.push("Invalid field: outputSchema (must be an object)");
|
|
106
|
+
}
|
|
107
|
+
if (errors.length > 0) {
|
|
108
|
+
return { valid: false, errors };
|
|
109
|
+
}
|
|
110
|
+
return { valid: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/registry/peer-store.ts
|
|
114
|
+
var PeerStore = class {
|
|
115
|
+
peers = /* @__PURE__ */ new Map();
|
|
116
|
+
/**
|
|
117
|
+
* Add or update a peer in the store.
|
|
118
|
+
* If a peer with the same publicKey exists, it will be updated.
|
|
119
|
+
*
|
|
120
|
+
* @param peer - The peer to add or update
|
|
121
|
+
*/
|
|
122
|
+
addOrUpdatePeer(peer) {
|
|
123
|
+
this.peers.set(peer.publicKey, peer);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Remove a peer from the store.
|
|
127
|
+
*
|
|
128
|
+
* @param publicKey - The public key of the peer to remove
|
|
129
|
+
* @returns true if the peer was removed, false if it didn't exist
|
|
130
|
+
*/
|
|
131
|
+
removePeer(publicKey) {
|
|
132
|
+
return this.peers.delete(publicKey);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get a peer by their public key.
|
|
136
|
+
*
|
|
137
|
+
* @param publicKey - The public key of the peer to retrieve
|
|
138
|
+
* @returns The peer if found, undefined otherwise
|
|
139
|
+
*/
|
|
140
|
+
getPeer(publicKey) {
|
|
141
|
+
return this.peers.get(publicKey);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Find all peers that offer a specific capability by name.
|
|
145
|
+
*
|
|
146
|
+
* @param name - The capability name to search for
|
|
147
|
+
* @returns Array of peers that have a capability with the given name
|
|
148
|
+
*/
|
|
149
|
+
findByCapability(name) {
|
|
150
|
+
const result = [];
|
|
151
|
+
for (const peer of this.peers.values()) {
|
|
152
|
+
const hasCapability = peer.capabilities.some((cap) => cap.name === name);
|
|
153
|
+
if (hasCapability) {
|
|
154
|
+
result.push(peer);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Find all peers that have capabilities with a specific tag.
|
|
161
|
+
*
|
|
162
|
+
* @param tag - The tag to search for
|
|
163
|
+
* @returns Array of peers that have at least one capability with the given tag
|
|
164
|
+
*/
|
|
165
|
+
findByTag(tag) {
|
|
166
|
+
const result = [];
|
|
167
|
+
for (const peer of this.peers.values()) {
|
|
168
|
+
const hasTag = peer.capabilities.some((cap) => cap.tags.includes(tag));
|
|
169
|
+
if (hasTag) {
|
|
170
|
+
result.push(peer);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get all peers in the store.
|
|
177
|
+
*
|
|
178
|
+
* @returns Array of all peers
|
|
179
|
+
*/
|
|
180
|
+
allPeers() {
|
|
181
|
+
return Array.from(this.peers.values());
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Remove peers that haven't been seen within the specified time window.
|
|
185
|
+
*
|
|
186
|
+
* @param maxAgeMs - Maximum age in milliseconds. Peers older than this will be removed.
|
|
187
|
+
* @param currentTime - Current timestamp (ms), defaults to Date.now()
|
|
188
|
+
* @returns Number of peers removed
|
|
189
|
+
*/
|
|
190
|
+
prune(maxAgeMs, currentTime = Date.now()) {
|
|
191
|
+
const cutoff = currentTime - maxAgeMs;
|
|
192
|
+
let removed = 0;
|
|
193
|
+
for (const [publicKey, peer] of this.peers.entries()) {
|
|
194
|
+
if (peer.lastSeen < cutoff) {
|
|
195
|
+
this.peers.delete(publicKey);
|
|
196
|
+
removed++;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return removed;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// src/registry/discovery-service.ts
|
|
204
|
+
var DiscoveryService = class {
|
|
205
|
+
constructor(peerStore, identity) {
|
|
206
|
+
this.peerStore = peerStore;
|
|
207
|
+
this.identity = identity;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Announce own capabilities to the network.
|
|
211
|
+
* Creates a capability_announce envelope that can be broadcast to peers.
|
|
212
|
+
*
|
|
213
|
+
* @param capabilities - List of capabilities this agent offers
|
|
214
|
+
* @param metadata - Optional metadata about this agent
|
|
215
|
+
* @returns A signed capability_announce envelope
|
|
216
|
+
*/
|
|
217
|
+
announce(capabilities, metadata) {
|
|
218
|
+
const payload = {
|
|
219
|
+
publicKey: this.identity.publicKey,
|
|
220
|
+
capabilities,
|
|
221
|
+
metadata: metadata ? {
|
|
222
|
+
...metadata,
|
|
223
|
+
lastSeen: Date.now()
|
|
224
|
+
} : {
|
|
225
|
+
lastSeen: Date.now()
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
return createEnvelope(
|
|
229
|
+
"capability_announce",
|
|
230
|
+
this.identity.publicKey,
|
|
231
|
+
this.identity.privateKey,
|
|
232
|
+
payload
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Handle an incoming capability_announce message.
|
|
237
|
+
* Updates the peer store with the announced capabilities.
|
|
238
|
+
*
|
|
239
|
+
* @param envelope - The capability_announce envelope to process
|
|
240
|
+
*/
|
|
241
|
+
handleAnnounce(envelope) {
|
|
242
|
+
const { payload } = envelope;
|
|
243
|
+
const peer = {
|
|
244
|
+
publicKey: payload.publicKey,
|
|
245
|
+
capabilities: payload.capabilities,
|
|
246
|
+
lastSeen: payload.metadata?.lastSeen || envelope.timestamp,
|
|
247
|
+
metadata: payload.metadata ? {
|
|
248
|
+
name: payload.metadata.name,
|
|
249
|
+
version: payload.metadata.version
|
|
250
|
+
} : void 0
|
|
251
|
+
};
|
|
252
|
+
this.peerStore.addOrUpdatePeer(peer);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Create a capability query payload.
|
|
256
|
+
*
|
|
257
|
+
* @param queryType - Type of query: 'name', 'tag', or 'schema'
|
|
258
|
+
* @param query - The query value (capability name, tag, or schema)
|
|
259
|
+
* @param filters - Optional filters (limit, minTrustScore)
|
|
260
|
+
* @returns A capability_query payload
|
|
261
|
+
*/
|
|
262
|
+
query(queryType, query, filters) {
|
|
263
|
+
return {
|
|
264
|
+
queryType,
|
|
265
|
+
query,
|
|
266
|
+
filters
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Handle an incoming capability_query message.
|
|
271
|
+
* Searches the local peer store and returns matching peers.
|
|
272
|
+
*
|
|
273
|
+
* @param envelope - The capability_query envelope to process
|
|
274
|
+
* @returns A capability_response envelope with matching peers
|
|
275
|
+
*/
|
|
276
|
+
handleQuery(envelope) {
|
|
277
|
+
const { payload } = envelope;
|
|
278
|
+
let peers = [];
|
|
279
|
+
if (payload.queryType === "name" && typeof payload.query === "string") {
|
|
280
|
+
peers = this.peerStore.findByCapability(payload.query);
|
|
281
|
+
} else if (payload.queryType === "tag" && typeof payload.query === "string") {
|
|
282
|
+
peers = this.peerStore.findByTag(payload.query);
|
|
283
|
+
} else if (payload.queryType === "schema") {
|
|
284
|
+
peers = [];
|
|
285
|
+
}
|
|
286
|
+
const limit = payload.filters?.limit;
|
|
287
|
+
const totalMatches = peers.length;
|
|
288
|
+
if (limit !== void 0 && limit > 0) {
|
|
289
|
+
peers = peers.slice(0, limit);
|
|
290
|
+
}
|
|
291
|
+
const responsePeers = peers.map((peer) => ({
|
|
292
|
+
publicKey: peer.publicKey,
|
|
293
|
+
capabilities: peer.capabilities,
|
|
294
|
+
metadata: peer.metadata ? {
|
|
295
|
+
name: peer.metadata.name,
|
|
296
|
+
version: peer.metadata.version,
|
|
297
|
+
lastSeen: peer.lastSeen
|
|
298
|
+
} : {
|
|
299
|
+
lastSeen: peer.lastSeen
|
|
300
|
+
},
|
|
301
|
+
// Trust score integration deferred to Phase 2b (RFC-001)
|
|
302
|
+
trustScore: void 0
|
|
303
|
+
}));
|
|
304
|
+
const responsePayload = {
|
|
305
|
+
queryId: envelope.id,
|
|
306
|
+
peers: responsePeers,
|
|
307
|
+
totalMatches
|
|
308
|
+
};
|
|
309
|
+
return createEnvelope(
|
|
310
|
+
"capability_response",
|
|
311
|
+
this.identity.publicKey,
|
|
312
|
+
this.identity.privateKey,
|
|
313
|
+
responsePayload,
|
|
314
|
+
Date.now(),
|
|
315
|
+
envelope.id
|
|
316
|
+
// inReplyTo
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Remove peers that haven't been seen within the specified time window.
|
|
321
|
+
*
|
|
322
|
+
* @param maxAgeMs - Maximum age in milliseconds
|
|
323
|
+
* @returns Number of peers removed
|
|
324
|
+
*/
|
|
325
|
+
pruneStale(maxAgeMs, currentTime = Date.now()) {
|
|
326
|
+
return this.peerStore.prune(maxAgeMs, currentTime);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// src/message/types/peer-discovery.ts
|
|
331
|
+
function validatePeerListRequest(payload) {
|
|
332
|
+
const errors = [];
|
|
333
|
+
if (typeof payload !== "object" || payload === null) {
|
|
334
|
+
errors.push("Payload must be an object");
|
|
335
|
+
return { valid: false, errors };
|
|
336
|
+
}
|
|
337
|
+
const p = payload;
|
|
338
|
+
if (p.filters !== void 0) {
|
|
339
|
+
if (typeof p.filters !== "object" || p.filters === null) {
|
|
340
|
+
errors.push("filters must be an object");
|
|
341
|
+
} else {
|
|
342
|
+
const filters = p.filters;
|
|
343
|
+
if (filters.activeWithin !== void 0 && typeof filters.activeWithin !== "number") {
|
|
344
|
+
errors.push("filters.activeWithin must be a number");
|
|
345
|
+
}
|
|
346
|
+
if (filters.limit !== void 0 && typeof filters.limit !== "number") {
|
|
347
|
+
errors.push("filters.limit must be a number");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return { valid: errors.length === 0, errors };
|
|
352
|
+
}
|
|
353
|
+
function validatePeerListResponse(payload) {
|
|
354
|
+
const errors = [];
|
|
355
|
+
if (typeof payload !== "object" || payload === null) {
|
|
356
|
+
errors.push("Payload must be an object");
|
|
357
|
+
return { valid: false, errors };
|
|
358
|
+
}
|
|
359
|
+
const p = payload;
|
|
360
|
+
if (!Array.isArray(p.peers)) {
|
|
361
|
+
errors.push("peers must be an array");
|
|
362
|
+
} else {
|
|
363
|
+
p.peers.forEach((peer, index) => {
|
|
364
|
+
if (typeof peer !== "object" || peer === null) {
|
|
365
|
+
errors.push(`peers[${index}] must be an object`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const peerObj = peer;
|
|
369
|
+
if (typeof peerObj.publicKey !== "string") {
|
|
370
|
+
errors.push(`peers[${index}].publicKey must be a string`);
|
|
371
|
+
}
|
|
372
|
+
if (typeof peerObj.lastSeen !== "number") {
|
|
373
|
+
errors.push(`peers[${index}].lastSeen must be a number`);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (typeof p.totalPeers !== "number") {
|
|
378
|
+
errors.push("totalPeers must be a number");
|
|
379
|
+
}
|
|
380
|
+
if (typeof p.relayPublicKey !== "string") {
|
|
381
|
+
errors.push("relayPublicKey must be a string");
|
|
382
|
+
}
|
|
383
|
+
return { valid: errors.length === 0, errors };
|
|
384
|
+
}
|
|
385
|
+
function validatePeerReferral(payload) {
|
|
386
|
+
const errors = [];
|
|
387
|
+
if (typeof payload !== "object" || payload === null) {
|
|
388
|
+
errors.push("Payload must be an object");
|
|
389
|
+
return { valid: false, errors };
|
|
390
|
+
}
|
|
391
|
+
const p = payload;
|
|
392
|
+
if (typeof p.publicKey !== "string") {
|
|
393
|
+
errors.push("publicKey must be a string");
|
|
394
|
+
}
|
|
395
|
+
if (p.endpoint !== void 0 && typeof p.endpoint !== "string") {
|
|
396
|
+
errors.push("endpoint must be a string");
|
|
397
|
+
}
|
|
398
|
+
if (p.comment !== void 0 && typeof p.comment !== "string") {
|
|
399
|
+
errors.push("comment must be a string");
|
|
400
|
+
}
|
|
401
|
+
if (p.trustScore !== void 0 && typeof p.trustScore !== "number") {
|
|
402
|
+
errors.push("trustScore must be a number");
|
|
403
|
+
}
|
|
404
|
+
return { valid: errors.length === 0, errors };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/config.ts
|
|
408
|
+
import { readFileSync, existsSync } from "fs";
|
|
409
|
+
import { readFile } from "fs/promises";
|
|
410
|
+
import { resolve } from "path";
|
|
411
|
+
import { homedir } from "os";
|
|
412
|
+
function getDefaultConfigPath() {
|
|
413
|
+
if (process.env.AGORA_CONFIG) {
|
|
414
|
+
return resolve(process.env.AGORA_CONFIG);
|
|
415
|
+
}
|
|
416
|
+
return resolve(homedir(), ".config", "agora", "config.json");
|
|
417
|
+
}
|
|
418
|
+
function parseConfig(config) {
|
|
419
|
+
const rawIdentity = config.identity;
|
|
420
|
+
if (!rawIdentity?.publicKey || !rawIdentity?.privateKey) {
|
|
421
|
+
throw new Error("Invalid config: missing identity.publicKey or identity.privateKey");
|
|
422
|
+
}
|
|
423
|
+
const identity = {
|
|
424
|
+
publicKey: rawIdentity.publicKey,
|
|
425
|
+
privateKey: rawIdentity.privateKey,
|
|
426
|
+
name: typeof rawIdentity.name === "string" ? rawIdentity.name : void 0
|
|
427
|
+
};
|
|
428
|
+
const peers = {};
|
|
429
|
+
if (config.peers && typeof config.peers === "object") {
|
|
430
|
+
for (const [name, entry] of Object.entries(config.peers)) {
|
|
431
|
+
const peer = entry;
|
|
432
|
+
if (peer && typeof peer.publicKey === "string") {
|
|
433
|
+
peers[name] = {
|
|
434
|
+
publicKey: peer.publicKey,
|
|
435
|
+
url: typeof peer.url === "string" ? peer.url : void 0,
|
|
436
|
+
token: typeof peer.token === "string" ? peer.token : void 0,
|
|
437
|
+
name: typeof peer.name === "string" ? peer.name : void 0
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
let relay;
|
|
443
|
+
const rawRelay = config.relay;
|
|
444
|
+
if (typeof rawRelay === "string") {
|
|
445
|
+
relay = { url: rawRelay, autoConnect: true };
|
|
446
|
+
} else if (rawRelay && typeof rawRelay === "object") {
|
|
447
|
+
const r = rawRelay;
|
|
448
|
+
if (typeof r.url === "string") {
|
|
449
|
+
relay = {
|
|
450
|
+
url: r.url,
|
|
451
|
+
autoConnect: typeof r.autoConnect === "boolean" ? r.autoConnect : true,
|
|
452
|
+
name: typeof r.name === "string" ? r.name : void 0,
|
|
453
|
+
reconnectMaxMs: typeof r.reconnectMaxMs === "number" ? r.reconnectMaxMs : void 0
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
identity,
|
|
459
|
+
peers,
|
|
460
|
+
...relay ? { relay } : {}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function loadAgoraConfig(path) {
|
|
464
|
+
const configPath = path ?? getDefaultConfigPath();
|
|
465
|
+
if (!existsSync(configPath)) {
|
|
466
|
+
throw new Error(`Config file not found at ${configPath}. Run 'npx @rookdaemon/agora init' first.`);
|
|
467
|
+
}
|
|
468
|
+
const content = readFileSync(configPath, "utf-8");
|
|
469
|
+
let config;
|
|
470
|
+
try {
|
|
471
|
+
config = JSON.parse(content);
|
|
472
|
+
} catch {
|
|
473
|
+
throw new Error(`Invalid JSON in config file: ${configPath}`);
|
|
474
|
+
}
|
|
475
|
+
return parseConfig(config);
|
|
476
|
+
}
|
|
477
|
+
async function loadAgoraConfigAsync(path) {
|
|
478
|
+
const configPath = path ?? getDefaultConfigPath();
|
|
479
|
+
let content;
|
|
480
|
+
try {
|
|
481
|
+
content = await readFile(configPath, "utf-8");
|
|
482
|
+
} catch (err) {
|
|
483
|
+
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
484
|
+
if (code === "ENOENT") {
|
|
485
|
+
throw new Error(`Config file not found at ${configPath}. Run 'npx @rookdaemon/agora init' first.`);
|
|
486
|
+
}
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
let config;
|
|
490
|
+
try {
|
|
491
|
+
config = JSON.parse(content);
|
|
492
|
+
} catch {
|
|
493
|
+
throw new Error(`Invalid JSON in config file: ${configPath}`);
|
|
494
|
+
}
|
|
495
|
+
return parseConfig(config);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/relay/message-buffer.ts
|
|
499
|
+
var MAX_MESSAGES_PER_AGENT = 100;
|
|
500
|
+
var MessageBuffer = class {
|
|
501
|
+
buffers = /* @__PURE__ */ new Map();
|
|
502
|
+
/**
|
|
503
|
+
* Add a message to an agent's buffer.
|
|
504
|
+
* Evicts the oldest message if the buffer is full.
|
|
505
|
+
*/
|
|
506
|
+
add(publicKey, message) {
|
|
507
|
+
let queue = this.buffers.get(publicKey);
|
|
508
|
+
if (!queue) {
|
|
509
|
+
queue = [];
|
|
510
|
+
this.buffers.set(publicKey, queue);
|
|
511
|
+
}
|
|
512
|
+
queue.push(message);
|
|
513
|
+
if (queue.length > MAX_MESSAGES_PER_AGENT) {
|
|
514
|
+
queue.shift();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Retrieve messages for an agent, optionally filtering by `since` timestamp.
|
|
519
|
+
* Returns messages with timestamp > since (exclusive).
|
|
520
|
+
*/
|
|
521
|
+
get(publicKey, since) {
|
|
522
|
+
const queue = this.buffers.get(publicKey) ?? [];
|
|
523
|
+
if (since === void 0) {
|
|
524
|
+
return [...queue];
|
|
525
|
+
}
|
|
526
|
+
return queue.filter((m) => m.timestamp > since);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Clear all messages for an agent (after polling without `since`).
|
|
530
|
+
*/
|
|
531
|
+
clear(publicKey) {
|
|
532
|
+
this.buffers.set(publicKey, []);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Remove all state for a disconnected agent.
|
|
536
|
+
*/
|
|
537
|
+
delete(publicKey) {
|
|
538
|
+
this.buffers.delete(publicKey);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// src/relay/jwt-auth.ts
|
|
543
|
+
import jwt from "jsonwebtoken";
|
|
544
|
+
import { randomBytes } from "crypto";
|
|
545
|
+
var revokedJtis = /* @__PURE__ */ new Map();
|
|
546
|
+
function pruneExpiredRevocations() {
|
|
547
|
+
const now = Date.now();
|
|
548
|
+
for (const [jti, expiry] of revokedJtis) {
|
|
549
|
+
if (expiry <= now) {
|
|
550
|
+
revokedJtis.delete(jti);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function getJwtSecret() {
|
|
555
|
+
const secret = process.env.AGORA_RELAY_JWT_SECRET;
|
|
556
|
+
if (!secret) {
|
|
557
|
+
throw new Error(
|
|
558
|
+
"AGORA_RELAY_JWT_SECRET environment variable is required but not set"
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
return secret;
|
|
562
|
+
}
|
|
563
|
+
function getExpirySeconds() {
|
|
564
|
+
const raw = process.env.AGORA_JWT_EXPIRY_SECONDS;
|
|
565
|
+
if (raw) {
|
|
566
|
+
const parsed = parseInt(raw, 10);
|
|
567
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
568
|
+
return parsed;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return 3600;
|
|
572
|
+
}
|
|
573
|
+
function createToken(payload) {
|
|
574
|
+
const secret = getJwtSecret();
|
|
575
|
+
const expirySeconds = getExpirySeconds();
|
|
576
|
+
const jti = `${Date.now()}-${randomBytes(16).toString("hex")}`;
|
|
577
|
+
const token = jwt.sign(
|
|
578
|
+
{ publicKey: payload.publicKey, name: payload.name, jti },
|
|
579
|
+
secret,
|
|
580
|
+
{ expiresIn: expirySeconds }
|
|
581
|
+
);
|
|
582
|
+
const expiresAt = Date.now() + expirySeconds * 1e3;
|
|
583
|
+
return { token, expiresAt };
|
|
584
|
+
}
|
|
585
|
+
function revokeToken(token) {
|
|
586
|
+
try {
|
|
587
|
+
const secret = getJwtSecret();
|
|
588
|
+
const decoded = jwt.verify(token, secret);
|
|
589
|
+
if (decoded.jti) {
|
|
590
|
+
const expiry = decoded.exp ? decoded.exp * 1e3 : Date.now();
|
|
591
|
+
revokedJtis.set(decoded.jti, expiry);
|
|
592
|
+
pruneExpiredRevocations();
|
|
593
|
+
}
|
|
594
|
+
} catch {
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
function requireAuth(req, res, next) {
|
|
598
|
+
const authHeader = req.headers.authorization;
|
|
599
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
600
|
+
res.status(401).json({ error: "Missing or malformed Authorization header" });
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const token = authHeader.slice(7);
|
|
604
|
+
try {
|
|
605
|
+
const secret = getJwtSecret();
|
|
606
|
+
const decoded = jwt.verify(token, secret);
|
|
607
|
+
if (decoded.jti && revokedJtis.has(decoded.jti)) {
|
|
608
|
+
res.status(401).json({ error: "Token has been revoked" });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
req.agent = { publicKey: decoded.publicKey, name: decoded.name };
|
|
612
|
+
next();
|
|
613
|
+
} catch (err) {
|
|
614
|
+
if (err instanceof jwt.TokenExpiredError) {
|
|
615
|
+
res.status(401).json({ error: "Token expired" });
|
|
616
|
+
} else {
|
|
617
|
+
res.status(401).json({ error: "Invalid token" });
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/relay/rest-api.ts
|
|
623
|
+
import { Router } from "express";
|
|
624
|
+
import { rateLimit } from "express-rate-limit";
|
|
625
|
+
var apiRateLimit = rateLimit({
|
|
626
|
+
windowMs: 6e4,
|
|
627
|
+
limit: 60,
|
|
628
|
+
standardHeaders: "draft-7",
|
|
629
|
+
legacyHeaders: false,
|
|
630
|
+
message: { error: "Too many requests \u2014 try again later" }
|
|
631
|
+
});
|
|
632
|
+
function pruneExpiredSessions(sessions, buffer) {
|
|
633
|
+
const now = Date.now();
|
|
634
|
+
for (const [publicKey, session] of sessions) {
|
|
635
|
+
if (session.expiresAt <= now) {
|
|
636
|
+
sessions.delete(publicKey);
|
|
637
|
+
buffer.delete(publicKey);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv) {
|
|
642
|
+
const router = Router();
|
|
643
|
+
router.use(apiRateLimit);
|
|
644
|
+
relay.on("message-relayed", (from, to, envelope) => {
|
|
645
|
+
if (!sessions.has(to)) return;
|
|
646
|
+
const agentMap = relay.getAgents();
|
|
647
|
+
const senderAgent = agentMap.get(from);
|
|
648
|
+
const env = envelope;
|
|
649
|
+
const msg = {
|
|
650
|
+
id: env.id,
|
|
651
|
+
from,
|
|
652
|
+
fromName: senderAgent?.name,
|
|
653
|
+
type: env.type,
|
|
654
|
+
payload: env.payload,
|
|
655
|
+
timestamp: env.timestamp,
|
|
656
|
+
inReplyTo: env.inReplyTo
|
|
657
|
+
};
|
|
658
|
+
buffer.add(to, msg);
|
|
659
|
+
});
|
|
660
|
+
router.post("/v1/register", async (req, res) => {
|
|
661
|
+
const { publicKey, privateKey, name, metadata } = req.body;
|
|
662
|
+
if (!publicKey || typeof publicKey !== "string") {
|
|
663
|
+
res.status(400).json({ error: "publicKey is required" });
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (!privateKey || typeof privateKey !== "string") {
|
|
667
|
+
res.status(400).json({ error: "privateKey is required" });
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const testEnvelope = createEnv(
|
|
671
|
+
"announce",
|
|
672
|
+
publicKey,
|
|
673
|
+
privateKey,
|
|
674
|
+
{ challenge: "register" },
|
|
675
|
+
Date.now()
|
|
676
|
+
);
|
|
677
|
+
const verification = verifyEnv(testEnvelope);
|
|
678
|
+
if (!verification.valid) {
|
|
679
|
+
res.status(400).json({ error: "Key pair verification failed: " + verification.reason });
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const { token, expiresAt } = createToken({ publicKey, name });
|
|
683
|
+
pruneExpiredSessions(sessions, buffer);
|
|
684
|
+
const session = {
|
|
685
|
+
publicKey,
|
|
686
|
+
privateKey,
|
|
687
|
+
name,
|
|
688
|
+
metadata,
|
|
689
|
+
registeredAt: Date.now(),
|
|
690
|
+
expiresAt,
|
|
691
|
+
token
|
|
692
|
+
};
|
|
693
|
+
sessions.set(publicKey, session);
|
|
694
|
+
const wsAgents = relay.getAgents();
|
|
695
|
+
const peers = [];
|
|
696
|
+
for (const agent of wsAgents.values()) {
|
|
697
|
+
if (agent.publicKey !== publicKey) {
|
|
698
|
+
peers.push({
|
|
699
|
+
publicKey: agent.publicKey,
|
|
700
|
+
name: agent.name,
|
|
701
|
+
lastSeen: agent.lastSeen
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
for (const s of sessions.values()) {
|
|
706
|
+
if (s.publicKey !== publicKey && !wsAgents.has(s.publicKey)) {
|
|
707
|
+
peers.push({
|
|
708
|
+
publicKey: s.publicKey,
|
|
709
|
+
name: s.name,
|
|
710
|
+
lastSeen: s.registeredAt
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
res.json({ token, expiresAt, peers });
|
|
715
|
+
});
|
|
716
|
+
router.post(
|
|
717
|
+
"/v1/send",
|
|
718
|
+
requireAuth,
|
|
719
|
+
async (req, res) => {
|
|
720
|
+
const { to, type, payload, inReplyTo } = req.body;
|
|
721
|
+
if (!to || typeof to !== "string") {
|
|
722
|
+
res.status(400).json({ error: "to is required" });
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (!type || typeof type !== "string") {
|
|
726
|
+
res.status(400).json({ error: "type is required" });
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (payload === void 0) {
|
|
730
|
+
res.status(400).json({ error: "payload is required" });
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const senderPublicKey = req.agent.publicKey;
|
|
734
|
+
const session = sessions.get(senderPublicKey);
|
|
735
|
+
if (!session) {
|
|
736
|
+
res.status(401).json({ error: "Session not found \u2014 please re-register" });
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const envelope = createEnv(
|
|
740
|
+
type,
|
|
741
|
+
senderPublicKey,
|
|
742
|
+
session.privateKey,
|
|
743
|
+
payload,
|
|
744
|
+
Date.now(),
|
|
745
|
+
inReplyTo
|
|
746
|
+
);
|
|
747
|
+
const wsAgents = relay.getAgents();
|
|
748
|
+
const wsRecipient = wsAgents.get(to);
|
|
749
|
+
if (wsRecipient && wsRecipient.socket) {
|
|
750
|
+
const ws = wsRecipient.socket;
|
|
751
|
+
const OPEN = 1;
|
|
752
|
+
if (ws.readyState !== OPEN) {
|
|
753
|
+
res.status(503).json({ error: "Recipient connection is not open" });
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
try {
|
|
757
|
+
const relayMsg = JSON.stringify({
|
|
758
|
+
type: "message",
|
|
759
|
+
from: senderPublicKey,
|
|
760
|
+
name: session.name,
|
|
761
|
+
envelope
|
|
762
|
+
});
|
|
763
|
+
ws.send(relayMsg);
|
|
764
|
+
res.json({ ok: true, envelopeId: envelope.id });
|
|
765
|
+
return;
|
|
766
|
+
} catch (err) {
|
|
767
|
+
res.status(500).json({
|
|
768
|
+
error: "Failed to deliver message: " + (err instanceof Error ? err.message : String(err))
|
|
769
|
+
});
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const restRecipient = sessions.get(to);
|
|
774
|
+
if (restRecipient) {
|
|
775
|
+
const senderAgent = wsAgents.get(senderPublicKey);
|
|
776
|
+
const msg = {
|
|
777
|
+
id: envelope.id,
|
|
778
|
+
from: senderPublicKey,
|
|
779
|
+
fromName: session.name ?? senderAgent?.name,
|
|
780
|
+
type: envelope.type,
|
|
781
|
+
payload: envelope.payload,
|
|
782
|
+
timestamp: envelope.timestamp,
|
|
783
|
+
inReplyTo: envelope.inReplyTo
|
|
784
|
+
};
|
|
785
|
+
buffer.add(to, msg);
|
|
786
|
+
res.json({ ok: true, envelopeId: envelope.id });
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
res.status(404).json({ error: "Recipient not connected" });
|
|
790
|
+
}
|
|
791
|
+
);
|
|
792
|
+
router.get(
|
|
793
|
+
"/v1/peers",
|
|
794
|
+
requireAuth,
|
|
795
|
+
(req, res) => {
|
|
796
|
+
const callerPublicKey = req.agent.publicKey;
|
|
797
|
+
const wsAgents = relay.getAgents();
|
|
798
|
+
const peerList = [];
|
|
799
|
+
for (const agent of wsAgents.values()) {
|
|
800
|
+
if (agent.publicKey !== callerPublicKey) {
|
|
801
|
+
peerList.push({
|
|
802
|
+
publicKey: agent.publicKey,
|
|
803
|
+
name: agent.name,
|
|
804
|
+
lastSeen: agent.lastSeen,
|
|
805
|
+
metadata: agent.metadata
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
for (const s of sessions.values()) {
|
|
810
|
+
if (s.publicKey !== callerPublicKey && !wsAgents.has(s.publicKey)) {
|
|
811
|
+
peerList.push({
|
|
812
|
+
publicKey: s.publicKey,
|
|
813
|
+
name: s.name,
|
|
814
|
+
lastSeen: s.registeredAt,
|
|
815
|
+
metadata: s.metadata
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
res.json({ peers: peerList });
|
|
820
|
+
}
|
|
821
|
+
);
|
|
822
|
+
router.get(
|
|
823
|
+
"/v1/messages",
|
|
824
|
+
requireAuth,
|
|
825
|
+
(req, res) => {
|
|
826
|
+
const publicKey = req.agent.publicKey;
|
|
827
|
+
const sinceRaw = req.query.since;
|
|
828
|
+
const limitRaw = req.query.limit;
|
|
829
|
+
const since = sinceRaw ? parseInt(sinceRaw, 10) : void 0;
|
|
830
|
+
const limit = Math.min(limitRaw ? parseInt(limitRaw, 10) : 50, 100);
|
|
831
|
+
let messages = buffer.get(publicKey, since);
|
|
832
|
+
const hasMore = messages.length > limit;
|
|
833
|
+
if (hasMore) {
|
|
834
|
+
messages = messages.slice(0, limit);
|
|
835
|
+
}
|
|
836
|
+
if (since === void 0) {
|
|
837
|
+
buffer.clear(publicKey);
|
|
838
|
+
}
|
|
839
|
+
res.json({ messages, hasMore });
|
|
840
|
+
}
|
|
841
|
+
);
|
|
842
|
+
router.delete(
|
|
843
|
+
"/v1/disconnect",
|
|
844
|
+
requireAuth,
|
|
845
|
+
(req, res) => {
|
|
846
|
+
const publicKey = req.agent.publicKey;
|
|
847
|
+
const authHeader = req.headers.authorization;
|
|
848
|
+
const token = authHeader.slice(7);
|
|
849
|
+
revokeToken(token);
|
|
850
|
+
sessions.delete(publicKey);
|
|
851
|
+
buffer.delete(publicKey);
|
|
852
|
+
res.json({ ok: true });
|
|
853
|
+
}
|
|
854
|
+
);
|
|
855
|
+
return router;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/relay/run-relay.ts
|
|
859
|
+
import http from "http";
|
|
860
|
+
import express from "express";
|
|
861
|
+
var createEnvelopeForRest = (type, sender, privateKey, payload, timestamp, inReplyTo) => createEnvelope(
|
|
862
|
+
type,
|
|
863
|
+
sender,
|
|
864
|
+
privateKey,
|
|
865
|
+
payload,
|
|
866
|
+
timestamp ?? Date.now(),
|
|
867
|
+
inReplyTo
|
|
868
|
+
);
|
|
869
|
+
async function runRelay(options = {}) {
|
|
870
|
+
const wsPort = options.wsPort ?? parseInt(process.env.PORT ?? "3001", 10);
|
|
871
|
+
const enableRest = options.enableRest ?? (typeof process.env.AGORA_RELAY_JWT_SECRET === "string" && process.env.AGORA_RELAY_JWT_SECRET.length > 0);
|
|
872
|
+
const relay = new RelayServer(options.relayOptions);
|
|
873
|
+
await relay.start(wsPort);
|
|
874
|
+
if (!enableRest) {
|
|
875
|
+
return { relay };
|
|
876
|
+
}
|
|
877
|
+
if (!process.env.AGORA_RELAY_JWT_SECRET) {
|
|
878
|
+
await relay.stop();
|
|
879
|
+
throw new Error(
|
|
880
|
+
"AGORA_RELAY_JWT_SECRET environment variable is required when REST API is enabled"
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
const restPort = options.restPort ?? wsPort + 1;
|
|
884
|
+
const messageBuffer = new MessageBuffer();
|
|
885
|
+
const restSessions = /* @__PURE__ */ new Map();
|
|
886
|
+
const app = express();
|
|
887
|
+
app.use(express.json());
|
|
888
|
+
const verifyForRest = (envelope) => verifyEnvelope(envelope);
|
|
889
|
+
const router = createRestRouter(
|
|
890
|
+
relay,
|
|
891
|
+
messageBuffer,
|
|
892
|
+
restSessions,
|
|
893
|
+
createEnvelopeForRest,
|
|
894
|
+
verifyForRest
|
|
895
|
+
);
|
|
896
|
+
app.use(router);
|
|
897
|
+
app.use((_req, res) => {
|
|
898
|
+
res.status(404).json({ error: "Not found" });
|
|
899
|
+
});
|
|
900
|
+
const httpServer = http.createServer(app);
|
|
901
|
+
await new Promise((resolve2, reject) => {
|
|
902
|
+
httpServer.listen(restPort, () => resolve2());
|
|
903
|
+
httpServer.on("error", reject);
|
|
904
|
+
});
|
|
905
|
+
return { relay, httpServer };
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/service.ts
|
|
909
|
+
var AgoraService = class {
|
|
910
|
+
config;
|
|
911
|
+
relayClient = null;
|
|
912
|
+
relayMessageHandler = null;
|
|
913
|
+
relayMessageHandlerWithName = null;
|
|
914
|
+
logger;
|
|
915
|
+
relayClientFactory;
|
|
916
|
+
constructor(config, logger, relayClientFactory) {
|
|
917
|
+
this.config = config;
|
|
918
|
+
this.logger = logger ?? null;
|
|
919
|
+
this.relayClientFactory = relayClientFactory ?? null;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Send a signed message to a named peer.
|
|
923
|
+
* Tries HTTP webhook first; falls back to relay if HTTP is unavailable.
|
|
924
|
+
*/
|
|
925
|
+
async sendMessage(options) {
|
|
926
|
+
const peer = this.config.peers.get(options.peerName);
|
|
927
|
+
if (!peer) {
|
|
928
|
+
return {
|
|
929
|
+
ok: false,
|
|
930
|
+
status: 0,
|
|
931
|
+
error: `Unknown peer: ${options.peerName}`
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
if (peer.url) {
|
|
935
|
+
const transportConfig = {
|
|
936
|
+
identity: {
|
|
937
|
+
publicKey: this.config.identity.publicKey,
|
|
938
|
+
privateKey: this.config.identity.privateKey
|
|
939
|
+
},
|
|
940
|
+
peers: /* @__PURE__ */ new Map([[peer.publicKey, peer]])
|
|
941
|
+
};
|
|
942
|
+
const httpResult = await sendToPeer(
|
|
943
|
+
transportConfig,
|
|
944
|
+
peer.publicKey,
|
|
945
|
+
options.type,
|
|
946
|
+
options.payload,
|
|
947
|
+
options.inReplyTo
|
|
948
|
+
);
|
|
949
|
+
if (httpResult.ok) {
|
|
950
|
+
return httpResult;
|
|
951
|
+
}
|
|
952
|
+
this.logger?.debug(`HTTP send to ${options.peerName} failed: ${httpResult.error}`);
|
|
953
|
+
}
|
|
954
|
+
if (this.relayClient?.connected() && this.config.relay) {
|
|
955
|
+
const relayResult = await sendViaRelay(
|
|
956
|
+
{
|
|
957
|
+
identity: this.config.identity,
|
|
958
|
+
relayUrl: this.config.relay.url,
|
|
959
|
+
relayClient: this.relayClient
|
|
960
|
+
},
|
|
961
|
+
peer.publicKey,
|
|
962
|
+
options.type,
|
|
963
|
+
options.payload,
|
|
964
|
+
options.inReplyTo
|
|
965
|
+
);
|
|
966
|
+
return {
|
|
967
|
+
ok: relayResult.ok,
|
|
968
|
+
status: 0,
|
|
969
|
+
error: relayResult.error
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
ok: false,
|
|
974
|
+
status: 0,
|
|
975
|
+
error: peer.url ? `HTTP send failed and relay not available for peer: ${options.peerName}` : `No webhook URL and relay not available for peer: ${options.peerName}`
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Decode and verify an inbound envelope from a webhook message.
|
|
980
|
+
*/
|
|
981
|
+
async decodeInbound(message) {
|
|
982
|
+
const peersByPubKey = /* @__PURE__ */ new Map();
|
|
983
|
+
for (const peer of this.config.peers.values()) {
|
|
984
|
+
peersByPubKey.set(peer.publicKey, peer);
|
|
985
|
+
}
|
|
986
|
+
const result = decodeInboundEnvelope(message, peersByPubKey);
|
|
987
|
+
if (result.ok) {
|
|
988
|
+
return { ok: true, envelope: result.envelope };
|
|
989
|
+
}
|
|
990
|
+
return { ok: false, reason: result.reason };
|
|
991
|
+
}
|
|
992
|
+
getPeers() {
|
|
993
|
+
return Array.from(this.config.peers.keys());
|
|
994
|
+
}
|
|
995
|
+
getPeerConfig(name) {
|
|
996
|
+
return this.config.peers.get(name);
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Connect to the relay server.
|
|
1000
|
+
*/
|
|
1001
|
+
async connectRelay(url) {
|
|
1002
|
+
if (this.relayClient) {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const maxReconnectDelay = this.config.relay?.reconnectMaxMs ?? 3e5;
|
|
1006
|
+
let name = this.config.identity.name ?? this.config.relay?.name;
|
|
1007
|
+
if (name && name === shortKey(this.config.identity.publicKey)) {
|
|
1008
|
+
name = void 0;
|
|
1009
|
+
}
|
|
1010
|
+
const opts = {
|
|
1011
|
+
relayUrl: url,
|
|
1012
|
+
publicKey: this.config.identity.publicKey,
|
|
1013
|
+
privateKey: this.config.identity.privateKey,
|
|
1014
|
+
name,
|
|
1015
|
+
pingInterval: 3e4,
|
|
1016
|
+
maxReconnectDelay
|
|
1017
|
+
};
|
|
1018
|
+
if (this.relayClientFactory) {
|
|
1019
|
+
this.relayClient = this.relayClientFactory(opts);
|
|
1020
|
+
} else {
|
|
1021
|
+
this.relayClient = new RelayClient(opts);
|
|
1022
|
+
}
|
|
1023
|
+
this.relayClient.on("error", (error) => {
|
|
1024
|
+
this.logger?.debug(`Agora relay error: ${error.message}`);
|
|
1025
|
+
});
|
|
1026
|
+
this.relayClient.on("message", (envelope, from, fromName) => {
|
|
1027
|
+
if (this.relayMessageHandlerWithName) {
|
|
1028
|
+
this.relayMessageHandlerWithName(envelope, from, fromName);
|
|
1029
|
+
} else if (this.relayMessageHandler) {
|
|
1030
|
+
this.relayMessageHandler(envelope);
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
try {
|
|
1034
|
+
await this.relayClient.connect();
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1037
|
+
this.logger?.debug(`Agora relay connect failed (${url}): ${message}`);
|
|
1038
|
+
this.relayClient = null;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
setRelayMessageHandler(handler) {
|
|
1042
|
+
this.relayMessageHandler = handler;
|
|
1043
|
+
this.relayMessageHandlerWithName = null;
|
|
1044
|
+
}
|
|
1045
|
+
setRelayMessageHandlerWithName(handler) {
|
|
1046
|
+
this.relayMessageHandlerWithName = handler;
|
|
1047
|
+
this.relayMessageHandler = null;
|
|
1048
|
+
}
|
|
1049
|
+
async disconnectRelay() {
|
|
1050
|
+
if (this.relayClient) {
|
|
1051
|
+
this.relayClient.disconnect();
|
|
1052
|
+
this.relayClient = null;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
isRelayConnected() {
|
|
1056
|
+
return this.relayClient?.connected() ?? false;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Load Agora configuration and return service config (peers as Map).
|
|
1060
|
+
*/
|
|
1061
|
+
static async loadConfig(path) {
|
|
1062
|
+
const configPath = path ?? getDefaultConfigPath();
|
|
1063
|
+
const loaded = await loadAgoraConfigAsync(configPath);
|
|
1064
|
+
const peers = /* @__PURE__ */ new Map();
|
|
1065
|
+
for (const [name, p] of Object.entries(loaded.peers)) {
|
|
1066
|
+
peers.set(name, {
|
|
1067
|
+
publicKey: p.publicKey,
|
|
1068
|
+
url: p.url,
|
|
1069
|
+
token: p.token
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
identity: loaded.identity,
|
|
1074
|
+
peers,
|
|
1075
|
+
relay: loaded.relay
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
export {
|
|
1080
|
+
AgoraService,
|
|
1081
|
+
DEFAULT_BOOTSTRAP_RELAYS,
|
|
1082
|
+
DiscoveryService,
|
|
1083
|
+
MessageBuffer,
|
|
1084
|
+
MessageStore,
|
|
1085
|
+
PeerDiscoveryService,
|
|
1086
|
+
PeerStore,
|
|
1087
|
+
RelayClient,
|
|
1088
|
+
RelayServer,
|
|
1089
|
+
ReputationStore,
|
|
1090
|
+
canonicalize,
|
|
1091
|
+
computeAllTrustScores,
|
|
1092
|
+
computeId,
|
|
1093
|
+
computeTrustScore,
|
|
1094
|
+
computeTrustScores,
|
|
1095
|
+
createCapability,
|
|
1096
|
+
createCommit,
|
|
1097
|
+
createEnvelope,
|
|
1098
|
+
createRestRouter,
|
|
1099
|
+
createReveal,
|
|
1100
|
+
createToken,
|
|
1101
|
+
createVerification,
|
|
1102
|
+
decay,
|
|
1103
|
+
decodeInboundEnvelope,
|
|
1104
|
+
exportKeyPair,
|
|
1105
|
+
formatDisplayName,
|
|
1106
|
+
generateKeyPair,
|
|
1107
|
+
getDefaultBootstrapRelay,
|
|
1108
|
+
getDefaultConfigPath,
|
|
1109
|
+
hashPrediction,
|
|
1110
|
+
importKeyPair,
|
|
1111
|
+
initPeerConfig,
|
|
1112
|
+
loadAgoraConfig,
|
|
1113
|
+
loadAgoraConfigAsync,
|
|
1114
|
+
loadPeerConfig,
|
|
1115
|
+
parseBootstrapRelay,
|
|
1116
|
+
requireAuth,
|
|
1117
|
+
resolveBroadcastName,
|
|
1118
|
+
revokeToken,
|
|
1119
|
+
runRelay,
|
|
1120
|
+
savePeerConfig,
|
|
1121
|
+
sendToPeer,
|
|
1122
|
+
shortKey,
|
|
1123
|
+
signMessage,
|
|
1124
|
+
validateCapability,
|
|
1125
|
+
validateCommitRecord,
|
|
1126
|
+
validatePeerListRequest,
|
|
1127
|
+
validatePeerListResponse,
|
|
1128
|
+
validatePeerReferral,
|
|
1129
|
+
validateRevealRecord,
|
|
1130
|
+
validateVerificationRecord,
|
|
1131
|
+
verifyEnvelope,
|
|
1132
|
+
verifyReveal,
|
|
1133
|
+
verifySignature,
|
|
1134
|
+
verifyVerificationSignature
|
|
1135
|
+
};
|
|
30
1136
|
//# sourceMappingURL=index.js.map
|