@orpc/server 2.0.0-beta.47 → 2.0.0-beta.48
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 +2 -3
- package/dist/adapters/aws-lambda/handler.d.mts +25 -0
- package/dist/adapters/aws-lambda/handler.mjs +21 -0
- package/dist/adapters/aws-lambda/index.d.mts +14 -49
- package/dist/adapters/aws-lambda/index.mjs +2 -39
- package/dist/adapters/aws-lambda/rpc-handler.d.mts +18 -0
- package/dist/adapters/aws-lambda/rpc-handler.mjs +10 -0
- package/dist/adapters/crossws/handler.d.mts +41 -0
- package/dist/adapters/crossws/handler.mjs +38 -0
- package/dist/adapters/crossws/index.d.mts +2 -63
- package/dist/adapters/crossws/index.mjs +2 -68
- package/dist/adapters/crossws/rpc-handler.d.mts +15 -0
- package/dist/adapters/crossws/rpc-handler.mjs +10 -0
- package/dist/adapters/fastify/handler.d.mts +21 -0
- package/dist/adapters/fastify/handler.mjs +20 -0
- package/dist/adapters/fastify/index.d.mts +2 -42
- package/dist/adapters/fastify/index.mjs +2 -39
- package/dist/adapters/fastify/rpc-handler.d.mts +14 -0
- package/dist/adapters/fastify/rpc-handler.mjs +10 -0
- package/dist/adapters/fetch/handler.d.mts +23 -0
- package/dist/adapters/fetch/handler.mjs +22 -0
- package/dist/adapters/fetch/index.d.mts +6 -46
- package/dist/adapters/fetch/index.mjs +4 -41
- package/dist/adapters/fetch/rpc-handler.d.mts +16 -0
- package/dist/adapters/fetch/rpc-handler.mjs +10 -0
- package/dist/adapters/message-port/handler.d.mts +57 -0
- package/dist/adapters/message-port/handler.mjs +62 -0
- package/dist/adapters/message-port/index.d.mts +2 -77
- package/dist/adapters/message-port/index.mjs +2 -87
- package/dist/adapters/message-port/rpc-handler.d.mts +15 -0
- package/dist/adapters/message-port/rpc-handler.mjs +10 -0
- package/dist/adapters/node/handler.d.mts +21 -0
- package/dist/adapters/node/handler.mjs +20 -0
- package/dist/adapters/node/index.d.mts +6 -43
- package/dist/adapters/node/index.mjs +4 -40
- package/dist/adapters/node/rpc-handler.d.mts +15 -0
- package/dist/adapters/node/rpc-handler.mjs +10 -0
- package/dist/adapters/standard/codec.d.mts +16 -0
- package/dist/adapters/standard/codec.mjs +1 -0
- package/dist/adapters/standard/handler.d.mts +68 -0
- package/dist/adapters/standard/handler.mjs +185 -0
- package/dist/adapters/standard/index.d.mts +6 -95
- package/dist/adapters/standard/index.mjs +5 -8
- package/dist/adapters/standard/plugin.d.mts +32 -0
- package/dist/adapters/standard/plugin.mjs +16 -0
- package/dist/adapters/standard/rpc-handler-codec.d.mts +42 -0
- package/dist/adapters/standard/rpc-handler-codec.mjs +51 -0
- package/dist/adapters/standard/rpc-matcher.d.mts +53 -0
- package/dist/adapters/standard/rpc-matcher.mjs +140 -0
- package/dist/adapters/standard/utils.d.mts +10 -0
- package/dist/adapters/standard/utils.mjs +6 -0
- package/dist/adapters/standard-peer/index.d.mts +1 -11
- package/dist/adapters/standard-peer/index.mjs +1 -10
- package/dist/adapters/standard-peer/utils.d.mts +4 -0
- package/dist/adapters/standard-peer/utils.mjs +11 -0
- package/dist/adapters/websocket/handler.d.mts +55 -0
- package/dist/adapters/websocket/handler.mjs +55 -0
- package/dist/adapters/websocket/index.d.mts +2 -77
- package/dist/adapters/websocket/index.mjs +2 -87
- package/dist/adapters/websocket/rpc-handler.d.mts +15 -0
- package/dist/adapters/websocket/rpc-handler.mjs +10 -0
- package/dist/builder-variants.d.mts +267 -0
- package/dist/builder-variants.mjs +1 -0
- package/dist/builder.d.mts +141 -0
- package/dist/builder.mjs +145 -0
- package/dist/constants.d.mts +2 -0
- package/dist/constants.mjs +2 -0
- package/dist/context.d.mts +13 -0
- package/dist/context.mjs +1 -0
- package/dist/extensions/callable.d.mts +16 -9
- package/dist/extensions/callable.mjs +5 -9
- package/dist/helpers/base64url.d.mts +31 -0
- package/dist/helpers/base64url.mjs +25 -0
- package/dist/helpers/cookie.d.mts +50 -0
- package/dist/helpers/cookie.mjs +35 -0
- package/dist/helpers/encryption.d.mts +28 -0
- package/dist/helpers/encryption.mjs +62 -0
- package/dist/helpers/index.d.mts +4 -167
- package/dist/helpers/index.mjs +4 -200
- package/dist/helpers/signing.d.mts +50 -0
- package/dist/helpers/signing.mjs +43 -0
- package/dist/implementer-procedure.d.mts +36 -0
- package/dist/implementer-procedure.mjs +38 -0
- package/dist/implementer-router.d.mts +78 -0
- package/dist/implementer-router.mjs +77 -0
- package/dist/implementer.d.mts +22 -0
- package/dist/implementer.mjs +30 -0
- package/dist/index.d.mts +57 -476
- package/dist/index.mjs +25 -458
- package/dist/lazy.d.mts +20 -0
- package/dist/lazy.mjs +22 -0
- package/dist/middleware-decorated.d.mts +27 -0
- package/dist/middleware-decorated.mjs +65 -0
- package/dist/middleware.d.mts +70 -0
- package/dist/middleware.mjs +1 -0
- package/dist/plugins/batch.d.mts +104 -0
- package/dist/plugins/batch.mjs +373 -0
- package/dist/plugins/cors.d.mts +67 -0
- package/dist/plugins/cors.mjs +134 -0
- package/dist/plugins/get-method-csrf-protection.d.mts +23 -0
- package/dist/plugins/get-method-csrf-protection.mjs +45 -0
- package/dist/plugins/head-method.d.mts +23 -0
- package/dist/plugins/head-method.mjs +95 -0
- package/dist/plugins/index.d.mts +39 -432
- package/dist/plugins/index.mjs +23 -965
- package/dist/plugins/method-override.d.mts +38 -0
- package/dist/plugins/method-override.mjs +50 -0
- package/dist/plugins/request-compression.d.mts +16 -0
- package/dist/plugins/request-compression.mjs +67 -0
- package/dist/plugins/request-headers.d.mts +22 -0
- package/dist/plugins/request-headers.mjs +21 -0
- package/dist/plugins/request-limit.d.mts +30 -0
- package/dist/plugins/request-limit.mjs +53 -0
- package/dist/plugins/response-compression.d.mts +59 -0
- package/dist/plugins/response-compression.mjs +172 -0
- package/dist/plugins/response-headers.d.mts +30 -0
- package/dist/plugins/response-headers.mjs +34 -0
- package/dist/plugins/rethrow.d.mts +33 -0
- package/dist/plugins/rethrow.mjs +46 -0
- package/dist/plugins/signal-sync.d.mts +13 -0
- package/dist/plugins/signal-sync.mjs +84 -0
- package/dist/plugins/timeout.d.mts +37 -0
- package/dist/plugins/timeout.mjs +56 -0
- package/dist/procedure-client.d.mts +41 -0
- package/dist/procedure-client.mjs +157 -0
- package/dist/procedure-decorated.d.mts +20 -0
- package/dist/procedure-decorated.mjs +41 -0
- package/dist/procedure-utils.d.mts +52 -0
- package/dist/procedure-utils.mjs +33 -0
- package/dist/procedure.d.mts +93 -0
- package/dist/procedure.mjs +20 -0
- package/dist/router-client.d.mts +27 -0
- package/dist/router-client.mjs +35 -0
- package/dist/router-hidden.d.mts +5 -0
- package/dist/router-hidden.mjs +12 -0
- package/dist/router-utils.d.mts +59 -0
- package/dist/router-utils.mjs +191 -0
- package/dist/router.d.mts +61 -0
- package/dist/router.mjs +1 -0
- package/package.json +11 -11
- package/dist/adapters/aws-lambda/index.d.ts +0 -51
- package/dist/adapters/crossws/index.d.ts +0 -63
- package/dist/adapters/fastify/index.d.ts +0 -42
- package/dist/adapters/fetch/index.d.ts +0 -47
- package/dist/adapters/message-port/index.d.ts +0 -77
- package/dist/adapters/node/index.d.ts +0 -44
- package/dist/adapters/standard/index.d.ts +0 -95
- package/dist/adapters/standard-peer/index.d.ts +0 -11
- package/dist/adapters/websocket/index.d.ts +0 -77
- package/dist/extensions/callable.d.ts +0 -10
- package/dist/helpers/index.d.ts +0 -167
- package/dist/index.d.ts +0 -476
- package/dist/plugins/index.d.ts +0 -432
- package/dist/shared/server.1ExWFMze.mjs +0 -69
- package/dist/shared/server.9B1jEm5a.d.mts +0 -105
- package/dist/shared/server.BB464y-6.mjs +0 -207
- package/dist/shared/server.BC5BIu02.d.mts +0 -73
- package/dist/shared/server.BSYGskId.d.ts +0 -14
- package/dist/shared/server.BvG3boBj.d.mts +0 -14
- package/dist/shared/server.CTV_mC2P.d.mts +0 -34
- package/dist/shared/server.CYbIpHbH.mjs +0 -239
- package/dist/shared/server.CgBF3Vj1.d.ts +0 -34
- package/dist/shared/server.Chq5FpXf.mjs +0 -353
- package/dist/shared/server.DFPxP2v3.d.mts +0 -166
- package/dist/shared/server.DFPxP2v3.d.ts +0 -166
- package/dist/shared/server.DOg7ke6y.d.ts +0 -105
- package/dist/shared/server.Dz7j9BEh.d.ts +0 -73
- package/dist/shared/server.W91HSRkE.mjs +0 -9
- package/dist/shared/server.sBQSgqK3.mjs +0 -41
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { StandardMethod } from "@standard-server/core";
|
|
2
|
+
import type { StandardHandlerOptions, StandardHandlerPlugin } from "../adapters/standard/index.mjs";
|
|
3
|
+
import type { Context } from "../context.mjs";
|
|
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
|
+
export class MethodOverrideHandlerPlugin {
|
|
5
|
+
name = "~method-override";
|
|
6
|
+
|
|
7
|
+
before = ["~batch"];
|
|
8
|
+
param;
|
|
9
|
+
methods;
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.param = options.param ?? "method";
|
|
12
|
+
this.methods = new Set((options.methods ?? [
|
|
13
|
+
"PUT",
|
|
14
|
+
"PATCH",
|
|
15
|
+
"DELETE"
|
|
16
|
+
]).map((method) => method.toUpperCase()));
|
|
17
|
+
}
|
|
18
|
+
init(options) {
|
|
19
|
+
const routingInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
20
|
+
const { request } = interceptorOptions;
|
|
21
|
+
if (request.method !== "POST") {
|
|
22
|
+
return next();
|
|
23
|
+
}
|
|
24
|
+
const [pathname, search, hash] = parseStandardUrl(request.url);
|
|
25
|
+
const params = new URLSearchParams(search);
|
|
26
|
+
const raw = params.getAll(this.param).at(-1);
|
|
27
|
+
if (raw === undefined) {
|
|
28
|
+
return next();
|
|
29
|
+
}
|
|
30
|
+
const method = raw.toUpperCase();
|
|
31
|
+
if (!this.methods.has(method)) {
|
|
32
|
+
return next();
|
|
33
|
+
}
|
|
34
|
+
params.delete(this.param);
|
|
35
|
+
const url = `${pathname}${params.size ? `?${params}` : ""}${hash ?? ""}`;
|
|
36
|
+
return next({
|
|
37
|
+
...interceptorOptions,
|
|
38
|
+
request: {
|
|
39
|
+
...request,
|
|
40
|
+
method,
|
|
41
|
+
url
|
|
42
|
+
}
|
|
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.mjs";
|
|
2
|
+
import type { Context } from "../context.mjs";
|
|
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,67 @@
|
|
|
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
|
+
const MAX_CONTENT_ENCODINGS = 5;
|
|
7
|
+
|
|
8
|
+
export class RequestCompressionHandlerPlugin {
|
|
9
|
+
name = "~request-compression";
|
|
10
|
+
|
|
11
|
+
after = ["~batch"];
|
|
12
|
+
init(options) {
|
|
13
|
+
const routingInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
14
|
+
const encodings = parseContentEncodings(flattenStandardHeader(interceptorOptions.request.headers["content-encoding"]));
|
|
15
|
+
if (encodings.length === 0 || !encodings.every(isSupportedEncoding)) {
|
|
16
|
+
return next();
|
|
17
|
+
}
|
|
18
|
+
const decompressedHeaders = {
|
|
19
|
+
...interceptorOptions.request.headers,
|
|
20
|
+
"content-length": undefined,
|
|
21
|
+
"content-encoding": undefined
|
|
22
|
+
};
|
|
23
|
+
return next({
|
|
24
|
+
...interceptorOptions,
|
|
25
|
+
request: {
|
|
26
|
+
...interceptorOptions.request,
|
|
27
|
+
headers: decompressedHeaders,
|
|
28
|
+
async resolveBody(hint) {
|
|
29
|
+
const stream = await interceptorOptions.request.resolveBody("octet-stream");
|
|
30
|
+
|
|
31
|
+
if (!(stream instanceof ReadableStream)) {
|
|
32
|
+
return stream;
|
|
33
|
+
}
|
|
34
|
+
if (encodings.length > MAX_CONTENT_ENCODINGS) {
|
|
35
|
+
throw new ORPCError("UNSUPPORTED_MEDIA_TYPE", { message: "Too many content encodings." });
|
|
36
|
+
}
|
|
37
|
+
let decompressedStream = stream;
|
|
38
|
+
for (let i = encodings.length - 1; i >= 0; i--) {
|
|
39
|
+
decompressedStream = decompressedStream.pipeThrough(new DecompressionStream(encodings[i]));
|
|
40
|
+
}
|
|
41
|
+
const response = new Response(decompressedStream, { headers: toFetchHeaders(decompressedHeaders) });
|
|
42
|
+
return toStandardBody(response, { hint });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
...options,
|
|
49
|
+
routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const SUPPORTED_ENCODINGS = [
|
|
54
|
+
"gzip",
|
|
55
|
+
"deflate",
|
|
56
|
+
"deflate-raw"
|
|
57
|
+
];
|
|
58
|
+
function isSupportedEncoding(encoding) {
|
|
59
|
+
return SUPPORTED_ENCODINGS.includes(encoding);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseContentEncodings(header) {
|
|
63
|
+
if (header === undefined) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return header.split(",").map((part) => part.trim().toLowerCase());
|
|
67
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { StandardHandlerOptions, StandardHandlerPlugin } from "../adapters/standard/index.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* The context shape into which the Request Headers Plugin injects `reqHeaders`.
|
|
4
|
+
*
|
|
5
|
+
* @see {@link https://orpc.dev/docs/plugins/request-headers | Request Headers Plugin}
|
|
6
|
+
*/
|
|
7
|
+
export interface RequestHeadersHandlerPluginContext {
|
|
8
|
+
/**
|
|
9
|
+
* Request headers as a Headers instance. This is injected by the Request Headers Plugin.
|
|
10
|
+
*/
|
|
11
|
+
reqHeaders?: Headers | undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The Request Headers Plugin injects a `reqHeaders` instance into the context,
|
|
15
|
+
* allowing access to request headers in oRPC.
|
|
16
|
+
*
|
|
17
|
+
* @see {@link https://orpc.dev/docs/plugins/request-headers | Request Headers Plugin}
|
|
18
|
+
*/
|
|
19
|
+
export declare class RequestHeadersHandlerPlugin<T extends RequestHeadersHandlerPluginContext> implements StandardHandlerPlugin<T> {
|
|
20
|
+
name: string;
|
|
21
|
+
init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { toArray } from "@orpc/shared";
|
|
2
|
+
import { toFetchHeaders } from "@standard-server/fetch";
|
|
3
|
+
|
|
4
|
+
export class RequestHeadersHandlerPlugin {
|
|
5
|
+
name = "~request-headers";
|
|
6
|
+
init(options) {
|
|
7
|
+
return {
|
|
8
|
+
...options,
|
|
9
|
+
interceptors: [(interceptorOptions) => {
|
|
10
|
+
const reqHeaders = interceptorOptions.context.reqHeaders ?? toFetchHeaders(interceptorOptions.request.headers);
|
|
11
|
+
return interceptorOptions.next({
|
|
12
|
+
...interceptorOptions,
|
|
13
|
+
context: {
|
|
14
|
+
...interceptorOptions.context,
|
|
15
|
+
reqHeaders
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}, ...toArray(options.interceptors)]
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { StandardHandlerOptions, StandardHandlerPlugin } from "../adapters/standard/index.mjs";
|
|
2
|
+
import type { Context } from "../context.mjs";
|
|
3
|
+
export interface RequestLimitHandlerPluginOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The maximum allowed request body size in bytes.
|
|
6
|
+
*/
|
|
7
|
+
maxBodySize: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Rejects requests whose body exceeds `maxBodySize`.
|
|
11
|
+
*
|
|
12
|
+
* When used with the request compression plugin, the limit applies to the
|
|
13
|
+
* decompressed payload rather than the compressed wire size.
|
|
14
|
+
*
|
|
15
|
+
* @see {@link https://orpc.dev/docs/plugins/request-limit | Request Limit Plugin}
|
|
16
|
+
*/
|
|
17
|
+
export declare class RequestLimitHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
|
|
18
|
+
name: string;
|
|
19
|
+
/**
|
|
20
|
+
* Should limit the original batch request body instead of sub-requests.
|
|
21
|
+
*/
|
|
22
|
+
after: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Should limit the final body size instead of the compressed one.
|
|
25
|
+
*/
|
|
26
|
+
before: string[];
|
|
27
|
+
private readonly maxBodySize;
|
|
28
|
+
constructor(options: RequestLimitHandlerPluginOptions);
|
|
29
|
+
init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
export class RequestLimitHandlerPlugin {
|
|
7
|
+
name = "~request-limit";
|
|
8
|
+
|
|
9
|
+
after = ["~batch"];
|
|
10
|
+
|
|
11
|
+
before = ["~request-compression"];
|
|
12
|
+
maxBodySize;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.maxBodySize = options.maxBodySize;
|
|
15
|
+
}
|
|
16
|
+
init(options) {
|
|
17
|
+
const maxBodySize = this.maxBodySize;
|
|
18
|
+
const routingInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
19
|
+
return next({
|
|
20
|
+
...interceptorOptions,
|
|
21
|
+
request: {
|
|
22
|
+
...interceptorOptions.request,
|
|
23
|
+
async resolveBody(hint) {
|
|
24
|
+
const contentLength = Number(flattenStandardHeader(interceptorOptions.request.headers["content-length"]));
|
|
25
|
+
if (Number.isFinite(contentLength) && contentLength > maxBodySize) {
|
|
26
|
+
throw new ORPCError("PAYLOAD_TOO_LARGE");
|
|
27
|
+
}
|
|
28
|
+
const stream = await interceptorOptions.request.resolveBody("octet-stream");
|
|
29
|
+
|
|
30
|
+
if (!(stream instanceof ReadableStream)) {
|
|
31
|
+
return stream;
|
|
32
|
+
}
|
|
33
|
+
let currentBodySize = 0;
|
|
34
|
+
const limitedStream = stream.pipeThrough(new TransformStream({ transform(chunk, controller) {
|
|
35
|
+
currentBodySize += chunk.byteLength;
|
|
36
|
+
if (currentBodySize > maxBodySize) {
|
|
37
|
+
controller.error(new ORPCError("PAYLOAD_TOO_LARGE"));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
controller.enqueue(chunk);
|
|
41
|
+
} }));
|
|
42
|
+
const response = new Response(limitedStream, { headers: toFetchHeaders(interceptorOptions.request.headers) });
|
|
43
|
+
return toStandardBody(response, { hint });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
...options,
|
|
50
|
+
routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { StandardHeaders, StandardResponse } from "@standard-server/core";
|
|
2
|
+
import type { StandardHandlerOptions, StandardHandlerPlugin, StandardHandlerRoutingInterceptorOptions } from "../adapters/standard/index.mjs";
|
|
3
|
+
import type { Context } from "../context.mjs";
|
|
4
|
+
export interface ResponseCompressionHandlerPluginOptions<T extends Context> {
|
|
5
|
+
/**
|
|
6
|
+
* The compression schemes to use for response compression.
|
|
7
|
+
* Schemes are prioritized by their order in this array and
|
|
8
|
+
* only applied if the client supports them (via Accept-Encoding).
|
|
9
|
+
*
|
|
10
|
+
* @default ['gzip', 'deflate']
|
|
11
|
+
*/
|
|
12
|
+
encodings?: readonly ("gzip" | "deflate" | "deflate-raw")[] | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* The minimum response size in bytes required to trigger compression.
|
|
15
|
+
* Responses smaller than this threshold will not be compressed to avoid overhead.
|
|
16
|
+
* If the response size cannot be determined, compression will still be applied.
|
|
17
|
+
*
|
|
18
|
+
* @default 1024 (1KB)
|
|
19
|
+
*/
|
|
20
|
+
threshold?: number | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Determines whether a response with the given Content-Type should be compressed.
|
|
23
|
+
* Only consulted for binary transfers (streams, files, and file form-data parts).
|
|
24
|
+
* Also receives the routing interceptor options for per-request decisions.
|
|
25
|
+
*
|
|
26
|
+
* @default isCompressibleContentType (covers common text-based formats)
|
|
27
|
+
*/
|
|
28
|
+
isCompressibleContentType?: ((contentType: string | null | undefined, options: StandardHandlerRoutingInterceptorOptions<T>) => boolean) | undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Compresses response bodies based on the client's Accept-Encoding header.
|
|
32
|
+
* Works at the standard handler level, so it supports all adapters.
|
|
33
|
+
*
|
|
34
|
+
* @see {@link https://orpc.dev/docs/plugins/response-compression | Response Compression Plugin}
|
|
35
|
+
*/
|
|
36
|
+
export declare class ResponseCompressionHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
|
|
37
|
+
name: string;
|
|
38
|
+
/**
|
|
39
|
+
* Compression should be done after batching, to compress the final response.
|
|
40
|
+
* Compression should also be done after response headers are set, to access final headers like Content-Type and Cache-Control.
|
|
41
|
+
*/
|
|
42
|
+
after: string[];
|
|
43
|
+
private readonly encodings;
|
|
44
|
+
private readonly threshold;
|
|
45
|
+
private readonly isCompressibleContentType;
|
|
46
|
+
constructor(options?: ResponseCompressionHandlerPluginOptions<T>);
|
|
47
|
+
init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Picks the first of `encodings` the client accepts, or `undefined` when the response
|
|
51
|
+
* must be sent as is because it is already encoded, partial, or forbids transforms.
|
|
52
|
+
* Shared by the response compression plugins so they agree on when to compress.
|
|
53
|
+
*/
|
|
54
|
+
export declare function negotiateResponseCompressionEncoding<TEncoding extends string>(response: StandardResponse, requestHeaders: StandardHeaders, encodings: readonly TEncoding[]): TEncoding | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* The headers of a response whose body is compressed with `encoding`.
|
|
57
|
+
* Shared by the response compression plugins so compressed responses carry the same headers.
|
|
58
|
+
*/
|
|
59
|
+
export declare function toCompressedHeaders(headers: StandardHeaders, encoding: string): StandardHeaders;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { isAcceptableEncoding, isAsyncIteratorObject, isCompressibleContentType, isNoTransformCacheControl, parseAcceptEncodingQualities, toArray, varyByAcceptEncoding } from "@orpc/shared";
|
|
2
|
+
import { flattenStandardHeader, generateContentDisposition } from "@standard-server/core";
|
|
3
|
+
import { toFetchBody } from "@standard-server/fetch";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const AVG_BYTES_PER_CHAR = 1.2;
|
|
7
|
+
|
|
8
|
+
export class ResponseCompressionHandlerPlugin {
|
|
9
|
+
name = "~response-compression";
|
|
10
|
+
|
|
11
|
+
after = ["~batch", "~response-headers"];
|
|
12
|
+
encodings;
|
|
13
|
+
threshold;
|
|
14
|
+
isCompressibleContentType;
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.encodings = options.encodings ?? ["gzip", "deflate"];
|
|
17
|
+
this.threshold = options.threshold ?? 1024;
|
|
18
|
+
this.isCompressibleContentType = options.isCompressibleContentType ?? isCompressibleContentType;
|
|
19
|
+
}
|
|
20
|
+
init(options) {
|
|
21
|
+
const routingInterceptor = async ({ next, ...interceptorOptions }) => {
|
|
22
|
+
const result = await next();
|
|
23
|
+
if (!result.matched) {
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
const response = result.response;
|
|
27
|
+
const encoding = negotiateResponseCompressionEncoding(response, interceptorOptions.request.headers, this.encodings);
|
|
28
|
+
if (encoding === undefined) {
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
const body = response.body;
|
|
32
|
+
const headers = response.headers;
|
|
33
|
+
if (body instanceof ReadableStream) {
|
|
34
|
+
const contentLength = Number(flattenStandardHeader(headers["content-length"]));
|
|
35
|
+
if ((!Number.isFinite(contentLength) || contentLength >= this.threshold) && this.isCompressibleContentType(flattenStandardHeader(headers["content-type"]), interceptorOptions)) {
|
|
36
|
+
return {
|
|
37
|
+
...result,
|
|
38
|
+
response: {
|
|
39
|
+
...response,
|
|
40
|
+
body: body.pipeThrough(new CompressionStream(encoding)),
|
|
41
|
+
headers: toCompressedHeaders(headers, encoding)
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
} else if (body instanceof Blob) {
|
|
46
|
+
|
|
47
|
+
const contentType = headers["content-type"] ?? body.type;
|
|
48
|
+
if ((!Number.isFinite(body.size) || body.size >= this.threshold) && this.isCompressibleContentType(flattenStandardHeader(contentType), interceptorOptions)) {
|
|
49
|
+
const contentDisposition = headers["content-disposition"] ?? generateContentDisposition(body instanceof File ? body.name : "blob");
|
|
50
|
+
return {
|
|
51
|
+
...result,
|
|
52
|
+
response: {
|
|
53
|
+
...response,
|
|
54
|
+
body: body.stream().pipeThrough(new CompressionStream(encoding)),
|
|
55
|
+
headers: {
|
|
56
|
+
...toCompressedHeaders(headers, encoding),
|
|
57
|
+
"standard-server": headers["standard-server"] ?? "file",
|
|
58
|
+
"content-type": contentType,
|
|
59
|
+
"content-disposition": contentDisposition
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
} else if (body instanceof FormData) {
|
|
65
|
+
const PART_OVERHEAD = 64;
|
|
66
|
+
let contentLength = 0;
|
|
67
|
+
for (const [key, value] of body) {
|
|
68
|
+
contentLength += PART_OVERHEAD + key.length;
|
|
69
|
+
if (value instanceof Blob) {
|
|
70
|
+
if (!Number.isFinite(value.size)) {
|
|
71
|
+
if (!this.isCompressibleContentType(value.type, interceptorOptions)) {
|
|
72
|
+
|
|
73
|
+
contentLength = -Infinity;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
contentLength = Infinity;
|
|
78
|
+
} else {
|
|
79
|
+
contentLength += this.isCompressibleContentType(value.type, interceptorOptions) ? value.size : -value.size;
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
contentLength += value.length * AVG_BYTES_PER_CHAR;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (contentLength >= this.threshold) {
|
|
86
|
+
const res = new Response(body);
|
|
87
|
+
const compressedStream = res.body.pipeThrough(new CompressionStream(encoding));
|
|
88
|
+
return {
|
|
89
|
+
...result,
|
|
90
|
+
response: {
|
|
91
|
+
...response,
|
|
92
|
+
body: compressedStream,
|
|
93
|
+
headers: {
|
|
94
|
+
...toCompressedHeaders(headers, encoding),
|
|
95
|
+
"standard-server": [],
|
|
96
|
+
"content-type": res.headers.get("content-type")
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
} else if (body instanceof URLSearchParams) {
|
|
102
|
+
const string = body.toString();
|
|
103
|
+
if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
|
|
104
|
+
const [, bodyHeaders] = toFetchBody(body, headers);
|
|
105
|
+
return {
|
|
106
|
+
...result,
|
|
107
|
+
response: {
|
|
108
|
+
...response,
|
|
109
|
+
body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
|
|
110
|
+
headers: {
|
|
111
|
+
...toCompressedHeaders(bodyHeaders, encoding),
|
|
112
|
+
|
|
113
|
+
"standard-server": bodyHeaders["standard-server"] ?? []
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
} else if (body !== undefined && !isAsyncIteratorObject(body)) {
|
|
119
|
+
const [string, bodyHeaders] = toFetchBody(body, headers);
|
|
120
|
+
if (string.length * AVG_BYTES_PER_CHAR >= this.threshold) {
|
|
121
|
+
return {
|
|
122
|
+
...result,
|
|
123
|
+
response: {
|
|
124
|
+
...response,
|
|
125
|
+
body: new Blob([string]).stream().pipeThrough(new CompressionStream(encoding)),
|
|
126
|
+
headers: {
|
|
127
|
+
...toCompressedHeaders(bodyHeaders, encoding),
|
|
128
|
+
"standard-server": bodyHeaders["standard-server"] ?? []
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
...options,
|
|
138
|
+
routingInterceptors: [routingInterceptor, ...toArray(options.routingInterceptors)]
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function negotiateResponseCompressionEncoding(response, requestHeaders, encodings) {
|
|
144
|
+
if (flattenStandardHeader(response.headers["content-encoding"]) !== undefined) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (response.status === 206 || flattenStandardHeader(response.headers["content-range"]) !== undefined) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (isNoTransformCacheControl(flattenStandardHeader(response.headers["cache-control"]))) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
const acceptEncodings = parseAcceptEncodingQualities(flattenStandardHeader(requestHeaders["accept-encoding"]));
|
|
156
|
+
return encodings.find((encoding) => isAcceptableEncoding(acceptEncodings, encoding));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function toCompressedHeaders(headers, encoding) {
|
|
160
|
+
return {
|
|
161
|
+
...headers,
|
|
162
|
+
"content-length": [],
|
|
163
|
+
"content-encoding": encoding,
|
|
164
|
+
"vary": varyByAcceptEncoding(flattenStandardHeader(headers.vary)),
|
|
165
|
+
|
|
166
|
+
"etag": weakenEtag(flattenStandardHeader(headers.etag)),
|
|
167
|
+
"accept-ranges": []
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function weakenEtag(etag) {
|
|
171
|
+
return etag === undefined || etag.startsWith("W/") ? etag : `W/${etag}`;
|
|
172
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { StandardHandlerOptions, StandardHandlerPlugin } from "../adapters/standard/index.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* The context shape into which the Response Headers Plugin injects `resHeaders`.
|
|
4
|
+
*
|
|
5
|
+
* @see {@link https://orpc.dev/docs/plugins/response-headers | Response Headers Plugin}
|
|
6
|
+
*/
|
|
7
|
+
export interface ResponseHeadersHandlerPluginContext {
|
|
8
|
+
/**
|
|
9
|
+
* Response headers as a Headers instance. This is injected by the Response Headers Plugin.
|
|
10
|
+
* When set before the response is sent, these headers will be included in the response. If not set, no additional headers will be added.
|
|
11
|
+
*/
|
|
12
|
+
resHeaders?: Headers | undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The Response Headers Plugin allows you to set response headers in oRPC.
|
|
16
|
+
* It injects a resHeaders instance into the context, enabling you to modify response headers easily.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* **Warning**: In a `streaming` batch, only the cookies of the first procedure to respond reach the client.
|
|
20
|
+
*
|
|
21
|
+
* @see {@link https://orpc.dev/docs/plugins/response-headers | Response Headers Plugin}
|
|
22
|
+
*/
|
|
23
|
+
export declare class ResponseHeadersHandlerPlugin<T extends ResponseHeadersHandlerPluginContext> implements StandardHandlerPlugin<T> {
|
|
24
|
+
name: string;
|
|
25
|
+
/**
|
|
26
|
+
* Interceptors should run after batch interceptors so headers are applied to each sub-response.
|
|
27
|
+
*/
|
|
28
|
+
before: string[];
|
|
29
|
+
init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { toArray } from "@orpc/shared";
|
|
2
|
+
import { mergeStandardHeaders } from "@standard-server/core";
|
|
3
|
+
import { toStandardHeaders } from "@standard-server/fetch";
|
|
4
|
+
|
|
5
|
+
export class ResponseHeadersHandlerPlugin {
|
|
6
|
+
name = "~response-headers";
|
|
7
|
+
|
|
8
|
+
before = ["~batch"];
|
|
9
|
+
init(options) {
|
|
10
|
+
return {
|
|
11
|
+
...options,
|
|
12
|
+
routingInterceptors: [async (interceptorOptions) => {
|
|
13
|
+
const resHeaders = new Headers(interceptorOptions.context.resHeaders);
|
|
14
|
+
const result = await interceptorOptions.next({
|
|
15
|
+
...interceptorOptions,
|
|
16
|
+
context: {
|
|
17
|
+
...interceptorOptions.context,
|
|
18
|
+
resHeaders
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
if (!result.response) {
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
...result,
|
|
26
|
+
response: {
|
|
27
|
+
...result.response,
|
|
28
|
+
headers: mergeStandardHeaders(result.response.headers, toStandardHeaders(resHeaders))
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}, ...toArray(options.routingInterceptors)]
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ThrowableError } from "@orpc/shared";
|
|
2
|
+
import type { StandardHandlerInterceptorOptions, StandardHandlerOptions, StandardHandlerPlugin } from "../adapters/standard/index.mjs";
|
|
3
|
+
import type { Context } from "../context.mjs";
|
|
4
|
+
export interface RethrowHandlerPluginOptions<T extends Context> {
|
|
5
|
+
/**
|
|
6
|
+
* Decide which errors should be rethrown.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const rethrowPlugin = new RethrowHandlerPlugin({
|
|
11
|
+
* filter: (error) => {
|
|
12
|
+
* // Rethrow all non-ORPCError errors
|
|
13
|
+
* return !(error instanceof ORPCError)
|
|
14
|
+
* }
|
|
15
|
+
* })
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
filter: (error: ThrowableError, options: StandardHandlerInterceptorOptions<T>) => boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The plugin can bypass oRPC's built-in error handling
|
|
22
|
+
* and rethrow matching errors directly to your framework's error handling mechanism
|
|
23
|
+
* (e.g., NestJS exception filters, Express error middleware).
|
|
24
|
+
*
|
|
25
|
+
* @see {@link https://orpc.dev/docs/plugins/rethrow | Rethrow Handler Plugin}
|
|
26
|
+
*/
|
|
27
|
+
export declare class RethrowHandlerPlugin<T extends Context> implements StandardHandlerPlugin<T> {
|
|
28
|
+
name: string;
|
|
29
|
+
private readonly filter;
|
|
30
|
+
private readonly CONTEXT_SYMBOL;
|
|
31
|
+
constructor(options: RethrowHandlerPluginOptions<T>);
|
|
32
|
+
init(options: StandardHandlerOptions<T>): StandardHandlerOptions<T>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { toArray } from "@orpc/shared";
|
|
2
|
+
|
|
3
|
+
export class RethrowHandlerPlugin {
|
|
4
|
+
name = "~rethrow";
|
|
5
|
+
filter;
|
|
6
|
+
CONTEXT_SYMBOL = Symbol("ORPC_RETHROW_HANDLER_PLUGIN_CONTEXT");
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.filter = options.filter;
|
|
9
|
+
}
|
|
10
|
+
init(options) {
|
|
11
|
+
const routingInterceptor = async (options) => {
|
|
12
|
+
const pluginContext = {};
|
|
13
|
+
const result = await options.next({
|
|
14
|
+
...options,
|
|
15
|
+
context: {
|
|
16
|
+
...options.context,
|
|
17
|
+
[this.CONTEXT_SYMBOL]: pluginContext
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
if (pluginContext.error) {
|
|
21
|
+
throw pluginContext.error.value;
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
const interceptor = async (options) => {
|
|
26
|
+
const pluginContext = options.context[this.CONTEXT_SYMBOL];
|
|
27
|
+
if (!pluginContext) {
|
|
28
|
+
throw new TypeError("[RethrowHandlerPlugin] Rethrow handler context has been corrupted or modified by another plugin or interceptor");
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
|
|
32
|
+
return await options.next();
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (this.filter(error, options)) {
|
|
35
|
+
pluginContext.error = { value: error };
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
...options,
|
|
42
|
+
routingInterceptors: [...toArray(options.routingInterceptors), routingInterceptor],
|
|
43
|
+
interceptors: [interceptor, ...toArray(options.interceptors)]
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|