@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/util.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import assert from 'node:assert'
2
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,15 +12,14 @@ 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
 
@@ -81,93 +80,104 @@ export function decodeQueryParam(
81
80
  }
82
81
  }
83
82
 
84
- export function getQueryParams(url = ''): Record<string, string | string[]> {
85
- const { searchParams } = new URL(url ?? '', 'http://x')
86
- 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)
87
94
  for (const key of searchParams.keys()) {
88
- result[key] = searchParams.getAll(key)
89
- if (result[key].length === 1) {
90
- 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
+ )
91
101
  }
102
+
103
+ const values = searchParams.getAll(key)
104
+ result[key] = values.length === 1 ? values[0] : values
92
105
  }
106
+
93
107
  return result
94
108
  }
95
109
 
96
- export function validateInput(
110
+ export function createInputVerifier(
97
111
  nsid: string,
98
112
  def: LexXrpcProcedure | LexXrpcQuery,
99
- req: express.Request,
100
- opts: RouteOpts,
113
+ options: RouteOptions,
101
114
  lexicons: Lexicons,
102
- ): HandlerInput | undefined {
103
- // 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
+ }
104
124
 
105
- const bodyPresence = getBodyPresence(req)
106
- if (bodyPresence === 'present' && (def.type !== 'procedure' || !def.input)) {
107
- throw new InvalidRequestError(
108
- `A request body was provided when none was expected`,
109
- )
110
- }
111
- if (def.type === 'query') {
112
- return
113
- }
114
- if (bodyPresence === 'missing' && def.input) {
115
- throw new InvalidRequestError(
116
- `A request body is expected but none was provided`,
117
- )
125
+ return undefined
126
+ }
118
127
  }
119
128
 
120
- // mimetype
121
- const inputEncoding = normalizeMime(req.headers['content-type'] || '')
122
- if (
123
- def.input?.encoding &&
124
- (!inputEncoding || !isValidEncoding(def.input?.encoding, inputEncoding))
125
- ) {
126
- 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) {
127
145
  throw new InvalidRequestError(
128
146
  `Request encoding (Content-Type) required but not provided`,
129
147
  )
130
- } else {
148
+ } else if (!isValidEncoding(input.encoding, reqEncoding)) {
131
149
  throw new InvalidRequestError(
132
- `Wrong request encoding (Content-Type): ${inputEncoding}`,
150
+ `Wrong request encoding (Content-Type): ${reqEncoding}`,
133
151
  )
134
152
  }
135
- }
136
153
 
137
- if (!inputEncoding) {
138
- // no input body
139
- return undefined
140
- }
154
+ if (bodyParser) {
155
+ await bodyParser(req, res)
156
+ }
141
157
 
142
- // if input schema, validate
143
- if (def.input?.schema) {
144
- try {
145
- const lexBody = req.body ? jsonToLex(req.body) : req.body
146
- req.body = lexicons.assertValidXrpcInput(nsid, lexBody)
147
- } catch (e) {
148
- 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
+ }
149
167
  }
150
- }
151
168
 
152
- // if middleware already got the body, we pass that along as input
153
- // otherwise, we pass along a decoded readable stream
154
- let body
155
- if (req.readableEnded) {
156
- body = req.body
157
- } else {
158
- body = decodeBodyStream(req, opts.blobLimit)
159
- }
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)
160
172
 
161
- return {
162
- encoding: inputEncoding,
163
- body,
173
+ return { encoding: reqEncoding, body }
164
174
  }
165
175
  }
166
176
 
167
177
  export function validateOutput(
168
178
  nsid: string,
169
179
  def: LexXrpcProcedure | LexXrpcQuery,
170
- output: HandlerSuccess | undefined,
180
+ output: HandlerSuccess | void,
171
181
  lexicons: Lexicons,
172
182
  ): void {
173
183
  // initial validation
@@ -211,42 +221,58 @@ export function validateOutput(
211
221
  }
212
222
  }
213
223
 
