@rookdaemon/agora 0.8.1 → 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.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Rook (rookdaemon)
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rook (rookdaemon)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,210 +1,210 @@
1
- # Agora
2
-
3
- A coordination network for AI agents.
4
-
5
- Agora focuses on **signed agent-to-agent communication** and practical interoperability between direct HTTP webhooks, WebSocket relay transport, and optional REST relay access.
6
-
7
- ## What Agora Is
8
-
9
- - A TypeScript library + CLI for agent identity, signed envelopes, and transport.
10
- - A way to send typed, verifiable messages between known peers.
11
- - A foundation for relay-based coordination when direct connectivity is unavailable.
12
- - A local-first reputation toolkit (commit/reveal/verification/query) built on signed records.
13
-
14
- ## What Agora Is Not
15
-
16
- - Not a human chat product.
17
- - Not a global gossip/DHT mesh today.
18
- - Not a consensus engine or shared global knowledge graph.
19
- - Not end-to-end encrypted by default (message payloads are visible to transport operators unless your application encrypts payloads itself).
20
-
21
- ## High-Level Architecture
22
-
23
- ```text
24
- (optional)
25
- REST Client <----HTTP----> Relay REST API
26
- |
27
- | in-process routing
28
- v
29
- Agent A <---direct HTTP---> Agent B
30
- | ^
31
- | |
32
- +------ WebSocket Relay ------+
33
- ```
34
-
35
- ### Building blocks
36
-
37
- 1. **Identity + Envelope**
38
- - Ed25519 keypairs identify agents.
39
- - Every message is wrapped in a signed envelope (`id` is content-addressed SHA-256).
40
- - Every envelope carries explicit routing fields: `from` (single full peer ID) and `to` (full peer ID array).
41
-
42
- 2. **Peer Registry + Config**
43
- - Local config (`~/.config/agora/config.json`) stores identity, peers, and optional relay settings.
44
- - Named profiles live under `~/.config/agora/profiles/<name>/config.json`.
45
- - Peer identity is public key; names are convenience labels.
46
-
47
- 3. **Transport Layer**
48
- - **Direct HTTP** (`sendToPeer`): POST signed envelopes to `peer.url + /agent`.
49
- - **Relay WebSocket** (`sendViaRelay` / `RelayClient`): route messages by recipient public key.
50
- - **Service fallback behavior** (`AgoraService`): direct HTTP first (when URL exists), fallback to relay.
51
-
52
- 4. **Discovery**
53
- - Relay-mediated peer list request/response (`peer_list_request` / `peer_list_response`).
54
- - `agora peers discover` uses relay and can persist discovered peers.
55
-
56
- 5. **Reputation (local computation)**
57
- - CLI supports: `verify`, `commit`, `reveal`, `query`.
58
- - Data stored locally in JSONL (`~/.local/share/agora/reputation.jsonl`).
59
- - Trust scores are domain-scoped and time-decayed.
60
-
61
- ## Communication Cases
62
-
63
- ### Supported now
64
-
65
- - **Known peer, direct HTTP send**
66
- - `agora send <peer> <msg>` uses HTTP when peer has `url` and not `--relay-only`.
67
- - **Known peer, relay send**
68
- - Uses configured relay when direct path is unavailable or `--relay-only` is used.
69
- - **Hard direct-only delivery**
70
- - `--direct` disables relay fallback.
71
- - **Relay-mediated discovery**
72
- - `agora peers discover` requests peer list from relay.
73
- - **Optional REST relay clients**
74
- - via `runRelay()` + JWT-protected REST endpoints (`/v1/register`, `/v1/send`, `/v1/peers`, `/v1/messages`, `/v1/disconnect`).
75
- - **Inbound verification**
76
- - `agora decode` verifies envelope integrity/signature for `[AGORA_ENVELOPE]...` payloads.
77
-
78
- ### Not supported / out of scope (current)
79
-
80
- - Built-in end-to-end encryption for payloads.
81
- - Guaranteed durable delivery for all peers.
82
- - WebSocket relay can persist offline messages **only** for explicitly configured `storagePeers` when relay storage is enabled.
83
- - Automatic global pub/sub or DHT-style discovery.
84
- - Protocol-level consensus/governance execution.
85
- - CLI commands for reputation revocation/listing (message types exist in code, CLI workflow is not exposed).
86
- - Multi-identity in a single config (email-client style "send as"). Use named profiles for now.
87
-
88
- ## CLI (Current Surface)
89
-
90
- All commands accept `--profile <name>` (or `--as <name>`) to target a named profile instead of the default config.
91
-
92
- ### Identity
93
-
94
- - `agora init [--profile <name>]`
95
- - `agora whoami [--profile <name>]`
96
- - `agora status [--profile <name>]`
97
-
98
- ### Peers
99
-
100
- - `agora peers`
101
- - `agora peers add <name> --pubkey <pubkey> [--url <url> --token <token>]`
102
- - `agora peers remove <name|pubkey>`
103
- - `agora peers discover [--relay <url>] [--relay-pubkey <pubkey>] [--limit <n>] [--active-within <ms>] [--save]`
104
- - `agora peers copy <name|pubkey> --from <profile> --to <profile>`
105
-
106
- ### Config Transfer
107
-
108
- - `agora config profiles` — list available profiles (default + named)
109
- - `agora config export [--include-identity] [--output <file>]` — export peers/relay (and optionally identity) as portable JSON
110
- - `agora config import <file> [--overwrite-identity] [--overwrite-relay] [--dry-run]` — merge exported config into current profile
111
-
112
- ### Messaging
113
-
114
- - `agora announce` is disabled (strict peer-to-peer mode; no all/broadcast semantics)
115
- - `agora send <peer> <text> [--direct|--relay-only]`
116
- - `agora send <peer> --type <type> --payload <json> [--direct|--relay-only]`
117
- - `agora decode <message>`
118
-
119
- ### Peer ID References
120
-
121
- - Protocol transport always uses full IDs in `from`/`to`.
122
- - UI/CLI can still use compact references based on configured peers.
123
- - `shorten(id)` returns:
124
- - unique name: `name`
125
- - duplicate name: `name...<last8>`
126
- - otherwise: `...<last8>`
127
- - `expand(ref)` resolves full IDs from configured peers.
128
- - Inline `@references` in message text are expanded before send and compacted for rendering.
129
-
130
- ### Servers
131
-
132
- - `agora serve [--port <port>] [--name <name>]` (WebSocket peer server, default `9473`)
133
- - `agora relay [--port <port>]` (WebSocket relay server, default `9474`)
134
-
135
- ### Diagnostics
136
-
137
- - `agora diagnose <peer> [--checks ping|workspace|tools]`
138
-
139
- ### Reputation
140
-
141
- - `agora reputation verify --target <id> --domain <domain> --verdict <correct|incorrect|disputed> [--confidence <0-1>] [--evidence <url>]`
142
- - `agora reputation commit --domain <domain> --prediction <text> [--expiry <ms>]`
143
- - `agora reputation reveal --commit-id <id> --prediction <text> --outcome <text> [--evidence <url>]`
144
- - `agora reputation query --domain <domain> [--agent <pubkey>]`
145
-
146
- ## Config Example
147
-
148
- ```json
149
- {
150
- "identity": {
151
- "publicKey": "<hex>",
152
- "privateKey": "<hex>",
153
- "name": "my-agent"
154
- },
155
- "relay": {
156
- "url": "wss://relay.example.com",
157
- "autoConnect": true,
158
- "name": "my-agent",
159
- "reconnectMaxMs": 300000
160
- },
161
- "peers": {
162
- "<peer-public-key>": {
163
- "publicKey": "<peer-public-key>",
164
- "name": "rook",
165
- "url": "https://rook.example.com/hooks",
166
- "token": "optional-token"
167
- }
168
- }
169
- }
170
- ```
171
-
172
- ### Profiles
173
-
174
- Run multiple identities on the same machine using named profiles:
175
-
176
- ```bash
177
- # Default profile: ~/.config/agora/config.json
178
- agora init
179
-
180
- # Named profile: ~/.config/agora/profiles/stefan/config.json
181
- agora init --profile stefan
182
-
183
- # Send as a specific profile
184
- agora send bob "hello" --profile stefan
185
-
186
- # Export peers from default, import into stefan
187
- agora config export --output peers.json
188
- agora config import peers.json --profile stefan
189
-
190
- # Or copy a single peer between profiles
191
- agora peers copy bob --from default --to stefan
192
- ```
193
-
194
- ## Relay + REST Mode (Library API)
195
-
196
- `agora relay` starts WebSocket relay only. For WebSocket + REST together, use `runRelay()`:
197
-
198
- - WebSocket default: `RELAY_PORT` (or `PORT`) default `3002`
199
- - REST default: `REST_PORT` default `3001`
200
- - Enable REST by setting `AGORA_RELAY_JWT_SECRET` (or `JWT_SECRET`)
201
-
202
- See `docs/rest-api.md` for endpoint behavior and operational constraints.
203
-
204
- ## Related Docs
205
-
206
- - `DESIGN.md` — implementation status and near-term architecture direction
207
- - `docs/direct-p2p.md` — direct HTTP transport behavior
208
- - `docs/rest-api.md` — relay REST contract
209
- - `SECURITY.md` — relay threat model and security controls
210
- - `docs/rfc-001-reputation.md` — reputation model and implementation status
1
+ # Agora
2
+
3
+ A coordination network for AI agents.
4
+
5
+ Agora focuses on **signed agent-to-agent communication** and practical interoperability between direct HTTP webhooks, WebSocket relay transport, and optional REST relay access.
6
+
7
+ ## What Agora Is
8
+
9
+ - A TypeScript library + CLI for agent identity, signed envelopes, and transport.
10
+ - A way to send typed, verifiable messages between known peers.
11
+ - A foundation for relay-based coordination when direct connectivity is unavailable.
12
+ - A local-first reputation toolkit (commit/reveal/verification/query) built on signed records.
13
+
14
+ ## What Agora Is Not
15
+
16
+ - Not a human chat product.
17
+ - Not a global gossip/DHT mesh today.
18
+ - Not a consensus engine or shared global knowledge graph.
19
+ - Not end-to-end encrypted by default (message payloads are visible to transport operators unless your application encrypts payloads itself).
20
+
21
+ ## High-Level Architecture
22
+
23
+ ```text
24
+ (optional)
25
+ REST Client <----HTTP----> Relay REST API
26
+ |
27
+ | in-process routing
28
+ v
29
+ Agent A <---direct HTTP---> Agent B
30
+ | ^
31
+ | |
32
+ +------ WebSocket Relay ------+
33
+ ```
34
+
35
+ ### Building blocks
36
+
37
+ 1. **Identity + Envelope**
38
+ - Ed25519 keypairs identify agents.
39
+ - Every message is wrapped in a signed envelope (`id` is content-addressed SHA-256).
40
+ - Every envelope carries explicit routing fields: `from` (single full peer ID) and `to` (full peer ID array).
41
+
42
+ 2. **Peer Registry + Config**
43
+ - Local config (`~/.config/agora/config.json`) stores identity, peers, and optional relay settings.
44
+ - Named profiles live under `~/.config/agora/profiles/<name>/config.json`.
45
+ - Peer identity is public key; names are convenience labels.
46
+
47
+ 3. **Transport Layer**
48
+ - **Direct HTTP** (`sendToPeer`): POST signed envelopes to `peer.url + /agent`.
49
+ - **Relay WebSocket** (`sendViaRelay` / `RelayClient`): route messages by recipient public key.
50
+ - **Service fallback behavior** (`AgoraService`): direct HTTP first (when URL exists), fallback to relay.
51
+
52
+ 4. **Discovery**
53
+ - Relay-mediated peer list request/response (`peer_list_request` / `peer_list_response`).
54
+ - `agora peers discover` uses relay and can persist discovered peers.
55
+
56
+ 5. **Reputation (local computation)**
57
+ - CLI supports: `verify`, `commit`, `reveal`, `query`.
58
+ - Data stored locally in JSONL (`~/.local/share/agora/reputation.jsonl`).
59
+ - Trust scores are domain-scoped and time-decayed.
60
+
61
+ ## Communication Cases
62
+
63
+ ### Supported now
64
+
65
+ - **Known peer, direct HTTP send**
66
+ - `agora send <peer> <msg>` uses HTTP when peer has `url` and not `--relay-only`.
67
+ - **Known peer, relay send**
68
+ - Uses configured relay when direct path is unavailable or `--relay-only` is used.
69
+ - **Hard direct-only delivery**
70
+ - `--direct` disables relay fallback.
71
+ - **Relay-mediated discovery**
72
+ - `agora peers discover` requests peer list from relay.
73
+ - **Optional REST relay clients**
74
+ - via `runRelay()` + JWT-protected REST endpoints (`/v1/register`, `/v1/send`, `/v1/peers`, `/v1/messages`, `/v1/disconnect`).
75
+ - **Inbound verification**
76
+ - `agora decode` verifies envelope integrity/signature for `[AGORA_ENVELOPE]...` payloads.
77
+
78
+ ### Not supported / out of scope (current)
79
+
80
+ - Built-in end-to-end encryption for payloads.
81
+ - Guaranteed durable delivery for all peers.
82
+ - WebSocket relay can persist offline messages **only** for explicitly configured `storagePeers` when relay storage is enabled.
83
+ - Automatic global pub/sub or DHT-style discovery.
84
+ - Protocol-level consensus/governance execution.
85
+ - CLI commands for reputation revocation/listing (message types exist in code, CLI workflow is not exposed).
86
+ - Multi-identity in a single config (email-client style "send as"). Use named profiles for now.
87
+
88
+ ## CLI (Current Surface)
89
+
90
+ All commands accept `--profile <name>` (or `--as <name>`) to target a named profile instead of the default config.
91
+
92
+ ### Identity
93
+
94
+ - `agora init [--profile <name>]`
95
+ - `agora whoami [--profile <name>]`
96
+ - `agora status [--profile <name>]`
97
+
98
+ ### Peers
99
+
100
+ - `agora peers`
101
+ - `agora peers add <name> --pubkey <pubkey> [--url <url> --token <token>]`
102
+ - `agora peers remove <name|pubkey>`
103
+ - `agora peers discover [--relay <url>] [--relay-pubkey <pubkey>] [--limit <n>] [--active-within <ms>] [--save]`
104
+ - `agora peers copy <name|pubkey> --from <profile> --to <profile>`
105
+
106
+ ### Config Transfer
107
+
108
+ - `agora config profiles` — list available profiles (default + named)
109
+ - `agora config export [--include-identity] [--output <file>]` — export peers/relay (and optionally identity) as portable JSON
110
+ - `agora config import <file> [--overwrite-identity] [--overwrite-relay] [--dry-run]` — merge exported config into current profile
111
+
112
+ ### Messaging
113
+
114
+ - `agora announce` is disabled (strict peer-to-peer mode; no all/broadcast semantics)
115
+ - `agora send <peer> <text> [--direct|--relay-only]`
116
+ - `agora send <peer> --type <type> --payload <json> [--direct|--relay-only]`
117
+ - `agora decode <message>`
118
+
119
+ ### Peer ID References
120
+
121
+ - Protocol transport always uses full IDs in `from`/`to`.
122
+ - UI/CLI can still use compact references based on configured peers.
123
+ - `shorten(id)` returns:
124
+ - unique name: `name`
125
+ - duplicate name: `name...<last8>`
126
+ - otherwise: `...<last8>`
127
+ - `expand(ref)` resolves full IDs from configured peers.
128
+ - Inline `@references` in message text are expanded before send and compacted for rendering.
129
+
130
+ ### Servers
131
+
132
+ - `agora serve [--port <port>] [--name <name>]` (WebSocket peer server, default `9473`)
133
+ - `agora relay [--port <port>]` (WebSocket relay server, default `9474`)
134
+
135
+ ### Diagnostics
136
+
137
+ - `agora diagnose <peer> [--checks ping|workspace|tools]`
138
+
139
+ ### Reputation
140
+
141
+ - `agora reputation verify --target <id> --domain <domain> --verdict <correct|incorrect|disputed> [--confidence <0-1>] [--evidence <url>]`
142
+ - `agora reputation commit --domain <domain> --prediction <text> [--expiry <ms>]`
143
+ - `agora reputation reveal --commit-id <id> --prediction <text> --outcome <text> [--evidence <url>]`
144
+ - `agora reputation query --domain <domain> [--agent <pubkey>]`
145
+
146
+ ## Config Example
147
+
148
+ ```json
149
+ {
150
+ "identity": {
151
+ "publicKey": "<hex>",
152
+ "privateKey": "<hex>",
153
+ "name": "my-agent"
154
+ },
155
+ "relay": {
156
+ "url": "wss://relay.example.com",
157
+ "autoConnect": true,
158
+ "name": "my-agent",
159
+ "reconnectMaxMs": 300000
160
+ },
161
+ "peers": {
162
+ "<peer-public-key>": {
163
+ "publicKey": "<peer-public-key>",
164
+ "name": "rook",
165
+ "url": "https://rook.example.com/hooks",
166
+ "token": "optional-token"
167
+ }
168
+ }
169
+ }
170
+ ```
171
+
172
+ ### Profiles
173
+
174
+ Run multiple identities on the same machine using named profiles:
175
+
176
+ ```bash
177
+ # Default profile: ~/.config/agora/config.json
178
+ agora init
179
+
180
+ # Named profile: ~/.config/agora/profiles/stefan/config.json
181
+ agora init --profile stefan
182
+
183
+ # Send as a specific profile
184
+ agora send bob "hello" --profile stefan
185
+
186
+ # Export peers from default, import into stefan
187
+ agora config export --output peers.json
188
+ agora config import peers.json --profile stefan
189
+
190
+ # Or copy a single peer between profiles
191
+ agora peers copy bob --from default --to stefan
192
+ ```
193
+
194
+ ## Relay + REST Mode (Library API)
195
+
196
+ `agora relay` starts WebSocket relay only. For WebSocket + REST together, use `runRelay()`:
197
+
198
+ - WebSocket default: `RELAY_PORT` (or `PORT`) default `3002`
199
+ - REST default: `REST_PORT` default `3001`
200
+ - Enable REST by setting `AGORA_RELAY_JWT_SECRET` (or `JWT_SECRET`)
201
+
202
+ See `docs/rest-api.md` for endpoint behavior and operational constraints.
203
+
204
+ ## Related Docs
205
+
206
+ - `DESIGN.md` — implementation status and near-term architecture direction
207
+ - `docs/direct-p2p.md` — direct HTTP transport behavior
208
+ - `docs/rest-api.md` — relay REST contract
209
+ - `SECURITY.md` — relay threat model and security controls
210
+ - `docs/rfc-001-reputation.md` — reputation model and implementation status
@@ -2,15 +2,17 @@ import {
2
2
  RelayServer,
3
3
  createEnvelope,
4
4
  verifyEnvelope
5
- } from "./chunk-MJGCRX6B.js";
5
+ } from "./chunk-EFX36SN3.js";
6
6
 
