@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,299 +0,0 @@
1
- import * as http from 'node:http'
2
- import { AddressInfo } from 'node:net'
3
- import { LexiconDoc } from '@atproto/lexicon'
4
- import { XRPCError, XRPCInvalidResponseError, XrpcClient } from '@atproto/xrpc'
5
- import * as xrpcServer from '../src/index.js'
6
- import { closeServer, createServer } from './_util.js'
7
-
8
- const UPSTREAM_LEXICONS: LexiconDoc[] = [
9
- {
10
- lexicon: 1,
11
- id: 'io.example.upstreamInvalidResponse',
12
- defs: {
13
- main: {
14
- type: 'query',
15
- output: {
16
- encoding: 'application/json',
17
- schema: {
18
- type: 'object',
19
- required: ['expectedValue'],
20
- properties: {
21
- expectedValue: { type: 'string' },
22
- },
23
- },
24
- },
25
- },
26
- },
27
- },
28
- ]
29
-
30
- const LEXICONS: LexiconDoc[] = [
31
- {
32
- lexicon: 1,
33
- id: 'io.example.error',
34
- defs: {
35
- main: {
36
- type: 'query',
37
- parameters: {
38
- type: 'params',
39
- properties: {
40
- which: { type: 'string', default: 'foo' },
41
- },
42
- },
43
- errors: [{ name: 'Foo' }, { name: 'Bar' }],
44
- },
45
- },
46
- },
47
- {
48
- lexicon: 1,
49
- id: 'io.example.throwFalsyValue',
50
- defs: {
51
- main: {
52
- type: 'query',
53
- },
54
- },
55
- },
56
- {
57
- lexicon: 1,
58
- id: 'io.example.query',
59
- defs: {
60
- main: {
61
- type: 'query',
62
- },
63
- },
64
- },
65
- {
66
- lexicon: 1,
67
- id: 'io.example.procedure',
68
- defs: {
69
- main: {
70
- type: 'procedure',
71
- },
72
- },
73
- },
74
- {
75
- lexicon: 1,
76
- id: 'io.example.invalidResponse',
77
- defs: {
78
- main: {
79
- type: 'query',
80
- output: {
81
- encoding: 'application/json',
82
- schema: {
83
- type: 'object',
84
- required: ['expectedValue'],
85
- properties: {
86
- expectedValue: { type: 'string' },
87
- },
88
- },
89
- },
90
- },
91
- },
92
- },
93
- {
94
- lexicon: 1,
95
- id: 'io.example.invalidUpstreamResponse',
96
- defs: {
97
- main: {
98
- type: 'query',
99
- },
100
- },
101
- },
102
- ]
103
-
104
- const MISMATCHED_LEXICONS: LexiconDoc[] = [
105
- {
106
- lexicon: 1,
107
- id: 'io.example.query',
108
- defs: {
109
- main: {
110
- type: 'procedure',
111
- },
112
- },
113
- },
114
- {
115
- lexicon: 1,
116
- id: 'io.example.procedure',
117
- defs: {
118
- main: {
119
- type: 'query',
120
- },
121
- },
122
- },
123
- {
124
- lexicon: 1,
125
- id: 'io.example.doesNotExist',
126
- defs: {
127
- main: {
128
- type: 'query',
129
- },
130
- },
131
- },
132
- ]
133
-
134
- describe('Errors', () => {
135
- let upstreamS: http.Server
136
- const upstreamServer = xrpcServer.createServer(UPSTREAM_LEXICONS, {
137
- validateResponse: false,
138
- }) // disable validateResponse to test client validation
139
- upstreamServer.method('io.example.upstreamInvalidResponse', () => {
140
- return { encoding: 'json', body: { something: 'else' } }
141
- })
142
-
143
- let upstreamClient: XrpcClient
144
-
145
- let s: http.Server
146
- const server = xrpcServer.createServer(LEXICONS, { validateResponse: false }) // disable validateResponse to test client validation
147
- server.method('io.example.error', (ctx) => {
148
- if (ctx.params.which === 'foo') {
149
- throw new xrpcServer.InvalidRequestError('It was this one!', 'Foo')
150
- } else if (ctx.params.which === 'bar') {
151
- return { status: 400, error: 'Bar', message: 'It was that one!' }
152
- } else {
153
- return { status: 400 }
154
- }
155
- })
156
- server.method('io.example.throwFalsyValue', () => {
157
- throw ''
158
- })
159
- server.method('io.example.query', () => {
160
- return undefined
161
- })
162
- // @ts-ignore We're intentionally giving the wrong response! -prf
163
- server.method('io.example.invalidResponse', () => {
164
- return { encoding: 'json', body: { something: 'else' } }
165
- })
166
- server.method('io.example.invalidUpstreamResponse', async () => {
167
- await upstreamClient.call('io.example.upstreamInvalidResponse')
168
- return {
169
- encoding: 'json',
170
- }
171
- })
172
- server.method('io.example.procedure', () => {
173
- return undefined
174
- })
175
-
176
- let client: XrpcClient
177
- let badClient: XrpcClient
178
- beforeAll(async () => {
179
- upstreamS = await createServer(upstreamServer)
180
- const { port: upstreamPort } = upstreamS.address() as AddressInfo
181
- upstreamClient = new XrpcClient(
182
- `http://localhost:${upstreamPort}`,
183
- UPSTREAM_LEXICONS,
184
- )
185
-
186
- s = await createServer(server)
187
- const { port } = s.address() as AddressInfo
188
- client = new XrpcClient(`http://localhost:${port}`, LEXICONS)
189
- badClient = new XrpcClient(`http://localhost:${port}`, MISMATCHED_LEXICONS)
190
- })
191
- afterAll(async () => {
192
- await closeServer(s)
193
- await closeServer(upstreamS)
194
- })
195
-
196
- it('serves requests', async () => {
197
- try {
198
- await client.call('io.example.error', {
199
- which: 'foo',
200
- })
201
- throw new Error('Didnt throw')
202
- } catch (e) {
203
- expect(e).toBeInstanceOf(XRPCError)
204
- expect((e as XRPCError).success).toBeFalsy()
205
- expect((e as XRPCError).error).toBe('Foo')
206
- expect((e as XRPCError).message).toBe('It was this one!')
207
- }
208
- try {
209
- await client.call('io.example.error', {
210
- which: 'bar',
211
- })
212
- throw new Error('Didnt throw')
213
- } catch (e) {
214
- expect(e).toBeInstanceOf(XRPCError)
215
- expect((e as XRPCError).success).toBeFalsy()
216
- expect((e as XRPCError).error).toBe('Bar')
217
- expect((e as XRPCError).message).toBe('It was that one!')
218
- }
219
- try {
220
- await client.call('io.example.throwFalsyValue')
221
- throw new Error('Didnt throw')
222
- } catch (e) {
223
- expect(e).toBeInstanceOf(XRPCError)
224
- expect((e as XRPCError).success).toBeFalsy()
225
- expect((e as XRPCError).error).toBe('InternalServerError')
226
- expect((e as XRPCError).message).toBe('Internal Server Error')
227
- }
228
- try {
229
- await client.call('io.example.error', {
230
- which: 'other',
231
- })
232
- throw new Error('Didnt throw')
233
- } catch (e) {
234
- expect(e).toBeInstanceOf(XRPCError)
235
- expect((e as XRPCError).success).toBeFalsy()
236
- expect((e as XRPCError).error).toBe('InvalidRequest')
237
- expect((e as XRPCError).message).toBe('Invalid Request')
238
- }
239
- try {
240
- await client.call('io.example.invalidResponse')
241
- throw new Error('Didnt throw')
242
- } catch (e: any) {
243
- expect(e).toBeInstanceOf(XRPCError)
244
- expect(e).toBeInstanceOf(XRPCInvalidResponseError)
245
- expect(e.success).toBeFalsy()
246
- expect(e.error).toBe('Invalid Response')
247
- expect(e.message).toBe(
248
- 'The server gave an invalid response and may be out of date.',
249
- )
250
- const err = e as XRPCInvalidResponseError
251
- expect(err.validationError.message).toBe(
252
- 'Output must have the property "expectedValue"',
253
- )
254
- expect(err.responseBody).toStrictEqual({ something: 'else' })
255
- }
256
- try {
257
- await client.call('io.example.invalidUpstreamResponse')
258
- throw new Error('Didnt throw')
259
- } catch (e: any) {
260
- expect(e).toBeInstanceOf(XRPCError)
261
- expect((e as XRPCError).status).toBe(500)
262
- expect((e as XRPCError).success).toBeFalsy()
263
- expect((e as XRPCError).error).toBe('InternalServerError')
264
- expect((e as XRPCError).message).toBe('Internal Server Error')
265
- }
266
- })
267
-
268
- it('serves error for missing/mismatch schemas', async () => {
269
- await client.call('io.example.query') // No error
270
- await client.call('io.example.procedure') // No error
271
- try {
272
- await badClient.call('io.example.query')
273
- throw new Error('Didnt throw')
274
- } catch (e: any) {
275
- expect(e).toBeInstanceOf(XRPCError)
276
- expect(e.success).toBeFalsy()
277
- expect(e.error).toBe('InvalidRequest')
278
- expect(e.message).toBe('Incorrect HTTP method (POST) expected GET')
279
- }
280
- try {
281
- await badClient.call('io.example.procedure')
282
- throw new Error('Didnt throw')
283
- } catch (e: any) {
284
- expect(e).toBeInstanceOf(XRPCError)
285
- expect(e.success).toBeFalsy()
286
- expect(e.error).toBe('InvalidRequest')
287
- expect(e.message).toBe('Incorrect HTTP method (GET) expected POST')
288
- }
289
- try {
290
- await badClient.call('io.example.doesNotExist')
291
- throw new Error('Didnt throw')
292
- } catch (e: any) {
293
- expect(e).toBeInstanceOf(XRPCError)
294
- expect(e.success).toBeFalsy()
295
- expect(e.error).toBe('MethodNotImplemented')
296
- expect(e.message).toBe('Method Not Implemented')
297
- }
298
- })
299
- })
@@ -1,135 +0,0 @@
1
- import { encode } from '@atproto/lex-cbor'
2
- import { ui8Equals } from '@atproto/lex-data'
3
- import { ErrorFrame, Frame, FrameType, MessageFrame } from '../src/index.js'
4
-
5
- describe('Frames', () => {
6
- it('creates and parses message frame.', async () => {
7
- const messageFrame = new MessageFrame(
8
- { a: 'b', c: [1, 2, 3] },
9
- { type: '#d' },
10
- )
11
-
12
- expect(messageFrame.header).toEqual({
13
- op: FrameType.Message,
14
- t: '#d',
15
- })
16
- expect(messageFrame.op).toEqual(FrameType.Message)
17
- expect(messageFrame.type).toEqual('#d')
18
- expect(messageFrame.body).toEqual({ a: 'b', c: [1, 2, 3] })
19
-
20
- const bytes = messageFrame.toBytes()
21
- expect(
22
- ui8Equals(
23
- bytes,
24
- new Uint8Array([
25
- /*header*/ 162, 97, 116, 98, 35, 100, 98, 111, 112, 1, /*body*/ 162,
26
- 97, 97, 97, 98, 97, 99, 131, 1, 2, 3,
27
- ]),
28
- ),
29
- ).toEqual(true)
30
-
31
- const parsedFrame = Frame.fromBytes(bytes)
32
- if (!(parsedFrame instanceof MessageFrame)) {
33
- throw new Error('Did not parse as message frame')
34
- }
35
-
36
- expect(parsedFrame.header).toEqual(messageFrame.header)
37
- expect(parsedFrame.op).toEqual(messageFrame.op)
38
- expect(parsedFrame.type).toEqual(messageFrame.type)
39
- expect(parsedFrame.body).toEqual(messageFrame.body)
40
- })
41
-
42
- it('creates and parses error frame.', async () => {
43
- const errorFrame = new ErrorFrame({
44
- error: 'BigOops',
45
- message: 'Something went awry',
46
- })
47
-
48
- expect(errorFrame.header).toEqual({ op: FrameType.Error })
49
- expect(errorFrame.op).toEqual(FrameType.Error)
50
- expect(errorFrame.code).toEqual('BigOops')
51
- expect(errorFrame.message).toEqual('Something went awry')
52
- expect(errorFrame.body).toEqual({
53
- error: 'BigOops',
54
- message: 'Something went awry',
55
- })
56
-
57
- const bytes = errorFrame.toBytes()
58
- expect(
59
- ui8Equals(
60
- bytes,
61
- new Uint8Array([
62
- /*header*/ 161, 98, 111, 112, 32, /*body*/ 162, 101, 101, 114, 114,
63
- 111, 114, 103, 66, 105, 103, 79, 111, 112, 115, 103, 109, 101, 115,
64
- 115, 97, 103, 101, 115, 83, 111, 109, 101, 116, 104, 105, 110, 103,
65
- 32, 119, 101, 110, 116, 32, 97, 119, 114, 121,
66
- ]),
67
- ),
68
- ).toEqual(true)
69
-
70
- const parsedFrame = Frame.fromBytes(bytes)
71
- if (!(parsedFrame instanceof ErrorFrame)) {
72
- throw new Error('Did not parse as error frame')
73
- }
74
-
75
- expect(parsedFrame.header).toEqual(errorFrame.header)
76
- expect(parsedFrame.op).toEqual(errorFrame.op)
77
- expect(parsedFrame.code).toEqual(errorFrame.code)
78
- expect(parsedFrame.message).toEqual(errorFrame.message)
79
- expect(parsedFrame.body).toEqual(errorFrame.body)
80
- })
81
-
82
- it('parsing fails when frame is not CBOR.', async () => {
83
- const bytes = Buffer.from('some utf8 bytes')
84
- const emptyBytes = Buffer.alloc(0)
85
- expect(() => Frame.fromBytes(bytes)).toThrow()
86
- expect(() => Frame.fromBytes(emptyBytes)).toThrow()
87
- })
88
-
89
- it('parsing fails when frame header is malformed.', async () => {
90
- const bytes = Buffer.concat([
91
- encode({ op: -2 }), // Unknown op
92
- encode({ a: 'b', c: [1, 2, 3] }),
93
- ])
94
-
95
- expect(() => Frame.fromBytes(bytes)).toThrow('Invalid frame header:')
96
- })
97
-
98
- it('parsing fails when frame is missing body.', async () => {
99
- const messageFrame = new MessageFrame(
100
- { a: 'b', c: [1, 2, 3] },
101
- { type: '#d' },
102
- )
103
-
104
- const headerBytes = encode(messageFrame.header)
105
-
106
- expect(() => Frame.fromBytes(headerBytes)).toThrow('Missing frame body')
107
- })
108
-
109
- it('parsing fails when frame has too many data items.', async () => {
110
- const messageFrame = new MessageFrame(
111
- { a: 'b', c: [1, 2, 3] },
112
- { type: '#d' },
113
- )
114
-
115
- const bytes = Buffer.concat([
116
- messageFrame.toBytes(),
117
- encode({ d: 'e', f: [4, 5, 6] }),
118
- ])
119
-
120
- expect(() => Frame.fromBytes(bytes)).toThrow(
121
- 'Too many CBOR data items in frame',
122
- )
123
- })
124
-
125
- it('parsing fails when error frame has invalid body.', async () => {
126
- const errorFrame = new ErrorFrame({ error: 'BadOops' })
127
-
128
- const bytes = Buffer.concat([
129
- encode(errorFrame.header),
130
- encode({ blah: 1 }),
131
- ])
132
-
133
- expect(() => Frame.fromBytes(bytes)).toThrow('Invalid error frame body:')
134
- })
135
- })
@@ -1,97 +0,0 @@
1
- import assert from 'node:assert'
2
- import * as http from 'node:http'
3
- import { AddressInfo } from 'node:net'
4
- import { CID } from 'multiformats/cid'
5
- import { LexiconDoc } from '@atproto/lexicon'
6
- import { XrpcClient } from '@atproto/xrpc'
7
- import * as xrpcServer from '../src/index.js'
8
- import { closeServer, createServer } from './_util.js'
9
-
10
- const LEXICONS: LexiconDoc[] = [
11
- {
12
- lexicon: 1,
13
- id: 'io.example.ipld',
14
- defs: {
15
- main: {
16
- type: 'procedure',
17
- input: {
18
- encoding: 'application/json',
19
- schema: {
20
- type: 'object',
21
- properties: {
22
- cid: {
23
- type: 'cid-link',
24
- },
25
- bytes: {
26
- type: 'bytes',
27
- },
28
- },
29
- },
30
- },
31
- output: {
32
- encoding: 'application/json',
33
- schema: {
34
- type: 'object',
35
- properties: {
36
- cid: {
37
- type: 'cid-link',
38
- },
39
- bytes: {
40
- type: 'bytes',
41
- },
42
- },
43
- },
44
- },
45
- },
46
- },
47
- },
48
- ]
49
-
50
- describe('Ipld vals', () => {
51
- let s: http.Server
52
- const server = xrpcServer.createServer(LEXICONS)
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
- })
67
-
68
- let client: XrpcClient
69
- beforeAll(async () => {
70
- s = await createServer(server)
71
- const { port } = s.address() as AddressInfo
72
- client = new XrpcClient(`http://localhost:${port}`, LEXICONS)
73
- })
74
- afterAll(async () => {
75
- await closeServer(s)
76
- })
77
-
78
- it('can send and receive ipld vals', async () => {
79
- const cid = CID.parse(
80
- 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
81
- )
82
- const bytes = new Uint8Array([0, 1, 2, 3])
83
- const res = await client.call(
84
- 'io.example.ipld',
85
- {},
86
- {
87
- cid,
88
- bytes,
89
- },
90
- { encoding: 'application/json' },
91
- )
92
- expect(res.success).toBeTruthy()
93
- expect(res.headers['content-type']).toBe('application/json; charset=utf-8')
94
- expect(cid.equals(res.data.cid)).toBeTruthy()
95
- expect(bytes).toEqual(res.data.bytes)
96
- })
97
- })