@orpc/server 1.14.11 → 1.14.13

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 (78) hide show
  1. package/README.md +98 -52
  2. package/dist/adapters/aws-lambda/index.d.mts +46 -0
  3. package/dist/adapters/aws-lambda/index.d.ts +46 -0
  4. package/dist/adapters/aws-lambda/index.mjs +40 -0
  5. package/dist/adapters/bun-ws/index.d.mts +36 -0
  6. package/dist/adapters/bun-ws/index.d.ts +36 -0
  7. package/dist/adapters/bun-ws/index.mjs +47 -0
  8. package/dist/adapters/crossws/index.d.mts +21 -45
  9. package/dist/adapters/crossws/index.d.ts +21 -45
  10. package/dist/adapters/crossws/index.mjs +18 -40
  11. package/dist/adapters/fastify/index.d.mts +53 -0
  12. package/dist/adapters/fastify/index.d.ts +53 -0
  13. package/dist/adapters/fastify/index.mjs +52 -0
  14. package/dist/adapters/fetch/index.d.mts +96 -64
  15. package/dist/adapters/fetch/index.d.ts +96 -64
  16. package/dist/adapters/fetch/index.mjs +139 -34
  17. package/dist/adapters/message-port/index.d.mts +34 -49
  18. package/dist/adapters/message-port/index.d.ts +34 -49
  19. package/dist/adapters/message-port/index.mjs +37 -68
  20. package/dist/adapters/node/index.d.mts +71 -63
  21. package/dist/adapters/node/index.d.ts +71 -63
  22. package/dist/adapters/node/index.mjs +113 -36
  23. package/dist/adapters/standard/index.d.mts +18 -16
  24. package/dist/adapters/standard/index.d.ts +18 -16
  25. package/dist/adapters/standard/index.mjs +5 -5
  26. package/dist/adapters/standard-peer/index.d.mts +14 -12
  27. package/dist/adapters/standard-peer/index.d.ts +14 -12
  28. package/dist/adapters/standard-peer/index.mjs +21 -2
  29. package/dist/adapters/websocket/index.d.mts +38 -53
  30. package/dist/adapters/websocket/index.d.ts +38 -53
  31. package/dist/adapters/websocket/index.mjs +42 -59
  32. package/dist/adapters/ws/index.d.mts +31 -0
  33. package/dist/adapters/ws/index.d.ts +31 -0
  34. package/dist/adapters/ws/index.mjs +41 -0
  35. package/dist/helpers/index.d.mts +4 -4
  36. package/dist/helpers/index.d.ts +4 -4
  37. package/dist/helpers/index.mjs +5 -13
  38. package/dist/hibernation/index.d.mts +44 -0
  39. package/dist/hibernation/index.d.ts +44 -0
  40. package/dist/hibernation/index.mjs +65 -0
  41. package/dist/index.d.mts +770 -166
  42. package/dist/index.d.ts +770 -166
  43. package/dist/index.mjs +399 -306
  44. package/dist/plugins/index.d.mts +121 -214
  45. package/dist/plugins/index.d.ts +121 -214
  46. package/dist/plugins/index.mjs +214 -576
  47. package/dist/shared/server.7cEtMB30.d.ts +74 -0
  48. package/dist/shared/server.B8gYOD5g.d.mts +12 -0
  49. package/dist/shared/server.BqadksTP.d.mts +74 -0
  50. package/dist/shared/server.C8_sRzQB.d.mts +42 -0
  51. package/dist/shared/server.CMf4nKky.mjs +230 -0
  52. package/dist/shared/server.ChyoA9XY.d.ts +42 -0
  53. package/dist/shared/server.D8IXzfqT.d.ts +32 -0
  54. package/dist/shared/server.DEBcqOjg.mjs +418 -0
  55. package/dist/shared/{server.W91HSRkE.mjs → server.DZ5BIITo.mjs} +2 -2
  56. package/dist/shared/server.DzE6WeuY.d.mts +32 -0
  57. package/dist/shared/server.EfTOZ2Q7.d.ts +12 -0
  58. package/dist/shared/server.TEVCLCFC.mjs +39 -0
  59. package/dist/shared/server.qKsRrdxW.d.mts +193 -0
  60. package/dist/shared/server.qKsRrdxW.d.ts +193 -0
  61. package/package.json +56 -26
  62. package/dist/extensions/callable.d.mts +0 -10
  63. package/dist/extensions/callable.d.ts +0 -10
  64. package/dist/extensions/callable.mjs +0 -11
  65. package/dist/shared/server.15O7oC1p.d.mts +0 -61
  66. package/dist/shared/server.B4BnEWWm.d.mts +0 -67
  67. package/dist/shared/server.BhsVw7m7.d.ts +0 -104
  68. package/dist/shared/server.BoiFpNT7.mjs +0 -224
  69. package/dist/shared/server.COPVXhDG.mjs +0 -271
  70. package/dist/shared/server.CX4vUnDk.mjs +0 -11
  71. package/dist/shared/server.CcR4kgje.d.mts +0 -206
  72. package/dist/shared/server.CcR4kgje.d.ts +0 -206
  73. package/dist/shared/server.CnN50DHH.d.ts +0 -67
  74. package/dist/shared/server.DBs0rrf9.mjs +0 -41
  75. package/dist/shared/server.DDFizwfU.d.mts +0 -104
  76. package/dist/shared/server.D_QauotT.mjs +0 -30
  77. package/dist/shared/server.DofEOwf-.mjs +0 -237
  78. package/dist/shared/server.m3szVJGU.d.ts +0 -61
