@warlock.js/logger 5.29.0 → 5.31.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 +48 -2
- package/README.md +14 -17
- package/cjs/index.cjs +569 -57
- package/cjs/index.cjs.map +1 -1
- package/esm/channels/console-log.d.mts +1 -1
- package/esm/channels/console-log.d.mts.map +1 -1
- package/esm/channels/console-log.mjs +1 -1
- package/esm/channels/console-log.mjs.map +1 -1
- package/esm/channels/file-log.d.mts +129 -13
- package/esm/channels/file-log.d.mts.map +1 -1
- package/esm/channels/file-log.mjs +204 -23
- package/esm/channels/file-log.mjs.map +1 -1
- package/esm/channels/file-name-pattern.d.mts +16 -0
- package/esm/channels/file-name-pattern.d.mts.map +1 -0
- package/esm/channels/file-name-pattern.mjs +41 -0
- package/esm/channels/file-name-pattern.mjs.map +1 -0
- package/esm/channels/index.mjs +1 -1
- package/esm/channels/json-file-log.d.mts +1 -1
- package/esm/channels/json-file-log.d.mts.map +1 -1
- package/esm/channels/json-file-log.mjs +25 -6
- package/esm/channels/json-file-log.mjs.map +1 -1
- package/esm/channels/sentry-log.d.mts +1 -1
- package/esm/channels/sentry-log.d.mts.map +1 -1
- package/esm/channels/sentry-log.mjs +1 -1
- package/esm/channels/sentry-log.mjs.map +1 -1
- package/esm/index.d.mts +8 -5
- package/esm/index.mjs +5 -3
- package/esm/log-channel.d.mts +24 -2
- package/esm/log-channel.d.mts.map +1 -1
- package/esm/log-channel.mjs +23 -1
- package/esm/log-channel.mjs.map +1 -1
- package/esm/logger.d.mts +78 -18
- package/esm/logger.d.mts.map +1 -1
- package/esm/logger.mjs +163 -22
- package/esm/logger.mjs.map +1 -1
- package/esm/redact/default-keys.d.mts +1 -1
- package/esm/redact/default-keys.d.mts.map +1 -1
- package/esm/redact/default-keys.mjs +1 -1
- package/esm/redact/default-keys.mjs.map +1 -1
- package/esm/redact/escape-log-value.d.mts +26 -0
- package/esm/redact/escape-log-value.d.mts.map +1 -0
- package/esm/redact/escape-log-value.mjs +37 -0
- package/esm/redact/escape-log-value.mjs.map +1 -0
- package/esm/redact/index.mjs +2 -0
- package/esm/redact/mask-email.d.mts +40 -0
- package/esm/redact/mask-email.d.mts.map +1 -0
- package/esm/redact/mask-email.mjs +70 -0
- package/esm/redact/mask-email.mjs.map +1 -0
- package/esm/redact/redact.d.mts +1 -1
- package/esm/redact/redact.d.mts.map +1 -1
- package/esm/redact/redact.mjs +1 -1
- package/esm/redact/redact.mjs.map +1 -1
- package/esm/types.d.mts +10 -2
- package/esm/types.d.mts.map +1 -1
- package/esm/utils/capture-unhandled-errors.d.mts +1 -1
- package/esm/utils/capture-unhandled-errors.d.mts.map +1 -1
- package/esm/utils/capture-unhandled-errors.mjs +1 -1
- package/esm/utils/capture-unhandled-errors.mjs.map +1 -1
- package/esm/utils/clear-message.d.mts +1 -1
- package/esm/utils/clear-message.d.mts.map +1 -1
- package/esm/utils/clear-message.mjs +1 -1
- package/esm/utils/clear-message.mjs.map +1 -1
- package/esm/utils/safe-json-stringify.d.mts +1 -1
- package/esm/utils/safe-json-stringify.d.mts.map +1 -1
- package/esm/utils/safe-json-stringify.mjs +1 -1
- package/esm/utils/safe-json-stringify.mjs.map +1 -1
- package/llms-full.txt +247 -147
- package/llms.txt +2 -2
- package/package.json +4 -3
- package/skills/capture-unhandled-errors/SKILL.md +9 -10
- package/skills/configure-logger/SKILL.md +7 -10
- package/skills/filter-log-entries/SKILL.md +2 -2
- package/skills/flush-logs-on-shutdown/SKILL.md +34 -15
- package/skills/index.md +4 -1
- package/skills/logger-basics/SKILL.md +21 -24
- package/skills/overview/SKILL.md +17 -6
- package/skills/pick-log-channel/SKILL.md +36 -24
- package/skills/redact-sensitive-log-fields/SKILL.md +48 -18
- package/skills/ship-logs-to-sentry/SKILL.md +18 -18
- package/skills/test-logging-code/SKILL.md +4 -2
- package/skills/write-custom-log-channel/SKILL.md +51 -18
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"default-keys.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"default-keys.mjs","names":[],"sources":["../../../../../../../../packages/logger/src/redact/default-keys.ts"],"sourcesContent":["/**\n * Built-in secret-key denylist — the redaction floor that applies with no\n * configuration at all.\n *\n * ## Why a default list exists\n *\n * Path-based `redact.paths` is a *tool*; it only protects an application whose\n * every call site was configured correctly, and nothing warns you when one\n * wasn't. The overwhelmingly common leak is mundane and needs no attacker\n * input: `log.error(\"auth\", \"login\", \"failed\", { headers: req.headers, body:\n * req.body })` puts a live bearer token and a cleartext password into every\n * sink. A key denylist that runs by default turns redaction from a tool into a\n * default protection.\n *\n * ## Matching semantics — exact, on a normalized key\n *\n * Keys are compared after {@link normalizeRedactKey}: lower-cased with every\n * non-alphanumeric character removed. So one entry covers every spelling\n * convention a codebase might use —\n * `apiKey` / `api_key` / `API-KEY` / `apikey` all normalize to `apikey`.\n *\n * The match is **exact on the normalized key, not a substring**. That is a\n * deliberate trade: substring matching (`*token*`) would also swallow\n * `tokenCount`, `passwordUpdatedAt`, `secretsLoaded` — silent, hard-to-debug\n * data loss in the one place engineers look when something is broken. An exact\n * list is auditable: you can read it and know precisely what disappears. The\n * cost is that unusual spellings are missed, which is why the list carries the\n * concrete wire-format variants that actually show up in HTTP headers\n * (`x-api-key`, `proxy-authorization`, `set-cookie`) and why apps can extend it\n * with `redact.keys`.\n *\n * ## What is deliberately NOT here\n *\n * `key`, `auth`, `hash`, `signature`, `salt`, `id` — all too generic. Each\n * would redact far more non-secret fields than secret ones (`key` alone would\n * blank out every `key` in a keyed collection). Applications that want them\n * add them via `redact.keys`.\n */\n\n/**\n * Normalize a key for denylist comparison: lower-case, strip every\n * non-alphanumeric character. Collapses `apiKey`, `api_key`, `API-KEY`,\n * `Api Key` and `apikey` onto a single entry.\n */\nexport function normalizeRedactKey(key: string): string {\n return key.toLowerCase().replace(/[^a-z0-9]/g, \"\");\n}\n\n/**\n * The default denylist, in source spelling. Anything whose normalized key\n * matches one of these is replaced by the configured censor (`\"[REDACTED]\"`\n * by default) at any depth of `context`, `message`, or an `Error`'s own\n * enumerable properties.\n *\n * Exported so applications can inspect, log, or build on the exact set they\n * are getting rather than guessing at it.\n */\nexport const DEFAULT_REDACT_KEYS: readonly string[] = [\n // --- Passwords ---------------------------------------------------------\n \"password\",\n \"passwd\",\n \"pwd\",\n \"passphrase\",\n \"passwordHash\",\n \"currentPassword\",\n \"newPassword\",\n \"passwordConfirmation\",\n\n // --- Shared secrets ----------------------------------------------------\n \"secret\",\n \"clientSecret\",\n \"appSecret\",\n \"apiSecret\",\n \"secretKey\",\n\n // --- Tokens ------------------------------------------------------------\n \"token\",\n \"accessToken\",\n \"refreshToken\",\n \"idToken\",\n \"authToken\",\n \"apiToken\",\n \"bearerToken\",\n \"csrfToken\",\n \"sessionToken\",\n \"resetToken\",\n \"verificationToken\",\n \"jwt\",\n \"otp\",\n\n // --- Keys --------------------------------------------------------------\n \"apiKey\",\n \"privateKey\",\n \"encryptionKey\",\n \"signingKey\",\n\n // --- HTTP credential headers -------------------------------------------\n // Header names are matched in their wire spelling; normalization removes\n // the dashes, so `x-api-key` and `X_API_KEY` both land on `xapikey`.\n \"authorization\",\n \"proxyAuthorization\",\n \"x-api-key\",\n \"x-auth-token\",\n \"cookie\",\n \"set-cookie\",\n\n // --- Session / credential containers -----------------------------------\n // `sessionId` is a bearer credential: whoever reads it from a log can\n // replay the session. Teams that log it deliberately for correlation can\n // drop it via `redact.keys` + `defaultKeys: false`, or keep a prefix with a\n // function censor.\n \"sessionId\",\n \"credentials\",\n \"credential\",\n \"connectionString\",\n\n // --- Financial / high-sensitivity PII ----------------------------------\n \"creditCard\",\n \"creditCardNumber\",\n \"cardNumber\",\n \"cvv\",\n \"cvc\",\n \"ssn\",\n \"socialSecurityNumber\",\n \"iban\",\n \"taxId\",\n];\n\n/**\n * The default denylist, pre-normalized. Frozen at module load so the hot path\n * never rebuilds it.\n */\nexport const DEFAULT_REDACT_KEY_SET: ReadonlySet<string> = new Set(\n DEFAULT_REDACT_KEYS.map(normalizeRedactKey),\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAgB,mBAAmB,KAAqB;CACtD,OAAO,IAAI,YAAY,CAAC,CAAC,QAAQ,cAAc,EAAE;AACnD;;;;;;;;;;AAWA,MAAa,sBAAyC;CAEpD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CAKA;CACA;CACA;CACA;CACA;CACA;CAOA;CACA;CACA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;AAMA,MAAa,yBAA8C,IAAI,IAC7D,oBAAoB,IAAI,kBAAkB,CAC5C"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region ../../packages/logger/src/redact/escape-log-value.d.ts
|
|
2
|
+
type EscapeLogValueOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Longest result, in characters. Longer values are cut and end with
|
|
5
|
+
* `…[truncated]`. Unlimited by default.
|
|
6
|
+
*/
|
|
7
|
+
maxLength?: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Make client-controlled text safe to put inside a log line: line breaks and
|
|
11
|
+
* tabs become the visible escapes `\r`, `\n`, `\t`, other control characters
|
|
12
|
+
* (including NUL, DEL, C1 controls and the Unicode line/paragraph separators)
|
|
13
|
+
* become `\uXXXX`, and ANSI terminal escape sequences are removed.
|
|
14
|
+
*
|
|
15
|
+
* Without it, a user agent or form field containing `\n2026-01-01 [info] …`
|
|
16
|
+
* writes a forged entry into a text log, and an ANSI sequence can rewrite what
|
|
17
|
+
* a terminal shows. Structured channels (JSON) are not affected, but console
|
|
18
|
+
* and file channels are.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* log.info("auth", "login", `Login from ${escapeLogValue(request.userAgent)}`);
|
|
22
|
+
*/
|
|
23
|
+
declare function escapeLogValue(value: unknown, options?: EscapeLogValueOptions): string;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { EscapeLogValueOptions, escapeLogValue };
|
|
26
|
+
//# sourceMappingURL=escape-log-value.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escape-log-value.d.mts","names":[],"sources":["../../../../../../../../packages/logger/src/redact/escape-log-value.ts"],"mappings":";KAWY,qBAAA;EAAA;;;;EAKV,SAAS;AAAA;;;;;;;;AAiBuE;;;;;;;iBAAlE,cAAA,CAAe,KAAA,WAAgB,OAAA,GAAS,qBAA0B"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region ../../packages/logger/src/redact/escape-log-value.ts
|
|
2
|
+
/**
|
|
3
|
+
* ANSI escape sequences: CSI (`ESC [ … final`) and the two-character
|
|
4
|
+
* `ESC x` escapes (`ESC c` resets a terminal). Bounded so matching stays linear on long input.
|
|
5
|
+
*/
|
|
6
|
+
const ANSI_ESCAPE = /\u001b(?:\[[0-?]{0,32}[ -/]{0,8}[@-~]|[0-~])/g;
|
|
7
|
+
/** Control characters that are not already handled as `\r`, `\n` or `\t`. */
|
|
8
|
+
const CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u2028\u2029]/g;
|
|
9
|
+
/**
|
|
10
|
+
* Make client-controlled text safe to put inside a log line: line breaks and
|
|
11
|
+
* tabs become the visible escapes `\r`, `\n`, `\t`, other control characters
|
|
12
|
+
* (including NUL, DEL, C1 controls and the Unicode line/paragraph separators)
|
|
13
|
+
* become `\uXXXX`, and ANSI terminal escape sequences are removed.
|
|
14
|
+
*
|
|
15
|
+
* Without it, a user agent or form field containing `\n2026-01-01 [info] …`
|
|
16
|
+
* writes a forged entry into a text log, and an ANSI sequence can rewrite what
|
|
17
|
+
* a terminal shows. Structured channels (JSON) are not affected, but console
|
|
18
|
+
* and file channels are.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* log.info("auth", "login", `Login from ${escapeLogValue(request.userAgent)}`);
|
|
22
|
+
*/
|
|
23
|
+
function escapeLogValue(value, options = {}) {
|
|
24
|
+
let text = typeof value === "string" ? value : String(value);
|
|
25
|
+
const maxLength = options.maxLength;
|
|
26
|
+
let truncated = false;
|
|
27
|
+
if (maxLength !== void 0 && maxLength >= 0 && text.length > maxLength) {
|
|
28
|
+
text = text.slice(0, maxLength);
|
|
29
|
+
truncated = true;
|
|
30
|
+
}
|
|
31
|
+
const escaped = text.replace(ANSI_ESCAPE, "").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(CONTROL, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
32
|
+
return truncated ? `${escaped}…[truncated]` : escaped;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { escapeLogValue };
|
|
37
|
+
//# sourceMappingURL=escape-log-value.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escape-log-value.mjs","names":[],"sources":["../../../../../../../../packages/logger/src/redact/escape-log-value.ts"],"sourcesContent":["/**\n * ANSI escape sequences: CSI (`ESC [ … final`) and the two-character\n * `ESC x` escapes (`ESC c` resets a terminal). Bounded so matching stays linear on long input.\n */\n// oxlint-disable-next-line eslint/no-control-regex -- matching terminal escapes is the point.\nconst ANSI_ESCAPE = /\\u001b(?:\\[[0-?]{0,32}[ -/]{0,8}[@-~]|[0-~])/g;\n\n/** Control characters that are not already handled as `\\r`, `\\n` or `\\t`. */\n// oxlint-disable-next-line eslint/no-control-regex -- escaping control characters is the point.\nconst CONTROL = /[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f-\\u009f\\u2028\\u2029]/g;\n\nexport type EscapeLogValueOptions = {\n /**\n * Longest result, in characters. Longer values are cut and end with\n * `…[truncated]`. Unlimited by default.\n */\n maxLength?: number;\n};\n\n/**\n * Make client-controlled text safe to put inside a log line: line breaks and\n * tabs become the visible escapes `\\r`, `\\n`, `\\t`, other control characters\n * (including NUL, DEL, C1 controls and the Unicode line/paragraph separators)\n * become `\\uXXXX`, and ANSI terminal escape sequences are removed.\n *\n * Without it, a user agent or form field containing `\\n2026-01-01 [info] …`\n * writes a forged entry into a text log, and an ANSI sequence can rewrite what\n * a terminal shows. Structured channels (JSON) are not affected, but console\n * and file channels are.\n *\n * @example\n * log.info(\"auth\", \"login\", `Login from ${escapeLogValue(request.userAgent)}`);\n */\nexport function escapeLogValue(value: unknown, options: EscapeLogValueOptions = {}): string {\n let text = typeof value === \"string\" ? value : String(value);\n\n const maxLength = options.maxLength;\n let truncated = false;\n if (maxLength !== undefined && maxLength >= 0 && text.length > maxLength) {\n text = text.slice(0, maxLength);\n truncated = true;\n }\n\n const escaped = text\n .replace(ANSI_ESCAPE, \"\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\t/g, \"\\\\t\")\n .replace(CONTROL, (char) => `\\\\u${char.charCodeAt(0).toString(16).padStart(4, \"0\")}`);\n\n return truncated ? `${escaped}…[truncated]` : escaped;\n}\n"],"mappings":";;;;;AAKA,MAAM,cAAc;;AAIpB,MAAM,UAAU;;;;;;;;;;;;;;;AAwBhB,SAAgB,eAAe,OAAgB,UAAiC,CAAC,GAAW;CAC1F,IAAI,OAAO,OAAO,UAAU,WAAW,QAAQ,OAAO,KAAK;CAE3D,MAAM,YAAY,QAAQ;CAC1B,IAAI,YAAY;CAChB,IAAI,cAAc,UAAa,aAAa,KAAK,KAAK,SAAS,WAAW;EACxE,OAAO,KAAK,MAAM,GAAG,SAAS;EAC9B,YAAY;CACd;CAEA,MAAM,UAAU,KACb,QAAQ,aAAa,EAAE,CAAC,CACxB,QAAQ,OAAO,KAAK,CAAC,CACrB,QAAQ,OAAO,KAAK,CAAC,CACrB,QAAQ,OAAO,KAAK,CAAC,CACrB,QAAQ,UAAU,SAAS,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,GAAG;CAEtF,OAAO,YAAY,GAAG,QAAQ,gBAAgB;AAChD"}
|
package/esm/redact/index.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_REDACT_KEYS, DEFAULT_REDACT_KEY_SET, normalizeRedactKey } from "./default-keys.mjs";
|
|
2
2
|
import { applyRedact, mergeRedact, resolveRedactKeys } from "./redact.mjs";
|
|
3
|
+
import { escapeLogValue } from "./escape-log-value.mjs";
|
|
4
|
+
import { MASK_EMAILS_MAX_LENGTH, maskEmail, maskEmailsInText } from "./mask-email.mjs";
|
|
3
5
|
|
|
4
6
|
export { };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region ../../packages/logger/src/redact/mask-email.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Longest text {@link maskEmailsInText} scans. Anything past it is cut off and
|
|
4
|
+
* replaced by a marker, so a huge cache key or message cannot cost more than a
|
|
5
|
+
* bounded amount of time per log line, and nothing past the cut can leak.
|
|
6
|
+
*/
|
|
7
|
+
declare const MASK_EMAILS_MAX_LENGTH = 16384;
|
|
8
|
+
/**
|
|
9
|
+
* Mask one email address, keeping its first character and its domain:
|
|
10
|
+
* `member@example.com` → `m***@example.com`. A value without an `@` is
|
|
11
|
+
* returned as `***`.
|
|
12
|
+
*
|
|
13
|
+
* Key-based redaction cannot reach an address that sits inside a message
|
|
14
|
+
* string or a cache key; mask it before it is logged.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* log.info("mail", "send", `Sending mail to: ${maskEmail(to)}`);
|
|
18
|
+
*/
|
|
19
|
+
declare function maskEmail(address: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Mask every email address the pattern above recognises in a piece of text
|
|
22
|
+
* with {@link maskEmail}. Text longer than {@link MASK_EMAILS_MAX_LENGTH}
|
|
23
|
+
* characters is truncated first (the cut is marked).
|
|
24
|
+
*
|
|
25
|
+
* Dots are valid in the local part of an address, so a dotted prefix that
|
|
26
|
+
* runs into the address is masked with it: in a cache key such as
|
|
27
|
+
* `email.member@example.com` the whole `email.member` reads as the local part.
|
|
28
|
+
*
|
|
29
|
+
* Not recognised (left as they are): quoted local parts (`"john doe"@x.com`),
|
|
30
|
+
* IP-literal domains (`user@[127.0.0.1]`) and percent-encoded addresses
|
|
31
|
+
* (`user%40example.com`).
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* maskEmailsInText("sent to member@example.com") // "sent to m***@example.com"
|
|
35
|
+
* maskEmailsInText("email.member@example.com") // "e***@example.com"
|
|
36
|
+
*/
|
|
37
|
+
declare function maskEmailsInText(text: string): string;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { MASK_EMAILS_MAX_LENGTH, maskEmail, maskEmailsInText };
|
|
40
|
+
//# sourceMappingURL=mask-email.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mask-email.d.mts","names":[],"sources":["../../../../../../../../packages/logger/src/redact/mask-email.ts"],"mappings":";;AAoBA;;;;cAAa,sBAAA;AAab;;;;AAAyC;AA4BzC;;;;AAA6C;;AA5B7C,iBAAgB,SAAA,CAAU,OAAe;;;;;;;;;;;;;;;;;;iBA4BzB,gBAAA,CAAiB,IAAY"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
//#region ../../packages/logger/src/redact/mask-email.ts
|
|
2
|
+
/**
|
|
3
|
+
* Email addresses inside free text: a local part and a dotted domain, letters
|
|
4
|
+
* and digits from any script (`jöhn@exämple.de`). Kept deliberately simple (no
|
|
5
|
+
* quoted local parts, no IP-literal domains, no percent-encoded `@`): it masks
|
|
6
|
+
* the addresses that show up in log messages and cache keys, not every string
|
|
7
|
+
* RFC 5322 allows.
|
|
8
|
+
*
|
|
9
|
+
* Every repetition is bounded (local part 64, label 63, at most 12 labels), so
|
|
10
|
+
* matching stays linear in the input length. Cache keys and messages are often
|
|
11
|
+
* built from request input; an unbounded pattern backtracks quadratically on a
|
|
12
|
+
* long run of address-like characters with no domain.
|
|
13
|
+
*/
|
|
14
|
+
const EMAIL_IN_TEXT = /[\p{L}\p{N}._%+-]{1,64}@[\p{L}\p{N}-]{1,63}(?:\.[\p{L}\p{N}-]{1,63}){0,10}\.\p{L}{2,63}/gu;
|
|
15
|
+
/**
|
|
16
|
+
* Longest text {@link maskEmailsInText} scans. Anything past it is cut off and
|
|
17
|
+
* replaced by a marker, so a huge cache key or message cannot cost more than a
|
|
18
|
+
* bounded amount of time per log line, and nothing past the cut can leak.
|
|
19
|
+
*/
|
|
20
|
+
const MASK_EMAILS_MAX_LENGTH = 16384;
|
|
21
|
+
/**
|
|
22
|
+
* Mask one email address, keeping its first character and its domain:
|
|
23
|
+
* `member@example.com` → `m***@example.com`. A value without an `@` is
|
|
24
|
+
* returned as `***`.
|
|
25
|
+
*
|
|
26
|
+
* Key-based redaction cannot reach an address that sits inside a message
|
|
27
|
+
* string or a cache key; mask it before it is logged.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* log.info("mail", "send", `Sending mail to: ${maskEmail(to)}`);
|
|
31
|
+
*/
|
|
32
|
+
function maskEmail(address) {
|
|
33
|
+
const at = address.lastIndexOf("@");
|
|
34
|
+
if (at <= 0) return "***";
|
|
35
|
+
return `${String.fromCodePoint(address.codePointAt(0))}***${address.slice(at)}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Mask every email address the pattern above recognises in a piece of text
|
|
39
|
+
* with {@link maskEmail}. Text longer than {@link MASK_EMAILS_MAX_LENGTH}
|
|
40
|
+
* characters is truncated first (the cut is marked).
|
|
41
|
+
*
|
|
42
|
+
* Dots are valid in the local part of an address, so a dotted prefix that
|
|
43
|
+
* runs into the address is masked with it: in a cache key such as
|
|
44
|
+
* `email.member@example.com` the whole `email.member` reads as the local part.
|
|
45
|
+
*
|
|
46
|
+
* Not recognised (left as they are): quoted local parts (`"john doe"@x.com`),
|
|
47
|
+
* IP-literal domains (`user@[127.0.0.1]`) and percent-encoded addresses
|
|
48
|
+
* (`user%40example.com`).
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* maskEmailsInText("sent to member@example.com") // "sent to m***@example.com"
|
|
52
|
+
* maskEmailsInText("email.member@example.com") // "e***@example.com"
|
|
53
|
+
*/
|
|
54
|
+
function maskEmailsInText(text) {
|
|
55
|
+
let scanned = text;
|
|
56
|
+
let marker = "";
|
|
57
|
+
if (scanned.length > 16384) {
|
|
58
|
+
let cut = MASK_EMAILS_MAX_LENGTH;
|
|
59
|
+
const last = scanned.charCodeAt(cut - 1);
|
|
60
|
+
if (last >= 55296 && last <= 56319) cut -= 1;
|
|
61
|
+
marker = `…[${scanned.length - cut} chars truncated]`;
|
|
62
|
+
scanned = scanned.slice(0, cut);
|
|
63
|
+
}
|
|
64
|
+
if (!scanned.includes("@")) return scanned + marker;
|
|
65
|
+
return scanned.replace(EMAIL_IN_TEXT, (match) => maskEmail(match)) + marker;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//#endregion
|
|
69
|
+
export { MASK_EMAILS_MAX_LENGTH, maskEmail, maskEmailsInText };
|
|
70
|
+
//# sourceMappingURL=mask-email.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mask-email.mjs","names":[],"sources":["../../../../../../../../packages/logger/src/redact/mask-email.ts"],"sourcesContent":["/**\n * Email addresses inside free text: a local part and a dotted domain, letters\n * and digits from any script (`jöhn@exämple.de`). Kept deliberately simple (no\n * quoted local parts, no IP-literal domains, no percent-encoded `@`): it masks\n * the addresses that show up in log messages and cache keys, not every string\n * RFC 5322 allows.\n *\n * Every repetition is bounded (local part 64, label 63, at most 12 labels), so\n * matching stays linear in the input length. Cache keys and messages are often\n * built from request input; an unbounded pattern backtracks quadratically on a\n * long run of address-like characters with no domain.\n */\nconst EMAIL_IN_TEXT =\n /[\\p{L}\\p{N}._%+-]{1,64}@[\\p{L}\\p{N}-]{1,63}(?:\\.[\\p{L}\\p{N}-]{1,63}){0,10}\\.\\p{L}{2,63}/gu;\n\n/**\n * Longest text {@link maskEmailsInText} scans. Anything past it is cut off and\n * replaced by a marker, so a huge cache key or message cannot cost more than a\n * bounded amount of time per log line, and nothing past the cut can leak.\n */\nexport const MASK_EMAILS_MAX_LENGTH = 16_384;\n\n/**\n * Mask one email address, keeping its first character and its domain:\n * `member@example.com` → `m***@example.com`. A value without an `@` is\n * returned as `***`.\n *\n * Key-based redaction cannot reach an address that sits inside a message\n * string or a cache key; mask it before it is logged.\n *\n * @example\n * log.info(\"mail\", \"send\", `Sending mail to: ${maskEmail(to)}`);\n */\nexport function maskEmail(address: string): string {\n const at = address.lastIndexOf(\"@\");\n\n if (at <= 0) return \"***\";\n\n // The first code point, not the first UTF-16 unit: never half an emoji.\n const first = String.fromCodePoint(address.codePointAt(0)!);\n\n return `${first}***${address.slice(at)}`;\n}\n\n/**\n * Mask every email address the pattern above recognises in a piece of text\n * with {@link maskEmail}. Text longer than {@link MASK_EMAILS_MAX_LENGTH}\n * characters is truncated first (the cut is marked).\n *\n * Dots are valid in the local part of an address, so a dotted prefix that\n * runs into the address is masked with it: in a cache key such as\n * `email.member@example.com` the whole `email.member` reads as the local part.\n *\n * Not recognised (left as they are): quoted local parts (`\"john doe\"@x.com`),\n * IP-literal domains (`user@[127.0.0.1]`) and percent-encoded addresses\n * (`user%40example.com`).\n *\n * @example\n * maskEmailsInText(\"sent to member@example.com\") // \"sent to m***@example.com\"\n * maskEmailsInText(\"email.member@example.com\") // \"e***@example.com\"\n */\nexport function maskEmailsInText(text: string): string {\n let scanned = text;\n let marker = \"\";\n\n if (scanned.length > MASK_EMAILS_MAX_LENGTH) {\n let cut = MASK_EMAILS_MAX_LENGTH;\n const last = scanned.charCodeAt(cut - 1);\n\n // Never end on the first half of a surrogate pair.\n if (last >= 0xd800 && last <= 0xdbff) cut -= 1;\n\n marker = `…[${scanned.length - cut} chars truncated]`;\n scanned = scanned.slice(0, cut);\n }\n\n if (!scanned.includes(\"@\")) return scanned + marker;\n\n return scanned.replace(EMAIL_IN_TEXT, (match) => maskEmail(match)) + marker;\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,MAAM,gBACJ;;;;;;AAOF,MAAa,yBAAyB;;;;;;;;;;;;AAatC,SAAgB,UAAU,SAAyB;CACjD,MAAM,KAAK,QAAQ,YAAY,GAAG;CAElC,IAAI,MAAM,GAAG,OAAO;CAKpB,OAAO,GAFO,OAAO,cAAc,QAAQ,YAAY,CAAC,CAE1C,EAAE,KAAK,QAAQ,MAAM,EAAE;AACvC;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,iBAAiB,MAAsB;CACrD,IAAI,UAAU;CACd,IAAI,SAAS;CAEb,IAAI,QAAQ,gBAAiC;EAC3C,IAAI,MAAM;EACV,MAAM,OAAO,QAAQ,WAAW,MAAM,CAAC;EAGvC,IAAI,QAAQ,SAAU,QAAQ,OAAQ,OAAO;EAE7C,SAAS,KAAK,QAAQ,SAAS,IAAI;EACnC,UAAU,QAAQ,MAAM,GAAG,GAAG;CAChC;CAEA,IAAI,CAAC,QAAQ,SAAS,GAAG,GAAG,OAAO,UAAU;CAE7C,OAAO,QAAQ,QAAQ,gBAAgB,UAAU,UAAU,KAAK,CAAC,IAAI;AACvE"}
|
package/esm/redact/redact.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LoggingData, RedactConfig } from "../types.mjs";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../../packages/logger/src/redact/redact.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Resolve the effective key denylist for a config: the built-in set (unless
|
|
6
6
|
* `defaultKeys: false`) plus any `keys` the application added.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redact.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"redact.d.mts","names":[],"sources":["../../../../../../../../packages/logger/src/redact/redact.ts"],"mappings":";;;;;AA+UA;;;;;iBAAgB,iBAAA,CACd,MAAA,EAAQ,YAAA,eACP,WAAW;;;AAAA;AAiDd;;;;;;;;;;;;;;AAA6F;AAgD7F;;;;;;;;;;iBAhDgB,WAAA,CAAY,IAAA,EAAM,WAAA,EAAa,MAAA,EAAQ,YAAA,eAA2B,WAAA;;;;AAmDnE;;;;;;;;;;;iBAHC,WAAA,CACd,IAAA,EAAM,YAAA,cACN,KAAA,EAAO,YAAA,eACN,YAAA"}
|
package/esm/redact/redact.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_REDACT_KEY_SET, normalizeRedactKey } from "./default-keys.mjs";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../../packages/logger/src/redact/redact.ts
|
|
4
4
|
const DEFAULT_CENSOR = "[REDACTED]";
|
|
5
5
|
/**
|
|
6
6
|
* Values we copy by reference rather than walking. Expanding these with
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redact.mjs","names":[],"sources":["../../../../../../../logger/src/redact/redact.ts"],"sourcesContent":["import type { LoggingData, RedactCensor, RedactConfig } from \"../types\";\nimport {\n DEFAULT_REDACT_KEY_SET,\n normalizeRedactKey,\n} 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(\n data: LoggingData,\n config: RedactConfig | undefined,\n): 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\n ? { ...extra!, defaultKeys: true }\n : 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 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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"}
|
package/esm/types.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region
|
|
1
|
+
//#region ../../packages/logger/src/types.d.ts
|
|
2
2
|
/**
|
|
3
3
|
* Severity levels, ordered debug < info ≈ success < warn < error < fatal.
|
|
4
4
|
*
|
|
@@ -169,6 +169,14 @@ interface LogContract {
|
|
|
169
169
|
*/
|
|
170
170
|
flushSync?(): void;
|
|
171
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Receives a channel's delivery failure: a synchronous throw from its `log()`
|
|
174
|
+
* or a rejection of the promise it returned, a redaction that threw, a
|
|
175
|
+
* `flush()` that timed out, or a channel's own background failure (such as
|
|
176
|
+
* `FileLog` retention failing to remove a file). Set with `Logger.onChannelError()` or
|
|
177
|
+
* `configure({ onChannelError })`. It may be async; a rejection is swallowed.
|
|
178
|
+
*/
|
|
179
|
+
type ChannelErrorHandler = (error: unknown, channel: LogContract, entry: LoggingData) => void | Promise<void>;
|
|
172
180
|
type LoggingData = {
|
|
173
181
|
type: LogLevel;
|
|
174
182
|
module: string;
|
|
@@ -178,5 +186,5 @@ type LoggingData = {
|
|
|
178
186
|
};
|
|
179
187
|
type OmittedLoggingData = Omit<LoggingData, "type">;
|
|
180
188
|
//#endregion
|
|
181
|
-
export { AutoFlushEvent, BasicLogConfigurations, DebugMode, LogContract, LogLevel, LogMessage, LoggingData, OmittedLoggingData, RedactCensor, RedactConfig };
|
|
189
|
+
export { AutoFlushEvent, BasicLogConfigurations, ChannelErrorHandler, DebugMode, LogContract, LogLevel, LogMessage, LoggingData, OmittedLoggingData, RedactCensor, RedactConfig };
|
|
182
190
|
//# sourceMappingURL=types.d.mts.map
|
package/esm/types.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../packages/logger/src/types.ts"],"mappings":";;AASA;;;;AAAoB;AAUpB;;;KAVY,QAAA;AAUc;AAQ1B;;;;AAAqB;AAOrB;;AAf0B,KAAd,cAAA;AAAA,KAQA,SAAA;AAOkD;AAuC9D;;;;AAvC8D,KAAlD,YAAA,cAA0B,KAAA,OAAY,IAAY;;;;;;AA2EvC;AAGvB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BuB;AAGvB;;;KAtEY,YAAA;EAuEV;;;;;;;EA/DA,KAAA;EAqEU;;;AACD;AAGX;;EAlEE,IAAA;EAqFU;;;;;;;;;;;;;;EAtEV,WAAA;EAmFS;AAAA;AAUX;;;EAvFE,MAAA,GAAS,YAAY;AAAA;AAAA,KAGX,sBAAA;EAwFO;;;;;EAlFjB,MAAA,GAAS,QAAA;EAiFT;;;EA7EA,UAAA;IACE,IAAA;IACA,IAAA;EAAA;EAmFc;;;EA9EhB,MAAA,IAAU,IAAA,EAAM,WAAA;EA4EhB;;;EAxEA,OAAA,IAAW,IAAA,EAAM,WAAA,KAAgB,OAAA,CAAQ,MAAA;EA0EzB;AAAA;AAGlB;;;;EAtEE,MAAA,GAAS,YAAA;AAAA;AAAA,KAGC,UAAA;EACV,OAAA;EACA,KAAA,EAAO,QAAA;EACP,IAAA;EACA,MAAA;EACA,MAAA;EACA,KAAA;EACA,OAAA,GAAU,MAAM;EAChB,SAAA;AAAA;AAAA,UAGe,WAAA;;;;EAIf,IAAA;;;;EAKA,WAAA;;;;EAKA,QAAA;;;;EAKA,GAAA,CAAI,IAAA,EAAM,WAAA,UAAqB,OAAA;;;;;;;EAQ/B,KAAA,YAAiB,OAAA;;;;EAKjB,SAAA;AAAA;;;;;;;;KAUU,mBAAA,IACV,KAAA,WACA,OAAA,EAAS,WAAA,EACT,KAAA,EAAO,WAAA,YACG,OAAA;AAAA,KAEA,WAAA;EACV,IAAA,EAAM,QAAA;EACN,MAAA;EACA,MAAA;EACA,OAAA;EACA,OAAA,GAAU,MAAM;AAAA;AAAA,KAGN,kBAAA,GAAqB,IAAI,CAAC,WAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture-unhandled-errors.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"capture-unhandled-errors.d.mts","names":[],"sources":["../../../../../../../../packages/logger/src/utils/capture-unhandled-errors.ts"],"mappings":";;AAKA;;KAAY,uBAAA;EAca;AAAA;AAkCzB;;;;AAAkF;;;;;;;EAlChF,uBAAuB;AAAA;;;;;;;;;;;;;;;;;;;;;;;;iBAkCT,4BAAA,CAA6B,OAAqC,GAA5B,uBAA4B"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { log } from "../logger.mjs";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../../packages/logger/src/utils/capture-unhandled-errors.ts
|
|
4
4
|
/**
|
|
5
5
|
* Best-effort budget (ms) for draining async channels (file, Sentry, …) before
|
|
6
6
|
* the forced exit. Bounded so a single stuck channel can't hang the process —
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture-unhandled-errors.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"capture-unhandled-errors.mjs","names":[],"sources":["../../../../../../../../packages/logger/src/utils/capture-unhandled-errors.ts"],"sourcesContent":["import { log } from \"../logger\";\n\n/**\n * Options for {@link captureAnyUnhandledRejection}.\n */\nexport type CaptureUnhandledOptions = {\n /**\n * Take the process down with `process.exit(1)` after an `uncaughtException`\n * is logged. Defaults to `true` — an uncaught exception leaves the process\n * in an undefined state, so Node's own default is to exit non-zero.\n *\n * Registering an `uncaughtException` listener *suppresses* that default, so a\n * listener that only logs turns an unrecoverable crash into a silent\n * `exit 0` (which is how a config file throwing at boot looks like \"the\n * server just stopped\"). Keep this `true` in production so supervisors\n * restart and the failure is never silent. Set it to `false` where the\n * process is expected to recover on its own — e.g. a dev server that reloads\n * via HMR — to log the exception without exiting.\n */\n exitOnUncaughtException?: boolean;\n};\n\n/**\n * Best-effort budget (ms) for draining async channels (file, Sentry, …) before\n * the forced exit. Bounded so a single stuck channel can't hang the process —\n * `process.exit()` skips `beforeExit`, so an `autoFlushOn: [\"beforeExit\"]`\n * handler would not cover this path.\n */\nconst FLUSH_BUDGET_BEFORE_EXIT = 1000;\n\n/**\n * Route Node's process-level failure events through the logger so they land in\n * every configured channel with full stack context, and make an\n * `uncaughtException` loud + visible instead of silently swallowed.\n *\n * Registers one listener for `unhandledRejection` and one for\n * `uncaughtException`; call once at startup after channels are configured.\n *\n * - `uncaughtException` → `log.fatal`, then (by default) `process.exit(1)`.\n * When no terminal channel has been configured yet — the early-boot window a\n * config file throwing at import time falls into — the stack is also written\n * to `console.error`, standing in for the Node default this listener\n * suppresses, so a fatal boot error is never invisible. A configured\n * `ConsoleLog` already prints it, so the fallback is skipped when a terminal\n * channel exists (no double output).\n * - `unhandledRejection` → `log.error` — a rejected promise is a failure but\n * not necessarily process-ending, so it stays at error and never exits.\n *\n * @example\n * log.configure({ channels: [new ConsoleLog(), new FileLog()] });\n * captureAnyUnhandledRejection(); // exits on uncaught\n * captureAnyUnhandledRejection({ exitOnUncaughtException: false }); // dev: log only\n */\nexport function captureAnyUnhandledRejection(options: CaptureUnhandledOptions = {}) {\n const { exitOnUncaughtException = true } = options;\n\n process.on(\"unhandledRejection\", (reason: any) => {\n log.error(\"app\", \"unhandledRejection\", reason);\n });\n\n process.on(\"uncaughtException\", (error) => {\n // Route through the logger first so file/Sentry channels capture it. A\n // crash handler must never itself throw (that would re-enter Node's\n // uncaught path), so the logging call is guarded.\n try {\n log.fatal(\"app\", \"uncaughtException\", error);\n } catch {\n // fall through to the stderr fallback + exit below\n }\n\n // Guarantee terminal visibility. `log.fatal` reaches nothing the user can\n // see when this fires before any terminal channel is configured — exactly\n // the early-boot window a config file throwing at import time falls into,\n // which is how a fatal boot error used to masquerade as a silent\n // `exit 0`. Stand in for the Node default this listener suppressed, but\n // only when no terminal channel already printed it, so a configured\n // `ConsoleLog` is never doubled.\n const hasTerminalChannel = log.channels.some((channel) => channel.terminal !== false);\n\n if (!hasTerminalChannel) {\n console.error(error);\n }\n\n if (!exitOnUncaughtException) {\n return;\n }\n\n // Best-effort, time-bounded flush of async channels, then exit non-zero so\n // process supervisors restart and `warlock start` surfaces the failure.\n void Promise.race([\n log.flush(),\n new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, FLUSH_BUDGET_BEFORE_EXIT);\n timer.unref?.();\n }),\n ]).then(\n () => process.exit(1),\n () => process.exit(1),\n );\n });\n}\n"],"mappings":";;;;;;;;;AA4BA,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;AAyBjC,SAAgB,6BAA6B,UAAmC,CAAC,GAAG;CAClF,MAAM,EAAE,0BAA0B,SAAS;CAE3C,QAAQ,GAAG,uBAAuB,WAAgB;EAChD,IAAI,MAAM,OAAO,sBAAsB,MAAM;CAC/C,CAAC;CAED,QAAQ,GAAG,sBAAsB,UAAU;EAIzC,IAAI;GACF,IAAI,MAAM,OAAO,qBAAqB,KAAK;EAC7C,QAAQ,CAER;EAWA,IAAI,CAFuB,IAAI,SAAS,MAAM,YAAY,QAAQ,aAAa,KAEzD,GACpB,QAAQ,MAAM,KAAK;EAGrB,IAAI,CAAC,yBACH;EAKF,AAAK,QAAQ,KAAK,CAChB,IAAI,MAAM,GACV,IAAI,SAAe,YAAY;GAE7B,AADc,WAAW,SAAS,wBAC9B,CAAC,CAAC,QAAQ;EAChB,CAAC,CACH,CAAC,CAAC,CAAC,WACK,QAAQ,KAAK,CAAC,SACd,QAAQ,KAAK,CAAC,CACtB;CACF,CAAC;AACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clear-message.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"clear-message.d.mts","names":[],"sources":["../../../../../../../../packages/logger/src/utils/clear-message.ts"],"mappings":";;AAGA;;iBAAgB,YAAA,CAAa,OAAY"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clear-message.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"clear-message.mjs","names":[],"sources":["../../../../../../../../packages/logger/src/utils/clear-message.ts"],"sourcesContent":["/**\n * Clear message from any terminal codes\n */\nexport function clearMessage(message: any) {\n if (typeof message !== \"string\") return message;\n\n // eslint-disable-next-line no-control-regex\n return message.replace(/\\u001b[^m]*?m/g, \"\");\n}\n"],"mappings":";;;;AAGA,SAAgB,aAAa,SAAc;CACzC,IAAI,OAAO,YAAY,UAAU,OAAO;CAGxC,OAAO,QAAQ,QAAQ,kBAAkB,EAAE;AAC7C"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region
|
|
1
|
+
//#region ../../packages/logger/src/utils/safe-json-stringify.d.ts
|
|
2
2
|
/**
|
|
3
3
|
* JSON-serialize log payloads safely. Circular refs, BigInt, and repeated
|
|
4
4
|
* non-tree references are handled by `safe-stable-stringify`; functions and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-json-stringify.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"safe-json-stringify.d.mts","names":[],"sources":["../../../../../../../../packages/logger/src/utils/safe-json-stringify.ts"],"mappings":";;AAyCA;;;;AAAgE;;;;iBAAhD,iBAAA,CAAkB,KAAA,WAAgB,KAAc"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { stringify } from "safe-stable-stringify";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../../packages/logger/src/utils/safe-json-stringify.ts
|
|
4
4
|
/**
|
|
5
5
|
* Replacer that surfaces Error data — `name`, `message`, and `stack` are
|
|
6
6
|
* non-enumerable on `Error`, so neither default JSON serialization nor an
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-json-stringify.mjs","names":["safeStableStringify"],"sources":["
|
|
1
|
+
{"version":3,"file":"safe-json-stringify.mjs","names":["safeStableStringify"],"sources":["../../../../../../../../packages/logger/src/utils/safe-json-stringify.ts"],"sourcesContent":["import { stringify as safeStableStringify } from \"safe-stable-stringify\";\n\n/**\n * Replacer that surfaces Error data — `name`, `message`, and `stack` are\n * non-enumerable on `Error`, so neither default JSON serialization nor an\n * object spread captures them (both produce `{}`). They are copied explicitly;\n * the trailing spread then captures any additional enumerable props the caller\n * (or a subclass) attached, such as a `code` field.\n */\nfunction errorReplacer<Value = unknown>(\n _key: string,\n value: Value,\n): Record<string, unknown> | Value {\n if (value instanceof Error) {\n // Spread first, then the explicit Error fields. `name`/`message`/`stack`\n // are non-enumerable on `Error`, so the spread never carries them — the\n // explicit assignments are what surface them. Placing the spread first\n // means those explicit keys legitimately take precedence over any\n // identically-named *enumerable* prop a subclass attached (resolving the\n // duplicate-key warning). Because `safe-stable-stringify` sorts keys, the\n // insertion order here does not affect the serialized bytes.\n return {\n ...value,\n name: value.name,\n message: value.message,\n stack: value.stack,\n };\n }\n\n return value;\n}\n\n/**\n * JSON-serialize log payloads safely. Circular refs, BigInt, and repeated\n * non-tree references are handled by `safe-stable-stringify`; functions and\n * symbols are dropped (standard JSON behavior); Errors are expanded via\n * `errorReplacer`. Class instances serialize as their enumerable props.\n *\n * @example\n * await fs.promises.writeFile(filePath, safeJsonStringify(payload, 2));\n */\nexport function safeJsonStringify(value: unknown, space?: number): string {\n return safeStableStringify(value, errorReplacer, space) ?? \"\";\n}\n"],"mappings":";;;;;;;;;;AASA,SAAS,cACP,MACA,OACiC;CACjC,IAAI,iBAAiB,OAQnB,OAAO;EACL,GAAG;EACH,MAAM,MAAM;EACZ,SAAS,MAAM;EACf,OAAO,MAAM;CACf;CAGF,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,kBAAkB,OAAgB,OAAwB;CACxE,OAAOA,UAAoB,OAAO,eAAe,KAAK,KAAK;AAC7D"}
|