191 lines
5 KiB
TypeScript
191 lines
5 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<Record<string, unknown>>((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<Record<string, unknown>>((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();
|
||
|
|
}
|