From 8dd54849c8faad6aba6e9093ca0506996185d848 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 31 Oct 2026 02:31:00 -0700 Subject: [PATCH] fix(extension): keep site access compatible with strict CSP --- apps/extension/package.json | 2 +- apps/extension/scripts/build.ts | 2 +- apps/extension/scripts/test-e2e.ts | 78 +++++++++++++++++++++++++++--- apps/extension/src/sidepanel.tsx | 54 +++++++++++++++------ bun.lock | 2 +- 5 files changed, 113 insertions(+), 25 deletions(-) diff --git a/apps/extension/package.json b/apps/extension/package.json index 9f4a578f7f8..0b9cab24e1d 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -1,6 +1,6 @@ { "name": "@sim/extension", - "version": "0.1.0", + "version": "0.1.1", "private": true, "type": "module", "license": "Apache-2.0", diff --git a/apps/extension/scripts/build.ts b/apps/extension/scripts/build.ts index 80b47369ec6..0f6524e7cf6 100644 --- a/apps/extension/scripts/build.ts +++ b/apps/extension/scripts/build.ts @@ -107,7 +107,7 @@ await writeFile( { manifest_version: 3, name: development ? 'Sim (Development)' : 'Sim', - version: '0.1.0', + version: '0.1.1', description: 'Ask Sim about the page you are reading, with your existing Sim conversations and tools.', minimum_chrome_version: '120', diff --git a/apps/extension/scripts/test-e2e.ts b/apps/extension/scripts/test-e2e.ts index d9ff9255368..3d949981a69 100644 --- a/apps/extension/scripts/test-e2e.ts +++ b/apps/extension/scripts/test-e2e.ts @@ -128,6 +128,13 @@ try { await signIn.goto(`${origin}/sign-in`) await signIn.close() const panel: Page = await context.newPage() + // Observe dispatch before the bridge so background metadata queries stay outside each action. + await panel.addInitScript(() => { + window.addEventListener('message', () => { + document.documentElement.dataset.queryCountAtDispatch = + document.documentElement.dataset.browserQueryCount ?? '0' + }) + }) await panel.goto(`chrome-extension://${extensionId}/sidepanel.html`) await panel.getByTitle('Sim chat').waitFor() const frame: Frame = panel.frames()[1] ?? (await panel.waitForEvent('frameattached')) @@ -588,6 +595,7 @@ try { callback?: (tabs: chrome.tabs.Tab[]) => void ) => { queries++ + document.documentElement.dataset.browserQueryCount = String(queries) return callback ? originalQuery(query, callback) : originalQuery(query) }) as typeof chrome.tabs.query window.addEventListener('message', (event: MessageEvent) => { @@ -599,24 +607,31 @@ try { event.data.type !== 'action' ) return - document.body.dataset.actionQueries = String(queries) + document.body.dataset.actionQueries = String( + queries - Number(document.documentElement.dataset.queryCountAtDispatch ?? '0') + ) document.body.dataset.observedActions = String(++actions) }) }) + await panel.evaluate(() => chrome.tabs.query({ active: true, currentWindow: true })) const parentCdp = await panel.context().newCDPSession(panel) const cdp = await panel.context().newCDPSession(app) await expect .poll( async () => { - const result = await parentCdp.send('Runtime.evaluate', { - expression: 'navigator.userActivation.isActive', - userGesture: false, - }) - return result.result.value + const results = await Promise.all( + [parentCdp, cdp].map((session) => + session.send('Runtime.evaluate', { + expression: 'navigator.userActivation.isActive', + userGesture: false, + }) + ) + ) + return results.map((result) => result.result.value) }, { timeout: 10_000 } ) - .toBe(false) + .toEqual([false, false]) const action = { channel: 'sim-extension', version: 1, @@ -653,6 +668,55 @@ try { await parentCdp.detach() } ) + await check( + 'Site access stays modal without violating the packaged security policy', + async () => { + const policyErrors: string[] = [] + panel.on('console', (message) => { + if (message.type() === 'error' && message.text().includes('Content Security Policy')) + policyErrors.push(message.text()) + }) + const openAccess = () => + app.evaluate( + ({ targetOrigin, sessionId }) => { + parent.postMessage( + { + channel: 'sim-extension', + version: 1, + type: 'action', + sessionId, + action: 'site-access', + }, + targetOrigin + ) + }, + { targetOrigin: `chrome-extension://${extensionId}`, sessionId: receiverSessionId } + ) + await panel.bringToFront() + await openAccess() + const dialog = panel.getByRole('dialog', { name: 'Site access' }) + await expect(dialog).toBeVisible() + await settleMessages() + assert(policyErrors.length === 0, `Site access violated CSP: ${policyErrors.join('\n')}`) + await panel.evaluate(() => document.querySelector('iframe')?.focus()) + assert( + await dialog.evaluate((element) => element.contains(document.activeElement)), + 'The hosted frame accepted focus behind the modal' + ) + await dialog.getByRole('button', { name: 'Close' }).press('Escape') + await expect(dialog, 'Escape closes the modal').toBeHidden() + await openAccess() + await expect(dialog).toBeVisible() + await dialog.getByRole('button', { name: 'Close' }).click() + await expect(dialog, 'The Close button dismisses the modal').toBeHidden() + await openAccess() + await expect(dialog).toBeVisible() + await dialog.click({ trial: true }) + await panel.mouse.click(1, 1) + await expect(dialog, 'The backdrop dismisses the modal').toBeHidden() + assert(policyErrors.length === 0, `Site access violated CSP: ${policyErrors.join('\n')}`) + } + ) await mkdir(dirname(output), { recursive: true }) await panel.screenshot({ path: output.replace(/\.json$/, '.png'), fullPage: true }) } catch (error) { diff --git a/apps/extension/src/sidepanel.tsx b/apps/extension/src/sidepanel.tsx index 2166938f23a..5e6114cecdd 100644 --- a/apps/extension/src/sidepanel.tsx +++ b/apps/extension/src/sidepanel.tsx @@ -4,7 +4,7 @@ import { type ExtensionToAppMessage, type PageSnapshot, } from '@sim/browser-extension-protocol' -import { Chip, ChipModal, ChipModalBody, ChipModalHeader } from '@sim/emcn' +import { Chip, ChipModalBody, ChipModalHeader, ChipModalSurface } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import { createRoot } from 'react-dom/client' import { captureActivePage } from '@/capture' @@ -16,6 +16,13 @@ interface SidePanelProps { origin: string } +/** Native modality avoids the inline scroll-lock stylesheet blocked by the extension CSP. */ +function mountAccessDialog(dialog: HTMLDialogElement | null) { + if (!dialog) return + dialog.showModal() + return () => dialog.close() +} + function SidePanel({ origin }: SidePanelProps) { const frameRef = useRef(null) const pendingRef = useRef(null) @@ -279,20 +286,37 @@ function SidePanel({ origin }: SidePanelProps) { referrerPolicy='no-referrer' sandbox='allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads' /> - - setAccessOpen(false)}>Site access - - {site ? ( - void toggleSite()}> - {`${site.allowed ? 'Remove access to' : 'Allow access to'} ${new URL(site.origin).hostname}`} - - ) : ( -

- Open a web page and click the Sim toolbar icon to manage access. -

- )} -
-
+ {accessOpen && ( + { + event.preventDefault() + setAccessOpen(false) + }} + onClick={(event) => { + if (event.target === event.currentTarget) setAccessOpen(false) + }} + > + + setAccessOpen(false)}> + Site access + + + {site ? ( + void toggleSite()}> + {`${site.allowed ? 'Remove access to' : 'Allow access to'} ${new URL(site.origin).hostname}`} + + ) : ( +

+ Open a web page and click the Sim toolbar icon to manage access. +

+ )} +
+
+
+ )} ) } diff --git a/bun.lock b/bun.lock index 0525115a6fd..fcdf12035b4 100644 --- a/bun.lock +++ b/bun.lock @@ -148,7 +148,7 @@ }, "apps/extension": { "name": "@sim/extension", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "@sim/browser-extension-protocol": "workspace:*", "@sim/emcn": "workspace:*",