From a70e346e5af2805d88bba387bf1898f8f5f014be Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 2 Dec 2025 09:05:35 -0800 Subject: [PATCH 1/7] fix(clerk-js): Default UserProfile API keys to use user id --- .../tests/machine-auth/component.test.ts | 38 ++++++++++++++++++- package.json | 2 +- .../src/ui/components/APIKeys/APIKeys.tsx | 4 +- .../OrganizationAPIKeysPage.tsx | 2 +- .../ui/components/UserProfile/APIKeysPage.tsx | 2 +- packages/shared/src/types/clerk.ts | 4 ++ .../unstable/page-objects/userProfile.ts | 3 ++ 7 files changed, 50 insertions(+), 5 deletions(-) diff --git a/integration/tests/machine-auth/component.test.ts b/integration/tests/machine-auth/component.test.ts index f03881e4632..86b5fe923cd 100644 --- a/integration/tests/machine-auth/component.test.ts +++ b/integration/tests/machine-auth/component.test.ts @@ -30,7 +30,7 @@ const mockAPIKeysEnvironmentSettings = async ( testAgainstRunningApps({ withEnv: [appConfigs.envs.withAPIKeys], withPattern: ['withMachine.next.appRouter'], -})('api keys component @machine', ({ app }) => { +})('api keys component @xmachine', ({ app }) => { test.describe.configure({ mode: 'serial' }); let fakeAdmin: FakeUser; @@ -285,6 +285,42 @@ testAgainstRunningApps({ await u.page.unrouteAll(); }); + test('UserProfile API keys uses user ID as subject even when organization is active', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + const admin = await u.services.users.getUser({ email: fakeAdmin.email }); + expect(admin).toBeDefined(); + const userId = admin.id; + + await u.po.signIn.goTo(); + await u.po.signIn.waitForMounted(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password }); + await u.po.expect.toBeSignedIn(); + + await u.po.organizationSwitcher.goTo(); + await u.po.organizationSwitcher.waitForMounted(); + await u.po.organizationSwitcher.waitForAnOrganizationToSelected(); + + // Set up request interception to capture the subject parameter + let capturedSubject: string | null = null; + await u.page.route('**/api_keys*', async route => { + const url = new URL(route.request().url()); + capturedSubject = url.searchParams.get('subject'); + await route.continue(); + }); + + // Navigate to UserProfile API keys page + await u.po.page.goToRelative('/user'); + await u.po.userProfile.waitForMounted(); + await u.po.userProfile.switchToAPIKeysTab(); + + // Verify the subject parameter is the user ID, not the organization ID + expect(capturedSubject).toBe(userId); + expect(capturedSubject).not.toBe(fakeOrganization.organization.id); + + await u.page.unrouteAll(); + }); + test('standalone API keys component in user context based on user_api_keys_enabled', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); diff --git a/package.json b/package.json index 3d9286c9143..ad08cc0c01f 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "test:integration:handshake": "DISABLE_WEB_SECURITY=true E2E_APP_1_ENV_KEY=sessions-prod-1 E2E_SESSIONS_APP_1_HOST=multiple-apps-e2e.clerk.app pnpm test:integration:base --grep @handshake", "test:integration:handshake:staging": "DISABLE_WEB_SECURITY=true E2E_APP_1_ENV_KEY=clerkstage-sessions-prod-1 E2E_SESSIONS_APP_1_HOST=clerkstage-sessions-prod-1-e2e.clerk.app pnpm test:integration:base --grep @handshake", "test:integration:localhost": "pnpm test:integration:base --grep @localhost", - "test:integration:machine": "E2E_APP_ID=withMachine.* pnpm test:integration:base --grep @machine", + "test:integration:machine": "E2E_APP_ID=withMachine.* pnpm test:integration:base --grep @xmachine", "test:integration:nextjs": "E2E_APP_ID=next.appRouter.* pnpm test:integration:base --grep @nextjs", "test:integration:nuxt": "E2E_APP_ID=nuxt.node npm run test:integration:base -- --grep @nuxt", "test:integration:quickstart": "E2E_APP_ID=quickstart.* pnpm test:integration:base --grep @quickstart", diff --git a/packages/clerk-js/src/ui/components/APIKeys/APIKeys.tsx b/packages/clerk-js/src/ui/components/APIKeys/APIKeys.tsx index 3d560ab09b6..b12e2ae753c 100644 --- a/packages/clerk-js/src/ui/components/APIKeys/APIKeys.tsx +++ b/packages/clerk-js/src/ui/components/APIKeys/APIKeys.tsx @@ -242,7 +242,9 @@ const _APIKeys = () => { // Do not use `useOrganization` to avoid triggering the in-app enable organizations prompt in development instance const organizationCtx = useOrganizationContext(); - const subject = organizationCtx?.organization?.id ?? user?.id ?? ''; + // Use subject from context if provided (set by UserProfile/OrganizationProfile), + // otherwise fall back to organization ID if active, or user ID + const subject = ctx.subject ?? organizationCtx?.organization?.id ?? user?.id ?? ''; return ( { textVariant='h2' /> - + { textVariant='h2' /> - + { await page.getByText(/Billing/i).click(); }, + switchToAPIKeysTab: async () => { + await page.getByText(/API keys/i).click(); + }, waitForMounted: () => { return page.waitForSelector('.cl-userProfile-root', { state: 'attached' }); }, From 74e106b7975be98d3ec0998fc16a13d27228a033 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 2 Dec 2025 09:06:13 -0800 Subject: [PATCH 2/7] remove machine test --- integration/tests/machine-auth/component.test.ts | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/tests/machine-auth/component.test.ts b/integration/tests/machine-auth/component.test.ts index 86b5fe923cd..f2f75893f11 100644 --- a/integration/tests/machine-auth/component.test.ts +++ b/integration/tests/machine-auth/component.test.ts @@ -30,7 +30,7 @@ const mockAPIKeysEnvironmentSettings = async ( testAgainstRunningApps({ withEnv: [appConfigs.envs.withAPIKeys], withPattern: ['withMachine.next.appRouter'], -})('api keys component @xmachine', ({ app }) => { +})('api keys component @machine', ({ app }) => { test.describe.configure({ mode: 'serial' }); let fakeAdmin: FakeUser; diff --git a/package.json b/package.json index ad08cc0c01f..3d9286c9143 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "test:integration:handshake": "DISABLE_WEB_SECURITY=true E2E_APP_1_ENV_KEY=sessions-prod-1 E2E_SESSIONS_APP_1_HOST=multiple-apps-e2e.clerk.app pnpm test:integration:base --grep @handshake", "test:integration:handshake:staging": "DISABLE_WEB_SECURITY=true E2E_APP_1_ENV_KEY=clerkstage-sessions-prod-1 E2E_SESSIONS_APP_1_HOST=clerkstage-sessions-prod-1-e2e.clerk.app pnpm test:integration:base --grep @handshake", "test:integration:localhost": "pnpm test:integration:base --grep @localhost", - "test:integration:machine": "E2E_APP_ID=withMachine.* pnpm test:integration:base --grep @xmachine", + "test:integration:machine": "E2E_APP_ID=withMachine.* pnpm test:integration:base --grep @machine", "test:integration:nextjs": "E2E_APP_ID=next.appRouter.* pnpm test:integration:base --grep @nextjs", "test:integration:nuxt": "E2E_APP_ID=nuxt.node npm run test:integration:base -- --grep @nuxt", "test:integration:quickstart": "E2E_APP_ID=quickstart.* pnpm test:integration:base --grep @quickstart", From 39149b7d577f454ad9d4ebe217003007f6290d52 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 2 Dec 2025 09:07:25 -0800 Subject: [PATCH 3/7] chore: clean up test --- integration/tests/machine-auth/component.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/integration/tests/machine-auth/component.test.ts b/integration/tests/machine-auth/component.test.ts index f2f75893f11..372590de265 100644 --- a/integration/tests/machine-auth/component.test.ts +++ b/integration/tests/machine-auth/component.test.ts @@ -301,7 +301,7 @@ testAgainstRunningApps({ await u.po.organizationSwitcher.waitForMounted(); await u.po.organizationSwitcher.waitForAnOrganizationToSelected(); - // Set up request interception to capture the subject parameter + // Capture the subject parameter let capturedSubject: string | null = null; await u.page.route('**/api_keys*', async route => { const url = new URL(route.request().url()); @@ -309,7 +309,6 @@ testAgainstRunningApps({ await route.continue(); }); - // Navigate to UserProfile API keys page await u.po.page.goToRelative('/user'); await u.po.userProfile.waitForMounted(); await u.po.userProfile.switchToAPIKeysTab(); From b084c4aa0e23c26130d0897eabd8d98e69dd7255 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 2 Dec 2025 09:32:03 -0800 Subject: [PATCH 4/7] revert initial changes --- packages/clerk-js/src/ui/components/APIKeys/APIKeys.tsx | 4 +--- .../OrganizationProfile/OrganizationAPIKeysPage.tsx | 2 +- .../clerk-js/src/ui/components/UserProfile/APIKeysPage.tsx | 2 +- packages/shared/src/react/hooks/useAPIKeys.rq.tsx | 4 +--- packages/shared/src/react/hooks/useAPIKeys.swr.tsx | 4 +--- packages/shared/src/types/clerk.ts | 4 ---- 6 files changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/clerk-js/src/ui/components/APIKeys/APIKeys.tsx b/packages/clerk-js/src/ui/components/APIKeys/APIKeys.tsx index b12e2ae753c..3d560ab09b6 100644 --- a/packages/clerk-js/src/ui/components/APIKeys/APIKeys.tsx +++ b/packages/clerk-js/src/ui/components/APIKeys/APIKeys.tsx @@ -242,9 +242,7 @@ const _APIKeys = () => { // Do not use `useOrganization` to avoid triggering the in-app enable organizations prompt in development instance const organizationCtx = useOrganizationContext(); - // Use subject from context if provided (set by UserProfile/OrganizationProfile), - // otherwise fall back to organization ID if active, or user ID - const subject = ctx.subject ?? organizationCtx?.organization?.id ?? user?.id ?? ''; + const subject = organizationCtx?.organization?.id ?? user?.id ?? ''; return ( { textVariant='h2' /> - + { textVariant='h2' /> - + (params?: T): UseAPIKeysRe keys: createCacheKeys({ stablePrefix: STABLE_KEYS.API_KEYS_KEY, authenticated: Boolean(clerk.user), - tracked: { - subject: safeValues.subject, - }, + tracked: {}, untracked: { args: hookParams, }, diff --git a/packages/shared/src/react/hooks/useAPIKeys.swr.tsx b/packages/shared/src/react/hooks/useAPIKeys.swr.tsx index dc7037ab621..d6d9677fa24 100644 --- a/packages/shared/src/react/hooks/useAPIKeys.swr.tsx +++ b/packages/shared/src/react/hooks/useAPIKeys.swr.tsx @@ -110,9 +110,7 @@ export function useAPIKeys(params?: T): UseAPIKeysRe keys: createCacheKeys({ stablePrefix: STABLE_KEYS.API_KEYS_KEY, authenticated: Boolean(clerk.user), - tracked: { - subject: safeValues.subject, - }, + tracked: {}, untracked: { args: hookParams, }, diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index c78c2d18f79..84e3b5832b6 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -2001,10 +2001,6 @@ export type APIKeysProps = { * @default false */ showDescription?: boolean; - /** - * @internal - */ - subject?: string; }; export type GetAPIKeysParams = ClerkPaginationParams<{ From 3232d854be1767e04c4ccd3a7e71404a3d839759 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Tue, 2 Dec 2025 09:40:02 -0800 Subject: [PATCH 5/7] chore: add changeset --- .changeset/friendly-adults-bathe.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/friendly-adults-bathe.md diff --git a/.changeset/friendly-adults-bathe.md b/.changeset/friendly-adults-bathe.md new file mode 100644 index 00000000000..d0439fad4e6 --- /dev/null +++ b/.changeset/friendly-adults-bathe.md @@ -0,0 +1,6 @@ +--- +"@clerk/shared": patch +"@clerk/testing": patch +--- + +Fixed an issue where API keys in `` are showing organization API keys. From 232cc91fc7a4917234971e0caaccdebf7b80d070 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 2 Dec 2025 10:21:42 -0800 Subject: [PATCH 6/7] chore: explicitly pass subject --- packages/shared/src/react/hooks/useAPIKeys.rq.tsx | 8 ++++++-- packages/shared/src/react/hooks/useAPIKeys.swr.tsx | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/react/hooks/useAPIKeys.rq.tsx b/packages/shared/src/react/hooks/useAPIKeys.rq.tsx index 633ea934902..7423a4bc59b 100644 --- a/packages/shared/src/react/hooks/useAPIKeys.rq.tsx +++ b/packages/shared/src/react/hooks/useAPIKeys.rq.tsx @@ -95,7 +95,9 @@ export function useAPIKeys(params?: T): UseAPIKeysRe const isEnabled = (safeValues.enabled ?? true) && clerk.loaded; return usePagesOrInfinite({ - fetcher: clerk.apiKeys?.getAll ? (params: GetAPIKeysParams) => clerk.apiKeys.getAll(params) : undefined, + fetcher: clerk.apiKeys?.getAll + ? (params: GetAPIKeysParams) => clerk.apiKeys.getAll({ ...params, subject: safeValues.subject }) + : undefined, config: { keepPreviousData: safeValues.keepPreviousData, infinite: safeValues.infinite, @@ -107,7 +109,9 @@ export function useAPIKeys(params?: T): UseAPIKeysRe keys: createCacheKeys({ stablePrefix: STABLE_KEYS.API_KEYS_KEY, authenticated: Boolean(clerk.user), - tracked: {}, + tracked: { + subject: safeValues.subject, + }, untracked: { args: hookParams, }, diff --git a/packages/shared/src/react/hooks/useAPIKeys.swr.tsx b/packages/shared/src/react/hooks/useAPIKeys.swr.tsx index d6d9677fa24..7780f8e85a1 100644 --- a/packages/shared/src/react/hooks/useAPIKeys.swr.tsx +++ b/packages/shared/src/react/hooks/useAPIKeys.swr.tsx @@ -98,7 +98,9 @@ export function useAPIKeys(params?: T): UseAPIKeysRe const isEnabled = (safeValues.enabled ?? true) && clerk.loaded; const result = usePagesOrInfinite({ - fetcher: clerk.apiKeys?.getAll ? (params: GetAPIKeysParams) => clerk.apiKeys.getAll(params) : undefined, + fetcher: clerk.apiKeys?.getAll + ? (params: GetAPIKeysParams) => clerk.apiKeys.getAll({ ...params, subject: safeValues.subject }) + : undefined, config: { keepPreviousData: safeValues.keepPreviousData, infinite: safeValues.infinite, @@ -110,7 +112,9 @@ export function useAPIKeys(params?: T): UseAPIKeysRe keys: createCacheKeys({ stablePrefix: STABLE_KEYS.API_KEYS_KEY, authenticated: Boolean(clerk.user), - tracked: {}, + tracked: { + subject: safeValues.subject, + }, untracked: { args: hookParams, }, From e8498ad67cb85e7964669d2460e5b3ddbe2dc463 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 2 Dec 2025 10:42:04 -0800 Subject: [PATCH 7/7] wait for request --- integration/tests/machine-auth/component.test.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/integration/tests/machine-auth/component.test.ts b/integration/tests/machine-auth/component.test.ts index 372590de265..b37527e0f36 100644 --- a/integration/tests/machine-auth/component.test.ts +++ b/integration/tests/machine-auth/component.test.ts @@ -301,23 +301,25 @@ testAgainstRunningApps({ await u.po.organizationSwitcher.waitForMounted(); await u.po.organizationSwitcher.waitForAnOrganizationToSelected(); - // Capture the subject parameter let capturedSubject: string | null = null; - await u.page.route('**/api_keys*', async route => { - const url = new URL(route.request().url()); - capturedSubject = url.searchParams.get('subject'); - await route.continue(); + const apiKeyRequestPromise = u.page.waitForRequest(request => { + if (request.url().includes('api_keys')) { + const url = new URL(request.url()); + capturedSubject = url.searchParams.get('subject'); + return true; + } + return false; }); await u.po.page.goToRelative('/user'); await u.po.userProfile.waitForMounted(); await u.po.userProfile.switchToAPIKeysTab(); + await apiKeyRequestPromise; + // Verify the subject parameter is the user ID, not the organization ID expect(capturedSubject).toBe(userId); expect(capturedSubject).not.toBe(fakeOrganization.organization.id); - - await u.page.unrouteAll(); }); test('standalone API keys component in user context based on user_api_keys_enabled', async ({ page, context }) => {