Skip to content

Commit 1ce09a2

Browse files
committed
improvement(release): harden favicons, sim links, and the extension from a pre-release audit
1 parent 3e23d39 commit 1ce09a2

30 files changed

Lines changed: 285 additions & 146 deletions

File tree

‎apps/docs/content/docs/search/meta.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"find-content",
77
"ask-questions",
88
"sim-links",
9-
"people",
109
"troubleshooting",
1110
"---Connect sources---",
1211
"connect-your-account",

‎apps/docs/content/docs/search/people.mdx‎

Lines changed: 0 additions & 43 deletions
This file was deleted.

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Short sim/ links that take everyone in your organization to the rig
55

66
Sim Links are short names for pages your team opens often, like `sim/handbook` or `sim/okta`. Every member of your organization can use them, with or without a Search seat.
77

8-
Open **Sim Links** in the organization sidebar to see your organization's links. Search by name, destination, or description, or use **Sort → Opens** to see the most used links.
8+
Open **Sim Links** in the organization sidebar to see your organization's links. Search by name, destination, or description, or use **Sort → Opens** to see the 50 most opened links.
99

1010
## Open a link
1111

@@ -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-
With the Sim Chrome extension, type `sim/handbook` in the address bar. To find a link, type `sim`, press **Space**, and start typing its name: matching links appear as you type, most opened first.
18+
With the Sim Chrome extension, type `sim/handbook` in the address bar. To find a link, type `sim`, press **Space**, and start typing its name: matching links appear as you type, most opened first. Unlisted links aren't suggested.
1919

2020
Without the extension, add Sim as a Chrome site search shortcut to type `sim handbook` instead:
2121

@@ -52,7 +52,7 @@ Values fill each `{*}` in order, and the last `{*}` takes the rest. A link with
5252
By default, any member can edit or delete a link. The link's creator and organization admins can change two options under **Access**:
5353

5454
- **Hide from the Sim Links page**: only the creator and admins see the link in the list, marked **Unlisted**. Anyone with the name can still open it.
55-
- **Only you and admins can edit**: only the creator and admins can edit or delete the link, marked **Restricted**.
55+
- **Only the creator and admins can edit**: only the creator and admins can edit or delete the link, marked **Restricted**.
5656

5757
Changes to links are recorded in your organization's [audit logs](/platform/enterprise/audit-logs).
5858

‎apps/extension/DESIGN.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,13 @@ other workspace resources or browser-control tools.
6060
- Other HTTP(S) origins are optional grants requested for the current site from a
6161
user gesture. Each grant can be revoked. Keeping the panel open is not a grant
6262
to read every subsequently selected tab.
63-
- No history, bookmarks, cookie API, debugger, network interception, native
64-
messaging, external messaging, or arbitrary remote code execution.
63+
- No history, bookmarks, cookie API, debugger, native messaging, external messaging,
64+
or arbitrary remote code execution. The only network interception is the static
65+
`sim/` redirect rule above.
66+
- The only background network request is the `sim` address-bar keyword: after
67+
`sim` and Space, the typed link name (never a page, URL, or search containing a
68+
space) is sent to the configured Sim origin with the signed-in session to suggest
69+
matching Sim links. Unlisted links are never suggested.
6570
- No always-running page reader. Extraction is tied to an explicit attachment action.
6671
- Page bytes are not written to extension local/sync storage. Temporary context-menu
6772
handoff uses trusted-context session storage, expiration and navigation cleanup.

‎apps/extension/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ SIM_EXTENSION_DEV=1 SIM_EXTENSION_ORIGIN=http://localhost:3000 bun run --cwd app
2727

2828
- Required capabilities: `activeTab`, `scripting`, `sidePanel`, `storage`, `contextMenus`, `declarativeNetRequestWithHostAccess`, and access to the configured Sim origin and to `http(s)://sim/*`.
2929
- Typing `sim/<name>` in the address bar opens the organization's [Sim link](https://docs.sim.ai/search/sim-links). A single static redirect rule rewrites top-level navigations to `http(s)://sim/*` to `<Sim origin>/sim/*` with the path and query unchanged. The `sim` host permission exists only for that rule: every navigation to that host is redirected before it loads, and no other request is matched or observed. An organization that already serves an intranet host named `sim` should not install this build, since its links would be redirected to Sim.
30-
- The `sim` address-bar keyword (`sim`, then Space) lists matching Sim links while you type and opens what you enter through the same resolver. It is the extension's only network request: the background worker sends the typed text, and nothing else, to `<Sim origin>/api/sim-links/suggestions` with the signed-in session, and the extension's content security policy allows connections to that origin alone. Unlisted links are never suggested.
30+
- The `sim` address-bar keyword (`sim`, then Space) lists matching Sim links while you type and opens what you enter through the same resolver. It is the extension's only network request: the background worker sends the typed link name, and nothing else (input containing a space is treated as a search and sends nothing), to `<Sim origin>/api/sim-links/suggestions` with the signed-in session, and the extension's content security policy allows connections to that origin alone. Unlisted links are never suggested.
3131
- Optional HTTP/HTTPS site access is requested only by a menu action naming that site. Chrome's `activeTab` access is temporary and does not follow arbitrary new sites.
3232
- Page capture runs on demand in Chrome's isolated world. It excludes editable fields, inputs, hidden elements, scripts, and embedded documents. It reads only the top frame. PDF viewers and canvas documents may have no accessible text.
3333
- Extraction stops at 50,000 text characters, 150,000 serialized UTF-8 bytes, 15,000 visited nodes, or 150ms. The composer exposes truncation.

