@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.
- package/CHANGELOG.md +34 -0
- package/dist/auth.js +11 -11
- package/dist/auth.js.map +1 -1
- package/dist/errors.d.ts +67 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +202 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/rate-limiter.d.ts +69 -32
- package/dist/rate-limiter.d.ts.map +1 -1
- package/dist/rate-limiter.js +58 -41
- package/dist/rate-limiter.js.map +1 -1
- package/dist/server.d.ts +19 -14
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +151 -137
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +80 -178
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -226
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +9 -8
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +148 -108
- package/dist/util.js.map +1 -1
- package/package.json +4 -3
- package/src/auth.ts +1 -1
- package/src/errors.ts +293 -0
- package/src/index.ts +9 -3
- package/src/rate-limiter.ts +188 -96
- package/src/server.ts +198 -154
- package/src/types.ts +144 -439
- package/src/util.ts +176 -125
- package/tests/auth.test.ts +2 -2
- package/tests/bodies.test.ts +18 -27
- package/tests/errors.test.ts +1 -1
- package/tests/ipld.test.ts +15 -14
- package/tests/parameters.test.ts +4 -7
- package/tests/procedures.test.ts +22 -34
- package/tests/queries.test.ts +9 -12
- package/tests/rate-limiter.test.ts +7 -7
- package/tests/responses.test.ts +12 -15
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/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
|
|
5
|
-
import
|
|
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
|
-
|
|
18
|
+
Awaitable,
|
|
17
19
|
HandlerSuccess,
|
|
18
|
-
|
|
19
|
-
InvalidRequestError,
|
|
20
|
+
Input,
|
|
20
21
|
Params,
|
|
21
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
const result:
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
111
|
+
export function createInputVerifier(
|
|
97
112
|
nsid: string,
|
|
98
113
|
def: LexXrpcProcedure | LexXrpcQuery,
|
|
99
|
-
|
|
100
|
-
opts: RouteOpts,
|
|
114
|
+
options: RouteOptions,
|
|
101
115
|
lexicons: Lexicons,
|
|
102
|
-
):
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
)
|
|
126
|
-
|
|
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
|
-
`
|
|
145
|
+
`A request body is expected but none was provided`,
|
|
129
146
|
)
|
|
130
|
-
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const reqEncoding = parseReqEncoding(req)
|
|
150
|
+
if (checkEncoding && !checkEncoding(reqEncoding)) {
|
|
131
151
|
throw new InvalidRequestError(
|
|
132
|
-
`Wrong request encoding (Content-Type): ${
|
|
152
|
+
`Wrong request encoding (Content-Type): ${reqEncoding}`,
|
|
133
153
|
)
|
|
134
154
|
}
|
|
135
|
-
}
|
|
136
155
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
156
|
+
if (bodyParser) {
|
|
157
|
+
await bodyParser(req, res)
|
|
158
|
+
}
|
|
141
159
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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 |
|
|
182
|
+
output: HandlerSuccess | void,
|
|
171
183
|
lexicons: Lexicons,
|
|
172
184
|
): void {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
def.output
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
}
|
|
209
|
-
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
240
|
+
if (!shortType) return null
|
|
220
241
|
return shortType
|
|
221
242
|
}
|
|
222
243
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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:
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
431
|
+
return undefined
|
|
381
432
|
} else if (char === 63 /* "?"" */) {
|
|
382
433
|
break
|
|
383
434
|
} else {
|
|
384
|
-
|
|
435
|
+
return undefined
|
|
385
436
|
}
|
|
386
437
|
}
|
|
387
438
|
|
|
388
439
|
// last char was one of: '-', '.', '/'
|
|
389
440
|
if (alphaNumRequired) {
|
|
390
|
-
|
|
441
|
+
return undefined
|
|
391
442
|
}
|
|
392
443
|
|
|
393
444
|
// A domain name consists of minimum two characters
|
|
394
445
|
if (curr - startOfNsid < 2) {
|
|
395
|
-
|
|
446
|
+
return undefined
|
|
396
447
|
}
|
|
397
448
|
|
|
398
|
-
// @TODO
|
|
449
|
+
// @TODO check max length of nsid
|
|
399
450
|
|
|
400
451
|
return url.slice(startOfNsid, curr)
|
|
401
452
|
}
|
package/tests/auth.test.ts
CHANGED
|
@@ -56,8 +56,8 @@ describe('Auth', () => {
|
|
|
56
56
|
return {
|
|
57
57
|
encoding: 'application/json',
|
|
58
58
|
body: {
|
|
59
|
-
username: auth
|
|
60
|
-
original: auth
|
|
59
|
+
username: auth.credentials.username,
|
|
60
|
+
original: auth.artifacts.original,
|
|
61
61
|
},
|
|
62
62
|
}
|
|
63
63
|
},
|
package/tests/bodies.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
})
|
package/tests/errors.test.ts
CHANGED
|
@@ -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
|
|
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') {
|
package/tests/ipld.test.ts
CHANGED
|
@@ -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
|
-
|
|
54
|
-
(ctx
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 () => {
|
package/tests/parameters.test.ts
CHANGED
|
@@ -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
|
-
'
|
|
39
|
-
|
|
40
|
-
|
|
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 () => {
|
package/tests/procedures.test.ts
CHANGED
|
@@ -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
|
|
89
|
+
server.method('io.example.pingOne', (ctx) => {
|
|
89
90
|
return { encoding: 'text/plain', body: ctx.params.message }
|
|
90
91
|
})
|
|
91
|
-
server.method(
|
|
92
|
-
'
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
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 () => {
|