@atproto/xrpc-server 0.7.19 → 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 +42 -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 +95 -26
  13. package/dist/rate-limiter.d.ts.map +1 -1
  14. package/dist/rate-limiter.js +179 -85
  15. package/dist/rate-limiter.js.map +1 -1
  16. package/dist/server.d.ts +20 -15
  17. package/dist/server.d.ts.map +1 -1
  18. package/dist/server.js +185 -220
  19. package/dist/server.js.map +1 -1
  20. package/dist/types.d.ts +80 -175
  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 +12 -9
  25. package/dist/util.d.ts.map +1 -1
  26. package/dist/util.js +114 -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 +270 -104
  33. package/src/server.ts +265 -276
  34. package/src/types.ts +144 -429
  35. package/src/util.ts +131 -85
  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 +8 -11
  44. package/tests/responses.test.ts +12 -15
  45. package/tsconfig.build.tsbuildinfo +1 -1
package/src/util.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import assert from 'node:assert'
2
- import { IncomingMessage } from 'node:http'
2
+ import { IncomingMessage, OutgoingMessage } from 'node:http'
3
3
  import { Duplex, Readable, pipeline } from 'node:stream'
4
- import express from 'express'
5
- import mime from 'mime-types'
4
+ import { Request, Response, json, text } from 'express'
5
+ import { contentType } from 'mime-types'
6
6
  import { MaxSizeChecker, createDecoders } from '@atproto/common'
