@atproto/xrpc-server 0.7.19 → 0.9.0

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/auth.js +11 -11
  3. package/dist/auth.js.map +1 -1
  4. package/dist/errors.d.ts +67 -0
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/errors.js +202 -0
  7. package/dist/errors.js.map +1 -0
  8. package/dist/index.d.ts +4 -3
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/rate-limiter.d.ts +95 -26
  13. package/dist/rate-limiter.d.ts.map +1 -1
  14. package/dist/rate-limiter.js +179 -85
  15. package/dist/rate-limiter.js.map +1 -1
  16. package/dist/server.d.ts +20 -15
  17. package/dist/server.d.ts.map +1 -1
  18. package/dist/server.js +185 -220
  19. package/dist/server.js.map +1 -1
  20. package/dist/types.d.ts +80 -175
  21. package/dist/types.d.ts.map +1 -1
  22. package/dist/types.js +9 -226
  23. package/dist/types.js.map +1 -1
  24. package/dist/util.d.ts +12 -9
  25. package/dist/util.d.ts.map +1 -1
  26. package/dist/util.js +114 -78
  27. package/dist/util.js.map +1 -1
  28. package/package.json +4 -3
  29. package/src/auth.ts +1 -1
  30. package/src/errors.ts +293 -0
  31. package/src/index.ts +4 -3
  32. package/src/rate-limiter.ts +270 -104
  33. package/src/server.ts +265 -276
  34. package/src/types.ts +144 -429
  35. package/src/util.ts +131 -85
  36. package/tests/auth.test.ts +2 -2
  37. package/tests/bodies.test.ts +18 -27
  38. package/tests/errors.test.ts +1 -1
  39. package/tests/ipld.test.ts +15 -14
  40. package/tests/parameters.test.ts +4 -7
  41. package/tests/procedures.test.ts +22 -34
  42. package/tests/queries.test.ts +9 -12
  43. package/tests/rate-limiter.test.ts +8 -11
  44. package/tests/responses.test.ts +12 -15
  45. package/tsconfig.build.tsbuildinfo +1 -1
package/src/types.ts CHANGED
@@ -1,34 +1,27 @@
1
1
  import { IncomingMessage } from 'node:http'
2
2
  import { Readable } from 'node:stream'
3
- import express from 'express'
4
- import { isHttpError } from 'http-errors'
3
+ import { NextFunction, Request, Response } from 'express'
5
4
  import { z } from 'zod'
6
- import {
7
- ResponseType,
8
- ResponseTypeStrings,
9
- XRPCError as XRPCClientError,
10
- httpResponseCodeToName,
11
- httpResponseCodeToString,
12
- } from '@atproto/xrpc'
5
+ import { ErrorResult, XRPCError } from './errors'
6
+ import { CalcKeyFn, CalcPointsFn, RateLimiterI } from './rate-limiter'
7
+
8
+ export type Awaitable<T> = T | Promise<T>
13
9
 
14
10
  export type CatchallHandler = (
15
- req: express.Request,
16
- _res: express.Response,
17
- next: express.NextFunction,
11
+ req: Request,
12
+ res: Response,
13
+ next: NextFunction,
18
14
  ) => unknown
19
15
 
