Skip to content

Commit c8f811f

Browse files
committed
improvement(links): keep sim/ URLs, tell the extension when sign-in finishes, and fix what the feature audit found
A user belongs to one organization, so org-scoped link URLs fixed nothing and lost values and queries. Restore them, and drop the redirect-loop guard, which refused real aliases and broke editing them. The hosted app now posts signed-in from the extension layout, so members without chat access still get shortcuts. Typed phrases find hyphenated names, Copy link confirms, the omnibox hint matches what Enter does, and the docs match the behaviour. Share the extension dialog shell and remove an unreachable recovery branch.
1 parent 918a433 commit c8f811f

29 files changed

Lines changed: 192 additions & 189 deletions

File tree

‎apps/docs/content/docs/browser-extension/index.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ Change the binding in Chrome’s **Extensions → Keyboard shortcuts**. If Chrom
3333

3434
## Open team links
3535

36-
Type a shortcut such as `go/handbook` or `sim/handbook` in Chrome’s address bar. Both work as soon as the extension is installed.
36+
Type a shortcut such as `go/handbook` or `sim/handbook` in Chrome’s address bar. Both work once you're signed in to Sim and Links is on for your organization.
3737

38-
For suggestions, type `sim`, press **Space**, and enter part of a link name. If Chrome searches instead of opening a shortcut, enable your search engine under the extension’s **+ → Browser shortcuts**. Other searches stay unchanged.
38+
For suggestions, type `sim`, press **Space**, and enter part of a link name. If Chrome searches instead of opening a shortcut, open the Sim extension and sign in. If it still searches, enable your search engine under the extension’s **+ → Browser shortcuts**. Other searches stay unchanged.
3939

