@geekmidas/logger 0.3.0 → 1.0.0

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 (41) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/console.cjs.map +1 -1
  3. package/dist/console.d.cts +2 -1
  4. package/dist/console.d.cts.map +1 -0
  5. package/dist/console.d.mts +2 -1
  6. package/dist/console.d.mts.map +1 -0
  7. package/dist/console.mjs.map +1 -1
  8. package/dist/index.d.cts +1 -1
  9. package/dist/index.d.mts +1 -1
  10. package/dist/pino.cjs.map +1 -1
  11. package/dist/pino.d.cts +3 -2
  12. package/dist/pino.d.cts.map +1 -0
  13. package/dist/pino.d.mts +3 -2
  14. package/dist/pino.d.mts.map +1 -0
  15. package/dist/pino.mjs.map +1 -1
  16. package/dist/{redact-paths-Br-tI2GZ.d.cts → redact-paths-CsK0_uz-.d.cts} +2 -1
  17. package/dist/redact-paths-CsK0_uz-.d.cts.map +1 -0
  18. package/dist/{redact-paths-CIsuxHH7.d.mts → redact-paths-D07eTMlH.d.mts} +2 -1
  19. package/dist/redact-paths-D07eTMlH.d.mts.map +1 -0
  20. package/dist/redact-paths-D0m0DIuQ.cjs.map +1 -1
  21. package/dist/redact-paths-DQoIXhkS.mjs.map +1 -1
  22. package/dist/redact-paths.d.cts +1 -1
  23. package/dist/redact-paths.d.mts +1 -1
  24. package/dist/{types-Bga8WDuP.d.mts → types-BDdpcrpy.d.mts} +2 -1
  25. package/dist/types-BDdpcrpy.d.mts.map +1 -0
  26. package/dist/{types-JxCFymH0.d.cts → types-Dk_k2-f_.d.cts} +2 -1
  27. package/dist/types-Dk_k2-f_.d.cts.map +1 -0
  28. package/dist/types-ag_0Cvbg.cjs.map +1 -1
  29. package/dist/types-yQ6XOihF.mjs.map +1 -1
  30. package/dist/types.d.cts +1 -1
  31. package/dist/types.d.mts +1 -1
  32. package/package.json +1 -1
  33. package/src/__tests__/console.spec.ts +648 -648
  34. package/src/__tests__/pino-redaction.integration.spec.ts +270 -270
  35. package/src/__tests__/pino.spec.ts +305 -305
  36. package/src/console.ts +100 -100
  37. package/src/index.ts +5 -5
  38. package/src/pino.ts +50 -50
  39. package/src/redact-paths.ts +52 -52
  40. package/src/types.ts +87 -87
  41. package/tsconfig.json +9 -0
@@ -7,62 +7,62 @@ import type { RedactOptions } from '../types';
7
7
  * Type for the resolved pino redact config.
8
8
  */
9
9
  type PinoRedactConfig =
10
- | string[]
11
- | {
12
- paths: string[];
13
- censor?: string | ((value: unknown, path: string[]) => unknown);
14
- remove?: boolean;
15
- };
10
+ | string[]
11
+ | {
12
+ paths: string[];
13
+ censor?: string | ((value: unknown, path: string[]) => unknown);
14
+ remove?: boolean;
15
+ };
16
16
 
17
17
  /**
18
18
  * Resolves redact options to pino-compatible config, applying merge logic.
19
19
  */
20
20
  function resolveRedactConfig(
21
- redact: RedactOptions | undefined,
21
+ redact: RedactOptions | undefined,
22
22
  ): PinoRedactConfig | undefined {
23
- if (redact === undefined) {
24
- return undefined;
25
- }
23
+ if (redact === undefined) {
24
+ return undefined;
25
+ }
26
26
 
27
- // Array syntax - merge with defaults
28
- if (Array.isArray(redact)) {
29
- return [...DEFAULT_REDACT_PATHS, ...redact];
30
- }
27
+ // Array syntax - merge with defaults
28
+ if (Array.isArray(redact)) {
29
+ return [...DEFAULT_REDACT_PATHS, ...redact];
30
+ }
31
31
 
32
- // Object syntax - check resolution mode
33
- const { resolution = 'merge', paths, censor, remove } = redact;
32
+ // Object syntax - check resolution mode
33
+ const { resolution = 'merge', paths, censor, remove } = redact;
34
34
 
35
- const resolvedPaths =
36
- resolution === 'override' ? paths : [...DEFAULT_REDACT_PATHS, ...paths];
35
+ const resolvedPaths =
36
+ resolution === 'override' ? paths : [...DEFAULT_REDACT_PATHS, ...paths];
37
37
 
38
- const config: PinoRedactConfig = { paths: resolvedPaths };
39
- if (censor !== undefined) config.censor = censor;
40
- if (remove !== undefined) config.remove = remove;
38
+ const config: PinoRedactConfig = { paths: resolvedPaths };
39
+ if (censor !== undefined) config.censor = censor;
40
+ if (remove !== undefined) config.remove = remove;
41
41
 
42
- return config;
42
+ return config;
43
43
  }
