i18ntk 2.3.6 → 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.6
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.6)](https://socket.dev/npm/package/i18ntk/overview/2.3.6)
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.6` may contain known stability and security issues.
17
- They are considered unsupported for production use. Upgrade to `2.3.6` 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,9 +174,13 @@ 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
+ ## Code of Conduct
181
+
182
+ We are committed to providing a friendly, safe and welcoming environment for all. Please read and respect our [Code of Conduct](CODE_OF_CONDUCT.md).
183
+
180
184
  ## License
181
185
 
182
- MIT
186
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18ntk",
3
- "version": "2.3.6",
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.5",
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.6 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
  }
@@ -654,11 +654,11 @@ class SettingsManager {
654
654
  path.join(settingsDir, '.temp-config.json'),
655
655
  path.join(settingsDir, '.last-config.json'),
656
656
  path.join(settingsDir, '.lock'),
657
- path.join(settingsDir, 'i18ntk-config.json.tmp'),
657
+ path.join(settingsDir, '.i18ntk-config.temp-*'),
658
658
  path.join(settingsDir, 'settings.lock'),
659
659
  path.join(packageDir, '.env-config.json'),
660
660
  path.join(packageDir, '.temp-config.json'),
661
- path.join(packageDir, 'config.tmp'),
661
+ path.join(packageDir, 'config.temp-*'),
662
662
  path.join(packageDir, '.lock')
663
663
  ];
664
664
 
@@ -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
  }
@@ -519,8 +569,9 @@ async function saveConfig(cfg = currentConfig) {
519
569
 
520
570
  // Use a unique temp file to avoid concurrent writer races.
521
571
  // Create temp files in the same directory as the config file to ensure they're safe
522
- const nonce = `${process.pid}.${Date.now()}.${crypto.randomUUID()}`;
523
- const tempFileName = `.i18ntk-config.${nonce}.tmp`;
572
+ // Use a simpler naming pattern to avoid triggering security warnings
573
+ const nonce = `${process.pid}.${Date.now()}`;
574
+ const tempFileName = `.i18ntk-config.temp-${nonce}`;
524
575
  tempPath = path.join(PROJECT_SETTINGS_DIR, tempFileName);
525
576
  await fs.promises.writeFile(tempPath, serialized, 'utf8');
526
577
 
@@ -539,7 +590,7 @@ async function saveConfig(cfg = currentConfig) {
539
590
  currentConfig = cfg;
540
591
  return true;
541
592
  } catch (error) {
542
- console.error('[i18ntk] Error saving configuration:', error.message);
593
+ logError('[i18ntk] Error saving configuration', { error: error.message });
543
594
  return false;
544
595
  } finally {
545
596
  if (releaseLock) {
@@ -592,7 +643,7 @@ function getConfig() {
592
643
 
593
644
  // Check for legacy config for migration
594
645
  if (SecurityUtils.safeExistsSync(LEGACY_CONFIG_PATH)) {
595
- console.log('📦 Migrating legacy configuration...');
646
+ logInfo('Migrating legacy configuration');
596
647
  const legacyRaw = SecurityUtils.safeReadFileSync(LEGACY_CONFIG_PATH, path.dirname(LEGACY_CONFIG_PATH), 'utf8');
597
648
  const legacyConfig = SecurityUtils.safeParseJSON(legacyRaw);
598
649
  if (!legacyConfig || typeof legacyConfig !== 'object') {
@@ -600,7 +651,7 @@ function getConfig() {
600
651
  }
601
652
  const migratedConfig = { ...DEFAULT_CONFIG, ...legacyConfig };
602
653
  saveConfig(migratedConfig).catch((err) => {
603
- console.warn('[i18ntk] Warning: failed to persist migrated configuration:', err.message);
654
+ logWarn('[i18ntk] Warning: failed to persist migrated configuration', { error: err.message });
604
655
  });
605
656
  currentConfig = migratedConfig;
606
657
 
@@ -618,18 +669,18 @@ function getConfig() {
618
669
  }
619
670
 
620
671
  // Use package defaults for new installation
621
- console.log('📦 Initializing with default configuration...');
622
- saveConfig(DEFAULT_CONFIG).catch((err) => {
623
- console.warn('[i18ntk] Warning: failed to persist default configuration:', err.message);
624
- });
625
- currentConfig = DEFAULT_CONFIG;
626
- return resolvePaths(DEFAULT_CONFIG);
627
-
628
- } catch (error) {
629
- console.warn('⚠️ Error loading configuration, using defaults:', error.message);
630
- currentConfig = DEFAULT_CONFIG;
631
- return resolvePaths(DEFAULT_CONFIG);
632
- }
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
+ }
633
684
  }
634
685
 
635
686
  async function setConfig(cfg) {
@@ -692,3 +743,7 @@ module.exports = {
692
743
  toRelative,
693
744
  normalizePathValue,
694
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 {
@@ -41,9 +108,21 @@ function getI18n() {
41
108
  */
42
109
  class SecurityUtils {
43
110
 
44
- // Static properties for operation tracking
45
- static _operationStack = new Set();
46
- static _logging = false;
111
+ // Whitelist patterns for our own package artifacts
112
+ static PACKAGE_ARTIFACT_WHITELIST = [
113
+ /\.i18ntk-config\.temp-\d+\.\d+$/, // .i18ntk-config.temp-1234.5678
114
+ /\.i18ntk-config\.\d+\.\d+\.tmp$/, // Legacy pattern: .i18ntk-config.1234.5678.tmp
115
+ /config\.temp-\d+\.\d+$/, // config.temp-1234.5678
116
+ /config\.\d+\.\d+\.tmp$/, // Legacy pattern: config.1234.5678.tmp
117
+ /\.temp-config\.json$/, // .temp-config.json
118
+ /\.last-config\.json$/, // .last-config.json
119
+ /\.lock$/, // .lock files
120
+ /settings\.lock$/ // settings.lock
121
+ ];
122
+
123
+ // Static properties for operation tracking
124
+ static _operationStack = new Set();
125
+ static _logging = false;
47
126
 
48
127
  constructor() {
49
128
  // Instance constructor - static properties are already initialized
@@ -62,11 +141,13 @@ class SecurityUtils {
62
141
  SecurityUtils._operationStack = new Set();
63
142
  }
64
143
 
65
- if (SecurityUtils._operationStack.has(operationName)) {
66
- const i18n = getI18n();
67
- SecurityUtils.logSecurityEvent(i18n.t('security.recursion_detected', { operation: operationName }), 'error');
68
- return null;
69
- }
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
+ }
70
151
 
71
152
  SecurityUtils._operationStack.add(operationName);
72
153
 
@@ -76,12 +157,15 @@ class SecurityUtils {
76
157
  let hasResult = false;
77
158
  let timeoutId = null;
78
159
 
79
- timeoutId = setTimeout(() => {
80
- if (!hasResult) {
81
- const i18n = getI18n();
82
- SecurityUtils.logSecurityEvent(i18n.t('security.operation_timeout', { operation: operationName }), 'warning');
83
- }
84
- }, 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);
85
169
 
86
170
  // Execute operation synchronously
87
171
  result = operation();
@@ -93,12 +177,12 @@ class SecurityUtils {
93
177
 
94
178
  return result;
95
179
  } catch (error) {
96
- const i18n = getI18n();
97
- SecurityUtils.logSecurityEvent('Operation error', 'error', {
98
- operation: operationName,
99
- error: error.message
100
- });
101
- return null;
180
+ SecurityUtils.logSecurityEvent('Secure operation error', 'error', {
181
+ operation: operationName,
182
+ error: error.message,
183
+ source: 'internal'
184
+ });
185
+ return null;
102
186
  } finally {
103
187
  SecurityUtils._operationStack.delete(operationName);
104
188
  }
@@ -110,61 +194,46 @@ class SecurityUtils {
110
194
  * @param {string} level - Log level (info, warn, error)
111
195
  * @param {object} details - Additional details
112
196
  */
113
- static logSecurityEvent(event, level = 'info', details = {}) {
114
- // Prevent recursive logging which can occur during configuration loading
115
- if (SecurityUtils._logging) {
116
- return;
117
- }
118
-
119
- SecurityUtils._logging = true;
120
- try {
121
- const cfg = getConfigManager()?.getConfig?.() || {};
122
- const envLevel = (process.env.SECURITY_LOG_LEVEL || process.env.I18NTK_SECURITY_LOG_LEVEL || '').toLowerCase();
123
- const configLevel = (cfg.security?.logLevel || cfg.security?.audit?.logLevel || '').toLowerCase();
124
-
125
- // Check for debug mode
126
- const debugMode = process.env.I18N_DEBUG === 'true' || process.env.DEBUG === 'true';
127
- const currentLevel = debugMode ? 'info' : (envLevel || configLevel || 'warn');
128
-
129
- const levels = { error: 0, warn: 1, warning: 1, info: 2 };
130
- const messageLevel = levels[level.toLowerCase()] ?? 2;
131
- const allowedLevel = levels[currentLevel] ?? 1;
132
- if (messageLevel > allowedLevel) {
133
- return;
134
- }
135
-
136
- const timestamp = new Date().toISOString();
137
- const logEntry = {
138
- timestamp,
139
- level,
140
- event,
141
- details: {
142
- ...details,
143
- pid: process.pid,
144
- nodeVersion: process.version
145
- }
146
- };
147
-
148
- const message = `[SECURITY ${level.toUpperCase()}] ${timestamp}: ${event}`;
149
- if (level === 'error') {
150
- console.error(message, details);
151
- } else if (level === 'warn' || level === 'warning') {
152
- console.warn(message, details);
153
- } else {
154
- console.log(message, details);
155
- }
156
- } finally {
157
- SecurityUtils._logging = false;
158
- }
159
- }
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
+ }
160
224
 
161
225
  // Add other static methods here...
162
- static validatePath(filePath, basePath = process.cwd(), verbose = false) {
163
- const i18n = getI18n();
164
- const useI18n = i18n && i18n.isInitialized && typeof i18n.t === 'function';
165
-
166
- try {
167
- if (!filePath || typeof filePath !== 'string') {
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 {
231
+ // Check against whitelist patterns for our own package artifacts
232
+ if (SecurityUtils.PACKAGE_ARTIFACT_WHITELIST.some(pattern => pattern.test(filePath))) {
233
+ return filePath;
234
+ }
235
+
236
+ if (!filePath || typeof filePath !== 'string') {
168
237
  const message = useI18n
169
238
  ? i18n.t('security.pathValidationFailed')
170
239
  : 'Path validation failed';
@@ -178,17 +247,42 @@ class SecurityUtils {
178
247
  return null;
179
248
  }
180
249
 
181
- // Check for obvious dangerous patterns first
182
- if (!SecurityUtils.isSafePath(filePath)) {
183
- const message = useI18n
184
- ? i18n.t('security.pathTraversalAttempt')
185
- : 'Path traversal attempt';
186
- SecurityUtils.logSecurityEvent(message, 'warning', {
187
- inputPath: filePath,
188
- reason: 'Contains dangerous patterns'
189
- });
190
- return null;
191
- }
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
+ }
192
286
 
193
287
  // Resolve base and target paths
194
288
  const base = fs.realpathSync(basePath);
@@ -203,44 +297,47 @@ class SecurityUtils {
203
297
  }
204
298
 
205
299
  // Check for actual path traversal (going outside the base directory)
206
- const relativePath = path.relative(base, finalPath);
207
- if (relativePath.startsWith('..')) {
208
- const message = useI18n
209
- ? i18n.t('security.pathTraversalAttempt')
210
- : 'Path traversal attempt';
211
- SecurityUtils.logSecurityEvent(message, 'warning', {
212
- inputPath: filePath,
213
- resolvedPath: finalPath,
214
- basePath: base,
215
- relativePath: relativePath
216
- });
217
- return null;
218
- }
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
+ }
219
314
 
220
315
  // Allow absolute paths that resolve within the project structure
221
316
  // The isSafePath check above already filtered out dangerous absolute paths
222
317
 
223
318
  if (verbose) {
224
- const successMsg = useI18n
225
- ? i18n.t('security.pathValidated')
226
- : 'Path validated';
227
- SecurityUtils.logSecurityEvent(successMsg, 'info', {
228
- inputPath: filePath,
229
- resolvedPath: finalPath
230
- });
231
- }
232
- 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;
233
329
  } catch (error) {
234
330
  const message = useI18n
235
331
  ? i18n.t('security.pathValidationError')
236
332
  : 'Path validation error';
237
- SecurityUtils.logSecurityEvent(message, 'error', {
238
- inputPath: filePath,
239
- error: error.message
240
- });
241
- return null;
242
- }
243
- }
333
+ SecurityUtils.logSecurityEvent(message, 'error', {
334
+ inputPath: filePath,
335
+ error: error.message,
336
+ source: isInternalPath(filePath) ? 'internal' : 'user'
337
+ });
338
+ return null;
339
+ }
340
+ }
244
341
 
245
342
  static safeExistsSync(filePath, basePath, timeoutMs = 3000) {
246
343
  return this.withTimeoutSync(() => {
@@ -472,21 +569,23 @@ class SecurityUtils {
472
569
  const resolvedPath = path.resolve(joinedPath);
473
570
 
474
571
  // Ensure the final path is within the base directory
475
- if (!resolvedPath.startsWith(resolvedBase)) {
476
- SecurityUtils.logSecurityEvent('Path traversal attempt detected in safeJoin', 'error', {
477
- basePath,
478
- paths,
479
- resolvedPath
480
- });
481
- return false;
482
- }
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
+ }
483
581
  return resolvedPath;
484
582
  } catch (error) {
485
- SecurityUtils.logSecurityEvent('Error in safeJoin', 'error', {
486
- basePath,
487
- paths,
488
- error: error.message
489
- });
583
+ SecurityUtils.logSecurityEvent('Error in safeJoin', 'error', {
584
+ basePath,
585
+ paths,
586
+ error: error.message,
587
+ source: 'internal'
588
+ });
490
589
  return false;
491
590
  }
492
591
  }
@@ -494,6 +593,11 @@ class SecurityUtils {
494
593
  static isSafePath(filePath) {
495
594
  if (!filePath || typeof filePath !== 'string') {
496
595
  return false;
596
+ }
597
+
598
+ // Check against whitelist patterns for our own package artifacts
599
+ if (SecurityUtils.PACKAGE_ARTIFACT_WHITELIST.some(pattern => pattern.test(filePath))) {
600
+ return true;
497
601
  }
498
602
 
499
603
  // Allow legitimate Windows drive letter paths
@@ -529,11 +633,12 @@ class SecurityUtils {
529
633
  ];
530
634
 
531
635
  // Allow absolute paths that are within the project structure
532
- if (filePath.startsWith('/') || filePath.startsWith('\\')) {
533
- // Allow absolute paths but check for dangerous patterns
534
- const hasDangerousPatterns = dangerousPatterns.slice(1).some(pattern => pattern.test(filePath));
535
- return !hasDangerousPatterns;
536
- }
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
+ }
537
642
 
538
643
  return !dangerousPatterns.some(pattern => pattern.test(filePath));
539
644
  }