i18ntk 1.10.2 → 2.0.3

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.
Files changed (108) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +141 -1191
  3. package/main/i18ntk-analyze.js +65 -84
  4. package/main/i18ntk-backup-class.js +420 -0
  5. package/main/i18ntk-backup.js +3 -3
  6. package/main/i18ntk-complete.js +90 -65
  7. package/main/i18ntk-doctor.js +123 -103
  8. package/main/i18ntk-fixer.js +61 -725
  9. package/main/i18ntk-go.js +14 -15
  10. package/main/i18ntk-init.js +77 -26
  11. package/main/i18ntk-java.js +27 -32
  12. package/main/i18ntk-js.js +70 -68
  13. package/main/i18ntk-manage.js +129 -30
  14. package/main/i18ntk-php.js +75 -75
  15. package/main/i18ntk-py.js +55 -56
  16. package/main/i18ntk-scanner.js +59 -57
  17. package/main/i18ntk-setup.js +9 -404
  18. package/main/i18ntk-sizing.js +6 -6
  19. package/main/i18ntk-summary.js +21 -18
  20. package/main/i18ntk-ui.js +11 -10
  21. package/main/i18ntk-usage.js +54 -18
  22. package/main/i18ntk-validate.js +13 -13
  23. package/main/manage/commands/AnalyzeCommand.js +1124 -0
  24. package/main/manage/commands/BackupCommand.js +62 -0
  25. package/main/manage/commands/CommandRouter.js +295 -0
  26. package/main/manage/commands/CompleteCommand.js +61 -0
  27. package/main/manage/commands/DoctorCommand.js +60 -0
  28. package/main/manage/commands/FixerCommand.js +624 -0
  29. package/main/manage/commands/InitCommand.js +62 -0
  30. package/main/manage/commands/ScannerCommand.js +654 -0
  31. package/main/manage/commands/SizingCommand.js +60 -0
  32. package/main/manage/commands/SummaryCommand.js +61 -0
  33. package/main/manage/commands/UsageCommand.js +60 -0
  34. package/main/manage/commands/ValidateCommand.js +978 -0
  35. package/main/manage/index-fixed.js +1447 -0
  36. package/main/manage/index.js +1462 -0
  37. package/main/manage/managers/DebugMenu.js +140 -0
  38. package/main/manage/managers/InteractiveMenu.js +177 -0
  39. package/main/manage/managers/LanguageMenu.js +62 -0
  40. package/main/manage/managers/SettingsMenu.js +53 -0
  41. package/main/manage/services/AuthenticationService.js +263 -0
  42. package/main/manage/services/ConfigurationService-fixed.js +449 -0
  43. package/main/manage/services/ConfigurationService.js +449 -0
  44. package/main/manage/services/FileManagementService.js +368 -0
  45. package/main/manage/services/FrameworkDetectionService.js +458 -0
  46. package/main/manage/services/InitService.js +1051 -0
  47. package/main/manage/services/SetupService.js +462 -0
  48. package/main/manage/services/SummaryService.js +450 -0
  49. package/main/manage/services/UsageService.js +1502 -0
  50. package/package.json +32 -29
  51. package/runtime/enhanced.d.ts +221 -221
  52. package/runtime/index.d.ts +29 -29
  53. package/runtime/index.full.d.ts +331 -331
  54. package/runtime/index.js +7 -6
  55. package/scripts/build-lite.js +17 -17
  56. package/scripts/deprecate-versions.js +23 -6
  57. package/scripts/export-translations.js +5 -5
  58. package/scripts/fix-all-i18n.js +3 -3
  59. package/scripts/fix-and-purify-i18n.js +3 -2
  60. package/scripts/fix-locale-control-chars.js +110 -0
  61. package/scripts/lint-locales.js +80 -0
  62. package/scripts/locale-optimizer.js +8 -8
  63. package/scripts/prepublish.js +21 -21
  64. package/scripts/security-check.js +117 -117
  65. package/scripts/sync-translations.js +4 -4
  66. package/scripts/sync-ui-locales.js +9 -8
  67. package/scripts/validate-all-translations.js +8 -7
  68. package/scripts/verify-deprecations.js +157 -161
  69. package/scripts/verify-translations.js +6 -5
  70. package/settings/i18ntk-config.json +282 -282
  71. package/settings/language-config.json +5 -5
  72. package/settings/settings-cli.js +9 -9
  73. package/settings/settings-manager.js +18 -18
  74. package/ui-locales/de.json +2417 -2348
  75. package/ui-locales/en.json +2415 -2352
  76. package/ui-locales/es.json +2425 -2353
  77. package/ui-locales/fr.json +2418 -2348
  78. package/ui-locales/ja.json +2463 -2361
  79. package/ui-locales/ru.json +2463 -2359
  80. package/ui-locales/zh.json +2418 -2351
  81. package/utils/admin-auth.js +2 -2
  82. package/utils/admin-cli.js +297 -297
  83. package/utils/admin-pin.js +9 -9
  84. package/utils/cli-helper.js +9 -9
  85. package/utils/config-helper.js +73 -104
  86. package/utils/config-manager.js +204 -171
  87. package/utils/config.js +5 -4
  88. package/utils/env-manager.js +249 -263
  89. package/utils/framework-detector.js +27 -24
  90. package/utils/i18n-helper.js +85 -41
  91. package/utils/init-helper.js +152 -94
  92. package/utils/json-output.js +98 -98
  93. package/utils/mini-commander.js +179 -0
  94. package/utils/missing-key-validator.js +5 -5
  95. package/utils/plugin-loader.js +40 -29
  96. package/utils/prompt.js +14 -44
  97. package/utils/safe-json.js +40 -0
  98. package/utils/secure-errors.js +3 -3
  99. package/utils/security-check-improved.js +390 -0
  100. package/utils/security-config.js +5 -5
  101. package/utils/security-fixed.js +607 -0
  102. package/utils/security.js +652 -602
  103. package/utils/setup-enforcer.js +136 -44
  104. package/utils/setup-validator.js +33 -32
  105. package/utils/ultra-performance-optimizer.js +11 -9
  106. package/utils/watch-locales.js +2 -1
  107. package/utils/prompt-fixed.js +0 -55
  108. package/utils/security-check.js +0 -454
