@atproto/xrpc-server 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/dist/auth.js +11 -11
- package/dist/auth.js.map +1 -1
- package/dist/errors.d.ts +67 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +202 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/rate-limiter.d.ts +69 -32
- package/dist/rate-limiter.d.ts.map +1 -1
- package/dist/rate-limiter.js +58 -41
- package/dist/rate-limiter.js.map +1 -1
- package/dist/server.d.ts +19 -14
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +151 -137
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +80 -178
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -226
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +9 -8
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +148 -108
- package/dist/util.js.map +1 -1
- package/package.json +4 -3
- package/src/auth.ts +1 -1
- package/src/errors.ts +293 -0
- package/src/index.ts +9 -3
- package/src/rate-limiter.ts +188 -96
- package/src/server.ts +198 -154
- package/src/types.ts +144 -439
- package/src/util.ts +176 -125
- package/tests/auth.test.ts +2 -2
- package/tests/bodies.test.ts +18 -27
- package/tests/errors.test.ts +1 -1
- package/tests/ipld.test.ts +15 -14
- package/tests/parameters.test.ts +4 -7
- package/tests/procedures.test.ts +22 -34
- package/tests/queries.test.ts +9 -12
- package/tests/rate-limiter.test.ts +7 -7
- package/tests/responses.test.ts +12 -15
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/rate-limiter.ts
CHANGED
|
@@ -1,76 +1,107 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'node:http'
|
|
1
2
|
import {
|
|
2
3
|
RateLimiterAbstract,
|
|
3
4
|
RateLimiterMemory,
|
|
4
5
|
RateLimiterRedis,
|
|
5
6
|
RateLimiterRes,
|
|
6
7
|
} from 'rate-limiter-flexible'
|
|
8
|
+
import { ResponseType, XRPCError } from './errors'
|
|
7
9
|
import { logger } from './logger'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
10
|
+
|
|
11
|
+
// @NOTE Do not depend (directly or indirectly) on "./types" here, as it would
|
|
12
|
+
// create a circular dependency.
|
|
13
|
+
|
|
14
|
+
export interface RateLimiterContext {
|
|
15
|
+
req: IncomingMessage
|
|
16
|
+
res?: ServerResponse
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type CalcKeyFn<C extends RateLimiterContext = RateLimiterContext> = (
|
|
20
|
+
ctx: C,
|
|
21
|
+
) => string | null
|
|
22
|
+
export type CalcPointsFn<C extends RateLimiterContext = RateLimiterContext> = (
|
|
23
|
+
ctx: C,
|
|
24
|
+
) => number
|
|
25
|
+
|
|
26
|
+
export interface RateLimiterI<
|
|
27
|
+
C extends RateLimiterContext = RateLimiterContext,
|
|
28
|
+
> {
|
|
29
|
+
consume: RateLimiterConsume<C>
|
|
30
|
+
reset: RateLimiterReset<C>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type RateLimiterConsumeOptions<
|
|
34
|
+
C extends RateLimiterContext = RateLimiterContext,
|
|
35
|
+
> = {
|
|
36
|
+
calcKey?: CalcKeyFn<C>
|
|
37
|
+
calcPoints?: CalcPointsFn<C>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type RateLimiterConsume<
|
|
41
|
+
C extends RateLimiterContext = RateLimiterContext,
|
|
42
|
+
> = (
|
|
43
|
+
ctx: C,
|
|
44
|
+
opts?: RateLimiterConsumeOptions<C>,
|
|
45
|
+
) => Promise<RateLimiterStatus | RateLimitExceededError | null>
|
|
46
|
+
|
|
47
|
+
export type RateLimiterStatus = {
|
|
48
|
+
limit: number
|
|
49
|
+
duration: number
|
|
50
|
+
remainingPoints: number
|
|
51
|
+
msBeforeNext: number
|
|
52
|
+
consumedPoints: number
|
|
53
|
+
isFirstInDuration: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type RateLimiterResetOptions<
|
|
57
|
+
C extends RateLimiterContext = RateLimiterContext,
|
|
58
|
+
> = {
|
|
59
|
+
calcKey?: CalcKeyFn<C>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type RateLimiterReset<
|
|
63
|
+
C extends RateLimiterContext = RateLimiterContext,
|
|
64
|
+
> = (ctx: C, opts?: RateLimiterResetOptions<C>) => Promise<void>
|
|
65
|
+
|
|
66
|
+
export type RateLimiterOptions<
|
|
67
|
+
C extends RateLimiterContext = RateLimiterContext,
|
|
68
|
+
> = {
|
|
23
69
|
keyPrefix: string
|
|
24
70
|
durationMs: number
|
|
25
71
|
points: number
|
|
26
|
-
calcKey
|
|
27
|
-
calcPoints
|
|
72
|
+
calcKey: CalcKeyFn<C>
|
|
73
|
+
calcPoints: CalcPointsFn<C>
|
|
28
74
|
failClosed?: boolean
|
|
29
75
|
}
|
|
30
76
|
|
|
31
|
-
export class RateLimiter
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
private failClosed?: boolean
|
|
35
|
-
|
|
36
|
-
|
|
77
|
+
export class RateLimiter<C extends RateLimiterContext = RateLimiterContext>
|
|
78
|
+
implements RateLimiterI<C>
|
|
79
|
+
{
|
|
80
|
+
private readonly failClosed?: boolean
|
|
81
|
+
private readonly calcKey: CalcKeyFn<C>
|
|
82
|
+
private readonly calcPoints: CalcPointsFn<C>
|
|
37
83
|
|
|
38
|
-
constructor(
|
|
84
|
+
constructor(
|
|
85
|
+
public limiter: RateLimiterAbstract,
|
|
86
|
+
options: RateLimiterOptions<C>,
|
|
87
|
+
) {
|
|
39
88
|
this.limiter = limiter
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
static memory(opts: RateLimiterOpts): RateLimiter {
|
|
45
|
-
const limiter = new RateLimiterMemory({
|
|
46
|
-
keyPrefix: opts.keyPrefix,
|
|
47
|
-
duration: Math.floor(opts.durationMs / 1000),
|
|
48
|
-
points: opts.points,
|
|
49
|
-
})
|
|
50
|
-
return new RateLimiter(limiter, opts)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
static redis(storeClient: unknown, opts: RateLimiterOpts): RateLimiter {
|
|
54
|
-
const limiter = new RateLimiterRedis({
|
|
55
|
-
storeClient,
|
|
56
|
-
keyPrefix: opts.keyPrefix,
|
|
57
|
-
duration: Math.floor(opts.durationMs / 1000),
|
|
58
|
-
points: opts.points,
|
|
59
|
-
})
|
|
60
|
-
return new RateLimiter(limiter, opts)
|
|
89
|
+
this.failClosed = options.failClosed ?? false
|
|
90
|
+
this.calcKey = options.calcKey
|
|
91
|
+
this.calcPoints = options.calcPoints
|
|
61
92
|
}
|
|
62
93
|
|
|
63
94
|
async consume(
|
|
64
|
-
ctx:
|
|
65
|
-
opts?: RateLimiterConsumeOptions
|
|
95
|
+
ctx: C,
|
|
96
|
+
opts?: RateLimiterConsumeOptions<C>,
|
|
66
97
|
): Promise<RateLimiterStatus | RateLimitExceededError | null> {
|
|
67
|
-
const
|
|
98
|
+
const calcKey = opts?.calcKey ?? this.calcKey
|
|
99
|
+
const key = calcKey(ctx)
|
|
68
100
|
if (key === null) {
|
|
69
101
|
return null
|
|
70
102
|
}
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
: this.calcPoints(ctx)
|
|
103
|
+
const calcPoints = opts?.calcPoints ?? this.calcPoints
|
|
104
|
+
const points = calcPoints(ctx)
|
|
74
105
|
if (points < 1) {
|
|
75
106
|
return null
|
|
76
107
|
}
|
|
@@ -100,10 +131,7 @@ export class RateLimiter implements RateLimiterI {
|
|
|
100
131
|
}
|
|
101
132
|
}
|
|
102
133
|
|
|
103
|
-
async reset(
|
|
104
|
-
ctx: XRPCReqContext,
|
|
105
|
-
opts?: RateLimiterResetOptions,
|
|
106
|
-
): Promise<void> {
|
|
134
|
+
async reset(ctx: C, opts?: RateLimiterResetOptions<C>): Promise<void> {
|
|
107
135
|
const key = opts?.calcKey ? opts.calcKey(ctx) : this.calcKey(ctx)
|
|
108
136
|
if (key === null) {
|
|
109
137
|
return
|
|
@@ -117,6 +145,33 @@ export class RateLimiter implements RateLimiterI {
|
|
|
117
145
|
}
|
|
118
146
|
}
|
|
119
147
|
|
|
148
|
+
export class MemoryRateLimiter<
|
|
149
|
+
C extends RateLimiterContext = RateLimiterContext,
|
|
150
|
+
> extends RateLimiter<C> {
|
|
151
|
+
constructor(options: RateLimiterOptions<C>) {
|
|
152
|
+
const limiter = new RateLimiterMemory({
|
|
153
|
+
keyPrefix: options.keyPrefix,
|
|
154
|
+
duration: Math.floor(options.durationMs / 1000),
|
|
155
|
+
points: options.points,
|
|
156
|
+
})
|
|
157
|
+
super(limiter, options)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export class RedisRateLimiter<
|
|
162
|
+
C extends RateLimiterContext = RateLimiterContext,
|
|
163
|
+
> extends RateLimiter<C> {
|
|
164
|
+
constructor(storeClient: unknown, options: RateLimiterOptions<C>) {
|
|
165
|
+
const limiter = new RateLimiterRedis({
|
|
166
|
+
storeClient,
|
|
167
|
+
keyPrefix: options.keyPrefix,
|
|
168
|
+
duration: Math.floor(options.durationMs / 1000),
|
|
169
|
+
points: options.points,
|
|
170
|
+
})
|
|
171
|
+
super(limiter, options)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
120
175
|
export const formatLimiterStatus = (
|
|
121
176
|
limiter: RateLimiterAbstract,
|
|
122
177
|
res: RateLimiterRes,
|
|
@@ -131,40 +186,45 @@ export const formatLimiterStatus = (
|
|
|
131
186
|
}
|
|
132
187
|
}
|
|
133
188
|
|
|
134
|
-
export type WrappedRateLimiterOptions
|
|
135
|
-
|
|
136
|
-
|
|
189
|
+
export type WrappedRateLimiterOptions<
|
|
190
|
+
C extends RateLimiterContext = RateLimiterContext,
|
|
191
|
+
> = {
|
|
192
|
+
calcKey?: CalcKeyFn<C>
|
|
193
|
+
calcPoints?: CalcPointsFn<C>
|
|
137
194
|
}
|
|
138
195
|
|
|
139
196
|
/**
|
|
140
197
|
* Wraps a {@link RateLimiterI} instance with custom key and points calculation
|
|
141
198
|
* functions.
|
|
142
199
|
*/
|
|
143
|
-
export class WrappedRateLimiter
|
|
200
|
+
export class WrappedRateLimiter<
|
|
201
|
+
C extends RateLimiterContext = RateLimiterContext,
|
|
202
|
+
> implements RateLimiterI<C>
|
|
203
|
+
{
|
|
144
204
|
private constructor(
|
|
145
|
-
private readonly rateLimiter: RateLimiterI
|
|
146
|
-
private readonly options: Readonly<WrappedRateLimiterOptions
|
|
205
|
+
private readonly rateLimiter: RateLimiterI<C>,
|
|
206
|
+
private readonly options: Readonly<WrappedRateLimiterOptions<C>>,
|
|
147
207
|
) {}
|
|
148
208
|
|
|
149
|
-
async consume(ctx:
|
|
209
|
+
async consume(ctx: C, opts?: RateLimiterConsumeOptions<C>) {
|
|
150
210
|
return this.rateLimiter.consume(ctx, {
|
|
151
211
|
calcKey: opts?.calcKey ?? this.options.calcKey,
|
|
152
212
|
calcPoints: opts?.calcPoints ?? this.options.calcPoints,
|
|
153
213
|
})
|
|
154
214
|
}
|
|
155
215
|
|
|
156
|
-
async reset(ctx:
|
|
216
|
+
async reset(ctx: C, opts?: RateLimiterResetOptions<C>) {
|
|
157
217
|
return this.rateLimiter.reset(ctx, {
|
|
158
218
|
calcKey: opts?.calcKey ?? this.options.calcKey,
|
|
159
219
|
})
|
|
160
220
|
}
|
|
161
221
|
|
|
162
|
-
static from(
|
|
163
|
-
rateLimiter: RateLimiterI
|
|
164
|
-
{ calcKey, calcPoints }: WrappedRateLimiterOptions = {},
|
|
165
|
-
): RateLimiterI {
|
|
222
|
+
static from<C extends RateLimiterContext = RateLimiterContext>(
|
|
223
|
+
rateLimiter: RateLimiterI<C>,
|
|
224
|
+
{ calcKey, calcPoints }: WrappedRateLimiterOptions<C> = {},
|
|
225
|
+
): RateLimiterI<C> {
|
|
166
226
|
if (!calcKey && !calcPoints) return rateLimiter
|
|
167
|
-
return new WrappedRateLimiter(rateLimiter, { calcKey, calcPoints })
|
|
227
|
+
return new WrappedRateLimiter<C>(rateLimiter, { calcKey, calcPoints })
|
|
168
228
|
}
|
|
169
229
|
}
|
|
170
230
|
|
|
@@ -174,22 +234,29 @@ export class WrappedRateLimiter implements RateLimiterI {
|
|
|
174
234
|
* The combined rate limiter will return the tightest (most restrictive) of all
|
|
175
235
|
* the provided rate limiters.
|
|
176
236
|
*/
|
|
177
|
-
export class CombinedRateLimiter
|
|
178
|
-
|
|
237
|
+
export class CombinedRateLimiter<
|
|
238
|
+
C extends RateLimiterContext = RateLimiterContext,
|
|
239
|
+
> implements RateLimiterI<C>
|
|
240
|
+
{
|
|
241
|
+
private constructor(
|
|
242
|
+
private readonly rateLimiters: readonly RateLimiterI<C>[],
|
|
243
|
+
) {}
|
|
179
244
|
|
|
180
|
-
async consume(ctx:
|
|
245
|
+
async consume(ctx: C, opts?: RateLimiterConsumeOptions<C>) {
|
|
181
246
|
const promises: ReturnType<RateLimiterConsume>[] = []
|
|
182
247
|
for (const rl of this.rateLimiters) promises.push(rl.consume(ctx, opts))
|
|
183
248
|
return Promise.all(promises).then(getTightestLimit)
|
|
184
249
|
}
|
|
185
250
|
|
|
186
|
-
async reset(ctx:
|
|
251
|
+
async reset(ctx: C, opts?: RateLimiterResetOptions<C>) {
|
|
187
252
|
const promises: ReturnType<RateLimiterReset>[] = []
|
|
188
253
|
for (const rl of this.rateLimiters) promises.push(rl.reset(ctx, opts))
|
|
189
254
|
await Promise.all(promises)
|
|
190
255
|
}
|
|
191
256
|
|
|
192
|
-
static from
|
|
257
|
+
static from<C extends RateLimiterContext = RateLimiterContext>(
|
|
258
|
+
rateLimiters: readonly RateLimiterI<C>[],
|
|
259
|
+
): RateLimiterI<C> | undefined {
|
|
193
260
|
if (rateLimiters.length === 0) return undefined
|
|
194
261
|
if (rateLimiters.length === 1) return rateLimiters[0]
|
|
195
262
|
return new CombinedRateLimiter(rateLimiters)
|
|
@@ -210,21 +277,25 @@ const getTightestLimit = (
|
|
|
210
277
|
return lowest
|
|
211
278
|
}
|
|
212
279
|
|
|
213
|
-
export type RouteRateLimiterOptions
|
|
214
|
-
|
|
280
|
+
export type RouteRateLimiterOptions<
|
|
281
|
+
C extends RateLimiterContext = RateLimiterContext,
|
|
282
|
+
> = {
|
|
283
|
+
bypass?: (ctx: C) => boolean
|
|
215
284
|
}
|
|
216
285
|
|
|
217
286
|
/**
|
|
218
287
|
* Wraps a {@link RateLimiterI} interface into a class that will apply the
|
|
219
288
|
* appropriate headers to the response if a limit is exceeded.
|
|
220
289
|
*/
|
|
221
|
-
export class RouteRateLimiter
|
|
290
|
+
export class RouteRateLimiter<C extends RateLimiterContext = RateLimiterContext>
|
|
291
|
+
implements RateLimiterI<C>
|
|
292
|
+
{
|
|
222
293
|
constructor(
|
|
223
|
-
private readonly rateLimiter: RateLimiterI
|
|
224
|
-
private readonly options: Readonly<RouteRateLimiterOptions
|
|
294
|
+
private readonly rateLimiter: RateLimiterI<C>,
|
|
295
|
+
private readonly options: Readonly<RouteRateLimiterOptions<C>> = {},
|
|
225
296
|
) {}
|
|
226
297
|
|
|
227
|
-
async handle(ctx:
|
|
298
|
+
async handle(ctx: C): Promise<RateLimiterStatus | null> {
|
|
228
299
|
const { bypass } = this.options
|
|
229
300
|
if (bypass && bypass(ctx)) {
|
|
230
301
|
return null
|
|
@@ -241,18 +312,18 @@ export class RouteRateLimiter implements RateLimiterI {
|
|
|
241
312
|
return result
|
|
242
313
|
}
|
|
243
314
|
|
|
244
|
-
async consume(...args: Parameters<RateLimiterConsume
|
|
315
|
+
async consume(...args: Parameters<RateLimiterConsume<C>>) {
|
|
245
316
|
return this.rateLimiter.consume(...args)
|
|
246
317
|
}
|
|
247
318
|
|
|
248
|
-
async reset(...args: Parameters<RateLimiterReset
|
|
319
|
+
async reset(...args: Parameters<RateLimiterReset<C>>) {
|
|
249
320
|
return this.rateLimiter.reset(...args)
|
|
250
321
|
}
|
|
251
322
|
|
|
252
|
-
static from(
|
|
253
|
-
rateLimiters: readonly RateLimiterI[],
|
|
254
|
-
{ bypass }: RouteRateLimiterOptions = {},
|
|
255
|
-
): RouteRateLimiter | undefined {
|
|
323
|
+
static from<C extends RateLimiterContext = RateLimiterContext>(
|
|
324
|
+
rateLimiters: readonly RateLimiterI<C>[],
|
|
325
|
+
{ bypass }: RouteRateLimiterOptions<C> = {},
|
|
326
|
+
): RouteRateLimiter<C> | undefined {
|
|
256
327
|
const rateLimiter = CombinedRateLimiter.from(rateLimiters)
|
|
257
328
|
if (!rateLimiter) return undefined
|
|
258
329
|
|
|
@@ -260,16 +331,37 @@ export class RouteRateLimiter implements RateLimiterI {
|
|
|
260
331
|
}
|
|
261
332
|
}
|
|
262
333
|
|
|
263
|
-
function setStatusHeaders
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
334
|
+
function setStatusHeaders<C extends RateLimiterContext = RateLimiterContext>(
|
|
335
|
+
ctx: C,
|
|
336
|
+
status: RateLimiterStatus,
|
|
337
|
+
) {
|
|
338
|
+
const resetAt = Math.floor((Date.now() + status.msBeforeNext) / 1e3)
|
|
339
|
+
|
|
340
|
+
ctx.res?.setHeader('RateLimit-Limit', status.limit)
|
|
341
|
+
ctx.res?.setHeader('RateLimit-Reset', resetAt)
|
|
342
|
+
ctx.res?.setHeader('RateLimit-Remaining', status.remainingPoints)
|
|
343
|
+
ctx.res?.setHeader('RateLimit-Policy', `${status.limit};w=${status.duration}`)
|
|
270
344
|
}
|
|
271
345
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
346
|
+
export class RateLimitExceededError extends XRPCError {
|
|
347
|
+
constructor(
|
|
348
|
+
public status: RateLimiterStatus,
|
|
349
|
+
errorMessage?: string,
|
|
350
|
+
customErrorName?: string,
|
|
351
|
+
options?: ErrorOptions,
|
|
352
|
+
) {
|
|
353
|
+
super(
|
|
354
|
+
ResponseType.RateLimitExceeded,
|
|
355
|
+
errorMessage,
|
|
356
|
+
customErrorName,
|
|
357
|
+
options,
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
[Symbol.hasInstance](instance: unknown): boolean {
|
|
362
|
+
return (
|
|
363
|
+
instance instanceof XRPCError &&
|
|
364
|
+
instance.type === ResponseType.RateLimitExceeded
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
}
|