@crimsonsunset/jsg-logger 1.7.14 → 1.7.15

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/README.md CHANGED
@@ -26,6 +26,7 @@ A sophisticated, fully generic logging system that automatically detects its env
26
26
  - 📁 **File-Level Overrides** - Per-file and pattern-based control
27
27
  - ⏰ **Timestamp Modes** - Absolute, readable, relative, or disabled
28
28
  - 🎛️ **Display Toggles** - Control every aspect of log output
29
+ - 🔒 **Automatic Redaction** - Protect sensitive data in logs (passwords, API keys, tokens)
29
30
  - 🎯 **Smart Level Resolution** - Hierarchical level determination
30
31
 
31
32
  ## 🚀 Quick Start
@@ -290,6 +291,121 @@ logger.controls.setDisplayOption('jsonPayload', false);
290
291
  logger.controls.toggleDisplayOption('level');
291
292
  ```
292
293
 
294
+ ## 🔒 **Redaction (Sensitive Data Protection)**
295
+
296
+ Automatically redact sensitive keys in logged objects to prevent accidental exposure of passwords, API keys, tokens, and other sensitive data.
297
+
298
+ ### **Basic Configuration**
299
+
300
+ ```javascript
301
+ const logger = JSGLogger.getInstanceSync({
302
+ redact: {
303
+ paths: ['password', 'token', '*key', '*secret'],
304
+ censor: '[REDACTED]'
305
+ }
306
+ });
307
+
308
+ logger.api.info('User login', {
309
+ username: 'john',
310
+ password: 'secret123', // → [REDACTED]
311
+ apiKey: 'abc123xyz', // → [REDACTED] (matches *key)
312
+ googleApiKey: 'key123', // → [REDACTED] (matches *key)
313
+ email: 'john@example.com' // → visible
314
+ });
315
+ ```
316
+
317
+ ### **Pattern Matching**
318
+
319
+ Redaction supports two pattern types:
320
+
321
+ - **Exact match**: `'password'`, `'token'` - matches keys exactly
322
+ - **Wildcard suffix**: `'*key'`, `'*secret'` - matches any key ending with the suffix (case-insensitive)
323
+
324
+ ```javascript
325
+ // Wildcard patterns match:
326
+ '*key' → apiKey, googleApiKey, secretKey, publicKey, API_KEY, api_key
327
+ '*secret' → secret, secretKey, mySecret, SECRET
328
+ '*apiKey' → apiKey, googleApiKey, customApiKey
329
+ '*api_key' → api_key, GOOGLE_API_KEY
330
+ ```
331
+
332
+ ### **Nested Objects & Arrays**
333
+
334
+ Redaction works recursively through nested objects and arrays:
335
+
336
+ ```javascript
337
+ logger.info('Config loaded', {
338
+ user: {
339
+ name: 'John',
340
+ password: 'secret', // → [REDACTED]
341
+ apiKey: 'key123' // → [REDACTED]
342
+ },
343
+ services: [
344
+ { name: 'API', token: 'abc' }, // → token: [REDACTED]
345
+ { name: 'DB', token: 'xyz' } // → token: [REDACTED]
346
+ ]
347
+ });
348
+ ```
349
+
350
+ ### **Custom Censor Text**
351
+
352
+ ```javascript
353
+ const logger = JSGLogger.getInstanceSync({
354
+ redact: {
355
+ paths: ['password'],
356
+ censor: '***HIDDEN***' // Custom replacement text
357
+ }
358
+ });
359
+ ```
360
+
361
+ ### **File-Specific Redaction**
362
+
363
+ Override redaction per file or pattern:
364
+
365
+ ```json
366
+ {
367
+ "redact": {
368
+ "paths": ["password", "*key"],
369
+ "censor": "[REDACTED]"
370
+ },
371
+ "fileOverrides": {
372
+ "src/auth/*.js": {
373
+ "redact": {
374
+ "paths": ["password", "token", "*key", "*secret"],
375
+ "censor": "***"
376
+ }
377
+ }
378
+ }
379
+ }
380
+ ```
381
+
382
+ ### **Default Configuration**
383
+
384
+ Default redaction patterns (if not configured):
385
+ - `password`
386
+ - `token`
387
+ - `*key` (matches any key ending in "key")
388
+ - `*secret` (matches any key ending in "secret")
389
+ - `*apiKey`
390
+ - `*api_key`
391
+
392
+ Default censor: `[REDACTED]`
393
+
394
+ ### **Disable Redaction**
395
+
396
+ Set empty paths array to disable:
397
+
398
+ ```javascript
399
+ const logger = JSGLogger.getInstanceSync({
400
+ redact: {
401
+ paths: [], // No redaction
402
+ censor: '[REDACTED]'
403
+ }
404
+ });
405
+ ```
406
+
407
+ **Note**: Redaction applies to all formatters (browser, CLI, and server) automatically.
408
+
293
409
  ## 🏗️ Architecture
294
410
 
295
411
  ```
@@ -533,6 +533,32 @@ export class ConfigManager {
533
533
  return baseDisplay;
534
534
  }
