import { realpath } from 'node:fs/promises';
import { dirname, basename, join, resolve, sep } from 'node:path';
export class PathError extends Error {
    code;
    constructor(code) {
        super(code);
        this.code = code;
        this.name = 'PathError';
    }
}
function isENOENT(err) {
    return (typeof err === 'object' &&
        err !== null &&
        err.code === 'ENOENT');
}
/**
 * Resolve `userPath` under `docsDir` and return the canonical absolute
 * path. Rejects symlinks that escape `docsDir`, sibling-prefix bypass
 * (`docsDir="/foo"` vs `/foobar`), and NUL-laced input. The returned
 * path is the realpath — callers should use it for all subsequent fs
 * calls to avoid TOCTOU reintroducing a symlink.
 */
export async function ensureInside(docsDir, userPath, opts = {}) {
    if (userPath.includes('\0')) {
        throw new PathError('forbidden');
    }
    const resolved = userPath ? resolve(docsDir, userPath) : docsDir;
    // Lexical containment: the *addressed* path (before any symlink
    // resolution) must live inside docsDir. `resolve()` collapses `..`
    // lexically so this catches traversal and sibling-prefix bypass
    // (`/tmp/foo` vs `/tmp/foobar`) without hitting the filesystem. This
    // check is enforced regardless of `allowSymlinks`.
    if (resolved !== docsDir && !resolved.startsWith(docsDir + sep)) {
        throw new PathError('forbidden');
    }
    let real;
    try {
        real = await realpath(resolved);
    }
    catch (err) {
        if (opts.allowMissing && isENOENT(err)) {
            const parent = await realpath(dirname(resolved)).catch(() => {
                throw new PathError('forbidden');
            });
            real = join(parent, basename(resolved));
        }
        else {
            throw new PathError('forbidden');
        }
    }
    if (opts.allowSymlinks) {
        // Escape hatch: the addressed path is already known to sit inside
        // docsDir (lexical check above). The realpath may point anywhere —
        // that is the whole point of the flag.
        return real;
    }
    const docsReal = await realpath(docsDir);
    if (real !== docsReal && !real.startsWith(docsReal + sep)) {
        throw new PathError('forbidden');
    }
    return real;
}
//# sourceMappingURL=path-sandbox.js.map