"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.startStaticPreviewServer = startStaticPreviewServer;
const child_process_1 = require("child_process");
const logger_1 = require("../utils/logger");
const log = (0, logger_1.createLogger)('static-preview-server');
/**
 * Spawn a minimal static file server bound to `rootPath` on the given port.
 *
 * Used by the agent-variants commit pipeline for the `static_preview` lane:
 * after committing, the materialized `index.html` (+ sibling assets) sits at
 * `destinationPath` with no Vite dev server backing it. This server fills that
 * gap so the iframe stays live and the visual editor has a real origin to
 * bind to. No HMR, no install — just a Node `http` server over the directory.
 *
 * The server runs as a child `node` process so the existing
 * `committedDevServer.proc: ChildProcess` lifecycle (stopDevServer,
 * lingeringCommittedDevServers, teardown) works unchanged.
 *
 * Resolves once the child reports `ready` on its IPC channel. Rejects if the
 * child exits or fails to come up within `timeoutMs`.
 */
async function startStaticPreviewServer(args) {
    const host = args.host ?? '127.0.0.1';
    const timeoutMs = args.timeoutMs ?? 10_000;
    const proc = (0, child_process_1.spawn)(process.execPath, ['-e', WORKER_SOURCE], {
        env: {
            ...process.env,
            STATIC_PREVIEW_ROOT: args.rootPath,
            STATIC_PREVIEW_PORT: String(args.port),
            STATIC_PREVIEW_HOST: host,
        },
        stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
        detached: false,
    });
    proc.stdout?.on('data', (data) => {
        log.debug(`[static-preview:${args.port}] ${data.toString().trim()}`);
    });
    proc.stderr?.on('data', (data) => {
        log.debug(`[static-preview:${args.port}] ${data.toString().trim()}`);
    });
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            cleanup();
            try {
                proc.kill('SIGTERM');
            }
            catch { /* ignore */ }
            reject(new Error(`Static preview server on port ${args.port} did not become ready within ${timeoutMs}ms`));
        }, timeoutMs);
        const onMessage = (msg) => {
            if (msg && typeof msg === 'object' && msg.type === 'ready') {
                cleanup();
                resolve(proc);
            }
            else if (msg && typeof msg === 'object' && msg.type === 'error') {
                const message = msg.message ?? 'unknown error';
                cleanup();
                try {
                    proc.kill('SIGTERM');
                }
                catch { /* ignore */ }
                reject(new Error(`Static preview server failed to start: ${message}`));
            }
        };
        const onExit = (code, signal) => {
            cleanup();
            reject(new Error(`Static preview server exited before ready (code=${code}, signal=${signal})`));
        };
        const onError = (err) => {
            cleanup();
            reject(err);
        };
        function cleanup() {
            clearTimeout(timer);
            proc.off('message', onMessage);
            proc.off('exit', onExit);
            proc.off('error', onError);
        }
        proc.on('message', onMessage);
        proc.on('exit', onExit);
        proc.on('error', onError);
    });
}
/**
 * Inline Node script for the child process. Kept here (rather than a sibling
 * worker file) so we don't need to track a separate `.js` artifact through the
 * build pipeline — the script is just a string passed via `node -e`.
 *
 * Serves files relative to STATIC_PREVIEW_ROOT. Directory requests fall
 * through to `index.html`. Path traversal outside the root is rejected with
 * 403. Missing files return 404. No caching — the iframe reload is the
 * refresh mechanism.
 */
const WORKER_SOURCE = `
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');

const root = path.resolve(process.env.STATIC_PREVIEW_ROOT || '.');
const port = Number(process.env.STATIC_PREVIEW_PORT || 0);
const host = process.env.STATIC_PREVIEW_HOST || '127.0.0.1';

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',
  '.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',
};

function send(res, status, body, headers) {
  res.writeHead(status, Object.assign({ 'Cache-Control': 'no-store' }, headers || {}));
  res.end(body);
}

function resolveTarget(reqPath) {
  const decoded = decodeURIComponent(reqPath || '/');
  const resolved = path.resolve(root, '.' + decoded);
  if (resolved !== root && !resolved.startsWith(root + path.sep)) return null;
  return resolved;
}

// Lexical containment isn't enough — fs.readFile follows symlinks. The
// committed project at destinationPath can legitimately contain files that
// the agent placed there, and a prompt-injected variant could plant a
// symlink (e.g. avatar.glb -> ~/.ssh/id_rsa) that the static server would
// then read and serve. After lexical resolve, realpath the target and
// re-check containment against the realpath'd root.
const realRoot = fs.realpathSync(root);

function isInsideRoot(realTarget) {
  return realTarget === realRoot || realTarget.startsWith(realRoot + path.sep);
}

const server = http.createServer((req, res) => {
  try {
    const parsed = url.parse(req.url || '/');
    let target = resolveTarget(parsed.pathname || '/');
    if (target === null) {
      send(res, 403, 'Forbidden');
      return;
    }
    fs.stat(target, (err, stat) => {
      if (err || !stat) {
        // Single-document fallback: every unknown route resolves to index.html
        // so client-side anchor routing works. Realpath-check the fallback
        // and apply the SAME hardlink check the main path uses — without
        // this, a hardlinked index.html that the main path would reject
        // could still be served by requesting any unknown URL.
        const fallback = path.join(root, 'index.html');
        fs.realpath(fallback, (rpErr, realFallback) => {
          if (rpErr || !isInsideRoot(realFallback)) {
            send(res, 404, 'Not found');
            return;
          }
          fs.stat(realFallback, (sErr, st) => {
            if (sErr || !st || st.nlink !== 1) {
              send(res, 404, 'Not found');
              return;
            }
            fs.readFile(realFallback, (fErr, data) => {
              if (fErr) { send(res, 404, 'Not found'); return; }
              send(res, 200, data, { 'Content-Type': MIME['.html'] });
            });
          });
        });
        return;
      }
      if (stat.isDirectory()) target = path.join(target, 'index.html');
      fs.realpath(target, (rpErr, realTarget) => {
        if (rpErr || !isInsideRoot(realTarget)) {
          send(res, 404, 'Not found');
          return;
        }
        // Hardlink defense: realpath collapses symlinks but not hardlinks.
        // An agent that planted a hardlink at commit time (e.g.
        // \`ln /etc/secret avatar.glb\`) would otherwise have it served
        // from the committed project. Reject multi-link files —
        // legitimate agent-generated assets have nlink === 1.
        fs.stat(realTarget, (sErr, st) => {
          if (sErr || !st || st.nlink !== 1) {
            send(res, 404, 'Not found');
            return;
          }
          fs.readFile(realTarget, (fErr, data) => {
            if (fErr) { send(res, 404, 'Not found'); return; }
            const ext = path.extname(realTarget).toLowerCase();
            send(res, 200, data, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
          });
        });
      });
    });
  } catch (e) {
    send(res, 500, 'Internal error');
  }
});

server.on('error', (err) => {
  try { process.send && process.send({ type: 'error', message: String(err && err.message || err) }); } catch (_) {}
  process.exit(1);
});

server.listen(port, host, () => {
  try { process.send && process.send({ type: 'ready', port: port, host: host }); } catch (_) {}
});

function shutdown() {
  try { server.close(() => process.exit(0)); } catch (_) { process.exit(0); }
  setTimeout(() => process.exit(0), 2000).unref();
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
`;
//# sourceMappingURL=StaticPreviewServer.js.map