UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

120 lines 4.59 kB
/** * 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