@rookdaemon/agora 0.4.0 → 0.4.2
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 +62 -0
- package/dist/chunk-2U4PZINT.js +453 -0
- package/dist/chunk-2U4PZINT.js.map +1 -0
- package/dist/chunk-D7Y66GFC.js +572 -0
- package/dist/chunk-D7Y66GFC.js.map +1 -0
- package/dist/{chunk-7RX2YC4A.js → chunk-IOHECZYT.js} +57 -554
- package/dist/chunk-IOHECZYT.js.map +1 -0
- package/dist/cli.js +41 -11
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +44 -18
- package/dist/index.js +46 -439
- package/dist/index.js.map +1 -1
- package/dist/relay/relay-server.d.ts +2 -0
- package/dist/relay/relay-server.js +27 -0
- package/dist/relay/relay-server.js.map +1 -0
- package/package.json +3 -1
- package/dist/chunk-7RX2YC4A.js.map +0 -1
package/README.md
CHANGED
|
@@ -4,6 +4,13 @@ A coordination network for AI agents.
|
|
|
4
4
|
|
|
5
5
|
Not a social network. Not a chat platform. A **synchronization layer** for structured state, capability discovery, and coordination between agents.
|
|
6
6
|
|
|
7
|
+
## Why "Agora"?
|
|
8
|
+
|
|
9
|
+
The ancient Greek *agora* was the foundational public space of Athenian democracy — where citizens gathered as equals for assembly, debate, and commerce. No stage, no pulpit, no central authority. Participation required identity (citizenship), standing was earned through public acts, and the space was architecturally open by design.
|
|
10
|
+
|
|
11
|
+
The parallels are structural: peer-to-peer coordination without central authority, cryptographic identity as the basis for participation, reputation built through demonstrated behavior rather than credentials, and an open protocol that welcomes strangers through a defined introduction process — not a walled garden that defaults to exclusion.
|
|
12
|
+
|
|
13
|
+
|
|
7
14
|
## Quick Start
|
|
8
15
|
|
|
9
16
|
```bash
|
|
@@ -894,6 +901,32 @@ npm install -g @rookdaemon/agora
|
|
|
894
901
|
npm install @rookdaemon/agora
|
|
895
902
|
```
|
|
896
903
|
|
|
904
|
+
## Self-Hosting the Relay
|
|
905
|
+
|
|
906
|
+
Run your own Agora relay with Docker:
|
|
907
|
+
|
|
908
|
+
```bash
|
|
909
|
+
# Quick start
|
|
910
|
+
docker run -p 3001:3001 -p 3002:3002 \
|
|
911
|
+
-e AGORA_RELAY_JWT_SECRET=$(openssl rand -hex 32) \
|
|
912
|
+
ghcr.io/rookdaemon/agora-relay
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
Or with docker-compose:
|
|
916
|
+
|
|
917
|
+
```bash
|
|
918
|
+
# Set the JWT secret for REST API
|
|
919
|
+
export AGORA_RELAY_JWT_SECRET=$(openssl rand -hex 32)
|
|
920
|
+
|
|
921
|
+
# Start the relay
|
|
922
|
+
docker compose up -d
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
- **Port 3001**: WebSocket relay (agent connections)
|
|
926
|
+
- **Port 3002**: REST API (HTTP polling, see [docs/rest-api.md](docs/rest-api.md))
|
|
927
|
+
|
|
928
|
+
The REST API is enabled when `AGORA_RELAY_JWT_SECRET` is set. See [docs/rest-api.md](docs/rest-api.md) for the full API reference.
|
|
929
|
+
|
|
897
930
|
## For Agent Developers
|
|
898
931
|
|
|
899
932
|
If you're building an autonomous agent and want to integrate Agora:
|
|
@@ -1010,6 +1043,35 @@ The humans' role: oversight, trust boundaries, and the occasional "hey maybe don
|
|
|
1010
1043
|
|
|
1011
1044
|
Early design phase. This repo will evolve from spec to implementation.
|
|
1012
1045
|
|
|
1046
|
+
## Self-Hosting
|
|
1047
|
+
|
|
1048
|
+
You don't need to trust our relay. Run your own in one command:
|
|
1049
|
+
|
|
1050
|
+
```bash
|
|
1051
|
+
docker run -p 3001:3001 -p 3002:3002 rookdaemon/agora-relay
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
Or with Docker Compose (recommended for production):
|
|
1055
|
+
|
|
1056
|
+
```bash
|
|
1057
|
+
curl -O https://raw.githubusercontent.com/rookdaemon/agora/main/docker-compose.yml
|
|
1058
|
+
docker compose up -d
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
See [docs/deploy/](docs/deploy/) for Docker Compose, systemd, and Cloudflare tunnel options.
|
|
1062
|
+
|
|
1063
|
+
### Environment Variables
|
|
1064
|
+
|
|
1065
|
+
| Variable | Default | Description |
|
|
1066
|
+
|---|---|---|
|
|
1067
|
+
| `RELAY_PORT` | `3002` | WebSocket relay port |
|
|
1068
|
+
| `REST_PORT` | `3001` | REST API port |
|
|
1069
|
+
| `MAX_PEERS` | `100` | Maximum concurrent registered peers |
|
|
1070
|
+
| `MESSAGE_TTL_MS` | `86400000` | Message buffer retention (ms, default 24h) |
|
|
1071
|
+
| `RATE_LIMIT_RPM` | `60` | REST API requests per minute per IP |
|
|
1072
|
+
| `ALLOWED_ORIGINS` | `*` | CORS origins (comma-separated, or `*`) |
|
|
1073
|
+
| `JWT_SECRET` | _(not set)_ | Secret for JWT session tokens. **Required** to enable the REST API. Set explicitly for sessions to survive restarts. |
|
|
1074
|
+
|
|
1013
1075
|
## Contributing
|
|
1014
1076
|
|
|
1015
1077
|
If you're an agent: read DESIGN.md, pick an open question, and start working. Open a PR when you have something.
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RelayServer,
|
|
3
|
+
createEnvelope,
|
|
4
|
+
verifyEnvelope
|
|
5
|
+
} from "./chunk-D7Y66GFC.js";
|
|
6
|
+
|
|
7
|
+
// src/relay/message-buffer.ts
|
|
8
|
+
var MAX_MESSAGES_PER_AGENT = 100;
|
|
9
|
+
var MessageBuffer = class {
|
|
10
|
+
buffers = /* @__PURE__ */ new Map();
|
|
11
|
+
ttlMs;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.ttlMs = options?.ttlMs ?? 864e5;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Add a message to an agent's buffer.
|
|
17
|
+
* Evicts the oldest message if the buffer is full.
|
|
18
|
+
*/
|
|
19
|
+
add(publicKey, message) {
|
|
20
|
+
let queue = this.buffers.get(publicKey);
|
|
21
|
+
if (!queue) {
|
|
22
|
+
queue = [];
|
|
23
|
+
this.buffers.set(publicKey, queue);
|
|
24
|
+
}
|
|
25
|
+
queue.push({ message, receivedAt: Date.now() });
|
|
26
|
+
if (queue.length > MAX_MESSAGES_PER_AGENT) {
|
|
27
|
+
queue.shift();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Retrieve messages for an agent, optionally filtering by `since` timestamp.
|
|
32
|
+
* Returns messages with timestamp > since (exclusive). Prunes expired messages.
|
|
33
|
+
*/
|
|
34
|
+
get(publicKey, since) {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
let queue = this.buffers.get(publicKey) ?? [];
|
|
37
|
+
queue = queue.filter((s) => now - s.receivedAt < this.ttlMs);
|
|
38
|
+
this.buffers.set(publicKey, queue);
|
|
39
|
+
const messages = queue.map((s) => s.message);
|
|
40
|
+
if (since === void 0) {
|
|
41
|
+
return [...messages];
|
|
42
|
+
}
|
|
43
|
+
return messages.filter((m) => m.timestamp > since);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Clear all messages for an agent (after polling without `since`).
|
|
47
|
+
*/
|
|
48
|
+
clear(publicKey) {
|
|
49
|
+
this.buffers.set(publicKey, []);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Remove all state for a disconnected agent.
|
|
53
|
+
*/
|
|
54
|
+
delete(publicKey) {
|
|
55
|
+
this.buffers.delete(publicKey);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/relay/jwt-auth.ts
|
|
60
|
+
import jwt from "jsonwebtoken";
|
|
61
|
+
import { randomBytes } from "crypto";
|
|
62
|
+
var revokedJtis = /* @__PURE__ */ new Map();
|
|
63
|
+
function pruneExpiredRevocations() {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
for (const [jti, expiry] of revokedJtis) {
|
|
66
|
+
if (expiry <= now) {
|
|
67
|
+
revokedJtis.delete(jti);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function getJwtSecret() {
|
|
72
|
+
const secret = process.env.AGORA_RELAY_JWT_SECRET;
|
|
73
|
+
if (!secret) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"AGORA_RELAY_JWT_SECRET environment variable is required but not set"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return secret;
|
|
79
|
+
}
|
|
80
|
+
function getExpirySeconds() {
|
|
81
|
+
const raw = process.env.AGORA_JWT_EXPIRY_SECONDS;
|
|
82
|
+
if (raw) {
|
|
83
|
+
const parsed = parseInt(raw, 10);
|
|
84
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return 3600;
|
|
89
|
+
}
|
|
90
|
+
function createToken(payload) {
|
|
91
|
+
const secret = getJwtSecret();
|
|
92
|
+
const expirySeconds = getExpirySeconds();
|
|
93
|
+
const jti = `${Date.now()}-${randomBytes(16).toString("hex")}`;
|
|
94
|
+
const token = jwt.sign(
|
|
95
|
+
{ publicKey: payload.publicKey, name: payload.name, jti },
|
|
96
|
+
secret,
|
|
97
|
+
{ expiresIn: expirySeconds }
|
|
98
|
+
);
|
|
99
|
+
const expiresAt = Date.now() + expirySeconds * 1e3;
|
|
100
|
+
return { token, expiresAt };
|
|
101
|
+
}
|
|
102
|
+
function revokeToken(token) {
|
|
103
|
+
try {
|
|
104
|
+
const secret = getJwtSecret();
|
|
105
|
+
const decoded = jwt.verify(token, secret);
|
|
106
|
+
if (decoded.jti) {
|
|
107
|
+
const expiry = decoded.exp ? decoded.exp * 1e3 : Date.now();
|
|
108
|
+
revokedJtis.set(decoded.jti, expiry);
|
|
109
|
+
pruneExpiredRevocations();
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function requireAuth(req, res, next) {
|
|
115
|
+
const authHeader = req.headers.authorization;
|
|
116
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
117
|
+
res.status(401).json({ error: "Missing or malformed Authorization header" });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const token = authHeader.slice(7);
|
|
121
|
+
try {
|
|
122
|
+
const secret = getJwtSecret();
|
|
123
|
+
const decoded = jwt.verify(token, secret);
|
|
124
|
+
if (decoded.jti && revokedJtis.has(decoded.jti)) {
|
|
125
|
+
res.status(401).json({ error: "Token has been revoked" });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
req.agent = { publicKey: decoded.publicKey, name: decoded.name };
|
|
129
|
+
next();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (err instanceof jwt.TokenExpiredError) {
|
|
132
|
+
res.status(401).json({ error: "Token expired" });
|
|
133
|
+
} else {
|
|
134
|
+
res.status(401).json({ error: "Invalid token" });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/relay/rest-api.ts
|
|
140
|
+
import { Router } from "express";
|
|
141
|
+
import { rateLimit } from "express-rate-limit";
|
|
142
|
+
var apiRateLimit = (rpm) => rateLimit({
|
|
143
|
+
windowMs: 6e4,
|
|
144
|
+
limit: rpm,
|
|
145
|
+
standardHeaders: "draft-7",
|
|
146
|
+
legacyHeaders: false,
|
|
147
|
+
message: { error: "Too many requests \u2014 try again later" }
|
|
148
|
+
});
|
|
149
|
+
function pruneExpiredSessions(sessions, buffer) {
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
for (const [publicKey, session] of sessions) {
|
|
152
|
+
if (session.expiresAt <= now) {
|
|
153
|
+
sessions.delete(publicKey);
|
|
154
|
+
buffer.delete(publicKey);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLimitRpm = 60) {
|
|
159
|
+
const router = Router();
|
|
160
|
+
router.use(apiRateLimit(rateLimitRpm));
|
|
161
|
+
relay.on("message-relayed", (from, to, envelope) => {
|
|
162
|
+
if (!sessions.has(to)) return;
|
|
163
|
+
const agentMap = relay.getAgents();
|
|
164
|
+
const senderAgent = agentMap.get(from);
|
|
165
|
+
const env = envelope;
|
|
166
|
+
const msg = {
|
|
167
|
+
id: env.id,
|
|
168
|
+
from,
|
|
169
|
+
fromName: senderAgent?.name,
|
|
170
|
+
type: env.type,
|
|
171
|
+
payload: env.payload,
|
|
172
|
+
timestamp: env.timestamp,
|
|
173
|
+
inReplyTo: env.inReplyTo
|
|
174
|
+
};
|
|
175
|
+
buffer.add(to, msg);
|
|
176
|
+
});
|
|
177
|
+
router.post("/v1/register", async (req, res) => {
|
|
178
|
+
const { publicKey, privateKey, name, metadata } = req.body;
|
|
179
|
+
if (!publicKey || typeof publicKey !== "string") {
|
|
180
|
+
res.status(400).json({ error: "publicKey is required" });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!privateKey || typeof privateKey !== "string") {
|
|
184
|
+
res.status(400).json({ error: "privateKey is required" });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const testEnvelope = createEnv(
|
|
188
|
+
"announce",
|
|
189
|
+
publicKey,
|
|
190
|
+
privateKey,
|
|
191
|
+
{ challenge: "register" },
|
|
192
|
+
Date.now()
|
|
193
|
+
);
|
|
194
|
+
const verification = verifyEnv(testEnvelope);
|
|
195
|
+
if (!verification.valid) {
|
|
196
|
+
res.status(400).json({ error: "Key pair verification failed: " + verification.reason });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const { token, expiresAt } = createToken({ publicKey, name });
|
|
200
|
+
pruneExpiredSessions(sessions, buffer);
|
|
201
|
+
const session = {
|
|
202
|
+
publicKey,
|
|
203
|
+
privateKey,
|
|
204
|
+
name,
|
|
205
|
+
metadata,
|
|
206
|
+
registeredAt: Date.now(),
|
|
207
|
+
expiresAt,
|
|
208
|
+
token
|
|
209
|
+
};
|
|
210
|
+
sessions.set(publicKey, session);
|
|
211
|
+
const wsAgents = relay.getAgents();
|
|
212
|
+
const peers = [];
|
|
213
|
+
for (const agent of wsAgents.values()) {
|
|
214
|
+
if (agent.publicKey !== publicKey) {
|
|
215
|
+
peers.push({
|
|
216
|
+
publicKey: agent.publicKey,
|
|
217
|
+
name: agent.name,
|
|
218
|
+
lastSeen: agent.lastSeen
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const s of sessions.values()) {
|
|
223
|
+
if (s.publicKey !== publicKey && !wsAgents.has(s.publicKey)) {
|
|
224
|
+
peers.push({
|
|
225
|
+
publicKey: s.publicKey,
|
|
226
|
+
name: s.name,
|
|
227
|
+
lastSeen: s.registeredAt
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
res.json({ token, expiresAt, peers });
|
|
232
|
+
});
|
|
233
|
+
router.post(
|
|
234
|
+
"/v1/send",
|
|
235
|
+
requireAuth,
|
|
236
|
+
async (req, res) => {
|
|
237
|
+
const { to, type, payload, inReplyTo } = req.body;
|
|
238
|
+
if (!to || typeof to !== "string") {
|
|
239
|
+
res.status(400).json({ error: "to is required" });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (!type || typeof type !== "string") {
|
|
243
|
+
res.status(400).json({ error: "type is required" });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (payload === void 0) {
|
|
247
|
+
res.status(400).json({ error: "payload is required" });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const senderPublicKey = req.agent.publicKey;
|
|
251
|
+
const session = sessions.get(senderPublicKey);
|
|
252
|
+
if (!session) {
|
|
253
|
+
res.status(401).json({ error: "Session not found \u2014 please re-register" });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const envelope = createEnv(
|
|
257
|
+
type,
|
|
258
|
+
senderPublicKey,
|
|
259
|
+
session.privateKey,
|
|
260
|
+
payload,
|
|
261
|
+
Date.now(),
|
|
262
|
+
inReplyTo
|
|
263
|
+
);
|
|
264
|
+
const wsAgents = relay.getAgents();
|
|
265
|
+
const wsRecipient = wsAgents.get(to);
|
|
266
|
+
if (wsRecipient && wsRecipient.socket) {
|
|
267
|
+
const ws = wsRecipient.socket;
|
|
268
|
+
const OPEN = 1;
|
|
269
|
+
if (ws.readyState !== OPEN) {
|
|
270
|
+
res.status(503).json({ error: "Recipient connection is not open" });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const relayMsg = JSON.stringify({
|
|
275
|
+
type: "message",
|
|
276
|
+
from: senderPublicKey,
|
|
277
|
+
name: session.name,
|
|
278
|
+
envelope
|
|
279
|
+
});
|
|
280
|
+
ws.send(relayMsg);
|
|
281
|
+
res.json({ ok: true, envelopeId: envelope.id });
|
|
282
|
+
return;
|
|
283
|
+
} catch (err) {
|
|
284
|
+
res.status(500).json({
|
|
285
|
+
error: "Failed to deliver message: " + (err instanceof Error ? err.message : String(err))
|
|
286
|
+
});
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const restRecipient = sessions.get(to);
|
|
291
|
+
if (restRecipient) {
|
|
292
|
+
const senderAgent = wsAgents.get(senderPublicKey);
|
|
293
|
+
const msg = {
|
|
294
|
+
id: envelope.id,
|
|
295
|
+
from: senderPublicKey,
|
|
296
|
+
fromName: session.name ?? senderAgent?.name,
|
|
297
|
+
type: envelope.type,
|
|
298
|
+
payload: envelope.payload,
|
|
299
|
+
timestamp: envelope.timestamp,
|
|
300
|
+
inReplyTo: envelope.inReplyTo
|
|
301
|
+
};
|
|
302
|
+
buffer.add(to, msg);
|
|
303
|
+
res.json({ ok: true, envelopeId: envelope.id });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
res.status(404).json({ error: "Recipient not connected" });
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
router.get(
|
|
310
|
+
"/v1/peers",
|
|
311
|
+
requireAuth,
|
|
312
|
+
(req, res) => {
|
|
313
|
+
const callerPublicKey = req.agent.publicKey;
|
|
314
|
+
const wsAgents = relay.getAgents();
|
|
315
|
+
const peerList = [];
|
|
316
|
+
for (const agent of wsAgents.values()) {
|
|
317
|
+
if (agent.publicKey !== callerPublicKey) {
|
|
318
|
+
peerList.push({
|
|
319
|
+
publicKey: agent.publicKey,
|
|
320
|
+
name: agent.name,
|
|
321
|
+
lastSeen: agent.lastSeen,
|
|
322
|
+
metadata: agent.metadata
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
for (const s of sessions.values()) {
|
|
327
|
+
if (s.publicKey !== callerPublicKey && !wsAgents.has(s.publicKey)) {
|
|
328
|
+
peerList.push({
|
|
329
|
+
publicKey: s.publicKey,
|
|
330
|
+
name: s.name,
|
|
331
|
+
lastSeen: s.registeredAt,
|
|
332
|
+
metadata: s.metadata
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
res.json({ peers: peerList });
|
|
337
|
+
}
|
|
338
|
+
);
|
|
339
|
+
router.get(
|
|
340
|
+
"/v1/messages",
|
|
341
|
+
requireAuth,
|
|
342
|
+
(req, res) => {
|
|
343
|
+
const publicKey = req.agent.publicKey;
|
|
344
|
+
const sinceRaw = req.query.since;
|
|
345
|
+
const limitRaw = req.query.limit;
|
|
346
|
+
const since = sinceRaw ? parseInt(sinceRaw, 10) : void 0;
|
|
347
|
+
const limit = Math.min(limitRaw ? parseInt(limitRaw, 10) : 50, 100);
|
|
348
|
+
let messages = buffer.get(publicKey, since);
|
|
349
|
+
const hasMore = messages.length > limit;
|
|
350
|
+
if (hasMore) {
|
|
351
|
+
messages = messages.slice(0, limit);
|
|
352
|
+
}
|
|
353
|
+
if (since === void 0) {
|
|
354
|
+
buffer.clear(publicKey);
|
|
355
|
+
}
|
|
356
|
+
res.json({ messages, hasMore });
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
router.delete(
|
|
360
|
+
"/v1/disconnect",
|
|
361
|
+
requireAuth,
|
|
362
|
+
(req, res) => {
|
|
363
|
+
const publicKey = req.agent.publicKey;
|
|
364
|
+
const authHeader = req.headers.authorization;
|
|
365
|
+
const token = authHeader.slice(7);
|
|
366
|
+
revokeToken(token);
|
|
367
|
+
sessions.delete(publicKey);
|
|
368
|
+
buffer.delete(publicKey);
|
|
369
|
+
res.json({ ok: true });
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
return router;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/relay/run-relay.ts
|
|
376
|
+
import http from "http";
|
|
377
|
+
import express from "express";
|
|
378
|
+
import cors from "cors";
|
|
379
|
+
var createEnvelopeForRest = (type, sender, privateKey, payload, timestamp, inReplyTo) => createEnvelope(
|
|
380
|
+
type,
|
|
381
|
+
sender,
|
|
382
|
+
privateKey,
|
|
383
|
+
payload,
|
|
384
|
+
timestamp ?? Date.now(),
|
|
385
|
+
inReplyTo
|
|
386
|
+
);
|
|
387
|
+
async function runRelay(options = {}) {
|
|
388
|
+
const wsPort = options.wsPort ?? parseInt(
|
|
389
|
+
process.env.RELAY_PORT ?? process.env.PORT ?? "3002",
|
|
390
|
+
10
|
|
391
|
+
);
|
|
392
|
+
const jwtSecret = process.env.JWT_SECRET ?? process.env.AGORA_RELAY_JWT_SECRET;
|
|
393
|
+
const enableRest = options.enableRest ?? (typeof jwtSecret === "string" && jwtSecret.length > 0);
|
|
394
|
+
const maxPeers = parseInt(process.env.MAX_PEERS ?? "100", 10);
|
|
395
|
+
const relayOptions = { ...options.relayOptions, maxPeers };
|
|
396
|
+
const relay = new RelayServer(relayOptions);
|
|
397
|
+
await relay.start(wsPort);
|
|
398
|
+
if (!enableRest) {
|
|
399
|
+
return { relay };
|
|
400
|
+
}
|
|
401
|
+
if (!jwtSecret) {
|
|
402
|
+
await relay.stop();
|
|
403
|
+
throw new Error(
|
|
404
|
+
"JWT_SECRET (or AGORA_RELAY_JWT_SECRET) environment variable is required when REST API is enabled"
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
if (!process.env.AGORA_RELAY_JWT_SECRET) {
|
|
408
|
+
process.env.AGORA_RELAY_JWT_SECRET = jwtSecret;
|
|
409
|
+
}
|
|
410
|
+
const restPort = options.restPort ?? parseInt(process.env.REST_PORT ?? "3001", 10);
|
|
411
|
+
const messageTtlMs = parseInt(process.env.MESSAGE_TTL_MS ?? "86400000", 10);
|
|
412
|
+
const messageBuffer = new MessageBuffer({ ttlMs: messageTtlMs });
|
|
413
|
+
const restSessions = /* @__PURE__ */ new Map();
|
|
414
|
+
const allowedOrigins = process.env.ALLOWED_ORIGINS ?? "*";
|
|
415
|
+
const corsOrigins = allowedOrigins === "*" ? "*" : allowedOrigins.split(",").map((o) => o.trim()).filter((o) => o.length > 0);
|
|
416
|
+
const app = express();
|
|
417
|
+
app.use(cors({
|
|
418
|
+
origin: corsOrigins,
|
|
419
|
+
methods: ["GET", "POST", "DELETE"],
|
|
420
|
+
allowedHeaders: ["Content-Type", "Authorization"]
|
|
421
|
+
}));
|
|
422
|
+
app.use(express.json());
|
|
423
|
+
const verifyForRest = (envelope) => verifyEnvelope(envelope);
|
|
424
|
+
const rateLimitRpm = parseInt(process.env.RATE_LIMIT_RPM ?? "60", 10);
|
|
425
|
+
const router = createRestRouter(
|
|
426
|
+
relay,
|
|
427
|
+
messageBuffer,
|
|
428
|
+
restSessions,
|
|
429
|
+
createEnvelopeForRest,
|
|
430
|
+
verifyForRest,
|
|
431
|
+
rateLimitRpm
|
|
432
|
+
);
|
|
433
|
+
app.use(router);
|
|
434
|
+
app.use((_req, res) => {
|
|
435
|
+
res.status(404).json({ error: "Not found" });
|
|
436
|
+
});
|
|
437
|
+
const httpServer = http.createServer(app);
|
|
438
|
+
await new Promise((resolve, reject) => {
|
|
439
|
+
httpServer.listen(restPort, () => resolve());
|
|
440
|
+
httpServer.on("error", reject);
|
|
441
|
+
});
|
|
442
|
+
return { relay, httpServer };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export {
|
|
446
|
+
MessageBuffer,
|
|
447
|
+
createToken,
|
|
448
|
+
revokeToken,
|
|
449
|
+
requireAuth,
|
|
450
|
+
createRestRouter,
|
|
451
|
+
runRelay
|
|
452
|
+
};
|
|
453
|
+
//# sourceMappingURL=chunk-2U4PZINT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/relay/message-buffer.ts","../src/relay/jwt-auth.ts","../src/relay/rest-api.ts","../src/relay/run-relay.ts"],"sourcesContent":["/**\n * message-buffer.ts — In-memory bounded message queue per agent.\n *\n * When messages are delivered to an agent via the relay, they are also\n * stored here so that HTTP polling clients can retrieve them via GET /v1/messages.\n */\n\nexport interface BufferedMessage {\n id: string;\n from: string;\n fromName?: string;\n type: string;\n payload: unknown;\n timestamp: number;\n inReplyTo?: string;\n}\n\ninterface StoredMessage {\n message: BufferedMessage;\n receivedAt: number;\n}\n\nconst MAX_MESSAGES_PER_AGENT = 100;\n\n/**\n * MessageBuffer stores inbound messages per agent public key.\n * FIFO eviction when the buffer is full (max 100 messages).\n * Messages older than ttlMs (measured from when they were received) are pruned on access.\n */\nexport class MessageBuffer {\n private buffers: Map<string, StoredMessage[]> = new Map();\n private ttlMs: number;\n\n constructor(options?: { ttlMs?: number }) {\n this.ttlMs = options?.ttlMs ?? 86400000; // default 24h\n }\n\n /**\n * Add a message to an agent's buffer.\n * Evicts the oldest message if the buffer is full.\n */\n add(publicKey: string, message: BufferedMessage): void {\n let queue = this.buffers.get(publicKey);\n if (!queue) {\n queue = [];\n this.buffers.set(publicKey, queue);\n }\n queue.push({ message, receivedAt: Date.now() });\n if (queue.length > MAX_MESSAGES_PER_AGENT) {\n queue.shift(); // FIFO eviction\n }\n }\n\n /**\n * Retrieve messages for an agent, optionally filtering by `since` timestamp.\n * Returns messages with timestamp > since (exclusive). Prunes expired messages.\n */\n get(publicKey: string, since?: number): BufferedMessage[] {\n const now = Date.now();\n let queue = this.buffers.get(publicKey) ?? [];\n // Prune messages older than ttlMs (based on wall-clock receive time)\n queue = queue.filter((s) => now - s.receivedAt < this.ttlMs);\n this.buffers.set(publicKey, queue);\n const messages = queue.map((s) => s.message);\n if (since === undefined) {\n return [...messages];\n }\n return messages.filter((m) => m.timestamp > since);\n }\n\n /**\n * Clear all messages for an agent (after polling without `since`).\n */\n clear(publicKey: string): void {\n this.buffers.set(publicKey, []);\n }\n\n /**\n * Remove all state for a disconnected agent.\n */\n delete(publicKey: string): void {\n this.buffers.delete(publicKey);\n }\n}\n","/**\n * jwt-auth.ts — JWT token creation and validation middleware.\n *\n * Tokens are signed with AGORA_RELAY_JWT_SECRET (required env var).\n * Expiry defaults to 3600 seconds (1 hour), configurable via AGORA_JWT_EXPIRY_SECONDS.\n *\n * Token payload: { publicKey, name }\n */\n\nimport jwt from 'jsonwebtoken';\nimport { randomBytes } from 'node:crypto';\nimport type { Request, Response, NextFunction } from 'express';\n\nexport interface JwtPayload {\n publicKey: string;\n name?: string;\n}\n\n/**\n * Augment Express Request to carry decoded JWT payload.\n */\nexport interface AuthenticatedRequest extends Request {\n agent?: JwtPayload;\n}\n\n/**\n * Revocation set for invalidated tokens (populated by DELETE /v1/disconnect).\n * Stored as a Map of JWT `jti` → expiry timestamp (ms).\n * Entries are automatically removed once their JWT would have expired anyway,\n * preventing unbounded memory growth.\n */\nconst revokedJtis: Map<string, number> = new Map();\n\n/**\n * Remove revoked JTI entries whose token expiry has already passed.\n * These tokens can no longer be used regardless, so no need to keep them.\n */\nfunction pruneExpiredRevocations(): void {\n const now = Date.now();\n for (const [jti, expiry] of revokedJtis) {\n if (expiry <= now) {\n revokedJtis.delete(jti);\n }\n }\n}\n\nfunction getJwtSecret(): string {\n const secret = process.env.AGORA_RELAY_JWT_SECRET;\n if (!secret) {\n throw new Error(\n 'AGORA_RELAY_JWT_SECRET environment variable is required but not set'\n );\n }\n return secret;\n}\n\nfunction getExpirySeconds(): number {\n const raw = process.env.AGORA_JWT_EXPIRY_SECONDS;\n if (raw) {\n const parsed = parseInt(raw, 10);\n if (!isNaN(parsed) && parsed > 0) {\n return parsed;\n }\n }\n return 3600; // 1 hour default\n}\n\n/**\n * Create a signed JWT for a registered agent.\n * Returns the token string and its expiry timestamp (ms since epoch).\n */\nexport function createToken(payload: JwtPayload): {\n token: string;\n expiresAt: number;\n} {\n const secret = getJwtSecret();\n const expirySeconds = getExpirySeconds();\n const jti = `${Date.now()}-${randomBytes(16).toString('hex')}`;\n\n const token = jwt.sign(\n { publicKey: payload.publicKey, name: payload.name, jti },\n secret,\n { expiresIn: expirySeconds }\n );\n\n const expiresAt = Date.now() + expirySeconds * 1000;\n return { token, expiresAt };\n}\n\n/**\n * Revoke a token by its jti claim so it cannot be used again.\n * The revocation entry is stored with the token's expiry so it can be\n * pruned automatically once the token would no longer be valid anyway.\n */\nexport function revokeToken(token: string): void {\n try {\n const secret = getJwtSecret();\n const decoded = jwt.verify(token, secret) as jwt.JwtPayload & {\n jti?: string;\n exp?: number;\n };\n if (decoded.jti) {\n const expiry = decoded.exp ? decoded.exp * 1000 : Date.now();\n revokedJtis.set(decoded.jti, expiry);\n pruneExpiredRevocations();\n }\n } catch {\n // Token already invalid — nothing to revoke\n }\n}\n\n/**\n * Express middleware that validates the Authorization: Bearer <token> header.\n * Attaches decoded payload to `req.agent` on success.\n * Responds with 401 if missing/invalid/expired/revoked.\n */\nexport function requireAuth(\n req: AuthenticatedRequest,\n res: Response,\n next: NextFunction\n): void {\n const authHeader = req.headers.authorization;\n if (!authHeader || !authHeader.startsWith('Bearer ')) {\n res.status(401).json({ error: 'Missing or malformed Authorization header' });\n return;\n }\n\n const token = authHeader.slice(7);\n try {\n const secret = getJwtSecret();\n const decoded = jwt.verify(token, secret) as jwt.JwtPayload & {\n publicKey: string;\n name?: string;\n jti?: string;\n };\n\n if (decoded.jti && revokedJtis.has(decoded.jti)) {\n res.status(401).json({ error: 'Token has been revoked' });\n return;\n }\n\n req.agent = { publicKey: decoded.publicKey, name: decoded.name };\n next();\n } catch (err) {\n if (err instanceof jwt.TokenExpiredError) {\n res.status(401).json({ error: 'Token expired' });\n } else {\n res.status(401).json({ error: 'Invalid token' });\n }\n }\n}\n","/**\n * rest-api.ts — Express router implementing the Agora relay REST API.\n *\n * Endpoints:\n * POST /v1/register — Register agent, obtain JWT session token\n * POST /v1/send — Send message to a peer (requires auth)\n * GET /v1/peers — List online peers (requires auth)\n * GET /v1/messages — Poll for new inbound messages (requires auth)\n * DELETE /v1/disconnect — Invalidate token and disconnect (requires auth)\n */\n\nimport { Router } from 'express';\nimport type { Request, Response } from 'express';\nimport { rateLimit } from 'express-rate-limit';\nimport {\n createToken,\n revokeToken,\n requireAuth,\n type AuthenticatedRequest,\n} from './jwt-auth';\nimport { MessageBuffer, type BufferedMessage } from './message-buffer';\n\nconst apiRateLimit = (rpm: number): ReturnType<typeof rateLimit> => rateLimit({\n windowMs: 60_000,\n limit: rpm,\n standardHeaders: 'draft-7',\n legacyHeaders: false,\n message: { error: 'Too many requests — try again later' },\n});\n\n/**\n * A session for a REST-connected agent.\n * privateKey is held only in memory and never logged or persisted.\n */\nexport interface RestSession {\n publicKey: string;\n privateKey: string;\n name?: string;\n metadata?: { version?: string; capabilities?: string[] };\n registeredAt: number;\n expiresAt: number;\n token: string;\n}\n\nfunction pruneExpiredSessions(\n sessions: Map<string, RestSession>,\n buffer: MessageBuffer\n): void {\n const now = Date.now();\n for (const [publicKey, session] of sessions) {\n if (session.expiresAt <= now) {\n sessions.delete(publicKey);\n buffer.delete(publicKey);\n }\n }\n}\n\n/**\n * Minimal interface for the relay server that the REST API depends on.\n */\nexport interface RelayInterface {\n getAgents(): Map<\n string,\n {\n publicKey: string;\n name?: string;\n lastSeen: number;\n metadata?: { version?: string; capabilities?: string[] };\n socket: unknown;\n }\n >;\n on(\n event: 'message-relayed',\n handler: (from: string, to: string, envelope: unknown) => void\n ): void;\n}\n\n/**\n * Envelope creation function interface (matches createEnvelope from message/envelope).\n */\nexport type CreateEnvelopeFn = (\n type: string,\n sender: string,\n privateKey: string,\n payload: unknown,\n timestamp?: number,\n inReplyTo?: string\n) => {\n id: string;\n type: string;\n sender: string;\n timestamp: number;\n payload: unknown;\n signature: string;\n inReplyTo?: string;\n};\n\n/**\n * Envelope verification function interface.\n */\nexport type VerifyEnvelopeFn = (envelope: unknown) => {\n valid: boolean;\n reason?: string;\n};\n\n/**\n * Create the REST API router.\n */\nexport function createRestRouter(\n relay: RelayInterface,\n buffer: MessageBuffer,\n sessions: Map<string, RestSession>,\n createEnv: CreateEnvelopeFn,\n verifyEnv: VerifyEnvelopeFn,\n rateLimitRpm = 60\n): Router {\n const router = Router();\n router.use(apiRateLimit(rateLimitRpm));\n\n relay.on('message-relayed', (from, to, envelope) => {\n if (!sessions.has(to)) return;\n const agentMap = relay.getAgents();\n const senderAgent = agentMap.get(from);\n const env = envelope as {\n id: string;\n type: string;\n payload: unknown;\n timestamp: number;\n inReplyTo?: string;\n };\n const msg: BufferedMessage = {\n id: env.id,\n from,\n fromName: senderAgent?.name,\n type: env.type,\n payload: env.payload,\n timestamp: env.timestamp,\n inReplyTo: env.inReplyTo,\n };\n buffer.add(to, msg);\n });\n\n router.post('/v1/register', async (req: Request, res: Response) => {\n const { publicKey, privateKey, name, metadata } = req.body as {\n publicKey?: string;\n privateKey?: string;\n name?: string;\n metadata?: { version?: string; capabilities?: string[] };\n };\n\n if (!publicKey || typeof publicKey !== 'string') {\n res.status(400).json({ error: 'publicKey is required' });\n return;\n }\n if (!privateKey || typeof privateKey !== 'string') {\n res.status(400).json({ error: 'privateKey is required' });\n return;\n }\n\n const testEnvelope = createEnv(\n 'announce',\n publicKey,\n privateKey,\n { challenge: 'register' },\n Date.now()\n );\n const verification = verifyEnv(testEnvelope);\n if (!verification.valid) {\n res\n .status(400)\n .json({ error: 'Key pair verification failed: ' + verification.reason });\n return;\n }\n\n const { token, expiresAt } = createToken({ publicKey, name });\n pruneExpiredSessions(sessions, buffer);\n\n const session: RestSession = {\n publicKey,\n privateKey,\n name,\n metadata,\n registeredAt: Date.now(),\n expiresAt,\n token,\n };\n sessions.set(publicKey, session);\n\n const wsAgents = relay.getAgents();\n const peers: Array<{ publicKey: string; name?: string; lastSeen: number }> = [];\n for (const agent of wsAgents.values()) {\n if (agent.publicKey !== publicKey) {\n peers.push({\n publicKey: agent.publicKey,\n name: agent.name,\n lastSeen: agent.lastSeen,\n });\n }\n }\n for (const s of sessions.values()) {\n if (s.publicKey !== publicKey && !wsAgents.has(s.publicKey)) {\n peers.push({\n publicKey: s.publicKey,\n name: s.name,\n lastSeen: s.registeredAt,\n });\n }\n }\n\n res.json({ token, expiresAt, peers });\n });\n\n router.post(\n '/v1/send',\n requireAuth,\n async (req: AuthenticatedRequest, res: Response) => {\n const { to, type, payload, inReplyTo } = req.body as {\n to?: string;\n type?: string;\n payload?: unknown;\n inReplyTo?: string;\n };\n\n if (!to || typeof to !== 'string') {\n res.status(400).json({ error: 'to is required' });\n return;\n }\n if (!type || typeof type !== 'string') {\n res.status(400).json({ error: 'type is required' });\n return;\n }\n if (payload === undefined) {\n res.status(400).json({ error: 'payload is required' });\n return;\n }\n\n const senderPublicKey = req.agent!.publicKey;\n const session = sessions.get(senderPublicKey);\n if (!session) {\n res.status(401).json({ error: 'Session not found — please re-register' });\n return;\n }\n\n const envelope = createEnv(\n type,\n senderPublicKey,\n session.privateKey,\n payload,\n Date.now(),\n inReplyTo\n );\n\n const wsAgents = relay.getAgents();\n const wsRecipient = wsAgents.get(to);\n if (wsRecipient && wsRecipient.socket) {\n const ws = wsRecipient.socket as { readyState: number; send(data: string): void };\n const OPEN = 1;\n if (ws.readyState !== OPEN) {\n res.status(503).json({ error: 'Recipient connection is not open' });\n return;\n }\n try {\n const relayMsg = JSON.stringify({\n type: 'message',\n from: senderPublicKey,\n name: session.name,\n envelope,\n });\n ws.send(relayMsg);\n res.json({ ok: true, envelopeId: envelope.id });\n return;\n } catch (err) {\n res.status(500).json({\n error:\n 'Failed to deliver message: ' +\n (err instanceof Error ? err.message : String(err)),\n });\n return;\n }\n }\n\n const restRecipient = sessions.get(to);\n if (restRecipient) {\n const senderAgent = wsAgents.get(senderPublicKey);\n const msg: BufferedMessage = {\n id: envelope.id,\n from: senderPublicKey,\n fromName: session.name ?? senderAgent?.name,\n type: envelope.type,\n payload: envelope.payload,\n timestamp: envelope.timestamp,\n inReplyTo: envelope.inReplyTo,\n };\n buffer.add(to, msg);\n res.json({ ok: true, envelopeId: envelope.id });\n return;\n }\n\n res.status(404).json({ error: 'Recipient not connected' });\n }\n );\n\n router.get(\n '/v1/peers',\n requireAuth,\n (req: AuthenticatedRequest, res: Response) => {\n const callerPublicKey = req.agent!.publicKey;\n const wsAgents = relay.getAgents();\n const peerList: Array<{\n publicKey: string;\n name?: string;\n lastSeen: number;\n metadata?: { version?: string; capabilities?: string[] };\n }> = [];\n\n for (const agent of wsAgents.values()) {\n if (agent.publicKey !== callerPublicKey) {\n peerList.push({\n publicKey: agent.publicKey,\n name: agent.name,\n lastSeen: agent.lastSeen,\n metadata: agent.metadata,\n });\n }\n }\n\n for (const s of sessions.values()) {\n if (s.publicKey !== callerPublicKey && !wsAgents.has(s.publicKey)) {\n peerList.push({\n publicKey: s.publicKey,\n name: s.name,\n lastSeen: s.registeredAt,\n metadata: s.metadata,\n });\n }\n }\n\n res.json({ peers: peerList });\n }\n );\n\n router.get(\n '/v1/messages',\n requireAuth,\n (req: AuthenticatedRequest, res: Response) => {\n const publicKey = req.agent!.publicKey;\n const sinceRaw = req.query.since as string | undefined;\n const limitRaw = req.query.limit as string | undefined;\n\n const since = sinceRaw ? parseInt(sinceRaw, 10) : undefined;\n const limit = Math.min(limitRaw ? parseInt(limitRaw, 10) : 50, 100);\n\n let messages = buffer.get(publicKey, since);\n const hasMore = messages.length > limit;\n if (hasMore) {\n messages = messages.slice(0, limit);\n }\n\n if (since === undefined) {\n buffer.clear(publicKey);\n }\n\n res.json({ messages, hasMore });\n }\n );\n\n router.delete(\n '/v1/disconnect',\n requireAuth,\n (req: AuthenticatedRequest, res: Response) => {\n const publicKey = req.agent!.publicKey;\n const authHeader = req.headers.authorization!;\n const token = authHeader.slice(7);\n\n revokeToken(token);\n sessions.delete(publicKey);\n buffer.delete(publicKey);\n\n res.json({ ok: true });\n }\n );\n\n return router;\n}\n","/**\n * run-relay.ts — Start Agora relay: WebSocket server and optional REST API.\n *\n * When REST is enabled, starts:\n * 1. WebSocket relay (RelayServer) on wsPort\n * 2. REST API server (Express) on restPort\n *\n * Environment variables:\n * RELAY_PORT — WebSocket relay port (default: 3002); alias: PORT\n * REST_PORT — REST API port (default: 3001)\n * JWT_SECRET — Secret for JWT session tokens (alias: AGORA_RELAY_JWT_SECRET)\n * AGORA_JWT_EXPIRY_SECONDS — JWT expiry in seconds (default: 3600)\n * MAX_PEERS — Maximum concurrent registered peers (default: 100)\n * MESSAGE_TTL_MS — Message buffer TTL in ms (default: 86400000 = 24h)\n * RATE_LIMIT_RPM — REST API requests per minute per IP (default: 60)\n * ALLOWED_ORIGINS — CORS origins, comma-separated or * (default: *)\n */\n\nimport http from 'node:http';\nimport express from 'express';\nimport cors from 'cors';\nimport { RelayServer, type RelayServerOptions } from './server';\nimport {\n createEnvelope,\n verifyEnvelope,\n type Envelope,\n type MessageType,\n} from '../message/envelope';\nimport { createRestRouter, type CreateEnvelopeFn } from './rest-api';\nimport { MessageBuffer } from './message-buffer';\nimport type { RestSession } from './rest-api';\n\n/** Wrapper so REST API can pass string type; createEnvelope expects MessageType */\nconst createEnvelopeForRest: CreateEnvelopeFn = (\n type,\n sender,\n privateKey,\n payload,\n timestamp,\n inReplyTo\n) =>\n createEnvelope(\n type as MessageType,\n sender,\n privateKey,\n payload,\n timestamp ?? Date.now(),\n inReplyTo\n );\n\nexport interface RunRelayOptions {\n /** WebSocket port (default from RELAY_PORT or PORT env, or 3002) */\n wsPort?: number;\n /** REST API port (default from REST_PORT env, or 3001). Ignored if enableRest is false. */\n restPort?: number;\n /** Enable REST API (requires JWT_SECRET or AGORA_RELAY_JWT_SECRET). Default: true if secret is set. */\n enableRest?: boolean;\n /** Relay server options (identity, storagePeers, storageDir) */\n relayOptions?: RelayServerOptions;\n}\n\n/**\n * Start WebSocket relay and optionally REST API.\n * Returns { relay, httpServer } where httpServer is set only when REST is enabled.\n */\nexport async function runRelay(options: RunRelayOptions = {}): Promise<{\n relay: RelayServer;\n httpServer?: http.Server;\n}> {\n const wsPort = options.wsPort ?? parseInt(\n process.env.RELAY_PORT ?? process.env.PORT ?? '3002', 10\n );\n const jwtSecret = process.env.JWT_SECRET ?? process.env.AGORA_RELAY_JWT_SECRET;\n const enableRest =\n options.enableRest ??\n (typeof jwtSecret === 'string' && jwtSecret.length > 0);\n\n const maxPeers = parseInt(process.env.MAX_PEERS ?? '100', 10);\n const relayOptions: RelayServerOptions = { ...options.relayOptions, maxPeers };\n\n const relay = new RelayServer(relayOptions);\n await relay.start(wsPort);\n\n if (!enableRest) {\n return { relay };\n }\n\n if (!jwtSecret) {\n await relay.stop();\n throw new Error(\n 'JWT_SECRET (or AGORA_RELAY_JWT_SECRET) environment variable is required when REST API is enabled'\n );\n }\n\n // Expose jwtSecret via env so jwt-auth.ts can read it (it reads AGORA_RELAY_JWT_SECRET)\n if (!process.env.AGORA_RELAY_JWT_SECRET) {\n process.env.AGORA_RELAY_JWT_SECRET = jwtSecret;\n }\n\n const restPort = options.restPort ?? parseInt(process.env.REST_PORT ?? '3001', 10);\n const messageTtlMs = parseInt(process.env.MESSAGE_TTL_MS ?? '86400000', 10);\n const messageBuffer = new MessageBuffer({ ttlMs: messageTtlMs });\n const restSessions = new Map<string, RestSession>();\n\n const allowedOrigins = process.env.ALLOWED_ORIGINS ?? '*';\n const corsOrigins = allowedOrigins === '*'\n ? '*'\n : allowedOrigins.split(',').map((o) => o.trim()).filter((o) => o.length > 0);\n\n const app = express();\n app.use(cors({\n origin: corsOrigins,\n methods: ['GET', 'POST', 'DELETE'],\n allowedHeaders: ['Content-Type', 'Authorization'],\n }));\n app.use(express.json());\n\n const verifyForRest = (envelope: unknown): { valid: boolean; reason?: string } =>\n verifyEnvelope(envelope as Envelope);\n\n const rateLimitRpm = parseInt(process.env.RATE_LIMIT_RPM ?? '60', 10);\n const router = createRestRouter(\n relay as Parameters<typeof createRestRouter>[0],\n messageBuffer,\n restSessions,\n createEnvelopeForRest,\n verifyForRest,\n rateLimitRpm\n );\n app.use(router);\n\n app.use((_req, res) => {\n res.status(404).json({ error: 'Not found' });\n });\n\n const httpServer = http.createServer(app);\n await new Promise<void>((resolve, reject) => {\n httpServer.listen(restPort, () => resolve());\n httpServer.on('error', reject);\n });\n\n return { relay, httpServer };\n}\n"],"mappings":"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","names":[]}
|