import { appendFileSync } from "node:fs"; import { EXTENSION_ID, type PermissionSystemExtensionConfig, } from "./extension-config"; import { OWNER_ONLY_FILE_MODE, restrictExistingPathToOwner, } from "./log-file-permissions"; import { redactedJsonStringify } from "./log-redaction"; export interface PermissionSystemLogger { debug: ( event: string, details?: Record, ) => string | undefined; review: ( event: string, details?: Record, ) => string | undefined; } interface PermissionSystemLoggerOptions { getConfig: () => PermissionSystemExtensionConfig; debugLogPath: string; reviewLogPath: string; ensureLogsDirectory: () => string | undefined; } export function createPermissionSystemLogger( options: PermissionSystemLoggerOptions, ): PermissionSystemLogger { const { debugLogPath, reviewLogPath, ensureLogsDirectory } = options; // Per-session, so a log inherited from an earlier version is tightened once // rather than on every line. Lives in the closure because the factory is // re-invoked per session, unlike module scope, which now outlives one. const hardened = new Set(); const writeLine = ( stream: "debug" | "review", path: string, event: string, details: Record, ): string | undefined => { const directoryError = ensureLogsDirectory(); if (directoryError) { return directoryError; } try { const line = redactedJsonStringify({ timestamp: new Date().toISOString(), extension: EXTENSION_ID, stream, event, ...details, }); if (!line) { return `Failed to write permission-system ${stream} log '${path}': event could not be serialized.`; } appendFileSync(path, `${line}\n`, { encoding: "utf-8", mode: OWNER_ONLY_FILE_MODE, }); if (!hardened.has(path)) { hardened.add(path); restrictExistingPathToOwner(path, OWNER_ONLY_FILE_MODE); } return undefined; } catch (error) { const message = error instanceof Error ? error.message : String(error); return `Failed to write permission-system ${stream} log '${path}': ${message}`; } }; const debug = ( event: string, details: Record = {}, ): string | undefined => { if (!options.getConfig().debugLog) { return undefined; } return writeLine("debug", debugLogPath, event, details); }; const review = ( event: string, details: Record = {}, ): string | undefined => { if (!options.getConfig().permissionReviewLog) { return undefined; } return writeLine("review", reviewLogPath, event, details); }; return { debug, review }; }