interface RateLimitResult { allowed: boolean; retryAfter: number; } export async function checkRateLimit( identifier: string, route: string, limit: number, ): Promise { const cache = typeof caches !== "undefined" ? (caches as any).default as Cache | undefined : undefined; if (!cache) return { allowed: true, retryAfter: 0 }; const minute = Math.floor(Date.now() / 60000); const cacheKey = new Request(`https://rate-limit.internal/${identifier}:${route}:${minute}`); let count = 0; const match = await cache.match(cacheKey); if (match) { count = parseInt(await match.text(), 10) || 0; } count++; await cache.put( cacheKey, new Response(String(count), { headers: { "Cache-Control": "public, max-age=60" }, }), ); if (count > limit) { const retryAfter = Math.ceil(((minute + 1) * 60000 - Date.now()) / 1000); return { allowed: false, retryAfter }; } return { allowed: true, retryAfter: 0 }; } export async function hashToken(token: string): Promise { const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token)); return Array.from(new Uint8Array(buf)) .map((b) => b.toString(16).padStart(2, "0")) .join(""); } export function getRouteLimit(method: string, pathname: string): { route: string; limit: number } { if (method === "POST" && pathname === "/api/trade") return { route: "trade", limit: 30 }; if (method === "POST" && /^\/api\/comments\/[^/]+\/vote$/.test(pathname)) return { route: "vote", limit: 20 }; if (method === "POST" && pathname === "/api/comments") return { route: "comments", limit: 10 }; if (method === "GET" && pathname === "/api/search") return { route: "search", limit: 30 }; return { route: "api", limit: 60 }; }