@logtape/redaction 1.4.0-dev.409 → 1.4.0-dev.413

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.
@@ -1,407 +0,0 @@
1
- import { suite } from "@alinea/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
-
352
- { // handles circular references to prevent stack overflow
353
- const formatter: ConsoleFormatter = (record: LogRecord) => [record.message];
354
-
355
- const circularObj: Record<string, unknown> = {
356
- email: "user@example.com",
357
- };
358
- circularObj.self = circularObj;
359
-
360
- const record: LogRecord = {
361
- level: "info",
362
- category: ["test"],
363
- message: ["Circular object:", circularObj],
364
- rawMessage: "Circular object: [object Object]",
365
- timestamp: Date.now(),
366
- properties: {},
367
- };
368
-
369
- const redactedFormatter = redactByPattern(formatter, [
370
- EMAIL_ADDRESS_PATTERN,
371
- ]);
372
- const output = redactedFormatter(record);
373
-
374
- const resultData = (output[0] as unknown[])[1] as Record<string, unknown>;
375
- assertEquals(resultData.email, "REDACTED@EMAIL.ADDRESS");
376
- assert(
377
- resultData.self === resultData,
378
- "Circular reference should be preserved",
379
- );
380
- }
381
-
382
- { // redacts fields in class instances
383
- const formatter: ConsoleFormatter = (record: LogRecord) => [record.message];
384
-
385
- class User {
386
- constructor(public email: string, public name: string) {}
387
- }
388
-
389
- const record: LogRecord = {
390
- level: "info",
391
- category: ["test"],
392
- message: ["User object:", new User("user@example.com", "Alice")],
393
- rawMessage: "User object: [object Object]",
394
- timestamp: Date.now(),
395
- properties: {},
396
- };
397
-
398
- const redactedFormatter = redactByPattern(formatter, [
399
- EMAIL_ADDRESS_PATTERN,
400
- ]);
401
- const output = redactedFormatter(record);
402
-
403
- const resultUser = (output[0] as unknown[])[1] as User;
404
- assertEquals(resultUser.email, "REDACTED@EMAIL.ADDRESS");
405
- assertEquals(resultUser.name, "Alice");
406
- }
407
- });
package/src/pattern.ts DELETED
@@ -1,221 +0,0 @@
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(
184
- object: unknown,
185
- visited: Map<object, object>,
186
- ): unknown {
187
- if (typeof object === "object" && object !== null) {
188
- if (visited.has(object)) {
189
- return visited.get(object)!; // Circular reference detected
190
- }
191
- }
192
-
193
- if (typeof object === "string") return replaceString(object);
194
- if (Array.isArray(object)) {
195
- const copy: unknown[] = [];
196
- visited.set(object, copy);
197
- object.forEach((item) => copy.push(replaceObject(item, visited)));
198
- return copy;
199
- }
200
- if (typeof object === "object" && object !== null) {
201
- const redacted: Record<string, unknown> = {};
202
- visited.set(object, redacted);
203
- for (const key in object) {
204
- if (Object.prototype.hasOwnProperty.call(object, key)) {
205
- redacted[key] = replaceObject(
206
- (object as Record<string, unknown>)[key],
207
- visited,
208
- );
209
- }
210
- }
211
- return redacted;
212
- }
213
- return object;
214
- }
215
-
216
- return (record: LogRecord) => {
217
- const output = formatter(record);
218
- if (typeof output === "string") return replaceString(output);
219
- return output.map((obj) => replaceObject(obj, new Map()));
220
- };
221
- }
package/tsdown.config.ts DELETED
@@ -1,11 +0,0 @@
1
- import { defineConfig } from "tsdown";
2
-
3
- export default defineConfig({
4
- entry: "src/mod.ts",
5
- dts: {
6
- sourcemap: true,
7
- },
8
- format: ["esm", "cjs"],
9
- platform: "neutral",
10
- unbundle: true,
11
- });