@atproto/identity 0.0.1 → 0.2.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/dist/types.d.ts CHANGED
@@ -3,9 +3,11 @@ export declare type IdentityResolverOpts = {
3
3
  timeout?: number;
4
4
  plcUrl?: string;
5
5
  didCache?: DidCache;
6
+ backupNameservers?: string[];
6
7
  };
7
8
  export declare type HandleResolverOpts = {
8
9
  timeout?: number;
10
+ backupNameservers?: string[];
9
11
  };
10
12
  export declare type DidResolverOpts = {
11
13
  timeout?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/identity",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,13 +10,16 @@ export const getDid = (doc: DidDocument): string => {
10
10
  }
11
11
 
12
12
  export const getKey = (doc: DidDocument): string | undefined => {
13
+ const did = getDid(doc)
13
14
  let keys = doc.verificationMethod
14
15
  if (!keys) return undefined
15
16
  if (typeof keys !== 'object') return undefined
16
17
  if (!Array.isArray(keys)) {
17
18
  keys = [keys]
18
19
  }
19
- const found = keys.find((key) => key.id === '#atproto')
20
+ const found = keys.find(
21
+ (key) => key.id === '#atproto' || key.id === `${did}#atproto`,
22
+ )
20
23
  if (!found) return undefined
21
24
 
22
25
  // @TODO support jwk
@@ -28,6 +31,9 @@ export const getKey = (doc: DidDocument): string | undefined => {
28
31
  didKey = crypto.formatDidKey(crypto.P256_JWT_ALG, keyBytes)
29
32
  } else if (found.type === 'EcdsaSecp256k1VerificationKey2019') {
30
33
  didKey = crypto.formatDidKey(crypto.SECP256K1_JWT_ALG, keyBytes)
34
+ } else if (found.type === 'Multikey') {
35
+ const parsed = crypto.parseMultikey(found.publicKeyMultibase)
36
+ didKey = crypto.formatDidKey(parsed.jwtAlg, parsed.keyBytes)
31
37
  }
32
38
  return didKey
33
39
  }
@@ -42,41 +48,24 @@ export const getHandle = (doc: DidDocument): string | undefined => {
42
48
  }
43
49
 
44
50
  export const getPds = (doc: DidDocument): string | undefined => {
45
- let services = doc.service
46
- if (!services) return undefined
47
- if (typeof services !== 'object') return undefined
48
- if (!Array.isArray(services)) {
49
- services = [services]
50
- }
51
- const found = services.find((service) => service.id === '#atproto_pds')
52
- if (!found) return undefined
53
- if (found.type !== 'AtprotoPersonalDataServer') {
54
- return undefined
55
- }
56
- if (typeof found.serviceEndpoint !== 'string') {
57
- return undefined
58
- }
59
- validateUrl(found.serviceEndpoint)
60
- return found.serviceEndpoint
51
+ return getServiceEndpoint(doc, {
52
+ id: '#atproto_pds',
53
+ type: 'AtprotoPersonalDataServer',
54
+ })
61
55
  }
62
56
 
63
57
  export const getFeedGen = (doc: DidDocument): string | undefined => {
64
- let services = doc.service
65
- if (!services) return undefined
66
- if (typeof services !== 'object') return undefined
67
- if (!Array.isArray(services)) {
68
- services = [services]
69
- }
70
- const found = services.find((service) => service.id === '#bsky_fg')
71
- if (!found) return undefined
72
- if (found.type !== 'BskyFeedGenerator') {
73
- return undefined
74
- }
75
- if (typeof found.serviceEndpoint !== 'string') {
76
- return undefined
77
- }
78
- validateUrl(found.serviceEndpoint)
79
- return found.serviceEndpoint
58
+ return getServiceEndpoint(doc, {
59
+ id: '#bsky_fg',
60
+ type: 'BskyFeedGenerator',
61
+ })
62
+ }
63
+
64
+ export const getNotif = (doc: DidDocument): string | undefined => {
65
+ return getServiceEndpoint(doc, {
66
+ id: '#bsky_notif',
67
+ type: 'BskyNotificationService',
68
+ })
80
69
  }
81
70
 
82
71
  export const parseToAtprotoDocument = (
@@ -118,3 +107,28 @@ const validateUrl = (url: string) => {
118
107
  throw new Error('Invalid pds hostname')
119
108
  }
120
109
  }
