@atproto/xrpc-server 0.11.4 → 0.11.6
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 +27 -0
- package/package.json +26 -21
- package/jest.config.cjs +0 -21
- package/src/auth.ts +0 -235
- package/src/errors.ts +0 -312
- package/src/index.ts +0 -14
- package/src/logger.ts +0 -8
- package/src/rate-limiter-http.ts +0 -82
- package/src/rate-limiter.ts +0 -279
- package/src/server.ts +0 -858
- package/src/stream/frames.ts +0 -125
- package/src/stream/index.ts +0 -5
- package/src/stream/logger.ts +0 -6
- package/src/stream/server.ts +0 -66
- package/src/stream/stream.ts +0 -39
- package/src/stream/subscription.ts +0 -96
- package/src/stream/types.ts +0 -27
- package/src/types.ts +0 -330
- package/src/util.ts +0 -708
- package/tests/_util.ts +0 -124
- package/tests/auth.test.ts +0 -333
- package/tests/bodies.test.ts +0 -608
- package/tests/errors.test.ts +0 -299
- package/tests/frames.test.ts +0 -135
- package/tests/ipld.test.ts +0 -97
- package/tests/parameters.test.ts +0 -331
- package/tests/parsing.test.ts +0 -89
- package/tests/procedures.test.ts +0 -176
- package/tests/queries.test.ts +0 -140
- package/tests/rate-limiter.test.ts +0 -312
- package/tests/responses.test.ts +0 -72
- package/tests/stream.test.ts +0 -169
- package/tests/subscriptions.test.ts +0 -398
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -7
package/src/rate-limiter-http.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import {
|
|
3
|
-
CombinedRateLimiter,
|
|
4
|
-
RateLimitExceededError,
|
|
5
|
-
RateLimiterConsume,
|
|
6
|
-
RateLimiterI,
|
|
7
|
-
RateLimiterReset,
|
|
8
|
-
RateLimiterStatus,
|
|
9
|
-
} from './rate-limiter.js'
|
|
10
|
-
|
|
11
|
-
export interface HttpRateLimiterContext {
|
|
12
|
-
req: IncomingMessage
|
|
13
|
-
res?: ServerResponse
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type HttpRateLimiterOptions<
|
|
17
|
-
C extends HttpRateLimiterContext = HttpRateLimiterContext,
|
|
18
|
-
> = {
|
|
19
|
-
bypass?: (ctx: C) => boolean
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Wraps a {@link RateLimiterI} class with an {@link RateLimiterI}
|
|
24
|
-
* implementation that will apply the appropriate headers to the response if a
|
|
25
|
-
* limit is exceeded.
|
|
26
|
-
*/
|
|
27
|
-
export class HttpRateLimiter<
|
|
28
|
-
C extends HttpRateLimiterContext = HttpRateLimiterContext,
|
|
29
|
-
> implements RateLimiterI<C>
|
|
30
|
-
{
|
|
31
|
-
constructor(
|
|
32
|
-
private readonly rateLimiter: RateLimiterI<C>,
|
|
33
|
-
private readonly options: Readonly<HttpRateLimiterOptions<C>> = {},
|
|
34
|
-
) {}
|
|
35
|
-
|
|
36
|
-
async handle(ctx: C): Promise<void> {
|
|
37
|
-
const { bypass } = this.options
|
|
38
|
-
if (bypass && bypass(ctx)) return
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
const result = await this.consume(ctx)
|
|
42
|
-
if (result != null) {
|
|
43
|
-
setStatusHeaders(ctx, result)
|
|
44
|
-
}
|
|
45
|
-
} catch (err) {
|
|
46
|
-
if (err instanceof RateLimitExceededError) {
|
|
47
|
-
setStatusHeaders(ctx, err.status)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
throw err
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async consume(...args: Parameters<RateLimiterConsume<C>>) {
|
|
55
|
-
return this.rateLimiter.consume(...args)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async reset(...args: Parameters<RateLimiterReset<C>>) {
|
|
59
|
-
return this.rateLimiter.reset(...args)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
static from<C extends HttpRateLimiterContext = HttpRateLimiterContext>(
|
|
63
|
-
rateLimiters: readonly RateLimiterI<C>[],
|
|
64
|
-
{ bypass }: HttpRateLimiterOptions<C> = {},
|
|
65
|
-
): HttpRateLimiter<C> | undefined {
|
|
66
|
-
const rateLimiter = CombinedRateLimiter.from(rateLimiters)
|
|
67
|
-
if (!rateLimiter) return undefined
|
|
68
|
-
|
|
69
|
-
return new HttpRateLimiter<C>(rateLimiter, { bypass })
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function setStatusHeaders<
|
|
74
|
-
C extends HttpRateLimiterContext = HttpRateLimiterContext,
|
|
75
|
-
>(ctx: C, status: RateLimiterStatus) {
|
|
76
|
-
const resetAt = Math.floor((Date.now() + status.msBeforeNext) / 1e3)
|
|
77
|
-
|
|
78
|
-
ctx.res?.setHeader('RateLimit-Limit', status.limit)
|
|
79
|
-
ctx.res?.setHeader('RateLimit-Reset', resetAt)
|
|
80
|
-
ctx.res?.setHeader('RateLimit-Remaining', status.remainingPoints)
|
|
81
|
-
ctx.res?.setHeader('RateLimit-Policy', `${status.limit};w=${status.duration}`)
|
|
82
|
-
}
|
package/src/rate-limiter.ts
DELETED
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
RateLimiterAbstract,
|
|
3
|
-
RateLimiterMemory,
|
|
4
|
-
RateLimiterRedis,
|
|
5
|
-
RateLimiterRes,
|
|
6
|
-
} from 'rate-limiter-flexible'
|
|
7
|
-
import { ResponseType, XRPCError } from './errors.js'
|
|
8
|
-
|
|
9
|
-
// @NOTE Do not depend (directly or indirectly) on "./types" here, as it would
|
|
10
|
-
// create a circular dependency.
|
|
11
|
-
|
|
12
|
-
export type { RateLimiterAbstract }
|
|
13
|
-
|
|
14
|
-
export type CalcKeyFn<C = unknown> = (ctx: C) => string | null
|
|
15
|
-
export type CalcPointsFn<C = unknown> = (ctx: C) => number
|
|
16
|
-
|
|
17
|
-
export interface RateLimiterI<C = unknown> {
|
|
18
|
-
consume: RateLimiterConsume<C>
|
|
19
|
-
reset: RateLimiterReset<C>
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export type RateLimiterConsumeOptions<C = unknown> = {
|
|
23
|
-
calcKey?: CalcKeyFn<C>
|
|
24
|
-
calcPoints?: CalcPointsFn<C>
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export type RateLimiterConsume<C = unknown> = (
|
|
28
|
-
ctx: C,
|
|
29
|
-
opts?: RateLimiterConsumeOptions<C>,
|
|
30
|
-
) => Promise<RateLimiterStatus | null>
|
|
31
|
-
|
|
32
|
-
export type RateLimiterStatus = {
|
|
33
|
-
limit: number
|
|
34
|
-
duration: number
|
|
35
|
-
remainingPoints: number
|
|
36
|
-
msBeforeNext: number
|
|
37
|
-
consumedPoints: number
|
|
38
|
-
isFirstInDuration: boolean
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export type RateLimiterResetOptions<C = unknown> = {
|
|
42
|
-
calcKey?: CalcKeyFn<C>
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export type RateLimiterReset<C = unknown> = (
|
|
46
|
-
ctx: C,
|
|
47
|
-
opts?: RateLimiterResetOptions<C>,
|
|
48
|
-
) => Promise<void>
|
|
49
|
-
|
|
50
|
-
export type RateLimiterErrorHandlerDetails = {
|
|
51
|
-
key: string
|
|
52
|
-
points: number
|
|
53
|
-
limiter: {
|
|
54
|
-
keyPrefix: string
|
|
55
|
-
points: number
|
|
56
|
-
duration: number
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
export type RateLimiterErrorHandler<C = unknown> = (
|
|
60
|
-
err: unknown,
|
|
61
|
-
ctx: C,
|
|
62
|
-
details: RateLimiterErrorHandlerDetails,
|
|
63
|
-
) => Promise<RateLimiterStatus | null>
|
|
64
|
-
|
|
65
|
-
export type RateLimiterOptions<C = unknown> = {
|
|
66
|
-
keyPrefix: string
|
|
67
|
-
durationMs: number
|
|
68
|
-
points: number
|
|
69
|
-
calcKey: CalcKeyFn<C>
|
|
70
|
-
calcPoints: CalcPointsFn<C>
|
|
71
|
-
onError?: RateLimiterErrorHandler<C>
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export class RateLimiter<C = unknown> implements RateLimiterI<C> {
|
|
75
|
-
private readonly onError?: RateLimiterErrorHandler<C>
|
|
76
|
-
private readonly calcKey: CalcKeyFn<C>
|
|
77
|
-
private readonly calcPoints: CalcPointsFn<C>
|
|
78
|
-
|
|
79
|
-
constructor(
|
|
80
|
-
public limiter: RateLimiterAbstract,
|
|
81
|
-
options: RateLimiterOptions<C>,
|
|
82
|
-
) {
|
|
83
|
-
this.limiter = limiter
|
|
84
|
-
this.onError = options.onError
|
|
85
|
-
this.calcKey = options.calcKey
|
|
86
|
-
this.calcPoints = options.calcPoints
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async consume(
|
|
90
|
-
ctx: C,
|
|
91
|
-
opts?: RateLimiterConsumeOptions<C>,
|
|
92
|
-
): Promise<RateLimiterStatus | null> {
|
|
93
|
-
const calcKey = opts?.calcKey ?? this.calcKey
|
|
94
|
-
const key = calcKey(ctx)
|
|
95
|
-
if (key === null) {
|
|
96
|
-
return null
|
|
97
|
-
}
|
|
98
|
-
const calcPoints = opts?.calcPoints ?? this.calcPoints
|
|
99
|
-
const points = calcPoints(ctx)
|
|
100
|
-
if (points < 1) {
|
|
101
|
-
return null
|
|
102
|
-
}
|
|
103
|
-
const { limiter } = this
|
|
104
|
-
try {
|
|
105
|
-
const res = await limiter.consume(key, points)
|
|
106
|
-
return formatLimiterStatus(limiter, res)
|
|
107
|
-
} catch (err) {
|
|
108
|
-
if (err instanceof RateLimiterRes) {
|
|
109
|
-
// Wrap rate-limiter-flexible error into our own error type
|
|
110
|
-
const status = formatLimiterStatus(limiter, err)
|
|
111
|
-
throw new RateLimitExceededError(status)
|
|
112
|
-
} else if (err instanceof RateLimitExceededError) {
|
|
113
|
-
// Propagate RateLimitExceededError errors
|
|
114
|
-
throw err
|
|
115
|
-
} else {
|
|
116
|
-
// Most likely a system error (failed to connect to Redis, etc). Allow
|
|
117
|
-
// the caller to decide how to handle it (fail open by allowing the
|
|
118
|
-
// request, fail closed by rejecting the request, etc).
|
|
119
|
-
const { onError } = this
|
|
120
|
-
if (onError) return onError(err, ctx, { key, points, limiter })
|
|
121
|
-
|
|
122
|
-
throw err
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async reset(ctx: C, opts?: RateLimiterResetOptions<C>): Promise<void> {
|
|
128
|
-
const key = opts?.calcKey ? opts.calcKey(ctx) : this.calcKey(ctx)
|
|
129
|
-
if (key === null) {
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
await this.limiter.delete(key)
|
|
135
|
-
} catch (cause) {
|
|
136
|
-
throw new Error(`rate limiter failed to reset key: ${key}`, { cause })
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export class MemoryRateLimiter<C = unknown> extends RateLimiter<C> {
|
|
142
|
-
constructor(options: RateLimiterOptions<C>) {
|
|
143
|
-
const limiter = new RateLimiterMemory({
|
|
144
|
-
keyPrefix: options.keyPrefix,
|
|
145
|
-
duration: Math.floor(options.durationMs / 1000),
|
|
146
|
-
points: options.points,
|
|
147
|
-
})
|
|
148
|
-
super(limiter, options)
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export class RedisRateLimiter<C = unknown> extends RateLimiter<C> {
|
|
153
|
-
constructor(storeClient: unknown, options: RateLimiterOptions<C>) {
|
|
154
|
-
const limiter = new RateLimiterRedis({
|
|
155
|
-
storeClient,
|
|
156
|
-
keyPrefix: options.keyPrefix,
|
|
157
|
-
duration: Math.floor(options.durationMs / 1000),
|
|
158
|
-
points: options.points,
|
|
159
|
-
})
|
|
160
|
-
super(limiter, options)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export const formatLimiterStatus = (
|
|
165
|
-
limiter: RateLimiterAbstract,
|
|
166
|
-
res: RateLimiterRes,
|
|
167
|
-
): RateLimiterStatus => {
|
|
168
|
-
return {
|
|
169
|
-
limit: limiter.points,
|
|
170
|
-
duration: limiter.duration,
|
|
171
|
-
remainingPoints: res.remainingPoints,
|
|
172
|
-
msBeforeNext: res.msBeforeNext,
|
|
173
|
-
consumedPoints: res.consumedPoints,
|
|
174
|
-
isFirstInDuration: res.isFirstInDuration,
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export type WrappedRateLimiterOptions<C = unknown> = {
|
|
179
|
-
calcKey?: CalcKeyFn<C>
|
|
180
|
-
calcPoints?: CalcPointsFn<C>
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Wraps a {@link RateLimiterI} instance with custom key and points calculation
|
|
185
|
-
* functions.
|
|
186
|
-
*/
|
|
187
|
-
export class WrappedRateLimiter<C = unknown> implements RateLimiterI<C> {
|
|
188
|
-
private constructor(
|
|
189
|
-
private readonly rateLimiter: RateLimiterI<C>,
|
|
190
|
-
private readonly options: Readonly<WrappedRateLimiterOptions<C>>,
|
|
191
|
-
) {}
|
|
192
|
-
|
|
193
|
-
async consume(ctx: C, opts?: RateLimiterConsumeOptions<C>) {
|
|
194
|
-
return this.rateLimiter.consume(ctx, {
|
|
195
|
-
calcKey: opts?.calcKey ?? this.options.calcKey,
|
|
196
|
-
calcPoints: opts?.calcPoints ?? this.options.calcPoints,
|
|
197
|
-
})
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async reset(ctx: C, opts?: RateLimiterResetOptions<C>) {
|
|
201
|
-
return this.rateLimiter.reset(ctx, {
|
|
202
|
-
calcKey: opts?.calcKey ?? this.options.calcKey,
|
|
203
|
-
})
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
static from<C = unknown>(
|
|
207
|
-
rateLimiter: RateLimiterI<C>,
|
|
208
|
-
{ calcKey, calcPoints }: WrappedRateLimiterOptions<C> = {},
|
|
209
|
-
): RateLimiterI<C> {
|
|
210
|
-
if (!calcKey && !calcPoints) return rateLimiter
|
|
211
|
-
return new WrappedRateLimiter<C>(rateLimiter, { calcKey, calcPoints })
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Combines multiple rate limiters into one.
|
|
217
|
-
*
|
|
218
|
-
* The combined rate limiter will return the tightest (most restrictive) of all
|
|
219
|
-
* the provided rate limiters.
|
|
220
|
-
*/
|
|
221
|
-
export class CombinedRateLimiter<C = unknown> implements RateLimiterI<C> {
|
|
222
|
-
private constructor(
|
|
223
|
-
private readonly rateLimiters: readonly RateLimiterI<C>[],
|
|
224
|
-
) {}
|
|
225
|
-
|
|
226
|
-
async consume(ctx: C, opts?: RateLimiterConsumeOptions<C>) {
|
|
227
|
-
const promises: ReturnType<RateLimiterConsume>[] = []
|
|
228
|
-
for (const rl of this.rateLimiters) promises.push(rl.consume(ctx, opts))
|
|
229
|
-
const results = await Promise.all(promises)
|
|
230
|
-
|
|
231
|
-
// Compute the tightest rate limit status (the one with the least remaining points)
|
|
232
|
-
let lowest: RateLimiterStatus | null = null
|
|
233
|
-
for (const resp of results) {
|
|
234
|
-
if (resp === null) continue
|
|
235
|
-
if (lowest === null || resp.remainingPoints < lowest.remainingPoints) {
|
|
236
|
-
lowest = resp
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return lowest
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async reset(ctx: C, opts?: RateLimiterResetOptions<C>) {
|
|
244
|
-
const promises: ReturnType<RateLimiterReset>[] = []
|
|
245
|
-
for (const rl of this.rateLimiters) promises.push(rl.reset(ctx, opts))
|
|
246
|
-
await Promise.all(promises)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
static from<C = unknown>(
|
|
250
|
-
rateLimiters: readonly RateLimiterI<C>[],
|
|
251
|
-
): RateLimiterI<C> | undefined {
|
|
252
|
-
if (rateLimiters.length === 0) return undefined
|
|
253
|
-
if (rateLimiters.length === 1) return rateLimiters[0]
|
|
254
|
-
return new CombinedRateLimiter(rateLimiters)
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export class RateLimitExceededError extends XRPCError {
|
|
259
|
-
constructor(
|
|
260
|
-
public status: RateLimiterStatus,
|
|
261
|
-
errorMessage?: string,
|
|
262
|
-
customErrorName?: string,
|
|
263
|
-
options?: ErrorOptions,
|
|
264
|
-
) {
|
|
265
|
-
super(
|
|
266
|
-
ResponseType.RateLimitExceeded,
|
|
267
|
-
errorMessage,
|
|
268
|
-
customErrorName,
|
|
269
|
-
options,
|
|
270
|
-
)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
[Symbol.hasInstance](instance: unknown): boolean {
|
|
274
|
-
return (
|
|
275
|
-
instance instanceof XRPCError &&
|
|
276
|
-
instance.type === ResponseType.RateLimitExceeded
|
|
277
|
-
)
|
|
278
|
-
}
|
|
279
|
-
}
|