Skip to content

Commit 3fb951d

Browse files
authored
Merge pull request #1037 from menawar/feat/cors-policy-management-issue-1000
feat: implement CORS policy management with dynamic origin whitelisting
2 parents ce5eaa5 + 0c60033 commit 3fb951d

2 files changed

Lines changed: 866 additions & 0 deletions

File tree

backend/server.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ import { createRateLimitMiddleware, RATE_LIMIT_HEADERS } from './services/shared
3737
import { applyCompression, compressionPrometheusMetrics } from './services/shared/compression';
3838
import { wrapWithMonitor, type MonitoredPool } from './services/shared/poolMonitor';
3939
import { SubscriptionTier } from '../src/types/subscription';
40+
import {
41+
processCorsRequest,
42+
upsertPolicy,
43+
getCorsAnalytics,
44+
getViolations,
45+
} from './services/shared/corsMiddleware';
4046

4147
export interface StartServerOptions {
4248
port?: number;
@@ -284,12 +290,57 @@ export async function startServer(options: StartServerOptions = {}): Promise<Run
284290

285291
const rateLimitMw = buildRateLimitMiddleware();
286292

293+
// Seed a default permissive CORS policy for the server's own tenant.
294+
// In production, policies should be loaded from the database per-tenant.
295+
upsertPolicy('default', {
296+
allowedOrigins: [
297+
{ origin: process.env['CORS_ALLOWED_ORIGIN'] ?? '*', isWildcard: true },
298+
],
299+
allowCredentials: false,
300+
exposedHeaders: ['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset'],
301+
maxAge: 86400,
302+
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
303+
allowHeaders: [
304+
'Content-Type',
305+
'Authorization',
306+
'X-Api-Key',
307+
'X-Request-Id',
308+
'X-Subscription-Tier',
309+
],
310+
active: true,
311+
});
312+
287313
const server = http.createServer(async (req, res) => {
288314
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
289315
const { pathname } = url;
290316
const method = req.method ?? 'GET';
291317

292318
try {
319+
// -----------------------------------------------------------------
320+
// CORS – applied to every request before routing
321+
// -----------------------------------------------------------------
322+
const origin = typeof req.headers['origin'] === 'string' ? req.headers['origin'] : undefined;
323+
const requestHeaders = typeof req.headers['access-control-request-headers'] === 'string'
324+
? req.headers['access-control-request-headers']
325+
: undefined;
326+
const { headers: corsHeaders, allowed: corsAllowed } = processCorsRequest({
327+
origin,
328+
method,
329+
requestHeaders,
330+
tenantId: 'default',
331+
});
332+
333+
for (const [name, value] of Object.entries(corsHeaders)) {
334+
if (value !== null) res.setHeader(name, value);
335+
}
336+
337+
// Short-circuit OPTIONS preflight
338+
if (method === 'OPTIONS') {
339+
res.writeHead(corsAllowed ? 204 : 403);
340+
res.end();
341+
return;
342+
}
343+
293344
// -----------------------------------------------------------------
294345
// Health (bypass rate limiting)
295346
// -----------------------------------------------------------------
@@ -341,6 +392,26 @@ export async function startServer(options: StartServerOptions = {}): Promise<Run
341392
return;
342393
}
343394

395+
// -----------------------------------------------------------------
396+
// CORS analytics GET /cors/analytics
397+
// -----------------------------------------------------------------
398+
if (pathname === '/cors/analytics' && method === 'GET') {
399+
sendJson(res, 200, getCorsAnalytics());
400+
return;
401+
}
402+
403+
// -----------------------------------------------------------------
404+
// CORS violations GET /cors/violations
405+
// -----------------------------------------------------------------
406+
if (pathname === '/cors/violations' && method === 'GET') {
407+
const tenantId = url.searchParams.get('tenantId') ?? undefined;
408+
const origin = url.searchParams.get('origin') ?? undefined;
409+
const limit = url.searchParams.get('limit') ? Number(url.searchParams.get('limit')) : 100;
410+
const since = url.searchParams.get('since') ?? undefined;
411+
sendJson(res, 200, getViolations({ tenantId, origin, limit, since }));
412+
return;
413+
}
414+
344415
// -----------------------------------------------------------------
345416
// Rate-limit analytics GET /rate-limits/analytics
346417
// -----------------------------------------------------------------
@@ -518,6 +589,8 @@ export async function startServer(options: StartServerOptions = {}): Promise<Run
518589
console.info(`[Server] RateLimit → GET /rate-limits/status/user?userId=...`);
519590
console.info(`[Server] RateLimit → POST /rate-limits/bypass`);
520591
console.info(`[Server] RateLimit → POST /rate-limits/config`);
592+
console.info(`[Server] CORS → GET /cors/analytics`);
593+
console.info(`[Server] CORS → GET /cors/violations`);
521594
resolve();
522595
});
523596
});

0 commit comments

Comments
 (0)