@particle-academy/agent-integrations 0.42.0 → 0.43.0

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 CHANGED
@@ -18,6 +18,35 @@ npm install @particle-academy/agent-integrations
18
18
  import "@particle-academy/agent-integrations/styles.css";
19
19
  ```
20
20
 
21
+ ## Which version range to depend on
22
+
23
+ **Use a caret. `^0.42.0` is correct here, and it is deliberate.**
24
+
25
+ ```jsonc
26
+ "@particle-academy/agent-integrations": "^0.42.0"
27
+ ```
28
+
29
+ This package is **pre-1.0, and breaking changes land in MINOR releases** — see
30
+ the note at the top of [`CHANGELOG.md`](./CHANGELOG.md). A caret on a `0.x` locks
31
+ the minor (npm reads `^0.42.0` as `>=0.42.0 <0.43.0`), so it gives you patches and
32
+ holds you at a surface you have already integrated against. That is the right
33
+ default when the next minor may change something under you.
34
+
35
+ **Moving up a minor is a deliberate act.** Read that version's `CHANGELOG.md`
36
+ entry before you do — breaking entries say what a consumer must actually DO, and
37
+ about half of what reads as breaking needs no action at all.
38
+
39
+ > **A note on the rest of the suite.** Some Fancy packages recommend an open
40
+ > `>=X <2.0` range instead. That is not an inconsistency: those packages carry a
41
+ > **runtime compatibility check** that fails loudly when a consumer is out of
42
+ > step — `fancy-connector-core`'s `CONNECTOR_API_VERSION` is the example — which
43
+ > is a stronger guarantee than a caret and makes the caret unnecessary. This
44
+ > package has no such check, so the caret is doing real work.
45
+ >
46
+ > The rule inside the suite differs again: first-party packages depend on each
47
+ > other with an open range because they are **released and tested together** at a
48
+ > kit version. A consumer is not, which is why the advice here is not the same.
49
+
21
50
  ## Tree-shake-safe imports
22
51
 
23
52
  The root barrel is **core-only** — MCP server, presence, undo, sharing,
@@ -64,15 +64,52 @@ var RelayBroker = class {
64
64
  this.subs.delete(id);
65
65
  return true;
66
66
  }
67
- /** Validate an authenticated touch and slide the TTL forward. */
68
- validate(id, token) {
69
- if (!id || !token) return false;
67
+ /**
68
+ * Validate an authenticated touch, saying WHY when it fails.
69
+ *
70
+ * `session_gone` and `invalid_token` are different facts and a client acts on
71
+ * them differently. A gone session means the page has closed or the TTL has
72
+ * elapsed — the NORMAL end of a relay lifecycle, since the browser is the
73
+ * server and no state persists across restarts. A bad token means the caller
74
+ * is wrong.
75
+ *
76
+ * They were collapsed into one boolean and reported as `invalid_token`, so an
77
+ * agent holding a perfectly good token against a closed page was told its
78
+ * credentials were wrong. Not a missing signal — an actively misleading one,
79
+ * which sends the reader to debug an auth path that was never the problem.
80
+ *
81
+ * Missing credentials stay `invalid_token`: a request that never named a
82
+ * session has not discovered a dead one.
83
+ *
84
+ * The reason is deliberately NOT widened to cover a wrong token on a live
85
+ * session — answering `session_gone` there would tell an unauthenticated
86
+ * caller which sessions exist.
87
+ *
88
+ * Requested by the Prism harness while designing a non-Node relay client.
89
+ */
90
+ check(id, token) {
91
+ if (!id || !token) return { ok: false, reason: "invalid_token" };
70
92
  const s = this.store.getSession(id);
71
- if (!s) return false;
72
- if (!timingSafeEqualHex(s.tokenHash, sha256Hex(token))) return false;
93
+ if (!s) return { ok: false, reason: "session_gone" };
94
+ if (!timingSafeEqualHex(s.tokenHash, sha256Hex(token))) {
95
+ return { ok: false, reason: "invalid_token" };
96
+ }
73
97
  s.lastSeen = Date.now();
74
98
  this.store.putSession(s);
75
- return true;
99
+ return { ok: true };
100
+ }
101
+ /**
102
+ * Validate an authenticated touch and slide the TTL forward.
103
+ *
104
+ * Kept as-is for existing callers; `check()` is the one that says why.
105
+ */
106
+ validate(id, token) {
107
+ return this.check(id, token).ok;
108
+ }
109
+ /** Drop a session — the page closed, or a test needs it gone. */
110
+ dropSession(id) {
111
+ this.store.deleteSession(id);
112
+ this.subs.delete(id);
76
113
  }
77
114
  /** Push a frame onto the inbound queue (external agent → browser). */
78
115
  inbox(id, token, payload) {
@@ -310,6 +347,10 @@ function createNodeRelay(opts = {}) {
310
347
  if (!session) return json(res, 400, { error: "missing_session" });
311
348
  const token = getQuery(req).get("token") ?? "";
312
349
  if (direction === "unregister") {
350
+ const state = broker.check(session, token);
351
+ if (!state.ok && state.reason === "session_gone") {
352
+ return json(res, 200, { ok: true, alreadyGone: true });
353
+ }
313
354
  const ok2 = broker.unregister(session, token);
314
355
  return json(res, ok2 ? 200 : 401, ok2 ? { ok: true } : { error: "invalid_token" });
315
356
  }
@@ -319,6 +360,10 @@ function createNodeRelay(opts = {}) {
319
360
  } catch (e) {
320
361
  return json(res, 413, { error: e instanceof Error ? e.message : "payload_error" });
321
362
  }
363
+ const auth = broker.check(session, token);
364
+ if (!auth.ok && auth.reason === "session_gone") {
365
+ return json(res, 410, { error: "session_gone" });
366
+ }
322
367
  const ok = direction === "inbound" ? broker.inbox(session, token, body) : broker.outbox(session, token, body);
323
368
  return json(res, ok ? 200 : 401, ok ? { ok: true } : { error: "invalid_token_or_frame" });
324
369
  };
@@ -418,5 +463,5 @@ function flush(res) {
418
463
  }
419
464
 
420
465
  export { RelayBroker, createNodeRelay };
421
- //# sourceMappingURL=chunk-GRHPEK6M.js.map
422
- //# sourceMappingURL=chunk-GRHPEK6M.js.map
466
+ //# sourceMappingURL=chunk-QOANLXSW.js.map
467
+ //# sourceMappingURL=chunk-QOANLXSW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/relay-server/core.ts","../src/relay-server/node.ts"],"names":["next","ok"],"mappings":"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","file":"chunk-QOANLXSW.js","sourcesContent":["import { createHash, randomBytes, timingSafeEqual } from \"node:crypto\";\n\n/**\n * RelayBroker — pure logic for the SSE+POST tunnel described in\n * docs/relay-protocol.md, hostable in any Node-compatible runtime\n * (Node, Bun, Deno-with-Node-compat, Cloudflare Workers via the Web\n * standards subset). No HTTP framework opinions; this class just\n * stores sessions, validates tokens, enqueues frames, and produces\n * SSE event payloads ready to flush.\n *\n * const broker = new RelayBroker();\n * const reg = broker.register(\"session-id\", \"token\"); // ok / error\n * broker.inbox(\"session-id\", \"token\", '{\"jsonrpc\":\"2.0\",…}'); // enqueue inbound\n * const sub = broker.subscribe(\"session-id\", \"token\", \"inbound\");\n * for await (const payload of sub.frames()) yield encodeSse(payload);\n *\n * Storage is an in-memory Map by default — fine for a single relay\n * process. To run multiple instances behind a load balancer, swap\n * `MemoryStore` for a Redis-backed equivalent (same Store interface).\n */\n\nexport type Direction = \"inbound\" | \"outbound\";\n\nexport type Session = {\n id: string;\n /** SHA-256 hex of the original token. Compared with timing-safe equals. */\n tokenHash: string;\n /** Last touched (ms since epoch). Used for TTL cleanup. */\n lastSeen: number;\n};\n\nexport type Subscriber = {\n id: string;\n direction: Direction;\n queue: string[];\n resolveNext: ((frame: string | null) => void) | null;\n};\n\nexport type RelayBrokerOptions = {\n /** Sessions auto-expire after this many ms of inactivity. Default 4h. */\n ttlMs?: number;\n /** Cleanup tick interval in ms. Default 60 000. */\n reapIntervalMs?: number;\n /** Bring-your-own storage layer (redis, etc.). Defaults to in-memory. */\n store?: Store;\n};\n\nexport interface Store {\n putSession(s: Session): void;\n getSession(id: string): Session | undefined;\n deleteSession(id: string): void;\n /** Used by the reap tick — return ids whose lastSeen < cutoff. */\n expiredSessionIds(cutoff: number): string[];\n}\n\nclass MemoryStore implements Store {\n private sessions = new Map<string, Session>();\n putSession(s: Session) { this.sessions.set(s.id, s); }\n getSession(id: string) { return this.sessions.get(id); }\n deleteSession(id: string) { this.sessions.delete(id); }\n expiredSessionIds(cutoff: number) {\n const out: string[] = [];\n for (const [id, s] of this.sessions) if (s.lastSeen < cutoff) out.push(id);\n return out;\n }\n}\n\nconst SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{4,64}$/;\n\nexport class RelayBroker {\n private readonly ttlMs: number;\n private readonly store: Store;\n /** Per-session, per-direction subscriber list. */\n private subs: Map<string, Map<string, Map<string, Subscriber>>> = new Map();\n private reaper?: ReturnType<typeof setInterval>;\n\n constructor(opts: RelayBrokerOptions = {}) {\n this.ttlMs = opts.ttlMs ?? 4 * 60 * 60 * 1000; // 4h\n this.store = opts.store ?? new MemoryStore();\n const tick = opts.reapIntervalMs ?? 60_000;\n if (tick > 0) {\n this.reaper = setInterval(() => this.reap(), tick);\n // Don't keep the process alive just for the reaper.\n if (typeof (this.reaper as { unref?: () => void }).unref === \"function\") {\n (this.reaper as { unref: () => void }).unref();\n }\n }\n }\n\n dispose() {\n if (this.reaper) clearInterval(this.reaper);\n this.subs.clear();\n }\n\n /** Register a session id + token. Idempotent — same id+token re-registers,\n * different token fails. */\n register(id: string, token: string): { ok: true } | { ok: false; reason: string } {\n if (!SESSION_ID_PATTERN.test(id)) return { ok: false, reason: \"invalid_session_id\" };\n if (typeof token !== \"string\" || token.length < 16 || token.length > 128) {\n return { ok: false, reason: \"invalid_token\" };\n }\n const existing = this.store.getSession(id);\n const hash = sha256Hex(token);\n if (existing) {\n if (!timingSafeEqualHex(existing.tokenHash, hash)) return { ok: false, reason: \"session_taken\" };\n existing.lastSeen = Date.now();\n this.store.putSession(existing);\n return { ok: true };\n }\n this.store.putSession({ id, tokenHash: hash, lastSeen: Date.now() });\n return { ok: true };\n }\n\n unregister(id: string, token: string): boolean {\n if (!this.validate(id, token)) return false;\n this.store.deleteSession(id);\n this.subs.delete(id);\n return true;\n }\n\n /**\n * Validate an authenticated touch, saying WHY when it fails.\n *\n * `session_gone` and `invalid_token` are different facts and a client acts on\n * them differently. A gone session means the page has closed or the TTL has\n * elapsed — the NORMAL end of a relay lifecycle, since the browser is the\n * server and no state persists across restarts. A bad token means the caller\n * is wrong.\n *\n * They were collapsed into one boolean and reported as `invalid_token`, so an\n * agent holding a perfectly good token against a closed page was told its\n * credentials were wrong. Not a missing signal — an actively misleading one,\n * which sends the reader to debug an auth path that was never the problem.\n *\n * Missing credentials stay `invalid_token`: a request that never named a\n * session has not discovered a dead one.\n *\n * The reason is deliberately NOT widened to cover a wrong token on a live\n * session — answering `session_gone` there would tell an unauthenticated\n * caller which sessions exist.\n *\n * Requested by the Prism harness while designing a non-Node relay client.\n */\n check(id: string, token: string): { ok: true } | { ok: false; reason: \"invalid_token\" | \"session_gone\" } {\n if (!id || !token) return { ok: false, reason: \"invalid_token\" };\n\n const s = this.store.getSession(id);\n if (!s) return { ok: false, reason: \"session_gone\" };\n\n if (!timingSafeEqualHex(s.tokenHash, sha256Hex(token))) {\n return { ok: false, reason: \"invalid_token\" };\n }\n\n s.lastSeen = Date.now();\n this.store.putSession(s);\n return { ok: true };\n }\n\n /**\n * Validate an authenticated touch and slide the TTL forward.\n *\n * Kept as-is for existing callers; `check()` is the one that says why.\n */\n validate(id: string, token: string): boolean {\n return this.check(id, token).ok;\n }\n\n /** Drop a session — the page closed, or a test needs it gone. */\n dropSession(id: string): void {\n this.store.deleteSession(id);\n this.subs.delete(id);\n }\n\n /** Push a frame onto the inbound queue (external agent → browser). */\n inbox(id: string, token: string, payload: string): boolean {\n if (!this.validate(id, token)) return false;\n if (!this.isFrame(payload)) return false;\n this.fanOut(id, \"inbound\", payload);\n return true;\n }\n\n /** Push a frame onto the outbound queue (browser server → external agents). */\n outbox(id: string, token: string, payload: string): boolean {\n if (!this.validate(id, token)) return false;\n if (!this.isFrame(payload)) return false;\n this.fanOut(id, \"outbound\", payload);\n return true;\n }\n\n /**\n * Subscribe to a session's queue for one direction. Returns an iterable\n * the caller (an HTTP handler) pumps as SSE.\n */\n subscribe(id: string, token: string, direction: Direction): SubscribeResult {\n if (!this.validate(id, token)) return { ok: false, reason: \"invalid_token\" };\n const subscriberId = randomBytes(8).toString(\"hex\");\n const subscriber: Subscriber = { id: subscriberId, direction, queue: [], resolveNext: null };\n this.getDirSubs(id, direction).set(subscriberId, subscriber);\n\n // Notify the inbound side (= browser) that an outbound subscriber\n // (= external agent) just connected.\n if (direction === \"outbound\") {\n this.fanOut(\n id,\n \"inbound\",\n JSON.stringify({\n jsonrpc: \"2.0\",\n method: \"notifications/peer_joined\",\n params: { subscriberId, ts: Date.now() },\n }),\n );\n }\n\n const unsubscribe = () => {\n this.getDirSubs(id, direction).delete(subscriberId);\n if (direction === \"outbound\") {\n this.fanOut(\n id,\n \"inbound\",\n JSON.stringify({\n jsonrpc: \"2.0\",\n method: \"notifications/peer_left\",\n params: { subscriberId, ts: Date.now() },\n }),\n );\n }\n };\n\n /** Async generator the HTTP handler drains. Yields raw frame payloads;\n * the handler is responsible for SSE framing (`event: mcp\\ndata: …`). */\n const frames = async function* (this: Subscriber): AsyncGenerator<string, void, void> {\n while (true) {\n if (this.queue.length > 0) {\n const next = this.queue.shift();\n if (next !== undefined) yield next;\n continue;\n }\n const next = await new Promise<string | null>((resolve) => {\n this.resolveNext = resolve;\n });\n this.resolveNext = null;\n if (next === null) return;\n yield next;\n }\n }.bind(subscriber);\n\n return {\n ok: true,\n subscriberId,\n frames: frames(),\n unsubscribe: () => {\n // Wake the generator and let it return cleanly.\n subscriber.resolveNext?.(null);\n unsubscribe();\n },\n };\n }\n\n // ────────────────────────────────────────────────────────────── internals\n\n private getDirSubs(sessionId: string, direction: Direction): Map<string, Subscriber> {\n let bySession = this.subs.get(sessionId);\n if (!bySession) {\n bySession = new Map();\n this.subs.set(sessionId, bySession);\n }\n let byDir = bySession.get(direction);\n if (!byDir) {\n byDir = new Map();\n bySession.set(direction, byDir);\n }\n return byDir;\n }\n\n private fanOut(sessionId: string, direction: Direction, payload: string) {\n const dir = this.subs.get(sessionId)?.get(direction);\n if (!dir) return;\n for (const sub of dir.values()) {\n sub.queue.push(payload);\n sub.resolveNext?.(sub.queue.shift() ?? null);\n }\n }\n\n /**\n * Validate that a peer-supplied payload is a well-formed JSON-RPC 2.0 frame\n * (or batch) before it is fanned out. Previously this was a substring match on\n * `\"jsonrpc\"`, which let any token holder inject spoofed frames — a forged\n * `notifications/peer_joined` / `peer_left` (broker-reserved presence control),\n * a fake `notifications/agent_activity {source:\"user\"}` (\"human took control\"),\n * or a forged response with a matching id. We now parse + shape-check, and\n * reject the broker-reserved control methods (the broker emits those itself,\n * never a peer).\n */\n private isFrame(payload: string): boolean {\n if (payload.length === 0) return false;\n let parsed: unknown;\n try {\n parsed = JSON.parse(payload);\n } catch {\n return false;\n }\n const frames = Array.isArray(parsed) ? parsed : [parsed];\n return frames.length > 0 && frames.every((f) => this.isValidFrame(f));\n }\n\n private isValidFrame(f: unknown): boolean {\n if (!f || typeof f !== \"object\" || Array.isArray(f)) return false;\n const o = f as Record<string, unknown>;\n if (o.jsonrpc !== \"2.0\") return false;\n const method = typeof o.method === \"string\" ? o.method : undefined;\n // Broker-reserved control frames may never arrive from a peer.\n if (method === \"notifications/peer_joined\" || method === \"notifications/peer_left\") return false;\n if (method !== undefined) return true; // request or notification\n // Response: needs an id and exactly one of result / error.\n const hasResult = \"result\" in o;\n const hasError = \"error\" in o;\n return \"id\" in o && hasResult !== hasError;\n }\n\n private reap() {\n const cutoff = Date.now() - this.ttlMs;\n for (const id of this.store.expiredSessionIds(cutoff)) {\n const dirs = this.subs.get(id);\n if (dirs) {\n for (const dir of dirs.values()) {\n for (const sub of dir.values()) sub.resolveNext?.(null);\n }\n }\n this.subs.delete(id);\n this.store.deleteSession(id);\n }\n }\n}\n\nexport type SubscribeResult =\n | { ok: false; reason: string }\n | {\n ok: true;\n subscriberId: string;\n frames: AsyncGenerator<string, void, void>;\n unsubscribe: () => void;\n };\n\nfunction sha256Hex(input: string): string {\n return createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\nfunction timingSafeEqualHex(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, \"hex\"), Buffer.from(b, \"hex\"));\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { URL } from \"node:url\";\nimport { RelayBroker, type RelayBrokerOptions } from \"./core\";\n\n/**\n * Node HTTP adapter for {@link RelayBroker}. Returns a single request\n * handler plus per-route handlers, so you can either drop it into\n * `http.createServer(...)` directly or mount the individual handlers\n * onto your existing Node HTTP framework (Express, Hono w/ node-adapter,\n * native http).\n *\n * const relay = createNodeRelay({ pathPrefix: \"/mcp-relay\" });\n * http.createServer(relay.handler).listen(8787);\n *\n * // Or piecemeal:\n * app.post(\"/mcp-relay/register\", relay.register);\n * app.post(\"/mcp-relay/:s/inbox\", relay.inbox);\n * app.post(\"/mcp-relay/:s/outbox\", relay.outbox);\n * app.get (\"/mcp-relay/:s/events\", relay.events);\n * app.post(\"/mcp-relay/:s/unregister\", relay.unregister);\n */\n\nexport type NodeRelayOptions = RelayBrokerOptions & {\n /** URL path prefix (without trailing slash). Default `\"\"` — handlers\n * expect paths like `/register`, `/{id}/inbox`, etc. directly. */\n pathPrefix?: string;\n /** Comma-separated origins (or `*`) for CORS. Default `*` — relays\n * are typically called cross-origin from the demo host. */\n corsAllowOrigin?: string;\n /**\n * Strict browser-origin allow-list. When set, only these Origins get a\n * matching `Access-Control-Allow-Origin` (the request's own Origin is\n * reflected, never `*`), which blunts DNS-rebinding + hostile cross-origin\n * pages from reading relay responses. Recommended for any browser-facing\n * deployment. When unset, `corsAllowOrigin` (default `*`) is used and the\n * session token is the only auth — pair that with a loopback bind.\n */\n allowedOrigins?: string[];\n};\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => unknown | Promise<unknown>;\n\nexport type NodeRelay = {\n broker: RelayBroker;\n /** Single-handler shape — routes internally based on method + URL. */\n handler: NodeHandler;\n /** Per-route handlers. Each handler ignores the URL prefix and\n * acts on the path remainder, so you can mount them under any\n * prefix in your existing app. */\n register: NodeHandler;\n inbox: NodeHandler;\n outbox: NodeHandler;\n events: NodeHandler;\n unregister: NodeHandler;\n};\n\nexport function createNodeRelay(opts: NodeRelayOptions = {}): NodeRelay {\n const broker = new RelayBroker(opts);\n const prefix = (opts.pathPrefix ?? \"\").replace(/\\/$/, \"\");\n const cors = opts.corsAllowOrigin ?? \"*\";\n\n const allowedOrigins = opts.allowedOrigins;\n\n function setCorsHeaders(res: ServerResponse, req?: IncomingMessage) {\n if (allowedOrigins && allowedOrigins.length) {\n // Strict mode: reflect the request Origin only when it's allow-listed;\n // otherwise emit an origin that no browser will match.\n const origin = req?.headers.origin;\n res.setHeader(\"access-control-allow-origin\", origin && allowedOrigins.includes(origin) ? origin : \"null\");\n res.setHeader(\"vary\", \"origin\");\n } else {\n res.setHeader(\"access-control-allow-origin\", cors);\n }\n res.setHeader(\"access-control-allow-methods\", \"GET, POST, OPTIONS\");\n // `authorization` is forward-compat for a header-borne session token.\n res.setHeader(\"access-control-allow-headers\", \"content-type, x-csrf-token, accept, authorization\");\n res.setHeader(\"access-control-max-age\", \"86400\");\n }\n\n function readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let bytes = 0;\n req.on(\"data\", (chunk: Buffer) => {\n bytes += chunk.length;\n // Cap individual frames at 256 KB — protect against runaway payloads.\n if (bytes > 256 * 1024) {\n reject(new Error(\"payload_too_large\"));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", reject);\n });\n }\n\n function json(res: ServerResponse, status: number, body: unknown): void {\n res.statusCode = status;\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify(body));\n }\n\n function getQuery(req: IncomingMessage): URLSearchParams {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n return u.searchParams;\n }\n\n function getPathname(req: IncomingMessage): string {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n return u.pathname;\n }\n\n const register: NodeHandler = async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"POST\") return json(res, 405, { error: \"method_not_allowed\" });\n let body: string;\n try { body = await readBody(req); } catch (e) {\n return json(res, 413, { error: e instanceof Error ? e.message : \"payload_error\" });\n }\n let parsed: unknown;\n try { parsed = JSON.parse(body); } catch { return json(res, 400, { error: \"invalid_json\" }); }\n const { session, token } = (parsed ?? {}) as { session?: string; token?: string };\n if (typeof session !== \"string\" || typeof token !== \"string\") {\n return json(res, 400, { error: \"missing_fields\" });\n }\n const result = broker.register(session, token);\n if (!result.ok) return json(res, 401, { error: result.reason });\n return json(res, 200, { ok: true });\n };\n\n /** Handler for endpoints with a `{session}` segment. The path matcher\n * caller passes the session id explicitly so this works mounted under\n * any route shape. */\n function makeSessionHandler(\n direction: Direction | \"unregister\",\n ): NodeHandler {\n return async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"POST\") return json(res, 405, { error: \"method_not_allowed\" });\n const session = extractSession(req, prefix);\n if (!session) return json(res, 400, { error: \"missing_session\" });\n const token = getQuery(req).get(\"token\") ?? \"\";\n\n if (direction === \"unregister\") {\n // Unregistering a session that is already gone is a SUCCESS, not an\n // auth failure. The caller wanted it gone and it is gone; reporting\n // `invalid_token` for a correct token would make a clean shutdown look\n // like a broken one, and a client retrying it would loop.\n const state = broker.check(session, token);\n if (!state.ok && state.reason === \"session_gone\") {\n return json(res, 200, { ok: true, alreadyGone: true });\n }\n\n const ok = broker.unregister(session, token);\n return json(res, ok ? 200 : 401, ok ? { ok: true } : { error: \"invalid_token\" });\n }\n\n let body: string;\n try { body = await readBody(req); } catch (e) {\n return json(res, 413, { error: e instanceof Error ? e.message : \"payload_error\" });\n }\n // A GONE SESSION IS REPORTED AS ITSELF, with 410, before the frame is\n // even considered.\n //\n // Every failure here used to come back as `401 invalid_token_or_frame`,\n // so an agent whose page had simply closed was told its credentials or\n // its JSON were wrong. A relay session ending is the NORMAL end of a\n // lifecycle -- the browser is the server and nothing persists -- so it is\n // the one outcome a server-side client must be able to recognise.\n //\n // 410 Gone rather than 404: the session existed and is now over, which is\n // exactly what the status means, and it distinguishes \"this is finished\"\n // from \"you have the wrong URL\".\n const auth = broker.check(session, token);\n if (!auth.ok && auth.reason === \"session_gone\") {\n return json(res, 410, { error: \"session_gone\" });\n }\n\n const ok = direction === \"inbound\"\n ? broker.inbox(session, token, body)\n : broker.outbox(session, token, body);\n return json(res, ok ? 200 : 401, ok ? { ok: true } : { error: \"invalid_token_or_frame\" });\n };\n }\n\n const inbox = makeSessionHandler(\"inbound\");\n const outbox = makeSessionHandler(\"outbound\");\n const unregister = makeSessionHandler(\"unregister\");\n\n const events: NodeHandler = async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"GET\") return json(res, 405, { error: \"method_not_allowed\" });\n const session = extractSession(req, prefix);\n if (!session) return json(res, 400, { error: \"missing_session\" });\n const q = getQuery(req);\n const token = q.get(\"token\") ?? \"\";\n const direction = q.get(\"direction\") === \"outbound\" ? \"outbound\" : \"inbound\";\n\n const sub = broker.subscribe(session, token, direction);\n if (!sub.ok) {\n res.statusCode = 401;\n res.setHeader(\"content-type\", \"text/event-stream\");\n res.write(`event: error\\ndata: ${sub.reason}\\n\\n`);\n return res.end();\n }\n\n res.statusCode = 200;\n res.setHeader(\"content-type\", \"text/event-stream\");\n res.setHeader(\"cache-control\", \"no-cache\");\n res.setHeader(\"connection\", \"keep-alive\");\n res.setHeader(\"x-accel-buffering\", \"no\");\n res.write(\"retry: 2000\\n\\n\");\n flush(res);\n\n let heartbeat: ReturnType<typeof setInterval> | null = setInterval(() => {\n res.write(\": keepalive\\n\\n\");\n flush(res);\n }, 15_000);\n if (heartbeat && typeof (heartbeat as { unref?: () => void }).unref === \"function\") {\n (heartbeat as { unref: () => void }).unref();\n }\n\n const cleanup = () => {\n if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }\n sub.unsubscribe();\n };\n req.on(\"close\", cleanup);\n req.on(\"error\", cleanup);\n\n try {\n for await (const frame of sub.frames) {\n res.write(`event: mcp\\ndata: ${frame}\\n\\n`);\n flush(res);\n }\n } catch {\n /* stream ended */\n } finally {\n cleanup();\n res.end();\n }\n };\n\n /**\n * Single handler — routes based on method + path. Useful for mounting\n * via `http.createServer(relay.handler)` without an Express layer.\n */\n const handler: NodeHandler = async (req, res) => {\n const pathname = getPathname(req);\n if (!pathname.startsWith(prefix + \"/\")) {\n return json(res, 404, { error: \"not_found\" });\n }\n const rest = pathname.slice(prefix.length); // \"/register\", \"/<id>/inbox\", etc.\n if (rest === \"/register\") return register(req, res);\n const m = /^\\/([A-Za-z0-9_-]{4,64})\\/(inbox|outbox|events|unregister)$/.exec(rest);\n if (!m) return json(res, 404, { error: \"not_found\" });\n const route = m[2];\n if (route === \"inbox\") return inbox(req, res);\n if (route === \"outbox\") return outbox(req, res);\n if (route === \"events\") return events(req, res);\n if (route === \"unregister\") return unregister(req, res);\n return json(res, 404, { error: \"not_found\" });\n };\n\n return { broker, handler, register, inbox, outbox, events, unregister };\n}\n\nfunction extractSession(req: IncomingMessage, prefix: string): string | null {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n const path = u.pathname;\n if (prefix && !path.startsWith(prefix + \"/\")) return null;\n const rest = prefix ? path.slice(prefix.length) : path;\n const m = /^\\/([A-Za-z0-9_-]{4,64})\\//.exec(rest);\n return m ? m[1] : null;\n}\n\nfunction flush(res: ServerResponse) {\n // Node doesn't expose explicit flush, but write returns false when buffered;\n // explicit flushHeaders + write is enough for SSE in practice.\n const r = res as { flush?: () => void };\n r.flush?.();\n}\n\ntype Direction = \"inbound\" | \"outbound\";\n"]}
@@ -64,8 +64,43 @@ declare class RelayBroker {
64
64
  reason: string;
65
65
  };
