"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.startServer = exports.isRivetCorsOriginAllowed = exports.isRivetUiStaticAssetPath = exports.isEmbeddedAgentUiHost = exports.resolveEmbeddedAgentPreviewOrigin = exports.resolveEmbeddedAgentUiOrigin = exports.resolveRivetListenHost = exports.sendInactiveVariantsEventStream = exports.injectRivetUiBootstrap = exports.DIST_UI_PATH = exports.RIVET_CORS_ALLOWED_HEADERS = exports.INACTIVE_VARIANTS_EVENT_HEARTBEAT_MS = void 0;
const Sentry = __importStar(require("@sentry/node"));
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const crypto_1 = require("crypto");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const index_core_1 = require("./index-core");
const _1 = require(".");
const components_1 = require("./routes/components");
const modifications_1 = require("./routes/modifications");
const controls_1 = require("./routes/controls");
const design_1 = require("./routes/design");
const WorktreeManager_1 = require("./services/WorktreeManager");
const git_1 = require("./routes/git");
const tokens_1 = require("./routes/tokens");
const support_1 = require("./routes/support");
const proxy_config_1 = require("./proxy-middleware/proxy-config");
const StaticFileService_1 = require("./services/StaticFileService");
const static_1 = require("./routes/static");
const ConfigManager_1 = require("./services/ConfigManager");
const accessTokenRefresh_1 = require("./services/accessTokenRefresh");
const evaluateFlags_1 = require("./config/evaluateFlags");
const FeatureFlagService_1 = require("./services/FeatureFlagService");
const DesktopVersionPolicyService_1 = require("./services/DesktopVersionPolicyService");
const featureFlagUserKey_1 = require("./config/featureFlagUserKey");
const mcp_1 = require("./routes/mcp");
const agentVariants_1 = require("./routes/agentVariants");
const demo_1 = require("./routes/demo");
const HostedDemoSessionService_1 = require("./services/HostedDemoSessionService");
const HostedDemoSessionStore_1 = require("./services/HostedDemoSessionStore");
const http_proxy_middleware_1 = require("http-proxy-middleware");
const HostedDemoAuthSessionService_1 = require("./services/HostedDemoAuthSessionService");
const HostedDemoAuthSessionStore_1 = require("./services/HostedDemoAuthSessionStore");
const shouldRecordHostedDemoSessionAction_1 = require("./utils/shouldRecordHostedDemoSessionAction");
const hostedDemoSessionAuthRefresh_1 = require("./services/hostedDemoSessionAuthRefresh");
const RequestAuthContext_1 = require("./services/RequestAuthContext");
const queueAccess_1 = require("./utils/queueAccess");
const log = (0, index_core_1.createLogger)('RivetServer');
const QUEUE_ACCESS_TOKEN_BYTES = 32;
const RIVET_BOOTSTRAP_SCRIPT_ID = 'rivet-bootstrap-config';
const EMBEDDED_AGENT_LISTEN_HOST = '127.0.0.1';
const EMBEDDED_AGENT_PREVIEW_HOST = 'localhost';
const OPAQUE_PREVIEW_ORIGIN = 'null';
exports.INACTIVE_VARIANTS_EVENT_HEARTBEAT_MS = 30_000;
exports.RIVET_CORS_ALLOWED_HEADERS = [
    'Content-Type',
    'Authorization',
    'x-rivet-queue-token',
];
/**
 * Resolve UI dist path for both development and packaged (desktop app) environments
 */
const resolveUiPath = () => {
    // Bundled CLI path (esbuild bundle with ui/ alongside bundle file)
    const bundledPath = path_1.default.join(__dirname, 'ui/dist');
    // Production path (desktop app packages CLI with ../ui/dist structure)
    const productionPath = path_1.default.join(__dirname, '../ui/dist');
    // Development path (normal repo structure)
    const devPath = path_1.default.join(__dirname, '../src/ui/dist');
    // Check bundled path first (for esbuild bundled CLI)
    if (fs_1.default.existsSync(bundledPath)) {
        log.info(`Using bundled UI path: ${bundledPath}`);
        return bundledPath;
    }
    // Check production path (for electron-packaged app)
    if (fs_1.default.existsSync(productionPath)) {
        log.info(`Using production UI path: ${productionPath}`);
        return productionPath;
    }
    // Fall back to development path
    log.info(`Using development UI path: ${devPath}`);
    return devPath;
};
exports.DIST_UI_PATH = resolveUiPath();
const setNoStoreHeaders = (res) => {
    res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
    res.setHeader('Pragma', 'no-cache');
    res.setHeader('Expires', '0');
};
const escapeScriptJson = (value) => JSON.stringify(value)
    .replace(/</g, '\\u003c')
    .replace(/\u2028/g, '\\u2028')
    .replace(/\u2029/g, '\\u2029');
const injectRivetUiBootstrap = (html, bootstrap) => {
    const script = `<script id="${RIVET_BOOTSTRAP_SCRIPT_ID}">window.__RIVET_BOOTSTRAP__=${escapeScriptJson(bootstrap)};</script>`;
    if (html.includes('</head>')) {
        return html.replace('</head>', `${script}</head>`);
    }
    return `${script}${html}`;
};
exports.injectRivetUiBootstrap = injectRivetUiBootstrap;
/**
 * Keeps the inactive variants SSE endpoint well-formed for clients that always subscribe.
 */
