Spaces:
Configuration error
Configuration error
File size: 5,039 Bytes
bcce530 5db0abb bcce530 5db0abb bcce530 5db0abb bcce530 5db0abb bcce530 5db0abb bcce530 5db0abb bcce530 5db0abb bcce530 | 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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | import { redisIncr, redisExpire, redisTTL } from './redis'
// Rate limit configurations
const RATE_LIMITS = {
guest: {
max: 10,
window: 3600, // 1 hour in seconds
},
user: {
max: 50,
window: 3600,
},
pro: {
max: 500,
window: 3600,
},
}
// Prefix-specific overrides (for specialized endpoints)
const PREFIX_LIMITS: Record<string, { max: number; window: number }> = {
'search-semantic': { max: 10, window: 3600 }, // 10 semantic searches/hr
'search-semantic-burst': { max: 3, window: 60 }, // 3 per minute burst
'search': { max: 60, window: 3600 }, // 60 regular searches/hr
'prompts': { max: 30, window: 3600 }, // 30 prompt creations/hr
}
// In-memory fallback when Redis is unavailable
const memoryCounters = new Map<string, { count: number; resetAt: number }>()
let lastCleanup = Date.now()
function checkMemoryLimit(key: string, max: number, window: number): { count: number; allowed: boolean } {
const now = Date.now()
// Periodic cleanup (every 5 minutes or when map is large)
if (now - lastCleanup > 300_000 || memoryCounters.size > 5000) {
for (const [k, v] of memoryCounters.entries()) {
if (v.resetAt <= now) memoryCounters.delete(k)
}
lastCleanup = now
}
const entry = memoryCounters.get(key)
if (!entry || entry.resetAt <= now) {
memoryCounters.set(key, { count: 1, resetAt: now + window * 1000 })
return { count: 1, allowed: true }
}
entry.count += 1
return { count: entry.count, allowed: entry.count <= max }
}
interface RateLimitResult {
success: boolean
limit: number
remaining: number
reset: number
error?: string
}
/**
* Check rate limit for a given identifier.
* Supports prefix-based overrides for specialized endpoints.
*/
export async function checkRateLimit(
identifier: string,
isAuthenticated: boolean = false,
isPro: boolean = false,
): Promise<RateLimitResult> {
// Check if there's a prefix-specific limit
let config = isPro ? RATE_LIMITS.pro : isAuthenticated ? RATE_LIMITS.user : RATE_LIMITS.guest
// Extract prefix (e.g., "search-semantic:user:abc123" → "search-semantic")
const colonIndex = identifier.indexOf(':')
if (colonIndex > 0) {
const prefix = identifier.substring(0, colonIndex)
if (PREFIX_LIMITS[prefix]) {
config = PREFIX_LIMITS[prefix]
}
}
const key = `ratelimit:${identifier}`
// Try Redis first
const currentCount = await redisIncr(key)
if (currentCount === null) {
// Redis unavailable — use in-memory fallback
const { count, allowed } = checkMemoryLimit(key, config.max, config.window)
const resetAt = Date.now() + config.window * 1000
if (!allowed) {
return {
success: false,
limit: config.max,
remaining: 0,
reset: resetAt,
error: `Rate limit exceeded. Try again in ${config.window} seconds.`,
}
}
return {
success: true,
limit: config.max,
remaining: Math.max(0, config.max - count),
reset: resetAt,
}
}
// Set expiration on first request
if (currentCount === 1) {
await redisExpire(key, config.window)
}
// Check if limit exceeded
if (currentCount > config.max) {
const ttl = await redisTTL(key)
const resetTime = Date.now() + (ttl || config.window) * 1000
return {
success: false,
limit: config.max,
remaining: 0,
reset: resetTime,
error: `Rate limit exceeded. Try again in ${ttl || config.window} seconds.`,
}
}
// Get TTL for reset time
const ttl = await redisTTL(key)
const resetTime = Date.now() + (ttl || config.window) * 1000
return {
success: true,
limit: config.max,
remaining: config.max - currentCount,
reset: resetTime,
}
}
/**
* Get client identifier from request (IP address or user ID)
*/
export function getClientIdentifier(request: Request, userId?: string): string {
if (userId) {
return `user:${userId}`
}
const cfConnectingIp = request.headers.get('cf-connecting-ip')
const forwarded = request.headers.get('x-forwarded-for')
const realIp = request.headers.get('x-real-ip')
const ip = cfConnectingIp || forwarded?.split(',')[0]?.trim() || realIp || 'unknown'
return `ip:${ip}`
}
/**
* Create rate limit headers for response
*/
export function getRateLimitHeaders(result: RateLimitResult): Headers {
const headers = new Headers()
headers.set('X-RateLimit-Limit', result.limit.toString())
headers.set('X-RateLimit-Remaining', Math.max(0, result.remaining).toString())
headers.set('X-RateLimit-Reset', Math.floor(result.reset / 1000).toString())
return headers
}
|