@orpc/server 1.14.6 → 2.0.0-beta.1

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 +74 -136
  2. package/dist/adapters/crossws/index.d.mts +42 -21
  3. package/dist/adapters/crossws/index.d.ts +42 -21
  4. package/dist/adapters/crossws/index.mjs +37 -18
  5. package/dist/adapters/fetch/index.d.mts +83 -67
  6. package/dist/adapters/fetch/index.d.ts +83 -67
  7. package/dist/adapters/fetch/index.mjs +131 -106
  8. package/dist/adapters/message-port/index.d.mts +51 -34
  9. package/dist/adapters/message-port/index.d.ts +51 -34
  10. package/dist/adapters/message-port/index.mjs +73 -38
  11. package/dist/adapters/node/index.d.mts +82 -60
  12. package/dist/adapters/node/index.d.ts +82 -60
  13. package/dist/adapters/node/index.mjs +127 -98
  14. package/dist/adapters/standard/index.d.mts +16 -18
  15. package/dist/adapters/standard/index.d.ts +16 -18
  16. package/dist/adapters/standard/index.mjs +5 -5
  17. package/dist/adapters/standard-peer/index.d.mts +12 -14
  18. package/dist/adapters/standard-peer/index.d.ts +12 -14
  19. package/dist/adapters/standard-peer/index.mjs +2 -21
  20. package/dist/adapters/websocket/index.d.mts +39 -34
  21. package/dist/adapters/websocket/index.d.ts +39 -34
  22. package/dist/adapters/websocket/index.mjs +42 -33
  23. package/dist/extensions/callable.d.mts +10 -0
  24. package/dist/extensions/callable.d.ts +10 -0
  25. package/dist/extensions/callable.mjs +11 -0
  26. package/dist/helpers/index.d.mts +2 -2
  27. package/dist/helpers/index.d.ts +2 -2
  28. package/dist/helpers/index.mjs +1 -1
  29. package/dist/index.d.mts +163 -770
  30. package/dist/index.d.ts +163 -770
  31. package/dist/index.mjs +296 -403
  32. package/dist/plugins/index.d.mts +105 -143
  33. package/dist/plugins/index.d.ts +105 -143
  34. package/dist/plugins/index.mjs +232 -255
  35. package/dist/shared/server.BB_Ik9Ph.d.mts +104 -0
  36. package/dist/shared/server.BL22TloH.d.mts +184 -0
  37. package/dist/shared/server.BL22TloH.d.ts +184 -0
  38. package/dist/shared/server.B_U9y00a.d.mts +66 -0
  39. package/dist/shared/server.BsNNjG5J.d.mts +61 -0
  40. package/dist/shared/server.BwHnWUuN.mjs +222 -0
  41. package/dist/shared/server.CX4vUnDk.mjs +11 -0
  42. package/dist/shared/server.CjOb6ItT.mjs +41 -0
  43. package/dist/shared/server.CrlKQucM.mjs +233 -0
  44. package/dist/shared/server.D_QauotT.mjs +30 -0
  45. package/dist/shared/server.EOHJ3NJr.d.ts +104 -0
  46. package/dist/shared/server.GDpX6Df8.mjs +271 -0
  47. package/dist/shared/server.Pa0F03f_.d.ts +61 -0
  48. package/dist/shared/server.T9F3bzZx.d.ts +66 -0
  49. package/dist/shared/{server.DZ5BIITo.mjs → server.W91HSRkE.mjs} +2 -2
  50. package/package.json +26 -55
  51. package/dist/adapters/aws-lambda/index.d.mts +0 -46
  52. package/dist/adapters/aws-lambda/index.d.ts +0 -46
  53. package/dist/adapters/aws-lambda/index.mjs +0 -40
  54. package/dist/adapters/bun-ws/index.d.mts +0 -36
  55. package/dist/adapters/bun-ws/index.d.ts +0 -36
  56. package/dist/adapters/bun-ws/index.mjs +0 -47
  57. package/dist/adapters/fastify/index.d.mts +0 -53
  58. package/dist/adapters/fastify/index.d.ts +0 -53
  59. package/dist/adapters/fastify/index.mjs +0 -52
  60. package/dist/adapters/ws/index.d.mts +0 -31
  61. package/dist/adapters/ws/index.d.ts +0 -31
  62. package/dist/adapters/ws/index.mjs +0 -37
  63. package/dist/hibernation/index.d.mts +0 -44
  64. package/dist/hibernation/index.d.ts +0 -44
  65. package/dist/hibernation/index.mjs +0 -65
  66. package/dist/shared/server.7cEtMB30.d.ts +0 -74
  67. package/dist/shared/server.B8gYOD5g.d.mts +0 -12
  68. package/dist/shared/server.BqadksTP.d.mts +0 -74
  69. package/dist/shared/server.C8_sRzQB.d.mts +0 -42
  70. package/dist/shared/server.ChUyt5-i.d.mts +0 -32
  71. package/dist/shared/server.ChyoA9XY.d.ts +0 -42
  72. package/dist/shared/server.DEBcqOjg.mjs +0 -418
  73. package/dist/shared/server.EfTOZ2Q7.d.ts +0 -12
  74. package/dist/shared/server.TEVCLCFC.mjs +0 -39
  75. package/dist/shared/server.ZxHCEN1h.mjs +0 -226
  76. package/dist/shared/server.qKsRrdxW.d.mts +0 -193
  77. package/dist/shared/server.qKsRrdxW.d.ts +0 -193
  78. package/dist/shared/server.yoEB3Fx4.d.ts +0 -32
