@orpc/node 2.0.0-beta.27 β†’ 2.0.0-beta.29

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
@@ -4,7 +4,7 @@
4
4
  <a href="https://codecov.io/gh/middleapi/orpc">
5
5
  <img alt="codecov" src="https://codecov.io/gh/middleapi/orpc/branch/main/graph/badge.svg">
6
6
  </a>
7
- <a href="https://www.npmjs.com/package/@orpc/node">
7
+ <a href="https://npmx.dev/package/@orpc/node">
8
8
  <img alt="weekly downloads" src="https://img.shields.io/npm/dw/%40orpc%2Fnode?logo=npm" />
9
9
  </a>
10
10
  <a href="https://app.codspeed.io/middleapi/orpc?utm_source=badge">
@@ -29,69 +29,62 @@ You can read the documentation [here](https://orpc.dev).
29
29
 
30
30
  **Core**
31
31
 
32
- - [@orpc/contract](https://www.npmjs.com/package/@orpc/contract): Define API contract as the single source of truth.
33
- - [@orpc/server](https://www.npmjs.com/package/@orpc/server): Build APIs or implement contracts.
34
- - [@orpc/client](https://www.npmjs.com/package/@orpc/client): Consume APIs with end-to-end type safety.
35
- - [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi): Add OpenAPI compatibility to APIs.
32
+ - [@orpc/contract](https://npmx.dev/package/@orpc/contract): Define API contract as the single source of truth.
33
+ - [@orpc/server](https://npmx.dev/package/@orpc/server): Build APIs or implement contracts.
34
+ - [@orpc/client](https://npmx.dev/package/@orpc/client): Consume APIs with end-to-end type safety.
35
+ - [@orpc/openapi](https://npmx.dev/package/@orpc/openapi): Add OpenAPI compatibility to APIs.
36
36
 
37
37
  **Schema validation**
38
38
 
39
- - [@orpc/zod](https://www.npmjs.com/package/@orpc/zod): Integrate with [Zod](https://zod.dev/).
40
- - [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot): Integrate with [Valibot](https://valibot.dev/).
41
- - [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype): Integrate with [ArkType](https://arktype.io/).
39
+ - [@orpc/zod](https://npmx.dev/package/@orpc/zod): Integrate with [Zod](https://zod.dev/).
40
+ - [@orpc/valibot](https://npmx.dev/package/@orpc/valibot): Integrate with [Valibot](https://valibot.dev/).
41
+ - [@orpc/arktype](https://npmx.dev/package/@orpc/arktype): Integrate with [ArkType](https://arktype.io/).
42
42
 
43
43
  **Built-in features**
44
44
 
45
- - [@orpc/publisher](https://www.npmjs.com/package/@orpc/publisher): Pub/Sub with memory, Redis, and Upstash adapters.
46
- - [@orpc/ratelimit](https://www.npmjs.com/package/@orpc/ratelimit): Rate limiting with memory, Redis, and Upstash adapters.
47
- - [@orpc/hibernation](https://www.npmjs.com/package/@orpc/hibernation): Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api).
48
- - [@orpc/json-schema](https://www.npmjs.com/package/@orpc/json-schema): Smart coercion for OpenAPI requests.
45
+ - [@orpc/publisher](https://npmx.dev/package/@orpc/publisher): Pub/Sub with memory, Redis, and Upstash adapters.
46
+ - [@orpc/ratelimit](https://npmx.dev/package/@orpc/ratelimit): Rate limiting with memory, Redis, and Upstash adapters.
47
+ - [@orpc/hibernation](https://npmx.dev/package/@orpc/hibernation): Leverage Hibernation APIs like [Cloudflare's Hibernation WebSocket](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api).
48
+ - [@orpc/json-schema](https://npmx.dev/package/@orpc/json-schema): Smart coercion for OpenAPI requests.
49
49
 
50
50
  **Framework & ecosystem integrations**
51
51
 
52
- - [@orpc/next](https://www.npmjs.com/package/@orpc/next): Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data).
53
- - [@orpc/ai-sdk](https://www.npmjs.com/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.
54
- - [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest).
55
- - [@orpc/pinia-colada](https://www.npmjs.com/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).
56
- - [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
57
- - [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
58
- - [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
59
- - [@orpc/node](https://www.npmjs.com/package/@orpc/node): [Node.js](https://nodejs.org/) plugins, helpers, like serving static files.
60
- - [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
61
- - [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
62
- - [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
52
+ - [@orpc/next](https://npmx.dev/package/@orpc/next): Integrate with [Next.js Server Functions](https://nextjs.org/docs/app/getting-started/mutating-data).
53
+ - [@orpc/ai-sdk](https://npmx.dev/package/@orpc/ai-sdk): Turn contracts and procedures into [AI SDK](https://ai-sdk.dev/) tools.
54
+ - [@orpc/tanstack-query](https://npmx.dev/package/@orpc/tanstack-query): Integrate with [TanStack Query](https://tanstack.com/query/latest).
55
+ - [@orpc/pinia-colada](https://npmx.dev/package/@orpc/pinia-colada): Integrate with [Pinia Colada](https://pinia-colada.esm.dev/).
56
+ - [@orpc/swr](https://npmx.dev/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
57
+ - [@orpc/experimental-effect](https://npmx.dev/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
58
+ - [@orpc/nest](https://npmx.dev/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
59
+ - [@orpc/node](https://npmx.dev/package/@orpc/node): [Node.js](https://nodejs.org/) plugins for static file serving and large uploads.
60
+ - [@orpc/bun](https://npmx.dev/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
61
+ - [@orpc/cloudflare](https://npmx.dev/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
62
+ - [@orpc/trpc](https://npmx.dev/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
63
63
 
64
64
  **Observability**
65
65
 
66
- - [@orpc/opentelemetry](https://www.npmjs.com/package/@orpc/opentelemetry): Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing.
67
- - [@orpc/pino](https://www.npmjs.com/package/@orpc/pino): Integrate with [Pino](https://getpino.io/) for logging.
68
- - [@orpc/evlog](https://www.npmjs.com/package/@orpc/evlog): Integrate with [Evlog](https://evlog.dev/) for logging.
66
+ - [@orpc/opentelemetry](https://npmx.dev/package/@orpc/opentelemetry): Integrate with [OpenTelemetry](https://opentelemetry.io/) for distributed tracing.
67
+ - [@orpc/pino](https://npmx.dev/package/@orpc/pino): Integrate with [Pino](https://getpino.io/) for logging.
68
+ - [@orpc/evlog](https://npmx.dev/package/@orpc/evlog): Integrate with [Evlog](https://evlog.dev/) for logging.
69
69
 
70
70
  ## Sponsors
71
71
 
72
72
  Like what we build over at [middleapi](https://github.com/middleapi)? You can help keep it going here: [GitHub Sponsors](https://github.com/sponsors/dinwwwh). Every bit helps! πŸš€
73
73
 
74
- ### πŸ† Platinum Sponsor
75
-
76
74
  <table>
77
75
  <tr>
78
- <td align="center"><a href="https://screenshotone.com/?ref=orpc" target="_blank" rel="sponsored noopener" title="ScreenshotOne.com"><img src="https://avatars.githubusercontent.com/u/97035603?v=4" width="279" alt="ScreenshotOne.com"/><br />ScreenshotOne.com</a></td>
76
+ <td width="2000"><a href="https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener" title="The screenshot API for developers"><img src="https://avatars.githubusercontent.com/u/97035603?v=4" width="64" align="left" hspace="12" alt="ScreenshotOne.com"/><b>ScreenshotOne.com</b></a><br /><sub>The screenshot API for developers</sub></td>
79
77
  </tr>
80
- </table>
81
-
82
- ### πŸ₯ˆ Silver Sponsor
83
-
84
- <table>
85
78
  <tr>
86
- <td align="center"><a href="https://misskey.io/?ref=orpc" target="_blank" rel="sponsored noopener" title="ζ‘δΈŠγ•γ‚“"><img src="https://avatars.githubusercontent.com/u/37681609?u=0dd4c7e4ba937cbb52b068c55914b1d8164dc0c7&amp;v=4" width="209" alt="ζ‘δΈŠγ•γ‚“"/><br />ζ‘δΈŠγ•γ‚“</a></td>
79
+ <td width="2000"><a href="https://misskey.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Decentralized microblogging SNS born on Earth"><img src="https://github.com/MisskeyIO.png" width="64" align="left" hspace="12" alt="MisskeyHQ"/><b>MisskeyHQ</b></a><br /><sub>Decentralized microblogging SNS born on Earth</sub></td>
87
80
  </tr>
88
81
  </table>
89
82
 
90
- ### Generous Sponsors
83
+ ### Organization Sponsors
91
84
 
92
85
  <table>
93
86
  <tr>
94
- <td align="center"><a href="https://github.com/ln-markets?ref=orpc" target="_blank" rel="sponsored noopener" title="LN Markets"><img src="https://avatars.githubusercontent.com/u/70597625?v=4" width="167" alt="LN Markets"/><br />LN Markets</a></td>
87
+ <td align="center"><a href="https://lnmarkets.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="LN Markets"><img src="https://avatars.githubusercontent.com/u/70597625?v=4" width="167" alt="LN Markets"/><br />LN Markets</a></td>
95
88
  </tr>
96
89
  </table>
97
90
 
@@ -99,25 +92,26 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
99
92
 
100
93
  <table>
101
94
  <tr>
102
- <td align="center"><a href="https://github.com/hrmcdonald?ref=orpc" target="_blank" rel="sponsored noopener" title="Reece McDonald"><img src="https://avatars.githubusercontent.com/u/39349270?v=4" width="139" alt="Reece McDonald"/><br />Reece McDonald</a></td>
103
- <td align="center"><a href="https://github.com/u1-liquid?ref=orpc" target="_blank" rel="sponsored noopener" title="あわわわとーにゅ"><img src="https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4" width="139" alt="あわわわとーにゅ"/><br />あわわわとーにゅ</a></td>
104
- <td align="center"><a href="https://github.com/nicognaW?ref=orpc" target="_blank" rel="sponsored noopener" title="nk"><img src="https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4" width="139" alt="nk"/><br />nk</a></td>
105
- <td align="center"><a href="https://github.com/supastarter?ref=orpc" target="_blank" rel="sponsored noopener" title="supastarter"><img src="https://avatars.githubusercontent.com/u/110960143?v=4" width="139" alt="supastarter"/><br />supastarter</a></td>
106
- <td align="center"><a href="https://github.com/divmgl?ref=orpc" target="_blank" rel="sponsored noopener" title="Dexter Miguel"><img src="https://avatars.githubusercontent.com/u/5452298?u=645993204be8696c085ecf0d228c3062efe2ed65&amp;v=4" width="139" alt="Dexter Miguel"/><br />Dexter Miguel</a></td>
107
- <td align="center"><a href="https://github.com/herrfugbaum?ref=orpc" target="_blank" rel="sponsored noopener" title="herrfugbaum"><img src="https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4" width="139" alt="herrfugbaum"/><br />herrfugbaum</a></td>
95
+ <td align="center"><a href="https://github.com/hrmcdonald?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Reece McDonald"><img src="https://avatars.githubusercontent.com/u/39349270?v=4" width="139" alt="Reece McDonald"/><br />Reece McDonald</a></td>
96
+ <td align="center"><a href="https://soymilk.party/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="あわわわとーにゅ"><img src="https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&amp;v=4" width="139" alt="あわわわとーにゅ"/><br />あわわわとーにゅ</a></td>
97
+ <td align="center"><a href="https://github.com/nicognaW?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="nk"><img src="https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&amp;v=4" width="139" alt="nk"/><br />nk</a></td>
98
+ <td align="center"><a href="https://supastarter.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="supastarter"><img src="https://avatars.githubusercontent.com/u/110960143?v=4" width="139" alt="supastarter"/><br />supastarter</a></td>
99
+ <td align="center"><a href="https://github.com/divmgl?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Dexter Miguel"><img src="https://avatars.githubusercontent.com/u/5452298?u=645993204be8696c085ecf0d228c3062efe2ed65&amp;v=4" width="139" alt="Dexter Miguel"/><br />Dexter Miguel</a></td>
100
+ <td align="center"><a href="https://github.com/herrfugbaum?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="herrfugbaum"><img src="https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&amp;v=4" width="139" alt="herrfugbaum"/><br />herrfugbaum</a></td>
108
101
  </tr>
109
102
  <tr>
110
- <td align="center"><a href="https://github.com/ryota-murakami?ref=orpc" target="_blank" rel="sponsored noopener" title="Ryota Murakami"><img src="https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4" width="139" alt="Ryota Murakami"/><br />Ryota Murakami</a></td>
111
- <td align="center"><a href="https://github.com/dcramer?ref=orpc" target="_blank" rel="sponsored noopener" title="David Cramer"><img src="https://avatars.githubusercontent.com/u/23610?v=4" width="139" alt="David Cramer"/><br />David Cramer</a></td>
112
- <td align="center"><a href="https://github.com/valerii15298?ref=orpc" target="_blank" rel="sponsored noopener" title="Valerii Petryniak"><img src="https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4" width="139" alt="Valerii Petryniak"/><br />Valerii Petryniak</a></td>
113
- <td align="center"><a href="https://github.com/letstri?ref=orpc" target="_blank" rel="sponsored noopener" title="Valerii Strilets"><img src="https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4" width="139" alt="Valerii Strilets"/><br />Valerii Strilets</a></td>
114
- <td align="center"><a href="https://github.com/K-Mistele?ref=orpc" target="_blank" rel="sponsored noopener" title="Kyle Mistele"><img src="https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4" width="139" alt="Kyle Mistele"/><br />Kyle Mistele</a></td>
115
- <td align="center"><a href="https://github.com/christ12938?ref=orpc" target="_blank" rel="sponsored noopener" title="christ12938"><img src="https://avatars.githubusercontent.com/u/25758598?v=4" width="139" alt="christ12938"/><br />christ12938</a></td>
103
+ <td align="center"><a href="https://laststance.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ryota Murakami"><img src="https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&amp;v=4" width="139" alt="Ryota Murakami"/><br />Ryota Murakami</a></td>
104
+ <td align="center"><a href="https://cra.mr/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="David Cramer"><img src="https://avatars.githubusercontent.com/u/23610?v=4" width="139" alt="David Cramer"/><br />David Cramer</a></td>
105
+ <td align="center"><a href="https://valerii15298.github.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Valerii Petryniak"><img src="https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&amp;v=4" width="139" alt="Valerii Petryniak"/><br />Valerii Petryniak</a></td>
106
+ <td align="center"><a href="https://letstri.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Valerii Strilets"><img src="https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&amp;v=4" width="139" alt="Valerii Strilets"/><br />Valerii Strilets</a></td>
107
+ <td align="center"><a href="https://blacklight.sh/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Kyle Mistele"><img src="https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&amp;v=4" width="139" alt="Kyle Mistele"/><br />Kyle Mistele</a></td>
108
+ <td align="center"><a href="https://github.com/christ12938?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="christ12938"><img src="https://avatars.githubusercontent.com/u/25758598?v=4" width="139" alt="christ12938"/><br />christ12938</a></td>
116
109
  </tr>
117
110
  <tr>
118
- <td align="center"><a href="https://github.com/Ryanjso?ref=orpc" target="_blank" rel="sponsored noopener" title="Ryan Soderberg"><img src="https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4" width="139" alt="Ryan Soderberg"/><br />Ryan Soderberg</a></td>
119
- <td align="center"><a href="https://github.com/itigoore01?ref=orpc" target="_blank" rel="sponsored noopener" title="shota"><img src="https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4" width="139" alt="shota"/><br />shota</a></td>
120
- <td align="center"><a href="https://github.com/ellis-driscoll?ref=orpc" target="_blank" rel="sponsored noopener" title="Ellis Driscoll"><img src="https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4" width="139" alt="Ellis Driscoll"/><br />Ellis Driscoll</a></td>
111
+ <td align="center"><a href="https://github.com/Ryanjso?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ryan Soderberg"><img src="https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&amp;v=4" width="139" alt="Ryan Soderberg"/><br />Ryan Soderberg</a></td>
112
+ <td align="center"><a href="https://github.com/itigoore01?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="shota"><img src="https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&amp;v=4" width="139" alt="shota"/><br />shota</a></td>
113
+ <td align="center"><a href="https://github.com/ellis-driscoll?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ellis Driscoll"><img src="https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&amp;v=4" width="139" alt="Ellis Driscoll"/><br />Ellis Driscoll</a></td>
114
+ <td align="center"><a href="https://github.com/hoangbn?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Hoang Nguyen"><img src="https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&amp;v=4" width="139" alt="Hoang Nguyen"/><br />Hoang Nguyen</a></td>
121
115
  </tr>
122
116
  </table>
123
117
 
@@ -125,31 +119,31 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
125
119
 
126
120
  <table>
127
121
  <tr>
128
- <td align="center"><a href="https://github.com/rhinodavid?ref=orpc" target="_blank" rel="sponsored noopener" title="David Walsh"><img src="https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4" width="119" alt="David Walsh"/><br />David Walsh</a></td>
129
- <td align="center"><a href="https://github.com/Robbe95?ref=orpc" target="_blank" rel="sponsored noopener" title="Robbe Vaes"><img src="https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4" width="119" alt="Robbe Vaes"/><br />Robbe Vaes</a></td>
130
- <td align="center"><a href="https://github.com/aidansunbury?ref=orpc" target="_blank" rel="sponsored noopener" title="Aidan Sunbury"><img src="https://avatars.githubusercontent.com/u/64103161?v=4" width="119" alt="Aidan Sunbury"/><br />Aidan Sunbury</a></td>
131
- <td align="center"><a href="https://github.com/soonoo?ref=orpc" target="_blank" rel="sponsored noopener" title="soonoo"><img src="https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4" width="119" alt="soonoo"/><br />soonoo</a></td>
132
- <td align="center"><a href="https://github.com/kporten?ref=orpc" target="_blank" rel="sponsored noopener" title="Kevin Porten"><img src="https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4" width="119" alt="Kevin Porten"/><br />Kevin Porten</a></td>
133
- <td align="center"><a href="https://github.com/pumpkinlink?ref=orpc" target="_blank" rel="sponsored noopener" title="Denis"><img src="https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4" width="119" alt="Denis"/><br />Denis</a></td>
134
- <td align="center"><a href="https://github.com/christopher-kapic?ref=orpc" target="_blank" rel="sponsored noopener" title="Christopher Kapic"><img src="https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4" width="119" alt="Christopher Kapic"/><br />Christopher Kapic</a></td>
122
+ <td align="center"><a href="https://github.com/rhinodavid?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="David Walsh"><img src="https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&amp;v=4" width="119" alt="David Walsh"/><br />David Walsh</a></td>
123
+ <td align="center"><a href="https://robbevaes.be/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Robbe Vaes"><img src="https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&amp;v=4" width="119" alt="Robbe Vaes"/><br />Robbe Vaes</a></td>
124
+ <td align="center"><a href="https://github.com/aidansunbury?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Aidan Sunbury"><img src="https://avatars.githubusercontent.com/u/64103161?v=4" width="119" alt="Aidan Sunbury"/><br />Aidan Sunbury</a></td>
125
+ <td align="center"><a href="https://github.com/soonoo?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="soonoo"><img src="https://avatars.githubusercontent.com/u/5436405?u=5d0b4aa955c87e30e6bda7f0cccae5402da99528&amp;v=4" width="119" alt="soonoo"/><br />soonoo</a></td>
126
+ <td align="center"><a href="https://kevinporten.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Kevin Porten"><img src="https://avatars.githubusercontent.com/u/1839345?u=dc2263d5cfe0d927ce1a0be04a1d55dd6b55405c&amp;v=4" width="119" alt="Kevin Porten"/><br />Kevin Porten</a></td>
127
+ <td align="center"><a href="https://github.com/pumpkinlink?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Denis"><img src="https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&amp;v=4" width="119" alt="Denis"/><br />Denis</a></td>
128
+ <td align="center"><a href="https://github.com/christopher-kapic?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Christopher Kapic"><img src="https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&amp;v=4" width="119" alt="Christopher Kapic"/><br />Christopher Kapic</a></td>
135
129
  </tr>
136
130
  <tr>
137
- <td align="center"><a href="https://github.com/thomasballinger?ref=orpc" target="_blank" rel="sponsored noopener" title="Tom Ballinger"><img src="https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4" width="119" alt="Tom Ballinger"/><br />Tom Ballinger</a></td>
138
- <td align="center"><a href="https://github.com/SSam0419?ref=orpc" target="_blank" rel="sponsored noopener" title="Sam"><img src="https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4" width="119" alt="Sam"/><br />Sam</a></td>
139
- <td align="center"><a href="https://github.com/Titoine?ref=orpc" target="_blank" rel="sponsored noopener" title="Titoine"><img src="https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4" width="119" alt="Titoine"/><br />Titoine</a></td>
140
- <td align="center"><a href="https://github.com/Mnigos?ref=orpc" target="_blank" rel="sponsored noopener" title="Igor Makowski"><img src="https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4" width="119" alt="Igor Makowski"/><br />Igor Makowski</a></td>
141
- <td align="center"><a href="https://github.com/hanayashiki?ref=orpc" target="_blank" rel="sponsored noopener" title="hanayashiki"><img src="https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4" width="119" alt="hanayashiki"/><br />hanayashiki</a></td>
142
- <td align="center"><a href="https://github.com/ldub?ref=orpc" target="_blank" rel="sponsored noopener" title="Lev Dubinets"><img src="https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4" width="119" alt="Lev Dubinets"/><br />Lev Dubinets</a></td>
143
- <td align="center"><a href="https://github.com/mr-kelly?ref=orpc" target="_blank" rel="sponsored noopener" title="Kelly Peilin Chan"><img src="https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4" width="119" alt="Kelly Peilin Chan"/><br />Kelly Peilin Chan</a></td>
131
+ <td align="center"><a href="http://ballingt.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Tom Ballinger"><img src="https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&amp;v=4" width="119" alt="Tom Ballinger"/><br />Tom Ballinger</a></td>
132
+ <td align="center"><a href="https://lee-sam.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Sam"><img src="https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&amp;v=4" width="119" alt="Sam"/><br />Sam</a></td>
133
+ <td align="center"><a href="https://github.com/Titoine?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Titoine"><img src="https://avatars.githubusercontent.com/u/3514286?u=1bb1e86b0c99c8a1121372e56d51a177eea12191&amp;v=4" width="119" alt="Titoine"/><br />Titoine</a></td>
134
+ <td align="center"><a href="https://rigtch.fm/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Igor Makowski"><img src="https://avatars.githubusercontent.com/u/56691628?u=ee8c879478f7c151b9156aef6c74243fa3e247a8&amp;v=4" width="119" alt="Igor Makowski"/><br />Igor Makowski</a></td>
135
+ <td align="center"><a href="https://blog.cwang.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="hanayashiki"><img src="https://avatars.githubusercontent.com/u/26056783?u=06c3b9205a16fd41a871e82da1cc2a09306d53f5&amp;v=4" width="119" alt="hanayashiki"/><br />hanayashiki</a></td>
136
+ <td align="center"><a href="https://dubinets.io/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Lev Dubinets"><img src="https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&amp;v=4" width="119" alt="Lev Dubinets"/><br />Lev Dubinets</a></td>
137
+ <td align="center"><a href="https://bika.ai/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Kelly Peilin Chan"><img src="https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&amp;v=4" width="119" alt="Kelly Peilin Chan"/><br />Kelly Peilin Chan</a></td>
144
138
  </tr>
145
139
  <tr>
146
- <td align="center"><a href="https://github.com/guyariely?ref=orpc" target="_blank" rel="sponsored noopener" title="Guy Ariely"><img src="https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4" width="119" alt="Guy Ariely"/><br />Guy Ariely</a></td>
147
- <td align="center"><a href="https://github.com/piscis?ref=orpc" target="_blank" rel="sponsored noopener" title="Alex"><img src="https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4" width="119" alt="Alex"/><br />Alex</a></td>
148
- <td align="center"><a href="https://github.com/finom?ref=orpc" target="_blank" rel="sponsored noopener" title="Andrey Gubanov"><img src="https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4" width="119" alt="Andrey Gubanov"/><br />Andrey Gubanov</a></td>
140
+ <td align="center"><a href="https://guyariely.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Guy Ariely"><img src="https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&amp;v=4" width="119" alt="Guy Ariely"/><br />Guy Ariely</a></td>
141
+ <td align="center"><a href="https://piscis.dev/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Alex"><img src="https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&amp;v=4" width="119" alt="Alex"/><br />Alex</a></td>
142
+ <td align="center"><a href="https://opensource.gubanov.eu/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Andrey Gubanov"><img src="https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&amp;v=4" width="119" alt="Andrey Gubanov"/><br />Andrey Gubanov</a></td>
149
143
  </tr>
150
144
  </table>
151
145
 
152
- With thanks to 37 past sponsors who helped get oRPC here.
146
+ With thanks to [37 past sponsors](https://htmlpreview.github.io/?https://github.com/middleapi/static/blob/main/sponsors.svg) who helped get oRPC here.
153
147
 
154
148
  ## References
155
149
 
package/dist/index.d.mts CHANGED
@@ -1,5 +1,52 @@
1
1
  import { Context } from '@orpc/server';
2
- import { StandardHandlerPlugin, StandardHandlerOptions } from '@orpc/server/standard';
2
+ import { StandardHandlerPlugin, StandardHandlerOptions, StandardHandlerRoutingInterceptorOptions } from '@orpc/server/standard';
3
+ import { Value, Promisable } from '@orpc/shared';
4
+ import { FilePropertyBag } from 'node:buffer';
5
+
6
+ interface BatchResponseCompressionHandlerPluginOptions {
7
+ /**
8
+ * The compression schemes to use for batch responses.
9
+ * Schemes are prioritized by their order in this array and
10
+ * only applied if the client supports them (via Accept-Encoding).
11
+ *
12
+ * @default ['gzip', 'deflate']
13
+ */
14
+ encodings?: readonly ('gzip' | 'deflate' | 'deflate-raw')[];
15
+ /**
16
+ * The minimum response size in bytes required to trigger compression.
17
+ * Responses smaller than this threshold will not be compressed to avoid overhead.
18
+ * A streaming batch response has no size until it ends, so it is always compressed.
19
+ *
20
+ * @default 1024 (1KB)
21
+ */
22
+ threshold?: number;
23
+ }
24
+ /**
25
+ * Compresses every successful [batch](https://orpc.dev/docs/plugins/batch) response, which the
26
+ * runtime-agnostic {@link https://orpc.dev/docs/plugins/response-compression | Response Compression Plugin}
27
+ * leaves untouched when framed: a batch frames subresponses of mixed content types, so its envelope
28
+ * has none of its own and nothing says whether the contents compress. Registering this plugin
29
+ * states that they do.
30
+ *
31
+ * @remarks
32
+ * Each message is flushed as soon as it is produced, so a streaming batch stays streaming.
33
+ * This is why the plugin lives in the Node.js package: `zlib` can flush mid-stream, while the
34
+ * web `CompressionStream` cannot, and would hold every early message until the batch ends.
35
+ *
36
+ * @see {@link https://orpc.dev/docs/plugins/batch-response-compression | Batch Response Compression Plugin}
37
+ */
38
+ declare class BatchResponseCompressionHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
39
+ name: string;
40
+ /**
41
+ * Compression should be done after batching, to compress the assembled batch response
42
+ * rather than each subresponse inside it.
43
+ */
44
+ after: string[];
45
+ private readonly encodings;
46
+ private readonly threshold;
47
+ constructor(options?: BatchResponseCompressionHandlerPluginOptions);
48
+ init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
49
+ }
3
50
 
4
51
  interface StaticFileHandlerPluginOptions {
5
52
  /**
@@ -126,5 +173,127 @@ declare class StaticFileHandlerPlugin<T extends Context> implements StandardHand
126
173
  private serve;
127
174
  }
128
175
 
129
- export { StaticFileHandlerPlugin };
130
- export type { StaticFileHandlerPluginOptions };
176
+ interface TmpFileUploadHandlerPluginMaxBodySize {
177
+ /**
178
+ * The maximum total size in bytes of request body content that is parsed
179
+ * into memory: JSON, URL-encoded forms, and the plain fields of a multipart
180
+ * body. A larger body rejects the request with `PAYLOAD_TOO_LARGE`. Usually
181
+ * the lowest of the three limits, because this content cannot stream anywhere.
182
+ */
183
+ memory: number;
184
+ /**
185
+ * The maximum total size in bytes of upload content a request streams into
186
+ * temporary files: file bodies and the file parts of a multipart body
187
+ * combined. A larger body rejects the request with `PAYLOAD_TOO_LARGE`.
188
+ */
189
+ file: number;
190
+ /**
191
+ * The maximum total size in bytes of a request body that is consumed as a
192
+ * stream: event streams and raw binary streams. Enforced while the stream is
193
+ * consumed, so an oversized stream fails at the reader. Usually the highest
194
+ * of the three limits, because this content is consumed on the fly.
195
+ */
196
+ stream: number;
197
+ }
198
+ interface TmpFileUploadHandlerPluginOptions<T extends Context> {
199
+ /**
200
+ * The directory temporary files are created under. Each request that spools
201
+ * an upload gets its own subdirectory inside it, removed when the request
202
+ * finishes.
203
+ *
204
+ * @default os.tmpdir()
205
+ */
206
+ tmpDir?: string;
207
+ /**
208
+ * The size limit for each kind of request body, either fixed or resolved
209
+ * per request, so limits can follow the request itself, such as a larger
210
+ * allowance for an authenticated user than for a guest. Every kind is
211
+ * required when the option is given, so none is left unbounded by accident;
212
+ * set a kind to `Number.POSITIVE_INFINITY` to deliberately leave it
213
+ * unlimited.
214
+ *
215
+ * A resolver runs only when a body is parsed, so a request without one never
216
+ * pays for it, and it receives the request as it arrived, before this plugin
217
+ * wraps its body resolution.
218
+ *
219
+ * @default unlimited for every kind
220
+ */
221
+ maxBodySize?: Value<Promisable<TmpFileUploadHandlerPluginMaxBodySize>, [options: StandardHandlerRoutingInterceptorOptions<T>]>;
222
+ }
223
+ /**
224
+ * A `File` whose content lives in a temporary file on disk and is read lazily,
225
+ * so it can represent an upload far larger than available memory.
226
+ *
227
+ * @remarks
228
+ * Node's `fs.openAsBlob`, which backs this class, has known bugs with files
229
+ * over 2 GiB ({@link https://github.com/nodejs/node/issues/52585}): `size` is
230
+ * truncated above 4 GiB and `slice()` is unreliable above 2 GiB, while
231
+ * whole-file reads like `stream()` stay correct. When size or random access
232
+ * matters for such a file, stat or read {@link path} directly.
233
+ *
234
+ * @see {@link https://orpc.dev/docs/plugins/tmp-file-upload | Tmp File Upload Plugin}
235
+ */
236
+ declare class TmpFile extends File {
237
+ /**
238
+ * Absolute path of the temporary file holding this file's content.
239
+ * The file is removed when the request finishes, so move or copy it
240
+ * to keep the content. The `File` reads from this path, so it must
241
+ * not be read after the file is moved or removed.
242
+ */
243
+ readonly path: string;
244
+ constructor(blob: Blob,
245
+ /**
246
+ * Absolute path of the temporary file holding this file's content.
247
+ * The file is removed when the request finishes, so move or copy it
248
+ * to keep the content. The `File` reads from this path, so it must
249
+ * not be read after the file is moved or removed.
250
+ */
251
+ path: string, name: string, options?: FilePropertyBag);
252
+ }
253
+ /**
254
+ * Parses file uploads into temporary files instead of memory. Bodies the standard
255
+ * body parser would buffer into an in-memory `File`, and multipart file parts, are
256
+ * streamed to disk and surfaced as lazily read {@link TmpFile} instances, so requests
257
+ * far larger than available memory parse in constant memory. Every other body is left
258
+ * to the standard parser.
259
+ *
260
+ * Request body sizes are limited per content category: memory-parsed, spooled to
261
+ * disk, and streamed, each limit fixed or resolved per request. This subsumes the
262
+ * request limit plugin while sizing each kind of body to what it actually costs.
263
+ *
264
+ * @remarks
265
+ * Temporary files are removed when the request finishes. A streaming response body,
266
+ * an event iterator or a raw stream, keeps them alive until it completes, so
267
+ * responses that read the upload while streaming work. Any other response is
268
+ * transmitted after removal, so one that embeds the upload itself, as a `File` or
269
+ * inside `FormData`, needs the content copied or the file moved first.
270
+ *
271
+ * @see {@link https://orpc.dev/docs/plugins/tmp-file-upload | Tmp File Upload Plugin}
272
+ */
273
+ declare class TmpFileUploadHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
274
+ name: string;
275
+ /**
276
+ * Should parse the original request body instead of batched sub-requests.
277
+ */
278
+ after: string[];
279
+ /**
280
+ * Should run inside the limit and compression plugins, so spooling receives
281
+ * the size-checked, decompressed bytes.
282
+ */
283
+ before: string[];
284
+ private readonly tmpDir;
285
+ private readonly maxBodySize;
286
+ constructor(options?: TmpFileUploadHandlerPluginOptions<T>);
287
+ init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
288
+ private resolveBody;
289
+ /**
290
+ * Enforces a size limit on a body the standard parser handles, counting the
291
+ * raw bytes before handing them back for regular parsing.
292
+ */
293
+ private parseLimitedBody;
294
+ private spoolFileBody;
295
+ private parseMultipartBody;
296
+ }
297
+
298
+ export { BatchResponseCompressionHandlerPlugin, StaticFileHandlerPlugin, TmpFile, TmpFileUploadHandlerPlugin };
299
+ export type { BatchResponseCompressionHandlerPluginOptions, StaticFileHandlerPluginOptions, TmpFileUploadHandlerPluginMaxBodySize, TmpFileUploadHandlerPluginOptions };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,52 @@
1
1
  import { Context } from '@orpc/server';
2
- import { StandardHandlerPlugin, StandardHandlerOptions } from '@orpc/server/standard';
2
+ import { StandardHandlerPlugin, StandardHandlerOptions, StandardHandlerRoutingInterceptorOptions } from '@orpc/server/standard';
3
+ import { Value, Promisable } from '@orpc/shared';
4
+ import { FilePropertyBag } from 'node:buffer';
5
+
6
+ interface BatchResponseCompressionHandlerPluginOptions {
7
+ /**
8
+ * The compression schemes to use for batch responses.
9
+ * Schemes are prioritized by their order in this array and
10
+ * only applied if the client supports them (via Accept-Encoding).
11
+ *
12
+ * @default ['gzip', 'deflate']
13
+ */
14
+ encodings?: readonly ('gzip' | 'deflate' | 'deflate-raw')[];
15
+ /**
16
+ * The minimum response size in bytes required to trigger compression.
17
+ * Responses smaller than this threshold will not be compressed to avoid overhead.
18
+ * A streaming batch response has no size until it ends, so it is always compressed.
19
+ *
20
+ * @default 1024 (1KB)
21
+ */
22
+ threshold?: number;
23
+ }
24
+ /**
25
+ * Compresses every successful [batch](https://orpc.dev/docs/plugins/batch) response, which the
26
+ * runtime-agnostic {@link https://orpc.dev/docs/plugins/response-compression | Response Compression Plugin}
27
+ * leaves untouched when framed: a batch frames subresponses of mixed content types, so its envelope
28
+ * has none of its own and nothing says whether the contents compress. Registering this plugin
29
+ * states that they do.
30
+ *
31
+ * @remarks
32
+ * Each message is flushed as soon as it is produced, so a streaming batch stays streaming.
33
+ * This is why the plugin lives in the Node.js package: `zlib` can flush mid-stream, while the
34
+ * web `CompressionStream` cannot, and would hold every early message until the batch ends.
35
+ *
36
+ * @see {@link https://orpc.dev/docs/plugins/batch-response-compression | Batch Response Compression Plugin}
37
+ */
38
+ declare class BatchResponseCompressionHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
39
+ name: string;
40
+ /**
41
+ * Compression should be done after batching, to compress the assembled batch response
42
+ * rather than each subresponse inside it.
43
+ */
44
+ after: string[];
45
+ private readonly encodings;
46
+ private readonly threshold;
47
+ constructor(options?: BatchResponseCompressionHandlerPluginOptions);
48
+ init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
49
+ }
3
50
 
4
51
  interface StaticFileHandlerPluginOptions {
5
52
  /**
@@ -126,5 +173,127 @@ declare class StaticFileHandlerPlugin<T extends Context> implements StandardHand
126
173
  private serve;
127
174
  }
128
175
 
129
- export { StaticFileHandlerPlugin };
130
- export type { StaticFileHandlerPluginOptions };
176
+ interface TmpFileUploadHandlerPluginMaxBodySize {
177
+ /**
178
+ * The maximum total size in bytes of request body content that is parsed
179
+ * into memory: JSON, URL-encoded forms, and the plain fields of a multipart
180
+ * body. A larger body rejects the request with `PAYLOAD_TOO_LARGE`. Usually
181
+ * the lowest of the three limits, because this content cannot stream anywhere.
182
+ */
183
+ memory: number;
184
+ /**
185
+ * The maximum total size in bytes of upload content a request streams into
186
+ * temporary files: file bodies and the file parts of a multipart body
187
+ * combined. A larger body rejects the request with `PAYLOAD_TOO_LARGE`.
188
+ */
189
+ file: number;
190
+ /**
191
+ * The maximum total size in bytes of a request body that is consumed as a
192
+ * stream: event streams and raw binary streams. Enforced while the stream is
193
+ * consumed, so an oversized stream fails at the reader. Usually the highest
194
+ * of the three limits, because this content is consumed on the fly.
195
+ */
196
+ stream: number;
197
+ }
198
+ interface TmpFileUploadHandlerPluginOptions<T extends Context> {
199
+ /**
200
+ * The directory temporary files are created under. Each request that spools
201
+ * an upload gets its own subdirectory inside it, removed when the request
202
+ * finishes.
203
+ *
204
+ * @default os.tmpdir()
205
+ */
206
+ tmpDir?: string;
207
+ /**
208
+ * The size limit for each kind of request body, either fixed or resolved
209
+ * per request, so limits can follow the request itself, such as a larger
210
+ * allowance for an authenticated user than for a guest. Every kind is
211
+ * required when the option is given, so none is left unbounded by accident;
212
+ * set a kind to `Number.POSITIVE_INFINITY` to deliberately leave it
213
+ * unlimited.
214
+ *
215
+ * A resolver runs only when a body is parsed, so a request without one never
216
+ * pays for it, and it receives the request as it arrived, before this plugin
217
+ * wraps its body resolution.
218
+ *
219
+ * @default unlimited for every kind
220
+ */
221
+ maxBodySize?: Value<Promisable<TmpFileUploadHandlerPluginMaxBodySize>, [options: StandardHandlerRoutingInterceptorOptions<T>]>;
222
+ }
223
+ /**
224
+ * A `File` whose content lives in a temporary file on disk and is read lazily,
225
+ * so it can represent an upload far larger than available memory.
226
+ *
227
+ * @remarks
228
+ * Node's `fs.openAsBlob`, which backs this class, has known bugs with files
229
+ * over 2 GiB ({@link https://github.com/nodejs/node/issues/52585}): `size` is
230
+ * truncated above 4 GiB and `slice()` is unreliable above 2 GiB, while
231
+ * whole-file reads like `stream()` stay correct. When size or random access
232
+ * matters for such a file, stat or read {@link path} directly.
233
+ *
234
+ * @see {@link https://orpc.dev/docs/plugins/tmp-file-upload | Tmp File Upload Plugin}
235
+ */
236
+ declare class TmpFile extends File {
237
+ /**
238
+ * Absolute path of the temporary file holding this file's content.
239
+ * The file is removed when the request finishes, so move or copy it
240
+ * to keep the content. The `File` reads from this path, so it must
241
+ * not be read after the file is moved or removed.
242
+ */
243
+ readonly path: string;
244
+ constructor(blob: Blob,
245
+ /**
246
+ * Absolute path of the temporary file holding this file's content.
247
+ * The file is removed when the request finishes, so move or copy it
248
+ * to keep the content. The `File` reads from this path, so it must
249
+ * not be read after the file is moved or removed.
250
+ */
251
+ path: string, name: string, options?: FilePropertyBag);
252
+ }
253
+ /**
254
+ * Parses file uploads into temporary files instead of memory. Bodies the standard
255
+ * body parser would buffer into an in-memory `File`, and multipart file parts, are
256
+ * streamed to disk and surfaced as lazily read {@link TmpFile} instances, so requests
257
+ * far larger than available memory parse in constant memory. Every other body is left
258
+ * to the standard parser.
259
+ *
260
+ * Request body sizes are limited per content category: memory-parsed, spooled to
261
+ * disk, and streamed, each limit fixed or resolved per request. This subsumes the
262
+ * request limit plugin while sizing each kind of body to what it actually costs.
263
+ *
264
+ * @remarks
265
+ * Temporary files are removed when the request finishes. A streaming response body,
266
+ * an event iterator or a raw stream, keeps them alive until it completes, so
267
+ * responses that read the upload while streaming work. Any other response is
268
+ * transmitted after removal, so one that embeds the upload itself, as a `File` or
269
+ * inside `FormData`, needs the content copied or the file moved first.
270
+ *
271
+ * @see {@link https://orpc.dev/docs/plugins/tmp-file-upload | Tmp File Upload Plugin}
272
+ */
273
+ declare class TmpFileUploadHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
274
+ name: string;
275
+ /**
276
+ * Should parse the original request body instead of batched sub-requests.
277
+ */
278
+ after: string[];
279
+ /**
280
+ * Should run inside the limit and compression plugins, so spooling receives
281
+ * the size-checked, decompressed bytes.
282
+ */
283
+ before: string[];
284
+ private readonly tmpDir;
285
+ private readonly maxBodySize;
286
+ constructor(options?: TmpFileUploadHandlerPluginOptions<T>);
287
+ init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
288
+ private resolveBody;
289
+ /**
290
+ * Enforces a size limit on a body the standard parser handles, counting the
291
+ * raw bytes before handing them back for regular parsing.
292
+ */
293
+ private parseLimitedBody;
294
+ private spoolFileBody;
295
+ private parseMultipartBody;
296
+ }
297
+
298
+ export { BatchResponseCompressionHandlerPlugin, StaticFileHandlerPlugin, TmpFile, TmpFileUploadHandlerPlugin };
299
+ export type { BatchResponseCompressionHandlerPluginOptions, StaticFileHandlerPluginOptions, TmpFileUploadHandlerPluginMaxBodySize, TmpFileUploadHandlerPluginOptions };
package/dist/index.mjs CHANGED
@@ -1,10 +1,122 @@
1
- import { createReadStream } from 'node:fs';
2
- import { realpath, stat } from 'node:fs/promises';
1
+ import { Duplex, Readable } from 'node:stream';
2
+ import { constants, createGzip, createDeflate, createDeflateRaw } from 'node:zlib';
3
+ import { toArray, isNoTransformCacheControl, parseAcceptEncodingQualities, stringifyJSON, varyByAcceptEncoding, mergeHttpPath, tryDecodeURIComponent, matchesHttpPathPrefix, isCompressibleContentType, getOpenTelemetryConfig, value, isAsyncIteratorObject, override, wrapAsyncIterator, wrapReadableStream } from '@orpc/shared';
4
+ import { flattenStandardHeader, parseStandardUrl, resolveStandardBodyHint, getFilenameFromContentDisposition } from '@standardserver/core';
5
+ import { createReadStream, openAsBlob } from 'node:fs';
6
+ import { realpath, stat, mkdtemp, appendFile, rm } from 'node:fs/promises';
3
7
  import path from 'node:path';
4
- import { Readable } from 'node:stream';
5
- import { toArray, mergeHttpPath, tryDecodeURIComponent, matchesHttpPathPrefix, isCompressibleContentType, parseAcceptEncodingQualities, getOpenTelemetryConfig } from '@orpc/shared';
6
- import { parseStandardUrl, flattenStandardHeader } from '@standardserver/core';
7
8
  import mime from 'mime';
9
+ import { Buffer } from 'node:buffer';
10
+ import { tmpdir } from 'node:os';
11
+ import { ORPCError } from '@orpc/server';
12
+ import { toFetchHeaders, toStandardBody } from '@standardserver/fetch';
13
+
14
+ class BatchResponseCompressionHandlerPlugin {
15
+ name = "~batch-response-compression";
16
+ /**
17
+ * Compression should be done after batching, to compress the assembled batch response
18
+ * rather than each subresponse inside it.
19
+ */
20
+ after = ["~batch"];
21
+ encodings;
22
+ threshold;
23
+ constructor(options = {}) {
24
+ this.encodings = options.encodings ?? ["gzip", "deflate"];
25
+ this.threshold = options.threshold ?? 1024;
26
+ }
27
+ init(options) {
28
+ const routingInterceptor = async ({ next, ...interceptorOptions }) => {
29
+ const result = await next();
30
+ if (!result.matched) {
31
+ return result;
32
+ }
33
+ if (flattenStandardHeader(interceptorOptions.request.headers["orpc-batch"]) === void 0) {
34
+ return result;
35
+ }
36
+ const response = result.response;
37
+ const headers = response.headers;
38
+ const body = response.body;
39
+ if (response.status >= 400) {
40
+ return result;
41
+ }
42
+ if (flattenStandardHeader(headers["content-encoding"]) !== void 0) {
43
+ return result;
44
+ }
45
+ if (response.status === 206 || flattenStandardHeader(headers["content-range"]) !== void 0) {
46
+ return result;
47
+ }
48
+ if (isNoTransformCacheControl(flattenStandardHeader(headers["cache-control"]))) {
49
+ return result;
50
+ }
51
+ const acceptEncodings = parseAcceptEncodingQualities(
52
+ flattenStandardHeader(interceptorOptions.request.headers["accept-encoding"])
53
+ );
54
+ const encoding = this.encodings.find((enc) => (acceptEncodings.get(enc) ?? 0) > 0);
55
+ if (encoding === void 0) {
56
+ return result;
57
+ }
58
+ let stream;
59
+ let bodyHeaders;
60
+ if (Array.isArray(body)) {
61
+ const json = new Blob([stringifyJSON(body)]);
62
+ if (json.size < this.threshold) {
63
+ return result;
64
+ }
65
+ stream = json.stream();
66
+ bodyHeaders = {
67
+ // Serialized here rather than by the adapter, which no longer sees the array
68
+ "content-type": "application/json",
69
+ "standard-server": []
70
+ };
71
+ } else if (body instanceof Blob) {
72
+ if (Number.isFinite(body.size) && body.size < this.threshold) {
73
+ return result;
74
+ }
75
+ stream = body.stream();
76
+ bodyHeaders = {
77
+ /**
78
+ * The blob is gone once it becomes a stream, so the type it carried is stated here. The
79
+ * batch plugin puts it there rather than in the headers, because it builds the body
80
+ * itself instead of leaving it to the adapter.
81
+ */
82
+ "content-type": body.type,
83
+ "standard-server": "octet-stream"
84
+ };
85
+ } else if (body instanceof ReadableStream) {
86
+ stream = body;
87
+ bodyHeaders = { "standard-server": "octet-stream" };
88
+ } else {
89
+ return result;
90
+ }
91
+ return {
92
+ ...result,
93
+ response: {
94
+ ...response,
95
+ body: stream.pipeThrough(createFlushingCompressionStream(encoding)),
96
+ headers: {
97
+ ...headers,
98
+ ...bodyHeaders,
99
+ "content-length": [],
100
+ "content-encoding": encoding,
101
+ "vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
102
+ }
103
+ }
104
+ };
105
+ };
106
+ return {
107
+ ...options,
108
+ routingInterceptors: [
109
+ routingInterceptor,
110
+ ...toArray(options.routingInterceptors)
111
+ ]
112
+ };
113
+ }
114
+ }
115
+ function createFlushingCompressionStream(encoding) {
116
+ const zlibOptions = { flush: constants.Z_SYNC_FLUSH };
117
+ const compressor = encoding === "gzip" ? createGzip(zlibOptions) : encoding === "deflate" ? createDeflate(zlibOptions) : createDeflateRaw(zlibOptions);
118
+ return Duplex.toWeb(compressor);
119
+ }
8
120
 
9
121
  const TEXT_CONTENT_TYPE_REGEX = /^text\//;
10
122
  const PRECOMPRESSED_ENCODINGS = [
@@ -325,4 +437,409 @@ function parseByteRange(range, size) {
325
437
  return [start, end];
326
438
  }
327
439
 
328
- export { StaticFileHandlerPlugin };
440
+ const MAX_PART_HEADER_SIZE = 16 * 1024;
441
+ const CRLF = Buffer.from("\r\n");
442
+ const HEADER_BLOCK_END = Buffer.from("\r\n\r\n");
443
+ async function parseMultipart(source, boundary, onPart) {
444
+ const delimiter = Buffer.from(`\r
445
+ --${boundary}`);
446
+ let buffer = CRLF;
447
+ let state = "preamble";
448
+ let writer;
449
+ let done = false;
450
+ const process = async () => {
451
+ while (true) {
452
+ if (state === "preamble" || state === "body") {
453
+ const index = buffer.indexOf(delimiter);
454
+ if (index === -1) {
455
+ const settledLength = buffer.length - delimiter.length + 1;
456
+ if (settledLength > 0) {
457
+ if (state === "body") {
458
+ await writer.write(buffer.subarray(0, settledLength));
459
+ }
460
+ buffer = buffer.subarray(settledLength);
461
+ }
462
+ return;
463
+ }
464
+ if (state === "body") {
465
+ if (index > 0) {
466
+ await writer.write(buffer.subarray(0, index));
467
+ }
468
+ await writer.end();
469
+ writer = void 0;
470
+ }
471
+ buffer = buffer.subarray(index + delimiter.length);
472
+ state = "delimiter-end";
473
+ continue;
474
+ }
475
+ if (state === "delimiter-end") {
476
+ if (buffer.length < 2) {
477
+ return;
478
+ }
479
+ if (buffer[0] === 45 && buffer[1] === 45) {
480
+ done = true;
481
+ buffer = Buffer.alloc(0);
482
+ return;
483
+ }
484
+ let index = 0;
485
+ while (index < buffer.length && (buffer[index] === 32 || buffer[index] === 9)) {
486
+ index++;
487
+ }
488
+ if (index + 1 >= buffer.length) {
489
+ return;
490
+ }
491
+ if (buffer[index] !== 13 || buffer[index + 1] !== 10) {
492
+ throw new TypeError("Invalid multipart body: expected CRLF after a boundary delimiter");
493
+ }
494
+ buffer = buffer.subarray(index + 2);
495
+ state = "headers";
496
+ continue;
497
+ }
498
+ if (buffer[0] === 13 && buffer[1] === 10) {
499
+ throw new TypeError("Invalid multipart body: part is missing the content-disposition header");
500
+ }
501
+ const headerEnd = buffer.indexOf(HEADER_BLOCK_END);
502
+ if (headerEnd === -1) {
503
+ if (buffer.length > MAX_PART_HEADER_SIZE) {
504
+ throw new TypeError("Invalid multipart body: part headers exceed the maximum allowed size");
505
+ }
506
+ return;
507
+ }
508
+ const part = parsePartHeaders(buffer.subarray(0, headerEnd).toString());
509
+ buffer = buffer.subarray(headerEnd + HEADER_BLOCK_END.length);
510
+ writer = await onPart(part);
511
+ state = "body";
512
+ }
513
+ };
514
+ for await (const chunk of source) {
515
+ if (done) {
516
+ continue;
517
+ }
518
+ buffer = buffer.length === 0 ? Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength) : Buffer.concat([buffer, chunk]);
519
+ await process();
520
+ }
521
+ if (!done) {
522
+ throw new TypeError("Invalid multipart body: unexpected end of body");
523
+ }
524
+ }
525
+ function parsePartHeaders(block) {
526
+ let contentDisposition;
527
+ let contentType;
528
+ for (const line of block.split("\r\n")) {
529
+ const colon = line.indexOf(":");
530
+ if (colon === -1) {
531
+ throw new TypeError("Invalid multipart body: malformed part header");
532
+ }
533
+ const name2 = line.slice(0, colon).trim().toLowerCase();
534
+ if (name2 === "content-disposition") {
535
+ contentDisposition = line.slice(colon + 1).trim();
536
+ } else if (name2 === "content-type") {
537
+ contentType = line.slice(colon + 1).trim();
538
+ }
539
+ }
540
+ if (contentDisposition === void 0) {
541
+ throw new TypeError("Invalid multipart body: part is missing the content-disposition header");
542
+ }
543
+ const semicolon = contentDisposition.indexOf(";");
544
+ const dispositionType = (semicolon === -1 ? contentDisposition : contentDisposition.slice(0, semicolon)).trim().toLowerCase();
545
+ if (dispositionType !== "form-data") {
546
+ throw new TypeError("Invalid multipart body: content-disposition must be form-data");
547
+ }
548
+ const parameters = parseHeaderParameters(contentDisposition);
549
+ const name = parameters.get("name");
550
+ if (name === void 0) {
551
+ throw new TypeError("Invalid multipart body: content-disposition header is missing the name parameter");
552
+ }
553
+ const filename = parameters.get("filename");
554
+ return {
555
+ name: decodeContentDispositionParameter(name),
556
+ filename: filename === void 0 ? void 0 : decodeContentDispositionParameter(filename),
557
+ type: contentType
558
+ };
559
+ }
560
+ function decodeContentDispositionParameter(value) {
561
+ return value.replaceAll("%22", '"').replaceAll("%0D", "\r").replaceAll("%0A", "\n");
562
+ }
563
+ function parseHeaderParameters(header) {
564
+ const parameters = /* @__PURE__ */ new Map();
565
+ let index = header.indexOf(";");
566
+ while (index !== -1 && index < header.length) {
567
+ index++;
568
+ while (index < header.length && (header[index] === " " || header[index] === " ")) {
569
+ index++;
570
+ }
571
+ const equals = header.indexOf("=", index);
572
+ const nextSemicolon = header.indexOf(";", index);
573
+ if (equals === -1 || nextSemicolon !== -1 && nextSemicolon < equals) {
574
+ if (nextSemicolon === -1) {
575
+ break;
576
+ }
577
+ index = nextSemicolon;
578
+ continue;
579
+ }
580
+ const key = header.slice(index, equals).trim().toLowerCase();
581
+ index = equals + 1;
582
+ let value;
583
+ if (header[index] === '"') {
584
+ const closing = header.indexOf('"', index + 1);
585
+ value = closing === -1 ? header.slice(index + 1) : header.slice(index + 1, closing);
586
+ index = closing === -1 ? -1 : header.indexOf(";", closing + 1);
587
+ } else {
588
+ const semicolon = header.indexOf(";", index);
589
+ value = (semicolon === -1 ? header.slice(index) : header.slice(index, semicolon)).trim();
590
+ index = semicolon;
591
+ }
592
+ if (!parameters.has(key)) {
593
+ parameters.set(key, value);
594
+ }
595
+ }
596
+ return parameters;
597
+ }
598
+
599
+ class TmpFile extends File {
600
+ constructor(blob, path2, name, options) {
601
+ super([blob], name, options);
602
+ this.path = path2;
603
+ }
604
+ }
605
+ class TmpFileUploadHandlerPlugin {
606
+ name = "~tmp-file-upload";
607
+ /**
608
+ * Should parse the original request body instead of batched sub-requests.
609
+ */
610
+ after = ["~batch"];
611
+ /**
612
+ * Should run inside the limit and compression plugins, so spooling receives
613
+ * the size-checked, decompressed bytes.
614
+ */
615
+ before = ["~request-limit", "~request-compression"];
616
+ tmpDir;
617
+ maxBodySize;
618
+ constructor(options = {}) {
619
+ this.tmpDir = options.tmpDir ?? tmpdir();
620
+ this.maxBodySize = options.maxBodySize ?? {
621
+ memory: Number.POSITIVE_INFINITY,
622
+ file: Number.POSITIVE_INFINITY,
623
+ stream: Number.POSITIVE_INFINITY
624
+ };
625
+ }
626
+ init(options) {
627
+ const routingInterceptor = async ({ next, ...interceptorOptions }) => {
628
+ const tmpFiles = new RequestTmpFiles(this.tmpDir);
629
+ let cleanupDeferred = false;
630
+ try {
631
+ const result = await next({
632
+ ...interceptorOptions,
633
+ request: {
634
+ ...interceptorOptions.request,
635
+ resolveBody: (hint) => this.resolveBody(interceptorOptions, hint, tmpFiles)
636
+ }
637
+ });
638
+ if (result.matched && tmpFiles.inUse) {
639
+ const body = result.response.body;
640
+ if (isAsyncIteratorObject(body)) {
641
+ cleanupDeferred = true;
642
+ return {
643
+ matched: true,
644
+ response: {
645
+ ...result.response,
646
+ /**
647
+ * @warning
648
+ * Remember use `override` for AsyncIteratorObject to remain other special properties
649
+ */
650
+ body: override(body, wrapAsyncIterator(body, { onFinish: () => tmpFiles.cleanup() }))
651
+ }
652
+ };
653
+ }
654
+ if (body instanceof ReadableStream) {
655
+ cleanupDeferred = true;
656
+ return {
657
+ matched: true,
658
+ response: {
659
+ ...result.response,
660
+ body: wrapReadableStream(body, { onFinish: () => tmpFiles.cleanup() })
661
+ }
662
+ };
663
+ }
664
+ }
665
+ return result;
666
+ } finally {
667
+ if (!cleanupDeferred) {
668
+ await tmpFiles.cleanup();
669
+ }
670
+ }
671
+ };
672
+ return {
673
+ ...options,
674
+ routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
675
+ };
676
+ }
677
+ async resolveBody(interceptorOptions, hint, tmpFiles) {
678
+ const { request } = interceptorOptions;
679
+ const resolvedHint = hint ?? resolveStandardBodyHint(request.headers);
680
+ if (resolvedHint === "none") {
681
+ return request.resolveBody(hint);
682
+ }
683
+ const maxBodySize = await value(this.maxBodySize, interceptorOptions);
684
+ if (resolvedHint === "form-data") {
685
+ return this.parseMultipartBody(request, tmpFiles, maxBodySize);
686
+ }
687
+ if (resolvedHint === "file") {
688
+ return this.spoolFileBody(request, tmpFiles, maxBodySize.file);
689
+ }
690
+ if (resolvedHint === "json" || resolvedHint === "url-search-params") {
691
+ return this.parseLimitedBody(request, hint, resolvedHint, maxBodySize.memory);
692
+ }
693
+ return this.parseLimitedBody(request, hint, resolvedHint, maxBodySize.stream);
694
+ }
695
+ /**
696
+ * Enforces a size limit on a body the standard parser handles, counting the
697
+ * raw bytes before handing them back for regular parsing.
698
+ */
699
+ async parseLimitedBody(request, hint, resolvedHint, limit) {
700
+ if (limit === Number.POSITIVE_INFINITY) {
701
+ return request.resolveBody(hint);
702
+ }
703
+ assertContentLengthWithin(request.headers, limit);
704
+ const stream = await request.resolveBody("octet-stream");
705
+ if (!(stream instanceof ReadableStream)) {
706
+ return stream;
707
+ }
708
+ const response = new Response(limitStream(stream, limit), {
709
+ headers: toFetchHeaders(request.headers)
710
+ });
711
+ return toStandardBody(response, { hint: resolvedHint });
712
+ }
713
+ async spoolFileBody(request, tmpFiles, fileLimit) {
714
+ assertContentLengthWithin(request.headers, fileLimit);
715
+ const stream = await request.resolveBody("octet-stream");
716
+ if (!(stream instanceof ReadableStream)) {
717
+ return stream;
718
+ }
719
+ const contentDisposition = flattenStandardHeader(request.headers["content-disposition"]);
720
+ const fileName = contentDisposition !== void 0 ? getFilenameFromContentDisposition(contentDisposition) : void 0;
721
+ const contentType = flattenStandardHeader(request.headers["content-type"]);
722
+ const limited = fileLimit === Number.POSITIVE_INFINITY ? stream : limitStream(stream, fileLimit);
723
+ const tmpPath = await tmpFiles.allocate();
724
+ for await (const chunk of limited) {
725
+ await tmpFiles.append(tmpPath, chunk);
726
+ }
727
+ return tmpFiles.seal(tmpPath, fileName ?? "blob", contentType ?? "");
728
+ }
729
+ async parseMultipartBody(request, tmpFiles, maxBodySize) {
730
+ const contentType = flattenStandardHeader(request.headers["content-type"]);
731
+ const boundary = contentType === void 0 ? void 0 : parseHeaderParameters(contentType).get("boundary");
732
+ if (boundary === void 0 || boundary === "") {
733
+ throw new TypeError("Invalid multipart body: content-type header is missing the boundary parameter");
734
+ }
735
+ const totalLimit = maxBodySize.memory + maxBodySize.file;
736
+ assertContentLengthWithin(request.headers, totalLimit);
737
+ const stream = await request.resolveBody("octet-stream");
738
+ if (!(stream instanceof ReadableStream)) {
739
+ return stream;
740
+ }
741
+ const limited = totalLimit === Number.POSITIVE_INFINITY ? stream : limitStream(stream, totalLimit);
742
+ const form = new FormData();
743
+ let memoryUsed = 0;
744
+ let fileUsed = 0;
745
+ await parseMultipart(limited, boundary, (part) => {
746
+ const name = part.name;
747
+ if (part.filename === void 0) {
748
+ const chunks = [];
749
+ return {
750
+ write: (chunk) => {
751
+ memoryUsed += chunk.length;
752
+ if (memoryUsed > maxBodySize.memory) {
753
+ throw new ORPCError("PAYLOAD_TOO_LARGE");
754
+ }
755
+ chunks.push(Buffer.from(chunk));
756
+ },
757
+ end: () => {
758
+ form.append(name, Buffer.concat(chunks).toString());
759
+ }
760
+ };
761
+ }
762
+ const filename = part.filename;
763
+ const type = part.type ?? "text/plain";
764
+ let tmpPath;
765
+ return {
766
+ write: async (chunk) => {
767
+ fileUsed += chunk.length;
768
+ if (fileUsed > maxBodySize.file) {
769
+ throw new ORPCError("PAYLOAD_TOO_LARGE");
770
+ }
771
+ tmpPath ??= await tmpFiles.allocate();
772
+ await tmpFiles.append(tmpPath, chunk);
773
+ },
774
+ end: async () => {
775
+ tmpPath ??= await tmpFiles.allocate();
776
+ form.append(name, await tmpFiles.seal(tmpPath, filename, type));
777
+ }
778
+ };
779
+ });
780
+ return form;
781
+ }
782
+ }
783
+ function assertContentLengthWithin(headers, limit) {
784
+ const contentLength = Number(flattenStandardHeader(headers["content-length"]));
785
+ if (Number.isFinite(contentLength) && contentLength > limit) {
786
+ throw new ORPCError("PAYLOAD_TOO_LARGE");
787
+ }
788
+ }
789
+ function limitStream(stream, limit) {
790
+ let total = 0;
791
+ return stream.pipeThrough(new TransformStream({
792
+ transform(chunk, controller) {
793
+ total += chunk.byteLength;
794
+ if (total > limit) {
795
+ controller.error(new ORPCError("PAYLOAD_TOO_LARGE"));
796
+ return;
797
+ }
798
+ controller.enqueue(chunk);
799
+ }
800
+ }));
801
+ }
802
+ const EMPTY_CHUNK = new Uint8Array(0);
803
+ class RequestTmpFiles {
804
+ constructor(tmpDir) {
805
+ this.tmpDir = tmpDir;
806
+ }
807
+ dir;
808
+ count = 0;
809
+ get inUse() {
810
+ return this.dir !== void 0;
811
+ }
812
+ async allocate() {
813
+ try {
814
+ this.dir ??= mkdtemp(path.join(this.tmpDir, "orpc-upload-"));
815
+ return path.join(await this.dir, String(this.count++));
816
+ } catch (cause) {
817
+ throw new ORPCError("INTERNAL_SERVER_ERROR", { cause });
818
+ }
819
+ }
820
+ async append(tmpPath, chunk) {
821
+ try {
822
+ await appendFile(tmpPath, chunk);
823
+ } catch (cause) {
824
+ throw new ORPCError("INTERNAL_SERVER_ERROR", { cause });
825
+ }
826
+ }
827
+ async seal(tmpPath, name, type) {
828
+ try {
829
+ await appendFile(tmpPath, EMPTY_CHUNK);
830
+ return new TmpFile(await openAsBlob(tmpPath), tmpPath, name, { type });
831
+ } catch (cause) {
832
+ throw new ORPCError("INTERNAL_SERVER_ERROR", { cause });
833
+ }
834
+ }
835
+ async cleanup() {
836
+ if (this.dir !== void 0) {
837
+ try {
838
+ await rm(await this.dir, { recursive: true, force: true });
839
+ } catch {
840
+ }
841
+ }
842
+ }
843
+ }
844
+
845
+ export { BatchResponseCompressionHandlerPlugin, StaticFileHandlerPlugin, TmpFile, TmpFileUploadHandlerPlugin };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@orpc/node",
3
3
  "type": "module",
4
- "version": "2.0.0-beta.27",
4
+ "version": "2.0.0-beta.29",
5
5
  "license": "MIT",
6
6
  "funding": "https://github.com/sponsors/dinwwwh",
7
7
  "homepage": "https://orpc.dev",
@@ -27,13 +27,15 @@
27
27
  "dist"
28
28
  ],
29
29
  "dependencies": {
30
- "@standardserver/core": "^0.7.1",
30
+ "@standardserver/core": "^0.8.0",
31
+ "@standardserver/fetch": "^0.8.0",
31
32
  "mime": "^4.1.0",
32
- "@orpc/server": "2.0.0-beta.27",
33
- "@orpc/shared": "2.0.0-beta.27"
33
+ "@orpc/server": "2.0.0-beta.29",
34
+ "@orpc/shared": "2.0.0-beta.29"
34
35
  },
35
36
  "devDependencies": {
36
- "supertest": "^7.2.2"
37
+ "supertest": "^7.2.2",
38
+ "@orpc/client": "2.0.0-beta.29"
37
39
  },
38
40
  "scripts": {
39
41
  "build": "unbuild",