"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ensureBundleRoot = exports.DiskBundleStore = void 0;
const fs_1 = __importDefault(require("fs"));
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const logger_1 = require("../utils/logger");
const log = (0, logger_1.createLogger)('BundleStore');
/** Reject paths that try to escape the bundle root. Mirrors the lexical
 *  containment + traversal checks used in VariantHistoryService. */
const assertSafeBundlePath = (relPath) => {
    if (relPath.length === 0 ||
        relPath.includes('\0') ||
        path_1.default.isAbsolute(relPath) ||
        relPath.split(/[\\/]+/).some((part) => part === '..')) {
        throw new Error(`BundleStore: invalid bundle path '${relPath}'`);
    }
};
/**
 * Filesystem-backed bundle store. Extracts each uploaded file under
 * `<root>/<deployId>/`. Single-instance only (a Render persistent disk is not
 * shared across instances) — fine for the MVP; swap for object storage later
 * behind this same interface.
 */
class DiskBundleStore {
    root;
    constructor(root) {
        this.root = root;
    }
    rootFor(deployId) {
        return path_1.default.join(this.root, deployId);
    }
    async put(deployId, files) {
        const bundleRoot = this.rootFor(deployId);
        // Stage then atomically swap so a partially written bundle is never served.
        const stagingRoot = `${bundleRoot}.tmp-${process.pid}-${Date.now()}`;
        await promises_1.default.rm(stagingRoot, { recursive: true, force: true });
        await promises_1.default.mkdir(stagingRoot, { recursive: true });
        try {
            for (const file of files) {
                assertSafeBundlePath(file.path);
                const target = path_1.default.join(stagingRoot, file.path);
                // Defense in depth: ensure the resolved path is still inside staging.
                const rel = path_1.default.relative(stagingRoot, target);
                if (rel.startsWith('..') || path_1.default.isAbsolute(rel)) {
                    throw new Error(`BundleStore: path escapes bundle root '${file.path}'`);
                }
                await promises_1.default.mkdir(path_1.default.dirname(target), { recursive: true });
                await promises_1.default.writeFile(target, Buffer.from(file.content, 'base64'));
            }
            await promises_1.default.rm(bundleRoot, { recursive: true, force: true });
            await promises_1.default.rename(stagingRoot, bundleRoot);
            log.info(`Stored bundle ${deployId} (${files.length} files)`);
        }
        catch (err) {
            await promises_1.default.rm(stagingRoot, { recursive: true, force: true });
            throw err;
        }
    }
    async has(deployId) {
        try {
            const stat = await promises_1.default.stat(this.rootFor(deployId));
            return stat.isDirectory();
        }
        catch {
            return false;
        }
    }
    async read(deployId, assetPath) {
        assertSafeBundlePath(assetPath);
        const bundleRoot = this.rootFor(deployId);
        const target = path_1.default.join(bundleRoot, assetPath);
        const rel = path_1.default.relative(bundleRoot, target);
        if (rel.startsWith('..') || path_1.default.isAbsolute(rel))
            return null;
        try {
            const stat = await promises_1.default.stat(target);
            if (!stat.isFile())
                return null;
            return await promises_1.default.readFile(target);
        }
        catch (err) {
            const code = err.code;
            if (code !== 'ENOENT')
                log.warn(`read failed for ${deployId}/${assetPath}`, err);
            return null;
        }
    }
}
exports.DiskBundleStore = DiskBundleStore;
/** Best-effort check that the configured bundle root is writable at boot. */
const ensureBundleRoot = async (root) => {
    await promises_1.default.mkdir(root, { recursive: true });
    if (!fs_1.default.existsSync(root)) {
        throw new Error(`BundleStore: bundle root '${root}' could not be created`);
    }
};
exports.ensureBundleRoot = ensureBundleRoot;
//# sourceMappingURL=BundleStore.js.map