Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

rate-limit.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

rate-limit.tsBlame103 lines · 2 contributors
45e31d0Claude1/**
2 * In-memory rate limiter middleware.
3 *
4 * Provides per-IP rate limiting with sliding window counters.
5 * For production, replace with Redis-based implementation.
6 */
7
8import { createMiddleware } from "hono/factory";
9
10interface RateLimitEntry {
11 count: number;
12 resetAt: number;
13}
14
15const store = new Map<string, RateLimitEntry>();
16
17// Cleanup stale entries every 60 seconds
ea52715copilot-swe-agent[bot]18const _cleanupInterval = setInterval(() => {
45e31d0Claude19 const now = Date.now();
20 for (const [key, entry] of store) {
21 if (entry.resetAt < now) {
22 store.delete(key);
23 }
24 }
25}, 60_000);
26
27/**
28 * Create a rate limiter middleware.
29 * @param maxRequests Maximum requests per window
30 * @param windowMs Window duration in milliseconds
31 * @param keyPrefix Prefix for the rate limit key (allows different limits per route group)
32 */
33export function rateLimit(
34 maxRequests: number,
35 windowMs: number,
36 keyPrefix = "global"
37) {
38 return createMiddleware(async (c, next) => {
699e5c7Claude39 // In test env, expose informational rate-limit headers but do not actually
40 // enforce limits — the shared in-memory store leaks across test files and
41 // would push requests into 429 once accumulated.
4127ecfClaude42 //
43 // Belt-and-braces: refuse to disable enforcement when NODE_ENV=production
44 // even if BUN_ENV/NODE_ENV are also somehow set to "test". A misconfigured
45 // production container with a leaked test env var must not silently drop
46 // rate limiting.
47 const isTestEnv =
48 process.env.NODE_ENV !== "production" &&
49 (process.env.NODE_ENV === "test" || process.env.BUN_ENV === "test");
50 if (isTestEnv) {
699e5c7Claude51 c.header("X-RateLimit-Limit", String(maxRequests));
52 c.header("X-RateLimit-Remaining", String(maxRequests));
53 c.header("X-RateLimit-Reset", String(Math.ceil((Date.now() + windowMs) / 1000)));
54 return next();
55 }
56
45e31d0Claude57 const ip =
58 c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ||
59 c.req.header("x-real-ip") ||
60 "unknown";
61
62 const key = `${keyPrefix}:${ip}`;
63 const now = Date.now();
64 let entry = store.get(key);
65
66 if (!entry || entry.resetAt < now) {
67 entry = { count: 0, resetAt: now + windowMs };
68 store.set(key, entry);
69 }
70
71 entry.count++;
72
73 // Set rate limit headers
74 c.header("X-RateLimit-Limit", String(maxRequests));
75 c.header("X-RateLimit-Remaining", String(Math.max(0, maxRequests - entry.count)));
76 c.header("X-RateLimit-Reset", String(Math.ceil(entry.resetAt / 1000)));
77
78 if (entry.count > maxRequests) {
79 c.header("Retry-After", String(Math.ceil((entry.resetAt - now) / 1000)));
80 return c.json(
81 {
82 error: "Rate limit exceeded",
83 retryAfter: Math.ceil((entry.resetAt - now) / 1000),
84 },
85 429
86 );
87 }
88
89 return next();
90 });
91}
92
93/**
94 * Pre-configured rate limiters for different route groups.
95 */
3e8f8e8Claude96export function clearRateLimitStore() {
97 store.clear();
98}
99
45e31d0Claude100export const apiRateLimit = rateLimit(100, 60_000, "api"); // 100 req/min
101export const authRateLimit = rateLimit(10, 60_000, "auth"); // 10 req/min (login/register)
102export const gitRateLimit = rateLimit(60, 60_000, "git"); // 60 req/min
103export const searchRateLimit = rateLimit(30, 60_000, "search"); // 30 req/min