72 lines
1.9 KiB
TypeScript
72 lines
1.9 KiB
TypeScript
interface RateLimitEntry {
|
|
count: number;
|
|
lastAttempt: number;
|
|
locked: boolean;
|
|
lockedUntil?: number;
|
|
}
|
|
|
|
const attempts = new Map<string, RateLimitEntry>();
|
|
|
|
const CLEANUP_INTERVAL = 10 * 60 * 1000;
|
|
const RESET_WINDOW = 15 * 60 * 1000;
|
|
const MAX_ATTEMPTS = 5;
|
|
const LOCK_THRESHOLD = 10;
|
|
const LOCK_DURATION = 15 * 60 * 1000;
|
|
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
for (const [key, entry] of attempts.entries()) {
|
|
if (now - entry.lastAttempt > RESET_WINDOW) {
|
|
attempts.delete(key);
|
|
}
|
|
}
|
|
}, CLEANUP_INTERVAL);
|
|
|
|
export function checkRateLimit(identifier: string) {
|
|
const now = Date.now();
|
|
let entry = attempts.get(identifier);
|
|
|
|
if (!entry) {
|
|
attempts.set(identifier, { count: 1, lastAttempt: now, locked: false });
|
|
return { allowed: true, delayMs: 0, locked: false };
|
|
}
|
|
|
|
if (now - entry.lastAttempt > RESET_WINDOW) {
|
|
attempts.set(identifier, { count: 1, lastAttempt: now, locked: false });
|
|
return { allowed: true, delayMs: 0, locked: false };
|
|
}
|
|
|
|
if (entry.locked && entry.lockedUntil) {
|
|
if (now < entry.lockedUntil) {
|
|
return { allowed: false, delayMs: entry.lockedUntil - now, locked: true };
|
|
} else {
|
|
entry.locked = false;
|
|
entry.count = 1;
|
|
entry.lastAttempt = now;
|
|
return { allowed: true, delayMs: 0, locked: false };
|
|
}
|
|
}
|
|
|
|
entry.count++;
|
|
entry.lastAttempt = now;
|
|
|
|
const delayMs = Math.min(Math.pow(2, entry.count - 2) * 1000, 30000);
|
|
|
|
if (entry.count >= LOCK_THRESHOLD) {
|
|
entry.locked = true;
|
|
entry.lockedUntil = now + LOCK_DURATION;
|
|
return { allowed: false, delayMs: LOCK_DURATION, locked: true };
|
|
}
|
|
|
|
const allowed = entry.count <= MAX_ATTEMPTS;
|
|
return { allowed, delayMs: allowed ? 0 : delayMs, locked: false };
|
|
}
|
|
|
|
export function resetRateLimit(identifier: string) {
|
|
attempts.delete(identifier);
|
|
}
|
|
|
|
export function getAttemptCount(identifier: string): number {
|
|
return attempts.get(identifier)?.count ?? 0;
|
|
}
|