"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createPrototypeHostRouter = void 0;
const crypto_1 = __importDefault(require("crypto"));
const path_1 = __importDefault(require("path"));
const express_1 = require("express");
const v3_1 = require("zod/v3");
const logger_1 = require("../utils/logger");
const log = (0, logger_1.createLogger)('PrototypeHostRouter');
/** ids are generated server-side via randomUUID(); also used to validate the
 *  `:deployId` path param so a serve request can't escape the bundle root. */
const SAFE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
const MIME = {
    '.html': 'text/html; charset=utf-8',
    '.htm': 'text/html; charset=utf-8',
    '.css': 'text/css; charset=utf-8',
    '.js': 'application/javascript; charset=utf-8',
    '.mjs': 'application/javascript; charset=utf-8',
    '.json': 'application/json; charset=utf-8',
    '.svg': 'image/svg+xml',
    '.png': 'image/png',
    '.jpg': 'image/jpeg',
    '.jpeg': 'image/jpeg',
    '.gif': 'image/gif',
    '.webp': 'image/webp',
    '.avif': 'image/avif',
    '.ico': 'image/x-icon',
    '.woff': 'font/woff',
    '.woff2': 'font/woff2',
    '.ttf': 'font/ttf',
    '.otf': 'font/otf',
    '.glb': 'model/gltf-binary',
    '.gltf': 'model/gltf+json',
    '.txt': 'text/plain; charset=utf-8',
    '.map': 'application/json; charset=utf-8',
    '.wasm': 'application/wasm',
};
const bundleFileSchema = v3_1.z.object({
    path: v3_1.z.string().min(1),
    content: v3_1.z.string(),
});
const publishBodySchema = v3_1.z.object({
    files: v3_1.z.array(bundleFileSchema).min(1),
});
/**
 * Inject a `<base href>` so a bundle hosted under `/p/<deployId>/` resolves its
 * relative asset URLs correctly. The client builds with `vite build --base=./`
 * (relative URLs); this anchors them at the deploy sub-path. Idempotent.
 * Adapted from agentVariants.injectBaseHref.
 */
