@orpc/server 2.0.0-beta.26 → 2.0.0-beta.28

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.
Files changed (41) hide show
  1. package/README.md +38 -76
  2. package/dist/adapters/aws-lambda/index.d.mts +4 -4
  3. package/dist/adapters/aws-lambda/index.d.ts +4 -4
  4. package/dist/adapters/aws-lambda/index.mjs +3 -3
  5. package/dist/adapters/crossws/index.d.mts +4 -4
  6. package/dist/adapters/crossws/index.d.ts +4 -4
  7. package/dist/adapters/crossws/index.mjs +3 -3
  8. package/dist/adapters/fastify/index.d.mts +4 -4
  9. package/dist/adapters/fastify/index.d.ts +4 -4
  10. package/dist/adapters/fastify/index.mjs +3 -3
  11. package/dist/adapters/fetch/index.d.mts +4 -4
  12. package/dist/adapters/fetch/index.d.ts +4 -4
  13. package/dist/adapters/fetch/index.mjs +3 -3
  14. package/dist/adapters/message-port/index.d.mts +4 -4
  15. package/dist/adapters/message-port/index.d.ts +4 -4
  16. package/dist/adapters/message-port/index.mjs +3 -3
  17. package/dist/adapters/node/index.d.mts +4 -4
  18. package/dist/adapters/node/index.d.ts +4 -4
  19. package/dist/adapters/node/index.mjs +3 -3
  20. package/dist/adapters/standard/index.d.mts +2 -2
  21. package/dist/adapters/standard/index.d.ts +2 -2
  22. package/dist/adapters/standard/index.mjs +3 -3
  23. package/dist/adapters/standard-peer/index.d.mts +2 -2
  24. package/dist/adapters/standard-peer/index.d.ts +2 -2
  25. package/dist/adapters/websocket/index.d.mts +4 -4
  26. package/dist/adapters/websocket/index.d.ts +4 -4
  27. package/dist/adapters/websocket/index.mjs +3 -3
  28. package/dist/extensions/callable.d.mts +1 -1
  29. package/dist/extensions/callable.d.ts +1 -1
  30. package/dist/extensions/callable.mjs +2 -2
  31. package/dist/index.d.mts +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.mjs +5 -5
  34. package/dist/plugins/index.d.mts +57 -17
  35. package/dist/plugins/index.d.ts +57 -17
  36. package/dist/plugins/index.mjs +114 -43
  37. package/dist/shared/{server.7T18Khvf.mjs → server.CkButhNT.mjs} +7 -2
  38. package/dist/shared/{server.Cbv46U7N.mjs → server.D9VprDph.mjs} +2 -2
  39. package/dist/shared/{server.DgunZU0v.mjs → server.Dh77P3ii.mjs} +1 -1
  40. package/dist/shared/{server.B2pXdfpL.mjs → server.eCTV8Vpp.mjs} +1 -1
  41. package/package.json +11 -11
@@ -1,9 +1,9 @@
1
- import { toArray, value, isCompressibleContentType, isAsyncIteratorObject, stringifyJSON } from '@orpc/shared';
1
+ import { toArray, value, parseAcceptEncodingQualities, isCompressibleContentType, isAsyncIteratorObject, stringifyJSON, anyAbortSignal, override, wrapAsyncIterator, wrapReadableStream, AbortError } from '@orpc/shared';
2
2
  import { flattenStandardHeader, parseStandardUrl, generateContentDisposition, mergeStandardHeaders } from '@standardserver/core';
3
3
  import { isClientPeerSendMessage, ServerPeer, encodePeerMessage } from '@standardserver/peer';
4
4
  import { toFetchHeaders, toStandardBody, toStandardHeaders } from '@standardserver/fetch';
5
5
  export { R as RequestLimitHandlerPlugin } from '../shared/server.Cd4Z1hpV.mjs';
6
- import { ORPCError } from '@orpc/client';
6
+ import '@orpc/client';
7
7
 
