@atproto/xrpc-server 0.11.5 → 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.
- package/CHANGELOG.md +20 -0
- package/package.json +26 -21
- package/jest.config.cjs +0 -21
- package/src/auth.ts +0 -235
- package/src/errors.ts +0 -312
- package/src/index.ts +0 -14
- package/src/logger.ts +0 -8
- package/src/rate-limiter-http.ts +0 -82
- package/src/rate-limiter.ts +0 -279
- package/src/server.ts +0 -858
- package/src/stream/frames.ts +0 -125
- package/src/stream/index.ts +0 -5
- package/src/stream/logger.ts +0 -6
- package/src/stream/server.ts +0 -66
- package/src/stream/stream.ts +0 -39
- package/src/stream/subscription.ts +0 -96
- package/src/stream/types.ts +0 -27
- package/src/types.ts +0 -330
- package/src/util.ts +0 -708
- package/tests/_util.ts +0 -124
- package/tests/auth.test.ts +0 -333
- package/tests/bodies.test.ts +0 -608
- package/tests/errors.test.ts +0 -299
- package/tests/frames.test.ts +0 -135
- package/tests/ipld.test.ts +0 -97
- package/tests/parameters.test.ts +0 -331
- package/tests/parsing.test.ts +0 -89
- package/tests/procedures.test.ts +0 -176
- package/tests/queries.test.ts +0 -140
- package/tests/rate-limiter.test.ts +0 -312
- package/tests/responses.test.ts +0 -72
- package/tests/stream.test.ts +0 -169
- package/tests/subscriptions.test.ts +0 -398
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -7
|
@@ -1,398 +0,0 @@
|
|
|
1
|
-
import * as http from 'node:http'
|
|
2
|
-
import { AddressInfo } from 'node:net'
|
|
3
|
-
import { WebSocket, createWebSocketStream } from 'ws'
|
|
4
|
-
import { wait } from '@atproto/common'
|
|
5
|
-
import { LexiconDoc, Lexicons } from '@atproto/lexicon'
|
|
6
|
-
import {
|
|
7
|
-
ErrorFrame,
|
|
8
|
-
Frame,
|
|
9
|
-
MessageFrame,
|
|
10
|
-
Subscription,
|
|
11
|
-
byFrame,
|
|
12
|
-
} from '../src/index.js'
|
|
13
|
-
import * as xrpcServer from '../src/index.js'
|
|
14
|
-
import {
|
|
15
|
-
basicAuthHeaders,
|
|
16
|
-
buildAddLexicons,
|
|
17
|
-
buildMethodLexicons,
|
|
18
|
-
closeServer,
|
|
19
|
-
createBasicAuth,
|
|
20
|
-
createServer,
|
|
21
|
-
} from './_util.js'
|
|
22
|
-
|
|
23
|
-
const LEXICONS = [
|
|
24
|
-
{
|
|
25
|
-
lexicon: 1,
|
|
26
|
-
id: 'io.example.streamOne',
|
|
27
|
-
defs: {
|
|
28
|
-
main: {
|
|
29
|
-
type: 'subscription',
|
|
30
|
-
parameters: {
|
|
31
|
-
type: 'params',
|
|
32
|
-
required: ['countdown'],
|
|
33
|
-
properties: {
|
|
34
|
-
countdown: { type: 'integer' },
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
message: {
|
|
38
|
-
schema: { type: 'union', refs: ['#countdownStatus'] },
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
countdownStatus: {
|
|
42
|
-
type: 'object',
|
|
43
|
-
required: ['count'],
|
|
44
|
-
properties: { count: { type: 'integer' } },
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
lexicon: 1,
|
|
50
|
-
id: 'io.example.streamTwo',
|
|
51
|
-
defs: {
|
|
52
|
-
main: {
|
|
53
|
-
type: 'subscription',
|
|
54
|
-
parameters: {
|
|
55
|
-
type: 'params',
|
|
56
|
-
required: ['countdown'],
|
|
57
|
-
properties: {
|
|
58
|
-
countdown: { type: 'integer' },
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
message: {
|
|
62
|
-
schema: {
|
|
63
|
-
type: 'union',
|
|
64
|
-
refs: ['#even', '#odd'],
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
even: {
|
|
69
|
-
type: 'object',
|
|
70
|
-
required: ['count'],
|
|
71
|
-
properties: { count: { type: 'integer' } },
|
|
72
|
-
},
|
|
73
|
-
odd: {
|
|
74
|
-
type: 'object',
|
|
75
|
-
required: ['count'],
|
|
76
|
-
properties: { count: { type: 'integer' } },
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
lexicon: 1,
|
|
82
|
-
id: 'io.example.streamAuth',
|
|
83
|
-
defs: {
|
|
84
|
-
main: {
|
|
85
|
-
type: 'subscription',
|
|
86
|
-
message: {
|
|
87
|
-
schema: { type: 'union', refs: ['#auth'] },
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
auth: {
|
|
91
|
-
type: 'object',
|
|
92
|
-
properties: {
|
|
93
|
-
credentials: { type: 'ref', ref: '#credentials' },
|
|
94
|
-
artifacts: { type: 'ref', ref: '#artifacts' },
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
credentials: {
|
|
98
|
-
type: 'object',
|
|
99
|
-
required: ['username'],
|
|
100
|
-
properties: {
|
|
101
|
-
username: { type: 'string' },
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
artifacts: {
|
|
105
|
-
type: 'object',
|
|
106
|
-
required: ['original'],
|
|
107
|
-
properties: {
|
|
108
|
-
original: { type: 'string' },
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
] as const satisfies LexiconDoc[]
|
|
114
|
-
|
|
115
|
-
const handlers = {
|
|
116
|
-
'io.example.streamOne': async function* ({
|
|
117
|
-
params,
|
|
118
|
-
}: xrpcServer.StreamContext) {
|
|
119
|
-
const countdown = Number(params.countdown ?? 0)
|
|
120
|
-
for (let i = countdown; i >= 0; i--) {
|
|
121
|
-
await wait(0)
|
|
122
|
-
yield { $type: 'io.example.streamOne#countdownStatus', count: i }
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
'io.example.streamTwo': async function* ({
|
|
126
|
-
params,
|
|
127
|
-
}: xrpcServer.StreamContext) {
|
|
128
|
-
const countdown = Number(params.countdown ?? 0)
|
|
129
|
-
for (let i = countdown; i >= 0; i--) {
|
|
130
|
-
await wait(200)
|
|
131
|
-
yield {
|
|
132
|
-
$type: `io.example.streamTwo${i % 2 === 0 ? '#even' : '#odd'}`,
|
|
133
|
-
count: i,
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
yield {
|
|
137
|
-
$type: 'io.example.otherNsid#done',
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
'io.example.streamAuth': {
|
|
141
|
-
auth: createBasicAuth({ username: 'admin', password: 'password' }),
|
|
142
|
-
handler: async function* ({ auth }) {
|
|
143
|
-
yield { ...auth, $type: 'io.example.streamAuth#auth' }
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
for (const buildServer of [buildMethodLexicons, buildAddLexicons]) {
|
|
149
|
-
describe(buildServer, () => {
|
|
150
|
-
// @NOTE we need to clone because "new Lexicons" will mutate the lexicon
|
|
151
|
-
// definitions
|
|
152
|
-
const lex = new Lexicons(structuredClone(LEXICONS))
|
|
153
|
-
|
|
154
|
-
let server: xrpcServer.Server
|
|
155
|
-
let s: http.Server
|
|
156
|
-
let port: number
|
|
157
|
-
beforeAll(async () => {
|
|
158
|
-
server = await buildServer(LEXICONS, handlers)
|
|
159
|
-
s = await createServer(server)
|
|
160
|
-
port = (s.address() as AddressInfo).port
|
|
161
|
-
})
|
|
162
|
-
afterAll(async () => {
|
|
163
|
-
if (s) await closeServer(s)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('streams messages', async () => {
|
|
167
|
-
const ws = new WebSocket(
|
|
168
|
-
`ws://localhost:${port}/xrpc/io.example.streamOne?countdown=5`,
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
const frames: Frame[] = []
|
|
172
|
-
for await (const frame of byFrame(ws)) {
|
|
173
|
-
frames.push(frame)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
expect(frames).toEqual([
|
|
177
|
-
new MessageFrame({ count: 5 }, { type: '#countdownStatus' }),
|
|
178
|
-
new MessageFrame({ count: 4 }, { type: '#countdownStatus' }),
|
|
179
|
-
new MessageFrame({ count: 3 }, { type: '#countdownStatus' }),
|
|
180
|
-
new MessageFrame({ count: 2 }, { type: '#countdownStatus' }),
|
|
181
|
-
new MessageFrame({ count: 1 }, { type: '#countdownStatus' }),
|
|
182
|
-
new MessageFrame({ count: 0 }, { type: '#countdownStatus' }),
|
|
183
|
-
])
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
it('streams messages in a union', async () => {
|
|
187
|
-
const ws = new WebSocket(
|
|
188
|
-
`ws://localhost:${port}/xrpc/io.example.streamTwo?countdown=5`,
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
const frames: Frame[] = []
|
|
192
|
-
for await (const frame of byFrame(ws)) {
|
|
193
|
-
frames.push(frame)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
expect(frames).toEqual([
|
|
197
|
-
new MessageFrame({ count: 5 }, { type: '#odd' }),
|
|
198
|
-
new MessageFrame({ count: 4 }, { type: '#even' }),
|
|
199
|
-
new MessageFrame({ count: 3 }, { type: '#odd' }),
|
|
200
|
-
new MessageFrame({ count: 2 }, { type: '#even' }),
|
|
201
|
-
new MessageFrame({ count: 1 }, { type: '#odd' }),
|
|
202
|
-
new MessageFrame({ count: 0 }, { type: '#even' }),
|
|
203
|
-
new MessageFrame({}, { type: 'io.example.otherNsid#done' }),
|
|
204
|
-
])
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
it('resolves auth into handler', async () => {
|
|
208
|
-
const ws = new WebSocket(
|
|
209
|
-
`ws://localhost:${port}/xrpc/io.example.streamAuth`,
|
|
210
|
-
{
|
|
211
|
-
headers: basicAuthHeaders({
|
|
212
|
-
username: 'admin',
|
|
213
|
-
password: 'password',
|
|
214
|
-
}),
|
|
215
|
-
},
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
const frames: Frame[] = []
|
|
219
|
-
for await (const frame of byFrame(ws)) {
|
|
220
|
-
frames.push(frame)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
expect(frames).toEqual([
|
|
224
|
-
new MessageFrame(
|
|
225
|
-
{
|
|
226
|
-
credentials: {
|
|
227
|
-
username: 'admin',
|
|
228
|
-
},
|
|
229
|
-
artifacts: {
|
|
230
|
-
original: 'YWRtaW46cGFzc3dvcmQ=',
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
type: '#auth',
|
|
235
|
-
},
|
|
236
|
-
),
|
|
237
|
-
])
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
it('errors immediately on bad parameter', async () => {
|
|
241
|
-
const ws = new WebSocket(
|
|
242
|
-
`ws://localhost:${port}/xrpc/io.example.streamOne`,
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
const frames: Frame[] = []
|
|
246
|
-
for await (const frame of byFrame(ws)) {
|
|
247
|
-
frames.push(frame)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
expect(frames).toEqual([
|
|
251
|
-
expect.objectContaining({
|
|
252
|
-
body: expect.objectContaining({
|
|
253
|
-
error: 'InvalidRequest',
|
|
254
|
-
message: expect.stringContaining('countdown'),
|
|
255
|
-
}),
|
|
256
|
-
}),
|
|
257
|
-
])
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
it('errors immediately on bad auth', async () => {
|
|
261
|
-
const ws = new WebSocket(
|
|
262
|
-
`ws://localhost:${port}/xrpc/io.example.streamAuth`,
|
|
263
|
-
{
|
|
264
|
-
headers: basicAuthHeaders({
|
|
265
|
-
username: 'bad',
|
|
266
|
-
password: 'wrong',
|
|
267
|
-
}),
|
|
268
|
-
},
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
const frames: Frame[] = []
|
|
272
|
-
for await (const frame of byFrame(ws)) {
|
|
273
|
-
frames.push(frame)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
expect(frames).toEqual([
|
|
277
|
-
new ErrorFrame({
|
|
278
|
-
error: 'AuthenticationRequired',
|
|
279
|
-
message: 'Authentication Required',
|
|
280
|
-
}),
|
|
281
|
-
])
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
it('does not websocket upgrade at bad endpoint', async () => {
|
|
285
|
-
const ws = new WebSocket(`ws://localhost:${port}/xrpc/does.not.exist`)
|
|
286
|
-
const drainStream = async () => {
|
|
287
|
-
for await (const _bytes of createWebSocketStream(ws)) {
|
|
288
|
-
// drain
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
await expect(drainStream).rejects.toHaveProperty('code', 'ECONNRESET')
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
describe('Subscription consumer', () => {
|
|
295
|
-
it('receives messages w/ skips', async () => {
|
|
296
|
-
const sub = new Subscription({
|
|
297
|
-
service: `ws://localhost:${port}`,
|
|
298
|
-
method: 'io.example.streamOne',
|
|
299
|
-
getParams: () => ({ countdown: 5 }),
|
|
300
|
-
validate: (obj) => {
|
|
301
|
-
const result = lex.assertValidXrpcMessage<{ count: number }>(
|
|
302
|
-
'io.example.streamOne',
|
|
303
|
-
obj,
|
|
304
|
-
)
|
|
305
|
-
if (!result.count || result.count % 2) {
|
|
306
|
-
return result
|
|
307
|
-
}
|
|
308
|
-
},
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
const messages: { count: number }[] = []
|
|
312
|
-
for await (const msg of sub) {
|
|
313
|
-
messages.push(msg)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
expect(messages).toEqual([
|
|
317
|
-
{ $type: 'io.example.streamOne#countdownStatus', count: 5 },
|
|
318
|
-
{ $type: 'io.example.streamOne#countdownStatus', count: 3 },
|
|
319
|
-
{ $type: 'io.example.streamOne#countdownStatus', count: 1 },
|
|
320
|
-
{ $type: 'io.example.streamOne#countdownStatus', count: 0 },
|
|
321
|
-
])
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
it('reconnects w/ param update', async () => {
|
|
325
|
-
let countdown = 10
|
|
326
|
-
let reconnects = 0
|
|
327
|
-
const sub = new Subscription({
|
|
328
|
-
service: `ws://localhost:${port}`,
|
|
329
|
-
method: 'io.example.streamOne',
|
|
330
|
-
onReconnectError: () => reconnects++,
|
|
331
|
-
getParams: () => ({ countdown }),
|
|
332
|
-
validate: (obj) => {
|
|
333
|
-
return lex.assertValidXrpcMessage<{ count: number }>(
|
|
334
|
-
'io.example.streamOne',
|
|
335
|
-
obj,
|
|
336
|
-
)
|
|
337
|
-
},
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
let disconnected = false
|
|
341
|
-
for await (const msg of sub) {
|
|
342
|
-
expect(msg.count).toBeGreaterThanOrEqual(countdown - 1) // No skips
|
|
343
|
-
countdown = Math.min(countdown, msg.count) // Only allow forward movement
|
|
344
|
-
if (msg.count <= 6 && !disconnected) {
|
|
345
|
-
disconnected = true
|
|
346
|
-
server.subscriptions.forEach(({ wss }) => {
|
|
347
|
-
wss.clients.forEach((c) => c.terminate())
|
|
348
|
-
})
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
expect(countdown).toEqual(0)
|
|
353
|
-
expect(reconnects).toBeGreaterThan(0)
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
it('aborts with signal', async () => {
|
|
357
|
-
const abortController = new AbortController()
|
|
358
|
-
const sub = new Subscription({
|
|
359
|
-
service: `ws://localhost:${port}`,
|
|
360
|
-
method: 'io.example.streamOne',
|
|
361
|
-
signal: abortController.signal,
|
|
362
|
-
getParams: () => ({ countdown: 10 }),
|
|
363
|
-
validate: (obj) => {
|
|
364
|
-
const result = lex.assertValidXrpcMessage<{ count: number }>(
|
|
365
|
-
'io.example.streamOne',
|
|
366
|
-
obj,
|
|
367
|
-
)
|
|
368
|
-
return result
|
|
369
|
-
},
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
let error
|
|
373
|
-
let disconnected = false
|
|
374
|
-
const messages: { count: number }[] = []
|
|
375
|
-
try {
|
|
376
|
-
for await (const msg of sub) {
|
|
377
|
-
messages.push(msg)
|
|
378
|
-
if (msg.count <= 6 && !disconnected) {
|
|
379
|
-
disconnected = true
|
|
380
|
-
abortController.abort(new Error('Oops!'))
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
} catch (err) {
|
|
384
|
-
error = err
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
expect(error).toEqual(new Error('Oops!'))
|
|
388
|
-
expect(messages).toEqual([
|
|
389
|
-
{ $type: 'io.example.streamOne#countdownStatus', count: 10 },
|
|
390
|
-
{ $type: 'io.example.streamOne#countdownStatus', count: 9 },
|
|
391
|
-
{ $type: 'io.example.streamOne#countdownStatus', count: 8 },
|
|
392
|
-
{ $type: 'io.example.streamOne#countdownStatus', count: 7 },
|
|
393
|
-
{ $type: 'io.example.streamOne#countdownStatus', count: 6 },
|
|
394
|
-
])
|
|
395
|
-
})
|
|
396
|
-
})
|
|
397
|
-
})
|
|
398
|
-
}
|
package/tsconfig.build.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":"7.0.0-dev.20260614.1","root":["./src/auth.ts","./src/errors.ts","./src/index.ts","./src/logger.ts","./src/rate-limiter-http.ts","./src/rate-limiter.ts","./src/server.ts","./src/types.ts","./src/util.ts","./src/stream/frames.ts","./src/stream/index.ts","./src/stream/logger.ts","./src/stream/server.ts","./src/stream/stream.ts","./src/stream/subscription.ts","./src/stream/types.ts"]}
|
package/tsconfig.json
DELETED