1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
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<string, RateLimiter>;
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<Response | null> =>
(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<Response | null> =>
(await clickCounterLimiter.isLimited(event)) ? tooManyRequests() : null;
|