Files
etcprs/src/lib/server/ratelimit.js
2026-03-18 03:06:27 -06:00

56 lines
1.6 KiB
JavaScript

import { env } from '$env/dynamic/private';
// In-memory store: Map<ip, { count, windowStart }>
const publishStore = new Map();
const readStore = new Map();
const PUBLISH_LIMIT = parseInt(env.RATE_LIMIT_PUBLISH ?? '5');
const READ_LIMIT = parseInt(env.RATE_LIMIT_READ ?? '100');
const PUBLISH_WINDOW_MS = 60 * 60 * 1000; // 1 hour
const READ_WINDOW_MS = 60 * 1000; // 1 minute
function check(store, ip, limit, windowMs) {
const now = Date.now();
const entry = store.get(ip);
if (!entry || now - entry.windowStart > windowMs) {
store.set(ip, { count: 1, windowStart: now });
return { allowed: true, remaining: limit - 1 };
}
if (entry.count >= limit) {
const retryAfter = Math.ceil((entry.windowStart + windowMs - now) / 1000);
return { allowed: false, remaining: 0, retryAfter };
}
entry.count += 1;
return { allowed: true, remaining: limit - entry.count };
}
export function checkPublishRate(ip) {
return check(publishStore, ip, PUBLISH_LIMIT, PUBLISH_WINDOW_MS);
}
export function checkReadRate(ip) {
return check(readStore, ip, READ_LIMIT, READ_WINDOW_MS);
}
export function getClientIp(request) {
return (
request.headers.get('x-forwarded-for')?.split(',')[0].trim() ??
request.headers.get('x-real-ip') ??
'127.0.0.1'
);
}
// Cleanup old entries every 10 minutes to prevent memory leak
setInterval(() => {
const now = Date.now();
for (const [ip, entry] of publishStore) {
if (now - entry.windowStart > PUBLISH_WINDOW_MS) publishStore.delete(ip);
}
for (const [ip, entry] of readStore) {
if (now - entry.windowStart > READ_WINDOW_MS) readStore.delete(ip);
}
}, 10 * 60 * 1000);