Skip to content

Commit b57d3c1

Browse files
committed
Merge remote-tracking branch 'origin/staging' into feat/search-multiple-accounts-per-provider
# Conflicts: # apps/sim/lib/assistant-actions/tool-permissions.integration.ts
2 parents 67788e3 + ab2a5c0 commit b57d3c1

92 files changed

Lines changed: 4606 additions & 2706 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<p align="center">
99
<a href="https://deepwiki.com/simstudioai/sim" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Ask-DeepWiki-E6E6E6?labelColor=C3C3C3&color=E6E6E6" alt="Ask DeepWiki"></a>
10-
<a href="https://cursor.com/link/prompt?text=Help%20me%20set%20up%20Sim%20locally.%20Follow%20these%20steps%3A%0A%0A1.%20First%2C%20verify%20Docker%20is%20installed%20and%20running%3A%0A%20%20%20docker%20--version%0A%20%20%20docker%20info%0A%0A2.%20Clone%20the%20repository%3A%0A%20%20%20git%20clone%20https%3A%2F%2Fgithub.com%2Fsimstudioai%2Fsim.git%0A%20%20%20cd%20sim%0A%0A3.%20Generate%20required%20secrets%20%28the%20stack%20will%20not%20start%20without%20them%29%3A%0A%20%20%20cat%20%3E%20.env%20%3C%3C%20EOF%0A%20%20%20BETTER_AUTH_SECRET%3D%24%28openssl%20rand%20-hex%2032%29%0A%20%20%20ENCRYPTION_KEY%3D%24%28openssl%20rand%20-hex%2032%29%0A%20%20%20INTERNAL_API_SECRET%3D%24%28openssl%20rand%20-hex%2032%29%0A%20%20%20CRON_SECRET%3D%24%28openssl%20rand%20-hex%2032%29%0A%20%20%20POSTGRES_PASSWORD%3D%24%28openssl%20rand%20-hex%2024%29%0A%20%20%20EOF%0A%0A4.%20Start%20the%20services%20with%20Docker%20Compose%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20up%20-d%0A%0A4.%20Wait%20for%20all%20containers%20to%20be%20healthy%20(this%20may%20take%201-2%20minutes)%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20ps%0A%0A5.%20Verify%20the%20app%20is%20accessible%20at%20http%3A%2F%2Flocalhost%3A3000%0A%0AIf%20there%20are%20any%20errors%2C%20help%20me%20troubleshoot%20them.%20Common%20issues%3A%0A-%20Port%203000%20or%203002%20already%20in%20use%0A-%20Docker%20not%20running%0A-%20Insufficient%20memory%20(needs%2012GB%2B%20RAM)%0A%0AFor%20local%20AI%20models%20with%20Ollama%2C%20use%20this%20instead%20of%20step%203%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.ollama.yml%20--profile%20setup%20up%20-d"><img src="https://img.shields.io/badge/Set%20Up%20with-Cursor-E6E6E6?logo=cursor&logoColor=1A1A1A&labelColor=C3C3C3&color=E6E6E6" alt="Set Up with Cursor"></a>
10+
<a href="https://cursor.com/link/prompt?text=Help%20me%20set%20up%20Sim%20locally.%20Follow%20these%20steps%3A%0A%0A1.%20First%2C%20verify%20Docker%20is%20installed%20and%20running%3A%0A%20%20%20docker%20--version%0A%20%20%20docker%20info%0A%0A2.%20Clone%20the%20repository%3A%0A%20%20%20git%20clone%20https%3A%2F%2Fgithub.com%2Fsimstudioai%2Fsim.git%0A%20%20%20cd%20sim%0A%0A3.%20Generate%20required%20secrets%20%28the%20stack%20will%20not%20start%20without%20them%29%3A%0A%20%20%20cat%20%3E%20.env%20%3C%3C%20EOF%0A%20%20%20BETTER_AUTH_SECRET%3D%24%28openssl%20rand%20-hex%2032%29%0A%20%20%20ENCRYPTION_KEY%3D%24%28openssl%20rand%20-hex%2032%29%0A%20%20%20INTERNAL_API_SECRET%3D%24%28openssl%20rand%20-hex%2032%29%0A%20%20%20CRON_SECRET%3D%24%28openssl%20rand%20-hex%2032%29%0A%20%20%20POSTGRES_PASSWORD%3D%24%28openssl%20rand%20-hex%2024%29%0A%20%20%20EOF%0A%0A4.%20Start%20the%20services%20with%20Docker%20Compose%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20up%20-d%0A%0A5.%20Wait%20for%20all%20containers%20to%20be%20healthy%20%28this%20may%20take%201-2%20minutes%29%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20ps%0A%0A6.%20Verify%20the%20app%20is%20accessible%20at%20http%3A%2F%2Flocalhost%3A3000%0A%0AIf%20there%20are%20any%20errors%2C%20help%20me%20troubleshoot%20them.%20Common%20issues%3A%0A-%20Port%203000%20or%203002%20already%20in%20use%0A-%20Docker%20not%20running%0A-%20Insufficient%20memory%20%28needs%2012GB%2B%20RAM%29%0A%0AFor%20local%20AI%20models%20with%20Ollama%2C%20use%20one%20of%20these%20instead%20of%20step%204%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.ollama.yml%20--profile%20gpu%20--profile%20setup%20up%20-d%0A%20%20%20docker%20compose%20-f%20docker-compose.ollama.yml%20--profile%20cpu%20--profile%20setup%20up%20-d%0AThen%20check%20status%20in%20step%205%20with%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.ollama.yml%20ps"><img src="https://img.shields.io/badge/Set%20Up%20with-Cursor-E6E6E6?logo=cursor&logoColor=1A1A1A&labelColor=C3C3C3&color=E6E6E6" alt="Set Up with Cursor"></a>
1111
</p>
1212