110
+
111
+ const getServiceEndpoint = (
112
+ doc: DidDocument,
113
+ opts: { id: string; type: string },
114
+ ) => {
115
+ const did = getDid(doc)
116
+ let services = doc.service
117
+ if (!services) return undefined
118
+ if (typeof services !== 'object') return undefined
119
+ if (!Array.isArray(services)) {
120
+ services = [services]
121
+ }
122
+ const found = services.find(
123
+ (service) => service.id === opts.id || service.id === `${did}${opts.id}`,
124
+ )
125
+ if (!found) return undefined
126
+ if (found.type !== opts.type) {
127
+ return undefined
128
+ }
129
+ if (typeof found.serviceEndpoint !== 'string') {
130
+ return undefined
131
+ }
132
+ validateUrl(found.serviceEndpoint)
133
+ return found.serviceEndpoint
134
+ }
@@ -6,9 +6,12 @@ const PREFIX = 'did='
6
6
 
7
7
  export class HandleResolver {
8
8
  public timeout: number
9
+ private backupNameservers: string[] | undefined
10
+ private backupNameserverIps: string[] | undefined
9
11
 
10
12
  constructor(opts: HandleResolverOpts = {}) {
11
13
  this.timeout = opts.timeout ?? 3000
14
+ this.backupNameservers = opts.backupNameservers
12
15
  }
13
16
 
14
17
  async resolve(handle: string): Promise<string | undefined> {
@@ -23,7 +26,11 @@ export class HandleResolver {
23
26
  httpAbort.abort()
24
27
  return dnsRes
25
28
  }
26
- return httpPromise
29
+ const res = await httpPromise
30
+ if (res) {
31
+ return res
32
+ }
33
+ return this.resolveDnsBackup(handle)
27
34
  }
28
35
 
29
36
  async resolveDns(handle: string): Promise<string | undefined> {
@@ -33,12 +40,7 @@ export class HandleResolver {
33
40
  } catch (err) {
34
41
  return undefined
35
42
  }
36
- const results = chunkedResults.map((chunks) => chunks.join(''))
37
- const found = results.filter((i) => i.startsWith(PREFIX))
38
- if (found.length !== 1) {
39
- return undefined
40
- }
41
- return found[0].slice(PREFIX.length)
43
+ return this.parseDnsResult(chunkedResults)
42
44
  }
43
45
 
44
46
  async resolveHttp(
@@ -57,4 +59,44 @@ export class HandleResolver {
57
59
  return undefined
58
60
  }
59
61
  }
62
+
63
+ async resolveDnsBackup(handle: string): Promise<string | undefined> {
64
+ let chunkedResults: string[][]
65
+ try {
66
+ const backupIps = await this.getBackupNameserverIps()
67
+ if (!backupIps || backupIps.length < 1) return undefined
68
+ const resolver = new dns.Resolver()
69
+ resolver.setServers(backupIps)
70
+ chunkedResults = await resolver.resolveTxt(`${SUBDOMAIN}.${handle}`)
71
+ } catch (err) {
72
+ return undefined
73
+ }
74
+ return this.parseDnsResult(chunkedResults)
75
+ }
76
+
77
+ parseDnsResult(chunkedResults: string[][]): string | undefined {
78
+ const results = chunkedResults.map((chunks) => chunks.join(''))
79
+ const found = results.filter((i) => i.startsWith(PREFIX))
80
+ if (found.length !== 1) {
81
+ return undefined
82
+ }
83
+ return found[0].slice(PREFIX.length)
84
+ }
85
+
86
+ private async getBackupNameserverIps(): Promise<string[] | undefined> {
87
+ if (!this.backupNameservers) {
88
+ return undefined
89
+ } else if (!this.backupNameserverIps) {
90
+ const responses = await Promise.allSettled(
91
+ this.backupNameservers.map((h) => dns.lookup(h)),
92
+ )
93
+ for (const res of responses) {
94
+ if (res.status === 'fulfilled') {
95
+ this.backupNameserverIps ??= []
96
+ this.backupNameserverIps.push(res.value.address)
97
+ }
98
+ }
99
+ }
100
+ return this.backupNameserverIps
101
+ }
60
102
  }
@@ -8,7 +8,10 @@ export class IdResolver {
8
8
 
9
9
  constructor(opts: IdentityResolverOpts = {}) {
10
10
  const { timeout = 3000, plcUrl, didCache } = opts
11
- this.handle = new HandleResolver({ timeout })
11
+ this.handle = new HandleResolver({
12
+ timeout,
13
+ backupNameservers: opts.backupNameservers,
14
+ })
12
15
  this.did = new DidResolver({ timeout, plcUrl, didCache })
13
16
  }
14
17
  }
