import { type Request } from "express"; import type { Server } from "http"; /** * v2.7 Tether Phase 5 — SSE keepalive helper. * * Phase 4a fixed the server-side cull (session reaper closing while a * GET stream was open). Post-Phase-4 smoke testing revealed a SECOND * disconnect class at ~2.5 min — daemon log showed nothing (no reap, * no close, no error), but the extension's client-side fetch reported * `SSE stream disconnected: TypeError: terminated`. Root cause: VS * Code's Electron-based fetch implementation has its own idle-stream * timeout on `response.body` reads, separate from Node's / undici's * timeouts. When no bytes flow for ~2.5 min, Electron declares the * stream dead. * * Fix: emit a periodic SSE comment frame (":keepalive\n\n") from the * daemon side. Per SSE spec, lines beginning with `:` are comments * that clients MUST ignore — so the keepalive is invisible to the * MCP message stream but visible to the fetch runtime as recent byte * activity. Default interval 20 s gives ~3-4× margin against the * typical 60-90 s intermediary idle threshold. * * Module-scope export so tests/v2-7-tether-sse-keepalive.test.ts can * exercise the timer logic with a fake response object. * * Contract: * - intervalMs <= 0 → no timer, returns a no-op cleanup. Lets * `RELAY_SSE_KEEPALIVE_MS=0` disable keepalive entirely. * - intervalMs > 0 → setInterval that writes `:keepalive\n\n` * while !res.writableEnded. Returns a cleanup function the * caller can invoke directly; ALSO wires `res.once("close", ...)` * so the timer is guaranteed to clear even if the caller forgets. * - Cleanup is idempotent — invoking twice is a no-op the second * time, so caller-explicit cleanup + the res-close handler are * safe to both fire. */ type WritableLike = { write: (chunk: string) => boolean | unknown; once: (event: "close", listener: () => void) => unknown; readonly writableEnded?: boolean; }; export declare function setupSseKeepalive(res: WritableLike, intervalMs: number): () => void; /** * v2.7 Tether Phase 4 — pure reaper predicate. Module-scope export so * tests/v2-7-tether-reaper-skip.test.ts can exercise the decision * logic without spinning a real HTTP daemon. Structural type lets the * test pass plain objects. * * Contract: * - openGetStreams > 0 → NEVER reap (active SSE subscriber). * - openGetStreams === 0 AND lastSeen < now - idleMs → REAP. * - openGetStreams === 0 AND lastSeen >= now - idleMs → NO-OP. */ export declare function shouldReapSession(session: { lastSeen: number; openGetStreams: number; }, now: number, idleMs: number): boolean; export declare function computeCsrfToken(secret: string): string; export declare function extractSourceIp(req: Request, trustedProxies: string[]): string; /** * Start an HTTP server that speaks MCP over Streamable HTTP. * Each POST to /mcp creates a new stateless transport. * Each client gets its own Server instance, all sharing the same SQLite DB. */ /** * v2.1 Phase 4n (F-3a.9): refuse to start on a non-loopback host without * RELAY_HTTP_SECRET set. Default bind is 127.0.0.1 so local-only is safe, * but RELAY_HTTP_HOST=0.0.0.0 (or Docker -p mappings) with no secret means * anyone reachable on the port can register_agent / register_webhook / etc. * * Conservative-by-default; explicit opt-in via RELAY_ALLOW_OPEN_PUBLIC=1. * Exported for testability. */ export declare const LOOPBACK_HOSTS: ReadonlySet; export declare function assertBindSafety(host: string, httpSecret: string | null): void; export declare function startHttpServer(port: number, host: string): Server; export {}; //# sourceMappingURL=http.d.ts.map