@orpc/server 2.0.0-beta.42 → 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.BFEVk-EI.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.BFEVk-EI.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.BFEVk-EI.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.BFEVk-EI.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.BFEVk-EI.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.BFEVk-EI.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.BFEVk-EI.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.BFEVk-EI.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>]>;
@@ -112,6 +118,8 @@ interface CORSHandlerPluginOptions<T extends Context> {
112
118
  /**
113
119
  * Configures the `Access-Control-Allow-Headers` header for preflight requests.
114
120
  * Falls back to the request's `Access-Control-Request-Headers` if not set.
121
+ * When set, it replaces that fallback, so include every non-safelisted header
122
+ * clients send, such as `Content-Type`.
115
123
  */
116
124
  allowHeaders?: readonly string[];
117
125
  /**
@@ -120,6 +128,9 @@ interface CORSHandlerPluginOptions<T extends Context> {
120
128
  maxAge?: number;
121
129
  /**
122
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.
123
134
  */
124
135
  credentials?: boolean;
125
136
  /**
@@ -205,31 +216,6 @@ declare class MethodOverrideHandlerPlugin<T extends Context> implements Standard
205
216
  init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
206
217
  }
207
218
 
208
- /**
209
- * Rejects requests whose decoded input contains prototype-polluting keys: an own
210
- * `__proto__` key, or an own `constructor` key holding a `prototype` key. oRPC's own
211
- * decoding never assigns through the prototype chain, so this plugin exists to stop such
212
- * keys from reaching application code that merges, clones, or path-sets input with a
213
- * library vulnerable to prototype pollution. An `AsyncIteratorObject` input is checked
214
- * value by value as it arrives, and a polluting value fails that iteration instead.
215
- *
216
- * @see {@link https://orpc.dev/docs/plugins/prototype-pollution-protection | Prototype Pollution Protection Plugin}
217
- */
218
- declare class PrototypePollutionProtectionHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
219
- name: string;
220
- init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
221
- private rejectPollutingInput;
222
- /**
223
- * Walks the containers the built-in codecs can produce: arrays, maps, sets, and plain
224
- * objects, including null-prototype ones. Arrays get the same key checks as plain objects,
225
- * since structured clone, as used over MessagePort, keeps named keys like `__proto__` on
226
- * them. Other objects, such as files and dates, carry no attacker-authored keys and are
227
- * left alone. The walk is iterative, so input nested deeper than the call stack allows
228
- * still gets the intended verdict instead of a `RangeError`.
229
- */
230
- private containsPollutingKey;
231
- }
232
-
233
219
  /**
234
220
  * Decompresses incoming request bodies based on the Content-Encoding header,
235
221
  * supporting gzip, deflate, and deflate-raw.
@@ -417,5 +403,5 @@ declare class TimeoutHandlerPlugin<T extends Context> implements StandardHandler
417
403
  init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
418
404
  }
419
405
 
420
- 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 };
421
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>]>;
@@ -112,6 +118,8 @@ interface CORSHandlerPluginOptions<T extends Context> {
112
118
  /**
113
119
  * Configures the `Access-Control-Allow-Headers` header for preflight requests.
114
120
  * Falls back to the request's `Access-Control-Request-Headers` if not set.
121
+ * When set, it replaces that fallback, so include every non-safelisted header
122
+ * clients send, such as `Content-Type`.
115
123
  */
116
124
  allowHeaders?: readonly string[];
117
125
  /**
@@ -120,6 +128,9 @@ interface CORSHandlerPluginOptions<T extends Context> {
120
128
  maxAge?: number;
121
129
  /**
122
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.
123
134
  */
124
135
  credentials?: boolean;