66
66
  unregister(id: string, token: string): boolean;
67
- /** Validate an authenticated touch and slide the TTL forward. */
67
+ /**
68
+ * Validate an authenticated touch, saying WHY when it fails.
69
+ *
70
+ * `session_gone` and `invalid_token` are different facts and a client acts on
71
+ * them differently. A gone session means the page has closed or the TTL has
72
+ * elapsed — the NORMAL end of a relay lifecycle, since the browser is the
73
+ * server and no state persists across restarts. A bad token means the caller
74
+ * is wrong.
75
+ *
76
+ * They were collapsed into one boolean and reported as `invalid_token`, so an
77
+ * agent holding a perfectly good token against a closed page was told its
78
+ * credentials were wrong. Not a missing signal — an actively misleading one,
79
+ * which sends the reader to debug an auth path that was never the problem.
80
+ *
81
+ * Missing credentials stay `invalid_token`: a request that never named a
82
+ * session has not discovered a dead one.
83
+ *
84
+ * The reason is deliberately NOT widened to cover a wrong token on a live
85
+ * session — answering `session_gone` there would tell an unauthenticated
86
+ * caller which sessions exist.
87
+ *
88
+ * Requested by the Prism harness while designing a non-Node relay client.
89
+ */
90
+ check(id: string, token: string): {
91
+ ok: true;
92
+ } | {
93
+ ok: false;
94
+ reason: "invalid_token" | "session_gone";
95
+ };
96
+ /**
97
+ * Validate an authenticated touch and slide the TTL forward.
98
+ *
99
+ * Kept as-is for existing callers; `check()` is the one that says why.
100
+ */
68
101
  validate(id: string, token: string): boolean;
102
+ /** Drop a session — the page closed, or a test needs it gone. */
103
+ dropSession(id: string): void;
69
104
  /** Push a frame onto the inbound queue (external agent → browser). */
70
105
  inbox(id: string, token: string, payload: string): boolean;
71
106
  /** Push a frame onto the outbound queue (browser server → external agents). */
@@ -64,8 +64,43 @@ declare class RelayBroker {
64
64
  reason: string;
65
65
  };
66
66
  unregister(id: string, token: string): boolean;
67
- /** Validate an authenticated touch and slide the TTL forward. */
67
+ /**
68
+ * Validate an authenticated touch, saying WHY when it fails.
69
+ *
70
+ * `session_gone` and `invalid_token` are different facts and a client acts on
71
+ * them differently. A gone session means the page has closed or the TTL has
72
+ * elapsed — the NORMAL end of a relay lifecycle, since the browser is the
73
+ * server and no state persists across restarts. A bad token means the caller
74
+ * is wrong.
75
+ *
76
+ * They were collapsed into one boolean and reported as `invalid_token`, so an
77
+ * agent holding a perfectly good token against a closed page was told its
78
+ * credentials were wrong. Not a missing signal — an actively misleading one,
79
+ * which sends the reader to debug an auth path that was never the problem.
80
+ *
81
+ * Missing credentials stay `invalid_token`: a request that never named a
82
+ * session has not discovered a dead one.
83
+ *
84
+ * The reason is deliberately NOT widened to cover a wrong token on a live
85
+ * session — answering `session_gone` there would tell an unauthenticated
86
+ * caller which sessions exist.
87
+ *
88
+ * Requested by the Prism harness while designing a non-Node relay client.
89
+ */
90
+ check(id: string, token: string): {
91
+ ok: true;
92
+ } | {
93
+ ok: false;
94
+ reason: "invalid_token" | "session_gone";
95
+ };
96
+ /**
97
+ * Validate an authenticated touch and slide the TTL forward.
98
+ *
99
+ * Kept as-is for existing callers; `check()` is the one that says why.
100
+ */
68
101
  validate(id: string, token: string): boolean;
102
+ /** Drop a session — the page closed, or a test needs it gone. */
103
+ dropSession(id: string): void;
69
104
  /** Push a frame onto the inbound queue (external agent → browser). */
70
105
  inbox(id: string, token: string, payload: string): boolean;
71
106
  /** Push a frame onto the outbound queue (browser server → external agents). */
@@ -67,15 +67,52 @@ var RelayBroker = class {
67
67
  this.subs.delete(id);
68
68
  return true;
69
69
  }
