@particle-academy/agent-integrations 0.45.0 → 0.46.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 +10 -3
- package/dist/{chunk-OZBUPO4K.js → chunk-5W34X4EY.js} +263 -58
- package/dist/chunk-5W34X4EY.js.map +1 -0
- package/dist/relay-server/index.d.cts +110 -26
- package/dist/relay-server/index.d.ts +110 -26
- package/dist/relay-server-cli.cjs +374 -89
- package/dist/relay-server-cli.cjs.map +1 -1
- package/dist/relay-server-cli.js +120 -38
- package/dist/relay-server-cli.js.map +1 -1
- package/dist/relay-server.cjs +255 -51
- package/dist/relay-server.cjs.map +1 -1
- package/dist/relay-server.js +1 -1
- package/docs/relay-protocol.md +41 -5
- package/docs/relay-server.md +102 -15
- package/package.json +7 -1
- package/dist/chunk-OZBUPO4K.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/relay-server/cli.ts"],"names":[],"mappings":"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","file":"relay-server-cli.js","sourcesContent":["#!/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/args.ts","../src/relay-server/cli.ts"],"names":[],"mappings":"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","file":"relay-server-cli.js","sourcesContent":["import { describeCorsPolicy, parseCorsOrigins, type CorsPolicy } from \"./cors\";\n\n/**\n * Settings resolution for `agent-integrations-relay`.\n *\n * **Precedence, for every setting: flag, then env var, then default.** A flag\n * is what the person starting the process typed; an env var is ambient and is\n * often set by a platform (Forge sets `PORT`; this package's Dockerfile sets\n * `HOST=0.0.0.0`), so the explicit one wins. When a flag overrides an env var\n * that is also set to something different, the override is reported in\n * `warnings` rather than applied silently — the relay's own deploy README once\n * told operators to set `CORS_ALLOW_ORIGIN` to override a start script that\n * passed `--cors`, and nothing ever said the env var was being ignored.\n *\n * An env var set to the empty string counts as unset, which is how platform\n * env editors write a cleared value.\n */\n\nexport type SettingSource = \"flag\" | \"env\" | \"default\";\n\nexport type RelayCliConfig = {\n port: number;\n host: string;\n prefix: string;\n ttlMs: number;\n cors: CorsPolicy;\n sources: { port: SettingSource; host: SettingSource; prefix: SettingSource; ttlMs: SettingSource; cors: SettingSource };\n warnings: string[];\n help: boolean;\n};\n\nexport type ParseResult = { ok: true; config: RelayCliConfig } | { ok: false; error: string };\n\ntype Key = \"port\" | \"host\" | \"prefix\" | \"ttlMs\" | \"cors\";\n\nconst SETTINGS: Record<Key, { flag: string; env: string; fallback: string }> = {\n port: { flag: \"--port\", env: \"PORT\", fallback: \"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\n // operator explicitly opts in with --host 0.0.0.0.\n host: { flag: \"--host\", env: \"HOST\", fallback: \"127.0.0.1\" },\n prefix: { flag: \"--prefix\", env: \"PREFIX\", fallback: \"\" },\n ttlMs: { flag: \"--ttl-ms\", env: \"TTL_MS\", fallback: String(4 * 60 * 60 * 1000) },\n cors: { flag: \"--cors\", env: \"CORS_ALLOW_ORIGIN\", fallback: \"*\" },\n};\n\nexport function parseRelayArgs(argv: readonly string[], env: Readonly<Record<string, string | undefined>>): ParseResult {\n const flags: Partial<Record<Key, string>> = {};\n let help = false;\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]!;\n if (arg === \"-h\" || arg === \"--help\") {\n help = true;\n continue;\n }\n\n const key = (Object.keys(SETTINGS) as Key[]).find((k) => SETTINGS[k].flag === arg);\n if (!key) return { ok: false, error: `unknown flag: ${arg}` };\n\n const value = argv[i + 1];\n if (value === undefined || value.startsWith(\"--\")) return { ok: false, error: `${arg} needs a value` };\n i++;\n\n // --cors is a LIST, so repeating it adds origins. Every other flag is a\n // single value, and the last one given wins.\n flags[key] = key === \"cors\" && flags.cors !== undefined ? `${flags.cors},${value}` : value;\n }\n\n const warnings: string[] = [];\n const raw = {} as Record<Key, string>;\n const sources = {} as Record<Key, SettingSource>;\n\n for (const key of Object.keys(SETTINGS) as Key[]) {\n const { flag, env: envName, fallback } = SETTINGS[key];\n const fromEnv = env[envName] === \"\" ? undefined : env[envName];\n const fromFlag = flags[key];\n\n if (fromFlag !== undefined) {\n raw[key] = fromFlag;\n sources[key] = \"flag\";\n if (fromEnv !== undefined && fromEnv !== fromFlag) {\n warnings.push(`${envName}=${fromEnv} is ignored: ${flag} ${fromFlag} was passed, and a flag wins over its env var.`);\n }\n } else if (fromEnv !== undefined) {\n raw[key] = fromEnv;\n sources[key] = \"env\";\n } else {\n raw[key] = fallback;\n sources[key] = \"default\";\n }\n }\n\n const named = (key: Key) => (sources[key] === \"env\" ? SETTINGS[key].env : SETTINGS[key].flag);\n\n const port = Number(raw.port);\n if (!Number.isInteger(port) || port <= 0 || port > 65535) {\n return { ok: false, error: `invalid ${named(\"port\")}: ${raw.port}` };\n }\n\n const ttlMs = Number(raw.ttlMs);\n if (!Number.isFinite(ttlMs) || ttlMs <= 0) {\n return { ok: false, error: `invalid ${named(\"ttlMs\")}: ${raw.ttlMs}` };\n }\n\n let cors: CorsPolicy;\n try {\n cors = parseCorsOrigins(raw.cors);\n } catch (e) {\n return { ok: false, error: `invalid ${named(\"cors\")}: ${e instanceof Error ? e.message : String(e)}` };\n }\n\n // Same-value warnings compare raw strings; compare the parsed policies too, so\n // `--cors \"https://a,https://b\"` against `CORS_ALLOW_ORIGIN=\"https://a, https://b\"`\n // is not reported as a conflict.\n if (sources.cors === \"flag\" && env.CORS_ALLOW_ORIGIN) {\n try {\n if (describeCorsPolicy(parseCorsOrigins(env.CORS_ALLOW_ORIGIN)) === describeCorsPolicy(cors)) {\n const i = warnings.findIndex((w) => w.startsWith(\"CORS_ALLOW_ORIGIN=\"));\n if (i !== -1) warnings.splice(i, 1);\n }\n } catch {\n /* an unparseable env var that lost to the flag stays reported */\n }\n }\n\n return {\n ok: true,\n config: {\n port,\n host: raw.host,\n prefix: raw.prefix,\n ttlMs,\n cors,\n sources: sources as RelayCliConfig[\"sources\"],\n warnings,\n help,\n },\n };\n}\n","#!/usr/bin/env node\nimport { createServer } from \"node:http\";\nimport { parseRelayArgs } from \"./args\";\nimport { describeCorsPolicy } from \"./cors\";\nimport { createNodeRelay } from \"./node\";\n\n/**\n * `agent-integrations-relay` — standalone Node HTTP server hosting the relay\n * broker. End users hit `Start share` on a demo page, get a session URL pointing\n * at this service, and paste it into their MCP client. No state persists across\n * restarts.\n *\n * Settings resolve flag → env var → default; see ./args.ts and HELP below.\n *\n * Health: any request to `/` returns 200 OK so platform health checks pass\n * without authenticating.\n */\nasync function main() {\n const parsed = parseRelayArgs(process.argv.slice(2), process.env);\n if (!parsed.ok) {\n process.stderr.write(`[relay] ${parsed.error}\\n`);\n process.exit(2);\n }\n\n const { config } = parsed;\n if (config.help) {\n process.stdout.write(HELP);\n process.exit(0);\n }\n\n for (const warning of config.warnings) {\n process.stderr.write(`[relay] WARNING: ${warning}\\n`);\n }\n\n const { port, host, prefix, ttlMs, cors } = config;\n const relay = createNodeRelay({\n pathPrefix: prefix,\n ttlMs,\n corsAllowOrigin: cors.kind === \"any\" ? \"*\" : cors.origins.join(\",\"),\n });\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\n const corsSource = { flag: \"--cors\", env: \"CORS_ALLOW_ORIGIN\", default: \"default\" }[config.sources.cors];\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=${describeCorsPolicy(cors)} from ${corsSource})\\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 <origins> Allowed browser origins (env: CORS_ALLOW_ORIGIN). Default \"*\".\n \"*\", or a comma-separated list such as\n \"https://example.com,https://www.example.com\". May be\n repeated. A listed Origin is echoed back with Vary: Origin;\n an unlisted one gets no Access-Control-Allow-Origin.\n -h, --help Show this help.\n\nPrecedence: a flag wins over its env var, which wins over the default. An env\nvar that a flag overrides is reported at startup. An empty env var is unset.\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 GET /<session>/poll?token=...&direction=inbound|outbound&wait=<ms>&subscriber=<id>\n Long-poll → { subscriber, frames }\n POST /<session>/unregister?token=... Tear down session\n GET / Healthcheck → 200\n\nA session that has ended answers 410 {\"error\":\"session_gone\"} on every session\nroute; a wrong or missing token answers 401.\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"]}
|
package/dist/relay-server.cjs
CHANGED
|
@@ -24,6 +24,7 @@ var MemoryStore = class {
|
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
var SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{4,64}$/;
|
|
27
|
+
var SUBSCRIBER_ID_PATTERN = /^[a-f0-9]{16}$/;
|
|
27
28
|
var RelayBroker = class {
|
|
28
29
|
constructor(opts = {}) {
|
|
29
30
|
/** Per-session, per-direction subscriber list. */
|
|
@@ -31,6 +32,7 @@ var RelayBroker = class {
|
|
|
31
32
|
/** Per-session map of JSON-RPC request id -> the client that asked. */
|
|
32
33
|
this.callers = /* @__PURE__ */ new Map();
|
|
33
34
|
this.ttlMs = opts.ttlMs ?? 4 * 60 * 60 * 1e3;
|
|
35
|
+
this.pollIdleMs = opts.pollIdleMs ?? 6e4;
|
|
34
36
|
this.store = opts.store ?? new MemoryStore();
|
|
35
37
|
const tick = opts.reapIntervalMs ?? 6e4;
|
|
36
38
|
if (tick > 0) {
|
|
@@ -42,6 +44,7 @@ var RelayBroker = class {
|
|
|
42
44
|
}
|
|
43
45
|
dispose() {
|
|
44
46
|
if (this.reaper) clearInterval(this.reaper);
|
|
47
|
+
for (const id of [...this.subs.keys()]) this.endSubscribers(id);
|
|
45
48
|
this.subs.clear();
|
|
46
49
|
this.callers.clear();
|
|
47
50
|
}
|
|
@@ -66,6 +69,7 @@ var RelayBroker = class {
|
|
|
66
69
|
unregister(id, token) {
|
|
67
70
|
if (!this.validate(id, token)) return false;
|
|
68
71
|
this.store.deleteSession(id);
|
|
72
|
+
this.endSubscribers(id);
|
|
69
73
|
this.subs.delete(id);
|
|
70
74
|
this.callers.delete(id);
|
|
71
75
|
return true;
|
|
@@ -115,6 +119,7 @@ var RelayBroker = class {
|
|
|
115
119
|
/** Drop a session — the page closed, or a test needs it gone. */
|
|
116
120
|
dropSession(id) {
|
|
117
121
|
this.store.deleteSession(id);
|
|
122
|
+
this.endSubscribers(id);
|
|
118
123
|
this.subs.delete(id);
|
|
119
124
|
this.callers.delete(id);
|
|
120
125
|
}
|
|
@@ -145,7 +150,8 @@ var RelayBroker = class {
|
|
|
145
150
|
* the caller (an HTTP handler) pumps as SSE.
|
|
146
151
|
*/
|
|
147
152
|
subscribe(id, token, direction, opts = {}) {
|
|
148
|
-
|
|
153
|
+
const auth = this.check(id, token);
|
|
154
|
+
if (!auth.ok) return auth;
|
|
149
155
|
const subscriberId = crypto.randomBytes(8).toString("hex");
|
|
150
156
|
const subscriber = {
|
|
151
157
|
id: subscriberId,
|
|
@@ -155,31 +161,7 @@ var RelayBroker = class {
|
|
|
155
161
|
client: opts.client
|
|
156
162
|
};
|
|
157
163
|
this.getDirSubs(id, direction).set(subscriberId, subscriber);
|
|
158
|
-
if (direction === "outbound")
|
|
159
|
-
this.fanOut(
|
|
160
|
-
id,
|
|
161
|
-
"inbound",
|
|
162
|
-
JSON.stringify({
|
|
163
|
-
jsonrpc: "2.0",
|
|
164
|
-
method: "notifications/peer_joined",
|
|
165
|
-
params: { subscriberId, ts: Date.now() }
|
|
166
|
-
})
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
const unsubscribe = () => {
|
|
170
|
-
this.getDirSubs(id, direction).delete(subscriberId);
|
|
171
|
-
if (direction === "outbound") {
|
|
172
|
-
this.fanOut(
|
|
173
|
-
id,
|
|
174
|
-
"inbound",
|
|
175
|
-
JSON.stringify({
|
|
176
|
-
jsonrpc: "2.0",
|
|
177
|
-
method: "notifications/peer_left",
|
|
178
|
-
params: { subscriberId, ts: Date.now() }
|
|
179
|
-
})
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
};
|
|
164
|
+
if (direction === "outbound") this.announcePeer(id, "notifications/peer_joined", subscriberId);
|
|
183
165
|
const frames = async function* () {
|
|
184
166
|
while (true) {
|
|
185
167
|
if (this.queue.length > 0) {
|
|
@@ -187,6 +169,7 @@ var RelayBroker = class {
|
|
|
187
169
|
if (next2 !== void 0) yield next2;
|
|
188
170
|
continue;
|
|
189
171
|
}
|
|
172
|
+
if (this.ended) return;
|
|
190
173
|
const next = await new Promise((resolve) => {
|
|
191
174
|
this.resolveNext = resolve;
|
|
192
175
|
});
|
|
@@ -200,8 +183,88 @@ var RelayBroker = class {
|
|
|
200
183
|
subscriberId,
|
|
201
184
|
frames: frames(),
|
|
202
185
|
unsubscribe: () => {
|
|
203
|
-
|
|
204
|
-
|
|
186
|
+
this.endSubscriber(subscriber);
|
|
187
|
+
this.removeSubscriber(id, subscriber);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Attach a LONG-POLL subscriber, or re-attach the one a previous poll created.
|
|
193
|
+
*
|
|
194
|
+
* The CDN-safe receive leg: a series of short requests instead of one open
|
|
195
|
+
* stream, because Cloudflare's HTTP/3 edge resets long-lived SSE. Same
|
|
196
|
+
* contract as the PHP relay in px-ui-sandbox and the one
|
|
197
|
+
* `@particle-academy/fancy-cf-relay` and `mcp-relay-client` speak:
|
|
198
|
+
*
|
|
199
|
+
* - The first poll gets a fresh 16-hex subscriber id; the client echoes it back
|
|
200
|
+
* as `subscriber` on every later poll and reads the same queue.
|
|
201
|
+
* - An id in that shape that this session does not know (pruned, say) is
|
|
202
|
+
* adopted as a NEW subscriber under that id, as the PHP relay does, so a
|
|
203
|
+
* client that went quiet and came back keeps its identity.
|
|
204
|
+
* - An id belonging to a STREAMING subscriber is never adopted: one queue read
|
|
205
|
+
* by two consumers splits its frames between them.
|
|
206
|
+
*
|
|
207
|
+
* Frames queue on the subscriber between polls, so nothing is lost in the gap
|
|
208
|
+
* between one request ending and the next arriving. A poller that stops is
|
|
209
|
+
* pruned after `pollIdleMs`.
|
|
210
|
+
*/
|
|
211
|
+
poll(id, token, direction, opts = {}) {
|
|
212
|
+
const auth = this.check(id, token);
|
|
213
|
+
if (!auth.ok) return auth;
|
|
214
|
+
const dir = this.getDirSubs(id, direction);
|
|
215
|
+
const requested = opts.subscriber && SUBSCRIBER_ID_PATTERN.test(opts.subscriber) ? opts.subscriber : void 0;
|
|
216
|
+
const existing = requested ? dir.get(requested) : void 0;
|
|
217
|
+
let subscriber;
|
|
218
|
+
let created;
|
|
219
|
+
if (existing?.poll) {
|
|
220
|
+
subscriber = existing;
|
|
221
|
+
created = false;
|
|
222
|
+
} else {
|
|
223
|
+
const subscriberId = requested && !existing ? requested : crypto.randomBytes(8).toString("hex");
|
|
224
|
+
subscriber = {
|
|
225
|
+
id: subscriberId,
|
|
226
|
+
direction,
|
|
227
|
+
queue: [],
|
|
228
|
+
resolveNext: null,
|
|
229
|
+
client: opts.client,
|
|
230
|
+
poll: { lastSeen: Date.now(), parked: 0, wakers: /* @__PURE__ */ new Set() }
|
|
231
|
+
};
|
|
232
|
+
dir.set(subscriberId, subscriber);
|
|
233
|
+
created = true;
|
|
234
|
+
if (direction === "outbound") this.announcePeer(id, "notifications/peer_joined", subscriberId);
|
|
235
|
+
}
|
|
236
|
+
const state = subscriber.poll;
|
|
237
|
+
state.lastSeen = Date.now();
|
|
238
|
+
return {
|
|
239
|
+
ok: true,
|
|
240
|
+
subscriberId: subscriber.id,
|
|
241
|
+
created,
|
|
242
|
+
wait: async (ms, signal) => {
|
|
243
|
+
state.parked++;
|
|
244
|
+
state.lastSeen = Date.now();
|
|
245
|
+
try {
|
|
246
|
+
if (subscriber.queue.length === 0 && ms > 0 && !subscriber.ended && !signal?.aborted) {
|
|
247
|
+
await new Promise((resolve) => {
|
|
248
|
+
const done = () => {
|
|
249
|
+
clearTimeout(timer);
|
|
250
|
+
signal?.removeEventListener("abort", done);
|
|
251
|
+
state.wakers.delete(done);
|
|
252
|
+
resolve();
|
|
253
|
+
};
|
|
254
|
+
const timer = setTimeout(done, ms);
|
|
255
|
+
state.wakers.add(done);
|
|
256
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (signal?.aborted) return [];
|
|
260
|
+
return subscriber.queue.splice(0);
|
|
261
|
+
} finally {
|
|
262
|
+
state.parked--;
|
|
263
|
+
state.lastSeen = Date.now();
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
requeue: (frames) => {
|
|
267
|
+
if (frames.length > 0 && !subscriber.ended) subscriber.queue.unshift(...frames);
|
|
205
268
|
}
|
|
206
269
|
};
|
|
207
270
|
}
|
|
@@ -294,16 +357,78 @@ var RelayBroker = class {
|
|
|
294
357
|
if (!dir) return;
|
|
295
358
|
for (const sub of dir.values()) {
|
|
296
359
|
if (sub.client !== client) continue;
|
|
297
|
-
|
|
298
|
-
sub.resolveNext?.(sub.queue.shift() ?? null);
|
|
360
|
+
this.deliver(sub, payload);
|
|
299
361
|
}
|
|
300
362
|
}
|
|
301
363
|
fanOut(sessionId, direction, payload) {
|
|
302
364
|
const dir = this.subs.get(sessionId)?.get(direction);
|
|
303
365
|
if (!dir) return;
|
|
304
|
-
for (const sub of dir.values())
|
|
305
|
-
|
|
306
|
-
|
|
366
|
+
for (const sub of dir.values()) this.deliver(sub, payload);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Hand one frame to one subscriber.
|
|
370
|
+
*
|
|
371
|
+
* The resolver is CLEARED before it is called. It used to stay set until the
|
|
372
|
+
* generator resumed on a later microtask, so a second frame in the same tick
|
|
373
|
+
* called an already-settled resolver — a no-op — after shifting its frame out
|
|
374
|
+
* of the queue, and that frame was lost. Cleared first, the second frame just
|
|
375
|
+
* waits in the queue for the generator to come back for it.
|
|
376
|
+
*/
|
|
377
|
+
deliver(sub, payload) {
|
|
378
|
+
if (sub.ended) return;
|
|
379
|
+
sub.queue.push(payload);
|
|
380
|
+
const resolve = sub.resolveNext;
|
|
381
|
+
if (resolve) {
|
|
382
|
+
sub.resolveNext = null;
|
|
383
|
+
resolve(sub.queue.shift() ?? null);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (sub.poll) for (const wake of [...sub.poll.wakers]) wake();
|
|
387
|
+
}
|
|
388
|
+
/** Tell the page (the inbound side) that an agent arrived or left. */
|
|
389
|
+
announcePeer(sessionId, method, subscriberId) {
|
|
390
|
+
this.fanOut(sessionId, "inbound", JSON.stringify({ jsonrpc: "2.0", method, params: { subscriberId, ts: Date.now() } }));
|
|
391
|
+
}
|
|
392
|
+
/** End one subscriber: close its stream, or release its parked polls. Idempotent. */
|
|
393
|
+
endSubscriber(sub) {
|
|
394
|
+
if (sub.ended) return;
|
|
395
|
+
sub.ended = true;
|
|
396
|
+
const resolve = sub.resolveNext;
|
|
397
|
+
sub.resolveNext = null;
|
|
398
|
+
resolve?.(null);
|
|
399
|
+
if (sub.poll) for (const wake of [...sub.poll.wakers]) wake();
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* End every subscriber of a session whose session is going away.
|
|
403
|
+
*
|
|
404
|
+
* Streams used to be dropped from the map WITHOUT being woken, so an SSE leg
|
|
405
|
+
* stayed open, heartbeating, for a session that no longer existed — and its
|
|
406
|
+
* client could not learn the page had closed. Ended, the stream closes, the
|
|
407
|
+
* client reconnects, and `events` answers 410.
|
|
408
|
+
*/
|
|
409
|
+
endSubscribers(sessionId) {
|
|
410
|
+
const dirs = this.subs.get(sessionId);
|
|
411
|
+
if (!dirs) return;
|
|
412
|
+
for (const dir of dirs.values()) for (const sub of dir.values()) this.endSubscriber(sub);
|
|
413
|
+
}
|
|
414
|
+
/** Remove a subscriber from its session, announcing an agent's departure once. */
|
|
415
|
+
removeSubscriber(sessionId, sub) {
|
|
416
|
+
const dir = this.subs.get(sessionId)?.get(sub.direction);
|
|
417
|
+
if (!dir || dir.get(sub.id) !== sub) return;
|
|
418
|
+
dir.delete(sub.id);
|
|
419
|
+
if (sub.direction === "outbound") this.announcePeer(sessionId, "notifications/peer_left", sub.id);
|
|
420
|
+
}
|
|
421
|
+
/** Drop long-poll subscribers that stopped polling. Never one with a poll parked. */
|
|
422
|
+
prunePollers(now) {
|
|
423
|
+
for (const [sessionId, dirs] of this.subs) {
|
|
424
|
+
for (const dir of dirs.values()) {
|
|
425
|
+
for (const sub of [...dir.values()]) {
|
|
426
|
+
if (!sub.poll || sub.poll.parked > 0) continue;
|
|
427
|
+
if (now - sub.poll.lastSeen <= this.pollIdleMs) continue;
|
|
428
|
+
this.endSubscriber(sub);
|
|
429
|
+
this.removeSubscriber(sessionId, sub);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
307
432
|
}
|
|
308
433
|
}
|
|
309
434
|
/**
|
|
@@ -339,18 +464,15 @@ var RelayBroker = class {
|
|
|
339
464
|
return "id" in o && hasResult !== hasError;
|
|
340
465
|
}
|
|
341
466
|
reap() {
|
|
342
|
-
const
|
|
467
|
+
const now = Date.now();
|
|
468
|
+
const cutoff = now - this.ttlMs;
|
|
343
469
|
for (const id of this.store.expiredSessionIds(cutoff)) {
|
|
344
|
-
|
|
345
|
-
if (dirs) {
|
|
346
|
-
for (const dir of dirs.values()) {
|
|
347
|
-
for (const sub of dir.values()) sub.resolveNext?.(null);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
470
|
+
this.endSubscribers(id);
|
|
350
471
|
this.subs.delete(id);
|
|
351
472
|
this.callers.delete(id);
|
|
352
473
|
this.store.deleteSession(id);
|
|
353
474
|
}
|
|
475
|
+
this.prunePollers(now);
|
|
354
476
|
}
|
|
355
477
|
};
|
|
356
478
|
function sha256Hex(input) {
|
|
@@ -360,19 +482,68 @@ function timingSafeEqualHex(a, b) {
|
|
|
360
482
|
if (a.length !== b.length) return false;
|
|
361
483
|
return crypto.timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
362
484
|
}
|
|
485
|
+
|
|
486
|
+
// src/relay-server/cors.ts
|
|
487
|
+
function parseCorsOrigins(value) {
|
|
488
|
+
const entries = (typeof value === "string" ? value.split(",") : [...value]).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
489
|
+
if (entries.length === 0) {
|
|
490
|
+
throw new Error("CORS origin list is empty: pass `*` or one or more origins, e.g. https://example.com");
|
|
491
|
+
}
|
|
492
|
+
if (entries.includes("*")) {
|
|
493
|
+
if (entries.length === 1) return { kind: "any" };
|
|
494
|
+
throw new Error(
|
|
495
|
+
"CORS `*` cannot be combined with named origins \u2014 `*` already allows every origin, so the list would mean nothing. Pass either `*` or the origins."
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
const origins = [];
|
|
499
|
+
for (const entry of entries) {
|
|
500
|
+
const origin = normalizeOrigin(entry);
|
|
501
|
+
if (!origins.includes(origin)) origins.push(origin);
|
|
502
|
+
}
|
|
503
|
+
return { kind: "list", origins };
|
|
504
|
+
}
|
|
505
|
+
function normalizeOrigin(raw) {
|
|
506
|
+
if (raw.toLowerCase() === "null") {
|
|
507
|
+
throw new Error("CORS origin `null` is refused: it is what sandboxed iframes and file:// pages send.");
|
|
508
|
+
}
|
|
509
|
+
let url;
|
|
510
|
+
try {
|
|
511
|
+
url = new URL(raw);
|
|
512
|
+
} catch {
|
|
513
|
+
throw new Error(`CORS origin "${raw}" is not an origin \u2014 expected scheme://host[:port], e.g. https://example.com`);
|
|
514
|
+
}
|
|
515
|
+
if (url.pathname !== "/" && url.pathname !== "" || url.search || url.hash || url.username || url.password) {
|
|
516
|
+
throw new Error(`CORS origin "${raw}" has a path, query or credentials \u2014 an origin is only scheme://host[:port]`);
|
|
517
|
+
}
|
|
518
|
+
if (url.origin !== "null") return url.origin;
|
|
519
|
+
if (url.host) return `${url.protocol}//${url.host}`;
|
|
520
|
+
throw new Error(`CORS origin "${raw}" is not an origin \u2014 expected scheme://host[:port], e.g. https://example.com`);
|
|
521
|
+
}
|
|
522
|
+
function allowOriginFor(policy, requestOrigin) {
|
|
523
|
+
if (policy.kind === "any") return "*";
|
|
524
|
+
return requestOrigin !== void 0 && policy.origins.includes(requestOrigin) ? requestOrigin : null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/relay-server/node.ts
|
|
528
|
+
var POLL_MAX_WAIT_MS = 25e3;
|
|
529
|
+
var POLL_DEFAULT_WAIT_MS = 2e4;
|
|
530
|
+
function pollWaitMs(raw) {
|
|
531
|
+
if (raw === null || raw === void 0) return POLL_DEFAULT_WAIT_MS;
|
|
532
|
+
const n = Number.parseInt(raw, 10);
|
|
533
|
+
if (!Number.isFinite(n)) return 0;
|
|
534
|
+
return Math.min(Math.max(n, 0), POLL_MAX_WAIT_MS);
|
|
535
|
+
}
|
|
363
536
|
function createNodeRelay(opts = {}) {
|
|
364
537
|
const broker = new RelayBroker(opts);
|
|
365
538
|
const prefix = (opts.pathPrefix ?? "").replace(/\/$/, "");
|
|
366
|
-
const cors = opts.corsAllowOrigin ?? "*";
|
|
367
|
-
const allowedOrigins = opts.allowedOrigins;
|
|
539
|
+
const cors = opts.allowedOrigins && opts.allowedOrigins.length > 0 ? parseCorsOrigins(opts.allowedOrigins) : parseCorsOrigins(opts.corsAllowOrigin ?? "*");
|
|
368
540
|
function setCorsHeaders(res, req) {
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
res.setHeader("access-control-allow-origin", origin && allowedOrigins.includes(origin) ? origin : "null");
|
|
372
|
-
res.setHeader("vary", "origin");
|
|
373
|
-
} else {
|
|
374
|
-
res.setHeader("access-control-allow-origin", cors);
|
|
541
|
+
if (cors.kind === "list") {
|
|
542
|
+
res.setHeader("vary", "Origin");
|
|
375
543
|
}
|
|
544
|
+
const origin = req?.headers.origin;
|
|
545
|
+
const allow = allowOriginFor(cors, typeof origin === "string" ? origin : void 0);
|
|
546
|
+
if (allow !== null) res.setHeader("access-control-allow-origin", allow);
|
|
376
547
|
res.setHeader("access-control-allow-methods", "GET, POST, OPTIONS");
|
|
377
548
|
res.setHeader("access-control-allow-headers", "content-type, x-csrf-token, accept, authorization");
|
|
378
549
|
res.setHeader("access-control-max-age", "86400");
|
|
@@ -489,7 +660,7 @@ function createNodeRelay(opts = {}) {
|
|
|
489
660
|
client: getQuery(req).get("client") ?? void 0
|
|
490
661
|
});
|
|
491
662
|
if (!sub.ok) {
|
|
492
|
-
res.statusCode = 401;
|
|
663
|
+
res.statusCode = sub.reason === "session_gone" ? 410 : 401;
|
|
493
664
|
res.setHeader("content-type", "text/event-stream");
|
|
494
665
|
res.write(`event: error
|
|
495
666
|
data: ${sub.reason}
|
|
@@ -534,6 +705,38 @@ data: ${frame}
|
|
|
534
705
|
res.end();
|
|
535
706
|
}
|
|
536
707
|
};
|
|
708
|
+
const poll = async (req, res) => {
|
|
709
|
+
setCorsHeaders(res, req);
|
|
710
|
+
if (req.method === "OPTIONS") {
|
|
711
|
+
res.statusCode = 204;
|
|
712
|
+
return res.end();
|
|
713
|
+
}
|
|
714
|
+
if (req.method !== "GET") return json(res, 405, { error: "method_not_allowed" });
|
|
715
|
+
const session = extractSession(req, prefix);
|
|
716
|
+
if (!session) return json(res, 400, { error: "missing_session" });
|
|
717
|
+
const q = getQuery(req);
|
|
718
|
+
const direction = q.get("direction") === "outbound" ? "outbound" : "inbound";
|
|
719
|
+
const attached = broker.poll(session, q.get("token") ?? "", direction, {
|
|
720
|
+
subscriber: q.get("subscriber") ?? void 0,
|
|
721
|
+
client: q.get("client") ?? void 0
|
|
722
|
+
});
|
|
723
|
+
if (!attached.ok) {
|
|
724
|
+
return json(res, attached.reason === "session_gone" ? 410 : 401, { error: attached.reason });
|
|
725
|
+
}
|
|
726
|
+
const gone = new AbortController();
|
|
727
|
+
const onClose = () => {
|
|
728
|
+
if (!res.writableEnded) gone.abort();
|
|
729
|
+
};
|
|
730
|
+
res.on("close", onClose);
|
|
731
|
+
const frames = await attached.wait(pollWaitMs(q.get("wait")), gone.signal);
|
|
732
|
+
res.off("close", onClose);
|
|
733
|
+
if (gone.signal.aborted || res.destroyed) {
|
|
734
|
+
attached.requeue(frames);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
res.setHeader("cache-control", "no-store");
|
|
738
|
+
return json(res, 200, { subscriber: attached.subscriberId, frames });
|
|
739
|
+
};
|
|
537
740
|
const handler = async (req, res) => {
|
|
538
741
|
const pathname = getPathname(req);
|
|
539
742
|
if (!pathname.startsWith(prefix + "/")) {
|
|
@@ -541,16 +744,17 @@ data: ${frame}
|
|
|
541
744
|
}
|
|
542
745
|
const rest = pathname.slice(prefix.length);
|
|
543
746
|
if (rest === "/register") return register(req, res);
|
|
544
|
-
const m = /^\/([A-Za-z0-9_-]{4,64})\/(inbox|outbox|events|unregister)$/.exec(rest);
|
|
747
|
+
const m = /^\/([A-Za-z0-9_-]{4,64})\/(inbox|outbox|events|poll|unregister)$/.exec(rest);
|
|
545
748
|
if (!m) return json(res, 404, { error: "not_found" });
|
|
546
749
|
const route = m[2];
|
|
547
750
|
if (route === "inbox") return inbox(req, res);
|
|
548
751
|
if (route === "outbox") return outbox(req, res);
|
|
549
752
|
if (route === "events") return events(req, res);
|
|
753
|
+
if (route === "poll") return poll(req, res);
|
|
550
754
|
if (route === "unregister") return unregister(req, res);
|
|
551
755
|
return json(res, 404, { error: "not_found" });
|
|
552
756
|
};
|
|
553
|
-
return { broker, handler, register, inbox, outbox, events, unregister };
|
|
757
|
+
return { broker, handler, register, inbox, outbox, events, poll, unregister };
|
|
554
758
|
}
|
|
555
759
|
function extractSession(req, prefix) {
|
|
556
760
|
const host = req.headers.host || "x";
|