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
12 changes: 6 additions & 6 deletions apps/docs/components/ui/icon-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ import {
EyeIcon,
FirecrawlIcon,
FirefliesIcon,
GitLabIcon,
GithubIcon,
GitLabIcon,
GmailIcon,
GongIcon,
GoogleBooksIcon,
Expand Down Expand Up @@ -71,9 +71,9 @@ import {
LinearIcon,
LinkedInIcon,
LinkupIcon,
MailServerIcon,
MailchimpIcon,
MailgunIcon,
MailServerIcon,
Mem0Icon,
MicrosoftDataverseIcon,
MicrosoftExcelIcon,
Expand Down Expand Up @@ -106,8 +106,6 @@ import {
ResendIcon,
RevenueCatIcon,
S3Icon,
SQSIcon,
STTIcon,
SalesforceIcon,
SearchIcon,
SendgridIcon,
Expand All @@ -119,17 +117,19 @@ import {
SimilarwebIcon,
SlackIcon,
SmtpIcon,
SQSIcon,
SshIcon,
STTIcon,
StagehandIcon,
StripeIcon,
SupabaseIcon,
TTSIcon,
TavilyIcon,
TelegramIcon,
TextractIcon,
TinybirdIcon,
TranslateIcon,
TrelloIcon,
TTSIcon,
TwilioIcon,
TypeformIcon,
UpstashIcon,
Expand All @@ -140,11 +140,11 @@ import {
WhatsAppIcon,
WikipediaIcon,
WordpressIcon,
xIcon,
YouTubeIcon,
ZendeskIcon,
ZepIcon,
ZoomIcon,
xIcon,
} from '@/components/icons'

type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/en/tools/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,4 @@
"zep",
"zoom"
]
}
}
45 changes: 45 additions & 0 deletions apps/sim/app/api/workflows/[id]/deploy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
deployedAt: null,
apiKey: null,
needsRedeployment: false,
isPublicApi: workflowData.isPublicApi ?? false,
})
}

Expand Down Expand Up @@ -98,6 +99,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
isDeployed: workflowData.isDeployed,
deployedAt: workflowData.deployedAt,
needsRedeployment,
isPublicApi: workflowData.isPublicApi ?? false,
})
} catch (error: any) {
logger.error(`[${requestId}] Error fetching deployment info: ${id}`, error)
Expand Down Expand Up @@ -301,6 +303,49 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
}
}

export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = generateRequestId()
const { id } = await params

try {
const { error, session } = await validateWorkflowPermissions(id, requestId, 'admin')
if (error) {
return createErrorResponse(error.message, error.status)
}

const body = await request.json()
const { isPublicApi } = body

if (typeof isPublicApi !== 'boolean') {
return createErrorResponse('Invalid request body: isPublicApi must be a boolean', 400)
}

if (isPublicApi) {
const { validatePublicApiAllowed, PublicApiNotAllowedError } = await import(
'@/ee/access-control/utils/permission-check'
)
try {
await validatePublicApiAllowed(session?.user?.id)
} catch (err) {
if (err instanceof PublicApiNotAllowedError) {
return createErrorResponse('Public API access is disabled', 403)
}
throw err
}
}

await db.update(workflow).set({ isPublicApi }).where(eq(workflow.id, id))

logger.info(`[${requestId}] Updated isPublicApi for workflow ${id} to ${isPublicApi}`)

return createSuccessResponse({ isPublicApi })
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Failed to update deployment settings'
logger.error(`[${requestId}] Error updating deployment settings: ${id}`, { error })
return createErrorResponse(message, 500)
}
}

export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
Expand Down
63 changes: 56 additions & 7 deletions apps/sim/app/api/workflows/[id]/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,49 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:

try {
const auth = await checkHybridAuth(req, { requireWorkflowId: false })

let userId: string
let isPublicApiAccess = false

if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
const hasExplicitCredentials =
req.headers.has('x-api-key') || req.headers.get('authorization')?.startsWith('Bearer ')
if (hasExplicitCredentials) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const { db: dbClient, workflow: workflowTable } = await import('@sim/db')
const { eq } = await import('drizzle-orm')
const [wf] = await dbClient
.select({
isPublicApi: workflowTable.isPublicApi,
isDeployed: workflowTable.isDeployed,
userId: workflowTable.userId,
})
.from(workflowTable)
.where(eq(workflowTable.id, workflowId))
.limit(1)

if (!wf?.isPublicApi || !wf.isDeployed) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const { isPublicApiDisabled } = await import('@/lib/core/config/feature-flags')
if (isPublicApiDisabled) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const { getUserPermissionConfig } = await import('@/ee/access-control/utils/permission-check')
const ownerConfig = await getUserPermissionConfig(wf.userId)
if (ownerConfig?.disablePublicApi) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

userId = wf.userId
isPublicApiAccess = true
} else {
userId = auth.userId
}
const userId = auth.userId

let body: any = {}
try {
Expand All @@ -284,7 +323,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
)
}

const defaultTriggerType = auth.authType === 'api_key' ? 'api' : 'manual'
const defaultTriggerType = isPublicApiAccess || auth.authType === 'api_key' ? 'api' : 'manual'