70
- /** Validate an authenticated touch and slide the TTL forward. */
71
- validate(id, token) {
72
- if (!id || !token) return false;
70
+ /**
71
+ * Validate an authenticated touch, saying WHY when it fails.
72
+ *
73
+ * `session_gone` and `invalid_token` are different facts and a client acts on
74
+ * them differently. A gone session means the page has closed or the TTL has
75
+ * elapsed — the NORMAL end of a relay lifecycle, since the browser is the
76
+ * server and no state persists across restarts. A bad token means the caller
77
+ * is wrong.
78
+ *
79
+ * They were collapsed into one boolean and reported as `invalid_token`, so an
80
+ * agent holding a perfectly good token against a closed page was told its
81
+ * credentials were wrong. Not a missing signal — an actively misleading one,
82
+ * which sends the reader to debug an auth path that was never the problem.
83
+ *
84
+ * Missing credentials stay `invalid_token`: a request that never named a
85
+ * session has not discovered a dead one.
86
+ *
87
+ * The reason is deliberately NOT widened to cover a wrong token on a live
88
+ * session — answering `session_gone` there would tell an unauthenticated
89
+ * caller which sessions exist.
90
+ *
91
+ * Requested by the Prism harness while designing a non-Node relay client.
92
+ */
93
+ check(id, token) {
94
+ if (!id || !token) return { ok: false, reason: "invalid_token" };
73
95
  const s = this.store.getSession(id);
74
- if (!s) return false;
75
- if (!timingSafeEqualHex(s.tokenHash, sha256Hex(token))) return false;
96
+ if (!s) return { ok: false, reason: "session_gone" };
97
+ if (!timingSafeEqualHex(s.tokenHash, sha256Hex(token))) {
98
+ return { ok: false, reason: "invalid_token" };
99
+ }
76
100
  s.lastSeen = Date.now();
77
101
  this.store.putSession(s);
78
- return true;
102
+ return { ok: true };
103
+ }
104
+ /**
105
+ * Validate an authenticated touch and slide the TTL forward.
106
+ *
107
+ * Kept as-is for existing callers; `check()` is the one that says why.
108
+ */
109
+ validate(id, token) {
110
+ return this.check(id, token).ok;
111
+ }
112
+ /** Drop a session — the page closed, or a test needs it gone. */
113
+ dropSession(id) {
114
+ this.store.deleteSession(id);
115
+ this.subs.delete(id);
79
116
  }
80
117
  /** Push a frame onto the inbound queue (external agent → browser). */
81
118
  inbox(id, token, payload) {
@@ -315,6 +352,10 @@ function createNodeRelay(opts = {}) {
315
352
  if (!session) return json(res, 400, { error: "missing_session" });
316
353
  const token = getQuery(req).get("token") ?? "";
317
354
  if (direction === "unregister") {
355
+ const state = broker.check(session, token);
356
+ if (!state.ok && state.reason === "session_gone") {
357
+ return json(res, 200, { ok: true, alreadyGone: true });
358
+ }
318
359
  const ok2 = broker.unregister(session, token);
319
360
  return json(res, ok2 ? 200 : 401, ok2 ? { ok: true } : { error: "invalid_token" });
320
361
  }
@@ -324,6 +365,10 @@ function createNodeRelay(opts = {}) {
324
365
  } catch (e) {
325
366
  return json(res, 413, { error: e instanceof Error ? e.message : "payload_error" });
326
367
  }
368
+ const auth = broker.check(session, token);
369
+ if (!auth.ok && auth.reason === "session_gone") {
370
+ return json(res, 410, { error: "session_gone" });
371
+ }
327
372
  const ok = direction === "inbound" ? broker.inbox(session, token, body) : broker.outbox(session, token, body);
328
373
  return json(res, ok ? 200 : 401, ok ? { ok: true } : { error: "invalid_token_or_frame" });
329
374
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/relay-server/core.ts","../src/relay-server/node.ts","../src/relay-server/cli.ts"],"names":["randomBytes","next","createHash","timingSafeEqual","URL","ok","createServer"],"mappings":"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","file":"relay-server-cli.cjs","sourcesContent":["import { createHash, randomBytes, timingSafeEqual } from \"node:crypto\";\n\n/**\n * RelayBroker — pure logic for the SSE+POST tunnel described in\n * docs/relay-protocol.md, hostable in any Node-compatible runtime\n * (Node, Bun, Deno-with-Node-compat, Cloudflare Workers via the Web\n * standards subset). No HTTP framework opinions; this class just\n * stores sessions, validates tokens, enqueues frames, and produces\n * SSE event payloads ready to flush.\n *\n * const broker = new RelayBroker();\n * const reg = broker.register(\"session-id\", \"token\"); // ok / error\n * broker.inbox(\"session-id\", \"token\", '{\"jsonrpc\":\"2.0\",…}'); // enqueue inbound\n * const sub = broker.subscribe(\"session-id\", \"token\", \"inbound\");\n * for await (const payload of sub.frames()) yield encodeSse(payload);\n *\n * Storage is an in-memory Map by default — fine for a single relay\n * process. To run multiple instances behind a load balancer, swap\n * `MemoryStore` for a Redis-backed equivalent (same Store interface).\n */\n\nexport type Direction = \"inbound\" | \"outbound\";\n\nexport type Session = {\n id: string;\n /** SHA-256 hex of the original token. Compared with timing-safe equals. */\n tokenHash: string;\n /** Last touched (ms since epoch). Used for TTL cleanup. */\n lastSeen: number;\n};\n\nexport type Subscriber = {\n id: string;\n direction: Direction;\n queue: string[];\n resolveNext: ((frame: string | null) => void) | null;\n};\n\nexport type RelayBrokerOptions = {\n /** Sessions auto-expire after this many ms of inactivity. Default 4h. */\n ttlMs?: number;\n /** Cleanup tick interval in ms. Default 60 000. */\n reapIntervalMs?: number;\n /** Bring-your-own storage layer (redis, etc.). Defaults to in-memory. */\n store?: Store;\n};\n\nexport interface Store {\n putSession(s: Session): void;\n getSession(id: string): Session | undefined;\n deleteSession(id: string): void;\n /** Used by the reap tick — return ids whose lastSeen < cutoff. */\n expiredSessionIds(cutoff: number): string[];\n}\n\nclass MemoryStore implements Store {\n private sessions = new Map<string, Session>();\n putSession(s: Session) { this.sessions.set(s.id, s); }\n getSession(id: string) { return this.sessions.get(id); }\n deleteSession(id: string) { this.sessions.delete(id); }\n expiredSessionIds(cutoff: number) {\n const out: string[] = [];\n for (const [id, s] of this.sessions) if (s.lastSeen < cutoff) out.push(id);\n return out;\n }\n}\n\nconst SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{4,64}$/;\n\nexport class RelayBroker {\n private readonly ttlMs: number;\n private readonly store: Store;\n /** Per-session, per-direction subscriber list. */\n private subs: Map<string, Map<string, Map<string, Subscriber>>> = new Map();\n private reaper?: ReturnType<typeof setInterval>;\n\n constructor(opts: RelayBrokerOptions = {}) {\n this.ttlMs = opts.ttlMs ?? 4 * 60 * 60 * 1000; // 4h\n this.store = opts.store ?? new MemoryStore();\n const tick = opts.reapIntervalMs ?? 60_000;\n if (tick > 0) {\n this.reaper = setInterval(() => this.reap(), tick);\n // Don't keep the process alive just for the reaper.\n if (typeof (this.reaper as { unref?: () => void }).unref === \"function\") {\n (this.reaper as { unref: () => void }).unref();\n }\n }\n }\n\n dispose() {\n if (this.reaper) clearInterval(this.reaper);\n this.subs.clear();\n }\n\n /** Register a session id + token. Idempotent — same id+token re-registers,\n * different token fails. */\n register(id: string, token: string): { ok: true } | { ok: false; reason: string } {\n if (!SESSION_ID_PATTERN.test(id)) return { ok: false, reason: \"invalid_session_id\" };\n if (typeof token !== \"string\" || token.length < 16 || token.length > 128) {\n return { ok: false, reason: \"invalid_token\" };\n }\n const existing = this.store.getSession(id);\n const hash = sha256Hex(token);\n if (existing) {\n if (!timingSafeEqualHex(existing.tokenHash, hash)) return { ok: false, reason: \"session_taken\" };\n existing.lastSeen = Date.now();\n this.store.putSession(existing);\n return { ok: true };\n }\n this.store.putSession({ id, tokenHash: hash, lastSeen: Date.now() });\n return { ok: true };\n }\n\n unregister(id: string, token: string): boolean {\n if (!this.validate(id, token)) return false;\n this.store.deleteSession(id);\n this.subs.delete(id);\n return true;\n }\n\n /** Validate an authenticated touch and slide the TTL forward. */\n validate(id: string, token: string): boolean {\n if (!id || !token) return false;\n const s = this.store.getSession(id);\n if (!s) return false;\n if (!timingSafeEqualHex(s.tokenHash, sha256Hex(token))) return false;\n s.lastSeen = Date.now();\n this.store.putSession(s);\n return true;\n }\n\n /** Push a frame onto the inbound queue (external agent → browser). */\n inbox(id: string, token: string, payload: string): boolean {\n if (!this.validate(id, token)) return false;\n if (!this.isFrame(payload)) return false;\n this.fanOut(id, \"inbound\", payload);\n return true;\n }\n\n /** Push a frame onto the outbound queue (browser server → external agents). */\n outbox(id: string, token: string, payload: string): boolean {\n if (!this.validate(id, token)) return false;\n if (!this.isFrame(payload)) return false;\n this.fanOut(id, \"outbound\", payload);\n return true;\n }\n\n /**\n * Subscribe to a session's queue for one direction. Returns an iterable\n * the caller (an HTTP handler) pumps as SSE.\n */\n subscribe(id: string, token: string, direction: Direction): SubscribeResult {\n if (!this.validate(id, token)) return { ok: false, reason: \"invalid_token\" };\n const subscriberId = randomBytes(8).toString(\"hex\");\n const subscriber: Subscriber = { id: subscriberId, direction, queue: [], resolveNext: null };\n this.getDirSubs(id, direction).set(subscriberId, subscriber);\n\n // Notify the inbound side (= browser) that an outbound subscriber\n // (= external agent) just connected.\n if (direction === \"outbound\") {\n this.fanOut(\n id,\n \"inbound\",\n JSON.stringify({\n jsonrpc: \"2.0\",\n method: \"notifications/peer_joined\",\n params: { subscriberId, ts: Date.now() },\n }),\n );\n }\n\n const unsubscribe = () => {\n this.getDirSubs(id, direction).delete(subscriberId);\n if (direction === \"outbound\") {\n this.fanOut(\n id,\n \"inbound\",\n JSON.stringify({\n jsonrpc: \"2.0\",\n method: \"notifications/peer_left\",\n params: { subscriberId, ts: Date.now() },\n }),\n );\n }\n };\n\n /** Async generator the HTTP handler drains. Yields raw frame payloads;\n * the handler is responsible for SSE framing (`event: mcp\\ndata: …`). */\n const frames = async function* (this: Subscriber): AsyncGenerator<string, void, void> {\n while (true) {\n if (this.queue.length > 0) {\n const next = this.queue.shift();\n if (next !== undefined) yield next;\n continue;\n }\n const next = await new Promise<string | null>((resolve) => {\n this.resolveNext = resolve;\n });\n this.resolveNext = null;\n if (next === null) return;\n yield next;\n }\n }.bind(subscriber);\n\n return {\n ok: true,\n subscriberId,\n frames: frames(),\n unsubscribe: () => {\n // Wake the generator and let it return cleanly.\n subscriber.resolveNext?.(null);\n unsubscribe();\n },\n };\n }\n\n // ────────────────────────────────────────────────────────────── internals\n\n private getDirSubs(sessionId: string, direction: Direction): Map<string, Subscriber> {\n let bySession = this.subs.get(sessionId);\n if (!bySession) {\n bySession = new Map();\n this.subs.set(sessionId, bySession);\n }\n let byDir = bySession.get(direction);\n if (!byDir) {\n byDir = new Map();\n bySession.set(direction, byDir);\n }\n return byDir;\n }\n\n private fanOut(sessionId: string, direction: Direction, payload: string) {\n const dir = this.subs.get(sessionId)?.get(direction);\n if (!dir) return;\n for (const sub of dir.values()) {\n sub.queue.push(payload);\n sub.resolveNext?.(sub.queue.shift() ?? null);\n }\n }\n\n /**\n * Validate that a peer-supplied payload is a well-formed JSON-RPC 2.0 frame\n * (or batch) before it is fanned out. Previously this was a substring match on\n * `\"jsonrpc\"`, which let any token holder inject spoofed frames — a forged\n * `notifications/peer_joined` / `peer_left` (broker-reserved presence control),\n * a fake `notifications/agent_activity {source:\"user\"}` (\"human took control\"),\n * or a forged response with a matching id. We now parse + shape-check, and\n * reject the broker-reserved control methods (the broker emits those itself,\n * never a peer).\n */\n private isFrame(payload: string): boolean {\n if (payload.length === 0) return false;\n let parsed: unknown;\n try {\n parsed = JSON.parse(payload);\n } catch {\n return false;\n }\n const frames = Array.isArray(parsed) ? parsed : [parsed];\n return frames.length > 0 && frames.every((f) => this.isValidFrame(f));\n }\n\n private isValidFrame(f: unknown): boolean {\n if (!f || typeof f !== \"object\" || Array.isArray(f)) return false;\n const o = f as Record<string, unknown>;\n if (o.jsonrpc !== \"2.0\") return false;\n const method = typeof o.method === \"string\" ? o.method : undefined;\n // Broker-reserved control frames may never arrive from a peer.\n if (method === \"notifications/peer_joined\" || method === \"notifications/peer_left\") return false;\n if (method !== undefined) return true; // request or notification\n // Response: needs an id and exactly one of result / error.\n const hasResult = \"result\" in o;\n const hasError = \"error\" in o;\n return \"id\" in o && hasResult !== hasError;\n }\n\n private reap() {\n const cutoff = Date.now() - this.ttlMs;\n for (const id of this.store.expiredSessionIds(cutoff)) {\n const dirs = this.subs.get(id);\n if (dirs) {\n for (const dir of dirs.values()) {\n for (const sub of dir.values()) sub.resolveNext?.(null);\n }\n }\n this.subs.delete(id);\n this.store.deleteSession(id);\n }\n }\n}\n\nexport type SubscribeResult =\n | { ok: false; reason: string }\n | {\n ok: true;\n subscriberId: string;\n frames: AsyncGenerator<string, void, void>;\n unsubscribe: () => void;\n };\n\nfunction sha256Hex(input: string): string {\n return createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\nfunction timingSafeEqualHex(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, \"hex\"), Buffer.from(b, \"hex\"));\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { URL } from \"node:url\";\nimport { RelayBroker, type RelayBrokerOptions } from \"./core\";\n\n/**\n * Node HTTP adapter for {@link RelayBroker}. Returns a single request\n * handler plus per-route handlers, so you can either drop it into\n * `http.createServer(...)` directly or mount the individual handlers\n * onto your existing Node HTTP framework (Express, Hono w/ node-adapter,\n * native http).\n *\n * const relay = createNodeRelay({ pathPrefix: \"/mcp-relay\" });\n * http.createServer(relay.handler).listen(8787);\n *\n * // Or piecemeal:\n * app.post(\"/mcp-relay/register\", relay.register);\n * app.post(\"/mcp-relay/:s/inbox\", relay.inbox);\n * app.post(\"/mcp-relay/:s/outbox\", relay.outbox);\n * app.get (\"/mcp-relay/:s/events\", relay.events);\n * app.post(\"/mcp-relay/:s/unregister\", relay.unregister);\n */\n\nexport type NodeRelayOptions = RelayBrokerOptions & {\n /** URL path prefix (without trailing slash). Default `\"\"` — handlers\n * expect paths like `/register`, `/{id}/inbox`, etc. directly. */\n pathPrefix?: string;\n /** Comma-separated origins (or `*`) for CORS. Default `*` — relays\n * are typically called cross-origin from the demo host. */\n corsAllowOrigin?: string;\n /**\n * Strict browser-origin allow-list. When set, only these Origins get a\n * matching `Access-Control-Allow-Origin` (the request's own Origin is\n * reflected, never `*`), which blunts DNS-rebinding + hostile cross-origin\n * pages from reading relay responses. Recommended for any browser-facing\n * deployment. When unset, `corsAllowOrigin` (default `*`) is used and the\n * session token is the only auth — pair that with a loopback bind.\n */\n allowedOrigins?: string[];\n};\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => unknown | Promise<unknown>;\n\nexport type NodeRelay = {\n broker: RelayBroker;\n /** Single-handler shape — routes internally based on method + URL. */\n handler: NodeHandler;\n /** Per-route handlers. Each handler ignores the URL prefix and\n * acts on the path remainder, so you can mount them under any\n * prefix in your existing app. */\n register: NodeHandler;\n inbox: NodeHandler;\n outbox: NodeHandler;\n events: NodeHandler;\n unregister: NodeHandler;\n};\n\nexport function createNodeRelay(opts: NodeRelayOptions = {}): NodeRelay {\n const broker = new RelayBroker(opts);\n const prefix = (opts.pathPrefix ?? \"\").replace(/\\/$/, \"\");\n const cors = opts.corsAllowOrigin ?? \"*\";\n\n const allowedOrigins = opts.allowedOrigins;\n\n function setCorsHeaders(res: ServerResponse, req?: IncomingMessage) {\n if (allowedOrigins && allowedOrigins.length) {\n // Strict mode: reflect the request Origin only when it's allow-listed;\n // otherwise emit an origin that no browser will match.\n const origin = req?.headers.origin;\n res.setHeader(\"access-control-allow-origin\", origin && allowedOrigins.includes(origin) ? origin : \"null\");\n res.setHeader(\"vary\", \"origin\");\n } else {\n res.setHeader(\"access-control-allow-origin\", cors);\n }\n res.setHeader(\"access-control-allow-methods\", \"GET, POST, OPTIONS\");\n // `authorization` is forward-compat for a header-borne session token.\n res.setHeader(\"access-control-allow-headers\", \"content-type, x-csrf-token, accept, authorization\");\n res.setHeader(\"access-control-max-age\", \"86400\");\n }\n\n function readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let bytes = 0;\n req.on(\"data\", (chunk: Buffer) => {\n bytes += chunk.length;\n // Cap individual frames at 256 KB — protect against runaway payloads.\n if (bytes > 256 * 1024) {\n reject(new Error(\"payload_too_large\"));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", reject);\n });\n }\n\n function json(res: ServerResponse, status: number, body: unknown): void {\n res.statusCode = status;\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify(body));\n }\n\n function getQuery(req: IncomingMessage): URLSearchParams {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n return u.searchParams;\n }\n\n function getPathname(req: IncomingMessage): string {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n return u.pathname;\n }\n\n const register: NodeHandler = async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"POST\") return json(res, 405, { error: \"method_not_allowed\" });\n let body: string;\n try { body = await readBody(req); } catch (e) {\n return json(res, 413, { error: e instanceof Error ? e.message : \"payload_error\" });\n }\n let parsed: unknown;\n try { parsed = JSON.parse(body); } catch { return json(res, 400, { error: \"invalid_json\" }); }\n const { session, token } = (parsed ?? {}) as { session?: string; token?: string };\n if (typeof session !== \"string\" || typeof token !== \"string\") {\n return json(res, 400, { error: \"missing_fields\" });\n }\n const result = broker.register(session, token);\n if (!result.ok) return json(res, 401, { error: result.reason });\n return json(res, 200, { ok: true });\n };\n\n /** Handler for endpoints with a `{session}` segment. The path matcher\n * caller passes the session id explicitly so this works mounted under\n * any route shape. */\n function makeSessionHandler(\n direction: Direction | \"unregister\",\n ): NodeHandler {\n return async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"POST\") return json(res, 405, { error: \"method_not_allowed\" });\n const session = extractSession(req, prefix);\n if (!session) return json(res, 400, { error: \"missing_session\" });\n const token = getQuery(req).get(\"token\") ?? \"\";\n\n if (direction === \"unregister\") {\n const ok = broker.unregister(session, token);\n return json(res, ok ? 200 : 401, ok ? { ok: true } : { error: \"invalid_token\" });\n }\n\n let body: string;\n try { body = await readBody(req); } catch (e) {\n return json(res, 413, { error: e instanceof Error ? e.message : \"payload_error\" });\n }\n const ok = direction === \"inbound\"\n ? broker.inbox(session, token, body)\n : broker.outbox(session, token, body);\n return json(res, ok ? 200 : 401, ok ? { ok: true } : { error: \"invalid_token_or_frame\" });\n };\n }\n\n const inbox = makeSessionHandler(\"inbound\");\n const outbox = makeSessionHandler(\"outbound\");\n const unregister = makeSessionHandler(\"unregister\");\n\n const events: NodeHandler = async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"GET\") return json(res, 405, { error: \"method_not_allowed\" });\n const session = extractSession(req, prefix);\n if (!session) return json(res, 400, { error: \"missing_session\" });\n const q = getQuery(req);\n const token = q.get(\"token\") ?? \"\";\n const direction = q.get(\"direction\") === \"outbound\" ? \"outbound\" : \"inbound\";\n\n const sub = broker.subscribe(session, token, direction);\n if (!sub.ok) {\n res.statusCode = 401;\n res.setHeader(\"content-type\", \"text/event-stream\");\n res.write(`event: error\\ndata: ${sub.reason}\\n\\n`);\n return res.end();\n }\n\n res.statusCode = 200;\n res.setHeader(\"content-type\", \"text/event-stream\");\n res.setHeader(\"cache-control\", \"no-cache\");\n res.setHeader(\"connection\", \"keep-alive\");\n res.setHeader(\"x-accel-buffering\", \"no\");\n res.write(\"retry: 2000\\n\\n\");\n flush(res);\n\n let heartbeat: ReturnType<typeof setInterval> | null = setInterval(() => {\n res.write(\": keepalive\\n\\n\");\n flush(res);\n }, 15_000);\n if (heartbeat && typeof (heartbeat as { unref?: () => void }).unref === \"function\") {\n (heartbeat as { unref: () => void }).unref();\n }\n\n const cleanup = () => {\n if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }\n sub.unsubscribe();\n };\n req.on(\"close\", cleanup);\n req.on(\"error\", cleanup);\n\n try {\n for await (const frame of sub.frames) {\n res.write(`event: mcp\\ndata: ${frame}\\n\\n`);\n flush(res);\n }\n } catch {\n /* stream ended */\n } finally {\n cleanup();\n res.end();\n }\n };\n\n /**\n * Single handler — routes based on method + path. Useful for mounting\n * via `http.createServer(relay.handler)` without an Express layer.\n */\n const handler: NodeHandler = async (req, res) => {\n const pathname = getPathname(req);\n if (!pathname.startsWith(prefix + \"/\")) {\n return json(res, 404, { error: \"not_found\" });\n }\n const rest = pathname.slice(prefix.length); // \"/register\", \"/<id>/inbox\", etc.\n if (rest === \"/register\") return register(req, res);\n const m = /^\\/([A-Za-z0-9_-]{4,64})\\/(inbox|outbox|events|unregister)$/.exec(rest);\n if (!m) return json(res, 404, { error: \"not_found\" });\n const route = m[2];\n if (route === \"inbox\") return inbox(req, res);\n if (route === \"outbox\") return outbox(req, res);\n if (route === \"events\") return events(req, res);\n if (route === \"unregister\") return unregister(req, res);\n return json(res, 404, { error: \"not_found\" });\n };\n\n return { broker, handler, register, inbox, outbox, events, unregister };\n}\n\nfunction extractSession(req: IncomingMessage, prefix: string): string | null {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n const path = u.pathname;\n if (prefix && !path.startsWith(prefix + \"/\")) return null;\n const rest = prefix ? path.slice(prefix.length) : path;\n const m = /^\\/([A-Za-z0-9_-]{4,64})\\//.exec(rest);\n return m ? m[1] : null;\n}\n\nfunction flush(res: ServerResponse) {\n // Node doesn't expose explicit flush, but write returns false when buffered;\n // explicit flushHeaders + write is enough for SSE in practice.\n const r = res as { flush?: () => void };\n r.flush?.();\n}\n\ntype Direction = \"inbound\" | \"outbound\";\n","#!/usr/bin/env node\nimport { createServer } from \"node:http\";\nimport { createNodeRelay } from \"./node\";\n\n/**\n * `agent-integrations-relay` — standalone Node HTTP server hosting the\n * SSE+POST relay broker. End users hit `Start share` on a /ui/demos\n * page, get a session URL pointing at this service, and paste it into\n * their MCP client. No state persists across restarts.\n *\n * Flags:\n * --port <n> Listen port. Default 8787.\n * --host <addr> Bind address. Default 127.0.0.1 (loopback). Pass\n * 0.0.0.0 to expose on all interfaces (warns).\n * --prefix <path> URL path prefix (no trailing slash). Default \"\".\n * Useful when mounting behind a reverse proxy.\n * --ttl-ms <n> Session TTL ms. Default 14_400_000 (4h).\n * --cors <origin> Access-Control-Allow-Origin. Default \"*\".\n * -h, --help Show this help.\n *\n * Health: any request to `/` returns 200 OK so platform health checks\n * pass without authenticating.\n */\nasync function main() {\n const argv = process.argv.slice(2);\n let port = Number(process.env.PORT ?? 8787);\n // Loopback by default: this relay fronts terminal_run (RCE) and register is\n // unauthenticated, so it must not listen on all interfaces unless the operator\n // explicitly opts in with --host 0.0.0.0.\n let host = process.env.HOST ?? \"127.0.0.1\";\n let prefix = process.env.PREFIX ?? \"\";\n let ttlMs = Number(process.env.TTL_MS ?? 4 * 60 * 60 * 1000);\n let cors = process.env.CORS_ALLOW_ORIGIN ?? \"*\";\n\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i];\n switch (a) {\n case \"--port\": port = Number(argv[++i]); break;\n case \"--host\": host = argv[++i]; break;\n case \"--prefix\": prefix = argv[++i]; break;\n case \"--ttl-ms\": ttlMs = Number(argv[++i]); break;\n case \"--cors\": cors = argv[++i]; break;\n case \"-h\":\n case \"--help\":\n process.stdout.write(HELP);\n process.exit(0);\n default:\n process.stderr.write(`[relay] unknown flag: ${a}\\n`);\n process.exit(2);\n }\n }\n\n if (!Number.isFinite(port) || port <= 0) {\n process.stderr.write(`[relay] invalid --port: ${port}\\n`);\n process.exit(2);\n }\n\n const relay = createNodeRelay({ pathPrefix: prefix, ttlMs, corsAllowOrigin: cors });\n\n const server = createServer((req, res) => {\n // Health: GET / always 200 so platform health checks succeed without auth.\n const url = req.url || \"/\";\n if ((url === \"/\" || url === \"/healthz\" || url === (prefix + \"/\")) && req.method === \"GET\") {\n res.statusCode = 200;\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify({ ok: true, service: \"agent-integrations-relay\" }));\n return;\n }\n relay.handler(req, res);\n });\n\n const isLoopback = host === \"127.0.0.1\" || host === \"::1\" || host === \"localhost\";\n if (!isLoopback) {\n process.stderr.write(\n `[relay] WARNING: binding ${host} exposes the relay on non-loopback interfaces. ` +\n `register is unauthenticated and a leaked session token grants terminal_run. ` +\n `Only do this behind an authenticating proxy / trusted network.\\n`,\n );\n }\n server.listen(port, host, () => {\n process.stdout.write(\n `[relay] listening on http://${host}:${port}${prefix || \"\"} ` +\n `(ttl=${Math.round(ttlMs / 1000)}s, cors=${cors})\\n`,\n );\n });\n\n const shutdown = () => {\n process.stdout.write(\"[relay] shutting down\\n\");\n relay.broker.dispose();\n server.close(() => process.exit(0));\n setTimeout(() => process.exit(0), 2000).unref();\n };\n process.on(\"SIGTERM\", shutdown);\n process.on(\"SIGINT\", shutdown);\n}\n\nconst HELP = `agent-integrations-relay — Node HTTP server for the MCP relay broker.\n\nUsage: agent-integrations-relay [options]\n\nOptions:\n --port <n> Listen port (env: PORT). Default 8787.\n --host <addr> Bind address (env: HOST). Default 127.0.0.1 (loopback).\n Pass 0.0.0.0 to expose on all interfaces (prints a warning).\n --prefix <path> URL path prefix (env: PREFIX). Default \"\".\n --ttl-ms <n> Session TTL ms (env: TTL_MS). Default 14_400_000.\n --cors <origin> CORS Access-Control-Allow-Origin (env: CORS_ALLOW_ORIGIN). Default \"*\".\n -h, --help Show this help.\n\nEndpoints (under --prefix):\n POST /register { session, token } → { ok: true }\n POST /<session>/inbox?token=... body: JSON-RPC frame\n POST /<session>/outbox?token=... body: JSON-RPC frame\n GET /<session>/events?token=...&direction=inbound|outbound\n Server-sent events stream\n POST /<session>/unregister?token=... Tear down session\n GET / Healthcheck → 200\n`;\n\nmain().catch((e) => {\n process.stderr.write(`[relay] fatal: ${e instanceof Error ? e.stack ?? e.message : String(e)}\\n`);\n process.exit(1);\n});\n"]}
1
+ {"version":3,"sources":["../src/relay-server/core.ts","../src/relay-server/node.ts","../src/relay-server/cli.ts"],"names":["randomBytes","next","createHash","timingSafeEqual","URL","ok","createServer"],"mappings":"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","file":"relay-server-cli.cjs","sourcesContent":["import { createHash, randomBytes, timingSafeEqual } from \"node:crypto\";\n\n/**\n * RelayBroker — pure logic for the SSE+POST tunnel described in\n * docs/relay-protocol.md, hostable in any Node-compatible runtime\n * (Node, Bun, Deno-with-Node-compat, Cloudflare Workers via the Web\n * standards subset). No HTTP framework opinions; this class just\n * stores sessions, validates tokens, enqueues frames, and produces\n * SSE event payloads ready to flush.\n *\n * const broker = new RelayBroker();\n * const reg = broker.register(\"session-id\", \"token\"); // ok / error\n * broker.inbox(\"session-id\", \"token\", '{\"jsonrpc\":\"2.0\",…}'); // enqueue inbound\n * const sub = broker.subscribe(\"session-id\", \"token\", \"inbound\");\n * for await (const payload of sub.frames()) yield encodeSse(payload);\n *\n * Storage is an in-memory Map by default — fine for a single relay\n * process. To run multiple instances behind a load balancer, swap\n * `MemoryStore` for a Redis-backed equivalent (same Store interface).\n */\n\nexport type Direction = \"inbound\" | \"outbound\";\n\nexport type Session = {\n id: string;\n /** SHA-256 hex of the original token. Compared with timing-safe equals. */\n tokenHash: string;\n /** Last touched (ms since epoch). Used for TTL cleanup. */\n lastSeen: number;\n};\n\nexport type Subscriber = {\n id: string;\n direction: Direction;\n queue: string[];\n resolveNext: ((frame: string | null) => void) | null;\n};\n\nexport type RelayBrokerOptions = {\n /** Sessions auto-expire after this many ms of inactivity. Default 4h. */\n ttlMs?: number;\n /** Cleanup tick interval in ms. Default 60 000. */\n reapIntervalMs?: number;\n /** Bring-your-own storage layer (redis, etc.). Defaults to in-memory. */\n store?: Store;\n};\n\nexport interface Store {\n putSession(s: Session): void;\n getSession(id: string): Session | undefined;\n deleteSession(id: string): void;\n /** Used by the reap tick — return ids whose lastSeen < cutoff. */\n expiredSessionIds(cutoff: number): string[];\n}\n\nclass MemoryStore implements Store {\n private sessions = new Map<string, Session>();\n putSession(s: Session) { this.sessions.set(s.id, s); }\n getSession(id: string) { return this.sessions.get(id); }\n deleteSession(id: string) { this.sessions.delete(id); }\n expiredSessionIds(cutoff: number) {\n const out: string[] = [];\n for (const [id, s] of this.sessions) if (s.lastSeen < cutoff) out.push(id);\n return out;\n }\n}\n\nconst SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{4,64}$/;\n\nexport class RelayBroker {\n private readonly ttlMs: number;\n private readonly store: Store;\n /** Per-session, per-direction subscriber list. */\n private subs: Map<string, Map<string, Map<string, Subscriber>>> = new Map();\n private reaper?: ReturnType<typeof setInterval>;\n\n constructor(opts: RelayBrokerOptions = {}) {\n this.ttlMs = opts.ttlMs ?? 4 * 60 * 60 * 1000; // 4h\n this.store = opts.store ?? new MemoryStore();\n const tick = opts.reapIntervalMs ?? 60_000;\n if (tick > 0) {\n this.reaper = setInterval(() => this.reap(), tick);\n // Don't keep the process alive just for the reaper.\n if (typeof (this.reaper as { unref?: () => void }).unref === \"function\") {\n (this.reaper as { unref: () => void }).unref();\n }\n }\n }\n\n dispose() {\n if (this.reaper) clearInterval(this.reaper);\n this.subs.clear();\n }\n\n /** Register a session id + token. Idempotent — same id+token re-registers,\n * different token fails. */\n register(id: string, token: string): { ok: true } | { ok: false; reason: string } {\n if (!SESSION_ID_PATTERN.test(id)) return { ok: false, reason: \"invalid_session_id\" };\n if (typeof token !== \"string\" || token.length < 16 || token.length > 128) {\n return { ok: false, reason: \"invalid_token\" };\n }\n const existing = this.store.getSession(id);\n const hash = sha256Hex(token);\n if (existing) {\n if (!timingSafeEqualHex(existing.tokenHash, hash)) return { ok: false, reason: \"session_taken\" };\n existing.lastSeen = Date.now();\n this.store.putSession(existing);\n return { ok: true };\n }\n this.store.putSession({ id, tokenHash: hash, lastSeen: Date.now() });\n return { ok: true };\n }\n\n unregister(id: string, token: string): boolean {\n if (!this.validate(id, token)) return false;\n this.store.deleteSession(id);\n this.subs.delete(id);\n return true;\n }\n\n /**\n * Validate an authenticated touch, saying WHY when it fails.\n *\n * `session_gone` and `invalid_token` are different facts and a client acts on\n * them differently. A gone session means the page has closed or the TTL has\n * elapsed — the NORMAL end of a relay lifecycle, since the browser is the\n * server and no state persists across restarts. A bad token means the caller\n * is wrong.\n *\n * They were collapsed into one boolean and reported as `invalid_token`, so an\n * agent holding a perfectly good token against a closed page was told its\n * credentials were wrong. Not a missing signal — an actively misleading one,\n * which sends the reader to debug an auth path that was never the problem.\n *\n * Missing credentials stay `invalid_token`: a request that never named a\n * session has not discovered a dead one.\n *\n * The reason is deliberately NOT widened to cover a wrong token on a live\n * session — answering `session_gone` there would tell an unauthenticated\n * caller which sessions exist.\n *\n * Requested by the Prism harness while designing a non-Node relay client.\n */\n check(id: string, token: string): { ok: true } | { ok: false; reason: \"invalid_token\" | \"session_gone\" } {\n if (!id || !token) return { ok: false, reason: \"invalid_token\" };\n\n const s = this.store.getSession(id);\n if (!s) return { ok: false, reason: \"session_gone\" };\n\n if (!timingSafeEqualHex(s.tokenHash, sha256Hex(token))) {\n return { ok: false, reason: \"invalid_token\" };\n }\n\n s.lastSeen = Date.now();\n this.store.putSession(s);\n return { ok: true };\n }\n\n /**\n * Validate an authenticated touch and slide the TTL forward.\n *\n * Kept as-is for existing callers; `check()` is the one that says why.\n */\n validate(id: string, token: string): boolean {\n return this.check(id, token).ok;\n }\n\n /** Drop a session — the page closed, or a test needs it gone. */\n dropSession(id: string): void {\n this.store.deleteSession(id);\n this.subs.delete(id);\n }\n\n /** Push a frame onto the inbound queue (external agent → browser). */\n inbox(id: string, token: string, payload: string): boolean {\n if (!this.validate(id, token)) return false;\n if (!this.isFrame(payload)) return false;\n this.fanOut(id, \"inbound\", payload);\n return true;\n }\n\n /** Push a frame onto the outbound queue (browser server → external agents). */\n outbox(id: string, token: string, payload: string): boolean {\n if (!this.validate(id, token)) return false;\n if (!this.isFrame(payload)) return false;\n this.fanOut(id, \"outbound\", payload);\n return true;\n }\n\n /**\n * Subscribe to a session's queue for one direction. Returns an iterable\n * the caller (an HTTP handler) pumps as SSE.\n */\n subscribe(id: string, token: string, direction: Direction): SubscribeResult {\n if (!this.validate(id, token)) return { ok: false, reason: \"invalid_token\" };\n const subscriberId = randomBytes(8).toString(\"hex\");\n const subscriber: Subscriber = { id: subscriberId, direction, queue: [], resolveNext: null };\n this.getDirSubs(id, direction).set(subscriberId, subscriber);\n\n // Notify the inbound side (= browser) that an outbound subscriber\n // (= external agent) just connected.\n if (direction === \"outbound\") {\n this.fanOut(\n id,\n \"inbound\",\n JSON.stringify({\n jsonrpc: \"2.0\",\n method: \"notifications/peer_joined\",\n params: { subscriberId, ts: Date.now() },\n }),\n );\n }\n\n const unsubscribe = () => {\n this.getDirSubs(id, direction).delete(subscriberId);\n if (direction === \"outbound\") {\n this.fanOut(\n id,\n \"inbound\",\n JSON.stringify({\n jsonrpc: \"2.0\",\n method: \"notifications/peer_left\",\n params: { subscriberId, ts: Date.now() },\n }),\n );\n }\n };\n\n /** Async generator the HTTP handler drains. Yields raw frame payloads;\n * the handler is responsible for SSE framing (`event: mcp\\ndata: …`). */\n const frames = async function* (this: Subscriber): AsyncGenerator<string, void, void> {\n while (true) {\n if (this.queue.length > 0) {\n const next = this.queue.shift();\n if (next !== undefined) yield next;\n continue;\n }\n const next = await new Promise<string | null>((resolve) => {\n this.resolveNext = resolve;\n });\n this.resolveNext = null;\n if (next === null) return;\n yield next;\n }\n }.bind(subscriber);\n\n return {\n ok: true,\n subscriberId,\n frames: frames(),\n unsubscribe: () => {\n // Wake the generator and let it return cleanly.\n subscriber.resolveNext?.(null);\n unsubscribe();\n },\n };\n }\n\n // ────────────────────────────────────────────────────────────── internals\n\n private getDirSubs(sessionId: string, direction: Direction): Map<string, Subscriber> {\n let bySession = this.subs.get(sessionId);\n if (!bySession) {\n bySession = new Map();\n this.subs.set(sessionId, bySession);\n }\n let byDir = bySession.get(direction);\n if (!byDir) {\n byDir = new Map();\n bySession.set(direction, byDir);\n }\n return byDir;\n }\n\n private fanOut(sessionId: string, direction: Direction, payload: string) {\n const dir = this.subs.get(sessionId)?.get(direction);\n if (!dir) return;\n for (const sub of dir.values()) {\n sub.queue.push(payload);\n sub.resolveNext?.(sub.queue.shift() ?? null);\n }\n }\n\n /**\n * Validate that a peer-supplied payload is a well-formed JSON-RPC 2.0 frame\n * (or batch) before it is fanned out. Previously this was a substring match on\n * `\"jsonrpc\"`, which let any token holder inject spoofed frames — a forged\n * `notifications/peer_joined` / `peer_left` (broker-reserved presence control),\n * a fake `notifications/agent_activity {source:\"user\"}` (\"human took control\"),\n * or a forged response with a matching id. We now parse + shape-check, and\n * reject the broker-reserved control methods (the broker emits those itself,\n * never a peer).\n */\n private isFrame(payload: string): boolean {\n if (payload.length === 0) return false;\n let parsed: unknown;\n try {\n parsed = JSON.parse(payload);\n } catch {\n return false;\n }\n const frames = Array.isArray(parsed) ? parsed : [parsed];\n return frames.length > 0 && frames.every((f) => this.isValidFrame(f));\n }\n\n private isValidFrame(f: unknown): boolean {\n if (!f || typeof f !== \"object\" || Array.isArray(f)) return false;\n const o = f as Record<string, unknown>;\n if (o.jsonrpc !== \"2.0\") return false;\n const method = typeof o.method === \"string\" ? o.method : undefined;\n // Broker-reserved control frames may never arrive from a peer.\n if (method === \"notifications/peer_joined\" || method === \"notifications/peer_left\") return false;\n if (method !== undefined) return true; // request or notification\n // Response: needs an id and exactly one of result / error.\n const hasResult = \"result\" in o;\n const hasError = \"error\" in o;\n return \"id\" in o && hasResult !== hasError;\n }\n\n private reap() {\n const cutoff = Date.now() - this.ttlMs;\n for (const id of this.store.expiredSessionIds(cutoff)) {\n const dirs = this.subs.get(id);\n if (dirs) {\n for (const dir of dirs.values()) {\n for (const sub of dir.values()) sub.resolveNext?.(null);\n }\n }\n this.subs.delete(id);\n this.store.deleteSession(id);\n }\n }\n}\n\nexport type SubscribeResult =\n | { ok: false; reason: string }\n | {\n ok: true;\n subscriberId: string;\n frames: AsyncGenerator<string, void, void>;\n unsubscribe: () => void;\n };\n\nfunction sha256Hex(input: string): string {\n return createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\nfunction timingSafeEqualHex(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, \"hex\"), Buffer.from(b, \"hex\"));\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { URL } from \"node:url\";\nimport { RelayBroker, type RelayBrokerOptions } from \"./core\";\n\n/**\n * Node HTTP adapter for {@link RelayBroker}. Returns a single request\n * handler plus per-route handlers, so you can either drop it into\n * `http.createServer(...)` directly or mount the individual handlers\n * onto your existing Node HTTP framework (Express, Hono w/ node-adapter,\n * native http).\n *\n * const relay = createNodeRelay({ pathPrefix: \"/mcp-relay\" });\n * http.createServer(relay.handler).listen(8787);\n *\n * // Or piecemeal:\n * app.post(\"/mcp-relay/register\", relay.register);\n * app.post(\"/mcp-relay/:s/inbox\", relay.inbox);\n * app.post(\"/mcp-relay/:s/outbox\", relay.outbox);\n * app.get (\"/mcp-relay/:s/events\", relay.events);\n * app.post(\"/mcp-relay/:s/unregister\", relay.unregister);\n */\n\nexport type NodeRelayOptions = RelayBrokerOptions & {\n /** URL path prefix (without trailing slash). Default `\"\"` — handlers\n * expect paths like `/register`, `/{id}/inbox`, etc. directly. */\n pathPrefix?: string;\n /** Comma-separated origins (or `*`) for CORS. Default `*` — relays\n * are typically called cross-origin from the demo host. */\n corsAllowOrigin?: string;\n /**\n * Strict browser-origin allow-list. When set, only these Origins get a\n * matching `Access-Control-Allow-Origin` (the request's own Origin is\n * reflected, never `*`), which blunts DNS-rebinding + hostile cross-origin\n * pages from reading relay responses. Recommended for any browser-facing\n * deployment. When unset, `corsAllowOrigin` (default `*`) is used and the\n * session token is the only auth — pair that with a loopback bind.\n */\n allowedOrigins?: string[];\n};\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => unknown | Promise<unknown>;\n\nexport type NodeRelay = {\n broker: RelayBroker;\n /** Single-handler shape — routes internally based on method + URL. */\n handler: NodeHandler;\n /** Per-route handlers. Each handler ignores the URL prefix and\n * acts on the path remainder, so you can mount them under any\n * prefix in your existing app. */\n register: NodeHandler;\n inbox: NodeHandler;\n outbox: NodeHandler;\n events: NodeHandler;\n unregister: NodeHandler;\n};\n\nexport function createNodeRelay(opts: NodeRelayOptions = {}): NodeRelay {\n const broker = new RelayBroker(opts);\n const prefix = (opts.pathPrefix ?? \"\").replace(/\\/$/, \"\");\n const cors = opts.corsAllowOrigin ?? \"*\";\n\n const allowedOrigins = opts.allowedOrigins;\n\n function setCorsHeaders(res: ServerResponse, req?: IncomingMessage) {\n if (allowedOrigins && allowedOrigins.length) {\n // Strict mode: reflect the request Origin only when it's allow-listed;\n // otherwise emit an origin that no browser will match.\n const origin = req?.headers.origin;\n res.setHeader(\"access-control-allow-origin\", origin && allowedOrigins.includes(origin) ? origin : \"null\");\n res.setHeader(\"vary\", \"origin\");\n } else {\n res.setHeader(\"access-control-allow-origin\", cors);\n }\n res.setHeader(\"access-control-allow-methods\", \"GET, POST, OPTIONS\");\n // `authorization` is forward-compat for a header-borne session token.\n res.setHeader(\"access-control-allow-headers\", \"content-type, x-csrf-token, accept, authorization\");\n res.setHeader(\"access-control-max-age\", \"86400\");\n }\n\n function readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let bytes = 0;\n req.on(\"data\", (chunk: Buffer) => {\n bytes += chunk.length;\n // Cap individual frames at 256 KB — protect against runaway payloads.\n if (bytes > 256 * 1024) {\n reject(new Error(\"payload_too_large\"));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", reject);\n });\n }\n\n function json(res: ServerResponse, status: number, body: unknown): void {\n res.statusCode = status;\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify(body));\n }\n\n function getQuery(req: IncomingMessage): URLSearchParams {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n return u.searchParams;\n }\n\n function getPathname(req: IncomingMessage): string {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n return u.pathname;\n }\n\n const register: NodeHandler = async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"POST\") return json(res, 405, { error: \"method_not_allowed\" });\n let body: string;\n try { body = await readBody(req); } catch (e) {\n return json(res, 413, { error: e instanceof Error ? e.message : \"payload_error\" });\n }\n let parsed: unknown;\n try { parsed = JSON.parse(body); } catch { return json(res, 400, { error: \"invalid_json\" }); }\n const { session, token } = (parsed ?? {}) as { session?: string; token?: string };\n if (typeof session !== \"string\" || typeof token !== \"string\") {\n return json(res, 400, { error: \"missing_fields\" });\n }\n const result = broker.register(session, token);\n if (!result.ok) return json(res, 401, { error: result.reason });\n return json(res, 200, { ok: true });\n };\n\n /** Handler for endpoints with a `{session}` segment. The path matcher\n * caller passes the session id explicitly so this works mounted under\n * any route shape. */\n function makeSessionHandler(\n direction: Direction | \"unregister\",\n ): NodeHandler {\n return async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"POST\") return json(res, 405, { error: \"method_not_allowed\" });\n const session = extractSession(req, prefix);\n if (!session) return json(res, 400, { error: \"missing_session\" });\n const token = getQuery(req).get(\"token\") ?? \"\";\n\n if (direction === \"unregister\") {\n // Unregistering a session that is already gone is a SUCCESS, not an\n // auth failure. The caller wanted it gone and it is gone; reporting\n // `invalid_token` for a correct token would make a clean shutdown look\n // like a broken one, and a client retrying it would loop.\n const state = broker.check(session, token);\n if (!state.ok && state.reason === \"session_gone\") {\n return json(res, 200, { ok: true, alreadyGone: true });\n }\n\n const ok = broker.unregister(session, token);\n return json(res, ok ? 200 : 401, ok ? { ok: true } : { error: \"invalid_token\" });\n }\n\n let body: string;\n try { body = await readBody(req); } catch (e) {\n return json(res, 413, { error: e instanceof Error ? e.message : \"payload_error\" });\n }\n // A GONE SESSION IS REPORTED AS ITSELF, with 410, before the frame is\n // even considered.\n //\n // Every failure here used to come back as `401 invalid_token_or_frame`,\n // so an agent whose page had simply closed was told its credentials or\n // its JSON were wrong. A relay session ending is the NORMAL end of a\n // lifecycle -- the browser is the server and nothing persists -- so it is\n // the one outcome a server-side client must be able to recognise.\n //\n // 410 Gone rather than 404: the session existed and is now over, which is\n // exactly what the status means, and it distinguishes \"this is finished\"\n // from \"you have the wrong URL\".\n const auth = broker.check(session, token);\n if (!auth.ok && auth.reason === \"session_gone\") {\n return json(res, 410, { error: \"session_gone\" });\n }\n\n const ok = direction === \"inbound\"\n ? broker.inbox(session, token, body)\n : broker.outbox(session, token, body);\n return json(res, ok ? 200 : 401, ok ? { ok: true } : { error: \"invalid_token_or_frame\" });\n };\n }\n\n const inbox = makeSessionHandler(\"inbound\");\n const outbox = makeSessionHandler(\"outbound\");\n const unregister = makeSessionHandler(\"unregister\");\n\n const events: NodeHandler = async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"GET\") return json(res, 405, { error: \"method_not_allowed\" });\n const session = extractSession(req, prefix);\n if (!session) return json(res, 400, { error: \"missing_session\" });\n const q = getQuery(req);\n const token = q.get(\"token\") ?? \"\";\n const direction = q.get(\"direction\") === \"outbound\" ? \"outbound\" : \"inbound\";\n\n const sub = broker.subscribe(session, token, direction);\n if (!sub.ok) {\n res.statusCode = 401;\n res.setHeader(\"content-type\", \"text/event-stream\");\n res.write(`event: error\\ndata: ${sub.reason}\\n\\n`);\n return res.end();\n }\n\n res.statusCode = 200;\n res.setHeader(\"content-type\", \"text/event-stream\");\n res.setHeader(\"cache-control\", \"no-cache\");\n res.setHeader(\"connection\", \"keep-alive\");\n res.setHeader(\"x-accel-buffering\", \"no\");\n res.write(\"retry: 2000\\n\\n\");\n flush(res);\n\n let heartbeat: ReturnType<typeof setInterval> | null = setInterval(() => {\n res.write(\": keepalive\\n\\n\");\n flush(res);\n }, 15_000);\n if (heartbeat && typeof (heartbeat as { unref?: () => void }).unref === \"function\") {\n (heartbeat as { unref: () => void }).unref();\n }\n\n const cleanup = () => {\n if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }\n sub.unsubscribe();\n };\n req.on(\"close\", cleanup);\n req.on(\"error\", cleanup);\n\n try {\n for await (const frame of sub.frames) {\n res.write(`event: mcp\\ndata: ${frame}\\n\\n`);\n flush(res);\n }\n } catch {\n /* stream ended */\n } finally {\n cleanup();\n res.end();\n }\n };\n\n /**\n * Single handler — routes based on method + path. Useful for mounting\n * via `http.createServer(relay.handler)` without an Express layer.\n */\n const handler: NodeHandler = async (req, res) => {\n const pathname = getPathname(req);\n if (!pathname.startsWith(prefix + \"/\")) {\n return json(res, 404, { error: \"not_found\" });\n }\n const rest = pathname.slice(prefix.length); // \"/register\", \"/<id>/inbox\", etc.\n if (rest === \"/register\") return register(req, res);\n const m = /^\\/([A-Za-z0-9_-]{4,64})\\/(inbox|outbox|events|unregister)$/.exec(rest);\n if (!m) return json(res, 404, { error: \"not_found\" });\n const route = m[2];\n if (route === \"inbox\") return inbox(req, res);\n if (route === \"outbox\") return outbox(req, res);\n if (route === \"events\") return events(req, res);\n if (route === \"unregister\") return unregister(req, res);\n return json(res, 404, { error: \"not_found\" });\n };\n\n return { broker, handler, register, inbox, outbox, events, unregister };\n}\n\nfunction extractSession(req: IncomingMessage, prefix: string): string | null {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n const path = u.pathname;\n if (prefix && !path.startsWith(prefix + \"/\")) return null;\n const rest = prefix ? path.slice(prefix.length) : path;\n const m = /^\\/([A-Za-z0-9_-]{4,64})\\//.exec(rest);\n return m ? m[1] : null;\n}\n\nfunction flush(res: ServerResponse) {\n // Node doesn't expose explicit flush, but write returns false when buffered;\n // explicit flushHeaders + write is enough for SSE in practice.\n const r = res as { flush?: () => void };\n r.flush?.();\n}\n\ntype Direction = \"inbound\" | \"outbound\";\n","#!/usr/bin/env node\nimport { createServer } from \"node:http\";\nimport { createNodeRelay } from \"./node\";\n\n/**\n * `agent-integrations-relay` — standalone Node HTTP server hosting the\n * SSE+POST relay broker. End users hit `Start share` on a /ui/demos\n * page, get a session URL pointing at this service, and paste it into\n * their MCP client. No state persists across restarts.\n *\n * Flags:\n * --port <n> Listen port. Default 8787.\n * --host <addr> Bind address. Default 127.0.0.1 (loopback). Pass\n * 0.0.0.0 to expose on all interfaces (warns).\n * --prefix <path> URL path prefix (no trailing slash). Default \"\".\n * Useful when mounting behind a reverse proxy.\n * --ttl-ms <n> Session TTL ms. Default 14_400_000 (4h).\n * --cors <origin> Access-Control-Allow-Origin. Default \"*\".\n * -h, --help Show this help.\n *\n * Health: any request to `/` returns 200 OK so platform health checks\n * pass without authenticating.\n */\nasync function main() {\n const argv = process.argv.slice(2);\n let port = Number(process.env.PORT ?? 8787);\n // Loopback by default: this relay fronts terminal_run (RCE) and register is\n // unauthenticated, so it must not listen on all interfaces unless the operator\n // explicitly opts in with --host 0.0.0.0.\n let host = process.env.HOST ?? \"127.0.0.1\";\n let prefix = process.env.PREFIX ?? \"\";\n let ttlMs = Number(process.env.TTL_MS ?? 4 * 60 * 60 * 1000);\n let cors = process.env.CORS_ALLOW_ORIGIN ?? \"*\";\n\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i];\n switch (a) {\n case \"--port\": port = Number(argv[++i]); break;\n case \"--host\": host = argv[++i]; break;\n case \"--prefix\": prefix = argv[++i]; break;\n case \"--ttl-ms\": ttlMs = Number(argv[++i]); break;\n case \"--cors\": cors = argv[++i]; break;\n case \"-h\":\n case \"--help\":\n process.stdout.write(HELP);\n process.exit(0);\n default:\n process.stderr.write(`[relay] unknown flag: ${a}\\n`);\n process.exit(2);\n }\n }\n\n if (!Number.isFinite(port) || port <= 0) {\n process.stderr.write(`[relay] invalid --port: ${port}\\n`);\n process.exit(2);\n }\n\n const relay = createNodeRelay({ pathPrefix: prefix, ttlMs, corsAllowOrigin: cors });\n\n const server = createServer((req, res) => {\n // Health: GET / always 200 so platform health checks succeed without auth.\n const url = req.url || \"/\";\n if ((url === \"/\" || url === \"/healthz\" || url === (prefix + \"/\")) && req.method === \"GET\") {\n res.statusCode = 200;\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify({ ok: true, service: \"agent-integrations-relay\" }));\n return;\n }\n relay.handler(req, res);\n });\n\n const isLoopback = host === \"127.0.0.1\" || host === \"::1\" || host === \"localhost\";\n if (!isLoopback) {\n process.stderr.write(\n `[relay] WARNING: binding ${host} exposes the relay on non-loopback interfaces. ` +\n `register is unauthenticated and a leaked session token grants terminal_run. ` +\n `Only do this behind an authenticating proxy / trusted network.\\n`,\n );\n }\n server.listen(port, host, () => {\n process.stdout.write(\n `[relay] listening on http://${host}:${port}${prefix || \"\"} ` +\n `(ttl=${Math.round(ttlMs / 1000)}s, cors=${cors})\\n`,\n );\n });\n\n const shutdown = () => {\n process.stdout.write(\"[relay] shutting down\\n\");\n relay.broker.dispose();\n server.close(() => process.exit(0));\n setTimeout(() => process.exit(0), 2000).unref();\n };\n process.on(\"SIGTERM\", shutdown);\n process.on(\"SIGINT\", shutdown);\n}\n\nconst HELP = `agent-integrations-relay — Node HTTP server for the MCP relay broker.\n\nUsage: agent-integrations-relay [options]\n\nOptions:\n --port <n> Listen port (env: PORT). Default 8787.\n --host <addr> Bind address (env: HOST). Default 127.0.0.1 (loopback).\n Pass 0.0.0.0 to expose on all interfaces (prints a warning).\n --prefix <path> URL path prefix (env: PREFIX). Default \"\".\n --ttl-ms <n> Session TTL ms (env: TTL_MS). Default 14_400_000.\n --cors <origin> CORS Access-Control-Allow-Origin (env: CORS_ALLOW_ORIGIN). Default \"*\".\n -h, --help Show this help.\n\nEndpoints (under --prefix):\n POST /register { session, token } → { ok: true }\n POST /<session>/inbox?token=... body: JSON-RPC frame\n POST /<session>/outbox?token=... body: JSON-RPC frame\n GET /<session>/events?token=...&direction=inbound|outbound\n Server-sent events stream\n POST /<session>/unregister?token=... Tear down session\n GET / Healthcheck → 200\n`;\n\nmain().catch((e) => {\n process.stderr.write(`[relay] fatal: ${e instanceof Error ? e.stack ?? e.message : String(e)}\\n`);\n process.exit(1);\n});\n"]}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { createNodeRelay } from './chunk-GRHPEK6M.js';
2
+ import { createNodeRelay } from './chunk-QOANLXSW.js';
3
3
  import { createServer } from 'http';
