Last active
November 26, 2025 08:48
-
-
Save R44VC0RP/55da3edea4c8d25bc079840eb1ae26c7 to your computer and use it in GitHub Desktop.
RFC Compliant domain verification. (now using tldts)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * RFC-compliant domain validation using the Public Suffix List | |
| * Properly handles multi-level TLDs (e.g., .co.uk, .com.au) | |
| * Works on both client and server side | |
| */ | |
| import { parse } from 'tldts' | |
| export interface DomainValidationResult { | |
| isValid: boolean | |
| domain: string | |
| normalizedDomain: string | null | |
| error: string | null | |
| rootDomain: string | null | |
| isSubdomain: boolean | |
| } | |
| /** | |
| * Validates a domain according to RFC 1035 and RFC 5321 specifications | |
| * Uses the Public Suffix List for proper TLD handling | |
| * | |
| * @param domain - The domain string to validate | |
| * @returns DomainValidationResult with validation details | |
| * | |
| * @example | |
| * validateDomain('example.com') // { isValid: true, ... } | |
| * validateDomain('mail.example.co.uk') // { isValid: true, isSubdomain: true, ... } | |
| * validateDomain('invalid') // { isValid: false, error: 'Invalid domain format' } | |
| */ | |
| export function validateDomain(domain: string): DomainValidationResult { | |
| // Normalize: lowercase and trim | |
| const normalizedDomain = domain?.toLowerCase().trim() || '' | |
| // Empty check | |
| if (!normalizedDomain) { | |
| return { | |
| isValid: false, | |
| domain, | |
| normalizedDomain: null, | |
| error: 'Domain is required', | |
| rootDomain: null, | |
| isSubdomain: false, | |
| } | |
| } | |
| // RFC 1035: Maximum domain length is 253 characters | |
| if (normalizedDomain.length > 253) { | |
| return { | |
| isValid: false, | |
| domain, | |
| normalizedDomain, | |
| error: 'Domain exceeds maximum length of 253 characters', | |
| rootDomain: null, | |
| isSubdomain: false, | |
| } | |
| } | |
| // RFC 1035: Each label must be 1-63 characters | |
| const labels = normalizedDomain.split('.') | |
| for (const label of labels) { | |
| if (label.length === 0) { | |
| return { | |
| isValid: false, | |
| domain, | |
| normalizedDomain, | |
| error: 'Domain contains empty label', | |
| rootDomain: null, | |
| isSubdomain: false, | |
| } | |
| } | |
| if (label.length > 63) { | |
| return { | |
| isValid: false, | |
| domain, | |
| normalizedDomain, | |
| error: 'Domain label exceeds maximum length of 63 characters', | |
| rootDomain: null, | |
| isSubdomain: false, | |
| } | |
| } | |
| } | |
| // RFC 1035: Labels must start with alphanumeric, can contain hyphens, must end with alphanumeric | |
| // Exception: numeric-only labels are allowed (for reverse DNS) | |
| const labelRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/ | |
| for (const label of labels) { | |
| if (!labelRegex.test(label)) { | |
| // Check if it's a single character (valid) | |
| if (label.length === 1 && /^[a-z0-9]$/.test(label)) { | |
| continue | |
| } | |
| return { | |
| isValid: false, | |
| domain, | |
| normalizedDomain, | |
| error: 'Domain contains invalid characters. Use only letters, numbers, and hyphens.', | |
| rootDomain: null, | |
| isSubdomain: false, | |
| } | |
| } | |
| // RFC 1035: Labels cannot start or end with hyphen | |
| if (label.startsWith('-') || label.endsWith('-')) { | |
| return { | |
| isValid: false, | |
| domain, | |
| normalizedDomain, | |
| error: 'Domain labels cannot start or end with a hyphen', | |
| rootDomain: null, | |
| isSubdomain: false, | |
| } | |
| } | |
| } | |
| // Use Public Suffix List to validate TLD and extract root domain | |
| try { | |
| const parsed = parse(normalizedDomain) | |
| // Check if it's an IP address (not a valid domain) | |
| if (parsed.isIp) { | |
| return { | |
| isValid: false, | |
| domain, | |
| normalizedDomain, | |
| error: 'IP addresses are not valid domain names', | |
| rootDomain: null, | |
| isSubdomain: false, | |
| } | |
| } | |
| // Check if TLD is recognized AND listed in the Public Suffix List (ICANN) | |
| // isIcann indicates if the TLD is in the ICANN part of the PSL | |
| if (!parsed.publicSuffix || parsed.isIcann === false) { | |
| return { | |
| isValid: false, | |
| domain, | |
| normalizedDomain, | |
| error: 'Invalid or unrecognized top-level domain (TLD)', | |
| rootDomain: null, | |
| isSubdomain: false, | |
| } | |
| } | |
| // Check if domain is valid (has SLD) | |
| if (!parsed.domain) { | |
| return { | |
| isValid: false, | |
| domain, | |
| normalizedDomain, | |
| error: 'Domain must have a valid second-level domain', | |
| rootDomain: null, | |
| isSubdomain: false, | |
| } | |
| } | |
| // Success - domain is valid | |
| const isSubdomain = parsed.subdomain !== null && parsed.subdomain.length > 0 | |
| return { | |
| isValid: true, | |
| domain, | |
| normalizedDomain, | |
| error: null, | |
| rootDomain: parsed.domain, | |
| isSubdomain, | |
| } | |
| } catch (error) { | |
| return { | |
| isValid: false, | |
| domain, | |
| normalizedDomain, | |
| error: 'Unable to validate domain format', | |
| rootDomain: null, | |
| isSubdomain: false, | |
| } | |
| } | |
| } | |
| /** | |
| * Quick check if a domain is valid (boolean only) | |
| * Useful for simple validation without detailed error info | |
| */ | |
| export function isValidDomain(domain: string): boolean { | |
| return validateDomain(domain).isValid | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment