@orpc/server 2.0.0-beta.48 → 2.0.0-beta.49
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 +4 -4
- package/dist/adapters/aws-lambda/handler.d.ts +25 -0
- package/dist/adapters/aws-lambda/handler.js +24 -0
- package/dist/adapters/aws-lambda/index.d.ts +16 -0
- package/dist/adapters/aws-lambda/index.js +2 -0
- package/dist/adapters/aws-lambda/rpc-handler.d.ts +19 -0
- package/dist/adapters/aws-lambda/rpc-handler.js +18 -0
- package/dist/adapters/crossws/handler.d.ts +45 -0
- package/dist/adapters/crossws/handler.js +60 -0
- package/dist/adapters/crossws/index.d.ts +2 -0
- package/dist/adapters/crossws/index.js +2 -0
- package/dist/adapters/crossws/rpc-handler.d.ts +16 -0
- package/dist/adapters/crossws/rpc-handler.js +15 -0
- package/dist/adapters/fastify/handler.d.ts +21 -0
- package/dist/adapters/fastify/handler.js +20 -0
- package/dist/adapters/fastify/index.d.ts +2 -0
- package/dist/adapters/fastify/index.js +2 -0
- package/dist/adapters/fastify/rpc-handler.d.ts +15 -0
- package/dist/adapters/fastify/rpc-handler.js +14 -0
- package/dist/adapters/fetch/handler.d.ts +23 -0
- package/dist/adapters/fetch/handler.js +22 -0
- package/dist/adapters/fetch/index.d.ts +7 -0
- package/dist/adapters/fetch/index.js +7 -0
- package/dist/adapters/fetch/rpc-handler.d.ts +17 -0
- package/dist/adapters/fetch/rpc-handler.js +16 -0
- package/dist/adapters/message-port/handler.d.ts +64 -0
- package/dist/adapters/message-port/handler.js +86 -0
- package/dist/adapters/message-port/index.d.ts +2 -0
- package/dist/adapters/message-port/index.js +2 -0
- package/dist/adapters/message-port/rpc-handler.d.ts +16 -0
- package/dist/adapters/message-port/rpc-handler.js +15 -0
- package/dist/adapters/node/handler.d.ts +21 -0
- package/dist/adapters/node/handler.js +20 -0
- package/dist/adapters/node/index.d.ts +7 -0
- package/dist/adapters/node/index.js +7 -0
- package/dist/adapters/node/rpc-handler.d.ts +16 -0
- package/dist/adapters/node/rpc-handler.js +15 -0
- package/dist/adapters/standard/codec.d.ts +16 -0
- package/dist/adapters/standard/handler.d.ts +68 -0
- package/dist/adapters/standard/handler.js +205 -0
- package/dist/adapters/standard/index.d.ts +6 -0
- package/dist/adapters/standard/index.js +5 -0
- package/dist/adapters/standard/plugin.d.ts +32 -0
- package/dist/adapters/standard/plugin.js +16 -0
- package/dist/adapters/standard/rpc-handler-codec.d.ts +46 -0
- package/dist/adapters/standard/rpc-handler-codec.js +51 -0
- package/dist/adapters/standard/rpc-matcher.d.ts +53 -0
- package/dist/adapters/standard/rpc-matcher.js +152 -0
- package/dist/adapters/standard/utils.d.ts +10 -0
- package/dist/adapters/standard/utils.js +6 -0
- package/dist/adapters/standard-peer/index.d.ts +1 -0
- package/dist/adapters/standard-peer/index.js +1 -0
- package/dist/adapters/standard-peer/{utils.d.mts → utils.d.ts} +3 -3
- package/dist/adapters/standard-peer/utils.js +7 -0
- package/dist/adapters/websocket/handler.d.ts +59 -0
- package/dist/adapters/websocket/handler.js +85 -0
- package/dist/adapters/websocket/index.d.ts +2 -0
- package/dist/adapters/websocket/index.js +2 -0
- package/dist/adapters/websocket/rpc-handler.d.ts +16 -0
- package/dist/adapters/websocket/rpc-handler.js +15 -0
- package/dist/builder-variants.d.ts +226 -0
- package/dist/builder.d.ts +127 -0
- package/dist/builder.js +226 -0
- package/dist/context.d.ts +6 -0
- package/dist/extensions/callable.d.ts +11 -0
- package/dist/extensions/callable.js +7 -0
- package/dist/helpers/base64url.d.ts +31 -0
- package/dist/helpers/base64url.js +53 -0
- package/dist/helpers/cookie.d.ts +51 -0
- package/dist/helpers/cookie.js +69 -0
- package/dist/helpers/encryption.d.ts +28 -0
- package/dist/helpers/encryption.js +80 -0
- package/dist/helpers/index.d.ts +4 -0
- package/dist/helpers/index.js +4 -0
- package/dist/helpers/signing.d.ts +50 -0
- package/dist/helpers/signing.js +88 -0
- package/dist/implementer-procedure.d.ts +16 -0
- package/dist/implementer-procedure.js +44 -0
- package/dist/implementer-router.d.ts +40 -0
- package/dist/implementer-router.js +117 -0
- package/dist/implementer.d.ts +16 -0
- package/dist/implementer.js +37 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +48 -0
- package/dist/lazy.d.ts +20 -0
- package/dist/lazy.js +31 -0
- package/dist/middleware-decorated.d.ts +10 -0
- package/dist/middleware-decorated.js +71 -0
- package/dist/middleware.d.ts +54 -0
- package/dist/plugins/batch.d.ts +102 -0
- package/dist/plugins/batch.js +405 -0
- package/dist/plugins/cors.d.ts +73 -0
- package/dist/plugins/cors.js +112 -0
- package/dist/plugins/get-method-csrf-protection.d.ts +23 -0
- package/dist/plugins/get-method-csrf-protection.js +64 -0
- package/dist/plugins/head-method.d.ts +23 -0
- package/dist/plugins/head-method.js +104 -0
- package/dist/plugins/index.d.ts +39 -0
- package/dist/plugins/index.js +29 -0
- package/dist/plugins/method-override.d.ts +38 -0
- package/dist/plugins/method-override.js +50 -0
- package/dist/plugins/request-compression.d.ts +16 -0
- package/dist/plugins/request-compression.js +82 -0
- package/dist/plugins/request-headers.d.ts +22 -0
- package/dist/plugins/request-headers.js +30 -0
- package/dist/plugins/request-limit.d.ts +30 -0
- package/dist/plugins/request-limit.js +68 -0
- package/dist/plugins/response-compression.d.ts +59 -0
- package/dist/plugins/response-compression.js +209 -0
- package/dist/plugins/response-headers.d.ts +30 -0
- package/dist/plugins/response-headers.js +47 -0
- package/dist/plugins/rethrow.d.ts +33 -0
- package/dist/plugins/rethrow.js +59 -0
- package/dist/plugins/signal-sync.d.ts +13 -0
- package/dist/plugins/signal-sync.js +91 -0
- package/dist/plugins/timeout.d.ts +37 -0
- package/dist/plugins/timeout.js +59 -0
- package/dist/procedure-client.d.ts +19 -0
- package/dist/procedure-client.js +175 -0
- package/dist/procedure-decorated.d.ts +10 -0
- package/dist/procedure-decorated.js +44 -0
- package/dist/procedure-utils.d.ts +41 -0
- package/dist/procedure-utils.js +49 -0
- package/dist/procedure.d.ts +72 -0
- package/dist/procedure.js +24 -0
- package/dist/router-client.d.ts +23 -0
- package/dist/router-client.js +43 -0
- package/dist/{router-hidden.d.mts → router-hidden.d.ts} +3 -3
- package/dist/router-hidden.js +14 -0
- package/dist/router-utils.d.ts +48 -0
- package/dist/router-utils.js +202 -0
- package/dist/router.d.ts +72 -0
- package/package.json +84 -86
- package/dist/adapters/aws-lambda/handler.d.mts +0 -25
- package/dist/adapters/aws-lambda/handler.mjs +0 -21
- package/dist/adapters/aws-lambda/index.d.mts +0 -16
- package/dist/adapters/aws-lambda/index.mjs +0 -2
- package/dist/adapters/aws-lambda/rpc-handler.d.mts +0 -18
- package/dist/adapters/aws-lambda/rpc-handler.mjs +0 -10
- package/dist/adapters/crossws/handler.d.mts +0 -41
- package/dist/adapters/crossws/handler.mjs +0 -38
- package/dist/adapters/crossws/index.d.mts +0 -2
- package/dist/adapters/crossws/index.mjs +0 -2
- package/dist/adapters/crossws/rpc-handler.d.mts +0 -15
- package/dist/adapters/crossws/rpc-handler.mjs +0 -10
- package/dist/adapters/fastify/handler.d.mts +0 -21
- package/dist/adapters/fastify/handler.mjs +0 -20
- package/dist/adapters/fastify/index.d.mts +0 -2
- package/dist/adapters/fastify/index.mjs +0 -2
- package/dist/adapters/fastify/rpc-handler.d.mts +0 -14
- package/dist/adapters/fastify/rpc-handler.mjs +0 -10
- package/dist/adapters/fetch/handler.d.mts +0 -23
- package/dist/adapters/fetch/handler.mjs +0 -22
- package/dist/adapters/fetch/index.d.mts +0 -7
- package/dist/adapters/fetch/index.mjs +0 -5
- package/dist/adapters/fetch/rpc-handler.d.mts +0 -16
- package/dist/adapters/fetch/rpc-handler.mjs +0 -10
- package/dist/adapters/message-port/handler.d.mts +0 -57
- package/dist/adapters/message-port/handler.mjs +0 -62
- package/dist/adapters/message-port/index.d.mts +0 -2
- package/dist/adapters/message-port/index.mjs +0 -2
- package/dist/adapters/message-port/rpc-handler.d.mts +0 -15
- package/dist/adapters/message-port/rpc-handler.mjs +0 -10
- package/dist/adapters/node/handler.d.mts +0 -21
- package/dist/adapters/node/handler.mjs +0 -20
- package/dist/adapters/node/index.d.mts +0 -7
- package/dist/adapters/node/index.mjs +0 -5
- package/dist/adapters/node/rpc-handler.d.mts +0 -15
- package/dist/adapters/node/rpc-handler.mjs +0 -10
- package/dist/adapters/standard/codec.d.mts +0 -16
- package/dist/adapters/standard/handler.d.mts +0 -68
- package/dist/adapters/standard/handler.mjs +0 -185
- package/dist/adapters/standard/index.d.mts +0 -6
- package/dist/adapters/standard/index.mjs +0 -5
- package/dist/adapters/standard/plugin.d.mts +0 -32
- package/dist/adapters/standard/plugin.mjs +0 -16
- package/dist/adapters/standard/rpc-handler-codec.d.mts +0 -42
- package/dist/adapters/standard/rpc-handler-codec.mjs +0 -51
- package/dist/adapters/standard/rpc-matcher.d.mts +0 -53
- package/dist/adapters/standard/rpc-matcher.mjs +0 -140
- package/dist/adapters/standard/utils.d.mts +0 -10
- package/dist/adapters/standard/utils.mjs +0 -6
- package/dist/adapters/standard-peer/index.d.mts +0 -1
- package/dist/adapters/standard-peer/index.mjs +0 -1
- package/dist/adapters/standard-peer/utils.mjs +0 -11
- package/dist/adapters/websocket/handler.d.mts +0 -55
- package/dist/adapters/websocket/handler.mjs +0 -55
- package/dist/adapters/websocket/index.d.mts +0 -2
- package/dist/adapters/websocket/index.mjs +0 -2
- package/dist/adapters/websocket/rpc-handler.d.mts +0 -15
- package/dist/adapters/websocket/rpc-handler.mjs +0 -10
- package/dist/builder-variants.d.mts +0 -267
- package/dist/builder.d.mts +0 -141
- package/dist/builder.mjs +0 -145
- package/dist/context.d.mts +0 -13
- package/dist/extensions/callable.d.mts +0 -17
- package/dist/extensions/callable.mjs +0 -7
- package/dist/helpers/base64url.d.mts +0 -31
- package/dist/helpers/base64url.mjs +0 -25
- package/dist/helpers/cookie.d.mts +0 -50
- package/dist/helpers/cookie.mjs +0 -35
- package/dist/helpers/encryption.d.mts +0 -28
- package/dist/helpers/encryption.mjs +0 -62
- package/dist/helpers/index.d.mts +0 -4
- package/dist/helpers/index.mjs +0 -4
- package/dist/helpers/signing.d.mts +0 -50
- package/dist/helpers/signing.mjs +0 -43
- package/dist/implementer-procedure.d.mts +0 -36
- package/dist/implementer-procedure.mjs +0 -38
- package/dist/implementer-router.d.mts +0 -78
- package/dist/implementer-router.mjs +0 -77
- package/dist/implementer.d.mts +0 -22
- package/dist/implementer.mjs +0 -30
- package/dist/index.d.mts +0 -57
- package/dist/index.mjs +0 -30
- package/dist/lazy.d.mts +0 -20
- package/dist/lazy.mjs +0 -22
- package/dist/middleware-decorated.d.mts +0 -27
- package/dist/middleware-decorated.mjs +0 -65
- package/dist/middleware.d.mts +0 -70
- package/dist/plugins/batch.d.mts +0 -104
- package/dist/plugins/batch.mjs +0 -373
- package/dist/plugins/cors.d.mts +0 -67
- package/dist/plugins/cors.mjs +0 -134
- package/dist/plugins/get-method-csrf-protection.d.mts +0 -23
- package/dist/plugins/get-method-csrf-protection.mjs +0 -45
- package/dist/plugins/head-method.d.mts +0 -23
- package/dist/plugins/head-method.mjs +0 -95
- package/dist/plugins/index.d.mts +0 -39
- package/dist/plugins/index.mjs +0 -23
- package/dist/plugins/method-override.d.mts +0 -38
- package/dist/plugins/method-override.mjs +0 -50
- package/dist/plugins/request-compression.d.mts +0 -16
- package/dist/plugins/request-compression.mjs +0 -67
- package/dist/plugins/request-headers.d.mts +0 -22
- package/dist/plugins/request-headers.mjs +0 -21
- package/dist/plugins/request-limit.d.mts +0 -30
- package/dist/plugins/request-limit.mjs +0 -53
- package/dist/plugins/response-compression.d.mts +0 -59
- package/dist/plugins/response-compression.mjs +0 -172
- package/dist/plugins/response-headers.d.mts +0 -30
- package/dist/plugins/response-headers.mjs +0 -34
- package/dist/plugins/rethrow.d.mts +0 -33
- package/dist/plugins/rethrow.mjs +0 -46
- package/dist/plugins/signal-sync.d.mts +0 -13
- package/dist/plugins/signal-sync.mjs +0 -84
- package/dist/plugins/timeout.d.mts +0 -37
- package/dist/plugins/timeout.mjs +0 -56
- package/dist/procedure-client.d.mts +0 -41
- package/dist/procedure-client.mjs +0 -157
- package/dist/procedure-decorated.d.mts +0 -20
- package/dist/procedure-decorated.mjs +0 -41
- package/dist/procedure-utils.d.mts +0 -52
- package/dist/procedure-utils.mjs +0 -33
- package/dist/procedure.d.mts +0 -93
- package/dist/procedure.mjs +0 -20
- package/dist/router-client.d.mts +0 -27
- package/dist/router-client.mjs +0 -35
- package/dist/router-hidden.mjs +0 -12
- package/dist/router-utils.d.mts +0 -59
- package/dist/router-utils.mjs +0 -191
- package/dist/router.d.mts +0 -61
- /package/dist/adapters/standard/{codec.mjs → codec.js} +0 -0
- /package/dist/{builder-variants.mjs → builder-variants.js} +0 -0
- /package/dist/{constants.d.mts → constants.d.ts} +0 -0
- /package/dist/{constants.mjs → constants.js} +0 -0
- /package/dist/{context.mjs → context.js} +0 -0
- /package/dist/{middleware.mjs → middleware.js} +0 -0
- /package/dist/{router.mjs → router.js} +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Promisable, Value } from '@orpc/shared';
|
|
2
|
+
import type { StandardHandlerOptions, StandardHandlerPlugin, StandardHandlerRoutingInterceptorOptions } from '../adapters/standard/index.ts';
|
|
3
|
+
import type { Context } from '../context.ts';
|
|
4
|
+
export interface CORSHandlerPluginOptions<T extends Context> {
|
|
5
|
+
/**
|
|
6
|
+
* Configures the `Access-Control-Allow-Origin` header.
|
|
7
|
+
* Can be a string, an array of allowed origins, or a function (optionally async) that returns the allowed origin(s).
|
|
8
|
+
*
|
|
9
|
+
* A function must only return origins it has validated: returning the incoming origin
|
|
10
|
+
* unchecked allows every origin.
|
|
11
|
+
*
|
|
12
|
+
* @default '*'
|
|
13
|
+
*/
|
|
14
|
+
origin?: Value<Promisable<string | readonly string[] | null | undefined>, [
|
|
15
|
+
origin: string,
|
|
16
|
+
options: StandardHandlerRoutingInterceptorOptions<T>
|
|
17
|
+
]> | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Configures the `Timing-Allow-Origin` header.
|
|
20
|
+
* Can be a string, an array of allowed origins, or a function (optionally async) that returns the allowed origin(s).
|
|
21
|
+
*/
|
|
22
|
+
timingOrigin?: Value<Promisable<string | readonly string[] | null | undefined>, [
|
|
23
|
+
origin: string,
|
|
24
|
+
options: StandardHandlerRoutingInterceptorOptions<T>
|
|
25
|
+
]> | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Configures the `Access-Control-Allow-Methods` header for preflight requests.
|
|
28
|
+
*
|
|
29
|
+
* @default ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH', 'QUERY']
|
|
30
|
+
*/
|
|
31
|
+
allowMethods?: readonly string[] | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Configures the `Access-Control-Allow-Headers` header for preflight requests.
|
|
34
|
+
* Falls back to the request's `Access-Control-Request-Headers` if not set.
|
|
35
|
+
* When set, it replaces that fallback, so include every non-safelisted header
|
|
36
|
+
* clients send, such as `Content-Type`.
|
|
37
|
+
*/
|
|
38
|
+
allowHeaders?: readonly string[] | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Configures the `Access-Control-Max-Age` header (in seconds) for preflight requests.
|
|
41
|
+
*/
|
|
42
|
+
maxAge?: number | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Configures the `Access-Control-Allow-Credentials` header.
|
|
45
|
+
*
|
|
46
|
+
* When enabled, `origin` must allow trusted origins only: every allowed origin can send
|
|
47
|
+
* authenticated requests as your users and read the responses.
|
|
48
|
+
*/
|
|
49
|
+
credentials?: boolean | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Configures the `Access-Control-Expose-Headers` header.
|
|
52
|
+
*/
|
|
53
|
+
exposeHeaders?: readonly string[] | undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Configures the [CORS Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
|
57
|
+
* for your API, including preflight requests.
|
|
58
|
+
*
|
|
59
|
+
* @see {@link https://orpc.dev/docs/plugins/cors | CORS Handler Plugin}
|
|
60
|
+
*/
|
|
61
|
+
export declare class CORSHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
|
|
62
|
+
private readonly options;
|
|
63
|
+
name: string;
|
|
64
|
+
/**
|
|
65
|
+
* - Do not create spans for CORS preflight requests.
|
|
66
|
+
* - Run CORS interceptors before batch interceptors so headers are applied to
|
|
67
|
+
* the actual response rather than sub-responses.
|
|
68
|
+
*/
|
|
69
|
+
after: string[];
|
|
70
|
+
constructor(options?: CORSHandlerPluginOptions<T>);
|
|
71
|
+
init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
|
|
72
|
+
private resolveOrigin;
|
|
73
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { toArray } from '@orpc/shared';
|
|
2
|
+
import { flattenStandardHeader } from '@standard-server/core';
|
|
3
|
+
/**
|
|
4
|
+
* Configures the [CORS Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
|
5
|
+
* for your API, including preflight requests.
|
|
6
|
+
*
|
|
7
|
+
* @see {@link https://orpc.dev/docs/plugins/cors | CORS Handler Plugin}
|
|
8
|
+
*/
|
|
9
|
+
export class CORSHandlerPlugin {
|
|
10
|
+
options;
|
|
11
|
+
name = '~cors';
|
|
12
|
+
/**
|
|
13
|
+
* - Do not create spans for CORS preflight requests.
|
|
14
|
+
* - Run CORS interceptors before batch interceptors so headers are applied to
|
|
15
|
+
* the actual response rather than sub-responses.
|
|
16
|
+
*/
|
|
17
|
+
after = ['~tracing', '~batch'];
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
const defaults = {
|
|
20
|
+
origin: '*',
|
|
21
|
+
allowMethods: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH', 'QUERY'],
|
|
22
|
+
};
|
|
23
|
+
this.options = {
|
|
24
|
+
...defaults,
|
|
25
|
+
...options,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
init(options) {
|
|
29
|
+
const corsHeadersInterceptor = async (interceptorOptions) => {
|
|
30
|
+
const result = await interceptorOptions.next();
|
|
31
|
+
if (!result.matched) {
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
const resHeaders = { ...result.response.headers };
|
|
35
|
+
const origin = flattenStandardHeader(interceptorOptions.request.headers.origin);
|
|
36
|
+
const allowOrigin = await this.resolveOrigin(this.options.origin, origin, interceptorOptions);
|
|
37
|
+
const timingAllowOrigin = await this.resolveOrigin(this.options.timingOrigin, origin, interceptorOptions);
|
|
38
|
+
if (allowOrigin.value !== undefined) {
|
|
39
|
+
resHeaders['access-control-allow-origin'] = allowOrigin.value;
|
|
40
|
+
}
|
|
41
|
+
if (timingAllowOrigin.value !== undefined) {
|
|
42
|
+
resHeaders['timing-allow-origin'] = timingAllowOrigin.value;
|
|
43
|
+
}
|
|
44
|
+
if (allowOrigin.varies || timingAllowOrigin.varies) {
|
|
45
|
+
const existingVary = flattenStandardHeader(resHeaders.vary);
|
|
46
|
+
if (!existingVary?.split(',').some((v) => v.trim().toLowerCase() === 'origin')) {
|
|
47
|
+
resHeaders.vary = existingVary ? `${existingVary}, Origin` : 'Origin';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (this.options.credentials) {
|
|
51
|
+
resHeaders['access-control-allow-credentials'] = 'true';
|
|
52
|
+
}
|
|
53
|
+
if (this.options.exposeHeaders?.length) {
|
|
54
|
+
resHeaders['access-control-expose-headers'] = flattenStandardHeader(this.options.exposeHeaders);
|
|
55
|
+
}
|
|
56
|
+
return { ...result, response: { ...result.response, headers: resHeaders } };
|
|
57
|
+
};
|
|
58
|
+
const preflightInterceptor = async (interceptorOptions) => {
|
|
59
|
+
if (interceptorOptions.request.method === 'OPTIONS') {
|
|
60
|
+
const resHeaders = {};
|
|
61
|
+
if (this.options.maxAge !== undefined) {
|
|
62
|
+
resHeaders['access-control-max-age'] = this.options.maxAge.toString();
|
|
63
|
+
}
|
|
64
|
+
if (this.options.allowMethods?.length) {
|
|
65
|
+
resHeaders['access-control-allow-methods'] = flattenStandardHeader(this.options.allowMethods);
|
|
66
|
+
}
|
|
67
|
+
const allowHeaders = this.options.allowHeaders ??
|
|
68
|
+
interceptorOptions.request.headers['access-control-request-headers'];
|
|
69
|
+
if (typeof allowHeaders === 'string' || allowHeaders?.length) {
|
|
70
|
+
resHeaders['access-control-allow-headers'] = flattenStandardHeader(allowHeaders);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
matched: true,
|
|
74
|
+
response: {
|
|
75
|
+
status: 204,
|
|
76
|
+
headers: resHeaders,
|
|
77
|
+
body: undefined,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return interceptorOptions.next();
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
...options,
|
|
85
|
+
routingInterceptors: [
|
|
86
|
+
corsHeadersInterceptor,
|
|
87
|
+
preflightInterceptor,
|
|
88
|
+
...toArray(options.routingInterceptors),
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async resolveOrigin(option, origin, interceptorOptions) {
|
|
93
|
+
if (typeof option === 'function') {
|
|
94
|
+
if (origin === undefined) {
|
|
95
|
+
return { value: undefined, varies: true };
|
|
96
|
+
}
|
|
97
|
+
const allowed = toArray(await option(origin, interceptorOptions));
|
|
98
|
+
if (allowed.includes('*')) {
|
|
99
|
+
return { value: '*', varies: true };
|
|
100
|
+
}
|
|
101
|
+
return { value: allowed.includes(origin) ? origin : undefined, varies: true };
|
|
102
|
+
}
|
|
103
|
+
const allowed = toArray(await option);
|
|
104
|
+
if (allowed.includes('*')) {
|
|
105
|
+
return { value: '*', varies: false };
|
|
106
|
+
}
|
|
107
|
+
if (origin !== undefined && allowed.includes(origin)) {
|
|
108
|
+
return { value: origin, varies: true };
|
|
109
|
+
}
|
|
110
|
+
return { value: undefined, varies: allowed.length > 0 };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { StandardHandlerOptions, StandardHandlerPlugin } from '../adapters/standard/index.ts';
|
|
2
|
+
import type { Context } from '../context.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Adds Cross-Site Request Forgery (CSRF) protection that makes the safe `GET` method as
|
|
5
|
+
* secure as unsafe ones such as `POST`. It rejects `GET` requests arriving as top-level
|
|
6
|
+
* navigations initiated cross-site or from outside the browser, the only context where
|
|
7
|
+
* another site can make a browser attach `SameSite=Lax` cookies to a safe-method request.
|
|
8
|
+
* Requests matching no procedure stay unmatched, so they can fall through to whatever
|
|
9
|
+
* serves the rest of the site.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* **Note**: Requests browsers send without `SameSite=Lax` cookies, such as cross-site `fetch`
|
|
13
|
+
* and `<img>`, pass through, so procedures stay reachable from other sites. This safeguard
|
|
14
|
+
* requires authentication cookies explicitly marked `SameSite=Lax` or `SameSite=Strict`,
|
|
15
|
+
* since browsers may attach other cookies to the requests that pass.
|
|
16
|
+
*
|
|
17
|
+
* @see {@link https://orpc.dev/docs/plugins/get-method-csrf-protection | GET Method CSRF Protection Plugin}
|
|
18
|
+
*/
|
|
19
|
+
export declare class GetMethodCsrfProtectionHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
|
|
20
|
+
name: string;
|
|
21
|
+
init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
|
|
22
|
+
private isAllowed;
|
|
23
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ORPCError } from '@orpc/client';
|
|
2
|
+
import { toArray } from '@orpc/shared';
|
|
3
|
+
import { flattenStandardHeader } from '@standard-server/core';
|
|
4
|
+
/**
|
|
5
|
+
* Adds Cross-Site Request Forgery (CSRF) protection that makes the safe `GET` method as
|
|
6
|
+
* secure as unsafe ones such as `POST`. It rejects `GET` requests arriving as top-level
|
|
7
|
+
* navigations initiated cross-site or from outside the browser, the only context where
|
|
8
|
+
* another site can make a browser attach `SameSite=Lax` cookies to a safe-method request.
|
|
9
|
+
* Requests matching no procedure stay unmatched, so they can fall through to whatever
|
|
10
|
+
* serves the rest of the site.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* **Note**: Requests browsers send without `SameSite=Lax` cookies, such as cross-site `fetch`
|
|
14
|
+
* and `<img>`, pass through, so procedures stay reachable from other sites. This safeguard
|
|
15
|
+
* requires authentication cookies explicitly marked `SameSite=Lax` or `SameSite=Strict`,
|
|
16
|
+
* since browsers may attach other cookies to the requests that pass.
|
|
17
|
+
*
|
|
18
|
+
* @see {@link https://orpc.dev/docs/plugins/get-method-csrf-protection | GET Method CSRF Protection Plugin}
|
|
19
|
+
*/
|
|
20
|
+
export class GetMethodCsrfProtectionHandlerPlugin {
|
|
21
|
+
name = '~get-method-csrf-protection';
|
|
22
|
+
init(options) {
|
|
23
|
+
/**
|
|
24
|
+
* Batch sub-requests are judged individually here, which is safe: navigations cannot
|
|
25
|
+
* carry the custom `orpc-batch` header, and the batch plugin's default `mapSubrequest`
|
|
26
|
+
* keeps the transport headers authoritative over client-authored sub-request headers.
|
|
27
|
+
*/
|
|
28
|
+
const interceptor = (interceptorOptions) => {
|
|
29
|
+
if (this.isAllowed(interceptorOptions)) {
|
|
30
|
+
return interceptorOptions.next();
|
|
31
|
+
}
|
|
32
|
+
throw new ORPCError('FORBIDDEN', { message: 'Request blocked by CSRF protection.' });
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
...options,
|
|
36
|
+
interceptors: [interceptor, ...toArray(options.interceptors)],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
isAllowed({ request }) {
|
|
40
|
+
// Navigations can only use `GET` or `POST` per the HTML spec, and `POST` is unsafe, so
|
|
41
|
+
// `GET` is the only method `SameSite=Lax` cookies ride cross-site.
|
|
42
|
+
if (request.method !== 'GET') {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
const site = flattenStandardHeader(request.headers['sec-fetch-site'])?.toLowerCase();
|
|
46
|
+
// Absent for non-browser clients, browsers too old to send Fetch Metadata, and any browser
|
|
47
|
+
// over plain HTTP, where the headers are never appended. Nothing to judge in those cases.
|
|
48
|
+
// Same-site origins are trusted, matching the `SameSite` cookie model where the site is
|
|
49
|
+
// the trust boundary.
|
|
50
|
+
if (site === undefined || site === 'same-origin' || site === 'same-site') {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
// Cross-site, and on browser-initiated requests such as email links, `Lax` cookies ride
|
|
54
|
+
// only top-level navigations: mode `navigate` targeting dest `document`. Embedded
|
|
55
|
+
// navigations such as `<iframe>` pass, since they are not top-level. Browsers send both
|
|
56
|
+
// headers together, so a stripped one blocks the request rather than bypassing the check.
|
|
57
|
+
const mode = flattenStandardHeader(request.headers['sec-fetch-mode'])?.toLowerCase();
|
|
58
|
+
const dest = flattenStandardHeader(request.headers['sec-fetch-dest'])?.toLowerCase();
|
|
59
|
+
if (mode === undefined || dest === undefined) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return mode !== 'navigate' || dest !== 'document';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { StandardHandlerOptions, StandardHandlerPlugin } from '../adapters/standard/index.ts';
|
|
2
|
+
import type { Context } from '../context.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Answers `HEAD` requests with the headers of the matching `GET` procedure, like Fastify's
|
|
5
|
+
* `exposeHeadRoutes`, so clients can check a resource's existence, type, and size without
|
|
6
|
+
* downloading it. A procedure routed as `HEAD` itself takes precedence over the `GET` one.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* - `HEAD` responses never carry a body: the plugin discards it, cancelling streams and
|
|
10
|
+
* event iterators, and keeps the headers it implies, such as `Content-Type`, and
|
|
11
|
+
* `Content-Length` when its size is known up front.
|
|
12
|
+
* - `GET` procedures still need to be reachable, so with `RPCHandler`, `GET` must be in `allowMethods`.
|
|
13
|
+
*
|
|
14
|
+
* @see {@link https://orpc.dev/docs/plugins/head-method | HEAD Method Plugin}
|
|
15
|
+
*/
|
|
16
|
+
export declare class HeadMethodHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
|
|
17
|
+
name: string;
|
|
18
|
+
/**
|
|
19
|
+
* Discard the body after response compression, so `HEAD` reports the same `Content-Encoding` as `GET`.
|
|
20
|
+
*/
|
|
21
|
+
after: string[];
|
|
22
|
+
init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { isAsyncIteratorObject, toArray } from '@orpc/shared';
|
|
2
|
+
import { cancelStandardBody, getRequiredStandardBodyHint } from '@standard-server/core';
|
|
3
|
+
import { toFetchBody } from '@standard-server/fetch';
|
|
4
|
+
const textEncoder = /* @__PURE__ */ new TextEncoder();
|
|
5
|
+
/**
|
|
6
|
+
* Answers `HEAD` requests with the headers of the matching `GET` procedure, like Fastify's
|
|
7
|
+
* `exposeHeadRoutes`, so clients can check a resource's existence, type, and size without
|
|
8
|
+
* downloading it. A procedure routed as `HEAD` itself takes precedence over the `GET` one.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* - `HEAD` responses never carry a body: the plugin discards it, cancelling streams and
|
|
12
|
+
* event iterators, and keeps the headers it implies, such as `Content-Type`, and
|
|
13
|
+
* `Content-Length` when its size is known up front.
|
|
14
|
+
* - `GET` procedures still need to be reachable, so with `RPCHandler`, `GET` must be in `allowMethods`.
|
|
15
|
+
*
|
|
16
|
+
* @see {@link https://orpc.dev/docs/plugins/head-method | HEAD Method Plugin}
|
|
17
|
+
*/
|
|
18
|
+
export class HeadMethodHandlerPlugin {
|
|
19
|
+
name = '~head-method';
|
|
20
|
+
/**
|
|
21
|
+
* Discard the body after response compression, so `HEAD` reports the same `Content-Encoding` as `GET`.
|
|
22
|
+
*/
|
|
23
|
+
after = ['~response-compression'];
|
|
24
|
+
init(options) {
|
|
25
|
+
// oxlint-disable-next-line unicorn/consistent-function-scoping
|
|
26
|
+
const bodyInterceptor = async ({ next, request }) => {
|
|
27
|
+
if (request.method !== 'HEAD') {
|
|
28
|
+
return next();
|
|
29
|
+
}
|
|
30
|
+
const result = await next();
|
|
31
|
+
if (!result.matched) {
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
return { ...result, response: toHeadResponse(result.response) };
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Placed last, so falling back to `GET` repeats routing rather than the routing interceptors before this one.
|
|
38
|
+
*/
|
|
39
|
+
// oxlint-disable-next-line unicorn/consistent-function-scoping
|
|
40
|
+
const fallbackInterceptor = async (interceptorOptions) => {
|
|
41
|
+
const { next, request } = interceptorOptions;
|
|
42
|
+
if (request.method !== 'HEAD') {
|
|
43
|
+
return next();
|
|
44
|
+
}
|
|
45
|
+
const result = await next();
|
|
46
|
+
if (result.matched) {
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
return next({
|
|
50
|
+
...interceptorOptions,
|
|
51
|
+
request: { ...request, method: 'GET' },
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
...options,
|
|
56
|
+
routingInterceptors: [
|
|
57
|
+
bodyInterceptor,
|
|
58
|
+
...toArray(options.routingInterceptors),
|
|
59
|
+
fallbackInterceptor,
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Replaces the body with an empty stream, keeping the headers adapters derive from it.
|
|
66
|
+
* An empty stream rather than no body, because adapters drop `Content-Type` and `Content-Length` when the body is `undefined`,
|
|
67
|
+
* and an empty `Standard-Server` hint rather than none, so they do not hint the stream as `octet-stream`.
|
|
68
|
+
*/
|
|
69
|
+
function toHeadResponse(response) {
|
|
70
|
+
const { body } = response;
|
|
71
|
+
if (body === undefined) {
|
|
72
|
+
return response;
|
|
73
|
+
}
|
|
74
|
+
if (isAsyncIteratorObject(body)) {
|
|
75
|
+
discardBody(body);
|
|
76
|
+
const headers = { ...response.headers, 'content-length': undefined };
|
|
77
|
+
headers['content-type'] ??= 'text/event-stream';
|
|
78
|
+
headers['standard-server'] = getRequiredStandardBodyHint('event-stream', headers) ?? [];
|
|
79
|
+
return { ...response, headers, body: createEmptyStream() };
|
|
80
|
+
}
|
|
81
|
+
const [encoded, headers] = toFetchBody(body, response.headers);
|
|
82
|
+
if (encoded instanceof ReadableStream) {
|
|
83
|
+
discardBody(encoded);
|
|
84
|
+
}
|
|
85
|
+
else if (encoded instanceof FormData) {
|
|
86
|
+
// The encoded body is never cancelled, since undici rejects unhandled when cancelled mid-serialization
|
|
87
|
+
headers['standard-server'] = [];
|
|
88
|
+
headers['content-type'] = new Response(encoded).headers.get('content-type');
|
|
89
|
+
}
|
|
90
|
+
else if (typeof encoded === 'string' || encoded instanceof URLSearchParams) {
|
|
91
|
+
headers['standard-server'] ??= [];
|
|
92
|
+
headers['content-length'] = textEncoder.encode(encoded.toString()).byteLength.toString();
|
|
93
|
+
}
|
|
94
|
+
return { ...response, headers, body: createEmptyStream() };
|
|
95
|
+
}
|
|
96
|
+
function createEmptyStream() {
|
|
97
|
+
return new ReadableStream({ start: (controller) => controller.close() });
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Nothing reads the body anymore, so a failure to cancel it has nowhere to go.
|
|
101
|
+
*/
|
|
102
|
+
function discardBody(body) {
|
|
103
|
+
cancelStandardBody(body).catch(() => { });
|
|
104
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export * from './batch.ts';
|
|
2
|
+
export * from './cors.ts';
|
|
3
|
+
export {
|
|
4
|
+
/**
|
|
5
|
+
* @deprecated Use `CORSHandlerPlugin` instead.
|
|
6
|
+
*/
|
|
7
|
+
CORSHandlerPlugin as CORSPlugin, } from './cors.ts';
|
|
8
|
+
export * from './get-method-csrf-protection.ts';
|
|
9
|
+
export * from './head-method.ts';
|
|
10
|
+
export * from './method-override.ts';
|
|
11
|
+
export * from './request-compression.ts';
|
|
12
|
+
export * from './request-headers.ts';
|
|
13
|
+
export {
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated Use `RequestHeadersHandlerPlugin` instead.
|
|
16
|
+
*/
|
|
17
|
+
RequestHeadersHandlerPlugin as RequestHeadersPlugin, } from './request-headers.ts';
|
|
18
|
+
export type {
|
|
19
|
+
/**
|
|
20
|
+
* @deprecated Use `RequestHeadersHandlerPluginContext` instead.
|
|
21
|
+
*/
|
|
22
|
+
RequestHeadersHandlerPluginContext as RequestHeadersPluginContext, } from './request-headers.ts';
|
|
23
|
+
export * from './request-limit.ts';
|
|
24
|
+
export * from './response-compression.ts';
|
|
25
|
+
export * from './response-headers.ts';
|
|
26
|
+
export {
|
|
27
|
+
/**
|
|
28
|
+
* @deprecated Use `ResponseHeadersHandlerPlugin` instead.
|
|
29
|
+
*/
|
|
30
|
+
ResponseHeadersHandlerPlugin as ResponseHeadersPlugin, } from './response-headers.ts';
|
|
31
|
+
export type {
|
|
32
|
+
/**
|
|
33
|
+
* @deprecated Use `ResponseHeadersHandlerPluginContext` instead.
|
|
34
|
+
*/
|
|
35
|
+
ResponseHeadersHandlerPluginContext as ResponseHeadersPluginContext, } from './response-headers.ts';
|
|
36
|
+
export * from './rethrow.ts';
|
|
37
|
+
export * from './signal-sync.ts';
|
|
38
|
+
export * from './timeout.ts';
|
|
39
|
+
export { BATCH_HEADER, DEFAULT_BATCH_MAX_SIZE } from '@orpc/client/plugins';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export * from './batch.js';
|
|
2
|
+
export * from './cors.js';
|
|
3
|
+
export {
|
|
4
|
+
/**
|
|
5
|
+
* @deprecated Use `CORSHandlerPlugin` instead.
|
|
6
|
+
*/
|
|
7
|
+
CORSHandlerPlugin as CORSPlugin, } from './cors.js';
|
|
8
|
+
export * from './get-method-csrf-protection.js';
|
|
9
|
+
export * from './head-method.js';
|
|
10
|
+
export * from './method-override.js';
|
|
11
|
+
export * from './request-compression.js';
|
|
12
|
+
export * from './request-headers.js';
|
|
13
|
+
export {
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated Use `RequestHeadersHandlerPlugin` instead.
|
|
16
|
+
*/
|
|
17
|
+
RequestHeadersHandlerPlugin as RequestHeadersPlugin, } from './request-headers.js';
|
|
18
|
+
export * from './request-limit.js';
|
|
19
|
+
export * from './response-compression.js';
|
|
20
|
+
export * from './response-headers.js';
|
|
21
|
+
export {
|
|
22
|
+
/**
|
|
23
|
+
* @deprecated Use `ResponseHeadersHandlerPlugin` instead.
|
|
24
|
+
*/
|
|
25
|
+
ResponseHeadersHandlerPlugin as ResponseHeadersPlugin, } from './response-headers.js';
|
|
26
|
+
export * from './rethrow.js';
|
|
27
|
+
export * from './signal-sync.js';
|
|
28
|
+
export * from './timeout.js';
|
|
29
|
+
export { BATCH_HEADER, DEFAULT_BATCH_MAX_SIZE } from '@orpc/client/plugins';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { StandardMethod } from '@standard-server/core';
|
|
2
|
+
import type { StandardHandlerOptions, StandardHandlerPlugin } from '../adapters/standard/index.ts';
|
|
3
|
+
import type { Context } from '../context.ts';
|
|
4
|
+
export interface MethodOverrideHandlerPluginOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The query parameter carrying the override method.
|
|
7
|
+
*
|
|
8
|
+
* @default 'method'
|
|
9
|
+
*/
|
|
10
|
+
param?: string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* The methods a POST request may be overridden to.
|
|
13
|
+
*
|
|
14
|
+
* GET and HEAD are excluded by default because they switch input decoding
|
|
15
|
+
* from the request body to the query string and widen the CSRF surface.
|
|
16
|
+
*
|
|
17
|
+
* @default ['PUT', 'PATCH', 'DELETE']
|
|
18
|
+
*/
|
|
19
|
+
methods?: readonly StandardMethod[] | undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Overrides the HTTP method of a POST request based on a query parameter,
|
|
23
|
+
* so HTML forms (which only support GET and POST) can invoke procedures
|
|
24
|
+
* routed as PUT, PATCH, or DELETE.
|
|
25
|
+
*
|
|
26
|
+
* @see {@link https://orpc.dev/docs/plugins/method-override | Method Override Plugin}
|
|
27
|
+
*/
|
|
28
|
+
export declare class MethodOverrideHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
|
|
29
|
+
name: string;
|
|
30
|
+
/**
|
|
31
|
+
* Should override batch sub-request methods, not the original batch request.
|
|
32
|
+
*/
|
|
33
|
+
before: string[];
|
|
34
|
+
private readonly param;
|
|
35
|
+
private readonly methods;
|
|
36
|
+
constructor(options?: MethodOverrideHandlerPluginOptions);
|
|
37
|
+
init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { toArray } from '@orpc/shared';
|
|
2
|
+
import { parseStandardUrl } from '@standard-server/core';
|
|
3
|
+
/**
|
|
4
|
+
* Overrides the HTTP method of a POST request based on a query parameter,
|
|
5
|
+
* so HTML forms (which only support GET and POST) can invoke procedures
|
|
6
|
+
* routed as PUT, PATCH, or DELETE.
|
|
7
|
+
*
|
|
8
|
+
* @see {@link https://orpc.dev/docs/plugins/method-override | Method Override Plugin}
|
|
9
|
+
*/
|
|
10
|
+
export class MethodOverrideHandlerPlugin {
|
|
11
|
+
name = '~method-override';
|
|
12
|
+
/**
|
|
13
|
+
* Should override batch sub-request methods, not the original batch request.
|
|
14
|
+
*/
|
|
15
|
+
before = ['~batch'];
|
|
16
|
+
param;
|
|
17
|
+
methods;
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.param = options.param ?? 'method';
|
|
20
|
+
this.methods = new Set((options.methods ?? ['PUT', 'PATCH', 'DELETE']).map((method) => method.toUpperCase()));
|
|
21
|
+
}
|
|
22
|
+
init(options) {
|
|
23
|
+
const routingInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
24
|
+
const { request } = interceptorOptions;
|
|
25
|
+
if (request.method !== 'POST') {
|
|
26
|
+
return next();
|
|
27
|
+
}
|
|
28
|
+
const [pathname, search, hash] = parseStandardUrl(request.url);
|
|
29
|
+
const params = new URLSearchParams(search);
|
|
30
|
+
const raw = params.getAll(this.param).at(-1);
|
|
31
|
+
if (raw === undefined) {
|
|
32
|
+
return next();
|
|
33
|
+
}
|
|
34
|
+
const method = raw.toUpperCase();
|
|
35
|
+
if (!this.methods.has(method)) {
|
|
36
|
+
return next();
|
|
37
|
+
}
|
|
38
|
+
params.delete(this.param);
|
|
39
|
+
const url = `${pathname}${params.size ? `?${params}` : ''}${hash ?? ''}`;
|
|
40
|
+
return next({
|
|
41
|
+
...interceptorOptions,
|
|
42
|
+
request: { ...request, method, url },
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
...options,
|
|
47
|
+
routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { StandardHandlerOptions, StandardHandlerPlugin } from '../adapters/standard/index.ts';
|
|
2
|
+
import type { Context } from '../context.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Decompresses incoming request bodies based on the Content-Encoding header,
|
|
5
|
+
* supporting gzip, deflate, and deflate-raw.
|
|
6
|
+
*
|
|
7
|
+
* @see {@link https://orpc.dev/docs/plugins/request-compression | Request Compression Plugin}
|
|
8
|
+
*/
|
|
9
|
+
export declare class RequestCompressionHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
|
|
10
|
+
name: string;
|
|
11
|
+
/**
|
|
12
|
+
* Should decompress the original batch request body instead of sub-requests.
|
|
13
|
+
*/
|
|
14
|
+
after: string[];
|
|
15
|
+
init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ORPCError } from '@orpc/client';
|
|
2
|
+
import { toArray } from '@orpc/shared';
|
|
3
|
+
import { flattenStandardHeader } from '@standard-server/core';
|
|
4
|
+
import { toFetchHeaders, toStandardBody } from '@standard-server/fetch';
|
|
5
|
+
/**
|
|
6
|
+
* Each coding adds a decompressor to the chain, so an unbounded list lets a tiny
|
|
7
|
+
* request cost unbounded CPU and memory. Same limit as undici and curl.
|
|
8
|
+
*/
|
|
9
|
+
const MAX_CONTENT_ENCODINGS = 5;
|
|
10
|
+
/**
|
|
11
|
+
* Decompresses incoming request bodies based on the Content-Encoding header,
|
|
12
|
+
* supporting gzip, deflate, and deflate-raw.
|
|
13
|
+
*
|
|
14
|
+
* @see {@link https://orpc.dev/docs/plugins/request-compression | Request Compression Plugin}
|
|
15
|
+
*/
|
|
16
|
+
export class RequestCompressionHandlerPlugin {
|
|
17
|
+
name = '~request-compression';
|
|
18
|
+
/**
|
|
19
|
+
* Should decompress the original batch request body instead of sub-requests.
|
|
20
|
+
*/
|
|
21
|
+
after = ['~batch'];
|
|
22
|
+
init(options) {
|
|
23
|
+
// oxlint-disable-next-line unicorn/consistent-function-scoping
|
|
24
|
+
const routingInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
25
|
+
const encodings = parseContentEncodings(flattenStandardHeader(interceptorOptions.request.headers['content-encoding']));
|
|
26
|
+
if (encodings.length === 0 || !encodings.every(isSupportedEncoding)) {
|
|
27
|
+
return next();
|
|
28
|
+
}
|
|
29
|
+
const decompressedHeaders = {
|
|
30
|
+
...interceptorOptions.request.headers,
|
|
31
|
+
'content-length': undefined,
|
|
32
|
+
'content-encoding': undefined,
|
|
33
|
+
};
|
|
34
|
+
return next({
|
|
35
|
+
...interceptorOptions,
|
|
36
|
+
request: {
|
|
37
|
+
...interceptorOptions.request,
|
|
38
|
+
headers: decompressedHeaders,
|
|
39
|
+
async resolveBody(hint) {
|
|
40
|
+
const stream = await interceptorOptions.request.resolveBody('octet-stream');
|
|
41
|
+
// adapter might not support hint (e.g peer adapter)
|
|
42
|
+
if (!(stream instanceof ReadableStream)) {
|
|
43
|
+
return stream;
|
|
44
|
+
}
|
|
45
|
+
if (encodings.length > MAX_CONTENT_ENCODINGS) {
|
|
46
|
+
throw new ORPCError('UNSUPPORTED_MEDIA_TYPE', {
|
|
47
|
+
message: 'Too many content encodings.',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
let decompressedStream = stream;
|
|
51
|
+
for (let i = encodings.length - 1; i >= 0; i--) {
|
|
52
|
+
decompressedStream = decompressedStream.pipeThrough(new DecompressionStream(encodings[i]));
|
|
53
|
+
}
|
|
54
|
+
const response = new Response(decompressedStream, {
|
|
55
|
+
headers: toFetchHeaders(decompressedHeaders),
|
|
56
|
+
});
|
|
57
|
+
return toStandardBody(response, { hint });
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
...options,
|
|
64
|
+
routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const SUPPORTED_ENCODINGS = ['gzip', 'deflate', 'deflate-raw'];
|
|
69
|
+
function isSupportedEncoding(encoding) {
|
|
70
|
+
return SUPPORTED_ENCODINGS.includes(encoding);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Parse Content-Encoding into ordered codings (order applied).
|
|
74
|
+
*
|
|
75
|
+
* @see https://www.rfc-editor.org/rfc/rfc9110.html#name-content-encoding
|
|
76
|
+
*/
|
|
77
|
+
function parseContentEncodings(header) {
|
|
78
|
+
if (header === undefined) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return header.split(',').map((part) => part.trim().toLowerCase());
|
|
82
|
+
}
|