Files
stack/apps/orchestrator/src/common/guards/throttler.guard.ts
Jason Woltje ce7fb27c46 fix(#338): Add rate limiting to orchestrator API
- Add @nestjs/throttler for rate limiting support
- Configure multiple throttle profiles: default (100/min), strict (10/min for spawn/kill), status (200/min for polling)
- Apply strict rate limits to spawn and kill endpoints to prevent DoS
- Apply higher rate limits to status/health endpoints for monitoring
- Add OrchestratorThrottlerGuard with X-Forwarded-For support for proxy setups
- Add unit tests for throttler guard

Refs #338

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 18:26:50 -06:00

64 lines
2.0 KiB
TypeScript

import { Injectable, ExecutionContext } from "@nestjs/common";
import { ThrottlerGuard, ThrottlerException } from "@nestjs/throttler";
interface RequestWithHeaders {
headers?: Record<string, string | string[]>;
ip?: string;
connection?: { remoteAddress?: string };
url?: string;
}
/**
* OrchestratorThrottlerGuard - Rate limiting guard for orchestrator API endpoints
*
* Uses the X-Forwarded-For header for client IP identification when behind a proxy,
* falling back to the direct connection IP.
*
* Usage:
* @UseGuards(OrchestratorThrottlerGuard)
* @Controller('agents')
* export class AgentsController { ... }
*/
@Injectable()
export class OrchestratorThrottlerGuard extends ThrottlerGuard {
/**
* Get the client IP address for rate limiting tracking
* Prioritizes X-Forwarded-For header for proxy setups
*/
protected getTracker(req: Record<string, unknown>): Promise<string> {
const request = req as RequestWithHeaders;
const headers = request.headers;
// Check X-Forwarded-For header first (for proxied requests)
if (headers) {
const forwardedFor = headers["x-forwarded-for"];
if (forwardedFor) {
// Get the first IP in the chain (original client)
const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
if (ips) {
const clientIp = ips.split(",")[0]?.trim();
if (clientIp) {
return Promise.resolve(clientIp);
}
}
}
}
// Fallback to direct connection IP
const ip = request.ip ?? request.connection?.remoteAddress ?? "unknown";
return Promise.resolve(ip);
}
/**
* Custom error message for rate limit exceeded
*/
protected throwThrottlingException(context: ExecutionContext): Promise<void> {
const request = context.switchToHttp().getRequest<RequestWithHeaders>();
const endpoint = request.url ?? "unknown";
throw new ThrottlerException(
`Rate limit exceeded for endpoint ${endpoint}. Please try again later.`
);
}
}