@atproto/xrpc-server 0.8.0 → 0.9.1

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 +34 -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 +5 -3
  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 +148 -108
  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 +9 -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 +176 -125
  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,10 +1,11 @@
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
+ LexXrpcBody,
8
9
  LexXrpcProcedure,
9
10
  LexXrpcQuery,
10
11
  LexXrpcSubscription,
@@ -12,15 +13,14 @@ import {
12
13
  jsonToLex,
13
14
  } from '@atproto/lexicon'
14
15
  import { ResponseType } from '@atproto/xrpc'
16
+ import { InternalServerError, InvalidRequestError, XRPCError } from './errors'
15
17
  import {
16
- HandlerInput,
18
+ Awaitable,
17
19
  HandlerSuccess,
18
- InternalServerError,
19
- InvalidRequestError,
20
+ Input,
20
21
  Params,
21
- RouteOpts,
22
+ RouteOptions,
22
23
  UndecodedParams,
23
- XRPCError,
24
24
  handlerSuccess,
25
25
  } from './types'
26
26
 
@@ -81,172 +81,217 @@ export function decodeQueryParam(
81
81
  }
82
82
  }
83
83
 
84
- export function getQueryParams(url = ''): Record<string, string | string[]> {
85
- const { searchParams } = new URL(url ?? '', 'http://x')
86
- const result: Record<string, string | string[]> = {}
84
+ export type QueryParams = Record<string, undefined | string | string[]>
85
+ export function getQueryParams(url = ''): QueryParams {
86
+ const result: QueryParams = Object.create(null)
87
+
88
+ const queryStringIdx = url.indexOf('?')
89
+ if (queryStringIdx === -1) return result
90
+
91
+ const queryString = url.slice(queryStringIdx + 1)
92
+ if (queryString === '') return result
93
+
94
+ const searchParams = new URLSearchParams(queryString)
87
95
  for (const key of searchParams.keys()) {
88
- result[key] = searchParams.getAll(key)
89
- if (result[key].length === 1) {
90
- result[key] = result[key][0]
96
+ if (key === '__proto__') {
97
+ // Prevent prototype pollution
98
+ throw new InvalidRequestError(
99
+ `Invalid query parameter: ${key}`,
100
+ 'InvalidQueryParameter',
101
+ )
91
102
  }
103
+
104
+ const values = searchParams.getAll(key)
105
+ result[key] = values.length === 1 ? values[0] : values
92
106
  }
107
+
93
108
  return result
94
109
  }
95
110
 
96
- export function validateInput(
111
+ export function createInputVerifier(
97
112
  nsid: string,
98
113
  def: LexXrpcProcedure | LexXrpcQuery,
99
- req: express.Request,
100
- opts: RouteOpts,
114
+ options: RouteOptions,
101
115
  lexicons: Lexicons,
102
- ): HandlerInput | undefined {
103
- // request expectation
116
+ ): (req: Request, res: Response) => Awaitable<Input> {
117
+ if (def.type === 'query' || !def.input) {
118
+ return (req) => {
119
+ // @NOTE We allow (and ignore) "empty" bodies
120
+ if (getBodyPresence(req) === 'present') {
121
+ throw new InvalidRequestError(
122
+ `A request body was provided when none was expected`,
123
+ )
124
+ }
104
125
 
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
- )
126
+ return undefined
127
+ }
118
128
  }
119
129
 
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) {
130
+ // Lexicon definition expects a request body
131
+
132
+ const { input } = def
133
+ const { blobLimit } = options
134
+
135
+ const allowedEncodings = parseDefEncoding(input)
136
+ const checkEncoding = allowedEncodings.includes(ENCODING_ANY)
137
+ ? undefined // No need to check
138
+ : (encoding: string) => allowedEncodings.includes(encoding)
139
+
140
+ const bodyParser = createBodyParser(input.encoding, options)
141
+
142
+ return async (req, res) => {
143
+ if (getBodyPresence(req) === 'missing') {
127
144
  throw new InvalidRequestError(
128
- `Request encoding (Content-Type) required but not provided`,
145
+ `A request body is expected but none was provided`,
129
146
  )
130
- } else {
147
+ }
148
+
149
+ const reqEncoding = parseReqEncoding(req)
150
+ if (checkEncoding && !checkEncoding(reqEncoding)) {
131
151
  throw new InvalidRequestError(
132
- `Wrong request encoding (Content-Type): ${inputEncoding}`,
152
+ `Wrong request encoding (Content-Type): ${reqEncoding}`,
133
153
  )
134
154
  }
135
- }
136
155
 
137
- if (!inputEncoding) {
138
- // no input body
139
- return undefined
140
- }
156
+ if (bodyParser) {
157
+ await bodyParser(req, res)
158
+ }
141
159
 
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))
160
+ if (input.schema) {
161
+ try {
162
+ const lexBody = req.body ? jsonToLex(req.body) : req.body
163
+ req.body = lexicons.assertValidXrpcInput(nsid, lexBody)
164
+ } catch (e) {
165
+ throw new InvalidRequestError(
166
+ e instanceof Error ? e.message : String(e),
167
+ )
168
+ }
149
169
  }
150
- }
151
170
 
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
- }
171
+ // if middleware already got the body, we pass that along as input
172
+ // otherwise, we pass along a decoded readable stream
173
+ const body = req.readableEnded ? req.body : decodeBodyStream(req, blobLimit)
160
174
 
