Fork of github.com/did-method-plc/did-method-plc
1import { P256Keypair } from '@atproto/crypto' 2import * as plc from '@did-plc/lib' 3import { CloseFn, runTestServer } from './_util' 4import { check } from '@atproto/common' 5import { Database } from '../src' 6import { didForCreateOp, PlcClientError } from '@did-plc/lib' 7 8describe('PLC server', () => { 9 let handle1 = 'at://alice.example.com' 10 let handle2 = 'at://bob.example.com' 11 let atpPds = 'https://example.com' 12 13 let close: CloseFn 14 let db: Database 15 let client: plc.Client 16 17 let signingKey: P256Keypair 18 let rotationKey1: P256Keypair 19 let rotationKey2: P256Keypair 20 let rotationKey3: P256Keypair 21 22 let did1: string 23 let did2: string 24 25 beforeAll(async () => { 26 const server = await runTestServer({ 27 dbSchema: 'server', 28 }) 29 30 db = server.db 31 close = server.close 32 client = new plc.Client(server.url) 33 signingKey = await P256Keypair.create() 34 rotationKey1 = await P256Keypair.create() 35 rotationKey2 = await P256Keypair.create() 36 rotationKey3 = await P256Keypair.create() 37 }) 38 39 afterAll(async () => { 40 if (close) { 41 await close() 42 } 43 }) 44 45 const verifyDoc = (doc: plc.DocumentData | null) => { 46 if (!doc) { 47 throw new Error('expected doc') 48 } 49 expect(doc.did).toEqual(did1) 50 expect(doc.verificationMethods).toEqual({ atproto: signingKey.did() }) 51 expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()]) 52 expect(doc.alsoKnownAs).toEqual([handle1]) 53 expect(doc.services).toEqual({ 54 atproto_pds: { 55 type: 'AtprotoPersonalDataServer', 56 endpoint: atpPds, 57 }, 58 }) 59 } 60 61 it('registers a did', async () => { 62 did1 = await client.createDid({ 63 signingKey: signingKey.did(), 64 rotationKeys: [rotationKey1.did(), rotationKey2.did()], 65 handle: handle1, 66 pds: atpPds, 67 signer: rotationKey1, 68 }) 69 70 did2 = await client.createDid({ 71 signingKey: signingKey.did(), 72 rotationKeys: [rotationKey3.did()], 73 handle: handle2, 74 pds: atpPds, 75 signer: rotationKey3, 76 }) 77 }) 78 79 it('retrieves did doc data', async () => { 80 const doc = await client.getDocumentData(did1) 81 verifyDoc(doc) 82 }) 83 84 it('can perform some updates', async () => { 85 const newRotationKey = await P256Keypair.create() 86 signingKey = await P256Keypair.create() 87 handle1 = 'at://ali.example2.com' 88 atpPds = 'https://example2.com' 89 90 await client.updateAtprotoKey(did1, rotationKey1, signingKey.did()) 91 await client.updateRotationKeys(did1, rotationKey1, [ 92 newRotationKey.did(), 93 rotationKey2.did(), 94 ]) 95 rotationKey1 = newRotationKey 96 97 await client.updateHandle(did1, rotationKey1, handle1) 98 await client.updatePds(did1, rotationKey1, atpPds) 99 100 const doc = await client.getDocumentData(did1) 101 verifyDoc(doc) 102 }) 103 104 it('does not allow *rotation* key types that we do not yet support', async () => { 105 // an ed25519 key, which we don't yet support 106 const newRotationKey = 107 'did:key:z6MkjwbBXZnFqL8su24wGL2Fdjti6GSLv9SWdYGswfazUPm9' 108 109 const promise = client.updateRotationKeys(did2, rotationKey3, [ 110 rotationKey2.did(), 111 newRotationKey, 112 ]) 113 await expect(promise).rejects.toThrow(PlcClientError) 114 }) 115 116 it('allows *verificationMethod* key types that we do not explicitly support', async () => { 117 // an ed25519 key, which we don't explicitly support 118 const newSigningKey = 119 'did:key:z6MkjwbBXZnFqL8su24wGL2Fdjti6GSLv9SWdYGswfazUPm9' 120 121 // Note: atproto itself does not currently support ed25519 keys, but PLC 122 // does not have opinions about atproto (or other services!) 123 await client.updateAtprotoKey(did2, rotationKey3, newSigningKey) 124 125 // a BLS12-381 key 126 const exoticSigningKeyFromTheFuture = 127 'did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY' 128 await client.updateAtprotoKey( 129 did2, 130 rotationKey3, 131 exoticSigningKeyFromTheFuture, 132 ) 133 134 // check that we can still read back the rendered did document 135 const doc = await client.getDocument(did2) 136 expect(doc.verificationMethod).toEqual([ 137 { 138 id: did2 + '#atproto', 139 type: 'Multikey', 140 controller: did2, 141 publicKeyMultibase: exoticSigningKeyFromTheFuture.slice(8), 142 }, 143 ]) 144 }) 145 146 it('does not allow syntactically invalid verificationMethod keys', async () => { 147 const promise1 = client.updateAtprotoKey( 148 did2, 149 rotationKey3, 150 'did:key:BJV2WY5DJMJQXGZJANFZSAYLXMVZW63LFEEQFY3ZP', // not b58 (b32!) 151 ) 152 await expect(promise1).rejects.toThrow(PlcClientError) 153 const promise2 = client.updateAtprotoKey( 154 did2, 155 rotationKey3, 156 'did:banana', // a malformed did:key 157 ) 158 await expect(promise2).rejects.toThrow(PlcClientError) 159 const promise3 = client.updateAtprotoKey( 160 did2, 161 rotationKey3, 162 'blah', // an even more malformed did:key 163 ) 164 await expect(promise3).rejects.toThrow(PlcClientError) 165 }) 166 167 it('does not allow unreasonably long verificationMethod keys', async () => { 168 const promise = client.updateAtprotoKey( 169 did2, 170 rotationKey3, 171 'did:key:z41vu8qtWtp8XRJ9Te5QhkyzU9ByBbiw7bZHKXDjZ8iYorixqZQmEZpxgVSteYirYWMBjqQuEbMYTDsCzXXCAanCSH2xG2cwpbCWGZ2coY2PnhbrDVo7QghsAHpm2X5zsRRwDLyUcm9MTNQAZuRs2B22ygQw3UwkKLA7PZ9ZQ9wMHppmkoaBapmUGaxRNjp1Mt4zxrm9RbEx8FiK3ANBL1fsjggNqvkKpbj6MjntRScPQnJCes9Vt1cFe3iwNP7Ya9RfbaKsVi1eothvSBcbWoouHActGeakHgqFLj1JpbkP7PL3hGGSWLQbXxzmdrfzBCYAtiUxGRvpf3JiaNA2WYbJTh58bzx', 172 ) 173 await expect(promise).rejects.toThrow(PlcClientError) 174 }) 175 176 it('retrieves the operation log', async () => { 177 const doc = await client.getDocumentData(did1) 178 const ops = await client.getOperationLog(did1) 179 const computedDoc = await plc.validateOperationLog(did1, ops) 180 expect(computedDoc).toEqual(doc) 181 }) 182 183 it('rejects on bad updates', async () => { 184 const newKey = await P256Keypair.create() 185 const operation = client.updateAtprotoKey(did1, newKey, newKey.did()) 186 await expect(operation).rejects.toThrow() 187 }) 188 189 it('allows for recovery through a forked history', async () => { 190 const attackerKey = await P256Keypair.create() 191 await client.updateRotationKeys(did1, rotationKey2, [attackerKey.did()]) 192 193 const newKey = await P256Keypair.create() 194 const ops = await client.getOperationLog(did1) 195 const forkPoint = ops.at(-2) 196 if (!check.is(forkPoint, plc.def.operation)) { 197 throw new Error('Could not find fork point') 198 } 199 const op = await plc.updateRotationKeysOp(forkPoint, rotationKey1, [ 200 rotationKey1.did(), 201 newKey.did(), 202 ]) 203 await client.sendOperation(did1, op) 204 205 rotationKey2 = newKey 206 207 const doc = await client.getDocumentData(did1) 208 verifyDoc(doc) 209 }) 210 211 it('retrieves the auditable operation log', async () => { 212 const log = await client.getOperationLog(did1) 213 const auditable = await client.getAuditableLog(did1) 214 // has one nullifed op 215 expect(auditable.length).toBe(log.length + 1) 216 expect(auditable.filter((op) => op.nullified).length).toBe(1) 217 expect(auditable.at(-2)?.nullified).toBe(true) 218 expect( 219 auditable.every((op) => check.is(op, plc.def.exportedOp)), 220 ).toBeTruthy() 221 }) 222 223 it('retrieves the did doc', async () => { 224 const data = await client.getDocumentData(did1) 225 const doc = await client.getDocument(did1) 226 expect(doc).toEqual(plc.formatDidDoc(data)) 227 }) 228 229 it('handles concurrent requests to many docs', async () => { 230 const COUNT = 20 231 const keys: P256Keypair[] = [] 232 for (let i = 0; i < COUNT; i++) { 233 keys.push(await P256Keypair.create()) 234 } 235 await Promise.all( 236 keys.map(async (key, index) => { 237 await client.createDid({ 238 signingKey: key.did(), 239 rotationKeys: [key.did()], 240 handle: `user${index}`, 241 pds: `example.com`, 242 signer: key, 243 }) 244 }), 245 ) 246 }) 247 248 it('resolves races into a coherent history with no forks', async () => { 249 const COUNT = 20 250 const keys: P256Keypair[] = [] 251 for (let i = 0; i < COUNT; i++) { 252 keys.push(await P256Keypair.create()) 253 } 254 // const prev = await client.getPrev(did) 255 256 let successes = 0 257 let failures = 0 258 await Promise.all( 259 keys.map(async (key) => { 260 try { 261 await client.updateAtprotoKey(did1, rotationKey1, key.did()) 262 successes++ 263 } catch (err) { 264 failures++ 265 } 266 }), 267 ) 268 expect(successes).toBe(1) 269 expect(failures).toBe(19) 270 271 const ops = await client.getOperationLog(did1) 272 await plc.validateOperationLog(did1, ops) 273 }) 274 275 it('tombstones the did', async () => { 276 await client.tombstone(did1, rotationKey1) 277 278 const promise = client.getDocument(did1) 279 await expect(promise).rejects.toThrow(PlcClientError) 280 const promise2 = client.getDocumentData(did1) 281 await expect(promise2).rejects.toThrow(PlcClientError) 282 }) 283 284 it('exports the data set', async () => { 285 const data = await client.export() 286 expect(data.every((row) => check.is(row, plc.def.exportedOp))).toBeTruthy() 287 expect(data.length).toBe(32) 288 for (let i = 1; i < data.length; i++) { 289 expect(data[i].createdAt >= data[i - 1].createdAt).toBeTruthy() 290 } 291 }) 292 293 it('disallows create v1s', async () => { 294 const createV1 = await plc.deprecatedSignCreate( 295 { 296 type: 'create', 297 signingKey: signingKey.did(), 298 recoveryKey: rotationKey1.did(), 299 handle: handle1, 300 service: atpPds, 301 prev: null, 302 }, 303 signingKey, 304 ) 305 const did = await didForCreateOp(createV1) 306 const attempt = client.sendOperation(did, createV1 as any) 307 await expect(attempt).rejects.toThrow() 308 }) 309 310 it('healthcheck succeeds when database is available.', async () => { 311 const res = await client.health() 312 expect(res).toEqual({ version: '0.0.0' }) 313 }) 314 315 it('healthcheck fails when database is unavailable.', async () => { 316 await db.db.destroy() 317 let error: PlcClientError 318 try { 319 await client.health() 320 throw new Error('Healthcheck should have failed') 321 } catch (err) { 322 if (err instanceof PlcClientError) { 323 error = err 324 } else { 325 throw err 326 } 327 } 328 expect(error.status).toEqual(503) 329 expect(error.data).toEqual({ 330 version: '0.0.0', 331 error: 'Service Unavailable', 332 }) 333 }) 334})