@logtape/redaction 1.1.4 → 1.1.5

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.1.4",
3
+ "version": "1.1.5",
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.1.4"
52
+ "@logtape/logtape": "^1.1.5"
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.1.4",
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,421 +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
-
122
- test("redactByField()", async () => {
123
- { // wraps sink and redacts properties
124
- const records: LogRecord[] = [];
125
- const originalSink: Sink = (record) => records.push(record);
126
-
127
- const wrappedSink = redactByField(originalSink, {
128
- fieldPatterns: ["password", "token"],
129
- });
130
-
131
- const record: LogRecord = {
132
- level: "info",
133
- category: ["test"],
134
- message: ["Test message"],
135
- rawMessage: "Test message",
136
- timestamp: Date.now(),
137
- properties: {
138
- username: "user123",
139
- password: "secret123",
140
- token: "abc123",
141
- },
142
- };
143
-
144
- wrappedSink(record);
145
-
146
- assertEquals(records.length, 1);
147
- assert("username" in records[0].properties);
148
- assertFalse("password" in records[0].properties);
149
- assertFalse("token" in records[0].properties);
150
- }
151
-
152
- { // uses default field patterns when not specified
153
- const records: LogRecord[] = [];
154
- const originalSink: Sink = (record) => records.push(record);
155
-
156
- const wrappedSink = redactByField(originalSink);
157
-
158
- const record: LogRecord = {
159
- level: "info",
160
- category: ["test"],
161
- message: ["Test message"],
162
- rawMessage: "Test message",
163
- timestamp: Date.now(),
164
- properties: {
165
- username: "user123",
166
- password: "secret123",
167
- email: "user@example.com",
168
- apiKey: "xyz789",
169
- },
170
- };
171
-
172
- wrappedSink(record);
173
-
174
- assertEquals(records.length, 1);
175
- assert("username" in records[0].properties);
176
- assertFalse("password" in records[0].properties);
177
- assertFalse("email" in records[0].properties);
178
- assertFalse("apiKey" in records[0].properties);
179
- }
180
-
181
- { // preserves Disposable behavior
182
- let disposed = false;
183
- const originalSink: Sink & Disposable = Object.assign(
184
- (_record: LogRecord) => {},
185
- {
186
- [Symbol.dispose]: () => {
187
- disposed = true;
188
- },
189
- },
190
- );
191
-
192
- const wrappedSink = redactByField(originalSink) as Sink & Disposable;
193
-
194
- assert(Symbol.dispose in wrappedSink);
195
- wrappedSink[Symbol.dispose]();
196
- assert(disposed);
197
- }
198
-
199
- { // preserves AsyncDisposable behavior
200
- let disposed = false;
201
- const originalSink: Sink & AsyncDisposable = Object.assign(
202
- (_record: LogRecord) => {},
203
- {
204
- [Symbol.asyncDispose]: () => {
205
- disposed = true;
206
- return Promise.resolve();
207
- },
208
- },
209
- );
210
-
211
- const wrappedSink = redactByField(originalSink) as Sink & AsyncDisposable;
212
-
213
- assert(Symbol.asyncDispose in wrappedSink);
214
- await wrappedSink[Symbol.asyncDispose]();
215
- assert(disposed);
216
- }
217
-
218
- { // redacts values in message array (string template)
219
- const records: LogRecord[] = [];
220
- const wrappedSink = redactByField((r) => records.push(r), {
221
- fieldPatterns: ["password"],
222
- action: () => "[REDACTED]",
223
- });
224
-
225
- wrappedSink({
226
- level: "info",
227
- category: ["test"],
228
- message: ["Password is ", "supersecret", ""],
229
- rawMessage: "Password is {password}",
230
- timestamp: Date.now(),
231
- properties: { password: "supersecret" },
232
- });
233
-
234
- assertEquals(records[0].message, ["Password is ", "[REDACTED]", ""]);
235
- assertEquals(records[0].properties.password, "[REDACTED]");
236
- }
237
-
238
- { // redacts multiple sensitive fields in message
239
- const records: LogRecord[] = [];
240
- const wrappedSink = redactByField((r) => records.push(r), {
241
- fieldPatterns: ["password", "email"],
242
- action: () => "[REDACTED]",
243
- });
244
-
245
- wrappedSink({
246
- level: "info",
247
- category: ["test"],
248
- message: ["Login: ", "user@example.com", " with ", "secret123", ""],
249
- rawMessage: "Login: {email} with {password}",
250
- timestamp: Date.now(),
251
- properties: { email: "user@example.com", password: "secret123" },
252
- });
253
-
254
- assertEquals(records[0].message[1], "[REDACTED]");
255
- assertEquals(records[0].message[3], "[REDACTED]");
256
- }
257
-
258
- { // redacts nested property path in message
259
- const records: LogRecord[] = [];
260
- const wrappedSink = redactByField((r) => records.push(r), {
261
- fieldPatterns: ["password"],
262
- action: () => "[REDACTED]",
263
- });
264
-
265
- wrappedSink({
266
- level: "info",
267
- category: ["test"],
268
- message: ["User password: ", "secret", ""],
269
- rawMessage: "User password: {user.password}",
270
- timestamp: Date.now(),
271
- properties: { user: { password: "secret" } },
272
- });
273
-
274
- assertEquals(records[0].message[1], "[REDACTED]");
275
- }
276
-
277
- { // delete action uses empty string in message
278
- const records: LogRecord[] = [];
279
- const wrappedSink = redactByField((r) => records.push(r), {
280
- fieldPatterns: ["password"],
281
- });
282
-
283
- wrappedSink({
284
- level: "info",
285
- category: ["test"],
286
- message: ["Password: ", "secret", ""],
287
- rawMessage: "Password: {password}",
288
- timestamp: Date.now(),
289
- properties: { password: "secret" },
290
- });
291
-
292
- assertEquals(records[0].message[1], "");
293
- assertFalse("password" in records[0].properties);
294
- }
295
-
296
- { // non-sensitive field in message is not redacted
297
- const records: LogRecord[] = [];
298
- const wrappedSink = redactByField((r) => records.push(r), {
299
- fieldPatterns: ["password"],
300
- action: () => "[REDACTED]",
301
- });
302
-
303
- wrappedSink({
304
- level: "info",
305
- category: ["test"],
306
- message: ["Username: ", "johndoe", ""],
307
- rawMessage: "Username: {username}",
308
- timestamp: Date.now(),
309
- properties: { username: "johndoe" },
310
- });
311
-
312
- assertEquals(records[0].message[1], "johndoe");
313
- }
314
-
315
- { // wildcard {*} in message uses redacted properties
316
- const records: LogRecord[] = [];
317
- const wrappedSink = redactByField((r) => records.push(r), {
318
- fieldPatterns: ["password"],
319
- action: () => "[REDACTED]",
320
- });
321
-
322
- const props = { username: "john", password: "secret" };
323
- wrappedSink({
324
- level: "info",
325
- category: ["test"],
326
- message: ["Props: ", props, ""],
327
- rawMessage: "Props: {*}",
328
- timestamp: Date.now(),
329
- properties: props,
330
- });
331
-
332
- // The {*} should be replaced with redacted properties
333
- assertEquals(records[0].message[1], {
334
- username: "john",
335
- password: "[REDACTED]",
336
- });
337
- assertEquals(records[0].properties.password, "[REDACTED]");
338
- }
339
-
340
- { // escaped braces are not treated as placeholders
341
- const records: LogRecord[] = [];
342
- const wrappedSink = redactByField((r) => records.push(r), {
343
- fieldPatterns: ["password"],
344
- action: () => "[REDACTED]",
345
- });
346
-
347
- wrappedSink({
348
- level: "info",
349
- category: ["test"],
350
- message: ["Value: ", "secret", ""],
351
- rawMessage: "Value: {{password}} {password}",
352
- timestamp: Date.now(),
353
- properties: { password: "secret" },
354
- });
355
-
356
- // Only the second {password} is a placeholder
357
- assertEquals(records[0].message[1], "[REDACTED]");
358
- }
359
-
360
- { // tagged template literal - redacts by comparing values
361
- const records: LogRecord[] = [];
362
- const wrappedSink = redactByField((r) => records.push(r), {
363
- fieldPatterns: ["password"],
364
- action: () => "[REDACTED]",
365
- });
366
-
367
- const rawMessage = ["Password: ", ""] as unknown as TemplateStringsArray;
368
- Object.defineProperty(rawMessage, "raw", { value: rawMessage });
369
-
370
- wrappedSink({
371
- level: "info",
372
- category: ["test"],
373
- message: ["Password: ", "secret", ""],
374
- rawMessage,
375
- timestamp: Date.now(),
376
- properties: { password: "secret" },
377
- });
378
-
379
- // Message should be redacted by value comparison
380
- assertEquals(records[0].message[1], "[REDACTED]");
381
- assertEquals(records[0].properties.password, "[REDACTED]");
382
- }
383
-
384
- { // array access path in message
385
- const records: LogRecord[] = [];
386
- const wrappedSink = redactByField((r) => records.push(r), {
387
- fieldPatterns: ["password"],
388
- action: () => "[REDACTED]",
389
- });
390
-
391
- wrappedSink({
392
- level: "info",
393
- category: ["test"],
394
- message: ["First user password: ", "secret1", ""],
395
- rawMessage: "First user password: {users[0].password}",
396
- timestamp: Date.now(),
397
- properties: { users: [{ password: "secret1" }] },
398
- });
399
-
400
- assertEquals(records[0].message[1], "[REDACTED]");
401
- }
402
-
403
- { // regex pattern matches in message placeholder
404
- const records: LogRecord[] = [];
405
- const wrappedSink = redactByField((r) => records.push(r), {
406
- fieldPatterns: [/pass/i],
407
- action: () => "[REDACTED]",
408
- });
409
-
410
- wrappedSink({
411
- level: "info",
412
- category: ["test"],
413
- message: ["Passphrase: ", "mysecret", ""],
414
- rawMessage: "Passphrase: {passphrase}",
415
- timestamp: Date.now(),
416
- properties: { passphrase: "mysecret" },
417
- });
418
-
419
- assertEquals(records[0].message[1], "[REDACTED]");
420
- }
421
- });