@logtape/redaction 1.2.1 → 1.3.0-dev.376

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/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logtape/redaction",
3
- "version": "1.2.1",
3
+ "version": "1.3.0-dev.376+3abddd8c",
4
4
  "license": "MIT",
5
5
  "exports": "./src/mod.ts",
6
6
  "exclude": [
package/dist/field.cjs CHANGED
@@ -54,8 +54,8 @@ function redactByField(sink, options = DEFAULT_REDACT_FIELDS) {
54
54
  let redactedMessage = record.message;
55
55
  if (typeof record.rawMessage === "string") {
56
56
  const placeholders = extractPlaceholderNames(record.rawMessage);
57
- const redactedIndices = getRedactedPlaceholderIndices(placeholders, opts.fieldPatterns);
58
- if (redactedIndices.size > 0) redactedMessage = redactMessageArray(record.message, redactedIndices, opts.action);
57
+ const { redactedIndices, wildcardIndices } = getRedactedPlaceholderIndices(placeholders, opts.fieldPatterns);
58
+ if (redactedIndices.size > 0 || wildcardIndices.size > 0) redactedMessage = redactMessageArray(record.message, redactedIndices, wildcardIndices, redactedProperties, opts.action);
59
59
  } else {
60
60
  const redactedValues = getRedactedValues(record.properties, redactedProperties);
61
61
  if (redactedValues.size > 0) redactedMessage = redactMessageByValues(record.message, redactedValues);
@@ -153,43 +153,53 @@ function parsePathSegments(path) {
153
153
  }
154
154
  /**
155
155
  * Determines which placeholder indices should be redacted based on field
156
- * patterns.
156
+ * patterns, and which are wildcard placeholders.
157
157
  * @param placeholders Array of placeholder names from the template.
158
158
  * @param fieldPatterns Field patterns to match against.
159
- * @returns Set of indices that should be redacted.
159
+ * @returns Object with redactedIndices and wildcardIndices.
160
160
  */
161
161
  function getRedactedPlaceholderIndices(placeholders, fieldPatterns) {
162
- const indices = /* @__PURE__ */ new Set();
162
+ const redactedIndices = /* @__PURE__ */ new Set();
163
+ const wildcardIndices = /* @__PURE__ */ new Set();
163
164
  for (let i = 0; i < placeholders.length; i++) {
164
165
  const placeholder = placeholders[i];
165
- if (placeholder === "*") continue;
166
+ if (placeholder === "*") {
167
+ wildcardIndices.add(i);
168
+ continue;
169
+ }
166
170
  if (shouldFieldRedacted(placeholder, fieldPatterns)) {
167
- indices.add(i);
171
+ redactedIndices.add(i);
168
172
  continue;
169
173
  }
170
174
  const segments = parsePathSegments(placeholder);
171
175
  for (const segment of segments) if (shouldFieldRedacted(segment, fieldPatterns)) {
172
- indices.add(i);
176
+ redactedIndices.add(i);
173
177
  break;
174
178
  }
175
179
  }
176
- return indices;
180
+ return {
181
+ redactedIndices,
182
+ wildcardIndices
183
+ };
177
184
  }
178
185
  /**
179
186
  * Redacts values in the message array based on the redacted placeholder
180
- * indices.
187
+ * indices and wildcard indices.
181
188
  * @param message The original message array.
182
189
  * @param redactedIndices Set of placeholder indices to redact.
190
+ * @param wildcardIndices Set of wildcard placeholder indices.
191
+ * @param redactedProperties The redacted properties object.
183
192
  * @param action The redaction action.
184
193
  * @returns New message array with redacted values.
185
194
  */
186
- function redactMessageArray(message, redactedIndices, action) {
187
- if (redactedIndices.size === 0) return message;
195
+ function redactMessageArray(message, redactedIndices, wildcardIndices, redactedProperties, action) {
196
+ if (redactedIndices.size === 0 && wildcardIndices.size === 0) return message;
188
197
  const result = [];
189
198
  let placeholderIndex = 0;
190
199
  for (let i = 0; i < message.length; i++) if (i % 2 === 0) result.push(message[i]);
191
200
  else {
192
- if (redactedIndices.has(placeholderIndex)) if (action == null || action === "delete") result.push("");
201
+ if (wildcardIndices.has(placeholderIndex)) result.push(redactedProperties);
202
+ else if (redactedIndices.has(placeholderIndex)) if (action == null || action === "delete") result.push("");
193
203
  else result.push(action(message[i]));
194
204
  else result.push(message[i]);
195
205
  placeholderIndex++;
package/dist/field.js CHANGED
@@ -53,8 +53,8 @@ function redactByField(sink, options = DEFAULT_REDACT_FIELDS) {
53
53
  let redactedMessage = record.message;
54
54
  if (typeof record.rawMessage === "string") {
55
55
  const placeholders = extractPlaceholderNames(record.rawMessage);
56
- const redactedIndices = getRedactedPlaceholderIndices(placeholders, opts.fieldPatterns);
57
- if (redactedIndices.size > 0) redactedMessage = redactMessageArray(record.message, redactedIndices, opts.action);
56
+ const { redactedIndices, wildcardIndices } = getRedactedPlaceholderIndices(placeholders, opts.fieldPatterns);
57
+ if (redactedIndices.size > 0 || wildcardIndices.size > 0) redactedMessage = redactMessageArray(record.message, redactedIndices, wildcardIndices, redactedProperties, opts.action);
58
58
  } else {
59
59
  const redactedValues = getRedactedValues(record.properties, redactedProperties);
60
60
  if (redactedValues.size > 0) redactedMessage = redactMessageByValues(record.message, redactedValues);
@@ -152,43 +152,53 @@ function parsePathSegments(path) {
152
152
  }
153
153
  /**
154
154
  * Determines which placeholder indices should be redacted based on field
155
- * patterns.
155
+ * patterns, and which are wildcard placeholders.
156
156
  * @param placeholders Array of placeholder names from the template.
157
157
  * @param fieldPatterns Field patterns to match against.
158
- * @returns Set of indices that should be redacted.
158
+ * @returns Object with redactedIndices and wildcardIndices.
159
159
  */
160
160
  function getRedactedPlaceholderIndices(placeholders, fieldPatterns) {
161
- const indices = /* @__PURE__ */ new Set();
161
+ const redactedIndices = /* @__PURE__ */ new Set();
162
+ const wildcardIndices = /* @__PURE__ */ new Set();
162
163
  for (let i = 0; i < placeholders.length; i++) {
163
164
  const placeholder = placeholders[i];
164
- if (placeholder === "*") continue;
165
+ if (placeholder === "*") {
166
+ wildcardIndices.add(i);
167
+ continue;
168
+ }
165
169
  if (shouldFieldRedacted(placeholder, fieldPatterns)) {
166
- indices.add(i);
170
+ redactedIndices.add(i);
167
171
  continue;
168
172
  }
169
173
  const segments = parsePathSegments(placeholder);
170
174
  for (const segment of segments) if (shouldFieldRedacted(segment, fieldPatterns)) {
171
- indices.add(i);
175
+ redactedIndices.add(i);
172
176
  break;
173
177
  }
174
178
  }
175
- return indices;
179
+ return {
180
+ redactedIndices,
181
+ wildcardIndices
182
+ };
176
183
  }
177
184
  /**
178
185
  * Redacts values in the message array based on the redacted placeholder
179
- * indices.
186
+ * indices and wildcard indices.
180
187
  * @param message The original message array.
181
188
  * @param redactedIndices Set of placeholder indices to redact.
189
+ * @param wildcardIndices Set of wildcard placeholder indices.
190
+ * @param redactedProperties The redacted properties object.
182
191
  * @param action The redaction action.
183
192
  * @returns New message array with redacted values.
184
193
  */
185
- function redactMessageArray(message, redactedIndices, action) {
186
- if (redactedIndices.size === 0) return message;
194
+ function redactMessageArray(message, redactedIndices, wildcardIndices, redactedProperties, action) {
195
+ if (redactedIndices.size === 0 && wildcardIndices.size === 0) return message;
187
196
  const result = [];
188
197
  let placeholderIndex = 0;
189
198
  for (let i = 0; i < message.length; i++) if (i % 2 === 0) result.push(message[i]);
190
199
  else {
191
- if (redactedIndices.has(placeholderIndex)) if (action == null || action === "delete") result.push("");
200
+ if (wildcardIndices.has(placeholderIndex)) result.push(redactedProperties);
201
+ else if (redactedIndices.has(placeholderIndex)) if (action == null || action === "delete") result.push("");
192
202
  else result.push(action(message[i]));
193
203
  else result.push(message[i]);
194
204
  placeholderIndex++;
package/dist/field.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"field.js","names":["DEFAULT_REDACT_FIELDS: FieldPatterns","sink: Sink | Sink & Disposable | Sink & AsyncDisposable","options: FieldRedactionOptions | FieldPatterns","record: LogRecord","properties: Record<string, unknown>","options: FieldRedactionOptions","field: string","fieldPatterns: FieldPatterns","template: string","placeholders: string[]","path: string","segments: string[]","message: readonly unknown[]","redactedIndices: Set<number>","action: \"delete\" | ((value: unknown) => unknown) | undefined","result: unknown[]","original: Record<string, unknown>","redacted: Record<string, unknown>","map: Map<unknown, unknown>","redactedValues: Map<unknown, unknown>"],"sources":["../src/field.ts"],"sourcesContent":["import type { LogRecord, Sink } from \"@logtape/logtape\";\n\n/**\n * The type for a field pattern used in redaction. A string or a regular\n * expression that matches field names.\n * @since 0.10.0\n */\nexport type FieldPattern = string | RegExp;\n\n/**\n * An array of field patterns used for redaction. Each pattern can be\n * a string or a regular expression that matches field names.\n * @since 0.10.0\n */\nexport type FieldPatterns = FieldPattern[];\n\n/**\n * Default field patterns for redaction. These patterns will match\n * common sensitive fields such as passwords, tokens, and personal\n * information.\n * @since 0.10.0\n */\nexport const DEFAULT_REDACT_FIELDS: FieldPatterns = [\n /pass(?:code|phrase|word)/i,\n /secret/i,\n /token/i,\n /key/i,\n /credential/i,\n /auth/i,\n /signature/i,\n /sensitive/i,\n /private/i,\n /ssn/i,\n /email/i,\n /phone/i,\n /address/i,\n];\n\n/**\n * Options for redacting fields in a {@link LogRecord}. Used by\n * the {@link redactByField} function.\n * @since 0.10.0\n */\nexport interface FieldRedactionOptions {\n /**\n * The field patterns to match against. This can be an array of\n * strings or regular expressions. If a field matches any of the\n * patterns, it will be redacted.\n * @defaultValue {@link DEFAULT_REDACT_FIELDS}\n */\n readonly fieldPatterns: FieldPatterns;\n\n /**\n * The action to perform on the matched fields. If not provided,\n * the default action is to delete the field from the properties.\n * If a function is provided, it will be called with the\n * value of the field, and the return value will be used to replace\n * the field in the properties.\n * If the action is `\"delete\"`, the field will be removed from the\n * properties.\n * @default `\"delete\"`\n */\n readonly action?: \"delete\" | ((value: unknown) => unknown);\n}\n\n/**\n * Redacts properties and message values in a {@link LogRecord} based on the\n * provided field patterns and action.\n *\n * Note that it is a decorator which wraps the sink and redacts properties\n * and message values before passing them to the sink.\n *\n * For string templates (e.g., `\"Hello, {name}!\"`), placeholder names are\n * matched against the field patterns to determine which values to redact.\n *\n * For tagged template literals (e.g., `` `Hello, ${name}!` ``), redaction\n * is performed by comparing message values with redacted property values.\n *\n * @example\n * ```ts\n * import { getConsoleSink } from \"@logtape/logtape\";\n * import { redactByField } from \"@logtape/redaction\";\n *\n * const sink = redactByField(getConsoleSink());\n * ```\n *\n * @param sink The sink to wrap.\n * @param options The redaction options.\n * @returns The wrapped sink.\n * @since 0.10.0\n */\nexport function redactByField(\n sink: Sink | Sink & Disposable | Sink & AsyncDisposable,\n options: FieldRedactionOptions | FieldPatterns = DEFAULT_REDACT_FIELDS,\n): Sink | Sink & Disposable | Sink & AsyncDisposable {\n const opts = Array.isArray(options) ? { fieldPatterns: options } : options;\n const wrapped = (record: LogRecord) => {\n const redactedProperties = redactProperties(record.properties, opts);\n let redactedMessage = record.message;\n\n if (typeof record.rawMessage === \"string\") {\n // String template: redact by placeholder names\n const placeholders = extractPlaceholderNames(record.rawMessage);\n const redactedIndices = getRedactedPlaceholderIndices(\n placeholders,\n opts.fieldPatterns,\n );\n if (redactedIndices.size > 0) {\n redactedMessage = redactMessageArray(\n record.message,\n redactedIndices,\n opts.action,\n );\n }\n } else {\n // Tagged template: redact by comparing values\n const redactedValues = getRedactedValues(\n record.properties,\n redactedProperties,\n );\n if (redactedValues.size > 0) {\n redactedMessage = redactMessageByValues(record.message, redactedValues);\n }\n }\n\n sink({\n ...record,\n message: redactedMessage,\n properties: redactedProperties,\n });\n };\n if (Symbol.dispose in sink) wrapped[Symbol.dispose] = sink[Symbol.dispose];\n if (Symbol.asyncDispose in sink) {\n wrapped[Symbol.asyncDispose] = sink[Symbol.asyncDispose];\n }\n return wrapped;\n}\n\n/**\n * Redacts properties from an object based on specified field patterns.\n *\n * This function creates a shallow copy of the input object and applies\n * redaction rules to its properties. For properties that match the redaction\n * patterns, the function either removes them or transforms their values based\n * on the provided action.\n *\n * The redaction process is recursive and will be applied to nested objects\n * as well, allowing for deep redaction of sensitive data in complex object\n * structures.\n * @param properties The properties to redact.\n * @param options The redaction options.\n * @returns The redacted properties.\n * @since 0.10.0\n */\nexport function redactProperties(\n properties: Record<string, unknown>,\n options: FieldRedactionOptions,\n): Record<string, unknown> {\n const copy = { ...properties };\n for (const field in copy) {\n if (shouldFieldRedacted(field, options.fieldPatterns)) {\n if (options.action == null || options.action === \"delete\") {\n delete copy[field];\n } else {\n copy[field] = options.action(copy[field]);\n }\n continue;\n }\n const value = copy[field];\n // Check if value is an array:\n if (Array.isArray(value)) {\n copy[field] = value.map((item) => {\n if (\n typeof item === \"object\" && item !== null &&\n (Object.getPrototypeOf(item) === Object.prototype ||\n Object.getPrototypeOf(item) === null)\n ) {\n // @ts-ignore: item is always Record<string, unknown>\n return redactProperties(item, options);\n }\n return item;\n });\n // Check if value is a vanilla object:\n } else if (\n typeof value === \"object\" && value !== null &&\n (Object.getPrototypeOf(value) === Object.prototype ||\n Object.getPrototypeOf(value) === null)\n ) {\n // @ts-ignore: value is always Record<string, unknown>\n copy[field] = redactProperties(value, options);\n }\n }\n return copy;\n}\n\n/**\n * Checks if a field should be redacted based on the provided field patterns.\n * @param field The field name to check.\n * @param fieldPatterns The field patterns to match against.\n * @returns `true` if the field should be redacted, `false` otherwise.\n * @since 0.10.0\n */\nexport function shouldFieldRedacted(\n field: string,\n fieldPatterns: FieldPatterns,\n): boolean {\n for (const fieldPattern of fieldPatterns) {\n if (typeof fieldPattern === \"string\") {\n if (fieldPattern === field) return true;\n } else {\n if (fieldPattern.test(field)) return true;\n }\n }\n return false;\n}\n\n/**\n * Extracts placeholder names from a message template string in order.\n * @param template The message template string.\n * @returns An array of placeholder names in the order they appear.\n */\nfunction extractPlaceholderNames(template: string): string[] {\n const placeholders: string[] = [];\n for (let i = 0; i < template.length; i++) {\n if (template[i] === \"{\") {\n // Check for escaped brace\n if (i + 1 < template.length && template[i + 1] === \"{\") {\n i++;\n continue;\n }\n const closeIndex = template.indexOf(\"}\", i + 1);\n if (closeIndex === -1) continue;\n const key = template.slice(i + 1, closeIndex).trim();\n placeholders.push(key);\n i = closeIndex;\n }\n }\n return placeholders;\n}\n\n/**\n * Parses a property path into its segments.\n * @param path The property path (e.g., \"user.password\" or \"users[0].email\").\n * @returns An array of path segments.\n */\nfunction parsePathSegments(path: string): string[] {\n const segments: string[] = [];\n let current = \"\";\n for (const char of path) {\n if (char === \".\" || char === \"[\") {\n if (current) segments.push(current);\n current = \"\";\n } else if (char === \"]\" || char === \"?\") {\n // Skip these characters\n } else {\n current += char;\n }\n }\n if (current) segments.push(current);\n return segments;\n}\n\n/**\n * Determines which placeholder indices should be redacted based on field\n * patterns.\n * @param placeholders Array of placeholder names from the template.\n * @param fieldPatterns Field patterns to match against.\n * @returns Set of indices that should be redacted.\n */\nfunction getRedactedPlaceholderIndices(\n placeholders: string[],\n fieldPatterns: FieldPatterns,\n): Set<number> {\n const indices = new Set<number>();\n for (let i = 0; i < placeholders.length; i++) {\n const placeholder = placeholders[i];\n // Skip wildcard {*}\n if (placeholder === \"*\") continue;\n\n // Check the full placeholder name\n if (shouldFieldRedacted(placeholder, fieldPatterns)) {\n indices.add(i);\n continue;\n }\n // For nested paths, check each segment\n const segments = parsePathSegments(placeholder);\n for (const segment of segments) {\n if (shouldFieldRedacted(segment, fieldPatterns)) {\n indices.add(i);\n break;\n }\n }\n }\n return indices;\n}\n\n/**\n * Redacts values in the message array based on the redacted placeholder\n * indices.\n * @param message The original message array.\n * @param redactedIndices Set of placeholder indices to redact.\n * @param action The redaction action.\n * @returns New message array with redacted values.\n */\nfunction redactMessageArray(\n message: readonly unknown[],\n redactedIndices: Set<number>,\n action: \"delete\" | ((value: unknown) => unknown) | undefined,\n): readonly unknown[] {\n if (redactedIndices.size === 0) return message;\n\n const result: unknown[] = [];\n let placeholderIndex = 0;\n\n for (let i = 0; i < message.length; i++) {\n if (i % 2 === 0) {\n // Even index: text segment\n result.push(message[i]);\n } else {\n // Odd index: value/placeholder\n if (redactedIndices.has(placeholderIndex)) {\n if (action == null || action === \"delete\") {\n result.push(\"\");\n } else {\n result.push(action(message[i]));\n }\n } else {\n result.push(message[i]);\n }\n placeholderIndex++;\n }\n }\n return result;\n}\n\n/**\n * Collects redacted value mappings from original to redacted properties.\n * @param original The original properties.\n * @param redacted The redacted properties.\n * @param map The map to populate with original -> redacted value pairs.\n */\nfunction collectRedactedValues(\n original: Record<string, unknown>,\n redacted: Record<string, unknown>,\n map: Map<unknown, unknown>,\n): void {\n for (const key in original) {\n const origVal = original[key];\n const redVal = redacted[key];\n\n if (origVal !== redVal) {\n map.set(origVal, redVal);\n }\n\n // Recurse into nested objects\n if (\n typeof origVal === \"object\" && origVal !== null &&\n typeof redVal === \"object\" && redVal !== null &&\n !Array.isArray(origVal)\n ) {\n collectRedactedValues(\n origVal as Record<string, unknown>,\n redVal as Record<string, unknown>,\n map,\n );\n }\n }\n}\n\n/**\n * Gets a map of original values to their redacted replacements.\n * @param original The original properties.\n * @param redacted The redacted properties.\n * @returns A map of original -> redacted values.\n */\nfunction getRedactedValues(\n original: Record<string, unknown>,\n redacted: Record<string, unknown>,\n): Map<unknown, unknown> {\n const map = new Map<unknown, unknown>();\n collectRedactedValues(original, redacted, map);\n return map;\n}\n\n/**\n * Redacts message array values by comparing with redacted property values.\n * Used for tagged template literals where placeholder names are not available.\n * @param message The original message array.\n * @param redactedValues Map of original -> redacted values.\n * @returns New message array with redacted values.\n */\nfunction redactMessageByValues(\n message: readonly unknown[],\n redactedValues: Map<unknown, unknown>,\n): readonly unknown[] {\n if (redactedValues.size === 0) return message;\n\n const result: unknown[] = [];\n for (let i = 0; i < message.length; i++) {\n if (i % 2 === 0) {\n result.push(message[i]);\n } else {\n const val = message[i];\n if (redactedValues.has(val)) {\n result.push(redactedValues.get(val));\n } else {\n result.push(val);\n }\n }\n }\n return result;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"field.js","names":["DEFAULT_REDACT_FIELDS: FieldPatterns","sink: Sink | Sink & Disposable | Sink & AsyncDisposable","options: FieldRedactionOptions | FieldPatterns","record: LogRecord","properties: Record<string, unknown>","options: FieldRedactionOptions","field: string","fieldPatterns: FieldPatterns","template: string","placeholders: string[]","path: string","segments: string[]","message: readonly unknown[]","redactedIndices: Set<number>","wildcardIndices: Set<number>","redactedProperties: Record<string, unknown>","action: \"delete\" | ((value: unknown) => unknown) | undefined","result: unknown[]","original: Record<string, unknown>","redacted: Record<string, unknown>","map: Map<unknown, unknown>","redactedValues: Map<unknown, unknown>"],"sources":["../src/field.ts"],"sourcesContent":["import type { LogRecord, Sink } from \"@logtape/logtape\";\n\n/**\n * The type for a field pattern used in redaction. A string or a regular\n * expression that matches field names.\n * @since 0.10.0\n */\nexport type FieldPattern = string | RegExp;\n\n/**\n * An array of field patterns used for redaction. Each pattern can be\n * a string or a regular expression that matches field names.\n * @since 0.10.0\n */\nexport type FieldPatterns = FieldPattern[];\n\n/**\n * Default field patterns for redaction. These patterns will match\n * common sensitive fields such as passwords, tokens, and personal\n * information.\n * @since 0.10.0\n */\nexport const DEFAULT_REDACT_FIELDS: FieldPatterns = [\n /pass(?:code|phrase|word)/i,\n /secret/i,\n /token/i,\n /key/i,\n /credential/i,\n /auth/i,\n /signature/i,\n /sensitive/i,\n /private/i,\n /ssn/i,\n /email/i,\n /phone/i,\n /address/i,\n];\n\n/**\n * Options for redacting fields in a {@link LogRecord}. Used by\n * the {@link redactByField} function.\n * @since 0.10.0\n */\nexport interface FieldRedactionOptions {\n /**\n * The field patterns to match against. This can be an array of\n * strings or regular expressions. If a field matches any of the\n * patterns, it will be redacted.\n * @defaultValue {@link DEFAULT_REDACT_FIELDS}\n */\n readonly fieldPatterns: FieldPatterns;\n\n /**\n * The action to perform on the matched fields. If not provided,\n * the default action is to delete the field from the properties.\n * If a function is provided, it will be called with the\n * value of the field, and the return value will be used to replace\n * the field in the properties.\n * If the action is `\"delete\"`, the field will be removed from the\n * properties.\n * @default `\"delete\"`\n */\n readonly action?: \"delete\" | ((value: unknown) => unknown);\n}\n\n/**\n * Redacts properties and message values in a {@link LogRecord} based on the\n * provided field patterns and action.\n *\n * Note that it is a decorator which wraps the sink and redacts properties\n * and message values before passing them to the sink.\n *\n * For string templates (e.g., `\"Hello, {name}!\"`), placeholder names are\n * matched against the field patterns to determine which values to redact.\n *\n * For tagged template literals (e.g., `` `Hello, ${name}!` ``), redaction\n * is performed by comparing message values with redacted property values.\n *\n * @example\n * ```ts\n * import { getConsoleSink } from \"@logtape/logtape\";\n * import { redactByField } from \"@logtape/redaction\";\n *\n * const sink = redactByField(getConsoleSink());\n * ```\n *\n * @param sink The sink to wrap.\n * @param options The redaction options.\n * @returns The wrapped sink.\n * @since 0.10.0\n */\nexport function redactByField(\n sink: Sink | Sink & Disposable | Sink & AsyncDisposable,\n options: FieldRedactionOptions | FieldPatterns = DEFAULT_REDACT_FIELDS,\n): Sink | Sink & Disposable | Sink & AsyncDisposable {\n const opts = Array.isArray(options) ? { fieldPatterns: options } : options;\n const wrapped = (record: LogRecord) => {\n const redactedProperties = redactProperties(record.properties, opts);\n let redactedMessage = record.message;\n\n if (typeof record.rawMessage === \"string\") {\n // String template: redact by placeholder names\n const placeholders = extractPlaceholderNames(record.rawMessage);\n const { redactedIndices, wildcardIndices } =\n getRedactedPlaceholderIndices(\n placeholders,\n opts.fieldPatterns,\n );\n if (redactedIndices.size > 0 || wildcardIndices.size > 0) {\n redactedMessage = redactMessageArray(\n record.message,\n redactedIndices,\n wildcardIndices,\n redactedProperties,\n opts.action,\n );\n }\n } else {\n // Tagged template: redact by comparing values\n const redactedValues = getRedactedValues(\n record.properties,\n redactedProperties,\n );\n if (redactedValues.size > 0) {\n redactedMessage = redactMessageByValues(record.message, redactedValues);\n }\n }\n\n sink({\n ...record,\n message: redactedMessage,\n properties: redactedProperties,\n });\n };\n if (Symbol.dispose in sink) wrapped[Symbol.dispose] = sink[Symbol.dispose];\n if (Symbol.asyncDispose in sink) {\n wrapped[Symbol.asyncDispose] = sink[Symbol.asyncDispose];\n }\n return wrapped;\n}\n\n/**\n * Redacts properties from an object based on specified field patterns.\n *\n * This function creates a shallow copy of the input object and applies\n * redaction rules to its properties. For properties that match the redaction\n * patterns, the function either removes them or transforms their values based\n * on the provided action.\n *\n * The redaction process is recursive and will be applied to nested objects\n * as well, allowing for deep redaction of sensitive data in complex object\n * structures.\n * @param properties The properties to redact.\n * @param options The redaction options.\n * @returns The redacted properties.\n * @since 0.10.0\n */\nexport function redactProperties(\n properties: Record<string, unknown>,\n options: FieldRedactionOptions,\n): Record<string, unknown> {\n const copy = { ...properties };\n for (const field in copy) {\n if (shouldFieldRedacted(field, options.fieldPatterns)) {\n if (options.action == null || options.action === \"delete\") {\n delete copy[field];\n } else {\n copy[field] = options.action(copy[field]);\n }\n continue;\n }\n const value = copy[field];\n // Check if value is an array:\n if (Array.isArray(value)) {\n copy[field] = value.map((item) => {\n if (\n typeof item === \"object\" && item !== null &&\n (Object.getPrototypeOf(item) === Object.prototype ||\n Object.getPrototypeOf(item) === null)\n ) {\n // @ts-ignore: item is always Record<string, unknown>\n return redactProperties(item, options);\n }\n return item;\n });\n // Check if value is a vanilla object:\n } else if (\n typeof value === \"object\" && value !== null &&\n (Object.getPrototypeOf(value) === Object.prototype ||\n Object.getPrototypeOf(value) === null)\n ) {\n // @ts-ignore: value is always Record<string, unknown>\n copy[field] = redactProperties(value, options);\n }\n }\n return copy;\n}\n\n/**\n * Checks if a field should be redacted based on the provided field patterns.\n * @param field The field name to check.\n * @param fieldPatterns The field patterns to match against.\n * @returns `true` if the field should be redacted, `false` otherwise.\n * @since 0.10.0\n */\nexport function shouldFieldRedacted(\n field: string,\n fieldPatterns: FieldPatterns,\n): boolean {\n for (const fieldPattern of fieldPatterns) {\n if (typeof fieldPattern === \"string\") {\n if (fieldPattern === field) return true;\n } else {\n if (fieldPattern.test(field)) return true;\n }\n }\n return false;\n}\n\n/**\n * Extracts placeholder names from a message template string in order.\n * @param template The message template string.\n * @returns An array of placeholder names in the order they appear.\n */\nfunction extractPlaceholderNames(template: string): string[] {\n const placeholders: string[] = [];\n for (let i = 0; i < template.length; i++) {\n if (template[i] === \"{\") {\n // Check for escaped brace\n if (i + 1 < template.length && template[i + 1] === \"{\") {\n i++;\n continue;\n }\n const closeIndex = template.indexOf(\"}\", i + 1);\n if (closeIndex === -1) continue;\n const key = template.slice(i + 1, closeIndex).trim();\n placeholders.push(key);\n i = closeIndex;\n }\n }\n return placeholders;\n}\n\n/**\n * Parses a property path into its segments.\n * @param path The property path (e.g., \"user.password\" or \"users[0].email\").\n * @returns An array of path segments.\n */\nfunction parsePathSegments(path: string): string[] {\n const segments: string[] = [];\n let current = \"\";\n for (const char of path) {\n if (char === \".\" || char === \"[\") {\n if (current) segments.push(current);\n current = \"\";\n } else if (char === \"]\" || char === \"?\") {\n // Skip these characters\n } else {\n current += char;\n }\n }\n if (current) segments.push(current);\n return segments;\n}\n\n/**\n * Determines which placeholder indices should be redacted based on field\n * patterns, and which are wildcard placeholders.\n * @param placeholders Array of placeholder names from the template.\n * @param fieldPatterns Field patterns to match against.\n * @returns Object with redactedIndices and wildcardIndices.\n */\nfunction getRedactedPlaceholderIndices(\n placeholders: string[],\n fieldPatterns: FieldPatterns,\n): { redactedIndices: Set<number>; wildcardIndices: Set<number> } {\n const redactedIndices = new Set<number>();\n const wildcardIndices = new Set<number>();\n\n for (let i = 0; i < placeholders.length; i++) {\n const placeholder = placeholders[i];\n\n // Track wildcard {*} separately\n if (placeholder === \"*\") {\n wildcardIndices.add(i);\n continue;\n }\n\n // Check the full placeholder name\n if (shouldFieldRedacted(placeholder, fieldPatterns)) {\n redactedIndices.add(i);\n continue;\n }\n // For nested paths, check each segment\n const segments = parsePathSegments(placeholder);\n for (const segment of segments) {\n if (shouldFieldRedacted(segment, fieldPatterns)) {\n redactedIndices.add(i);\n break;\n }\n }\n }\n return { redactedIndices, wildcardIndices };\n}\n\n/**\n * Redacts values in the message array based on the redacted placeholder\n * indices and wildcard indices.\n * @param message The original message array.\n * @param redactedIndices Set of placeholder indices to redact.\n * @param wildcardIndices Set of wildcard placeholder indices.\n * @param redactedProperties The redacted properties object.\n * @param action The redaction action.\n * @returns New message array with redacted values.\n */\nfunction redactMessageArray(\n message: readonly unknown[],\n redactedIndices: Set<number>,\n wildcardIndices: Set<number>,\n redactedProperties: Record<string, unknown>,\n action: \"delete\" | ((value: unknown) => unknown) | undefined,\n): readonly unknown[] {\n if (redactedIndices.size === 0 && wildcardIndices.size === 0) return message;\n\n const result: unknown[] = [];\n let placeholderIndex = 0;\n\n for (let i = 0; i < message.length; i++) {\n if (i % 2 === 0) {\n // Even index: text segment\n result.push(message[i]);\n } else {\n // Odd index: value/placeholder\n if (wildcardIndices.has(placeholderIndex)) {\n // Wildcard {*}: replace with redacted properties\n result.push(redactedProperties);\n } else if (redactedIndices.has(placeholderIndex)) {\n if (action == null || action === \"delete\") {\n result.push(\"\");\n } else {\n result.push(action(message[i]));\n }\n } else {\n result.push(message[i]);\n }\n placeholderIndex++;\n }\n }\n return result;\n}\n\n/**\n * Collects redacted value mappings from original to redacted properties.\n * @param original The original properties.\n * @param redacted The redacted properties.\n * @param map The map to populate with original -> redacted value pairs.\n */\nfunction collectRedactedValues(\n original: Record<string, unknown>,\n redacted: Record<string, unknown>,\n map: Map<unknown, unknown>,\n): void {\n for (const key in original) {\n const origVal = original[key];\n const redVal = redacted[key];\n\n if (origVal !== redVal) {\n map.set(origVal, redVal);\n }\n\n // Recurse into nested objects\n if (\n typeof origVal === \"object\" && origVal !== null &&\n typeof redVal === \"object\" && redVal !== null &&\n !Array.isArray(origVal)\n ) {\n collectRedactedValues(\n origVal as Record<string, unknown>,\n redVal as Record<string, unknown>,\n map,\n );\n }\n }\n}\n\n/**\n * Gets a map of original values to their redacted replacements.\n * @param original The original properties.\n * @param redacted The redacted properties.\n * @returns A map of original -> redacted values.\n */\nfunction getRedactedValues(\n original: Record<string, unknown>,\n redacted: Record<string, unknown>,\n): Map<unknown, unknown> {\n const map = new Map<unknown, unknown>();\n collectRedactedValues(original, redacted, map);\n return map;\n}\n\n/**\n * Redacts message array values by comparing with redacted property values.\n * Used for tagged template literals where placeholder names are not available.\n * @param message The original message array.\n * @param redactedValues Map of original -> redacted values.\n * @returns New message array with redacted values.\n */\nfunction redactMessageByValues(\n message: readonly unknown[],\n redactedValues: Map<unknown, unknown>,\n): readonly unknown[] {\n if (redactedValues.size === 0) return message;\n\n const result: unknown[] = [];\n for (let i = 0; i < message.length; i++) {\n if (i % 2 === 0) {\n result.push(message[i]);\n } else {\n const val = message[i];\n if (redactedValues.has(val)) {\n result.push(redactedValues.get(val));\n } else {\n result.push(val);\n }\n }\n }\n return result;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logtape/redaction",
3
- "version": "1.2.1",
3
+ "version": "1.3.0-dev.376+3abddd8c",
4
4
  "description": "Redact sensitive data from log messages",
5
5
  "keywords": [
6
6
  "logging",
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "sideEffects": false,
48
48
  "peerDependencies": {
49
- "@logtape/logtape": "^1.2.1"
49
+ "@logtape/logtape": "^1.3.0-dev.376+3abddd8c"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@alinea/suite": "^0.6.3",
package/src/field.test.ts CHANGED
@@ -439,7 +439,7 @@ test("redactByField()", async () => {
439
439
  assertEquals(records[0].message[1], "johndoe");
440
440
  }
441
441
 
442
- { // wildcard {*} in message is not redacted
442
+ { // wildcard {*} in message uses redacted properties
443
443
  const records: LogRecord[] = [];
444
444
  const wrappedSink = redactByField((r) => records.push(r), {
445
445
  fieldPatterns: ["password"],
@@ -456,8 +456,12 @@ test("redactByField()", async () => {
456
456
  properties: props,
457
457
  });
458
458
 
459
- // The {*} itself should not be redacted
460
- assertEquals(records[0].message[1], props);
459
+ // The {*} should be replaced with redacted properties
460
+ assertEquals(records[0].message[1], {
461
+ username: "john",
462
+ password: "[REDACTED]",
463
+ });
464
+ assertEquals(records[0].properties.password, "[REDACTED]");
461
465
  }
462
466
 
463
467
  { // escaped braces are not treated as placeholders
package/src/field.ts CHANGED
@@ -101,14 +101,17 @@ export function redactByField(
101
101
  if (typeof record.rawMessage === "string") {
102
102
  // String template: redact by placeholder names
103
103
  const placeholders = extractPlaceholderNames(record.rawMessage);
104
- const redactedIndices = getRedactedPlaceholderIndices(
105
- placeholders,
106
- opts.fieldPatterns,
107
- );
108
- if (redactedIndices.size > 0) {
104
+ const { redactedIndices, wildcardIndices } =
105
+ getRedactedPlaceholderIndices(
106
+ placeholders,
107
+ opts.fieldPatterns,
108
+ );
109
+ if (redactedIndices.size > 0 || wildcardIndices.size > 0) {
109
110
  redactedMessage = redactMessageArray(
110
111
  record.message,
111
112
  redactedIndices,
113
+ wildcardIndices,
114
+ redactedProperties,
112
115
  opts.action,
113
116
  );
114
117
  }
@@ -262,52 +265,62 @@ function parsePathSegments(path: string): string[] {
262
265
 
263
266
  /**
264
267
  * Determines which placeholder indices should be redacted based on field
265
- * patterns.
268
+ * patterns, and which are wildcard placeholders.
266
269
  * @param placeholders Array of placeholder names from the template.
267
270
  * @param fieldPatterns Field patterns to match against.
268
- * @returns Set of indices that should be redacted.
271
+ * @returns Object with redactedIndices and wildcardIndices.
269
272
  */
270
273
  function getRedactedPlaceholderIndices(
271
274
  placeholders: string[],
272
275
  fieldPatterns: FieldPatterns,
273
- ): Set<number> {
274
- const indices = new Set<number>();
276
+ ): { redactedIndices: Set<number>; wildcardIndices: Set<number> } {
277
+ const redactedIndices = new Set<number>();
278
+ const wildcardIndices = new Set<number>();
279
+
275
280
  for (let i = 0; i < placeholders.length; i++) {
276
281
  const placeholder = placeholders[i];
277
- // Skip wildcard {*}
278
- if (placeholder === "*") continue;
282
+
283
+ // Track wildcard {*} separately
284
+ if (placeholder === "*") {
285
+ wildcardIndices.add(i);
286
+ continue;
287
+ }
279
288
 
280
289
  // Check the full placeholder name
281
290
  if (shouldFieldRedacted(placeholder, fieldPatterns)) {
282
- indices.add(i);
291
+ redactedIndices.add(i);
283
292
  continue;
284
293
  }
285
294
  // For nested paths, check each segment
286
295
  const segments = parsePathSegments(placeholder);
287
296
  for (const segment of segments) {
288
297
  if (shouldFieldRedacted(segment, fieldPatterns)) {
289
- indices.add(i);
298
+ redactedIndices.add(i);
290
299
  break;
291
300
  }
292
301
  }
293
302
  }
294
- return indices;
303
+ return { redactedIndices, wildcardIndices };
295
304
  }
296
305
 
297
306
  /**
298
307
  * Redacts values in the message array based on the redacted placeholder
299
- * indices.
308
+ * indices and wildcard indices.
300
309
  * @param message The original message array.
301
310
  * @param redactedIndices Set of placeholder indices to redact.
311
+ * @param wildcardIndices Set of wildcard placeholder indices.
312
+ * @param redactedProperties The redacted properties object.
302
313
  * @param action The redaction action.
303
314
  * @returns New message array with redacted values.
304
315
  */
305
316
  function redactMessageArray(
306
317
  message: readonly unknown[],
307
318
  redactedIndices: Set<number>,
319
+ wildcardIndices: Set<number>,
320
+ redactedProperties: Record<string, unknown>,
308
321
  action: "delete" | ((value: unknown) => unknown) | undefined,
309
322
  ): readonly unknown[] {
310
- if (redactedIndices.size === 0) return message;
323
+ if (redactedIndices.size === 0 && wildcardIndices.size === 0) return message;
311
324
 
312
325
  const result: unknown[] = [];
313
326
  let placeholderIndex = 0;
@@ -318,7 +331,10 @@ function redactMessageArray(
318
331
  result.push(message[i]);
319
332
  } else {
320
333
  // Odd index: value/placeholder
321
- if (redactedIndices.has(placeholderIndex)) {
334
+ if (wildcardIndices.has(placeholderIndex)) {
335
+ // Wildcard {*}: replace with redacted properties
336
+ result.push(redactedProperties);
337
+ } else if (redactedIndices.has(placeholderIndex)) {
322
338
  if (action == null || action === "delete") {
323
339
  result.push("");
324
340
  } else {