161
- return {
162
- encoding: inputEncoding,
163
- body,
175
+ return { encoding: reqEncoding, body }
164
176
  }
165
177
  }
166
178
 
167
179
  export function validateOutput(
168
180
  nsid: string,
169
181
  def: LexXrpcProcedure | LexXrpcQuery,
170
- output: HandlerSuccess | undefined,
182
+ output: HandlerSuccess | void,
171
183
  lexicons: Lexicons,
172
184
  ): void {
173
- // initial validation
174
- if (output) {
175
- handlerSuccess.parse(output)
176
- }
185
+ if (def.output) {
186
+ // An output is expected
187
+ if (output === undefined) {
188
+ throw new InternalServerError(
189
+ `A response body is expected but none was provided`,
190
+ )
191
+ }
177
192
 
178
- // response expectation
179
- if (output?.body && !def.output) {
180
- throw new InternalServerError(
181
- `A response body was provided when none was expected`,
182
- )
183
- }
184
- if (!output?.body && def.output) {
185
- throw new InternalServerError(
186
- `A response body is expected but none was provided`,
187
- )
188
- }
193
+ // Fool-proofing (should not be necessary due to type system)
194
+ const result = handlerSuccess.safeParse(output)
195
+ if (!result.success) {
196
+ throw new InternalServerError(`Invalid handler output`, undefined, {
197
+ cause: result.error,
198
+ })
199
+ }
189
200
 
190
- // mimetype
191
- if (
192
- def.output?.encoding &&
193
- (!output?.encoding ||
194
- !isValidEncoding(def.output?.encoding, output?.encoding))
195
- ) {
196
- throw new InternalServerError(
197
- `Invalid response encoding: ${output?.encoding}`,
198
- )
199
- }
201
+ // output mime
202
+ const { encoding } = output
203
+ if (!encoding || !isValidEncoding(def.output, encoding)) {
204
+ throw new InternalServerError(`Invalid response encoding: ${encoding}`)
205
+ }
200
206
 
201
- // output schema
202
- if (def.output?.schema) {
203
- try {
204
- const result = lexicons.assertValidXrpcOutput(nsid, output?.body)
205
- if (output) {
206
- output.body = result
207
+ // output schema
208
+ if (def.output.schema) {
209
+ try {
210
+ output.body = lexicons.assertValidXrpcOutput(nsid, output.body)
211
+ } catch (e) {
212
+ throw new InternalServerError(
213
+ e instanceof Error ? e.message : String(e),
214
+ )
207
215
  }
208
- } catch (e) {
209
- throw new InternalServerError(e instanceof Error ? e.message : String(e))
216
+ }
217
+ } else {
218
+ // Expects no output
219
+ if (output !== undefined) {
220
+ throw new InternalServerError(
221
+ `A response body was provided when none was expected`,
222
+ )
210
223
  }
211
224
  }
212
225
  }
213
226
 
214
- export function normalizeMime(v: string) {
215
- if (!v) return false
216
- const fullType = mime.contentType(v)
217
- if (!fullType) return false
227
+ export function parseReqEncoding(req: IncomingMessage): string {
228
+ const encoding = normalizeMime(req.headers['content-type'])
229
+ if (encoding) return encoding
230
+ throw new InvalidRequestError(
231
+ `Request encoding (Content-Type) required but not provided`,
232
+ )
233
+ }
234
+
235
+ function normalizeMime(v?: string): string | null {
236
+ if (!v) return null
237
+ const fullType = contentType(v)
238
+ if (!fullType) return null
218
239
  const shortType = fullType.split(';')[0]
219
- if (!shortType) return false
240
+ if (!shortType) return null
220
241
  return shortType
221
242
  }
222
243
 
223
- function isValidEncoding(possibleStr: string, value: string) {
224
- const possible = possibleStr.split(',').map((v) => v.trim())
225
- const normalized = normalizeMime(value)
244
+ const ENCODING_ANY = '*/*'
245
+
246
+ function parseDefEncoding({ encoding }: LexXrpcBody) {
247
+ return encoding.split(',').map(trimString)
248
+ }
249
+
250
+ function trimString(str: string): string {
251
+ return str.trim()
252
+ }
253
+
254
+ function isValidEncoding(output: LexXrpcBody, encoding: string) {
255
+ const normalized = normalizeMime(encoding)
226
256
  if (!normalized) return false
227
- if (possible.includes('*/*')) return true
228
- return possible.includes(normalized)
257
+
258
+ const allowed = parseDefEncoding(output)
259
+ return allowed.includes(ENCODING_ANY) || allowed.includes(normalized)
229
260
  }
230
261
 
231
262
  type BodyPresence = 'missing' | 'empty' | 'present'
232
263
 
233
- function getBodyPresence(req: express.Request): BodyPresence {
264
+ function getBodyPresence(req: IncomingMessage): BodyPresence {
234
265
  if (req.headers['transfer-encoding'] != null) return 'present'
235
266
  if (req.headers['content-length'] === '0') return 'empty'
236
267
  if (req.headers['content-length'] != null) return 'present'
237
268
  return 'missing'
238
269
  }
239
270
 
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
- })
271
+ function createBodyParser(inputEncoding: string, options: RouteOptions) {
272
+ if (inputEncoding === ENCODING_ANY) {
273
+ // When the lexicon's input encoding is */*, the handler will determine how to process it
274
+ return
275
+ }
276
+ const { jsonLimit, textLimit } = options
277
+ const jsonParser = json({ limit: jsonLimit })
278
+ const textParser = text({ limit: textLimit })
279
+ // Transform json and text parser middlewares into a single function
280
+ return (req: Request, res: Response) => {
281
+ return new Promise<void>((resolve, reject) => {
282
+ jsonParser(req, res, (err) => {
283
+ if (err) return reject(XRPCError.fromError(err))
284
+ textParser(req, res, (err) => {
285
+ if (err) return reject(XRPCError.fromError(err))
286
+ resolve()
287
+ })
288
+ })
289
+ })
290
+ }
246
291
  }
247
292
 
248
293
  function decodeBodyStream(
249
- req: express.Request,
294
+ req: IncomingMessage,
250
295
  maxSize: number | undefined,
251
296
  ): Readable {
252
297
  const contentEncoding = req.headers['content-encoding']
@@ -332,13 +377,19 @@ export interface ServerTiming {
332
377
  description?: string
333
378
  }
334
379
 
335
- export const parseReqNsid = (req: express.Request | IncomingMessage) =>
380
+ export const parseReqNsid = (req: Request | IncomingMessage) =>
336
381
  parseUrlNsid('originalUrl' in req ? req.originalUrl : req.url || '/')
337
382
 
338
383
  /**
339
384
  * Validates and extracts the nsid from an xrpc path
340
385
  */
341
386
  export const parseUrlNsid = (url: string): string => {
387
+ const nsid = extractUrlNsid(url)
388
+ if (nsid) return nsid
389
+ throw new InvalidRequestError('invalid xrpc path')
390
+ }
391
+
392
+ export const extractUrlNsid = (url: string): string | undefined => {
342
393
  // /!\ Hot path
343
394
 
344
395
  if (
@@ -351,7 +402,7 @@ export const parseUrlNsid = (url: string): string => {
351
402
  url[1] !== 'x' ||
352
403
  url[0] !== '/'
353
404
  ) {
354
- throw new InvalidRequestError('invalid xrpc path')
405
+ return undefined
355
406
  }
356
407
 
357
408
  const startOfNsid = 6
@@ -369,7 +420,7 @@ export const parseUrlNsid = (url: string): string => {
369
420
  alphaNumRequired = false
370
421
  } else if (char === 45 /* "-" */ || char === 46 /* "." */) {
371
422
  if (alphaNumRequired) {
372
- throw new InvalidRequestError('invalid xrpc path')
423
+ return undefined
373
424
  }
374
425
  alphaNumRequired = true
375
426
  } else if (char === 47 /* "/" */) {
@@ -377,25 +428,25 @@ export const parseUrlNsid = (url: string): string => {
377
428
  if (curr === url.length - 1 || url.charCodeAt(curr + 1) === 63) {
378
429
  break
379
430
  }
380
- throw new InvalidRequestError('invalid xrpc path')
431
+ return undefined
381
432
  } else if (char === 63 /* "?"" */) {
382
433
  break
383
434
  } else {
384
- throw new InvalidRequestError('invalid xrpc path')
435
+ return undefined
385
436
  }
386
437
  }
387
438
 
388
439
  // last char was one of: '-', '.', '/'
389
440
  if (alphaNumRequired) {
390
- throw new InvalidRequestError('invalid xrpc path')
441
+ return undefined
391
442
  }
392
443
 
393
444
  // A domain name consists of minimum two characters
394
445
  if (curr - startOfNsid < 2) {
395
- throw new InvalidRequestError('invalid xrpc path')
446
+ return undefined
396
447
  }
397
448
 
398
- // @TODO is there a max ?
449
+ // @TODO check max length of nsid
399
450
 
400
451
  return url.slice(startOfNsid, curr)
401
452
  }
@@ -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 () => {