domain.ts

2.6 KB · raw

1import { getDomain } from "tldts";23/**4 * Extract eTLD+1 domain from a URL string.5 * Returns null for invalid URLs.6 *7 * `allowPrivateDomains: true` is the origin-binding control, not a preference:8 * under tldts' default every host beneath a shared private suffix (`github.io`,9 * `vercel.app`, `s3.amazonaws.com` and thousands more) collapses to that10 * suffix, so `octocat.github.io` and `evil.github.io` compared EQUAL and a11 * saved login was offered to the attacker's tenant. The sibling passkey matcher12 * (passkey-utils.ts) already passes the option (PB-26-901).13 *14 * Keys derived from this value — `manualSummonDomains`, the15 * `peach_state11_shown:<domain>` flag — were previously the collapsed suffix.16 * Those entries simply stop matching rather than matching more broadly, which17 * is the fail-safe direction, so they are left in place instead of migrating18 * (the stored records carry no URL to re-derive from).19 */20export function extractDomain(url: string): string | null {21  try {22    const normalized = url.startsWith("http") ? url : "https://" + url;23    const parsed = getDomain(normalized, { allowPrivateDomains: true });24    return parsed ?? null;25  } catch {26    return null;27  }28}2930/**31 * Get displayable domain from a URL string.32 */33export function getDisplayDomain(url: string): string {34  try {35    const normalized = url.startsWith("http") ? url : "https://" + url;36    const { hostname } = new URL(normalized);37    return hostname;38  } catch {39    return url;40  }41}4243/**44 * Preserve the browser security boundary users need to inspect before a45 * credential or trust decision: scheme, complete hostname, and port.46 */47export function getSecurityOriginDisplay(url: string): string {48  try {49    return new URL(url).origin;50  } catch {51    return url;52  }53}5455/**56 * LV-2: true when the URL's hostname contains non-ASCII characters — a57 * potential homograph (Cyrillic "аpple.com" etc.) — OR a punycode (xn--)58 * label, which is the URL parser's normalized form of the same risk.59 * Matching/keying is NOT affected (tldts keeps Unicode labels distinct,60 * preventing cross-matching); this only drives a non-blocking UI warning so a61 * user who saved or imported such a URL can see it isn't the ASCII site.62 * Accepts bare hostnames as well as full URLs.63 */64export function detectHomograph(url: string): boolean {65  try {66    const hostname = new URL(url.includes("://") ? url : `https://${url}`).hostname;67    return (68      // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional — detects non-ASCII hostnames69      /[^\x00-\x7F]/.test(hostname) ||70      hostname.split(".").some((l) => l.startsWith("xn--"))71    );72  } catch {73    return false;74  }75}