i18ntk 2.5.0 → 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.
- package/CHANGELOG.md +366 -0
- package/CODE_OF_CONDUCT.md +133 -0
- package/CONTRIBUTING.md +41 -0
- package/FUNDING.md +5 -0
- package/README.md +38 -16
- package/SECURITY.md +52 -0
- package/main/i18ntk-analyze.js +4 -4
- package/main/i18ntk-scanner.js +14 -12
- package/main/i18ntk-validate.js +25 -18
- package/main/manage/commands/AnalyzeCommand.js +7 -4
- package/main/manage/commands/FixerCommand.js +11 -1
- package/main/manage/commands/ScannerCommand.js +12 -10
- package/main/manage/commands/ValidateCommand.js +21 -17
- package/main/manage/index.js +6 -7
- package/package.json +12 -165
- package/runtime/enhanced.js +64 -10
- package/runtime/i18ntk.d.ts +10 -6
- package/runtime/index.js +45 -22
- package/utils/admin-auth.js +85 -21
- package/utils/config-helper.js +43 -37
- package/utils/config-manager.js +59 -49
- package/utils/config.js +13 -4
- package/utils/env-manager.js +3 -1
- package/utils/i18n-helper.js +41 -13
- package/utils/init-helper.js +23 -21
- package/utils/secure-errors.js +10 -6
- package/utils/security.js +30 -4
- package/utils/setup-enforcer.js +22 -33
- package/utils/watch-locales.js +12 -5
package/utils/config-manager.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
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
|
|
90
|
-
const
|
|
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(
|
|
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(
|
|
334
|
+
const stats = await fs.promises.stat(lockPath);
|
|
333
335
|
if (Date.now() - stats.mtimeMs > CONFIG_LOCK_STALE_MS) {
|
|
334
|
-
await fs.promises.unlink(
|
|
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(
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
584
|
-
await fs.promises.rename(tempPath,
|
|
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
|
-
|
|
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(
|
|
627
|
-
fs.mkdirSync(
|
|
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(
|
|
635
|
-
const rawConfig = SecurityUtils.safeReadFileSync(
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/utils/env-manager.js
CHANGED
|
@@ -258,7 +258,9 @@ class EnvironmentManager {
|
|
|
258
258
|
|
|
259
259
|
getBoolean(name) {
|
|
260
260
|
const value = this.get(name);
|
|
261
|
-
|
|
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
|
/**
|
package/utils/i18n-helper.js
CHANGED
|
@@ -11,26 +11,53 @@ function getSecurityUtils() {
|
|
|
11
11
|
try {
|
|
12
12
|
securityUtils = require('./security');
|
|
13
13
|
} catch (error) {
|
|
14
|
-
// Fallback: use basic fs operations
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
safeStatSync: (targetPath, basePath) => {
|
|
32
55
|
try {
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
417
|
-
|
|
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
|
|
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);
|
package/utils/init-helper.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
/**
|
package/utils/secure-errors.js
CHANGED
|
@@ -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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
600
|
+
if (relativePath.startsWith('..')) {
|
|
575
601
|
SecurityUtils.logSecurityEvent('Path traversal attempt detected in safeJoin', 'error', {
|
|
576
602
|
basePath,
|
|
577
603
|
paths,
|
package/utils/setup-enforcer.js
CHANGED
|
@@ -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 =
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
286
|
-
} else {
|
|
287
|
-
process.exit(0);
|
|
284
|
+
return true;
|
|
288
285
|
}
|
|
289
|
-
|
|
286
|
+
process.exit(0);
|
|
290
287
|
}
|
|
291
288
|
|
|
292
289
|
try {
|
|
293
|
-
const configRaw = SecurityUtils.safeReadFileSync(configPath,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
309
|
+
return true;
|
|
317
310
|
} else if (newConfig.version && newConfig.sourceDir && (newConfig.detectedFramework || (newConfig.framework && newConfig.framework.detected !== false))) {
|
|
318
|
-
|
|
319
|
-
} else {
|
|
320
|
-
process.exit(0);
|
|
311
|
+
return true;
|
|
321
312
|
}
|
|
322
|
-
|
|
313
|
+
process.exit(0);
|
|
323
314
|
}
|
|
324
315
|
|
|
325
|
-
|
|
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,
|
|
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
|
-
|
|
326
|
+
return true;
|
|
337
327
|
} else if (newConfig.version && newConfig.sourceDir && (newConfig.detectedFramework || (newConfig.framework && newConfig.framework.detected !== false))) {
|
|
338
|
-
|
|
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
|
-
|
|
336
|
+
throw error;
|
|
348
337
|
} finally {
|
|
349
338
|
SetupEnforcer._setupCheckInProgress = false;
|
|
350
339
|
}
|
|
351
|
-
});
|
|
340
|
+
})();
|
|
352
341
|
|
|
353
342
|
return SetupEnforcer._setupCheckPromise;
|
|
354
343
|
}
|