const injectBaseHref = (html, baseHref) => {
    if (/<base\s/i.test(html))
        return html;
    const tag = `<base href="${baseHref.replace(/"/g, '&quot;')}">`;
    const headMatch = html.match(/<head[^>]*>/i);
    if (headMatch) {
        const insertAt = headMatch.index + headMatch[0].length;
        return html.slice(0, insertAt) + tag + html.slice(insertAt);
    }
    const doctypeMatch = html.match(/<!doctype[^>]*>/i);
    if (doctypeMatch) {
        const insertAt = doctypeMatch.index + doctypeMatch[0].length;
        return html.slice(0, insertAt) + tag + html.slice(insertAt);
    }
    return tag + html;
};
const extractBearer = (req) => {
    const header = req.headers['authorization'];
    if (typeof header === 'string' && header.startsWith('Bearer ')) {
        return header.slice(7);
    }
    const rivetToken = req.headers['x-rivet-auth-token'];
    return typeof rivetToken === 'string' && rivetToken.length > 0
        ? rivetToken
        : null;
};
const normalizeAssetPath = (raw) => {
    if (Array.isArray(raw))
        return raw.join('/');
    if (typeof raw === 'string')
        return raw;
    return '';
};
const createPrototypeHostRouter = (deps) => {
    const router = (0, express_1.Router)();
    const maxBundleBytes = deps.maxBundleBytes ?? 25 * 1024 * 1024;
    const publicBaseUrl = deps.publicBaseUrl.replace(/\/+$/, '');
    router.get('/health', (_req, res) => {
        res.json({ ok: true });
    });
    // Publish a built prototype bundle. Auth: the user's Rivet JWT — the host
    // holds all hosting infrastructure, the client only ever sends its token.
    router.post('/publish', async (req, res) => {
        const token = extractBearer(req);
        if (!token) {
            res.status(401).json({
                errorCode: 'AUTH_REQUIRED',
                message: 'Please run `rivet login` to deploy a prototype.',
            });
            return;
        }
        const user = await deps.auth.verifyToken(token);
        if (!user) {
            res.status(401).json({
                errorCode: 'AUTH_INVALID',
                message: 'Your session may have expired. Run `rivet login` again.',
            });
            return;
        }
        const parsed = publishBodySchema.safeParse(req.body);
        if (!parsed.success) {
            res.status(400).json({
                errorCode: 'SCHEMA_VALIDATION_FAILED',
                message: parsed.error.message,
            });
            return;
        }
        const files = parsed.data.files;
        const totalBytes = files.reduce((sum, f) => sum + Math.ceil((f.content.length * 3) / 4), 0);
        if (totalBytes > maxBundleBytes) {
            res.status(413).json({
                errorCode: 'BUNDLE_TOO_LARGE',
                message: `Prototype bundle exceeds the ${Math.round(maxBundleBytes / (1024 * 1024))}MB limit.`,
            });
            return;
        }
        if (!files.some((f) => f.path === 'index.html')) {
            res.status(400).json({
                errorCode: 'BUNDLE_MISSING_INDEX',
                message: 'Bundle must contain an index.html at its root.',
            });
            return;
        }
        const deployId = crypto_1.default.randomUUID();
        try {
            await deps.store.put(deployId, files);
        }
        catch (err) {
            log.error(`publish failed for user ${user.id}`, err);
            res.status(500).json({
                errorCode: 'PUBLISH_FAILED',
                message: err instanceof Error ? err.message : 'Failed to store bundle.',
            });
            return;
        }
        const shareUrl = `${publicBaseUrl}/p/${deployId}/`;
        log.info(`Published prototype ${deployId} for user ${user.id}`);
        res.json({ deployId, shareUrl });
    });
    const serve = async (req, res, deployId, assetPath) => {
        if (!SAFE_ID_PATTERN.test(deployId)) {
            res.status(404).send('Not found');
            return;
        }
        if (!(await deps.store.has(deployId))) {
            res.status(404).send('Not found');
            return;
        }
        // Shared prototypes are meant to be opened and embedded anywhere — do NOT
        // lock framing down. Keep nosniff to prevent MIME confusion.
        res.setHeader('X-Content-Type-Options', 'nosniff');
        res.setHeader('Cache-Control', 'no-store');
        const cleanPath = assetPath.replace(/^\/+/, '');
        const wantsIndex = cleanPath === '' || cleanPath.endsWith('/');
        const baseHref = `/p/${deployId}/`;
        const serveIndex = async () => {
            const html = await deps.store.read(deployId, 'index.html');
            if (!html) {
                res.status(404).send('Not found');
                return;
            }
            res
                .status(200)
                .setHeader('Content-Type', MIME['.html'])
                .send(injectBaseHref(html.toString('utf8'), baseHref));
        };
        if (wantsIndex) {
            await serveIndex();
            return;
        }
        const decoded = decodeURIComponent(cleanPath);
        const body = await deps.store.read(deployId, decoded);
        if (!body) {
            // SPA fallback: unknown routes resolve to index.html so client-side
            // routing works when the link is shared with a deep path.
            await serveIndex();
            return;
        }
        const ext = path_1.default.extname(decoded).toLowerCase();
        res
            .status(200)
            .setHeader('Content-Type', MIME[ext] || 'application/octet-stream')
            .send(body);
    };
    router.get('/p/:deployId', async (req, res) => {
        await serve(req, res, req.params.deployId, '');
    });
    router.get('/p/:deployId/{*assetPath}', async (req, res) => {
        await serve(req, res, req.params.deployId, normalizeAssetPath(req.params.assetPath));
    });
    return router;
};
exports.createPrototypeHostRouter = createPrototypeHostRouter;
//# sourceMappingURL=prototypeHost.js.map