/**
 * Centralized Path Validation Utility
 *
 * Provides secure path validation and resolution for MCP tools.
 * Prevents path traversal attacks and ensures operations stay within allowed directories.
 *
 * Inspired by MCP filesystem server's security model.
 */
import { resolve, isAbsolute, normalize, sep } from 'path';
import { realpath } from 'fs/promises';
// Environment variable configuration
const ENV_PROJECT_ROOT = 'CCW_PROJECT_ROOT';
const ENV_ALLOWED_DIRS = 'CCW_ALLOWED_DIRS';
/**
 * Get project root directory
 * Priority: CCW_PROJECT_ROOT > process.cwd()
 */
export function getProjectRoot() {
    return process.env[ENV_PROJECT_ROOT] || process.cwd();
}
/**
 * Get allowed directories list
 * Priority: CCW_ALLOWED_DIRS > [getProjectRoot()]
 */
export function getAllowedDirectories() {
    const envDirs = process.env[ENV_ALLOWED_DIRS];
    if (envDirs) {
        return envDirs.split(',').map(d => d.trim()).filter(Boolean);
    }
    return [getProjectRoot()];
}
/**
 * Normalize path (unify separators to forward slash)
 */
export function normalizePath(p) {
    return normalize(p).replace(/\\/g, '/');
}
function canonicalizeForComparison(p) {
    const base = getProjectRoot();
    const absolute = isAbsolute(p) ? p : resolve(base, p);
    let canonical = normalize(absolute);
    // Remove trailing separators (except drive roots like C:\ and posix root /)
    canonical = canonical.replace(/[\\/]+$/, '');
    if (/^[a-zA-Z]:$/.test(canonical)) {
        canonical += sep;
    }
    else if (canonical === '') {
        canonical = sep;
    }
    // Windows paths are case-insensitive.
    if (process.platform === 'win32') {
        canonical = canonical.toLowerCase();
    }
    return canonical;
}
/**
 * Check if path is within allowed directories
 */
export function isPathWithinAllowedDirectories(targetPath, allowedDirectories) {
    const canonicalTarget = canonicalizeForComparison(targetPath);
    return allowedDirectories.some(dir => {
        const canonicalDir = canonicalizeForComparison(dir);
        if (canonicalTarget === canonicalDir)
            return true;
        const boundary = canonicalDir.endsWith(sep) ? canonicalDir : canonicalDir + sep;
        return canonicalTarget.startsWith(boundary);
    });
}
/**
 * Validate and resolve path (core function)
 *
 * Security model:
 * 1. Resolve to absolute path
 * 2. Check against allowed directories
 * 3. Resolve symlinks and re-verify
 *
 * @param filePath - Path to validate
 * @param options - Validation options
 * @returns Validated absolute path
 * @throws Error if path is outside allowed directories or validation fails
 */
export async function validatePath(filePath, options = {}) {
    const allowedDirs = options.allowedDirectories || getAllowedDirectories();
    // 1. Resolve to absolute path
    const absolutePath = isAbsolute(filePath)
        ? filePath
        : resolve(getProjectRoot(), filePath);
    const normalizedPath = normalizePath(absolutePath);
    // 2. Initial sandbox check
    if (!isPathWithinAllowedDirectories(normalizedPath, allowedDirs)) {
        throw new Error(`Access denied: path "${normalizedPath}" is outside allowed directories. ` +
            `Allowed: [${allowedDirs.join(', ')}]`);
    }
    // 3. Try to resolve symlinks and re-verify
    try {
        const realPath = await realpath(absolutePath);
        const normalizedReal = normalizePath(realPath);
        if (!isPathWithinAllowedDirectories(normalizedReal, allowedDirs)) {
            throw new Error(`Access denied: symlink target "${normalizedReal}" is outside allowed directories`);
        }
        return normalizedReal;
    }
    catch (error) {
        // File doesn't exist - validate parent directory
        if (error.code === 'ENOENT') {
            if (options.mustExist) {
                throw new Error(`File not found: ${absolutePath}`);
            }
            // Validate parent directory's real path
            const parentDir = resolve(absolutePath, '..');
            try {
                const realParent = await realpath(parentDir);
                const normalizedParent = normalizePath(realParent);
                if (!isPathWithinAllowedDirectories(normalizedParent, allowedDirs)) {
                    throw new Error(`Access denied: parent directory "${normalizedParent}" is outside allowed directories`);
                }
            }
            catch (parentError) {
                if (parentError.code === 'ENOENT') {
                    // Parent directory doesn't exist either - return original absolute path
                    // Let the caller create it if needed
                    return absolutePath;
                }
                throw parentError;
            }
            return absolutePath;
        }
        // Re-throw access denied errors
        if (error.message?.includes('Access denied')) {
            throw error;
        }
        throw error;
    }
}
/**
 * Resolve project-relative path (simplified, no strict validation)
 * Use for cases where strict security validation is not needed
 */
export function resolveProjectPath(...pathSegments) {
    return resolve(getProjectRoot(), ...pathSegments);
}
//# sourceMappingURL=path-validator.js.map