diff --git a/apps/sim/lib/copilot/chat/post.test.ts b/apps/sim/lib/copilot/chat/post.test.ts index 3c114eccba..1a6dee8a4b 100644 --- a/apps/sim/lib/copilot/chat/post.test.ts +++ b/apps/sim/lib/copilot/chat/post.test.ts @@ -93,10 +93,18 @@ vi.mock('@sim/db', () => ({ })), })), })), + select: vi.fn(() => ({ + from: vi.fn(() => ({ + where: vi.fn(() => ({ + limit: vi.fn().mockResolvedValue([{ permissionType: 'write' }]), + })), + })), + })), }, })) vi.mock('drizzle-orm', () => ({ + and: vi.fn(() => ({})), eq: vi.fn(() => ({})), sql: (strings: TemplateStringsArray, ...values: unknown[]) => ({ strings, values }), })) diff --git a/apps/sim/lib/copilot/chat/post.ts b/apps/sim/lib/copilot/chat/post.ts index a745f209c9..a9d1eb30ad 100644 --- a/apps/sim/lib/copilot/chat/post.ts +++ b/apps/sim/lib/copilot/chat/post.ts @@ -1,9 +1,9 @@ import { type Context as OtelContext, context as otelContextApi } from '@opentelemetry/api' import { db } from '@sim/db' -import { copilotChats } from '@sim/db/schema' +import { copilotChats, permissions } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' -import { eq, sql } from 'drizzle-orm' +import { and, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { isZodError, validationErrorResponse } from '@/lib/api/server' @@ -506,14 +506,12 @@ async function resolveBranch(params: { } const resolvedWorkflowId = resolved.workflowId - let resolvedWorkspaceId = requestedWorkspaceId - if (!resolvedWorkspaceId) { - try { - const workflow = await getWorkflowById(resolvedWorkflowId) - resolvedWorkspaceId = workflow?.workspaceId ?? undefined - } catch { - // best effort; downstream calls can still proceed - } + let resolvedWorkspaceId: string | undefined + try { + const workflow = await getWorkflowById(resolvedWorkflowId) + resolvedWorkspaceId = workflow?.workspaceId ?? requestedWorkspaceId + } catch { + resolvedWorkspaceId = requestedWorkspaceId } const selectedModel = model || DEFAULT_MODEL @@ -569,6 +567,22 @@ async function resolveBranch(params: { return createBadRequestResponse('workspaceId is required when workflowId is not provided') } + const [permissionRow] = await db + .select({ permissionType: permissions.permissionType }) + .from(permissions) + .where( + and( + eq(permissions.userId, authenticatedUserId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, requestedWorkspaceId) + ) + ) + .limit(1) + + if (!permissionRow) { + return createBadRequestResponse('Workspace not found or access denied') + } + return { kind: 'workspace', workspaceId: requestedWorkspaceId, diff --git a/apps/sim/lib/copilot/chat/process-contents.ts b/apps/sim/lib/copilot/chat/process-contents.ts index 78d9b94db0..d570bab39e 100644 --- a/apps/sim/lib/copilot/chat/process-contents.ts +++ b/apps/sim/lib/copilot/chat/process-contents.ts @@ -116,8 +116,8 @@ export async function processContextsServer( currentWorkspaceId ) } - if (ctx.kind === 'table' && ctx.tableId) { - const result = await resolveTableResource(ctx.tableId) + if (ctx.kind === 'table' && ctx.tableId && currentWorkspaceId) { + const result = await resolveTableResource(ctx.tableId, currentWorkspaceId) if (!result) return null return { type: 'table', tag: ctx.label ? `@${ctx.label}` : '@', content: result.content } } @@ -701,7 +701,7 @@ export async function resolveActiveResourceContext( resourceType: string, resourceId: string, workspaceId: string, - _userId: string, + userId: string, chatId?: string ): Promise { try { @@ -709,10 +709,10 @@ export async function resolveActiveResourceContext( case 'workflow': { const ctx = await processWorkflowFromDb( resourceId, - undefined, + userId, '@active_resource', 'current_workflow', - undefined, + workspaceId, chatId ) if (!ctx) return null @@ -721,7 +721,7 @@ export async function resolveActiveResourceContext( case 'knowledgebase': { const ctx = await processKnowledgeFromDb( resourceId, - undefined, + userId, '@active_resource', workspaceId ) @@ -729,7 +729,7 @@ export async function resolveActiveResourceContext( return { type: 'active_resource', tag: '@active_resource', content: ctx.content } } case 'table': { - return await resolveTableResource(resourceId) + return await resolveTableResource(resourceId, workspaceId) } case 'file': { return await resolveFileResource(resourceId, workspaceId) @@ -745,9 +745,13 @@ export async function resolveActiveResourceContext( return null } } -async function resolveTableResource(tableId: string): Promise { +async function resolveTableResource( + tableId: string, + workspaceId: string +): Promise { const table = await getTableById(tableId) if (!table) return null + if (table.workspaceId !== workspaceId) return null return { type: 'active_resource', tag: '@active_resource', diff --git a/apps/sim/lib/data-drains/destinations/webhook.ts b/apps/sim/lib/data-drains/destinations/webhook.ts index ba192b9943..525898e348 100644 --- a/apps/sim/lib/data-drains/destinations/webhook.ts +++ b/apps/sim/lib/data-drains/destinations/webhook.ts @@ -99,12 +99,11 @@ function sign(body: Buffer, secret: string, timestamp: number): string { function sleepUntilAborted(ms: number, signal: AbortSignal): Promise { if (signal.aborted) return Promise.resolve() return new Promise((resolve) => { - let timeoutId: ReturnType const onAbort = () => { clearTimeout(timeoutId) resolve() } - timeoutId = setTimeout(() => { + const timeoutId = setTimeout(() => { signal.removeEventListener('abort', onAbort) resolve() }, ms)