@logtape/redaction 0.11.0 → 0.12.0-dev.181

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.
Files changed (54) hide show
  1. package/deno.json +34 -0
  2. package/dist/field.cjs +101 -0
  3. package/dist/field.d.cts +88 -0
  4. package/dist/field.d.cts.map +1 -0
  5. package/{types → dist}/field.d.ts +29 -33
  6. package/dist/field.d.ts.map +1 -0
  7. package/dist/field.js +100 -0
  8. package/dist/field.js.map +1 -0
  9. package/dist/mod.cjs +11 -0
  10. package/dist/mod.d.cts +3 -0
  11. package/dist/mod.d.ts +3 -0
  12. package/dist/mod.js +4 -0
  13. package/dist/pattern.cjs +75 -0
  14. package/dist/pattern.d.cts +126 -0
  15. package/dist/pattern.d.cts.map +1 -0
  16. package/{types → dist}/pattern.d.ts +27 -21
  17. package/dist/pattern.d.ts.map +1 -0
  18. package/dist/pattern.js +70 -0
  19. package/dist/pattern.js.map +1 -0
  20. package/field.test.ts +217 -0
  21. package/field.ts +164 -0
  22. package/mod.ts +8 -0
  23. package/package.json +28 -25
  24. package/pattern.test.ts +351 -0
  25. package/pattern.ts +209 -0
  26. package/tsdown.config.ts +11 -0
  27. package/esm/field.js +0 -112
  28. package/esm/mod.js +0 -2
  29. package/esm/package.json +0 -3
  30. package/esm/pattern.js +0 -84
  31. package/script/field.js +0 -118
  32. package/script/mod.js +0 -21
  33. package/script/package.json +0 -3
  34. package/script/pattern.js +0 -88
  35. package/types/_dnt.test_shims.d.ts.map +0 -1
  36. package/types/deps/jsr.io/@std/assert/0.222.1/_constants.d.ts.map +0 -1
  37. package/types/deps/jsr.io/@std/assert/0.222.1/_diff.d.ts.map +0 -1
  38. package/types/deps/jsr.io/@std/assert/0.222.1/_format.d.ts.map +0 -1
  39. package/types/deps/jsr.io/@std/assert/0.222.1/assert.d.ts.map +0 -1
  40. package/types/deps/jsr.io/@std/assert/0.222.1/assert_equals.d.ts.map +0 -1
  41. package/types/deps/jsr.io/@std/assert/0.222.1/assert_exists.d.ts.map +0 -1
  42. package/types/deps/jsr.io/@std/assert/0.222.1/assert_false.d.ts.map +0 -1
  43. package/types/deps/jsr.io/@std/assert/0.222.1/assert_is_error.d.ts.map +0 -1
  44. package/types/deps/jsr.io/@std/assert/0.222.1/assert_match.d.ts.map +0 -1
  45. package/types/deps/jsr.io/@std/assert/0.222.1/assert_throws.d.ts.map +0 -1
  46. package/types/deps/jsr.io/@std/assert/0.222.1/assertion_error.d.ts.map +0 -1
  47. package/types/deps/jsr.io/@std/assert/0.222.1/equal.d.ts.map +0 -1
  48. package/types/deps/jsr.io/@std/fmt/0.222.1/colors.d.ts.map +0 -1
  49. package/types/field.d.ts.map +0 -1
  50. package/types/field.test.d.ts.map +0 -1
  51. package/types/mod.d.ts +0 -3
  52. package/types/mod.d.ts.map +0 -1
  53. package/types/pattern.d.ts.map +0 -1
  54. package/types/pattern.test.d.ts.map +0 -1
