forked from bluesky-social/atproto
-
Notifications
You must be signed in to change notification settings - Fork 0
/
handle.ts
168 lines (157 loc) · 5.29 KB
/
handle.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import { reservedSubdomains } from './reserved'
export const INVALID_HANDLE = 'handle.invalid'
// Currently these are registration-time restrictions, not protocol-level
// restrictions. We have a couple accounts in the wild that we need to clean up
// before hard-disallow.
// See also: https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains
const DISALLOWED_TLDS = [
'.local',
'.arpa',
'.invalid',
'.localhost',
'.internal',
// policy could concievably change on ".onion" some day
'.onion',
// NOTE: .test is allowed in testing and devopment. In practical terms
// "should" "never" actually resolve and get registered in production
]
// 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',
)
}
}
}
// simple regex translation of above constraints
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)')
}
}
export const normalizeHandle = (handle: string): string => {
return handle.toLowerCase()
}
export const normalizeAndEnsureValidHandle = (handle: string): string => {
const normalized = normalizeHandle(handle)
ensureValidHandle(normalized)
return normalized
}
export const isValidHandle = (handle: string): boolean => {
try {
ensureValidHandle(handle)
} catch (err) {
if (err instanceof InvalidHandleError) {
return false
}
throw err
}
return true
}
export const ensureHandleServiceConstraints = (
handle: string,
availableUserDomains: string[],
reserved = reservedSubdomains,
): void => {
const disallowedTld = DISALLOWED_TLDS.find((domain) =>
handle.endsWith(domain),
)
if (disallowedTld) {
throw new DisallowedDomainError('Handle TLD is invalid or disallowed')
}
const supportedDomain = availableUserDomains.find((domain) =>
handle.endsWith(domain),
)
if (!supportedDomain) {
throw new UnsupportedDomainError('Not a supported handle domain')
}
const front = handle.slice(0, handle.length - supportedDomain.length)
if (front.indexOf('.') > -1) {
throw new InvalidHandleError('Invalid characters in handle')
}
if (front.length < 3) {
throw new InvalidHandleError('Handle too short')
}
if (handle.length > 30) {
throw new InvalidHandleError('Handle too long')
}
if (reserved[front]) {
throw new ReservedHandleError('Reserved handle')
}
}
export const fulfillsHandleServiceConstraints = (
handle: string,
availableUserDomains: string[],
reserved = reservedSubdomains,
): boolean => {
try {
ensureHandleServiceConstraints(handle, availableUserDomains, reserved)
} catch (err) {
if (
err instanceof InvalidHandleError ||
err instanceof ReservedHandleError ||
err instanceof UnsupportedDomainError ||
err instanceof DisallowedDomainError
) {
return false
}
throw err
}
return true
}
export class InvalidHandleError extends Error {}
export class ReservedHandleError extends Error {}
export class UnsupportedDomainError extends Error {}
export class DisallowedDomainError extends Error {}