/** * Identifier normalization functions * * Each normalizer strips formatting and standardizes the value * before it is hashed. This ensures consistent matching regardless * of how the original value was entered. */ const GMAIL_DOMAINS = new Set(['gmail.com', 'googlemail.com']); /** * Normalize an email address: lowercase, trim whitespace, * handle Gmail dot-insensitivity and plus-addressing */ export function normalizeEmail(value: string): string { const trimmed = value.trim().toLowerCase(); const [localPart, domain] = trimmed.split('@'); if (!localPart || !domain) { return trimmed; } if (GMAIL_DOMAINS.has(domain)) { const withoutPlus = localPart.split('+')[0]; const withoutDots = withoutPlus.replace(/\./g, ''); return `${withoutDots}@${domain}`; } return trimmed; } /** * Normalize a phone number: strip formatting, apply E.164 */ export function normalizePhone(value: string): string { const digits = value.replace(/\D/g, ''); if (digits.length === 10) { return `1${digits}`; } return digits; } /** * Normalize a legal name: lowercase, collapse whitespace, * remove diacritics, strip common suffixes (Jr, Sr, III) */ export function normalizeLegalName(value: string): string { let normalized = value .trim() .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, ''); normalized = normalized.replace(/\s+/g, ' '); normalized = normalized.replace(/\b(jr|sr|ii|iii|iv)\b/gi, '').trim(); normalized = normalized.replace(/\s+/g, ' ').trim(); return normalized; } /** * Normalize a card number: strip spaces and dashes, * validate Luhn checksum format */ export function normalizeCardNumber(value: string): string { return value.replace(/[\s\-\.]/g, ''); } /** * Normalize a payment app ID: lowercase, trim, * strip leading @ or $ symbols */ export function normalizePaymentAppId(value: string): string { const trimmed = value.trim().toLowerCase(); return trimmed.replace(/^[@$]/, ''); } /** * Normalize a canvas fingerprint: identity (hash is already deterministic) */ export function normalizeCanvasFp(value: string): string { return value.trim(); } /** * Normalize a WebGL fingerprint: lowercase and trim */ export function normalizeWebglFp(value: string): string { return value.trim().toLowerCase(); } /** * Normalize an audio fingerprint: identity (hash is already deterministic) */ export function normalizeAudioFp(value: string): string { return value.trim(); } /** * Normalize a WebRTC local IP: strip port if present, trim */ export function normalizeWebrtcLocalIp(value: string): string { const trimmed = value.trim(); // Only strip port from IPv4 addresses (exactly one colon, digits after it) // IPv6 addresses contain multiple colons and must not be modified const colonCount = (trimmed.match(/:/g) ?? []).length; if (colonCount === 1) { const colonIndex = trimmed.indexOf(':'); const afterColon = trimmed.slice(colonIndex + 1); if (/^\d+$/.test(afterColon)) { return trimmed.slice(0, colonIndex); } } return trimmed; } /** * Normalize screen geometry: sort keys, JSON.stringify for consistency */ export function normalizeScreenGeometry(value: string): string { const trimmed = value.trim(); try { const parsed = JSON.parse(trimmed); const sorted = Object.keys(parsed).sort().reduce>((acc, key) => { acc[key] = parsed[key]; return acc; }, {}); return JSON.stringify(sorted); } catch { // Already in "WxH:D:R" string format — normalize as-is return trimmed.toLowerCase(); } } /** * Normalize timezone/locale: lowercase and trim */ export function normalizeTimezoneLocale(value: string): string { return value.trim().toLowerCase(); } /** * Normalize font set: sort font names, join, lowercase */ export function normalizeFontSet(value: string): string { const trimmed = value.trim().toLowerCase(); try { const fonts: string[] = JSON.parse(trimmed); return fonts.sort().join(','); } catch { // Already a comma-separated or hashed string return trimmed.split(',').map((f) => f.trim()).sort().join(','); } } /** * Normalize hardware profile: sort keys, JSON.stringify for consistency */ export function normalizeHardwareProfile(value: string): string { const trimmed = value.trim(); try { const parsed = JSON.parse(trimmed); const sorted = Object.keys(parsed).sort().reduce>((acc, key) => { acc[key] = parsed[key]; return acc; }, {}); return JSON.stringify(sorted); } catch { // Already in "cores:mem:touch:mediaDevices" string format return trimmed.toLowerCase(); } } /** * Normalize a typing cadence hash: identity (hash of timing pattern) */ export function normalizeTypingCadence(value: string): string { return value.trim(); } /** * Normalize a mouse dynamics hash: identity (hash of movement pattern) */ export function normalizeMouseDynamics(value: string): string { return value.trim(); }