/** * Runtime-neutral core of the LLui agent server. Exports everything * that works on any runtime with `crypto.subtle` + `Request`/`Response` * + long-lived connection primitives — in practice: Node, Bun, Deno, * Deno Deploy, Cloudflare Workers + Durable Objects. * * Intentionally does NOT import the `ws` library or any `node:*` * modules. Node-specific wiring lives in `./factory.ts` * (`createLluiAgentServer`); web runtimes use `./web/` adapters on * top of this core. */ import type { TokenStore } from './token-store.js'; import type { IdentityResolver } from './identity.js'; import type { AuditSink } from './audit.js'; import type { RateLimiter } from './rate-limit.js'; import type { PairingConnection, PairingRegistry } from './ws/pairing-registry.js'; import { type ClientAddressResolver } from './client-ip.js'; /** * Options accepted by `createLluiAgentCore`. Strict subset of * `ServerOptions` — everything needed to build the router, registry, * and accept-connection primitive. The Node factory adds WebSocket * upgrade wiring on top. */ export type CoreOptions = { tokenStore?: TokenStore; identityResolver?: IdentityResolver; auditSink?: AuditSink; rateLimiter?: RateLimiter; lapBasePath?: string; /** * Allow minting tokens for unauthenticated callers (identity resolves * to `null`). SECURITY: defaults to `false` (fail closed). See * `MintDeps.allowAnonymous`. */ allowAnonymous?: boolean; /** * Number of TRUSTED reverse proxies in front of this server, for * deriving the rate-limit bucket key of a caller with no resolved * identity (`/agent/mint`, the MCP `initialize` path). * * SECURITY: defaults to `0` — no forwarding header is read, because * on a direct-to-origin deployment those are written by the caller * and one bucket per caller-chosen value is not a limit. Set it only * when proxies you control are guaranteed to be in the path AND to * APPEND to `X-Forwarded-For`; `X-Real-IP` is never read. See * `client-ip.ts`. */ trustProxy?: boolean | number; /** * Peer (socket) address of the request, which a WHATWG `Request` does * not carry. Supply it to give unidentified callers per-connection * throttle buckets without trusting any header: Node from * `socket.remoteAddress`, Cloudflare from `cf-connecting-ip`. It is * also how a deployment declares a proxy header `trustProxy` will not * trust on its own, e.g. `(req) => req.headers.get('x-real-ip')`. * Without it (and without `trustProxy`) they share one bucket. */ clientAddress?: ClientAddressResolver; /** * Sliding (inactivity) TTL in ms. When set, a token unused for longer * than this is rejected on every verify (LAP/MCP and WS upgrade) even * before its hard expiry. Undefined / `0` disables the check. */ slidingTtlMs?: number; /** * Allowed `Origin` allowlist for WebSocket upgrades (CSWSH defense). * Unset → same-origin only. Stored on the returned handle as * `allowedOrigins` for the runtime upgrade adapters to enforce. */ corsOrigins?: readonly string[]; /** * Override the default `InMemoryPairingRegistry`. Web runtimes that * need a different pairing implementation (e.g. a Cloudflare * Durable Object that persists across isolates) pass it here. */ registry?: PairingRegistry; /** * How long, in milliseconds, a token's record stays in * `pending-resume` after the WS pairing closes. During this window * the same browser can reconnect with the same bearer token and * the WS re-pairs without going through the rotate-on-resume path * (`/resume/claim`). The agent's existing token stays valid the * whole time, so brief network drops, page reloads, and quick * server restarts don't invalidate the agent's session. * * After the window, LAP calls report `X-LLui-Reconnect: expired` * and the record becomes resume-claimable (rotation required). * Set to `0` to opt out — the WS close immediately drops the * record and any reconnect must go through `/resume/claim`. * * Default: 60 seconds — long enough for laptop sleep, brief Wi-Fi * flicker, and a server restart; short enough that a deliberately- * closed tab doesn't keep the record alive forever. * * Doubles as the retention window for a closed session's registry * buffers (`describe_recent_actions` ring + buffered confirm * outcomes): they survive a drop for exactly this long, then are * swept — a memory bound, since holding them leaked one buffer per * browser-tab lifecycle (#101). * * Note what that costs: `/resume/claim` rotates the bearer but keeps * the SAME tid, and the registry is keyed by tid, so a resume later * than this window reattaches to a session whose recent-action history * has been dropped. `describe_recent_actions` starts empty there. With * `0`, it starts empty after any close at all. */ pendingResumeGraceMs?: number; }; export type AcceptResult = { ok: true; tid: string; } | { ok: false; status: number; code: 'auth-failed' | 'revoked'; }; /** * Handle returned by `createLluiAgentCore`. Purely runtime-neutral — * `router` is a Fetch-style handler, `acceptConnection` is the * primitive that runtime-specific WebSocket adapters call after * accepting a socket in their native way. */ export type AgentCoreHandle = { router: (req: Request) => Promise; registry: PairingRegistry; tokenStore: TokenStore; auditSink: AuditSink; /** * The active rate limiter. Exposed so surfaces composed AROUND the * core — notably the MCP router, which runs BEFORE `router` and would * otherwise never consult one — share the same buckets instead of * running unthrottled or building a second limiter with its own state. */ rateLimiter: RateLimiter; /** * The bucket key this deployment uses for a caller with no resolved * identity. Exposed for the same reason as `rateLimiter`: the MCP * router runs BEFORE `router` and has to key its own limiter, and * two surfaces disagreeing about which hop is trustworthy is how one * of them ends up trusting an attacker-supplied header. */ clientIp: (req: Request) => string; /** * Origin allowlist for WebSocket upgrades (CSWSH defense), mirroring * the `corsOrigins` core option. `undefined`/empty means same-origin * only. Runtime upgrade adapters (`web/upgrade.ts`, the Node * `wsUpgrade`) read this to validate the handshake `Origin`. */ allowedOrigins?: readonly string[]; /** * Sliding (inactivity) TTL in ms, mirroring the `slidingTtlMs` core * option. The WS upgrade adapters apply this on acceptance via * `acceptConnection`, which already enforces it server-side. */ slidingTtlMs?: number; /** * Validate an agent token and register a `PairingConnection` with * the registry. Use this after accepting a WebSocket upgrade via * your runtime's native API (e.g. `WebSocketPair` on Cloudflare, * `Deno.upgradeWebSocket` on Deno, `server.upgrade` on Bun). * * On success: marks the token `awaiting-claude`, writes an audit * entry, and returns `{ok: true, tid}`. On failure: returns an * appropriate HTTP status for the caller to encode into the * upgrade response (401 for auth failure, 403 for revoked). */ acceptConnection: (token: string, conn: PairingConnection) => Promise; }; /** * Compose the runtime-neutral agent server. The returned handle has * everything the LAP HTTP routes and the WebSocket acceptance * plumbing need; runtime adapters wire the native upgrade API on * top (see `@llui/agent/server` for Node, `@llui/agent/server/web` * for WHATWG runtimes). */ export declare function createLluiAgentCore(opts?: CoreOptions): AgentCoreHandle; //# sourceMappingURL=core.d.ts.map