@orpc/server 2.0.0-beta.3 → 2.0.0-beta.30

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 (56) hide show
  1. package/README.md +71 -101
  2. package/dist/adapters/aws-lambda/index.d.mts +93 -0
  3. package/dist/adapters/aws-lambda/index.d.ts +93 -0
  4. package/dist/adapters/aws-lambda/index.mjs +66 -0
  5. package/dist/adapters/crossws/index.d.mts +17 -8
  6. package/dist/adapters/crossws/index.d.ts +17 -8
  7. package/dist/adapters/crossws/index.mjs +10 -7
  8. package/dist/adapters/fastify/index.d.mts +85 -0
  9. package/dist/adapters/fastify/index.d.ts +85 -0
  10. package/dist/adapters/fastify/index.mjs +66 -0
  11. package/dist/adapters/fetch/index.d.mts +16 -67
  12. package/dist/adapters/fetch/index.d.ts +16 -67
  13. package/dist/adapters/fetch/index.mjs +8 -144
  14. package/dist/adapters/message-port/index.d.mts +19 -16
  15. package/dist/adapters/message-port/index.d.ts +19 -16
  16. package/dist/adapters/message-port/index.mjs +28 -32
  17. package/dist/adapters/node/index.d.mts +15 -49
  18. package/dist/adapters/node/index.d.ts +15 -49
  19. package/dist/adapters/node/index.mjs +9 -120
  20. package/dist/adapters/standard/index.d.mts +6 -6
  21. package/dist/adapters/standard/index.d.ts +6 -6
  22. package/dist/adapters/standard/index.mjs +3 -3
  23. package/dist/adapters/standard-peer/index.d.mts +2 -2
  24. package/dist/adapters/standard-peer/index.d.ts +2 -2
  25. package/dist/adapters/websocket/index.d.mts +42 -26
  26. package/dist/adapters/websocket/index.d.ts +42 -26
  27. package/dist/adapters/websocket/index.mjs +33 -25
  28. package/dist/extensions/callable.d.mts +2 -2
  29. package/dist/extensions/callable.d.ts +2 -2
  30. package/dist/extensions/callable.mjs +2 -2
  31. package/dist/helpers/index.d.mts +23 -6
  32. package/dist/helpers/index.d.ts +23 -6
  33. package/dist/helpers/index.mjs +13 -5
  34. package/dist/index.d.mts +311 -16
  35. package/dist/index.d.ts +311 -16
  36. package/dist/index.mjs +126 -34
  37. package/dist/plugins/index.d.mts +236 -18
  38. package/dist/plugins/index.d.ts +236 -18
  39. package/dist/plugins/index.mjs +474 -15
  40. package/dist/shared/server.BhHrioCw.d.ts +34 -0
  41. package/dist/shared/{server.T9F3bzZx.d.ts → server.C2n16pp0.d.ts} +34 -9
  42. package/dist/shared/server.Cd4Z1hpV.mjs +69 -0
  43. package/dist/shared/{server.CrlKQucM.mjs → server.CkButhNT.mjs} +21 -38
  44. package/dist/shared/{server.B_U9y00a.d.mts → server.CkNnZ5F-.d.mts} +34 -9
  45. package/dist/shared/{server.BL22TloH.d.mts → server.CwrYlF72.d.mts} +31 -41
  46. package/dist/shared/{server.BL22TloH.d.ts → server.CwrYlF72.d.ts} +31 -41
  47. package/dist/shared/server.D0Ipbmdv.d.mts +34 -0
  48. package/dist/shared/{server.GDpX6Df8.mjs → server.D9VprDph.mjs} +82 -49
  49. package/dist/shared/{server.BwHnWUuN.mjs → server.Dh77P3ii.mjs} +17 -10
  50. package/dist/shared/{server.Pa0F03f_.d.ts → server.Dm0os-OP.d.mts} +24 -12
  51. package/dist/shared/{server.EOHJ3NJr.d.ts → server.DrN1Pj1-.d.ts} +3 -3
  52. package/dist/shared/{server.BsNNjG5J.d.mts → server.DtfwuV6U.d.ts} +24 -12
  53. package/dist/shared/{server.BB_Ik9Ph.d.mts → server.JbCIPL4P.d.mts} +3 -3
  54. package/dist/shared/{server.CjOb6ItT.mjs → server.eCTV8Vpp.mjs} +1 -1
  55. package/package.json +46 -12
  56. package/dist/shared/server.D_QauotT.mjs +0 -30
