Skip to content

Commit 5750458

Browse files
committed
Merge origin/staging (#9879) into improvement/platform-core-chat
Removes the api-key-read-only flag from the Platform surface too: the surface contract, organization surface resolver, the read-only key hook, and both create-key modals now always offer read-only access.
2 parents 2efcff9 + abc5ead commit 5750458

21 files changed

Lines changed: 18 additions & 140 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ A read-only key cannot change workspace resources or run, resume, or cancel work
4444

4545
A refused request fails with `403` and the message `This API key is read-only and cannot perform this operation`. Where the surface carries a code, it is `READ_ONLY_API_KEY`: in `error.details.code` on the v2 API and the Sim MCP server, in `details.code` on the v1 API and workflow execution, and in `error.data.code` on a workflow MCP server.
4646

47-
Access narrows a key; it never grants more than the key's identity already has. Access is fixed at creation, so create a new key to change it. Keys created before access levels existed have full access. Keys created from the deploy modal or for a workflow MCP server always have full access, because they exist to run workflows. If **Access** isn't shown when you create a key, read-only keys aren't available on your deployment yet.
47+
Access narrows a key; it never grants more than the key's identity already has. Access is fixed at creation, so create a new key to change it. Keys created before access levels existed have full access. Keys created from the deploy modal or for a workflow MCP server always have full access, because they exist to run workflows.
4848

4949
### Expiration [#key-expiration]
5050

‎apps/sim/app/(product)/_components/destinations.test.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ function organization({
8484
'mothership-memory-spaces': false,
8585
projects: false,
8686
'platform-preview': false,
87-
'api-key-read-only': false,
8887
},
8988
settingsSections: [],
9089
}

‎apps/sim/app/(product)/o/[organizationId]/search/_components/search-surface/live-search-route.dom.test.tsx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ const NARROW_MEMBER_SURFACE = {
102102
'mothership-memory-spaces': false,
103103
projects: false,
104104
'platform-preview': false,
105-
'api-key-read-only': false,
106105
},
107106
settingsSections: [],
108107
} satisfies PlatformOrganization

‎apps/sim/app/(product)/workspace/[workspaceId]/_components/create-api-key-modal/create-api-key-modal.tsx‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
createLogger,
1414
DAY_MS,
1515
useCreateApiKey,
16-
useReadOnlyApiKeysEnabled,
1716
} from '@/platform/lib/data/api-keys'
1817

