| 1 | import { randomSuffix } from "../auth/crypto"; |
| 2 | |
| 3 | // Names that must never become a public handle (routes, infra, impersonation). |
| 4 | const RESERVED = new Set([ |
| 5 | "admin", "administrator", "root", "system", "support", "help", "about", "api", |
| 6 | "auth", "login", "logout", "register", "signup", "signin", "reset", "verify", |
| 7 | "forgot", "account", "settings", "me", "u", "user", "users", "profile", |
| 8 | "reasonix", "www", "mail", "no-reply", "noreply", "null", "undefined", "anonymous", |
| 9 | ]); |
| 10 | |
| 11 | // Lowercase, collapse runs of disallowed characters to single underscores, and |
| 12 | // trim underscores from the ends. |
| 13 | export function normalizeHandle(raw: string): string { |
| 14 | return raw |
| 15 | .toLowerCase() |
| 16 | .replace(/[^a-z0-9_]+/g, "_") |
| 17 | .replace(/_+/g, "_") |
| 18 | .replace(/^_+|_+$/g, ""); |
| 19 | } |
| 20 | |
| 21 | // 3–30 chars, [a-z0-9_], must start and end with an alphanumeric, not reserved. |
| 22 | export function isValidHandle(handle: string): boolean { |
| 23 | if (handle.length < 3 || handle.length > 30) return false; |
| 24 | if (!/^[a-z0-9](?:[a-z0-9_]*[a-z0-9])?$/.test(handle)) return false; |
| 25 | return !RESERVED.has(handle); |
| 26 | } |
| 27 | |
| 28 | // A normalized, valid starting handle derived from the email local-part. Falls |
| 29 | // back to a random "user…" handle when the local-part can't yield a valid one. |
| 30 | export function deriveHandleBase(email: string): string { |
| 31 | let base = normalizeHandle(email.split("@")[0] ?? ""); |
| 32 | if (base.length > 24) base = base.slice(0, 24).replace(/_+$/g, ""); |
| 33 | if (base.length < 3 || RESERVED.has(base)) base = `user${randomSuffix(4)}`; |
| 34 | return base; |
| 35 | } |
| 36 |