agileflow 2.89.2 → 2.90.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/CHANGELOG.md +10 -0
- package/README.md +3 -3
- package/lib/content-sanitizer.js +463 -0
- package/lib/error-codes.js +544 -0
- package/lib/errors.js +336 -5
- package/lib/feedback.js +561 -0
- package/lib/path-resolver.js +396 -0
- package/lib/placeholder-registry.js +617 -0
- package/lib/session-registry.js +461 -0
- package/lib/smart-json-file.js +653 -0
- package/lib/table-formatter.js +504 -0
- package/lib/transient-status.js +374 -0
- package/lib/ui-manager.js +612 -0
- package/lib/validate-args.js +213 -0
- package/lib/validate-names.js +143 -0
- package/lib/validate-paths.js +434 -0
- package/lib/validate.js +38 -584
- package/package.json +4 -1
- package/scripts/agileflow-configure.js +40 -1440
- package/scripts/agileflow-welcome.js +2 -1
- package/scripts/check-update.js +16 -3
- package/scripts/lib/configure-detect.js +383 -0
- package/scripts/lib/configure-features.js +811 -0
- package/scripts/lib/configure-repair.js +314 -0
- package/scripts/lib/configure-utils.js +115 -0
- package/scripts/lib/frontmatter-parser.js +3 -3
- package/scripts/lib/sessionRegistry.js +682 -0
- package/scripts/obtain-context.js +417 -113
- package/scripts/ralph-loop.js +1 -1
- package/scripts/session-manager.js +77 -10
- package/scripts/tui/App.js +176 -0
- package/scripts/tui/index.js +75 -0
- package/scripts/tui/lib/crashRecovery.js +302 -0
- package/scripts/tui/lib/eventStream.js +316 -0
- package/scripts/tui/lib/keyboard.js +252 -0
- package/scripts/tui/lib/loopControl.js +371 -0
- package/scripts/tui/panels/OutputPanel.js +278 -0
- package/scripts/tui/panels/SessionPanel.js +178 -0
- package/scripts/tui/panels/TracePanel.js +333 -0
- package/src/core/commands/tui.md +91 -0
- package/tools/cli/commands/config.js +10 -33
- package/tools/cli/commands/doctor.js +48 -40
- package/tools/cli/commands/list.js +49 -37
- package/tools/cli/commands/status.js +13 -37
- package/tools/cli/commands/uninstall.js +12 -41
- package/tools/cli/installers/core/installer.js +75 -12
- package/tools/cli/installers/ide/_interface.js +238 -0
- package/tools/cli/installers/ide/codex.js +2 -2
- package/tools/cli/installers/ide/manager.js +15 -0
- package/tools/cli/lib/command-context.js +374 -0
- package/tools/cli/lib/config-manager.js +394 -0
- package/tools/cli/lib/content-injector.js +69 -16
- package/tools/cli/lib/ide-errors.js +163 -29
- package/tools/cli/lib/ide-registry.js +186 -0
- package/tools/cli/lib/npm-utils.js +16 -3
- package/tools/cli/lib/self-update.js +148 -0
- package/tools/cli/lib/validation-middleware.js +491 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgileFlow CLI - Path Validation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Path traversal protection and filesystem path validation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Path validation error with context.
|
|
12
|
+
*/
|
|
13
|
+
class PathValidationError extends Error {
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} message - Error message
|
|
16
|
+
* @param {string} inputPath - The problematic path
|
|
17
|
+
* @param {string} reason - Reason for rejection
|
|
18
|
+
*/
|
|
19
|
+
constructor(message, inputPath, reason) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'PathValidationError';
|
|
22
|
+
this.inputPath = inputPath;
|
|
23
|
+
this.reason = reason;
|
|
24
|
+
Error.captureStackTrace(this, this.constructor);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check the depth of a symlink chain (how many symlinks to follow to reach final target).
|
|
30
|
+
* Returns early if chain exceeds maxDepth to prevent infinite loops from circular symlinks.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} filePath - Starting path to check
|
|
33
|
+
* @param {number} maxDepth - Maximum allowed symlink chain depth
|
|
34
|
+
* @returns {{ ok: boolean, depth: number, error?: string, isCircular?: boolean }}
|
|
35
|
+
*/
|
|
36
|
+
function checkSymlinkChainDepth(filePath, maxDepth) {
|
|
37
|
+
let current = filePath;
|
|
38
|
+
let depth = 0;
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
|
|
41
|
+
// Loop until we find a non-symlink or exceed max depth
|
|
42
|
+
while (true) {
|
|
43
|
+
// Check for circular symlinks
|
|
44
|
+
if (seen.has(current)) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
depth,
|
|
48
|
+
error: `Circular symlink detected at: ${current}`,
|
|
49
|
+
isCircular: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
seen.add(current);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const stats = fs.lstatSync(current);
|
|
56
|
+
if (!stats.isSymbolicLink()) {
|
|
57
|
+
// Reached a real file/directory, chain ends
|
|
58
|
+
return { ok: true, depth };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Increment depth before checking limit
|
|
62
|
+
depth++;
|
|
63
|
+
|
|
64
|
+
// Check if we've exceeded max depth
|
|
65
|
+
if (depth > maxDepth) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
depth,
|
|
69
|
+
error: `Symlink chain depth (${depth}) exceeds maximum (${maxDepth})`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Read symlink target
|
|
74
|
+
const target = fs.readlinkSync(current);
|
|
75
|
+
|
|
76
|
+
// Resolve target path (could be relative or absolute)
|
|
77
|
+
if (path.isAbsolute(target)) {
|
|
78
|
+
current = target;
|
|
79
|
+
} else {
|
|
80
|
+
current = path.resolve(path.dirname(current), target);
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {
|
|
83
|
+
if (e.code === 'ENOENT') {
|
|
84
|
+
// Path doesn't exist, chain ends here
|
|
85
|
+
return { ok: true, depth };
|
|
86
|
+
}
|
|
87
|
+
// Other error (permission denied, etc.)
|
|
88
|
+
return { ok: true, depth };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validate that a path is safe and within the allowed base directory.
|
|
95
|
+
* Prevents path traversal attacks by:
|
|
96
|
+
* 1. Resolving the path to absolute form
|
|
97
|
+
* 2. Ensuring it stays within the base directory
|
|
98
|
+
* 3. Rejecting symbolic links (optional)
|
|
99
|
+
* 4. When symlinks allowed, verifying symlink targets stay within base
|
|
100
|
+
* 5. Limiting symlink chain depth to prevent infinite loops
|
|
101
|
+
*
|
|
102
|
+
* @param {string} inputPath - The path to validate (can be relative or absolute)
|
|
103
|
+
* @param {string} baseDir - The allowed base directory (must be absolute)
|
|
104
|
+
* @param {Object} options - Validation options
|
|
105
|
+
* @param {boolean} [options.allowSymlinks=false] - Allow symbolic links
|
|
106
|
+
* @param {boolean} [options.mustExist=false] - Path must exist on filesystem
|
|
107
|
+
* @param {number} [options.maxSymlinkDepth=3] - Maximum symlink chain depth (when symlinks allowed)
|
|
108
|
+
* @returns {{ ok: boolean, resolvedPath?: string, realPath?: string, error?: PathValidationError }}
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* // Validate a file path within project directory
|
|
112
|
+
* const result = validatePath('./config.yaml', '/home/user/project');
|
|
113
|
+
* if (result.ok) {
|
|
114
|
+
* console.log('Safe path:', result.resolvedPath);
|
|
115
|
+
* }
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* // Reject path traversal attempt
|
|
119
|
+
* const result = validatePath('../../../etc/passwd', '/home/user/project');
|
|
120
|
+
* // result.ok === false
|
|
121
|
+
* // result.error.reason === 'path_traversal'
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* // Reject deep symlink chains
|
|
125
|
+
* const result = validatePath('link1', baseDir, { allowSymlinks: true, maxSymlinkDepth: 3 });
|
|
126
|
+
* // If link1 -> link2 -> link3 -> link4 -> target, this fails with 'symlink_chain_too_deep'
|
|
127
|
+
*/
|
|
128
|
+
function validatePath(inputPath, baseDir, options = {}) {
|
|
129
|
+
const { allowSymlinks = false, mustExist = false, maxSymlinkDepth = 3 } = options;
|
|
130
|
+
|
|
131
|
+
// Input validation
|
|
132
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
error: new PathValidationError(
|
|
136
|
+
'Path is required and must be a string',
|
|
137
|
+
String(inputPath),
|
|
138
|
+
'invalid_input'
|
|
139
|
+
),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!baseDir || typeof baseDir !== 'string') {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
error: new PathValidationError(
|
|
147
|
+
'Base directory is required and must be a string',
|
|
148
|
+
inputPath,
|
|
149
|
+
'invalid_base'
|
|
150
|
+
),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Base directory must be absolute
|
|
155
|
+
if (!path.isAbsolute(baseDir)) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
error: new PathValidationError(
|
|
159
|
+
'Base directory must be an absolute path',
|
|
160
|
+
inputPath,
|
|
161
|
+
'relative_base'
|
|
162
|
+
),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Normalize the base directory
|
|
167
|
+
const normalizedBase = path.resolve(baseDir);
|
|
168
|
+
|
|
169
|
+
// Resolve the input path relative to base directory
|
|
170
|
+
let resolvedPath;
|
|
171
|
+
if (path.isAbsolute(inputPath)) {
|
|
172
|
+
resolvedPath = path.resolve(inputPath);
|
|
173
|
+
} else {
|
|
174
|
+
resolvedPath = path.resolve(normalizedBase, inputPath);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Helper function to check if path is within base
|
|
178
|
+
const checkWithinBase = pathToCheck => {
|
|
179
|
+
const baseWithSep = normalizedBase.endsWith(path.sep)
|
|
180
|
+
? normalizedBase
|
|
181
|
+
: normalizedBase + path.sep;
|
|
182
|
+
return pathToCheck === normalizedBase || pathToCheck.startsWith(baseWithSep);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Check for path traversal: resolved path must start with base directory
|
|
186
|
+
if (!checkWithinBase(resolvedPath)) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
error: new PathValidationError(
|
|
190
|
+
`Path escapes base directory: ${inputPath}`,
|
|
191
|
+
inputPath,
|
|
192
|
+
'path_traversal'
|
|
193
|
+
),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check if path exists (if required)
|
|
198
|
+
if (mustExist) {
|
|
199
|
+
try {
|
|
200
|
+
fs.accessSync(resolvedPath);
|
|
201
|
+
} catch {
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
error: new PathValidationError(
|
|
205
|
+
`Path does not exist: ${resolvedPath}`,
|
|
206
|
+
inputPath,
|
|
207
|
+
'not_found'
|
|
208
|
+
),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check for symbolic links
|
|
214
|
+
if (!allowSymlinks) {
|
|
215
|
+
// Symlinks not allowed - reject if found
|
|
216
|
+
try {
|
|
217
|
+
const stats = fs.lstatSync(resolvedPath);
|
|
218
|
+
if (stats.isSymbolicLink()) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
error: new PathValidationError(
|
|
222
|
+
`Symbolic links are not allowed: ${inputPath}`,
|
|
223
|
+
inputPath,
|
|
224
|
+
'symlink_rejected'
|
|
225
|
+
),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
// Path doesn't exist yet, which is fine if mustExist is false
|
|
230
|
+
// Check parent directories for symlinks
|
|
231
|
+
const parts = path.relative(normalizedBase, resolvedPath).split(path.sep);
|
|
232
|
+
let currentPath = normalizedBase;
|
|
233
|
+
|
|
234
|
+
for (const part of parts) {
|
|
235
|
+
currentPath = path.join(currentPath, part);
|
|
236
|
+
try {
|
|
237
|
+
const stats = fs.lstatSync(currentPath);
|
|
238
|
+
if (stats.isSymbolicLink()) {
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
error: new PathValidationError(
|
|
242
|
+
`Path contains symbolic link: ${currentPath}`,
|
|
243
|
+
inputPath,
|
|
244
|
+
'symlink_in_path'
|
|
245
|
+
),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// Part of path doesn't exist, stop checking
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
// Symlinks allowed - but we must verify the target stays within base!
|
|
256
|
+
// This prevents symlink-based escape attacks
|
|
257
|
+
try {
|
|
258
|
+
const stats = fs.lstatSync(resolvedPath);
|
|
259
|
+
if (stats.isSymbolicLink()) {
|
|
260
|
+
// Check symlink chain depth to prevent DoS via infinite loops
|
|
261
|
+
const chainResult = checkSymlinkChainDepth(resolvedPath, maxSymlinkDepth);
|
|
262
|
+
if (!chainResult.ok) {
|
|
263
|
+
const reason = chainResult.isCircular ? 'symlink_circular' : 'symlink_chain_too_deep';
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
error: new PathValidationError(chainResult.error, inputPath, reason),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Resolve the symlink target to its real path
|
|
271
|
+
const realPath = fs.realpathSync(resolvedPath);
|
|
272
|
+
|
|
273
|
+
// Verify the real path is also within base directory
|
|
274
|
+
if (!checkWithinBase(realPath)) {
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
error: new PathValidationError(
|
|
278
|
+
`Symlink target escapes base directory: ${inputPath} -> ${realPath}`,
|
|
279
|
+
inputPath,
|
|
280
|
+
'symlink_escape'
|
|
281
|
+
),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Return both the resolved path and the real path
|
|
286
|
+
return {
|
|
287
|
+
ok: true,
|
|
288
|
+
resolvedPath,
|
|
289
|
+
realPath,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
} catch {
|
|
293
|
+
// Path doesn't exist - that's okay for non-mustExist scenarios
|
|
294
|
+
// Also check parent directories for symlinks that might escape
|
|
295
|
+
const parts = path.relative(normalizedBase, resolvedPath).split(path.sep);
|
|
296
|
+
let currentPath = normalizedBase;
|
|
297
|
+
|
|
298
|
+
for (const part of parts) {
|
|
299
|
+
currentPath = path.join(currentPath, part);
|
|
300
|
+
try {
|
|
301
|
+
const stats = fs.lstatSync(currentPath);
|
|
302
|
+
if (stats.isSymbolicLink()) {
|
|
303
|
+
// Check symlink chain depth
|
|
304
|
+
const chainResult = checkSymlinkChainDepth(currentPath, maxSymlinkDepth);
|
|
305
|
+
if (!chainResult.ok) {
|
|
306
|
+
const reason = chainResult.isCircular ? 'symlink_circular' : 'symlink_chain_too_deep';
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
error: new PathValidationError(chainResult.error, inputPath, reason),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Resolve this symlink and check its target
|
|
314
|
+
const realPath = fs.realpathSync(currentPath);
|
|
315
|
+
if (!checkWithinBase(realPath)) {
|
|
316
|
+
return {
|
|
317
|
+
ok: false,
|
|
318
|
+
error: new PathValidationError(
|
|
319
|
+
`Path contains symlink escaping base: ${currentPath} -> ${realPath}`,
|
|
320
|
+
inputPath,
|
|
321
|
+
'symlink_escape'
|
|
322
|
+
),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
// Part of path doesn't exist, stop checking
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
ok: true,
|
|
336
|
+
resolvedPath,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Synchronous version that throws on invalid paths.
|
|
342
|
+
* Use when you want exceptions rather than result objects.
|
|
343
|
+
*
|
|
344
|
+
* @param {string} inputPath - The path to validate
|
|
345
|
+
* @param {string} baseDir - The allowed base directory
|
|
346
|
+
* @param {Object} options - Validation options
|
|
347
|
+
* @returns {string} The validated absolute path
|
|
348
|
+
* @throws {PathValidationError} If path is invalid
|
|
349
|
+
*/
|
|
350
|
+
function validatePathSync(inputPath, baseDir, options = {}) {
|
|
351
|
+
const result = validatePath(inputPath, baseDir, options);
|
|
352
|
+
if (!result.ok) {
|
|
353
|
+
throw result.error;
|
|
354
|
+
}
|
|
355
|
+
return result.resolvedPath;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Check if a path contains dangerous patterns without resolving.
|
|
360
|
+
* Useful for quick pre-validation before expensive operations.
|
|
361
|
+
*
|
|
362
|
+
* @param {string} inputPath - The path to check
|
|
363
|
+
* @returns {{ safe: boolean, reason?: string }}
|
|
364
|
+
*/
|
|
365
|
+
function hasUnsafePathPatterns(inputPath) {
|
|
366
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
367
|
+
return { safe: false, reason: 'invalid_input' };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Check for null bytes (can bypass security in some systems)
|
|
371
|
+
if (inputPath.includes('\0')) {
|
|
372
|
+
return { safe: false, reason: 'null_byte' };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Check for obvious traversal patterns
|
|
376
|
+
if (inputPath.includes('..')) {
|
|
377
|
+
return { safe: false, reason: 'dot_dot_sequence' };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check for absolute paths on Unix when expecting relative
|
|
381
|
+
if (inputPath.startsWith('/') && !path.isAbsolute(inputPath)) {
|
|
382
|
+
return { safe: false, reason: 'unexpected_absolute' };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Check for Windows-style absolute paths
|
|
386
|
+
if (/^[a-zA-Z]:/.test(inputPath)) {
|
|
387
|
+
return { safe: false, reason: 'windows_absolute' };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return { safe: true };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Sanitize a filename by removing dangerous characters.
|
|
395
|
+
* Does NOT validate the full path - use with validatePath().
|
|
396
|
+
*
|
|
397
|
+
* @param {string} filename - The filename to sanitize
|
|
398
|
+
* @param {Object} options - Sanitization options
|
|
399
|
+
* @param {string} [options.replacement='_'] - Character to replace with
|
|
400
|
+
* @param {number} [options.maxLength=255] - Maximum filename length
|
|
401
|
+
* @returns {string} Sanitized filename
|
|
402
|
+
*/
|
|
403
|
+
function sanitizeFilename(filename, options = {}) {
|
|
404
|
+
const { replacement = '_', maxLength = 255 } = options;
|
|
405
|
+
|
|
406
|
+
if (!filename || typeof filename !== 'string') {
|
|
407
|
+
return '';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Remove or replace dangerous characters
|
|
411
|
+
let sanitized = filename
|
|
412
|
+
.replace(/[<>:"/\\|?*\x00-\x1f]/g, replacement) // Control chars and reserved
|
|
413
|
+
.replace(/\.{2,}/g, replacement) // Multiple dots
|
|
414
|
+
.replace(/^\.+/, replacement) // Leading dots
|
|
415
|
+
.replace(/^-+/, replacement); // Leading dashes (prevent flag injection)
|
|
416
|
+
|
|
417
|
+
// Truncate if too long
|
|
418
|
+
if (sanitized.length > maxLength) {
|
|
419
|
+
const ext = path.extname(sanitized);
|
|
420
|
+
const base = path.basename(sanitized, ext);
|
|
421
|
+
sanitized = base.slice(0, maxLength - ext.length) + ext;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return sanitized;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
module.exports = {
|
|
428
|
+
PathValidationError,
|
|
429
|
+
checkSymlinkChainDepth,
|
|
430
|
+
validatePath,
|
|
431
|
+
validatePathSync,
|
|
432
|
+
hasUnsafePathPatterns,
|
|
433
|
+
sanitizeFilename,
|
|
434
|
+
};
|