1+ import crypto from 'crypto'
12import { eq } from 'drizzle-orm'
23import { type NextRequest , NextResponse } from 'next/server'
34import { z } from 'zod'
45import { env } from '@/lib/env'
56import { createLogger } from '@/lib/logs/console/logger'
6- import { getUserEntityPermissions } from '@/lib/permissions/utils'
7- import { SIM_AGENT_API_URL_DEFAULT , simAgentClient } from '@/lib/sim-agent'
8- import {
9- loadWorkflowFromNormalizedTables ,
10- saveWorkflowToNormalizedTables ,
11- } from '@/lib/workflows/db-helpers'
12- import { getUserId as getOAuthUserId } from '@/app/api/auth/oauth/utils'
13- import { getBlock } from '@/blocks'
14- import { getAllBlocks } from '@/blocks/registry'
15- import type { BlockConfig } from '@/blocks/types'
16- import { resolveOutputType } from '@/blocks/utils'
177import { db } from '@/db'
18- import { workflowCheckpoints , workflow as workflowTable } from '@/db/schema'
8+ import { workflow as workflowTable , workflowCheckpoints } from '@/db/schema'
9+ import { getAllBlocks , getBlock } from '@/blocks'
10+ import type { BlockConfig } from '@/blocks/types'
1911import { generateLoopBlocks , generateParallelBlocks } from '@/stores/workflows/workflow/utils'
12+ import { resolveOutputType } from '@/blocks/utils'
13+ import { getUserId } from '@/app/api/auth/oauth/utils'
14+ import { simAgentClient } from '@/lib/sim-agent'
15+ import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/validation'
16+ import { loadWorkflowFromNormalizedTables , saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers'
2017
21- const SIM_AGENT_API_URL = env . SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
22-
23- export const dynamic = 'force-dynamic'
24-
25- const logger = createLogger ( 'WorkflowYamlAPI' )
18+ const logger = createLogger ( 'YamlWorkflowAPI' )
2619
2720const YamlWorkflowRequestSchema = z . object ( {
2821 yamlContent : z . string ( ) . min ( 1 , 'YAML content is required' ) ,
2922 description : z . string ( ) . optional ( ) ,
30- chatId : z . string ( ) . optional ( ) , // For copilot checkpoints
31- source : z . enum ( [ 'copilot' , 'import ' , 'editor ' ] ) . default ( 'editor' ) ,
32- applyAutoLayout : z . boolean ( ) . default ( true ) ,
33- createCheckpoint : z . boolean ( ) . default ( false ) ,
23+ chatId : z . string ( ) . optional ( ) ,
24+ source : z . enum ( [ 'copilot' , 'editor ' , 'import ' ] ) . default ( 'editor' ) ,
25+ applyAutoLayout : z . boolean ( ) . optional ( ) . default ( false ) ,
26+ createCheckpoint : z . boolean ( ) . optional ( ) . default ( false ) ,
3427} )
3528
36- type YamlWorkflowRequest = z . infer < typeof YamlWorkflowRequestSchema >
29+ function updateBlockReferences (
30+ value : any ,
31+ blockIdMapping : Map < string , string > ,
32+ requestId : string
33+ ) : any {
34+ if ( typeof value === 'string' ) {
35+ // Replace references in string values
36+ for ( const [ oldId , newId ] of blockIdMapping . entries ( ) ) {
37+ if ( value . includes ( oldId ) ) {
38+ value = value
39+ . replaceAll ( `<${ oldId } .` , `<${ newId } .` )
40+ . replaceAll ( `%${ oldId } .` , `%${ newId } .` )
41+ }
42+ }
43+ return value
44+ }
45+
46+ if ( Array . isArray ( value ) ) {
47+ return value . map ( ( item ) => updateBlockReferences ( item , blockIdMapping , requestId ) )
48+ }
49+
50+ if ( value && typeof value === 'object' ) {
51+ const result : Record < string , any > = { }
52+ for ( const [ key , val ] of Object . entries ( value ) ) {
53+ result [ key ] = updateBlockReferences ( val , blockIdMapping , requestId )
54+ }
55+ return result
56+ }
57+
58+ return value
59+ }
3760
3861/**
3962 * Helper function to create a checkpoint before workflow changes
@@ -68,7 +91,7 @@ async function createWorkflowCheckpoint(
6891 { } as Record < string , BlockConfig >
6992 )
7093
71- const generateResponse = await fetch ( `${ SIM_AGENT_API_URL } /api/workflow/to-yaml` , {
94+ const generateResponse = await fetch ( `${ env . SIM_AGENT_API_URL } /api/workflow/to-yaml` , {
7295 method : 'POST' ,
7396 headers : {
7497 'Content-Type' : 'application/json' ,
@@ -114,120 +137,6 @@ async function createWorkflowCheckpoint(
114137 }
115138}
116139
117- /**
118- * Helper function to get user ID with proper authentication for both tool calls and direct requests
119- */
120- async function getUserId ( requestId : string , workflowId : string ) : Promise < string | null > {
121- // Use the OAuth utils function that handles both session and workflow-based auth
122- const userId = await getOAuthUserId ( requestId , workflowId )
123-
124- if ( ! userId ) {
125- logger . warn ( `[${ requestId } ] Could not determine user ID for workflow ${ workflowId } ` )
126- return null
127- }
128-
129- // For additional security, verify the user has permission to access this workflow
130- const workflowData = await db
131- . select ( )
132- . from ( workflowTable )
133- . where ( eq ( workflowTable . id , workflowId ) )
134- . then ( ( rows ) => rows [ 0 ] )
135-
136- if ( ! workflowData ) {
137- logger . warn ( `[${ requestId } ] Workflow ${ workflowId } not found` )
138- return null
139- }
140-
141- // Check if user has permission to update this workflow
142- let canUpdate = false
143-
144- // Case 1: User owns the workflow
145- if ( workflowData . userId === userId ) {
146- canUpdate = true
147- }
148-
149- // Case 2: Workflow belongs to a workspace and user has write or admin permission
150- if ( ! canUpdate && workflowData . workspaceId ) {
151- try {
152- const userPermission = await getUserEntityPermissions (
153- userId ,
154- 'workspace' ,
155- workflowData . workspaceId
156- )
157- if ( userPermission === 'write' || userPermission === 'admin' ) {
158- canUpdate = true
159- }
160- } catch ( error ) {
161- logger . warn ( `[${ requestId } ] Error checking workspace permissions:` , error )
162- }
163- }
164-
165- if ( ! canUpdate ) {
166- logger . warn ( `[${ requestId } ] User ${ userId } denied permission to update workflow ${ workflowId } ` )
167- return null
168- }
169-
170- return userId
171- }
172-
173- /**
174- * Helper function to update block references in values with new mapped IDs
175- */
176- function updateBlockReferences (
177- value : any ,
178- blockIdMapping : Map < string , string > ,
179- requestId : string
180- ) : any {
181- if ( typeof value === 'string' && value . includes ( '<' ) && value . includes ( '>' ) ) {
182- let processedValue = value
183- const blockMatches = value . match ( / < ( [ ^ > ] + ) > / g)
184-
185- if ( blockMatches ) {
186- for ( const match of blockMatches ) {
187- const path = match . slice ( 1 , - 1 )
188- const [ blockRef ] = path . split ( '.' )
189-
190- // Skip system references (start, loop, parallel, variable)
191- if ( [ 'start' , 'loop' , 'parallel' , 'variable' ] . includes ( blockRef . toLowerCase ( ) ) ) {
192- continue
193- }
194-
195- // Check if this references an old block ID that needs mapping
196- const newMappedId = blockIdMapping . get ( blockRef )
197- if ( newMappedId ) {
198- logger . info ( `[${ requestId } ] Updating block reference: ${ blockRef } -> ${ newMappedId } ` )
199- processedValue = processedValue . replace (
200- new RegExp ( `<${ blockRef } \\.` , 'g' ) ,
201- `<${ newMappedId } .`
202- )
203- processedValue = processedValue . replace (
204- new RegExp ( `<${ blockRef } >` , 'g' ) ,
205- `<${ newMappedId } >`
206- )
207- }
208- }
209- }
210-
211- return processedValue
212- }
213-
214- // Handle arrays
215- if ( Array . isArray ( value ) ) {
216- return value . map ( ( item ) => updateBlockReferences ( item , blockIdMapping , requestId ) )
217- }
218-
219- // Handle objects
220- if ( value !== null && typeof value === 'object' ) {
221- const result = { ...value }
222- for ( const key in result ) {
223- result [ key ] = updateBlockReferences ( result [ key ] , blockIdMapping , requestId )
224- }
225- return result
226- }
227-
228- return value
229- }
230-
231140/**
232141 * PUT /api/workflows/[id]/yaml
233142 * Consolidated YAML workflow saving endpoint
@@ -281,7 +190,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
281190 { } as Record < string , BlockConfig >
282191 )
283192
284- const conversionResponse = await fetch ( `${ SIM_AGENT_API_URL } /api/yaml/to-workflow` , {
193+ const conversionResponse = await fetch ( `${ env . SIM_AGENT_API_URL } /api/yaml/to-workflow` , {
285194 method : 'POST' ,
286195 headers : {
287196 'Content-Type' : 'application/json' ,
@@ -541,7 +450,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
541450 }
542451 }
543452
544- // Debug: Log block parent-child relationships before generating loops
545453 // Generate loop and parallel configurations
546454 const loops = generateLoopBlocks ( newWorkflowState . blocks )
547455 const parallels = generateParallelBlocks ( newWorkflowState . blocks )
@@ -626,6 +534,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
626534 }
627535 }
628536
537+ // Sanitize custom tools in agent blocks before saving
538+ const { blocks : sanitizedBlocks , warnings : sanitationWarnings } = sanitizeAgentToolsInBlocks (
539+ newWorkflowState . blocks
540+ )
541+ if ( sanitationWarnings . length > 0 ) {
542+ logger . warn (
543+ `[${ requestId } ] Tool sanitation produced ${ sanitationWarnings . length } warning(s)`
544+ )
545+ }
546+ newWorkflowState . blocks = sanitizedBlocks
547+
629548 // Save to database
630549 const saveResult = await saveWorkflowToNormalizedTables ( workflowId , newWorkflowState )
631550
@@ -635,7 +554,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
635554 success : false ,
636555 message : `Database save failed: ${ saveResult . error || 'Unknown error' } ` ,
637556 errors : [ saveResult . error || 'Database save failed' ] ,
638- warnings,
557+ warnings : [ ... warnings , ... sanitationWarnings ] ,
639558 } )
640559 }
641560
@@ -687,7 +606,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
687606 parallelsCount : Object . keys ( parallels ) . length ,
688607 } ,
689608 errors : [ ] ,
690- warnings,
609+ warnings : [ ... warnings , ... sanitationWarnings ] ,
691610 } )
692611 } catch ( error ) {
693612 const elapsed = Date . now ( ) - startTime
0 commit comments