UNPKG

webpods

Version:

Append-only log service with OAuth authentication

153 lines 5.63 kB
/** * Rate limiting domain logic */ import { createLogger } from "../logger.js"; import { getConfig } from "../config-loader.js"; const logger = createLogger("webpods:domain:ratelimit"); // Get rate limits from config function getRateLimits() { const config = getConfig(); return { read: config.rateLimits.reads, write: config.rateLimits.writes, pod_create: config.rateLimits.podCreate, stream_create: config.rateLimits.streamCreate, }; } /** * Increment rate limit counter for tracking purposes */ export async function incrementRateLimit(db, identifier, type) { const windowMs = 60 * 60 * 1000; // 1 hour const now = new Date(); const windowEnd = new Date(Math.ceil(now.getTime() / windowMs) * windowMs); const actualWindowStart = new Date(windowEnd.getTime() - windowMs); try { const rateLimitRecord = await db.oneOrNone(`SELECT * FROM rate_limit WHERE identifier = $(identifier) AND action = $(type) AND window_start = $(windowStart)`, { identifier, type, windowStart: actualWindowStart }); if (!rateLimitRecord) { // Create new window with count 1 await db.none(`INSERT INTO rate_limit (id, identifier, action, count, window_start, window_end) VALUES (gen_random_uuid(), $(identifier), $(type), 1, $(windowStart), $(windowEnd))`, { identifier, type, windowStart: actualWindowStart, windowEnd }); } else { // Increment existing counter await db.none(`UPDATE rate_limit SET count = count + 1 WHERE id = $(id)`, { id: rateLimitRecord.id }); } } catch (error) { // Log but don't fail the operation logger.error("Failed to increment rate limit", { error, identifier, type }); } } /** * Check if request is rate limited */ export async function checkRateLimit(db, identifier, type) { const limits = getRateLimits(); const limit = limits[type]; const windowMs = 60 * 60 * 1000; // 1 hour const now = new Date(); const windowStart = new Date(now.getTime() - windowMs); try { // Get or create window const windowEnd = new Date(Math.ceil(now.getTime() / windowMs) * windowMs); const actualWindowStart = new Date(windowEnd.getTime() - windowMs); let rateLimitRecord = await db.oneOrNone(`SELECT * FROM rate_limit WHERE identifier = $(identifier) AND action = $(type) AND window_start = $(windowStart)`, { identifier, type, windowStart: actualWindowStart }); if (!rateLimitRecord) { // Create new window rateLimitRecord = await db.one(`INSERT INTO rate_limit (id, identifier, action, count, window_start, window_end) VALUES (gen_random_uuid(), $(identifier), $(type), 0, $(windowStart), $(windowEnd)) RETURNING *`, { identifier, type, windowStart: actualWindowStart, windowEnd }); } const count = rateLimitRecord.count; const remaining = Math.max(0, limit - count); // Clean old windows (do this before checking limit) await db.none(`DELETE FROM rate_limit WHERE window_start < $(windowStart)`, { windowStart }); if (count >= limit) { return { success: true, data: { allowed: false, remaining: 0, resetAt: windowEnd, }, }; } // Increment counter only if allowed await db.none(`UPDATE rate_limit SET count = count + 1 WHERE id = $(id)`, { id: rateLimitRecord.id }); return { success: true, data: { allowed: true, remaining: remaining - 1, resetAt: windowEnd, }, }; } catch (error) { logger.error("Failed to check rate limit", { error, identifier, type }); // Allow request on error to avoid blocking users return { success: true, data: { allowed: true, remaining: limit, resetAt: new Date(now.getTime() + windowMs), }, }; } } /** * Get rate limit status without incrementing */ export async function getRateLimitStatus(db, identifier, type) { const limits = getRateLimits(); const limit = limits[type]; const windowMs = 60 * 60 * 1000; // 1 hour const now = new Date(); try { const windowEnd = new Date(Math.ceil(now.getTime() / windowMs) * windowMs); const actualWindowStart = new Date(windowEnd.getTime() - windowMs); const rateLimitRecord = await db.oneOrNone(`SELECT * FROM rate_limit WHERE identifier = $(identifier) AND action = $(type) AND window_start = $(windowStart)`, { identifier, type, windowStart: actualWindowStart }); const used = rateLimitRecord?.count || 0; const remaining = Math.max(0, limit - used); const resetAt = windowEnd; return { success: true, data: { limit, used, remaining, resetAt, }, }; } catch (error) { logger.error("Failed to get rate limit status", { error, identifier, type, }); return { success: false, error: { code: "DATABASE_ERROR", message: "Failed to get rate limit status", }, }; } } //# sourceMappingURL=ratelimit.js.map