Skip to content

Commit ef7c8e2

Browse files
feat(platform): settings permissions, admin, billing attribution (#5545)
* fix(invites): preserve active organization for external access Keep organization activation server-owned so failed membership checks cannot clear a valid session context. * feat(admin, billing, settings): cleanup settings visibility, billing actor resolution, new admin routes * address comments * chore(db): reset pending migrations before staging merge Remove locally generated migrations so they can be regenerated against the latest staging schema without preserving stale snapshots or numbering. * regen migrations * address comments * chore(db): reset generated migrations before staging merge Remove this branch's generated migrations so they can be regenerated against the latest staging schema with fresh numbering. * upgrade global work * fix lint * address comments * legacy callbacks correctness * address comments * update * guardrail attribution
1 parent 73f33db commit ef7c8e2

522 files changed

Lines changed: 70182 additions & 8415 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.

apps/docs/content/docs/de/api-reference/authentication.mdx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,23 @@ To access the Sim API, you need an API key. Sim supports two types of API keys
1212

1313
| | **Personal Keys** | **Workspace Keys** |
1414
| --- | --- | --- |
15-
| **Billed to** | Your individual account | Workspace owner |
15+
| **Billing** | Workspace payer for workspace-hosted usage | Workspace payer |
1616
| **Scope** | Across workspaces you have access to | Shared across the workspace |
1717
| **Managed by** | Each user individually | Workspace admins |
1818
| **Permissions** | Must be enabled at workspace level | Require admin permissions |
1919

2020
<Callout type="info">
21-
Workspace admins can disable personal API key usage for their workspace. If disabled, only workspace keys can be used.
21+
Personal keys identify the user making a request; they do not select who pays.
22+
Hosted usage is billed to the workspace's organization or personal billing
23+
account and, for organizations, is attributed to the actor's member cap.
24+
Workspace admins can disable personal API key usage for their workspace. If
25+
disabled, only workspace keys can be used.
2226
</Callout>
2327

2428
## Generating API Keys
2529

26-
To generate a key, open the Sim dashboard and navigate to **Settings**, then go to **Sim Keys** and click **Create**.
30+
To generate a personal key, open **Account settings****Sim API keys**. Workspace
31+
administrators can create shared keys from **Workspace settings****Sim API keys**.
2732

2833
<Callout type="warn">
2934
API keys are only shown once when generated. Store your key securely — you will not be able to view it again.

apps/docs/content/docs/en/api-reference/authentication.mdx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,23 @@ To access the Sim API, you need an API key. Sim supports two types of API keys
1212

1313
| | **Personal Keys** | **Workspace Keys** |
1414
| --- | --- | --- |
15-
| **Billed to** | Your individual account | Workspace owner |
15+
| **Billing** | Workspace payer for workspace-hosted usage | Workspace payer |
1616
| **Scope** | Across workspaces you have access to | Shared across the workspace |
1717
| **Managed by** | Each user individually | Workspace admins |
1818
| **Permissions** | Must be enabled at workspace level | Require admin permissions |
1919

2020
<Callout type="info">
21-
Workspace admins can disable personal API key usage for their workspace. If disabled, only workspace keys can be used.
21+
Personal keys identify the user making a request; they do not select who pays.
22+
Hosted usage is billed to the workspace's organization or personal billing
23+
account and, for organizations, is attributed to the actor's member cap.
24+
Workspace admins can disable personal API key usage for their workspace. If
25+
disabled, only workspace keys can be used.
2226
</Callout>
2327

2428
## Generating API Keys
2529

26-
To generate a key, open the Sim dashboard and navigate to **Settings**, then go to **Sim Keys** and click **Create**.
30+
To generate a personal key, open **Account settings****Sim API keys**. Workspace
31+
administrators can create shared keys from **Workspace settings****Sim API keys**.
2732

2833
<Callout type="warn">
2934
API keys are only shown once when generated. Store your key securely — you will not be able to view it again.

apps/docs/content/docs/en/platform/costs.mdx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,9 @@ By default, your usage is capped at the credits included in your plan. To allow
305305
- **Disable On-Demand**: Resets your usage limit back to the plan's included amount (only available if your current usage hasn't already exceeded it).
306306

