@orpc/server
Version:
<div align="center"> <image align="center" src="https://orpc.unnoq.com/logo.webp" width=280 alt="oRPC logo" /> </div>
286 lines (279 loc) • 10.6 kB
JavaScript
import { runWithSpan, value, setSpanError, isAsyncIteratorObject, clone } from '@orpc/shared';
import { flattenHeader } from '@orpc/standard-server';
import { parseBatchRequest, toBatchResponse } from '@orpc/standard-server/batch';
import { toFetchHeaders } from '@orpc/standard-server-fetch';
import { ORPCError } from '@orpc/client';
export { S as StrictGetMethodPlugin } from '../shared/server.BW-nUGgA.mjs';
import '@orpc/contract';
class BatchHandlerPlugin {
maxSize;
mapRequestItem;
successStatus;
headers;
order = 5e6;
constructor(options = {}) {
this.maxSize = options.maxSize ?? 10;
this.mapRequestItem = options.mapRequestItem ?? ((request, { request: batchRequest }) => ({
...request,
headers: {
...batchRequest.headers,
...request.headers
}
}));
this.successStatus = options.successStatus ?? 207;
this.headers = options.headers ?? {};
}
init(options) {
options.rootInterceptors ??= [];
options.rootInterceptors.unshift(async (options2) => {
const xHeader = flattenHeader(options2.request.headers["x-orpc-batch"]);
if (xHeader === void 0) {
return options2.next();
}
let isParsing = false;
try {
return await runWithSpan({ name: "handle_batch_request" }, async (span) => {
const mode = xHeader === "buffered" ? "buffered" : "streaming";
isParsing = true;
const parsed = parseBatchRequest({ ...options2.request, body: await options2.request.body() });
isParsing = false;
span?.setAttribute("batch.mode", mode);
span?.setAttribute("batch.size", parsed.length);
const maxSize = await value(this.maxSize, options2);
if (parsed.length > maxSize) {
const message = "Batch request size exceeds the maximum allowed size";
setSpanError(span, message);
return {
matched: true,
response: {
status: 413,
headers: {},
body: message
}
};
}
const responses = parsed.map(
(request, index) => {
const mapped = this.mapRequestItem(request, options2);
return options2.next({ ...options2, request: { ...mapped, body: () => Promise.resolve(mapped.body) } }).then(({ response: response2, matched }) => {
span?.addEvent(`response.${index}.${matched ? "success" : "not_matched"}`);
if (matched) {
if (response2.body instanceof Blob || response2.body instanceof FormData || isAsyncIteratorObject(response2.body)) {
return {
index,
status: 500,
headers: {},
body: "Batch responses do not support file/blob, or event-iterator. Please call this procedure separately outside of the batch request."
};
}
return { ...response2, index };
}
return { index, status: 404, headers: {}, body: "No procedure matched" };
}).catch((err) => {
Promise.reject(err);
return { index, status: 500, headers: {}, body: "Internal server error" };
});
}
);
await Promise.race(responses);
const status = await value(this.successStatus, responses, options2);
const headers = await value(this.headers, responses, options2);
const response = await toBatchResponse({
status,
headers,
mode,
body: async function* () {
const promises = [...responses];
while (true) {
const handling = promises.filter((p) => p !== void 0);
if (handling.length === 0) {
return;
}
const result = await Promise.race(handling);
promises[result.index] = void 0;
yield result;
}
}()
});
return {
matched: true,
response
};
});
} catch (cause) {
if (isParsing) {
return {
matched: true,
response: { status: 400, headers: {}, body: "Invalid batch request, this could be caused by a malformed request body or a missing header" }
};
}
throw cause;
}
});
}
}
class CORSPlugin {
options;
order = 9e6;
constructor(options = {}) {
const defaults = {
origin: (origin) => origin,
allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"]
};
this.options = {
...defaults,
...options
};
}
init(options) {
options.rootInterceptors ??= [];
options.rootInterceptors.unshift(async (interceptorOptions) => {
if (interceptorOptions.request.method === "OPTIONS") {
const resHeaders = {};
if (this.options.maxAge !== void 0) {
resHeaders["access-control-max-age"] = this.options.maxAge.toString();
}
if (this.options.allowMethods?.length) {
resHeaders["access-control-allow-methods"] = flattenHeader(this.options.allowMethods);
}
const allowHeaders = this.options.allowHeaders ?? interceptorOptions.request.headers["access-control-request-headers"];
if (typeof allowHeaders === "string" || allowHeaders?.length) {
resHeaders["access-control-allow-headers"] = flattenHeader(allowHeaders);
}
return {
matched: true,
response: {
status: 204,
headers: resHeaders,
body: void 0
}
};
}
return interceptorOptions.next();
});
options.rootInterceptors.unshift(async (interceptorOptions) => {
const result = await interceptorOptions.next();
if (!result.matched) {
return result;
}
const origin = flattenHeader(interceptorOptions.request.headers.origin) ?? "";
const allowedOrigin = await value(this.options.origin, origin, interceptorOptions);
const allowedOriginArr = Array.isArray(allowedOrigin) ? allowedOrigin : [allowedOrigin];
if (allowedOriginArr.includes("*")) {
result.response.headers["access-control-allow-origin"] = "*";
} else {
if (allowedOriginArr.includes(origin)) {
result.response.headers["access-control-allow-origin"] = origin;
}
result.response.headers.vary = interceptorOptions.request.headers.vary ?? "origin";
}
const allowedTimingOrigin = await value(this.options.timingOrigin, origin, interceptorOptions);
const allowedTimingOriginArr = Array.isArray(allowedTimingOrigin) ? allowedTimingOrigin : [allowedTimingOrigin];
if (allowedTimingOriginArr.includes("*")) {
result.response.headers["timing-allow-origin"] = "*";
} else if (allowedTimingOriginArr.includes(origin)) {
result.response.headers["timing-allow-origin"] = origin;
}
if (this.options.credentials) {
result.response.headers["access-control-allow-credentials"] = "true";
}
if (this.options.exposeHeaders?.length) {
result.response.headers["access-control-expose-headers"] = flattenHeader(this.options.exposeHeaders);
}
return result;
});
}
}
class RequestHeadersPlugin {
init(options) {
options.rootInterceptors ??= [];
options.rootInterceptors.push((interceptorOptions) => {
const reqHeaders = interceptorOptions.context.reqHeaders ?? toFetchHeaders(interceptorOptions.request.headers);
return interceptorOptions.next({
...interceptorOptions,
context: {
...interceptorOptions.context,
reqHeaders
}
});
});
}
}
class ResponseHeadersPlugin {
init(options) {
options.rootInterceptors ??= [];
options.rootInterceptors.push(async (interceptorOptions) => {
const resHeaders = interceptorOptions.context.resHeaders ?? new Headers();
const result = await interceptorOptions.next({
...interceptorOptions,
context: {
...interceptorOptions.context,
resHeaders
}
});
if (!result.matched) {
return result;
}
const responseHeaders = clone(result.response.headers);
for (const [key, value] of resHeaders) {
if (Array.isArray(responseHeaders[key])) {
responseHeaders[key].push(value);
} else if (responseHeaders[key] !== void 0) {
responseHeaders[key] = [responseHeaders[key], value];
} else {
responseHeaders[key] = value;
}
}
return {
...result,
response: {
...result.response,
headers: responseHeaders
}
};
});
}
}
const SIMPLE_CSRF_PROTECTION_CONTEXT_SYMBOL = Symbol("SIMPLE_CSRF_PROTECTION_CONTEXT");
class SimpleCsrfProtectionHandlerPlugin {
headerName;
headerValue;
exclude;
error;
constructor(options = {}) {
this.headerName = options.headerName ?? "x-csrf-token";
this.headerValue = options.headerValue ?? "orpc";
this.exclude = options.exclude ?? false;
this.error = options.error ?? new ORPCError("CSRF_TOKEN_MISMATCH", {
status: 403,
message: "Invalid CSRF token"
});
}
order = 8e6;
init(options) {
options.rootInterceptors ??= [];
options.clientInterceptors ??= [];
options.rootInterceptors.unshift(async (options2) => {
const headerName = await value(this.headerName, options2);
const headerValue = await value(this.headerValue, options2);
return options2.next({
...options2,
context: {
...options2.context,
[SIMPLE_CSRF_PROTECTION_CONTEXT_SYMBOL]: options2.request.headers[headerName] === headerValue
}
});
});
options.clientInterceptors.unshift(async (options2) => {
if (typeof options2.context[SIMPLE_CSRF_PROTECTION_CONTEXT_SYMBOL] !== "boolean") {
throw new TypeError("[SimpleCsrfProtectionHandlerPlugin] CSRF protection context has been corrupted or modified by another plugin or interceptor");
}
const excluded = await value(this.exclude, options2);
if (!excluded && !options2.context[SIMPLE_CSRF_PROTECTION_CONTEXT_SYMBOL]) {
throw this.error;
}
return options2.next();
});
}
}
export { BatchHandlerPlugin, CORSPlugin, RequestHeadersPlugin, ResponseHeadersPlugin, SimpleCsrfProtectionHandlerPlugin };