@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.
Files changed (35) hide show
  1. package/README.md.bak +197 -0
  2. package/dist/.gemini_graph_cache.json.bak +1641 -0
  3. package/dist/graph.d.ts +7 -0
  4. package/dist/graph.js +136 -113
  5. package/dist/intelligent-code.d.ts +8 -0
  6. package/dist/intelligent-code.js +152 -125
  7. package/dist/intelligent-code.test.d.ts +1 -0
  8. package/dist/intelligent-code.test.js +104 -0
  9. package/dist/lib/formatters.d.ts +9 -0
  10. package/dist/lib/formatters.js +119 -0
  11. package/dist/lib/validators.d.ts +45 -0
  12. package/dist/lib/validators.js +232 -0
  13. package/dist/lib.js +23 -265
  14. package/dist/tools/sports/core/base.d.ts +3 -2
  15. package/dist/tools/sports/core/base.js +12 -10
  16. package/dist/tools/sports/core/cache.d.ts +9 -0
  17. package/dist/tools/sports/core/cache.js +25 -3
  18. package/dist/tools/sports/core/types.d.ts +6 -2
  19. package/dist/tools/sports/providers/api.d.ts +4 -0
  20. package/dist/tools/sports/providers/api.js +110 -27
  21. package/dist/tools/sports/tools/betting.js +16 -16
  22. package/dist/tools/sports/tools/league.d.ts +2 -7
  23. package/dist/tools/sports/tools/league.js +198 -8
  24. package/dist/tools/sports/tools/live.js +80 -38
  25. package/dist/tools/sports/tools/match-calculations.d.ts +51 -0
  26. package/dist/tools/sports/tools/match-calculations.js +171 -0
  27. package/dist/tools/sports/tools/match-helpers.d.ts +21 -0
  28. package/dist/tools/sports/tools/match-helpers.js +57 -0
  29. package/dist/tools/sports/tools/match.js +227 -125
  30. package/dist/tools/sports.js +3 -3
  31. package/dist/utils.d.ts +111 -44
  32. package/dist/utils.js +510 -305
  33. package/dist/utils.test.js +3 -3
  34. package/package.json +1 -1
  35. 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
- // ============= Command Injection Protection =============
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
- * Blocking these prevents command chaining like: cmd; rm -rf /
25
+ * Shell metacharacters that could enable command injection.
26
+ * Using character class to avoid regex complexity issues.
11
27
  */
