diff --git a/apps/sim/lib/core/execution-limits/types.ts b/apps/sim/lib/core/execution-limits/types.ts index 9b0251668..c77e1022d 100644 --- a/apps/sim/lib/core/execution-limits/types.ts +++ b/apps/sim/lib/core/execution-limits/types.ts @@ -65,7 +65,7 @@ export function getExecutionTimeout( type: 'sync' | 'async' = 'sync' ): number { if (!isBillingEnabled) { - return EXECUTION_TIMEOUTS.enterprise[type] + return EXECUTION_TIMEOUTS.free[type] } return EXECUTION_TIMEOUTS[plan || 'free'][type] } @@ -74,9 +74,7 @@ export function getMaxExecutionTimeout(): number { return EXECUTION_TIMEOUTS.enterprise.async } -export const DEFAULT_EXECUTION_TIMEOUT_MS = isBillingEnabled - ? EXECUTION_TIMEOUTS.free.sync - : EXECUTION_TIMEOUTS.enterprise.sync +export const DEFAULT_EXECUTION_TIMEOUT_MS = EXECUTION_TIMEOUTS.free.sync export function isTimeoutError(error: unknown): boolean { if (!error) return false diff --git a/apps/sim/lib/core/rate-limiter/rate-limiter.test.ts b/apps/sim/lib/core/rate-limiter/rate-limiter.test.ts index 733c10bc3..6aaf4ef33 100644 --- a/apps/sim/lib/core/rate-limiter/rate-limiter.test.ts +++ b/apps/sim/lib/core/rate-limiter/rate-limiter.test.ts @@ -5,6 +5,7 @@ import type { ConsumeResult, RateLimitStorageAdapter, TokenStatus } from './stor import { MANUAL_EXECUTION_LIMIT, RATE_LIMITS, RateLimitError } from './types' vi.mock('@sim/logger', () => loggerMock) +vi.mock('@/lib/core/config/feature-flags', () => ({ isBillingEnabled: true })) interface MockAdapter { consumeTokens: Mock diff --git a/apps/sim/lib/core/rate-limiter/rate-limiter.ts b/apps/sim/lib/core/rate-limiter/rate-limiter.ts index c0af626b5..53711429f 100644 --- a/apps/sim/lib/core/rate-limiter/rate-limiter.ts +++ b/apps/sim/lib/core/rate-limiter/rate-limiter.ts @@ -1,13 +1,9 @@ import { createLogger } from '@sim/logger' +import { createStorageAdapter, type RateLimitStorageAdapter } from './storage' import { - createStorageAdapter, - type RateLimitStorageAdapter, - type TokenBucketConfig, -} from './storage' -import { + getRateLimit, MANUAL_EXECUTION_LIMIT, RATE_LIMIT_WINDOW_MS, - RATE_LIMITS, type RateLimitCounterType, type SubscriptionPlan, type TriggerType, @@ -57,21 +53,6 @@ export class RateLimiter { return isAsync ? 'async' : 'sync' } - private getBucketConfig( - plan: SubscriptionPlan, - counterType: RateLimitCounterType - ): TokenBucketConfig { - const config = RATE_LIMITS[plan] - switch (counterType) { - case 'api-endpoint': - return config.apiEndpoint - case 'async': - return config.async - case 'sync': - return config.sync - } - } - private buildStorageKey(rateLimitKey: string, counterType: RateLimitCounterType): string { return `${rateLimitKey}:${counterType}` } @@ -84,15 +65,6 @@ export class RateLimiter { } } - private createUnlimitedStatus(config: TokenBucketConfig): RateLimitStatus { - return { - requestsPerMinute: MANUAL_EXECUTION_LIMIT, - maxBurst: MANUAL_EXECUTION_LIMIT, - remaining: MANUAL_EXECUTION_LIMIT, - resetAt: new Date(Date.now() + config.refillIntervalMs), - } - } - async checkRateLimitWithSubscription( userId: string, subscription: SubscriptionInfo | null, @@ -107,7 +79,7 @@ export class RateLimiter { const plan = (subscription?.plan || 'free') as SubscriptionPlan const rateLimitKey = this.getRateLimitKey(userId, subscription) const counterType = this.getCounterType(triggerType, isAsync) - const config = this.getBucketConfig(plan, counterType) + const config = getRateLimit(plan, counterType) const storageKey = this.buildStorageKey(rateLimitKey, counterType) const result = await this.storage.consumeTokens(storageKey, 1, config) @@ -152,10 +124,15 @@ export class RateLimiter { try { const plan = (subscription?.plan || 'free') as SubscriptionPlan const counterType = this.getCounterType(triggerType, isAsync) - const config = this.getBucketConfig(plan, counterType) + const config = getRateLimit(plan, counterType) if (triggerType === 'manual') { - return this.createUnlimitedStatus(config) + return { + requestsPerMinute: MANUAL_EXECUTION_LIMIT, + maxBurst: MANUAL_EXECUTION_LIMIT, + remaining: MANUAL_EXECUTION_LIMIT, + resetAt: new Date(Date.now() + config.refillIntervalMs), + } } const rateLimitKey = this.getRateLimitKey(userId, subscription) @@ -178,7 +155,7 @@ export class RateLimiter { }) const plan = (subscription?.plan || 'free') as SubscriptionPlan const counterType = this.getCounterType(triggerType, isAsync) - const config = this.getBucketConfig(plan, counterType) + const config = getRateLimit(plan, counterType) return { requestsPerMinute: config.refillRate, maxBurst: config.maxTokens, diff --git a/apps/sim/lib/core/rate-limiter/types.ts b/apps/sim/lib/core/rate-limiter/types.ts index 9dac4edb4..560d531e0 100644 --- a/apps/sim/lib/core/rate-limiter/types.ts +++ b/apps/sim/lib/core/rate-limiter/types.ts @@ -1,4 +1,5 @@ import { env } from '@/lib/core/config/env' +import { isBillingEnabled } from '@/lib/core/config/feature-flags' import type { CoreTriggerType } from '@/stores/logs/filters/types' import type { TokenBucketConfig } from './storage' @@ -6,6 +7,8 @@ export type TriggerType = CoreTriggerType | 'form' | 'api-endpoint' export type RateLimitCounterType = 'sync' | 'async' | 'api-endpoint' +type RateLimitConfigKey = 'sync' | 'async' | 'apiEndpoint' + export type SubscriptionPlan = 'free' | 'pro' | 'team' | 'enterprise' export interface RateLimitConfig { @@ -18,6 +21,17 @@ export const RATE_LIMIT_WINDOW_MS = Number.parseInt(env.RATE_LIMIT_WINDOW_MS) || export const MANUAL_EXECUTION_LIMIT = Number.parseInt(env.MANUAL_EXECUTION_LIMIT) || 999999 +const DEFAULT_RATE_LIMITS = { + free: { sync: 50, async: 200, apiEndpoint: 30 }, + pro: { sync: 150, async: 1000, apiEndpoint: 100 }, + team: { sync: 300, async: 2500, apiEndpoint: 200 }, + enterprise: { sync: 600, async: 5000, apiEndpoint: 500 }, +} as const + +function toConfigKey(type: RateLimitCounterType): RateLimitConfigKey { + return type === 'api-endpoint' ? 'apiEndpoint' : type +} + function createBucketConfig(ratePerMinute: number, burstMultiplier = 2): TokenBucketConfig { return { maxTokens: ratePerMinute * burstMultiplier, @@ -26,29 +40,64 @@ function createBucketConfig(ratePerMinute: number, burstMultiplier = 2): TokenBu } } +function getRateLimitForPlan(plan: SubscriptionPlan, type: RateLimitConfigKey): TokenBucketConfig { + const envVarMap: Record> = { + free: { + sync: env.RATE_LIMIT_FREE_SYNC, + async: env.RATE_LIMIT_FREE_ASYNC, + apiEndpoint: undefined, + }, + pro: { sync: env.RATE_LIMIT_PRO_SYNC, async: env.RATE_LIMIT_PRO_ASYNC, apiEndpoint: undefined }, + team: { + sync: env.RATE_LIMIT_TEAM_SYNC, + async: env.RATE_LIMIT_TEAM_ASYNC, + apiEndpoint: undefined, + }, + enterprise: { + sync: env.RATE_LIMIT_ENTERPRISE_SYNC, + async: env.RATE_LIMIT_ENTERPRISE_ASYNC, + apiEndpoint: undefined, + }, + } + + const rate = Number.parseInt(envVarMap[plan][type] || '') || DEFAULT_RATE_LIMITS[plan][type] + return createBucketConfig(rate) +} + export const RATE_LIMITS: Record = { free: { - sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_FREE_SYNC) || 50), - async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_FREE_ASYNC) || 200), - apiEndpoint: createBucketConfig(30), + sync: getRateLimitForPlan('free', 'sync'), + async: getRateLimitForPlan('free', 'async'), + apiEndpoint: getRateLimitForPlan('free', 'apiEndpoint'), }, pro: { - sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_PRO_SYNC) || 150), - async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_PRO_ASYNC) || 1000), - apiEndpoint: createBucketConfig(100), + sync: getRateLimitForPlan('pro', 'sync'), + async: getRateLimitForPlan('pro', 'async'), + apiEndpoint: getRateLimitForPlan('pro', 'apiEndpoint'), }, team: { - sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_TEAM_SYNC) || 300), - async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_TEAM_ASYNC) || 2500), - apiEndpoint: createBucketConfig(200), + sync: getRateLimitForPlan('team', 'sync'), + async: getRateLimitForPlan('team', 'async'), + apiEndpoint: getRateLimitForPlan('team', 'apiEndpoint'), }, enterprise: { - sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_ENTERPRISE_SYNC) || 600), - async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_ENTERPRISE_ASYNC) || 5000), - apiEndpoint: createBucketConfig(500), + sync: getRateLimitForPlan('enterprise', 'sync'), + async: getRateLimitForPlan('enterprise', 'async'), + apiEndpoint: getRateLimitForPlan('enterprise', 'apiEndpoint'), }, } +export function getRateLimit( + plan: SubscriptionPlan | undefined, + type: RateLimitCounterType +): TokenBucketConfig { + const key = toConfigKey(type) + if (!isBillingEnabled) { + return RATE_LIMITS.free[key] + } + return RATE_LIMITS[plan || 'free'][key] +} + export class RateLimitError extends Error { statusCode: number constructor(message: string, statusCode = 429) { diff --git a/bun.lock b/bun.lock index 742f99a24..defa6c36f 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "simstudio", @@ -12,7 +13,7 @@ "glob": "13.0.0", "husky": "9.1.7", "lint-staged": "16.0.0", - "turbo": "2.8.0", + "turbo": "2.8.3", }, }, "apps/docs": { @@ -3429,19 +3430,19 @@ "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - "turbo": ["turbo@2.8.0", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.0", "turbo-darwin-arm64": "2.8.0", "turbo-linux-64": "2.8.0", "turbo-linux-arm64": "2.8.0", "turbo-windows-64": "2.8.0", "turbo-windows-arm64": "2.8.0" }, "bin": { "turbo": "bin/turbo" } }, "sha512-hYbxnLEdvJF+DLALS+Ia+PbfNtn0sDP0hH2u9AFoskSUDmcVHSrtwHpzdX94MrRJKo9D9tYxY3MyP20gnlrWyA=="], + "turbo": ["turbo@2.8.3", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.3", "turbo-darwin-arm64": "2.8.3", "turbo-linux-64": "2.8.3", "turbo-linux-arm64": "2.8.3", "turbo-windows-64": "2.8.3", "turbo-windows-arm64": "2.8.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-8Osxz5Tu/Dw2kb31EAY+nhq/YZ3wzmQSmYa1nIArqxgCAldxv9TPlrAiaBUDVnKA4aiPn0OFBD1ACcpc5VFOAQ=="], - "turbo-darwin-64": ["turbo-darwin-64@2.8.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N7f4PYqz25yk8c5kituk09bJ89tE4wPPqKXgYccT6nbEQnGnrdvlyCHLyqViNObTgjjrddqjb1hmDkv7VcxE0g=="], + "turbo-darwin-64": ["turbo-darwin-64@2.8.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-4kXRLfcygLOeNcP6JquqRLmGB/ATjjfehiojL2dJkL7GFm3SPSXbq7oNj8UbD8XriYQ5hPaSuz59iF1ijPHkTw=="], - "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eVzejaP5fn51gmJAPW68U6mSjFaAZ26rPiE36mMdk+tMC4XBGmJHT/fIgrhcrXMvINCl27RF8VmguRe+MBlSuQ=="], + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xF7uCeC0UY0Hrv/tqax0BMbFlVP1J/aRyeGQPZT4NjvIPj8gSPDgFhfkfz06DhUwDg5NgMo04uiSkAWE8WB/QQ=="], - "turbo-linux-64": ["turbo-linux-64@2.8.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ILR45zviYae3icf4cmUISdj8X17ybNcMh3Ms4cRdJF5sS50qDDTv8qeWqO/lPeHsu6r43gVWDofbDZYVuXYL7Q=="], + "turbo-linux-64": ["turbo-linux-64@2.8.3", "", { "os": "linux", "cpu": "x64" }, "sha512-vxMDXwaOjweW/4etY7BxrXCSkvtwh0PbwVafyfT1Ww659SedUxd5rM3V2ZCmbwG8NiCfY7d6VtxyHx3Wh1GoZA=="], - "turbo-linux-arm64": ["turbo-linux-arm64@2.8.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-z9pUa8ENFuHmadPfjEYMRWlXO82t1F/XBDx2XTg+cWWRZHf85FnEB6D4ForJn/GoKEEvwdPhFLzvvhOssom2ug=="], + "turbo-linux-arm64": ["turbo-linux-arm64@2.8.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-mQX7uYBZFkuPLLlKaNe9IjR1JIef4YvY8f21xFocvttXvdPebnq3PK1Zjzl9A1zun2BEuWNUwQIL8lgvN9Pm3Q=="], - "turbo-windows-64": ["turbo-windows-64@2.8.0", "", { "os": "win32", "cpu": "x64" }, "sha512-J6juRSRjmSErEqJCv7nVIq2DgZ2NHXqyeV8NQTFSyIvrThKiWe7FDOO6oYpuR06+C1NW82aoN4qQt4/gYvz25w=="], + "turbo-windows-64": ["turbo-windows-64@2.8.3", "", { "os": "win32", "cpu": "x64" }, "sha512-YLGEfppGxZj3VWcNOVa08h6ISsVKiG85aCAWosOKNUjb6yErWEuydv6/qImRJUI+tDLvDvW7BxopAkujRnWCrw=="], - "turbo-windows-arm64": ["turbo-windows-arm64@2.8.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-qarBZvCu6uka35739TS+y/3CBU3zScrVAfohAkKHG+So+93Wn+5tKArs8HrO2fuTaGou8fMIeTV7V5NgzCVkSQ=="], + "turbo-windows-arm64": ["turbo-windows-arm64@2.8.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-afTUGKBRmOJU1smQSBnFGcbq0iabAPwh1uXu2BVk7BREg30/1gMnJh9DFEQTah+UD3n3ru8V55J83RQNFfqoyw=="], "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], diff --git a/package.json b/package.json index 25613e1a3..c7fe68c2d 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "glob": "13.0.0", "husky": "9.1.7", "lint-staged": "16.0.0", - "turbo": "2.8.0" + "turbo": "2.8.3" }, "lint-staged": { "*.{js,jsx,ts,tsx,json,css,scss}": [