@warlock.js/logger 4.15.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"logger.mjs","names":[],"sources":["../../../../../../logger/src/logger.ts"],"sourcesContent":["import { Random } from \"@mongez/reinforcements\";\r\nimport type { LogChannel } from \"./log-channel\";\r\nimport { applyRedact, mergeRedact } from \"./redact\";\r\nimport type {\r\n AutoFlushEvent,\r\n LoggingData,\r\n LogLevel,\r\n OmittedLoggingData,\r\n RedactConfig,\r\n} from \"./types\";\r\nimport { clearMessage } from \"./utils/clear-message\";\r\n\r\nconst SIGNAL_EVENTS: ReadonlySet<AutoFlushEvent> = new Set([\r\n \"SIGINT\",\r\n \"SIGTERM\",\r\n \"SIGHUP\",\r\n \"SIGBREAK\",\r\n \"SIGUSR2\",\r\n]);\r\n\r\n/**\r\n * Severity ranks used by `setMinLevel`. Higher number = more severe. The\r\n * ordering matches conventional log-level hierarchies: `debug` is noisiest\r\n * and easiest to drop; `error` is the loudest and never dropped by the\r\n * minimum-level filter. `success` sits beside `info` — it's an informational\r\n * outcome, not a warning.\r\n */\r\nconst LEVEL_RANK: Record<LogLevel, number> = {\r\n debug: 0,\r\n info: 1,\r\n success: 1,\r\n warn: 2,\r\n error: 3,\r\n fatal: 4,\r\n};\r\n\r\nexport class Logger {\r\n /**\r\n * Current channel\r\n */\r\n public channels: LogChannel[] = [];\r\n\r\n public id = \"logger-\" + Random.string(32);\r\n\r\n /**\r\n * Registered auto-flush handlers, keyed by event name. Stored so repeated\r\n * calls to `enableAutoFlush` replace rather than stack, and so\r\n * `disableAutoFlush` can remove them cleanly.\r\n */\r\n private autoFlushHandlers = new Map<AutoFlushEvent, () => void>();\r\n\r\n /**\r\n * Logger-wide minimum severity. When set, entries below this level are\r\n * dropped before any channel is invoked — cheaper than per-channel `levels`\r\n * filters because the fan-out loop is skipped entirely. `undefined` means\r\n * no minimum (every entry reaches every channel that accepts it).\r\n */\r\n private minLevel?: LogLevel;\r\n\r\n /**\r\n * Logger-wide redaction floor. Applied once before fan-out — every\r\n * channel receives an entry with these paths already censored. Channel\r\n * configs can extend the path list (additive); they cannot remove paths\r\n * set here.\r\n */\r\n private redactConfig?: RedactConfig;\r\n\r\n /**\r\n * Add a new channel\r\n */\r\n public addChannel(channel: LogChannel) {\r\n this.channels.push(channel);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set base configurations\r\n */\r\n public configure(config: {\r\n channels?: LogChannel[];\r\n autoFlushOn?: AutoFlushEvent[];\r\n minLevel?: LogLevel;\r\n redact?: RedactConfig;\r\n }) {\r\n if (config.channels) {\r\n this.channels = config.channels;\r\n }\r\n\r\n if (config.autoFlushOn) {\r\n this.enableAutoFlush(config.autoFlushOn);\r\n }\r\n\r\n if (config.minLevel !== undefined) {\r\n this.setMinLevel(config.minLevel);\r\n }\r\n\r\n if (config.redact !== undefined) {\r\n this.setRedact(config.redact);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set the logger-wide redaction floor. Applied to every entry before\r\n * fan-out; channel configs add more paths on top, never fewer. Pass\r\n * `undefined` to clear.\r\n *\r\n * @example\r\n * log.setRedact({\r\n * paths: [\"context.password\", \"context.*.token\"],\r\n * censor: \"[REDACTED]\",\r\n * });\r\n */\r\n public setRedact(config: RedactConfig | undefined): this {\r\n this.redactConfig = config;\r\n return this;\r\n }\r\n\r\n /**\r\n * Read the active logger-wide redact config (or `undefined`).\r\n */\r\n public getRedact(): RedactConfig | undefined {\r\n return this.redactConfig;\r\n }\r\n\r\n /**\r\n * Drop every entry whose severity is below `level` before fan-out. Cheaper\r\n * than per-channel `levels` filters because the loop never runs and no\r\n * channel receives the entry. Pass `undefined` to clear and accept all\r\n * levels again.\r\n *\r\n * @example\r\n * // production: silence debug noise everywhere at once\r\n * logger.setMinLevel(\"info\");\r\n */\r\n public setMinLevel(level: LogLevel | undefined): this {\r\n this.minLevel = level;\r\n return this;\r\n }\r\n\r\n /**\r\n * Read the active minimum severity (or `undefined` when none is set).\r\n */\r\n public getMinLevel(): LogLevel | undefined {\r\n return this.minLevel;\r\n }\r\n\r\n /**\r\n * Set channels\r\n */\r\n public setChannels(channels: LogChannel[]) {\r\n this.channels = channels;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Normalize log data to a single object\r\n */\r\n private normalizeLogData(\r\n dataOrModule: LoggingData | OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n level?: LogLevel,\r\n context?: Record<string, any>,\r\n ): LoggingData {\r\n if (typeof dataOrModule === \"object\") {\r\n // If level is provided, override type\r\n return {\r\n type: (level || (dataOrModule as any).type || \"info\") as LogLevel,\r\n module: dataOrModule.module,\r\n action: dataOrModule.action,\r\n message: dataOrModule.message,\r\n ...(context ? { context } : dataOrModule.context ? { context: dataOrModule.context } : {}),\r\n };\r\n }\r\n return {\r\n type: (level || \"info\") as LogLevel,\r\n module: dataOrModule,\r\n action: action as string,\r\n message,\r\n ...(context ? { context } : {}),\r\n };\r\n }\r\n\r\n /**\r\n * Make log\r\n *\r\n * Fans out a single log entry to every registered channel. Non-terminal\r\n * channels receive a copy whose `message` has had ANSI color codes stripped\r\n * — each channel sees its own shallow clone so one channel cannot observe\r\n * another's mutations (e.g. a later terminal channel still sees the original\r\n * colored message).\r\n */\r\n public async log(data: LoggingData) {\r\n if (this.minLevel && LEVEL_RANK[data.type] < LEVEL_RANK[this.minLevel]) {\r\n return this;\r\n }\r\n\r\n // Apply the logger-wide redact floor once. Every channel sees the\r\n // result; no channel can undo a logger-wide redaction (additive-only\r\n // semantics).\r\n const baseEntry = applyRedact(data, this.redactConfig);\r\n\r\n for (const channel of this.channels) {\r\n const channelRedact = channel.getRedactConfig?.();\r\n const effectiveRedact = channelRedact\r\n ? mergeRedact(this.redactConfig, channelRedact)\r\n : undefined;\r\n\r\n // When the channel adds paths, redact again from `data` rather than\r\n // from `baseEntry` so the merged config (which already contains the\r\n // logger-wide paths) does the full pass — avoids double-cloning the\r\n // already-redacted base.\r\n let payload = effectiveRedact ? applyRedact(data, effectiveRedact) : baseEntry;\r\n\r\n if (channel.terminal === false) {\r\n payload = { ...payload, message: clearMessage(payload.message) };\r\n }\r\n\r\n channel.log(payload);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Make debug log\r\n */\r\n public debug(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"debug\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make info log\r\n */\r\n public info(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"info\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make warn log\r\n */\r\n public warn(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"warn\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make error log\r\n */\r\n public error(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"error\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make success log\r\n */\r\n public success(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"success\", context);\r\n\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make fatal log — for unrecoverable failures where the application is going\r\n * down (failed bootstrap, lost connection to a required dependency that the\r\n * caller has decided not to retry, an `uncaughtException`).\r\n *\r\n * Identical shape to {@link error}; the level is purely informational —\r\n * `fatal` does NOT auto-flush or exit. The caller decides whether to call\r\n * `await log.flush()` and `process.exit(...)`.\r\n */\r\n public fatal(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"fatal\", context);\r\n\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Log an `error` entry when `condition` is falsy. No-op otherwise — the\r\n * entry is never built and channels are not invoked, so this is genuinely\r\n * free in the happy path. Mirrors the spirit of `console.assert` but routes\r\n * through the logger pipeline so persistent channels capture failures.\r\n *\r\n * @example\r\n * log.assert(user !== null, \"auth\", \"session\", \"user vanished mid-flight\", { sessionId });\r\n */\r\n public assert(\r\n condition: unknown,\r\n module: string,\r\n action: string,\r\n message: any,\r\n context?: Record<string, any>,\r\n ): Promise<Logger> | Logger {\r\n if (condition) return this;\r\n return this.error(module, action, message, context);\r\n }\r\n\r\n /**\r\n * Start a duration timer. The returned function emits an `info` entry\r\n * with `completed in <ms>ms` and a `durationMs` field in `context` when\r\n * called. Pass an object to `end()` to merge extra fields into context.\r\n *\r\n * @example\r\n * const end = log.timer(\"db\", \"users.findById\");\r\n * const user = await usersRepo.findById(id);\r\n * end({ id, found: !!user });\r\n */\r\n public timer(\r\n module: string,\r\n action: string,\r\n ): (extra?: Record<string, any>) => Promise<Logger> {\r\n const startedAt = Date.now();\r\n return (extra?: Record<string, any>) => {\r\n const durationMs = Date.now() - startedAt;\r\n return this.info(module, action, `completed in ${durationMs}ms`, {\r\n durationMs,\r\n ...(extra ?? {}),\r\n });\r\n };\r\n }\r\n\r\n /**\r\n * Get channel by name\r\n */\r\n public channel(name: string) {\r\n return this.channels.find((channel) => channel.name === name);\r\n }\r\n\r\n /**\r\n * Synchronously flush logs\r\n */\r\n public flushSync() {\r\n for (const channel of this.channels) {\r\n if (channel.flushSync) {\r\n channel.flushSync();\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Asynchronously drain every channel that implements `flush()`.\r\n *\r\n * Unlike {@link flushSync}, this awaits each channel's async I/O — the\r\n * correct call for a graceful shutdown that can afford to wait\r\n * (`await log.flush()` after closing the HTTP server, before\r\n * `process.exit`). A channel whose delivery is async (a network transport,\r\n * an async disk write) implements `flush()`, not `flushSync()`.\r\n *\r\n * Channels are isolated: a channel whose flush rejects can neither prevent\r\n * the others from draining nor escape as an unhandled rejection. Channels\r\n * without `flush()` are skipped.\r\n *\r\n * @example\r\n * async function shutdown() {\r\n * await httpServer.close();\r\n * await log.flush();\r\n * process.exit(0);\r\n * }\r\n */\r\n public async flush(): Promise<void> {\r\n await Promise.allSettled(\r\n this.channels.map(async (channel) => {\r\n if (!channel.flush) {\r\n return;\r\n }\r\n\r\n try {\r\n await channel.flush();\r\n } catch {\r\n // A single channel must never break shutdown for the others —\r\n // a graceful drain is best-effort across every channel.\r\n }\r\n }),\r\n );\r\n }\r\n\r\n /**\r\n * Register one process-level handler per event that calls `flushSync()`\r\n * before the process terminates.\r\n *\r\n * For signal events (`SIGINT`, `SIGTERM`, `SIGHUP`, `SIGBREAK`, `SIGUSR2`)\r\n * the handler flushes and then re-raises the signal so Node's default exit\r\n * behavior runs. For `beforeExit`, the handler flushes in place — Node exits\r\n * naturally afterwards.\r\n *\r\n * Idempotent: calling with the same events replaces the previous handlers.\r\n * Call `disableAutoFlush()` to unregister.\r\n *\r\n * @example\r\n * log.configure({\r\n * channels: [new ConsoleLog(), new FileLog()],\r\n * autoFlushOn: [\"SIGINT\", \"SIGTERM\", \"beforeExit\"],\r\n * });\r\n */\r\n public enableAutoFlush(events: AutoFlushEvent[]): this {\r\n this.disableAutoFlush();\r\n\r\n for (const event of events) {\r\n const handler = SIGNAL_EVENTS.has(event)\r\n ? () => {\r\n this.flushSync();\r\n process.off(event, handler);\r\n process.kill(process.pid, event as NodeJS.Signals);\r\n }\r\n : () => {\r\n this.flushSync();\r\n };\r\n\r\n process.on(event, handler);\r\n this.autoFlushHandlers.set(event, handler);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Remove every handler previously registered by `enableAutoFlush`.\r\n * Safe to call when no handlers are registered.\r\n */\r\n public disableAutoFlush(): this {\r\n for (const [event, handler] of this.autoFlushHandlers) {\r\n process.off(event, handler);\r\n }\r\n\r\n this.autoFlushHandlers.clear();\r\n\r\n return this;\r\n }\r\n}\r\n\r\n/**\r\n * The package singleton. Use this for everyday logging — `log.info(...)`,\r\n * `log.error(...)`, `log.configure(...)`. Custom logger instances can be\r\n * created by instantiating `Logger` directly.\r\n *\r\n * The name is intentionally short: `log` reads naturally at the call site\r\n * (`log.info(\"auth\", \"login\", \"ok\")`) and matches the convention used in\r\n * pino, bunyan, and most JS logging tutorials.\r\n *\r\n * Note that `log` is a `Logger` instance, **not** a function — the bare\r\n * callable form was removed when the dual `log` / `logger` exports were\r\n * collapsed into a single name. Use `log.info(...)` (or any other level\r\n * shortcut) to emit entries.\r\n */\r\nexport const log = new Logger();\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"logger.mjs","names":[],"sources":["../../../../../../logger/src/logger.ts"],"sourcesContent":["import { Random } from \"@mongez/reinforcements\";\r\nimport type { LogChannel } from \"./log-channel\";\r\nimport { applyRedact, mergeRedact } from \"./redact\";\r\nimport type {\r\n AutoFlushEvent,\r\n LoggingData,\r\n LogLevel,\r\n OmittedLoggingData,\r\n RedactConfig,\r\n} from \"./types\";\r\nimport { clearMessage } from \"./utils/clear-message\";\r\n\r\nconst SIGNAL_EVENTS: ReadonlySet<AutoFlushEvent> = new Set([\r\n \"SIGINT\",\r\n \"SIGTERM\",\r\n \"SIGHUP\",\r\n \"SIGBREAK\",\r\n \"SIGUSR2\",\r\n]);\r\n\r\n/**\r\n * Severity ranks used by `setMinLevel`. Higher number = more severe. The\r\n * ordering matches conventional log-level hierarchies: `debug` is noisiest\r\n * and easiest to drop; `error` is the loudest and never dropped by the\r\n * minimum-level filter. `success` sits beside `info` — it's an informational\r\n * outcome, not a warning.\r\n */\r\nconst LEVEL_RANK: Record<LogLevel, number> = {\r\n debug: 0,\r\n info: 1,\r\n success: 1,\r\n warn: 2,\r\n error: 3,\r\n fatal: 4,\r\n};\r\n\r\nexport class Logger {\r\n /**\r\n * Current channel\r\n */\r\n public channels: LogChannel[] = [];\r\n\r\n public id = \"logger-\" + Random.string(32);\r\n\r\n /**\r\n * Registered auto-flush handlers, keyed by event name. Stored so repeated\r\n * calls to `enableAutoFlush` replace rather than stack, and so\r\n * `disableAutoFlush` can remove them cleanly.\r\n */\r\n private autoFlushHandlers = new Map<AutoFlushEvent, () => void>();\r\n\r\n /**\r\n * Logger-wide minimum severity. When set, entries below this level are\r\n * dropped before any channel is invoked — cheaper than per-channel `levels`\r\n * filters because the fan-out loop is skipped entirely. `undefined` means\r\n * no minimum (every entry reaches every channel that accepts it).\r\n */\r\n private minLevel?: LogLevel;\r\n\r\n /**\r\n * Logger-wide redaction floor. Applied once before fan-out — every\r\n * channel receives an entry with these paths already censored. Channel\r\n * configs can extend the path list (additive); they cannot remove paths\r\n * set here.\r\n */\r\n private redactConfig?: RedactConfig;\r\n\r\n /**\r\n * Add a new channel\r\n */\r\n public addChannel(channel: LogChannel) {\r\n this.channels.push(channel);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set base configurations\r\n */\r\n public configure(config: {\r\n channels?: LogChannel[];\r\n autoFlushOn?: AutoFlushEvent[];\r\n minLevel?: LogLevel;\r\n redact?: RedactConfig;\r\n }) {\r\n if (config.channels) {\r\n this.channels = config.channels;\r\n }\r\n\r\n if (config.autoFlushOn) {\r\n this.enableAutoFlush(config.autoFlushOn);\r\n }\r\n\r\n if (config.minLevel !== undefined) {\r\n this.setMinLevel(config.minLevel);\r\n }\r\n\r\n if (config.redact !== undefined) {\r\n this.setRedact(config.redact);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set the logger-wide redaction floor. Applied to every entry before\r\n * fan-out; channel configs add more paths on top, never fewer. Pass\r\n * `undefined` to clear.\r\n *\r\n * @example\r\n * log.setRedact({\r\n * paths: [\"context.password\", \"context.*.token\"],\r\n * censor: \"[REDACTED]\",\r\n * });\r\n */\r\n public setRedact(config: RedactConfig | undefined): this {\r\n this.redactConfig = config;\r\n return this;\r\n }\r\n\r\n /**\r\n * Read the active logger-wide redact config (or `undefined`).\r\n */\r\n public getRedact(): RedactConfig | undefined {\r\n return this.redactConfig;\r\n }\r\n\r\n /**\r\n * Drop every entry whose severity is below `level` before fan-out. Cheaper\r\n * than per-channel `levels` filters because the loop never runs and no\r\n * channel receives the entry. Pass `undefined` to clear and accept all\r\n * levels again.\r\n *\r\n * @example\r\n * // production: silence debug noise everywhere at once\r\n * logger.setMinLevel(\"info\");\r\n */\r\n public setMinLevel(level: LogLevel | undefined): this {\r\n this.minLevel = level;\r\n return this;\r\n }\r\n\r\n /**\r\n * Read the active minimum severity (or `undefined` when none is set).\r\n */\r\n public getMinLevel(): LogLevel | undefined {\r\n return this.minLevel;\r\n }\r\n\r\n /**\r\n * Set channels\r\n */\r\n public setChannels(channels: LogChannel[]) {\r\n this.channels = channels;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Normalize log data to a single object\r\n */\r\n private normalizeLogData(\r\n dataOrModule: LoggingData | OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n level?: LogLevel,\r\n context?: Record<string, any>,\r\n ): LoggingData {\r\n if (typeof dataOrModule === \"object\") {\r\n // If level is provided, override type\r\n return {\r\n type: (level || (dataOrModule as any).type || \"info\") as LogLevel,\r\n module: dataOrModule.module,\r\n action: dataOrModule.action,\r\n message: dataOrModule.message,\r\n ...(context ? { context } : dataOrModule.context ? { context: dataOrModule.context } : {}),\r\n };\r\n }\r\n return {\r\n type: (level || \"info\") as LogLevel,\r\n module: dataOrModule,\r\n action: action as string,\r\n message,\r\n ...(context ? { context } : {}),\r\n };\r\n }\r\n\r\n /**\r\n * Make log\r\n *\r\n * Fans out a single log entry to every registered channel. Non-terminal\r\n * channels receive a copy whose `message` has had ANSI color codes stripped\r\n * — each channel sees its own shallow clone so one channel cannot observe\r\n * another's mutations (e.g. a later terminal channel still sees the original\r\n * colored message).\r\n */\r\n public async log(data: LoggingData) {\r\n if (this.minLevel && LEVEL_RANK[data.type] < LEVEL_RANK[this.minLevel]) {\r\n return this;\r\n }\r\n\r\n // Apply the logger-wide redact floor once. Every channel sees the\r\n // result; no channel can undo a logger-wide redaction (additive-only\r\n // semantics).\r\n const baseEntry = applyRedact(data, this.redactConfig);\r\n\r\n for (const channel of this.channels) {\r\n const channelRedact = channel.getRedactConfig?.();\r\n const effectiveRedact = channelRedact\r\n ? mergeRedact(this.redactConfig, channelRedact)\r\n : undefined;\r\n\r\n // When the channel adds paths, redact again from `data` rather than\r\n // from `baseEntry` so the merged config (which already contains the\r\n // logger-wide paths) does the full pass — avoids double-cloning the\r\n // already-redacted base.\r\n let payload = effectiveRedact ? applyRedact(data, effectiveRedact) : baseEntry;\r\n\r\n if (channel.terminal === false) {\r\n payload = { ...payload, message: clearMessage(payload.message) };\r\n }\r\n\r\n channel.log(payload);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Make debug log\r\n */\r\n public debug(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"debug\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make info log\r\n */\r\n public info(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"info\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make warn log\r\n */\r\n public warn(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"warn\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make error log\r\n */\r\n public error(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"error\", context);\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make success log\r\n */\r\n public success(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"success\", context);\r\n\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Make fatal log — for unrecoverable failures where the application is going\r\n * down (failed bootstrap, lost connection to a required dependency that the\r\n * caller has decided not to retry, an `uncaughtException`).\r\n *\r\n * Identical shape to {@link error}; the level is purely informational —\r\n * `fatal` does NOT auto-flush or exit. The caller decides whether to call\r\n * `await log.flush()` and `process.exit(...)`.\r\n */\r\n public fatal(\r\n dataOrModule: OmittedLoggingData | string,\r\n action?: string,\r\n message: any = \"\",\r\n context?: Record<string, any>,\r\n ) {\r\n const data = this.normalizeLogData(dataOrModule, action, message, \"fatal\", context);\r\n\r\n return this.log(data);\r\n }\r\n\r\n /**\r\n * Log an `error` entry when `condition` is falsy. No-op otherwise — the\r\n * entry is never built and channels are not invoked, so this is genuinely\r\n * free in the happy path. Mirrors the spirit of `console.assert` but routes\r\n * through the logger pipeline so persistent channels capture failures.\r\n *\r\n * @example\r\n * log.assert(user !== null, \"auth\", \"session\", \"user vanished mid-flight\", { sessionId });\r\n */\r\n public assert(\r\n condition: unknown,\r\n module: string,\r\n action: string,\r\n message: any,\r\n context?: Record<string, any>,\r\n ): Promise<Logger> | Logger {\r\n if (condition) return this;\r\n return this.error(module, action, message, context);\r\n }\r\n\r\n /**\r\n * Start a duration timer. The returned function emits an `info` entry\r\n * with `completed in <ms>ms` and a `durationMs` field in `context` when\r\n * called. Pass an object to `end()` to merge extra fields into context.\r\n *\r\n * @example\r\n * const end = log.timer(\"db\", \"users.findById\");\r\n * const user = await usersRepo.findById(id);\r\n * end({ id, found: !!user });\r\n */\r\n public timer(\r\n module: string,\r\n action: string,\r\n ): (extra?: Record<string, any>) => Promise<Logger> {\r\n const startedAt = Date.now();\r\n return (extra?: Record<string, any>) => {\r\n const durationMs = Date.now() - startedAt;\r\n return this.info(module, action, `completed in ${durationMs}ms`, {\r\n durationMs,\r\n ...(extra ?? {}),\r\n });\r\n };\r\n }\r\n\r\n /**\r\n * Get channel by name\r\n */\r\n public channel(name: string) {\r\n return this.channels.find((channel) => channel.name === name);\r\n }\r\n\r\n /**\r\n * Synchronously flush logs\r\n */\r\n public flushSync() {\r\n for (const channel of this.channels) {\r\n if (channel.flushSync) {\r\n channel.flushSync();\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Asynchronously drain every channel that implements `flush()`.\r\n *\r\n * Unlike {@link flushSync}, this awaits each channel's async I/O — the\r\n * correct call for a graceful shutdown that can afford to wait\r\n * (`await log.flush()` after closing the HTTP server, before\r\n * `process.exit`). A channel whose delivery is async (a network transport,\r\n * an async disk write) implements `flush()`, not `flushSync()`.\r\n *\r\n * Channels are isolated: a channel whose flush rejects can neither prevent\r\n * the others from draining nor escape as an unhandled rejection. Channels\r\n * without `flush()` are skipped.\r\n *\r\n * @example\r\n * async function shutdown() {\r\n * await httpServer.close();\r\n * await log.flush();\r\n * process.exit(0);\r\n * }\r\n */\r\n public async flush(): Promise<void> {\r\n await Promise.allSettled(\r\n this.channels.map(async (channel) => {\r\n if (!channel.flush) {\r\n return;\r\n }\r\n\r\n try {\r\n await channel.flush();\r\n } catch {\r\n // A single channel must never break shutdown for the others —\r\n // a graceful drain is best-effort across every channel.\r\n }\r\n }),\r\n );\r\n }\r\n\r\n /**\r\n * Register one process-level handler per event that calls `flushSync()`\r\n * before the process terminates.\r\n *\r\n * For signal events (`SIGINT`, `SIGTERM`, `SIGHUP`, `SIGBREAK`, `SIGUSR2`)\r\n * the handler flushes and then re-raises the signal so Node's default exit\r\n * behavior runs. For `beforeExit`, the handler flushes in place — Node exits\r\n * naturally afterwards.\r\n *\r\n * Idempotent: calling with the same events replaces the previous handlers.\r\n * Call `disableAutoFlush()` to unregister.\r\n *\r\n * @example\r\n * log.configure({\r\n * channels: [new ConsoleLog(), new FileLog()],\r\n * autoFlushOn: [\"SIGINT\", \"SIGTERM\", \"beforeExit\"],\r\n * });\r\n */\r\n public enableAutoFlush(events: AutoFlushEvent[]): this {\r\n this.disableAutoFlush();\r\n\r\n for (const event of events) {\r\n const handler = SIGNAL_EVENTS.has(event)\r\n ? () => {\r\n this.flushSync();\r\n process.off(event, handler);\r\n process.kill(process.pid, event as NodeJS.Signals);\r\n }\r\n : () => {\r\n this.flushSync();\r\n };\r\n\r\n process.on(event, handler);\r\n this.autoFlushHandlers.set(event, handler);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Remove every handler previously registered by `enableAutoFlush`.\r\n * Safe to call when no handlers are registered.\r\n */\r\n public disableAutoFlush(): this {\r\n for (const [event, handler] of this.autoFlushHandlers) {\r\n process.off(event, handler);\r\n }\r\n\r\n this.autoFlushHandlers.clear();\r\n\r\n return this;\r\n }\r\n}\r\n\r\n/**\r\n * The package singleton. Use this for everyday logging — `log.info(...)`,\r\n * `log.error(...)`, `log.configure(...)`. Custom logger instances can be\r\n * created by instantiating `Logger` directly.\r\n *\r\n * The name is intentionally short: `log` reads naturally at the call site\r\n * (`log.info(\"auth\", \"login\", \"ok\")`) and matches the convention used in\r\n * pino, bunyan, and most JS logging tutorials.\r\n *\r\n * Note that `log` is a `Logger` instance, **not** a function — the bare\r\n * callable form was removed when the dual `log` / `logger` exports were\r\n * collapsed into a single name. Use `log.info(...)` (or any other level\r\n * shortcut) to emit entries.\r\n */\r\nexport const log = new Logger();\r\n"],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ //#region ../logger/src/redact/default-keys.d.ts
2
+ /**
3
+ * Built-in secret-key denylist — the redaction floor that applies with no
4
+ * configuration at all.
5
+ *
6
+ * ## Why a default list exists
7
+ *
8
+ * Path-based `redact.paths` is a *tool*; it only protects an application whose
9
+ * every call site was configured correctly, and nothing warns you when one
10
+ * wasn't. The overwhelmingly common leak is mundane and needs no attacker
11
+ * input: `log.error("auth", "login", "failed", { headers: req.headers, body:
12
+ * req.body })` puts a live bearer token and a cleartext password into every
13
+ * sink. A key denylist that runs by default turns redaction from a tool into a
14
+ * default protection.
15
+ *
16
+ * ## Matching semantics — exact, on a normalized key
17
+ *
18
+ * Keys are compared after {@link normalizeRedactKey}: lower-cased with every
19
+ * non-alphanumeric character removed. So one entry covers every spelling
20
+ * convention a codebase might use —
21
+ * `apiKey` / `api_key` / `API-KEY` / `apikey` all normalize to `apikey`.
22
+ *
23
+ * The match is **exact on the normalized key, not a substring**. That is a
24
+ * deliberate trade: substring matching (`*token*`) would also swallow
25
+ * `tokenCount`, `passwordUpdatedAt`, `secretsLoaded` — silent, hard-to-debug
26
+ * data loss in the one place engineers look when something is broken. An exact
27
+ * list is auditable: you can read it and know precisely what disappears. The
28
+ * cost is that unusual spellings are missed, which is why the list carries the
29
+ * concrete wire-format variants that actually show up in HTTP headers
30
+ * (`x-api-key`, `proxy-authorization`, `set-cookie`) and why apps can extend it
31
+ * with `redact.keys`.
32
+ *
33
+ * ## What is deliberately NOT here
34
+ *
35
+ * `key`, `auth`, `hash`, `signature`, `salt`, `id` — all too generic. Each
36
+ * would redact far more non-secret fields than secret ones (`key` alone would
37
+ * blank out every `key` in a keyed collection). Applications that want them
38
+ * add them via `redact.keys`.
39
+ */
40
+ /**
41
+ * Normalize a key for denylist comparison: lower-case, strip every
42
+ * non-alphanumeric character. Collapses `apiKey`, `api_key`, `API-KEY`,
43
+ * `Api Key` and `apikey` onto a single entry.
44
+ */
45
+ declare function normalizeRedactKey(key: string): string;
46
+ /**
47
+ * The default denylist, in source spelling. Anything whose normalized key
48
+ * matches one of these is replaced by the configured censor (`"[REDACTED]"`
49
+ * by default) at any depth of `context`, `message`, or an `Error`'s own
50
+ * enumerable properties.
51
+ *
52
+ * Exported so applications can inspect, log, or build on the exact set they
53
+ * are getting rather than guessing at it.
54
+ */
55
+ declare const DEFAULT_REDACT_KEYS: readonly string[];
56
+ /**
57
+ * The default denylist, pre-normalized. Frozen at module load so the hot path
58
+ * never rebuilds it.
59
+ */
60
+ declare const DEFAULT_REDACT_KEY_SET: ReadonlySet<string>;
61
+ //#endregion
62
+ export { DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, normalizeRedactKey };
63
+ //# sourceMappingURL=default-keys.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-keys.d.mts","names":[],"sources":["../../../../../../../logger/src/redact/default-keys.ts"],"mappings":";;AA4CA;;;;AAA8C;AAa9C;;;;AAqEC;AAMD;;;;AAAgD;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAxFhC,kBAAA,CAAmB,GAAW;;;;;;;;;;cAajC,mBAAA;;;;;cA2EA,sBAAA,EAAwB,WAAW"}
@@ -0,0 +1,116 @@
1
+ //#region ../logger/src/redact/default-keys.ts
2
+ /**
3
+ * Built-in secret-key denylist — the redaction floor that applies with no
4
+ * configuration at all.
5
+ *
6
+ * ## Why a default list exists
7
+ *
8
+ * Path-based `redact.paths` is a *tool*; it only protects an application whose
9
+ * every call site was configured correctly, and nothing warns you when one
10
+ * wasn't. The overwhelmingly common leak is mundane and needs no attacker
11
+ * input: `log.error("auth", "login", "failed", { headers: req.headers, body:
12
+ * req.body })` puts a live bearer token and a cleartext password into every
13
+ * sink. A key denylist that runs by default turns redaction from a tool into a
14
+ * default protection.
15
+ *
16
+ * ## Matching semantics — exact, on a normalized key
17
+ *
18
+ * Keys are compared after {@link normalizeRedactKey}: lower-cased with every
19
+ * non-alphanumeric character removed. So one entry covers every spelling
20
+ * convention a codebase might use —
21
+ * `apiKey` / `api_key` / `API-KEY` / `apikey` all normalize to `apikey`.
22
+ *
23
+ * The match is **exact on the normalized key, not a substring**. That is a
24
+ * deliberate trade: substring matching (`*token*`) would also swallow
25
+ * `tokenCount`, `passwordUpdatedAt`, `secretsLoaded` — silent, hard-to-debug
26
+ * data loss in the one place engineers look when something is broken. An exact
27
+ * list is auditable: you can read it and know precisely what disappears. The
28
+ * cost is that unusual spellings are missed, which is why the list carries the
29
+ * concrete wire-format variants that actually show up in HTTP headers
30
+ * (`x-api-key`, `proxy-authorization`, `set-cookie`) and why apps can extend it
31
+ * with `redact.keys`.
32
+ *
33
+ * ## What is deliberately NOT here
34
+ *
35
+ * `key`, `auth`, `hash`, `signature`, `salt`, `id` — all too generic. Each
36
+ * would redact far more non-secret fields than secret ones (`key` alone would
37
+ * blank out every `key` in a keyed collection). Applications that want them
38
+ * add them via `redact.keys`.
39
+ */
40
+ /**
41
+ * Normalize a key for denylist comparison: lower-case, strip every
42
+ * non-alphanumeric character. Collapses `apiKey`, `api_key`, `API-KEY`,
43
+ * `Api Key` and `apikey` onto a single entry.
44
+ */
45
+ function normalizeRedactKey(key) {
46
+ return key.toLowerCase().replace(/[^a-z0-9]/g, "");
47
+ }
48
+ /**
49
+ * The default denylist, in source spelling. Anything whose normalized key
50
+ * matches one of these is replaced by the configured censor (`"[REDACTED]"`
51
+ * by default) at any depth of `context`, `message`, or an `Error`'s own
52
+ * enumerable properties.
53
+ *
54
+ * Exported so applications can inspect, log, or build on the exact set they
55
+ * are getting rather than guessing at it.
56
+ */
57
+ const DEFAULT_REDACT_KEYS = [
58
+ "password",
59
+ "passwd",
60
+ "pwd",
61
+ "passphrase",
62
+ "passwordHash",
63
+ "currentPassword",
64
+ "newPassword",
65
+ "passwordConfirmation",
66
+ "secret",
67
+ "clientSecret",
68
+ "appSecret",
69
+ "apiSecret",
70
+ "secretKey",
71
+ "token",
72
+ "accessToken",
73
+ "refreshToken",
74
+ "idToken",
75
+ "authToken",
76
+ "apiToken",
77
+ "bearerToken",
78
+ "csrfToken",
79
+ "sessionToken",
80
+ "resetToken",
81
+ "verificationToken",
82
+ "jwt",
83
+ "otp",
84
+ "apiKey",
85
+ "privateKey",
86
+ "encryptionKey",
87
+ "signingKey",
88
+ "authorization",
89
+ "proxyAuthorization",
90
+ "x-api-key",
91
+ "x-auth-token",
92
+ "cookie",
93
+ "set-cookie",
94
+ "sessionId",
95
+ "credentials",
96
+ "credential",
97
+ "connectionString",
98
+ "creditCard",
99
+ "creditCardNumber",
100
+ "cardNumber",
101
+ "cvv",
102
+ "cvc",
103
+ "ssn",
104
+ "socialSecurityNumber",
105
+ "iban",
106
+ "taxId"
107
+ ];
108
+ /**
109
+ * The default denylist, pre-normalized. Frozen at module load so the hot path
110
+ * never rebuilds it.
111
+ */
112
+ const DEFAULT_REDACT_KEY_SET = new Set(DEFAULT_REDACT_KEYS.map(normalizeRedactKey));
113
+
114
+ //#endregion
115
+ export { DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, normalizeRedactKey };
116
+ //# sourceMappingURL=default-keys.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-keys.mjs","names":[],"sources":["../../../../../../../logger/src/redact/default-keys.ts"],"sourcesContent":["/**\n * Built-in secret-key denylist — the redaction floor that applies with no\n * configuration at all.\n *\n * ## Why a default list exists\n *\n * Path-based `redact.paths` is a *tool*; it only protects an application whose\n * every call site was configured correctly, and nothing warns you when one\n * wasn't. The overwhelmingly common leak is mundane and needs no attacker\n * input: `log.error(\"auth\", \"login\", \"failed\", { headers: req.headers, body:\n * req.body })` puts a live bearer token and a cleartext password into every\n * sink. A key denylist that runs by default turns redaction from a tool into a\n * default protection.\n *\n * ## Matching semantics — exact, on a normalized key\n *\n * Keys are compared after {@link normalizeRedactKey}: lower-cased with every\n * non-alphanumeric character removed. So one entry covers every spelling\n * convention a codebase might use —\n * `apiKey` / `api_key` / `API-KEY` / `apikey` all normalize to `apikey`.\n *\n * The match is **exact on the normalized key, not a substring**. That is a\n * deliberate trade: substring matching (`*token*`) would also swallow\n * `tokenCount`, `passwordUpdatedAt`, `secretsLoaded` — silent, hard-to-debug\n * data loss in the one place engineers look when something is broken. An exact\n * list is auditable: you can read it and know precisely what disappears. The\n * cost is that unusual spellings are missed, which is why the list carries the\n * concrete wire-format variants that actually show up in HTTP headers\n * (`x-api-key`, `proxy-authorization`, `set-cookie`) and why apps can extend it\n * with `redact.keys`.\n *\n * ## What is deliberately NOT here\n *\n * `key`, `auth`, `hash`, `signature`, `salt`, `id` — all too generic. Each\n * would redact far more non-secret fields than secret ones (`key` alone would\n * blank out every `key` in a keyed collection). Applications that want them\n * add them via `redact.keys`.\n */\n\n/**\n * Normalize a key for denylist comparison: lower-case, strip every\n * non-alphanumeric character. Collapses `apiKey`, `api_key`, `API-KEY`,\n * `Api Key` and `apikey` onto a single entry.\n */\nexport function normalizeRedactKey(key: string): string {\n return key.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n}\n\n/**\n * The default denylist, in source spelling. Anything whose normalized key\n * matches one of these is replaced by the configured censor (`\"[REDACTED]\"`\n * by default) at any depth of `context`, `message`, or an `Error`'s own\n * enumerable properties.\n *\n * Exported so applications can inspect, log, or build on the exact set they\n * are getting rather than guessing at it.\n */\nexport const DEFAULT_REDACT_KEYS: readonly string[] = [\n // --- Passwords ---------------------------------------------------------\n \"password\",\n \"passwd\",\n \"pwd\",\n \"passphrase\",\n \"passwordHash\",\n \"currentPassword\",\n \"newPassword\",\n \"passwordConfirmation\",\n\n // --- Shared secrets ----------------------------------------------------\n \"secret\",\n \"clientSecret\",\n \"appSecret\",\n \"apiSecret\",\n \"secretKey\",\n\n // --- Tokens ------------------------------------------------------------\n \"token\",\n \"accessToken\",\n \"refreshToken\",\n \"idToken\",\n \"authToken\",\n \"apiToken\",\n \"bearerToken\",\n \"csrfToken\",\n \"sessionToken\",\n \"resetToken\",\n \"verificationToken\",\n \"jwt\",\n \"otp\",\n\n // --- Keys --------------------------------------------------------------\n \"apiKey\",\n \"privateKey\",\n \"encryptionKey\",\n \"signingKey\",\n\n // --- HTTP credential headers -------------------------------------------\n // Header names are matched in their wire spelling; normalization removes\n // the dashes, so `x-api-key` and `X_API_KEY` both land on `xapikey`.\n \"authorization\",\n \"proxyAuthorization\",\n \"x-api-key\",\n \"x-auth-token\",\n \"cookie\",\n \"set-cookie\",\n\n // --- Session / credential containers -----------------------------------\n // `sessionId` is a bearer credential: whoever reads it from a log can\n // replay the session. Teams that log it deliberately for correlation can\n // drop it via `redact.keys` + `defaultKeys: false`, or keep a prefix with a\n // function censor.\n \"sessionId\",\n \"credentials\",\n \"credential\",\n \"connectionString\",\n\n // --- Financial / high-sensitivity PII ----------------------------------\n \"creditCard\",\n \"creditCardNumber\",\n \"cardNumber\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n \"socialSecurityNumber\",\n \"iban\",\n \"taxId\",\n];\n\n/**\n * The default denylist, pre-normalized. Frozen at module load so the hot path\n * never rebuilds it.\n */\nexport const DEFAULT_REDACT_KEY_SET: ReadonlySet<string> = new Set(\n DEFAULT_REDACT_KEYS.map(normalizeRedactKey),\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAgB,mBAAmB,KAAqB;CACtD,OAAO,IAAI,YAAY,CAAC,CAAC,QAAQ,cAAc,EAAE;AACnD;;;;;;;;;;AAWA,MAAa,sBAAyC;CAEpD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CAKA;CACA;CACA;CACA;CACA;CACA;CAOA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;AAMA,MAAa,yBAA8C,IAAI,IAC7D,oBAAoB,IAAI,kBAAkB,CAC5C"}
@@ -0,0 +1,4 @@
1
+ import { DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, normalizeRedactKey } from "./default-keys.mjs";
2
+ import { applyRedact, mergeRedact, resolveRedactKeys } from "./redact.mjs";
3
+
4
+ export { };
@@ -2,24 +2,57 @@ import { LoggingData, RedactConfig } from "../types.mjs";
2
2
 
3
3
  //#region ../logger/src/redact/redact.d.ts
4
4
  /**
5
- * Produce a new `LoggingData` with every path in `config.paths` replaced
6
- * by `config.censor`. The original entry is never mutated — channels and
7
- * other call sites can hold references to the input safely.
5
+ * Resolve the effective key denylist for a config: the built-in set (unless
6
+ * `defaultKeys: false`) plus any `keys` the application added.
8
7
  *
9
- * No-op (returns the input by reference) when `config` is `undefined` or
10
- * its `paths` array is empty, so the fast path stays fast.
8
+ * Returns `undefined` only when there is nothing to match i.e. defaults are
9
+ * explicitly off and no custom keys were supplied.
10
+ */
11
+ declare function resolveRedactKeys(config: RedactConfig | undefined): ReadonlySet<string> | undefined;
12
+ /**
13
+ * Produce a new `LoggingData` with sensitive data censored:
14
+ *
15
+ * 1. every path in `config.paths` (opt-in globs), then
16
+ * 2. every key matching the denylist — the built-in
17
+ * {@link DEFAULT_REDACT_KEYS} plus `config.keys`, at any depth of
18
+ * `context`, `message`, and an `Error`'s own enumerable properties.
19
+ *
20
+ * Step 2 runs **with no config at all**: passing `undefined` still censors
21
+ * `password`, `authorization`, `token`, `apiKey` and friends. Pass
22
+ * `{ defaultKeys: false }` to opt out.
23
+ *
24
+ * Paths run first so a function censor sees the original value rather than a
25
+ * mask; leaves the path pass already censored are skipped by the key pass.
26
+ *
27
+ * The original entry is never mutated — channels and other call sites can
28
+ * hold references to the input safely. Returns the input **by reference**
29
+ * when nothing matched, so the fast path stays allocation-free.
30
+ *
31
+ * ## Residual gaps (by design, documented rather than silently absent)
32
+ *
33
+ * - **Secrets interpolated into a `message` string** (`` `token=${t}` ``)
34
+ * cannot be reached — neither a path nor a key names a substring.
35
+ * - **`Map`/`Set`/`Buffer` contents** are not traversed (see {@link isOpaque}).
36
+ * - **Non-enumerable / getter-backed properties** are not walked, so an HTTP
37
+ * client that exposes request config behind a getter still slips through.
38
+ * Enumerable ones (axios's `.config`, `.response`) *are* covered.
11
39
  */
12
40
  declare function applyRedact(data: LoggingData, config: RedactConfig | undefined): LoggingData;
13
41
  /**
14
42
  * Combine two redact configs into one effective config. Used to merge a
15
43
  * channel's additive paths on top of the logger-wide floor.
16
44
  *
17
- * - `paths` are concatenated; duplicates are kept (the matcher tolerates
18
- * them, and de-duping cross-config would mask a developer typo).
45
+ * - `paths` and `keys` are concatenated; duplicates are kept (the matcher
46
+ * tolerates them, and de-duping cross-config would mask a developer typo).
19
47
  * - `censor` from the channel wins; falls back to the logger's; falls back
20
48
  * to the default `"[REDACTED]"`.
49
+ * - `defaultKeys` follows the additive-only contract: a channel can turn the
50
+ * built-in denylist back *on* (`true`) but can never turn off one the
51
+ * logger-wide floor left enabled. When the channel is silent, the logger's
52
+ * choice is inherited — opting out logger-wide is not quietly undone by
53
+ * any channel that happens to set a `redact` option.
21
54
  */
22
55
  declare function mergeRedact(base: RedactConfig | undefined, extra: RedactConfig | undefined): RedactConfig | undefined;
23
56
  //#endregion
24
- export { applyRedact, mergeRedact };
57
+ export { applyRedact, mergeRedact, resolveRedactKeys };
25
58
  //# sourceMappingURL=redact.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"redact.d.mts","names":[],"sources":["../../../../../../../logger/src/redact/redact.ts"],"mappings":";;;;;AAyIA;;;;;;iBAAgB,WAAA,CACd,IAAA,EAAM,WAAA,EACN,MAAA,EAAQ,YAAA,eACP,WAAA;;;;;;;;AAAW;AA0Bd;iBAAgB,WAAA,CACd,IAAA,EAAM,YAAA,cACN,KAAA,EAAO,YAAA,eACN,YAAA"}
1
+ {"version":3,"file":"redact.d.mts","names":[],"sources":["../../../../../../../logger/src/redact/redact.ts"],"mappings":";;;;;AAmUA;;;;;iBAAgB,iBAAA,CACd,MAAA,EAAQ,YAAA,eACP,WAAW;;;AAAA;AAiDd;;;;;;;;;;;;;;AAGc;AAgDd;;;;;;;;;;iBAnDgB,WAAA,CACd,IAAA,EAAM,WAAA,EACN,MAAA,EAAQ,YAAA,eACP,WAAA;;;;AAmDY;;;;;;;;;;;iBAHC,WAAA,CACd,IAAA,EAAM,YAAA,cACN,KAAA,EAAO,YAAA,eACN,YAAA"}