Skip to content

Commit 04a6f9d

Browse files
committed
Credential masking
1 parent 76dd4a0 commit 04a6f9d

File tree

4 files changed

+126
-8
lines changed

4 files changed

+126
-8
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
7878
mode,
7979
setMode,
8080
isAborting,
81+
maskCredentialValue,
8182
} = useCopilotStore()
8283

8384
const messageCheckpoints = isUser ? allMessageCheckpoints[message.id] || [] : []
@@ -210,7 +211,10 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
210211
const isLastTextBlock =
211212
index === message.contentBlocks!.length - 1 && block.type === 'text'
212213
const parsed = parseSpecialTags(block.content)
213-
const cleanBlockContent = parsed.cleanContent.replace(/\n{3,}/g, '\n\n')
214+
// Mask credential IDs in the displayed content
215+
const cleanBlockContent = maskCredentialValue(
216+
parsed.cleanContent.replace(/\n{3,}/g, '\n\n')
217+
)
214218

215219
if (!cleanBlockContent.trim()) return null
216220

@@ -238,7 +242,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
238242
return (
239243
<div key={blockKey} className='w-full'>
240244
<ThinkingBlock
241-
content={block.content}
245+
content={maskCredentialValue(block.content)}
242246
isStreaming={isActivelyStreaming}
243247
hasFollowingContent={hasFollowingContent}
244248
hasSpecialTags={hasSpecialTags}
@@ -261,7 +265,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
261265
}
262266
return null
263267
})
264-
}, [message.contentBlocks, isActivelyStreaming, parsedTags, isLastMessage])
268+
}, [message.contentBlocks, isActivelyStreaming, parsedTags, isLastMessage, maskCredentialValue])
265269