@@ -1,124 +1,173 @@
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';
5
- import { ORPCError } from '@orpc/client';
6
- export { S as StrictGetMethodPlugin } from '../shared/server.TEVCLCFC.mjs';
7
- import '@orpc/contract';
1
+ import { toArray, value } from '@orpc/shared';
2
+ import { flattenStandardHeader, parseStandardUrl, 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, toStandardHeaders } from '@standardserver/fetch';
6
+ import '@orpc/client';
8
7
 
9
8
  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"];
10
15
  maxSize;
11
- mapRequestItem;
16
+ mapSubrequest;
12
17
  successStatus;
13
18
  headers;
14
- order = 5e6;
15
19
  constructor(options = {}) {
16
20
  this.maxSize = options.maxSize ?? 10;
17
- this.mapRequestItem = options.mapRequestItem ?? ((request, { request: batchRequest }) => ({
18
- ...request,
21
+ this.mapSubrequest = options.mapSubrequest ?? ((subRequest, { request: batchRequest }) => ({
22
+ ...subRequest,
19
23
  headers: {
20
24
  ...batchRequest.headers,
21
- ...request.headers
25
+ ...subRequest.headers,
26
+ "orpc-batch": void 0
27
+ // useful in case batch plugin is used multiple times
22
28
  }
23
29
  }));
24
30
  this.successStatus = options.successStatus ?? 207;
25
31
  this.headers = options.headers ?? {};
26
32
  }
27
33
  init(options) {
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();
34
+ const routingInterceptor = async (interceptorOptions) => {
35
+ const batchHeader = flattenStandardHeader(interceptorOptions.request.headers["orpc-batch"]);
36
+ if (batchHeader === void 0) {
37
+ return interceptorOptions.next();
33
38
  }
34
- let isParsing = false;
39
+ const mode = batchHeader === "buffered" ? "buffered" : "streaming";
40
+ let messages;
35
41
  try {
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);
42
+ if (interceptorOptions.request.method === "GET") {
43
+ const [, search] = parseStandardUrl(interceptorOptions.request.url);
44
+ const params = new URLSearchParams(search);
45
+ const data = params.getAll("data").at(-1);
46
+ if (!data) {
47
47
  return {
48
48
  matched: true,
49
- response: {
50
- status: 413,
51
- headers: {},
52
- body: message
53
- }
49
+ response: { status: 400, headers: {}, body: "Missing data parameter for batch request" }
54
50
  };
55
51
  }
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
- };
52
+ const mightBeMessages = JSON.parse(data);
53
+ if (!Array.isArray(mightBeMessages) || mightBeMessages.some((m) => !isClientPeerSendMessage(m))) {
54
+ return {
55
+ matched: true,
56
+ response: { status: 400, headers: {}, body: "Invalid batch request data parameter" }
57
+ };
58
+ }
59
+ messages = mightBeMessages;
60
+ } else {
61
+ const mightBeMessages = await interceptorOptions.request.resolveBody();
62
+ if (!Array.isArray(mightBeMessages)) {
63
+ return {
64
+ matched: true,
65
+ response: { status: 400, headers: {}, body: "Invalid batch request body" }
66
+ };
67
+ }
68
+ messages = mightBeMessages;
69
+ }
70
+ } catch {
71
+ return {
72
+ matched: true,
73
+ response: { status: 400, headers: {}, body: "Invalid batch request" }
74
+ };
75
+ }
76
+ const maxSize = await value(this.maxSize, interceptorOptions);
77
+ if (messages.length > maxSize) {
78
+ return {
79
+ matched: true,
80
+ response: { status: 413, headers: {}, body: "Batch request size exceeds the maximum allowed size" }
81
+ };
82
+ }
83
+ const handleIndividualRequest = async (request) => {
84
+ try {
85
+ request = this.mapSubrequest(request, interceptorOptions);
86
+ const { matched, response } = await interceptorOptions.next({ ...interceptorOptions, request });
87
+ if (!matched) {
88
+ return { status: 404, headers: {}, body: "No procedure matched" };
89
+ }
90
+ return response;
91
+ } catch (err) {
92
+ Promise.reject(err);
93
+ return { status: 500, headers: {}, body: "Internal server error" };
94
+ }
95
+ };
96
+ const status = await value(this.successStatus, interceptorOptions);
97
+ const headers = await value(this.headers, interceptorOptions);
98
+ if (mode === "buffered") {
99
+ const responseMessages = [];
100
+ const peer2 = new ServerPeer(async (message) => {
101
+ responseMessages.push(message);
105
102
  });
106
- } catch (cause) {
107
- if (isParsing) {
103
+ await Promise.all(messages.map((msg) => peer2.message(msg, handleIndividualRequest)));
104
+ await peer2.close();
105
+ if (responseMessages.some((msg) => msg.binary !== void 0)) {
106
+ const chunks = [];
107
+ for (const message of responseMessages) {
108
+ const encoded = await encodePeerMessage(message);
109
+ const bytes = typeof encoded === "string" ? new TextEncoder().encode(encoded) : encoded;
110
+ const lengthBuffer = new ArrayBuffer(4);
111
+ new DataView(lengthBuffer).setUint32(0, bytes.byteLength, false);
112
+ chunks.push(new Uint8Array(lengthBuffer));
113
+ chunks.push(bytes);
114
+ }
108
115
  return {
109
116
  matched: true,
110
- response: { status: 400, headers: {}, body: "Invalid batch request, this could be caused by a malformed request body or a missing header" }
117
+ response: {
118
+ status,
119
+ headers,
120
+ body: new Blob(chunks, { type: "application/octet-stream" })
121
+ }
111
122
  };
112
123
  }
113
- throw cause;
124
+ return {
125
+ matched: true,
126
+ response: { status, headers, body: responseMessages }
127
+ };
114
128
  }
115
- });
129
+ let streamController;
130
+ const stream = new ReadableStream({
131
+ start(controller) {
132
+ streamController = controller;
133
+ }
134
+ });
135
+ const peer = new ServerPeer(async (message) => {
136
+ const encoded = await encodePeerMessage(message);
137
+ const bytes = typeof encoded === "string" ? new TextEncoder().encode(encoded) : encoded;
138
+ const lengthBuffer = new ArrayBuffer(4);
139
+ new DataView(lengthBuffer).setUint32(0, bytes.byteLength, false);
140
+ streamController.enqueue(new Uint8Array(lengthBuffer));
141
+ streamController.enqueue(bytes);
142
+ });
143
+ Promise.all(messages.map((msg) => peer.message(msg, handleIndividualRequest))).then(async () => {
144
+ streamController.close();
145
+ await peer.close();
146
+ }).catch(async (error) => {
147
+ streamController.error(error);
148
+ await peer.close(error);
149
+ });
150
+ return {
151
+ matched: true,
152
+ response: { status, headers, body: stream }
153
+ };
154
+ };
155
+ return {
156
+ ...options,
157
+ routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
158
+ };
116
159
  }
