chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
import {
|
||||
buildCanonicalIndex,
|
||||
buildSubBlockValues,
|
||||
evaluateSubBlockCondition,
|
||||
hasAdvancedValues,
|
||||
isSubBlockFeatureEnabled,
|
||||
isSubBlockVisibleForMode,
|
||||
type SubBlockCondition,
|
||||
} from '@/lib/workflows/subblocks/visibility'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { BlockState, SubBlockState, WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
// Credential types based on actual patterns in the codebase
|
||||
enum CredentialType {
|
||||
OAUTH = 'oauth',
|
||||
SECRET = 'secret', // password: true (covers API keys, bot tokens, passwords, etc.)
|
||||
}
|
||||
|
||||
// Type for credential requirement
|
||||
export interface CredentialRequirement {
|
||||
type: CredentialType
|
||||
serviceId?: string // For OAuth (e.g., 'google-drive', 'slack')
|
||||
label: string // Human-readable label
|
||||
blockType: string // The block type that requires this
|
||||
subBlockId: string // The subblock ID for reference
|
||||
required: boolean
|
||||
}
|
||||
|
||||
// Workspace-specific subblock types that should be cleared
|
||||
const WORKSPACE_SPECIFIC_TYPES = new Set([
|
||||
'knowledge-base-selector',
|
||||
'knowledge-tag-filters',
|
||||
'document-selector',
|
||||
'document-tag-entry',
|
||||
'file-selector', // Workspace files
|
||||
'file-upload', // Uploaded files in workspace
|
||||
'project-selector', // Workspace-specific projects
|
||||
'channel-selector', // Workspace-specific channels
|
||||
'folder-selector', // User-specific folders
|
||||
'mcp-server-selector', // User-specific MCP servers
|
||||
])
|
||||
|
||||
// Field IDs that are workspace-specific
|
||||
const WORKSPACE_SPECIFIC_FIELDS = new Set([
|
||||
'knowledgeBaseId',
|
||||
'tagFilters',
|
||||
'documentTags',
|
||||
'documentId',
|
||||
'fileId',
|
||||
'projectId',
|
||||
'channelId',
|
||||
'folderId',
|
||||
])
|
||||
|
||||
/**
|
||||
* Extract required credentials from a workflow state
|
||||
* This analyzes all blocks and their subblocks to identify credential requirements
|
||||
*/
|
||||
export function extractRequiredCredentials(
|
||||
state: Partial<WorkflowState> | null | undefined
|
||||
): CredentialRequirement[] {
|
||||
const credentials: CredentialRequirement[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
if (!state?.blocks) {
|
||||
return credentials
|
||||
}
|
||||
|
||||
// Process each block
|
||||
Object.values(state.blocks).forEach((block: BlockState) => {
|
||||
if (!block?.type) return
|
||||
|
||||
const blockConfig = getBlock(block.type)
|
||||
if (!blockConfig) return
|
||||
|
||||
// Add OAuth credential if block has OAuth auth mode
|
||||
if (blockConfig.authMode === AuthMode.OAuth) {
|
||||
const blockName = blockConfig.name || block.type
|
||||
const key = `oauth-${block.type}`
|
||||
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key)
|
||||
credentials.push({
|
||||
type: CredentialType.OAUTH,
|
||||
serviceId: block.type,
|
||||
label: `Credential for ${blockName}`,
|
||||
blockType: block.type,
|
||||
subBlockId: 'oauth',
|
||||
required: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Process password fields (API keys, tokens, etc)
|
||||
blockConfig.subBlocks?.forEach((subBlockConfig: SubBlockConfig) => {
|
||||
if (!isSubBlockVisible(block, subBlockConfig)) return
|
||||
if (!subBlockConfig.password) return
|
||||
|
||||
const blockName = blockConfig.name || block.type
|
||||
const suffix = block?.triggerMode ? ' Trigger' : ''
|
||||
const fieldLabel = subBlockConfig.title || formatFieldName(subBlockConfig.id)
|
||||
const key = `secret-${block.type}-${subBlockConfig.id}-${block?.triggerMode ? 'trigger' : 'default'}`
|
||||
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key)
|
||||
credentials.push({
|
||||
type: CredentialType.SECRET,
|
||||
label: `${fieldLabel} for ${blockName}${suffix}`,
|
||||
blockType: block.type,
|
||||
subBlockId: subBlockConfig.id,
|
||||
required: subBlockConfig.required !== false,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/** Helper to check visibility, respecting mode and conditions */
|
||||
function isSubBlockVisible(block: BlockState, subBlockConfig: SubBlockConfig): boolean {
|
||||
if (!isSubBlockFeatureEnabled(subBlockConfig)) return false
|
||||
|
||||
const values = buildSubBlockValues(block?.subBlocks || {})
|
||||
const blockConfig = getBlock(block.type)
|
||||
const blockSubBlocks = blockConfig?.subBlocks || []
|
||||
const canonicalIndex = buildCanonicalIndex(blockSubBlocks)
|
||||
const effectiveAdvanced =
|
||||
(block?.advancedMode ?? false) || hasAdvancedValues(blockSubBlocks, values, canonicalIndex)
|
||||
const canonicalModeOverrides = block.data?.canonicalModes
|
||||
|
||||
if (subBlockConfig.mode === 'trigger' && !block?.triggerMode) return false
|
||||
if (block?.triggerMode && subBlockConfig.mode && subBlockConfig.mode !== 'trigger') return false
|
||||
|
||||
if (
|
||||
!isSubBlockVisibleForMode(
|
||||
subBlockConfig,
|
||||
effectiveAdvanced,
|
||||
canonicalIndex,
|
||||
values,
|
||||
canonicalModeOverrides
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return evaluateSubBlockCondition(subBlockConfig.condition as SubBlockCondition, values)
|
||||
}
|
||||
|
||||
// Sort: OAuth first, then secrets, alphabetically within each type
|
||||
credentials.sort((a, b) => {
|
||||
if (a.type !== b.type) {
|
||||
return a.type === CredentialType.OAUTH ? -1 : 1
|
||||
}
|
||||
return a.label.localeCompare(b.label)
|
||||
})
|
||||
|
||||
return credentials
|
||||
}
|
||||
|
||||
/**
|
||||
* Format field name to be human-readable
|
||||
*/
|
||||
function formatFieldName(fieldName: string): string {
|
||||
return fieldName
|
||||
.replace(/[_-]/g, ' ')
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
/** Block state with mutable subBlocks for sanitization */
|
||||
interface MutableBlockState extends Omit<BlockState, 'subBlocks'> {
|
||||
subBlocks: Record<string, SubBlockState | null | undefined>
|
||||
data?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove malformed subBlocks from a block that may have been created by bugs.
|
||||
* This includes subBlocks with:
|
||||
* - Key "undefined" (caused by assigning to undefined key)
|
||||
* - Missing required `id` field
|
||||
* - Type "unknown" (indicates malformed data)
|
||||
*/
|
||||
function removeMalformedSubBlocks(block: MutableBlockState): void {
|
||||
if (!block.subBlocks) return
|
||||
|
||||
const keysToRemove: string[] = []
|
||||
|
||||
Object.entries(block.subBlocks).forEach(([key, subBlock]) => {
|
||||
// Flag subBlocks with invalid keys (literal "undefined" string)
|
||||
if (key === 'undefined') {
|
||||
keysToRemove.push(key)
|
||||
return
|
||||
}
|
||||
|
||||
// Flag subBlocks that are null or not objects
|
||||
if (!subBlock || typeof subBlock !== 'object') {
|
||||
keysToRemove.push(key)
|
||||
return
|
||||
}
|
||||
|
||||
// Flag subBlocks with type "unknown" (malformed data)
|
||||
// Cast to string for comparison since SubBlockType doesn't include 'unknown'
|
||||
if ((subBlock.type as string) === 'unknown') {
|
||||
keysToRemove.push(key)
|
||||
return
|
||||
}
|
||||
|
||||
// Flag subBlocks missing required id field
|
||||
if (!subBlock.id) {
|
||||
keysToRemove.push(key)
|
||||
}
|
||||
})
|
||||
|
||||
// Remove the flagged keys
|
||||
keysToRemove.forEach((key) => {
|
||||
delete block.subBlocks[key]
|
||||
})
|
||||
}
|
||||
|
||||
/** Sanitized workflow state structure */
|
||||
interface SanitizedWorkflowState {
|
||||
blocks?: Record<string, MutableBlockState>
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize workflow state by removing all credentials and workspace-specific data
|
||||
* This is used for both template creation and workflow export to ensure consistency
|
||||
*
|
||||
* @param state - The workflow state to sanitize
|
||||
* @param options - Options for sanitization behavior
|
||||
*/
|
||||
export function sanitizeWorkflowForSharing(
|
||||
state: Partial<WorkflowState> | null | undefined,
|
||||
options: {
|
||||
preserveEnvVars?: boolean // Keep {{VAR}} references for export
|
||||
} = {}
|
||||
): SanitizedWorkflowState {
|
||||
const sanitized = structuredClone(state) as SanitizedWorkflowState
|
||||
|
||||
if (!sanitized?.blocks) {
|
||||
return sanitized
|
||||
}
|
||||
|
||||
Object.values(sanitized.blocks).forEach((block: MutableBlockState) => {
|
||||
if (!block?.type) return
|
||||
|
||||
// First, remove any malformed subBlocks that may have been created by bugs
|
||||
removeMalformedSubBlocks(block)
|
||||
|
||||
const blockConfig = getBlock(block.type)
|
||||
|
||||
// Process subBlocks with config
|
||||
if (blockConfig) {
|
||||
blockConfig.subBlocks?.forEach((subBlockConfig: SubBlockConfig) => {
|
||||
if (block.subBlocks?.[subBlockConfig.id]) {
|
||||
const subBlock = block.subBlocks[subBlockConfig.id]
|
||||
|
||||
// Clear OAuth credentials (type: 'oauth-input')
|
||||
if (subBlockConfig.type === 'oauth-input') {
|
||||
block.subBlocks[subBlockConfig.id]!.value = null
|
||||
}
|
||||
|
||||
// Clear secret fields (password: true)
|
||||
else if (subBlockConfig.password === true) {
|
||||
// Preserve environment variable references if requested
|
||||
if (
|
||||
options.preserveEnvVars &&
|
||||
typeof subBlock?.value === 'string' &&
|
||||
subBlock.value.startsWith('{{') &&
|
||||
subBlock.value.endsWith('}}')
|
||||
) {
|
||||
// Keep the env var reference
|
||||
} else {
|
||||
block.subBlocks[subBlockConfig.id]!.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// Clear workspace-specific selectors
|
||||
else if (WORKSPACE_SPECIFIC_TYPES.has(subBlockConfig.type)) {
|
||||
block.subBlocks[subBlockConfig.id]!.value = null
|
||||
}
|
||||
|
||||
// Clear workspace-specific fields by ID
|
||||
else if (WORKSPACE_SPECIFIC_FIELDS.has(subBlockConfig.id)) {
|
||||
block.subBlocks[subBlockConfig.id]!.value = null
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Process subBlocks without config (fallback)
|
||||
if (block.subBlocks) {
|
||||
Object.entries(block.subBlocks).forEach(([key, subBlock]) => {
|
||||
// Clear workspace-specific fields by key name
|
||||
if (WORKSPACE_SPECIFIC_FIELDS.has(key) && subBlock) {
|
||||
subBlock.value = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Clear data field (for backward compatibility)
|
||||
if (block.data) {
|
||||
Object.entries(block.data).forEach(([key]) => {
|
||||
// Clear anything that looks like credentials
|
||||
if (/credential|oauth|api[_-]?key|token|secret|auth|password|bearer/i.test(key)) {
|
||||
block.data![key] = null
|
||||
}
|
||||
// Clear workspace-specific data
|
||||
if (WORKSPACE_SPECIFIC_FIELDS.has(key)) {
|
||||
block.data![key] = null
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize workflow state for templates (removes credentials and workspace data)
|
||||
* Wrapper for backward compatibility
|
||||
*/
|
||||
export function sanitizeCredentials(
|
||||
state: Partial<WorkflowState> | null | undefined
|
||||
): SanitizedWorkflowState {
|
||||
return sanitizeWorkflowForSharing(state, { preserveEnvVars: false })
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize workflow state for export (preserves env vars)
|
||||
* Convenience wrapper for workflow export
|
||||
*/
|
||||
export function sanitizeForExport(
|
||||
state: Partial<WorkflowState> | null | undefined
|
||||
): SanitizedWorkflowState {
|
||||
return sanitizeWorkflowForSharing(state, { preserveEnvVars: true })
|
||||
}
|
||||
Reference in New Issue
Block a user