@@ -254,10 +254,49 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
254254
255255 try {
256256 const auth = await checkHybridAuth ( req , { requireWorkflowId : false } )
257+
258+ let userId : string
259+ let isPublicApiAccess = false
260+
257261 if ( ! auth . success || ! auth . userId ) {
258- return NextResponse . json ( { error : auth . error || 'Unauthorized' } , { status : 401 } )
262+ const hasExplicitCredentials =
263+ req . headers . has ( 'x-api-key' ) || req . headers . get ( 'authorization' ) ?. startsWith ( 'Bearer ' )
264+ if ( hasExplicitCredentials ) {
265+ return NextResponse . json ( { error : auth . error || 'Unauthorized' } , { status : 401 } )
266+ }
267+
268+ const { db : dbClient , workflow : workflowTable } = await import ( '@sim/db' )
269+ const { eq } = await import ( 'drizzle-orm' )
270+ const [ wf ] = await dbClient
271+ . select ( {
272+ isPublicApi : workflowTable . isPublicApi ,
273+ isDeployed : workflowTable . isDeployed ,
274+ userId : workflowTable . userId ,
275+ } )
276+ . from ( workflowTable )
277+ . where ( eq ( workflowTable . id , workflowId ) )
278+ . limit ( 1 )
279+
280+ if ( ! wf ?. isPublicApi || ! wf . isDeployed ) {
281+ return NextResponse . json ( { error : auth . error || 'Unauthorized' } , { status : 401 } )
282+ }
283+
284+ const { isPublicApiDisabled } = await import ( '@/lib/core/config/feature-flags' )
285+ if ( isPublicApiDisabled ) {
286+ return NextResponse . json ( { error : auth . error || 'Unauthorized' } , { status : 401 } )
287+ }
288+
289+ const { getUserPermissionConfig } = await import ( '@/ee/access-control/utils/permission-check' )
290+ const ownerConfig = await getUserPermissionConfig ( wf . userId )
291+ if ( ownerConfig ?. disablePublicApi ) {
292+ return NextResponse . json ( { error : auth . error || 'Unauthorized' } , { status : 401 } )
293+ }
294+
295+ userId = wf . userId
296+ isPublicApiAccess = true
297+ } else {
298+ userId = auth . userId
259299 }
260- const userId = auth . userId
261300
262301 let body : any = { }
263302 try {
@@ -284,7 +323,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
284323 )
285324 }
286325
287- const defaultTriggerType = auth . authType === 'api_key' ? 'api' : 'manual'
326+ const defaultTriggerType = isPublicApiAccess || auth . authType === 'api_key' ? 'api' : 'manual'
288327
289328 const {
290329 selectedOutputs,
@@ -341,7 +380,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
341380 // For API key and internal JWT auth, the entire body is the input (except for our control fields)
342381 // For session auth, the input is explicitly provided in the input field
343382 const input =
344- auth . authType === 'api_key' || auth . authType === 'internal_jwt'
383+ isPublicApiAccess || auth . authType === 'api_key' || auth . authType === 'internal_jwt'
345384 ? ( ( ) => {
346385 const {
347386 selectedOutputs,
@@ -360,7 +399,16 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
360399 } ) ( )
361400 : validatedInput
362401
363- const shouldUseDraftState = useDraftState ?? auth . authType === 'session'
402+ // Public API callers must not override workflow state, stop execution early, or resume from a block.
403+ // These are administrative/debugging features restricted to authenticated users.
404+ const sanitizedWorkflowStateOverride = isPublicApiAccess ? undefined : workflowStateOverride
405+ const sanitizedStopAfterBlockId = isPublicApiAccess ? undefined : stopAfterBlockId
406+ const sanitizedRunFromBlock = isPublicApiAccess ? undefined : resolvedRunFromBlock
407+
408+ // Public API callers always execute the deployed state, never the draft.
409+ const shouldUseDraftState = isPublicApiAccess
410+ ? false
411+ : ( useDraftState ?? auth . authType === 'session' )
364412 const workflowAuthorization = await authorizeWorkflowByWorkspacePermission ( {
365413 workflowId,
366414 userId,
@@ -533,7 +581,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
533581 )
534582 }
535583
536- const effectiveWorkflowStateOverride = workflowStateOverride || cachedWorkflowData || undefined
584+ const effectiveWorkflowStateOverride =
585+ sanitizedWorkflowStateOverride || cachedWorkflowData || undefined
537586
538587 if ( ! enableSSE ) {
539588 logger . info ( `[${ requestId } ] Using non-SSE execution (direct JSON response)` )
@@ -575,8 +624,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
575624 loggingSession,
576625 includeFileBase64,
577626 base64MaxBytes,
578- stopAfterBlockId,
579- runFromBlock : resolvedRunFromBlock ,
627+ stopAfterBlockId : sanitizedStopAfterBlockId ,
628+ runFromBlock : sanitizedRunFromBlock ,
580629 abortSignal : timeoutController . signal ,
581630 } )
582631
@@ -973,8 +1022,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
9731022 abortSignal : timeoutController . signal ,
9741023 includeFileBase64,
9751024 base64MaxBytes,
976- stopAfterBlockId,
977- runFromBlock : resolvedRunFromBlock ,
1025+ stopAfterBlockId : sanitizedStopAfterBlockId ,
1026+ runFromBlock : sanitizedRunFromBlock ,
9781027 } )
9791028
9801029 if ( result . status === 'paused' ) {
0 commit comments