UNPKG

alepha

Version:

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

175 lines (174 loc) 5.62 kB
import { $atom, $hook, $inject, $module, $state, AlephaError, createMiddleware, z } from "alepha"; import { $logger } from "alepha/logger"; import { ServerRouterProvider } from "alepha/server"; //#region ../../src/server/cors/providers/ServerCorsProvider.ts /** * CORS configuration atom (global defaults) */ 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 } }); var ServerCorsProvider = class { log = $logger(); serverRouterProvider = $inject(ServerRouterProvider); globalOptions = $state(corsOptions); /** * Registered CORS configurations with their path patterns */ registeredConfigs = []; /** * Register a CORS configuration (called by primitives) */ registerCors(config) { this.registeredConfigs.push(config); } onStart = $hook({ on: "start", handler: async () => { 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 */ buildCorsOptions(config) { 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 */ applyCorsHeaders(request, options) { 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)); } 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); } }); } } }); onRequest = $hook({ on: "server:onRequest", handler: ({ route, request }) => { const corsConfig = route.cors ?? this.globalOptions; this.applyCorsHeaders(request, corsConfig); } }); isOriginAllowed(origin, allowed) { if (!allowed) return false; if (allowed === "*") return true; return allowed.split(",").map((o) => o.trim()).includes(origin ?? ""); } }; //#endregion //#region ../../src/server/cors/primitives/$cors.ts /** * Middleware that applies CORS headers to the response and handles OPTIONS preflight. * * Reads the request from the ALS context and applies the configured * CORS headers via `ServerCorsProvider`. Options are merged with * global CORS defaults. * * For OPTIONS preflight requests, the middleware short-circuits with a 204 response * and skips the handler entirely. * * **Route middleware** — requires a request context (`$action`). Throws if used outside one. * * ```typescript * class ApiController { * getOrders = $action({ * use: [$cors({ origin: "https://app.example.com", credentials: true })], * handler: async ({ query }) => { ... }, * }); * } * ``` */ const $cors = (options) => { return createMiddleware({ name: "$cors", options, handler: ({ alepha, next }) => { const corsProvider = alepha.inject(ServerCorsProvider); return async (...args) => { const request = alepha.get("alepha.http.request"); if (!request) throw new AlephaError("$cors requires a request context (use inside $action)"); const corsConfig = corsProvider.buildCorsOptions(options ?? {}); corsProvider.applyCorsHeaders(request, corsConfig); if (request.method === "OPTIONS") { request.reply.setStatus(204); return; } return next(...args); }; } }); }; //#endregion //#region ../../src/server/cors/index.ts /** * Cross-Origin Resource Sharing configuration. * * **Features:** * - CORS policy definition * * @module alepha.server.cors */ const AlephaServerCors = $module({ name: "alepha.server.cors", services: [ServerCorsProvider] }); //#endregion export { $cors, AlephaServerCors, ServerCorsProvider, corsOptions }; //# sourceMappingURL=index.js.map