locai 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +928 -0
- package/dist/cli.js +718 -0
- package/dist/commands/review.js +357 -0
- package/dist/commands/translator.js +29 -0
- package/dist/config/index.js +52 -0
- package/dist/core/adapters/base-adapter.js +1 -0
- package/dist/core/adapters/factory.js +44 -0
- package/dist/core/adapters/json-adapter.js +20 -0
- package/dist/core/adapters/po-adapter.js +50 -0
- package/dist/core/adapters/properties-adapter.js +31 -0
- package/dist/core/adapters/yaml-adapter.js +24 -0
- package/dist/core/context-processor.js +247 -0
- package/dist/core/detection/framework-detector.js +56 -0
- package/dist/core/detection/structure-detector.js +30 -0
- package/dist/core/fallback-provider.js +442 -0
- package/dist/core/orchestrator.js +438 -0
- package/dist/core/pipeline/Pipeline.js +48 -0
- package/dist/core/pipeline/PipelineStep.js +13 -0
- package/dist/core/pipeline/context.js +21 -0
- package/dist/core/pipeline/steps/CacheReadStep.js +80 -0
- package/dist/core/pipeline/steps/CacheWriteStep.js +32 -0
- package/dist/core/pipeline/steps/ConfidenceCheckStep.js +67 -0
- package/dist/core/pipeline/steps/GlossaryPostStep.js +17 -0
- package/dist/core/pipeline/steps/GlossaryPreStep.js +20 -0
- package/dist/core/pipeline/steps/InputValidationStep.js +26 -0
- package/dist/core/pipeline/steps/QualityCheckStep.js +48 -0
- package/dist/core/pipeline/steps/ToneCheckStep.js +58 -0
- package/dist/core/pipeline/steps/TranslationStep.js +65 -0
- package/dist/core/pipeline/steps/VectorCacheReadStep.js +84 -0
- package/dist/core/pipeline/steps/VectorCacheWriteStep.js +42 -0
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +100 -0
- package/dist/core/provider-factory.js +139 -0
- package/dist/providers/base-provider.js +230 -0
- package/dist/providers/dashscope.js +128 -0
- package/dist/providers/deepseek.js +111 -0
- package/dist/providers/gemini.js +135 -0
- package/dist/providers/openai.js +203 -0
- package/dist/providers/xai.js +111 -0
- package/dist/services/embedding-provider.js +50 -0
- package/dist/services/translation-service.js +609 -0
- package/dist/services/vector-store.js +79 -0
- package/dist/utils/ai-context-analyzer.js +460 -0
- package/dist/utils/confidence-scorer.js +277 -0
- package/dist/utils/error-helper.js +386 -0
- package/dist/utils/file-manager.js +357 -0
- package/dist/utils/glossary-manager.js +194 -0
- package/dist/utils/graceful-shutdown.js +118 -0
- package/dist/utils/input-validator.js +663 -0
- package/dist/utils/logger.js +374 -0
- package/dist/utils/object-transformer.js +177 -0
- package/dist/utils/progress-tracker.js +277 -0
- package/dist/utils/prompt-templates.js +415 -0
- package/dist/utils/quality/__tests__/tone-verifier.test.js +91 -0
- package/dist/utils/quality/base-checker.js +73 -0
- package/dist/utils/quality/code-block-checker.js +214 -0
- package/dist/utils/quality/html-tag-checker.js +64 -0
- package/dist/utils/quality/index.js +220 -0
- package/dist/utils/quality/length-checker.js +140 -0
- package/dist/utils/quality/markdown-checker.js +220 -0
- package/dist/utils/quality/placeholder-checker.js +142 -0
- package/dist/utils/quality/punctuation-checker.js +50 -0
- package/dist/utils/quality/quote-balance-checker.js +143 -0
- package/dist/utils/quality/special-characters-checker.js +229 -0
- package/dist/utils/quality/style-guide-checker.js +192 -0
- package/dist/utils/quality/text-sanitizer.js +120 -0
- package/dist/utils/quality/tone-verifier.js +91 -0
- package/dist/utils/rate-limiter.js +398 -0
- package/dist/utils/retry-helper.js +166 -0
- package/dist/utils/state-manager.js +162 -0
- package/dist/utils/statistics-manager.js +51 -0
- package/dist/utils/ui-manager.js +98 -0
- package/package.json +80 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input validation utility for security and data integrity.
|
|
3
|
+
* Defines regex patterns, limits, and validation methods.
|
|
4
|
+
*/
|
|
5
|
+
import path from "path";
|
|
6
|
+
class InputValidator {
|
|
7
|
+
static LANGUAGE_CODE_PATTERN = /^[a-z]{2}(-[a-z]{2})?$/;
|
|
8
|
+
static VALID_PROVIDERS = ["dashscope", "xai", "openai", "deepseek", "gemini"];
|
|
9
|
+
static MAX_TEXT_LENGTH = 10000;
|
|
10
|
+
static MAX_KEY_LENGTH = 500;
|
|
11
|
+
static MAX_PATH_LENGTH = 1000;
|
|
12
|
+
static MAX_CONFIG_DEPTH = 10;
|
|
13
|
+
static DANGEROUS_PATTERNS = [
|
|
14
|
+
/\.\.\//g,
|
|
15
|
+
/\0/g,
|
|
16
|
+
/[\x00-\x08\x0b\x0c\x0e-\x1f\x7F]/g, // eslint-disable-line no-control-regex
|
|
17
|
+
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
|
|
18
|
+
/javascript:/gi,
|
|
19
|
+
/data:.*base64/gi,
|
|
20
|
+
];
|
|
21
|
+
static _requestCounts;
|
|
22
|
+
/**
|
|
23
|
+
* Validate and sanitize language code.
|
|
24
|
+
* @param {string} langCode - Language code to validate.
|
|
25
|
+
* @param {string} [paramName="language"] - Parameter name for error messages.
|
|
26
|
+
* @returns {string} - Sanitized language code.
|
|
27
|
+
* @throws {Error} If validation fails.
|
|
28
|
+
*/
|
|
29
|
+
static validateLanguageCode(langCode, paramName = "language") {
|
|
30
|
+
if (!langCode || typeof langCode !== "string") {
|
|
31
|
+
throw new Error(`${paramName} must be a non-empty string`);
|
|
32
|
+
}
|
|
33
|
+
const sanitized = langCode.trim().toLowerCase();
|
|
34
|
+
if (sanitized.length === 0) {
|
|
35
|
+
throw new Error(`${paramName} cannot be empty after trimming`);
|
|
36
|
+
}
|
|
37
|
+
if (sanitized.length > 10) {
|
|
38
|
+
throw new Error(`${paramName} code too long: '${langCode}' (max 10 characters)`);
|
|
39
|
+
}
|
|
40
|
+
if (sanitized.includes("..") || sanitized.includes("/") || sanitized.includes("\\")) {
|
|
41
|
+
throw new Error(`${paramName} code contains forbidden characters: '${langCode}'`);
|
|
42
|
+
}
|
|
43
|
+
if (!this.LANGUAGE_CODE_PATTERN.test(sanitized)) {
|
|
44
|
+
throw new Error(`Invalid ${paramName} code: '${langCode}'. Must match pattern: ${this.LANGUAGE_CODE_PATTERN}`);
|
|
45
|
+
}
|
|
46
|
+
return sanitized;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Validate array of language codes.
|
|
50
|
+
* @param {string[]} langCodes - Array of language codes.
|
|
51
|
+
* @param {string} [paramName="languages"] - Parameter name for errors.
|
|
52
|
+
* @returns {string[]} - Sanitized array of language codes.
|
|
53
|
+
* @throws {Error} If array is empty or contains invalid codes.
|
|
54
|
+
*/
|
|
55
|
+
static validateLanguageCodes(langCodes, paramName = "languages") {
|
|
56
|
+
if (!Array.isArray(langCodes)) {
|
|
57
|
+
throw new Error(`${paramName} must be an array`);
|
|
58
|
+
}
|
|
59
|
+
if (langCodes.length === 0) {
|
|
60
|
+
throw new Error(`${paramName} array cannot be empty`);
|
|
61
|
+
}
|
|
62
|
+
if (langCodes.length > 50) {
|
|
63
|
+
throw new Error(`${paramName} array too large (max 50 languages)`);
|
|
64
|
+
}
|
|
65
|
+
return langCodes.map((code, index) => this.validateLanguageCode(code, `${paramName}[${index}]`));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Validate and sanitize directory path.
|
|
69
|
+
* @param {string} dirPath - Directory path.
|
|
70
|
+
* @param {string} [paramName="directory"] - Parameter name for errors.
|
|
71
|
+
* @returns {string} - Resolved and validated absolute path.
|
|
72
|
+
* @throws {Error} If path is invalid or outside working directory.
|
|
73
|
+
*/
|
|
74
|
+
static validateDirectoryPath(dirPath, paramName = "directory") {
|
|
75
|
+
if (!dirPath || typeof dirPath !== "string") {
|
|
76
|
+
throw new Error(`${paramName} must be a non-empty string`);
|
|
77
|
+
}
|
|
78
|
+
const resolved = path.resolve(dirPath);
|
|
79
|
+
const cwd = process.cwd();
|
|
80
|
+
if (!resolved.startsWith(cwd)) {
|
|
81
|
+
throw new Error(`${paramName} path '${dirPath}' is outside working directory (resolved: ${resolved})`);
|
|
82
|
+
}
|
|
83
|
+
return resolved;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Validate translation text.
|
|
87
|
+
* @param {string} text - Text to validate.
|
|
88
|
+
* @param {string} [paramName="text"] - Parameter name for errors.
|
|
89
|
+
* @returns {string} - Validated text.
|
|
90
|
+
* @throws {Error} If text is invalid or too long.
|
|
91
|
+
*/
|
|
92
|
+
static validateText(text, paramName = "text") {
|
|
93
|
+
if (text === null || text === undefined) {
|
|
94
|
+
throw new Error(`${paramName} cannot be null or undefined`);
|
|
95
|
+
}
|
|
96
|
+
if (typeof text !== "string") {
|
|
97
|
+
throw new Error(`${paramName} must be a string`);
|
|
98
|
+
}
|
|
99
|
+
if (text.length > this.MAX_TEXT_LENGTH) {
|
|
100
|
+
throw new Error(`${paramName} too long (${text.length} chars, max ${this.MAX_TEXT_LENGTH})`);
|
|
101
|
+
}
|
|
102
|
+
return text;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Validate translation key.
|
|
106
|
+
* @param {string} key - Key to validate.
|
|
107
|
+
* @param {string} [paramName="key"] - Parameter name.
|
|
108
|
+
* @returns {string} - Validated key.
|
|
109
|
+
* @throws {Error} If key is invalid, too long, or contains traversal.
|
|
110
|
+
*/
|
|
111
|
+
static validateKey(key, paramName = "key") {
|
|
112
|
+
if (!key || typeof key !== "string") {
|
|
113
|
+
throw new Error(`${paramName} must be a non-empty string`);
|
|
114
|
+
}
|
|
115
|
+
if (key.length > this.MAX_KEY_LENGTH) {
|
|
116
|
+
throw new Error(`${paramName} too long (${key.length} chars, max ${this.MAX_KEY_LENGTH})`);
|
|
117
|
+
}
|
|
118
|
+
if (key.includes("../") || key.includes("..\\")) {
|
|
119
|
+
throw new Error(`${paramName} contains path traversal sequences: '${key}'`);
|
|
120
|
+
}
|
|
121
|
+
return key;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Validate API provider name.
|
|
125
|
+
* @param {string} provider - Provider name.
|
|
126
|
+
* @param {string} [fieldName="API provider"] - Parameter name.
|
|
127
|
+
* @returns {string} - Normalized provider name.
|
|
128
|
+
* @throws {Error} If provider is unknown.
|
|
129
|
+
*/
|
|
130
|
+
static validateProvider(provider, fieldName = "API provider") {
|
|
131
|
+
if (!provider || typeof provider !== "string") {
|
|
132
|
+
throw new Error(`${fieldName} must be a non-empty string`);
|
|
133
|
+
}
|
|
134
|
+
const normalized = provider.toLowerCase().trim();
|
|
135
|
+
if (!this.VALID_PROVIDERS.includes(normalized)) {
|
|
136
|
+
throw new Error(`Invalid ${fieldName}: '${provider}'. Valid providers: ${this.VALID_PROVIDERS.join(", ")}`);
|
|
137
|
+
}
|
|
138
|
+
return normalized;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Sanitize filename to prevent path traversal.
|
|
142
|
+
* @param {string} filename - Filename to sanitize.
|
|
143
|
+
* @param {string|null} [extension=null] - Optional extension to enforce.
|
|
144
|
+
* @returns {string} - Sanitized filename.
|
|
145
|
+
* @throws {Error} If filename is invalid.
|
|
146
|
+
*/
|
|
147
|
+
static sanitizeFilename(filename, extension = null) {
|
|
148
|
+
if (!filename || typeof filename !== "string") {
|
|
149
|
+
throw new Error("Filename must be a non-empty string");
|
|
150
|
+
}
|
|
151
|
+
let sanitized = path
|
|
152
|
+
.basename(filename)
|
|
153
|
+
.replace(/[<>:"/\\|?*\x00-\x1f]/g, "") // eslint-disable-line no-control-regex
|
|
154
|
+
.replace(/^\.+/, "")
|
|
155
|
+
.trim();
|
|
156
|
+
if (!sanitized) {
|
|
157
|
+
throw new Error(`Filename '${filename}' results in empty name after sanitization`);
|
|
158
|
+
}
|
|
159
|
+
if (extension && !sanitized.endsWith(extension)) {
|
|
160
|
+
sanitized = sanitized.replace(/\.[^.]*$/, "") + extension;
|
|
161
|
+
}
|
|
162
|
+
return sanitized;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Create safe file path within a directory.
|
|
166
|
+
* @param {string} baseDir - Base directory.
|
|
167
|
+
* @param {string} filename - Filename.
|
|
168
|
+
* @returns {string} - Safe full path.
|
|
169
|
+
* @throws {Error} If path escapes base directory.
|
|
170
|
+
*/
|
|
171
|
+
static createSafeFilePath(baseDir, filename) {
|
|
172
|
+
const safeFilename = this.sanitizeFilename(filename);
|
|
173
|
+
const fullPath = path.join(baseDir, safeFilename);
|
|
174
|
+
const resolved = path.resolve(fullPath);
|
|
175
|
+
const resolvedBase = path.resolve(baseDir);
|
|
176
|
+
if (!resolved.startsWith(resolvedBase + path.sep) && resolved !== resolvedBase) {
|
|
177
|
+
throw new Error(`Generated path '${fullPath}' is outside base directory '${baseDir}'`);
|
|
178
|
+
}
|
|
179
|
+
return fullPath;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Validate configuration object.
|
|
183
|
+
* @param {any} config - Configuration object to validate.
|
|
184
|
+
* @returns {AppConfig} - Validated configuration.
|
|
185
|
+
* @throws {Error} If configuration is invalid.
|
|
186
|
+
*/
|
|
187
|
+
static validateConfig(config) {
|
|
188
|
+
if (!config || typeof config !== "object") {
|
|
189
|
+
throw new Error("Configuration must be an object");
|
|
190
|
+
}
|
|
191
|
+
// Check config depth to prevent circular references
|
|
192
|
+
this.validateObjectDepth(config);
|
|
193
|
+
const errors = [];
|
|
194
|
+
const warnings = [];
|
|
195
|
+
// Required fields validation
|
|
196
|
+
if (!config.source) {
|
|
197
|
+
errors.push("Missing required field 'source' (source language)");
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
try {
|
|
201
|
+
this.validateLanguageCode(config.source, "source language");
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
errors.push(`Invalid source language: ${error.message}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!config.targets || !Array.isArray(config.targets) || config.targets.length === 0) {
|
|
208
|
+
errors.push("Missing or empty 'targets' array (target languages)");
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
try {
|
|
212
|
+
this.validateLanguageCodes(config.targets, "target languages");
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
errors.push(`Invalid target languages: ${error.message}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!config.localesDir) {
|
|
219
|
+
errors.push("Missing required field 'localesDir' (locales directory path)");
|
|
220
|
+
}
|
|
221
|
+
// API Provider validation
|
|
222
|
+
if (config.apiProvider) {
|
|
223
|
+
try {
|
|
224
|
+
this.validateProvider(config.apiProvider, "API provider");
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
errors.push(`Invalid API provider: ${error.message}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
warnings.push("No API provider specified, will use first available provider");
|
|
232
|
+
}
|
|
233
|
+
// Fallback order validation
|
|
234
|
+
if (config.fallbackOrder) {
|
|
235
|
+
if (!Array.isArray(config.fallbackOrder)) {
|
|
236
|
+
errors.push("'fallbackOrder' must be an array");
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
for (const provider of config.fallbackOrder) {
|
|
240
|
+
try {
|
|
241
|
+
this.validateProvider(provider, "fallback provider");
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
errors.push(`Invalid provider in fallbackOrder: ${error.message}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Concurrency validation
|
|
250
|
+
if (config.concurrencyLimit !== undefined) {
|
|
251
|
+
const limit = parseInt(config.concurrencyLimit);
|
|
252
|
+
if (isNaN(limit) || limit < 1 || limit > 50) {
|
|
253
|
+
errors.push("'concurrencyLimit' must be a number between 1 and 50");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Cache configuration validation
|
|
257
|
+
if (config.cacheSize !== undefined) {
|
|
258
|
+
const size = parseInt(config.cacheSize);
|
|
259
|
+
if (isNaN(size) || size < 100 || size > 10000) {
|
|
260
|
+
errors.push("'cacheSize' must be a number between 100 and 10000");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (config.cacheTTL !== undefined) {
|
|
264
|
+
const ttl = parseInt(config.cacheTTL);
|
|
265
|
+
if (isNaN(ttl) || ttl < 0) {
|
|
266
|
+
errors.push("'cacheTTL' must be a non-negative number (milliseconds)");
|
|
267
|
+
}
|
|
268
|
+
if (ttl > 7 * 24 * 60 * 60 * 1000) {
|
|
269
|
+
warnings.push("'cacheTTL' is very large (>7 days), consider reducing it");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// API Config validation
|
|
273
|
+
if (config.apiConfig) {
|
|
274
|
+
if (typeof config.apiConfig !== "object") {
|
|
275
|
+
errors.push("'apiConfig' must be an object");
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
this.validateApiConfig(config.apiConfig, errors, warnings);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Rate Limiter validation
|
|
282
|
+
if (config.rateLimiter) {
|
|
283
|
+
if (typeof config.rateLimiter !== "object") {
|
|
284
|
+
errors.push("'rateLimiter' must be an object");
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
this.validateRateLimiterConfig(config.rateLimiter, errors, warnings);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Retry options validation
|
|
291
|
+
if (config.retryOptions) {
|
|
292
|
+
if (typeof config.retryOptions !== "object") {
|
|
293
|
+
errors.push("'retryOptions' must be an object");
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
this.validateRetryOptions(config.retryOptions, errors, warnings);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Context configuration validation
|
|
300
|
+
if (config.context) {
|
|
301
|
+
if (typeof config.context !== "object") {
|
|
302
|
+
errors.push("'context' must be an object");
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
this.validateContextConfig(config.context, errors, warnings);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Quality checks validation
|
|
309
|
+
if (config.qualityChecks) {
|
|
310
|
+
if (typeof config.qualityChecks !== "object") {
|
|
311
|
+
errors.push("'qualityChecks' must be an object");
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
this.validateQualityChecks(config.qualityChecks, errors, warnings);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Length control validation
|
|
318
|
+
if (config.lengthControl) {
|
|
319
|
+
if (typeof config.lengthControl !== "object") {
|
|
320
|
+
errors.push("'lengthControl' must be an object");
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
this.validateLengthControl(config.lengthControl, errors);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Advanced settings validation
|
|
327
|
+
if (config.advanced) {
|
|
328
|
+
if (typeof config.advanced !== "object") {
|
|
329
|
+
errors.push("'advanced' must be an object");
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
this.validateAdvancedConfig(config.advanced, errors);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// File operations validation
|
|
336
|
+
if (config.fileOperations) {
|
|
337
|
+
if (typeof config.fileOperations !== "object") {
|
|
338
|
+
errors.push("'fileOperations' must be an object");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Report errors and warnings
|
|
342
|
+
if (errors.length > 0) {
|
|
343
|
+
const errorMessage = [
|
|
344
|
+
"Configuration validation failed:",
|
|
345
|
+
...errors.map((err, i) => ` ${i + 1}. ${err}`),
|
|
346
|
+
].join("\n");
|
|
347
|
+
throw new Error(errorMessage);
|
|
348
|
+
}
|
|
349
|
+
if (warnings.length > 0 && (config.debug || config.verbose)) {
|
|
350
|
+
console.warn("Configuration warnings:");
|
|
351
|
+
warnings.forEach((warn, i) => console.warn(` ${i + 1}. ${warn}`));
|
|
352
|
+
}
|
|
353
|
+
return config;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Validate API config for each provider.
|
|
357
|
+
* @param {Record<string, ApiProviderConfig>} apiConfig - API configuration object.
|
|
358
|
+
* @param {string[]} errors - Array to push errors to.
|
|
359
|
+
* @param {string[]} warnings - Array to push warnings to.
|
|
360
|
+
*/
|
|
361
|
+
static validateApiConfig(apiConfig, errors, warnings) {
|
|
362
|
+
for (const [provider, settings] of Object.entries(apiConfig)) {
|
|
363
|
+
if (!this.VALID_PROVIDERS.includes(provider)) {
|
|
364
|
+
warnings.push(`Unknown provider in apiConfig: '${provider}'`);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (typeof settings !== "object") {
|
|
368
|
+
errors.push(`apiConfig.${provider} must be an object`);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
// Validate temperature
|
|
372
|
+
if (settings.temperature !== undefined) {
|
|
373
|
+
const temp = parseFloat(settings.temperature);
|
|
374
|
+
if (isNaN(temp) || temp < 0 || temp > 2) {
|
|
375
|
+
errors.push(`apiConfig.${provider}.temperature must be between 0 and 2`);
|
|
376
|
+
}
|
|
377
|
+
if (temp > 0.5) {
|
|
378
|
+
warnings.push(`apiConfig.${provider}.temperature is high (${temp}), may reduce consistency`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Validate maxTokens
|
|
382
|
+
if (settings.maxTokens !== undefined) {
|
|
383
|
+
const tokens = parseInt(settings.maxTokens);
|
|
384
|
+
if (isNaN(tokens) || tokens < 100 || tokens > 8000) {
|
|
385
|
+
errors.push(`apiConfig.${provider}.maxTokens must be between 100 and 8000`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Validate rate limiter configuration.
|
|
392
|
+
* @param {RateLimiterConfig} rateLimiter - Rate limiter configuration.
|
|
393
|
+
* @param {string[]} errors - Array to push errors to.
|
|
394
|
+
* @param {string[]} warnings - Array to push warnings to.
|
|
395
|
+
*/
|
|
396
|
+
static validateRateLimiterConfig(rateLimiter, errors, _warnings) {
|
|
397
|
+
if (rateLimiter.queueStrategy) {
|
|
398
|
+
const validStrategies = ["fifo", "priority"];
|
|
399
|
+
if (!validStrategies.includes(rateLimiter.queueStrategy)) {
|
|
400
|
+
errors.push(`rateLimiter.queueStrategy must be one of: ${validStrategies.join(", ")}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (rateLimiter.queueTimeout !== undefined) {
|
|
404
|
+
const timeout = parseInt(rateLimiter.queueTimeout);
|
|
405
|
+
if (isNaN(timeout) || timeout < 1000 || timeout > 120000) {
|
|
406
|
+
errors.push("rateLimiter.queueTimeout must be between 1000 and 120000 ms");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (rateLimiter.providerLimits) {
|
|
410
|
+
if (typeof rateLimiter.providerLimits !== "object") {
|
|
411
|
+
errors.push("rateLimiter.providerLimits must be an object");
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
for (const [provider, limits] of Object.entries(rateLimiter.providerLimits)) {
|
|
415
|
+
if (!this.VALID_PROVIDERS.includes(provider)) {
|
|
416
|
+
_warnings.push(`Unknown provider in providerLimits: '${provider}'`);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (limits.rpm !== undefined) {
|
|
420
|
+
const rpm = parseInt(limits.rpm);
|
|
421
|
+
if (isNaN(rpm) || rpm < 1 || rpm > 10000) {
|
|
422
|
+
errors.push(`rateLimiter.providerLimits.${provider}.rpm must be between 1 and 10000`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (limits.concurrency !== undefined) {
|
|
426
|
+
const conc = parseInt(limits.concurrency);
|
|
427
|
+
if (isNaN(conc) || conc < 1 || conc > 50) {
|
|
428
|
+
errors.push(`rateLimiter.providerLimits.${provider}.concurrency must be between 1 and 50`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Validate retry options.
|
|
437
|
+
* @param {RetryOptions} retryOptions - Retry options.
|
|
438
|
+
* @param {string[]} errors - Array to push errors to.
|
|
439
|
+
* @param {string[]} warnings - Array to push warnings to.
|
|
440
|
+
*/
|
|
441
|
+
static validateRetryOptions(retryOptions, errors, _warnings) {
|
|
442
|
+
if (retryOptions.maxRetries !== undefined) {
|
|
443
|
+
const retries = parseInt(retryOptions.maxRetries);
|
|
444
|
+
if (isNaN(retries) || retries < 0 || retries > 10) {
|
|
445
|
+
errors.push("retryOptions.maxRetries must be between 0 and 10");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (retryOptions.initialDelay !== undefined) {
|
|
449
|
+
const delay = parseInt(retryOptions.initialDelay);
|
|
450
|
+
if (isNaN(delay) || delay < 100 || delay > 10000) {
|
|
451
|
+
errors.push("retryOptions.initialDelay must be between 100 and 10000 ms");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (retryOptions.maxDelay !== undefined) {
|
|
455
|
+
const delay = parseInt(retryOptions.maxDelay);
|
|
456
|
+
if (isNaN(delay) || delay < 1000 || delay > 60000) {
|
|
457
|
+
errors.push("retryOptions.maxDelay must be between 1000 and 60000 ms");
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (retryOptions.initialDelay !== undefined &&
|
|
461
|
+
retryOptions.maxDelay !== undefined &&
|
|
462
|
+
retryOptions.initialDelay > retryOptions.maxDelay) {
|
|
463
|
+
errors.push("retryOptions.initialDelay cannot be greater than maxDelay");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Validate context configuration.
|
|
468
|
+
* @param {ContextConfig} context - Context configuration.
|
|
469
|
+
* @param {string[]} errors - Array to push errors to.
|
|
470
|
+
* @param {string[]} warnings - Array to push warnings to.
|
|
471
|
+
*/
|
|
472
|
+
static validateContextConfig(context, errors, warnings) {
|
|
473
|
+
if (context.aiProvider) {
|
|
474
|
+
try {
|
|
475
|
+
this.validateProvider(context.aiProvider, "context.aiProvider");
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
errors.push(`Invalid context.aiProvider: ${error.message}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (context.minTextLength !== undefined) {
|
|
482
|
+
const length = parseInt(context.minTextLength);
|
|
483
|
+
if (isNaN(length) || length < 0 || length > 1000) {
|
|
484
|
+
errors.push("context.minTextLength must be between 0 and 1000");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (context.detection) {
|
|
488
|
+
if (context.detection.threshold !== undefined) {
|
|
489
|
+
const threshold = parseInt(context.detection.threshold);
|
|
490
|
+
if (isNaN(threshold) || threshold < 1 || threshold > 20) {
|
|
491
|
+
errors.push("context.detection.threshold must be between 1 and 20");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (context.detection.minConfidence !== undefined) {
|
|
495
|
+
const confidence = parseFloat(context.detection.minConfidence);
|
|
496
|
+
if (isNaN(confidence) || confidence < 0 || confidence > 1) {
|
|
497
|
+
errors.push("context.detection.minConfidence must be between 0 and 1");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (context.categories) {
|
|
502
|
+
if (typeof context.categories !== "object") {
|
|
503
|
+
errors.push("context.categories must be an object");
|
|
504
|
+
}
|
|
505
|
+
else if (Object.keys(context.categories).length === 0) {
|
|
506
|
+
warnings.push("context.categories is empty, context detection may not work");
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Validate quality checks configuration.
|
|
512
|
+
* @param {QualityChecksConfig} qualityChecks - Quality checks configuration.
|
|
513
|
+
* @param {string[]} errors - Array to push errors to.
|
|
514
|
+
* @param {string[]} warnings - Array to push warnings to.
|
|
515
|
+
*/
|
|
516
|
+
static validateQualityChecks(qualityChecks, errors, warnings) {
|
|
517
|
+
if (qualityChecks.rules && typeof qualityChecks.rules !== "object") {
|
|
518
|
+
errors.push("qualityChecks.rules must be an object");
|
|
519
|
+
}
|
|
520
|
+
if (qualityChecks.enabled === false && qualityChecks.autoFix === true) {
|
|
521
|
+
warnings.push("qualityChecks.autoFix is enabled but qualityChecks.enabled is false");
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Validate length control configuration.
|
|
526
|
+
* @param {LengthControlConfig} lengthControl - Length control configuration.
|
|
527
|
+
* @param {string[]} errors - Array to push errors to.
|
|
528
|
+
*/
|
|
529
|
+
static validateLengthControl(lengthControl, errors) {
|
|
530
|
+
if (lengthControl.mode) {
|
|
531
|
+
const validModes = ["strict", "flexible", "exact", "relaxed", "smart"];
|
|
532
|
+
if (!validModes.includes(lengthControl.mode)) {
|
|
533
|
+
errors.push(`lengthControl.mode must be one of: ${validModes.join(", ")}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (lengthControl.rules) {
|
|
537
|
+
if (typeof lengthControl.rules !== "object") {
|
|
538
|
+
errors.push("lengthControl.rules must be an object");
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Validate advanced configuration.
|
|
544
|
+
* @param {AdvancedConfig} advanced - Advanced configuration.
|
|
545
|
+
* @param {string[]} errors - Array to push errors to.
|
|
546
|
+
*/
|
|
547
|
+
static validateAdvancedConfig(advanced, errors) {
|
|
548
|
+
if (advanced.timeoutMs !== undefined) {
|
|
549
|
+
const timeout = parseInt(advanced.timeoutMs);
|
|
550
|
+
if (isNaN(timeout) || timeout < 1000 || timeout > 300000) {
|
|
551
|
+
errors.push("advanced.timeoutMs must be between 1000 and 300000 ms (5 min)");
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (advanced.maxKeyLength !== undefined) {
|
|
555
|
+
const length = parseInt(advanced.maxKeyLength);
|
|
556
|
+
if (isNaN(length) || length < 100 || length > 50000) {
|
|
557
|
+
errors.push("advanced.maxKeyLength must be between 100 and 50000");
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (advanced.maxBatchSize !== undefined) {
|
|
561
|
+
const size = parseInt(advanced.maxBatchSize);
|
|
562
|
+
if (isNaN(size) || size < 1 || size > 100) {
|
|
563
|
+
errors.push("advanced.maxBatchSize must be between 1 and 100");
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Sanitize translation text for security.
|
|
569
|
+
* @param {string | null} text - Text to sanitize.
|
|
570
|
+
* @returns {string | null} - Sanitized text.
|
|
571
|
+
*/
|
|
572
|
+
static sanitizeTranslationText(text) {
|
|
573
|
+
if (!text || typeof text !== "string") {
|
|
574
|
+
return text;
|
|
575
|
+
}
|
|
576
|
+
let sanitized = text;
|
|
577
|
+
for (const pattern of this.DANGEROUS_PATTERNS) {
|
|
578
|
+
sanitized = sanitized.replace(pattern, "");
|
|
579
|
+
}
|
|
580
|
+
sanitized = sanitized
|
|
581
|
+
.replace(/\r\n/g, "\n")
|
|
582
|
+
.replace(/\r/g, "\n")
|
|
583
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
584
|
+
.trim();
|
|
585
|
+
return sanitized;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Validate API key format.
|
|
589
|
+
* @param {string} apiKey - API key to validate.
|
|
590
|
+
* @param {string} providerName - Provider name.
|
|
591
|
+
* @returns {boolean} - True if valid.
|
|
592
|
+
* @throws {Error} If key is invalid.
|
|
593
|
+
*/
|
|
594
|
+
static validateApiKeyFormat(apiKey, providerName) {
|
|
595
|
+
if (!apiKey || typeof apiKey !== "string") {
|
|
596
|
+
throw new Error(`${providerName} API key must be a non-empty string`);
|
|
597
|
+
}
|
|
598
|
+
if (apiKey.length < 10) {
|
|
599
|
+
throw new Error(`${providerName} API key appears too short`);
|
|
600
|
+
}
|
|
601
|
+
if (apiKey.length > 200) {
|
|
602
|
+
throw new Error(`${providerName} API key appears too long`);
|
|
603
|
+
}
|
|
604
|
+
const lowercaseKey = apiKey.toLowerCase();
|
|
605
|
+
const invalidPatterns = ["test", "placeholder", "example", "your-api-key", "sk-test"];
|
|
606
|
+
for (const pattern of invalidPatterns) {
|
|
607
|
+
if (lowercaseKey.includes(pattern)) {
|
|
608
|
+
throw new Error(`${providerName} API key appears to be a placeholder`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Validate object depth to prevent circular references and deep nesting.
|
|
615
|
+
* @param {any} obj - Object to validate.
|
|
616
|
+
* @param {number} [maxDepth=10] - Maximum depth.
|
|
617
|
+
* @param {number} [currentDepth=0] - Current depth.
|
|
618
|
+
* @returns {boolean} - True if valid.
|
|
619
|
+
* @throws {Error} If depth exceeded.
|
|
620
|
+
*/
|
|
621
|
+
static validateObjectDepth(obj, maxDepth = this.MAX_CONFIG_DEPTH, currentDepth = 0) {
|
|
622
|
+
if (currentDepth > maxDepth) {
|
|
623
|
+
throw new Error(`Configuration object too deeply nested (max depth: ${maxDepth})`);
|
|
624
|
+
}
|
|
625
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
626
|
+
for (const value of Object.values(obj)) {
|
|
627
|
+
if (value && typeof value === "object") {
|
|
628
|
+
this.validateObjectDepth(value, maxDepth, currentDepth + 1);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Validate request rate for an identifier.
|
|
636
|
+
* @param {string} identifier - Identifier (e.g., user ID or IP).
|
|
637
|
+
* @param {number} [maxRequestsPerMinute=60] - Max requests per minute.
|
|
638
|
+
* @returns {boolean} - True if allowed.
|
|
639
|
+
* @throws {Error} If rate limit exceeded.
|
|
640
|
+
*/
|
|
641
|
+
static validateRequestRate(identifier, maxRequestsPerMinute = 60) {
|
|
642
|
+
const now = Date.now();
|
|
643
|
+
const minute = Math.floor(now / 60000);
|
|
644
|
+
if (!this._requestCounts) {
|
|
645
|
+
this._requestCounts = new Map();
|
|
646
|
+
}
|
|
647
|
+
const key = `${identifier}-${minute}`;
|
|
648
|
+
const currentCount = this._requestCounts.get(key) || 0;
|
|
649
|
+
if (currentCount >= maxRequestsPerMinute) {
|
|
650
|
+
throw new Error(`Too many requests from ${identifier}. Please try again later.`);
|
|
651
|
+
}
|
|
652
|
+
this._requestCounts.set(key, currentCount + 1);
|
|
653
|
+
// Cleanup old entries
|
|
654
|
+
for (const [k] of this._requestCounts) {
|
|
655
|
+
const [, keyMinute] = k.split("-");
|
|
656
|
+
if (parseInt(keyMinute) < minute - 5) {
|
|
657
|
+
this._requestCounts.delete(k);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
export default InputValidator;
|