import { constants } from "node:fs";
import { lstat, mkdir, open, rename } from "node:fs/promises";
import path from "node:path";
/**
 * Hatch keeps authorization-adjacent state only in directories owned by the
 * installing user.  This is deliberately fail-closed: correcting a loose
 * permission automatically would hide a deployment mistake and can race an
 * attacker who already controls that path.
 */
export async function ensurePrivateDirectory(directory, label) {
    await mkdir(directory, { recursive: true, mode: 0o700 });
    const stat = await lstat(directory);
    if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) {
        throw new Error(`${label} must be a private non-symlink directory.`);
    }
    assertOwner(stat, label);
}
export async function readPrivateText(filePath, label) {
    try {
        const handle = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
        try {
            assertPrivateRegularFile(await handle.stat(), label);
            return await handle.readFile("utf8");
        }
        finally {
            await handle.close();
        }
    }
    catch (error) {
        if (error?.code === "ENOENT")
            return null;
        throw error;
    }
}
export async function writePrivateText(filePath, contents, label) {
    const directory = path.dirname(filePath);
    await ensurePrivateDirectory(directory, `${label} directory`);
    const temporary = path.join(directory, `.${path.basename(filePath)}-${process.pid}-${Date.now()}.tmp`);
    const handle = await open(temporary, "wx", 0o600);
    try {
        await handle.writeFile(contents, "utf8");
        await handle.sync();
    }
    finally {
        await handle.close();
    }
    await rename(temporary, filePath);
}
export function assertPrivateRegularFile(stat, label) {
    if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || (stat.mode & 0o777) !== 0o600) {
        throw new Error(`${label} must be one mode-0600 regular file.`);
    }
    assertOwner(stat, label);
}
export function assertOwner(stat, label) {
    if (typeof process.getuid === "function" && stat.uid !== process.getuid()) {
        throw new Error(`${label} must be owned by the updater user.`);
    }
}
