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

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/src/field.test.ts DELETED
@@ -1,584 +0,0 @@
1
- import { suite } from "@alinea/suite";
2
- import type { LogRecord, Sink } from "@logtape/logtape";
3
- import { assert } from "@std/assert/assert";
4
- import { assertEquals } from "@std/assert/equals";
5
- import { assertExists } from "@std/assert/exists";
6
- import { assertFalse } from "@std/assert/false";
7
- import {
8
- type FieldPatterns,
9
- redactByField,
10
- redactProperties,
11
- shouldFieldRedacted,
12
- } from "./field.ts";
13
-
14
- const test = suite(import.meta);
15
-
16
- test("shouldFieldRedacted()", () => {
17
- { // matches string pattern
18
- const fieldPatterns: FieldPatterns = ["password", "secret"];
19
- assertEquals(shouldFieldRedacted("password", fieldPatterns), true);
20
- assertEquals(shouldFieldRedacted("secret", fieldPatterns), true);
21
- assertEquals(shouldFieldRedacted("username", fieldPatterns), false);
22
- }
23
-
24
- { // matches regex pattern
25
- const fieldPatterns: FieldPatterns = [/pass/i, /secret/i];
26
- assertEquals(shouldFieldRedacted("password", fieldPatterns), true);
27
- assertEquals(shouldFieldRedacted("secretKey", fieldPatterns), true);
28
- assertEquals(shouldFieldRedacted("myPassword", fieldPatterns), true);
29
- assertEquals(shouldFieldRedacted("username", fieldPatterns), false);
30
- }
31
-
32
- { // case sensitivity in regex
33
- const caseSensitivePatterns: FieldPatterns = [/pass/, /secret/];
34
- const caseInsensitivePatterns: FieldPatterns = [/pass/i, /secret/i];
35
-
36
- assertEquals(shouldFieldRedacted("Password", caseSensitivePatterns), false);
37
- assertEquals(
38
- shouldFieldRedacted("Password", caseInsensitivePatterns),
39
- true,
40
- );
41
- }
42
- });
43
-
44
- test("redactProperties()", () => {
45
- { // delete action (default)
46
- const properties = {
47
- username: "user123",
48
- password: "secret123",
49
- email: "user@example.com",
50
- message: "Hello world",
51
- };
52
-
53
- const result = redactProperties(properties, {
54
- fieldPatterns: ["password", "email"],
55
- });
56
-
57
- assert("username" in result);
58
- assertFalse("password" in result);
59
- assertFalse("email" in result);
60
- assert("message" in result);
61
-
62
- const nestedObject = {
63
- ...properties,
64
- nested: {
65
- foo: "bar",
66
- baz: "qux",
67
- passphrase: "asdf",
68
- },
69
- };
70
- const result2 = redactProperties(nestedObject, {
71
- fieldPatterns: ["password", "email", "passphrase"],
72
- });
73
-
74
- assert("username" in result2);
75
- assertFalse("password" in result2);
76
- assertFalse("email" in result2);
77
- assert("message" in result2);
78
- assert("nested" in result2);
79
- assert(typeof result2.nested === "object");
80
- assertExists(result2.nested);
81
- assert("foo" in result2.nested);
82
- assert("baz" in result2.nested);
83
- assertFalse("passphrase" in result2.nested);
84
- }
85
-
86
- { // custom action function
87
- const properties = {
88
- username: "user123",
89
- password: "secret123",
90
- token: "abc123",
91
- message: "Hello world",
92
- };
93
-
94
- const result = redactProperties(properties, {
95
- fieldPatterns: [/password/i, /token/i],
96
- action: () => "REDACTED",
97
- });
98
-
99
- assertEquals(result.username, "user123");
100
- assertEquals(result.password, "REDACTED");
101
- assertEquals(result.token, "REDACTED");
102
- assertEquals(result.message, "Hello world");
103
- }
104
-
105
- { // preserves other properties
106
- const properties = {
107
- username: "user123",
108
- data: { nested: "value" },
109
- sensitive: "hidden",
110
- };
111
-
112
- const result = redactProperties(properties, {
113
- fieldPatterns: ["sensitive"],
114
- });
115
-
116
- assertEquals(result.username, "user123");
117
- assertEquals(result.data, { nested: "value" });
118
- assertFalse("sensitive" in result);
119
- }
120
-
121
- { // redacts fields in objects within arrays
122
- const properties = {
123
- configs: [
124
- { password: "secret", username: "user1" },
125
- { token: "abc", email: "user2@example.com" },
126
- ],
127
- };
128
-
129
- const result = redactProperties(properties, {
130
- fieldPatterns: ["password", "token"],
131
- });
132
-
133
- // deno-lint-ignore no-explicit-any
134
- const configs = result.configs as any;
135
- assertEquals(configs.length, 2);
136
- assertEquals(configs[0], { username: "user1" });
137
- assertEquals(configs[1], { email: "user2@example.com" });
138
- }
139
-
140
- { // preserves non-object items in arrays
141
- const properties = {
142
- data: [
143
- { password: "secret" },
144
- "plain string",
145
- 42,
146
- { token: "abc" },
147
- ],
148
- };
149
-
150
- const result = redactProperties(properties, {
151
- fieldPatterns: ["password", "token"],
152
- });
153
-
154
- // deno-lint-ignore no-explicit-any
155
- const data = result.data as any;
156
- assertEquals(data.length, 4);
157
- assertEquals(data[0], {});
158
- assertEquals(data[1], "plain string");
159
- assertEquals(data[2], 42);
160
- assertEquals(data[3], {});
161
- }
162
-
163
- { // redacts nested arrays within objects in arrays
164
- const properties = {
165
- items: [
166
- {
167
- config: {
168
- password: "secret",
169
- nestedArray: [
170
- { token: "abc", value: 1 },
171
- { key: "xyz", value: 2 },
172
- ],
173
- },
174
- },
175
- ],
176
- };
177
-
178
- const result = redactProperties(properties, {
179
- fieldPatterns: ["password", "token", "key"],
180
- });
181
-
182
- // deno-lint-ignore no-explicit-any
183
- const items = result.items as any;
184
- // deno-lint-ignore no-explicit-any
185
- const first = items[0] as any;
186
- // deno-lint-ignore no-explicit-any
187
- const nestedArray = first.config.nestedArray as any;
188
- assertEquals(items.length, 1);
189
- assertEquals(first.config.password, undefined);
190
- assertEquals(nestedArray.length, 2);
191
- assertEquals(nestedArray[0], { value: 1 });
192
- assertEquals(nestedArray[1], { value: 2 });
193
- }
194
-
195
- { // uses custom action in arrays
196
- const properties = {
197
- users: [
198
- { password: "secret1", name: "user1" },
199
- { password: "secret2", name: "user2" },
200
- ],
201
- };
202
-
203
- const result = redactProperties(properties, {
204
- fieldPatterns: ["password"],
205
- action: () => "[REDACTED]",
206
- });
207
-
208
- // deno-lint-ignore no-explicit-any
209
- const users = result.users as any;
210
- assertEquals(users.length, 2);
211
- assertEquals(users[0], {
212
- password: "[REDACTED]",
213
- name: "user1",
214
- });
215
- assertEquals(users[1], {
216
- password: "[REDACTED]",
217
- name: "user2",
218
- });
219
- }
220
-
221
- { // handles circular references to prevent stack overflow
222
- const obj: Record<string, unknown> = {
223
- a: 1,
224
- password: "some-password",
225
- };
226
- obj.self = obj; // Create circular reference
227
-
228
- const result = redactProperties(obj, {
229
- fieldPatterns: ["password"],
230
- action: () => "REDACTED",
231
- });
232
-
233
- assertEquals(result.a, 1);
234
- assertEquals(result.password, "REDACTED");
235
- assert(result.self === result, "Circular reference should be preserved");
236
- }
237
-
238
- { // redacts fields in class instances
239
- class User {
240
- constructor(public name: string, public password: string) {}
241
- }
242
-
243
- const properties = {
244
- user: new User("Alice", "alice-secret-password"),
245
- };
246
-
247
- const result = redactProperties(properties, {
248
- fieldPatterns: ["password"],
249
- action: () => "REDACTED",
250
- });
251
-
252
- const redactedUser = result.user as User;
253
- assertEquals(redactedUser.name, "Alice");
254
- assertEquals(redactedUser.password, "REDACTED");
255
- }
256
- });
257
-
258
- test("redactByField()", async () => {
259
- { // wraps sink and redacts properties
260
- const records: LogRecord[] = [];
261
- const originalSink: Sink = (record) => records.push(record);
262
-
263
- const wrappedSink = redactByField(originalSink, {
264
- fieldPatterns: ["password", "token"],
265
- });
266
-
267
- const record: LogRecord = {
268
- level: "info",
269
- category: ["test"],
270
- message: ["Test message"],
271
- rawMessage: "Test message",
272
- timestamp: Date.now(),
273
- properties: {
274
- username: "user123",
275
- password: "secret123",
276
- token: "abc123",
277
- },
278
- };
279
-
280
- wrappedSink(record);
281
-
282
- assertEquals(records.length, 1);
283
- assert("username" in records[0].properties);
284
- assertFalse("password" in records[0].properties);
285
- assertFalse("token" in records[0].properties);
286
- }
287
-
288
- { // uses default field patterns when not specified
289
- const records: LogRecord[] = [];
290
- const originalSink: Sink = (record) => records.push(record);
291
-
292
- const wrappedSink = redactByField(originalSink);
293
-
294
- const record: LogRecord = {
295
- level: "info",
296
- category: ["test"],
297
- message: ["Test message"],
298
- rawMessage: "Test message",
299
- timestamp: Date.now(),
300
- properties: {
301
- username: "user123",
302
- password: "secret123",
303
- email: "user@example.com",
304
- apiKey: "xyz789",
305
- },
306
- };
307
-
308
- wrappedSink(record);
309
-
310
- assertEquals(records.length, 1);
311
- assert("username" in records[0].properties);
312
- assertFalse("password" in records[0].properties);
313
- assertFalse("email" in records[0].properties);
314
- assertFalse("apiKey" in records[0].properties);
315
- }
316
-
317
- { // preserves Disposable behavior
318
- let disposed = false;
319
- const originalSink: Sink & Disposable = Object.assign(
320
- (_record: LogRecord) => {},
321
- {
322
- [Symbol.dispose]: () => {
323
- disposed = true;
324
- },
325
- },
326
- );
327
-
328
- const wrappedSink = redactByField(originalSink) as Sink & Disposable;
329
-
330
- assert(Symbol.dispose in wrappedSink);
331
- wrappedSink[Symbol.dispose]();
332
- assert(disposed);
333
- }
334
-
335
- { // preserves AsyncDisposable behavior
336
- let disposed = false;
337
- const originalSink: Sink & AsyncDisposable = Object.assign(
338
- (_record: LogRecord) => {},
339
- {
340
- [Symbol.asyncDispose]: () => {
341
- disposed = true;
342
- return Promise.resolve();
343
- },
344
- },
345
- );
346
-
347
- const wrappedSink = redactByField(originalSink) as Sink & AsyncDisposable;
348
-
349
- assert(Symbol.asyncDispose in wrappedSink);
350
- await wrappedSink[Symbol.asyncDispose]();
351
- assert(disposed);
352
- }
353
-
354
- { // redacts fields in arrays from issue #94
355
- const records: LogRecord[] = [];
356
- const originalSink: Sink = (record) => records.push(record);
357
-
358
- const wrappedSink = redactByField(originalSink, {
359
- fieldPatterns: ["password"],
360
- });
361
-
362
- const record: LogRecord = {
363
- level: "info",
364
- category: ["test"],
365
- message: ["Loaded config"],
366
- rawMessage: "Loaded config",
367
- timestamp: Date.now(),
368
- properties: {
369
- configs: [{ password: "secret", username: "user" }],
370
- },
371
- };
372
-
373
- wrappedSink(record);
374
-
375
- assertEquals(records.length, 1);
376
- // deno-lint-ignore no-explicit-any
377
- const configs = records[0].properties.configs as any;
378
- assertEquals(configs[0], { username: "user" });
379
- }
380
-
381
- { // redacts values in message array (string template)
382
- const records: LogRecord[] = [];
383
- const wrappedSink = redactByField((r) => records.push(r), {
384
- fieldPatterns: ["password"],
385
- action: () => "[REDACTED]",
386
- });
387
-
388
- wrappedSink({
389
- level: "info",
390
- category: ["test"],
391
- message: ["Password is ", "supersecret", ""],
392
- rawMessage: "Password is {password}",
393
- timestamp: Date.now(),
394
- properties: { password: "supersecret" },
395
- });
396
-
397
- assertEquals(records[0].message, ["Password is ", "[REDACTED]", ""]);
398
- assertEquals(records[0].properties.password, "[REDACTED]");
399
- }
400
-
401
- { // redacts multiple sensitive fields in message
402
- const records: LogRecord[] = [];
403
- const wrappedSink = redactByField((r) => records.push(r), {
404
- fieldPatterns: ["password", "email"],
405
- action: () => "[REDACTED]",
406
- });
407
-
408
- wrappedSink({
409
- level: "info",
410
- category: ["test"],
411
- message: ["Login: ", "user@example.com", " with ", "secret123", ""],
412
- rawMessage: "Login: {email} with {password}",
413
- timestamp: Date.now(),
414
- properties: { email: "user@example.com", password: "secret123" },
415
- });
416
-
417
- assertEquals(records[0].message[1], "[REDACTED]");
418
- assertEquals(records[0].message[3], "[REDACTED]");
419
- }
420
-
421
- { // redacts nested property path in message
422
- const records: LogRecord[] = [];
423
- const wrappedSink = redactByField((r) => records.push(r), {
424
- fieldPatterns: ["password"],
425
- action: () => "[REDACTED]",
426
- });
427
-
428
- wrappedSink({
429
- level: "info",
430
- category: ["test"],
431
- message: ["User password: ", "secret", ""],
432
- rawMessage: "User password: {user.password}",
433
- timestamp: Date.now(),
434
- properties: { user: { password: "secret" } },
435
- });
436
-
437
- assertEquals(records[0].message[1], "[REDACTED]");
438
- }
439
-
440
- { // delete action uses empty string in message
441
- const records: LogRecord[] = [];
442
- const wrappedSink = redactByField((r) => records.push(r), {
443
- fieldPatterns: ["password"],
444
- });
445
-
446
- wrappedSink({
447
- level: "info",
448
- category: ["test"],
449
- message: ["Password: ", "secret", ""],
450
- rawMessage: "Password: {password}",
451
- timestamp: Date.now(),
452
- properties: { password: "secret" },
453
- });
454
-
455
- assertEquals(records[0].message[1], "");
456
- assertFalse("password" in records[0].properties);
457
- }
458
-
459
- { // non-sensitive field in message is not redacted
460
- const records: LogRecord[] = [];
461
- const wrappedSink = redactByField((r) => records.push(r), {
462
- fieldPatterns: ["password"],
463
- action: () => "[REDACTED]",
464
- });
465
-
466
- wrappedSink({
467
- level: "info",
468
- category: ["test"],
469
- message: ["Username: ", "johndoe", ""],
470
- rawMessage: "Username: {username}",
471
- timestamp: Date.now(),
472
- properties: { username: "johndoe" },
473
- });
474
-
475
- assertEquals(records[0].message[1], "johndoe");
476
- }
477
-
478
- { // wildcard {*} in message uses redacted properties
479
- const records: LogRecord[] = [];
480
- const wrappedSink = redactByField((r) => records.push(r), {
481
- fieldPatterns: ["password"],
482
- action: () => "[REDACTED]",
483
- });
484
-
485
- const props = { username: "john", password: "secret" };
486
- wrappedSink({
487
- level: "info",
488
- category: ["test"],
489
- message: ["Props: ", props, ""],
490
- rawMessage: "Props: {*}",
491
- timestamp: Date.now(),
492
- properties: props,
493
- });
494
-
495
- // The {*} should be replaced with redacted properties
496
- assertEquals(records[0].message[1], {
497
- username: "john",
498
- password: "[REDACTED]",
499
- });
500
- assertEquals(records[0].properties.password, "[REDACTED]");
501
- }
502
-
503
- { // escaped braces are not treated as placeholders
504
- const records: LogRecord[] = [];
505
- const wrappedSink = redactByField((r) => records.push(r), {
506
- fieldPatterns: ["password"],
507
- action: () => "[REDACTED]",
508
- });
509
-
510
- wrappedSink({
511
- level: "info",
512
- category: ["test"],
513
- message: ["Value: ", "secret", ""],
514
- rawMessage: "Value: {{password}} {password}",
515
- timestamp: Date.now(),
516
- properties: { password: "secret" },
517
- });
518
-
519
- // Only the second {password} is a placeholder
520
- assertEquals(records[0].message[1], "[REDACTED]");
521
- }
522
-
523
- { // tagged template literal - redacts by comparing values
524
- const records: LogRecord[] = [];
525
- const wrappedSink = redactByField((r) => records.push(r), {
526
- fieldPatterns: ["password"],
527
- action: () => "[REDACTED]",
528
- });
529
-
530
- const rawMessage = ["Password: ", ""] as unknown as TemplateStringsArray;
531
- Object.defineProperty(rawMessage, "raw", { value: rawMessage });
532
-
533
- wrappedSink({
534
- level: "info",
535
- category: ["test"],
536
- message: ["Password: ", "secret", ""],
537
- rawMessage,
538
- timestamp: Date.now(),
539
- properties: { password: "secret" },
540
- });
541
-
542
- // Message should be redacted by value comparison
543
- assertEquals(records[0].message[1], "[REDACTED]");
544
- assertEquals(records[0].properties.password, "[REDACTED]");
545
- }
546
-
547
- { // array access path in message
548
- const records: LogRecord[] = [];
549
- const wrappedSink = redactByField((r) => records.push(r), {
550
- fieldPatterns: ["password"],
551
- action: () => "[REDACTED]",
552
- });
553
-
554
- wrappedSink({
555
- level: "info",
556
- category: ["test"],
557
- message: ["First user password: ", "secret1", ""],
558
- rawMessage: "First user password: {users[0].password}",
559
- timestamp: Date.now(),
560
- properties: { users: [{ password: "secret1" }] },
561
- });
562
-
563
- assertEquals(records[0].message[1], "[REDACTED]");
564
- }
565
-
566
- { // regex pattern matches in message placeholder
567
- const records: LogRecord[] = [];
568
- const wrappedSink = redactByField((r) => records.push(r), {
569
- fieldPatterns: [/pass/i],
570
- action: () => "[REDACTED]",
571
- });
572
-
573
- wrappedSink({
574
- level: "info",
575
- category: ["test"],
576
- message: ["Passphrase: ", "mysecret", ""],
577
- rawMessage: "Passphrase: {passphrase}",
578
- timestamp: Date.now(),
579
- properties: { passphrase: "mysecret" },
580
- });
581
-
582
- assertEquals(records[0].message[1], "[REDACTED]");
583
- }
584
- });