@@ -1,10 +1,11 @@
1
- import { toArray, value } from '@orpc/shared';
2
- import { flattenStandardHeader, parseStandardUrl, mergeStandardHeaders } from '@standardserver/core';
1
+ import { toArray, value, isNoTransformCacheControl, parseAcceptEncodingQualities, isCompressibleContentType, varyByAcceptEncoding, isAsyncIteratorObject, stringifyJSON, anyAbortSignal, override, wrapAsyncIterator, wrapReadableStream, AbortError } from '@orpc/shared';
2
+ import { flattenStandardHeader, parseStandardUrl, generateContentDisposition, mergeStandardHeaders } from '@standardserver/core';
3
3
  import { isClientPeerSendMessage, ServerPeer, encodePeerMessage } from '@standardserver/peer';
4
- export { C as CSRFGuardHandlerPlugin } from '../shared/server.D_QauotT.mjs';
5
- import { toFetchHeaders, toStandardHeaders } from '@standardserver/fetch';
4
+ import { toFetchHeaders, toStandardBody, toStandardHeaders } from '@standardserver/fetch';
5
+ export { R as RequestLimitHandlerPlugin } from '../shared/server.Cd4Z1hpV.mjs';
6
6
  import '@orpc/client';
7
7
 
8
+ const BATCH_CONTENT_TYPE = "application/vnd.orpc.batch";
8
9
  class BatchHandlerPlugin {
9
10
  name = "~batch";
10
11
  /**
@@ -16,6 +17,8 @@ class BatchHandlerPlugin {
16
17
  mapSubrequest;
17
18
  successStatus;
18
19
  headers;
20
+ keepAliveEnabled;
21
+ keepAliveInterval;
19
22
  constructor(options = {}) {
20
23
  this.maxSize = options.maxSize ?? 10;
21
24
  this.mapSubrequest = options.mapSubrequest ?? ((subRequest, { request: batchRequest }) => ({
@@ -29,6 +32,8 @@ class BatchHandlerPlugin {
29
32
  }));
30
33
  this.successStatus = options.successStatus ?? 207;
31
34
  this.headers = options.headers ?? {};
35
+ this.keepAliveEnabled = options.keepAlive?.enabled ?? true;
36
+ this.keepAliveInterval = options.keepAlive?.interval ?? 15e3;
32
37
  }
33
38
  init(options) {
34
39
  const routingInterceptor = async (interceptorOptions) => {
@@ -59,7 +64,7 @@ class BatchHandlerPlugin {
59
64
  messages = mightBeMessages;
60
65
  } else {
61
66
  const mightBeMessages = await interceptorOptions.request.resolveBody();
62
- if (!Array.isArray(mightBeMessages)) {
67
+ if (!Array.isArray(mightBeMessages) || mightBeMessages.some((m) => !isClientPeerSendMessage(m))) {
63
68
  return {
64
69
  matched: true,
65
70
  response: { status: 400, headers: {}, body: "Invalid batch request body" }
@@ -73,6 +78,13 @@ class BatchHandlerPlugin {
73
78
  response: { status: 400, headers: {}, body: "Invalid batch request" }
74
79
  };
75
80
  }
81
+ const outerMethod = interceptorOptions.request.method;
82
+ if ((outerMethod === "GET" || outerMethod === "QUERY") && messages.some((message) => message.kind === "request" && message.json.method !== outerMethod)) {
83
+ return {
84
+ matched: true,
85
+ response: { status: 400, headers: {}, body: `${outerMethod} batch requests only accept ${outerMethod} sub-requests` }
86
+ };
87
+ }
76
88
  const maxSize = await value(this.maxSize, interceptorOptions);
77
89
  if (messages.length > maxSize) {
78
90
  return {
@@ -117,7 +129,7 @@ class BatchHandlerPlugin {
117
129
  response: {
118
130
  status,
119
131
  headers,
120
- body: new Blob(chunks, { type: "application/octet-stream" })
132
+ body: new Blob(chunks, { type: BATCH_CONTENT_TYPE })
121
133
  }
122
134
  };
123
135
  }
@@ -127,9 +139,35 @@ class BatchHandlerPlugin {
127
139
  };
128
140
  }
129
141
  let streamController;
142
+ let keepAliveTimer;
143
+ const clearKeepAlive = () => {
144
+ if (keepAliveTimer !== void 0) {
145
+ clearInterval(keepAliveTimer);
146
+ keepAliveTimer = void 0;
147
+ }
148
+ };
149
+ const scheduleKeepAlive = () => {
150
+ if (!this.keepAliveEnabled) {
151
+ return;
152
+ }
153
+ clearKeepAlive();
154
+ keepAliveTimer = setInterval(() => {
155
+ try {
156
+ const lengthBuffer = new ArrayBuffer(4);
157
+ new DataView(lengthBuffer).setUint32(0, 0, false);
158
+ streamController.enqueue(new Uint8Array(lengthBuffer));
159
+ } catch {
160
+ clearKeepAlive();
161
+ }
162
+ }, this.keepAliveInterval);
163
+ };
130
164
  const stream = new ReadableStream({
131
165
  start(controller) {
132
166
  streamController = controller;
167
+ scheduleKeepAlive();
168
+ },
169
+ cancel() {
170
+ clearKeepAlive();
133
171
  }
134
172
  });
135
173
  const peer = new ServerPeer(async (message) => {
@@ -139,17 +177,24 @@ class BatchHandlerPlugin {
139
177
  new DataView(lengthBuffer).setUint32(0, bytes.byteLength, false);
140
178
  streamController.enqueue(new Uint8Array(lengthBuffer));
141
179
  streamController.enqueue(bytes);
180
+ scheduleKeepAlive();
142
181
  });
143
182
  Promise.all(messages.map((msg) => peer.message(msg, handleIndividualRequest))).then(async () => {
183
+ clearKeepAlive();
144
184
  streamController.close();
145
185
  await peer.close();
146
186
  }).catch(async (error) => {
187
+ clearKeepAlive();
147
188
  streamController.error(error);
148
189
  await peer.close(error);
149
190
  });
150
191
  return {
151
192
  matched: true,
152
- response: { status, headers, body: stream }
193
+ response: {
194
+ status,
195
+ headers: { ...headers, "content-type": BATCH_CONTENT_TYPE },
196
+ body: stream
197
+ }
153
198
  };
154
199
  };
155
200
  return {
@@ -171,7 +216,7 @@ class CORSHandlerPlugin {
171
216
  constructor(options = {}) {
172
217
  const defaults = {
173
218
  origin: (origin) => origin,
174
- allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"]
219
+ allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH", "QUERY"]
175
220
  };
176
221
  this.options = {
177
222
  ...defaults,
@@ -185,20 +230,23 @@ class CORSHandlerPlugin {
185
230
  return result;
186
231
  }
187
232
  const resHeaders = { ...result.response.headers };
188
- const origin = flattenStandardHeader(interceptorOptions.request.headers.origin) ?? "";
189
- const allowedOrigins = toArray(await value(this.options.origin, origin, interceptorOptions));
233
+ const origin = flattenStandardHeader(interceptorOptions.request.headers.origin);
234
+ const allowedOrigins = toArray(value(this.options.origin, origin, interceptorOptions));
190
235
  if (allowedOrigins.includes("*")) {
191
236
  resHeaders["access-control-allow-origin"] = "*";
192
237
  } else {
193
- if (allowedOrigins.includes(origin)) {
238
+ if (origin !== void 0 && allowedOrigins.includes(origin)) {
194
239
  resHeaders["access-control-allow-origin"] = origin;
195
240
  }
196
- resHeaders.vary = interceptorOptions.request.headers.vary ?? "origin";
241
+ const existingVary = flattenStandardHeader(resHeaders.vary);
242
+ if (!existingVary?.split(",").some((v) => v.trim().toLowerCase() === "origin")) {
243
+ resHeaders.vary = existingVary ? `${existingVary}, Origin` : "Origin";
244
+ }
197
245
  }
198
- const allowedTimingOrigins = toArray(await value(this.options.timingOrigin, origin, interceptorOptions));
246
+ const allowedTimingOrigins = toArray(value(this.options.timingOrigin, origin, interceptorOptions));
199
247
  if (allowedTimingOrigins.includes("*")) {
200
248
  resHeaders["timing-allow-origin"] = "*";
201
- } else if (allowedTimingOrigins.includes(origin)) {
249
+ } else if (origin !== void 0 && allowedTimingOrigins.includes(origin)) {
202
250
  resHeaders["timing-allow-origin"] = origin;
203
251
  }
204
252
  if (this.options.credentials) {
@@ -244,6 +292,147 @@ class CORSHandlerPlugin {
244
292
  }
245
293
  }
246
294
 
295
+ class GetMethodCsrfProtectionHandlerPlugin {
296
+ name = "~get-method-csrf-protection";
297
+ /** Judge the real request, before batch splits it into client-authored sub-requests. */
298
+ after = ["~batch"];
299
+ init(options) {
300
+ const routingInterceptor = (interceptorOptions) => {
301
+ if (this.isAllowed(interceptorOptions)) {
302
+ return interceptorOptions.next();
303
+ }
304
+ return Promise.resolve({
305
+ matched: true,
306
+ response: { status: 403, headers: {}, body: "Request blocked by CSRF protection." }
307
+ });
308
+ };
309
+ return {
310
+ ...options,
311
+ routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
312
+ };
313
+ }
314
+ isAllowed({ request }) {
315
+ if (request.method !== "GET") {
316
+ return true;
317
+ }
318
+ const site = flattenStandardHeader(request.headers["sec-fetch-site"])?.toLowerCase();
319
+ if (site === void 0 || site === "same-origin" || site === "same-site") {
320
+ return true;
321
+ }
322
+ const mode = flattenStandardHeader(request.headers["sec-fetch-mode"])?.toLowerCase();
323
+ const dest = flattenStandardHeader(request.headers["sec-fetch-dest"])?.toLowerCase();
324
+ if (mode === void 0 || dest === void 0) {
325
+ return false;
326
+ }
327
+ return mode !== "navigate" || dest !== "document";
328
+ }
329
+ }
330
+
331
+ class MethodOverrideHandlerPlugin {
332
+ name = "~method-override";
333
+ /**
334
+ * Should override batch sub-request methods, not the original batch request.
335
+ */
336
+ before = ["~batch"];
337
+ param;
338
+ methods;
339
+ constructor(options = {}) {
340
+ this.param = options.param ?? "method";
341
+ this.methods = new Set((options.methods ?? ["PUT", "PATCH", "DELETE"]).map((method) => method.toUpperCase()));
342
+ }
343
+ init(options) {
344
+ const routingInterceptor = async ({ next, ...interceptorOptions }) => {
345
+ const { request } = interceptorOptions;
346
+ if (request.method !== "POST") {
347
+ return next();
348
+ }
349
+ const [pathname, search, hash] = parseStandardUrl(request.url);
350
+ const params = new URLSearchParams(search);
351
+ const raw = params.getAll(this.param).at(-1);
352
+ if (raw === void 0) {
353
+ return next();
354
+ }
355
+ const method = raw.toUpperCase();
356
+ if (!this.methods.has(method)) {
357
+ return next();
358
+ }
359
+ params.delete(this.param);
360
+ const url = `${pathname}${params.size ? `?${params}` : ""}${hash ?? ""}`;
361
+ return next({
362
+ ...interceptorOptions,
363
+ request: { ...request, method, url }
364
+ });
365
+ };
366
+ return {
367
+ ...options,
368
+ routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
369
+ };
370
+ }
371
+ }
372
+
373
+ class RequestCompressionHandlerPlugin {
374
+ name = "~request-compression";
375
+ /**
376
+ * Should decompress the original batch request body instead of sub-requests.
377
+ */
378
+ after = ["~batch"];
379
+ init(options) {
380
+ const routingInterceptor = async ({ next, ...interceptorOptions }) => {
381
+ const encodings = parseContentEncodings(
382
+ flattenStandardHeader(interceptorOptions.request.headers["content-encoding"])
383
+ );
384
+ if (encodings.length === 0 || !encodings.every(isSupportedEncoding)) {
385
+ return next();
386
+ }
387
+ const decompressedHeaders = {
388
+ ...interceptorOptions.request.headers,
389
+ "content-length": void 0,
390
+ "content-encoding": void 0
391
+ };
392
+ return next({
393
+ ...interceptorOptions,
394
+ request: {
395
+ ...interceptorOptions.request,
396
+ headers: decompressedHeaders,
397
+ async resolveBody(hint) {
398
+ const stream = await interceptorOptions.request.resolveBody("octet-stream");
399
+ if (!(stream instanceof ReadableStream)) {
400
+ return stream;
401
+ }
402
+ let decompressedStream = stream;
403
+ for (let i = encodings.length - 1; i >= 0; i--) {
404
+ decompressedStream = decompressedStream.pipeThrough(
405
+ new DecompressionStream(encodings[i])
406
+ );
407
+ }
408
+ const response = new Response(decompressedStream, {
409
+ headers: toFetchHeaders(decompressedHeaders)
410
+ });
411
+ return toStandardBody(response, { hint });
412
+ }
413
+ }
414
+ });
415
+ };
416
+ return {
417
+ ...options,
418
+ routingInterceptors: [
419
+ routingInterceptor,
420
+ ...toArray(options.routingInterceptors)
421
+ ]
422
+ };
423
+ }
424
+ }
425
+ const SUPPORTED_ENCODINGS = ["gzip", "deflate", "deflate-raw"];
426
+ function isSupportedEncoding(encoding) {
427
+ return SUPPORTED_ENCODINGS.includes(encoding);
428
+ }
429
+ function parseContentEncodings(header) {
430
+ if (header === void 0) {
431
+ return [];
432
+ }
433
+ return header.split(",").map((part) => part.trim().toLowerCase());
434
+ }
435
+
247
436
  class RequestHeadersHandlerPlugin {
248
437
  name = "~request-headers";
249
438
  init(options) {
@@ -266,6 +455,175 @@ class RequestHeadersHandlerPlugin {
266
455
  }
267
456
  }
268
457
 
458
+ const AVG_BYTES_PER_CHAR = 1.2;
459
+ class ResponseCompressionHandlerPlugin {
460
+ name = "~response-compression";
461
+ /**
462
+ * Compression should be done after batching, to compress the final response.
463
+ * Compression should also be done after response headers are set, to access final headers like Content-Type and Cache-Control.
464
+ */
465
+ after = ["~batch", "~response-headers"];
466
+ encodings;
467
+ threshold;
468
+ constructor(options = {}) {
469
+ this.encodings = options.encodings ?? ["gzip", "deflate"];
470
+ this.threshold = options.threshold ?? 1024;
471
+ }
472
+ init(options) {
473
+ const routingInterceptor = async ({ next, ...interceptorOptions }) => {
474
+ const result = await next();
475
+ if (!result.matched) {
476
+ return result;
477
+ }
478
+ const response = result.response;
479
+ const contentEncoding = flattenStandardHeader(response.headers["content-encoding"])?.trim()?.toLowerCase();
480
+ if (contentEncoding !== void 0) {
481
+ return result;
482
+ }
483
+ if (response.status === 206 || response.headers["content-range"] !== void 0) {
484
+ return result;
485
+ }
486
+ if (isNoTransformCacheControl(flattenStandardHeader(response.headers["cache-control"]))) {
487
+ return result;
488
+ }
489
+ const acceptEncodings = parseAcceptEncodingQualities(
490
+ flattenStandardHeader(interceptorOptions.request.headers["accept-encoding"])
491
+ );
492
+ const encoding = this.encodings.find((enc) => (acceptEncodings.get(enc) ?? 0) > 0);
493
+ if (encoding === void 0) {
494
+ return result;
495
+ }
496
+ const body = response.body;
497
+ const headers = response.headers;
498
+ if (body instanceof ReadableStream) {
499
+ const contentLength = Number(flattenStandardHeader(headers["content-length"]));
500
+ if ((!Number.isFinite(contentLength) || contentLength >= this.threshold) && isCompressibleContentType(flattenStandardHeader(headers["content-type"]))) {
501
+ return {
502
+ ...result,
503
+ response: {
504
+ ...response,
505
+ body: body.pipeThrough(new CompressionStream(encoding)),
506
+ headers: {
507
+ ...headers,
508
+ "standard-server": "octet-stream",
509
+ "content-length": [],
510
+ "content-encoding": encoding,
511
+ "vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
512
+ }
513
+ }
514
+ };
515
+ }
516
+ } else if (body instanceof Blob) {
517
+ if ((!Number.isFinite(body.size) || body.size >= this.threshold) && isCompressibleContentType(body.type)) {
518
+ const contentDisposition = headers["content-disposition"] ?? generateContentDisposition(
519
+ body instanceof File ? body.name : "blob"
520
+ );
521
+ return {
522
+ ...result,
523
+ response: {
524
+ ...response,
525
+ body: body.stream().pipeThrough(new CompressionStream(encoding)),
526
+ headers: {
527
+ ...headers,
528
+ "standard-server": "file",
529
+ "content-type": body.type,
530
+ "content-length": [],
531
+ "content-disposition": contentDisposition,
532
+ "content-encoding": encoding,
533
+ "vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
534
+ }
535
+ }
536
+ };
537
+ }
538
+ } else if (body instanceof FormData) {
539
+ const PART_OVERHEAD = 64;
540
+ let contentLength = 0;
541
+ for (const [key, value] of body) {
542
+ contentLength += PART_OVERHEAD + key.length;
543
+ if (value instanceof Blob) {
544
+ if (!Number.isFinite(value.size)) {
545
+ if (!isCompressibleContentType(value.type)) {
546
+ contentLength = -Infinity;
547
+ break;
548
+ }
549
+ contentLength = Infinity;
550
+ } else {
551
+ contentLength += isCompressibleContentType(value.type) ? value.size : -value.size;
552
+ }
553
+ } else {
554
+ contentLength += value.length * AVG_BYTES_PER_CHAR;
555
+ }
556
+ }
557
+ if (contentLength >= this.threshold) {
558
+ const res = new Response(body);
559
+ const compressedStream = res.body.pipeThrough(new CompressionStream(encoding));
560
+ return {
561
+ ...result,
562
+ response: {
563
+ ...response,
564
+ body: compressedStream,
565
+ headers: {
566
+ ...headers,
567
+ "standard-server": [],
568
+ "content-type": res.headers.get("content-type"),
569
+ "content-length": [],
570
+ "content-encoding": encoding,
571
+ "vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
572
+ }
573
+ }
574
+ };
575
+ }
576
+ } else if (body instanceof URLSearchParams) {
577
+ const string = body.toString();
578
+ if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
579
+ return {
580
+ ...result,
581
+ response: {
582
+ ...response,
583
+ body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
584
+ headers: {
585
+ ...headers,
586
+ "standard-server": [],
587
+ "content-type": "application/x-www-form-urlencoded",
588
+ "content-length": [],
589
+ "content-encoding": encoding,
590
+ "vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
591
+ }
592
+ }
593
+ };
594
+ }
595
+ } else if (body !== void 0 && !isAsyncIteratorObject(body)) {
596
+ const string = stringifyJSON(body);
597
+ if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
598
+ return {
599
+ ...result,
600
+ response: {
601
+ ...response,
602
+ body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
603
+ headers: {
604
+ ...headers,
605
+ "standard-server": [],
606
+ "content-type": "application/json",
607
+ "content-length": [],
608
+ "content-encoding": encoding,
609
+ "vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
610
+ }
611
+ }
612
+ };
613
+ }
614
+ }
615
+ return result;
616
+ };
617
+ return {
618
+ ...options,
619
+ routingInterceptors: [
620
+ routingInterceptor,
621
+ ...toArray(options.routingInterceptors)
622
+ ]
623
+ };
624
+ }
625
+ }
626
+
269
627
  class ResponseHeadersHandlerPlugin {
270
628
  name = "~response-headers";
271
629
  /**
@@ -302,4 +660,105 @@ class ResponseHeadersHandlerPlugin {
302
660
  }
303
661
  }
304
662
 
305
- export { BatchHandlerPlugin, CORSHandlerPlugin, RequestHeadersHandlerPlugin, ResponseHeadersHandlerPlugin };
663
+ class RethrowHandlerPlugin {
664
+ name = "~rethrow";
665
+ filter;
666
+ CONTEXT_SYMBOL = Symbol("ORPC_RETHROW_HANDLER_PLUGIN_CONTEXT");
667
+ constructor(options) {
668
+ this.filter = options.filter;
669
+ }
670
+ init(options) {
671
+ const routingInterceptor = async (options2) => {
672
+ const pluginContext = {};
673
+ const result = await options2.next({
674
+ ...options2,
675
+ context: {
676
+ ...options2.context,
677
+ [this.CONTEXT_SYMBOL]: pluginContext
678
+ }
679
+ });
680
+ if (pluginContext.error) {
681
+ throw pluginContext.error.value;
682
+ }
683
+ return result;
684
+ };
685
+ const interceptor = async (options2) => {
686
+ const pluginContext = options2.context[this.CONTEXT_SYMBOL];
687
+ if (!pluginContext) {
688
+ throw new TypeError("[RethrowHandlerPlugin] Rethrow handler context has been corrupted or modified by another plugin or interceptor");
689
+ }
690
+ try {
691
+ return await options2.next();
692
+ } catch (error) {
693
+ if (this.filter(error, options2)) {
694
+ pluginContext.error = { value: error };
695
+ }
696
+ throw error;
697
+ }
698
+ };
699
+ return {
700
+ ...options,
701
+ routingInterceptors: [
702
+ ...toArray(options.routingInterceptors),
703
+ routingInterceptor
704
+ // rethrow as early as possible and keep it close to `interceptor` to avoid a corrupted or modified context
705
+ ],
706
+ interceptors: [
707
+ interceptor,
708
+ // catch as many errors as possible
709
+ ...toArray(options.interceptors)
710
+ ]
711
+ };
712
+ }
713
+ }
714
+
715
+ class TimeoutHandlerPlugin {
716
+ timeout;
717
+ streamingTimeout;
718
+ name = "~timeout";
719
+ constructor(options) {
720
+ this.timeout = options.timeout;
721
+ this.streamingTimeout = options.streamingTimeout;
722
+ }
723
+ init(options) {
724
+ const interceptor = async (interceptorOptions) => {
725
+ const timeoutMs = value(this.timeout, interceptorOptions);
726
+ const streamingTimeoutMs = value(this.streamingTimeout, interceptorOptions);
727
+ const hasTimeout = timeoutMs !== null && timeoutMs !== void 0;
728
+ const hasStreamingTimeout = streamingTimeoutMs !== null && streamingTimeoutMs !== void 0;
729
+ if (!hasTimeout && !hasStreamingTimeout) {
730
+ return interceptorOptions.next();
731
+ }
732
+ const controller = new AbortController();
733
+ const abortWithTimeout = (ms) => {
734
+ controller.abort(new AbortError(`Request timed out after ${ms}ms`));
735
+ };
736
+ const timeoutId = hasTimeout ? setTimeout(abortWithTimeout, timeoutMs, timeoutMs) : void 0;
737
+ const signal = anyAbortSignal([interceptorOptions.request.signal, controller.signal]);
738
+ try {
739
+ const response = await interceptorOptions.next({
740
+ ...interceptorOptions,
741
+ request: { ...interceptorOptions.request, signal }
742
+ });
743
+ if (hasStreamingTimeout) {
744
+ const body = response.body;
745
+ const isIterator = isAsyncIteratorObject(body);
746
+ if (isIterator || body instanceof ReadableStream) {
747
+ const streamingTimeoutId = setTimeout(abortWithTimeout, streamingTimeoutMs, streamingTimeoutMs);
748
+ const onFinish = () => clearTimeout(streamingTimeoutId);
749
+ return {
750
+ ...response,
751
+ body: isIterator ? override(body, wrapAsyncIterator(body, { onFinish })) : override(body, wrapReadableStream(body, { onFinish }))
752
+ };
753
+ }
754
+ }
755
+ return response;
756
+ } finally {
757
+ clearTimeout(timeoutId);
758
+ }
759
+ };
760
+ return { ...options, interceptors: [interceptor, ...toArray(options.interceptors)] };
761
+ }
762
+ }
763
+
764
+ export { BATCH_CONTENT_TYPE, BatchHandlerPlugin, CORSHandlerPlugin, CORSHandlerPlugin as CORSPlugin, GetMethodCsrfProtectionHandlerPlugin, MethodOverrideHandlerPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestHeadersHandlerPlugin as RequestHeadersPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, ResponseHeadersHandlerPlugin as ResponseHeadersPlugin, RethrowHandlerPlugin, TimeoutHandlerPlugin };
@@ -0,0 +1,34 @@
1
+ import { C as Context } from './server.CwrYlF72.js';
2
+ import { S as StandardHandlerPlugin, a as StandardHandlerOptions } from './server.DrN1Pj1-.js';
3
+
4
+ interface RequestLimitHandlerPluginOptions {
5
+ /**
6
+ * The maximum allowed request body size in bytes.
7
+ */
8
+ maxBodySize: number;
9
+ }
10
+ /**
11
+ * Rejects requests whose body exceeds `maxBodySize`.
12
+ *
13
+ * When used with the request compression plugin, the limit applies to the
14
+ * decompressed payload rather than the compressed wire size.
15
+ *
16
+ * @see {@link https://orpc.dev/docs/plugins/request-limit | Request Limit Plugin}
17
+ */
18
+ declare class RequestLimitHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
19
+ name: string;
20
+ /**
21
+ * Should limit the original batch request body instead of sub-requests.
22
+ */
23
+ after: string[];
24
+ /**
25
+ * Should limit the final body size instead of the compressed one.
26
+ */
27
+ before: string[];
28
+ private readonly maxBodySize;
29
+ constructor(options: RequestLimitHandlerPluginOptions);
30
+ init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
31
+ }
32
+
33
+ export { RequestLimitHandlerPlugin as R };
34
+ export type { RequestLimitHandlerPluginOptions as a };