@atproto/xrpc-server 0.11.4 → 0.11.6

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.
@@ -1,140 +0,0 @@
1
- import * as http from 'node:http'
2
- import { AddressInfo } from 'node:net'
3
- import { LexiconDoc } from '@atproto/lexicon'
4
- import { XrpcClient } from '@atproto/xrpc'
5
- import * as xrpcServer from '../src/index.js'
6
- import {
7
- buildAddLexicons,
8
- buildMethodLexicons,
9
- closeServer,
10
- createServer,
11
- } from './_util.js'
12
-
13
- const LEXICONS = [
14
- {
15
- lexicon: 1,
16
- id: 'io.example.pingOne',
17
- defs: {
18
- main: {
19
- type: 'query',
20
- parameters: {
21
- type: 'params',
22
- properties: {
23
- message: { type: 'string' },
24
- },
25
- },
26
- output: {
27
- encoding: 'text/plain',
28
- },
29
- },
30
- },
31
- },
32
- {
33
- lexicon: 1,
34
- id: 'io.example.pingTwo',
35
- defs: {
36
- main: {
37
- type: 'query',
38
- parameters: {
39
- type: 'params',
40
- properties: {
41
- message: { type: 'string' },
42
- },
43
- },
44
- output: {
45
- encoding: 'application/octet-stream',
46
- },
47
- },
48
- },
49
- },
50
- {
51
- lexicon: 1,
52
- id: 'io.example.pingThree',
53
- defs: {
54
- main: {
55
- type: 'query',
56
- parameters: {
57
- type: 'params',
58
- properties: {
59
- message: { type: 'string' },
60
- },
61
- },
62
- output: {
63
- encoding: 'application/json',
64
- schema: {
65
- type: 'object',
66
- required: ['message'],
67
- properties: { message: { type: 'string' } },
68
- },
69
- },
70
- },
71
- },
72
- },
73
- ] as const satisfies LexiconDoc[]
74
-
75
- const handlers = {
76
- 'io.example.pingOne': (ctx: xrpcServer.HandlerContext) => {
77
- return { encoding: 'text/plain', body: ctx.params.message }
78
- },
79
- 'io.example.pingTwo': (ctx: xrpcServer.HandlerContext) => {
80
- return {
81
- encoding: 'application/octet-stream',
82
- body: new TextEncoder().encode(String(ctx.params.message)),
83
- }
84
- },
85
- 'io.example.pingThree': (ctx: xrpcServer.HandlerContext) => {
86
- return {
87
- encoding: 'application/json',
88
- body: { message: ctx.params.message },
89
- headers: { 'x-test-header-name': 'test-value' },
90
- }
91
- },
92
- }
93
-
94
- for (const buildServer of [buildMethodLexicons, buildAddLexicons]) {
95
- describe(buildServer, () => {
96
- let s: http.Server
97
- let client: XrpcClient
98
- let url: string
99
- beforeAll(async () => {
100
- const server = await buildServer(LEXICONS, handlers)
101
- s = await createServer(server)
102
- const { port } = s.address() as AddressInfo
103
- url = `http://localhost:${port}`
104
- client = new XrpcClient(url, LEXICONS)
105
- })
106
- afterAll(async () => {
107
- if (s) await closeServer(s)
108
- })
109
-
110
- test('io.example.pingOne', async () => {
111
- const res = await client.call('io.example.pingOne', {
112
- message: 'hello world',
113
- })
114
- expect(res.success).toBeTruthy()
115
- expect(res.headers['content-type']).toBe('text/plain; charset=utf-8')
116
- expect(res.data).toBe('hello world')
117
- })
118
-
119
- test('io.example.pingTwo', async () => {
120
- const res = await client.call('io.example.pingTwo', {
121
- message: 'hello world',
122
- })
123
- expect(res.success).toBeTruthy()
124
- expect(res.headers['content-type']).toBe('application/octet-stream')
125
- expect(new TextDecoder().decode(res.data)).toBe('hello world')
126
- })
127
-
128
- test('io.example.pingThree', async () => {
129
- const res = await client.call('io.example.pingThree', {
130
- message: 'hello world',
131
- })
132
- expect(res.success).toBeTruthy()
133
- expect(res.headers['content-type']).toBe(
134
- 'application/json; charset=utf-8',
135
- )
136
- expect(res.data?.message).toBe('hello world')
137
- expect(res.headers['x-test-header-name']).toEqual('test-value')
138
- })
139
- })
140
- }
@@ -1,312 +0,0 @@
1
- import * as http from 'node:http'
2
- import { AddressInfo } from 'node:net'
3
- import { MINUTE } from '@atproto/common'
4
- import { LexiconDoc } from '@atproto/lexicon'
5
- import { XrpcClient } from '@atproto/xrpc'
6
- import * as xrpcServer from '../src/index.js'
7
- import { MemoryRateLimiter } from '../src/index.js'
8
- import { closeServer, createServer } from './_util.js'
9
-
10
- const LEXICONS: LexiconDoc[] = [
11
- {
12
- lexicon: 1,
13
- id: 'io.example.routeLimit',
14
- defs: {
15
- main: {
16
- type: 'query',
17
- parameters: {
18
- type: 'params',
19
- required: ['str'],
20
- properties: {
21
- str: { type: 'string' },
22
- },
23
- },
24
- output: {
25
- encoding: 'application/json',
26
- },
27
- },
28
- },
29
- },
30
- {
31
- lexicon: 1,
32
- id: 'io.example.routeLimitReset',
33
- defs: {
34
- main: {
35
- type: 'query',
36
- parameters: {
37
- type: 'params',
38
- required: ['count'],
39
- properties: {
40
- count: { type: 'integer' },
41
- },
42
- },
43
- output: {
44
- encoding: 'application/json',
45
- },
46
- },
47
- },
48
- },
49
- {
50
- lexicon: 1,
51
- id: 'io.example.sharedLimitOne',
52
- defs: {
53
- main: {
54
- type: 'query',
55
- parameters: {
56
- type: 'params',
57
- required: ['points'],
58
- properties: {
59
- points: { type: 'integer' },
60
- },
61
- },
62
- output: {
63
- encoding: 'application/json',
64
- },
65
- },
66
- },
67
- },
68
- {
69
- lexicon: 1,
70
- id: 'io.example.sharedLimitTwo',
71
- defs: {
72
- main: {
73
- type: 'query',
74
- parameters: {
75
- type: 'params',
76
- required: ['points'],
77
- properties: {
78
- points: { type: 'integer' },
79
- },
80
- },
81
- output: {
82
- encoding: 'application/json',
83
- },
84
- },
85
- },
86
- },
87
- {
88
- lexicon: 1,
89
- id: 'io.example.toggleLimit',
90
- defs: {
91
- main: {
92
- type: 'query',
93
- parameters: {
94
- type: 'params',
95
- properties: {
96
- shouldCount: { type: 'boolean' },
97
- },
98
- },
99
- output: {
100
- encoding: 'application/json',
101
- },
102
- },
103
- },
104
- },
105
- {
106
- lexicon: 1,
107
- id: 'io.example.noLimit',
108
- defs: {
109
- main: {
110
- type: 'query',
111
- output: {
112
- encoding: 'application/json',
113
- },
114
- },
115
- },
116
- },
117
- {
118
- lexicon: 1,
119
- id: 'io.example.nonExistent',
120
- defs: {
121
- main: {
122
- type: 'query',
123
- output: {
124
- encoding: 'application/json',
125
- },
126
- },
127
- },
128
- },
129
- ]
130
-
131
- describe('Parameters', () => {
132
- let s: http.Server
133
- const server = xrpcServer.createServer(LEXICONS, {
134
- rateLimits: {
135
- creator: (opts) => new MemoryRateLimiter(opts),
136
- bypass: ({ req }) => req.headers['x-ratelimit-bypass'] === 'bypass',
137
- shared: [
138
- {
139
- name: 'shared-limit',
140
- durationMs: 5 * MINUTE,
141
- points: 6,
142
- },
143
- ],
144
- global: [
145
- {
146
- name: 'global-ip',
147
- durationMs: 5 * MINUTE,
148
- points: 100,
149
- },
150
- ],
151
- },
152
- })
153
- server.method('io.example.routeLimit', {
154
- rateLimit: {
155
- durationMs: 5 * MINUTE,
156
- points: 5,
157
- calcKey: ({ params }) => params.str as string,
158
- },
159
- handler: (ctx) => ({
160
- encoding: 'json',
161
- body: ctx.params,
162
- }),
163
- })
164
- server.method('io.example.routeLimitReset', {
165
- rateLimit: {
166
- durationMs: 5 * MINUTE,
167
- points: 2,
168
- },
169
- handler: (ctx) => {
170
- if (ctx.params.count === 1) {
171
- ctx.resetRouteRateLimits()
172
- }
173
-
174
- return {
175
- encoding: 'json',
176
- body: {},
177
- }
178
- },
179
- })
180
- server.method('io.example.sharedLimitOne', {
181
- rateLimit: {
182
- name: 'shared-limit',
183
- calcPoints: ({ params }) => params.points as number,
184
- },
185
- handler: (ctx) => ({
186
- encoding: 'json',
187
- body: ctx.params,
188
- }),
189
- })
190
- server.method('io.example.sharedLimitTwo', {
191
- rateLimit: {
192
- name: 'shared-limit',
193
- calcPoints: ({ params }) => params.points as number,
194
- },
195
- handler: (ctx) => ({
196
- encoding: 'json',
197
- body: ctx.params,
198
- }),
199
- })
200
- server.method('io.example.toggleLimit', {
201
- rateLimit: [
202
- {
203
- durationMs: 5 * MINUTE,
204
- points: 5,
205
- calcPoints: ({ params }) => (params.shouldCount ? 1 : 0),
206
- },
207
- {
208
- durationMs: 5 * MINUTE,
209
- points: 10,
210
- },
211
- ],
212
- handler: (ctx) => ({
213
- encoding: 'json',
214
- body: ctx.params,
215
- }),
216
- })
217
- server.method('io.example.noLimit', {
218
- handler: () => ({
219
- encoding: 'json',
220
- body: {},
221
- }),
222
- })
223
-
224
- let client: XrpcClient
225
- beforeAll(async () => {
226
- s = await createServer(server)
227
- const { port } = s.address() as AddressInfo
228
- client = new XrpcClient(`http://localhost:${port}`, LEXICONS)
229
- })
230
- afterAll(async () => {
231
- await closeServer(s)
232
- })
233
-
234
- it('rate limits a given route', async () => {
235
- const makeCall = () => client.call('io.example.routeLimit', { str: 'test' })
236
- for (let i = 0; i < 5; i++) {
237
- await makeCall()
238
- }
239
- await expect(makeCall).rejects.toThrow('Rate Limit Exceeded')
240
- })
241
-
242
- it('can reset route rate limits', async () => {
243
- // Limit is 2.
244
- // Call 0 is OK (1/2).
245
- // Call 1 is OK (2/2), and resets the limit.
246
- // Call 2 is OK (1/2).
247
- // Call 3 is OK (2/2).
248
- for (let i = 0; i < 4; i++) {
249
- await client.call('io.example.routeLimitReset', { count: i })
250
- }
251
-
252
- // Call 4 exceeds the limit (3/2).
253
- await expect(
254
- client.call('io.example.routeLimitReset', { count: 4 }),
255
- ).rejects.toThrow('Rate Limit Exceeded')
256
- })
257
-
258
- it('rate limits on a shared route', async () => {
259
- await client.call('io.example.sharedLimitOne', { points: 1 })
260
- await client.call('io.example.sharedLimitTwo', { points: 1 })
261
- await client.call('io.example.sharedLimitOne', { points: 2 })
262
- await client.call('io.example.sharedLimitTwo', { points: 2 })
263
- await expect(
264
- client.call('io.example.sharedLimitOne', { points: 1 }),
265
- ).rejects.toThrow('Rate Limit Exceeded')
266
- await expect(
267
- client.call('io.example.sharedLimitTwo', { points: 1 }),
268
- ).rejects.toThrow('Rate Limit Exceeded')
269
- })
270
-
271
- it('applies multiple rate-limits', async () => {
272
- const makeCall = (shouldCount: boolean) =>
273
- client.call('io.example.toggleLimit', { shouldCount })
274
- for (let i = 0; i < 5; i++) {
275
- await makeCall(true)
276
- }
277
- await expect(() => makeCall(true)).rejects.toThrow('Rate Limit Exceeded')
278
- for (let i = 0; i < 4; i++) {
279
- await makeCall(false)
280
- }
281
- await expect(() => makeCall(false)).rejects.toThrow('Rate Limit Exceeded')
282
- })
283
-
284
- it('applies global limits', async () => {
285
- const makeCall = () => client.call('io.example.noLimit')
286
- const calls: Promise<unknown>[] = []
287
- for (let i = 0; i < 110; i++) {
288
- calls.push(makeCall())
289
- }
290
- await expect(Promise.all(calls)).rejects.toThrow('Rate Limit Exceeded')
291
- })
292
-
293
- it('applies global limits to xrpc catchall', async () => {
294
- const makeCall = () => client.call('io.example.nonExistent')
295
- await expect(makeCall()).rejects.toThrow('Rate Limit Exceeded')
296
- })
297
-
298
- it('can bypass rate limits', async () => {
299
- const makeCall = () =>
300
- client.call(
301
- 'io.example.noLimit',
302
- {},
303
- {},
304
- { headers: { 'X-RateLimit-Bypass': 'bypass' } },
305
- )
306
- const calls: Promise<unknown>[] = []
307
- for (let i = 0; i < 110; i++) {
308
- calls.push(makeCall())
309
- }
310
- await Promise.all(calls)
311
- })
312
- })
@@ -1,72 +0,0 @@
1
- import * as http from 'node:http'
2
- import { AddressInfo } from 'node:net'
3
- import { byteIterableToStream } from '@atproto/common'
4
- import { LexiconDoc } from '@atproto/lexicon'
5
- import { XrpcClient } from '@atproto/xrpc'
6
- import * as xrpcServer from '../src/index.js'
7
- import { closeServer, createServer } from './_util.js'
8
-
9
- const LEXICONS: LexiconDoc[] = [
10
- {
11
- lexicon: 1,
12
- id: 'io.example.readableStream',
13
- defs: {
14
- main: {
15
- type: 'query',
16
- parameters: {
17
- type: 'params',
18
- properties: {
19
- shouldErr: { type: 'boolean' },
20
- },
21
- },
22
- output: {
23
- encoding: 'application/vnd.ipld.car',
24
- },
25
- },
26
- },
27
- },
28
- ]
29
-
30
- describe('Responses', () => {
31
- let s: http.Server
32
- const server = xrpcServer.createServer(LEXICONS)
33
- server.method('io.example.readableStream', async (ctx) => {
34
- async function* iter(): AsyncIterable<Uint8Array> {
35
- for (let i = 0; i < 5; i++) {
36
- yield new Uint8Array([i])
37
- }
38
- if (ctx.params.shouldErr) {
39
- throw new Error('error')
40
- }
41
- }
42
- return {
43
- encoding: 'application/vnd.ipld.car',
44
- body: byteIterableToStream(iter()),
45
- }
46
- })
47
-
48
- let client: XrpcClient
49
- beforeAll(async () => {
50
- s = await createServer(server)
51
- const { port } = s.address() as AddressInfo
52
- client = new XrpcClient(`http://localhost:${port}`, LEXICONS)
53
- })
54
- afterAll(async () => {
55
- await closeServer(s)
56
- })
57
-
58
- it('returns readable streams of bytes', async () => {
59
- const res = await client.call('io.example.readableStream', {
60
- shouldErr: false,
61
- })
62
- const expected = new Uint8Array([0, 1, 2, 3, 4])
63
- expect(res.data).toEqual(expected)
64
- })
65
-
66
- it('handles errs on readable streams of bytes', async () => {
67
- const attempt = client.call('io.example.readableStream', {
68
- shouldErr: true,
69
- })
70
- await expect(attempt).rejects.toThrow()
71
- })
72
- })
@@ -1,169 +0,0 @@
1
- import { once } from 'node:events'
2
- import * as http from 'node:http'
3
- import { AddressInfo } from 'node:net'
4
- import { WebSocket } from 'ws'
5
- import { XRPCError } from '@atproto/xrpc'
6
- import {
7
- ErrorFrame,
8
- Frame,
9
- MessageFrame,
10
- XrpcStreamServer,
11
- byFrame,
12
- byMessage,
13
- } from '../src/index.js'
14
-
15
- describe('Stream', () => {
16
- const wait = (ms) => new Promise((res) => setTimeout(res, ms))
17
- it('streams message and info frames.', async () => {
18
- const httpServer = http.createServer()
19
- const server = new XrpcStreamServer({
20
- server: httpServer,
21
- handler: async function* () {
22
- await wait(1)
23
- yield new MessageFrame(1)
24
- await wait(1)
25
- yield new MessageFrame(2)
26
- await wait(1)
27
- yield new MessageFrame(3)
28
- return
29
- },
30
- })
31
-
32
- await once(httpServer.listen(), 'listening')
33
- const { port } = server.wss.address() as AddressInfo
34
-
35
- const ws = new WebSocket(`ws://localhost:${port}`)
36
- const frames: Frame[] = []
37
- for await (const frame of byFrame(ws)) {
38
- frames.push(frame)
39
- }
40
-
41
- expect(frames).toEqual([
42
- new MessageFrame(1),
43
- new MessageFrame(2),
44
- new MessageFrame(3),
45
- ])
46
-
47
- httpServer.close()
48
- })
49
-
50
- it('kills handler and closes on error frame.', async () => {
51
- let proceededAfterError = false
52
- const httpServer = http.createServer()
53
- const server = new XrpcStreamServer({
54
- server: httpServer,
55
- handler: async function* () {
56
- await wait(1)
57
- yield new MessageFrame(1)
58
- await wait(1)
59
- yield new MessageFrame(2)
60
- await wait(1)
61
- yield new ErrorFrame({ error: 'BadOops' })
62
- proceededAfterError = true
63
- await wait(1)
64
- yield new MessageFrame(3)
65
- return
66
- },
67
- })
68
-
69
- await once(httpServer.listen(), 'listening')
70
- const { port } = server.wss.address() as AddressInfo
71
-
72
- const ws = new WebSocket(`ws://localhost:${port}`)
73
- const frames: Frame[] = []
74
- for await (const frame of byFrame(ws)) {
75
- frames.push(frame)
76
- }
77
-
78
- await wait(5) // Ensure handler hasn't kept running
79
- expect(proceededAfterError).toEqual(false)
80
-
81
- expect(frames).toEqual([
82
- new MessageFrame(1),
83
- new MessageFrame(2),
84
- new ErrorFrame({ error: 'BadOops' }),
85
- ])
86
-
87
- httpServer.close()
88
- })
89
-
90
- it('kills handler and closes client disconnect.', async () => {
91
- const httpServer = http.createServer()
92
- let i = 1
93
- const server = new XrpcStreamServer({
94
- server: httpServer,
95
- handler: async function* () {
96
- while (true) {
97
- await wait(0)
98
- yield new MessageFrame(i++)
99
- }
100
- },
101
- })
102
-
103
- await once(httpServer.listen(), 'listening')
104
- const { port } = server.wss.address() as AddressInfo
105
-
106
- const ws = new WebSocket(`ws://localhost:${port}`)
107
- const frames: Frame[] = []
108
- for await (const frame of byFrame(ws)) {
109
- frames.push(frame)
110
- if (frame.body === 3) ws.terminate()
111
- }
112
-
113
- // Grace period to let close take place on the server
114
- await wait(5)
115
- // Ensure handler hasn't kept running
116
- const currentCount = i
117
- await wait(5)
118
- expect(i).toBe(currentCount)
119
-
120
- httpServer.close()
121
- })
122
-
123
- describe('byMessage()', () => {
124
- it('kills handler and closes client disconnect on error frame.', async () => {
125
- const httpServer = http.createServer()
126
- const server = new XrpcStreamServer({
127
- server: httpServer,
128
- handler: async function* () {
129
- await wait(1)
130
- yield new MessageFrame(1)
131
- await wait(1)
132
- yield new MessageFrame(2)
133
- await wait(1)
134
- yield new ErrorFrame({
135
- error: 'BadOops',
136
- message: 'That was a bad one',
137
- })
138
- await wait(1)
139
- yield new MessageFrame(3)
140
- return
141
- },
142
- })
143
- await once(httpServer.listen(), 'listening')
144
- const { port } = server.wss.address() as AddressInfo
145
-
146
- const ws = new WebSocket(`ws://localhost:${port}`)
147
- const frames: Frame[] = []
148
-
149
- let error
150
- try {
151
- for await (const frame of byMessage(ws)) {
152
- frames.push(frame)
153
- }
154
- } catch (err) {
155
- error = err
156
- }
157
-
158
- expect(ws.readyState).toEqual(ws.CLOSING)
159
- expect(frames).toEqual([new MessageFrame(1), new MessageFrame(2)])
160
- expect(error).toBeInstanceOf(XRPCError)
161
- if (error instanceof XRPCError) {
162
- expect(error.error).toEqual('BadOops')
163
- expect(error.message).toEqual('That was a bad one')
164
- }
165
-
166
- httpServer.close()
167
- })
168
- })
169
- })