@orpc/node 2.0.0-beta.39 → 2.0.0-beta.41
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 +5 -5
- package/dist/index.d.mts +12 -8
- package/dist/index.d.ts +12 -8
- package/dist/index.mjs +98 -54
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
|
|
|
85
85
|
|
|
86
86
|
<table>
|
|
87
87
|
<tr>
|
|
88
|
-
<td width="2000"><a href="https://screenshotone.com/?ref=middleapi&utm_source=middleapi&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>
|
|
88
|
+
<td width="2000"><a href="https://screenshotone.com/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" 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>
|
|
89
89
|
</tr>
|
|
90
90
|
<tr>
|
|
91
91
|
<td width="2000"><a href="https://yuzu.health/careers?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="We're hiring NYC based engineers"><img src="https://avatars.githubusercontent.com/u/102488956?v=4" width="64" align="left" hspace="12" alt="Yuzu"/><b>Yuzu</b></a><br /><sub>We're hiring NYC based engineers</sub></td>
|
|
@@ -127,19 +127,18 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
|
|
|
127
127
|
<td align="center"><a href="https://soymilk.party/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="あわわわとーにゅ"><img src="https://avatars.githubusercontent.com/u/17376330?u=de3353804be889f009f7e0a1582daf04d0ab292d&v=4" width="139" alt="あわわわとーにゅ"/><br />あわわわとーにゅ</a></td>
|
|
128
128
|
<td align="center"><a href="https://github.com/nicognaW?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="nk"><img src="https://avatars.githubusercontent.com/u/66731869?u=4699bda3a9092d3ec34fbd959450767bcc8b8b6d&v=4" width="139" alt="nk"/><br />nk</a></td>
|
|
129
129
|
<td align="center"><a href="https://supastarter.dev/?ref=middleapi&utm_source=middleapi&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>
|
|
130
|
-
<td align="center"><a href="https://github.com/divmgl?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Dexter Miguel"><img src="https://avatars.githubusercontent.com/u/5452298?u=645993204be8696c085ecf0d228c3062efe2ed65&v=4" width="139" alt="Dexter Miguel"/><br />Dexter Miguel</a></td>
|
|
131
130
|
<td align="center"><a href="https://github.com/herrfugbaum?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="herrfugbaum"><img src="https://avatars.githubusercontent.com/u/12859776?u=644dc1666d0220bc0468eb0de3c56b919f635b16&v=4" width="139" alt="herrfugbaum"/><br />herrfugbaum</a></td>
|
|
131
|
+
<td align="center"><a href="https://laststance.io/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ryota Murakami"><img src="https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&v=4" width="139" alt="Ryota Murakami"/><br />Ryota Murakami</a></td>
|
|
132
132
|
</tr>
|
|
133
133
|
<tr>
|
|
134
|
-
<td align="center"><a href="https://laststance.io/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ryota Murakami"><img src="https://avatars.githubusercontent.com/u/5501268?u=599389e03340734325726ca3f8f423c021d47d7f&v=4" width="139" alt="Ryota Murakami"/><br />Ryota Murakami</a></td>
|
|
135
134
|
<td align="center"><a href="https://cra.mr/?ref=middleapi&utm_source=middleapi&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>
|
|
136
135
|
<td align="center"><a href="https://valerii15298.github.io/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Valerii Petryniak"><img src="https://avatars.githubusercontent.com/u/44531564?u=88ac74d9bacd20401518441907acad21063cd397&v=4" width="139" alt="Valerii Petryniak"/><br />Valerii Petryniak</a></td>
|
|
137
136
|
<td align="center"><a href="https://letstri.dev/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Valerii Strilets"><img src="https://avatars.githubusercontent.com/u/13253748?u=c7b10399ccc8f8081e24db94ec32cd9858e86ac3&v=4" width="139" alt="Valerii Strilets"/><br />Valerii Strilets</a></td>
|
|
138
137
|
<td align="center"><a href="https://blacklight.sh/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Kyle Mistele"><img src="https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&v=4" width="139" alt="Kyle Mistele"/><br />Kyle Mistele</a></td>
|
|
139
138
|
<td align="center"><a href="https://github.com/christ12938?ref=middleapi&utm_source=middleapi&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>
|
|
139
|
+
<td align="center"><a href="https://github.com/Ryanjso?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ryan Soderberg"><img src="https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&v=4" width="139" alt="Ryan Soderberg"/><br />Ryan Soderberg</a></td>
|
|
140
140
|
</tr>
|
|
141
141
|
<tr>
|
|
142
|
-
<td align="center"><a href="https://github.com/Ryanjso?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ryan Soderberg"><img src="https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&v=4" width="139" alt="Ryan Soderberg"/><br />Ryan Soderberg</a></td>
|
|
143
142
|
<td align="center"><a href="https://github.com/itigoore01?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="shota"><img src="https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&v=4" width="139" alt="shota"/><br />shota</a></td>
|
|
144
143
|
<td align="center"><a href="https://github.com/ellis-driscoll?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Ellis Driscoll"><img src="https://avatars.githubusercontent.com/u/70685966?u=c5f95bc33b5991d9744abe00052542e4a2ed3cb9&v=4" width="139" alt="Ellis Driscoll"/><br />Ellis Driscoll</a></td>
|
|
145
144
|
<td align="center"><a href="https://github.com/hoangbn?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Hoang Nguyen"><img src="https://avatars.githubusercontent.com/u/38968280?u=c90084c6de65c56facabab7ba13a72a49ddbc3e4&v=4" width="139" alt="Hoang Nguyen"/><br />Hoang Nguyen</a></td>
|
|
@@ -173,12 +172,13 @@ Like what we build over at [middleapi](https://github.com/middleapi)? You can he
|
|
|
173
172
|
<td align="center"><a href="https://kellychan.im/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Kelly Peilin Chan"><img src="https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&v=4" width="119" alt="Kelly Peilin Chan"/><br />Kelly Peilin Chan</a></td>
|
|
174
173
|
<td align="center"><a href="https://guyariely.com/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Guy Ariely"><img src="https://avatars.githubusercontent.com/u/42813496?u=edb6b7f563bf28e160a290832e7da57c0506f8ca&v=4" width="119" alt="Guy Ariely"/><br />Guy Ariely</a></td>
|
|
175
174
|
<td align="center"><a href="https://paulsenon.com/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="PaulSenon"><img src="https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&v=4" width="119" alt="PaulSenon"/><br />PaulSenon</a></td>
|
|
175
|
+
<td align="center"><a href="https://nakasyou.how/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Shotaro Nakamura"><img src="https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&v=4" width="119" alt="Shotaro Nakamura"/><br />Shotaro Nakamura</a></td>
|
|
176
176
|
<td align="center"><a href="https://piscis.dev/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Alex"><img src="https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&v=4" width="119" alt="Alex"/><br />Alex</a></td>
|
|
177
177
|
<td align="center"><a href="https://opensource.gubanov.eu/?ref=middleapi&utm_source=middleapi&utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Andrey Gubanov"><img src="https://avatars.githubusercontent.com/u/1082083?u=c5f2daf7ebece498e85c83367bb37b4e10e2649d&v=4" width="119" alt="Andrey Gubanov"/><br />Andrey Gubanov</a></td>
|
|
178
178
|
</tr>
|
|
179
179
|
</table>
|
|
180
180
|
|
|
181
|
-
With thanks to [
|
|
181
|
+
With thanks to [38 past sponsors](https://orpc.dev/sponsors#past-sponsors) who helped get oRPC here.
|
|
182
182
|
|
|
183
183
|
## References
|
|
184
184
|
|
package/dist/index.d.mts
CHANGED
|
@@ -83,6 +83,8 @@ interface StaticFileHandlerPluginOptions {
|
|
|
83
83
|
cacheControl?: string | false;
|
|
84
84
|
/**
|
|
85
85
|
* Whether files and directories whose name starts with a dot can be served.
|
|
86
|
+
* When disabled, names shaped like 8.3 short names, such as `ENV~1`, are refused too,
|
|
87
|
+
* since NTFS and FAT resolve them to dotfiles.
|
|
86
88
|
*
|
|
87
89
|
* @default false
|
|
88
90
|
*/
|
|
@@ -175,10 +177,11 @@ declare class StaticFileHandlerPlugin<T extends Context> implements StandardHand
|
|
|
175
177
|
|
|
176
178
|
interface TmpFileUploadHandlerPluginMaxBodySize {
|
|
177
179
|
/**
|
|
178
|
-
* The maximum
|
|
179
|
-
*
|
|
180
|
-
* body
|
|
181
|
-
* the lowest of the three limits,
|
|
180
|
+
* The maximum size in bytes of request body content buffered into memory:
|
|
181
|
+
* JSON, URL-encoded forms, and the fields and part headers of a multipart
|
|
182
|
+
* body in total, and each event of an event stream on its own. Exceeding it
|
|
183
|
+
* rejects with `PAYLOAD_TOO_LARGE`. Usually the lowest of the three limits,
|
|
184
|
+
* because this content cannot stream anywhere.
|
|
182
185
|
*/
|
|
183
186
|
memory: number;
|
|
184
187
|
/**
|
|
@@ -191,7 +194,8 @@ interface TmpFileUploadHandlerPluginMaxBodySize {
|
|
|
191
194
|
* The maximum total size in bytes of a request body that is consumed as a
|
|
192
195
|
* stream: event streams and raw binary streams. Enforced while the stream is
|
|
193
196
|
* consumed, so an oversized stream fails at the reader. Usually the highest
|
|
194
|
-
* of the three limits,
|
|
197
|
+
* of the three limits, but keep it finite so no client can stream
|
|
198
|
+
* indefinitely.
|
|
195
199
|
*/
|
|
196
200
|
stream: number;
|
|
197
201
|
}
|
|
@@ -257,8 +261,8 @@ declare class TmpFile extends File {
|
|
|
257
261
|
* far larger than available memory parse in constant memory. Every other body is left
|
|
258
262
|
* to the standard parser.
|
|
259
263
|
*
|
|
260
|
-
* Request body sizes are limited per content category: memory
|
|
261
|
-
* disk, and streamed, each limit fixed or resolved per request. This subsumes the
|
|
264
|
+
* Request body sizes are limited per content category: buffered in memory, spooled
|
|
265
|
+
* to disk, and streamed, each limit fixed or resolved per request. This subsumes the
|
|
262
266
|
* request limit plugin while sizing each kind of body to what it actually costs.
|
|
263
267
|
*
|
|
264
268
|
* @remarks
|
|
@@ -287,7 +291,7 @@ declare class TmpFileUploadHandlerPlugin<T extends Context> implements StandardH
|
|
|
287
291
|
init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
|
|
288
292
|
private resolveBody;
|
|
289
293
|
/**
|
|
290
|
-
* Enforces
|
|
294
|
+
* Enforces size limits on a body the standard parser handles, counting the
|
|
291
295
|
* raw bytes before handing them back for regular parsing.
|
|
292
296
|
*/
|
|
293
297
|
private parseLimitedBody;
|
package/dist/index.d.ts
CHANGED
|
@@ -83,6 +83,8 @@ interface StaticFileHandlerPluginOptions {
|
|
|
83
83
|
cacheControl?: string | false;
|
|
84
84
|
/**
|
|
85
85
|
* Whether files and directories whose name starts with a dot can be served.
|
|
86
|
+
* When disabled, names shaped like 8.3 short names, such as `ENV~1`, are refused too,
|
|
87
|
+
* since NTFS and FAT resolve them to dotfiles.
|
|
86
88
|
*
|
|
87
89
|
* @default false
|
|
88
90
|
*/
|
|
@@ -175,10 +177,11 @@ declare class StaticFileHandlerPlugin<T extends Context> implements StandardHand
|
|
|
175
177
|
|
|
176
178
|
interface TmpFileUploadHandlerPluginMaxBodySize {
|
|
177
179
|
/**
|
|
178
|
-
* The maximum
|
|
179
|
-
*
|
|
180
|
-
* body
|
|
181
|
-
* the lowest of the three limits,
|
|
180
|
+
* The maximum size in bytes of request body content buffered into memory:
|
|
181
|
+
* JSON, URL-encoded forms, and the fields and part headers of a multipart
|
|
182
|
+
* body in total, and each event of an event stream on its own. Exceeding it
|
|
183
|
+
* rejects with `PAYLOAD_TOO_LARGE`. Usually the lowest of the three limits,
|
|
184
|
+
* because this content cannot stream anywhere.
|
|
182
185
|
*/
|
|
183
186
|
memory: number;
|
|
184
187
|
/**
|
|
@@ -191,7 +194,8 @@ interface TmpFileUploadHandlerPluginMaxBodySize {
|
|
|
191
194
|
* The maximum total size in bytes of a request body that is consumed as a
|
|
192
195
|
* stream: event streams and raw binary streams. Enforced while the stream is
|
|
193
196
|
* consumed, so an oversized stream fails at the reader. Usually the highest
|
|
194
|
-
* of the three limits,
|
|
197
|
+
* of the three limits, but keep it finite so no client can stream
|
|
198
|
+
* indefinitely.
|
|
195
199
|
*/
|
|
196
200
|
stream: number;
|
|
197
201
|
}
|
|
@@ -257,8 +261,8 @@ declare class TmpFile extends File {
|
|
|
257
261
|
* far larger than available memory parse in constant memory. Every other body is left
|
|
258
262
|
* to the standard parser.
|
|
259
263
|
*
|
|
260
|
-
* Request body sizes are limited per content category: memory
|
|
261
|
-
* disk, and streamed, each limit fixed or resolved per request. This subsumes the
|
|
264
|
+
* Request body sizes are limited per content category: buffered in memory, spooled
|
|
265
|
+
* to disk, and streamed, each limit fixed or resolved per request. This subsumes the
|
|
262
266
|
* request limit plugin while sizing each kind of body to what it actually costs.
|
|
263
267
|
*
|
|
264
268
|
* @remarks
|
|
@@ -287,7 +291,7 @@ declare class TmpFileUploadHandlerPlugin<T extends Context> implements StandardH
|
|
|
287
291
|
init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
|
|
288
292
|
private resolveBody;
|
|
289
293
|
/**
|
|
290
|
-
* Enforces
|
|
294
|
+
* Enforces size limits on a body the standard parser handles, counting the
|
|
291
295
|
* raw bytes before handing them back for regular parsing.
|
|
292
296
|
*/
|
|
293
297
|
private parseLimitedBody;
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Duplex } from 'node:stream';
|
|
2
2
|
import { constants, createGzip, createDeflate, createDeflateRaw } from 'node:zlib';
|
|
3
|
-
import {
|
|
3
|
+
import { negotiateResponseCompressionEncoding, toCompressedHeaders } from '@orpc/server/plugins';
|
|
4
|
+
import { toArray, stringifyJSON, mergeHttpPath, safeDecodeURIComponent, matchesHttpPathPrefix, safeEncodeURIComponent, isCompressibleContentType, parseAcceptEncodingQualities, isAcceptableEncoding, getTracer, value, isAsyncIteratorObject, override, wrapAsyncIterator, wrapReadableStream } from '@orpc/shared';
|
|
4
5
|
import { flattenStandardHeader, parseStandardUrl, resolveStandardBodyHint, getFilenameFromContentDisposition } from '@standard-server/core';
|
|
5
6
|
import { createReadStream, openAsBlob } from 'node:fs';
|
|
6
7
|
import { realpath, stat, mkdtemp, appendFile, rm } from 'node:fs/promises';
|
|
@@ -40,19 +41,7 @@ class BatchResponseCompressionHandlerPlugin {
|
|
|
40
41
|
if (response.status >= 400) {
|
|
41
42
|
return result;
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
-
return result;
|
|
45
|
-
}
|
|
46
|
-
if (response.status === 206 || flattenStandardHeader(headers["content-range"]) !== void 0) {
|
|
47
|
-
return result;
|
|
48
|
-
}
|
|
49
|
-
if (isNoTransformCacheControl(flattenStandardHeader(headers["cache-control"]))) {
|
|
50
|
-
return result;
|
|
51
|
-
}
|
|
52
|
-
const acceptEncodings = parseAcceptEncodingQualities(
|
|
53
|
-
flattenStandardHeader(interceptorOptions.request.headers["accept-encoding"])
|
|
54
|
-
);
|
|
55
|
-
const encoding = this.encodings.find((enc) => (acceptEncodings.get(enc) ?? 0) > 0);
|
|
44
|
+
const encoding = negotiateResponseCompressionEncoding(response, interceptorOptions.request.headers, this.encodings);
|
|
56
45
|
if (encoding === void 0) {
|
|
57
46
|
return result;
|
|
58
47
|
}
|
|
@@ -95,11 +84,8 @@ class BatchResponseCompressionHandlerPlugin {
|
|
|
95
84
|
...response,
|
|
96
85
|
body: stream.pipeThrough(createFlushingCompressionStream(encoding)),
|
|
97
86
|
headers: {
|
|
98
|
-
...headers,
|
|
99
|
-
...bodyHeaders
|
|
100
|
-
"content-length": [],
|
|
101
|
-
"content-encoding": encoding,
|
|
102
|
-
"vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary))
|
|
87
|
+
...toCompressedHeaders(headers, encoding),
|
|
88
|
+
...bodyHeaders
|
|
103
89
|
}
|
|
104
90
|
}
|
|
105
91
|
};
|
|
@@ -120,6 +106,7 @@ function createFlushingCompressionStream(encoding) {
|
|
|
120
106
|
}
|
|
121
107
|
|
|
122
108
|
const TEXT_CONTENT_TYPE_REGEX = /^text\//;
|
|
109
|
+
const SHORT_NAME_ALIAS_REGEX = /^[^.]{0,6}~\d/;
|
|
123
110
|
const PRECOMPRESSED_ENCODINGS = [
|
|
124
111
|
["br", ".br"],
|
|
125
112
|
["zstd", ".zst"],
|
|
@@ -258,7 +245,7 @@ class StaticFileHandlerPlugin {
|
|
|
258
245
|
if (segment.includes("\0") || segment.includes("/") || segment.includes("\\")) {
|
|
259
246
|
return void 0;
|
|
260
247
|
}
|
|
261
|
-
if (!this.dotfiles && segment.startsWith(".")) {
|
|
248
|
+
if (!this.dotfiles && (segment.startsWith(".") || SHORT_NAME_ALIAS_REGEX.test(segment))) {
|
|
262
249
|
return void 0;
|
|
263
250
|
}
|
|
264
251
|
segments.push(segment);
|
|
@@ -309,7 +296,7 @@ class StaticFileHandlerPlugin {
|
|
|
309
296
|
let contentEncoding;
|
|
310
297
|
if (negotiatesEncoding) {
|
|
311
298
|
const qualities = parseAcceptEncodingQualities(flattenStandardHeader(request.headers["accept-encoding"]));
|
|
312
|
-
const variants = await Promise.all(PRECOMPRESSED_ENCODINGS.filter(([encoding]) => (qualities
|
|
299
|
+
const variants = await Promise.all(PRECOMPRESSED_ENCODINGS.filter(([encoding]) => isAcceptableEncoding(qualities, encoding)).map(async ([encoding, extension]) => {
|
|
313
300
|
const candidatePath = filePath + extension;
|
|
314
301
|
const [candidateStats, containedRealPath] = await Promise.all([
|
|
315
302
|
stat(candidatePath).catch(() => void 0),
|
|
@@ -482,32 +469,39 @@ async function parseMultipart(source, boundary, onPart) {
|
|
|
482
469
|
buffer = Buffer.alloc(0);
|
|
483
470
|
return;
|
|
484
471
|
}
|
|
472
|
+
state = "transport-padding";
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (state === "transport-padding") {
|
|
485
476
|
let index = 0;
|
|
486
477
|
while (index < buffer.length && (buffer[index] === 32 || buffer[index] === 9)) {
|
|
487
478
|
index++;
|
|
488
479
|
}
|
|
489
|
-
|
|
480
|
+
buffer = buffer.subarray(index);
|
|
481
|
+
if (buffer.length < 2) {
|
|
490
482
|
return;
|
|
491
483
|
}
|
|
492
|
-
if (buffer[
|
|
484
|
+
if (buffer[0] !== 13 || buffer[1] !== 10) {
|
|
493
485
|
throw new TypeError("Invalid multipart body: expected CRLF after a boundary delimiter");
|
|
494
486
|
}
|
|
495
|
-
buffer = buffer.subarray(
|
|
487
|
+
buffer = buffer.subarray(2);
|
|
496
488
|
state = "headers";
|
|
497
489
|
continue;
|
|
498
490
|
}
|
|
499
491
|
if (buffer[0] === 13 && buffer[1] === 10) {
|
|
500
492
|
throw new TypeError("Invalid multipart body: part is missing the content-disposition header");
|
|
501
493
|
}
|
|
502
|
-
const
|
|
494
|
+
const headerWindow = MAX_PART_HEADER_SIZE + HEADER_BLOCK_END.length;
|
|
495
|
+
const headerEnd = buffer.subarray(0, headerWindow).indexOf(HEADER_BLOCK_END);
|
|
503
496
|
if (headerEnd === -1) {
|
|
504
|
-
if (buffer.length
|
|
497
|
+
if (buffer.length >= headerWindow) {
|
|
505
498
|
throw new TypeError("Invalid multipart body: part headers exceed the maximum allowed size");
|
|
506
499
|
}
|
|
507
500
|
return;
|
|
508
501
|
}
|
|
509
|
-
const
|
|
510
|
-
|
|
502
|
+
const headerSize = headerEnd + HEADER_BLOCK_END.length;
|
|
503
|
+
const part = parsePartHeaders(buffer.subarray(0, headerEnd).toString(), headerSize);
|
|
504
|
+
buffer = buffer.subarray(headerSize);
|
|
511
505
|
writer = await onPart(part);
|
|
512
506
|
state = "body";
|
|
513
507
|
}
|
|
@@ -523,7 +517,7 @@ async function parseMultipart(source, boundary, onPart) {
|
|
|
523
517
|
throw new TypeError("Invalid multipart body: unexpected end of body");
|
|
524
518
|
}
|
|
525
519
|
}
|
|
526
|
-
function parsePartHeaders(block) {
|
|
520
|
+
function parsePartHeaders(block, headerSize) {
|
|
527
521
|
let contentDisposition;
|
|
528
522
|
let contentType;
|
|
529
523
|
for (const line of block.split("\r\n")) {
|
|
@@ -555,7 +549,8 @@ function parsePartHeaders(block) {
|
|
|
555
549
|
return {
|
|
556
550
|
name: decodeContentDispositionParameter(name),
|
|
557
551
|
filename: filename === void 0 ? void 0 : decodeContentDispositionParameter(filename),
|
|
558
|
-
type: contentType
|
|
552
|
+
type: contentType,
|
|
553
|
+
headerSize
|
|
559
554
|
};
|
|
560
555
|
}
|
|
561
556
|
function decodeContentDispositionParameter(value) {
|
|
@@ -569,13 +564,15 @@ function parseHeaderParameters(header) {
|
|
|
569
564
|
while (index < header.length && (header[index] === " " || header[index] === " ")) {
|
|
570
565
|
index++;
|
|
571
566
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
567
|
+
let equals = index;
|
|
568
|
+
while (equals < header.length && header[equals] !== "=" && header[equals] !== ";") {
|
|
569
|
+
equals++;
|
|
570
|
+
}
|
|
571
|
+
if (equals === header.length) {
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
if (header[equals] === ";") {
|
|
575
|
+
index = equals;
|
|
579
576
|
continue;
|
|
580
577
|
}
|
|
581
578
|
const key = header.slice(index, equals).trim().toLowerCase();
|
|
@@ -695,14 +692,15 @@ class TmpFileUploadHandlerPlugin {
|
|
|
695
692
|
if (resolvedHint === "json" || resolvedHint === "url-search-params") {
|
|
696
693
|
return this.parseLimitedBody(request, hint, resolvedHint, maxBodySize.memory);
|
|
697
694
|
}
|
|
698
|
-
|
|
695
|
+
const eventLimit = resolvedHint === "event-stream" ? maxBodySize.memory : Number.POSITIVE_INFINITY;
|
|
696
|
+
return this.parseLimitedBody(request, hint, resolvedHint, maxBodySize.stream, eventLimit);
|
|
699
697
|
}
|
|
700
698
|
/**
|
|
701
|
-
* Enforces
|
|
699
|
+
* Enforces size limits on a body the standard parser handles, counting the
|
|
702
700
|
* raw bytes before handing them back for regular parsing.
|
|
703
701
|
*/
|
|
704
|
-
async parseLimitedBody(request, hint, resolvedHint, limit) {
|
|
705
|
-
if (limit === Number.POSITIVE_INFINITY) {
|
|
702
|
+
async parseLimitedBody(request, hint, resolvedHint, limit, eventLimit = Number.POSITIVE_INFINITY) {
|
|
703
|
+
if (limit === Number.POSITIVE_INFINITY && eventLimit === Number.POSITIVE_INFINITY) {
|
|
706
704
|
return request.resolveBody(hint);
|
|
707
705
|
}
|
|
708
706
|
assertContentLengthWithin(request.headers, limit);
|
|
@@ -710,7 +708,8 @@ class TmpFileUploadHandlerPlugin {
|
|
|
710
708
|
if (!(stream instanceof ReadableStream)) {
|
|
711
709
|
return stream;
|
|
712
710
|
}
|
|
713
|
-
const
|
|
711
|
+
const fits = eventLimit < limit ? createEventSizeCheck(eventLimit) : void 0;
|
|
712
|
+
const response = new Response(limitStream(stream, limit, fits), {
|
|
714
713
|
headers: toFetchHeaders(request.headers)
|
|
715
714
|
});
|
|
716
715
|
return toStandardBody(response, { hint: resolvedHint });
|
|
@@ -745,18 +744,21 @@ class TmpFileUploadHandlerPlugin {
|
|
|
745
744
|
}
|
|
746
745
|
const limited = totalLimit === Number.POSITIVE_INFINITY ? stream : limitStream(stream, totalLimit);
|
|
747
746
|
const form = new FormData();
|
|
748
|
-
|
|
749
|
-
|
|
747
|
+
const used = { memory: 0, file: 0 };
|
|
748
|
+
const charge = (category, size) => {
|
|
749
|
+
used[category] += size;
|
|
750
|
+
if (used[category] > maxBodySize[category]) {
|
|
751
|
+
throw new ORPCError("PAYLOAD_TOO_LARGE");
|
|
752
|
+
}
|
|
753
|
+
};
|
|
750
754
|
await parseMultipart(limited, boundary, (part) => {
|
|
751
755
|
const name = part.name;
|
|
756
|
+
charge("memory", part.headerSize);
|
|
752
757
|
if (part.filename === void 0) {
|
|
753
758
|
const chunks = [];
|
|
754
759
|
return {
|
|
755
760
|
write: (chunk) => {
|
|
756
|
-
|
|
757
|
-
if (memoryUsed > maxBodySize.memory) {
|
|
758
|
-
throw new ORPCError("PAYLOAD_TOO_LARGE");
|
|
759
|
-
}
|
|
761
|
+
charge("memory", chunk.length);
|
|
760
762
|
chunks.push(Buffer.from(chunk));
|
|
761
763
|
},
|
|
762
764
|
end: () => {
|
|
@@ -769,10 +771,7 @@ class TmpFileUploadHandlerPlugin {
|
|
|
769
771
|
let tmpPath;
|
|
770
772
|
return {
|
|
771
773
|
write: async (chunk) => {
|
|
772
|
-
|
|
773
|
-
if (fileUsed > maxBodySize.file) {
|
|
774
|
-
throw new ORPCError("PAYLOAD_TOO_LARGE");
|
|
775
|
-
}
|
|
774
|
+
charge("file", chunk.length);
|
|
776
775
|
tmpPath ??= await tmpFiles.allocate();
|
|
777
776
|
await tmpFiles.append(tmpPath, chunk);
|
|
778
777
|
},
|
|
@@ -791,12 +790,12 @@ function assertContentLengthWithin(headers, limit) {
|
|
|
791
790
|
throw new ORPCError("PAYLOAD_TOO_LARGE");
|
|
792
791
|
}
|
|
793
792
|
}
|
|
794
|
-
function limitStream(stream, limit) {
|
|
793
|
+
function limitStream(stream, limit, fits = () => true) {
|
|
795
794
|
let total = 0;
|
|
796
795
|
return stream.pipeThrough(new TransformStream({
|
|
797
796
|
transform(chunk, controller) {
|
|
798
797
|
total += chunk.byteLength;
|
|
799
|
-
if (total > limit) {
|
|
798
|
+
if (total > limit || !fits(chunk)) {
|
|
800
799
|
controller.error(new ORPCError("PAYLOAD_TOO_LARGE"));
|
|
801
800
|
return;
|
|
802
801
|
}
|
|
@@ -804,6 +803,51 @@ function limitStream(stream, limit) {
|
|
|
804
803
|
}
|
|
805
804
|
}));
|
|
806
805
|
}
|
|
806
|
+
const CR = 13;
|
|
807
|
+
const LF = 10;
|
|
808
|
+
function createEventSizeCheck(eventLimit) {
|
|
809
|
+
let eventSize = 0;
|
|
810
|
+
let trailing = "none";
|
|
811
|
+
return (chunk) => {
|
|
812
|
+
const bytes = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
813
|
+
let nextCR = indexOrEnd(bytes, CR, 0);
|
|
814
|
+
let nextLF = indexOrEnd(bytes, LF, 0);
|
|
815
|
+
let offset = 0;
|
|
816
|
+
while (true) {
|
|
817
|
+
const lineEnding = Math.min(nextCR, nextLF);
|
|
818
|
+
if (lineEnding > offset) {
|
|
819
|
+
eventSize += lineEnding - offset;
|
|
820
|
+
trailing = "none";
|
|
821
|
+
}
|
|
822
|
+
if (lineEnding === bytes.length) {
|
|
823
|
+
return eventSize <= eventLimit;
|
|
824
|
+
}
|
|
825
|
+
const byte = bytes[lineEnding];
|
|
826
|
+
offset = lineEnding + 1;
|
|
827
|
+
if (byte === CR) {
|
|
828
|
+
nextCR = indexOrEnd(bytes, CR, offset);
|
|
829
|
+
} else {
|
|
830
|
+
nextLF = indexOrEnd(bytes, LF, offset);
|
|
831
|
+
}
|
|
832
|
+
if (eventSize === 0) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
if (++eventSize > eventLimit) {
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
if (trailing === "line" || trailing === "cr" && byte === CR) {
|
|
839
|
+
eventSize = 0;
|
|
840
|
+
trailing = "none";
|
|
841
|
+
} else {
|
|
842
|
+
trailing = byte === CR ? "cr" : "line";
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function indexOrEnd(bytes, byte, from) {
|
|
848
|
+
const index = bytes.indexOf(byte, from);
|
|
849
|
+
return index === -1 ? bytes.length : index;
|
|
850
|
+
}
|
|
807
851
|
const EMPTY_CHUNK = new Uint8Array(0);
|
|
808
852
|
class RequestTmpFiles {
|
|
809
853
|
constructor(tmpDir) {
|
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.
|
|
4
|
+
"version": "2.0.0-beta.41",
|
|
5
5
|
"description": "Node.js plugins for oRPC: static file serving, temporary file uploads, and batch response compression",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"funding": [
|
|
@@ -36,15 +36,15 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@orpc/server": "2.0.0-beta.
|
|
40
|
-
"@orpc/shared": "2.0.0-beta.
|
|
41
|
-
"@standard-server/core": "~0.
|
|
42
|
-
"@standard-server/fetch": "~0.
|
|
43
|
-
"@standard-server/node": "~0.
|
|
39
|
+
"@orpc/server": "2.0.0-beta.41",
|
|
40
|
+
"@orpc/shared": "2.0.0-beta.41",
|
|
41
|
+
"@standard-server/core": "~0.10.0",
|
|
42
|
+
"@standard-server/fetch": "~0.10.0",
|
|
43
|
+
"@standard-server/node": "~0.10.0",
|
|
44
44
|
"mime": "^4.1.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@orpc/client": "2.0.0-beta.
|
|
47
|
+
"@orpc/client": "2.0.0-beta.41",
|
|
48
48
|
"supertest": "^7.2.2"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|