import { createInterface, type Interface } from "node:readline"; /** * Host-side admission policy for clients (connecting browsers — each gets a full * VS Code session, so this is a real gate, not just UX). * - "auto": admit everyone with the token (default; the token is the gate). * - "confirm": hold each new client until the host approves it at the terminal, * unless its label matches a pre-approved `--allow` pattern. */ export type ApprovePolicy = "auto" | "confirm"; export interface ApproverOptions { policy: ApprovePolicy; /** Case-insensitive label substrings auto-approved even under "confirm". */ allow: string[]; /** Tear down a live client's connection (kick). */ kick: (clientId: string) => void; /** Emit a "pending approval" hint to a client that's now waiting on a human. */ notifyPending: (clientId: string) => void; /** Whether stdin is an interactive terminal (defaults to process.stdin.isTTY). */ isTTY?: boolean; /** Injected input/output for tests; default to the real process streams. */ input?: NodeJS.ReadableStream; output?: NodeJS.WritableStream; log?: (msg: string) => void; } interface Pending { clientId: string; label: string; resolve: (ok: boolean) => void; } /** * Decides whether to admit clients and drives an interactive terminal console * for approve / deny / kick / list. Self-contained (no WebRTC deps) so the * daemon stays thin and this stays unit-testable. */ export class Approver { private opts: ApproverOptions; private isTTY: boolean; private out: NodeJS.WritableStream; private log: (msg: string) => void; /** Labels approved with "a(lways)" this session — auto-admit on sight. */ private sessionAllow = new Set(); /** Live clients (admitted + connected), for `list` / `kick`. */ private active = new Map(); /** Approvals waiting on the host; the head is the one being asked. */ private queue: Pending[] = []; private asking = false; private rl: Interface | null = null; constructor(opts: ApproverOptions) { this.opts = opts; this.isTTY = opts.isTTY ?? Boolean(process.stdin.isTTY); this.out = opts.output ?? process.stdout; this.log = opts.log ?? ((m) => console.log(m)); } /** Announce the active policy once at startup. */ banner(): void { if (this.opts.policy === "auto") return; if (this.isTTY) { this.log("[codehost] approval: confirm — new clients wait for your OK. Commands: list · kick "); } else { this.log( "[codehost] approval: confirm, but no terminal is attached — clients can't be approved " + "interactively and will be denied. Use --approve auto or --allow