const sendInactiveVariantsEventStream = (req, res) => {
    res.set({
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache, no-transform',
        Connection: 'keep-alive',
        'X-Accel-Buffering': 'no',
    });
    res.flushHeaders?.();
    res.write('data: {"active":false}\n\n');
    const heartbeat = setInterval(() => {
        res.write(': heartbeat\n\n');
    }, exports.INACTIVE_VARIANTS_EVENT_HEARTBEAT_MS);
    req.on('close', () => {
        clearInterval(heartbeat);
    });
};
exports.sendInactiveVariantsEventStream = sendInactiveVariantsEventStream;
/**
 * Returns the host interface Rivet should bind for the current runtime.
 */
const resolveRivetListenHost = (agentApplyMode) => agentApplyMode === 'embedded' ? EMBEDDED_AGENT_LISTEN_HOST : undefined;
exports.resolveRivetListenHost = resolveRivetListenHost;
const resolveEmbeddedAgentUiOrigin = (port) => `http://${EMBEDDED_AGENT_LISTEN_HOST}:${port}`;
exports.resolveEmbeddedAgentUiOrigin = resolveEmbeddedAgentUiOrigin;
const resolveEmbeddedAgentPreviewOrigin = (port) => `http://${EMBEDDED_AGENT_PREVIEW_HOST}:${port}`;
exports.resolveEmbeddedAgentPreviewOrigin = resolveEmbeddedAgentPreviewOrigin;
const isEmbeddedAgentUiHost = (hostname, agentApplyMode) => agentApplyMode !== 'embedded' || hostname === EMBEDDED_AGENT_LISTEN_HOST;
exports.isEmbeddedAgentUiHost = isEmbeddedAgentUiHost;
const isRivetUiStaticAssetPath = (requestPath) => requestPath === '/rivet/favicon.ico' ||
    requestPath.startsWith('/rivet/assets/') ||
    requestPath.startsWith('/rivet/fonts/');
exports.isRivetUiStaticAssetPath = isRivetUiStaticAssetPath;
/**
 * Allows browser requests from the Rivet UI origin and the opaque sandbox
 * origin used by embedded agent previews when loading preview assets.
 */
const isRivetCorsOriginAllowed = ({ requestOrigin, requestHostname, requestPath, allowedOrigins, agentApplyMode, }) => {
    if (!requestOrigin) {
        return true;
    }
    if (allowedOrigins.has(requestOrigin)) {
        return true;
    }
    return (agentApplyMode === 'embedded' &&
        requestHostname === EMBEDDED_AGENT_PREVIEW_HOST &&
        !requestPath.startsWith('/api/') &&
        !requestPath.startsWith('/rivet') &&
        requestOrigin === OPAQUE_PREVIEW_ORIGIN);
};
exports.isRivetCorsOriginAllowed = isRivetCorsOriginAllowed;
/**
 * Path to the dev-mode port marker. Written by the Rivet backend after listen()
 * succeeds and consumed by `src/ui/vite.config.ts` so that `yarn dev` in the UI
 * always proxies `/api/*` to the right backend port — DEFAULT_PORT (4000) is
 * just a starting point; the backend may bind on any port via findAvailablePort
 * when 4000 is taken (e.g. by `rivet-web`), and Vite needs to track that.
 *
 * Located at the repo root in dev mode (`__dirname` is `<repo>/src`). In bundled
 * / packaged installs `__dirname` lives elsewhere; the write is best-effort and
 * its failure is non-fatal because Vite dev mode is not used in those installs.
 */
