/**
 * Path Validation Utilities
 *
 * Security-focused path validation to prevent path traversal attacks
 * and ensure paths are within allowed boundaries.
 *
 * @module util/paths
 */
import { stat, lstat, realpath } from "fs/promises";
import path from "path";
/**
 * Error thrown when path validation fails
 */
export class PathValidationError extends Error {
    path;
    reason;
    constructor(message, path, reason) {
        super(message);
        this.path = path;
        this.reason = reason;
        this.name = "PathValidationError";
    }
}
/**
 * Validate a project path for security
 *
 * Checks:
 * 1. Path is absolute after resolution
 * 2. Path exists on the filesystem
 * 3. Path is a directory (optional)
 * 4. Symlinks are resolved and checked (optional)
 * 5. Path doesn't escape base directory (optional)
 *
 * @param projectPath - The path to validate
 * @param options - Validation options
 * @returns The validated, resolved absolute path
 * @throws PathValidationError if validation fails
 */
export async function validateProjectPath(projectPath, options = {}) {
    const { requireDirectory = true, allowSymlinks = false, basePath } = options;
    // Resolve to absolute path
    const resolvedPath = path.resolve(projectPath);
    // Check for path traversal attempts
    if (projectPath.includes("..") && !path.isAbsolute(projectPath)) {
        // Verify the resolved path doesn't escape intended boundaries
        const normalizedInput = path.normalize(projectPath);
        if (normalizedInput.startsWith("..")) {
            throw new PathValidationError(`Path traversal detected: ${projectPath}`, projectPath, "path_traversal");
        }
    }
    try {
        // Check if path exists (using lstat to detect symlinks)
        const lstats = await lstat(resolvedPath);
        // Handle symlinks
        if (lstats.isSymbolicLink()) {
            if (!allowSymlinks) {
                // Resolve the symlink and verify it doesn't escape
                const realPath = await realpath(resolvedPath);
                // If basePath is specified, ensure symlink target is within it
                if (basePath) {
                    const resolvedBase = path.resolve(basePath);
                    if (!realPath.startsWith(resolvedBase)) {
                        throw new PathValidationError(`Symlink escapes base directory: ${projectPath} -> ${realPath}`, projectPath, "symlink_escape");
                    }
                }
                // Use the real path
                const realStats = await stat(realPath);
                if (requireDirectory && !realStats.isDirectory()) {
                    throw new PathValidationError(`Path is not a directory: ${projectPath}`, projectPath, "not_directory");
                }
                return realPath;
            }
        }
        // Check if it's a directory (if required)
        if (requireDirectory && !lstats.isDirectory()) {
            throw new PathValidationError(`Path is not a directory: ${projectPath}`, projectPath, "not_directory");
        }
        // If basePath is specified, ensure the path is within it
        if (basePath) {
            const resolvedBase = path.resolve(basePath);
            if (!resolvedPath.startsWith(resolvedBase)) {
                throw new PathValidationError(`Path is outside base directory: ${projectPath}`, projectPath, "path_traversal");
            }
        }
        return resolvedPath;
    }
    catch (error) {
        if (error instanceof PathValidationError) {
            throw error;
        }
        // Handle ENOENT (file not found)
        if (error.code === "ENOENT") {
            throw new PathValidationError(`Path does not exist: ${projectPath}`, projectPath, "not_found");
        }
        // Re-throw other errors
        throw new PathValidationError(`Invalid path: ${projectPath} - ${error.message}`, projectPath, "invalid_path");
    }
}
/**
 * Validate a file path (not requiring directory)
 */
export async function validateFilePath(filePath, options = {}) {
    return validateProjectPath(filePath, { ...options, requireDirectory: false });
}
/**
 * Check if a path is safe (doesn't throw, returns boolean)
 */
export async function isPathSafe(projectPath, options = {}) {
    try {
        await validateProjectPath(projectPath, options);
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Sanitize a path for use in error messages (remove sensitive info)
 */
export function sanitizePathForLogging(inputPath) {
    // Remove home directory prefix if present
    const homeDir = process.env.HOME || process.env.USERPROFILE || "";
    if (homeDir && inputPath.startsWith(homeDir)) {
        return "~" + inputPath.slice(homeDir.length);
    }
    return inputPath;
}
//# sourceMappingURL=paths.js.map