// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
import { randomUUID } from 'node:crypto';
import { trace } from '@opentelemetry/api';
import { createLogger } from '../../common/logger.js';
import { getTraceContext, createTraceContext } from '../../common/trace.js';
const logger = createLogger({ component: 'api-security' });
const tracer = trace.getTracer('vorion-api-security');
/**
 * Default security headers configuration
 */
const DEFAULT_SECURITY_CONFIG = {
    cors: true,
    csp: false, // Disabled by default as it can break APIs
    hsts: { maxAge: 31536000, includeSubDomains: true },
    noSniff: true,
    frameOptions: 'DENY',
    xssProtection: true,
    referrerPolicy: 'strict-origin-when-cross-origin',
    permissionsPolicy: 'geolocation=(), microphone=(), camera=()',
};
/**
 * Default request logging configuration
 */
const DEFAULT_LOGGING_CONFIG = {
    logRequest: true,
    logResponse: true,
    logBody: false,
    logResponseBody: false,
    maxBodyLogSize: 10000,
    excludePaths: ['/health', '/ready', '/metrics'],
    excludeHeaders: ['authorization', 'cookie', 'x-api-key'],
    sensitiveFields: [
        'password',
        'secret',
        'token',
        'apiKey',
        'api_key',
        'accessToken',
        'access_token',
        'refreshToken',
        'refresh_token',
        'credential',
        'ssn',
        'socialSecurityNumber',
        'creditCard',
        'credit_card',
        'cardNumber',
        'card_number',
        'cvv',
        'pin',
        'privateKey',
        'private_key',
    ],
};
/**
 * Build CSP header value from config
 */
function buildCspHeader(config) {
    const directives = [];
    if (config.defaultSrc) {
        directives.push(`default-src ${config.defaultSrc.join(' ')}`);
    }
    if (config.scriptSrc) {
        directives.push(`script-src ${config.scriptSrc.join(' ')}`);
    }
    if (config.styleSrc) {
        directives.push(`style-src ${config.styleSrc.join(' ')}`);
    }
    if (config.imgSrc) {
        directives.push(`img-src ${config.imgSrc.join(' ')}`);
    }
    if (config.connectSrc) {
        directives.push(`connect-src ${config.connectSrc.join(' ')}`);
    }
    if (config.fontSrc) {
        directives.push(`font-src ${config.fontSrc.join(' ')}`);
    }
    if (config.objectSrc) {
        directives.push(`object-src ${config.objectSrc.join(' ')}`);
    }
    if (config.mediaSrc) {
        directives.push(`media-src ${config.mediaSrc.join(' ')}`);
    }
    if (config.frameSrc) {
        directives.push(`frame-src ${config.frameSrc.join(' ')}`);
    }
    if (config.childSrc) {
        directives.push(`child-src ${config.childSrc.join(' ')}`);
    }
    if (config.workerSrc) {
        directives.push(`worker-src ${config.workerSrc.join(' ')}`);
    }
    if (config.frameAncestors) {
        directives.push(`frame-ancestors ${config.frameAncestors.join(' ')}`);
    }
    if (config.formAction) {
        directives.push(`form-action ${config.formAction.join(' ')}`);
    }
    if (config.upgradeInsecureRequests) {
        directives.push('upgrade-insecure-requests');
    }
    if (config.blockAllMixedContent) {
        directives.push('block-all-mixed-content');
    }
    if (config.reportUri) {
        directives.push(`report-uri ${config.reportUri}`);
    }
    return directives.join('; ');
}
/**
 * Build HSTS header value from config
 */
function buildHstsHeader(config) {
    let header = `max-age=${config.maxAge}`;
    if (config.includeSubDomains) {
        header += '; includeSubDomains';
    }
    if (config.preload) {
        header += '; preload';
    }
    return header;
}
/**
 * Mask sensitive data in an object
 */
