alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
175 lines (174 loc) • 5.62 kB
JavaScript
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