4
4
 
5
5
  async function main() {
@@ -66,15 +66,52 @@ var RelayBroker = class {
66
66
  this.subs.delete(id);
67
67
  return true;
68
68
  }
69
- /** Validate an authenticated touch and slide the TTL forward. */
70
- validate(id, token) {
71
- if (!id || !token) return false;
69
+ /**
70
+ * Validate an authenticated touch, saying WHY when it fails.
71
+ *
72
+ * `session_gone` and `invalid_token` are different facts and a client acts on
73
+ * them differently. A gone session means the page has closed or the TTL has
74
+ * elapsed — the NORMAL end of a relay lifecycle, since the browser is the
75
+ * server and no state persists across restarts. A bad token means the caller
76
+ * is wrong.
77
+ *
78
+ * They were collapsed into one boolean and reported as `invalid_token`, so an
79
+ * agent holding a perfectly good token against a closed page was told its
80
+ * credentials were wrong. Not a missing signal — an actively misleading one,
81
+ * which sends the reader to debug an auth path that was never the problem.
82
+ *
83
+ * Missing credentials stay `invalid_token`: a request that never named a
84
+ * session has not discovered a dead one.
85
+ *
86
+ * The reason is deliberately NOT widened to cover a wrong token on a live
87
+ * session — answering `session_gone` there would tell an unauthenticated
88
+ * caller which sessions exist.
89
+ *
90
+ * Requested by the Prism harness while designing a non-Node relay client.
91
+ */
92
+ check(id, token) {
93
+ if (!id || !token) return { ok: false, reason: "invalid_token" };
72
94
  const s = this.store.getSession(id);
73
- if (!s) return false;
74
- if (!timingSafeEqualHex(s.tokenHash, sha256Hex(token))) return false;
95
+ if (!s) return { ok: false, reason: "session_gone" };
96
+ if (!timingSafeEqualHex(s.tokenHash, sha256Hex(token))) {
97
+ return { ok: false, reason: "invalid_token" };
98
+ }
75
99
  s.lastSeen = Date.now();
76
100
  this.store.putSession(s);
77
- return true;
101
+ return { ok: true };
102
+ }
103
+ /**
104
+ * Validate an authenticated touch and slide the TTL forward.
105
+ *
106
+ * Kept as-is for existing callers; `check()` is the one that says why.
107
+ */
108
+ validate(id, token) {
109
+ return this.check(id, token).ok;
110
+ }
111
+ /** Drop a session — the page closed, or a test needs it gone. */
112
+ dropSession(id) {
113
+ this.store.deleteSession(id);
114
+ this.subs.delete(id);
78
115
  }
79
116
  /** Push a frame onto the inbound queue (external agent → browser). */
80
117
  inbox(id, token, payload) {
@@ -312,6 +349,10 @@ function createNodeRelay(opts = {}) {
312
349
  if (!session) return json(res, 400, { error: "missing_session" });
313
350
  const token = getQuery(req).get("token") ?? "";
314
351
  if (direction === "unregister") {
352
+ const state = broker.check(session, token);
353
+ if (!state.ok && state.reason === "session_gone") {
354
+ return json(res, 200, { ok: true, alreadyGone: true });
355
+ }
315
356
  const ok2 = broker.unregister(session, token);
316
357
  return json(res, ok2 ? 200 : 401, ok2 ? { ok: true } : { error: "invalid_token" });
317
358
  }
@@ -321,6 +362,10 @@ function createNodeRelay(opts = {}) {
321
362
  } catch (e) {
322
363
  return json(res, 413, { error: e instanceof Error ? e.message : "payload_error" });
323
364
  }
365
+ const auth = broker.check(session, token);
366
+ if (!auth.ok && auth.reason === "session_gone") {
367
+ return json(res, 410, { error: "session_gone" });
368
+ }
324
369
  const ok = direction === "inbound" ? broker.inbox(session, token, body) : broker.outbox(session, token, body);
325
370
  return json(res, ok ? 200 : 401, ok ? { ok: true } : { error: "invalid_token_or_frame" });
326
371
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/relay-server/core.ts","../src/relay-server/node.ts"],"names":["randomBytes","next","createHash","timingSafeEqual","URL","ok"],"mappings":"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","file":"relay-server.cjs","sourcesContent":["import { createHash, randomBytes, timingSafeEqual } from \"node:crypto\";\n\n/**\n * RelayBroker — pure logic for the SSE+POST tunnel described in\n * docs/relay-protocol.md, hostable in any Node-compatible runtime\n * (Node, Bun, Deno-with-Node-compat, Cloudflare Workers via the Web\n * standards subset). No HTTP framework opinions; this class just\n * stores sessions, validates tokens, enqueues frames, and produces\n * SSE event payloads ready to flush.\n *\n * const broker = new RelayBroker();\n * const reg = broker.register(\"session-id\", \"token\"); // ok / error\n * broker.inbox(\"session-id\", \"token\", '{\"jsonrpc\":\"2.0\",…}'); // enqueue inbound\n * const sub = broker.subscribe(\"session-id\", \"token\", \"inbound\");\n * for await (const payload of sub.frames()) yield encodeSse(payload);\n *\n * Storage is an in-memory Map by default — fine for a single relay\n * process. To run multiple instances behind a load balancer, swap\n * `MemoryStore` for a Redis-backed equivalent (same Store interface).\n */\n\nexport type Direction = \"inbound\" | \"outbound\";\n\nexport type Session = {\n id: string;\n /** SHA-256 hex of the original token. Compared with timing-safe equals. */\n tokenHash: string;\n /** Last touched (ms since epoch). Used for TTL cleanup. */\n lastSeen: number;\n};\n\nexport type Subscriber = {\n id: string;\n direction: Direction;\n queue: string[];\n resolveNext: ((frame: string | null) => void) | null;\n};\n\nexport type RelayBrokerOptions = {\n /** Sessions auto-expire after this many ms of inactivity. Default 4h. */\n ttlMs?: number;\n /** Cleanup tick interval in ms. Default 60 000. */\n reapIntervalMs?: number;\n /** Bring-your-own storage layer (redis, etc.). Defaults to in-memory. */\n store?: Store;\n};\n\nexport interface Store {\n putSession(s: Session): void;\n getSession(id: string): Session | undefined;\n deleteSession(id: string): void;\n /** Used by the reap tick — return ids whose lastSeen < cutoff. */\n expiredSessionIds(cutoff: number): string[];\n}\n\nclass MemoryStore implements Store {\n private sessions = new Map<string, Session>();\n putSession(s: Session) { this.sessions.set(s.id, s); }\n getSession(id: string) { return this.sessions.get(id); }\n deleteSession(id: string) { this.sessions.delete(id); }\n expiredSessionIds(cutoff: number) {\n const out: string[] = [];\n for (const [id, s] of this.sessions) if (s.lastSeen < cutoff) out.push(id);\n return out;\n }\n}\n\nconst SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{4,64}$/;\n\nexport class RelayBroker {\n private readonly ttlMs: number;\n private readonly store: Store;\n /** Per-session, per-direction subscriber list. */\n private subs: Map<string, Map<string, Map<string, Subscriber>>> = new Map();\n private reaper?: ReturnType<typeof setInterval>;\n\n constructor(opts: RelayBrokerOptions = {}) {\n this.ttlMs = opts.ttlMs ?? 4 * 60 * 60 * 1000; // 4h\n this.store = opts.store ?? new MemoryStore();\n const tick = opts.reapIntervalMs ?? 60_000;\n if (tick > 0) {\n this.reaper = setInterval(() => this.reap(), tick);\n // Don't keep the process alive just for the reaper.\n if (typeof (this.reaper as { unref?: () => void }).unref === \"function\") {\n (this.reaper as { unref: () => void }).unref();\n }\n }\n }\n\n dispose() {\n if (this.reaper) clearInterval(this.reaper);\n this.subs.clear();\n }\n\n /** Register a session id + token. Idempotent — same id+token re-registers,\n * different token fails. */\n register(id: string, token: string): { ok: true } | { ok: false; reason: string } {\n if (!SESSION_ID_PATTERN.test(id)) return { ok: false, reason: \"invalid_session_id\" };\n if (typeof token !== \"string\" || token.length < 16 || token.length > 128) {\n return { ok: false, reason: \"invalid_token\" };\n }\n const existing = this.store.getSession(id);\n const hash = sha256Hex(token);\n if (existing) {\n if (!timingSafeEqualHex(existing.tokenHash, hash)) return { ok: false, reason: \"session_taken\" };\n existing.lastSeen = Date.now();\n this.store.putSession(existing);\n return { ok: true };\n }\n this.store.putSession({ id, tokenHash: hash, lastSeen: Date.now() });\n return { ok: true };\n }\n\n unregister(id: string, token: string): boolean {\n if (!this.validate(id, token)) return false;\n this.store.deleteSession(id);\n this.subs.delete(id);\n return true;\n }\n\n /** Validate an authenticated touch and slide the TTL forward. */\n validate(id: string, token: string): boolean {\n if (!id || !token) return false;\n const s = this.store.getSession(id);\n if (!s) return false;\n if (!timingSafeEqualHex(s.tokenHash, sha256Hex(token))) return false;\n s.lastSeen = Date.now();\n this.store.putSession(s);\n return true;\n }\n\n /** Push a frame onto the inbound queue (external agent → browser). */\n inbox(id: string, token: string, payload: string): boolean {\n if (!this.validate(id, token)) return false;\n if (!this.isFrame(payload)) return false;\n this.fanOut(id, \"inbound\", payload);\n return true;\n }\n\n /** Push a frame onto the outbound queue (browser server → external agents). */\n outbox(id: string, token: string, payload: string): boolean {\n if (!this.validate(id, token)) return false;\n if (!this.isFrame(payload)) return false;\n this.fanOut(id, \"outbound\", payload);\n return true;\n }\n\n /**\n * Subscribe to a session's queue for one direction. Returns an iterable\n * the caller (an HTTP handler) pumps as SSE.\n */\n subscribe(id: string, token: string, direction: Direction): SubscribeResult {\n if (!this.validate(id, token)) return { ok: false, reason: \"invalid_token\" };\n const subscriberId = randomBytes(8).toString(\"hex\");\n const subscriber: Subscriber = { id: subscriberId, direction, queue: [], resolveNext: null };\n this.getDirSubs(id, direction).set(subscriberId, subscriber);\n\n // Notify the inbound side (= browser) that an outbound subscriber\n // (= external agent) just connected.\n if (direction === \"outbound\") {\n this.fanOut(\n id,\n \"inbound\",\n JSON.stringify({\n jsonrpc: \"2.0\",\n method: \"notifications/peer_joined\",\n params: { subscriberId, ts: Date.now() },\n }),\n );\n }\n\n const unsubscribe = () => {\n this.getDirSubs(id, direction).delete(subscriberId);\n if (direction === \"outbound\") {\n this.fanOut(\n id,\n \"inbound\",\n JSON.stringify({\n jsonrpc: \"2.0\",\n method: \"notifications/peer_left\",\n params: { subscriberId, ts: Date.now() },\n }),\n );\n }\n };\n\n /** Async generator the HTTP handler drains. Yields raw frame payloads;\n * the handler is responsible for SSE framing (`event: mcp\\ndata: …`). */\n const frames = async function* (this: Subscriber): AsyncGenerator<string, void, void> {\n while (true) {\n if (this.queue.length > 0) {\n const next = this.queue.shift();\n if (next !== undefined) yield next;\n continue;\n }\n const next = await new Promise<string | null>((resolve) => {\n this.resolveNext = resolve;\n });\n this.resolveNext = null;\n if (next === null) return;\n yield next;\n }\n }.bind(subscriber);\n\n return {\n ok: true,\n subscriberId,\n frames: frames(),\n unsubscribe: () => {\n // Wake the generator and let it return cleanly.\n subscriber.resolveNext?.(null);\n unsubscribe();\n },\n };\n }\n\n // ────────────────────────────────────────────────────────────── internals\n\n private getDirSubs(sessionId: string, direction: Direction): Map<string, Subscriber> {\n let bySession = this.subs.get(sessionId);\n if (!bySession) {\n bySession = new Map();\n this.subs.set(sessionId, bySession);\n }\n let byDir = bySession.get(direction);\n if (!byDir) {\n byDir = new Map();\n bySession.set(direction, byDir);\n }\n return byDir;\n }\n\n private fanOut(sessionId: string, direction: Direction, payload: string) {\n const dir = this.subs.get(sessionId)?.get(direction);\n if (!dir) return;\n for (const sub of dir.values()) {\n sub.queue.push(payload);\n sub.resolveNext?.(sub.queue.shift() ?? null);\n }\n }\n\n /**\n * Validate that a peer-supplied payload is a well-formed JSON-RPC 2.0 frame\n * (or batch) before it is fanned out. Previously this was a substring match on\n * `\"jsonrpc\"`, which let any token holder inject spoofed frames — a forged\n * `notifications/peer_joined` / `peer_left` (broker-reserved presence control),\n * a fake `notifications/agent_activity {source:\"user\"}` (\"human took control\"),\n * or a forged response with a matching id. We now parse + shape-check, and\n * reject the broker-reserved control methods (the broker emits those itself,\n * never a peer).\n */\n private isFrame(payload: string): boolean {\n if (payload.length === 0) return false;\n let parsed: unknown;\n try {\n parsed = JSON.parse(payload);\n } catch {\n return false;\n }\n const frames = Array.isArray(parsed) ? parsed : [parsed];\n return frames.length > 0 && frames.every((f) => this.isValidFrame(f));\n }\n\n private isValidFrame(f: unknown): boolean {\n if (!f || typeof f !== \"object\" || Array.isArray(f)) return false;\n const o = f as Record<string, unknown>;\n if (o.jsonrpc !== \"2.0\") return false;\n const method = typeof o.method === \"string\" ? o.method : undefined;\n // Broker-reserved control frames may never arrive from a peer.\n if (method === \"notifications/peer_joined\" || method === \"notifications/peer_left\") return false;\n if (method !== undefined) return true; // request or notification\n // Response: needs an id and exactly one of result / error.\n const hasResult = \"result\" in o;\n const hasError = \"error\" in o;\n return \"id\" in o && hasResult !== hasError;\n }\n\n private reap() {\n const cutoff = Date.now() - this.ttlMs;\n for (const id of this.store.expiredSessionIds(cutoff)) {\n const dirs = this.subs.get(id);\n if (dirs) {\n for (const dir of dirs.values()) {\n for (const sub of dir.values()) sub.resolveNext?.(null);\n }\n }\n this.subs.delete(id);\n this.store.deleteSession(id);\n }\n }\n}\n\nexport type SubscribeResult =\n | { ok: false; reason: string }\n | {\n ok: true;\n subscriberId: string;\n frames: AsyncGenerator<string, void, void>;\n unsubscribe: () => void;\n };\n\nfunction sha256Hex(input: string): string {\n return createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\nfunction timingSafeEqualHex(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, \"hex\"), Buffer.from(b, \"hex\"));\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { URL } from \"node:url\";\nimport { RelayBroker, type RelayBrokerOptions } from \"./core\";\n\n/**\n * Node HTTP adapter for {@link RelayBroker}. Returns a single request\n * handler plus per-route handlers, so you can either drop it into\n * `http.createServer(...)` directly or mount the individual handlers\n * onto your existing Node HTTP framework (Express, Hono w/ node-adapter,\n * native http).\n *\n * const relay = createNodeRelay({ pathPrefix: \"/mcp-relay\" });\n * http.createServer(relay.handler).listen(8787);\n *\n * // Or piecemeal:\n * app.post(\"/mcp-relay/register\", relay.register);\n * app.post(\"/mcp-relay/:s/inbox\", relay.inbox);\n * app.post(\"/mcp-relay/:s/outbox\", relay.outbox);\n * app.get (\"/mcp-relay/:s/events\", relay.events);\n * app.post(\"/mcp-relay/:s/unregister\", relay.unregister);\n */\n\nexport type NodeRelayOptions = RelayBrokerOptions & {\n /** URL path prefix (without trailing slash). Default `\"\"` — handlers\n * expect paths like `/register`, `/{id}/inbox`, etc. directly. */\n pathPrefix?: string;\n /** Comma-separated origins (or `*`) for CORS. Default `*` — relays\n * are typically called cross-origin from the demo host. */\n corsAllowOrigin?: string;\n /**\n * Strict browser-origin allow-list. When set, only these Origins get a\n * matching `Access-Control-Allow-Origin` (the request's own Origin is\n * reflected, never `*`), which blunts DNS-rebinding + hostile cross-origin\n * pages from reading relay responses. Recommended for any browser-facing\n * deployment. When unset, `corsAllowOrigin` (default `*`) is used and the\n * session token is the only auth — pair that with a loopback bind.\n */\n allowedOrigins?: string[];\n};\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => unknown | Promise<unknown>;\n\nexport type NodeRelay = {\n broker: RelayBroker;\n /** Single-handler shape — routes internally based on method + URL. */\n handler: NodeHandler;\n /** Per-route handlers. Each handler ignores the URL prefix and\n * acts on the path remainder, so you can mount them under any\n * prefix in your existing app. */\n register: NodeHandler;\n inbox: NodeHandler;\n outbox: NodeHandler;\n events: NodeHandler;\n unregister: NodeHandler;\n};\n\nexport function createNodeRelay(opts: NodeRelayOptions = {}): NodeRelay {\n const broker = new RelayBroker(opts);\n const prefix = (opts.pathPrefix ?? \"\").replace(/\\/$/, \"\");\n const cors = opts.corsAllowOrigin ?? \"*\";\n\n const allowedOrigins = opts.allowedOrigins;\n\n function setCorsHeaders(res: ServerResponse, req?: IncomingMessage) {\n if (allowedOrigins && allowedOrigins.length) {\n // Strict mode: reflect the request Origin only when it's allow-listed;\n // otherwise emit an origin that no browser will match.\n const origin = req?.headers.origin;\n res.setHeader(\"access-control-allow-origin\", origin && allowedOrigins.includes(origin) ? origin : \"null\");\n res.setHeader(\"vary\", \"origin\");\n } else {\n res.setHeader(\"access-control-allow-origin\", cors);\n }\n res.setHeader(\"access-control-allow-methods\", \"GET, POST, OPTIONS\");\n // `authorization` is forward-compat for a header-borne session token.\n res.setHeader(\"access-control-allow-headers\", \"content-type, x-csrf-token, accept, authorization\");\n res.setHeader(\"access-control-max-age\", \"86400\");\n }\n\n function readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let bytes = 0;\n req.on(\"data\", (chunk: Buffer) => {\n bytes += chunk.length;\n // Cap individual frames at 256 KB — protect against runaway payloads.\n if (bytes > 256 * 1024) {\n reject(new Error(\"payload_too_large\"));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", reject);\n });\n }\n\n function json(res: ServerResponse, status: number, body: unknown): void {\n res.statusCode = status;\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify(body));\n }\n\n function getQuery(req: IncomingMessage): URLSearchParams {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n return u.searchParams;\n }\n\n function getPathname(req: IncomingMessage): string {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n return u.pathname;\n }\n\n const register: NodeHandler = async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"POST\") return json(res, 405, { error: \"method_not_allowed\" });\n let body: string;\n try { body = await readBody(req); } catch (e) {\n return json(res, 413, { error: e instanceof Error ? e.message : \"payload_error\" });\n }\n let parsed: unknown;\n try { parsed = JSON.parse(body); } catch { return json(res, 400, { error: \"invalid_json\" }); }\n const { session, token } = (parsed ?? {}) as { session?: string; token?: string };\n if (typeof session !== \"string\" || typeof token !== \"string\") {\n return json(res, 400, { error: \"missing_fields\" });\n }\n const result = broker.register(session, token);\n if (!result.ok) return json(res, 401, { error: result.reason });\n return json(res, 200, { ok: true });\n };\n\n /** Handler for endpoints with a `{session}` segment. The path matcher\n * caller passes the session id explicitly so this works mounted under\n * any route shape. */\n function makeSessionHandler(\n direction: Direction | \"unregister\",\n ): NodeHandler {\n return async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"POST\") return json(res, 405, { error: \"method_not_allowed\" });\n const session = extractSession(req, prefix);\n if (!session) return json(res, 400, { error: \"missing_session\" });\n const token = getQuery(req).get(\"token\") ?? \"\";\n\n if (direction === \"unregister\") {\n const ok = broker.unregister(session, token);\n return json(res, ok ? 200 : 401, ok ? { ok: true } : { error: \"invalid_token\" });\n }\n\n let body: string;\n try { body = await readBody(req); } catch (e) {\n return json(res, 413, { error: e instanceof Error ? e.message : \"payload_error\" });\n }\n const ok = direction === \"inbound\"\n ? broker.inbox(session, token, body)\n : broker.outbox(session, token, body);\n return json(res, ok ? 200 : 401, ok ? { ok: true } : { error: \"invalid_token_or_frame\" });\n };\n }\n\n const inbox = makeSessionHandler(\"inbound\");\n const outbox = makeSessionHandler(\"outbound\");\n const unregister = makeSessionHandler(\"unregister\");\n\n const events: NodeHandler = async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"GET\") return json(res, 405, { error: \"method_not_allowed\" });\n const session = extractSession(req, prefix);\n if (!session) return json(res, 400, { error: \"missing_session\" });\n const q = getQuery(req);\n const token = q.get(\"token\") ?? \"\";\n const direction = q.get(\"direction\") === \"outbound\" ? \"outbound\" : \"inbound\";\n\n const sub = broker.subscribe(session, token, direction);\n if (!sub.ok) {\n res.statusCode = 401;\n res.setHeader(\"content-type\", \"text/event-stream\");\n res.write(`event: error\\ndata: ${sub.reason}\\n\\n`);\n return res.end();\n }\n\n res.statusCode = 200;\n res.setHeader(\"content-type\", \"text/event-stream\");\n res.setHeader(\"cache-control\", \"no-cache\");\n res.setHeader(\"connection\", \"keep-alive\");\n res.setHeader(\"x-accel-buffering\", \"no\");\n res.write(\"retry: 2000\\n\\n\");\n flush(res);\n\n let heartbeat: ReturnType<typeof setInterval> | null = setInterval(() => {\n res.write(\": keepalive\\n\\n\");\n flush(res);\n }, 15_000);\n if (heartbeat && typeof (heartbeat as { unref?: () => void }).unref === \"function\") {\n (heartbeat as { unref: () => void }).unref();\n }\n\n const cleanup = () => {\n if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }\n sub.unsubscribe();\n };\n req.on(\"close\", cleanup);\n req.on(\"error\", cleanup);\n\n try {\n for await (const frame of sub.frames) {\n res.write(`event: mcp\\ndata: ${frame}\\n\\n`);\n flush(res);\n }\n } catch {\n /* stream ended */\n } finally {\n cleanup();\n res.end();\n }\n };\n\n /**\n * Single handler — routes based on method + path. Useful for mounting\n * via `http.createServer(relay.handler)` without an Express layer.\n */\n const handler: NodeHandler = async (req, res) => {\n const pathname = getPathname(req);\n if (!pathname.startsWith(prefix + \"/\")) {\n return json(res, 404, { error: \"not_found\" });\n }\n const rest = pathname.slice(prefix.length); // \"/register\", \"/<id>/inbox\", etc.\n if (rest === \"/register\") return register(req, res);\n const m = /^\\/([A-Za-z0-9_-]{4,64})\\/(inbox|outbox|events|unregister)$/.exec(rest);\n if (!m) return json(res, 404, { error: \"not_found\" });\n const route = m[2];\n if (route === \"inbox\") return inbox(req, res);\n if (route === \"outbox\") return outbox(req, res);\n if (route === \"events\") return events(req, res);\n if (route === \"unregister\") return unregister(req, res);\n return json(res, 404, { error: \"not_found\" });\n };\n\n return { broker, handler, register, inbox, outbox, events, unregister };\n}\n\nfunction extractSession(req: IncomingMessage, prefix: string): string | null {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n const path = u.pathname;\n if (prefix && !path.startsWith(prefix + \"/\")) return null;\n const rest = prefix ? path.slice(prefix.length) : path;\n const m = /^\\/([A-Za-z0-9_-]{4,64})\\//.exec(rest);\n return m ? m[1] : null;\n}\n\nfunction flush(res: ServerResponse) {\n // Node doesn't expose explicit flush, but write returns false when buffered;\n // explicit flushHeaders + write is enough for SSE in practice.\n const r = res as { flush?: () => void };\n r.flush?.();\n}\n\ntype Direction = \"inbound\" | \"outbound\";\n"]}
1
+ {"version":3,"sources":["../src/relay-server/core.ts","../src/relay-server/node.ts"],"names":["randomBytes","next","createHash","timingSafeEqual","URL","ok"],"mappings":"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","file":"relay-server.cjs","sourcesContent":["import { createHash, randomBytes, timingSafeEqual } from \"node:crypto\";\n\n/**\n * RelayBroker — pure logic for the SSE+POST tunnel described in\n * docs/relay-protocol.md, hostable in any Node-compatible runtime\n * (Node, Bun, Deno-with-Node-compat, Cloudflare Workers via the Web\n * standards subset). No HTTP framework opinions; this class just\n * stores sessions, validates tokens, enqueues frames, and produces\n * SSE event payloads ready to flush.\n *\n * const broker = new RelayBroker();\n * const reg = broker.register(\"session-id\", \"token\"); // ok / error\n * broker.inbox(\"session-id\", \"token\", '{\"jsonrpc\":\"2.0\",…}'); // enqueue inbound\n * const sub = broker.subscribe(\"session-id\", \"token\", \"inbound\");\n * for await (const payload of sub.frames()) yield encodeSse(payload);\n *\n * Storage is an in-memory Map by default — fine for a single relay\n * process. To run multiple instances behind a load balancer, swap\n * `MemoryStore` for a Redis-backed equivalent (same Store interface).\n */\n\nexport type Direction = \"inbound\" | \"outbound\";\n\nexport type Session = {\n id: string;\n /** SHA-256 hex of the original token. Compared with timing-safe equals. */\n tokenHash: string;\n /** Last touched (ms since epoch). Used for TTL cleanup. */\n lastSeen: number;\n};\n\nexport type Subscriber = {\n id: string;\n direction: Direction;\n queue: string[];\n resolveNext: ((frame: string | null) => void) | null;\n};\n\nexport type RelayBrokerOptions = {\n /** Sessions auto-expire after this many ms of inactivity. Default 4h. */\n ttlMs?: number;\n /** Cleanup tick interval in ms. Default 60 000. */\n reapIntervalMs?: number;\n /** Bring-your-own storage layer (redis, etc.). Defaults to in-memory. */\n store?: Store;\n};\n\nexport interface Store {\n putSession(s: Session): void;\n getSession(id: string): Session | undefined;\n deleteSession(id: string): void;\n /** Used by the reap tick — return ids whose lastSeen < cutoff. */\n expiredSessionIds(cutoff: number): string[];\n}\n\nclass MemoryStore implements Store {\n private sessions = new Map<string, Session>();\n putSession(s: Session) { this.sessions.set(s.id, s); }\n getSession(id: string) { return this.sessions.get(id); }\n deleteSession(id: string) { this.sessions.delete(id); }\n expiredSessionIds(cutoff: number) {\n const out: string[] = [];\n for (const [id, s] of this.sessions) if (s.lastSeen < cutoff) out.push(id);\n return out;\n }\n}\n\nconst SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{4,64}$/;\n\nexport class RelayBroker {\n private readonly ttlMs: number;\n private readonly store: Store;\n /** Per-session, per-direction subscriber list. */\n private subs: Map<string, Map<string, Map<string, Subscriber>>> = new Map();\n private reaper?: ReturnType<typeof setInterval>;\n\n constructor(opts: RelayBrokerOptions = {}) {\n this.ttlMs = opts.ttlMs ?? 4 * 60 * 60 * 1000; // 4h\n this.store = opts.store ?? new MemoryStore();\n const tick = opts.reapIntervalMs ?? 60_000;\n if (tick > 0) {\n this.reaper = setInterval(() => this.reap(), tick);\n // Don't keep the process alive just for the reaper.\n if (typeof (this.reaper as { unref?: () => void }).unref === \"function\") {\n (this.reaper as { unref: () => void }).unref();\n }\n }\n }\n\n dispose() {\n if (this.reaper) clearInterval(this.reaper);\n this.subs.clear();\n }\n\n /** Register a session id + token. Idempotent — same id+token re-registers,\n * different token fails. */\n register(id: string, token: string): { ok: true } | { ok: false; reason: string } {\n if (!SESSION_ID_PATTERN.test(id)) return { ok: false, reason: \"invalid_session_id\" };\n if (typeof token !== \"string\" || token.length < 16 || token.length > 128) {\n return { ok: false, reason: \"invalid_token\" };\n }\n const existing = this.store.getSession(id);\n const hash = sha256Hex(token);\n if (existing) {\n if (!timingSafeEqualHex(existing.tokenHash, hash)) return { ok: false, reason: \"session_taken\" };\n existing.lastSeen = Date.now();\n this.store.putSession(existing);\n return { ok: true };\n }\n this.store.putSession({ id, tokenHash: hash, lastSeen: Date.now() });\n return { ok: true };\n }\n\n unregister(id: string, token: string): boolean {\n if (!this.validate(id, token)) return false;\n this.store.deleteSession(id);\n this.subs.delete(id);\n return true;\n }\n\n /**\n * Validate an authenticated touch, saying WHY when it fails.\n *\n * `session_gone` and `invalid_token` are different facts and a client acts on\n * them differently. A gone session means the page has closed or the TTL has\n * elapsed — the NORMAL end of a relay lifecycle, since the browser is the\n * server and no state persists across restarts. A bad token means the caller\n * is wrong.\n *\n * They were collapsed into one boolean and reported as `invalid_token`, so an\n * agent holding a perfectly good token against a closed page was told its\n * credentials were wrong. Not a missing signal — an actively misleading one,\n * which sends the reader to debug an auth path that was never the problem.\n *\n * Missing credentials stay `invalid_token`: a request that never named a\n * session has not discovered a dead one.\n *\n * The reason is deliberately NOT widened to cover a wrong token on a live\n * session — answering `session_gone` there would tell an unauthenticated\n * caller which sessions exist.\n *\n * Requested by the Prism harness while designing a non-Node relay client.\n */\n check(id: string, token: string): { ok: true } | { ok: false; reason: \"invalid_token\" | \"session_gone\" } {\n if (!id || !token) return { ok: false, reason: \"invalid_token\" };\n\n const s = this.store.getSession(id);\n if (!s) return { ok: false, reason: \"session_gone\" };\n\n if (!timingSafeEqualHex(s.tokenHash, sha256Hex(token))) {\n return { ok: false, reason: \"invalid_token\" };\n }\n\n s.lastSeen = Date.now();\n this.store.putSession(s);\n return { ok: true };\n }\n\n /**\n * Validate an authenticated touch and slide the TTL forward.\n *\n * Kept as-is for existing callers; `check()` is the one that says why.\n */\n validate(id: string, token: string): boolean {\n return this.check(id, token).ok;\n }\n\n /** Drop a session — the page closed, or a test needs it gone. */\n dropSession(id: string): void {\n this.store.deleteSession(id);\n this.subs.delete(id);\n }\n\n /** Push a frame onto the inbound queue (external agent → browser). */\n inbox(id: string, token: string, payload: string): boolean {\n if (!this.validate(id, token)) return false;\n if (!this.isFrame(payload)) return false;\n this.fanOut(id, \"inbound\", payload);\n return true;\n }\n\n /** Push a frame onto the outbound queue (browser server → external agents). */\n outbox(id: string, token: string, payload: string): boolean {\n if (!this.validate(id, token)) return false;\n if (!this.isFrame(payload)) return false;\n this.fanOut(id, \"outbound\", payload);\n return true;\n }\n\n /**\n * Subscribe to a session's queue for one direction. Returns an iterable\n * the caller (an HTTP handler) pumps as SSE.\n */\n subscribe(id: string, token: string, direction: Direction): SubscribeResult {\n if (!this.validate(id, token)) return { ok: false, reason: \"invalid_token\" };\n const subscriberId = randomBytes(8).toString(\"hex\");\n const subscriber: Subscriber = { id: subscriberId, direction, queue: [], resolveNext: null };\n this.getDirSubs(id, direction).set(subscriberId, subscriber);\n\n // Notify the inbound side (= browser) that an outbound subscriber\n // (= external agent) just connected.\n if (direction === \"outbound\") {\n this.fanOut(\n id,\n \"inbound\",\n JSON.stringify({\n jsonrpc: \"2.0\",\n method: \"notifications/peer_joined\",\n params: { subscriberId, ts: Date.now() },\n }),\n );\n }\n\n const unsubscribe = () => {\n this.getDirSubs(id, direction).delete(subscriberId);\n if (direction === \"outbound\") {\n this.fanOut(\n id,\n \"inbound\",\n JSON.stringify({\n jsonrpc: \"2.0\",\n method: \"notifications/peer_left\",\n params: { subscriberId, ts: Date.now() },\n }),\n );\n }\n };\n\n /** Async generator the HTTP handler drains. Yields raw frame payloads;\n * the handler is responsible for SSE framing (`event: mcp\\ndata: …`). */\n const frames = async function* (this: Subscriber): AsyncGenerator<string, void, void> {\n while (true) {\n if (this.queue.length > 0) {\n const next = this.queue.shift();\n if (next !== undefined) yield next;\n continue;\n }\n const next = await new Promise<string | null>((resolve) => {\n this.resolveNext = resolve;\n });\n this.resolveNext = null;\n if (next === null) return;\n yield next;\n }\n }.bind(subscriber);\n\n return {\n ok: true,\n subscriberId,\n frames: frames(),\n unsubscribe: () => {\n // Wake the generator and let it return cleanly.\n subscriber.resolveNext?.(null);\n unsubscribe();\n },\n };\n }\n\n // ────────────────────────────────────────────────────────────── internals\n\n private getDirSubs(sessionId: string, direction: Direction): Map<string, Subscriber> {\n let bySession = this.subs.get(sessionId);\n if (!bySession) {\n bySession = new Map();\n this.subs.set(sessionId, bySession);\n }\n let byDir = bySession.get(direction);\n if (!byDir) {\n byDir = new Map();\n bySession.set(direction, byDir);\n }\n return byDir;\n }\n\n private fanOut(sessionId: string, direction: Direction, payload: string) {\n const dir = this.subs.get(sessionId)?.get(direction);\n if (!dir) return;\n for (const sub of dir.values()) {\n sub.queue.push(payload);\n sub.resolveNext?.(sub.queue.shift() ?? null);\n }\n }\n\n /**\n * Validate that a peer-supplied payload is a well-formed JSON-RPC 2.0 frame\n * (or batch) before it is fanned out. Previously this was a substring match on\n * `\"jsonrpc\"`, which let any token holder inject spoofed frames — a forged\n * `notifications/peer_joined` / `peer_left` (broker-reserved presence control),\n * a fake `notifications/agent_activity {source:\"user\"}` (\"human took control\"),\n * or a forged response with a matching id. We now parse + shape-check, and\n * reject the broker-reserved control methods (the broker emits those itself,\n * never a peer).\n */\n private isFrame(payload: string): boolean {\n if (payload.length === 0) return false;\n let parsed: unknown;\n try {\n parsed = JSON.parse(payload);\n } catch {\n return false;\n }\n const frames = Array.isArray(parsed) ? parsed : [parsed];\n return frames.length > 0 && frames.every((f) => this.isValidFrame(f));\n }\n\n private isValidFrame(f: unknown): boolean {\n if (!f || typeof f !== \"object\" || Array.isArray(f)) return false;\n const o = f as Record<string, unknown>;\n if (o.jsonrpc !== \"2.0\") return false;\n const method = typeof o.method === \"string\" ? o.method : undefined;\n // Broker-reserved control frames may never arrive from a peer.\n if (method === \"notifications/peer_joined\" || method === \"notifications/peer_left\") return false;\n if (method !== undefined) return true; // request or notification\n // Response: needs an id and exactly one of result / error.\n const hasResult = \"result\" in o;\n const hasError = \"error\" in o;\n return \"id\" in o && hasResult !== hasError;\n }\n\n private reap() {\n const cutoff = Date.now() - this.ttlMs;\n for (const id of this.store.expiredSessionIds(cutoff)) {\n const dirs = this.subs.get(id);\n if (dirs) {\n for (const dir of dirs.values()) {\n for (const sub of dir.values()) sub.resolveNext?.(null);\n }\n }\n this.subs.delete(id);\n this.store.deleteSession(id);\n }\n }\n}\n\nexport type SubscribeResult =\n | { ok: false; reason: string }\n | {\n ok: true;\n subscriberId: string;\n frames: AsyncGenerator<string, void, void>;\n unsubscribe: () => void;\n };\n\nfunction sha256Hex(input: string): string {\n return createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\nfunction timingSafeEqualHex(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, \"hex\"), Buffer.from(b, \"hex\"));\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { URL } from \"node:url\";\nimport { RelayBroker, type RelayBrokerOptions } from \"./core\";\n\n/**\n * Node HTTP adapter for {@link RelayBroker}. Returns a single request\n * handler plus per-route handlers, so you can either drop it into\n * `http.createServer(...)` directly or mount the individual handlers\n * onto your existing Node HTTP framework (Express, Hono w/ node-adapter,\n * native http).\n *\n * const relay = createNodeRelay({ pathPrefix: \"/mcp-relay\" });\n * http.createServer(relay.handler).listen(8787);\n *\n * // Or piecemeal:\n * app.post(\"/mcp-relay/register\", relay.register);\n * app.post(\"/mcp-relay/:s/inbox\", relay.inbox);\n * app.post(\"/mcp-relay/:s/outbox\", relay.outbox);\n * app.get (\"/mcp-relay/:s/events\", relay.events);\n * app.post(\"/mcp-relay/:s/unregister\", relay.unregister);\n */\n\nexport type NodeRelayOptions = RelayBrokerOptions & {\n /** URL path prefix (without trailing slash). Default `\"\"` — handlers\n * expect paths like `/register`, `/{id}/inbox`, etc. directly. */\n pathPrefix?: string;\n /** Comma-separated origins (or `*`) for CORS. Default `*` — relays\n * are typically called cross-origin from the demo host. */\n corsAllowOrigin?: string;\n /**\n * Strict browser-origin allow-list. When set, only these Origins get a\n * matching `Access-Control-Allow-Origin` (the request's own Origin is\n * reflected, never `*`), which blunts DNS-rebinding + hostile cross-origin\n * pages from reading relay responses. Recommended for any browser-facing\n * deployment. When unset, `corsAllowOrigin` (default `*`) is used and the\n * session token is the only auth — pair that with a loopback bind.\n */\n allowedOrigins?: string[];\n};\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => unknown | Promise<unknown>;\n\nexport type NodeRelay = {\n broker: RelayBroker;\n /** Single-handler shape — routes internally based on method + URL. */\n handler: NodeHandler;\n /** Per-route handlers. Each handler ignores the URL prefix and\n * acts on the path remainder, so you can mount them under any\n * prefix in your existing app. */\n register: NodeHandler;\n inbox: NodeHandler;\n outbox: NodeHandler;\n events: NodeHandler;\n unregister: NodeHandler;\n};\n\nexport function createNodeRelay(opts: NodeRelayOptions = {}): NodeRelay {\n const broker = new RelayBroker(opts);\n const prefix = (opts.pathPrefix ?? \"\").replace(/\\/$/, \"\");\n const cors = opts.corsAllowOrigin ?? \"*\";\n\n const allowedOrigins = opts.allowedOrigins;\n\n function setCorsHeaders(res: ServerResponse, req?: IncomingMessage) {\n if (allowedOrigins && allowedOrigins.length) {\n // Strict mode: reflect the request Origin only when it's allow-listed;\n // otherwise emit an origin that no browser will match.\n const origin = req?.headers.origin;\n res.setHeader(\"access-control-allow-origin\", origin && allowedOrigins.includes(origin) ? origin : \"null\");\n res.setHeader(\"vary\", \"origin\");\n } else {\n res.setHeader(\"access-control-allow-origin\", cors);\n }\n res.setHeader(\"access-control-allow-methods\", \"GET, POST, OPTIONS\");\n // `authorization` is forward-compat for a header-borne session token.\n res.setHeader(\"access-control-allow-headers\", \"content-type, x-csrf-token, accept, authorization\");\n res.setHeader(\"access-control-max-age\", \"86400\");\n }\n\n function readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let bytes = 0;\n req.on(\"data\", (chunk: Buffer) => {\n bytes += chunk.length;\n // Cap individual frames at 256 KB — protect against runaway payloads.\n if (bytes > 256 * 1024) {\n reject(new Error(\"payload_too_large\"));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", reject);\n });\n }\n\n function json(res: ServerResponse, status: number, body: unknown): void {\n res.statusCode = status;\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify(body));\n }\n\n function getQuery(req: IncomingMessage): URLSearchParams {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n return u.searchParams;\n }\n\n function getPathname(req: IncomingMessage): string {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n return u.pathname;\n }\n\n const register: NodeHandler = async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"POST\") return json(res, 405, { error: \"method_not_allowed\" });\n let body: string;\n try { body = await readBody(req); } catch (e) {\n return json(res, 413, { error: e instanceof Error ? e.message : \"payload_error\" });\n }\n let parsed: unknown;\n try { parsed = JSON.parse(body); } catch { return json(res, 400, { error: \"invalid_json\" }); }\n const { session, token } = (parsed ?? {}) as { session?: string; token?: string };\n if (typeof session !== \"string\" || typeof token !== \"string\") {\n return json(res, 400, { error: \"missing_fields\" });\n }\n const result = broker.register(session, token);\n if (!result.ok) return json(res, 401, { error: result.reason });\n return json(res, 200, { ok: true });\n };\n\n /** Handler for endpoints with a `{session}` segment. The path matcher\n * caller passes the session id explicitly so this works mounted under\n * any route shape. */\n function makeSessionHandler(\n direction: Direction | \"unregister\",\n ): NodeHandler {\n return async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"POST\") return json(res, 405, { error: \"method_not_allowed\" });\n const session = extractSession(req, prefix);\n if (!session) return json(res, 400, { error: \"missing_session\" });\n const token = getQuery(req).get(\"token\") ?? \"\";\n\n if (direction === \"unregister\") {\n // Unregistering a session that is already gone is a SUCCESS, not an\n // auth failure. The caller wanted it gone and it is gone; reporting\n // `invalid_token` for a correct token would make a clean shutdown look\n // like a broken one, and a client retrying it would loop.\n const state = broker.check(session, token);\n if (!state.ok && state.reason === \"session_gone\") {\n return json(res, 200, { ok: true, alreadyGone: true });\n }\n\n const ok = broker.unregister(session, token);\n return json(res, ok ? 200 : 401, ok ? { ok: true } : { error: \"invalid_token\" });\n }\n\n let body: string;\n try { body = await readBody(req); } catch (e) {\n return json(res, 413, { error: e instanceof Error ? e.message : \"payload_error\" });\n }\n // A GONE SESSION IS REPORTED AS ITSELF, with 410, before the frame is\n // even considered.\n //\n // Every failure here used to come back as `401 invalid_token_or_frame`,\n // so an agent whose page had simply closed was told its credentials or\n // its JSON were wrong. A relay session ending is the NORMAL end of a\n // lifecycle -- the browser is the server and nothing persists -- so it is\n // the one outcome a server-side client must be able to recognise.\n //\n // 410 Gone rather than 404: the session existed and is now over, which is\n // exactly what the status means, and it distinguishes \"this is finished\"\n // from \"you have the wrong URL\".\n const auth = broker.check(session, token);\n if (!auth.ok && auth.reason === \"session_gone\") {\n return json(res, 410, { error: \"session_gone\" });\n }\n\n const ok = direction === \"inbound\"\n ? broker.inbox(session, token, body)\n : broker.outbox(session, token, body);\n return json(res, ok ? 200 : 401, ok ? { ok: true } : { error: \"invalid_token_or_frame\" });\n };\n }\n\n const inbox = makeSessionHandler(\"inbound\");\n const outbox = makeSessionHandler(\"outbound\");\n const unregister = makeSessionHandler(\"unregister\");\n\n const events: NodeHandler = async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"GET\") return json(res, 405, { error: \"method_not_allowed\" });\n const session = extractSession(req, prefix);\n if (!session) return json(res, 400, { error: \"missing_session\" });\n const q = getQuery(req);\n const token = q.get(\"token\") ?? \"\";\n const direction = q.get(\"direction\") === \"outbound\" ? \"outbound\" : \"inbound\";\n\n const sub = broker.subscribe(session, token, direction);\n if (!sub.ok) {\n res.statusCode = 401;\n res.setHeader(\"content-type\", \"text/event-stream\");\n res.write(`event: error\\ndata: ${sub.reason}\\n\\n`);\n return res.end();\n }\n\n res.statusCode = 200;\n res.setHeader(\"content-type\", \"text/event-stream\");\n res.setHeader(\"cache-control\", \"no-cache\");\n res.setHeader(\"connection\", \"keep-alive\");\n res.setHeader(\"x-accel-buffering\", \"no\");\n res.write(\"retry: 2000\\n\\n\");\n flush(res);\n\n let heartbeat: ReturnType<typeof setInterval> | null = setInterval(() => {\n res.write(\": keepalive\\n\\n\");\n flush(res);\n }, 15_000);\n if (heartbeat && typeof (heartbeat as { unref?: () => void }).unref === \"function\") {\n (heartbeat as { unref: () => void }).unref();\n }\n\n const cleanup = () => {\n if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }\n sub.unsubscribe();\n };\n req.on(\"close\", cleanup);\n req.on(\"error\", cleanup);\n\n try {\n for await (const frame of sub.frames) {\n res.write(`event: mcp\\ndata: ${frame}\\n\\n`);\n flush(res);\n }\n } catch {\n /* stream ended */\n } finally {\n cleanup();\n res.end();\n }\n };\n\n /**\n * Single handler — routes based on method + path. Useful for mounting\n * via `http.createServer(relay.handler)` without an Express layer.\n */\n const handler: NodeHandler = async (req, res) => {\n const pathname = getPathname(req);\n if (!pathname.startsWith(prefix + \"/\")) {\n return json(res, 404, { error: \"not_found\" });\n }\n const rest = pathname.slice(prefix.length); // \"/register\", \"/<id>/inbox\", etc.\n if (rest === \"/register\") return register(req, res);\n const m = /^\\/([A-Za-z0-9_-]{4,64})\\/(inbox|outbox|events|unregister)$/.exec(rest);\n if (!m) return json(res, 404, { error: \"not_found\" });\n const route = m[2];\n if (route === \"inbox\") return inbox(req, res);\n if (route === \"outbox\") return outbox(req, res);\n if (route === \"events\") return events(req, res);\n if (route === \"unregister\") return unregister(req, res);\n return json(res, 404, { error: \"not_found\" });\n };\n\n return { broker, handler, register, inbox, outbox, events, unregister };\n}\n\nfunction extractSession(req: IncomingMessage, prefix: string): string | null {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n const path = u.pathname;\n if (prefix && !path.startsWith(prefix + \"/\")) return null;\n const rest = prefix ? path.slice(prefix.length) : path;\n const m = /^\\/([A-Za-z0-9_-]{4,64})\\//.exec(rest);\n return m ? m[1] : null;\n}\n\nfunction flush(res: ServerResponse) {\n // Node doesn't expose explicit flush, but write returns false when buffered;\n // explicit flushHeaders + write is enough for SSE in practice.\n const r = res as { flush?: () => void };\n r.flush?.();\n}\n\ntype Direction = \"inbound\" | \"outbound\";\n"]}
@@ -1,3 +1,3 @@
1
- export { RelayBroker, createNodeRelay } from './chunk-GRHPEK6M.js';
1
+ export { RelayBroker, createNodeRelay } from './chunk-QOANLXSW.js';
2
2
  //# sourceMappingURL=relay-server.js.map
