@atproto/xrpc-server 0.10.14 → 0.10.15
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 +15 -0
- package/dist/auth.d.ts +3 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +18 -0
- package/dist/auth.js.map +1 -1
- package/dist/errors.d.ts +7 -14
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +19 -6
- package/dist/errors.js.map +1 -1
- package/dist/server.d.ts +27 -11
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +115 -78
- package/dist/server.js.map +1 -1
- package/dist/stream/frames.d.ts +5 -1
- package/dist/stream/frames.d.ts.map +1 -1
- package/dist/stream/frames.js +32 -5
- package/dist/stream/frames.js.map +1 -1
- package/dist/stream/types.d.ts +18 -44
- package/dist/stream/types.d.ts.map +1 -1
- package/dist/stream/types.js +10 -10
- package/dist/stream/types.js.map +1 -1
- package/dist/types.d.ts +47 -70
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +28 -15
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +18 -9
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +180 -37
- package/dist/util.js.map +1 -1
- package/package.json +12 -8
- package/src/auth.ts +28 -2
- package/src/errors.ts +23 -7
- package/src/server.ts +307 -111
- package/src/stream/frames.ts +39 -6
- package/src/stream/types.ts +14 -14
- package/src/types.ts +106 -25
- package/src/util.ts +272 -60
- package/tests/_util.ts +62 -5
- package/tests/bodies.test.ts +442 -387
- package/tests/procedures.test.ts +71 -52
- package/tests/queries.test.ts +56 -39
- package/tests/subscriptions.test.ts +234 -221
package/src/server.ts
CHANGED
|
@@ -6,12 +6,11 @@ import express, {
|
|
|
6
6
|
Application,
|
|
7
7
|
ErrorRequestHandler,
|
|
8
8
|
Express,
|
|
9
|
-
Request,
|
|
10
9
|
RequestHandler,
|
|
11
10
|
Router,
|
|
12
11
|
} from 'express'
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { LexValue } from '@atproto/lex-data'
|
|
13
|
+
import { l } from '@atproto/lex-schema'
|
|
15
14
|
import {
|
|
16
15
|
LexXrpcProcedure,
|
|
17
16
|
LexXrpcQuery,
|
|
@@ -26,7 +25,6 @@ import {
|
|
|
26
25
|
MethodNotImplementedError,
|
|
27
26
|
XRPCError,
|
|
28
27
|
excludeErrorResult,
|
|
29
|
-
isErrorResult,
|
|
30
28
|
} from './errors'
|
|
31
29
|
import log, { LOGGER_NAME } from './logger'
|
|
32
30
|
import {
|
|
@@ -44,28 +42,46 @@ import {
|
|
|
44
42
|
AuthVerifier,
|
|
45
43
|
CatchallHandler,
|
|
46
44
|
HandlerContext,
|
|
47
|
-
HandlerSuccess,
|
|
48
45
|
Input,
|
|
46
|
+
LexMethodConfig,
|
|
47
|
+
LexMethodHandler,
|
|
48
|
+
LexMethodInput,
|
|
49
|
+
LexMethodOutput,
|
|
50
|
+
LexMethodParams,
|
|
51
|
+
LexSubscriptionConfig,
|
|
52
|
+
LexSubscriptionHandler,
|
|
53
|
+
MethodAuthContext,
|
|
49
54
|
MethodConfig,
|
|
50
55
|
MethodConfigOrHandler,
|
|
56
|
+
MethodHandler,
|
|
51
57
|
Options,
|
|
58
|
+
Output,
|
|
52
59
|
Params,
|
|
53
60
|
RouteOptions,
|
|
54
61
|
ServerRateLimitDescription,
|
|
62
|
+
StreamAuthContext,
|
|
55
63
|
StreamConfig,
|
|
56
64
|
StreamConfigOrHandler,
|
|
65
|
+
StreamContext,
|
|
57
66
|
isHandlerPipeThroughBuffer,
|
|
58
67
|
isHandlerPipeThroughStream,
|
|
68
|
+
isHandlerSuccess,
|
|
59
69
|
isSharedRateLimitOpts,
|
|
60
70
|
} from './types'
|
|
61
71
|
import {
|
|
72
|
+
AuthVerifierInternal,
|
|
73
|
+
InputVerifierInternal,
|
|
74
|
+
OutputVerifierInternal,
|
|
75
|
+
ParamsVerifierInternal,
|
|
62
76
|
asArray,
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
createLexiconInputVerifier,
|
|
78
|
+
createLexiconOutputVerifier,
|
|
79
|
+
createLexiconParamsVerifier,
|
|
80
|
+
createSchemaInputVerifier,
|
|
81
|
+
createSchemaOutputVerifier,
|
|
82
|
+
createSchemaParamsVerifier,
|
|
65
83
|
extractUrlNsid,
|
|
66
|
-
getQueryParams,
|
|
67
84
|
setHeaders,
|
|
68
|
-
validateOutput,
|
|
69
85
|
} from './util'
|
|
70
86
|
|
|
71
87
|
export function createServer(lexicons?: LexiconDoc[], options?: Options) {
|
|
@@ -114,13 +130,145 @@ export class Server {
|
|
|
114
130
|
}
|
|
115
131
|
}
|
|
116
132
|
|
|
133
|
+
listen(port: number, callback?: () => void) {
|
|
134
|
+
return this.router.listen(port, callback)
|
|
135
|
+
}
|
|
136
|
+
|
|
117
137
|
// handlers
|
|
118
138
|
// =
|
|
119
139
|
|
|
140
|
+
// Routes with auth
|
|
141
|
+
add<M extends l.Procedure | l.Query | l.Subscription, A extends AuthResult>(
|
|
142
|
+
ns: l.Main<M>,
|
|
143
|
+
config: M extends l.Procedure | l.Query
|
|
144
|
+
? LexMethodConfig<M, A> & { auth: Exclude<unknown, void> }
|
|
145
|
+
: M extends l.Subscription
|
|
146
|
+
? LexSubscriptionConfig<M, A> & { auth: Exclude<unknown, void> }
|
|
147
|
+
: never,
|
|
148
|
+
): void
|
|
149
|
+
// Routes without auth
|
|
150
|
+
add<M extends l.Procedure | l.Query | l.Subscription>(
|
|
151
|
+
ns: l.Main<M>,
|
|
152
|
+
config: M extends l.Procedure | l.Query
|
|
153
|
+
? LexMethodConfig<M, void> | LexMethodHandler<M, void>
|
|
154
|
+
: M extends l.Subscription
|
|
155
|
+
? LexSubscriptionConfig<M, void> | LexSubscriptionHandler<M, void>
|
|
156
|
+
: never,
|
|
157
|
+
): void
|
|
158
|
+
add<M extends l.Procedure | l.Query | l.Subscription, A extends Auth>(
|
|
159
|
+
ns: l.Main<M>,
|
|
160
|
+
configOfHandler: M extends l.Procedure | l.Query
|
|
161
|
+
? LexMethodConfig<M, A> | LexMethodHandler<M, A>
|
|
162
|
+
: M extends l.Subscription
|
|
163
|
+
? LexSubscriptionConfig<M, A> | LexSubscriptionHandler<M, A>
|
|
164
|
+
: never,
|
|
165
|
+
): void {
|
|
166
|
+
const schema = l.getMain(ns)
|
|
167
|
+
const config =
|
|
168
|
+
typeof configOfHandler === 'function'
|
|
169
|
+
? { handler: configOfHandler }
|
|
170
|
+
: configOfHandler
|
|
171
|
+
switch (schema.type) {
|
|
172
|
+
case 'procedure':
|
|
173
|
+
return this.addProcedureSchema(
|
|
174
|
+
schema,
|
|
175
|
+
config as LexMethodConfig<l.Procedure, A>,
|
|
176
|
+
)
|
|
177
|
+
case 'query':
|
|
178
|
+
return this.addQuerySchema(
|
|
179
|
+
schema,
|
|
180
|
+
config as LexMethodConfig<l.Query, A>,
|
|
181
|
+
)
|
|
182
|
+
case 'subscription':
|
|
183
|
+
return this.addSubscriptionSchema(
|
|
184
|
+
schema,
|
|
185
|
+
config as LexSubscriptionConfig<l.Subscription, A>,
|
|
186
|
+
)
|
|
187
|
+
default:
|
|
188
|
+
throw new TypeError(
|
|
189
|
+
// @ts-expect-error should never happen
|
|
190
|
+
`Unsupported schema ${schema.nsid} of type ${schema.type}`,
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
protected addProcedureSchema<M extends l.Procedure, A extends Auth>(
|
|
196
|
+
schema: M,
|
|
197
|
+
config: LexMethodConfig<M, A>,
|
|
198
|
+
): void {
|
|
199
|
+
this.routes.post(
|
|
200
|
+
`/xrpc/${schema.nsid}`,
|
|
201
|
+
this.createHandlerInternal<
|
|
202
|
+
A,
|
|
203
|
+
LexMethodParams<M>,
|
|
204
|
+
LexMethodInput<M>,
|
|
205
|
+
LexMethodOutput<M>
|
|
206
|
+
>(
|
|
207
|
+
this.createAuthVerifier(config),
|
|
208
|
+
this.createSchemaParamsVerifier(schema),
|
|
209
|
+
this.createSchemaInputVerifier(schema, config.opts),
|
|
210
|
+
this.createRouteRateLimiter(schema.nsid, config),
|
|
211
|
+
config.handler,
|
|
212
|
+
this.createSchemaOutputVerifier(schema),
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
protected addQuerySchema<M extends l.Query, A extends Auth>(
|
|
218
|
+
schema: M,
|
|
219
|
+
config: LexMethodConfig<M, A>,
|
|
220
|
+
): void {
|
|
221
|
+
this.routes.get(
|
|
222
|
+
`/xrpc/${schema.nsid}`,
|
|
223
|
+
this.createHandlerInternal<
|
|
224
|
+
A,
|
|
225
|
+
LexMethodParams<M>,
|
|
226
|
+
LexMethodInput<M>,
|
|
227
|
+
LexMethodOutput<M>
|
|
228
|
+
>(
|
|
229
|
+
this.createAuthVerifier(config),
|
|
230
|
+
this.createSchemaParamsVerifier(schema),
|
|
231
|
+
this.createSchemaInputVerifier(schema, config.opts),
|
|
232
|
+
this.createRouteRateLimiter(schema.nsid, config),
|
|
233
|
+
config.handler,
|
|
234
|
+
this.createSchemaOutputVerifier(schema),
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
protected addSubscriptionSchema<
|
|
240
|
+
M extends l.Subscription,
|
|
241
|
+
A extends Auth = void,
|
|
242
|
+
>(schema: M, config: LexSubscriptionConfig<M, A>): void {
|
|
243
|
+
const { handler } = config
|
|
244
|
+
const messageSchema =
|
|
245
|
+
this.options.validateResponse === false ? undefined : schema.message
|
|
246
|
+
|
|
247
|
+
return this.addSubscriptionInternal(
|
|
248
|
+
schema.nsid,
|
|
249
|
+
this.createSchemaParamsVerifier(schema),
|
|
250
|
+
this.createAuthVerifier(config),
|
|
251
|
+
// Wrap the handler to validate outgoing messages if a message schema
|
|
252
|
+
// is available
|
|
253
|
+
messageSchema
|
|
254
|
+
? async function* (ctx) {
|
|
255
|
+
for await (const item of handler(ctx)) {
|
|
256
|
+
if (item instanceof Frame) {
|
|
257
|
+
messageSchema.validate(item.body)
|
|
258
|
+
yield item
|
|
259
|
+
} else {
|
|
260
|
+
yield messageSchema.validate(item)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
: handler,
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
120
268
|
method<A extends Auth = Auth>(
|
|
121
269
|
nsid: string,
|
|
122
270
|
configOrFn: MethodConfigOrHandler<A>,
|
|
123
|
-
) {
|
|
271
|
+
): void {
|
|
124
272
|
this.addMethod(nsid, configOrFn)
|
|
125
273
|
}
|
|
126
274
|
|
|
@@ -140,14 +288,14 @@ export class Server {
|
|
|
140
288
|
|
|
141
289
|
streamMethod<A extends Auth = Auth>(
|
|
142
290
|
nsid: string,
|
|
143
|
-
configOrFn: StreamConfigOrHandler<A>,
|
|
291
|
+
configOrFn: StreamConfigOrHandler<A, Params>,
|
|
144
292
|
) {
|
|
145
293
|
this.addStreamMethod(nsid, configOrFn)
|
|
146
294
|
}
|
|
147
295
|
|
|
148
296
|
addStreamMethod<A extends Auth = Auth>(
|
|
149
297
|
nsid: string,
|
|
150
|
-
configOrFn: StreamConfigOrHandler<A>,
|
|
298
|
+
configOrFn: StreamConfigOrHandler<A, Params>,
|
|
151
299
|
) {
|
|
152
300
|
const config =
|
|
153
301
|
typeof configOrFn === 'function' ? { handler: configOrFn } : configOrFn
|
|
@@ -239,66 +387,43 @@ export class Server {
|
|
|
239
387
|
}
|
|
240
388
|
}
|
|
241
389
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const params: Params = decodeQueryParams(def, queryParams)
|
|
249
|
-
try {
|
|
250
|
-
return this.lex.assertValidXrpcParams(nsid, params) as Params
|
|
251
|
-
} catch (e) {
|
|
252
|
-
throw new InvalidRequestError(String(e))
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
protected createInputVerifier(
|
|
390
|
+
createHandler<
|
|
391
|
+
A extends Auth = Auth,
|
|
392
|
+
P extends Params = Params,
|
|
393
|
+
I extends Input = Input,
|
|
394
|
+
O extends Output = Output,
|
|
395
|
+
>(
|
|
258
396
|
nsid: string,
|
|
259
397
|
def: LexXrpcQuery | LexXrpcProcedure,
|
|
260
|
-
|
|
261
|
-
) {
|
|
262
|
-
return
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return async (ctx: C) => {
|
|
272
|
-
const result = await auth(ctx)
|
|
273
|
-
return excludeErrorResult(result)
|
|
274
|
-
}
|
|
398
|
+
cfg: MethodConfig<A, P, I, O>,
|
|
399
|
+
): RequestHandler {
|
|
400
|
+
return this.createHandlerInternal<A, P, I, O>(
|
|
401
|
+
this.createAuthVerifier(cfg),
|
|
402
|
+
this.createLexiconParamsVerifier<P>(nsid, def),
|
|
403
|
+
this.createLexiconInputVerifier<I>(nsid, def, cfg.opts),
|
|
404
|
+
this.createRouteRateLimiter(nsid, cfg),
|
|
405
|
+
cfg.handler,
|
|
406
|
+
this.createLexiconOutputVerifier<O>(nsid, def),
|
|
407
|
+
)
|
|
275
408
|
}
|
|
276
409
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
410
|
+
protected createHandlerInternal<
|
|
411
|
+
A extends Auth,
|
|
412
|
+
P extends Params,
|
|
413
|
+
I extends Input,
|
|
414
|
+
O extends Output,
|
|
415
|
+
>(
|
|
416
|
+
authVerifier: AuthVerifierInternal<MethodAuthContext<P>, A> | null,
|
|
417
|
+
paramsVerifier: ParamsVerifierInternal<P>,
|
|
418
|
+
inputVerifier: InputVerifierInternal<I>,
|
|
419
|
+
routeLimiter: RouteRateLimiter<HandlerContext<A, P, I>> | undefined,
|
|
420
|
+
handler: MethodHandler<A, P, I, O>,
|
|
421
|
+
validateResOutput: null | OutputVerifierInternal<O>,
|
|
281
422
|
): RequestHandler {
|
|
282
|
-
const authVerifier = this.createAuthVerifier(cfg)
|
|
283
|
-
const paramsVerifier = this.createParamsVerifier(nsid, def)
|
|
284
|
-
const inputVerifier = this.createInputVerifier(nsid, def, {
|
|
285
|
-
blobLimit: cfg.opts?.blobLimit ?? this.options.payload?.blobLimit,
|
|
286
|
-
jsonLimit: cfg.opts?.jsonLimit ?? this.options.payload?.jsonLimit,
|
|
287
|
-
textLimit: cfg.opts?.textLimit ?? this.options.payload?.textLimit,
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
const validateResOutput =
|
|
291
|
-
this.options.validateResponse === false
|
|
292
|
-
? null
|
|
293
|
-
: (output: void | HandlerSuccess) =>
|
|
294
|
-
validateOutput(nsid, def, output, this.lex)
|
|
295
|
-
|
|
296
|
-
const routeLimiter = this.createRouteRateLimiter(nsid, cfg)
|
|
297
|
-
|
|
298
423
|
return async function (req, res, next) {
|
|
299
424
|
try {
|
|
300
425
|
// parse & validate params
|
|
301
|
-
const params
|
|
426
|
+
const params = paramsVerifier(req)
|
|
302
427
|
|
|
303
428
|
// authenticate request
|
|
304
429
|
const auth: A = authVerifier
|
|
@@ -306,9 +431,9 @@ export class Server {
|
|
|
306
431
|
: (undefined as A)
|
|
307
432
|
|
|
308
433
|
// parse & validate input
|
|
309
|
-
const input:
|
|
434
|
+
const input: I = await inputVerifier(req, res)
|
|
310
435
|
|
|
311
|
-
const ctx: HandlerContext<A> = {
|
|
436
|
+
const ctx: HandlerContext<A, P, I> = {
|
|
312
437
|
params,
|
|
313
438
|
input,
|
|
314
439
|
auth,
|
|
@@ -321,7 +446,7 @@ export class Server {
|
|
|
321
446
|
if (routeLimiter) await routeLimiter.handle(ctx)
|
|
322
447
|
|
|
323
448
|
// run the handler
|
|
324
|
-
const output = await
|
|
449
|
+
const output = (await handler(ctx)) as O
|
|
325
450
|
|
|
326
451
|
if (!output) {
|
|
327
452
|
validateResOutput?.(output)
|
|
@@ -337,25 +462,25 @@ export class Server {
|
|
|
337
462
|
res.status(200)
|
|
338
463
|
res.header('Content-Type', output.encoding)
|
|
339
464
|
res.end(output.buffer)
|
|
340
|
-
} else if (
|
|
341
|
-
next(XRPCError.fromError(output))
|
|
342
|
-
} else {
|
|
465
|
+
} else if (isHandlerSuccess(output)) {
|
|
343
466
|
validateResOutput?.(output)
|
|
344
467
|
|
|
345
468
|
res.status(200)
|
|
346
469
|
setHeaders(res, output.headers)
|
|
347
470
|
|
|
348
|
-
|
|
349
|
-
output.encoding === 'application/json'
|
|
350
|
-
|
|
351
|
-
)
|
|
471
|
+
const encoding =
|
|
472
|
+
output.encoding === 'json' ? 'application/json' : output.encoding
|
|
473
|
+
|
|
474
|
+
res.header('Content-Type', encoding)
|
|
475
|
+
|
|
476
|
+
if (output.body instanceof Readable) {
|
|
477
|
+
// The "Readable" check comes first to avoid calling "lexToJson" on
|
|
478
|
+
// a stream, which would be a bug.
|
|
479
|
+
await pipeline(output.body, res)
|
|
480
|
+
} else if (encoding === 'application/json') {
|
|
352
481
|
const json = lexToJson(output.body)
|
|
353
482
|
res.json(json)
|
|
354
|
-
} else if (output.body instanceof Readable) {
|
|
355
|
-
res.header('Content-Type', output.encoding)
|
|
356
|
-
await pipeline(output.body, res)
|
|
357
483
|
} else {
|
|
358
|
-
res.header('Content-Type', output.encoding)
|
|
359
484
|
res.send(
|
|
360
485
|
Buffer.isBuffer(output.body)
|
|
361
486
|
? output.body
|
|
@@ -364,6 +489,8 @@ export class Server {
|
|
|
364
489
|
: output.body,
|
|
365
490
|
)
|
|
366
491
|
}
|
|
492
|
+
} else {
|
|
493
|
+
next(XRPCError.fromError(output))
|
|
367
494
|
}
|
|
368
495
|
} catch (err: unknown) {
|
|
369
496
|
// Express will not call the next middleware (errorMiddleware in this case)
|
|
@@ -381,12 +508,24 @@ export class Server {
|
|
|
381
508
|
protected async addSubscription<A extends Auth = Auth>(
|
|
382
509
|
nsid: string,
|
|
383
510
|
def: LexXrpcSubscription,
|
|
384
|
-
cfg: StreamConfig<A>,
|
|
511
|
+
cfg: StreamConfig<A, Params>,
|
|
385
512
|
) {
|
|
386
|
-
|
|
387
|
-
|
|
513
|
+
this.addSubscriptionInternal(
|
|
514
|
+
nsid,
|
|
515
|
+
this.createLexiconParamsVerifier(nsid, def),
|
|
516
|
+
this.createAuthVerifier(cfg),
|
|
517
|
+
// @NOTE outgoing messages are not validated against the lexicon schema
|
|
518
|
+
// (unlike the handlers for @atproto/lex based subscriptions)
|
|
519
|
+
cfg.handler,
|
|
520
|
+
)
|
|
521
|
+
}
|
|
388
522
|
|
|
389
|
-
|
|
523
|
+
protected addSubscriptionInternal<A extends Auth, P extends Params>(
|
|
524
|
+
nsid: string,
|
|
525
|
+
paramsVerifier: ParamsVerifierInternal<P>,
|
|
526
|
+
authVerifier: AuthVerifierInternal<StreamAuthContext<P>, A> | null,
|
|
527
|
+
handler: (ctx: StreamContext<A, P>) => AsyncIterable<unknown>,
|
|
528
|
+
) {
|
|
390
529
|
this.subscriptions.set(
|
|
391
530
|
nsid,
|
|
392
531
|
new XrpcStreamServer({
|
|
@@ -395,46 +534,98 @@ export class Server {
|
|
|
395
534
|
try {
|
|
396
535
|
// validate request
|
|
397
536
|
const params = paramsVerifier(req)
|
|
537
|
+
|
|
398
538
|
// authenticate request
|
|
399
539
|
const auth = authVerifier
|
|
400
540
|
? await authVerifier({ req, params })
|
|
401
541
|
: (undefined as A)
|
|
542
|
+
|
|
402
543
|
// stream
|
|
403
544
|
for await (const item of handler({ req, params, auth, signal })) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
const type = item?.['$type']
|
|
409
|
-
if (!check.is(item, schema.map) || typeof type !== 'string') {
|
|
410
|
-
yield new MessageFrame(item as LexValue)
|
|
411
|
-
continue
|
|
412
|
-
}
|
|
413
|
-
const split = type.split('#')
|
|
414
|
-
let t: string
|
|
415
|
-
if (
|
|
416
|
-
split.length === 2 &&
|
|
417
|
-
(split[0] === '' || split[0] === nsid)
|
|
418
|
-
) {
|
|
419
|
-
t = `#${split[1]}`
|
|
420
|
-
} else {
|
|
421
|
-
t = type
|
|
422
|
-
}
|
|
423
|
-
const { $type: _, ...clone } = item as LexMap
|
|
424
|
-
yield new MessageFrame(clone, { type: t })
|
|
545
|
+
yield item instanceof Frame
|
|
546
|
+
? item
|
|
547
|
+
: MessageFrame.fromLexValue(item as LexValue, nsid)
|
|
425
548
|
}
|
|
426
549
|
} catch (err) {
|
|
427
|
-
|
|
428
|
-
yield new ErrorFrame({
|
|
429
|
-
error: xrpcErrPayload.error ?? 'Unknown',
|
|
430
|
-
message: xrpcErrPayload.message,
|
|
431
|
-
})
|
|
550
|
+
yield ErrorFrame.fromError(err)
|
|
432
551
|
}
|
|
433
552
|
},
|
|
434
553
|
}),
|
|
435
554
|
)
|
|
436
555
|
}
|
|
437
556
|
|
|
557
|
+
private createAuthVerifier<C, A extends AuthResult>(cfg: {
|
|
558
|
+
auth?: AuthVerifier<C, A>
|
|
559
|
+
}): null | AuthVerifierInternal<C, A> {
|
|
560
|
+
const { auth } = cfg
|
|
561
|
+
if (!auth) return null
|
|
562
|
+
|
|
563
|
+
return async (ctx) => {
|
|
564
|
+
const result = await auth(ctx)
|
|
565
|
+
return excludeErrorResult(result)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private createLexiconParamsVerifier<P extends Params = Params>(
|
|
570
|
+
nsid: string,
|
|
571
|
+
def: LexXrpcQuery | LexXrpcProcedure | LexXrpcSubscription,
|
|
572
|
+
) {
|
|
573
|
+
return createLexiconParamsVerifier<P>(nsid, def, this.lex)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private createLexiconInputVerifier<I extends Input = Input>(
|
|
577
|
+
nsid: string,
|
|
578
|
+
def: LexXrpcQuery | LexXrpcProcedure,
|
|
579
|
+
opts?: RouteOptions,
|
|
580
|
+
): InputVerifierInternal<I> {
|
|
581
|
+
return createLexiconInputVerifier(
|
|
582
|
+
nsid,
|
|
583
|
+
def,
|
|
584
|
+
{
|
|
585
|
+
blobLimit: opts?.blobLimit ?? this.options.payload?.blobLimit,
|
|
586
|
+
jsonLimit: opts?.jsonLimit ?? this.options.payload?.jsonLimit,
|
|
587
|
+
textLimit: opts?.textLimit ?? this.options.payload?.textLimit,
|
|
588
|
+
},
|
|
589
|
+
this.lex,
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private createLexiconOutputVerifier<O extends Output = Output>(
|
|
594
|
+
nsid: string,
|
|
595
|
+
def: LexXrpcQuery | LexXrpcProcedure,
|
|
596
|
+
): null | OutputVerifierInternal<O> {
|
|
597
|
+
if (this.options.validateResponse === false) {
|
|
598
|
+
return null
|
|
599
|
+
}
|
|
600
|
+
return createLexiconOutputVerifier(nsid, def, this.lex)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private createSchemaParamsVerifier<
|
|
604
|
+
M extends l.Procedure | l.Query | l.Subscription,
|
|
605
|
+
>(ns: l.Main<M>): ParamsVerifierInternal<LexMethodParams<M>> {
|
|
606
|
+
return createSchemaParamsVerifier<M>(ns)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private createSchemaInputVerifier<M extends l.Procedure | l.Query>(
|
|
610
|
+
ns: l.Main<M>,
|
|
611
|
+
opts?: RouteOptions,
|
|
612
|
+
): InputVerifierInternal<LexMethodInput<M>> {
|
|
613
|
+
return createSchemaInputVerifier<M>(ns, {
|
|
614
|
+
blobLimit: opts?.blobLimit ?? this.options.payload?.blobLimit,
|
|
615
|
+
jsonLimit: opts?.jsonLimit ?? this.options.payload?.jsonLimit,
|
|
616
|
+
textLimit: opts?.textLimit ?? this.options.payload?.textLimit,
|
|
617
|
+
})
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private createSchemaOutputVerifier<M extends l.Procedure | l.Query>(
|
|
621
|
+
ns: l.Main<M>,
|
|
622
|
+
): null | OutputVerifierInternal<LexMethodOutput<M>> {
|
|
623
|
+
if (this.options.validateResponse === false) {
|
|
624
|
+
return null
|
|
625
|
+
}
|
|
626
|
+
return createSchemaOutputVerifier<M>(ns)
|
|
627
|
+
}
|
|
628
|
+
|
|
438
629
|
private enableStreamingOnListen(app: Application) {
|
|
439
630
|
const _listen = app.listen
|
|
440
631
|
app.listen = (...args) => {
|
|
@@ -452,10 +643,15 @@ export class Server {
|
|
|
452
643
|
}
|
|
453
644
|
}
|
|
454
645
|
|
|
455
|
-
private createRouteRateLimiter<
|
|
646
|
+
private createRouteRateLimiter<
|
|
647
|
+
A extends Auth,
|
|
648
|
+
P extends Params,
|
|
649
|
+
I extends Input,
|
|
650
|
+
O extends Output,
|
|
651
|
+
>(
|
|
456
652
|
nsid: string,
|
|
457
|
-
config: MethodConfig<A>,
|
|
458
|
-
): RouteRateLimiter<
|
|
653
|
+
config: MethodConfig<A, P, I, O>,
|
|
654
|
+
): RouteRateLimiter<HandlerContext<A, P, I>> | undefined {
|
|
459
655
|
// @NOTE global & shared rate limiters are instantiated with a context of
|
|
460
656
|
// HandlerContext which is compatible (more generic) with the context of
|
|
461
657
|
// this route specific rate limiters (C). For this reason, it's safe to
|
package/src/stream/frames.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeAll, encode } from '@atproto/lex-cbor'
|
|
2
|
+
import { LexValue, isPlainObject } from '@atproto/lex-data'
|
|
3
|
+
import { XRPCError } from '../errors'
|
|
2
4
|
import {
|
|
3
5
|
ErrorFrameBody,
|
|
4
6
|
ErrorFrameHeader,
|
|
@@ -35,19 +37,21 @@ export abstract class Frame<T extends LexValue = LexValue> {
|
|
|
35
37
|
|
|
36
38
|
const parsedHeader = frameHeader.safeParse(header)
|
|
37
39
|
if (!parsedHeader.success) {
|
|
38
|
-
throw new Error(`Invalid frame header: ${parsedHeader.
|
|
40
|
+
throw new Error(`Invalid frame header: ${parsedHeader.reason.message}`)
|
|
39
41
|
}
|
|
40
|
-
const frameOp = parsedHeader.
|
|
42
|
+
const frameOp = parsedHeader.value.op
|
|
41
43
|
if (frameOp === FrameType.Message) {
|
|
42
44
|
return new MessageFrame(body, {
|
|
43
|
-
type: parsedHeader.
|
|
45
|
+
type: parsedHeader.value.t,
|
|
44
46
|
})
|
|
45
47
|
} else if (frameOp === FrameType.Error) {
|
|
46
48
|
const parsedBody = errorFrameBody.safeParse(body)
|
|
47
49
|
if (!parsedBody.success) {
|
|
48
|
-
throw new Error(
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Invalid error frame body: ${parsedBody.reason.message}`,
|
|
52
|
+
)
|
|
49
53
|
}
|
|
50
|
-
return new ErrorFrame(parsedBody.
|
|
54
|
+
return new ErrorFrame(parsedBody.value)
|
|
51
55
|
} else {
|
|
52
56
|
const exhaustiveCheck: never = frameOp
|
|
53
57
|
throw new Error(`Unknown frame op: ${exhaustiveCheck}`)
|
|
@@ -70,6 +74,29 @@ export class MessageFrame<T extends LexValue = LexValue> extends Frame<T> {
|
|
|
70
74
|
get type() {
|
|
71
75
|
return this.header.t
|
|
72
76
|
}
|
|
77
|
+
|
|
78
|
+
static fromLexValue(data: LexValue, nsid: string) {
|
|
79
|
+
if (!isPlainObject(data)) {
|
|
80
|
+
return new MessageFrame(data)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const $type = data?.['$type']
|
|
84
|
+
if (typeof $type !== 'string') {
|
|
85
|
+
return new MessageFrame(data)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let type: string
|
|
89
|
+
|
|
90
|
+
const split = $type.split('#')
|
|
91
|
+
if (split.length === 2 && (split[0] === '' || split[0] === nsid)) {
|
|
92
|
+
type = `#${split[1]}`
|
|
93
|
+
} else {
|
|
94
|
+
type = $type
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { $type: _, ...clone } = data
|
|
98
|
+
return new MessageFrame(clone, { type })
|
|
99
|
+
}
|
|
73
100
|
}
|
|
74
101
|
|
|
75
102
|
export class ErrorFrame<T extends string = string> extends Frame<
|
|
@@ -89,4 +116,10 @@ export class ErrorFrame<T extends string = string> extends Frame<
|
|
|
89
116
|
get message() {
|
|
90
117
|
return this.body.message
|
|
91
118
|
}
|
|
119
|
+
|
|
120
|
+
static fromError(err: unknown): ErrorFrame {
|
|
121
|
+
if (err instanceof ErrorFrame) return err
|
|
122
|
+
const { error = 'Unknown', message } = XRPCError.fromError(err).payload
|
|
123
|
+
return new ErrorFrame({ error, message })
|
|
124
|
+
}
|
|
92
125
|
}
|
package/src/stream/types.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { l } from '@atproto/lex-schema'
|
|
2
2
|
|
|
3
3
|
export enum FrameType {
|
|
4
4
|
Message = 1,
|
|
5
5
|
Error = -1,
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export const messageFrameHeader =
|
|
9
|
-
op:
|
|
10
|
-
t:
|
|
8
|
+
export const messageFrameHeader = l.object({
|
|
9
|
+
op: l.literal(FrameType.Message), // Frame op
|
|
10
|
+
t: l.optional(l.string()), // Message body type discriminator
|
|
11
11
|
})
|
|
12
|
-
export type MessageFrameHeader =
|
|
12
|
+
export type MessageFrameHeader = l.Infer<typeof messageFrameHeader>
|
|
13
13
|
|
|
14
|
-
export const errorFrameHeader =
|
|
15
|
-
op:
|
|
14
|
+
export const errorFrameHeader = l.object({
|
|
15
|
+
op: l.literal(FrameType.Error),
|
|
16
16
|
})
|
|
17
|
-
export const errorFrameBody =
|
|
18
|
-
error:
|
|
19
|
-
message:
|
|
17
|
+
export const errorFrameBody = l.object({
|
|
18
|
+
error: l.string(), // Error code
|
|
19
|
+
message: l.optional(l.string()), // Error message
|
|
20
20
|
})
|
|
21
|
-
export type ErrorFrameHeader =
|
|
22
|
-
export type ErrorFrameBody<T extends string = string> = { error: T } &
|
|
21
|
+
export type ErrorFrameHeader = l.Infer<typeof errorFrameHeader>
|
|
22
|
+
export type ErrorFrameBody<T extends string = string> = { error: T } & l.Infer<
|
|
23
23
|
typeof errorFrameBody
|
|
24
24
|
>
|
|
25
25
|
|
|
26
|
-
export const frameHeader =
|
|
27
|
-
export type FrameHeader =
|
|
26
|
+
export const frameHeader = l.union([messageFrameHeader, errorFrameHeader])
|
|
27
|
+
export type FrameHeader = l.Infer<typeof frameHeader>
|