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.tsBlame71 lines · 1 contributor
3ef4c9dClaude1/**
2 * Rate limiter — in-memory fixed-window counter keyed by IP (or token).
3 *
4 * Simple on purpose. For multi-node deployments swap the Map for the
5 * `rate_limit_buckets` Postgres table (schema is already there).
6 */
7
8import { createMiddleware } from "hono/factory";
9
10interface Bucket {
11 count: number;
12 windowStart: number;
13}
14
15const store = new Map<string, Bucket>();
16
17function clientKey(c: any, prefix: string): string {
18 const auth = c.req.header("authorization") || "";
19 if (auth.startsWith("Bearer ")) {
20 return `${prefix}:token:${auth.slice(7, 20)}`;
21 }
22 const ip =
23 c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ||
24 c.req.header("x-real-ip") ||
25 c.env?.ip ||
26 "unknown";
27 return `${prefix}:ip:${ip}`;
28}
29
30export function rateLimit(opts: { windowMs: number; max: number; prefix?: string }) {
31 const prefix = opts.prefix || "rl";
32 return createMiddleware(async (c, next) => {
33 const key = clientKey(c, prefix);
34 const now = Date.now();
35 const bucket = store.get(key);
36 let count = 1;
37 if (!bucket || now - bucket.windowStart > opts.windowMs) {
38 store.set(key, { count: 1, windowStart: now });
39 } else {
40 bucket.count++;
41 count = bucket.count;
42 if (bucket.count > opts.max) {
43 const retryMs = opts.windowMs - (now - bucket.windowStart);
44 return c.json(
45 { error: "Too many requests", retryAfterMs: retryMs },
46 429,
47 {
48 "Retry-After": String(Math.ceil(retryMs / 1000)),
49 "X-RateLimit-Limit": String(opts.max),
50 "X-RateLimit-Remaining": "0",
51 }
52 );
53 }
54 }
55 // Periodic sweep (every 5 min worth of requests)
56 if (store.size > 10_000) {
57 for (const [k, b] of store) {
58 if (now - b.windowStart > opts.windowMs * 2) store.delete(k);
59 }
60 }
61 await next();
62 // Set rate-limit headers on the final response (persists even if handler errored)
63 if (c.res) {
64 c.res.headers.set("X-RateLimit-Limit", String(opts.max));
65 c.res.headers.set(
66 "X-RateLimit-Remaining",
67 String(Math.max(0, opts.max - count))
68 );
69 }
70 });
71}