307307
<Callout type="info">
308-
On-demand billing is managed by workspace admins for team plans. Non-admin team members cannot toggle on-demand billing.
308+
On-demand billing for team plans is managed by organization owners and
309+
administrators. Workspace admin permission alone does not grant billing
310+
authority.
309311
</Callout>
310312

311313
## Plan Limits
@@ -371,7 +373,8 @@ Sim uses a **base subscription + overage** billing model:
371373

372374
**Team Plans:**
373375
- Usage is pooled across internal team members in the organization
374-
- External workspace members keep their own organization or personal billing context for runs where they are the billing actor
376+
- Usage in an organization-owned workspace is billed to that workspace's organization, including work performed by external collaborators
377+
- Each authenticated actor's usage is also counted toward their member cap for that organization
375378
- Overage is calculated from total team usage against the pooled limit
376379
- Organization owner receives one bill
377380

apps/docs/content/docs/es/api-reference/authentication.mdx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,23 @@ To access the Sim API, you need an API key. Sim supports two types of API keys
1212

1313
| | **Personal Keys** | **Workspace Keys** |
1414
| --- | --- | --- |
15-
| **Billed to** | Your individual account | Workspace owner |
15+
| **Billing** | Workspace payer for workspace-hosted usage | Workspace payer |
1616
| **Scope** | Across workspaces you have access to | Shared across the workspace |
1717
| **Managed by** | Each user individually | Workspace admins |
1818
| **Permissions** | Must be enabled at workspace level | Require admin permissions |
1919

2020
<Callout type="info">
21-
Workspace admins can disable personal API key usage for their workspace. If disabled, only workspace keys can be used.
21+
Personal keys identify the user making a request; they do not select who pays.
22+
Hosted usage is billed to the workspace's organization or personal billing
23+
account and, for organizations, is attributed to the actor's member cap.
24+
Workspace admins can disable personal API key usage for their workspace. If
25+
disabled, only workspace keys can be used.
2226
</Callout>
2327

2428
## Generating API Keys
2529

26-
To generate a key, open the Sim dashboard and navigate to **Settings**, then go to **Sim Keys** and click **Create**.
30+
To generate a personal key, open **Account settings****Sim API keys**. Workspace
31+
administrators can create shared keys from **Workspace settings****Sim API keys**.
2732

2833
<Callout type="warn">
2934
API keys are only shown once when generated. Store your key securely — you will not be able to view it again.

apps/docs/content/docs/fr/api-reference/authentication.mdx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,23 @@ To access the Sim API, you need an API key. Sim supports two types of API keys
1212

1313
| | **Personal Keys** | **Workspace Keys** |
1414
| --- | --- | --- |
15-
| **Billed to** | Your individual account | Workspace owner |
15+
| **Billing** | Workspace payer for workspace-hosted usage | Workspace payer |
1616
| **Scope** | Across workspaces you have access to | Shared across the workspace |
1717
| **Managed by** | Each user individually | Workspace admins |
1818
| **Permissions** | Must be enabled at workspace level | Require admin permissions |
1919

2020
<Callout type="info">
21-
Workspace admins can disable personal API key usage for their workspace. If disabled, only workspace keys can be used.
21+
Personal keys identify the user making a request; they do not select who pays.
22+
Hosted usage is billed to the workspace's organization or personal billing
23+
account and, for organizations, is attributed to the actor's member cap.
24+
Workspace admins can disable personal API key usage for their workspace. If
25+
disabled, only workspace keys can be used.
2226
</Callout>
2327

2428
## Generating API Keys
2529

