@logtape/redaction 1.3.5 → 1.4.0-dev.408
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 +34 -0
- package/dist/field.cjs +9 -38
- package/dist/field.js +9 -38
- package/dist/field.js.map +1 -1
- package/dist/pattern.cjs +0 -10
- package/dist/pattern.d.cts.map +1 -1
- package/dist/pattern.d.ts.map +1 -1
- package/dist/pattern.js +0 -10
- package/dist/pattern.js.map +1 -1
- package/package.json +2 -5
- package/src/field.test.ts +584 -0
- package/src/field.ts +432 -0
- package/src/mod.ts +8 -0
- package/src/pattern.test.ts +407 -0
- package/src/pattern.ts +221 -0
- package/tsdown.config.ts +11 -0
package/deno.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@logtape/redaction",
|
|
3
|
+
"version": "1.4.0-dev.408+3fd750bb",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"exports": "./src/mod.ts",
|
|
6
|
+
"exclude": [
|
|
7
|
+
"coverage/",
|
|
8
|
+
"npm/",
|
|
9
|
+
".dnt-import-map.json"
|
|
10
|
+
],
|
|
11
|
+
"tasks": {
|
|
12
|
+
"build": "pnpm build",
|
|
13
|
+
"test": "deno test",
|
|
14
|
+
"test:node": {
|
|
15
|
+
"dependencies": [
|
|
16
|
+
"build"
|
|
17
|
+
],
|
|
18
|
+
"command": "node --experimental-transform-types --test"
|
|
19
|
+
},
|
|
20
|
+
"test:bun": {
|
|
21
|
+
"dependencies": [
|
|
22
|
+
"build"
|
|
23
|
+
],
|
|
24
|
+
"command": "bun test"
|
|
25
|
+
},
|
|
26
|
+
"test-all": {
|
|
27
|
+
"dependencies": [
|
|
28
|
+
"test",
|
|
29
|
+
"test:node",
|
|
30
|
+
"test:bun"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/field.cjs
CHANGED
|
@@ -55,7 +55,7 @@ function redactByField(sink, options = DEFAULT_REDACT_FIELDS) {
|
|
|
55
55
|
if (typeof record.rawMessage === "string") {
|
|
56
56
|
const placeholders = extractPlaceholderNames(record.rawMessage);
|
|
57
57
|
const { redactedIndices, wildcardIndices } = getRedactedPlaceholderIndices(placeholders, opts.fieldPatterns);
|
|
58
|
-
redactedMessage = redactMessageArray(record.message,
|
|
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);
|
|
@@ -96,40 +96,16 @@ function redactProperties(properties, options, visited = /* @__PURE__ */ new Map
|
|
|
96
96
|
continue;
|
|
97
97
|
}
|
|
98
98
|
const value = properties[field];
|
|
99
|
-
if (Array.isArray(value)) copy[field] =
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
if (Array.isArray(value)) copy[field] = value.map((item) => {
|
|
100
|
+
if (typeof item === "object" && item !== null) return redactProperties(item, options, visited);
|
|
101
|
+
return item;
|
|
102
|
+
});
|
|
103
|
+
else if (typeof value === "object" && value !== null) copy[field] = redactProperties(value, options, visited);
|
|
102
104
|
else copy[field] = value;
|
|
103
105
|
}
|
|
104
106
|
return copy;
|
|
105
107
|
}
|
|
106
108
|
/**
|
|
107
|
-
* Redacts sensitive fields in an array recursively.
|
|
108
|
-
* @param array The array to process.
|
|
109
|
-
* @param options The redaction options.
|
|
110
|
-
* @param visited Map of visited objects to prevent circular reference issues.
|
|
111
|
-
* @returns A new array with redacted values.
|
|
112
|
-
*/
|
|
113
|
-
function redactArray(array, options, visited) {
|
|
114
|
-
return array.map((item) => {
|
|
115
|
-
if (Array.isArray(item)) return redactArray(item, options, visited);
|
|
116
|
-
if (typeof item === "object" && item !== null) {
|
|
117
|
-
if (isBuiltInObject(item)) return item;
|
|
118
|
-
return redactProperties(item, options, visited);
|
|
119
|
-
}
|
|
120
|
-
return item;
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Checks if a value is a built-in object that should not be recursively
|
|
125
|
-
* processed (e.g., Error, Date, RegExp, Map, Set, etc.).
|
|
126
|
-
* @param value The value to check.
|
|
127
|
-
* @returns `true` if the value is a built-in object, `false` otherwise.
|
|
128
|
-
*/
|
|
129
|
-
function isBuiltInObject(value) {
|
|
130
|
-
return value instanceof Error || value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet || value instanceof Promise || value instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer || ArrayBuffer.isView(value);
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
109
|
* Checks if a field should be redacted based on the provided field patterns.
|
|
134
110
|
* @param field The field name to check.
|
|
135
111
|
* @param fieldPatterns The field patterns to match against.
|
|
@@ -212,14 +188,14 @@ function getRedactedPlaceholderIndices(placeholders, fieldPatterns) {
|
|
|
212
188
|
* Redacts values in the message array based on the redacted placeholder
|
|
213
189
|
* indices and wildcard indices.
|
|
214
190
|
* @param message The original message array.
|
|
215
|
-
* @param placeholders Array of placeholder names from the template.
|
|
216
191
|
* @param redactedIndices Set of placeholder indices to redact.
|
|
217
192
|
* @param wildcardIndices Set of wildcard placeholder indices.
|
|
218
193
|
* @param redactedProperties The redacted properties object.
|
|
219
194
|
* @param action The redaction action.
|
|
220
195
|
* @returns New message array with redacted values.
|
|
221
196
|
*/
|
|
222
|
-
function redactMessageArray(message,
|
|
197
|
+
function redactMessageArray(message, redactedIndices, wildcardIndices, redactedProperties, action) {
|
|
198
|
+
if (redactedIndices.size === 0 && wildcardIndices.size === 0) return message;
|
|
223
199
|
const result = [];
|
|
224
200
|
let placeholderIndex = 0;
|
|
225
201
|
for (let i = 0; i < message.length; i++) if (i % 2 === 0) result.push(message[i]);
|
|
@@ -227,12 +203,7 @@ function redactMessageArray(message, placeholders, redactedIndices, wildcardIndi
|
|
|
227
203
|
if (wildcardIndices.has(placeholderIndex)) result.push(redactedProperties);
|
|
228
204
|
else if (redactedIndices.has(placeholderIndex)) if (action == null || action === "delete") result.push("");
|
|
229
205
|
else result.push(action(message[i]));
|
|
230
|
-
else
|
|
231
|
-
const placeholderName = placeholders[placeholderIndex];
|
|
232
|
-
const rootKey = parsePathSegments(placeholderName)[0];
|
|
233
|
-
if (rootKey in redactedProperties) result.push(redactedProperties[rootKey]);
|
|
234
|
-
else result.push(message[i]);
|
|
235
|
-
}
|
|
206
|
+
else result.push(message[i]);
|
|
236
207
|
placeholderIndex++;
|
|
237
208
|
}
|
|
238
209
|
return result;
|
package/dist/field.js
CHANGED
|
@@ -54,7 +54,7 @@ function redactByField(sink, options = DEFAULT_REDACT_FIELDS) {
|
|
|
54
54
|
if (typeof record.rawMessage === "string") {
|
|
55
55
|
const placeholders = extractPlaceholderNames(record.rawMessage);
|
|
56
56
|
const { redactedIndices, wildcardIndices } = getRedactedPlaceholderIndices(placeholders, opts.fieldPatterns);
|
|
57
|
-
redactedMessage = redactMessageArray(record.message,
|
|
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);
|
|
@@ -95,40 +95,16 @@ function redactProperties(properties, options, visited = /* @__PURE__ */ new Map
|
|
|
95
95
|
continue;
|
|
96
96
|
}
|
|
97
97
|
const value = properties[field];
|
|
98
|
-
if (Array.isArray(value)) copy[field] =
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
if (Array.isArray(value)) copy[field] = value.map((item) => {
|
|
99
|
+
if (typeof item === "object" && item !== null) return redactProperties(item, options, visited);
|
|
100
|
+
return item;
|
|
101
|
+
});
|
|
102
|
+
else if (typeof value === "object" && value !== null) copy[field] = redactProperties(value, options, visited);
|
|
101
103
|
else copy[field] = value;
|
|
102
104
|
}
|
|
103
105
|
return copy;
|
|
104
106
|
}
|
|
105
107
|
/**
|
|
106
|
-
* Redacts sensitive fields in an array recursively.
|
|
107
|
-
* @param array The array to process.
|
|
108
|
-
* @param options The redaction options.
|
|
109
|
-
* @param visited Map of visited objects to prevent circular reference issues.
|
|
110
|
-
* @returns A new array with redacted values.
|
|
111
|
-
*/
|
|
112
|
-
function redactArray(array, options, visited) {
|
|
113
|
-
return array.map((item) => {
|
|
114
|
-
if (Array.isArray(item)) return redactArray(item, options, visited);
|
|
115
|
-
if (typeof item === "object" && item !== null) {
|
|
116
|
-
if (isBuiltInObject(item)) return item;
|
|
117
|
-
return redactProperties(item, options, visited);
|
|
118
|
-
}
|
|
119
|
-
return item;
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Checks if a value is a built-in object that should not be recursively
|
|
124
|
-
* processed (e.g., Error, Date, RegExp, Map, Set, etc.).
|
|
125
|
-
* @param value The value to check.
|
|
126
|
-
* @returns `true` if the value is a built-in object, `false` otherwise.
|
|
127
|
-
*/
|
|
128
|
-
function isBuiltInObject(value) {
|
|
129
|
-
return value instanceof Error || value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet || value instanceof Promise || value instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer || ArrayBuffer.isView(value);
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
108
|
* Checks if a field should be redacted based on the provided field patterns.
|
|
133
109
|
* @param field The field name to check.
|
|
134
110
|
* @param fieldPatterns The field patterns to match against.
|
|
@@ -211,14 +187,14 @@ function getRedactedPlaceholderIndices(placeholders, fieldPatterns) {
|
|
|
211
187
|
* Redacts values in the message array based on the redacted placeholder
|
|
212
188
|
* indices and wildcard indices.
|
|
213
189
|
* @param message The original message array.
|
|
214
|
-
* @param placeholders Array of placeholder names from the template.
|
|
215
190
|
* @param redactedIndices Set of placeholder indices to redact.
|
|
216
191
|
* @param wildcardIndices Set of wildcard placeholder indices.
|
|
217
192
|
* @param redactedProperties The redacted properties object.
|
|
218
193
|
* @param action The redaction action.
|
|
219
194
|
* @returns New message array with redacted values.
|
|
220
195
|
*/
|
|
221
|
-
function redactMessageArray(message,
|
|
196
|
+
function redactMessageArray(message, redactedIndices, wildcardIndices, redactedProperties, action) {
|
|
197
|
+
if (redactedIndices.size === 0 && wildcardIndices.size === 0) return message;
|
|
222
198
|
const result = [];
|
|
223
199
|
let placeholderIndex = 0;
|
|
224
200
|
for (let i = 0; i < message.length; i++) if (i % 2 === 0) result.push(message[i]);
|
|
@@ -226,12 +202,7 @@ function redactMessageArray(message, placeholders, redactedIndices, wildcardIndi
|
|
|
226
202
|
if (wildcardIndices.has(placeholderIndex)) result.push(redactedProperties);
|
|
227
203
|
else if (redactedIndices.has(placeholderIndex)) if (action == null || action === "delete") result.push("");
|
|
228
204
|
else result.push(action(message[i]));
|
|
229
|
-
else
|
|
230
|
-
const placeholderName = placeholders[placeholderIndex];
|
|
231
|
-
const rootKey = parsePathSegments(placeholderName)[0];
|
|
232
|
-
if (rootKey in redactedProperties) result.push(redactedProperties[rootKey]);
|
|
233
|
-
else result.push(message[i]);
|
|
234
|
-
}
|
|
205
|
+
else result.push(message[i]);
|
|
235
206
|
placeholderIndex++;
|
|
236
207
|
}
|
|
237
208
|
return result;
|
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","copy: Record<string, unknown>","array: unknown[]","visited: Map<object, object>","value: object","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 redactedMessage = redactMessageArray(\n record.message,\n placeholders,\n redactedIndices,\n wildcardIndices,\n redactedProperties,\n opts.action,\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 visited = new Map<object, object>(),\n): Record<string, unknown> {\n if (visited.has(properties)) {\n return visited.get(properties) as Record<string, unknown>;\n }\n\n const copy: Record<string, unknown> = {};\n visited.set(properties, copy);\n\n for (const field in properties) {\n if (shouldFieldRedacted(field, options.fieldPatterns)) {\n if (typeof options.action === \"function\") {\n copy[field] = options.action(properties[field]);\n }\n continue;\n }\n\n const value = properties[field];\n if (Array.isArray(value)) {\n copy[field] = redactArray(value, options, visited);\n } else if (typeof value === \"object\" && value !== null) {\n if (isBuiltInObject(value)) {\n copy[field] = value;\n } else {\n copy[field] = redactProperties(\n value as Record<string, unknown>,\n options,\n visited,\n );\n }\n } else {\n copy[field] = value;\n }\n }\n return copy;\n}\n\n/**\n * Redacts sensitive fields in an array recursively.\n * @param array The array to process.\n * @param options The redaction options.\n * @param visited Map of visited objects to prevent circular reference issues.\n * @returns A new array with redacted values.\n */\nfunction redactArray(\n array: unknown[],\n options: FieldRedactionOptions,\n visited: Map<object, object>,\n): unknown[] {\n return array.map((item) => {\n if (Array.isArray(item)) {\n return redactArray(item, options, visited);\n }\n if (typeof item === \"object\" && item !== null) {\n if (isBuiltInObject(item)) {\n return item;\n }\n return redactProperties(\n item as Record<string, unknown>,\n options,\n visited,\n );\n }\n return item;\n });\n}\n\n/**\n * Checks if a value is a built-in object that should not be recursively\n * processed (e.g., Error, Date, RegExp, Map, Set, etc.).\n * @param value The value to check.\n * @returns `true` if the value is a built-in object, `false` otherwise.\n */\nfunction isBuiltInObject(value: object): boolean {\n return value instanceof Error ||\n value instanceof Date ||\n value instanceof RegExp ||\n value instanceof Map ||\n value instanceof Set ||\n value instanceof WeakMap ||\n value instanceof WeakSet ||\n value instanceof Promise ||\n value instanceof ArrayBuffer ||\n (typeof SharedArrayBuffer !== \"undefined\" &&\n value instanceof SharedArrayBuffer) ||\n ArrayBuffer.isView(value);\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 placeholders Array of placeholder names from the template.\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 placeholders: string[],\n redactedIndices: Set<number>,\n wildcardIndices: Set<number>,\n redactedProperties: Record<string, unknown>,\n action: \"delete\" | ((value: unknown) => unknown) | undefined,\n): readonly unknown[] {\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 // For non-redacted placeholders, use value from redactedProperties\n // to ensure nested sensitive fields are redacted\n const placeholderName = placeholders[placeholderIndex];\n const rootKey = parsePathSegments(placeholderName)[0];\n if (rootKey in redactedProperties) {\n result.push(redactedProperties[rootKey]);\n } else {\n result.push(message[i]);\n }\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","copy: Record<string, unknown>","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 visited = new Map<object, object>(),\n): Record<string, unknown> {\n if (visited.has(properties)) {\n return visited.get(properties) as Record<string, unknown>;\n }\n\n const copy: Record<string, unknown> = {};\n visited.set(properties, copy);\n\n for (const field in properties) {\n if (shouldFieldRedacted(field, options.fieldPatterns)) {\n if (typeof options.action === \"function\") {\n copy[field] = options.action(properties[field]);\n }\n continue;\n }\n\n const value = properties[field];\n if (Array.isArray(value)) {\n copy[field] = value.map((item) => {\n if (typeof item === \"object\" && item !== null) {\n return redactProperties(\n item as Record<string, unknown>,\n options,\n visited,\n );\n }\n return item;\n });\n } else if (typeof value === \"object\" && value !== null) {\n copy[field] = redactProperties(\n value as Record<string, unknown>,\n options,\n visited,\n );\n } else {\n copy[field] = value;\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/dist/pattern.cjs
CHANGED
|
@@ -41,15 +41,6 @@ const JWT_PATTERN = {
|
|
|
41
41
|
pattern: /eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g,
|
|
42
42
|
replacement: "[JWT REDACTED]"
|
|
43
43
|
};
|
|
44
|
-
/**
|
|
45
|
-
* Checks if a value is a built-in object that should not be recursively
|
|
46
|
-
* processed (e.g., Error, Date, RegExp, Map, Set, etc.).
|
|
47
|
-
* @param value The value to check.
|
|
48
|
-
* @returns `true` if the value is a built-in object, `false` otherwise.
|
|
49
|
-
*/
|
|
50
|
-
function isBuiltInObject(value) {
|
|
51
|
-
return value instanceof Error || value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet || value instanceof Promise || value instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer || ArrayBuffer.isView(value);
|
|
52
|
-
}
|
|
53
44
|
function redactByPattern(formatter, patterns) {
|
|
54
45
|
for (const { pattern } of patterns) if (!pattern.global) throw new TypeError(`Pattern ${pattern} does not have the global flag set.`);
|
|
55
46
|
function replaceString(str) {
|
|
@@ -68,7 +59,6 @@ function redactByPattern(formatter, patterns) {
|
|
|
68
59
|
return copy;
|
|
69
60
|
}
|
|
70
61
|
if (typeof object === "object" && object !== null) {
|
|
71
|
-
if (isBuiltInObject(object)) return object;
|
|
72
62
|
const redacted = {};
|
|
73
63
|
visited.set(object, redacted);
|
|
74
64
|
for (const key in object) if (Object.prototype.hasOwnProperty.call(object, key)) redacted[key] = replaceObject(object[key], visited);
|
package/dist/pattern.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pattern.d.cts","names":[],"sources":["../src/pattern.ts"],"sourcesContent":[],"mappings":";;;;;;AAWA;AAsBA;AAUA;AASa,UAzCI,gBAAA,CAyCY;EAUhB;AASb;AASA;
|
|
1
|
+
{"version":3,"file":"pattern.d.cts","names":[],"sources":["../src/pattern.ts"],"sourcesContent":[],"mappings":";;;;;;AAWA;AAsBA;AAUA;AASa,UAzCI,gBAAA,CAyCY;EAUhB;AASb;AASA;AAoCA;EAA+B,SAAA,OAAA,EApGX,MAoGW;EAAA;;;AAGf;AAmChB;EAA+B,SAAA,WAAA,EAAA,MAAA,GAAA,CAAA,CAAA,KAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,SAAA,GAAA,EAAA,EAAA,GAAA,MAAA,CAAA;;;;AAGZ;;cA5HN,uBAAuB;;;;;cAUvB,4BAA4B;;;;;cAS5B,gBAAgB;;;;;;cAUhB,gBAAgB;;;;;cAShB,aAAa;;;;;KASd,iBAAA,YAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoCzB,eAAA,YACH,yBACD,oBACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmCa,eAAA,YACH,4BACD,oBACT"}
|
package/dist/pattern.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pattern.d.ts","names":[],"sources":["../src/pattern.ts"],"sourcesContent":[],"mappings":";;;;;;AAWA;AAsBA;AAUA;AASa,UAzCI,gBAAA,CAyCY;EAUhB;AASb;AASA;
|
|
1
|
+
{"version":3,"file":"pattern.d.ts","names":[],"sources":["../src/pattern.ts"],"sourcesContent":[],"mappings":";;;;;;AAWA;AAsBA;AAUA;AASa,UAzCI,gBAAA,CAyCY;EAUhB;AASb;AASA;AAoCA;EAA+B,SAAA,OAAA,EApGX,MAoGW;EAAA;;;AAGf;AAmChB;EAA+B,SAAA,WAAA,EAAA,MAAA,GAAA,CAAA,CAAA,KAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,SAAA,GAAA,EAAA,EAAA,GAAA,MAAA,CAAA;;;;AAGZ;;cA5HN,uBAAuB;;;;;cAUvB,4BAA4B;;;;;cAS5B,gBAAgB;;;;;;cAUhB,gBAAgB;;;;;cAShB,aAAa;;;;;KASd,iBAAA,YAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoCzB,eAAA,YACH,yBACD,oBACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmCa,eAAA,YACH,4BACD,oBACT"}
|
package/dist/pattern.js
CHANGED
|
@@ -40,15 +40,6 @@ const JWT_PATTERN = {
|
|
|
40
40
|
pattern: /eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g,
|
|
41
41
|
replacement: "[JWT REDACTED]"
|
|
42
42
|
};
|
|
43
|
-
/**
|
|
44
|
-
* Checks if a value is a built-in object that should not be recursively
|
|
45
|
-
* processed (e.g., Error, Date, RegExp, Map, Set, etc.).
|
|
46
|
-
* @param value The value to check.
|
|
47
|
-
* @returns `true` if the value is a built-in object, `false` otherwise.
|
|
48
|
-
*/
|
|
49
|
-
function isBuiltInObject(value) {
|
|
50
|
-
return value instanceof Error || value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet || value instanceof Promise || value instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer || ArrayBuffer.isView(value);
|
|
51
|
-
}
|
|
52
43
|
function redactByPattern(formatter, patterns) {
|
|
53
44
|
for (const { pattern } of patterns) if (!pattern.global) throw new TypeError(`Pattern ${pattern} does not have the global flag set.`);
|
|
54
45
|
function replaceString(str) {
|
|
@@ -67,7 +58,6 @@ function redactByPattern(formatter, patterns) {
|
|
|
67
58
|
return copy;
|
|
68
59
|
}
|
|
69
60
|
if (typeof object === "object" && object !== null) {
|
|
70
|
-
if (isBuiltInObject(object)) return object;
|
|
71
61
|
const redacted = {};
|
|
72
62
|
visited.set(object, redacted);
|
|
73
63
|
for (const key in object) if (Object.prototype.hasOwnProperty.call(object, key)) redacted[key] = replaceObject(object[key], visited);
|
package/dist/pattern.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pattern.js","names":["EMAIL_ADDRESS_PATTERN: RedactionPattern","CREDIT_CARD_NUMBER_PATTERN: RedactionPattern","US_SSN_PATTERN: RedactionPattern","KR_RRN_PATTERN: RedactionPattern","JWT_PATTERN: RedactionPattern","
|
|
1
|
+
{"version":3,"file":"pattern.js","names":["EMAIL_ADDRESS_PATTERN: RedactionPattern","CREDIT_CARD_NUMBER_PATTERN: RedactionPattern","US_SSN_PATTERN: RedactionPattern","KR_RRN_PATTERN: RedactionPattern","JWT_PATTERN: RedactionPattern","formatter: TextFormatter | ConsoleFormatter","patterns: RedactionPatterns","str: string","object: unknown","visited: Map<object, object>","copy: unknown[]","redacted: Record<string, unknown>","record: LogRecord"],"sources":["../src/pattern.ts"],"sourcesContent":["import type {\n ConsoleFormatter,\n LogRecord,\n TextFormatter,\n} from \"@logtape/logtape\";\n\n/**\n * A redaction pattern, which is a pair of regular expression and replacement\n * string or function.\n * @since 0.10.0\n */\nexport interface RedactionPattern {\n /**\n * The regular expression to match against. Note that it must have the\n * `g` (global) flag set, otherwise it will throw a `TypeError`.\n */\n readonly pattern: RegExp;\n\n /**\n * The replacement string or function. If the replacement is a function,\n * it will be called with the matched string and any capture groups (the same\n * signature as `String.prototype.replaceAll()`).\n */\n readonly replacement:\n | string\n // deno-lint-ignore no-explicit-any\n | ((match: string, ...rest: readonly any[]) => string);\n}\n\n/**\n * A redaction pattern for email addresses.\n * @since 0.10.0\n */\nexport const EMAIL_ADDRESS_PATTERN: RedactionPattern = {\n pattern:\n /[\\p{L}0-9.!#$%&'*+/=?^_`{|}~-]+@[\\p{L}0-9](?:[\\p{L}0-9-]{0,61}[\\p{L}0-9])?(?:\\.[\\p{L}0-9](?:[\\p{L}0-9-]{0,61}[\\p{L}0-9])?)+/gu,\n replacement: \"REDACTED@EMAIL.ADDRESS\",\n};\n\n/**\n * A redaction pattern for credit card numbers (including American Express).\n * @since 0.10.0\n */\nexport const CREDIT_CARD_NUMBER_PATTERN: RedactionPattern = {\n pattern: /(?:\\d{4}-){3}\\d{4}|(?:\\d{4}-){2}\\d{6}/g,\n replacement: \"XXXX-XXXX-XXXX-XXXX\",\n};\n\n/**\n * A redaction pattern for U.S. Social Security numbers.\n * @since 0.10.0\n */\nexport const US_SSN_PATTERN: RedactionPattern = {\n pattern: /\\d{3}-\\d{2}-\\d{4}/g,\n replacement: \"XXX-XX-XXXX\",\n};\n\n/**\n * A redaction pattern for South Korean resident registration numbers\n * (住民登錄番號).\n * @since 0.10.0\n */\nexport const KR_RRN_PATTERN: RedactionPattern = {\n pattern: /\\d{6}-\\d{7}/g,\n replacement: \"XXXXXX-XXXXXXX\",\n};\n\n/**\n * A redaction pattern for JSON Web Tokens (JWT).\n * @since 0.10.0\n */\nexport const JWT_PATTERN: RedactionPattern = {\n pattern: /eyJ[a-zA-Z0-9_-]*\\.[a-zA-Z0-9_-]*\\.[a-zA-Z0-9_-]*/g,\n replacement: \"[JWT REDACTED]\",\n};\n\n/**\n * A list of {@link RedactionPattern}s.\n * @since 0.10.0\n */\nexport type RedactionPatterns = readonly RedactionPattern[];\n\n/**\n * Applies data redaction to a {@link TextFormatter}.\n *\n * Note that there are some built-in redaction patterns:\n *\n * - {@link CREDIT_CARD_NUMBER_PATTERN}\n * - {@link EMAIL_ADDRESS_PATTERN}\n * - {@link JWT_PATTERN}\n * - {@link KR_RRN_PATTERN}\n * - {@link US_SSN_PATTERN}\n *\n * @example\n * ```ts\n * import { getFileSink } from \"@logtape/file\";\n * import { getAnsiColorFormatter } from \"@logtape/logtape\";\n * import {\n * CREDIT_CARD_NUMBER_PATTERN,\n * EMAIL_ADDRESS_PATTERN,\n * JWT_PATTERN,\n * redactByPattern,\n * } from \"@logtape/redaction\";\n *\n * const formatter = redactByPattern(getAnsiConsoleFormatter(), [\n * CREDIT_CARD_NUMBER_PATTERN,\n * EMAIL_ADDRESS_PATTERN,\n * JWT_PATTERN,\n * ]);\n * const sink = getFileSink(\"my-app.log\", { formatter });\n * ```\n * @param formatter The text formatter to apply redaction to.\n * @param patterns The redaction patterns to apply.\n * @returns The redacted text formatter.\n * @since 0.10.0\n */\nexport function redactByPattern(\n formatter: TextFormatter,\n patterns: RedactionPatterns,\n): TextFormatter;\n\n/**\n * Applies data redaction to a {@link ConsoleFormatter}.\n *\n * Note that there are some built-in redaction patterns:\n *\n * - {@link CREDIT_CARD_NUMBER_PATTERN}\n * - {@link EMAIL_ADDRESS_PATTERN}\n * - {@link JWT_PATTERN}\n * - {@link KR_RRN_PATTERN}\n * - {@link US_SSN_PATTERN}\n *\n * @example\n * ```ts\n * import { defaultConsoleFormatter, getConsoleSink } from \"@logtape/logtape\";\n * import {\n * CREDIT_CARD_NUMBER_PATTERN,\n * EMAIL_ADDRESS_PATTERN,\n * JWT_PATTERN,\n * redactByPattern,\n * } from \"@logtape/redaction\";\n *\n * const formatter = redactByPattern(defaultConsoleFormatter, [\n * CREDIT_CARD_NUMBER_PATTERN,\n * EMAIL_ADDRESS_PATTERN,\n * JWT_PATTERN,\n * ]);\n * const sink = getConsoleSink({ formatter });\n * ```\n * @param formatter The console formatter to apply redaction to.\n * @param patterns The redaction patterns to apply.\n * @returns The redacted console formatter.\n * @since 0.10.0\n */\nexport function redactByPattern(\n formatter: ConsoleFormatter,\n patterns: RedactionPatterns,\n): ConsoleFormatter;\n\nexport function redactByPattern(\n formatter: TextFormatter | ConsoleFormatter,\n patterns: RedactionPatterns,\n): (record: LogRecord) => string | readonly unknown[] {\n for (const { pattern } of patterns) {\n if (!pattern.global) {\n throw new TypeError(\n `Pattern ${pattern} does not have the global flag set.`,\n );\n }\n }\n\n function replaceString(str: string): string {\n for (const p of patterns) {\n // The following ternary operator may seem strange, but it's for\n // making TypeScript happy:\n str = typeof p.replacement === \"string\"\n ? str.replaceAll(p.pattern, p.replacement)\n : str.replaceAll(p.pattern, p.replacement);\n }\n return str;\n }\n\n function replaceObject(\n object: unknown,\n visited: Map<object, object>,\n ): unknown {\n if (typeof object === \"object\" && object !== null) {\n if (visited.has(object)) {\n return visited.get(object)!; // Circular reference detected\n }\n }\n\n if (typeof object === \"string\") return replaceString(object);\n if (Array.isArray(object)) {\n const copy: unknown[] = [];\n visited.set(object, copy);\n object.forEach((item) => copy.push(replaceObject(item, visited)));\n return copy;\n }\n if (typeof object === \"object\" && object !== null) {\n const redacted: Record<string, unknown> = {};\n visited.set(object, redacted);\n for (const key in object) {\n if (Object.prototype.hasOwnProperty.call(object, key)) {\n redacted[key] = replaceObject(\n (object as Record<string, unknown>)[key],\n visited,\n );\n }\n }\n return redacted;\n }\n return object;\n }\n\n return (record: LogRecord) => {\n const output = formatter(record);\n if (typeof output === \"string\") return replaceString(output);\n return output.map((obj) => replaceObject(obj, new Map()));\n };\n}\n"],"mappings":";;;;;AAiCA,MAAaA,wBAA0C;CACrD,SACE;CACF,aAAa;AACd;;;;;AAMD,MAAaC,6BAA+C;CAC1D,SAAS;CACT,aAAa;AACd;;;;;AAMD,MAAaC,iBAAmC;CAC9C,SAAS;CACT,aAAa;AACd;;;;;;AAOD,MAAaC,iBAAmC;CAC9C,SAAS;CACT,aAAa;AACd;;;;;AAMD,MAAaC,cAAgC;CAC3C,SAAS;CACT,aAAa;AACd;AAqFD,SAAgB,gBACdC,WACAC,UACoD;AACpD,MAAK,MAAM,EAAE,SAAS,IAAI,SACxB,MAAK,QAAQ,OACX,OAAM,IAAI,WACP,UAAU,QAAQ;CAKzB,SAAS,cAAcC,KAAqB;AAC1C,OAAK,MAAM,KAAK,SAGd,cAAa,EAAE,gBAAgB,WAC3B,IAAI,WAAW,EAAE,SAAS,EAAE,YAAY,GACxC,IAAI,WAAW,EAAE,SAAS,EAAE,YAAY;AAE9C,SAAO;CACR;CAED,SAAS,cACPC,QACAC,SACS;AACT,aAAW,WAAW,YAAY,WAAW,MAC3C;OAAI,QAAQ,IAAI,OAAO,CACrB,QAAO,QAAQ,IAAI,OAAO;EAC3B;AAGH,aAAW,WAAW,SAAU,QAAO,cAAc,OAAO;AAC5D,MAAI,MAAM,QAAQ,OAAO,EAAE;GACzB,MAAMC,OAAkB,CAAE;AAC1B,WAAQ,IAAI,QAAQ,KAAK;AACzB,UAAO,QAAQ,CAAC,SAAS,KAAK,KAAK,cAAc,MAAM,QAAQ,CAAC,CAAC;AACjE,UAAO;EACR;AACD,aAAW,WAAW,YAAY,WAAW,MAAM;GACjD,MAAMC,WAAoC,CAAE;AAC5C,WAAQ,IAAI,QAAQ,SAAS;AAC7B,QAAK,MAAM,OAAO,OAChB,KAAI,OAAO,UAAU,eAAe,KAAK,QAAQ,IAAI,CACnD,UAAS,OAAO,cACb,OAAmC,MACpC,QACD;AAGL,UAAO;EACR;AACD,SAAO;CACR;AAED,QAAO,CAACC,WAAsB;EAC5B,MAAM,SAAS,UAAU,OAAO;AAChC,aAAW,WAAW,SAAU,QAAO,cAAc,OAAO;AAC5D,SAAO,OAAO,IAAI,CAAC,QAAQ,cAAc,qBAAK,IAAI,MAAM,CAAC;CAC1D;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logtape/redaction",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0-dev.408+3fd750bb",
|
|
4
4
|
"description": "Redact sensitive data from log messages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"logging",
|
|
@@ -45,11 +45,8 @@
|
|
|
45
45
|
"./package.json": "./package.json"
|
|
46
46
|
},
|
|
47
47
|
"sideEffects": false,
|
|
48
|
-
"files": [
|
|
49
|
-
"dist/"
|
|
50
|
-
],
|
|
51
48
|
"peerDependencies": {
|
|
52
|
-
"@logtape/logtape": "^1.
|
|
49
|
+
"@logtape/logtape": "^1.4.0-dev.408+3fd750bb"
|
|
53
50
|
},
|
|
54
51
|
"devDependencies": {
|
|
55
52
|
"@alinea/suite": "^0.6.3",
|