import { Injectable,NestMiddleware } from '@nestjs/common';
import type { NextFunction,Request,Response } from 'express';
type Bucket={windowStart:number;count:number};
@Injectable()
export class RequestSecurityMiddleware implements NestMiddleware {
private readonly buckets=new Map<string,Bucket>();
use(req:Request,res:Response,next:NextFunction) {
const maxBytes=Number(process.env.MAX_REQUEST_BODY_BYTES??2_000_000);
const contentLength=Number(req.header('content-length')??0);
if(contentLength>maxBytes) {
res.status(413).json({error:{code:'http.payload_too_large'}});
return;
}
const allowed=(process.env.CORS_ALLOWED_ORIGINS??'')
.split(',').map((x:string)=>x.trim()).filter(Boolean);
const origin=req.header('origin');
const allowedOrigin = typeof origin === 'string' && allowed.includes(origin)
? origin
: undefined;
if(allowedOrigin) {
res.setHeader('Access-Control-Allow-Origin',allowedOrigin);
res.setHeader('Vary','Origin');
res.setHeader('Access-Control-Allow-Credentials','false');
res.setHeader('Access-Control-Allow-Methods','GET,POST,PUT,PATCH,DELETE,OPTIONS');
res.setHeader(
'Access-Control-Allow-Headers',
'Authorization,Content-Type,Idempotency-Key,X-Correlation-Id,X-Dev-User-Id,X-Location-Id,X-Organization-Id',
);
res.setHeader('Access-Control-Max-Age','600');
}
if(req.method==='OPTIONS') {
if(allowedOrigin) {
res.status(204).end();
return;
}
next();
return;
}
res.setHeader('X-Content-Type-Options','nosniff');
res.setHeader('Referrer-Policy','no-referrer');
res.setHeader('Permissions-Policy','camera=(), microphone=(), geolocation=()');
res.setHeader('Cross-Origin-Resource-Policy','same-site');
// Low-cost per-process abuse protection. Production edge/WAF rate limits remain required.
const windowMs=60_000;
const max=Number(process.env.API_RATE_LIMIT_PER_MINUTE??600);
const key=`${req.ip}:${req.path.startsWith('/api/v1/public/')?'public':'api'}`;
const now=Date.now();
let b=this.buckets.get(key);
if(!b || now-b.windowStart>=windowMs) b={windowStart:now,count:0};
b.count++;
this.buckets.set(key,b);
if(b.count>max) {
res.setHeader('Retry-After','60');
res.status(429).json({error:{code:'http.rate_limited'}});
return;
}
if(this.buckets.size>20_000) {
for(const [k,v] of this.buckets) if(now-v.windowStart>windowMs*2) this.buckets.delete(k);
}
next();
}
}