117
160
  }
118
161
 
119
- class CORSPlugin {
162
+ class CORSHandlerPlugin {
120
163
  options;
121
- order = 9e6;
164
+ name = "~cors";
165
+ /**
166
+ * - Do not create spans for CORS preflight requests.
167
+ * - Run CORS interceptors before batch interceptors so headers are applied to
168
+ * the actual response rather than sub-responses.
169
+ */
170
+ after = ["~opentelemetry", "~batch"];
122
171
  constructor(options = {}) {
123
172
  const defaults = {
124
173
  origin: (origin) => origin,
@@ -130,19 +179,48 @@ class CORSPlugin {
130
179
  };
131
180
  }
132
181
  init(options) {
133
- options.rootInterceptors ??= [];
134
- options.rootInterceptors.unshift(async (interceptorOptions) => {
182
+ const corsHeadersInterceptor = async (interceptorOptions) => {
183
+ const result = await interceptorOptions.next();
184
+ if (!result.matched) {
185
+ return result;
186
+ }
187
+ const resHeaders = { ...result.response.headers };
188
+ const origin = flattenStandardHeader(interceptorOptions.request.headers.origin) ?? "";
189
+ const allowedOrigins = toArray(await value(this.options.origin, origin, interceptorOptions));
190
+ if (allowedOrigins.includes("*")) {
191
+ resHeaders["access-control-allow-origin"] = "*";
192
+ } else {
193
+ if (allowedOrigins.includes(origin)) {
194
+ resHeaders["access-control-allow-origin"] = origin;
195
+ }
196
+ resHeaders.vary = interceptorOptions.request.headers.vary ?? "origin";
197
+ }
198
+ const allowedTimingOrigins = toArray(await value(this.options.timingOrigin, origin, interceptorOptions));
199
+ if (allowedTimingOrigins.includes("*")) {
200
+ resHeaders["timing-allow-origin"] = "*";
201
+ } else if (allowedTimingOrigins.includes(origin)) {
202
+ resHeaders["timing-allow-origin"] = origin;
203
+ }
204
+ if (this.options.credentials) {
205
+ resHeaders["access-control-allow-credentials"] = "true";
206
+ }
207
+ if (this.options.exposeHeaders?.length) {
208
+ resHeaders["access-control-expose-headers"] = flattenStandardHeader(this.options.exposeHeaders);
209
+ }
210
+ return { ...result, response: { ...result.response, headers: resHeaders } };
211
+ };
212
+ const preflightInterceptor = async (interceptorOptions) => {
135
213
  if (interceptorOptions.request.method === "OPTIONS") {
136
214
  const resHeaders = {};
137
215
  if (this.options.maxAge !== void 0) {
138
216
  resHeaders["access-control-max-age"] = this.options.maxAge.toString();
139
217
  }
140
218
  if (this.options.allowMethods?.length) {
141
- resHeaders["access-control-allow-methods"] = flattenHeader(this.options.allowMethods);
219
+ resHeaders["access-control-allow-methods"] = flattenStandardHeader(this.options.allowMethods);
142
220
  }
143
221
  const allowHeaders = this.options.allowHeaders ?? interceptorOptions.request.headers["access-control-request-headers"];
144
222
  if (typeof allowHeaders === "string" || allowHeaders?.length) {
145
- resHeaders["access-control-allow-headers"] = flattenHeader(allowHeaders);
223
+ resHeaders["access-control-allow-headers"] = flattenStandardHeader(allowHeaders);
146
224
  }
147
225
  return {
148
226
  matched: true,
@@ -154,175 +232,74 @@ class CORSPlugin {
154
232
  };
155
233
  }
156
234
  return interceptorOptions.next();
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
- result.response.headers.vary = interceptorOptions.request.headers.vary ?? "origin";
173
- }
174
- const allowedTimingOrigin = await value(this.options.timingOrigin, origin, interceptorOptions);
175
- const allowedTimingOriginArr = Array.isArray(allowedTimingOrigin) ? allowedTimingOrigin : [allowedTimingOrigin];
176
- if (allowedTimingOriginArr.includes("*")) {
177
- result.response.headers["timing-allow-origin"] = "*";
178
- } else if (allowedTimingOriginArr.includes(origin)) {
179
- result.response.headers["timing-allow-origin"] = origin;
180
- }
181
- if (this.options.credentials) {
182
- result.response.headers["access-control-allow-credentials"] = "true";
183
- }
184
- if (this.options.exposeHeaders?.length) {
185
- result.response.headers["access-control-expose-headers"] = flattenHeader(this.options.exposeHeaders);
186
- }
187
- return result;
188
- });
189
- }
190
- }
191
-
192
- class RequestHeadersPlugin {
193
- init(options) {
194
- options.rootInterceptors ??= [];
195
- options.rootInterceptors.push((interceptorOptions) => {
196
- const reqHeaders = interceptorOptions.context.reqHeaders ?? toFetchHeaders(interceptorOptions.request.headers);
197
- return interceptorOptions.next({
198
- ...interceptorOptions,
199
- context: {
200
- ...interceptorOptions.context,
201
- reqHeaders
202
- }
203
- });
204
- });
205
- }
206
- }
207
-
208
- class ResponseHeadersPlugin {
209
- init(options) {
210
- options.rootInterceptors ??= [];
211
- options.rootInterceptors.push(async (interceptorOptions) => {
212
- const resHeaders = interceptorOptions.context.resHeaders ?? new Headers();
213
- const result = await interceptorOptions.next({
214
- ...interceptorOptions,
215
- context: {
216
- ...interceptorOptions.context,
217
- resHeaders
218
- }
219
- });
220
- if (!result.matched) {
221
- return result;
222
- }
223
- const responseHeaders = clone(result.response.headers);
224
- for (const [key, value] of resHeaders) {
225
- if (Array.isArray(responseHeaders[key])) {
226
- responseHeaders[key].push(value);
227
- } else if (responseHeaders[key] !== void 0) {
228
- responseHeaders[key] = [responseHeaders[key], value];
229
- } else {
230
- responseHeaders[key] = value;
231
- }
232
- }
233
- return {
234
- ...result,
235
- response: {
236
- ...result.response,
237
- headers: responseHeaders
238
- }
239
- };
240
- });
235
+ };
236
+ return {
237
+ ...options,
238
+ routingInterceptors: [
239
+ corsHeadersInterceptor,
240
+ preflightInterceptor,
241
+ ...toArray(options.routingInterceptors)
242
+ ]
243
+ };
241
244
  }
242
245
  }
243
246
 
244
- class experimental_RethrowHandlerPlugin {
245
- filter;
246
- CONTEXT_SYMBOL = Symbol("ORPC_RETHROW_HANDLER_PLUGIN_CONTEXT");
247
- constructor(options) {
248
- this.filter = options.filter;
249
- }
247
+ class RequestHeadersHandlerPlugin {
248
+ name = "~request-headers";
250
249
  init(options) {
251
- options.rootInterceptors ??= [];
252
- options.interceptors ??= [];
253
- options.rootInterceptors.push(async (options2) => {
254
- const pluginContext = {};
255
- const result = await options2.next({
256
- ...options2,
257
- context: {
258
- ...options2.context,
259
- [this.CONTEXT_SYMBOL]: pluginContext
260
- }
261
- });
262
- if (pluginContext.error) {
263
- throw pluginContext.error.value;
264
- }
265
- return result;
266
- });
267
- options.interceptors.unshift(async (options2) => {
268
- const pluginContext = options2.context[this.CONTEXT_SYMBOL];
269
- if (!pluginContext) {
270
- throw new TypeError("[RethrowHandlerPlugin] Rethrow handler context has been corrupted or modified by another plugin or interceptor");
271
- }
272
- try {
273
- return await options2.next();
274
- } catch (error) {
275
- if (value(this.filter, error, options2)) {
276
- pluginContext.error = { value: error };
277
- return { matched: false, response: void 0 };
278
- }
279
- throw error;
280
- }
281
- });
250
+ return {
251
+ ...options,
252
+ interceptors: [
253
+ (interceptorOptions) => {
254
+ const reqHeaders = interceptorOptions.context.reqHeaders ?? toFetchHeaders(interceptorOptions.request.headers);
255
+ return interceptorOptions.next({
256
+ ...interceptorOptions,
257
+ context: {
258
+ ...interceptorOptions.context,
259
+ reqHeaders
260
+ }
261
+ });
262
+ },
263
+ ...toArray(options.interceptors)
264
+ ]
265
+ };
282
266
  }
283
267
  }
284
268
 
285
- const SIMPLE_CSRF_PROTECTION_CONTEXT_SYMBOL = Symbol("SIMPLE_CSRF_PROTECTION_CONTEXT");
286
- class SimpleCsrfProtectionHandlerPlugin {
287
- headerName;
288
- headerValue;
289
- exclude;
290
- error;
291
- constructor(options = {}) {
292
- this.headerName = options.headerName ?? "x-csrf-token";
293
- this.headerValue = options.headerValue ?? "orpc";
294
- this.exclude = options.exclude ?? false;
295
- this.error = options.error ?? new ORPCError("CSRF_TOKEN_MISMATCH", {
296
- status: 403,
297
- message: "Invalid CSRF token"
298
- });
299
- }
300
- order = 8e6;
269
+ class ResponseHeadersHandlerPlugin {
270
+ name = "~response-headers";
271
+ /**
272
+ * Interceptors should run after batch interceptors so headers are applied to each sub-response.
273
+ */
274
+ before = ["~batch"];
301
275
  init(options) {
302
- options.rootInterceptors ??= [];
303
- options.clientInterceptors ??= [];
304
- options.rootInterceptors.unshift(async (options2) => {
305
- const headerName = await value(this.headerName, options2);
306
- const headerValue = await value(this.headerValue, options2);
307
- return options2.next({
308
- ...options2,
309
- context: {
310
- ...options2.context,
311
- [SIMPLE_CSRF_PROTECTION_CONTEXT_SYMBOL]: options2.request.headers[headerName] === headerValue
312
- }
313
- });
314
- });
315
- options.clientInterceptors.unshift(async (options2) => {
316
- if (typeof options2.context[SIMPLE_CSRF_PROTECTION_CONTEXT_SYMBOL] !== "boolean") {
317
- throw new TypeError("[SimpleCsrfProtectionHandlerPlugin] CSRF protection context has been corrupted or modified by another plugin or interceptor");
318
- }
319
- const excluded = await value(this.exclude, options2);
320
- if (!excluded && !options2.context[SIMPLE_CSRF_PROTECTION_CONTEXT_SYMBOL]) {
321
- throw this.error;
322
- }
323
- return options2.next();
324
- });
276
+ return {
277
+ ...options,
278
+ routingInterceptors: [
279
+ async (interceptorOptions) => {
280
+ const resHeaders = new Headers(interceptorOptions.context.resHeaders);
281
+ const result = await interceptorOptions.next({
282
+ ...interceptorOptions,
283
+ context: {
284
+ ...interceptorOptions.context,
285
+ resHeaders
286
+ }
287
+ });
288
+ if (!result.response) {
289
+ return result;
290
+ }
291
+ return {
292
+ ...result,
293
+ response: {
294
+ ...result.response,
295
+ headers: mergeStandardHeaders(result.response.headers, toStandardHeaders(resHeaders))
296
+ }
297
+ };
298
+ },
299
+ ...toArray(options.routingInterceptors)
300
+ ]
301
+ };
325
302
  }
326
303
  }
327
304
 
328
- export { BatchHandlerPlugin, CORSPlugin, RequestHeadersPlugin, ResponseHeadersPlugin, SimpleCsrfProtectionHandlerPlugin, experimental_RethrowHandlerPlugin };
305
+ export { BatchHandlerPlugin, CORSHandlerPlugin, RequestHeadersHandlerPlugin, ResponseHeadersHandlerPlugin };
@@ -0,0 +1,104 @@
1
+ import { AnyORPCError } from '@orpc/client';
2
+ import { OrderablePlugin, Interceptor, Promisable } from '@orpc/shared';
3
+ import { StandardLazyRequest, StandardResponse } from '@standardserver/core';
4
+ import { C as Context, c as ProcedureClientInterceptor, A as AnyProcedure } from './server.BL22TloH.mjs';
5
+ import { Schema, ErrorMap } from '@orpc/contract';
6
+
7
+ interface StandardHandlerPlugin<T extends Context> extends OrderablePlugin {
8
+ /**
9
+ * Initializes the plugin and returns new handler options.
10
+ * Called once per plugin instance during composition.
11
+ *
12
+ * This method allows plugins to wrap, extend, or transform handler options
13
+ * such as interceptors, or configuration.
14
+ *
15
+ * @param options - The current handler options from previous plugins or base configuration
16
+ * @returns Transformed handler options with plugin's modifications applied
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * init(options) {
21
+ * return {
22
+ * ...options,
23
+ * interceptors: [...(options.interceptors || []), myInterceptor]
24
+ * }
25
+ * }
26
+ * ```
27
+ */
28
+ init?(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
29
+ }
30
+ declare class CompositeStandardHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
31
+ readonly name = "~composite";
32
+ protected readonly plugins: StandardHandlerPlugin<T>[];
33
+ constructor(plugins?: StandardHandlerPlugin<T>[]);
34
+ init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
35
+ }
36
+
37
+ interface StandardHandlerHandleOptions<T extends Context> {
38
+ prefix?: `/${string}` | undefined;
39
+ context: T;
40
+ }
41
+ type StandardHandlerHandleResult = {
42
+ matched: true;
43
+ response: StandardResponse;
44
+ } | {
45
+ matched: false;
46
+ response?: undefined;
47
+ };
48
+ interface StandardHandlerInterceptorOptions<T extends Context> extends StandardHandlerCodecResolvedProcedure, StandardHandlerHandleOptions<T> {
49
+ request: StandardLazyRequest;
50
+ }
51
+ type StandardHandlerInterceptor<T extends Context> = Interceptor<StandardHandlerInterceptorOptions<T>, Promise<StandardResponse>>;
52
+ interface StandardHandlerRoutingInterceptorOptions<T extends Context> extends StandardHandlerHandleOptions<T> {
53
+ request: StandardLazyRequest;
54
+ }
55
+ type StandardHandlerRoutingInterceptor<T extends Context> = Interceptor<StandardHandlerRoutingInterceptorOptions<T>, Promise<StandardHandlerHandleResult>>;
56
+ interface StandardHandlerOptions<TContext extends Context> {
57
+ /**
58
+ * Fired on every request before routing, useful when you want
59
+ * to intercept all requests regardless of whether they match a procedure or not.
60
+ *
61
+ * @examples
62
+ * - batch plugins - separate one request into multiple and call multiple next
63
+ * - openapi spec plugin - to intercept a request and early response
64
+ */
65
+ routingInterceptors?: StandardHandlerRoutingInterceptor<TContext>[];
66
+ /**
67
+ * interceptor run after routing and before error handler,
68
+ * useful for error handling, logging, metrics, etc.
69
+ */
70
+ interceptors?: StandardHandlerInterceptor<TContext>[];
71
+ /**
72
+ *
73
+ * ClientInterceptor equivalent with createRouterClient.interceptors / createProcedure.interceptors
74
+ * useful for error handling, logging, metrics, etc. (not counting encoding/decoding)
75
+ */
76
+ clientInterceptors?: ProcedureClientInterceptor<TContext, Schema<unknown>, ErrorMap, any>[];
77
+ plugins?: StandardHandlerPlugin<TContext>[];
78
+ }
79
+ declare class StandardHandler<T extends Context> {
80
+ private readonly codec;
81
+ private readonly routingInterceptors;
82
+ private readonly interceptors;
83
+ private readonly clientInterceptors;
84
+ constructor(codec: StandardHandlerCodec<T>, options: StandardHandlerOptions<T>);
85
+ handle(request: StandardLazyRequest, { context, prefix }: StandardHandlerHandleOptions<T>): Promise<StandardHandlerHandleResult>;
86
+ }
87
+ declare class OtelHandlerPlugin implements StandardHandlerPlugin<any> {
88
+ name: string;
89
+ init(options: StandardHandlerOptions<any>): StandardHandlerOptions<any>;
90
+ }
91
+
92
+ interface StandardHandlerCodecResolvedProcedure {
93
+ path: string[];
94
+ procedure: AnyProcedure;
95
+ decodeInput: () => Promise<unknown>;
96
+ }
97
+ interface StandardHandlerCodec<T extends Context> {
98
+ resolveProcedure(request: StandardLazyRequest, options: StandardHandlerHandleOptions<T>): Promisable<StandardHandlerCodecResolvedProcedure | undefined>;
99
+ encodeOutput(output: unknown, procedure: AnyProcedure, path: string[], options: StandardHandlerHandleOptions<T>): Promisable<StandardResponse>;
100
+ encodeError(error: AnyORPCError, procedure: AnyProcedure, path: string[], options: StandardHandlerHandleOptions<T>): Promisable<StandardResponse>;
101
+ }
102
+
103
+ export { CompositeStandardHandlerPlugin as C, OtelHandlerPlugin as O, StandardHandler as S };
104
+ export type { StandardHandlerOptions as a, StandardHandlerHandleOptions as b, StandardHandlerCodec as c, StandardHandlerCodecResolvedProcedure as d, StandardHandlerPlugin as e, StandardHandlerRoutingInterceptorOptions as f, StandardHandlerHandleResult as g, StandardHandlerInterceptor as h, StandardHandlerInterceptorOptions as i, StandardHandlerRoutingInterceptor as j };