i18ntk 2.3.4 → 2.3.6

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.4
1
+ # i18ntk v2.3.6
2
2
 
3
3
  Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, and translation completion.
4
4
 
@@ -9,15 +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.4)](https://socket.dev/npm/package/i18ntk/overview/2.3.4)
12
+ [![socket](https://socket.dev/api/badge/npm/package/i18ntk/2.3.6)](https://socket.dev/npm/package/i18ntk/overview/2.3.6)
13
13
 
14
14
  ## Upgrade Notice
15
15
 
16
- Versions earlier than `2.3.4` may contain known stability and security issues.
17
- They are considered unsupported for production use. Upgrade to `2.3.4` or newer.
18
- The CLI now checks npm registry metadata at startup and warns when your installed version is out of date.
19
- Set `I18NTK_DISABLE_UPDATE_CHECK=true` to disable this warning in restricted/offline environments.
20
- Set `I18NTK_DISABLE_AUTOSAVE=1` in server/runtime environments to keep config in memory and skip disk writes.
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.
21
18
 
22
19
  ## What i18ntk Does
23
20
 
@@ -154,7 +151,7 @@ Example `.i18ntk-config`:
154
151
 
155
152
  ```json
156
153
  {
157
- "version": "2.3.4",
154
+ "version": "2.3.6",
158
155
  "sourceDir": "./locales",
159
156
  "i18nDir": "./locales",
160
157
  "outputDir": "./i18ntk-reports",
@@ -177,7 +174,7 @@ See [docs/api/CONFIGURATION.md](docs/api/CONFIGURATION.md) for the full configur
177
174
  - [Runtime API Guide](docs/runtime.md)
178
175
  - [Scanner Guide](docs/scanner-guide.md)
179
176
  - [Environment Variables](docs/environment-variables.md)
180
- - [Migration Guide v2.3.4](docs/migration-guide-v2.3.4.md)
177
+ - [Migration Guide v2.3.5](docs/migration-guide-v2.3.6.md)
181
178
  - [Optimization Prompt](docs/development/package-optimization-prompt.md)
182
179
 
183
180
  ## License
@@ -15,12 +15,20 @@ const SecurityUtils = require('../utils/security');
15
15
  *
16
16
  * Class-based implementation of backup functionality for use with CommandRouter
17
17
  */
18
- class I18nBackup {
18
+ class I18nBackup {
19
19
  constructor(config = {}) {
20
20
  this.config = config;
21
21
  this.backupDir = path.join(process.cwd(), 'i18ntk-backups');
22
22
  this.maxBackups = Math.min(Math.max(parseInt(config.backup?.maxBackups, 10) || 1, 1), 3);
23
- }
23
+ }
24
+
25
+ validateInProject(targetPath, label = 'path') {
26
+ const validated = SecurityUtils.validatePath(targetPath, process.cwd());
27
+ if (!validated) {
28
+ throw new Error(`Invalid ${label}: ${targetPath}`);
29
+ }
30
+ return validated;
31
+ }
24
32
 
25
33
  /**
26
34
  * Main run method for the backup command
@@ -79,13 +87,15 @@ Options:
79
87
  }
80
88
 
81
89
  // Command handlers
82
- async handleCreate(options = {}) {
83
- // Use absolute path for the locales directory
84
- const dir = (options._ && options._[1]) || options.dir || path.join(__dirname, '..', 'locales');
85
- const outputDir = options.output || this.backupDir;
86
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
87
- const backupName = `backup-${timestamp}.json`;
88
- const backupPath = path.join(outputDir, backupName);
90
+ async handleCreate(options = {}) {
91
+ // Use absolute path for the locales directory
92
+ const requestedDir = (options._ && options._[1]) || options.dir || path.join(process.cwd(), 'locales');
93
+ const requestedOutputDir = options.output || this.backupDir;
94
+ const dir = this.validateInProject(path.resolve(process.cwd(), requestedDir), 'source directory');
95
+ const outputDir = this.validateInProject(path.resolve(process.cwd(), requestedOutputDir), 'backup directory');
96
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
97
+ const backupName = `backup-${timestamp}.json`;
98
+ const backupPath = this.validateInProject(path.join(outputDir, backupName), 'backup file');
89
99
 
90
100
  // Log the paths for debugging
91
101
  logger.debug(`Source directory: ${dir}`);
@@ -104,7 +114,7 @@ Options:
104
114
  }
105
115
 
106
116
  // Validate directory
107
- const sourceDir = path.resolve(dir);
117
+ const sourceDir = dir;
108
118
  try {
109
119
  const stats = await fsp.stat(sourceDir);
110
120
  if (!stats.isDirectory()) {
@@ -163,16 +173,17 @@ Options:
163
173
  };
164
174
  }
165
175
 
166
- async handleRestore(options = {}) {
167
- const backupFile = options._ && options._[1];
168
- if (!backupFile) {
169
- throw new Error('Backup file path is required');
170
- }
171
-
172
- const backupPath = path.resolve(process.cwd(), backupFile);
173
- const outputDir = options.output
174
- ? path.resolve(process.cwd(), options.output)
175
- : path.join(process.cwd(), 'restored');
176
+ async handleRestore(options = {}) {
177
+ const backupFile = options._ && options._[1];
178
+ if (!backupFile) {
179
+ throw new Error('Backup file path is required');
180
+ }
181
+
182
+ const backupPath = this.validateInProject(path.resolve(process.cwd(), backupFile), 'backup file');
183
+ const outputDir = this.validateInProject(
184
+ options.output ? path.resolve(process.cwd(), options.output) : path.join(process.cwd(), 'restored'),
185
+ 'restore output directory'
186
+ );
176
187
 
177
188
  // Validate backup file
178
189
  if (!SecurityUtils.safeExistsSync(backupPath, process.cwd())) {
@@ -183,21 +194,21 @@ Options:
183
194
 
184
195
  try {
185
196
  // Read the backup file
186
- const backupData = await fs.readFile(backupPath, 'utf8');
187
- const translations = JSON.parse(backupData);
197
+ const backupData = await fsp.readFile(backupPath, 'utf8');
198
+ const translations = JSON.parse(backupData);
188
199
 
189
200
  // Create output directory if it doesn't exist
190
201
  try {
191
- await fs.mkdir(outputDir, { recursive: true });
192
- } catch (err) {
193
- if (err.code !== 'EEXIST') throw err;
194
- }
202
+ await fsp.mkdir(outputDir, { recursive: true });
203
+ } catch (err) {
204
+ if (err.code !== 'EEXIST') throw err;
205
+ }
195
206
 
196
207
  // Write the restored files
197
- for (const [file, content] of Object.entries(translations)) {
198
- const filePath = path.join(outputDir, file);
199
- await fs.writeFile(filePath, JSON.stringify(content, null, 2));
200
- }
208
+ for (const [file, content] of Object.entries(translations)) {
209
+ const filePath = this.validateInProject(path.join(outputDir, file), 'restore file');
210
+ await fsp.writeFile(filePath, JSON.stringify(content, null, 2), 'utf8');
211
+ }
201
212
 
202
213
  logger.success('Backup restored successfully');
203
214
  logger.info(` Restored ${Object.keys(translations).length} files to: ${outputDir}`);
@@ -214,12 +225,13 @@ Options:
214
225
  }
215
226
  }
216
227
 
217
- async handleList() {
218
- try {
219
- // Ensure backup directory exists
220
- try {
221
- await fsp.access(this.backupDir);
222
- } catch (err) {
228
+ async handleList() {
229
+ const backupDir = this.validateInProject(this.backupDir, 'backup directory');
230
+ try {
231
+ // Ensure backup directory exists
232
+ try {
233
+ await fsp.access(backupDir);
234
+ } catch (err) {
223
235
  if (err.code === 'ENOENT') {
224
236
  logger.warn('No backups found. The backup directory does not exist yet.');
225
237
  } else {
@@ -228,13 +240,13 @@ Options:
228
240
  return { success: true, backups: [] };
229
241
  }
230
242
 
231
- const files = await fsp.readdir(this.backupDir);
243
+ const files = await fsp.readdir(backupDir);
232
244
  const backups = [];
233
245
 
234
246
  for (const file of files) {
235
247
  if (file.startsWith('backup-') && file.endsWith('.json')) {
236
248
  try {
237
- const filePath = path.join(this.backupDir, file);
249
+ const filePath = this.validateInProject(path.join(backupDir, file), 'backup file');
238
250
  const stats = await fsp.stat(filePath);
239
251
  backups.push({
240
252
  name: file,
@@ -288,7 +300,7 @@ Options:
288
300
  throw new Error('Backup file path is required');
289
301
  }
290
302
 
291
- const backupPath = path.resolve(process.cwd(), backupFile);
303
+ const backupPath = this.validateInProject(path.resolve(process.cwd(), backupFile), 'backup file');
292
304
 
293
305
  // Validate backup file
294
306
  if (!SecurityUtils.safeExistsSync(backupPath, process.cwd())) {
@@ -328,21 +340,22 @@ Options:
328
340
  }
329
341
  }
330
342
 
331
- async handleCleanup(options = {}) {
332
- const keep = options.keep ? parseInt(options.keep, 10) : this.maxBackups;
343
+ async handleCleanup(options = {}) {
344
+ const keep = options.keep ? parseInt(options.keep, 10) : this.maxBackups;
345
+ const backupDir = this.validateInProject(this.backupDir, 'backup directory');
333
346
 
334
347
  logger.info('\nCleaning up old backups...');
335
348
 
336
349
  try {
337
- const files = await fsp.readdir(this.backupDir);
338
- const backupFiles = files
339
- .filter(file => file.startsWith('backup-') && file.endsWith('.json'))
340
- .map(file => ({
341
- name: file,
342
- path: path.join(this.backupDir, file),
343
- time: fs.statSync(path.join(this.backupDir, file)).mtime.getTime()
350
+ const files = await fsp.readdir(backupDir);
351
+ const backupFiles = files
352
+ .filter(file => file.startsWith('backup-') && file.endsWith('.json'))
353
+ .map(file => ({
354
+ name: file,
355
+ path: this.validateInProject(path.join(backupDir, file), 'backup file'),
356
+ time: (SecurityUtils.safeStatSync(path.join(backupDir, file), process.cwd()) || { mtime: new Date(0) }).mtime.getTime()
344
357
  }))
345
- .sort((a, b) => b.time - a.time);
358
+ .sort((a, b) => b.time - a.time);
346
359
 
347
360
  // Keep only the most recent 'keep' files
348
361
  const toDelete = backupFiles.slice(keep);
@@ -379,27 +392,28 @@ Options:
379
392
  }
380
393
  }
381
394
 
382
- async cleanupOldBackups(outputDir) {
383
- try {
384
- const files = await fs.readdir(outputDir);
385
- const backupFiles = files
386
- .filter(file => file.startsWith('backup-') && file.endsWith('.json'))
387
- .map(file => ({
388
- name: file,
389
- path: path.join(outputDir, file),
390
- time: fs.statSync(path.join(outputDir, file)).mtime.getTime()
391
- }))
392
- .sort((a, b) => b.time - a.time);
395
+ async cleanupOldBackups(outputDir) {
396
+ try {
397
+ const safeOutputDir = this.validateInProject(outputDir, 'backup output directory');
398
+ const files = await fsp.readdir(safeOutputDir);
399
+ const backupFiles = files
400
+ .filter(file => file.startsWith('backup-') && file.endsWith('.json'))
401
+ .map(file => ({
402
+ name: file,
403
+ path: this.validateInProject(path.join(safeOutputDir, file), 'backup file'),
404
+ time: (SecurityUtils.safeStatSync(path.join(safeOutputDir, file), process.cwd()) || { mtime: new Date(0) }).mtime.getTime()
405
+ }))
406
+ .sort((a, b) => b.time - a.time);
393
407
 
394
408
  // Keep only the most recent files
395
409
  const toDelete = backupFiles.slice(this.maxBackups);
396
410
 
397
- for (const file of toDelete) {
398
- try {
399
- await fs.unlink(file.path);
400
- } catch (err) {
401
- // Ignore cleanup errors
402
- }
411
+ for (const file of toDelete) {
412
+ try {
413
+ await fsp.unlink(file.path);
414
+ } catch (err) {
415
+ // Ignore cleanup errors
416
+ }
403
417
  }
404
418
  } catch (error) {
405
419
  // Ignore cleanup errors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18ntk",
3
- "version": "2.3.4",
3
+ "version": "2.3.6",
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",
@@ -112,7 +112,7 @@
112
112
  },
113
113
  "license": "MIT",
114
114
  "author": {
115
- "name": "Vladimir Noskov",
115
+ "name": "Vlad Noskov",
116
116
  "url": "https://github.com/vladnoskv"
117
117
  },
118
118
  "type": "commonjs",
@@ -213,10 +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",
217
- "deprecate:versions": "node scripts/deprecate-versions.js",
218
- "deprecate:dry-run": "node scripts/deprecate-versions.js --dry-run",
219
- "deprecate:verify": "node scripts/verify-deprecations.js"
216
+ "lint:locales": "node scripts/lint-locales.js"
220
217
  },
221
218
  "engines": {
222
219
  "node": ">=16.0.0",
@@ -227,7 +224,7 @@
227
224
  },
228
225
  "preferGlobal": true,
229
226
  "versionInfo": {
230
- "version": "2.3.4",
227
+ "version": "2.3.5",
231
228
  "releaseDate": "12/04/2026",
232
229
  "lastUpdated": "12/04/2026",
233
230
  "maintainer": "Vlad Noskov",
@@ -238,9 +235,10 @@
238
235
  "HOTFIX: Removed deprecated package-path fallback that caused production build warnings for non-exported subpaths.",
239
236
  "CRITICAL FIX: Resolved sizing and usage-analysis regressions in v2 command flow.",
240
237
  "PACKAGING: Reduced publish footprint by removing internal development scripts and legacy fixed-file artifacts.",
241
- "SECURITY: Hardened release checks and added explicit support guidance to update from pre-2.3.4 versions.",
238
+ "SECURITY: Hardened release checks and added explicit support guidance to update from pre-2.3.5 versions.",
242
239
  "CONFIG: Added cross-process file locking for .i18ntk-config writes to prevent production rename races.",
243
240
  "CONFIG: Made autosave runtime-safe with non-throwing save failures and I18NTK_DISABLE_AUTOSAVE support.",
241
+ "SECURITY: Hardened reset/backup path handling and made npm update checks explicit opt-in.",
244
242
  "CLI: Added npm registry version check with upgrade warning for out-of-date installs.",
245
243
  "I18N: Completed internal UI locale parity and actionable untranslated-key cleanup across supported languages."
246
244
  ],
@@ -267,7 +265,7 @@
267
265
  "spring-boot": ">=2.5.0",
268
266
  "laravel": ">=8.0.0"
269
267
  },
270
- "supportPolicy": "Versions earlier than 2.3.4 may be unstable or insecure. Upgrade to 2.3.4 or newer."
268
+ "supportPolicy": "Versions earlier than 2.3.6 may be unstable or insecure. Upgrade to 2.3.6 or newer."
271
269
  },
272
270
  "_comment": "This package is zero-dependency and uses only native Node.js modules"
273
271
  }
@@ -6,14 +6,15 @@ const SecurityUtils = require('../utils/security');
6
6
 
7
7
  class SettingsManager {
8
8
  constructor() {
9
- // Use centralized .i18ntk-settings file as single source of truth
10
- this.configDir = path.resolve(__dirname, '..');
11
- this.configFile = path.join(process.cwd(), '.i18ntk-settings');
12
- this.backupDir = path.join(process.cwd(), 'i18ntk-backups');
9
+ // Use centralized .i18ntk-settings file as single source of truth
10
+ this.configDir = path.resolve(__dirname, '..');
11
+ const projectRoot = path.resolve(process.cwd());
12
+ this.configFile = SecurityUtils.safeJoin(projectRoot, '.i18ntk-settings') || path.join(projectRoot, '.i18ntk-settings');
13
+ this.backupDir = SecurityUtils.safeJoin(projectRoot, 'i18ntk-backups') || path.join(projectRoot, 'i18ntk-backups');
13
14
  this.saveTimeout = null;
14
15
 
15
16
  this.defaultConfig = {
16
- "version": "1.10.1",
17
+ "version": "2.3.6",
17
18
  "language": "en",
18
19
  "uiLanguage": "en",
19
20
  "theme": "dark",
@@ -312,6 +313,18 @@ class SettingsManager {
312
313
  this.settings = this.loadSettings();
313
314
  }
314
315
 
316
+ resolveSafePath(targetPath, basePath = process.cwd()) {
317
+ return SecurityUtils.validatePath(path.resolve(targetPath), basePath);
318
+ }
319
+
320
+ safeDeleteFile(targetPath, basePath = process.cwd()) {
321
+ const validated = this.resolveSafePath(targetPath, basePath);
322
+ if (!validated) return false;
323
+ if (!SecurityUtils.safeExistsSync(validated, basePath)) return false;
324
+ fs.unlinkSync(validated);
325
+ return true;
326
+ }
327
+
315
328
  /**
316
329
  * Load settings from file or return default settings
317
330
  * @returns {object} Settings object
@@ -432,12 +445,18 @@ class SettingsManager {
432
445
  */
433
446
  _saveImmediately() {
434
447
  try {
435
- if (!SecurityUtils.safeExistsSync(this.configDir)) {
436
- fs.mkdirSync(this.configDir, { recursive: true });
448
+ const configDir = path.dirname(this.configFile);
449
+ const validatedConfigDir = this.resolveSafePath(configDir, process.cwd());
450
+ if (!validatedConfigDir) {
451
+ throw new Error('Invalid configuration directory');
452
+ }
453
+
454
+ if (!SecurityUtils.safeExistsSync(validatedConfigDir, process.cwd())) {
455
+ SecurityUtils.safeMkdirSync(validatedConfigDir, process.cwd(), { recursive: true });
437
456
  }
438
457
 
439
458
  const content = JSON.stringify(this.settings, null, 4);
440
- SecurityUtils.safeWriteFileSync(this.configFile, content, path.dirname(this.configFile), 'utf8');
459
+ SecurityUtils.safeWriteFileSync(this.configFile, content, validatedConfigDir, 'utf8');
441
460
 
442
461
  // Create backup if enabled
443
462
  if (this.settings.backup?.enabled) {
@@ -466,13 +485,19 @@ class SettingsManager {
466
485
  }
467
486
 
468
487
  if (!SecurityUtils.safeExistsSync(validatedBackupDir, process.cwd())) {
469
- fs.mkdirSync(validatedBackupDir, { recursive: true });
488
+ SecurityUtils.safeMkdirSync(validatedBackupDir, process.cwd(), { recursive: true });
470
489
  }
471
490
 
472
491
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
473
492
  const backupFile = path.join(validatedBackupDir, `config-${timestamp}.json`);
493
+ const validatedConfigFile = this.resolveSafePath(this.configFile, process.cwd());
494
+ const validatedBackupFile = this.resolveSafePath(backupFile, process.cwd());
495
+ if (!validatedConfigFile || !validatedBackupFile) {
496
+ console.error('Error creating backup: Invalid source or destination path');
497
+ return;
498
+ }
474
499
 
475
- fs.copyFileSync(this.configFile, backupFile);
500
+ fs.copyFileSync(validatedConfigFile, validatedBackupFile);
476
501
 
477
502
  // Clean old backups
478
503
  this.cleanupOldBackups(validatedBackupDir);
@@ -486,17 +511,20 @@ class SettingsManager {
486
511
  */
487
512
  cleanupOldBackups(backupDirectory = null) {
488
513
  try {
489
- const activeBackupDir = backupDirectory || this.backupDir;
514
+ const activeBackupDir = this.resolveSafePath(backupDirectory || this.backupDir, process.cwd());
515
+ if (!activeBackupDir) {
516
+ return;
517
+ }
490
518
  if (!SecurityUtils.safeExistsSync(activeBackupDir, process.cwd())) {
491
519
  return;
492
520
  }
493
521
 
494
- const files = fs.readdirSync(activeBackupDir)
522
+ const files = (SecurityUtils.safeReaddirSync(activeBackupDir, process.cwd()) || [])
495
523
  .filter(file => file.startsWith('config-') && file.endsWith('.json'))
496
524
  .map(file => ({
497
525
  name: file,
498
526
  path: path.join(activeBackupDir, file),
499
- mtime: fs.statSync(path.join(activeBackupDir, file)).mtime
527
+ mtime: (SecurityUtils.safeStatSync(path.join(activeBackupDir, file), process.cwd()) || { mtime: new Date(0) }).mtime
500
528
  }))
501
529
  .sort((a, b) => b.mtime - a.mtime);
502
530
 
@@ -506,7 +534,7 @@ class SettingsManager {
506
534
  : 1;
507
535
  if (files.length > maxBackups) {
508
536
  files.slice(maxBackups).forEach(file => {
509
- fs.unlinkSync(file.path);
537
+ this.safeDeleteFile(file.path, process.cwd());
510
538
  });
511
539
  }
512
540
  } catch (error) {
@@ -546,52 +574,49 @@ class SettingsManager {
546
574
 
547
575
  // 2. Remove actual configuration files used by the system
548
576
  const packageDir = path.resolve(__dirname, '..');
549
- const settingsDir = path.join(packageDir, 'settings');
577
+ const projectRoot = path.resolve(process.cwd());
578
+ const settingsDir = SecurityUtils.safeJoin(packageDir, 'settings');
550
579
 
551
580
  // Main configuration file
552
581
  const mainConfigPath = path.join(settingsDir, 'i18ntk-config.json');
553
- if (SecurityUtils.safeExistsSync(mainConfigPath)) {
554
- fs.unlinkSync(mainConfigPath);
582
+ if (this.safeDeleteFile(mainConfigPath, packageDir)) {
555
583
  console.log('✅ Main configuration file removed');
556
584
  }
557
585
 
558
586
  // Project configuration file
559
587
  const projectConfigPath = path.join(settingsDir, 'project-config.json');
560
- if (SecurityUtils.safeExistsSync(projectConfigPath)) {
561
- fs.unlinkSync(projectConfigPath);
588
+ if (this.safeDeleteFile(projectConfigPath, packageDir)) {
562
589
  console.log('✅ Project configuration removed');
563
590
  }
564
591
 
565
592
  // Setup tracking file
566
593
  const setupFile = path.join(settingsDir, 'setup.json');
567
- if (SecurityUtils.safeExistsSync(setupFile)) {
568
- fs.unlinkSync(setupFile);
594
+ if (this.safeDeleteFile(setupFile, packageDir)) {
569
595
  console.log('✅ Setup tracking cleared');
570
596
  }
571
597
 
572
598
  // 3. Clear all backup files from backups directory
573
599
  const backupsDir = path.join(packageDir, 'backups');
574
- if (SecurityUtils.safeExistsSync(backupsDir)) {
575
- const backupFiles = fs.readdirSync(backupsDir);
600
+ if (SecurityUtils.safeExistsSync(backupsDir, packageDir)) {
601
+ const backupFiles = SecurityUtils.safeReaddirSync(backupsDir, packageDir) || [];
576
602
  for (const file of backupFiles) {
577
603
  if (file.endsWith('.json') || file.endsWith('.bak')) {
578
- fs.unlinkSync(path.join(backupsDir, file));
604
+ this.safeDeleteFile(path.join(backupsDir, file), packageDir);
579
605
  }
580
606
  }
581
607
  console.log('✅ All backup files cleared');
582
608
  }
583
609
 
584
- // 4. Clear admin PIN configuration (multiple possible locations including root)
610
+ // 4. Clear admin PIN configuration (package/project scope only)
585
611
  const adminConfigPaths = [
586
612
  path.join(packageDir, '.i18n-admin-config.json'),
587
613
  path.join(settingsDir, '.i18n-admin-config.json'),
588
614
  path.join(settingsDir, 'admin-config.json'),
589
- path.join(packageDir, '..', '.i18n-admin-config.json') // Root level
615
+ SecurityUtils.safeJoin(projectRoot, '.i18n-admin-config.json') || path.join(projectRoot, '.i18n-admin-config.json')
590
616
  ];
591
617
 
592
618
  for (const adminConfigPath of adminConfigPaths) {
593
- if (SecurityUtils.safeExistsSync(adminConfigPath)) {
594
- fs.unlinkSync(adminConfigPath);
619
+ if (this.safeDeleteFile(adminConfigPath, projectRoot)) {
595
620
  console.log('✅ Admin PIN configuration cleared');
596
621
  }
597
622
  }
@@ -603,8 +628,7 @@ class SettingsManager {
603
628
  ];
604
629
 
605
630
  for (const initFile of initFiles) {
606
- if (SecurityUtils.safeExistsSync(initFile)) {
607
- fs.unlinkSync(initFile);
631
+ if (this.safeDeleteFile(initFile, packageDir)) {
608
632
  console.log('✅ Initialization tracking cleared');
609
633
  }
610
634
  }
@@ -616,10 +640,10 @@ class SettingsManager {
616
640
  ];
617
641
 
618
642
  for (const cacheDir of cacheDirs) {
619
- if (SecurityUtils.safeExistsSync(cacheDir)) {
620
- const cacheFiles = fs.readdirSync(cacheDir);
643
+ if (SecurityUtils.safeExistsSync(cacheDir, packageDir)) {
644
+ const cacheFiles = SecurityUtils.safeReaddirSync(cacheDir, packageDir) || [];
621
645
  for (const file of cacheFiles) {
622
- fs.unlinkSync(path.join(cacheDir, file));
646
+ this.safeDeleteFile(path.join(cacheDir, file), packageDir);
623
647
  }
624
648
  console.log('✅ Cache cleared');
625
649
  }
@@ -639,9 +663,7 @@ class SettingsManager {
639
663
  ];
640
664
 
641
665
  for (const tempFile of tempFiles) {
642
- if (SecurityUtils.safeExistsSync(tempFile)) {
643
- fs.unlinkSync(tempFile);
644
- }
666
+ this.safeDeleteFile(tempFile, packageDir);
645
667
  }
646
668
  console.log('✅ Temporary files cleared');
647
669
 
@@ -656,8 +678,7 @@ class SettingsManager {
656
678
  ];
657
679
 
658
680
  for (const file of additionalFiles) {
659
- if (SecurityUtils.safeExistsSync(file)) {
660
- fs.unlinkSync(file);
681
+ if (this.safeDeleteFile(file, packageDir)) {
661
682
  console.log(`✅ Removed ${path.basename(file)}`);
662
683
  }
663
684
  }
@@ -982,4 +1003,4 @@ class SettingsManager {
982
1003
  }
983
1004
 
984
1005
  module.exports = SettingsManager;
985
-
1006
+
@@ -1,22 +1,25 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
- const crypto = require('crypto');
5
- const SecurityUtils = require('./security');
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const crypto = require('crypto');
5
+ const SecurityUtils = require('./security');
6
6
 
7
7
  // Determine package directory and user project root
8
8
  const packageDir = path.resolve(__dirname, '..');
9
- const userProjectRoot = process.cwd();
9
+ const userProjectRoot = path.resolve(process.cwd());
10
10
 
11
11
  // Always use current working directory for settings to support test environments
12
12
  // This ensures config works correctly when tests change the working directory
13
- const PROJECT_CONFIG_PATH = path.join(process.cwd(), '.i18ntk-config');
14
- const PROJECT_SETTINGS_DIR = path.dirname(PROJECT_CONFIG_PATH);
15
- const CONFIG_LOCK_PATH = `${PROJECT_CONFIG_PATH}.lock`;
16
- const CONFIG_LOCK_TIMEOUT_MS = 5000;
17
- const CONFIG_LOCK_STALE_MS = 15000;
18
- const CONFIG_LOCK_RETRY_MS = 50;
19
- let autosaveDisabledWarned = false;
13
+ const PROJECT_CONFIG_PATH = SecurityUtils.safeJoin(userProjectRoot, '.i18ntk-config');
14
+ if (!PROJECT_CONFIG_PATH) {
15
+ throw new Error('Invalid project config path - potential path traversal attempt');
16
+ }
17
+ const PROJECT_SETTINGS_DIR = path.dirname(PROJECT_CONFIG_PATH);
18
+ const CONFIG_LOCK_PATH = `${PROJECT_CONFIG_PATH}.lock`;
19
+ const CONFIG_LOCK_TIMEOUT_MS = 5000;
20
+ const CONFIG_LOCK_STALE_MS = 15000;
21
+ const CONFIG_LOCK_RETRY_MS = 50;
22
+ let autosaveDisabledWarned = false;
20
23
 
21
24
  // Setup tracking file
22
25
  const SETUP_COMPLETED_FILE = path.join(PROJECT_SETTINGS_DIR, 'setup.json');
@@ -175,14 +178,14 @@ const DEFAULT_CONFIG = {
175
178
  "memoryPooling": true,
176
179
  "stringInterning": true
177
180
  },
178
- "backup": {
179
- "enabled": false,
180
- "location": "./i18ntk-backups",
181
- "singleFileMode": false,
182
- "singleBackupFile": "i18ntk-central-backup.json",
183
- "retentionDays": 30,
184
- "maxBackups": 1
185
- },
181
+ "backup": {
182
+ "enabled": false,
183
+ "location": "./i18ntk-backups",
184
+ "singleFileMode": false,
185
+ "singleBackupFile": "i18ntk-central-backup.json",
186
+ "retentionDays": 30,
187
+ "maxBackups": 1
188
+ },
186
189
  "security": {
187
190
  "adminPinEnabled": false,
188
191
  "adminPinPromptOnInit": true,
@@ -250,69 +253,69 @@ const DEFAULT_CONFIG = {
250
253
 
251
254
  // Environment variable support has been removed in favor of exclusive .i18ntk-config configuration
252
255
 
253
- let currentConfig = null;
254
- let configLoadInProgress = false;
255
- let recursionDepth = 0;
256
- const MAX_RECURSION_DEPTH = 15; // Increased to handle legitimate sequential calls
257
- let configSaveQueue = Promise.resolve();
258
-
259
- function sleep(ms) {
260
- return new Promise((resolve) => setTimeout(resolve, ms));
261
- }
262
-
263
- async function acquireConfigLock(timeoutMs = CONFIG_LOCK_TIMEOUT_MS) {
264
- const start = Date.now();
265
- let lockHandle = null;
266
-
267
- while (!lockHandle) {
268
- try {
269
- lockHandle = await fs.promises.open(CONFIG_LOCK_PATH, 'wx');
270
- await lockHandle.writeFile(String(process.pid), 'utf8');
271
- break;
272
- } catch (error) {
273
- if (!error || error.code !== 'EEXIST') {
274
- throw error;
275
- }
276
-
277
- // Recover from stale lock files left by crashed processes.
278
- try {
279
- const stats = await fs.promises.stat(CONFIG_LOCK_PATH);
280
- if (Date.now() - stats.mtimeMs > CONFIG_LOCK_STALE_MS) {
281
- await fs.promises.unlink(CONFIG_LOCK_PATH);
282
- continue;
283
- }
284
- } catch (_) {
285
- // Lock may have been released between exists check and stat/unlink.
286
- }
287
-
288
- if (Date.now() - start >= timeoutMs) {
289
- throw new Error(`Timed out waiting for config lock after ${timeoutMs}ms`);
290
- }
291
-
292
- await sleep(CONFIG_LOCK_RETRY_MS);
293
- }
294
- }
295
-
296
- return async function releaseConfigLock() {
297
- try {
298
- if (lockHandle) {
299
- await lockHandle.close();
300
- }
301
- } catch (_) {
302
- // Best-effort close only.
303
- }
304
- try {
305
- await fs.promises.unlink(CONFIG_LOCK_PATH);
306
- } catch (_) {
307
- // Best-effort cleanup only.
308
- }
309
- };
310
- }
311
-
312
- function isAutosaveDisabled() {
313
- const flag = String(process.env.I18NTK_DISABLE_AUTOSAVE || '').trim().toLowerCase();
314
- return flag === '1' || flag === 'true' || flag === 'yes';
315
- }
256
+ let currentConfig = null;
257
+ let configLoadInProgress = false;
258
+ let recursionDepth = 0;
259
+ const MAX_RECURSION_DEPTH = 15; // Increased to handle legitimate sequential calls
260
+ let configSaveQueue = Promise.resolve();
261
+
262
+ function sleep(ms) {
263
+ return new Promise((resolve) => setTimeout(resolve, ms));
264
+ }
265
+
266
+ async function acquireConfigLock(timeoutMs = CONFIG_LOCK_TIMEOUT_MS) {
267
+ const start = Date.now();
268
+ let lockHandle = null;
269
+
270
+ while (!lockHandle) {
271
+ try {
272
+ lockHandle = await fs.promises.open(CONFIG_LOCK_PATH, 'wx');
273
+ await lockHandle.writeFile(String(process.pid), 'utf8');
274
+ break;
275
+ } catch (error) {
276
+ if (!error || error.code !== 'EEXIST') {
277
+ throw error;
278
+ }
279
+
280
+ // Recover from stale lock files left by crashed processes.
281
+ try {
282
+ const stats = await fs.promises.stat(CONFIG_LOCK_PATH);
283
+ if (Date.now() - stats.mtimeMs > CONFIG_LOCK_STALE_MS) {
284
+ await fs.promises.unlink(CONFIG_LOCK_PATH);
285
+ continue;
286
+ }
287
+ } catch (_) {
288
+ // Lock may have been released between exists check and stat/unlink.
289
+ }
290
+
291
+ if (Date.now() - start >= timeoutMs) {
292
+ throw new Error(`Timed out waiting for config lock after ${timeoutMs}ms`);
293
+ }
294
+
295
+ await sleep(CONFIG_LOCK_RETRY_MS);
296
+ }
297
+ }
298
+
299
+ return async function releaseConfigLock() {
300
+ try {
301
+ if (lockHandle) {
302
+ await lockHandle.close();
303
+ }
304
+ } catch (_) {
305
+ // Best-effort close only.
306
+ }
307
+ try {
308
+ await fs.promises.unlink(CONFIG_LOCK_PATH);
309
+ } catch (_) {
310
+ // Best-effort cleanup only.
311
+ }
312
+ };
313
+ }
314
+
315
+ function isAutosaveDisabled() {
316
+ const flag = String(process.env.I18NTK_DISABLE_AUTOSAVE || '').trim().toLowerCase();
317
+ return flag === '1' || flag === 'true' || flag === 'yes';
318
+ }
316
319
 
317
320
  function clone(obj) {
318
321
  return JSON.parse(JSON.stringify(obj));
@@ -382,21 +385,21 @@ function tryReadJson(filePath) {
382
385
  return null;
383
386
  }
384
387
 
385
- const parsed = SecurityUtils.safeParseJSON(data);
386
- if (parsed && typeof parsed === 'object') {
387
- return parsed;
388
- }
389
-
390
- console.error(`[i18ntk] Error parsing JSON from ${filePath}: Invalid JSON content`);
391
- // Create a backup of the corrupted file
392
- const backupPath = `${filePath}.corrupted-${Date.now()}.bak`;
393
- try {
394
- SecurityUtils.safeWriteFileSync(backupPath, data, path.dirname(backupPath), 'utf8');
395
- console.warn(`[i18ntk] Created backup of corrupted config at ${backupPath}`);
396
- } catch (backupError) {
397
- console.error(`[i18ntk] Failed to create backup of corrupted config: ${backupError.message}`);
398
- }
399
- return null;
388
+ const parsed = SecurityUtils.safeParseJSON(data);
389
+ if (parsed && typeof parsed === 'object') {
390
+ return parsed;
391
+ }
392
+
393
+ console.error(`[i18ntk] Error parsing JSON from ${filePath}: Invalid JSON content`);
394
+ // Create a backup of the corrupted file
395
+ const backupPath = `${filePath}.corrupted-${Date.now()}.bak`;
396
+ try {
397
+ SecurityUtils.safeWriteFileSync(backupPath, data, path.dirname(backupPath), 'utf8');
398
+ console.warn(`[i18ntk] Created backup of corrupted config at ${backupPath}`);
399
+ } catch (backupError) {
400
+ console.error(`[i18ntk] Failed to create backup of corrupted config: ${backupError.message}`);
401
+ }
402
+ return null;
400
403
  } catch (error) {
401
404
  console.error(`[i18ntk] Error reading config file at ${filePath}: ${error.message}`);
402
405
  return null;
@@ -417,13 +420,13 @@ async function migrateLegacyIfNeeded(baseCfg) {
417
420
  // Best-effort removal of legacy file to prevent future use
418
421
  try { fs.unlinkSync(LEGACY_CONFIG_PATH); } catch (_) {}
419
422
  // Deprecation notice
420
- console.warn('[i18ntk] Deprecated config location detected (~/.i18ntk). Configuration was migrated to project .i18ntk-config.');
421
- return merged;
422
- } catch (_) {
423
- // If write fails, fall back to in-memory config without deleting legacy
424
- console.warn('[i18ntk] Deprecated config location detected (~/.i18ntk). Using migrated settings in memory; failed to persist to .i18ntk-config.');
425
- return merged;
426
- }
423
+ console.warn('[i18ntk] Deprecated config location detected (~/.i18ntk). Configuration was migrated to project .i18ntk-config.');
424
+ return merged;
425
+ } catch (_) {
426
+ // 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.');
428
+ return merged;
429
+ }
427
430
  }
428
431
  }
429
432
  return null;
@@ -487,73 +490,75 @@ function loadConfig() {
487
490
  }
488
491
  }
489
492
 
490
- async function saveConfig(cfg = currentConfig) {
491
- if (!cfg || typeof cfg !== 'object') return;
492
-
493
- // Runtime/server safety valve: allow disabling disk writes entirely.
494
- if (isAutosaveDisabled()) {
495
- currentConfig = cfg;
496
- if (!autosaveDisabledWarned) {
497
- autosaveDisabledWarned = true;
498
- console.warn('[i18ntk] Autosave disabled by I18NTK_DISABLE_AUTOSAVE. Keeping configuration in memory only.');
499
- }
500
- return false;
501
- }
502
-
503
- configSaveQueue = configSaveQueue.then(async () => {
504
- let tempPath = null;
505
- let releaseLock = null;
506
- try {
507
- // Ensure settings directory exists before any lock/file operations.
508
- await fs.promises.mkdir(PROJECT_SETTINGS_DIR, { recursive: true });
509
-
510
- releaseLock = await acquireConfigLock();
511
-
512
- const serialized = JSON.stringify(cfg, null, 2);
513
- if (typeof serialized !== 'string' || serialized.length === 0) {
514
- throw new Error('Cannot save empty configuration payload');
515
- }
516
-
517
- // Use a unique temp file to avoid concurrent writer races.
518
- const nonce = `${process.pid}.${Date.now()}.${crypto.randomUUID()}`;
519
- tempPath = `${PROJECT_CONFIG_PATH}.${nonce}.tmp`;
520
- await fs.promises.writeFile(tempPath, serialized, 'utf8');
521
-
522
- try {
523
- await fs.promises.rename(tempPath, PROJECT_CONFIG_PATH);
524
- } catch (renameError) {
525
- // If destination dir disappeared between checks, recreate and retry once.
526
- if (renameError && renameError.code === 'ENOENT') {
527
- await fs.promises.mkdir(PROJECT_SETTINGS_DIR, { recursive: true });
528
- await fs.promises.rename(tempPath, PROJECT_CONFIG_PATH);
529
- } else {
530
- throw renameError;
531
- }
532
- }
533
-
534
- currentConfig = cfg;
535
- return true;
536
- } catch (error) {
537
- console.error('[i18ntk] Error saving configuration:', error.message);
538
- return false;
539
- } finally {
540
- if (releaseLock) {
541
- await releaseLock();
542
- }
543
- if (tempPath) {
544
- try {
545
- if (SecurityUtils.safeExistsSync(tempPath, path.dirname(tempPath))) {
546
- fs.unlinkSync(tempPath);
547
- }
548
- } catch (_) {
549
- // Best-effort temp cleanup only.
550
- }
551
- }
552
- }
553
- });
554
-
555
- return configSaveQueue;
556
- }
493
+ async function saveConfig(cfg = currentConfig) {
494
+ if (!cfg || typeof cfg !== 'object') return;
495
+
496
+ // Runtime/server safety valve: allow disabling disk writes entirely.
497
+ if (isAutosaveDisabled()) {
498
+ currentConfig = cfg;
499
+ if (!autosaveDisabledWarned) {
500
+ autosaveDisabledWarned = true;
501
+ console.warn('[i18ntk] Autosave disabled by I18NTK_DISABLE_AUTOSAVE. Keeping configuration in memory only.');
502
+ }
503
+ return false;
504
+ }
505
+
506
+ configSaveQueue = configSaveQueue.then(async () => {
507
+ let tempPath = null;
508
+ let releaseLock = null;
509
+ try {
510
+ // Ensure settings directory exists before any lock/file operations.
511
+ await fs.promises.mkdir(PROJECT_SETTINGS_DIR, { recursive: true });
512
+
513
+ releaseLock = await acquireConfigLock();
514
+
515
+ const serialized = JSON.stringify(cfg, null, 2);
516
+ if (typeof serialized !== 'string' || serialized.length === 0) {
517
+ throw new Error('Cannot save empty configuration payload');
518
+ }
519
+
520
+ // Use a unique temp file to avoid concurrent writer races.
521
+ // 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`;
524
+ tempPath = path.join(PROJECT_SETTINGS_DIR, tempFileName);
525
+ await fs.promises.writeFile(tempPath, serialized, 'utf8');
526
+
527
+ try {
528
+ await fs.promises.rename(tempPath, PROJECT_CONFIG_PATH);
529
+ } catch (renameError) {
530
+ // If destination dir disappeared between checks, recreate and retry once.
531
+ if (renameError && renameError.code === 'ENOENT') {
532
+ await fs.promises.mkdir(PROJECT_SETTINGS_DIR, { recursive: true });
533
+ await fs.promises.rename(tempPath, PROJECT_CONFIG_PATH);
534
+ } else {
535
+ throw renameError;
536
+ }
537
+ }
538
+
539
+ currentConfig = cfg;
540
+ return true;
541
+ } catch (error) {
542
+ console.error('[i18ntk] Error saving configuration:', error.message);
543
+ return false;
544
+ } finally {
545
+ if (releaseLock) {
546
+ await releaseLock();
547
+ }
548
+ if (tempPath) {
549
+ try {
550
+ if (SecurityUtils.safeExistsSync(tempPath, path.dirname(tempPath))) {
551
+ fs.unlinkSync(tempPath);
552
+ }
553
+ } catch (_) {
554
+ // Best-effort temp cleanup only.
555
+ }
556
+ }
557
+ }
558
+ });
559
+
560
+ return configSaveQueue;
561
+ }
557
562
 