@@ -0,0 +1,351 @@
1
+ import { suite } from "@hongminhee/suite";
2
+ import type {
3
+ ConsoleFormatter,
4
+ LogRecord,
5
+ TextFormatter,
6
+ } from "@logtape/logtape";
7
+ import { assert } from "@std/assert/assert";
8
+ import { assertEquals } from "@std/assert/equals";
9
+ import { assertMatch } from "@std/assert/match";
10
+ import { assertThrows } from "@std/assert/throws";
11
+ import {
12
+ CREDIT_CARD_NUMBER_PATTERN,
13
+ EMAIL_ADDRESS_PATTERN,
14
+ JWT_PATTERN,
15
+ KR_RRN_PATTERN,
16
+ redactByPattern,
17
+ type RedactionPattern,
18
+ US_SSN_PATTERN,
19
+ } from "./pattern.ts";
20
+
21
+ const test = suite(import.meta);
22
+
23
+ test("EMAIL_ADDRESS_PATTERN", () => {
24
+ const { pattern, replacement } = EMAIL_ADDRESS_PATTERN;
25
+
26
+ // Test valid email addresses
27
+ const validEmails = [
28
+ "user@example.com",
29
+ "first.last@example.co.uk",
30
+ "user+tag@example.org",
31
+ "user123@sub.domain.com",
32
+ "user-name@example.com",
33
+ "user_name@example.com",
34
+ "user.name@example-domain.co",
35
+ // Ensure international domains work:
36
+ // cSpell: disable
37
+ "用户@例子.世界",
38
+ "пользователь@пример.рф",
39
+ // cSpell: enable
40
+ ];
41
+
42
+ for (const email of validEmails) {
43
+ assertMatch(email, pattern);
44
+ pattern.lastIndex = 0;
45
+ }
46
+
47
+ // Test replacements
48
+ assertEquals(
49
+ "Contact at user@example.com for more info.".replaceAll(
50
+ pattern,
51
+ replacement as string,
52
+ ),
53
+ "Contact at REDACTED@EMAIL.ADDRESS for more info.",
54
+ );
55
+ assertEquals(
56
+ "My email is user@example.com".replaceAll(pattern, replacement as string),
57
+ "My email is REDACTED@EMAIL.ADDRESS",
58
+ );
59
+ assertEquals(
60
+ "Emails: user1@example.com and user2@example.org".replaceAll(
61
+ pattern,
62
+ replacement as string,
63
+ ),
64
+ "Emails: REDACTED@EMAIL.ADDRESS and REDACTED@EMAIL.ADDRESS",
65
+ );
66
+
67
+ // Ensure the global flag is set
68
+ assert(
69
+ pattern.global,
70
+ "EMAIL_ADDRESS_PATTERN should have the global flag set",
71
+ );
72
+ });
73
+
74
+ test("CREDIT_CARD_NUMBER_PATTERN", () => {
75
+ const { pattern, replacement } = CREDIT_CARD_NUMBER_PATTERN;
76
+
77
+ // Test valid credit card numbers with dashes
78
+ assertMatch("1234-5678-9012-3456", pattern); // Regular 16-digit card
79
+ pattern.lastIndex = 0;
80
+ assertMatch("1234-5678-901234", pattern); // American Express format
81
+ pattern.lastIndex = 0;
82
+
83
+ // Test replacements
84
+ assertEquals(
85
+ "Card: 1234-5678-9012-3456".replaceAll(pattern, replacement as string),
86
+ "Card: XXXX-XXXX-XXXX-XXXX",
87
+ );
88
+ assertEquals(
89
+ "AmEx: 1234-5678-901234".replaceAll(pattern, replacement as string),
90
+ "AmEx: XXXX-XXXX-XXXX-XXXX",
91
+ );
92
+ assertEquals(
93
+ "Cards: 1234-5678-9012-3456 and 1234-5678-901234".replaceAll(
94
+ pattern,
95
+ replacement as string,
96
+ ),
97
+ "Cards: XXXX-XXXX-XXXX-XXXX and XXXX-XXXX-XXXX-XXXX",
98
+ );
99
+ });
100
+
101
+ test("US_SSN_PATTERN", () => {
102
+ const { pattern, replacement } = US_SSN_PATTERN;
103
+
104
+ // Test valid US Social Security numbers
105
+ assertMatch("123-45-6789", pattern);
106
+ pattern.lastIndex = 0;
107
+
108
+ // Test replacements
109
+ assertEquals(
110
+ "SSN: 123-45-6789".replaceAll(pattern, replacement as string),
111
+ "SSN: XXX-XX-XXXX",
112
+ );
113
+ assertEquals(
114
+ "SSNs: 123-45-6789 and 987-65-4321".replaceAll(
115
+ pattern,
116
+ replacement as string,
117
+ ),
118
+ "SSNs: XXX-XX-XXXX and XXX-XX-XXXX",
119
+ );
120
+ });
121
+
122
+ test("KR_RRN_PATTERN", () => {
123
+ const { pattern, replacement } = KR_RRN_PATTERN;
124
+
125
+ // Test valid South Korean resident registration numbers
126
+ assertMatch("123456-7890123", pattern);
127
+ pattern.lastIndex = 0;
128
+
129
+ // Test replacements
130
+ assertEquals(
131
+ "RRN: 123456-7890123".replaceAll(pattern, replacement as string),
132
+ "RRN: XXXXXX-XXXXXXX",
133
+ );
134
+ assertEquals(
135
+ "RRNs: 123456-7890123 and 654321-0987654".replaceAll(
136
+ pattern,
137
+ replacement as string,
138
+ ),
139
+ "RRNs: XXXXXX-XXXXXXX and XXXXXX-XXXXXXX",
140
+ );
141
+ });
142
+
143
+ test("JWT_PATTERN", () => {
144
+ const { pattern, replacement } = JWT_PATTERN;
145
+
146
+ // Test valid JWT tokens
147
+ const sampleJwt =
148
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
149
+ assertMatch(sampleJwt, pattern);
150
+ pattern.lastIndex = 0;
151
+
152
+ // Test replacements
153
+ assertEquals(
154
+ `Token: ${sampleJwt}`.replaceAll(pattern, replacement as string),
155
+ "Token: [JWT REDACTED]",
156
+ );
157
+ assertEquals(
158
+ `First: ${sampleJwt}, Second: ${sampleJwt}`.replaceAll(
159
+ pattern,
160
+ replacement as string,
161
+ ),
162
+ "First: [JWT REDACTED], Second: [JWT REDACTED]",
163
+ );
164
+ });
165
+
166
+ test("redactByPattern(TextFormatter)", () => {
167
+ { // redacts sensitive information in text output
168
+ // Create a simple TextFormatter that returns a string
169
+ const formatter: TextFormatter = (record: LogRecord) => {
170
+ return `[${record.level.toUpperCase()}] ${record.message.join(" ")}`;
171
+ };
172
+
173
+ // Test data with multiple patterns to redact
174
+ const record: LogRecord = {
175
+ level: "info",
176
+ category: ["test"],
177
+ message: [
178
+ "Sensitive info: email = user@example.com, cc = 1234-5678-9012-3456, ssn = 123-45-6789",
179
+ ],
180
+ rawMessage:
181
+ "Sensitive info: email = user@example.com, cc = 1234-5678-9012-3456, ssn = 123-45-6789",
182
+ timestamp: Date.now(),
183
+ properties: {},
184
+ };
185
+
186
+ // Apply redaction with multiple patterns
187
+ const redactedFormatter = redactByPattern(formatter, [
188
+ EMAIL_ADDRESS_PATTERN,
189
+ CREDIT_CARD_NUMBER_PATTERN,
190
+ US_SSN_PATTERN,
191
+ ]);
192
+
193
+ const output = redactedFormatter(record);
194
+
195
+ // Verify all sensitive data was redacted
196
+ assertEquals(
197
+ output,
198
+ "[INFO] Sensitive info: email = REDACTED@EMAIL.ADDRESS, cc = XXXX-XXXX-XXXX-XXXX, ssn = XXX-XX-XXXX",
199
+ );
200
+ }
201
+
202
+ { // handles function-based replacements
203
+ const formatter: TextFormatter = (record: LogRecord) => {
204
+ return record.message.join(" ");
205
+ };
206
+
207
+ // Custom pattern with function replacement
208
+ const customPattern: RedactionPattern = {
209
+ pattern: /\b(password|pw)=([^\s,]+)/g,
210
+ replacement: (_match, key) => `${key}=[HIDDEN]`,
211
+ };
212
+
213
+ const record: LogRecord = {
214
+ level: "info",
215
+ category: ["test"],
216
+ message: ["Credentials: password=secret123, pw=another-secret"],
217
+ rawMessage: "Credentials: password=secret123, pw=another-secret",
218
+ timestamp: Date.now(),
219
+ properties: {},
220
+ };
221
+
222
+ const redactedFormatter = redactByPattern(formatter, [customPattern]);
223
+ const output = redactedFormatter(record);
224
+
225
+ assertEquals(
226
+ output,
227
+ "Credentials: password=[HIDDEN], pw=[HIDDEN]",
228
+ );
229
+ }
230
+
231
+ { // throws error if global flag is not set
232
+ const formatter: TextFormatter = (record: LogRecord) =>
233
+ record.message.join(" ");
234
+
235
+ const invalidPattern: RedactionPattern = {
236
+ pattern: /password/, // Missing global flag
237
+ replacement: "****",
238
+ };
239
+
240
+ assertThrows(
241
+ () => redactByPattern(formatter, [invalidPattern]),
242
+ TypeError,
243
+ "does not have the global flag set",
244
+ );
245
+ }
246
+ });
247
+
248
+ test("redactByPattern(ConsoleFormatter)", () => {
249
+ { // redacts sensitive information in console formatter arrays
250
+ // Create a simple ConsoleFormatter that returns an array of values
251
+ const formatter: ConsoleFormatter = (record: LogRecord) => {
252
+ return [
253
+ `[${record.level.toUpperCase()}]`,
254
+ ...record.message,
255
+ ];
256
+ };
257
+
258
+ // Create test record with sensitive data
259
+ const record: LogRecord = {
260
+ level: "info",
261
+ category: ["test"],
262
+ message: [
263
+ "User data:",
264
+ {
265
+ name: "John Doe",
266
+ email: "john@example.com",
267
+ creditCard: "1234-5678-9012-3456",
268
+ },
269
+ ],
270
+ rawMessage: "User data: [object Object]",
271
+ timestamp: Date.now(),
272
+ properties: {},
273
+ };
274
+
275
+ // Apply redaction
276
+ const redactedFormatter = redactByPattern(formatter, [
277
+ EMAIL_ADDRESS_PATTERN,
278
+ CREDIT_CARD_NUMBER_PATTERN,
279
+ ]);
280
+
281
+ const output = redactedFormatter(record);
282
+
283
+ // Verify output structure is preserved and data is redacted
284
+ assertEquals(output[0], "[INFO]");
285
+ assertEquals(output[1], "User data:");
286
+ assertEquals(
287
+ (output[2] as { name: string; email: string; creditCard: string }).name,
288
+ "John Doe",
289
+ );
290
+ assertEquals(
291
+ (output[2] as { name: string; email: string; creditCard: string })
292
+ .email,
293
+ "REDACTED@EMAIL.ADDRESS",
294
+ );
295
+ assertEquals(
296
+ (output[2] as { name: string; email: string; creditCard: string })
297
+ .creditCard,
298
+ "XXXX-XXXX-XXXX-XXXX",
299
+ );
300
+ }
301
+
302
+ { // handles nested objects and arrays in console output
303
+ const formatter: ConsoleFormatter = (record: LogRecord) => {
304
+ return [record.level, record.message];
305
+ };
306
+
307
+ const nestedData = {
308
+ user: {
309
+ contact: {
310
+ email: "user@example.com",
311
+ phone: "123-456-7890",
312
+ },
313
+ payment: {
314
+ cards: [
315
+ "1234-5678-9012-3456",
316
+ "8765-4321-8765-4321",
317
+ ],
318
+ },
319
+ documents: {
320
+ ssn: "123-45-6789",
321
+ },
322
+ },
323
+ };
324
+
325
+ const record: LogRecord = {
326
+ level: "info",
327
+ category: ["test"],
328
+ message: ["Data:", nestedData],
329
+ rawMessage: "Data: [object Object]",
330
+ timestamp: Date.now(),
331
+ properties: {},
332
+ };
333
+
334
+ const redactedFormatter = redactByPattern(formatter, [
335
+ EMAIL_ADDRESS_PATTERN,
336
+ CREDIT_CARD_NUMBER_PATTERN,
337
+ US_SSN_PATTERN,
338
+ ]);
339
+
340
+ const output = redactedFormatter(record);
341
+
342
+ // Verify deep redaction in nested structures
343
+ const resultData =
344
+ (output[1] as unknown[])[1] as unknown as typeof nestedData;
345
+ assertEquals(resultData.user.contact.email, "REDACTED@EMAIL.ADDRESS");
346
+ assertEquals(resultData.user.contact.phone, "123-456-7890"); // Not redacted
347
+ assertEquals(resultData.user.payment.cards[0], "XXXX-XXXX-XXXX-XXXX");
348
+ assertEquals(resultData.user.payment.cards[1], "XXXX-XXXX-XXXX-XXXX");
349
+ assertEquals(resultData.user.documents.ssn, "XXX-XX-XXXX");
350
+ }
351
+ });
package/pattern.ts ADDED
@@ -0,0 +1,209 @@
1
+ import type {
2
+ ConsoleFormatter,
3
+ LogRecord,
4
+ TextFormatter,
5
+ } from "@logtape/logtape";
6
+
7
+ /**
8
+ * A redaction pattern, which is a pair of regular expression and replacement
9
+ * string or function.
10
+ * @since 0.10.0
11
+ */
12
+ export interface RedactionPattern {
13
+ /**
14
+ * The regular expression to match against. Note that it must have the
15
+ * `g` (global) flag set, otherwise it will throw a `TypeError`.
16
+ */
17
+ readonly pattern: RegExp;
18
+
19
+ /**
20
+ * The replacement string or function. If the replacement is a function,
21
+ * it will be called with the matched string and any capture groups (the same
22
+ * signature as `String.prototype.replaceAll()`).
23
+ */
24
+ readonly replacement:
25
+ | string
26
+ // deno-lint-ignore no-explicit-any
27
+ | ((match: string, ...rest: readonly any[]) => string);
28
+ }
29
+
30
+ /**
31
+ * A redaction pattern for email addresses.
32
+ * @since 0.10.0
33
+ */
34
+ export const EMAIL_ADDRESS_PATTERN: RedactionPattern = {
35
+ pattern:
36
+ /[\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,
37
+ replacement: "REDACTED@EMAIL.ADDRESS",
38
+ };
39
+
40
+ /**
41
+ * A redaction pattern for credit card numbers (including American Express).
42
+ * @since 0.10.0
43
+ */
44
+ export const CREDIT_CARD_NUMBER_PATTERN: RedactionPattern = {
45
+ pattern: /(?:\d{4}-){3}\d{4}|(?:\d{4}-){2}\d{6}/g,
46
+ replacement: "XXXX-XXXX-XXXX-XXXX",
47
+ };
48
+
49
+ /**
50
+ * A redaction pattern for U.S. Social Security numbers.
51
+ * @since 0.10.0
52
+ */
53
+ export const US_SSN_PATTERN: RedactionPattern = {
54
+ pattern: /\d{3}-\d{2}-\d{4}/g,
55
+ replacement: "XXX-XX-XXXX",
56
+ };
57
+
58
+ /**
59
+ * A redaction pattern for South Korean resident registration numbers
60
+ * (住民登錄番號).
61
+ * @since 0.10.0
62
+ */
63
+ export const KR_RRN_PATTERN: RedactionPattern = {
64
+ pattern: /\d{6}-\d{7}/g,
65
+ replacement: "XXXXXX-XXXXXXX",
66
+ };
67
+
68
+ /**
69
+ * A redaction pattern for JSON Web Tokens (JWT).
70
+ * @since 0.10.0
71
+ */
72
+ export const JWT_PATTERN: RedactionPattern = {
73
+ pattern: /eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g,
74
+ replacement: "[JWT REDACTED]",
75
+ };
76
+
77
+ /**
78
+ * A list of {@link RedactionPattern}s.
79
+ * @since 0.10.0
80
+ */
81
+ export type RedactionPatterns = readonly RedactionPattern[];
82
+
83
+ /**
84
+ * Applies data redaction to a {@link TextFormatter}.
85
+ *
86
+ * Note that there are some built-in redaction patterns:
87
+ *
88
+ * - {@link CREDIT_CARD_NUMBER_PATTERN}
89
+ * - {@link EMAIL_ADDRESS_PATTERN}
90
+ * - {@link JWT_PATTERN}
91
+ * - {@link KR_RRN_PATTERN}
92
+ * - {@link US_SSN_PATTERN}
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * import { getFileSink } from "@logtape/file";
97
+ * import { getAnsiColorFormatter } from "@logtape/logtape";
98
+ * import {
99
+ * CREDIT_CARD_NUMBER_PATTERN,
100
+ * EMAIL_ADDRESS_PATTERN,
101
+ * JWT_PATTERN,
102
+ * redactByPattern,
103
+ * } from "@logtape/redaction";
104
+ *
105
+ * const formatter = redactByPattern(getAnsiConsoleFormatter(), [
106
+ * CREDIT_CARD_NUMBER_PATTERN,
107
+ * EMAIL_ADDRESS_PATTERN,
108
+ * JWT_PATTERN,
109
+ * ]);
110
+ * const sink = getFileSink("my-app.log", { formatter });
111
+ * ```
112
+ * @param formatter The text formatter to apply redaction to.
113
+ * @param patterns The redaction patterns to apply.
114
+ * @returns The redacted text formatter.
115
+ * @since 0.10.0
116
+ */
117
+ export function redactByPattern(
118
+ formatter: TextFormatter,
119
+ patterns: RedactionPatterns,
120
+ ): TextFormatter;
121
+
122
+ /**
123
+ * Applies data redaction to a {@link ConsoleFormatter}.
124
+ *
125
+ * Note that there are some built-in redaction patterns:
126
+ *
127
+ * - {@link CREDIT_CARD_NUMBER_PATTERN}
128
+ * - {@link EMAIL_ADDRESS_PATTERN}
129
+ * - {@link JWT_PATTERN}
130
+ * - {@link KR_RRN_PATTERN}
131
+ * - {@link US_SSN_PATTERN}
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * import { defaultConsoleFormatter, getConsoleSink } from "@logtape/logtape";
136
+ * import {
137
+ * CREDIT_CARD_NUMBER_PATTERN,
138
+ * EMAIL_ADDRESS_PATTERN,
139
+ * JWT_PATTERN,
140
+ * redactByPattern,
141
+ * } from "@logtape/redaction";
142
+ *
143
+ * const formatter = redactByPattern(defaultConsoleFormatter, [
144
+ * CREDIT_CARD_NUMBER_PATTERN,
145
+ * EMAIL_ADDRESS_PATTERN,
146
+ * JWT_PATTERN,
147
+ * ]);
148
+ * const sink = getConsoleSink({ formatter });
149
+ * ```
150
+ * @param formatter The console formatter to apply redaction to.
151
+ * @param patterns The redaction patterns to apply.
152
+ * @returns The redacted console formatter.
153
+ * @since 0.10.0
154
+ */
155
+ export function redactByPattern(
156
+ formatter: ConsoleFormatter,
157
+ patterns: RedactionPatterns,
158
+ ): ConsoleFormatter;
159
+
160
+ export function redactByPattern(
161
+ formatter: TextFormatter | ConsoleFormatter,
162
+ patterns: RedactionPatterns,
163
+ ): (record: LogRecord) => string | readonly unknown[] {
164
+ for (const { pattern } of patterns) {
165
+ if (!pattern.global) {
166
+ throw new TypeError(
167
+ `Pattern ${pattern} does not have the global flag set.`,
168
+ );
169
+ }
170
+ }
171
+
172
+ function replaceString(str: string): string {
173
+ for (const p of patterns) {
174
+ // The following ternary operator may seem strange, but it's for
175
+ // making TypeScript happy:
176
+ str = typeof p.replacement === "string"
177
+ ? str.replaceAll(p.pattern, p.replacement)
178
+ : str.replaceAll(p.pattern, p.replacement);
179
+ }
180
+ return str;
181
+ }
182
+
183
+ function replaceObject(object: unknown): unknown {
184
+ if (typeof object === "string") return replaceString(object);
185
+ else if (Array.isArray(object)) return object.map(replaceObject);
186
+ else if (typeof object === "object" && object !== null) {
187
+ // Check if object is a vanilla object:
188
+ if (
189
+ Object.getPrototypeOf(object) === Object.prototype ||
190
+ Object.getPrototypeOf(object) === null
191
+ ) {
192
+ const redacted: Record<string, unknown> = {};
193
+ for (const key in object) {
194
+ redacted[key] =
195
+ // @ts-ignore: object always has key
196
+ replaceObject(object[key]);
197
+ }
198
+ return redacted;
199
+ }
200
+ }
201
+ return object;
202
+ }
203
+
204
+ return (record: LogRecord) => {
205
+ const output = formatter(record);
206
+ if (typeof output === "string") return replaceString(output);
207
+ return output.map(replaceObject);
208
+ };
209
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ entry: "mod.ts",
5
+ dts: {
6
+ sourcemap: true,
7
+ },
8
+ format: ["esm", "cjs"],
9
+ platform: "neutral",
10
+ unbundle: true,
11
+ });
package/esm/field.js DELETED
@@ -1,112 +0,0 @@
1
- /**
2
- * Default field patterns for redaction. These patterns will match
3
- * common sensitive fields such as passwords, tokens, and personal
4
- * information.
5
- * @since 0.10.0
6
- */
7
- export const DEFAULT_REDACT_FIELDS = [
8
- /pass(?:code|phrase|word)/i,
9
- /secret/i,
10
- /token/i,
11
- /key/i,
12
- /credential/i,
13
- /auth/i,
14
- /signature/i,
15
- /sensitive/i,
16
- /private/i,
17
- /ssn/i,
18
- /email/i,
19
- /phone/i,
20
- /address/i,
21
- ];
22
- /**
23
- * Redacts properties in a {@link LogRecord} based on the provided field
24
- * patterns and action.
25
- *
26
- * Note that it is a decorator which wraps the sink and redacts properties
27
- * before passing them to the sink.
28
- *
29
- * @example
30
- * ```ts
31
- * import { getConsoleSink } from "@logtape/logtape";
32
- * import { redactByField } from "@logtape/redaction";
33
- *
34
- * const sink = redactByField(getConsoleSink());
35
- * ```
36
- *
37
- * @param sink The sink to wrap.
38
- * @param options The redaction options.
39
- * @returns The wrapped sink.
40
- * @since 0.10.0
41
- */
42
- export function redactByField(sink, options = DEFAULT_REDACT_FIELDS) {
43
- const opts = Array.isArray(options) ? { fieldPatterns: options } : options;
44
- const wrapped = (record) => {
45
- sink({ ...record, properties: redactProperties(record.properties, opts) });
46
- };
47
- if (Symbol.dispose in sink)
48
- wrapped[Symbol.dispose] = sink[Symbol.dispose];
49
- if (Symbol.asyncDispose in sink) {
50
- wrapped[Symbol.asyncDispose] = sink[Symbol.asyncDispose];
51
- }
52
- return wrapped;
53
- }
54
- /**
55
- * Redacts properties from an object based on specified field patterns.
56
- *
57
- * This function creates a shallow copy of the input object and applies
58
- * redaction rules to its properties. For properties that match the redaction
59
- * patterns, the function either removes them or transforms their values based
60
- * on the provided action.
61
- *
62
- * The redaction process is recursive and will be applied to nested objects
63
- * as well, allowing for deep redaction of sensitive data in complex object
64
- * structures.
65
- * @param properties The properties to redact.
66
- * @param options The redaction options.
67
- * @returns The redacted properties.
68
- * @since 0.10.0
69
- */
70
- export function redactProperties(properties, options) {
71
- const copy = { ...properties };
72
- for (const field in copy) {
73
- if (shouldFieldRedacted(field, options.fieldPatterns)) {
74
- if (options.action == null || options.action === "delete") {
75
- delete copy[field];
76
- }
77
- else {
78
- copy[field] = options.action(copy[field]);
79
- }
80
- continue;
81
- }
82
- const value = copy[field];
83
- // Check if value is a vanilla object:
84
- if (typeof value === "object" && value !== null &&
85
- (Object.getPrototypeOf(value) === Object.prototype ||
86
- Object.getPrototypeOf(value) === null)) {
87
- // @ts-ignore: value is always Record<string, unknown>
88
- copy[field] = redactProperties(value, options);
89
- }
90
- }
91
- return copy;
92
- }
93
- /**
94
- * Checks if a field should be redacted based on the provided field patterns.
95
- * @param field The field name to check.
96
- * @param fieldPatterns The field patterns to match against.
97
- * @returns `true` if the field should be redacted, `false` otherwise.
98
- * @since 0.10.0
99
- */
100
- export function shouldFieldRedacted(field, fieldPatterns) {
101
- for (const fieldPattern of fieldPatterns) {
102
- if (typeof fieldPattern === "string") {
103
- if (fieldPattern === field)
104
- return true;
105
- }
106
- else {
107
- if (fieldPattern.test(field))
108
- return true;
109
- }
110
- }
111
- return false;
112
- }
package/esm/mod.js DELETED
@@ -1,2 +0,0 @@
1
- export { DEFAULT_REDACT_FIELDS, redactByField, } from "./field.js";
2
- export * from "./pattern.js";
package/esm/package.json DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "type": "module"
3
- }