@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.
- package/CHANGELOG.md +42 -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 +3 -2
- package/dist/index.js.map +1 -1
- package/dist/rate-limiter.d.ts +95 -26
- package/dist/rate-limiter.d.ts.map +1 -1
- package/dist/rate-limiter.js +179 -85
- package/dist/rate-limiter.js.map +1 -1
- package/dist/server.d.ts +20 -15
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +185 -220
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +80 -175
- 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 +12 -9
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +114 -78
- 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 +4 -3
- package/src/rate-limiter.ts +270 -104
- package/src/server.ts +265 -276
- package/src/types.ts +144 -429
- package/src/util.ts +131 -85
- 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 +8 -11
- package/tests/responses.test.ts +12 -15
- 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
|
|
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
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
|
-
|
|
17
|
+
Awaitable,
|
|
17
18
|
HandlerSuccess,
|
|
18
|
-
|
|
19
|
-
InvalidRequestError,
|
|
19
|
+
Input,
|
|
20
20
|
Params,
|
|
21
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
const result:
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
110
|
+
export function createInputVerifier(
|
|
83
111
|
nsid: string,
|
|
84
112
|
def: LexXrpcProcedure | LexXrpcQuery,
|
|
85
|
-
|
|
86
|
-
opts: RouteOpts,
|
|
113
|
+
options: RouteOptions,
|
|
87
114
|
lexicons: Lexicons,
|
|
88
|
-
):
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
112
|
-
|
|
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): ${
|
|
150
|
+
`Wrong request encoding (Content-Type): ${reqEncoding}`,
|
|
119
151
|
)
|
|
120
152
|
}
|
|
121
|
-
}
|
|
122
153
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
154
|
+
if (bodyParser) {
|
|
155
|
+
await bodyParser(req, res)
|
|
156
|
+
}
|
|
127
157
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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 |
|
|
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
|
|
224
|
+
export function normalizeMime(v?: string): string | false {
|
|
201
225
|
if (!v) return false
|
|
202
|
-
const fullType =
|
|
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(
|
|
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:
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
412
|
+
return undefined
|
|
367
413
|
} else if (char === 63 /* "?"" */) {
|
|
368
414
|
break
|
|
369
415
|
} else {
|
|
370
|
-
|
|
416
|
+
return undefined
|
|
371
417
|
}
|
|
372
418
|
}
|
|
373
419
|
|
|
374
420
|
// last char was one of: '-', '.', '/'
|
|
375
421
|
if (alphaNumRequired) {
|
|
376
|
-
|
|
422
|
+
return undefined
|
|
377
423
|
}
|
|
378
424
|
|
|
379
425
|
// A domain name consists of minimum two characters
|
|
380
426
|
if (curr - startOfNsid < 2) {
|
|
381
|
-
|
|
427
|
+
return undefined
|
|
382
428
|
}
|
|
383
429
|
|
|
384
|
-
// @TODO
|
|
430
|
+
// @TODO check max length of nsid
|
|
385
431
|
|
|
386
432
|
return url.slice(startOfNsid, curr)
|
|
387
433
|
}
|
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 () => {
|
package/tests/queries.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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 {
|
|
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
|
|
136
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
212
|
+
handler: (ctx) => ({
|
|
216
213
|
encoding: 'json',
|
|
217
214
|
body: ctx.params,
|
|
218
215
|
}),
|