@orpc/server 2.0.0-beta.43 → 2.0.0-beta.44

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.
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
  <img alt="Discord" src="https://img.shields.io/discord/1308966753044398161?color=7389D8&label&logo=discord&logoColor=ffffff" />
18
18
  </a>
19
19
  <a href="https://deepwiki.com/middleapi/orpc">
20
- <img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki">
20
+ <img alt="Ask DeepWiki" src="https://img.shields.io/badge/Ask-DeepWiki-blue" />
21
21
  </a>
22
22
  </div>
23
23
 
@@ -95,14 +95,6 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
95
95
  </tr>
96
96
  </table>
97
97
 
98
- ### Special Sponsors
99
-
100
- <table>
101
- <tr>
102
- <td align="center"><a href="http://twitter.com/rauchg?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Guillermo Rauch"><img src="https://avatars.githubusercontent.com/u/13041?u=1ee8d111657cdd02ff6d253df00978d17ee6d722&amp;v=4" width="279" alt="Guillermo Rauch"/><br />Guillermo Rauch</a></td>
103
- </tr>
104
- </table>
105
-
106
98
  ### Premium Sponsors
107
99
 
108
100
  <table>
@@ -178,7 +170,7 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
178
170
  </tr>
179
171
  </table>
180
172
 
181
- With thanks to [38 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.
173
+ With thanks to [39 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.
182
174
 
183
175
  ## References
184
176
 
@@ -1,7 +1,7 @@
1
1
  import { resolveMaybeOptionalOptions } from '@orpc/shared';
2
2
  import { toStandardLazyRequest, sendStandardResponse } from '@standard-server/aws-lambda';
3
3
  import { r as resolveFriendlyStandardHandlerHandleOptions } from '../../shared/server.W91HSRkE.mjs';
4
- import { R as RPCHandlerCodec, S as StandardHandler } from '../../shared/server.B1ecixZJ.mjs';
4
+ import { R as RPCHandlerCodec, S as StandardHandler } from '../../shared/server.B34dZrzY.mjs';
5
5
  import '@orpc/client';
6
6
  import '@standard-server/core';
7
7
  import '../../shared/server.FEqy7Hrj.mjs';
@@ -1,7 +1,7 @@
1
1
  import { resolveMaybeOptionalOptions } from '@orpc/shared';
2
2
  import { ServerPeer, encodePeerMessage, decodePeerMessage, isClientPeerSendMessage } from '@standard-server/peer';
3
3
  import { createStandardPeerRequestHandler } from '../standard-peer/index.mjs';
4
- import { R as RPCHandlerCodec, S as StandardHandler } from '../../shared/server.B1ecixZJ.mjs';
4
+ import { R as RPCHandlerCodec, S as StandardHandler } from '../../shared/server.B34dZrzY.mjs';
5
5
  import '../../shared/server.W91HSRkE.mjs';
6
6
  import '@orpc/client';
7
7
  import '@standard-server/core';
@@ -1,7 +1,7 @@
1
1
  import { resolveMaybeOptionalOptions } from '@orpc/shared';
2
2
  import { toStandardLazyRequest, sendStandardResponse } from '@standard-server/fastify';
3
3
  import { r as resolveFriendlyStandardHandlerHandleOptions } from '../../shared/server.W91HSRkE.mjs';
4
- import { R as RPCHandlerCodec, S as StandardHandler } from '../../shared/server.B1ecixZJ.mjs';
4
+ import { R as RPCHandlerCodec, S as StandardHandler } from '../../shared/server.B34dZrzY.mjs';
5
5
  import '@orpc/client';
6
6
  import '@standard-server/core';
7
7
  import '../../shared/server.FEqy7Hrj.mjs';
@@ -2,7 +2,7 @@ export { R as BodyLimitPlugin } from '../../shared/server.1ExWFMze.mjs';
2
2
  import { resolveMaybeOptionalOptions } from '@orpc/shared';
3
3
  import { toStandardLazyRequest, toFetchResponse } from '@standard-server/fetch';
4
4
  import { r as resolveFriendlyStandardHandlerHandleOptions } from '../../shared/server.W91HSRkE.mjs';
5
- import { R as RPCHandlerCodec, S as StandardHandler } from '../../shared/server.B1ecixZJ.mjs';
5
+ import { R as RPCHandlerCodec, S as StandardHandler } from '../../shared/server.B34dZrzY.mjs';
6
6
  import '@orpc/client';
7
7
  import '@standard-server/core';
8
8
  import '../../shared/server.FEqy7Hrj.mjs';
@@ -2,7 +2,7 @@ import { onMessagePortMessage, onMessagePortClose, postMessagePortMessage } from
2
2
  import { value, resolveMaybeOptionalOptions } from '@orpc/shared';
3
3
  import { ServerPeer, encodePeerMessage, decodePeerMessage, isClientPeerSendMessage, isPeerMessage } from '@standard-server/peer';
4
4
  import { createStandardPeerRequestHandler } from '../standard-peer/index.mjs';
5
- import { R as RPCHandlerCodec, S as StandardHandler } from '../../shared/server.B1ecixZJ.mjs';
5
+ import { R as RPCHandlerCodec, S as StandardHandler } from '../../shared/server.B34dZrzY.mjs';
6
6
  import '../../shared/server.W91HSRkE.mjs';
7
7
  import '@orpc/client';
8
8
  import '@standard-server/core';
@@ -2,7 +2,7 @@ export { R as BodyLimitPlugin } from '../../shared/server.1ExWFMze.mjs';
2
2
  import { resolveMaybeOptionalOptions } from '@orpc/shared';
3
3
  import { toStandardLazyRequest, sendStandardResponse } from '@standard-server/node';
4
4
  import { r as resolveFriendlyStandardHandlerHandleOptions } from '../../shared/server.W91HSRkE.mjs';
5
- import { R as RPCHandlerCodec, S as StandardHandler } from '../../shared/server.B1ecixZJ.mjs';
5
+ import { R as RPCHandlerCodec, S as StandardHandler } from '../../shared/server.B34dZrzY.mjs';
6
6
  import '@orpc/client';
7
7
  import '@standard-server/core';
8
8
  import '@standard-server/fetch';
@@ -43,6 +43,8 @@ declare class RPCMatcher {
43
43
  private readonly rootRouter;
44
44
  private readonly tree;
45
45
  private readonly pendingLazyRouters;
46
+ /** segment count of the deepest pending key, never lowered so it stays an upper bound */
47
+ private maxPendingLazyRouterDepth;
46
48
  constructor(router: AnyRouter, options?: RPCMatcherOptions);
47
49
  private index;
48
50
  match(method: StandardMethod, pathname: `/${string}`, prefix: `/${string}` | undefined): Promise<{
@@ -50,7 +52,6 @@ declare class RPCMatcher {
50
52
  procedure: AnyProcedure;
51
53
  } | undefined>;
52
54
  private resolvePendingLazyRouters;
53
- private loadPendingLazyRouters;
54
55
  private loadPendingLazyRouter;
55
56
  private indexPendingLazyRouter;
56
57
  private resolveProcedure;
@@ -43,6 +43,8 @@ declare class RPCMatcher {
43
43
  private readonly rootRouter;
44
44
  private readonly tree;
45
45
  private readonly pendingLazyRouters;
46
+ /** segment count of the deepest pending key, never lowered so it stays an upper bound */
47
+ private maxPendingLazyRouterDepth;
46
48
  constructor(router: AnyRouter, options?: RPCMatcherOptions);
47
49
  private index;
48
50
  match(method: StandardMethod, pathname: `/${string}`, prefix: `/${string}` | undefined): Promise<{
@@ -50,7 +52,6 @@ declare class RPCMatcher {
50
52
  procedure: AnyProcedure;
51
53
  } | undefined>;
52
54
  private resolvePendingLazyRouters;
53
- private loadPendingLazyRouters;
54
55
  private loadPendingLazyRouter;
55
56
  private indexPendingLazyRouter;
56
57
  private resolveProcedure;
@@ -1,4 +1,4 @@
1
- export { C as CompositeStandardHandlerPlugin, R as RPCHandlerCodec, a as RPCMatcher, b as RPC_DEFAULT_ALLOW_METHODS, S as StandardHandler, T as TracingHandlerPlugin } from '../../shared/server.B1ecixZJ.mjs';
1
+ export { C as CompositeStandardHandlerPlugin, R as RPCHandlerCodec, a as RPCMatcher, b as RPC_DEFAULT_ALLOW_METHODS, S as StandardHandler, T as TracingHandlerPlugin } from '../../shared/server.B34dZrzY.mjs';
2
2
  export { r as resolveFriendlyStandardHandlerHandleOptions } from '../../shared/server.W91HSRkE.mjs';
3
3
  import '@orpc/client';
4
4
  import '@orpc/shared';
@@ -1,7 +1,7 @@
1
1
  import { toStringOrBytes, resolveMaybeOptionalOptions, sequential, loadBytes } from '@orpc/shared';
2
2
  import { ServerPeer, encodePeerMessage, decodePeerMessage, isClientPeerSendMessage } from '@standard-server/peer';
3
3
  import { createStandardPeerRequestHandler } from '../standard-peer/index.mjs';
4
- import { R as RPCHandlerCodec, S as StandardHandler } from '../../shared/server.B1ecixZJ.mjs';
4
+ import { R as RPCHandlerCodec, S as StandardHandler } from '../../shared/server.B34dZrzY.mjs';
5
5
  import '../../shared/server.W91HSRkE.mjs';
6
6
  import '@orpc/client';
7
7
  import '@standard-server/core';
@@ -17,7 +17,8 @@ import { ParseOptions, SetCookie, StringifyOptions } from 'cookie';
17
17
  declare function encodeBase64url(data: Uint8Array): string;
18
18
  /**
19
19
  * Decodes a base64url string to Uint8Array
20
- * Returns undefined if the input is invalid
20
+ * Only accepts canonical, unpadded base64url (the form produced by `encodeBase64url`),
21
+ * returns undefined for anything else, including padded or standard base64 input
21
22
  *
22
23
  * @example
23
24
  * ```ts
@@ -17,7 +17,8 @@ import { ParseOptions, SetCookie, StringifyOptions } from 'cookie';
17
17
  declare function encodeBase64url(data: Uint8Array): string;
18
18
  /**
19
19
  * Decodes a base64url string to Uint8Array
20
- * Returns undefined if the input is invalid
20
+ * Only accepts canonical, unpadded base64url (the form produced by `encodeBase64url`),
21
+ * returns undefined for anything else, including padded or standard base64 input
21
22
  *
22
23
  * @example
23
24
  * ```ts
@@ -24,7 +24,7 @@ function decodeBase64url(base64url) {
24
24
  for (let i = 0; i < binaryString.length; i++) {
25
25
  bytes[i] = binaryString.charCodeAt(i);
26
26
  }
27
- return bytes;
27
+ return encodeBase64url(bytes) === base64url ? bytes : void 0;
28
28
  } catch {
29
29
  return void 0;
30
30
  }
@@ -8,7 +8,7 @@ import '@orpc/contract';
8
8
 
9
9
  /**
10
10
  * Content type for batch responses that use the length-prefixed binary framing
11
- * (streaming mode, and buffered mode when any sub-response contains binary).
11
+ * (streaming mode, and buffered mode when any sub-response contains binary or is a stream).
12
12
  *
13
13
  * Decoding is driven by the `standard-server` body hint, not this header,
14
14
  * so it only serves to describe the payload to logs, proxies, and dev tools.
@@ -45,7 +45,7 @@ interface BatchHandlerPluginOptions<T extends Context> {
45
45
  *
46
46
  * When enabled, a zero-length length-prefixed frame is sent periodically while the
47
47
  * stream is idle (no message sent for `interval` ms). Clients ignore these frames.
48
- * Only applies to streaming mode.
48
+ * Only applies to streaming responses: streaming mode, or a buffered batch with a stream sub-response.
49
49
  *
50
50
  * @default { enabled: true, interval: 15000 }
51
51
  */
@@ -69,6 +69,9 @@ interface BatchHandlerPluginOptions<T extends Context> {
69
69
  * batch into sub-requests and streaming their responses back together.
70
70
  *
71
71
  * @remarks
72
+ * In buffered mode, the batch still streams once a sub-response is a stream
73
+ * (an event iterator or a `ReadableStream`), since it may never end.
74
+ *
72
75
  * **Note**: HTTP/2 and later already multiplex requests over a single connection, which often makes this plugin unnecessary.
73
76
  *
74
77
  * @see {@link https://orpc.dev/docs/plugins/batch | Batch Plugin}
@@ -95,6 +98,9 @@ interface CORSHandlerPluginOptions<T extends Context> {
95
98
  * Configures the `Access-Control-Allow-Origin` header.
96
99
  * Can be a string, an array of allowed origins, or a function (optionally async) that returns the allowed origin(s).
97
100
  *
101
+ * A function must only return origins it has validated: returning the incoming origin
102
+ * unchecked allows every origin.
103
+ *
98
104
  * @default '*'
99
105
  */
100
106
  origin?: Value<Promisable<string | readonly string[] | null | undefined>, [origin: string, options: StandardHandlerRoutingInterceptorOptions<T>]>;
@@ -122,6 +128,9 @@ interface CORSHandlerPluginOptions<T extends Context> {
122
128
  maxAge?: number;
123
129
  /**
124
130
  * Configures the `Access-Control-Allow-Credentials` header.
131
+ *
132
+ * When enabled, `origin` must allow trusted origins only: every allowed origin can send
133
+ * authenticated requests as your users and read the responses.
125
134
  */
126
135
  credentials?: boolean;
127
136
  /**
@@ -207,31 +216,6 @@ declare class MethodOverrideHandlerPlugin<T extends Context> implements Standard
207
216
  init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
208
217
  }
209
218
 
210
- /**
211
- * Rejects requests whose decoded input contains prototype-polluting keys: an own
212
- * `__proto__` key, or an own `constructor` key holding a `prototype` key. oRPC's own
213
- * decoding never assigns through the prototype chain, so this plugin exists to stop such
214
- * keys from reaching application code that merges, clones, or path-sets input with a
215
- * library vulnerable to prototype pollution. An `AsyncIteratorObject` input is checked
216
- * value by value as it arrives, and a polluting value fails that iteration instead.
217
- *
218
- * @see {@link https://orpc.dev/docs/plugins/prototype-pollution-protection | Prototype Pollution Protection Plugin}
219
- */
220
- declare class PrototypePollutionProtectionHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
221
- name: string;
222
- init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
223
- private rejectPollutingInput;
224
- /**
225
- * Walks the containers the built-in codecs can produce: arrays, maps, sets, and plain
226
- * objects, including null-prototype ones. Arrays get the same key checks as plain objects,
227
- * since structured clone, as used over MessagePort, keeps named keys like `__proto__` on
228
- * them. Other objects, such as files and dates, carry no attacker-authored keys and are
229
- * left alone. The walk is iterative, so input nested deeper than the call stack allows
230
- * still gets the intended verdict instead of a `RangeError`.
231
- */
232
- private containsPollutingKey;
233
- }
234
-
235
219
  /**
236
220
  * Decompresses incoming request bodies based on the Content-Encoding header,
237
221
  * supporting gzip, deflate, and deflate-raw.
@@ -419,5 +403,5 @@ declare class TimeoutHandlerPlugin<T extends Context> implements StandardHandler
419
403
  init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
420
404
  }
421
405
 
422
- export { BATCH_CONTENT_TYPE, BatchHandlerPlugin, CORSHandlerPlugin, CORSHandlerPlugin as CORSPlugin, GetMethodCsrfProtectionHandlerPlugin, MethodOverrideHandlerPlugin, PrototypePollutionProtectionHandlerPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestHeadersHandlerPlugin as RequestHeadersPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, ResponseHeadersHandlerPlugin as ResponseHeadersPlugin, RethrowHandlerPlugin, TimeoutHandlerPlugin, negotiateResponseCompressionEncoding, toCompressedHeaders };
406
+ export { BATCH_CONTENT_TYPE, BatchHandlerPlugin, CORSHandlerPlugin, CORSHandlerPlugin as CORSPlugin, GetMethodCsrfProtectionHandlerPlugin, MethodOverrideHandlerPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestHeadersHandlerPlugin as RequestHeadersPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, ResponseHeadersHandlerPlugin as ResponseHeadersPlugin, RethrowHandlerPlugin, TimeoutHandlerPlugin, negotiateResponseCompressionEncoding, toCompressedHeaders };
423
407
  export type { BatchHandlerPluginOptions, CORSHandlerPluginOptions, MethodOverrideHandlerPluginOptions, RequestHeadersHandlerPluginContext, RequestHeadersHandlerPluginContext as RequestHeadersPluginContext, ResponseCompressionHandlerPluginOptions, ResponseHeadersHandlerPluginContext, ResponseHeadersHandlerPluginContext as ResponseHeadersPluginContext, RethrowHandlerPluginOptions, TimeoutHandlerPluginOptions };
@@ -8,7 +8,7 @@ import '@orpc/contract';
8
8
 
9
9
  /**
10
10
  * Content type for batch responses that use the length-prefixed binary framing
11
- * (streaming mode, and buffered mode when any sub-response contains binary).
11
+ * (streaming mode, and buffered mode when any sub-response contains binary or is a stream).
12
12
  *
13
13
  * Decoding is driven by the `standard-server` body hint, not this header,
14
14
  * so it only serves to describe the payload to logs, proxies, and dev tools.
@@ -45,7 +45,7 @@ interface BatchHandlerPluginOptions<T extends Context> {
45
45
  *
46
46
  * When enabled, a zero-length length-prefixed frame is sent periodically while the
47
47
  * stream is idle (no message sent for `interval` ms). Clients ignore these frames.
48
- * Only applies to streaming mode.
48
+ * Only applies to streaming responses: streaming mode, or a buffered batch with a stream sub-response.
49
49
  *
50
50
  * @default { enabled: true, interval: 15000 }
51
51
  */
@@ -69,6 +69,9 @@ interface BatchHandlerPluginOptions<T extends Context> {
69
69
  * batch into sub-requests and streaming their responses back together.
70
70
  *
71
71
  * @remarks
72
+ * In buffered mode, the batch still streams once a sub-response is a stream
73
+ * (an event iterator or a `ReadableStream`), since it may never end.
74
+ *
72
75
  * **Note**: HTTP/2 and later already multiplex requests over a single connection, which often makes this plugin unnecessary.
73
76
  *
74
77
  * @see {@link https://orpc.dev/docs/plugins/batch | Batch Plugin}
@@ -95,6 +98,9 @@ interface CORSHandlerPluginOptions<T extends Context> {
95
98
  * Configures the `Access-Control-Allow-Origin` header.
96
99
  * Can be a string, an array of allowed origins, or a function (optionally async) that returns the allowed origin(s).
97
100
  *
101
+ * A function must only return origins it has validated: returning the incoming origin
102
+ * unchecked allows every origin.
103
+ *
98
104
  * @default '*'
99
105
  */
100
106
  origin?: Value<Promisable<string | readonly string[] | null | undefined>, [origin: string, options: StandardHandlerRoutingInterceptorOptions<T>]>;
@@ -122,6 +128,9 @@ interface CORSHandlerPluginOptions<T extends Context> {
122
128
  maxAge?: number;
123
129
  /**
124
130
  * Configures the `Access-Control-Allow-Credentials` header.
131
+ *
132
+ * When enabled, `origin` must allow trusted origins only: every allowed origin can send
133
+ * authenticated requests as your users and read the responses.
125
134
  */
126
135
  credentials?: boolean;
127
136
  /**
@@ -207,31 +216,6 @@ declare class MethodOverrideHandlerPlugin<T extends Context> implements Standard
207
216
  init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
208
217
  }
209
218
 
210
- /**
211
- * Rejects requests whose decoded input contains prototype-polluting keys: an own
212
- * `__proto__` key, or an own `constructor` key holding a `prototype` key. oRPC's own
213
- * decoding never assigns through the prototype chain, so this plugin exists to stop such
214
- * keys from reaching application code that merges, clones, or path-sets input with a
215
- * library vulnerable to prototype pollution. An `AsyncIteratorObject` input is checked
216
- * value by value as it arrives, and a polluting value fails that iteration instead.
217
- *
218
- * @see {@link https://orpc.dev/docs/plugins/prototype-pollution-protection | Prototype Pollution Protection Plugin}
219
- */
220
- declare class PrototypePollutionProtectionHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
221
- name: string;
222
- init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
223
- private rejectPollutingInput;
224
- /**
225
- * Walks the containers the built-in codecs can produce: arrays, maps, sets, and plain
226
- * objects, including null-prototype ones. Arrays get the same key checks as plain objects,
227
- * since structured clone, as used over MessagePort, keeps named keys like `__proto__` on
228
- * them. Other objects, such as files and dates, carry no attacker-authored keys and are
229
- * left alone. The walk is iterative, so input nested deeper than the call stack allows
230
- * still gets the intended verdict instead of a `RangeError`.
231
- */
232
- private containsPollutingKey;
233
- }
234
-
235
219
  /**
236
220
  * Decompresses incoming request bodies based on the Content-Encoding header,
237
221
  * supporting gzip, deflate, and deflate-raw.
@@ -419,5 +403,5 @@ declare class TimeoutHandlerPlugin<T extends Context> implements StandardHandler
419
403
  init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
420
404
  }
421
405
 
422
- export { BATCH_CONTENT_TYPE, BatchHandlerPlugin, CORSHandlerPlugin, CORSHandlerPlugin as CORSPlugin, GetMethodCsrfProtectionHandlerPlugin, MethodOverrideHandlerPlugin, PrototypePollutionProtectionHandlerPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestHeadersHandlerPlugin as RequestHeadersPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, ResponseHeadersHandlerPlugin as ResponseHeadersPlugin, RethrowHandlerPlugin, TimeoutHandlerPlugin, negotiateResponseCompressionEncoding, toCompressedHeaders };
406
+ export { BATCH_CONTENT_TYPE, BatchHandlerPlugin, CORSHandlerPlugin, CORSHandlerPlugin as CORSPlugin, GetMethodCsrfProtectionHandlerPlugin, MethodOverrideHandlerPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestHeadersHandlerPlugin as RequestHeadersPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, ResponseHeadersHandlerPlugin as ResponseHeadersPlugin, RethrowHandlerPlugin, TimeoutHandlerPlugin, negotiateResponseCompressionEncoding, toCompressedHeaders };
423
407
  export type { BatchHandlerPluginOptions, CORSHandlerPluginOptions, MethodOverrideHandlerPluginOptions, RequestHeadersHandlerPluginContext, RequestHeadersHandlerPluginContext as RequestHeadersPluginContext, ResponseCompressionHandlerPluginOptions, ResponseHeadersHandlerPluginContext, ResponseHeadersHandlerPluginContext as ResponseHeadersPluginContext, RethrowHandlerPluginOptions, TimeoutHandlerPluginOptions };
@@ -1,11 +1,22 @@
1
1
  import { ORPCError } from '@orpc/client';
2
- import { toArray, value, isPlainObject, isTypescriptObject, isAsyncIteratorObject, override, wrapAsyncIterator, isCompressibleContentType, isNoTransformCacheControl, parseAcceptEncodingQualities, isAcceptableEncoding, varyByAcceptEncoding, stringifyJSON, anyAbortSignal, wrapReadableStream, AbortError } from '@orpc/shared';
2
+ import { toArray, value, promiseWithResolvers, isAsyncIteratorObject, isCompressibleContentType, isNoTransformCacheControl, parseAcceptEncodingQualities, isAcceptableEncoding, varyByAcceptEncoding, stringifyJSON, anyAbortSignal, override, wrapAsyncIterator, wrapReadableStream, AbortError } from '@orpc/shared';
3
3
  import { flattenStandardHeader, parseStandardUrl, generateContentDisposition, mergeStandardHeaders } from '@standard-server/core';
4
4
  import { isClientPeerSendMessage, ServerPeer, encodePeerMessage } from '@standard-server/peer';
5
5
  import { toFetchHeaders, toStandardBody, toStandardHeaders } from '@standard-server/fetch';
6
6
  export { R as RequestLimitHandlerPlugin } from '../shared/server.1ExWFMze.mjs';
7
7
 
8
8
  const BATCH_CONTENT_TYPE = "application/vnd.orpc.batch";
9
+ const textEncoder = new TextEncoder();
10
+ function encodeLengthPrefix(length) {
11
+ const prefix = new Uint8Array(4);
12
+ new DataView(prefix.buffer).setUint32(0, length, false);
13
+ return prefix;
14
+ }
15
+ async function encodeFrame(message) {
16
+ const encoded = await encodePeerMessage(message);
17
+ const bytes = typeof encoded === "string" ? textEncoder.encode(encoded) : encoded;
18
+ return [encodeLengthPrefix(bytes.byteLength), bytes];
19
+ }
9
20
  class BatchHandlerPlugin {
10
21
  name = "~batch";
11
22
  /**
@@ -101,6 +112,63 @@ class BatchHandlerPlugin {
101
112
  response: { status: 413, headers: {}, body: "Batch request size exceeds the maximum allowed size" }
102
113
  };
103
114
  }
115
+ const status = await value(this.successStatus, interceptorOptions);
116
+ const headers = await value(this.headers, interceptorOptions);
117
+ const bufferedMessages = [];
118
+ let streamController;
119
+ let keepAliveTimer;
120
+ const clearKeepAlive = () => {
121
+ if (keepAliveTimer !== void 0) {
122
+ clearInterval(keepAliveTimer);
123
+ keepAliveTimer = void 0;
124
+ }
125
+ };
126
+ const scheduleKeepAlive = () => {
127
+ if (!this.keepAliveEnabled) {
128
+ return;
129
+ }
130
+ clearKeepAlive();
131
+ keepAliveTimer = setInterval(() => {
132
+ try {
133
+ streamController.enqueue(encodeLengthPrefix(0));
134
+ } catch {
135
+ clearKeepAlive();
136
+ }
137
+ }, this.keepAliveInterval);
138
+ };
139
+ const enqueueMessage = async (message) => {
140
+ for (const chunk of await encodeFrame(message)) {
141
+ streamController.enqueue(chunk);
142
+ }
143
+ scheduleKeepAlive();
144
+ };
145
+ const peer = new ServerPeer(async (message) => {
146
+ if (mode === "streaming") {
147
+ startStreaming();
148
+ }
149
+ if (streamController === void 0) {
150
+ bufferedMessages.push(message);
151
+ } else {
152
+ await enqueueMessage(message);
153
+ }
154
+ });
155
+ const streaming = promiseWithResolvers();
156
+ function startStreaming() {
157
+ if (streamController !== void 0) {
158
+ return;
159
+ }
160
+ const body = new ReadableStream({
161
+ start(controller) {
162
+ streamController = controller;
163
+ scheduleKeepAlive();
164
+ },
165
+ async cancel(reason) {
166
+ clearKeepAlive();
167
+ await peer.close(reason);
168
+ }
169
+ });
170
+ streaming.resolve({ body, flushed: Promise.all(bufferedMessages.splice(0).map(enqueueMessage)) });
171
+ }
104
172
  const handleIndividualRequest = async (request) => {
105
173
  try {
106
174
  request = this.mapSubrequest(request, interceptorOptions);
@@ -108,16 +176,19 @@ class BatchHandlerPlugin {
108
176
  if (!matched) {
109
177
  return { status: 404, headers: {}, body: "No procedure matched" };
110
178
  }
179
+ if (response.body instanceof ReadableStream || isAsyncIteratorObject(response.body)) {
180
+ startStreaming();
181
+ }
111
182
  return response;
112
183
  } catch (err) {
113
184
  Promise.reject(err);
114
185
  return { status: 500, headers: {}, body: "Internal server error" };
115
186
  }
116
187
  };
117
- const runSubrequests = async (peer2) => {
118
- const promise = Promise.all(messages.map((msg) => peer2.message(msg, handleIndividualRequest)));
188
+ const runSubrequests = async () => {
189
+ const promise = Promise.all(messages.map((msg) => peer.message(msg, handleIndividualRequest)));
119
190
  const signal = interceptorOptions.request.signal;
120
- const closePeer = () => peer2.close(signal?.reason);
191
+ const closePeer = () => peer.close(signal?.reason);
121
192
  if (signal?.aborted) {
122
193
  closePeer();
123
194
  }
@@ -128,25 +199,12 @@ class BatchHandlerPlugin {
128
199
  signal?.removeEventListener("abort", closePeer);
129
200
  }
130
201
  };
131
- const status = await value(this.successStatus, interceptorOptions);
132
- const headers = await value(this.headers, interceptorOptions);
133
- if (mode === "buffered") {
134
- const responseMessages = [];
135
- const peer2 = new ServerPeer(async (message) => {
136
- responseMessages.push(message);
137
- });
138
- await runSubrequests(peer2);
139
- await peer2.close();
140
- if (responseMessages.some((msg) => msg.binary !== void 0)) {
141
- const chunks = [];
142
- for (const message of responseMessages) {
143
- const encoded = await encodePeerMessage(message);
144
- const bytes = typeof encoded === "string" ? new TextEncoder().encode(encoded) : encoded;
145
- const lengthBuffer = new ArrayBuffer(4);
146
- new DataView(lengthBuffer).setUint32(0, bytes.byteLength, false);
147
- chunks.push(new Uint8Array(lengthBuffer));
148
- chunks.push(bytes);
149
- }
202
+ const subrequests = runSubrequests();
203
+ const started = await Promise.race([streaming.promise, subrequests.then(() => void 0)]);
204
+ if (started === void 0) {
205
+ await peer.close();
206
+ if (bufferedMessages.some((msg) => msg.binary !== void 0)) {
207
+ const chunks = (await Promise.all(bufferedMessages.map(encodeFrame))).flat();
150
208
  return {
151
209
  matched: true,
152
210
  response: {
@@ -158,52 +216,10 @@ class BatchHandlerPlugin {
158
216
  }
159
217
  return {
160
218
  matched: true,
161
- response: { status, headers, body: responseMessages }
219
+ response: { status, headers, body: bufferedMessages }
162
220
  };
163
221
  }
164
- let streamController;
165
- let keepAliveTimer;
166
- const clearKeepAlive = () => {
167
- if (keepAliveTimer !== void 0) {
168
- clearInterval(keepAliveTimer);
169
- keepAliveTimer = void 0;
170
- }
171
- };
172
- const scheduleKeepAlive = () => {
173
- if (!this.keepAliveEnabled) {
174
- return;
175
- }
176
- clearKeepAlive();
177
- keepAliveTimer = setInterval(() => {
178
- try {
179
- const lengthBuffer = new ArrayBuffer(4);
180
- new DataView(lengthBuffer).setUint32(0, 0, false);
181
- streamController.enqueue(new Uint8Array(lengthBuffer));
182
- } catch {
183
- clearKeepAlive();
184
- }
185
- }, this.keepAliveInterval);
186
- };
187
- const peer = new ServerPeer(async (message) => {
188
- const encoded = await encodePeerMessage(message);
189
- const bytes = typeof encoded === "string" ? new TextEncoder().encode(encoded) : encoded;
190
- const lengthBuffer = new ArrayBuffer(4);
191
- new DataView(lengthBuffer).setUint32(0, bytes.byteLength, false);
192
- streamController.enqueue(new Uint8Array(lengthBuffer));
193
- streamController.enqueue(bytes);
194
- scheduleKeepAlive();
195
- });
196
- const stream = new ReadableStream({
197
- start(controller) {
198
- streamController = controller;
199
- scheduleKeepAlive();
200
- },
201
- async cancel(reason) {
202
- clearKeepAlive();
203
- await peer.close(reason);
204
- }
205
- });
206
- runSubrequests(peer).then(async () => {
222
+ Promise.all([started.flushed, subrequests]).then(async () => {
207
223
  clearKeepAlive();
208
224
  streamController.close();
209
225
  await peer.close();
@@ -217,7 +233,7 @@ class BatchHandlerPlugin {
217
233
  response: {
218
234
  status,
219
235
  headers: { ...headers, "content-type": BATCH_CONTENT_TYPE },
220
- body: stream
236
+ body: started.body
221
237
  }
222
238
  };
223
239
  };
@@ -405,75 +421,6 @@ class MethodOverrideHandlerPlugin {
405
421
  }
406
422
  }
407
423
 
408
- class PrototypePollutionProtectionHandlerPlugin {
409
- name = "~prototype-pollution-protection";
410
- init(options) {
411
- const interceptor = (interceptorOptions) => {
412
- const input = interceptorOptions.input;
413
- if (isAsyncIteratorObject(input)) {
414
- const guardedInput = override(input, wrapAsyncIterator(input, {
415
- mapResult: (result) => {
416
- this.rejectPollutingInput(result.value);
417
- return result;
418
- }
419
- }));
420
- return interceptorOptions.next({ ...interceptorOptions, input: guardedInput });
421
- }
422
- this.rejectPollutingInput(input);
423
- return interceptorOptions.next();
424
- };
425
- return {
426
- ...options,
427
- clientInterceptors: [interceptor, ...toArray(options.clientInterceptors)]
428
- };
429
- }
430
- rejectPollutingInput(input) {
431
- if (this.containsPollutingKey(input)) {
432
- throw new ORPCError("BAD_REQUEST", { message: "Request blocked by prototype pollution protection." });
433
- }
434
- }
435
- /**
436
- * Walks the containers the built-in codecs can produce: arrays, maps, sets, and plain
437
- * objects, including null-prototype ones. Arrays get the same key checks as plain objects,
438
- * since structured clone, as used over MessagePort, keeps named keys like `__proto__` on
439
- * them. Other objects, such as files and dates, carry no attacker-authored keys and are
440
- * left alone. The walk is iterative, so input nested deeper than the call stack allows
441
- * still gets the intended verdict instead of a `RangeError`.
442
- */
443
- containsPollutingKey(root) {
444
- const visited = /* @__PURE__ */ new WeakSet();
445
- const stack = [root];
446
- while (stack.length !== 0) {
447
- const value = stack.pop();
448
- if (typeof value !== "object" || value === null || visited.has(value)) {
449
- continue;
450
- }
451
- visited.add(value);
452
- if (value instanceof Map || value instanceof Set) {
453
- for (const entry of value) {
454
- stack.push(entry);
455
- }
456
- continue;
457
- }
458
- if (!Array.isArray(value) && !isPlainObject(value)) {
459
- continue;
460
- }
461
- if (Object.hasOwn(value, "__proto__")) {
462
- return true;
463
- }
464
- if (Object.hasOwn(value, "constructor") && isTypescriptObject(value.constructor) && Object.hasOwn(value.constructor, "prototype")) {
465
- return true;
466
- }
467
- for (const item of Object.values(value)) {
468
- if (typeof item === "object" && item !== null) {
469
- stack.push(item);
470
- }
471
- }
472
- }
473
- return false;
474
- }
475
- }
476
-
477
424
  const MAX_CONTENT_ENCODINGS = 5;
478
425
  class RequestCompressionHandlerPlugin {
479
426
  name = "~request-compression";
@@ -608,7 +555,8 @@ class ResponseCompressionHandlerPlugin {
608
555
  };
609
556
  }
610
557
  } else if (body instanceof Blob) {
611
- if ((!Number.isFinite(body.size) || body.size >= this.threshold) && this.isCompressibleContentType(body.type, interceptorOptions)) {
558
+ const contentType = headers["content-type"] ?? body.type;
559
+ if ((!Number.isFinite(body.size) || body.size >= this.threshold) && this.isCompressibleContentType(flattenStandardHeader(contentType), interceptorOptions)) {
612
560
  const contentDisposition = headers["content-disposition"] ?? generateContentDisposition(
613
561
  body instanceof File ? body.name : "blob"
614
562
  );
@@ -620,7 +568,7 @@ class ResponseCompressionHandlerPlugin {
620
568
  headers: {
621
569
  ...toCompressedHeaders(headers, encoding),
622
570
  "standard-server": "file",
623
- "content-type": body.type,
571
+ "content-type": contentType,
624
572
  "content-disposition": contentDisposition
625
573
  }
626
574
  }
@@ -875,4 +823,4 @@ class TimeoutHandlerPlugin {
875
823
  }
876
824
  }
877
825
 
878
- export { BATCH_CONTENT_TYPE, BatchHandlerPlugin, CORSHandlerPlugin, CORSHandlerPlugin as CORSPlugin, GetMethodCsrfProtectionHandlerPlugin, MethodOverrideHandlerPlugin, PrototypePollutionProtectionHandlerPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestHeadersHandlerPlugin as RequestHeadersPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, ResponseHeadersHandlerPlugin as ResponseHeadersPlugin, RethrowHandlerPlugin, TimeoutHandlerPlugin, negotiateResponseCompressionEncoding, toCompressedHeaders };
826
+ export { BATCH_CONTENT_TYPE, BatchHandlerPlugin, CORSHandlerPlugin, CORSHandlerPlugin as CORSPlugin, GetMethodCsrfProtectionHandlerPlugin, MethodOverrideHandlerPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestHeadersHandlerPlugin as RequestHeadersPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, ResponseHeadersHandlerPlugin as ResponseHeadersPlugin, RethrowHandlerPlugin, TimeoutHandlerPlugin, negotiateResponseCompressionEncoding, toCompressedHeaders };
@@ -191,6 +191,8 @@ class RPCMatcher {
191
191
  rootRouter;
192
192
  tree = /* @__PURE__ */ new Map();
193
193
  pendingLazyRouters = /* @__PURE__ */ new Map();
194
+ /** segment count of the deepest pending key, never lowered so it stays an upper bound */
195
+ maxPendingLazyRouterDepth = 0;
194
196
  constructor(router, options = {}) {
195
197
  this.filter = options.filter ?? true;
196
198
  const allowMethods = options.allowMethods ?? RPC_DEFAULT_ALLOW_METHODS;
@@ -216,6 +218,7 @@ class RPCMatcher {
216
218
  }, path);
217
219
  for (const result of lazyResults) {
218
220
  this.pendingLazyRouters.set(pathToHttpPath(result.path), result);
221
+ this.maxPendingLazyRouterDepth = Math.max(this.maxPendingLazyRouterDepth, result.path.length);
219
222
  }
220
223
  }
221
224
  async match(method, pathname, prefix) {
@@ -270,22 +273,12 @@ class RPCMatcher {
270
273
  return;
271
274
  }
272
275
  let slashIndex = 0;
273
- while (slashIndex !== -1) {
274
- const nextSlashIndex = pathname.indexOf("/", slashIndex + 1);
275
- const httpPathPrefix = nextSlashIndex === -1 ? pathname : pathname.slice(0, nextSlashIndex);
276
- if (this.pendingLazyRouters.has(httpPathPrefix)) {
277
- return this.loadPendingLazyRouters(pathname, slashIndex);
278
- }
279
- slashIndex = nextSlashIndex;
280
- }
281
- }
282
- async loadPendingLazyRouters(pathname, slashIndex) {
283
- while (slashIndex !== -1) {
276
+ for (let depth = 1; slashIndex !== -1 && depth <= this.maxPendingLazyRouterDepth; depth++) {
284
277
  const nextSlashIndex = pathname.indexOf("/", slashIndex + 1);
285
278
  const httpPathPrefix = nextSlashIndex === -1 ? pathname : pathname.slice(0, nextSlashIndex);
286
279
  const pending = this.pendingLazyRouters.get(httpPathPrefix);
287
280
  if (pending !== void 0) {
288
- await this.loadPendingLazyRouter(httpPathPrefix, pending);
281
+ return this.loadPendingLazyRouter(httpPathPrefix, pending).then(() => this.resolvePendingLazyRouters(pathname));
289
282
  }
290
283
  slashIndex = nextSlashIndex;
291
284
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@orpc/server",
3
3
  "type": "module",
4
- "version": "2.0.0-beta.43",
4
+ "version": "2.0.0-beta.44",
5
5
  "description": "Build typesafe APIs with oRPC: RPC and OpenAPI support, middleware, and adapters for Node.js, Bun, Deno, Fetch, Fastify, WebSocket, AWS Lambda, and more",
6
6
  "license": "MIT",
7
7
  "funding": [
@@ -117,23 +117,23 @@
117
117
  }
118
118
  },
119
119
  "dependencies": {
120
- "@orpc/client": "2.0.0-beta.43",
121
- "@orpc/contract": "2.0.0-beta.43",
122
- "@orpc/shared": "2.0.0-beta.43",
123
- "@standard-server/aws-lambda": "~0.10.2",
124
- "@standard-server/core": "~0.10.2",
125
- "@standard-server/fastify": "~0.10.2",
126
- "@standard-server/fetch": "~0.10.2",
127
- "@standard-server/node": "~0.10.2",
128
- "@standard-server/peer": "~0.10.2",
120
+ "@orpc/client": "2.0.0-beta.44",
121
+ "@orpc/contract": "2.0.0-beta.44",
122
+ "@orpc/shared": "2.0.0-beta.44",
123
+ "@standard-server/aws-lambda": "~0.10.3",
124
+ "@standard-server/core": "~0.10.3",
125
+ "@standard-server/fastify": "~0.10.3",
126
+ "@standard-server/fetch": "~0.10.3",
127
+ "@standard-server/node": "~0.10.3",
128
+ "@standard-server/peer": "~0.10.3",
129
129
  "cookie": "^2.0.1"
130
130
  },
131
131
  "devDependencies": {
132
- "crossws": "^0.4.6",
133
- "fastify": "^5.12.1",
134
- "supertest": "^7.2.2",
135
- "ws": "^8.21.3",
136
- "zod": "^4.5.4"
132
+ "crossws": "^0.4.15",
133
+ "fastify": "^5.12.5",
134
+ "supertest": "^7.3.1",
135
+ "ws": "^8.22.0",
136
+ "zod": "^4.6.5"
137
137
  },
138
138
  "scripts": {
139
139
  "type:check": "tsc -b"