@gotza02/sequential-thinking 10000.2.0 → 10000.2.2
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.bak +197 -0
- package/dist/.gemini_graph_cache.json.bak +1641 -0
- package/dist/graph.d.ts +7 -0
- package/dist/graph.js +136 -113
- package/dist/intelligent-code.d.ts +8 -0
- package/dist/intelligent-code.js +152 -125
- package/dist/intelligent-code.test.d.ts +1 -0
- package/dist/intelligent-code.test.js +104 -0
- package/dist/lib/formatters.d.ts +9 -0
- package/dist/lib/formatters.js +119 -0
- package/dist/lib/validators.d.ts +45 -0
- package/dist/lib/validators.js +232 -0
- package/dist/lib.js +23 -265
- package/dist/tools/sports/core/base.d.ts +3 -2
- package/dist/tools/sports/core/base.js +12 -10
- package/dist/tools/sports/core/cache.d.ts +9 -0
- package/dist/tools/sports/core/cache.js +25 -3
- package/dist/tools/sports/core/types.d.ts +6 -2
- package/dist/tools/sports/providers/api.d.ts +4 -0
- package/dist/tools/sports/providers/api.js +110 -27
- package/dist/tools/sports/tools/betting.js +16 -16
- package/dist/tools/sports/tools/league.d.ts +2 -7
- package/dist/tools/sports/tools/league.js +198 -8
- package/dist/tools/sports/tools/live.js +80 -38
- package/dist/tools/sports/tools/match-calculations.d.ts +51 -0
- package/dist/tools/sports/tools/match-calculations.js +171 -0
- package/dist/tools/sports/tools/match-helpers.d.ts +21 -0
- package/dist/tools/sports/tools/match-helpers.js +57 -0
- package/dist/tools/sports/tools/match.js +227 -125
- package/dist/tools/sports.js +3 -3
- package/dist/utils.d.ts +111 -44
- package/dist/utils.js +510 -305
- package/dist/utils.test.js +3 -3
- package/package.json +1 -1
- package/CLAUDE.md +0 -231
package/dist/utils.js
CHANGED
|
@@ -1,214 +1,362 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import * as dns from 'dns/promises';
|
|
5
|
-
import { URL } from 'url';
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as dns from 'node:dns/promises';
|
|
5
|
+
import { URL } from 'node:url';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
-
//
|
|
7
|
+
// ==========================================
|
|
8
|
+
// Constants & Configuration
|
|
9
|
+
// ==========================================
|
|
10
|
+
const CONFIG = {
|
|
11
|
+
COMMAND_TIMEOUT: 60_000,
|
|
12
|
+
MAX_BUFFER: 5 * 1024 * 1024,
|
|
13
|
+
MAX_PATTERN_LENGTH: 500,
|
|
14
|
+
MAX_QUANTIFIERS: 20,
|
|
15
|
+
MAX_LOCKS: 1000,
|
|
16
|
+
DNS_TIMEOUT: 5_000,
|
|
17
|
+
MAX_INPUT_LENGTH: 10_000,
|
|
18
|
+
RETRY: {
|
|
19
|
+
MAX_RETRIES: 3,
|
|
20
|
+
BASE_DELAY: 1000,
|
|
21
|
+
BACKOFF_MULTIPLIER: 2,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
8
24
|
/**
|
|
9
|
-
* Shell metacharacters that could enable command injection
|
|
10
|
-
*
|
|
25
|
+
* Shell metacharacters that could enable command injection.
|
|
26
|
+
* Using character class to avoid regex complexity issues.
|
|
11
27
|
*/
|
|
12
|
-
const SHELL_METACHARACTERS = /[;|&`$()<>\{\}\[\]\*\?~]
|
|
28
|
+
const SHELL_METACHARACTERS = /[;|&`$()<>\{\}\[\]\*\?~]/;
|
|
13
29
|
/**
|
|
14
|
-
* Whitelist of safe commands
|
|
15
|
-
* Commands must be explicitly allowed for security
|
|
30
|
+
* Whitelist of safe commands. Consider loading from config file for flexibility.
|
|
16
31
|
*/
|
|
17
32
|
const SAFE_COMMANDS = new Set([
|
|
18
|
-
// Common development tools
|
|
19
33
|
'ls', 'cd', 'pwd', 'cat', 'head', 'tail', 'grep', 'find',
|
|
20
34
|
'echo', 'printf', 'wc', 'sort', 'uniq', 'cut', 'awk', 'sed',
|
|
21
|
-
// Build tools
|
|
22
35
|
'npm', 'pnpm', 'yarn', 'bun', 'python', 'python3', 'node', 'deno',
|
|
23
36
|
'make', 'cmake', 'cargo', 'go', 'rustc', 'gcc', 'g++', 'clang',
|
|
24
|
-
|
|
25
|
-
'git',
|
|
26
|
-
// File operations (safe ones)
|
|
27
|
-
'cp', 'mv', 'mkdir', 'touch', 'rm', 'rmdir',
|
|
28
|
-
// System info (read-only)
|
|
37
|
+
'git', 'cp', 'mv', 'mkdir', 'touch', 'rm', 'rmdir',
|
|
29
38
|
'df', 'du', 'free', 'uname', 'which', 'whereis', 'type',
|
|
30
|
-
// Network diagnostics
|
|
31
39
|
'ping', 'traceroute', 'nslookup', 'curl', 'wget', 'ssh',
|
|
32
|
-
// Text editors
|
|
33
40
|
'vi', 'vim', 'nano', 'code',
|
|
34
|
-
// Archive tools
|
|
35
41
|
'tar', 'gzip', 'gunzip', 'zip', 'unzip',
|
|
36
|
-
// Permissions (read-only checks)
|
|
37
42
|
'test', 'stat', 'file',
|
|
38
43
|
]);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const DANGEROUS_PATTERNS = [
|
|
45
|
+
/rm\s+-[rf]+\s*([/~]|\.{2,})/i, // rm -rf patterns
|
|
46
|
+
/mkfs/i, // Filesystem formatting
|
|
47
|
+
/\bdd\s+if=/i, // Direct disk write
|
|
48
|
+
/chmod\s+0+/, // Remove permissions
|
|
49
|
+
/chown\s+-R\s+root:/, // Recursive ownership
|
|
50
|
+
/fdisk/i, // Partition tools
|
|
51
|
+
/>\s*\/dev\/\w+/, // Device writes
|
|
52
|
+
/(?:curl|wget)\s+.*\|\s*(?:sh|bash|zsh)/i, // Pipe to shell
|
|
53
|
+
/base64\s+-[di].*\|/i, // Decode pipes
|
|
54
|
+
/eval\s*\(/i, // Eval
|
|
55
|
+
/\$\([^)]*\)/, // Command substitution
|
|
56
|
+
/`[^`]*`/, // Backtick substitution
|
|
57
|
+
/\$\{[^}]*\}/, // Variable expansion
|
|
58
|
+
];
|
|
59
|
+
// ==========================================
|
|
60
|
+
// Custom Error Classes
|
|
61
|
+
// ==========================================
|
|
62
|
+
export class SecurityError extends Error {
|
|
63
|
+
code;
|
|
64
|
+
constructor(message, code) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.code = code;
|
|
67
|
+
this.name = 'SecurityError';
|
|
68
|
+
Error.captureStackTrace(this, this.constructor);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export class ValidationError extends Error {
|
|
72
|
+
field;
|
|
73
|
+
constructor(message, field) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.field = field;
|
|
76
|
+
this.name = 'ValidationError';
|
|
77
|
+
Error.captureStackTrace(this, this.constructor);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export class TimeoutError extends Error {
|
|
81
|
+
timeout;
|
|
82
|
+
constructor(message, timeout) {
|
|
83
|
+
super(message);
|
|
84
|
+
this.timeout = timeout;
|
|
85
|
+
this.name = 'TimeoutError';
|
|
86
|
+
Error.captureStackTrace(this, this.constructor);
|
|
87
|
+
}
|
|
44
88
|
}
|
|
45
89
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
90
|
+
* Robust command parser supporting quotes and escapes.
|
|
91
|
+
* State machine implementation for correctness.
|
|
48
92
|
*/
|
|
49
93
|
function parseCommand(command) {
|
|
50
|
-
// Remove leading/trailing whitespace
|
|
51
94
|
const trimmed = command.trim();
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return { command: trimmed, args: [] };
|
|
56
|
-
}
|
|
57
|
-
const cmd = trimmed.slice(0, firstSpace);
|
|
58
|
-
const argsString = trimmed.slice(firstSpace + 1);
|
|
59
|
-
// Simple argument splitting (handles quoted strings)
|
|
95
|
+
if (!trimmed) {
|
|
96
|
+
throw new ValidationError('Empty command');
|
|
97
|
+
}
|
|
60
98
|
const args = [];
|
|
61
99
|
let current = '';
|
|
62
|
-
let
|
|
63
|
-
let
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
100
|
+
let inDoubleQuotes = false;
|
|
101
|
+
let inSingleQuotes = false;
|
|
102
|
+
let escapeNext = false;
|
|
103
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
104
|
+
const char = trimmed[i];
|
|
105
|
+
const nextChar = trimmed[i + 1];
|
|
106
|
+
if (escapeNext) {
|
|
107
|
+
if (inDoubleQuotes) {
|
|
108
|
+
// Only specific chars are escapable in double quotes
|
|
109
|
+
if (char === '"' || char === '\\' || char === '$' || char === '`') {
|
|
110
|
+
current += char;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
current += '\\' + char;
|
|
114
|
+
}
|
|
74
115
|
}
|
|
75
116
|
else {
|
|
76
117
|
current += char;
|
|
77
118
|
}
|
|
119
|
+
escapeNext = false;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (char === '\\' && !inSingleQuotes) {
|
|
123
|
+
escapeNext = true;
|
|
124
|
+
continue;
|
|
78
125
|
}
|
|
79
|
-
|
|
126
|
+
if (char === "'" && !inDoubleQuotes) {
|
|
127
|
+
inSingleQuotes = !inSingleQuotes;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (char === '"' && !inSingleQuotes) {
|
|
131
|
+
inDoubleQuotes = !inDoubleQuotes;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (/\s/.test(char) && !inSingleQuotes && !inDoubleQuotes) {
|
|
80
135
|
if (current) {
|
|
81
136
|
args.push(current);
|
|
82
137
|
current = '';
|
|
83
138
|
}
|
|
139
|
+
continue;
|
|
84
140
|
}
|
|
85
|
-
|
|
86
|
-
current += char;
|
|
87
|
-
}
|
|
141
|
+
current += char;
|
|
88
142
|
}
|
|
89
|
-
if (
|
|
143
|
+
if (inSingleQuotes || inDoubleQuotes) {
|
|
144
|
+
throw new ValidationError('Unclosed quotes in command');
|
|
145
|
+
}
|
|
146
|
+
if (escapeNext) {
|
|
147
|
+
current += '\\';
|
|
148
|
+
}
|
|
149
|
+
if (current || args.length === 0) {
|
|
90
150
|
args.push(current);
|
|
91
151
|
}
|
|
92
|
-
|
|
152
|
+
const [cmd, ...cmdArgs] = args;
|
|
153
|
+
return { command: cmd, args: cmdArgs };
|
|
154
|
+
}
|
|
155
|
+
function hasShellMetacharacters(command) {
|
|
156
|
+
return SHELL_METACHARACTERS.test(command);
|
|
93
157
|
}
|
|
94
158
|
/**
|
|
95
|
-
*
|
|
96
|
-
* @throws
|
|
159
|
+
* Validates command against injection attacks.
|
|
160
|
+
* @throws SecurityError if validation fails
|
|
97
161
|
*/
|
|
98
162
|
function validateCommand(command) {
|
|
99
|
-
// Check for shell metacharacters
|
|
100
163
|
if (hasShellMetacharacters(command)) {
|
|
101
|
-
throw new
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
throw new Error(`Command '${baseCommand}' is not in the allowed commands list. ` +
|
|
109
|
-
`If this command is safe, add it to SAFE_COMMANDS in utils.ts.`);
|
|
110
|
-
}
|
|
111
|
-
// Additional checks for potentially dangerous patterns
|
|
112
|
-
// Using string-based RegExp with proper escaping
|
|
113
|
-
const dangerousPatterns = [
|
|
114
|
-
new RegExp('rm\\s+-rf?\\s+([/~]|\\.\\.|\\.$)'), // rm -rf patterns
|
|
115
|
-
new RegExp('mkfs'), // Filesystem formatting
|
|
116
|
-
new RegExp('dd\\s+if='), // Direct disk write
|
|
117
|
-
new RegExp('chmod\\s+000'), // Remove all permissions
|
|
118
|
-
new RegExp('chown\\s+-R\\s+root:'), // Recursive ownership change
|
|
119
|
-
new RegExp('fdisk'), // Partition tools
|
|
120
|
-
new RegExp('>\\s*/dev/\\w+\\s*\\+w'), // Write to device
|
|
121
|
-
// New patterns:
|
|
122
|
-
new RegExp('curl\\s+.*\\s*\\|\\s*sh'), // curl | sh
|
|
123
|
-
new RegExp('wget\\s+.*\\s*\\|\\s*sh'), // wget | sh
|
|
124
|
-
new RegExp('base64\\s+-d\\s*\\|'), // encoded commands
|
|
125
|
-
new RegExp('eval\\s*\\('), // eval()
|
|
126
|
-
new RegExp('\\$\\(\\(.*\\)\\)'), // $() command substitution
|
|
127
|
-
new RegExp('`.*`'), // backtick substitution
|
|
128
|
-
];
|
|
129
|
-
for (const pattern of dangerousPatterns) {
|
|
164
|
+
throw new SecurityError('Command contains shell metacharacters (;|&`$()<>). Use programmatic APIs instead.', 'CMD_INJECTION_DETECTED');
|
|
165
|
+
}
|
|
166
|
+
const { command: baseCmd } = parseCommand(command);
|
|
167
|
+
if (!SAFE_COMMANDS.has(baseCmd)) {
|
|
168
|
+
throw new SecurityError(`Command '${baseCmd}' is not whitelisted. Add to SAFE_COMMANDS if safe.`, 'CMD_NOT_WHITELISTED');
|
|
169
|
+
}
|
|
170
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
130
171
|
if (pattern.test(command)) {
|
|
131
|
-
throw new
|
|
172
|
+
throw new SecurityError(`Command matches dangerous pattern: ${pattern.source}`, 'DANGEROUS_PATTERN');
|
|
132
173
|
}
|
|
133
174
|
}
|
|
134
175
|
}
|
|
135
|
-
// export const execAsync = promisify(exec); // Removed simple promisify
|
|
136
176
|
/**
|
|
137
|
-
* Execute
|
|
138
|
-
* Uses spawn() instead of exec() to avoid shell interpretation.
|
|
139
|
-
*
|
|
140
|
-
* @param command - The command string to execute
|
|
141
|
-
* @param options - Execution options (timeout, maxBuffer)
|
|
142
|
-
* @returns Promise with stdout and stderr
|
|
143
|
-
* @throws Error if command contains shell metacharacters or is not whitelisted
|
|
177
|
+
* Execute command securely using spawn (no shell interpretation).
|
|
144
178
|
*/
|
|
145
|
-
export function execAsync(command, options = {}) {
|
|
179
|
+
export async function execAsync(command, options = {}) {
|
|
180
|
+
validateCommand(command);
|
|
181
|
+
const { command: cmd, args } = parseCommand(command);
|
|
182
|
+
const timeout = options.timeout ?? CONFIG.COMMAND_TIMEOUT;
|
|
183
|
+
const maxBuffer = options.maxBuffer ?? CONFIG.MAX_BUFFER;
|
|
146
184
|
return new Promise((resolve, reject) => {
|
|
147
|
-
|
|
148
|
-
// Validate command for security
|
|
149
|
-
validateCommand(command);
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
return reject(error);
|
|
153
|
-
}
|
|
154
|
-
const timeout = options.timeout || 60000; // Default 60s timeout
|
|
155
|
-
const maxBuffer = options.maxBuffer || 1024 * 1024 * 5; // Default 5MB buffer
|
|
156
|
-
// Parse command into parts
|
|
157
|
-
const { command: cmd, args } = parseCommand(command);
|
|
158
|
-
// Use spawn instead of exec to avoid shell interpretation
|
|
185
|
+
const startTime = performance.now();
|
|
159
186
|
const child = spawn(cmd, args, {
|
|
160
187
|
timeout,
|
|
188
|
+
cwd: options.cwd,
|
|
189
|
+
env: { ...process.env, ...options.env },
|
|
190
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
191
|
+
detached: false,
|
|
161
192
|
});
|
|
162
193
|
let stdout = '';
|
|
163
194
|
let stderr = '';
|
|
195
|
+
let stdoutLength = 0;
|
|
196
|
+
let stderrLength = 0;
|
|
197
|
+
let killed = false;
|
|
198
|
+
const kill = (signal = 'SIGTERM') => {
|
|
199
|
+
if (!killed && !child.killed) {
|
|
200
|
+
killed = true;
|
|
201
|
+
child.kill(signal);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
// Buffer limits to prevent memory exhaustion
|
|
164
205
|
child.stdout?.on('data', (data) => {
|
|
165
|
-
|
|
206
|
+
if (stdoutLength + data.length > maxBuffer) {
|
|
207
|
+
kill();
|
|
208
|
+
reject(new SecurityError('Stdout exceeded maxBuffer', 'BUFFER_OVERFLOW'));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
stdout += data.toString('utf-8');
|
|
212
|
+
stdoutLength += data.length;
|
|
166
213
|
});
|
|
167
214
|
child.stderr?.on('data', (data) => {
|
|
168
|
-
|
|
215
|
+
if (stderrLength + data.length > maxBuffer) {
|
|
216
|
+
kill();
|
|
217
|
+
reject(new SecurityError('Stderr exceeded maxBuffer', 'BUFFER_OVERFLOW'));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
stderr += data.toString('utf-8');
|
|
221
|
+
stderrLength += data.length;
|
|
169
222
|
});
|
|
170
|
-
|
|
223
|
+
const errorHandler = (error) => {
|
|
224
|
+
kill('SIGKILL');
|
|
171
225
|
reject(error);
|
|
172
|
-
}
|
|
173
|
-
child.on('
|
|
174
|
-
|
|
226
|
+
};
|
|
227
|
+
child.on('error', errorHandler);
|
|
228
|
+
child.on('close', (code, signal) => {
|
|
229
|
+
const executionTime = Math.round(performance.now() - startTime);
|
|
230
|
+
if (signal) {
|
|
231
|
+
reject(new Error(`Command terminated by signal ${signal}`));
|
|
232
|
+
}
|
|
233
|
+
else if (code !== 0) {
|
|
175
234
|
const error = new Error(`Command failed with exit code ${code}`);
|
|
176
235
|
error.stdout = stdout;
|
|
177
236
|
error.stderr = stderr;
|
|
178
|
-
error.exitCode = code;
|
|
237
|
+
error.exitCode = code ?? -1;
|
|
238
|
+
error.executionTime = executionTime;
|
|
179
239
|
reject(error);
|
|
180
240
|
}
|
|
181
241
|
else {
|
|
182
|
-
resolve({
|
|
242
|
+
resolve({
|
|
243
|
+
stdout,
|
|
244
|
+
stderr,
|
|
245
|
+
exitCode: code ?? 0,
|
|
246
|
+
executionTime,
|
|
247
|
+
});
|
|
183
248
|
}
|
|
184
249
|
});
|
|
185
250
|
});
|
|
186
251
|
}
|
|
252
|
+
// ==========================================
|
|
253
|
+
// Synchronization Primitives
|
|
254
|
+
// ==========================================
|
|
187
255
|
export class AsyncMutex {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
256
|
+
queue = [];
|
|
257
|
+
locked = false;
|
|
258
|
+
async acquire() {
|
|
259
|
+
if (!this.locked) {
|
|
260
|
+
this.locked = true;
|
|
261
|
+
return () => this.release();
|
|
262
|
+
}
|
|
263
|
+
return new Promise((resolve) => {
|
|
264
|
+
this.queue.push(() => resolve(() => this.release()));
|
|
193
265
|
});
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
266
|
+
}
|
|
267
|
+
release() {
|
|
268
|
+
if (this.queue.length > 0) {
|
|
269
|
+
const next = this.queue.shift();
|
|
270
|
+
next();
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
this.locked = false;
|
|
274
|
+
}
|
|
199
275
|
}
|
|
200
276
|
async dispatch(fn) {
|
|
201
|
-
const
|
|
277
|
+
const release = await this.acquire();
|
|
202
278
|
try {
|
|
203
279
|
return await Promise.resolve(fn());
|
|
204
280
|
}
|
|
205
281
|
finally {
|
|
206
|
-
|
|
282
|
+
release();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
export class FileLock {
|
|
287
|
+
locks = new Map();
|
|
288
|
+
queue = new Map();
|
|
289
|
+
maxLocks;
|
|
290
|
+
constructor(maxLocks = CONFIG.MAX_LOCKS) {
|
|
291
|
+
this.maxLocks = maxLocks;
|
|
292
|
+
}
|
|
293
|
+
async acquire(filePath, timeout = 5000) {
|
|
294
|
+
const absolutePath = path.resolve(filePath);
|
|
295
|
+
if (this.locks.has(absolutePath)) {
|
|
296
|
+
// Wait for existing lock with timeout
|
|
297
|
+
await this.waitForLock(absolutePath, timeout);
|
|
298
|
+
}
|
|
299
|
+
if (this.locks.size >= this.maxLocks) {
|
|
300
|
+
this.cleanupStaleLocks();
|
|
301
|
+
}
|
|
302
|
+
this.locks.set(absolutePath, {
|
|
303
|
+
acquiredAt: Date.now(),
|
|
304
|
+
timeout,
|
|
305
|
+
resolve: () => { }, // Placeholder
|
|
306
|
+
});
|
|
307
|
+
return {
|
|
308
|
+
release: () => {
|
|
309
|
+
this.release(absolutePath);
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
waitForLock(filePath, timeout) {
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
const timer = setTimeout(() => {
|
|
316
|
+
reject(new TimeoutError(`Timeout acquiring lock for ${filePath}`, timeout));
|
|
317
|
+
}, timeout);
|
|
318
|
+
if (!this.queue.has(filePath)) {
|
|
319
|
+
this.queue.set(filePath, []);
|
|
320
|
+
}
|
|
321
|
+
this.queue.get(filePath).push(() => {
|
|
322
|
+
clearTimeout(timer);
|
|
323
|
+
resolve();
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
release(filePath) {
|
|
328
|
+
this.locks.delete(filePath);
|
|
329
|
+
const waiting = this.queue.get(filePath);
|
|
330
|
+
if (waiting?.length) {
|
|
331
|
+
const next = waiting.shift();
|
|
332
|
+
next();
|
|
333
|
+
if (waiting.length === 0) {
|
|
334
|
+
this.queue.delete(filePath);
|
|
335
|
+
}
|
|
207
336
|
}
|
|
208
337
|
}
|
|
338
|
+
cleanupStaleLocks() {
|
|
339
|
+
const now = Date.now();
|
|
340
|
+
const staleThreshold = 30_000; // 30 seconds
|
|
341
|
+
for (const [filePath, info] of this.locks.entries()) {
|
|
342
|
+
if (now - info.acquiredAt > info.timeout + staleThreshold) {
|
|
343
|
+
this.locks.delete(filePath);
|
|
344
|
+
this.queue.delete(filePath);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
getLockCount() {
|
|
349
|
+
return this.locks.size;
|
|
350
|
+
}
|
|
351
|
+
getQueueCount() {
|
|
352
|
+
return Array.from(this.queue.values()).reduce((sum, arr) => sum + arr.length, 0);
|
|
353
|
+
}
|
|
209
354
|
}
|
|
355
|
+
export const fileLock = new FileLock();
|
|
356
|
+
// ==========================================
|
|
357
|
+
// Validation Utilities
|
|
358
|
+
// ==========================================
|
|
210
359
|
export function validatePath(requestedPath) {
|
|
211
|
-
// Special case for current directory to avoid resolution issues in restricted environments
|
|
212
360
|
if (requestedPath === '.' || requestedPath === './' || !requestedPath) {
|
|
213
361
|
return fs.realpathSync(process.cwd());
|
|
214
362
|
}
|
|
@@ -216,268 +364,325 @@ export function validatePath(requestedPath) {
|
|
|
216
364
|
let absolutePath;
|
|
217
365
|
let rootDir;
|
|
218
366
|
try {
|
|
219
|
-
// Use realpath to resolve symlinks, which is common in environments like Termux
|
|
220
367
|
absolutePath = fs.realpathSync(resolvedPath);
|
|
221
368
|
rootDir = fs.realpathSync(process.cwd());
|
|
222
369
|
}
|
|
223
370
|
catch (e) {
|
|
224
|
-
//
|
|
371
|
+
// File doesn't exist yet, use resolved path
|
|
225
372
|
absolutePath = resolvedPath;
|
|
226
373
|
rootDir = path.resolve(process.cwd());
|
|
227
374
|
}
|
|
228
|
-
|
|
229
|
-
|
|
375
|
+
// Ensure path is within project root (prevent directory traversal)
|
|
376
|
+
const relative = path.relative(rootDir, absolutePath);
|
|
377
|
+
if (relative.startsWith('..') || relative.startsWith('\\..')) {
|
|
378
|
+
throw new SecurityError(`Path '${requestedPath}' is outside project root`, 'PATH_TRAVERSAL');
|
|
230
379
|
}
|
|
231
380
|
return absolutePath;
|
|
232
381
|
}
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
// IPv4 check
|
|
237
|
-
const parts = cleanIp.split('.').map(Number);
|
|
238
|
-
if (parts.length === 4) {
|
|
239
|
-
if (parts[0] === 127)
|
|
240
|
-
return true;
|
|
241
|
-
if (parts[0] === 10)
|
|
242
|
-
return true;
|
|
243
|
-
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31)
|
|
244
|
-
return true;
|
|
245
|
-
if (parts[0] === 192 && parts[1] === 168)
|
|
246
|
-
return true;
|
|
247
|
-
if (parts[0] === 0)
|
|
248
|
-
return true;
|
|
382
|
+
function isPrivateIPv4(ip) {
|
|
383
|
+
const parts = ip.split('.').map(Number);
|
|
384
|
+
if (parts.length !== 4 || parts.some(p => isNaN(p) || p < 0 || p > 255)) {
|
|
249
385
|
return false;
|
|
250
386
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
387
|
+
const [a, b, c] = parts;
|
|
388
|
+
return (a === 127 || // Loopback
|
|
389
|
+
a === 10 || // Private 10/8
|
|
390
|
+
(a === 172 && b >= 16 && b <= 31) || // Private 172.16/12
|
|
391
|
+
(a === 192 && b === 168) || // Private 192.168/16
|
|
392
|
+
a === 0 || // Current network
|
|
393
|
+
(a === 169 && b === 254) // Link-local
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
function isPrivateIPv6(ip) {
|
|
397
|
+
const normalized = ip.toLowerCase().replace(/^\[|\]$/g, '');
|
|
398
|
+
// Loopback
|
|
399
|
+
if (normalized === '::1' || normalized === '0:0:0:0:0:0:0:1')
|
|
256
400
|
return true;
|
|
257
|
-
//
|
|
401
|
+
// Unique Local (fc00::/7)
|
|
258
402
|
if (normalized.startsWith('fc') || normalized.startsWith('fd'))
|
|
259
|
-
return true;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
return true;
|
|
403
|
+
return true;
|
|
404
|
+
// Link Local (fe80::/10)
|
|
405
|
+
if (normalized.startsWith('fe8') || normalized.startsWith('fe9') ||
|
|
406
|
+
normalized.startsWith('fea') || normalized.startsWith('feb'))
|
|
407
|
+
return true;
|
|
408
|
+
// IPv4-mapped IPv6
|
|
409
|
+
if (normalized.startsWith('::ffff:')) {
|
|
410
|
+
const ipv4Part = normalized.slice(7);
|
|
411
|
+
return isPrivateIPv4(ipv4Part);
|
|
412
|
+
}
|
|
264
413
|
return false;
|
|
265
414
|
}
|
|
266
415
|
export async function validatePublicUrl(urlString) {
|
|
267
|
-
|
|
416
|
+
let parsed;
|
|
417
|
+
try {
|
|
418
|
+
parsed = new URL(urlString);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
throw new ValidationError('Invalid URL format');
|
|
422
|
+
}
|
|
268
423
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
269
|
-
throw new
|
|
424
|
+
throw new SecurityError('Only HTTP/HTTPS protocols allowed', 'INVALID_PROTOCOL');
|
|
425
|
+
}
|
|
426
|
+
// Check for IP literals
|
|
427
|
+
if (isPrivateIPv4(parsed.hostname)) {
|
|
428
|
+
throw new SecurityError(`Private IPv4 address not allowed: ${parsed.hostname}`, 'PRIVATE_IP');
|
|
270
429
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
throw new Error(`Access denied: Host '${parsed.hostname}' is a private IP`);
|
|
430
|
+
if (isPrivateIPv6(parsed.hostname)) {
|
|
431
|
+
throw new SecurityError(`Private IPv6 address not allowed: ${parsed.hostname}`, 'PRIVATE_IP');
|
|
274
432
|
}
|
|
275
|
-
//
|
|
433
|
+
// DNS resolution check with timeout
|
|
276
434
|
try {
|
|
277
|
-
const addresses = await
|
|
435
|
+
const addresses = await Promise.race([
|
|
436
|
+
dns.lookup(parsed.hostname, { all: true }),
|
|
437
|
+
new Promise((_, reject) => setTimeout(() => reject(new TimeoutError('DNS lookup timeout', CONFIG.DNS_TIMEOUT)), CONFIG.DNS_TIMEOUT)),
|
|
438
|
+
]);
|
|
278
439
|
for (const addr of addresses) {
|
|
279
|
-
if (
|
|
280
|
-
throw new
|
|
440
|
+
if (isPrivateIPv4(addr.address) || isPrivateIPv6(addr.address)) {
|
|
441
|
+
throw new SecurityError(`Host resolves to private IP: ${addr.address}`, 'DNS_PRIVATE_IP');
|
|
281
442
|
}
|
|
282
443
|
}
|
|
283
444
|
}
|
|
284
445
|
catch (error) {
|
|
285
|
-
|
|
286
|
-
if (error instanceof Error && error.message.startsWith('Access denied')) {
|
|
446
|
+
if (error instanceof SecurityError)
|
|
287
447
|
throw error;
|
|
288
|
-
|
|
289
|
-
// Ignore other DNS errors, let fetch handle them
|
|
448
|
+
// Ignore other DNS errors, let actual fetch handle them
|
|
290
449
|
}
|
|
291
450
|
}
|
|
292
451
|
class Logger {
|
|
293
452
|
level;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
453
|
+
useColors;
|
|
454
|
+
output;
|
|
455
|
+
static LEVELS = ['debug', 'info', 'warn', 'error'];
|
|
456
|
+
static COLORS = {
|
|
457
|
+
debug: chalk.gray,
|
|
458
|
+
info: chalk.blue,
|
|
459
|
+
warn: chalk.yellow,
|
|
460
|
+
error: chalk.red,
|
|
461
|
+
};
|
|
462
|
+
constructor(options = {}) {
|
|
463
|
+
this.level = options.level ?? process.env.LOG_LEVEL ?? 'info';
|
|
464
|
+
this.useColors = options.useColors ?? chalk.level > 0;
|
|
465
|
+
this.output = options.output ?? 'stderr';
|
|
466
|
+
}
|
|
467
|
+
shouldLog(level) {
|
|
468
|
+
return Logger.LEVELS.indexOf(level) >= Logger.LEVELS.indexOf(this.level);
|
|
469
|
+
}
|
|
470
|
+
formatMessage(level, msg) {
|
|
471
|
+
const timestamp = new Date().toISOString();
|
|
472
|
+
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
|
|
473
|
+
return this.useColors ? Logger.COLORS[level](prefix) + ` ${msg}` : `${prefix} ${msg}`;
|
|
474
|
+
}
|
|
475
|
+
print(level, msg, ...args) {
|
|
476
|
+
if (!this.shouldLog(level))
|
|
477
|
+
return;
|
|
478
|
+
const output = this.output === 'stdout' ? console.log : console.error;
|
|
479
|
+
const formatted = this.formatMessage(level, msg);
|
|
480
|
+
if (args.length > 0) {
|
|
481
|
+
output(formatted, ...args);
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
output(formatted);
|
|
485
|
+
}
|
|
300
486
|
}
|
|
301
487
|
debug(msg, ...args) {
|
|
302
|
-
|
|
303
|
-
console.error(chalk.gray(`[DEBUG] ${msg}`), ...args);
|
|
488
|
+
this.print('debug', msg, ...args);
|
|
304
489
|
}
|
|
305
490
|
info(msg, ...args) {
|
|
306
|
-
|
|
307
|
-
console.error(chalk.blue(`[INFO] ${msg}`), ...args);
|
|
491
|
+
this.print('info', msg, ...args);
|
|
308
492
|
}
|
|
309
493
|
warn(msg, ...args) {
|
|
310
|
-
|
|
311
|
-
console.error(chalk.yellow(`[WARN] ${msg}`), ...args);
|
|
494
|
+
this.print('warn', msg, ...args);
|
|
312
495
|
}
|
|
313
496
|
error(msg, ...args) {
|
|
314
|
-
|
|
315
|
-
|
|
497
|
+
this.print('error', msg, ...args);
|
|
498
|
+
}
|
|
499
|
+
setLevel(level) {
|
|
500
|
+
this.level = level;
|
|
316
501
|
}
|
|
317
502
|
}
|
|
318
503
|
export const logger = new Logger();
|
|
319
|
-
//
|
|
504
|
+
// ==========================================
|
|
505
|
+
// ReDoS Protection
|
|
506
|
+
// ==========================================
|
|
320
507
|
/**
|
|
321
|
-
*
|
|
322
|
-
* These patterns
|
|
323
|
-
* Using RegExp constructor to avoid escaping issues
|
|
508
|
+
* ReDoS vulnerability patterns - detect nested quantifiers in regex patterns
|
|
509
|
+
* These patterns check if a regex pattern string contains dangerous nested quantifiers
|
|
324
510
|
*/
|
|
325
511
|
const REDOS_PATTERNS = [
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
new RegExp('\\\\(\\\\d\\\\+\\\\)\\\\+\\\\$'), // End-anchored digits
|
|
512
|
+
/\([^)]*[*+][^)]*\)[*+]/, // Group with quantifier followed by quantifier: (a+)+, (.*)*
|
|
513
|
+
/\([^)]*\{[^}]+\}[^)]*\)[*+]/, // Group with {n} followed by quantifier
|
|
514
|
+
/\([^)]*[*+][^)]*\)\{[^}]+\}/, // Group with quantifier followed by {n}
|
|
515
|
+
/\(\[.*?\][*+]\)[*+]/, // Character class with quantifier in group followed by quantifier
|
|
331
516
|
];
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
* Maximum number of quantifiers allowed in a regex pattern
|
|
338
|
-
*/
|
|
339
|
-
const MAX_QUANTIFIERS = 20;
|
|
340
|
-
/**
|
|
341
|
-
* Validate a regex pattern for ReDoS vulnerabilities
|
|
342
|
-
* @throws Error if pattern is unsafe
|
|
343
|
-
*/
|
|
517
|
+
export class ReDoSError extends SecurityError {
|
|
518
|
+
constructor(message) {
|
|
519
|
+
super(message, 'REDOS_DETECTED');
|
|
520
|
+
}
|
|
521
|
+
}
|
|
344
522
|
export function validateRegexPattern(pattern) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
throw new Error(`Regex pattern too long (${pattern.length} chars). Maximum is ${MAX_PATTERN_LENGTH}.`);
|
|
523
|
+
if (pattern.length > CONFIG.MAX_PATTERN_LENGTH) {
|
|
524
|
+
throw new ReDoSError(`Pattern exceeds ${CONFIG.MAX_PATTERN_LENGTH} characters`);
|
|
348
525
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
throw new Error(`Regex pattern contains too many quantifiers (${quantifierCount}). Maximum is ${MAX_QUANTIFIERS}.`);
|
|
526
|
+
const quantifiers = (pattern.match(/[*+?{]/g) || []).length;
|
|
527
|
+
if (quantifiers > CONFIG.MAX_QUANTIFIERS) {
|
|
528
|
+
throw new ReDoSError(`Pattern contains too many quantifiers (${quantifiers})`);
|
|
353
529
|
}
|
|
354
|
-
// Check
|
|
530
|
+
// Check known ReDoS patterns
|
|
355
531
|
for (const redosPattern of REDOS_PATTERNS) {
|
|
356
532
|
if (redosPattern.test(pattern)) {
|
|
357
|
-
throw new
|
|
358
|
-
`Please simplify your regex or contact maintainers.`);
|
|
533
|
+
throw new ReDoSError(`Pattern matches known ReDoS structure: ${redosPattern.source}`);
|
|
359
534
|
}
|
|
360
535
|
}
|
|
361
|
-
// Test
|
|
536
|
+
// Test with timeout using Web Workers or setImmediate to not block
|
|
537
|
+
testRegexPerformance(pattern);
|
|
538
|
+
}
|
|
539
|
+
function testRegexPerformance(pattern) {
|
|
540
|
+
let regex;
|
|
362
541
|
try {
|
|
363
|
-
|
|
364
|
-
const testString = 'a'.repeat(100); // 100 chars
|
|
365
|
-
// Use a timeout to catch slow patterns
|
|
366
|
-
const startTime = Date.now();
|
|
367
|
-
const TEST_TIMEOUT = 100; // 100ms max
|
|
368
|
-
let iterations = 0;
|
|
369
|
-
const MAX_ITERATIONS = 1000;
|
|
370
|
-
while (regex.exec(testString) !== null && iterations < MAX_ITERATIONS) {
|
|
371
|
-
iterations++;
|
|
372
|
-
if (Date.now() - startTime > TEST_TIMEOUT) {
|
|
373
|
-
throw new Error(`Regex pattern causes catastrophic backtracking. ` +
|
|
374
|
-
`Please simplify your pattern.`);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
542
|
+
regex = new RegExp(pattern, 'g');
|
|
377
543
|
}
|
|
378
|
-
catch (
|
|
379
|
-
|
|
380
|
-
|
|
544
|
+
catch (e) {
|
|
545
|
+
throw new ValidationError(`Invalid regex syntax: ${e}`);
|
|
546
|
+
}
|
|
547
|
+
// Simple backtracking test with exponential string
|
|
548
|
+
const testStrings = [
|
|
549
|
+
'a'.repeat(20) + (pattern.includes('(') ? 'b' : ''), // Quick fail
|
|
550
|
+
'a'.repeat(50), // Potential backtracking target
|
|
551
|
+
];
|
|
552
|
+
for (const testStr of testStrings) {
|
|
553
|
+
const start = performance.now();
|
|
554
|
+
try {
|
|
555
|
+
regex.lastIndex = 0;
|
|
556
|
+
regex.test(testStr);
|
|
557
|
+
regex.exec(testStr);
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
// Syntax errors already caught
|
|
561
|
+
}
|
|
562
|
+
if (performance.now() - start > 100) {
|
|
563
|
+
throw new ReDoSError(`Pattern performance too slow (potential ReDoS)`);
|
|
381
564
|
}
|
|
382
|
-
// Other regex errors are fine (like invalid syntax) - let them propagate
|
|
383
565
|
}
|
|
384
566
|
}
|
|
385
|
-
/**
|
|
386
|
-
* Create a RegExp object with ReDoS protection
|
|
387
|
-
* @param pattern - The regex pattern
|
|
388
|
-
* @param flags - Regex flags (g, i, m, etc.)
|
|
389
|
-
* @returns Safe RegExp object
|
|
390
|
-
* @throws Error if pattern is unsafe
|
|
391
|
-
*/
|
|
392
567
|
export function createSafeRegex(pattern, flags) {
|
|
393
568
|
validateRegexPattern(pattern);
|
|
394
569
|
return new RegExp(pattern, flags);
|
|
395
570
|
}
|
|
571
|
+
// ==========================================
|
|
572
|
+
// HTTP Utilities
|
|
573
|
+
// ==========================================
|
|
396
574
|
export const DEFAULT_HEADERS = {
|
|
397
575
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
398
576
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
|
399
577
|
'Accept-Language': 'en-US,en;q=0.9',
|
|
578
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
400
579
|
};
|
|
401
|
-
export async function fetchWithRetry(url, options = {}
|
|
580
|
+
export async function fetchWithRetry(url, options = {}) {
|
|
581
|
+
const retries = options.retries ?? CONFIG.RETRY.MAX_RETRIES;
|
|
582
|
+
let backoff = options.backoff ?? CONFIG.RETRY.BASE_DELAY;
|
|
583
|
+
if (options.validateUrl !== false) {
|
|
584
|
+
await validatePublicUrl(url);
|
|
585
|
+
}
|
|
402
586
|
const fetchOptions = {
|
|
403
587
|
...options,
|
|
404
|
-
headers: {
|
|
588
|
+
headers: {
|
|
589
|
+
...DEFAULT_HEADERS,
|
|
590
|
+
...options.headers,
|
|
591
|
+
},
|
|
405
592
|
};
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
593
|
+
let lastError;
|
|
594
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
595
|
+
try {
|
|
596
|
+
const response = await fetch(url, fetchOptions);
|
|
597
|
+
if (response.status === 429 && attempt < retries) {
|
|
598
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
599
|
+
const delay = retryAfter ? parseInt(retryAfter, 10) * 1000 : backoff;
|
|
600
|
+
await sleep(delay);
|
|
601
|
+
backoff *= CONFIG.RETRY.BACKOFF_MULTIPLIER;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (!response.ok && response.status >= 500 && attempt < retries) {
|
|
605
|
+
await sleep(backoff);
|
|
606
|
+
backoff *= CONFIG.RETRY.BACKOFF_MULTIPLIER;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
return response;
|
|
417
610
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
611
|
+
catch (error) {
|
|
612
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
613
|
+
if (attempt < retries) {
|
|
614
|
+
await sleep(backoff);
|
|
615
|
+
backoff *= CONFIG.RETRY.BACKOFF_MULTIPLIER;
|
|
616
|
+
}
|
|
424
617
|
}
|
|
425
|
-
throw error;
|
|
426
618
|
}
|
|
619
|
+
throw lastError ?? new Error('Fetch failed after all retries');
|
|
427
620
|
}
|
|
428
|
-
export async function withRetry(fn,
|
|
429
|
-
|
|
621
|
+
export async function withRetry(fn, options = {}) {
|
|
622
|
+
const maxRetries = options.maxRetries ?? 3;
|
|
623
|
+
const baseDelay = options.baseDelay ?? 100;
|
|
430
624
|
for (let i = 0; i < maxRetries; i++) {
|
|
431
625
|
try {
|
|
432
626
|
return await fn();
|
|
433
627
|
}
|
|
434
628
|
catch (error) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
throw error;
|
|
629
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
630
|
+
if (options.shouldRetry && !options.shouldRetry(err)) {
|
|
631
|
+
throw err;
|
|
439
632
|
}
|
|
440
|
-
if (
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
633
|
+
if (err instanceof SecurityError ||
|
|
634
|
+
err.code === 'ENOENT' ||
|
|
635
|
+
err.code === 'EACCES') {
|
|
636
|
+
throw err;
|
|
444
637
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
throw lastError;
|
|
448
|
-
}
|
|
449
|
-
export class FileLock {
|
|
450
|
-
locks = new Map();
|
|
451
|
-
async acquire(filePath, timeout = 5000) {
|
|
452
|
-
const startTime = Date.now();
|
|
453
|
-
while (this.locks.has(filePath)) {
|
|
454
|
-
if (Date.now() - startTime > timeout) {
|
|
455
|
-
throw new Error(`Timeout acquiring lock for ${filePath}`);
|
|
638
|
+
if (i === maxRetries - 1) {
|
|
639
|
+
throw err;
|
|
456
640
|
}
|
|
457
|
-
|
|
641
|
+
const delay = baseDelay * Math.pow(2, i);
|
|
642
|
+
options.onRetry?.(err, i + 1);
|
|
643
|
+
await sleep(delay);
|
|
458
644
|
}
|
|
459
|
-
this.locks.set(filePath, true);
|
|
460
|
-
return {
|
|
461
|
-
release: () => {
|
|
462
|
-
this.locks.delete(filePath);
|
|
463
|
-
}
|
|
464
|
-
};
|
|
465
645
|
}
|
|
646
|
+
throw new Error('Unreachable');
|
|
466
647
|
}
|
|
467
|
-
|
|
468
|
-
|
|
648
|
+
function sleep(ms) {
|
|
649
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
650
|
+
}
|
|
651
|
+
export function sanitizeInput(input, options = {}) {
|
|
652
|
+
const config = {
|
|
653
|
+
maxLength: CONFIG.MAX_INPUT_LENGTH,
|
|
654
|
+
removeNull: true,
|
|
655
|
+
trim: true,
|
|
656
|
+
...options,
|
|
657
|
+
};
|
|
469
658
|
if (typeof input !== 'string') {
|
|
470
659
|
return '';
|
|
471
660
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
661
|
+
let sanitized = input;
|
|
662
|
+
if (config.trim) {
|
|
663
|
+
sanitized = sanitized.trim();
|
|
664
|
+
}
|
|
665
|
+
if (config.removeNull) {
|
|
666
|
+
sanitized = sanitized.replace(/\x00/g, '');
|
|
667
|
+
}
|
|
668
|
+
if (config.allowedPattern) {
|
|
669
|
+
sanitized = sanitized.replace(config.allowedPattern, '');
|
|
670
|
+
}
|
|
671
|
+
// Normalize Unicode to prevent homograph attacks
|
|
672
|
+
try {
|
|
673
|
+
sanitized = sanitized.normalize('NFC');
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
// Invalid UTF-16 sequences, return empty or partial
|
|
677
|
+
sanitized = sanitized.replace(/[\uDC00-\uDFFF]/g, '').replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/g, '');
|
|
678
|
+
sanitized = sanitized.normalize('NFC');
|
|
679
|
+
}
|
|
680
|
+
if (sanitized.length > config.maxLength) {
|
|
681
|
+
sanitized = sanitized.substring(0, config.maxLength);
|
|
477
682
|
}
|
|
478
|
-
// Remove null bytes
|
|
479
|
-
sanitized = sanitized.replace(/\x00/g, '');
|
|
480
|
-
// Normalize unicode (prevent homograph attacks)
|
|
481
|
-
sanitized = sanitized.normalize('NFC');
|
|
482
683
|
return sanitized;
|
|
483
684
|
}
|
|
685
|
+
// ==========================================
|
|
686
|
+
// Exports
|
|
687
|
+
// ==========================================
|
|
688
|
+
export { CONFIG, SAFE_COMMANDS, parseCommand, isPrivateIPv4, isPrivateIPv6, Logger, };
|