Skip to content

Commit 0673dbe

Browse files
waleedlatif1claude
andcommitted
fix(executor): skip Response block formatting for internal JWT callers
The workflow executor tool received `{error: true}` despite successful child workflow execution when the child had a Response block. This happened because `createHttpResponseFromBlock()` hijacked the response with raw user-defined data, and the executor's `transformResponse` expected the standard `{success, executionId, output, metadata}` wrapper. Fix: skip Response block formatting when `authType === INTERNAL_JWT` since Response blocks are designed for external API consumers, not internal workflow-to-workflow calls. Also extract `AuthType` constants from magic strings across all auth type comparisons in the codebase. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4cb0f4a commit 0673dbe

File tree

11 files changed

+140
-34
lines changed

11 files changed

+140
-34
lines changed

apps/sim/app/api/a2a/serve/[agentId]/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
isTerminalState,
1414
parseWorkflowSSEChunk,
1515
} from '@/lib/a2a/utils'
16-
import { type AuthResult, checkHybridAuth } from '@/lib/auth/hybrid'
16+
import { type AuthResult, AuthType, checkHybridAuth } from '@/lib/auth/hybrid'
1717
import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis'
1818
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
1919
import { SSE_HEADERS } from '@/lib/core/utils/sse'
@@ -242,9 +242,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
242242

