@orpc/node 2.0.0-beta.40 → 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 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&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>
88
+ <td width="2000"><a href="https://screenshotone.com/?ref=middleapi&amp;utm_source=middleapi&amp;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&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="We&#39;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&#39;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&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>
128
128
  <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>
129
129
  <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>
130
- <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>
131
130
  <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>
131
+ <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>
132
132
  </tr>
133
133
  <tr>
134
- <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>
135
134
  <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>
136
135
  <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>
137
136
  <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>
138
137
  <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>
139
138
  <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>
139
+ <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>
140
140
  </tr>
141
141
  <tr>
142
- <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>
143
142
  <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>
144
143
  <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>
145
144
  <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>
@@ -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&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>
174
173
  <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>
175
174
  <td align="center"><a href="https://paulsenon.com/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="PaulSenon"><img src="https://avatars.githubusercontent.com/u/19531087?u=6385741eb91d200b8c513ec6045482e301767ca6&amp;v=4" width="119" alt="PaulSenon"/><br />PaulSenon</a></td>
175
+ <td align="center"><a href="https://nakasyou.how/?ref=middleapi&amp;utm_source=middleapi&amp;utm_medium=sponsor" target="_blank" rel="noopener sponsored" title="Shotaro Nakamura"><img src="https://avatars.githubusercontent.com/u/79000684?u=f644df3f29f0e8677a90967115774564f1d9d6ab&amp;v=4" width="119" alt="Shotaro Nakamura"/><br />Shotaro Nakamura</a></td>
176
176
  <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>
177
177
  <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>
178
178
  </tr>
179
179
  </table>
180
180
 
181
- With thanks to [36 past sponsors](https://htmlpreview.github.io/?https://github.com/middleapi/static/blob/main/sponsors.svg) who helped get oRPC here.
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 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.
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, because this content is consumed on the fly.
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-parsed, spooled to
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 a size limit on a body the standard parser handles, counting the
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 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.
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, because this content is consumed on the fly.
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-parsed, spooled to
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 a size limit on a body the standard parser handles, counting the
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 { toArray, isNoTransformCacheControl, parseAcceptEncodingQualities, stringifyJSON, varyByAcceptEncoding, mergeHttpPath, safeDecodeURIComponent, matchesHttpPathPrefix, safeEncodeURIComponent, isCompressibleContentType, getTracer, value, isAsyncIteratorObject, override, wrapAsyncIterator, wrapReadableStream } from '@orpc/shared';
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
- if (flattenStandardHeader(headers["content-encoding"]) !== void 0) {
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.get(encoding) ?? qualities.get("*") ?? 0) > 0).map(async ([encoding, extension]) => {
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
- if (index + 1 >= buffer.length) {
480
+ buffer = buffer.subarray(index);
481
+ if (buffer.length < 2) {
490
482
  return;
491
483
  }
492
- if (buffer[index] !== 13 || buffer[index + 1] !== 10) {
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(index + 2);
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 headerEnd = buffer.indexOf(HEADER_BLOCK_END);
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 > MAX_PART_HEADER_SIZE) {
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 part = parsePartHeaders(buffer.subarray(0, headerEnd).toString());
510
- buffer = buffer.subarray(headerEnd + HEADER_BLOCK_END.length);
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
- const equals = header.indexOf("=", index);
573
- const nextSemicolon = header.indexOf(";", index);
574
- if (equals === -1 || nextSemicolon !== -1 && nextSemicolon < equals) {
575
- if (nextSemicolon === -1) {
576
- break;
577
- }
578
- index = nextSemicolon;
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
- return this.parseLimitedBody(request, hint, resolvedHint, maxBodySize.stream);
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 a size limit on a body the standard parser handles, counting the
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 response = new Response(limitStream(stream, limit), {
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
- let memoryUsed = 0;
749
- let fileUsed = 0;
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
- memoryUsed += chunk.length;
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
- fileUsed += chunk.length;
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.40",
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",
40
- "@orpc/shared": "2.0.0-beta.40",
41
- "@standard-server/core": "~0.9.2",
42
- "@standard-server/fetch": "~0.9.2",
43
- "@standard-server/node": "~0.9.2",
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.40",
47
+ "@orpc/client": "2.0.0-beta.41",
48
48
  "supertest": "^7.2.2"
49
49
  },
50
50
  "scripts": {