UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

196 lines (173 loc) 5.57 kB
import { $atom, $hook, $inject, $state, type Static, z } from "alepha"; import { $logger } from "alepha/logger"; import { ServerRouterProvider } from "alepha/server"; // --------------------------------------------------------------------------------------------------------------------- /** * CORS configuration atom (global defaults) */ export const corsOptions = $atom({ name: "alepha.server.cors.options", schema: z.object({ origin: z .string() .describe( "Allowed origins (* for all, string for single, comma-separated for multiple)", ) .default("*") .optional(), methods: z .array(z.string()) .describe("Allowed HTTP methods") .default(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]), headers: z .array(z.string()) .describe("Allowed headers") .default(["Content-Type", "Authorization"]), credentials: z .boolean() .describe("Allow credentials") .default(false) .optional(), maxAge: z .number() .describe("Preflight cache duration in seconds") .optional(), }), default: { origin: "*", methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], headers: ["Content-Type", "Authorization"], credentials: false, }, }); export type CorsOptions = Static<typeof corsOptions.schema>; declare module "alepha" { interface State { [corsOptions.key]: CorsOptions; } } // --------------------------------------------------------------------------------------------------------------------- export interface CorsRegistration extends Partial<CorsOptions> { /** * Name identifier for this CORS config. */ name?: string; /** * Path patterns to match (supports wildcards like /api/*). */ paths?: string[]; } export class ServerCorsProvider { protected readonly log = $logger(); protected readonly serverRouterProvider = $inject(ServerRouterProvider); protected readonly globalOptions = $state(corsOptions); /** * Registered CORS configurations with their path patterns */ public readonly registeredConfigs: CorsRegistration[] = []; /** * Register a CORS configuration (called by primitives) */ public registerCors(config: CorsRegistration): void { this.registeredConfigs.push(config); } protected readonly onStart = $hook({ on: "start", handler: async () => { // Apply path-specific CORS configs to routes for (const config of this.registeredConfigs) { if (config.paths) { for (const pattern of config.paths) { const matchedRoutes = this.serverRouterProvider.getRoutes(pattern); for (const route of matchedRoutes) { route.cors = this.buildCorsOptions(config); } } } } if (this.registeredConfigs.length > 0) { this.log.info( `Initialized with ${this.registeredConfigs.length} registered CORS configurations.`, ); } }, }); /** * Build complete CORS options by merging with global defaults */ public buildCorsOptions(config: Partial<CorsOptions>): CorsOptions { return { origin: config.origin ?? this.globalOptions.origin, methods: config.methods ?? this.globalOptions.methods, headers: config.headers ?? this.globalOptions.headers, credentials: config.credentials ?? this.globalOptions.credentials, maxAge: config.maxAge ?? this.globalOptions.maxAge, }; } /** * Apply CORS headers to the response */ public applyCorsHeaders( request: { headers: { origin?: string }; reply: { setHeader: (name: string, value: string) => void }; }, options: CorsOptions, ): void { const reqOrigin = request.headers.origin; const { origin, methods, headers, credentials, maxAge } = options; if (reqOrigin && this.isOriginAllowed(reqOrigin, origin)) { request.reply.setHeader("Access-Control-Allow-Origin", reqOrigin); } if (credentials) { request.reply.setHeader("Access-Control-Allow-Credentials", "true"); } request.reply.setHeader("Access-Control-Allow-Methods", methods.join(", ")); request.reply.setHeader("Access-Control-Allow-Headers", headers.join(", ")); if (maxAge != null) { request.reply.setHeader("Access-Control-Max-Age", String(maxAge)); } } protected readonly configure = $hook({ on: "start", handler: () => { const routes = this.serverRouterProvider.getRoutes(); for (const route of routes) { if ( !route.method || route.method === "GET" || route.method === "OPTIONS" ) { continue; } this.serverRouterProvider.createRoute({ path: route.path, method: "OPTIONS", handler: ({ reply }) => { reply.setStatus(204); }, }); } }, }); protected readonly onRequest = $hook({ on: "server:onRequest", handler: ({ route, request }) => { // Use route-specific CORS if defined, otherwise use global options const corsConfig = route.cors ?? this.globalOptions; this.applyCorsHeaders(request, corsConfig); }, }); public isOriginAllowed( origin: string | undefined, allowed: CorsOptions["origin"], ): boolean { if (!allowed) return false; if (allowed === "*") return true; return allowed .split(",") .map((o) => o.trim()) .includes(origin ?? ""); } } export type ServerCorsProviderOptions = CorsOptions;