"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
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.handleTraceProcessorProxyUpgrade = handleTraceProcessorProxyUpgrade;
const express_1 = __importStar(require("express"));
const net_1 = __importDefault(require("net"));
const config_1 = require("../config");
const auth_1 = require("../middleware/auth");
const traceProcessorService_1 = require("../services/traceProcessorService");
const traceProcessorLeaseStore_1 = require("../services/traceProcessorLeaseStore");
const traceProcessorSqlWorker_1 = require("../services/traceProcessorSqlWorker");
const rbac_1 = require("../services/rbac");
const enterpriseSsoService_1 = require("../services/enterpriseSsoService");
const enterpriseApiKeyService_1 = require("../services/enterpriseApiKeyService");
const router = (0, express_1.Router)();
const READY_STATES = new Set(['ready', 'idle', 'active']);
const CONFLICT_STATES = new Set(['draining', 'released', 'failed']);
const FRONTEND_VISIBILITIES = new Set(['visible', 'hidden', 'offline']);
const HOP_BY_HOP_HEADERS = new Set([
    'connection',
    'host',
    'keep-alive',
    'proxy-authenticate',
    'proxy-authorization',
    'te',
    'trailer',
    'transfer-encoding',
    'upgrade',
]);
class TraceProcessorProxyError extends Error {
    constructor(statusCode, message) {
        super(message);
        this.statusCode = statusCode;
        this.name = 'TraceProcessorProxyError';
    }
}
function sanitizeContextId(value) {
    if (typeof value !== 'string')
        return '';
    return value.trim().replace(/[^a-zA-Z0-9._:-]/g, '').slice(0, 128);
}
function getHeader(req, name) {
    const value = req.headers[name.toLowerCase()];
    if (Array.isArray(value))
        return value[0] || '';
    return typeof value === 'string' ? value : '';
}
function getFirstHeader(req, names) {
    for (const name of names) {
        const value = getHeader(req, name);
        if (value.trim())
            return value;
    }
    return '';
}
function parseHeaderList(req, names, fallback) {
    const raw = getFirstHeader(req, names);
    if (!raw.trim())
        return fallback;
    const parsed = raw
        .split(',')
        .map(value => sanitizeContextId(value))
        .filter(Boolean);
    return parsed.length > 0 ? parsed : fallback;
}
function trustedHeadersEnabled() {
    const value = process.env.SMARTPERFETTO_SSO_TRUSTED_HEADERS;
    return ['1', 'true', 'yes', 'on', 'enabled'].includes(String(value || '').trim().toLowerCase());
}
function defaultRolesForAuthType(authType) {
    return authType === 'dev' ? ['org_admin'] : ['analyst'];
}
function defaultScopesForAuthType(authType) {
    return authType === 'dev'
        ? ['*']
        : ['trace:read', 'trace:write', 'agent:run', 'report:read'];
}
function resolveTrustedSsoIdentity(req) {
    if (!trustedHeadersEnabled())
        return null;
    const userId = sanitizeContextId(getFirstHeader(req, [
        'x-smartperfetto-sso-user-id',
        'x-sso-user-id',
        'x-auth-request-user',
    ]));
    if (!userId)
        return null;
    return {
        userId,
        authType: 'sso',
        tenantId: sanitizeContextId(getFirstHeader(req, [
            'x-smartperfetto-sso-tenant-id',
            'x-sso-tenant-id',
            'x-tenant-id',
        ])) || undefined,
        workspaceId: sanitizeContextId(getFirstHeader(req, [
            'x-smartperfetto-sso-workspace-id',
            'x-sso-workspace-id',
            'x-workspace-id',
        ])) || undefined,
        roles: parseHeaderList(req, [
            'x-smartperfetto-sso-roles',
            'x-sso-roles',
        ], defaultRolesForAuthType('sso')),
        scopes: parseHeaderList(req, [
            'x-smartperfetto-sso-scopes',
            'x-sso-scopes',
        ], defaultScopesForAuthType('sso')),
    };
}
function queryValue(req, key) {
    const url = new URL(req.url || '/', 'http://127.0.0.1');
    return sanitizeContextId(url.searchParams.get(key) || '');
}
function contextFromIdentity(req, identity) {
    const authType = identity.authType;
    const requestId = sanitizeContextId(getHeader(req, 'x-request-id')) ||
        `ws-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
    const windowId = sanitizeContextId(getHeader(req, 'x-window-id')) ||
        queryValue(req, 'windowId') ||
        undefined;
    return {
        tenantId: identity.tenantId ||
            sanitizeContextId(getFirstHeader(req, ['x-tenant-id', 'x-sso-tenant-id'])) ||
            queryValue(req, 'tenantId') ||
            auth_1.DEFAULT_TENANT_ID,
        workspaceId: identity.workspaceId ||
            sanitizeContextId(getFirstHeader(req, ['x-workspace-id', 'x-sso-workspace-id'])) ||
            queryValue(req, 'workspaceId') ||
            auth_1.DEFAULT_WORKSPACE_ID,
        userId: identity.userId,
        authType,
        roles: identity.roles ?? defaultRolesForAuthType(authType),
        scopes: identity.scopes ?? defaultScopesForAuthType(authType),
        requestId,
        ...(windowId ? { windowId } : {}),
    };
}
function resolveUpgradeRequestContext(req) {
    const trustedIdentity = resolveTrustedSsoIdentity(req);
    if (trustedIdentity)
        return contextFromIdentity(req, trustedIdentity);
    try {
        const ssoIdentity = enterpriseSsoService_1.EnterpriseSsoService.getInstance()
            .resolveRequestIdentityFromRequest(req);
        if (ssoIdentity)
            return contextFromIdentity(req, ssoIdentity);
    }
    catch {
        // Fall through to API key or dev fallback.
    }
    try {
        const apiKeyIdentity = enterpriseApiKeyService_1.EnterpriseApiKeyService.getInstance()
            .resolveRequestIdentityFromRequest(req);
        if (apiKeyIdentity)
            return contextFromIdentity(req, apiKeyIdentity);
    }
    catch {
        // Fall through to dev fallback.
    }
    if (!(0, config_1.resolveFeatureConfig)().enterprise) {
        return contextFromIdentity(req, {
            userId: queryValue(req, 'userId') || auth_1.DEFAULT_DEV_USER_ID,
            authType: 'dev',
        });
    }
    return null;
}
function leaseScopeFromContext(context) {
    return {
        tenantId: context.tenantId,
        workspaceId: context.workspaceId,
        userId: context.userId,
    };
}
function frontendHolderForContext(context, metadata = {}, frontendVisibility) {
    const holderRef = context.windowId || context.requestId || context.userId;
    return {
        holderType: 'frontend_http_rpc',
        holderRef,
        windowId: context.windowId,
        ...(frontendVisibility ? { frontendVisibility } : {}),
        metadata: {
            requestId: context.requestId,
            proxy: 'trace_processor',
            ...metadata,
        },
    };
}
function parseFrontendVisibility(value) {
    if (value === undefined || value === null || value === '')
        return 'visible';
    if (typeof value !== 'string') {
        throw new TraceProcessorProxyError(400, 'frontend visibility must be visible, hidden, or offline');
    }
    const normalized = value.trim().toLowerCase();
    if (FRONTEND_VISIBILITIES.has(normalized)) {
        return normalized;
    }
    throw new TraceProcessorProxyError(400, 'frontend visibility must be visible, hidden, or offline');
}
function ensureTraceRead(context) {
    if (!(0, rbac_1.hasRbacPermission)(context, 'trace:read')) {
        throw new TraceProcessorProxyError(403, 'Trace processor proxy requires trace:read permission');
    }
}
function ensureRuntimeManage(context) {
    if (!(0, rbac_1.hasRbacPermission)(context, 'runtime:manage')) {
        throw new TraceProcessorProxyError(403, 'Trace processor lease admin requires runtime:manage permission');
    }
}
function leaseAdminReason(req) {
    const reason = typeof req.body?.reason === 'string' ? req.body.reason.trim() : '';
    return reason ? reason.slice(0, 500) : undefined;
}
async function resolveProxyTargetForContext(context, leaseId, holderMetadata = {}) {
    ensureTraceRead(context);
    const store = (0, traceProcessorLeaseStore_1.getTraceProcessorLeaseStore)();
    const scope = leaseScopeFromContext(context);
    let lease = store.getLeaseById(scope, leaseId);
    if (!lease) {
        throw new TraceProcessorProxyError(404, 'Trace processor lease not found');
    }
    if (CONFLICT_STATES.has(lease.state)) {
        throw new TraceProcessorProxyError(409, `Trace processor lease is ${lease.state}`);
    }
    lease = store.acquireHolderForLease(scope, lease.id, frontendHolderForContext(context, holderMetadata));
    if (!READY_STATES.has(lease.state)) {
        throw new TraceProcessorProxyError(503, `Trace processor lease is not ready (${lease.state})`);
    }
    const traceProcessorService = (0, traceProcessorService_1.getTraceProcessorService)();
    const trace = await traceProcessorService.getOrLoadTrace(lease.traceId);
    if (!trace) {
        throw new TraceProcessorProxyError(404, 'Trace not found for trace processor lease');
    }
    await traceProcessorService.ensureProcessorForLease(lease.traceId, lease.id, lease.mode, scope);
    const traceWithPort = traceProcessorService.getTraceWithLeasePort(lease.traceId, lease.id, lease.mode);
    if (!traceWithPort?.port) {
        throw new TraceProcessorProxyError(503, 'Trace processor HTTP RPC port is not ready');
    }
    return {
        lease,
        port: traceWithPort.port,
        scope,
    };
}
async function resolveProxyTarget(req, leaseId) {
    const context = (0, auth_1.requireRequestContext)(req);
    return resolveProxyTargetForContext(context, leaseId);
}
function copyUpstreamResponseHeaders(upstream, res) {
    for (const [name, value] of upstream.headers.entries()) {
        if (HOP_BY_HOP_HEADERS.has(name.toLowerCase()))
            continue;
        if (name.toLowerCase() === 'content-length')
            continue;
        res.setHeader(name, value);
    }
}
function requestBody(req) {
    if (Buffer.isBuffer(req.body))
        return req.body;
    if (req.body instanceof Uint8Array)
        return Buffer.from(req.body);
    return undefined;
}
function upstreamRequestHeaders(req, body) {
    const headers = {};
    const contentType = req.get('content-type');
    if (contentType)
        headers['content-type'] = contentType;
    const accept = req.get('accept');
    if (accept)
        headers.accept = accept;
    if (body)
        headers['content-length'] = String(body.length);
    return headers;
}
async function forwardHttpRpc(req, res, upstreamPath) {
    const leaseId = sanitizeContextId(req.params.leaseId);
    if (!leaseId) {
        res.status(400).json({ success: false, error: 'leaseId is required' });
        return;
    }
    const target = await resolveProxyTarget(req, leaseId);
    const body = requestBody(req);
    const upstream = await fetch(`http://127.0.0.1:${target.port}${upstreamPath}`, {
        method: 'POST',
        headers: upstreamRequestHeaders(req, body),
        ...(body ? { body } : {}),
    });
    const responseBody = Buffer.from(await upstream.arrayBuffer());
    copyUpstreamResponseHeaders(upstream, res);
    res.status(upstream.status).send(responseBody);
}
async function forwardQueryRpc(req, res) {
    const leaseId = sanitizeContextId(req.params.leaseId);
    if (!leaseId) {
        res.status(400).json({ success: false, error: 'leaseId is required' });
        return;
    }
    const body = requestBody(req);
    if (!body) {
        res.status(400).json({ success: false, error: 'query protobuf body is required' });
        return;
    }
    const priority = (0, traceProcessorSqlWorker_1.normalizeTraceProcessorQueryPriority)(req.get('x-smartperfetto-query-priority') || req.query.priority, 'p0');
    const target = await resolveProxyTargetForContext((0, auth_1.requireRequestContext)(req), leaseId, {
        lastQueryAt: Date.now(),
        queryPriority: priority,
    });
    const responseBody = await (0, traceProcessorService_1.getTraceProcessorService)().queryRaw(target.lease.traceId, body, {
        priority,
        leaseId: target.lease.id,
        leaseMode: target.lease.mode,
        leaseScope: target.scope,
    });
    res.setHeader('content-type', 'application/x-protobuf');
    res.status(200).send(responseBody);
}
async function heartbeatLease(req, res) {
    const leaseId = sanitizeContextId(req.params.leaseId);
    if (!leaseId) {
        res.status(400).json({ success: false, error: 'leaseId is required' });
        return;
    }
    const context = (0, auth_1.requireRequestContext)(req);
    ensureTraceRead(context);
    const visibility = parseFrontendVisibility(req.body?.visibility);
    const scope = leaseScopeFromContext(context);
    const store = (0, traceProcessorLeaseStore_1.getTraceProcessorLeaseStore)();
    let lease = store.getLeaseById(scope, leaseId);
    if (!lease) {
        throw new TraceProcessorProxyError(404, 'Trace processor lease not found');
    }
    if (CONFLICT_STATES.has(lease.state)) {
        throw new TraceProcessorProxyError(409, `Trace processor lease is ${lease.state}`);
    }
    const holder = frontendHolderForContext(context, {
        heartbeat: 'frontend',
        lastHeartbeatAt: Date.now(),
    }, visibility);
    try {
        lease = store.acquireHolderForLease(scope, lease.id, holder);
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        if (message.includes('not acquirable')) {
            throw new TraceProcessorProxyError(409, message);
        }
        if (message.includes('not found')) {
            throw new TraceProcessorProxyError(404, 'Trace processor lease not found');
        }
        throw error;
    }
    res.json({
        success: true,
        action: 'heartbeat',
        lease,
        holder: {
            holderType: holder.holderType,
            holderRef: holder.holderRef,
            windowId: holder.windowId ?? null,
            frontendVisibility: visibility,
        },
    });
}
async function drainLease(req, res) {
    const leaseId = sanitizeContextId(req.params.leaseId);
    if (!leaseId) {
        res.status(400).json({ success: false, error: 'leaseId is required' });
        return;
    }
    const context = (0, auth_1.requireRequestContext)(req);
    ensureRuntimeManage(context);
    const scope = leaseScopeFromContext(context);
    const store = (0, traceProcessorLeaseStore_1.getTraceProcessorLeaseStore)();
    const lease = store.getLeaseById(scope, leaseId);
    if (!lease) {
        throw new TraceProcessorProxyError(404, 'Trace processor lease not found');
    }
    if (lease.state === 'released' || lease.state === 'failed') {
        throw new TraceProcessorProxyError(409, `Trace processor lease is ${lease.state}`);
    }
    const drained = store.beginDraining(scope, lease.id);
    res.json({
        success: true,
        action: 'drain',
        reason: leaseAdminReason(req),
        lease: drained,
    });
}
async function restartLease(req, res) {
    const leaseId = sanitizeContextId(req.params.leaseId);
    if (!leaseId) {
        res.status(400).json({ success: false, error: 'leaseId is required' });
        return;
    }
    const context = (0, auth_1.requireRequestContext)(req);
    ensureRuntimeManage(context);
    const scope = leaseScopeFromContext(context);
    const store = (0, traceProcessorLeaseStore_1.getTraceProcessorLeaseStore)();
    const lease = store.getLeaseById(scope, leaseId);
    if (!lease) {
        throw new TraceProcessorProxyError(404, 'Trace processor lease not found');
    }
    if (CONFLICT_STATES.has(lease.state)) {
        throw new TraceProcessorProxyError(409, `Trace processor lease is ${lease.state}`);
    }
    const traceProcessorService = (0, traceProcessorService_1.getTraceProcessorService)();
    const trace = await traceProcessorService.getOrLoadTrace(lease.traceId);
    if (!trace) {
        throw new TraceProcessorProxyError(404, 'Trace not found for trace processor lease');
    }
    await traceProcessorService.restartLease(lease.traceId, lease.id, lease.mode, scope);
    const restarted = store.getLeaseById(scope, lease.id);
    res.json({
        success: true,
        action: 'restart',
        reason: leaseAdminReason(req),
        lease: restarted,
    });
}
function sendProxyError(res, error) {
    if (error instanceof TraceProcessorProxyError) {
        if (error.statusCode === 403) {
            (0, rbac_1.sendForbidden)(res, error.message);
            return;
        }
        res.status(error.statusCode).json({
            success: false,
            error: error.message,
        });
        return;
    }
    const message = error instanceof Error ? error.message : String(error);
    console.error('[TraceProcessorProxy] Proxy error:', message);
    res.status(502).json({
        success: false,
        error: 'Trace processor proxy failed',
        details: message,
    });
}
function writeUpgradeError(socket, statusCode, message) {
    if (!socket.writable)
        return;
    socket.write(`HTTP/1.1 ${statusCode} ${message}\r\n`
        + 'Connection: close\r\n'
        + 'Content-Type: text/plain; charset=utf-8\r\n'
        + `Content-Length: ${Buffer.byteLength(message)}\r\n`
        + '\r\n'
        + message);
    socket.end();
}
function websocketRequestHeaders(req, targetPort) {
    const headers = [
        `Host: 127.0.0.1:${targetPort}`,
        'Connection: Upgrade',
        'Upgrade: websocket',
    ];
    for (let i = 0; i < req.rawHeaders.length; i += 2) {
        const name = req.rawHeaders[i];
        const value = req.rawHeaders[i + 1];
        if (!name || value === undefined)
            continue;
        if (HOP_BY_HOP_HEADERS.has(name.toLowerCase()))
            continue;
        headers.push(`${name}: ${value}`);
    }
    return headers;
}
async function proxyWebSocket(req, socket, head, leaseId) {
    const context = resolveUpgradeRequestContext(req);
    if (!context) {
        throw new TraceProcessorProxyError(401, 'Trace processor WebSocket requires authentication');
    }
    const target = await resolveProxyTargetForContext(context, leaseId, {
        websocketConnectedAt: Date.now(),
    });
    const upstream = net_1.default.connect({
        host: '127.0.0.1',
        port: target.port,
    });
    upstream.once('connect', () => {
        const request = [
            'GET /websocket HTTP/1.1',
            ...websocketRequestHeaders(req, target.port),
            '',
            '',
        ].join('\r\n');
        upstream.write(request);
        if (head.length > 0)
            upstream.write(head);
        socket.pipe(upstream);
        upstream.pipe(socket);
    });
    upstream.once('error', (error) => {
        if (!socket.destroyed) {
            writeUpgradeError(socket, 502, `Trace processor WebSocket proxy failed: ${error.message}`);
        }
    });
    socket.once('error', () => upstream.destroy());
    socket.once('close', () => upstream.destroy());
    upstream.once('close', () => socket.destroy());
}
router.use(auth_1.authenticate);
router.post('/:leaseId/status', express_1.default.raw({ type: '*/*', limit: config_1.serverConfig.bodyLimit }), async (req, res) => {
    try {
        await forwardHttpRpc(req, res, '/status');
    }
    catch (error) {
        sendProxyError(res, error);
    }
});
router.post('/:leaseId/query', express_1.default.raw({ type: '*/*', limit: config_1.serverConfig.bodyLimit }), async (req, res) => {
    try {
        await forwardQueryRpc(req, res);
    }
    catch (error) {
        sendProxyError(res, error);
    }
});
router.post('/:leaseId/heartbeat', express_1.default.json({ limit: '32kb' }), async (req, res) => {
    try {
        await heartbeatLease(req, res);
    }
    catch (error) {
        sendProxyError(res, error);
    }
});
router.post('/:leaseId/drain', express_1.default.json({ limit: '32kb' }), async (req, res) => {
    try {
        await drainLease(req, res);
    }
    catch (error) {
        sendProxyError(res, error);
    }
});
router.post('/:leaseId/restart', express_1.default.json({ limit: '32kb' }), async (req, res) => {
    try {
        await restartLease(req, res);
    }
    catch (error) {
        sendProxyError(res, error);
    }
});
function handleTraceProcessorProxyUpgrade(req, socket, head) {
    const url = new URL(req.url || '/', 'http://127.0.0.1');
    const match = url.pathname.match(/^\/api\/tp\/([^/]+)\/websocket$/);
    if (!match)
        return false;
    const leaseId = sanitizeContextId(decodeURIComponent(match[1]));
    if (!leaseId) {
        writeUpgradeError(socket, 400, 'leaseId is required');
        return true;
    }
    void proxyWebSocket(req, socket, head, leaseId).catch((error) => {
        if (error instanceof TraceProcessorProxyError) {
            writeUpgradeError(socket, error.statusCode, error.message);
            return;
        }
        const message = error instanceof Error ? error.message : String(error);
        console.error('[TraceProcessorProxy] WebSocket proxy error:', message);
        writeUpgradeError(socket, 502, 'Trace processor WebSocket proxy failed');
    });
    return true;
}
exports.default = router;
//# sourceMappingURL=traceProcessorProxyRoutes.js.map