remix-utils-rt
Version:
This package contains simple utility functions to use with [React Router](https://reactrouter.com/home).
180 lines • 6.39 kB
JavaScript
const DEFAULT_OPTIONS = {
origin: true,
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
allowedHeaders: [],
exposedHeaders: [],
};
class CORS {
options;
constructor(options) {
// Merge user options with default options
this.options = Object.assign({}, DEFAULT_OPTIONS, options);
}
async exec(request, response) {
let isPreflight = request.method.toLowerCase() === "options";
await this.configureOrigin(response.headers, request);
this.configureCredentials(response.headers);
this.configureExposedHeaders(response.headers);
if (isPreflight) {
this.configureMethods(response.headers);
this.configureAllowedHeaders(response.headers, request);
this.configureMaxAge(response.headers);
// waiting for the body
if (response.status === 204)
response.headers.set("Content-Length", "0");
}
return response;
}
async resolveOrigin(request) {
let { origin } = this.options;
if (typeof origin === "function") {
return await origin(request.headers.get("origin") ?? "");
}
return origin;
}
configureMaxAge(headers) {
const { maxAge } = this.options;
if (!this.isNumber(maxAge))
return headers;
headers.append("Access-Control-Max-Age", maxAge.toString());
return headers;
}
configureExposedHeaders(headers) {
let exposedHeaders = this.options.exposedHeaders?.join(",");
if (!this.isString(exposedHeaders) || exposedHeaders === "")
return headers;
headers.append("Access-Control-Expose-Headers", exposedHeaders);
return null;
}
configureAllowedHeaders(headers, request) {
let allowedHeaders = this.options.allowedHeaders?.join(",");
if (!allowedHeaders) {
// headers wasn't specified, so reflect the request headers
let requestHeaders = request.headers.get("Access-Control-Request-Headers");
if (this.isString(requestHeaders))
allowedHeaders = requestHeaders;
headers.append("Vary", "Access-Control-Request-Headers");
}
if (allowedHeaders && allowedHeaders !== "") {
headers.append("Access-Control-Allow-Headers", allowedHeaders);
}
return headers;
}
configureCredentials(headers) {
if (this.options.credentials === true) {
headers.append("Access-Control-Allow-Credentials", "true");
}
return headers;
}
configureMethods(headers) {
let methods = this.options.methods?.join(",");
if (!this.isString(methods))
return headers;
headers.append("Access-Control-Allow-Methods", methods);
return headers;
}
async configureOrigin(headers, request) {
let origin = await this.resolveOrigin(request);
let requestOrigin = request.headers.get("origin");
if (!requestOrigin || origin === false)
return headers;
if (origin === undefined || origin === "*") {
// allow any origin
headers.append("Access-Control-Allow-Origin", "*");
return headers;
}
if (this.isString(origin)) {
// fixed origin
headers.append("Access-Control-Allow-Origin", origin);
headers.append("Vary", "Origin");
return headers;
}
if (!this.isOriginAllowed(requestOrigin, origin))
return headers;
// reflect origin
headers.append("Access-Control-Allow-Origin", requestOrigin);
headers.append("Vary", "Origin");
return headers;
}
isOriginAllowed(origin, allowedOrigin) {
if (Array.isArray(allowedOrigin)) {
for (let element of allowedOrigin) {
if (this.isOriginAllowed(origin, element))
return true;
}
return false;
}
if (this.isString(allowedOrigin)) {
return origin === allowedOrigin;
}
if (allowedOrigin instanceof RegExp) {
return allowedOrigin.test(origin);
}
return !!allowedOrigin;
}
isString(value) {
return typeof value === "string" || value instanceof String;
}
isNumber(value) {
return typeof value === "number" || value instanceof Number;
}
}
/**
* Setup CORS for a giving Request and Response objects pair using the specified
* options.
*
* The default options are:
* - origin: true
* - methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"]
* - allowedHeaders: []
* - exposedHeaders: []
* - credentials: false
* - maxAge: 0
*
* @param request The Request object
* @param response The Response object
* @param options Optional configuration for CORS
* @returns The same Response object mutated
*
* @example
* // Create a response, then setup CORS for it
* export async function loader({ request }: LoaderFunctionArgs) {
* let data = await getData(request);
* let response = json<LoaderData>(data);
* return await cors(request, response);
* }
* @example
* // Create response and setup CORS in a single line
* export async function loader({ request }: LoaderFunctionArgs) {
* let data = await getData(request);
* return await cors(request, json<LoaderData>(data));
* }
* @example
* // Setup for any data request
* export let handleDataRequest: HandleDataRequestFunction = async (
* response,
* { request }
* ) => {
* return await cors(request, response);
* };
* @example
* // Pass a configuration object to setup CORS
* export async function loader({ request }: LoaderFunctionArgs) {
* let data = await getData(request);
* return await cors(request, json<LoaderData>(data), {
* origin: "https://example.com"
* });
* }
* @example
* // Mutate response and then return it
* export async function loader({ request }: LoaderFunctionArgs) {
* let data = await getData(request);
* let response = json<LoaderData>(data);
* await cors(request, response); // this mutates the Response object
* return response;
* }
*/
export async function cors(request, response, options = DEFAULT_OPTIONS) {
return new CORS(options).exec(request, response);
}
//# sourceMappingURL=cors.js.map