/** * ApprovalInbox — Web-accessible human-in-the-loop approval system * * Provides an HTTP API and SSE streaming for managing approval requests * from AI agents. Designed to work as an ApprovalCallback for ApprovalGate * while also exposing a REST-like HTTP interface. * * Features: * - Queues approval requests with unique IDs and optional timeouts * - REST API: list, get, approve, deny, stats * - SSE stream for real-time notifications * - Auto-expiry for stale requests * - Standalone HTTP server or mountable handler * * @module ApprovalInbox * @version 1.0.0 */ import { EventEmitter } from 'events'; import { IncomingMessage, ServerResponse, Server } from 'http'; import type { ApprovalRequest, ApprovalDecision, ApprovalCallback } from './agent-runtime'; /** Status of an approval entry */ export type ApprovalStatus = 'pending' | 'approved' | 'denied' | 'expired'; /** A queued approval entry */ export interface ApprovalEntry { /** Unique identifier for this approval */ id: string; /** The original approval request */ request: ApprovalRequest; /** Current status */ status: ApprovalStatus; /** Decision if resolved */ decision?: ApprovalDecision; /** When the request was created */ createdAt: number; /** When the request was resolved */ resolvedAt?: number; /** Timeout for this specific request (ms) */ timeoutMs: number; } /** Options for the ApprovalInbox */ export interface ApprovalInboxOptions { /** * Bearer token required for every route (GET list/stats/sse/:id and * POST approve/deny) once configured. Strongly recommended in * production — without a secret, any process that can reach the HTTP * server can read queued action details (command strings, file paths, * justifications) and approve/deny agent actions (GHSA-mxjx-28vx-xjjj, * GHSA-m4jg-6w3q-gm86). Clients must send: `Authorization: Bearer `. */ secret?: string; /** Default timeout for approval requests in ms (default: 300000 = 5 min) */ defaultTimeoutMs?: number; /** Maximum number of pending approvals (default: 100) */ maxPending?: number; /** Maximum history entries to keep (default: 1000) */ maxHistory?: number; /** URL path prefix for HTTP handler (default: '/approvals') */ pathPrefix?: string; /** * Allowed CORS origins for cross-origin browser requests. If omitted, * no `Access-Control-Allow-Origin` header is sent at all — the safe * default, since without it browsers enforce same-origin and no cross- * site page can read responses. Pass an explicit list of exact origin * strings (e.g. `['https://example.com']`) to opt in to cross-origin * access from those origins only. The wildcard `*` is never emitted * (GHSA-m4jg-6w3q-gm86). */ allowedOrigins?: string[]; } /** SSE event types */ export type InboxEventType = 'new' | 'approved' | 'denied' | 'expired'; /** SSE event payload */ export interface InboxEvent { type: InboxEventType; entry: ApprovalEntry; } /** Stats snapshot */ export interface InboxStats { pending: number; approved: number; denied: number; expired: number; total: number; } /** * Web-accessible approval inbox for human-in-the-loop agent workflows. * * Use `inbox.callback()` to get an ApprovalCallback for ApprovalGate, * and `inbox.httpHandler()` to mount the HTTP API on a server. * * @example * ```ts * const inbox = new ApprovalInbox(); * const gate = new ApprovalGate(inbox.callback()); * const server = inbox.createServer(3002); * ``` */ export declare class ApprovalInbox extends EventEmitter { private readonly pending; private readonly history; private readonly sseClients; private readonly defaultTimeoutMs; private readonly maxPending; private readonly maxHistory; private readonly pathPrefix; private readonly secret; private readonly allowedOrigins; private approvedCount; private deniedCount; private expiredCount; constructor(options?: ApprovalInboxOptions); /** * Returns an ApprovalCallback suitable for ApprovalGate. * Each call enqueues a pending approval and waits for resolution. */ callback(): ApprovalCallback; /** * Enqueue an approval request. Returns a promise that resolves when * the request is approved, denied, or expires. */ enqueue(request: ApprovalRequest, timeoutMs?: number): Promise; /** * Approve a pending request. * @returns The resolved entry, or undefined if not found/already resolved. */ approve(id: string, approvedBy: string, reason?: string): ApprovalEntry | undefined; /** * Deny a pending request. * @returns The resolved entry, or undefined if not found/already resolved. */ deny(id: string, deniedBy?: string, reason?: string): ApprovalEntry | undefined; /** Get a single entry by ID (pending or historical) */ get(id: string): ApprovalEntry | undefined; /** List entries by status (default: 'pending') */ list(status?: ApprovalStatus | 'all'): ApprovalEntry[]; /** Get aggregate stats */ stats(): InboxStats; /** Number of pending approvals */ get pendingCount(): number; /** * Returns an HTTP request handler for the approval inbox API. * * Routes (relative to pathPrefix): * GET / — List approvals (?status=pending|approved|denied|expired|all) * GET /stats — Aggregate stats * GET /sse — SSE event stream * GET /:id — Get single entry * POST /:id/approve — Approve (body: { approvedBy, reason? }) * POST /:id/deny — Deny (body: { deniedBy?, reason? }) */ httpHandler(): (req: IncomingMessage, res: ServerResponse) => void; /** * Resolves the Access-Control-Allow-Origin value for this request. * Returns null (no header) unless `allowedOrigins` is configured and the * request's Origin header exactly matches an entry — the matched origin * is echoed back, never `*` (GHSA-m4jg-6w3q-gm86). */ private resolveCorsOrigin; /** * Create a standalone HTTP server for the inbox. * @returns The HTTP server instance (already listening). */ startServer(port: number, hostname?: string): Server; /** Broadcast an event to all connected SSE clients */ private broadcastSSE; /** Register an SSE client */ private addSSEClient; private resolve; private expire; private addToHistory; private routeRequest; /** * Validates the Authorization: Bearer header. Gates every route * (GET list/stats/sse/:id and POST approve/deny) when a secret is * configured — closing the read-route disclosure gap from * GHSA-m4jg-6w3q-gm86, an incomplete fix for GHSA-mxjx-28vx-xjjj that * only gated the two POST routes. * Returns true if the request is authorized (or no secret is configured). * Sends a 401/403 response and returns false if authorization fails. * Uses constant-time comparison to prevent timing attacks (GHSA-mxjx-28vx-xjjj). */ private checkAuth; private sendJson; private readBody; } //# sourceMappingURL=approval-inbox.d.ts.map