Skip to content
Open
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
20 changes: 5 additions & 15 deletions apps/sim/app/api/v2/workflows/[id]/deploy/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const mocks = vi.hoisted(() => ({
}))

vi.mock('@/lib/api/server/routes', () => ({
createInternalSessionOrExecutorAuth: vi.fn(() => ({ kind: 'internal-workflow' })),
defineV2JsonRoute: mocks.defineRoute,
v2ApiKeyAuth: { kind: 'v2-api-key' },
v2RateLimits: { publicApi: { kind: 'public-api' } },
Expand Down Expand Up @@ -39,6 +40,7 @@ describe('/api/v2/workflows/[id]/deploy route definitions', () => {
workflowId: 'workflow-1',
name: undefined,
description: undefined,
analytics: 'human',
})
)

Expand Down Expand Up @@ -78,21 +80,9 @@ describe('/api/v2/workflows/[id]/deploy route definitions', () => {
expect(v2DeployWorkflowContract.response.schema.parse(body)).toEqual(body)
})

it('keeps product analytics on the v2 adapter', async () => {
const result = { workflowId: 'workflow-1', workspaceId: 'workspace-1' }
await Reflect.get(
POST,
'onSuccess'
)({
principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' },
result,
})
expect(mocks.capture).toHaveBeenCalledWith(
'user-1',
'workflow_deployed',
{ workflow_id: 'workflow-1', workspace_id: 'workspace-1' },
expect.objectContaining({ groups: { workspace: 'workspace-1' } })
)
it('defers deploy analytics to durable activation', () => {
expect(Reflect.get(POST, 'onSuccess')).toBeUndefined()
expect(mocks.capture).not.toHaveBeenCalled()
})

it('keeps undeploy on the authorized operation and declared response schema', () => {
Expand Down
15 changes: 1 addition & 14 deletions apps/sim/app/api/v2/workflows/[id]/deploy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const POST = defineV2JsonRoute({
name: body.name,
description: body.description ?? undefined,
requestId: generateRequestId(),
analytics: 'human' as const,
}),
useCase: deployWorkflow,
present: (result) => ({
Expand All @@ -43,20 +44,6 @@ export const POST = defineV2JsonRoute({
latestDeploymentAttempt: result.latestDeploymentAttempt ?? null,
},
}),
onSuccess: ({ principal, result }) => {
if (principal.kind !== 'personal_api_key') {
throw new Error('Admin deployment unexpectedly admitted a workspace API key')
}
captureServerEvent(
principal.userId,
'workflow_deployed',
{ workflow_id: result.workflowId, workspace_id: result.workspaceId },
{
groups: { workspace: result.workspaceId },
setOnce: { first_workflow_deployed_at: new Date().toISOString() },
}
)
},
})

export const DELETE = defineV2JsonRoute({
Expand Down
19 changes: 19 additions & 0 deletions apps/sim/app/api/v2/workflows/[id]/execute/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,9 @@ describe('POST /api/v2/workflows/[id]/execute', () => {

const okRes = await callPublicExecute({ input: {} })
expect(okRes.status).toBe(200)
expect(mockCheckPreAuthRate.mock.invocationCallOrder[0]).toBeLessThan(
dbChainMockFns.select.mock.invocationCallOrder[0]
)
expect(mockAuthenticateV2ApiKey).not.toHaveBeenCalled()
expect(mockCheckOperationRate).not.toHaveBeenCalled()
expect(mockPreprocessExecution).toHaveBeenCalledWith(
Expand All @@ -506,6 +509,22 @@ describe('POST /api/v2/workflows/[id]/execute', () => {
expect(asyncRes.status).toBe(400)
})

it('rejects anonymous abuse before looking up the workflow', async () => {
mockCheckPreAuthRate.mockResolvedValueOnce({
allowed: false,
remaining: 0,
resetAt: new Date('2026-08-08T05:00:00Z'),
retryAfterMs: 10_000,
})

const response = await callPublicExecute({ input: {} })

expect(response.status).toBe(429)
expect(dbChainMockFns.select).not.toHaveBeenCalled()
expect(mockValidatePublicApiAllowed).not.toHaveBeenCalled()
expect(mockAuthenticateV2ApiKey).not.toHaveBeenCalled()
})

it('401s non-public workflows without a key', async () => {
dbChainMockFns.limit.mockResolvedValueOnce([
{ isPublicApi: false, isDeployed: true, userId: 'owner-1', workspaceId: 'workspace-1' },
Expand Down
19 changes: 10 additions & 9 deletions apps/sim/app/api/v2/workflows/[id]/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '@/lib/api/contracts/v2/workflows'
import { parseRequest } from '@/lib/api/server'
import {
admitV2Request,
admitOptionalV2Request,
V2RouteInfrastructureError,
v2ApiKeyAuth,
v2RateLimits,
Expand Down Expand Up @@ -112,14 +112,15 @@ export const POST = withRouteHandler(
let isPublicApiAccess = false
let apiKeyPrincipal: V2ApiKeyPrincipal | undefined

if (req.headers.has('x-api-key')) {
const admission = await admitV2Request(
req,
workflowOperations.execute,
v2ApiKeyAuth,
v2RateLimits.publicApi
)
if (!admission.success) return admission.response
const admission = await admitOptionalV2Request(
req,
workflowOperations.execute,
v2ApiKeyAuth,
v2RateLimits.publicApi
)
if (!admission.success) return admission.response

if (admission.auth) {
apiKeyPrincipal = admission.auth.principal
userId = admission.auth.rolloutUserId
} else {
Expand Down
20 changes: 4 additions & 16 deletions apps/sim/app/api/v2/workflows/[id]/rollback/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ import { describe, expect, it, vi } from 'vitest'

const mocks = vi.hoisted(() => ({
defineRoute: vi.fn((definition) => definition),
capture: vi.fn(),
}))

vi.mock('@/lib/api/server/routes', () => ({
createInternalSessionOrExecutorAuth: vi.fn(() => ({ kind: 'internal-workflow' })),
defineV2JsonRoute: mocks.defineRoute,
v2ApiKeyAuth: { kind: 'v2-api-key' },
v2RateLimits: { publicApi: { kind: 'public-api' } },
v2OrchestrationErrorPolicy: { kind: 'orchestration-errors' },
}))
vi.mock('@/lib/posthog/server', () => ({ captureServerEvent: mocks.capture }))

import { v2RollbackWorkflowContract } from '@/lib/api/contracts/v2/workflows'
import { v2WorkflowErrorPolicies } from '@/lib/workflows/api'
Expand All @@ -36,6 +35,7 @@ describe('/api/v2/workflows/[id]/rollback route definition', () => {
workflowId: 'workflow-1',
version: undefined,
transition: 'rollback',
analytics: 'human',
})
)

Expand Down Expand Up @@ -75,19 +75,7 @@ describe('/api/v2/workflows/[id]/rollback route definition', () => {
expect(v2RollbackWorkflowContract.response.schema.parse(body)).toEqual(body)
})

it('keeps activation analytics on the v2 adapter', async () => {
await Reflect.get(
POST,
'onSuccess'
)({
principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' },
result: { workflowId: 'workflow-1', workspaceId: 'workspace-1', version: 1 },
})
expect(mocks.capture).toHaveBeenCalledWith(
'user-1',
'deployment_version_activated',
{ workflow_id: 'workflow-1', workspace_id: 'workspace-1', version: 1 },
{ groups: { workspace: 'workspace-1' } }
)
it('defers activation analytics to durable activation', () => {
expect(Reflect.get(POST, 'onSuccess')).toBeUndefined()
})
})
17 changes: 1 addition & 16 deletions apps/sim/app/api/v2/workflows/[id]/rollback/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { v2RollbackWorkflowContract } from '@/lib/api/contracts/v2/workflows'
import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes'
import { generateRequestId } from '@/lib/core/utils/request'
import { captureServerEvent } from '@/lib/posthog/server'
import { v2WorkflowErrorPolicies } from '@/lib/workflows/api'
import { activateWorkflowVersion } from '@/lib/workflows/application/deployments'
import { workflowOperations } from '@/lib/workflows/application/operations'
Expand All @@ -27,6 +26,7 @@ export const POST = defineV2JsonRoute({
version: body.version,
transition: 'rollback' as const,
requestId: generateRequestId(),
analytics: 'human' as const,
}),
useCase: activateWorkflowVersion,
present: (result) => ({
Expand All @@ -40,19 +40,4 @@ export const POST = defineV2JsonRoute({
latestDeploymentAttempt: result.latestDeploymentAttempt ?? null,
},
}),
onSuccess: ({ principal, result }) => {
if (principal.kind !== 'personal_api_key') {
throw new Error('Admin activation unexpectedly admitted a workspace API key')
}
captureServerEvent(
principal.userId,
'deployment_version_activated',
{
workflow_id: result.workflowId,
workspace_id: result.workspaceId,
version: result.version,
},
{ groups: { workspace: result.workspaceId } }
)
},
})
Loading