Fork of github.com/did-method-plc/did-method-plc
1import { CID } from 'multiformats/cid' 2import { check, cidForCbor, HOUR } from '@atproto/common' 3import * as t from './types' 4import { 5 assureValidCreationOp, 6 assureValidSig, 7 normalizeOp, 8} from './operations' 9import { 10 ImproperOperationError, 11 LateRecoveryError, 12 MisorderedOperationError, 13} from './error' 14 15export const assureValidNextOp = async ( 16 did: string, 17 ops: t.IndexedOperation[], 18 proposed: t.CompatibleOpOrTombstone, 19): Promise<{ nullified: CID[]; prev: CID | null }> => { 20 // special case if account creation 21 if (ops.length === 0) { 22 await assureValidCreationOp(did, proposed) 23 return { nullified: [], prev: null } 24 } 25 26 const proposedPrev = proposed.prev ? CID.parse(proposed.prev) : undefined 27 if (!proposedPrev) { 28 throw new MisorderedOperationError() 29 } 30 31 const indexOfPrev = ops.findIndex((op) => proposedPrev.equals(op.cid)) 32 if (indexOfPrev < 0) { 33 throw new MisorderedOperationError() 34 } 35 36 // if we are forking history, these are the ops still in the proposed canonical history 37 const opsInHistory = ops.slice(0, indexOfPrev + 1) 38 const nullified = ops.slice(indexOfPrev + 1) 39 const lastOp = opsInHistory.at(-1) 40 if (!lastOp) { 41 throw new MisorderedOperationError() 42 } 43 if (check.is(lastOp.operation, t.def.tombstone)) { 44 throw new MisorderedOperationError() 45 } 46 const lastOpNormalized = normalizeOp(lastOp.operation) 47 const firstNullified = nullified[0] 48 49 // if this does not involve nullification 50 if (!firstNullified) { 51 await assureValidSig(lastOpNormalized.rotationKeys, proposed) 52 return { nullified: [], prev: proposedPrev } 53 } 54 55 const disputedSigner = await assureValidSig( 56 lastOpNormalized.rotationKeys, 57 firstNullified.operation, 58 ) 59 60 const indexOfSigner = lastOpNormalized.rotationKeys.indexOf(disputedSigner) 61 const morePowerfulKeys = lastOpNormalized.rotationKeys.slice(0, indexOfSigner) 62 63 await assureValidSig(morePowerfulKeys, proposed) 64 65 // recovery key gets a 72hr window to do historical re-wrties 66 if (nullified.length > 0) { 67 const RECOVERY_WINDOW = 72 * HOUR 68 const timeLapsed = Date.now() - firstNullified.createdAt.getTime() 69 if (timeLapsed > RECOVERY_WINDOW) { 70 throw new LateRecoveryError(timeLapsed) 71 } 72 } 73 74 return { 75 nullified: nullified.map((op) => op.cid), 76 prev: proposedPrev, 77 } 78} 79 80export const validateOperationLog = async ( 81 did: string, 82 ops: t.CompatibleOpOrTombstone[], 83): Promise<t.DocumentData | null> => { 84 // make sure they're all validly formatted operations 85 const [first, ...rest] = ops 86 if (!check.is(first, t.def.compatibleOp)) { 87 throw new ImproperOperationError('incorrect structure', first) 88 } 89 for (const op of rest) { 90 if (!check.is(op, t.def.opOrTombstone)) { 91 throw new ImproperOperationError('incorrect structure', op) 92 } 93 } 94 95 // ensure the first op is a valid & signed create operation 96 let doc = await assureValidCreationOp(did, first) 97 let prev = await cidForCbor(first) 98 99 for (let i = 0; i < rest.length; i++) { 100 const op = rest[i] 101 if (!op.prev || !CID.parse(op.prev).equals(prev)) { 102 throw new MisorderedOperationError() 103 } 104 await assureValidSig(doc.rotationKeys, op) 105 const data = opToData(did, op) 106 // if tombstone & last op, return null. else throw 107 if (data === null) { 108 if (i === rest.length - 1) { 109 return null 110 } else { 111 throw new MisorderedOperationError() 112 } 113 } 114 doc = data 115 prev = await cidForCbor(op) 116 } 117 118 return doc 119} 120 121export const opToData = ( 122 did: string, 123 op: t.CompatibleOpOrTombstone, 124): t.DocumentData | null => { 125 if (check.is(op, t.def.tombstone)) { 126 return null 127 } 128 const { verificationMethods, rotationKeys, alsoKnownAs, services } = 129 normalizeOp(op) 130 return { did, verificationMethods, rotationKeys, alsoKnownAs, services } 131} 132 133export const getLastOpWithCid = async ( 134 ops: t.CompatibleOpOrTombstone[], 135): Promise<{ op: t.CompatibleOpOrTombstone; cid: CID }> => { 136 const op = ops.at(-1) 137 if (!op) { 138 throw new Error('log is empty') 139 } 140 const cid = await cidForCbor(op) 141 return { op, cid } 142}