Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/sim/app/api/billing/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { member } from '@/db/schema'

const logger = createLogger('UnifiedBillingAPI')

export const dynamic = 'force-dynamic'

/**
* Unified Billing Endpoint
*/
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { chat } from '@/db/schema'

const logger = createLogger('ChatAPI')

export const dynamic = 'force-dynamic'

const chatSchema = z.object({
workflowId: z.string().min(1, 'Workflow ID is required'),
subdomain: z
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/app/api/chat/subdomains/validate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'

export const dynamic = 'force-dynamic'

import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
import { db } from '@/db'
import { chat } from '@/db/schema'
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/app/api/environment/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'

export const dynamic = 'force-dynamic'

import { decryptSecret, encryptSecret } from '@/lib/utils'
import { db } from '@/db'
import { environment } from '@/db/schema'
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/app/api/folders/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'

export const dynamic = 'force-dynamic'

import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { db } from '@/db'
import { workflow, workflowFolder } from '@/db/schema'
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/app/api/folders/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { workflowFolder } from '@/db/schema'

const logger = createLogger('FoldersAPI')

export const dynamic = 'force-dynamic'

// GET - Fetch folders for a workspace
export async function GET(request: NextRequest) {
try {
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/app/api/jobs/[jobId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'

export const dynamic = 'force-dynamic'

import { apiKey as apiKeyTable } from '@/db/schema'
import { createErrorResponse } from '../../workflows/utils'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'

export const dynamic = 'force-dynamic'

import { db } from '@/db'
import { document, embedding } from '@/db/schema'
import { checkChunkAccess } from '../../../../../utils'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'

export const dynamic = 'force-dynamic'

import { estimateTokenCount } from '@/lib/tokenization/estimators'
import { getUserId } from '@/app/api/auth/oauth/utils'
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'

export const dynamic = 'force-dynamic'

import { db } from '@/db'
import { document, embedding } from '@/db/schema'
import { checkDocumentAccess, checkDocumentWriteAccess, processDocumentAsync } from '../../../utils'
Expand Down
73 changes: 61 additions & 12 deletions apps/sim/app/api/knowledge/search/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ vi.mock('@/providers/utils', () => ({
}),
}))

const mockCheckKnowledgeBaseAccess = vi.fn()
vi.mock('@/app/api/knowledge/utils', () => ({
checkKnowledgeBaseAccess: mockCheckKnowledgeBaseAccess,
}))

mockConsoleLogger()

describe('Knowledge Search API Route', () => {
Expand Down Expand Up @@ -132,7 +137,11 @@ describe('Knowledge Search API Route', () => {
it('should perform search successfully with single knowledge base', async () => {
mockGetUserId.mockResolvedValue('user-123')

mockDbChain.where.mockResolvedValueOnce(mockKnowledgeBases)
// Mock knowledge base access check to return success
mockCheckKnowledgeBaseAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: mockKnowledgeBases[0],
})

mockDbChain.limit.mockResolvedValueOnce(mockSearchResults)

Expand All @@ -149,6 +158,10 @@ describe('Knowledge Search API Route', () => {
const response = await POST(req)
const data = await response.json()

if (response.status !== 200) {
console.log('Test failed with response:', data)
}

expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.data.results).toHaveLength(2)
Expand All @@ -171,7 +184,10 @@ describe('Knowledge Search API Route', () => {

mockGetUserId.mockResolvedValue('user-123')

mockDbChain.where.mockResolvedValueOnce(multiKbs)
// Mock knowledge base access check to return success for both KBs
mockCheckKnowledgeBaseAccess
.mockResolvedValueOnce({ hasAccess: true, knowledgeBase: multiKbs[0] })
.mockResolvedValueOnce({ hasAccess: true, knowledgeBase: multiKbs[1] })

mockDbChain.limit.mockResolvedValueOnce(mockSearchResults)

Expand Down Expand Up @@ -201,9 +217,13 @@ describe('Knowledge Search API Route', () => {

mockGetUserId.mockResolvedValue('user-123')

mockDbChain.where.mockResolvedValueOnce(mockKnowledgeBases) // First call: get knowledge bases
// Mock knowledge base access check to return success
mockCheckKnowledgeBaseAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: mockKnowledgeBases[0],
})

mockDbChain.limit.mockResolvedValueOnce(mockSearchResults) // Second call: search results
mockDbChain.limit.mockResolvedValueOnce(mockSearchResults) // Search results

mockFetch.mockResolvedValue({
ok: true,
Expand Down Expand Up @@ -255,7 +275,11 @@ describe('Knowledge Search API Route', () => {
it('should return not found for non-existent knowledge base', async () => {
mockGetUserId.mockResolvedValue('user-123')

mockDbChain.where.mockResolvedValueOnce([]) // No knowledge bases found
// Mock knowledge base access check to return no access
mockCheckKnowledgeBaseAccess.mockResolvedValue({
hasAccess: false,
notFound: true,
})

const req = createMockRequest('POST', validSearchData)
const { POST } = await import('./route')
Expand All @@ -274,15 +298,18 @@ describe('Knowledge Search API Route', () => {

mockGetUserId.mockResolvedValue('user-123')

mockDbChain.where.mockResolvedValueOnce(mockKnowledgeBases) // Only kb-123 found
// Mock access check: first KB has access, second doesn't
mockCheckKnowledgeBaseAccess
.mockResolvedValueOnce({ hasAccess: true, knowledgeBase: mockKnowledgeBases[0] })
.mockResolvedValueOnce({ hasAccess: false, notFound: true })

const req = createMockRequest('POST', multiKbData)
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()

expect(response.status).toBe(404)
expect(data.error).toBe('Knowledge bases not found: kb-missing')
expect(data.error).toBe('Knowledge bases not found or access denied: kb-missing')
})

it.concurrent('should validate search parameters', async () => {
Expand Down Expand Up @@ -310,9 +337,13 @@ describe('Knowledge Search API Route', () => {

mockGetUserId.mockResolvedValue('user-123')

mockDbChain.where.mockResolvedValueOnce(mockKnowledgeBases) // First call: get knowledge bases
// Mock knowledge base access check to return success
mockCheckKnowledgeBaseAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: mockKnowledgeBases[0],
})

mockDbChain.limit.mockResolvedValueOnce(mockSearchResults) // Second call: search results
mockDbChain.limit.mockResolvedValueOnce(mockSearchResults) // Search results

mockFetch.mockResolvedValue({
ok: true,
Expand Down Expand Up @@ -416,7 +447,13 @@ describe('Knowledge Search API Route', () => {
describe('Cost tracking', () => {
it.concurrent('should include cost information in successful search response', async () => {
mockGetUserId.mockResolvedValue('user-123')
mockDbChain.where.mockResolvedValueOnce(mockKnowledgeBases)

// Mock knowledge base access check to return success
mockCheckKnowledgeBaseAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: mockKnowledgeBases[0],
})

mockDbChain.limit.mockResolvedValueOnce(mockSearchResults)

mockFetch.mockResolvedValue({
Expand Down Expand Up @@ -458,7 +495,13 @@ describe('Knowledge Search API Route', () => {
const { calculateCost } = await import('@/providers/utils')

mockGetUserId.mockResolvedValue('user-123')
mockDbChain.where.mockResolvedValueOnce(mockKnowledgeBases)

// Mock knowledge base access check to return success
mockCheckKnowledgeBaseAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: mockKnowledgeBases[0],
})

mockDbChain.limit.mockResolvedValueOnce(mockSearchResults)

mockFetch.mockResolvedValue({
Expand Down Expand Up @@ -509,7 +552,13 @@ describe('Knowledge Search API Route', () => {
}

mockGetUserId.mockResolvedValue('user-123')
mockDbChain.where.mockResolvedValueOnce(mockKnowledgeBases)

// Mock knowledge base access check to return success
mockCheckKnowledgeBaseAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: mockKnowledgeBases[0],
})

mockDbChain.limit.mockResolvedValueOnce(mockSearchResults)

mockFetch.mockResolvedValue({
Expand Down
53 changes: 26 additions & 27 deletions apps/sim/app/api/knowledge/search/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { and, eq, inArray, isNull, sql } from 'drizzle-orm'
import { and, eq, inArray, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { retryWithExponentialBackoff } from '@/lib/documents/utils'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { estimateTokenCount } from '@/lib/tokenization/estimators'
import { getUserId } from '@/app/api/auth/oauth/utils'
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'
import { db } from '@/db'
import { embedding, knowledgeBase } from '@/db/schema'
import { embedding } from '@/db/schema'
import { calculateCost } from '@/providers/utils'

const logger = createLogger('VectorSearchAPI')
Expand Down Expand Up @@ -261,47 +262,45 @@ export async function POST(request: NextRequest) {
? validatedData.knowledgeBaseIds
: [validatedData.knowledgeBaseIds]

const [kb, queryEmbedding] = await Promise.all([
db
.select()
.from(knowledgeBase)
.where(
and(
inArray(knowledgeBase.id, knowledgeBaseIds),
eq(knowledgeBase.userId, userId),
isNull(knowledgeBase.deletedAt)
)
),
generateSearchEmbedding(validatedData.query),
])

if (kb.length === 0) {
// Check access permissions for each knowledge base using proper workspace-based permissions
const accessibleKbIds: string[] = []
for (const kbId of knowledgeBaseIds) {
const accessCheck = await checkKnowledgeBaseAccess(kbId, userId)
if (accessCheck.hasAccess) {
accessibleKbIds.push(kbId)
}
}

if (accessibleKbIds.length === 0) {
return NextResponse.json(
{ error: 'Knowledge base not found or access denied' },
{ status: 404 }
)
}

const foundKbIds = kb.map((k) => k.id)
const missingKbIds = knowledgeBaseIds.filter((id) => !foundKbIds.includes(id))
// Generate query embedding in parallel with access checks
const queryEmbedding = await generateSearchEmbedding(validatedData.query)
Comment on lines +281 to +282
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Moving embedding generation after access checks removes the optimization of parallel execution. Consider moving this back inside the access check loop or running it concurrently with the first access check.


// Check if any requested knowledge bases were not accessible
const inaccessibleKbIds = knowledgeBaseIds.filter((id) => !accessibleKbIds.includes(id))

if (missingKbIds.length > 0) {
if (inaccessibleKbIds.length > 0) {
return NextResponse.json(
{ error: `Knowledge bases not found: ${missingKbIds.join(', ')}` },
{ error: `Knowledge bases not found or access denied: ${inaccessibleKbIds.join(', ')}` },
{ status: 404 }
)
}
Comment on lines +287 to 292
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: This check for inaccessible KBs creates a potential information leak - it reveals which specific knowledge base IDs exist but are inaccessible. Consider returning a generic error message instead.


// Adaptive query strategy based on KB count and parameters
const strategy = getQueryStrategy(foundKbIds.length, validatedData.topK)
// Adaptive query strategy based on accessible KB count and parameters
const strategy = getQueryStrategy(accessibleKbIds.length, validatedData.topK)
const queryVector = JSON.stringify(queryEmbedding)

let results: any[]

if (strategy.useParallel) {
// Execute parallel queries for better performance with many KBs
const parallelResults = await executeParallelQueries(
foundKbIds,
accessibleKbIds,
queryVector,
validatedData.topK,
strategy.distanceThreshold,
Expand All @@ -311,7 +310,7 @@ export async function POST(request: NextRequest) {
} else {
// Execute single optimized query for fewer KBs
results = await executeSingleQuery(
foundKbIds,
accessibleKbIds,
queryVector,
validatedData.topK,
strategy.distanceThreshold,
Expand Down Expand Up @@ -350,8 +349,8 @@ export async function POST(request: NextRequest) {
similarity: 1 - result.distance,
})),
query: validatedData.query,
knowledgeBaseIds: foundKbIds,
knowledgeBaseId: foundKbIds[0],
knowledgeBaseIds: accessibleKbIds,
knowledgeBaseId: accessibleKbIds[0],
topK: validatedData.topK,
totalResults: results.length,
...(cost && tokenCount
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/app/api/organizations/[id]/invitations/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { invitation, member, organization, user, workspace, workspaceInvitation

const logger = createLogger('OrganizationInvitationsAPI')

export const dynamic = 'force-dynamic'

interface WorkspaceInvitation {
workspaceId: string
permission: 'admin' | 'write' | 'read'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { member, user, userStats } from '@/db/schema'

const logger = createLogger('OrganizationMemberAPI')

export const dynamic = 'force-dynamic'

/**
* GET /api/organizations/[id]/members/[memberId]
* Get individual organization member details
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/app/api/organizations/[id]/members/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { invitation, member, organization, user, userStats } from '@/db/schema'

const logger = createLogger('OrganizationMembersAPI')

export const dynamic = 'force-dynamic'

/**
* GET /api/organizations/[id]/members
* Get organization members with optional usage data
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/app/api/organizations/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
updateOrganizationSeats,
} from '@/lib/billing/validation/seat-management'
import { createLogger } from '@/lib/logs/console-logger'

export const dynamic = 'force-dynamic'

import { db } from '@/db'
import { member, organization } from '@/db/schema'

Expand Down
Loading