7
7
  import {
8
8
  LexXrpcProcedure,
@@ -12,18 +12,31 @@ import {
12
12
  jsonToLex,
13
13
  } from '@atproto/lexicon'
14
14
  import { ResponseType } from '@atproto/xrpc'
15
+ import { InternalServerError, InvalidRequestError, XRPCError } from './errors'
15
16
  import {
16
- HandlerInput,
17
+ Awaitable,
17
18
  HandlerSuccess,
18
- InternalServerError,
19
- InvalidRequestError,
19
+ Input,
20
20
  Params,
21
- RouteOpts,
21
+ RouteOptions,
22
22
  UndecodedParams,
23
- XRPCError,
24
23
  handlerSuccess,
25
24
  } from './types'
26
25
 
26
+ export const asArray = <T>(arr: T | T[]): T[] =>
27
+ Array.isArray(arr) ? arr : [arr]
28
+
29
+ export function setHeaders(
30
+ res: OutgoingMessage,
31
+ headers?: Record<string, string | number>,
32
+ ) {
33
+ if (headers) {
34
+ for (const [name, val] of Object.entries(headers)) {
35
+ if (val != null) res.setHeader(name, val)
36
+ }
37
+ }
38
+ }
39
+
27
40
  export function decodeQueryParams(
28
41
  def: LexXrpcProcedure | LexXrpcQuery | LexXrpcSubscription,
29
42
  params: UndecodedParams,
@@ -67,93 +80,104 @@ export function decodeQueryParam(
67
80
  }
68
81
  }
69
82
 
70
- export function getQueryParams(url = ''): Record<string, string | string[]> {
71
- const { searchParams } = new URL(url ?? '', 'http://x')
72
- const result: Record<string, string | string[]> = {}
83
+ export type QueryParams = Record<string, undefined | string | string[]>
84
+ export function getQueryParams(url = ''): QueryParams {
85
+ const result: QueryParams = Object.create(null)
86
+
87
+ const queryStringIdx = url.indexOf('?')
88
+ if (queryStringIdx === -1) return result
89
+
90
+ const queryString = url.slice(queryStringIdx + 1)
91
+ if (queryString === '') return result
92
+
93
+ const searchParams = new URLSearchParams(queryString)
73
94
  for (const key of searchParams.keys()) {
74
- result[key] = searchParams.getAll(key)
75
- if (result[key].length === 1) {
76
- result[key] = result[key][0]
95
+ if (key === '__proto__') {
96
+ // Prevent prototype pollution
97
+ throw new InvalidRequestError(
98
+ `Invalid query parameter: ${key}`,
99
+ 'InvalidQueryParameter',
100
+ )
77
101
  }
102
+
103
+ const values = searchParams.getAll(key)
104
+ result[key] = values.length === 1 ? values[0] : values
78
105
  }
106
+
79
107
  return result
80
108
  }
81
109
 
82
- export function validateInput(
110
+ export function createInputVerifier(
83
111
  nsid: string,
84
112
  def: LexXrpcProcedure | LexXrpcQuery,
85
- req: express.Request,
86
- opts: RouteOpts,
113
+ options: RouteOptions,
87
114
  lexicons: Lexicons,
88
- ): HandlerInput | undefined {
89
- // request expectation
115
+ ): (req: Request, res: Response) => Awaitable<Input> {
116
+ if (def.type === 'query' || !def.input) {
117
+ return (req) => {
118
+ // @NOTE We allow (and ignore) "empty" bodies
119
+ if (getBodyPresence(req) === 'present') {
120
+ throw new InvalidRequestError(
121
+ `A request body was provided when none was expected`,
122
+ )
123
+ }
90
124
 
91
- const bodyPresence = getBodyPresence(req)
92
- if (bodyPresence === 'present' && (def.type !== 'procedure' || !def.input)) {
93
- throw new InvalidRequestError(
94
- `A request body was provided when none was expected`,
95
- )
96
- }
97
- if (def.type === 'query') {
98
- return
99
- }
100
- if (bodyPresence === 'missing' && def.input) {
101
- throw new InvalidRequestError(
102
- `A request body is expected but none was provided`,
103
- )
125
+ return undefined
126
+ }
104
127
  }
105
128
 
106
- // mimetype
107
- const inputEncoding = normalizeMime(req.headers['content-type'] || '')
108
- if (
109
- def.input?.encoding &&
110
- (!inputEncoding || !isValidEncoding(def.input?.encoding, inputEncoding))
111
- ) {
112
- if (!inputEncoding) {
129
+ // Lexicon definition expects a request body
130
+
131
+ const { input } = def
132
+ const { blobLimit } = options
133
+
134
+ const bodyParser = createBodyParser(input.encoding, options)
135
+
136
+ return async (req, res) => {
137
+ if (getBodyPresence(req) === 'missing') {
138
+ throw new InvalidRequestError(
139
+ `A request body is expected but none was provided`,
140
+ )
141
+ }
142
+
143
+ const reqEncoding = normalizeMime(req.headers['content-type'])
144
+ if (!reqEncoding) {
113
145
  throw new InvalidRequestError(
114
146
  `Request encoding (Content-Type) required but not provided`,
115
147
  )
116
- } else {
148
+ } else if (!isValidEncoding(input.encoding, reqEncoding)) {
117
149
  throw new InvalidRequestError(
118
- `Wrong request encoding (Content-Type): ${inputEncoding}`,
150
+ `Wrong request encoding (Content-Type): ${reqEncoding}`,
119
151
  )
120
152
  }
121
- }
122
153
 
123
- if (!inputEncoding) {
124
- // no input body
125
- return undefined
126
- }
154
+ if (bodyParser) {
155
+ await bodyParser(req, res)
156
+ }
127
157
 
128
- // if input schema, validate
129
- if (def.input?.schema) {
130
- try {
131
- const lexBody = req.body ? jsonToLex(req.body) : req.body
132
- req.body = lexicons.assertValidXrpcInput(nsid, lexBody)
133
- } catch (e) {
134
- throw new InvalidRequestError(e instanceof Error ? e.message : String(e))
158
+ if (input.schema) {
159
+ try {
160
+ const lexBody = req.body ? jsonToLex(req.body) : req.body
161
+ req.body = lexicons.assertValidXrpcInput(nsid, lexBody)
162
+ } catch (e) {
163
+ throw new InvalidRequestError(
164
+ e instanceof Error ? e.message : String(e),
165
+ )
166
+ }
135
167
  }
136
- }
137
168
 
138
- // if middleware already got the body, we pass that along as input
139
- // otherwise, we pass along a decoded readable stream
140
- let body
141
- if (req.readableEnded) {
142
- body = req.body
143
- } else {
144
- body = decodeBodyStream(req, opts.blobLimit)
145
- }
169
+ // if middleware already got the body, we pass that along as input
170
+ // otherwise, we pass along a decoded readable stream
171
+ const body = req.readableEnded ? req.body : decodeBodyStream(req, blobLimit)
146
172
 
147
- return {
148
- encoding: inputEncoding,
149
- body,
173
+ return { encoding: reqEncoding, body }
150
174
  }
151
175
  }
152
176
 
153
177
  export function validateOutput(
154
178
  nsid: string,
155
179
  def: LexXrpcProcedure | LexXrpcQuery,
156
- output: HandlerSuccess | undefined,
180
+ output: HandlerSuccess | void,
157
181
  lexicons: Lexicons,
158
182
  ): void {
159
183
  // initial validation
@@ -197,42 +221,58 @@ export function validateOutput(
197
221
  }
198
222
  }
199
223
 
200
- export function normalizeMime(v: string) {
224
+ export function normalizeMime(v?: string): string | false {
201
225
  if (!v) return false
202
- const fullType = mime.contentType(v)
226
+ const fullType = contentType(v)
203
227
  if (!fullType) return false
204
228
  const shortType = fullType.split(';')[0]
205
229
  if (!shortType) return false
206
230
  return shortType
207
231
  }
208
232
 
233
+ const ENCODING_ANY = '*/*'
234
+
209
235
  function isValidEncoding(possibleStr: string, value: string) {
210
236
  const possible = possibleStr.split(',').map((v) => v.trim())
211
237
  const normalized = normalizeMime(value)
212
238
  if (!normalized) return false
213
- if (possible.includes('*/*')) return true
239
+ if (possible.includes(ENCODING_ANY)) return true
214
240
  return possible.includes(normalized)
215
241
  }
216
242
 
217
243
  type BodyPresence = 'missing' | 'empty' | 'present'
218
244
 
219
- function getBodyPresence(req: express.Request): BodyPresence {
245
+ function getBodyPresence(req: IncomingMessage): BodyPresence {
220
246
  if (req.headers['transfer-encoding'] != null) return 'present'
221
247
  if (req.headers['content-length'] === '0') return 'empty'
222
248
  if (req.headers['content-length'] != null) return 'present'
223
249
  return 'missing'
224
250
  }
225
251
 
226
- export function processBodyAsBytes(req: express.Request): Promise<Uint8Array> {
227
- return new Promise((resolve) => {
228
- const chunks: Buffer[] = []
229
- req.on('data', (chunk) => chunks.push(chunk))
230
- req.on('end', () => resolve(new Uint8Array(Buffer.concat(chunks))))
231
- })
252
+ function createBodyParser(inputEncoding: string, options: RouteOptions) {
253
+ if (inputEncoding === ENCODING_ANY) {
254
+ // When the lexicon's input encoding is */*, the handler will determine how to process it
255
+ return
256
+ }
257
+ const { jsonLimit, textLimit } = options
258
+ const jsonParser = json({ limit: jsonLimit })
259
+ const textParser = text({ limit: textLimit })
260
+ // Transform json and text parser middlewares into a single function
261
+ return (req: Request, res: Response) => {
262
+ return new Promise<void>((resolve, reject) => {
263
+ jsonParser(req, res, (err) => {
264
+ if (err) return reject(XRPCError.fromError(err))
265
+ textParser(req, res, (err) => {
266
+ if (err) return reject(XRPCError.fromError(err))
267
+ resolve()
268
+ })
269
+ })
270
+ })
271
+ }
232
272
  }
233
273
 
234
274
  function decodeBodyStream(
235
- req: express.Request,
275
+ req: IncomingMessage,
236
276
  maxSize: number | undefined,
237
277
  ): Readable {
238
278
  const contentEncoding = req.headers['content-encoding']
@@ -318,13 +358,19 @@ export interface ServerTiming {
318
358
  description?: string
319
359
  }
320
360
 
321
- export const parseReqNsid = (req: express.Request | IncomingMessage) =>
361
+ export const parseReqNsid = (req: Request | IncomingMessage) =>
322
362
  parseUrlNsid('originalUrl' in req ? req.originalUrl : req.url || '/')
323
363
 
324
364
  /**
325
365
  * Validates and extracts the nsid from an xrpc path
326
366
  */
327
367
  export const parseUrlNsid = (url: string): string => {
368
+ const nsid = extractUrlNsid(url)
369
+ if (nsid) return nsid
370
+ throw new InvalidRequestError('invalid xrpc path')
371
+ }
372
+
373
+ export const extractUrlNsid = (url: string): string | undefined => {
328
374
  // /!\ Hot path
329
375
 
330
376
  if (
@@ -337,7 +383,7 @@ export const parseUrlNsid = (url: string): string => {
337
383
  url[1] !== 'x' ||
338
384
  url[0] !== '/'
339
385
  ) {
340
- throw new InvalidRequestError('invalid xrpc path')
386
+ return undefined
341
387
  }
342
388
 
343
389
  const startOfNsid = 6
@@ -355,7 +401,7 @@ export const parseUrlNsid = (url: string): string => {
355
401
  alphaNumRequired = false
356
402
  } else if (char === 45 /* "-" */ || char === 46 /* "." */) {
357
403
  if (alphaNumRequired) {
358
- throw new InvalidRequestError('invalid xrpc path')
404
+ return undefined
359
405
  }
360
406
  alphaNumRequired = true
361
407
  } else if (char === 47 /* "/" */) {
@@ -363,25 +409,25 @@ export const parseUrlNsid = (url: string): string => {
363
409
  if (curr === url.length - 1 || url.charCodeAt(curr + 1) === 63) {
364
410
  break
365
411
  }
366
- throw new InvalidRequestError('invalid xrpc path')
412
+ return undefined
367
413
  } else if (char === 63 /* "?"" */) {
368
414
  break
369
415
  } else {
370
- throw new InvalidRequestError('invalid xrpc path')
416
+ return undefined
371
417
  }
372
418
  }
373
419
 
374
420
  // last char was one of: '-', '.', '/'
375
421
  if (alphaNumRequired) {
376
- throw new InvalidRequestError('invalid xrpc path')
422
+ return undefined
377
423
  }
378
424
 
379
425
  // A domain name consists of minimum two characters
380
426
  if (curr - startOfNsid < 2) {
381
- throw new InvalidRequestError('invalid xrpc path')
427
+ return undefined
382
428
  }
383
429
 
384
- // @TODO is there a max ?
430
+ // @TODO check max length of nsid
385
431
 
386
432
  return url.slice(startOfNsid, curr)
387
433
  }
@@ -56,8 +56,8 @@ describe('Auth', () => {
56
56
  return {
57
57
  encoding: 'application/json',
58
58
  body: {
59
- username: auth?.credentials?.username,
60
- original: auth?.artifacts?.original,
59
+ username: auth.credentials.username,
60
+ original: auth.artifacts.original,
61
61
  },
62
62
  }
63
63
  },
@@ -1,3 +1,4 @@
1
+ import assert from 'node:assert'
1
2
  import * as http from 'node:http'
2
3
  import { AddressInfo } from 'node:net'
3
4
  import { Readable } from 'node:stream'
@@ -121,34 +122,27 @@ describe('Bodies', () => {
121
122
  blobLimit: BLOB_LIMIT,
122
123
  },
123
124
  })
124
- server.method(
125
- 'io.example.validationTest',
126
- (ctx: { params: xrpcServer.Params; input?: xrpcServer.HandlerInput }) => {
127
- if (ctx.input?.body instanceof Readable) {
128
- throw new Error('Input is readable')
129
- }
125
+ server.method('io.example.validationTest', (ctx) => {
126
+ assert(!(ctx.input?.body instanceof Readable), 'Input is readable')
130
127
 
131
- return {
132
- encoding: 'json',
133
- body: ctx.input?.body ?? null,
134
- }
135
- },
136
- )
128
+ return {
129
+ encoding: 'json',
130
+ body: ctx.input?.body ?? null,
131
+ }
132
+ })
137
133
  server.method('io.example.validationTestTwo', () => ({
138
134
  encoding: 'json',
139
135
  body: { wrong: 'data' },
140
136
  }))
141
- server.method(
142
- 'io.example.blobTest',
143
- async (ctx: { input?: xrpcServer.HandlerInput }) => {
144
- const buffer = await consumeInput(ctx.input?.body)
145
- const cid = await cidForCbor(buffer)
146
- return {
147
- encoding: 'json',
148
- body: { cid: cid.toString() },
149
- }
150
- },
151
- )
137
+ server.method('io.example.blobTest', async (ctx) => {
138
+ assert(ctx.input?.body != null, 'Input body is required')
139
+ const buffer = await consumeInput(ctx.input.body)
140
+ const cid = await cidForCbor(buffer)
141
+ return {
142
+ encoding: 'json',
143
+ body: { cid: cid.toString() },
144
+ }
145
+ })
152
146
 
153
147
  let client: XrpcClient
154
148
  let url: string
@@ -295,10 +289,7 @@ describe('Bodies', () => {
295
289
  expect(fileResponse.data.cid).toEqual(expectedCid.toString())
296
290
  })
297
291
 
298
- // This does not work because the xrpc-server will add a json middleware
299
- // regardless of the "input" definition. This is probably a behavior that
300
- // should be fixed in the xrpc-server.
301
- it.skip('supports upload of json data', async () => {
292
+ it('supports upload of json data', async () => {
302
293
  const jsonFile = new Blob([Buffer.from(`{"foo":"bar","baz":[3, null]}`)], {
303
294
  type: 'application/json',
304
295
  })
@@ -144,7 +144,7 @@ describe('Errors', () => {
144
144
 
145
145
  let s: http.Server
146
146
  const server = xrpcServer.createServer(LEXICONS, { validateResponse: false }) // disable validateResponse to test client validation
147
- server.method('io.example.error', (ctx: { params: xrpcServer.Params }) => {
147
+ server.method('io.example.error', (ctx) => {
148
148
  if (ctx.params.which === 'foo') {
149
149
  throw new xrpcServer.InvalidRequestError('It was this one!', 'Foo')
150
150
  } else if (ctx.params.which === 'bar') {
@@ -1,3 +1,4 @@
1
+ import assert from 'node:assert'
1
2
  import * as http from 'node:http'
2
3
  import { AddressInfo } from 'node:net'
3
4
  import { CID } from 'multiformats/cid'
@@ -49,20 +50,20 @@ const LEXICONS: LexiconDoc[] = [
49
50
  describe('Ipld vals', () => {
50
51
  let s: http.Server
51
52
  const server = xrpcServer.createServer(LEXICONS)
52
- server.method(
53
- 'io.example.ipld',
54
- (ctx: { input?: xrpcServer.HandlerInput }) => {
55
- const asCid = CID.asCID(ctx.input?.body.cid)
56
- if (!(asCid instanceof CID)) {
57
- throw new Error('expected cid')
58
- }
59
- const bytes = ctx.input?.body.bytes
60
- if (!(bytes instanceof Uint8Array)) {
61
- throw new Error('expected bytes')
62
- }
63
- return { encoding: 'application/json', body: ctx.input?.body }
64
- },
65
- )
53
+ server.method('io.example.ipld', (ctx) => {
54
+ assert(ctx.input?.body, 'expected input body')
55
+ assert(typeof ctx.input.body === 'object', 'expected input body')
56
+
57
+ const asCid = CID.asCID(ctx.input.body['cid'])
58
+ if (!(asCid instanceof CID)) {
59
+ throw new Error('expected cid')
60
+ }
61
+ const bytes = ctx.input.body['bytes']
62
+ if (!(bytes instanceof Uint8Array)) {
63
+ throw new Error('expected bytes')
64
+ }
65
+ return { encoding: 'application/json', body: ctx.input.body }
66
+ })
66
67
 
67
68
  let client: XrpcClient
68
69
  beforeAll(async () => {
@@ -34,13 +34,10 @@ const LEXICONS: LexiconDoc[] = [
34
34
  describe('Parameters', () => {
35
35
  let s: http.Server
36
36
  const server = xrpcServer.createServer(LEXICONS)
37
- server.method(
38
- 'io.example.paramTest',
39
- (ctx: { params: xrpcServer.Params }) => ({
40
- encoding: 'json',
41
- body: ctx.params,
42
- }),
43
- )
37
+ server.method('io.example.paramTest', (ctx) => ({
38
+ encoding: 'json',
39
+ body: ctx.params,
40
+ }))
44
41
 
45
42
  let client: XrpcClient
46
43
  beforeAll(async () => {
@@ -1,3 +1,4 @@
1
+ import assert from 'node:assert'
1
2
  import * as http from 'node:http'
2
3
  import { AddressInfo } from 'node:net'
3
4
  import { Readable } from 'node:stream'
@@ -85,42 +86,29 @@ const LEXICONS: LexiconDoc[] = [
85
86
  describe('Procedures', () => {
86
87
  let s: http.Server
87
88
  const server = xrpcServer.createServer(LEXICONS)
88
- server.method('io.example.pingOne', (ctx: { params: xrpcServer.Params }) => {
89
+ server.method('io.example.pingOne', (ctx) => {
89
90
  return { encoding: 'text/plain', body: ctx.params.message }
90
91
  })
91
- server.method(
92
- 'io.example.pingTwo',
93
- (ctx: { params: xrpcServer.Params; input?: xrpcServer.HandlerInput }) => {
94
- return { encoding: 'text/plain', body: ctx.input?.body }
95
- },
96
- )
97
- server.method(
98
- 'io.example.pingThree',
99
- async (ctx: {
100
- params: xrpcServer.Params
101
- input?: xrpcServer.HandlerInput
102
- }) => {
103
- if (!(ctx.input?.body instanceof Readable))
104
- throw new Error('Input not readable')
105
- const buffers: Buffer[] = []
106
- for await (const data of ctx.input.body) {
107
- buffers.push(data)
108
- }
109
- return {
110
- encoding: 'application/octet-stream',
111
- body: Buffer.concat(buffers),
112
- }
113
- },
114
- )
115
- server.method(
116
- 'io.example.pingFour',
117
- (ctx: { params: xrpcServer.Params; input?: xrpcServer.HandlerInput }) => {
118
- return {
119
- encoding: 'application/json',
120
- body: { message: ctx.input?.body?.message },
121
- }
122
- },
123
- )
92
+ server.method('io.example.pingTwo', (ctx) => {
93
+ return { encoding: 'text/plain', body: ctx.input?.body }
94
+ })
95
+ server.method('io.example.pingThree', async (ctx) => {
96
+ assert(ctx.input?.body instanceof Readable, 'Input not readable')
97
+ const buffers: Buffer[] = []
98
+ for await (const data of ctx.input.body) {
99
+ buffers.push(data)
100
+ }
101
+ return {
102
+ encoding: 'application/octet-stream',
103
+ body: Buffer.concat(buffers),
104
+ }
105
+ })
106
+ server.method('io.example.pingFour', (ctx) => {
107
+ return {
108
+ encoding: 'application/json',
109
+ body: { message: ctx.input?.body?.['message'] },
110
+ }
111
+ })
124
112
 
125
113
  let client: XrpcClient
126
114
  beforeAll(async () => {
@@ -70,25 +70,22 @@ const LEXICONS: LexiconDoc[] = [
70
70
  describe('Queries', () => {
71
71
  let s: http.Server
72
72
  const server = xrpcServer.createServer(LEXICONS)
73
- server.method('io.example.pingOne', (ctx: { params: xrpcServer.Params }) => {
73
+ server.method('io.example.pingOne', (ctx) => {
74
74
  return { encoding: 'text/plain', body: ctx.params.message }
75
75
  })
76
- server.method('io.example.pingTwo', (ctx: { params: xrpcServer.Params }) => {
76
+ server.method('io.example.pingTwo', (ctx) => {
77
77
  return {
78
78
  encoding: 'application/octet-stream',
79
79
  body: new TextEncoder().encode(String(ctx.params.message)),
80
80
  }
81
81
  })
82
- server.method(
83
- 'io.example.pingThree',
84
- (ctx: { params: xrpcServer.Params }) => {
85
- return {
86
- encoding: 'application/json',
87
- body: { message: ctx.params.message },
88
- headers: { 'x-test-header-name': 'test-value' },
89
- }
90
- },
91
- )
82
+ server.method('io.example.pingThree', (ctx) => {
83
+ return {
84
+ encoding: 'application/json',
85
+ body: { message: ctx.params.message },
86
+ headers: { 'x-test-header-name': 'test-value' },
87
+ }
88
+ })
92
89
 
93
90
  let client: XrpcClient
94
91
  beforeAll(async () => {
@@ -4,7 +4,7 @@ import { MINUTE } from '@atproto/common'
4
4
  import { LexiconDoc } from '@atproto/lexicon'
5
5
  import { XrpcClient } from '@atproto/xrpc'
6
6
  import * as xrpcServer from '../src'
7
- import { RateLimiter } from '../src'
7
+ import { MemoryRateLimiter } from '../src'
8
8
  import { closeServer, createServer } from './_util'
9
9
 
10
10
  const LEXICONS: LexiconDoc[] = [
@@ -132,11 +132,8 @@ describe('Parameters', () => {
132
132
  let s: http.Server
133
133
  const server = xrpcServer.createServer(LEXICONS, {
134
134
  rateLimits: {
135
- creator: (opts: xrpcServer.RateLimiterOpts) =>
136
- RateLimiter.memory({
137
- bypassSecret: 'bypass',
138
- ...opts,
139
- }),
135
+ creator: (opts) => new MemoryRateLimiter(opts),
136
+ bypass: ({ req }) => req.headers['x-ratelimit-bypass'] === 'bypass',
140
137
  shared: [
141
138
  {
142
139
  name: 'shared-limit',
@@ -159,7 +156,7 @@ describe('Parameters', () => {
159
156
  points: 5,
160
157
  calcKey: ({ params }) => params.str as string,
161
158
  },
162
- handler: (ctx: { params: xrpcServer.Params }) => ({
159
+ handler: (ctx) => ({
163
160
  encoding: 'json',
164
161
  body: ctx.params,
165
162
  }),
@@ -169,7 +166,7 @@ describe('Parameters', () => {
169
166
  durationMs: 5 * MINUTE,
170
167
  points: 2,
171
168
  },
172
- handler: (ctx: xrpcServer.XRPCReqContext) => {
169
+ handler: (ctx) => {
173
170
  if (ctx.params.count === 1) {
174
171
  ctx.resetRouteRateLimits()
175
172
  }
@@ -185,7 +182,7 @@ describe('Parameters', () => {
185
182
  name: 'shared-limit',
186
183
  calcPoints: ({ params }) => params.points as number,
187
184
  },
188
- handler: (ctx: { params: xrpcServer.Params }) => ({
185
+ handler: (ctx) => ({
189
186
  encoding: 'json',
190
187
  body: ctx.params,
191
188
  }),
@@ -195,7 +192,7 @@ describe('Parameters', () => {
195
192
  name: 'shared-limit',
196
193
  calcPoints: ({ params }) => params.points as number,
197
194
  },
198
- handler: (ctx: { params: xrpcServer.Params }) => ({
195
+ handler: (ctx) => ({
199
196
  encoding: 'json',
200
197
  body: ctx.params,
201
198
  }),
@@ -212,7 +209,7 @@ describe('Parameters', () => {
212
209
  points: 10,
213
210
  },
214
211
  ],
215
- handler: (ctx: { params: xrpcServer.Params }) => ({
212
+ handler: (ctx) => ({
216
213
  encoding: 'json',
217
214
  body: ctx.params,
218
215
  }),