const DEFAULT_ALLOWED_METHODS = [ "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", ]; const DEFAULT_ALLOWED_HEADERS = [ "Content-Type", "Authorization", "X-Requested-With", ]; const DEFAULT_EXPOSED_HEADERS = ["Content-Length", "Content-Type"]; const IS_DEVELOPMENT = Deno.env.get("DEVELOPMENT") === "true"; export interface CorsOptions { allowedOrigins: Set; allowCredentials?: boolean; allowedMethods?: string[]; allowedHeaders?: string[]; exposeHeaders?: string[]; requestHeaders?: string | null; maxAgeSeconds?: number; } function normalizeOrigin(raw: string | null | undefined): string | null { if (!raw) return null; try { const trimmed = raw.trim(); const hasScheme = /^https?:\/\//i.test(trimmed); const needsHttp = /^(localhost|127\.0\.0\.1)/i.test(trimmed); const candidate = hasScheme ? trimmed : `${needsHttp ? "http" : "https"}://${trimmed}`; const url = new URL(candidate); if (url.protocol !== "http:" && url.protocol !== "https:") { return null; } return `${url.protocol}//${url.host}`; } catch { return null; } } function parseOriginList(raw: string | null | undefined): Set { const allowed = new Set(); if (!raw) return allowed; for (const entry of raw.split(",")) { const normalized = normalizeOrigin(entry.trim()); if (normalized) { allowed.add(normalized); } } return allowed; } function deriveDefaultEdgeOrigins(): Set { const allowed = new Set(); const redirectHome = normalizeOrigin(Deno.env.get("REDIRECT_HOME")); if (redirectHome) { allowed.add(redirectHome); } const hostname = Deno.env.get("HOSTNAME_DENO"); const port = Deno.env.get("PORT"); if (hostname && port) { const devOrigin = normalizeOrigin(`http://${hostname}:${port}`); if (devOrigin) { allowed.add(devOrigin); } } return allowed; } const EDGE_ALLOWED_ORIGINS = (() => { const fromEnv = parseOriginList(Deno.env.get("EDGE_ALLOWED_ORIGINS")); if (fromEnv.size > 0) { return fromEnv; } return deriveDefaultEdgeOrigins(); })(); const ISLAND_ALLOWED_ORIGINS = new Set([ "https://cyberhybridhub.com", "http://cyberhybridhub.com", "http://localhost:8080", "http://96.3.218.128", "https://96.3.218.126", ]); function mergeVary(current: string | null, incoming: string): string { const parts = new Set(); for (const entry of (current ?? "").split(",")) { const trimmed = entry.trim(); if (trimmed) { parts.add(trimmed); } } for (const entry of incoming.split(",")) { const trimmed = entry.trim(); if (trimmed) { parts.add(trimmed); } } return Array.from(parts).join(", "); } function buildAllowedHeaders( baseHeaders: string[], requestHeaders?: string | null, ): string[] { const allowed = new Set(baseHeaders.map((header) => header.toLowerCase())); if (requestHeaders) { for (const header of requestHeaders.split(",")) { const trimmed = header.trim(); if (trimmed) { allowed.add(trimmed.toLowerCase()); } } } return Array.from(allowed).map((header) => header .split("-") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join("-") ); } export function isIslandFetchRequestAllowed( url: URL, originHeader?: string | null, ): boolean { if (IS_DEVELOPMENT) return true; // Check if the request origin is allowed if (originHeader) { const normalizedOrigin = normalizeOrigin(originHeader); if (normalizedOrigin && ISLAND_ALLOWED_ORIGINS.has(normalizedOrigin)) { return true; } } // Fallback to checking the URL origin return ISLAND_ALLOWED_ORIGINS.has(url.origin); } export function getCorsHeaders( origin: string | null, options: CorsOptions, ): Headers { const headers = new Headers(); const normalizedOrigin = normalizeOrigin(origin); if (!normalizedOrigin || !options.allowedOrigins.has(normalizedOrigin)) { return headers; } headers.set("Access-Control-Allow-Origin", normalizedOrigin); headers.set( "Access-Control-Allow-Methods", (options.allowedMethods ?? DEFAULT_ALLOWED_METHODS).join(", "), ); headers.set( "Access-Control-Allow-Headers", buildAllowedHeaders( options.allowedHeaders ?? DEFAULT_ALLOWED_HEADERS, options.requestHeaders, ).join(", "), ); headers.set( "Access-Control-Expose-Headers", (options.exposeHeaders ?? DEFAULT_EXPOSED_HEADERS).join(", "), ); if (options.allowCredentials ?? true) { headers.set("Access-Control-Allow-Credentials", "true"); } if (options.maxAgeSeconds) { headers.set("Access-Control-Max-Age", `${options.maxAgeSeconds}`); } headers.set( "Vary", "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", ); return headers; } export function getEdgeCorsHeaders( origin: string | null, requestHeaders?: string | null, ): Headers { return getCorsHeaders(origin, { allowedOrigins: EDGE_ALLOWED_ORIGINS, requestHeaders, }); } export function getIslandCorsHeaders( origin: string | null, requestHeaders?: string | null, ): Headers { return getCorsHeaders(origin, { allowedOrigins: ISLAND_ALLOWED_ORIGINS, requestHeaders, }); } export function withCors(response: Response, corsHeaders: Headers): Response { if (!corsHeaders || corsHeaders.values().toArray().length === 0) { return response; } const headers = new Headers(response.headers); for (const [key, value] of corsHeaders.entries()) { if (key.toLowerCase() === "vary") { headers.set("Vary", mergeVary(headers.get("Vary"), value)); } else { headers.set(key, value); } } return new Response(response.body, { status: response.status, statusText: response.statusText, headers, }); } export function createCorsPreflightResponse( _request: Request, corsHeaders: Headers, ): Response { if (!corsHeaders || !corsHeaders.get("Access-Control-Allow-Origin")) { return new Response("Forbidden", { status: 403 }); } return new Response(null, { status: 204, headers: corsHeaders }); } export function getEdgeAllowedOrigins(): string[] { return Array.from(EDGE_ALLOWED_ORIGINS); }