45 lines
1.1 KiB
TypeScript
45 lines
1.1 KiB
TypeScript
import "server-only";
|
|
|
|
import { getRedisClient } from "@/lib/redis";
|
|
import { ShareHttpError } from "@/lib/share/http-error";
|
|
|
|
const FIXED_WINDOW_SCRIPT = `
|
|
local count = redis.call("INCR", KEYS[1])
|
|
if count == 1 then
|
|
redis.call("EXPIRE", KEYS[1], ARGV[1])
|
|
end
|
|
return count
|
|
`;
|
|
|
|
export async function enforceShareRateLimit({
|
|
key,
|
|
limit,
|
|
windowSeconds,
|
|
}: {
|
|
key: string;
|
|
limit: number;
|
|
windowSeconds: number;
|
|
}) {
|
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
const window = Math.floor(nowSeconds / windowSeconds);
|
|
const retryAfter = windowSeconds - (nowSeconds % windowSeconds);
|
|
|
|
try {
|
|
const redis = await getRedisClient();
|
|
const count = Number(
|
|
await redis.send("EVAL", [
|
|
FIXED_WINDOW_SCRIPT,
|
|
"1",
|
|
`erika:share:${key}:${window}`,
|
|
String(windowSeconds + 1),
|
|
])
|
|
);
|
|
if (count > limit) {
|
|
throw new ShareHttpError("Rate limit exceeded", 429, retryAfter);
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof ShareHttpError) throw error;
|
|
throw new ShareHttpError("Rate limiting is temporarily unavailable", 503);
|
|
}
|
|
}
|