"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEV_ROUTER_PORT = void 0;
exports.parseMountConflictError = parseMountConflictError;
exports.buildLocalDevAppUrl = buildLocalDevAppUrl;
exports.terminateChildProcessTree = terminateChildProcessTree;
exports.ensureDevRouterRunning = ensureDevRouterRunning;
exports.updateMountedDevRuntimeAssetManifest = updateMountedDevRuntimeAssetManifest;
exports.startDevServer = startDevServer;
exports.isDevServerAvailable = isDevServerAvailable;
exports.createDevRouterServer = createDevRouterServer;
exports.listenDevRouterServer = listenDevRouterServer;
exports.runDevRouterServer = runDevRouterServer;
const node_http_1 = __importDefault(require("node:http"));
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const node_child_process_1 = require("node:child_process");
const node_crypto_1 = require("node:crypto");
exports.DEV_ROUTER_PORT = 8888;
const DEV_ROUTER_HOST = '127.0.0.1';
const CONTROL_PREFIX = '/_playdrop';
const DEV_ROUTER_PROTOCOL_VERSION = 3;
const APPROVED_PLAYDROP_WEB_ORIGINS = new Set([
    'https://playdrop.ai',
    'https://www.playdrop.ai',
]);
const CONTENT_TYPE_BY_EXTENSION = {
    '.html': 'text/html; charset=utf-8',
    '.js': 'application/javascript; charset=utf-8',
    '.mjs': 'application/javascript; charset=utf-8',
    '.cjs': 'application/javascript; charset=utf-8',
    '.css': 'text/css; charset=utf-8',
    '.json': 'application/json; charset=utf-8',
    '.png': 'image/png',
    '.jpg': 'image/jpeg',
    '.jpeg': 'image/jpeg',
    '.svg': 'image/svg+xml',
    '.webp': 'image/webp',
    '.glb': 'model/gltf-binary',
    '.gltf': 'model/gltf+json',
    '.wasm': 'application/wasm',
    '.mp4': 'video/mp4',
    '.webm': 'video/webm',
};
const routerMountsById = new Map();
const routerMountIdsByKey = new Map();
function resolveApprovedDevRouterOrigin(originHeader) {
    const origin = originHeader?.trim();
    if (!origin) {
        return null;
    }
    let parsedOrigin;
    try {
        parsedOrigin = new URL(origin);
    }
    catch {
        return null;
    }
    if (APPROVED_PLAYDROP_WEB_ORIGINS.has(parsedOrigin.origin)) {
        return parsedOrigin.origin;
    }
    const isLocalWebOrigin = (parsedOrigin.protocol === 'http:' || parsedOrigin.protocol === 'https:')
        && (parsedOrigin.hostname === 'localhost' || parsedOrigin.hostname === '127.0.0.1');
    return isLocalWebOrigin ? parsedOrigin.origin : null;
}
function resolveContentType(filePath) {
    const extension = (0, node_path_1.extname)(filePath).toLowerCase();
    return CONTENT_TYPE_BY_EXTENSION[extension] || 'application/octet-stream';
}
function getCliEntrypointPath() {
    return (0, node_path_1.resolve)(__dirname, '..', 'index.js');
}
function buildMountKey(creatorUsername, appType, appName) {
    return `${creatorUsername}:${appType}:${appName}`;
}
function encodeMountConflictError(details) {
    const encoded = Buffer.from(JSON.stringify(details), 'utf8').toString('base64url');
    return `mount_conflict:${encoded}`;
}
function parseMountConflictError(message) {
    if (!message.startsWith('mount_conflict:')) {
        return null;
    }
    try {
        const decoded = Buffer.from(message.slice('mount_conflict:'.length), 'base64url').toString('utf8');
        const payload = JSON.parse(decoded);
        if (typeof payload.ref !== 'string'
            || typeof payload.ownerPid !== 'number'
            || !Number.isInteger(payload.ownerPid)
            || payload.ownerPid <= 0
            || typeof payload.repoRoot !== 'string'
            || typeof payload.htmlPath !== 'string') {
            return null;
        }
        return {
            ref: payload.ref,
            ownerPid: payload.ownerPid,
            repoRoot: payload.repoRoot,
            htmlPath: payload.htmlPath,
        };
    }
    catch {
        return null;
    }
}
function buildLocalDevAppUrl(input) {
    const port = input.port ?? exports.DEV_ROUTER_PORT;
    return `http://${DEV_ROUTER_HOST}:${port}/apps/dev/${encodeURIComponent(input.creatorUsername)}/${encodeURIComponent(input.appType)}/${encodeURIComponent(input.appName)}/index.html`;
}
function respondWithBuffer(res, method, statusCode, contentType, payload) {
    res.statusCode = statusCode;
    res.setHeader('Content-Type', contentType);
    res.setHeader('Content-Length', String(payload.length));
    res.setHeader('Cache-Control', 'no-store');
    if (method === 'HEAD') {
        res.end();
        return;
    }
    res.end(payload);
}
function respondWithText(res, method, statusCode, message) {
    const payload = Buffer.from(message, 'utf8');
    respondWithBuffer(res, method, statusCode, 'text/plain; charset=utf-8', payload);
}
function sendJson(res, statusCode, payload) {
    const body = Buffer.from(JSON.stringify(payload), 'utf8');
    respondWithBuffer(res, 'GET', statusCode, 'application/json; charset=utf-8', body);
}
function isPidAlive(pid) {
    if (!Number.isInteger(pid) || pid <= 0) {
        return false;
    }
    if (process.platform === 'linux') {
        const info = readLinuxProcessInfo(pid);
        return info !== null && info.state !== 'Z';
    }
    try {
        process.kill(pid, 0);
        return true;
    }
    catch {
        return false;
    }
}
function readLinuxProcessInfo(pid) {
    if (!Number.isInteger(pid) || pid <= 0 || process.platform !== 'linux') {
        return null;
    }
    try {
        const raw = (0, node_fs_1.readFileSync)(`/proc/${pid}/stat`, 'utf8');
        const commandEnd = raw.lastIndexOf(')');
        if (commandEnd < 0) {
            return null;
        }
        const fields = raw.slice(commandEnd + 2).trim().split(/\s+/u);
        const state = fields[0];
        const groupId = Number(fields[2]);
        if (typeof state !== 'string' || !Number.isInteger(groupId) || groupId <= 0) {
            return null;
        }
        return { state, groupId };
    }
    catch (error) {
        if (error?.code === 'ENOENT') {
            return null;
        }
        throw error;
    }
}
function removeRouterMount(mountId) {
    const mount = routerMountsById.get(mountId);
    if (!mount) {
        return;
    }
    routerMountsById.delete(mountId);
    routerMountIdsByKey.delete(mount.key);
}
function cleanupStaleRouterMounts() {
    for (const [mountId, mount] of routerMountsById.entries()) {
        if (!isPidAlive(mount.ownerPid)) {
            removeRouterMount(mountId);
        }
    }
}
function findRouterMountByCreatorAndApp(creatorUsername, appName) {
    let match = null;
    for (const mount of routerMountsById.values()) {
        if (mount.creatorUsername !== creatorUsername || mount.appName !== appName) {
            continue;
        }
        if (match) {
            throw new Error(`ambiguous_mount_lookup:${creatorUsername}/${appName}`);
        }
        match = mount;
    }
    return match;
}
function resolveStaticPath(staticRoot, rawRelativePath) {
    const normalizedRelativePath = (0, node_path_1.normalize)(rawRelativePath.replace(/^\/+/, ''));
    if (!normalizedRelativePath || normalizedRelativePath === '.' || normalizedRelativePath.startsWith('..')) {
        return null;
    }
    const absolutePath = (0, node_path_1.resolve)(staticRoot, normalizedRelativePath);
    const rootPrefix = staticRoot.endsWith(node_path_1.sep) ? staticRoot : `${staticRoot}${node_path_1.sep}`;
    if (absolutePath !== staticRoot && !absolutePath.startsWith(rootPrefix)) {
        return null;
    }
    return absolutePath;
}
function parseMountPath(pathname) {
    if (!pathname.startsWith('/apps/dev/')) {
        return null;
    }
    const segments = pathname.split('/').filter(Boolean);
    if (segments.length < 6) {
        return null;
    }
    // filter(Boolean) removes the leading empty segment from absolute paths, so the
    // creator tuple starts at indices 2..4 for /apps/dev/<creator>/<type>/<name>/...
    const [, , creatorUsername, appType, appName, ...rest] = segments;
    if (!creatorUsername || !appType || !appName || rest.length === 0) {
        return null;
    }
    return {
        key: buildMountKey(decodeURIComponent(creatorUsername), decodeURIComponent(appType), decodeURIComponent(appName)),
        assetPath: rest.map((entry) => decodeURIComponent(entry)).join('/'),
    };
}
function getOwnedRuntimeAssetFile(mount, ownedAssetName, role) {
    const files = mount.runtimeAssetManifest?.ownedAssetFiles?.[ownedAssetName];
    if (!files) {
        return null;
    }
    return files[role.trim().toLowerCase()] ?? null;
}
async function readJsonBody(req) {
    const chunks = [];
    for await (const chunk of req) {
        chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
    }
    const raw = Buffer.concat(chunks).toString('utf8').trim();
    return raw ? JSON.parse(raw) : {};
}
function spawnDevScript(projectInfo) {
    if (!projectInfo.projectDir || !projectInfo.packageJson || typeof projectInfo.packageJson.scripts?.dev !== 'string') {
        return null;
    }
    const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
    const child = (0, node_child_process_1.spawn)(npmCommand, ['run', 'dev'], {
        cwd: projectInfo.projectDir,
        stdio: 'inherit',
        env: { ...process.env },
        detached: process.platform !== 'win32',
    });
    return child;
}
function signalProcess(pid, signal, useGroup) {
    const targetPid = useGroup ? -pid : pid;
    try {
        process.kill(targetPid, signal);
        return true;
    }
    catch (error) {
        if (error?.code === 'ESRCH') {
            return false;
        }
        throw error;
    }
}
function isProcessGroupAlive(pid) {
    if (!Number.isInteger(pid) || pid <= 0) {
        return false;
    }
    if (process.platform === 'linux') {
        for (const entry of (0, node_fs_1.readdirSync)('/proc', { withFileTypes: true })) {
            if (!entry.isDirectory() || !/^\d+$/u.test(entry.name)) {
                continue;
            }
            const info = readLinuxProcessInfo(Number(entry.name));
            if (info && info.groupId === pid && info.state !== 'Z') {
                return true;
            }
        }
        return false;
    }
    try {
        process.kill(-pid, 0);
        return true;
    }
    catch (error) {
        if (error?.code === 'ESRCH') {
            return false;
        }
        if (error?.code === 'EPERM') {
            return true;
        }
        throw error;
    }
}
async function waitForChildExit(child, timeoutMs) {
    const pid = child.pid;
    if (!pid || !isPidAlive(pid)) {
        return true;
    }
    return await new Promise((resolvePromise) => {
        let settled = false;
        const onExit = () => {
            if (settled) {
                return;
            }
            settled = true;
            clearTimeout(timeout);
            child.off('exit', onExit);
            resolvePromise(true);
        };
        const timeout = setTimeout(() => {
            if (settled) {
                return;
            }
            settled = true;
            child.off('exit', onExit);
            resolvePromise(!pid || !isPidAlive(pid));
        }, timeoutMs);
        timeout.unref?.();
        child.once('exit', onExit);
    });
}
async function waitForProcessGroupExit(pid, timeoutMs) {
    if (!isProcessGroupAlive(pid)) {
        return true;
    }
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        await new Promise((resolvePromise) => setTimeout(resolvePromise, 50));
        if (!isProcessGroupAlive(pid)) {
            return true;
        }
    }
    return !isProcessGroupAlive(pid);
}
async function terminateChildProcessTree(child, timeoutMs = 2000) {
    const pid = child?.pid;
    if (!child || !pid || !isPidAlive(pid)) {
        return;
    }
    const useGroup = process.platform !== 'win32';
    signalProcess(pid, 'SIGTERM', useGroup);
    const exited = useGroup
        ? await waitForProcessGroupExit(pid, timeoutMs)
        : await waitForChildExit(child, timeoutMs);
    if (exited) {
        return;
    }
    signalProcess(pid, 'SIGKILL', useGroup);
    if (useGroup) {
        await waitForProcessGroupExit(pid, timeoutMs);
        return;
    }
    await waitForChildExit(child, timeoutMs);
}
async function fetchRouterJson(path, init = {}, port = exports.DEV_ROUTER_PORT) {
    const response = await fetch(`http://${DEV_ROUTER_HOST}:${port}${path}`, init);
    if (!response.ok) {
        if (response.status === 404) {
            return null;
        }
        const payload = await response.json().catch(() => null);
        throw new Error(typeof payload?.error === 'string'
            ? payload.error
            : `router_request_failed:${response.status}`);
    }
    return await response.json();
}
async function fetchRouterHealth(port = exports.DEV_ROUTER_PORT, timeoutMs = 400) {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), timeoutMs);
    timeout.unref?.();
    try {
        const response = await fetch(`http://${DEV_ROUTER_HOST}:${port}${CONTROL_PREFIX}/health`, {
            method: 'GET',
            signal: controller.signal,
        });
        if (!response.ok) {
            return null;
        }
        const payload = await response.json().catch(() => null);
        if (!payload || typeof payload !== 'object' || payload.status !== 'ok') {
            return null;
        }
        return payload;
    }
    catch {
        return null;
    }
    finally {
        clearTimeout(timeout);
    }
}
function isCompatibleRouterHealth(health) {
    return health.protocolVersion === DEV_ROUTER_PROTOCOL_VERSION
        && health.capabilities?.runtimeAssetManifest === true
        && health.capabilities?.runtimeAssetManifestLookup === true;
}
async function ensureDevRouterRunning(port = exports.DEV_ROUTER_PORT) {
    const existingHealth = await fetchRouterHealth(port);
    if (existingHealth) {
        if (isCompatibleRouterHealth(existingHealth)) {
            return;
        }
        throw new Error(`dev_router_incompatible:port_${port}`);
    }
    const cliEntrypoint = getCliEntrypointPath();
    const child = (0, node_child_process_1.spawn)(process.execPath, [cliEntrypoint, 'project', '_dev-router', 'serve'], {
        detached: true,
        stdio: 'ignore',
        env: {
            ...process.env,
            PLAYDROP_DEV_ROUTER_PORT: String(port),
        },
    });
    child.unref();
    const deadline = Date.now() + 5000;
    while (Date.now() < deadline) {
        const health = await fetchRouterHealth(port, 250);
        if (health && isCompatibleRouterHealth(health)) {
            return;
        }
        await new Promise((resolveTimeout) => setTimeout(resolveTimeout, 150));
    }
    throw new Error('dev_router_start_failed');
}
async function registerDevMount(input, port = exports.DEV_ROUTER_PORT) {
    const response = await fetch(`http://${DEV_ROUTER_HOST}:${port}${CONTROL_PREFIX}/mounts`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(input),
    });
    const payload = await response.json().catch(() => null);
    if (!response.ok || !payload) {
        throw new Error(`mount_register_failed:${response.status}`);
    }
    if ('error' in payload) {
        throw new Error(encodeMountConflictError({
            ref: `${payload.creatorUsername}/${payload.appType}/${payload.appName}`,
            ownerPid: payload.ownerPid,
            repoRoot: payload.repoRoot,
            htmlPath: payload.htmlPath,
        }));
    }
    return {
        mountId: payload.mountId,
        appUrl: payload.appUrl,
    };
}
async function heartbeatDevMount(mountId, ownerPid, port = exports.DEV_ROUTER_PORT) {
    await fetchRouterJson(`${CONTROL_PREFIX}/mounts/${encodeURIComponent(mountId)}/heartbeat`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ ownerPid }),
    }, port);
}
async function unregisterDevMount(mountId, port = exports.DEV_ROUTER_PORT) {
    const response = await fetch(`http://${DEV_ROUTER_HOST}:${port}${CONTROL_PREFIX}/mounts/${encodeURIComponent(mountId)}`, {
        method: 'DELETE',
    });
    if (!response.ok && response.status !== 404) {
        throw new Error(`mount_unregister_failed:${response.status}`);
    }
}
async function updateMountedDevRuntimeAssetManifest(input) {
    const port = input.port ?? exports.DEV_ROUTER_PORT;
    const response = await fetch(`http://${DEV_ROUTER_HOST}:${port}${CONTROL_PREFIX}/apps/dev/${encodeURIComponent(input.creatorUsername)}/${encodeURIComponent(input.appName)}/runtime-assets`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            runtimeAssetManifest: input.runtimeAssetManifest ?? null,
        }),
    });
    if (!response.ok) {
        const payload = await response.json().catch(() => null);
        throw new Error(typeof payload?.error === 'string'
            ? payload.error
            : `runtime_asset_manifest_update_failed:${response.status}`);
    }
}
async function startDevServer(options) {
    const { appName, appType, creatorUsername, htmlPath, port, projectInfo, runtimeAssetManifest } = options;
    const ownerPid = process.pid;
    let closing = false;
    const devProcess = spawnDevScript(projectInfo);
    if (devProcess) {
        devProcess.on('exit', code => {
            if (closing) {
                return;
            }
            if (code !== 0 && code !== null) {
                console.warn(`npm run dev exited with code ${code}.`);
            }
        });
    }
    const startedDevProcess = Boolean(devProcess);
    const staticRoot = (0, node_path_1.resolve)((0, node_path_1.dirname)(htmlPath));
    const repoRoot = (0, node_path_1.resolve)(projectInfo.projectDir ?? (0, node_path_1.dirname)(htmlPath));
    let mountId = '';
    let appUrl = '';
    try {
        await ensureDevRouterRunning(port);
        const registeredMount = await registerDevMount({
            creatorUsername,
            appType,
            appName,
            htmlPath,
            staticRoot,
            repoRoot,
            ownerPid,
            runtimeAssetManifest: runtimeAssetManifest ?? null,
        }, port);
        mountId = registeredMount.mountId;
        appUrl = registeredMount.appUrl;
    }
    catch (error) {
        if (devProcess && !devProcess.killed) {
            devProcess.kill();
        }
        throw error;
    }
    const heartbeat = setInterval(() => {
        heartbeatDevMount(mountId, ownerPid, port).catch(() => {
            // Let the serving request fail naturally if the router disappears.
        });
    }, 2000);
    return {
        server: null,
        devProcess,
        startedDevProcess,
        appUrl,
        close: async () => {
            if (closing)
                return;
            closing = true;
            clearInterval(heartbeat);
            try {
                await unregisterDevMount(mountId, port);
            }
            finally {
                await terminateChildProcessTree(devProcess);
            }
        },
    };
}
async function isDevServerAvailable(input, timeoutMs = 1000) {
    const port = input.port ?? exports.DEV_ROUTER_PORT;
    const health = await fetchRouterHealth(port, Math.min(timeoutMs, 400));
    if (!health || !isCompatibleRouterHealth(health)) {
        return false;
    }
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), timeoutMs);
    timeout.unref?.();
    try {
        const response = await fetch(buildLocalDevAppUrl({
            ...input,
            port,
        }), {
            method: 'GET',
            signal: controller.signal,
        });
        return response.ok;
    }
    catch {
        return false;
    }
    finally {
        clearTimeout(timeout);
    }
}
function createDevRouterServer(initialPort = exports.DEV_ROUTER_PORT) {
    let activePort = initialPort;
    // eslint-disable-next-line complexity
    const handleRequest = async (req, res) => {
        const method = req.method || 'GET';
        const allowedOrigin = resolveApprovedDevRouterOrigin(typeof req.headers.origin === 'string' ? req.headers.origin : undefined);
        if (allowedOrigin) {
            res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
        }
        res.setHeader('Vary', 'Origin');
        res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
        res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Playdrop-Client, X-Playdrop-Client-Version, X-Playdrop-Client-Build, X-Playdrop-Platform, X-Playdrop-Platform-Version');
        res.setHeader('Access-Control-Allow-Private-Network', 'true');
        if (method === 'OPTIONS') {
            res.statusCode = 200;
            res.setHeader('Access-Control-Max-Age', '600');
            res.end();
            return;
        }
        let requestUrl;
        try {
            requestUrl = new URL(req.url || '/', `http://${DEV_ROUTER_HOST}:${activePort}`);
        }
        catch {
            respondWithText(res, method, 400, 'Invalid URL path');
            return;
        }
        const pathname = decodeURIComponent(requestUrl.pathname);
        const searchParams = requestUrl.searchParams;
        if (pathname === `${CONTROL_PREFIX}/health` && method === 'GET') {
            sendJson(res, 200, {
                status: 'ok',
                protocolVersion: DEV_ROUTER_PROTOCOL_VERSION,
                capabilities: {
                    runtimeAssetManifest: true,
                    runtimeAssetManifestLookup: true,
                },
            });
            return;
        }
        if (pathname.startsWith(`${CONTROL_PREFIX}/apps/dev/`) && pathname.endsWith('/runtime-assets') && method === 'GET') {
            cleanupStaleRouterMounts();
            const segments = pathname.split('/').filter(Boolean);
            if (segments.length !== 6) {
                sendJson(res, 404, { error: 'runtime_asset_manifest_not_found' });
                return;
            }
            const creatorUsername = decodeURIComponent(segments[3] ?? '');
            const appName = decodeURIComponent(segments[4] ?? '');
            if (!creatorUsername || !appName) {
                sendJson(res, 400, { error: 'invalid_runtime_asset_manifest_lookup' });
                return;
            }
            let mount;
            try {
                mount = findRouterMountByCreatorAndApp(creatorUsername, appName);
            }
            catch (error) {
                const message = error instanceof Error ? error.message : 'ambiguous_mount_lookup';
                sendJson(res, 409, { error: message });
                return;
            }
            if (!mount?.runtimeAssetManifest) {
                sendJson(res, 404, { error: 'runtime_asset_manifest_not_found' });
                return;
            }
            sendJson(res, 200, mount.runtimeAssetManifest.response);
            return;
        }
        if (pathname.startsWith(`${CONTROL_PREFIX}/apps/dev/`) && pathname.endsWith('/runtime-assets') && method === 'POST') {
            cleanupStaleRouterMounts();
            const segments = pathname.split('/').filter(Boolean);
            if (segments.length !== 6) {
                sendJson(res, 404, { error: 'runtime_asset_manifest_not_found' });
                return;
            }
            const creatorUsername = decodeURIComponent(segments[3] ?? '');
            const appName = decodeURIComponent(segments[4] ?? '');
            if (!creatorUsername || !appName) {
                sendJson(res, 400, { error: 'invalid_runtime_asset_manifest_lookup' });
                return;
            }
            let mount;
            try {
                mount = findRouterMountByCreatorAndApp(creatorUsername, appName);
            }
            catch (error) {
                const message = error instanceof Error ? error.message : 'ambiguous_mount_lookup';
                sendJson(res, 409, { error: message });
                return;
            }
            if (!mount) {
                sendJson(res, 404, { error: 'runtime_asset_manifest_not_found' });
                return;
            }
            const body = await readJsonBody(req);
            mount.runtimeAssetManifest = body.runtimeAssetManifest ?? null;
            mount.updatedAt = Date.now();
            sendJson(res, 200, { ok: true });
            return;
        }
        if (pathname === `${CONTROL_PREFIX}/mounts` && method === 'POST') {
            cleanupStaleRouterMounts();
            const body = await readJsonBody(req);
            const creatorUsername = body.creatorUsername?.trim();
            const appType = body.appType?.trim();
            const appName = body.appName?.trim();
            const htmlPath = body.htmlPath?.trim();
            const staticRoot = body.staticRoot?.trim();
            const repoRoot = body.repoRoot?.trim();
            const ownerPid = Number(body.ownerPid);
            if (!creatorUsername || !appType || !appName || !htmlPath || !staticRoot || !repoRoot || !Number.isInteger(ownerPid) || ownerPid <= 0) {
                sendJson(res, 400, { error: 'invalid_mount_request' });
                return;
            }
            const key = buildMountKey(creatorUsername, appType, appName);
            const existingMountId = routerMountIdsByKey.get(key);
            if (existingMountId) {
                const existingMount = routerMountsById.get(existingMountId);
                if (existingMount && isPidAlive(existingMount.ownerPid)) {
                    if (existingMount.htmlPath !== htmlPath || existingMount.staticRoot !== staticRoot) {
                        sendJson(res, 409, {
                            error: 'mount_conflict',
                            creatorUsername,
                            appType,
                            appName,
                            ownerPid: existingMount.ownerPid,
                            repoRoot: existingMount.repoRoot,
                            htmlPath: existingMount.htmlPath,
                        });
                        return;
                    }
                    existingMount.ownerPid = ownerPid;
                    existingMount.updatedAt = Date.now();
                    existingMount.runtimeAssetManifest = body.runtimeAssetManifest ?? null;
                    sendJson(res, 200, {
                        mountId: existingMount.id,
                        appUrl: buildLocalDevAppUrl({ creatorUsername, appType, appName, port: activePort }),
                        creatorUsername,
                        appType,
                        appName,
                    });
                    return;
                }
                removeRouterMount(existingMountId);
            }
            const mount = {
                id: (0, node_crypto_1.randomUUID)(),
                key,
                creatorUsername,
                appType,
                appName,
                htmlPath,
                staticRoot,
                repoRoot,
                ownerPid,
                updatedAt: Date.now(),
                runtimeAssetManifest: body.runtimeAssetManifest ?? null,
            };
            routerMountsById.set(mount.id, mount);
            routerMountIdsByKey.set(key, mount.id);
            sendJson(res, 200, {
                mountId: mount.id,
                appUrl: buildLocalDevAppUrl({ creatorUsername, appType, appName, port: activePort }),
                creatorUsername,
                appType,
                appName,
            });
            return;
        }
        if (pathname.startsWith(`${CONTROL_PREFIX}/mounts/`) && pathname.endsWith('/heartbeat') && method === 'POST') {
            cleanupStaleRouterMounts();
            const mountId = pathname.slice(`${CONTROL_PREFIX}/mounts/`.length, -'/heartbeat'.length).replace(/\/$/, '');
            const mount = routerMountsById.get(mountId);
            if (!mount) {
                sendJson(res, 404, { error: 'mount_not_found' });
                return;
            }
            const body = await readJsonBody(req);
            const ownerPid = Number(body.ownerPid);
            if (!Number.isInteger(ownerPid) || ownerPid <= 0) {
                sendJson(res, 400, { error: 'invalid_mount_owner_pid' });
                return;
            }
            if (ownerPid !== mount.ownerPid) {
                sendJson(res, 409, { error: 'mount_owner_pid_mismatch' });
                return;
            }
            mount.ownerPid = ownerPid;
            mount.updatedAt = Date.now();
            sendJson(res, 200, { ok: true });
            return;
        }
        if (pathname.startsWith(`${CONTROL_PREFIX}/mounts/`) && method === 'DELETE') {
            const mountId = pathname.slice(`${CONTROL_PREFIX}/mounts/`.length);
            removeRouterMount(mountId);
            sendJson(res, 200, { ok: true });
            return;
        }
        if (method !== 'GET' && method !== 'HEAD') {
            res.setHeader('Allow', 'GET, HEAD, POST, DELETE, OPTIONS');
            respondWithText(res, method, 405, 'Method not allowed');
            return;
        }
        cleanupStaleRouterMounts();
        const parsedMountPath = parseMountPath(pathname);
        if (!parsedMountPath) {
            respondWithText(res, method, 404, 'Not found');
            return;
        }
        const mountId = routerMountIdsByKey.get(parsedMountPath.key);
        const mount = mountId ? routerMountsById.get(mountId) : null;
        if (!mount) {
            respondWithText(res, method, 404, 'Not found');
            return;
        }
        if (parsedMountPath.assetPath === '_playdrop/runtime-assets') {
            if (!mount.runtimeAssetManifest) {
                sendJson(res, 500, { error: 'runtime_asset_manifest_not_registered' });
                return;
            }
            sendJson(res, 200, mount.runtimeAssetManifest.response);
            return;
        }
        if (parsedMountPath.assetPath.startsWith('_playdrop/runtime-assets/files/')) {
            const ownedAssetName = parsedMountPath.assetPath.slice('_playdrop/runtime-assets/files/'.length).trim();
            const role = (searchParams.get('role') || 'primary').trim().toLowerCase();
            if (!ownedAssetName || !role) {
                respondWithText(res, method, 400, 'Invalid runtime asset file request');
                return;
            }
            const ownedFile = getOwnedRuntimeAssetFile(mount, ownedAssetName, role);
            if (!ownedFile) {
                respondWithText(res, method, 404, 'Not found');
                return;
            }
            try {
                const stats = (0, node_fs_1.statSync)(ownedFile.absolutePath);
                if (!stats.isFile()) {
                    respondWithText(res, method, 404, 'Not found');
                    return;
                }
                const content = (0, node_fs_1.readFileSync)(ownedFile.absolutePath);
                respondWithBuffer(res, method, 200, ownedFile.contentType || resolveContentType(ownedFile.absolutePath), content);
            }
            catch {
                respondWithText(res, method, 404, 'Not found');
            }
            return;
        }
        if (parsedMountPath.assetPath === 'index.html') {
            try {
                const freshHtml = (0, node_fs_1.readFileSync)(mount.htmlPath);
                respondWithBuffer(res, method, 200, 'text/html; charset=utf-8', freshHtml);
            }
            catch (error) {
                respondWithText(res, method, 500, `Failed to read app file: ${error?.message || 'unknown error'}`);
            }
            return;
        }
        const absolutePath = resolveStaticPath(mount.staticRoot, parsedMountPath.assetPath);
        if (!absolutePath) {
            respondWithText(res, method, 400, 'Invalid static asset path');
            return;
        }
        try {
            const stats = (0, node_fs_1.statSync)(absolutePath);
            if (!stats.isFile()) {
                respondWithText(res, method, 404, 'Not found');
                return;
            }
            const content = (0, node_fs_1.readFileSync)(absolutePath);
            respondWithBuffer(res, method, 200, resolveContentType(absolutePath), content);
        }
        catch {
            respondWithText(res, method, 404, 'Not found');
        }
    };
    const server = node_http_1.default.createServer((req, res) => {
        void handleRequest(req, res);
    });
    server.on('listening', () => {
        const address = server.address();
        if (address && typeof address === 'object' && typeof address.port === 'number') {
            activePort = address.port;
        }
    });
    return server;
}
async function listenDevRouterServer(server, port = exports.DEV_ROUTER_PORT) {
    await new Promise((resolveListen, rejectListen) => {
        const onError = (error) => {
            server.off('listening', onListening);
            rejectListen(error);
        };
        const onListening = () => {
            server.off('error', onError);
            resolveListen();
        };
        server.once('error', onError);
        server.once('listening', onListening);
        server.listen(port, DEV_ROUTER_HOST);
    });
    const address = server.address();
    if (!address || typeof address !== 'object' || typeof address.port !== 'number') {
        throw new Error('dev_router_port_unavailable');
    }
    return address.port;
}
async function runDevRouterServer(port = Number(process.env.PLAYDROP_DEV_ROUTER_PORT) || exports.DEV_ROUTER_PORT) {
    const server = createDevRouterServer(port);
    await listenDevRouterServer(server, port);
    await new Promise(() => {
        // Keep the router alive until the process exits.
    });
}