1918
const logger = createLogger('CreateApiKeyModal')
@@ -65,7 +64,7 @@ export function CreateApiKeyModal({
6564
const [keyType, setKeyType] = useState<KeyType>(defaultKeyType)
6665
const [access, setAccess] = useState<ApiKeyAccessLevel>('write')
6766
const [expiry, setExpiry] = useState<ExpiryOption>('never')
68-
const canChooseAccess = useReadOnlyApiKeysEnabled() && !fullAccessOnly
67+
const canChooseAccess = !fullAccessOnly
6968
const [created, setCreated] = useState<CreatedApiKey | null>(null)
7069
const canCreate =
7170
keyType === 'personal' ? allowPersonalApiKeys : canManageWorkspaceKeys && Boolean(workspaceId)

‎apps/sim/app/(site)/account/settings/layout.test.tsx‎

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

‎apps/sim/app/(site)/account/settings/layout.tsx‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { redirect } from 'next/navigation'
22
import { StandaloneSettingsShell } from '@/components/settings/standalone-settings-shell'
3-
import { isReadOnlyApiKeyCreationEnabled } from '@/lib/api-key/feature-flag'
43
import { getSession } from '@/lib/auth'
54
import { resolveDeploymentShape } from '@/lib/core/config/deployment-shape'
65
import { FeatureFlagsProvider } from '@/lib/core/config/feature-flags-context'
@@ -9,10 +8,7 @@ import { isPlatformAdmin } from '@/lib/permissions/super-user'
98
export default async function AccountSettingsLayout({ children }: { children: React.ReactNode }) {
109
const session = await getSession()
1110
if (!session?.user) redirect('/login')
12-
const [isSuperUser, readOnlyApiKeysEnabled] = await Promise.all([
13-
isPlatformAdmin(session.user.id),
14-
isReadOnlyApiKeyCreationEnabled(),
15-
])
11+
const isSuperUser = await isPlatformAdmin(session.user.id)
1612

1713
/** Account settings host no workspace surface, so only the flags its sections read are on. */
1814
return (
@@ -22,7 +18,6 @@ export default async function AccountSettingsLayout({ children }: { children: Re
2218
'workflow-tests': false,
2319
'mothership-model-selector': false,
2420
'mothership-plan-mode': false,
25-
'api-key-read-only': readOnlyApiKeysEnabled,
2621
}}
2722
>
2823
<StandaloneSettingsShell

‎apps/sim/components/settings/sections/api-keys/components/create-api-key-modal/create-api-key-modal.tsx‎

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
import { createLogger } from '@sim/logger'
1616
import { getErrorMessage } from '@sim/utils/errors'
1717
import { type ApiKeyAccessLevel, DAY_MS } from '@/lib/api/contracts/api-keys'
18-
import { useFeatureFlag } from '@/lib/core/config/feature-flags-context'
1918
import { type CreatedApiKey, useCreateApiKey } from '@/hooks/queries/api-keys'
2019

2120
const logger = createLogger('CreateApiKeyModal')
@@ -54,7 +53,6 @@ export function CreateApiKeyModal({
5453
const [newKey, setNewKey] = useState<CreatedApiKey | null>(null)
5554
const [showNewKeyDialog, setShowNewKeyDialog] = useState(false)
5655
const createApiKeyMutation = useCreateApiKey()
57-
const readOnlyKeysEnabled = useFeatureFlag('api-key-read-only')
5856

5957
/**
6058
* The form is reset when the modal OPENS, not when this component mounts: it
@@ -92,7 +90,7 @@ export function CreateApiKeyModal({
9290
const data = await createApiKeyMutation.mutateAsync({
9391
keyType: 'personal',
9492
name: trimmedName,
95-
access: readOnlyKeysEnabled ? access : 'write',
93+
access,
9694
expiresAt: expiresAtFor(expiry),
9795
})
9896

@@ -138,17 +136,15 @@ export function CreateApiKeyModal({
138136
autoComplete='off'
139137
required
140138
/>
141-
{readOnlyKeysEnabled && (
142-
<ChipModalField type='custom' title='Access'>
143-
<ChipButtonGroup
144-
value={access}
145-
onValueChange={(value) => setAccess(value as ApiKeyAccessLevel)}
146-
>
147-
<ChipButtonGroupItem value='write'>Full access</ChipButtonGroupItem>
148-
<ChipButtonGroupItem value='read'>Read only</ChipButtonGroupItem>
149-
</ChipButtonGroup>
150-
</ChipModalField>
151-
)}
139+
<ChipModalField type='custom' title='Access'>
140+
<ChipButtonGroup
141+
value={access}
142+
onValueChange={(value) => setAccess(value as ApiKeyAccessLevel)}
143+
>
144+
<ChipButtonGroupItem value='write'>Full access</ChipButtonGroupItem>
145+
<ChipButtonGroupItem value='read'>Read only</ChipButtonGroupItem>
146+
</ChipButtonGroup>
147+
</ChipModalField>
152148
<ChipModalField
153149
type='dropdown'
154150
title='Expires'

‎apps/sim/lib/api-key/__integration__/read-only-access.integration.ts‎

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
} from '@sim/db/schema'
2323
import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures'
2424
import { authMock, authMockFns } from '@sim/testing'
25-
import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock'
25+
import { featureFlagsMock } from '@sim/testing/mocks/feature-flags.mock'
2626
import { generateId } from '@sim/utils/id'
2727
import { eq } from 'drizzle-orm'
2828
import { NextRequest } from 'next/server'
@@ -60,7 +60,6 @@ const expiredKey = `sk-sim-fixture-${generateId()}`
6060

6161
let readPersonalKey = ''
6262
let readWorkspaceKey = ''
63-
let readOnlyKeysEnabled = false
6463

6564
type RouteHandler = (request: NextRequest, context: { params: Promise<never> }) => Promise<Response>
6665

@@ -176,9 +175,6 @@ describe('read-only and expiring API keys in PostgreSQL', () => {
176175
displayName: 'Topic',
177176
fieldType: 'text',
178177
})
179-
featureFlagsMockFns.mockIsFeatureEnabled.mockImplementation(
180-
async (flag) => flag === 'api-key-read-only' && readOnlyKeysEnabled
181-
)
182178
await db.insert(apiKey).values([
183179
{
184180
id: generateId(),
@@ -231,22 +227,6 @@ describe('read-only and expiring API keys in PostgreSQL', () => {
231227
expect(after).toHaveLength(before.length)
232228
})
233229

234-
it('refuses to create a read-only key while the api-key-read-only flag is off', async () => {
235-
const before = await db.select({ id: apiKey.id }).from(apiKey).where(eq(apiKey.userId, userId))
236-
const personal = await call(createPersonalKey, '/api/users/me/api-keys', {
237-
body: { name: `Flag off personal ${generateId()}`, access: 'read' },
238-
})
239-
expect(personal.status).toBe(400)
240-
expect(personal.body).toEqual({ error: 'Read-only API keys are not available yet' })
241-
const scoped = await call(createWorkspaceKey, `/api/workspaces/${workspaceId}/api-keys`, {
242-
body: { name: `Flag off workspace ${generateId()}`, access: 'read' },
243-
params: { id: workspaceId },
244-
})
245-
expect(scoped.status).toBe(400)
246-
const after = await db.select({ id: apiKey.id }).from(apiKey).where(eq(apiKey.userId, userId))
247-
expect(after).toHaveLength(before.length)
248-
})
249-
250230
it('reserves the delegation key-name prefix', async () => {
251231
const created = await call(createPersonalKey, '/api/users/me/api-keys', {
252232
body: { name: `mothership-delegation:${userId}`, access: 'write' },
@@ -255,7 +235,6 @@ describe('read-only and expiring API keys in PostgreSQL', () => {
255235
})
256236

257237
it('mints read-only personal and workspace keys that report their access and expiry', async () => {
258-
readOnlyKeysEnabled = true
259238
const expiresAt = new Date(Date.now() + 30 * DAY_MS).toISOString()
260239
const personal = await call(createPersonalKey, '/api/users/me/api-keys', {
261240
body: { name: `Read personal ${generateId()}`, access: 'read', expiresAt },

‎apps/sim/lib/api-key/feature-flag.ts‎

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

‎apps/sim/lib/api-key/orchestration/index.ts‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { and, eq } from 'drizzle-orm'
99
import { DAY_MS } from '@/lib/api/contracts/api-keys'
1010
import { createApiKey, createWorkspaceApiKey } from '@/lib/api-key/auth'
1111
import { hashApiKey } from '@/lib/api-key/crypto'
12-
import { isReadOnlyApiKeyCreationEnabled } from '@/lib/api-key/feature-flag'
1312
import { DELEGATION_KEY_NAME_PREFIX } from '@/lib/api-key/reserved-names'
1413
import { PlatformEvents } from '@/lib/core/telemetry'
1514

@@ -70,16 +69,13 @@ type ResolvedIssueSettings =
7069
| { ok: false; error: string }
7170

7271
/** Validates what a caller asked a new key to be, before anything is written. */
73-
async function resolveIssueSettings(
72+
function resolveIssueSettings(
7473
params: ApiKeyIssueSettings & { name: string }
75-
): Promise<ResolvedIssueSettings> {
74+
): ResolvedIssueSettings {
7675
if (params.name.startsWith(DELEGATION_KEY_NAME_PREFIX)) {
7776
return { ok: false, error: `API key names may not start with "${DELEGATION_KEY_NAME_PREFIX}"` }
7877
}
7978
const access = params.access ?? 'write'
80-
if (access === 'read' && !(await isReadOnlyApiKeyCreationEnabled())) {
81-
return { ok: false, error: 'Read-only API keys are not available yet' }
82-
}
8379
const expiry = resolveApiKeyExpiry(params.expiresAt)
8480
if (!expiry.ok) return expiry
8581
return { ok: true, access, expiresAt: expiry.expiresAt }
@@ -131,7 +127,7 @@ export interface PerformCreatePersonalApiKeyResult {
131127
export async function performCreatePersonalApiKey(
132128
params: PerformCreatePersonalApiKeyParams
133129
): Promise<PerformCreatePersonalApiKeyResult> {
134-
const settings = await resolveIssueSettings(params)
130+
const settings = resolveIssueSettings(params)
135131
if (!settings.ok) return { success: false, errorCode: 'validation', error: settings.error }
136132
const { access, expiresAt } = settings
137133
try {
@@ -216,7 +212,7 @@ export async function performCreatePersonalApiKey(
216212
export async function performCreateWorkspaceApiKey(
217213
params: PerformCreateWorkspaceApiKeyParams
218214
): Promise<PerformCreateWorkspaceApiKeyResult> {
219-
const settings = await resolveIssueSettings(params)
215+
const settings = resolveIssueSettings(params)
220216
if (!settings.ok) return { success: false, errorCode: 'validation', error: settings.error }
221217
const { access, expiresAt } = settings
222218
try {

0 commit comments

Comments
 (0)