i18ntk 2.3.7 → 2.3.8

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
@@ -1,4 +1,4 @@
1
- # i18ntk v2.3.7
1
+ # i18ntk v2.3.8
2
2
 
3
3
  Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, and translation completion.
4
4
 
@@ -9,12 +9,12 @@ Zero-dependency internationalization toolkit for setup, scanning, analysis, vali
9
9
  [![node](https://img.shields.io/badge/node-%3E%3D16-339933)](https://nodejs.org)
10
10
  [![dependencies](https://img.shields.io/badge/dependencies-0-success)](https://www.npmjs.com/package/i18ntk)
11
11
  [![license](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
12
- [![socket](https://socket.dev/api/badge/npm/package/i18ntk/2.3.7)](https://socket.dev/npm/package/i18ntk/overview/2.3.7)
12
+ [![socket](https://socket.dev/api/badge/npm/package/i18ntk/2.3.8)](https://socket.dev/npm/package/i18ntk/overview/2.3.8)
13
13
 
14
14
  ## Upgrade Notice
15
15
 
16
- Versions earlier than `2.3.7` may contain known stability and security issues.
17
- They are considered unsupported for production use. Upgrade to `2.3.7` or newer.
16
+ Versions earlier than `2.3.8` may contain known stability and security issues.
17
+ They are considered unsupported for production use. Upgrade to `2.3.8` or newer.
18
18
 
19
19
  ## What i18ntk Does
20
20
 
@@ -151,7 +151,7 @@ Example `.i18ntk-config`:
151
151
 
152
152
  ```json
153
153
  {
154
- "version": "2.3.6",
154
+ "version": "2.3.8",
155
155
  "sourceDir": "./locales",
156
156
  "i18nDir": "./locales",
157
157
  "outputDir": "./i18ntk-reports",
@@ -174,7 +174,7 @@ See [docs/api/CONFIGURATION.md](docs/api/CONFIGURATION.md) for the full configur
174
174
  - [Runtime API Guide](docs/runtime.md)
175
175
  - [Scanner Guide](docs/scanner-guide.md)
176
176
  - [Environment Variables](docs/environment-variables.md)
177
- - [Migration Guide v2.3.5](docs/migration-guide-v2.3.6.md)
177
+ - [Migration Guide v2.3.8](docs/migration-guide-v2.3.8.md)
178
178
  - [Optimization Prompt](docs/development/package-optimization-prompt.md)
179
179
 
180
180
  ## Code of Conduct
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18ntk",
3
- "version": "2.3.7",
3
+ "version": "2.3.8",
4
4
  "description": "🚀 The fastest internationalization toolkit with 97% performance boost! Zero-dependency, enterprise-grade internationalization for React, Vue, Angular, Python, Java, PHP & more. Features PIN protection, auto framework detection, 7+ UI languages, and comprehensive translation management. Perfect for startups to enterprises.",
5
5
  "keywords": [
6
6
  "i18n",
@@ -213,7 +213,7 @@
213
213
  "languages:select": "node settings/settings-cli.js",
214
214
  "languages:list": "node settings/settings-cli.js --list-languages",
215
215
  "languages:status": "node settings/settings-cli.js --language-status",
216
- "lint:locales": "node scripts/lint-locales.js"
216
+ "lint:locales": "node scripts/lint-locales.js"
217
217
  },
218
218
  "engines": {
219
219
  "node": ">=16.0.0",
@@ -224,14 +224,17 @@
224
224
  },
225
225
  "preferGlobal": true,
226
226
  "versionInfo": {
227
- "version": "2.3.7",
228
- "releaseDate": "12/04/2026",
229
- "lastUpdated": "12/04/2026",
227
+ "version": "2.3.8",
228
+ "releaseDate": "13/04/2026",
229
+ "lastUpdated": "13/04/2026",
230
230
  "maintainer": "Vlad Noskov",
231
231
  "changelog": "./CHANGELOG.md",
232
232
  "documentation": "./README.md",
233
233
  "apiReference": "./docs/api/API_REFERENCE.md",
234
234
  "majorChanges": [
235
+ "LOGGING: Introduced centralized structured logger with silent-by-default production behavior and DEBUG_MODE/JSON_LOG toggles.",
236
+ "SECURITY: Added internal path whitelist detection to prevent false-positive traversal warnings for package/project internals.",
237
+ "I18N: Added missing-key warning TTL cache to eliminate repeated translation-key spam during builds.",
235
238
  "HOTFIX: Removed deprecated package-path fallback that caused production build warnings for non-exported subpaths.",
236
239
  "CRITICAL FIX: Resolved sizing and usage-analysis regressions in v2 command flow.",
237
240
  "PACKAGING: Reduced publish footprint by removing internal development scripts and legacy fixed-file artifacts.",
@@ -265,7 +268,7 @@
265
268
  "spring-boot": ">=2.5.0",
266
269
  "laravel": ">=8.0.0"
267
270
  },
268
- "supportPolicy": "Versions earlier than 2.3.7 may be unstable or insecure. Upgrade to 2.3.6 or newer."
271
+ "supportPolicy": "Versions earlier than 2.3.8 may be unstable or insecure. Upgrade to 2.3.8 or newer."
269
272
  },
270
273
  "_comment": "This package is zero-dependency and uses only native Node.js modules"
271
274
  }
@@ -1,8 +1,9 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const os = require('os');
4
- const crypto = require('crypto');
5
- const SecurityUtils = require('./security');
3
+ const os = require('os');
4
+ const crypto = require('crypto');
5
+ const SecurityUtils = require('./security');
6
+ const { logger } = require('./logger');
6
7
 
7
8
  // Determine package directory and user project root
8
9
  const packageDir = path.resolve(__dirname, '..');
@@ -19,7 +20,55 @@ const CONFIG_LOCK_PATH = `${PROJECT_CONFIG_PATH}.lock`;
19
20
  const CONFIG_LOCK_TIMEOUT_MS = 5000;
20
21
  const CONFIG_LOCK_STALE_MS = 15000;
21
22
  const CONFIG_LOCK_RETRY_MS = 50;
22
- let autosaveDisabledWarned = false;
23
+ let autosaveDisabledWarned = false;
24
+ let defaultConfigNoticeShown = false;
25
+ let configFallbackNoticeShown = false;
26
+
27
+ function logInfo(message, details) {
28
+ logger.info(message, details);
29
+ }
30
+
31
+ function logWarn(message, details) {
32
+ logger.warn(message, details);
33
+ }
34
+
35
+ function logError(message, details) {
36
+ logger.error(message, details);
37
+ }
38
+
39
+ function notifyDefaultConfig(reason, error) {
40
+ if (defaultConfigNoticeShown) {
41
+ return;
42
+ }
43
+ defaultConfigNoticeShown = true;
44
+
45
+ logger.info('Using default configuration (reason: configuration error)', { reason });
46
+ if (logger.isDebugMode() && error) {
47
+ logger.debug(`Default configuration reason details: ${error.message}`, {
48
+ stack: error.stack
49
+ });
50
+ }
51
+ }
52
+
53
+ function notifyConfigFallback(error) {
54
+ if (!configFallbackNoticeShown) {
55
+ configFallbackNoticeShown = true;
56
+ logger.info('Using default configuration (reason: configuration error)', {
57
+ reason: 'configuration error'
58
+ });
59
+ }
60
+
61
+ logger.recordFirstError('config:load-fallback', {
62
+ message: error && error.message ? error.message : 'Unknown configuration error',
63
+ stack: error && error.stack ? error.stack : null
64
+ });
65
+
66
+ if (logger.isDebugMode() && error) {
67
+ logger.debug(`Configuration load fallback details: ${error.message}`, {
68
+ stack: error.stack
69
+ });
70
+ }
71
+ }
23
72
 
24
73
  // Setup tracking file
25
74
  const SETUP_COMPLETED_FILE = path.join(PROJECT_SETTINGS_DIR, 'setup.json');
@@ -381,7 +430,7 @@ function tryReadJson(filePath) {
381
430
 
382
431
  const data = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
383
432
  if (!data || data.trim() === '') {
384
- console.warn(`[i18ntk] Warning: Empty or invalid JSON file at ${filePath}`);
433
+ logWarn(`[i18ntk] Warning: Empty or invalid JSON file at ${filePath}`);
385
434
  return null;
386
435
  }
387
436
 
@@ -390,18 +439,18 @@ function tryReadJson(filePath) {
390
439
  return parsed;
391
440
  }
392
441
 
393
- console.error(`[i18ntk] Error parsing JSON from ${filePath}: Invalid JSON content`);
442
+ logError(`[i18ntk] Error parsing JSON from ${filePath}: Invalid JSON content`);
394
443
  // Create a backup of the corrupted file
395
444
  const backupPath = `${filePath}.corrupted-${Date.now()}.bak`;
396
445
  try {
397
446
  SecurityUtils.safeWriteFileSync(backupPath, data, path.dirname(backupPath), 'utf8');
398
- console.warn(`[i18ntk] Created backup of corrupted config at ${backupPath}`);
447
+ logWarn(`[i18ntk] Created backup of corrupted config at ${backupPath}`);
399
448
  } catch (backupError) {
400
- console.error(`[i18ntk] Failed to create backup of corrupted config: ${backupError.message}`);
449
+ logError(`[i18ntk] Failed to create backup of corrupted config: ${backupError.message}`);
401
450
  }
402
451
  return null;
403
452
  } catch (error) {
404
- console.error(`[i18ntk] Error reading config file at ${filePath}: ${error.message}`);
453
+ logError(`[i18ntk] Error reading config file at ${filePath}: ${error.message}`);
405
454
  return null;
406
455
  }
407
456
  }
@@ -420,11 +469,11 @@ async function migrateLegacyIfNeeded(baseCfg) {
420
469
  // Best-effort removal of legacy file to prevent future use
421
470
  try { fs.unlinkSync(LEGACY_CONFIG_PATH); } catch (_) {}
422
471
  // Deprecation notice
423
- console.warn('[i18ntk] Deprecated config location detected (~/.i18ntk). Configuration was migrated to project .i18ntk-config.');
472
+ logWarn('[i18ntk] Deprecated config location detected (~/.i18ntk). Configuration was migrated to project .i18ntk-config.');
424
473
  return merged;
425
474
  } catch (_) {
426
475
  // If write fails, fall back to in-memory config without deleting legacy
427
- console.warn('[i18ntk] Deprecated config location detected (~/.i18ntk). Using migrated settings in memory; failed to persist to .i18ntk-config.');
476
+ logWarn('[i18ntk] Deprecated config location detected (~/.i18ntk). Using migrated settings in memory; failed to persist to .i18ntk-config.');
428
477
  return merged;
429
478
  }
430
479
  }
@@ -445,7 +494,7 @@ function loadConfig() {
445
494
 
446
495
  // Prevent concurrent loading
447
496
  if (configLoadInProgress) {
448
- console.warn('[i18ntk] Configuration loading already in progress, returning defaults');
497
+ logWarn('[i18ntk] Configuration loading already in progress, returning defaults');
449
498
  resetRecursionGuard();
450
499
  return clone(DEFAULT_CONFIG);
451
500
  }
@@ -472,7 +521,7 @@ function loadConfig() {
472
521
  // Attempt to migrate to project settings
473
522
  // Ignore migration errors; we still return merged cfg in memory
474
523
  // eslint-disable-next-line no-unused-vars
475
- console.warn('[i18ntk] Detected legacy config at ~/.i18ntk. Migrating to project settings directory...');
524
+ logWarn('[i18ntk] Detected legacy config at ~/.i18ntk. Migrating to project settings directory...');
476
525
  const _ = (async () => { await migrateLegacyIfNeeded(DEFAULT_CONFIG); })();
477
526
  }
478
527
  }
@@ -481,7 +530,8 @@ function loadConfig() {
481
530
  currentConfig = cfg;
482
531
  return currentConfig;
483
532
  } catch (error) {
484
- console.error('[i18ntk] Error in loadConfig:', error.message);
533
+ logError('[i18ntk] Error in loadConfig', { error: error.message });
534
+ notifyConfigFallback(error);
485
535
  currentConfig = clone(DEFAULT_CONFIG);
486
536
  return currentConfig;
487
537
  } finally {
@@ -498,7 +548,7 @@ async function saveConfig(cfg = currentConfig) {
498
548
  currentConfig = cfg;
499
549
  if (!autosaveDisabledWarned) {
500
550
  autosaveDisabledWarned = true;
501
- console.warn('[i18ntk] Autosave disabled by I18NTK_DISABLE_AUTOSAVE. Keeping configuration in memory only.');
551
+ logWarn('[i18ntk] Autosave disabled by I18NTK_DISABLE_AUTOSAVE. Keeping configuration in memory only.');
502
552
  }
503
553
  return false;
504
554
  }
@@ -540,7 +590,7 @@ async function saveConfig(cfg = currentConfig) {
540
590
  currentConfig = cfg;
541
591
  return true;
542
592
  } catch (error) {
543
- console.error('[i18ntk] Error saving configuration:', error.message);
593
+ logError('[i18ntk] Error saving configuration', { error: error.message });
544
594
  return false;
545
595
  } finally {
546
596
  if (releaseLock) {
@@ -593,7 +643,7 @@ function getConfig() {
593
643
 
594
644
  // Check for legacy config for migration
595
645
  if (SecurityUtils.safeExistsSync(LEGACY_CONFIG_PATH)) {
596
- console.log('📦 Migrating legacy configuration...');
646
+ logInfo('Migrating legacy configuration');
597
647
  const legacyRaw = SecurityUtils.safeReadFileSync(LEGACY_CONFIG_PATH, path.dirname(LEGACY_CONFIG_PATH), 'utf8');
598
648
  const legacyConfig = SecurityUtils.safeParseJSON(legacyRaw);
599
649
  if (!legacyConfig || typeof legacyConfig !== 'object') {
@@ -601,7 +651,7 @@ function getConfig() {
601
651
  }
602
652
  const migratedConfig = { ...DEFAULT_CONFIG, ...legacyConfig };
603
653
  saveConfig(migratedConfig).catch((err) => {
604
- console.warn('[i18ntk] Warning: failed to persist migrated configuration:', err.message);
654
+ logWarn('[i18ntk] Warning: failed to persist migrated configuration', { error: err.message });
605
655
  });
606
656
  currentConfig = migratedConfig;
607
657
 
@@ -619,18 +669,18 @@ function getConfig() {
619
669
  }
620
670
 
621
671
  // Use package defaults for new installation
622
- console.log('📦 Initializing with default configuration...');
623
- saveConfig(DEFAULT_CONFIG).catch((err) => {
624
- console.warn('[i18ntk] Warning: failed to persist default configuration:', err.message);
625
- });
626
- currentConfig = DEFAULT_CONFIG;
627
- return resolvePaths(DEFAULT_CONFIG);
628
-
629
- } catch (error) {
630
- console.warn('⚠️ Error loading configuration, using defaults:', error.message);
631
- currentConfig = DEFAULT_CONFIG;
632
- return resolvePaths(DEFAULT_CONFIG);
633
- }
672
+ notifyDefaultConfig('default initialization');
673
+ saveConfig(DEFAULT_CONFIG).catch((err) => {
674
+ logWarn('[i18ntk] Warning: failed to persist default configuration', { error: err.message });
675
+ });
676
+ currentConfig = DEFAULT_CONFIG;
677
+ return resolvePaths(DEFAULT_CONFIG);
678
+
679
+ } catch (error) {
680
+ notifyConfigFallback(error);
681
+ currentConfig = DEFAULT_CONFIG;
682
+ return resolvePaths(DEFAULT_CONFIG);
683
+ }
634
684
  }
635
685
 
636
686
  async function setConfig(cfg) {
@@ -693,3 +743,7 @@ module.exports = {
693
743
  toRelative,
694
744
  normalizePathValue,
695
745
  }
746
+
747
+
748
+
749
+
package/utils/config.js CHANGED
@@ -1,6 +1,7 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const SecurityUtils = require('./security');
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const SecurityUtils = require('./security');
4
+ const { logger } = require('./logger');
4
5
 
5
6
  const settingsManager = require('../settings/settings-manager');
6
7
  const CONFIG_FILE = 'i18ntk-config.json';
@@ -66,11 +67,11 @@ function loadConfig(cwd = settingsManager.configDir) {
66
67
  }
67
68
 
68
69
  throw new Error('Invalid configuration format');
69
- } catch (error) {
70
- console.error(`Error loading config: ${error.message}`);
71
- return null;
72
- }
73
- }
70
+ } catch (error) {
71
+ logger.debug(`Error loading config: ${error.message}`, { stack: error.stack });
72
+ return null;
73
+ }
74
+ }
74
75
 
75
76
  /**
76
77
  * Saves configuration to file
@@ -96,11 +97,11 @@ function saveConfig(config, cwd = settingsManager.configDir) {
96
97
  );
97
98
 
98
99
  return true;
99
- } catch (error) {
100
- console.error(`Error saving config: ${error.message}`);
101
- return false;
102
- }
103
- }
100
+ } catch (error) {
101
+ logger.error(`Error saving config: ${error.message}`);
102
+ return false;
103
+ }
104
+ }
104
105
 
105
106
  /**
106
107
  * Ensures default values for configuration
@@ -127,4 +128,4 @@ module.exports = {
127
128
  saveConfig,
128
129
  ensureConfigDefaults,
129
130
  validatePath // Exported for testing
130
- };
131
+ };
@@ -1,6 +1,7 @@
1
1
  // utils/i18n-helper.js
2
- const path = require('path');
3
- const fs = require('fs');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const { logger } = require('./logger');
4
5
 
5
6
  // Lazy load SecurityUtils to prevent circular dependencies
6
7
  let securityUtils;
@@ -200,10 +201,21 @@ function findLocaleFilesAllDirs(lang, preferredDir) {
200
201
  return files;
201
202
  }
202
203
 
203
- let translations = {};
204
- let currentLanguage = 'en';
205
- let isInitialized = false;
206
- const missingWarned = new Set();
204
+ let translations = {};
205
+ let currentLanguage = 'en';
206
+ let isInitialized = false;
207
+ const missingKeyCache = new Map();
208
+ const missingKeyTtlMs = 5 * 60 * 1000;
209
+
210
+ function shouldReportMissingKey(key) {
211
+ const now = Date.now();
212
+ const expiresAt = missingKeyCache.get(key) || 0;
213
+ if (expiresAt > now) {
214
+ return false;
215
+ }
216
+ missingKeyCache.set(key, now + missingKeyTtlMs);
217
+ return true;
218
+ }
207
219
 
208
220
  function loadTranslations(language) {
209
221
  const cfg = safeRequireConfig();
@@ -286,9 +298,11 @@ function loadTranslations(language) {
286
298
  currentLanguage = 'en';
287
299
  isInitialized = true;
288
300
 
289
- if (loadErrors.length > 0) {
290
- console.warn(`⚠️ No valid UI locale files found. Using built-in English strings.`);
291
- }
301
+ if (loadErrors.length > 0) {
302
+ logger.warn('No valid UI locale files found. Using built-in English strings.', {
303
+ errorCount: loadErrors.length
304
+ });
305
+ }
292
306
 
293
307
  return translations;
294
308
  }
@@ -337,23 +351,24 @@ function t(key, params = {}) {
337
351
  }
338
352
  }
339
353
 
340
- if (typeof value === 'undefined') {
341
- if (!missingWarned.has(key)) {
342
- missingWarned.add(key);
343
- console.warn(`Translation key not found: ${key}`);
344
- }
345
- return key;
346
- }
354
+ if (typeof value === 'undefined') {
355
+ if (shouldReportMissingKey(key)) {
356
+ logger.logMissingTranslationKey(key, 'Configuration error');
357
+ }
358
+ return key;
359
+ }
347
360
 
348
361
  // If we found a string, interpolate parameters
349
362
  if (typeof value === 'string') {
350
363
  return interpolateParams(value, params);
351
364
  }
352
365
 
353
- // Return the key if the final value is not a string
354
- console.warn(`Translation key does not resolve to a string: ${key}`);
355
- return key;
356
- }
366
+ // Return the key if the final value is not a string
367
+ if (shouldReportMissingKey(`${key}:non-string`)) {
368
+ logger.warn(`Translation key does not resolve to a string: ${key}`);
369
+ }
370
+ return key;
371
+ }
357
372
 
358
373
  /**
359
374
  * Interpolate parameters into a translation string
package/utils/logger.js CHANGED
@@ -1,64 +1,233 @@
1
- const colors = require('./colors-new');
2
- const { envManager } = require('./env-manager');
3
-
4
- // Enhanced logger with TTY detection and proper stream handling
5
- const logger = {
6
- // Basic log function with color support
7
- log: (message, color = '') => {
8
- const output = color ? color(message) : message;
9
- if (process.stdout.isTTY) {
10
- process.stdout.write(output + '\n');
11
- } else {
12
- console.log(output);
13
- }
14
- },
15
-
16
- // Error logging (goes to stderr)
17
- error: (message) => {
18
- const output = `❌ ${message}`;
19
- if (process.stderr.isTTY) {
20
- process.stderr.write(colors.bright(colors.red(output)) + '\n');
21
- } else {
22
- console.error(output);
23
- }
24
- },
25
-
26
- // Success logging
27
- success: (message) => {
28
- const output = `✅ ${message}`;
29
- logger.log(output, colors.green);
30
- },
31
-
32
- // Warning logging (goes to stderr)
33
- warn: (message) => {
34
- const output = `⚠️ ${message}`;
35
- if (process.stderr.isTTY) {
36
- process.stderr.write(colors.bright(colors.yellow(output)) + '\n');
37
- } else {
38
- console.warn(output);
39
- }
40
- },
41
-
42
- // Info logging
43
- info: (message) => {
44
- const output = `ℹ️ ${message}`;
45
- logger.log(output, colors.blue);
46
- },
47
-
48
- // Debug logging (only when DEBUG env var is set or log level is debug)
49
- debug: (message) => {
50
- const logLevel = envManager.get('I18NTK_LOG_LEVEL');
51
- const debugEnabled = logLevel === 'debug';
52
-
53
- if (debugEnabled) {
54
- const output = `[DEBUG] ${message}`;
55
- if (process.stderr.isTTY) {
56
- process.stderr.write(colors.gray(output) + '\n');
57
- } else {
58
- console.debug(output);
59
- }
60
- }
61
- }
62
- };
63
-
64
- module.exports = { colors, logger };
1
+ const colors = require('./colors-new');
2
+
3
+ const LEVELS = { error: 0, warn: 1, info: 2, debug: 3 };
4
+ const DEFAULT_INTERNAL_PREFIXES = ['[BUILD]', '[WORKERS]', '[I18N]', '[SECURITY]', '[SUCCESS]', '[WARN]', '[ERROR]', '[INFO]', '[DEBUG]'];
5
+ const FIRST_ERROR_CONTEXT = new Map();
6
+
7
+ function asBoolean(value) {
8
+ return String(value || '').trim().toLowerCase() === 'true';
9
+ }
10
+
11
+ function resolveLevel() {
12
+ const debugMode = asBoolean(process.env.DEBUG_MODE);
13
+ const configured = String(process.env.I18NTK_LOG_LEVEL || '').trim().toLowerCase();
14
+ if (configured && Object.prototype.hasOwnProperty.call(LEVELS, configured)) {
15
+ return configured;
16
+ }
17
+
18
+ if (debugMode) {
19
+ return 'debug';
20
+ }
21
+
22
+ // Silent-by-default in production-like builds.
23
+ if (process.env.NODE_ENV === 'production' || asBoolean(process.env.CI)) {
24
+ return 'error';
25
+ }
26
+
27
+ return 'warn';
28
+ }
29
+
30
+ function shouldLog(level) {
31
+ const active = resolveLevel();
32
+ return LEVELS[level] <= LEVELS[active];
33
+ }
34
+
35
+ function normalizePrefix(prefix, fallback) {
36
+ const value = String(prefix || fallback || '[INFO]').trim();
37
+ if (/^\[[^\]]+\]$/.test(value)) {
38
+ return value;
39
+ }
40
+ return `[${value.replace(/^[\[]|[\]]$/g, '').toUpperCase()}]`;
41
+ }
42
+
43
+ function write(level, message, options = {}) {
44
+ if (!shouldLog(level)) return;
45
+
46
+ const jsonMode = asBoolean(process.env.JSON_LOG);
47
+ const prefix = normalizePrefix(options.prefix, `[${level.toUpperCase()}]`);
48
+ const details = options.details && typeof options.details === 'object' ? options.details : undefined;
49
+ const text = String(message || '').trim();
50
+
51
+ if (jsonMode) {
52
+ const payload = {
53
+ timestamp: new Date().toISOString(),
54
+ level,
55
+ prefix,
56
+ message: text,
57
+ ...(details ? { details } : {})
58
+ };
59
+ const line = JSON.stringify(payload);
60
+ if (level === 'error' || level === 'warn') {
61
+ process.stderr.write(`${line}\n`);
62
+ } else {
63
+ process.stdout.write(`${line}\n`);
64
+ }
65
+ return;
66
+ }
67
+
68
+ const line = `${prefix} ${text}`;
69
+ if (level === 'error') {
70
+ process.stderr.write(`${colors.red(line)}\n`);
71
+ return;
72
+ }
73
+ if (level === 'warn') {
74
+ process.stderr.write(`${colors.yellow(line)}\n`);
75
+ return;
76
+ }
77
+ if (level === 'debug') {
78
+ process.stdout.write(`${colors.gray(line)}\n`);
79
+ return;
80
+ }
81
+
82
+ process.stdout.write(`${line}\n`);
83
+ }
84
+
85
+ function formatDuration(ms) {
86
+ if (!Number.isFinite(ms) || ms < 0) return 'instant';
87
+ if (ms < 50) return 'instant';
88
+ if (ms < 1000) return `${Math.round(ms)}ms`;
89
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
90
+ const minutes = Math.floor(ms / 60000);
91
+ const seconds = Math.round((ms % 60000) / 1000);
92
+ return `${minutes}m${seconds}s`;
93
+ }
94
+
95
+ function recordFirstError(key, context) {
96
+ if (!key || FIRST_ERROR_CONTEXT.has(key)) {
97
+ return;
98
+ }
99
+
100
+ FIRST_ERROR_CONTEXT.set(key, {
101
+ timestamp: new Date().toISOString(),
102
+ ...context
103
+ });
104
+ }
105
+
106
+ function getFirstErrorContext(key) {
107
+ return FIRST_ERROR_CONTEXT.get(key) || null;
108
+ }
109
+
110
+ function flushErrorContexts() {
111
+ const entries = Array.from(FIRST_ERROR_CONTEXT.entries()).map(([key, value]) => ({ key, ...value }));
112
+ FIRST_ERROR_CONTEXT.clear();
113
+ return entries;
114
+ }
115
+
116
+ function emitErrorContextSummary({ force = false } = {}) {
117
+ const contexts = Array.from(FIRST_ERROR_CONTEXT.entries());
118
+ if (!contexts.length) return;
119
+ if (!force && !asBoolean(process.env.DEBUG_MODE)) return;
120
+
121
+ for (const [key, context] of contexts) {
122
+ write('debug', `First error context (${key})`, { prefix: '[ERROR]', details: context });
123
+ }
124
+ }
125
+
126
+ function logMissingTranslationKey(key, fallback = 'Configuration error') {
127
+ write('warn', `Missing key: ${key} (fallback: '${fallback}')`, { prefix: '[I18N]' });
128
+ }
129
+
130
+ function createBuildProgressReporter(totalPages) {
131
+ const safeTotal = Math.max(1, Number(totalPages) || 1);
132
+ let lastBucket = 0;
133
+
134
+ return {
135
+ update(completedPages) {
136
+ const completed = Math.max(0, Math.min(safeTotal, Number(completedPages) || 0));
137
+ const percent = Math.floor((completed / safeTotal) * 100);
138
+ const bucket = Math.floor(percent / 10) * 10;
139
+ if (bucket <= lastBucket || bucket === 0) return;
140
+ lastBucket = bucket;
141
+
142
+ const pct = `${String(bucket).padStart(3, ' ')}%`;
143
+ write('info', `${pct} (${completed}/${safeTotal} pages)`, { prefix: '[BUILD]' });
144
+ }
145
+ };
146
+ }
147
+
148
+ function buildSuccessSummary({ durationMs, pages, warnings = 0 }) {
149
+ const duration = formatDuration(durationMs);
150
+ write('info', `Build completed in ${duration} (${pages} pages, ${warnings} warnings)`, { prefix: '[SUCCESS]' });
151
+ }
152
+
153
+ function createWorkerPoolMonitor(activeWorkers = 0) {
154
+ const startedAt = Date.now();
155
+ let tasks = 0;
156
+ let totalTaskDurationMs = 0;
157
+ let workers = Math.max(0, Number(activeWorkers) || 0);
158
+
159
+ return {
160
+ setActive(count) {
161
+ workers = Math.max(0, Number(count) || 0);
162
+ },
163
+ recordTask(durationMs) {
164
+ tasks += 1;
165
+ totalTaskDurationMs += Math.max(0, Number(durationMs) || 0);
166
+ },
167
+ report() {
168
+ const avg = tasks > 0 ? (totalTaskDurationMs / tasks) / 1000 : 0;
169
+ write('info', `${workers} active (avg ${avg.toFixed(1)}s/task)`, { prefix: '[WORKERS]' });
170
+ return {
171
+ workers,
172
+ tasks,
173
+ avgSecondsPerTask: Number(avg.toFixed(2)),
174
+ elapsed: formatDuration(Date.now() - startedAt)
175
+ };
176
+ }
177
+ };
178
+ }
179
+
180
+ const logger = {
181
+ log(message, color = null) {
182
+ const output = typeof color === 'function' ? color(String(message)) : String(message);
183
+ write('info', output, { prefix: '[INFO]' });
184
+ },
185
+ info(message, details) {
186
+ write('info', String(message), { prefix: '[INFO]', details });
187
+ },
188
+ warn(message, details) {
189
+ write('warn', String(message), { prefix: '[WARN]', details });
190
+ },
191
+ error(message, details) {
192
+ write('error', String(message), { prefix: '[ERROR]', details });
193
+ },
194
+ debug(message, details) {
195
+ write('debug', String(message), { prefix: '[DEBUG]', details });
196
+ },
197
+ success(message, details) {
198
+ write('info', String(message), { prefix: '[SUCCESS]', details });
199
+ },
200
+ security(level, message, details) {
201
+ write(level, String(message), { prefix: '[SECURITY]', details });
202
+ },
203
+ build(message, details) {
204
+ write('info', String(message), { prefix: '[BUILD]', details });
205
+ },
206
+ isDebugMode() {
207
+ return asBoolean(process.env.DEBUG_MODE) || resolveLevel() === 'debug';
208
+ },
209
+ shouldLog,
210
+ formatDuration,
211
+ recordFirstError,
212
+ getFirstErrorContext,
213
+ flushErrorContexts,
214
+ emitErrorContextSummary,
215
+ buildSuccessSummary,
216
+ logMissingTranslationKey,
217
+ createBuildProgressReporter,
218
+ createWorkerPoolMonitor,
219
+ // Keep compatibility for callers that rely on known prefixes.
220
+ KNOWN_PREFIXES: DEFAULT_INTERNAL_PREFIXES
221
+ };
222
+
223
+ module.exports = {
224
+ colors,
225
+ logger,
226
+ formatDuration,
227
+ createBuildProgressReporter,
228
+ createWorkerPoolMonitor,
229
+ recordFirstError,
230
+ getFirstErrorContext,
231
+ flushErrorContexts,
232
+ emitErrorContextSummary
233
+ };
package/utils/security.js CHANGED
@@ -1,26 +1,93 @@
1
- const path = require('path');
2
- const fs = require('fs');
3
- const crypto = require('crypto');
4
-
5
-
6
- // Lazy load configManager to avoid circular dependency
7
- let configManager;
8
- let configManagerLoadAttempted = false;
9
- function getConfigManager() {
10
- if (!configManager && !configManagerLoadAttempted) {
11
- configManagerLoadAttempted = true;
12
- try {
13
- configManager = require('./config-manager');
14
- } catch (error) {
15
- // Return null if config-manager can't be loaded
16
- return null;
17
- }
18
- }
19
- return configManager;
20
- }
21
-
22
- // Lazy load i18n to prevent initialization race conditions
23
- let i18n;
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const crypto = require('crypto');
4
+ const { logger } = require('./logger');
5
+
6
+ const INTERNAL_MANIFEST_CACHE = {
7
+ initialized: false,
8
+ roots: new Set()
9
+ };
10
+
11
+ function findPackageRoot(startDir) {
12
+ let current = path.resolve(startDir || process.cwd());
13
+ while (true) {
14
+ const manifest = path.join(current, 'package.json');
15
+ if (fs.existsSync(manifest)) {
16
+ return current;
17
+ }
18
+ const parent = path.dirname(current);
19
+ if (parent === current) {
20
+ return null;
21
+ }
22
+ current = parent;
23
+ }
24
+ }
25
+
26
+ function initializeInternalRoots() {
27
+ if (INTERNAL_MANIFEST_CACHE.initialized) {
28
+ return INTERNAL_MANIFEST_CACHE.roots;
29
+ }
30
+
31
+ INTERNAL_MANIFEST_CACHE.initialized = true;
32
+ const roots = INTERNAL_MANIFEST_CACHE.roots;
33
+ const candidates = [
34
+ process.cwd(),
35
+ path.resolve(__dirname, '..'),
36
+ findPackageRoot(process.cwd()),
37
+ findPackageRoot(path.resolve(__dirname, '..'))
38
+ ].filter(Boolean);
39
+
40
+ for (const candidate of candidates) {
41
+ roots.add(path.resolve(candidate));
42
+ }
43
+
44
+ const custom = String(process.env.I18NTK_INTERNAL_PATH_PREFIXES || '')
45
+ .split(',')
46
+ .map((entry) => entry.trim())
47
+ .filter(Boolean);
48
+ for (const prefix of custom) {
49
+ roots.add(path.resolve(prefix));
50
+ }
51
+
52
+ return roots;
53
+ }
54
+
55
+ function normalizeForCompare(value) {
56
+ return String(value || '').replace(/\\/g, '/').toLowerCase();
57
+ }
58
+
59
+ function isPathInside(root, target) {
60
+ const normalizedRoot = normalizeForCompare(path.resolve(root));
61
+ const normalizedTarget = normalizeForCompare(path.resolve(target));
62
+ return normalizedTarget === normalizedRoot || normalizedTarget.startsWith(`${normalizedRoot}/`);
63
+ }
64
+
65
+ function isInternalPath(inputPath) {
66
+ if (!inputPath || typeof inputPath !== 'string') {
67
+ return false;
68
+ }
69
+ const roots = initializeInternalRoots();
70
+ const absolute = path.resolve(inputPath);
71
+ for (const root of roots) {
72
+ if (isPathInside(root, absolute)) {
73
+ return true;
74
+ }
75
+ }
76
+ return false;
77
+ }
78
+
79
+ function detectDangerReason(filePath) {
80
+ if (/\.\.(\/|\\|$)/.test(filePath)) return 'Contains parent directory traversal segments';
81
+ if (/(^|[\/\\])~([\/\\]|$)/.test(filePath)) return 'Contains home-directory shorthand';
82
+ if (/\$\{/.test(filePath)) return 'Contains variable expansion token';
83
+ if (/`/.test(filePath)) return 'Contains command substitution token';
84
+ if (/[|;&<>]/.test(filePath)) return 'Contains shell metacharacters';
85
+ return null;
86
+ }
87
+
88
+
89
+ // Lazy load i18n to prevent initialization race conditions
90
+ let i18n;
24
91
  function getI18n() {
25
92
  if (!i18n) {
26
93
  try {
@@ -74,11 +141,13 @@ static _logging = false;
74
141
  SecurityUtils._operationStack = new Set();
75
142
  }
76
143
 
77
- if (SecurityUtils._operationStack.has(operationName)) {
78
- const i18n = getI18n();
79
- SecurityUtils.logSecurityEvent(i18n.t('security.recursion_detected', { operation: operationName }), 'error');
80
- return null;
81
- }
144
+ if (SecurityUtils._operationStack.has(operationName)) {
145
+ SecurityUtils.logSecurityEvent('Recursion detected while performing secure operation', 'warn', {
146
+ operation: operationName,
147
+ source: 'internal'
148
+ });
149
+ return null;
150
+ }
82
151
 
83
152
  SecurityUtils._operationStack.add(operationName);
84
153
 
@@ -88,12 +157,15 @@ static _logging = false;
88
157
  let hasResult = false;
89
158
  let timeoutId = null;
90
159
 
91
- timeoutId = setTimeout(() => {
92
- if (!hasResult) {
93
- const i18n = getI18n();
94
- SecurityUtils.logSecurityEvent(i18n.t('security.operation_timeout', { operation: operationName }), 'warning');
95
- }
96
- }, timeoutMs);
160
+ timeoutId = setTimeout(() => {
161
+ if (!hasResult) {
162
+ SecurityUtils.logSecurityEvent('Secure operation timeout', 'warn', {
163
+ operation: operationName,
164
+ timeoutMs,
165
+ source: 'internal'
166
+ });
167
+ }
168
+ }, timeoutMs);
97
169
 
98
170
  // Execute operation synchronously
99
171
  result = operation();
@@ -105,12 +177,12 @@ static _logging = false;
105
177
 
106
178
  return result;
107
179
  } catch (error) {
108
- const i18n = getI18n();
109
- SecurityUtils.logSecurityEvent('Operation error', 'error', {
110
- operation: operationName,
111
- error: error.message
112
- });
113
- return null;
180
+ SecurityUtils.logSecurityEvent('Secure operation error', 'error', {
181
+ operation: operationName,
182
+ error: error.message,
183
+ source: 'internal'
184
+ });
185
+ return null;
114
186
  } finally {
115
187
  SecurityUtils._operationStack.delete(operationName);
116
188
  }
@@ -122,60 +194,40 @@ static _logging = false;
122
194
  * @param {string} level - Log level (info, warn, error)
123
195
  * @param {object} details - Additional details
124
196
  */
125
- static logSecurityEvent(event, level = 'info', details = {}) {
126
- // Prevent recursive logging which can occur during configuration loading
127
- if (SecurityUtils._logging) {
128
- return;
129
- }
130
-
131
- SecurityUtils._logging = true;
132
- try {
133
- const cfg = getConfigManager()?.getConfig?.() || {};
134
- const envLevel = (process.env.SECURITY_LOG_LEVEL || process.env.I18NTK_SECURITY_LOG_LEVEL || '').toLowerCase();
135
- const configLevel = (cfg.security?.logLevel || cfg.security?.audit?.logLevel || '').toLowerCase();
136
-
137
- // Check for debug mode
138
- const debugMode = process.env.I18N_DEBUG === 'true' || process.env.DEBUG === 'true';
139
- const currentLevel = debugMode ? 'info' : (envLevel || configLevel || 'warn');
140
-
141
- const levels = { error: 0, warn: 1, warning: 1, info: 2 };
142
- const messageLevel = levels[level.toLowerCase()] ?? 2;
143
- const allowedLevel = levels[currentLevel] ?? 1;
144
- if (messageLevel > allowedLevel) {
145
- return;
146
- }
147
-
148
- const timestamp = new Date().toISOString();
149
- const logEntry = {
150
- timestamp,
151
- level,
152
- event,
153
- details: {
154
- ...details,
155
- pid: process.pid,
156
- nodeVersion: process.version
157
- }
158
- };
159
-
160
- const message = `[SECURITY ${level.toUpperCase()}] ${timestamp}: ${event}`;
161
- if (level === 'error') {
162
- console.error(message, details);
163
- } else if (level === 'warn' || level === 'warning') {
164
- console.warn(message, details);
165
- } else {
166
- console.log(message, details);
167
- }
168
- } finally {
169
- SecurityUtils._logging = false;
170
- }
171
- }
197
+ static logSecurityEvent(event, level = 'info', details = {}) {
198
+ if (SecurityUtils._logging) {
199
+ return;
200
+ }
201
+
202
+ SecurityUtils._logging = true;
203
+ try {
204
+ const debugMode = logger.isDebugMode();
205
+ const explicitSecurityLogs = process.env.I18NTK_ENABLE_SECURITY_LOGS === 'true';
206
+ const source = details && details.source ? String(details.source).toLowerCase() : 'internal';
207
+ const levelName = String(level || 'info').toLowerCase();
208
+ const normalizedLevel = levelName === 'warning' ? 'warn' : levelName;
209
+
210
+ // Security warnings from internal paths are noise during builds.
211
+ if (!debugMode && !explicitSecurityLogs && source !== 'user') {
212
+ return;
213
+ }
214
+
215
+ logger.security(normalizedLevel, event, {
216
+ ...details,
217
+ pid: process.pid,
218
+ nodeVersion: process.version
219
+ });
220
+ } finally {
221
+ SecurityUtils._logging = false;
222
+ }
223
+ }
172
224
 
173
225
  // Add other static methods here...
174
- static validatePath(filePath, basePath = process.cwd(), verbose = false) {
175
- const i18n = getI18n();
176
- const useI18n = i18n && i18n.isInitialized && typeof i18n.t === 'function';
177
-
178
- try {
226
+ static validatePath(filePath, basePath = process.cwd(), verbose = false) {
227
+ const i18n = getI18n();
228
+ const useI18n = i18n && i18n.isInitialized && typeof i18n.t === 'function';
229
+
230
+ try {
179
231
  // Check against whitelist patterns for our own package artifacts
180
232
  if (SecurityUtils.PACKAGE_ARTIFACT_WHITELIST.some(pattern => pattern.test(filePath))) {
181
233
  return filePath;
@@ -195,17 +247,42 @@ static _logging = false;
195
247
  return null;
196
248
  }
197
249
 
198
- // Check for obvious dangerous patterns first
199
- if (!SecurityUtils.isSafePath(filePath)) {
200
- const message = useI18n
201
- ? i18n.t('security.pathTraversalAttempt')
202
- : 'Path traversal attempt';
203
- SecurityUtils.logSecurityEvent(message, 'warning', {
204
- inputPath: filePath,
205
- reason: 'Contains dangerous patterns'
206
- });
207
- return null;
208
- }
250
+ const isWindowsAbsolute = /^[A-Z]:[\/\\]/i.test(filePath);
251
+ const isUnixAbsolute = filePath.startsWith('/') || filePath.startsWith('\\');
252
+ const isAbsolutePath = isWindowsAbsolute || isUnixAbsolute;
253
+ const dangerousReason = detectDangerReason(filePath);
254
+ const source = isInternalPath(filePath) ? 'internal' : 'user';
255
+
256
+ if (isAbsolutePath && isInternalPath(filePath)) {
257
+ return path.resolve(filePath);
258
+ }
259
+
260
+ // For absolute paths, defer trust decision to base-path containment checks below,
261
+ // but still reject obvious shell/injection markers.
262
+ if (isAbsolutePath && dangerousReason) {
263
+ const message = useI18n
264
+ ? i18n.t('security.pathTraversalAttempt')
265
+ : 'Path traversal attempt';
266
+ SecurityUtils.logSecurityEvent(message, 'warning', {
267
+ inputPath: filePath,
268
+ reason: dangerousReason,
269
+ source
270
+ });
271
+ return null;
272
+ }
273
+
274
+ // Check for obvious dangerous patterns first for relative paths
275
+ if (!isAbsolutePath && !SecurityUtils.isSafePath(filePath)) {
276
+ const message = useI18n
277
+ ? i18n.t('security.pathTraversalAttempt')
278
+ : 'Path traversal attempt';
279
+ SecurityUtils.logSecurityEvent(message, 'warning', {
280
+ inputPath: filePath,
281
+ reason: dangerousReason || 'Contains unsafe path segments',
282
+ source
283
+ });
284
+ return null;
285
+ }
209
286
 
210
287
  // Resolve base and target paths
211
288
  const base = fs.realpathSync(basePath);
@@ -220,44 +297,47 @@ static _logging = false;
220
297
  }
221
298
 
222
299
  // Check for actual path traversal (going outside the base directory)
223
- const relativePath = path.relative(base, finalPath);
224
- if (relativePath.startsWith('..')) {
225
- const message = useI18n
226
- ? i18n.t('security.pathTraversalAttempt')
227
- : 'Path traversal attempt';
228
- SecurityUtils.logSecurityEvent(message, 'warning', {
229
- inputPath: filePath,
230
- resolvedPath: finalPath,
231
- basePath: base,
232
- relativePath: relativePath
233
- });
234
- return null;
235
- }
300
+ const relativePath = path.relative(base, finalPath);
301
+ if (relativePath.startsWith('..')) {
302
+ const message = useI18n
303
+ ? i18n.t('security.pathTraversalAttempt')
304
+ : 'Path traversal attempt';
305
+ SecurityUtils.logSecurityEvent(message, 'warning', {
306
+ inputPath: filePath,
307
+ resolvedPath: finalPath,
308
+ basePath: base,
309
+ relativePath: relativePath,
310
+ source
311
+ });
312
+ return null;
313
+ }
236
314
 
237
315
  // Allow absolute paths that resolve within the project structure
238
316
  // The isSafePath check above already filtered out dangerous absolute paths
239
317
 
240
318
  if (verbose) {
241
- const successMsg = useI18n
242
- ? i18n.t('security.pathValidated')
243
- : 'Path validated';
244
- SecurityUtils.logSecurityEvent(successMsg, 'info', {
245
- inputPath: filePath,
246
- resolvedPath: finalPath
247
- });
248
- }
249
- return finalPath;
319
+ const successMsg = useI18n
320
+ ? i18n.t('security.pathValidated')
321
+ : 'Path validated';
322
+ SecurityUtils.logSecurityEvent(successMsg, 'info', {
323
+ inputPath: filePath,
324
+ resolvedPath: finalPath,
325
+ source
326
+ });
327
+ }
328
+ return finalPath;
250
329
  } catch (error) {
251
330
  const message = useI18n
252
331
  ? i18n.t('security.pathValidationError')
253
332
  : 'Path validation error';
254
- SecurityUtils.logSecurityEvent(message, 'error', {
255
- inputPath: filePath,
256
- error: error.message
257
- });
258
- return null;
259
- }
260
- }
333
+ SecurityUtils.logSecurityEvent(message, 'error', {
334
+ inputPath: filePath,
335
+ error: error.message,
336
+ source: isInternalPath(filePath) ? 'internal' : 'user'
337
+ });
338
+ return null;
339
+ }
340
+ }
261
341
 
262
342
  static safeExistsSync(filePath, basePath, timeoutMs = 3000) {
263
343
  return this.withTimeoutSync(() => {
@@ -489,21 +569,23 @@ static _logging = false;
489
569
  const resolvedPath = path.resolve(joinedPath);
490
570
 
491
571
  // Ensure the final path is within the base directory
492
- if (!resolvedPath.startsWith(resolvedBase)) {
493
- SecurityUtils.logSecurityEvent('Path traversal attempt detected in safeJoin', 'error', {
494
- basePath,
495
- paths,
496
- resolvedPath
497
- });
498
- return false;
499
- }
572
+ if (!resolvedPath.startsWith(resolvedBase)) {
573
+ SecurityUtils.logSecurityEvent('Path traversal attempt detected in safeJoin', 'error', {
574
+ basePath,
575
+ paths,
576
+ resolvedPath,
577
+ source: 'internal'
578
+ });
579
+ return false;
580
+ }
500
581
  return resolvedPath;
501
582
  } catch (error) {
502
- SecurityUtils.logSecurityEvent('Error in safeJoin', 'error', {
503
- basePath,
504
- paths,
505
- error: error.message
506
- });
583
+ SecurityUtils.logSecurityEvent('Error in safeJoin', 'error', {
584
+ basePath,
585
+ paths,
586
+ error: error.message,
587
+ source: 'internal'
588
+ });
507
589
  return false;
508
590
  }
509
591
  }
@@ -551,11 +633,12 @@ static _logging = false;
551
633
  ];
552
634
 
553
635
  // Allow absolute paths that are within the project structure
554
- if (filePath.startsWith('/') || filePath.startsWith('\\')) {
555
- // Allow absolute paths but check for dangerous patterns
556
- const hasDangerousPatterns = dangerousPatterns.slice(1).some(pattern => pattern.test(filePath));
557
- return !hasDangerousPatterns;
558
- }
636
+ if (filePath.startsWith('/') || filePath.startsWith('\\')) {
637
+ // Treat raw Unix-style absolute input as dangerous by default in this helper.
638
+ // `validatePath` can still permit absolute paths if they resolve within basePath.
639
+ const hasDangerousPatterns = dangerousPatterns.slice(1).some(pattern => pattern.test(filePath));
640
+ return !hasDangerousPatterns;
641
+ }
559
642
 
560
643
  return !dangerousPatterns.some(pattern => pattern.test(filePath));
561
644
  }