26-
To generate a key, open the Sim dashboard and navigate to **Settings**, then go to **Sim Keys** and click **Create**.
30+
To generate a personal key, open **Account settings****Sim API keys**. Workspace
31+
administrators can create shared keys from **Workspace settings****Sim API keys**.
2732

2833
<Callout type="warn">
2934
API keys are only shown once when generated. Store your key securely — you will not be able to view it again.

apps/docs/content/docs/ja/api-reference/authentication.mdx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,23 @@ To access the Sim API, you need an API key. Sim supports two types of API keys
1212

1313
| | **Personal Keys** | **Workspace Keys** |
1414
| --- | --- | --- |
15-
| **Billed to** | Your individual account | Workspace owner |
15+
| **Billing** | Workspace payer for workspace-hosted usage | Workspace payer |
1616
| **Scope** | Across workspaces you have access to | Shared across the workspace |
1717
| **Managed by** | Each user individually | Workspace admins |
1818
| **Permissions** | Must be enabled at workspace level | Require admin permissions |
1919

2020
<Callout type="info">
21-
Workspace admins can disable personal API key usage for their workspace. If disabled, only workspace keys can be used.
21+
Personal keys identify the user making a request; they do not select who pays.
22+
Hosted usage is billed to the workspace's organization or personal billing
23+
account and, for organizations, is attributed to the actor's member cap.
24+
Workspace admins can disable personal API key usage for their workspace. If
25+
disabled, only workspace keys can be used.
2226
</Callout>
2327

2428
## Generating API Keys
2529

26-
To generate a key, open the Sim dashboard and navigate to **Settings**, then go to **Sim Keys** and click **Create**.
30+
To generate a personal key, open **Account settings****Sim API keys**. Workspace
31+
administrators can create shared keys from **Workspace settings****Sim API keys**.
2732

2833
<Callout type="warn">
2934
API keys are only shown once when generated. Store your key securely — you will not be able to view it again.

apps/docs/content/docs/zh/api-reference/authentication.mdx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,23 @@ To access the Sim API, you need an API key. Sim supports two types of API keys
1212

1313
| | **Personal Keys** | **Workspace Keys** |
1414
| --- | --- | --- |
15-
| **Billed to** | Your individual account | Workspace owner |
15+
| **Billing** | Workspace payer for workspace-hosted usage | Workspace payer |
1616
| **Scope** | Across workspaces you have access to | Shared across the workspace |
1717
| **Managed by** | Each user individually | Workspace admins |
1818
| **Permissions** | Must be enabled at workspace level | Require admin permissions |
1919

2020
<Callout type="info">
21-
Workspace admins can disable personal API key usage for their workspace. If disabled, only workspace keys can be used.
21+
Personal keys identify the user making a request; they do not select who pays.
22+
Hosted usage is billed to the workspace's organization or personal billing
23+
account and, for organizations, is attributed to the actor's member cap.
24+
Workspace admins can disable personal API key usage for their workspace. If
25+
disabled, only workspace keys can be used.
2226
</Callout>
2327

2428
## Generating API Keys
2529

26-
To generate a key, open the Sim platform and navigate to **Settings**, then go to **Sim Keys** and click **Create**.
30+
To generate a personal key, open **Account settings****Sim API keys**. Workspace
31+
administrators can create shared keys from **Workspace settings****Sim API keys**.
2732

2833
<Callout type="warn">
2934
API keys are only shown once when generated. Store your key securely — you will not be able to view it again.

apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/resume-page-client.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,18 @@ export default function ResumeExecutionPage({
819819
</div>
820820
</div>
821821

822+
{selectedDetail.pausePoint.automaticResumeWaitingReason && (
823+
<div className='rounded-[8px] border border-[var(--border)] bg-[var(--surface-1)] px-4 py-3'>
824+
<Label>Waiting to resume automatically</Label>
825+
<p className='mt-1 text-[13px] text-[var(--text-secondary)]'>
826+
{selectedDetail.pausePoint.automaticResumeWaitingReason}
827+
</p>
828+
<p className='mt-1 text-[12px] text-[var(--text-muted)]'>
829+
Sim will retry automatically.
830+
</p>
831+
</div>
832+
)}
833+
822834
{/* Already resolved - show form fields with submitted values */}
823835
{selectedStatus === 'resumed' || selectedStatus === 'failed' ? (
824836
<div className='overflow-hidden rounded-[8px] border border-[var(--border)] bg-[var(--surface-1)]'>

apps/sim/app/_shell/providers/session-provider.test.tsx

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,22 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
66
import { createRoot, type Root } from 'react-dom/client'
77
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
88

9-
const { mockGetSession, mockSetActive, mockRequestJson } = vi.hoisted(() => ({
9+
const { mockGetSession, mockListOrganizations, mockSetActive } = vi.hoisted(() => ({
1010
mockGetSession: vi.fn(),
11+
mockListOrganizations: vi.fn(),
1112
mockSetActive: vi.fn(),
12-
mockRequestJson: vi.fn(),
1313
}))
1414

1515
vi.mock('@/lib/auth/auth-client', () => ({
1616
client: {
1717
getSession: mockGetSession,
18-
organization: { setActive: mockSetActive },
18+
organization: {
19+
list: mockListOrganizations,
20+
setActive: mockSetActive,
21+
},
1922
},
2023
}))
2124

22-
vi.mock('@/lib/api/client/request', () => ({
23-
requestJson: mockRequestJson,
24-
}))
25-
2625
vi.mock('posthog-js', () => ({
2726
default: {
2827
identify: vi.fn(),
@@ -66,6 +65,16 @@ const FRESH_SESSION: AppSession = {
6665
session: { id: 's1', userId: 'user-1', activeOrganizationId: 'org-1' },
6766
}
6867

68+
const NO_ACTIVE_ORGANIZATION_SESSION: AppSession = {
69+
user: { id: 'user-1', email: 'u@x.com', name: 'No active organization' },
70+
session: { id: 's1', userId: 'user-1' },
71+
}
72+
73+
const RECOVERED_ORGANIZATION_SESSION: AppSession = {
74+
user: { id: 'user-1', email: 'u@x.com', name: 'Recovered organization' },
75+
session: { id: 's1', userId: 'user-1', activeOrganizationId: 'org-member' },
76+
}
77+
6978
interface Harness {
7079
ctx: () => SessionHookResult | null
7180
queryClient: QueryClient
@@ -152,6 +161,8 @@ describe('useSessionQuery', () => {
152161
describe('SessionProvider', () => {
153162
beforeEach(() => {
154163
vi.clearAllMocks()
164+
mockListOrganizations.mockResolvedValue({ data: [], error: null })
165+
mockSetActive.mockResolvedValue({ data: null, error: null })
155166
setSearch('')
156167
})
157168

@@ -179,6 +190,85 @@ describe('SessionProvider', () => {
179190
h.unmount()
180191
})
181192

193+
it('preserves an intentional no-active-organization state on a normal load', async () => {
194+
mockGetSession.mockResolvedValue({ data: NO_ACTIVE_ORGANIZATION_SESSION })
195+
mockListOrganizations.mockResolvedValue({
196+
data: [{ id: 'org-member', name: 'Member organization' }],
197+
error: null,
198+
})
199+
200+
const h = renderProvider()
201+
await flushUntil(() => h.ctx()?.data != null)
202+
203+
expect(h.ctx()?.data).toEqual(NO_ACTIVE_ORGANIZATION_SESSION)
204+
expect(mockListOrganizations).not.toHaveBeenCalled()
205+
expect(mockSetActive).not.toHaveBeenCalled()
206+
207+
h.unmount()
208+
})
209+
210+
it('does not auto-select an organization for an external-only user during recovery', async () => {
211+
setSearch('?upgraded=true')
212+
mockGetSession.mockResolvedValue({ data: NO_ACTIVE_ORGANIZATION_SESSION })
213+
mockListOrganizations.mockResolvedValue({ data: [], error: null })
214+
215+
const h = renderProvider()
216+
await flushUntil(() => mockListOrganizations.mock.calls.length > 0)
217+
218+
expect(mockListOrganizations).toHaveBeenCalledTimes(1)
219+
expect(mockSetActive).not.toHaveBeenCalled()
220+
221+
h.unmount()
222+
})
223+
224+
it('recovers the sole valid viewer organization membership when upgrade recovery is explicit', async () => {
225+
window.history.replaceState({}, '', '/workspace/workspace-b?upgraded=true')
226+
mockListOrganizations.mockResolvedValue({
227+
data: [{ id: 'org-member', name: 'Member organization' }],
228+
error: null,
229+
})
230+
mockGetSession.mockImplementation(() =>
231+
Promise.resolve({
232+
data:
233+
mockSetActive.mock.calls.length > 0
234+
? RECOVERED_ORGANIZATION_SESSION
235+
: NO_ACTIVE_ORGANIZATION_SESSION,
236+
})
237+
)
238+
239+
const h = renderProvider()
240+
await flushUntil(
241+
() =>
242+
h.queryClient.getQueryData<AppSession>(sessionKeys.detail())?.session
243+
?.activeOrganizationId === 'org-member'
244+
)
245+
246+
expect(mockSetActive).toHaveBeenCalledWith({ organizationId: 'org-member' })
247+
expect(h.queryClient.getQueryData(sessionKeys.detail())).toEqual(RECOVERED_ORGANIZATION_SESSION)
248+
249+
h.unmount()
250+
})
251+
252+
it('does not choose arbitrarily when multiple valid memberships need recovery', async () => {
253+
setSearch('?upgraded=true')
254+
mockGetSession.mockResolvedValue({ data: NO_ACTIVE_ORGANIZATION_SESSION })
255+
mockListOrganizations.mockResolvedValue({
256+
data: [
257+
{ id: 'org-a', name: 'Organization A' },
258+
{ id: 'org-b', name: 'Organization B' },
259+
],
260+
error: null,
261+
})
262+
263+
const h = renderProvider()
264+
await flushUntil(() => mockListOrganizations.mock.calls.length > 0)
265+
266+
expect(mockListOrganizations).toHaveBeenCalledTimes(1)
267+
expect(mockSetActive).not.toHaveBeenCalled()
268+
269+
h.unmount()
270+
})
271+
182272
it('upgrade path: fresh disableCookieCache read wins even when the stale mount query resolves LAST', async () => {
183273
setSearch('?upgraded=true')
184274

apps/sim/app/_shell/providers/session-provider.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import type React from 'react'
44
import { createContext, useEffect, useMemo } from 'react'
55
import { createLogger } from '@sim/logger'
66
import { useQueryClient } from '@tanstack/react-query'
7-
import { requestJson } from '@/lib/api/client/request'
8-
import { listCreatorOrganizationsContract } from '@/lib/api/contracts/organizations'
97
import { client } from '@/lib/auth/auth-client'
108
import {
119
type AppSession,
@@ -79,10 +77,9 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
7977
}
8078

8179
try {
82-
const orgData = await requestJson(listCreatorOrganizationsContract, {}).catch(() => null)
83-
if (!orgData) return
84-
85-
const organizationId = orgData.organizations?.[0]?.id
80+
const organizationsResponse = await client.organization.list()
81+
const organizations = organizationsResponse.data ?? []
82+
const organizationId = organizations.length === 1 ? organizations[0]?.id : null
8683

8784
if (!organizationId || isCancelled) {
8885
return

0 commit comments

Comments
 (0)