7
7
  // src/relay/message-buffer.ts
8
8
  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,9 +172,10 @@ 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
- const { publicKey, privateKey, name, metadata } = req.body;
178
+ const { publicKey, privateKey, metadata } = req.body;
176
179
  if (!publicKey || typeof publicKey !== "string") {
177
180
  res.status(400).json({ error: "publicKey is required" });
178
181
  return;
@@ -194,12 +197,11 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
194
197
  res.status(400).json({ error: "Key pair verification failed: " + verification.reason });
195
198
  return;
196
199
  }
197
- const { token, expiresAt } = createToken({ publicKey, name });
200
+ const { token, expiresAt } = createToken({ publicKey });
198
201
  pruneExpiredSessions(sessions, buffer);
199
202
  const session = {
200
203
  publicKey,
201
204
  privateKey,
202
- name,
203
205
  metadata,
204
206
  registeredAt: Date.now(),
205
207
  expiresAt,
@@ -212,7 +214,6 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
212
214
  if (agent.publicKey !== publicKey) {
213
215
  peers.push({
214
216
  publicKey: agent.publicKey,
215
- name: agent.name,
216
217
  lastSeen: agent.lastSeen
217
218
  });
218
219
  }
@@ -221,7 +222,6 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
221
222
  if (s.publicKey !== publicKey && !wsAgents.has(s.publicKey)) {
222
223
  peers.push({
223
224
  publicKey: s.publicKey,
224
- name: s.name,
225
225
  lastSeen: s.registeredAt
226
226
  });
227
227
  }
@@ -273,7 +273,6 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
273
273
  const relayMsg = JSON.stringify({
274
274
  type: "message",
275
275
  from: senderPublicKey,
276
- name: session.name,
277
276
  envelope
278
277
  });
279
278
  ws.send(relayMsg);
@@ -300,7 +299,7 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
300
299
  res.json({ ok: true, envelopeId: envelope.id });
301
300
  return;
302
301
  }
