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.tsBlame95 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.
42 if (process.env.NODE_ENV === "test" || process.env.BUN_ENV === "test") {
43 c.header("X-RateLimit-Limit", String(maxRequests));
44 c.header("X-RateLimit-Remaining", String(maxRequests));
45 c.header("X-RateLimit-Reset", String(Math.ceil((Date.now() + windowMs) / 1000)));
46 return next();
47 }
48
45e31d0Claude49 const ip =
50 c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ||
51 c.req.header("x-real-ip") ||
52 "unknown";
53
54 const key = `${keyPrefix}:${ip}`;
55 const now = Date.now();
56 let entry = store.get(key);
57
58 if (!entry || entry.resetAt < now) {
59 entry = { count: 0, resetAt: now + windowMs };
60 store.set(key, entry);
61 }
62
63 entry.count++;
64
65 // Set rate limit headers
66 c.header("X-RateLimit-Limit", String(maxRequests));
67 c.header("X-RateLimit-Remaining", String(Math.max(0, maxRequests - entry.count)));
68 c.header("X-RateLimit-Reset", String(Math.ceil(entry.resetAt / 1000)));
69
70 if (entry.count > maxRequests) {
71 c.header("Retry-After", String(Math.ceil((entry.resetAt - now) / 1000)));
72 return c.json(
73 {
74 error: "Rate limit exceeded",
75 retryAfter: Math.ceil((entry.resetAt - now) / 1000),
76 },
77 429
78 );
79 }
80
81 return next();
82 });
83}
84
85/**
86 * Pre-configured rate limiters for different route groups.
87 */
3e8f8e8Claude88export function clearRateLimitStore() {
89 store.clear();
90}
91
45e31d0Claude92export const apiRateLimit = rateLimit(100, 60_000, "api"); // 100 req/min
93export const authRateLimit = rateLimit(10, 60_000, "auth"); // 10 req/min (login/register)
94export const gitRateLimit = rateLimit(60, 60_000, "git"); // 60 req/min
95export const searchRateLimit = rateLimit(30, 60_000, "search"); // 30 req/min