/** * Phone identity. * * Phone is the primary identity on this platform, which makes its *string form* * load-bearing: `users.phone` carries a UNIQUE constraint, bans are enforced per * account, and duplicate detection matches on it. If "+34600111222" and * "0034 600 111 222" can both be stored, then one handset holds two distinct * "unique" accounts — which defeats the constraint, lets a banned user return, * and hides the duplicate from `findPossibleDuplicates`. * * So there is exactly one accepted stored form: E.164, no spaces, no separators. * Normalise on the way in, reject anything that cannot be normalised. */ /** E.164: a leading +, a nonzero leading digit, and 8–15 digits total. */ const E164 = /^\+[1-9]\d{7,14}$/; export function isE164(value: string): boolean { return E164.test(value); } /** * Coerce common user input into E.164, or return null if it cannot be done * unambiguously. * * Handles the shapes people actually type: spaces, hyphens, parentheses and dots * as separators, and a `00` international prefix instead of `+`. It deliberately * does NOT guess a country code for a bare national number — "600111222" is * meaningless without knowing the country, and silently assuming one would * attach a real person's account to the wrong number. */ export function toE164(input: string | null | undefined): string | null { if (!input) return null; // Strip everything a human might use as a separator. let s = input.trim().replace(/[\s().-]/g, ''); if (s.length === 0) return null; // "0034..." is the same as "+34..." if (s.startsWith('00')) s = `+${s.slice(2)}`; // A bare national number is ambiguous — refuse rather than guess a country. if (!s.startsWith('+')) return null; if (!/^\+\d+$/.test(s)) return null; return isE164(s) ? s : null; } /** * Last four digits, for display ("••• ••• 222"). Never render a full number * belonging to someone other than the viewer. */ export function phoneLast4(e164: string): string { return e164.slice(-4); }