44
44
 
45
45
  /**
46
46
  * Creates a writable stream that captures pino output as parsed JSON objects.
47
47
  */
48
48
  function createCaptureStream() {
49
- const logs: Record<string, unknown>[] = [];
50
-
51
- const stream = new Writable({
52
- write(chunk, _encoding, callback) {
53
- try {
54
- const line = chunk.toString().trim();
55
- if (line) {
56
- logs.push(JSON.parse(line));
57
- }
58
- } catch {
59
- // Ignore non-JSON lines (e.g., pretty output)
60
- }
61
- callback();
62
- },
63
- });
64
-
65
- return { stream, logs };
49
+ const logs: Record<string, unknown>[] = [];
50
+
51
+ const stream = new Writable({
52
+ write(chunk, _encoding, callback) {
53
+ try {
54
+ const line = chunk.toString().trim();
55
+ if (line) {
56
+ logs.push(JSON.parse(line));
57
+ }
58
+ } catch {
59
+ // Ignore non-JSON lines (e.g., pretty output)
60
+ }
61
+ callback();
62
+ },
63
+ });
64
+
65
+ return { stream, logs };
66
66
  }
67
67
 
68
68
  /**
@@ -70,238 +70,238 @@ function createCaptureStream() {
70
70
  * Note: We can't use pretty mode here as it's not JSON parseable.
71
71
  */
72
72
  function createTestLogger(redact: RedactOptions | undefined) {
73
- const { stream, logs } = createCaptureStream();
74
-
75
- // Import pino directly to create with custom destination
76
- const { pino } = require('pino');
77
-
78
- // Apply our merge logic before passing to pino
79
- const resolvedRedact = resolveRedactConfig(redact);
80
-
81
- const logger = pino(
82
- {
83
- redact: resolvedRedact,
84
- // Disable pretty for JSON parsing
85
- formatters: {
86
- level: (label: string) => ({ level: label }),
87
- },
88
- },
89
- stream,
90
- );
91
-
92
- return { logger, logs };
73
+ const { stream, logs } = createCaptureStream();
74
+
75
+ // Import pino directly to create with custom destination
76
+ const { pino } = require('pino');
77
+
78
+ // Apply our merge logic before passing to pino
79
+ const resolvedRedact = resolveRedactConfig(redact);
80
+
81
+ const logger = pino(
82
+ {
83
+ redact: resolvedRedact,
84
+ // Disable pretty for JSON parsing
85
+ formatters: {
86
+ level: (label: string) => ({ level: label }),
87
+ },
88
+ },
89
+ stream,
90
+ );
91
+
92
+ return { logger, logs };
93
93
  }
94
94
 
