import type { Middleware } from "../../core/types.ts";
export type CorsOptions = {
origin?: string | string[] | RegExp | ((origin: string | null) => string | boolean | null | undefined);
allowMethods?: string[];
allowHeaders?: string[];
exposeHeaders?: string[];
credentials?: boolean;
maxAge?: number;
preflightContinue?: boolean;
optionsSuccessStatus?: number;
};
export function cors(options: CorsOptions = {}): Middleware {
const {
origin = "*",
allowMethods = ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
allowHeaders,
exposeHeaders,
credentials = false,
maxAge,
preflightContinue = false,
optionsSuccessStatus = 204,
} = options;
const resolveOrigin = (requestOrigin: string): string | null => {
if (typeof origin === "function") {
const res = origin(requestOrigin);
if (res === true) return requestOrigin;
if (typeof res === "string") return res;
return res ? requestOrigin : null;
}
if (typeof origin === "string") {
if (origin === "*") return "*";
return origin === requestOrigin ? requestOrigin : null;
}
if (Array.isArray(origin)) {
return origin.includes(requestOrigin) ? requestOrigin : null;
}
if (origin instanceof RegExp) {
return origin.test(requestOrigin) ? requestOrigin : null;
}
return null;
};
const applyOrigin = (headers: Headers, requestOrigin: string, resolved: string): void => {
if (credentials && resolved === "*") {
headers.set("Access-Control-Allow-Origin", requestOrigin);
headers.append("Vary", "Origin");
} else {
headers.set("Access-Control-Allow-Origin", resolved);
if (resolved !== "*") {
headers.append("Vary", "Origin");
}
}
if (credentials) {
headers.set("Access-Control-Allow-Credentials", "true");
}
};
return async (req, _ctx, next) => {
const requestOrigin = req.headers.get("Origin");
const isPreflight =
req.method === "OPTIONS" &&
requestOrigin !== null &&
req.headers.get("Access-Control-Request-Method") !== null;
if (!requestOrigin) {
return next();
}
const resolved = resolveOrigin(requestOrigin);
if (!resolved) {
return next();
}
if (isPreflight) {
const resHeaders = new Headers();
applyOrigin(resHeaders, requestOrigin, resolved);
resHeaders.set("Access-Control-Allow-Methods", allowMethods.join(", "));
const reqAllowed = req.headers.get("Access-Control-Request-Headers");
if (allowHeaders && allowHeaders.length > 0) {
resHeaders.set("Access-Control-Allow-Headers", allowHeaders.join(", "));
} else if (reqAllowed) {
resHeaders.set("Access-Control-Allow-Headers", reqAllowed);
resHeaders.append("Vary", "Access-Control-Request-Headers");
}
if (typeof maxAge === "number") {
resHeaders.set("Access-Control-Max-Age", String(maxAge));
}
if (preflightContinue) {
const response = await next();
for (const [k, v] of resHeaders.entries()) {
response.headers.set(k, v);
}
return response;
}
return new Response(null, { status: optionsSuccessStatus, headers: resHeaders });
}
const response = await next();
applyOrigin(response.headers, requestOrigin, resolved);
if (exposeHeaders && exposeHeaders.length > 0) {
response.headers.set("Access-Control-Expose-Headers", exposeHeaders.join(", "));
}
return response;
};
}
export const corsMiddleware: Middleware = cors();