This commit is contained in:
2026-02-16 19:34:35 +00:00
commit 9ab0940ca4
784 changed files with 41710 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
import type { Context, MiddlewareHandler } from 'hono';
import * as errors from '../custom-errors';
type RateLimitEntry = {
count: number;
resetAt: number;
};
type RateLimiterOptions = {
windowMs: number;
max: number;
keyGenerator?: (ctx: Context) => string;
message?: string;
};
const stores = new Map<string, Map<string, RateLimitEntry>>();
function cleanupStore(store: Map<string, RateLimitEntry>) {
const now = Date.now();
for (const [key, entry] of store) {
if (entry.resetAt <= now) {
store.delete(key);
}
}
}
const { PUBLIC_BUILD_ENV } = process.env;
const isProduction = PUBLIC_BUILD_ENV === 'production' || PUBLIC_BUILD_ENV === 'staging';
export function rateLimiter(options: RateLimiterOptions): MiddlewareHandler {
const {
windowMs,
max,
keyGenerator = (ctx) => ctx.req.header('x-forwarded-for') || 'unknown',
message = 'Too many requests, please try again later',
} = options;
const storeId = `${windowMs}-${max}-${Math.random()}`;
const store = new Map<string, RateLimitEntry>();
stores.set(storeId, store);
// Cleanup every minute
setInterval(() => cleanupStore(store), 60_000);
return async (ctx, next) => {
if (!isProduction) return next();
const key = keyGenerator(ctx);
const now = Date.now();
const entry = store.get(key);
if (entry && entry.resetAt > now) {
if (entry.count >= max) {
const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
throw errors.TOO_MANY_REQUESTS(message, retryAfter);
}
entry.count++;
} else {
store.set(key, {
count: 1,
resetAt: now + windowMs,
});
}
await next();
};
}
// Pre-configured rate limiters for common use cases
export const signinRateLimiter = rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5,
keyGenerator: (ctx) => {
const body = ctx.get('body');
return body?.email || ctx.req.header('x-forwarded-for') || 'unknown';
},
message: 'Too many sign in attempts, please try again in 15 minutes',
});
export const forgotPasswordRateLimiter = rateLimiter({
windowMs: 60 * 60 * 1000, // 1 hour
max: 3,
keyGenerator: (ctx) => {
const body = ctx.get('body');
return body?.email || ctx.req.header('x-forwarded-for') || 'unknown';
},
message: 'Too many password reset requests, please try again in 1 hour',
});
export const signupRateLimiter = rateLimiter({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5,
keyGenerator: (ctx) => ctx.req.header('x-forwarded-for') || 'unknown',
message: 'Too many sign up attempts, please try again in 1 hour',
});
export const passkeyRateLimiter = rateLimiter({
windowMs: 60 * 1000, // 1 minute
max: 10,
keyGenerator: (ctx) => {
const email = ctx.req.param('email');
return email || ctx.req.header('x-forwarded-for') || 'unknown';
},
message: 'Too many passkey requests, please try again in 1 minute',
});