@atproto/syntax 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/LICENSE.txt +1 -1
- package/benchmark.js +208 -0
- package/dist/at-identifier.d.ts +5 -0
- package/dist/at-identifier.d.ts.map +1 -0
- package/dist/at-identifier.js +19 -0
- package/dist/at-identifier.js.map +1 -0
- package/dist/aturi.d.ts +8 -6
- package/dist/aturi.d.ts.map +1 -1
- package/dist/aturi.js +36 -45
- package/dist/aturi.js.map +1 -1
- package/dist/aturi_validation.d.ts +5 -2
- package/dist/aturi_validation.d.ts.map +1 -1
- package/dist/aturi_validation.js +54 -66
- package/dist/aturi_validation.js.map +1 -1
- package/dist/datetime.d.ts +11 -4
- package/dist/datetime.d.ts.map +1 -1
- package/dist/datetime.js +16 -16
- package/dist/datetime.js.map +1 -1
- package/dist/did.d.ts +3 -2
- package/dist/did.d.ts.map +1 -1
- package/dist/did.js +8 -8
- package/dist/did.js.map +1 -1
- package/dist/handle.d.ts +7 -6
- package/dist/handle.d.ts.map +1 -1
- package/dist/handle.js +28 -31
- package/dist/handle.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/nsid.d.ts +30 -5
- package/dist/nsid.d.ts.map +1 -1
- package/dist/nsid.js +137 -47
- package/dist/nsid.js.map +1 -1
- package/dist/recordkey.d.ts +3 -2
- package/dist/recordkey.d.ts.map +1 -1
- package/dist/recordkey.js +33 -22
- package/dist/recordkey.js.map +1 -1
- package/dist/tid.d.ts +3 -2
- package/dist/tid.d.ts.map +1 -1
- package/dist/tid.js +7 -7
- package/dist/tid.js.map +1 -1
- package/package.json +1 -1
- package/src/at-identifier.ts +22 -0
- package/src/aturi.ts +59 -46
- package/src/aturi_validation.ts +62 -57
- package/src/datetime.ts +17 -4
- package/src/did.ts +5 -2
- package/src/handle.ts +23 -22
- package/src/index.ts +8 -7
- package/src/nsid.ts +146 -47
- package/src/recordkey.ts +40 -17
- package/src/tid.ts +4 -2
- package/tests/nsid.test.ts +117 -77
- package/tsconfig.build.json +1 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.tests.tsbuildinfo +1 -1
package/src/aturi.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { AtIdentifierString, ensureValidAtIdentifier } from './at-identifier.js'
|
|
2
|
+
import { AtUriString } from './aturi_validation.js'
|
|
3
|
+
import { ensureValidNsid } from './nsid.js'
|
|
4
|
+
|
|
5
|
+
export * from './aturi_validation.js'
|
|
2
6
|
|
|
3
7
|
export const ATP_URI_REGEX =
|
|
4
8
|
// proto- --did-------------- --name---------------- --path---- --query-- --hash--
|
|
@@ -8,32 +12,23 @@ const RELATIVE_REGEX = /^(\/[^?#\s]*)?(\?[^#\s]+)?(#[^\s]+)?$/i
|
|
|
8
12
|
|
|
9
13
|
export class AtUri {
|
|
10
14
|
hash: string
|
|
11
|
-
host:
|
|
15
|
+
host: AtIdentifierString
|
|
12
16
|
pathname: string
|
|
13
17
|
searchParams: URLSearchParams
|
|
14
18
|
|
|
15
|
-
constructor(uri: string, base?: string) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const relativep = parseRelative(uri)
|
|
23
|
-
if (!relativep) {
|
|
24
|
-
throw new Error(`Invalid path: ${uri}`)
|
|
25
|
-
}
|
|
26
|
-
Object.assign(parsed, relativep)
|
|
27
|
-
} else {
|
|
28
|
-
parsed = parse(uri)
|
|
29
|
-
if (!parsed) {
|
|
30
|
-
throw new Error(`Invalid at uri: ${uri}`)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
19
|
+
constructor(uri: string, base?: string | AtUri) {
|
|
20
|
+
const parsed =
|
|
21
|
+
base !== undefined
|
|
22
|
+
? typeof base === 'string'
|
|
23
|
+
? Object.assign(parse(base), parseRelative(uri))
|
|
24
|
+
: Object.assign({ host: base.host }, parseRelative(uri))
|
|
25
|
+
: parse(uri)
|
|
33
26
|
|
|
34
|
-
|
|
27
|
+
ensureValidAtIdentifier(parsed.host)
|
|
28
|
+
|
|
29
|
+
this.hash = parsed.hash ?? ''
|
|
35
30
|
this.host = parsed.host
|
|
36
|
-
this.pathname = parsed.pathname
|
|
31
|
+
this.pathname = parsed.pathname ?? ''
|
|
37
32
|
this.searchParams = parsed.searchParams
|
|
38
33
|
}
|
|
39
34
|
|
|
@@ -49,7 +44,7 @@ export class AtUri {
|
|
|
49
44
|
}
|
|
50
45
|
|
|
51
46
|
get origin() {
|
|
52
|
-
return `at://${this.host}`
|
|
47
|
+
return `at://${this.host}` as const
|
|
53
48
|
}
|
|
54
49
|
|
|
55
50
|
get hostname() {
|
|
@@ -57,6 +52,7 @@ export class AtUri {
|
|
|
57
52
|
}
|
|
58
53
|
|
|
59
54
|
set hostname(v: string) {
|
|
55
|
+
ensureValidAtIdentifier(v)
|
|
60
56
|
this.host = v
|
|
61
57
|
}
|
|
62
58
|
|
|
@@ -73,6 +69,7 @@ export class AtUri {
|
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
set collection(v: string) {
|
|
72
|
+
ensureValidNsid(v)
|
|
76
73
|
const parts = this.pathname.split('/').filter(Boolean)
|
|
77
74
|
parts[0] = v
|
|
78
75
|
this.pathname = parts.join('/')
|
|
@@ -84,7 +81,7 @@ export class AtUri {
|
|
|
84
81
|
|
|
85
82
|
set rkey(v: string) {
|
|
86
83
|
const parts = this.pathname.split('/').filter(Boolean)
|
|
87
|
-
|
|
84
|
+
parts[0] ||= 'undefined'
|
|
88
85
|
parts[1] = v
|
|
89
86
|
this.pathname = parts.join('/')
|
|
90
87
|
}
|
|
@@ -93,44 +90,60 @@ export class AtUri {
|
|
|
93
90
|
return this.toString()
|
|
94
91
|
}
|
|
95
92
|
|
|
96
|
-
toString() {
|
|
93
|
+
toString(): AtUriString {
|
|
97
94
|
let path = this.pathname || '/'
|
|
98
95
|
if (!path.startsWith('/')) {
|
|
99
96
|
path = `/${path}`
|
|
100
97
|
}
|
|
101
|
-
let qs =
|
|
102
|
-
if (
|
|
103
|
-
qs = `?${
|
|
98
|
+
let qs = ''
|
|
99
|
+
if (this.searchParams.size) {
|
|
100
|
+
qs = `?${this.searchParams.toString()}`
|
|
104
101
|
}
|
|
105
102
|
let hash = this.hash
|
|
106
103
|
if (hash && !hash.startsWith('#')) {
|
|
107
104
|
hash = `#${hash}`
|
|
108
105
|
}
|
|
109
|
-
return `at://${this.host}${path}${qs}${hash}`
|
|
106
|
+
return `at://${this.host}${path}${qs}${hash}` as AtUriString
|
|
110
107
|
}
|
|
111
108
|
}
|
|
112
109
|
|
|
113
110
|
function parse(str: string) {
|
|
114
|
-
const match =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
111
|
+
const match = str.match(ATP_URI_REGEX) as null | {
|
|
112
|
+
0: string
|
|
113
|
+
1: string | undefined // proto
|
|
114
|
+
2: string // host
|
|
115
|
+
3: string | undefined // path
|
|
116
|
+
4: string | undefined // query
|
|
117
|
+
5: string | undefined // hash
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!match) {
|
|
121
|
+
throw new Error(`Invalid AT uri: ${str}`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
host: match[2],
|
|
126
|
+
hash: match[5],
|
|
127
|
+
pathname: match[3],
|
|
128
|
+
searchParams: new URLSearchParams(match[4]),
|
|
122
129
|
}
|
|
123
|
-
return undefined
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
function parseRelative(str: string) {
|
|
127
|
-
const match =
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
const match = str.match(RELATIVE_REGEX) as null | {
|
|
134
|
+
0: string
|
|
135
|
+
1: string | undefined // path
|
|
136
|
+
2: string | undefined // query
|
|
137
|
+
3: string | undefined // hash
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!match) {
|
|
141
|
+
throw new Error(`Invalid path: ${str}`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
hash: match[3],
|
|
146
|
+
pathname: match[1],
|
|
147
|
+
searchParams: new URLSearchParams(match[2]),
|
|
134
148
|
}
|
|
135
|
-
return undefined
|
|
136
149
|
}
|
package/src/aturi_validation.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { AtIdentifierString, ensureValidAtIdentifier } from './at-identifier.js'
|
|
2
|
+
import { ensureValidDidRegex } from './did.js'
|
|
3
|
+
import { ensureValidHandleRegex } from './handle.js'
|
|
4
|
+
import { NsidString, isValidNsid } from './nsid.js'
|
|
5
|
+
|
|
6
|
+
export type AtUriString =
|
|
7
|
+
| `at://${AtIdentifierString}`
|
|
8
|
+
| `at://${AtIdentifierString}/${NsidString}`
|
|
9
|
+
| `at://${AtIdentifierString}/${NsidString}/${string}`
|
|
4
10
|
|
|
5
11
|
// Human-readable constraints on ATURI:
|
|
6
12
|
// - following regular URLs, a 8KByte hard total length limit
|
|
@@ -15,55 +21,76 @@ import { ensureValidNsid, ensureValidNsidRegex } from './nsid'
|
|
|
15
21
|
// [a-zA-Z0-9._~:@!$&'\(\)*+,;=-]
|
|
16
22
|
// - rkey must have at least one char
|
|
17
23
|
// - regardless of path component, a fragment can follow as "#" and then a JSON pointer (RFC-6901)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
|
|
24
|
+
|
|
25
|
+
export function ensureValidAtUri(input: string): asserts input is AtUriString {
|
|
26
|
+
const fragmentIndex = input.indexOf('#')
|
|
27
|
+
if (fragmentIndex !== -1) {
|
|
28
|
+
if (input.charCodeAt(fragmentIndex + 1) !== 47) {
|
|
29
|
+
throw new Error('ATURI fragment must be non-empty and start with slash')
|
|
30
|
+
}
|
|
31
|
+
if (input.includes('#', fragmentIndex + 1)) {
|
|
32
|
+
throw new Error('ATURI can have at most one "#", separating fragment out')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// NOTE: enforcing *some* checks here for sanity. Eg, at least no whitespace
|
|
36
|
+
const fragment = input.slice(fragmentIndex + 1)
|
|
37
|
+
if (!/^\/[a-zA-Z0-9._~:@!$&')(*+,;=%[\]/-]*$/.test(fragment)) {
|
|
38
|
+
throw new Error('Disallowed characters in ATURI fragment (ASCII)')
|
|
39
|
+
}
|
|
23
40
|
}
|
|
24
|
-
const fragmentPart = uriParts[1] || null
|
|
25
|
-
uri = uriParts[0]
|
|
26
41
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
42
|
+
const uri = fragmentIndex === -1 ? input : input.slice(0, fragmentIndex)
|
|
43
|
+
|
|
44
|
+
if (uri.length > 8 * 1024) {
|
|
45
|
+
throw new Error('ATURI is far too long')
|
|
30
46
|
}
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
if (parts.length >= 3 && (parts[0] !== 'at:' || parts[1].length !== 0)) {
|
|
48
|
+
if (!uri.startsWith('at://')) {
|
|
34
49
|
throw new Error('ATURI must start with "at://"')
|
|
35
50
|
}
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
|
|
52
|
+
// check that all chars are boring ASCII
|
|
53
|
+
if (!/^[a-zA-Z0-9._~:@!$&')(*+,;=%/-]*$/.test(uri)) {
|
|
54
|
+
throw new Error('Disallowed characters in ATURI (ASCII)')
|
|
38
55
|
}
|
|
39
56
|
|
|
57
|
+
const authorityEnd = uri.indexOf('/', 5)
|
|
58
|
+
const authority =
|
|
59
|
+
authorityEnd === -1 ? uri.slice(5) : uri.slice(5, authorityEnd)
|
|
40
60
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
ensureValidHandle(parts[2])
|
|
45
|
-
}
|
|
46
|
-
} catch {
|
|
47
|
-
throw new Error('ATURI authority must be a valid handle or DID')
|
|
61
|
+
ensureValidAtIdentifier(authority)
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
throw new Error('ATURI authority must be a valid handle or DID', { cause })
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
const collectionStart = authorityEnd === -1 ? -1 : authorityEnd + 1
|
|
67
|
+
const collectionEnd =
|
|
68
|
+
collectionStart === -1 ? -1 : uri.indexOf('/', collectionStart)
|
|
69
|
+
|
|
70
|
+
if (collectionStart !== -1) {
|
|
71
|
+
const collection =
|
|
72
|
+
collectionEnd === -1
|
|
73
|
+
? uri.slice(collectionStart)
|
|
74
|
+
: uri.slice(collectionStart, collectionEnd)
|
|
75
|
+
|
|
76
|
+
if (collection.length === 0) {
|
|
52
77
|
throw new Error(
|
|
53
78
|
'ATURI can not have a slash after authority without a path segment',
|
|
54
79
|
)
|
|
55
80
|
}
|
|
56
|
-
|
|
57
|
-
ensureValidNsid(parts[3])
|
|
58
|
-
} catch {
|
|
81
|
+
if (!isValidNsid(collection)) {
|
|
59
82
|
throw new Error(
|
|
60
83
|
'ATURI requires first path segment (if supplied) to be valid NSID',
|
|
61
84
|
)
|
|
62
85
|
}
|
|
63
86
|
}
|
|
64
87
|
|
|
65
|
-
|
|
66
|
-
|
|
88
|
+
const recordKeyStart = collectionEnd === -1 ? -1 : collectionEnd + 1
|
|
89
|
+
const recordKeyEnd =
|
|
90
|
+
recordKeyStart === -1 ? -1 : uri.indexOf('/', recordKeyStart)
|
|
91
|
+
|
|
92
|
+
if (recordKeyStart !== -1) {
|
|
93
|
+
if (recordKeyStart === uri.length) {
|
|
67
94
|
throw new Error(
|
|
68
95
|
'ATURI can not have a slash after collection, unless record key is provided',
|
|
69
96
|
)
|
|
@@ -71,32 +98,14 @@ export const ensureValidAtUri = (uri: string) => {
|
|
|
71
98
|
// would validate rkey here, but there are basically no constraints!
|
|
72
99
|
}
|
|
73
100
|
|
|
74
|
-
if (
|
|
101
|
+
if (recordKeyEnd !== -1) {
|
|
75
102
|
throw new Error(
|
|
76
103
|
'ATURI path can have at most two parts, and no trailing slash',
|
|
77
104
|
)
|
|
78
105
|
}
|
|
79
|
-
|
|
80
|
-
if (uriParts.length >= 2 && fragmentPart == null) {
|
|
81
|
-
throw new Error('ATURI fragment must be non-empty and start with slash')
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (fragmentPart != null) {
|
|
85
|
-
if (fragmentPart.length === 0 || fragmentPart[0] !== '/') {
|
|
86
|
-
throw new Error('ATURI fragment must be non-empty and start with slash')
|
|
87
|
-
}
|
|
88
|
-
// NOTE: enforcing *some* checks here for sanity. Eg, at least no whitespace
|
|
89
|
-
if (!/^\/[a-zA-Z0-9._~:@!$&')(*+,;=%[\]/-]*$/.test(fragmentPart)) {
|
|
90
|
-
throw new Error('Disallowed characters in ATURI fragment (ASCII)')
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (uri.length > 8 * 1024) {
|
|
95
|
-
throw new Error('ATURI is far too long')
|
|
96
|
-
}
|
|
97
106
|
}
|
|
98
107
|
|
|
99
|
-
export
|
|
108
|
+
export function ensureValidAtUriRegex(uri: string): asserts uri is AtUriString {
|
|
100
109
|
// simple regex to enforce most constraints via just regex and length.
|
|
101
110
|
// hand wrote this regex based on above constraints. whew!
|
|
102
111
|
const aturiRegex =
|
|
@@ -117,12 +126,8 @@ export const ensureValidAtUriRegex = (uri: string): void => {
|
|
|
117
126
|
}
|
|
118
127
|
}
|
|
119
128
|
|
|
120
|
-
if (groups.collection) {
|
|
121
|
-
|
|
122
|
-
ensureValidNsidRegex(groups.collection)
|
|
123
|
-
} catch {
|
|
124
|
-
throw new Error('ATURI collection path segment must be a valid NSID')
|
|
125
|
-
}
|
|
129
|
+
if (groups.collection && !isValidNsid(groups.collection)) {
|
|
130
|
+
throw new Error('ATURI collection path segment must be a valid NSID')
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
if (uri.length > 8 * 1024) {
|
package/src/datetime.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
+
/** An ISO 8601 formatted datetime string (YYYY-MM-DDTHH:mm:ss.sssZ) */
|
|
2
|
+
export type DatetimeString =
|
|
3
|
+
`${string}-${string}-${string}T${string}:${string}:${string}${'Z' | `+${string}` | `-${string}`}`
|
|
4
|
+
|
|
5
|
+
// Allow date.toISOString() to be used where datetime format is expected
|
|
6
|
+
declare global {
|
|
7
|
+
interface Date {
|
|
8
|
+
toISOString(): `${string}-${string}-${string}T${string}:${string}:${string}Z`
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
1
12
|
/* Validates datetime string against atproto Lexicon 'datetime' format.
|
|
2
13
|
* Syntax is described at: https://atproto.com/specs/lexicon#datetime
|
|
3
14
|
*/
|
|
4
|
-
export
|
|
15
|
+
export function ensureValidDatetime(
|
|
16
|
+
dtStr: string,
|
|
17
|
+
): asserts dtStr is DatetimeString {
|
|
5
18
|
const date = new Date(dtStr)
|
|
6
19
|
// must parse as ISO 8601; this also verifies semantics like month is not 13 or 00
|
|
7
20
|
if (isNaN(date.getTime())) {
|
|
@@ -33,7 +46,7 @@ export const ensureValidDatetime = (dtStr: string): void => {
|
|
|
33
46
|
|
|
34
47
|
/* Same logic as ensureValidDatetime(), but returns a boolean instead of throwing an exception.
|
|
35
48
|
*/
|
|
36
|
-
export
|
|
49
|
+
export function isValidDatetime(dtStr: string): dtStr is DatetimeString {
|
|
37
50
|
try {
|
|
38
51
|
ensureValidDatetime(dtStr)
|
|
39
52
|
} catch (err) {
|
|
@@ -56,7 +69,7 @@ export const isValidDatetime = (dtStr: string): boolean => {
|
|
|
56
69
|
*
|
|
57
70
|
* Expected output format: YYYY-MM-DDTHH:mm:ss.sssZ
|
|
58
71
|
*/
|
|
59
|
-
export
|
|
72
|
+
export function normalizeDatetime(dtStr: string): DatetimeString {
|
|
60
73
|
if (isValidDatetime(dtStr)) {
|
|
61
74
|
const outStr = new Date(dtStr).toISOString()
|
|
62
75
|
if (isValidDatetime(outStr)) {
|
|
@@ -96,7 +109,7 @@ export const normalizeDatetime = (dtStr: string): string => {
|
|
|
96
109
|
*
|
|
97
110
|
* If a InvalidDatetimeError is encountered, returns the UNIX epoch time as a UTC datetime (1970-01-01T00:00:00.000Z).
|
|
98
111
|
*/
|
|
99
|
-
export const normalizeDatetimeAlways = (dtStr: string):
|
|
112
|
+
export const normalizeDatetimeAlways = (dtStr: string): DatetimeString => {
|
|
100
113
|
try {
|
|
101
114
|
return normalizeDatetime(dtStr)
|
|
102
115
|
} catch (err) {
|
package/src/did.ts
CHANGED
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
// - in current atproto, only allowing did:plc and did:web. But not *forcing* this at lexicon layer
|
|
12
12
|
// - hard length limit of 8KBytes
|
|
13
13
|
// - not going to validate "percent encoding" here
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
export type DidString<M extends string = string> = `did:${M}:${string}`
|
|
16
|
+
|
|
17
|
+
export function ensureValidDid(did: string): asserts did is DidString {
|
|
15
18
|
if (!did.startsWith('did:')) {
|
|
16
19
|
throw new InvalidDidError('DID requires "did:" prefix')
|
|
17
20
|
}
|
|
@@ -43,7 +46,7 @@ export const ensureValidDid = (did: string): void => {
|
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
export
|
|
49
|
+
export function ensureValidDidRegex(did: string): asserts did is DidString {
|
|
47
50
|
// simple regex to enforce most constraints via just regex and length.
|
|
48
51
|
// hand wrote this regex based on above constraints
|
|
49
52
|
if (!/^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/.test(did)) {
|
package/src/handle.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export const INVALID_HANDLE = 'handle.invalid'
|
|
2
2
|
|
|
3
|
+
export type HandleString = `${string}.${string}`
|
|
4
|
+
|
|
3
5
|
// Currently these are registration-time restrictions, not protocol-level
|
|
4
6
|
// restrictions. We have a couple accounts in the wild that we need to clean up
|
|
5
7
|
// before hard-disallow.
|
|
@@ -37,7 +39,9 @@ export const DISALLOWED_TLDS = [
|
|
|
37
39
|
// - does not validate whether domain or TLD exists, or is a reserved or
|
|
38
40
|
// special TLD (eg, .onion or .local)
|
|
39
41
|
// - does not validate punycode
|
|
40
|
-
export
|
|
42
|
+
export function ensureValidHandle(
|
|
43
|
+
handle: string,
|
|
44
|
+
): asserts handle is HandleString {
|
|
41
45
|
// check that all chars are boring ASCII
|
|
42
46
|
if (!/^[a-zA-Z0-9.-]*$/.test(handle)) {
|
|
43
47
|
throw new InvalidHandleError(
|
|
@@ -74,46 +78,43 @@ export const ensureValidHandle = (handle: string): void => {
|
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
// simple regex translation of above constraints
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
throw new InvalidHandleError("Handle didn't validate via regex")
|
|
84
|
-
}
|
|
81
|
+
const HANDLE_REGEX =
|
|
82
|
+
/^([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])?$/
|
|
83
|
+
|
|
84
|
+
export function ensureValidHandleRegex(
|
|
85
|
+
handle: string,
|
|
86
|
+
): asserts handle is HandleString {
|
|
85
87
|
if (handle.length > 253) {
|
|
86
88
|
throw new InvalidHandleError('Handle is too long (253 chars max)')
|
|
87
89
|
}
|
|
90
|
+
if (!HANDLE_REGEX.test(handle)) {
|
|
91
|
+
throw new InvalidHandleError("Handle didn't validate via regex")
|
|
92
|
+
}
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
export
|
|
95
|
+
export function normalizeHandle(handle: string): string {
|
|
91
96
|
return handle.toLowerCase()
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
export
|
|
99
|
+
export function normalizeAndEnsureValidHandle(handle: string): HandleString {
|
|
95
100
|
const normalized = normalizeHandle(handle)
|
|
96
101
|
ensureValidHandle(normalized)
|
|
97
102
|
return normalized
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
export
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
export function isValidHandle(handle: string): handle is HandleString {
|
|
106
|
+
return handle.length <= 253 && HANDLE_REGEX.test(handle)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isValidTld(handle: string): boolean {
|
|
110
|
+
for (const tld of DISALLOWED_TLDS) {
|
|
111
|
+
if (handle.endsWith(tld)) {
|
|
105
112
|
return false
|
|
106
113
|
}
|
|
107
|
-
throw err
|
|
108
114
|
}
|
|
109
|
-
|
|
110
115
|
return true
|
|
111
116
|
}
|
|
112
117
|
|
|
113
|
-
export const isValidTld = (handle: string): boolean => {
|
|
114
|
-
return !DISALLOWED_TLDS.some((domain) => handle.endsWith(domain))
|
|
115
|
-
}
|
|
116
|
-
|
|
117
118
|
export class InvalidHandleError extends Error {}
|
|
118
119
|
/** @deprecated Never used */
|
|
119
120
|
export class ReservedHandleError extends Error {}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export * from './handle'
|
|
2
|
-
export * from './did'
|
|
3
|
-
export * from './nsid'
|
|
4
|
-
export * from './aturi'
|
|
5
|
-
export * from './
|
|
6
|
-
export * from './
|
|
7
|
-
export * from './
|
|
1
|
+
export * from './handle.js'
|
|
2
|
+
export * from './did.js'
|
|
3
|
+
export * from './nsid.js'
|
|
4
|
+
export * from './aturi.js'
|
|
5
|
+
export * from './at-identifier.js'
|
|
6
|
+
export * from './tid.js'
|
|
7
|
+
export * from './recordkey.js'
|
|
8
|
+
export * from './datetime.js'
|