@@ -1,206 +1,124 @@
1
- import { toArray, value, isCompressibleContentType, isAsyncIteratorObject, stringifyJSON } from '@orpc/shared';
2
- import { flattenStandardHeader, parseStandardUrl, generateContentDisposition, mergeStandardHeaders } from '@standardserver/core';
3
- import { isClientPeerSendMessage, ServerPeer, encodePeerMessage } from '@standardserver/peer';
4
- export { C as CSRFGuardHandlerPlugin } from '../shared/server.D_QauotT.mjs';
5
- import { toFetchHeaders, toStandardBody, toStandardHeaders } from '@standardserver/fetch';
1
+ import { runWithSpan, value, setSpanError, isAsyncIteratorObject, AsyncIteratorClass, clone } from '@orpc/shared';
2
+ import { flattenHeader } from '@orpc/standard-server';
3
+ import { parseBatchRequest, toBatchResponse } from '@orpc/standard-server/batch';
4
+ import { toFetchHeaders } from '@orpc/standard-server-fetch';
6
5
  import { ORPCError } from '@orpc/client';
6
+ export { S as StrictGetMethodPlugin } from '../shared/server.TEVCLCFC.mjs';
7
+ import '@orpc/contract';
7
8
 
8
9
  class BatchHandlerPlugin {
9
- name = "~batch";
10
- /**
11
- * Run batch interceptors before OpenTelemetry interceptors
12
- * so each subrequest gets its own span instead of sharing one batch-level span.
13
- */
14
- after = ["~opentelemetry"];
15
10
  maxSize;
16
- mapSubrequest;
11
+ mapRequestItem;
17
12
  successStatus;
18
13
  headers;
19
- keepAliveEnabled;
20
- keepAliveInterval;
14
+ order = 5e6;
21
15
  constructor(options = {}) {
22
16
  this.maxSize = options.maxSize ?? 10;
23
- this.mapSubrequest = options.mapSubrequest ?? ((subRequest, { request: batchRequest }) => ({
24
- ...subRequest,
17
+ this.mapRequestItem = options.mapRequestItem ?? ((request, { request: batchRequest }) => ({
18
+ ...request,
25
19
  headers: {
26
20
  ...batchRequest.headers,
27
- ...subRequest.headers,
28
- "orpc-batch": void 0
29
- // useful in case batch plugin is used multiple times
21
+ ...request.headers
30
22
  }
31
23
  }));
32
24
  this.successStatus = options.successStatus ?? 207;
33
25
  this.headers = options.headers ?? {};
34
- this.keepAliveEnabled = options.keepAlive?.enabled ?? true;
35
- this.keepAliveInterval = options.keepAlive?.interval ?? 15e3;
36
26
  }
37
27
  init(options) {
38
- const routingInterceptor = async (interceptorOptions) => {
39
- const batchHeader = flattenStandardHeader(interceptorOptions.request.headers["orpc-batch"]);
40
- if (batchHeader === void 0) {
41
- return interceptorOptions.next();
28
+ options.rootInterceptors ??= [];
29
+ options.rootInterceptors.unshift(async (options2) => {
30
+ const xHeader = flattenHeader(options2.request.headers["x-orpc-batch"]);
31
+ if (xHeader === void 0) {
32
+ return options2.next();
42
33
  }
43
- const mode = batchHeader === "buffered" ? "buffered" : "streaming";
44
- let messages;
34
+ let isParsing = false;
45
35
  try {
46
- if (interceptorOptions.request.method === "GET") {
47
- const [, search] = parseStandardUrl(interceptorOptions.request.url);
48
- const params = new URLSearchParams(search);
49
- const data = params.getAll("data").at(-1);
50
- if (!data) {
36
+ return await runWithSpan({ name: "handle_batch_request" }, async (span) => {
37
+ const mode = xHeader === "buffered" ? "buffered" : "streaming";
38
+ isParsing = true;
39
+ const parsed = parseBatchRequest({ ...options2.request, body: await options2.request.body() });
40
+ isParsing = false;
41
+ span?.setAttribute("batch.mode", mode);
42
+ span?.setAttribute("batch.size", parsed.length);
43
+ const maxSize = await value(this.maxSize, options2);
44
+ if (parsed.length > maxSize) {
45
+ const message = "Batch request size exceeds the maximum allowed size";
46
+ setSpanError(span, message);
51
47
  return {
52
48
  matched: true,
53
- response: { status: 400, headers: {}, body: "Missing data parameter for batch request" }
54
- };
55
- }
56
- const mightBeMessages = JSON.parse(data);
57
- if (!Array.isArray(mightBeMessages) || mightBeMessages.some((m) => !isClientPeerSendMessage(m))) {
58
- return {
59
- matched: true,
60
- response: { status: 400, headers: {}, body: "Invalid batch request data parameter" }
61
- };
62
- }
63
- messages = mightBeMessages;
64
- } else {
65
- const mightBeMessages = await interceptorOptions.request.resolveBody();
66
- if (!Array.isArray(mightBeMessages)) {
67
- return {
68
- matched: true,
69
- response: { status: 400, headers: {}, body: "Invalid batch request body" }
49
+ response: {
50
+ status: 413,
51
+ headers: {},
52
+ body: message
53
+ }
70
54
  };
71
55
  }
72
- messages = mightBeMessages;
73
- }
74
- } catch {
75
- return {
76
- matched: true,
77
- response: { status: 400, headers: {}, body: "Invalid batch request" }
78
- };
79
- }
80
- const maxSize = await value(this.maxSize, interceptorOptions);
81
- if (messages.length > maxSize) {
82
- return {
83
- matched: true,
84
- response: { status: 413, headers: {}, body: "Batch request size exceeds the maximum allowed size" }
85
- };
86
- }
87
- const handleIndividualRequest = async (request) => {
88
- try {
89
- request = this.mapSubrequest(request, interceptorOptions);
90
- const { matched, response } = await interceptorOptions.next({ ...interceptorOptions, request });
91
- if (!matched) {
92
- return { status: 404, headers: {}, body: "No procedure matched" };
93
- }
94
- return response;
95
- } catch (err) {
96
- Promise.reject(err);
97
- return { status: 500, headers: {}, body: "Internal server error" };
98
- }
99
- };
100
- const status = await value(this.successStatus, interceptorOptions);
101
- const headers = await value(this.headers, interceptorOptions);
102
- if (mode === "buffered") {
103
- const responseMessages = [];
104
- const peer2 = new ServerPeer(async (message) => {
105
- responseMessages.push(message);
56
+ const responses = parsed.map(
57
+ (request, index) => {
58
+ const mapped = this.mapRequestItem(request, options2);
59
+ return options2.next({ ...options2, request: { ...mapped, body: () => Promise.resolve(mapped.body) } }).then(({ response: response2, matched }) => {
60
+ span?.addEvent(`response.${index}.${matched ? "success" : "not_matched"}`);
61
+ if (matched) {
62
+ if (response2.body instanceof Blob || response2.body instanceof FormData || isAsyncIteratorObject(response2.body)) {
63
+ return {
64
+ index,
65
+ status: 500,
66
+ headers: {},
67
+ body: "Batch responses do not support file/blob, or event-iterator. Please call this procedure separately outside of the batch request."
68
+ };
69
+ }
70
+ return { ...response2, index };
71
+ }
72
+ return { index, status: 404, headers: {}, body: "No procedure matched" };
73
+ }).catch((err) => {
74
+ Promise.reject(err);
75
+ return { index, status: 500, headers: {}, body: "Internal server error" };
76
+ });
77
+ }
78
+ );
79
+ await Promise.race(responses);
80
+ const status = await value(this.successStatus, responses, options2);
81
+ const headers = await value(this.headers, responses, options2);
82
+ const promises = [...responses];
83
+ const response = await toBatchResponse({
84
+ status,
85
+ headers,
86
+ mode,
87
+ body: new AsyncIteratorClass(
88
+ async () => {
89
+ const handling = promises.filter((p) => p !== void 0);
90
+ if (handling.length <= 0) {
91
+ return { done: true, value: void 0 };
92
+ }
93
+ const value2 = await Promise.race(handling);
94
+ promises[value2.index] = void 0;
95
+ return { done: false, value: value2 };
96
+ },
97
+ async () => {
98
+ }
99
+ )
100
+ });
101
+ return {
102
+ matched: true,
103
+ response
104
+ };
106
105
  });
107
- await Promise.all(messages.map((msg) => peer2.message(msg, handleIndividualRequest)));
108
- await peer2.close();
109
- if (responseMessages.some((msg) => msg.binary !== void 0)) {
110
- const chunks = [];
111
- for (const message of responseMessages) {
112
- const encoded = await encodePeerMessage(message);
113
- const bytes = typeof encoded === "string" ? new TextEncoder().encode(encoded) : encoded;
114
- const lengthBuffer = new ArrayBuffer(4);
115
- new DataView(lengthBuffer).setUint32(0, bytes.byteLength, false);
116
- chunks.push(new Uint8Array(lengthBuffer));
117
- chunks.push(bytes);
118
- }
106
+ } catch (cause) {
107
+ if (isParsing) {
119
108
  return {
120
109
  matched: true,
121
- response: {
122
- status,
123
- headers,
124
- body: new Blob(chunks, { type: "application/octet-stream" })
125
- }
110
+ response: { status: 400, headers: {}, body: "Invalid batch request, this could be caused by a malformed request body or a missing header" }
126
111
  };
127
112
  }
128
- return {
129
- matched: true,
130
- response: { status, headers, body: responseMessages }
131
- };
113
+ throw cause;
132
114
  }
133
- let streamController;
134
- let keepAliveTimer;
135
- const clearKeepAlive = () => {
136
- if (keepAliveTimer !== void 0) {
137
- clearInterval(keepAliveTimer);
138
- keepAliveTimer = void 0;
139
- }
140
- };
141
- const scheduleKeepAlive = () => {
142
- if (!this.keepAliveEnabled) {
143
- return;
144
- }
145
- clearKeepAlive();
146
- keepAliveTimer = setInterval(() => {
147
- try {
148
- const lengthBuffer = new ArrayBuffer(4);
149
- new DataView(lengthBuffer).setUint32(0, 0, false);
150
- streamController.enqueue(new Uint8Array(lengthBuffer));
151
- } catch {
152
- clearKeepAlive();
153
- }
154
- }, this.keepAliveInterval);
155
- };
156
- const stream = new ReadableStream({
157
- start(controller) {
158
- streamController = controller;
159
- scheduleKeepAlive();
160
- },
161
- cancel() {
162
- clearKeepAlive();
163
- }
164
- });
165
- const peer = new ServerPeer(async (message) => {
166
- const encoded = await encodePeerMessage(message);
167
- const bytes = typeof encoded === "string" ? new TextEncoder().encode(encoded) : encoded;
168
- const lengthBuffer = new ArrayBuffer(4);
169
- new DataView(lengthBuffer).setUint32(0, bytes.byteLength, false);
170
- streamController.enqueue(new Uint8Array(lengthBuffer));
171
- streamController.enqueue(bytes);
172
- scheduleKeepAlive();
173
- });
174
- Promise.all(messages.map((msg) => peer.message(msg, handleIndividualRequest))).then(async () => {
175
- clearKeepAlive();
176
- streamController.close();
177
- await peer.close();
178
- }).catch(async (error) => {
179
- clearKeepAlive();
180
- streamController.error(error);
181
- await peer.close(error);
182
- });
183
- return {
184
- matched: true,
185
- response: { status, headers, body: stream }
186
- };
187
- };
188
- return {
189
- ...options,
190
- routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
191
- };
115
+ });
192
116
  }
193
117
  }
194
118
 
195
- class CORSHandlerPlugin {
119
+ class CORSPlugin {
196
120
  options;
197
- name = "~cors";
198
- /**
199
- * - Do not create spans for CORS preflight requests.
200
- * - Run CORS interceptors before batch interceptors so headers are applied to
201
- * the actual response rather than sub-responses.
202
- */
203
- after = ["~opentelemetry", "~batch"];
121
+ order = 9e6;
204
122
  constructor(options = {}) {
205
123
  const defaults = {
206
124
  origin: (origin) => origin,
@@ -212,51 +130,19 @@ class CORSHandlerPlugin {
212
130
  };
213
131
  }
214
132
  init(options) {
215
- const corsHeadersInterceptor = async (interceptorOptions) => {
216
- const result = await interceptorOptions.next();
217
- if (!result.matched) {
218
- return result;
219
- }
220
- const resHeaders = { ...result.response.headers };
221
- const origin = flattenStandardHeader(interceptorOptions.request.headers.origin) ?? "";
222
- const allowedOrigins = toArray(await value(this.options.origin, origin, interceptorOptions));
223
- if (allowedOrigins.includes("*")) {
224
- resHeaders["access-control-allow-origin"] = "*";
225
- } else {
226
- if (allowedOrigins.includes(origin)) {
227
- resHeaders["access-control-allow-origin"] = origin;
228
- }
229
- const existingVary = flattenStandardHeader(resHeaders.vary);
230
- if (!existingVary?.split(",").some((v) => v.trim().toLowerCase() === "origin")) {
231
- resHeaders.vary = existingVary ? `${existingVary}, Origin` : "Origin";
232
- }
233
- }
234
- const allowedTimingOrigins = toArray(await value(this.options.timingOrigin, origin, interceptorOptions));
235
- if (allowedTimingOrigins.includes("*")) {
236
- resHeaders["timing-allow-origin"] = "*";
237
- } else if (allowedTimingOrigins.includes(origin)) {
238
- resHeaders["timing-allow-origin"] = origin;
239
- }
240
- if (this.options.credentials) {
241
- resHeaders["access-control-allow-credentials"] = "true";
242
- }
243
- if (this.options.exposeHeaders?.length) {
244
- resHeaders["access-control-expose-headers"] = flattenStandardHeader(this.options.exposeHeaders);
245
- }
246
- return { ...result, response: { ...result.response, headers: resHeaders } };
247
- };
248
- const preflightInterceptor = async (interceptorOptions) => {
133
+ options.rootInterceptors ??= [];
134
+ options.rootInterceptors.unshift(async (interceptorOptions) => {
249
135
  if (interceptorOptions.request.method === "OPTIONS") {
250
136
  const resHeaders = {};
251
137
  if (this.options.maxAge !== void 0) {
252
138
  resHeaders["access-control-max-age"] = this.options.maxAge.toString();
253
139
  }
254
140
  if (this.options.allowMethods?.length) {
255
- resHeaders["access-control-allow-methods"] = flattenStandardHeader(this.options.allowMethods);
141
+ resHeaders["access-control-allow-methods"] = flattenHeader(this.options.allowMethods);
256
142
  }
257
143
  const allowHeaders = this.options.allowHeaders ?? interceptorOptions.request.headers["access-control-request-headers"];
258
144
  if (typeof allowHeaders === "string" || allowHeaders?.length) {
259
- resHeaders["access-control-allow-headers"] = flattenStandardHeader(allowHeaders);
145
+ resHeaders["access-control-allow-headers"] = flattenHeader(allowHeaders);
260
146
  }
261
147
  return {
262
148
  matched: true,
@@ -268,385 +154,106 @@ class CORSHandlerPlugin {
268
154
  };
269
155
  }
270
156
  return interceptorOptions.next();
271
- };
272
- return {
273
- ...options,
274
- routingInterceptors: [
275
- corsHeadersInterceptor,
276
- preflightInterceptor,
277
- ...toArray(options.routingInterceptors)
278
- ]
279
- };
157
+ });
158
+ options.rootInterceptors.unshift(async (interceptorOptions) => {
159
+ const result = await interceptorOptions.next();
160
+ if (!result.matched) {
161
+ return result;
162
+ }
163
+ const origin = flattenHeader(interceptorOptions.request.headers.origin) ?? "";
164
+ const allowedOrigin = await value(this.options.origin, origin, interceptorOptions);
165
+ const allowedOriginArr = Array.isArray(allowedOrigin) ? allowedOrigin : [allowedOrigin];
166
+ if (allowedOriginArr.includes("*")) {
167
+ result.response.headers["access-control-allow-origin"] = "*";
168
+ } else {
169
+ if (allowedOriginArr.includes(origin)) {
170
+ result.response.headers["access-control-allow-origin"] = origin;
171
+ }
172
+ const existingVary = flattenHeader(result.response.headers.vary);
173
+ if (!existingVary?.split(",").some((v) => v.trim().toLowerCase() === "origin")) {
174
+ result.response.headers.vary = existingVary ? `${existingVary}, Origin` : "Origin";
175
+ }
176
+ }
177
+ const allowedTimingOrigin = await value(this.options.timingOrigin, origin, interceptorOptions);
178
+ const allowedTimingOriginArr = Array.isArray(allowedTimingOrigin) ? allowedTimingOrigin : [allowedTimingOrigin];
179
+ if (allowedTimingOriginArr.includes("*")) {
180
+ result.response.headers["timing-allow-origin"] = "*";
181
+ } else if (allowedTimingOriginArr.includes(origin)) {
182
+ result.response.headers["timing-allow-origin"] = origin;
183
+ }
184
+ if (this.options.credentials) {
185
+ result.response.headers["access-control-allow-credentials"] = "true";
186
+ }
187
+ if (this.options.exposeHeaders?.length) {
188
+ result.response.headers["access-control-expose-headers"] = flattenHeader(this.options.exposeHeaders);
189
+ }
190
+ return result;
191
+ });
280
192
  }
281
193
  }
282
194
 
283
- class RequestCompressionHandlerPlugin {
284
- name = "~request-compression";
285
- /**
286
- * Should decompress the original batch request body instead of sub-requests.
287
- */
288
- after = ["~batch"];
195
+ class RequestHeadersPlugin {
289
196
  init(options) {
290
- const routingInterceptor = async ({ next, ...interceptorOptions }) => {
291
- const encodings = parseContentEncodings(
292
- flattenStandardHeader(interceptorOptions.request.headers["content-encoding"])
293
- );
294
- if (encodings.length === 0 || !encodings.every(isSupportedEncoding)) {
295
- return next();
296
- }
297
- const decompressedHeaders = {
298
- ...interceptorOptions.request.headers,
299
- "content-length": void 0,
300
- "content-encoding": void 0
301
- };
302
- return next({
197
+ options.rootInterceptors ??= [];
198
+ options.rootInterceptors.push((interceptorOptions) => {
199
+ const reqHeaders = interceptorOptions.context.reqHeaders ?? toFetchHeaders(interceptorOptions.request.headers);
200
+ return interceptorOptions.next({
303
201
  ...interceptorOptions,
304
- request: {
305
- ...interceptorOptions.request,
306
- headers: decompressedHeaders,
307
- async resolveBody(hint) {
308
- const stream = await interceptorOptions.request.resolveBody("octet-stream");
309
- if (!(stream instanceof ReadableStream)) {
310
- return stream;
311
- }
312
- let decompressedStream = stream;
313
- for (let i = encodings.length - 1; i >= 0; i--) {
314
- decompressedStream = decompressedStream.pipeThrough(
315
- new DecompressionStream(encodings[i])
316
- );
317
- }
318
- const response = new Response(decompressedStream, {
319
- headers: toFetchHeaders(decompressedHeaders)
320
- });
321
- return toStandardBody(response, { hint });
322
- }
202
+ context: {
203
+ ...interceptorOptions.context,
204
+ reqHeaders
323
205
  }
324
206
  });
325
- };
326
- return {
327
- ...options,
328
- routingInterceptors: [
329
- routingInterceptor,
330
- ...toArray(options.routingInterceptors)
331
- ]
332
- };
207
+ });
333
208
  }
334
209
  }
335
- const SUPPORTED_ENCODINGS = ["gzip", "deflate", "deflate-raw"];
336
- function isSupportedEncoding(encoding) {
337
- return SUPPORTED_ENCODINGS.includes(encoding);
338
- }
339
- function parseContentEncodings(header) {
340
- if (header === void 0) {
341
- return [];
342
- }
343
- return header.split(",").map((part) => part.trim().toLowerCase());
344
- }
345
210
 
346
- class RequestHeadersHandlerPlugin {
347
- name = "~request-headers";
211
+ class ResponseHeadersPlugin {
348
212
  init(options) {
349
- return {
350
- ...options,
351
- interceptors: [
352
- (interceptorOptions) => {
353
- const reqHeaders = interceptorOptions.context.reqHeaders ?? toFetchHeaders(interceptorOptions.request.headers);
354
- return interceptorOptions.next({
355
- ...interceptorOptions,
356
- context: {
357
- ...interceptorOptions.context,
358
- reqHeaders
359
- }
360
- });
361
- },
362
- ...toArray(options.interceptors)
363
- ]
364
- };
365
- }
366
- }
367
-
368
- class RequestLimitHandlerPlugin {
369
- name = "~request-limit";
370
- /**
371
- * Should limit the original batch request body instead of sub-requests.
372
- */
373
- after = ["~batch"];
374
- /**
375
- * Should limit the final body size instead of the compressed one.
376
- */
377
- before = ["~request-compression"];
378
- maxBodySize;
379
- constructor(options) {
380
- this.maxBodySize = options.maxBodySize;
381
- }
382
- init(options) {
383
- const maxBodySize = this.maxBodySize;
384
- const routingInterceptor = async ({ next, ...interceptorOptions }) => {
385
- return next({
213
+ options.rootInterceptors ??= [];
214
+ options.rootInterceptors.push(async (interceptorOptions) => {
215
+ const resHeaders = interceptorOptions.context.resHeaders ?? new Headers();
216
+ const result = await interceptorOptions.next({
386
217
  ...interceptorOptions,
387
- request: {
388
- ...interceptorOptions.request,
389
- async resolveBody(hint) {
390
- const contentLength = Number(
391
- flattenStandardHeader(interceptorOptions.request.headers["content-length"])
392
- );
393
- if (Number.isFinite(contentLength) && contentLength > maxBodySize) {
394
- throw new ORPCError("PAYLOAD_TOO_LARGE");
395
- }
396
- const stream = await interceptorOptions.request.resolveBody("octet-stream");
397
- if (!(stream instanceof ReadableStream)) {
398
- return stream;
399
- }
400
- let currentBodySize = 0;
401
- const limitedStream = stream.pipeThrough(
402
- new TransformStream({
403
- transform(chunk, controller) {
404
- currentBodySize += chunk.byteLength;
405
- if (currentBodySize > maxBodySize) {
406
- controller.error(new ORPCError("PAYLOAD_TOO_LARGE"));
407
- return;
408
- }
409
- controller.enqueue(chunk);
410
- }
411
- })
412
- );
413
- const response = new Response(limitedStream, {
414
- headers: toFetchHeaders(interceptorOptions.request.headers)
415
- });
416
- return toStandardBody(response, { hint });
417
- }
218
+ context: {
219
+ ...interceptorOptions.context,
220
+ resHeaders
418
221
  }
419
222
  });
420
- };
421
- return {
422
- ...options,
423
- routingInterceptors: [
424
- routingInterceptor,
425
- ...toArray(options.routingInterceptors)
426
- ]
427
- };
428
- }
429
- }
430
-
431
- const AVG_BYTES_PER_CHAR = 1.2;
432
- class ResponseCompressionHandlerPlugin {
433
- name = "~response-compression";
434
- /**
435
- * Compression should be done after batching, to compress the final response.
436
- * Compression should also be done after response headers are set, to access final headers like Content-Type and Cache-Control.
437
- */
438
- after = ["~batch", "~response-headers"];
439
- encodings;
440
- threshold;
441
- constructor(options = {}) {
442
- this.encodings = options.encodings ?? ["gzip", "deflate"];
443
- this.threshold = options.threshold ?? 1024;
444
- }
445
- init(options) {
446
- const routingInterceptor = async ({ next, ...interceptorOptions }) => {
447
- const result = await next();
448
223
  if (!result.matched) {
449
224
  return result;
450
225
  }
451
- const response = result.response;
452
- const contentEncoding = flattenStandardHeader(response.headers["content-encoding"])?.trim()?.toLowerCase();
453
- if (contentEncoding !== void 0) {
454
- return result;
455
- }
456
- if (isNoTransformCacheControl(flattenStandardHeader(response.headers["cache-control"]))) {
457
- return result;
458
- }
459
- const acceptEncodings = parseAcceptEncodings(
460
- flattenStandardHeader(interceptorOptions.request.headers["accept-encoding"])
461
- );
462
- const encoding = this.encodings.find((enc) => acceptEncodings.includes(enc));
463
- if (encoding === void 0) {
464
- return result;
465
- }
466
- const body = response.body;
467
- const headers = response.headers;
468
- if (body instanceof ReadableStream) {
469
- const contentLength = Number(flattenStandardHeader(headers["content-length"]));
470
- if ((!Number.isFinite(contentLength) || contentLength >= this.threshold) && isCompressibleContentType(flattenStandardHeader(headers["content-type"]))) {
471
- return {
472
- ...result,
473
- response: {
474
- ...response,
475
- body: body.pipeThrough(new CompressionStream(encoding)),
476
- headers: {
477
- ...headers,
478
- "standard-server": "octet-stream",
479
- "content-length": [],
480
- "content-encoding": encoding
481
- }
482
- }
483
- };
484
- }
485
- } else if (body instanceof Blob) {
486
- if ((!Number.isFinite(body.size) || body.size >= this.threshold) && isCompressibleContentType(body.type)) {
487
- const contentDisposition = headers["content-disposition"] ?? generateContentDisposition(
488
- body instanceof File ? body.name : "blob"
489
- );
490
- return {
491
- ...result,
492
- response: {
493
- ...response,
494
- body: body.stream().pipeThrough(new CompressionStream(encoding)),
495
- headers: {
496
- ...headers,
497
- "standard-server": "file",
498
- "content-type": body.type,
499
- "content-length": [],
500
- "content-disposition": contentDisposition,
501
- "content-encoding": encoding
502
- }
503
- }
504
- };
505
- }
506
- } else if (body instanceof FormData) {
507
- const PART_OVERHEAD = 64;
508
- let contentLength = 0;
509
- for (const [key, value] of body) {
510
- contentLength += PART_OVERHEAD + key.length;
511
- if (value instanceof Blob) {
512
- if (!Number.isFinite(value.size)) {
513
- if (!isCompressibleContentType(value.type)) {
514
- contentLength = -Infinity;
515
- break;
516
- }
517
- contentLength = Infinity;
518
- } else {
519
- contentLength += isCompressibleContentType(value.type) ? value.size : -value.size;
520
- }
521
- } else {
522
- contentLength += value.length * AVG_BYTES_PER_CHAR;
523
- }
524
- }
525
- if (contentLength >= this.threshold) {
526
- const res = new Response(body);
527
- const compressedStream = res.body.pipeThrough(new CompressionStream(encoding));
528
- return {
529
- ...result,
530
- response: {
531
- ...response,
532
- body: compressedStream,
533
- headers: {
534
- ...headers,
535
- "standard-server": [],
536
- "content-type": res.headers.get("content-type"),
537
- "content-length": [],
538
- "content-encoding": encoding
539
- }
540
- }
541
- };
542
- }
543
- } else if (body instanceof URLSearchParams) {
544
- const string = body.toString();
545
- if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
546
- return {
547
- ...result,
548
- response: {
549
- ...response,
550
- body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
551
- headers: {
552
- ...headers,
553
- "standard-server": [],
554
- "content-type": "application/x-www-form-urlencoded",
555
- "content-length": [],
556
- "content-encoding": encoding
557
- }
558
- }
559
- };
560
- }
561
- } else if (body !== void 0 && !isAsyncIteratorObject(body)) {
562
- const string = stringifyJSON(body);
563
- if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
564
- return {
565
- ...result,
566
- response: {
567
- ...response,
568
- body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
569
- headers: {
570
- ...headers,
571
- "standard-server": [],
572
- "content-type": "application/json",
573
- "content-length": [],
574
- "content-encoding": encoding
575
- }
576
- }
577
- };
226
+ const responseHeaders = clone(result.response.headers);
227
+ for (const [key, value] of resHeaders) {
228
+ if (Array.isArray(responseHeaders[key])) {
229
+ responseHeaders[key].push(value);
230
+ } else if (responseHeaders[key] !== void 0) {
231
+ responseHeaders[key] = [responseHeaders[key], value];
232
+ } else {
233
+ responseHeaders[key] = value;
578
234
  }
579
235
  }
580
- return result;
581
- };
582
- return {
583
- ...options,
584
- routingInterceptors: [
585
- routingInterceptor,
586
- ...toArray(options.routingInterceptors)
587
- ]
588
- };
589
- }
590
- }
591
- function parseAcceptEncodings(header) {
592
- if (header === void 0) {
593
- return [];
594
- }
595
- return header.split(",").map((part) => part.trim().split(";")[0].trim().toLowerCase()).filter(Boolean);
596
- }
597
- const CACHE_CONTROL_NO_TRANSFORM_REGEX = /(?:^|,)\s*no-transform\s*(?:,|$)/i;
598
- function isNoTransformCacheControl(cacheControl) {
599
- if (cacheControl === void 0) {
600
- return false;
601
- }
602
- return CACHE_CONTROL_NO_TRANSFORM_REGEX.test(cacheControl);
603
- }
604
-
605
- class ResponseHeadersHandlerPlugin {
606
- name = "~response-headers";
607
- /**
608
- * Interceptors should run after batch interceptors so headers are applied to each sub-response.
609
- */
610
- before = ["~batch"];
611
- init(options) {
612
- return {
613
- ...options,
614
- routingInterceptors: [
615
- async (interceptorOptions) => {
616
- const resHeaders = new Headers(interceptorOptions.context.resHeaders);
617
- const result = await interceptorOptions.next({
618
- ...interceptorOptions,
619
- context: {
620
- ...interceptorOptions.context,
621
- resHeaders
622
- }
623
- });
624
- if (!result.response) {
625
- return result;
626
- }
627
- return {
628
- ...result,
629
- response: {
630
- ...result.response,
631
- headers: mergeStandardHeaders(result.response.headers, toStandardHeaders(resHeaders))
632
- }
633
- };
634
- },
635
- ...toArray(options.routingInterceptors)
636
- ]
637
- };
236
+ return {
237
+ ...result,
238
+ response: {
239
+ ...result.response,
240
+ headers: responseHeaders
241
+ }
242
+ };
243
+ });
638
244
  }
639
245
  }
640
246
 
641
- class RethrowHandlerPlugin {
642
- name = "~rethrow";
247
+ class experimental_RethrowHandlerPlugin {
643
248
  filter;
644
249
  CONTEXT_SYMBOL = Symbol("ORPC_RETHROW_HANDLER_PLUGIN_CONTEXT");
645
250
  constructor(options) {
646
251
  this.filter = options.filter;
647
252
  }
648
253
  init(options) {
649
- const routingInterceptor = async (options2) => {
254
+ options.rootInterceptors ??= [];
255
+ options.interceptors ??= [];
256
+ options.rootInterceptors.push(async (options2) => {
650
257
  const pluginContext = {};
651
258
  const result = await options2.next({
652
259
  ...options2,
@@ -659,8 +266,8 @@ class RethrowHandlerPlugin {
659
266
  throw pluginContext.error.value;
660
267
  }
661
268
  return result;
662
- };
663
- const interceptor = async (options2) => {
269
+ });
270
+ options.interceptors.unshift(async (options2) => {
664
271
  const pluginContext = options2.context[this.CONTEXT_SYMBOL];
665
272
  if (!pluginContext) {
666
273
  throw new TypeError("[RethrowHandlerPlugin] Rethrow handler context has been corrupted or modified by another plugin or interceptor");
@@ -668,26 +275,57 @@ class RethrowHandlerPlugin {
668
275
  try {
669
276
  return await options2.next();
670
277
  } catch (error) {
671
- if (this.filter(error, options2)) {
278
+ if (value(this.filter, error, options2)) {
672
279
  pluginContext.error = { value: error };
280
+ return { matched: false, response: void 0 };
673
281
  }
674
282
  throw error;
675
283
  }
676
- };
677
- return {
678
- ...options,
679
- routingInterceptors: [
680
- ...toArray(options.routingInterceptors),
681
- routingInterceptor
682
- // rethrow as early as possible and keep it close to `interceptor` to avoid a corrupted or modified context
683
- ],
684
- interceptors: [
685
- interceptor,
686
- // catch as many errors as possible
687
- ...toArray(options.interceptors)
688
- ]
689
- };
284
+ });
285
+ }
286
+ }
287
+
288
+ const SIMPLE_CSRF_PROTECTION_CONTEXT_SYMBOL = Symbol("SIMPLE_CSRF_PROTECTION_CONTEXT");
289
+ class SimpleCsrfProtectionHandlerPlugin {
290
+ headerName;
291
+ headerValue;
292
+ exclude;
293
+ error;
294
+ constructor(options = {}) {
295
+ this.headerName = options.headerName ?? "x-csrf-token";
296
+ this.headerValue = options.headerValue ?? "orpc";
297
+ this.exclude = options.exclude ?? false;
298
+ this.error = options.error ?? new ORPCError("CSRF_TOKEN_MISMATCH", {
299
+ status: 403,
300
+ message: "Invalid CSRF token"
301
+ });
302
+ }
303
+ order = 8e6;
304
+ init(options) {
305
+ options.rootInterceptors ??= [];
306
+ options.clientInterceptors ??= [];
307
+ options.rootInterceptors.unshift(async (options2) => {
308
+ const headerName = await value(this.headerName, options2);
309
+ const headerValue = await value(this.headerValue, options2);
310
+ return options2.next({
311
+ ...options2,
312
+ context: {
313
+ ...options2.context,
314
+ [SIMPLE_CSRF_PROTECTION_CONTEXT_SYMBOL]: options2.request.headers[headerName] === headerValue
315
+ }
316
+ });
317
+ });
318
+ options.clientInterceptors.unshift(async (options2) => {
319
+ if (typeof options2.context[SIMPLE_CSRF_PROTECTION_CONTEXT_SYMBOL] !== "boolean") {
320
+ throw new TypeError("[SimpleCsrfProtectionHandlerPlugin] CSRF protection context has been corrupted or modified by another plugin or interceptor");
321
+ }
322
+ const excluded = await value(this.exclude, options2);
323
+ if (!excluded && !options2.context[SIMPLE_CSRF_PROTECTION_CONTEXT_SYMBOL]) {
324
+ throw this.error;
325
+ }
326
+ return options2.next();
327
+ });
690
328
  }
691
329
  }
692
330
 
693
- export { BatchHandlerPlugin, CORSHandlerPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestLimitHandlerPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, RethrowHandlerPlugin };
331
+ export { BatchHandlerPlugin, CORSPlugin, RequestHeadersPlugin, ResponseHeadersPlugin, SimpleCsrfProtectionHandlerPlugin, experimental_RethrowHandlerPlugin };