@atproto/did 0.1.0

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/src/did.ts ADDED
@@ -0,0 +1,258 @@
1
+ import { z } from 'zod'
2
+ import { DidError, InvalidDidError } from './did-error.js'
3
+
4
+ const DID_PREFIX = 'did:'
5
+ const DID_PREFIX_LENGTH = DID_PREFIX.length
6
+ export { DID_PREFIX }
7
+
8
+ /**
9
+ * Type representation of a Did, with method.
10
+ *
11
+ * ```bnf
12
+ * did = "did:" method-name ":" method-specific-id
13
+ * method-name = 1*method-char
14
+ * method-char = %x61-7A / DIGIT
15
+ * method-specific-id = *( *idchar ":" ) 1*idchar
16
+ * idchar = ALPHA / DIGIT / "." / "-" / "_" / pct-encoded
17
+ * pct-encoded = "%" HEXDIG HEXDIG
18
+ * ```
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * type DidWeb = Did<'web'> // `did:web:${string}`
23
+ * type DidCustom = Did<'web' | 'plc'> // `did:${'web' | 'plc'}:${string}`
24
+ * type DidNever = Did<' invalid 🥴 '> // never
25
+ * type DidFoo = Did<'foo' | ' invalid 🥴 '> // `did:foo:${string}`
26
+ * ```
27
+ *
28
+ * @see {@link https://www.w3.org/TR/did-core/#did-syntax}
29
+ */
30
+ export type Did<M extends string = string> = `did:${AsDidMethod<M>}:${string}`
31
+
32
+ /**
33
+ * DID Method
34
+ */
35
+ export type AsDidMethod<M> = string extends M
36
+ ? string // can't know...
37
+ : AsDidMethodInternal<M, ''>
38
+
39
+ type AlphanumericChar = DigitChar | LowerAlphaChar
40
+ type DigitChar = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
41
+ type LowerAlphaChar =
42
+ | 'a'
43
+ | 'b'
44
+ | 'c'
45
+ | 'd'
46
+ | 'e'
47
+ | 'f'
48
+ | 'g'
49
+ | 'h'
50
+ | 'i'
51
+ | 'j'
52
+ | 'k'
53
+ | 'l'
54
+ | 'm'
55
+ | 'n'
56
+ | 'o'
57
+ | 'p'
58
+ | 'q'
59
+ | 'r'
60
+ | 's'
61
+ | 't'
62
+ | 'u'
63
+ | 'v'
64
+ | 'w'
65
+ | 'x'
66
+ | 'y'
67
+ | 'z'
68
+
69
+ type AsDidMethodInternal<
70
+ S,
71
+ Acc extends string,
72
+ > = S extends `${infer H}${infer T}`
73
+ ? H extends AlphanumericChar
74
+ ? AsDidMethodInternal<T, `${Acc}${H}`>
75
+ : never
76
+ : Acc extends ''
77
+ ? never
78
+ : Acc
79
+
80
+ /**
81
+ * DID Method-name check function.
82
+ *
83
+ * Check if the input is a valid DID method name, at the position between
84
+ * `start` (inclusive) and `end` (exclusive).
85
+ */
86
+ export function checkDidMethod(
87
+ input: string,
88
+ start = 0,
89
+ end = input.length,
90
+ ): void {
91
+ if (
92
+ !Number.isFinite(end) ||
93
+ !Number.isFinite(start) ||
94
+ end < start ||
95
+ end > input.length
96
+ ) {
97
+ throw new TypeError('Invalid start or end position')
98
+ }
99
+ if (end === start) {
100
+ throw new InvalidDidError(input, `Empty method name`)
101
+ }
102
+
103
+ let c: number
104
+ for (let i = start; i < end; i++) {
105
+ c = input.charCodeAt(i)
106
+ if (
107
+ (c < 0x61 || c > 0x7a) && // a-z
108
+ (c < 0x30 || c > 0x39) // 0-9
109
+ ) {
110
+ throw new InvalidDidError(
111
+ input,
112
+ `Invalid character at position ${i} in DID method name`,
113
+ )
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * This method assumes the input is a valid Did
120
+ */
121
+ export function extractDidMethod<D extends Did>(did: D) {
122
+ const msidSep = did.indexOf(':', DID_PREFIX_LENGTH)
123
+ const method = did.slice(DID_PREFIX_LENGTH, msidSep)
124
+ return method as D extends Did<infer M> ? M : string
125
+ }
126
+
127
+ /**
128
+ * DID Method-specific identifier check function.
129
+ *
130
+ * Check if the input is a valid DID method-specific identifier, at the position
131
+ * between `start` (inclusive) and `end` (exclusive).
132
+ */
133
+ export function checkDidMsid(
134
+ input: string,
135
+ start = 0,
136
+ end = input.length,
137
+ ): void {
138
+ if (
139
+ !Number.isFinite(end) ||
140
+ !Number.isFinite(start) ||
141
+ end < start ||
142
+ end > input.length
143
+ ) {
144
+ throw new TypeError('Invalid start or end position')
145
+ }
146
+ if (end === start) {
147
+ throw new InvalidDidError(input, `DID method-specific id must not be empty`)
148
+ }
149
+
150
+ let c: number
151
+ for (let i = start; i < end; i++) {
152
+ c = input.charCodeAt(i)
153
+
154
+ // Check for frequent chars first
155
+ if (
156
+ (c < 0x61 || c > 0x7a) && // a-z
157
+ (c < 0x41 || c > 0x5a) && // A-Z
158
+ (c < 0x30 || c > 0x39) && // 0-9
159
+ c !== 0x2e && // .
160
+ c !== 0x2d && // -
161
+ c !== 0x5f // _
162
+ ) {
163
+ // Less frequent chars are checked here
164
+
165
+ // ":"
166
+ if (c === 0x3a) {
167
+ if (i === end - 1) {
168
+ throw new InvalidDidError(input, `DID cannot end with ":"`)
169
+ }
170
+ continue
171
+ }
172
+
173
+ // pct-encoded
174
+ if (c === 0x25) {
175
+ c = input.charCodeAt(++i)
176
+ if ((c < 0x30 || c > 0x39) && (c < 0x41 || c > 0x46)) {
177
+ throw new InvalidDidError(
178
+ input,
179
+ `Invalid pct-encoded character at position ${i}`,
180
+ )
181
+ }
182
+
183
+ c = input.charCodeAt(++i)
184
+ if ((c < 0x30 || c > 0x39) && (c < 0x41 || c > 0x46)) {
185
+ throw new InvalidDidError(
186
+ input,
187
+ `Invalid pct-encoded character at position ${i}`,
188
+ )
189
+ }
190
+
191
+ // There must always be 2 HEXDIG after a "%"
192
+ if (i >= end) {
193
+ throw new InvalidDidError(
194
+ input,
195
+ `Incomplete pct-encoded character at position ${i - 2}`,
196
+ )
197
+ }
198
+
199
+ continue
200
+ }
201
+
202
+ throw new InvalidDidError(
203
+ input,
204
+ `Disallowed character in DID at position ${i}`,
205
+ )
206
+ }
207
+ }
208
+ }
209
+
210
+ export function checkDid(input: unknown): asserts input is Did {
211
+ if (typeof input !== 'string') {
212
+ throw new InvalidDidError(typeof input, `DID must be a string`)
213
+ }
214
+
215
+ const { length } = input
216
+ if (length > 2048) {
217
+ throw new InvalidDidError(input, `DID is too long (2048 chars max)`)
218
+ }
219
+
220
+ if (!input.startsWith(DID_PREFIX)) {
221
+ throw new InvalidDidError(input, `DID requires "${DID_PREFIX}" prefix`)
222
+ }
223
+
224
+ const idSep = input.indexOf(':', DID_PREFIX_LENGTH)
225
+ if (idSep === -1) {
226
+ throw new InvalidDidError(input, `Missing colon after method name`)
227
+ }
228
+
229
+ checkDidMethod(input, DID_PREFIX_LENGTH, idSep)
230
+ checkDidMsid(input, idSep + 1, length)
231
+ }
232
+
233
+ export function isDid(input: unknown): input is Did {
234
+ try {
235
+ checkDid(input)
236
+ return true
237
+ } catch (err) {
238
+ if (err instanceof DidError) {
239
+ return false
240
+ }
241
+ throw err
242
+ }
243
+ }
244
+
245
+ export const didSchema = z
246
+ .string()
247
+ .superRefine((value: string, ctx: z.RefinementCtx): value is Did => {
248
+ try {
249
+ checkDid(value)
250
+ return true
251
+ } catch (err) {
252
+ ctx.addIssue({
253
+ code: z.ZodIssueCode.custom,
254
+ message: err instanceof Error ? err.message : 'Unexpected error',
255
+ })
256
+ return false
257
+ }
258
+ })
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './did-document.js'
2
+ export * from './did-error.js'
3
+ export * from './did.js'
4
+ export * from './methods.js'
@@ -0,0 +1,40 @@
1
+ import { InvalidDidError } from '../did-error.js'
2
+ import { Did } from '../did.js'
3
+
4
+ const DID_PLC_PREFIX = `did:plc:`
5
+ const DID_PLC_PREFIX_LENGTH = DID_PLC_PREFIX.length
6
+ const DID_PLC_LENGTH = 32
7
+
8
+ export { DID_PLC_PREFIX }
9
+
10
+ export function isDidPlc(input: unknown): input is Did<'plc'> {
11
+ if (typeof input !== 'string') return false
12
+ try {
13
+ checkDidPlc(input)
14
+ return true
15
+ } catch {
16
+ return false
17
+ }
18
+ }
19
+
20
+ export function checkDidPlc(input: string): asserts input is Did<'plc'> {
21
+ if (input.length !== DID_PLC_LENGTH) {
22
+ throw new InvalidDidError(
23
+ input,
24
+ `did:plc must be ${DID_PLC_LENGTH} characters long`,
25
+ )
26
+ }
27
+
28
+ if (!input.startsWith(DID_PLC_PREFIX)) {
29
+ throw new InvalidDidError(input, `Invalid did:plc prefix`)
30
+ }
31
+
32
+ let c: number
33
+ for (let i = DID_PLC_PREFIX_LENGTH; i < DID_PLC_LENGTH; i++) {
34
+ c = input.charCodeAt(i)
35
+ // Base32 encoding ([a-z2-7])
36
+ if ((c < 0x61 || c > 0x7a) && (c < 0x32 || c > 0x37)) {
37
+ throw new InvalidDidError(input, `Invalid character at position ${i}`)
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,78 @@
1
+ import { InvalidDidError } from '../did-error.js'
2
+ import { Did, checkDidMsid } from '../did.js'
3
+
4
+ export const DID_WEB_PREFIX = `did:web:`
5
+
6
+ /**
7
+ * This function checks if the input is a valid Web DID, as per DID spec.
8
+ * ATPROTO adds additional constraints to allowed DID values for the `did:web`
9
+ * method. Use {@link isAtprotoDidWeb} if that's what you need.
10
+ */
11
+ export function isDidWeb(input: unknown): input is Did<'web'> {
12
+ if (typeof input !== 'string') return false
13
+ try {
14
+ didWebToUrl(input)
15
+ return true
16
+ } catch {
17
+ return false
18
+ }
19
+ }
20
+
21
+ /**
22
+ * @see {@link https://atproto.com/specs/did#blessed-did-methods}
23
+ */
24
+ export function isAtprotoDidWeb(input: unknown): input is Did<'web'> {
25
+ // Optimization: make cheap checks first
26
+ if (typeof input !== 'string') {
27
+ return false
28
+ }
29
+
30
+ // Path are not allowed
31
+ if (input.includes(':', DID_WEB_PREFIX.length)) {
32
+ return false
33
+ }
34
+
35
+ // Port numbers are not allowed, except for localhost
36
+ if (
37
+ input.includes('%3A', DID_WEB_PREFIX.length) &&
38
+ !input.startsWith('did:web:localhost%3A')
39
+ ) {
40
+ return false
41
+ }
42
+
43
+ return isDidWeb(input)
44
+ }
45
+
46
+ export function checkDidWeb(input: string): asserts input is Did<'web'> {
47
+ didWebToUrl(input)
48
+ }
49
+
50
+ export function didWebToUrl(did: string): URL {
51
+ if (!did.startsWith(DID_WEB_PREFIX)) {
52
+ throw new InvalidDidError(did, `did:web must start with ${DID_WEB_PREFIX}`)
53
+ }
54
+
55
+ if (did.charAt(DID_WEB_PREFIX.length) === ':') {
56
+ throw new InvalidDidError(did, 'did:web MSID must not start with a colon')
57
+ }
58
+
59
+ // Make sure every char is valid (per DID spec)
60
+ checkDidMsid(did, DID_WEB_PREFIX.length)
61
+
62
+ try {
63
+ const msid = did.slice(DID_WEB_PREFIX.length)
64
+ const parts = msid.split(':').map(decodeURIComponent)
65
+ return new URL(`https://${parts.join('/')}`)
66
+ } catch (cause) {
67
+ throw new InvalidDidError(did, 'Invalid Web DID', cause)
68
+ }
69
+ }
70
+
71
+ export function urlToDidWeb(url: URL): Did<'web'> {
72
+ const path =
73
+ url.pathname === '/'
74
+ ? ''
75
+ : url.pathname.slice(1).split('/').map(encodeURIComponent).join(':')
76
+
77
+ return `did:web:${encodeURIComponent(url.host)}${path ? `:${path}` : ''}`
78
+ }
package/src/methods.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './methods/plc.js'
2
+ export * from './methods/web.js'
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig/isomorphic.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./dist"
6
+ },
7
+ "include": ["./src"]
8
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "include": [],
3
+ "references": [{ "path": "./tsconfig.build.json" }]
4
+ }