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
6 changes: 3 additions & 3 deletions apps/sim/app/api/a2a/serve/[agentId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -242,9 +242,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R

const { id, method, params: rpcParams } = body
const requestApiKey = request.headers.get('X-API-Key')
const apiKey = authenticatedAuthType === 'api_key' ? requestApiKey : null
const apiKey = authenticatedAuthType === AuthType.API_KEY ? requestApiKey : null
const isPersonalApiKeyCaller =
authenticatedAuthType === 'api_key' && authenticatedApiKeyType === 'personal'
authenticatedAuthType === AuthType.API_KEY && authenticatedApiKeyType === 'personal'
const billedUserId = await getWorkspaceBilledAccountUserId(agent.workspaceId)
if (!billedUserId) {
logger.error('Unable to resolve workspace billed account for A2A execution', {
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/auth/oauth/credentials/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const { mockCheckSessionOrInternalAuth, mockLogger } = vi.hoisted(() => {
})

vi.mock('@/lib/auth/hybrid', () => ({
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
}))

Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/auth/oauth/token/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/app/api/auth/oauth/token/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 })
}

Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/files/delete/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/files/parse/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/files/serve/[...path]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}))

Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/files/upload/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/function/execute/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}))

Expand Down
6 changes: 3 additions & 3 deletions apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 })
Expand Down Expand Up @@ -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 })
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/knowledge/search/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}))

Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/mcp/serve/[serverId]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/app/api/mcp/serve/[serverId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -137,7 +137,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
executeAuthContext = {
authType: auth.authType,
userId: auth.userId,
apiKey: auth.authType === 'api_key' ? request.headers.get('X-API-Key') : null,
apiKey: auth.authType === AuthType.API_KEY ? request.headers.get('X-API-Key') : null,
}
}

Expand Down Expand Up @@ -295,7 +295,7 @@ async function handleToolsCall(
const internalToken = await generateInternalToken(publicServerOwnerId)
headers.Authorization = `Bearer ${internalToken}`
} else if (executeAuthContext) {
if (executeAuthContext.authType === 'api_key' && executeAuthContext.apiKey) {
if (executeAuthContext.authType === AuthType.API_KEY && executeAuthContext.apiKey) {
headers['X-API-Key'] = executeAuthContext.apiKey
} else {
const internalToken = await generateInternalToken(executeAuthContext.userId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { AuthType } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { preprocessExecution } from '@/lib/execution/preprocessing'
import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager'
Expand Down Expand Up @@ -39,7 +40,7 @@ export async function POST(

const resumeInput = payload?.input ?? payload ?? {}
const isPersonalApiKeyCaller =
access.auth?.authType === 'api_key' && access.auth?.apiKeyType === 'personal'
access.auth?.authType === AuthType.API_KEY && access.auth?.apiKeyType === 'personal'

let userId: string
if (isPersonalApiKeyCaller && access.auth?.userId) {
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/tools/custom/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ vi.mock('@/lib/auth', () => ({
}))

vi.mock('@/lib/auth/hybrid', () => ({
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
checkSessionOrInternalAuth: (...args: unknown[]) => mockCheckSessionOrInternalAuth(...args),
}))

Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/users/me/usage-limits/route.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/workflows/[id]/chat/status/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}))

Expand Down
115 changes: 115 additions & 0 deletions apps/sim/app/api/workflows/[id]/execute/response-block.test.ts
Original file line number Diff line number Diff line change
@@ -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> = {}): 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)
})
})
16 changes: 9 additions & 7 deletions apps/sim/app/api/workflows/[id]/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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')
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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)
}

Expand Down
Loading
Loading