// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
/**
 * Server-Side Fingerprint Service
 *
 * Computes and validates server-side fingerprints from request headers
 * for session binding and anomaly detection.
 *
 * Features:
 * - Deterministic fingerprint generation from request headers
 * - SHA-256 hashing for compact, collision-resistant fingerprints
 * - Configurable strictness (warn vs block)
 * - Configurable components for fingerprint computation
 *
 * @packageDocumentation
 */
import { createHash } from 'node:crypto';
import { createLogger } from '../common/logger.js';
import { getConfig } from '../common/config.js';
const logger = createLogger({ component: 'fingerprint-service' });
const DEFAULT_CONFIG = {
    enabled: true,
    strictness: 'warn',
    components: ['userAgent', 'acceptLanguage'],
};
/**
 * Server-side fingerprint service for session binding
 */
export class FingerprintService {
    config;
    constructor(config = {}) {
        this.config = { ...DEFAULT_CONFIG, ...config };
        logger.debug('Fingerprint service initialized', {
            enabled: this.config.enabled,
            strictness: this.config.strictness,
            components: this.config.components,
        });
    }
    /**
     * Compute a fingerprint from request headers
     *
     * @param headers - Request headers to fingerprint
     * @returns Fingerprint result with hash and metadata
     */
    computeFingerprint(headers) {
        const componentsUsed = [];
        const missingComponents = [];
        const parts = [];
        // Build fingerprint from configured components
        for (const component of this.config.components) {
            const value = this.getHeaderValue(headers, component);
            if (value !== undefined && value !== '') {
                componentsUsed.push(component);
                parts.push(`${component}:${value}`);
            }
            else {
                missingComponents.push(component);
            }
        }
        // Create deterministic fingerprint string
        const fingerprintData = parts.sort().join('|');
        // Hash with SHA-256 for compact, collision-resistant output
        const fingerprint = createHash('sha256')
            .update(fingerprintData)
            .digest('hex');
        logger.debug('Fingerprint computed', {
            fingerprintPrefix: fingerprint.substring(0, 8),
            componentsUsed,
            missingComponents,
        });
        return {
            fingerprint,
            componentsUsed,
            missingComponents,
        };
    }
    /**
     * Validate a fingerprint against a stored fingerprint
     *
     * @param headers - Current request headers
     * @param storedFingerprint - Previously stored fingerprint from session
     * @param sessionId - Session ID for logging
     * @returns Validation result
     */
    validateFingerprint(headers, storedFingerprint, sessionId) {
        if (!this.config.enabled) {
            return {
                valid: true,
                shouldBlock: false,
                reason: 'Fingerprint validation disabled',
            };
        }
        const current = this.computeFingerprint(headers);
        // Exact match
        if (current.fingerprint === storedFingerprint) {
            logger.debug('Fingerprint validated successfully', {
                sessionId,
                fingerprintPrefix: current.fingerprint.substring(0, 8),
            });
            return {
                valid: true,
                shouldBlock: false,
                currentFingerprint: current.fingerprint,
                storedFingerprint,
            };
        }
        // Mismatch detected
        const changedComponents = this.detectChangedComponents(headers, storedFingerprint);
        const result = {
            valid: false,
            reason: 'Fingerprint mismatch detected',
            shouldBlock: this.config.strictness === 'block',
            currentFingerprint: current.fingerprint,
            storedFingerprint,
            changedComponents,
        };
        // Log based on strictness
        if (this.config.strictness === 'block') {
            logger.warn({
                sessionId,
                currentFingerprintPrefix: current.fingerprint.substring(0, 8),
                storedFingerprintPrefix: storedFingerprint.substring(0, 8),
                changedComponents,
            }, 'Fingerprint mismatch - blocking request');
        }
        else {
            logger.info({
                sessionId,
                currentFingerprintPrefix: current.fingerprint.substring(0, 8),
                storedFingerprintPrefix: storedFingerprint.substring(0, 8),
                changedComponents,
            }, 'Fingerprint mismatch - warning only');
        }
        return result;
    }
    /**
     * Check if fingerprint validation is enabled
     */
    isEnabled() {
        return this.config.enabled;
    }
    /**
     * Get the current strictness setting
     */
    getStrictness() {
        return this.config.strictness;
    }
    /**
     * Get the configured components
     */
    getComponents() {
        return [...this.config.components];
    }
    /**
     * Extract a header value by component name
     */
    getHeaderValue(headers, component) {
        switch (component) {
            case 'userAgent':
                return headers.userAgent;
            case 'acceptLanguage':
                return headers.acceptLanguage;
            case 'acceptEncoding':
                return headers.acceptEncoding;
            case 'dnt':
                return headers.dnt;
            case 'accept':
                return headers.accept;
            case 'secChUa':
                return headers.secChUa;
            case 'secChUaMobile':
                return headers.secChUaMobile;
            case 'secChUaPlatform':
                return headers.secChUaPlatform;
            default:
                logger.warn({ component }, 'Unknown fingerprint component');
                return undefined;
        }
    }
    /**
     * Attempt to detect which components changed
     * This is approximate as we can't reverse the hash
     */
    detectChangedComponents(_headers, _storedFingerprint) {
        // Since fingerprints are hashed, we can't determine exact changes
        // without storing individual component hashes
        // For now, return all configured components as potentially changed
        return [...this.config.components];
    }
}
// Singleton instance
let fingerprintService = null;
/**
 * Get the fingerprint service singleton
 *
 * Initializes with configuration from getConfig() on first call.
 */
export function getFingerprintService() {
    if (!fingerprintService) {
        const appConfig = getConfig();
        fingerprintService = new FingerprintService({
            enabled: appConfig.session.fingerprintEnabled,
            strictness: appConfig.session.fingerprintStrictness,
            components: appConfig.session.fingerprintComponents,
        });
        logger.info('Fingerprint service singleton initialized');
    }
    return fingerprintService;
}
/**
 * Reset the singleton instance (useful for testing)
 *
 * @internal
 */
export function resetFingerprintService() {
    fingerprintService = null;
    logger.debug('Fingerprint service singleton reset');
}
/**
 * Extract fingerprint-relevant headers from a Fastify request
 *
 * @param requestHeaders - Raw request headers object
 * @returns Normalized headers for fingerprint computation
 */
export function extractFingerprintHeaders(requestHeaders) {
    const getHeader = (name) => {
        const value = requestHeaders[name] ?? requestHeaders[name.toLowerCase()];
        return typeof value === 'string' ? value : value?.[0];
    };
    return {
        userAgent: getHeader('user-agent'),
        acceptLanguage: getHeader('accept-language'),
        acceptEncoding: getHeader('accept-encoding'),
        dnt: getHeader('dnt'),
        accept: getHeader('accept'),
        secChUa: getHeader('sec-ch-ua'),
        secChUaMobile: getHeader('sec-ch-ua-mobile'),
        secChUaPlatform: getHeader('sec-ch-ua-platform'),
    };
}
export default FingerprintService;
//# sourceMappingURL=fingerprint-service.js.map