"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 __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkUsage = exports.attachRequestContext = exports.authenticate = exports.requireRequestContext = exports.getRequestContext = exports.DEFAULT_DEV_USER_ID = exports.DEFAULT_WORKSPACE_ID = exports.DEFAULT_TENANT_ID = void 0;
const crypto_1 = __importDefault(require("crypto"));
const config_1 = require("../config");
const enterpriseApiKeyService_1 = require("../services/enterpriseApiKeyService");
const enterpriseSsoService_1 = require("../services/enterpriseSsoService");
const API_KEY_ENV = 'SMARTPERFETTO_API_KEY';
const SSO_TRUSTED_HEADERS_ENV = 'SMARTPERFETTO_SSO_TRUSTED_HEADERS';
const SSO_SESSION_TOKEN_PREFIX = 'sp_sso_';
const SSO_SESSION_COOKIE_NAME = 'sp_sso_session';
exports.DEFAULT_TENANT_ID = 'default-dev-tenant';
exports.DEFAULT_WORKSPACE_ID = 'default-workspace';
exports.DEFAULT_DEV_USER_ID = 'dev-user-123';
const USAGE_WINDOW_MS = Number.parseInt(process.env.SMARTPERFETTO_USAGE_WINDOW_MS || '', 10) || 24 * 60 * 60 * 1000;
const MAX_REQUESTS = Number.parseInt(process.env.SMARTPERFETTO_USAGE_MAX_REQUESTS || '', 10);
const MAX_TRACE_REQUESTS = Number.parseInt(process.env.SMARTPERFETTO_USAGE_MAX_TRACE_REQUESTS || '', 10);
const usageTracker = new Map();
const getProvidedApiKey = (req) => {
    const authHeader = req.headers.authorization;
    if (authHeader && authHeader.startsWith('Bearer ')) {
        return authHeader.slice('Bearer '.length).trim();
    }
    const headerKey = req.headers['x-api-key'];
    if (typeof headerKey === 'string' && headerKey.trim().length > 0) {
        return headerKey.trim();
    }
    return undefined;
};
const requestHasSsoSessionCredential = (req) => {
    const authHeader = req.headers.authorization;
    if (typeof authHeader === 'string' && authHeader.startsWith('Bearer ')) {
        return authHeader.slice('Bearer '.length).trim().startsWith(SSO_SESSION_TOKEN_PREFIX);
    }
    return typeof req.headers.cookie === 'string'
        && req.headers.cookie.split(';').some((cookie) => {
            const [name, value = ''] = cookie.trim().split('=');
            return name === SSO_SESSION_COOKIE_NAME
                && decodeURIComponent(value).startsWith(SSO_SESSION_TOKEN_PREFIX);
        });
};
const safeEquals = (a, b) => {
    const aBuf = Buffer.from(a);
    const bBuf = Buffer.from(b);
    if (aBuf.length !== bBuf.length) {
        return false;
    }
    return crypto_1.default.timingSafeEqual(aBuf, bBuf);
};
const hashApiKey = (apiKey) => crypto_1.default.createHash('sha256').update(apiKey).digest('hex').slice(0, 8);
const truthyEnv = (value) => {
    if (!value)
        return false;
    return ['1', 'true', 'yes', 'on', 'enabled'].includes(value.trim().toLowerCase());
};
const sanitizeContextId = (value) => {
    if (typeof value !== 'string')
        return '';
    return value.trim().replace(/[^a-zA-Z0-9._:-]/g, '').slice(0, 128);
};
const sanitizeHeaderText = (value) => {
    if (typeof value !== 'string')
        return '';
    return value.trim().replace(/[\r\n]/g, '').slice(0, 320);
};
const getHeaderValue = (req, name) => {
    const value = req.headers[name.toLowerCase()];
    if (Array.isArray(value))
        return value[0] || '';
    return typeof value === 'string' ? value : '';
};
const getFirstHeaderValue = (req, names) => {
    for (const name of names) {
        const value = getHeaderValue(req, name);
        if (value.trim().length > 0)
            return value;
    }
    return '';
};
const parseHeaderList = (req, names, fallback) => {
    const raw = getFirstHeaderValue(req, names);
    if (!raw.trim())
        return fallback;
    const parsed = raw
        .split(',')
        .map(value => sanitizeContextId(value))
        .filter(Boolean);
    return parsed.length > 0 ? parsed : fallback;
};
const defaultRolesForAuthType = (authType) => authType === 'dev' ? ['org_admin'] : ['analyst'];
const defaultScopesForAuthType = (authType) => authType === 'dev'
    ? ['*']
    : ['trace:read', 'trace:write', 'agent:run', 'report:read'];