95
95
  describe('Pino Redaction Integration', () => {
96
- describe('with redact: true (default paths)', () => {
97
- it('should redact password field', () => {
98
- const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
99
-
100
- logger.info({ password: 'secret123', username: 'john' }, 'Login attempt');
101
- logger.flush?.();
102
-
103
- expect(logs).toHaveLength(1);
104
- expect(logs[0].password).toBe('[Redacted]');
105
- expect(logs[0].username).toBe('john');
106
- });
107
-
108
- it('should redact token field', () => {
109
- const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
110
-
111
- logger.info({ token: 'jwt.token.here', userId: 123 }, 'Auth check');
112
- logger.flush?.();
113
-
114
- expect(logs).toHaveLength(1);
115
- expect(logs[0].token).toBe('[Redacted]');
116
- expect(logs[0].userId).toBe(123);
117
- });
118
-
119
- it('should redact apiKey field', () => {
120
- const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
121
-
122
- logger.info({ apiKey: 'sk-1234567890', service: 'openai' }, 'API call');
123
- logger.flush?.();
124
-
125
- expect(logs).toHaveLength(1);
126
- expect(logs[0].apiKey).toBe('[Redacted]');
127
- expect(logs[0].service).toBe('openai');
128
- });
129
-
130
- it('should redact nested sensitive fields with wildcards', () => {
131
- const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
132
-
133
- logger.info(
134
- {
135
- user: { password: 'secret', name: 'John' },
136
- config: { secret: 'shh', debug: true },
137
- },
138
- 'Nested data',
139
- );
140
- logger.flush?.();
141
-
142
- expect(logs).toHaveLength(1);
143
- expect(logs[0].user).toEqual({ password: '[Redacted]', name: 'John' });
144
- expect(logs[0].config).toEqual({ secret: '[Redacted]', debug: true });
145
- });
146
-
147
- it('should redact authorization headers', () => {
148
- const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
149
-
150
- logger.info(
151
- {
152
- headers: {
153
- authorization: 'Bearer xyz123',
154
- 'content-type': 'application/json',
155
- },
156
- },
157
- 'Request headers',
158
- );
159
- logger.flush?.();
160
-
161
- expect(logs).toHaveLength(1);
162
- expect(logs[0].headers).toEqual({
163
- authorization: '[Redacted]',
164
- 'content-type': 'application/json',
165
- });
166
- });
167
-
168
- it('should redact credit card fields', () => {
169
- const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
170
-
171
- logger.info(
172
- {
173
- creditCard: '4111-1111-1111-1111',
174
- cvv: '123',
175
- cardHolder: 'John Doe',
176
- },
177
- 'Payment info',
178
- );
179
- logger.flush?.();
180
-
181
- expect(logs).toHaveLength(1);
182
- expect(logs[0].creditCard).toBe('[Redacted]');
183
- expect(logs[0].cvv).toBe('[Redacted]');
184
- expect(logs[0].cardHolder).toBe('John Doe');
185
- });
186
- });
187
-
188
- describe('with custom paths (merge mode - default)', () => {
189
- it('should merge custom paths with defaults', () => {
190
- const { logger, logs } = createTestLogger(['customSecret', 'data.key']);
191
-
192
- logger.info(
193
- {
194
- customSecret: 'hidden',
195
- password: 'also-hidden-from-defaults',
196
- data: { key: 'hidden', value: 'visible' },
197
- },
198
- 'Merged redaction',
199
- );
200
- logger.flush?.();
201
-
202
- expect(logs).toHaveLength(1);
203
- expect(logs[0].customSecret).toBe('[Redacted]');
204
- // password is redacted because it's in DEFAULT_REDACT_PATHS
205
- expect(logs[0].password).toBe('[Redacted]');
206
- expect(logs[0].data).toEqual({ key: '[Redacted]', value: 'visible' });
207
- });
208
-
209
- it('should support wildcard paths merged with defaults', () => {
210
- const { logger, logs } = createTestLogger(['items[*].customField']);
211
-
212
- logger.info(
213
- {
214
- password: 'hidden-by-default',
215
- items: [
216
- { id: 1, customField: 'a' },
217
- { id: 2, customField: 'b' },
218
- ],
219
- },
220
- 'Array redaction',
221
- );
222
- logger.flush?.();
223
-
224
- expect(logs).toHaveLength(1);
225
- expect(logs[0].password).toBe('[Redacted]');
226
- expect(logs[0].items).toEqual([
227
- { id: 1, customField: '[Redacted]' },
228
- { id: 2, customField: '[Redacted]' },
229
- ]);
230
- });
231
- });
232
-
233
- describe('with resolution: override', () => {
234
- it('should redact only specified paths when override', () => {
235
- const { logger, logs } = createTestLogger({
236
- paths: ['customSecret', 'data.key'],
237
- resolution: 'override',
238
- });
239
-
240
- logger.info(
241
- {
242
- customSecret: 'hidden',
243
- password: 'visible-because-override',
244
- data: { key: 'hidden', value: 'visible' },
245
- },
246
- 'Override redaction',
247
- );
248
- logger.flush?.();
249
-
250
- expect(logs).toHaveLength(1);
251
- expect(logs[0].customSecret).toBe('[Redacted]');
252
- // password is NOT redacted because we're overriding defaults
253
- expect(logs[0].password).toBe('visible-because-override');
254
- expect(logs[0].data).toEqual({ key: '[Redacted]', value: 'visible' });
255
- });
256
- });
257
-
258
- describe('with object config', () => {
259
- it('should use custom censor string', () => {
260
- const { logger, logs } = createTestLogger({
261
- paths: ['password'],
262
- censor: '***HIDDEN***',
263
- });
264
-
265
- logger.info({ password: 'secret', user: 'john' }, 'Custom censor');
266
- logger.flush?.();
267
-
268
- expect(logs).toHaveLength(1);
269
- expect(logs[0].password).toBe('***HIDDEN***');
270
- expect(logs[0].user).toBe('john');
271
- });
272
-
273
- it('should remove field when remove: true', () => {
274
- const { logger, logs } = createTestLogger({
275
- paths: ['password', 'secret'],
276
- remove: true,
277
- });
278
-
279
- logger.info(
280
- { password: 'secret', secret: 'shh', username: 'john' },
281
- 'Remove mode',
282
- );
283
- logger.flush?.();
284
-
285
- expect(logs).toHaveLength(1);
286
- expect(logs[0]).not.toHaveProperty('password');
287
- expect(logs[0]).not.toHaveProperty('secret');
288
- expect(logs[0].username).toBe('john');
289
- });
290
- });
291
-
292
- describe('without redaction', () => {
293
- it('should not redact when redact is undefined', () => {
294
- const { logger, logs } = createTestLogger(undefined);
295
-
296
- logger.info(
297
- { password: 'visible', token: 'also-visible' },
298
- 'No redaction',
299
- );
300
- logger.flush?.();
301
-
302
- expect(logs).toHaveLength(1);
303
- expect(logs[0].password).toBe('visible');
304
- expect(logs[0].token).toBe('also-visible');
305
- });
306
- });
96
+ describe('with redact: true (default paths)', () => {
97
+ it('should redact password field', () => {
98
+ const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
99
+
100
+ logger.info({ password: 'secret123', username: 'john' }, 'Login attempt');
101
+ logger.flush?.();
102
+
103
+ expect(logs).toHaveLength(1);
104
+ expect(logs[0].password).toBe('[Redacted]');
105
+ expect(logs[0].username).toBe('john');
106
+ });
107
+
108
+ it('should redact token field', () => {
109
+ const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
110
+
111
+ logger.info({ token: 'jwt.token.here', userId: 123 }, 'Auth check');
112
+ logger.flush?.();
113
+
114
+ expect(logs).toHaveLength(1);
115
+ expect(logs[0].token).toBe('[Redacted]');
116
+ expect(logs[0].userId).toBe(123);
117
+ });
118
+
119
+ it('should redact apiKey field', () => {
120
+ const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
121
+
122
+ logger.info({ apiKey: 'sk-1234567890', service: 'openai' }, 'API call');
123
+ logger.flush?.();
124
+
125
+ expect(logs).toHaveLength(1);
126
+ expect(logs[0].apiKey).toBe('[Redacted]');
127
+ expect(logs[0].service).toBe('openai');
128
+ });
129
+
130
+ it('should redact nested sensitive fields with wildcards', () => {
131
+ const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
132
+
133
+ logger.info(
134
+ {
135
+ user: { password: 'secret', name: 'John' },
136
+ config: { secret: 'shh', debug: true },
137
+ },
138
+ 'Nested data',
139
+ );
140
+ logger.flush?.();
141
+
142
+ expect(logs).toHaveLength(1);
143
+ expect(logs[0].user).toEqual({ password: '[Redacted]', name: 'John' });
144
+ expect(logs[0].config).toEqual({ secret: '[Redacted]', debug: true });
145
+ });
146
+
147
+ it('should redact authorization headers', () => {
148
+ const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
149
+
150
+ logger.info(
151
+ {
152
+ headers: {
153
+ authorization: 'Bearer xyz123',
154
+ 'content-type': 'application/json',
155
+ },
156
+ },
157
+ 'Request headers',
158
+ );
159
+ logger.flush?.();
160
+
161
+ expect(logs).toHaveLength(1);
162
+ expect(logs[0].headers).toEqual({
163
+ authorization: '[Redacted]',
164
+ 'content-type': 'application/json',
165
+ });
166
+ });
167
+
168
+ it('should redact credit card fields', () => {
169
+ const { logger, logs } = createTestLogger(DEFAULT_REDACT_PATHS);
170
+
171
+ logger.info(
172
+ {
173
+ creditCard: '4111-1111-1111-1111',
174
+ cvv: '123',
175
+ cardHolder: 'John Doe',
176
+ },
177
+ 'Payment info',
178
+ );
179
+ logger.flush?.();
180
+
181
+ expect(logs).toHaveLength(1);
182
+ expect(logs[0].creditCard).toBe('[Redacted]');
183
+ expect(logs[0].cvv).toBe('[Redacted]');
184
+ expect(logs[0].cardHolder).toBe('John Doe');
185
+ });
186
+ });
187
+
188
+ describe('with custom paths (merge mode - default)', () => {
189
+ it('should merge custom paths with defaults', () => {
190
+ const { logger, logs } = createTestLogger(['customSecret', 'data.key']);
191
+
192
+ logger.info(
193
+ {
194
+ customSecret: 'hidden',
195
+ password: 'also-hidden-from-defaults',
196
+ data: { key: 'hidden', value: 'visible' },
197
+ },
198
+ 'Merged redaction',
199
+ );
200
+ logger.flush?.();
201
+
202
+ expect(logs).toHaveLength(1);
203
+ expect(logs[0].customSecret).toBe('[Redacted]');
204
+ // password is redacted because it's in DEFAULT_REDACT_PATHS
205
+ expect(logs[0].password).toBe('[Redacted]');
206
+ expect(logs[0].data).toEqual({ key: '[Redacted]', value: 'visible' });
207
+ });
208
+
209
+ it('should support wildcard paths merged with defaults', () => {
210
+ const { logger, logs } = createTestLogger(['items[*].customField']);
211
+
212
+ logger.info(
213
+ {
214
+ password: 'hidden-by-default',
215
+ items: [
216
+ { id: 1, customField: 'a' },
217
+ { id: 2, customField: 'b' },
218
+ ],
219
+ },
220
+ 'Array redaction',
221
+ );
222
+ logger.flush?.();
223
+
224
+ expect(logs).toHaveLength(1);
225
+ expect(logs[0].password).toBe('[Redacted]');
226
+ expect(logs[0].items).toEqual([
227
+ { id: 1, customField: '[Redacted]' },
228
+ { id: 2, customField: '[Redacted]' },
229
+ ]);
230
+ });
231
+ });
232
+
233
+ describe('with resolution: override', () => {
234
+ it('should redact only specified paths when override', () => {
235
+ const { logger, logs } = createTestLogger({
236
+ paths: ['customSecret', 'data.key'],
237
+ resolution: 'override',
238
+ });
239
+
240
+ logger.info(
241
+ {
242
+ customSecret: 'hidden',
243
+ password: 'visible-because-override',
244
+ data: { key: 'hidden', value: 'visible' },
245
+ },
246
+ 'Override redaction',
247
+ );
248
+ logger.flush?.();
249
+
250
+ expect(logs).toHaveLength(1);
251
+ expect(logs[0].customSecret).toBe('[Redacted]');
252
+ // password is NOT redacted because we're overriding defaults
253
+ expect(logs[0].password).toBe('visible-because-override');
254
+ expect(logs[0].data).toEqual({ key: '[Redacted]', value: 'visible' });
255
+ });
256
+ });
257
+
258
+ describe('with object config', () => {
259
+ it('should use custom censor string', () => {
260
+ const { logger, logs } = createTestLogger({
261
+ paths: ['password'],
262
+ censor: '***HIDDEN***',
263
+ });
264
+
265
+ logger.info({ password: 'secret', user: 'john' }, 'Custom censor');
266
+ logger.flush?.();
267
+
268
+ expect(logs).toHaveLength(1);
269
+ expect(logs[0].password).toBe('***HIDDEN***');
270
+ expect(logs[0].user).toBe('john');
271
+ });
272
+
273
+ it('should remove field when remove: true', () => {
274
+ const { logger, logs } = createTestLogger({
275
+ paths: ['password', 'secret'],
276
+ remove: true,
277
+ });
278
+
279
+ logger.info(
280
+ { password: 'secret', secret: 'shh', username: 'john' },
281
+ 'Remove mode',
282
+ );
283
+ logger.flush?.();
284
+
285
+ expect(logs).toHaveLength(1);
286
+ expect(logs[0]).not.toHaveProperty('password');
287
+ expect(logs[0]).not.toHaveProperty('secret');
288
+ expect(logs[0].username).toBe('john');
289
+ });
290
+ });
291
+
292
+ describe('without redaction', () => {
293
+ it('should not redact when redact is undefined', () => {
294
+ const { logger, logs } = createTestLogger(undefined);
295
+
296
+ logger.info(
297
+ { password: 'visible', token: 'also-visible' },
298
+ 'No redaction',
299
+ );
300
+ logger.flush?.();
301
+
302
+ expect(logs).toHaveLength(1);
303
+ expect(logs[0].password).toBe('visible');
304
+ expect(logs[0].token).toBe('also-visible');
305
+ });
306
+ });
307
307
  });