Skip to content
Merged
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
122 changes: 31 additions & 91 deletions apps/sim/lib/workflows/sanitization/json-sanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { Edge } from 'reactflow'
import { sanitizeWorkflowForSharing } from '@/lib/workflows/credentials/credential-extractor'
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
import { TRIGGER_PERSISTED_SUBBLOCK_IDS } from '@/triggers/constants'

/**
* Sanitized workflow state for copilot (removes all UI-specific data)
Expand Down Expand Up @@ -65,41 +64,6 @@ export interface ExportWorkflowState {
}
}

/**
* Check if a subblock contains sensitive/secret data
*/
function isSensitiveSubBlock(key: string, subBlock: BlockState['subBlocks'][string]): boolean {
if (TRIGGER_PERSISTED_SUBBLOCK_IDS.includes(key)) {
return false
}

// Check if it's an OAuth input type
if (subBlock.type === 'oauth-input') {
return true
}

// Check if the field name suggests it contains sensitive data
const sensitivePattern = /credential|oauth|api[_-]?key|token|secret|auth|password|bearer/i
if (sensitivePattern.test(key)) {
return true
}

// Check if the value itself looks like a secret (but not environment variable references)
if (typeof subBlock.value === 'string' && subBlock.value.length > 0) {
// Don't sanitize environment variable references like {{VAR_NAME}}
if (subBlock.value.startsWith('{{') && subBlock.value.endsWith('}}')) {
return false
}

// If it matches sensitive patterns in the value, it's likely a hardcoded secret
if (sensitivePattern.test(subBlock.value)) {
return true
}
}

return false
}

/**
* Sanitize condition blocks by removing UI-specific metadata
* Returns cleaned JSON string (not parsed array)
Expand Down Expand Up @@ -171,86 +135,62 @@ function sanitizeTools(tools: any[]): any[] {
}

/**
* Sanitize subblocks by removing null values, secrets, and simplifying structure
* Sort object keys recursively for consistent comparison
*/
function sortKeysRecursively(item: any): any {
if (Array.isArray(item)) {
return item.map(sortKeysRecursively)
}
if (item !== null && typeof item === 'object') {
return Object.keys(item)
.sort()
.reduce((result: any, key: string) => {
result[key] = sortKeysRecursively(item[key])
return result
}, {})
}
return item
}

/**
* Sanitize subblocks by removing null values and simplifying structure
* Maps each subblock key directly to its value instead of the full object
* Note: responseFormat is kept as an object for better copilot understanding
*/
function sanitizeSubBlocks(
subBlocks: BlockState['subBlocks']
): Record<string, string | number | string[][] | object> {
const sanitized: Record<string, string | number | string[][] | object> = {}

Object.entries(subBlocks).forEach(([key, subBlock]) => {
// Special handling for responseFormat - process BEFORE null check
// so we can detect when it's added/removed
// Skip null/undefined values
if (subBlock.value === null || subBlock.value === undefined) {
return
}

// Normalize responseFormat for consistent key ordering (important for training data)
if (key === 'responseFormat') {
try {
// Handle null/undefined - skip if no value
if (subBlock.value === null || subBlock.value === undefined) {
return
}

let obj = subBlock.value

// Handle string values - parse them first
// Parse JSON string if needed
if (typeof subBlock.value === 'string') {
const trimmed = subBlock.value.trim()
if (!trimmed) {
// Empty string - skip this field
return
}
obj = JSON.parse(trimmed)
}

// Handle object values - normalize keys and keep as object for copilot
// Sort keys for consistent comparison
if (obj && typeof obj === 'object') {
// Sort keys recursively for consistent comparison
const sortKeys = (item: any): any => {
if (Array.isArray(item)) {
return item.map(sortKeys)
}
if (item !== null && typeof item === 'object') {
return Object.keys(item)
.sort()
.reduce((result: any, key: string) => {
result[key] = sortKeys(item[key])
return result
}, {})
}
return item
}

// Keep as object (not stringified) for better copilot understanding
const normalized = sortKeys(obj)
sanitized[key] = normalized
sanitized[key] = sortKeysRecursively(obj)
return
}

// If we get here, obj is not an object (maybe null or primitive) - skip it
return
} catch (error) {
// Invalid JSON - skip this field to avoid crashes
return
}
}

// Skip null/undefined values for other fields
if (subBlock.value === null || subBlock.value === undefined) {
return
}

// For sensitive fields, either omit or replace with placeholder
if (isSensitiveSubBlock(key, subBlock)) {
// If it's an environment variable reference, keep it
if (
typeof subBlock.value === 'string' &&
subBlock.value.startsWith('{{') &&
subBlock.value.endsWith('}}')
) {
} catch {
// Invalid JSON - pass through as-is
sanitized[key] = subBlock.value
return
}
// Otherwise omit the sensitive value entirely
return
}

// Special handling for condition-input type - clean UI metadata
Expand Down