@warlock.js/logger 5.32.0 → 5.34.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.
- package/CHANGELOG.md +8 -103
- package/cjs/index.cjs +230 -125
- package/cjs/index.cjs.map +1 -1
- package/esm/index.d.mts +2 -1
- package/esm/index.mjs +3 -2
- package/esm/log-channel.d.mts.map +1 -1
- package/esm/log-channel.mjs +3 -1
- package/esm/log-channel.mjs.map +1 -1
- package/esm/logger.d.mts.map +1 -1
- package/esm/logger.mjs +3 -2
- package/esm/logger.mjs.map +1 -1
- package/esm/redact/index.mjs +2 -1
- package/esm/redact/redact.d.mts +8 -6
- package/esm/redact/redact.d.mts.map +1 -1
- package/esm/redact/redact.mjs +11 -8
- package/esm/redact/redact.mjs.map +1 -1
- package/esm/redact/registered-secrets.d.mts +11 -0
- package/esm/redact/registered-secrets.d.mts.map +1 -0
- package/esm/redact/registered-secrets.mjs +105 -0
- package/esm/redact/registered-secrets.mjs.map +1 -0
- package/llms-full.txt +17 -2
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/redact-sensitive-log-fields/SKILL.md +17 -2
package/esm/index.d.mts
CHANGED
|
@@ -10,7 +10,8 @@ import { DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, normalizeRedactKey } from
|
|
|
10
10
|
import { applyRedact, mergeRedact, resolveRedactKeys } from "./redact/redact.mjs";
|
|
11
11
|
import { EscapeLogValueOptions, escapeLogValue } from "./redact/escape-log-value.mjs";
|
|
12
12
|
import { MASK_EMAILS_MAX_LENGTH, maskEmail, maskEmailsInText } from "./redact/mask-email.mjs";
|
|
13
|
+
import { maskRegisteredLogSecrets, registerLogSecret } from "./redact/registered-secrets.mjs";
|
|
13
14
|
import { CaptureUnhandledOptions, captureAnyUnhandledRejection } from "./utils/capture-unhandled-errors.mjs";
|
|
14
15
|
import { clearMessage } from "./utils/clear-message.mjs";
|
|
15
16
|
import { safeJsonStringify } from "./utils/safe-json-stringify.mjs";
|
|
16
|
-
export { AutoFlushEvent, BasicLogConfigurations, CaptureUnhandledOptions, ChannelErrorHandler, ChannelErrorReporter, ConsoleLog, ConsoleLogConfig, DAILY_FILE_NAME, DEFAULT_FLUSH_TIMEOUT_MS, DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, DebugMode, type EscapeLogValueOptions, FileLog, FileLogConfig, FileLogRetention, type FileNameOption, HOURLY_FILE_NAME, JSONFileLog, LogChannel, LogContract, LogLevel, LogMessage, Logger, LoggingData, MASK_EMAILS_MAX_LENGTH, OmittedLoggingData, ROTATE_FILE_NAME, RedactCensor, RedactConfig, SentryBreadcrumb, SentryForwarder, SentryInitOptions, SentryLog, SentryLogConfig, SentryScopeLike, applyRedact, captureAnyUnhandledRejection, clearMessage, escapeLogValue, log, maskEmail, maskEmailsInText, mergeRedact, normalizeRedactKey, resolveRedactKeys, safeJsonStringify };
|
|
17
|
+
export { AutoFlushEvent, BasicLogConfigurations, CaptureUnhandledOptions, ChannelErrorHandler, ChannelErrorReporter, ConsoleLog, ConsoleLogConfig, DAILY_FILE_NAME, DEFAULT_FLUSH_TIMEOUT_MS, DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, DebugMode, type EscapeLogValueOptions, FileLog, FileLogConfig, FileLogRetention, type FileNameOption, HOURLY_FILE_NAME, JSONFileLog, LogChannel, LogContract, LogLevel, LogMessage, Logger, LoggingData, MASK_EMAILS_MAX_LENGTH, OmittedLoggingData, ROTATE_FILE_NAME, RedactCensor, RedactConfig, SentryBreadcrumb, SentryForwarder, SentryInitOptions, SentryLog, SentryLogConfig, SentryScopeLike, applyRedact, captureAnyUnhandledRejection, clearMessage, escapeLogValue, log, maskEmail, maskEmailsInText, maskRegisteredLogSecrets, mergeRedact, normalizeRedactKey, registerLogSecret, resolveRedactKeys, safeJsonStringify };
|
package/esm/index.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, normalizeRedactKey } from "./redact/default-keys.mjs";
|
|
2
|
+
import { maskRegisteredLogSecrets, registerLogSecret } from "./redact/registered-secrets.mjs";
|
|
1
3
|
import { LogChannel } from "./log-channel.mjs";
|
|
2
4
|
import { ConsoleLog } from "./channels/console-log.mjs";
|
|
3
5
|
import { DAILY_FILE_NAME, FileLog, HOURLY_FILE_NAME, ROTATE_FILE_NAME } from "./channels/file-log.mjs";
|
|
@@ -5,7 +7,6 @@ import { safeJsonStringify } from "./utils/safe-json-stringify.mjs";
|
|
|
5
7
|
import { JSONFileLog } from "./channels/json-file-log.mjs";
|
|
6
8
|
import { SentryLog } from "./channels/sentry-log.mjs";
|
|
7
9
|
import "./channels/index.mjs";
|
|
8
|
-
import { DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, normalizeRedactKey } from "./redact/default-keys.mjs";
|
|
9
10
|
import { applyRedact, mergeRedact, resolveRedactKeys } from "./redact/redact.mjs";
|
|
10
11
|
import { escapeLogValue } from "./redact/escape-log-value.mjs";
|
|
11
12
|
import { MASK_EMAILS_MAX_LENGTH, maskEmail, maskEmailsInText } from "./redact/mask-email.mjs";
|
|
@@ -15,4 +16,4 @@ import { DEFAULT_FLUSH_TIMEOUT_MS, Logger, log } from "./logger.mjs";
|
|
|
15
16
|
import { captureAnyUnhandledRejection } from "./utils/capture-unhandled-errors.mjs";
|
|
16
17
|
import "./utils/index.mjs";
|
|
17
18
|
|
|
18
|
-
export { ConsoleLog, DAILY_FILE_NAME, DEFAULT_FLUSH_TIMEOUT_MS, DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, FileLog, HOURLY_FILE_NAME, JSONFileLog, LogChannel, Logger, MASK_EMAILS_MAX_LENGTH, ROTATE_FILE_NAME, SentryLog, applyRedact, captureAnyUnhandledRejection, clearMessage, escapeLogValue, log, maskEmail, maskEmailsInText, mergeRedact, normalizeRedactKey, resolveRedactKeys, safeJsonStringify };
|
|
19
|
+
export { ConsoleLog, DAILY_FILE_NAME, DEFAULT_FLUSH_TIMEOUT_MS, DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, FileLog, HOURLY_FILE_NAME, JSONFileLog, LogChannel, Logger, MASK_EMAILS_MAX_LENGTH, ROTATE_FILE_NAME, SentryLog, applyRedact, captureAnyUnhandledRejection, clearMessage, escapeLogValue, log, maskEmail, maskEmailsInText, maskRegisteredLogSecrets, mergeRedact, normalizeRedactKey, registerLogSecret, resolveRedactKeys, safeJsonStringify };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-channel.d.mts","names":[],"sources":["../../../../../../../packages/logger/src/log-channel.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"log-channel.d.mts","names":[],"sources":["../../../../../../../packages/logger/src/log-channel.ts"],"mappings":";;;;;;;YAQY,wBAAwB,gBAAgB,OAAO;8BAErC,WACpB,gBAAgB,yBAAyB,mCAC9B;;;;EAIJ;;;;EAKA;;;;EAKA;;;;YAKG,uBAAuB;;;;YAKvB,uBAAuB;;;;YAKvB;;;;;YAMA,gBAAgB;;;;EAK1B,YAAmB,iBAAiB;;;;;;EAmB7B,iBAAiB,UAAU;;;;;YAQxB,YAAY,gBAAgB,OAAO;;;;YAmBnC,gBAAgB;;;;YAKhB,OAAO,gBAAgB,SAAS,KAAK,IAAI,QAAQ;;;;YAOjD,kBAAkB,gBAAgB;;;;YAYlC,eAAe,MAAM;;;;WAkBf,IAAI,MAAM,qBAAqB;;;;EAKxC;;;;;;;;EASA,iBAAiB;;;;;;;EAQjB,mBAAmB;;;;YAOhB;;;;;;;YAWA,wBAAwB,gBAAgB,QAAQ,WAAW"}
|
package/esm/log-channel.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { maskRegisteredLogSecrets } from "./redact/registered-secrets.mjs";
|
|
2
|
+
|
|
1
3
|
//#region ../../packages/logger/src/log-channel.ts
|
|
2
4
|
var LogChannel = class {
|
|
3
5
|
/**
|
|
@@ -33,7 +35,7 @@ var LogChannel = class {
|
|
|
33
35
|
return;
|
|
34
36
|
}
|
|
35
37
|
const reason = error instanceof Error ? error.message : String(error);
|
|
36
|
-
process.stderr.write(`[@warlock.js/logger] channel "${this.name}" failed: ${reason}\n`);
|
|
38
|
+
process.stderr.write(maskRegisteredLogSecrets(`[@warlock.js/logger] channel "${this.name}" failed: ${reason}\n`));
|
|
37
39
|
} catch {}
|
|
38
40
|
}
|
|
39
41
|
/**
|
package/esm/log-channel.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-channel.mjs","names":[],"sources":["../../../../../../../packages/logger/src/log-channel.ts"],"sourcesContent":["import type { BasicLogConfigurations, LogContract, LoggingData, RedactConfig } from \"./types\";\n\n/**\n * Receives a failure the channel hits outside a `log()` call (for example\n * `FileLog` retention). The logger installs one that forwards to its\n * `onChannelError` handler.\n */\nexport type ChannelErrorReporter = (error: unknown, entry: LoggingData) => void;\n\nexport abstract class LogChannel<\n Options extends BasicLogConfigurations = BasicLogConfigurations,\n> implements LogContract {\n /**\n * Channel name\n */\n public name!: string;\n\n /**\n * Channel description\n */\n public description?: string;\n\n /**\n * Determine if channel is logging in terminal\n */\n public terminal = false;\n\n /**\n * Default Configurations\n */\n protected defaultConfigurations: Options = {} as Options;\n\n /**\n * Channel configurations\n */\n protected channelConfigurations: Options = {} as Options; //\n\n /**\n * Determine whether the channel is fully initialized\n */\n protected isInitialized = false;\n\n /**\n * Where {@link reportError} sends failures; set by the logger the channel\n * is added to.\n */\n protected errorReporter?: ChannelErrorReporter;\n\n /**\n * Constructor\n */\n public constructor(configurations?: Options) {\n if (configurations) {\n this.setConfigurations(configurations);\n }\n\n setTimeout(async () => {\n if (this.init) {\n await this.init();\n }\n\n this.isInitialized = true;\n }, 0);\n }\n\n /**\n * Route this channel's own failures (those outside a `log()` call) to the\n * given reporter. `Logger` calls this when the channel is added, so the\n * failures reach `log.onChannelError(...)`.\n */\n public setErrorReporter(reporter: ChannelErrorReporter | undefined): void {\n this.errorReporter = reporter;\n }\n\n /**\n * Report a failure that happened outside a `log()` call. Never throws:\n * without a reporter it writes one line to stderr.\n */\n protected reportError(error: unknown, entry: LoggingData): void {\n try {\n if (this.errorReporter) {\n this.errorReporter(error, entry);\n return;\n }\n\n const reason = error instanceof Error ? error.message : String(error);\n process.stderr.write(`[@warlock.js/logger] channel \"${this.name}\" failed: ${reason}\\n`);\n } catch {\n // Reporting a failure must never become one.\n }\n }\n\n /**\n * Initialize the channel\n */\n protected init?(): void | Promise<void>;\n\n /**\n * Get config value\n */\n protected config<K extends keyof Options>(key: K): Options[K] {\n return this.channelConfigurations[key] ?? (this.defaultConfigurations ?? {})[key];\n }\n\n /**\n * Set configurations\n */\n protected setConfigurations(configurations: Options) {\n this.channelConfigurations = {\n ...this.channelConfigurations,\n ...configurations,\n };\n\n return this;\n }\n\n /**\n * Determine if the message should be logged\n */\n protected shouldBeLogged(data: LoggingData): boolean {\n // check for debug mode\n const allowedLevels = this.config(\"levels\");\n\n if (allowedLevels?.length && !allowedLevels.includes(data.type)) return false;\n\n const filter = this.config(\"filter\");\n\n if (filter) {\n return filter(data);\n }\n\n return true;\n }\n\n /**\n * Log the given message\n */\n public abstract log(data: LoggingData): void | Promise<void>;\n\n /**\n * Synchronously flush messages\n */\n public flushSync?(): void;\n\n /**\n * Asynchronously flush buffered messages.\n *\n * Optional — channels that write synchronously per entry (e.g. `ConsoleLog`)\n * don't implement it. Buffering channels override this to drain their buffer\n * over async I/O so callers can `await log.flush()` on graceful shutdown.\n */\n public flush?(): void | Promise<void>;\n\n /**\n * Read the channel's redact config (if any). Used by `Logger` to apply\n * per-channel additive redaction on top of the logger-wide floor.\n * Subclasses normally don't override this — set `redact` in your channel\n * configuration instead.\n */\n public getRedactConfig(): RedactConfig | undefined {\n return this.config(\"redact\" as keyof Options) as RedactConfig | undefined;\n }\n\n /**\n * Get date and time formats\n */\n protected getDateAndTimeFormat() {\n const dateFormat = this.config(\"dateFormat\");\n const date = dateFormat?.date ?? \"DD-MM-YYYY\";\n const time = dateFormat?.time ?? \"HH:mm:ss\";\n\n return { date, time };\n }\n\n /**\n * get basic configurations with the given ones\n */\n protected withBasicConfigurations(configurations: Partial<Options>): Options {\n return {\n filter: () => true,\n ...configurations,\n } as any as Options;\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"log-channel.mjs","names":[],"sources":["../../../../../../../packages/logger/src/log-channel.ts"],"sourcesContent":["import { maskRegisteredLogSecrets } from \"./redact/registered-secrets\";\nimport type { BasicLogConfigurations, LogContract, LoggingData, RedactConfig } from \"./types\";\n\n/**\n * Receives a failure the channel hits outside a `log()` call (for example\n * `FileLog` retention). The logger installs one that forwards to its\n * `onChannelError` handler.\n */\nexport type ChannelErrorReporter = (error: unknown, entry: LoggingData) => void;\n\nexport abstract class LogChannel<\n Options extends BasicLogConfigurations = BasicLogConfigurations,\n> implements LogContract {\n /**\n * Channel name\n */\n public name!: string;\n\n /**\n * Channel description\n */\n public description?: string;\n\n /**\n * Determine if channel is logging in terminal\n */\n public terminal = false;\n\n /**\n * Default Configurations\n */\n protected defaultConfigurations: Options = {} as Options;\n\n /**\n * Channel configurations\n */\n protected channelConfigurations: Options = {} as Options; //\n\n /**\n * Determine whether the channel is fully initialized\n */\n protected isInitialized = false;\n\n /**\n * Where {@link reportError} sends failures; set by the logger the channel\n * is added to.\n */\n protected errorReporter?: ChannelErrorReporter;\n\n /**\n * Constructor\n */\n public constructor(configurations?: Options) {\n if (configurations) {\n this.setConfigurations(configurations);\n }\n\n setTimeout(async () => {\n if (this.init) {\n await this.init();\n }\n\n this.isInitialized = true;\n }, 0);\n }\n\n /**\n * Route this channel's own failures (those outside a `log()` call) to the\n * given reporter. `Logger` calls this when the channel is added, so the\n * failures reach `log.onChannelError(...)`.\n */\n public setErrorReporter(reporter: ChannelErrorReporter | undefined): void {\n this.errorReporter = reporter;\n }\n\n /**\n * Report a failure that happened outside a `log()` call. Never throws:\n * without a reporter it writes one line to stderr.\n */\n protected reportError(error: unknown, entry: LoggingData): void {\n try {\n if (this.errorReporter) {\n this.errorReporter(error, entry);\n return;\n }\n\n const reason = error instanceof Error ? error.message : String(error);\n process.stderr.write(\n maskRegisteredLogSecrets(`[@warlock.js/logger] channel \"${this.name}\" failed: ${reason}\\n`),\n );\n } catch {\n // Reporting a failure must never become one.\n }\n }\n\n /**\n * Initialize the channel\n */\n protected init?(): void | Promise<void>;\n\n /**\n * Get config value\n */\n protected config<K extends keyof Options>(key: K): Options[K] {\n return this.channelConfigurations[key] ?? (this.defaultConfigurations ?? {})[key];\n }\n\n /**\n * Set configurations\n */\n protected setConfigurations(configurations: Options) {\n this.channelConfigurations = {\n ...this.channelConfigurations,\n ...configurations,\n };\n\n return this;\n }\n\n /**\n * Determine if the message should be logged\n */\n protected shouldBeLogged(data: LoggingData): boolean {\n // check for debug mode\n const allowedLevels = this.config(\"levels\");\n\n if (allowedLevels?.length && !allowedLevels.includes(data.type)) return false;\n\n const filter = this.config(\"filter\");\n\n if (filter) {\n return filter(data);\n }\n\n return true;\n }\n\n /**\n * Log the given message\n */\n public abstract log(data: LoggingData): void | Promise<void>;\n\n /**\n * Synchronously flush messages\n */\n public flushSync?(): void;\n\n /**\n * Asynchronously flush buffered messages.\n *\n * Optional — channels that write synchronously per entry (e.g. `ConsoleLog`)\n * don't implement it. Buffering channels override this to drain their buffer\n * over async I/O so callers can `await log.flush()` on graceful shutdown.\n */\n public flush?(): void | Promise<void>;\n\n /**\n * Read the channel's redact config (if any). Used by `Logger` to apply\n * per-channel additive redaction on top of the logger-wide floor.\n * Subclasses normally don't override this — set `redact` in your channel\n * configuration instead.\n */\n public getRedactConfig(): RedactConfig | undefined {\n return this.config(\"redact\" as keyof Options) as RedactConfig | undefined;\n }\n\n /**\n * Get date and time formats\n */\n protected getDateAndTimeFormat() {\n const dateFormat = this.config(\"dateFormat\");\n const date = dateFormat?.date ?? \"DD-MM-YYYY\";\n const time = dateFormat?.time ?? \"HH:mm:ss\";\n\n return { date, time };\n }\n\n /**\n * get basic configurations with the given ones\n */\n protected withBasicConfigurations(configurations: Partial<Options>): Options {\n return {\n filter: () => true,\n ...configurations,\n } as any as Options;\n }\n}\n"],"mappings":";;;AAUA,IAAsB,aAAtB,MAEyB;;;;CAwCvB,AAAO,YAAY,gBAA0B;kBA1B3B;+BAKyB,CAAC;+BAKD,CAAC;uBAKlB;EAYxB,IAAI,gBACF,KAAK,kBAAkB,cAAc;EAGvC,WAAW,YAAY;GACrB,IAAI,KAAK,MACP,MAAM,KAAK,KAAK;GAGlB,KAAK,gBAAgB;EACvB,GAAG,CAAC;CACN;;;;;;CAOA,AAAO,iBAAiB,UAAkD;EACxE,KAAK,gBAAgB;CACvB;;;;;CAMA,AAAU,YAAY,OAAgB,OAA0B;EAC9D,IAAI;GACF,IAAI,KAAK,eAAe;IACtB,KAAK,cAAc,OAAO,KAAK;IAC/B;GACF;GAEA,MAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GACpE,QAAQ,OAAO,MACb,yBAAyB,iCAAiC,KAAK,KAAK,YAAY,OAAO,GAAG,CAC5F;EACF,QAAQ,CAER;CACF;;;;CAUA,AAAU,OAAgC,KAAoB;EAC5D,OAAO,KAAK,sBAAsB,SAAS,KAAK,yBAAyB,CAAC,EAAC,CAAE;CAC/E;;;;CAKA,AAAU,kBAAkB,gBAAyB;EACnD,KAAK,wBAAwB;GAC3B,GAAG,KAAK;GACR,GAAG;EACL;EAEA,OAAO;CACT;;;;CAKA,AAAU,eAAe,MAA4B;EAEnD,MAAM,gBAAgB,KAAK,OAAO,QAAQ;EAE1C,IAAI,eAAe,UAAU,CAAC,cAAc,SAAS,KAAK,IAAI,GAAG,OAAO;EAExE,MAAM,SAAS,KAAK,OAAO,QAAQ;EAEnC,IAAI,QACF,OAAO,OAAO,IAAI;EAGpB,OAAO;CACT;;;;;;;CA2BA,AAAO,kBAA4C;EACjD,OAAO,KAAK,OAAO,QAAyB;CAC9C;;;;CAKA,AAAU,uBAAuB;EAC/B,MAAM,aAAa,KAAK,OAAO,YAAY;EAI3C,OAAO;GAAE,MAHI,YAAY,QAAQ;GAGlB,MAFF,YAAY,QAAQ;EAEb;CACtB;;;;CAKA,AAAU,wBAAwB,gBAA2C;EAC3E,OAAO;GACL,cAAc;GACd,GAAG;EACL;CACF;AACF"}
|
package/esm/logger.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.mts","names":["_$__types0.BasicLogConfigurations"],"sources":["../../../../../../../packages/logger/src/logger.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"logger.d.mts","names":["_$__types0.BasicLogConfigurations"],"sources":["../../../../../../../packages/logger/src/logger.ts"],"mappings":";;;qBAuCa;;;;EAIJ,UAAU;EAEV;;;;;;UAOC;;;;;;;UAQA;;;;;;;UAQA;;;;;;;UAQA;;;;;UAMA;;;;EAKD,WAAW,SAAS;;;;EASpB,UAAU;IACf,WAAW;IACX,cAAc;IACd,WAAW;IACX,SAAS;IACT,iBAAiB;;;;;;;;;;;;EAmCZ,eAAe,SAAS;;;;;;;;;;;;EAgBxB,UAAU,QAAQ;;;;EAQlB,aAAa;;;;;;;;;;;EAcb,YAAY,OAAO;;;;EAQnB,eAAe;;;;EAOf,YAAY,UAAU;;;;;UAcrB;;;;UAWA;;;;;;;;;;;;;;;EAwCD,IAAI,MAAM;;;;;;UAkCT;UAqDA;;;;EAqBD,MACL,cAAc,6BACd,iBACA,eACA,UAAU;;;;EASL,KACL,cAAc,6BACd,iBACA,eACA,UAAU;;;;EASL,KACL,cAAc,6BACd,iBACA,eACA,UAAU;;;;EASL,MACL,cAAc,6BACd,iBACA,eACA,UAAU;;;;EASL,QACL,cAAc,6BACd,iBACA,eACA,UAAU;;;;;;;;;;EAgBL,MACL,cAAc,6BACd,iBACA,eACA,UAAU;;;;;;;;;;EAgBL,OACL,oBACA,gBACA,gBACA,cACA,UAAU;;;;;;;;;;;EAgBL,MAAM,gBAAgB,kBAAkB,QAAQ;;;;EAchD,QAAQ,eAAY,WAd4BA;;;;EAqBhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmCM,MAAM;IAAW;MAA0B;;;;;UA0D1C;;;;;;;;;;;;;;;;;;;EAiCP,gBAAgB,QAAQ;;;;;EAyBxB;;;;;;qBAeI;;;;;;;;;;;;;;;qBAgDA,KAAG"}
|
package/esm/logger.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { maskRegisteredLogSecrets, redactRegisteredSecrets } from "./redact/registered-secrets.mjs";
|
|
1
2
|
import { applyRedact, mergeRedact } from "./redact/redact.mjs";
|
|
2
3
|
import "./redact/index.mjs";
|
|
3
4
|
import { clearMessage } from "./utils/clear-message.mjs";
|
|
@@ -203,7 +204,7 @@ var Logger = class {
|
|
|
203
204
|
}
|
|
204
205
|
reportChannelError(error, channel, entry) {
|
|
205
206
|
try {
|
|
206
|
-
const outcome = this.channelErrorHandler(error, channel, entry);
|
|
207
|
+
const outcome = this.channelErrorHandler(redactRegisteredSecrets(error), channel, redactRegisteredSecrets(entry));
|
|
207
208
|
if (outcome && typeof outcome.then === "function") outcome.then(void 0, () => {});
|
|
208
209
|
} catch {}
|
|
209
210
|
}
|
|
@@ -442,7 +443,7 @@ function redactionFailedEntry(data) {
|
|
|
442
443
|
function reportChannelError(error, channel) {
|
|
443
444
|
const reason = error instanceof Error ? error.message : String(error);
|
|
444
445
|
try {
|
|
445
|
-
process.stderr.write(`[@warlock.js/logger] channel "${channel.name}" failed to log: ${reason}\n`);
|
|
446
|
+
process.stderr.write(maskRegisteredLogSecrets(`[@warlock.js/logger] channel "${channel.name}" failed to log: ${reason}\n`));
|
|
446
447
|
} catch {}
|
|
447
448
|
}
|
|
448
449
|
/**
|
package/esm/logger.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.mjs","names":[],"sources":["../../../../../../../packages/logger/src/logger.ts"],"sourcesContent":["import { Random } from \"@mongez/reinforcements\";\nimport type { LogChannel } from \"./log-channel\";\nimport { applyRedact, mergeRedact } from \"./redact\";\nimport type {\n AutoFlushEvent,\n ChannelErrorHandler,\n LogContract,\n LoggingData,\n LogLevel,\n OmittedLoggingData,\n RedactConfig,\n} from \"./types\";\nimport { clearMessage } from \"./utils/clear-message\";\n\nconst SIGNAL_EVENTS: ReadonlySet<AutoFlushEvent> = new Set([\n \"SIGINT\",\n \"SIGTERM\",\n \"SIGHUP\",\n \"SIGBREAK\",\n \"SIGUSR2\",\n]);\n\n/**\n * Severity ranks used by `setMinLevel`. Higher number = more severe. The\n * ordering matches conventional log-level hierarchies: `debug` is noisiest\n * and easiest to drop; `error` is the loudest and never dropped by the\n * minimum-level filter. `success` sits beside `info` — it's an informational\n * outcome, not a warning.\n */\nconst LEVEL_RANK: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n success: 1,\n warn: 2,\n error: 3,\n fatal: 4,\n};\n\nexport class Logger {\n /**\n * Current channel\n */\n public channels: LogChannel[] = [];\n\n public id = \"logger-\" + Random.string(32);\n\n /**\n * Registered auto-flush handlers, keyed by event name. Stored so repeated\n * calls to `enableAutoFlush` replace rather than stack, and so\n * `disableAutoFlush` can remove them cleanly.\n */\n private autoFlushHandlers = new Map<AutoFlushEvent, () => void>();\n\n /**\n * Logger-wide minimum severity. When set, entries below this level are\n * dropped before any channel is invoked — cheaper than per-channel `levels`\n * filters because the fan-out loop is skipped entirely. `undefined` means\n * no minimum (every entry reaches every channel that accepts it).\n */\n private minLevel?: LogLevel;\n\n /**\n * Logger-wide redaction floor. Applied once before fan-out — every\n * channel receives an entry with these paths already censored. Channel\n * configs can extend the path list (additive); they cannot remove paths\n * set here.\n */\n private redactConfig?: RedactConfig;\n\n /**\n * Deliveries still in flight on async channels, per channel. `log()` never\n * awaits them; `flush()` does, so a graceful shutdown still waits for every\n * entry. Kept per channel so one hung channel cannot hold up another's\n * drain.\n */\n private pendingDeliveries = new Map<LogChannel, Set<Promise<void>>>();\n\n /**\n * Receives a channel's delivery failure (a sync throw or a rejected async\n * `log()`). Defaults to a single stderr line, never back through the logger.\n */\n private channelErrorHandler: ChannelErrorHandler = reportChannelError;\n\n /**\n * Add a new channel\n */\n public addChannel(channel: LogChannel) {\n this.channels.push(this.attachChannel(channel));\n\n return this;\n }\n\n /**\n * Set base configurations\n */\n public configure(config: {\n channels?: LogChannel[];\n autoFlushOn?: AutoFlushEvent[];\n minLevel?: LogLevel;\n redact?: RedactConfig;\n onChannelError?: ChannelErrorHandler;\n }) {\n if (config.channels) {\n this.setChannels(config.channels);\n }\n\n if (config.autoFlushOn) {\n this.enableAutoFlush(config.autoFlushOn);\n }\n\n if (config.minLevel !== undefined) {\n this.setMinLevel(config.minLevel);\n }\n\n if (config.redact !== undefined) {\n this.setRedact(config.redact);\n }\n\n if (config.onChannelError) {\n this.onChannelError(config.onChannelError);\n }\n\n return this;\n }\n\n /**\n * Handle a channel's delivery failure. Logging is fire-and-forget, so a\n * channel that throws or rejects never reaches the call site and never\n * becomes an unhandled rejection; it goes to this handler instead. The\n * default writes one line to stderr. The handler must not log through this\n * logger (a failing channel would loop).\n *\n * @example\n * log.onChannelError((error, channel) => metrics.increment(`log.${channel.name}.failed`));\n */\n public onChannelError(handler: ChannelErrorHandler): this {\n this.channelErrorHandler = handler;\n return this;\n }\n\n /**\n * Set the logger-wide redaction floor. Applied to every entry before\n * fan-out; channel configs add more paths on top, never fewer. Pass\n * `undefined` to clear.\n *\n * @example\n * log.setRedact({\n * paths: [\"context.password\", \"context.*.token\"],\n * censor: \"[REDACTED]\",\n * });\n */\n public setRedact(config: RedactConfig | undefined): this {\n this.redactConfig = config;\n return this;\n }\n\n /**\n * Read the active logger-wide redact config (or `undefined`).\n */\n public getRedact(): RedactConfig | undefined {\n return this.redactConfig;\n }\n\n /**\n * Drop every entry whose severity is below `level` before fan-out. Cheaper\n * than per-channel `levels` filters because the loop never runs and no\n * channel receives the entry. Pass `undefined` to clear and accept all\n * levels again.\n *\n * @example\n * // production: silence debug noise everywhere at once\n * logger.setMinLevel(\"info\");\n */\n public setMinLevel(level: LogLevel | undefined): this {\n this.minLevel = level;\n return this;\n }\n\n /**\n * Read the active minimum severity (or `undefined` when none is set).\n */\n public getMinLevel(): LogLevel | undefined {\n return this.minLevel;\n }\n\n /**\n * Set channels\n */\n public setChannels(channels: LogChannel[]) {\n this.channels = channels;\n\n for (const channel of channels) {\n this.attachChannel(channel);\n }\n\n return this;\n }\n\n /**\n * Route a channel's own failures (outside `log()`, such as file retention)\n * to this logger's channel error handler.\n */\n private attachChannel(channel: LogChannel): LogChannel {\n if (typeof channel.setErrorReporter === \"function\") {\n channel.setErrorReporter((error, entry) => this.reportChannelError(error, channel, entry));\n }\n\n return channel;\n }\n\n /**\n * Normalize log data to a single object\n */\n private normalizeLogData(\n dataOrModule: LoggingData | OmittedLoggingData | string,\n action?: string,\n message: any = \"\",\n level?: LogLevel,\n context?: Record<string, any>,\n ): LoggingData {\n if (typeof dataOrModule === \"object\") {\n // If level is provided, override type\n return {\n type: (level || (dataOrModule as any).type || \"info\") as LogLevel,\n module: dataOrModule.module,\n action: dataOrModule.action,\n message: dataOrModule.message,\n ...(context ? { context } : dataOrModule.context ? { context: dataOrModule.context } : {}),\n };\n }\n return {\n type: (level || \"info\") as LogLevel,\n module: dataOrModule,\n action: action as string,\n message,\n ...(context ? { context } : {}),\n };\n }\n\n /**\n * Make log\n *\n * Fans out a single log entry to every registered channel. Non-terminal\n * channels receive a copy whose `message` has had ANSI color codes stripped\n * — each channel sees its own shallow clone so one channel cannot observe\n * another's mutations (e.g. a later terminal channel still sees the original\n * colored message).\n *\n * Synchronous and fire-and-forget: it returns the logger, not a promise.\n * Async channel deliveries are tracked and awaited by {@link flush}; a\n * channel that throws or rejects is isolated from the others and reported\n * to {@link onChannelError}.\n */\n public log(data: LoggingData): this {\n if (this.minLevel && LEVEL_RANK[data.type] < LEVEL_RANK[this.minLevel]) {\n return this;\n }\n\n // Apply the logger-wide redact floor once. Every channel sees the\n // result; no channel can undo a logger-wide redaction (additive-only\n // semantics). A redaction that throws (a bad censor function) drops the\n // entry: delivering it unredacted would leak exactly what the config\n // exists to hide.\n let baseEntry: LoggingData;\n\n try {\n baseEntry = applyRedact(data, this.redactConfig);\n } catch (error) {\n for (const channel of this.channels) {\n this.reportChannelError(error, channel, redactionFailedEntry(data));\n }\n\n return this;\n }\n\n for (const channel of this.channels) {\n this.deliver(channel, data, baseEntry);\n }\n\n return this;\n }\n\n /**\n * Prepare and hand one entry to one channel, isolating its failure (a\n * channel redaction that throws included) and tracking an async delivery\n * until it settles.\n */\n private deliver(channel: LogChannel, data: LoggingData, baseEntry: LoggingData) {\n let payload: LoggingData = baseEntry;\n let result: void | Promise<void>;\n\n try {\n const channelRedact = channel.getRedactConfig?.();\n\n // When the channel adds paths, redact again from `data` rather than\n // from `baseEntry` so the merged config (which already contains the\n // logger-wide paths) does the full pass — avoids double-cloning the\n // already-redacted base.\n if (channelRedact) {\n payload = applyRedact(data, mergeRedact(this.redactConfig, channelRedact));\n }\n\n if (channel.terminal === false) {\n payload = { ...payload, message: clearMessage(payload.message) };\n }\n\n result = channel.log(payload);\n } catch (error) {\n // `payload` is the logger-wide redacted entry until the channel's own\n // redaction succeeded, so the handler never sees less-redacted data\n // than some channel would have.\n this.reportChannelError(error, channel, payload);\n return;\n }\n\n if (!result || typeof (result as Promise<void>).then !== \"function\") {\n return;\n }\n\n let pending = this.pendingDeliveries.get(channel);\n\n if (!pending) {\n pending = new Set();\n this.pendingDeliveries.set(channel, pending);\n }\n\n const tracked = pending;\n const delivery: Promise<void> = Promise.resolve(result)\n .catch((error) => this.reportChannelError(error, channel, payload))\n .finally(() => {\n tracked.delete(delivery);\n\n if (tracked.size === 0 && this.pendingDeliveries.get(channel) === tracked) {\n this.pendingDeliveries.delete(channel);\n }\n });\n\n tracked.add(delivery);\n }\n\n private reportChannelError(error: unknown, channel: LogContract, entry: LoggingData) {\n try {\n const outcome: unknown = this.channelErrorHandler(error, channel, entry);\n\n // An async handler that rejects must not become an unhandled rejection\n // (which an app may route straight back into this logger).\n if (outcome && typeof (outcome as Promise<unknown>).then === \"function\") {\n (outcome as Promise<unknown>).then(undefined, () => {});\n }\n } catch {\n // An error handler that throws must not turn logging into a crash.\n }\n }\n\n /**\n * Make debug log\n */\n public debug(\n dataOrModule: OmittedLoggingData | string,\n action?: string,\n message: any = \"\",\n context?: Record<string, any>,\n ) {\n const data = this.normalizeLogData(dataOrModule, action, message, \"debug\", context);\n return this.log(data);\n }\n\n /**\n * Make info log\n */\n public info(\n dataOrModule: OmittedLoggingData | string,\n action?: string,\n message: any = \"\",\n context?: Record<string, any>,\n ) {\n const data = this.normalizeLogData(dataOrModule, action, message, \"info\", context);\n return this.log(data);\n }\n\n /**\n * Make warn log\n */\n public warn(\n dataOrModule: OmittedLoggingData | string,\n action?: string,\n message: any = \"\",\n context?: Record<string, any>,\n ) {\n const data = this.normalizeLogData(dataOrModule, action, message, \"warn\", context);\n return this.log(data);\n }\n\n /**\n * Make error log\n */\n public error(\n dataOrModule: OmittedLoggingData | string,\n action?: string,\n message: any = \"\",\n context?: Record<string, any>,\n ) {\n const data = this.normalizeLogData(dataOrModule, action, message, \"error\", context);\n return this.log(data);\n }\n\n /**\n * Make success log\n */\n public success(\n dataOrModule: OmittedLoggingData | string,\n action?: string,\n message: any = \"\",\n context?: Record<string, any>,\n ) {\n const data = this.normalizeLogData(dataOrModule, action, message, \"success\", context);\n\n return this.log(data);\n }\n\n /**\n * Make fatal log — for unrecoverable failures where the application is going\n * down (failed bootstrap, lost connection to a required dependency that the\n * caller has decided not to retry, an `uncaughtException`).\n *\n * Identical shape to {@link error}; the level is purely informational —\n * `fatal` does NOT auto-flush or exit. The caller decides whether to call\n * `await log.flush()` and `process.exit(...)`.\n */\n public fatal(\n dataOrModule: OmittedLoggingData | string,\n action?: string,\n message: any = \"\",\n context?: Record<string, any>,\n ) {\n const data = this.normalizeLogData(dataOrModule, action, message, \"fatal\", context);\n\n return this.log(data);\n }\n\n /**\n * Log an `error` entry when `condition` is falsy. No-op otherwise — the\n * entry is never built and channels are not invoked, so this is genuinely\n * free in the happy path. Mirrors the spirit of `console.assert` but routes\n * through the logger pipeline so persistent channels capture failures.\n *\n * @example\n * log.assert(user !== null, \"auth\", \"session\", \"user vanished mid-flight\", { sessionId });\n */\n public assert(\n condition: unknown,\n module: string,\n action: string,\n message: any,\n context?: Record<string, any>,\n ): this {\n if (condition) return this;\n return this.error(module, action, message, context);\n }\n\n /**\n * Start a duration timer. The returned function emits an `info` entry\n * with `completed in <ms>ms` and a `durationMs` field in `context` when\n * called. Pass an object to `end()` to merge extra fields into context.\n *\n * @example\n * const end = log.timer(\"db\", \"users.findById\");\n * const user = await usersRepo.findById(id);\n * end({ id, found: !!user });\n */\n public timer(module: string, action: string): (extra?: Record<string, any>) => this {\n const startedAt = Date.now();\n return (extra?: Record<string, any>) => {\n const durationMs = Date.now() - startedAt;\n return this.info(module, action, `completed in ${durationMs}ms`, {\n durationMs,\n ...(extra ?? {}),\n });\n };\n }\n\n /**\n * Get channel by name\n */\n public channel(name: string) {\n return this.channels.find((channel) => channel.name === name);\n }\n\n /**\n * Synchronously flush logs\n */\n public flushSync() {\n for (const channel of this.channels) {\n if (channel.flushSync) {\n channel.flushSync();\n }\n }\n }\n\n /**\n * Wait for every in-flight async delivery, then drain every channel that\n * implements `flush()` — bounded by `timeout` (default\n * {@link DEFAULT_FLUSH_TIMEOUT_MS}).\n *\n * Unlike {@link flushSync}, this awaits each channel's async I/O — the\n * correct call for a graceful shutdown that can afford to wait\n * (`await log.flush()` after closing the HTTP server, before\n * `process.exit`). A channel whose delivery is async (a network transport,\n * an async disk write) implements `flush()`, not `flushSync()`.\n *\n * Channels drain concurrently and independently: each waits for its own\n * pending deliveries (including ones logged while the flush runs), then\n * runs its `flush()`. A channel whose delivery hangs therefore cannot hold\n * up another channel's drain, and a channel whose flush rejects can neither\n * stop the others nor escape as an unhandled rejection. When `timeout`\n * elapses, `flush()` resolves anyway and each channel still draining is\n * reported to {@link onChannelError}. Channels without `flush()` only wait\n * for their deliveries.\n *\n * @example\n * async function shutdown() {\n * await httpServer.close();\n * await log.flush({ timeout: 3000 });\n * process.exit(0);\n * }\n */\n public async flush(options: { timeout?: number } = {}): Promise<void> {\n const timeout = options.timeout ?? DEFAULT_FLUSH_TIMEOUT_MS;\n const draining = new Set<LogChannel>(this.channels);\n\n for (const channel of this.pendingDeliveries.keys()) {\n draining.add(channel);\n }\n\n const unfinished = new Set(draining);\n\n const drains = [...draining].map(async (channel) => {\n try {\n await this.drainChannel(channel);\n } catch {\n // A single channel must never break shutdown for the others —\n // a graceful drain is best-effort across every channel.\n } finally {\n unfinished.delete(channel);\n }\n });\n\n const settled = Promise.allSettled(drains).then(() => false);\n\n // `Infinity` (or any non-finite value) waits without a limit; setTimeout\n // would otherwise overflow it to an immediate timeout.\n if (!Number.isFinite(timeout)) {\n await settled;\n return;\n }\n\n let resolveTimeout!: (timedOut: boolean) => void;\n const timeoutReached = new Promise<boolean>((resolve) => {\n resolveTimeout = resolve;\n });\n const timer = setTimeout(() => resolveTimeout(true), Math.max(0, timeout));\n\n // Never keep the process alive just to time a flush out.\n (timer as { unref?: () => void }).unref?.();\n\n const timedOut = await Promise.race([settled, timeoutReached]);\n\n clearTimeout(timer);\n\n if (!timedOut) return;\n\n for (const channel of unfinished) {\n this.reportChannelError(\n new Error(`flush timed out after ${timeout}ms; entries still in flight may be lost`),\n channel,\n { type: \"warn\", module: \"logger\", action: \"flush\", message: \"flush timed out\" },\n );\n }\n }\n\n /**\n * Wait until the channel has no delivery in flight, run its `flush()`, and\n * repeat while that flush let new deliveries in.\n */\n private async drainChannel(channel: LogChannel) {\n do {\n let pending = this.pendingDeliveries.get(channel);\n\n while (pending && pending.size > 0) {\n await Promise.allSettled([...pending]);\n pending = this.pendingDeliveries.get(channel);\n }\n\n if (channel.flush) {\n await channel.flush();\n }\n } while ((this.pendingDeliveries.get(channel)?.size ?? 0) > 0);\n }\n\n /**\n * Register one process-level handler per event that calls `flushSync()`\n * before the process terminates.\n *\n * For signal events (`SIGINT`, `SIGTERM`, `SIGHUP`, `SIGBREAK`, `SIGUSR2`)\n * the handler flushes and then re-raises the signal so Node's default exit\n * behavior runs. For `beforeExit`, the handler flushes in place — Node exits\n * naturally afterwards.\n *\n * Idempotent: calling with the same events replaces the previous handlers.\n * Call `disableAutoFlush()` to unregister.\n *\n * @example\n * log.configure({\n * channels: [new ConsoleLog(), new FileLog()],\n * autoFlushOn: [\"SIGINT\", \"SIGTERM\", \"beforeExit\"],\n * });\n */\n public enableAutoFlush(events: AutoFlushEvent[]): this {\n this.disableAutoFlush();\n\n for (const event of events) {\n const handler = SIGNAL_EVENTS.has(event)\n ? () => {\n this.flushSync();\n process.off(event, handler);\n process.kill(process.pid, event as NodeJS.Signals);\n }\n : () => {\n this.flushSync();\n };\n\n process.on(event, handler);\n this.autoFlushHandlers.set(event, handler);\n }\n\n return this;\n }\n\n /**\n * Remove every handler previously registered by `enableAutoFlush`.\n * Safe to call when no handlers are registered.\n */\n public disableAutoFlush(): this {\n for (const [event, handler] of this.autoFlushHandlers) {\n process.off(event, handler);\n }\n\n this.autoFlushHandlers.clear();\n\n return this;\n }\n}\n\n/**\n * Default `timeout` of {@link Logger.flush}: long enough for a healthy network\n * channel, short enough that a hung one cannot eat a shutdown budget.\n */\nexport const DEFAULT_FLUSH_TIMEOUT_MS = 5000;\n\n/**\n * What the error handler receives when the logger-wide redaction itself\n * failed: the entry's identity only, never the unredacted message or context.\n */\nfunction redactionFailedEntry(data: LoggingData): LoggingData {\n return {\n type: data.type,\n module: data.module,\n action: data.action,\n message: \"[entry dropped: redaction failed]\",\n };\n}\n\n/**\n * Default {@link ChannelErrorHandler}: one stderr line per failure. Written\n * directly to the stream, never through a logger, so a broken channel cannot\n * recurse.\n */\nfunction reportChannelError(error: unknown, channel: LogContract) {\n const reason = error instanceof Error ? error.message : String(error);\n\n try {\n process.stderr.write(\n `[@warlock.js/logger] channel \"${channel.name}\" failed to log: ${reason}\\n`,\n );\n } catch {\n // stderr itself is gone; nothing left to report to.\n }\n}\n\n/**\n * The package singleton. Use this for everyday logging — `log.info(...)`,\n * `log.error(...)`, `log.configure(...)`. Custom logger instances can be\n * created by instantiating `Logger` directly.\n *\n * The name is intentionally short: `log` reads naturally at the call site\n * (`log.info(\"auth\", \"login\", \"ok\")`) and matches the convention used in\n * pino, bunyan, and most JS logging tutorials.\n *\n * Note that `log` is a `Logger` instance, **not** a function — the bare\n * callable form was removed when the dual `log` / `logger` exports were\n * collapsed into a single name. Use `log.info(...)` (or any other level\n * shortcut) to emit entries.\n */\nexport const log = new Logger();\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"logger.mjs","names":[],"sources":["../../../../../../../packages/logger/src/logger.ts"],"sourcesContent":["import { Random } from \"@mongez/reinforcements\";\nimport type { LogChannel } from \"./log-channel\";\nimport { applyRedact, mergeRedact, maskRegisteredLogSecrets } from \"./redact\";\nimport { redactRegisteredSecrets } from \"./redact/registered-secrets\";\nimport type {\n AutoFlushEvent,\n ChannelErrorHandler,\n LogContract,\n LoggingData,\n LogLevel,\n OmittedLoggingData,\n RedactConfig,\n} from \"./types\";\nimport { clearMessage } from \"./utils/clear-message\";\n\nconst SIGNAL_EVENTS: ReadonlySet<AutoFlushEvent> = new Set([\n \"SIGINT\",\n \"SIGTERM\",\n \"SIGHUP\",\n \"SIGBREAK\",\n \"SIGUSR2\",\n]);\n\n/**\n * Severity ranks used by `setMinLevel`. Higher number = more severe. The\n * ordering matches conventional log-level hierarchies: `debug` is noisiest\n * and easiest to drop; `error` is the loudest and never dropped by the\n * minimum-level filter. `success` sits beside `info` — it's an informational\n * outcome, not a warning.\n */\nconst LEVEL_RANK: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n success: 1,\n warn: 2,\n error: 3,\n fatal: 4,\n};\n\nexport class Logger {\n /**\n * Current channel\n */\n public channels: LogChannel[] = [];\n\n public id = \"logger-\" + Random.string(32);\n\n /**\n * Registered auto-flush handlers, keyed by event name. Stored so repeated\n * calls to `enableAutoFlush` replace rather than stack, and so\n * `disableAutoFlush` can remove them cleanly.\n */\n private autoFlushHandlers = new Map<AutoFlushEvent, () => void>();\n\n /**\n * Logger-wide minimum severity. When set, entries below this level are\n * dropped before any channel is invoked — cheaper than per-channel `levels`\n * filters because the fan-out loop is skipped entirely. `undefined` means\n * no minimum (every entry reaches every channel that accepts it).\n */\n private minLevel?: LogLevel;\n\n /**\n * Logger-wide redaction floor. Applied once before fan-out — every\n * channel receives an entry with these paths already censored. Channel\n * configs can extend the path list (additive); they cannot remove paths\n * set here.\n */\n private redactConfig?: RedactConfig;\n\n /**\n * Deliveries still in flight on async channels, per channel. `log()` never\n * awaits them; `flush()` does, so a graceful shutdown still waits for every\n * entry. Kept per channel so one hung channel cannot hold up another's\n * drain.\n */\n private pendingDeliveries = new Map<LogChannel, Set<Promise<void>>>();\n\n /**\n * Receives a channel's delivery failure (a sync throw or a rejected async\n * `log()`). Defaults to a single stderr line, never back through the logger.\n */\n private channelErrorHandler: ChannelErrorHandler = reportChannelError;\n\n /**\n * Add a new channel\n */\n public addChannel(channel: LogChannel) {\n this.channels.push(this.attachChannel(channel));\n\n return this;\n }\n\n /**\n * Set base configurations\n */\n public configure(config: {\n channels?: LogChannel[];\n autoFlushOn?: AutoFlushEvent[];\n minLevel?: LogLevel;\n redact?: RedactConfig;\n onChannelError?: ChannelErrorHandler;\n }) {\n if (config.channels) {\n this.setChannels(config.channels);\n }\n\n if (config.autoFlushOn) {\n this.enableAutoFlush(config.autoFlushOn);\n }\n\n if (config.minLevel !== undefined) {\n this.setMinLevel(config.minLevel);\n }\n\n if (config.redact !== undefined) {\n this.setRedact(config.redact);\n }\n\n if (config.onChannelError) {\n this.onChannelError(config.onChannelError);\n }\n\n return this;\n }\n\n /**\n * Handle a channel's delivery failure. Logging is fire-and-forget, so a\n * channel that throws or rejects never reaches the call site and never\n * becomes an unhandled rejection; it goes to this handler instead. The\n * default writes one line to stderr. The handler must not log through this\n * logger (a failing channel would loop).\n *\n * @example\n * log.onChannelError((error, channel) => metrics.increment(`log.${channel.name}.failed`));\n */\n public onChannelError(handler: ChannelErrorHandler): this {\n this.channelErrorHandler = handler;\n return this;\n }\n\n /**\n * Set the logger-wide redaction floor. Applied to every entry before\n * fan-out; channel configs add more paths on top, never fewer. Pass\n * `undefined` to clear.\n *\n * @example\n * log.setRedact({\n * paths: [\"context.password\", \"context.*.token\"],\n * censor: \"[REDACTED]\",\n * });\n */\n public setRedact(config: RedactConfig | undefined): this {\n this.redactConfig = config;\n return this;\n }\n\n /**\n * Read the active logger-wide redact config (or `undefined`).\n */\n public getRedact(): RedactConfig | undefined {\n return this.redactConfig;\n }\n\n /**\n * Drop every entry whose severity is below `level` before fan-out. Cheaper\n * than per-channel `levels` filters because the loop never runs and no\n * channel receives the entry. Pass `undefined` to clear and accept all\n * levels again.\n *\n * @example\n * // production: silence debug noise everywhere at once\n * logger.setMinLevel(\"info\");\n */\n public setMinLevel(level: LogLevel | undefined): this {\n this.minLevel = level;\n return this;\n }\n\n /**\n * Read the active minimum severity (or `undefined` when none is set).\n */\n public getMinLevel(): LogLevel | undefined {\n return this.minLevel;\n }\n\n /**\n * Set channels\n */\n public setChannels(channels: LogChannel[]) {\n this.channels = channels;\n\n for (const channel of channels) {\n this.attachChannel(channel);\n }\n\n return this;\n }\n\n /**\n * Route a channel's own failures (outside `log()`, such as file retention)\n * to this logger's channel error handler.\n */\n private attachChannel(channel: LogChannel): LogChannel {\n if (typeof channel.setErrorReporter === \"function\") {\n channel.setErrorReporter((error, entry) => this.reportChannelError(error, channel, entry));\n }\n\n return channel;\n }\n\n /**\n * Normalize log data to a single object\n */\n private normalizeLogData(\n dataOrModule: LoggingData | OmittedLoggingData | string,\n action?: string,\n message: any = \"\",\n level?: LogLevel,\n context?: Record<string, any>,\n ): LoggingData {\n if (typeof dataOrModule === \"object\") {\n // If level is provided, override type\n return {\n type: (level || (dataOrModule as any).type || \"info\") as LogLevel,\n module: dataOrModule.module,\n action: dataOrModule.action,\n message: dataOrModule.message,\n ...(context ? { context } : dataOrModule.context ? { context: dataOrModule.context } : {}),\n };\n }\n return {\n type: (level || \"info\") as LogLevel,\n module: dataOrModule,\n action: action as string,\n message,\n ...(context ? { context } : {}),\n };\n }\n\n /**\n * Make log\n *\n * Fans out a single log entry to every registered channel. Non-terminal\n * channels receive a copy whose `message` has had ANSI color codes stripped\n * — each channel sees its own shallow clone so one channel cannot observe\n * another's mutations (e.g. a later terminal channel still sees the original\n * colored message).\n *\n * Synchronous and fire-and-forget: it returns the logger, not a promise.\n * Async channel deliveries are tracked and awaited by {@link flush}; a\n * channel that throws or rejects is isolated from the others and reported\n * to {@link onChannelError}.\n */\n public log(data: LoggingData): this {\n if (this.minLevel && LEVEL_RANK[data.type] < LEVEL_RANK[this.minLevel]) {\n return this;\n }\n\n // Apply the logger-wide redact floor once. Every channel sees the\n // result; no channel can undo a logger-wide redaction (additive-only\n // semantics). A redaction that throws (a bad censor function) drops the\n // entry: delivering it unredacted would leak exactly what the config\n // exists to hide.\n let baseEntry: LoggingData;\n\n try {\n baseEntry = applyRedact(data, this.redactConfig);\n } catch (error) {\n for (const channel of this.channels) {\n this.reportChannelError(error, channel, redactionFailedEntry(data));\n }\n\n return this;\n }\n\n for (const channel of this.channels) {\n this.deliver(channel, data, baseEntry);\n }\n\n return this;\n }\n\n /**\n * Prepare and hand one entry to one channel, isolating its failure (a\n * channel redaction that throws included) and tracking an async delivery\n * until it settles.\n */\n private deliver(channel: LogChannel, data: LoggingData, baseEntry: LoggingData) {\n let payload: LoggingData = baseEntry;\n let result: void | Promise<void>;\n\n try {\n const channelRedact = channel.getRedactConfig?.();\n\n // When the channel adds paths, redact again from `data` rather than\n // from `baseEntry` so the merged config (which already contains the\n // logger-wide paths) does the full pass — avoids double-cloning the\n // already-redacted base.\n if (channelRedact) {\n payload = applyRedact(data, mergeRedact(this.redactConfig, channelRedact));\n }\n\n if (channel.terminal === false) {\n payload = { ...payload, message: clearMessage(payload.message) };\n }\n\n result = channel.log(payload);\n } catch (error) {\n // `payload` is the logger-wide redacted entry until the channel's own\n // redaction succeeded, so the handler never sees less-redacted data\n // than some channel would have.\n this.reportChannelError(error, channel, payload);\n return;\n }\n\n if (!result || typeof (result as Promise<void>).then !== \"function\") {\n return;\n }\n\n let pending = this.pendingDeliveries.get(channel);\n\n if (!pending) {\n pending = new Set();\n this.pendingDeliveries.set(channel, pending);\n }\n\n const tracked = pending;\n const delivery: Promise<void> = Promise.resolve(result)\n .catch((error) => this.reportChannelError(error, channel, payload))\n .finally(() => {\n tracked.delete(delivery);\n\n if (tracked.size === 0 && this.pendingDeliveries.get(channel) === tracked) {\n this.pendingDeliveries.delete(channel);\n }\n });\n\n tracked.add(delivery);\n }\n\n private reportChannelError(error: unknown, channel: LogContract, entry: LoggingData) {\n try {\n const outcome: unknown = this.channelErrorHandler(\n redactRegisteredSecrets(error),\n channel,\n redactRegisteredSecrets(entry),\n );\n\n // An async handler that rejects must not become an unhandled rejection\n // (which an app may route straight back into this logger).\n if (outcome && typeof (outcome as Promise<unknown>).then === \"function\") {\n (outcome as Promise<unknown>).then(undefined, () => {});\n }\n } catch {\n // An error handler that throws must not turn logging into a crash.\n }\n }\n\n /**\n * Make debug log\n */\n public debug(\n dataOrModule: OmittedLoggingData | string,\n action?: string,\n message: any = \"\",\n context?: Record<string, any>,\n ) {\n const data = this.normalizeLogData(dataOrModule, action, message, \"debug\", context);\n return this.log(data);\n }\n\n /**\n * Make info log\n */\n public info(\n dataOrModule: OmittedLoggingData | string,\n action?: string,\n message: any = \"\",\n context?: Record<string, any>,\n ) {\n const data = this.normalizeLogData(dataOrModule, action, message, \"info\", context);\n return this.log(data);\n }\n\n /**\n * Make warn log\n */\n public warn(\n dataOrModule: OmittedLoggingData | string,\n action?: string,\n message: any = \"\",\n context?: Record<string, any>,\n ) {\n const data = this.normalizeLogData(dataOrModule, action, message, \"warn\", context);\n return this.log(data);\n }\n\n /**\n * Make error log\n */\n public error(\n dataOrModule: OmittedLoggingData | string,\n action?: string,\n message: any = \"\",\n context?: Record<string, any>,\n ) {\n const data = this.normalizeLogData(dataOrModule, action, message, \"error\", context);\n return this.log(data);\n }\n\n /**\n * Make success log\n */\n public success(\n dataOrModule: OmittedLoggingData | string,\n action?: string,\n message: any = \"\",\n context?: Record<string, any>,\n ) {\n const data = this.normalizeLogData(dataOrModule, action, message, \"success\", context);\n\n return this.log(data);\n }\n\n /**\n * Make fatal log — for unrecoverable failures where the application is going\n * down (failed bootstrap, lost connection to a required dependency that the\n * caller has decided not to retry, an `uncaughtException`).\n *\n * Identical shape to {@link error}; the level is purely informational —\n * `fatal` does NOT auto-flush or exit. The caller decides whether to call\n * `await log.flush()` and `process.exit(...)`.\n */\n public fatal(\n dataOrModule: OmittedLoggingData | string,\n action?: string,\n message: any = \"\",\n context?: Record<string, any>,\n ) {\n const data = this.normalizeLogData(dataOrModule, action, message, \"fatal\", context);\n\n return this.log(data);\n }\n\n /**\n * Log an `error` entry when `condition` is falsy. No-op otherwise — the\n * entry is never built and channels are not invoked, so this is genuinely\n * free in the happy path. Mirrors the spirit of `console.assert` but routes\n * through the logger pipeline so persistent channels capture failures.\n *\n * @example\n * log.assert(user !== null, \"auth\", \"session\", \"user vanished mid-flight\", { sessionId });\n */\n public assert(\n condition: unknown,\n module: string,\n action: string,\n message: any,\n context?: Record<string, any>,\n ): this {\n if (condition) return this;\n return this.error(module, action, message, context);\n }\n\n /**\n * Start a duration timer. The returned function emits an `info` entry\n * with `completed in <ms>ms` and a `durationMs` field in `context` when\n * called. Pass an object to `end()` to merge extra fields into context.\n *\n * @example\n * const end = log.timer(\"db\", \"users.findById\");\n * const user = await usersRepo.findById(id);\n * end({ id, found: !!user });\n */\n public timer(module: string, action: string): (extra?: Record<string, any>) => this {\n const startedAt = Date.now();\n return (extra?: Record<string, any>) => {\n const durationMs = Date.now() - startedAt;\n return this.info(module, action, `completed in ${durationMs}ms`, {\n durationMs,\n ...(extra ?? {}),\n });\n };\n }\n\n /**\n * Get channel by name\n */\n public channel(name: string) {\n return this.channels.find((channel) => channel.name === name);\n }\n\n /**\n * Synchronously flush logs\n */\n public flushSync() {\n for (const channel of this.channels) {\n if (channel.flushSync) {\n channel.flushSync();\n }\n }\n }\n\n /**\n * Wait for every in-flight async delivery, then drain every channel that\n * implements `flush()` — bounded by `timeout` (default\n * {@link DEFAULT_FLUSH_TIMEOUT_MS}).\n *\n * Unlike {@link flushSync}, this awaits each channel's async I/O — the\n * correct call for a graceful shutdown that can afford to wait\n * (`await log.flush()` after closing the HTTP server, before\n * `process.exit`). A channel whose delivery is async (a network transport,\n * an async disk write) implements `flush()`, not `flushSync()`.\n *\n * Channels drain concurrently and independently: each waits for its own\n * pending deliveries (including ones logged while the flush runs), then\n * runs its `flush()`. A channel whose delivery hangs therefore cannot hold\n * up another channel's drain, and a channel whose flush rejects can neither\n * stop the others nor escape as an unhandled rejection. When `timeout`\n * elapses, `flush()` resolves anyway and each channel still draining is\n * reported to {@link onChannelError}. Channels without `flush()` only wait\n * for their deliveries.\n *\n * @example\n * async function shutdown() {\n * await httpServer.close();\n * await log.flush({ timeout: 3000 });\n * process.exit(0);\n * }\n */\n public async flush(options: { timeout?: number } = {}): Promise<void> {\n const timeout = options.timeout ?? DEFAULT_FLUSH_TIMEOUT_MS;\n const draining = new Set<LogChannel>(this.channels);\n\n for (const channel of this.pendingDeliveries.keys()) {\n draining.add(channel);\n }\n\n const unfinished = new Set(draining);\n\n const drains = [...draining].map(async (channel) => {\n try {\n await this.drainChannel(channel);\n } catch {\n // A single channel must never break shutdown for the others —\n // a graceful drain is best-effort across every channel.\n } finally {\n unfinished.delete(channel);\n }\n });\n\n const settled = Promise.allSettled(drains).then(() => false);\n\n // `Infinity` (or any non-finite value) waits without a limit; setTimeout\n // would otherwise overflow it to an immediate timeout.\n if (!Number.isFinite(timeout)) {\n await settled;\n return;\n }\n\n let resolveTimeout!: (timedOut: boolean) => void;\n const timeoutReached = new Promise<boolean>((resolve) => {\n resolveTimeout = resolve;\n });\n const timer = setTimeout(() => resolveTimeout(true), Math.max(0, timeout));\n\n // Never keep the process alive just to time a flush out.\n (timer as { unref?: () => void }).unref?.();\n\n const timedOut = await Promise.race([settled, timeoutReached]);\n\n clearTimeout(timer);\n\n if (!timedOut) return;\n\n for (const channel of unfinished) {\n this.reportChannelError(\n new Error(`flush timed out after ${timeout}ms; entries still in flight may be lost`),\n channel,\n { type: \"warn\", module: \"logger\", action: \"flush\", message: \"flush timed out\" },\n );\n }\n }\n\n /**\n * Wait until the channel has no delivery in flight, run its `flush()`, and\n * repeat while that flush let new deliveries in.\n */\n private async drainChannel(channel: LogChannel) {\n do {\n let pending = this.pendingDeliveries.get(channel);\n\n while (pending && pending.size > 0) {\n await Promise.allSettled([...pending]);\n pending = this.pendingDeliveries.get(channel);\n }\n\n if (channel.flush) {\n await channel.flush();\n }\n } while ((this.pendingDeliveries.get(channel)?.size ?? 0) > 0);\n }\n\n /**\n * Register one process-level handler per event that calls `flushSync()`\n * before the process terminates.\n *\n * For signal events (`SIGINT`, `SIGTERM`, `SIGHUP`, `SIGBREAK`, `SIGUSR2`)\n * the handler flushes and then re-raises the signal so Node's default exit\n * behavior runs. For `beforeExit`, the handler flushes in place — Node exits\n * naturally afterwards.\n *\n * Idempotent: calling with the same events replaces the previous handlers.\n * Call `disableAutoFlush()` to unregister.\n *\n * @example\n * log.configure({\n * channels: [new ConsoleLog(), new FileLog()],\n * autoFlushOn: [\"SIGINT\", \"SIGTERM\", \"beforeExit\"],\n * });\n */\n public enableAutoFlush(events: AutoFlushEvent[]): this {\n this.disableAutoFlush();\n\n for (const event of events) {\n const handler = SIGNAL_EVENTS.has(event)\n ? () => {\n this.flushSync();\n process.off(event, handler);\n process.kill(process.pid, event as NodeJS.Signals);\n }\n : () => {\n this.flushSync();\n };\n\n process.on(event, handler);\n this.autoFlushHandlers.set(event, handler);\n }\n\n return this;\n }\n\n /**\n * Remove every handler previously registered by `enableAutoFlush`.\n * Safe to call when no handlers are registered.\n */\n public disableAutoFlush(): this {\n for (const [event, handler] of this.autoFlushHandlers) {\n process.off(event, handler);\n }\n\n this.autoFlushHandlers.clear();\n\n return this;\n }\n}\n\n/**\n * Default `timeout` of {@link Logger.flush}: long enough for a healthy network\n * channel, short enough that a hung one cannot eat a shutdown budget.\n */\nexport const DEFAULT_FLUSH_TIMEOUT_MS = 5000;\n\n/**\n * What the error handler receives when the logger-wide redaction itself\n * failed: the entry's identity only, never the unredacted message or context.\n */\nfunction redactionFailedEntry(data: LoggingData): LoggingData {\n return {\n type: data.type,\n module: data.module,\n action: data.action,\n message: \"[entry dropped: redaction failed]\",\n };\n}\n\n/**\n * Default {@link ChannelErrorHandler}: one stderr line per failure. Written\n * directly to the stream, never through a logger, so a broken channel cannot\n * recurse.\n */\nfunction reportChannelError(error: unknown, channel: LogContract) {\n const reason = error instanceof Error ? error.message : String(error);\n\n try {\n process.stderr.write(\n maskRegisteredLogSecrets(\n `[@warlock.js/logger] channel \"${channel.name}\" failed to log: ${reason}\\n`,\n ),\n );\n } catch {\n // stderr itself is gone; nothing left to report to.\n }\n}\n\n/**\n * The package singleton. Use this for everyday logging — `log.info(...)`,\n * `log.error(...)`, `log.configure(...)`. Custom logger instances can be\n * created by instantiating `Logger` directly.\n *\n * The name is intentionally short: `log` reads naturally at the call site\n * (`log.info(\"auth\", \"login\", \"ok\")`) and matches the convention used in\n * pino, bunyan, and most JS logging tutorials.\n *\n * Note that `log` is a `Logger` instance, **not** a function — the bare\n * callable form was removed when the dual `log` / `logger` exports were\n * collapsed into a single name. Use `log.info(...)` (or any other level\n * shortcut) to emit entries.\n */\nexport const log = new Logger();\n"],"mappings":"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"}
|
package/esm/redact/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, normalizeRedactKey } from "./default-keys.mjs";
|
|
2
|
+
import { maskRegisteredLogSecrets, registerLogSecret } from "./registered-secrets.mjs";
|
|
2
3
|
import { applyRedact, mergeRedact, resolveRedactKeys } from "./redact.mjs";
|
|
3
4
|
import { escapeLogValue } from "./escape-log-value.mjs";
|
|
4
5
|
import { MASK_EMAILS_MAX_LENGTH, maskEmail, maskEmailsInText } from "./mask-email.mjs";
|
|
5
6
|
|
|
6
|
-
export { DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, MASK_EMAILS_MAX_LENGTH, applyRedact, escapeLogValue, maskEmail, maskEmailsInText, mergeRedact, normalizeRedactKey, resolveRedactKeys };
|
|
7
|
+
export { DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, MASK_EMAILS_MAX_LENGTH, applyRedact, escapeLogValue, maskEmail, maskEmailsInText, maskRegisteredLogSecrets, mergeRedact, normalizeRedactKey, registerLogSecret, resolveRedactKeys };
|
package/esm/redact/redact.d.mts
CHANGED
|
@@ -29,12 +29,14 @@ export declare function resolveRedactKeys(config: RedactConfig | undefined): Rea
|
|
|
29
29
|
*
|
|
30
30
|
* ## Residual gaps (by design, documented rather than silently absent)
|
|
31
31
|
*
|
|
32
|
-
* - **
|
|
33
|
-
*
|
|
34
|
-
* - **`Map`/`Set`/`Buffer` contents** are
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
32
|
+
* - **Unregistered secrets interpolated into strings** cannot be reached.
|
|
33
|
+
* registerLogSecret protects known literal values, including Error text.
|
|
34
|
+
* - Without registered credentials, **`Map`/`Set`/`Buffer` contents** are
|
|
35
|
+
* not traversed (see {@link isOpaque}). Registered credentials add a
|
|
36
|
+
* final safety pass that traverses Maps/Sets and masks opaque objects.
|
|
37
|
+
* - Key/path redaction does not walk non-enumerable properties. With
|
|
38
|
+
* registered credentials the final pass walks data descriptors and replaces
|
|
39
|
+
* accessors without invoking them.
|
|
38
40
|
*/
|
|
39
41
|
export declare function applyRedact(data: LoggingData, config: RedactConfig | undefined): LoggingData;
|
|
40
42
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redact.d.mts","names":[],"sources":["../../../../../../../../packages/logger/src/redact/redact.ts"],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"redact.d.mts","names":[],"sources":["../../../../../../../../packages/logger/src/redact/redact.ts"],"mappings":";;;;;;;;;wBAgVgB,kBACd,QAAQ,2BACP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAmDa,YAAY,MAAM,aAAa,QAAQ,2BAA2B;;;;;;;;;;;;;;;wBAgDlE,YACd,MAAM,0BACN,OAAO,2BACN"}
|
package/esm/redact/redact.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_REDACT_KEY_SET, normalizeRedactKey } from "./default-keys.mjs";
|
|
2
|
+
import { redactRegisteredSecrets } from "./registered-secrets.mjs";
|
|
2
3
|
|
|
3
4
|
//#region ../../packages/logger/src/redact/redact.ts
|
|
4
5
|
const DEFAULT_CENSOR = "[REDACTED]";
|
|
@@ -205,17 +206,19 @@ function resolveRedactKeys(config) {
|
|
|
205
206
|
*
|
|
206
207
|
* ## Residual gaps (by design, documented rather than silently absent)
|
|
207
208
|
*
|
|
208
|
-
* - **
|
|
209
|
-
*
|
|
210
|
-
* - **`Map`/`Set`/`Buffer` contents** are
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
209
|
+
* - **Unregistered secrets interpolated into strings** cannot be reached.
|
|
210
|
+
* registerLogSecret protects known literal values, including Error text.
|
|
211
|
+
* - Without registered credentials, **`Map`/`Set`/`Buffer` contents** are
|
|
212
|
+
* not traversed (see {@link isOpaque}). Registered credentials add a
|
|
213
|
+
* final safety pass that traverses Maps/Sets and masks opaque objects.
|
|
214
|
+
* - Key/path redaction does not walk non-enumerable properties. With
|
|
215
|
+
* registered credentials the final pass walks data descriptors and replaces
|
|
216
|
+
* accessors without invoking them.
|
|
214
217
|
*/
|
|
215
218
|
function applyRedact(data, config) {
|
|
216
219
|
const paths = config?.paths ?? [];
|
|
217
220
|
const keys = resolveRedactKeys(config);
|
|
218
|
-
if (paths.length === 0 && !keys) return data;
|
|
221
|
+
if (paths.length === 0 && !keys) return redactRegisteredSecrets(data);
|
|
219
222
|
const censor = config?.censor ?? DEFAULT_CENSOR;
|
|
220
223
|
let result = data;
|
|
221
224
|
const censored = /* @__PURE__ */ new Set();
|
|
@@ -232,7 +235,7 @@ function applyRedact(data, config) {
|
|
|
232
235
|
censor,
|
|
233
236
|
alreadyCensored: censored
|
|
234
237
|
});
|
|
235
|
-
return result;
|
|
238
|
+
return redactRegisteredSecrets(result);
|
|
236
239
|
}
|
|
237
240
|
/**
|
|
238
241
|
* Combine two redact configs into one effective config. Used to merge a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redact.mjs","names":[],"sources":["../../../../../../../../packages/logger/src/redact/redact.ts"],"sourcesContent":["import type { LoggingData, RedactCensor, RedactConfig } from \"../types\";\nimport { DEFAULT_REDACT_KEY_SET, normalizeRedactKey } from \"./default-keys\";\n\nconst DEFAULT_CENSOR = \"[REDACTED]\";\n\n/**\n * Per-key redaction settings threaded through the clone walk. Absent when the\n * walk is a plain structural clone (path-only redaction).\n */\ntype KeyRedaction = {\n /** Normalized key names to censor wherever they appear. */\n keys: ReadonlySet<string>;\n censor: RedactCensor;\n /**\n * Dotted paths the path-glob pass already censored. Skipped here so a\n * function censor is never invoked twice on the same leaf (the second call\n * would receive the already-masked value, not the original).\n */\n alreadyCensored: ReadonlySet<string>;\n};\n\n/**\n * Values we copy by reference rather than walking. Expanding these with\n * `Object.keys` would be destructive, not protective — a `Buffer` becomes a\n * multi-thousand-key index map, a `Map`/`Set`/`RegExp` becomes `{}`. Their\n * contents are consequently *not* reachable by redaction; see the residual\n * gaps documented on `applyRedact`.\n */\nfunction isOpaque(value: object): boolean {\n return (\n ArrayBuffer.isView(value) ||\n value instanceof ArrayBuffer ||\n value instanceof Map ||\n value instanceof Set ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof Promise ||\n value instanceof RegExp\n );\n}\n\n/**\n * Deep-clone a value with structural fidelity for log entries — handles plain\n * objects, arrays, `Date`, `Error`, and primitives. Anything else (buffers,\n * maps, sets, promises, regexes) is copied by reference: we only redact\n * through walkable structures anyway, and rebuilding e.g. a `Buffer` would\n * change semantics.\n *\n * Purpose-built rather than reaching for `structuredClone`: `Error` instances\n * lose their `message`/`stack` under `structuredClone` in some Node versions,\n * and the logger pipeline carries them often.\n *\n * When `redaction` is supplied, keys matching its denylist are censored during\n * the same pass — one traversal, not two.\n */\nfunction cloneEntry<T>(\n value: T,\n seen = new WeakMap<object, any>(),\n redaction?: KeyRedaction,\n trail: string[] = [],\n): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n const asObject = value as unknown as object;\n\n if (seen.has(asObject)) {\n return seen.get(asObject);\n }\n\n if (value instanceof Date) {\n return new Date(value.getTime()) as unknown as T;\n }\n\n if (isOpaque(asObject)) {\n return value;\n }\n\n if (value instanceof Error) {\n return cloneError(value, seen, redaction, trail) as unknown as T;\n }\n\n if (Array.isArray(value)) {\n const arr: any[] = [];\n seen.set(asObject, arr);\n value.forEach((item, index) => {\n // Array indices are positions, not names — never key-matched, but the\n // trail still carries them so function censors see a usable path.\n arr.push(cloneEntry(item, seen, redaction, [...trail, String(index)]));\n });\n return arr as unknown as T;\n }\n\n const out: Record<string, any> = {};\n seen.set(asObject, out);\n copyOwnKeys(value as Record<string, any>, out, seen, redaction, trail);\n return out as unknown as T;\n}\n\n/**\n * Copy every own enumerable key from `source` onto `target`, censoring the\n * ones the key denylist matches and recursing into the rest.\n */\nfunction copyOwnKeys(\n source: Record<string, any>,\n target: Record<string, any>,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): void {\n for (const key of Object.keys(source)) {\n const childTrail = [...trail, key];\n\n if (\n redaction &&\n redaction.keys.has(normalizeRedactKey(key)) &&\n !redaction.alreadyCensored.has(childTrail.join(\".\"))\n ) {\n target[key] = applyCensor(source[key], redaction.censor, childTrail);\n continue;\n }\n\n target[key] = cloneEntry(source[key], seen, redaction, childTrail);\n }\n}\n\n/**\n * Clone an `Error`, preserving its own enumerable properties.\n *\n * Those extra properties matter for redaction: HTTP clients (axios, got)\n * attach `.config`/`.request`/`.response` to the errors they throw, and those\n * routinely carry the *outgoing* `Authorization` header of the failed request.\n * Copying them through the walk is what lets the key denylist reach in and\n * censor them — dropping them instead (the pre-4.15.0 behavior) hid the\n * secret only when redaction happened to be configured, and took `.code` and\n * friends with it.\n *\n * `name`/`message`/`stack` are non-enumerable on `Error`, so they are carried\n * over explicitly.\n */\nfunction cloneError(\n value: Error,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): Error {\n let copy: Error;\n\n try {\n copy = new (value.constructor as ErrorConstructor)(value.message);\n } catch {\n // A subclass whose constructor demands a different signature must not\n // take the whole log call down with it.\n copy = new Error(value.message);\n }\n\n // Assigned rather than trusted to the constructor: a subclass that derives\n // its message from a structured argument (`new HttpError({ detail })`)\n // produces an empty message when replayed with a plain string. Defined\n // non-enumerably to match native `Error`, so the clone serializes with the\n // same shape as the original.\n for (const [key, source] of [\n [\"message\", value.message],\n [\"name\", value.name],\n [\"stack\", value.stack],\n ] as const) {\n Object.defineProperty(copy, key, {\n value: source,\n writable: true,\n enumerable: false,\n configurable: true,\n });\n }\n\n seen.set(value, copy);\n copyOwnKeys(value as unknown as Record<string, any>, copy as any, seen, redaction, trail);\n\n return copy;\n}\n\n/**\n * Apply a single censor decision to a value. String censors are returned\n * verbatim; function censors receive the original value plus the dotted\n * path so callers can implement value-aware redaction (mask all but the\n * last 4 chars, hash, etc.).\n */\nfunction applyCensor(value: any, censor: RedactCensor, path: string[]): any {\n if (typeof censor === \"function\") {\n return censor(value, path.join(\".\"));\n }\n return censor;\n}\n\n/**\n * Walk `target` following the remaining `segments` of a path pattern,\n * replacing matched leaves via `censor`. Operates in place — the caller\n * is responsible for cloning before calling.\n *\n * Every censored leaf's dotted path is recorded in `censored` so a following\n * key-denylist pass can leave it alone.\n *\n * Wildcards:\n * - `*` matches exactly one segment (any key on a plain object, any index\n * on an array — stringified for the path that's passed to a function\n * censor).\n * - `**` matches zero or more segments greedily; the rest of the pattern\n * is then attempted at the current level and at every descendant.\n */\nfunction redactAtPath(\n target: any,\n segments: string[],\n censor: RedactCensor,\n pathTrail: string[],\n censored: Set<string>,\n): void {\n if (target === null || typeof target !== \"object\") {\n return;\n }\n\n if (segments.length === 0) {\n return;\n }\n\n const [head, ...rest] = segments;\n\n if (head === undefined) {\n // The `segments.length === 0` guard above already makes this unreachable,\n // but the compiler cannot narrow a DESTRUCTURED element from a length\n // check, so under `noUncheckedIndexedAccess` `head` is `string |\n // undefined` and every use of it below inherits that.\n //\n // Stated rather than asserted away with `!`, because of what this function\n // is: if `head` were ever undefined, `keysToVisit` would carry an\n // `undefined` key, `target[key]` would read and WRITE the literal property\n // \"undefined\", and the field that was supposed to be censored would be\n // left in the log untouched. A redaction path is the wrong place to be\n // approximately right.\n return;\n }\n\n if (head === \"**\") {\n // Try matching `rest` at the current level (the zero-segment match\n // case), then recurse into every child carrying the `**` forward so\n // it keeps matching at deeper levels too.\n if (rest.length > 0) {\n redactAtPath(target, rest, censor, pathTrail, censored);\n }\n const keys = Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target);\n for (const key of keys) {\n redactAtPath(target[key], segments, censor, [...pathTrail, key], censored);\n }\n return;\n }\n\n const keysToVisit =\n head === \"*\"\n ? Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target)\n : Array.isArray(target)\n ? // Numeric segment can index into an array.\n /^\\d+$/.test(head) && Number(head) < target.length\n ? [head]\n : []\n : Object.prototype.hasOwnProperty.call(target, head)\n ? [head]\n : [];\n\n for (const key of keysToVisit) {\n if (rest.length === 0) {\n const leafTrail = [...pathTrail, key];\n target[key] = applyCensor(target[key], censor, leafTrail);\n censored.add(leafTrail.join(\".\"));\n } else {\n redactAtPath(target[key], rest, censor, [...pathTrail, key], censored);\n }\n }\n}\n\n/**\n * Cheap pre-scan: does this graph contain any denylisted key at all?\n *\n * Lets the default-on key pass stay allocation-free for the overwhelming\n * majority of entries, which carry no secrets — we only pay for a clone when\n * there is actually something to censor. Also keeps `applyRedact`'s\n * \"returns the input by reference when nothing changed\" contract intact.\n */\nfunction hasDenylistedKey(\n value: any,\n keys: ReadonlySet<string>,\n seen = new WeakSet<object>(),\n): boolean {\n if (value === null || typeof value !== \"object\") {\n return false;\n }\n\n const asObject = value as object;\n\n if (seen.has(asObject)) return false;\n seen.add(asObject);\n\n if (value instanceof Date || isOpaque(asObject)) {\n return false;\n }\n\n if (Array.isArray(value)) {\n return value.some((item) => hasDenylistedKey(item, keys, seen));\n }\n\n for (const key of Object.keys(value)) {\n if (keys.has(normalizeRedactKey(key))) return true;\n if (hasDenylistedKey(value[key], keys, seen)) return true;\n }\n\n return false;\n}\n\n/**\n * Cache of resolved key sets, keyed by the config object they came from.\n * Logger-wide and channel configs are long-lived references, so this makes\n * the per-entry cost a single map lookup. Merged configs (rebuilt per entry\n * by `mergeRedact`) fall out of the `WeakMap` on their own.\n */\nconst keySetCache = new WeakMap<RedactConfig, ReadonlySet<string>>();\n\n/**\n * Resolve the effective key denylist for a config: the built-in set (unless\n * `defaultKeys: false`) plus any `keys` the application added.\n *\n * Returns `undefined` only when there is nothing to match — i.e. defaults are\n * explicitly off and no custom keys were supplied.\n */\nexport function resolveRedactKeys(\n config: RedactConfig | undefined,\n): ReadonlySet<string> | undefined {\n const useDefaults = config?.defaultKeys !== false;\n const extra = config?.keys;\n\n if (!extra || extra.length === 0) {\n return useDefaults ? DEFAULT_REDACT_KEY_SET : undefined;\n }\n\n const cached = config && keySetCache.get(config);\n if (cached) return cached;\n\n const resolved = new Set<string>(useDefaults ? DEFAULT_REDACT_KEY_SET : []);\n for (const key of extra) {\n resolved.add(normalizeRedactKey(key));\n }\n\n if (config) keySetCache.set(config, resolved);\n\n return resolved;\n}\n\n/**\n * Produce a new `LoggingData` with sensitive data censored:\n *\n * 1. every path in `config.paths` (opt-in globs), then\n * 2. every key matching the denylist — the built-in\n * {@link DEFAULT_REDACT_KEYS} plus `config.keys`, at any depth of\n * `context`, `message`, and an `Error`'s own enumerable properties.\n *\n * Step 2 runs **with no config at all**: passing `undefined` still censors\n * `password`, `authorization`, `token`, `apiKey` and friends. Pass\n * `{ defaultKeys: false }` to opt out.\n *\n * Paths run first so a function censor sees the original value rather than a\n * mask; leaves the path pass already censored are skipped by the key pass.\n *\n * The original entry is never mutated — channels and other call sites can\n * hold references to the input safely. Returns the input **by reference**\n * when nothing matched, so the fast path stays allocation-free.\n *\n * ## Residual gaps (by design, documented rather than silently absent)\n *\n * - **Secrets interpolated into a `message` string** (`` `token=${t}` ``)\n * cannot be reached — neither a path nor a key names a substring.\n * - **`Map`/`Set`/`Buffer` contents** are not traversed (see {@link isOpaque}).\n * - **Non-enumerable / getter-backed properties** are not walked, so an HTTP\n * client that exposes request config behind a getter still slips through.\n * Enumerable ones (axios's `.config`, `.response`) *are* covered.\n */\nexport function applyRedact(data: LoggingData, config: RedactConfig | undefined): LoggingData {\n const paths = config?.paths ?? [];\n const keys = resolveRedactKeys(config);\n\n if (paths.length === 0 && !keys) {\n return data;\n }\n\n const censor = config?.censor ?? DEFAULT_CENSOR;\n\n let result = data;\n const censored = new Set<string>();\n\n if (paths.length > 0) {\n result = cloneEntry(data);\n\n for (const pattern of paths) {\n const segments = pattern.split(\".\").filter((segment) => segment.length > 0);\n if (segments.length === 0) continue;\n redactAtPath(result, segments, censor, [], censored);\n }\n }\n\n if (keys && hasDenylistedKey(result, keys)) {\n result = cloneEntry(result, new WeakMap(), {\n keys,\n censor,\n alreadyCensored: censored,\n });\n }\n\n return result;\n}\n\n/**\n * Combine two redact configs into one effective config. Used to merge a\n * channel's additive paths on top of the logger-wide floor.\n *\n * - `paths` and `keys` are concatenated; duplicates are kept (the matcher\n * tolerates them, and de-duping cross-config would mask a developer typo).\n * - `censor` from the channel wins; falls back to the logger's; falls back\n * to the default `\"[REDACTED]\"`.\n * - `defaultKeys` follows the additive-only contract: a channel can turn the\n * built-in denylist back *on* (`true`) but can never turn off one the\n * logger-wide floor left enabled. When the channel is silent, the logger's\n * choice is inherited — opting out logger-wide is not quietly undone by\n * any channel that happens to set a `redact` option.\n */\nexport function mergeRedact(\n base: RedactConfig | undefined,\n extra: RedactConfig | undefined,\n): RedactConfig | undefined {\n if (!base && !extra) return undefined;\n\n if (!base) {\n // An absent logger-wide config still means \"built-in denylist on\" — it is\n // the floor, not the absence of one. A channel may not switch it off, so\n // its `defaultKeys: false` is overridden here rather than inherited.\n return extra!.defaultKeys === false ? { ...extra!, defaultKeys: true } : extra;\n }\n\n if (!extra) return base;\n\n return {\n paths: [...(base.paths ?? []), ...(extra.paths ?? [])],\n keys: [...(base.keys ?? []), ...(extra.keys ?? [])],\n defaultKeys: extra.defaultKeys === true ? true : base.defaultKeys,\n censor: extra.censor ?? base.censor,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"redact.mjs","names":[],"sources":["../../../../../../../../packages/logger/src/redact/redact.ts"],"sourcesContent":["import { redactRegisteredSecrets } from \"./registered-secrets\";\nimport type { LoggingData, RedactCensor, RedactConfig } from \"../types\";\nimport { DEFAULT_REDACT_KEY_SET, normalizeRedactKey } from \"./default-keys\";\n\nconst DEFAULT_CENSOR = \"[REDACTED]\";\n\n/**\n * Per-key redaction settings threaded through the clone walk. Absent when the\n * walk is a plain structural clone (path-only redaction).\n */\ntype KeyRedaction = {\n /** Normalized key names to censor wherever they appear. */\n keys: ReadonlySet<string>;\n censor: RedactCensor;\n /**\n * Dotted paths the path-glob pass already censored. Skipped here so a\n * function censor is never invoked twice on the same leaf (the second call\n * would receive the already-masked value, not the original).\n */\n alreadyCensored: ReadonlySet<string>;\n};\n\n/**\n * Values we copy by reference rather than walking. Expanding these with\n * `Object.keys` would be destructive, not protective — a `Buffer` becomes a\n * multi-thousand-key index map, a `Map`/`Set`/`RegExp` becomes `{}`. Their\n * contents are consequently *not* reachable by redaction; see the residual\n * gaps documented on `applyRedact`.\n */\nfunction isOpaque(value: object): boolean {\n return (\n ArrayBuffer.isView(value) ||\n value instanceof ArrayBuffer ||\n value instanceof Map ||\n value instanceof Set ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof Promise ||\n value instanceof RegExp\n );\n}\n\n/**\n * Deep-clone a value with structural fidelity for log entries — handles plain\n * objects, arrays, `Date`, `Error`, and primitives. Anything else (buffers,\n * maps, sets, promises, regexes) is copied by reference: we only redact\n * through walkable structures anyway, and rebuilding e.g. a `Buffer` would\n * change semantics.\n *\n * Purpose-built rather than reaching for `structuredClone`: `Error` instances\n * lose their `message`/`stack` under `structuredClone` in some Node versions,\n * and the logger pipeline carries them often.\n *\n * When `redaction` is supplied, keys matching its denylist are censored during\n * the same pass — one traversal, not two.\n */\nfunction cloneEntry<T>(\n value: T,\n seen = new WeakMap<object, any>(),\n redaction?: KeyRedaction,\n trail: string[] = [],\n): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n const asObject = value as unknown as object;\n\n if (seen.has(asObject)) {\n return seen.get(asObject);\n }\n\n if (value instanceof Date) {\n return new Date(value.getTime()) as unknown as T;\n }\n\n if (isOpaque(asObject)) {\n return value;\n }\n\n if (value instanceof Error) {\n return cloneError(value, seen, redaction, trail) as unknown as T;\n }\n\n if (Array.isArray(value)) {\n const arr: any[] = [];\n seen.set(asObject, arr);\n value.forEach((item, index) => {\n // Array indices are positions, not names — never key-matched, but the\n // trail still carries them so function censors see a usable path.\n arr.push(cloneEntry(item, seen, redaction, [...trail, String(index)]));\n });\n return arr as unknown as T;\n }\n\n const out: Record<string, any> = {};\n seen.set(asObject, out);\n copyOwnKeys(value as Record<string, any>, out, seen, redaction, trail);\n return out as unknown as T;\n}\n\n/**\n * Copy every own enumerable key from `source` onto `target`, censoring the\n * ones the key denylist matches and recursing into the rest.\n */\nfunction copyOwnKeys(\n source: Record<string, any>,\n target: Record<string, any>,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): void {\n for (const key of Object.keys(source)) {\n const childTrail = [...trail, key];\n\n if (\n redaction &&\n redaction.keys.has(normalizeRedactKey(key)) &&\n !redaction.alreadyCensored.has(childTrail.join(\".\"))\n ) {\n target[key] = applyCensor(source[key], redaction.censor, childTrail);\n continue;\n }\n\n target[key] = cloneEntry(source[key], seen, redaction, childTrail);\n }\n}\n\n/**\n * Clone an `Error`, preserving its own enumerable properties.\n *\n * Those extra properties matter for redaction: HTTP clients (axios, got)\n * attach `.config`/`.request`/`.response` to the errors they throw, and those\n * routinely carry the *outgoing* `Authorization` header of the failed request.\n * Copying them through the walk is what lets the key denylist reach in and\n * censor them — dropping them instead (the pre-4.15.0 behavior) hid the\n * secret only when redaction happened to be configured, and took `.code` and\n * friends with it.\n *\n * `name`/`message`/`stack` are non-enumerable on `Error`, so they are carried\n * over explicitly.\n */\nfunction cloneError(\n value: Error,\n seen: WeakMap<object, any>,\n redaction: KeyRedaction | undefined,\n trail: string[],\n): Error {\n let copy: Error;\n\n try {\n copy = new (value.constructor as ErrorConstructor)(value.message);\n } catch {\n // A subclass whose constructor demands a different signature must not\n // take the whole log call down with it.\n copy = new Error(value.message);\n }\n\n // Assigned rather than trusted to the constructor: a subclass that derives\n // its message from a structured argument (`new HttpError({ detail })`)\n // produces an empty message when replayed with a plain string. Defined\n // non-enumerably to match native `Error`, so the clone serializes with the\n // same shape as the original.\n for (const [key, source] of [\n [\"message\", value.message],\n [\"name\", value.name],\n [\"stack\", value.stack],\n ] as const) {\n Object.defineProperty(copy, key, {\n value: source,\n writable: true,\n enumerable: false,\n configurable: true,\n });\n }\n\n seen.set(value, copy);\n copyOwnKeys(value as unknown as Record<string, any>, copy as any, seen, redaction, trail);\n\n return copy;\n}\n\n/**\n * Apply a single censor decision to a value. String censors are returned\n * verbatim; function censors receive the original value plus the dotted\n * path so callers can implement value-aware redaction (mask all but the\n * last 4 chars, hash, etc.).\n */\nfunction applyCensor(value: any, censor: RedactCensor, path: string[]): any {\n if (typeof censor === \"function\") {\n return censor(value, path.join(\".\"));\n }\n return censor;\n}\n\n/**\n * Walk `target` following the remaining `segments` of a path pattern,\n * replacing matched leaves via `censor`. Operates in place — the caller\n * is responsible for cloning before calling.\n *\n * Every censored leaf's dotted path is recorded in `censored` so a following\n * key-denylist pass can leave it alone.\n *\n * Wildcards:\n * - `*` matches exactly one segment (any key on a plain object, any index\n * on an array — stringified for the path that's passed to a function\n * censor).\n * - `**` matches zero or more segments greedily; the rest of the pattern\n * is then attempted at the current level and at every descendant.\n */\nfunction redactAtPath(\n target: any,\n segments: string[],\n censor: RedactCensor,\n pathTrail: string[],\n censored: Set<string>,\n): void {\n if (target === null || typeof target !== \"object\") {\n return;\n }\n\n if (segments.length === 0) {\n return;\n }\n\n const [head, ...rest] = segments;\n\n if (head === undefined) {\n // The `segments.length === 0` guard above already makes this unreachable,\n // but the compiler cannot narrow a DESTRUCTURED element from a length\n // check, so under `noUncheckedIndexedAccess` `head` is `string |\n // undefined` and every use of it below inherits that.\n //\n // Stated rather than asserted away with `!`, because of what this function\n // is: if `head` were ever undefined, `keysToVisit` would carry an\n // `undefined` key, `target[key]` would read and WRITE the literal property\n // \"undefined\", and the field that was supposed to be censored would be\n // left in the log untouched. A redaction path is the wrong place to be\n // approximately right.\n return;\n }\n\n if (head === \"**\") {\n // Try matching `rest` at the current level (the zero-segment match\n // case), then recurse into every child carrying the `**` forward so\n // it keeps matching at deeper levels too.\n if (rest.length > 0) {\n redactAtPath(target, rest, censor, pathTrail, censored);\n }\n const keys = Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target);\n for (const key of keys) {\n redactAtPath(target[key], segments, censor, [...pathTrail, key], censored);\n }\n return;\n }\n\n const keysToVisit =\n head === \"*\"\n ? Array.isArray(target)\n ? target.map((_, index) => String(index))\n : Object.keys(target)\n : Array.isArray(target)\n ? // Numeric segment can index into an array.\n /^\\d+$/.test(head) && Number(head) < target.length\n ? [head]\n : []\n : Object.prototype.hasOwnProperty.call(target, head)\n ? [head]\n : [];\n\n for (const key of keysToVisit) {\n if (rest.length === 0) {\n const leafTrail = [...pathTrail, key];\n target[key] = applyCensor(target[key], censor, leafTrail);\n censored.add(leafTrail.join(\".\"));\n } else {\n redactAtPath(target[key], rest, censor, [...pathTrail, key], censored);\n }\n }\n}\n\n/**\n * Cheap pre-scan: does this graph contain any denylisted key at all?\n *\n * Lets the default-on key pass stay allocation-free for the overwhelming\n * majority of entries, which carry no secrets — we only pay for a clone when\n * there is actually something to censor. Also keeps `applyRedact`'s\n * \"returns the input by reference when nothing changed\" contract intact.\n */\nfunction hasDenylistedKey(\n value: any,\n keys: ReadonlySet<string>,\n seen = new WeakSet<object>(),\n): boolean {\n if (value === null || typeof value !== \"object\") {\n return false;\n }\n\n const asObject = value as object;\n\n if (seen.has(asObject)) return false;\n seen.add(asObject);\n\n if (value instanceof Date || isOpaque(asObject)) {\n return false;\n }\n\n if (Array.isArray(value)) {\n return value.some((item) => hasDenylistedKey(item, keys, seen));\n }\n\n for (const key of Object.keys(value)) {\n if (keys.has(normalizeRedactKey(key))) return true;\n if (hasDenylistedKey(value[key], keys, seen)) return true;\n }\n\n return false;\n}\n\n/**\n * Cache of resolved key sets, keyed by the config object they came from.\n * Logger-wide and channel configs are long-lived references, so this makes\n * the per-entry cost a single map lookup. Merged configs (rebuilt per entry\n * by `mergeRedact`) fall out of the `WeakMap` on their own.\n */\nconst keySetCache = new WeakMap<RedactConfig, ReadonlySet<string>>();\n\n/**\n * Resolve the effective key denylist for a config: the built-in set (unless\n * `defaultKeys: false`) plus any `keys` the application added.\n *\n * Returns `undefined` only when there is nothing to match — i.e. defaults are\n * explicitly off and no custom keys were supplied.\n */\nexport function resolveRedactKeys(\n config: RedactConfig | undefined,\n): ReadonlySet<string> | undefined {\n const useDefaults = config?.defaultKeys !== false;\n const extra = config?.keys;\n\n if (!extra || extra.length === 0) {\n return useDefaults ? DEFAULT_REDACT_KEY_SET : undefined;\n }\n\n const cached = config && keySetCache.get(config);\n if (cached) return cached;\n\n const resolved = new Set<string>(useDefaults ? DEFAULT_REDACT_KEY_SET : []);\n for (const key of extra) {\n resolved.add(normalizeRedactKey(key));\n }\n\n if (config) keySetCache.set(config, resolved);\n\n return resolved;\n}\n\n/**\n * Produce a new `LoggingData` with sensitive data censored:\n *\n * 1. every path in `config.paths` (opt-in globs), then\n * 2. every key matching the denylist — the built-in\n * {@link DEFAULT_REDACT_KEYS} plus `config.keys`, at any depth of\n * `context`, `message`, and an `Error`'s own enumerable properties.\n *\n * Step 2 runs **with no config at all**: passing `undefined` still censors\n * `password`, `authorization`, `token`, `apiKey` and friends. Pass\n * `{ defaultKeys: false }` to opt out.\n *\n * Paths run first so a function censor sees the original value rather than a\n * mask; leaves the path pass already censored are skipped by the key pass.\n *\n * The original entry is never mutated — channels and other call sites can\n * hold references to the input safely. Returns the input **by reference**\n * when nothing matched, so the fast path stays allocation-free.\n *\n * ## Residual gaps (by design, documented rather than silently absent)\n *\n * - **Unregistered secrets interpolated into strings** cannot be reached.\n * registerLogSecret protects known literal values, including Error text.\n * - Without registered credentials, **`Map`/`Set`/`Buffer` contents** are\n * not traversed (see {@link isOpaque}). Registered credentials add a\n * final safety pass that traverses Maps/Sets and masks opaque objects.\n * - Key/path redaction does not walk non-enumerable properties. With\n * registered credentials the final pass walks data descriptors and replaces\n * accessors without invoking them.\n */\nexport function applyRedact(data: LoggingData, config: RedactConfig | undefined): LoggingData {\n const paths = config?.paths ?? [];\n const keys = resolveRedactKeys(config);\n\n if (paths.length === 0 && !keys) {\n return redactRegisteredSecrets(data);\n }\n\n const censor = config?.censor ?? DEFAULT_CENSOR;\n\n let result = data;\n const censored = new Set<string>();\n\n if (paths.length > 0) {\n result = cloneEntry(data);\n\n for (const pattern of paths) {\n const segments = pattern.split(\".\").filter((segment) => segment.length > 0);\n if (segments.length === 0) continue;\n redactAtPath(result, segments, censor, [], censored);\n }\n }\n\n if (keys && hasDenylistedKey(result, keys)) {\n result = cloneEntry(result, new WeakMap(), {\n keys,\n censor,\n alreadyCensored: censored,\n });\n }\n\n return redactRegisteredSecrets(result);\n}\n\n/**\n * Combine two redact configs into one effective config. Used to merge a\n * channel's additive paths on top of the logger-wide floor.\n *\n * - `paths` and `keys` are concatenated; duplicates are kept (the matcher\n * tolerates them, and de-duping cross-config would mask a developer typo).\n * - `censor` from the channel wins; falls back to the logger's; falls back\n * to the default `\"[REDACTED]\"`.\n * - `defaultKeys` follows the additive-only contract: a channel can turn the\n * built-in denylist back *on* (`true`) but can never turn off one the\n * logger-wide floor left enabled. When the channel is silent, the logger's\n * choice is inherited — opting out logger-wide is not quietly undone by\n * any channel that happens to set a `redact` option.\n */\nexport function mergeRedact(\n base: RedactConfig | undefined,\n extra: RedactConfig | undefined,\n): RedactConfig | undefined {\n if (!base && !extra) return undefined;\n\n if (!base) {\n // An absent logger-wide config still means \"built-in denylist on\" — it is\n // the floor, not the absence of one. A channel may not switch it off, so\n // its `defaultKeys: false` is overridden here rather than inherited.\n return extra!.defaultKeys === false ? { ...extra!, defaultKeys: true } : extra;\n }\n\n if (!extra) return base;\n\n return {\n paths: [...(base.paths ?? []), ...(extra.paths ?? [])],\n keys: [...(base.keys ?? []), ...(extra.keys ?? [])],\n defaultKeys: extra.defaultKeys === true ? true : base.defaultKeys,\n censor: extra.censor ?? base.censor,\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region ../../packages/logger/src/redact/registered-secrets.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Register credentials for process-lifetime masking in every Logger instance.
|
|
4
|
+
* Empty values are ignored. Old values remain protected after rotation/removal.
|
|
5
|
+
* This safety floor is independent of redact config and custom censors.
|
|
6
|
+
*/
|
|
7
|
+
export declare function registerLogSecret(value: string, secretKeys?: readonly string[]): void;
|
|
8
|
+
/** Exact substring masking; no encoding, hashing, or fuzzy matching. */
|
|
9
|
+
export declare function maskRegisteredLogSecrets(text: string): string;
|
|
10
|
+
//#endregion
|
|
11
|
+
//# sourceMappingURL=registered-secrets.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registered-secrets.d.mts","names":[],"sources":["../../../../../../../../packages/logger/src/redact/registered-secrets.ts"],"mappings":";;;;;;wBAYgB,kBAAkB,eAAe;;wBAWjC,yBAAyB"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { normalizeRedactKey } from "./default-keys.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../../packages/logger/src/redact/registered-secrets.ts
|
|
4
|
+
const keys = /* @__PURE__ */ new Set();
|
|
5
|
+
const values = /* @__PURE__ */ new Set();
|
|
6
|
+
const MASK = "[REDACTED]";
|
|
7
|
+
let orderedValues = [];
|
|
8
|
+
/**
|
|
9
|
+
* Register credentials for process-lifetime masking in every Logger instance.
|
|
10
|
+
* Empty values are ignored. Old values remain protected after rotation/removal.
|
|
11
|
+
* This safety floor is independent of redact config and custom censors.
|
|
12
|
+
*/
|
|
13
|
+
function registerLogSecret(value, secretKeys = []) {
|
|
14
|
+
for (const key of secretKeys) {
|
|
15
|
+
const normalized = normalizeRedactKey(key);
|
|
16
|
+
if (normalized) keys.add(normalized);
|
|
17
|
+
}
|
|
18
|
+
if (!value || values.has(value)) return;
|
|
19
|
+
values.add(value);
|
|
20
|
+
orderedValues = [...values].sort((left, right) => right.length - left.length);
|
|
21
|
+
}
|
|
22
|
+
/** Exact substring masking; no encoding, hashing, or fuzzy matching. */
|
|
23
|
+
function maskRegisteredLogSecrets(text) {
|
|
24
|
+
if (orderedValues.length === 0) return text;
|
|
25
|
+
let cursor = 0;
|
|
26
|
+
let result = "";
|
|
27
|
+
while (cursor < text.length) {
|
|
28
|
+
let next = -1;
|
|
29
|
+
let length = 0;
|
|
30
|
+
for (const secret of orderedValues) {
|
|
31
|
+
const index = text.indexOf(secret, cursor);
|
|
32
|
+
if (index !== -1 && (next === -1 || index < next)) {
|
|
33
|
+
next = index;
|
|
34
|
+
length = secret.length;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (next === -1) return result + text.slice(cursor);
|
|
38
|
+
result += text.slice(cursor, next) + MASK;
|
|
39
|
+
cursor = next + length;
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
/** @internal Final safety pass, after user censors have run. */
|
|
44
|
+
function redactRegisteredSecrets(value) {
|
|
45
|
+
if (keys.size === 0 && values.size === 0) return value;
|
|
46
|
+
const seen = /* @__PURE__ */ new WeakMap();
|
|
47
|
+
function visit(value) {
|
|
48
|
+
if (typeof value === "string") return maskRegisteredLogSecrets(value);
|
|
49
|
+
if (typeof value === "function" || typeof value === "symbol") return MASK;
|
|
50
|
+
if (value === null || typeof value !== "object") return value;
|
|
51
|
+
if (seen.has(value)) return seen.get(value);
|
|
52
|
+
if (value instanceof Date) return new Date(Date.prototype.getTime.call(value));
|
|
53
|
+
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer || value instanceof WeakMap || value instanceof WeakSet || value instanceof Promise || value instanceof RegExp) return MASK;
|
|
54
|
+
if (value instanceof Map) {
|
|
55
|
+
const copy = /* @__PURE__ */ new Map();
|
|
56
|
+
seen.set(value, copy);
|
|
57
|
+
for (const [key, child] of Map.prototype.entries.call(value)) copy.set(visit(key), typeof key === "string" && keys.has(normalizeRedactKey(key)) ? MASK : visit(child));
|
|
58
|
+
return copy;
|
|
59
|
+
}
|
|
60
|
+
if (value instanceof Set) {
|
|
61
|
+
const copy = /* @__PURE__ */ new Set();
|
|
62
|
+
seen.set(value, copy);
|
|
63
|
+
for (const child of Set.prototype.values.call(value)) copy.add(visit(child));
|
|
64
|
+
return copy;
|
|
65
|
+
}
|
|
66
|
+
const copy = value instanceof Error ? /* @__PURE__ */ new Error() : Array.isArray(value) ? [] : {};
|
|
67
|
+
seen.set(value, copy);
|
|
68
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
69
|
+
if (Array.isArray(value) && key === "length") continue;
|
|
70
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
71
|
+
if (!descriptor) continue;
|
|
72
|
+
if (typeof key === "symbol") continue;
|
|
73
|
+
const name = maskRegisteredLogSecrets(key);
|
|
74
|
+
const hidden = keys.has(normalizeRedactKey(key));
|
|
75
|
+
const child = "value" in descriptor ? descriptor.value : MASK;
|
|
76
|
+
Object.defineProperty(copy, name, {
|
|
77
|
+
value: hidden ? MASK : visit(child),
|
|
78
|
+
enumerable: descriptor.enumerable,
|
|
79
|
+
writable: true,
|
|
80
|
+
configurable: true
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (value instanceof Error && !Object.hasOwn(copy, "name")) {
|
|
84
|
+
let prototype = Object.getPrototypeOf(value);
|
|
85
|
+
while (prototype) {
|
|
86
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, "name");
|
|
87
|
+
if (descriptor) {
|
|
88
|
+
const name = "value" in descriptor ? descriptor.value : MASK;
|
|
89
|
+
Object.defineProperty(copy, "name", {
|
|
90
|
+
value: typeof name === "string" ? maskRegisteredLogSecrets(name) : MASK,
|
|
91
|
+
configurable: true
|
|
92
|
+
});
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return copy;
|
|
99
|
+
}
|
|
100
|
+
return visit(value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
export { maskRegisteredLogSecrets, redactRegisteredSecrets, registerLogSecret };
|
|
105
|
+
//# sourceMappingURL=registered-secrets.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registered-secrets.mjs","names":[],"sources":["../../../../../../../../packages/logger/src/redact/registered-secrets.ts"],"sourcesContent":["import { normalizeRedactKey } from \"./default-keys\";\n\nconst keys = new Set<string>();\nconst values = new Set<string>();\nconst MASK = \"[REDACTED]\";\nlet orderedValues: string[] = [];\n\n/**\n * Register credentials for process-lifetime masking in every Logger instance.\n * Empty values are ignored. Old values remain protected after rotation/removal.\n * This safety floor is independent of redact config and custom censors.\n */\nexport function registerLogSecret(value: string, secretKeys: readonly string[] = []): void {\n for (const key of secretKeys) {\n const normalized = normalizeRedactKey(key);\n if (normalized) keys.add(normalized);\n }\n if (!value || values.has(value)) return;\n values.add(value);\n orderedValues = [...values].sort((left, right) => right.length - left.length);\n}\n\n/** Exact substring masking; no encoding, hashing, or fuzzy matching. */\nexport function maskRegisteredLogSecrets(text: string): string {\n if (orderedValues.length === 0) return text;\n let cursor = 0;\n let result = \"\";\n while (cursor < text.length) {\n let next = -1;\n let length = 0;\n // Find the leftmost match; length order breaks ties. Literal scanning avoids\n // regex compilation errors whose messages could contain a credential.\n for (const secret of orderedValues) {\n const index = text.indexOf(secret, cursor);\n if (index !== -1 && (next === -1 || index < next)) {\n next = index;\n length = secret.length;\n }\n }\n if (next === -1) return result + text.slice(cursor);\n result += text.slice(cursor, next) + MASK;\n cursor = next + length;\n }\n return result;\n}\n\n/** @internal Final safety pass, after user censors have run. */\nexport function redactRegisteredSecrets<T>(value: T): T {\n if (keys.size === 0 && values.size === 0) return value;\n const seen = new WeakMap<object, unknown>();\n\n function visit(value: unknown): unknown {\n if (typeof value === \"string\") return maskRegisteredLogSecrets(value);\n if (typeof value === \"function\" || typeof value === \"symbol\") return MASK;\n if (value === null || typeof value !== \"object\") return value;\n if (seen.has(value)) return seen.get(value);\n if (value instanceof Date) return new Date(Date.prototype.getTime.call(value));\n // Opaque objects can expose plaintext via custom serializers or inspection.\n // Mask them entirely; Maps/Sets are handled as inspectable containers below.\n if (\n ArrayBuffer.isView(value) ||\n value instanceof ArrayBuffer ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof Promise ||\n value instanceof RegExp\n )\n return MASK;\n if (value instanceof Map) {\n const copy = new Map<unknown, unknown>();\n seen.set(value, copy);\n for (const [key, child] of Map.prototype.entries.call(value) as IterableIterator<\n [unknown, unknown]\n >) {\n copy.set(\n visit(key),\n typeof key === \"string\" && keys.has(normalizeRedactKey(key)) ? MASK : visit(child),\n );\n }\n return copy;\n }\n if (value instanceof Set) {\n const copy = new Set<unknown>();\n seen.set(value, copy);\n for (const child of Set.prototype.values.call(value) as IterableIterator<unknown>)\n copy.add(visit(child));\n return copy;\n }\n const copy: object = value instanceof Error ? new Error() : Array.isArray(value) ? [] : {};\n seen.set(value, copy);\n // Descriptors avoid invoking getters and custom toJSON/inspect methods.\n for (const key of Reflect.ownKeys(value)) {\n if (Array.isArray(value) && key === \"length\") continue;\n const descriptor = Object.getOwnPropertyDescriptor(value, key);\n if (!descriptor) continue;\n if (typeof key === \"symbol\") continue;\n const name = maskRegisteredLogSecrets(key);\n const hidden = keys.has(normalizeRedactKey(key));\n const child: unknown = \"value\" in descriptor ? descriptor.value : MASK;\n Object.defineProperty(copy, name, {\n value: hidden ? MASK : visit(child),\n enumerable: descriptor.enumerable,\n writable: true,\n configurable: true,\n });\n }\n if (value instanceof Error && !Object.hasOwn(copy, \"name\")) {\n let prototype: object | null = Object.getPrototypeOf(value) as object | null;\n while (prototype) {\n const descriptor = Object.getOwnPropertyDescriptor(prototype, \"name\");\n if (descriptor) {\n const name: unknown = \"value\" in descriptor ? descriptor.value : MASK;\n Object.defineProperty(copy, \"name\", {\n value: typeof name === \"string\" ? maskRegisteredLogSecrets(name) : MASK,\n configurable: true,\n });\n break;\n }\n prototype = Object.getPrototypeOf(prototype) as object | null;\n }\n }\n return copy;\n }\n\n return visit(value) as T;\n}\n"],"mappings":"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"}
|