1313
<p align="center">
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
3+
vi.mock('electron', () => import('@/test/electron-mock'))
4+
5+
import { settingsRoute } from '@/main/app-routes'
6+
7+
describe('settingsRoute', () => {
8+
it('opens the settings of the organization the user was last in', () => {
9+
expect(settingsRoute('/o/org-1/search?q=x')).toBe('/o/org-1/settings/general')
10+
})
11+
12+
it('keeps the desktop settings of the workspace the user was last in', () => {
13+
expect(settingsRoute('/workspace/ws-1/home')).toBe('/workspace/ws-1/settings/desktop')
14+
})
15+
16+
it.each([
17+
['the workspace picker', '/workspace'],
18+
['the app entry', '/home'],
19+
['no saved route', undefined],
20+
['a route that only starts like an organization', '/oauth/o/org-1'],
21+
['an unsafe route', '//evil.example/o/org-1'],
22+
])('resolves settings through the web app from %s', (_label, lastRoute) => {
23+
expect(settingsRoute(lastRoute)).toBe('/workspace?redirect=settings')
24+
})
25+
})

‎apps/desktop/src/main/app-routes.ts‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ import { isSafeInternalPath } from '@/main/config'
1616
*/
1717
export const APP_ENTRY_ROUTE = '/home'
1818