‎apps/extension/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@sim/extension",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"private": true,
55
"type": "module",
66
"license": "Apache-2.0",

‎apps/extension/scripts/build.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@ await writeFile(
107107
{
108108
manifest_version: 3,
109109
name: development ? 'Sim (Development)' : 'Sim',
110-
version: '0.1.1',
110+
version: '0.1.2',
111111
description:
112-
'Ask Sim about the page you are reading, with your existing Sim conversations and tools.',
112+
"Ask Sim about the page you are reading, and open your organization's sim/ links from the address bar.",
113113
minimum_chrome_version: '120',
114114
permissions: [
115115
'activeTab',

‎apps/extension/src/omnibox.ts‎

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import { SIM_ORIGIN } from '@/config'
99

1010
const logger = createLogger('BrowserExtensionOmnibox')
1111

12-
/** Long enough to skip the requests a fast typist's intermediate keystrokes would make. */
13-
const SUGGEST_DELAY_MS = 120
12+
/** Longer than the gap between a typist's keystrokes, so a name is looked up once it settles. */
13+
const SUGGEST_DELAY_MS = 220
14+
15+
/** Link names are at most this long, so nothing longer can match. */
16+
const MAX_NAME_LENGTH = 64
1417

1518
/** A Sim link as suggested: its name, and what it shows beside the name. */
1619
interface SuggestedLink {
@@ -20,20 +23,37 @@ interface SuggestedLink {
2023

2124
/**
2225
* What was typed after the `sim` keyword, split where the name ends: `sim/jira/ABC-1?tab=all`
23-
* (the `sim/` is optional) has the name `jira` and the rest `/ABC-1?tab=all`. Only the space
24-
* before the name and the prefix are dropped; values and the query stay exactly as typed.
26+
* (the `sim/` is optional) has the name `jira` and the rest `/ABC-1?tab=all`. Surrounding spaces,
27+
* the prefix, leading slashes, and any `#fragment` are dropped, as the address bar would; values
28+
* and the query otherwise stay exactly as typed.
2529
*/
2630
function readTyped(text: string): { name: string; rest: string } {
27-
const typed = text.trimStart().replace(/^sim\//i, '')
31+
const typed = text
32+
.trim()
33+
.replace(/^sim\//i, '')
34+
.replace(/^\/+/, '')
35+
.replace(/#.*$/, '')
2836
const nameEnd = typed.search(/[/?]/)
2937
return nameEnd === -1
3038
? { name: typed, rest: '' }
3139
: { name: typed.slice(0, nameEnd), rest: typed.slice(nameEnd) }
3240
}
3341

34-
/** Suggestion text is XML, where these five characters are markup. */
42+
/**
43+
* Suggestion text is XML, where these five characters are markup and control characters are not
44+
* allowed even as references, so one bad description can't make Chrome drop every suggestion.
45+
*/
3546
function escapeXml(text: string): string {
36-
return text.replace(/[&<>"']/g, (character) => `&#${character.charCodeAt(0)};`)
47+
return text
48+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, '')
49+
.replace(/[&<>"']/g, (character) => `&#${character.charCodeAt(0)};`)
50+
}
51+
52+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g
53+
54+
/** `encodeURIComponent`, except a lone surrogate (which it refuses) is dropped. */
55+
function encodeSegment(segment: string): string {
56+
return encodeURIComponent(segment.replace(LONE_SURROGATE, ''))
3757
}
3858

3959
/**
@@ -45,17 +65,20 @@ function simLinkUrl(text: string): string {
4565
const queryStart = rest.indexOf('?')
4666
const path = `${name}${queryStart === -1 ? rest : rest.slice(0, queryStart)}`.replace(/\/$/, '')
4767
const query = queryStart === -1 ? '' : new URLSearchParams(rest.slice(queryStart + 1)).toString()
48-
const segments = path ? path.split('/').map(encodeURIComponent) : []
68+
const segments = path ? path.split('/').map(encodeSegment) : []
4969
return `${SIM_ORIGIN}/${['sim', ...segments].join('/')}${query ? `?${query}` : ''}`
5070
}
5171

52-
async function fetchSuggestedLinks(name: string, signal: AbortSignal): Promise<SuggestedLink[]> {
72+
/** The links Sim suggests for `name`, or `null` when it couldn't answer (signed out, rate limited). */
73+
async function fetchSuggestedLinks(
74+
name: string,
75+
signal: AbortSignal
76+
): Promise<SuggestedLink[] | null> {
5377
const response = await fetch(
5478
`${SIM_ORIGIN}${SIM_LINK_SUGGESTIONS_PATH}?${new URLSearchParams({ q: name })}`,
5579
{ credentials: 'include', signal }
5680
)
57-
// Signed out, or Sim links are unavailable: the typed link still opens through the resolver.
58-
if (!response.ok) return []
81+
if (!response.ok) return null
5982
const { links } = simLinkSuggestionsSchema.parse(await response.json())
6083
return links.map((link) => ({ name: link.name, detail: link.description ?? link.url }))
6184
}
@@ -91,6 +114,11 @@ export function registerSimLinkOmnibox() {
91114
chrome.omnibox.setDefaultSuggestion({
92115
description: `Open <match>sim/${escapeXml(`${name}${rest}`)}</match>`,
93116
})
117+
// A name can't hold a space, so `sim card prices` is a search, not a link: ask Sim nothing.
118+
if (/\s/.test(name) || name.length > MAX_NAME_LENGTH) {
119+
suggest([])
120+
return
121+
}
94122
const key = name.toLowerCase()
95123
const known = linksByName.get(key)
96124
if (known) {
@@ -102,9 +130,11 @@ export function registerSimLinkOmnibox() {
102130
void (async () => {
103131
await sleep(SUGGEST_DELAY_MS)
104132
if (controller.signal.aborted) return
133+
// Signed out or rate limited: the typed link still opens through the resolver, and the name
134+
// is asked about again on the next keystroke rather than remembered as having no links.
105135
const links = await fetchSuggestedLinks(key, controller.signal)
106-
linksByName.set(key, links)
107-
if (!controller.signal.aborted) suggest(toSuggestions(links, rest))
136+
if (links) linksByName.set(key, links)
137+
if (!controller.signal.aborted) suggest(toSuggestions(links ?? [], rest))
108138
})().catch(() => {
109139
if (!controller.signal.aborted) logger.warn('Could not load Sim link suggestions')
110140
})

‎apps/sim/app/api/favicon/route.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ export const GET = defineInternalBinaryRoute({
1717
operation: getFavicon.operation,
1818
rateLimit: internalRateLimits.user({
1919
bucketName: 'favicon',
20-
/** A page can render dozens of icons at once; the browser caches each one after its first load. */
21-
config: { maxTokens: 300, refillRate: 300, refillIntervalMs: 60_000 },
20+
// A table can show hundreds of sites at once; requests that reach the icon providers are
21+
// budgeted separately (favicon use case), so this only bounds cheap cached reads.
22+
config: { maxTokens: 1200, refillRate: 1200, refillIntervalMs: 60_000 },
2223
}),
2324
errorPolicy: internalOrchestrationErrorPolicy,
2425
mapInput: ({ query }) => ({ host: query.host }),

‎apps/sim/app/o/[organizationId]/layout.tsx‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,16 @@ export default async function OrganizationLayout({
6565
if (!context) {
6666
return <OrganizationAccessDenied />
6767
}
68-
const peopleEnabled = await canBrowsePeople(organizationId, context.viewer.role)
69-
if (!context.mothershipAvailable && !context.searchAccess.memberScoped && !peopleEnabled)
68+
const [peopleEnabled, simLinksEnabled] = await Promise.all([
69+
canBrowsePeople(organizationId, context.viewer.role),
70+
isSimLinksEnabled(organizationId),
71+
])
72+
if (
73+
!context.mothershipAvailable &&
74+
!context.searchAccess.memberScoped &&
75+
!peopleEnabled &&
76+
!simLinksEnabled
77+
)
7078
redirect(WORKSPACE_SETTINGS_PATH)
7179

7280
const [
@@ -78,7 +86,6 @@ export default async function OrganizationLayout({
7886
workflowTestsEnabled,
7987
changelogEnabled,
8088
projectsEnabled,
81-
simLinksEnabled,
8289
] = await Promise.all([
8390
prefetchOrganizationSidebar(
8491
queryClient,
@@ -96,7 +103,6 @@ export default async function OrganizationLayout({
96103
isWorkflowTestsEnabled(organizationId),
97104
isChangelogEnabled(organizationId),
98105
isProjectsEnabled(),
99-
isSimLinksEnabled(organizationId),
100106
])
101107
const initialSidebarCollapsed = cookieStore.get('sidebar_collapsed')?.value === '1'
102108

0 commit comments

Comments
 (0)