@atproto/xrpc-server 0.8.0 → 0.9.0
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 +24 -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 +3 -2
- 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 +103 -78
- 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 +4 -3
- package/src/rate-limiter.ts +188 -96
- package/src/server.ts +198 -154
- package/src/types.ts +144 -439
- package/src/util.ts +116 -84
- 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/server.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
|
+
import { IncomingMessage } from 'node:http'
|
|
2
3
|
import { Readable } from 'node:stream'
|
|
3
4
|
import { pipeline } from 'node:stream/promises'
|
|
4
5
|
import express, {
|
|
5
6
|
Application,
|
|
6
7
|
ErrorRequestHandler,
|
|
7
8
|
Express,
|
|
8
|
-
NextFunction,
|
|
9
9
|
Request,
|
|
10
10
|
RequestHandler,
|
|
11
|
-
Response,
|
|
12
11
|
Router,
|
|
13
|
-
json as jsonParser,
|
|
14
|
-
text as textParser,
|
|
15
12
|
} from 'express'
|
|
16
13
|
import { check, schema } from '@atproto/common'
|
|
17
14
|
import {
|
|
@@ -22,36 +19,51 @@ import {
|
|
|
22
19
|
Lexicons,
|
|
23
20
|
lexToJson,
|
|
24
21
|
} from '@atproto/lexicon'
|
|
22
|
+
import {
|
|
23
|
+
InternalServerError,
|
|
24
|
+
InvalidRequestError,
|
|
25
|
+
MethodNotImplementedError,
|
|
26
|
+
XRPCError,
|
|
27
|
+
excludeErrorResult,
|
|
28
|
+
isErrorResult,
|
|
29
|
+
} from './errors'
|
|
25
30
|
import log, { LOGGER_NAME } from './logger'
|
|
26
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
CalcKeyFn,
|
|
33
|
+
CalcPointsFn,
|
|
34
|
+
RateLimiterI,
|
|
35
|
+
RateLimiterOptions,
|
|
36
|
+
RouteRateLimiter,
|
|
37
|
+
WrappedRateLimiter,
|
|
38
|
+
} from './rate-limiter'
|
|
27
39
|
import { ErrorFrame, Frame, MessageFrame, XrpcStreamServer } from './stream'
|
|
28
40
|
import {
|
|
41
|
+
Auth,
|
|
42
|
+
AuthResult,
|
|
29
43
|
AuthVerifier,
|
|
30
|
-
|
|
44
|
+
CatchallHandler,
|
|
45
|
+
HandlerContext,
|
|
31
46
|
HandlerSuccess,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
Input,
|
|
48
|
+
MethodConfig,
|
|
49
|
+
MethodConfigOrHandler,
|
|
35
50
|
Options,
|
|
36
51
|
Params,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
XRPCReqContext,
|
|
42
|
-
XRPCStreamHandler,
|
|
43
|
-
XRPCStreamHandlerConfig,
|
|
44
|
-
isHandlerError,
|
|
52
|
+
RouteOptions,
|
|
53
|
+
ServerRateLimitDescription,
|
|
54
|
+
StreamConfig,
|
|
55
|
+
StreamConfigOrHandler,
|
|
45
56
|
isHandlerPipeThroughBuffer,
|
|
46
57
|
isHandlerPipeThroughStream,
|
|
47
|
-
|
|
58
|
+
isSharedRateLimitOpts,
|
|
48
59
|
} from './types'
|
|
49
60
|
import {
|
|
50
61
|
asArray,
|
|
62
|
+
createInputVerifier,
|
|
51
63
|
decodeQueryParams,
|
|
64
|
+
extractUrlNsid,
|
|
52
65
|
getQueryParams,
|
|
53
66
|
setHeaders,
|
|
54
|
-
validateInput,
|
|
55
67
|
validateOutput,
|
|
56
68
|
} from './util'
|
|
57
69
|
|
|
@@ -65,43 +77,36 @@ export class Server {
|
|
|
65
77
|
subscriptions = new Map<string, XrpcStreamServer>()
|
|
66
78
|
lex = new Lexicons()
|
|
67
79
|
options: Options
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
sharedRateLimiters?: Map<string, RateLimiterI>
|
|
80
|
+
globalRateLimiter?: RouteRateLimiter<HandlerContext>
|
|
81
|
+
sharedRateLimiters?: Map<string, RateLimiterI<HandlerContext>>
|
|
71
82
|
|
|
72
83
|
constructor(lexicons?: LexiconDoc[], opts: Options = {}) {
|
|
73
84
|
if (lexicons) {
|
|
74
85
|
this.addLexicons(lexicons)
|
|
75
86
|
}
|
|
76
87
|
this.router.use(this.routes)
|
|
77
|
-
this.router.use(
|
|
88
|
+
this.router.use(this.catchall)
|
|
78
89
|
this.router.use(createErrorMiddleware(opts))
|
|
79
90
|
this.router.once('mount', (app: Application) => {
|
|
80
91
|
this.enableStreamingOnListen(app)
|
|
81
92
|
})
|
|
82
93
|
this.options = opts
|
|
83
|
-
this.middleware = {
|
|
84
|
-
json: jsonParser({ limit: opts?.payload?.jsonLimit }),
|
|
85
|
-
text: textParser({ limit: opts?.payload?.textLimit }),
|
|
86
|
-
}
|
|
87
94
|
|
|
88
95
|
if (opts.rateLimits) {
|
|
89
96
|
const { global, shared, creator, bypass } = opts.rateLimits
|
|
90
97
|
|
|
91
98
|
if (global) {
|
|
92
99
|
this.globalRateLimiter = RouteRateLimiter.from(
|
|
93
|
-
global.map((
|
|
94
|
-
creator({ ...limit, keyPrefix: `rl-${name}` }),
|
|
95
|
-
),
|
|
100
|
+
global.map((options) => creator(buildRateLimiterOptions(options))),
|
|
96
101
|
{ bypass },
|
|
97
102
|
)
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
if (shared) {
|
|
101
106
|
this.sharedRateLimiters = new Map(
|
|
102
|
-
shared.map((
|
|
103
|
-
name,
|
|
104
|
-
creator(
|
|
107
|
+
shared.map((options) => [
|
|
108
|
+
options.name,
|
|
109
|
+
creator(buildRateLimiterOptions(options)),
|
|
105
110
|
]),
|
|
106
111
|
)
|
|
107
112
|
}
|
|
@@ -111,11 +116,17 @@ export class Server {
|
|
|
111
116
|
// handlers
|
|
112
117
|
// =
|
|
113
118
|
|
|
114
|
-
method
|
|
119
|
+
method<A extends Auth = Auth>(
|
|
120
|
+
nsid: string,
|
|
121
|
+
configOrFn: MethodConfigOrHandler<A>,
|
|
122
|
+
) {
|
|
115
123
|
this.addMethod(nsid, configOrFn)
|
|
116
124
|
}
|
|
117
125
|
|
|
118
|
-
addMethod
|
|
126
|
+
addMethod<A extends Auth = Auth>(
|
|
127
|
+
nsid: string,
|
|
128
|
+
configOrFn: MethodConfigOrHandler<A>,
|
|
129
|
+
) {
|
|
119
130
|
const config =
|
|
120
131
|
typeof configOrFn === 'function' ? { handler: configOrFn } : configOrFn
|
|
121
132
|
const def = this.lex.getDef(nsid)
|
|
@@ -126,16 +137,16 @@ export class Server {
|
|
|
126
137
|
}
|
|
127
138
|
}
|
|
128
139
|
|
|
129
|
-
streamMethod(
|
|
140
|
+
streamMethod<A extends Auth = Auth>(
|
|
130
141
|
nsid: string,
|
|
131
|
-
configOrFn:
|
|
142
|
+
configOrFn: StreamConfigOrHandler<A>,
|
|
132
143
|
) {
|
|
133
144
|
this.addStreamMethod(nsid, configOrFn)
|
|
134
145
|
}
|
|
135
146
|
|
|
136
|
-
addStreamMethod(
|
|
147
|
+
addStreamMethod<A extends Auth = Auth>(
|
|
137
148
|
nsid: string,
|
|
138
|
-
configOrFn:
|
|
149
|
+
configOrFn: StreamConfigOrHandler<A>,
|
|
139
150
|
) {
|
|
140
151
|
const config =
|
|
141
152
|
typeof configOrFn === 'function' ? { handler: configOrFn } : configOrFn
|
|
@@ -163,29 +174,31 @@ export class Server {
|
|
|
163
174
|
// http
|
|
164
175
|
// =
|
|
165
176
|
|
|
166
|
-
protected async addRoute(
|
|
177
|
+
protected async addRoute<A extends Auth = Auth>(
|
|
167
178
|
nsid: string,
|
|
168
179
|
def: LexXrpcQuery | LexXrpcProcedure,
|
|
169
|
-
config:
|
|
180
|
+
config: MethodConfig<A>,
|
|
170
181
|
) {
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
middleware.push(this.middleware.json)
|
|
179
|
-
middleware.push(this.middleware.text)
|
|
182
|
+
const path = `/xrpc/${nsid}`
|
|
183
|
+
const handler = this.createHandler(nsid, def, config)
|
|
184
|
+
|
|
185
|
+
if (def.type === 'procedure') {
|
|
186
|
+
this.routes.post(path, handler)
|
|
187
|
+
} else {
|
|
188
|
+
this.routes.get(path, handler)
|
|
180
189
|
}
|
|
181
|
-
this.routes[verb](
|
|
182
|
-
`/xrpc/${nsid}`,
|
|
183
|
-
...middleware,
|
|
184
|
-
this.createHandler(nsid, def, config),
|
|
185
|
-
)
|
|
186
190
|
}
|
|
187
191
|
|
|
188
|
-
async
|
|
192
|
+
catchall: CatchallHandler = async (req, res, next) => {
|
|
193
|
+
// catchall handler only applies to XRPC routes
|
|
194
|
+
if (!req.url.startsWith('/xrpc/')) return next()
|
|
195
|
+
|
|
196
|
+
// Validate the NSID
|
|
197
|
+
const nsid = extractUrlNsid(req.url)
|
|
198
|
+
if (!nsid) {
|
|
199
|
+
return next(new InvalidRequestError('invalid xrpc path'))
|
|
200
|
+
}
|
|
201
|
+
|
|
189
202
|
if (this.globalRateLimiter) {
|
|
190
203
|
try {
|
|
191
204
|
await this.globalRateLimiter.handle({
|
|
@@ -203,7 +216,7 @@ export class Server {
|
|
|
203
216
|
|
|
204
217
|
// Ensure that known XRPC methods are only called with the correct HTTP
|
|
205
218
|
// method.
|
|
206
|
-
const def = this.lex.getDef(
|
|
219
|
+
const def = this.lex.getDef(nsid)
|
|
207
220
|
if (def) {
|
|
208
221
|
const expectedMethod =
|
|
209
222
|
def.type === 'procedure' ? 'POST' : def.type === 'query' ? 'GET' : null
|
|
@@ -225,53 +238,89 @@ export class Server {
|
|
|
225
238
|
}
|
|
226
239
|
}
|
|
227
240
|
|
|
228
|
-
|
|
241
|
+
protected createParamsVerifier(
|
|
242
|
+
nsid: string,
|
|
243
|
+
def: LexXrpcQuery | LexXrpcProcedure | LexXrpcSubscription,
|
|
244
|
+
) {
|
|
245
|
+
return (req: Request | IncomingMessage): Params => {
|
|
246
|
+
const queryParams = 'query' in req ? req.query : getQueryParams(req.url)
|
|
247
|
+
const params: Params = decodeQueryParams(def, queryParams)
|
|
248
|
+
try {
|
|
249
|
+
return this.lex.assertValidXrpcParams(nsid, params) as Params
|
|
250
|
+
} catch (e) {
|
|
251
|
+
throw new InvalidRequestError(String(e))
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
protected createInputVerifier(
|
|
229
257
|
nsid: string,
|
|
230
258
|
def: LexXrpcQuery | LexXrpcProcedure,
|
|
231
|
-
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
|
|
259
|
+
routeOpts: RouteOptions,
|
|
260
|
+
) {
|
|
261
|
+
return createInputVerifier(nsid, def, routeOpts, this.lex)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
protected createAuthVerifier<C, A extends Auth>(cfg: {
|
|
265
|
+
auth?: AuthVerifier<C, A & AuthResult>
|
|
266
|
+
}): null | ((ctx: C) => Promise<A>) {
|
|
267
|
+
const { auth } = cfg
|
|
268
|
+
if (!auth) return null
|
|
269
|
+
|
|
270
|
+
return async (ctx: C) => {
|
|
271
|
+
const result = await auth(ctx)
|
|
272
|
+
return excludeErrorResult(result)
|
|
235
273
|
}
|
|
236
|
-
|
|
237
|
-
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
createHandler<A extends Auth = Auth>(
|
|
277
|
+
nsid: string,
|
|
278
|
+
def: LexXrpcQuery | LexXrpcProcedure,
|
|
279
|
+
cfg: MethodConfig<A>,
|
|
280
|
+
): RequestHandler {
|
|
281
|
+
const authVerifier = this.createAuthVerifier(cfg)
|
|
282
|
+
const paramsVerifier = this.createParamsVerifier(nsid, def)
|
|
283
|
+
const inputVerifier = this.createInputVerifier(nsid, def, {
|
|
284
|
+
blobLimit: cfg.opts?.blobLimit ?? this.options.payload?.blobLimit,
|
|
285
|
+
jsonLimit: cfg.opts?.jsonLimit ?? this.options.payload?.jsonLimit,
|
|
286
|
+
textLimit: cfg.opts?.textLimit ?? this.options.payload?.textLimit,
|
|
287
|
+
})
|
|
288
|
+
|
|
238
289
|
const validateResOutput =
|
|
239
290
|
this.options.validateResponse === false
|
|
240
291
|
? null
|
|
241
|
-
: (output:
|
|
292
|
+
: (output: void | HandlerSuccess) =>
|
|
242
293
|
validateOutput(nsid, def, output, this.lex)
|
|
243
|
-
const assertValidXrpcParams = (params: unknown) =>
|
|
244
|
-
this.lex.assertValidXrpcParams(nsid, params)
|
|
245
294
|
|
|
246
|
-
const routeLimiter = this.createRouteRateLimiter(nsid,
|
|
295
|
+
const routeLimiter = this.createRouteRateLimiter(nsid, cfg)
|
|
247
296
|
|
|
248
297
|
return async function (req, res, next) {
|
|
249
298
|
try {
|
|
250
|
-
// validate
|
|
251
|
-
|
|
252
|
-
try {
|
|
253
|
-
params = assertValidXrpcParams(params) as Params
|
|
254
|
-
} catch (e) {
|
|
255
|
-
throw new InvalidRequestError(String(e))
|
|
256
|
-
}
|
|
257
|
-
const input = validateReqInput(req)
|
|
299
|
+
// parse & validate params
|
|
300
|
+
const params: Params = paramsVerifier(req)
|
|
258
301
|
|
|
259
|
-
|
|
302
|
+
// authenticate request
|
|
303
|
+
const auth: A = authVerifier
|
|
304
|
+
? await authVerifier({ req, res, params })
|
|
305
|
+
: (undefined as A)
|
|
260
306
|
|
|
261
|
-
|
|
307
|
+
// parse & validate input
|
|
308
|
+
const input: Input = await inputVerifier(req, res)
|
|
309
|
+
|
|
310
|
+
const ctx: HandlerContext<A> = {
|
|
262
311
|
params,
|
|
263
312
|
input,
|
|
264
|
-
auth
|
|
313
|
+
auth,
|
|
265
314
|
req,
|
|
266
315
|
res,
|
|
267
|
-
resetRouteRateLimits: async () => routeLimiter?.reset(
|
|
316
|
+
resetRouteRateLimits: async () => routeLimiter?.reset(ctx),
|
|
268
317
|
}
|
|
269
318
|
|
|
270
319
|
// handle rate limits
|
|
271
|
-
if (routeLimiter) await routeLimiter.handle(
|
|
320
|
+
if (routeLimiter) await routeLimiter.handle(ctx)
|
|
272
321
|
|
|
273
322
|
// run the handler
|
|
274
|
-
const output = await
|
|
323
|
+
const output = await cfg.handler(ctx)
|
|
275
324
|
|
|
276
325
|
if (!output) {
|
|
277
326
|
validateResOutput?.(output)
|
|
@@ -287,7 +336,7 @@ export class Server {
|
|
|
287
336
|
res.status(200)
|
|
288
337
|
res.header('Content-Type', output.encoding)
|
|
289
338
|
res.end(output.buffer)
|
|
290
|
-
} else if (
|
|
339
|
+
} else if (isErrorResult(output)) {
|
|
291
340
|
next(XRPCError.fromError(output))
|
|
292
341
|
} else {
|
|
293
342
|
validateResOutput?.(output)
|
|
@@ -328,34 +377,29 @@ export class Server {
|
|
|
328
377
|
}
|
|
329
378
|
}
|
|
330
379
|
|
|
331
|
-
protected async addSubscription(
|
|
380
|
+
protected async addSubscription<A extends Auth = Auth>(
|
|
332
381
|
nsid: string,
|
|
333
382
|
def: LexXrpcSubscription,
|
|
334
|
-
|
|
383
|
+
cfg: StreamConfig<A>,
|
|
335
384
|
) {
|
|
336
|
-
const
|
|
337
|
-
|
|
385
|
+
const paramsVerifier = this.createParamsVerifier(nsid, def)
|
|
386
|
+
const authVerifier = this.createAuthVerifier(cfg)
|
|
387
|
+
|
|
388
|
+
const { handler } = cfg
|
|
338
389
|
this.subscriptions.set(
|
|
339
390
|
nsid,
|
|
340
391
|
new XrpcStreamServer({
|
|
341
392
|
noServer: true,
|
|
342
393
|
handler: async function* (req, signal) {
|
|
343
394
|
try {
|
|
344
|
-
// authenticate request
|
|
345
|
-
const auth = await config.auth?.({ req })
|
|
346
|
-
if (isHandlerError(auth)) {
|
|
347
|
-
throw XRPCError.fromHandlerError(auth)
|
|
348
|
-
}
|
|
349
395
|
// validate request
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
396
|
+
const params = paramsVerifier(req)
|
|
397
|
+
// authenticate request
|
|
398
|
+
const auth = authVerifier
|
|
399
|
+
? await authVerifier({ req, params })
|
|
400
|
+
: (undefined as A)
|
|
356
401
|
// stream
|
|
357
|
-
const
|
|
358
|
-
for await (const item of items) {
|
|
402
|
+
for await (const item of handler({ req, params, auth, signal })) {
|
|
359
403
|
if (item instanceof Frame) {
|
|
360
404
|
yield item
|
|
361
405
|
continue
|
|
@@ -397,10 +441,8 @@ export class Server {
|
|
|
397
441
|
// @ts-ignore the args spread
|
|
398
442
|
const httpServer = _listen.call(app, ...args)
|
|
399
443
|
httpServer.on('upgrade', (req, socket, head) => {
|
|
400
|
-
const
|
|
401
|
-
const sub =
|
|
402
|
-
? this.subscriptions.get(url.pathname.replace('/xrpc/', ''))
|
|
403
|
-
: undefined
|
|
444
|
+
const nsid = req.url ? extractUrlNsid(req.url) : undefined
|
|
445
|
+
const sub = nsid ? this.subscriptions.get(nsid) : undefined
|
|
404
446
|
if (!sub) return socket.destroy()
|
|
405
447
|
sub.wss.handleUpgrade(req, socket, head, (ws) =>
|
|
406
448
|
sub.wss.emit('connection', ws, req),
|
|
@@ -410,74 +452,57 @@ export class Server {
|
|
|
410
452
|
}
|
|
411
453
|
}
|
|
412
454
|
|
|
413
|
-
private createRouteRateLimiter(
|
|
455
|
+
private createRouteRateLimiter<A extends Auth, C extends HandlerContext>(
|
|
414
456
|
nsid: string,
|
|
415
|
-
config:
|
|
416
|
-
): RouteRateLimiter | undefined {
|
|
457
|
+
config: MethodConfig<A>,
|
|
458
|
+
): RouteRateLimiter<C> | undefined {
|
|
459
|
+
// @NOTE global & shared rate limiters are instantiated with a context of
|
|
460
|
+
// HandlerContext which is compatible (more generic) with the context of
|
|
461
|
+
// this route specific rate limiters (C). For this reason, it's safe to
|
|
462
|
+
// cast these with an `any` context
|
|
463
|
+
|
|
464
|
+
const globalRateLimiter = this.globalRateLimiter as
|
|
465
|
+
| RouteRateLimiter<any>
|
|
466
|
+
| undefined
|
|
467
|
+
|
|
417
468
|
// No route specific rate limiting configured, use the global rate limiter.
|
|
418
|
-
if (!config.rateLimit) return
|
|
469
|
+
if (!config.rateLimit) return globalRateLimiter
|
|
419
470
|
|
|
420
471
|
const { rateLimits } = this.options
|
|
421
472
|
|
|
422
473
|
// @NOTE Silently ignore creation of route specific rate limiter if the
|
|
423
474
|
// `rateLimits` options was not provided to the constructor.
|
|
424
|
-
if (!rateLimits) return
|
|
475
|
+
if (!rateLimits) return globalRateLimiter
|
|
425
476
|
|
|
426
477
|
const { creator, bypass } = rateLimits
|
|
427
478
|
|
|
428
479
|
const rateLimiters = asArray(config.rateLimit).map((options, i) => {
|
|
429
|
-
if (
|
|
480
|
+
if (isSharedRateLimitOpts(options)) {
|
|
430
481
|
const rateLimiter = this.sharedRateLimiters?.get(options.name)
|
|
431
482
|
|
|
432
483
|
// The route config references a shared rate limiter that does not
|
|
433
484
|
// exist. This is a configuration error.
|
|
434
485
|
assert(rateLimiter, `Shared rate limiter "${options.name}" not defined`)
|
|
435
486
|
|
|
436
|
-
return WrappedRateLimiter.from(rateLimiter, options)
|
|
487
|
+
return WrappedRateLimiter.from<any>(rateLimiter, options)
|
|
437
488
|
} else {
|
|
438
|
-
return creator({
|
|
489
|
+
return creator({
|
|
490
|
+
...options,
|
|
491
|
+
calcKey: options.calcKey ?? defaultKey,
|
|
492
|
+
calcPoints: options.calcPoints ?? defaultPoints,
|
|
493
|
+
keyPrefix: `${nsid}-${i}`,
|
|
494
|
+
})
|
|
439
495
|
}
|
|
440
496
|
})
|
|
441
497
|
|
|
442
498
|
// If the route config contains an empty array, use global rate limiter.
|
|
443
|
-
if (!rateLimiters.length) return
|
|
499
|
+
if (!rateLimiters.length) return globalRateLimiter
|
|
444
500
|
|
|
445
501
|
// The global rate limiter (if present) should be applied in addition to
|
|
446
502
|
// the route specific rate limiters.
|
|
447
|
-
if (
|
|
503
|
+
if (globalRateLimiter) rateLimiters.push(globalRateLimiter)
|
|
448
504
|
|
|
449
|
-
return RouteRateLimiter.from(rateLimiters, { bypass })
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const kRequestLocals = Symbol('requestLocals')
|
|
454
|
-
|
|
455
|
-
function createLocalsMiddleware(nsid: string): RequestHandler {
|
|
456
|
-
return function (req, _res, next) {
|
|
457
|
-
const locals: RequestLocals = { auth: undefined, nsid }
|
|
458
|
-
req[kRequestLocals] = locals
|
|
459
|
-
return next()
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
type RequestLocals = {
|
|
464
|
-
auth: HandlerAuth | undefined
|
|
465
|
-
nsid: string
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function createAuthMiddleware(verifier: AuthVerifier): RequestHandler {
|
|
469
|
-
return async function (req, res, next) {
|
|
470
|
-
try {
|
|
471
|
-
const result = await verifier({ req, res })
|
|
472
|
-
if (isHandlerError(result)) {
|
|
473
|
-
throw XRPCError.fromHandlerError(result)
|
|
474
|
-
}
|
|
475
|
-
const locals: RequestLocals = req[kRequestLocals]
|
|
476
|
-
locals.auth = result
|
|
477
|
-
next()
|
|
478
|
-
} catch (err: unknown) {
|
|
479
|
-
next(err)
|
|
480
|
-
}
|
|
505
|
+
return RouteRateLimiter.from<any>(rateLimiters, { bypass })
|
|
481
506
|
}
|
|
482
507
|
}
|
|
483
508
|
|
|
@@ -485,9 +510,7 @@ function createErrorMiddleware({
|
|
|
485
510
|
errorParser = (err) => XRPCError.fromError(err),
|
|
486
511
|
}: Options): ErrorRequestHandler {
|
|
487
512
|
return (err, req, res, next) => {
|
|
488
|
-
const
|
|
489
|
-
const methodSuffix = locals ? ` method ${locals.nsid}` : ''
|
|
490
|
-
|
|
513
|
+
const nsid = extractUrlNsid(req.originalUrl)
|
|
491
514
|
const xrpcError = errorParser(err)
|
|
492
515
|
|
|
493
516
|
// Use the request's logger (if available) to benefit from request context
|
|
@@ -496,6 +519,10 @@ function createErrorMiddleware({
|
|
|
496
519
|
|
|
497
520
|
const isInternalError = xrpcError instanceof InternalServerError
|
|
498
521
|
|
|
522
|
+
const msgPrefix = isInternalError ? 'unhandled exception' : 'error'
|
|
523
|
+
const msgSuffix = nsid ? `xrpc method ${nsid}` : `${req.method} ${req.url}`
|
|
524
|
+
const msg = `${msgPrefix} in ${msgSuffix}`
|
|
525
|
+
|
|
499
526
|
logger.error(
|
|
500
527
|
{
|
|
501
528
|
// @NOTE Computation of error stack is an expensive operation, so
|
|
@@ -506,7 +533,7 @@ function createErrorMiddleware({
|
|
|
506
533
|
: toSimplifiedErrorLike(err),
|
|
507
534
|
|
|
508
535
|
// XRPC specific properties, for easier browsing of logs
|
|
509
|
-
nsid
|
|
536
|
+
nsid,
|
|
510
537
|
type: xrpcError.type,
|
|
511
538
|
status: xrpcError.statusCode,
|
|
512
539
|
payload: xrpcError.payload,
|
|
@@ -515,9 +542,7 @@ function createErrorMiddleware({
|
|
|
515
542
|
// the name of the pino-http logger, to ensure consistency across logs.
|
|
516
543
|
name: LOGGER_NAME,
|
|
517
544
|
},
|
|
518
|
-
|
|
519
|
-
? `unhandled exception in xrpc${methodSuffix}`
|
|
520
|
-
: `error in xrpc${methodSuffix}`,
|
|
545
|
+
msg,
|
|
521
546
|
)
|
|
522
547
|
|
|
523
548
|
if (res.headersSent) {
|
|
@@ -528,7 +553,7 @@ function createErrorMiddleware({
|
|
|
528
553
|
}
|
|
529
554
|
}
|
|
530
555
|
|
|
531
|
-
function isPinoHttpRequest(req:
|
|
556
|
+
function isPinoHttpRequest(req: IncomingMessage): req is IncomingMessage & {
|
|
532
557
|
log: { error: (obj: unknown, msg: string) => void }
|
|
533
558
|
} {
|
|
534
559
|
return typeof (req as { log?: any }).log?.error === 'function'
|
|
@@ -553,3 +578,22 @@ function toSimplifiedErrorLike(err: unknown): unknown {
|
|
|
553
578
|
|
|
554
579
|
return err
|
|
555
580
|
}
|
|
581
|
+
|
|
582
|
+
function buildRateLimiterOptions<C extends HandlerContext = HandlerContext>({
|
|
583
|
+
name,
|
|
584
|
+
calcKey = defaultKey,
|
|
585
|
+
calcPoints = defaultPoints,
|
|
586
|
+
...desc
|
|
587
|
+
}: ServerRateLimitDescription<C>): RateLimiterOptions<C> {
|
|
588
|
+
return { ...desc, calcKey, calcPoints, keyPrefix: `rl-${name}` }
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const defaultPoints: CalcPointsFn = () => 1
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* @note when using a proxy, ensure headers are getting forwarded correctly:
|
|
595
|
+
* `app.set('trust proxy', true)`
|
|
596
|
+
*
|
|
597
|
+
* @see {@link https://expressjs.com/en/guide/behind-proxies.html}
|
|
598
|
+
*/
|
|
599
|
+
const defaultKey: CalcKeyFn<HandlerContext> = ({ req }) => req.ip
|