package/src/types.ts CHANGED
@@ -4,10 +4,12 @@ export type IdentityResolverOpts = {
4
4
  timeout?: number
5
5
  plcUrl?: string
6
6
  didCache?: DidCache
7
+ backupNameservers?: string[]
7
8
  }
8
9
 
9
10
  export type HandleResolverOpts = {
10
11
  timeout?: number
12
+ backupNameservers?: string[]
11
13
  }
12
14
 
13
15
  export type DidResolverOpts = {
package/test.log CHANGED
@@ -4814,3 +4814,18 @@
4814
4814
  {"level":30,"time":1684278727274,"pid":65486,"hostname":"Daniels-MBP.attlocal.net","req":{"id":5,"method":"GET","url":"/did%3Aplc%3Aasdf","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/0.27.2","host":"localhost:51012","connection":"close"},"remoteAddress":"::1","remotePort":51026},"res":{"statusCode":404,"headers":{"x-powered-by":"Express","access-control-allow-origin":"*","content-type":"application/json; charset=utf-8","content-length":"46","etag":"W/\"2e-ouyx9dlGpAF6gs2puC1AAUwbx84\""}},"responseTime":321,"msg":"request completed"}
4815
4815
  {"level":30,"time":1684278727276,"pid":65486,"hostname":"Daniels-MBP.attlocal.net","req":{"id":6,"method":"GET","url":"/did%3Aplc","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/0.27.2","host":"localhost:51012","connection":"close"},"remoteAddress":"::1","remotePort":51027},"err":{"type":"ServerError","message":"DID not registered: did:plc","stack":"Error: DID not registered: did:plc\n at /Users/dholms/Projects/bluesky/did-method-plc/packages/server/dist/index.js:63293:13\n at processTicksAndRejections (node:internal/process/task_queues:95:5)","status":404},"msg":"handled server error"}
4816
4816
  {"level":30,"time":1684278727276,"pid":65486,"hostname":"Daniels-MBP.attlocal.net","req":{"id":6,"method":"GET","url":"/did%3Aplc","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/0.27.2","host":"localhost:51012","connection":"close"},"remoteAddress":"::1","remotePort":51027},"res":{"statusCode":404,"headers":{"x-powered-by":"Express","access-control-allow-origin":"*","content-type":"application/json; charset=utf-8","content-length":"41","etag":"W/\"29-pS6VolLLFKlzTQkRVMexS+2oUm8\""}},"responseTime":0,"msg":"request completed"}