3
3
  //# sourceMappingURL=relay-server.js.map
@@ -18,6 +18,40 @@ across restarts.
18
18
  Code → the relay is hosted somewhere reachable from both the browser and the
19
19
  agent's machine.
20
20
 
21
+ ## When a session is gone
22
+
23
+ A relay session ends when the page closes or its TTL elapses. That is the
24
+ **normal** end of a lifecycle, not an error condition: the browser is the server,
25
+ it owns the tools and the state, and nothing persists across restarts.
26
+
27
+ So a client must be able to tell "this session is over" from "your credentials
28
+ are wrong". The broker reports them separately:
29
+
30
+ | condition | `check()` | HTTP |
31
+ |---|---|---|
32
+ | live session, good token | `{ok: true}` | `200` |
33
+ | session never existed, or has expired | `{ok: false, reason: "session_gone"}` | **`410 Gone`** |
34
+ | wrong token on a LIVE session | `{ok: false, reason: "invalid_token"}` | `401` |
35
+ | no session id or no token supplied | `{ok: false, reason: "invalid_token"}` | `401` |
36
+
37
+ `410` rather than `404`: the session existed and is now over, which is what the
38
+ status means, and it separates "this is finished" from "you have the wrong URL".
39
+
40
+ Two deliberate choices:
41
+
42
+ - **A wrong token on a live session is never `session_gone`.** Answering
43
+ otherwise would tell an unauthenticated caller which sessions exist.
44
+ - **Unregistering an already-gone session succeeds** (`{ok: true, alreadyGone:
45
+ true}`). The caller wanted it gone and it is gone; reporting an auth failure
46
+ for a correct token would make a clean shutdown look broken, and a client
47
+ retrying it would loop.
48
+
49
+ Every failure previously came back as `401 invalid_token_or_frame`, so an agent
50
+ whose page had simply closed was told its credentials or its JSON were wrong —
51
+ an actively misleading answer that sends the reader to debug an auth path that
52
+ was never the problem. `validate()` still returns a boolean for existing
53
+ callers; `check()` is the one that says why.
54
+
21
55
  ## Connecting a client to a session