8
8
  const BATCH_CONTENT_TYPE = "application/vnd.orpc.batch";
9
9
  class BatchHandlerPlugin {
@@ -61,16 +61,10 @@ class BatchHandlerPlugin {
61
61
  response: { status: 400, headers: {}, body: "Invalid batch request data parameter" }
62
62
  };
63
63
  }
64
- if (mightBeMessages.some((m) => m.kind === "request" && m.json.method !== "GET")) {
65
- return {
66
- matched: true,
67
- response: { status: 400, headers: {}, body: "GET batch requests only accept GET sub-requests" }
68
- };
69
- }
70
64
  messages = mightBeMessages;
71
65
  } else {
72
66
  const mightBeMessages = await interceptorOptions.request.resolveBody();
73
- if (!Array.isArray(mightBeMessages)) {
67
+ if (!Array.isArray(mightBeMessages) || mightBeMessages.some((m) => !isClientPeerSendMessage(m))) {
74
68
  return {
75
69
  matched: true,
76
70
  response: { status: 400, headers: {}, body: "Invalid batch request body" }
@@ -84,6 +78,13 @@ class BatchHandlerPlugin {
84
78
  response: { status: 400, headers: {}, body: "Invalid batch request" }
85
79
  };
86
80
  }
81
+ const outerMethod = interceptorOptions.request.method;
82
+ if ((outerMethod === "GET" || outerMethod === "QUERY") && messages.some((message) => message.kind === "request" && message.json.method !== outerMethod)) {
83
+ return {
84
+ matched: true,
85
+ response: { status: 400, headers: {}, body: `${outerMethod} batch requests only accept ${outerMethod} sub-requests` }
86
+ };
87
+ }
87
88
  const maxSize = await value(this.maxSize, interceptorOptions);
88
89
  if (messages.length > maxSize) {
89
90
  return {
@@ -215,7 +216,7 @@ class CORSHandlerPlugin {
215
216
  constructor(options = {}) {
216
217
  const defaults = {
217
218
  origin: (origin) => origin,
218
- allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"]
219
+ allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH", "QUERY"]
219
220
  };
220
221
  this.options = {
221
222
  ...defaults,
@@ -229,12 +230,12 @@ class CORSHandlerPlugin {
229
230
  return result;
230
231
  }
231
232
  const resHeaders = { ...result.response.headers };
232
- const origin = flattenStandardHeader(interceptorOptions.request.headers.origin) ?? "";
233
- const allowedOrigins = toArray(await value(this.options.origin, origin, interceptorOptions));
233
+ const origin = flattenStandardHeader(interceptorOptions.request.headers.origin);
234
+ const allowedOrigins = toArray(value(this.options.origin, origin, interceptorOptions));
234
235
  if (allowedOrigins.includes("*")) {
235
236
  resHeaders["access-control-allow-origin"] = "*";
236
237
  } else {
237
- if (allowedOrigins.includes(origin)) {
238
+ if (origin !== void 0 && allowedOrigins.includes(origin)) {
238
239
  resHeaders["access-control-allow-origin"] = origin;
239
240
  }
240
241
  const existingVary = flattenStandardHeader(resHeaders.vary);
@@ -242,10 +243,10 @@ class CORSHandlerPlugin {
242
243
  resHeaders.vary = existingVary ? `${existingVary}, Origin` : "Origin";
243
244
  }
244
245
  }
245
- const allowedTimingOrigins = toArray(await value(this.options.timingOrigin, origin, interceptorOptions));
246
+ const allowedTimingOrigins = toArray(value(this.options.timingOrigin, origin, interceptorOptions));
246
247
  if (allowedTimingOrigins.includes("*")) {
247
248
  resHeaders["timing-allow-origin"] = "*";
248
- } else if (allowedTimingOrigins.includes(origin)) {
249
+ } else if (origin !== void 0 && allowedTimingOrigins.includes(origin)) {
249
250
  resHeaders["timing-allow-origin"] = origin;
250
251
  }
251
252
  if (this.options.credentials) {
@@ -291,6 +292,42 @@ class CORSHandlerPlugin {
291
292
  }
292
293
  }
293
294
 
295
+ class GetMethodCsrfProtectionHandlerPlugin {
296
+ name = "~get-method-csrf-protection";
297
+ /** Judge the real request, before batch splits it into client-authored sub-requests. */
298
+ after = ["~batch"];
299
+ init(options) {
300
+ const routingInterceptor = (interceptorOptions) => {
301
+ if (this.isAllowed(interceptorOptions)) {
302
+ return interceptorOptions.next();
303
+ }
304
+ return Promise.resolve({
305
+ matched: true,
306
+ response: { status: 403, headers: {}, body: "Request blocked by CSRF protection." }
307
+ });
308
+ };
309
+ return {
310
+ ...options,
311
+ routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
312
+ };
313
+ }
314
+ isAllowed({ request }) {
315
+ if (request.method !== "GET") {
316
+ return true;
317
+ }
318
+ const site = flattenStandardHeader(request.headers["sec-fetch-site"])?.toLowerCase();
319
+ if (site === void 0 || site === "same-origin" || site === "same-site") {
320
+ return true;
321
+ }
322
+ const mode = flattenStandardHeader(request.headers["sec-fetch-mode"])?.toLowerCase();
323
+ const dest = flattenStandardHeader(request.headers["sec-fetch-dest"])?.toLowerCase();
324
+ if (mode === void 0 || dest === void 0) {
325
+ return false;
326
+ }
327
+ return mode !== "navigate" || dest !== "document";
328
+ }
329
+ }
330
+
294
331
  class MethodOverrideHandlerPlugin {
295
332
  name = "~method-override";
296
333
  /**
@@ -443,13 +480,16 @@ class ResponseCompressionHandlerPlugin {
443
480
  if (contentEncoding !== void 0) {
444
481
  return result;
445
482
  }
483
+ if (response.status === 206 || response.headers["content-range"] !== void 0) {
484
+ return result;
485
+ }
446
486
  if (isNoTransformCacheControl(flattenStandardHeader(response.headers["cache-control"]))) {
447
487
  return result;
448
488
  }
449
- const acceptEncodings = parseAcceptEncodings(
489
+ const acceptEncodings = parseAcceptEncodingQualities(
450
490
  flattenStandardHeader(interceptorOptions.request.headers["accept-encoding"])
451
491
  );
452
- const encoding = this.encodings.find((enc) => acceptEncodings.includes(enc));
492
+ const encoding = this.encodings.find((enc) => (acceptEncodings.get(enc) ?? 0) > 0);
453
493
  if (encoding === void 0) {
454
494
  return result;
455
495
  }
@@ -467,7 +507,8 @@ class ResponseCompressionHandlerPlugin {
467
507
  ...headers,
468
508
  "standard-server": "octet-stream",
469
509
  "content-length": [],
470
- "content-encoding": encoding
510
+ "content-encoding": encoding,
511
+ "vary": varyByAcceptEncoding(headers.vary)
471
512
  }
472
513
  }
473
514
  };
@@ -488,7 +529,8 @@ class ResponseCompressionHandlerPlugin {
488
529
  "content-type": body.type,
489
530
  "content-length": [],
490
531
  "content-disposition": contentDisposition,
491
- "content-encoding": encoding
532
+ "content-encoding": encoding,
533
+ "vary": varyByAcceptEncoding(headers.vary)
492
534
  }
493
535
  }
494
536
  };
@@ -525,7 +567,8 @@ class ResponseCompressionHandlerPlugin {
525
567
  "standard-server": [],
526
568
  "content-type": res.headers.get("content-type"),
527
569
  "content-length": [],
528
- "content-encoding": encoding
570
+ "content-encoding": encoding,
571
+ "vary": varyByAcceptEncoding(headers.vary)
529
572
  }
530
573
  }
531
574
  };
@@ -543,7 +586,8 @@ class ResponseCompressionHandlerPlugin {
543
586
  "standard-server": [],
544
587
  "content-type": "application/x-www-form-urlencoded",
545
588
  "content-length": [],
546
- "content-encoding": encoding
589
+ "content-encoding": encoding,
590
+ "vary": varyByAcceptEncoding(headers.vary)
547
591
  }
548
592
  }
549
593
  };
@@ -561,7 +605,8 @@ class ResponseCompressionHandlerPlugin {
561
605
  "standard-server": [],
562
606
  "content-type": "application/json",
563
607
  "content-length": [],
564
- "content-encoding": encoding
608
+ "content-encoding": encoding,
609
+ "vary": varyByAcceptEncoding(headers.vary)
565
610
  }
566
611
  }
567
612
  };
@@ -578,11 +623,13 @@ class ResponseCompressionHandlerPlugin {
578
623
  };
579
624
  }
580
625
  }
581
- function parseAcceptEncodings(header) {
582
- if (header === void 0) {
583
- return [];
626
+ function varyByAcceptEncoding(vary) {
627
+ const current = flattenStandardHeader(vary);
628
+ if (current === void 0) {
629
+ return "accept-encoding";
584
630
  }
585
- return header.split(",").map((part) => part.trim().split(";")[0].trim().toLowerCase()).filter(Boolean);
631
+ const fields = current.split(",").map((field) => field.trim().toLowerCase());
632
+ return fields.includes("accept-encoding") || fields.includes("*") ? current : `${current}, accept-encoding`;
586
633
  }
587
634
  const CACHE_CONTROL_NO_TRANSFORM_REGEX = /(?:^|,)\s*no-transform\s*(?:,|$)/i;
588
635
  function isNoTransformCacheControl(cacheControl) {
@@ -680,29 +727,53 @@ class RethrowHandlerPlugin {
680
727
  }
681
728
  }
682
729
 
683
- class SimpleCsrfProtectionHandlerPlugin {
684
- name = "~simple-csrf-protection";
730
+ class TimeoutHandlerPlugin {
731
+ timeout;
732
+ streamingTimeout;
733
+ name = "~timeout";
734
+ constructor(options) {
735
+ this.timeout = options.timeout;
736
+ this.streamingTimeout = options.streamingTimeout;
737
+ }
685
738
  init(options) {
686
739
  const interceptor = async (interceptorOptions) => {
687
- const mode = flattenStandardHeader(
688
- interceptorOptions.request.headers["sec-fetch-mode"]
689
- )?.toLowerCase();
690
- if (mode === void 0) {
740
+ const timeoutMs = value(this.timeout, interceptorOptions);
741
+ const streamingTimeoutMs = value(this.streamingTimeout, interceptorOptions);
742
+ const hasTimeout = timeoutMs !== null && timeoutMs !== void 0;
743
+ const hasStreamingTimeout = streamingTimeoutMs !== null && streamingTimeoutMs !== void 0;
744
+ if (!hasTimeout && !hasStreamingTimeout) {
691
745
  return interceptorOptions.next();
692
746
  }
693
- if (mode === "cors" || mode === "same-origin") {
694
- return interceptorOptions.next();
747
+ const controller = new AbortController();
748
+ const abortWithTimeout = (ms) => {
749
+ controller.abort(new AbortError(`Request timed out after ${ms}ms`));
750
+ };
751
+ const timeoutId = hasTimeout ? setTimeout(abortWithTimeout, timeoutMs, timeoutMs) : void 0;
752
+ const signal = anyAbortSignal([interceptorOptions.request.signal, controller.signal]);
753
+ try {
754
+ const response = await interceptorOptions.next({
755
+ ...interceptorOptions,
756
+ request: { ...interceptorOptions.request, signal }
757
+ });
758
+ if (hasStreamingTimeout) {
759
+ const body = response.body;
760
+ const isIterator = isAsyncIteratorObject(body);
761
+ if (isIterator || body instanceof ReadableStream) {
762
+ const streamingTimeoutId = setTimeout(abortWithTimeout, streamingTimeoutMs, streamingTimeoutMs);
763
+ const onFinish = () => clearTimeout(streamingTimeoutId);
764
+ return {
765
+ ...response,
766
+ body: isIterator ? override(body, wrapAsyncIterator(body, { onFinish })) : override(body, wrapReadableStream(body, { onFinish }))
767
+ };
768
+ }
769
+ }
770
+ return response;
771
+ } finally {
772
+ clearTimeout(timeoutId);
695
773
  }
696
- throw new ORPCError("FORBIDDEN", {
697
- message: "Request blocked by CSRF protection."
698
- });
699
- };
700
- return {
701
- ...options,
702
- // appended last so user's interceptors can catch ORPCError
703
- interceptors: [...toArray(options.interceptors), interceptor]
704
774
  };
775
+ return { ...options, interceptors: [interceptor, ...toArray(options.interceptors)] };
705
776
  }
706
777
  }
707
778
 
708
- export { BATCH_CONTENT_TYPE, BatchHandlerPlugin, CORSHandlerPlugin, CORSHandlerPlugin as CORSPlugin, MethodOverrideHandlerPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestHeadersHandlerPlugin as RequestHeadersPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, ResponseHeadersHandlerPlugin as ResponseHeadersPlugin, RethrowHandlerPlugin, SimpleCsrfProtectionHandlerPlugin };
779
+ export { BATCH_CONTENT_TYPE, BatchHandlerPlugin, CORSHandlerPlugin, CORSHandlerPlugin as CORSPlugin, GetMethodCsrfProtectionHandlerPlugin, MethodOverrideHandlerPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestHeadersHandlerPlugin as RequestHeadersPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, ResponseHeadersHandlerPlugin as ResponseHeadersPlugin, RethrowHandlerPlugin, TimeoutHandlerPlugin };
@@ -1,6 +1,6 @@
1
1
  import { wrapAsyncIteratorPreservingEventMeta, ORPCError, cloneORPCError } from '@orpc/client';
2
2
  import { createORPCErrorConstructorMap, reconcileORPCError, ValidationError, ProcedureContract } from '@orpc/contract';
3
- import { getConstructor, isTypescriptObject, resolveMaybeOptionalOptions, toArray, value, runWithSpan, intercept, isAsyncIteratorObject, override, traceAsyncIterator, traceReadableStream } from '@orpc/shared';
3
+ import { getConstructor, isTypescriptObject, resolveMaybeOptionalOptions, toArray, value, runWithSpan, intercept, isAsyncIteratorObject, override, traceAsyncIterator, traceReadableStream, isPlainObject, mergeTwoLevels } from '@orpc/shared';
4
4
 
5
5
  class Lazy {
6
6
  "~orpc";
@@ -127,7 +127,12 @@ async function executeProcedureInternal(procedure, options) {
127
127
  const endInputIndex = midIndex === orderedMiddlewares.length ? inputSchemas.length : orderedMiddlewares[midIndex].inputSchemasLengthAtUse ?? 0;
128
128
  if (!procedure["~orpc"].disableInputValidation) {
129
129
  for (let i = startInputIndex; i < endInputIndex; i++) {
130
- currentInput = await validateInput(i, inputSchemas[i], currentInput);
130
+ const validated = await validateInput(
131
+ i,
132
+ inputSchemas[i],
133
+ inputSchemas.length > 1 && isPlainObject(currentInput) ? options.input : currentInput
134
+ );
135
+ currentInput = i !== 0 ? mergeTwoLevels(currentInput, validated) : validated;
131
136
  }
132
137
  }
133
138
  let currentOutput;
@@ -1,8 +1,8 @@
1
1
  import { ORPCError, toORPCError, RPCSerializer, COMMON_ERROR_STATUS_MAP } from '@orpc/client';
2
2
  import { sortPlugins, getOpenTelemetryConfig, runWithSpan, toArray, matchesHttpPathPrefix, intercept, ORPC_NAME, isAsyncIteratorObject, override, traceAsyncIterator, recordSpanError, pathToHttpPath, normalizeHttpPath, value, parseEmptyableJSON } from '@orpc/shared';
3
3
  import { flattenStandardHeader, parseStandardUrl } from '@standardserver/core';
4
- import { c as createProcedureClient, u as unlazy, P as Procedure } from './server.7T18Khvf.mjs';
5
- import { w as walkProcedureContractsSync, g as getRouter, c as createContractProcedure, D as DEFAULT_SUCCESS_STATUS, a as DEFAULT_ERROR_STATUS } from './server.DgunZU0v.mjs';
4
+ import { c as createProcedureClient, u as unlazy, P as Procedure } from './server.CkButhNT.mjs';
5
+ import { w as walkProcedureContractsSync, g as getRouter, c as createContractProcedure, D as DEFAULT_SUCCESS_STATUS, a as DEFAULT_ERROR_STATUS } from './server.Dh77P3ii.mjs';
6
6
 
7
7
  class CompositeStandardHandlerPlugin {
8
8
  name = "~composite";
@@ -1,4 +1,4 @@
1
- import { c as createProcedureClient, P as Procedure, L as Lazy, u as unlazy } from './server.7T18Khvf.mjs';
1
+ import { c as createProcedureClient, P as Procedure, L as Lazy, u as unlazy } from './server.CkButhNT.mjs';
2
2
  import { resolveMetaPlugins, mergeErrorMap, ProcedureContract } from '@orpc/contract';
3
3
  import { omit, isTypescriptObject } from '@orpc/shared';
4
4
 
@@ -1,5 +1,5 @@
1
1
  import { resolveMetaPlugins, mergeErrorMap, getHiddenMetaPlugins } from '@orpc/contract';
2
- import { P as Procedure } from './server.7T18Khvf.mjs';
2
+ import { P as Procedure } from './server.CkButhNT.mjs';
3
3
 
4
4
  class DecoratedProcedure extends Procedure {
5
5
  meta(...plugins) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@orpc/server",
3
3
  "type": "module",
4
- "version": "2.0.0-beta.26",
4
+ "version": "2.0.0-beta.28",
5
5
  "license": "MIT",
6
6
  "funding": "https://github.com/sponsors/dinwwwh",
7
7
  "homepage": "https://orpc.dev",
@@ -100,20 +100,20 @@
100
100
  }
101
101
  },
102
102
  "dependencies": {
103
- "@standardserver/aws-lambda": "^0.7.1",
104
- "@standardserver/core": "^0.7.1",
105
- "@standardserver/fastify": "^0.7.1",
106
- "@standardserver/fetch": "^0.7.1",
107
- "@standardserver/node": "^0.7.1",
108
- "@standardserver/peer": "^0.7.1",
103
+ "@standardserver/aws-lambda": "^0.8.0",
104
+ "@standardserver/core": "^0.8.0",
105
+ "@standardserver/fastify": "^0.8.0",
106
+ "@standardserver/fetch": "^0.8.0",
107
+ "@standardserver/node": "^0.8.0",
108
+ "@standardserver/peer": "^0.8.0",
109
109
  "cookie": "^2.0.1",
110
- "@orpc/client": "2.0.0-beta.26",
111
- "@orpc/contract": "2.0.0-beta.26",
112
- "@orpc/shared": "2.0.0-beta.26"
110
+ "@orpc/client": "2.0.0-beta.28",
111
+ "@orpc/shared": "2.0.0-beta.28",
112
+ "@orpc/contract": "2.0.0-beta.28"
113
113
  },
114
114
  "devDependencies": {
115
115
  "crossws": "^0.4.6",
116
- "fastify": "^5.11.0",
116
+ "fastify": "^5.11.3",
117
117
  "supertest": "^7.2.2",
118
118
  "zod": "^4.4.3"
119
119
  },