/** * Parse a millisecond deadline from an env override, falling back to `fallback` * unless the value is a finite positive number. A bare `Number(env) || fallback` * silently accepts a NEGATIVE override (`-5 || 8000` is -5) — a non-positive * deadline makes the fail-closed wait/request return immediately, defeating the * deadline. Unset/blank/NaN/<=0 all take the sane positive fallback; a load-time * throw is deliberately avoided so a misconfigured env can never crash these * fail-closed hooks into a fail-OPEN non-load. * @param {string|undefined} raw the env override value * @param {number} fallback the sane positive default * @returns {number} */ export function positiveMsOr(raw: string | undefined, fallback: number): number; /** * The shape the redactor returns: plain mode `{text, found}`, map mode * `{text, pairs, found}` or `{unmappable}`. All fields optional so a consumer * narrows the variant it expects. * @typedef {object} RedactResponse * @property {string} [text] * @property {string[]} [found] * @property {{placeholder: string, original: string, start: number}[]} [pairs] * @property {string} [unmappable] */ /** * Classify the socket path before we connect and hand it live credentials. * The request body carries collectEnvSecrets() — plaintext key VALUES — and the * socket lives at a predictable, world-visible $TMPDIR path any co-tenant can * reach. This is the one channel in the hook suite that ships secrets, so it * needs the same squat defense markerIsTrusted / writeFileNoFollow apply to the * marker/sentinel files. lstatSync does NOT traverse a final symlink, so a * planted symlink reads as a symlink (isSocket() false) and a foreign daemon * fails the uid check. * - "absent" → nothing there yet: let createConnection ENOENT so the caller's * respawn path spawns OUR daemon (never a refuse — that would * break the cold-start spawn). * - "untrusted" → something IS bound there but it is not our socket under a dir * only a trusted uid can write (a co-tenant squat): refuse, so * no secret is written. * - "ok" → our socket, our uid, under a dir isTrustedSocketDir accepts. * `lstat`/`uid` are injectable seams so a test can drive a stat shape the test * process cannot create (a dir owned by another uid); production binds the real ones. * @param {string} socketPath * @param {{lstat?: typeof lstatSync, uid?: number}} [deps] * @returns {"absent" | "untrusted" | "ok"} */ export function classifySocket(socketPath: string, deps?: { lstat?: typeof lstatSync; uid?: number; }): "absent" | "untrusted" | "ok"; /** * Open one connection, send `request`, resolve with the parsed response object * (or null). Rejects on connect failure, a malformed/oversize/short frame, or an * {error} response — every one of which the caller turns into a fail-closed. A * socket present but not owned by us fails closed WITHOUT respawning (the error * carries no errno, so isRespawnable is false), so we never dial into a squat. * @param {string} socketPath * @param {{text: string, map: boolean, web_ingress: boolean}} request * @param {number} [deadlineMs] total exchange deadline; defaults to the env-tunable value * @returns {Promise} */ export function connectAndRequest(socketPath: string, request: { text: string; map: boolean; web_ingress: boolean; }, deadlineMs?: number): Promise; /** * Spawn the daemon detached so it outlives this hook process. The daemon's bind() * is the cross-process mutex, so a racing second spawn just exits — the spawn is * idempotent and needs no lock here. * @param {string} socketPath * @param {string[]} [command] daemon command as [argv0, ...leadingArgs] * (injectable so tests can drive the missing-binary arm in-process; * production always uses daemonCommand()) */ export function spawnDaemon(socketPath: string, command?: string[]): void; /** * Poll until the daemon is accepting connections or the deadline passes. Probes by * connecting (not just existsSync) so it waits for listen(), not merely bind(). * @param {string} socketPath * @param {{deadlineMs?: number, stepMs?: number}} [opts] * @returns {Promise} */ export function waitForSocket(socketPath: string, { deadlineMs, stepMs }?: { deadlineMs?: number; stepMs?: number; }): Promise; /** * Redact `text` via the daemon. Returns the response object (`{text, found}` for * plain, `{text, pairs, found}` / `{unmappable}` for map) or null when nothing was * redacted (plain mode). Throws to fail closed when the text cannot be vetted. * * `connect`/`spawn`/`waitForSocket` are injectable seams (default to the real * implementations) so callers can stub the daemon in-process. `deadline` is the * caller's shared wall-clock budget (makeDeadline): when supplied, every dial and * the respawn wait are bounded by the budget REMAINING at that moment, and a spent * budget fails CLOSED without dialing — never dial with a non-positive deadline, * which would race and could return the raw, unvetted secret (fail open). Omitted, * the redactor keeps its own per-call request deadline (the standalone default). * @param {string} text * @param {{map?: boolean, webIngress?: boolean, socketPath?: string, * deadline?: {remainingMs: () => number}, * connect?: typeof connectAndRequest, spawn?: typeof spawnDaemon, * waitForSocket?: typeof waitForSocket, now?: () => number}} [opts] * @returns {Promise} */ export function redactViaDaemon(text: string, opts?: { map?: boolean; webIngress?: boolean; socketPath?: string; deadline?: { remainingMs: () => number; }; connect?: typeof connectAndRequest; spawn?: typeof spawnDaemon; waitForSocket?: typeof waitForSocket; now?: () => number; }): Promise; export const FRAME_CAP: number; export const DEFAULT_SOCKET_PATH: string; /** * The shape the redactor returns: plain mode `{text, found}`, map mode * `{text, pairs, found}` or `{unmappable}`. All fields optional so a consumer * narrows the variant it expects. */ export type RedactResponse = { text?: string | undefined; found?: string[] | undefined; pairs?: { placeholder: string; original: string; start: number; }[] | undefined; unmappable?: string | undefined; }; import { lstatSync } from "node:fs";