Fork of github.com/did-method-plc/did-method-plc
1import { DAY, HOUR, cborEncode } from '@atproto/common' 2import * as plc from '@did-plc/lib' 3import { ServerError } from './error' 4import { parseDidKey } from '@atproto/crypto' 5 6const MAX_OP_BYTES = 4000 7const MAX_AKA_ENTRIES = 10 8const MAX_AKA_LENGTH = 256 9const MAX_ROTATION_ENTRIES = 10 10const MAX_SERVICE_ENTRIES = 10 11const MAX_SERVICE_TYPE_LENGTH = 256 12const MAX_SERVICE_ENDPOINT_LENGTH = 512 13const MAX_ID_LENGTH = 32 14 15export function validateIncomingOp(input: unknown): plc.OpOrTombstone { 16 const byteLength = cborEncode(input).byteLength 17 if (byteLength > MAX_OP_BYTES) { 18 throw new ServerError( 19 400, 20 `Operation too large (${MAX_OP_BYTES} bytes maximum in cbor encoding)`, 21 ) 22 } 23 24 // We *need* to parse, and use the result of the parsing, to ensure that any 25 // unknown fields are removed from the input. "@atproto/common"'s check 26 // function will not remove unknown fields. 27 const result = plc.def.opOrTombstone.safeParse(input) 28 29 if (!result.success) { 30 const errors = result.error.errors.map( 31 (e) => `${e.message} at /${e.path.join('/')}`, 32 ) 33 throw new ServerError( 34 400, 35 errors.length 36 ? errors.join('. ') + '.' 37 : `Not a valid operation: ${JSON.stringify(input)}`, 38 ) 39 } 40 41 const op = result.data 42 43 if (op.type === 'plc_tombstone') { 44 return op 45 } 46 if (op.alsoKnownAs.length > MAX_AKA_ENTRIES) { 47 throw new ServerError( 48 400, 49 `To many alsoKnownAs entries (max ${MAX_AKA_ENTRIES})`, 50 ) 51 } 52 const akaDupe = new Set<string>() 53 for (const aka of op.alsoKnownAs) { 54 if (aka.length > MAX_AKA_LENGTH) { 55 throw new ServerError( 56 400, 57 `alsoKnownAs entry too long (max ${MAX_AKA_LENGTH}): ${aka}`, 58 ) 59 } 60 if (akaDupe.has(aka)) { 61 throw new ServerError(400, `duplicate alsoKnownAs entry: ${aka}`) 62 } else { 63 akaDupe.add(aka) 64 } 65 } 66 if (op.rotationKeys.length > MAX_ROTATION_ENTRIES) { 67 throw new ServerError( 68 400, 69 `Too many rotationKey entries (max ${MAX_ROTATION_ENTRIES})`, 70 ) 71 } 72 for (const key of op.rotationKeys) { 73 try { 74 parseDidKey(key) 75 } catch (err) { 76 throw new ServerError(400, `Invalid rotationKey: ${key}`) 77 } 78 } 79 const serviceEntries = Object.entries(op.services) 80 if (serviceEntries.length > MAX_SERVICE_ENTRIES) { 81 throw new ServerError( 82 400, 83 `To many service entries (max ${MAX_SERVICE_ENTRIES})`, 84 ) 85 } 86 for (const [id, service] of serviceEntries) { 87 if (id.length > MAX_ID_LENGTH) { 88 throw new ServerError( 89 400, 90 `Service id too long (max ${MAX_ID_LENGTH}): ${id}`, 91 ) 92 } 93 if (service.type.length > MAX_SERVICE_TYPE_LENGTH) { 94 throw new ServerError( 95 400, 96 `Service type too long (max ${MAX_SERVICE_TYPE_LENGTH})`, 97 ) 98 } 99 if (service.endpoint.length > MAX_SERVICE_ENDPOINT_LENGTH) { 100 throw new ServerError( 101 400, 102 `Service endpoint too long (max ${MAX_SERVICE_ENDPOINT_LENGTH})`, 103 ) 104 } 105 } 106 const verifyMethods = Object.entries(op.verificationMethods) 107 for (const [id, key] of verifyMethods) { 108 if (id.length > MAX_ID_LENGTH) { 109 throw new ServerError( 110 400, 111 `Verification Method id too long (max ${MAX_ID_LENGTH}): ${id}`, 112 ) 113 } 114 try { 115 parseDidKey(key) 116 } catch (err) { 117 throw new ServerError(400, `Invalid verificationMethod key: ${key}`) 118 } 119 } 120 121 return op 122} 123 124const HOUR_LIMIT = 10 125const DAY_LIMIT = 30 126const WEEK_LIMIT = 100 127 128export const enforceOpsRateLimit = (ops: plc.IndexedOperation[]) => { 129 const hourAgo = new Date(Date.now() - HOUR) 130 const dayAgo = new Date(Date.now() - DAY) 131 const weekAgo = new Date(Date.now() - DAY * 7) 132 let withinHour = 0 133 let withinDay = 0 134 let withinWeek = 0 135 for (const op of ops) { 136 if (op.createdAt > weekAgo) { 137 withinWeek++ 138 if (withinWeek >= WEEK_LIMIT) { 139 throw new ServerError( 140 400, 141 `To many operations within last week (max ${WEEK_LIMIT})`, 142 ) 143 } 144 } 145 if (op.createdAt > dayAgo) { 146 withinDay++ 147 if (withinDay >= DAY_LIMIT) { 148 throw new ServerError( 149 400, 150 `To many operations within last day (max ${DAY_LIMIT})`, 151 ) 152 } 153 } 154 if (op.createdAt > hourAgo) { 155 withinHour++ 156 if (withinHour >= HOUR_LIMIT) { 157 throw new ServerError( 158 400, 159 `To many operations within last hour (max ${HOUR_LIMIT})`, 160 ) 161 } 162 } 163 } 164}