@orpc/server 2.0.0-beta.5 → 2.0.0-beta.50

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 (171) hide show
  1. package/README.md +137 -102
  2. package/dist/adapters/aws-lambda/handler.d.ts +27 -0
  3. package/dist/adapters/aws-lambda/handler.js +24 -0
  4. package/dist/adapters/aws-lambda/index.d.ts +4 -0
  5. package/dist/adapters/aws-lambda/index.js +2 -0
  6. package/dist/adapters/aws-lambda/rpc-handler.d.ts +19 -0
  7. package/dist/adapters/aws-lambda/rpc-handler.js +21 -0
  8. package/dist/adapters/crossws/handler.d.ts +47 -0
  9. package/dist/adapters/crossws/handler.js +53 -0
  10. package/dist/adapters/crossws/index.d.ts +3 -54
  11. package/dist/adapters/crossws/index.js +2 -0
  12. package/dist/adapters/crossws/rpc-handler.d.ts +16 -0
  13. package/dist/adapters/crossws/rpc-handler.js +18 -0
  14. package/dist/adapters/fastify/handler.d.ts +23 -0
  15. package/dist/adapters/fastify/handler.js +20 -0
  16. package/dist/adapters/fastify/index.d.ts +3 -0
  17. package/dist/adapters/fastify/index.js +2 -0
  18. package/dist/adapters/fastify/rpc-handler.d.ts +15 -0
  19. package/dist/adapters/fastify/rpc-handler.js +17 -0
  20. package/dist/adapters/fetch/handler.d.ts +25 -0
  21. package/dist/adapters/fetch/handler.js +22 -0
  22. package/dist/adapters/fetch/index.d.ts +4 -140
  23. package/dist/adapters/fetch/index.js +3 -0
  24. package/dist/adapters/fetch/rpc-handler.d.ts +17 -0
  25. package/dist/adapters/fetch/rpc-handler.js +19 -0
  26. package/dist/adapters/message-port/handler.d.ts +62 -0
  27. package/dist/adapters/message-port/handler.js +72 -0
  28. package/dist/adapters/message-port/index.d.ts +3 -74
  29. package/dist/adapters/message-port/index.js +2 -0
  30. package/dist/adapters/message-port/rpc-handler.d.ts +16 -0
  31. package/dist/adapters/message-port/rpc-handler.js +18 -0
  32. package/dist/adapters/node/handler.d.ts +23 -0
  33. package/dist/adapters/node/handler.js +20 -0
  34. package/dist/adapters/node/index.d.ts +4 -121
  35. package/dist/adapters/node/index.js +3 -0
  36. package/dist/adapters/node/rpc-handler.d.ts +16 -0
  37. package/dist/adapters/node/rpc-handler.js +18 -0
  38. package/dist/adapters/standard/codec.d.ts +16 -0
  39. package/dist/adapters/standard/handler.d.ts +68 -0
  40. package/dist/adapters/standard/handler.js +205 -0
  41. package/dist/adapters/standard/index.d.ts +7 -19
  42. package/dist/adapters/standard/index.js +4 -0
  43. package/dist/adapters/standard/plugin.d.ts +26 -0
  44. package/dist/adapters/standard/plugin.js +14 -0
  45. package/dist/adapters/standard/rpc-handler-codec.d.ts +42 -0
  46. package/dist/adapters/standard/rpc-handler-codec.js +50 -0
  47. package/dist/adapters/standard/rpc-matcher.d.ts +53 -0
  48. package/dist/adapters/standard/rpc-matcher.js +106 -0
  49. package/dist/adapters/standard/utils.d.ts +10 -0
  50. package/dist/adapters/standard/utils.js +8 -0
  51. package/dist/adapters/standard-peer/index.d.ts +2 -16
  52. package/dist/adapters/standard-peer/index.js +1 -0
  53. package/dist/adapters/standard-peer/utils.d.ts +6 -0
  54. package/dist/adapters/standard-peer/utils.js +13 -0
  55. package/dist/adapters/websocket/handler.d.ts +61 -0
  56. package/dist/adapters/websocket/handler.js +75 -0
  57. package/dist/adapters/websocket/index.d.ts +3 -61
  58. package/dist/adapters/websocket/index.js +2 -0
  59. package/dist/adapters/websocket/rpc-handler.d.ts +16 -0
  60. package/dist/adapters/websocket/rpc-handler.js +18 -0
  61. package/dist/builder-variants.d.ts +226 -0
  62. package/dist/builder.d.ts +126 -0
  63. package/dist/builder.js +200 -0
  64. package/dist/constants.d.ts +2 -0
  65. package/dist/constants.js +4 -0
  66. package/dist/context.d.ts +6 -0
  67. package/dist/extensions/callable.d.ts +11 -10
  68. package/dist/extensions/callable.js +8 -0
  69. package/dist/helpers/base64url.d.ts +31 -0
  70. package/dist/helpers/base64url.js +48 -0
  71. package/dist/helpers/cookie.d.ts +50 -0
  72. package/dist/helpers/cookie.js +62 -0
  73. package/dist/helpers/encryption.d.ts +28 -0
  74. package/dist/helpers/encryption.js +90 -0
  75. package/dist/helpers/index.d.ts +5 -149
  76. package/dist/helpers/index.js +4 -0
  77. package/dist/helpers/signing.d.ts +50 -0
  78. package/dist/helpers/signing.js +78 -0
  79. package/dist/implementer-procedure.d.ts +15 -0
  80. package/dist/implementer-procedure.js +37 -0
  81. package/dist/implementer-router.d.ts +34 -0
  82. package/dist/implementer-router.js +98 -0
  83. package/dist/implementer.d.ts +16 -0
  84. package/dist/implementer.js +30 -0
  85. package/dist/index.d.ts +22 -196
  86. package/dist/index.js +16 -0
  87. package/dist/lazy.d.ts +20 -0
  88. package/dist/lazy.js +20 -0
  89. package/dist/middleware-decorated.d.ts +9 -0
  90. package/dist/middleware-decorated.js +67 -0
  91. package/dist/middleware.d.ts +54 -0
  92. package/dist/plugins/batch.d.ts +102 -0
  93. package/dist/plugins/batch.js +371 -0
  94. package/dist/plugins/cors.d.ts +69 -0
  95. package/dist/plugins/cors.js +119 -0
  96. package/dist/plugins/get-method-csrf-protection.d.ts +25 -0
  97. package/dist/plugins/get-method-csrf-protection.js +48 -0
  98. package/dist/plugins/head-method.d.ts +25 -0
  99. package/dist/plugins/head-method.js +107 -0
  100. package/dist/plugins/index.d.ts +15 -197
  101. package/dist/plugins/index.js +14 -0
  102. package/dist/plugins/method-override.d.ts +40 -0
  103. package/dist/plugins/method-override.js +54 -0
  104. package/dist/plugins/request-compression.d.ts +18 -0
  105. package/dist/plugins/request-compression.js +55 -0
  106. package/dist/plugins/request-headers.d.ts +24 -0
  107. package/dist/plugins/request-headers.js +28 -0
  108. package/dist/plugins/request-limit.d.ts +32 -0
  109. package/dist/plugins/request-limit.js +61 -0
  110. package/dist/plugins/response-compression.d.ts +62 -0
  111. package/dist/plugins/response-compression.js +157 -0
  112. package/dist/plugins/response-headers.d.ts +32 -0
  113. package/dist/plugins/response-headers.js +44 -0
  114. package/dist/plugins/rethrow.d.ts +35 -0
  115. package/dist/plugins/rethrow.js +47 -0
  116. package/dist/plugins/signal-sync.d.ts +15 -0
  117. package/dist/plugins/signal-sync.js +76 -0
  118. package/dist/plugins/timeout.d.ts +39 -0
  119. package/dist/plugins/timeout.js +62 -0
  120. package/dist/procedure-client.d.ts +18 -0
  121. package/dist/procedure-client.js +158 -0
  122. package/dist/procedure-decorated.d.ts +10 -0
  123. package/dist/procedure-decorated.js +36 -0
  124. package/dist/procedure-utils.d.ts +21 -0
  125. package/dist/procedure-utils.js +48 -0
  126. package/dist/procedure.d.ts +72 -0
  127. package/dist/procedure.js +18 -0
  128. package/dist/router-client.d.ts +21 -0
  129. package/dist/router-client.js +35 -0
  130. package/dist/router-hidden.js +12 -0
  131. package/dist/router-utils.d.ts +31 -0
  132. package/dist/router-utils.js +157 -0
  133. package/dist/router.d.ts +58 -0
  134. package/package.json +95 -63
  135. package/dist/adapters/crossws/index.d.mts +0 -54
  136. package/dist/adapters/crossws/index.mjs +0 -64
  137. package/dist/adapters/fetch/index.d.mts +0 -140
  138. package/dist/adapters/fetch/index.mjs +0 -204
  139. package/dist/adapters/message-port/index.d.mts +0 -74
  140. package/dist/adapters/message-port/index.mjs +0 -90
  141. package/dist/adapters/node/index.d.mts +0 -121
  142. package/dist/adapters/node/index.mjs +0 -179
  143. package/dist/adapters/standard/index.d.mts +0 -19
  144. package/dist/adapters/standard/index.mjs +0 -8
  145. package/dist/adapters/standard-peer/index.d.mts +0 -16
  146. package/dist/adapters/standard-peer/index.mjs +0 -2
  147. package/dist/adapters/websocket/index.d.mts +0 -61
  148. package/dist/adapters/websocket/index.mjs +0 -76
  149. package/dist/extensions/callable.d.mts +0 -10
  150. package/dist/extensions/callable.mjs +0 -11
  151. package/dist/helpers/index.d.mts +0 -149
  152. package/dist/helpers/index.mjs +0 -198
  153. package/dist/index.d.mts +0 -196
  154. package/dist/index.mjs +0 -385
  155. package/dist/plugins/index.d.mts +0 -197
  156. package/dist/plugins/index.mjs +0 -357
  157. package/dist/shared/server.BL22TloH.d.mts +0 -184
  158. package/dist/shared/server.BL22TloH.d.ts +0 -184
  159. package/dist/shared/server.BsNNjG5J.d.mts +0 -61
  160. package/dist/shared/server.BwHnWUuN.mjs +0 -222
  161. package/dist/shared/server.CX4vUnDk.mjs +0 -11
  162. package/dist/shared/server.Cj5lgPuG.d.mts +0 -66
  163. package/dist/shared/server.CjOb6ItT.mjs +0 -41
  164. package/dist/shared/server.CrlKQucM.mjs +0 -233
  165. package/dist/shared/server.DBANIheG.d.ts +0 -104
  166. package/dist/shared/server.DJIkub_t.d.mts +0 -104
  167. package/dist/shared/server.D_QauotT.mjs +0 -30
  168. package/dist/shared/server.GDpX6Df8.mjs +0 -271
  169. package/dist/shared/server.Pa0F03f_.d.ts +0 -61
  170. package/dist/shared/server.W91HSRkE.mjs +0 -9
  171. package/dist/shared/server.d_2NS3g5.d.ts +0 -66