20
16
  export type Options = {
21
17
  validateResponse?: boolean
22
18
  catchall?: CatchallHandler
23
- payload?: {
24
- jsonLimit?: number
25
- blobLimit?: number
26
- textLimit?: number
27
- }
19
+ payload?: RouteOptions
28
20
  rateLimits?: {
29
- creator: RateLimiterCreator
30
- global?: ServerRateLimitDescription[]
31
- shared?: ServerRateLimitDescription[]
21
+ creator: RateLimiterCreator<HandlerContext>
22
+ global?: ServerRateLimitDescription<HandlerContext>[]
23
+ shared?: ServerRateLimitDescription<HandlerContext>[]
24
+ bypass?: (ctx: HandlerContext) => boolean
32
25
  }
33
26
  /**
34
27
  * By default, errors are converted to {@link XRPCError} using
@@ -43,30 +36,31 @@ export type Options = {
43
36
  errorParser?: (err: unknown) => XRPCError
44
37
  }
45
38
 
46
- export type UndecodedParams = (typeof express.request)['query']
39
+ export type UndecodedParams = Request['query']
47
40
 
48
41
  export type Primitive = string | number | boolean
49
- export type Params = Record<string, Primitive | Primitive[] | undefined>
42
+ export type Params = { [P in string]?: undefined | Primitive | Primitive[] }
50
43
 
51
- export const handlerInput = z.object({
52
- encoding: z.string(),
53
- body: z.any(),
54
- })
55
- export type HandlerInput = z.infer<typeof handlerInput>
44
+ export type HandlerInput = {
45
+ encoding: string
46
+ body: unknown
47
+ }
56
48
 
57
- export const handlerAuth = z.object({
58
- credentials: z.any(),
59
- artifacts: z.any(),
60
- })
61
- export type HandlerAuth = z.infer<typeof handlerAuth>
49
+ export type AuthResult = {
50
+ credentials: unknown
51
+ artifacts?: unknown
52
+ }
62
53
 
63
54
  export const headersSchema = z.record(z.string())
64
55
 
56
+ export type Headers = z.infer<typeof headersSchema>
57
+
65
58
  export const handlerSuccess = z.object({
66
59
  encoding: z.string(),
67
60
  body: z.any(),
68
61
  headers: headersSchema.optional(),
69
62
  })
63
+
70
64
  export type HandlerSuccess = z.infer<typeof handlerSuccess>
71
65
 
72
66
  export const handlerPipeThroughBuffer = z.object({
@@ -92,442 +86,163 @@ export const handlerPipeThrough = z.union([
92
86
 
93
87
  export type HandlerPipeThrough = z.infer<typeof handlerPipeThrough>
94
88
 
95
- export const handlerError = z.object({
96
- status: z.number(),
97
- error: z.string().optional(),
98
- message: z.string().optional(),
99
- })
100
- export type HandlerError = z.infer<typeof handlerError>
101
-
102
- export type HandlerOutput = HandlerSuccess | HandlerPipeThrough | HandlerError
89
+ export type Auth = void | AuthResult
90
+ export type Input = void | HandlerInput
91
+ export type Output = void | HandlerSuccess | ErrorResult
103
92
 
104
- export type XRPCReqContext = {
105
- auth: HandlerAuth | undefined
106
- params: Params
107
- input: HandlerInput | undefined
108
- req: express.Request
109
- res: express.Response
110
- resetRouteRateLimits: () => Promise<void>
111
- }
112
-
113
- export type XRPCHandler = (
114
- ctx: XRPCReqContext,
115
- ) => Promise<HandlerOutput> | HandlerOutput | undefined
116
-
117
- export type XRPCStreamHandler = (ctx: {
118
- auth: HandlerAuth | undefined
119
- params: Params
120
- req: IncomingMessage
121
- signal: AbortSignal
122
- }) => AsyncIterable<unknown>
93
+ export type AuthVerifier<C, A extends AuthResult = AuthResult> =
94
+ | ((ctx: C) => Awaitable<A | ErrorResult>)
95
+ | ((ctx: C) => Awaitable<A>)
123
96
 
124
- export type AuthOutput = HandlerAuth | HandlerError
125
-
126
- export interface AuthVerifierContext {
127
- req: express.Request
128
- res: express.Response
129
- }
130
-
131
- export type AuthVerifier = (
132
- ctx: AuthVerifierContext,
133
- ) => Promise<AuthOutput> | AuthOutput
134
-
135
- export interface StreamAuthVerifierContext {
136
- req: IncomingMessage
97
+ export type MethodAuthContext<P extends Params = Params> = {
98
+ params: P
99
+ req: Request
100
+ res: Response
137
101
  }
138
102
 
139
- export type StreamAuthVerifier = (
140
- ctx: StreamAuthVerifierContext,
141
- ) => Promise<AuthOutput> | AuthOutput
142
-
143
- export type CalcKeyFn = (ctx: XRPCReqContext) => string | null
144
- export type CalcPointsFn = (ctx: XRPCReqContext) => number
103
+ export type MethodAuthVerifier<
104
+ A extends AuthResult = AuthResult,
105
+ P extends Params = Params,
106
+ > = AuthVerifier<MethodAuthContext<P>, A>
145
107
 
146
- export interface RateLimiterI {
147
- consume: RateLimiterConsume
148
- reset: RateLimiterReset
108
+ export type HandlerContext<
109
+ A extends Auth = Auth,
110
+ P extends Params = Params,
111
+ I extends Input = Input,
112
+ > = MethodAuthContext<P> & {
113
+ auth: A
114
+ input: I
115
+ resetRouteRateLimits: () => Promise<void>
149
116
  }
150
117
 
151
- export type RateLimiterConsume = (
152
- ctx: XRPCReqContext,
153
- opts?: { calcKey?: CalcKeyFn; calcPoints?: CalcPointsFn },
154
- ) => Promise<RateLimiterStatus | RateLimitExceededError | null>
155
-
156
- export type RateLimiterReset = (
157
- ctx: XRPCReqContext,
158
- opts?: { calcKey?: CalcKeyFn },
159
- ) => Promise<void>
118
+ export type MethodHandler<
119
+ A extends Auth = Auth,
120
+ P extends Params = Params,
121
+ I extends Input = Input,
122
+ O extends Output = Output,
123
+ > = (ctx: HandlerContext<A, P, I>) => Awaitable<O | HandlerPipeThrough>
160
124
 
161
- export type RateLimiterCreator = (opts: {
125
+ export type RateLimiterCreator<T extends HandlerContext = HandlerContext> = <
126
+ C extends T = T,
127
+ >(opts: {
162
128
  keyPrefix: string
163
129
  durationMs: number
164
130
  points: number
165
- calcKey?: CalcKeyFn
166
- calcPoints?: CalcPointsFn
167
- }) => RateLimiterI
168
-
169
- export type ServerRateLimitDescription = {
131
+ calcKey: CalcKeyFn<C>
132
+ calcPoints: CalcPointsFn<C>
133
+ failClosed?: boolean
134
+ }) => RateLimiterI<C>
135
+
136
+ export type ServerRateLimitDescription<
137
+ C extends HandlerContext = HandlerContext,
138
+ > = {
170
139
  name: string
171
140
  durationMs: number
172
141
  points: number
173
- calcKey?: CalcKeyFn
174
- calcPoints?: CalcPointsFn
142
+ calcKey?: CalcKeyFn<C>
143
+ calcPoints?: CalcPointsFn<C>
144
+ failClosed?: boolean
175
145
  }
176
146
 
177
- export type SharedRateLimitOpts = {
147
+ export type SharedRateLimitOpts<C extends HandlerContext = HandlerContext> = {
178
148
  name: string
179
- calcKey?: CalcKeyFn
180
- calcPoints?: CalcPointsFn
149
+ calcKey?: CalcKeyFn<C>
150
+ calcPoints?: CalcPointsFn<C>
181
151
  }
182
152
 
183
- export type RouteRateLimitOpts = {
153
+ export type RouteRateLimitOpts<C extends HandlerContext = HandlerContext> = {
184
154
  durationMs: number
185
155
  points: number
186
- calcKey?: CalcKeyFn
187
- calcPoints?: CalcPointsFn
156
+ calcKey?: CalcKeyFn<C>
157
+ calcPoints?: CalcPointsFn<C>
188
158
  }
189
159
 
190
- export type HandlerRateLimitOpts = SharedRateLimitOpts | RouteRateLimitOpts
160
+ export type RateLimitOpts<C extends HandlerContext = HandlerContext> =
161
+ | SharedRateLimitOpts<C>
162
+ | RouteRateLimitOpts<C>
191
163
 
192
- export const isShared = (
193
- opts: HandlerRateLimitOpts,
194
- ): opts is SharedRateLimitOpts => {
164
+ export function isSharedRateLimitOpts<
165
+ C extends HandlerContext = HandlerContext,
166
+ >(opts: RateLimitOpts<C>): opts is SharedRateLimitOpts<C> {
195
167
  return typeof opts['name'] === 'string'
196
168
  }
197
169
 
198
- export type RateLimiterStatus = {
199
- limit: number
200
- duration: number
201
- remainingPoints: number
202
- msBeforeNext: number
203
- consumedPoints: number
204
- isFirstInDuration: boolean
205
- }
206
-
207
- export type RouteOpts = {
170
+ export type RouteOptions = {
208
171
  blobLimit?: number
172
+ jsonLimit?: number
173
+ textLimit?: number
174
+ }
175
+
176
+ export type MethodConfig<
177
+ A extends Auth = Auth,
178
+ P extends Params = Params,
179
+ I extends Input = Input,
180
+ O extends Output = Output,
181
+ > = {
182
+ handler: MethodHandler<A, P, I, O>
183
+ auth?: MethodAuthVerifier<Extract<A, AuthResult>, P>
184
+ opts?: RouteOptions
185
+ rateLimit?:
186
+ | RateLimitOpts<HandlerContext<A, P, I>>
187
+ | RateLimitOpts<HandlerContext<A, P, I>>[]
188
+ }
189
+
190
+ export type MethodConfigOrHandler<
191
+ A extends Auth = Auth,
192
+ P extends Params = Params,
193
+ I extends Input = Input,
194
+ O extends Output = Output,
195
+ > = MethodHandler<A, P, I, O> | MethodConfig<A, P, I, O>
196
+
197
+ export type StreamAuthContext<P extends Params = Params> = {
198
+ params: P
199
+ req: IncomingMessage
209
200
  }
210
201
 
211
- export type XRPCHandlerConfig = {
212
- opts?: RouteOpts
213
- rateLimit?: HandlerRateLimitOpts | HandlerRateLimitOpts[]
214
- auth?: AuthVerifier
215
- handler: XRPCHandler
216
- }
217
-
218
- export type XRPCStreamHandlerConfig = {
219
- auth?: StreamAuthVerifier
220
- handler: XRPCStreamHandler
221
- }
202
+ export type StreamAuthVerifier<
203
+ A extends AuthResult = AuthResult,
204
+ P extends Params = Params,
205
+ > = AuthVerifier<StreamAuthContext<P>, A>
222
206
 
223
- export { ResponseType }
224
-
225
- /**
226
- * Converts an upstream XRPC {@link ResponseType} into a downstream {@link ResponseType}.
227
- */
228
- function mapFromClientError(error: XRPCClientError): {
229
- error: string
230
- message: string
231
- type: ResponseType
232
- } {
233
- switch (error.status) {
234
- case ResponseType.InvalidResponse:
235
- // Upstream server returned an XRPC response that is not compatible with our internal lexicon definitions for that XRPC method.
236
- // @NOTE This could be reflected as both a 500 ("we" are at fault) and 502 ("they" are at fault). Let's be gents about it.
237
- return {
238
- error: httpResponseCodeToName(ResponseType.InternalServerError),
239
- message: httpResponseCodeToString(ResponseType.InternalServerError),
240
- type: ResponseType.InternalServerError,
241
- }
242
- case ResponseType.Unknown:
243
- // Typically a network error / unknown host
244
- return {
245
- error: httpResponseCodeToName(ResponseType.InternalServerError),
246
- message: httpResponseCodeToString(ResponseType.InternalServerError),
247
- type: ResponseType.InternalServerError,
248
- }
249
- default:
250
- return {
251
- error: error.error,
252
- message: error.message,
253
- type: error.status,
254
- }
255
- }
207
+ export type StreamContext<
208
+ A extends Auth = Auth,
209
+ P extends Params = Params,
210
+ > = StreamAuthContext<P> & {
211
+ auth: A
212
+ signal: AbortSignal
256
213
  }
257
214
 
258
- export class XRPCError extends Error {
259
- constructor(
260
- public type: ResponseType,
261
- public errorMessage?: string,
262
- public customErrorName?: string,
263
- options?: ErrorOptions,
264
- ) {
265
- super(errorMessage, options)
266
- }
267
-
268
- get statusCode(): number {
269
- const { type } = this
270
-
271
- // Fool-proofing. `new XRPCError(123.5 as number, '')` does not generate a TypeScript error.
272
- // Because of this, we can end-up with any numeric value instead of an actual `ResponseType`.
273
- // For legacy reasons, the `type` argument is not checked in the constructor, so we check it here.
274
- if (type < 400 || type >= 600 || !Number.isFinite(type)) {
275
- return 500
276
- }
277
-
278
- return type
279
- }
280
-
281
- get payload() {
282
- return {
283
- error: this.customErrorName ?? this.typeName,
284
- message:
285
- this.type === ResponseType.InternalServerError
286
- ? this.typeStr // Do not respond with error details for 500s
287
- : this.errorMessage || this.typeStr,
288
- }
289
- }
290
-
291
- get typeName(): string | undefined {
292
- return ResponseType[this.type]
293
- }
294
-
295
- get typeStr(): string | undefined {
296
- return ResponseTypeStrings[this.type]
297
- }
298
-
299
- static fromError(cause: unknown): XRPCError {
300
- if (cause instanceof XRPCError) {
301
- return cause
302
- }
303
-
304
- if (cause instanceof XRPCClientError) {
305
- const { error, message, type } = mapFromClientError(cause)
306
- return new XRPCError(type, message, error, { cause })
307
- }
308
-
309
- if (isHttpError(cause)) {
310
- return new XRPCError(cause.status, cause.message, cause.name, { cause })
311
- }
312
-
313
- if (isHandlerError(cause)) {
314
- return this.fromHandlerError(cause)
315
- }
316
-
317
- if (cause instanceof Error) {
318
- return new InternalServerError(cause.message, undefined, { cause })
319
- }
320
-
321
- return new InternalServerError(
322
- 'Unexpected internal server error',
323
- undefined,
324
- { cause },
325
- )
326
- }
215
+ export type StreamHandler<
216
+ A extends Auth = Auth,
217
+ P extends Params = Params,
218
+ O = unknown,
219
+ > = (ctx: StreamContext<A, P>) => AsyncIterable<O>
327
220
 
328
- static fromHandlerError(err: HandlerError): XRPCError {
329
- return new XRPCError(err.status, err.message, err.error, { cause: err })
330
- }
221
+ export type StreamConfig<
222
+ A extends Auth = Auth,
223
+ P extends Params = Params,
224
+ O = unknown,
225
+ > = {
226
+ auth?: StreamAuthVerifier<Extract<A, AuthResult>, P>
227
+ handler: StreamHandler<A, P, O>
331
228
  }
332
229
 
333
- export function isHandlerError(v: unknown): v is HandlerError {
334
- return (
335
- !!v &&
336
- typeof v === 'object' &&
337
- typeof v['status'] === 'number' &&
338
- (v['error'] === undefined || typeof v['error'] === 'string') &&
339
- (v['message'] === undefined || typeof v['message'] === 'string')
340
- )
341
- }
230
+ export type StreamConfigOrHandler<
231
+ A extends Auth = Auth,
232
+ P extends Params = Params,
233
+ O = unknown,
234
+ > = StreamHandler<A, P, O> | StreamConfig<A, P, O>
342
235
 
343
236
  export function isHandlerPipeThroughBuffer(
344
- v: HandlerOutput,
345
- ): v is HandlerPipeThroughBuffer {
346
- // We only need to discriminate between possible HandlerOutput values
347
- return v['buffer'] !== undefined
237
+ output: Output,
238
+ ): output is HandlerPipeThroughBuffer {
239
+ // We only need to discriminate between possible Output values
240
+ return output != null && 'buffer' in output && output['buffer'] !== undefined
348
241
  }
349
242
 
350
243
  export function isHandlerPipeThroughStream(
351
- v: HandlerOutput,
352
- ): v is HandlerPipeThroughStream {
353
- // We only need to discriminate between possible HandlerOutput values
354
- return v['stream'] !== undefined
355
- }
356
-
357
- export class InvalidRequestError extends XRPCError {
358
- constructor(
359
- errorMessage?: string,
360
- customErrorName?: string,
361
- options?: ErrorOptions,
362
- ) {
363
- super(ResponseType.InvalidRequest, errorMessage, customErrorName, options)
364
- }
365
-
366
- [Symbol.hasInstance](instance: unknown): boolean {
367
- return (
368
- instance instanceof XRPCError &&
369
- instance.type === ResponseType.InvalidRequest
370
- )
371
- }
372
- }
373
-
374
- export class AuthRequiredError extends XRPCError {
375
- constructor(
376
- errorMessage?: string,
377
- customErrorName?: string,
378
- options?: ErrorOptions,
379
- ) {
380
- super(
381
- ResponseType.AuthenticationRequired,
382
- errorMessage,
383
- customErrorName,
384
- options,
385
- )
386
- }
387
-
388
- [Symbol.hasInstance](instance: unknown): boolean {
389
- return (
390
- instance instanceof XRPCError &&
391
- instance.type === ResponseType.AuthenticationRequired
392
- )
393
- }
394
- }
395
-
396
- export class ForbiddenError extends XRPCError {
397
- constructor(
398
- errorMessage?: string,
399
- customErrorName?: string,
400
- options?: ErrorOptions,
401
- ) {
402
- super(ResponseType.Forbidden, errorMessage, customErrorName, options)
403
- }
404
-
405
- [Symbol.hasInstance](instance: unknown): boolean {
406
- return (
407
- instance instanceof XRPCError && instance.type === ResponseType.Forbidden
408
- )
409
- }
410
- }
411
-
412
- export class RateLimitExceededError extends XRPCError {
413
- constructor(
414
- public status: RateLimiterStatus,
415
- errorMessage?: string,
416
- customErrorName?: string,
417
- options?: ErrorOptions,
418
- ) {
419
- super(
420
- ResponseType.RateLimitExceeded,
421
- errorMessage,
422
- customErrorName,
423
- options,
424
- )
425
- }
426
-
427
- [Symbol.hasInstance](instance: unknown): boolean {
428
- return (
429
- instance instanceof XRPCError &&
430
- instance.type === ResponseType.RateLimitExceeded
431
- )
432
- }
433
- }
434
-
435
- export class InternalServerError extends XRPCError {
436
- constructor(
437
- errorMessage?: string,
438
- customErrorName?: string,
439
- options?: ErrorOptions,
440
- ) {
441
- super(
442
- ResponseType.InternalServerError,
443
- errorMessage,
444
- customErrorName,
445
- options,
446
- )
447
- }
448
-
449
- [Symbol.hasInstance](instance: unknown): boolean {
450
- return (
451
- instance instanceof XRPCError &&
452
- instance.type === ResponseType.InternalServerError
453
- )
454
- }
455
- }
456
-
457
- export class UpstreamFailureError extends XRPCError {
458
- constructor(
459
- errorMessage?: string,
460
- customErrorName?: string,
461
- options?: ErrorOptions,
462
- ) {
463
- super(ResponseType.UpstreamFailure, errorMessage, customErrorName, options)
464
- }
465
-
466
- [Symbol.hasInstance](instance: unknown): boolean {
467
- return (
468
- instance instanceof XRPCError &&
469
- instance.type === ResponseType.UpstreamFailure
470
- )
471
- }
472
- }
473
-
474
- export class NotEnoughResourcesError extends XRPCError {
475
- constructor(
476
- errorMessage?: string,
477
- customErrorName?: string,
478
- options?: ErrorOptions,
479
- ) {
480
- super(
481
- ResponseType.NotEnoughResources,
482
- errorMessage,
483
- customErrorName,
484
- options,
485
- )
486
- }
487
-
488
- [Symbol.hasInstance](instance: unknown): boolean {
489
- return (
490
- instance instanceof XRPCError &&
491
- instance.type === ResponseType.NotEnoughResources
492
- )
493
- }
494
- }
495
-
496
- export class UpstreamTimeoutError extends XRPCError {
497
- constructor(
498
- errorMessage?: string,
499
- customErrorName?: string,
500
- options?: ErrorOptions,
501
- ) {
502
- super(ResponseType.UpstreamTimeout, errorMessage, customErrorName, options)
503
- }
504
-
505
- [Symbol.hasInstance](instance: unknown): boolean {
506
- return (
507
- instance instanceof XRPCError &&
508
- instance.type === ResponseType.UpstreamTimeout
509
- )
510
- }
511
- }
512
-
513
- export class MethodNotImplementedError extends XRPCError {
514
- constructor(
515
- errorMessage?: string,
516
- customErrorName?: string,
517
- options?: ErrorOptions,
518
- ) {
519
- super(
520
- ResponseType.MethodNotImplemented,
521
- errorMessage,
522
- customErrorName,
523
- options,
524
- )
525
- }
526
-
527
- [Symbol.hasInstance](instance: unknown): boolean {
528
- return (
529
- instance instanceof XRPCError &&
530
- instance.type === ResponseType.MethodNotImplemented
531
- )
532
- }
244
+ output: Output,
245
+ ): output is HandlerPipeThroughStream {
246
+ // We only need to discriminate between possible Output values
247
+ return output != null && 'stream' in output && output['stream'] !== undefined
533
248
  }