Fork of github.com/did-method-plc/did-method-plc
1import { DAY, HOUR, cborEncode, check } 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 assertValidIncomingOp( 16 op: unknown, 17): asserts op is plc.OpOrTombstone { 18 const byteLength = cborEncode(op).byteLength 19 if (byteLength > MAX_OP_BYTES) { 20 throw new ServerError( 21 400, 22 `Operation too large (${MAX_OP_BYTES} bytes maximum in cbor encoding)`, 23 ) 24 } 25 if (!check.is(op, plc.def.opOrTombstone)) { 26 throw new ServerError(400, `Not a valid operation: ${JSON.stringify(op)}`) 27 } 28 if (op.type === 'plc_tombstone') { 29 return 30 } 31 if (op.alsoKnownAs.length > MAX_AKA_ENTRIES) { 32 throw new ServerError( 33 400, 34 `To many alsoKnownAs entries (max ${MAX_AKA_ENTRIES})`, 35 ) 36 } 37 const akaDupe: Record<string, boolean> = {} 38 for (const aka of op.alsoKnownAs) { 39 if (aka.length > MAX_AKA_LENGTH) { 40 throw new ServerError( 41 400, 42 `alsoKnownAs entry too long (max ${MAX_AKA_LENGTH}): ${aka}`, 43 ) 44 } 45 if (akaDupe[aka]) { 46 throw new ServerError(400, `duplicate alsoKnownAs entry: ${aka}`) 47 } else { 48 akaDupe[aka] = true 49 } 50 } 51 if (op.rotationKeys.length > MAX_ROTATION_ENTRIES) { 52 throw new ServerError( 53 400, 54 `Too many rotationKey entries (max ${MAX_ROTATION_ENTRIES})`, 55 ) 56 } 57 for (const key of op.rotationKeys) { 58 try { 59 parseDidKey(key) 60 } catch (err) { 61 throw new ServerError(400, `Invalid rotationKey: ${key}`) 62 } 63 } 64 const serviceEntries = Object.entries(op.services) 65 if (serviceEntries.length > MAX_SERVICE_ENTRIES) { 66 throw new ServerError( 67 400, 68 `To many service entries (max ${MAX_SERVICE_ENTRIES})`, 69 ) 70 } 71 for (const [id, service] of serviceEntries) { 72 if (id.length > MAX_ID_LENGTH) { 73 throw new ServerError( 74 400, 75 `Service id too long (max ${MAX_ID_LENGTH}): ${id}`, 76 ) 77 } 78 if (service.type.length > MAX_SERVICE_TYPE_LENGTH) { 79 throw new ServerError( 80 400, 81 `Service type too long (max ${MAX_SERVICE_TYPE_LENGTH})`, 82 ) 83 } 84 if (service.endpoint.length > MAX_SERVICE_ENDPOINT_LENGTH) { 85 throw new ServerError( 86 400, 87 `Service endpoint too long (max ${MAX_SERVICE_ENDPOINT_LENGTH})`, 88 ) 89 } 90 } 91 const verifyMethods = Object.entries(op.verificationMethods) 92 for (const [id, key] of verifyMethods) { 93 if (id.length > MAX_ID_LENGTH) { 94 throw new ServerError( 95 400, 96 `Verification Method id too long (max ${MAX_ID_LENGTH}): ${id}`, 97 ) 98 } 99 try { 100 parseDidKey(key) 101 } catch (err) { 102 throw new ServerError(400, `Invalid verificationMethod key: ${key}`) 103 } 104 } 105} 106 107const HOUR_LIMIT = 10 108const DAY_LIMIT = 30 109const WEEK_LIMIT = 100 110 111export const enforceOpsRateLimit = (ops: plc.IndexedOperation[]) => { 112 const hourAgo = new Date(Date.now() - HOUR) 113 const dayAgo = new Date(Date.now() - DAY) 114 const weekAgo = new Date(Date.now() - DAY * 7) 115 let withinHour = 0 116 let withinDay = 0 117 let withinWeek = 0 118 for (const op of ops) { 119 if (op.createdAt > weekAgo) { 120 withinWeek++ 121 if (withinWeek >= WEEK_LIMIT) { 122 throw new ServerError( 123 400, 124 `To many operations within last week (max ${WEEK_LIMIT})`, 125 ) 126 } 127 } 128 if (op.createdAt > dayAgo) { 129 withinDay++ 130 if (withinDay >= DAY_LIMIT) { 131 throw new ServerError( 132 400, 133 `To many operations within last day (max ${DAY_LIMIT})`, 134 ) 135 } 136 } 137 if (op.createdAt > hourAgo) { 138 withinHour++ 139 if (withinHour >= HOUR_LIMIT) { 140 throw new ServerError( 141 400, 142 `To many operations within last hour (max ${HOUR_LIMIT})`, 143 ) 144 } 145 } 146 } 147}