@logtape/redaction 1.2.2 → 1.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logtape/redaction",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Redact sensitive data from log messages",
5
5
  "keywords": [
6
6
  "logging",
@@ -45,8 +45,11 @@
45
45
  "./package.json": "./package.json"
46
46
  },
47
47
  "sideEffects": false,
48
+ "files": [
49
+ "dist/"
50
+ ],
48
51
  "peerDependencies": {
49
- "@logtape/logtape": "^1.2.2"
52
+ "@logtape/logtape": "^1.2.3"
50
53
  },
51
54
  "devDependencies": {
52
55
  "@alinea/suite": "^0.6.3",
package/deno.json DELETED
@@ -1,34 +0,0 @@
1
- {
2
- "name": "@logtape/redaction",
3
- "version": "1.2.2",
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/src/field.test.ts DELETED
@@ -1,548 +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
-
222
- test("redactByField()", async () => {
223
- { // wraps sink and redacts properties
224
- const records: LogRecord[] = [];
225
- const originalSink: Sink = (record) => records.push(record);
226
-
227
- const wrappedSink = redactByField(originalSink, {
228
- fieldPatterns: ["password", "token"],
229
- });
230
-
231
- const record: LogRecord = {
232
- level: "info",
233
- category: ["test"],
234
- message: ["Test message"],
235
- rawMessage: "Test message",
236
- timestamp: Date.now(),
237
- properties: {
238
- username: "user123",
239
- password: "secret123",
240
- token: "abc123",
241
- },
242
- };
243
-
244
- wrappedSink(record);
245
-
246
- assertEquals(records.length, 1);
247
- assert("username" in records[0].properties);
248
- assertFalse("password" in records[0].properties);
249
- assertFalse("token" in records[0].properties);
250
- }
251
-
252
- { // uses default field patterns when not specified
253
- const records: LogRecord[] = [];
254
- const originalSink: Sink = (record) => records.push(record);
255
-
256
- const wrappedSink = redactByField(originalSink);
257
-
258
- const record: LogRecord = {
259
- level: "info",
260
- category: ["test"],
261
- message: ["Test message"],
262
- rawMessage: "Test message",
263
- timestamp: Date.now(),
264
- properties: {
265
- username: "user123",
266
- password: "secret123",
267
- email: "user@example.com",
268
- apiKey: "xyz789",
269
- },
270
- };
271
-
272
- wrappedSink(record);
273
-
274
- assertEquals(records.length, 1);
275
- assert("username" in records[0].properties);
276
- assertFalse("password" in records[0].properties);
277
- assertFalse("email" in records[0].properties);
278
- assertFalse("apiKey" in records[0].properties);
279
- }
280
-
281
- { // preserves Disposable behavior
282
- let disposed = false;
283
- const originalSink: Sink & Disposable = Object.assign(
284
- (_record: LogRecord) => {},
285
- {
286
- [Symbol.dispose]: () => {
287
- disposed = true;
288
- },
289
- },
290
- );
291
-
292
- const wrappedSink = redactByField(originalSink) as Sink & Disposable;
293
-
294
- assert(Symbol.dispose in wrappedSink);
295
- wrappedSink[Symbol.dispose]();
296
- assert(disposed);
297
- }
298
-
299
- { // preserves AsyncDisposable behavior
300
- let disposed = false;
301
- const originalSink: Sink & AsyncDisposable = Object.assign(
302
- (_record: LogRecord) => {},
303
- {
304
- [Symbol.asyncDispose]: () => {
305
- disposed = true;
306
- return Promise.resolve();
307
- },
308
- },
309
- );
310
-
311
- const wrappedSink = redactByField(originalSink) as Sink & AsyncDisposable;
312
-
313
- assert(Symbol.asyncDispose in wrappedSink);
314
- await wrappedSink[Symbol.asyncDispose]();
315
- assert(disposed);
316
- }
317
-
318
- { // redacts fields in arrays from issue #94
319
- const records: LogRecord[] = [];
320
- const originalSink: Sink = (record) => records.push(record);
321
-
322
- const wrappedSink = redactByField(originalSink, {
323
- fieldPatterns: ["password"],
324
- });
325
-
326
- const record: LogRecord = {
327
- level: "info",
328
- category: ["test"],
329
- message: ["Loaded config"],
330
- rawMessage: "Loaded config",
331
- timestamp: Date.now(),
332
- properties: {
333
- configs: [{ password: "secret", username: "user" }],
334
- },
335
- };
336
-
337
- wrappedSink(record);
338
-
339
- assertEquals(records.length, 1);
340
- // deno-lint-ignore no-explicit-any
341
- const configs = records[0].properties.configs as any;
342
- assertEquals(configs[0], { username: "user" });
343
- }
344
-
345
- { // redacts values in message array (string template)
346
- const records: LogRecord[] = [];
347
- const wrappedSink = redactByField((r) => records.push(r), {
348
- fieldPatterns: ["password"],
349
- action: () => "[REDACTED]",
350
- });
351
-
352
- wrappedSink({
353
- level: "info",
354
- category: ["test"],
355
- message: ["Password is ", "supersecret", ""],
356
- rawMessage: "Password is {password}",
357
- timestamp: Date.now(),
358
- properties: { password: "supersecret" },
359
- });
360
-
361
- assertEquals(records[0].message, ["Password is ", "[REDACTED]", ""]);
362
- assertEquals(records[0].properties.password, "[REDACTED]");
363
- }
364
-
365
- { // redacts multiple sensitive fields in message
366
- const records: LogRecord[] = [];
367
- const wrappedSink = redactByField((r) => records.push(r), {
368
- fieldPatterns: ["password", "email"],
369
- action: () => "[REDACTED]",
370
- });
371
-
372
- wrappedSink({
373
- level: "info",
374
- category: ["test"],
375
- message: ["Login: ", "user@example.com", " with ", "secret123", ""],
376
- rawMessage: "Login: {email} with {password}",
377
- timestamp: Date.now(),
378
- properties: { email: "user@example.com", password: "secret123" },
379
- });
380
-
381
- assertEquals(records[0].message[1], "[REDACTED]");
382
- assertEquals(records[0].message[3], "[REDACTED]");
383
- }
384
-
385
- { // redacts nested property path in message
386
- const records: LogRecord[] = [];
387
- const wrappedSink = redactByField((r) => records.push(r), {
388
- fieldPatterns: ["password"],
389
- action: () => "[REDACTED]",
390
- });
391
-
392
- wrappedSink({
393
- level: "info",
394
- category: ["test"],
395
- message: ["User password: ", "secret", ""],
396
- rawMessage: "User password: {user.password}",
397
- timestamp: Date.now(),
398
- properties: { user: { password: "secret" } },
399
- });
400
-
401
- assertEquals(records[0].message[1], "[REDACTED]");
402
- }
403
-
404
- { // delete action uses empty string in message
405
- const records: LogRecord[] = [];
406
- const wrappedSink = redactByField((r) => records.push(r), {
407
- fieldPatterns: ["password"],
408
- });
409
-
410
- wrappedSink({
411
- level: "info",
412
- category: ["test"],
413
- message: ["Password: ", "secret", ""],
414
- rawMessage: "Password: {password}",
415
- timestamp: Date.now(),
416
- properties: { password: "secret" },
417
- });
418
-
419
- assertEquals(records[0].message[1], "");
420
- assertFalse("password" in records[0].properties);
421
- }
422
-
423
- { // non-sensitive field in message is not redacted
424
- const records: LogRecord[] = [];
425
- const wrappedSink = redactByField((r) => records.push(r), {
426
- fieldPatterns: ["password"],
427
- action: () => "[REDACTED]",
428
- });
429
-
430
- wrappedSink({
431
- level: "info",
432
- category: ["test"],
433
- message: ["Username: ", "johndoe", ""],
434
- rawMessage: "Username: {username}",
435
- timestamp: Date.now(),
436
- properties: { username: "johndoe" },
437
- });
438
-
439
- assertEquals(records[0].message[1], "johndoe");
440
- }
441
-
442
- { // wildcard {*} in message uses redacted properties
443
- const records: LogRecord[] = [];
444
- const wrappedSink = redactByField((r) => records.push(r), {
445
- fieldPatterns: ["password"],
446
- action: () => "[REDACTED]",
447
- });
448
-
449
- const props = { username: "john", password: "secret" };
450
- wrappedSink({
451
- level: "info",
452
- category: ["test"],
453
- message: ["Props: ", props, ""],
454
- rawMessage: "Props: {*}",
455
- timestamp: Date.now(),
456
- properties: props,
457
- });
458
-
459
- // The {*} should be replaced with redacted properties
460
- assertEquals(records[0].message[1], {
461
- username: "john",
462
- password: "[REDACTED]",
463
- });
464
- assertEquals(records[0].properties.password, "[REDACTED]");
465
- }
466
-
467
- { // escaped braces are not treated as placeholders
468
- const records: LogRecord[] = [];
469
- const wrappedSink = redactByField((r) => records.push(r), {
470
- fieldPatterns: ["password"],
471
- action: () => "[REDACTED]",
472
- });
473
-
474
- wrappedSink({
475
- level: "info",
476
- category: ["test"],
477
- message: ["Value: ", "secret", ""],
478
- rawMessage: "Value: {{password}} {password}",
479
- timestamp: Date.now(),
480
- properties: { password: "secret" },
481
- });
482
-
483
- // Only the second {password} is a placeholder
484
- assertEquals(records[0].message[1], "[REDACTED]");
485
- }
486
-
487
- { // tagged template literal - redacts by comparing values
488
- const records: LogRecord[] = [];
489
- const wrappedSink = redactByField((r) => records.push(r), {
490
- fieldPatterns: ["password"],
491
- action: () => "[REDACTED]",
492
- });
493
-
494
- const rawMessage = ["Password: ", ""] as unknown as TemplateStringsArray;
495
- Object.defineProperty(rawMessage, "raw", { value: rawMessage });
496
-
497
- wrappedSink({
498
- level: "info",
499
- category: ["test"],
500
- message: ["Password: ", "secret", ""],
501
- rawMessage,
502
- timestamp: Date.now(),
503
- properties: { password: "secret" },
504
- });
505
-
506
- // Message should be redacted by value comparison
507
- assertEquals(records[0].message[1], "[REDACTED]");
508
- assertEquals(records[0].properties.password, "[REDACTED]");
509
- }
510
-
511
- { // array access path in message
512
- const records: LogRecord[] = [];
513
- const wrappedSink = redactByField((r) => records.push(r), {
514
- fieldPatterns: ["password"],
515
- action: () => "[REDACTED]",
516
- });
517
-
518
- wrappedSink({
519
- level: "info",
520
- category: ["test"],
521
- message: ["First user password: ", "secret1", ""],
522
- rawMessage: "First user password: {users[0].password}",
523
- timestamp: Date.now(),
524
- properties: { users: [{ password: "secret1" }] },
525
- });
526
-
527
- assertEquals(records[0].message[1], "[REDACTED]");
528
- }
529
-
530
- { // regex pattern matches in message placeholder
531
- const records: LogRecord[] = [];
532
- const wrappedSink = redactByField((r) => records.push(r), {
533
- fieldPatterns: [/pass/i],
534
- action: () => "[REDACTED]",
535
- });
536
-
537
- wrappedSink({
538
- level: "info",
539
- category: ["test"],
540
- message: ["Passphrase: ", "mysecret", ""],
541
- rawMessage: "Passphrase: {passphrase}",
542
- timestamp: Date.now(),
543
- properties: { passphrase: "mysecret" },
544
- });
545
-
546
- assertEquals(records[0].message[1], "[REDACTED]");
547
- }
548
- });