forked from boshify/style-stealer
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrate-limiter.ts
More file actions
132 lines (112 loc) · 3.19 KB
/
Copy pathrate-limiter.ts
File metadata and controls
132 lines (112 loc) · 3.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/**
* Enhanced rate limiting with multiple strategies
*/
interface RateLimitConfig {
windowMs: number; // Time window in milliseconds
maxRequests: number; // Max requests per window
}
interface RateLimitEntry {
timestamps: number[];
blocked: boolean;
blockedUntil?: number;
}
// In-memory store (use Redis in production for distributed systems)
const rateLimitStore = new Map<string, RateLimitEntry>();
// Default configurations
const CONFIGS = {
perIP: {
windowMs: 60 * 60 * 1000, // 1 hour
maxRequests: parseInt(process.env.RATE_LIMIT_PER_HOUR || '10'),
},
perApiKey: {
windowMs: 60 * 60 * 1000, // 1 hour
maxRequests: parseInt(process.env.RATE_LIMIT_API_KEY || '100'),
},
global: {
windowMs: 60 * 1000, // 1 minute
maxRequests: parseInt(process.env.RATE_LIMIT_GLOBAL || '50'),
},
};
/**
* Check if a key is rate limited
*/
export function checkRateLimit(
key: string,
config: RateLimitConfig = CONFIGS.perIP
): { allowed: boolean; remaining: number; resetAt: number } {
const now = Date.now();
const entry = rateLimitStore.get(key) || { timestamps: [], blocked: false };
// Check if temporarily blocked
if (entry.blocked && entry.blockedUntil && entry.blockedUntil > now) {
return {
allowed: false,
remaining: 0,
resetAt: entry.blockedUntil,
};
}
// Remove expired timestamps
entry.timestamps = entry.timestamps.filter(
(timestamp) => now - timestamp < config.windowMs
);
// Check if limit exceeded
if (entry.timestamps.length >= config.maxRequests) {
// Block for 5 minutes if they exceed the limit
entry.blocked = true;
entry.blockedUntil = now + 5 * 60 * 1000;
rateLimitStore.set(key, entry);
return {
allowed: false,
remaining: 0,
resetAt: entry.blockedUntil,
};
}
// Add current timestamp
entry.timestamps.push(now);
entry.blocked = false;
delete entry.blockedUntil;
rateLimitStore.set(key, entry);
return {
allowed: true,
remaining: config.maxRequests - entry.timestamps.length,
resetAt: now + config.windowMs,
};
}
/**
* Rate limit by IP address
*/
export function rateLimitByIP(ip: string) {
return checkRateLimit(`ip:${ip}`, CONFIGS.perIP);
}
/**
* Rate limit by API key
*/
export function rateLimitByApiKey(apiKey: string) {
return checkRateLimit(`key:${apiKey}`, CONFIGS.perApiKey);
}
/**
* Global rate limit (all requests combined)
*/
export function rateLimitGlobal() {
return checkRateLimit('global', CONFIGS.global);
}
/**
* Clean up old entries periodically
*/
export function cleanupRateLimitStore() {
const now = Date.now();
const maxAge = Math.max(CONFIGS.perIP.windowMs, CONFIGS.perApiKey.windowMs);
for (const [key, entry] of rateLimitStore.entries()) {
// Remove if all timestamps are expired and not blocked
const hasValidTimestamps = entry.timestamps.some(
(timestamp) => now - timestamp < maxAge
);
const isBlocked = entry.blocked && entry.blockedUntil && entry.blockedUntil > now;
if (!hasValidTimestamps && !isBlocked) {
rateLimitStore.delete(key);
}
}
}
// Run cleanup every 5 minutes
if (typeof global !== 'undefined') {
setInterval(cleanupRateLimitStore, 5 * 60 * 1000);
}