243243
const { id, method, params: rpcParams } = body
244244
const requestApiKey = request.headers.get('X-API-Key')
245-
const apiKey = authenticatedAuthType === 'api_key' ? requestApiKey : null
245+
const apiKey = authenticatedAuthType === AuthType.API_KEY ? requestApiKey : null
246246
const isPersonalApiKeyCaller =
247-
authenticatedAuthType === 'api_key' && authenticatedApiKeyType === 'personal'
247+
authenticatedAuthType === AuthType.API_KEY && authenticatedApiKeyType === 'personal'
248248
const billedUserId = await getWorkspaceBilledAccountUserId(agent.workspaceId)
249249
if (!billedUserId) {
250250
logger.error('Unable to resolve workspace billed account for A2A execution', {

apps/sim/app/api/auth/oauth/token/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
44
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
5-
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
5+
import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
66
import { generateRequestId } from '@/lib/core/utils/request'
77
import { getCredential, getOAuthToken, refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
88

@@ -72,7 +72,7 @@ export async function POST(request: NextRequest) {
7272
})
7373

7474
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
75-
if (!auth.success || auth.authType !== 'session' || !auth.userId) {
75+
if (!auth.success || auth.authType !== AuthType.SESSION || !auth.userId) {
7676
logger.warn(`[${requestId}] Unauthorized request for credentialAccountUserId path`, {
7777
success: auth.success,
7878
authType: auth.authType,
@@ -202,7 +202,7 @@ export async function GET(request: NextRequest) {
202202
credentialId,
203203
requireWorkflowIdForInternal: false,
204204
})
205-
if (!authz.ok || authz.authType !== 'session' || !authz.credentialOwnerUserId) {
205+
if (!authz.ok || authz.authType !== AuthType.SESSION || !authz.credentialOwnerUserId) {
206206
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
207207
}
208208

apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'
22
import { createLogger } from '@sim/logger'
33
import { type NextRequest, NextResponse } from 'next/server'
44
import { z } from 'zod'
5-
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
5+
import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
66
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
77
import { createTagDefinition, getTagDefinitions } from '@/lib/knowledge/tags/service'
88
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'
@@ -25,7 +25,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
2525
}
2626

2727
// For session auth, verify KB access. Internal JWT is trusted.
28-
if (auth.authType === 'session' && auth.userId) {
28+
if (auth.authType === AuthType.SESSION && auth.userId) {
2929
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
3030
if (!accessCheck.hasAccess) {
3131
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
@@ -62,7 +62,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
6262
}
6363

6464
// For session auth, verify KB access. Internal JWT is trusted.
65-
if (auth.authType === 'session' && auth.userId) {
65+
if (auth.authType === AuthType.SESSION && auth.userId) {
6666
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
6767
if (!accessCheck.hasAccess) {
6868
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })

apps/sim/app/api/mcp/serve/[serverId]/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
1919
import { createLogger } from '@sim/logger'
2020
import { and, eq } from 'drizzle-orm'
2121
import { type NextRequest, NextResponse } from 'next/server'
22-
import { type AuthResult, checkHybridAuth } from '@/lib/auth/hybrid'
22+
import { type AuthResult, AuthType, checkHybridAuth } from '@/lib/auth/hybrid'
2323
import { generateInternalToken } from '@/lib/auth/internal'
2424
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
2525
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
@@ -137,7 +137,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
137137
executeAuthContext = {
138138
authType: auth.authType,
139139
userId: auth.userId,
140-
apiKey: auth.authType === 'api_key' ? request.headers.get('X-API-Key') : null,
140+
apiKey: auth.authType === AuthType.API_KEY ? request.headers.get('X-API-Key') : null,
141141
}
142142
}
143143

@@ -295,7 +295,7 @@ async function handleToolsCall(
295295
const internalToken = await generateInternalToken(publicServerOwnerId)
296296
headers.Authorization = `Bearer ${internalToken}`
297297
} else if (executeAuthContext) {
298-
if (executeAuthContext.authType === 'api_key' && executeAuthContext.apiKey) {
298+
if (executeAuthContext.authType === AuthType.API_KEY && executeAuthContext.apiKey) {
299299
headers['X-API-Key'] = executeAuthContext.apiKey
300300
} else {
301301
const internalToken = await generateInternalToken(executeAuthContext.userId)

apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { randomUUID } from 'crypto'
22
import { createLogger } from '@sim/logger'
33
import { type NextRequest, NextResponse } from 'next/server'
4+
import { AuthType } from '@/lib/auth/hybrid'
45
import { generateRequestId } from '@/lib/core/utils/request'
56
import { preprocessExecution } from '@/lib/execution/preprocessing'
67
import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager'
@@ -39,7 +40,7 @@ export async function POST(
3940

4041
const resumeInput = payload?.input ?? payload ?? {}
4142
const isPersonalApiKeyCaller =
42-
access.auth?.authType === 'api_key' && access.auth?.apiKeyType === 'personal'
43+
access.auth?.authType === AuthType.API_KEY && access.auth?.apiKeyType === 'personal'
4344

4445
let userId: string
4546
if (isPersonalApiKeyCaller && access.auth?.userId) {

apps/sim/app/api/users/me/usage-limits/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
3-
import { checkHybridAuth } from '@/lib/auth/hybrid'
3+
import { AuthType, checkHybridAuth } from '@/lib/auth/hybrid'
44
import { checkServerSideUsageLimits } from '@/lib/billing'
55
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
66
import { getEffectiveCurrentPeriodCost } from '@/lib/billing/core/usage'
@@ -20,7 +20,7 @@ export async function GET(request: NextRequest) {
2020

2121
const userSubscription = await getHighestPrioritySubscription(authenticatedUserId)
2222
const rateLimiter = new RateLimiter()
23-
const triggerType = auth.authType === 'api_key' ? 'api' : 'manual'
23+
const triggerType = auth.authType === AuthType.API_KEY ? 'api' : 'manual'
2424
const [syncStatus, asyncStatus] = await Promise.all([
2525
rateLimiter.getRateLimitStatusWithSubscription(
2626
authenticatedUserId,

apps/sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { validate as uuidValidate, v4 as uuidv4 } from 'uuid'
44
import { z } from 'zod'
5-
import { checkHybridAuth } from '@/lib/auth/hybrid'
5+
import { AuthType, checkHybridAuth } from '@/lib/auth/hybrid'
66
import { getJobQueue, shouldExecuteInline } from '@/lib/core/async-jobs'
77
import {
88
createTimeoutAbortController,
@@ -322,7 +322,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
322322
)
323323
}
324324

325-
const defaultTriggerType = isPublicApiAccess || auth.authType === 'api_key' ? 'api' : 'manual'
325+
const defaultTriggerType =
326+
isPublicApiAccess || auth.authType === AuthType.API_KEY ? 'api' : 'manual'
326327

327328
const {
328329
selectedOutputs,
@@ -381,7 +382,9 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
381382
// For API key and internal JWT auth, the entire body is the input (except for our control fields)
382383
// For session auth, the input is explicitly provided in the input field
383384
const input =
384-
isPublicApiAccess || auth.authType === 'api_key' || auth.authType === 'internal_jwt'
385+
isPublicApiAccess ||
386+
auth.authType === AuthType.API_KEY ||
387+
auth.authType === AuthType.INTERNAL_JWT
385388
? (() => {
386389
const {
387390
selectedOutputs,
@@ -407,7 +410,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
407410
// Public API callers always execute the deployed state, never the draft.
408411
const shouldUseDraftState = isPublicApiAccess
409412
? false
410-
: (useDraftState ?? auth.authType === 'session')
413+
: (useDraftState ?? auth.authType === AuthType.SESSION)
411414
const streamHeader = req.headers.get('X-Stream-Response') === 'true'
412415
const enableSSE = streamHeader || streamParam === true
413416
const executionModeHeader = req.headers.get('X-Execution-Mode')
@@ -440,7 +443,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
440443
// Client-side sessions and personal API keys bill/permission-check the
441444
// authenticated user, not the workspace billed account.
442445
const useAuthenticatedUserAsActor =
443-
isClientSession || (auth.authType === 'api_key' && auth.apiKeyType === 'personal')
446+
isClientSession || (auth.authType === AuthType.API_KEY && auth.apiKeyType === 'personal')
444447

445448
// Authorization fetches the full workflow record and checks workspace permissions.
446449
// 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:
670673

671674
const resultWithBase64 = { ...result, output: outputWithBase64 }
672675

673-
const hasResponseBlock = workflowHasResponseBlock(resultWithBase64)
674-
if (hasResponseBlock) {
676+
if (auth.authType !== AuthType.INTERNAL_JWT && workflowHasResponseBlock(resultWithBase64)) {
675677
return createHttpResponseFromBlock(resultWithBase64)
676678
}
677679

apps/sim/app/api/workflows/[id]/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { and, eq, isNull, ne } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
77
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
8-
import { checkHybridAuth, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
8+
import { AuthType, checkHybridAuth, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
99
import { env } from '@/lib/core/config/env'
1010
import { PlatformEvents } from '@/lib/core/telemetry'
1111
import { generateRequestId } from '@/lib/core/utils/request'
@@ -39,7 +39,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
3939
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
4040
}
4141

42-
const isInternalCall = auth.authType === 'internal_jwt'
42+
const isInternalCall = auth.authType === AuthType.INTERNAL_JWT
4343
const userId = auth.userId || null
4444

4545
let workflowData = await getWorkflowById(workflowId)

apps/sim/lib/auth/credential-access.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { db } from '@sim/db'
22
import { account, credential, credentialMember, workflow as workflowTable } from '@sim/db/schema'
33
import { and, eq } from 'drizzle-orm'
44
import type { NextRequest } from 'next/server'
5-
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
5+
import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
66
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
77

88
export interface CredentialAccessResult {
99
ok: boolean
1010
error?: string
11-
authType?: 'session' | 'internal_jwt'
11+
authType?: typeof AuthType.SESSION | typeof AuthType.INTERNAL_JWT
1212
requesterUserId?: string
1313
credentialOwnerUserId?: string
1414
workspaceId?: string
@@ -39,7 +39,7 @@ export async function authorizeCredentialUse(
3939
return { ok: false, error: auth.error || 'Authentication required' }
4040
}
4141

42-
const actingUserId = auth.authType === 'internal_jwt' ? callerUserId : auth.userId
42+
const actingUserId = auth.authType === AuthType.INTERNAL_JWT ? callerUserId : auth.userId
4343

4444
const [workflowContext] = workflowId
4545
? await db
@@ -217,7 +217,7 @@ export async function authorizeCredentialUse(
217217
return { ok: false, error: 'Credential not found' }
218218
}
219219

220-
if (auth.authType === 'internal_jwt') {
220+
if (auth.authType === AuthType.INTERNAL_JWT) {
221221
return { ok: false, error: 'workflowId is required' }
222222
}
223223

apps/sim/lib/auth/hybrid.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,20 @@ import { verifyInternalToken } from '@/lib/auth/internal'
66

77
const logger = createLogger('HybridAuth')
88

9+
export const AuthType = {
10+
SESSION: 'session',
11+
API_KEY: 'api_key',
12+
INTERNAL_JWT: 'internal_jwt',
13+
} as const
14+
15+
export type AuthTypeValue = (typeof AuthType)[keyof typeof AuthType]
16+
917
export interface AuthResult {
1018
success: boolean
1119
userId?: string
1220
userName?: string | null
1321
userEmail?: string | null
14-
authType?: 'session' | 'api_key' | 'internal_jwt'
22+
authType?: AuthTypeValue
1523
apiKeyType?: 'personal' | 'workspace'
1624
error?: string
1725
}
@@ -46,14 +54,14 @@ async function resolveUserFromJwt(
4654
}
4755

4856
if (userId) {
49-
return { success: true, userId, authType: 'internal_jwt' }
57+
return { success: true, userId, authType: AuthType.INTERNAL_JWT }
5058
}
5159

5260
if (options.requireWorkflowId !== false) {
5361
return { success: false, error: 'userId required for internal JWT calls' }
5462
}
5563

56-
return { success: true, authType: 'internal_jwt' }
64+
return { success: true, authType: AuthType.INTERNAL_JWT }
5765
}
5866

5967
/**
@@ -146,7 +154,7 @@ export async function checkSessionOrInternalAuth(
146154
userId: session.user.id,
147155
userName: session.user.name,
148156
userEmail: session.user.email,
149-
authType: 'session',
157+
authType: AuthType.SESSION,
150158
}
151159
}
152160

@@ -195,7 +203,7 @@ export async function checkHybridAuth(
195203
userId: session.user.id,
196204
userName: session.user.name,
197205
userEmail: session.user.email,
198-
authType: 'session',
206+
authType: AuthType.SESSION,
199207
}
200208
}
201209

@@ -208,7 +216,7 @@ export async function checkHybridAuth(
208216
return {
209217
success: true,
210218
userId: result.userId!,
211-
authType: 'api_key',
219+
authType: AuthType.API_KEY,
212220
apiKeyType: result.keyType,
213221
}
214222
}

0 commit comments

Comments
 (0)