@particle-academy/agent-integrations 0.45.0 → 0.47.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.
Files changed (110) hide show
  1. package/CHANGELOG.md +835 -0
  2. package/LICENSE +21 -0
  3. package/README.md +54 -6
  4. package/dist/bridges/artboard.d.cts +3 -3
  5. package/dist/bridges/artboard.d.ts +3 -3
  6. package/dist/bridges/catalog.d.cts +3 -3
  7. package/dist/bridges/catalog.d.ts +3 -3
  8. package/dist/bridges/charts.d.cts +3 -3
  9. package/dist/bridges/charts.d.ts +3 -3
  10. package/dist/bridges/cms.d.cts +3 -3
  11. package/dist/bridges/cms.d.ts +3 -3
  12. package/dist/bridges/code.d.cts +3 -3
  13. package/dist/bridges/code.d.ts +3 -3
  14. package/dist/bridges/doc.d.cts +3 -3
  15. package/dist/bridges/doc.d.ts +3 -3
  16. package/dist/bridges/features.d.cts +3 -3
  17. package/dist/bridges/features.d.ts +3 -3
  18. package/dist/bridges/files.d.cts +3 -3
  19. package/dist/bridges/files.d.ts +3 -3
  20. package/dist/bridges/flow.d.cts +3 -3
  21. package/dist/bridges/flow.d.ts +3 -3
  22. package/dist/bridges/forms.d.cts +3 -3
  23. package/dist/bridges/forms.d.ts +3 -3
  24. package/dist/bridges/git.d.cts +3 -3
  25. package/dist/bridges/git.d.ts +3 -3
  26. package/dist/bridges/grid.d.cts +3 -3
  27. package/dist/bridges/grid.d.ts +3 -3
  28. package/dist/bridges/map.d.cts +3 -3
  29. package/dist/bridges/map.d.ts +3 -3
  30. package/dist/bridges/navigation.d.cts +127 -0
  31. package/dist/bridges/navigation.d.ts +127 -0
  32. package/dist/bridges/passkeys.d.cts +4 -4
  33. package/dist/bridges/passkeys.d.ts +4 -4
  34. package/dist/bridges/scene.d.cts +3 -3
  35. package/dist/bridges/scene.d.ts +3 -3
  36. package/dist/bridges/screens.d.cts +3 -3
  37. package/dist/bridges/screens.d.ts +3 -3
  38. package/dist/bridges/sheets.d.cts +3 -3
  39. package/dist/bridges/sheets.d.ts +3 -3
  40. package/dist/bridges/slides.d.cts +3 -3
  41. package/dist/bridges/slides.d.ts +3 -3
  42. package/dist/bridges/terminal.d.cts +3 -3
  43. package/dist/bridges/terminal.d.ts +3 -3
  44. package/dist/bridges/tui.d.cts +2 -2
  45. package/dist/bridges/tui.d.ts +2 -2
  46. package/dist/bridges/whiteboard.d.cts +3 -3
  47. package/dist/bridges/whiteboard.d.ts +3 -3
  48. package/dist/bridges-navigation.cjs +391 -0
  49. package/dist/bridges-navigation.cjs.map +1 -0
  50. package/dist/bridges-navigation.js +7 -0
  51. package/dist/bridges-navigation.js.map +1 -0
  52. package/dist/bridges-passkeys.cjs.map +1 -1
  53. package/dist/bridges-passkeys.js +1 -1
  54. package/dist/{chunk-OZBUPO4K.js → chunk-5W34X4EY.js} +263 -58
  55. package/dist/chunk-5W34X4EY.js.map +1 -0
  56. package/dist/{chunk-PWYEW6NL.js → chunk-G6CRHFWU.js} +2 -2
  57. package/dist/{chunk-PWYEW6NL.js.map → chunk-G6CRHFWU.js.map} +1 -1
  58. package/dist/chunk-JGJHPH65.js +271 -0
  59. package/dist/chunk-JGJHPH65.js.map +1 -0
  60. package/dist/dom-navigation/index.d.cts +164 -0
  61. package/dist/dom-navigation/index.d.ts +164 -0
  62. package/dist/dom-navigation/script.d.cts +3 -0
  63. package/dist/dom-navigation/script.d.ts +3 -0
  64. package/dist/dom-navigation-script.cjs +11 -0
  65. package/dist/dom-navigation-script.cjs.map +1 -0
  66. package/dist/dom-navigation-script.js +9 -0
  67. package/dist/dom-navigation-script.js.map +1 -0
  68. package/dist/dom-navigation.cjs +387 -0
  69. package/dist/dom-navigation.cjs.map +1 -0
  70. package/dist/dom-navigation.js +370 -0
  71. package/dist/dom-navigation.js.map +1 -0
  72. package/dist/index.cjs +31 -31
  73. package/dist/index.cjs.map +1 -1
  74. package/dist/index.d.cts +10 -125
  75. package/dist/index.d.ts +10 -125
  76. package/dist/index.js +12 -275
  77. package/dist/index.js.map +1 -1
  78. package/dist/mcp/index.d.cts +5 -5
  79. package/dist/mcp/index.d.ts +5 -5
  80. package/dist/mcp/stdio.d.cts +3 -3
  81. package/dist/mcp/stdio.d.ts +3 -3
  82. package/dist/presence/index.d.cts +1 -1
  83. package/dist/presence/index.d.ts +1 -1
  84. package/dist/relay-server/index.d.cts +110 -26
  85. package/dist/relay-server/index.d.ts +110 -26
  86. package/dist/relay-server-cli.cjs +374 -89
  87. package/dist/relay-server-cli.cjs.map +1 -1
  88. package/dist/relay-server-cli.js +120 -38
  89. package/dist/relay-server-cli.js.map +1 -1
  90. package/dist/relay-server.cjs +255 -51
  91. package/dist/relay-server.cjs.map +1 -1
  92. package/dist/relay-server.js +1 -1
  93. package/dist/{server-BIcA3ZtK.d.cts → server-1y0n61R5.d.cts} +2 -2
  94. package/dist/{server-DnUyeavY.d.ts → server-vq6qVl3T.d.ts} +2 -2
  95. package/dist/sharing/index.d.cts +3 -3
  96. package/dist/sharing/index.d.ts +3 -3
  97. package/dist/sheets-adapter.d.cts +3 -3
  98. package/dist/sheets-adapter.d.ts +3 -3
  99. package/dist/{tool-host-LVBgCN5m.d.cts → tool-host-D67N2a2h.d.cts} +1 -1
  100. package/dist/{tool-host-Wo0pkY83.d.ts → tool-host-LYfdPE8u.d.ts} +1 -1
  101. package/dist/{types-EcElKOlk.d.cts → types-DP5yQZQU.d.cts} +1 -1
  102. package/dist/{types-BxNRKSVd.d.ts → types-VX_9je7w.d.ts} +1 -1
  103. package/dist/{types-C2DtnHIn.d.cts → types-gzIpsUDE.d.cts} +1 -1
  104. package/dist/{types-C2DtnHIn.d.ts → types-gzIpsUDE.d.ts} +1 -1
  105. package/dist/undo/index.d.cts +2 -2
  106. package/dist/undo/index.d.ts +2 -2
  107. package/docs/relay-protocol.md +41 -5
  108. package/docs/relay-server.md +102 -15
  109. package/package.json +41 -3
  110. 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"]}
@@ -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
- if (!this.validate(id, token)) return { ok: false, reason: "invalid_token" };
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
- subscriber.resolveNext?.(null);
204
- unsubscribe();
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
- sub.queue.push(payload);
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
- sub.queue.push(payload);
306
- sub.resolveNext?.(sub.queue.shift() ?? null);
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 cutoff = Date.now() - this.ttlMs;
467
+ const now = Date.now();
468
+ const cutoff = now - this.ttlMs;
343
469
  for (const id of this.store.expiredSessionIds(cutoff)) {
344
- const dirs = this.subs.get(id);
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 (allowedOrigins && allowedOrigins.length) {
370
- const origin = req?.headers.origin;
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";