import type { Context } from 'hono'; import type { MiddlewareHandler } from 'hono/types'; import { AppError, AppErrorCode } from '../../errors/app-error'; import { getIpAddress } from '../../universal/get-ip-address'; import type { createRateLimit, RateLimitCheckResult } from './rate-limit'; /** * Set rate limit response headers on a Hono context. */ const setRateLimitHeaders = (c: Context, result: RateLimitCheckResult) => { c.header('X-RateLimit-Limit', String(result.limit)); c.header('X-RateLimit-Remaining', String(result.remaining)); c.header('X-RateLimit-Reset', String(Math.ceil(result.reset.getTime() / 1000))); }; /** * Create a Hono middleware that applies rate limiting to a route. * * Uses IP address for identification. Optionally accepts an identifier * function for per-user/per-entity limiting. */ export const createRateLimitMiddleware = ( limiter: ReturnType, options?: { identifierFn?: (c: Context) => string | undefined }, ): MiddlewareHandler => { return async (c, next) => { let ip: string; try { ip = getIpAddress(c.req.raw); } catch { ip = 'unknown'; } const identifier = options?.identifierFn?.(c); const result = await limiter.check({ ip, identifier }); setRateLimitHeaders(c, result); if (result.isLimited) { c.header('Retry-After', String(Math.max(1, Math.ceil((result.reset.getTime() - Date.now()) / 1000)))); return c.json({ error: 'Too many requests, please try again later.' }, 429); } await next(); }; }; /** * Helper for inline rate limit checks in Hono auth routes. * * Returns a 429 Response with rate limit headers if limited, or `null` if allowed. */ export const rateLimitResponse = (c: Context, result: RateLimitCheckResult): Response | null => { setRateLimitHeaders(c, result); if (result.isLimited) { c.header('Retry-After', String(Math.max(1, Math.ceil((result.reset.getTime() - Date.now()) / 1000)))); return c.json({ error: 'Too many requests, please try again later.' }, 429); } return null; }; /** * Helper for inline rate limit checks in tRPC routes. * * Throws an AppError with TOO_MANY_REQUESTS code if limited. */ export const assertRateLimit = (result: RateLimitCheckResult): void => { if (result.isLimited) { const retryAfter = String(Math.max(1, Math.ceil((result.reset.getTime() - Date.now()) / 1000))); throw new AppError(AppErrorCode.TOO_MANY_REQUESTS, { message: 'Too many requests, please try again later.', headers: { 'X-RateLimit-Limit': String(result.limit), 'X-RateLimit-Remaining': String(result.remaining), 'X-RateLimit-Reset': String(Math.ceil(result.reset.getTime() / 1000)), 'Retry-After': retryAfter, }, }); } };