platform-codebase/@packages/@infrastructure/identifier-utils/src/normalizers.ts

191 lines
5 KiB
TypeScript
Raw Normal View History

/**
* 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();
}