@loggerjs/processors 0.1.0 → 0.3.1

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/README.md CHANGED
@@ -50,7 +50,7 @@ const middleware = [
50
50
  ];
51
51
 
52
52
  const processors = [
53
- redactProcessor({ keys: ["password", "token", /secret/i] }),
53
+ redactProcessor({ keys: ["password", "token", /secret/i], censor: "[hidden]" }),
54
54
  privacyGuardProcessor({ maxStringLength: 8192, allowKeys: ["publicToken"] }),
55
55
  schemaDevCheckProcessor({
56
56
  validators: { "order.created": (data) => (typeof data === "object" && data ? true : "bad payload") },
@@ -91,6 +91,7 @@ const processors = [
91
91
  - **Prefer middleware** for metadata and enrichment that can run before event projection — it's the cheapest place to drop or annotate a record.
92
92
  - **Use processors** for event-only behavior: routing, schema checks, sampling, buffering, and filtering on the resolved event shape.
93
93
  - **Configuring any processor disables the record fast path** for that logger, because every log must then be projected to an event. That is the correct trade when you need event-level behavior — see [PROCESSORS.md](../../docs/PROCESSORS.md) and [PERFORMANCE.md](../../docs/PERFORMANCE.md).
94
+ - `redactProcessor()` supports exact `keys`/`paths`, regex/custom matchers, the Pino-compatible `censor` alias, and `remove: true` for omitting matched object fields. It does not compile user paths with `eval` or `new Function`.
94
95
  - `fingersCrossedProcessor` accepts a `flushTo` transport or sink to replay buffered pre-trigger events. `routeProcessor` targets **named** transports, so name the transports you route to. Expensive I/O and serialization belong in transports, not processors.
95
96
 
96
97
  ## Documentation
package/dist/index.cjs CHANGED
@@ -4,7 +4,12 @@ let _loggerjs_core = require("@loggerjs/core");
4
4
  function matchesKey(key, path, value, matchers) {
5
5
  return matchers.some((matcher) => {
6
6
  if (typeof matcher === "string") return matcher.toLowerCase() === key.toLowerCase();
7
- if (matcher instanceof RegExp) return matcher.test(key) || matcher.test(path);
7
+ if (matcher instanceof RegExp) {
8
+ matcher.lastIndex = 0;
9
+ const keyMatches = matcher.test(key);
10
+ matcher.lastIndex = 0;
11
+ return keyMatches || matcher.test(path);
12
+ }
8
13
  return matcher(key, path, value);
9
14
  });
10
15
  }
@@ -29,8 +34,10 @@ function redactValue(value, options, path = "", depth = 0, seen = /* @__PURE__ *
29
34
  seen.set(value, out);
30
35
  for (const [key, child] of Object.entries(input)) {
31
36
  const childPath = path ? `${path}.${key}` : key;
32
- if (matchesKey(key, childPath, child, options.keys ?? []) || pathMatches(childPath, options.paths ?? [])) out[key] = options.replacement;
33
- else out[key] = redactValue(child, options, childPath, depth + 1, seen);
37
+ if (matchesKey(key, childPath, child, options.keys ?? []) || pathMatches(childPath, options.paths ?? [])) {
38
+ if (options.remove) continue;
39
+ out[key] = options.replacement;
40
+ } else out[key] = redactValue(child, options, childPath, depth + 1, seen);
34
41
  }
35
42
  return out;
36
43
  }
@@ -48,7 +55,8 @@ function redactProcessor(options = {}) {
48
55
  "api_key"
49
56
  ],
50
57
  paths: options.paths ?? [],
51
- replacement: options.replacement ?? "[REDACTED]",
58
+ replacement: options.replacement ?? options.censor ?? "[REDACTED]",
59
+ remove: options.remove ?? false,
52
60
  maxDepth: options.maxDepth ?? 8
53
61
  };
54
62
  return (event) => ({
@@ -999,21 +1007,87 @@ function passesLuhn(input) {
999
1007
  }
1000
1008
  return sum % 10 === 0;
1001
1009
  }
1002
- const DEFAULT_PATTERNS = [
1003
- {
1004
- name: "email",
1005
- pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi
1006
- },
1007
- {
1008
- name: "bearer-token",
1009
- pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]+\b/g
1010
- },
1011
- {
1012
- name: "credit-card",
1013
- pattern: /\b(?:\d[ -]*?){13,19}\b/g,
1014
- validate: passesLuhn
1010
+ const DEFAULT_PATTERNS = [{
1011
+ name: "bearer-token",
1012
+ pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]+\b/g
1013
+ }, {
1014
+ name: "credit-card",
1015
+ pattern: /\b(?:\d[ -]*?){13,19}\b/g,
1016
+ validate: passesLuhn
1017
+ }];
1018
+ function isAsciiAlpha(code) {
1019
+ return code >= 65 && code <= 90 || code >= 97 && code <= 122;
1020
+ }
1021
+ function isAsciiAlphaNumeric(code) {
1022
+ return isAsciiAlpha(code) || code >= 48 && code <= 57;
1023
+ }
1024
+ function hasChar(code, chars) {
1025
+ return chars.includes(String.fromCharCode(code));
1026
+ }
1027
+ function isEmailLocalChar(code) {
1028
+ return isAsciiAlphaNumeric(code) || hasChar(code, "._%+-");
1029
+ }
1030
+ function isEmailDomainChar(code) {
1031
+ return isAsciiAlphaNumeric(code) || hasChar(code, "-.");
1032
+ }
1033
+ function hasValidEmailDomain(input, start, end) {
1034
+ let labelLength = 0;
1035
+ let dotCount = 0;
1036
+ let tldStart = -1;
1037
+ for (let index = start; index < end; index += 1) {
1038
+ if (input.charCodeAt(index) === 46) {
1039
+ if (labelLength === 0) return false;
1040
+ dotCount += 1;
1041
+ labelLength = 0;
1042
+ tldStart = index + 1;
1043
+ continue;
1044
+ }
1045
+ labelLength += 1;
1015
1046
  }
1016
- ];
1047
+ if (dotCount === 0 || labelLength === 0 || tldStart < 0 || end - tldStart < 2) return false;
1048
+ for (let index = tldStart; index < end; index += 1) if (!isAsciiAlpha(input.charCodeAt(index))) return false;
1049
+ return true;
1050
+ }
1051
+ function redactEmailAddresses(input, path, options) {
1052
+ let output = "";
1053
+ let last = 0;
1054
+ let changed = false;
1055
+ let index = 0;
1056
+ while (index < input.length) {
1057
+ if (!isAsciiAlphaNumeric(input.charCodeAt(index))) {
1058
+ index += 1;
1059
+ continue;
1060
+ }
1061
+ const localStart = index;
1062
+ let cursor = index + 1;
1063
+ while (cursor < input.length && isEmailLocalChar(input.charCodeAt(cursor))) cursor += 1;
1064
+ if (input.charCodeAt(cursor) !== 64) {
1065
+ index = cursor + 1;
1066
+ continue;
1067
+ }
1068
+ const domainStart = cursor + 1;
1069
+ cursor = domainStart;
1070
+ while (cursor < input.length && isEmailDomainChar(input.charCodeAt(cursor))) cursor += 1;
1071
+ if (domainStart < cursor && hasValidEmailDomain(input, domainStart, cursor) && (cursor >= input.length || !isEmailLocalChar(input.charCodeAt(cursor)))) {
1072
+ output += input.slice(last, localStart);
1073
+ output += options.replacement;
1074
+ options.onRedact?.(path, "email");
1075
+ last = cursor;
1076
+ changed = true;
1077
+ index = cursor;
1078
+ continue;
1079
+ }
1080
+ index = domainStart;
1081
+ }
1082
+ if (!changed) return {
1083
+ value: input,
1084
+ changed: false
1085
+ };
1086
+ return {
1087
+ value: output + input.slice(last),
1088
+ changed: true
1089
+ };
1090
+ }
1017
1091
  function matcherMatches(matcher, key, path, value) {
1018
1092
  if (typeof matcher === "string") return matcher.toLowerCase() === key.toLowerCase();
1019
1093
  if (matcher instanceof RegExp) {
@@ -1031,21 +1105,34 @@ function redact(path, reason, options) {
1031
1105
  }
1032
1106
  function guardString(input, path, options) {
1033
1107
  let value = input;
1108
+ let changed = false;
1109
+ if (value.length > options.maxStringLength) {
1110
+ options.onRedact?.(path, "max-string-length");
1111
+ value = `${value.slice(0, options.maxStringLength)}${options.truncateSuffix}`;
1112
+ changed = true;
1113
+ }
1114
+ if (options.redactDefaultEmailPattern) {
1115
+ const emailResult = redactEmailAddresses(value, path, options);
1116
+ value = emailResult.value;
1117
+ changed ||= emailResult.changed;
1118
+ }
1034
1119
  for (const item of options.patterns) {
1035
1120
  item.pattern.lastIndex = 0;
1036
1121
  value = value.replace(item.pattern, (match) => {
1037
1122
  if (item.validate && !item.validate(match)) return match;
1038
1123
  options.onRedact?.(path, item.name);
1124
+ changed = true;
1039
1125
  return item.replacement ?? options.replacement;
1040
1126
  });
1041
1127
  }
1042
1128
  if (value.length > options.maxStringLength) {
1043
1129
  options.onRedact?.(path, "max-string-length");
1044
1130
  value = `${value.slice(0, options.maxStringLength)}${options.truncateSuffix}`;
1131
+ changed = true;
1045
1132
  }
1046
1133
  return {
1047
1134
  value,
1048
- changed: value !== input
1135
+ changed: changed || value !== input
1049
1136
  };
1050
1137
  }
1051
1138
  function guardValue(value, path, key, depth, options, seen) {
@@ -1124,6 +1211,7 @@ function privacyGuardProcessor(options = {}) {
1124
1211
  denyKeys: options.denyKeys ?? DEFAULT_DENY_KEYS,
1125
1212
  allowKeys: options.allowKeys ?? [],
1126
1213
  patterns: options.patterns ?? DEFAULT_PATTERNS,
1214
+ redactDefaultEmailPattern: options.patterns === void 0,
1127
1215
  replacement: options.replacement ?? "[REDACTED]",
1128
1216
  maxDepth: Math.max(0, Math.floor(options.maxDepth ?? 8)),
1129
1217
  maxStringLength: Math.max(1, Math.floor(options.maxStringLength ?? 8192)),