535
535
 
536
+ /**
537
+ * Get redact configuration with file override support
538
+ * @param {string} filePath - Optional file path for override checking
539
+ * @returns {Object} Redact configuration with paths array and censor string
540
+ */
541
+ getRedactConfig(filePath = null) {
542
+ const baseRedact = this.config.redact || {
543
+ paths: [],
544
+ censor: '[REDACTED]'
545
+ };
546
+
547
+ // Check for file-specific redact overrides
548
+ const checkFile = filePath || this.currentFile;
549
+ if (checkFile) {
550
+ const fileOverride = this.getFileOverride(checkFile);
551
+ if (fileOverride && fileOverride.redact) {
552
+ return {
553
+ ...baseRedact,
554
+ ...fileOverride.redact
555
+ };
556
+ }
557
+ }
558
+
559
+ return baseRedact;
560
+ }
561
+
536
562
  /**
537
563
  * Get project name
538
564
  * @returns {string} Project name
@@ -18,6 +18,10 @@
18
18
  "jsonPayload": true,
19
19
  "stackTrace": true
20
20
  },
21
+ "redact": {
22
+ "paths": ["password", "token", "*key", "*secret", "*apiKey", "*api_key"],
23
+ "censor": "[REDACTED]"
24
+ },
21
25
  "levels": {
22
26
  "10": { "name": "TRACE", "emoji": "🔍", "color": "#6C7B7F" },
23
27
  "20": { "name": "DEBUG", "emoji": "🐛", "color": "#74B9FF" },
@@ -16,6 +16,10 @@
16
16
  "jsonPayload": true,
17
17
  "stackTrace": true
18
18
  },
19
+ "redact": {
20
+ "paths": ["password", "token", "*key", "*secret", "*apiKey", "*api_key"],
21
+ "censor": "[REDACTED]"
22
+ },
19
23
  "components": {
20
24
  "core": {
21
25
  "emoji": "🎯",
@@ -116,6 +120,10 @@
116
120
  "display": {
117
121
  "jsonPayload": true,
118
122
  "stackTrace": true
123
+ },
124
+ "redact": {
125
+ "paths": ["password", "token", "*key", "*secret"],
126
+ "censor": "***"
119
127
  }
120
128
  },
121
129
  "src/database/*.js": {
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import {configManager} from '../config/config-manager.js';
8
+ import {redactValue} from '../utils/redaction.js';
8
9
 
9
10
  /**
10
11
  * Create browser console formatter for a specific component
@@ -28,6 +29,7 @@ export const createBrowserFormatter = (componentName, logStore = null) => {
28
29
  const component = configManager.getComponentConfig(componentName, filePath);
29
30
  const level = configManager.getLevelConfig(logData.level);
30
31
  const displayConfig = configManager.getDisplayConfig(filePath);
32
+ const redactConfig = configManager.getRedactConfig(filePath);
31
33
 
32
34
  // Check if this log should be displayed based on effective level
33
35
  const effectiveLevel = configManager.getEffectiveLevel(componentName, filePath);
@@ -84,7 +86,8 @@ export const createBrowserFormatter = (componentName, logStore = null) => {
84
86
  if (displayConfig.jsonPayload) {
85
87
  const contextData = extractContextData(logData);
86
88
  if (Object.keys(contextData).length > 0) {
87
- displayContextData(contextData);
89
+ const redactedData = redactValue(contextData, redactConfig);
90
+ displayContextData(redactedData);
88
91
  }
89
92
  }
90
93
 
@@ -158,6 +161,7 @@ function extractContextData(logData) {
158
161
  return contextData;
159
162
  }
160
163
 
164
+
161
165
  /**
162
166
  * Display context data with tree-like structure
163
167
  * @param {Object} contextData - Context data to display
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { LEVEL_SCHEME } from '../config/component-schemes.js';
7
7
  import { configManager } from '../config/config-manager.js';
8
+ import { redactValue } from '../utils/redaction.js';
8
9
 
9
10
  /**
10
11
  * Format a value for display in context tree
@@ -27,6 +28,7 @@ const formatValue = (value) => {
27
28
  return String(value);
28
29
  };
29
30
 
31
+
30
32
  /**
31
33
  * Create CLI formatter with context data support
32
34
  * @returns {Object} Stream-like object for Pino
@@ -62,10 +64,14 @@ export const createCLIFormatter = () => {
62
64
  const contextKeys = Object.keys(log).filter(key => !internalFields.includes(key));
63
65
 
64
66
  if (contextKeys.length > 0) {
67
+ // Get redact config and apply redaction
68
+ const redactConfig = configManager.getRedactConfig();
69
+ const redactedLog = redactValue(log, redactConfig);
70
+
65
71
  contextKeys.forEach((key, index) => {
66
72
  const isLast = index === contextKeys.length - 1;
67
73
  const prefix = isLast ? ' └─' : ' ├─';
68
- const value = formatValue(log[key]);
74
+ const value = formatValue(redactedLog[key]);
69
75
  console.log(`${prefix} ${key}: ${value}`);
70
76
  });
71
77
  }
@@ -3,6 +3,8 @@
3
3
  * Structured JSON output for production logging and log aggregation
4
4
  */