19+
/**
20+
* Settings for a viewer whose last route names no workspace or organization. The web
21+
* app resolves it to their most recent workspace, or to their organization's settings.
22+
*/
23+
const SETTINGS_ENTRY_ROUTE = '/workspace?redirect=settings'
24+
1925
/** Workspace id from the last visited route, or null when it carries none. */
2026
function workspaceIdFromRoute(lastRoute: string | undefined): string | null {
2127
if (isSafeInternalPath(lastRoute)) {
@@ -27,6 +33,16 @@ function workspaceIdFromRoute(lastRoute: string | undefined): string | null {
2733
return null
2834
}
2935

36+
function organizationIdFromRoute(lastRoute: string | undefined): string | null {
37+
if (isSafeInternalPath(lastRoute)) {
38+
const match = /^\/o\/([^/?#]+)/.exec(lastRoute)
39+
if (match) {
40+
return match[1]
41+
}
42+
}
43+
return null
44+
}
45+
3046
/**
3147
* Route for "New Chat": the home (chat) surface of the workspace the user was
3248
* last in, falling back to the app entry when the last route carries no
@@ -38,10 +54,14 @@ export function newChatRoute(lastRoute: string | undefined): string {
3854
}
3955

4056
/**
41-
* Route for "Settings…": the Sim app's settings surface for the workspace the
42-
* user was last in, falling back to the app entry.
57+
* Route for "Settings…": the settings of the workspace or organization the user
58+
* was last in. Never the app entry, which lands on a page without settings.
4359
*/
4460
export function settingsRoute(lastRoute: string | undefined): string {
4561
const workspaceId = workspaceIdFromRoute(lastRoute)
46-
return workspaceId ? `/workspace/${workspaceId}/settings/desktop` : APP_ENTRY_ROUTE
62+
if (workspaceId) {
63+
return `/workspace/${workspaceId}/settings/desktop`
64+
}
65+
const organizationId = organizationIdFromRoute(lastRoute)
66+
return organizationId ? `/o/${organizationId}/settings/general` : SETTINGS_ENTRY_ROUTE
4767
}

‎apps/desktop/src/main/handoff.test.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { get as httpGet } from 'node:http'
2+
import type { BrowserWindow } from 'electron'
23
import { describe, expect, it, vi } from 'vitest'
34

45
vi.mock('electron', () => import('@/test/electron-mock'))
56

67
import {
8+
createAuthFlow,
79
createHandoffManager,
810
type HandoffCallback,
911
type HandoffCallbacks,
@@ -161,6 +163,31 @@ describe('createHandoffManager', () => {
161163
})
162164
})
163165

166+
describe('login handoff landing', () => {
167+
it('lands on the app entry after redeeming the login token', async () => {
168+
const win = {
169+
isDestroyed: () => false,
170+
loadURL: vi.fn(async () => {}),
171+
show: vi.fn(),
172+
focus: vi.fn(),
173+
webContents: {
174+
getURL: () => 'https://sim.ai/login',
175+
executeJavaScript: vi.fn(async () => 200),
176+
},
177+
}
178+
const flow = createAuthFlow({
179+
handoff: { consume: () => true } as unknown as ReturnType<typeof createHandoffManager>,
180+
origin: () => 'https://sim.ai',
181+
events: makeEvents(),
182+
ensureMainWindow: async () => win as unknown as BrowserWindow,
183+
})
184+
185+
await flow.handleCallback({ token: VALID_TOKEN, state: _VALID_STATE })
186+
187+
expect(win.loadURL).toHaveBeenCalledExactlyOnceWith('https://sim.ai/home')
188+
})
189+
})
190+
164191
describe('connect handoff account pinning', () => {
165192
it('keeps a source request correlated across the browser and native completion', async () => {
166193
const deps = makeDeps()

‎apps/desktop/src/main/handoff.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getErrorMessage } from '@sim/utils/errors'
66
import { generateShortId } from '@sim/utils/id'
77
import type { BrowserWindow } from 'electron'
88
import { app } from 'electron'
9+
import { APP_ENTRY_ROUTE } from '@/main/app-routes'
910
import { showShellDialog } from '@/main/dialogs'
1011
import type { EventRecorder } from '@/main/observability'
1112

@@ -451,7 +452,7 @@ export interface AuthFlow {
451452

452453
/**
453454
* Orchestrates the login handoff: opening the system browser, consuming the
454-
* loopback callback, redeeming the token, and navigating to the workspace. A
455+
* loopback callback, redeeming the token, and navigating to the app entry. A
455456
* failed or expired callback never leaves a partial session — the window lands
456457
* back on /login.
457458
*/
@@ -521,7 +522,7 @@ export function createAuthFlow(deps: AuthFlowDeps): AuthFlow {
521522
}
522523
deps.events.record('handoff_redeem_ok')
523524
try {
524-
await win.loadURL(`${origin}/workspace`)
525+
await win.loadURL(`${origin}${APP_ENTRY_ROUTE}`)
525526
} catch {}
526527
win.show()
527528
win.focus()

‎apps/docs/content/docs/introduction/index.mdx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,15 @@ For tools outside the catalog, connect an [external MCP server](/agents/mcp) or
5555

5656
The core source is available on [GitHub](https://github.com/simstudioai/sim) under the Apache 2.0 license. Enterprise features use a separate license that requires an active Enterprise subscription for production use. See [self-hosting](/platform/self-hosting) for installation requirements and configuration.
5757

58+
## Security and compliance
59+
60+
Sim is SOC 2 Type II compliant. Find reports and security details in the [trust center](https://trust.sim.ai/).
61+
5862
## Related docs
5963

6064
- [Getting started](/getting-started): Build and run a simple summarizer.
6165
- [Chat](/chat): Build and manage workspace resources conversationally.
6266
- [Workflows](/workflows): Blocks, connections, triggers, and execution.
6367
- [Quick reference](/quick-reference): Look up builder actions.
68+
- [Pricing](https://www.sim.ai/pricing): Compare Sim Cloud plans.
69+
- [Comparisons](https://www.sim.ai/comparisons): Sim vs Microsoft Copilot Studio, n8n, Zapier, Make, Dust, and other AI agent platforms.

‎apps/sim/app/(landing)/pricing/pricing.tsx‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { INTEGRATION_COUNT_LABEL } from '@sim/deployment-config/integration-claims'
2+
import { CREDITS_PER_DOLLAR } from '@/lib/billing/constants'
23
import { PricingPlans } from '@/app/(landing)/pricing/components/pricing-plans'
34
import { PricingStructuredData } from '@/app/(landing)/pricing/components/pricing-structured-data'
45

@@ -54,6 +55,19 @@ export default function Pricing() {
5455
className='mx-auto flex w-full max-w-[1728px] flex-col gap-7 px-10 pt-8 max-md:px-7 max-lg:px-8 max-xl:px-9'
5556
>
5657
<PricingPlans heading={PRICING_HEADING} />
58+
<p className='mx-auto max-w-[720px] text-balance text-center text-[var(--text-body)] text-small'>
59+
Sim is open source under the Apache 2.0 license, so you can self-host it for free with
60+
Docker Compose or Kubernetes. Enterprise features need a license.{' '}
61+
<a
62+
href='https://docs.sim.ai/platform/self-hosting'
63+
target='_blank'
64+
rel='noopener noreferrer'
65+
className='underline underline-offset-2'
66+
>
67+
Read the self-hosting guide
68+
</a>
69+
. On Sim Cloud, one credit is worth ${1 / CREDITS_PER_DOLLAR}.
70+
</p>
5771
</section>
5872
</main>
5973
)

‎apps/sim/app/api/workspaces/route.test.ts‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import {
1616
} from '@sim/testing/mocks/workspaces-policy.mock'
1717
import { beforeEach, describe, expect, it, vi } from 'vitest'
1818

19-
const { mockCreateWorkspace } = vi.hoisted(() => ({
19+
const { mockCreateWorkspace, mockResolveRolledOutOrganizationId } = vi.hoisted(() => ({
2020
mockCreateWorkspace: vi.fn(),
21+
mockResolveRolledOutOrganizationId: vi.fn(),
2122
}))
2223

2324
vi.mock('@/lib/auth/session-response', () => ({
@@ -32,6 +33,10 @@ vi.mock('@/lib/workspaces/list', () => ({
3233
listWorkspacesForViewer: vi.fn(),
3334
}))
3435

36+
vi.mock('@/lib/navigation/resolve-app-entry', () => ({
37+
resolveRolledOutOrganizationId: mockResolveRolledOutOrganizationId,
38+
}))
39+
3540
vi.mock('@/lib/posthog/server', () => posthogServerMock)
3641

3742
vi.mock('@sim/audit', () => auditMock)
@@ -99,3 +104,43 @@ describe('POST /api/workspaces capability refusal', () => {
99104
await expect(response.json()).resolves.toEqual({ error: 'Unauthorized' })
100105
})
101106
})
107+
108+
describe('GET /api/workspaces blocked viewer forwarding', () => {
109+
const blockedList = {
110+
workspaces: [],
111+
lastActiveWorkspaceId: null,
112+
pinnedWorkspaceIds: [],
113+
creationPolicy: { canCreate: false },
114+
}
115+
const routeContext = { params: Promise.resolve({}) }
116+
const listRequest = () =>
117+
createMockRequest('GET', undefined, {}, 'http://localhost/api/workspaces')
118+
119+
beforeEach(() => {
120+
mockGetSession.mockResolvedValue({
121+
user: { id: 'user-1', name: 'A', email: 'a@example.com' },
122+
})
123+
vi.mocked(listWorkspacesForViewer).mockResolvedValue(
124+
blockedList as unknown as Awaited<ReturnType<typeof listWorkspacesForViewer>>
125+
)
126+
})
127+
128+
it('names the organization that can serve a viewer with nothing to pick', async () => {
129+
mockResolveRolledOutOrganizationId.mockResolvedValue('org-1')
130+
const response = await GET(listRequest(), routeContext)
131+
expect((await response.json()).forwardOrganizationId).toBe('org-1')
132+
})
133+
134+
it('leaves the blocked picker in place when no organization can serve them', async () => {
135+
mockResolveRolledOutOrganizationId.mockResolvedValue(null)
136+
const response = await GET(listRequest(), routeContext)
137+
expect(await response.json()).not.toHaveProperty('forwardOrganizationId')
138+
})
139+
140+
it('still returns the list when the organization lookup fails', async () => {
141+
mockResolveRolledOutOrganizationId.mockRejectedValue(new Error('down'))
142+
const response = await GET(listRequest(), routeContext)
143+
expect(response.status).toBe(200)
144+
expect(await response.json()).not.toHaveProperty('forwardOrganizationId')
145+
})
146+
})

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
22
import { db } from '@sim/db'
33
import { type WorkspaceMode, workflow } from '@sim/db/schema'
44
import { createLogger } from '@sim/logger'
5-
import { getPostgresErrorCode } from '@sim/utils/errors'
5+
import { getErrorMessage, getPostgresErrorCode } from '@sim/utils/errors'
66
import { and, eq, isNull } from 'drizzle-orm'
77
import { type NextRequest, NextResponse } from 'next/server'
88
import { listWorkspacesQuerySchema } from '@/lib/api/contracts'
@@ -11,6 +11,7 @@ import { parseRequest } from '@/lib/api/server'
1111
import { getSession } from '@/lib/auth'
1212
import { getActiveOrganizationId } from '@/lib/auth/session-response'
1313
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
14+
import { resolveRolledOutOrganizationId } from '@/lib/navigation/resolve-app-entry'
1415
import { capabilityRefusalResponse } from '@/lib/permission-groups/capability-response'
1516
import { captureServerEvent } from '@/lib/posthog/server'
1617
import { createWorkspace } from '@/lib/workspaces/create'
@@ -24,6 +25,20 @@ import {
2425

2526
const logger = createLogger('Workspaces')
2627

28+
/** A failed lookup leaves the viewer on the blocked picker rather than failing the list. */
29+
async function resolveForwardOrganizationId(session: {
30+
user: { id: string }
31+
}): Promise<string | undefined> {
32+
try {
33+
return (await resolveRolledOutOrganizationId(session)) ?? undefined
34+
} catch (error) {
35+
logger.warn('Could not resolve an organization to forward a blocked viewer to', {
36+
error: getErrorMessage(error),
37+
})
38+
return undefined
39+
}
40+
}
41+
2742
// Get all workspaces for the current user
2843
export const GET = withRouteHandler(async (request: Request) => {
2944
const session = await getSession()
@@ -58,6 +73,7 @@ export const GET = withRouteHandler(async (request: Request) => {
5873
lastActiveWorkspaceId,
5974
pinnedWorkspaceIds,
6075
creationPolicy,
76+
forwardOrganizationId: await resolveForwardOrganizationId(session),
6177
})
6278
}
6379

‎apps/sim/app/o/[organizationId]/_redesign/redesign-host.tsx‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,12 @@ interface RedesignSurfaceProps {
1818
export function RedesignSurface({ children }: RedesignSurfaceProps) {
1919
return <PlatformSurface theme='system'>{children}</PlatformSurface>
2020
}
21+
22+
/** Fills the app's content slot, and the grid stretches the surface so its canvas reaches the end. */
23+
export function RedesignPageSurface({ children }: RedesignSurfaceProps) {
24+
return (
25+
<div className='grid h-full min-h-0 min-w-0 flex-1 grid-cols-[minmax(0,1fr)] overflow-y-auto'>
26+
<RedesignSurface>{children}</RedesignSurface>
27+
</div>
28+
)
29+
}

0 commit comments

Comments
 (0)