@rookdaemon/agora 0.8.2 → 0.8.3

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.
@@ -9,8 +9,10 @@ var MAX_MESSAGES_PER_AGENT = 100;
9
9
  var MessageBuffer = class {
10
10
  buffers = /* @__PURE__ */ new Map();
11
11
  ttlMs;
12
+ maxMessages;
12
13
  constructor(options) {
13
14
  this.ttlMs = options?.ttlMs ?? 864e5;
15
+ this.maxMessages = options?.maxMessages ?? MAX_MESSAGES_PER_AGENT;
14
16
  }
15
17
  /**
16
18
  * Add a message to an agent's buffer.
@@ -23,7 +25,7 @@ var MessageBuffer = class {
23
25
  this.buffers.set(publicKey, queue);
24
26
  }
25
27
  queue.push({ message, receivedAt: Date.now() });
26
- if (queue.length > MAX_MESSAGES_PER_AGENT) {
28
+ if (queue.length > this.maxMessages) {
27
29
  queue.shift();
28
30
  }
29
31
  }
@@ -155,7 +157,7 @@ function pruneExpiredSessions(sessions, buffer) {
155
157
  }
156
158
  }
157
159
  }
158
- function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLimitRpm = 60) {
160
+ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLimitRpm = 60, replayBuffer, replayRetentionMs = 7 * 24 * 60 * 60 * 1e3) {
159
161
  const router = Router();
160
162
  router.use(apiRateLimit(rateLimitRpm));
161
163
  relay.on("message-relayed", (from, to, envelope) => {
@@ -170,6 +172,7 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
170
172
  inReplyTo: env.inReplyTo
171
173
  };
172
174
  buffer.add(to, msg);
175
+ replayBuffer?.add(to, msg);
173
176
  });
174
177
  router.post("/v1/register", async (req, res) => {
175
178
  const { publicKey, privateKey, metadata } = req.body;
@@ -347,6 +350,52 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
347
350
  res.json({ messages, hasMore });
348
351
  }
349
352
  );
353
+ router.get(
354
+ "/v1/messages/replay",
355
+ requireAuth,
356
+ (req, res) => {
357
+ if (!replayBuffer) {
358
+ res.status(501).json({ error: "Replay endpoint is not enabled on this relay" });
359
+ return;
360
+ }
361
+ const publicKey = req.agent.publicKey;
362
+ const sinceRaw = req.query.since;
363
+ const limitRaw = req.query.limit;
364
+ if (!sinceRaw) {
365
+ res.status(400).json({ error: "`since` query parameter is required (ISO8601 timestamp)" });
366
+ return;
367
+ }
368
+ const sinceDate = new Date(sinceRaw);
369
+ if (isNaN(sinceDate.getTime())) {
370
+ res.status(400).json({ error: "Invalid `since` value \u2014 must be a valid ISO8601 timestamp" });
371
+ return;
372
+ }
373
+ const sinceMs = sinceDate.getTime();
374
+ const oldestAllowed = Date.now() - replayRetentionMs;
375
+ if (sinceMs < oldestAllowed) {
376
+ res.status(400).json({
377
+ error: "retention_exceeded",
378
+ message: `\`since\` is older than the ${Math.round(replayRetentionMs / 864e5)}-day retention window`
379
+ });
380
+ return;
381
+ }
382
+ let limit = 100;
383
+ if (limitRaw !== void 0) {
384
+ const parsedLimit = parseInt(limitRaw, 10);
385
+ if (isNaN(parsedLimit) || parsedLimit < 1) {
386
+ res.status(400).json({ error: "Invalid `limit` value \u2014 must be a positive integer" });
387
+ return;
388
+ }
389
+ limit = Math.min(parsedLimit, 500);
390
+ }
391
+ let messages = replayBuffer.get(publicKey, sinceMs);
392
+ const hasMore = messages.length > limit;
393
+ if (hasMore) {
394
+ messages = messages.slice(0, limit);
395
+ }
396
+ res.json({ messages, hasMore });
397
+ }
398
+ );
350
399
  router.delete(
351
400
  "/v1/disconnect",
352
401
  requireAuth,
@@ -402,6 +451,8 @@ async function runRelay(options = {}) {
402
451
  const restPort = options.restPort ?? parseInt(process.env.REST_PORT ?? "3001", 10);
403
452
  const messageTtlMs = parseInt(process.env.MESSAGE_TTL_MS ?? "86400000", 10);
404
453
  const messageBuffer = new MessageBuffer({ ttlMs: messageTtlMs });
454
+ const replayRetentionMs = 7 * 24 * 60 * 60 * 1e3;
455
+ const replayBuffer = new MessageBuffer({ ttlMs: replayRetentionMs, maxMessages: 1e4 });
405
456
  const restSessions = /* @__PURE__ */ new Map();
406
457
  const allowedOrigins = process.env.ALLOWED_ORIGINS ?? "*";
407
458
  const corsOrigins = allowedOrigins === "*" ? "*" : allowedOrigins.split(",").map((o) => o.trim()).filter((o) => o.length > 0);
@@ -420,7 +471,9 @@ async function runRelay(options = {}) {
420
471
  restSessions,
421
472
  createEnvelopeForRest,
422
473
  verifyForRest,
423
- rateLimitRpm
474
+ rateLimitRpm,
475
+ replayBuffer,
476
+ replayRetentionMs
424
477
  );
425
478
  app.use(router);
426
479
  app.use((_req, res) => {
@@ -442,4 +495,4 @@ export {
442
495
  createRestRouter,
443
496
  runRelay
444
497
  };
445
- //# sourceMappingURL=chunk-RG3ZM57F.js.map
498
+ //# sourceMappingURL=chunk-52RKJA35.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 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 maxMessages 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 private maxMessages: number;\n\n constructor(options?: { ttlMs?: number; maxMessages?: number }) {\n this.ttlMs = options?.ttlMs ?? 86400000; // default 24h\n this.maxMessages = options?.maxMessages ?? MAX_MESSAGES_PER_AGENT;\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 > this.maxMessages) {\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 * GET /v1/messages/replay — Replay messages since a timestamp (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 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 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 from: string,\n to: string[],\n privateKey: string,\n payload: unknown,\n timestamp?: number,\n inReplyTo?: string\n) => {\n id: string;\n type: string;\n from: string;\n to: 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 replayBuffer?: MessageBuffer,\n replayRetentionMs = 7 * 24 * 60 * 60 * 1000\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 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 type: env.type,\n payload: env.payload,\n timestamp: env.timestamp,\n inReplyTo: env.inReplyTo,\n };\n buffer.add(to, msg);\n replayBuffer?.add(to, msg);\n });\n\n router.post('/v1/register', async (req: Request, res: Response) => {\n const { publicKey, privateKey, metadata } = req.body as {\n publicKey?: string;\n privateKey?: 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 [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 });\n pruneExpiredSessions(sessions, buffer);\n\n const session: RestSession = {\n publicKey,\n privateKey,\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; lastSeen: number }> = [];\n for (const agent of wsAgents.values()) {\n if (agent.publicKey !== publicKey) {\n peers.push({\n publicKey: agent.publicKey,\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 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 [to],\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 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 msg: BufferedMessage = {\n id: envelope.id,\n from: senderPublicKey,\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: ${to}` });\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 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 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 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.get(\n '/v1/messages/replay',\n requireAuth,\n (req: AuthenticatedRequest, res: Response) => {\n if (!replayBuffer) {\n res.status(501).json({ error: 'Replay endpoint is not enabled on this relay' });\n return;\n }\n\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 if (!sinceRaw) {\n res.status(400).json({ error: '`since` query parameter is required (ISO8601 timestamp)' });\n return;\n }\n\n const sinceDate = new Date(sinceRaw);\n if (isNaN(sinceDate.getTime())) {\n res.status(400).json({ error: 'Invalid `since` value — must be a valid ISO8601 timestamp' });\n return;\n }\n\n const sinceMs = sinceDate.getTime();\n const oldestAllowed = Date.now() - replayRetentionMs;\n\n if (sinceMs < oldestAllowed) {\n res.status(400).json({\n error: 'retention_exceeded',\n message: `\\`since\\` is older than the ${Math.round(replayRetentionMs / 86400000)}-day retention window`,\n });\n return;\n }\n\n let limit = 100;\n if (limitRaw !== undefined) {\n const parsedLimit = parseInt(limitRaw, 10);\n if (isNaN(parsedLimit) || parsedLimit < 1) {\n res.status(400).json({ error: 'Invalid `limit` value — must be a positive integer' });\n return;\n }\n limit = Math.min(parsedLimit, 500);\n }\n\n let messages = replayBuffer.get(publicKey, sinceMs);\n const hasMore = messages.length > limit;\n if (hasMore) {\n messages = messages.slice(0, limit);\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 from,\n to,\n privateKey,\n payload,\n timestamp,\n inReplyTo\n) =>\n createEnvelope(\n type as MessageType,\n from,\n privateKey,\n payload,\n timestamp ?? Date.now(),\n inReplyTo,\n to\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 replayRetentionMs = 7 * 24 * 60 * 60 * 1000; // 7 days\n const replayBuffer = new MessageBuffer({ ttlMs: replayRetentionMs, maxMessages: 10000 });\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 replayBuffer,\n replayRetentionMs\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":";;;;;;;AAqBA,IAAM,yBAAyB;AAOxB,IAAM,gBAAN,MAAoB;AAAA,EACjB,UAAwC,oBAAI,IAAI;AAAA,EAChD;AAAA,EACA;AAAA,EAER,YAAY,SAAoD;AAC9D,SAAK,QAAQ,SAAS,SAAS;AAC/B,SAAK,cAAc,SAAS,eAAe;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,WAAmB,SAAgC;AACrD,QAAI,QAAQ,KAAK,QAAQ,IAAI,SAAS;AACtC,QAAI,CAAC,OAAO;AACV,cAAQ,CAAC;AACT,WAAK,QAAQ,IAAI,WAAW,KAAK;AAAA,IACnC;AACA,UAAM,KAAK,EAAE,SAAS,YAAY,KAAK,IAAI,EAAE,CAAC;AAC9C,QAAI,MAAM,SAAS,KAAK,aAAa;AACnC,YAAM,MAAM;AAAA,IACd;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,WAAmB,OAAmC;AACxD,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,QAAQ,KAAK,QAAQ,IAAI,SAAS,KAAK,CAAC;AAE5C,YAAQ,MAAM,OAAO,CAAC,MAAM,MAAM,EAAE,aAAa,KAAK,KAAK;AAC3D,SAAK,QAAQ,IAAI,WAAW,KAAK;AACjC,UAAM,WAAW,MAAM,IAAI,CAAC,MAAM,EAAE,OAAO;AAC3C,QAAI,UAAU,QAAW;AACvB,aAAO,CAAC,GAAG,QAAQ;AAAA,IACrB;AACA,WAAO,SAAS,OAAO,CAAC,MAAM,EAAE,YAAY,KAAK;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAAyB;AAC7B,SAAK,QAAQ,IAAI,WAAW,CAAC,CAAC;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,WAAyB;AAC9B,SAAK,QAAQ,OAAO,SAAS;AAAA,EAC/B;AACF;;;AC3EA,OAAO,SAAS;AAChB,SAAS,mBAAmB;AAqB5B,IAAM,cAAmC,oBAAI,IAAI;AAMjD,SAAS,0BAAgC;AACvC,QAAM,MAAM,KAAK,IAAI;AACrB,aAAW,CAAC,KAAK,MAAM,KAAK,aAAa;AACvC,QAAI,UAAU,KAAK;AACjB,kBAAY,OAAO,GAAG;AAAA,IACxB;AAAA,EACF;AACF;AAEA,SAAS,eAAuB;AAC9B,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,mBAA2B;AAClC,QAAM,MAAM,QAAQ,IAAI;AACxB,MAAI,KAAK;AACP,UAAM,SAAS,SAAS,KAAK,EAAE;AAC/B,QAAI,CAAC,MAAM,MAAM,KAAK,SAAS,GAAG;AAChC,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAMO,SAAS,YAAY,SAG1B;AACA,QAAM,SAAS,aAAa;AAC5B,QAAM,gBAAgB,iBAAiB;AACvC,QAAM,MAAM,GAAG,KAAK,IAAI,CAAC,IAAI,YAAY,EAAE,EAAE,SAAS,KAAK,CAAC;AAE5D,QAAM,QAAQ,IAAI;AAAA,IAChB,EAAE,WAAW,QAAQ,WAAW,MAAM,QAAQ,MAAM,IAAI;AAAA,IACxD;AAAA,IACA,EAAE,WAAW,cAAc;AAAA,EAC7B;AAEA,QAAM,YAAY,KAAK,IAAI,IAAI,gBAAgB;AAC/C,SAAO,EAAE,OAAO,UAAU;AAC5B;AAOO,SAAS,YAAY,OAAqB;AAC/C,MAAI;AACF,UAAM,SAAS,aAAa;AAC5B,UAAM,UAAU,IAAI,OAAO,OAAO,MAAM;AAIxC,QAAI,QAAQ,KAAK;AACf,YAAM,SAAS,QAAQ,MAAM,QAAQ,MAAM,MAAO,KAAK,IAAI;AAC3D,kBAAY,IAAI,QAAQ,KAAK,MAAM;AACnC,8BAAwB;AAAA,IAC1B;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAOO,SAAS,YACd,KACA,KACA,MACM;AACN,QAAM,aAAa,IAAI,QAAQ;AAC/B,MAAI,CAAC,cAAc,CAAC,WAAW,WAAW,SAAS,GAAG;AACpD,QAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,4CAA4C,CAAC;AAC3E;AAAA,EACF;AAEA,QAAM,QAAQ,WAAW,MAAM,CAAC;AAChC,MAAI;AACF,UAAM,SAAS,aAAa;AAC5B,UAAM,UAAU,IAAI,OAAO,OAAO,MAAM;AAMxC,QAAI,QAAQ,OAAO,YAAY,IAAI,QAAQ,GAAG,GAAG;AAC/C,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,yBAAyB,CAAC;AACxD;AAAA,IACF;AAEA,QAAI,QAAQ,EAAE,WAAW,QAAQ,WAAW,MAAM,QAAQ,KAAK;AAC/D,SAAK;AAAA,EACP,SAAS,KAAK;AACZ,QAAI,eAAe,IAAI,mBAAmB;AACxC,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,gBAAgB,CAAC;AAAA,IACjD,OAAO;AACL,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,gBAAgB,CAAC;AAAA,IACjD;AAAA,EACF;AACF;;;AC1IA,SAAS,cAAc;AAEvB,SAAS,iBAAiB;AAS1B,IAAM,eAAe,CAAC,QAA8C,UAAU;AAAA,EAC5E,UAAU;AAAA,EACV,OAAO;AAAA,EACP,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,SAAS,EAAE,OAAO,2CAAsC;AAC1D,CAAC;AAeD,SAAS,qBACP,UACA,QACM;AACN,QAAM,MAAM,KAAK,IAAI;AACrB,aAAW,CAAC,WAAW,OAAO,KAAK,UAAU;AAC3C,QAAI,QAAQ,aAAa,KAAK;AAC5B,eAAS,OAAO,SAAS;AACzB,aAAO,OAAO,SAAS;AAAA,IACzB;AAAA,EACF;AACF;AAsDO,SAAS,iBACd,OACA,QACA,UACA,WACA,WACA,eAAe,IACf,cACA,oBAAoB,IAAI,KAAK,KAAK,KAAK,KAC/B;AACR,QAAM,SAAS,OAAO;AACtB,SAAO,IAAI,aAAa,YAAY,CAAC;AAErC,QAAM,GAAG,mBAAmB,CAAC,MAAM,IAAI,aAAa;AAClD,QAAI,CAAC,SAAS,IAAI,EAAE,EAAG;AACvB,UAAM,MAAM;AAOZ,UAAM,MAAuB;AAAA,MAC3B,IAAI,IAAI;AAAA,MACR;AAAA,MACA,MAAM,IAAI;AAAA,MACV,SAAS,IAAI;AAAA,MACb,WAAW,IAAI;AAAA,MACf,WAAW,IAAI;AAAA,IACjB;AACA,WAAO,IAAI,IAAI,GAAG;AAClB,kBAAc,IAAI,IAAI,GAAG;AAAA,EAC3B,CAAC;AAED,SAAO,KAAK,gBAAgB,OAAO,KAAc,QAAkB;AACjE,UAAM,EAAE,WAAW,YAAY,SAAS,IAAI,IAAI;AAMhD,QAAI,CAAC,aAAa,OAAO,cAAc,UAAU;AAC/C,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,wBAAwB,CAAC;AACvD;AAAA,IACF;AACA,QAAI,CAAC,cAAc,OAAO,eAAe,UAAU;AACjD,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,yBAAyB,CAAC;AACxD;AAAA,IACF;AAEA,UAAM,eAAe;AAAA,MACnB;AAAA,MACA;AAAA,MACA,CAAC,SAAS;AAAA,MACV;AAAA,MACA,EAAE,WAAW,WAAW;AAAA,MACxB,KAAK,IAAI;AAAA,IACX;AACA,UAAM,eAAe,UAAU,YAAY;AAC3C,QAAI,CAAC,aAAa,OAAO;AACvB,UACG,OAAO,GAAG,EACV,KAAK,EAAE,OAAO,mCAAmC,aAAa,OAAO,CAAC;AACzE;AAAA,IACF;AAEA,UAAM,EAAE,OAAO,UAAU,IAAI,YAAY,EAAE,UAAU,CAAC;AACtD,yBAAqB,UAAU,MAAM;AAErC,UAAM,UAAuB;AAAA,MAC3B;AAAA,MACA;AAAA,MACA;AAAA,MACA,cAAc,KAAK,IAAI;AAAA,MACvB;AAAA,MACA;AAAA,IACF;AACA,aAAS,IAAI,WAAW,OAAO;AAE/B,UAAM,WAAW,MAAM,UAAU;AACjC,UAAM,QAAwD,CAAC;AAC/D,eAAW,SAAS,SAAS,OAAO,GAAG;AACrC,UAAI,MAAM,cAAc,WAAW;AACjC,cAAM,KAAK;AAAA,UACT,WAAW,MAAM;AAAA,UACjB,UAAU,MAAM;AAAA,QAClB,CAAC;AAAA,MACH;AAAA,IACF;AACA,eAAW,KAAK,SAAS,OAAO,GAAG;AACjC,UAAI,EAAE,cAAc,aAAa,CAAC,SAAS,IAAI,EAAE,SAAS,GAAG;AAC3D,cAAM,KAAK;AAAA,UACT,WAAW,EAAE;AAAA,UACb,UAAU,EAAE;AAAA,QACd,CAAC;AAAA,MACH;AAAA,IACF;AAEA,QAAI,KAAK,EAAE,OAAO,WAAW,MAAM,CAAC;AAAA,EACtC,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,OAAO,KAA2B,QAAkB;AAClD,YAAM,EAAE,IAAI,MAAM,SAAS,UAAU,IAAI,IAAI;AAO7C,UAAI,CAAC,MAAM,OAAO,OAAO,UAAU;AACjC,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,iBAAiB,CAAC;AAChD;AAAA,MACF;AACA,UAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;AACrC,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,mBAAmB,CAAC;AAClD;AAAA,MACF;AACA,UAAI,YAAY,QAAW;AACzB,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,sBAAsB,CAAC;AACrD;AAAA,MACF;AAEA,YAAM,kBAAkB,IAAI,MAAO;AACnC,YAAM,UAAU,SAAS,IAAI,eAAe;AAC5C,UAAI,CAAC,SAAS;AACZ,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,8CAAyC,CAAC;AACxE;AAAA,MACF;AAEA,YAAM,WAAW;AAAA,QACf;AAAA,QACA;AAAA,QACA,CAAC,EAAE;AAAA,QACH,QAAQ;AAAA,QACR;AAAA,QACA,KAAK,IAAI;AAAA,QACT;AAAA,MACF;AAEA,YAAM,WAAW,MAAM,UAAU;AACjC,YAAM,cAAc,SAAS,IAAI,EAAE;AACnC,UAAI,eAAe,YAAY,QAAQ;AACrC,cAAM,KAAK,YAAY;AACvB,cAAM,OAAO;AACb,YAAI,GAAG,eAAe,MAAM;AAC1B,cAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,mCAAmC,CAAC;AAClE;AAAA,QACF;AACA,YAAI;AACF,gBAAM,WAAW,KAAK,UAAU;AAAA,YAC9B,MAAM;AAAA,YACN,MAAM;AAAA,YACN;AAAA,UACF,CAAC;AACD,aAAG,KAAK,QAAQ;AAChB,cAAI,KAAK,EAAE,IAAI,MAAM,YAAY,SAAS,GAAG,CAAC;AAC9C;AAAA,QACF,SAAS,KAAK;AACZ,cAAI,OAAO,GAAG,EAAE,KAAK;AAAA,YACnB,OACE,iCACC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,UACpD,CAAC;AACD;AAAA,QACF;AAAA,MACF;AAEA,YAAM,gBAAgB,SAAS,IAAI,EAAE;AACrC,UAAI,eAAe;AACjB,cAAM,MAAuB;AAAA,UAC3B,IAAI,SAAS;AAAA,UACb,MAAM;AAAA,UACN,MAAM,SAAS;AAAA,UACf,SAAS,SAAS;AAAA,UAClB,WAAW,SAAS;AAAA,UACpB,WAAW,SAAS;AAAA,QACtB;AACA,eAAO,IAAI,IAAI,GAAG;AAClB,YAAI,KAAK,EAAE,IAAI,MAAM,YAAY,SAAS,GAAG,CAAC;AAC9C;AAAA,MACF;AAEA,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,4BAA4B,EAAE,GAAG,CAAC;AAAA,IAClE;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,CAAC,KAA2B,QAAkB;AAC5C,YAAM,kBAAkB,IAAI,MAAO;AACnC,YAAM,WAAW,MAAM,UAAU;AACjC,YAAM,WAID,CAAC;AAEN,iBAAW,SAAS,SAAS,OAAO,GAAG;AACrC,YAAI,MAAM,cAAc,iBAAiB;AACvC,mBAAS,KAAK;AAAA,YACZ,WAAW,MAAM;AAAA,YACjB,UAAU,MAAM;AAAA,YAChB,UAAU,MAAM;AAAA,UAClB,CAAC;AAAA,QACH;AAAA,MACF;AAEA,iBAAW,KAAK,SAAS,OAAO,GAAG;AACjC,YAAI,EAAE,cAAc,mBAAmB,CAAC,SAAS,IAAI,EAAE,SAAS,GAAG;AACjE,mBAAS,KAAK;AAAA,YACZ,WAAW,EAAE;AAAA,YACb,UAAU,EAAE;AAAA,YACZ,UAAU,EAAE;AAAA,UACd,CAAC;AAAA,QACH;AAAA,MACF;AAEA,UAAI,KAAK,EAAE,OAAO,SAAS,CAAC;AAAA,IAC9B;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,CAAC,KAA2B,QAAkB;AAC5C,YAAM,YAAY,IAAI,MAAO;AAC7B,YAAM,WAAW,IAAI,MAAM;AAC3B,YAAM,WAAW,IAAI,MAAM;AAE3B,YAAM,QAAQ,WAAW,SAAS,UAAU,EAAE,IAAI;AAClD,YAAM,QAAQ,KAAK,IAAI,WAAW,SAAS,UAAU,EAAE,IAAI,IAAI,GAAG;AAElE,UAAI,WAAW,OAAO,IAAI,WAAW,KAAK;AAC1C,YAAM,UAAU,SAAS,SAAS;AAClC,UAAI,SAAS;AACX,mBAAW,SAAS,MAAM,GAAG,KAAK;AAAA,MACpC;AAEA,UAAI,UAAU,QAAW;AACvB,eAAO,MAAM,SAAS;AAAA,MACxB;AAEA,UAAI,KAAK,EAAE,UAAU,QAAQ,CAAC;AAAA,IAChC;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,CAAC,KAA2B,QAAkB;AAC5C,UAAI,CAAC,cAAc;AACjB,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,+CAA+C,CAAC;AAC9E;AAAA,MACF;AAEA,YAAM,YAAY,IAAI,MAAO;AAC7B,YAAM,WAAW,IAAI,MAAM;AAC3B,YAAM,WAAW,IAAI,MAAM;AAE3B,UAAI,CAAC,UAAU;AACb,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,0DAA0D,CAAC;AACzF;AAAA,MACF;AAEA,YAAM,YAAY,IAAI,KAAK,QAAQ;AACnC,UAAI,MAAM,UAAU,QAAQ,CAAC,GAAG;AAC9B,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,iEAA4D,CAAC;AAC3F;AAAA,MACF;AAEA,YAAM,UAAU,UAAU,QAAQ;AAClC,YAAM,gBAAgB,KAAK,IAAI,IAAI;AAEnC,UAAI,UAAU,eAAe;AAC3B,YAAI,OAAO,GAAG,EAAE,KAAK;AAAA,UACnB,OAAO;AAAA,UACP,SAAS,+BAA+B,KAAK,MAAM,oBAAoB,KAAQ,CAAC;AAAA,QAClF,CAAC;AACD;AAAA,MACF;AAEA,UAAI,QAAQ;AACZ,UAAI,aAAa,QAAW;AAC1B,cAAM,cAAc,SAAS,UAAU,EAAE;AACzC,YAAI,MAAM,WAAW,KAAK,cAAc,GAAG;AACzC,cAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,0DAAqD,CAAC;AACpF;AAAA,QACF;AACA,gBAAQ,KAAK,IAAI,aAAa,GAAG;AAAA,MACnC;AAEA,UAAI,WAAW,aAAa,IAAI,WAAW,OAAO;AAClD,YAAM,UAAU,SAAS,SAAS;AAClC,UAAI,SAAS;AACX,mBAAW,SAAS,MAAM,GAAG,KAAK;AAAA,MACpC;AAEA,UAAI,KAAK,EAAE,UAAU,QAAQ,CAAC;AAAA,IAChC;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,CAAC,KAA2B,QAAkB;AAC5C,YAAM,YAAY,IAAI,MAAO;AAC7B,YAAM,aAAa,IAAI,QAAQ;AAC/B,YAAM,QAAQ,WAAW,MAAM,CAAC;AAEhC,kBAAY,KAAK;AACjB,eAAS,OAAO,SAAS;AACzB,aAAO,OAAO,SAAS;AAEvB,UAAI,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,IACvB;AAAA,EACF;AAEA,SAAO;AACT;;;AC7ZA,OAAO,UAAU;AACjB,OAAO,aAAa;AACpB,OAAO,UAAU;AAajB,IAAM,wBAA0C,CAC9C,MACA,MACA,IACA,YACA,SACA,WACA,cAEA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa,KAAK,IAAI;AAAA,EACtB;AAAA,EACA;AACF;AAiBF,eAAsB,SAAS,UAA2B,CAAC,GAGxD;AACD,QAAM,SAAS,QAAQ,UAAU;AAAA,IAC/B,QAAQ,IAAI,cAAc,QAAQ,IAAI,QAAQ;AAAA,IAAQ;AAAA,EACxD;AACA,QAAM,YAAY,QAAQ,IAAI,cAAc,QAAQ,IAAI;AACxD,QAAM,aACJ,QAAQ,eACP,OAAO,cAAc,YAAY,UAAU,SAAS;AAEvD,QAAM,WAAW,SAAS,QAAQ,IAAI,aAAa,OAAO,EAAE;AAC5D,QAAM,eAAmC,EAAE,GAAG,QAAQ,cAAc,SAAS;AAE7E,QAAM,QAAQ,IAAI,YAAY,YAAY;AAC1C,QAAM,MAAM,MAAM,MAAM;AAExB,MAAI,CAAC,YAAY;AACf,WAAO,EAAE,MAAM;AAAA,EACjB;AAEA,MAAI,CAAC,WAAW;AACd,UAAM,MAAM,KAAK;AACjB,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAGA,MAAI,CAAC,QAAQ,IAAI,wBAAwB;AACvC,YAAQ,IAAI,yBAAyB;AAAA,EACvC;AAEA,QAAM,WAAW,QAAQ,YAAY,SAAS,QAAQ,IAAI,aAAa,QAAQ,EAAE;AACjF,QAAM,eAAe,SAAS,QAAQ,IAAI,kBAAkB,YAAY,EAAE;AAC1E,QAAM,gBAAgB,IAAI,cAAc,EAAE,OAAO,aAAa,CAAC;AAC/D,QAAM,oBAAoB,IAAI,KAAK,KAAK,KAAK;AAC7C,QAAM,eAAe,IAAI,cAAc,EAAE,OAAO,mBAAmB,aAAa,IAAM,CAAC;AACvF,QAAM,eAAe,oBAAI,IAAyB;AAElD,QAAM,iBAAiB,QAAQ,IAAI,mBAAmB;AACtD,QAAM,cAAc,mBAAmB,MACnC,MACA,eAAe,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAE7E,QAAM,MAAM,QAAQ;AACpB,MAAI,IAAI,KAAK;AAAA,IACX,QAAQ;AAAA,IACR,SAAS,CAAC,OAAO,QAAQ,QAAQ;AAAA,IACjC,gBAAgB,CAAC,gBAAgB,eAAe;AAAA,EAClD,CAAC,CAAC;AACF,MAAI,IAAI,QAAQ,KAAK,CAAC;AAEtB,QAAM,gBAAgB,CAAC,aACrB,eAAe,QAAoB;AAErC,QAAM,eAAe,SAAS,QAAQ,IAAI,kBAAkB,MAAM,EAAE;AACpE,QAAM,SAAS;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,MAAI,IAAI,MAAM;AAEd,MAAI,IAAI,CAAC,MAAM,QAAQ;AACrB,QAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,YAAY,CAAC;AAAA,EAC7C,CAAC;AAED,QAAM,aAAa,KAAK,aAAa,GAAG;AACxC,QAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,eAAW,OAAO,UAAU,MAAM,QAAQ,CAAC;AAC3C,eAAW,GAAG,SAAS,MAAM;AAAA,EAC/B,CAAC;AAED,SAAO,EAAE,OAAO,WAAW;AAC7B;","names":[]}
package/dist/index.d.ts CHANGED
@@ -999,14 +999,16 @@ interface BufferedMessage {
999
999
  }
1000
1000
  /**
1001
1001
  * MessageBuffer stores inbound messages per agent public key.
1002
- * FIFO eviction when the buffer is full (max 100 messages).
1002
+ * FIFO eviction when the buffer is full (max maxMessages messages).
1003
1003
  * Messages older than ttlMs (measured from when they were received) are pruned on access.
1004
1004
  */
1005
1005
  declare class MessageBuffer {
1006
1006
  private buffers;
1007
1007
  private ttlMs;
1008
+ private maxMessages;
1008
1009
  constructor(options?: {
1009
1010
  ttlMs?: number;
1011
+ maxMessages?: number;
1010
1012
  });
1011
1013
  /**
1012
1014
  * Add a message to an agent's buffer.
@@ -1151,11 +1153,12 @@ declare function requireAuth(req: AuthenticatedRequest, res: Response, next: Nex
1151
1153
  * rest-api.ts — Express router implementing the Agora relay REST API.
1152
1154
  *
1153
1155
  * Endpoints:
1154
- * POST /v1/register — Register agent, obtain JWT session token
1155
- * POST /v1/send — Send message to a peer (requires auth)
1156
- * GET /v1/peers — List online peers (requires auth)
1157
- * GET /v1/messages — Poll for new inbound messages (requires auth)
1158
- * DELETE /v1/disconnect Invalidate token and disconnect (requires auth)
1156
+ * POST /v1/register — Register agent, obtain JWT session token
1157
+ * POST /v1/send — Send message to a peer (requires auth)
1158
+ * GET /v1/peers — List online peers (requires auth)
1159
+ * GET /v1/messages — Poll for new inbound messages (requires auth)
1160
+ * GET /v1/messages/replay Replay messages since a timestamp (requires auth)
1161
+ * DELETE /v1/disconnect — Invalidate token and disconnect (requires auth)
1159
1162
  */
1160
1163
 
1161
1164
  /**
@@ -1211,7 +1214,7 @@ type VerifyEnvelopeFn = (envelope: unknown) => {
1211
1214
  /**
1212
1215
  * Create the REST API router.
1213
1216
  */
1214
- declare function createRestRouter(relay: RelayInterface, buffer: MessageBuffer, sessions: Map<string, RestSession>, createEnv: CreateEnvelopeFn, verifyEnv: VerifyEnvelopeFn, rateLimitRpm?: number): Router;
1217
+ declare function createRestRouter(relay: RelayInterface, buffer: MessageBuffer, sessions: Map<string, RestSession>, createEnv: CreateEnvelopeFn, verifyEnv: VerifyEnvelopeFn, rateLimitRpm?: number, replayBuffer?: MessageBuffer, replayRetentionMs?: number): Router;
1215
1218
 
1216
1219
  /**
1217
1220
  * run-relay.ts — Start Agora relay: WebSocket server and optional REST API.
package/dist/index.js CHANGED
@@ -55,7 +55,7 @@ import {
55
55
  requireAuth,
56
56
  revokeToken,
57
57
  runRelay
58
- } from "./chunk-RG3ZM57F.js";
58
+ } from "./chunk-52RKJA35.js";
59
59
  import {
60
60
  MessageStore,
61
61
  RelayServer,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runRelay
3
- } from "../chunk-RG3ZM57F.js";
3
+ } from "../chunk-52RKJA35.js";
4
4
  import "../chunk-EFX36SN3.js";
5
5
 
6
6
  // src/relay/relay-server.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rookdaemon/agora",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "A coordination network for AI agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1 +0,0 @@
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 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 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 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 from: string,\n to: string[],\n privateKey: string,\n payload: unknown,\n timestamp?: number,\n inReplyTo?: string\n) => {\n id: string;\n type: string;\n from: string;\n to: 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 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 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, metadata } = req.body as {\n publicKey?: string;\n privateKey?: 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 [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 });\n pruneExpiredSessions(sessions, buffer);\n\n const session: RestSession = {\n publicKey,\n privateKey,\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; lastSeen: number }> = [];\n for (const agent of wsAgents.values()) {\n if (agent.publicKey !== publicKey) {\n peers.push({\n publicKey: agent.publicKey,\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 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 [to],\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 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 msg: BufferedMessage = {\n id: envelope.id,\n from: senderPublicKey,\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: ${to}` });\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 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 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 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 from,\n to,\n privateKey,\n payload,\n timestamp,\n inReplyTo\n) =>\n createEnvelope(\n type as MessageType,\n from,\n privateKey,\n payload,\n timestamp ?? Date.now(),\n inReplyTo,\n to\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":[]}