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}