4817
+ {"level":30,"time":1692316309954,"pid":36904,"hostname":"Daniels-MBP.attlocal.net","req":{"id":1,"method":"POST","url":"/did%3Aplc%3A4jfkmzpiy76fnm2sqnr6ou5n","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","content-type":"application/json","user-agent":"axios/1.3.4","content-length":"433","accept-encoding":"gzip, compress, deflate, br","host":"localhost:64197","connection":"close"},"remoteAddress":"::1","remotePort":64198},"res":{"statusCode":200,"headers":{"x-powered-by":"Express","access-control-allow-origin":"*","content-type":"text/plain; charset=utf-8","content-length":"2","etag":"W/\"2-nOO9QiTIwXgNtWtBJezz8kv3SLc\""}},"responseTime":27,"msg":"request completed"}
4818
+ {"level":30,"time":1692316309975,"pid":36904,"hostname":"Daniels-MBP.attlocal.net","req":{"id":2,"method":"GET","url":"/did%3Aplc%3A4jfkmzpiy76fnm2sqnr6ou5n","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/0.27.2","host":"localhost:64197","connection":"close"},"remoteAddress":"::1","remotePort":64199},"res":{"statusCode":200,"headers":{"x-powered-by":"Express","access-control-allow-origin":"*","content-type":"application/did+ld+json; charset=utf-8","content-length":"526","etag":"W/\"20e-Bz5tHB3Hmhzr3BdJmjjW66tBssI\""}},"responseTime":2,"msg":"request completed"}
4819
+ {"level":30,"time":1692316309982,"pid":36904,"hostname":"Daniels-MBP.attlocal.net","req":{"id":3,"method":"GET","url":"/did%3Aplc%3A4jfkmzpiy76fnm2sqnr6ou5n","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/0.27.2","host":"localhost:64197","connection":"close"},"remoteAddress":"::1","remotePort":64200},"res":{"statusCode":200,"headers":{"x-powered-by":"Express","access-control-allow-origin":"*","content-type":"application/did+ld+json; charset=utf-8","content-length":"526","etag":"W/\"20e-Bz5tHB3Hmhzr3BdJmjjW66tBssI\""}},"responseTime":1,"msg":"request completed"}
4820
+ {"level":30,"time":1692316309983,"pid":36904,"hostname":"Daniels-MBP.attlocal.net","req":{"id":4,"method":"GET","url":"/did%3Aplc%3A4jfkmzpiy76fnm2sqnr6ou5n","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/0.27.2","host":"localhost:64197","connection":"close"},"remoteAddress":"::1","remotePort":64201},"res":{"statusCode":200,"headers":{"x-powered-by":"Express","access-control-allow-origin":"*","content-type":"application/did+ld+json; charset=utf-8","content-length":"526","etag":"W/\"20e-Bz5tHB3Hmhzr3BdJmjjW66tBssI\""}},"responseTime":0,"msg":"request completed"}
4821
+ {"level":30,"time":1692316309991,"pid":36904,"hostname":"Daniels-MBP.attlocal.net","req":{"id":5,"method":"GET","url":"/did%3Aplc%3A4jfkmzpiy76fnm2sqnr6ou5n","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/0.27.2","host":"localhost:64197","connection":"close"},"remoteAddress":"::1","remotePort":64202},"res":{"statusCode":200,"headers":{"x-powered-by":"Express","access-control-allow-origin":"*","content-type":"application/did+ld+json; charset=utf-8","content-length":"526","etag":"W/\"20e-Bz5tHB3Hmhzr3BdJmjjW66tBssI\""}},"responseTime":0,"msg":"request completed"}
4822
+ {"level":30,"time":1692316309994,"pid":36904,"hostname":"Daniels-MBP.attlocal.net","req":{"id":6,"method":"GET","url":"/did%3Aplc%3A4jfkmzpiy76fnm2sqnr6ou5n","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/0.27.2","host":"localhost:64197","connection":"close"},"remoteAddress":"::1","remotePort":64203},"res":{"statusCode":200,"headers":{"x-powered-by":"Express","access-control-allow-origin":"*","content-type":"application/did+ld+json; charset=utf-8","content-length":"526","etag":"W/\"20e-Bz5tHB3Hmhzr3BdJmjjW66tBssI\""}},"responseTime":1,"msg":"request completed"}
4823
+ {"level":30,"time":1692316310002,"pid":36904,"hostname":"Daniels-MBP.attlocal.net","req":{"id":7,"method":"GET","url":"/did%3Aplc%3A4jfkmzpiy76fnm2sqnr6ou5n","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/0.27.2","host":"localhost:64197","connection":"close"},"remoteAddress":"::1","remotePort":64204},"res":{"statusCode":200,"headers":{"x-powered-by":"Express","access-control-allow-origin":"*","content-type":"application/did+ld+json; charset=utf-8","content-length":"526","etag":"W/\"20e-Bz5tHB3Hmhzr3BdJmjjW66tBssI\""}},"responseTime":1,"msg":"request completed"}
4824
+ {"level":30,"time":1692316310138,"pid":36903,"hostname":"Daniels-MBP.attlocal.net","req":{"id":1,"method":"POST","url":"/did%3Aplc%3Aaqvw54euhbiaqangapth7iiq","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","content-type":"application/json","user-agent":"axios/1.3.4","content-length":"434","accept-encoding":"gzip, compress, deflate, br","host":"localhost:64206","connection":"close"},"remoteAddress":"::1","remotePort":64207},"res":{"statusCode":200,"headers":{"x-powered-by":"Express","access-control-allow-origin":"*","content-type":"text/plain; charset=utf-8","content-length":"2","etag":"W/\"2-nOO9QiTIwXgNtWtBJezz8kv3SLc\""}},"responseTime":23,"msg":"request completed"}
4825
+ {"level":30,"time":1692316310148,"pid":36903,"hostname":"Daniels-MBP.attlocal.net","req":{"id":2,"method":"GET","url":"/did%3Aplc%3Aaqvw54euhbiaqangapth7iiq","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/1.3.4","accept-encoding":"gzip, compress, deflate, br","host":"localhost:64206","connection":"close"},"remoteAddress":"::1","remotePort":64208},"res":{"statusCode":200,"headers":{"x-powered-by":"Express","access-control-allow-origin":"*","content-type":"application/did+ld+json; charset=utf-8","content-length":"527","etag":"W/\"20f-nrk1WS+r5hYW1H3OJW80loBMcdw\""}},"responseTime":1,"msg":"request completed"}
4826
+ {"level":30,"time":1692316313207,"pid":36903,"hostname":"Daniels-MBP.attlocal.net","req":{"id":3,"method":"GET","url":"/did%3Aplc%3Aaqvw54euhbiaqangapth7iiq","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/0.27.2","host":"localhost:64206","connection":"close"},"remoteAddress":"::1","remotePort":64212},"res":{"statusCode":200,"headers":{"x-powered-by":"Express","access-control-allow-origin":"*","content-type":"application/did+ld+json; charset=utf-8","content-length":"527","etag":"W/\"20f-nrk1WS+r5hYW1H3OJW80loBMcdw\""}},"responseTime":1,"msg":"request completed"}
4827
+ {"level":30,"time":1692316313212,"pid":36903,"hostname":"Daniels-MBP.attlocal.net","req":{"id":4,"method":"GET","url":"/did%3Aplc%3Aaqvw54euhbiaqangapth7iiq","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/0.27.2","host":"localhost:64206","connection":"close"},"remoteAddress":"::1","remotePort":64213},"res":{"statusCode":200,"headers":{"x-powered-by":"Express","access-control-allow-origin":"*","content-type":"application/did+ld+json; charset=utf-8","content-length":"527","etag":"W/\"20f-nrk1WS+r5hYW1H3OJW80loBMcdw\""}},"responseTime":1,"msg":"request completed"}
4828
+ {"level":30,"time":1692316313215,"pid":36903,"hostname":"Daniels-MBP.attlocal.net","req":{"id":5,"method":"GET","url":"/did%3Aplc%3Aasdf","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/0.27.2","host":"localhost:64206","connection":"close"},"remoteAddress":"::1","remotePort":64214},"err":{"type":"ServerError","message":"DID not registered: did:plc:asdf","stack":"Error: DID not registered: did:plc:asdf\n at /Users/dholms/projects/bluesky/did-method-plc/packages/server/dist/index.js:63293:13\n at processTicksAndRejections (node:internal/process/task_queues:95:5)","status":404},"msg":"handled server error"}
4829
+ {"level":30,"time":1692316313509,"pid":36903,"hostname":"Daniels-MBP.attlocal.net","req":{"id":5,"method":"GET","url":"/did%3Aplc%3Aasdf","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/0.27.2","host":"localhost:64206","connection":"close"},"remoteAddress":"::1","remotePort":64214},"res":{"statusCode":404,"headers":{"x-powered-by":"Express","access-control-allow-origin":"*","content-type":"application/json; charset=utf-8","content-length":"46","etag":"W/\"2e-ouyx9dlGpAF6gs2puC1AAUwbx84\""}},"responseTime":294,"msg":"request completed"}
4830
+ {"level":30,"time":1692316313512,"pid":36903,"hostname":"Daniels-MBP.attlocal.net","req":{"id":6,"method":"GET","url":"/did%3Aplc","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/0.27.2","host":"localhost:64206","connection":"close"},"remoteAddress":"::1","remotePort":64215},"err":{"type":"ServerError","message":"DID not registered: did:plc","stack":"Error: DID not registered: did:plc\n at /Users/dholms/projects/bluesky/did-method-plc/packages/server/dist/index.js:63293:13\n at processTicksAndRejections (node:internal/process/task_queues:95:5)","status":404},"msg":"handled server error"}
4831
+ {"level":30,"time":1692316313512,"pid":36903,"hostname":"Daniels-MBP.attlocal.net","req":{"id":6,"method":"GET","url":"/did%3Aplc","query":{},"params":{},"headers":{"accept":"application/json, text/plain, */*","user-agent":"axios/0.27.2","host":"localhost:64206","connection":"close"},"remoteAddress":"::1","remotePort":64215},"res":{"statusCode":404,"headers":{"x-powered-by":"Express","access-control-allow-origin":"*","content-type":"application/json; charset=utf-8","content-length":"41","etag":"W/\"29-pS6VolLLFKlzTQkRVMexS+2oUm8\""}},"responseTime":0,"msg":"request completed"}
@@ -0,0 +1,103 @@
1
+ import { DidResolver, ensureAtpDocument } from '../src'
2
+
3
+ describe('did parsing', () => {
4
+ it('throws on bad DID document', async () => {
5
+ const did = 'did:plc:yk4dd2qkboz2yv6tpubpc6co'
6
+ const docJson = `{
7
+ "ideep": "did:plc:yk4dd2qkboz2yv6tpubpc6co",
8
+ "blah": [
9
+ "https://dholms.xyz"
10
+ ],
11
+ "zoot": [
12
+ {
13
+ "id": "#elsewhere",
14
+ "type": "EcdsaSecp256k1VerificationKey2019",
15
+ "controller": "did:plc:yk4dd2qkboz2yv6tpubpc6co",
16
+ "publicKeyMultibase": "zQYEBzXeuTM9UR3rfvNag6L3RNAs5pQZyYPsomTsgQhsxLdEgCrPTLgFna8yqCnxPpNT7DBk6Ym3dgPKNu86vt9GR"
17
+ }
18
+ ],
19
+ "yarg": [ ]
20
+ }`
21
+ const resolver = new DidResolver({})
22
+ expect(() => {
23
+ resolver.validateDidDoc(did, JSON.parse(docJson))
24
+ }).toThrow()
25
+ })
26
+
27
+ it('parses legacy DID format, extracts atpData', async () => {
28
+ const did = 'did:plc:yk4dd2qkboz2yv6tpubpc6co'
29
+ const docJson = `{
30
+ "@context": [
31
+ "https://www.w3.org/ns/did/v1",
32
+ "https://w3id.org/security/suites/secp256k1-2019/v1"
33
+ ],
34
+ "id": "did:plc:yk4dd2qkboz2yv6tpubpc6co",
35
+ "alsoKnownAs": [
36
+ "at://dholms.xyz"
37
+ ],
38
+ "verificationMethod": [
39
+ {
40
+ "id": "#atproto",
41
+ "type": "EcdsaSecp256k1VerificationKey2019",
42
+ "controller": "did:plc:yk4dd2qkboz2yv6tpubpc6co",
43
+ "publicKeyMultibase": "zQYEBzXeuTM9UR3rfvNag6L3RNAs5pQZyYPsomTsgQhsxLdEgCrPTLgFna8yqCnxPpNT7DBk6Ym3dgPKNu86vt9GR"
44
+ }
45
+ ],
46
+ "service": [
47
+ {
48
+ "id": "#atproto_pds",
49
+ "type": "AtprotoPersonalDataServer",
50
+ "serviceEndpoint": "https://bsky.social"
51
+ }
52
+ ]
53
+ }`
54
+ const resolver = new DidResolver({})
55
+ const doc = resolver.validateDidDoc(did, JSON.parse(docJson))
56
+ const atpData = ensureAtpDocument(doc)
57
+ expect(atpData.did).toEqual(did)
58
+ expect(atpData.handle).toEqual('dholms.xyz')
59
+ expect(atpData.pds).toEqual('https://bsky.social')
60
+ expect(atpData.signingKey).toEqual(
61
+ 'did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF',
62
+ )
63
+ })
64
+
65
+ it('parses newer Multikey DID format, extracts atpData', async () => {
66
+ const did = 'did:plc:yk4dd2qkboz2yv6tpubpc6co'
67
+ const docJson = `{
68
+ "@context": [
69
+ "https://www.w3.org/ns/did/v1",
70
+ "https://w3id.org/security/multikey/v1",
71
+ "https://w3id.org/security/suites/secp256k1-2019/v1"
72
+ ],
73
+ "id": "did:plc:yk4dd2qkboz2yv6tpubpc6co",
74
+ "alsoKnownAs": [
75
+ "at://dholms.xyz"
76
+ ],
77
+ "verificationMethod": [
78
+ {
79
+ "id": "did:plc:yk4dd2qkboz2yv6tpubpc6co#atproto",
80
+ "type": "Multikey",
81
+ "controller": "did:plc:yk4dd2qkboz2yv6tpubpc6co",
82
+ "publicKeyMultibase": "zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF"
83
+ }
84
+ ],
85
+ "service": [
86
+ {
87
+ "id": "#atproto_pds",
88
+ "type": "AtprotoPersonalDataServer",
89
+ "serviceEndpoint": "https://bsky.social"
90
+ }
91
+ ]
92
+ }`
93
+ const resolver = new DidResolver({})
94
+ const doc = resolver.validateDidDoc(did, JSON.parse(docJson))
95
+ const atpData = ensureAtpDocument(doc)
96
+ expect(atpData.did).toEqual(did)
97
+ expect(atpData.handle).toEqual('dholms.xyz')
98
+ expect(atpData.pds).toEqual('https://bsky.social')
99
+ expect(atpData.signingKey).toEqual(
100
+ 'did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF',
101
+ )
102
+ })
103
+ })