import type { RequestEvent } from "@sveltejs/kit"; import { RateLimiter, type RateLimiterStore, } from "sveltekit-rate-limiter/server"; // Storage. The library's default store is in-memory and PER INSTANCE. On // Vercel's serverless runtime, counters do not aggregate across lambda // instances or regions, so these limiters are best-effort backstops there. // Provision a shared store (Upstash Redis / Vercel KV) implementing // RateLimiterStore and return it here for durable limits. Coarse per-IP abuse // protection also runs at the edge via the Vercel WAF (see // docs/vercel-firewall.md); these app-level limiters add what the WAF cannot // express — per-user windows and the per-(IP, badge) click counter. const createStore = (): RateLimiterStore | undefined => { // e.g. `if (env.UPSTASH_REDIS_REST_URL) return new UpstashStore(...)` return undefined; }; const store = createStore(); // Module-level singletons: the counters must persist across requests. (The // previous implementation built a new RateLimiter on every call, so its store // was always empty and it never actually limited anything.) const limiters = { auth: new RateLimiter({ IP: [10, "m"], store }), mutation: new RateLimiter({ IP: [30, "m"], store }), read: new RateLimiter({ IP: [60, "m"], store }), } satisfies Record; export type RateLimitClass = keyof typeof limiters; const tooManyRequests = () => new Response("Too Many Requests", { status: 429 }); export const checkRateLimit = async ( event: RequestEvent, limit: RateLimitClass = "mutation", ): Promise => (await limiters[limit].isLimited(event)) ? tooManyRequests() : null; // Click counter: bound inflation of a single badge without throttling a viewer // browsing many DIFFERENT badges. Keyed on (IP, badge) — which the edge WAF // can't express, since it keys on IP alone. const clickCounterLimiter = new RateLimiter({ store, plugins: [ { rate: [2, "m"], hash: async (event) => { const badgeId = event.url.searchParams.get("incrementClickCount"); return badgeId ? `click:${event.getClientAddress()}:${badgeId}` : true; }, }, ], }); export const checkClickCounterLimit = async ( event: RequestEvent, ): Promise => (await clickCounterLimiter.isLimited(event)) ? tooManyRequests() : null;