4040
See [Links](/search/sim-links#open-links-from-the-address-bar) for supported inputs, Glean conflicts, and team rollout. Administrators can [allow the extension for a Chrome organizational unit](https://support.google.com/chrome/a/answer/6306504).
4141

‎apps/docs/content/docs/platform/enterprise/audit-logs.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ Events use identifiers such as `resource.action`. These categories are represent
101101
| API keys | `api_key.created`, `api_key.revoked` |
102102
| Credentials | `credential.created`, `credential.deleted`, `oauth.disconnected` |
103103
| Organization | `organization.updated`, `org_member.added`, `org_member.role_changed`, `organization.sso_provider.updated` |
104-
| Sim links | `sim_link.created`, `sim_link.updated`, `sim_link.deleted` |
104+
| Links | `sim_link.created`, `sim_link.updated`, `sim_link.deleted` |
105105
| Exports and drains | `audit_logs.exported`, `data_drain.created`, `data_drain.updated`, `data_drain.ran`, `data_drain.tested` |
106106

107107
Authentication failures and password-reset requests use an anonymous actor and record the target account separately. Impersonation and administrative session revocations identify the operator and target organization. Table-row mutations record summaries and counts without copying row contents.

‎apps/docs/content/docs/search/sim-links.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Names ignore case and punctuation, so `sim/Team-Handbook` and `sim/teamhandbook`
1515

1616
## Open links from the address bar
1717

18-
[Install Sim for Chrome](https://chromewebstore.google.com/detail/sim/oanlcahddodnjpdoopcpjlcageejjpmg) in your work profile, then type `go/handbook` or `sim/handbook` in the address bar. Both work as soon as it's installed.
18+
[Install Sim for Chrome](https://chromewebstore.google.com/detail/sim/oanlcahddodnjpdoopcpjlcageejjpmg) in your work profile, then type `go/handbook` or `sim/handbook` in the address bar. Both work once you're signed in to Sim.
1919

2020
For suggestions, type `sim`, press **Space**, and enter part of a name. Matching links appear with the most opened first. Unlisted links still open by name.
2121

@@ -31,7 +31,7 @@ Turning shortcuts off keeps site access. To remove it, open Chrome’s **Extensi
3131

3232
### Roll out to your team
3333

34-
Administrators can [force-install Sim for Chrome](https://support.google.com/chrome/a/answer/6306504) for an organizational unit. Shortcuts start working once each person signs in to Sim and Chrome next starts.
34+
Administrators can [force-install Sim for Chrome](https://support.google.com/chrome/a/answer/6306504) for an organizational unit. Shortcuts start working once each person signs in through the extension, or the next time Chrome starts after they've signed in.
3535

3636
### If another tool uses go/ or sim
3737

‎apps/extension/DESIGN.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,20 @@ other workspace resources or browser-control tools.
6060
## Permission and lifetime rules
6161

6262
- Required browser capabilities: side panel, scripting, active tab, context menus,
63-
and short-lived session storage for selection handoff. Required host access is
63+
short-lived session storage for selection handoff, and local storage for preferences. Required host access is
6464
limited to the configured Sim origin and to `http(s)://sim/*` and `http(s)://go/*`, which exist
6565
only so one static redirect rule can send top-level navigations to those hosts to the link resolver.
6666
- HTTP(S) page access is one optional browser-wide grant, requested from a direct
6767
user gesture. The browser owns revocation and restrictions. Approval enables
6868
capture across sites; switching tabs alone does not extract content.
69-
- No history, bookmarks, cookie API, debugger, native messaging, external messaging,
69+
- No history permission, bookmarks, cookie API, debugger, native messaging, external messaging,
7070
or arbitrary remote code execution. The only network interception is the static
7171
`sim/` and `go/` redirect rule above, plus opt-in search recovery.
72-
- The only background network request is the `sim` address-bar keyword: after
72+
- Background network requests are limited to two. Once a week at browser start, and after
73+
signing in to the panel, the worker asks the Sim origin whether links are on for the session;
74+
if so, it opens `http://go/` and `http://sim/` in background tabs that close themselves, which
75+
teaches Chrome's address bar these hosts and adds entries to browser history. The other is the
76+
`sim` address-bar keyword: after
7377
`sim` and Space, the text typed before the first `/` or `?` is sent to the
7478
configured Sim origin with the signed-in session to suggest matching Sim links,
7579
unless it contains a space (a search) or is longer than a link name can be. Page

‎apps/extension/README.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ SIM_EXTENSION_DEV=1 SIM_EXTENSION_ORIGIN=http://localhost:3000 bun run --cwd app
3838
- Extraction stops at 50,000 text characters, 150,000 serialized UTF-8 bytes, 15,000 visited nodes, or 150ms. The composer exposes truncation.
3939
- Attached pages share a 150,000-character and 450,000-byte snapshot budget per message, within the general 100-context ceiling. Shorter pages can share the available budget without a separate page-count restriction.
4040
- Source URLs omit credentials, query parameters, and fragments. Human-readable text can still contain sensitive information; it is the user-selected material they review and send into their conversation.
41-
- Snapshots stay in memory until handed to Sim and acknowledged or expired after 30 seconds. Extension storage contains only a short-lived intent with a tab/document identity and a SHA-256 URL digest, never raw URLs, captured text, or credentials. Closing a tab or navigating invalidates its pending intent. Sim applies its normal conversation retention once a message is sent.
42-
- No browser history, bookmarks, cookie access, debugger access, remote extension JavaScript, arbitrary network proxy, or background content collection is requested. Incognito use is disabled.
41+
- Snapshots stay in memory until handed to Sim and acknowledged or expired after 30 seconds. For captures, extension storage contains only a short-lived intent with a tab/document identity and a SHA-256 URL digest, never raw URLs, captured text, or credentials. Closing a tab or navigating invalidates its pending intent. Sim applies its normal conversation retention once a message is sent.
42+
- No browser history permission, bookmarks, cookie access, debugger access, remote extension JavaScript, arbitrary network proxy, or background content collection is requested. Incognito use is disabled.
4343
- The iframe bridge checks both the exact configured origin and the exact frame, then validates versioned messages. The hosted composer may request only page capture, selection capture, the native page-access and browser-shortcut dialogs, or the browser's fixed microphone settings page. Requests require the current conversation nonce and Chrome transient user activation; arbitrary URLs, tabs, script execution, and network operations are never accepted. Permission grants still require a click in the extension-owned page-access control and browser approval. Shortcut settings are advertised separately so older installed shells keep working without the new action.
4444
- Captures and queued toolbar intents are scoped to the receiving conversation. Changing conversation, workspace, or account discards an unfinished handoff. Captured attachments remain explicit snapshots when the source tab changes.
4545
- The complete hosted document, including portaled source dialogs and menus, is blocked from session replay. Clipboard writing and microphone capture are delegated only to the configured Sim origin; clipboard reading and camera access are not delegated. Voice input starts only after the user clicks the microphone and the browser grants permission. The shared Sim voice service streams microphone audio to ElevenLabs for transcription into the editable draft. Stop ends capture while allowing the final transcript to arrive; Send and conversation changes cancel unfinished transcription. There is no tab-audio capture or background recording.
4646

47-
With **Reference current page** enabled, a composer Send requests a single fresh snapshot for that conversation. The extension stores only the preference in local storage. A tab change does not read any content. Explicit snapshots take precedence; access denial, navigation and timeouts keep the draft unsent. **Allow across websites** works even when Chrome hides the active address; no address entry is required. The grant covers HTTP and HTTPS sites across tabs until the user revokes or restricts it in browser settings. Uploaded screenshot files stay in the hosted app’s normal attachment flow.
47+
With **Reference current page** enabled, a composer Send requests a single fresh snapshot for that conversation. Local storage holds only this preference, search recovery choices, and when the short hosts were last visited. A tab change does not read any content. Explicit snapshots take precedence; access denial, navigation and timeouts keep the draft unsent. **Allow across websites** works even when Chrome hides the active address; no address entry is required. The grant covers HTTP and HTTPS sites across tabs until the user revokes or restricts it in browser settings. Uploaded screenshot files stay in the hosted app’s normal attachment flow.
4848

4949
## Verification
5050

@@ -56,7 +56,7 @@ bun run --cwd apps/extension build
5656

5757
The E2E suite uses real Chromium, the packaged extension, and a disposable HTTP app fixture. It pregrants only the fixture page origin in a temporary package to exercise extraction without automating the browser's permission dialog. It checks HttpOnly session-cookie transport, origin/frame spoofing, sensitive-field exclusion, source URL minimization, selected excerpts, UTF-8 limits, stale-document handoffs, denied host access, and absence of page bytes in extension storage. The cookie check uses a fixture, not Better Auth or SSO. JSON results and a screenshot are written beside the caller-supplied report path. The E2E suite builds into a temporary directory and leaves shipping `dist` untouched.
5858

59-
Release verification must additionally exercise the native toolbar and shortcut, Chrome's real grant/revoke prompt, sign-in/SSO and expiry against the running Sim app, side-panel closing/reopening, workspace changes, narrow widths, and the real Mothership streaming response. The fixture suite does not establish those integrations. The design document records launch scope and release criteria.
59+
Release verification must additionally exercise typing `go/<name>` in Chrome's real address bar on a clean profile (the e2e proves the visits, not the address bar), the native toolbar and shortcut, Chrome's real grant/revoke prompt, sign-in/SSO and expiry against the running Sim app, side-panel closing/reopening, workspace changes, narrow widths, and the real Mothership streaming response. The fixture suite does not establish those integrations. The design document records launch scope and release criteria.
6060

6161
The authenticated acceptance suite runs against a real local app, Better Auth, and a disposable Postgres database whose name contains `test`. Enable `KNOWLEDGE_MEMBER_ACCESS`, `CREDENTIAL_GROUPS`, `ACCESS_CONTROL_ENABLED`, and `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` on that app to exercise restricted organization members:
6262

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { SecretReveal } from '@sim/emcn'
1818
import { createLogger } from '@sim/logger'
1919
import { getErrorMessage } from '@sim/utils/errors'
20+
import { sleep } from '@sim/utils/helpers'
2021
import { generateId } from '@sim/utils/id'
2122
import { renderToStaticMarkup } from 'react-dom/server'
2223

@@ -226,6 +227,7 @@ try {
226227
)
227228
await check('Installing signed out leaves go/ and sim/ as ordinary searches', async () => {
228229
await expect.poll(() => linksChecks, { timeout: 30_000 }).toBeGreaterThan(0)
230+
await sleep(2_000)
229231
assert((await shortHostsVisitedAt()) === 'undefined', 'Visits were recorded while signed out')
230232
assert(shortHostVisits === 0, 'Short hosts were visited while signed out')
231233
})
@@ -526,6 +528,14 @@ try {
526528
await check(
527529
'A signed-in panel visits each short host in tabs that close themselves',
528530
async () => {
531+
await app.evaluate(
532+
(targetOrigin) =>
533+
parent.postMessage(
534+
{ channel: 'sim-extension', version: 1, type: 'signed-in' },
535+
targetOrigin
536+
),
537+
`chrome-extension://${extensionId}`
538+
)
529539
await expect.poll(shortHostsVisitedAt, { timeout: 30_000 }).toBe('number')
530540
assert(shortHostVisits === SIM_LINK_HOSTS.length, `Visited ${shortHostVisits} short hosts`)
531541
const leftBehind = await worker.evaluate(
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { ReactNode } from 'react'
2+
3+
interface ExtensionDialogProps {
4+
labelledBy: string
5+
describedBy?: string
6+
onClose: () => void
7+
children: ReactNode
8+
}
9+
10+
/** Chrome owns modality so the dialog works without the scroll-lock stylesheet the CSP blocks. */
11+
function showDialog(dialog: HTMLDialogElement | null) {
12+
if (!dialog) return
13+
dialog.showModal()
14+
return () => dialog.close()
15+
}
16+
17+
export function ExtensionDialog({
18+
labelledBy,
19+
describedBy,
20+
onClose,
21+
children,
22+
}: ExtensionDialogProps) {
23+
return (
24+
<dialog
25+
ref={showDialog}
26+
aria-labelledby={labelledBy}
27+
aria-describedby={describedBy}
28+
className='m-auto max-h-[85vh] w-[90vw] max-w-[440px] overflow-visible border-0 bg-transparent p-0 text-inherit backdrop:bg-black/10 backdrop:backdrop-blur-[2px]'
29+
onCancel={(event) => {
30+
event.preventDefault()
31+
onClose()
32+
}}
33+
onClick={(event) => {
34+
if (event.target === event.currentTarget) onClose()
35+
}}
36+
>
37+
{children}
38+
</dialog>
39+
)
40+
}

‎apps/extension/src/omnibox.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const SUGGEST_DELAY_MS = 220
1515
const SEARCH_HINT = 'Find a link in Sim'
1616
// Chrome persists this template and substitutes the input when rendering the default row.
1717
const OPEN_LINK_HINT = 'Open <match>%s</match> in Sim'
18+
const SEARCH_LINKS_HINT = 'Find <match>%s</match> in Sim'
1819

1920
/** A Sim link as suggested: its name, and what it shows beside the name. */
2021
interface SuggestedLink {
@@ -140,9 +141,10 @@ export function registerSimLinkOmnibox() {
140141
chrome.omnibox.onInputChanged.addListener((text, suggest) => {
141142
pending?.abort()
142143
const { name, rest } = readTyped(text)
143-
showHint(name || rest ? OPEN_LINK_HINT : SEARCH_HINT)
144144
// A name can't hold a space, so `sim card prices` is a search, not a link: ask Sim nothing.
145-
if (/\s/.test(name) || name.length > SIM_LINK_NAME_MAX_LENGTH) {
145+
const phrase = /\s/.test(name)
146+
showHint(phrase ? SEARCH_LINKS_HINT : name || rest ? OPEN_LINK_HINT : SEARCH_HINT)
147+
if (phrase || name.length > SIM_LINK_NAME_MAX_LENGTH) {
146148
suggest([])
147149
return
148150
}

‎apps/extension/src/search-recovery-dialog.tsx‎

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ChipModalSurface,
1010
Switch,
1111
} from '@sim/emcn'
12+
import { ExtensionDialog } from '@/extension-dialog'
1213
import {
1314
readSearchRecovery,
1415
SEARCH_RECOVERY_KEY,
@@ -23,13 +24,6 @@ interface SearchRecoveryDialogProps {
2324
type SearchProvider = (typeof SEARCH_RECOVERY_PROVIDERS)[number]
2425
type SearchRecovery = Awaited<ReturnType<typeof readSearchRecovery>>
2526

26-
/** Chrome owns modality so the dialog works without an injected scroll-lock stylesheet. */
27-
function mountSearchRecoveryDialog(dialog: HTMLDialogElement | null) {
28-
if (!dialog) return
29-
dialog.showModal()
30-
return () => dialog.close()
31-
}
32-
3327
export function SearchRecoveryDialog({ onClose }: SearchRecoveryDialogProps) {
3428
const generation = useRef(0)
3529
const [settings, setSettings] = useState<SearchRecovery>()
@@ -93,26 +87,19 @@ export function SearchRecoveryDialog({ onClose }: SearchRecoveryDialogProps) {
9387
}
9488

9589
return (
96-
<dialog
97-
ref={mountSearchRecoveryDialog}
98-
aria-labelledby='search-recovery-title'
99-
aria-describedby='search-recovery-description'
100-
className='m-auto max-h-[85vh] w-[90vw] max-w-[440px] overflow-visible border-0 bg-transparent p-0 text-inherit backdrop:bg-black/10 backdrop:backdrop-blur-[2px]'
101-
onCancel={(event) => {
102-
event.preventDefault()
103-
onClose()
104-
}}
105-
onClick={(event) => {
106-
if (event.target === event.currentTarget) onClose()
107-
}}
90+
<ExtensionDialog
91+
labelledBy='search-recovery-title'
92+
describedBy='search-recovery-description'
93+
onClose={onClose}
10894
>
10995
<ChipModalSurface className='max-h-[85vh]'>
11096
<ChipModalHeader onClose={onClose}>
11197
<span id='search-recovery-title'>Browser shortcuts</span>
11298
</ChipModalHeader>
11399
<ChipModalBody>
114100
<ChipModalDescription id='search-recovery-description'>
115-
Open go/ and sim/ links from search. Other searches stay unchanged.
101+
If Chrome searches for a go/ or sim/ link instead of opening it, open the link anyway.
102+
Other searches stay unchanged.
116103
</ChipModalDescription>
117104
{SEARCH_RECOVERY_PROVIDERS.map((provider) => (
118105
<ChipModalField
@@ -151,6 +138,6 @@ export function SearchRecoveryDialog({ onClose }: SearchRecoveryDialogProps) {
151138
</ChipModalBody>
152139
<ChipModalFooter defaultAction='dismiss' cancelLabel='Done' onCancel={onClose} />
153140
</ChipModalSurface>
154-
</dialog>
141+
</ExtensionDialog>
155142
)
156143
}

‎apps/extension/src/search-recovery.ts‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const SEARCH_RECOVERY_PROVIDERS = [
4040
},
4141
] as const
4242

43+
type SearchRecoveryProvider = (typeof SEARCH_RECOVERY_PROVIDERS)[number]
4344
type ProviderId = (typeof SEARCH_RECOVERY_PROVIDERS)[number]['id']
4445
type RecoveryPreferences = Record<ProviderId, boolean>
4546
const logger = createLogger('BrowserShortcutRecovery')
@@ -122,20 +123,16 @@ export async function setSearchRecovery(provider: ProviderId, enabled: boolean):
122123
/** Reconcile persisted, permission-bound rules at startup, on revocation, and after an explicit choice. */
123124
export function registerSearchRecovery() {
124125
let pending = Promise.resolve()
125-
const update = (choice?: { provider: ProviderId; enabled: boolean }) => {
126+
const update = (choice?: { provider: SearchRecoveryProvider; enabled: boolean }) => {
126127
const operation = pending.then(async () => {
127128
const preferences = await readSearchRecovery()
128129
if (choice) {
129-
const provider = SEARCH_RECOVERY_PROVIDERS.find(
130-
(provider) => provider.id === choice.provider
131-
)
132-
if (!provider) throw new Error('Unknown shortcut provider.')
133130
if (
134131
choice.enabled &&
135-
!(await chrome.permissions.contains({ origins: [...provider.origins] }))
132+
!(await chrome.permissions.contains({ origins: [...choice.provider.origins] }))
136133
)
137134
throw new Error('Search site access has not been granted.')
138-
preferences[choice.provider] = choice.enabled
135+
preferences[choice.provider.id] = choice.enabled
139136
}
140137
const rules = SEARCH_RECOVERY_PROVIDERS.filter(
141138
(provider) => preferences[provider.id]
@@ -190,7 +187,7 @@ export function registerSearchRecovery() {
190187
return
191188
const provider = SEARCH_RECOVERY_PROVIDERS.find((provider) => provider.id === message.provider)
192189
if (!provider) return
193-
void update({ provider: provider.id, enabled: message.enabled }).then(
190+
void update({ provider, enabled: message.enabled }).then(
194191
() => respond({ ok: true }),
195192
() => respond({ ok: false })
196193
)

0 commit comments

Comments
 (0)