Skip to content

Commit 6a5eebc

Browse files
authored
v0.6.79: rate limits, tables checkboxes, drizzle config changes, billing txes
2 parents c09a2c9 + c3ac54e commit 6a5eebc

34 files changed

Lines changed: 1509 additions & 256 deletions

File tree

apps/realtime/src/database/operations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const socketDb = drizzle(
3030
prepare: false,
3131
idle_timeout: 10,
3232
connect_timeout: 20,
33-
max: 30,
33+
max: 15,
3434
onnotice: () => {},
3535
}),
3636
{ schema }

apps/sim/app/api/auth/socket-token/route.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
import { createLogger } from '@sim/logger'
22
import { toError } from '@sim/utils/errors'
33
import { headers } from 'next/headers'
4-
import { NextResponse } from 'next/server'
4+
import { type NextRequest, NextResponse } from 'next/server'
55
import { auth } from '@/lib/auth'
66
import { isAuthDisabled } from '@/lib/core/config/feature-flags'
7+
import { enforceIpRateLimit } from '@/lib/core/rate-limiter'
78
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
89

910
const logger = createLogger('SocketTokenAPI')
1011

11-
export const POST = withRouteHandler(async () => {
12+
export const POST = withRouteHandler(async (request: NextRequest) => {
1213
if (isAuthDisabled) {
1314
return NextResponse.json({ token: 'anonymous-socket-token' })
1415
}
1516

17+
const rateLimited = await enforceIpRateLimit('socket-token', request, {
18+
maxTokens: 30,
19+
refillRate: 30,
20+
refillIntervalMs: 60_000,
21+
})
22+
if (rateLimited) return rateLimited
23+
1624
try {
1725
const hdrs = await headers()
1826
const response = await auth.api.generateOneTimeToken({

apps/sim/app/api/auth/sso/providers/route.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { type NextRequest, NextResponse } from 'next/server'
55
import { listSsoProvidersContract } from '@/lib/api/contracts/auth'
66
import { parseRequest } from '@/lib/api/server'
77
import { getSession } from '@/lib/auth'
8+
import { enforceIpRateLimit } from '@/lib/core/rate-limiter'
89
import { REDACTED_MARKER } from '@/lib/core/security/redaction'
910
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1011

@@ -13,6 +14,14 @@ const logger = createLogger('SSOProvidersRoute')
1314
export const GET = withRouteHandler(async (request: NextRequest) => {
1415
try {
1516
const session = await getSession()
17+
if (!session?.user?.id) {
18+
const rateLimited = await enforceIpRateLimit('sso-providers', request, {
19+
maxTokens: 20,
20+
refillRate: 20,
21+
refillIntervalMs: 60_000,
22+
})
23+
if (rateLimited) return rateLimited
24+
}
1625
const parsed = await parseRequest(listSsoProvidersContract, request, {})
1726
if (!parsed.success) return parsed.response
1827
const { organizationId } = parsed.data.query

apps/sim/app/api/chat/[identifier]/otp/route.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ describe('Chat OTP API Route', () => {
423423
expect(headerSet).toHaveBeenCalledWith('Retry-After', '900')
424424
})
425425

426-
it('skips IP rate limit when client IP is unknown', async () => {
426+
it('folds spoofed `unknown` client IPs into a single shared bucket', async () => {
427427
requestUtilsMockFns.mockGetClientIp.mockReturnValueOnce('unknown')
428428
buildDeploymentSelect()
429429

@@ -434,8 +434,11 @@ describe('Chat OTP API Route', () => {
434434

435435
await POST(request, { params: Promise.resolve({ identifier: mockIdentifier }) })
436436

437-
// Only the email-scoped check should run, not the IP-scoped one
438-
expect(mockCheckRateLimitDirect).toHaveBeenCalledTimes(1)
437+
expect(mockCheckRateLimitDirect).toHaveBeenCalledTimes(2)
438+
expect(mockCheckRateLimitDirect).toHaveBeenCalledWith(
439+
expect.stringMatching(/^chat-otp:ip:.*:unknown$/),
440+
expect.any(Object)
441+
)
439442
expect(mockCheckRateLimitDirect).toHaveBeenCalledWith(
440443
expect.stringContaining('chat-otp:email:'),
441444
expect.any(Object)

apps/sim/app/api/chat/[identifier]/otp/route.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -223,20 +223,18 @@ export const POST = withRouteHandler(
223223

224224
try {
225225
const ip = getClientIp(request)
226-
if (ip !== 'unknown') {
227-
const ipRateLimit = await rateLimiter.checkRateLimitDirect(
228-
`chat-otp:ip:${identifier}:${ip}`,
229-
OTP_IP_RATE_LIMIT
226+
const ipRateLimit = await rateLimiter.checkRateLimitDirect(
227+
`chat-otp:ip:${identifier}:${ip}`,
228+
OTP_IP_RATE_LIMIT
229+
)
230+
if (!ipRateLimit.allowed) {
231+
logger.warn(`[${requestId}] OTP IP rate limit exceeded for ${identifier} from ${ip}`)
232+
const retryAfter = Math.ceil(
233+
(ipRateLimit.retryAfterMs ?? OTP_IP_RATE_LIMIT.refillIntervalMs) / 1000
230234
)
231-
if (!ipRateLimit.allowed) {
232-
logger.warn(`[${requestId}] OTP IP rate limit exceeded for ${identifier} from ${ip}`)
233-
const retryAfter = Math.ceil(
234-
(ipRateLimit.retryAfterMs ?? OTP_IP_RATE_LIMIT.refillIntervalMs) / 1000
235-
)
236-
const response = createErrorResponse('Too many requests. Please try again later.', 429)
237-
response.headers.set('Retry-After', String(retryAfter))
238-
return addCorsHeaders(response, request)
239-
}
235+
const response = createErrorResponse('Too many requests. Please try again later.', 429)
236+
response.headers.set('Retry-After', String(retryAfter))
237+
return addCorsHeaders(response, request)
240238
}
241239

242240
const parsed = await parseRequest(requestChatEmailOtpContract, request, context, {

apps/sim/app/api/chat/[identifier]/sso/route.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,18 @@ export const POST = withRouteHandler(
3030
const requestId = generateRequestId()
3131

3232
const ip = getClientIp(request)
33-
if (ip !== 'unknown') {
34-
const ipRateLimit = await rateLimiter.checkRateLimitDirect(
35-
`chat-sso:ip:${ip}`,
36-
SSO_IP_RATE_LIMIT
33+
const ipRateLimit = await rateLimiter.checkRateLimitDirect(
34+
`chat-sso:ip:${ip}`,
35+
SSO_IP_RATE_LIMIT
36+
)
37+
if (!ipRateLimit.allowed) {
38+
logger.warn(`[${requestId}] SSO eligibility rate limit exceeded from ${ip}`)
39+
const retryAfter = Math.ceil(
40+
(ipRateLimit.retryAfterMs ?? SSO_IP_RATE_LIMIT.refillIntervalMs) / 1000
3741
)
38-
if (!ipRateLimit.allowed) {
39-
logger.warn(`[${requestId}] SSO eligibility rate limit exceeded from ${ip}`)
40-
const retryAfter = Math.ceil(
41-
(ipRateLimit.retryAfterMs ?? SSO_IP_RATE_LIMIT.refillIntervalMs) / 1000
42-
)
43-
const response = createErrorResponse('Too many requests. Please try again later.', 429)
44-
response.headers.set('Retry-After', String(retryAfter))
45-
return addCorsHeaders(response, request)
46-
}
42+
const response = createErrorResponse('Too many requests. Please try again later.', 429)
43+
response.headers.set('Retry-After', String(retryAfter))
44+
return addCorsHeaders(response, request)
4745
}
4846

4947
const parsed = await parseRequest(chatSSOContract, request, context)

apps/sim/app/api/mcp/servers/[id]/refresh/route.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { db } from '@sim/db'
22
import { mcpServers, workflow, workflowBlocks } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { toError } from '@sim/utils/errors'
5-
import { and, eq, isNull } from 'drizzle-orm'
5+
import { and, eq, inArray, isNull } from 'drizzle-orm'
66
import type { NextRequest } from 'next/server'
77
import { mcpServerIdParamsSchema } from '@/lib/api/contracts/mcp'
88
import { validationErrorResponse } from '@/lib/api/server'
@@ -77,13 +77,11 @@ async function syncToolSchemasToWorkflows(
7777
subBlocks: workflowBlocks.subBlocks,
7878
})
7979
.from(workflowBlocks)
80-
.where(eq(workflowBlocks.type, 'agent'))
80+
.where(and(eq(workflowBlocks.type, 'agent'), inArray(workflowBlocks.workflowId, workflowIds)))
8181

8282
const updatedWorkflowIds = new Set<string>()
8383

8484
for (const block of agentBlocks) {
85-
if (!workflowIds.includes(block.workflowId)) continue
86-
8785
const subBlocks = block.subBlocks as Record<string, unknown> | null
8886
if (!subBlocks) continue
8987

apps/sim/app/api/mcp/tools/stored/route.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { db } from '@sim/db'
22
import { workflow, workflowBlocks } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { toError } from '@sim/utils/errors'
5-
import { eq } from 'drizzle-orm'
5+
import { and, eq, inArray } from 'drizzle-orm'
66
import type { NextRequest } from 'next/server'
77
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
88
import { withMcpAuth } from '@/lib/mcp/middleware'
@@ -33,13 +33,13 @@ export const GET = withRouteHandler(
3333
const agentBlocks = await db
3434
.select({ workflowId: workflowBlocks.workflowId, subBlocks: workflowBlocks.subBlocks })
3535
.from(workflowBlocks)
36-
.where(eq(workflowBlocks.type, 'agent'))
36+
.where(
37+
and(eq(workflowBlocks.type, 'agent'), inArray(workflowBlocks.workflowId, workflowIds))
38+
)
3739

3840
const storedTools: StoredMcpTool[] = []
3941

4042
for (const block of agentBlocks) {
41-
if (!workflowMap.has(block.workflowId)) continue
42-
4343
const subBlocks = block.subBlocks as Record<string, unknown> | null
4444
if (!subBlocks) continue
4545

apps/sim/app/api/telemetry/route.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { telemetryContract } from '@/lib/api/contracts/telemetry'
44
import { parseRequest } from '@/lib/api/server'
55
import { env } from '@/lib/core/config/env'
66
import { isProd } from '@/lib/core/config/feature-flags'
7+
import { enforceIpRateLimit } from '@/lib/core/rate-limiter'
78
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
89

910
const logger = createLogger('TelemetryAPI')
@@ -148,6 +149,13 @@ async function forwardToCollector(data: Record<string, unknown>): Promise<boolea
148149
* Endpoint that receives telemetry events and forwards them to OpenTelemetry collector
149150
*/
150151
export const POST = withRouteHandler(async (req: NextRequest) => {
152+
const rateLimited = await enforceIpRateLimit('telemetry', req, {
153+
maxTokens: 60,
154+
refillRate: 30,
155+
refillIntervalMs: 60_000,
156+
})
157+
if (rateLimited) return rateLimited
158+
151159
try {
152160
const parsed = await parseRequest(telemetryContract, req, {})
153161
if (!parsed.success) return parsed.response

apps/sim/app/api/templates/[id]/route.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { type NextRequest, NextResponse } from 'next/server'
77
import { templateIdParamsSchema, updateTemplateContract } from '@/lib/api/contracts/templates'
88
import { parseRequest } from '@/lib/api/server'
99
import { getSession } from '@/lib/auth'
10-
import { generateRequestId } from '@/lib/core/utils/request'
10+
import { RateLimiter } from '@/lib/core/rate-limiter'
11+
import { generateRequestId, getClientIp } from '@/lib/core/utils/request'
1112
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1213
import { canAccessTemplate } from '@/lib/templates/permissions'
1314
import {
@@ -18,6 +19,18 @@ import type { WorkflowState } from '@/stores/workflows/workflow/types'
1819

1920
const logger = createLogger('TemplateByIdAPI')
2021

22+
const viewRateLimiter = new RateLimiter()
23+
24+
/**
25+
* Per-IP, per-template view-counter dedup bucket: one increment per 10 minutes.
26+
* Prevents scripted inflation of `templates.views` from the public GET handler.
27+
*/
28+
const TEMPLATE_VIEW_DEDUP = {
29+
maxTokens: 1,
30+
refillRate: 1,
31+
refillIntervalMs: 10 * 60_000,
32+
}
33+
2134
export const revalidate = 0
2235

2336
export const GET = withRouteHandler(
@@ -63,21 +76,31 @@ export const GET = withRouteHandler(
6376
isStarred = starResult.length > 0
6477
}
6578

66-
const shouldIncrementView = template.status === 'approved'
79+
let shouldIncrementView = template.status === 'approved'
6780

6881
if (shouldIncrementView) {
69-
try {
70-
await db
71-
.update(templates)
72-
.set({
73-
views: sql`${templates.views} + 1`,
74-
})
75-
.where(eq(templates.id, id))
76-
} catch (viewError) {
77-
logger.warn(
78-
`[${requestId}] Failed to increment view count for template: ${id}`,
79-
viewError
80-
)
82+
const viewer = session?.user?.id ?? `ip:${getClientIp(request)}`
83+
const dedupKey = `template-view:${id}:${viewer}`
84+
const { allowed } = await viewRateLimiter.checkRateLimitDirect(
85+
dedupKey,
86+
TEMPLATE_VIEW_DEDUP
87+
)
88+
if (!allowed) {
89+
shouldIncrementView = false
90+
} else {
91+
try {
92+
await db
93+
.update(templates)
94+
.set({
95+
views: sql`${templates.views} + 1`,
96+
})
97+
.where(eq(templates.id, id))
98+
} catch (viewError) {
99+
logger.warn(
100+
`[${requestId}] Failed to increment view count for template: ${id}`,
101+
viewError
102+
)
103+
}
81104
}
82105
}
83106

0 commit comments

Comments
 (0)