diff --git a/apps/sim/app/api/a2a/serve/[agentId]/route.ts b/apps/sim/app/api/a2a/serve/[agentId]/route.ts index 20cb4879e39..5ac5ac4ae30 100644 --- a/apps/sim/app/api/a2a/serve/[agentId]/route.ts +++ b/apps/sim/app/api/a2a/serve/[agentId]/route.ts @@ -13,7 +13,7 @@ import { isTerminalState, parseWorkflowSSEChunk, } from '@/lib/a2a/utils' -import { type AuthResult, checkHybridAuth } from '@/lib/auth/hybrid' +import { type AuthResult, AuthType, checkHybridAuth } from '@/lib/auth/hybrid' import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis' import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server' import { SSE_HEADERS } from '@/lib/core/utils/sse' @@ -242,9 +242,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise { }) vi.mock('@/lib/auth/hybrid', () => ({ + AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' }, checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth, })) diff --git a/apps/sim/app/api/auth/oauth/token/route.test.ts b/apps/sim/app/api/auth/oauth/token/route.test.ts index 7054576c3c5..4f3d06d3a5a 100644 --- a/apps/sim/app/api/auth/oauth/token/route.test.ts +++ b/apps/sim/app/api/auth/oauth/token/route.test.ts @@ -51,6 +51,7 @@ vi.mock('@/lib/auth/credential-access', () => ({ })) vi.mock('@/lib/auth/hybrid', () => ({ + AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' }, checkHybridAuth: vi.fn(), checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth, checkInternalAuth: vi.fn(), diff --git a/apps/sim/app/api/auth/oauth/token/route.ts b/apps/sim/app/api/auth/oauth/token/route.ts index d8b1b457413..fcc6f128702 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 { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { getCredential, getOAuthToken, refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -72,7 +72,7 @@ export async function POST(request: NextRequest) { }) const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - if (!auth.success || auth.authType !== 'session' || !auth.userId) { + if (!auth.success || auth.authType !== AuthType.SESSION || !auth.userId) { logger.warn(`[${requestId}] Unauthorized request for credentialAccountUserId path`, { success: auth.success, authType: auth.authType, @@ -202,7 +202,7 @@ export async function GET(request: NextRequest) { credentialId, requireWorkflowIdForInternal: false, }) - if (!authz.ok || authz.authType !== 'session' || !authz.credentialOwnerUserId) { + if (!authz.ok || authz.authType !== AuthType.SESSION || !authz.credentialOwnerUserId) { return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 }) } diff --git a/apps/sim/app/api/files/delete/route.test.ts b/apps/sim/app/api/files/delete/route.test.ts index e63955015dc..60149483ded 100644 --- a/apps/sim/app/api/files/delete/route.test.ts +++ b/apps/sim/app/api/files/delete/route.test.ts @@ -91,6 +91,7 @@ vi.mock('@/lib/auth', () => ({ })) vi.mock('@/lib/auth/hybrid', () => ({ + AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' }, checkHybridAuth: mocks.mockCheckHybridAuth, checkSessionOrInternalAuth: mocks.mockCheckSessionOrInternalAuth, checkInternalAuth: mocks.mockCheckInternalAuth, diff --git a/apps/sim/app/api/files/parse/route.test.ts b/apps/sim/app/api/files/parse/route.test.ts index 51aeecf3ee9..02baaffb6c0 100644 --- a/apps/sim/app/api/files/parse/route.test.ts +++ b/apps/sim/app/api/files/parse/route.test.ts @@ -106,6 +106,7 @@ vi.mock('@/lib/auth', () => ({ })) vi.mock('@/lib/auth/hybrid', () => ({ + AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' }, checkInternalAuth: mockCheckInternalAuth, checkHybridAuth: mockCheckHybridAuth, checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth, 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 390348c4c01..bc5b35647c0 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.test.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.test.ts @@ -49,6 +49,7 @@ vi.mock('fs/promises', () => ({ })) vi.mock('@/lib/auth/hybrid', () => ({ + AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' }, checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth, })) diff --git a/apps/sim/app/api/files/upload/route.test.ts b/apps/sim/app/api/files/upload/route.test.ts index a2361fd506a..75c06b429f9 100644 --- a/apps/sim/app/api/files/upload/route.test.ts +++ b/apps/sim/app/api/files/upload/route.test.ts @@ -100,6 +100,7 @@ vi.mock('@/lib/auth', () => ({ })) vi.mock('@/lib/auth/hybrid', () => ({ + AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' }, checkHybridAuth: mocks.mockCheckHybridAuth, checkSessionOrInternalAuth: mocks.mockCheckSessionOrInternalAuth, checkInternalAuth: mocks.mockCheckInternalAuth, diff --git a/apps/sim/app/api/function/execute/route.test.ts b/apps/sim/app/api/function/execute/route.test.ts index 70a56b06b19..b57c8fdb77e 100644 --- a/apps/sim/app/api/function/execute/route.test.ts +++ b/apps/sim/app/api/function/execute/route.test.ts @@ -18,6 +18,7 @@ vi.mock('@/lib/execution/isolated-vm', () => ({ })) vi.mock('@/lib/auth/hybrid', () => ({ + AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' }, checkInternalAuth: mockCheckInternalAuth, })) 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 cbc5ac90e67..ad1c7f4dddd 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 { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { AuthType, 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' @@ -25,7 +25,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id: } // For session auth, verify KB access. Internal JWT is trusted. - if (auth.authType === 'session' && auth.userId) { + if (auth.authType === AuthType.SESSION && auth.userId) { const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId) if (!accessCheck.hasAccess) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) @@ -62,7 +62,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: } // For session auth, verify KB access. Internal JWT is trusted. - if (auth.authType === 'session' && auth.userId) { + if (auth.authType === AuthType.SESSION && auth.userId) { const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId) if (!accessCheck.hasAccess) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) diff --git a/apps/sim/app/api/knowledge/search/route.test.ts b/apps/sim/app/api/knowledge/search/route.test.ts index d257bb71655..d736edc44e9 100644 --- a/apps/sim/app/api/knowledge/search/route.test.ts +++ b/apps/sim/app/api/knowledge/search/route.test.ts @@ -68,6 +68,7 @@ vi.mock('@sim/db', () => ({ })) vi.mock('@/lib/auth/hybrid', () => ({ + AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' }, checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth, })) diff --git a/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts b/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts index 97f887e9559..77dd1adebf6 100644 --- a/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts +++ b/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts @@ -59,6 +59,7 @@ vi.mock('@sim/db/schema', () => ({ })) vi.mock('@/lib/auth/hybrid', () => ({ + AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' }, checkHybridAuth: mockCheckHybridAuth, checkSessionOrInternalAuth: vi.fn(), checkInternalAuth: vi.fn(), diff --git a/apps/sim/app/api/mcp/serve/[serverId]/route.ts b/apps/sim/app/api/mcp/serve/[serverId]/route.ts index 99f2a83089c..29c70120978 100644 --- a/apps/sim/app/api/mcp/serve/[serverId]/route.ts +++ b/apps/sim/app/api/mcp/serve/[serverId]/route.ts @@ -19,7 +19,7 @@ import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { type AuthResult, checkHybridAuth } from '@/lib/auth/hybrid' +import { type AuthResult, AuthType, checkHybridAuth } from '@/lib/auth/hybrid' import { generateInternalToken } from '@/lib/auth/internal' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' @@ -137,7 +137,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise ({ })) vi.mock('@/lib/auth/hybrid', () => ({ + AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' }, checkSessionOrInternalAuth: (...args: unknown[]) => mockCheckSessionOrInternalAuth(...args), })) diff --git a/apps/sim/app/api/users/me/usage-limits/route.ts b/apps/sim/app/api/users/me/usage-limits/route.ts index 26db257efcb..19e3403da09 100644 --- a/apps/sim/app/api/users/me/usage-limits/route.ts +++ b/apps/sim/app/api/users/me/usage-limits/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 { AuthType, checkHybridAuth } from '@/lib/auth/hybrid' import { checkServerSideUsageLimits } from '@/lib/billing' import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' import { getEffectiveCurrentPeriodCost } from '@/lib/billing/core/usage' @@ -20,7 +20,7 @@ export async function GET(request: NextRequest) { const userSubscription = await getHighestPrioritySubscription(authenticatedUserId) const rateLimiter = new RateLimiter() - const triggerType = auth.authType === 'api_key' ? 'api' : 'manual' + const triggerType = auth.authType === AuthType.API_KEY ? 'api' : 'manual' const [syncStatus, asyncStatus] = await Promise.all([ rateLimiter.getRateLimitStatusWithSubscription( authenticatedUserId, diff --git a/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts b/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts index 3456e372e8f..3be0cba6e2e 100644 --- a/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts @@ -49,6 +49,7 @@ vi.mock('@sim/db/schema', () => ({ })) vi.mock('@/lib/auth/hybrid', () => ({ + AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' }, checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth, })) diff --git a/apps/sim/app/api/workflows/[id]/execute/response-block.test.ts b/apps/sim/app/api/workflows/[id]/execute/response-block.test.ts new file mode 100644 index 00000000000..16808cca6f9 --- /dev/null +++ b/apps/sim/app/api/workflows/[id]/execute/response-block.test.ts @@ -0,0 +1,115 @@ +/** + * Tests that internal JWT callers receive the standard response format + * even when the child workflow has a Response block. + * + * @vitest-environment node + */ + +import { beforeEach, describe, expect, it } from 'vitest' +import { AuthType } from '@/lib/auth/hybrid' +import type { ExecutionResult } from '@/lib/workflows/types' +import { createHttpResponseFromBlock, workflowHasResponseBlock } from '@/lib/workflows/utils' + +function buildExecutionResult(overrides: Partial = {}): ExecutionResult { + return { + success: true, + output: { data: { issues: [] }, status: 200, headers: {} }, + logs: [ + { + blockId: 'response-1', + blockType: 'response', + blockName: 'Response', + success: true, + output: { data: { issues: [] }, status: 200, headers: {} }, + startedAt: '2026-01-01T00:00:00Z', + endedAt: '2026-01-01T00:00:01Z', + }, + ], + metadata: { + duration: 500, + startTime: '2026-01-01T00:00:00Z', + endTime: '2026-01-01T00:00:01Z', + }, + ...overrides, + } +} + +describe('Response block gating by auth type', () => { + let resultWithResponseBlock: ExecutionResult + + beforeEach(() => { + resultWithResponseBlock = buildExecutionResult() + }) + + it('should detect a Response block in execution result', () => { + expect(workflowHasResponseBlock(resultWithResponseBlock)).toBe(true) + }) + + it('should not detect a Response block when none exists', () => { + const resultWithoutResponseBlock = buildExecutionResult({ + output: { result: 'hello' }, + logs: [ + { + blockId: 'agent-1', + blockType: 'agent', + blockName: 'Agent', + success: true, + output: { result: 'hello' }, + startedAt: '2026-01-01T00:00:00Z', + endedAt: '2026-01-01T00:00:01Z', + }, + ], + }) + expect(workflowHasResponseBlock(resultWithoutResponseBlock)).toBe(false) + }) + + it('should skip Response block formatting for internal JWT callers', () => { + const authType = AuthType.INTERNAL_JWT + const hasResponseBlock = workflowHasResponseBlock(resultWithResponseBlock) + + expect(hasResponseBlock).toBe(true) + + // This mirrors the route.ts condition: + // if (auth.authType !== AuthType.INTERNAL_JWT && workflowHasResponseBlock(...)) + const shouldFormatAsResponseBlock = authType !== AuthType.INTERNAL_JWT && hasResponseBlock + expect(shouldFormatAsResponseBlock).toBe(false) + }) + + it('should apply Response block formatting for API key callers', () => { + const authType = AuthType.API_KEY + const hasResponseBlock = workflowHasResponseBlock(resultWithResponseBlock) + + const shouldFormatAsResponseBlock = authType !== AuthType.INTERNAL_JWT && hasResponseBlock + expect(shouldFormatAsResponseBlock).toBe(true) + + const response = createHttpResponseFromBlock(resultWithResponseBlock) + expect(response.status).toBe(200) + }) + + it('should apply Response block formatting for session callers', () => { + const authType = AuthType.SESSION + const hasResponseBlock = workflowHasResponseBlock(resultWithResponseBlock) + + const shouldFormatAsResponseBlock = authType !== AuthType.INTERNAL_JWT && hasResponseBlock + expect(shouldFormatAsResponseBlock).toBe(true) + }) + + it('should return raw user data via createHttpResponseFromBlock', async () => { + const response = createHttpResponseFromBlock(resultWithResponseBlock) + const body = await response.json() + + // Response block returns the user-defined data directly (no success/executionId wrapper) + expect(body).toEqual({ issues: [] }) + expect(body.success).toBeUndefined() + expect(body.executionId).toBeUndefined() + }) + + it('should respect custom status codes from Response block', () => { + const result = buildExecutionResult({ + output: { data: { error: 'Not found' }, status: 404, headers: {} }, + }) + + const response = createHttpResponseFromBlock(result) + expect(response.status).toBe(404) + }) +}) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 3c1e27080e5..6f469e24866 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { validate as uuidValidate, v4 as uuidv4 } from 'uuid' import { z } from 'zod' -import { checkHybridAuth } from '@/lib/auth/hybrid' +import { AuthType, checkHybridAuth } from '@/lib/auth/hybrid' import { getJobQueue, shouldExecuteInline } from '@/lib/core/async-jobs' import { createTimeoutAbortController, @@ -322,7 +322,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: ) } - const defaultTriggerType = isPublicApiAccess || auth.authType === 'api_key' ? 'api' : 'manual' + const defaultTriggerType = + isPublicApiAccess || auth.authType === AuthType.API_KEY ? 'api' : 'manual' const { selectedOutputs, @@ -381,7 +382,9 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: // For API key and internal JWT auth, the entire body is the input (except for our control fields) // For session auth, the input is explicitly provided in the input field const input = - isPublicApiAccess || auth.authType === 'api_key' || auth.authType === 'internal_jwt' + isPublicApiAccess || + auth.authType === AuthType.API_KEY || + auth.authType === AuthType.INTERNAL_JWT ? (() => { const { selectedOutputs, @@ -407,7 +410,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: // Public API callers always execute the deployed state, never the draft. const shouldUseDraftState = isPublicApiAccess ? false - : (useDraftState ?? auth.authType === 'session') + : (useDraftState ?? auth.authType === AuthType.SESSION) const streamHeader = req.headers.get('X-Stream-Response') === 'true' const enableSSE = streamHeader || streamParam === true const executionModeHeader = req.headers.get('X-Execution-Mode') @@ -440,7 +443,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: // Client-side sessions and personal API keys bill/permission-check the // authenticated user, not the workspace billed account. const useAuthenticatedUserAsActor = - isClientSession || (auth.authType === 'api_key' && auth.apiKeyType === 'personal') + isClientSession || (auth.authType === AuthType.API_KEY && auth.apiKeyType === 'personal') // Authorization fetches the full workflow record and checks workspace permissions. // Run it first so we can pass the record to preprocessing (eliminates a duplicate DB query). @@ -670,8 +673,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: const resultWithBase64 = { ...result, output: outputWithBase64 } - const hasResponseBlock = workflowHasResponseBlock(resultWithBase64) - if (hasResponseBlock) { + if (auth.authType !== AuthType.INTERNAL_JWT && workflowHasResponseBlock(resultWithBase64)) { return createHttpResponseFromBlock(resultWithBase64) } diff --git a/apps/sim/app/api/workflows/[id]/form/status/route.test.ts b/apps/sim/app/api/workflows/[id]/form/status/route.test.ts index 8099d2d844e..4e16e491fd0 100644 --- a/apps/sim/app/api/workflows/[id]/form/status/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/form/status/route.test.ts @@ -44,6 +44,7 @@ vi.mock('@sim/db/schema', () => ({ })) vi.mock('@/lib/auth/hybrid', () => ({ + AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' }, checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth, })) diff --git a/apps/sim/app/api/workflows/[id]/route.test.ts b/apps/sim/app/api/workflows/[id]/route.test.ts index fba05c92c9c..d886e27d466 100644 --- a/apps/sim/app/api/workflows/[id]/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/route.test.ts @@ -43,6 +43,7 @@ vi.mock('@/lib/auth', () => ({ })) vi.mock('@/lib/auth/hybrid', () => ({ + AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' }, checkHybridAuth: (...args: unknown[]) => mockCheckHybridAuth(...args), checkSessionOrInternalAuth: (...args: unknown[]) => mockCheckSessionOrInternalAuth(...args), })) diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index 140cc8ef53f..19d89e8eeb7 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -5,7 +5,7 @@ import { and, eq, isNull, ne } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' -import { checkHybridAuth, checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { AuthType, checkHybridAuth, checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { env } from '@/lib/core/config/env' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' @@ -39,7 +39,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const isInternalCall = auth.authType === 'internal_jwt' + const isInternalCall = auth.authType === AuthType.INTERNAL_JWT const userId = auth.userId || null let workflowData = await getWorkflowById(workflowId) diff --git a/apps/sim/app/api/workflows/[id]/variables/route.test.ts b/apps/sim/app/api/workflows/[id]/variables/route.test.ts index 68c863502a3..99a07d6f12b 100644 --- a/apps/sim/app/api/workflows/[id]/variables/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/variables/route.test.ts @@ -18,6 +18,7 @@ const { mockCheckSessionOrInternalAuth, mockAuthorizeWorkflowByWorkspacePermissi vi.mock('@/lib/audit/log', () => auditMock) vi.mock('@/lib/auth/hybrid', () => ({ + AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' }, checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth, })) diff --git a/apps/sim/app/api/workflows/route.test.ts b/apps/sim/app/api/workflows/route.test.ts index e1b83bdb0eb..bff62acfc83 100644 --- a/apps/sim/app/api/workflows/route.test.ts +++ b/apps/sim/app/api/workflows/route.test.ts @@ -64,6 +64,7 @@ vi.mock('@/lib/audit/log', () => ({ })) vi.mock('@/lib/auth/hybrid', () => ({ + AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' }, checkHybridAuth: vi.fn(), checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth, checkInternalAuth: vi.fn(), diff --git a/apps/sim/lib/auth/credential-access.ts b/apps/sim/lib/auth/credential-access.ts index b4ecc184760..5b4ba2edd1c 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, credential, credentialMember, workflow as workflowTable } from '@sim/db/schema' import { and, eq } from 'drizzle-orm' import type { NextRequest } from 'next/server' -import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' export interface CredentialAccessResult { ok: boolean error?: string - authType?: 'session' | 'internal_jwt' + authType?: typeof AuthType.SESSION | typeof AuthType.INTERNAL_JWT requesterUserId?: string credentialOwnerUserId?: string workspaceId?: string @@ -39,7 +39,7 @@ export async function authorizeCredentialUse( return { ok: false, error: auth.error || 'Authentication required' } } - const actingUserId = auth.authType === 'internal_jwt' ? callerUserId : auth.userId + const actingUserId = auth.authType === AuthType.INTERNAL_JWT ? callerUserId : auth.userId const [workflowContext] = workflowId ? await db @@ -217,7 +217,7 @@ export async function authorizeCredentialUse( return { ok: false, error: 'Credential not found' } } - if (auth.authType === 'internal_jwt') { + if (auth.authType === AuthType.INTERNAL_JWT) { return { ok: false, error: 'workflowId is required' } } diff --git a/apps/sim/lib/auth/hybrid.ts b/apps/sim/lib/auth/hybrid.ts index 0cc00e72e95..c3c03acb872 100644 --- a/apps/sim/lib/auth/hybrid.ts +++ b/apps/sim/lib/auth/hybrid.ts @@ -6,12 +6,20 @@ import { verifyInternalToken } from '@/lib/auth/internal' const logger = createLogger('HybridAuth') +export const AuthType = { + SESSION: 'session', + API_KEY: 'api_key', + INTERNAL_JWT: 'internal_jwt', +} as const + +export type AuthTypeValue = (typeof AuthType)[keyof typeof AuthType] + export interface AuthResult { success: boolean userId?: string userName?: string | null userEmail?: string | null - authType?: 'session' | 'api_key' | 'internal_jwt' + authType?: AuthTypeValue apiKeyType?: 'personal' | 'workspace' error?: string } @@ -46,14 +54,14 @@ async function resolveUserFromJwt( } if (userId) { - return { success: true, userId, authType: 'internal_jwt' } + return { success: true, userId, authType: AuthType.INTERNAL_JWT } } if (options.requireWorkflowId !== false) { return { success: false, error: 'userId required for internal JWT calls' } } - return { success: true, authType: 'internal_jwt' } + return { success: true, authType: AuthType.INTERNAL_JWT } } /** @@ -146,7 +154,7 @@ export async function checkSessionOrInternalAuth( userId: session.user.id, userName: session.user.name, userEmail: session.user.email, - authType: 'session', + authType: AuthType.SESSION, } } @@ -195,7 +203,7 @@ export async function checkHybridAuth( userId: session.user.id, userName: session.user.name, userEmail: session.user.email, - authType: 'session', + authType: AuthType.SESSION, } } @@ -208,7 +216,7 @@ export async function checkHybridAuth( return { success: true, userId: result.userId!, - authType: 'api_key', + authType: AuthType.API_KEY, apiKeyType: result.keyType, } } diff --git a/apps/sim/tools/workflow/executor.test.ts b/apps/sim/tools/workflow/executor.test.ts index 14b42619049..e49020ef0cc 100644 --- a/apps/sim/tools/workflow/executor.test.ts +++ b/apps/sim/tools/workflow/executor.test.ts @@ -242,6 +242,101 @@ describe('workflowExecutorTool', () => { }) }) + describe('transformResponse', () => { + const transformResponse = workflowExecutorTool.transformResponse! + + function mockResponse(body: any, status = 200): Response { + return { + ok: status >= 200 && status < 300, + status, + json: async () => body, + } as unknown as Response + } + + it.concurrent('should parse standard format response', async () => { + const body = { + success: true, + executionId: '550e8400-e29b-41d4-a716-446655440000', + output: { result: 'hello' }, + metadata: { duration: 500 }, + } + + const result = await transformResponse(mockResponse(body)) + + expect(result.success).toBe(true) + expect(result.output).toEqual({ result: 'hello' }) + expect(result.duration).toBe(500) + expect(result.error).toBeUndefined() + }) + + it.concurrent('should parse standard format failure', async () => { + const body = { + success: false, + executionId: '550e8400-e29b-41d4-a716-446655440000', + output: {}, + error: 'Something went wrong', + } + + const result = await transformResponse(mockResponse(body)) + + expect(result.success).toBe(false) + expect(result.error).toBe('Something went wrong') + }) + + it.concurrent('should default success to false when missing', async () => { + const body = { output: { data: 'test' } } + + const result = await transformResponse(mockResponse(body)) + + expect(result.success).toBe(false) + expect(result.output).toEqual({ data: 'test' }) + }) + + it.concurrent('should default output to empty object when missing', async () => { + const body = { success: true } + + const result = await transformResponse(mockResponse(body)) + + expect(result.success).toBe(true) + expect(result.output).toEqual({}) + expect(result.result).toEqual({}) + }) + + it.concurrent('should extract metadata duration', async () => { + const body = { + success: true, + output: {}, + metadata: { duration: 1234 }, + } + + const result = await transformResponse(mockResponse(body)) + + expect(result.duration).toBe(1234) + }) + + it.concurrent('should default duration to 0 when metadata is missing', async () => { + const body = { success: true, output: {} } + + const result = await transformResponse(mockResponse(body)) + + expect(result.duration).toBe(0) + }) + + it.concurrent('should extract workflowId and workflowName', async () => { + const body = { + success: true, + output: {}, + workflowId: 'wf-123', + workflowName: 'My Workflow', + } + + const result = await transformResponse(mockResponse(body)) + + expect(result.childWorkflowId).toBe('wf-123') + expect(result.childWorkflowName).toBe('My Workflow') + }) + }) + describe('tool metadata', () => { it.concurrent('should have correct id', () => { expect(workflowExecutorTool.id).toBe('workflow_executor') diff --git a/packages/testing/src/index.ts b/packages/testing/src/index.ts index 48881879ac8..ce84686a152 100644 --- a/packages/testing/src/index.ts +++ b/packages/testing/src/index.ts @@ -45,6 +45,7 @@ export * from './assertions' export * from './builders' export * from './factories' export { + AuthTypeMock, auditMock, clearRedisMocks, createEnvMock, diff --git a/packages/testing/src/mocks/hybrid-auth.mock.ts b/packages/testing/src/mocks/hybrid-auth.mock.ts index 7ddb2ecb1eb..add5babbcc3 100644 --- a/packages/testing/src/mocks/hybrid-auth.mock.ts +++ b/packages/testing/src/mocks/hybrid-auth.mock.ts @@ -6,12 +6,22 @@ import { vi } from 'vitest' import type { MockUser } from './auth.mock' import { defaultMockUser } from './auth.mock' +/** + * Auth type constants matching @/lib/auth/hybrid AuthType. + * Include this in vi.mock() factories so route code can reference AuthType.*. + */ +export const AuthTypeMock = { + SESSION: 'session', + API_KEY: 'api_key', + INTERNAL_JWT: 'internal_jwt', +} as const + interface HybridAuthResponse { success: boolean userId?: string userName?: string | null userEmail?: string | null - authType?: 'session' | 'api_key' | 'internal_jwt' + authType?: (typeof AuthTypeMock)[keyof typeof AuthTypeMock] error?: string } @@ -46,6 +56,7 @@ export function mockHybridAuth(user: MockUser = defaultMockUser): MockHybridAuth const mockCheckInternalAuth = vi.fn<() => Promise>() vi.doMock('@/lib/auth/hybrid', () => ({ + AuthType: AuthTypeMock, checkHybridAuth: mockCheckHybridAuth, checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth, checkInternalAuth: mockCheckInternalAuth, diff --git a/packages/testing/src/mocks/index.ts b/packages/testing/src/mocks/index.ts index f2c3fc6730b..69a2e5984d5 100644 --- a/packages/testing/src/mocks/index.ts +++ b/packages/testing/src/mocks/index.ts @@ -64,7 +64,7 @@ export { setupGlobalFetchMock, } from './fetch.mock' // Hybrid auth mocks -export { type MockHybridAuthResult, mockHybridAuth } from './hybrid-auth.mock' +export { AuthTypeMock, type MockHybridAuthResult, mockHybridAuth } from './hybrid-auth.mock' // Logger mocks export { clearLoggerMocks, createMockLogger, getLoggerCalls, loggerMock } from './logger.mock' // Redis mocks