Skip to content

Commit b09afff

Browse files
committed
Merge branch 'staging' into feat/workspace-files-folders
2 parents 90a6050 + 4a9e248 commit b09afff

53 files changed

Lines changed: 2246 additions & 449 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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/(landing)/seo.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,21 @@ const SEO_SCAN_DIRS = [
2626

2727
const SEO_SCAN_INDIVIDUAL_FILES = [
2828
path.resolve(APP_DIR, 'page.tsx'),
29+
path.resolve(APP_DIR, 'robots.ts'),
30+
path.resolve(APP_DIR, 'sitemap.ts'),
2931
path.resolve(SIM_ROOT, 'ee', 'whitelabeling', 'metadata.ts'),
3032
]
3133

34+
/**
35+
* Files whose entire URL output is SEO-facing (robots.txt, sitemap.xml).
36+
* Unlike metadata exports, these don't use `metadataBase`, so the existing
37+
* `getBaseUrl()`-in-metadata check would miss a regression here.
38+
*/
39+
const SEO_DEFAULT_EXPORT_FILES = [
40+
path.resolve(APP_DIR, 'robots.ts'),
41+
path.resolve(APP_DIR, 'sitemap.ts'),
42+
]
43+
3244
function collectFiles(dir: string, exts: string[]): string[] {
3345
const results: string[] = []
3446
if (!fs.existsSync(dir)) return results
@@ -97,6 +109,21 @@ describe('SEO canonical URLs', () => {
97109
).toHaveLength(0)
98110
})
99111

112+
it('robots.ts and sitemap.ts do not import getBaseUrl', () => {
113+
const violations: string[] = []
114+
for (const file of SEO_DEFAULT_EXPORT_FILES) {
115+
if (!fs.existsSync(file)) continue
116+
const content = fs.readFileSync(file, 'utf-8')
117+
if (content.includes('getBaseUrl')) {
118+
violations.push(path.relative(SIM_ROOT, file))
119+
}
120+
}
121+
expect(
122+
violations,
123+
`robots.ts/sitemap.ts must use SITE_URL, not getBaseUrl():\n${violations.join('\n')}`
124+
).toHaveLength(0)
125+
})
126+
100127
it('public pages do not use getBaseUrl() for SEO metadata', () => {
101128
const files = getAllSeoFiles(['.ts', '.tsx'])
102129
const violations: string[] = []

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

0 commit comments

Comments
 (0)