Fork of github.com/did-method-plc/did-method-plc
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #5 from bluesky-social/did-doc-semantics

Mirror did doc semantics

authored by

Daniel Holmgren and committed by
GitHub
f6152c42 d92d3f76

+528 -421
+72 -42
packages/lib/src/client.ts
··· 1 1 import { check, cidForCbor } from '@atproto/common' 2 2 import { Keypair } from '@atproto/crypto' 3 3 import axios from 'axios' 4 - import { didForCreateOp, normalizeOp, signOperation } from './operations' 4 + import { 5 + atprotoOp, 6 + createUpdateOp, 7 + didForCreateOp, 8 + tombstoneOp, 9 + updateAtprotoKeyOp, 10 + updateHandleOp, 11 + updatePdsOp, 12 + updateRotationKeysOp, 13 + } from './operations' 5 14 import * as t from './types' 6 15 7 16 export class Client { ··· 40 49 return res.data 41 50 } 42 51 43 - async applyPartialOp( 44 - did: string, 45 - delta: Partial<t.UnsignedOperation>, 46 - key: Keypair, 47 - ) { 48 - const lastOp = await this.getLastOp(did) 49 - if (check.is(lastOp, t.def.tombstone)) { 50 - throw new Error('Cannot apply op to tombstone') 51 - } 52 - const prev = await cidForCbor(lastOp) 53 - const { signingKey, rotationKeys, handles, services } = normalizeOp(lastOp) 54 - const op = await signOperation( 55 - { 56 - signingKey, 57 - rotationKeys, 58 - handles, 59 - services, 60 - prev: prev.toString(), 61 - ...delta, 62 - }, 63 - key, 64 - ) 65 - await this.sendOperation(did, op) 66 - } 67 - 68 - async create( 69 - op: Omit<t.UnsignedOperation, 'prev'>, 70 - key: Keypair, 71 - ): Promise<string> { 72 - const createOp = await signOperation( 73 - { 74 - ...op, 75 - prev: null, 76 - }, 77 - key, 78 - ) 79 - const did = await didForCreateOp(createOp) 80 - await this.sendOperation(did, createOp) 81 - return did 82 - } 83 - 84 52 async sendOperation(did: string, op: t.OpOrTombstone) { 85 53 await axios.post(this.postOpUrl(did), op) 86 54 } ··· 96 64 const res = await axios.get(url.toString()) 97 65 const lines = res.data.split('\n') 98 66 return lines.map((l) => JSON.parse(l)) 67 + } 68 + 69 + async createDid(opts: { 70 + signingKey: string 71 + handle: string 72 + pds: string 73 + rotationKeys: string[] 74 + signer: Keypair 75 + }): Promise<string> { 76 + const op = await atprotoOp({ ...opts, prev: null }) 77 + const did = await didForCreateOp(op) 78 + await this.sendOperation(did, op) 79 + return did 80 + } 81 + 82 + private async ensureLastOp(did) { 83 + const lastOp = await this.getLastOp(did) 84 + if (check.is(lastOp, t.def.tombstone)) { 85 + throw new Error('Cannot apply op to tombstone') 86 + } 87 + return lastOp 88 + } 89 + 90 + async updateData( 91 + did: string, 92 + signer: Keypair, 93 + fn: (lastOp: t.UnsignedOperation) => Omit<t.UnsignedOperation, 'prev'>, 94 + ) { 95 + const lastOp = await this.ensureLastOp(did) 96 + const op = await createUpdateOp(lastOp, signer, fn) 97 + await this.sendOperation(did, op) 98 + } 99 + 100 + async updateAtprotoKey(did: string, signer: Keypair, atprotoKey: string) { 101 + const lastOp = await this.ensureLastOp(did) 102 + const op = await updateAtprotoKeyOp(lastOp, signer, atprotoKey) 103 + await this.sendOperation(did, op) 104 + } 105 + 106 + async updateHandle(did: string, signer: Keypair, handle: string) { 107 + const lastOp = await this.ensureLastOp(did) 108 + const op = await updateHandleOp(lastOp, signer, handle) 109 + await this.sendOperation(did, op) 110 + } 111 + 112 + async updatePds(did: string, signer: Keypair, endpoint: string) { 113 + const lastOp = await this.ensureLastOp(did) 114 + const op = await updatePdsOp(lastOp, signer, endpoint) 115 + await this.sendOperation(did, op) 116 + } 117 + 118 + async updateRotationKeys(did: string, signer: Keypair, keys: string[]) { 119 + const lastOp = await this.ensureLastOp(did) 120 + const op = await updateRotationKeysOp(lastOp, signer, keys) 121 + await this.sendOperation(did, op) 122 + } 123 + 124 + async tombstone(did: string, signer: Keypair) { 125 + const lastOp = await this.ensureLastOp(did) 126 + const prev = await cidForCbor(lastOp) 127 + const op = await tombstoneOp(prev, signer) 128 + await this.sendOperation(did, op) 99 129 } 100 130 101 131 async health() {
+2 -2
packages/lib/src/data.ts
··· 112 112 throw new MisorderedOperationError() 113 113 } 114 114 } 115 - const { signingKey, rotationKeys, handles, services } = op 116 - doc = { did, signingKey, rotationKeys, handles, services } 115 + const { verificationMethods, rotationKeys, alsoKnownAs, services } = op 116 + doc = { did, verificationMethods, rotationKeys, alsoKnownAs, services } 117 117 prev = await cidForCbor(op) 118 118 } 119 119
+25 -27
packages/lib/src/document.ts
··· 7 7 export const formatDidDoc = (data: t.DocumentData): t.DidDocument => { 8 8 const context = ['https://www.w3.org/ns/did/v1'] 9 9 10 - const signingKeyInfo = formatKeyAndContext(data.signingKey) 11 - if (!context.includes(signingKeyInfo.context)) { 12 - context.push(signingKeyInfo.context) 10 + const verificationMethods: VerificationMethod[] = [] 11 + for (const [keyid, key] of Object.entries(data.verificationMethods)) { 12 + const info = formatKeyAndContext(key) 13 + if (!context.includes(info.context)) { 14 + context.push(info.context) 15 + } 16 + verificationMethods.push({ 17 + id: `#${keyid}`, 18 + type: info.type, 19 + controller: data.did, 20 + publicKeyMultibase: info.publicKeyMultibase, 21 + }) 13 22 } 14 23 15 - const alsoKnownAs = data.handles.map((h) => ensureHttpPrefix(h)) 16 24 const services: Service[] = [] 17 - if (data.services.atpPds) { 25 + for (const [serviceId, service] of Object.entries(data.services)) { 18 26 services.push({ 19 - id: `#atpPds`, 20 - type: 'AtpPersonalDataServer', 21 - serviceEndpoint: ensureHttpPrefix(data.services.atpPds), 27 + id: `#${serviceId}`, 28 + type: service.type, 29 + serviceEndpoint: service.endpoint, 22 30 }) 23 31 } 24 32 25 33 return { 26 34 '@context': context, 27 35 id: data.did, 28 - alsoKnownAs: alsoKnownAs, 29 - verificationMethod: [ 30 - { 31 - id: `#signingKey`, 32 - type: signingKeyInfo.type, 33 - controller: data.did, 34 - publicKeyMultibase: signingKeyInfo.publicKeyMultibase, 35 - }, 36 - ], 37 - assertionMethod: [`#signingKey`], 38 - capabilityInvocation: [`#signingKey`], 39 - capabilityDelegation: [`#signingKey`], 36 + alsoKnownAs: data.alsoKnownAs, 37 + verificationMethod: verificationMethods, 40 38 service: services, 41 39 } 40 + } 41 + 42 + type VerificationMethod = { 43 + id: string 44 + type: string 45 + controller: string 46 + publicKeyMultibase: string 42 47 } 43 48 44 49 type Service = { ··· 77 82 } 78 83 throw new UnsupportedKeyError(key, `Unsupported key type: ${jwtAlg}`) 79 84 } 80 - 81 - export const ensureHttpPrefix = (str: string): string => { 82 - if (str.startsWith('http://') || str.startsWith('https://')) { 83 - return str 84 - } 85 - return `https://${str}` 86 - }
+195 -19
packages/lib/src/operations.ts
··· 2 2 import { CID } from 'multiformats/cid' 3 3 import * as uint8arrays from 'uint8arrays' 4 4 import { Keypair, parseDidKey, sha256, verifySignature } from '@atproto/crypto' 5 - import { check } from '@atproto/common' 5 + import { check, cidForCbor } from '@atproto/common' 6 6 import * as t from './types' 7 7 import { 8 8 GenesisHashError, ··· 20 20 return `did:plc:${truncated}` 21 21 } 22 22 23 + // Operations formatting 24 + // --------------------------- 25 + 26 + export const formatAtprotoOp = (opts: { 27 + signingKey: string 28 + handle: string 29 + pds: string 30 + rotationKeys: string[] 31 + prev: CID | null 32 + }): t.UnsignedOperation => { 33 + return { 34 + type: 'plc_operation', 35 + verificationMethods: { 36 + atproto: opts.signingKey, 37 + }, 38 + rotationKeys: opts.rotationKeys, 39 + alsoKnownAs: [ensureAtprotoPrefix(opts.handle)], 40 + services: { 41 + atproto_pds: { 42 + type: 'AtprotoPersonalDataServer', 43 + endpoint: ensureHttpPrefix(opts.pds), 44 + }, 45 + }, 46 + prev: opts.prev?.toString() ?? null, 47 + } 48 + } 49 + 50 + export const atprotoOp = async (opts: { 51 + signingKey: string 52 + handle: string 53 + pds: string 54 + rotationKeys: string[] 55 + prev: CID | null 56 + signer: Keypair 57 + }) => { 58 + return addSignature(formatAtprotoOp(opts), opts.signer) 59 + } 60 + 61 + export const createOp = async (opts: { 62 + signingKey: string 63 + handle: string 64 + pds: string 65 + rotationKeys: string[] 66 + signer: Keypair 67 + }): Promise<{ op: t.Operation; did: string }> => { 68 + const op = await atprotoOp({ ...opts, prev: null }) 69 + const did = await didForCreateOp(op) 70 + return { op, did } 71 + } 72 + 73 + export const createUpdateOp = async ( 74 + lastOp: t.CompatibleOp, 75 + signer: Keypair, 76 + fn: (normalized: t.UnsignedOperation) => Omit<t.UnsignedOperation, 'prev'>, 77 + ): Promise<t.Operation> => { 78 + const prev = await cidForCbor(lastOp) 79 + // omit sig so it doesn't accidentally make its way into the next operation 80 + const { sig, ...normalized } = normalizeOp(lastOp) 81 + const unsigned = await fn(normalized) 82 + return addSignature( 83 + { 84 + ...unsigned, 85 + prev: prev.toString(), 86 + }, 87 + signer, 88 + ) 89 + } 90 + 91 + export const updateAtprotoKeyOp = async ( 92 + lastOp: t.CompatibleOp, 93 + signer: Keypair, 94 + atprotoKey: string, 95 + ): Promise<t.Operation> => { 96 + return createUpdateOp(lastOp, signer, (normalized) => ({ 97 + ...normalized, 98 + verificationMethods: { 99 + ...normalized.verificationMethods, 100 + atproto: atprotoKey, 101 + }, 102 + })) 103 + } 104 + 105 + export const updateHandleOp = async ( 106 + lastOp: t.CompatibleOp, 107 + signer: Keypair, 108 + handle: string, 109 + ): Promise<t.Operation> => { 110 + const formatted = ensureAtprotoPrefix(handle) 111 + return createUpdateOp(lastOp, signer, (normalized) => { 112 + const handleI = normalized.alsoKnownAs.findIndex((h) => 113 + h.startsWith('at://'), 114 + ) 115 + let aka: string[] 116 + if (handleI < 0) { 117 + aka = [formatted, ...normalized.alsoKnownAs] 118 + } else { 119 + aka = [ 120 + ...normalized.alsoKnownAs.slice(0, handleI), 121 + formatted, 122 + ...normalized.alsoKnownAs.slice(handleI + 1), 123 + ] 124 + } 125 + return { 126 + ...normalized, 127 + alsoKnownAs: aka, 128 + } 129 + }) 130 + } 131 + 132 + export const updatePdsOp = async ( 133 + lastOp: t.CompatibleOp, 134 + signer: Keypair, 135 + endpoint: string, 136 + ): Promise<t.Operation> => { 137 + const formatted = ensureHttpPrefix(endpoint) 138 + return createUpdateOp(lastOp, signer, (normalized) => { 139 + return { 140 + ...normalized, 141 + services: { 142 + ...normalized.services, 143 + atproto_pds: { 144 + type: 'AtprotoPersonalDataServer', 145 + endpoint: formatted, 146 + }, 147 + }, 148 + } 149 + }) 150 + } 151 + 152 + export const updateRotationKeysOp = async ( 153 + lastOp: t.CompatibleOp, 154 + signer: Keypair, 155 + rotationKeys: string[], 156 + ): Promise<t.Operation> => { 157 + return createUpdateOp(lastOp, signer, (normalized) => { 158 + return { 159 + ...normalized, 160 + rotationKeys, 161 + } 162 + }) 163 + } 164 + 165 + export const tombstoneOp = async ( 166 + prev: CID, 167 + key: Keypair, 168 + ): Promise<t.Tombstone> => { 169 + return addSignature( 170 + { 171 + type: 'plc_tombstone', 172 + prev: prev.toString(), 173 + }, 174 + key, 175 + ) 176 + } 177 + 178 + // Signing operations 179 + // --------------------------- 180 + 23 181 export const addSignature = async <T extends Record<string, unknown>>( 24 182 object: T, 25 183 key: Keypair, ··· 39 197 return addSignature(op, signingKey) 40 198 } 41 199 42 - export const signTombstone = async ( 43 - prev: CID, 44 - key: Keypair, 45 - ): Promise<t.Tombstone> => { 46 - return addSignature( 47 - { 48 - tombstone: true, 49 - prev: prev.toString(), 50 - }, 51 - key, 52 - ) 53 - } 200 + // Backwards compatibility 201 + // --------------------------- 54 202 55 203 export const deprecatedSignCreate = async ( 56 204 op: t.UnsignedCreateOpV1, ··· 64 212 return op 65 213 } 66 214 return { 67 - signingKey: op.signingKey, 215 + type: 'plc_operation', 216 + verificationMethods: { 217 + atproto: op.signingKey, 218 + }, 68 219 rotationKeys: [op.recoveryKey, op.signingKey], 69 - handles: [op.handle], 220 + alsoKnownAs: [ensureAtprotoPrefix(op.handle)], 70 221 services: { 71 - atpPds: op.service, 222 + atproto_pds: { 223 + type: 'AtprotoPersonalDataServer', 224 + endpoint: ensureHttpPrefix(op.service), 225 + }, 72 226 }, 73 227 prev: op.prev, 74 228 sig: op.sig, 75 229 } 76 230 } 77 231 232 + // Verifying operations/signatures 233 + // --------------------------- 234 + 78 235 export const assureValidOp = async (op: t.OpOrTombstone) => { 79 236 if (check.is(op, t.def.tombstone)) { 80 237 return true 81 238 } 82 239 // ensure we support the op's keys 83 - const keys = [op.signingKey, ...op.rotationKeys] 240 + const keys = [...Object.values(op.verificationMethods), ...op.rotationKeys] 84 241 await Promise.all( 85 242 keys.map(async (k) => { 86 243 try { ··· 118 275 if (op.prev !== null) { 119 276 throw new ImproperOperationError('expected null prev on create', op) 120 277 } 121 - const { signingKey, rotationKeys, handles, services } = normalized 122 - return { did, signingKey, rotationKeys, handles, services } 278 + const { verificationMethods, rotationKeys, alsoKnownAs, services } = 279 + normalized 280 + return { did, verificationMethods, rotationKeys, alsoKnownAs, services } 123 281 } 124 282 125 283 export const assureValidSig = async ( ··· 137 295 } 138 296 throw new InvalidSignatureError(op) 139 297 } 298 + 299 + // Util 300 + // --------------------------- 301 + 302 + export const ensureHttpPrefix = (str: string): string => { 303 + if (str.startsWith('http://') || str.startsWith('https://')) { 304 + return str 305 + } 306 + return `https://${str}` 307 + } 308 + 309 + export const ensureAtprotoPrefix = (str: string): string => { 310 + if (str.startsWith('at://')) { 311 + return str 312 + } 313 + const stripped = str.replace('http://', '').replace('https://', '') 314 + return `at://${stripped}` 315 + }
+13 -14
packages/lib/src/types.ts
··· 8 8 }) 9 9 .transform((obj: unknown) => mf.CID.asCID(obj) as mf.CID) 10 10 11 + const service = z.object({ 12 + type: z.string(), 13 + endpoint: z.string(), 14 + }) 15 + 11 16 const documentData = z.object({ 12 17 did: z.string(), 13 - signingKey: z.string(), 14 18 rotationKeys: z.array(z.string()), 15 - handles: z.array(z.string()), 16 - services: z.object({ 17 - atpPds: z.string().optional(), 18 - }), 19 + verificationMethods: z.record(z.string()), 20 + alsoKnownAs: z.array(z.string()), 21 + services: z.record(service), 19 22 }) 20 23 export type DocumentData = z.infer<typeof documentData> 21 24 ··· 32 35 export type CreateOpV1 = z.infer<typeof createOpV1> 33 36 34 37 const unsignedOperation = z.object({ 35 - signingKey: z.string(), 38 + type: z.literal('plc_operation'), 36 39 rotationKeys: z.array(z.string()), 37 - handles: z.array(z.string()), 38 - services: z.object({ 39 - atpPds: z.string().optional(), 40 - }), 40 + verificationMethods: z.record(z.string()), 41 + alsoKnownAs: z.array(z.string()), 42 + services: z.record(service), 41 43 prev: z.string().nullable(), 42 44 }) 43 45 export type UnsignedOperation = z.infer<typeof unsignedOperation> ··· 45 47 export type Operation = z.infer<typeof operation> 46 48 47 49 const unsignedTombstone = z.object({ 48 - tombstone: z.literal(true), 50 + type: z.literal('plc_tombstone'), 49 51 prev: z.string(), 50 52 }) 51 53 export type UnsignedTombstone = z.infer<typeof unsignedTombstone> ··· 95 97 id: z.string(), 96 98 alsoKnownAs: z.array(z.string()), 97 99 verificationMethod: z.array(didDocVerificationMethod), 98 - assertionMethod: z.array(z.string()), 99 - capabilityInvocation: z.array(z.string()), 100 - capabilityDelegation: z.array(z.string()), 101 100 service: z.array(didDocService), 102 101 }) 103 102 export type DidDocument = z.infer<typeof didDocument>
+18 -12
packages/lib/tests/compatibility.test.ts
··· 6 6 deprecatedSignCreate, 7 7 didForCreateOp, 8 8 normalizeOp, 9 - signOperation, 9 + updateRotationKeysOp, 10 + updateAtprotoKeyOp, 10 11 validateOperationLog, 11 12 } from '../src' 12 13 ··· 41 42 42 43 const normalized = normalizeOp(legacyOp) 43 44 expect(normalized).toEqual({ 44 - signingKey: signingKey.did(), 45 + type: 'plc_operation', 46 + verificationMethods: { 47 + atproto: signingKey.did(), 48 + }, 45 49 rotationKeys: [recoveryKey.did(), signingKey.did()], 46 - handles: [handle], 50 + alsoKnownAs: [`at://${handle}`], 47 51 services: { 48 - atpPds: service, 52 + atproto_pds: { 53 + type: 'AtprotoPersonalDataServer', 54 + endpoint: service, 55 + }, 49 56 }, 50 57 prev: null, 51 58 sig: legacyOp.sig, ··· 56 63 const legacyCid = await cidForCbor(legacyOp) 57 64 const newSigner = await Secp256k1Keypair.create() 58 65 const newRotater = await Secp256k1Keypair.create() 59 - const nextOp = await signOperation( 60 - { 61 - signingKey: newSigner.did(), 62 - rotationKeys: [newRotater.did()], 63 - handles: [handle], 64 - services: { atpPds: service }, 65 - prev: legacyCid.toString(), 66 - }, 66 + const nextOp = await updateAtprotoKeyOp( 67 + legacyOp, 67 68 signingKey, 69 + newSigner.did(), 68 70 ) 71 + const anotherOp = await updateRotationKeysOp(nextOp, signingKey, [ 72 + newRotater.did(), 73 + ]) 69 74 await validateOperationLog(did, [legacyOp, nextOp]) 75 + await validateOperationLog(did, [legacyOp, nextOp, anotherOp]) 70 76 71 77 const indexedLegacy = { 72 78 did,
+67 -130
packages/lib/tests/data.test.ts
··· 1 1 import { check, cidForCbor } from '@atproto/common' 2 - import { EcdsaKeypair, Keypair, Secp256k1Keypair } from '@atproto/crypto' 2 + import { EcdsaKeypair, Secp256k1Keypair } from '@atproto/crypto' 3 3 import { 4 4 GenesisHashError, 5 5 ImproperOperationError, ··· 17 17 let rotationKey1: Secp256k1Keypair 18 18 let rotationKey2: EcdsaKeypair 19 19 let did: string 20 - let handle = 'alice.example.com' 20 + let handle = 'at://alice.example.com' 21 21 let atpPds = 'https://example.com' 22 22 23 23 let oldRotationKey1: Secp256k1Keypair ··· 28 28 rotationKey2 = await EcdsaKeypair.create() 29 29 }) 30 30 31 - const makeNextOp = async ( 32 - changes: Partial<t.Operation>, 33 - key: Keypair, 34 - ): Promise<t.Operation> => { 31 + const lastOp = () => { 35 32 const lastOp = ops.at(-1) 36 33 if (!lastOp) { 37 34 throw new Error('expected an op on log') 38 35 } 39 - const prev = await cidForCbor(lastOp) 40 - return operations.signOperation( 41 - { 42 - signingKey: lastOp.signingKey, 43 - rotationKeys: lastOp.rotationKeys, 44 - handles: lastOp.handles, 45 - services: lastOp.services, 46 - prev: prev.toString(), 47 - ...changes, 36 + return lastOp 37 + } 38 + 39 + const verifyDoc = (doc: t.DocumentData | null) => { 40 + if (!doc) { 41 + throw new Error('expected doc') 42 + } 43 + expect(doc.did).toEqual(did) 44 + expect(doc.verificationMethods).toEqual({ atproto: signingKey.did() }) 45 + expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()]) 46 + expect(doc.alsoKnownAs).toEqual([handle]) 47 + expect(doc.services).toEqual({ 48 + atproto_pds: { 49 + type: 'AtprotoPersonalDataServer', 50 + endpoint: atpPds, 48 51 }, 49 - key, 50 - ) 52 + }) 51 53 } 52 54 53 55 it('creates a valid create op', async () => { 54 - const createOp = await operations.signOperation( 55 - { 56 - signingKey: signingKey.did(), 57 - rotationKeys: [rotationKey1.did(), rotationKey2.did()], 58 - handles: [handle], 59 - services: { 60 - atpPds, 61 - }, 62 - prev: null, 63 - }, 64 - rotationKey1, 65 - ) 56 + const createOp = await operations.atprotoOp({ 57 + signingKey: signingKey.did(), 58 + rotationKeys: [rotationKey1.did(), rotationKey2.did()], 59 + handle, 60 + pds: atpPds, 61 + prev: null, 62 + signer: rotationKey1, 63 + }) 66 64 const isValid = check.is(createOp, t.def.operation) 67 65 expect(isValid).toBeTruthy() 68 66 ops.push(createOp) ··· 71 69 72 70 it('parses an operation log with no updates', async () => { 73 71 const doc = await data.validateOperationLog(did, ops) 74 - 75 - if (!doc) { 76 - throw new Error('expected doc') 77 - } 78 - expect(doc.did).toEqual(did) 79 - expect(doc.signingKey).toEqual(signingKey.did()) 80 - expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()]) 81 - expect(doc.handles).toEqual([handle]) 82 - expect(doc.services).toEqual({ atpPds }) 72 + verifyDoc(doc) 83 73 }) 84 74 85 75 it('updates handle', async () => { 86 - handle = 'ali.example2.com' 87 - const op = await makeNextOp({ handles: [handle] }, rotationKey1) 76 + const noPrefix = 'ali.exampl2.com' 77 + handle = `at://${noPrefix}` 78 + const op = await operations.updateHandleOp(lastOp(), rotationKey1, noPrefix) 88 79 ops.push(op) 89 80 90 81 const doc = await data.validateOperationLog(did, ops) 91 - if (!doc) { 92 - throw new Error('expected doc') 93 - } 94 - expect(doc.did).toEqual(did) 95 - expect(doc.signingKey).toEqual(signingKey.did()) 96 - expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()]) 97 - expect(doc.handles).toEqual([handle]) 98 - expect(doc.services).toEqual({ atpPds }) 82 + verifyDoc(doc) 99 83 }) 100 84 101 85 it('updates atpPds', async () => { 102 - atpPds = 'https://example2.com' 103 - const op = await makeNextOp( 104 - { 105 - services: { 106 - atpPds, 107 - }, 108 - }, 109 - rotationKey1, 110 - ) 86 + const noPrefix = 'example2.com' 87 + atpPds = `https://${noPrefix}` 88 + const op = await operations.updatePdsOp(lastOp(), rotationKey1, noPrefix) 111 89 ops.push(op) 112 90 113 91 const doc = await data.validateOperationLog(did, ops) 114 - if (!doc) { 115 - throw new Error('expected doc') 116 - } 117 - expect(doc.did).toEqual(did) 118 - expect(doc.signingKey).toEqual(signingKey.did()) 119 - expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()]) 120 - expect(doc.handles).toEqual([handle]) 121 - expect(doc.services).toEqual({ atpPds }) 92 + verifyDoc(doc) 122 93 }) 123 94 124 95 it('rotates signingKey', async () => { 125 96 const newSigningKey = await Secp256k1Keypair.create() 126 - const op = await makeNextOp( 127 - { 128 - signingKey: newSigningKey.did(), 129 - }, 97 + const op = await operations.updateAtprotoKeyOp( 98 + lastOp(), 130 99 rotationKey1, 100 + newSigningKey.did(), 131 101 ) 132 102 ops.push(op) 133 103 134 104 signingKey = newSigningKey 135 105 136 106 const doc = await data.validateOperationLog(did, ops) 137 - if (!doc) { 138 - throw new Error('expected doc') 139 - } 140 - expect(doc.did).toEqual(did) 141 - expect(doc.signingKey).toEqual(signingKey.did()) 142 - expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()]) 143 - expect(doc.handles).toEqual([handle]) 144 - expect(doc.services).toEqual({ atpPds }) 107 + verifyDoc(doc) 145 108 }) 146 109 147 110 it('rotates rotation keys', async () => { 148 111 const newRotationKey = await Secp256k1Keypair.create() 149 - const op = await makeNextOp( 150 - { 151 - rotationKeys: [newRotationKey.did(), rotationKey2.did()], 152 - }, 153 - rotationKey1, 154 - ) 112 + const op = await operations.updateRotationKeysOp(lastOp(), rotationKey1, [ 113 + newRotationKey.did(), 114 + rotationKey2.did(), 115 + ]) 155 116 ops.push(op) 156 117 157 118 oldRotationKey1 = rotationKey1 158 119 rotationKey1 = newRotationKey 159 120 160 121 const doc = await data.validateOperationLog(did, ops) 161 - if (!doc) { 162 - throw new Error('expected doc') 163 - } 164 - 165 - expect(doc.did).toEqual(did) 166 - expect(doc.signingKey).toEqual(signingKey.did()) 167 - expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()]) 168 - expect(doc.handles).toEqual([handle]) 169 - expect(doc.services).toEqual({ atpPds }) 122 + verifyDoc(doc) 170 123 }) 171 124 172 125 it('no longer allows operations from old rotation key', async () => { 173 - const op = await makeNextOp( 174 - { 175 - handles: ['bob'], 176 - }, 126 + const op = await operations.updateHandleOp( 127 + lastOp(), 177 128 oldRotationKey1, 129 + 'at://bob', 178 130 ) 179 131 expect(data.validateOperationLog(did, [...ops, op])).rejects.toThrow( 180 132 InvalidSignatureError, ··· 182 134 }) 183 135 184 136 it('does not allow operations from the signingKey', async () => { 185 - const op = await makeNextOp( 186 - { 187 - handles: ['bob'], 188 - }, 189 - signingKey, 190 - ) 137 + const op = await operations.updateHandleOp(lastOp(), signingKey, 'at://bob') 191 138 expect(data.validateOperationLog(did, [...ops, op])).rejects.toThrow( 192 139 InvalidSignatureError, 193 140 ) 194 141 }) 195 142 196 143 it('allows for operations from either rotation key', async () => { 197 - const newHandle = 'ali.example.com' 198 - const op = await makeNextOp( 199 - { 200 - handles: [newHandle], 201 - }, 144 + const newHandle = 'at://ali.example.com' 145 + const op = await operations.updateHandleOp( 146 + lastOp(), 202 147 rotationKey2, 148 + newHandle, 203 149 ) 204 150 ops.push(op) 205 151 handle = newHandle 206 152 const doc = await data.validateOperationLog(did, ops) 207 - if (!doc) { 208 - throw new Error('expected doc') 209 - } 210 - expect(doc.did).toEqual(did) 211 - expect(doc.signingKey).toEqual(signingKey.did()) 212 - expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()]) 213 - expect(doc.handles).toEqual([handle]) 214 - expect(doc.services).toEqual({ atpPds }) 153 + verifyDoc(doc) 215 154 }) 216 155 217 156 it('allows tombstoning a DID', async () => { 218 157 const last = await data.getLastOpWithCid(ops) 219 - const op = await operations.signTombstone(last.cid, rotationKey1) 158 + const op = await operations.tombstoneOp(last.cid, rotationKey1) 220 159 const doc = await data.validateOperationLog(did, [...ops, op]) 221 160 expect(doc).toBe(null) 222 161 }) 223 162 224 163 it('requires operations to be in order', async () => { 225 - const prev = await cidForCbor(ops[ops.length - 2]) 226 - const op = await makeNextOp( 227 - { 228 - handles: ['bob.test'], 229 - prev: prev.toString(), 230 - }, 164 + const op = await operations.updateHandleOp( 165 + ops[ops.length - 2], 231 166 rotationKey1, 167 + 'at://bob.test', 232 168 ) 233 169 expect(data.validateOperationLog(did, [...ops, op])).rejects.toThrow( 234 170 MisorderedOperationError, ··· 236 172 }) 237 173 238 174 it('does not allow a create operation in the middle of the log', async () => { 239 - const op = await makeNextOp( 240 - { 241 - handles: ['bob.test'], 242 - prev: null, 243 - }, 244 - rotationKey1, 245 - ) 175 + const op = await operations.atprotoOp({ 176 + signingKey: signingKey.did(), 177 + rotationKeys: [rotationKey1.did(), rotationKey2.did()], 178 + handle, 179 + pds: atpPds, 180 + prev: null, 181 + signer: rotationKey1, 182 + }) 246 183 expect(data.validateOperationLog(did, [...ops, op])).rejects.toThrow( 247 184 MisorderedOperationError, 248 185 ) ··· 250 187 251 188 it('does not allow a tombstone in the middle of the log', async () => { 252 189 const prev = await cidForCbor(ops[ops.length - 2]) 253 - const tombstone = await operations.signTombstone(prev, rotationKey1) 190 + const tombstone = await operations.tombstoneOp(prev, rotationKey1) 254 191 expect( 255 192 data.validateOperationLog(did, [ 256 193 ...ops.slice(0, ops.length - 1),
+46 -69
packages/lib/tests/document.test.ts
··· 5 5 6 6 describe('document', () => { 7 7 it('formats a valid DID document', async () => { 8 - const signingKey = await Secp256k1Keypair.create() 8 + const atprotoKey = await Secp256k1Keypair.create() 9 + const otherKey = await EcdsaKeypair.create() 9 10 const rotate1 = await Secp256k1Keypair.create() 10 11 const rotate2 = await EcdsaKeypair.create() 11 - const handles = ['alice.test', 'bob.test'] 12 + const alsoKnownAs = ['at://alice.test', 'https://bob.test'] 12 13 const atpPds = 'https://example.com' 14 + const otherService = 'https://other.com' 13 15 const data: t.DocumentData = { 14 16 did: 'did:example:alice', 15 - signingKey: signingKey.did(), 17 + verificationMethods: { 18 + atproto: atprotoKey.did(), 19 + other: otherKey.did(), 20 + }, 16 21 rotationKeys: [rotate1.did(), rotate2.did()], 17 - handles, 22 + alsoKnownAs, 18 23 services: { 19 - atpPds, 24 + atproto_pds: { 25 + type: 'AtprotoPersonalDataServer', 26 + endpoint: atpPds, 27 + }, 28 + other: { 29 + type: 'SomeService', 30 + endpoint: otherService, 31 + }, 20 32 }, 21 33 } 22 34 const doc = await document.formatDidDoc(data) 35 + // only expected keys 36 + expect(Object.keys(doc).sort()).toEqual( 37 + ['@context', 'id', 'alsoKnownAs', 'verificationMethod', 'service'].sort(), 38 + ) 23 39 expect(doc['@context']).toEqual([ 24 40 'https://www.w3.org/ns/did/v1', 25 41 'https://w3id.org/security/suites/secp256k1-2019/v1', 42 + 'https://w3id.org/security/suites/ecdsa-2019/v1', 26 43 ]) 27 44 expect(doc.id).toEqual(data.did) 28 - const formattedHandles = handles.map((h) => `https://${h}`) 29 - expect(doc.alsoKnownAs).toEqual(formattedHandles) 30 - expect(doc.verificationMethod.length).toBe(1) 31 - expect(doc.verificationMethod[0].id).toEqual('#signingKey') 45 + expect(doc.alsoKnownAs).toEqual(alsoKnownAs) 46 + 47 + expect(doc.verificationMethod.length).toBe(2) 48 + 49 + expect(doc.verificationMethod[0].id).toEqual('#atproto') 32 50 expect(doc.verificationMethod[0].type).toEqual( 33 51 'EcdsaSecp256k1VerificationKey2019', 34 52 ) 35 53 expect(doc.verificationMethod[0].controller).toEqual(data.did) 36 - const parsedSigningKey = parseDidKey(signingKey.did()) 37 - const signingKeyMultibase = 38 - 'z' + uint8arrays.toString(parsedSigningKey.keyBytes, 'base58btc') 54 + const parsedAtprotoKey = parseDidKey(atprotoKey.did()) 55 + const atprotoKeyMultibase = 56 + 'z' + uint8arrays.toString(parsedAtprotoKey.keyBytes, 'base58btc') 39 57 expect(doc.verificationMethod[0].publicKeyMultibase).toEqual( 40 - signingKeyMultibase, 58 + atprotoKeyMultibase, 41 59 ) 42 - expect(doc.assertionMethod).toEqual(['#signingKey']) 43 - expect(doc.capabilityInvocation).toEqual(['#signingKey']) 44 - expect(doc.capabilityDelegation).toEqual(['#signingKey']) 45 - expect(doc.service.length).toBe(1) 46 - expect(doc.service[0].id).toEqual('#atpPds') 47 - expect(doc.service[0].type).toEqual('AtpPersonalDataServer') 48 - expect(doc.service[0].serviceEndpoint).toEqual(atpPds) 49 - }) 50 60 51 - it('handles P-256 keys', async () => { 52 - const signingKey = await EcdsaKeypair.create() 53 - const rotate1 = await Secp256k1Keypair.create() 54 - const rotate2 = await EcdsaKeypair.create() 55 - const handles = ['alice.test', 'bob.test'] 56 - const atpPds = 'https://example.com' 57 - const data: t.DocumentData = { 58 - did: 'did:example:alice', 59 - signingKey: signingKey.did(), 60 - rotationKeys: [rotate1.did(), rotate2.did()], 61 - handles, 62 - services: { 63 - atpPds, 64 - }, 65 - } 66 - const doc = await document.formatDidDoc(data) 67 - expect(doc.verificationMethod.length).toBe(1) 68 - expect(doc['@context']).toEqual([ 69 - 'https://www.w3.org/ns/did/v1', 70 - 'https://w3id.org/security/suites/ecdsa-2019/v1', 71 - ]) 72 - expect(doc.verificationMethod[0].id).toEqual('#signingKey') 73 - expect(doc.verificationMethod[0].type).toEqual( 61 + expect(doc.verificationMethod[1].id).toEqual('#other') 62 + expect(doc.verificationMethod[1].type).toEqual( 74 63 'EcdsaSecp256r1VerificationKey2019', 75 64 ) 76 - expect(doc.verificationMethod[0].controller).toEqual(data.did) 77 - const parsedSigningKey = parseDidKey(signingKey.did()) 78 - const signingKeyMultibase = 79 - 'z' + uint8arrays.toString(parsedSigningKey.keyBytes, 'base58btc') 80 - expect(doc.verificationMethod[0].publicKeyMultibase).toEqual( 81 - signingKeyMultibase, 65 + expect(doc.verificationMethod[1].controller).toEqual(data.did) 66 + const parsedOtherKey = parseDidKey(otherKey.did()) 67 + const otherKeyMultibase = 68 + 'z' + uint8arrays.toString(parsedOtherKey.keyBytes, 'base58btc') 69 + expect(doc.verificationMethod[1].publicKeyMultibase).toEqual( 70 + otherKeyMultibase, 82 71 ) 83 - }) 84 72 85 - it('formats a valid DID document regardless of leading https://', async () => { 86 - const signingKey = await Secp256k1Keypair.create() 87 - const rotate1 = await Secp256k1Keypair.create() 88 - const rotate2 = await EcdsaKeypair.create() 89 - const handles = ['https://alice.test', 'bob.test'] 90 - const atpPds = 'example.com' 91 - const data: t.DocumentData = { 92 - did: 'did:example:alice', 93 - signingKey: signingKey.did(), 94 - rotationKeys: [rotate1.did(), rotate2.did()], 95 - handles, 96 - services: { 97 - atpPds, 98 - }, 99 - } 100 - const doc = await document.formatDidDoc(data) 101 - expect(doc.alsoKnownAs).toEqual(['https://alice.test', 'https://bob.test']) 102 - expect(doc.service[0].serviceEndpoint).toEqual(`https://${atpPds}`) 73 + expect(doc.service.length).toBe(2) 74 + expect(doc.service[0].id).toEqual('#atproto_pds') 75 + expect(doc.service[0].type).toEqual('AtprotoPersonalDataServer') 76 + expect(doc.service[0].serviceEndpoint).toEqual(atpPds) 77 + expect(doc.service[1].id).toEqual('#other') 78 + expect(doc.service[1].type).toEqual('SomeService') 79 + expect(doc.service[1].serviceEndpoint).toEqual(otherService) 103 80 }) 104 81 })
+11 -13
packages/lib/tests/recovery.test.ts
··· 46 46 signer: Keypair, 47 47 otherChanges: Partial<t.Operation> = {}, 48 48 ) => { 49 - const op = await operations.signOperation( 50 - { 49 + const unsigned = { 50 + ...operations.formatAtprotoOp({ 51 51 signingKey: signingKey.did(), 52 52 rotationKeys: keys.map((k) => k.did()), 53 - handles: [handle], 54 - services: { 55 - atpPds, 56 - }, 57 - prev: prev ? prev.toString() : null, 58 - ...otherChanges, 59 - }, 60 - signer, 61 - ) 53 + handle, 54 + pds: atpPds, 55 + prev, 56 + }), 57 + ...otherChanges, 58 + } 59 + const op = await operations.addSignature(unsigned, signer) 62 60 const indexed = await formatIndexed(op) 63 61 return { op, indexed } 64 62 } ··· 89 87 [rotationKey3], 90 88 rotate.indexed.cid, 91 89 rotationKey3, 92 - { handles: ['newhandle.test'] }, 90 + { alsoKnownAs: ['newhandle.test'] }, 93 91 ) 94 92 95 93 log.push({ ··· 161 159 }) 162 160 163 161 it('allows recovery from a tombstoned DID', async () => { 164 - const tombstone = await operations.signTombstone(createCid, rotationKey2) 162 + const tombstone = await operations.tombstoneOp(createCid, rotationKey2) 165 163 const cid = await cidForCbor(tombstone) 166 164 const tombstoneOps = [ 167 165 log[0],
+4 -1
packages/server/src/db/index.ts
··· 92 92 return results 93 93 } 94 94 95 - async validateAndAddOp(did: string, proposed: plc.Operation): Promise<void> { 95 + async validateAndAddOp( 96 + did: string, 97 + proposed: plc.OpOrTombstone, 98 + ): Promise<void> { 96 99 const ops = await this.indexedOpsForDid(did) 97 100 // throws if invalid 98 101 const { nullified, prev } = await plc.assureValidNextOp(did, ops, proposed)
+4 -1
packages/server/src/db/mock.ts
··· 11 11 async close(): Promise<void> {} 12 12 async healthCheck(): Promise<void> {} 13 13 14 - async validateAndAddOp(did: string, proposed: plc.Operation): Promise<void> { 14 + async validateAndAddOp( 15 + did: string, 16 + proposed: plc.OpOrTombstone, 17 + ): Promise<void> { 15 18 this.contents[did] ??= [] 16 19 const opsBefore = this.contents[did] 17 20 // throws if invalid
+1 -1
packages/server/src/db/types.ts
··· 4 4 export interface PlcDatabase { 5 5 close(): Promise<void> 6 6 healthCheck(): Promise<void> 7 - validateAndAddOp(did: string, proposed: plc.Operation): Promise<void> 7 + validateAndAddOp(did: string, proposed: plc.OpOrTombstone): Promise<void> 8 8 opsForDid(did: string): Promise<plc.CompatibleOpOrTombstone[]> 9 9 indexedOpsForDid( 10 10 did: string,
+1 -1
packages/server/src/routes.ts
··· 109 109 router.post('/:did', async function (req, res) { 110 110 const { did } = req.params 111 111 const op = req.body 112 - if (!check.is(op, plc.def.operation)) { 112 + if (!check.is(op, plc.def.opOrTombstone)) { 113 113 throw new ServerError(400, `Not a valid operation: ${JSON.stringify(op)}`) 114 114 } 115 115 await ctx.db.validateAndAddOp(did, op)
+69 -89
packages/server/tests/server.test.ts
··· 1 1 import { EcdsaKeypair } from '@atproto/crypto' 2 2 import * as plc from '@did-plc/lib' 3 3 import { CloseFn, runTestServer } from './_util' 4 - import { check, cidForCbor } from '@atproto/common' 4 + import { check } from '@atproto/common' 5 5 import { AxiosError } from 'axios' 6 6 import { Database } from '../src' 7 - import { signOperation } from '@did-plc/lib' 8 7 9 8 describe('PLC server', () => { 10 - let handle = 'alice.example.com' 11 - let atpPds = 'example.com' 9 + let handle = 'at://alice.example.com' 10 + let atpPds = 'https://example.com' 12 11 13 12 let close: CloseFn 14 13 let db: Database ··· 39 38 } 40 39 }) 41 40 42 - it('registers a did', async () => { 43 - did = await client.create( 44 - { 45 - signingKey: signingKey.did(), 46 - rotationKeys: [rotationKey1.did(), rotationKey2.did()], 47 - handles: [handle], 48 - services: { 49 - atpPds, 50 - }, 41 + const verifyDoc = (doc: plc.DocumentData | null) => { 42 + if (!doc) { 43 + throw new Error('expected doc') 44 + } 45 + expect(doc.did).toEqual(did) 46 + expect(doc.verificationMethods).toEqual({ atproto: signingKey.did() }) 47 + expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()]) 48 + expect(doc.alsoKnownAs).toEqual([handle]) 49 + expect(doc.services).toEqual({ 50 + atproto_pds: { 51 + type: 'AtprotoPersonalDataServer', 52 + endpoint: atpPds, 51 53 }, 52 - rotationKey1, 53 - ) 54 + }) 55 + } 56 + 57 + it('registers a did', async () => { 58 + did = await client.createDid({ 59 + signingKey: signingKey.did(), 60 + rotationKeys: [rotationKey1.did(), rotationKey2.did()], 61 + handle, 62 + pds: atpPds, 63 + signer: rotationKey1, 64 + }) 54 65 }) 55 66 56 67 it('retrieves did doc data', async () => { 57 68 const doc = await client.getDocumentData(did) 58 - expect(doc.did).toEqual(did) 59 - expect(doc.signingKey).toEqual(signingKey.did()) 60 - expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()]) 61 - expect(doc.handles).toEqual([handle]) 62 - expect(doc.services).toEqual({ atpPds }) 69 + verifyDoc(doc) 63 70 }) 64 71 65 72 it('can perform some updates', async () => { 66 73 const newRotationKey = await EcdsaKeypair.create() 67 74 signingKey = await EcdsaKeypair.create() 68 - handle = 'ali.example2.com' 69 - atpPds = 'example2.com' 75 + handle = 'at://ali.example2.com' 76 + atpPds = 'https://example2.com' 70 77 71 - await client.applyPartialOp( 72 - did, 73 - { signingKey: signingKey.did() }, 74 - rotationKey1, 75 - ) 76 - 77 - await client.applyPartialOp( 78 - did, 79 - { rotationKeys: [newRotationKey.did(), rotationKey2.did()] }, 80 - rotationKey1, 81 - ) 78 + await client.updateAtprotoKey(did, rotationKey1, signingKey.did()) 79 + await client.updateRotationKeys(did, rotationKey1, [ 80 + newRotationKey.did(), 81 + rotationKey2.did(), 82 + ]) 82 83 rotationKey1 = newRotationKey 83 84 84 - await client.applyPartialOp(did, { handles: [handle] }, rotationKey1) 85 - await client.applyPartialOp(did, { services: { atpPds } }, rotationKey1) 85 + await client.updateHandle(did, rotationKey1, handle) 86 + await client.updatePds(did, rotationKey1, atpPds) 86 87 87 88 const doc = await client.getDocumentData(did) 88 - expect(doc.did).toEqual(did) 89 - expect(doc.signingKey).toEqual(signingKey.did()) 90 - expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()]) 91 - expect(doc.handles).toEqual([handle]) 92 - expect(doc.services).toEqual({ atpPds }) 89 + verifyDoc(doc) 93 90 }) 94 91 95 92 it('does not allow key types that we do not support', async () => { ··· 97 94 const newSigningKey = 98 95 'did:key:z6MkjwbBXZnFqL8su24wGL2Fdjti6GSLv9SWdYGswfazUPm9' 99 96 100 - const promise = client.applyPartialOp( 101 - did, 102 - { signingKey: newSigningKey }, 103 - rotationKey1, 104 - ) 97 + const promise = client.updateAtprotoKey(did, rotationKey1, newSigningKey) 105 98 await expect(promise).rejects.toThrow(AxiosError) 99 + 100 + const promise2 = client.updateRotationKeys(did, rotationKey1, [ 101 + newSigningKey, 102 + ]) 103 + await expect(promise2).rejects.toThrow(AxiosError) 106 104 }) 107 105 108 106 it('retrieves the operation log', async () => { ··· 114 112 115 113 it('rejects on bad updates', async () => { 116 114 const newKey = await EcdsaKeypair.create() 117 - const operation = client.applyPartialOp( 118 - did, 119 - { signingKey: newKey.did() }, 120 - newKey, 121 - ) 115 + const operation = client.updateAtprotoKey(did, newKey, newKey.did()) 122 116 await expect(operation).rejects.toThrow() 123 117 }) 124 118 125 119 it('allows for recovery through a forked history', async () => { 126 120 const attackerKey = await EcdsaKeypair.create() 127 - await client.applyPartialOp( 128 - did, 129 - { signingKey: attackerKey.did(), rotationKeys: [attackerKey.did()] }, 130 - rotationKey2, 131 - ) 121 + await client.updateRotationKeys(did, rotationKey2, [attackerKey.did()]) 132 122 133 123 const newKey = await EcdsaKeypair.create() 134 124 const ops = await client.getOperationLog(did) ··· 136 126 if (!check.is(forkPoint, plc.def.operation)) { 137 127 throw new Error('Could not find fork point') 138 128 } 139 - const forkCid = await cidForCbor(forkPoint) 140 - const op = await signOperation( 141 - { 142 - signingKey: signingKey.did(), 143 - rotationKeys: [newKey.did()], 144 - handles: forkPoint.handles, 145 - services: forkPoint.services, 146 - prev: forkCid.toString(), 147 - }, 148 - rotationKey1, 149 - ) 129 + const op = await plc.updateRotationKeysOp(forkPoint, rotationKey1, [ 130 + rotationKey1.did(), 131 + newKey.did(), 132 + ]) 150 133 await client.sendOperation(did, op) 151 134 152 - rotationKey1 = newKey 135 + rotationKey2 = newKey 153 136 154 137 const doc = await client.getDocumentData(did) 155 - expect(doc.did).toEqual(did) 156 - expect(doc.signingKey).toEqual(signingKey.did()) 157 - expect(doc.rotationKeys).toEqual([newKey.did()]) 158 - expect(doc.handles).toEqual([handle]) 159 - expect(doc.services).toEqual({ atpPds }) 138 + verifyDoc(doc) 160 139 }) 161 140 162 141 it('retrieves the auditable operation log', async () => { ··· 185 164 } 186 165 await Promise.all( 187 166 keys.map(async (key, index) => { 188 - await client.create( 189 - { 190 - signingKey: key.did(), 191 - rotationKeys: [key.did()], 192 - handles: [`user${index}`], 193 - services: { 194 - atpPds: `example.com`, 195 - }, 196 - }, 197 - key, 198 - ) 167 + await client.createDid({ 168 + signingKey: key.did(), 169 + rotationKeys: [key.did()], 170 + handle: `user${index}`, 171 + pds: `example.com`, 172 + signer: key, 173 + }) 199 174 }), 200 175 ) 201 176 }) ··· 213 188 await Promise.all( 214 189 keys.map(async (key) => { 215 190 try { 216 - await client.applyPartialOp( 217 - did, 218 - { signingKey: key.did() }, 219 - rotationKey1, 220 - ) 191 + await client.updateAtprotoKey(did, rotationKey1, key.did()) 221 192 successes++ 222 193 } catch (err) { 223 194 failures++ ··· 231 202 await plc.validateOperationLog(did, ops) 232 203 }) 233 204 205 + it('tombstones the did', async () => { 206 + await client.tombstone(did, rotationKey1) 207 + 208 + const promise = client.getDocument(did) 209 + await expect(promise).rejects.toThrow(AxiosError) 210 + const promise2 = client.getDocumentData(did) 211 + await expect(promise2).rejects.toThrow(AxiosError) 212 + }) 213 + 234 214 it('exports the data set', async () => { 235 215 const data = await client.export() 236 216 expect(data.every((row) => check.is(row, plc.def.exportedOp))).toBeTruthy() 237 - expect(data.length).toBe(58) 217 + expect(data.length).toBe(59) 238 218 for (let i = 1; i < data.length; i++) { 239 219 expect(data[i].createdAt >= data[i - 1].createdAt).toBeTruthy() 240 220 }