@@ -1,99 +1,99 @@
1
- /**
2
- * JSON Output Utility for i18ntk commands
3
- * Provides consistent machine-readable output format for CI/CD integration
4
- */
5
-
6
- class JsonOutput {
7
- constructor(command) {
8
- this.command = command;
9
- this.version = this.getPackageVersion();
10
- this.startTime = Date.now();
11
- this.data = {
12
- command: this.command,
13
- version: this.version,
14
- status: 'ok',
15
- stats: {},
16
- issues: [],
17
- metadata: {
18
- timestamp: new Date().toISOString(),
19
- duration: 0
20
- }
21
- };
22
- }
23
-
24
- getPackageVersion() {
25
- try {
26
- const packageJson = require('../package.json');
27
- return packageJson.version;
28
- } catch (error) {
29
- return '1.8.3';
30
- }
31
- }
32
-
33
- /**
34
- * Set the overall status
35
- * @param {'ok'|'warn'|'error'} status
36
- */
37
- setStatus(status) {
38
- this.data.status = status;
39
- }
40
-
41
- /**
42
- * Add statistics to the output
43
- * @param {Object} stats
44
- */
45
- setStats(stats) {
46
- this.data.stats = { ...this.data.stats, ...stats };
47
- }
48
-
49
- /**
50
- * Add an issue to the output
51
- * @param {Object} issue
52
- */
53
- addIssue(issue) {
54
- this.data.issues.push({
55
- file: issue.file || '',
56
- key: issue.key || '',
57
- type: issue.type || 'unknown',
58
- message: issue.message || '',
59
- severity: issue.severity || 'info'
60
- });
61
- }
62
-
63
- /**
64
- * Add metadata information
65
- * @param {Object} metadata
66
- */
67
- addMetadata(metadata) {
68
- this.data.metadata = { ...this.data.metadata, ...metadata };
69
- }
70
-
71
- /**
72
- * Finalize and output the JSON
73
- */
74
- output() {
75
- this.data.metadata.duration = Date.now() - this.startTime;
76
-
77
- if (process.env.NODE_ENV !== 'test') {
78
- console.log(JSON.stringify(this.data, null, 2));
79
- }
80
-
81
- return this.data;
82
- }
83
-
84
- /**
85
- * Output error in JSON format
86
- * @param {Error} error
87
- */
88
- outputError(error) {
89
- this.setStatus('error');
90
- this.addIssue({
91
- type: 'error',
92
- message: error.message,
93
- severity: 'error'
94
- });
95
- this.output();
96
- }
97
- }
98
-
1
+ /**
2
+ * JSON Output Utility for i18ntk commands
3
+ * Provides consistent machine-readable output format for CI/CD integration
4
+ */
5
+
6
+ class JsonOutput {
7
+ constructor(command) {
8
+ this.command = command;
9
+ this.version = this.getPackageVersion();
10
+ this.startTime = Date.now();
11
+ this.data = {
12
+ command: this.command,
13
+ version: this.version,
14
+ status: 'ok',
15
+ stats: {},
16
+ issues: [],
17
+ metadata: {
18
+ timestamp: new Date().toISOString(),
19
+ duration: 0
20
+ }
21
+ };
22
+ }
23
+
24
+ getPackageVersion() {
25
+ try {
26
+ const packageJson = require('../package.json');
27
+ return packageJson.version;
28
+ } catch (error) {
29
+ return '1.8.3';
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Set the overall status
35
+ * @param {'ok'|'warn'|'error'} status
36
+ */
37
+ setStatus(status) {
38
+ this.data.status = status;
39
+ }
40
+
41
+ /**
42
+ * Add statistics to the output
43
+ * @param {Object} stats
44
+ */
45
+ setStats(stats) {
46
+ this.data.stats = { ...this.data.stats, ...stats };
47
+ }
48
+
49
+ /**
50
+ * Add an issue to the output
51
+ * @param {Object} issue
52
+ */
53
+ addIssue(issue) {
54
+ this.data.issues.push({
55
+ file: issue.file || '',
56
+ key: issue.key || '',
57
+ type: issue.type || 'unknown',
58
+ message: issue.message || '',
59
+ severity: issue.severity || 'info'
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Add metadata information
65
+ * @param {Object} metadata
66
+ */
67
+ addMetadata(metadata) {
68
+ this.data.metadata = { ...this.data.metadata, ...metadata };
69
+ }
70
+
71
+ /**
72
+ * Finalize and output the JSON
73
+ */
74
+ output() {
75
+ this.data.metadata.duration = Date.now() - this.startTime;
76
+
77
+ if (process.env.NODE_ENV !== 'test') {
78
+ console.log(JSON.stringify(this.data, null, 2));
79
+ }
80
+
81
+ return this.data;
82
+ }
83
+
84
+ /**
85
+ * Output error in JSON format
86
+ * @param {Error} error
87
+ */
88
+ outputError(error) {
89
+ this.setStatus('error');
90
+ this.addIssue({
91
+ type: 'error',
92
+ message: error.message,
93
+ severity: 'error'
94
+ });
95
+ this.output();
96
+ }
97
+ }
98
+
99
99
  module.exports = JsonOutput;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Minimal zero-dependency subset of commander used by i18ntk language CLIs.
3
+ */
4
+
5
+ function toCamelCase(input) {
6
+ return String(input || '').replace(/-([a-z])/g, (_, char) => char.toUpperCase());
7
+ }
8
+
9
+ function parseOptionDefinition(flags, defaultValue) {
10
+ const longMatch = flags.match(/--([a-zA-Z0-9-]+)/);
11
+ const shortMatch = flags.match(/(^|\s)-([a-zA-Z])(\s|,|$)/);
12
+ const hasValue = /<[^>]+>/.test(flags);
13
+ const longName = longMatch ? longMatch[1] : null;
14
+ const shortName = shortMatch ? shortMatch[2] : null;
15
+ const name = toCamelCase(longName || shortName || '');
16
+
17
+ return {
18
+ flags,
19
+ hasValue,
20
+ defaultValue,
21
+ longFlag: longName ? `--${longName}` : null,
22
+ shortFlag: shortName ? `-${shortName}` : null,
23
+ name
24
+ };
25
+ }
26
+
27
+ function parseOptions(args, optionDefs) {
28
+ const options = {};
29
+
30
+ for (const def of optionDefs) {
31
+ if (def.defaultValue !== undefined) {
32
+ options[def.name] = def.defaultValue;
33
+ } else if (!def.hasValue) {
34
+ options[def.name] = false;
35
+ }
36
+ }
37
+
38
+ for (let i = 0; i < args.length; i++) {
39
+ const arg = args[i];
40
+ if (!arg || !arg.startsWith('-')) {
41
+ continue;
42
+ }
43
+
44
+ let matchedDef = null;
45
+ let inlineValue;
46
+
47
+ if (arg.startsWith('--')) {
48
+ const [flag, value] = arg.split('=', 2);
49
+ matchedDef = optionDefs.find(def => def.longFlag === flag);
50
+ inlineValue = value;
51
+ } else {
52
+ matchedDef = optionDefs.find(def => def.shortFlag === arg);
53
+ }
54
+
55
+ if (!matchedDef) {
56
+ continue;
57
+ }
58
+
59
+ if (!matchedDef.hasValue) {
60
+ options[matchedDef.name] = true;
61
+ continue;
62
+ }
63
+
64
+ let value = inlineValue;
65
+ if (value === undefined) {
66
+ const next = args[i + 1];
67
+ if (next !== undefined && !String(next).startsWith('-')) {
68
+ value = next;
69
+ i++;
70
+ }
71
+ }
72
+
73
+ options[matchedDef.name] = value !== undefined ? value : '';
74
+ }
75
+
76
+ return options;
77
+ }
78
+
79
+ class MiniCommand {
80
+ constructor(name) {
81
+ this._name = name;
82
+ this._description = '';
83
+ this._options = [];
84
+ this._action = null;
85
+ }
86
+
87
+ description(text) {
88
+ this._description = text;
89
+ return this;
90
+ }
91
+
92
+ option(flags, _description, defaultValue) {
93
+ this._options.push(parseOptionDefinition(flags, defaultValue));
94
+ return this;
95
+ }
96
+
97
+ action(handler) {
98
+ this._action = handler;
99
+ return this;
100
+ }
101
+
102
+ execute(args) {
103
+ const parsed = parseOptions(args, this._options);
104
+ if (typeof this._action === 'function') {
105
+ const result = this._action(parsed);
106
+ if (result && typeof result.then === 'function') {
107
+ result.catch(error => {
108
+ console.error(error && error.message ? error.message : String(error));
109
+ process.exit(1);
110
+ });
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ class MiniProgram {
117
+ constructor() {
118
+ this._name = '';
119
+ this._description = '';
120
+ this._version = '';
121
+ this._options = [];
122
+ this._commands = [];
123
+ this._opts = {};
124
+ }
125
+
126
+ name(value) {
127
+ this._name = value;
128
+ return this;
129
+ }
130
+
131
+ description(value) {
132
+ this._description = value;
133
+ return this;
134
+ }
135
+
136
+ version(value) {
137
+ this._version = value;
138
+ return this;
139
+ }
140
+
141
+ option(flags, _description, defaultValue) {
142
+ this._options.push(parseOptionDefinition(flags, defaultValue));
143
+ return this;
144
+ }
145
+
146
+ command(name) {
147
+ const command = new MiniCommand(name);
148
+ this._commands.push(command);
149
+ return command;
150
+ }
151
+
152
+ opts() {
153
+ return { ...this._opts };
154
+ }
155
+
156
+ parse(argv = process.argv) {
157
+ const args = argv.slice(2);
158
+
159
+ if (this._commands.length > 0 && args.length > 0 && !args[0].startsWith('-')) {
160
+ const command = this._commands.find(item => item._name === args[0]);
161
+ if (command) {
162
+ command.execute(args.slice(1));
163
+ return this;
164
+ }
165
+ }
166
+
167
+ this._opts = parseOptions(args, this._options);
168
+ return this;
169
+ }
170
+ }
171
+
172
+ function createProgram() {
173
+ return new MiniProgram();
174
+ }
175
+
176
+ module.exports = {
177
+ createProgram,
178
+ program: createProgram()
179
+ };
@@ -775,14 +775,14 @@ class MissingKeyValidator {
775
775
  }
776
776
 
777
777
  async validateLanguageKeys(language) {
778
- const localePath = path.join(this.projectRoot, 'ui-locales', `${language}.json`);
778
+ const localePath = path.join(this.projectRoot, 'resources', 'i18n', 'ui-locales', `${language}.json`);
779
779
 
780
- if (!fs.existsSync(localePath)) {
780
+ if (!SecurityUtils.safeExistsSync(localePath)) {
781
781
  return this.requiredKeys; // All keys missing if file doesn't exist
782
782
  }
783
783
 
784
784
  try {
785
- const localeData = JSON.parse(fs.readFileSync(localePath, 'utf8'));
785
+ const localeData = JSON.parse(SecurityUtils.safeReadFileSync(localePath, 'utf8'));
786
786
  const existingKeys = Object.keys(localeData);
787
787
 
788
788
  return this.requiredKeys.filter(key => !existingKeys.includes(key));
@@ -807,7 +807,7 @@ class MissingKeyValidator {
807
807
  };
808
808
 
809
809
  const reportPath = path.join(__dirname, 'missing-keys-report.json');
810
- fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
810
+ SecurityUtils.safeWriteFileSync(reportPath, JSON.stringify(report, null, 2));
811
811
 
812
812
  console.log('\n📊 Missing Keys Report:');
813
813
  console.log(`Total Languages: ${report.summary.totalLanguages}`);
@@ -837,7 +837,7 @@ class MissingKeyValidator {
837
837
  });
838
838
 
839
839
  const templatePath = path.join(__dirname, 'translation-template.json');
840
- fs.writeFileSync(templatePath, JSON.stringify(template, null, 2));
840
+ SecurityUtils.safeWriteFileSync(templatePath, JSON.stringify(template, null, 2));
841
841
 
842
842
  console.log(`Translation template saved to: ${templatePath}`);
843
843
  return template;
@@ -1,6 +1,20 @@
1
- const path = require('path');
1
+ function loadOptionalModule(name) {
2
+ // Security hardening: allow only known built-in optional plugins.
3
+ // This avoids dynamic runtime require of arbitrary package names.
4
+ const builtinPlugins = {
5
+ regex: () => require('./extractors/regex'),
6
+ 'i18ntk-extractor-regex': () => require('./extractors/regex')
7
+ };
8
+
9
+ const direct = builtinPlugins[name];
10
+ if (direct) {
11
+ try {
12
+ return direct();
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
2
17
 
3
- function loadOptionalModule(name, cwd = process.cwd()) {
4
18
  // Sanitize the module name to prevent path traversal
5
19
  const sanitizedName = name.replace(/[^a-zA-Z0-9@/_-]/g, '');
6
20
  if (sanitizedName !== name) {
@@ -8,31 +22,28 @@ function loadOptionalModule(name, cwd = process.cwd()) {
8
22
  return null;
9
23
  }
10
24
 
11
- try {
12
- const resolved = require.resolve(sanitizedName, { paths: [cwd] });
13
- return require(resolved);
14
- } catch {
15
- return null;
16
- }
25
+ // Unknown optional modules are disabled by default.
26
+ // Register plugins programmatically through PluginLoader.registerPlugin instead.
27
+ return null;
17
28
  }
18
- class PluginLoader {
19
- constructor() {
20
- this.plugins = {};
21
- }
22
-
23
- registerPlugin(plugin) {
24
- if (!plugin || !plugin.type) return;
25
- const type = plugin.type;
26
- if (!this.plugins[type]) {
27
- this.plugins[type] = [];
28
- }
29
- this.plugins[type].push(plugin);
30
- }
31
-
32
- getPlugins(type) {
33
- return this.plugins[type] || [];
34
- }
35
- }
36
-
37
- module.exports = PluginLoader;
38
- module.exports.loadOptionalModule = loadOptionalModule;
29
+ class PluginLoader {
30
+ constructor() {
31
+ this.plugins = {};
32
+ }
33
+
34
+ registerPlugin(plugin) {
35
+ if (!plugin || !plugin.type) return;
36
+ const type = plugin.type;
37
+ if (!this.plugins[type]) {
38
+ this.plugins[type] = [];
39
+ }
40
+ this.plugins[type].push(plugin);
41
+ }
42
+
43
+ getPlugins(type) {
44
+ return this.plugins[type] || [];
45
+ }
46
+ }
47
+
48
+ module.exports = PluginLoader;
49
+ module.exports.loadOptionalModule = loadOptionalModule;
package/utils/prompt.js CHANGED
@@ -1,9 +1,6 @@
1
- const readline = require('node:readline');
2
- const { stdin: input, stdout: output } = require('node:process');
1
+ const readline = require('readline');
2
+ const { logger } = require('./logger');
3
3
 
4
- /**
5
- * Simple prompt utility for CLI interactions
6
- */
7
4
  class Prompt {
8
5
  constructor() {
9
6
  this.rl = readline.createInterface({
@@ -26,60 +23,33 @@ class Prompt {
26
23
 
27
24
  async confirm(questionText, defaultValue = false) {
28
25
  const answer = await this.question(`${questionText} (${defaultValue ? 'Y/n' : 'y/N'}) `);
29
- const answer = await this.question(`${message} (${defaultValue ? 'Y/n' : 'y/N'}): `);
30
- return answer ? answer.toLowerCase().startsWith('y') : defaultValue;
31
- }
32
-
33
- async select(message, choices, defaultIndex = 0) {
34
- logger.log(`\n${message}:`);
35
- async confirm(question, defaultValue = false) {
36
- const answer = await this.question(`${question} (${defaultValue ? 'Y/n' : 'y/N'}) `);
37
26
  if (answer === '') return defaultValue;
38
27
  return /^y|yes$/i.test(answer);
39
28
  }
40
29
 
41
- /**
42
- * Present a list of choices and let the user select one
43
- * @param {string} question - The question to ask
44
- * @param {Array<string>} choices - Array of choices
45
- * @returns {Promise<string>} The selected choice
46
- */
47
- async list(question, choices) {
48
- if (!choices || !choices.length) {
49
- throw new Error('No choices provided');
50
- }
51
-
52
- console.log(question);
30
+ async select(questionText, choices, defaultIndex = 0) {
31
+ logger.log(`\n${questionText}:`);
53
32
  choices.forEach((choice, index) => {
54
- console.log(` ${index + 1}. ${choice}`);
33
+ logger.log(` ${index + 1}. ${choice}`);
55
34
  });
56
35
 
57
36
  while (true) {
58
- const answer = await this.question(`Select an option (1-${choices.length}): `);
59
- const index = parseInt(answer.trim(), 10) - 1;
60
-
61
- if (index >= 0 && index < choices.length) {
62
- return choices[index];
37
+ const answer = await this.question(`\nSelect an option (1-${choices.length}): `);
38
+ const selected = parseInt(answer, 10) - 1;
39
+ if (!isNaN(selected) && selected >= 0 && selected < choices.length) {
40
+ return selected;
63
41
  }
64
-
65
- console.log('Invalid selection. Please try again.');
42
+ logger.log('Invalid selection. Please try again.');
66
43
  }
67
44
  }
68
45
 
69
- /**
70
- * Close the readline interface
71
- */
72
- close() {
73
- this.rl.close();
46
+ async input(questionText, defaultValue = '') {
47
+ const answer = await this.question(`${questionText}${defaultValue ? ` [${defaultValue}]` : ''}: `);
48
+ return answer || defaultValue;
74
49
  }
75
50
  }
76
51
 
77
- // Create a singleton instance
78
52
  const prompt = new Prompt();
79
-
80
- // Handle process termination
81
- process.on('exit', () => {
82
- prompt.close();
83
- });
53
+ process.on('exit', () => prompt.close());
84
54
 
85
55
  module.exports = prompt;
@@ -0,0 +1,40 @@
1
+ // utils/safe-json.js
2
+ const { readFile } = require('fs/promises');
3
+
4
+ function stripBOM(s) {
5
+ if (typeof s === 'string' && s.charCodeAt(0) === 0xFEFF) return s.slice(1);
6
+ return s;
7
+ }
8
+
9
+ /**
10
+ * Safe JSON load with guardrails.
11
+ * - Max file size (default 1MB)
12
+ * - BOM stripping
13
+ * - Single, typed error (no loops)
14
+ */
15
+ async function readJsonSafe(filePath, { maxBytes = 1_000_000 } = {}) {
16
+ const buf = await readFile(filePath);
17
+ if (buf.length === 0) {
18
+ const err = new Error('Empty JSON file');
19
+ err.code = 'EJSONEMPTY';
20
+ err.path = filePath;
21
+ throw err;
22
+ }
23
+ if (buf.length > maxBytes) {
24
+ const err = new Error(`JSON too large (${buf.length} bytes)`);
25
+ err.code = 'EJSONTOOBIG';
26
+ err.path = filePath;
27
+ throw err;
28
+ }
29
+ try {
30
+ return JSON.parse(stripBOM(buf.toString('utf8')));
31
+ } catch (e) {
32
+ const err = new Error(`Invalid JSON`);
33
+ err.code = 'EJSONPARSE';
34
+ err.path = filePath;
35
+ err.cause = e;
36
+ throw err;
37
+ }
38
+ }
39
+
40
+ module.exports = { readJsonSafe };
@@ -76,7 +76,7 @@ function secureErrorHandler(options = {}) {
76
76
  const config = { ...defaults, ...options };
77
77
 
78
78
  // Ensure log directory exists
79
- if (config.logFilePath && !fs.existsSync(path.dirname(config.logFilePath))) {
79
+ if (config.logFilePath && !SecurityUtils.safeExistsSync(path.dirname(config.logFilePath))) {
80
80
  try {
81
81
  fs.mkdirSync(path.dirname(config.logFilePath), { recursive: true });
82
82
  } catch (e) {
@@ -126,12 +126,12 @@ function secureErrorHandler(options = {}) {
126
126
 
127
127
  // Log to console
128
128
  if (typeof config.logFunction === 'function') {
129
- config.logFunction(JSON.stringify(logEntry, null, 2));
129
+ SecurityUtils.safeWriteFileSync(config.logFilePath, JSON.stringify(logEntry, null, 2));
130
130
  }
131
131
 
132
132
  // Log to file if configured
133
133
  if (config.logFilePath) {
134
- fs.appendFileSync(
134
+ SecurityUtils.safeWriteFileSync(
135
135
  config.logFilePath,
136
136
  JSON.stringify(logEntry) + '\n',
137
137
  'utf8'