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

client & server tests

dholms 41ab4e76 1fc7679f

Changed files
+137 -171
packages
+65 -51
packages/lib/src/client.ts
···
-
import { check, cidForCbor } from '@atproto/common'
+
import { check } from '@atproto/common'
import { Keypair } from '@atproto/crypto'
import axios from 'axios'
-
import { CID } from 'multiformats/cid'
-
import { didForCreateOp, normalizeOp, signOperation } from './operations'
+
import {
+
atprotoOp,
+
createUpdateOp,
+
didForCreateOp,
+
updateAtprotoKeyOp,
+
updateHandleOp,
+
updatePdsOp,
+
updateRotationKeysOp,
+
} from './operations'
import * as t from './types'
export class Client {
···
return res.data
}
-
async updateData(
-
did: string,
-
key: Keypair,
-
fn: (lastOp: t.Operation, prev: CID) => Promise<t.UnsignedOperation>,
-
) {
-
const lastOp = await this.getLastOp(did)
-
if (check.is(lastOp, t.def.tombstone)) {
-
throw new Error('Cannot apply op to tombstone')
-
}
-
const normalized = normalizeOp(lastOp)
-
const prev = await cidForCbor(lastOp)
-
const unsigned = await fn(normalized, prev)
-
const op = await signOperation(unsigned, key)
-
await this.sendOperation(did, op)
-
}
-
-
async applyPartialOp(
-
did: string,
-
delta: Partial<t.UnsignedOperation>,
-
key: Keypair,
-
) {
-
await this.updateData(did, key, async (lastOp, prev) => ({
-
type: 'plc_operation',
-
verificationMethods: lastOp.verificationMethods,
-
rotationKeys: lastOp.rotationKeys,
-
alsoKnownAs: lastOp.alsoKnownAs,
-
services: lastOp.services,
-
prev: prev.toString(),
-
...delta,
-
}))
-
}
-
-
async create(
-
op: Omit<t.UnsignedOperation, 'prev'>,
-
key: Keypair,
-
): Promise<string> {
-
const createOp = await signOperation(
-
{
-
...op,
-
prev: null,
-
},
-
key,
-
)
-
const did = await didForCreateOp(createOp)
-
await this.sendOperation(did, createOp)
-
return did
-
}
-
async sendOperation(did: string, op: t.OpOrTombstone) {
await axios.post(this.postOpUrl(did), op)
}
···
const res = await axios.get(url.toString())
const lines = res.data.split('\n')
return lines.map((l) => JSON.parse(l))
+
}
+
+
async createDid(opts: {
+
signingKey: string
+
handle: string
+
pds: string
+
rotationKeys: string[]
+
signer: Keypair
+
}): Promise<string> {
+
const op = await atprotoOp({ ...opts, prev: null })
+
const did = await didForCreateOp(op)
+
await this.sendOperation(did, op)
+
return did
+
}
+
+
private async ensureLastOp(did) {
+
const lastOp = await this.getLastOp(did)
+
if (check.is(lastOp, t.def.tombstone)) {
+
throw new Error('Cannot apply op to tombstone')
+
}
+
return lastOp
+
}
+
+
async updateData(
+
did: string,
+
signer: Keypair,
+
fn: (lastOp: t.UnsignedOperation) => Omit<t.UnsignedOperation, 'prev'>,
+
) {
+
const lastOp = await this.ensureLastOp(did)
+
const op = await createUpdateOp(lastOp, signer, fn)
+
await this.sendOperation(did, op)
+
}
+
+
async updateAtprotoKey(did: string, signer: Keypair, atprotoKey: string) {
+
const lastOp = await this.ensureLastOp(did)
+
const op = await updateAtprotoKeyOp(lastOp, signer, atprotoKey)
+
await this.sendOperation(did, op)
+
}
+
+
async updateHandle(did: string, signer: Keypair, handle: string) {
+
const lastOp = await this.ensureLastOp(did)
+
const op = await updateHandleOp(lastOp, signer, handle)
+
await this.sendOperation(did, op)
+
}
+
+
async updatePds(did: string, signer: Keypair, endpoint: string) {
+
const lastOp = await this.ensureLastOp(did)
+
const op = await updatePdsOp(lastOp, signer, endpoint)
+
await this.sendOperation(did, op)
+
}
+
+
async updateRotationKeys(did: string, signer: Keypair, keys: string[]) {
+
const lastOp = await this.ensureLastOp(did)
+
const op = await updateRotationKeysOp(lastOp, signer, keys)
+
await this.sendOperation(did, op)
}
async health() {
+3 -6
packages/lib/src/operations.ts
···
export const createUpdateOp = async (
lastOp: t.CompatibleOp,
signer: Keypair,
-
fn: (
-
normalized: t.UnsignedOperation,
-
prev: CID,
-
) => Omit<t.UnsignedOperation, 'prev'>,
+
fn: (normalized: t.UnsignedOperation) => Omit<t.UnsignedOperation, 'prev'>,
): Promise<t.Operation> => {
const prev = await cidForCbor(lastOp)
// omit sig so it doesn't accidentally make its way into the next operation
const { sig, ...normalized } = normalizeOp(lastOp)
-
const unsigned = await fn(normalized, prev)
+
const unsigned = await fn(normalized)
return addSignature(
{
...unsigned,
···
})
}
-
export const updateRotationkeysOp = async (
+
export const updateRotationKeysOp = async (
lastOp: t.CompatibleOp,
signer: Keypair,
rotationKeys: string[],
+3 -21
packages/lib/tests/compatibility.test.ts
···
deprecatedSignCreate,
didForCreateOp,
normalizeOp,
-
updateRotationkeysOp,
+
updateRotationKeysOp,
updateAtprotoKeyOp,
validateOperationLog,
} from '../src'
···
signingKey,
newSigner.did(),
)
-
const anotherOp = await updateRotationkeysOp(nextOp, signingKey, [
+
const anotherOp = await updateRotationKeysOp(nextOp, signingKey, [
newRotater.did(),
])
-
// {
-
// type: 'plc_operation',
-
// verificationMethods: {
-
// atproto: newSigner.did(),
-
// },
-
// rotationKeys: [newRotater.did()],
-
// alsoKnownAs: [`at://${handle}`],
-
// services: {
-
// atproto_pds: {
-
// type: 'AtprotoPersonalDataServer',
-
// endpoint: service,
-
// },
-
// },
-
// prev: legacyCid.toString(),
-
// },
-
// signingKey,
-
// )
await validateOperationLog(did, [legacyOp, nextOp])
-
// await validateOperationLog(did, [legacyOp, nextOp, anotherOp])
+
await validateOperationLog(did, [legacyOp, nextOp, anotherOp])
-
return
const indexedLegacy = {
did,
operation: legacyOp,
+7 -5
packages/lib/tests/data.test.ts
···
})
it('updates handle', async () => {
-
handle = 'at://ali.example2.com'
-
const op = await operations.updateHandleOp(lastOp(), rotationKey1, handle)
+
const noPrefix = 'ali.exampl2.com'
+
handle = `at://${noPrefix}`
+
const op = await operations.updateHandleOp(lastOp(), rotationKey1, noPrefix)
ops.push(op)
const doc = await data.validateOperationLog(did, ops)
···
})
it('updates atpPds', async () => {
-
atpPds = 'https://example2.com'
-
const op = await operations.updatePdsOp(lastOp(), rotationKey1, atpPds)
+
const noPrefix = 'example2.com'
+
atpPds = `https://${noPrefix}`
+
const op = await operations.updatePdsOp(lastOp(), rotationKey1, noPrefix)
ops.push(op)
const doc = await data.validateOperationLog(did, ops)
···
it('rotates rotation keys', async () => {
const newRotationKey = await Secp256k1Keypair.create()
-
const op = await operations.updateRotationkeysOp(lastOp(), rotationKey1, [
+
const op = await operations.updateRotationKeysOp(lastOp(), rotationKey1, [
newRotationKey.did(),
rotationKey2.did(),
])
+59 -88
packages/server/tests/server.test.ts
···
import { EcdsaKeypair } from '@atproto/crypto'
import * as plc from '@did-plc/lib'
import { CloseFn, runTestServer } from './_util'
-
import { check, cidForCbor } from '@atproto/common'
+
import { check } from '@atproto/common'
import { AxiosError } from 'axios'
import { Database } from '../src'
-
import { signOperation } from '@did-plc/lib'
describe('PLC server', () => {
-
let handle = 'alice.example.com'
-
let atpPds = 'example.com'
+
let handle = 'at://alice.example.com'
+
let atpPds = 'https://example.com'
let close: CloseFn
let db: Database
···
}
})
-
it('registers a did', async () => {
-
did = await client.create(
-
{
-
signingKey: signingKey.did(),
-
rotationKeys: [rotationKey1.did(), rotationKey2.did()],
-
handles: [handle],
-
services: {
-
atpPds,
-
},
+
const verifyDoc = (doc: plc.DocumentData | null) => {
+
if (!doc) {
+
throw new Error('expected doc')
+
}
+
expect(doc.did).toEqual(did)
+
expect(doc.verificationMethods).toEqual({ atproto: signingKey.did() })
+
expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()])
+
expect(doc.alsoKnownAs).toEqual([handle])
+
expect(doc.services).toEqual({
+
atproto_pds: {
+
type: 'AtprotoPersonalDataServer',
+
endpoint: atpPds,
},
-
rotationKey1,
-
)
+
})
+
}
+
+
it('registers a did', async () => {
+
did = await client.createDid({
+
signingKey: signingKey.did(),
+
rotationKeys: [rotationKey1.did(), rotationKey2.did()],
+
handle,
+
pds: atpPds,
+
signer: rotationKey1,
+
})
})
it('retrieves did doc data', async () => {
const doc = await client.getDocumentData(did)
-
expect(doc.did).toEqual(did)
-
expect(doc.signingKey).toEqual(signingKey.did())
-
expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()])
-
expect(doc.handles).toEqual([handle])
-
expect(doc.services).toEqual({ atpPds })
+
verifyDoc(doc)
})
it('can perform some updates', async () => {
const newRotationKey = await EcdsaKeypair.create()
signingKey = await EcdsaKeypair.create()
-
handle = 'ali.example2.com'
-
atpPds = 'example2.com'
+
handle = 'at://ali.example2.com'
+
atpPds = 'https://example2.com'
-
await client.applyPartialOp(
-
did,
-
{ signingKey: signingKey.did() },
-
rotationKey1,
-
)
-
-
await client.applyPartialOp(
-
did,
-
{ rotationKeys: [newRotationKey.did(), rotationKey2.did()] },
-
rotationKey1,
-
)
+
await client.updateAtprotoKey(did, rotationKey1, signingKey.did())
+
await client.updateRotationKeys(did, rotationKey1, [
+
newRotationKey.did(),
+
rotationKey2.did(),
+
])
rotationKey1 = newRotationKey
-
await client.applyPartialOp(did, { handles: [handle] }, rotationKey1)
-
await client.applyPartialOp(did, { services: { atpPds } }, rotationKey1)
+
await client.updateHandle(did, rotationKey1, handle)
+
await client.updatePds(did, rotationKey1, atpPds)
const doc = await client.getDocumentData(did)
-
expect(doc.did).toEqual(did)
-
expect(doc.signingKey).toEqual(signingKey.did())
-
expect(doc.rotationKeys).toEqual([rotationKey1.did(), rotationKey2.did()])
-
expect(doc.handles).toEqual([handle])
-
expect(doc.services).toEqual({ atpPds })
+
verifyDoc(doc)
})
it('does not allow key types that we do not support', async () => {
···
const newSigningKey =
'did:key:z6MkjwbBXZnFqL8su24wGL2Fdjti6GSLv9SWdYGswfazUPm9'
-
const promise = client.applyPartialOp(
-
did,
-
{ signingKey: newSigningKey },
-
rotationKey1,
-
)
+
const promise = client.updateAtprotoKey(did, rotationKey1, newSigningKey)
await expect(promise).rejects.toThrow(AxiosError)
+
+
const promise2 = client.updateRotationKeys(did, rotationKey1, [
+
newSigningKey,
+
])
+
await expect(promise2).rejects.toThrow(AxiosError)
})
it('retrieves the operation log', async () => {
···
it('rejects on bad updates', async () => {
const newKey = await EcdsaKeypair.create()
-
const operation = client.applyPartialOp(
-
did,
-
{ signingKey: newKey.did() },
-
newKey,
-
)
+
const operation = client.updateAtprotoKey(did, newKey, newKey.did())
await expect(operation).rejects.toThrow()
})
it('allows for recovery through a forked history', async () => {
const attackerKey = await EcdsaKeypair.create()
-
await client.applyPartialOp(
-
did,
-
{ signingKey: attackerKey.did(), rotationKeys: [attackerKey.did()] },
-
rotationKey2,
-
)
+
await client.updateRotationKeys(did, rotationKey2, [attackerKey.did()])
const newKey = await EcdsaKeypair.create()
const ops = await client.getOperationLog(did)
···
if (!check.is(forkPoint, plc.def.operation)) {
throw new Error('Could not find fork point')
}
-
const forkCid = await cidForCbor(forkPoint)
-
const op = await signOperation(
-
{
-
signingKey: signingKey.did(),
-
rotationKeys: [newKey.did()],
-
handles: forkPoint.handles,
-
services: forkPoint.services,
-
prev: forkCid.toString(),
-
},
-
rotationKey1,
-
)
+
const op = await plc.updateRotationKeysOp(forkPoint, rotationKey1, [
+
rotationKey1.did(),
+
newKey.did(),
+
])
await client.sendOperation(did, op)
-
rotationKey1 = newKey
+
rotationKey2 = newKey
const doc = await client.getDocumentData(did)
-
expect(doc.did).toEqual(did)
-
expect(doc.signingKey).toEqual(signingKey.did())
-
expect(doc.rotationKeys).toEqual([newKey.did()])
-
expect(doc.handles).toEqual([handle])
-
expect(doc.services).toEqual({ atpPds })
+
verifyDoc(doc)
})
it('retrieves the auditable operation log', async () => {
···
}
await Promise.all(
keys.map(async (key, index) => {
-
await client.create(
-
{
-
signingKey: key.did(),
-
rotationKeys: [key.did()],
-
handles: [`user${index}`],
-
services: {
-
atpPds: `example.com`,
-
},
-
},
-
key,
-
)
+
await client.createDid({
+
signingKey: key.did(),
+
rotationKeys: [key.did()],
+
handle: `user${index}`,
+
pds: `example.com`,
+
signer: key,
+
})
}),
)
})
···
await Promise.all(
keys.map(async (key) => {
try {
-
await client.applyPartialOp(
-
did,
-
{ signingKey: key.did() },
-
rotationKey1,
-
)
+
await client.updateAtprotoKey(did, rotationKey1, key.did())
successes++
} catch (err) {
failures++