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

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 (35) hide show
  1. package/dist/adapters/aws-lambda/index.d.mts +1 -12
  2. package/dist/adapters/aws-lambda/index.d.ts +1 -12
  3. package/dist/adapters/aws-lambda/index.mjs +2 -9
  4. package/dist/adapters/crossws/index.d.mts +1 -1
  5. package/dist/adapters/crossws/index.d.ts +1 -1
  6. package/dist/adapters/crossws/index.mjs +1 -1
  7. package/dist/adapters/fastify/index.d.mts +1 -12
  8. package/dist/adapters/fastify/index.d.ts +1 -12
  9. package/dist/adapters/fastify/index.mjs +2 -9
  10. package/dist/adapters/fetch/index.d.mts +1 -12
  11. package/dist/adapters/fetch/index.d.ts +1 -12
  12. package/dist/adapters/fetch/index.mjs +2 -9
  13. package/dist/adapters/message-port/index.d.mts +1 -1
  14. package/dist/adapters/message-port/index.d.ts +1 -1
  15. package/dist/adapters/message-port/index.mjs +1 -1
  16. package/dist/adapters/node/index.d.mts +1 -12
  17. package/dist/adapters/node/index.d.ts +1 -12
  18. package/dist/adapters/node/index.mjs +2 -9
  19. package/dist/adapters/standard/index.d.mts +1 -1
  20. package/dist/adapters/standard/index.d.ts +1 -1
  21. package/dist/adapters/standard/index.mjs +1 -1
  22. package/dist/adapters/websocket/index.d.mts +1 -1
  23. package/dist/adapters/websocket/index.d.ts +1 -1
  24. package/dist/adapters/websocket/index.mjs +1 -1
  25. package/dist/index.d.mts +260 -0
  26. package/dist/index.d.ts +260 -0
  27. package/dist/index.mjs +73 -0
  28. package/dist/plugins/index.d.mts +49 -11
  29. package/dist/plugins/index.d.ts +49 -11
  30. package/dist/plugins/index.mjs +75 -3
  31. package/dist/shared/{server.BUvC0G_8.d.ts → server.C2n16pp0.d.ts} +24 -2
  32. package/dist/shared/{server.DEGjTAPW.mjs → server.Cbv46U7N.mjs} +19 -3
  33. package/dist/shared/{server.zySQaS-n.d.mts → server.CkNnZ5F-.d.mts} +24 -2
  34. package/package.json +5 -4
  35. package/dist/shared/server.D_QauotT.mjs +0 -30
@@ -1,10 +1,9 @@
1
1
  import { toArray, value, isCompressibleContentType, isAsyncIteratorObject, stringifyJSON } from '@orpc/shared';
2
2
  import { flattenStandardHeader, parseStandardUrl, generateContentDisposition, mergeStandardHeaders } from '@standardserver/core';
3
3
  import { isClientPeerSendMessage, ServerPeer, encodePeerMessage } from '@standardserver/peer';
4
- export { C as CSRFGuardHandlerPlugin } from '../shared/server.D_QauotT.mjs';
5
4
  import { toFetchHeaders, toStandardBody, toStandardHeaders } from '@standardserver/fetch';
6
5
  export { R as RequestLimitHandlerPlugin } from '../shared/server.Cd4Z1hpV.mjs';
7
- import '@orpc/client';
6
+ import { ORPCError } from '@orpc/client';
8
7
 
9
8
  const BATCH_CONTENT_TYPE = "application/vnd.orpc.batch";
10
9
  class BatchHandlerPlugin {
@@ -62,6 +61,12 @@ class BatchHandlerPlugin {
62
61
  response: { status: 400, headers: {}, body: "Invalid batch request data parameter" }
63
62
  };
64
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
+ }
65
70
  messages = mightBeMessages;
66
71
  } else {
67
72
  const mightBeMessages = await interceptorOptions.request.resolveBody();
@@ -286,6 +291,48 @@ class CORSHandlerPlugin {
286
291
  }
287
292
  }
