Skip to content

Commit abc5ead

Browse files
authored
chore(api-keys): remove the api-key-read-only flag now that read-only keys are live (#9879)
Every production pod runs v0.9.53, which enforces key access, and the flag is on in prod AppConfig. Read-only key creation is now always available: the create modal always offers Access (except where a key must be full access), and the create use case no longer consults the flag.
1 parent a4bbaf7 commit abc5ead

12 files changed

Lines changed: 8 additions & 113 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/account/settings/layout.test.tsx‎

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

‎apps/sim/app/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 { isPlatformAdmin } from '@/lib/permissions/super-user'
@@ -9,10 +8,7 @@ import { FeatureFlagsProvider } from '@/app/workspace/[workspaceId]/providers/fe
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 (
@@ -23,7 +19,6 @@ export default async function AccountSettingsLayout({ children }: { children: Re
2319
changelog: false,
2420
'mothership-model-selector': false,
2521
'mothership-plan-mode': false,
26-
'api-key-read-only': readOnlyApiKeysEnabled,
2722
}}
2823
>
2924
<StandaloneSettingsShell

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
33
import { cookies } from 'next/headers'
44
import { redirect } from 'next/navigation'
55
import { SettingsNavigationProvider } from '@/components/settings/settings-navigation-provider'
6-
import { isReadOnlyApiKeyCreationEnabled } from '@/lib/api-key/feature-flag'
76
import { getSession } from '@/lib/auth'
87
import { getActiveOrganizationId } from '@/lib/auth/session-response'
98
import { canUseBenchmarks } from '@/lib/benchmarks/config'
@@ -90,7 +89,6 @@ export default async function OrganizationLayout({
9089
simLinksEnabled,
9190
peopleEnabled,
9291
simLinkOrganizationId,
93-
readOnlyApiKeysEnabled,
9492
] = await Promise.all([
9593
prefetchOrganizationSidebar(
9694
queryClient,
@@ -115,7 +113,6 @@ export default async function OrganizationLayout({
115113
userId: session.user.id,
116114
sessionId: session.session.id,
117115
}),
118-
isReadOnlyApiKeyCreationEnabled(),
119116
])
120117
const initialSidebarCollapsed = cookieStore.get('sidebar_collapsed')?.value === '1'
121118

@@ -131,7 +128,6 @@ export default async function OrganizationLayout({
131128
'mothership-plan-mode': benchmarkEnabled,
132129
'mothership-memory-spaces': benchmarkEnabled,
133130
projects: projectsEnabled,
134-
'api-key-read-only': readOnlyApiKeysEnabled,
135131
}}
136132
>
137133
<OrganizationProvider context={context}>

‎apps/sim/app/workspace/[workspaceId]/layout.tsx‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { cookies } from 'next/headers'
44
import { redirect } from 'next/navigation'
55
import { SettingsNavigationProvider } from '@/components/settings/settings-navigation-provider'
66
import type { WorkspaceHostContext } from '@/lib/api/contracts/workspaces'
7-
import { isReadOnlyApiKeyCreationEnabled } from '@/lib/api-key/feature-flag'
87
import { getSession } from '@/lib/auth'
98
import { getActiveOrganizationId } from '@/lib/auth/session-response'
109
import { isChangelogEnabled } from '@/lib/changelog/feature-flag'
@@ -138,7 +137,6 @@ async function WorkspaceContent({
138137
computerUseEnabled,
139138
projectsEnabled,
140139
simLinkOrganizationId,
141-
readOnlyApiKeysEnabled,
142140
] = await Promise.all([
143141
cookies(),
144142
orgSettings,
@@ -159,7 +157,6 @@ async function WorkspaceContent({
159157
isComputerUseAvailable(hostContext.hostOrganizationId),
160158
isProjectsEnabled(),
161159
resolveSimLinkOrganization(principal),
162-
isReadOnlyApiKeyCreationEnabled(),
163160
prefetchWorkspaceAccess(queryClient, workspaceId, principal),
164161
prefetchWorkspaceForkAvailability(queryClient, workspaceId, principal, hostContext),
165162
])
@@ -176,7 +173,6 @@ async function WorkspaceContent({
176173
'mothership-computer-use': computerUseEnabled,
177174
'mothership-plan-mode': planModeEnabled,
178175
projects: projectsEnabled,
179-
'api-key-read-only': readOnlyApiKeysEnabled,
180176
}}
181177
>
182178
<WorkspaceHostProvider workspaceId={workspaceId} initialContext={hostContext}>

‎apps/sim/app/workspace/[workspaceId]/providers/feature-flags-provider.tsx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export interface WorkspaceFeatureFlags {
1111
'mothership-computer-use'?: boolean
1212
'mothership-memory-spaces'?: boolean
1313
projects?: boolean
14-
'api-key-read-only'?: boolean
1514
}
1615

1716
const FeatureFlagsContext = createContext<WorkspaceFeatureFlags | null>(null)

‎apps/sim/app/workspace/[workspaceId]/settings/components/api-keys/components/create-api-key-modal/create-api-key-modal.tsx‎

Lines changed: 1 addition & 3 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 '@/app/workspace/[workspaceId]/providers/feature-flags-provider'
1918
import { type CreatedApiKey, useCreateApiKey } from '@/hooks/queries/api-keys'
2019

2120
const logger = createLogger('CreateApiKeyModal')
@@ -75,8 +74,7 @@ export function CreateApiKeyModal({
7574
const [newKey, setNewKey] = useState<CreatedApiKey | null>(null)
7675
const [showNewKeyDialog, setShowNewKeyDialog] = useState(false)
7776
const createApiKeyMutation = useCreateApiKey()
78-
const readOnlyKeysEnabled = useFeatureFlag('api-key-read-only')
79-
const canChooseAccess = !fullAccessOnly && readOnlyKeysEnabled
77+
const canChooseAccess = !fullAccessOnly
8078

8179
/**
8280
* The form is seeded when the modal OPENS, not when this component mounts.

‎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)