const {
selectedOutputs,
Expand All @@ -305,7 +344,9 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
| { startBlockId: string; sourceSnapshot: SerializableExecutionState }
| undefined
if (rawRunFromBlock) {
if (rawRunFromBlock.sourceSnapshot) {
if (rawRunFromBlock.sourceSnapshot && !isPublicApiAccess) {
// Public API callers cannot inject arbitrary block state via sourceSnapshot.
// They must use executionId to resume from a server-stored execution state.
resolvedRunFromBlock = {
startBlockId: rawRunFromBlock.startBlockId,
sourceSnapshot: rawRunFromBlock.sourceSnapshot as SerializableExecutionState,
Expand Down Expand Up @@ -341,7 +382,7 @@ 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 =
auth.authType === 'api_key' || auth.authType === 'internal_jwt'
isPublicApiAccess || auth.authType === 'api_key' || auth.authType === 'internal_jwt'
? (() => {
const {
selectedOutputs,
Expand All @@ -360,7 +401,14 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
})()
: validatedInput

const shouldUseDraftState = useDraftState ?? auth.authType === 'session'
// Public API callers must not inject arbitrary workflow state overrides (code injection risk).
// stopAfterBlockId and runFromBlock are safe — they control execution flow within the deployed state.
const sanitizedWorkflowStateOverride = isPublicApiAccess ? undefined : workflowStateOverride

// Public API callers always execute the deployed state, never the draft.
const shouldUseDraftState = isPublicApiAccess
? false
: (useDraftState ?? auth.authType === 'session')
const workflowAuthorization = await authorizeWorkflowByWorkspacePermission({
workflowId,
userId,
Expand Down Expand Up @@ -533,7 +581,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
)
}

const effectiveWorkflowStateOverride = workflowStateOverride || cachedWorkflowData || undefined
const effectiveWorkflowStateOverride =
sanitizedWorkflowStateOverride || cachedWorkflowData || undefined

if (!enableSSE) {
logger.info(`[${requestId}] Using non-SSE execution (direct JSON response)`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface WorkflowDeploymentInfo {
endpoint: string
exampleCommand: string
needsRedeployment: boolean
isPublicApi?: boolean
}

interface ApiDeployProps {
Expand Down Expand Up @@ -107,12 +108,12 @@ export function ApiDeploy({
if (!info) return ''
const endpoint = getBaseEndpoint()
const payload = getPayloadObject()
const isPublic = info.isPublicApi

switch (language) {
case 'curl':
return `curl -X POST \\
-H "X-API-Key: $SIM_API_KEY" \\
-H "Content-Type: application/json" \\
${isPublic ? '' : ' -H "X-API-Key: $SIM_API_KEY" \\\n'} -H "Content-Type: application/json" \\
-d '${JSON.stringify(payload)}' \\
${endpoint}`

Expand All @@ -123,8 +124,7 @@ import requests
response = requests.post(
"${endpoint}",
headers={
"X-API-Key": os.environ.get("SIM_API_KEY"),
"Content-Type": "application/json"
${isPublic ? '' : ' "X-API-Key": os.environ.get("SIM_API_KEY"),\n'} "Content-Type": "application/json"
},
json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')}
)
Expand All @@ -135,8 +135,7 @@ print(response.json())`
return `const response = await fetch("${endpoint}", {
method: "POST",
headers: {
"X-API-Key": process.env.SIM_API_KEY,
"Content-Type": "application/json"
${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json"
},
body: JSON.stringify(${JSON.stringify(payload)})
});
Expand All @@ -148,8 +147,7 @@ console.log(data);`
return `const response = await fetch("${endpoint}", {
method: "POST",
headers: {
"X-API-Key": process.env.SIM_API_KEY,
"Content-Type": "application/json"
${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json"
},
body: JSON.stringify(${JSON.stringify(payload)})
});
Expand All @@ -166,12 +164,12 @@ console.log(data);`
if (!info) return ''
const endpoint = getBaseEndpoint()
const payload = getStreamPayloadObject()
const isPublic = info.isPublicApi

switch (language) {
case 'curl':
return `curl -X POST \\
-H "X-API-Key: $SIM_API_KEY" \\
-H "Content-Type: application/json" \\
${isPublic ? '' : ' -H "X-API-Key: $SIM_API_KEY" \\\n'} -H "Content-Type: application/json" \\
-d '${JSON.stringify(payload)}' \\
${endpoint}`

Expand All @@ -182,8 +180,7 @@ import requests
response = requests.post(
"${endpoint}",
headers={
"X-API-Key": os.environ.get("SIM_API_KEY"),
"Content-Type": "application/json"
${isPublic ? '' : ' "X-API-Key": os.environ.get("SIM_API_KEY"),\n'} "Content-Type": "application/json"
},
json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')},
stream=True
Expand All @@ -197,8 +194,7 @@ for line in response.iter_lines():
return `const response = await fetch("${endpoint}", {
method: "POST",
headers: {
"X-API-Key": process.env.SIM_API_KEY,
"Content-Type": "application/json"
${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json"
},
body: JSON.stringify(${JSON.stringify(payload)})
});
Expand All @@ -216,8 +212,7 @@ while (true) {
return `const response = await fetch("${endpoint}", {
method: "POST",
headers: {
"X-API-Key": process.env.SIM_API_KEY,
"Content-Type": "application/json"
${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json"
},
body: JSON.stringify(${JSON.stringify(payload)})
});
Expand All @@ -241,14 +236,14 @@ while (true) {
const endpoint = getBaseEndpoint()
const baseUrl = endpoint.split('/api/workflows/')[0]
const payload = getPayloadObject()
const isPublic = info.isPublicApi

switch (asyncExampleType) {
case 'execute':
switch (language) {
case 'curl':
return `curl -X POST \\
-H "X-API-Key: $SIM_API_KEY" \\
-H "Content-Type: application/json" \\
${isPublic ? '' : ' -H "X-API-Key: $SIM_API_KEY" \\\n'} -H "Content-Type: application/json" \\
-H "X-Execution-Mode: async" \\
-d '${JSON.stringify(payload)}' \\
${endpoint}`
Expand All @@ -260,8 +255,7 @@ import requests
response = requests.post(
"${endpoint}",
headers={
"X-API-Key": os.environ.get("SIM_API_KEY"),
"Content-Type": "application/json",
${isPublic ? '' : ' "X-API-Key": os.environ.get("SIM_API_KEY"),\n'} "Content-Type": "application/json",
"X-Execution-Mode": "async"
},
json=${JSON.stringify(payload, null, 4).replace(/\n/g, '\n ')}
Expand All @@ -274,8 +268,7 @@ print(job) # Contains jobId and executionId`
return `const response = await fetch("${endpoint}", {
method: "POST",
headers: {
"X-API-Key": process.env.SIM_API_KEY,
"Content-Type": "application/json",
${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json",
"X-Execution-Mode": "async"
},
body: JSON.stringify(${JSON.stringify(payload)})
Expand All @@ -288,8 +281,7 @@ console.log(job); // Contains jobId and executionId`
return `const response = await fetch("${endpoint}", {
method: "POST",
headers: {
"X-API-Key": process.env.SIM_API_KEY,
"Content-Type": "application/json",
${isPublic ? '' : ' "X-API-Key": process.env.SIM_API_KEY,\n'} "Content-Type": "application/json",
"X-Execution-Mode": "async"
},
body: JSON.stringify(${JSON.stringify(payload)})
Expand Down
Loading