@@ -0,0 +1,102 @@
1
+ import "../adapters/standard/index.js";
2
+ import { StandardHandlerPlugin } from "../adapters/standard/plugin.js";
3
+ import { StandardHandlerOptions, StandardHandlerRoutingInterceptorOptions } from "../adapters/standard/handler.js";
4
+ import { Context } from "../context.js";
5
+ import { Promisable, Value } from "@orpc/shared";
6
+ import { StandardHeaders, StandardLazyRequest, StandardResponse } from "@standard-server/core";
7
+ /**
8
+ * Content type for batch responses that use the length-prefixed binary framing,
9
+ * which they do in `streaming` mode, or when any sub-response contains binary or is a stream.
10
+ *
11
+ * Decoding is driven by the `standard-server` body hint, not this header,
12
+ * so it only serves to describe the payload to logs, proxies, and dev tools.
13
+ */
14
+ export declare const BATCH_CONTENT_TYPE = "application/vnd.orpc.batch";
15
+ export interface BatchHandlerPluginOptions<T extends Context> {
16
+ /**
17
+ * The max size of the batch allowed. Larger batches are rejected with a `413`,
18
+ * so keep the client Batch Link Plugin's `maxSize` at or below it.
19
+ *
20
+ * @default DEFAULT_BATCH_MAX_SIZE (10)
21
+ * @see {@link https://orpc.dev/docs/plugins/batch#max-size | Batch Plugin - Max Size}
22
+ */
23
+ maxSize?: Value<Promisable<number>, [options: StandardHandlerRoutingInterceptorOptions<T>]> | undefined;
24
+ /**
25
+ * Map each subrequest in the batch before it is processed.
26
+ *
27
+ * @default merges the batch request headers into the subrequest, giving them priority over
28
+ * the subrequest headers, and removes the `orpc-batch` header to prevent nested batching
29
+ */
30
+ mapSubrequest?: ((subrequest: StandardLazyRequest, batchOptions: StandardHandlerRoutingInterceptorOptions<T>) => StandardLazyRequest) | undefined;
31
+ /**
32
+ * Map each sub-response in the batch before it is sent.
33
+ *
34
+ * @default removes the headers the batch response already has with the same value, which the client
35
+ * merges back, and `set-cookie`, which the browser ignores there and JavaScript could read
36
+ */
37
+ mapSubresponse?: ((subresponse: StandardResponse, partialBatchResponse: Pick<StandardResponse, "status" | "headers">, batchOptions: StandardHandlerRoutingInterceptorOptions<T>) => StandardResponse) | undefined;
38
+ /**
39
+ * Success batch response status code.
40
+ *
41
+ * @default 207
42
+ */
43
+ successStatus?: Value<Promisable<number>, [batchOptions: StandardHandlerRoutingInterceptorOptions<T>]> | undefined;
44
+ /**
45
+ * Success batch response headers.
46
+ *
47
+ * In `buffered` mode, the batch response also gets the cookies its sub-responses set, and the headers they all share.
48
+ * In `streaming` mode, it gets only the cookies of the first sub-response to be ready.
49
+ * Headers set here win, and setting one to `undefined` keeps it off, except `set-cookie` and `vary`, which merge.
50
+ *
51
+ * @default {}
52
+ */
53
+ headers?: Value<Promisable<StandardHeaders>, [batchOptions: StandardHandlerRoutingInterceptorOptions<T>]> | undefined;
54
+ /**
55
+ * Keep-alive frames for batch responses that stream.
56
+ *
57
+ * A zero-length length-prefixed frame is sent periodically while the stream is idle
58
+ * (no message sent for `interval` ms). Clients ignore these frames.
59
+ *
60
+ * `false` disables it, an object customizes it.
61
+ *
62
+ * @default true
63
+ */
64
+ keepAlive?: boolean | {
65
+ /**
66
+ * Interval (in milliseconds) between keep-alive frames after the last message.
67
+ *
68
+ * @default 15000
69
+ */
70
+ interval?: number | undefined;
71
+ } | undefined;
72
+ }
73
+ /**
74
+ * Handles batch requests sent by the client Batch Link Plugin, splitting each
75
+ * batch into sub-requests and sending their responses back together.
76
+ *
77
+ * @remarks
78
+ * The client Batch Link Plugin's `mode` picks how each batch responds: `buffered` (default) waits for every
79
+ * sub-response and carries their cookies and shared headers; `streaming` starts once the first is ready and
80
+ * carries only its cookies.
81
+ *
82
+ * **Note**: HTTP/2 and later already multiplex requests over a single connection, which often makes this plugin unnecessary.
83
+ *
84
+ * @see {@link https://orpc.dev/docs/plugins/batch | Batch Plugin}
85
+ */
86
+ export declare class BatchHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
87
+ name: string;
88
+ /**
89
+ * Run batch interceptors inside the tracing interceptors
90
+ * so each subrequest gets its own span instead of sharing one batch-level span.
91
+ */
92
+ after: string[];
93
+ private readonly maxSize;
94
+ private readonly mapSubrequest;
95
+ private readonly mapSubresponse;
96
+ private readonly successStatus;
97
+ private readonly headers;
98
+ private readonly keepAliveEnabled;
99
+ private readonly keepAliveInterval;
100
+ constructor(options?: BatchHandlerPluginOptions<T>);
101
+ init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
102
+ }
@@ -0,0 +1,371 @@
1
+ import { isAsyncIteratorObject, listenAbort, promiseWithResolvers, toArray, value } from "@orpc/shared";
2
+ import { ORPCError } from "@orpc/client";
3
+ import { flattenStandardHeader, parseStandardUrl } from "@standard-server/core";
4
+ import { BATCH_HEADER, DEFAULT_BATCH_MAX_SIZE } from "@orpc/client/plugins";
5
+ import { ServerPeer, encodePeerMessage, isClientPeerSendMessage, isPeerMessage } from "@standard-server/peer";
6
+ // src/plugins/batch.ts
7
+ /**
8
+ * Content type for batch responses that use the length-prefixed binary framing,
9
+ * which they do in `streaming` mode, or when any sub-response contains binary or is a stream.
10
+ *
11
+ * Decoding is driven by the `standard-server` body hint, not this header,
12
+ * so it only serves to describe the payload to logs, proxies, and dev tools.
13
+ */
14
+ const BATCH_CONTENT_TYPE = "application/vnd.orpc.batch";
15
+ const textEncoder = /* @__PURE__ */ new TextEncoder();
16
+ function encodeLengthPrefix(length) {
17
+ const prefix = /* @__PURE__ */ new Uint8Array(4);
18
+ new DataView(prefix.buffer).setUint32(0, length, false);
19
+ return prefix;
20
+ }
21
+ /**
22
+ * Encodes a message as a length-prefixed frame: its 4-byte big-endian length, then its bytes.
23
+ */
24
+ async function encodeFrame(message) {
25
+ const encoded = await encodePeerMessage(message);
26
+ const bytes = typeof encoded === "string" ? textEncoder.encode(encoded) : encoded;
27
+ return [encodeLengthPrefix(bytes.byteLength), bytes];
28
+ }
29
+ /**
30
+ * Whether a header describes a subresponse's own body, which the batch response does not share.
31
+ */
32
+ function isBodyHeader(name) {
33
+ return name.startsWith("content-") || name === "standard-server";
34
+ }
35
+ /**
36
+ * Whether untrusted batch JSON is a list of client peer messages.
37
+ * `isPeerMessage` must run first, since `isClientPeerSendMessage` does not check `id` or `binary`.
38
+ */
39
+ function isClientPeerSendMessages(maybe) {
40
+ return Array.isArray(maybe) && maybe.every((m) => isPeerMessage(m) && isClientPeerSendMessage(m));
41
+ }
42
+ /**
43
+ * Errors should not occur at the routing interceptor level.
44
+ * Reject the promise so it can be handled by the unhandledRejection handler
45
+ * for global logging or error handling.
46
+ */
47
+ function internalError(err) {
48
+ Promise.reject(err);
49
+ return {
50
+ status: 500,
51
+ headers: {},
52
+ body: "Internal server error"
53
+ };
54
+ }
55
+ /**
56
+ * Resolves with the first subresponse head, or `undefined` if none has one.
57
+ */
58
+ function firstSubresponseHead(heads) {
59
+ return new Promise((resolve) => {
60
+ for (const promise of heads) promise.then((head) => head && resolve(head));
61
+ Promise.all(heads).then(() => resolve(void 0));
62
+ });
63
+ }
64
+ /**
65
+ * Sets the distinct cookies of every source on the headers.
66
+ */
67
+ function liftCookies(headers, sources) {
68
+ const cookies = new Set(sources.flatMap((source) => toArray(source["set-cookie"])));
69
+ return cookies.size > 0 ? {
70
+ ...headers,
71
+ "set-cookie": [...cookies]
72
+ } : headers;
73
+ }
74
+ /**
75
+ * Builds the batch response headers: the headers the subresponses all have with the same value, as
76
+ * the client does with the headers every subrequest shares, then the batch headers, which win, and
77
+ * the distinct cookies and `vary` fields of both. A cache must vary on whatever any subresponse varies on.
78
+ */
79
+ function liftSubresponseHeaders(batchHeaders, subresponseHeaders) {
80
+ const [first = {}, ...rest] = subresponseHeaders;
81
+ const shared = {};
82
+ for (const [name, value] of Object.entries(first)) {
83
+ if (value === void 0 || isBodyHeader(name)) continue;
84
+ const flattened = flattenStandardHeader(value);
85
+ if (rest.every((other) => flattenStandardHeader(other[name]) === flattened)) shared[name] = value;
86
+ }
87
+ const headers = liftCookies({
88
+ ...shared,
89
+ ...batchHeaders
90
+ }, [batchHeaders, ...subresponseHeaders]);
91
+ const varies = /* @__PURE__ */ new Map();
92
+ for (const value of [batchHeaders, ...subresponseHeaders].flatMap((other) => toArray(other.vary))) for (const field of value.split(",")) {
93
+ const name = field.trim();
94
+ const key = name.toLowerCase();
95
+ if (name && !varies.has(key)) varies.set(key, name);
96
+ }
97
+ if (varies.size > 0) headers.vary = varies.has("*") ? "*" : [...varies.values()].join(", ");
98
+ return headers;
99
+ }
100
+ /**
101
+ * Handles batch requests sent by the client Batch Link Plugin, splitting each
102
+ * batch into sub-requests and sending their responses back together.
103
+ *
104
+ * @remarks
105
+ * The client Batch Link Plugin's `mode` picks how each batch responds: `buffered` (default) waits for every
106
+ * sub-response and carries their cookies and shared headers; `streaming` starts once the first is ready and
107
+ * carries only its cookies.
108
+ *
109
+ * **Note**: HTTP/2 and later already multiplex requests over a single connection, which often makes this plugin unnecessary.
110
+ *
111
+ * @see {@link https://orpc.dev/docs/plugins/batch | Batch Plugin}
112
+ */
113
+ var BatchHandlerPlugin = class {
114
+ name = "~batch";
115
+ /**
116
+ * Run batch interceptors inside the tracing interceptors
117
+ * so each subrequest gets its own span instead of sharing one batch-level span.
118
+ */
119
+ after = ["~tracing"];
120
+ maxSize;
121
+ mapSubrequest;
122
+ mapSubresponse;
123
+ successStatus;
124
+ headers;
125
+ keepAliveEnabled;
126
+ keepAliveInterval;
127
+ constructor(options = {}) {
128
+ this.maxSize = options.maxSize ?? DEFAULT_BATCH_MAX_SIZE;
129
+ this.mapSubrequest = options.mapSubrequest ?? ((subRequest, { request: batchRequest }) => ({
130
+ ...subRequest,
131
+ headers: {
132
+ ...subRequest.headers,
133
+ /**
134
+ * The batch request headers win over the subrequest ones. They are the only ones the
135
+ * transport actually saw, so headers the browser injects on its own, such as `cookie`
136
+ * or `origin`, cannot be overridden by a subrequest, which is just request payload.
137
+ */
138
+ ...batchRequest.headers,
139
+ [BATCH_HEADER]: void 0
140
+ }
141
+ }));
142
+ this.mapSubresponse = options.mapSubresponse ?? ((subresponse, { headers: batchHeaders }) => {
143
+ const headers = {};
144
+ for (const [name, value] of Object.entries(subresponse.headers)) if (name !== "set-cookie" && flattenStandardHeader(value) !== flattenStandardHeader(batchHeaders[name])) headers[name] = value;
145
+ return {
146
+ ...subresponse,
147
+ headers
148
+ };
149
+ });
150
+ this.successStatus = options.successStatus ?? 207;
151
+ this.headers = options.headers ?? {};
152
+ this.keepAliveEnabled = options.keepAlive !== false;
153
+ this.keepAliveInterval = (typeof options.keepAlive === "object" ? options.keepAlive.interval : void 0) ?? 15e3;
154
+ }
155
+ init(options) {
156
+ const routingInterceptor = async (interceptorOptions) => {
157
+ const batchHeader = flattenStandardHeader(interceptorOptions.request.headers[BATCH_HEADER]);
158
+ if (batchHeader === void 0) return interceptorOptions.next();
159
+ const streaming = batchHeader === "streaming";
160
+ let messages;
161
+ try {
162
+ if (interceptorOptions.request.method === "GET") {
163
+ const [, search] = parseStandardUrl(interceptorOptions.request.url);
164
+ const data = new URLSearchParams(search).getAll("data").at(-1);
165
+ if (!data) return {
166
+ matched: true,
167
+ response: {
168
+ status: 400,
169
+ headers: {},
170
+ body: "Missing data parameter for batch request"
171
+ }
172
+ };
173
+ const mightBeMessages = JSON.parse(data);
174
+ if (!isClientPeerSendMessages(mightBeMessages)) return {
175
+ matched: true,
176
+ response: {
177
+ status: 400,
178
+ headers: {},
179
+ body: "Invalid batch request data parameter"
180
+ }
181
+ };
182
+ messages = mightBeMessages;
183
+ } else {
184
+ const mightBeMessages = await interceptorOptions.request.resolveBody();
185
+ if (!isClientPeerSendMessages(mightBeMessages)) return {
186
+ matched: true,
187
+ response: {
188
+ status: 400,
189
+ headers: {},
190
+ body: "Invalid batch request body"
191
+ }
192
+ };
193
+ messages = mightBeMessages;
194
+ }
195
+ } catch (error) {
196
+ return {
197
+ matched: true,
198
+ response: {
199
+ status: 400,
200
+ headers: {},
201
+ body: error instanceof ORPCError ? error.message : "Invalid batch request"
202
+ }
203
+ };
204
+ }
205
+ const outerMethod = interceptorOptions.request.method;
206
+ if ((outerMethod === "GET" || outerMethod === "QUERY") && messages.some((message) => message.kind === "request" && message.json.method !== outerMethod)) return {
207
+ matched: true,
208
+ response: {
209
+ status: 400,
210
+ headers: {},
211
+ body: `${outerMethod} batch requests only accept ${outerMethod} sub-requests`
212
+ }
213
+ };
214
+ const maxSize = await value(this.maxSize, interceptorOptions);
215
+ if (messages.length > maxSize) return {
216
+ matched: true,
217
+ response: {
218
+ status: 413,
219
+ headers: {},
220
+ body: "Batch request size exceeds the maximum allowed size"
221
+ }
222
+ };
223
+ const status = await value(this.successStatus, interceptorOptions);
224
+ const headers = await value(this.headers, interceptorOptions);
225
+ const bufferedMessages = [];
226
+ let streamController;
227
+ let keepAliveTimer;
228
+ const ready = messages.map(() => promiseWithResolvers());
229
+ const heads = ready.map(({ promise }) => promise);
230
+ const lifting = streaming ? firstSubresponseHead(heads).then((head) => ({
231
+ batchHeaders: liftCookies(headers, [headers, head?.headers ?? {}]),
232
+ streams: true
233
+ })) : Promise.all(heads).then((all) => {
234
+ const present = all.filter((head) => head !== void 0);
235
+ return {
236
+ batchHeaders: liftSubresponseHeaders(headers, present.map((head) => head.headers)),
237
+ streams: present.some((head) => head.stream)
238
+ };
239
+ });
240
+ const clearKeepAlive = () => {
241
+ if (keepAliveTimer !== void 0) {
242
+ clearInterval(keepAliveTimer);
243
+ keepAliveTimer = void 0;
244
+ }
245
+ };
246
+ const scheduleKeepAlive = () => {
247
+ if (!this.keepAliveEnabled) return;
248
+ clearKeepAlive();
249
+ keepAliveTimer = setInterval(() => {
250
+ try {
251
+ streamController.enqueue(encodeLengthPrefix(0));
252
+ } catch {
253
+ clearKeepAlive();
254
+ }
255
+ }, this.keepAliveInterval);
256
+ };
257
+ const enqueueMessage = async (message) => {
258
+ for (const chunk of await encodeFrame(message)) streamController.enqueue(chunk);
259
+ scheduleKeepAlive();
260
+ };
261
+ const peer = new ServerPeer(async (message) => {
262
+ if (streamController === void 0) bufferedMessages.push(message);
263
+ else await enqueueMessage(message);
264
+ });
265
+ const handleIndividualRequest = async (request, markReady) => {
266
+ let response;
267
+ try {
268
+ request = this.mapSubrequest(request, interceptorOptions);
269
+ const result = await interceptorOptions.next({
270
+ ...interceptorOptions,
271
+ request
272
+ });
273
+ response = result.matched ? result.response : {
274
+ status: 404,
275
+ headers: {},
276
+ body: "No procedure matched"
277
+ };
278
+ } catch (err) {
279
+ response = internalError(err);
280
+ }
281
+ markReady({
282
+ headers: response.headers,
283
+ stream: response.body instanceof ReadableStream || isAsyncIteratorObject(response.body)
284
+ });
285
+ const { batchHeaders } = await lifting;
286
+ try {
287
+ return this.mapSubresponse(response, {
288
+ status,
289
+ headers: batchHeaders
290
+ }, interceptorOptions);
291
+ } catch (err) {
292
+ return internalError(err);
293
+ }
294
+ };
295
+ const runSubrequests = async () => {
296
+ const promise = Promise.all(messages.map((msg, i) => {
297
+ const { resolve } = ready[i];
298
+ return peer.message(msg, (request) => handleIndividualRequest(request, resolve)).finally(() => resolve(void 0));
299
+ }));
300
+ const unlisten = listenAbort(interceptorOptions.request.signal, (reason) => void peer.close(reason));
301
+ try {
302
+ await promise;
303
+ } finally {
304
+ unlisten();
305
+ }
306
+ };
307
+ const subrequests = runSubrequests();
308
+ const { batchHeaders, streams } = await lifting;
309
+ const framedHeaders = {
310
+ ...batchHeaders,
311
+ "content-type": batchHeaders["content-type"] ?? "application/vnd.orpc.batch"
312
+ };
313
+ if (!streams) {
314
+ await subrequests;
315
+ await peer.close();
316
+ if (bufferedMessages.some((msg) => msg.binary !== void 0)) {
317
+ const chunks = (await Promise.all(bufferedMessages.map(encodeFrame))).flat();
318
+ return {
319
+ matched: true,
320
+ response: {
321
+ status,
322
+ headers: framedHeaders,
323
+ body: new Blob(chunks)
324
+ }
325
+ };
326
+ }
327
+ return {
328
+ matched: true,
329
+ response: {
330
+ status,
331
+ headers: batchHeaders,
332
+ body: bufferedMessages
333
+ }
334
+ };
335
+ }
336
+ const body = new ReadableStream({
337
+ start(controller) {
338
+ streamController = controller;
339
+ scheduleKeepAlive();
340
+ },
341
+ async cancel(reason) {
342
+ clearKeepAlive();
343
+ await peer.close(reason);
344
+ }
345
+ });
346
+ const flushed = Promise.all(bufferedMessages.splice(0).map(enqueueMessage));
347
+ Promise.all([flushed, subrequests]).then(async () => {
348
+ clearKeepAlive();
349
+ streamController.close();
350
+ await peer.close();
351
+ }).catch(async (error) => {
352
+ clearKeepAlive();
353
+ streamController.error(error);
354
+ await peer.close(error);
355
+ });
356
+ return {
357
+ matched: true,
358
+ response: {
359
+ status,
360
+ headers: framedHeaders,
361
+ body
362
+ }
363
+ };
364
+ };
365
+ return {
366
+ ...options,
367
+ routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
368
+ };
369
+ }
370
+ };
371
+ export { BATCH_CONTENT_TYPE, BatchHandlerPlugin };
@@ -0,0 +1,69 @@
1
+ import "../adapters/standard/index.js";
2
+ import { StandardHandlerPlugin } from "../adapters/standard/plugin.js";
3
+ import { StandardHandlerOptions, StandardHandlerRoutingInterceptorOptions } from "../adapters/standard/handler.js";
4
+ import { Context } from "../context.js";
5
+ import { Promisable, Value } from "@orpc/shared";
6
+ export interface CORSHandlerPluginOptions<T extends Context> {
7
+ /**
8
+ * Configures the `Access-Control-Allow-Origin` header.
9
+ * Can be a string, an array of allowed origins, or a function (optionally async) that returns the allowed origin(s).
10
+ *
11
+ * A function must only return origins it has validated: returning the incoming origin
12
+ * unchecked allows every origin.
13
+ *
14
+ * @default '*'
15
+ */
16
+ origin?: Value<Promisable<string | readonly string[] | null | undefined>, [origin: string, options: StandardHandlerRoutingInterceptorOptions<T>]> | undefined;
17
+ /**
18
+ * Configures the `Timing-Allow-Origin` header.
19
+ * Can be a string, an array of allowed origins, or a function (optionally async) that returns the allowed origin(s).
20
+ */
21
+ timingOrigin?: Value<Promisable<string | readonly string[] | null | undefined>, [origin: string, options: StandardHandlerRoutingInterceptorOptions<T>]> | undefined;
22
+ /**
23
+ * Configures the `Access-Control-Allow-Methods` header for preflight requests.
24
+ *
25
+ * @default ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH', 'QUERY']
26
+ */
27
+ allowMethods?: readonly string[] | undefined;
28
+ /**
29
+ * Configures the `Access-Control-Allow-Headers` header for preflight requests.
30
+ * Falls back to the request's `Access-Control-Request-Headers` if not set.
31
+ * When set, it replaces that fallback, so include every non-safelisted header
32
+ * clients send, such as `Content-Type`.
33
+ */
34
+ allowHeaders?: readonly string[] | undefined;
35
+ /**
36
+ * Configures the `Access-Control-Max-Age` header (in seconds) for preflight requests.
37
+ */
38
+ maxAge?: number | undefined;
39
+ /**
40
+ * Configures the `Access-Control-Allow-Credentials` header.
41
+ *
42
+ * When enabled, `origin` must allow trusted origins only: every allowed origin can send
43
+ * authenticated requests as your users and read the responses.
44
+ */
45
+ credentials?: boolean | undefined;
46
+ /**
47
+ * Configures the `Access-Control-Expose-Headers` header.
48
+ */
49
+ exposeHeaders?: readonly string[] | undefined;
50
+ }
51
+ /**
52
+ * Configures the [CORS Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
53
+ * for your API, including preflight requests.
54
+ *
55
+ * @see {@link https://orpc.dev/docs/plugins/cors | CORS Handler Plugin}
56
+ */
57
+ export declare class CORSHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
58
+ private readonly options;
59
+ name: string;
60
+ /**
61
+ * - Do not create spans for CORS preflight requests.
62
+ * - Run CORS interceptors before batch interceptors so headers are applied to
63
+ * the actual response rather than sub-responses.
64
+ */
65
+ after: string[];
66
+ constructor(options?: CORSHandlerPluginOptions<T>);
67
+ init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
68
+ private resolveOrigin;
69
+ }
@@ -0,0 +1,119 @@
1
+ import { toArray } from "@orpc/shared";
2
+ import { flattenStandardHeader } from "@standard-server/core";
3
+ // src/plugins/cors.ts
4
+ /**
5
+ * Configures the [CORS Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
6
+ * for your API, including preflight requests.
7
+ *
8
+ * @see {@link https://orpc.dev/docs/plugins/cors | CORS Handler Plugin}
9
+ */
10
+ var CORSHandlerPlugin = class {
11
+ options;
12
+ name = "~cors";
13
+ /**
14
+ * - Do not create spans for CORS preflight requests.
15
+ * - Run CORS interceptors before batch interceptors so headers are applied to
16
+ * the actual response rather than sub-responses.
17
+ */
18
+ after = ["~tracing", "~batch"];
19
+ constructor(options = {}) {
20
+ const defaults = {
21
+ origin: "*",
22
+ allowMethods: [
23
+ "GET",
24
+ "HEAD",
25
+ "PUT",
26
+ "POST",
27
+ "DELETE",
28
+ "PATCH",
29
+ "QUERY"
30
+ ]
31
+ };
32
+ this.options = {
33
+ ...defaults,
34
+ ...options
35
+ };
36
+ }
37
+ init(options) {
38
+ const corsHeadersInterceptor = async (interceptorOptions) => {
39
+ const result = await interceptorOptions.next();
40
+ if (!result.matched) return result;
41
+ const resHeaders = { ...result.response.headers };
42
+ const origin = flattenStandardHeader(interceptorOptions.request.headers.origin);
43
+ const allowOrigin = await this.resolveOrigin(this.options.origin, origin, interceptorOptions);
44
+ const timingAllowOrigin = await this.resolveOrigin(this.options.timingOrigin, origin, interceptorOptions);
45
+ if (allowOrigin.value !== void 0) resHeaders["access-control-allow-origin"] = allowOrigin.value;
46
+ if (timingAllowOrigin.value !== void 0) resHeaders["timing-allow-origin"] = timingAllowOrigin.value;
47
+ if (allowOrigin.varies || timingAllowOrigin.varies) {
48
+ const existingVary = flattenStandardHeader(resHeaders.vary);
49
+ if (!existingVary?.split(",").some((v) => v.trim().toLowerCase() === "origin")) resHeaders.vary = existingVary ? `${existingVary}, Origin` : "Origin";
50
+ }
51
+ if (this.options.credentials) resHeaders["access-control-allow-credentials"] = "true";
52
+ if (this.options.exposeHeaders?.length) resHeaders["access-control-expose-headers"] = flattenStandardHeader(this.options.exposeHeaders);
53
+ return {
54
+ ...result,
55
+ response: {
56
+ ...result.response,
57
+ headers: resHeaders
58
+ }
59
+ };
60
+ };
61
+ const preflightInterceptor = async (interceptorOptions) => {
62
+ if (interceptorOptions.request.method === "OPTIONS") {
63
+ const resHeaders = {};
64
+ if (this.options.maxAge !== void 0) resHeaders["access-control-max-age"] = this.options.maxAge.toString();
65
+ if (this.options.allowMethods?.length) resHeaders["access-control-allow-methods"] = flattenStandardHeader(this.options.allowMethods);
66
+ const allowHeaders = this.options.allowHeaders ?? interceptorOptions.request.headers["access-control-request-headers"];
67
+ if (typeof allowHeaders === "string" || allowHeaders?.length) resHeaders["access-control-allow-headers"] = flattenStandardHeader(allowHeaders);
68
+ return {
69
+ matched: true,
70
+ response: {
71
+ status: 204,
72
+ headers: resHeaders,
73
+ body: void 0
74
+ }
75
+ };
76
+ }
77
+ return interceptorOptions.next();
78
+ };
79
+ return {
80
+ ...options,
81
+ routingInterceptors: [
82
+ corsHeadersInterceptor,
83
+ preflightInterceptor,
84
+ ...toArray(options.routingInterceptors)
85
+ ]
86
+ };
87
+ }
88
+ async resolveOrigin(option, origin, interceptorOptions) {
89
+ if (typeof option === "function") {
90
+ if (origin === void 0) return {
91
+ value: void 0,
92
+ varies: true
93
+ };
94
+ const allowed = toArray(await option(origin, interceptorOptions));
95
+ if (allowed.includes("*")) return {
96
+ value: "*",
97
+ varies: true
98
+ };
99
+ return {
100
+ value: allowed.includes(origin) ? origin : void 0,
101
+ varies: true
102
+ };
103
+ }
104
+ const allowed = toArray(await option);
105
+ if (allowed.includes("*")) return {
106
+ value: "*",
107
+ varies: false
108
+ };
109
+ if (origin !== void 0 && allowed.includes(origin)) return {
110
+ value: origin,
111
+ varies: true
112
+ };
113
+ return {
114
+ value: void 0,
115
+ varies: allowed.length > 0
116
+ };
117
+ }
118
+ };
119
+ export { CORSHandlerPlugin };
@@ -0,0 +1,25 @@
1
+ import "../adapters/standard/index.js";
2
+ import { StandardHandlerPlugin } from "../adapters/standard/plugin.js";
3
+ import { StandardHandlerOptions } from "../adapters/standard/handler.js";
4
+ import { Context } from "../context.js";
5
+ /**
6
+ * Adds Cross-Site Request Forgery (CSRF) protection that makes the safe `GET` method as
7
+ * secure as unsafe ones such as `POST`. It rejects `GET` requests arriving as top-level
8
+ * navigations initiated cross-site or from outside the browser, the only context where
9
+ * another site can make a browser attach `SameSite=Lax` cookies to a safe-method request.
10
+ * Requests matching no procedure stay unmatched, so they can fall through to whatever
11
+ * serves the rest of the site.
12
+ *
13
+ * @remarks
14
+ * **Note**: Requests browsers send without `SameSite=Lax` cookies, such as cross-site `fetch`
15
+ * and `<img>`, pass through, so procedures stay reachable from other sites. This safeguard
16
+ * requires authentication cookies explicitly marked `SameSite=Lax` or `SameSite=Strict`,
17
+ * since browsers may attach other cookies to the requests that pass.
18
+ *
19
+ * @see {@link https://orpc.dev/docs/plugins/get-method-csrf-protection | GET Method CSRF Protection Plugin}
20
+ */
21
+ export declare class GetMethodCsrfProtectionHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
22
+ name: string;
23
+ init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
24
+ private isAllowed;
25
+ }