@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/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 { check, schema } from '@atproto/common'
14
- import { LexMap, LexValue } from '@atproto/lex-data'
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
- createInputVerifier,
64
- decodeQueryParams,
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
- protected createParamsVerifier(
243
- nsid: string,
244
- def: LexXrpcQuery | LexXrpcProcedure | LexXrpcSubscription,
245
- ) {
246
- return (req: Request | IncomingMessage): Params => {
247
- const queryParams = 'query' in req ? req.query : getQueryParams(req.url)
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
- routeOpts: RouteOptions,
261
- ) {
262
- return createInputVerifier(nsid, def, routeOpts, this.lex)
263
- }
264
-
265
- protected createAuthVerifier<C, A extends Auth>(cfg: {
266
- auth?: AuthVerifier<C, A & AuthResult>
267
- }): null | ((ctx: C) => Promise<A>) {
268
- const { auth } = cfg
269
- if (!auth) return null
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
- createHandler<A extends Auth = Auth>(
278
- nsid: string,
279
- def: LexXrpcQuery | LexXrpcProcedure,
280
- cfg: MethodConfig<A>,
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: Params = paramsVerifier(req)
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: Input = await inputVerifier(req, res)
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 cfg.handler(ctx)
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 (isErrorResult(output)) {
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
- if (
349
- output.encoding === 'application/json' ||
350
- output.encoding === 'json'
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
- const paramsVerifier = this.createParamsVerifier(nsid, def)
387
- const authVerifier = this.createAuthVerifier(cfg)
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
- const { handler } = cfg
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
- if (item instanceof Frame) {
405
- yield item
406
- continue
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
- const xrpcErrPayload = XRPCError.fromError(err).payload
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<A extends Auth, C extends HandlerContext>(
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<C> | undefined {
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
@@ -1,4 +1,6 @@
1
- import { LexValue, decodeAll, encode } from '@atproto/lex-cbor'
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.error.message}`)
40
+ throw new Error(`Invalid frame header: ${parsedHeader.reason.message}`)
39
41
  }
40
- const frameOp = parsedHeader.data.op
42
+ const frameOp = parsedHeader.value.op
41
43
  if (frameOp === FrameType.Message) {
42
44
  return new MessageFrame(body, {
43
- type: parsedHeader.data.t,
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(`Invalid error frame body: ${parsedBody.error.message}`)
50
+ throw new Error(
51
+ `Invalid error frame body: ${parsedBody.reason.message}`,
52
+ )
49
53
  }
50
- return new ErrorFrame(parsedBody.data)
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
  }
@@ -1,27 +1,27 @@
1
- import { z } from 'zod'
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 = z.object({
9
- op: z.literal(FrameType.Message), // Frame op
10
- t: z.string().optional(), // Message body type discriminator
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 = z.infer<typeof messageFrameHeader>
12
+ export type MessageFrameHeader = l.Infer<typeof messageFrameHeader>
13
13
 
14
- export const errorFrameHeader = z.object({
15
- op: z.literal(FrameType.Error),
14
+ export const errorFrameHeader = l.object({
15
+ op: l.literal(FrameType.Error),
16
16
  })
17
- export const errorFrameBody = z.object({
18
- error: z.string(), // Error code
19
- message: z.string().optional(), // Error 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 = z.infer<typeof errorFrameHeader>
22
- export type ErrorFrameBody<T extends string = string> = { error: T } & z.infer<
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 = z.union([messageFrameHeader, errorFrameHeader])
27
- export type FrameHeader = z.infer<typeof frameHeader>
26
+ export const frameHeader = l.union([messageFrameHeader, errorFrameHeader])
27
+ export type FrameHeader = l.Infer<typeof frameHeader>