UNPKG

@stacksjs/router

Version:
74 lines (73 loc) 2.4 kB
import { HttpError } from "@stacksjs/error-handling"; import { RateLimitError, RateLimiter, defaultIdentity } from "ts-rate-limiter"; import { getCurrentRequest } from "./request-context"; const PERIOD_SECONDS = { second: 1, minute: 60, hour: 3600, day: 86400 }, limiterCache = new Map; function getLimiter(max, windowMs) { const cacheKey = `${windowMs}:${max}`; let limiter = limiterCache.get(cacheKey); if (!limiter) { limiter = new RateLimiter({ windowMs, maxRequests: max, algorithm: "fixed-window", standardHeaders: !1, legacyHeaders: !1 }); limiterCache.set(cacheKey, limiter); } return limiter; } function resolveIdentity(explicit) { if (explicit !== void 0) return explicit; const req = getCurrentRequest(); return req ? defaultIdentity(req) : "anon"; } export function rateLimit(key, max, options = {}) { const id = resolveIdentity(options.identity), bucketKey = `${key}:${id}`, run = async (windowMs) => { const limiter = getLimiter(max, windowMs); try { await limiter.enforce(bucketKey); } catch (err) { if (err instanceof RateLimitError) throw Object.assign(new HttpError(429, "Too many requests", { key, max, retryAfter: err.retryAfter }), { headers: err.toHeaders() }); throw err; } }; return { async per(period) { const seconds = PERIOD_SECONDS[period]; if (!seconds) throw Error(`rateLimit().per: unknown period '${period}'`); await run(seconds * 1000); }, async over(ttlSeconds) { if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) throw Error(`rateLimit().over: ttl must be a positive number, got ${ttlSeconds}`); await run(ttlSeconds * 1000); } }; } export async function rateLimitStatus(key, max, windowSeconds, options = {}) { const id = resolveIdentity(options.identity), bucketKey = `${key}:${id}`, result = await getLimiter(max, windowSeconds * 1000).peek(bucketKey); if (!result) return null; return { count: result.current, limit: result.limit, remaining: Math.max(0, result.limit - result.current) }; } export async function clearRateLimit(key, max, windowSeconds, options = {}) { const id = resolveIdentity(options.identity), bucketKey = `${key}:${id}`; await getLimiter(max, windowSeconds * 1000).reset(bucketKey); }