12
- const SHELL_METACHARACTERS = /[;|&`$()<>\{\}\[\]\*\?~]|\$\{[^}]*\}|\\x[0-9a-fA-F]{2}|\$[a-zA-Z_][a-zA-Z0-9_]*/gu;
28
+ const SHELL_METACHARACTERS = /[;|&`$()<>\{\}\[\]\*\?~]/;
13
29
  /**
14
- * Whitelist of safe commands that can be executed
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
- // Git
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
- * Detect dangerous shell metacharacters in command string
41
- */
42
- function hasShellMetacharacters(command) {
43
- return SHELL_METACHARACTERS.test(command);
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
- * Parse command string into command and arguments safely
47
- * This prevents shell injection by avoiding shell interpretation
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
- // Find first space to separate command from arguments
53
- const firstSpace = trimmed.indexOf(' ');
54
- if (firstSpace === -1) {
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 inQuotes = false;
63
- let quoteChar = '';
64
- for (let i = 0; i < argsString.length; i++) {
65
- const char = argsString[i];
66
- if ((char === '"' || char === "'") && (i === 0 || argsString[i - 1] !== '\\')) {
67
- if (!inQuotes) {
68
- inQuotes = true;
69
- quoteChar = char;
70
- }
71
- else if (char === quoteChar) {
72
- inQuotes = false;
73
- quoteChar = '';
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
- else if (char === ' ' && !inQuotes) {
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
- else {
86
- current += char;
87
- }
141
+ current += char;
88
142
  }
89
- if (current) {
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
- return { command: cmd, args };
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
- * Validate command against security rules
96
- * @throws Error if command is unsafe
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 Error('Command blocked: Contains shell metacharacters (;|&`$()<>). ' +
102
- 'These are not allowed for security reasons.');
103
- }
104
- // Parse command to get base command name
105
- const { command: baseCommand } = parseCommand(command);
106
- // Check if command is in whitelist
107
- if (!SAFE_COMMANDS.has(baseCommand)) {
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 Error(`Command blocked: Matches dangerous pattern`);
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 a shell command with security protections against command injection.
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
- try {
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
- stdout += data.toString();
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
- stderr += data.toString();
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
- child.on('error', (error) => {
223
+ const errorHandler = (error) => {
224
+ kill('SIGKILL');
171
225
  reject(error);
172
- });
173
- child.on('close', (code) => {
174
- if (code !== 0) {
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({ stdout, stderr });
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
- mutex = Promise.resolve();
189
- lock() {
190
- let unlock = () => { };
191
- const nextLock = new Promise(resolve => {
192
- unlock = resolve;
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
- // The caller waits for the previous lock to release
195
- const willLock = this.mutex.then(() => unlock);
196
- // The next caller will wait for this lock to release
197
- this.mutex = nextLock;
198
- return willLock;
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 unlock = await this.lock();
277
+ const release = await this.acquire();
202
278
  try {
203
279
  return await Promise.resolve(fn());
204
280
  }
205
281
  finally {
206
- unlock();
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
- // If file doesn't exist yet, we can't get realpath, so fallback to resolved path
371
+ // File doesn't exist yet, use resolved path
225
372
  absolutePath = resolvedPath;
226
373
  rootDir = path.resolve(process.cwd());
227
374
  }
228
- if (!absolutePath.startsWith(rootDir) || (absolutePath.length > rootDir.length && absolutePath[rootDir.length] !== path.sep)) {
229
- throw new Error(`Access denied: Path '${requestedPath}' (resolved to '${absolutePath}') is outside the project root '${rootDir}'.`);
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 isPrivateIP(ip) {
234
- // Remove brackets if present (IPv6 literals in hostnames)
235
- const cleanIp = ip.replace(/^\[|\]$/g, '');
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
- // IPv6 check
252
- // Normalize: remove leading/trailing colons and convert to lowercase
253
- const normalized = cleanIp.toLowerCase();
254
- // Loopback: ::1, 0:0:0:0:0:0:0:1
255
- if (normalized === '::1' || normalized === '::' || normalized.replace(/:0/g, ':').replace(/^0+/, '') === '::1')
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
- // Private/Link-local ranges
401
+ // Unique Local (fc00::/7)
258
402
  if (normalized.startsWith('fc') || normalized.startsWith('fd'))
259
- return true; // Unique Local
260
- if (normalized.startsWith('fe80'))
261
- return true; // Link Local
262
- if (normalized.startsWith('::ffff:7f') || normalized.startsWith('::ffff:10.') || normalized.startsWith('::ffff:192.168.'))
263
- return true; // IPv4-mapped private
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
- const parsed = new URL(urlString);
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 Error('Invalid protocol. Only http and https are allowed.');
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
- // 1. Direct check for IP literals in hostname
272
- if (isPrivateIP(parsed.hostname)) {
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
- // 2. Resolve and check all resulting IPs
433
+ // DNS resolution check with timeout
276
434
  try {
277
- const addresses = await dns.lookup(parsed.hostname, { all: true });
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 (isPrivateIP(addr.address)) {
280
- throw new Error(`Access denied: Host '${parsed.hostname}' resolves to private IP ${addr.address}`);
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
- // If it's the specific access denied error, rethrow
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
- constructor() {
295
- this.level = process.env.LOG_LEVEL || 'info';
296
- }
297
- shouldLog(lvl) {
298
- const levels = ['debug', 'info', 'warn', 'error'];
299
- return levels.indexOf(lvl) >= levels.indexOf(this.level);
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
- if (this.shouldLog('debug'))
303
- console.error(chalk.gray(`[DEBUG] ${msg}`), ...args);
488
+ this.print('debug', msg, ...args);
304
489
  }
305
490
  info(msg, ...args) {
306
- if (this.shouldLog('info'))
307
- console.error(chalk.blue(`[INFO] ${msg}`), ...args);
491
+ this.print('info', msg, ...args);
308
492
  }
309
493
  warn(msg, ...args) {
310
- if (this.shouldLog('warn'))
311
- console.error(chalk.yellow(`[WARN] ${msg}`), ...args);
494
+ this.print('warn', msg, ...args);
312
495
  }
313
496
  error(msg, ...args) {
314
- if (this.shouldLog('error'))
315
- console.error(chalk.red(`[ERROR] ${msg}`), ...args);
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
- // ============= ReDoS Protection =============
504
+ // ==========================================
505
+ // ReDoS Protection
506
+ // ==========================================
320
507
  /**
321
- * Patterns that could cause ReDoS (Regular Expression Denial of Service)
322
- * These patterns contain nested quantifiers or catastrophic backtracking risks
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
- new RegExp('\\\\(\\\\.\\\\[\\\\*\\\\+\\\\]\\\\)\\\\1'), // Nested quantifiers
327
- new RegExp('\\\\(\\\\[\\\\*\\\\+\\\\]\\\\[\\\\*\\\\+\\\\]\\\\)'), // Nested quantifiers
328
- new RegExp('\\\\(\\\\[a-z\\\\]\\\\+\\\\)\\\\+'), // Ambiguous nested quantifiers
329
- new RegExp('\\\\(\\\\[\\\\^\\\\s\\\\]\\\\+\\\\)\\\\+\\\\$'), // End-anchored
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
- * Maximum regex pattern length to prevent overly complex patterns
334
- */
335
- const MAX_PATTERN_LENGTH = 500;
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
- // Check pattern length
346
- if (pattern.length > MAX_PATTERN_LENGTH) {
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
- // Count quantifiers to detect complex patterns
350
- const quantifierCount = (pattern.match(/[*+?{]/g) || []).length;
351
- if (quantifierCount > MAX_QUANTIFIERS) {
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 for known ReDoS patterns
530
+ // Check known ReDoS patterns
355
531
  for (const redosPattern of REDOS_PATTERNS) {
356
532
  if (redosPattern.test(pattern)) {
357
- throw new Error(`Regex pattern contains a known ReDoS vulnerability pattern. ` +
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 the regex with a timeout to catch catastrophic backtracking
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
- const regex = new RegExp(pattern, 'g');
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 (error) {
379
- if (error instanceof Error && error.message.includes('catastrophic')) {
380
- throw error;
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 = {}, retries = 3, backoff = 1000) {
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: { ...DEFAULT_HEADERS, ...options.headers }
588
+ headers: {
589
+ ...DEFAULT_HEADERS,
590
+ ...options.headers,
591
+ },
405
592
  };
406
- try {
407
- const response = await fetch(url, fetchOptions);
408
- if (response.status === 429 && retries > 0) {
409
- const retryAfter = response.headers.get('Retry-After');
410
- const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : backoff;
411
- await new Promise(resolve => setTimeout(resolve, waitTime));
412
- return fetchWithRetry(url, options, retries - 1, backoff * 2);
413
- }
414
- if (!response.ok && retries > 0 && response.status >= 500) {
415
- await new Promise(resolve => setTimeout(resolve, backoff));
416
- return fetchWithRetry(url, options, retries - 1, backoff * 2);
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
- return response;
419
- }
420
- catch (error) {
421
- if (retries > 0) {
422
- await new Promise(resolve => setTimeout(resolve, backoff));
423
- return fetchWithRetry(url, options, retries - 1, backoff * 2);
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, maxRetries = 3, baseDelay = 100) {
429
- let lastError;
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
- lastError = error;
436
- // Don't retry on certain errors
437
- if (error.code === 'ENOENT' || error.code === 'EACCES' || error.code === 'EPERM') {
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 (i < maxRetries - 1) {
441
- const delay = baseDelay * Math.pow(2, i);
442
- console.warn(`[Retry] Attempt ${i + 1} failed, retrying in ${delay}ms...`);
443
- await new Promise(resolve => setTimeout(resolve, delay));
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
- await new Promise(resolve => setTimeout(resolve, 10));
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
- export const fileLock = new FileLock();
468
- export function sanitizeInput(input, maxLength = 10000) {
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
- // Trim whitespace
473
- let sanitized = input.trim();
474
- // Limit length
475
- if (sanitized.length > maxLength) {
476
- sanitized = sanitized.substring(0, maxLength);
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, };