locai 2.6.2 → 2.7.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 +23 -25
- package/dist/cli/helpers.d.ts +34 -2
- package/dist/cli/helpers.js +96 -37
- package/dist/cli.js +40 -39
- package/dist/commands/review.d.ts +33 -8
- package/dist/commands/review.js +89 -22
- package/dist/commands/translator.d.ts +4 -2
- package/dist/commands/translator.js +1 -1
- package/dist/config/index.d.ts +53 -66
- package/dist/config/index.js +2 -1
- package/dist/config/setup.d.ts +3 -2
- package/dist/config/setup.js +5 -3
- package/dist/core/adapters/android-adapter.d.ts +3 -2
- package/dist/core/adapters/android-adapter.js +12 -13
- package/dist/core/adapters/base-adapter.d.ts +13 -2
- package/dist/core/adapters/ios-adapter.d.ts +3 -2
- package/dist/core/adapters/json-adapter.d.ts +4 -3
- package/dist/core/adapters/json-adapter.js +1 -3
- package/dist/core/adapters/po-adapter.d.ts +3 -2
- package/dist/core/adapters/po-adapter.js +4 -5
- package/dist/core/adapters/properties-adapter.d.ts +3 -2
- package/dist/core/adapters/properties-adapter.js +2 -4
- package/dist/core/adapters/yaml-adapter.d.ts +4 -3
- package/dist/core/adapters/yaml-adapter.js +1 -3
- package/dist/core/context-processor.d.ts +21 -6
- package/dist/core/context-processor.js +2 -7
- package/dist/core/detection/framework-detector.js +1 -1
- package/dist/core/detection/structure-detector.js +1 -1
- package/dist/core/fallback-provider.d.ts +3 -4
- package/dist/core/fallback-provider.js +15 -21
- package/dist/core/orchestrator.d.ts +98 -26
- package/dist/core/orchestrator.js +56 -116
- package/dist/core/pipeline/Pipeline.d.ts +2 -2
- package/dist/core/pipeline/Pipeline.js +1 -3
- package/dist/core/pipeline/context.d.ts +69 -13
- package/dist/core/pipeline/context.js +1 -7
- package/dist/core/pipeline/steps/CacheReadStep.d.ts +10 -7
- package/dist/core/pipeline/steps/CacheReadStep.js +1 -12
- package/dist/core/pipeline/steps/CacheWriteStep.d.ts +2 -2
- package/dist/core/pipeline/steps/CacheWriteStep.js +1 -8
- package/dist/core/pipeline/steps/ConfidenceCheckStep.d.ts +5 -0
- package/dist/core/pipeline/steps/ConfidenceCheckStep.js +24 -44
- package/dist/core/pipeline/steps/ContextEnrichmentStep.js +3 -14
- package/dist/core/pipeline/steps/QualityCheckStep.d.ts +1 -1
- package/dist/core/pipeline/steps/QualityCheckStep.js +0 -2
- package/dist/core/pipeline/steps/ToneCheckStep.js +1 -1
- package/dist/core/pipeline/steps/TranslationStep.d.ts +13 -3
- package/dist/core/pipeline/steps/TranslationStep.js +21 -23
- package/dist/core/pipeline/steps/VectorCacheReadStep.d.ts +9 -1
- package/dist/core/pipeline/steps/VectorCacheReadStep.js +3 -5
- package/dist/core/pipeline/steps/VectorCacheWriteStep.d.ts +2 -1
- package/dist/core/pipeline/steps/VectorCacheWriteStep.js +2 -1
- package/dist/core/provider-factory.d.ts +15 -9
- package/dist/core/provider-factory.js +42 -32
- package/dist/providers/anthropic.d.ts +5 -4
- package/dist/providers/anthropic.js +3 -2
- package/dist/providers/api-types.d.ts +74 -0
- package/dist/providers/api-types.js +5 -0
- package/dist/providers/base-provider.d.ts +27 -31
- package/dist/providers/base-provider.js +22 -46
- package/dist/providers/dashscope.js +0 -2
- package/dist/providers/deepseek.js +0 -1
- package/dist/providers/gemini.js +0 -1
- package/dist/providers/openai.d.ts +3 -3
- package/dist/providers/openai.js +0 -1
- package/dist/providers/xai.js +0 -1
- package/dist/services/embedding-provider.js +4 -2
- package/dist/services/language-processor.d.ts +5 -3
- package/dist/services/language-processor.js +38 -43
- package/dist/services/source-analyzer.d.ts +24 -0
- package/dist/services/source-analyzer.js +161 -68
- package/dist/services/translation-service.d.ts +31 -13
- package/dist/services/translation-service.js +116 -71
- package/dist/services/vector-store.d.ts +15 -2
- package/dist/services/vector-store.js +14 -10
- package/dist/types/index.d.ts +31 -33
- package/dist/utils/ai-context-analyzer.d.ts +12 -3
- package/dist/utils/ai-context-analyzer.js +18 -10
- package/dist/utils/confidence-scorer.d.ts +16 -19
- package/dist/utils/confidence-scorer.js +36 -125
- package/dist/utils/error-helper.d.ts +27 -6
- package/dist/utils/error-helper.js +10 -9
- package/dist/utils/file-manager.d.ts +8 -7
- package/dist/utils/file-manager.js +22 -23
- package/dist/utils/glossary-manager.d.ts +22 -3
- package/dist/utils/glossary-manager.js +47 -28
- package/dist/utils/graceful-shutdown.d.ts +7 -1
- package/dist/utils/graceful-shutdown.js +1 -2
- package/dist/utils/input-validator.d.ts +53 -66
- package/dist/utils/input-validator.js +100 -151
- package/dist/utils/locale-paths.d.ts +7 -0
- package/dist/utils/locale-paths.js +16 -0
- package/dist/utils/logger.d.ts +8 -8
- package/dist/utils/logger.js +7 -15
- package/dist/utils/object-transformer.d.ts +20 -5
- package/dist/utils/object-transformer.js +20 -28
- package/dist/utils/progress-tracker.js +2 -3
- package/dist/utils/prompt-templates.d.ts +45 -5
- package/dist/utils/prompt-templates.js +61 -5
- package/dist/utils/quality/base-checker.d.ts +40 -18
- package/dist/utils/quality/base-checker.js +1 -2
- package/dist/utils/quality/code-block-checker.d.ts +13 -5
- package/dist/utils/quality/index.d.ts +2 -1
- package/dist/utils/quality/index.js +8 -27
- package/dist/utils/quality/length-checker.d.ts +35 -5
- package/dist/utils/quality/length-checker.js +2 -3
- package/dist/utils/quality/markdown-checker.d.ts +14 -7
- package/dist/utils/quality/special-characters-checker.d.ts +18 -1
- package/dist/utils/quality/special-characters-checker.js +0 -1
- package/dist/utils/quality/style-guide-checker.d.ts +1 -1
- package/dist/utils/quality/style-guide-checker.js +0 -2
- package/dist/utils/quality/tone-verifier.js +1 -1
- package/dist/utils/rate-limiter.d.ts +17 -1
- package/dist/utils/rate-limiter.js +12 -11
- package/dist/utils/retry-helper.d.ts +21 -3
- package/dist/utils/retry-helper.js +11 -3
- package/dist/utils/review-queue.d.ts +45 -0
- package/dist/utils/review-queue.js +53 -0
- package/dist/utils/state-manager.d.ts +12 -3
- package/dist/utils/state-manager.js +32 -7
- package/dist/utils/statistics-manager.d.ts +11 -1
- package/dist/utils/statistics-manager.js +8 -0
- package/dist/utils/ui-manager.d.ts +2 -1
- package/dist/utils/ui-manager.js +15 -1
- package/package.json +14 -9
- package/dist/commands/interactive-review.d.ts +0 -9
- package/dist/commands/interactive-review.js +0 -109
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.d.ts +0 -1
- package/dist/core/pipeline/steps/__tests__/ToneCheckStep.test.js +0 -100
- package/dist/utils/quality/__tests__/tone-verifier.test.d.ts +0 -1
- package/dist/utils/quality/__tests__/tone-verifier.test.js +0 -91
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
* Defines regex patterns, limits, and validation methods.
|
|
4
4
|
*/
|
|
5
5
|
import path from "path";
|
|
6
|
+
const isObject = (value) => typeof value === "object" && value !== null;
|
|
7
|
+
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
6
8
|
class InputValidator {
|
|
7
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Well-formed BCP 47 language tag, limited to the subtags locai supports:
|
|
11
|
+
* a 2-3 letter language, an optional 4-letter script and an optional
|
|
12
|
+
* 2-letter or 3-digit region (e.g. "en", "fil", "pt-BR", "zh-Hant-TW", "es-419").
|
|
13
|
+
* Matching is case-insensitive; see validateLanguageCode for canonical casing.
|
|
14
|
+
*/
|
|
15
|
+
static LANGUAGE_CODE_PATTERN = /^([a-z]{2,3})(?:-([a-z]{4}))?(?:-([a-z]{2}|\d{3}))?$/i;
|
|
8
16
|
static VALID_PROVIDERS = [
|
|
9
17
|
"dashscope",
|
|
10
18
|
"xai",
|
|
@@ -17,44 +25,43 @@ class InputValidator {
|
|
|
17
25
|
static MAX_KEY_LENGTH = 500;
|
|
18
26
|
static MAX_PATH_LENGTH = 1000;
|
|
19
27
|
static MAX_CONFIG_DEPTH = 10;
|
|
20
|
-
static DANGEROUS_PATTERNS = [
|
|
21
|
-
/\.\.\//g,
|
|
22
|
-
/\0/g,
|
|
23
|
-
/[\x00-\x08\x0b\x0c\x0e-\x1f\x7F]/g, // eslint-disable-line no-control-regex
|
|
24
|
-
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
|
|
25
|
-
/javascript:/gi,
|
|
26
|
-
/data:.*base64/gi,
|
|
27
|
-
];
|
|
28
|
-
static _requestCounts;
|
|
29
28
|
/**
|
|
30
|
-
* Validate
|
|
31
|
-
*
|
|
29
|
+
* Validate a BCP 47 language tag and return it in canonical casing:
|
|
30
|
+
* lowercase language, title-case script, uppercase region
|
|
31
|
+
* (e.g. "ZH-hant-tw" -> "zh-Hant-TW", "PT-br" -> "pt-BR").
|
|
32
|
+
* @param {unknown} langCode - Language tag to validate.
|
|
32
33
|
* @param {string} [paramName="language"] - Parameter name for error messages.
|
|
33
|
-
* @returns {string} -
|
|
34
|
+
* @returns {string} - Canonical language tag.
|
|
34
35
|
* @throws {Error} If validation fails.
|
|
35
36
|
*/
|
|
36
37
|
static validateLanguageCode(langCode, paramName = "language") {
|
|
37
38
|
if (!langCode || typeof langCode !== "string") {
|
|
38
39
|
throw new Error(`${paramName} must be a non-empty string`);
|
|
39
40
|
}
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
41
|
+
const trimmed = langCode.trim();
|
|
42
|
+
if (trimmed.length === 0) {
|
|
42
43
|
throw new Error(`${paramName} cannot be empty after trimming`);
|
|
43
44
|
}
|
|
44
|
-
if (
|
|
45
|
-
throw new Error(`${paramName} code too long: '${langCode}' (max 10 characters)`);
|
|
46
|
-
}
|
|
47
|
-
if (sanitized.includes("..") || sanitized.includes("/") || sanitized.includes("\\")) {
|
|
45
|
+
if (trimmed.includes("..") || trimmed.includes("/") || trimmed.includes("\\")) {
|
|
48
46
|
throw new Error(`${paramName} code contains forbidden characters: '${langCode}'`);
|
|
49
47
|
}
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const match = this.LANGUAGE_CODE_PATTERN.exec(trimmed);
|
|
49
|
+
if (!match) {
|
|
50
|
+
throw new Error(`Invalid ${paramName} code: '${langCode}'. Expected a BCP 47 language tag such as 'en', 'fil', 'pt-BR', 'zh-Hant' or 'es-419'`);
|
|
52
51
|
}
|
|
53
|
-
|
|
52
|
+
const [, language, script, region] = match;
|
|
53
|
+
let canonical = language.toLowerCase();
|
|
54
|
+
if (script) {
|
|
55
|
+
canonical += `-${script.charAt(0).toUpperCase()}${script.slice(1).toLowerCase()}`;
|
|
56
|
+
}
|
|
57
|
+
if (region) {
|
|
58
|
+
canonical += `-${region.toUpperCase()}`;
|
|
59
|
+
}
|
|
60
|
+
return canonical;
|
|
54
61
|
}
|
|
55
62
|
/**
|
|
56
63
|
* Validate array of language codes.
|
|
57
|
-
* @param {
|
|
64
|
+
* @param {unknown} langCodes - Array of language codes.
|
|
58
65
|
* @param {string} [paramName="languages"] - Parameter name for errors.
|
|
59
66
|
* @returns {string[]} - Sanitized array of language codes.
|
|
60
67
|
* @throws {Error} If array is empty or contains invalid codes.
|
|
@@ -73,7 +80,7 @@ class InputValidator {
|
|
|
73
80
|
}
|
|
74
81
|
/**
|
|
75
82
|
* Validate and sanitize directory path.
|
|
76
|
-
* @param {
|
|
83
|
+
* @param {unknown} dirPath - Directory path.
|
|
77
84
|
* @param {string} [paramName="directory"] - Parameter name for errors.
|
|
78
85
|
* @returns {string} - Resolved and validated absolute path.
|
|
79
86
|
* @throws {Error} If path is invalid or outside working directory.
|
|
@@ -91,7 +98,7 @@ class InputValidator {
|
|
|
91
98
|
}
|
|
92
99
|
/**
|
|
93
100
|
* Validate translation text.
|
|
94
|
-
* @param {
|
|
101
|
+
* @param {unknown} text - Text to validate.
|
|
95
102
|
* @param {string} [paramName="text"] - Parameter name for errors.
|
|
96
103
|
* @returns {string} - Validated text.
|
|
97
104
|
* @throws {Error} If text is invalid or too long.
|
|
@@ -110,7 +117,7 @@ class InputValidator {
|
|
|
110
117
|
}
|
|
111
118
|
/**
|
|
112
119
|
* Validate translation key.
|
|
113
|
-
* @param {
|
|
120
|
+
* @param {unknown} key - Key to validate.
|
|
114
121
|
* @param {string} [paramName="key"] - Parameter name.
|
|
115
122
|
* @returns {string} - Validated key.
|
|
116
123
|
* @throws {Error} If key is invalid, too long, or contains traversal.
|
|
@@ -129,7 +136,7 @@ class InputValidator {
|
|
|
129
136
|
}
|
|
130
137
|
/**
|
|
131
138
|
* Validate API provider name.
|
|
132
|
-
* @param {
|
|
139
|
+
* @param {unknown} provider - Provider name.
|
|
133
140
|
* @param {string} [fieldName="API provider"] - Parameter name.
|
|
134
141
|
* @returns {string} - Normalized provider name.
|
|
135
142
|
* @throws {Error} If provider is unknown.
|
|
@@ -157,7 +164,7 @@ class InputValidator {
|
|
|
157
164
|
}
|
|
158
165
|
let sanitized = path
|
|
159
166
|
.basename(filename)
|
|
160
|
-
.replace(/[<>:"/\\|?*\x00-\x1f]/g, "") // eslint-disable-line no-control-regex
|
|
167
|
+
.replace(/[<>:"/\\|?*\x00-\x1f]/g, "") // eslint-disable-line no-control-regex -- strip ASCII control characters
|
|
161
168
|
.replace(/^\.+/, "")
|
|
162
169
|
.trim();
|
|
163
170
|
if (!sanitized) {
|
|
@@ -187,12 +194,14 @@ class InputValidator {
|
|
|
187
194
|
}
|
|
188
195
|
/**
|
|
189
196
|
* Validate configuration object.
|
|
190
|
-
*
|
|
197
|
+
* Rewrites `source` and `targets` in place to canonical BCP 47 casing so that
|
|
198
|
+
* file lookups and per-language settings all see the same tag.
|
|
199
|
+
* @param {unknown} config - Configuration object to validate.
|
|
191
200
|
* @returns {AppConfig} - Validated configuration.
|
|
192
201
|
* @throws {Error} If configuration is invalid.
|
|
193
202
|
*/
|
|
194
203
|
static validateConfig(config) {
|
|
195
|
-
if (!config
|
|
204
|
+
if (!isObject(config)) {
|
|
196
205
|
throw new Error("Configuration must be an object");
|
|
197
206
|
}
|
|
198
207
|
// Check config depth to prevent circular references
|
|
@@ -205,10 +214,10 @@ class InputValidator {
|
|
|
205
214
|
}
|
|
206
215
|
else {
|
|
207
216
|
try {
|
|
208
|
-
this.validateLanguageCode(config.source, "source language");
|
|
217
|
+
config.source = this.validateLanguageCode(config.source, "source language");
|
|
209
218
|
}
|
|
210
219
|
catch (error) {
|
|
211
|
-
errors.push(`Invalid source language: ${error
|
|
220
|
+
errors.push(`Invalid source language: ${errorMessage(error)}`);
|
|
212
221
|
}
|
|
213
222
|
}
|
|
214
223
|
if (!config.targets || !Array.isArray(config.targets) || config.targets.length === 0) {
|
|
@@ -216,10 +225,10 @@ class InputValidator {
|
|
|
216
225
|
}
|
|
217
226
|
else {
|
|
218
227
|
try {
|
|
219
|
-
this.validateLanguageCodes(config.targets, "target languages");
|
|
228
|
+
config.targets = this.validateLanguageCodes(config.targets, "target languages");
|
|
220
229
|
}
|
|
221
230
|
catch (error) {
|
|
222
|
-
errors.push(`Invalid target languages: ${error
|
|
231
|
+
errors.push(`Invalid target languages: ${errorMessage(error)}`);
|
|
223
232
|
}
|
|
224
233
|
}
|
|
225
234
|
if (!config.localesDir) {
|
|
@@ -231,7 +240,7 @@ class InputValidator {
|
|
|
231
240
|
this.validateProvider(config.apiProvider, "API provider");
|
|
232
241
|
}
|
|
233
242
|
catch (error) {
|
|
234
|
-
errors.push(`Invalid API provider: ${error
|
|
243
|
+
errors.push(`Invalid API provider: ${errorMessage(error)}`);
|
|
235
244
|
}
|
|
236
245
|
}
|
|
237
246
|
else {
|
|
@@ -248,27 +257,27 @@ class InputValidator {
|
|
|
248
257
|
this.validateProvider(provider, "fallback provider");
|
|
249
258
|
}
|
|
250
259
|
catch (error) {
|
|
251
|
-
errors.push(`Invalid provider in fallbackOrder: ${error
|
|
260
|
+
errors.push(`Invalid provider in fallbackOrder: ${errorMessage(error)}`);
|
|
252
261
|
}
|
|
253
262
|
}
|
|
254
263
|
}
|
|
255
264
|
}
|
|
256
265
|
// Concurrency validation
|
|
257
266
|
if (config.concurrencyLimit !== undefined) {
|
|
258
|
-
const limit = parseInt(config.concurrencyLimit);
|
|
267
|
+
const limit = parseInt(String(config.concurrencyLimit));
|
|
259
268
|
if (isNaN(limit) || limit < 1 || limit > 50) {
|
|
260
269
|
errors.push("'concurrencyLimit' must be a number between 1 and 50");
|
|
261
270
|
}
|
|
262
271
|
}
|
|
263
272
|
// Cache configuration validation
|
|
264
273
|
if (config.cacheSize !== undefined) {
|
|
265
|
-
const size = parseInt(config.cacheSize);
|
|
274
|
+
const size = parseInt(String(config.cacheSize));
|
|
266
275
|
if (isNaN(size) || size < 100 || size > 10000) {
|
|
267
276
|
errors.push("'cacheSize' must be a number between 100 and 10000");
|
|
268
277
|
}
|
|
269
278
|
}
|
|
270
279
|
if (config.cacheTTL !== undefined) {
|
|
271
|
-
const ttl = parseInt(config.cacheTTL);
|
|
280
|
+
const ttl = parseInt(String(config.cacheTTL));
|
|
272
281
|
if (isNaN(ttl) || ttl < 0) {
|
|
273
282
|
errors.push("'cacheTTL' must be a non-negative number (milliseconds)");
|
|
274
283
|
}
|
|
@@ -278,7 +287,7 @@ class InputValidator {
|
|
|
278
287
|
}
|
|
279
288
|
// API Config validation
|
|
280
289
|
if (config.apiConfig) {
|
|
281
|
-
if (
|
|
290
|
+
if (!isObject(config.apiConfig)) {
|
|
282
291
|
errors.push("'apiConfig' must be an object");
|
|
283
292
|
}
|
|
284
293
|
else {
|
|
@@ -287,7 +296,7 @@ class InputValidator {
|
|
|
287
296
|
}
|
|
288
297
|
// Rate Limiter validation
|
|
289
298
|
if (config.rateLimiter) {
|
|
290
|
-
if (
|
|
299
|
+
if (!isObject(config.rateLimiter)) {
|
|
291
300
|
errors.push("'rateLimiter' must be an object");
|
|
292
301
|
}
|
|
293
302
|
else {
|
|
@@ -296,7 +305,7 @@ class InputValidator {
|
|
|
296
305
|
}
|
|
297
306
|
// Retry options validation
|
|
298
307
|
if (config.retryOptions) {
|
|
299
|
-
if (
|
|
308
|
+
if (!isObject(config.retryOptions)) {
|
|
300
309
|
errors.push("'retryOptions' must be an object");
|
|
301
310
|
}
|
|
302
311
|
else {
|
|
@@ -305,7 +314,7 @@ class InputValidator {
|
|
|
305
314
|
}
|
|
306
315
|
// Context configuration validation
|
|
307
316
|
if (config.context) {
|
|
308
|
-
if (
|
|
317
|
+
if (!isObject(config.context)) {
|
|
309
318
|
errors.push("'context' must be an object");
|
|
310
319
|
}
|
|
311
320
|
else {
|
|
@@ -314,7 +323,7 @@ class InputValidator {
|
|
|
314
323
|
}
|
|
315
324
|
// Quality checks validation
|
|
316
325
|
if (config.qualityChecks) {
|
|
317
|
-
if (
|
|
326
|
+
if (!isObject(config.qualityChecks)) {
|
|
318
327
|
errors.push("'qualityChecks' must be an object");
|
|
319
328
|
}
|
|
320
329
|
else {
|
|
@@ -323,7 +332,7 @@ class InputValidator {
|
|
|
323
332
|
}
|
|
324
333
|
// Length control validation
|
|
325
334
|
if (config.lengthControl) {
|
|
326
|
-
if (
|
|
335
|
+
if (!isObject(config.lengthControl)) {
|
|
327
336
|
errors.push("'lengthControl' must be an object");
|
|
328
337
|
}
|
|
329
338
|
else {
|
|
@@ -332,7 +341,7 @@ class InputValidator {
|
|
|
332
341
|
}
|
|
333
342
|
// Advanced settings validation
|
|
334
343
|
if (config.advanced) {
|
|
335
|
-
if (
|
|
344
|
+
if (!isObject(config.advanced)) {
|
|
336
345
|
errors.push("'advanced' must be an object");
|
|
337
346
|
}
|
|
338
347
|
else {
|
|
@@ -361,7 +370,7 @@ class InputValidator {
|
|
|
361
370
|
}
|
|
362
371
|
/**
|
|
363
372
|
* Validate API config for each provider.
|
|
364
|
-
* @param {Record<string,
|
|
373
|
+
* @param {Record<string, unknown>} apiConfig - Raw API configuration object.
|
|
365
374
|
* @param {string[]} errors - Array to push errors to.
|
|
366
375
|
* @param {string[]} warnings - Array to push warnings to.
|
|
367
376
|
*/
|
|
@@ -371,13 +380,13 @@ class InputValidator {
|
|
|
371
380
|
warnings.push(`Unknown provider in apiConfig: '${provider}'`);
|
|
372
381
|
continue;
|
|
373
382
|
}
|
|
374
|
-
if (
|
|
383
|
+
if (!isObject(settings)) {
|
|
375
384
|
errors.push(`apiConfig.${provider} must be an object`);
|
|
376
385
|
continue;
|
|
377
386
|
}
|
|
378
387
|
// Validate temperature
|
|
379
388
|
if (settings.temperature !== undefined) {
|
|
380
|
-
const temp = parseFloat(settings.temperature);
|
|
389
|
+
const temp = parseFloat(String(settings.temperature));
|
|
381
390
|
if (isNaN(temp) || temp < 0 || temp > 2) {
|
|
382
391
|
errors.push(`apiConfig.${provider}.temperature must be between 0 and 2`);
|
|
383
392
|
}
|
|
@@ -387,7 +396,7 @@ class InputValidator {
|
|
|
387
396
|
}
|
|
388
397
|
// Validate maxTokens
|
|
389
398
|
if (settings.maxTokens !== undefined) {
|
|
390
|
-
const tokens = parseInt(settings.maxTokens);
|
|
399
|
+
const tokens = parseInt(String(settings.maxTokens));
|
|
391
400
|
if (isNaN(tokens) || tokens < 100 || tokens > 8000) {
|
|
392
401
|
errors.push(`apiConfig.${provider}.maxTokens must be between 100 and 8000`);
|
|
393
402
|
}
|
|
@@ -396,25 +405,26 @@ class InputValidator {
|
|
|
396
405
|
}
|
|
397
406
|
/**
|
|
398
407
|
* Validate rate limiter configuration.
|
|
399
|
-
* @param {
|
|
408
|
+
* @param {Record<string, unknown>} rateLimiter - Raw rate limiter configuration.
|
|
400
409
|
* @param {string[]} errors - Array to push errors to.
|
|
401
410
|
* @param {string[]} warnings - Array to push warnings to.
|
|
402
411
|
*/
|
|
403
412
|
static validateRateLimiterConfig(rateLimiter, errors, _warnings) {
|
|
404
413
|
if (rateLimiter.queueStrategy) {
|
|
405
414
|
const validStrategies = ["fifo", "priority"];
|
|
406
|
-
if (
|
|
415
|
+
if (typeof rateLimiter.queueStrategy !== "string" ||
|
|
416
|
+
!validStrategies.includes(rateLimiter.queueStrategy)) {
|
|
407
417
|
errors.push(`rateLimiter.queueStrategy must be one of: ${validStrategies.join(", ")}`);
|
|
408
418
|
}
|
|
409
419
|
}
|
|
410
420
|
if (rateLimiter.queueTimeout !== undefined) {
|
|
411
|
-
const timeout = parseInt(rateLimiter.queueTimeout);
|
|
421
|
+
const timeout = parseInt(String(rateLimiter.queueTimeout));
|
|
412
422
|
if (isNaN(timeout) || timeout < 1000 || timeout > 120000) {
|
|
413
423
|
errors.push("rateLimiter.queueTimeout must be between 1000 and 120000 ms");
|
|
414
424
|
}
|
|
415
425
|
}
|
|
416
426
|
if (rateLimiter.providerLimits) {
|
|
417
|
-
if (
|
|
427
|
+
if (!isObject(rateLimiter.providerLimits)) {
|
|
418
428
|
errors.push("rateLimiter.providerLimits must be an object");
|
|
419
429
|
}
|
|
420
430
|
else {
|
|
@@ -423,14 +433,24 @@ class InputValidator {
|
|
|
423
433
|
_warnings.push(`Unknown provider in providerLimits: '${provider}'`);
|
|
424
434
|
continue;
|
|
425
435
|
}
|
|
436
|
+
if (limits === null) {
|
|
437
|
+
errors.push(`rateLimiter.providerLimits.${provider} must be an object`);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
// Primitives and arrays always loaded (the rate limiter ignores them), so
|
|
441
|
+
// they stay accepted with a warning rather than failing validation.
|
|
442
|
+
if (!isObject(limits) || Array.isArray(limits)) {
|
|
443
|
+
_warnings.push(`rateLimiter.providerLimits.${provider} should be an object, ignoring it`);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
426
446
|
if (limits.rpm !== undefined) {
|
|
427
|
-
const rpm = parseInt(limits.rpm);
|
|
447
|
+
const rpm = parseInt(String(limits.rpm));
|
|
428
448
|
if (isNaN(rpm) || rpm < 1 || rpm > 10000) {
|
|
429
449
|
errors.push(`rateLimiter.providerLimits.${provider}.rpm must be between 1 and 10000`);
|
|
430
450
|
}
|
|
431
451
|
}
|
|
432
452
|
if (limits.concurrency !== undefined) {
|
|
433
|
-
const conc = parseInt(limits.concurrency);
|
|
453
|
+
const conc = parseInt(String(limits.concurrency));
|
|
434
454
|
if (isNaN(conc) || conc < 1 || conc > 50) {
|
|
435
455
|
errors.push(`rateLimiter.providerLimits.${provider}.concurrency must be between 1 and 50`);
|
|
436
456
|
}
|
|
@@ -441,38 +461,40 @@ class InputValidator {
|
|
|
441
461
|
}
|
|
442
462
|
/**
|
|
443
463
|
* Validate retry options.
|
|
444
|
-
* @param {
|
|
464
|
+
* @param {Record<string, unknown>} retryOptions - Raw retry options.
|
|
445
465
|
* @param {string[]} errors - Array to push errors to.
|
|
446
466
|
* @param {string[]} warnings - Array to push warnings to.
|
|
447
467
|
*/
|
|
448
468
|
static validateRetryOptions(retryOptions, errors, _warnings) {
|
|
449
469
|
if (retryOptions.maxRetries !== undefined) {
|
|
450
|
-
const retries = parseInt(retryOptions.maxRetries);
|
|
470
|
+
const retries = parseInt(String(retryOptions.maxRetries));
|
|
451
471
|
if (isNaN(retries) || retries < 0 || retries > 10) {
|
|
452
472
|
errors.push("retryOptions.maxRetries must be between 0 and 10");
|
|
453
473
|
}
|
|
454
474
|
}
|
|
455
475
|
if (retryOptions.initialDelay !== undefined) {
|
|
456
|
-
const delay = parseInt(retryOptions.initialDelay);
|
|
476
|
+
const delay = parseInt(String(retryOptions.initialDelay));
|
|
457
477
|
if (isNaN(delay) || delay < 100 || delay > 10000) {
|
|
458
478
|
errors.push("retryOptions.initialDelay must be between 100 and 10000 ms");
|
|
459
479
|
}
|
|
460
480
|
}
|
|
461
481
|
if (retryOptions.maxDelay !== undefined) {
|
|
462
|
-
const delay = parseInt(retryOptions.maxDelay);
|
|
482
|
+
const delay = parseInt(String(retryOptions.maxDelay));
|
|
463
483
|
if (isNaN(delay) || delay < 1000 || delay > 60000) {
|
|
464
484
|
errors.push("retryOptions.maxDelay must be between 1000 and 60000 ms");
|
|
465
485
|
}
|
|
466
486
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
487
|
+
// Compare the parsed numbers like the range checks above: raw strings compare lexically.
|
|
488
|
+
// A missing or unparseable delay is NaN, which never compares greater.
|
|
489
|
+
const initialDelay = parseInt(String(retryOptions.initialDelay));
|
|
490
|
+
const maxDelay = parseInt(String(retryOptions.maxDelay));
|
|
491
|
+
if (initialDelay > maxDelay) {
|
|
470
492
|
errors.push("retryOptions.initialDelay cannot be greater than maxDelay");
|
|
471
493
|
}
|
|
472
494
|
}
|
|
473
495
|
/**
|
|
474
496
|
* Validate context configuration.
|
|
475
|
-
* @param {
|
|
497
|
+
* @param {Record<string, unknown>} context - Raw context configuration.
|
|
476
498
|
* @param {string[]} errors - Array to push errors to.
|
|
477
499
|
* @param {string[]} warnings - Array to push warnings to.
|
|
478
500
|
*/
|
|
@@ -482,24 +504,24 @@ class InputValidator {
|
|
|
482
504
|
this.validateProvider(context.aiProvider, "context.aiProvider");
|
|
483
505
|
}
|
|
484
506
|
catch (error) {
|
|
485
|
-
errors.push(`Invalid context.aiProvider: ${error
|
|
507
|
+
errors.push(`Invalid context.aiProvider: ${errorMessage(error)}`);
|
|
486
508
|
}
|
|
487
509
|
}
|
|
488
510
|
if (context.minTextLength !== undefined) {
|
|
489
|
-
const length = parseInt(context.minTextLength);
|
|
511
|
+
const length = parseInt(String(context.minTextLength));
|
|
490
512
|
if (isNaN(length) || length < 0 || length > 1000) {
|
|
491
513
|
errors.push("context.minTextLength must be between 0 and 1000");
|
|
492
514
|
}
|
|
493
515
|
}
|
|
494
|
-
if (context.detection) {
|
|
516
|
+
if (isObject(context.detection)) {
|
|
495
517
|
if (context.detection.threshold !== undefined) {
|
|
496
|
-
const threshold = parseInt(context.detection.threshold);
|
|
518
|
+
const threshold = parseInt(String(context.detection.threshold));
|
|
497
519
|
if (isNaN(threshold) || threshold < 1 || threshold > 20) {
|
|
498
520
|
errors.push("context.detection.threshold must be between 1 and 20");
|
|
499
521
|
}
|
|
500
522
|
}
|
|
501
523
|
if (context.detection.minConfidence !== undefined) {
|
|
502
|
-
const confidence = parseFloat(context.detection.minConfidence);
|
|
524
|
+
const confidence = parseFloat(String(context.detection.minConfidence));
|
|
503
525
|
if (isNaN(confidence) || confidence < 0 || confidence > 1) {
|
|
504
526
|
errors.push("context.detection.minConfidence must be between 0 and 1");
|
|
505
527
|
}
|
|
@@ -516,7 +538,7 @@ class InputValidator {
|
|
|
516
538
|
}
|
|
517
539
|
/**
|
|
518
540
|
* Validate quality checks configuration.
|
|
519
|
-
* @param {
|
|
541
|
+
* @param {Record<string, unknown>} qualityChecks - Raw quality checks configuration.
|
|
520
542
|
* @param {string[]} errors - Array to push errors to.
|
|
521
543
|
* @param {string[]} warnings - Array to push warnings to.
|
|
522
544
|
*/
|
|
@@ -530,13 +552,14 @@ class InputValidator {
|
|
|
530
552
|
}
|
|
531
553
|
/**
|
|
532
554
|
* Validate length control configuration.
|
|
533
|
-
* @param {
|
|
555
|
+
* @param {Record<string, unknown>} lengthControl - Raw length control configuration.
|
|
534
556
|
* @param {string[]} errors - Array to push errors to.
|
|
535
557
|
*/
|
|
536
558
|
static validateLengthControl(lengthControl, errors) {
|
|
537
559
|
if (lengthControl.mode) {
|
|
538
560
|
const validModes = ["strict", "flexible", "exact", "relaxed", "smart"];
|
|
539
|
-
if (
|
|
561
|
+
if (typeof lengthControl.mode !== "string" ||
|
|
562
|
+
!validModes.includes(lengthControl.mode)) {
|
|
540
563
|
errors.push(`lengthControl.mode must be one of: ${validModes.join(", ")}`);
|
|
541
564
|
}
|
|
542
565
|
}
|
|
@@ -548,78 +571,32 @@ class InputValidator {
|
|
|
548
571
|
}
|
|
549
572
|
/**
|
|
550
573
|
* Validate advanced configuration.
|
|
551
|
-
* @param {
|
|
574
|
+
* @param {Record<string, unknown>} advanced - Raw advanced configuration.
|
|
552
575
|
* @param {string[]} errors - Array to push errors to.
|
|
553
576
|
*/
|
|
554
577
|
static validateAdvancedConfig(advanced, errors) {
|
|
555
578
|
if (advanced.timeoutMs !== undefined) {
|
|
556
|
-
const timeout = parseInt(advanced.timeoutMs);
|
|
579
|
+
const timeout = parseInt(String(advanced.timeoutMs));
|
|
557
580
|
if (isNaN(timeout) || timeout < 1000 || timeout > 300000) {
|
|
558
581
|
errors.push("advanced.timeoutMs must be between 1000 and 300000 ms (5 min)");
|
|
559
582
|
}
|
|
560
583
|
}
|
|
561
584
|
if (advanced.maxKeyLength !== undefined) {
|
|
562
|
-
const length = parseInt(advanced.maxKeyLength);
|
|
585
|
+
const length = parseInt(String(advanced.maxKeyLength));
|
|
563
586
|
if (isNaN(length) || length < 100 || length > 50000) {
|
|
564
587
|
errors.push("advanced.maxKeyLength must be between 100 and 50000");
|
|
565
588
|
}
|
|
566
589
|
}
|
|
567
590
|
if (advanced.maxBatchSize !== undefined) {
|
|
568
|
-
const size = parseInt(advanced.maxBatchSize);
|
|
591
|
+
const size = parseInt(String(advanced.maxBatchSize));
|
|
569
592
|
if (isNaN(size) || size < 1 || size > 100) {
|
|
570
593
|
errors.push("advanced.maxBatchSize must be between 1 and 100");
|
|
571
594
|
}
|
|
572
595
|
}
|
|
573
596
|
}
|
|
574
|
-
/**
|
|
575
|
-
* Sanitize translation text for security.
|
|
576
|
-
* @param {string | null} text - Text to sanitize.
|
|
577
|
-
* @returns {string | null} - Sanitized text.
|
|
578
|
-
*/
|
|
579
|
-
static sanitizeTranslationText(text) {
|
|
580
|
-
if (!text || typeof text !== "string") {
|
|
581
|
-
return text;
|
|
582
|
-
}
|
|
583
|
-
let sanitized = text;
|
|
584
|
-
for (const pattern of this.DANGEROUS_PATTERNS) {
|
|
585
|
-
sanitized = sanitized.replace(pattern, "");
|
|
586
|
-
}
|
|
587
|
-
sanitized = sanitized
|
|
588
|
-
.replace(/\r\n/g, "\n")
|
|
589
|
-
.replace(/\r/g, "\n")
|
|
590
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
591
|
-
.trim();
|
|
592
|
-
return sanitized;
|
|
593
|
-
}
|
|
594
|
-
/**
|
|
595
|
-
* Validate API key format.
|
|
596
|
-
* @param {string} apiKey - API key to validate.
|
|
597
|
-
* @param {string} providerName - Provider name.
|
|
598
|
-
* @returns {boolean} - True if valid.
|
|
599
|
-
* @throws {Error} If key is invalid.
|
|
600
|
-
*/
|
|
601
|
-
static validateApiKeyFormat(apiKey, providerName) {
|
|
602
|
-
if (!apiKey || typeof apiKey !== "string") {
|
|
603
|
-
throw new Error(`${providerName} API key must be a non-empty string`);
|
|
604
|
-
}
|
|
605
|
-
if (apiKey.length < 10) {
|
|
606
|
-
throw new Error(`${providerName} API key appears too short`);
|
|
607
|
-
}
|
|
608
|
-
if (apiKey.length > 200) {
|
|
609
|
-
throw new Error(`${providerName} API key appears too long`);
|
|
610
|
-
}
|
|
611
|
-
const lowercaseKey = apiKey.toLowerCase();
|
|
612
|
-
const invalidPatterns = ["test", "placeholder", "example", "your-api-key", "sk-test"];
|
|
613
|
-
for (const pattern of invalidPatterns) {
|
|
614
|
-
if (lowercaseKey.includes(pattern)) {
|
|
615
|
-
throw new Error(`${providerName} API key appears to be a placeholder`);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
return true;
|
|
619
|
-
}
|
|
620
597
|
/**
|
|
621
598
|
* Validate object depth to prevent circular references and deep nesting.
|
|
622
|
-
* @param {
|
|
599
|
+
* @param {unknown} obj - Object to validate.
|
|
623
600
|
* @param {number} [maxDepth=10] - Maximum depth.
|
|
624
601
|
* @param {number} [currentDepth=0] - Current depth.
|
|
625
602
|
* @returns {boolean} - True if valid.
|
|
@@ -638,33 +615,5 @@ class InputValidator {
|
|
|
638
615
|
}
|
|
639
616
|
return true;
|
|
640
617
|
}
|
|
641
|
-
/**
|
|
642
|
-
* Validate request rate for an identifier.
|
|
643
|
-
* @param {string} identifier - Identifier (e.g., user ID or IP).
|
|
644
|
-
* @param {number} [maxRequestsPerMinute=60] - Max requests per minute.
|
|
645
|
-
* @returns {boolean} - True if allowed.
|
|
646
|
-
* @throws {Error} If rate limit exceeded.
|
|
647
|
-
*/
|
|
648
|
-
static validateRequestRate(identifier, maxRequestsPerMinute = 60) {
|
|
649
|
-
const now = Date.now();
|
|
650
|
-
const minute = Math.floor(now / 60000);
|
|
651
|
-
if (!this._requestCounts) {
|
|
652
|
-
this._requestCounts = new Map();
|
|
653
|
-
}
|
|
654
|
-
const key = `${identifier}-${minute}`;
|
|
655
|
-
const currentCount = this._requestCounts.get(key) || 0;
|
|
656
|
-
if (currentCount >= maxRequestsPerMinute) {
|
|
657
|
-
throw new Error(`Too many requests from ${identifier}. Please try again later.`);
|
|
658
|
-
}
|
|
659
|
-
this._requestCounts.set(key, currentCount + 1);
|
|
660
|
-
// Cleanup old entries
|
|
661
|
-
for (const [k] of this._requestCounts) {
|
|
662
|
-
const [, keyMinute] = k.split("-");
|
|
663
|
-
if (parseInt(keyMinute) < minute - 5) {
|
|
664
|
-
this._requestCounts.delete(k);
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
return true;
|
|
668
|
-
}
|
|
669
618
|
}
|
|
670
619
|
export default InputValidator;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The locale file the translations of `sourceFile` into `language` belong in, in the source's
|
|
3
|
+
* layout: `<root>/<source>/translation.json` gives `<root>/<language>/translation.json`, any
|
|
4
|
+
* other source file gives `<language>.json` next to it.
|
|
5
|
+
* @throws {Error} If `language` would put the file outside the locales directory.
|
|
6
|
+
*/
|
|
7
|
+
export declare function targetLocaleFile(sourceFile: string, language: string): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import InputValidator from "./input-validator.js";
|
|
3
|
+
/**
|
|
4
|
+
* The locale file the translations of `sourceFile` into `language` belong in, in the source's
|
|
5
|
+
* layout: `<root>/<source>/translation.json` gives `<root>/<language>/translation.json`, any
|
|
6
|
+
* other source file gives `<language>.json` next to it.
|
|
7
|
+
* @throws {Error} If `language` would put the file outside the locales directory.
|
|
8
|
+
*/
|
|
9
|
+
export function targetLocaleFile(sourceFile, language) {
|
|
10
|
+
const sourceDir = path.dirname(sourceFile);
|
|
11
|
+
if (path.basename(sourceFile) === "translation.json") {
|
|
12
|
+
const languageDir = InputValidator.createSafeFilePath(path.dirname(sourceDir), language);
|
|
13
|
+
return path.join(languageDir, "translation.json");
|
|
14
|
+
}
|
|
15
|
+
return InputValidator.createSafeFilePath(sourceDir, `${language}.json`);
|
|
16
|
+
}
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ declare class Logger {
|
|
|
42
42
|
* Parse size string (e.g., "10MB") to bytes.
|
|
43
43
|
* @param sizeStr - Size string to parse.
|
|
44
44
|
*/
|
|
45
|
-
parseSize(sizeStr:
|
|
45
|
+
parseSize(sizeStr: unknown): number;
|
|
46
46
|
/**
|
|
47
47
|
* Check and rotate logs if needed.
|
|
48
48
|
*/
|
|
@@ -64,14 +64,14 @@ declare class Logger {
|
|
|
64
64
|
* @param message - Log message.
|
|
65
65
|
* @param data - Additional data to log.
|
|
66
66
|
*/
|
|
67
|
-
formatMessage(level: string, message: string, data?:
|
|
67
|
+
formatMessage(level: string, message: string, data?: unknown): string;
|
|
68
68
|
/**
|
|
69
69
|
* Write to log file.
|
|
70
70
|
* @param level - Log level.
|
|
71
71
|
* @param message - Log message.
|
|
72
72
|
* @param data - Additional data to log.
|
|
73
73
|
*/
|
|
74
|
-
writeToFile(level: string, message: string, data?:
|
|
74
|
+
writeToFile(level: string, message: string, data?: unknown): Promise<void>;
|
|
75
75
|
/**
|
|
76
76
|
* Simple console log with debug awareness.
|
|
77
77
|
* @param message - Message to log
|
|
@@ -83,32 +83,32 @@ declare class Logger {
|
|
|
83
83
|
* @param message - Error message.
|
|
84
84
|
* @param data - Additional data.
|
|
85
85
|
*/
|
|
86
|
-
error(message: string, data?:
|
|
86
|
+
error(message: string, data?: unknown): Promise<void>;
|
|
87
87
|
/**
|
|
88
88
|
* Log warning.
|
|
89
89
|
* @param message - Warning message.
|
|
90
90
|
* @param data - Additional data.
|
|
91
91
|
*/
|
|
92
|
-
warn(message: string, data?:
|
|
92
|
+
warn(message: string, data?: unknown): Promise<void>;
|
|
93
93
|
/**
|
|
94
94
|
* Log info.
|
|
95
95
|
* @param message - Info message.
|
|
96
96
|
* @param data - Additional data.
|
|
97
97
|
*/
|
|
98
|
-
info(message: string, data?:
|
|
98
|
+
info(message: string, data?: unknown): Promise<void>;
|
|
99
99
|
/**
|
|
100
100
|
* Log debug.
|
|
101
101
|
* @param message - Debug message.
|
|
102
102
|
* @param data - Additional data.
|
|
103
103
|
*/
|
|
104
|
-
debug(message: string, data?:
|
|
104
|
+
debug(message: string, data?: unknown): Promise<void>;
|
|
105
105
|
/**
|
|
106
106
|
* Log based on diagnostics level.
|
|
107
107
|
* @param level - Log level.
|
|
108
108
|
* @param message - Log message.
|
|
109
109
|
* @param data - Additional data.
|
|
110
110
|
*/
|
|
111
|
-
diagnostics(level: string, message: string, data?:
|
|
111
|
+
diagnostics(level: string, message: string, data?: unknown): Promise<void>;
|
|
112
112
|
/**
|
|
113
113
|
* Get log statistics.
|
|
114
114
|
* @returns Object containing log stats.
|