| 1 | import { FixedWindowRateLimiter } from './security.js';
|
| 2 |
|
| 3 | const limiter = new FixedWindowRateLimiter();
|
| 4 |
|
| 5 | function clientAddress(request: Request): string {
|
| 6 | return (
|
| 7 | request.headers.get('x-real-ip') ??
|
| 8 | request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??
|
| 9 | 'unknown'
|
| 10 | );
|
| 11 | }
|
| 12 |
|
| 13 | export async function guardRequest(
|
| 14 | request: Request,
|
| 15 | bucket: string,
|
| 16 | options: { limit: number; windowMs?: number; maximumBytes?: number },
|
| 17 | ): Promise<Response | undefined> {
|
| 18 | const result = limiter.consume(
|
| 19 | `${bucket}:${clientAddress(request)}`,
|
| 20 | options.limit,
|
| 21 | options.windowMs ?? 60_000,
|
| 22 | );
|
| 23 | if (!result.allowed)
|
| 24 | return Response.json(
|
| 25 | { error: 'rate_limit_exceeded' },
|
| 26 | {
|
| 27 | status: 429,
|
| 28 | headers: {
|
| 29 | 'cache-control': 'no-store',
|
| 30 | 'retry-after': String(result.retryAfterSeconds),
|
| 31 | },
|
| 32 | },
|
| 33 | );
|
| 34 |
|
| 35 | if (options.maximumBytes !== undefined) {
|
| 36 | const declared = Number(request.headers.get('content-length') ?? 0);
|
| 37 | if (Number.isFinite(declared) && declared > options.maximumBytes)
|
| 38 | return Response.json({ error: 'payload_too_large' }, { status: 413 });
|
| 39 | const body = await request.clone().arrayBuffer();
|
| 40 | if (body.byteLength > options.maximumBytes)
|
| 41 | return Response.json({ error: 'payload_too_large' }, { status: 413 });
|
| 42 | }
|
| 43 | return undefined;
|
| 44 | }
|