@atproto/xrpc-server 0.6.0 → 0.6.1

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @atproto/xrpc-server
2
2
 
3
+ ## 0.6.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`2bdf75d7a`](https://github.com/bluesky-social/atproto/commit/2bdf75d7a63924c10e7a311f16cb447d595b933e), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd), [`b934b396b`](https://github.com/bluesky-social/atproto/commit/b934b396b13ba32bf2bf7e75ecdf6871e5f310dd)]:
8
+ - @atproto/lexicon@0.4.1
9
+ - @atproto/xrpc@0.6.0
10
+
3
11
  ## 0.6.0
4
12
 
5
13
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/xrpc-server",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "license": "MIT",
5
5
  "description": "atproto HTTP API (XRPC) server library",
6
6
  "keywords": [
@@ -26,8 +26,8 @@
26
26
  "zod": "^3.23.8",
27
27
  "@atproto/common": "^0.4.1",
28
28
  "@atproto/crypto": "^0.4.0",
29
- "@atproto/lexicon": "^0.4.0",
30
- "@atproto/xrpc": "^0.5.0"
29
+ "@atproto/lexicon": "^0.4.1",
30
+ "@atproto/xrpc": "^0.6.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/express": "^4.17.13",
@@ -7,7 +7,7 @@ import * as ui8 from 'uint8arrays'
7
7
  import { MINUTE } from '@atproto/common'
8
8
  import { Secp256k1Keypair } from '@atproto/crypto'
9
9
  import { LexiconDoc } from '@atproto/lexicon'
10
- import xrpc, { ServiceClient, XRPCError } from '@atproto/xrpc'
10
+ import { XrpcClient, XRPCError } from '@atproto/xrpc'
11
11
  import * as xrpcServer from '../src'
12
12
  import {
13
13
  createServer,
@@ -62,13 +62,12 @@ describe('Auth', () => {
62
62
  }
63
63
  },
64
64
  })
65
- xrpc.addLexicons(LEXICONS)
66
65
 
67
- let client: ServiceClient
66
+ let client: XrpcClient
68
67
  beforeAll(async () => {
69
68
  const port = await getPort()
70
69
  s = await createServer(port, server)
71
- client = xrpc.service(`http://localhost:${port}`)
70
+ client = new XrpcClient(`http://localhost:${port}`, LEXICONS)
72
71
  })
73
72
 
