import { createMiddleware } from "hono/factory";
interface RateLimitEntry {
count: number;
resetAt: number;
}
const store = new Map<string, RateLimitEntry>();
const _cleanupInterval = setInterval(() => {
const now = Date.now();
for (const [key, entry] of store) {
if (entry.resetAt < now) {
store.delete(key);
}
}
}, 60_000);
export function rateLimit(
maxRequests: number,
windowMs: number,
keyPrefix = "global"
) {
return createMiddleware(async (c, next) => {
const isTestEnv =
process.env.NODE_ENV !== "production" &&
(process.env.NODE_ENV === "test" || process.env.BUN_ENV === "test");
if (isTestEnv) {
c.header("X-RateLimit-Limit", String(maxRequests));
c.header("X-RateLimit-Remaining", String(maxRequests));
c.header("X-RateLimit-Reset", String(Math.ceil((Date.now() + windowMs) / 1000)));
return next();
}
const ip =
c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ||
c.req.header("x-real-ip") ||
"unknown";
const key = `${keyPrefix}:${ip}`;
const now = Date.now();
let entry = store.get(key);
if (!entry || entry.resetAt < now) {
entry = { count: 0, resetAt: now + windowMs };
store.set(key, entry);
}
entry.count++;
c.header("X-RateLimit-Limit", String(maxRequests));
c.header("X-RateLimit-Remaining", String(Math.max(0, maxRequests - entry.count)));
c.header("X-RateLimit-Reset", String(Math.ceil(entry.resetAt / 1000)));
if (entry.count > maxRequests) {
c.header("Retry-After", String(Math.ceil((entry.resetAt - now) / 1000)));
return c.json(
{
error: "Rate limit exceeded",
retryAfter: Math.ceil((entry.resetAt - now) / 1000),
},
429
);
}
return next();
});
}
export function clearRateLimitStore() {
store.clear();
}
export const apiRateLimit = rateLimit(100, 60_000, "api");
export const authRateLimit = rateLimit(10, 60_000, "auth");
export const gitRateLimit = rateLimit(60, 60_000, "git");
export const searchRateLimit = rateLimit(30, 60_000, "search");
|