import type { IMatcher, IHeaderMatchOptions } from '../types.js'; /** * HeaderMatcher provides HTTP header matching functionality * Supporting exact matches, patterns, and case-insensitive matching */ export class HeaderMatcher implements IMatcher { /** * Match a header value against a pattern * @param pattern The pattern to match * @param value The header value to test * @param options Matching options * @returns true if the value matches the pattern */ static match( pattern: string, value: string | undefined, options: IHeaderMatchOptions = {} ): boolean { // Handle missing header if (value === undefined || value === null) { return pattern === '' || pattern === null || pattern === undefined; } // Convert to string and normalize const normalizedPattern = String(pattern); const normalizedValue = String(value); // Apply case sensitivity const comparePattern = options.caseInsensitive !== false ? normalizedPattern.toLowerCase() : normalizedPattern; const compareValue = options.caseInsensitive !== false ? normalizedValue.toLowerCase() : normalizedValue; // Exact match if (options.exactMatch !== false) { return comparePattern === compareValue; } // Pattern matching (simple wildcard support) if (comparePattern.includes('*')) { const regex = new RegExp( '^' + comparePattern.replace(/\*/g, '.*') + '$', options.caseInsensitive !== false ? 'i' : '' ); return regex.test(normalizedValue); } // Contains match (if not exact match mode) return compareValue.includes(comparePattern); } /** * Match multiple headers against a set of required headers * @param requiredHeaders Headers that must match * @param actualHeaders Actual request headers * @param options Matching options * @returns true if all required headers match */ static matchAll( requiredHeaders: Record, actualHeaders: Record, options: IHeaderMatchOptions = {} ): boolean { for (const [name, pattern] of Object.entries(requiredHeaders)) { const headerName = options.caseInsensitive !== false ? name.toLowerCase() : name; // Find the actual header (case-insensitive search if needed) let actualValue: string | undefined; if (options.caseInsensitive !== false) { const actualKey = Object.keys(actualHeaders).find( key => key.toLowerCase() === headerName ); const rawValue = actualKey ? actualHeaders[actualKey] : undefined; // Handle array values (multiple headers with same name) actualValue = Array.isArray(rawValue) ? rawValue.join(', ') : rawValue; } else { const rawValue = actualHeaders[name]; // Handle array values (multiple headers with same name) actualValue = Array.isArray(rawValue) ? rawValue.join(', ') : rawValue; } // Check if this header matches if (!this.match(pattern, actualValue, options)) { return false; } } return true; } /** * Calculate the specificity of header requirements * More headers = more specific */ static calculateSpecificity(headers: Record): number { const count = Object.keys(headers).length; let score = count * 10; // Bonus for headers without wildcards (more specific) for (const value of Object.values(headers)) { if (!value.includes('*')) { score += 5; } } return score; } /** * Instance method for interface compliance */ match(pattern: string, value: string, options?: IHeaderMatchOptions): boolean { return HeaderMatcher.match(pattern, value, options); } }