@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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@ All notable changes to `@warlock.js/logger` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
6
|
|
|
7
|
+
## 5.0.0 - 2026-08-25
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
|
|
12
|
+
|
|
13
|
+
## 4.16.0 - 2026-08-18
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- **Secrets are now redacted by default. This is a behavior change — logs that previously showed these values in cleartext will now show `[REDACTED]`.** Redaction used to be a *tool* (`redact.paths`, entirely opt-in): unless an application configured it, a `password` in `context`, an `authorization` header, or an `apiKey` on a logged `Error` reached every sink — console, log file, JSON log file, Sentry — verbatim. Protection existed only where every call site had been configured correctly, with no signal when one hadn't. It is now a *default*.
|
|
18
|
+
|
|
19
|
+
A built-in denylist (`DEFAULT_REDACT_KEYS`) censors matching keys at any depth of `context`, `message`, and an `Error`'s own enumerable properties, applied at the same logger-wide choke point as `redact.paths` — so every channel inherits it, including custom ones. The set covers passwords, shared secrets, tokens (access/refresh/id/CSRF/session), API and private keys, HTTP credential headers (`authorization`, `x-api-key`, `cookie`, `set-cookie`), session IDs, connection strings, and high-sensitivity financial PII (card numbers, CVV, SSN, IBAN).
|
|
20
|
+
|
|
21
|
+
Keys are matched **exactly**, after normalizing to lower-case with separators stripped — so one entry covers `apiKey` / `api_key` / `API-KEY` / `x-api-key`. Matching is deliberately not substring-based: `*token*` would also blank `tokenCount` and `passwordUpdatedAt`, which is silent data loss in the one place engineers look when something is broken. The trade is that unusual spellings are missed, which is what `redact.keys` is for.
|
|
22
|
+
|
|
23
|
+
Two new knobs on `RedactConfig`, both usable logger-wide or per channel:
|
|
24
|
+
|
|
25
|
+
- `keys: string[]` — extra key names, unioned with the built-in set.
|
|
26
|
+
- `defaultKeys: false` — opt out of the built-in set entirely (restores the pre-4.15.0 behavior). An escape hatch, not a tuning knob; prefer a function `censor` if you need to keep a prefix.
|
|
27
|
+
|
|
28
|
+
Existing `redact.paths` config is untouched and keeps working — paths and keys are independent matchers, and both apply. `paths` is now optional, so `redact: { keys: [...] }` alone is valid. The additive-only contract still holds in both directions: a channel can add keys or re-enable defaults, but **cannot** switch off a default the logger-wide config left on; conversely, a logger-wide `defaultKeys: false` is inherited rather than silently re-enabled by any channel that happens to set a `redact` option.
|
|
29
|
+
|
|
30
|
+
Paths are evaluated before keys, so a function `censor` on an explicit path still receives the original value rather than a mask, and that leaf is not censored twice.
|
|
31
|
+
|
|
32
|
+
Known residual gaps, unchanged by this release and documented on `applyRedact`: secrets interpolated into a `message` *string* (`` `token=${t}` ``) cannot be reached by either matcher; `Map` / `Set` / `Buffer` contents are not traversed; and properties exposed only via getters or non-enumerable descriptors are not walked, so an HTTP client that hides its request config behind a getter can still slip through. Enumerable ones — axios's `.config` / `.response`, which carry the outgoing `Authorization` header — *are* now covered.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Cloning a log entry for redaction no longer discards an `Error`'s own enumerable properties. Previously, configuring `redact` at all silently reduced every logged `Error` to `message` / `stack` / `name` — dropping `.code` and friends as an unadvertised side effect, and putting `.config.headers.authorization` permanently out of reach of any path pattern. Those properties are now carried through the clone (and censored by the key denylist above). An `Error` subclass whose constructor takes a non-string argument also keeps its `message` instead of being rebuilt as an empty one.
|
|
37
|
+
- The redaction clone no longer expands buffers, typed arrays, `Map`, `Set`, `Promise`, or `RegExp` into plain objects — matching what the code already documented. A `Buffer` in `context` had been rebuilt as a multi-thousand-key index map.
|
|
38
|
+
|
|
39
|
+
### Dependencies
|
|
40
|
+
|
|
41
|
+
- Bumped `@mongez/reinforcements` to `^4.0.1`. The major makes `Random.string/nanoid/id/token/uuid` CSPRNG-backed (WebCrypto) and removes `Random.seed()` support. This package uses `Random.string(32)` only for the non-security `logger-<id>` instance identifier; audited for `Random.seed(` with no hits, so no code changes were needed.
|
|
42
|
+
|
|
7
43
|
## 4.12.0
|
|
8
44
|
|
|
9
45
|
### Changed
|
package/cjs/index.cjs
CHANGED
|
@@ -822,40 +822,217 @@ var SentryLog = class extends LogChannel {
|
|
|
822
822
|
}
|
|
823
823
|
};
|
|
824
824
|
|
|
825
|
+
//#endregion
|
|
826
|
+
//#region ../logger/src/redact/default-keys.ts
|
|
827
|
+
/**
|
|
828
|
+
* Built-in secret-key denylist — the redaction floor that applies with no
|
|
829
|
+
* configuration at all.
|
|
830
|
+
*
|
|
831
|
+
* ## Why a default list exists
|
|
832
|
+
*
|
|
833
|
+
* Path-based `redact.paths` is a *tool*; it only protects an application whose
|
|
834
|
+
* every call site was configured correctly, and nothing warns you when one
|
|
835
|
+
* wasn't. The overwhelmingly common leak is mundane and needs no attacker
|
|
836
|
+
* input: `log.error("auth", "login", "failed", { headers: req.headers, body:
|
|
837
|
+
* req.body })` puts a live bearer token and a cleartext password into every
|
|
838
|
+
* sink. A key denylist that runs by default turns redaction from a tool into a
|
|
839
|
+
* default protection.
|
|
840
|
+
*
|
|
841
|
+
* ## Matching semantics — exact, on a normalized key
|
|
842
|
+
*
|
|
843
|
+
* Keys are compared after {@link normalizeRedactKey}: lower-cased with every
|
|
844
|
+
* non-alphanumeric character removed. So one entry covers every spelling
|
|
845
|
+
* convention a codebase might use —
|
|
846
|
+
* `apiKey` / `api_key` / `API-KEY` / `apikey` all normalize to `apikey`.
|
|
847
|
+
*
|
|
848
|
+
* The match is **exact on the normalized key, not a substring**. That is a
|
|
849
|
+
* deliberate trade: substring matching (`*token*`) would also swallow
|
|
850
|
+
* `tokenCount`, `passwordUpdatedAt`, `secretsLoaded` — silent, hard-to-debug
|
|
851
|
+
* data loss in the one place engineers look when something is broken. An exact
|
|
852
|
+
* list is auditable: you can read it and know precisely what disappears. The
|
|
853
|
+
* cost is that unusual spellings are missed, which is why the list carries the
|
|
854
|
+
* concrete wire-format variants that actually show up in HTTP headers
|
|
855
|
+
* (`x-api-key`, `proxy-authorization`, `set-cookie`) and why apps can extend it
|
|
856
|
+
* with `redact.keys`.
|
|
857
|
+
*
|
|
858
|
+
* ## What is deliberately NOT here
|
|
859
|
+
*
|
|
860
|
+
* `key`, `auth`, `hash`, `signature`, `salt`, `id` — all too generic. Each
|
|
861
|
+
* would redact far more non-secret fields than secret ones (`key` alone would
|
|
862
|
+
* blank out every `key` in a keyed collection). Applications that want them
|
|
863
|
+
* add them via `redact.keys`.
|
|
864
|
+
*/
|
|
865
|
+
/**
|
|
866
|
+
* Normalize a key for denylist comparison: lower-case, strip every
|
|
867
|
+
* non-alphanumeric character. Collapses `apiKey`, `api_key`, `API-KEY`,
|
|
868
|
+
* `Api Key` and `apikey` onto a single entry.
|
|
869
|
+
*/
|
|
870
|
+
function normalizeRedactKey(key) {
|
|
871
|
+
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* The default denylist, in source spelling. Anything whose normalized key
|
|
875
|
+
* matches one of these is replaced by the configured censor (`"[REDACTED]"`
|
|
876
|
+
* by default) at any depth of `context`, `message`, or an `Error`'s own
|
|
877
|
+
* enumerable properties.
|
|
878
|
+
*
|
|
879
|
+
* Exported so applications can inspect, log, or build on the exact set they
|
|
880
|
+
* are getting rather than guessing at it.
|
|
881
|
+
*/
|
|
882
|
+
const DEFAULT_REDACT_KEYS = [
|
|
883
|
+
"password",
|
|
884
|
+
"passwd",
|
|
885
|
+
"pwd",
|
|
886
|
+
"passphrase",
|
|
887
|
+
"passwordHash",
|
|
888
|
+
"currentPassword",
|
|
889
|
+
"newPassword",
|
|
890
|
+
"passwordConfirmation",
|
|
891
|
+
"secret",
|
|
892
|
+
"clientSecret",
|
|
893
|
+
"appSecret",
|
|
894
|
+
"apiSecret",
|
|
895
|
+
"secretKey",
|
|
896
|
+
"token",
|
|
897
|
+
"accessToken",
|
|
898
|
+
"refreshToken",
|
|
899
|
+
"idToken",
|
|
900
|
+
"authToken",
|
|
901
|
+
"apiToken",
|
|
902
|
+
"bearerToken",
|
|
903
|
+
"csrfToken",
|
|
904
|
+
"sessionToken",
|
|
905
|
+
"resetToken",
|
|
906
|
+
"verificationToken",
|
|
907
|
+
"jwt",
|
|
908
|
+
"otp",
|
|
909
|
+
"apiKey",
|
|
910
|
+
"privateKey",
|
|
911
|
+
"encryptionKey",
|
|
912
|
+
"signingKey",
|
|
913
|
+
"authorization",
|
|
914
|
+
"proxyAuthorization",
|
|
915
|
+
"x-api-key",
|
|
916
|
+
"x-auth-token",
|
|
917
|
+
"cookie",
|
|
918
|
+
"set-cookie",
|
|
919
|
+
"sessionId",
|
|
920
|
+
"credentials",
|
|
921
|
+
"credential",
|
|
922
|
+
"connectionString",
|
|
923
|
+
"creditCard",
|
|
924
|
+
"creditCardNumber",
|
|
925
|
+
"cardNumber",
|
|
926
|
+
"cvv",
|
|
927
|
+
"cvc",
|
|
928
|
+
"ssn",
|
|
929
|
+
"socialSecurityNumber",
|
|
930
|
+
"iban",
|
|
931
|
+
"taxId"
|
|
932
|
+
];
|
|
933
|
+
/**
|
|
934
|
+
* The default denylist, pre-normalized. Frozen at module load so the hot path
|
|
935
|
+
* never rebuilds it.
|
|
936
|
+
*/
|
|
937
|
+
const DEFAULT_REDACT_KEY_SET = new Set(DEFAULT_REDACT_KEYS.map(normalizeRedactKey));
|
|
938
|
+
|
|
825
939
|
//#endregion
|
|
826
940
|
//#region ../logger/src/redact/redact.ts
|
|
941
|
+
const DEFAULT_CENSOR = "[REDACTED]";
|
|
942
|
+
/**
|
|
943
|
+
* Values we copy by reference rather than walking. Expanding these with
|
|
944
|
+
* `Object.keys` would be destructive, not protective — a `Buffer` becomes a
|
|
945
|
+
* multi-thousand-key index map, a `Map`/`Set`/`RegExp` becomes `{}`. Their
|
|
946
|
+
* contents are consequently *not* reachable by redaction; see the residual
|
|
947
|
+
* gaps documented on `applyRedact`.
|
|
948
|
+
*/
|
|
949
|
+
function isOpaque(value) {
|
|
950
|
+
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;
|
|
951
|
+
}
|
|
827
952
|
/**
|
|
828
953
|
* Deep-clone a value with structural fidelity for log entries — handles plain
|
|
829
|
-
* objects, arrays, `Date`, `Error`, and primitives. Anything else
|
|
830
|
-
* by reference
|
|
831
|
-
* and rebuilding e.g. a `Buffer` would
|
|
954
|
+
* objects, arrays, `Date`, `Error`, and primitives. Anything else (buffers,
|
|
955
|
+
* maps, sets, promises, regexes) is copied by reference: we only redact
|
|
956
|
+
* through walkable structures anyway, and rebuilding e.g. a `Buffer` would
|
|
957
|
+
* change semantics.
|
|
832
958
|
*
|
|
833
959
|
* Purpose-built rather than reaching for `structuredClone`: `Error` instances
|
|
834
960
|
* lose their `message`/`stack` under `structuredClone` in some Node versions,
|
|
835
961
|
* and the logger pipeline carries them often.
|
|
962
|
+
*
|
|
963
|
+
* When `redaction` is supplied, keys matching its denylist are censored during
|
|
964
|
+
* the same pass — one traversal, not two.
|
|
836
965
|
*/
|
|
837
|
-
function cloneEntry(value, seen = /* @__PURE__ */ new WeakMap()) {
|
|
966
|
+
function cloneEntry(value, seen = /* @__PURE__ */ new WeakMap(), redaction, trail = []) {
|
|
838
967
|
if (value === null || typeof value !== "object") return value;
|
|
839
|
-
|
|
968
|
+
const asObject = value;
|
|
969
|
+
if (seen.has(asObject)) return seen.get(asObject);
|
|
840
970
|
if (value instanceof Date) return new Date(value.getTime());
|
|
841
|
-
if (
|
|
842
|
-
|
|
843
|
-
copy.stack = value.stack;
|
|
844
|
-
copy.name = value.name;
|
|
845
|
-
return copy;
|
|
846
|
-
}
|
|
971
|
+
if (isOpaque(asObject)) return value;
|
|
972
|
+
if (value instanceof Error) return cloneError(value, seen, redaction, trail);
|
|
847
973
|
if (Array.isArray(value)) {
|
|
848
974
|
const arr = [];
|
|
849
|
-
seen.set(
|
|
850
|
-
|
|
975
|
+
seen.set(asObject, arr);
|
|
976
|
+
value.forEach((item, index) => {
|
|
977
|
+
arr.push(cloneEntry(item, seen, redaction, [...trail, String(index)]));
|
|
978
|
+
});
|
|
851
979
|
return arr;
|
|
852
980
|
}
|
|
853
981
|
const out = {};
|
|
854
|
-
seen.set(
|
|
855
|
-
|
|
982
|
+
seen.set(asObject, out);
|
|
983
|
+
copyOwnKeys(value, out, seen, redaction, trail);
|
|
856
984
|
return out;
|
|
857
985
|
}
|
|
858
986
|
/**
|
|
987
|
+
* Copy every own enumerable key from `source` onto `target`, censoring the
|
|
988
|
+
* ones the key denylist matches and recursing into the rest.
|
|
989
|
+
*/
|
|
990
|
+
function copyOwnKeys(source, target, seen, redaction, trail) {
|
|
991
|
+
for (const key of Object.keys(source)) {
|
|
992
|
+
const childTrail = [...trail, key];
|
|
993
|
+
if (redaction && redaction.keys.has(normalizeRedactKey(key)) && !redaction.alreadyCensored.has(childTrail.join("."))) {
|
|
994
|
+
target[key] = applyCensor(source[key], redaction.censor, childTrail);
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
target[key] = cloneEntry(source[key], seen, redaction, childTrail);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Clone an `Error`, preserving its own enumerable properties.
|
|
1002
|
+
*
|
|
1003
|
+
* Those extra properties matter for redaction: HTTP clients (axios, got)
|
|
1004
|
+
* attach `.config`/`.request`/`.response` to the errors they throw, and those
|
|
1005
|
+
* routinely carry the *outgoing* `Authorization` header of the failed request.
|
|
1006
|
+
* Copying them through the walk is what lets the key denylist reach in and
|
|
1007
|
+
* censor them — dropping them instead (the pre-4.15.0 behavior) hid the
|
|
1008
|
+
* secret only when redaction happened to be configured, and took `.code` and
|
|
1009
|
+
* friends with it.
|
|
1010
|
+
*
|
|
1011
|
+
* `name`/`message`/`stack` are non-enumerable on `Error`, so they are carried
|
|
1012
|
+
* over explicitly.
|
|
1013
|
+
*/
|
|
1014
|
+
function cloneError(value, seen, redaction, trail) {
|
|
1015
|
+
let copy;
|
|
1016
|
+
try {
|
|
1017
|
+
copy = new value.constructor(value.message);
|
|
1018
|
+
} catch {
|
|
1019
|
+
copy = new Error(value.message);
|
|
1020
|
+
}
|
|
1021
|
+
for (const [key, source] of [
|
|
1022
|
+
["message", value.message],
|
|
1023
|
+
["name", value.name],
|
|
1024
|
+
["stack", value.stack]
|
|
1025
|
+
]) Object.defineProperty(copy, key, {
|
|
1026
|
+
value: source,
|
|
1027
|
+
writable: true,
|
|
1028
|
+
enumerable: false,
|
|
1029
|
+
configurable: true
|
|
1030
|
+
});
|
|
1031
|
+
seen.set(value, copy);
|
|
1032
|
+
copyOwnKeys(value, copy, seen, redaction, trail);
|
|
1033
|
+
return copy;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
859
1036
|
* Apply a single censor decision to a value. String censors are returned
|
|
860
1037
|
* verbatim; function censors receive the original value plus the dotted
|
|
861
1038
|
* path so callers can implement value-aware redaction (mask all but the
|
|
@@ -870,6 +1047,9 @@ function applyCensor(value, censor, path) {
|
|
|
870
1047
|
* replacing matched leaves via `censor`. Operates in place — the caller
|
|
871
1048
|
* is responsible for cloning before calling.
|
|
872
1049
|
*
|
|
1050
|
+
* Every censored leaf's dotted path is recorded in `censored` so a following
|
|
1051
|
+
* key-denylist pass can leave it alone.
|
|
1052
|
+
*
|
|
873
1053
|
* Wildcards:
|
|
874
1054
|
* - `*` matches exactly one segment (any key on a plain object, any index
|
|
875
1055
|
* on an array — stringified for the path that's passed to a function
|
|
@@ -877,54 +1057,144 @@ function applyCensor(value, censor, path) {
|
|
|
877
1057
|
* - `**` matches zero or more segments greedily; the rest of the pattern
|
|
878
1058
|
* is then attempted at the current level and at every descendant.
|
|
879
1059
|
*/
|
|
880
|
-
function redactAtPath(target, segments, censor, pathTrail) {
|
|
1060
|
+
function redactAtPath(target, segments, censor, pathTrail, censored) {
|
|
881
1061
|
if (target === null || typeof target !== "object") return;
|
|
882
1062
|
if (segments.length === 0) return;
|
|
883
1063
|
const [head, ...rest] = segments;
|
|
884
1064
|
if (head === "**") {
|
|
885
|
-
if (rest.length > 0) redactAtPath(target, rest, censor, pathTrail);
|
|
1065
|
+
if (rest.length > 0) redactAtPath(target, rest, censor, pathTrail, censored);
|
|
886
1066
|
const keys = Array.isArray(target) ? target.map((_, index) => String(index)) : Object.keys(target);
|
|
887
|
-
for (const key of keys) redactAtPath(target[key], segments, censor, [...pathTrail, key]);
|
|
1067
|
+
for (const key of keys) redactAtPath(target[key], segments, censor, [...pathTrail, key], censored);
|
|
888
1068
|
return;
|
|
889
1069
|
}
|
|
890
1070
|
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] : [];
|
|
891
|
-
for (const key of keysToVisit) if (rest.length === 0)
|
|
892
|
-
|
|
1071
|
+
for (const key of keysToVisit) if (rest.length === 0) {
|
|
1072
|
+
const leafTrail = [...pathTrail, key];
|
|
1073
|
+
target[key] = applyCensor(target[key], censor, leafTrail);
|
|
1074
|
+
censored.add(leafTrail.join("."));
|
|
1075
|
+
} else redactAtPath(target[key], rest, censor, [...pathTrail, key], censored);
|
|
893
1076
|
}
|
|
894
1077
|
/**
|
|
895
|
-
*
|
|
896
|
-
* by `config.censor`. The original entry is never mutated — channels and
|
|
897
|
-
* other call sites can hold references to the input safely.
|
|
1078
|
+
* Cheap pre-scan: does this graph contain any denylisted key at all?
|
|
898
1079
|
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
1080
|
+
* Lets the default-on key pass stay allocation-free for the overwhelming
|
|
1081
|
+
* majority of entries, which carry no secrets — we only pay for a clone when
|
|
1082
|
+
* there is actually something to censor. Also keeps `applyRedact`'s
|
|
1083
|
+
* "returns the input by reference when nothing changed" contract intact.
|
|
1084
|
+
*/
|
|
1085
|
+
function hasDenylistedKey(value, keys, seen = /* @__PURE__ */ new WeakSet()) {
|
|
1086
|
+
if (value === null || typeof value !== "object") return false;
|
|
1087
|
+
const asObject = value;
|
|
1088
|
+
if (seen.has(asObject)) return false;
|
|
1089
|
+
seen.add(asObject);
|
|
1090
|
+
if (value instanceof Date || isOpaque(asObject)) return false;
|
|
1091
|
+
if (Array.isArray(value)) return value.some((item) => hasDenylistedKey(item, keys, seen));
|
|
1092
|
+
for (const key of Object.keys(value)) {
|
|
1093
|
+
if (keys.has(normalizeRedactKey(key))) return true;
|
|
1094
|
+
if (hasDenylistedKey(value[key], keys, seen)) return true;
|
|
1095
|
+
}
|
|
1096
|
+
return false;
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Cache of resolved key sets, keyed by the config object they came from.
|
|
1100
|
+
* Logger-wide and channel configs are long-lived references, so this makes
|
|
1101
|
+
* the per-entry cost a single map lookup. Merged configs (rebuilt per entry
|
|
1102
|
+
* by `mergeRedact`) fall out of the `WeakMap` on their own.
|
|
1103
|
+
*/
|
|
1104
|
+
const keySetCache = /* @__PURE__ */ new WeakMap();
|
|
1105
|
+
/**
|
|
1106
|
+
* Resolve the effective key denylist for a config: the built-in set (unless
|
|
1107
|
+
* `defaultKeys: false`) plus any `keys` the application added.
|
|
1108
|
+
*
|
|
1109
|
+
* Returns `undefined` only when there is nothing to match — i.e. defaults are
|
|
1110
|
+
* explicitly off and no custom keys were supplied.
|
|
1111
|
+
*/
|
|
1112
|
+
function resolveRedactKeys(config) {
|
|
1113
|
+
const useDefaults = config?.defaultKeys !== false;
|
|
1114
|
+
const extra = config?.keys;
|
|
1115
|
+
if (!extra || extra.length === 0) return useDefaults ? DEFAULT_REDACT_KEY_SET : void 0;
|
|
1116
|
+
const cached = config && keySetCache.get(config);
|
|
1117
|
+
if (cached) return cached;
|
|
1118
|
+
const resolved = new Set(useDefaults ? DEFAULT_REDACT_KEY_SET : []);
|
|
1119
|
+
for (const key of extra) resolved.add(normalizeRedactKey(key));
|
|
1120
|
+
if (config) keySetCache.set(config, resolved);
|
|
1121
|
+
return resolved;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Produce a new `LoggingData` with sensitive data censored:
|
|
1125
|
+
*
|
|
1126
|
+
* 1. every path in `config.paths` (opt-in globs), then
|
|
1127
|
+
* 2. every key matching the denylist — the built-in
|
|
1128
|
+
* {@link DEFAULT_REDACT_KEYS} plus `config.keys`, at any depth of
|
|
1129
|
+
* `context`, `message`, and an `Error`'s own enumerable properties.
|
|
1130
|
+
*
|
|
1131
|
+
* Step 2 runs **with no config at all**: passing `undefined` still censors
|
|
1132
|
+
* `password`, `authorization`, `token`, `apiKey` and friends. Pass
|
|
1133
|
+
* `{ defaultKeys: false }` to opt out.
|
|
1134
|
+
*
|
|
1135
|
+
* Paths run first so a function censor sees the original value rather than a
|
|
1136
|
+
* mask; leaves the path pass already censored are skipped by the key pass.
|
|
1137
|
+
*
|
|
1138
|
+
* The original entry is never mutated — channels and other call sites can
|
|
1139
|
+
* hold references to the input safely. Returns the input **by reference**
|
|
1140
|
+
* when nothing matched, so the fast path stays allocation-free.
|
|
1141
|
+
*
|
|
1142
|
+
* ## Residual gaps (by design, documented rather than silently absent)
|
|
1143
|
+
*
|
|
1144
|
+
* - **Secrets interpolated into a `message` string** (`` `token=${t}` ``)
|
|
1145
|
+
* cannot be reached — neither a path nor a key names a substring.
|
|
1146
|
+
* - **`Map`/`Set`/`Buffer` contents** are not traversed (see {@link isOpaque}).
|
|
1147
|
+
* - **Non-enumerable / getter-backed properties** are not walked, so an HTTP
|
|
1148
|
+
* client that exposes request config behind a getter still slips through.
|
|
1149
|
+
* Enumerable ones (axios's `.config`, `.response`) *are* covered.
|
|
901
1150
|
*/
|
|
902
1151
|
function applyRedact(data, config) {
|
|
903
|
-
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1152
|
+
const paths = config?.paths ?? [];
|
|
1153
|
+
const keys = resolveRedactKeys(config);
|
|
1154
|
+
if (paths.length === 0 && !keys) return data;
|
|
1155
|
+
const censor = config?.censor ?? DEFAULT_CENSOR;
|
|
1156
|
+
let result = data;
|
|
1157
|
+
const censored = /* @__PURE__ */ new Set();
|
|
1158
|
+
if (paths.length > 0) {
|
|
1159
|
+
result = cloneEntry(data);
|
|
1160
|
+
for (const pattern of paths) {
|
|
1161
|
+
const segments = pattern.split(".").filter((segment) => segment.length > 0);
|
|
1162
|
+
if (segments.length === 0) continue;
|
|
1163
|
+
redactAtPath(result, segments, censor, [], censored);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (keys && hasDenylistedKey(result, keys)) result = cloneEntry(result, /* @__PURE__ */ new WeakMap(), {
|
|
1167
|
+
keys,
|
|
1168
|
+
censor,
|
|
1169
|
+
alreadyCensored: censored
|
|
1170
|
+
});
|
|
1171
|
+
return result;
|
|
912
1172
|
}
|
|
913
1173
|
/**
|
|
914
1174
|
* Combine two redact configs into one effective config. Used to merge a
|
|
915
1175
|
* channel's additive paths on top of the logger-wide floor.
|
|
916
1176
|
*
|
|
917
|
-
* - `paths` are concatenated; duplicates are kept (the matcher
|
|
918
|
-
* them, and de-duping cross-config would mask a developer typo).
|
|
1177
|
+
* - `paths` and `keys` are concatenated; duplicates are kept (the matcher
|
|
1178
|
+
* tolerates them, and de-duping cross-config would mask a developer typo).
|
|
919
1179
|
* - `censor` from the channel wins; falls back to the logger's; falls back
|
|
920
1180
|
* to the default `"[REDACTED]"`.
|
|
1181
|
+
* - `defaultKeys` follows the additive-only contract: a channel can turn the
|
|
1182
|
+
* built-in denylist back *on* (`true`) but can never turn off one the
|
|
1183
|
+
* logger-wide floor left enabled. When the channel is silent, the logger's
|
|
1184
|
+
* choice is inherited — opting out logger-wide is not quietly undone by
|
|
1185
|
+
* any channel that happens to set a `redact` option.
|
|
921
1186
|
*/
|
|
922
1187
|
function mergeRedact(base, extra) {
|
|
923
1188
|
if (!base && !extra) return void 0;
|
|
924
|
-
if (!base) return extra
|
|
1189
|
+
if (!base) return extra.defaultKeys === false ? {
|
|
1190
|
+
...extra,
|
|
1191
|
+
defaultKeys: true
|
|
1192
|
+
} : extra;
|
|
925
1193
|
if (!extra) return base;
|
|
926
1194
|
return {
|
|
927
|
-
paths: [...base.paths, ...extra.paths],
|
|
1195
|
+
paths: [...base.paths ?? [], ...extra.paths ?? []],
|
|
1196
|
+
keys: [...base.keys ?? [], ...extra.keys ?? []],
|
|
1197
|
+
defaultKeys: extra.defaultKeys === true ? true : base.defaultKeys,
|
|
928
1198
|
censor: extra.censor ?? base.censor
|
|
929
1199
|
};
|
|
930
1200
|
}
|
|
@@ -1308,6 +1578,8 @@ function captureAnyUnhandledRejection(options = {}) {
|
|
|
1308
1578
|
|
|
1309
1579
|
//#endregion
|
|
1310
1580
|
exports.ConsoleLog = ConsoleLog;
|
|
1581
|
+
exports.DEFAULT_REDACT_KEYS = DEFAULT_REDACT_KEYS;
|
|
1582
|
+
exports.DEFAULT_REDACT_KEY_SET = DEFAULT_REDACT_KEY_SET;
|
|
1311
1583
|
exports.FileLog = FileLog;
|
|
1312
1584
|
exports.JSONFileLog = JSONFileLog;
|
|
1313
1585
|
exports.LogChannel = LogChannel;
|
|
@@ -1318,5 +1590,7 @@ exports.captureAnyUnhandledRejection = captureAnyUnhandledRejection;
|
|
|
1318
1590
|
exports.clearMessage = clearMessage;
|
|
1319
1591
|
exports.log = log;
|
|
1320
1592
|
exports.mergeRedact = mergeRedact;
|
|
1593
|
+
exports.normalizeRedactKey = normalizeRedactKey;
|
|
1594
|
+
exports.resolveRedactKeys = resolveRedactKeys;
|
|
1321
1595
|
exports.safeJsonStringify = safeJsonStringify;
|
|
1322
1596
|
//# sourceMappingURL=index.cjs.map
|