558
563
  function getConfig() {
559
564
  // Check for recursion
@@ -575,28 +580,28 @@ function getConfig() {
575
580
  // No need to check here - handled by getUnifiedConfig
576
581
 
577
582
  // Check if config file exists
578
- if (SecurityUtils.safeExistsSync(PROJECT_CONFIG_PATH)) {
579
- const rawConfig = SecurityUtils.safeReadFileSync(PROJECT_CONFIG_PATH, path.dirname(PROJECT_CONFIG_PATH), 'utf8');
580
- const config = SecurityUtils.safeParseJSON(rawConfig);
581
- if (config && typeof config === 'object') {
582
- currentConfig = config;
583
- return resolvePaths(config);
584
- }
585
- throw new Error('Invalid project configuration JSON');
586
- }
583
+ if (SecurityUtils.safeExistsSync(PROJECT_CONFIG_PATH)) {
584
+ const rawConfig = SecurityUtils.safeReadFileSync(PROJECT_CONFIG_PATH, path.dirname(PROJECT_CONFIG_PATH), 'utf8');
585
+ const config = SecurityUtils.safeParseJSON(rawConfig);
586
+ if (config && typeof config === 'object') {
587
+ currentConfig = config;
588
+ return resolvePaths(config);
589
+ }
590
+ throw new Error('Invalid project configuration JSON');
591
+ }
587
592
 
588
593
  // Check for legacy config for migration
589
594
  if (SecurityUtils.safeExistsSync(LEGACY_CONFIG_PATH)) {
590
595
  console.log('📦 Migrating legacy configuration...');
591
- const legacyRaw = SecurityUtils.safeReadFileSync(LEGACY_CONFIG_PATH, path.dirname(LEGACY_CONFIG_PATH), 'utf8');
592
- const legacyConfig = SecurityUtils.safeParseJSON(legacyRaw);
593
- if (!legacyConfig || typeof legacyConfig !== 'object') {
594
- throw new Error('Invalid legacy configuration JSON');
595
- }
596
+ const legacyRaw = SecurityUtils.safeReadFileSync(LEGACY_CONFIG_PATH, path.dirname(LEGACY_CONFIG_PATH), 'utf8');
597
+ const legacyConfig = SecurityUtils.safeParseJSON(legacyRaw);
598
+ if (!legacyConfig || typeof legacyConfig !== 'object') {
599
+ throw new Error('Invalid legacy configuration JSON');
600
+ }
596
601
  const migratedConfig = { ...DEFAULT_CONFIG, ...legacyConfig };
597
- saveConfig(migratedConfig).catch((err) => {
598
- console.warn('[i18ntk] Warning: failed to persist migrated configuration:', err.message);
599
- });
602
+ saveConfig(migratedConfig).catch((err) => {
603
+ console.warn('[i18ntk] Warning: failed to persist migrated configuration:', err.message);
604
+ });
600
605
  currentConfig = migratedConfig;
601
606
 
602
607
  // Clean up legacy config
@@ -614,9 +619,9 @@ function getConfig() {
614
619
 
615
620
  // Use package defaults for new installation
616
621
  console.log('📦 Initializing with default configuration...');
617
- saveConfig(DEFAULT_CONFIG).catch((err) => {
618
- console.warn('[i18ntk] Warning: failed to persist default configuration:', err.message);
619
- });
622
+ saveConfig(DEFAULT_CONFIG).catch((err) => {
623
+ console.warn('[i18ntk] Warning: failed to persist default configuration:', err.message);
624
+ });
620
625
  currentConfig = DEFAULT_CONFIG;
621
626
  return resolvePaths(DEFAULT_CONFIG);
622
627
 
@@ -686,4 +691,4 @@ module.exports = {
686
691
  resolvePaths,
687
692
  toRelative,
688
693
  normalizePathValue,
689
- }
694
+ }
@@ -108,6 +108,11 @@ async function printUpgradeWarningIfOutdated({
108
108
  currentVersion,
109
109
  timeoutMs = DEFAULT_TIMEOUT_MS
110
110
  }) {
111
+ const enabled = String(process.env.I18NTK_ENABLE_UPDATE_CHECK || '').toLowerCase();
112
+ if (!(enabled === '1' || enabled === 'true' || enabled === 'yes')) {
113
+ return;
114
+ }
115
+
111
116
  if (process.env.I18NTK_DISABLE_UPDATE_CHECK === 'true') {
112
117
  return;
113
118
  }
package/utils/security.js CHANGED
@@ -27,7 +27,7 @@ function getI18n() {
27
27
  i18n = require('./i18n-helper');
28
28
  } catch (error) {
29
29
  // Fallback to simple identity function if i18n fails to load
30
- console.warn('i18n-helper not available, using fallback messages');
30
+ SecurityUtils.logSecurityEvent('i18n-helper not available, using fallback messages', 'warn');
31
31
  return { t: (key, params = {}) => key };
32
32
  }
33
33
  }
@@ -93,9 +93,12 @@ class SecurityUtils {
93
93
 
94
94
  return result;
95
95
  } catch (error) {
96
- const i18n = getI18n();
97
- console.warn(i18n.t('security.operation_error', { operation: operationName, error: error.message }));
98
- return null;
96
+ const i18n = getI18n();
97
+ SecurityUtils.logSecurityEvent('Operation error', 'error', {
98
+ operation: operationName,
99
+ error: error.message
100
+ });
101
+ return null;
99
102
  } finally {
100
103
  SecurityUtils._operationStack.delete(operationName);
101
104
  }
@@ -108,17 +111,20 @@ class SecurityUtils {
108
111
  * @param {object} details - Additional details
109
112
  */
110
113
  static logSecurityEvent(event, level = 'info', details = {}) {
111
- // Prevent recursive logging which can occur during configuration loading
112
- if (SecurityUtils._logging) {
113
- return;
114
- }
115
-
116
- SecurityUtils._logging = true;
117
- try {
118
- const cfg = getConfigManager()?.getConfig?.() || {};
119
- const envLevel = (process.env.SECURITY_LOG_LEVEL || process.env.I18NTK_SECURITY_LOG_LEVEL || '').toLowerCase();
120
- const configLevel = (cfg.security?.logLevel || cfg.security?.audit?.logLevel || '').toLowerCase();
121
- const currentLevel = envLevel || configLevel || 'warn';
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');
122
128
 
123
129
  const levels = { error: 0, warn: 1, warning: 1, info: 2 };
124
130
  const messageLevel = levels[level.toLowerCase()] ?? 2;
@@ -263,14 +269,14 @@ class SecurityUtils {
263
269
  // Read file with size limit (10MB max)
264
270
  const stats = fs.statSync(validatedPath);
265
271
  if (stats.size > 10 * 1024 * 1024) {
266
- console.warn(i18n.t('security.file_too_large', { filePath: validatedPath }));
267
- return null;
272
+ SecurityUtils.logSecurityEvent('File too large', 'warn', { filePath: validatedPath });
273
+ return null;
268
274
  }
269
275
 
270
276
  return fs.readFileSync(validatedPath, encoding);
271
277
  } catch (error) {
272
- console.warn(i18n.t('security.file_read_error', { errorMessage: error.message }));
273
- return null;
278
+ SecurityUtils.logSecurityEvent('File read error', 'error', { errorMessage: error.message });
279
+ return null;
274
280
  }
275
281
  }
276
282
 
@@ -284,7 +290,7 @@ class SecurityUtils {
284
290
  // Validate content is a string or Buffer
285
291
  if (typeof content !== 'string' && !Buffer.isBuffer(content)) {
286
292
  const i18n = getI18n();
287
- console.warn(i18n.t('security.file_write_error', { errorMessage: 'Content must be a string or Buffer' }));
293
+ SecurityUtils.logSecurityEvent('File write error: Content must be a string or Buffer', 'error');
288
294
  return false;
289
295
  }
290
296
 
@@ -292,7 +298,7 @@ class SecurityUtils {
292
298
  const contentSize = typeof content === 'string' ? content.length : content.length;
293
299
  if (contentSize > 10 * 1024 * 1024) {
294
300
  const i18n = getI18n();
295
- console.warn(i18n.t('security.content_too_large_for_file', { filePath: validatedPath }));
301
+ SecurityUtils.logSecurityEvent('Content too large for file', 'warn', { filePath: validatedPath });
296
302
  return false;
297
303
  }
298
304
 
@@ -305,7 +311,7 @@ class SecurityUtils {
305
311
  return true;
306
312
  } catch (error) {
307
313
  const i18n = getI18n();
308
- console.warn(i18n.t('security.file_write_error', { errorMessage: error.message }));
314
+ SecurityUtils.logSecurityEvent('File write error', 'error', { errorMessage: error.message });
309
315
  return false;
310
316
  }
311
317
  }
@@ -399,8 +405,8 @@ class SecurityUtils {
399
405
  const normalized = trimmed.charCodeAt(0) === 0xFEFF ? trimmed.slice(1) : trimmed;
400
406
  return JSON.parse(normalized);
401
407
  } catch (error) {
402
- console.warn(`Invalid JSON content: ${error.message}`);
403
- return fallback;
408
+ SecurityUtils.logSecurityEvent('Invalid JSON content', 'error', { error: error.message });
409
+ return fallback;
404
410
  }
405
411
  }
406
412
 
@@ -443,7 +449,7 @@ class SecurityUtils {
443
449
  const isCommonContent = sanitized.length < 1000 && !sanitized.includes('<script');
444
450
  if (!isFilePath && !isCommonContent) {
445
451
  const i18n = getI18n();
446
- console.warn(i18n.t('security.inputDisallowedCharacters'));
452
+ SecurityUtils.logSecurityEvent('Input contains disallowed characters', 'warn');
447
453
  }
448
454
  // Allow more characters for file paths and content
449
455
  sanitized = sanitized.replace(/[^a-zA-Z0-9\s\-_\.\,\!\?\(\)\[\]\{\}\:\;"'\/\\]/g, '');
@@ -456,10 +462,39 @@ class SecurityUtils {
456
462
  return crypto.createHash('sha256').update(content).digest('hex');
457
463
  }
458
464
 
465
+ static safeJoin(basePath, ...paths) {
466
+ if (!basePath || typeof basePath !== 'string') {
467
+ return false;
468
+ }
469
+ try {
470
+ const resolvedBase = path.resolve(basePath);
471
+ const joinedPath = path.join(resolvedBase, ...paths);
472
+ const resolvedPath = path.resolve(joinedPath);
473
+
474
+ // 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
+ }
483
+ return resolvedPath;
484
+ } catch (error) {
485
+ SecurityUtils.logSecurityEvent('Error in safeJoin', 'error', {
486
+ basePath,
487
+ paths,
488
+ error: error.message
489
+ });
490
+ return false;
491
+ }
492
+ }
493
+
459
494
  static isSafePath(filePath) {
460
- if (!filePath || typeof filePath !== 'string') {
461
- return false;
462
- }
495
+ if (!filePath || typeof filePath !== 'string') {
496
+ return false;
497
+ }
463
498
 
464
499
  // Allow legitimate Windows drive letter paths
465
500
  if (filePath.match(/^[A-Z]:[\/\\]/)) {
@@ -531,12 +566,12 @@ class SecurityUtils {
531
566
  'theme', 'ui', 'setup', 'reports', 'display', 'interface',
532
567
  // Security and settings
533
568
  'security', 'settings', 'preferences', 'config', 'configuration',
534
- // Additional common properties
535
- 'autoSave', 'autoBackup', 'validateOnSave', 'showWarnings', 'verbose',
536
- 'timeout', 'retries', 'batchSize', 'maxConcurrency', 'cacheEnabled',
537
- // Date and reporting options used by existing settings
538
- 'dateFormat', 'timeFormat', 'timezone', 'reportLanguage', 'dateTime'
539
- ]);
569
+ // Additional common properties
570
+ 'autoSave', 'autoBackup', 'validateOnSave', 'showWarnings', 'verbose',
571
+ 'timeout', 'retries', 'batchSize', 'maxConcurrency', 'cacheEnabled',
572
+ // Date and reporting options used by existing settings
573
+ 'dateFormat', 'timeFormat', 'timezone', 'reportLanguage', 'dateTime'
574
+ ]);
540
575
 
541
576
  // Remove unknown properties
542
577
  Object.keys(sanitized).forEach(key => {