(
req: NextRequest,
params: Promise<{ id: string }>
)
| 373 | ) |
| 374 | |
| 375 | async function handleExecutePost( |
| 376 | req: NextRequest, |
| 377 | params: Promise<{ id: string }> |
| 378 | ): Promise<NextResponse | Response> { |
| 379 | const requestId = generateRequestId() |
| 380 | const { id: workflowId } = await params |
| 381 | let reqLogger = logger.withMetadata({ requestId, workflowId }) |
| 382 | |
| 383 | const incomingCallChain = parseCallChain(req.headers.get(SIM_VIA_HEADER)) |
| 384 | const callChainError = validateCallChain(incomingCallChain) |
| 385 | if (callChainError) { |
| 386 | reqLogger.warn(`Call chain rejected: ${callChainError}`) |
| 387 | return NextResponse.json({ error: callChainError }, { status: 409 }) |
| 388 | } |
| 389 | const callChain = buildNextCallChain(incomingCallChain, workflowId) |
| 390 | |
| 391 | // Hoisted so the outer catch can release a reserved billing slot when a throw |
| 392 | // after preprocessExecution exits before the stream takes over its release. |
| 393 | let executionId = '' |
| 394 | |
| 395 | try { |
| 396 | const auth = await checkHybridAuth(req, { requireWorkflowId: false }) |
| 397 | |
| 398 | // CSRF guard: reject session-cookie execution that is provably cross-site |
| 399 | // (a different site driving the user's browser). same-origin and same-site |
| 400 | // are allowed so multi-subdomain deployments (e.g. www.<domain> calling |
| 401 | // <domain>) keep working. Scoped to session auth — API-key / public-API / |
| 402 | // internal-JWT callers don't use cookies. Not a defense against a non-browser |
| 403 | // client forging headers; that's covered by the credit/rate-limit gates. |
| 404 | if (auth.success && auth.authType === AuthType.SESSION && isCrossSiteSessionRequest(req)) { |
| 405 | reqLogger.warn('Rejected cross-site session-authenticated execute request') |
| 406 | return NextResponse.json({ error: 'Access denied' }, { status: 403 }) |
| 407 | } |
| 408 | |
| 409 | const isMcpBridgeRequest = |
| 410 | auth.authType === AuthType.INTERNAL_JWT && req.headers.get(MCP_TOOL_BRIDGE_HEADER) === 'true' |
| 411 | const useMcpBridgeAuthenticatedUserAsActor = |
| 412 | isMcpBridgeRequest && req.headers.get(MCP_TOOL_BRIDGE_ACTOR_HEADER) === 'authenticated-user' |
| 413 | |
| 414 | let userId: string |
| 415 | let isPublicApiAccess = false |
| 416 | let gateWorkspaceId: string | undefined |
| 417 | |
| 418 | if (!auth.success || !auth.userId) { |
| 419 | const hasExplicitCredentials = |
| 420 | req.headers.has('x-api-key') || req.headers.get('authorization')?.startsWith('Bearer ') |
| 421 | if (hasExplicitCredentials) { |
| 422 | return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) |
| 423 | } |
| 424 | |
| 425 | const [wf] = await db |
| 426 | .select({ |
| 427 | isPublicApi: workflowTable.isPublicApi, |
| 428 | isDeployed: workflowTable.isDeployed, |
| 429 | userId: workflowTable.userId, |
| 430 | workspaceId: workflowTable.workspaceId, |
| 431 | }) |
| 432 | .from(workflowTable) |
no test coverage detected