72 lines
1.8 KiB
TypeScript
72 lines
1.8 KiB
TypeScript
/**
|
|
* In-memory sliding-window rate limiter.
|
|
*
|
|
* Suitable for single-node deployments (pilot scale).
|
|
* For multi-replica, replace with Redis-backed store.
|
|
*/
|
|
|
|
interface RateLimitEntry {
|
|
timestamps: number[];
|
|
}
|
|
|
|
const store = new Map<string, RateLimitEntry>();
|
|
|
|
// Cleanup stale entries every 10 minutes
|
|
if (typeof globalThis !== "undefined") {
|
|
// Use globalThis to survive HMR in dev — only one interval
|
|
const key = "__rateLimitCleanup";
|
|
if (!(globalThis as any)[key]) {
|
|
(globalThis as any)[key] = setInterval(() => {
|
|
const now = Date.now();
|
|
for (const [k, entry] of store) {
|
|
entry.timestamps = entry.timestamps.filter((t) => now - t < 3_600_000);
|
|
if (entry.timestamps.length === 0) store.delete(k);
|
|
}
|
|
}, 600_000);
|
|
}
|
|
}
|
|
|
|
export interface RateLimitResult {
|
|
allowed: boolean;
|
|
remaining: number;
|
|
/** Milliseconds until the oldest request in the window expires */
|
|
resetMs: number;
|
|
}
|
|
|
|
/**
|
|
* Check and record a rate-limited action.
|
|
*
|
|
* @param key - Unique key, e.g. `register:${ip}`
|
|
* @param limit - Max allowed actions in the window
|
|
* @param windowMs - Window size in milliseconds
|
|
*/
|
|
export function rateLimit(
|
|
key: string,
|
|
limit: number,
|
|
windowMs: number,
|
|
): RateLimitResult {
|
|
const now = Date.now();
|
|
const entry = store.get(key) ?? { timestamps: [] };
|
|
|
|
// Prune expired timestamps
|
|
entry.timestamps = entry.timestamps.filter((t) => now - t < windowMs);
|
|
|
|
if (entry.timestamps.length >= limit) {
|
|
const oldest = entry.timestamps[0];
|
|
return {
|
|
allowed: false,
|
|
remaining: 0,
|
|
resetMs: oldest + windowMs - now,
|
|
};
|
|
}
|
|
|
|
entry.timestamps.push(now);
|
|
store.set(key, entry);
|
|
|
|
return {
|
|
allowed: true,
|
|
remaining: limit - entry.timestamps.length,
|
|
resetMs: entry.timestamps[0] + windowMs - now,
|
|
};
|
|
}
|