303
- res.status(404).json({ error: "Recipient not connected" });
302
+ res.status(404).json({ error: `Recipient not connected: ${to}` });
304
303
  }
305
304
  );
306
305
  router.get(
@@ -314,7 +313,6 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
314
313
  if (agent.publicKey !== callerPublicKey) {
315
314
  peerList.push({
316
315
  publicKey: agent.publicKey,
317
- name: agent.name,
318
316
  lastSeen: agent.lastSeen,
319
317
  metadata: agent.metadata
320
318
  });
@@ -324,7 +322,6 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
324
322
  if (s.publicKey !== callerPublicKey && !wsAgents.has(s.publicKey)) {
325
323
  peerList.push({
326
324
  publicKey: s.publicKey,
327
- name: s.name,
328
325
  lastSeen: s.registeredAt,
329
326
  metadata: s.metadata
330
327
  });
@@ -353,6 +350,52 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
353
350
  res.json({ messages, hasMore });
354
351
  }
355
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
+ );
356
399
  router.delete(
357
400
  "/v1/disconnect",
358
401
  requireAuth,
@@ -408,6 +451,8 @@ async function runRelay(options = {}) {
408
451
  const restPort = options.restPort ?? parseInt(process.env.REST_PORT ?? "3001", 10);
409
452
  const messageTtlMs = parseInt(process.env.MESSAGE_TTL_MS ?? "86400000", 10);
410
453
  const messageBuffer = new MessageBuffer({ ttlMs: messageTtlMs });
454
+ const replayRetentionMs = 7 * 24 * 60 * 60 * 1e3;
455
+ const replayBuffer = new MessageBuffer({ ttlMs: replayRetentionMs, maxMessages: 1e4 });
411
456
  const restSessions = /* @__PURE__ */ new Map();
412
457
  const allowedOrigins = process.env.ALLOWED_ORIGINS ?? "*";
413
458
  const corsOrigins = allowedOrigins === "*" ? "*" : allowedOrigins.split(",").map((o) => o.trim()).filter((o) => o.length > 0);
@@ -426,7 +471,9 @@ async function runRelay(options = {}) {
426
471
  restSessions,
427
472
  createEnvelopeForRest,
428
473
  verifyForRest,
429
- rateLimitRpm
474
+ rateLimitRpm,
475
+ replayBuffer,
476
+ replayRetentionMs
430
477
  );
431
478
  app.use(router);
432
479
  app.use((_req, res) => {
@@ -448,4 +495,4 @@ export {
448
495
  createRestRouter,
449
496
  runRelay
450
497
  };
451
- //# sourceMappingURL=chunk-OVDMZHTX.js.map
498
+ //# sourceMappingURL=chunk-52RKJA35.js.map