@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44import { and , eq , inArray , or , sql } from 'drizzle-orm'
55import { drizzle } from 'drizzle-orm/postgres-js'
66import postgres from 'postgres'
7+ import { AuditAction , AuditResourceType , recordAudit } from '@/lib/audit/log'
78import { env } from '@/lib/core/config/env'
89import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions'
910import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
@@ -207,6 +208,17 @@ export async function persistWorkflowOperation(workflowId: string, operation: an
207208 }
208209 } )
209210
211+ // Audit workflow-level lock/unlock operations
212+ if (
213+ target === OPERATION_TARGETS . BLOCKS &&
214+ op === BLOCKS_OPERATIONS . BATCH_TOGGLE_LOCKED &&
215+ userId
216+ ) {
217+ auditWorkflowLockToggle ( workflowId , userId ) . catch ( ( error ) => {
218+ logger . error ( 'Failed to audit workflow lock toggle' , { error, workflowId } )
219+ } )
220+ }
221+
210222 const duration = Date . now ( ) - startTime
211223 if ( duration > 100 ) {
212224 logger . warn ( 'Slow socket DB operation:' , {
@@ -226,6 +238,45 @@ export async function persistWorkflowOperation(workflowId: string, operation: an
226238 }
227239}
228240
241+ /**
242+ * Records an audit log entry when all blocks in a workflow are locked or unlocked.
243+ * Only audits workflow-level transitions (all locked or all unlocked), not partial toggles.
244+ */
245+ async function auditWorkflowLockToggle ( workflowId : string , actorId : string ) : Promise < void > {
246+ const [ wf ] = await db
247+ . select ( { name : workflow . name , workspaceId : workflow . workspaceId } )
248+ . from ( workflow )
249+ . where ( eq ( workflow . id , workflowId ) )
250+
251+ if ( ! wf ) return
252+
253+ const blocks = await db
254+ . select ( { locked : workflowBlocks . locked } )
255+ . from ( workflowBlocks )
256+ . where ( eq ( workflowBlocks . workflowId , workflowId ) )
257+
258+ if ( blocks . length === 0 ) return
259+
260+ const allLocked = blocks . every ( ( b ) => b . locked )
261+ const allUnlocked = blocks . every ( ( b ) => ! b . locked )
262+
263+ // Only audit workflow-level transitions, not partial toggles
264+ if ( ! allLocked && ! allUnlocked ) return
265+
266+ recordAudit ( {
267+ workspaceId : wf . workspaceId ,
268+ actorId,
269+ action : allLocked ? AuditAction . WORKFLOW_LOCKED : AuditAction . WORKFLOW_UNLOCKED ,
270+ resourceType : AuditResourceType . WORKFLOW ,
271+ resourceId : workflowId ,
272+ resourceName : wf . name ,
273+ description : allLocked
274+ ? `Locked workflow "${ wf . name } "`
275+ : `Unlocked workflow "${ wf . name } "` ,
276+ metadata : { blockCount : blocks . length } ,
277+ } )
278+ }
279+
229280async function handleBlockOperationTx (
230281 tx : any ,
231282 workflowId : string ,
0 commit comments