74
73
  afterAll(async () => {
@@ -3,8 +3,8 @@ import { Readable } from 'stream'
3
3
  import { gzipSync } from 'zlib'
4
4
  import getPort from 'get-port'
5
5
  import { LexiconDoc } from '@atproto/lexicon'
6
- import xrpc, { ServiceClient } from '@atproto/xrpc'
7
- import { bytesToStream, cidForCbor } from '@atproto/common'
6
+ import { XrpcClient } from '@atproto/xrpc'
7
+ import { cidForCbor } from '@atproto/common'
8
8
  import { randomBytes } from '@atproto/crypto'
9
9
  import { createServer, closeServer } from './_util'
10
10
  import * as xrpcServer from '../src'
@@ -131,15 +131,14 @@ describe('Bodies', () => {
131
131
  }
132
132
  },
133
133
  )
134
- xrpc.addLexicons(LEXICONS)
135
134
 
136
- let client: ServiceClient
135
+ let client: XrpcClient
137
136
  let url: string
138
137
  beforeAll(async () => {
139
138
  const port = await getPort()
140
139
  s = await createServer(port, server)
141
140
  url = `http://localhost:${port}`
142
- client = xrpc.service(url)
141
+ client = new XrpcClient(url, LEXICONS)
143
142
  })
144
143
  afterAll(async () => {
145
144
  await closeServer(s)
@@ -174,7 +173,65 @@ describe('Bodies', () => {
174
173
  { foo: 'hello', bar: 123 },
175
174
  { encoding: 'image/jpeg' },
176
175
  ),
176
+ ).rejects.toThrow(`Unable to encode object as image/jpeg data`)
177
+ await expect(
178
+ client.call(
179
+ 'io.example.validationTest',
180
+ {},
181
+ // Does not need to be a valid jpeg
182
+ new Blob([randomBytes(123)], { type: 'image/jpeg' }),
183
+ ),
177
184
  ).rejects.toThrow(`Wrong request encoding (Content-Type): image/jpeg`)
185
+ await expect(
186
+ client.call(
187
+ 'io.example.validationTest',
188
+ {},
189
+ (() => {
190
+ const formData = new FormData()
191
+ formData.append('foo', 'bar')
192
+ return formData
193
+ })(),
194
+ ),
195
+ ).rejects.toThrow(
196
+ `Wrong request encoding (Content-Type): multipart/form-data`,
197
+ )
198
+ await expect(
199
+ client.call(
200
+ 'io.example.validationTest',
201
+ {},
202
+ new URLSearchParams([['foo', 'bar']]),
203
+ ),
204
+ ).rejects.toThrow(
205
+ `Wrong request encoding (Content-Type): application/x-www-form-urlencoded`,
206
+ )
207
+ await expect(
208
+ client.call(
209
+ 'io.example.validationTest',
210
+ {},
211
+ new Blob([new Uint8Array([1])]),
212
+ ),
213
+ ).rejects.toThrow(
214
+ `Wrong request encoding (Content-Type): application/octet-stream`,
215
+ )
216
+ await expect(
217
+ client.call(
218
+ 'io.example.validationTest',
219
+ {},
220
+ new ReadableStream({
221
+ pull(ctrl) {
222
+ ctrl.enqueue(new Uint8Array([1]))
223
+ ctrl.close()
224
+ },
225
+ }),
226
+ ),
227
+ ).rejects.toThrow(
228
+ `Wrong request encoding (Content-Type): application/octet-stream`,
229
+ )
230
+ await expect(
231
+ client.call('io.example.validationTest', {}, new Uint8Array([1])),
232
+ ).rejects.toThrow(
233
+ `Wrong request encoding (Content-Type): application/octet-stream`,
234
+ )
178
235
 
179
236
  // 500 responses don't include details, so we nab details from the logger.
180
237
  let error: string | undefined
@@ -201,6 +258,86 @@ describe('Bodies', () => {
201
258
  expect(bytesResponse.data.cid).toEqual(expectedCid.toString())
202
259
  })
203
260
 
261
+ it('supports empty payload on procedues with encoding', async () => {
262
+ const bytes = new Uint8Array(0)
263
+ const expectedCid = await cidForCbor(bytes)
264
+ const bytesResponse = await client.call('io.example.blobTest', {}, bytes)
265
+ expect(bytesResponse.data.cid).toEqual(expectedCid.toString())
266
+ })
267
+
268
+ it('supports upload of empty txt file', async () => {
269
+ const txtFile = new Blob([], { type: 'text/plain' })
270
+ const expectedCid = await cidForCbor(await txtFile.arrayBuffer())
271
+ const fileResponse = await client.call('io.example.blobTest', {}, txtFile)
272
+ expect(fileResponse.data.cid).toEqual(expectedCid.toString())
273
+ })
274
+
275
+ // This does not work because the xrpc-server will add a json middleware
276
+ // regardless of the "input" definition. This is probably a behavior that
277
+ // should be fixed in the xrpc-server.
278
+ it.skip('supports upload of json data', async () => {
279
+ const jsonFile = new Blob([Buffer.from(`{"foo":"bar","baz":[3, null]}`)], {
280
+ type: 'application/json',
281
+ })
282
+ const expectedCid = await cidForCbor(await jsonFile.arrayBuffer())
283
+ const fileResponse = await client.call('io.example.blobTest', {}, jsonFile)
284
+ expect(fileResponse.data.cid).toEqual(expectedCid.toString())
285
+ })
286
+
287
+ it('supports ArrayBufferView', async () => {
288
+ const bytes = randomBytes(1024)
289
+ const expectedCid = await cidForCbor(bytes)
290
+
291
+ const bufferResponse = await client.call(
292
+ 'io.example.blobTest',
293
+ {},
294
+ Buffer.from(bytes),
295
+ )
296
+ expect(bufferResponse.data.cid).toEqual(expectedCid.toString())
297
+ })
298
+
299
+ it('supports Blob', async () => {
300
+ const bytes = randomBytes(1024)
301
+ const expectedCid = await cidForCbor(bytes)
302
+
303
+ const blobResponse = await client.call(
304
+ 'io.example.blobTest',
305
+ {},
306
+ new Blob([bytes], { type: 'application/octet-stream' }),
307
+ )
308
+ expect(blobResponse.data.cid).toEqual(expectedCid.toString())
309
+ })
310
+
311
+ it('supports Blob without explicit type', async () => {
312
+ const bytes = randomBytes(1024)
313
+ const expectedCid = await cidForCbor(bytes)
314
+
315
+ const blobResponse = await client.call(
316
+ 'io.example.blobTest',
317
+ {},
318
+ new Blob([bytes]),
319
+ )
320
+ expect(blobResponse.data.cid).toEqual(expectedCid.toString())
321
+ })
322
+
323
+ it('supports ReadableStream', async () => {
324
+ const bytes = randomBytes(1024)
325
+ const expectedCid = await cidForCbor(bytes)
326
+
327
+ const streamResponse = await client.call(
328
+ 'io.example.blobTest',
329
+ {},
330
+ // ReadableStream.from not available in node < 20
331
+ new ReadableStream({
332
+ pull(ctrl) {
333
+ ctrl.enqueue(bytes)
334
+ ctrl.close()
335
+ },
336
+ }),
337
+ )
338
+ expect(streamResponse.data.cid).toEqual(expectedCid.toString())
339
+ })
340
+
204
341
  it('supports blobs and compression', async () => {
205
342
  const bytes = randomBytes(1024)
206
343
  const expectedCid = await cidForCbor(bytes)
@@ -230,10 +367,11 @@ describe('Bodies', () => {
230
367
  })
231
368
 
232
369
  it('supports empty payload', async () => {
233
- const expectedCid = await cidForCbor(new Uint8Array(0))
370
+ const bytes = new Uint8Array(0)
371
+ const expectedCid = await cidForCbor(bytes)
234
372
 
235
373
  // Using "undefined" as body to avoid encoding as lexicon { $bytes: "<base64>" }
236
- const result = await client.call('io.example.blobTest', {}, undefined, {
374
+ const result = await client.call('io.example.blobTest', {}, bytes, {
237
375
  encoding: 'text/plain',
238
376
  })
239
377
 
@@ -264,7 +402,7 @@ describe('Bodies', () => {
264
402
  await client.call(
265
403
  'io.example.blobTest',
266
404
  {},
267
- bytesToStream(bytes.slice(0, BLOB_LIMIT)),
405
+ bytesToReadableStream(bytes.slice(0, BLOB_LIMIT)),
268
406
  {
269
407
  encoding: 'application/octet-stream',
270
408
  },
@@ -274,7 +412,7 @@ describe('Bodies', () => {
274
412
  const promise = client.call(
275
413
  'io.example.blobTest',
276
414
  {},
277
- bytesToStream(bytes),
415
+ bytesToReadableStream(bytes),
278
416
  {
279
417
  encoding: 'application/octet-stream',
280
418
  },
@@ -310,3 +448,13 @@ describe('Bodies', () => {
310
448
  })
311
449
  })
312
450
  })
451
+
452
+ const bytesToReadableStream = (bytes: Uint8Array): ReadableStream => {
453
+ // not using ReadableStream.from(), which lacks support in some contexts including nodejs v18.
454
+ return new ReadableStream({
455
+ pull(ctrl) {
456
+ ctrl.enqueue(bytes)
457
+ ctrl.close()
458
+ },
459
+ })
460
+ }
@@ -1,14 +1,9 @@
1
1
  import * as http from 'http'
2
2
  import getPort from 'get-port'
3
3
  import { LexiconDoc } from '@atproto/lexicon'
4
+ import { XRPCError, XRPCInvalidResponseError, XrpcClient } from '@atproto/xrpc'
4
5
  import { createServer, closeServer } from './_util'
5
6
  import * as xrpcServer from '../src'
6
- import xrpc, {
7
- Client,
8
- ServiceClient,
9
- XRPCError,
10
- XRPCInvalidResponseError,
11
- } from '@atproto/xrpc'
12
7
 
13
8
  const LEXICONS: LexiconDoc[] = [
14
9
  {
@@ -130,17 +125,14 @@ describe('Errors', () => {
130
125
  server.method('io.example.procedure', () => {
131
126
  return undefined
132
127
  })
133
- xrpc.addLexicons(LEXICONS)
134
- const badXrpc = new Client()
135
- badXrpc.addLexicons(MISMATCHED_LEXICONS)
136
128
 
137
- let client: ServiceClient
138
- let badClient: ServiceClient
129
+ let client: XrpcClient
130
+ let badClient: XrpcClient
139
131
  beforeAll(async () => {
140
132
  const port = await getPort()
141
133
  s = await createServer(port, server)
142
- client = xrpc.service(`http://localhost:${port}`)
143
- badClient = badXrpc.service(`http://localhost:${port}`)
134
+ client = new XrpcClient(`http://localhost:${port}`, LEXICONS)
135
+ badClient = new XrpcClient(`http://localhost:${port}`, MISMATCHED_LEXICONS)
144
136
  })
145
137
  afterAll(async () => {
146
138
  await closeServer(s)
@@ -1,6 +1,6 @@
1
1
  import * as http from 'http'
2
2
  import { LexiconDoc } from '@atproto/lexicon'
3
- import xrpc, { ServiceClient } from '@atproto/xrpc'
3
+ import { XrpcClient } from '@atproto/xrpc'
4
4
  import { CID } from 'multiformats/cid'
5
5
  import getPort from 'get-port'
6
6
  import { createServer, closeServer } from './_util'
@@ -63,13 +63,12 @@ describe('Ipld vals', () => {
63
63
  return { encoding: 'application/json', body: ctx.input?.body }
64
64
  },
65
65
  )
66
- xrpc.addLexicons(LEXICONS)
67
66
 
68
- let client: ServiceClient
67
+ let client: XrpcClient
69
68
  beforeAll(async () => {
70
69
  const port = await getPort()
71
70
  s = await createServer(port, server)
72
- client = xrpc.service(`http://localhost:${port}`)
71
+ client = new XrpcClient(`http://localhost:${port}`, LEXICONS)
73
72
  })
74
73
  afterAll(async () => {
75
74
  await closeServer(s)
@@ -1,7 +1,7 @@
1
1
  import * as http from 'http'
2
2
  import getPort from 'get-port'
3
3
  import { LexiconDoc } from '@atproto/lexicon'
4
- import xrpc, { ServiceClient } from '@atproto/xrpc'
4
+ import { XrpcClient } from '@atproto/xrpc'
5
5
  import { createServer, closeServer } from './_util'
6
6
  import * as xrpcServer from '../src'
7
7
 
@@ -41,13 +41,12 @@ describe('Parameters', () => {
41
41
  body: ctx.params,
42
42
  }),
43
43
  )
44
- xrpc.addLexicons(LEXICONS)
45
44
 
46
- let client: ServiceClient
45
+ let client: XrpcClient
47
46
  beforeAll(async () => {
48
47
  const port = await getPort()
49
48
  s = await createServer(port, server)
50
- client = xrpc.service(`http://localhost:${port}`)
49
+ client = new XrpcClient(`http://localhost:${port}`, LEXICONS)
51
50
  })
52
51
  afterAll(async () => {
53
52
  await closeServer(s)
@@ -1,7 +1,7 @@
1
1
  import * as http from 'http'
2
2
  import { Readable } from 'stream'
3
3
  import { LexiconDoc } from '@atproto/lexicon'
4
- import xrpc, { ServiceClient } from '@atproto/xrpc'
4
+ import { XrpcClient } from '@atproto/xrpc'
5
5
  import getPort from 'get-port'
6
6
  import { createServer, closeServer } from './_util'
7
7
  import * as xrpcServer from '../src'
@@ -121,13 +121,12 @@ describe('Procedures', () => {
121
121
  }
122
122
  },
123
123
  )
124
- xrpc.addLexicons(LEXICONS)
125
124
 
126
- let client: ServiceClient
125
+ let client: XrpcClient
127
126
  beforeAll(async () => {
128
127
  const port = await getPort()
129
128
  s = await createServer(port, server)
130
- client = xrpc.service(`http://localhost:${port}`)
129
+ client = new XrpcClient(`http://localhost:${port}`, LEXICONS)
131
130
  })
132
131
  afterAll(async () => {
133
132
  await closeServer(s)
@@ -1,7 +1,7 @@
1
1
  import * as http from 'http'
2
2
  import getPort from 'get-port'
3
3
  import { LexiconDoc } from '@atproto/lexicon'
4
- import xrpc, { ServiceClient } from '@atproto/xrpc'
4
+ import { XrpcClient } from '@atproto/xrpc'
5
5
  import { createServer, closeServer } from './_util'
6
6
  import * as xrpcServer from '../src'
7
7
 
@@ -89,13 +89,12 @@ describe('Queries', () => {
89
89
  }
90
90
  },
91
91
  )
92
- xrpc.addLexicons(LEXICONS)
93
92
 
94
- let client: ServiceClient
93
+ let client: XrpcClient
95
94
  beforeAll(async () => {
96
95
  const port = await getPort()
97
96
  s = await createServer(port, server)
98
- client = xrpc.service(`http://localhost:${port}`)
97
+ client = new XrpcClient(`http://localhost:${port}`, LEXICONS)
99
98
  })
100
99
  afterAll(async () => {
101
100
  await closeServer(s)
@@ -1,7 +1,7 @@
1
1
  import * as http from 'http'
2
2
  import getPort from 'get-port'
3
3
  import { LexiconDoc } from '@atproto/lexicon'
4
- import xrpc, { ServiceClient } from '@atproto/xrpc'
4
+ import { XrpcClient } from '@atproto/xrpc'
5
5
  import { createServer, closeServer } from './_util'
6
6
  import * as xrpcServer from '../src'
7
7
  import { RateLimiter } from '../src'
@@ -190,13 +190,11 @@ describe('Parameters', () => {
190
190
  }),
191
191
  })
192
192
 
193
- xrpc.addLexicons(LEXICONS)
194
-
195
- let client: ServiceClient
193
+ let client: XrpcClient
196
194
  beforeAll(async () => {
197
195
  const port = await getPort()
198
196
  s = await createServer(port, server)
199
- client = xrpc.service(`http://localhost:${port}`)
197
+ client = new XrpcClient(`http://localhost:${port}`, LEXICONS)
200
198
  })
201
199
  afterAll(async () => {
202
200
  await closeServer(s)
@@ -1,7 +1,7 @@
1
1
  import * as http from 'http'
2
2
  import getPort from 'get-port'
3
3
  import { LexiconDoc } from '@atproto/lexicon'
4
- import xrpc, { ServiceClient } from '@atproto/xrpc'
4
+ import { XrpcClient } from '@atproto/xrpc'
5
5
  import { byteIterableToStream } from '@atproto/common'
6
6
  import { createServer, closeServer } from './_util'
7
7
  import * as xrpcServer from '../src'
@@ -47,15 +47,12 @@ describe('Responses', () => {
47
47
  }
48
48
  },
49
49
  )
50
- xrpc.addLexicons(LEXICONS)
51
50
 
52
- let client: ServiceClient
53
- let url: string
51
+ let client: XrpcClient
54
52
  beforeAll(async () => {
55
53
  const port = await getPort()
56
54
  s = await createServer(port, server)
57
- url = `http://localhost:${port}`
58
- client = xrpc.service(url)
55
+ client = new XrpcClient(`http://localhost:${port}`, LEXICONS)
59
56
  })
60
57
  afterAll(async () => {
61
58
  await closeServer(s)