export function maskSensitiveData(obj, sensitiveFields = DEFAULT_LOGGING_CONFIG.sensitiveFields, maxDepth = 10) {
    if (maxDepth <= 0)
        return '[MAX_DEPTH_EXCEEDED]';
    if (obj === null || obj === undefined) {
        return obj;
    }
    if (typeof obj === 'string') {
        return obj;
    }
    if (Array.isArray(obj)) {
        return obj.map((item) => maskSensitiveData(item, sensitiveFields, maxDepth - 1));
    }
    if (typeof obj === 'object') {
        const masked = {};
        for (const [key, value] of Object.entries(obj)) {
            const lowerKey = key.toLowerCase();
            const isSensitive = sensitiveFields.some((field) => lowerKey.includes(field.toLowerCase()));
            if (isSensitive) {
                masked[key] = '[REDACTED]';
            }
            else if (typeof value === 'object' && value !== null) {
                masked[key] = maskSensitiveData(value, sensitiveFields, maxDepth - 1);
            }
            else {
                masked[key] = value;
            }
        }
        return masked;
    }
    return obj;
}
/**
 * Mask sensitive headers
 */
function maskHeaders(headers, excludeHeaders = DEFAULT_LOGGING_CONFIG.excludeHeaders) {
    const masked = {};
    for (const [key, value] of Object.entries(headers)) {
        const lowerKey = key.toLowerCase();
        if (excludeHeaders.some((h) => lowerKey.includes(h.toLowerCase()))) {
            masked[key] = '[REDACTED]';
        }
        else {
            masked[key] = value;
        }
    }
    return masked;
}
/**
 * Create security headers middleware
 *
 * @param config - Security headers configuration
 * @returns Fastify onRequest hook
 */
