@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,331 +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: LexiconDoc[] = [
14
- {
15
- lexicon: 1,
16
- id: 'io.example.paramTest',
17
- defs: {
18
- main: {
19
- type: 'query',
20
- parameters: {
21
- type: 'params',
22
- required: ['str', 'int', 'bool', 'arr'],
23
- properties: {
24
- str: { type: 'string', minLength: 2, maxLength: 10 },
25
- int: { type: 'integer', minimum: 2, maximum: 10 },
26
- bool: { type: 'boolean' },
27
- arr: { type: 'array', items: { type: 'integer' }, maxLength: 2 },
28
- def: { type: 'integer', default: 0 },
29
- },
30
- },
31
- output: {
32
- encoding: 'application/json',
33
- },
34
- },
35
- },
36
- },
37
- ]
38
-
39
- describe('Parameters', () => {
40
- let s: http.Server
41
- const server = xrpcServer.createServer(LEXICONS)
42
- server.method('io.example.paramTest', (ctx) => ({
43
- encoding: 'json',
44
- body: ctx.params,
45
- }))
46
-
47
- let client: XrpcClient
48
- beforeAll(async () => {
49
- s = await createServer(server)
50
- const { port } = s.address() as AddressInfo
51
- client = new XrpcClient(
52
- `http://localhost:${port}`,
53
- structuredClone(LEXICONS),
54
- )
55
- })
56
- afterAll(async () => {
57
- await closeServer(s)
58
- })
59
-
60
- it('validates query params', async () => {
61
- const res1 = await client.call('io.example.paramTest', {
62
- str: 'valid',
63
- int: 5,
64
- bool: true,
65
- arr: [1, 2],
66
- def: 5,
67
- })
68
- expect(res1.success).toBeTruthy()
69
- expect(res1.data.str).toBe('valid')
70
- expect(res1.data.int).toBe(5)
71
- expect(res1.data.bool).toBe(true)
72
- expect(res1.data.arr).toEqual([1, 2])
73
- expect(res1.data.def).toEqual(5)
74
-
75
- const res2 = await client.call('io.example.paramTest', {
76
- str: 10,
77
- int: '5',
78
- bool: 'foo',
79
- arr: '3',
80
- })
81
- expect(res2.success).toBeTruthy()
82
- expect(res2.data.str).toBe('10')
83
- expect(res2.data.int).toBe(5)
84
- expect(res2.data.bool).toBe(true)
85
- expect(res2.data.arr).toEqual([3])
86
- expect(res2.data.def).toEqual(0)
87
-
88
- // @TODO test sending blatantly bad types
89
- await expect(
90
- client.call('io.example.paramTest', {
91
- str: 'n',
92
- int: 5,
93
- bool: true,
94
- arr: [1],
95
- }),
96
- ).rejects.toThrow('str must not be shorter than 2 characters')
97
- await expect(
98
- client.call('io.example.paramTest', {
99
- str: 'loooooooooooooong',
100
- int: 5,
101
- bool: true,
102
- arr: [1],
103
- }),
104
- ).rejects.toThrow('str must not be longer than 10 characters')
105
- await expect(
106
- client.call('io.example.paramTest', {
107
- int: 5,
108
- bool: true,
109
- arr: [1],
110
- }),
111
- ).rejects.toThrow(`Params must have the property "str"`)
112
-
113
- await expect(
114
- client.call('io.example.paramTest', {
115
- str: 'valid',
116
- int: -1,
117
- bool: true,
118
- arr: [1],
119
- }),
120
- ).rejects.toThrow('int can not be less than 2')
121
- await expect(
122
- client.call('io.example.paramTest', {
123
- str: 'valid',
124
- int: 11,
125
- bool: true,
126
- arr: [1],
127
- }),
128
- ).rejects.toThrow('int can not be greater than 10')
129
- await expect(
130
- client.call('io.example.paramTest', {
131
- str: 'valid',
132
- bool: true,
133
- arr: [1],
134
- }),
135
- ).rejects.toThrow(`Params must have the property "int"`)
136
-
137
- await expect(
138
- client.call('io.example.paramTest', {
139
- str: 'valid',
140
- int: 5,
141
- arr: [1],
142
- }),
143
- ).rejects.toThrow(`Params must have the property "bool"`)
144
-
145
- await expect(
146
- client.call('io.example.paramTest', {
147
- str: 'valid',
148
- int: 5,
149
- bool: true,
150
- arr: [],
151
- }),
152
- ).rejects.toThrow('Error: Params must have the property "arr"')
153
- await expect(
154
- client.call('io.example.paramTest', {
155
- str: 'valid',
156
- int: 5,
157
- bool: true,
158
- arr: [1, 2, 3],
159
- }),
160
- ).rejects.toThrow('Error: arr must not have more than 2 elements')
161
- })
162
- })
163
-
164
- const LOOSE_PARAMS_LEXICONS = [
165
- {
166
- lexicon: 1,
167
- id: 'io.example.looseParamsTest',
168
- defs: {
169
- main: {
170
- type: 'query',
171
- parameters: {
172
- type: 'params',
173
- required: ['str'],
174
- properties: {
175
- str: { type: 'string' },
176
- arr: { type: 'array', items: { type: 'string' } },
177
- },
178
- },
179
- output: {
180
- encoding: 'application/json',
181
- },
182
- },
183
- },
184
- },
185
- ] as const satisfies LexiconDoc[]
186
-
187
- for (const buildServer of [buildMethodLexicons, buildAddLexicons]) {
188
- describe(buildServer, () => {
189
- let s: http.Server
190
- let url: string
191
-
192
- beforeAll(async () => {
193
- const server = await buildServer(LOOSE_PARAMS_LEXICONS, {
194
- 'io.example.looseParamsTest': {
195
- opts:
196
- buildServer === buildAddLexicons
197
- ? { paramsParseLoose: true }
198
- : undefined,
199
- handler: (ctx: xrpcServer.HandlerContext) => ({
200
- encoding: 'application/json',
201
- body: ctx.params,
202
- }),
203
- },
204
- })
205
- s = await createServer(server)
206
- const { port } = s.address() as AddressInfo
207
- url = `http://localhost:${port}`
208
- })
209
-
210
- afterAll(async () => {
211
- await closeServer(s)
212
- })
213
-
214
- it('converts bracket[] array syntax to standard params', async () => {
215
- const res = await fetch(
216
- `${url}/xrpc/io.example.looseParamsTest?str=hello&arr[]=one&arr[]=two`,
217
- )
218
- expect(res.status).toBe(200)
219
- const body = await res.json()
220
- expect(body.str).toBe('hello')
221
- expect(body.arr).toEqual(['one', 'two'])
222
- })
223
-
224
- it('converts bracket[n] array syntax to standard params', async () => {
225
- const res = await fetch(
226
- `${url}/xrpc/io.example.looseParamsTest?str=hello&arr[0]=one&arr[1]=two`,
227
- )
228
- expect(res.status).toBe(200)
229
- const body = await res.json()
230
- expect(body.str).toBe('hello')
231
- expect(body.arr).toEqual(['one', 'two'])
232
- })
233
-
234
- it('ignores empty indices when using bracket[n] syntax', async () => {
235
- const res = await fetch(
236
- `${url}/xrpc/io.example.looseParamsTest?str=hello&arr[4]=one&arr[9]=two`,
237
- )
238
- expect(res.status).toBe(200)
239
- const body = await res.json()
240
- expect(body.str).toBe('hello')
241
- expect(body.arr).toEqual(['one', 'two'])
242
- })
243
-
244
- it('still handles standard array syntax', async () => {
245
- const res = await fetch(
246
- `${url}/xrpc/io.example.looseParamsTest?str=hello&arr=one&arr=two`,
247
- )
248
- expect(res.status).toBe(200)
249
- const body = await res.json()
250
- expect(body.str).toBe('hello')
251
- expect(body.arr).toEqual(['one', 'two'])
252
- })
253
-
254
- it('handles single bracket value', async () => {
255
- const res = await fetch(
256
- `${url}/xrpc/io.example.looseParamsTest?str=hello&arr[]=only`,
257
- )
258
- expect(res.status).toBe(200)
259
- const body = await res.json()
260
- expect(body.str).toBe('hello')
261
- expect(body.arr).toEqual(['only'])
262
- })
263
-
264
- it('handles single indexed bracket value', async () => {
265
- const res = await fetch(
266
- `${url}/xrpc/io.example.looseParamsTest?str=hello&arr[0]=only`,
267
- )
268
- expect(res.status).toBe(200)
269
- const body = await res.json()
270
- expect(body.str).toBe('hello')
271
- expect(body.arr).toEqual(['only'])
272
- })
273
- })
274
- }
275
-
276
- describe('paramsParseLoose option', () => {
277
- it('throws when used with method()', () => {
278
- const server = xrpcServer.createServer(
279
- structuredClone(LOOSE_PARAMS_LEXICONS),
280
- )
281
- expect(() => {
282
- server.method('io.example.looseParamsTest', {
283
- opts: { paramsParseLoose: true },
284
- handler: () => ({
285
- encoding: 'application/json',
286
- body: {},
287
- }),
288
- })
289
- }).toThrow('paramsParseLoose is not supported with method()')
290
- expect(() => {
291
- server.method('io.example.looseParamsTest', {
292
- opts: { paramsParseLoose: false },
293
- handler: () => ({
294
- encoding: 'application/json',
295
- body: {},
296
- }),
297
- })
298
- }).toThrow('paramsParseLoose is not supported with method()')
299
- })
300
- })
301
-
302
- describe(buildAddLexicons, () => {
303
- it('does not use loose parsing by default', async () => {
304
- const server = await buildAddLexicons(LOOSE_PARAMS_LEXICONS, {
305
- 'io.example.looseParamsTest': (ctx: xrpcServer.HandlerContext) => ({
306
- encoding: 'application/json',
307
- body: ctx.params,
308
- }),
309
- })
310
-
311
- await using httpServer = await createServer(server)
312
- const { port } = httpServer.address() as AddressInfo
313
- const url = `http://localhost:${port}`
314
-
315
- // standard array syntax works
316
- const res = await fetch(
317
- `${url}/xrpc/io.example.looseParamsTest?str=hello&arr=one&arr=two`,
318
- )
319
- expect(res.status).toBe(200)
320
- const body = await res.json()
321
- expect(body.arr).toEqual(['one', 'two'])
322
-
323
- // bracket syntax is not converted without paramsParseLoose
324
- const bracketRes = await fetch(
325
- `${url}/xrpc/io.example.looseParamsTest?str=hello&arr[]=one&arr[]=two`,
326
- )
327
- expect(bracketRes.status).toBe(200)
328
- const bracketBody = await bracketRes.json()
329
- expect(bracketBody.arr).toBeUndefined()
330
- })
331
- })
@@ -1,89 +0,0 @@
1
- import { parseUrlNsid } from '../src/util.js'
2
-
3
- const testValid = (url: string, expected: string) => {
4
- expect(parseUrlNsid(url)).toBe(expected)
5
- }
6
-
7
- const testInvalid = (url: string, errorMessage = 'invalid xrpc path') => {
8
- expect(() => parseUrlNsid(url)).toThrow(errorMessage)
9
- }
10
-
11
- describe('parseUrlNsid', () => {
12
- it('should extract the NSID from the URL', () => {
13
- testValid('/xrpc/blee.blah.bloo', 'blee.blah.bloo')
14
- testValid('/xrpc/blee.blah.bloo?foo[]', 'blee.blah.bloo')
15
- testValid('/xrpc/blee.blah.bloo?foo=bar', 'blee.blah.bloo')
16
- testValid('/xrpc/com.example.nsid', 'com.example.nsid')
17
- testValid('/xrpc/com.example.nsid?foo=bar', 'com.example.nsid')
18
- testValid('/xrpc/com.example-domain.nsid', 'com.example-domain.nsid')
19
- })
20
-
21
- it('should allow a trailing slash', () => {
22
- testValid('/xrpc/blee.blah.bloo/?', 'blee.blah.bloo')
23
- testValid('/xrpc/blee.blah.bloo/?foo=', 'blee.blah.bloo')
24
- testValid('/xrpc/blee.blah.bloo/?bool', 'blee.blah.bloo')
25
- testValid('/xrpc/com.example.nsid/', 'com.example.nsid')
26
- })
27
-
28
- it('should throw an error if the URL is too short', () => {
29
- testInvalid('/xrpc/a')
30
- })
31
-
32
- it('should throw an error if the URL is empty', () => {
33
- testInvalid('')
34
- })
35
-
36
- it('should throw an error if the URL is missing the NSID', () => {
37
- testInvalid('/xrpc/')
38
- testInvalid('/xrpc/?')
39
- testInvalid('/xrpc/?foo=bar')
40
- })
41
-
42
- it('should throw an error if the URL contains extra path segments', () => {
43
- testInvalid('/xrpc/123/extra')
44
- testInvalid('/xrpc/123/extra?foo=bar')
45
- })
46
-
47
- it('should throw an error if the URL is missing the XRPC path prefix', () => {
48
- testInvalid('/foo/123')
49
- testInvalid('/foo/com.example.nsid')
50
- })
51
-
52
- it('should throw an error if the NSID starts with a dot', () => {
53
- testInvalid('/xrpc/.')
54
- testInvalid('/xrpc/..')
55
- testInvalid('/xrpc/....')
56
- testInvalid('/xrpc/.com.example.nsid')
57
- testInvalid('/xrpc/com..example.nsid')
58
- testInvalid('/xrpc/com.example..nsid')
59
- testInvalid('/xrpc/com.example.nsid.')
60
- testInvalid('/xrpc/com.example.nsid./')
61
- testInvalid('/xrpc/com.example.nsid.?foo=bar')
62
- testInvalid('/xrpc/com.example.nsid./?foo=bar')
63
- })
64
-
65
- it('should throw an error if the NSID contains a misplaced dash', () => {
66
- testInvalid('/xrpc/-')
67
- testInvalid('/xrpc/com.example.-nsid')
68
- testInvalid('/xrpc/com.example-.nsid')
69
- testInvalid('/xrpc/com.-example.nsid')
70
- testInvalid('/xrpc/com.-example-.nsid')
71
- testInvalid('/xrpc/com.example.nsid-')
72
- testInvalid('/xrpc/-com.example.nsid')
73
- testInvalid('/xrpc/com.example--domain.nsid')
74
- })
75
-
76
- it('should throw an error if the URL starts with a space', () => {
77
- testInvalid(' /xrpc/com.example.nsid')
78
- })
79
-
80
- it('should throw an error if the NSID contains invalid characters', () => {
81
- testInvalid('/xrpc/com.example.nsid#')
82
- testInvalid('/xrpc/com.example.nsid!')
83
- testInvalid('/xrpc/com.example#?nsid')
84
- testInvalid('/xrpc/!com.example.nsid')
85
- testInvalid('/xrpc/com.example.nsid ')
86
- testInvalid('/xrpc/ com.example.nsid')
87
- testInvalid('/xrpc/com. example.nsid')
88
- })
89
- })
@@ -1,176 +0,0 @@
1
- import assert from 'node:assert'
2
- import * as http from 'node:http'
3
- import { AddressInfo } from 'node:net'
4
- import { Readable } from 'node:stream'
5
- import { LexiconDoc } from '@atproto/lexicon'
6
- import { XrpcClient } from '@atproto/xrpc'
7
- import * as xrpcServer from '../src/index.js'
8
- import {
9
- buildAddLexicons,
10
- buildMethodLexicons,
11
- closeServer,
12
- createServer,
13
- } from './_util.js'
14
-
15
- const LEXICONS = [
16
- {
17
- lexicon: 1,
18
- id: 'io.example.pingOne',
19
- defs: {
20
- main: {
21
- type: 'procedure',
22
- parameters: {
23
- type: 'params',
24
- properties: {
25
- message: { type: 'string' },
26
- },
27
- },
28
- output: {
29
- encoding: 'text/plain',
30
- },
31
- },
32
- },
33
- },
34
- {
35
- lexicon: 1,
36
- id: 'io.example.pingTwo',
37
- defs: {
38
- main: {
39
- type: 'procedure',
40
- input: {
41
- encoding: 'text/plain',
42
- },
43
- output: {
44
- encoding: 'text/plain',
45
- },
46
- },
47
- },
48
- },
49
- {
50
- lexicon: 1,
51
- id: 'io.example.pingThree',
52
- defs: {
53
- main: {
54
- type: 'procedure',
55
- input: {
56
- encoding: 'application/octet-stream',
57
- },
58
- output: {
59
- encoding: 'application/octet-stream',
60
- },
61
- },
62
- },
63
- },
64
- {
65
- lexicon: 1,
66
- id: 'io.example.pingFour',
67
- defs: {
68
- main: {
69
- type: 'procedure',
70
- input: {
71
- encoding: 'application/json',
72
- schema: {
73
- type: 'object',
74
- required: ['message'],
75
- properties: { message: { type: 'string' } },
76
- },
77
- },
78
- output: {
79
- encoding: 'application/json',
80
- schema: {
81
- type: 'object',
82
- required: ['message'],
83
- properties: { message: { type: 'string' } },
84
- },
85
- },
86
- },
87
- },
88
- },
89
- ] as const satisfies LexiconDoc[]
90
-
91
- const handlers = {
92
- 'io.example.pingOne': (ctx: xrpcServer.HandlerContext) => {
93
- return { encoding: 'text/plain', body: ctx.params.message }
94
- },
95
- 'io.example.pingTwo': (ctx: xrpcServer.HandlerContext) => {
96
- return { encoding: 'text/plain', body: ctx.input?.body }
97
- },
98
- 'io.example.pingThree': async (ctx: xrpcServer.HandlerContext) => {
99
- assert(ctx.input?.body instanceof Readable, 'Input not readable')
100
- const buffers: Buffer[] = []
101
- for await (const data of ctx.input.body) {
102
- buffers.push(data)
103
- }
104
- return {
105
- encoding: 'application/octet-stream',
106
- body: Buffer.concat(buffers),
107
- }
108
- },
109
- 'io.example.pingFour': (ctx: xrpcServer.HandlerContext) => {
110
- return {
111
- encoding: 'application/json',
112
- body: { message: ctx.input?.body?.['message'] },
113
- }
114
- },
115
- }
116
-
117
- for (const buildServer of [buildMethodLexicons, buildAddLexicons]) {
118
- describe(buildServer, () => {
119
- let s: http.Server
120
- let client: XrpcClient
121
- let url: string
122
- beforeAll(async () => {
123
- const server = await buildServer(LEXICONS, handlers)
124
- s = await createServer(server)
125
- const { port } = s.address() as AddressInfo
126
- url = `http://localhost:${port}`
127
- client = new XrpcClient(url, structuredClone(LEXICONS))
128
- })
129
- afterAll(async () => {
130
- if (s) await closeServer(s)
131
- })
132
-
133
- test('io.example.pingOne', async () => {
134
- const res = await client.call('io.example.pingOne', {
135
- message: 'hello world',
136
- })
137
- expect(res.success).toBeTruthy()
138
- expect(res.headers['content-type']).toBe('text/plain; charset=utf-8')
139
- expect(res.data).toBe('hello world')
140
- })
141
-
142
- test('io.example.pingTwo', async () => {
143
- const res = await client.call('io.example.pingTwo', {}, 'hello world', {
144
- encoding: 'text/plain',
145
- })
146
- expect(res.success).toBeTruthy()
147
- expect(res.headers['content-type']).toBe('text/plain; charset=utf-8')
148
- expect(res.data).toBe('hello world')
149
- })
150
-
151
- test('io.example.pingThree', async () => {
152
- const res = await client.call(
153
- 'io.example.pingThree',
154
- {},
155
- new TextEncoder().encode('hello world'),
156
- { encoding: 'application/octet-stream' },
157
- )
158
- expect(res.success).toBeTruthy()
159
- expect(res.headers['content-type']).toBe('application/octet-stream')
160
- expect(new TextDecoder().decode(res.data)).toBe('hello world')
161
- })
162
-
163
- test('io.example.pingFour', async () => {
164
- const res = await client.call(
165
- 'io.example.pingFour',
166
- {},
167
- { message: 'hello world' },
168
- )
169
- expect(res.success).toBeTruthy()
170
- expect(res.headers['content-type']).toBe(
171
- 'application/json; charset=utf-8',
172
- )
173
- expect(res.data?.message).toBe('hello world')
174
- })
175
- })
176
- }