const buildRequestContext = (req, identity) => {
    const tenantId = identity.tenantId
        || sanitizeContextId(getFirstHeaderValue(req, ['x-tenant-id', 'x-sso-tenant-id']))
        || exports.DEFAULT_TENANT_ID;
    const workspaceId = identity.workspaceId
        || sanitizeContextId(getFirstHeaderValue(req, ['x-workspace-id', 'x-sso-workspace-id']))
        || exports.DEFAULT_WORKSPACE_ID;
    const requestId = sanitizeContextId(getHeaderValue(req, 'x-request-id')) ||
        `req-${Date.now()}-${crypto_1.default.randomBytes(4).toString('hex')}`;
    const windowId = sanitizeContextId(getHeaderValue(req, 'x-window-id')) || undefined;
    return {
        tenantId,
        workspaceId,
        userId: identity.userId,
        authType: identity.authType,
        roles: identity.roles ?? defaultRolesForAuthType(identity.authType),
        scopes: identity.scopes ?? defaultScopesForAuthType(identity.authType),
        requestId,
        ...(windowId ? { windowId } : {}),
    };
};
const makeDevIdentity = () => ({
    userId: exports.DEFAULT_DEV_USER_ID,
    email: 'dev@example.com',
    subscription: 'pro',
    authType: 'dev',
});
const makeApiKeyIdentity = (apiKey) => ({
    userId: `api-key-${hashApiKey(apiKey)}`,
    email: '',
    subscription: 'pro',
    authType: 'api_key',
});
const resolveTrustedSsoIdentity = (req) => {
    if (!truthyEnv(process.env[SSO_TRUSTED_HEADERS_ENV]))
        return null;
    const userId = sanitizeContextId(getFirstHeaderValue(req, [
        'x-smartperfetto-sso-user-id',
        'x-sso-user-id',
        'x-auth-request-user',
    ]));
    if (!userId)
        return null;
    return {
        userId,
        email: sanitizeHeaderText(getFirstHeaderValue(req, [
            'x-smartperfetto-sso-email',
            'x-sso-email',
            'x-auth-request-email',
        ])),
        subscription: 'enterprise',
        authType: 'sso',
        tenantId: sanitizeContextId(getFirstHeaderValue(req, [
            'x-smartperfetto-sso-tenant-id',
            'x-sso-tenant-id',
            'x-tenant-id',
        ])) || undefined,
        workspaceId: sanitizeContextId(getFirstHeaderValue(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')),
    };
};
const attachIdentity = (req, identity) => {
    req.user = {
        id: identity.userId,
        email: identity.email,
        subscription: identity.subscription,
    };
    req.requestContext = buildRequestContext(req, identity);
};
const sendUnauthorized = (res, details) => {
    const error = {
        error: 'Unauthorized',
        details,
    };
    res.status(401).json(error);
};
const getRequestContext = (req) => req.requestContext;
exports.getRequestContext = getRequestContext;
const requireRequestContext = (req) => {
    const context = (0, exports.getRequestContext)(req);
    if (!context) {
        throw new Error('RequestContext is missing. Did you forget to mount authenticate/attachRequestContext?');
    }
    return context;
};
exports.requireRequestContext = requireRequestContext;
/**
 * Authentication middleware - API key based (optional for dev)
 */
const authenticate = async (req, res, next) => {
    const ssoIdentity = resolveTrustedSsoIdentity(req);
    if (ssoIdentity) {
        attachIdentity(req, ssoIdentity);
        next();
        return;
    }
    if (requestHasSsoSessionCredential(req)) {
        try {
            const sessionIdentity = enterpriseSsoService_1.EnterpriseSsoService.getInstance().resolveRequestIdentityFromRequest(req);
            if (sessionIdentity) {
                attachIdentity(req, sessionIdentity);
                next();
                return;
            }
        }
        catch (error) {
            if ((0, config_1.resolveFeatureConfig)(process.env).enterprise) {
                sendUnauthorized(res, error instanceof Error ? error.message : 'Invalid SSO session');
                return;
            }
        }
    }
    if ((0, enterpriseApiKeyService_1.requestHasEnterpriseApiKeyCredential)(req)) {
        try {
            const apiKeyIdentity = enterpriseApiKeyService_1.EnterpriseApiKeyService.getInstance().resolveRequestIdentityFromRequest(req);
            if (apiKeyIdentity) {
                attachIdentity(req, apiKeyIdentity);
                next();
                return;
            }
            sendUnauthorized(res, 'Invalid or expired API key');
            return;
        }
        catch (error) {
            if ((0, config_1.resolveFeatureConfig)(process.env).enterprise) {
                sendUnauthorized(res, error instanceof Error ? error.message : 'Invalid or expired API key');
                return;
            }
        }
    }
    const configuredKey = process.env[API_KEY_ENV];
    if (!configuredKey) {
        if ((0, config_1.resolveFeatureConfig)(process.env).enterprise) {
            sendUnauthorized(res, 'Enterprise mode requires SSO or API key authentication');
            return;
        }
        attachIdentity(req, makeDevIdentity());
        next();
        return;
    }
    const providedKey = getProvidedApiKey(req);
    if (!providedKey || !safeEquals(providedKey, configuredKey)) {
        sendUnauthorized(res, 'Invalid or missing API key');
        return;
    }
    attachIdentity(req, makeApiKeyIdentity(providedKey));
    next();
};
exports.authenticate = authenticate;
exports.attachRequestContext = exports.authenticate;
/**
 * Usage check middleware - in-memory rate limiting (optional)
 */
const checkUsage = (isTraceAnalysis = false) => {
    return async (req, res, next) => {
        const hasTotalLimit = Number.isFinite(MAX_REQUESTS);
        const hasTraceLimit = Number.isFinite(MAX_TRACE_REQUESTS);
        if (!hasTotalLimit && !hasTraceLimit) {
            next();
            return;
        }
        const apiKey = getProvidedApiKey(req);
        const identity = req.user?.id
            || (apiKey ? `api-key-${hashApiKey(apiKey)}` : undefined)
            || req.ip
            || 'anonymous';
        const now = Date.now();
        const entry = usageTracker.get(identity);
        const record = entry && entry.resetAt > now
            ? entry
            : { resetAt: now + USAGE_WINDOW_MS, total: 0, trace: 0 };
        record.total += 1;
        if (isTraceAnalysis) {
            record.trace += 1;
        }
        usageTracker.set(identity, record);
        if (hasTotalLimit && record.total > MAX_REQUESTS) {
            const error = {
                error: 'Usage limit exceeded',
                details: `Exceeded max requests (${MAX_REQUESTS}) in current window`,
            };
            res.status(429).json(error);
            return;
        }
        if (isTraceAnalysis && hasTraceLimit && record.trace > MAX_TRACE_REQUESTS) {
            const error = {
                error: 'Trace analysis limit exceeded',
                details: `Exceeded max trace analyses (${MAX_TRACE_REQUESTS}) in current window`,
            };
            res.status(429).json(error);
            return;
        }
        next();
    };
};
exports.checkUsage = checkUsage;
//# sourceMappingURL=auth.js.map