export function securityHeaders(config = {}) {
    const mergedConfig = { ...DEFAULT_SECURITY_CONFIG, ...config };
    return async (_request, reply) => {
        // HSTS
        if (mergedConfig.hsts) {
            const hstsConfig = typeof mergedConfig.hsts === 'boolean'
                ? { maxAge: 31536000, includeSubDomains: true }
                : mergedConfig.hsts;
            reply.header('Strict-Transport-Security', buildHstsHeader(hstsConfig));
        }
        // X-Content-Type-Options
        if (mergedConfig.noSniff) {
            reply.header('X-Content-Type-Options', 'nosniff');
        }
        // X-Frame-Options
        if (mergedConfig.frameOptions) {
            reply.header('X-Frame-Options', mergedConfig.frameOptions);
        }
        // X-XSS-Protection
        if (mergedConfig.xssProtection) {
            reply.header('X-XSS-Protection', '1; mode=block');
        }
        // Referrer-Policy
        if (mergedConfig.referrerPolicy) {
            reply.header('Referrer-Policy', mergedConfig.referrerPolicy);
        }
        // Permissions-Policy
        if (mergedConfig.permissionsPolicy) {
            reply.header('Permissions-Policy', mergedConfig.permissionsPolicy);
        }
        // CSP
        if (mergedConfig.csp) {
            const cspConfig = typeof mergedConfig.csp === 'boolean'
                ? { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'"] }
                : mergedConfig.csp;
            reply.header('Content-Security-Policy', buildCspHeader(cspConfig));
        }
        // Custom headers
        if (mergedConfig.customHeaders) {
            for (const [name, value] of Object.entries(mergedConfig.customHeaders)) {
                reply.header(name, value);
            }
        }
    };
}
/**
 * Create request ID injection middleware
 *
 * Injects a unique request ID into each request for tracing and debugging.
 *
 * @returns Fastify onRequest hook
 */
export function requestIdInjection() {
    return async (request, reply) => {
        // Use existing request ID or generate new one
        const incomingRequestId = request.headers['x-request-id'];
        const requestId = typeof incomingRequestId === 'string'
            ? incomingRequestId
            : randomUUID();
        // Store on request (Fastify's built-in id)
        request.id = requestId;
        // Set response header
        reply.header('X-Request-ID', requestId);
        // Get or create trace context
        let traceContext = getTraceContext();
        if (!traceContext) {
            const traceparent = request.headers['traceparent'];
            traceContext = createTraceContext(typeof traceparent === 'string' ? traceparent : undefined);
        }
        // Add trace ID to response
        reply.header('X-Trace-ID', traceContext.traceId);
        // Store trace context on request for later use
        request.traceContext = traceContext;
    };
}
/**
 * Create request logging middleware
 *
 * @param config - Logging configuration
 * @returns Object with onRequest and onResponse hooks
 */
export function requestLogging(config = {}) {
    const mergedConfig = { ...DEFAULT_LOGGING_CONFIG, ...config };
    const onRequest = async (request, _reply) => {
        // Skip excluded paths
        if (mergedConfig.excludePaths?.some((path) => request.url.startsWith(path))) {
            return;
        }
        if (!mergedConfig.logRequest) {
            return;
        }
        // Store start time
        request.startTime = process.hrtime.bigint();
        const logData = {
            requestId: request.id,
            method: request.method,
            url: request.url,
            headers: maskHeaders(request.headers, mergedConfig.excludeHeaders),
            ip: request.ip,
            userAgent: request.headers['user-agent'],
        };
        // Log body if enabled and exists
        if (mergedConfig.logBody && request.body) {
            const body = maskSensitiveData(request.body, mergedConfig.sensitiveFields);
            const bodyStr = JSON.stringify(body);
            if (bodyStr.length <= (mergedConfig.maxBodyLogSize ?? 10000)) {
                logData.body = body;
            }
            else {
                logData.body = '[BODY_TOO_LARGE]';
                logData.bodySize = bodyStr.length;
            }
        }
        // Get tenant ID if available
        const user = request.user;
        if (user?.tenantId) {
            logData.tenantId = user.tenantId;
        }
        if (user?.sub) {
            logData.userId = user.sub;
        }
        logger.info(logData, 'Incoming request');
    };
    const onResponse = async (request, reply) => {
        // Skip excluded paths
        if (mergedConfig.excludePaths?.some((path) => request.url.startsWith(path))) {
            return;
        }
        if (!mergedConfig.logResponse) {
            return;
        }
        const startTime = request.startTime;
        const durationMs = startTime
            ? Number(process.hrtime.bigint() - startTime) / 1_000_000
            : undefined;
        const logData = {
            requestId: request.id,
            method: request.method,
            url: request.url,
            statusCode: reply.statusCode,
            durationMs,
        };
        // Add trace ID
        const traceContext = request.traceContext;
        if (traceContext) {
            logData.traceId = traceContext.traceId;
        }
        // Get tenant ID if available
        const user = request.user;
        if (user?.tenantId) {
            logData.tenantId = user.tenantId;
        }
        // Log level based on status code
        if (reply.statusCode >= 500) {
            logger.error(logData, 'Request completed with server error');
        }
        else if (reply.statusCode >= 400) {
            logger.warn(logData, 'Request completed with client error');
        }
        else {
            logger.info(logData, 'Request completed');
        }
    };
    return { onRequest, onResponse };
}
/**
 * Combined security middleware that applies all security features
 *
 * @param options - Configuration options
 * @returns Fastify preHandler hook
 */
export function combinedSecurityMiddleware(options) {
    const headersMiddleware = securityHeaders(options?.headers);
    const requestIdMiddleware = requestIdInjection();
    return async (request, reply) => {
        await requestIdMiddleware(request, reply);
        await headersMiddleware(request, reply);
    };
}
/**
 * Register security middleware plugin for Fastify
 *
 * @param server - Fastify instance
 * @param options - Configuration options
 */
export async function registerSecurityPlugin(server, options) {
    const skipPaths = options?.skipPaths ?? ['/health', '/ready', '/metrics'];
    // Create middleware instances once
    const requestIdHook = requestIdInjection();
    const headersHook = securityHeaders(options?.headers);
    const loggingHooks = requestLogging(options?.logging);
    // Add request ID injection first
    server.addHook('onRequest', async (request, reply) => {
        if (skipPaths.some((path) => request.url.startsWith(path))) {
            return;
        }
        return requestIdHook(request, reply);
    });
    // Add security headers
    server.addHook('onRequest', async (request, reply) => {
        if (skipPaths.some((path) => request.url.startsWith(path))) {
            return;
        }
        return headersHook(request, reply);
    });
    // Add request logging
    server.addHook('onRequest', loggingHooks.onRequest);
    server.addHook('onResponse', loggingHooks.onResponse);
    // Decorate server with utility functions
    server.decorate('vorionMaskSensitiveData', maskSensitiveData);
    logger.info({ component: 'api-security' }, 'Security plugin registered');
}
/**
 * Create timing-safe string comparison to prevent timing attacks
 */
export function timingSafeEqual(a, b) {
    if (a.length !== b.length) {
        // Compare against itself to maintain constant time
        const dummy = Buffer.from(a);
        require('crypto').timingSafeEqual(dummy, dummy);
        return false;
    }
    const bufA = Buffer.from(a);
    const bufB = Buffer.from(b);
    return require('crypto').timingSafeEqual(bufA, bufB);
}
//# sourceMappingURL=security.js.map