/** Which host wrote the line; also the file name (`.log`). */ export type DiagRole = "worker" | "supervisor" | "main"; /** * The catalogue of things worth recording. A closed union rather than a free * string so that adding an event is a deliberate edit in THIS file, next to the * redaction rule above — the moment to ask "what exactly am I about to write?". */ export type DiagEvent = "startup" | "shutdown" | "conn.register_ok" | "conn.register_failed" | "conn.device_reset" | "conn.ticket_failed" | "conn.ticket_error" | "conn.ticket_invalid" | "conn.ws_open" | "conn.ws_close" | "conn.ws_error" | "conn.ws_upgrade_failed" | "conn.retry" | "conn.status" | "conn.token_rotated" | "conn.token_rotate_failed" | "job.start" | "job.refused" | "job.start_failed" | "job.cancel" | "job.exit" | "path.error" | "autostart.repaired" | "autostart.repair_failed" | "update.available" | "update.none" | "update.check_failed" | "supervisor.worker_spawned" | "supervisor.worker_exit" | "supervisor.worker_stalled" | "integrity.ok" | "integrity.damaged" | "integrity.check_failed" | "desktop.watchdog_restart" | "desktop.uncaught" | "desktop.unhandled_rejection" | "diag.dropped"; /** * Scalars only. Objects and Error instances are rejected by the type system * because that is how command text, env maps and fs error messages (which carry * the path they failed on) would otherwise arrive here by accident. */ export type DiagValue = string | number | boolean | null | undefined; export type DiagFields = Record; export declare function redactHomePaths(text: string): string; /** * The last line of defence for every string that reaches the file. Callers are * expected not to hand us secrets in the first place; this is what makes a * mistake harmless instead of a credential in a vendor's inbox. */ export declare function redactDiagText(text: string): string; /** * Where the log lives when the host does not name a directory. * * Deliberately mirrors `resolveJobsRoot`'s ladder (job-manager.ts) — same * reasoning, same answer, so an operator finds jobs and logs side by side — and * deliberately NOT the /etc credential directory: this is variable data. * 1. an explicit `configDir` (the desktop's per-user data dir); * 2. AICOMMANDER_CONFIG_DIR, for machines whose defaults are volatile (QNAP); * 3. /var/lib/aicommander for a root service (FHS); * 4. otherwise a per-user data dir. */ export declare function resolveDiagLogDir(configDir?: string): string; export interface DiagLogOptions { /** Directory to write into; created 0700 if missing. */ dir: string; /** Names the file (`.log`) and every line. One file per process role. */ role: DiagRole; /** Test seam: rotate sooner than 512 KiB. */ maxFileBytes?: number; } /** * Point the logger at a file. Idempotent-ish: a second call replaces the target * (the tests rely on that), and a failure here simply leaves the logger inert — * `diag()` then costs a null check. * * This is the ONLY synchronous I/O in the module and it runs once, at startup, * before anything is connected. */ export declare function initDiagLog(opts: DiagLogOptions): void; /** Absolute path of the live log file, or null when logging is inert. */ export declare function diagLogPath(): string | null; /** * Record one event. Never throws, never blocks, never touches the disk on the * calling turn. Inert until a host calls `initDiagLog`. */ export declare function diag(event: DiagEvent, fields?: DiagFields): void; /** * Wait for queued lines to reach the disk. For tests and for the rare shutdown * path that can afford one await; nothing on a hot path may call it. */ export declare function flushDiagLog(): Promise; /** * Exit, but not before the lines that explain WHY are on disk. * * `diag()` deliberately defers its I/O to a timer, and `process.exit` never lets * that timer fire — so every fatal path that logged its cause and then exited * wrote that cause to memory and threw it away. The events lost this way were * precisely the ones this log exists for: `conn.register_failed` and the * `path.error`s on our own credential, state and jobs paths, i.e. the antivirus * signature. Every `process.exit` in this package that follows a `diag()` goes * through here instead, and a test asserts there are no others. * * Bounded: a flush that cannot complete (a disk that is gone, an AV holding the * file) must not hold the process open, so the wait is raced against a timer and * the exit happens either way. Silence stays the contract. */ export declare function exitAfterDiagFlush(code: number): Promise; /** Stop logging and drop any pending lines. Tests and teardown only. */ export declare function closeDiagLog(): void; /** * The safe half of an unknown error: for an errno failure (`EACCES`, `EPERM`, * `ENOENT` on our own paths — the antivirus signature) the `code` and `syscall`, * which is the whole diagnosis, and never `err.message`, which embeds the path. * Anything else degrades to the error's class name. */ export declare function errorFields(err: unknown): DiagFields; /** Mark an error as already recorded under its own name. */ export declare function noteDiagnosed(err: unknown): void; /** Has this error already been recorded? Outer handlers must not rename it. */ export declare function alreadyDiagnosed(err: unknown): boolean; /** * The only shape a job id may reach the log in. * * A job id is the APPROVED way to talk about a job here (the header's rule), and * that permission rests entirely on a job id being a 16-hex token of OUR OWN * making. Several of these lines are written from a relay frame whose fields are * untrusted and, on the cancel path, not yet validated by the job manager — so a * malformed or hostile frame could otherwise put command text or a user's path * into a vendor-bound file through the nominal `jobId` field. Anything that is * not a job id is recorded AS not being one: `invalid` says the frame was * malformed, which is itself the diagnostic, and quoting the value back would be * the vulnerability. * * The pattern is the shared one job-manager.ts validates against before a jobId * can reach a path join — the log must not accept anything weaker. */ export declare function diagJobId(jobId: unknown): string; /** * Facts about this process that a support ticket always needs and that nobody * can read off a running machine: what is installed where, and with what * privileges. Emitted by each host right after `initDiagLog`. */ export declare function logStartup(fields?: DiagFields): void;