22
56
 
23
57
  This doc covers running the **broker**. The agent connects from the other end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/agent-integrations",
3
- "version": "0.42.0",
3
+ "version": "0.43.0",
4
4
  "description": "MCP-driven agent presence in collab sessions: per-session micro-MCP server, pluggable bridges to fancy-* packages, and agent UX components (panel + on-canvas cursor).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/relay-server/core.ts","../src/relay-server/node.ts"],"names":["next","ok"],"mappings":"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","file":"chunk-GRHPEK6M.js","sourcesContent":["import { createHash, randomBytes, timingSafeEqual } from \"node:crypto\";\n\n/**\n * RelayBroker — pure logic for the SSE+POST tunnel described in\n * docs/relay-protocol.md, hostable in any Node-compatible runtime\n * (Node, Bun, Deno-with-Node-compat, Cloudflare Workers via the Web\n * standards subset). No HTTP framework opinions; this class just\n * stores sessions, validates tokens, enqueues frames, and produces\n * SSE event payloads ready to flush.\n *\n * const broker = new RelayBroker();\n * const reg = broker.register(\"session-id\", \"token\"); // ok / error\n * broker.inbox(\"session-id\", \"token\", '{\"jsonrpc\":\"2.0\",…}'); // enqueue inbound\n * const sub = broker.subscribe(\"session-id\", \"token\", \"inbound\");\n * for await (const payload of sub.frames()) yield encodeSse(payload);\n *\n * Storage is an in-memory Map by default — fine for a single relay\n * process. To run multiple instances behind a load balancer, swap\n * `MemoryStore` for a Redis-backed equivalent (same Store interface).\n */\n\nexport type Direction = \"inbound\" | \"outbound\";\n\nexport type Session = {\n id: string;\n /** SHA-256 hex of the original token. Compared with timing-safe equals. */\n tokenHash: string;\n /** Last touched (ms since epoch). Used for TTL cleanup. */\n lastSeen: number;\n};\n\nexport type Subscriber = {\n id: string;\n direction: Direction;\n queue: string[];\n resolveNext: ((frame: string | null) => void) | null;\n};\n\nexport type RelayBrokerOptions = {\n /** Sessions auto-expire after this many ms of inactivity. Default 4h. */\n ttlMs?: number;\n /** Cleanup tick interval in ms. Default 60 000. */\n reapIntervalMs?: number;\n /** Bring-your-own storage layer (redis, etc.). Defaults to in-memory. */\n store?: Store;\n};\n\nexport interface Store {\n putSession(s: Session): void;\n getSession(id: string): Session | undefined;\n deleteSession(id: string): void;\n /** Used by the reap tick — return ids whose lastSeen < cutoff. */\n expiredSessionIds(cutoff: number): string[];\n}\n\nclass MemoryStore implements Store {\n private sessions = new Map<string, Session>();\n putSession(s: Session) { this.sessions.set(s.id, s); }\n getSession(id: string) { return this.sessions.get(id); }\n deleteSession(id: string) { this.sessions.delete(id); }\n expiredSessionIds(cutoff: number) {\n const out: string[] = [];\n for (const [id, s] of this.sessions) if (s.lastSeen < cutoff) out.push(id);\n return out;\n }\n}\n\nconst SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{4,64}$/;\n\nexport class RelayBroker {\n private readonly ttlMs: number;\n private readonly store: Store;\n /** Per-session, per-direction subscriber list. */\n private subs: Map<string, Map<string, Map<string, Subscriber>>> = new Map();\n private reaper?: ReturnType<typeof setInterval>;\n\n constructor(opts: RelayBrokerOptions = {}) {\n this.ttlMs = opts.ttlMs ?? 4 * 60 * 60 * 1000; // 4h\n this.store = opts.store ?? new MemoryStore();\n const tick = opts.reapIntervalMs ?? 60_000;\n if (tick > 0) {\n this.reaper = setInterval(() => this.reap(), tick);\n // Don't keep the process alive just for the reaper.\n if (typeof (this.reaper as { unref?: () => void }).unref === \"function\") {\n (this.reaper as { unref: () => void }).unref();\n }\n }\n }\n\n dispose() {\n if (this.reaper) clearInterval(this.reaper);\n this.subs.clear();\n }\n\n /** Register a session id + token. Idempotent — same id+token re-registers,\n * different token fails. */\n register(id: string, token: string): { ok: true } | { ok: false; reason: string } {\n if (!SESSION_ID_PATTERN.test(id)) return { ok: false, reason: \"invalid_session_id\" };\n if (typeof token !== \"string\" || token.length < 16 || token.length > 128) {\n return { ok: false, reason: \"invalid_token\" };\n }\n const existing = this.store.getSession(id);\n const hash = sha256Hex(token);\n if (existing) {\n if (!timingSafeEqualHex(existing.tokenHash, hash)) return { ok: false, reason: \"session_taken\" };\n existing.lastSeen = Date.now();\n this.store.putSession(existing);\n return { ok: true };\n }\n this.store.putSession({ id, tokenHash: hash, lastSeen: Date.now() });\n return { ok: true };\n }\n\n unregister(id: string, token: string): boolean {\n if (!this.validate(id, token)) return false;\n this.store.deleteSession(id);\n this.subs.delete(id);\n return true;\n }\n\n /** Validate an authenticated touch and slide the TTL forward. */\n validate(id: string, token: string): boolean {\n if (!id || !token) return false;\n const s = this.store.getSession(id);\n if (!s) return false;\n if (!timingSafeEqualHex(s.tokenHash, sha256Hex(token))) return false;\n s.lastSeen = Date.now();\n this.store.putSession(s);\n return true;\n }\n\n /** Push a frame onto the inbound queue (external agent → browser). */\n inbox(id: string, token: string, payload: string): boolean {\n if (!this.validate(id, token)) return false;\n if (!this.isFrame(payload)) return false;\n this.fanOut(id, \"inbound\", payload);\n return true;\n }\n\n /** Push a frame onto the outbound queue (browser server → external agents). */\n outbox(id: string, token: string, payload: string): boolean {\n if (!this.validate(id, token)) return false;\n if (!this.isFrame(payload)) return false;\n this.fanOut(id, \"outbound\", payload);\n return true;\n }\n\n /**\n * Subscribe to a session's queue for one direction. Returns an iterable\n * the caller (an HTTP handler) pumps as SSE.\n */\n subscribe(id: string, token: string, direction: Direction): SubscribeResult {\n if (!this.validate(id, token)) return { ok: false, reason: \"invalid_token\" };\n const subscriberId = randomBytes(8).toString(\"hex\");\n const subscriber: Subscriber = { id: subscriberId, direction, queue: [], resolveNext: null };\n this.getDirSubs(id, direction).set(subscriberId, subscriber);\n\n // Notify the inbound side (= browser) that an outbound subscriber\n // (= external agent) just connected.\n if (direction === \"outbound\") {\n this.fanOut(\n id,\n \"inbound\",\n JSON.stringify({\n jsonrpc: \"2.0\",\n method: \"notifications/peer_joined\",\n params: { subscriberId, ts: Date.now() },\n }),\n );\n }\n\n const unsubscribe = () => {\n this.getDirSubs(id, direction).delete(subscriberId);\n if (direction === \"outbound\") {\n this.fanOut(\n id,\n \"inbound\",\n JSON.stringify({\n jsonrpc: \"2.0\",\n method: \"notifications/peer_left\",\n params: { subscriberId, ts: Date.now() },\n }),\n );\n }\n };\n\n /** Async generator the HTTP handler drains. Yields raw frame payloads;\n * the handler is responsible for SSE framing (`event: mcp\\ndata: …`). */\n const frames = async function* (this: Subscriber): AsyncGenerator<string, void, void> {\n while (true) {\n if (this.queue.length > 0) {\n const next = this.queue.shift();\n if (next !== undefined) yield next;\n continue;\n }\n const next = await new Promise<string | null>((resolve) => {\n this.resolveNext = resolve;\n });\n this.resolveNext = null;\n if (next === null) return;\n yield next;\n }\n }.bind(subscriber);\n\n return {\n ok: true,\n subscriberId,\n frames: frames(),\n unsubscribe: () => {\n // Wake the generator and let it return cleanly.\n subscriber.resolveNext?.(null);\n unsubscribe();\n },\n };\n }\n\n // ────────────────────────────────────────────────────────────── internals\n\n private getDirSubs(sessionId: string, direction: Direction): Map<string, Subscriber> {\n let bySession = this.subs.get(sessionId);\n if (!bySession) {\n bySession = new Map();\n this.subs.set(sessionId, bySession);\n }\n let byDir = bySession.get(direction);\n if (!byDir) {\n byDir = new Map();\n bySession.set(direction, byDir);\n }\n return byDir;\n }\n\n private fanOut(sessionId: string, direction: Direction, payload: string) {\n const dir = this.subs.get(sessionId)?.get(direction);\n if (!dir) return;\n for (const sub of dir.values()) {\n sub.queue.push(payload);\n sub.resolveNext?.(sub.queue.shift() ?? null);\n }\n }\n\n /**\n * Validate that a peer-supplied payload is a well-formed JSON-RPC 2.0 frame\n * (or batch) before it is fanned out. Previously this was a substring match on\n * `\"jsonrpc\"`, which let any token holder inject spoofed frames — a forged\n * `notifications/peer_joined` / `peer_left` (broker-reserved presence control),\n * a fake `notifications/agent_activity {source:\"user\"}` (\"human took control\"),\n * or a forged response with a matching id. We now parse + shape-check, and\n * reject the broker-reserved control methods (the broker emits those itself,\n * never a peer).\n */\n private isFrame(payload: string): boolean {\n if (payload.length === 0) return false;\n let parsed: unknown;\n try {\n parsed = JSON.parse(payload);\n } catch {\n return false;\n }\n const frames = Array.isArray(parsed) ? parsed : [parsed];\n return frames.length > 0 && frames.every((f) => this.isValidFrame(f));\n }\n\n private isValidFrame(f: unknown): boolean {\n if (!f || typeof f !== \"object\" || Array.isArray(f)) return false;\n const o = f as Record<string, unknown>;\n if (o.jsonrpc !== \"2.0\") return false;\n const method = typeof o.method === \"string\" ? o.method : undefined;\n // Broker-reserved control frames may never arrive from a peer.\n if (method === \"notifications/peer_joined\" || method === \"notifications/peer_left\") return false;\n if (method !== undefined) return true; // request or notification\n // Response: needs an id and exactly one of result / error.\n const hasResult = \"result\" in o;\n const hasError = \"error\" in o;\n return \"id\" in o && hasResult !== hasError;\n }\n\n private reap() {\n const cutoff = Date.now() - this.ttlMs;\n for (const id of this.store.expiredSessionIds(cutoff)) {\n const dirs = this.subs.get(id);\n if (dirs) {\n for (const dir of dirs.values()) {\n for (const sub of dir.values()) sub.resolveNext?.(null);\n }\n }\n this.subs.delete(id);\n this.store.deleteSession(id);\n }\n }\n}\n\nexport type SubscribeResult =\n | { ok: false; reason: string }\n | {\n ok: true;\n subscriberId: string;\n frames: AsyncGenerator<string, void, void>;\n unsubscribe: () => void;\n };\n\nfunction sha256Hex(input: string): string {\n return createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\nfunction timingSafeEqualHex(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, \"hex\"), Buffer.from(b, \"hex\"));\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { URL } from \"node:url\";\nimport { RelayBroker, type RelayBrokerOptions } from \"./core\";\n\n/**\n * Node HTTP adapter for {@link RelayBroker}. Returns a single request\n * handler plus per-route handlers, so you can either drop it into\n * `http.createServer(...)` directly or mount the individual handlers\n * onto your existing Node HTTP framework (Express, Hono w/ node-adapter,\n * native http).\n *\n * const relay = createNodeRelay({ pathPrefix: \"/mcp-relay\" });\n * http.createServer(relay.handler).listen(8787);\n *\n * // Or piecemeal:\n * app.post(\"/mcp-relay/register\", relay.register);\n * app.post(\"/mcp-relay/:s/inbox\", relay.inbox);\n * app.post(\"/mcp-relay/:s/outbox\", relay.outbox);\n * app.get (\"/mcp-relay/:s/events\", relay.events);\n * app.post(\"/mcp-relay/:s/unregister\", relay.unregister);\n */\n\nexport type NodeRelayOptions = RelayBrokerOptions & {\n /** URL path prefix (without trailing slash). Default `\"\"` — handlers\n * expect paths like `/register`, `/{id}/inbox`, etc. directly. */\n pathPrefix?: string;\n /** Comma-separated origins (or `*`) for CORS. Default `*` — relays\n * are typically called cross-origin from the demo host. */\n corsAllowOrigin?: string;\n /**\n * Strict browser-origin allow-list. When set, only these Origins get a\n * matching `Access-Control-Allow-Origin` (the request's own Origin is\n * reflected, never `*`), which blunts DNS-rebinding + hostile cross-origin\n * pages from reading relay responses. Recommended for any browser-facing\n * deployment. When unset, `corsAllowOrigin` (default `*`) is used and the\n * session token is the only auth — pair that with a loopback bind.\n */\n allowedOrigins?: string[];\n};\n\nexport type NodeHandler = (req: IncomingMessage, res: ServerResponse) => unknown | Promise<unknown>;\n\nexport type NodeRelay = {\n broker: RelayBroker;\n /** Single-handler shape — routes internally based on method + URL. */\n handler: NodeHandler;\n /** Per-route handlers. Each handler ignores the URL prefix and\n * acts on the path remainder, so you can mount them under any\n * prefix in your existing app. */\n register: NodeHandler;\n inbox: NodeHandler;\n outbox: NodeHandler;\n events: NodeHandler;\n unregister: NodeHandler;\n};\n\nexport function createNodeRelay(opts: NodeRelayOptions = {}): NodeRelay {\n const broker = new RelayBroker(opts);\n const prefix = (opts.pathPrefix ?? \"\").replace(/\\/$/, \"\");\n const cors = opts.corsAllowOrigin ?? \"*\";\n\n const allowedOrigins = opts.allowedOrigins;\n\n function setCorsHeaders(res: ServerResponse, req?: IncomingMessage) {\n if (allowedOrigins && allowedOrigins.length) {\n // Strict mode: reflect the request Origin only when it's allow-listed;\n // otherwise emit an origin that no browser will match.\n const origin = req?.headers.origin;\n res.setHeader(\"access-control-allow-origin\", origin && allowedOrigins.includes(origin) ? origin : \"null\");\n res.setHeader(\"vary\", \"origin\");\n } else {\n res.setHeader(\"access-control-allow-origin\", cors);\n }\n res.setHeader(\"access-control-allow-methods\", \"GET, POST, OPTIONS\");\n // `authorization` is forward-compat for a header-borne session token.\n res.setHeader(\"access-control-allow-headers\", \"content-type, x-csrf-token, accept, authorization\");\n res.setHeader(\"access-control-max-age\", \"86400\");\n }\n\n function readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let bytes = 0;\n req.on(\"data\", (chunk: Buffer) => {\n bytes += chunk.length;\n // Cap individual frames at 256 KB — protect against runaway payloads.\n if (bytes > 256 * 1024) {\n reject(new Error(\"payload_too_large\"));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n req.on(\"error\", reject);\n });\n }\n\n function json(res: ServerResponse, status: number, body: unknown): void {\n res.statusCode = status;\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify(body));\n }\n\n function getQuery(req: IncomingMessage): URLSearchParams {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n return u.searchParams;\n }\n\n function getPathname(req: IncomingMessage): string {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n return u.pathname;\n }\n\n const register: NodeHandler = async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"POST\") return json(res, 405, { error: \"method_not_allowed\" });\n let body: string;\n try { body = await readBody(req); } catch (e) {\n return json(res, 413, { error: e instanceof Error ? e.message : \"payload_error\" });\n }\n let parsed: unknown;\n try { parsed = JSON.parse(body); } catch { return json(res, 400, { error: \"invalid_json\" }); }\n const { session, token } = (parsed ?? {}) as { session?: string; token?: string };\n if (typeof session !== \"string\" || typeof token !== \"string\") {\n return json(res, 400, { error: \"missing_fields\" });\n }\n const result = broker.register(session, token);\n if (!result.ok) return json(res, 401, { error: result.reason });\n return json(res, 200, { ok: true });\n };\n\n /** Handler for endpoints with a `{session}` segment. The path matcher\n * caller passes the session id explicitly so this works mounted under\n * any route shape. */\n function makeSessionHandler(\n direction: Direction | \"unregister\",\n ): NodeHandler {\n return async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"POST\") return json(res, 405, { error: \"method_not_allowed\" });\n const session = extractSession(req, prefix);\n if (!session) return json(res, 400, { error: \"missing_session\" });\n const token = getQuery(req).get(\"token\") ?? \"\";\n\n if (direction === \"unregister\") {\n const ok = broker.unregister(session, token);\n return json(res, ok ? 200 : 401, ok ? { ok: true } : { error: \"invalid_token\" });\n }\n\n let body: string;\n try { body = await readBody(req); } catch (e) {\n return json(res, 413, { error: e instanceof Error ? e.message : \"payload_error\" });\n }\n const ok = direction === \"inbound\"\n ? broker.inbox(session, token, body)\n : broker.outbox(session, token, body);\n return json(res, ok ? 200 : 401, ok ? { ok: true } : { error: \"invalid_token_or_frame\" });\n };\n }\n\n const inbox = makeSessionHandler(\"inbound\");\n const outbox = makeSessionHandler(\"outbound\");\n const unregister = makeSessionHandler(\"unregister\");\n\n const events: NodeHandler = async (req, res) => {\n setCorsHeaders(res, req);\n if (req.method === \"OPTIONS\") { res.statusCode = 204; return res.end(); }\n if (req.method !== \"GET\") return json(res, 405, { error: \"method_not_allowed\" });\n const session = extractSession(req, prefix);\n if (!session) return json(res, 400, { error: \"missing_session\" });\n const q = getQuery(req);\n const token = q.get(\"token\") ?? \"\";\n const direction = q.get(\"direction\") === \"outbound\" ? \"outbound\" : \"inbound\";\n\n const sub = broker.subscribe(session, token, direction);\n if (!sub.ok) {\n res.statusCode = 401;\n res.setHeader(\"content-type\", \"text/event-stream\");\n res.write(`event: error\\ndata: ${sub.reason}\\n\\n`);\n return res.end();\n }\n\n res.statusCode = 200;\n res.setHeader(\"content-type\", \"text/event-stream\");\n res.setHeader(\"cache-control\", \"no-cache\");\n res.setHeader(\"connection\", \"keep-alive\");\n res.setHeader(\"x-accel-buffering\", \"no\");\n res.write(\"retry: 2000\\n\\n\");\n flush(res);\n\n let heartbeat: ReturnType<typeof setInterval> | null = setInterval(() => {\n res.write(\": keepalive\\n\\n\");\n flush(res);\n }, 15_000);\n if (heartbeat && typeof (heartbeat as { unref?: () => void }).unref === \"function\") {\n (heartbeat as { unref: () => void }).unref();\n }\n\n const cleanup = () => {\n if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }\n sub.unsubscribe();\n };\n req.on(\"close\", cleanup);\n req.on(\"error\", cleanup);\n\n try {\n for await (const frame of sub.frames) {\n res.write(`event: mcp\\ndata: ${frame}\\n\\n`);\n flush(res);\n }\n } catch {\n /* stream ended */\n } finally {\n cleanup();\n res.end();\n }\n };\n\n /**\n * Single handler — routes based on method + path. Useful for mounting\n * via `http.createServer(relay.handler)` without an Express layer.\n */\n const handler: NodeHandler = async (req, res) => {\n const pathname = getPathname(req);\n if (!pathname.startsWith(prefix + \"/\")) {\n return json(res, 404, { error: \"not_found\" });\n }\n const rest = pathname.slice(prefix.length); // \"/register\", \"/<id>/inbox\", etc.\n if (rest === \"/register\") return register(req, res);\n const m = /^\\/([A-Za-z0-9_-]{4,64})\\/(inbox|outbox|events|unregister)$/.exec(rest);\n if (!m) return json(res, 404, { error: \"not_found\" });\n const route = m[2];\n if (route === \"inbox\") return inbox(req, res);\n if (route === \"outbox\") return outbox(req, res);\n if (route === \"events\") return events(req, res);\n if (route === \"unregister\") return unregister(req, res);\n return json(res, 404, { error: \"not_found\" });\n };\n\n return { broker, handler, register, inbox, outbox, events, unregister };\n}\n\nfunction extractSession(req: IncomingMessage, prefix: string): string | null {\n const host = req.headers.host || \"x\";\n const u = new URL(req.url || \"/\", `http://${host}`);\n const path = u.pathname;\n if (prefix && !path.startsWith(prefix + \"/\")) return null;\n const rest = prefix ? path.slice(prefix.length) : path;\n const m = /^\\/([A-Za-z0-9_-]{4,64})\\//.exec(rest);\n return m ? m[1] : null;\n}\n\nfunction flush(res: ServerResponse) {\n // Node doesn't expose explicit flush, but write returns false when buffered;\n // explicit flushHeaders + write is enough for SSE in practice.\n const r = res as { flush?: () => void };\n r.flush?.();\n}\n\ntype Direction = \"inbound\" | \"outbound\";\n"]}