@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/auth.js +11 -11
  3. package/dist/auth.js.map +1 -1
  4. package/dist/errors.d.ts +67 -0
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/errors.js +202 -0
  7. package/dist/errors.js.map +1 -0
  8. package/dist/index.d.ts +4 -3
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/rate-limiter.d.ts +69 -32
  13. package/dist/rate-limiter.d.ts.map +1 -1
  14. package/dist/rate-limiter.js +58 -41
  15. package/dist/rate-limiter.js.map +1 -1
  16. package/dist/server.d.ts +19 -14
  17. package/dist/server.d.ts.map +1 -1
  18. package/dist/server.js +151 -137
  19. package/dist/server.js.map +1 -1
  20. package/dist/types.d.ts +80 -178
  21. package/dist/types.d.ts.map +1 -1
  22. package/dist/types.js +9 -226
  23. package/dist/types.js.map +1 -1
  24. package/dist/util.d.ts +9 -8
  25. package/dist/util.d.ts.map +1 -1
  26. package/dist/util.js +103 -78
  27. package/dist/util.js.map +1 -1
  28. package/package.json +4 -3
  29. package/src/auth.ts +1 -1
  30. package/src/errors.ts +293 -0
  31. package/src/index.ts +4 -3
  32. package/src/rate-limiter.ts +188 -96
  33. package/src/server.ts +198 -154
  34. package/src/types.ts +144 -439
  35. package/src/util.ts +116 -84
  36. package/tests/auth.test.ts +2 -2
  37. package/tests/bodies.test.ts +18 -27
  38. package/tests/errors.test.ts +1 -1
  39. package/tests/ipld.test.ts +15 -14
  40. package/tests/parameters.test.ts +4 -7
  41. package/tests/procedures.test.ts +22 -34
  42. package/tests/queries.test.ts +9 -12
  43. package/tests/rate-limiter.test.ts +7 -7
  44. package/tests/responses.test.ts +12 -15
  45. 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 { RouteRateLimiter, WrappedRateLimiter } from './rate-limiter'
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
- HandlerAuth,
44
+ CatchallHandler,
45
+ HandlerContext,
31
46
  HandlerSuccess,
32
- InternalServerError,
33
- InvalidRequestError,
34
- MethodNotImplementedError,
47
+ Input,
48
+ MethodConfig,
49
+ MethodConfigOrHandler,
35
50
  Options,
36
51
  Params,
37
- RateLimiterI,
38
- XRPCError,
39
- XRPCHandler,
40
- XRPCHandlerConfig,
41
- XRPCReqContext,
42
- XRPCStreamHandler,
43
- XRPCStreamHandlerConfig,
44
- isHandlerError,
52
+ RouteOptions,
53
+ ServerRateLimitDescription,
54
+ StreamConfig,
55
+ StreamConfigOrHandler,
45
56
  isHandlerPipeThroughBuffer,
46
57
  isHandlerPipeThroughStream,
47
- isShared,
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
- middleware: Record<'json' | 'text', RequestHandler>
69
- globalRateLimiter?: RouteRateLimiter
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('/xrpc/:methodId', this.catchall.bind(this))
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(({ name, ...limit }) =>
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(({ name, ...limit }) => [
103
- name,
104
- creator({ ...limit, keyPrefix: `rl-${name}` }),
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(nsid: string, configOrFn: XRPCHandlerConfig | XRPCHandler) {
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(nsid: string, configOrFn: XRPCHandlerConfig | XRPCHandler) {
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: XRPCStreamHandlerConfig | XRPCStreamHandler,
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: XRPCStreamHandlerConfig | XRPCStreamHandler,
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: XRPCHandlerConfig,
180
+ config: MethodConfig<A>,
170
181
  ) {
171
- const verb: 'post' | 'get' = def.type === 'procedure' ? 'post' : 'get'
172
- const middleware: RequestHandler[] = []
173
- middleware.push(createLocalsMiddleware(nsid))
174
- if (config.auth) {
175
- middleware.push(createAuthMiddleware(config.auth))
176
- }
177
- if (verb === 'post') {
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 catchall(req: Request, res: Response, next: NextFunction) {
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(req.params.methodId)
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
- createHandler(
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
- routeCfg: XRPCHandlerConfig,
232
- ): RequestHandler {
233
- const routeOpts = {
234
- blobLimit: routeCfg.opts?.blobLimit ?? this.options.payload?.blobLimit,
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
- const validateReqInput = (req: Request) =>
237
- validateInput(nsid, def, req, routeOpts, this.lex)
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: undefined | HandlerSuccess) =>
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, routeCfg)
295
+ const routeLimiter = this.createRouteRateLimiter(nsid, cfg)
247
296
 
248
297
  return async function (req, res, next) {
249
298
  try {
250
- // validate request
251
- let params = decodeQueryParams(def, req.query)
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
- const locals: RequestLocals = req[kRequestLocals]
302
+ // authenticate request
303
+ const auth: A = authVerifier
304
+ ? await authVerifier({ req, res, params })
305
+ : (undefined as A)
260
306
 
261
- const reqCtx: XRPCReqContext = {
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: locals.auth,
313
+ auth,
265
314
  req,
266
315
  res,
267
- resetRouteRateLimits: async () => routeLimiter?.reset(reqCtx),
316
+ resetRouteRateLimits: async () => routeLimiter?.reset(ctx),
268
317
  }
269
318
 
270
319
  // handle rate limits
271
- if (routeLimiter) await routeLimiter.handle(reqCtx)
320
+ if (routeLimiter) await routeLimiter.handle(ctx)
272
321
 
273
322
  // run the handler
274
- const output = await routeCfg.handler(reqCtx)
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 (isHandlerError(output)) {
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
- config: XRPCStreamHandlerConfig,
383
+ cfg: StreamConfig<A>,
335
384
  ) {
336
- const assertValidXrpcParams = (params: unknown) =>
337
- this.lex.assertValidXrpcParams(nsid, params)
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
- let params = decodeQueryParams(def, getQueryParams(req.url))
351
- try {
352
- params = assertValidXrpcParams(params) as Params
353
- } catch (e) {
354
- throw new InvalidRequestError(String(e))
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 items = config.handler({ req, params, auth, signal })
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 url = new URL(req.url || '', 'http://x')
401
- const sub = url.pathname.startsWith('/xrpc/')
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: XRPCHandlerConfig,
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 this.globalRateLimiter
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 this.globalRateLimiter
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 (isShared(options)) {
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({ ...options, keyPrefix: `${nsid}-${i}` })
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 this.globalRateLimiter
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 (this.globalRateLimiter) rateLimiters.push(this.globalRateLimiter)
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 locals: RequestLocals | undefined = req[kRequestLocals]
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: locals?.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
- isInternalError
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: Request): req is Request & {
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