@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/CHANGELOG.md +7 -0
- package/LICENSE.txt +7 -0
- package/dist/did-document.d.ts +335 -0
- package/dist/did-document.d.ts.map +1 -0
- package/dist/did-document.js +135 -0
- package/dist/did-document.js.map +1 -0
- package/dist/did-error.d.ts +16 -0
- package/dist/did-error.d.ts.map +1 -0
- package/dist/did-error.js +58 -0
- package/dist/did-error.js.map +1 -0
- package/dist/did.d.ts +56 -0
- package/dist/did.d.ts.map +1 -0
- package/dist/did.js +148 -0
- package/dist/did.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/methods/plc.d.ts +6 -0
- package/dist/methods/plc.d.ts.map +1 -0
- package/dist/methods/plc.js +38 -0
- package/dist/methods/plc.js.map +1 -0
- package/dist/methods/web.d.ts +16 -0
- package/dist/methods/web.d.ts.map +1 -0
- package/dist/methods/web.js +74 -0
- package/dist/methods/web.js.map +1 -0
- package/dist/methods.d.ts +3 -0
- package/dist/methods.d.ts.map +1 -0
- package/dist/methods.js +19 -0
- package/dist/methods.js.map +1 -0
- package/package.json +36 -0
- package/src/did-document.ts +151 -0
- package/src/did-error.ts +49 -0
- package/src/did.ts +258 -0
- package/src/index.ts +4 -0
- package/src/methods/plc.ts +40 -0
- package/src/methods/web.ts +78 -0
- package/src/methods.ts +2 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +4 -0
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,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
package/tsconfig.json
ADDED