5
5
 
6
+ import {configManager} from '../config/config-manager.js';
7
+
6
8
  /**
7
9
  * Create server formatter (structured JSON)
8
10
  * @returns {null} No custom formatter - uses Pino's default JSON output
@@ -18,6 +20,8 @@ export const createServerFormatter = () => {
18
20
  * @returns {Object} Pino configuration for server environments
19
21
  */
20
22
  export const getServerConfig = () => {
23
+ const redactConfig = configManager.getRedactConfig();
24
+
21
25
  return {
22
26
  level: 'info', // More conservative logging in production
23
27
  formatters: {
@@ -33,10 +37,10 @@ export const getServerConfig = () => {
33
37
  };
34
38
  }
35
39
  },
36
- // Redact sensitive information in production
40
+ // Redact sensitive information in production (configurable)
37
41
  redact: {
38
- paths: ['password', 'token', 'key', 'secret'],
39
- censor: '[REDACTED]'
42
+ paths: redactConfig.paths.length > 0 ? redactConfig.paths : ['password', 'token', 'key', 'secret'],
43
+ censor: redactConfig.censor || '[REDACTED]'
40
44
  }
41
45
  };
42
46
  };
package/index.js CHANGED
@@ -309,12 +309,6 @@ class JSGLogger {
309
309
 
310
310
  // Create loggers for all available components using default config
311
311
  const components = configManager.getAvailableComponents();
312
-
313
- // Debug: Log components being created during initialization
314
- // Note: Using console.log because this.loggers is empty at this point
315
- if (components.length > 0) {
316
- console.log(`[JSG-LOGGER] Creating ${components.length} loggers during initSync:`, components);
317
- }
318
312
 
319
313
  components.forEach(componentName => {
320
314
  // Use original createLogger to bypass utility method caching
@@ -341,7 +335,7 @@ class JSGLogger {
341
335
  try {
342
336
  callback(currentComponents);
343
337
  } catch (error) {
344
- console.error('Component subscriber error:', error);
338
+ metaError('Component subscriber error:', error);
345
339
  }
346
340
  });
347
341
  }
@@ -566,7 +560,7 @@ class JSGLogger {
566
560
  try {
567
561
  callback(currentComponents);
568
562
  } catch (error) {
569
- console.error('Component subscriber error (initial call):', error);
563
+ this.loggers.core?.error('Component subscriber error (initial call):', error);
570
564
  }
571
565
  // Return unsubscribe function
572
566
  return () => {
@@ -899,7 +893,7 @@ class JSGLogger {
899
893
  try {
900
894
  callback(currentComponents);
901
895
  } catch (error) {
902
- console.error('Component subscriber error:', error);
896
+ this.loggers.core?.error('Component subscriber error:', error);
903
897
  }
904
898
  });
905
899
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crimsonsunset/jsg-logger",
3
- "version": "1.7.14",
3
+ "version": "1.7.15",
4
4
  "type": "module",
5
5
  "description": "Multi-environment logger with smart detection, file-level overrides, and beautiful console formatting. Test it live: https://logger.joesangiorgio.com/",
6
6
  "main": "index.js",
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Redaction utility functions for sensitive data protection
3
+ * Used by browser, CLI, and server formatters
4
+ */
5
+
6
+ /**
7
+ * Check if a key should be redacted based on patterns
8
+ * @param {string} key - Key to check
9
+ * @param {string[]} paths - Array of patterns (exact match or wildcard like *key)
10
+ * @returns {boolean} Whether key should be redacted
11
+ */
12
+ export function shouldRedactKey(key, paths) {
13
+ return paths.some(pattern => {
14
+ if (pattern.startsWith('*')) {
15
+ const suffix = pattern.slice(1).toLowerCase();
16
+ return key.toLowerCase().endsWith(suffix);
17
+ }
18
+ return key.toLowerCase() === pattern.toLowerCase();
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Redact sensitive values from an object based on key patterns
24
+ * @param {*} value - Value to redact (object, array, or primitive)
25
+ * @param {Object} redactConfig - Redaction configuration with paths and censor
26
+ * @returns {*} Redacted value
27
+ */
28
+ export function redactValue(value, redactConfig) {
29
+ if (!redactConfig || !redactConfig.paths || redactConfig.paths.length === 0) {
30
+ return value;
31
+ }
32
+
33
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
34
+ const redacted = {};
35
+ for (const [key, val] of Object.entries(value)) {
36
+ if (shouldRedactKey(key, redactConfig.paths)) {
37
+ redacted[key] = redactConfig.censor || '[REDACTED]';
38
+ } else {
39
+ redacted[key] = redactValue(val, redactConfig);
40
+ }
41
+ }
42
+ return redacted;
43
+ }
44
+
45
+ if (Array.isArray(value)) {
46
+ return value.map(item => redactValue(item, redactConfig));
47
+ }
48
+
49
+ return value;
50
+ }
51
+