GeezerVison is showing github.com — fix(copilot): redact sim_key API keys from persisted Mothership chat … · simstudioai/sim@ae20d1c
File treeExpand file treeCollapse file tree Open diff view settings app/workspace/[workspaceId] components/message-content/components/special-tags
api-keys/components/create-api-key-modal
components/emcn/components
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +40-24Lines changed: 40 additions & 24 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 2 | 2 |
| 3 | 3 | import { createElement, useMemo, useState } from 'react' | 4 | 4 | import { useParams } from 'next/navigation' | 5 | | -import { ArrowRight, ChevronDown, Expandable, ExpandableContent } from '@/components/emcn' | | 5 | +import { | | 6 | + ArrowRight, | | 7 | + ChevronDown, | | 8 | + Expandable, | | 9 | + ExpandableContent, | | 10 | + SecretReveal, | | 11 | +} from '@/components/emcn' | 6 | 12 | import { cn } from '@/lib/core/utils/cn' | 7 | 13 | import { OAUTH_PROVIDERS } from '@/lib/oauth/oauth' | 8 | 14 | import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon' | @@ -47,9 +53,10 @@ export const CREDENTIAL_TAG_TYPES = [ | 47 | 53 | export type CredentialTagType = (typeof CREDENTIAL_TAG_TYPES)[number] | 48 | 54 |
| 49 | 55 | export interface CredentialTagData { | 50 | | - value: string | | 56 | + value?: string | 51 | 57 | type: CredentialTagType | 52 | 58 | provider?: string | | 59 | + redacted?: boolean | 53 | 60 | } | 54 | 61 |
| 55 | 62 | export interface MothershipErrorTagData { | @@ -140,12 +147,15 @@ function isUsageUpgradeTagData(value: unknown): value is UsageUpgradeTagData { | 140 | 147 |
| 141 | 148 | function isCredentialTagData(value: unknown): value is CredentialTagData { | 142 | 149 | if (!isRecord(value)) return false | 143 | | - return ( | 144 | | - typeof value.value === 'string' && | 145 | | - typeof value.type === 'string' && | 146 | | - (CREDENTIAL_TAG_TYPES as readonly string[]).includes(value.type) && | 147 | | - (value.provider === undefined || typeof value.provider === 'string') | 148 | | - ) | | 150 | + if ( | | 151 | + typeof value.type !== 'string' || | | 152 | + !(CREDENTIAL_TAG_TYPES as readonly string[]).includes(value.type) | | 153 | + ) { | | 154 | + return false | | 155 | + } | | 156 | + if (value.provider !== undefined && typeof value.provider !== 'string') return false | | 157 | + if (value.redacted === true) return value.value === undefined || typeof value.value === 'string' | | 158 | + return typeof value.value === 'string' | 149 | 159 | } | 150 | 160 |
| 151 | 161 | function isMothershipErrorTagData(value: unknown): value is MothershipErrorTagData { | @@ -595,24 +605,30 @@ const LockIcon = (props: { className?: string }) => ( | 595 | 605 | ) | 596 | 606 |
| 597 | 607 | function CredentialDisplay({ data }: { data: CredentialTagData }) { | 598 | | - if (data.type !== 'link' || !data.provider) return null | | 608 | + if (data.type === 'link') { | | 609 | + if (!data.provider) return null | | 610 | + const Icon = getCredentialIcon(data.provider) ?? LockIcon | | 611 | + return ( | | 612 | + <a | | 613 | + href={data.value} | | 614 | + target='_blank' | | 615 | + rel='noopener noreferrer' | | 616 | + className='flex items-center gap-2 rounded-lg border border-[var(--divider)] px-3 py-2.5 transition-colors hover-hover:bg-[var(--surface-5)]' | | 617 | + > | | 618 | + {createElement(Icon, { className: 'h-[16px] w-[16px] shrink-0' })} | | 619 | + <span className='flex-1 font-base text-[var(--text-body)] text-sm'> | | 620 | + Connect {data.provider} | | 621 | + </span> | | 622 | + <ArrowRight className='h-[16px] w-[16px] shrink-0 text-[var(--text-icon)]' /> | | 623 | + </a> | | 624 | + ) | | 625 | + } | 599 | 626 |
| 600 | | - const Icon = getCredentialIcon(data.provider) ?? LockIcon | | 627 | + if (data.type === 'sim_key') { | | 628 | + return <SecretReveal value={data.value} redacted={data.redacted || !data.value} /> | | 629 | + } | 601 | 630 |
| 602 | | - return ( | 603 | | - <a | 604 | | - href={data.value} | 605 | | - target='_blank' | 606 | | - rel='noopener noreferrer' | 607 | | - className='flex items-center gap-2 rounded-lg border border-[var(--divider)] px-3 py-2.5 transition-colors hover-hover:bg-[var(--surface-5)]' | 608 | | - > | 609 | | - {createElement(Icon, { className: 'h-[16px] w-[16px] shrink-0' })} | 610 | | - <span className='flex-1 font-base text-[var(--text-body)] text-sm'> | 611 | | - Connect {data.provider} | 612 | | - </span> | 613 | | - <ArrowRight className='h-[16px] w-[16px] shrink-0 text-[var(--text-icon)]' /> | 614 | | - </a> | 615 | | - ) | | 631 | + return null | 616 | 632 | } | 617 | 633 |
| 618 | 634 | function MothershipErrorDisplay({ data }: { data: MothershipErrorTagData }) { | |
Collapse file Copy file name to clipboardExpand all lines: apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +15-4Lines changed: 15 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -13,6 +13,11 @@ import type { | 13 | 13 | PersistedMessage, | 14 | 14 | } from '@/lib/copilot/chat/persisted-message' | 15 | 15 | import { normalizeMessage, withBlockTiming } from '@/lib/copilot/chat/persisted-message' | | 16 | +import { | | 17 | + captureRevealedSimKeys, | | 18 | + type RevealedSimKeysByMessage, | | 19 | + restoreRevealedSimKeysForMessage, | | 20 | +} from '@/lib/copilot/chat/sim-key-redaction' | 16 | 21 | import { resolveStreamToolOutcome } from '@/lib/copilot/chat/stream-tool-outcome' | 17 | 22 | import { MOTHERSHIP_CHAT_API_PATH, STREAM_STORAGE_KEY } from '@/lib/copilot/constants' | 18 | 23 | import type { | @@ -1063,6 +1068,7 @@ export function useChat( | 1063 | 1068 | ) | 1064 | 1069 | const [genericResourceData, setGenericResourceData] = useState<GenericResourceData | null>(null) | 1065 | 1070 | const onResourceEventRef = useRef(options?.onResourceEvent) | | 1071 | + const revealedSimKeysRef = useRef<RevealedSimKeysByMessage>(new Map()) | 1066 | 1072 | onResourceEventRef.current = options?.onResourceEvent | 1067 | 1073 | const apiPathRef = useRef(options?.apiPath ?? MOTHERSHIP_CHAT_API_PATH) | 1068 | 1074 | apiPathRef.current = options?.apiPath ?? MOTHERSHIP_CHAT_API_PATH | @@ -1384,10 +1390,10 @@ export function useChat( | 1384 | 1390 | }, [clearActiveTurn, clearQueueDispatchState, resetEphemeralPreviewState, setTransportIdle]) | 1385 | 1391 |
| 1386 | 1392 | const { data: chatHistory } = useChatHistory(resolvedChatId) | 1387 | | - const messages = useMemo( | 1388 | | - () => chatHistory?.messages.map(toDisplayMessage) ?? pendingMessages, | 1389 | | - [chatHistory, pendingMessages] | 1390 | | - ) | | 1393 | + const messages = useMemo(() => { | | 1394 | + const source = chatHistory?.messages.map(toDisplayMessage) ?? pendingMessages | | 1395 | + return source.map((m) => restoreRevealedSimKeysForMessage(m, revealedSimKeysRef.current)) | | 1396 | + }, [chatHistory, pendingMessages]) | 1391 | 1397 | const addResource = useCallback((resource: MothershipResource): boolean => { | 1392 | 1398 | if (resourcesRef.current.some((r) => r.type === resource.type && r.id === resource.id)) { | 1393 | 1399 | return false | @@ -1888,6 +1894,11 @@ export function useChat( | 1888 | 1894 | const flush = () => { | 1889 | 1895 | if (isStale()) return | 1890 | 1896 | streamingBlocksRef.current = [...blocks] | | 1897 | + captureRevealedSimKeys( | | 1898 | + revealedSimKeysRef.current, | | 1899 | + [assistantId, streamRequestId], | | 1900 | + runningText | | 1901 | + ) | 1891 | 1902 | const activeChatId = chatIdRef.current | 1892 | 1903 | if (!activeChatId) { | 1893 | 1904 | const snapshot: Partial<ChatMessage> = { | |
Collapse file Copy file name to clipboardExpand all lines: apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx +2-31Lines changed: 2 additions & 31 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 2 | 2 |
| 3 | 3 | import { useState } from 'react' | 4 | 4 | import { createLogger } from '@sim/logger' | 5 | | -import { Check, Copy } from 'lucide-react' | 6 | 5 | import { | 7 | 6 | Button, | 8 | 7 | ButtonGroup, | @@ -13,6 +12,7 @@ import { | 13 | 12 | ModalContent, | 14 | 13 | ModalFooter, | 15 | 14 | ModalHeader, | | 15 | + SecretReveal, | 16 | 16 | } from '@/components/emcn' | 17 | 17 | import { type ApiKey, useCreateApiKey } from '@/hooks/queries/api-keys' | 18 | 18 |
| @@ -50,8 +50,6 @@ export function CreateApiKeyModal({ | 50 | 50 | const [createError, setCreateError] = useState<string | null>(null) | 51 | 51 | const [newKey, setNewKey] = useState<ApiKey | null>(null) | 52 | 52 | const [showNewKeyDialog, setShowNewKeyDialog] = useState(false) | 53 | | - const [copySuccess, setCopySuccess] = useState(false) | 54 | | - | 55 | 53 | const createApiKeyMutation = useCreateApiKey() | 56 | 54 |
| 57 | 55 | const handleCreateKey = async () => { | @@ -105,12 +103,6 @@ export function CreateApiKeyModal({ | 105 | 103 | setCreateError(null) | 106 | 104 | } | 107 | 105 |
| 108 | | - const copyToClipboard = (key: string) => { | 109 | | - navigator.clipboard.writeText(key) | 110 | | - setCopySuccess(true) | 111 | | - setTimeout(() => setCopySuccess(false), 2000) | 112 | | - } | 113 | | - | 114 | 106 | return ( | 115 | 107 | <> | 116 | 108 | {/* Create API Key Dialog */} | @@ -209,7 +201,6 @@ export function CreateApiKeyModal({ | 209 | 201 | setShowNewKeyDialog(dialogOpen) | 210 | 202 | if (!dialogOpen) { | 211 | 203 | setNewKey(null) | 212 | | - setCopySuccess(false) | 213 | 204 | } | 214 | 205 | }} | 215 | 206 | > | @@ -223,27 +214,7 @@ export function CreateApiKeyModal({ | 223 | 214 | </span> | 224 | 215 | </p> | 225 | 216 |
| 226 | | - {newKey && ( | 227 | | - <div className='relative mt-2.5'> | 228 | | - <div className='flex h-9 items-center rounded-md border bg-[var(--surface-1)] px-2.5 pr-10'> | 229 | | - <code className='flex-1 truncate font-mono text-[var(--text-primary)] text-sm'> | 230 | | - {newKey.key} | 231 | | - </code> | 232 | | - </div> | 233 | | - <Button | 234 | | - variant='ghost' | 235 | | - className='-translate-y-1/2 absolute top-1/2 right-[4px] h-[28px] w-[28px] rounded-sm text-[var(--text-muted)] hover-hover:text-[var(--text-primary)]' | 236 | | - onClick={() => copyToClipboard(newKey.key)} | 237 | | - > | 238 | | - {copySuccess ? ( | 239 | | - <Check className='h-[14px] w-[14px]' /> | 240 | | - ) : ( | 241 | | - <Copy className='h-[14px] w-[14px]' /> | 242 | | - )} | 243 | | - <span className='sr-only'>Copy to clipboard</span> | 244 | | - </Button> | 245 | | - </div> | 246 | | - )} | | 217 | + {newKey && <SecretReveal value={newKey.key} className='mt-2.5' />} | 247 | 218 | </ModalBody> | 248 | 219 | </ModalContent> | 249 | 220 | </Modal> | |
Collapse file Copy file name to clipboardExpand all lines: apps/sim/app/workspace/[workspaceId]/settings/components/copilot/copilot.tsx +3-30Lines changed: 3 additions & 30 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -4,7 +4,7 @@ import { useMemo, useState } from 'react' | 4 | 4 | // import { useParams } from 'next/navigation' | 5 | 5 | import { createLogger } from '@sim/logger' | 6 | 6 | import { formatDate } from '@sim/utils/formatting' | 7 | | -import { Check, Copy, Plus, Search } from 'lucide-react' | | 7 | +import { Plus, Search } from 'lucide-react' | 8 | 8 | import { | 9 | 9 | Button, | 10 | 10 | Input as EmcnInput, | @@ -13,6 +13,7 @@ import { | 13 | 13 | ModalContent, | 14 | 14 | ModalFooter, | 15 | 15 | ModalHeader, | | 16 | + SecretReveal, | 16 | 17 | // Switch, | 17 | 18 | } from '@/components/emcn' | 18 | 19 | import { Input } from '@/components/ui' | @@ -58,7 +59,6 @@ export function Copilot() { | 58 | 59 | const [newKeyName, setNewKeyName] = useState('') | 59 | 60 | const [newKey, setNewKey] = useState<string | null>(null) | 60 | 61 | const [showNewKeyDialog, setShowNewKeyDialog] = useState(false) | 61 | | - const [copySuccess, setCopySuccess] = useState(false) | 62 | 62 | const [deleteKey, setDeleteKey] = useState<CopilotKey | null>(null) | 63 | 63 | const [showDeleteDialog, setShowDeleteDialog] = useState(false) | 64 | 64 | const [searchTerm, setSearchTerm] = useState('') | @@ -115,12 +115,6 @@ export function Copilot() { | 115 | 115 | } | 116 | 116 | } | 117 | 117 |
| 118 | | - const copyToClipboard = (key: string) => { | 119 | | - navigator.clipboard.writeText(key) | 120 | | - setCopySuccess(true) | 121 | | - setTimeout(() => setCopySuccess(false), 2000) | 122 | | - } | 123 | | - | 124 | 118 | const handleDeleteKey = async () => { | 125 | 119 | if (!deleteKey) return | 126 | 120 | try { | @@ -316,7 +310,6 @@ export function Copilot() { | 316 | 310 | setShowNewKeyDialog(open) | 317 | 311 | if (!open) { | 318 | 312 | setNewKey(null) | 319 | | - setCopySuccess(false) | 320 | 313 | } | 321 | 314 | }} | 322 | 315 | > | @@ -330,27 +323,7 @@ export function Copilot() { | 330 | 323 | </span> | 331 | 324 | </p> | 332 | 325 |
| 333 | | - {newKey && ( | 334 | | - <div className='relative mt-2.5'> | 335 | | - <div className='flex h-9 items-center rounded-md border bg-[var(--surface-1)] px-2.5 pr-10'> | 336 | | - <code className='flex-1 truncate font-mono text-[var(--text-primary)] text-sm'> | 337 | | - {newKey} | 338 | | - </code> | 339 | | - </div> | 340 | | - <Button | 341 | | - variant='ghost' | 342 | | - className='-translate-y-1/2 absolute top-1/2 right-[4px] h-[28px] w-[28px] rounded-sm text-[var(--text-muted)] hover-hover:text-[var(--text-primary)]' | 343 | | - onClick={() => copyToClipboard(newKey)} | 344 | | - > | 345 | | - {copySuccess ? ( | 346 | | - <Check className='h-[14px] w-[14px]' /> | 347 | | - ) : ( | 348 | | - <Copy className='h-[14px] w-[14px]' /> | 349 | | - )} | 350 | | - <span className='sr-only'>Copy to clipboard</span> | 351 | | - </Button> | 352 | | - </div> | 353 | | - )} | | 326 | + {newKey && <SecretReveal value={newKey} className='mt-2.5' />} | 354 | 327 | </ModalBody> | 355 | 328 | </ModalContent> | 356 | 329 | </Modal> | |
Collapse file Copy file name to clipboard +4-21Lines changed: 4 additions & 21 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | 'use client' | 2 | 2 |
| 3 | | -import { useCallback, useEffect, useRef, useState } from 'react' | 4 | 3 | import { Button, Check, Copy } from '@/components/emcn' | 5 | 4 | import { cn } from '@/lib/core/utils/cn' | | 5 | +import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' | 6 | 6 |
| 7 | 7 | interface CopyCodeButtonProps { | 8 | 8 | code: string | 9 | 9 | className?: string | 10 | 10 | } | 11 | 11 |
| 12 | 12 | export function CopyCodeButton({ code, className }: CopyCodeButtonProps) { | 13 | | - const [copied, setCopied] = useState(false) | 14 | | - const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null) | 15 | | - | 16 | | - const handleCopy = useCallback(async () => { | 17 | | - try { | 18 | | - await navigator.clipboard.writeText(code) | 19 | | - setCopied(true) | 20 | | - if (timerRef.current) clearTimeout(timerRef.current) | 21 | | - timerRef.current = setTimeout(() => setCopied(false), 2000) | 22 | | - } catch {} | 23 | | - }, [code]) | 24 | | - | 25 | | - useEffect( | 26 | | - () => () => { | 27 | | - if (timerRef.current) clearTimeout(timerRef.current) | 28 | | - }, | 29 | | - [] | 30 | | - ) | | 13 | + const { copied, copy } = useCopyToClipboard() | 31 | 14 |
| 32 | 15 | return ( | 33 | 16 | <Button | 34 | 17 | type='button' | 35 | 18 | variant='ghost' | 36 | | - onClick={handleCopy} | | 19 | + onClick={() => copy(code)} | 37 | 20 | className={cn('flex items-center gap-1 rounded px-1.5 py-0.5 text-xs', className)} | 38 | 21 | > | 39 | | - {copied ? <Check className='size-3.5' /> : <Copy className='size-3.5' />} | | 22 | + {copied ? <Check className='h-[14px] w-[14px]' /> : <Copy className='h-[14px] w-[14px]' />} | 40 | 23 | </Button> | 41 | 24 | ) | 42 | 25 | } |
Collapse file Copy file name to clipboardExpand all lines: apps/sim/components/emcn/components/index.ts +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -126,6 +126,7 @@ export { | 126 | 126 | SModalTrigger, | 127 | 127 | } from './s-modal/s-modal' | 128 | 128 | export { SecretInput, type SecretInputProps } from './secret-input/secret-input' | | 129 | +export { SecretReveal, type SecretRevealProps } from './secret-reveal/secret-reveal' | 129 | 130 | export { Skeleton } from './skeleton/skeleton' | 130 | 131 | export { Slider, type SliderProps } from './slider/slider' | 131 | 132 | export { Switch } from './switch/switch' | |
|