288
293
 
294
+ class MethodOverrideHandlerPlugin {
295
+ name = "~method-override";
296
+ /**
297
+ * Should override batch sub-request methods, not the original batch request.
298
+ */
299
+ before = ["~batch"];
300
+ param;
301
+ methods;
302
+ constructor(options = {}) {
303
+ this.param = options.param ?? "method";
304
+ this.methods = new Set((options.methods ?? ["PUT", "PATCH", "DELETE"]).map((method) => method.toUpperCase()));
305
+ }
306
+ init(options) {
307
+ const routingInterceptor = async ({ next, ...interceptorOptions }) => {
308
+ const { request } = interceptorOptions;
309
+ if (request.method !== "POST") {
310
+ return next();
311
+ }
312
+ const [pathname, search, hash] = parseStandardUrl(request.url);
313
+ const params = new URLSearchParams(search);
314
+ const raw = params.getAll(this.param).at(-1);
315
+ if (raw === void 0) {
316
+ return next();
317
+ }
318
+ const method = raw.toUpperCase();
319
+ if (!this.methods.has(method)) {
320
+ return next();
321
+ }
322
+ params.delete(this.param);
323
+ const url = `${pathname}${params.size ? `?${params}` : ""}${hash ?? ""}`;
324
+ return next({
325
+ ...interceptorOptions,
326
+ request: { ...request, method, url }
327
+ });
328
+ };
329
+ return {
330
+ ...options,
331
+ routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
332
+ };
333
+ }
334
+ }
335
+
289
336
  class RequestCompressionHandlerPlugin {
290
337
  name = "~request-compression";
291
338
  /**
@@ -633,4 +680,29 @@ class RethrowHandlerPlugin {
633
680
  }
634
681
  }
635
682
 
636
- export { BATCH_CONTENT_TYPE, BatchHandlerPlugin, CORSHandlerPlugin, CORSHandlerPlugin as CORSPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestHeadersHandlerPlugin as RequestHeadersPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, ResponseHeadersHandlerPlugin as ResponseHeadersPlugin, RethrowHandlerPlugin };
683
+ class SimpleCsrfProtectionHandlerPlugin {
684
+ name = "~simple-csrf-protection";
685
+ init(options) {
686
+ const interceptor = async (interceptorOptions) => {
687
+ const mode = flattenStandardHeader(
688
+ interceptorOptions.request.headers["sec-fetch-mode"]
689
+ )?.toLowerCase();
690
+ if (mode === void 0) {
691
+ return interceptorOptions.next();
692
+ }
693
+ if (mode === "cors" || mode === "same-origin") {
694
+ return interceptorOptions.next();
695
+ }
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
+ };
705
+ }
706
+ }
707
+
708
+ export { BATCH_CONTENT_TYPE, BatchHandlerPlugin, CORSHandlerPlugin, CORSHandlerPlugin as CORSPlugin, MethodOverrideHandlerPlugin, RequestCompressionHandlerPlugin, RequestHeadersHandlerPlugin, RequestHeadersHandlerPlugin as RequestHeadersPlugin, ResponseCompressionHandlerPlugin, ResponseHeadersHandlerPlugin, ResponseHeadersHandlerPlugin as ResponseHeadersPlugin, RethrowHandlerPlugin, SimpleCsrfProtectionHandlerPlugin };
@@ -6,6 +6,16 @@ import { A as AnyRouter } from './server.DtfwuV6U.js';
6
6
  import { AnyProcedureContract } from '@orpc/contract';
7
7
  import { c as StandardHandlerHandleOptions, d as StandardHandlerCodec, e as StandardHandlerCodecResolvedProcedure } from './server.DrN1Pj1-.js';
8
8
 
9
+ /**
10
+ * Methods that can invoke procedures by default. Browsers cannot trigger them
11
+ * cross-site without a CORS preflight or an HTML form, unlike `GET`, which a plain
12
+ * `<a>` click or redirect can trigger with `SameSite=Lax` cookies attached. Other
13
+ * methods (`HEAD`, `OPTIONS`, `QUERY`, ...) have safe semantics that should not
14
+ * invoke a procedure that can modify data.
15
+ *
16
+ * @see {@link https://orpc.dev/docs/rpc/handler#supported-http-methods | RPC Handler - Supported HTTP Methods}
17
+ */
18
+ declare const RPC_DEFAULT_ALLOW_METHODS: readonly StandardMethod[];
9
19
  interface RPCMatcherOptions {
10
20
  /**
11
21
  * Filter which procedures are exposed for matching. Return `false` to exclude.
@@ -13,15 +23,27 @@ interface RPCMatcherOptions {
13
23
  * @default true
14
24
  */
15
25
  filter?: Value<boolean, [procedure: AnyProcedureContract | AnyProcedure, path: string[]]>;
26
+ /**
27
+ * Restricts which HTTP methods can invoke procedures, either with a list of allowed
28
+ * methods or decided per request via a function. Requests using a disallowed method
29
+ * are treated as unmatched. `GET` is excluded by default because it is exposed to
30
+ * Cross-Site Request Forgery (CSRF) attacks.
31
+ *
32
+ * @default RPC_DEFAULT_ALLOW_METHODS (['POST', 'PUT', 'PATCH', 'DELETE'])
33
+ * @see {@link https://orpc.dev/docs/rpc/handler#supported-http-methods | RPC Handler - Supported HTTP Methods}
34
+ */
35
+ allowMethods?: readonly StandardMethod[] | ((method: StandardMethod, procedure: AnyProcedure, path: string[]) => boolean);
16
36
  }
17
37
  declare class RPCMatcher {
18
38
  private readonly filter;
39
+ private readonly allowMethodsSet;
40
+ private readonly allowMethodsFn;
19
41
  private readonly rootRouter;
20
42
  private readonly tree;
21
43
  private readonly pendingLazyRouters;
22
44
  constructor(router: AnyRouter, options?: RPCMatcherOptions);
23
45
  private index;
24
- match(_method: StandardMethod, pathname: `/${string}`, prefix: `/${string}` | undefined): Promise<{
46
+ match(method: StandardMethod, pathname: `/${string}`, prefix: `/${string}` | undefined): Promise<{
25
47
  path: string[];
26
48
  procedure: AnyProcedure;
27
49
  } | undefined>;
@@ -65,5 +87,5 @@ declare class RPCHandlerCodec<T extends Context> implements StandardHandlerCodec
65
87
  encodeError(error: AnyORPCError, _procedure: AnyProcedure, _path: string[], _options: StandardHandlerHandleOptions<T>): Promisable<StandardResponse>;
66
88
  }
67
89
 
68
- export { RPCHandlerCodec as a, RPCMatcher as b };
90
+ export { RPCHandlerCodec as a, RPCMatcher as b, RPC_DEFAULT_ALLOW_METHODS as d };
69
91
  export type { RPCHandlerCodecOptions as R, RPCMatcherOptions as c };
@@ -121,13 +121,22 @@ class OtelHandlerPlugin {
121
121
  }
122
122
  }
123
123
 
124
+ const RPC_DEFAULT_ALLOW_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
124
125
  class RPCMatcher {
125
126
  filter;
127
+ allowMethodsSet;
128
+ allowMethodsFn;
126
129
  rootRouter;
127
130
  tree = /* @__PURE__ */ new Map();
128
131
  pendingLazyRouters = /* @__PURE__ */ new Map();
129
132
  constructor(router, options = {}) {
130
133
  this.filter = options.filter ?? true;
134
+ const allowMethods = options.allowMethods ?? RPC_DEFAULT_ALLOW_METHODS;
135
+ if (typeof allowMethods !== "function") {
136
+ this.allowMethodsSet = new Set(allowMethods);
137
+ } else {
138
+ this.allowMethodsFn = allowMethods;
139
+ }
131
140
  this.rootRouter = router;
132
141
  this.index(router);
133
142
  }
@@ -147,7 +156,10 @@ class RPCMatcher {
147
156
  this.pendingLazyRouters.set(pathToHttpPath(result.path), result);
148
157
  }
149
158
  }
150
- async match(_method, pathname, prefix) {
159
+ async match(method, pathname, prefix) {
160
+ if (this.allowMethodsSet && !this.allowMethodsSet.has(method)) {
161
+ return void 0;
162
+ }
151
163
  if (pathname.length > 1 && pathname.endsWith("/")) {
152
164
  pathname = pathname.slice(0, -1);
153
165
  }
@@ -182,9 +194,13 @@ class RPCMatcher {
182
194
  if (entry === void 0) {
183
195
  return void 0;
184
196
  }
197
+ const procedure = entry.procedure ?? await this.resolveProcedure(entry);
198
+ if (this.allowMethodsFn && !this.allowMethodsFn(method, procedure, entry.path)) {
199
+ return void 0;
200
+ }
185
201
  return {
186
202
  path: entry.path,
187
- procedure: entry.procedure ?? await this.resolveProcedure(entry)
203
+ procedure
188
204
  };
189
205
  }
190
206
  resolvePendingLazyRouters(pathname) {
@@ -285,4 +301,4 @@ class RPCHandlerCodec {
285
301
  }
286
302
  }
287
303
 
288
- export { CompositeStandardHandlerPlugin as C, OtelHandlerPlugin as O, RPCHandlerCodec as R, StandardHandler as S, RPCMatcher as a };
304
+ export { CompositeStandardHandlerPlugin as C, OtelHandlerPlugin as O, RPCHandlerCodec as R, StandardHandler as S, RPCMatcher as a, RPC_DEFAULT_ALLOW_METHODS as b };
@@ -6,6 +6,16 @@ import { A as AnyRouter } from './server.Dm0os-OP.mjs';
6
6
  import { AnyProcedureContract } from '@orpc/contract';
7
7
  import { c as StandardHandlerHandleOptions, d as StandardHandlerCodec, e as StandardHandlerCodecResolvedProcedure } from './server.JbCIPL4P.mjs';
8
8
 
9
+ /**
10
+ * Methods that can invoke procedures by default. Browsers cannot trigger them
11
+ * cross-site without a CORS preflight or an HTML form, unlike `GET`, which a plain
12
+ * `<a>` click or redirect can trigger with `SameSite=Lax` cookies attached. Other
13
+ * methods (`HEAD`, `OPTIONS`, `QUERY`, ...) have safe semantics that should not
14
+ * invoke a procedure that can modify data.
15
+ *
16
+ * @see {@link https://orpc.dev/docs/rpc/handler#supported-http-methods | RPC Handler - Supported HTTP Methods}
17
+ */
18
+ declare const RPC_DEFAULT_ALLOW_METHODS: readonly StandardMethod[];
9
19
  interface RPCMatcherOptions {
10
20
  /**
11
21
  * Filter which procedures are exposed for matching. Return `false` to exclude.
@@ -13,15 +23,27 @@ interface RPCMatcherOptions {
13
23
  * @default true
14
24
  */
15
25
  filter?: Value<boolean, [procedure: AnyProcedureContract | AnyProcedure, path: string[]]>;
26
+ /**
27
+ * Restricts which HTTP methods can invoke procedures, either with a list of allowed
28
+ * methods or decided per request via a function. Requests using a disallowed method
29
+ * are treated as unmatched. `GET` is excluded by default because it is exposed to
30
+ * Cross-Site Request Forgery (CSRF) attacks.
31
+ *
32
+ * @default RPC_DEFAULT_ALLOW_METHODS (['POST', 'PUT', 'PATCH', 'DELETE'])
33
+ * @see {@link https://orpc.dev/docs/rpc/handler#supported-http-methods | RPC Handler - Supported HTTP Methods}
34
+ */
35
+ allowMethods?: readonly StandardMethod[] | ((method: StandardMethod, procedure: AnyProcedure, path: string[]) => boolean);
16
36
  }
17
37
  declare class RPCMatcher {
18
38
  private readonly filter;
39
+ private readonly allowMethodsSet;
40
+ private readonly allowMethodsFn;
19
41
  private readonly rootRouter;
20
42
  private readonly tree;
21
43
  private readonly pendingLazyRouters;
22
44
  constructor(router: AnyRouter, options?: RPCMatcherOptions);
23
45
  private index;
24
- match(_method: StandardMethod, pathname: `/${string}`, prefix: `/${string}` | undefined): Promise<{
46
+ match(method: StandardMethod, pathname: `/${string}`, prefix: `/${string}` | undefined): Promise<{
25
47
  path: string[];
26
48
  procedure: AnyProcedure;
27
49
  } | undefined>;
@@ -65,5 +87,5 @@ declare class RPCHandlerCodec<T extends Context> implements StandardHandlerCodec
65
87
  encodeError(error: AnyORPCError, _procedure: AnyProcedure, _path: string[], _options: StandardHandlerHandleOptions<T>): Promisable<StandardResponse>;
66
88
  }
67
89
 
68
- export { RPCHandlerCodec as a, RPCMatcher as b };
90
+ export { RPCHandlerCodec as a, RPCMatcher as b, RPC_DEFAULT_ALLOW_METHODS as d };
69
91
  export type { RPCHandlerCodecOptions as R, RPCMatcherOptions as c };
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@orpc/server",
3
3
  "type": "module",
4
- "version": "2.0.0-beta.24",
4
+ "version": "2.0.0-beta.26",
5
5
  "license": "MIT",
6
+ "funding": "https://github.com/sponsors/dinwwwh",
6
7
  "homepage": "https://orpc.dev",
7
8
  "repository": {
8
9
  "type": "git",
@@ -106,9 +107,9 @@
106
107
  "@standardserver/node": "^0.7.1",
107
108
  "@standardserver/peer": "^0.7.1",
108
109
  "cookie": "^2.0.1",
109
- "@orpc/client": "2.0.0-beta.24",
110
- "@orpc/contract": "2.0.0-beta.24",
111
- "@orpc/shared": "2.0.0-beta.24"
110
+ "@orpc/client": "2.0.0-beta.26",
111
+ "@orpc/contract": "2.0.0-beta.26",
112
+ "@orpc/shared": "2.0.0-beta.26"
112
113
  },
113
114
  "devDependencies": {
114
115
  "crossws": "^0.4.6",
@@ -1,30 +0,0 @@
1
- import { ORPCError } from '@orpc/client';
2
- import { toArray } from '@orpc/shared';
3
- import { flattenStandardHeader } from '@standardserver/core';
4
-
5
- class CSRFGuardHandlerPlugin {
6
- name = "~csrf-guard";
7
- init(options) {
8
- const interceptor = async (interceptorOptions) => {
9
- const mode = flattenStandardHeader(
10
- interceptorOptions.request.headers["sec-fetch-mode"]
11
- )?.toLowerCase();
12
- if (mode === void 0) {
13
- return interceptorOptions.next();
14
- }
15
- if (mode === "cors" || mode === "same-origin") {
16
- return interceptorOptions.next();
17
- }
18
- throw new ORPCError("FORBIDDEN", {
19
- message: "Request blocked by CSRF protection."
20
- });
21
- };
22
- return {
23
- ...options,
24
- // appended last so user's interceptors can catch ORPCError
25
- interceptors: [...toArray(options.interceptors), interceptor]
26
- };
27
- }
28
- }
29
-
30
- export { CSRFGuardHandlerPlugin as C };