125
136
  /**
@@ -205,31 +216,6 @@ declare class MethodOverrideHandlerPlugin<T extends Context> implements Standard
205
216
  init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
206
217
  }
207
218
 
208
- /**
209
- * Rejects requests whose decoded input contains prototype-polluting keys: an own
210
- * `__proto__` key, or an own `constructor` key holding a `prototype` key. oRPC's own
211
- * decoding never assigns through the prototype chain, so this plugin exists to stop such
212
- * keys from reaching application code that merges, clones, or path-sets input with a
213
- * library vulnerable to prototype pollution. An `AsyncIteratorObject` input is checked
214
- * value by value as it arrives, and a polluting value fails that iteration instead.
215
- *
216
- * @see {@link https://orpc.dev/docs/plugins/prototype-pollution-protection | Prototype Pollution Protection Plugin}
217
- */
218
- declare class PrototypePollutionProtectionHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
219
- name: string;
220
- init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
221
- private rejectPollutingInput;
222
- /**
223
- * Walks the containers the built-in codecs can produce: arrays, maps, sets, and plain
224
- * objects, including null-prototype ones. Arrays get the same key checks as plain objects,
225
- * since structured clone, as used over MessagePort, keeps named keys like `__proto__` on
226
- * them. Other objects, such as files and dates, carry no attacker-authored keys and are
227
- * left alone. The walk is iterative, so input nested deeper than the call stack allows
228
- * still gets the intended verdict instead of a `RangeError`.
229
- */
230
- private containsPollutingKey;
231
- }
232
-
233
219
  /**
234
220
  * Decompresses incoming request bodies based on the Content-Encoding header,
235
221
  * supporting gzip, deflate, and deflate-raw.
@@ -417,5 +403,5 @@ declare class TimeoutHandlerPlugin<T extends Context> implements StandardHandler
417
403
  init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
418
404
  }
419
405
 
420
- 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 };
421
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 };
@@ -183,6 +183,7 @@ class TracingHandlerPlugin {
183
183
  }
184
184
 
185
185
  const RPC_DEFAULT_ALLOW_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
186
+ const NORMALIZABLE_PATHNAME_REGEX = /[^\w\-.!~*'()/]/;
186
187
  class RPCMatcher {
187
188
  filter;
188
189
  allowMethodsSet;
@@ -190,6 +191,8 @@ class RPCMatcher {
190
191
  rootRouter;
191
192
  tree = /* @__PURE__ */ new Map();
192
193
  pendingLazyRouters = /* @__PURE__ */ new Map();
194
+ /** segment count of the deepest pending key, never lowered so it stays an upper bound */
195
+ maxPendingLazyRouterDepth = 0;
193
196
  constructor(router, options = {}) {
194
197
  this.filter = options.filter ?? true;
195
198
  const allowMethods = options.allowMethods ?? RPC_DEFAULT_ALLOW_METHODS;
@@ -215,6 +218,7 @@ class RPCMatcher {
215
218
  }, path);
216
219
  for (const result of lazyResults) {
217
220
  this.pendingLazyRouters.set(pathToHttpPath(result.path), result);
221
+ this.maxPendingLazyRouterDepth = Math.max(this.maxPendingLazyRouterDepth, result.path.length);
218
222
  }
219
223
  }
220
224
  async match(method, pathname, prefix) {
@@ -244,7 +248,7 @@ class RPCMatcher {
244
248
  await loading;
245
249
  }
246
250
  let entry = this.tree.get(pathname);
247
- if (entry === void 0 && pathname.includes("%")) {
251
+ if (entry === void 0 && NORMALIZABLE_PATHNAME_REGEX.test(pathname)) {
248
252
  const normalizedPathname = normalizeHttpPath(pathname);
249
253
  const normalizedLoading = this.resolvePendingLazyRouters(normalizedPathname);
250
254
  if (normalizedLoading !== void 0) {
@@ -269,22 +273,12 @@ class RPCMatcher {
269
273
  return;
270
274
  }
271
275
  let slashIndex = 0;
272
- while (slashIndex !== -1) {
273
- const nextSlashIndex = pathname.indexOf("/", slashIndex + 1);
274
- const httpPathPrefix = nextSlashIndex === -1 ? pathname : pathname.slice(0, nextSlashIndex);
275
- if (this.pendingLazyRouters.has(httpPathPrefix)) {
276
- return this.loadPendingLazyRouters(pathname, slashIndex);
277
- }
278
- slashIndex = nextSlashIndex;
279
- }
280
- }
281
- async loadPendingLazyRouters(pathname, slashIndex) {
282
- while (slashIndex !== -1) {
276
+ for (let depth = 1; slashIndex !== -1 && depth <= this.maxPendingLazyRouterDepth; depth++) {
283
277
  const nextSlashIndex = pathname.indexOf("/", slashIndex + 1);
284
278
  const httpPathPrefix = nextSlashIndex === -1 ? pathname : pathname.slice(0, nextSlashIndex);
285
279
  const pending = this.pendingLazyRouters.get(httpPathPrefix);
286
280
  if (pending !== void 0) {
287
- await this.loadPendingLazyRouter(httpPathPrefix, pending);
281
+ return this.loadPendingLazyRouter(httpPathPrefix, pending).then(() => this.resolvePendingLazyRouters(pathname));
288
282
  }
289
283
  slashIndex = nextSlashIndex;
290
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.42",
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.42",
121
- "@orpc/contract": "2.0.0-beta.42",
122
- "@orpc/shared": "2.0.0-beta.42",
123
- "@standard-server/aws-lambda": "~0.10.1",
124
- "@standard-server/core": "~0.10.1",
125
- "@standard-server/fastify": "~0.10.1",
126
- "@standard-server/fetch": "~0.10.1",
127
- "@standard-server/node": "~0.10.1",
128
- "@standard-server/peer": "~0.10.1",
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"