/**
 * CSRF Protection Module
 *
 * Provides CSRF (Cross-Site Request Forgery) protection for web interfaces.
 * Used when Code Buddy exposes HTTP endpoints (REST API, webhooks, etc.)
 *
 * Features:
 * - Token generation and validation
 * - Double-submit cookie pattern
 * - SameSite cookie configuration
 * - Token rotation
 */
import crypto from 'crypto';
import { EventEmitter } from 'events';
const DEFAULT_CONFIG = {
    tokenLength: 32,
    tokenExpiry: 3600000, // 1 hour
    cookieName: '_csrf',
    headerName: 'X-CSRF-Token',
    doubleSubmit: true,
    sameSite: 'strict',
    secure: process.env.NODE_ENV === 'production',
};
/**
 * CSRF Protection Manager
 */
export class CSRFProtection extends EventEmitter {
    config;
    tokens = new Map();
    cleanupInterval = null;
    constructor(config = {}) {
        super();
        this.config = { ...DEFAULT_CONFIG, ...config };
        // Start cleanup interval
        this.startCleanup();
    }
    /**
     * Generate a new CSRF token
     */
    generateToken(sessionId) {
        const token = crypto.randomBytes(this.config.tokenLength).toString('hex');
        const now = new Date();
        const csrfToken = {
            token,
            createdAt: now,
            expiresAt: new Date(now.getTime() + this.config.tokenExpiry),
            sessionId,
        };
        this.tokens.set(token, csrfToken);
        this.emit('token-generated', { token: token.slice(0, 8) + '...', sessionId });
        return csrfToken;
    }
    /**
     * Validate a CSRF token
     */
    validateToken(token, options = {}) {
        // Check if token exists
        const storedToken = this.tokens.get(token);
        if (!storedToken) {
            this.emit('validation-failed', { reason: 'token-not-found' });
            return { valid: false, error: 'Invalid CSRF token' };
        }
        // Check expiry
        if (new Date() > storedToken.expiresAt) {
            this.tokens.delete(token);
            this.emit('validation-failed', { reason: 'token-expired' });
            return { valid: false, error: 'CSRF token expired' };
        }
        // Check session binding if provided
        if (options.sessionId && storedToken.sessionId !== options.sessionId) {
            this.emit('validation-failed', { reason: 'session-mismatch' });
            return { valid: false, error: 'CSRF token session mismatch' };
        }
        // Double-submit cookie validation
        if (this.config.doubleSubmit && options.cookieToken) {
            if (token !== options.cookieToken) {
                this.emit('validation-failed', { reason: 'cookie-mismatch' });
                return { valid: false, error: 'CSRF cookie token mismatch' };
            }
        }
        this.emit('validation-success', { token: token.slice(0, 8) + '...' });
        return { valid: true, token: storedToken };
    }
    /**
     * Rotate a token (invalidate old, generate new)
     */
    rotateToken(oldToken, sessionId) {
        const validation = this.validateToken(oldToken, { sessionId });
        if (!validation.valid) {
            return null;
        }
        // Invalidate old token
        this.tokens.delete(oldToken);
        // Generate new token
        return this.generateToken(sessionId);
    }
    /**
     * Invalidate a specific token
     */
    invalidateToken(token) {
        return this.tokens.delete(token);
    }
    /**
     * Invalidate all tokens for a session
     */
    invalidateSession(sessionId) {
        let count = 0;
        for (const [token, csrfToken] of this.tokens) {
            if (csrfToken.sessionId === sessionId) {
                this.tokens.delete(token);
                count++;
            }
        }
        return count;
    }
    /**
     * Get cookie options for setting CSRF cookie
     */
    getCookieOptions() {
        return {
            name: this.config.cookieName,
            options: {
                httpOnly: false, // Must be readable by JavaScript for double-submit
                secure: this.config.secure,
                sameSite: this.config.sameSite,
                maxAge: this.config.tokenExpiry,
                path: '/',
            },
        };
    }
    /**
     * Get header name for CSRF token
     */
    getHeaderName() {
        return this.config.headerName;
    }
    /**
     * Express/Connect middleware for CSRF protection
     */
    middleware() {
        return (req, res, next) => {
            // Skip safe methods
            const safeMethods = ['GET', 'HEAD', 'OPTIONS'];
            if (safeMethods.includes(req.method?.toUpperCase() || '')) {
                // Generate token for safe methods if not present
                if (!req.cookies?.[this.config.cookieName]) {
                    const csrfToken = this.generateToken(req.sessionID);
                    const cookieOpts = this.getCookieOptions();
                    res.cookie(cookieOpts.name, csrfToken.token, cookieOpts.options);
                    req.csrfToken = () => csrfToken.token;
                }
                else {
                    const cookieToken = req.cookies?.[this.config.cookieName] || '';
                    req.csrfToken = () => cookieToken;
                }
                return next();
            }
            // Validate token for unsafe methods
            const headerToken = req.headers[this.config.headerName.toLowerCase()];
            const bodyToken = req.body?._csrf;
            const queryToken = req.query?._csrf;
            const cookieToken = req.cookies?.[this.config.cookieName];
            const token = headerToken || bodyToken || queryToken;
            if (!token) {
                return next(new Error('CSRF token missing'));
            }
            const validation = this.validateToken(token, {
                sessionId: req.sessionID,
                cookieToken,
            });
            if (!validation.valid) {
                return next(new Error(validation.error || 'Invalid CSRF token'));
            }
            // Attach token function to request
            req.csrfToken = () => token;
            next();
        };
    }
    /**
     * Generate HTML meta tag for CSRF token
     */
    getMetaTag(token) {
        return `<meta name="csrf-token" content="${this.escapeHtml(token)}">`;
    }
    /**
     * Generate hidden form field for CSRF token
     */
    getFormField(token) {
        return `<input type="hidden" name="_csrf" value="${this.escapeHtml(token)}">`;
    }
    /**
     * Escape HTML special characters
     */
    escapeHtml(str) {
        const htmlEscapes = {
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#39;',
        };
        return str.replace(/[&<>"']/g, (char) => htmlEscapes[char] || char);
    }
    /**
     * Start cleanup interval for expired tokens
     */
    startCleanup() {
        // Cleanup every 5 minutes
        this.cleanupInterval = setInterval(() => {
            this.cleanup();
        }, 300000);
        // Don't prevent process exit
        if (this.cleanupInterval.unref) {
            this.cleanupInterval.unref();
        }
    }
    /**
     * Clean up expired tokens
     */
    cleanup() {
        const now = new Date();
        let cleaned = 0;
        for (const [token, csrfToken] of this.tokens) {
            if (now > csrfToken.expiresAt) {
                this.tokens.delete(token);
                cleaned++;
            }
        }
        if (cleaned > 0) {
            this.emit('cleanup', { cleaned });
        }
        return cleaned;
    }
    /**
     * Get statistics
     */
    getStats() {
        let oldestToken;
        let newestToken;
        for (const csrfToken of this.tokens.values()) {
            if (!oldestToken || csrfToken.createdAt < oldestToken) {
                oldestToken = csrfToken.createdAt;
            }
            if (!newestToken || csrfToken.createdAt > newestToken) {
                newestToken = csrfToken.createdAt;
            }
        }
        return {
            activeTokens: this.tokens.size,
            oldestToken,
            newestToken,
        };
    }
    /**
     * Dispose and cleanup resources
     */
    dispose() {
        if (this.cleanupInterval) {
            clearInterval(this.cleanupInterval);
            this.cleanupInterval = null;
        }
        this.tokens.clear();
        this.removeAllListeners();
    }
}
// Singleton instance
let csrfInstance = null;
/**
 * Get the CSRF protection instance
 */
export function getCSRFProtection(config) {
    if (!csrfInstance) {
        csrfInstance = new CSRFProtection(config);
    }
    return csrfInstance;
}
/**
 * Reset the CSRF protection instance
 */
export function resetCSRFProtection() {
    if (csrfInstance) {
        csrfInstance.dispose();
    }
    csrfInstance = null;
}
export default CSRFProtection;
//# sourceMappingURL=csrf-protection.js.map