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/runtime/enhanced.js
CHANGED
|
@@ -24,6 +24,33 @@ const IV_LENGTH = 16;
|
|
|
24
24
|
const AUTH_TAG_LENGTH = 16;
|
|
25
25
|
const SALT_LENGTH = 32;
|
|
26
26
|
|
|
27
|
+
// Track active instances to ensure cleanup is registered only once
|
|
28
|
+
let activeInstances = new Set();
|
|
29
|
+
let processHandlersRegistered = false;
|
|
30
|
+
|
|
31
|
+
function registerProcessHandlers() {
|
|
32
|
+
if (processHandlersRegistered) return;
|
|
33
|
+
processHandlersRegistered = true;
|
|
34
|
+
|
|
35
|
+
process.on('exit', () => {
|
|
36
|
+
for (const instance of activeInstances) {
|
|
37
|
+
try { instance.cleanup(); } catch (_) { /* best-effort */ }
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
process.on('SIGINT', () => {
|
|
41
|
+
for (const instance of activeInstances) {
|
|
42
|
+
try { instance.cleanup(); } catch (_) { /* best-effort */ }
|
|
43
|
+
}
|
|
44
|
+
process.exit(0);
|
|
45
|
+
});
|
|
46
|
+
process.on('uncaughtException', () => {
|
|
47
|
+
for (const instance of activeInstances) {
|
|
48
|
+
try { instance.cleanup(); } catch (_) { /* best-effort */ }
|
|
49
|
+
}
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
27
54
|
class I18nEnhancedRuntime extends EventEmitter {
|
|
28
55
|
constructor() {
|
|
29
56
|
super();
|
|
@@ -84,14 +111,14 @@ class I18nEnhancedRuntime extends EventEmitter {
|
|
|
84
111
|
() => this.checkMemoryUsage(),
|
|
85
112
|
30000 // Check every 30 seconds
|
|
86
113
|
);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (process && process.on) {
|
|
90
|
-
process.on('exit', () => this.cleanup());
|
|
91
|
-
process.on('SIGINT', () => this.cleanup());
|
|
92
|
-
process.on('uncaughtException', () => this.cleanup());
|
|
114
|
+
if (typeof this.memoryCheckInterval.unref === 'function') {
|
|
115
|
+
this.memoryCheckInterval.unref();
|
|
93
116
|
}
|
|
94
117
|
|
|
118
|
+
// Register this instance for process-wide cleanup
|
|
119
|
+
activeInstances.add(this);
|
|
120
|
+
registerProcessHandlers();
|
|
121
|
+
|
|
95
122
|
// Add default translations namespace
|
|
96
123
|
this.addNamespace('default', {
|
|
97
124
|
en: {
|
|
@@ -178,12 +205,32 @@ class I18nEnhancedRuntime extends EventEmitter {
|
|
|
178
205
|
}
|
|
179
206
|
|
|
180
207
|
async decryptData(encryptedData, key = this.encryptionKey) {
|
|
181
|
-
if (!key)
|
|
208
|
+
if (!key) {
|
|
209
|
+
throw new EncryptionError('Encryption key not set', {
|
|
210
|
+
operation: 'decrypt',
|
|
211
|
+
keyType: typeof key
|
|
212
|
+
});
|
|
213
|
+
}
|
|
182
214
|
|
|
183
215
|
try {
|
|
184
|
-
|
|
185
|
-
|
|
216
|
+
let data;
|
|
217
|
+
try {
|
|
218
|
+
data = JSON.parse(encryptedData);
|
|
219
|
+
} catch (parseError) {
|
|
220
|
+
throw new EncryptionError('Failed to parse encrypted data', {
|
|
221
|
+
operation: 'decrypt',
|
|
222
|
+
error: parseError.message
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!data || !data.iv || !data.authTag || !data.encrypted) {
|
|
227
|
+
throw new EncryptionError('Invalid encrypted data format', {
|
|
228
|
+
operation: 'decrypt',
|
|
229
|
+
missingFields: ['iv', 'authTag', 'encrypted'].filter(f => !(f in (data || {})))
|
|
230
|
+
});
|
|
231
|
+
}
|
|
186
232
|
|
|
233
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, Buffer.from(key, 'hex'), Buffer.from(data.iv, 'hex'));
|
|
187
234
|
decipher.setAuthTag(Buffer.from(data.authTag, 'hex'));
|
|
188
235
|
|
|
189
236
|
let decrypted = decipher.update(data.encrypted, 'hex', 'utf8');
|
|
@@ -191,7 +238,12 @@ class I18nEnhancedRuntime extends EventEmitter {
|
|
|
191
238
|
|
|
192
239
|
return decrypted;
|
|
193
240
|
} catch (error) {
|
|
194
|
-
|
|
241
|
+
if (error instanceof SecureError) throw error;
|
|
242
|
+
|
|
243
|
+
throw new EncryptionError('Decryption failed', {
|
|
244
|
+
operation: 'decrypt',
|
|
245
|
+
errorId: crypto.randomBytes(4).toString('hex')
|
|
246
|
+
});
|
|
195
247
|
}
|
|
196
248
|
}
|
|
197
249
|
|
|
@@ -619,6 +671,8 @@ class I18nEnhancedRuntime extends EventEmitter {
|
|
|
619
671
|
if (this.config.encryption) {
|
|
620
672
|
this.config.encryption.salt = null;
|
|
621
673
|
}
|
|
674
|
+
|
|
675
|
+
activeInstances.delete(this);
|
|
622
676
|
}
|
|
623
677
|
|
|
624
678
|
// Add or update a cache entry
|
package/runtime/i18ntk.d.ts
CHANGED
|
@@ -447,12 +447,12 @@ export interface I18nRuntime {
|
|
|
447
447
|
*/
|
|
448
448
|
export interface BasicI18nRuntime {
|
|
449
449
|
/**
|
|
450
|
-
* Translate a key with parameters
|
|
450
|
+
* Translate a key with parameters (synchronous)
|
|
451
451
|
*/
|
|
452
452
|
translate(key: string, params?: TranslationParams): string;
|
|
453
453
|
|
|
454
454
|
/**
|
|
455
|
-
* Alias for translate function
|
|
455
|
+
* Alias for translate function (synchronous)
|
|
456
456
|
*/
|
|
457
457
|
t(key: string, params?: TranslationParams): string;
|
|
458
458
|
|
|
@@ -467,7 +467,7 @@ export interface BasicI18nRuntime {
|
|
|
467
467
|
getLanguage(): string;
|
|
468
468
|
|
|
469
469
|
/**
|
|
470
|
-
* Get available languages
|
|
470
|
+
* Get available languages (synchronous)
|
|
471
471
|
*/
|
|
472
472
|
getAvailableLanguages(): string[];
|
|
473
473
|
|
|
@@ -478,16 +478,20 @@ export interface BasicI18nRuntime {
|
|
|
478
478
|
}
|
|
479
479
|
|
|
480
480
|
/**
|
|
481
|
-
*
|
|
481
|
+
* Initialize the enhanced i18ntk runtime (async, returns full I18nRuntime)
|
|
482
482
|
*/
|
|
483
483
|
export declare function initI18nRuntime(config: I18nConfig): Promise<I18nRuntime>;
|
|
484
484
|
|
|
485
485
|
/**
|
|
486
|
-
*
|
|
486
|
+
* Initialize the basic lightweight runtime (synchronous)
|
|
487
|
+
* This is the default export from 'i18ntk/runtime'
|
|
487
488
|
*/
|
|
488
|
-
export declare function initRuntime(
|
|
489
|
+
export declare function initRuntime(options: {
|
|
489
490
|
baseDir: string;
|
|
490
491
|
language?: string;
|
|
492
|
+
fallbackLanguage?: string;
|
|
493
|
+
keySeparator?: string;
|
|
494
|
+
preload?: boolean;
|
|
491
495
|
}): BasicI18nRuntime;
|
|
492
496
|
|
|
493
497
|
/**
|
package/runtime/index.js
CHANGED
|
@@ -30,7 +30,18 @@ function stripBOMAndComments(s) {
|
|
|
30
30
|
|
|
31
31
|
function readJsonSafe(file) {
|
|
32
32
|
const raw = SecurityUtils.safeReadFileSync(file, path.dirname(file), 'utf8');
|
|
33
|
-
|
|
33
|
+
if (raw === null || raw === undefined) {
|
|
34
|
+
throw new Error(`Unable to read JSON file: ${file}`);
|
|
35
|
+
}
|
|
36
|
+
const cleaned = stripBOMAndComments(raw);
|
|
37
|
+
if (!cleaned) {
|
|
38
|
+
throw new Error(`Empty JSON file: ${file}`);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(cleaned);
|
|
42
|
+
} catch (parseError) {
|
|
43
|
+
throw new Error(`Invalid JSON in file ${file}: ${parseError.message}`);
|
|
44
|
+
}
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
function deepMerge(target, source) {
|
|
@@ -93,13 +104,17 @@ function listJsonFilesRecursively(dir) {
|
|
|
93
104
|
while (stack.length) {
|
|
94
105
|
const d = stack.pop();
|
|
95
106
|
if (!SecurityUtils.safeExistsSync(d)) continue;
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
try {
|
|
108
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
109
|
+
const full = path.join(d, entry.name);
|
|
110
|
+
if (entry.isDirectory()) {
|
|
111
|
+
stack.push(full);
|
|
112
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
|
|
113
|
+
results.push(full);
|
|
114
|
+
}
|
|
102
115
|
}
|
|
116
|
+
} catch (_) {
|
|
117
|
+
// Skip directories we cannot read
|
|
103
118
|
}
|
|
104
119
|
}
|
|
105
120
|
return results;
|
|
@@ -111,7 +126,8 @@ function readLanguageFromBase(baseDir, lang) {
|
|
|
111
126
|
const langDir = path.join(baseDir, lang);
|
|
112
127
|
|
|
113
128
|
// Prefer folder if exists, otherwise single file
|
|
114
|
-
|
|
129
|
+
const langDirStat = SecurityUtils.safeStatSync(langDir, path.dirname(langDir));
|
|
130
|
+
if (langDirStat && langDirStat.isDirectory()) {
|
|
115
131
|
const files = listJsonFilesRecursively(langDir);
|
|
116
132
|
for (const file of files) {
|
|
117
133
|
try {
|
|
@@ -121,11 +137,14 @@ function readLanguageFromBase(baseDir, lang) {
|
|
|
121
137
|
// Skip unreadable/invalid files
|
|
122
138
|
}
|
|
123
139
|
}
|
|
124
|
-
} else
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
140
|
+
} else {
|
|
141
|
+
const langFileStat = SecurityUtils.safeStatSync(langFile, path.dirname(langFile));
|
|
142
|
+
if (langFileStat && langFileStat.isFile()) {
|
|
143
|
+
try {
|
|
144
|
+
const data = readJsonSafe(langFile);
|
|
145
|
+
if (data && typeof data === 'object') deepMerge(merged, data);
|
|
146
|
+
} catch (_) { /* ignore */ }
|
|
147
|
+
}
|
|
129
148
|
}
|
|
130
149
|
|
|
131
150
|
return merged;
|
|
@@ -211,16 +230,20 @@ function getAvailableLanguages() {
|
|
|
211
230
|
const langs = new Set();
|
|
212
231
|
if (!state.baseDir) state.baseDir = resolveBaseDir();
|
|
213
232
|
if (!SecurityUtils.safeExistsSync(state.baseDir)) return ['en'];
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
233
|
+
try {
|
|
234
|
+
for (const entry of fs.readdirSync(state.baseDir, { withFileTypes: true })) {
|
|
235
|
+
if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
|
|
236
|
+
langs.add(entry.name.replace(/\.json$/i, ''));
|
|
237
|
+
} else if (entry.isDirectory()) {
|
|
238
|
+
const lang = entry.name;
|
|
239
|
+
const idx = path.join(state.baseDir, lang, `${lang}.json`);
|
|
240
|
+
if (SecurityUtils.safeExistsSync(idx)) langs.add(lang);
|
|
241
|
+
else langs.add(lang); // be permissive
|
|
242
|
+
}
|
|
223
243
|
}
|
|
244
|
+
} catch (_) {
|
|
245
|
+
// Unreadable directory
|
|
246
|
+
return ['en'];
|
|
224
247
|
}
|
|
225
248
|
return Array.from(langs.size ? langs : new Set(['en']));
|
|
226
249
|
}
|
package/utils/admin-auth.js
CHANGED
|
@@ -188,14 +188,60 @@ class AdminAuth {
|
|
|
188
188
|
return crypto.timingSafeEqual(left, right);
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
hasUsablePinConfig(config) {
|
|
192
|
+
return Boolean(
|
|
193
|
+
config &&
|
|
194
|
+
config.enabled === true &&
|
|
195
|
+
typeof config.pinHash === 'string' &&
|
|
196
|
+
config.pinHash.length > 0 &&
|
|
197
|
+
typeof config.salt === 'string' &&
|
|
198
|
+
config.salt.length > 0
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
logMissingPinConfig(context) {
|
|
203
|
+
SecurityUtils.logSecurityEvent(
|
|
204
|
+
'admin_auth_config_invalid',
|
|
205
|
+
'warning',
|
|
206
|
+
{ message: `Admin PIN is required but not usable during ${context}` }
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
getSessionExpiryTime(session) {
|
|
211
|
+
if (!session || typeof session !== 'object') {
|
|
212
|
+
return NaN;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (Number.isFinite(session.expiresAt)) {
|
|
216
|
+
return session.expiresAt;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (typeof session.expiresAt === 'string') {
|
|
220
|
+
const parsedExpiresAt = Date.parse(session.expiresAt);
|
|
221
|
+
if (Number.isFinite(parsedExpiresAt)) {
|
|
222
|
+
return parsedExpiresAt;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (typeof session.expires === 'string') {
|
|
227
|
+
const parsedExpires = Date.parse(session.expires);
|
|
228
|
+
if (Number.isFinite(parsedExpires)) {
|
|
229
|
+
return parsedExpires;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return NaN;
|
|
234
|
+
}
|
|
235
|
+
|
|
191
236
|
/**
|
|
192
237
|
* Verify PIN
|
|
193
238
|
*/
|
|
194
239
|
async verifyPin(pin) {
|
|
195
240
|
try {
|
|
196
241
|
const config = await this.loadConfig();
|
|
197
|
-
if (!config
|
|
198
|
-
|
|
242
|
+
if (!this.hasUsablePinConfig(config)) {
|
|
243
|
+
this.logMissingPinConfig('PIN verification');
|
|
244
|
+
return false;
|
|
199
245
|
}
|
|
200
246
|
|
|
201
247
|
// Check for lockout
|
|
@@ -256,7 +302,7 @@ class AdminAuth {
|
|
|
256
302
|
*/
|
|
257
303
|
async isPinConfigured() {
|
|
258
304
|
const config = await this.loadConfig();
|
|
259
|
-
return
|
|
305
|
+
return this.hasUsablePinConfig(config);
|
|
260
306
|
}
|
|
261
307
|
|
|
262
308
|
/**
|
|
@@ -270,7 +316,10 @@ class AdminAuth {
|
|
|
270
316
|
}
|
|
271
317
|
|
|
272
318
|
const config = await this.loadConfig();
|
|
273
|
-
|
|
319
|
+
if (!this.hasUsablePinConfig(config)) {
|
|
320
|
+
this.logMissingPinConfig('auth-required check');
|
|
321
|
+
}
|
|
322
|
+
return true;
|
|
274
323
|
}
|
|
275
324
|
|
|
276
325
|
/**
|
|
@@ -283,12 +332,6 @@ class AdminAuth {
|
|
|
283
332
|
return false;
|
|
284
333
|
}
|
|
285
334
|
|
|
286
|
-
// Check if admin PIN is actually configured
|
|
287
|
-
const config = await this.loadConfig();
|
|
288
|
-
if (!config || !config.enabled || !config.pinHash) {
|
|
289
|
-
return false; // Don't require PIN if admin PIN is not configured
|
|
290
|
-
}
|
|
291
|
-
|
|
292
335
|
// Check if PIN protection is enabled
|
|
293
336
|
const pinProtection = globalSettings.security?.pinProtection;
|
|
294
337
|
if (!pinProtection || !pinProtection.enabled) {
|
|
@@ -297,7 +340,16 @@ class AdminAuth {
|
|
|
297
340
|
|
|
298
341
|
// Check if this specific script requires protection
|
|
299
342
|
const protectedScripts = pinProtection.protectedScripts || {};
|
|
300
|
-
|
|
343
|
+
if (protectedScripts[scriptName] === false) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const config = await this.loadConfig();
|
|
348
|
+
if (!this.hasUsablePinConfig(config)) {
|
|
349
|
+
this.logMissingPinConfig(`script auth-required check for ${scriptName}`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return true; // Default to true if not explicitly set
|
|
301
353
|
}
|
|
302
354
|
|
|
303
355
|
/**
|
|
@@ -322,7 +374,10 @@ class AdminAuth {
|
|
|
322
374
|
process.exit(0);
|
|
323
375
|
});
|
|
324
376
|
process.on('uncaughtException', (error) => {
|
|
325
|
-
SecurityUtils.logSecurityEvent('uncaught_exception', 'error',
|
|
377
|
+
SecurityUtils.logSecurityEvent('uncaught_exception', 'error', {
|
|
378
|
+
message: error && error.message ? error.message : 'Unknown uncaught exception',
|
|
379
|
+
stack: error && error.stack ? String(error.stack).split('\n').slice(0, 3).join('\n') : undefined
|
|
380
|
+
});
|
|
326
381
|
cleanup();
|
|
327
382
|
process.exit(1);
|
|
328
383
|
});
|
|
@@ -336,11 +391,14 @@ class AdminAuth {
|
|
|
336
391
|
sessionId = this.generateSessionId();
|
|
337
392
|
}
|
|
338
393
|
|
|
394
|
+
const now = Date.now();
|
|
395
|
+
const expiresAt = now + this.sessionTimeout;
|
|
339
396
|
const session = {
|
|
340
397
|
id: sessionId,
|
|
341
|
-
created: new Date().toISOString(),
|
|
342
|
-
lastActivity: new Date().toISOString(),
|
|
343
|
-
expires: new Date(
|
|
398
|
+
created: new Date(now).toISOString(),
|
|
399
|
+
lastActivity: new Date(now).toISOString(),
|
|
400
|
+
expires: new Date(expiresAt).toISOString(),
|
|
401
|
+
expiresAt
|
|
344
402
|
};
|
|
345
403
|
|
|
346
404
|
this.activeSessions.set(sessionId, session);
|
|
@@ -373,10 +431,10 @@ class AdminAuth {
|
|
|
373
431
|
return false;
|
|
374
432
|
}
|
|
375
433
|
|
|
376
|
-
const now =
|
|
377
|
-
const
|
|
434
|
+
const now = Date.now();
|
|
435
|
+
const expiresAt = this.getSessionExpiryTime(session);
|
|
378
436
|
|
|
379
|
-
if (now >
|
|
437
|
+
if (!Number.isFinite(expiresAt) || now > expiresAt) {
|
|
380
438
|
this.activeSessions.delete(sessionId);
|
|
381
439
|
this.clearCurrentSession();
|
|
382
440
|
SecurityUtils.logSecurityEvent(
|
|
@@ -388,8 +446,10 @@ class AdminAuth {
|
|
|
388
446
|
}
|
|
389
447
|
|
|
390
448
|
// Update last activity
|
|
391
|
-
|
|
392
|
-
session.
|
|
449
|
+
const nextExpiresAt = now + this.sessionTimeout;
|
|
450
|
+
session.lastActivity = new Date(now).toISOString();
|
|
451
|
+
session.expires = new Date(nextExpiresAt).toISOString();
|
|
452
|
+
session.expiresAt = nextExpiresAt;
|
|
393
453
|
this.activeSessions.set(sessionId, session);
|
|
394
454
|
|
|
395
455
|
return true;
|
|
@@ -571,9 +631,13 @@ class AdminAuth {
|
|
|
571
631
|
let cleaned = 0;
|
|
572
632
|
|
|
573
633
|
for (const [sessionId, session] of this.activeSessions.entries()) {
|
|
574
|
-
const expiresAt =
|
|
634
|
+
const expiresAt = this.getSessionExpiryTime(session);
|
|
575
635
|
if (!Number.isFinite(expiresAt) || now > expiresAt) {
|
|
576
636
|
this.activeSessions.delete(sessionId);
|
|
637
|
+
if (this.currentSession && this.currentSession.id === sessionId) {
|
|
638
|
+
this.currentSession = null;
|
|
639
|
+
this.sessionStartTime = null;
|
|
640
|
+
}
|
|
577
641
|
cleaned++;
|
|
578
642
|
}
|
|
579
643
|
}
|
package/utils/config-helper.js
CHANGED
|
@@ -51,7 +51,9 @@ async function getUnifiedConfig(scriptName, cliArgs = {}) {
|
|
|
51
51
|
}
|
|
52
52
|
settingsDir = safeConfigDir;
|
|
53
53
|
const configFile = path.join(settingsDir, 'i18ntk-config.json');
|
|
54
|
-
|
|
54
|
+
const rawConfig = SecurityUtils.safeReadFileSync(configFile, settingsDir, 'utf8');
|
|
55
|
+
cfg = rawConfig ? SecurityUtils.safeParseJSON(rawConfig) : {};
|
|
56
|
+
if (!cfg || typeof cfg !== 'object') cfg = {};
|
|
55
57
|
projectRoot = settingsDir;
|
|
56
58
|
cfg.projectRoot = projectRoot;
|
|
57
59
|
cfg.sourceDir = path.resolve(projectRoot, toStr(cfg.sourceDir) || './locales');
|
|
@@ -60,11 +62,15 @@ async function getUnifiedConfig(scriptName, cliArgs = {}) {
|
|
|
60
62
|
} else {
|
|
61
63
|
cfg = configManager.getConfig();
|
|
62
64
|
// Use current working directory instead of hardcoded path
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
+
const isSuspiciousPath = cfg.projectRoot && (
|
|
66
|
+
cfg.projectRoot.includes('i18n-management-toolkit-main') ||
|
|
67
|
+
cfg.projectRoot.includes('i18ntk-') ||
|
|
68
|
+
!SecurityUtils.safeExistsSync(cfg.projectRoot, path.dirname(cfg.projectRoot || '.'))
|
|
69
|
+
);
|
|
70
|
+
projectRoot = isSuspiciousPath ? process.cwd() : path.resolve(cfg.projectRoot || '.');
|
|
65
71
|
|
|
66
72
|
// Update config with dynamic project root
|
|
67
|
-
if (
|
|
73
|
+
if (isSuspiciousPath) {
|
|
68
74
|
cfg.projectRoot = '.';
|
|
69
75
|
}
|
|
70
76
|
|
|
@@ -110,7 +116,7 @@ async function getUnifiedConfig(scriptName, cliArgs = {}) {
|
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
// Auto-fix i18nDir if missing but sourceDir exists
|
|
113
|
-
if (!SecurityUtils.safeExistsSync(cfg.i18nDir) && SecurityUtils.safeExistsSync(cfg.sourceDir)) {
|
|
119
|
+
if (!SecurityUtils.safeExistsSync(cfg.i18nDir, projectRoot) && SecurityUtils.safeExistsSync(cfg.sourceDir, projectRoot)) {
|
|
114
120
|
await configManager.updateConfig({ i18nDir: configManager.toRelative(cfg.sourceDir) });
|
|
115
121
|
cfg.i18nDir = cfg.sourceDir;
|
|
116
122
|
}
|
|
@@ -414,8 +420,8 @@ function ensureDirectory(dirPath) {
|
|
|
414
420
|
// Silently handle undefined or invalid paths to prevent security errors
|
|
415
421
|
return;
|
|
416
422
|
}
|
|
417
|
-
if (!SecurityUtils.safeExistsSync(dirPath)) {
|
|
418
|
-
|
|
423
|
+
if (!SecurityUtils.safeExistsSync(dirPath, process.cwd())) {
|
|
424
|
+
SecurityUtils.safeMkdirSync(dirPath, process.cwd(), { recursive: true });
|
|
419
425
|
}
|
|
420
426
|
}
|
|
421
427
|
|
|
@@ -499,49 +505,49 @@ async function initializeSourceFiles(sourceDir, sourceLang) {
|
|
|
499
505
|
ensureDirectory(sourceDir);
|
|
500
506
|
|
|
501
507
|
// Write the default source language file
|
|
502
|
-
SecurityUtils.safeWriteFileSync(sourceFile, JSON.stringify(defaultContent, null, 2));
|
|
508
|
+
SecurityUtils.safeWriteFileSync(sourceFile, JSON.stringify(defaultContent, null, 2), sourceDir, 'utf8');
|
|
503
509
|
|
|
504
510
|
// Create directories for supported languages
|
|
505
511
|
const supportedLanguages = ['es', 'fr', 'de', 'ja', 'ru', 'zh', 'pt'];
|
|
506
512
|
|
|
507
513
|
supportedLanguages.forEach(lang => {
|
|
508
514
|
const langFile = path.join(sourceDir, `${lang}.json`);
|
|
509
|
-
if (!SecurityUtils.safeExistsSync(langFile)) {
|
|
515
|
+
if (!SecurityUtils.safeExistsSync(langFile, sourceDir)) {
|
|
510
516
|
// Create empty object structure for each language
|
|
511
517
|
const emptyStructure = {
|
|
512
518
|
app: {},
|
|
513
519
|
common: {},
|
|
514
520
|
navigation: {}
|
|
515
521
|
};
|
|
516
|
-
SecurityUtils.safeWriteFileSync(langFile, JSON.stringify(emptyStructure, null, 2));
|
|
522
|
+
SecurityUtils.safeWriteFileSync(langFile, JSON.stringify(emptyStructure, null, 2), sourceDir, 'utf8');
|
|
517
523
|
}
|
|
518
524
|
});
|
|
519
525
|
|
|
520
|
-
// Create v2 project config if it doesn't exist
|
|
521
|
-
const configFile = '.i18ntk-config';
|
|
522
|
-
if (!SecurityUtils.safeExistsSync(configFile)) {
|
|
523
|
-
const version = (() => {
|
|
524
|
-
try {
|
|
525
|
-
return require('../package.json').version;
|
|
526
|
-
} catch {
|
|
527
|
-
return '2.0.0';
|
|
528
|
-
}
|
|
529
|
-
})();
|
|
530
|
-
const defaultConfig = {
|
|
531
|
-
version,
|
|
532
|
-
sourceDir: sourceDir,
|
|
533
|
-
outputDir: "./i18ntk-reports",
|
|
534
|
-
defaultLanguage: sourceLang,
|
|
535
|
-
supportedLanguages: [sourceLang, 'es', 'fr', 'de', 'ja', 'ru', 'zh', 'pt'],
|
|
536
|
-
setup: {
|
|
537
|
-
completed: true,
|
|
538
|
-
completedAt: new Date().toISOString(),
|
|
539
|
-
version,
|
|
540
|
-
setupId: `setup_${Date.now()}`
|
|
541
|
-
},
|
|
542
|
-
security: {
|
|
543
|
-
adminPinEnabled: true,
|
|
544
|
-
sessionTimeout: 1800000,
|
|
526
|
+
// Create v2 project config if it doesn't exist
|
|
527
|
+
const configFile = '.i18ntk-config';
|
|
528
|
+
if (!SecurityUtils.safeExistsSync(configFile, process.cwd())) {
|
|
529
|
+
const version = (() => {
|
|
530
|
+
try {
|
|
531
|
+
return require('../package.json').version;
|
|
532
|
+
} catch {
|
|
533
|
+
return '2.0.0';
|
|
534
|
+
}
|
|
535
|
+
})();
|
|
536
|
+
const defaultConfig = {
|
|
537
|
+
version,
|
|
538
|
+
sourceDir: sourceDir,
|
|
539
|
+
outputDir: "./i18ntk-reports",
|
|
540
|
+
defaultLanguage: sourceLang,
|
|
541
|
+
supportedLanguages: [sourceLang, 'es', 'fr', 'de', 'ja', 'ru', 'zh', 'pt'],
|
|
542
|
+
setup: {
|
|
543
|
+
completed: true,
|
|
544
|
+
completedAt: new Date().toISOString(),
|
|
545
|
+
version,
|
|
546
|
+
setupId: `setup_${Date.now()}`
|
|
547
|
+
},
|
|
548
|
+
security: {
|
|
549
|
+
adminPinEnabled: true,
|
|
550
|
+
sessionTimeout: 1800000,
|
|
545
551
|
maxFailedAttempts: 3
|
|
546
552
|
},
|
|
547
553
|
performance: {
|
|
@@ -550,8 +556,8 @@ async function initializeSourceFiles(sourceDir, sourceLang) {
|
|
|
550
556
|
batchSize: 1000
|
|
551
557
|
}
|
|
552
558
|
};
|
|
553
|
-
SecurityUtils.safeWriteFileSync(configFile, JSON.stringify(defaultConfig, null, 2));
|
|
554
|
-
}
|
|
559
|
+
SecurityUtils.safeWriteFileSync(configFile, JSON.stringify(defaultConfig, null, 2), process.cwd(), 'utf8');
|
|
560
|
+
}
|
|
555
561
|
}
|
|
556
562
|
|
|
557
563
|
|