diff --git a/packages/api/src/config/api.config.ts b/packages/api/src/config/api.config.ts index 8812edb..fd0e65e 100644 --- a/packages/api/src/config/api.config.ts +++ b/packages/api/src/config/api.config.ts @@ -8,6 +8,7 @@ const envSchema = z.object({ API_RATE_LIMIT_WINDOW: z.string().transform(Number).default(60000), // 1 minute API_RATE_LIMIT_MAX_REQUESTS: z.string().transform(Number).default(100), CORS_ORIGIN: z.string().default('http://localhost:5173'), + ALLOWED_ORIGINS: z.string().default(''), }); export const apiEnv = envSchema.parse({ @@ -17,8 +18,50 @@ export const apiEnv = envSchema.parse({ API_RATE_LIMIT_WINDOW: process.env.API_RATE_LIMIT_WINDOW, API_RATE_LIMIT_MAX_REQUESTS: process.env.API_RATE_LIMIT_MAX_REQUESTS, CORS_ORIGIN: process.env.CORS_ORIGIN, + ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS, }); +/** + * Parse ALLOWED_ORIGINS into a validated set. + * In production, rejects wildcards ('*') and empty values. + * In development, falls back to localhost. + */ +export function getCorsOrigins(): string | string[] { + const origins = (apiEnv.ALLOWED_ORIGINS || '').split(',').filter(Boolean); + + if (apiEnv.NODE_ENV === 'production') { + if (origins.length === 0) { + throw new Error( + 'CORS origin validation (FRE-4749): ALLOWED_ORIGINS is empty in production. ' + + 'Set ALLOWED_ORIGINS to a comma-separated list of allowed origins.' + ); + } + for (const origin of origins) { + if (origin === '*') { + throw new Error( + 'CORS origin validation (FRE-4749): wildcard (*) ALLOWED_ORIGIN in production.' + ); + } + try { + const url = new URL(origin); + if (url.protocol !== 'https:' && url.protocol !== 'http:') { + throw new Error( + `CORS origin validation (FRE-4749): invalid protocol "${url.protocol}" in "${origin}". Expected http: or https:` + ); + } + } catch (err) { + if (err instanceof Error && err.message.startsWith('CORS origin')) throw err; + throw new Error( + `CORS origin validation (FRE-4749): malformed origin "${origin}": ${err instanceof Error ? err.message : String(err)}` + ); + } + } + return origins; + } + + return apiEnv.CORS_ORIGIN || 'http://localhost:5173'; +} + // Rate limit configuration by tier export const rateLimitConfig = { basic: { diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index e0b718f..a3350a3 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -6,8 +6,9 @@ import { rateLimitMiddleware } from './middleware/rate-limit.middleware'; import { spamRateLimitMiddleware } from './middleware/spam-rate-limit.middleware'; import { errorHandlingMiddleware } from './middleware/error-handling.middleware'; import { loggingMiddleware } from './middleware/logging.middleware'; -import { apiEnv, loggingConfig } from './config/api.config'; +import { apiEnv, loggingConfig, getCorsOrigins } from './config/api.config'; import { routes } from './routes'; +import { initDatadog, initSentry, setSentryUser } from '@shieldai/monitoring'; const fastify = Fastify({ logger: loggingConfig, @@ -15,11 +16,15 @@ const fastify = Fastify({ maxParamLength: 500, }); +// Initialize monitoring (must be first import for auto-instrumentation) +initDatadog(); +initSentry(); + // Register plugins async function registerPlugins() { // CORS configuration await fastify.register(cors, { - origin: apiEnv.CORS_ORIGIN, + origin: getCorsOrigins(), methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], credentials: true, }); diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index 474d994..57b4182 100644 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -7,6 +7,11 @@ import { authMiddleware } from "./middleware/auth.middleware"; import { darkwatchRoutes } from "./routes/darkwatch.routes"; import { voiceprintRoutes } from "./routes/voiceprint.routes"; import { correlationRoutes } from "./routes/correlation.routes"; +import { initDatadog, initSentry, captureSentryError } from "@shieldai/monitoring"; +import { getCorsOrigins } from "./config/api.config"; + +initDatadog(); +initSentry(); const app = Fastify({ logger: { @@ -15,7 +20,8 @@ const app = Fastify({ }); async function bootstrap() { - await app.register(cors, { origin: process.env.CORS_ORIGIN || "http://localhost:5173" }); + const corsOrigins = getCorsOrigins(); + await app.register(cors, { origin: corsOrigins }); await app.register(helmet); await app.register(sensible); @@ -42,6 +48,7 @@ async function bootstrap() { app.log.info(`Server listening on port ${process.env.PORT || 3000}`); } catch (err) { app.log.error(err); + captureSentryError(err as Error, { context: "server_startup" }); process.exit(1); } }