@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.
- package/CHANGELOG.md +24 -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 +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 +103 -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 +188 -96
- package/src/server.ts +198 -154
- package/src/types.ts +144 -439
- package/src/util.ts +116 -84
- 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,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
|
|
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,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
|
-
|
|
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
|
|
|
@@ -81,93 +80,104 @@ export function decodeQueryParam(
|
|
|
81
80
|
}
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
export
|
|
85
|
-
|
|
86
|
-
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)
|
|
87
94
|
for (const key of searchParams.keys()) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
110
|
+
export function createInputVerifier(
|
|
97
111
|
nsid: string,
|
|
98
112
|
def: LexXrpcProcedure | LexXrpcQuery,
|
|
99
|
-
|
|
100
|
-
opts: RouteOpts,
|
|
113
|
+
options: RouteOptions,
|
|
101
114
|
lexicons: Lexicons,
|
|
102
|
-
):
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
125
|
+
return undefined
|
|
126
|
+
}
|
|
118
127
|
}
|
|
119
128
|
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
)
|
|
126
|
-
|
|
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): ${
|
|
150
|
+
`Wrong request encoding (Content-Type): ${reqEncoding}`,
|
|
133
151
|
)
|
|
134
152
|
}
|
|
135
|
-
}
|
|
136
153
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
154
|
+
if (bodyParser) {
|
|
155
|
+
await bodyParser(req, res)
|
|
156
|
+
}
|
|
141
157
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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 |
|
|
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
|
|
224
|
+
export function normalizeMime(v?: string): string | false {
|
|
215
225
|
if (!v) return false
|
|
216
|
-
const fullType =
|
|
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(
|
|
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:
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
412
|
+
return undefined
|
|
381
413
|
} else if (char === 63 /* "?"" */) {
|
|
382
414
|
break
|
|
383
415
|
} else {
|
|
384
|
-
|
|
416
|
+
return undefined
|
|
385
417
|
}
|
|
386
418
|
}
|
|
387
419
|
|
|
388
420
|
// last char was one of: '-', '.', '/'
|
|
389
421
|
if (alphaNumRequired) {
|
|
390
|
-
|
|
422
|
+
return undefined
|
|
391
423
|
}
|
|
392
424
|
|
|
393
425
|
// A domain name consists of minimum two characters
|
|
394
426
|
if (curr - startOfNsid < 2) {
|
|
395
|
-
|
|
427
|
+
return undefined
|
|
396
428
|
}
|
|
397
429
|
|
|
398
|
-
// @TODO
|
|
430
|
+
// @TODO check max length of nsid
|
|
399
431
|
|
|
400
432
|
return url.slice(startOfNsid, curr)
|
|
401
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,7 +132,7 @@ describe('Parameters', () => {
|
|
|
132
132
|
let s: http.Server
|
|
133
133
|
const server = xrpcServer.createServer(LEXICONS, {
|
|
134
134
|
rateLimits: {
|
|
135
|
-
creator: (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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
212
|
+
handler: (ctx) => ({
|
|
213
213
|
encoding: 'json',
|
|
214
214
|
body: ctx.params,
|
|
215
215
|
}),
|