i18ntk 2.5.1 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,16 +8,20 @@ const { envManager } = require('./env-manager');
8
8
 
9
9
  // Determine package directory and user project root
10
10
  const packageDir = path.resolve(__dirname, '..');
11
- const userProjectRoot = path.resolve(process.cwd());
12
11
 
13
- // Always use current working directory for settings to support test environments
14
- // This ensures config works correctly when tests change the working directory
15
- const PROJECT_CONFIG_PATH = SecurityUtils.safeJoin(userProjectRoot, '.i18ntk-config');
16
- if (!PROJECT_CONFIG_PATH) {
17
- throw new Error('Invalid project config path - potential path traversal attempt');
12
+ function getUserProjectRoot() {
13
+ return path.resolve(process.cwd());
18
14
  }
19
- const PROJECT_SETTINGS_DIR = path.dirname(PROJECT_CONFIG_PATH);
20
- const CONFIG_LOCK_PATH = `${PROJECT_CONFIG_PATH}.lock`;
15
+
16
+ function getProjectConfigPath() {
17
+ const root = getUserProjectRoot();
18
+ const configPath = SecurityUtils.safeJoin(root, '.i18ntk-config');
19
+ if (!configPath) {
20
+ throw new Error('Invalid project config path - potential path traversal attempt');
21
+ }
22
+ return configPath;
23
+ }
24
+
21
25
  const CONFIG_LOCK_TIMEOUT_MS = 5000;
22
26
  const CONFIG_LOCK_STALE_MS = 15000;
23
27
  const CONFIG_LOCK_RETRY_MS = 50;
@@ -72,7 +76,7 @@ function notifyConfigFallback(error) {
72
76
  }
73
77
 
74
78
  // Setup tracking file
75
- const SETUP_COMPLETED_FILE = path.join(PROJECT_SETTINGS_DIR, 'setup.json');
79
+ const getSetupCompletedFile = () => path.join(path.dirname(getProjectConfigPath()), 'setup.json');
76
80
 
77
81
  // Legacy home directory config (for migration only)
78
82
  const LEGACY_CONFIG_DIR = path.join(os.homedir(), '.i18ntk');
@@ -82,12 +86,9 @@ const LEGACY_CONFIG_PATH = path.join(LEGACY_CONFIG_DIR, 'i18ntk-config.json');
82
86
  const PACKAGE_SETTINGS_DIR = path.join(packageDir, 'settings');
83
87
  const PACKAGE_CONFIG_PATH = path.join(PACKAGE_SETTINGS_DIR, 'i18ntk-config.json');
84
88
 
85
- // Keep projectRoot for path resolution functions
86
- const projectRoot = userProjectRoot;
87
-
88
89
  // Back-compat: expose CONFIG_DIR/CONFIG_PATH but point to project settings
89
- const CONFIG_DIR = PROJECT_SETTINGS_DIR;
90
- const CONFIG_PATH = PROJECT_CONFIG_PATH;
90
+ const getConfigDir = () => path.dirname(getProjectConfigPath());
91
+ const getConfigPath = getProjectConfigPath;
91
92
 
92
93
  // Default configuration values - comprehensive configuration
93
94
  const DEFAULT_CONFIG = {
@@ -314,12 +315,13 @@ function sleep(ms) {
314
315
  }
315
316
 
316
317
  async function acquireConfigLock(timeoutMs = CONFIG_LOCK_TIMEOUT_MS) {
318
+ const lockPath = `${getProjectConfigPath()}.lock`;
317
319
  const start = Date.now();
318
320
  let lockHandle = null;
319
321
 
320
322
  while (!lockHandle) {
321
323
  try {
322
- lockHandle = await fs.promises.open(CONFIG_LOCK_PATH, 'wx');
324
+ lockHandle = await fs.promises.open(lockPath, 'wx');
323
325
  await lockHandle.writeFile(String(process.pid), 'utf8');
324
326
  break;
325
327
  } catch (error) {
@@ -329,9 +331,9 @@ async function acquireConfigLock(timeoutMs = CONFIG_LOCK_TIMEOUT_MS) {
329
331
 
330
332
  // Recover from stale lock files left by crashed processes.
331
333
  try {
332
- const stats = await fs.promises.stat(CONFIG_LOCK_PATH);
334
+ const stats = await fs.promises.stat(lockPath);
333
335
  if (Date.now() - stats.mtimeMs > CONFIG_LOCK_STALE_MS) {
334
- await fs.promises.unlink(CONFIG_LOCK_PATH);
336
+ await fs.promises.unlink(lockPath);
335
337
  continue;
336
338
  }
337
339
  } catch (_) {
@@ -355,7 +357,7 @@ async function acquireConfigLock(timeoutMs = CONFIG_LOCK_TIMEOUT_MS) {
355
357
  // Best-effort close only.
356
358
  }
357
359
  try {
358
- await fs.promises.unlink(CONFIG_LOCK_PATH);
360
+ await fs.promises.unlink(lockPath);
359
361
  } catch (_) {
360
362
  // Best-effort cleanup only.
361
363
  }
@@ -382,9 +384,12 @@ function resetRecursionGuard() {
382
384
  }
383
385
 
384
386
  function ensureProjectSettingsDir() {
385
- // Only create settings directory within the package, not in user projects
386
- // Since PROJECT_SETTINGS_DIR is now set to package internals, no action needed
387
- // if directory doesn't exist, we'll use package defaults
387
+ const settingsDir = path.dirname(getProjectConfigPath());
388
+ try {
389
+ fs.mkdirSync(settingsDir, { recursive: true });
390
+ } catch (_) {
391
+ // Directory may already exist or be unwritable
392
+ }
388
393
  }
389
394
 
390
395
  function normalizePathValue(keyPath, value) {
@@ -457,7 +462,8 @@ function tryReadJson(filePath) {
457
462
 
458
463
  async function migrateLegacyIfNeeded(baseCfg) {
459
464
  // If project config does not exist but legacy exists, migrate once
460
- if (!SecurityUtils.safeExistsSync(PROJECT_CONFIG_PATH) && SecurityUtils.safeExistsSync(LEGACY_CONFIG_PATH)) {
465
+ const configPath = getProjectConfigPath();
466
+ if (!SecurityUtils.safeExistsSync(configPath) && SecurityUtils.safeExistsSync(LEGACY_CONFIG_PATH)) {
461
467
  const legacy = tryReadJson(LEGACY_CONFIG_PATH);
462
468
  if (legacy && typeof legacy === 'object') {
463
469
  const merged = deepMerge(clone(baseCfg), legacy);
@@ -465,9 +471,9 @@ async function migrateLegacyIfNeeded(baseCfg) {
465
471
  try { merged.migrationComplete = true; } catch (_) {}
466
472
  ensureProjectSettingsDir();
467
473
  try {
468
- await fs.promises.writeFile(PROJECT_CONFIG_PATH, JSON.stringify(merged, null, 2), 'utf8');
474
+ await fs.promises.writeFile(configPath, JSON.stringify(merged, null, 2), 'utf8');
469
475
  // Best-effort removal of legacy file to prevent future use
470
- try { fs.unlinkSync(LEGACY_CONFIG_PATH); } catch (_) {}
476
+ try { SecurityUtils.safeUnlinkSync(LEGACY_CONFIG_PATH, path.dirname(LEGACY_CONFIG_PATH)); } catch (_) {}
471
477
  // Deprecation notice
472
478
  logWarn('[i18ntk] Deprecated config location detected (~/.i18ntk). Configuration was migrated to project .i18ntk-config.');
473
479
  return merged;
@@ -503,8 +509,9 @@ function loadConfig() {
503
509
 
504
510
  try {
505
511
  let cfg = clone(DEFAULT_CONFIG);
512
+ const configPath = getProjectConfigPath();
506
513
  // 1) Project config (primary)
507
- const projectCfg = tryReadJson(PROJECT_CONFIG_PATH);
514
+ const projectCfg = tryReadJson(configPath);
508
515
  if (projectCfg) {
509
516
  cfg = deepMerge(clone(DEFAULT_CONFIG), projectCfg);
510
517
  } else {
@@ -557,8 +564,10 @@ async function saveConfig(cfg = currentConfig) {
557
564
  let tempPath = null;
558
565
  let releaseLock = null;
559
566
  try {
567
+ const settingsDir = path.dirname(getProjectConfigPath());
568
+ const configFilePath = getProjectConfigPath();
560
569
  // Ensure settings directory exists before any lock/file operations.
561
- await fs.promises.mkdir(PROJECT_SETTINGS_DIR, { recursive: true });
570
+ await fs.promises.mkdir(settingsDir, { recursive: true });
562
571
 
563
572
  releaseLock = await acquireConfigLock();
564
573
 
@@ -572,16 +581,16 @@ async function saveConfig(cfg = currentConfig) {
572
581
  // Use a simpler naming pattern to avoid triggering security warnings
573
582
  const nonce = `${process.pid}.${Date.now()}`;
574
583
  const tempFileName = `.i18ntk-config.temp-${nonce}`;
575
- tempPath = path.join(PROJECT_SETTINGS_DIR, tempFileName);
584
+ tempPath = path.join(settingsDir, tempFileName);
576
585
  await fs.promises.writeFile(tempPath, serialized, 'utf8');
577
586
 
578
587
  try {
579
- await fs.promises.rename(tempPath, PROJECT_CONFIG_PATH);
588
+ await fs.promises.rename(tempPath, configFilePath);
580
589
  } catch (renameError) {
581
590
  // If destination dir disappeared between checks, recreate and retry once.
582
591
  if (renameError && renameError.code === 'ENOENT') {
583
- await fs.promises.mkdir(PROJECT_SETTINGS_DIR, { recursive: true });
584
- await fs.promises.rename(tempPath, PROJECT_CONFIG_PATH);
592
+ await fs.promises.mkdir(settingsDir, { recursive: true });
593
+ await fs.promises.rename(tempPath, configFilePath);
585
594
  } else {
586
595
  throw renameError;
587
596
  }
@@ -598,9 +607,7 @@ async function saveConfig(cfg = currentConfig) {
598
607
  }
599
608
  if (tempPath) {
600
609
  try {
601
- if (SecurityUtils.safeExistsSync(tempPath, path.dirname(tempPath))) {
602
- fs.unlinkSync(tempPath);
603
- }
610
+ SecurityUtils.safeUnlinkSync(tempPath, path.dirname(tempPath));
604
611
  } catch (_) {
605
612
  // Best-effort temp cleanup only.
606
613
  }
@@ -622,17 +629,19 @@ function getConfig() {
622
629
  }
623
630
 
624
631
  try {
632
+ const configPath = getProjectConfigPath();
633
+ const settingsDir = path.dirname(configPath);
625
634
  // Ensure settings directory exists
626
- if (!SecurityUtils.safeExistsSync(PROJECT_SETTINGS_DIR)) {
627
- fs.mkdirSync(PROJECT_SETTINGS_DIR, { recursive: true });
635
+ if (!SecurityUtils.safeExistsSync(settingsDir)) {
636
+ fs.mkdirSync(settingsDir, { recursive: true });
628
637
  }
629
638
 
630
639
  // Setup is now handled automatically by the unified config system
631
640
  // No need to check here - handled by getUnifiedConfig
632
641
 
633
642
  // Check if config file exists
634
- if (SecurityUtils.safeExistsSync(PROJECT_CONFIG_PATH)) {
635
- const rawConfig = SecurityUtils.safeReadFileSync(PROJECT_CONFIG_PATH, path.dirname(PROJECT_CONFIG_PATH), 'utf8');
643
+ if (SecurityUtils.safeExistsSync(configPath)) {
644
+ const rawConfig = SecurityUtils.safeReadFileSync(configPath, settingsDir, 'utf8');
636
645
  const config = SecurityUtils.safeParseJSON(rawConfig);
637
646
  if (config && typeof config === 'object') {
638
647
  currentConfig = config;
@@ -655,15 +664,15 @@ function getConfig() {
655
664
  });
656
665
  currentConfig = migratedConfig;
657
666
 
658
- // Clean up legacy config
659
- try {
660
- fs.unlinkSync(LEGACY_CONFIG_PATH);
661
- if (fs.readdirSync(LEGACY_CONFIG_DIR).length === 0) {
662
- fs.rmdirSync(LEGACY_CONFIG_DIR);
663
- }
664
- } catch (cleanupError) {
665
- // Ignore cleanup errors
666
- }
667
+ // Clean up legacy config
668
+ try {
669
+ SecurityUtils.safeUnlinkSync(LEGACY_CONFIG_PATH, path.dirname(LEGACY_CONFIG_PATH));
670
+ if (SecurityUtils.safeReaddirSync(LEGACY_CONFIG_DIR, path.dirname(LEGACY_CONFIG_DIR)).length === 0) {
671
+ SecurityUtils.safeRmdirSync(LEGACY_CONFIG_DIR, path.dirname(LEGACY_CONFIG_DIR));
672
+ }
673
+ } catch (cleanupError) {
674
+ // Ignore cleanup errors
675
+ }
667
676
 
668
677
  return resolvePaths(migratedConfig);
669
678
  }
@@ -706,7 +715,7 @@ function resolvePaths(cfg) {
706
715
  if (!cfg) {
707
716
  cfg = clone(DEFAULT_CONFIG);
708
717
  }
709
- const root = path.resolve(projectRoot, cfg.projectRoot || '.');
718
+ const root = path.resolve(getUserProjectRoot(), cfg.projectRoot || '.');
710
719
  const resolved = clone(cfg);
711
720
  resolved.projectRoot = root;
712
721
  ['sourceDir', 'i18nDir', 'outputDir'].forEach(key => {
@@ -723,7 +732,7 @@ function resolvePaths(cfg) {
723
732
 
724
733
  function toRelative(absPath) {
725
734
  if (!absPath) return absPath;
726
- const rel = path.relative(projectRoot, absPath);
735
+ const rel = path.relative(getUserProjectRoot(), absPath);
727
736
  const normalized = rel ? `./${rel.replace(/\\/g, '/')}` : '.';
728
737
  return normalized;
729
738
  }
@@ -731,7 +740,7 @@ function toRelative(absPath) {
731
740
 
732
741
 
733
742
  module.exports = {
734
- CONFIG_PATH,
743
+ get CONFIG_PATH() { return getProjectConfigPath(); },
735
744
  DEFAULT_CONFIG,
736
745
  loadConfig,
737
746
  saveConfig,
@@ -742,6 +751,7 @@ module.exports = {
742
751
  resolvePaths,
743
752
  toRelative,
744
753
  normalizePathValue,
754
+ migrateLegacyIfNeeded,
745
755
  }
746
756
 
747
757
 
package/utils/config.js CHANGED
@@ -85,16 +85,25 @@ function saveConfig(config, cwd = settingsManager.configDir) {
85
85
  const dir = path.dirname(configPath);
86
86
 
87
87
  // Ensure directory exists
88
- if (!SecurityUtils.safeExistsSync(dir)) {
89
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
88
+ if (!SecurityUtils.safeExistsSync(dir, settingsManager.configDir)) {
89
+ SecurityUtils.safeMkdirSync(dir, settingsManager.configDir, { recursive: true, mode: 0o700 });
90
90
  }
91
91
 
92
92
  // Write file with secure permissions (read/write for owner only)
93
- SecurityUtils.safeWriteFileSync(
93
+ const written = SecurityUtils.safeWriteFileSync(
94
94
  configPath,
95
95
  JSON.stringify(config, null, 2),
96
- { mode: 0o600, encoding: 'utf8' }
96
+ dir,
97
+ 'utf8'
97
98
  );
99
+ if (!written) {
100
+ throw new Error('Failed to write configuration file');
101
+ }
102
+ try {
103
+ fs.chmodSync(configPath, 0o600);
104
+ } catch (_) {
105
+ // Best-effort: permissions are a hardening measure, not required
106
+ }
98
107
 
99
108
  return true;
100
109
  } catch (error) {
@@ -258,7 +258,9 @@ class EnvironmentManager {
258
258
 
259
259
  getBoolean(name) {
260
260
  const value = this.get(name);
261
- return value === true || value === 'true' || value === '1';
261
+ if (typeof value === 'boolean') return value;
262
+ if (value === 'true' || value === '1' || value === 'yes') return true;
263
+ return false;
262
264
  }
263
265
 
264
266
  /**
@@ -11,26 +11,53 @@ function getSecurityUtils() {
11
11
  try {
12
12
  securityUtils = require('./security');
13
13
  } catch (error) {
14
- // Fallback: use basic fs operations if SecurityUtils is not available
14
+ // Fallback: use basic fs operations with path containment
15
+ const path = require('path');
16
+ const fallbackBase = path.resolve(__dirname, '..');
15
17
  return {
16
- safeExistsSync: (targetPath) => {
18
+ safeExistsSync: (targetPath, basePath) => {
17
19
  try {
18
- require('fs').accessSync(targetPath);
20
+ const resolved = path.resolve(basePath || fallbackBase, targetPath);
21
+ if (!resolved.startsWith(path.resolve(basePath || fallbackBase))) {
22
+ return false;
23
+ }
24
+ require('fs').accessSync(resolved);
19
25
  return true;
20
26
  } catch {
21
27
  return false;
22
28
  }
23
29
  },
24
- safeWriteFileSync: (targetPath, data, _basePath, encoding = 'utf8') => {
30
+ safeWriteFileSync: (targetPath, data, basePath, encoding = 'utf8') => {
31
+ try {
32
+ const resolved = path.resolve(basePath || fallbackBase, targetPath);
33
+ if (!resolved.startsWith(path.resolve(basePath || fallbackBase))) {
34
+ return null;
35
+ }
36
+ require('fs').mkdirSync(path.dirname(resolved), { recursive: true });
37
+ require('fs').writeFileSync(resolved, data, encoding);
38
+ return true;
39
+ } catch {
40
+ return null;
41
+ }
42
+ },
43
+ safeReadFileSync: (targetPath, basePath, encoding = 'utf8') => {
25
44
  try {
26
- return require('fs').writeFileSync(targetPath, data, encoding);
45
+ const resolved = path.resolve(basePath || fallbackBase, targetPath);
46
+ if (!resolved.startsWith(path.resolve(basePath || fallbackBase))) {
47
+ return null;
48
+ }
49
+ return require('fs').readFileSync(resolved, encoding);
27
50
  } catch {
28
51
  return null;
29
52
  }
30
53
  },
31
- safeReadFileSync: (targetPath, _basePath, encoding = 'utf8') => {
54
+ safeStatSync: (targetPath, basePath) => {
32
55
  try {
33
- return require('fs').readFileSync(targetPath, encoding);
56
+ const resolved = path.resolve(basePath || fallbackBase, targetPath);
57
+ if (!resolved.startsWith(path.resolve(basePath || fallbackBase))) {
58
+ return null;
59
+ }
60
+ return require('fs').statSync(resolved);
34
61
  } catch {
35
62
  return null;
36
63
  }
@@ -68,7 +95,8 @@ function getValidationBase(targetPath) {
68
95
  let current = path.resolve(path.dirname(targetPath));
69
96
  while (true) {
70
97
  try {
71
- if (fs.statSync(current).isDirectory()) {
98
+ const stat = getSecurityUtils().safeStatSync(current, current);
99
+ if (stat && stat.isDirectory()) {
72
100
  return current;
73
101
  }
74
102
  } catch {
@@ -175,8 +203,6 @@ function findLocaleFilesAllDirs(lang, preferredDir) {
175
203
  try {
176
204
  const stats = SecurityUtils.safeStatSync(candidate, getValidationBase(candidate));
177
205
  if (stats && stats.isFile() && stats.size > 0) {
178
- // Validate file is readable and parseable
179
- fs.accessSync(candidate, fs.constants.R_OK);
180
206
  // Quick JSON validation
181
207
  const content = safeReadFile(candidate, 'utf8');
182
208
  if (content) {
@@ -413,10 +439,12 @@ function getAvailableLanguages() {
413
439
  try {
414
440
  const SecurityUtils = getSecurityUtils();
415
441
  if (!SecurityUtils.safeExistsSync(d, getValidationBase(d))) continue;
416
- for (const f of fs.readdirSync(d)) {
417
- if (f.endsWith('.json')) langs.add(path.basename(f, '.json'));
442
+ const files = SecurityUtils.safeReaddirSync(d, getValidationBase(d), { withFileTypes: true });
443
+ if (!files) continue;
444
+ for (const f of files) {
445
+ if (f.isFile() && f.name.endsWith('.json')) langs.add(path.basename(f.name, '.json'));
418
446
  }
419
- for (const f of fs.readdirSync(d, { withFileTypes: true })) {
447
+ for (const f of files) {
420
448
  const nestedPath = path.join(d, f.name, `${f.name}.json`);
421
449
  if (f.isDirectory() && SecurityUtils.safeExistsSync(nestedPath, getValidationBase(nestedPath))) {
422
450
  langs.add(f.name);
@@ -4,11 +4,11 @@ const configManager = require('./config-manager');
4
4
  const SecurityUtils = require('./security');
5
5
  const packageJson = require('../package.json');
6
6
 
7
- function ensureDirectory(dirPath) {
8
- if (!dirPath || typeof dirPath !== 'string') return;
9
- if (!SecurityUtils.safeExistsSync(dirPath)) {
10
- fs.mkdirSync(dirPath, { recursive: true });
11
- }
7
+ function ensureDirectory(dirPath) {
8
+ if (!dirPath || typeof dirPath !== 'string') return;
9
+ if (!SecurityUtils.safeExistsSync(dirPath, process.cwd())) {
10
+ SecurityUtils.safeMkdirSync(dirPath, process.cwd(), { recursive: true });
11
+ }
12
12
  }
13
13
 
14
14
  function readJsonSafe(filePath) {
@@ -22,22 +22,24 @@ function readJsonSafe(filePath) {
22
22
  }
23
23
  }
24
24
 
25
- function hasSourceLanguageFiles(sourceDir, sourceLanguage) {
26
- const baseSourceDir = path.resolve(sourceDir);
27
- const modularLanguageDir = path.join(baseSourceDir, sourceLanguage);
28
- const singleLanguageFile = path.join(baseSourceDir, `${sourceLanguage}.json`);
29
-
30
- if (SecurityUtils.safeExistsSync(modularLanguageDir)) {
31
- try {
32
- if (fs.statSync(modularLanguageDir).isDirectory()) {
33
- return fs.readdirSync(modularLanguageDir).some(file => file.endsWith('.json'));
34
- }
35
- } catch {
36
- // Continue with single-file fallback.
37
- }
38
- }
39
-
40
- return SecurityUtils.safeExistsSync(singleLanguageFile);
25
+ function hasSourceLanguageFiles(sourceDir, sourceLanguage) {
26
+ const baseSourceDir = path.resolve(sourceDir);
27
+ const modularLanguageDir = path.join(baseSourceDir, sourceLanguage);
28
+ const singleLanguageFile = path.join(baseSourceDir, `${sourceLanguage}.json`);
29
+
30
+ if (SecurityUtils.safeExistsSync(modularLanguageDir, process.cwd())) {
31
+ try {
32
+ const stat = SecurityUtils.safeStatSync(modularLanguageDir, process.cwd());
33
+ if (stat && stat.isDirectory()) {
34
+ const items = SecurityUtils.safeReaddirSync(modularLanguageDir, process.cwd(), { withFileTypes: true });
35
+ return items ? items.some(item => item.isFile() && item.name.endsWith('.json')) : false;
36
+ }
37
+ } catch {
38
+ // Continue with single-file fallback.
39
+ }
40
+ }
41
+
42
+ return SecurityUtils.safeExistsSync(singleLanguageFile, process.cwd());
41
43
  }
42
44
 
43
45
  /**
@@ -78,11 +78,14 @@ function secureErrorHandler(options = {}) {
78
78
  const config = { ...defaults, ...options };
79
79
 
80
80
  // Ensure log directory exists
81
- if (config.logFilePath && !SecurityUtils.safeExistsSync(path.dirname(config.logFilePath))) {
82
- try {
83
- fs.mkdirSync(path.dirname(config.logFilePath), { recursive: true });
84
- } catch (e) {
85
- console.error('Failed to create log directory:', e);
81
+ if (config.logFilePath) {
82
+ const logDir = path.dirname(config.logFilePath);
83
+ if (!SecurityUtils.safeExistsSync(logDir, process.cwd())) {
84
+ try {
85
+ SecurityUtils.safeMkdirSync(logDir, process.cwd(), { recursive: true });
86
+ } catch (e) {
87
+ console.error('Failed to create log directory:', e);
88
+ }
86
89
  }
87
90
  }
88
91
 
@@ -128,7 +131,7 @@ function secureErrorHandler(options = {}) {
128
131
 
129
132
  // Log to console
130
133
  if (typeof config.logFunction === 'function') {
131
- SecurityUtils.safeWriteFileSync(config.logFilePath, JSON.stringify(logEntry, null, 2));
134
+ config.logFunction(logEntry);
132
135
  }
133
136
 
134
137
  // Log to file if configured
@@ -136,6 +139,7 @@ function secureErrorHandler(options = {}) {
136
139
  SecurityUtils.safeWriteFileSync(
137
140
  config.logFilePath,
138
141
  JSON.stringify(logEntry) + '\n',
142
+ path.dirname(config.logFilePath),
139
143
  'utf8'
140
144
  );
141
145
  }
package/utils/security.js CHANGED
@@ -299,7 +299,7 @@ static _logging = false;
299
299
 
300
300
  // Check for actual path traversal (going outside the base directory)
301
301
  const relativePath = path.relative(base, finalPath);
302
- if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) {
302
+ if (relativePath.startsWith('..')) {
303
303
  const message = useI18n
304
304
  ? i18n.t('security.pathTraversalAttempt')
305
305
  : 'Path traversal attempt';
@@ -474,6 +474,32 @@ static _logging = false;
474
474
  }
475
475
  }
476
476
 
477
+ static safeUnlinkSync(filePath, basePath) {
478
+ const validatedPath = this.validatePath(filePath, basePath);
479
+ if (!validatedPath) {
480
+ return false;
481
+ }
482
+ try {
483
+ fs.unlinkSync(validatedPath);
484
+ return true;
485
+ } catch {
486
+ return false;
487
+ }
488
+ }
489
+
490
+ static safeRmdirSync(dirPath, basePath) {
491
+ const validatedPath = this.validatePath(dirPath, basePath);
492
+ if (!validatedPath) {
493
+ return false;
494
+ }
495
+ try {
496
+ fs.rmdirSync(validatedPath);
497
+ return true;
498
+ } catch {
499
+ return false;
500
+ }
501
+ }
502
+
477
503
  /**
478
504
  * Safely parse JSON content.
479
505
  * Accepts both raw JSON strings and already-parsed objects.
@@ -486,8 +512,8 @@ static _logging = false;
486
512
  return fallback;
487
513
  }
488
514
 
489
- if (typeof input === 'object') {
490
- return input;
515
+ if (typeof input === 'object' && !Array.isArray(input)) {
516
+ return JSON.parse(JSON.stringify(input));
491
517
  }
492
518
 
493
519
  if (typeof input !== 'string') {
@@ -571,7 +597,7 @@ static _logging = false;
571
597
  const relativePath = path.relative(resolvedBase, resolvedPath);
572
598
 
573
599
  // Ensure the final path is within the base directory
574
- if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) {
600
+ if (relativePath.startsWith('..')) {
575
601
  SecurityUtils.logSecurityEvent('Path traversal attempt detected in safeJoin', 'error', {
576
602
  basePath,
577
603
  paths,
@@ -271,84 +271,73 @@ static async handleInvalidConfig() {
271
271
 
272
272
  // Create new promise and store it
273
273
  SetupEnforcer._setupCheckInProgress = true;
274
- SetupEnforcer._setupCheckPromise = new Promise(async (resolve, reject) => {
275
- try {
276
- // Avoid circular dependency - use direct path resolution
277
- const path = require('path');
278
- const configPath = path.join(process.cwd(), '.i18ntk-config');
274
+ SetupEnforcer._setupCheckPromise = (async () => {
275
+ try {
276
+ // Avoid circular dependency - use direct path resolution
277
+ const pathModule = require('path');
278
+ const configPath = pathModule.join(process.cwd(), '.i18ntk-config');
279
279
  const exists = SecurityUtils.safeExistsSync(configPath);
280
280
  if (!exists) {
281
281
  await SetupEnforcer.handleMissingSetup();
282
- // After setup is done, re-check the config
283
282
  const existsAfter = SecurityUtils.safeExistsSync(configPath);
284
283
  if (existsAfter) {
285
- resolve(true);
286
- } else {
287
- process.exit(0);
284
+ return true;
288
285
  }
289
- return;
286
+ process.exit(0);
290
287
  }
291
288
 
292
289
  try {
293
- const configRaw = SecurityUtils.safeReadFileSync(configPath, path.dirname(configPath), 'utf8');
290
+ const configRaw = SecurityUtils.safeReadFileSync(configPath, pathModule.dirname(configPath), 'utf8');
294
291
  const config = SecurityUtils.safeParseJSON(configRaw);
295
292
  if (!config || typeof config !== 'object') {
296
293
  await SetupEnforcer.handleInvalidConfig();
297
294
  process.exit(0);
298
295
  }
299
-
300
- // Check if setup has been explicitly marked as completed
296
+
301
297
  if (config.setup && config.setup.completed === true) {
302
- resolve(true);
303
- return;
298
+ return true;
304
299
  }
305
-
306
- // Fallback: check if config has required fields (for backward compatibility)
300
+
307
301
  if (!config.version || !config.sourceDir || (!config.detectedFramework && !(config.framework && config.framework.detected !== false))) {
308
302
  await SetupEnforcer.handleIncompleteSetup();
309
- // After setup is done, re-check the config
310
- const newConfigRaw = SecurityUtils.safeReadFileSync(configPath, path.dirname(configPath), 'utf8');
303
+ const newConfigRaw = SecurityUtils.safeReadFileSync(configPath, pathModule.dirname(configPath), 'utf8');
311
304
  const newConfig = SecurityUtils.safeParseJSON(newConfigRaw);
312
305
  if (!newConfig || typeof newConfig !== 'object') {
313
306
  process.exit(0);
314
307
  }
315
308
  if (newConfig.setup && newConfig.setup.completed === true) {
316
- resolve(true);
309
+ return true;
317
310
  } else if (newConfig.version && newConfig.sourceDir && (newConfig.detectedFramework || (newConfig.framework && newConfig.framework.detected !== false))) {
318
- resolve(true);
319
- } else {
320
- process.exit(0);
311
+ return true;
321
312
  }
322
- return;
313
+ process.exit(0);
323
314
  }
324
315
 
325
- resolve(true);
316
+ return true;
326
317
  } catch (error) {
327
318
  await SetupEnforcer.handleInvalidConfig();
328
- // After setup is done, re-check the config
329
319
  try {
330
- const newConfigRaw = SecurityUtils.safeReadFileSync(configPath, path.dirname(configPath), 'utf8');
320
+ const newConfigRaw = SecurityUtils.safeReadFileSync(configPath, pathModule.dirname(configPath), 'utf8');
331
321
  const newConfig = SecurityUtils.safeParseJSON(newConfigRaw);
332
322
  if (!newConfig || typeof newConfig !== 'object') {
333
323
  process.exit(0);
334
324
  }
335
325
  if (newConfig.setup && newConfig.setup.completed === true) {
336
- resolve(true);
326
+ return true;
337
327
  } else if (newConfig.version && newConfig.sourceDir && (newConfig.detectedFramework || (newConfig.framework && newConfig.framework.detected !== false))) {
338
- resolve(true);
339
- } else {
340
- process.exit(0);
328
+ return true;
341
329
  }
330
+ process.exit(0);
342
331
  } catch (e) {
343
332
  process.exit(0);
344
333
  }
345
334
  }
346
335
  } catch (error) {
347
- reject(error);
336
+ throw error;
348
337
  } finally {
349
338
  SetupEnforcer._setupCheckInProgress = false;
350
339
  }
351
- });
340
+ })();
352
341
 
353
342
  return SetupEnforcer._setupCheckPromise;
354
343
  }