Skip to content

Commit 6656eba

Browse files
committed
fix(extension): revalidate access and protect page previews
1 parent cae3593 commit 6656eba

8 files changed

Lines changed: 353 additions & 160 deletions

File tree

‎.github/workflows/browser-extension.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ on:
1717
- 'apps/sim/next.config.ts'
1818
- 'apps/sim/package.json'
1919
- 'packages/emcn/**'
20+
- 'packages/logger/**'
21+
- 'packages/utils/**'
2022
- 'bun.lock'
2123
workflow_dispatch:
2224

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const fixture = Bun.serve({
3636
const received = [];
3737
window.addEventListener('message', event => {
3838
if (event.source !== parent || !event.origin.startsWith('chrome-extension://')) return;
39+
if (event.data.type === 'e2e-barrier') document.body.dataset.barrierId = event.data.id;
3940
if (event.data.type === 'context' && event.data.sessionId === document.body.dataset.sessionId) {
4041
if (document.body.dataset.ignoreCaptures) return;
4142
received.push(event.data.snapshot);
@@ -206,6 +207,19 @@ try {
206207
}
207208
})
208209

210+
const settleMessages = async () => {
211+
const barrierId = generateId()
212+
await panel.evaluate(
213+
({ origin, barrierId }) => {
214+
const target = document.querySelector('iframe')?.contentWindow
215+
if (!target) throw new Error('Missing hosted frame')
216+
target.postMessage({ type: 'e2e-barrier', id: barrierId }, origin)
217+
},
218+
{ origin, barrierId }
219+
)
220+
await app.waitForFunction((id) => document.body.dataset.barrierId === id, barrierId)
221+
}
222+
209223
await check('Capture stays local until the trusted hosted frame is ready', async () => {
210224
await capture()
211225
await panel.getByRole('status').filter({ hasText: 'Waiting for Sim' }).waitFor()
@@ -225,6 +239,7 @@ try {
225239
}),
226240
receiverSessionId
227241
)
242+
await settleMessages()
228243
assert((await snapshots()).length === 0, 'Wrong-origin readiness was accepted')
229244
const wrongFrameDelivered = panel.evaluate(
230245
() =>
@@ -246,6 +261,7 @@ try {
246261
document.body.append(child)
247262
}, receiverSessionId)
248263
await wrongFrameDelivered
264+
await settleMessages()
249265
assert((await snapshots()).length === 0, 'Wrong-frame readiness was accepted')
250266
await page.evaluate(() => {
251267
document.body.textContent = 'SECOND_CAPTURE_SENTINEL'

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
'use client'
22

3+
import { useState } from 'react'
34
import {
45
Chip,
56
ChipLink,
7+
cn,
68
DropdownMenu,
79
DropdownMenuContent,
810
DropdownMenuItem,
911
DropdownMenuSeparator,
1012
DropdownMenuTrigger,
13+
scrollFadeAttributes,
14+
scrollFadeClass,
1115
Tooltip,
16+
useScrollEdges,
1217
} from '@sim/emcn'
1318
import { ArrowUpRight, BubbleChatDelay, ChevronDown, Plus } from '@sim/emcn/icons'
1419
import { useRouter } from 'next/navigation'
@@ -35,6 +40,10 @@ export function ExtensionHeader({
3540
const router = useRouter()
3641
const { data: workspaces } = useWorkspacesQuery()
3742
const { data: chats, isPending, error } = history
43+
const [scopeElement, setScopeElement] = useState<HTMLDivElement | null>(null)
44+
const scopeEdges = useScrollEdges(scopeElement)
45+
const [historyElement, setHistoryElement] = useState<HTMLDivElement | null>(null)
46+
const historyEdges = useScrollEdges(historyElement)
3847
return (
3948
<header className='flex shrink-0 items-center gap-1 border-[var(--border)] border-b px-3 py-2'>
4049
<DropdownMenu>
@@ -43,7 +52,12 @@ export function ExtensionHeader({
4352
{scopeName}
4453
</Chip>
4554
</DropdownMenuTrigger>
46-
<DropdownMenuContent align='start'>
55+
<DropdownMenuContent
56+
ref={setScopeElement}
57+
align='start'
58+
{...scrollFadeAttributes(scopeEdges)}
59+
className={scrollFadeClass}
60+
>
4761
<DropdownMenuItem
4862
active={!workspaceId}
4963
disabled={!workspaceId}
@@ -76,7 +90,12 @@ export function ExtensionHeader({
7690
</Tooltip.Trigger>
7791
<Tooltip.Content>Chat history</Tooltip.Content>
7892
</Tooltip.Root>
79-
<DropdownMenuContent align='end' className='max-h-[320px] w-[240px] overflow-y-auto'>
93+
<DropdownMenuContent
94+
ref={setHistoryElement}
95+
align='end'
96+
{...scrollFadeAttributes(historyEdges)}
97+
className={cn('max-h-[320px] w-[240px] overflow-y-auto', scrollFadeClass)}
98+
>
8099
{isPending || error || !chats?.length ? (
81100
<DropdownMenuItem disabled>
82101
{isPending ? 'Loading chats…' : error ? 'Chat history unavailable' : 'No chats yet'}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,20 @@ import { WorkspacePermissionsProvider } from '@/app/workspace/[workspaceId]/prov
1414
import { WorkspaceScopeSync } from '@/app/workspace/[workspaceId]/providers/workspace-scope-sync'
1515
import { PermissionAccessBoundary } from '@/ee/access-requests/components/permission-access-boundary'
1616
import { BrandingProvider } from '@/ee/whitelabeling/components/branding-provider'
17-
import { useWorkspaceHostContextQuery } from '@/hooks/queries/workspace-host'
17+
import {
18+
useWorkspaceHostContextQuery,
19+
WORKSPACE_HOST_CONTEXT_STALE_TIME,
20+
} from '@/hooks/queries/workspace-host'
1821

1922
interface ExtensionWorkspaceProps {
2023
workspaceId: string
2124
}
2225

2326
export function ExtensionWorkspace({ workspaceId }: ExtensionWorkspaceProps) {
24-
const { data, isPending, error, refetch } = useWorkspaceHostContextQuery(workspaceId)
27+
const { data, isPending, error, refetch } = useWorkspaceHostContextQuery(workspaceId, {
28+
refetchInterval: WORKSPACE_HOST_CONTEXT_STALE_TIME,
29+
refetchOnWindowFocus: true,
30+
})
2531
if (isApiClientError(error) && error.status === 401) return <ExtensionSignIn />
2632
if (isPending) return <ExtensionLoading />
2733
const accessDenied = isApiClientError(error) && (error.status === 403 || error.status === 404)

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/page-context.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,13 @@ export function PageContext({ snapshot, onRemove, label }: PageContextProps) {
4646
onRemove={onRemove}
4747
removeLabel='Remove page context'
4848
/>
49-
<ChipModal open={open} onOpenChange={setOpen} srTitle='Page context' size='sm'>
49+
<ChipModal
50+
open={open}
51+
onOpenChange={setOpen}
52+
srTitle='Page context'
53+
size='sm'
54+
className='ph-no-capture'
55+
>
5056
<ChipModalHeader onClose={() => setOpen(false)}>{title}</ChipModalHeader>
5157
<ChipModalBody>
5258
<div className='flex flex-wrap items-center justify-between gap-2'>

‎apps/sim/hooks/queries/organization.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export function useOrganizationSummary(organizationId: string) {
5555
queryFn: ({ signal }) =>
5656
requestJson(getOrganizationContract, { params: { id: organizationId }, query: {}, signal }),
5757
staleTime: ORGANIZATION_DETAIL_STALE_TIME,
58+
refetchInterval: ORGANIZATION_DETAIL_STALE_TIME,
59+
refetchOnWindowFocus: true,
5860
})
5961
}
6062

‎apps/sim/hooks/queries/workspace-host.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,21 @@ async function fetchWorkspaceHostContext(
2323
})
2424
}
2525

26+
interface WorkspaceHostContextQueryOptions {
27+
refetchInterval?: number | false
28+
refetchOnWindowFocus?: boolean
29+
}
30+
2631
/**
2732
* Loads identity, host organization, owner-plan entitlements, and the viewer's
2833
* route-scoped authorization for one workspace.
2934
*/
30-
export function useWorkspaceHostContextQuery(workspaceId: string) {
35+
export function useWorkspaceHostContextQuery(
36+
workspaceId: string,
37+
options: WorkspaceHostContextQueryOptions = {}
38+
) {
3139
return useQuery({
40+
...options,
3241
queryKey: workspaceHostKeys.detail(workspaceId),
3342
queryFn: ({ signal }) => fetchWorkspaceHostContext(workspaceId, signal),
3443
enabled: Boolean(workspaceId),

0 commit comments

Comments
 (0)