@warlock.js/logger 4.15.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/cjs/index.cjs +312 -38
- package/cjs/index.cjs.map +1 -1
- package/esm/index.d.mts +3 -2
- package/esm/index.mjs +4 -2
- package/esm/logger.mjs +1 -0
- package/esm/logger.mjs.map +1 -1
- package/esm/redact/default-keys.d.mts +63 -0
- package/esm/redact/default-keys.d.mts.map +1 -0
- package/esm/redact/default-keys.mjs +116 -0
- package/esm/redact/default-keys.mjs.map +1 -0
- package/esm/redact/index.mjs +4 -0
- package/esm/redact/redact.d.mts +41 -8
- package/esm/redact/redact.d.mts.map +1 -1
- package/esm/redact/redact.mjs +196 -38
- package/esm/redact/redact.mjs.map +1 -1
- package/esm/types.d.mts +49 -12
- package/esm/types.d.mts.map +1 -1
- package/llms-full.txt +39 -5
- package/llms.txt +1 -1
- package/package.json +3 -3
- package/skills/redact-sensitive-log-fields/SKILL.md +39 -5
package/esm/redact/redact.mjs
CHANGED
|
@@ -1,36 +1,101 @@
|
|
|
1
|
+
import { DEFAULT_REDACT_KEY_SET, normalizeRedactKey } from "./default-keys.mjs";
|
|
2
|
+
|
|
1
3
|
//#region ../logger/src/redact/redact.ts
|
|
4
|
+
const DEFAULT_CENSOR = "[REDACTED]";
|
|
5
|
+
/**
|
|
6
|
+
* Values we copy by reference rather than walking. Expanding these with
|
|
7
|
+
* `Object.keys` would be destructive, not protective — a `Buffer` becomes a
|
|
8
|
+
* multi-thousand-key index map, a `Map`/`Set`/`RegExp` becomes `{}`. Their
|
|
9
|
+
* contents are consequently *not* reachable by redaction; see the residual
|
|
10
|
+
* gaps documented on `applyRedact`.
|
|
11
|
+
*/
|
|
12
|
+
function isOpaque(value) {
|
|
13
|
+
return ArrayBuffer.isView(value) || value instanceof ArrayBuffer || value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet || value instanceof Promise || value instanceof RegExp;
|
|
14
|
+
}
|
|
2
15
|
/**
|
|
3
16
|
* Deep-clone a value with structural fidelity for log entries — handles plain
|
|
4
|
-
* objects, arrays, `Date`, `Error`, and primitives. Anything else
|
|
5
|
-
* by reference
|
|
6
|
-
* and rebuilding e.g. a `Buffer` would
|
|
17
|
+
* objects, arrays, `Date`, `Error`, and primitives. Anything else (buffers,
|
|
18
|
+
* maps, sets, promises, regexes) is copied by reference: we only redact
|
|
19
|
+
* through walkable structures anyway, and rebuilding e.g. a `Buffer` would
|
|
20
|
+
* change semantics.
|
|
7
21
|
*
|
|
8
22
|
* Purpose-built rather than reaching for `structuredClone`: `Error` instances
|
|
9
23
|
* lose their `message`/`stack` under `structuredClone` in some Node versions,
|
|
10
24
|
* and the logger pipeline carries them often.
|
|
25
|
+
*
|
|
26
|
+
* When `redaction` is supplied, keys matching its denylist are censored during
|
|
27
|
+
* the same pass — one traversal, not two.
|
|
11
28
|
*/
|
|
12
|
-
function cloneEntry(value, seen = /* @__PURE__ */ new WeakMap()) {
|
|
29
|
+
function cloneEntry(value, seen = /* @__PURE__ */ new WeakMap(), redaction, trail = []) {
|
|
13
30
|
if (value === null || typeof value !== "object") return value;
|
|
14
|
-
|
|
31
|
+
const asObject = value;
|
|
32
|
+
if (seen.has(asObject)) return seen.get(asObject);
|
|
15
33
|
if (value instanceof Date) return new Date(value.getTime());
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
copy.stack = value.stack;
|
|
19
|
-
copy.name = value.name;
|
|
20
|
-
return copy;
|
|
21
|
-
}
|
|
34
|
+
if (isOpaque(asObject)) return value;
|
|
35
|
+
if (value instanceof Error) return cloneError(value, seen, redaction, trail);
|
|
22
36
|
if (Array.isArray(value)) {
|
|
23
37
|
const arr = [];
|
|
24
|
-
seen.set(
|
|
25
|
-
|
|
38
|
+
seen.set(asObject, arr);
|
|
39
|
+
value.forEach((item, index) => {
|
|
40
|
+
arr.push(cloneEntry(item, seen, redaction, [...trail, String(index)]));
|
|
41
|
+
});
|
|
26
42
|
return arr;
|
|
27
43
|
}
|
|
28
44
|
const out = {};
|
|
29
|
-
seen.set(
|
|
30
|
-
|
|
45
|
+
seen.set(asObject, out);
|
|
46
|
+
copyOwnKeys(value, out, seen, redaction, trail);
|
|
31
47
|
return out;
|
|
32
48
|
}
|
|
33
49
|
/**
|
|
50
|
+
* Copy every own enumerable key from `source` onto `target`, censoring the
|
|
51
|
+
* ones the key denylist matches and recursing into the rest.
|
|
52
|
+
*/
|
|
53
|
+
function copyOwnKeys(source, target, seen, redaction, trail) {
|
|
54
|
+
for (const key of Object.keys(source)) {
|
|
55
|
+
const childTrail = [...trail, key];
|
|
56
|
+
if (redaction && redaction.keys.has(normalizeRedactKey(key)) && !redaction.alreadyCensored.has(childTrail.join("."))) {
|
|
57
|
+
target[key] = applyCensor(source[key], redaction.censor, childTrail);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
target[key] = cloneEntry(source[key], seen, redaction, childTrail);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Clone an `Error`, preserving its own enumerable properties.
|
|
65
|
+
*
|
|
66
|
+
* Those extra properties matter for redaction: HTTP clients (axios, got)
|
|
67
|
+
* attach `.config`/`.request`/`.response` to the errors they throw, and those
|
|
68
|
+
* routinely carry the *outgoing* `Authorization` header of the failed request.
|
|
69
|
+
* Copying them through the walk is what lets the key denylist reach in and
|
|
70
|
+
* censor them — dropping them instead (the pre-4.15.0 behavior) hid the
|
|
71
|
+
* secret only when redaction happened to be configured, and took `.code` and
|
|
72
|
+
* friends with it.
|
|
73
|
+
*
|
|
74
|
+
* `name`/`message`/`stack` are non-enumerable on `Error`, so they are carried
|
|
75
|
+
* over explicitly.
|
|
76
|
+
*/
|
|
77
|
+
function cloneError(value, seen, redaction, trail) {
|
|
78
|
+
let copy;
|
|
79
|
+
try {
|
|
80
|
+
copy = new value.constructor(value.message);
|
|
81
|
+
} catch {
|
|
82
|
+
copy = new Error(value.message);
|
|
83
|
+
}
|
|
84
|
+
for (const [key, source] of [
|
|
85
|
+
["message", value.message],
|
|
86
|
+
["name", value.name],
|
|
87
|
+
["stack", value.stack]
|
|
88
|
+
]) Object.defineProperty(copy, key, {
|
|
89
|
+
value: source,
|
|
90
|
+
writable: true,
|
|
91
|
+
enumerable: false,
|
|
92
|
+
configurable: true
|
|
93
|
+
});
|
|
94
|
+
seen.set(value, copy);
|
|
95
|
+
copyOwnKeys(value, copy, seen, redaction, trail);
|
|
96
|
+
return copy;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
34
99
|
* Apply a single censor decision to a value. String censors are returned
|
|
35
100
|
* verbatim; function censors receive the original value plus the dotted
|
|
36
101
|
* path so callers can implement value-aware redaction (mask all but the
|
|
@@ -45,6 +110,9 @@ function applyCensor(value, censor, path) {
|
|
|
45
110
|
* replacing matched leaves via `censor`. Operates in place — the caller
|
|
46
111
|
* is responsible for cloning before calling.
|
|
47
112
|
*
|
|
113
|
+
* Every censored leaf's dotted path is recorded in `censored` so a following
|
|
114
|
+
* key-denylist pass can leave it alone.
|
|
115
|
+
*
|
|
48
116
|
* Wildcards:
|
|
49
117
|
* - `*` matches exactly one segment (any key on a plain object, any index
|
|
50
118
|
* on an array — stringified for the path that's passed to a function
|
|
@@ -52,58 +120,148 @@ function applyCensor(value, censor, path) {
|
|
|
52
120
|
* - `**` matches zero or more segments greedily; the rest of the pattern
|
|
53
121
|
* is then attempted at the current level and at every descendant.
|
|
54
122
|
*/
|
|
55
|
-
function redactAtPath(target, segments, censor, pathTrail) {
|
|
123
|
+
function redactAtPath(target, segments, censor, pathTrail, censored) {
|
|
56
124
|
if (target === null || typeof target !== "object") return;
|
|
57
125
|
if (segments.length === 0) return;
|
|
58
126
|
const [head, ...rest] = segments;
|
|
59
127
|
if (head === "**") {
|
|
60
|
-
if (rest.length > 0) redactAtPath(target, rest, censor, pathTrail);
|
|
128
|
+
if (rest.length > 0) redactAtPath(target, rest, censor, pathTrail, censored);
|
|
61
129
|
const keys = Array.isArray(target) ? target.map((_, index) => String(index)) : Object.keys(target);
|
|
62
|
-
for (const key of keys) redactAtPath(target[key], segments, censor, [...pathTrail, key]);
|
|
130
|
+
for (const key of keys) redactAtPath(target[key], segments, censor, [...pathTrail, key], censored);
|
|
63
131
|
return;
|
|
64
132
|
}
|
|
65
133
|
const keysToVisit = head === "*" ? Array.isArray(target) ? target.map((_, index) => String(index)) : Object.keys(target) : Array.isArray(target) ? /^\d+$/.test(head) && Number(head) < target.length ? [head] : [] : Object.prototype.hasOwnProperty.call(target, head) ? [head] : [];
|
|
66
|
-
for (const key of keysToVisit) if (rest.length === 0)
|
|
67
|
-
|
|
134
|
+
for (const key of keysToVisit) if (rest.length === 0) {
|
|
135
|
+
const leafTrail = [...pathTrail, key];
|
|
136
|
+
target[key] = applyCensor(target[key], censor, leafTrail);
|
|
137
|
+
censored.add(leafTrail.join("."));
|
|
138
|
+
} else redactAtPath(target[key], rest, censor, [...pathTrail, key], censored);
|
|
68
139
|
}
|
|
69
140
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
141
|
+
* Cheap pre-scan: does this graph contain any denylisted key at all?
|
|
142
|
+
*
|
|
143
|
+
* Lets the default-on key pass stay allocation-free for the overwhelming
|
|
144
|
+
* majority of entries, which carry no secrets — we only pay for a clone when
|
|
145
|
+
* there is actually something to censor. Also keeps `applyRedact`'s
|
|
146
|
+
* "returns the input by reference when nothing changed" contract intact.
|
|
147
|
+
*/
|
|
148
|
+
function hasDenylistedKey(value, keys, seen = /* @__PURE__ */ new WeakSet()) {
|
|
149
|
+
if (value === null || typeof value !== "object") return false;
|
|
150
|
+
const asObject = value;
|
|
151
|
+
if (seen.has(asObject)) return false;
|
|
152
|
+
seen.add(asObject);
|
|
153
|
+
if (value instanceof Date || isOpaque(asObject)) return false;
|
|
154
|
+
if (Array.isArray(value)) return value.some((item) => hasDenylistedKey(item, keys, seen));
|
|
155
|
+
for (const key of Object.keys(value)) {
|
|
156
|
+
if (keys.has(normalizeRedactKey(key))) return true;
|
|
157
|
+
if (hasDenylistedKey(value[key], keys, seen)) return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Cache of resolved key sets, keyed by the config object they came from.
|
|
163
|
+
* Logger-wide and channel configs are long-lived references, so this makes
|
|
164
|
+
* the per-entry cost a single map lookup. Merged configs (rebuilt per entry
|
|
165
|
+
* by `mergeRedact`) fall out of the `WeakMap` on their own.
|
|
166
|
+
*/
|
|
167
|
+
const keySetCache = /* @__PURE__ */ new WeakMap();
|
|
168
|
+
/**
|
|
169
|
+
* Resolve the effective key denylist for a config: the built-in set (unless
|
|
170
|
+
* `defaultKeys: false`) plus any `keys` the application added.
|
|
171
|
+
*
|
|
172
|
+
* Returns `undefined` only when there is nothing to match — i.e. defaults are
|
|
173
|
+
* explicitly off and no custom keys were supplied.
|
|
174
|
+
*/
|
|
175
|
+
function resolveRedactKeys(config) {
|
|
176
|
+
const useDefaults = config?.defaultKeys !== false;
|
|
177
|
+
const extra = config?.keys;
|
|
178
|
+
if (!extra || extra.length === 0) return useDefaults ? DEFAULT_REDACT_KEY_SET : void 0;
|
|
179
|
+
const cached = config && keySetCache.get(config);
|
|
180
|
+
if (cached) return cached;
|
|
181
|
+
const resolved = new Set(useDefaults ? DEFAULT_REDACT_KEY_SET : []);
|
|
182
|
+
for (const key of extra) resolved.add(normalizeRedactKey(key));
|
|
183
|
+
if (config) keySetCache.set(config, resolved);
|
|
184
|
+
return resolved;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Produce a new `LoggingData` with sensitive data censored:
|
|
188
|
+
*
|
|
189
|
+
* 1. every path in `config.paths` (opt-in globs), then
|
|
190
|
+
* 2. every key matching the denylist — the built-in
|
|
191
|
+
* {@link DEFAULT_REDACT_KEYS} plus `config.keys`, at any depth of
|
|
192
|
+
* `context`, `message`, and an `Error`'s own enumerable properties.
|
|
193
|
+
*
|
|
194
|
+
* Step 2 runs **with no config at all**: passing `undefined` still censors
|
|
195
|
+
* `password`, `authorization`, `token`, `apiKey` and friends. Pass
|
|
196
|
+
* `{ defaultKeys: false }` to opt out.
|
|
197
|
+
*
|
|
198
|
+
* Paths run first so a function censor sees the original value rather than a
|
|
199
|
+
* mask; leaves the path pass already censored are skipped by the key pass.
|
|
200
|
+
*
|
|
201
|
+
* The original entry is never mutated — channels and other call sites can
|
|
202
|
+
* hold references to the input safely. Returns the input **by reference**
|
|
203
|
+
* when nothing matched, so the fast path stays allocation-free.
|
|
204
|
+
*
|
|
205
|
+
* ## Residual gaps (by design, documented rather than silently absent)
|
|
73
206
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
207
|
+
* - **Secrets interpolated into a `message` string** (`` `token=${t}` ``)
|
|
208
|
+
* cannot be reached — neither a path nor a key names a substring.
|
|
209
|
+
* - **`Map`/`Set`/`Buffer` contents** are not traversed (see {@link isOpaque}).
|
|
210
|
+
* - **Non-enumerable / getter-backed properties** are not walked, so an HTTP
|
|
211
|
+
* client that exposes request config behind a getter still slips through.
|
|
212
|
+
* Enumerable ones (axios's `.config`, `.response`) *are* covered.
|
|
76
213
|
*/
|
|
77
214
|
function applyRedact(data, config) {
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
215
|
+
const paths = config?.paths ?? [];
|
|
216
|
+
const keys = resolveRedactKeys(config);
|
|
217
|
+
if (paths.length === 0 && !keys) return data;
|
|
218
|
+
const censor = config?.censor ?? DEFAULT_CENSOR;
|
|
219
|
+
let result = data;
|
|
220
|
+
const censored = /* @__PURE__ */ new Set();
|
|
221
|
+
if (paths.length > 0) {
|
|
222
|
+
result = cloneEntry(data);
|
|
223
|
+
for (const pattern of paths) {
|
|
224
|
+
const segments = pattern.split(".").filter((segment) => segment.length > 0);
|
|
225
|
+
if (segments.length === 0) continue;
|
|
226
|
+
redactAtPath(result, segments, censor, [], censored);
|
|
227
|
+
}
|
|
85
228
|
}
|
|
86
|
-
|
|
229
|
+
if (keys && hasDenylistedKey(result, keys)) result = cloneEntry(result, /* @__PURE__ */ new WeakMap(), {
|
|
230
|
+
keys,
|
|
231
|
+
censor,
|
|
232
|
+
alreadyCensored: censored
|
|
233
|
+
});
|
|
234
|
+
return result;
|
|
87
235
|
}
|
|
88
236
|
/**
|
|
89
237
|
* Combine two redact configs into one effective config. Used to merge a
|
|
90
238
|
* channel's additive paths on top of the logger-wide floor.
|
|
91
239
|
*
|
|
92
|
-
* - `paths` are concatenated; duplicates are kept (the matcher
|
|
93
|
-
* them, and de-duping cross-config would mask a developer typo).
|
|
240
|
+
* - `paths` and `keys` are concatenated; duplicates are kept (the matcher
|
|
241
|
+
* tolerates them, and de-duping cross-config would mask a developer typo).
|
|
94
242
|
* - `censor` from the channel wins; falls back to the logger's; falls back
|
|
95
243
|
* to the default `"[REDACTED]"`.
|
|
244
|
+
* - `defaultKeys` follows the additive-only contract: a channel can turn the
|
|
245
|
+
* built-in denylist back *on* (`true`) but can never turn off one the
|
|
246
|
+
* logger-wide floor left enabled. When the channel is silent, the logger's
|
|
247
|
+
* choice is inherited — opting out logger-wide is not quietly undone by
|
|
248
|
+
* any channel that happens to set a `redact` option.
|
|
96
249
|
*/
|
|
97
250
|
function mergeRedact(base, extra) {
|
|
98
251
|
if (!base && !extra) return void 0;
|
|
99
|
-
if (!base) return extra
|
|
252
|
+
if (!base) return extra.defaultKeys === false ? {
|
|
253
|
+
...extra,
|
|
254
|
+
defaultKeys: true
|
|
255
|
+
} : extra;
|
|
100
256
|
if (!extra) return base;
|
|
101
257
|
return {
|
|
102
|
-
paths: [...base.paths, ...extra.paths],
|
|
258
|
+
paths: [...base.paths ?? [], ...extra.paths ?? []],
|
|
259
|
+
keys: [...base.keys ?? [], ...extra.keys ?? []],
|
|
260
|
+
defaultKeys: extra.defaultKeys === true ? true : base.defaultKeys,
|
|
103
261
|
censor: extra.censor ?? base.censor
|
|
104
262
|
};
|
|
105
263
|
}
|
|
106
264
|
|
|
107
265
|
//#endregion
|
|
108
|
-
export { applyRedact, mergeRedact };
|
|
266
|
+
export { applyRedact, mergeRedact, resolveRedactKeys };
|
|
109
267
|
//# sourceMappingURL=redact.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redact.mjs","names":[],"sources":["../../../../../../../logger/src/redact/redact.ts"],"sourcesContent":["import type { LoggingData, RedactCensor, RedactConfig } from \"../types\";\n\n/**\n * Deep-clone a value with structural fidelity for log entries — handles plain\n * objects, arrays, `Date`, `Error`, and primitives. Anything else is copied\n * by reference (we only redact paths through plain objects/arrays anyway,\n * and rebuilding e.g. a `Buffer` would 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 */\nfunction cloneEntry<T>(value: T, seen = new WeakMap<object, any>()): T {\n if (value === null || typeof value !== \"object\") {\n return value;\n }\n\n if (seen.has(value as unknown as object)) {\n return seen.get(value as unknown as object);\n }\n\n if (value instanceof Date) {\n return new Date(value.getTime()) as unknown as T;\n }\n\n if (value instanceof Error) {\n const copy = new (value.constructor as ErrorConstructor)(value.message);\n copy.stack = value.stack;\n copy.name = value.name;\n return copy as unknown as T;\n }\n\n if (Array.isArray(value)) {\n const arr: any[] = [];\n seen.set(value as unknown as object, arr);\n for (const item of value) {\n arr.push(cloneEntry(item, seen));\n }\n return arr as unknown as T;\n }\n\n const out: Record<string, any> = {};\n seen.set(value as unknown as object, out);\n for (const key of Object.keys(value as Record<string, any>)) {\n out[key] = cloneEntry((value as Record<string, any>)[key], seen);\n }\n return out as unknown as T;\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 * 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): 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 === \"**\") {\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);\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]);\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 target[key] = applyCensor(target[key], censor, [...pathTrail, key]);\n } else {\n redactAtPath(target[key], rest, censor, [...pathTrail, key]);\n }\n }\n}\n\n/**\n * Produce a new `LoggingData` with every path in `config.paths` replaced\n * by `config.censor`. The original entry is never mutated — channels and\n * other call sites can hold references to the input safely.\n *\n * No-op (returns the input by reference) when `config` is `undefined` or\n * its `paths` array is empty, so the fast path stays fast.\n */\nexport function applyRedact(\n data: LoggingData,\n config: RedactConfig | undefined,\n): LoggingData {\n if (!config || config.paths.length === 0) {\n return data;\n }\n\n const censor = config.censor ?? \"[REDACTED]\";\n const cloned = cloneEntry(data);\n\n for (const pattern of config.paths) {\n const segments = pattern.split(\".\").filter((segment) => segment.length > 0);\n if (segments.length === 0) continue;\n redactAtPath(cloned, segments, censor, []);\n }\n\n return cloned;\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` are concatenated; duplicates are kept (the matcher tolerates\n * 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 */\nexport function mergeRedact(\n base: RedactConfig | undefined,\n extra: RedactConfig | undefined,\n): RedactConfig | undefined {\n if (!base && !extra) return undefined;\n if (!base) return extra;\n if (!extra) return base;\n\n return {\n paths: [...base.paths, ...extra.paths],\n censor: extra.censor ?? base.censor,\n };\n}\n"],"mappings":"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"}
|
|
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 === \"**\") {\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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"}
|
package/esm/types.d.mts
CHANGED
|
@@ -28,24 +28,36 @@ type RedactCensor = string | ((value: any, path: string) => any);
|
|
|
28
28
|
/**
|
|
29
29
|
* Strip sensitive fields from log entries before they reach a channel.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* Two independent matchers, both applied at the same choke point (so every
|
|
32
|
+
* channel inherits them):
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* 1. **Key denylist** (`keys` + the built-in `DEFAULT_REDACT_KEYS`) — matches
|
|
35
|
+
* by *key name* at any depth, case- and separator-insensitively. **On by
|
|
36
|
+
* default**, with no configuration: `password`, `authorization`, `apiKey`,
|
|
37
|
+
* `token`, `cookie`, … are censored out of the box.
|
|
38
|
+
* 2. **Path globs** (`paths`) — opt-in, dotted patterns evaluated against the
|
|
39
|
+
* `LoggingData` itself (`context.password`, `message.token`). Wildcards:
|
|
40
|
+
* - `*` — matches a single segment (any one key)
|
|
41
|
+
* - `**` — matches zero or more segments (any depth, any key)
|
|
36
42
|
*
|
|
37
43
|
* Configurable in two places:
|
|
38
44
|
*
|
|
39
45
|
* 1. **Logger-wide** via `Logger.configure({ redact })` — applied once before
|
|
40
46
|
* fan-out. This is the security floor; no channel can undo it.
|
|
41
|
-
* 2. **Per channel** via the channel's options. Channel paths are
|
|
42
|
-
* they extend (never replace) the logger-wide list, so a
|
|
43
|
-
* redact more, never less.
|
|
47
|
+
* 2. **Per channel** via the channel's options. Channel paths/keys are
|
|
48
|
+
* *additive*: they extend (never replace) the logger-wide list, so a
|
|
49
|
+
* channel can only redact more, never less.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // Nothing to configure for the common secrets — this is the default:
|
|
53
|
+
* log.info("auth", "login", "ok", { password: "hunter2" });
|
|
54
|
+
* // channel sees { password: "[REDACTED]" }
|
|
44
55
|
*
|
|
45
56
|
* @example
|
|
46
57
|
* logger.configure({
|
|
47
58
|
* redact: {
|
|
48
|
-
*
|
|
59
|
+
* keys: ["internalRef"], // extends the built-in denylist
|
|
60
|
+
* paths: ["context.*.token", "context.headers.authorization"],
|
|
49
61
|
* censor: "[REDACTED]",
|
|
50
62
|
* },
|
|
51
63
|
* });
|
|
@@ -55,10 +67,34 @@ type RedactConfig = {
|
|
|
55
67
|
* Glob path patterns to redact. Paths are evaluated against the full
|
|
56
68
|
* `LoggingData` object — so prefix with `context.` or `message.` to scope
|
|
57
69
|
* to either field.
|
|
70
|
+
*
|
|
71
|
+
* Optional: omit it to rely on key-based redaction alone.
|
|
72
|
+
*/
|
|
73
|
+
paths?: string[];
|
|
74
|
+
/**
|
|
75
|
+
* Extra key names to censor anywhere they appear, in addition to the
|
|
76
|
+
* built-in {@link DEFAULT_REDACT_KEYS}. Matched case- and
|
|
77
|
+
* separator-insensitively on the normalized key, so `"internal_ref"`,
|
|
78
|
+
* `"internalRef"` and `"INTERNAL-REF"` are one entry.
|
|
79
|
+
*/
|
|
80
|
+
keys?: string[];
|
|
81
|
+
/**
|
|
82
|
+
* Set to `false` to drop the built-in secret-key denylist and rely solely
|
|
83
|
+
* on `paths`/`keys`.
|
|
84
|
+
*
|
|
85
|
+
* This is an escape hatch, not a tuning knob — turning it off restores the
|
|
86
|
+
* pre-4.15.0 behavior where a `password` in `context` reaches every sink in
|
|
87
|
+
* cleartext. Prefer narrowing with a function `censor` over disabling.
|
|
88
|
+
*
|
|
89
|
+
* A *channel* may set this to `true` to re-enable defaults the logger-wide
|
|
90
|
+
* config turned off (channels can only redact more, never less); a channel
|
|
91
|
+
* setting it to `false` cannot disable a logger-wide default.
|
|
92
|
+
*
|
|
93
|
+
* @default true
|
|
58
94
|
*/
|
|
59
|
-
|
|
95
|
+
defaultKeys?: boolean;
|
|
60
96
|
/**
|
|
61
|
-
* Replacement applied at each matched path.
|
|
97
|
+
* Replacement applied at each matched path or key.
|
|
62
98
|
*
|
|
63
99
|
* @default "[REDACTED]"
|
|
64
100
|
*/
|
|
@@ -88,8 +124,9 @@ type BasicLogConfigurations = {
|
|
|
88
124
|
context?: (data: LoggingData) => Promise<Record<string, any>>;
|
|
89
125
|
/**
|
|
90
126
|
* Channel-specific redaction. Additive on top of the logger-wide config —
|
|
91
|
-
* the channel's paths extend (never replace) the logger floor. The
|
|
92
|
-
* `censor` here, when omitted, falls back to the logger-wide censor.
|
|
127
|
+
* the channel's paths/keys extend (never replace) the logger floor. The
|
|
128
|
+
* `censor` here, when omitted, falls back to the logger-wide censor. The
|
|
129
|
+
* built-in key denylist applies to every channel whether or not this is set.
|
|
93
130
|
*/
|
|
94
131
|
redact?: RedactConfig;
|
|
95
132
|
};
|
package/esm/types.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../logger/src/types.ts"],"mappings":";;AASA;;;;AAAoB;AAUpB;;;KAVY,QAAA;AAUc;AAQ1B;;;;AAAqB;AAOrB;;AAf0B,KAAd,cAAA;AAAA,KAQA,SAAA;AASkB;
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../logger/src/types.ts"],"mappings":";;AASA;;;;AAAoB;AAUpB;;;KAVY,QAAA;AAUc;AAQ1B;;;;AAAqB;AAOrB;;AAf0B,KAAd,cAAA;AAAA,KAQA,SAAA;AASkB;AAuC9B;;;;AAvC8B,KAFlB,YAAA,cAEN,KAAA,OAAY,IAAY;;;;;;AA2EP;AAGvB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BuB;AAGvB;;;KAtEY,YAAA;EAuEV;;;;;;;EA/DA,KAAA;EAqEU;;;AACD;AAGX;;EAlEE,IAAA;EAqFU;;;;;;;;;;;;;;EAtEV,WAAA;EAmFS;AAAA;AAGX;;;EAhFE,MAAA,GAAS,YAAY;AAAA;AAAA,KAGX,sBAAA;EA+EV;;;;;EAzEA,MAAA,GAAS,QAAA;EA4EO;AAGlB;;EA3EE,UAAA;IACE,IAAA;IACA,IAAA;EAAA;;;;EAKF,MAAA,IAAU,IAAA,EAAM,WAAA;;;;EAIhB,OAAA,IAAW,IAAA,EAAM,WAAA,KAAgB,OAAA,CAAQ,MAAA;;;;;;;EAOzC,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;AAAA,KAGU,WAAA;EACV,IAAA,EAAM,QAAA;EACN,MAAA;EACA,MAAA;EACA,OAAA;EACA,OAAA,GAAU,MAAM;AAAA;AAAA,KAGN,kBAAA,GAAqB,IAAI,CAAC,WAAA"}
|
package/llms-full.txt
CHANGED
|
@@ -822,14 +822,48 @@ If two channels share a `name`, only one is reachable this way — the search re
|
|
|
822
822
|
|
|
823
823
|
---
|
|
824
824
|
name: redact-sensitive-log-fields
|
|
825
|
-
description: 'Strip secrets from log output — two-layer additive redaction via log.configure({redact: {paths}}) (logger floor) + per-channel redact (more
|
|
825
|
+
description: 'Strip secrets from log output — a built-in secret-key denylist on by default (DEFAULT_REDACT_KEYS), plus two-layer additive redaction via log.configure({redact: {paths, keys}}) (logger floor) + per-channel redact (more on top). Dotted glob paths (*, **). Triggers: `redact`, `paths`, `keys`, `defaultKeys`, `censor`, `log.setRedact`, `applyRedact`; "redact passwords in logs", "strip tokens from log output", "hide authorization headers", "scrub PII before logging", "turn off default redaction"; typical import `import { log } from "@warlock.js/logger"`. Skip: filtering — `@warlock.js/logger/filter-log-entries/SKILL.md`; custom sinks — `@warlock.js/logger/write-custom-log-channel/SKILL.md`; competing libs `pino.redact`, `fast-redact`.'
|
|
826
826
|
---
|
|
827
827
|
|
|
828
828
|
# Redaction — keeping secrets out of logs
|
|
829
829
|
|
|
830
|
-
|
|
830
|
+
Three layers: a **built-in key denylist that is on by default**, plus opt-in path globs configured at the logger and/or per channel.
|
|
831
831
|
|
|
832
|
-
##
|
|
832
|
+
## Layer 0 — the default denylist (since 4.15.0, no configuration needed)
|
|
833
|
+
|
|
834
|
+
Common secret **key names** are censored at any depth of `context`, `message`, and an `Error`'s own enumerable properties — before any of your config runs:
|
|
835
|
+
|
|
836
|
+
```ts
|
|
837
|
+
log.error("auth", "login", "failed", { headers: req.headers, body: req.body });
|
|
838
|
+
// context.headers.authorization → "[REDACTED]"
|
|
839
|
+
// context.body.password → "[REDACTED]"
|
|
840
|
+
// context.body.email → untouched
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
Keys are matched **exactly**, on a normalized form (lower-cased, separators stripped) — so one entry covers `apiKey` / `api_key` / `API-KEY` / `x-api-key`. It is not substring matching: `tokenCount` and `passwordUpdatedAt` survive. Read the exact set from `DEFAULT_REDACT_KEYS`.
|
|
844
|
+
|
|
845
|
+
```ts
|
|
846
|
+
import { DEFAULT_REDACT_KEYS } from "@warlock.js/logger";
|
|
847
|
+
|
|
848
|
+
log.configure({
|
|
849
|
+
redact: {
|
|
850
|
+
keys: ["internalRef"], // union with the built-in set
|
|
851
|
+
defaultKeys: false, // opt out of the built-in set entirely
|
|
852
|
+
},
|
|
853
|
+
});
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
`defaultKeys: false` is an escape hatch, not a tuning knob — it restores the pre-4.15.0 behavior where a `password` in `context` reaches every sink in cleartext. Prefer adding a function `censor` if you only need to keep a prefix.
|
|
857
|
+
|
|
858
|
+
**A channel cannot turn the default set off** (it can only add keys, or turn it back *on* if the logger-wide config disabled it) — same additive-only contract as paths, below. And `log.setRedact(undefined)` clears *your* paths, not the default denylist.
|
|
859
|
+
|
|
860
|
+
Not reachable by any layer: secrets interpolated into a `message` string (`` `token=${t}` ``), `Map`/`Set`/`Buffer` contents, and getter-backed or non-enumerable properties.
|
|
861
|
+
|
|
862
|
+
## Layers 1 & 2 — path globs, opt-in
|
|
863
|
+
|
|
864
|
+
For anything the denylist can't name by key — a secret under an app-specific key, or a value you want partially masked rather than blanked.
|
|
865
|
+
|
|
866
|
+
The model in one line:
|
|
833
867
|
|
|
834
868
|
> Logger-wide redaction is the security floor. Per-channel redaction adds more paths. **No channel can ever undo a logger-wide redaction.**
|
|
835
869
|
|
|
@@ -853,7 +887,7 @@ log.configure({
|
|
|
853
887
|
|
|
854
888
|
// runtime equivalent:
|
|
855
889
|
log.setRedact({ paths: ["context.password"] });
|
|
856
|
-
log.setRedact(undefined); // clear
|
|
890
|
+
log.setRedact(undefined); // clear your paths (the default denylist stays on)
|
|
857
891
|
```
|
|
858
892
|
|
|
859
893
|
Every channel sees the redacted entry. Cheap: applied **once** before fan-out; channels share the redacted clone unless they add their own paths.
|
|
@@ -936,7 +970,7 @@ If `message` is a plain object, paths under `message.*` work as expected. If `me
|
|
|
936
970
|
|
|
937
971
|
## Performance notes
|
|
938
972
|
|
|
939
|
-
- **No redact configured** →
|
|
973
|
+
- **No redact configured** → the default denylist still runs (since 4.15.0): a cheap presence scan for a denylisted key on every `log()` call, with the deep clone + censor pass skipped entirely when nothing matches. Only `{ defaultKeys: false }` (no `paths`, no extra `keys`) gets back to zero work.
|
|
940
974
|
- **Logger-wide redact only** → one deep clone + one path-walk per `log()` call, shared by every channel.
|
|
941
975
|
- **Channel adds paths** → that channel re-clones from the original input and runs the merged pass once. Other channels still share the cheaper logger-wide clone.
|
|
942
976
|
- Each path is matched independently; cost grows linearly with `paths.length`.
|
package/llms.txt
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
- [logger-basics](@warlock.js/logger/logger-basics/SKILL.md): (no description)
|
|
14
14
|
- [overview](@warlock.js/logger/overview/SKILL.md): (no description)
|
|
15
15
|
- [pick-log-channel](@warlock.js/logger/pick-log-channel/SKILL.md): (no description)
|
|
16
|
-
- [redact-sensitive-log-fields](@warlock.js/logger/redact-sensitive-log-fields/SKILL.md): Strip secrets from log output — two-layer additive redaction via log.configure({redact: {paths}}) (logger floor) + per-channel redact (more
|
|
16
|
+
- [redact-sensitive-log-fields](@warlock.js/logger/redact-sensitive-log-fields/SKILL.md): Strip secrets from log output — a built-in secret-key denylist on by default (DEFAULT_REDACT_KEYS), plus two-layer additive redaction via log.configure({redact: {paths, keys}}) (logger floor) + per-channel redact (more on top). Dotted glob paths (*, **). Triggers: `redact`, `paths`, `keys`, `defaultKeys`, `censor`, `log.setRedact`, `applyRedact`; "redact passwords in logs", "strip tokens from log output", "hide authorization headers", "scrub PII before logging", "turn off default redaction"; typical import `import { log } from "@warlock.js/logger"`. Skip: filtering — `@warlock.js/logger/filter-log-entries/SKILL.md`; custom sinks — `@warlock.js/logger/write-custom-log-channel/SKILL.md`; competing libs `pino.redact`, `fast-redact`.
|
|
17
17
|
- [ship-logs-to-sentry](@warlock.js/logger/ship-logs-to-sentry/SKILL.md): (no description)
|
|
18
18
|
- [test-logging-code](@warlock.js/logger/test-logging-code/SKILL.md): Test code that touches the logger — silence globally via log.setChannels([]) in setupFiles, assert specific log lines via a capturing LogChannel subclass (prefer it over vi.spyOn — it asserts on delivered entries, not just method calls, and isolates the shared singleton cleanly). Triggers: `log.setChannels`, `LogChannel`, `LoggingData`, `Logger`, `log.channels`; "silence logger in vitest", "assert a log line was emitted", "capture log output in tests", "test code that logs"; typical import `import { log, Logger, LogChannel, type LoggingData } from "@warlock.js/logger"`. Skip: custom sinks — `@warlock.js/logger/write-custom-log-channel/SKILL.md`; filtering — `@warlock.js/logger/filter-log-entries/SKILL.md`; competing `vi.spyOn(console)`, `jest.spyOn`.
|
|
19
19
|
- [use-log-helpers](@warlock.js/logger/use-log-helpers/SKILL.md): Two DX shortcuts on every Logger — log.assert(condition, module, action, message, context?) logs an error when condition is falsy (free on the happy path), log.timer(module, action) returns an end-function emitting an info entry with measured duration. Triggers: `log.assert`, `log.timer`, `durationMs`; "assert an invariant via logger", "measure how long an operation took", "time a request", "log operation duration"; typical import `import { log } from "@warlock.js/logger"`. Skip: basics — `@warlock.js/logger/logger-basics/SKILL.md`; filtering — `@warlock.js/logger/filter-log-entries/SKILL.md`; competing `console.assert`, `console.time`, `console.timeEnd`, `perf_hooks.performance.now`.
|
package/package.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"description": "A powerful logging system for messages and errors in nodejs.",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@mongez/copper": "^2.1.2",
|
|
6
|
-
"@mongez/reinforcements": "^
|
|
7
|
-
"@warlock.js/fs": "
|
|
6
|
+
"@mongez/reinforcements": "^4.0.1",
|
|
7
|
+
"@warlock.js/fs": "5.0.0",
|
|
8
8
|
"dayjs": "^1.11.9",
|
|
9
9
|
"safe-stable-stringify": "^2.5.0"
|
|
10
10
|
},
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"author": "hassanzohdy",
|
|
31
31
|
"license": "MIT",
|
|
32
|
-
"version": "
|
|
32
|
+
"version": "5.0.0",
|
|
33
33
|
"main": "./cjs/index.cjs",
|
|
34
34
|
"module": "./esm/index.mjs",
|
|
35
35
|
"types": "./esm/index.d.mts",
|