@beignet/core
Version:
Core framework primitives for Beignet
120 lines • 4.59 kB
JavaScript
/**
* CORS hook utilities for @beignet/core/server
*/
import { BEIGNET_ERROR_OWNER_HEADER } from "../../contracts/types.js";
const DEFAULT_CORS = {
origins: "*",
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
headers: ["Content-Type", "Authorization"],
exposedHeaders: [],
credentials: false,
};
function resolveCorsConfig(config) {
const resolved = {
origins: config.origins ?? DEFAULT_CORS.origins,
methods: config.methods ?? DEFAULT_CORS.methods,
headers: config.headers ?? DEFAULT_CORS.headers,
exposedHeaders: config.exposedHeaders ?? DEFAULT_CORS.exposedHeaders,
credentials: config.credentials ?? DEFAULT_CORS.credentials,
};
if (resolved.credentials &&
(resolved.origins === "*" ||
(Array.isArray(resolved.origins) && resolved.origins.includes("*")))) {
throw new Error('Credentialed CORS cannot use a wildcard origin. Use origins: ["https://app.example.com"] with credentials: true.');
}
return resolved;
}
function appendVaryOrigin(headers) {
const varyKey = Object.keys(headers).find((key) => key.toLowerCase() === "vary") ?? "Vary";
const currentValue = headers[varyKey];
const current = typeof currentValue === "string" ? currentValue : currentValue?.join(", ");
const values = current?.split(",").map((value) => value.trim().toLowerCase());
if (values?.includes("origin"))
return;
headers[varyKey] = current ? `${current}, Origin` : "Origin";
}
function appendCommaSeparatedHeader(headers, name, appended) {
const key = Object.keys(headers).find((candidate) => candidate.toLowerCase() === name.toLowerCase()) ?? name;
const existing = headers[key];
const values = [
...(typeof existing === "string"
? existing.split(",")
: (existing ?? []).flatMap((value) => value.split(","))),
...appended,
]
.map((value) => value.trim())
.filter(Boolean);
const seen = new Set();
const unique = values.filter((value) => {
const normalized = value.toLowerCase();
if (seen.has(normalized))
return false;
seen.add(normalized);
return true;
});
headers[key] = unique.join(", ");
}
/**
* Apply CORS response headers to a mutable header record.
*
* Credentialed CORS rejects wildcard origins. Use an explicit origin allow-list
* when cookies or authorization headers are allowed cross-origin.
*/
export function applyCorsHeaders(headers, req, corsConfig) {
const { origins, methods, headers: allowedHeaders, exposedHeaders, credentials, } = resolveCorsConfig(corsConfig);
if (origins === "*") {
headers["Access-Control-Allow-Origin"] = "*";
}
else if (Array.isArray(origins)) {
const requestOrigin = req.headers.get("Origin");
if (requestOrigin && origins.includes(requestOrigin)) {
headers["Access-Control-Allow-Origin"] = requestOrigin;
appendVaryOrigin(headers);
}
}
headers["Access-Control-Allow-Methods"] = methods.join(", ");
headers["Access-Control-Allow-Headers"] = allowedHeaders.join(", ");
appendCommaSeparatedHeader(headers, "Access-Control-Expose-Headers", [
BEIGNET_ERROR_OWNER_HEADER,
...exposedHeaders,
]);
if (credentials) {
headers["Access-Control-Allow-Credentials"] = "true";
}
}
/**
* Create CORS hooks for preflight and regular responses.
*
* CORS preflight requests short-circuit with a 204 response. Explicit
* `OPTIONS` routes without `Access-Control-Request-Method` continue through the
* normal route pipeline. All responses are decorated in `beforeSend`.
*/
export function createCorsHooks(config) {
const corsConfig = resolveCorsConfig(config);
return {
name: "cors",
onRequest: ({ req }) => {
if (req.method !== "OPTIONS" ||
!req.headers.get("Origin") ||
!req.headers.get("Access-Control-Request-Method")) {
return undefined;
}
const headers = {};
applyCorsHeaders(headers, req, corsConfig);
return {
status: 204,
headers,
body: null,
};
},
beforeSend: ({ req, response }) => {
const headers = { ...(response.headers ?? {}) };
applyCorsHeaders(headers, req, corsConfig);
return {
...response,
headers,
};
},
};
}
//# sourceMappingURL=cors.js.map