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}