Files
worldmonitor/api/_rate-limit.js
Elie Habib 37f07a6af2 fix(prod): CORS fallback, rate-limit bump, RSS proxy allowlist (#814)
- Add wildcard CORS headers in vercel.json for /api/* routes so Vercel
  infra 500s (which bypass edge function code) still include CORS headers
- Bump rate limit from 300 to 600 req/60s in both rate-limit files to
  accommodate dashboard init burst (~30-40 parallel requests)
- Add smartraveller.gov.au (bare + www) to Railway relay RSS allowlist
- Add redirect hostname validation in fetchWithRedirects to prevent SSRF
  via open redirects on allowed domains
2026-03-03 00:25:09 +04:00

59 lines
1.4 KiB
JavaScript

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
let ratelimit = null;
function getRatelimit() {
if (ratelimit) return ratelimit;
const url = process.env.UPSTASH_REDIS_REST_URL;
const token = process.env.UPSTASH_REDIS_REST_TOKEN;
if (!url || !token) return null;
ratelimit = new Ratelimit({
redis: new Redis({ url, token }),
limiter: Ratelimit.slidingWindow(600, '60 s'),
prefix: 'rl',
analytics: false,
});
return ratelimit;
}
function getClientIp(request) {
return (
request.headers.get('x-real-ip') ||
request.headers.get('cf-connecting-ip') ||
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
'0.0.0.0'
);
}
export async function checkRateLimit(request, corsHeaders) {
const rl = getRatelimit();
if (!rl) return null;
const ip = getClientIp(request);
try {
const { success, limit, reset } = await rl.limit(ip);
if (!success) {
return new Response(JSON.stringify({ error: 'Too many requests' }), {
status: 429,
headers: {
'Content-Type': 'application/json',
'X-RateLimit-Limit': String(limit),
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': String(reset),
'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)),
...corsHeaders,
},
});
}
return null;
} catch {
return null;
}
}