@agentproto/relay 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/daemon-client.ts","../src/rate-limiter.ts","../src/timing-safe.ts","../src/server.ts","../src/cli.ts"],"names":[],"mappings":"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","file":"cli.mjs","sourcesContent":["/**\n * Startup config, read once from env at process boot. Deliberately\n * fails loud (throws) on anything that would otherwise leave the relay\n * running in an unsafe or ambiguous state — this service is designed to\n * sit on the public internet behind a tunnel, so there is no such thing\n * as a \"harmless\" missing setting here.\n */\n\nexport type TargetVia = \"agent\" | \"terminal\"\n\nexport interface RateLimitConfig {\n /** Max requests allowed per window. */\n max: number\n /** Window size in milliseconds. */\n windowMs: number\n}\n\nexport interface RelayConfig {\n /** The ONE session id/name this relay instance may wake up. Never\n * accepted as a request parameter — baked in at startup only. */\n targetSession: string\n /** How `text` gets delivered to the target session. */\n targetVia: TargetVia\n /** Shared-secret bearer token gating POST /relay/inbound. */\n token: string\n /** Base URL of the agentproto daemon this relay talks to. */\n daemonUrl: string\n rateLimit: RateLimitConfig\n}\n\nconst DEFAULT_DAEMON_URL = \"http://127.0.0.1:18790\"\nconst DEFAULT_TARGET_VIA: TargetVia = \"agent\"\nconst DEFAULT_RATE_LIMIT_MAX = 20\nconst DEFAULT_RATE_LIMIT_WINDOW_MS = 60_000\n\nexport function loadConfigFromEnv(env: NodeJS.ProcessEnv = process.env): RelayConfig {\n const targetSession = (env.AGENTPROTO_RELAY_TARGET_SESSION ?? \"\").trim()\n if (!targetSession) {\n throw new Error(\n \"AGENTPROTO_RELAY_TARGET_SESSION is required — set it to the id or name of \" +\n \"the ONE session this relay is allowed to wake up. It is never accepted as \" +\n \"a request parameter, by design.\",\n )\n }\n\n const rawVia = (env.AGENTPROTO_RELAY_TARGET_VIA ?? DEFAULT_TARGET_VIA).trim()\n if (rawVia !== \"agent\" && rawVia !== \"terminal\") {\n throw new Error(\n `AGENTPROTO_RELAY_TARGET_VIA must be \"agent\" or \"terminal\" (got \"${rawVia}\").`,\n )\n }\n\n const token = env.AGENTPROTO_RELAY_TOKEN ?? \"\"\n if (!token) {\n throw new Error(\n \"AGENTPROTO_RELAY_TOKEN is required — refusing to start without a shared \" +\n \"secret. This service is designed to be exposed publicly; there is no \" +\n \"no-auth fallback.\",\n )\n }\n\n const daemonUrl = (env.AGENTPROTO_DAEMON_URL || DEFAULT_DAEMON_URL).replace(/\\/+$/, \"\")\n\n return {\n targetSession,\n targetVia: rawVia,\n token,\n daemonUrl,\n rateLimit: {\n max: parsePositiveInt(env.AGENTPROTO_RELAY_RATE_LIMIT, DEFAULT_RATE_LIMIT_MAX),\n windowMs: parsePositiveInt(\n env.AGENTPROTO_RELAY_RATE_WINDOW_MS,\n DEFAULT_RATE_LIMIT_WINDOW_MS,\n ),\n },\n }\n}\n\nfunction parsePositiveInt(raw: string | undefined, fallback: number): number {\n if (!raw) return fallback\n const n = Number(raw)\n return Number.isInteger(n) && n > 0 ? n : fallback\n}\n","/**\n * Thin HTTP(S)/WS client for the ONE thing this relay needs from an\n * agentproto daemon: \"is my configured target session alive, and can I\n * push text into it.\" Talks to the daemon purely over its public\n * REST/WS surface — this package is not an MCP client and does not\n * link against @agentproto/runtime.\n */\n\nimport { readFile } from \"node:fs/promises\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\nimport WebSocket from \"ws\"\n\nexport interface SessionAliveResult {\n ok: boolean\n /** Canonical session id, resolved from id-or-name by the daemon. */\n id?: string\n status?: string\n /** Present when ok is false — human-readable cause. */\n reason?: string\n}\n\nexport interface DeliveryResult {\n ok: boolean\n status?: number\n message?: string\n}\n\nconst DEAD_STATUSES = new Set([\"exited\", \"killed\", \"error\"])\n\n/**\n * GET /sessions/:id — resolves id-or-name to a canonical session, and\n * confirms it's actually alive (not exited/killed/errored, process\n * still running). Read-only route, no daemon auth token needed.\n */\nexport async function checkSessionAlive(\n daemonUrl: string,\n target: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<SessionAliveResult> {\n let res: Response\n try {\n res = await fetchImpl(`${daemonUrl}/sessions/${encodeURIComponent(target)}`, {\n signal: AbortSignal.timeout(10_000),\n })\n } catch (err) {\n return {\n ok: false,\n reason: `could not reach daemon at ${daemonUrl}: ${err instanceof Error ? err.message : String(err)}`,\n }\n }\n if (res.status === 404) {\n return { ok: false, reason: `no session \"${target}\" on the daemon` }\n }\n if (!res.ok) {\n return { ok: false, reason: `daemon returned HTTP ${res.status} for GET /sessions/${target}` }\n }\n let desc: { id?: unknown; status?: unknown; processAlive?: unknown }\n try {\n desc = (await res.json()) as { id?: unknown; status?: unknown; processAlive?: unknown }\n } catch {\n return { ok: false, reason: \"daemon returned a non-JSON session descriptor\" }\n }\n if (typeof desc.id !== \"string\") {\n return { ok: false, reason: \"daemon's session descriptor is missing an id\" }\n }\n const status = typeof desc.status === \"string\" ? desc.status : undefined\n if (status && DEAD_STATUSES.has(status)) {\n return { ok: false, id: desc.id, status, reason: `session \"${target}\" has status \"${status}\"` }\n }\n if (desc.processAlive === false) {\n return { ok: false, id: desc.id, status, reason: `session \"${target}\" process is not alive` }\n }\n return { ok: true, id: desc.id, status }\n}\n\ninterface RegistryMetaLike {\n pid?: unknown\n token?: unknown\n}\n\nfunction isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0)\n return true\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === \"EPERM\"\n }\n}\n\n/**\n * A daemon publishes its per-boot bearer token in two places (see\n * @agentproto/runtime's agentproto-dir.ts): a workspace-scoped\n * `<workspace>/.agentproto/runtime.json`, and a workspace-independent\n * `~/.agentproto/daemons/<port>.json`. The central one is keyed purely\n * by port, so we check it first; the workspace one requires an extra\n * round trip to /health to learn the workspace path, and is a\n * best-effort write on the daemon's side (it can be absent even for a\n * live, otherwise-healthy daemon), so it's the fallback.\n */\nexport async function resolveDaemonToken(\n daemonUrl: string,\n opts: { fetchImpl?: typeof fetch; homeDir?: string } = {},\n): Promise<string | undefined> {\n const fetchImpl = opts.fetchImpl ?? fetch\n const homeDir = opts.homeDir ?? homedir()\n\n const fromRegistry = await readRegistryToken(daemonUrl, homeDir)\n if (fromRegistry) return fromRegistry\n return readWorkspaceToken(daemonUrl, fetchImpl)\n}\n\nasync function readRegistryToken(daemonUrl: string, homeDir: string): Promise<string | undefined> {\n let port: string\n try {\n port = new URL(daemonUrl).port\n } catch {\n return undefined\n }\n if (!port) return undefined\n try {\n const raw = await readFile(join(homeDir, \".agentproto\", \"daemons\", `${port}.json`), \"utf8\")\n const meta = JSON.parse(raw) as RegistryMetaLike\n if (typeof meta.pid === \"number\" && !isPidAlive(meta.pid)) return undefined\n return typeof meta.token === \"string\" && meta.token ? meta.token : undefined\n } catch {\n return undefined\n }\n}\n\nasync function readWorkspaceToken(\n daemonUrl: string,\n fetchImpl: typeof fetch,\n): Promise<string | undefined> {\n try {\n const res = await fetchImpl(`${daemonUrl}/health`, { signal: AbortSignal.timeout(5_000) })\n if (!res.ok) return undefined\n const health = (await res.json()) as { workspace?: unknown }\n if (typeof health.workspace !== \"string\" || !health.workspace) return undefined\n const raw = await readFile(join(health.workspace, \".agentproto\", \"runtime.json\"), \"utf8\")\n const meta = JSON.parse(raw) as RegistryMetaLike\n return typeof meta.token === \"string\" && meta.token ? meta.token : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * POST /sessions/:id/prompt?wait=false — the REST equivalent of the\n * `agent_prompt` MCP tool. Fire-and-forget on purpose: an external\n * webhook sender may have a short timeout, and there's no reason to\n * hold its connection open for an entire LLM turn just to relay one\n * message in.\n */\nexport async function sendAgentPrompt(\n daemonUrl: string,\n sessionId: string,\n text: string,\n token: string | undefined,\n fetchImpl: typeof fetch = fetch,\n): Promise<DeliveryResult> {\n try {\n const res = await fetchImpl(\n `${daemonUrl}/sessions/${encodeURIComponent(sessionId)}/prompt?wait=false`,\n {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...(token ? { authorization: `Bearer ${token}` } : {}),\n },\n body: JSON.stringify({ prompt: text }),\n signal: AbortSignal.timeout(15_000),\n },\n )\n if (res.ok) return { ok: true, status: res.status }\n return { ok: false, status: res.status, message: await describeErrorResponse(res) }\n } catch (err) {\n return { ok: false, message: err instanceof Error ? err.message : String(err) }\n }\n}\n\nasync function describeErrorResponse(res: Response): Promise<string> {\n try {\n const body = (await res.json()) as { message?: unknown; error?: unknown }\n if (typeof body.message === \"string\") return body.message\n if (typeof body.error === \"string\") return body.error\n return JSON.stringify(body)\n } catch {\n return `HTTP ${res.status}`\n }\n}\n\n/**\n * Writes text to a live PTY session's stdin. Unlike `agent_prompt`,\n * there is no plain-HTTP REST route for `terminal_input` on the\n * daemon — the only transport is the WebSocket upgrade at\n * /sessions/:id/pty (JSON frames), so this is the one delivery path\n * that isn't a simple fetch() call.\n */\nexport async function sendTerminalInput(\n daemonUrl: string,\n sessionId: string,\n text: string,\n token: string | undefined,\n timeoutMs = 10_000,\n): Promise<DeliveryResult> {\n const wsUrl = `${daemonUrl.replace(/^http/, \"ws\")}/sessions/${encodeURIComponent(sessionId)}/pty`\n return new Promise<DeliveryResult>(resolvePromise => {\n let settled = false\n const settle = (result: DeliveryResult): void => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n resolvePromise(result)\n }\n\n const ws = new WebSocket(wsUrl, {\n headers: token ? { authorization: `Bearer ${token}` } : {},\n })\n const timer = setTimeout(() => {\n ws.terminate()\n settle({ ok: false, message: \"terminal_input: timed out connecting to the daemon\" })\n }, timeoutMs)\n\n ws.once(\"open\", () => {\n ws.send(JSON.stringify({ kind: \"input\", text }))\n ws.close(1000)\n settle({ ok: true })\n })\n ws.once(\"unexpected-response\", (_req, res) => {\n ws.terminate()\n settle({\n ok: false,\n status: res.statusCode,\n message: `terminal_input: daemon rejected the WebSocket upgrade with HTTP ${res.statusCode}`,\n })\n })\n ws.once(\"error\", err => {\n settle({ ok: false, message: err instanceof Error ? err.message : String(err) })\n })\n })\n}\n","/**\n * Global (not per-caller) in-memory sliding-window rate limiter for\n * POST /relay/inbound. Global rather than per-IP on purpose: this relay\n * has exactly one legitimate caller by design (whoever holds the\n * bearer token), and keying by a client-supplied/proxy-supplied IP\n * (e.g. X-Forwarded-For behind a tunnel) would let a caller grow an\n * unbounded map of fake keys — a memory-exhaustion vector worse than\n * the abuse it would prevent.\n */\n\nexport interface RateLimiter {\n /** Returns true if the call is allowed under the current window. */\n allow(): boolean\n}\n\nexport interface RateLimiterOptions {\n /** Max calls allowed per window. */\n max: number\n /** Window size in milliseconds. */\n windowMs: number\n /** Injectable clock — tests pass a fake one. Defaults to Date.now. */\n now?: () => number\n}\n\nexport function createRateLimiter(opts: RateLimiterOptions): RateLimiter {\n const { max, windowMs } = opts\n const now = opts.now ?? Date.now\n const timestamps: number[] = []\n\n return {\n allow(): boolean {\n const t = now()\n const cutoff = t - windowMs\n while (timestamps.length > 0 && (timestamps[0] as number) <= cutoff) {\n timestamps.shift()\n }\n if (timestamps.length >= max) return false\n timestamps.push(t)\n return true\n },\n }\n}\n","/**\n * Constant-time string equality for comparing a caller-supplied bearer\n * token against the configured secret. `crypto.timingSafeEqual` alone\n * isn't enough here — it throws on mismatched buffer lengths, and a\n * caller can measure that throw (vs. a full compare) to learn the\n * secret's length one guess at a time. HMAC-blinding both inputs to a\n * fixed-size digest first removes the length signal entirely: any two\n * strings, of any length, produce same-length digests to compare.\n */\n\nimport { createHmac, randomBytes, timingSafeEqual } from \"node:crypto\"\n\nexport function timingSafeEqualStrings(a: string, b: string): boolean {\n const key = randomBytes(32)\n const digestA = createHmac(\"sha256\", key).update(a, \"utf8\").digest()\n const digestB = createHmac(\"sha256\", key).update(b, \"utf8\").digest()\n return timingSafeEqual(digestA, digestB)\n}\n","/**\n * The relay's own HTTP surface:\n *\n * POST /relay/inbound auth + rate-limited; wakes the ONE configured\n * target session with the request's `text`.\n * GET /relay/health trivial healthcheck, no auth.\n *\n * The target session is never a request parameter — it's baked into\n * `config.targetSession` at process startup (see config.ts) and that's\n * the only session this server will ever touch.\n */\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\"\nimport * as defaultDaemonClient from \"./daemon-client.js\"\nimport type { DeliveryResult, SessionAliveResult } from \"./daemon-client.js\"\nimport { createRateLimiter, type RateLimiter } from \"./rate-limiter.js\"\nimport { timingSafeEqualStrings } from \"./timing-safe.js\"\nimport type { RelayConfig } from \"./config.js\"\n\nexport interface DaemonClient {\n checkSessionAlive(daemonUrl: string, target: string): Promise<SessionAliveResult>\n resolveDaemonToken(daemonUrl: string): Promise<string | undefined>\n sendAgentPrompt(\n daemonUrl: string,\n sessionId: string,\n text: string,\n token: string | undefined,\n ): Promise<DeliveryResult>\n sendTerminalInput(\n daemonUrl: string,\n sessionId: string,\n text: string,\n token: string | undefined,\n ): Promise<DeliveryResult>\n}\n\nexport interface CreateRelayServerOptions {\n config: RelayConfig\n daemonClient?: DaemonClient\n rateLimiter?: RateLimiter\n}\n\nconst MAX_BODY_BYTES = 1_000_000\n\nexport function createRelayServer(opts: CreateRelayServerOptions): Server {\n const daemonClient = opts.daemonClient ?? defaultDaemonClient\n const rateLimiter = opts.rateLimiter ?? createRateLimiter(opts.config.rateLimit)\n\n return createServer((req, res) => {\n handleRequest(req, res, opts.config, daemonClient, rateLimiter).catch(err => {\n if (!res.headersSent) {\n writeJson(res, 500, {\n error: \"internal_error\",\n message: err instanceof Error ? err.message : String(err),\n })\n }\n })\n })\n}\n\nasync function handleRequest(\n req: IncomingMessage,\n res: ServerResponse,\n config: RelayConfig,\n daemonClient: DaemonClient,\n rateLimiter: RateLimiter,\n): Promise<void> {\n const url = new URL(req.url ?? \"/\", \"http://relay.local\")\n\n if (url.pathname === \"/relay/health\" && req.method === \"GET\") {\n writeJson(res, 200, { ok: true })\n return\n }\n\n if (url.pathname === \"/relay/inbound\" && req.method === \"POST\") {\n await handleInbound(req, res, config, daemonClient, rateLimiter)\n return\n }\n\n writeJson(res, 404, { error: \"not_found\" })\n}\n\nasync function handleInbound(\n req: IncomingMessage,\n res: ServerResponse,\n config: RelayConfig,\n daemonClient: DaemonClient,\n rateLimiter: RateLimiter,\n): Promise<void> {\n // Rate limit before anything else — including before auth — so a\n // flood of requests (valid token or not) can't cost more than the\n // configured budget of daemon calls.\n if (!rateLimiter.allow()) {\n writeJson(res, 429, {\n error: \"rate_limited\",\n message: `Too many requests — limit is ${config.rateLimit.max} per ${config.rateLimit.windowMs}ms.`,\n })\n return\n }\n\n const authHeader = req.headers.authorization ?? \"\"\n const expected = `Bearer ${config.token}`\n if (!timingSafeEqualStrings(authHeader, expected)) {\n writeJson(res, 401, { error: \"unauthorized\" })\n return\n }\n\n let body: unknown\n try {\n body = await readJsonBody(req, MAX_BODY_BYTES)\n } catch (err) {\n writeJson(res, 400, {\n error: \"invalid_body\",\n message: err instanceof Error ? err.message : String(err),\n })\n return\n }\n if (body === INVALID_JSON) {\n writeJson(res, 400, { error: \"invalid_body\", message: \"Body must be valid JSON.\" })\n return\n }\n const text = (body as { text?: unknown } | null)?.text\n if (typeof text !== \"string\" || text.length === 0) {\n writeJson(res, 400, {\n error: \"missing_text\",\n message: \"Body must include a non-empty top-level `text` string. All other fields are ignored.\",\n })\n return\n }\n\n const alive = await daemonClient.checkSessionAlive(config.daemonUrl, config.targetSession)\n if (!alive.ok || !alive.id) {\n writeJson(res, 502, {\n error: \"target_session_unavailable\",\n message: alive.reason ?? \"target session is not available\",\n })\n return\n }\n\n const token = await daemonClient.resolveDaemonToken(config.daemonUrl)\n\n const result =\n config.targetVia === \"terminal\"\n ? await daemonClient.sendTerminalInput(config.daemonUrl, alive.id, text, token)\n : await daemonClient.sendAgentPrompt(config.daemonUrl, alive.id, text, token)\n\n if (!result.ok) {\n const status = result.status && result.status >= 400 && result.status < 500 ? result.status : 502\n writeJson(res, status, {\n error: \"relay_delivery_failed\",\n message: result.message ?? \"delivery to the target session failed\",\n })\n return\n }\n\n writeJson(res, 202, { ok: true, sessionId: alive.id, via: config.targetVia })\n}\n\nfunction writeJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { \"content-type\": \"application/json\" })\n res.end(JSON.stringify(body))\n}\n\nconst INVALID_JSON = Symbol(\"invalid_json\")\n\nfunction readJsonBody(req: IncomingMessage, maxBytes: number): Promise<unknown> {\n return new Promise((resolve, reject) => {\n let size = 0\n const chunks: Buffer[] = []\n req.on(\"data\", (chunk: Buffer) => {\n size += chunk.length\n if (size > maxBytes) {\n reject(new Error(`request body exceeds ${maxBytes} bytes`))\n req.destroy()\n return\n }\n chunks.push(chunk)\n })\n req.on(\"end\", () => {\n if (chunks.length === 0) {\n resolve(null)\n return\n }\n try {\n resolve(JSON.parse(Buffer.concat(chunks).toString(\"utf8\")))\n } catch {\n resolve(INVALID_JSON)\n }\n })\n req.on(\"error\", reject)\n })\n}\n","#!/usr/bin/env node\n/**\n * `agentproto-relay` — the standalone binary.\n *\n * agentproto-relay --port 8790 [--tunnel]\n *\n * Reads its target session, delivery mode, bearer token, and daemon\n * URL from env (see config.ts / the package README). `--tunnel` asks\n * the daemon to spawn a public cloudflared quick tunnel for `--port`\n * and prints the URL to paste into whatever external system delivers\n * the webhook.\n */\n\nimport { loadConfigFromEnv } from \"./config.js\"\nimport { createRelayServer } from \"./server.js\"\n\ninterface CliArgs {\n port: number\n tunnel: boolean\n}\n\nfunction parseArgs(argv: string[]): CliArgs {\n let port = 8790\n let tunnel = false\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]\n if (arg === \"--port\") {\n const value = argv[++i]\n port = Number(value)\n } else if (arg?.startsWith(\"--port=\")) {\n port = Number(arg.slice(\"--port=\".length))\n } else if (arg === \"--tunnel\") {\n tunnel = true\n } else if (arg === \"--help\" || arg === \"-h\") {\n printHelp()\n process.exit(0)\n }\n }\n if (!Number.isInteger(port) || port <= 0 || port > 65535) {\n throw new Error(`invalid --port value — expected an integer 1-65535`)\n }\n return { port, tunnel }\n}\n\nfunction printHelp(): void {\n console.log(\n [\n \"agentproto-relay — wake one pre-configured agentproto session from an external webhook.\",\n \"\",\n \"Usage:\",\n \" agentproto-relay --port <port> [--tunnel]\",\n \"\",\n \"Options:\",\n \" --port <n> Local port to bind (default 8790).\",\n \" --tunnel Also spawn a public cloudflared quick tunnel for --port via the\",\n \" daemon's tunnel_create, and print the public /relay/inbound URL.\",\n \" --help Show this message.\",\n \"\",\n \"Required env vars: AGENTPROTO_RELAY_TARGET_SESSION, AGENTPROTO_RELAY_TOKEN.\",\n \"See the package README for the full list.\",\n ].join(\"\\n\"),\n )\n}\n\ninterface TunnelDescriptorLike {\n publicUrl?: unknown\n status?: unknown\n}\n\nasync function createPublicTunnel(daemonUrl: string, port: number): Promise<string> {\n const res = await fetch(`${daemonUrl}/tunnels`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ targetPort: port, label: \"agentproto-relay\" }),\n })\n if (!res.ok) {\n const text = await res.text().catch(() => \"\")\n throw new Error(`failed to create tunnel: HTTP ${res.status} ${text}`)\n }\n const desc = (await res.json()) as TunnelDescriptorLike\n if (typeof desc.publicUrl !== \"string\" || !desc.publicUrl) {\n throw new Error(`daemon did not return a publicUrl: ${JSON.stringify(desc)}`)\n }\n return desc.publicUrl\n}\n\nasync function main(): Promise<void> {\n const { port, tunnel } = parseArgs(process.argv.slice(2))\n const config = loadConfigFromEnv(process.env)\n\n const server = createRelayServer({ config })\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject)\n server.listen(port, \"127.0.0.1\", () => resolve())\n })\n\n console.log(`agentproto-relay listening on http://127.0.0.1:${port}`)\n console.log(` target session : ${config.targetSession} (via ${config.targetVia})`)\n console.log(` daemon : ${config.daemonUrl}`)\n console.log(\n ` rate limit : ${config.rateLimit.max} requests / ${config.rateLimit.windowMs}ms`,\n )\n\n if (tunnel) {\n console.log(\"\")\n console.log(`creating public tunnel for port ${port}...`)\n const publicUrl = await createPublicTunnel(config.daemonUrl, port)\n console.log(\"\")\n console.log(\"Public relay URL — paste this into whatever external system sends the webhook:\")\n console.log(` ${publicUrl}/relay/inbound`)\n console.log(\"\")\n }\n\n const shutdown = (signal: string): void => {\n console.log(`\\nreceived ${signal}, shutting down...`)\n server.close(() => process.exit(0))\n }\n process.once(\"SIGINT\", () => shutdown(\"SIGINT\"))\n process.once(\"SIGTERM\", () => shutdown(\"SIGTERM\"))\n}\n\nmain().catch(err => {\n console.error(err instanceof Error ? err.message : String(err))\n process.exit(1)\n})\n"]}
@@ -0,0 +1,147 @@
1
+ import { Server } from 'node:http';
2
+
3
+ /**
4
+ * Startup config, read once from env at process boot. Deliberately
5
+ * fails loud (throws) on anything that would otherwise leave the relay
6
+ * running in an unsafe or ambiguous state — this service is designed to
7
+ * sit on the public internet behind a tunnel, so there is no such thing
8
+ * as a "harmless" missing setting here.
9
+ */
10
+ type TargetVia = "agent" | "terminal";
11
+ interface RateLimitConfig {
12
+ /** Max requests allowed per window. */
13
+ max: number;
14
+ /** Window size in milliseconds. */
15
+ windowMs: number;
16
+ }
17
+ interface RelayConfig {
18
+ /** The ONE session id/name this relay instance may wake up. Never
19
+ * accepted as a request parameter — baked in at startup only. */
20
+ targetSession: string;
21
+ /** How `text` gets delivered to the target session. */
22
+ targetVia: TargetVia;
23
+ /** Shared-secret bearer token gating POST /relay/inbound. */
24
+ token: string;
25
+ /** Base URL of the agentproto daemon this relay talks to. */
26
+ daemonUrl: string;
27
+ rateLimit: RateLimitConfig;
28
+ }
29
+ declare function loadConfigFromEnv(env?: NodeJS.ProcessEnv): RelayConfig;
30
+
31
+ /**
32
+ * Thin HTTP(S)/WS client for the ONE thing this relay needs from an
33
+ * agentproto daemon: "is my configured target session alive, and can I
34
+ * push text into it." Talks to the daemon purely over its public
35
+ * REST/WS surface — this package is not an MCP client and does not
36
+ * link against @agentproto/runtime.
37
+ */
38
+ interface SessionAliveResult {
39
+ ok: boolean;
40
+ /** Canonical session id, resolved from id-or-name by the daemon. */
41
+ id?: string;
42
+ status?: string;
43
+ /** Present when ok is false — human-readable cause. */
44
+ reason?: string;
45
+ }
46
+ interface DeliveryResult {
47
+ ok: boolean;
48
+ status?: number;
49
+ message?: string;
50
+ }
51
+ /**
52
+ * GET /sessions/:id — resolves id-or-name to a canonical session, and
53
+ * confirms it's actually alive (not exited/killed/errored, process
54
+ * still running). Read-only route, no daemon auth token needed.
55
+ */
56
+ declare function checkSessionAlive(daemonUrl: string, target: string, fetchImpl?: typeof fetch): Promise<SessionAliveResult>;
57
+ /**
58
+ * A daemon publishes its per-boot bearer token in two places (see
59
+ * @agentproto/runtime's agentproto-dir.ts): a workspace-scoped
60
+ * `<workspace>/.agentproto/runtime.json`, and a workspace-independent
61
+ * `~/.agentproto/daemons/<port>.json`. The central one is keyed purely
62
+ * by port, so we check it first; the workspace one requires an extra
63
+ * round trip to /health to learn the workspace path, and is a
64
+ * best-effort write on the daemon's side (it can be absent even for a
65
+ * live, otherwise-healthy daemon), so it's the fallback.
66
+ */
67
+ declare function resolveDaemonToken(daemonUrl: string, opts?: {
68
+ fetchImpl?: typeof fetch;
69
+ homeDir?: string;
70
+ }): Promise<string | undefined>;
71
+ /**
72
+ * POST /sessions/:id/prompt?wait=false — the REST equivalent of the
73
+ * `agent_prompt` MCP tool. Fire-and-forget on purpose: an external
74
+ * webhook sender may have a short timeout, and there's no reason to
75
+ * hold its connection open for an entire LLM turn just to relay one
76
+ * message in.
77
+ */
78
+ declare function sendAgentPrompt(daemonUrl: string, sessionId: string, text: string, token: string | undefined, fetchImpl?: typeof fetch): Promise<DeliveryResult>;
79
+ /**
80
+ * Writes text to a live PTY session's stdin. Unlike `agent_prompt`,
81
+ * there is no plain-HTTP REST route for `terminal_input` on the
82
+ * daemon — the only transport is the WebSocket upgrade at
83
+ * /sessions/:id/pty (JSON frames), so this is the one delivery path
84
+ * that isn't a simple fetch() call.
85
+ */
86
+ declare function sendTerminalInput(daemonUrl: string, sessionId: string, text: string, token: string | undefined, timeoutMs?: number): Promise<DeliveryResult>;
87
+
88
+ /**
89
+ * Global (not per-caller) in-memory sliding-window rate limiter for
90
+ * POST /relay/inbound. Global rather than per-IP on purpose: this relay
91
+ * has exactly one legitimate caller by design (whoever holds the
92
+ * bearer token), and keying by a client-supplied/proxy-supplied IP
93
+ * (e.g. X-Forwarded-For behind a tunnel) would let a caller grow an
94
+ * unbounded map of fake keys — a memory-exhaustion vector worse than
95
+ * the abuse it would prevent.
96
+ */
97
+ interface RateLimiter {
98
+ /** Returns true if the call is allowed under the current window. */
99
+ allow(): boolean;
100
+ }
101
+ interface RateLimiterOptions {
102
+ /** Max calls allowed per window. */
103
+ max: number;
104
+ /** Window size in milliseconds. */
105
+ windowMs: number;
106
+ /** Injectable clock — tests pass a fake one. Defaults to Date.now. */
107
+ now?: () => number;
108
+ }
109
+ declare function createRateLimiter(opts: RateLimiterOptions): RateLimiter;
110
+
111
+ /**
112
+ * The relay's own HTTP surface:
113
+ *
114
+ * POST /relay/inbound auth + rate-limited; wakes the ONE configured
115
+ * target session with the request's `text`.
116
+ * GET /relay/health trivial healthcheck, no auth.
117
+ *
118
+ * The target session is never a request parameter — it's baked into
119
+ * `config.targetSession` at process startup (see config.ts) and that's
120
+ * the only session this server will ever touch.
121
+ */
122
+
123
+ interface DaemonClient {
124
+ checkSessionAlive(daemonUrl: string, target: string): Promise<SessionAliveResult>;
125
+ resolveDaemonToken(daemonUrl: string): Promise<string | undefined>;
126
+ sendAgentPrompt(daemonUrl: string, sessionId: string, text: string, token: string | undefined): Promise<DeliveryResult>;
127
+ sendTerminalInput(daemonUrl: string, sessionId: string, text: string, token: string | undefined): Promise<DeliveryResult>;
128
+ }
129
+ interface CreateRelayServerOptions {
130
+ config: RelayConfig;
131
+ daemonClient?: DaemonClient;
132
+ rateLimiter?: RateLimiter;
133
+ }
134
+ declare function createRelayServer(opts: CreateRelayServerOptions): Server;
135
+
136
+ /**
137
+ * Constant-time string equality for comparing a caller-supplied bearer
138
+ * token against the configured secret. `crypto.timingSafeEqual` alone
139
+ * isn't enough here — it throws on mismatched buffer lengths, and a
140
+ * caller can measure that throw (vs. a full compare) to learn the
141
+ * secret's length one guess at a time. HMAC-blinding both inputs to a
142
+ * fixed-size digest first removes the length signal entirely: any two
143
+ * strings, of any length, produce same-length digests to compare.
144
+ */
145
+ declare function timingSafeEqualStrings(a: string, b: string): boolean;
146
+
147
+ export { type CreateRelayServerOptions, type DaemonClient, type DeliveryResult, type RateLimitConfig, type RateLimiter, type RateLimiterOptions, type RelayConfig, type SessionAliveResult, type TargetVia, checkSessionAlive, createRateLimiter, createRelayServer, loadConfigFromEnv, resolveDaemonToken, sendAgentPrompt, sendTerminalInput, timingSafeEqualStrings };
package/dist/index.mjs ADDED
@@ -0,0 +1,363 @@
1
+ import { createServer } from 'http';
2
+ import { readFile } from 'fs/promises';
3
+ import { homedir } from 'os';
4
+ import { join } from 'path';
5
+ import WebSocket from 'ws';
6
+ import { randomBytes, createHmac, timingSafeEqual } from 'crypto';
7
+
8
+ /**
9
+ * @agentproto/relay v0.1.0-alpha
10
+ * Standalone webhook-to-session relay: one fixed target session, one bearer token.
11
+ */
12
+ var __defProp = Object.defineProperty;
13
+ var __export = (target, all) => {
14
+ for (var name in all)
15
+ __defProp(target, name, { get: all[name], enumerable: true });
16
+ };
17
+
18
+ // src/config.ts
19
+ var DEFAULT_DAEMON_URL = "http://127.0.0.1:18790";
20
+ var DEFAULT_TARGET_VIA = "agent";
21
+ var DEFAULT_RATE_LIMIT_MAX = 20;
22
+ var DEFAULT_RATE_LIMIT_WINDOW_MS = 6e4;
23
+ function loadConfigFromEnv(env = process.env) {
24
+ const targetSession = (env.AGENTPROTO_RELAY_TARGET_SESSION ?? "").trim();
25
+ if (!targetSession) {
26
+ throw new Error(
27
+ "AGENTPROTO_RELAY_TARGET_SESSION is required \u2014 set it to the id or name of the ONE session this relay is allowed to wake up. It is never accepted as a request parameter, by design."
28
+ );
29
+ }
30
+ const rawVia = (env.AGENTPROTO_RELAY_TARGET_VIA ?? DEFAULT_TARGET_VIA).trim();
31
+ if (rawVia !== "agent" && rawVia !== "terminal") {
32
+ throw new Error(
33
+ `AGENTPROTO_RELAY_TARGET_VIA must be "agent" or "terminal" (got "${rawVia}").`
34
+ );
35
+ }
36
+ const token = env.AGENTPROTO_RELAY_TOKEN ?? "";
37
+ if (!token) {
38
+ throw new Error(
39
+ "AGENTPROTO_RELAY_TOKEN is required \u2014 refusing to start without a shared secret. This service is designed to be exposed publicly; there is no no-auth fallback."
40
+ );
41
+ }
42
+ const daemonUrl = (env.AGENTPROTO_DAEMON_URL || DEFAULT_DAEMON_URL).replace(/\/+$/, "");
43
+ return {
44
+ targetSession,
45
+ targetVia: rawVia,
46
+ token,
47
+ daemonUrl,
48
+ rateLimit: {
49
+ max: parsePositiveInt(env.AGENTPROTO_RELAY_RATE_LIMIT, DEFAULT_RATE_LIMIT_MAX),
50
+ windowMs: parsePositiveInt(
51
+ env.AGENTPROTO_RELAY_RATE_WINDOW_MS,
52
+ DEFAULT_RATE_LIMIT_WINDOW_MS
53
+ )
54
+ }
55
+ };
56
+ }
57
+ function parsePositiveInt(raw, fallback) {
58
+ if (!raw) return fallback;
59
+ const n = Number(raw);
60
+ return Number.isInteger(n) && n > 0 ? n : fallback;
61
+ }
62
+
63
+ // src/daemon-client.ts
64
+ var daemon_client_exports = {};
65
+ __export(daemon_client_exports, {
66
+ checkSessionAlive: () => checkSessionAlive,
67
+ resolveDaemonToken: () => resolveDaemonToken,
68
+ sendAgentPrompt: () => sendAgentPrompt,
69
+ sendTerminalInput: () => sendTerminalInput
70
+ });
71
+ var DEAD_STATUSES = /* @__PURE__ */ new Set(["exited", "killed", "error"]);
72
+ async function checkSessionAlive(daemonUrl, target, fetchImpl = fetch) {
73
+ let res;
74
+ try {
75
+ res = await fetchImpl(`${daemonUrl}/sessions/${encodeURIComponent(target)}`, {
76
+ signal: AbortSignal.timeout(1e4)
77
+ });
78
+ } catch (err) {
79
+ return {
80
+ ok: false,
81
+ reason: `could not reach daemon at ${daemonUrl}: ${err instanceof Error ? err.message : String(err)}`
82
+ };
83
+ }
84
+ if (res.status === 404) {
85
+ return { ok: false, reason: `no session "${target}" on the daemon` };
86
+ }
87
+ if (!res.ok) {
88
+ return { ok: false, reason: `daemon returned HTTP ${res.status} for GET /sessions/${target}` };
89
+ }
90
+ let desc;
91
+ try {
92
+ desc = await res.json();
93
+ } catch {
94
+ return { ok: false, reason: "daemon returned a non-JSON session descriptor" };
95
+ }
96
+ if (typeof desc.id !== "string") {
97
+ return { ok: false, reason: "daemon's session descriptor is missing an id" };
98
+ }
99
+ const status = typeof desc.status === "string" ? desc.status : void 0;
100
+ if (status && DEAD_STATUSES.has(status)) {
101
+ return { ok: false, id: desc.id, status, reason: `session "${target}" has status "${status}"` };
102
+ }
103
+ if (desc.processAlive === false) {
104
+ return { ok: false, id: desc.id, status, reason: `session "${target}" process is not alive` };
105
+ }
106
+ return { ok: true, id: desc.id, status };
107
+ }
108
+ function isPidAlive(pid) {
109
+ try {
110
+ process.kill(pid, 0);
111
+ return true;
112
+ } catch (err) {
113
+ return err.code === "EPERM";
114
+ }
115
+ }
116
+ async function resolveDaemonToken(daemonUrl, opts = {}) {
117
+ const fetchImpl = opts.fetchImpl ?? fetch;
118
+ const homeDir = opts.homeDir ?? homedir();
119
+ const fromRegistry = await readRegistryToken(daemonUrl, homeDir);
120
+ if (fromRegistry) return fromRegistry;
121
+ return readWorkspaceToken(daemonUrl, fetchImpl);
122
+ }
123
+ async function readRegistryToken(daemonUrl, homeDir) {
124
+ let port;
125
+ try {
126
+ port = new URL(daemonUrl).port;
127
+ } catch {
128
+ return void 0;
129
+ }
130
+ if (!port) return void 0;
131
+ try {
132
+ const raw = await readFile(join(homeDir, ".agentproto", "daemons", `${port}.json`), "utf8");
133
+ const meta = JSON.parse(raw);
134
+ if (typeof meta.pid === "number" && !isPidAlive(meta.pid)) return void 0;
135
+ return typeof meta.token === "string" && meta.token ? meta.token : void 0;
136
+ } catch {
137
+ return void 0;
138
+ }
139
+ }
140
+ async function readWorkspaceToken(daemonUrl, fetchImpl) {
141
+ try {
142
+ const res = await fetchImpl(`${daemonUrl}/health`, { signal: AbortSignal.timeout(5e3) });
143
+ if (!res.ok) return void 0;
144
+ const health = await res.json();
145
+ if (typeof health.workspace !== "string" || !health.workspace) return void 0;
146
+ const raw = await readFile(join(health.workspace, ".agentproto", "runtime.json"), "utf8");
147
+ const meta = JSON.parse(raw);
148
+ return typeof meta.token === "string" && meta.token ? meta.token : void 0;
149
+ } catch {
150
+ return void 0;
151
+ }
152
+ }
153
+ async function sendAgentPrompt(daemonUrl, sessionId, text, token, fetchImpl = fetch) {
154
+ try {
155
+ const res = await fetchImpl(
156
+ `${daemonUrl}/sessions/${encodeURIComponent(sessionId)}/prompt?wait=false`,
157
+ {
158
+ method: "POST",
159
+ headers: {
160
+ "content-type": "application/json",
161
+ ...token ? { authorization: `Bearer ${token}` } : {}
162
+ },
163
+ body: JSON.stringify({ prompt: text }),
164
+ signal: AbortSignal.timeout(15e3)
165
+ }
166
+ );
167
+ if (res.ok) return { ok: true, status: res.status };
168
+ return { ok: false, status: res.status, message: await describeErrorResponse(res) };
169
+ } catch (err) {
170
+ return { ok: false, message: err instanceof Error ? err.message : String(err) };
171
+ }
172
+ }
173
+ async function describeErrorResponse(res) {
174
+ try {
175
+ const body = await res.json();
176
+ if (typeof body.message === "string") return body.message;
177
+ if (typeof body.error === "string") return body.error;
178
+ return JSON.stringify(body);
179
+ } catch {
180
+ return `HTTP ${res.status}`;
181
+ }
182
+ }
183
+ async function sendTerminalInput(daemonUrl, sessionId, text, token, timeoutMs = 1e4) {
184
+ const wsUrl = `${daemonUrl.replace(/^http/, "ws")}/sessions/${encodeURIComponent(sessionId)}/pty`;
185
+ return new Promise((resolvePromise) => {
186
+ let settled = false;
187
+ const settle = (result) => {
188
+ if (settled) return;
189
+ settled = true;
190
+ clearTimeout(timer);
191
+ resolvePromise(result);
192
+ };
193
+ const ws = new WebSocket(wsUrl, {
194
+ headers: token ? { authorization: `Bearer ${token}` } : {}
195
+ });
196
+ const timer = setTimeout(() => {
197
+ ws.terminate();
198
+ settle({ ok: false, message: "terminal_input: timed out connecting to the daemon" });
199
+ }, timeoutMs);
200
+ ws.once("open", () => {
201
+ ws.send(JSON.stringify({ kind: "input", text }));
202
+ ws.close(1e3);
203
+ settle({ ok: true });
204
+ });
205
+ ws.once("unexpected-response", (_req, res) => {
206
+ ws.terminate();
207
+ settle({
208
+ ok: false,
209
+ status: res.statusCode,
210
+ message: `terminal_input: daemon rejected the WebSocket upgrade with HTTP ${res.statusCode}`
211
+ });
212
+ });
213
+ ws.once("error", (err) => {
214
+ settle({ ok: false, message: err instanceof Error ? err.message : String(err) });
215
+ });
216
+ });
217
+ }
218
+
219
+ // src/rate-limiter.ts
220
+ function createRateLimiter(opts) {
221
+ const { max, windowMs } = opts;
222
+ const now = opts.now ?? Date.now;
223
+ const timestamps = [];
224
+ return {
225
+ allow() {
226
+ const t = now();
227
+ const cutoff = t - windowMs;
228
+ while (timestamps.length > 0 && timestamps[0] <= cutoff) {
229
+ timestamps.shift();
230
+ }
231
+ if (timestamps.length >= max) return false;
232
+ timestamps.push(t);
233
+ return true;
234
+ }
235
+ };
236
+ }
237
+ function timingSafeEqualStrings(a, b) {
238
+ const key = randomBytes(32);
239
+ const digestA = createHmac("sha256", key).update(a, "utf8").digest();
240
+ const digestB = createHmac("sha256", key).update(b, "utf8").digest();
241
+ return timingSafeEqual(digestA, digestB);
242
+ }
243
+
244
+ // src/server.ts
245
+ var MAX_BODY_BYTES = 1e6;
246
+ function createRelayServer(opts) {
247
+ const daemonClient = opts.daemonClient ?? daemon_client_exports;
248
+ const rateLimiter = opts.rateLimiter ?? createRateLimiter(opts.config.rateLimit);
249
+ return createServer((req, res) => {
250
+ handleRequest(req, res, opts.config, daemonClient, rateLimiter).catch((err) => {
251
+ if (!res.headersSent) {
252
+ writeJson(res, 500, {
253
+ error: "internal_error",
254
+ message: err instanceof Error ? err.message : String(err)
255
+ });
256
+ }
257
+ });
258
+ });
259
+ }
260
+ async function handleRequest(req, res, config, daemonClient, rateLimiter) {
261
+ const url = new URL(req.url ?? "/", "http://relay.local");
262
+ if (url.pathname === "/relay/health" && req.method === "GET") {
263
+ writeJson(res, 200, { ok: true });
264
+ return;
265
+ }
266
+ if (url.pathname === "/relay/inbound" && req.method === "POST") {
267
+ await handleInbound(req, res, config, daemonClient, rateLimiter);
268
+ return;
269
+ }
270
+ writeJson(res, 404, { error: "not_found" });
271
+ }
272
+ async function handleInbound(req, res, config, daemonClient, rateLimiter) {
273
+ if (!rateLimiter.allow()) {
274
+ writeJson(res, 429, {
275
+ error: "rate_limited",
276
+ message: `Too many requests \u2014 limit is ${config.rateLimit.max} per ${config.rateLimit.windowMs}ms.`
277
+ });
278
+ return;
279
+ }
280
+ const authHeader = req.headers.authorization ?? "";
281
+ const expected = `Bearer ${config.token}`;
282
+ if (!timingSafeEqualStrings(authHeader, expected)) {
283
+ writeJson(res, 401, { error: "unauthorized" });
284
+ return;
285
+ }
286
+ let body;
287
+ try {
288
+ body = await readJsonBody(req, MAX_BODY_BYTES);
289
+ } catch (err) {
290
+ writeJson(res, 400, {
291
+ error: "invalid_body",
292
+ message: err instanceof Error ? err.message : String(err)
293
+ });
294
+ return;
295
+ }
296
+ if (body === INVALID_JSON) {
297
+ writeJson(res, 400, { error: "invalid_body", message: "Body must be valid JSON." });
298
+ return;
299
+ }
300
+ const text = body?.text;
301
+ if (typeof text !== "string" || text.length === 0) {
302
+ writeJson(res, 400, {
303
+ error: "missing_text",
304
+ message: "Body must include a non-empty top-level `text` string. All other fields are ignored."
305
+ });
306
+ return;
307
+ }
308
+ const alive = await daemonClient.checkSessionAlive(config.daemonUrl, config.targetSession);
309
+ if (!alive.ok || !alive.id) {
310
+ writeJson(res, 502, {
311
+ error: "target_session_unavailable",
312
+ message: alive.reason ?? "target session is not available"
313
+ });
314
+ return;
315
+ }
316
+ const token = await daemonClient.resolveDaemonToken(config.daemonUrl);
317
+ const result = config.targetVia === "terminal" ? await daemonClient.sendTerminalInput(config.daemonUrl, alive.id, text, token) : await daemonClient.sendAgentPrompt(config.daemonUrl, alive.id, text, token);
318
+ if (!result.ok) {
319
+ const status = result.status && result.status >= 400 && result.status < 500 ? result.status : 502;
320
+ writeJson(res, status, {
321
+ error: "relay_delivery_failed",
322
+ message: result.message ?? "delivery to the target session failed"
323
+ });
324
+ return;
325
+ }
326
+ writeJson(res, 202, { ok: true, sessionId: alive.id, via: config.targetVia });
327
+ }
328
+ function writeJson(res, status, body) {
329
+ res.writeHead(status, { "content-type": "application/json" });
330
+ res.end(JSON.stringify(body));
331
+ }
332
+ var INVALID_JSON = /* @__PURE__ */ Symbol("invalid_json");
333
+ function readJsonBody(req, maxBytes) {
334
+ return new Promise((resolve, reject) => {
335
+ let size = 0;
336
+ const chunks = [];
337
+ req.on("data", (chunk) => {
338
+ size += chunk.length;
339
+ if (size > maxBytes) {
340
+ reject(new Error(`request body exceeds ${maxBytes} bytes`));
341
+ req.destroy();
342
+ return;
343
+ }
344
+ chunks.push(chunk);
345
+ });
346
+ req.on("end", () => {
347
+ if (chunks.length === 0) {
348
+ resolve(null);
349
+ return;
350
+ }
351
+ try {
352
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
353
+ } catch {
354
+ resolve(INVALID_JSON);
355
+ }
356
+ });
357
+ req.on("error", reject);
358
+ });
359
+ }
360
+
361
+ export { checkSessionAlive, createRateLimiter, createRelayServer, loadConfigFromEnv, resolveDaemonToken, sendAgentPrompt, sendTerminalInput, timingSafeEqualStrings };
362
+ //# sourceMappingURL=index.mjs.map
363
+ //# sourceMappingURL=index.mjs.map