/** * Deliberately narrow audit vocabulary. A record identifies who acted on * which session and when, but can never carry terminal bytes, URLs, cookies, * authorization codes, or capability tokens. `audit.dropped` is the sink's own * bookkeeping: it marks that N earlier best-effort records were discarded under * backpressure, so gaps in the file are visible instead of silent. */ export type ControlAuditAction = 'auth.login' | 'auth.login_denied' | 'auth.logout' /** owner 在工作台里自取了一次常驻链接(`GET /api/workbench/standing-link`)。 * 记的是「谁、什么时候取的」;链接与 token 本身绝不进这条记录——那正是这套 * 审计词汇表刻意保持狭窄的原因。 */ | 'auth.standing_link_issued' | 'terminal.takeover' | 'terminal.takeover_reused' | 'terminal.release' | 'terminal.expired' | 'terminal.disconnected' | 'terminal.input' | 'preview.unlock' | 'preview.activity' | 'preview.lock' | 'preview.idle_relock' | 'preview.session_relock' /** P1-13:预览目标本身失效(worker 换代 / 会话关闭 / 端口易主)导致的收回。 */ | 'preview.target_relock' | 'audit.dropped'; export interface ControlAuditRecord { timestamp: string; user: string; session: string; action: ControlAuditAction; /** Optional non-sensitive count for input auditing. Input content is never retained. */ bytes?: number; /** Only on `audit.dropped` markers: how many queued records were discarded. */ dropped?: number; } export interface ControlAuditSink { append(record: ControlAuditRecord): void; } export interface FileControlAuditSinkOptions { path?: string; } export declare function defaultControlAuditPath(): string; export declare function controlAuditRecord(user: string, session: string, action: ControlAuditAction, opts?: { now?: Date; bytes?: number; }): ControlAuditRecord; /** * Append-only 0600 NDJSON sink. `appendFileSync` issues one O_APPEND write per * compact record, so workers may safely share the file without a read/modify/ * rename race. Audit failures are intentionally non-fatal to terminal I/O; * callers may inject a strict sink in deployments that require fail-closed * accounting. */ export declare class FileControlAuditSink implements ControlAuditSink { private readonly path; constructor(opts?: FileControlAuditSinkOptions); append(record: ControlAuditRecord): void; } /** * Backpressure cap for the async sink's in-memory queue. While the stream is * still opening or `write()` has signalled backpressure, at most this many * records wait in memory; older ones are dropped (and counted) first, so a * slow/NFS disk under sustained terminal input can never turn into unbounded * heap growth. */ export declare const AUDIT_ASYNC_MAX_PENDING = 2000; /** The minimal stream surface the async sink relies on. Structurally satisfied * by `fs.WriteStream`; tests inject a hand-rolled fake to steer `write()`'s * return value and fire `drain` deterministically. */ export interface AuditStreamLike { write(chunk: string, encoding: BufferEncoding): boolean; once(event: 'drain', listener: () => void): unknown; on(event: 'error', listener: (error: unknown) => void): unknown; } export interface AsyncFileControlAuditSinkOptions extends FileControlAuditSinkOptions { /** Queue cap override (tests use a small value). Defaults to {@link AUDIT_ASYNC_MAX_PENDING}. */ maxPending?: number; /** Test seam: bypass the filesystem and adopt the provided stream instead. */ streamFactory?: () => Promise; } /** * High-frequency, best-effort sink for terminal input counters. Directory and * permissions are established exactly once, then compact records are queued to * one O_APPEND stream so a slow/NFS home directory never blocks the worker's * PTY event loop for every keystroke. Security-boundary actions keep using the * synchronous FileControlAuditSink above so takeover can remain fail-closed. * * Backpressure contract: `append()` always returns synchronously (the audit is * a fire-and-forget side channel — it must never block or slow the caller). * When `write()` returns false the sink stops writing until `drain`; arriving * records queue up to {@link AUDIT_ASYNC_MAX_PENDING}, beyond which the oldest * are dropped and counted. Once writing resumes, an `audit.dropped` marker * record lands first so the gap is visible in the file. */ export declare class AsyncFileControlAuditSink implements ControlAuditSink { private readonly path; private readonly maxPending; private readonly streamFactory?; private stream; private opening; private pending; private droppedCount; private waitingDrain; private unavailable; constructor(opts?: AsyncFileControlAuditSinkOptions); append(record: ControlAuditRecord): void; /** Queue with the cap enforced: drop-oldest keeps the freshest records and * bounds memory no matter how long the disk stalls. */ private enqueue; /** Drain the queue onto the stream until it pushes back. Synchronous; safe to * call at any time (no-ops while unavailable, still opening, or awaiting * `drain`). The dropped-marker goes out first — everything it accounts for * is older than every surviving queued record. */ private flush; private takeDroppedMarker; private markUnavailable; private openStream; private openFileStream; } export declare function appendControlAudit(record: ControlAuditRecord): void; /** Test seam; production code should use the default append-only file sink. */ export declare function setDefaultControlAuditSinkForTest(sink: ControlAuditSink | undefined): void; //# sourceMappingURL=control-audit.d.ts.map