Skip to content

Commit 32bd39c

Browse files
committed
fix(extension): exclude revealed secrets and keep large composers usable
1 parent 649f0fd commit 32bd39c

7 files changed

Lines changed: 221 additions & 20 deletions

File tree

‎apps/extension/scripts/test-e2e.ts‎

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
55
import { tmpdir } from 'node:os'
66
import { dirname, resolve } from 'node:path'
77
import { fileURLToPath } from 'node:url'
8+
import { createElement } from 'react'
89
import { type BrowserContext, chromium, expect, type Frame, type Page } from '@playwright/test'
910
import { pageSnapshotSchema } from '@sim/browser-extension-protocol'
11+
import { SecretReveal } from '@sim/emcn'
1012
import { createLogger } from '@sim/logger'
1113
import { getErrorMessage } from '@sim/utils/errors'
1214
import { generateId } from '@sim/utils/id'
15+
import { renderToStaticMarkup } from 'react-dom/server'
1316

1417
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
1518
const logger = createLogger('BrowserExtensionE2E')
@@ -18,6 +21,9 @@ if (!reportPath) throw new Error('Set EXTENSION_REPORT_PATH to the output JSON r
1821
const output = resolve(root, reportPath)
1922
const checks: { name: string; status: 'passed' | 'failed'; durationMs: number; error?: string }[] =
2023
[]
24+
const secretReveal = renderToStaticMarkup(
25+
createElement(SecretReveal, { value: 'SECRET_REVEAL_SENTINEL' })
26+
)
2127
const fixture = Bun.serve({
2228
hostname: '127.0.0.1',
2329
port: 0,
@@ -55,6 +61,7 @@ const fixture = Bun.serve({
5561
<input type="password" value="PASSWORD_SENTINEL"/><textarea>TEXTAREA_SENTINEL</textarea>
5662
<div contenteditable="true" id="editable">EDITABLE_SENTINEL</div><div hidden>HIDDEN_SENTINEL</div>
5763
<div style="display:none">CSS_HIDDEN_SENTINEL</div><div aria-hidden="true">ARIA_SENTINEL</div>
64+
<section id="secret-reveal"><p>Credential instructions</p>${secretReveal}<code>visible-code-control</code></section>
5865
<script>const secret='SCRIPT_SENTINEL'</script>
5966
</body></html>`,
6067
{ headers: { 'content-type': 'text/html' } }
@@ -236,9 +243,11 @@ try {
236243
'A typed sim/ link opens the Sim link resolver on the configured origin',
237244
async () => {
238245
const tab = await page.context().newPage()
239-
for (const path of ['handbook', 'jira/123?view=board']) {
240-
await tab.goto(`http://sim/${path}`)
241-
assert(tab.url() === `${origin}/sim/${path}`, `sim/${path} reached ${tab.url()}`)
246+
for (const scheme of ['http', 'https']) {
247+
for (const path of ['handbook', 'jira/123?view=board', 'jira/a%2Fb?view=board%20view']) {
248+
await tab.goto(`${scheme}://sim/${path}`)
249+
assert(tab.url() === `${origin}/sim/${path}`, `sim/${path} reached ${tab.url()}`)
250+
}
242251
}
243252
await tab.close()
244253
}
@@ -315,6 +324,7 @@ try {
315324
await check('Page extraction excludes secrets and strips URL query and fragment', async () => {
316325
const [snapshot] = await snapshots()
317326
assert(snapshot.text.includes('visible-reference-482'), 'Visible page content missing')
327+
assert(snapshot.text.includes('visible-code-control'), 'Ordinary code was excluded')
318328
for (const sentinel of [
319329
'PASSWORD_SENTINEL',
320330
'TEXTAREA_SENTINEL',
@@ -323,6 +333,7 @@ try {
323333
'CSS_HIDDEN_SENTINEL',
324334
'ARIA_SENTINEL',
325335
'SCRIPT_SENTINEL',
336+
'SECRET_REVEAL_SENTINEL',
326337
]) {
327338
assert(!snapshot.text.includes(sentinel), `Captured ${sentinel}`)
328339
}
@@ -715,6 +726,45 @@ try {
715726
assert((await snapshots()).length === before, 'An excluded Sim path was captured')
716727
}
717728
})
729+
await check(
730+
'Revealed credentials stay excluded from exact and surrounding selections',
731+
async () => {
732+
await page.goto(`${origin}/workspace/example`)
733+
await page.bringToFront()
734+
const before = (await snapshots()).length
735+
await capture()
736+
await expect.poll(async () => (await snapshots()).length).toBe(before + 1)
737+
await expect(panel.getByRole('status')).toHaveText('')
738+
await page
739+
.locator('#secret-reveal code')
740+
.first()
741+
.evaluate((element) => {
742+
const range = document.createRange()
743+
range.selectNodeContents(element)
744+
window.getSelection()?.removeAllRanges()
745+
window.getSelection()?.addRange(range)
746+
})
747+
await capture('Selection')
748+
await expect(panel.getByRole('status')).toContainText('could not be read')
749+
assert((await snapshots()).length === before + 1, 'A revealed credential was attached')
750+
await page.locator('#secret-reveal').evaluate((element) => {
751+
const range = document.createRange()
752+
range.selectNodeContents(element)
753+
window.getSelection()?.removeAllRanges()
754+
window.getSelection()?.addRange(range)
755+
})
756+
await capture('Selection')
757+
await expect.poll(async () => (await snapshots()).length).toBe(before + 2)
758+
const snapshot = (await snapshots()).at(-1)
759+
assert(snapshot, 'Missing surrounding selection snapshot')
760+
assert(snapshot.text.includes('Credential instructions'), 'Public selection text is missing')
761+
assert(snapshot.text.includes('visible-code-control'), 'Ordinary selected code was excluded')
762+
assert(
763+
!snapshot.text.includes('SECRET_REVEAL_SENTINEL'),
764+
'A surrounding selection leaked the credential'
765+
)
766+
}
767+
)
718768
await check(
719769
'Site access stays modal without violating the packaged security policy',
720770
async () => {

‎apps/sim/app/extension/[workspaceId]/chat.tsx‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,7 @@ export function ExtensionChat({ workspaceId, workspaceName }: ExtensionChatProps
172172
composerToolbarActions={composer.toolbarActions}
173173
composerModelSelectorOptions={composer.modelSelectorOptions}
174174
layout='copilot-view'
175-
className={cn(
176-
'h-auto min-h-0',
177-
selectedChatId || chat.messages.length > 0 ? 'flex-1' : 'shrink-0'
178-
)}
175+
className={cn('h-auto min-h-0', (selectedChatId || chat.messages.length > 0) && 'flex-1')}
179176
/>
180177
<ChipModal
181178
open={Boolean(sourceMessage)}

‎apps/sim/app/o/[organizationId]/components/organization-conversation.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
organizationSearchUrlKeys,
3636
} from '@/app/o/[organizationId]/search/search-params'
3737
import { ChatResourcePanel } from '@/app/workspace/[workspaceId]/home/components/chat-resource-panel'
38+
import { ComposerScrollArea } from '@/app/workspace/[workspaceId]/home/components/composer-scroll-area'
3839
import { useSearchHistoryActions } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-history-context'
3940
import { SearchIntegrationConnection } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/search-integration-connection'
4041
import { MothershipChat } from '@/app/workspace/[workspaceId]/home/components/mothership-chat'
@@ -537,7 +538,7 @@ function OrganizationConversationContent({
537538
<div className='flex flex-1 items-center justify-center px-5'>
538539
<h1 className='text-lg'>How can I help?</h1>
539540
</div>
540-
<div className='px-3 pb-3'>{composer}</div>
541+
<ComposerScrollArea className='px-3 pb-3'>{composer}</ComposerScrollArea>
541542
</>
542543
) : (
543544
<OrganizationLanding
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use client'
2+
3+
import { type AnimationEventHandler, type ReactNode, useRef } from 'react'
4+
import { cn, scrollFadeAttributes, scrollFadeClass, useScrollEdges } from '@sim/emcn'
5+
6+
interface ComposerScrollAreaProps {
7+
children: ReactNode
8+
className?: string
9+
onAnimationEnd?: AnimationEventHandler<HTMLDivElement>
10+
}
11+
12+
/** Keeps the complete composer reachable when its contents exceed the available chat height. */
13+
export function ComposerScrollArea({
14+
children,
15+
className,
16+
onAnimationEnd,
17+
}: ComposerScrollAreaProps) {
18+
const scrollRef = useRef<HTMLDivElement>(null)
19+
const edges = useScrollEdges(scrollRef)
20+
21+
return (
22+
<div
23+
ref={scrollRef}
24+
className={cn('min-h-0 overflow-y-auto overscroll-contain', scrollFadeClass, className)}
25+
{...scrollFadeAttributes(edges)}
26+
onAnimationEnd={onAnimationEnd}
27+
>
28+
{children}
29+
</div>
30+
)
31+
}

‎apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { MessageActions } from '@/app/workspace/[workspaceId]/components/message
3535
import { ChatMessageAttachments } from '@/app/workspace/[workspaceId]/home/components/chat-message-attachments'
3636
import { ChatPlanNotice } from '@/app/workspace/[workspaceId]/home/components/chat-plan-notice'
3737
import { ChatSurfaceProvider } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
38+
import { ComposerScrollArea } from '@/app/workspace/[workspaceId]/home/components/composer-scroll-area'
3839
import {
3940
assistantMessageHasRenderableContent,
4041
getOrchestratorMessageText,
@@ -175,7 +176,7 @@ const LAYOUT_STYLES = {
175176
userBubble:
176177
'max-w-[85%] md:max-w-[70%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3.5 py-2',
177178
assistantRow: 'group/msg',
178-
footer: 'shrink-0 px-3 pb-[max(12px,env(safe-area-inset-bottom))] md:px-[24px] md:pb-[16px]',
179+
footer: 'px-3 pb-[max(12px,env(safe-area-inset-bottom))] md:px-[24px] md:pb-[16px]',
179180
footerInner: 'mx-auto max-w-chat',
180181
},
181182
'copilot-view': {
@@ -187,7 +188,7 @@ const LAYOUT_STYLES = {
187188
attachmentWidth: 'max-w-[85%]',
188189
userBubble: 'max-w-[85%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3 py-2',
189190
assistantRow: 'group/msg',
190-
footer: 'shrink-0 px-3 pb-3',
191+
footer: 'px-3 pb-3',
191192
footerInner: '',
192193
},
193194
} as const
@@ -1030,7 +1031,7 @@ export function MothershipChat({
10301031
)}
10311032
</div>
10321033

1033-
<div
1034+
<ComposerScrollArea
10341035
className={cn(styles.footer, animateInput && 'animate-slide-in-bottom')}
10351036
onAnimationEnd={animateInput ? onInputAnimationEnd : undefined}
10361037
>
@@ -1066,7 +1067,7 @@ export function MothershipChat({
10661067
/>
10671068
))}
10681069
</div>
1069-
</div>
1070+
</ComposerScrollArea>
10701071
</div>
10711072
</ChatSurfaceProvider>
10721073
)

0 commit comments

Comments
 (0)