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

better did doc semantics & adding in op formatters

dholms a99dbe07 9ffb23a5

+22 -14
packages/lib/src/client.ts
···
import { check, cidForCbor } from '@atproto/common'
import { Keypair } from '@atproto/crypto'
import axios from 'axios'
import { didForCreateOp, normalizeOp, signOperation } from './operations'
import * as t from './types'
···
return res.data
}
-
async applyPartialOp(
did: string,
-
delta: Partial<t.UnsignedOperation>,
key: Keypair,
) {
const lastOp = await this.getLastOp(did)
if (check.is(lastOp, t.def.tombstone)) {
throw new Error('Cannot apply op to tombstone')
}
const prev = await cidForCbor(lastOp)
-
const { signingKey, rotationKeys, handles, services } = normalizeOp(lastOp)
-
const op = await signOperation(
-
{
-
signingKey,
-
rotationKeys,
-
handles,
-
services,
-
prev: prev.toString(),
-
...delta,
-
},
-
key,
-
)
await this.sendOperation(did, op)
}
async create(
···
import { check, cidForCbor } from '@atproto/common'
import { Keypair } from '@atproto/crypto'
import axios from 'axios'
+
import { CID } from 'multiformats/cid'
import { didForCreateOp, normalizeOp, signOperation } from './operations'
import * as t from './types'
···
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(
+2 -2
packages/lib/src/data.ts
···
throw new MisorderedOperationError()
}
}
-
const { signingKey, rotationKeys, handles, services } = op
-
doc = { did, signingKey, rotationKeys, handles, services }
prev = await cidForCbor(op)
}
···
throw new MisorderedOperationError()
}
}
+
const { verificationMethods, rotationKeys, alsoKnownAs, services } = op
+
doc = { did, verificationMethods, rotationKeys, alsoKnownAs, services }
prev = await cidForCbor(op)
}
+25 -27
packages/lib/src/document.ts
···
export const formatDidDoc = (data: t.DocumentData): t.DidDocument => {
const context = ['https://www.w3.org/ns/did/v1']
-
const signingKeyInfo = formatKeyAndContext(data.signingKey)
-
if (!context.includes(signingKeyInfo.context)) {
-
context.push(signingKeyInfo.context)
}
-
const alsoKnownAs = data.handles.map((h) => ensureHttpPrefix(h))
const services: Service[] = []
-
if (data.services.atpPds) {
services.push({
-
id: `#atpPds`,
-
type: 'AtpPersonalDataServer',
-
serviceEndpoint: ensureHttpPrefix(data.services.atpPds),
})
}
return {
'@context': context,
id: data.did,
-
alsoKnownAs: alsoKnownAs,
-
verificationMethod: [
-
{
-
id: `#signingKey`,
-
type: signingKeyInfo.type,
-
controller: data.did,
-
publicKeyMultibase: signingKeyInfo.publicKeyMultibase,
-
},
-
],
-
assertionMethod: [`#signingKey`],
-
capabilityInvocation: [`#signingKey`],
-
capabilityDelegation: [`#signingKey`],
service: services,
}
}
type Service = {
···
}
throw new UnsupportedKeyError(key, `Unsupported key type: ${jwtAlg}`)
}
-
-
export const ensureHttpPrefix = (str: string): string => {
-
if (str.startsWith('http://') || str.startsWith('https://')) {
-
return str
-
}
-
return `https://${str}`
-
}
···
export const formatDidDoc = (data: t.DocumentData): t.DidDocument => {
const context = ['https://www.w3.org/ns/did/v1']
+
const verificationMethods: VerificationMethod[] = []
+
for (const [keyid, key] of Object.entries(data.verificationMethods)) {
+
const info = formatKeyAndContext(key)
+
if (!context.includes(info.context)) {
+
context.push(info.context)
+
}
+
verificationMethods.push({
+
id: `#${keyid}`,
+
type: info.type,
+
controller: data.did,
+
publicKeyMultibase: info.publicKeyMultibase,
+
})
}
const services: Service[] = []
+
for (const [serviceId, service] of Object.entries(data.services)) {
services.push({
+
id: `#${serviceId}`,
+
type: service.type,
+
serviceEndpoint: service.endpoint,
})
}
return {
'@context': context,
id: data.did,
+
alsoKnownAs: data.alsoKnownAs,
+
verificationMethod: verificationMethods,
service: services,
}
+
}
+
+
type VerificationMethod = {
+
id: string
+
type: string
+
controller: string
+
publicKeyMultibase: string
}
type Service = {
···
}
throw new UnsupportedKeyError(key, `Unsupported key type: ${jwtAlg}`)
}
+160 -8
packages/lib/src/operations.ts
···
import { CID } from 'multiformats/cid'
import * as uint8arrays from 'uint8arrays'
import { Keypair, parseDidKey, sha256, verifySignature } from '@atproto/crypto'
-
import { check } from '@atproto/common'
import * as t from './types'
import {
GenesisHashError,
···
}
}
export const signOperation = async (
op: t.UnsignedOperation,
signingKey: Keypair,
···
): Promise<t.Tombstone> => {
return addSignature(
{
-
tombstone: true,
prev: prev.toString(),
},
key,
···
return op
}
return {
-
signingKey: op.signingKey,
rotationKeys: [op.recoveryKey, op.signingKey],
-
handles: [op.handle],
services: {
-
atpPds: op.service,
},
prev: op.prev,
sig: op.sig,
···
return true
}
// ensure we support the op's keys
-
const keys = [op.signingKey, ...op.rotationKeys]
await Promise.all(
keys.map(async (k) => {
try {
···
if (op.prev !== null) {
throw new ImproperOperationError('expected null prev on create', op)
}
-
const { signingKey, rotationKeys, handles, services } = normalized
-
return { did, signingKey, rotationKeys, handles, services }
}
export const assureValidSig = async (
···
}
throw new InvalidSignatureError(op)
}
···
import { CID } from 'multiformats/cid'
import * as uint8arrays from 'uint8arrays'
import { Keypair, parseDidKey, sha256, verifySignature } from '@atproto/crypto'
+
import { check, cidForCbor } from '@atproto/common'
import * as t from './types'
import {
GenesisHashError,
···
}
}
+
export const formatAtprotoOp = (opts: {
+
signingKey: string
+
handle: string
+
pds: string
+
rotationKeys: string[]
+
prev: CID | null
+
}): t.UnsignedOperation => {
+
return {
+
type: 'plc_operation',
+
verificationMethods: {
+
atproto: opts.signingKey,
+
},
+
rotationKeys: opts.rotationKeys,
+
alsoKnownAs: [ensureAtprotoPrefix(opts.handle)],
+
services: {
+
atproto_pds: {
+
type: 'AtprotoPersonalDataServer',
+
endpoint: ensureHttpPrefix(opts.pds),
+
},
+
},
+
prev: opts.prev?.toString() ?? null,
+
}
+
}
+
+
export const atprotoOp = async (opts: {
+
signingKey: string
+
handle: string
+
pds: string
+
rotationKeys: string[]
+
prev: CID | null
+
signer: Keypair
+
}) => {
+
return addSignature(formatAtprotoOp(opts), opts.signer)
+
}
+
+
export const createUpdateOp = async (
+
lastOp: t.CompatibleOp,
+
signer: Keypair,
+
fn: (
+
normalized: t.UnsignedOperation,
+
prev: CID,
+
) => 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)
+
return addSignature(
+
{
+
...unsigned,
+
prev: prev.toString(),
+
},
+
signer,
+
)
+
}
+
+
export const updateAtprotoKeyOp = async (
+
lastOp: t.CompatibleOp,
+
signer: Keypair,
+
atprotoKey: string,
+
): Promise<t.Operation> => {
+
return createUpdateOp(lastOp, signer, (normalized) => ({
+
...normalized,
+
verificationMethods: {
+
...normalized.verificationMethods,
+
atproto: atprotoKey,
+
},
+
}))
+
}
+
+
export const updateHandleOp = async (
+
lastOp: t.CompatibleOp,
+
signer: Keypair,
+
handle: string,
+
): Promise<t.Operation> => {
+
const formatted = ensureAtprotoPrefix(handle)
+
return createUpdateOp(lastOp, signer, (normalized) => {
+
const handleI = normalized.alsoKnownAs.findIndex((h) =>
+
h.startsWith('at://'),
+
)
+
let aka: string[]
+
if (handleI < 0) {
+
aka = [formatted, ...normalized.alsoKnownAs]
+
} else {
+
aka = [
+
...normalized.alsoKnownAs.slice(0, handleI),
+
formatted,
+
...normalized.alsoKnownAs.slice(handleI + 1),
+
]
+
}
+
return {
+
...normalized,
+
alsoKnownAs: aka,
+
}
+
})
+
}
+
+
export const updatePdsOp = async (
+
lastOp: t.CompatibleOp,
+
signer: Keypair,
+
endpoint: string,
+
): Promise<t.Operation> => {
+
const formatted = ensureHttpPrefix(endpoint)
+
return createUpdateOp(lastOp, signer, (normalized) => {
+
return {
+
...normalized,
+
services: {
+
...normalized.services,
+
atproto_pds: {
+
type: 'AtprotoPersonalDataServer',
+
endpoint: formatted,
+
},
+
},
+
}
+
})
+
}
+
+
export const updateRotationkeysOp = async (
+
lastOp: t.CompatibleOp,
+
signer: Keypair,
+
rotationKeys: string[],
+
): Promise<t.Operation> => {
+
return createUpdateOp(lastOp, signer, (normalized) => {
+
return {
+
...normalized,
+
rotationKeys,
+
}
+
})
+
}
+
export const signOperation = async (
op: t.UnsignedOperation,
signingKey: Keypair,
···
): Promise<t.Tombstone> => {
return addSignature(
{
+
type: 'plc_tombstone',
prev: prev.toString(),
},
key,
···
return op
}
return {
+
type: 'plc_operation',
+
verificationMethods: {
+
atproto: op.signingKey,
+
},
rotationKeys: [op.recoveryKey, op.signingKey],
+
alsoKnownAs: [ensureAtprotoPrefix(op.handle)],
services: {
+
atproto_pds: {
+
type: 'AtprotoPersonalDataServer',
+
endpoint: ensureHttpPrefix(op.service),
+
},
},
prev: op.prev,
sig: op.sig,
···
return true
}
// ensure we support the op's keys
+
const keys = [...Object.values(op.verificationMethods), ...op.rotationKeys]
await Promise.all(
keys.map(async (k) => {
try {
···
if (op.prev !== null) {
throw new ImproperOperationError('expected null prev on create', op)
}
+
const { verificationMethods, rotationKeys, alsoKnownAs, services } =
+
normalized
+
return { did, verificationMethods, rotationKeys, alsoKnownAs, services }
}
export const assureValidSig = async (
···
}
throw new InvalidSignatureError(op)
}
+
+
export const ensureHttpPrefix = (str: string): string => {
+
if (str.startsWith('http://') || str.startsWith('https://')) {
+
return str
+
}
+
return `https://${str}`
+
}
+
+
export const ensureAtprotoPrefix = (str: string): string => {
+
if (str.startsWith('at://')) {
+
return str
+
}
+
const stripped = str.replace('http://', '').replace('https://', '')
+
return `at://${stripped}`
+
}
+13 -14
packages/lib/src/types.ts
···
})
.transform((obj: unknown) => mf.CID.asCID(obj) as mf.CID)
const documentData = z.object({
did: z.string(),
-
signingKey: z.string(),
rotationKeys: z.array(z.string()),
-
handles: z.array(z.string()),
-
services: z.object({
-
atpPds: z.string().optional(),
-
}),
})
export type DocumentData = z.infer<typeof documentData>
···
export type CreateOpV1 = z.infer<typeof createOpV1>
const unsignedOperation = z.object({
-
signingKey: z.string(),
rotationKeys: z.array(z.string()),
-
handles: z.array(z.string()),
-
services: z.object({
-
atpPds: z.string().optional(),
-
}),
prev: z.string().nullable(),
})
export type UnsignedOperation = z.infer<typeof unsignedOperation>
···
export type Operation = z.infer<typeof operation>
const unsignedTombstone = z.object({
-
tombstone: z.literal(true),
prev: z.string(),
})
export type UnsignedTombstone = z.infer<typeof unsignedTombstone>
···
id: z.string(),
alsoKnownAs: z.array(z.string()),
verificationMethod: z.array(didDocVerificationMethod),
-
assertionMethod: z.array(z.string()),
-
capabilityInvocation: z.array(z.string()),
-
capabilityDelegation: z.array(z.string()),
service: z.array(didDocService),
})
export type DidDocument = z.infer<typeof didDocument>
···
})
.transform((obj: unknown) => mf.CID.asCID(obj) as mf.CID)
+
const service = z.object({
+
type: z.string(),
+
endpoint: z.string(),
+
})
+
const documentData = z.object({
did: z.string(),
rotationKeys: z.array(z.string()),
+
verificationMethods: z.record(z.string()),
+
alsoKnownAs: z.array(z.string()),
+
services: z.record(service),
})
export type DocumentData = z.infer<typeof documentData>
···
export type CreateOpV1 = z.infer<typeof createOpV1>
const unsignedOperation = z.object({
+
type: z.literal('plc_operation'),
rotationKeys: z.array(z.string()),
+
verificationMethods: z.record(z.string()),
+
alsoKnownAs: z.array(z.string()),
+
services: z.record(service),
prev: z.string().nullable(),
})
export type UnsignedOperation = z.infer<typeof unsignedOperation>
···
export type Operation = z.infer<typeof operation>
const unsignedTombstone = z.object({
+
type: z.literal('plc_tombstone'),
prev: z.string(),
})
export type UnsignedTombstone = z.infer<typeof unsignedTombstone>
···
id: z.string(),
alsoKnownAs: z.array(z.string()),
verificationMethod: z.array(didDocVerificationMethod),
service: z.array(didDocService),
})
export type DidDocument = z.infer<typeof didDocument>
+36 -12
packages/lib/tests/compatibility.test.ts
···
deprecatedSignCreate,
didForCreateOp,
normalizeOp,
-
signOperation,
validateOperationLog,
} from '../src'
···
const normalized = normalizeOp(legacyOp)
expect(normalized).toEqual({
-
signingKey: signingKey.did(),
rotationKeys: [recoveryKey.did(), signingKey.did()],
-
handles: [handle],
services: {
-
atpPds: service,
},
prev: null,
sig: legacyOp.sig,
···
const legacyCid = await cidForCbor(legacyOp)
const newSigner = await Secp256k1Keypair.create()
const newRotater = await Secp256k1Keypair.create()
-
const nextOp = await signOperation(
-
{
-
signingKey: newSigner.did(),
-
rotationKeys: [newRotater.did()],
-
handles: [handle],
-
services: { atpPds: service },
-
prev: legacyCid.toString(),
-
},
signingKey,
)
await validateOperationLog(did, [legacyOp, nextOp])
const indexedLegacy = {
did,
operation: legacyOp,
···
deprecatedSignCreate,
didForCreateOp,
normalizeOp,
+
updateRotationkeysOp,
+
updateAtprotoKeyOp,
validateOperationLog,
} from '../src'
···
const normalized = normalizeOp(legacyOp)
expect(normalized).toEqual({
+
type: 'plc_operation',
+
verificationMethods: {
+
atproto: signingKey.did(),
+
},
rotationKeys: [recoveryKey.did(), signingKey.did()],
+
alsoKnownAs: [`at://${handle}`],
services: {
+
atproto_pds: {
+
type: 'AtprotoPersonalDataServer',
+
endpoint: service,
+
},
},
prev: null,
sig: legacyOp.sig,
···
const legacyCid = await cidForCbor(legacyOp)
const newSigner = await Secp256k1Keypair.create()
const newRotater = await Secp256k1Keypair.create()
+
const nextOp = await updateAtprotoKeyOp(
+
legacyOp,
signingKey,
+
newSigner.did(),
)
+
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])
+
return
const indexedLegacy = {
did,
operation: legacyOp,
+29 -23
packages/lib/tests/data.test.ts
···
const prev = await cidForCbor(lastOp)
return operations.signOperation(
{
-
signingKey: lastOp.signingKey,
rotationKeys: lastOp.rotationKeys,
-
handles: lastOp.handles,
services: lastOp.services,
prev: prev.toString(),
...changes,
···
it('creates a valid create op', async () => {
const createOp = await operations.signOperation(
{
-
signingKey: signingKey.did(),
rotationKeys: [rotationKey1.did(), rotationKey2.did()],
-
handles: [handle],
services: {
atpPds,
},
···
throw new Error('expected doc')
}
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 })
})
it('updates handle', async () => {
handle = 'ali.example2.com'
-
const op = await makeNextOp({ handles: [handle] }, rotationKey1)
ops.push(op)
const doc = await data.validateOperationLog(did, ops)
···
throw new Error('expected doc')
}
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 })
})
···
throw new Error('expected doc')
}
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 })
})
···
const newSigningKey = await Secp256k1Keypair.create()
const op = await makeNextOp(
{
-
signingKey: newSigningKey.did(),
},
rotationKey1,
)
···
throw new Error('expected doc')
}
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 })
})
···
}
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 })
})
it('no longer allows operations from old rotation key', async () => {
const op = await makeNextOp(
{
-
handles: ['bob'],
},
oldRotationKey1,
)
···
it('does not allow operations from the signingKey', async () => {
const op = await makeNextOp(
{
-
handles: ['bob'],
},
signingKey,
)
···
const newHandle = 'ali.example.com'
const op = await makeNextOp(
{
-
handles: [newHandle],
},
rotationKey2,
)
···
throw new Error('expected doc')
}
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 })
})
···
const prev = await cidForCbor(ops[ops.length - 2])
const op = await makeNextOp(
{
-
handles: ['bob.test'],
prev: prev.toString(),
},
rotationKey1,
···
it('does not allow a create operation in the middle of the log', async () => {
const op = await makeNextOp(
{
-
handles: ['bob.test'],
prev: null,
},
rotationKey1,
···
const prev = await cidForCbor(lastOp)
return operations.signOperation(
{
+
type: 'plc_operation',
+
verificationMethods: lastOp.verificationMethods,
rotationKeys: lastOp.rotationKeys,
+
alsoKnownAs: lastOp.alsoKnownAs,
services: lastOp.services,
prev: prev.toString(),
...changes,
···
it('creates a valid create op', async () => {
const createOp = await operations.signOperation(
{
+
type: 'plc_operation',
+
verificationMethods: {
+
atproto: signingKey.did(),
+
},
rotationKeys: [rotationKey1.did(), rotationKey2.did()],
+
alsoKnownAs: [handle],
services: {
atpPds,
},
···
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({ atpPds })
})
it('updates handle', async () => {
handle = 'ali.example2.com'
+
const op = await makeNextOp({ alsoKnownAs: [handle] }, rotationKey1)
ops.push(op)
const doc = await data.validateOperationLog(did, ops)
···
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({ atpPds })
})
···
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({ atpPds })
})
···
const newSigningKey = await Secp256k1Keypair.create()
const op = await makeNextOp(
{
+
verificationMethods: {
+
atproto: newSigningKey.did(),
+
},
},
rotationKey1,
)
···
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({ atpPds })
})
···
}
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({ atpPds })
})
it('no longer allows operations from old rotation key', async () => {
const op = await makeNextOp(
{
+
alsoKnownAs: ['bob'],
},
oldRotationKey1,
)
···
it('does not allow operations from the signingKey', async () => {
const op = await makeNextOp(
{
+
alsoKnownAs: ['bob'],
},
signingKey,
)
···
const newHandle = 'ali.example.com'
const op = await makeNextOp(
{
+
alsoKnownAs: [newHandle],
},
rotationKey2,
)
···
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({ atpPds })
})
···
const prev = await cidForCbor(ops[ops.length - 2])
const op = await makeNextOp(
{
+
alsoKnownAs: ['bob.test'],
prev: prev.toString(),
},
rotationKey1,
···
it('does not allow a create operation in the middle of the log', async () => {
const op = await makeNextOp(
{
+
alsoKnownAs: ['bob.test'],
prev: null,
},
rotationKey1,
+46 -69
packages/lib/tests/document.test.ts
···
describe('document', () => {
it('formats a valid DID document', async () => {
-
const signingKey = await Secp256k1Keypair.create()
const rotate1 = await Secp256k1Keypair.create()
const rotate2 = await EcdsaKeypair.create()
-
const handles = ['alice.test', 'bob.test']
const atpPds = 'https://example.com'
const data: t.DocumentData = {
did: 'did:example:alice',
-
signingKey: signingKey.did(),
rotationKeys: [rotate1.did(), rotate2.did()],
-
handles,
services: {
-
atpPds,
},
}
const doc = await document.formatDidDoc(data)
expect(doc['@context']).toEqual([
'https://www.w3.org/ns/did/v1',
'https://w3id.org/security/suites/secp256k1-2019/v1',
])
expect(doc.id).toEqual(data.did)
-
const formattedHandles = handles.map((h) => `https://${h}`)
-
expect(doc.alsoKnownAs).toEqual(formattedHandles)
-
expect(doc.verificationMethod.length).toBe(1)
-
expect(doc.verificationMethod[0].id).toEqual('#signingKey')
expect(doc.verificationMethod[0].type).toEqual(
'EcdsaSecp256k1VerificationKey2019',
)
expect(doc.verificationMethod[0].controller).toEqual(data.did)
-
const parsedSigningKey = parseDidKey(signingKey.did())
-
const signingKeyMultibase =
-
'z' + uint8arrays.toString(parsedSigningKey.keyBytes, 'base58btc')
expect(doc.verificationMethod[0].publicKeyMultibase).toEqual(
-
signingKeyMultibase,
)
-
expect(doc.assertionMethod).toEqual(['#signingKey'])
-
expect(doc.capabilityInvocation).toEqual(['#signingKey'])
-
expect(doc.capabilityDelegation).toEqual(['#signingKey'])
-
expect(doc.service.length).toBe(1)
-
expect(doc.service[0].id).toEqual('#atpPds')
-
expect(doc.service[0].type).toEqual('AtpPersonalDataServer')
-
expect(doc.service[0].serviceEndpoint).toEqual(atpPds)
-
})
-
it('handles P-256 keys', async () => {
-
const signingKey = await EcdsaKeypair.create()
-
const rotate1 = await Secp256k1Keypair.create()
-
const rotate2 = await EcdsaKeypair.create()
-
const handles = ['alice.test', 'bob.test']
-
const atpPds = 'https://example.com'
-
const data: t.DocumentData = {
-
did: 'did:example:alice',
-
signingKey: signingKey.did(),
-
rotationKeys: [rotate1.did(), rotate2.did()],
-
handles,
-
services: {
-
atpPds,
-
},
-
}
-
const doc = await document.formatDidDoc(data)
-
expect(doc.verificationMethod.length).toBe(1)
-
expect(doc['@context']).toEqual([
-
'https://www.w3.org/ns/did/v1',
-
'https://w3id.org/security/suites/ecdsa-2019/v1',
-
])
-
expect(doc.verificationMethod[0].id).toEqual('#signingKey')
-
expect(doc.verificationMethod[0].type).toEqual(
'EcdsaSecp256r1VerificationKey2019',
)
-
expect(doc.verificationMethod[0].controller).toEqual(data.did)
-
const parsedSigningKey = parseDidKey(signingKey.did())
-
const signingKeyMultibase =
-
'z' + uint8arrays.toString(parsedSigningKey.keyBytes, 'base58btc')
-
expect(doc.verificationMethod[0].publicKeyMultibase).toEqual(
-
signingKeyMultibase,
)
-
})
-
it('formats a valid DID document regardless of leading https://', async () => {
-
const signingKey = await Secp256k1Keypair.create()
-
const rotate1 = await Secp256k1Keypair.create()
-
const rotate2 = await EcdsaKeypair.create()
-
const handles = ['https://alice.test', 'bob.test']
-
const atpPds = 'example.com'
-
const data: t.DocumentData = {
-
did: 'did:example:alice',
-
signingKey: signingKey.did(),
-
rotationKeys: [rotate1.did(), rotate2.did()],
-
handles,
-
services: {
-
atpPds,
-
},
-
}
-
const doc = await document.formatDidDoc(data)
-
expect(doc.alsoKnownAs).toEqual(['https://alice.test', 'https://bob.test'])
-
expect(doc.service[0].serviceEndpoint).toEqual(`https://${atpPds}`)
})
})
···
describe('document', () => {
it('formats a valid DID document', async () => {
+
const atprotoKey = await Secp256k1Keypair.create()
+
const otherKey = await EcdsaKeypair.create()
const rotate1 = await Secp256k1Keypair.create()
const rotate2 = await EcdsaKeypair.create()
+
const alsoKnownAs = ['at://alice.test', 'https://bob.test']
const atpPds = 'https://example.com'
+
const otherService = 'https://other.com'
const data: t.DocumentData = {
did: 'did:example:alice',
+
verificationMethods: {
+
atproto: atprotoKey.did(),
+
other: otherKey.did(),
+
},
rotationKeys: [rotate1.did(), rotate2.did()],
+
alsoKnownAs,
services: {
+
atproto_pds: {
+
type: 'AtprotoPersonalDataServer',
+
endpoint: atpPds,
+
},
+
other: {
+
type: 'SomeService',
+
endpoint: otherService,
+
},
},
}
const doc = await document.formatDidDoc(data)
+
// only epxected keys
+
expect(Object.keys(doc).sort()).toEqual(
+
['@context', 'id', 'alsoKnownAs', 'verificationMethod', 'service'].sort(),
+
)
expect(doc['@context']).toEqual([
'https://www.w3.org/ns/did/v1',
'https://w3id.org/security/suites/secp256k1-2019/v1',
+
'https://w3id.org/security/suites/ecdsa-2019/v1',
])
expect(doc.id).toEqual(data.did)
+
expect(doc.alsoKnownAs).toEqual(alsoKnownAs)
+
+
expect(doc.verificationMethod.length).toBe(2)
+
+
expect(doc.verificationMethod[0].id).toEqual('#atproto')
expect(doc.verificationMethod[0].type).toEqual(
'EcdsaSecp256k1VerificationKey2019',
)
expect(doc.verificationMethod[0].controller).toEqual(data.did)
+
const parsedAtprotoKey = parseDidKey(atprotoKey.did())
+
const atprotoKeyMultibase =
+
'z' + uint8arrays.toString(parsedAtprotoKey.keyBytes, 'base58btc')
expect(doc.verificationMethod[0].publicKeyMultibase).toEqual(
+
atprotoKeyMultibase,
)
+
expect(doc.verificationMethod[1].id).toEqual('#other')
+
expect(doc.verificationMethod[1].type).toEqual(
'EcdsaSecp256r1VerificationKey2019',
)
+
expect(doc.verificationMethod[1].controller).toEqual(data.did)
+
const parsedOtherKey = parseDidKey(otherKey.did())
+
const otherKeyMultibase =
+
'z' + uint8arrays.toString(parsedOtherKey.keyBytes, 'base58btc')
+
expect(doc.verificationMethod[1].publicKeyMultibase).toEqual(
+
otherKeyMultibase,
)
+
expect(doc.service.length).toBe(2)
+
expect(doc.service[0].id).toEqual('#atproto_pds')
+
expect(doc.service[0].type).toEqual('AtprotoPersonalDataServer')
+
expect(doc.service[0].serviceEndpoint).toEqual(atpPds)
+
expect(doc.service[1].id).toEqual('#other')
+
expect(doc.service[1].type).toEqual('SomeService')
+
expect(doc.service[1].serviceEndpoint).toEqual(otherService)
})
})
+10 -12
packages/lib/tests/recovery.test.ts
···
signer: Keypair,
otherChanges: Partial<t.Operation> = {},
) => {
-
const op = await operations.signOperation(
-
{
signingKey: signingKey.did(),
rotationKeys: keys.map((k) => k.did()),
-
handles: [handle],
-
services: {
-
atpPds,
-
},
-
prev: prev ? prev.toString() : null,
-
...otherChanges,
-
},
-
signer,
-
)
const indexed = await formatIndexed(op)
return { op, indexed }
}
···
[rotationKey3],
rotate.indexed.cid,
rotationKey3,
-
{ handles: ['newhandle.test'] },
)
log.push({
···
signer: Keypair,
otherChanges: Partial<t.Operation> = {},
) => {
+
const unsigned = {
+
...operations.formatAtprotoOp({
signingKey: signingKey.did(),
rotationKeys: keys.map((k) => k.did()),
+
handle,
+
pds: atpPds,
+
prev,
+
}),
+
...otherChanges,
+
}
+
const op = await operations.addSignature(unsigned, signer)
const indexed = await formatIndexed(op)
return { op, indexed }
}
···
[rotationKey3],
rotate.indexed.cid,
rotationKey3,
+
{ alsoKnownAs: ['newhandle.test'] },
)
log.push({