214
- export function normalizeMime(v: string) {
224
+ export function normalizeMime(v?: string): string | false {
215
225
  if (!v) return false
216
- const fullType = mime.contentType(v)
226
+ const fullType = contentType(v)
217
227
  if (!fullType) return false
218
228
  const shortType = fullType.split(';')[0]
219
229
  if (!shortType) return false
220
230
  return shortType
221
231
  }
222
232
 
233
+ const ENCODING_ANY = '*/*'
234
+
223
235
  function isValidEncoding(possibleStr: string, value: string) {
224
236
  const possible = possibleStr.split(',').map((v) => v.trim())
225
237
  const normalized = normalizeMime(value)
226
238
  if (!normalized) return false
227
- if (possible.includes('*/*')) return true
239
+ if (possible.includes(ENCODING_ANY)) return true
228
240
  return possible.includes(normalized)
229
241
  }
230
242
 
231
243
  type BodyPresence = 'missing' | 'empty' | 'present'
232
244
 
233
- function getBodyPresence(req: express.Request): BodyPresence {
245
+ function getBodyPresence(req: IncomingMessage): BodyPresence {
234
246
  if (req.headers['transfer-encoding'] != null) return 'present'
235
247
  if (req.headers['content-length'] === '0') return 'empty'
236
248
  if (req.headers['content-length'] != null) return 'present'
237
249
  return 'missing'
238
250
  }
239
251
 
240
- export function processBodyAsBytes(req: express.Request): Promise<Uint8Array> {
241
- return new Promise((resolve) => {
242
- const chunks: Buffer[] = []
243
- req.on('data', (chunk) => chunks.push(chunk))
244
- req.on('end', () => resolve(new Uint8Array(Buffer.concat(chunks))))
245
- })
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
+ }
246
272
  }
247
273
 
248
274
  function decodeBodyStream(
249
- req: express.Request,
275
+ req: IncomingMessage,
250
276
  maxSize: number | undefined,
251
277
  ): Readable {
252
278
  const contentEncoding = req.headers['content-encoding']
@@ -332,13 +358,19 @@ export interface ServerTiming {
332
358
  description?: string
333
359
  }
334
360
 
335
- export const parseReqNsid = (req: express.Request | IncomingMessage) =>
361
+ export const parseReqNsid = (req: Request | IncomingMessage) =>
336
362
  parseUrlNsid('originalUrl' in req ? req.originalUrl : req.url || '/')
337
363
 
338
364
  /**
339
365
  * Validates and extracts the nsid from an xrpc path
340
366
  */
341
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 => {
342
374
  // /!\ Hot path
343
375
 
344
376
  if (
@@ -351,7 +383,7 @@ export const parseUrlNsid = (url: string): string => {
351
383
  url[1] !== 'x' ||
352
384
  url[0] !== '/'
353
385
  ) {
354
- throw new InvalidRequestError('invalid xrpc path')
386
+ return undefined
355
387
  }
356
388
 
357
389
  const startOfNsid = 6
@@ -369,7 +401,7 @@ export const parseUrlNsid = (url: string): string => {
369
401
  alphaNumRequired = false
370
402
  } else if (char === 45 /* "-" */ || char === 46 /* "." */) {
371
403
  if (alphaNumRequired) {
372
- throw new InvalidRequestError('invalid xrpc path')
404
+ return undefined
373
405
  }
374
406
  alphaNumRequired = true
375
407
  } else if (char === 47 /* "/" */) {
@@ -377,25 +409,25 @@ export const parseUrlNsid = (url: string): string => {
377
409
  if (curr === url.length - 1 || url.charCodeAt(curr + 1) === 63) {
378
410
  break
379
411
  }
380
- throw new InvalidRequestError('invalid xrpc path')
412
+ return undefined
381
413
  } else if (char === 63 /* "?"" */) {
382
414
  break
383
415
  } else {
384
- throw new InvalidRequestError('invalid xrpc path')
416
+ return undefined
385
417
  }
386
418
  }
387
419
 
388
420
  // last char was one of: '-', '.', '/'
389
421
  if (alphaNumRequired) {
390
- throw new InvalidRequestError('invalid xrpc path')
422
+ return undefined
391
423
  }
392
424
 
393
425
  // A domain name consists of minimum two characters
394
426
  if (curr - startOfNsid < 2) {
395
- throw new InvalidRequestError('invalid xrpc path')
427
+ return undefined
396
428
  }
397
429
 
398
- // @TODO is there a max ?
430
+ // @TODO check max length of nsid
399
431
 
400
432
  return url.slice(startOfNsid, curr)
401
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,7 +132,7 @@ describe('Parameters', () => {
132
132
  let s: http.Server
133
133
  const server = xrpcServer.createServer(LEXICONS, {
134
134
  rateLimits: {
135
- creator: (opts) => RateLimiter.memory(opts),
135
+ creator: (opts) => new MemoryRateLimiter(opts),
136
136
  bypass: ({ req }) => req.headers['x-ratelimit-bypass'] === 'bypass',
137
137
  shared: [
138
138
  {
@@ -156,7 +156,7 @@ describe('Parameters', () => {
156
156
  points: 5,
157
157
  calcKey: ({ params }) => params.str as string,
158
158
  },
159
- handler: (ctx: { params: xrpcServer.Params }) => ({
159
+ handler: (ctx) => ({
160
160
  encoding: 'json',
161
161
  body: ctx.params,
162
162
  }),
@@ -166,7 +166,7 @@ describe('Parameters', () => {
166
166
  durationMs: 5 * MINUTE,
167
167
  points: 2,
168
168
  },
169
- handler: (ctx: xrpcServer.XRPCReqContext) => {
169
+ handler: (ctx) => {
170
170
  if (ctx.params.count === 1) {
171
171
  ctx.resetRouteRateLimits()
172
172
  }
@@ -182,7 +182,7 @@ describe('Parameters', () => {
182
182
  name: 'shared-limit',
183
183
  calcPoints: ({ params }) => params.points as number,
184
184
  },
185
- handler: (ctx: { params: xrpcServer.Params }) => ({
185
+ handler: (ctx) => ({
186
186
  encoding: 'json',
187
187
  body: ctx.params,
188
188
  }),
@@ -192,7 +192,7 @@ describe('Parameters', () => {
192
192
  name: 'shared-limit',
193
193
  calcPoints: ({ params }) => params.points as number,
194
194
  },
195
- handler: (ctx: { params: xrpcServer.Params }) => ({
195
+ handler: (ctx) => ({
196
196
  encoding: 'json',
197
197
  body: ctx.params,
198
198
  }),
@@ -209,7 +209,7 @@ describe('Parameters', () => {
209
209
  points: 10,
210
210
  },
211
211
  ],
212
- handler: (ctx: { params: xrpcServer.Params }) => ({
212
+ handler: (ctx) => ({
213
213
  encoding: 'json',
214
214
  body: ctx.params,
215
215
  }),