i18ntk 2.3.5 → 2.3.7

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.5
1
+ # i18ntk v2.3.7
2
2
 
3
3
  Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, and translation completion.
4
4
 
@@ -9,16 +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.5)](https://socket.dev/npm/package/i18ntk/overview/2.3.5)
12
+ [![socket](https://socket.dev/api/badge/npm/package/i18ntk/2.3.7)](https://socket.dev/npm/package/i18ntk/overview/2.3.7)
13
13
 
14
14
  ## Upgrade Notice
15
15
 
16
- Versions earlier than `2.3.5` may contain known stability and security issues.
17
- They are considered unsupported for production use. Upgrade to `2.3.5` or newer.
18
- The CLI can check npm registry metadata and warn when your installed version is out of date.
19
- Set `I18NTK_ENABLE_UPDATE_CHECK=true` to enable this behavior.
20
- Set `I18NTK_DISABLE_UPDATE_CHECK=true` to force-disable it in restricted/offline environments.
21
- Set `I18NTK_DISABLE_AUTOSAVE=1` in server/runtime environments to keep config in memory and skip disk writes.
16
+ Versions earlier than `2.3.7` may contain known stability and security issues.
17
+ They are considered unsupported for production use. Upgrade to `2.3.7` or newer.
22
18
 
23
19
  ## What i18ntk Does
24
20
 
@@ -155,7 +151,7 @@ Example `.i18ntk-config`:
155
151
 
156
152
  ```json
157
153
  {
158
- "version": "2.3.5",
154
+ "version": "2.3.6",
159
155
  "sourceDir": "./locales",
160
156
  "i18nDir": "./locales",
161
157
  "outputDir": "./i18ntk-reports",
@@ -178,9 +174,13 @@ See [docs/api/CONFIGURATION.md](docs/api/CONFIGURATION.md) for the full configur
178
174
  - [Runtime API Guide](docs/runtime.md)
179
175
  - [Scanner Guide](docs/scanner-guide.md)
180
176
  - [Environment Variables](docs/environment-variables.md)
181
- - [Migration Guide v2.3.5](docs/migration-guide-v2.3.5.md)
177
+ - [Migration Guide v2.3.5](docs/migration-guide-v2.3.6.md)
182
178
  - [Optimization Prompt](docs/development/package-optimization-prompt.md)
183
179
 
180
+ ## Code of Conduct
181
+
182
+ We are committed to providing a friendly, safe and welcoming environment for all. Please read and respect our [Code of Conduct](CODE_OF_CONDUCT.md).
183
+
184
184
  ## License
185
185
 
186
- MIT
186
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18ntk",
3
- "version": "2.3.5",
3
+ "version": "2.3.7",
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.5",
227
+ "version": "2.3.7",
231
228
  "releaseDate": "12/04/2026",
232
229
  "lastUpdated": "12/04/2026",
233
230
  "maintainer": "Vlad Noskov",
@@ -268,7 +265,7 @@
268
265
  "spring-boot": ">=2.5.0",
269
266
  "laravel": ">=8.0.0"
270
267
  },
271
- "supportPolicy": "Versions earlier than 2.3.5 may be unstable or insecure. Upgrade to 2.3.5 or newer."
268
+ "supportPolicy": "Versions earlier than 2.3.7 may be unstable or insecure. Upgrade to 2.3.6 or newer."
272
269
  },
273
270
  "_comment": "This package is zero-dependency and uses only native Node.js modules"
274
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",
@@ -573,8 +574,8 @@ class SettingsManager {
573
574
 
574
575
  // 2. Remove actual configuration files used by the system
575
576
  const packageDir = path.resolve(__dirname, '..');
576
- const projectRoot = process.cwd();
577
- const settingsDir = path.join(packageDir, 'settings');
577
+ const projectRoot = path.resolve(process.cwd());
578
+ const settingsDir = SecurityUtils.safeJoin(packageDir, 'settings');
578
579
 
579
580
  // Main configuration file
580
581
  const mainConfigPath = path.join(settingsDir, 'i18ntk-config.json');
@@ -611,7 +612,7 @@ class SettingsManager {
611
612
  path.join(packageDir, '.i18n-admin-config.json'),
612
613
  path.join(settingsDir, '.i18n-admin-config.json'),
613
614
  path.join(settingsDir, 'admin-config.json'),
614
- path.join(projectRoot, '.i18n-admin-config.json')
615
+ SecurityUtils.safeJoin(projectRoot, '.i18n-admin-config.json') || path.join(projectRoot, '.i18n-admin-config.json')
615
616
  ];
616
617
 
617
618
  for (const adminConfigPath of adminConfigPaths) {
@@ -653,11 +654,11 @@ class SettingsManager {
653
654
  path.join(settingsDir, '.temp-config.json'),
654
655
  path.join(settingsDir, '.last-config.json'),
655
656
  path.join(settingsDir, '.lock'),
656
- path.join(settingsDir, 'i18ntk-config.json.tmp'),
657
+ path.join(settingsDir, '.i18ntk-config.temp-*'),
657
658
  path.join(settingsDir, 'settings.lock'),
658
659
  path.join(packageDir, '.env-config.json'),
659
660
  path.join(packageDir, '.temp-config.json'),
660
- path.join(packageDir, 'config.tmp'),
661
+ path.join(packageDir, 'config.temp-*'),
661
662
  path.join(packageDir, '.lock')
662
663
  ];
663
664
 
@@ -1002,4 +1003,4 @@ class SettingsManager {
1002
1003
  }
1003
1004
 
1004
1005
  module.exports = SettingsManager;
1005
-
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,76 @@ 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
+ // Use a simpler naming pattern to avoid triggering security warnings
523
+ const nonce = `${process.pid}.${Date.now()}`;
524
+ const tempFileName = `.i18ntk-config.temp-${nonce}`;
525
+ tempPath = path.join(PROJECT_SETTINGS_DIR, tempFileName);
526
+ await fs.promises.writeFile(tempPath, serialized, 'utf8');
527
+
528
+ try {
529
+ await fs.promises.rename(tempPath, PROJECT_CONFIG_PATH);
530
+ } catch (renameError) {
531
+ // If destination dir disappeared between checks, recreate and retry once.
532
+ if (renameError && renameError.code === 'ENOENT') {
533
+ await fs.promises.mkdir(PROJECT_SETTINGS_DIR, { recursive: true });
534
+ await fs.promises.rename(tempPath, PROJECT_CONFIG_PATH);
535
+ } else {
536
+ throw renameError;
537
+ }
538
+ }
539
+
540
+ currentConfig = cfg;
541
+ return true;
542
+ } catch (error) {
543
+ console.error('[i18ntk] Error saving configuration:', error.message);
544
+ return false;
545
+ } finally {
546
+ if (releaseLock) {
547
+ await releaseLock();
548
+ }
549
+ if (tempPath) {
550
+ try {
551
+ if (SecurityUtils.safeExistsSync(tempPath, path.dirname(tempPath))) {
552
+ fs.unlinkSync(tempPath);
553
+ }
554
+ } catch (_) {
555
+ // Best-effort temp cleanup only.
556
+ }
557
+ }
558
+ }
559
+ });
560
+
561
+ return configSaveQueue;
562
+ }
557
563
 
558
564
  function getConfig() {
559
565
  // Check for recursion
@@ -575,28 +581,28 @@ function getConfig() {
575
581
  // No need to check here - handled by getUnifiedConfig
576
582
 
577
583
  // 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
- }
584
+ if (SecurityUtils.safeExistsSync(PROJECT_CONFIG_PATH)) {
585
+ const rawConfig = SecurityUtils.safeReadFileSync(PROJECT_CONFIG_PATH, path.dirname(PROJECT_CONFIG_PATH), 'utf8');
586
+ const config = SecurityUtils.safeParseJSON(rawConfig);
587
+ if (config && typeof config === 'object') {
588
+ currentConfig = config;
589
+ return resolvePaths(config);
590
+ }
591
+ throw new Error('Invalid project configuration JSON');
592
+ }
587
593
 
588
594
  // Check for legacy config for migration
589
595
  if (SecurityUtils.safeExistsSync(LEGACY_CONFIG_PATH)) {
590
596
  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
- }
597
+ const legacyRaw = SecurityUtils.safeReadFileSync(LEGACY_CONFIG_PATH, path.dirname(LEGACY_CONFIG_PATH), 'utf8');
598
+ const legacyConfig = SecurityUtils.safeParseJSON(legacyRaw);
599
+ if (!legacyConfig || typeof legacyConfig !== 'object') {
600
+ throw new Error('Invalid legacy configuration JSON');
601
+ }
596
602
  const migratedConfig = { ...DEFAULT_CONFIG, ...legacyConfig };
597
- saveConfig(migratedConfig).catch((err) => {
598
- console.warn('[i18ntk] Warning: failed to persist migrated configuration:', err.message);
599
- });
603
+ saveConfig(migratedConfig).catch((err) => {
604
+ console.warn('[i18ntk] Warning: failed to persist migrated configuration:', err.message);
605
+ });
600
606
  currentConfig = migratedConfig;
601
607
 
602
608
  // Clean up legacy config
@@ -614,9 +620,9 @@ function getConfig() {
614
620
 
615
621
  // Use package defaults for new installation
616
622
  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
- });
623
+ saveConfig(DEFAULT_CONFIG).catch((err) => {
624
+ console.warn('[i18ntk] Warning: failed to persist default configuration:', err.message);
625
+ });
620
626
  currentConfig = DEFAULT_CONFIG;
621
627
  return resolvePaths(DEFAULT_CONFIG);
622
628
 
@@ -686,4 +692,4 @@ module.exports = {
686
692
  resolvePaths,
687
693
  toRelative,
688
694
  normalizePathValue,
689
- }
695
+ }
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
  }
@@ -41,9 +41,21 @@ function getI18n() {
41
41
  */
42
42
  class SecurityUtils {
43
43
 
44
- // Static properties for operation tracking
45
- static _operationStack = new Set();
46
- static _logging = false;
44
+ // Whitelist patterns for our own package artifacts
45
+ static PACKAGE_ARTIFACT_WHITELIST = [
46
+ /\.i18ntk-config\.temp-\d+\.\d+$/, // .i18ntk-config.temp-1234.5678
47
+ /\.i18ntk-config\.\d+\.\d+\.tmp$/, // Legacy pattern: .i18ntk-config.1234.5678.tmp
48
+ /config\.temp-\d+\.\d+$/, // config.temp-1234.5678
49
+ /config\.\d+\.\d+\.tmp$/, // Legacy pattern: config.1234.5678.tmp
50
+ /\.temp-config\.json$/, // .temp-config.json
51
+ /\.last-config\.json$/, // .last-config.json
52
+ /\.lock$/, // .lock files
53
+ /settings\.lock$/ // settings.lock
54
+ ];
55
+
56
+ // Static properties for operation tracking
57
+ static _operationStack = new Set();
58
+ static _logging = false;
47
59
 
48
60
  constructor() {
49
61
  // Instance constructor - static properties are already initialized
@@ -93,9 +105,12 @@ class SecurityUtils {
93
105
 
94
106
  return result;
95
107
  } catch (error) {
96
- const i18n = getI18n();
97
- console.warn(i18n.t('security.operation_error', { operation: operationName, error: error.message }));
98
- return null;
108
+ const i18n = getI18n();
109
+ SecurityUtils.logSecurityEvent('Operation error', 'error', {
110
+ operation: operationName,
111
+ error: error.message
112
+ });
113
+ return null;
99
114
  } finally {
100
115
  SecurityUtils._operationStack.delete(operationName);
101
116
  }
@@ -108,17 +123,20 @@ class SecurityUtils {
108
123
  * @param {object} details - Additional details
109
124
  */
110
125
  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';
126
+ // Prevent recursive logging which can occur during configuration loading
127
+ if (SecurityUtils._logging) {
128
+ return;
129
+ }
130
+
131
+ SecurityUtils._logging = true;
132
+ try {
133
+ const cfg = getConfigManager()?.getConfig?.() || {};
134
+ const envLevel = (process.env.SECURITY_LOG_LEVEL || process.env.I18NTK_SECURITY_LOG_LEVEL || '').toLowerCase();
135
+ const configLevel = (cfg.security?.logLevel || cfg.security?.audit?.logLevel || '').toLowerCase();
136
+
137
+ // Check for debug mode
138
+ const debugMode = process.env.I18N_DEBUG === 'true' || process.env.DEBUG === 'true';
139
+ const currentLevel = debugMode ? 'info' : (envLevel || configLevel || 'warn');
122
140
 
123
141
  const levels = { error: 0, warn: 1, warning: 1, info: 2 };
124
142
  const messageLevel = levels[level.toLowerCase()] ?? 2;
@@ -158,7 +176,12 @@ class SecurityUtils {
158
176
  const useI18n = i18n && i18n.isInitialized && typeof i18n.t === 'function';
159
177
 
160
178
  try {
161
- if (!filePath || typeof filePath !== 'string') {
179
+ // Check against whitelist patterns for our own package artifacts
180
+ if (SecurityUtils.PACKAGE_ARTIFACT_WHITELIST.some(pattern => pattern.test(filePath))) {
181
+ return filePath;
182
+ }
183
+
184
+ if (!filePath || typeof filePath !== 'string') {
162
185
  const message = useI18n
163
186
  ? i18n.t('security.pathValidationFailed')
164
187
  : 'Path validation failed';
@@ -263,14 +286,14 @@ class SecurityUtils {
263
286
  // Read file with size limit (10MB max)
264
287
  const stats = fs.statSync(validatedPath);
265
288
  if (stats.size > 10 * 1024 * 1024) {
266
- console.warn(i18n.t('security.file_too_large', { filePath: validatedPath }));
267
- return null;
289
+ SecurityUtils.logSecurityEvent('File too large', 'warn', { filePath: validatedPath });
290
+ return null;
268
291
  }
269
292
 
270
293
  return fs.readFileSync(validatedPath, encoding);
271
294
  } catch (error) {
272
- console.warn(i18n.t('security.file_read_error', { errorMessage: error.message }));
273
- return null;
295
+ SecurityUtils.logSecurityEvent('File read error', 'error', { errorMessage: error.message });
296
+ return null;
274
297
  }
275
298
  }
276
299
 
@@ -284,7 +307,7 @@ class SecurityUtils {
284
307
  // Validate content is a string or Buffer
285
308
  if (typeof content !== 'string' && !Buffer.isBuffer(content)) {
286
309
  const i18n = getI18n();
287
- console.warn(i18n.t('security.file_write_error', { errorMessage: 'Content must be a string or Buffer' }));
310
+ SecurityUtils.logSecurityEvent('File write error: Content must be a string or Buffer', 'error');
288
311
  return false;
289
312
  }
290
313
 
@@ -292,7 +315,7 @@ class SecurityUtils {
292
315
  const contentSize = typeof content === 'string' ? content.length : content.length;
293
316
  if (contentSize > 10 * 1024 * 1024) {
294
317
  const i18n = getI18n();
295
- console.warn(i18n.t('security.content_too_large_for_file', { filePath: validatedPath }));
318
+ SecurityUtils.logSecurityEvent('Content too large for file', 'warn', { filePath: validatedPath });
296
319
  return false;
297
320
  }
298
321
 
@@ -305,7 +328,7 @@ class SecurityUtils {
305
328
  return true;
306
329
  } catch (error) {
307
330
  const i18n = getI18n();
308
- console.warn(i18n.t('security.file_write_error', { errorMessage: error.message }));
331
+ SecurityUtils.logSecurityEvent('File write error', 'error', { errorMessage: error.message });
309
332
  return false;
310
333
  }
311
334
  }
@@ -399,8 +422,8 @@ class SecurityUtils {
399
422
  const normalized = trimmed.charCodeAt(0) === 0xFEFF ? trimmed.slice(1) : trimmed;
400
423
  return JSON.parse(normalized);
401
424
  } catch (error) {
402
- console.warn(`Invalid JSON content: ${error.message}`);
403
- return fallback;
425
+ SecurityUtils.logSecurityEvent('Invalid JSON content', 'error', { error: error.message });
426
+ return fallback;
404
427
  }
405
428
  }
406
429
 
@@ -443,7 +466,7 @@ class SecurityUtils {
443
466
  const isCommonContent = sanitized.length < 1000 && !sanitized.includes('<script');
444
467
  if (!isFilePath && !isCommonContent) {
445
468
  const i18n = getI18n();
446
- console.warn(i18n.t('security.inputDisallowedCharacters'));
469
+ SecurityUtils.logSecurityEvent('Input contains disallowed characters', 'warn');
447
470
  }
448
471
  // Allow more characters for file paths and content
449
472
  sanitized = sanitized.replace(/[^a-zA-Z0-9\s\-_\.\,\!\?\(\)\[\]\{\}\:\;"'\/\\]/g, '');
@@ -456,10 +479,44 @@ class SecurityUtils {
456
479
  return crypto.createHash('sha256').update(content).digest('hex');
457
480
  }
458
481
 
482
+ static safeJoin(basePath, ...paths) {
483
+ if (!basePath || typeof basePath !== 'string') {
484
+ return false;
485
+ }
486
+ try {
487
+ const resolvedBase = path.resolve(basePath);
488
+ const joinedPath = path.join(resolvedBase, ...paths);
489
+ const resolvedPath = path.resolve(joinedPath);
490
+
491
+ // Ensure the final path is within the base directory
492
+ if (!resolvedPath.startsWith(resolvedBase)) {
493
+ SecurityUtils.logSecurityEvent('Path traversal attempt detected in safeJoin', 'error', {
494
+ basePath,
495
+ paths,
496
+ resolvedPath
497
+ });
498
+ return false;
499
+ }
500
+ return resolvedPath;
501
+ } catch (error) {
502
+ SecurityUtils.logSecurityEvent('Error in safeJoin', 'error', {
503
+ basePath,
504
+ paths,
505
+ error: error.message
506
+ });
507
+ return false;
508
+ }
509
+ }
510
+
459
511
  static isSafePath(filePath) {
460
- if (!filePath || typeof filePath !== 'string') {
461
- return false;
462
- }
512
+ if (!filePath || typeof filePath !== 'string') {
513
+ return false;
514
+ }
515
+
516
+ // Check against whitelist patterns for our own package artifacts
517
+ if (SecurityUtils.PACKAGE_ARTIFACT_WHITELIST.some(pattern => pattern.test(filePath))) {
518
+ return true;
519
+ }
463
520
 
464
521
  // Allow legitimate Windows drive letter paths
465
522
  if (filePath.match(/^[A-Z]:[\/\\]/)) {
@@ -531,12 +588,12 @@ class SecurityUtils {
531
588
  'theme', 'ui', 'setup', 'reports', 'display', 'interface',
532
589
  // Security and settings
533
590
  '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
- ]);
591
+ // Additional common properties
592
+ 'autoSave', 'autoBackup', 'validateOnSave', 'showWarnings', 'verbose',
593
+ 'timeout', 'retries', 'batchSize', 'maxConcurrency', 'cacheEnabled',
594
+ // Date and reporting options used by existing settings
595
+ 'dateFormat', 'timeFormat', 'timezone', 'reportLanguage', 'dateTime'
596
+ ]);
540
597
 
541
598
  // Remove unknown properties
542
599
  Object.keys(sanitized).forEach(key => {