From 193b95cfec8e2c20dd134d2812b76c2106bd998f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 6 Feb 2026 22:07:55 -0800 Subject: [PATCH] fix(auth): swap out hybrid auth in relevant callsites (#3160) * fix(logs): execution files should always use our internal route * correct degree of access control * fix tests * fix tag defs flag * fix type check * fix mcp tools * make webhooks consistent * fix ollama and vllm visibility * remove dup test --- .../sim/app/api/a2a/agents/[agentId]/route.ts | 10 +-- apps/sim/app/api/a2a/agents/route.ts | 6 +- .../app/api/auth/oauth/credentials/route.ts | 4 +- .../app/api/auth/oauth/token/route.test.ts | 48 ++++-------- apps/sim/app/api/auth/oauth/token/route.ts | 6 +- apps/sim/app/api/files/delete/route.test.ts | 2 +- apps/sim/app/api/files/delete/route.ts | 4 +- apps/sim/app/api/files/download/route.ts | 4 +- apps/sim/app/api/files/parse/route.test.ts | 2 +- apps/sim/app/api/files/parse/route.ts | 4 +- .../api/files/serve/[...path]/route.test.ts | 10 +-- .../app/api/files/serve/[...path]/route.ts | 4 +- .../knowledge/[id]/tag-definitions/route.ts | 22 +----- .../api/logs/execution/[executionId]/route.ts | 4 +- apps/sim/app/api/memory/[id]/route.ts | 4 +- apps/sim/app/api/memory/route.ts | 8 +- .../app/api/tools/a2a/cancel-task/route.ts | 4 +- .../a2a/delete-push-notification/route.ts | 4 +- .../app/api/tools/a2a/get-agent-card/route.ts | 4 +- .../tools/a2a/get-push-notification/route.ts | 4 +- apps/sim/app/api/tools/a2a/get-task/route.ts | 4 +- .../app/api/tools/a2a/resubscribe/route.ts | 4 +- .../app/api/tools/a2a/send-message/route.ts | 4 +- .../tools/a2a/set-push-notification/route.ts | 4 +- apps/sim/app/api/users/me/usage-logs/route.ts | 4 +- .../file-download/file-download.tsx | 18 +---- apps/sim/blocks/blocks/agent.ts | 30 +------- apps/sim/blocks/types.ts | 4 +- apps/sim/blocks/utils.ts | 76 +++++++++++++------ .../executor/handlers/agent/agent-handler.ts | 13 +++- .../handlers/evaluator/evaluator-handler.ts | 2 +- .../handlers/router/router-handler.ts | 2 + apps/sim/lib/auth/credential-access.ts | 20 ++--- apps/sim/lib/mcp/middleware.ts | 4 +- apps/sim/lib/webhooks/processor.ts | 18 ++++- .../workflows/subblocks/visibility.test.ts | 9 +++ .../sim/lib/workflows/subblocks/visibility.ts | 7 +- apps/sim/tools/index.ts | 8 +- 38 files changed, 196 insertions(+), 193 deletions(-) diff --git a/apps/sim/app/api/a2a/agents/[agentId]/route.ts b/apps/sim/app/api/a2a/agents/[agentId]/route.ts index 65f22e5b6..1c8eea273 100644 --- a/apps/sim/app/api/a2a/agents/[agentId]/route.ts +++ b/apps/sim/app/api/a2a/agents/[agentId]/route.ts @@ -5,7 +5,7 @@ import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { generateAgentCard, generateSkillsFromWorkflow } from '@/lib/a2a/agent-card' import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { getRedisClient } from '@/lib/core/config/redis' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' @@ -40,7 +40,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise { const mockRefreshTokenIfNeeded = vi.fn() const mockGetOAuthToken = vi.fn() const mockAuthorizeCredentialUse = vi.fn() - const mockCheckHybridAuth = vi.fn() + const mockCheckSessionOrInternalAuth = vi.fn() const mockLogger = createMockLogger() @@ -42,7 +42,7 @@ describe('OAuth Token API Routes', () => { })) vi.doMock('@/lib/auth/hybrid', () => ({ - checkHybridAuth: mockCheckHybridAuth, + checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth, })) }) @@ -235,7 +235,7 @@ describe('OAuth Token API Routes', () => { describe('credentialAccountUserId + providerId path', () => { it('should reject unauthenticated requests', async () => { - mockCheckHybridAuth.mockResolvedValueOnce({ + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ success: false, error: 'Authentication required', }) @@ -255,30 +255,8 @@ describe('OAuth Token API Routes', () => { expect(mockGetOAuthToken).not.toHaveBeenCalled() }) - it('should reject API key authentication', async () => { - mockCheckHybridAuth.mockResolvedValueOnce({ - success: true, - authType: 'api_key', - userId: 'test-user-id', - }) - - const req = createMockRequest('POST', { - credentialAccountUserId: 'test-user-id', - providerId: 'google', - }) - - const { POST } = await import('@/app/api/auth/oauth/token/route') - - const response = await POST(req) - const data = await response.json() - - expect(response.status).toBe(401) - expect(data).toHaveProperty('error', 'User not authenticated') - expect(mockGetOAuthToken).not.toHaveBeenCalled() - }) - it('should reject internal JWT authentication', async () => { - mockCheckHybridAuth.mockResolvedValueOnce({ + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ success: true, authType: 'internal_jwt', userId: 'test-user-id', @@ -300,7 +278,7 @@ describe('OAuth Token API Routes', () => { }) it('should reject requests for other users credentials', async () => { - mockCheckHybridAuth.mockResolvedValueOnce({ + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ success: true, authType: 'session', userId: 'attacker-user-id', @@ -322,7 +300,7 @@ describe('OAuth Token API Routes', () => { }) it('should allow session-authenticated users to access their own credentials', async () => { - mockCheckHybridAuth.mockResolvedValueOnce({ + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ success: true, authType: 'session', userId: 'test-user-id', @@ -345,7 +323,7 @@ describe('OAuth Token API Routes', () => { }) it('should return 404 when credential not found for user', async () => { - mockCheckHybridAuth.mockResolvedValueOnce({ + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ success: true, authType: 'session', userId: 'test-user-id', @@ -373,7 +351,7 @@ describe('OAuth Token API Routes', () => { */ describe('GET handler', () => { it('should return access token successfully', async () => { - mockCheckHybridAuth.mockResolvedValueOnce({ + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ success: true, authType: 'session', userId: 'test-user-id', @@ -402,7 +380,7 @@ describe('OAuth Token API Routes', () => { expect(response.status).toBe(200) expect(data).toHaveProperty('accessToken', 'fresh-token') - expect(mockCheckHybridAuth).toHaveBeenCalled() + expect(mockCheckSessionOrInternalAuth).toHaveBeenCalled() expect(mockGetCredential).toHaveBeenCalledWith(mockRequestId, 'credential-id', 'test-user-id') expect(mockRefreshTokenIfNeeded).toHaveBeenCalled() }) @@ -421,7 +399,7 @@ describe('OAuth Token API Routes', () => { }) it('should handle authentication failure', async () => { - mockCheckHybridAuth.mockResolvedValueOnce({ + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ success: false, error: 'Authentication required', }) @@ -440,7 +418,7 @@ describe('OAuth Token API Routes', () => { }) it('should handle credential not found', async () => { - mockCheckHybridAuth.mockResolvedValueOnce({ + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ success: true, authType: 'session', userId: 'test-user-id', @@ -461,7 +439,7 @@ describe('OAuth Token API Routes', () => { }) it('should handle missing access token', async () => { - mockCheckHybridAuth.mockResolvedValueOnce({ + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ success: true, authType: 'session', userId: 'test-user-id', @@ -487,7 +465,7 @@ describe('OAuth Token API Routes', () => { }) it('should handle token refresh failure', async () => { - mockCheckHybridAuth.mockResolvedValueOnce({ + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ success: true, authType: 'session', userId: 'test-user-id', diff --git a/apps/sim/app/api/auth/oauth/token/route.ts b/apps/sim/app/api/auth/oauth/token/route.ts index 7c7d1f463..f6728fe69 100644 --- a/apps/sim/app/api/auth/oauth/token/route.ts +++ b/apps/sim/app/api/auth/oauth/token/route.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { authorizeCredentialUse } from '@/lib/auth/credential-access' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { getCredential, getOAuthToken, refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -71,7 +71,7 @@ export async function POST(request: NextRequest) { providerId, }) - const auth = await checkHybridAuth(request, { requireWorkflowId: false }) + const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!auth.success || auth.authType !== 'session' || !auth.userId) { logger.warn(`[${requestId}] Unauthorized request for credentialAccountUserId path`, { success: auth.success, @@ -187,7 +187,7 @@ export async function GET(request: NextRequest) { const { credentialId } = parseResult.data // For GET requests, we only support session-based authentication - const auth = await checkHybridAuth(request, { requireWorkflowId: false }) + const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!auth.success || auth.authType !== 'session' || !auth.userId) { return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) } diff --git a/apps/sim/app/api/files/delete/route.test.ts b/apps/sim/app/api/files/delete/route.test.ts index 669ea86ad..0cc9824f7 100644 --- a/apps/sim/app/api/files/delete/route.test.ts +++ b/apps/sim/app/api/files/delete/route.test.ts @@ -29,7 +29,7 @@ function setupFileApiMocks( } vi.doMock('@/lib/auth/hybrid', () => ({ - checkHybridAuth: vi.fn().mockResolvedValue({ + checkSessionOrInternalAuth: vi.fn().mockResolvedValue({ success: authenticated, userId: authenticated ? 'test-user-id' : undefined, error: authenticated ? undefined : 'Unauthorized', diff --git a/apps/sim/app/api/files/delete/route.ts b/apps/sim/app/api/files/delete/route.ts index 1a5f49138..273500461 100644 --- a/apps/sim/app/api/files/delete/route.ts +++ b/apps/sim/app/api/files/delete/route.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import type { StorageContext } from '@/lib/uploads/config' import { deleteFile, hasCloudStorage } from '@/lib/uploads/core/storage-service' import { extractStorageKey, inferContextFromKey } from '@/lib/uploads/utils/file-utils' @@ -24,7 +24,7 @@ const logger = createLogger('FilesDeleteAPI') */ export async function POST(request: NextRequest) { try { - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { logger.warn('Unauthorized file delete request', { diff --git a/apps/sim/app/api/files/download/route.ts b/apps/sim/app/api/files/download/route.ts index bd718ed8f..45f9ebb24 100644 --- a/apps/sim/app/api/files/download/route.ts +++ b/apps/sim/app/api/files/download/route.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import type { StorageContext } from '@/lib/uploads/config' import { hasCloudStorage } from '@/lib/uploads/core/storage-service' import { verifyFileAccess } from '@/app/api/files/authorization' @@ -12,7 +12,7 @@ export const dynamic = 'force-dynamic' export async function POST(request: NextRequest) { try { - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { logger.warn('Unauthorized download URL request', { diff --git a/apps/sim/app/api/files/parse/route.test.ts b/apps/sim/app/api/files/parse/route.test.ts index 801795570..bfdc3bbe7 100644 --- a/apps/sim/app/api/files/parse/route.test.ts +++ b/apps/sim/app/api/files/parse/route.test.ts @@ -35,7 +35,7 @@ function setupFileApiMocks( } vi.doMock('@/lib/auth/hybrid', () => ({ - checkHybridAuth: vi.fn().mockResolvedValue({ + checkInternalAuth: vi.fn().mockResolvedValue({ success: authenticated, userId: authenticated ? 'test-user-id' : undefined, error: authenticated ? undefined : 'Unauthorized', diff --git a/apps/sim/app/api/files/parse/route.ts b/apps/sim/app/api/files/parse/route.ts index 25112133f..4b1882f86 100644 --- a/apps/sim/app/api/files/parse/route.ts +++ b/apps/sim/app/api/files/parse/route.ts @@ -5,7 +5,7 @@ import path from 'path' import { createLogger } from '@sim/logger' import binaryExtensionsList from 'binary-extensions' import { type NextRequest, NextResponse } from 'next/server' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkInternalAuth } from '@/lib/auth/hybrid' import { secureFetchWithPinnedIP, validateUrlWithDNS, @@ -66,7 +66,7 @@ export async function POST(request: NextRequest) { const startTime = Date.now() try { - const authResult = await checkHybridAuth(request, { requireWorkflowId: true }) + const authResult = await checkInternalAuth(request, { requireWorkflowId: true }) if (!authResult.success) { logger.warn('Unauthorized file parse request', { diff --git a/apps/sim/app/api/files/serve/[...path]/route.test.ts b/apps/sim/app/api/files/serve/[...path]/route.test.ts index fe833f3aa..d09adf048 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.test.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.test.ts @@ -55,7 +55,7 @@ describe('File Serve API Route', () => { }) vi.doMock('@/lib/auth/hybrid', () => ({ - checkHybridAuth: vi.fn().mockResolvedValue({ + checkSessionOrInternalAuth: vi.fn().mockResolvedValue({ success: true, userId: 'test-user-id', }), @@ -165,7 +165,7 @@ describe('File Serve API Route', () => { })) vi.doMock('@/lib/auth/hybrid', () => ({ - checkHybridAuth: vi.fn().mockResolvedValue({ + checkSessionOrInternalAuth: vi.fn().mockResolvedValue({ success: true, userId: 'test-user-id', }), @@ -226,7 +226,7 @@ describe('File Serve API Route', () => { })) vi.doMock('@/lib/auth/hybrid', () => ({ - checkHybridAuth: vi.fn().mockResolvedValue({ + checkSessionOrInternalAuth: vi.fn().mockResolvedValue({ success: true, userId: 'test-user-id', }), @@ -291,7 +291,7 @@ describe('File Serve API Route', () => { })) vi.doMock('@/lib/auth/hybrid', () => ({ - checkHybridAuth: vi.fn().mockResolvedValue({ + checkSessionOrInternalAuth: vi.fn().mockResolvedValue({ success: true, userId: 'test-user-id', }), @@ -350,7 +350,7 @@ describe('File Serve API Route', () => { for (const test of contentTypeTests) { it(`should serve ${test.ext} file with correct content type`, async () => { vi.doMock('@/lib/auth/hybrid', () => ({ - checkHybridAuth: vi.fn().mockResolvedValue({ + checkSessionOrInternalAuth: vi.fn().mockResolvedValue({ success: true, userId: 'test-user-id', }), diff --git a/apps/sim/app/api/files/serve/[...path]/route.ts b/apps/sim/app/api/files/serve/[...path]/route.ts index e339615f8..9c562fb26 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.ts @@ -2,7 +2,7 @@ import { readFile } from 'fs/promises' import { createLogger } from '@sim/logger' import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { CopilotFiles, isUsingCloudStorage } from '@/lib/uploads' import type { StorageContext } from '@/lib/uploads/config' import { downloadFile } from '@/lib/uploads/core/storage-service' @@ -49,7 +49,7 @@ export async function GET( return await handleLocalFilePublic(fullPath) } - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { logger.warn('Unauthorized file access attempt', { diff --git a/apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts b/apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts index ba52994c8..cbc5ac90e 100644 --- a/apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts +++ b/apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'crypto' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants' import { createTagDefinition, getTagDefinitions } from '@/lib/knowledge/tags/service' import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils' @@ -19,19 +19,11 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id: try { logger.info(`[${requestId}] Getting tag definitions for knowledge base ${knowledgeBaseId}`) - const auth = await checkHybridAuth(req, { requireWorkflowId: false }) + const auth = await checkSessionOrInternalAuth(req, { requireWorkflowId: false }) if (!auth.success) { return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) } - // Only allow session and internal JWT auth (not API key) - if (auth.authType === 'api_key') { - return NextResponse.json( - { error: 'API key auth not supported for this endpoint' }, - { status: 401 } - ) - } - // For session auth, verify KB access. Internal JWT is trusted. if (auth.authType === 'session' && auth.userId) { const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId) @@ -64,19 +56,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: try { logger.info(`[${requestId}] Creating tag definition for knowledge base ${knowledgeBaseId}`) - const auth = await checkHybridAuth(req, { requireWorkflowId: false }) + const auth = await checkSessionOrInternalAuth(req, { requireWorkflowId: false }) if (!auth.success) { return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) } - // Only allow session and internal JWT auth (not API key) - if (auth.authType === 'api_key') { - return NextResponse.json( - { error: 'API key auth not supported for this endpoint' }, - { status: 401 } - ) - } - // For session auth, verify KB access. Internal JWT is trusted. if (auth.authType === 'session' && auth.userId) { const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId) diff --git a/apps/sim/app/api/logs/execution/[executionId]/route.ts b/apps/sim/app/api/logs/execution/[executionId]/route.ts index 8d7004ef5..27a75298d 100644 --- a/apps/sim/app/api/logs/execution/[executionId]/route.ts +++ b/apps/sim/app/api/logs/execution/[executionId]/route.ts @@ -8,7 +8,7 @@ import { import { createLogger } from '@sim/logger' import { and, eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types' @@ -23,7 +23,7 @@ export async function GET( try { const { executionId } = await params - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized execution data access attempt for: ${executionId}`) return NextResponse.json( diff --git a/apps/sim/app/api/memory/[id]/route.ts b/apps/sim/app/api/memory/[id]/route.ts index 2f5b5ae1c..4a4c96b11 100644 --- a/apps/sim/app/api/memory/[id]/route.ts +++ b/apps/sim/app/api/memory/[id]/route.ts @@ -4,7 +4,7 @@ import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' @@ -36,7 +36,7 @@ async function validateMemoryAccess( requestId: string, action: 'read' | 'write' ): Promise<{ userId: string } | { error: NextResponse }> { - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized memory ${action} attempt`) return { diff --git a/apps/sim/app/api/memory/route.ts b/apps/sim/app/api/memory/route.ts index 072756c7a..c5a4638d7 100644 --- a/apps/sim/app/api/memory/route.ts +++ b/apps/sim/app/api/memory/route.ts @@ -3,7 +3,7 @@ import { memory } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, isNull, like } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' @@ -16,7 +16,7 @@ export async function GET(request: NextRequest) { const requestId = generateRequestId() try { - const authResult = await checkHybridAuth(request) + const authResult = await checkInternalAuth(request) if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized memory access attempt`) return NextResponse.json( @@ -89,7 +89,7 @@ export async function POST(request: NextRequest) { const requestId = generateRequestId() try { - const authResult = await checkHybridAuth(request) + const authResult = await checkInternalAuth(request) if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized memory creation attempt`) return NextResponse.json( @@ -228,7 +228,7 @@ export async function DELETE(request: NextRequest) { const requestId = generateRequestId() try { - const authResult = await checkHybridAuth(request) + const authResult = await checkInternalAuth(request) if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized memory deletion attempt`) return NextResponse.json( diff --git a/apps/sim/app/api/tools/a2a/cancel-task/route.ts b/apps/sim/app/api/tools/a2a/cancel-task/route.ts index 9298273ce..d36b63e6b 100644 --- a/apps/sim/app/api/tools/a2a/cancel-task/route.ts +++ b/apps/sim/app/api/tools/a2a/cancel-task/route.ts @@ -3,7 +3,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { createA2AClient } from '@/lib/a2a/utils' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' const logger = createLogger('A2ACancelTaskAPI') @@ -20,7 +20,7 @@ export async function POST(request: NextRequest) { const requestId = generateRequestId() try { - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success) { logger.warn(`[${requestId}] Unauthorized A2A cancel task attempt`) diff --git a/apps/sim/app/api/tools/a2a/delete-push-notification/route.ts b/apps/sim/app/api/tools/a2a/delete-push-notification/route.ts index f222ef883..e2ed939c5 100644 --- a/apps/sim/app/api/tools/a2a/delete-push-notification/route.ts +++ b/apps/sim/app/api/tools/a2a/delete-push-notification/route.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { createA2AClient } from '@/lib/a2a/utils' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' export const dynamic = 'force-dynamic' @@ -20,7 +20,7 @@ export async function POST(request: NextRequest) { const requestId = generateRequestId() try { - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success) { logger.warn( diff --git a/apps/sim/app/api/tools/a2a/get-agent-card/route.ts b/apps/sim/app/api/tools/a2a/get-agent-card/route.ts index c26ed764b..8562b651b 100644 --- a/apps/sim/app/api/tools/a2a/get-agent-card/route.ts +++ b/apps/sim/app/api/tools/a2a/get-agent-card/route.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { createA2AClient } from '@/lib/a2a/utils' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' export const dynamic = 'force-dynamic' @@ -18,7 +18,7 @@ export async function POST(request: NextRequest) { const requestId = generateRequestId() try { - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success) { logger.warn(`[${requestId}] Unauthorized A2A get agent card attempt: ${authResult.error}`) diff --git a/apps/sim/app/api/tools/a2a/get-push-notification/route.ts b/apps/sim/app/api/tools/a2a/get-push-notification/route.ts index 5feedf4de..337e79a9d 100644 --- a/apps/sim/app/api/tools/a2a/get-push-notification/route.ts +++ b/apps/sim/app/api/tools/a2a/get-push-notification/route.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { createA2AClient } from '@/lib/a2a/utils' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' export const dynamic = 'force-dynamic' @@ -19,7 +19,7 @@ export async function POST(request: NextRequest) { const requestId = generateRequestId() try { - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success) { logger.warn( diff --git a/apps/sim/app/api/tools/a2a/get-task/route.ts b/apps/sim/app/api/tools/a2a/get-task/route.ts index 35aa5e278..eda09dfd0 100644 --- a/apps/sim/app/api/tools/a2a/get-task/route.ts +++ b/apps/sim/app/api/tools/a2a/get-task/route.ts @@ -3,7 +3,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { createA2AClient } from '@/lib/a2a/utils' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' export const dynamic = 'force-dynamic' @@ -21,7 +21,7 @@ export async function POST(request: NextRequest) { const requestId = generateRequestId() try { - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success) { logger.warn(`[${requestId}] Unauthorized A2A get task attempt: ${authResult.error}`) diff --git a/apps/sim/app/api/tools/a2a/resubscribe/route.ts b/apps/sim/app/api/tools/a2a/resubscribe/route.ts index 75c0d24ae..38ac95a3c 100644 --- a/apps/sim/app/api/tools/a2a/resubscribe/route.ts +++ b/apps/sim/app/api/tools/a2a/resubscribe/route.ts @@ -10,7 +10,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' const logger = createLogger('A2AResubscribeAPI') @@ -27,7 +27,7 @@ export async function POST(request: NextRequest) { const requestId = generateRequestId() try { - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success) { logger.warn(`[${requestId}] Unauthorized A2A resubscribe attempt`) diff --git a/apps/sim/app/api/tools/a2a/send-message/route.ts b/apps/sim/app/api/tools/a2a/send-message/route.ts index 4c98dc67a..1cf7f966e 100644 --- a/apps/sim/app/api/tools/a2a/send-message/route.ts +++ b/apps/sim/app/api/tools/a2a/send-message/route.ts @@ -3,7 +3,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' @@ -32,7 +32,7 @@ export async function POST(request: NextRequest) { const requestId = generateRequestId() try { - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success) { logger.warn(`[${requestId}] Unauthorized A2A send message attempt: ${authResult.error}`) diff --git a/apps/sim/app/api/tools/a2a/set-push-notification/route.ts b/apps/sim/app/api/tools/a2a/set-push-notification/route.ts index 132bb6be2..e12fbd6d9 100644 --- a/apps/sim/app/api/tools/a2a/set-push-notification/route.ts +++ b/apps/sim/app/api/tools/a2a/set-push-notification/route.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { createA2AClient } from '@/lib/a2a/utils' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' @@ -22,7 +22,7 @@ export async function POST(request: NextRequest) { const requestId = generateRequestId() try { - const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!authResult.success) { logger.warn(`[${requestId}] Unauthorized A2A set push notification attempt`, { diff --git a/apps/sim/app/api/users/me/usage-logs/route.ts b/apps/sim/app/api/users/me/usage-logs/route.ts index 3c4f1229f..038cf2ece 100644 --- a/apps/sim/app/api/users/me/usage-logs/route.ts +++ b/apps/sim/app/api/users/me/usage-logs/route.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { getUserUsageLogs, type UsageLogSource } from '@/lib/billing/core/usage-log' const logger = createLogger('UsageLogsAPI') @@ -20,7 +20,7 @@ const QuerySchema = z.object({ */ export async function GET(req: NextRequest) { try { - const auth = await checkHybridAuth(req, { requireWorkflowId: false }) + const auth = await checkSessionOrInternalAuth(req, { requireWorkflowId: false }) if (!auth.success || !auth.userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx index 3dd05f8d8..5985a00c0 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx @@ -74,8 +74,7 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps) } if (isExecutionFile) { - const serveUrl = - file.url || `/api/files/serve/${encodeURIComponent(file.key)}?context=execution` + const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=execution` window.open(serveUrl, '_blank') logger.info(`Opened execution file serve URL: ${serveUrl}`) } else { @@ -88,16 +87,12 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps) logger.warn( `Could not construct viewer URL for file: ${file.name}, falling back to serve URL` ) - const serveUrl = - file.url || `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace` + const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace` window.open(serveUrl, '_blank') } } } catch (error) { logger.error(`Failed to download file ${file.name}:`, error) - if (file.url) { - window.open(file.url, '_blank') - } } finally { setIsDownloading(false) } @@ -198,8 +193,7 @@ export function FileDownload({ } if (isExecutionFile) { - const serveUrl = - file.url || `/api/files/serve/${encodeURIComponent(file.key)}?context=execution` + const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=execution` window.open(serveUrl, '_blank') logger.info(`Opened execution file serve URL: ${serveUrl}`) } else { @@ -212,16 +206,12 @@ export function FileDownload({ logger.warn( `Could not construct viewer URL for file: ${file.name}, falling back to serve URL` ) - const serveUrl = - file.url || `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace` + const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace` window.open(serveUrl, '_blank') } } } catch (error) { logger.error(`Failed to download file ${file.name}:`, error) - if (file.url) { - window.open(file.url, '_blank') - } } finally { setIsDownloading(false) } diff --git a/apps/sim/blocks/blocks/agent.ts b/apps/sim/blocks/blocks/agent.ts index bf8ec0d66..7e7b12f96 100644 --- a/apps/sim/blocks/blocks/agent.ts +++ b/apps/sim/blocks/blocks/agent.ts @@ -1,11 +1,10 @@ import { createLogger } from '@sim/logger' import { AgentIcon } from '@/components/icons' -import { isHosted } from '@/lib/core/config/feature-flags' import type { BlockConfig } from '@/blocks/types' import { AuthMode } from '@/blocks/types' +import { getApiKeyCondition } from '@/blocks/utils' import { getBaseModelProviders, - getHostedModels, getMaxTemperature, getProviderIcon, getReasoningEffortValuesForModel, @@ -17,15 +16,6 @@ import { providers, supportsTemperature, } from '@/providers/utils' - -const getCurrentOllamaModels = () => { - return useProvidersStore.getState().providers.ollama.models -} - -const getCurrentVLLMModels = () => { - return useProvidersStore.getState().providers.vllm.models -} - import { useProvidersStore } from '@/stores/providers' import type { ToolResponse } from '@/tools/types' @@ -421,23 +411,7 @@ Return ONLY the JSON array.`, password: true, connectionDroppable: false, required: true, - // Hide API key for hosted models, Ollama models, vLLM models, Vertex models (uses OAuth), and Bedrock (uses AWS credentials) - condition: isHosted - ? { - field: 'model', - value: [...getHostedModels(), ...providers.vertex.models, ...providers.bedrock.models], - not: true, // Show for all models EXCEPT those listed - } - : () => ({ - field: 'model', - value: [ - ...getCurrentOllamaModels(), - ...getCurrentVLLMModels(), - ...providers.vertex.models, - ...providers.bedrock.models, - ], - not: true, // Show for all models EXCEPT Ollama, vLLM, Vertex, and Bedrock models - }), + condition: getApiKeyCondition(), }, { id: 'memoryType', diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index a9904dd2e..08a716925 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -208,7 +208,7 @@ export interface SubBlockConfig { not?: boolean } } - | (() => { + | ((values?: Record) => { field: string value: string | number | boolean | Array not?: boolean @@ -261,7 +261,7 @@ export interface SubBlockConfig { not?: boolean } } - | (() => { + | ((values?: Record) => { field: string value: string | number | boolean | Array not?: boolean diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index eed4a5c37..8c003e0ad 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -1,6 +1,6 @@ import { isHosted } from '@/lib/core/config/feature-flags' import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types' -import { getHostedModels, providers } from '@/providers/utils' +import { getHostedModels, getProviderFromModel, providers } from '@/providers/utils' import { useProvidersStore } from '@/stores/providers/store' /** @@ -48,11 +48,54 @@ const getCurrentOllamaModels = () => { return useProvidersStore.getState().providers.ollama.models } -/** - * Helper to get current vLLM models from store - */ -const getCurrentVLLMModels = () => { - return useProvidersStore.getState().providers.vllm.models +function buildModelVisibilityCondition(model: string, shouldShow: boolean) { + if (!model) { + return { field: 'model', value: '__no_model_selected__' } + } + + return shouldShow ? { field: 'model', value: model } : { field: 'model', value: model, not: true } +} + +function shouldRequireApiKeyForModel(model: string): boolean { + const normalizedModel = model.trim().toLowerCase() + if (!normalizedModel) return false + + const hostedModels = getHostedModels() + const isHostedModel = hostedModels.some( + (hostedModel) => hostedModel.toLowerCase() === normalizedModel + ) + if (isHosted && isHostedModel) return false + + if (normalizedModel.startsWith('vertex/') || normalizedModel.startsWith('bedrock/')) { + return false + } + + if (normalizedModel.startsWith('vllm/')) { + return false + } + + const currentOllamaModels = getCurrentOllamaModels() + if (currentOllamaModels.some((ollamaModel) => ollamaModel.toLowerCase() === normalizedModel)) { + return false + } + + if (!isHosted) { + try { + const providerId = getProviderFromModel(model) + if ( + providerId === 'ollama' || + providerId === 'vllm' || + providerId === 'vertex' || + providerId === 'bedrock' + ) { + return false + } + } catch { + // If model resolution fails, fall through and require an API key. + } + } + + return true } /** @@ -60,22 +103,11 @@ const getCurrentVLLMModels = () => { * Handles hosted vs self-hosted environments and excludes providers that don't need API key. */ export function getApiKeyCondition() { - return isHosted - ? { - field: 'model', - value: [...getHostedModels(), ...providers.vertex.models, ...providers.bedrock.models], - not: true, - } - : () => ({ - field: 'model', - value: [ - ...getCurrentOllamaModels(), - ...getCurrentVLLMModels(), - ...providers.vertex.models, - ...providers.bedrock.models, - ], - not: true, - }) + return (values?: Record) => { + const model = typeof values?.model === 'string' ? values.model : '' + const shouldShow = shouldRequireApiKeyForModel(model) + return buildModelVisibilityCondition(model, shouldShow) + } } /** diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index a1f0cee0d..524407506 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -378,6 +378,9 @@ export class AgentBlockHandler implements BlockHandler { if (ctx.workflowId) { params.workflowId = ctx.workflowId } + if (ctx.userId) { + params.userId = ctx.userId + } const url = buildAPIUrl('/api/tools/custom', params) const response = await fetch(url.toString(), { @@ -488,7 +491,9 @@ export class AgentBlockHandler implements BlockHandler { usageControl: tool.usageControl || 'auto', executeFunction: async (callParams: Record) => { const headers = await buildAuthHeaders() - const execUrl = buildAPIUrl('/api/mcp/tools/execute') + const execParams: Record = {} + if (ctx.userId) execParams.userId = ctx.userId + const execUrl = buildAPIUrl('/api/mcp/tools/execute', execParams) const execResponse = await fetch(execUrl.toString(), { method: 'POST', @@ -597,6 +602,7 @@ export class AgentBlockHandler implements BlockHandler { serverId, workspaceId: ctx.workspaceId, workflowId: ctx.workflowId, + ...(ctx.userId ? { userId: ctx.userId } : {}), }) const maxAttempts = 2 @@ -671,7 +677,9 @@ export class AgentBlockHandler implements BlockHandler { usageControl: tool.usageControl || 'auto', executeFunction: async (callParams: Record) => { const headers = await buildAuthHeaders() - const execUrl = buildAPIUrl('/api/mcp/tools/execute') + const discoverExecParams: Record = {} + if (ctx.userId) discoverExecParams.userId = ctx.userId + const execUrl = buildAPIUrl('/api/mcp/tools/execute', discoverExecParams) const execResponse = await fetch(execUrl.toString(), { method: 'POST', @@ -1056,6 +1064,7 @@ export class AgentBlockHandler implements BlockHandler { responseFormat: providerRequest.responseFormat, workflowId: providerRequest.workflowId, workspaceId: ctx.workspaceId, + userId: ctx.userId, stream: providerRequest.stream, messages: 'messages' in providerRequest ? providerRequest.messages : undefined, environmentVariables: ctx.environmentVariables || {}, diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts index 8c432f1da..65ea2f9ea 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts @@ -104,7 +104,7 @@ export class EvaluatorBlockHandler implements BlockHandler { } try { - const url = buildAPIUrl('/api/providers') + const url = buildAPIUrl('/api/providers', ctx.userId ? { userId: ctx.userId } : {}) const providerRequest: Record = { provider: providerId, diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index 541cdccca..a42956c66 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -80,6 +80,7 @@ export class RouterBlockHandler implements BlockHandler { try { const url = new URL('/api/providers', getBaseUrl()) + if (ctx.userId) url.searchParams.set('userId', ctx.userId) const messages = [{ role: 'user', content: routerConfig.prompt }] const systemPrompt = generateRouterPrompt(routerConfig.prompt, targetBlocks) @@ -209,6 +210,7 @@ export class RouterBlockHandler implements BlockHandler { try { const url = new URL('/api/providers', getBaseUrl()) + if (ctx.userId) url.searchParams.set('userId', ctx.userId) const messages = [{ role: 'user', content: routerConfig.context }] const systemPrompt = generateRouterV2Prompt(routerConfig.context, routes) diff --git a/apps/sim/lib/auth/credential-access.ts b/apps/sim/lib/auth/credential-access.ts index be7b7e1bd..61b0f655a 100644 --- a/apps/sim/lib/auth/credential-access.ts +++ b/apps/sim/lib/auth/credential-access.ts @@ -2,13 +2,13 @@ import { db } from '@sim/db' import { account, workflow as workflowTable } from '@sim/db/schema' import { eq } from 'drizzle-orm' import type { NextRequest } from 'next/server' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' export interface CredentialAccessResult { ok: boolean error?: string - authType?: 'session' | 'api_key' | 'internal_jwt' + authType?: 'session' | 'internal_jwt' requesterUserId?: string credentialOwnerUserId?: string workspaceId?: string @@ -16,10 +16,10 @@ export interface CredentialAccessResult { /** * Centralizes auth + collaboration rules for credential use. - * - Uses checkHybridAuth to authenticate the caller + * - Uses checkSessionOrInternalAuth to authenticate the caller * - Fetches credential owner * - Authorization rules: - * - session/api_key: allow if requester owns the credential; otherwise require workflowId and + * - session: allow if requester owns the credential; otherwise require workflowId and * verify BOTH requester and owner have access to the workflow's workspace * - internal_jwt: require workflowId (by default) and verify credential owner has access to the * workflow's workspace (requester identity is the system/workflow) @@ -30,7 +30,9 @@ export async function authorizeCredentialUse( ): Promise { const { credentialId, workflowId, requireWorkflowIdForInternal = true } = params - const auth = await checkHybridAuth(request, { requireWorkflowId: requireWorkflowIdForInternal }) + const auth = await checkSessionOrInternalAuth(request, { + requireWorkflowId: requireWorkflowIdForInternal, + }) if (!auth.success || !auth.userId) { return { ok: false, error: auth.error || 'Authentication required' } } @@ -52,7 +54,7 @@ export async function authorizeCredentialUse( if (auth.authType !== 'internal_jwt' && auth.userId === credentialOwnerUserId) { return { ok: true, - authType: auth.authType, + authType: auth.authType as CredentialAccessResult['authType'], requesterUserId: auth.userId, credentialOwnerUserId, } @@ -85,14 +87,14 @@ export async function authorizeCredentialUse( } return { ok: true, - authType: auth.authType, + authType: auth.authType as CredentialAccessResult['authType'], requesterUserId: auth.userId, credentialOwnerUserId, workspaceId: wf.workspaceId, } } - // Session/API key: verify BOTH requester and owner belong to the workflow's workspace + // Session: verify BOTH requester and owner belong to the workflow's workspace const requesterPerm = await getUserEntityPermissions(auth.userId, 'workspace', wf.workspaceId) const ownerPerm = await getUserEntityPermissions( credentialOwnerUserId, @@ -105,7 +107,7 @@ export async function authorizeCredentialUse( return { ok: true, - authType: auth.authType, + authType: auth.authType as CredentialAccessResult['authType'], requesterUserId: auth.userId, credentialOwnerUserId, workspaceId: wf.workspaceId, diff --git a/apps/sim/lib/mcp/middleware.ts b/apps/sim/lib/mcp/middleware.ts index f994990c6..f95e4eac7 100644 --- a/apps/sim/lib/mcp/middleware.ts +++ b/apps/sim/lib/mcp/middleware.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' import type { NextRequest, NextResponse } from 'next/server' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { createMcpErrorResponse } from '@/lib/mcp/utils' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' @@ -43,7 +43,7 @@ async function validateMcpAuth( const requestId = generateRequestId() try { - const auth = await checkHybridAuth(request, { requireWorkflowId: false }) + const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) if (!auth.success || !auth.userId) { logger.warn(`[${requestId}] Authentication failed: ${auth.error}`) return { diff --git a/apps/sim/lib/webhooks/processor.ts b/apps/sim/lib/webhooks/processor.ts index 6f738855f..15013ab2f 100644 --- a/apps/sim/lib/webhooks/processor.ts +++ b/apps/sim/lib/webhooks/processor.ts @@ -24,6 +24,7 @@ import { validateTypeformSignature, verifyProviderWebhook, } from '@/lib/webhooks/utils.server' +import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' import { executeWebhookJob } from '@/background/webhook-execution' import { resolveEnvVarReferences } from '@/executor/utils/reference-validation' import { isGitHubEventMatch } from '@/triggers/github/utils' @@ -1003,10 +1004,23 @@ export async function queueWebhookExecution( } } + if (!foundWorkflow.workspaceId) { + logger.error(`[${options.requestId}] Workflow ${foundWorkflow.id} has no workspaceId`) + return NextResponse.json({ error: 'Workflow has no associated workspace' }, { status: 500 }) + } + + const actorUserId = await getWorkspaceBilledAccountUserId(foundWorkflow.workspaceId) + if (!actorUserId) { + logger.error( + `[${options.requestId}] No billing account for workspace ${foundWorkflow.workspaceId}` + ) + return NextResponse.json({ error: 'Unable to resolve billing account' }, { status: 500 }) + } + const payload = { webhookId: foundWebhook.id, workflowId: foundWorkflow.id, - userId: foundWorkflow.userId, + userId: actorUserId, provider: foundWebhook.provider, body, headers, @@ -1017,7 +1031,7 @@ export async function queueWebhookExecution( const jobQueue = await getJobQueue() const jobId = await jobQueue.enqueue('webhook-execution', payload, { - metadata: { workflowId: foundWorkflow.id, userId: foundWorkflow.userId }, + metadata: { workflowId: foundWorkflow.id, userId: actorUserId }, }) logger.info( `[${options.requestId}] Queued webhook execution task ${jobId} for ${foundWebhook.provider} webhook` diff --git a/apps/sim/lib/workflows/subblocks/visibility.test.ts b/apps/sim/lib/workflows/subblocks/visibility.test.ts index 07b1f1818..b55bfad5f 100644 --- a/apps/sim/lib/workflows/subblocks/visibility.test.ts +++ b/apps/sim/lib/workflows/subblocks/visibility.test.ts @@ -156,6 +156,15 @@ describe('evaluateSubBlockCondition', () => { expect(evaluateSubBlockCondition(condition, values)).toBe(true) }) + it.concurrent('passes current values into function conditions', () => { + const condition = (values?: Record) => ({ + field: 'model', + value: typeof values?.model === 'string' ? values.model : '__no_model_selected__', + }) + const values = { model: 'ollama/gemma3:4b' } + expect(evaluateSubBlockCondition(condition, values)).toBe(true) + }) + it.concurrent('handles boolean values', () => { const condition = { field: 'enabled', value: true } const values = { enabled: true } diff --git a/apps/sim/lib/workflows/subblocks/visibility.ts b/apps/sim/lib/workflows/subblocks/visibility.ts index 74eda40fd..1ce0076b4 100644 --- a/apps/sim/lib/workflows/subblocks/visibility.ts +++ b/apps/sim/lib/workflows/subblocks/visibility.ts @@ -100,11 +100,14 @@ export function resolveCanonicalMode( * Evaluate a subblock condition against a map of raw values. */ export function evaluateSubBlockCondition( - condition: SubBlockCondition | (() => SubBlockCondition) | undefined, + condition: + | SubBlockCondition + | ((values?: Record) => SubBlockCondition) + | undefined, values: Record ): boolean { if (!condition) return true - const actual = typeof condition === 'function' ? condition() : condition + const actual = typeof condition === 'function' ? condition(values) : condition const fieldValue = values[actual.field] const valueMatch = Array.isArray(actual.value) ? fieldValue != null && diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 09c3ac616..040a40a27 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -961,6 +961,7 @@ async function executeMcpTool( const workspaceId = params._context?.workspaceId || executionContext?.workspaceId const workflowId = params._context?.workflowId || executionContext?.workflowId + const userId = params._context?.userId || executionContext?.userId if (!workspaceId) { return { @@ -1002,7 +1003,12 @@ async function executeMcpTool( hasToolSchema: !!toolSchema, }) - const response = await fetch(`${baseUrl}/api/mcp/tools/execute`, { + const mcpUrl = new URL('/api/mcp/tools/execute', baseUrl) + if (userId) { + mcpUrl.searchParams.set('userId', userId) + } + + const response = await fetch(mcpUrl.toString(), { method: 'POST', headers, body,