• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

apowers313 / aiforge / 21002763057

14 Jan 2026 05:00PM UTC coverage: 82.93% (-1.8%) from 84.765%
21002763057

push

github

apowers313
chore: delint

993 of 1165 branches covered (85.24%)

Branch coverage included in aggregate %.

5206 of 6310 relevant lines covered (82.5%)

15.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.7
/src/server/api/middleware/rateLimit.ts
1
/**
2
 * Rate limiting middleware for authentication endpoints
3
 */
4
import type { Request, Response, NextFunction, RequestHandler } from 'express';
5
import { ApiError } from './error.js';
1✔
6

7
interface RateLimitOptions {
8
  windowMs: number;       // Time window in milliseconds
9
  maxRequests: number;    // Max requests per window
10
  message?: string;       // Error message when rate limited
11
}
12

13
interface RateLimitEntry {
14
  count: number;
15
  resetTime: number;
16
}
17

18
/**
19
 * Simple in-memory rate limiter
20
 * Uses IP address as the key
21
 */
22
export function createRateLimiter(options: RateLimitOptions): RequestHandler {
1✔
23
  const {
×
24
    windowMs,
×
25
    maxRequests,
×
26
    message = 'Too many requests, please try again later',
×
27
  } = options;
×
28

29
  // Store rate limit data per IP
30
  const store = new Map<string, RateLimitEntry>();
×
31

32
  // Cleanup old entries periodically
33
  const cleanupInterval = setInterval(() => {
×
34
    const now = Date.now();
×
35
    for (const [key, entry] of store.entries()) {
×
36
      if (entry.resetTime <= now) {
×
37
        store.delete(key);
×
38
      }
×
39
    }
×
40
  }, windowMs);
×
41

42
  // Don't prevent process from exiting
43
  cleanupInterval.unref();
×
44

45
  return (req: Request, res: Response, next: NextFunction): void => {
×
46
    const key = getClientKey(req);
×
47
    const now = Date.now();
×
48

49
    let entry = store.get(key);
×
50

51
    // If no entry or window expired, create new entry
52
    if (!entry || entry.resetTime <= now) {
×
53
      entry = {
×
54
        count: 0,
×
55
        resetTime: now + windowMs,
×
56
      };
×
57
      store.set(key, entry);
×
58
    }
×
59

60
    // Increment request count
61
    entry.count++;
×
62

63
    // Set rate limit headers
64
    res.setHeader('X-RateLimit-Limit', String(maxRequests));
×
65
    res.setHeader('X-RateLimit-Remaining', String(Math.max(0, maxRequests - entry.count)));
×
66
    res.setHeader('X-RateLimit-Reset', String(Math.ceil(entry.resetTime / 1000)));
×
67

68
    // Check if over limit
69
    if (entry.count > maxRequests) {
×
70
      const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
×
71
      res.setHeader('Retry-After', String(retryAfter));
×
72

73
      next(ApiError.tooManyRequests(message));
×
74
      return;
×
75
    }
×
76

77
    next();
×
78
  };
×
79
}
×
80

81
/**
82
 * Get client identifier for rate limiting
83
 * Uses X-Forwarded-For if behind proxy, otherwise IP
84
 */
85
function getClientKey(req: Request): string {
34✔
86
  const forwarded = req.headers['x-forwarded-for'];
34✔
87
  if (forwarded) {
34!
88
    const firstIp = Array.isArray(forwarded) ? forwarded[0] : forwarded.split(',')[0];
×
89
    return firstIp?.trim() ?? req.ip ?? 'unknown';
×
90
  }
×
91
  return req.ip ?? 'unknown';
34!
92
}
34✔
93

94
/**
95
 * Creates a rate limiter with a reset function for testing
96
 */
97
export interface ResettableRateLimiter extends RequestHandler {
98
  reset: () => void;
99
}
100

101
/**
102
 * Create a rate limiter that can be reset (useful for testing)
103
 */
104
export function createResettableRateLimiter(options: RateLimitOptions): ResettableRateLimiter {
1✔
105
  const {
6✔
106
    windowMs,
6✔
107
    maxRequests,
6✔
108
    message = 'Too many requests, please try again later',
6✔
109
  } = options;
6✔
110

111
  // Store rate limit data per IP
112
  const store = new Map<string, RateLimitEntry>();
6✔
113

114
  // Cleanup old entries periodically
115
  const cleanupInterval = setInterval(() => {
6✔
116
    const now = Date.now();
×
117
    for (const [key, entry] of store.entries()) {
×
118
      if (entry.resetTime <= now) {
×
119
        store.delete(key);
×
120
      }
×
121
    }
×
122
  }, windowMs);
6✔
123

124
  // Don't prevent process from exiting
125
  cleanupInterval.unref();
6✔
126

127
  const middleware = ((req: Request, res: Response, next: NextFunction): void => {
6✔
128
    const key = getClientKey(req);
34✔
129
    const now = Date.now();
34✔
130

131
    let entry = store.get(key);
34✔
132

133
    // If no entry or window expired, create new entry
134
    if (!entry || entry.resetTime <= now) {
34✔
135
      entry = {
9✔
136
        count: 0,
9✔
137
        resetTime: now + windowMs,
9✔
138
      };
9✔
139
      store.set(key, entry);
9✔
140
    }
9✔
141

142
    // Increment request count
143
    entry.count++;
34✔
144

145
    // Set rate limit headers
146
    res.setHeader('X-RateLimit-Limit', String(maxRequests));
34✔
147
    res.setHeader('X-RateLimit-Remaining', String(Math.max(0, maxRequests - entry.count)));
34✔
148
    res.setHeader('X-RateLimit-Reset', String(Math.ceil(entry.resetTime / 1000)));
34✔
149

150
    // Check if over limit
151
    if (entry.count > maxRequests) {
34✔
152
      const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
5✔
153
      res.setHeader('Retry-After', String(retryAfter));
5✔
154

155
      next(ApiError.tooManyRequests(message));
5✔
156
      return;
5✔
157
    }
5✔
158

159
    next();
29✔
160
  }) as ResettableRateLimiter;
34✔
161

162
  // Add reset function for testing
163
  middleware.reset = (): void => {
6✔
164
    store.clear();
4✔
165
  };
4✔
166

167
  return middleware;
6✔
168
}
6✔
169

170
/**
171
 * Pre-configured rate limiter for login endpoint
172
 * 10 attempts per 15 minutes
173
 */
174
export const loginRateLimiter = createResettableRateLimiter({
1✔
175
  windowMs: 15 * 60 * 1000, // 15 minutes
1✔
176
  maxRequests: 10,
1✔
177
  message: 'Too many login attempts, please try again later',
1✔
178
});
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc