localgreenchain/lib/security/cors.ts
Claude 5ea8bab5c3
Add production deployment infrastructure (Agent 4)
- Docker: Multi-stage Dockerfile with security hardening, docker-compose
  for production and development environments
- Environment: Comprehensive .env.example with all config options,
  lib/config/env.ts for typed environment validation
- Logging: Structured JSON logging with request/response middleware
- Monitoring: Prometheus metrics endpoint, Grafana dashboard, health
  checks (liveness/readiness probes)
- Security: Security headers, rate limiting, CORS middleware
- CI/CD: GitHub Actions workflows for CI, production deploy, and
  preview deployments
- Error tracking: Sentry integration foundation

Files created:
- Docker: Dockerfile, docker-compose.yml, docker-compose.dev.yml, .dockerignore
- Config: lib/config/env.ts, lib/config/index.ts
- Logging: lib/logging/logger.ts, lib/logging/middleware.ts
- Monitoring: lib/monitoring/sentry.ts, lib/monitoring/metrics.ts,
  lib/monitoring/health.ts
- Security: lib/security/headers.ts, lib/security/rateLimit.ts,
  lib/security/cors.ts
- API: pages/api/health/*, pages/api/metrics.ts
- Infra: infra/prometheus/prometheus.yml, infra/grafana/*
2025-11-23 03:54:03 +00:00

131 lines
3.1 KiB
TypeScript

/**
* CORS Middleware
* Agent 4: Production Deployment
*
* Configures Cross-Origin Resource Sharing for API routes.
*/
import type { NextApiRequest, NextApiResponse, NextApiHandler } from 'next';
import { env } from '../config';
interface CorsConfig {
origins: string[];
methods: string[];
allowedHeaders: string[];
exposedHeaders: string[];
credentials: boolean;
maxAge: number;
}
const DEFAULT_CONFIG: CorsConfig = {
origins: env.corsOrigins,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-Requested-With',
'X-Request-Id',
'X-API-Key',
],
exposedHeaders: [
'X-Request-Id',
'X-RateLimit-Limit',
'X-RateLimit-Remaining',
'X-RateLimit-Reset',
],
credentials: true,
maxAge: 86400, // 24 hours
};
/**
* Check if origin is allowed
*/
function isOriginAllowed(origin: string | undefined, allowedOrigins: string[]): boolean {
if (!origin) return false;
return allowedOrigins.some((allowed) => {
// Exact match
if (allowed === origin) return true;
// Wildcard subdomain match (e.g., *.example.com)
if (allowed.startsWith('*.')) {
const domain = allowed.slice(2);
return origin.endsWith(domain) || origin === `https://${domain}` || origin === `http://${domain}`;
}
// All origins allowed
if (allowed === '*') return true;
return false;
});
}
/**
* Apply CORS headers to response
*/
export function applyCorsHeaders(
req: NextApiRequest,
res: NextApiResponse,
config: CorsConfig = DEFAULT_CONFIG
): void {
const origin = req.headers.origin;
// Set allowed origin
if (isOriginAllowed(origin, config.origins)) {
res.setHeader('Access-Control-Allow-Origin', origin!);
} else if (config.origins.includes('*')) {
res.setHeader('Access-Control-Allow-Origin', '*');
}
// Set other CORS headers
if (config.credentials) {
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
res.setHeader('Access-Control-Allow-Methods', config.methods.join(', '));
res.setHeader('Access-Control-Allow-Headers', config.allowedHeaders.join(', '));
res.setHeader('Access-Control-Expose-Headers', config.exposedHeaders.join(', '));
res.setHeader('Access-Control-Max-Age', config.maxAge.toString());
}
/**
* CORS middleware for API routes
*/
export function withCors(
handler: NextApiHandler,
config?: Partial<CorsConfig>
): NextApiHandler {
const mergedConfig: CorsConfig = { ...DEFAULT_CONFIG, ...config };
return async (req: NextApiRequest, res: NextApiResponse) => {
applyCorsHeaders(req, res, mergedConfig);
// Handle preflight requests
if (req.method === 'OPTIONS') {
return res.status(204).end();
}
return handler(req, res);
};
}
/**
* Strict CORS for internal APIs only
*/
export const strictCors = (handler: NextApiHandler) =>
withCors(handler, {
origins: ['http://localhost:3001'],
credentials: true,
});
/**
* Open CORS for public APIs
*/
export const openCors = (handler: NextApiHandler) =>
withCors(handler, {
origins: ['*'],
credentials: false,
});
// Export types
export type { CorsConfig };