266270
if (isUser) {
267271
return (

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,7 @@ const SubagentContentRenderer = memo(function SubagentContentRenderer({
782782
const [isExpanded, setIsExpanded] = useState(true)
783783
const [duration, setDuration] = useState(0)
784784
const startTimeRef = useRef<number>(Date.now())
785+
const maskCredentialValue = useCopilotStore((s) => s.maskCredentialValue)
785786
const wasStreamingRef = useRef(false)
786787

787788
// Only show streaming animations for current message
@@ -816,14 +817,16 @@ const SubagentContentRenderer = memo(function SubagentContentRenderer({
816817
currentText += parsed.cleanContent
817818
} else if (block.type === 'subagent_tool_call' && block.toolCall) {
818819
if (currentText.trim()) {
819-
segments.push({ type: 'text', content: currentText })
820+
// Mask any credential IDs in the accumulated text before displaying
821+
segments.push({ type: 'text', content: maskCredentialValue(currentText) })
820822
currentText = ''
821823
}
822824
segments.push({ type: 'tool', block })
823825
}
824826
}
825827
if (currentText.trim()) {
826-
segments.push({ type: 'text', content: currentText })
828+
// Mask any credential IDs in the accumulated text before displaying
829+
segments.push({ type: 'text', content: maskCredentialValue(currentText) })
827830
}
828831

829832
const allParsed = parseSpecialTags(allRawText)
@@ -952,6 +955,7 @@ const WorkflowEditSummary = memo(function WorkflowEditSummary({
952955
toolCall: CopilotToolCall
953956
}) {
954957
const blocks = useWorkflowStore((s) => s.blocks)
958+
const maskCredentialValue = useCopilotStore((s) => s.maskCredentialValue)
955959

956960
const cachedBlockInfoRef = useRef<Record<string, { name: string; type: string }>>({})
957961

@@ -983,6 +987,7 @@ const WorkflowEditSummary = memo(function WorkflowEditSummary({
983987
title: string
984988
value: any
985989
isPassword?: boolean
990+
isCredential?: boolean
986991
}
987992

988993
interface BlockChange {
@@ -1091,6 +1096,7 @@ const WorkflowEditSummary = memo(function WorkflowEditSummary({
10911096
title: subBlockConfig.title ?? subBlockConfig.id,
10921097
value,
10931098
isPassword: subBlockConfig.password === true,
1099+
isCredential: subBlockConfig.type === 'oauth-input',
10941100
})
10951101
}
10961102
}
@@ -1172,8 +1178,15 @@ const WorkflowEditSummary = memo(function WorkflowEditSummary({
11721178
{subBlocksToShow && subBlocksToShow.length > 0 && (
11731179
<div className='border-[var(--border-1)] border-t px-2.5 py-1.5'>
11741180
{subBlocksToShow.map((sb) => {
1175-
// Mask password fields like the canvas does
1176-
const displayValue = sb.isPassword ? '•••' : getDisplayValue(sb.value)
1181+
// Mask password fields and credential IDs
1182+
let displayValue: string
1183+
if (sb.isPassword) {
1184+
displayValue = '•••'
1185+
} else {
1186+
// Get display value first, then mask any credential IDs that might be in it
1187+
const rawValue = getDisplayValue(sb.value)
1188+
displayValue = maskCredentialValue(rawValue)
1189+
}
11771190
return (
11781191
<div key={sb.id} className='flex items-start gap-1.5 py-0.5 text-[11px]'>
11791192
<span

apps/sim/stores/panel/copilot/store.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,12 +771,50 @@ function deepClone<T>(obj: T): T {
771771
}
772772
}
773773

774+
/**
775+
* Recursively masks credential IDs in any value (string, object, or array).
776+
* Used during serialization to ensure sensitive IDs are never persisted.
777+
*/
778+
function maskCredentialIdsInValue(value: any, credentialIds: Set<string>): any {
779+
if (!value || credentialIds.size === 0) return value
780+
781+
if (typeof value === 'string') {
782+
let masked = value
783+
// Sort by length descending to mask longer IDs first
784+
const sortedIds = Array.from(credentialIds).sort((a, b) => b.length - a.length)
785+
for (const id of sortedIds) {
786+
if (id && masked.includes(id)) {
787+
masked = masked.split(id).join('••••••••')
788+
}
789+
}
790+
return masked
791+
}
792+
793+
if (Array.isArray(value)) {
794+
return value.map((item) => maskCredentialIdsInValue(item, credentialIds))
795+
}
796+
797+
if (typeof value === 'object') {
798+
const masked: any = {}
799+
for (const key of Object.keys(value)) {
800+
masked[key] = maskCredentialIdsInValue(value[key], credentialIds)
801+
}
802+
return masked
803+
}
804+
805+
return value
806+
}
807+
774808
/**
775809
* Serializes messages for database storage.
776810
* Deep clones all fields to ensure proper JSON serialization.
811+
* Masks sensitive credential IDs before persisting.
777812
* This ensures they render identically when loaded back.
778813
*/
779814
function serializeMessagesForDB(messages: CopilotMessage[]): any[] {
815+
// Get credential IDs to mask
816+
const credentialIds = useCopilotStore.getState().sensitiveCredentialIds
817+
780818
const result = messages
781819
.map((msg) => {
782820
// Deep clone the entire message to ensure all nested data is serializable
@@ -824,7 +862,8 @@ function serializeMessagesForDB(messages: CopilotMessage[]): any[] {
824862
serialized.errorType = msg.errorType
825863
}
826864

827-
return serialized
865+
// Mask credential IDs in the serialized message before persisting
866+
return maskCredentialIdsInValue(serialized, credentialIds)
828867
})
829868
.filter((msg) => {
830869
// Filter out empty assistant messages
@@ -2294,6 +2333,7 @@ const initialState = {
22942333
autoAllowedTools: [] as string[],
22952334
messageQueue: [] as import('./types').QueuedMessage[],
22962335
suppressAbortContinueOption: false,
2336+
sensitiveCredentialIds: new Set<string>(),
22972337
}
22982338

22992339
export const useCopilotStore = create<CopilotStore>()(
@@ -2676,6 +2716,9 @@ export const useCopilotStore = create<CopilotStore>()(
26762716
}))
26772717
}
26782718

2719+
// Load sensitive credential IDs for masking before streaming starts
2720+
await get().loadSensitiveCredentialIds()
2721+
26792722
let newMessages: CopilotMessage[]
26802723
if (revertState) {
26812724
const currentMessages = get().messages
@@ -3968,6 +4011,57 @@ export const useCopilotStore = create<CopilotStore>()(
39684011
return autoAllowedTools.includes(toolId)
39694012
},
39704013

4014+
// Credential masking
4015+
loadSensitiveCredentialIds: async () => {
4016+
try {
4017+
const res = await fetch('/api/copilot/execute-copilot-server-tool', {
4018+
method: 'POST',
4019+
headers: { 'Content-Type': 'application/json' },
4020+
body: JSON.stringify({ toolName: 'get_credentials', payload: {} }),
4021+
})
4022+
if (!res.ok) {
4023+
logger.warn('[loadSensitiveCredentialIds] Failed to fetch credentials', {
4024+
status: res.status,
4025+
})
4026+
return
4027+
}
4028+
const json = await res.json()
4029+
// Credentials are at result.oauth.connected.credentials
4030+
const credentials = json?.result?.oauth?.connected?.credentials || []
4031+
logger.info('[loadSensitiveCredentialIds] Response', {
4032+
hasResult: !!json?.result,
4033+
credentialCount: credentials.length,
4034+
})
4035+
const ids = new Set<string>()
4036+
for (const cred of credentials) {
4037+
if (cred?.id) {
4038+
ids.add(cred.id)
4039+
}
4040+
}
4041+
set({ sensitiveCredentialIds: ids })
4042+
logger.info('[loadSensitiveCredentialIds] Loaded credential IDs', {
4043+
count: ids.size,
4044+
})
4045+
} catch (err) {
4046+
logger.warn('[loadSensitiveCredentialIds] Error loading credentials', err)
4047+
}
4048+
},
4049+
4050+
maskCredentialValue: (value: string) => {
4051+
const { sensitiveCredentialIds } = get()
4052+
if (!value || sensitiveCredentialIds.size === 0) return value
4053+
4054+
let masked = value
4055+
// Sort by length descending to mask longer IDs first
4056+
const sortedIds = Array.from(sensitiveCredentialIds).sort((a, b) => b.length - a.length)
4057+
for (const id of sortedIds) {
4058+
if (id && masked.includes(id)) {
4059+
masked = masked.split(id).join('••••••••')
4060+
}
4061+
}
4062+
return masked
4063+
},
4064+
39714065
// Message queue actions
39724066
addToQueue: (message, options) => {
39734067
const queuedMessage: import('./types').QueuedMessage = {

apps/sim/stores/panel/copilot/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ export interface CopilotState {
156156

157157
// Message queue for messages sent while another is in progress
158158
messageQueue: QueuedMessage[]
159+
160+
// Credential IDs to mask in UI (for sensitive data protection)
161+
sensitiveCredentialIds: Set<string>
159162
}
160163

161164
export interface CopilotActions {
@@ -235,6 +238,10 @@ export interface CopilotActions {
235238
removeAutoAllowedTool: (toolId: string) => Promise<void>
236239
isToolAutoAllowed: (toolId: string) => boolean
237240

241+
// Credential masking
242+
loadSensitiveCredentialIds: () => Promise<void>
243+
maskCredentialValue: (value: string) => string
244+
238245
// Message queue actions
239246
addToQueue: (
240247
message: string,

0 commit comments

Comments
 (0)