Suite of AT Protocol TypeScript libraries built on web standards
1export const INVALID_HANDLE = "handle.invalid";
2
3/** Registration-time restrictions, not protocol-level restrictions.
4 * `.test` is allowed but only should be used in testing and development.
5 * @see {https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains}
6 */
7export const DISALLOWED_TLDS = [
8 ".local",
9 ".arpa",
10 ".invalid",
11 ".localhost",
12 ".internal",
13 ".example",
14 ".alt",
15 // policy could concievably change on ".onion" some day
16 ".onion",
17];
18
19/**
20 * Ensure a handle is valid
21 * @throws If handle is invalid
22 *
23 * Handle constraints, in English:
24 * - must be a possible domain name
25 * - RFC-1035 is commonly referenced, but has been updated. eg, RFC-3696,
26 * section 2. and RFC-3986, section 3. can now have leading numbers (eg,
27 * 4chan.org)
28 * - "labels" (sub-names) are made of ASCII letters, digits, hyphens
29 * - can not start or end with a hyphen
30 * - TLD (last component) should not start with a digit
31 * - can't end with a hyphen (can end with digit)
32 * - each segment must be between 1 and 63 characters (not including any periods)
33 * - overall length can't be more than 253 characters
34 * - separated by (ASCII) periods; does not start or end with period
35 * - case insensitive
36 * - domains (handles) are equal if they are the same lower-case
37 * - punycode allowed for internationalization
38 * - no whitespace, null bytes, joining chars, etc
39 * - does not validate whether domain or TLD exists, or is a reserved or
40 * special TLD (eg, .onion or .local)
41 * - does not validate punycode
42 */
43export const ensureValidHandle = (handle: string): void => {
44 // check that all chars are boring ASCII
45 if (!/^[a-zA-Z0-9.-]*$/.test(handle)) {
46 throw new InvalidHandleError(
47 "Disallowed characters in handle (ASCII letters, digits, dashes, periods only)",
48 );
49 }
50
51 if (handle.length > 253) {
52 throw new InvalidHandleError("Handle is too long (253 chars max)");
53 }
54 const labels = handle.split(".");
55 if (labels.length < 2) {
56 throw new InvalidHandleError("Handle domain needs at least two parts");
57 }
58 for (let i = 0; i < labels.length; i++) {
59 const l = labels[i];
60 if (l.length < 1) {
61 throw new InvalidHandleError("Handle parts can not be empty");
62 }
63 if (l.length > 63) {
64 throw new InvalidHandleError("Handle part too long (max 63 chars)");
65 }
66 if (l.endsWith("-") || l.startsWith("-")) {
67 throw new InvalidHandleError(
68 "Handle parts can not start or end with hyphens",
69 );
70 }
71 if (i + 1 === labels.length && !/^[a-zA-Z]/.test(l)) {
72 throw new InvalidHandleError(
73 "Handle final component (TLD) must start with ASCII letter",
74 );
75 }
76 }
77};
78
79/**
80 * Ensure a handle is valid using a regex pattern.
81 * @throws If handle is invalid
82 */
83export const ensureValidHandleRegex = (handle: string): void => {
84 if (
85 !/^([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])?$/
86 .test(
87 handle,
88 )
89 ) {
90 throw new InvalidHandleError("Handle didn't validate via regex");
91 }
92 if (handle.length > 253) {
93 throw new InvalidHandleError("Handle is too long (253 chars max)");
94 }
95};
96
97/**
98 * Converts a handle to lowercase.
99 */
100export const normalizeHandle = (handle: string): string => {
101 return handle.toLowerCase();
102};
103
104/**
105 * Converts a handle to lowercase and ensures it is valid.
106 * @returns The normalized handle if it is valid
107 * @throws If handle is invalid
108 */
109export const normalizeAndEnsureValidHandle = (handle: string): string => {
110 const normalized = normalizeHandle(handle);
111 ensureValidHandle(normalized);
112 return normalized;
113};
114
115/**
116 * Checks if a handle is valid and returns a boolean.
117 *
118 * @returns True if handle is valid
119 */
120export const isValidHandle = (handle: string): boolean => {
121 try {
122 ensureValidHandle(handle);
123 } catch (err) {
124 if (err instanceof InvalidHandleError) {
125 return false;
126 }
127 throw err;
128 }
129
130 return true;
131};
132
133/**
134 * Check if a TLD is valid.
135 *
136 * Disallowed TLDs: {@linkcode DISALLOWED_TLDS}
137 */
138export const isValidTld = (handle: string): boolean => {
139 return !DISALLOWED_TLDS.some((domain) => handle.endsWith(domain));
140};
141
142/**
143 * Thrown when a handle is invalid.
144 * Caused by invalid characters (only ASCII letters, digits, dashes, periods are allowed),
145 * length longer than 253 characters, or one of the {@linkcode DISALLOWED_TLDS} used.
146 */
147export class InvalidHandleError extends Error {}
148
149/** @deprecated Use {@linkcode InvalidHandleError} */
150export class ReservedHandleError extends Error {}
151/** @deprecated Use {@linkcode InvalidHandleError} */
152export class UnsupportedDomainError extends Error {}
153/** @deprecated Use {@linkcode InvalidHandleError} */
154export class DisallowedDomainError extends Error {}