i18ntk 2.3.5 → 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.5
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,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.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.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.6` may contain known stability and security issues.
17
+ They are considered unsupported for production use. Upgrade to `2.3.6` 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,7 +174,7 @@ 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
 
184
180
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18ntk",
3
- "version": "2.3.5",
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",
@@ -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.6 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) {
@@ -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,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
+ }
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 => {