rate-limit.ts 1.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. const WINDOW_MS = 15 * 60 * 1000; // 15 minutes
  2. const MAX_ATTEMPTS = 5;
  3. interface RateLimitEntry {
  4. count: number;
  5. resetAt: number;
  6. }
  7. const store = new Map<string, RateLimitEntry>();
  8. function cleanup() {
  9. const now = Date.now();
  10. for (const [key, entry] of store) {
  11. if (now >= entry.resetAt) {
  12. store.delete(key);
  13. }
  14. }
  15. }
  16. let cleanupInterval: ReturnType<typeof setInterval> | null = null;
  17. function ensureCleanupRunning() {
  18. if (cleanupInterval) return;
  19. cleanupInterval = setInterval(cleanup, 5 * 60 * 1000);
  20. if (typeof cleanupInterval === "object" && "unref" in cleanupInterval) {
  21. cleanupInterval.unref();
  22. }
  23. }
  24. export function checkRateLimit(key: string): {
  25. allowed: boolean;
  26. remaining: number;
  27. retryAfterMs: number;
  28. } {
  29. ensureCleanupRunning();
  30. const now = Date.now();
  31. const entry = store.get(key);
  32. if (!entry || now >= entry.resetAt) {
  33. store.set(key, { count: 1, resetAt: now + WINDOW_MS });
  34. return { allowed: true, remaining: MAX_ATTEMPTS - 1, retryAfterMs: 0 };
  35. }
  36. if (entry.count >= MAX_ATTEMPTS) {
  37. return {
  38. allowed: false,
  39. remaining: 0,
  40. retryAfterMs: entry.resetAt - now,
  41. };
  42. }
  43. entry.count += 1;
  44. return {
  45. allowed: true,
  46. remaining: MAX_ATTEMPTS - entry.count,
  47. retryAfterMs: 0,
  48. };
  49. }
  50. export function getClientIp(request: Request): string {
  51. const forwarded = request.headers.get("x-forwarded-for");
  52. if (forwarded) {
  53. return forwarded.split(",")[0].trim();
  54. }
  55. return "unknown";
  56. }