mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
feat(rate-limits): make rate limits configurable via environment variables (#892)
* feat(rate-limits): make rate limits configurable via environment variables * add defaults for CI
This commit is contained in:
@@ -110,6 +110,18 @@ export const env = createEnv({
|
||||
// Data Retention
|
||||
FREE_PLAN_LOG_RETENTION_DAYS: z.string().optional(), // Log retention days for free plan users
|
||||
|
||||
// Rate Limiting Configuration
|
||||
RATE_LIMIT_WINDOW_MS: z.string().optional().default('60000'), // Rate limit window duration in milliseconds (default: 1 minute)
|
||||
MANUAL_EXECUTION_LIMIT: z.string().optional().default('999999'), // Manual execution bypass value (effectively unlimited)
|
||||
RATE_LIMIT_FREE_SYNC: z.string().optional().default('10'), // Free tier sync API executions per minute
|
||||
RATE_LIMIT_FREE_ASYNC: z.string().optional().default('50'), // Free tier async API executions per minute
|
||||
RATE_LIMIT_PRO_SYNC: z.string().optional().default('25'), // Pro tier sync API executions per minute
|
||||
RATE_LIMIT_PRO_ASYNC: z.string().optional().default('200'), // Pro tier async API executions per minute
|
||||
RATE_LIMIT_TEAM_SYNC: z.string().optional().default('75'), // Team tier sync API executions per minute
|
||||
RATE_LIMIT_TEAM_ASYNC: z.string().optional().default('500'), // Team tier async API executions per minute
|
||||
RATE_LIMIT_ENTERPRISE_SYNC: z.string().optional().default('150'), // Enterprise tier sync API executions per minute
|
||||
RATE_LIMIT_ENTERPRISE_ASYNC: z.string().optional().default('1000'), // Enterprise tier async API executions per minute
|
||||
|
||||
// Real-time Communication
|
||||
SOCKET_SERVER_URL: z.string().url().optional(), // WebSocket server URL for real-time features
|
||||
SOCKET_PORT: z.number().optional(), // Port for WebSocket server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { RateLimiter } from '@/services/queue/RateLimiter'
|
||||
import { RATE_LIMITS } from '@/services/queue/types'
|
||||
import { MANUAL_EXECUTION_LIMIT, RATE_LIMITS } from '@/services/queue/types'
|
||||
|
||||
// Mock the database module
|
||||
vi.mock('@/db', () => ({
|
||||
@@ -34,7 +34,7 @@ describe('RateLimiter', () => {
|
||||
const result = await rateLimiter.checkRateLimit(testUserId, 'free', 'manual', false)
|
||||
|
||||
expect(result.allowed).toBe(true)
|
||||
expect(result.remaining).toBe(999999)
|
||||
expect(result.remaining).toBe(MANUAL_EXECUTION_LIMIT)
|
||||
expect(result.resetAt).toBeInstanceOf(Date)
|
||||
expect(db.select).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -144,8 +144,8 @@ describe('RateLimiter', () => {
|
||||
const status = await rateLimiter.getRateLimitStatus(testUserId, 'free', 'manual', false)
|
||||
|
||||
expect(status.used).toBe(0)
|
||||
expect(status.limit).toBe(999999)
|
||||
expect(status.remaining).toBe(999999)
|
||||
expect(status.limit).toBe(MANUAL_EXECUTION_LIMIT)
|
||||
expect(status.remaining).toBe(MANUAL_EXECUTION_LIMIT)
|
||||
expect(status.resetAt).toBeInstanceOf(Date)
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,13 @@ import { eq, sql } from 'drizzle-orm'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { db } from '@/db'
|
||||
import { userRateLimits } from '@/db/schema'
|
||||
import { RATE_LIMITS, type SubscriptionPlan, type TriggerType } from '@/services/queue/types'
|
||||
import {
|
||||
MANUAL_EXECUTION_LIMIT,
|
||||
RATE_LIMIT_WINDOW_MS,
|
||||
RATE_LIMITS,
|
||||
type SubscriptionPlan,
|
||||
type TriggerType,
|
||||
} from '@/services/queue/types'
|
||||
|
||||
const logger = createLogger('RateLimiter')
|
||||
|
||||
@@ -21,8 +27,8 @@ export class RateLimiter {
|
||||
if (triggerType === 'manual') {
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: 999999,
|
||||
resetAt: new Date(Date.now() + 60000),
|
||||
remaining: MANUAL_EXECUTION_LIMIT,
|
||||
resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +38,7 @@ export class RateLimiter {
|
||||
: limit.syncApiExecutionsPerMinute
|
||||
|
||||
const now = new Date()
|
||||
const windowStart = new Date(now.getTime() - 60000) // 1 minute ago
|
||||
const windowStart = new Date(now.getTime() - RATE_LIMIT_WINDOW_MS)
|
||||
|
||||
// Get or create rate limit record
|
||||
const [rateLimitRecord] = await db
|
||||
@@ -78,7 +84,9 @@ export class RateLimiter {
|
||||
|
||||
// Check if we exceeded the limit
|
||||
if (actualCount > execLimit) {
|
||||
const resetAt = new Date(new Date(insertedRecord.windowStart).getTime() + 60000)
|
||||
const resetAt = new Date(
|
||||
new Date(insertedRecord.windowStart).getTime() + RATE_LIMIT_WINDOW_MS
|
||||
)
|
||||
|
||||
await db
|
||||
.update(userRateLimits)
|
||||
@@ -98,7 +106,7 @@ export class RateLimiter {
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: execLimit - actualCount,
|
||||
resetAt: new Date(new Date(insertedRecord.windowStart).getTime() + 60000),
|
||||
resetAt: new Date(new Date(insertedRecord.windowStart).getTime() + RATE_LIMIT_WINDOW_MS),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +132,9 @@ export class RateLimiter {
|
||||
|
||||
// Check if we exceeded the limit AFTER the atomic increment
|
||||
if (actualNewRequests > execLimit) {
|
||||
const resetAt = new Date(new Date(rateLimitRecord.windowStart).getTime() + 60000)
|
||||
const resetAt = new Date(
|
||||
new Date(rateLimitRecord.windowStart).getTime() + RATE_LIMIT_WINDOW_MS
|
||||
)
|
||||
|
||||
logger.info(
|
||||
`Rate limit exceeded - request ${actualNewRequests} > limit ${execLimit} for user ${userId}`,
|
||||
@@ -154,7 +164,7 @@ export class RateLimiter {
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: execLimit - actualNewRequests,
|
||||
resetAt: new Date(new Date(rateLimitRecord.windowStart).getTime() + 60000),
|
||||
resetAt: new Date(new Date(rateLimitRecord.windowStart).getTime() + RATE_LIMIT_WINDOW_MS),
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error checking rate limit:', error)
|
||||
@@ -162,7 +172,7 @@ export class RateLimiter {
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: 0,
|
||||
resetAt: new Date(Date.now() + 60000),
|
||||
resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,9 +191,9 @@ export class RateLimiter {
|
||||
if (triggerType === 'manual') {
|
||||
return {
|
||||
used: 0,
|
||||
limit: 999999,
|
||||
remaining: 999999,
|
||||
resetAt: new Date(Date.now() + 60000),
|
||||
limit: MANUAL_EXECUTION_LIMIT,
|
||||
remaining: MANUAL_EXECUTION_LIMIT,
|
||||
resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +202,7 @@ export class RateLimiter {
|
||||
? limit.asyncApiExecutionsPerMinute
|
||||
: limit.syncApiExecutionsPerMinute
|
||||
const now = new Date()
|
||||
const windowStart = new Date(now.getTime() - 60000)
|
||||
const windowStart = new Date(now.getTime() - RATE_LIMIT_WINDOW_MS)
|
||||
|
||||
const [rateLimitRecord] = await db
|
||||
.select()
|
||||
@@ -205,7 +215,7 @@ export class RateLimiter {
|
||||
used: 0,
|
||||
limit: execLimit,
|
||||
remaining: execLimit,
|
||||
resetAt: new Date(now.getTime() + 60000),
|
||||
resetAt: new Date(now.getTime() + RATE_LIMIT_WINDOW_MS),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +224,7 @@ export class RateLimiter {
|
||||
used,
|
||||
limit: execLimit,
|
||||
remaining: Math.max(0, execLimit - used),
|
||||
resetAt: new Date(new Date(rateLimitRecord.windowStart).getTime() + 60000),
|
||||
resetAt: new Date(new Date(rateLimitRecord.windowStart).getTime() + RATE_LIMIT_WINDOW_MS),
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error getting rate limit status:', error)
|
||||
@@ -225,7 +235,7 @@ export class RateLimiter {
|
||||
used: 0,
|
||||
limit: execLimit,
|
||||
remaining: execLimit,
|
||||
resetAt: new Date(Date.now() + 60000),
|
||||
resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { InferSelectModel } from 'drizzle-orm'
|
||||
import { env } from '@/lib/env'
|
||||
import type { userRateLimits } from '@/db/schema'
|
||||
|
||||
// Database types
|
||||
@@ -16,22 +17,28 @@ export interface RateLimitConfig {
|
||||
asyncApiExecutionsPerMinute: number
|
||||
}
|
||||
|
||||
// Rate limit window duration in milliseconds
|
||||
export const RATE_LIMIT_WINDOW_MS = Number.parseInt(env.RATE_LIMIT_WINDOW_MS) || 60000
|
||||
|
||||
// Manual execution bypass value (effectively unlimited)
|
||||
export const MANUAL_EXECUTION_LIMIT = Number.parseInt(env.MANUAL_EXECUTION_LIMIT) || 999999
|
||||
|
||||
export const RATE_LIMITS: Record<SubscriptionPlan, RateLimitConfig> = {
|
||||
free: {
|
||||
syncApiExecutionsPerMinute: 10,
|
||||
asyncApiExecutionsPerMinute: 50,
|
||||
syncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_FREE_SYNC) || 10,
|
||||
asyncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_FREE_ASYNC) || 50,
|
||||
},
|
||||
pro: {
|
||||
syncApiExecutionsPerMinute: 25,
|
||||
asyncApiExecutionsPerMinute: 200,
|
||||
syncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_PRO_SYNC) || 25,
|
||||
asyncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_PRO_ASYNC) || 200,
|
||||
},
|
||||
team: {
|
||||
syncApiExecutionsPerMinute: 75,
|
||||
asyncApiExecutionsPerMinute: 500,
|
||||
syncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_TEAM_SYNC) || 75,
|
||||
asyncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_TEAM_ASYNC) || 500,
|
||||
},
|
||||
enterprise: {
|
||||
syncApiExecutionsPerMinute: 150,
|
||||
asyncApiExecutionsPerMinute: 1000,
|
||||
syncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_ENTERPRISE_SYNC) || 150,
|
||||
asyncApiExecutionsPerMinute: Number.parseInt(env.RATE_LIMIT_ENTERPRISE_ASYNC) || 1000,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,18 @@ app:
|
||||
NEXT_PUBLIC_DOCUMENTATION_URL: "" # Documentation URL (leave empty for none)
|
||||
NEXT_PUBLIC_TERMS_URL: "" # Terms of service URL (leave empty for none)
|
||||
NEXT_PUBLIC_PRIVACY_URL: "" # Privacy policy URL (leave empty for none)
|
||||
|
||||
# Rate Limiting Configuration
|
||||
RATE_LIMIT_WINDOW_MS: "60000" # Rate limit window in milliseconds (1 minute)
|
||||
MANUAL_EXECUTION_LIMIT: "999999" # Manual execution limit (effectively unlimited)
|
||||
RATE_LIMIT_FREE_SYNC: "10" # Free tier sync API executions per minute
|
||||
RATE_LIMIT_FREE_ASYNC: "50" # Free tier async API executions per minute
|
||||
RATE_LIMIT_PRO_SYNC: "25" # Pro tier sync API executions per minute
|
||||
RATE_LIMIT_PRO_ASYNC: "200" # Pro tier async API executions per minute
|
||||
RATE_LIMIT_TEAM_SYNC: "75" # Team tier sync API executions per minute
|
||||
RATE_LIMIT_TEAM_ASYNC: "500" # Team tier async API executions per minute
|
||||
RATE_LIMIT_ENTERPRISE_SYNC: "150" # Enterprise tier sync API executions per minute
|
||||
RATE_LIMIT_ENTERPRISE_ASYNC: "1000" # Enterprise tier async API executions per minute
|
||||
|
||||
# Service configuration
|
||||
service:
|
||||
|
||||
Reference in New Issue
Block a user