export const INVALID_HANDLE = "handle.invalid"; /** Registration-time restrictions, not protocol-level restrictions. * `.test` is allowed but only should be used in testing and development. * @see {https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains} */ export const DISALLOWED_TLDS = [ ".local", ".arpa", ".invalid", ".localhost", ".internal", ".example", ".alt", // policy could concievably change on ".onion" some day ".onion", ]; /** * Ensure a handle is valid * @throws If handle is invalid * * Handle constraints, in English: * - must be a possible domain name * - RFC-1035 is commonly referenced, but has been updated. eg, RFC-3696, * section 2. and RFC-3986, section 3. can now have leading numbers (eg, * 4chan.org) * - "labels" (sub-names) are made of ASCII letters, digits, hyphens * - can not start or end with a hyphen * - TLD (last component) should not start with a digit * - can't end with a hyphen (can end with digit) * - each segment must be between 1 and 63 characters (not including any periods) * - overall length can't be more than 253 characters * - separated by (ASCII) periods; does not start or end with period * - case insensitive * - domains (handles) are equal if they are the same lower-case * - punycode allowed for internationalization * - no whitespace, null bytes, joining chars, etc * - does not validate whether domain or TLD exists, or is a reserved or * special TLD (eg, .onion or .local) * - does not validate punycode */ export const ensureValidHandle = (handle: string): void => { // check that all chars are boring ASCII if (!/^[a-zA-Z0-9.-]*$/.test(handle)) { throw new InvalidHandleError( "Disallowed characters in handle (ASCII letters, digits, dashes, periods only)", ); } if (handle.length > 253) { throw new InvalidHandleError("Handle is too long (253 chars max)"); } const labels = handle.split("."); if (labels.length < 2) { throw new InvalidHandleError("Handle domain needs at least two parts"); } for (let i = 0; i < labels.length; i++) { const l = labels[i]; if (l.length < 1) { throw new InvalidHandleError("Handle parts can not be empty"); } if (l.length > 63) { throw new InvalidHandleError("Handle part too long (max 63 chars)"); } if (l.endsWith("-") || l.startsWith("-")) { throw new InvalidHandleError( "Handle parts can not start or end with hyphens", ); } if (i + 1 === labels.length && !/^[a-zA-Z]/.test(l)) { throw new InvalidHandleError( "Handle final component (TLD) must start with ASCII letter", ); } } }; /** * Ensure a handle is valid using a regex pattern. * @throws If handle is invalid */ export const ensureValidHandleRegex = (handle: string): void => { if ( !/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/ .test( handle, ) ) { throw new InvalidHandleError("Handle didn't validate via regex"); } if (handle.length > 253) { throw new InvalidHandleError("Handle is too long (253 chars max)"); } }; /** * Converts a handle to lowercase. */ export const normalizeHandle = (handle: string): string => { return handle.toLowerCase(); }; /** * Converts a handle to lowercase and ensures it is valid. * @returns The normalized handle if it is valid * @throws If handle is invalid */ export const normalizeAndEnsureValidHandle = (handle: string): string => { const normalized = normalizeHandle(handle); ensureValidHandle(normalized); return normalized; }; /** * Checks if a handle is valid and returns a boolean. * * @returns True if handle is valid */ export const isValidHandle = (handle: string): boolean => { try { ensureValidHandle(handle); } catch (err) { if (err instanceof InvalidHandleError) { return false; } throw err; } return true; }; /** * Check if a TLD is valid. * * Disallowed TLDs: {@linkcode DISALLOWED_TLDS} */ export const isValidTld = (handle: string): boolean => { return !DISALLOWED_TLDS.some((domain) => handle.endsWith(domain)); }; /** * Thrown when a handle is invalid. * Caused by invalid characters (only ASCII letters, digits, dashes, periods are allowed), * length longer than 253 characters, or one of the {@linkcode DISALLOWED_TLDS} used. */ export class InvalidHandleError extends Error {} /** @deprecated Use {@linkcode InvalidHandleError} */ export class ReservedHandleError extends Error {} /** @deprecated Use {@linkcode InvalidHandleError} */ export class UnsupportedDomainError extends Error {} /** @deprecated Use {@linkcode InvalidHandleError} */ export class DisallowedDomainError extends Error {}