@warlock.js/logger 4.15.0 → 4.16.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 CHANGED
@@ -4,6 +4,36 @@ 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
+ ## 4.16.0 - 2026-08-18
8
+
9
+ ### Security
10
+
11
+ - **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*.
12
+
13
+ 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).
14
+
15
+ 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.
16
+
17
+ Two new knobs on `RedactConfig`, both usable logger-wide or per channel:
18
+
19
+ - `keys: string[]` — extra key names, unioned with the built-in set.
20
+ - `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.
21
+
22
+ 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.
23
+
24
+ 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.
25
+
26
+ 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.
27
+
28
+ ### Fixed
29
+
30
+ - 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.
31
+ - 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.
32
+
33
+ ### Dependencies
34
+
35
+ - 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.
36
+
7
37
  ## 4.12.0
8
38
 
9
39
  ### 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 is copied
830
- * by reference (we only redact paths through plain objects/arrays anyway,
831
- * and rebuilding e.g. a `Buffer` would change semantics).
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
- if (seen.has(value)) return seen.get(value);
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 (value instanceof Error) {
842
- const copy = new value.constructor(value.message);
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(value, arr);
850
- for (const item of value) arr.push(cloneEntry(item, seen));
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(value, out);
855
- for (const key of Object.keys(value)) out[key] = cloneEntry(value[key], seen);
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) target[key] = applyCensor(target[key], censor, [...pathTrail, key]);
892
- else redactAtPath(target[key], rest, censor, [...pathTrail, key]);
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
- * Produce a new `LoggingData` with every path in `config.paths` replaced
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
- * No-op (returns the input by reference) when `config` is `undefined` or
900
- * its `paths` array is empty, so the fast path stays fast.
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
- if (!config || config.paths.length === 0) return data;
904
- const censor = config.censor ?? "[REDACTED]";
905
- const cloned = cloneEntry(data);
906
- for (const pattern of config.paths) {
907
- const segments = pattern.split(".").filter((segment) => segment.length > 0);
908
- if (segments.length === 0) continue;
909
- redactAtPath(cloned, segments, censor, []);
910
- }
911
- return cloned;
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 tolerates
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