const DEV_PORT_FILENAME = '.rivet-dev-port';
const resolveDevPortFile = () => path_1.default.join(__dirname, '..', DEV_PORT_FILENAME);
const writeDevPortFile = (port) => {
    const filePath = resolveDevPortFile();
    const payload = JSON.stringify({
        port,
        pid: process.pid,
        writtenAt: new Date().toISOString(),
    });
    try {
        fs_1.default.writeFileSync(filePath, payload, 'utf8');
        log.debug(`Wrote dev-port marker at ${filePath} (port ${port})`);
    }
    catch (err) {
        log.debug(`Failed to write dev-port marker at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
    }
};
const removeDevPortFile = () => {
    // Only remove if our pid wrote it — avoids clobbering a newer backend that
    // already took over the marker after we started shutting down.
    const filePath = resolveDevPortFile();
    try {
        const existing = fs_1.default.readFileSync(filePath, 'utf8');
        const parsed = JSON.parse(existing);
        if (parsed.pid === process.pid) {
            fs_1.default.unlinkSync(filePath);
            log.debug(`Removed dev-port marker at ${filePath}`);
        }
    }
    catch {
        // File missing or unreadable — nothing to clean up.
    }
};
const startServer = async (options) => {
    const { userPort, userHost, telemetry, projectPath, framework, staticEntry, styleFramework, tailwindConfig, sessionBridge, mcpEditor, agentApplyMode, agentRunController, skipProcessHandlers, } = options;
    const userPortWithFallback = userPort ?? _1.DEFAULT_USER_PORT;
    const userHostWithFallback = userHost ?? 'localhost';
    const isDemoModeEnabled = options.demoMode?.enabled ?? false;
    const requiresDemoAuth = options.demoMode?.requireSignedInUsers ?? true;
    const isGitEnabled = options.isGitEnabled ?? !isDemoModeEnabled;
    const queueAccessToken = sessionBridge && agentApplyMode === 'embedded'
        ? (0, crypto_1.randomBytes)(QUEUE_ACCESS_TOKEN_BYTES).toString('base64url')
        : null;
    const rivetUiIndexHtml = fs_1.default.readFileSync(path_1.default.join(exports.DIST_UI_PATH, 'index.html'), 'utf8');
    if (process.env.SENTRY_DSN) {
        Sentry.init({
            dsn: process.env.SENTRY_DSN,
            environment: process.env.NODE_ENV ?? 'development',
        });
    }
    const sessionService = new index_core_1.SessionService(projectPath, isGitEnabled);
    // Initialize git session
    try {
        await sessionService.initializeSession();
        log.info('Git session initialized successfully');
    }
    catch (error) {
        log.warn('Failed to initialize git session, continuing without git tracking:', error);
    }
    // Initialize static file service for static projects
    let staticService = null;
    if (framework === 'static') {
        const entryPoint = staticEntry || 'index.html';
        staticService = new StaticFileService_1.StaticFileService(projectPath, entryPoint);
    }
    // Setup cleanup on process exit — skip when embedded in another process (e.g. MCP server)
    if (!skipProcessHandlers) {
        const shutdownAndExit = async (code) => {
            log.info('Server shutting down, cleaning up...');
            (0, WorktreeManager_1.cleanupOrphanedWorktrees)();
            const results = await Promise.allSettled([
                telemetry?.shutdown(),
                (0, FeatureFlagService_1.getFeatureFlagService)().shutdown(),
            ]);
            const labels = ['telemetry', 'feature flags'];
            for (let i = 0; i < results.length; i++) {
                if (results[i].status === 'rejected') {
                    log.error(`Failed to cleanup ${labels[i]}:`, results[i].reason);
                }
            }
            process.exit(code);
        };
        process.on('SIGINT', () => shutdownAndExit(0));
        process.on('SIGTERM', () => shutdownAndExit(0));
        process.on('uncaughtException', (err) => {
            log.error('Uncaught exception:', err);
            telemetry?.captureException(err, 'uncaught_exception');
            shutdownAndExit(1);
        });
        process.on('unhandledRejection', (reason) => {
            log.error('Unhandled rejection:', reason);
            telemetry?.captureException(reason, 'unhandled_rejection');
            shutdownAndExit(1);
        });
    }
    const listenPort = options.rivetPort ?? _1.DEFAULT_PORT;
    const app = (0, express_1.default)();
    const corsOrigins = new Set(agentApplyMode === 'embedded'
        ? [(0, exports.resolveEmbeddedAgentUiOrigin)(listenPort)]
        : [`http://localhost:${listenPort}`, `http://127.0.0.1:${listenPort}`]);
    const rawCorsOrigins = process.env.RIVET_CORS_ORIGINS;
    if (rawCorsOrigins) {
        rawCorsOrigins
            .split(',')
            .map((origin) => origin.trim())
            .filter((origin) => origin.length > 0)
            .forEach((origin) => corsOrigins.add(origin));
    }
    if (isDemoModeEnabled) {
        corsOrigins.add('https://rivet.design');
        corsOrigins.add('https://demo.rivet.design');
    }
    // CORS configuration for local development
    app.use((req, res, next) => {
        (0, cors_1.default)({
            origin: (origin, callback) => {
                callback(null, (0, exports.isRivetCorsOriginAllowed)({
                    requestOrigin: origin,
                    requestHostname: req.hostname,
                    requestPath: req.path,
                    allowedOrigins: corsOrigins,
                    agentApplyMode,
                }));
            },
            credentials: true,
            methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
            allowedHeaders: exports.RIVET_CORS_ALLOWED_HEADERS,
        })(req, res, next);
    });
    const jsonBodyParser = express_1.default.json({ limit: '50mb' });
    app.use((req, res, next) => {
        if (req.originalUrl.startsWith('/try/')) {
            next();
            return;
        }
        jsonBodyParser(req, res, next);
    });
    // Request logging middleware
    app.use((req, res, next) => {
        log.debug(`${req.method} ${req.originalUrl}`);
        next();
    });
    const uiStaticOptions = {
        setHeaders: (res, filePath) => {
            // index.html is the entry doc that references hashed bundles. It must
            // re-fetch on every load so a rebuilt SPA (new bundle hash) actually
            // reaches the user — caching the HTML pins them to the old hash forever
            // (observed in Arc Boost: even hard-refresh keeps serving stale HTML
            // until the cache TTL expires). Hashed assets under /assets/* are
            // content-addressed and safe to long-cache.
            if (filePath.endsWith('.html')) {
                setNoStoreHeaders(res);
            }
            else if (process.env.NODE_ENV !== 'production') {
                // Dev: defeat caching for everything so HMR-like behavior is reliable
                // without rebuilds bumping a hash.
                setNoStoreHeaders(res);
            }
            else {
                // Production hashed assets are content-addressed (`main-<hash>.js`),
                // so long-cache with immutable is correct and shaves the conditional
                // GETs off every navigation.
                res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
            }
        },
    };
    const sendRivetUiIndex = (req, res) => {
        if (!(0, exports.isEmbeddedAgentUiHost)(req.hostname, agentApplyMode)) {
            res.redirect(302, `${(0, exports.resolveEmbeddedAgentUiOrigin)(listenPort)}${req.originalUrl}`);
            return;
        }
        setNoStoreHeaders(res);
        res
            .type('html')
            .send((0, exports.injectRivetUiBootstrap)(rivetUiIndexHtml, {
            agentApplyMode: agentApplyMode ?? null,
            queueAccessToken,
        }));
    };
    // Serve hashed UI assets from root so both / and /rivet entrypoints can load them.
    app.use('/assets', express_1.default.static(path_1.default.join(exports.DIST_UI_PATH, 'assets'), uiStaticOptions));
    app.use('/rivet/assets', express_1.default.static(path_1.default.join(exports.DIST_UI_PATH, 'assets'), uiStaticOptions));
    // Serve public font files used by auth and hosted demo UI.
    app.use('/fonts', express_1.default.static(path_1.default.join(exports.DIST_UI_PATH, 'fonts'), uiStaticOptions));
    app.use('/rivet/fonts', express_1.default.static(path_1.default.join(exports.DIST_UI_PATH, 'fonts'), uiStaticOptions));
    // Serve favicon at root for hosted/static deployments.
    app.get('/favicon.ico', (_req, res) => {
        res.sendFile('favicon.ico', { root: exports.DIST_UI_PATH });
    });
    app.get('/rivet/favicon.ico', (_req, res) => {
        res.sendFile('favicon.ico', { root: exports.DIST_UI_PATH });
    });
    // Serve Rivet UI at /rivet for non-static modes.
    if (framework !== 'static') {
        app.get(/^\/rivet(?:\/.*)?$/, (req, res, next) => {
            if ((0, exports.isRivetUiStaticAssetPath)(req.path)) {
                next();
                return;
            }
            sendRivetUiIndex(req, res);
        });
    }
    log.info(`Serving UI assets from: ${exports.DIST_UI_PATH}`);
    // Static mode - use root entrypoint for Rivet UI (no dev server to proxy to)
    if (framework === 'static') {
        app.get('/', (req, res) => {
            log.debug(`${framework} mode - serving Rivet at root`);
            sendRivetUiIndex(req, res);
        });
    }
    // For web frameworks, root route falls through to catch-all proxy (no special handling needed)
    const getBearerTokenFromHeader = (authorizationHeader) => {
        if (typeof authorizationHeader !== 'string') {
            return undefined;
        }
        if (!authorizationHeader.startsWith('Bearer ')) {
            return undefined;
        }
        const token = authorizationHeader.slice('Bearer '.length).trim();
        return token.length > 0 ? token : undefined;
    };
    /**
     * @effect Hydrates request auth context from Authorization header for all modes.
     * @deps Runs per request and reads req.headers.authorization.
     */
    app.use((req, _res, next) => {
        const headerToken = getBearerTokenFromHeader(req.headers.authorization);
        if (!headerToken) {
            next();
            return;
        }
        (0, RequestAuthContext_1.runWithRequestAuthContext)({ token: headerToken }, () => {
            next();
        });
    });
    const demoAuthSessionService = isDemoModeEnabled
        ? new HostedDemoAuthSessionService_1.HostedDemoAuthSessionService({
            store: options.demoMode?.authRedisUrl
                ? new HostedDemoAuthSessionStore_1.RedisHostedDemoAuthSessionStore({
                    redisUrl: options.demoMode.authRedisUrl,
                    keyPrefix: options.demoMode.authRedisKeyPrefix ??
                        'rivet:hosted-demo:auth-session',
                })
                : undefined,
        })
        : null;
    if (demoAuthSessionService) {
        await demoAuthSessionService.initialize();
    }
    if (demoAuthSessionService) {
        app.use(async (req, res, next) => {
            try {
                const sessionId = demoAuthSessionService.getOrCreateSessionId(req, res);
                const auth = await (0, hostedDemoSessionAuthRefresh_1.resolveHostedDemoSessionAuthWithRefresh)({
                    demoAuthSessionService,
                    sessionId,
                    res,
                    getProxyUrl: () => (0, ConfigManager_1.getConfigManager)().getProxyUrl(),
                });
                const headerToken = getBearerTokenFromHeader(req.headers.authorization);
                res.locals.demoAuthSessionId = sessionId;
                (0, RequestAuthContext_1.runWithRequestAuthContext)({
                    token: auth?.token ?? headerToken,
                    refreshToken: auth?.refreshToken,
                    email: auth?.email,
                }, () => {
                    next();
                });
            }
            catch (error) {
                next(error);
            }
        });
    }
    const isDemoRequestAuthenticated = (req) => {
        if (!isDemoModeEnabled || !requiresDemoAuth) {
            return true;
        }
        return Boolean(demoAuthSessionService?.getAuthForRequest(req));
    };
    const getDemoRequestOwnerUserId = (req) => {
        if (!requiresDemoAuth) {
            return null;
        }
        const cookieAuthEmail = demoAuthSessionService
            ?.getAuthForRequest(req)
            ?.email?.trim()
            .toLowerCase();
        if (cookieAuthEmail) {
            return cookieAuthEmail;
        }
        const email = (0, ConfigManager_1.getConfigManager)().getEmail();
        if (!email) {
            return null;
        }
        return email.trim().toLowerCase();
    };
    const isDemoAuthExemptApiPath = (pathName) => {
        if (pathName === '/config' ||
            pathName === '/health' ||
            pathName === '/auth/store') {
            return true;
        }
        if (pathName === '/demo/session' || pathName.startsWith('/demo/session/')) {
            return true;
        }
        return false;
    };
    const isHostedRootApiPath = (pathName) => {
        if (pathName === '/config' ||
            pathName === '/health' ||
            pathName === '/auth/store' ||
            pathName === '/support') {
            return true;
        }
        if (pathName === '/demo/session' || pathName.startsWith('/demo/session/')) {
            return true;
        }
        return false;
    };
    let demoSessionService = null;
    if (isDemoModeEnabled &&
        options.demoMode?.allowSessionProvisioning !== false) {
        const sessionMetadataRedisUrl = options.demoMode?.sessionMetadataRedisUrl ??
            options.demoMode?.authRedisUrl;
        const sessionMetadataStore = sessionMetadataRedisUrl
            ? new HostedDemoSessionStore_1.RedisHostedDemoSessionStore({
                redisUrl: sessionMetadataRedisUrl,
                keyPrefix: options.demoMode?.sessionMetadataRedisKeyPrefix ??
                    'rivet:hosted-demo:demo-session',
            })
            : undefined;
        demoSessionService = new HostedDemoSessionService_1.HostedDemoSessionService({
            repoPath: projectPath,
            telemetry,
            templateProjectPath: options.demoMode?.templateProjectPath ?? 'examples/microsoft-paint',
            worktreesRootPath: options.demoMode?.worktreesRootPath,
            sessionTtlMs: options.demoMode?.sessionTtlMs,
            sessionIdleTtlMs: options.demoMode?.sessionIdleTtlMs,
            maxActiveSessions: options.demoMode?.maxActiveSessions,
            baseRivetPort: options.demoMode?.baseRivetPort,
            publicBaseUrl: options.demoMode?.publicBaseUrl,
            sessionMetadataStore,
        });
        await demoSessionService.initialize();
    }
    if (isDemoModeEnabled && requiresDemoAuth) {
        app.use('/api', (req, res, next) => {
            if (isDemoAuthExemptApiPath(req.path)) {
                next();
                return;
            }
            if (!isDemoRequestAuthenticated(req)) {
                res.status(401).json({ error: 'Authentication required' });
                return;
            }
            next();
        });
    }
    if (demoSessionService) {
        const activeSessionApiProxy = (0, http_proxy_middleware_1.createProxyMiddleware)({
            changeOrigin: true,
            pathRewrite: (pathName) => `/api${pathName}`,
            on: {
                proxyReq: (proxyReq, req) => {
                    (0, http_proxy_middleware_1.fixRequestBody)(proxyReq, req);
                },
            },
            router: (req) => {
                const activeDemoSessionId = demoAuthSessionService?.getActiveDemoSessionIdForRequest(req) ?? null;
                if (!activeDemoSessionId) {
                    return undefined;
                }
                return (demoSessionService.getSessionProxyTarget(activeDemoSessionId) ??
                    undefined);
            },
        });
        app.use('/api', (req, res, next) => {
            if (isHostedRootApiPath(req.path)) {
                next();
                return;
            }
            const activeDemoSessionId = demoAuthSessionService?.getActiveDemoSessionIdForRequest(req) ?? null;
            if (!activeDemoSessionId) {
                res.status(409).json({
                    error: 'No active demo session. Call /api/demo/session first.',
                });
                return;
            }
            const sessionOwnerUserId = demoSessionService.getSessionOwnerUserId(activeDemoSessionId);
            const requestOwnerUserId = getDemoRequestOwnerUserId(req);
            if (requiresDemoAuth &&
                sessionOwnerUserId &&
                requestOwnerUserId &&
                sessionOwnerUserId !== requestOwnerUserId) {
                res.status(403).json({ error: 'Session access denied' });
                return;
            }
            const auth = demoAuthSessionService?.getAuthForRequest(req);
            if (auth?.token) {
                req.headers.authorization = `Bearer ${auth.token}`;
            }
            void (async () => {
                try {
                    await demoSessionService.ensureRuntimeReady(activeDemoSessionId);
                }
                catch (error) {
                    const message = error instanceof Error ? error.message : '';
                    if (message === 'Session not found') {
                        demoAuthSessionService?.clearActiveDemoSessionIdForRequest(req, res);
                        if (!res.headersSent) {
                            res.status(409).json({
                                error: 'No active demo session. Call /api/demo/session first.',
                            });
                        }
                        return;
                    }
                    log.warn('Hosted demo runtime rehydration failed for active session API', error);
                    if (!res.headersSent) {
                        res.status(503).json({
                            error: 'Demo session runtime unavailable',
                        });
                    }
                    return;
                }
                const target = demoSessionService.getSessionProxyTarget(activeDemoSessionId);
                if (!target) {
                    demoAuthSessionService?.clearActiveDemoSessionIdForRequest(req, res);
                    if (!res.headersSent) {
                        res.status(409).json({
                            error: 'No active demo session. Call /api/demo/session first.',
                        });
                    }
                    return;
                }
                if ((0, shouldRecordHostedDemoSessionAction_1.shouldRecordHostedDemoSessionAction)(req)) {
                    try {
                        await demoSessionService.recordSessionAction(activeDemoSessionId);
                    }
                    catch (error) {
                        log.warn('Failed to persist hosted demo session action timestamp', error);
                    }
                }
                activeSessionApiProxy(req, res, next);
            })();
        });
    }
    // API routes for Rivet functionality
    app.use('/api', (0, components_1.createComponentRouter)(projectPath, telemetry));
    app.use('/api', (0, modifications_1.createModificationRouter)(projectPath, telemetry));
    app.use('/api', (0, design_1.createDesignRouter)(projectPath));
    app.use('/api', (0, controls_1.createControlsRouter)());
    void (0, WorktreeManager_1.pruneWorktrees)(projectPath); // prune stale registry entries from prior crashes
    if (!isGitEnabled) {
        log.info('Git routes disabled for this server instance');
    }
    app.use('/api', (0, git_1.createGitRouter)(sessionService, projectPath, { isGitEnabled }));
    let trySessionProxy = null;
    if (demoSessionService) {
        app.use('/api', (0, demo_1.createDemoRouter)(demoSessionService, {
            requireSignedInUsers: requiresDemoAuth,
            getActiveDemoSessionIdForRequest: demoAuthSessionService
                ? (req) => demoAuthSessionService.getActiveDemoSessionIdForRequest(req)
                : undefined,
            storeActiveDemoSessionIdForRequest: demoAuthSessionService
                ? (req, res, demoSessionId) => demoAuthSessionService.storeActiveDemoSessionIdForRequest(req, res, demoSessionId)
                : undefined,
            clearActiveDemoSessionIdForRequest: demoAuthSessionService
                ? (req, res) => demoAuthSessionService.clearActiveDemoSessionIdForRequest(req, res)
                : undefined,
            telemetry,
            deploymentEnv: process.env.NODE_ENV ?? 'development',
        }));
        const mountedTrySessionProxy = (0, http_proxy_middleware_1.createProxyMiddleware)({
            changeOrigin: true,
            ws: true,
            router: (req) => {
                const sessionIdParam = req.params.sessionId;
                const sessionId = Array.isArray(sessionIdParam)
                    ? sessionIdParam[0]
                    : sessionIdParam;
                return demoSessionService.getSessionProxyTarget(sessionId) ?? undefined;
            },
        });
        trySessionProxy = mountedTrySessionProxy;
        // Browsers resolve default favicon relative to the document URL under /try/:id/,
        // so requests hit this path instead of /. Serve the Rivet UI favicon here too.
        app.get('/try/:sessionId/favicon.ico', (_req, res) => {
            res.sendFile('favicon.ico', { root: exports.DIST_UI_PATH });
        });
        app.use('/try/:sessionId', (req, res, next) => {
            if (!isDemoRequestAuthenticated(req)) {
                if (req.accepts('html')) {
                    res.redirect('/');
                    return;
                }
                res.status(401).json({ error: 'Authentication required' });
                return;
            }
            const sessionIdParam = req.params.sessionId;
            const sessionId = Array.isArray(sessionIdParam)
                ? sessionIdParam[0]
                : sessionIdParam;
            const sessionOwnerUserId = demoSessionService.getSessionOwnerUserId(sessionId);
            const requestOwnerUserId = getDemoRequestOwnerUserId(req);
            if (requiresDemoAuth &&
                sessionOwnerUserId &&
                requestOwnerUserId &&
                sessionOwnerUserId !== requestOwnerUserId) {
                res.status(403).json({ error: 'Session access denied' });
                return;
            }
            const auth = demoAuthSessionService?.getAuthForRequest(req);
            if (auth?.token) {
                req.headers.authorization = `Bearer ${auth.token}`;
            }
            void (async () => {
                try {
                    await demoSessionService.ensureRuntimeReady(sessionId);
                }
                catch (error) {
                    const message = error instanceof Error ? error.message : '';
                    if (message === 'Session not found') {
                        demoAuthSessionService?.clearActiveDemoSessionIdForRequest(req, res);
                        telemetry?.trackTryoutProxySessionMissing({
                            demoSessionId: sessionId,
                            deploymentEnv: process.env.NODE_ENV ?? 'development',
                        });
                        if (!res.headersSent) {
                            res.status(404).json({ error: 'Session not found' });
                        }
                        return;
                    }
                    log.warn('Hosted demo runtime rehydration failed for try session', error);
                    if (!res.headersSent) {
                        res.status(503).json({
                            error: 'Demo session runtime unavailable',
                        });
                    }
                    return;
                }
                const target = demoSessionService.getSessionProxyTarget(sessionId);
                if (!target) {
                    demoAuthSessionService?.clearActiveDemoSessionIdForRequest(req, res);
                    telemetry?.trackTryoutProxySessionMissing({
                        demoSessionId: sessionId,
                        deploymentEnv: process.env.NODE_ENV ?? 'development',
                    });
                    if (!res.headersSent) {
                        res.status(404).json({ error: 'Session not found' });
                    }
                    return;
                }
                if ((0, shouldRecordHostedDemoSessionAction_1.shouldRecordHostedDemoSessionAction)(req)) {
                    try {
                        await demoSessionService.recordSessionAction(sessionId);
                    }
                    catch (error) {
                        log.warn('Failed to persist hosted demo session action timestamp', error);
                    }
                }
                mountedTrySessionProxy(req, res, next);
            })();
        });
    }
    app.use('/api', (0, tokens_1.createTokenRouter)(projectPath, styleFramework, tailwindConfig));
    app.use('/api', (0, support_1.createSupportRouter)(telemetry));
    // Static routes (only for static projects)
    if (staticService) {
        app.use((0, static_1.createStaticRoutes)(staticService, projectPath, {
            isPreviewBridgeEnabled: agentApplyMode === 'embedded',
            expectedPreviewParentOrigin: agentApplyMode === 'embedded'
                ? (0, exports.resolveEmbeddedAgentUiOrigin)(listenPort)
                : undefined,
        }));
        log.info('Static routes mounted at /static');
    }
    // Health check endpoint
    app.get('/api/health', (req, res) => {
        res.json({
            status: 'healthy',
            timestamp: new Date().toISOString(),
            framework,
            ports: {
                rivet: listenPort,
                userDevServer: userPortWithFallback,
            },
        });
    });
    app.post('/api/agent/stop', (req, res) => {
        if (agentApplyMode !== 'embedded' || !agentRunController) {
            return res.status(404).json({ error: 'No embedded agent run active' });
        }
        if (!(0, queueAccess_1.isQueueAccessAuthorized)({
            queueAccessToken,
            getHeader: (name) => req.header(name),
        })) {
            return res.status(403).json({
                error: 'Queue access denied',
                reason: 'queue_token_required',
            });
        }
        const result = agentRunController.cancelActiveRun();
        return res.json({ success: true, ...result });
    });
    // Config endpoint
    app.get('/api/config', async (req, res) => {
        const configManager = (0, ConfigManager_1.getConfigManager)();
        const demoSessionId = res.locals.demoAuthSessionId;
        const demoAuth = demoAuthSessionService?.getAuthForSessionId(demoSessionId ?? '');
        const isAuthenticatedInDemoMode = Boolean(demoAuth?.token);
        const isAuthenticated = demoAuthSessionService
            ? isAuthenticatedInDemoMode
            : configManager.isAuthenticated();
        const email = demoAuthSessionService
            ? demoAuth?.email
            : configManager.getEmail();
        const userId = demoAuthSessionService
            ? demoAuth?.userId
            : configManager.getUserId();
        // Display name lives in the access token's user_metadata (Google OAuth).
        // Decode the demo session's token directly; otherwise let ConfigManager
        // read it off the locally stored token. May be undefined (email-only
        // logins) — the UI falls back to email-derived initials.
        const name = demoAuthSessionService
            ? demoAuth?.token
                ? ((0, accessTokenRefresh_1.getJwtDisplayName)(demoAuth.token) ?? undefined)
                : undefined
            : configManager.getDisplayName();
        const featureFlagUserKey = (0, featureFlagUserKey_1.getConfigFeatureFlagUserKey)({
            headers: req.headers,
            fallbackEmail: email,
        });
        const activeDemoSessionId = demoAuth?.activeDemoSessionId ?? null;
        const activeSession = activeDemoSessionId
            ? demoSessionService?.getSession(activeDemoSessionId)
            : null;
        const [featureFlags, desktopVersionPolicy] = await Promise.all([
            (0, evaluateFlags_1.getFeatureFlags)({ userKey: featureFlagUserKey }),
            (0, DesktopVersionPolicyService_1.getDesktopVersionPolicyService)().getPolicy(),
        ]);
        // In the zero-to-one flow the outer `projectPath` arg points at the
        // generated subproject (`<workspaceRoot>/.rivet/<slug>/`), but the
        // bridge tracks the workspaceRoot itself — which is where
        // `.rivet/variants/` and per-project comments live. Prefer the bridge's
        // view so the UI's projectPathAtom resolves to the directory that owns
        // shared state, not the subproject of a single session.
        const livePath = activeSession?.projectPath ??
            sessionBridge?.getProjectPath() ??
            projectPath;
        res.json({
            agentModeAvailable: isAuthenticated,
            email,
            userId,
            name,
            activeDemoSessionId,
            isMCPSession: sessionBridge?.isActive() ?? false,
            mcpEditor: mcpEditor ?? null,
            agentApplyMode: sessionBridge?.isActive()
                ? (agentApplyMode ?? 'external')
                : null,
            featureFlags,
            projectPath: livePath,
            demoMode: isDemoModeEnabled,
            desktopVersionPolicy,
        });
    });
    // Store auth credentials after browser-based OAuth completion.
    // Hosted demo mode stores auth by browser session; non-demo keeps local config behavior.
    app.post('/api/auth/store', (req, res) => {
        const { email, token, refreshToken, userId } = req.body ?? {};
        if (!email || !token) {
            if (demoAuthSessionService) {
                telemetry?.trackTryoutAuthStoreFailed({
                    reason: 'missing_email_or_token',
                    statusCode: 400,
                    deploymentEnv: process.env.NODE_ENV ?? 'development',
                });
            }
            return res.status(400).json({ error: 'Missing email or token' });
        }
        try {
            if (demoAuthSessionService) {
                const sessionId = res.locals.demoAuthSessionId;
                const demoSessionId = sessionId
                    ? demoAuthSessionService.storeAuthForSessionId(sessionId, res, {
                        token,
                        email,
                        refreshToken,
                        userId,
                    })
                    : demoAuthSessionService.storeAuthForRequest(req, res, {
                        token,
                        email,
                        refreshToken,
                        userId,
                    });
                telemetry?.trackTryoutAuthStoreCompleted({
                    demoSessionId,
                    hasRefreshToken: Boolean(refreshToken),
                    deploymentEnv: process.env.NODE_ENV ?? 'development',
                });
            }
            else {
                const configManager = (0, ConfigManager_1.getConfigManager)();
                configManager.setAuth(token, email, refreshToken, userId);
            }
        }
        catch (error) {
            if (demoAuthSessionService) {
                telemetry?.trackTryoutAuthStoreFailed({
                    reason: error instanceof Error ? error.message : 'auth_store_failed',
                    statusCode: 500,
                    deploymentEnv: process.env.NODE_ENV ?? 'development',
                });
            }
            return res
                .status(500)
                .json({ error: 'Failed to store auth credentials' });
        }
        telemetry?.identifyUser();
        log.info(`Stored auth for ${email} via browser OAuth`);
        res.json({ success: true });
    });
    app.post('/api/auth/logout', (req, res) => {
        try {
            if (demoAuthSessionService) {
                demoAuthSessionService.clearAuthForRequest(req, res);
            }
            else {
                (0, ConfigManager_1.getConfigManager)().clearAuth();
            }
        }
        catch (error) {
            log.warn('Logout failed', error);
            return res.status(500).json({ error: 'Failed to log out' });
        }
        log.info('Cleared auth via logout');
        res.json({ success: true });
    });
    // MCP bridge routes (only when session bridge is provided)
    if (sessionBridge) {
        app.use('/api/mcp', (0, mcp_1.createMCPRouter)(sessionBridge, options.telemetry, options.agentVariantsOrchestrator, { queueAccessToken: queueAccessToken ?? undefined }));
        log.info('MCP bridge routes mounted at /api/mcp');
    }
    // Agent-driven variants — Express routes back the iframe chip (cycle pills,
    // commit, cancel) and the proxy retarget on pill click.
    if (options.agentVariantsOrchestrator) {
        app.use('/api', (0, agentVariants_1.createAgentVariantsRouter)(options.agentVariantsOrchestrator, sessionBridge, telemetry));
        log.info('agent-variants routes mounted at /api/variants');
    }
    else {
        app.get('/api/variants/active', (_req, res) => {
            res.json({ active: false });
        });
        app.get('/api/variants/events', (req, res) => {
            (0, exports.sendInactiveVariantsEventStream)(req, res);
        });
    }
    // Proxy all other routes to user's dev server (catch-all)
    // This must come AFTER all Rivet-specific routes
    // Skip proxy setup for static apps
    let userDevProxy = null;
    if (framework !== 'static') {
        userDevProxy = (0, proxy_config_1.createUserDevServerProxy)(userPortWithFallback, userHostWithFallback, {
            isPreviewBridgeEnabled: agentApplyMode === 'embedded',
            expectedPreviewParentOrigin: agentApplyMode === 'embedded'
                ? (0, exports.resolveEmbeddedAgentUiOrigin)(listenPort)
                : undefined,
        });
        // Inject the variant-target setter into the bridge so MCP-driven variant
        // sessions can swap upstreams without re-creating the proxy.
        sessionBridge?.registerVariantTargetSetter(userDevProxy.setVariantTarget);
        // Use a more explicit catch-all that works with Express
        app.get('/{*any}', userDevProxy.middleware);
        app.post('/{*any}', userDevProxy.middleware);
        app.put('/{*any}', userDevProxy.middleware);
        app.delete('/{*any}', userDevProxy.middleware);
        app.patch('/{*any}', userDevProxy.middleware);
        log.info(`Proxying all unmatched routes to user dev server on ${userHostWithFallback}:${userPortWithFallback}`);
    }
    else {
        log.info(`${framework} mode - skipping proxy setup`);
    }
    // Sentry must be registered before any other error handler — Express runs error handlers
    // in order, and our handler below never calls next(err), so Sentry would never see errors
    // if registered after it.
    if (process.env.SENTRY_DSN) {
        Sentry.setupExpressErrorHandler(app);
    }
    // Global error handler — catches errors passed via next(err) in route handlers
    app.use((err, req, res, _next) => {
        const requestAborted = err.code === 'ECONNABORTED' ||
            err.type === 'request.aborted';
        if (requestAborted) {
            log.debug('Request aborted by client');
            return;
        }
        log.error('Unhandled route error:', err);
        telemetry?.captureException(err, 'express_error_handler');
        if (!res.headersSent) {
            res.status(500).json({
                error: 'Internal server error',
                details: err.message,
            });
        }
    });
    // Start server
    const activeSockets = new Set();
    const httpServer = await new Promise((resolve, reject) => {
        const listenHost = (0, exports.resolveRivetListenHost)(agentApplyMode);
        const server = listenHost
            ? app.listen(listenPort, listenHost, () => {
                log.info(`🎨 Rivet server running on http://${listenHost}:${listenPort}`);
                writeDevPortFile(listenPort);
                resolve(server);
            })
            : app.listen(listenPort, () => {
                log.info(`🎨 Rivet server running on http://localhost:${listenPort}`);
                writeDevPortFile(listenPort);
                resolve(server);
            });
        server.on('error', (err) => {
            reject(err);
        });
        // Track connections so we can destroy them on close (prevents keep-alive hang)
        server.on('connection', (socket) => {
            activeSockets.add(socket);
            socket.on('close', () => activeSockets.delete(socket));
        });
        // Handle WebSocket upgrade requests
        server.on('upgrade', (req, socket, head) => {
            const pathname = req.url?.split('?')[0] ?? '';
            log.debug(`WebSocket upgrade request for: ${pathname}`);
            const trySessionMatch = /^\/try\/([^/]+)/.exec(pathname);
            if (trySessionMatch && trySessionProxy?.upgrade && demoSessionService) {
                const sessionId = trySessionMatch[1];
                void (async () => {
                    try {
                        await demoSessionService.ensureRuntimeReady(sessionId);
                    }
                    catch {
                        socket.destroy();
                        return;
                    }
                    const target = demoSessionService.getSessionProxyTarget(sessionId);
                    if (!target) {
                        socket.destroy();
                        return;
                    }
                    if (requiresDemoAuth) {
                        const auth = demoAuthSessionService?.getAuthForRequest(req);
                        if (!auth?.token) {
                            log.debug('Rejecting unauthenticated hosted demo upgrade request');
                            socket.destroy();
                            return;
                        }
                        const sessionOwnerUserId = demoSessionService.getSessionOwnerUserId(sessionId);
                        const requestOwnerUserId = auth.email?.trim().toLowerCase() ?? null;
                        if (sessionOwnerUserId &&
                            requestOwnerUserId &&
                            sessionOwnerUserId !== requestOwnerUserId) {
                            log.debug('Rejecting non-owner hosted demo upgrade request');
                            socket.destroy();
                            return;
                        }
                    }
                    log.debug('Proxying upgrade to hosted demo session for HMR');
                    trySessionProxy.upgrade(req, socket, head);
                })();
                return;
            }
            // Proxy to user's dev server for HMR
            const proxyUpgrade = userDevProxy?.middleware?.upgrade;
            if (framework !== 'static' && proxyUpgrade) {
                log.debug('Proxying upgrade to user dev server for HMR');
                proxyUpgrade(req, socket, head);
                return;
            }
            // No handler found - destroy socket
            log.warn(`No handler for WebSocket upgrade: ${pathname}`);
            socket.destroy();
        });
    });
    return {
        close: () => new Promise((resolve, reject) => {
            // Clean up the dev-port marker eagerly so a stale port doesn't outlive
            // a graceful shutdown. We re-check pid ownership inside removeDevPortFile.
            removeDevPortFile();
            httpServer.close((err) => {
                if (err) {
                    reject(err);
                    return;
                }
                void (async () => {
                    if (demoSessionService) {
                        await demoSessionService.shutdown();
                    }
                    demoAuthSessionService?.shutdown();
                    if (telemetry) {
                        await telemetry.shutdown();
                    }
                    resolve();
                })();
            });
            // Destroy all active connections so close() resolves immediately
            for (const socket of activeSockets) {
                socket.destroy();
            }
        }),
    };
};
exports.startServer = startServer;
//# sourceMappingURL=server.js.map