Files
simstudioai--sim/apps/sim/lib/workflows/credentials/credential-extractor.ts
T
wehub-resource-sync d25d482dc2
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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

341 lines
10 KiB
TypeScript

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 })
}