From f937aa6d896f90a093094d3a618a3eee51ef10e9 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:58:32 -0500 Subject: [PATCH 1/7] chore(e2e): move more instances to PLAPI --- .changeset/cute-berries-beg.md | 2 + integration/configs/oauth-provider.js | 14 +++ integration/configs/sessions-dev-1.js | 15 ++++ integration/configs/sessions-dev-2.js | 15 ++++ integration/configs/with-email-codes.js | 90 +++++++++---------- integration/configs/with-email-links.js | 25 ++++++ integration/configs/with-enterprise-sso.js | 10 +++ integration/configs/with-legal-consent.js | 24 +++++ .../configs/with-needs-client-trust.js | 18 ++++ integration/configs/with-passkeys.js | 21 +++++ integration/configs/with-restricted-mode.js | 23 +++++ integration/configs/with-reverification.js | 34 +++++++ .../with-session-tasks-reset-password.js | 19 ++++ .../configs/with-session-tasks-setup-mfa.js | 34 +++++++ integration/configs/with-session-tasks.js | 33 +++++++ integration/configs/with-waitlist-mode.js | 23 +++++ integration/presets/envs.ts | 9 ++ integration/tests/custom-flows/oauth.test.ts | 2 +- integration/tests/dynamic-keys.test.ts | 2 +- integration/tests/oauth-flows.test.ts | 24 ++--- .../tests/session-tasks-sign-in.test.ts | 2 +- 21 files changed, 379 insertions(+), 60 deletions(-) create mode 100644 .changeset/cute-berries-beg.md create mode 100644 integration/configs/oauth-provider.js create mode 100644 integration/configs/sessions-dev-1.js create mode 100644 integration/configs/sessions-dev-2.js create mode 100644 integration/configs/with-email-links.js create mode 100644 integration/configs/with-enterprise-sso.js create mode 100644 integration/configs/with-legal-consent.js create mode 100644 integration/configs/with-needs-client-trust.js create mode 100644 integration/configs/with-passkeys.js create mode 100644 integration/configs/with-restricted-mode.js create mode 100644 integration/configs/with-reverification.js create mode 100644 integration/configs/with-session-tasks-reset-password.js create mode 100644 integration/configs/with-session-tasks-setup-mfa.js create mode 100644 integration/configs/with-session-tasks.js create mode 100644 integration/configs/with-waitlist-mode.js diff --git a/.changeset/cute-berries-beg.md b/.changeset/cute-berries-beg.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/cute-berries-beg.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/integration/configs/oauth-provider.js b/integration/configs/oauth-provider.js new file mode 100644 index 00000000000..8aa2ffa41ff --- /dev/null +++ b/integration/configs/oauth-provider.js @@ -0,0 +1,14 @@ +import { defineConfig } from '../presets/platformApplication.js'; + +export default defineConfig({ + config: { + auth_attack_protection: { + bot_protection: { + captcha_enabled: false, + }, + }, + auth_password: { + required: false, + }, + }, +}); diff --git a/integration/configs/sessions-dev-1.js b/integration/configs/sessions-dev-1.js new file mode 100644 index 00000000000..bb2f60deacc --- /dev/null +++ b/integration/configs/sessions-dev-1.js @@ -0,0 +1,15 @@ +import { defineConfig } from '../presets/platformApplication.js'; + +export default defineConfig({ + config: { + organization_settings: { + enabled: true, + force_organization_selection: false, + }, + auth_password: { + device_trust: { + enabled: false, + }, + }, + }, +}); diff --git a/integration/configs/sessions-dev-2.js b/integration/configs/sessions-dev-2.js new file mode 100644 index 00000000000..bb2f60deacc --- /dev/null +++ b/integration/configs/sessions-dev-2.js @@ -0,0 +1,15 @@ +import { defineConfig } from '../presets/platformApplication.js'; + +export default defineConfig({ + config: { + organization_settings: { + enabled: true, + force_organization_selection: false, + }, + auth_password: { + device_trust: { + enabled: false, + }, + }, + }, +}); diff --git a/integration/configs/with-email-codes.js b/integration/configs/with-email-codes.js index 38550077eed..15cea4aa707 100644 --- a/integration/configs/with-email-codes.js +++ b/integration/configs/with-email-codes.js @@ -1,10 +1,9 @@ +import { createClerkClient } from '@clerk/backend'; import { parsePublishableKey } from '@clerk/shared/keys'; +import { instanceKeys } from '../presets/instanceKeys'; import { defineConfig } from '../presets/platformApplication.js'; -// this is the oauth-provider instance in the integration testing workspace -const oauthProviderUrl = 'https://honest-wildcat-44.clerk.accounts.dev'; - export default defineConfig({ config: { auth_access_control: { @@ -99,54 +98,55 @@ export default defineConfig({ }, }, }, - setup: async ({ applicationName, clerkClient, publishableKey, patchConfig }) => { + setup: async context => { // setup allowed origins for the electron tests - await clerkClient.instance.update({ allowedOrigins: ['clerk://app'] }); + await context.clerkClient.instance.update({ allowedOrigins: ['clerk://app'] }); + await setup(context); + }, +}); - const parsedPublishableKey = parsePublishableKey(publishableKey); - if (!parsedPublishableKey) { - throw new Error('The created application has an invalid publishable key.'); - } +export async function setup({ applicationName, publishableKey, patchConfig }, consentScreenEnabled = true) { + const parsedPublishableKey = parsePublishableKey(publishableKey); + if (!parsedPublishableKey) { + throw new Error('The created application has an invalid publishable key.'); + } - const registrationResponse = await fetch(`${oauthProviderUrl}/oauth/register`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - client_name: applicationName, - redirect_uris: [`https://${parsedPublishableKey.frontendApi}/v1/oauth_callback`], - }), - }); + const oauthProviderUrl = `https://${parsePublishableKey(instanceKeys.get('oauth-provider').pk).frontendApi}`; - if (!registrationResponse.ok) { - throw new Error(`OAuth client registration failed: ${await registrationResponse.text()}`); - } + const client = await createClerkClient({ + secretKey: instanceKeys.get('oauth-provider').sk, + }).oauthApplications.create({ + name: applicationName, + redirectUris: [`https://${parsedPublishableKey.frontendApi}/v1/oauth_callback`], + scopes: 'profile email', + public: false, + consentScreenEnabled, + }); - const client = await registrationResponse.json(); - if (typeof client.client_id !== 'string' || typeof client.client_secret !== 'string') { - throw new Error('The OAuth client registration response does not contain a client ID and secret.'); - } + if (typeof client.clientId !== 'string' || typeof client.clientSecret !== 'string') { + throw new Error('The OAuth client registration response does not contain a client ID and secret.'); + } - await patchConfig({ - connections_oauth_custom: { - e2e_oauth_provider: { - auth_url: `${oauthProviderUrl}/oauth/authorize`, - authenticatable: true, - base_scopes: [], - client_id: client.client_id, - client_secret: client.client_secret, - discovery_url: `${oauthProviderUrl}/.well-known/openid-configuration`, - enabled: true, - name: 'E2E OAuth Provider', - requires_pkce: false, - token_url: `${oauthProviderUrl}/oauth/token`, - user_info_url: `${oauthProviderUrl}/oauth/userinfo`, - user_mapping: { - id: { - path: 'user_id', - }, + await patchConfig({ + connections_oauth_custom: { + e2e_oauth_provider: { + auth_url: `${oauthProviderUrl}/oauth/authorize`, + authenticatable: true, + base_scopes: [], + client_id: client.clientId, + client_secret: client.clientSecret, + discovery_url: `${oauthProviderUrl}/.well-known/openid-configuration`, + enabled: true, + name: 'E2E OAuth Provider', + requires_pkce: true, + token_url: `${oauthProviderUrl}/oauth/token`, + user_info_url: `${oauthProviderUrl}/oauth/userinfo`, + user_mapping: { + id: { + path: 'user_id', }, }, }, - }); - }, -}); + }, + }); +} diff --git a/integration/configs/with-email-links.js b/integration/configs/with-email-links.js new file mode 100644 index 00000000000..b192e3fdb64 --- /dev/null +++ b/integration/configs/with-email-links.js @@ -0,0 +1,25 @@ +import { defineConfig } from '../presets/platformApplication.js'; + +export default defineConfig({ + config: { + auth_attack_protection: { + bot_protection: { + captcha_enabled: false, + }, + email_link_require_same_client: false, + }, + organization_settings: { + enabled: true, + force_organization_selection: false, + }, + auth_email: { + verification_strategies: ['email_link'], + sign_in_strategies: ['email_link'], + }, + auth_password: { + device_trust: { + enabled: false, + }, + }, + }, +}); diff --git a/integration/configs/with-enterprise-sso.js b/integration/configs/with-enterprise-sso.js new file mode 100644 index 00000000000..511763ccae8 --- /dev/null +++ b/integration/configs/with-enterprise-sso.js @@ -0,0 +1,10 @@ +import { defineConfig } from '../presets/platformApplication.js'; + +export default defineConfig({ + config: { + organization_settings: { + enabled: true, + force_organization_selection: false, + }, + }, +}); diff --git a/integration/configs/with-legal-consent.js b/integration/configs/with-legal-consent.js new file mode 100644 index 00000000000..ee9406dc631 --- /dev/null +++ b/integration/configs/with-legal-consent.js @@ -0,0 +1,24 @@ +import { defineConfig } from '../presets/platformApplication.js'; +import { setup } from './with-email-codes.js'; + +export default defineConfig({ + config: { + auth_attack_protection: { + bot_protection: { + captcha_enabled: false, + }, + }, + organization_settings: { + enabled: true, + force_organization_selection: false, + }, + compliance: { + legal_consent: { + enabled: true, + privacy_policy_url: 'http://example.com/privacy', + terms_of_service_url: 'http://example.com/terms', + }, + }, + }, + setup: context => setup(context, false), +}); diff --git a/integration/configs/with-needs-client-trust.js b/integration/configs/with-needs-client-trust.js new file mode 100644 index 00000000000..ff1b01057bd --- /dev/null +++ b/integration/configs/with-needs-client-trust.js @@ -0,0 +1,18 @@ +import { defineConfig } from '../presets/platformApplication.js'; + +export default defineConfig({ + config: { + organization_settings: { + enabled: true, + force_organization_selection: false, + }, + auth_email: { + sign_in_strategies: [], + }, + auth_password: { + device_trust: { + enabled: true, + }, + }, + }, +}); diff --git a/integration/configs/with-passkeys.js b/integration/configs/with-passkeys.js new file mode 100644 index 00000000000..056a488a030 --- /dev/null +++ b/integration/configs/with-passkeys.js @@ -0,0 +1,21 @@ +import { defineConfig } from '../presets/platformApplication.js'; + +export default defineConfig({ + config: { + organization_settings: { + enabled: true, + force_organization_selection: false, + }, + auth_email: { + sign_in_strategies: [], + }, + auth_password: { + device_trust: { + enabled: false, + }, + }, + auth_passkey: { + used_for_sign_in: true, + }, + }, +}); diff --git a/integration/configs/with-restricted-mode.js b/integration/configs/with-restricted-mode.js new file mode 100644 index 00000000000..c7d4496fe87 --- /dev/null +++ b/integration/configs/with-restricted-mode.js @@ -0,0 +1,23 @@ +import { defineConfig } from '../presets/platformApplication.js'; + +export default defineConfig({ + config: { + auth_attack_protection: { + bot_protection: { + captcha_enabled: false, + }, + }, + organization_settings: { + enabled: true, + force_organization_selection: false, + }, + auth_access_control: { + sign_up_mode: 'restricted', + }, + auth_password: { + device_trust: { + enabled: false, + }, + }, + }, +}); diff --git a/integration/configs/with-reverification.js b/integration/configs/with-reverification.js new file mode 100644 index 00000000000..2fc4037c12c --- /dev/null +++ b/integration/configs/with-reverification.js @@ -0,0 +1,34 @@ +import { defineConfig } from '../presets/platformApplication.js'; + +export default defineConfig({ + config: { + auth_email: { + sign_in_strategies: [], + }, + auth_password: { + device_trust: { + enabled: false, + }, + }, + auth_phone: { + used_for_sign_up: true, + verification_strategies: ['phone_code'], + verify_at_sign_up: true, + }, + organization_settings: { + enabled: true, + force_organization_selection: false, + slug_disabled: false, + organization_creation_defaults: { + enabled: false, + }, + }, + }, + async setup({ clerkClient }) { + await clerkClient.organizationRoles.createOrganizationRole({ + key: 'org:viewer', + name: 'Viewer', + permissions: [], + }); + }, +}); diff --git a/integration/configs/with-session-tasks-reset-password.js b/integration/configs/with-session-tasks-reset-password.js new file mode 100644 index 00000000000..d551388e983 --- /dev/null +++ b/integration/configs/with-session-tasks-reset-password.js @@ -0,0 +1,19 @@ +import { defineConfig } from '../presets/platformApplication.js'; + +export default defineConfig({ + config: { + auth_password: { + device_trust: { + enabled: false, + }, + }, + organization_settings: { + enabled: true, + force_organization_selection: true, + slug_disabled: false, + organization_creation_defaults: { + enabled: false, + }, + }, + }, +}); diff --git a/integration/configs/with-session-tasks-setup-mfa.js b/integration/configs/with-session-tasks-setup-mfa.js new file mode 100644 index 00000000000..4db6e05f4db --- /dev/null +++ b/integration/configs/with-session-tasks-setup-mfa.js @@ -0,0 +1,34 @@ +import { defineConfig } from '../presets/platformApplication.js'; + +export default defineConfig({ + config: { + organization_settings: { + enabled: true, + force_organization_selection: false, + }, + auth_email: { + sign_in_strategies: [], + }, + auth_password: { + device_trust: { + enabled: false, + }, + }, + auth_phone: { + used_for_sign_up: true, + used_for_second_factor: true, + verification_strategies: ['phone_code'], + second_factor_strategies: ['phone_code'], + verify_at_sign_up: true, + }, + auth_multi_factor: { + required_for_sign_up: true, + authenticator_app: { + enabled: true, + }, + backup_code: { + enabled: true, + }, + }, + }, +}); diff --git a/integration/configs/with-session-tasks.js b/integration/configs/with-session-tasks.js new file mode 100644 index 00000000000..4a2c74316c5 --- /dev/null +++ b/integration/configs/with-session-tasks.js @@ -0,0 +1,33 @@ +import { defineConfig } from '../presets/platformApplication.js'; +import { setup } from './with-email-codes.js'; + +export default defineConfig({ + config: { + auth_attack_protection: { + bot_protection: { + captcha_enabled: false, + }, + }, + auth_email: { + sign_in_strategies: [], + verify_at_sign_up: false, + }, + auth_password: { + device_trust: { + enabled: false, + }, + }, + organization_settings: { + enabled: true, + force_organization_selection: true, + slug_disabled: false, + organization_creation_defaults: { + enabled: false, + }, + }, + session_settings: { + multi_session_enabled: true, + }, + }, + setup: context => setup(context, false), +}); diff --git a/integration/configs/with-waitlist-mode.js b/integration/configs/with-waitlist-mode.js new file mode 100644 index 00000000000..7d028757893 --- /dev/null +++ b/integration/configs/with-waitlist-mode.js @@ -0,0 +1,23 @@ +import { defineConfig } from '../presets/platformApplication.js'; + +export default defineConfig({ + config: { + auth_attack_protection: { + bot_protection: { + captcha_enabled: false, + }, + }, + organization_settings: { + enabled: true, + force_organization_selection: false, + }, + auth_access_control: { + sign_up_mode: 'waitlist', + }, + auth_password: { + device_trust: { + enabled: false, + }, + }, + }, +}); diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 88be57a72e6..1a3ab444dd5 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -157,6 +157,8 @@ automatedEnvironmentVariables.forEach(name => { withKeyless.setEnvVariable('private', name, 'false'); }); +const oauthProvider = await withInstanceKeys('oauth-provider', base.clone().setId('oauthProvider')); + const withEmailCodes = await withInstanceKeys( 'with-email-codes', base @@ -185,6 +187,10 @@ const withSharedUIVariant = withEmailCodes const withEmailLinks = await withInstanceKeys('with-email-links', base.clone().setId('withEmailLinks')); +const sessionsDev1 = await withInstanceKeys('sessions-dev-1', base.clone().setId('sessionsDev1')); + +const sessionsDev2 = await withInstanceKeys('sessions-dev-2', base.clone().setId('sessionsDev2')); + const withEnterpriseSso = await withInstanceKeys( 'with-enterprise-sso', base @@ -323,6 +329,9 @@ const withPasskeys = await withInstanceKeys('with-passkeys', base.clone().setId( export const envs = { base, + oauthProvider, + sessionsDev1, + sessionsDev2, sessionsProd1, withAPIKeys, withAPCore3ClerkLatest, diff --git a/integration/tests/custom-flows/oauth.test.ts b/integration/tests/custom-flows/oauth.test.ts index 511ecb17d87..d342bbcc5be 100644 --- a/integration/tests/custom-flows/oauth.test.ts +++ b/integration/tests/custom-flows/oauth.test.ts @@ -86,7 +86,7 @@ test.describe('Custom Flows OAuth @custom', () => { expect(secondPost.method()).toBe('POST'); // Complete the OAuth flow end-to-end and assert we're signed in on the app instance. - await u.page.getByText('Sign in to oauth-provider').waitFor(); + await u.page.getByText(/Sign in to .*oauth-provider/).waitFor(); await u.po.signIn.setIdentifier(fakeUser.email); await u.po.signIn.continue(); await u.po.signIn.enterTestOtpCode(); diff --git a/integration/tests/dynamic-keys.test.ts b/integration/tests/dynamic-keys.test.ts index 378ef00330b..4e770f03338 100644 --- a/integration/tests/dynamic-keys.test.ts +++ b/integration/tests/dynamic-keys.test.ts @@ -30,7 +30,7 @@ test.describe('dynamic keys @nextjs', () => { const count = await client.users?.getCount(); - if (count){ + if (count !== undefined){ return NextResponse.redirect(new URL('/users-count', request.url)); } } diff --git a/integration/tests/oauth-flows.test.ts b/integration/tests/oauth-flows.test.ts index daec351ec98..75052c3510b 100644 --- a/integration/tests/oauth-flows.test.ts +++ b/integration/tests/oauth-flows.test.ts @@ -55,7 +55,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo await u.po.signUp.goTo(); await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); - await u.page.getByText('Sign in to oauth-provider').waitFor(); + await u.page.getByText(/Sign in to .*oauth-provider/).waitFor(); await u.po.signIn.setIdentifier(fakeUser.email); await u.po.signIn.continue(); @@ -74,7 +74,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo await u.po.signIn.goTo(); await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); - await u.page.getByText('Sign in to oauth-provider').waitFor(); + await u.page.getByText(/Sign in to .*oauth-provider/).waitFor(); await u.po.signIn.setIdentifier(fakeUser.email); await u.po.signIn.continue(); @@ -98,7 +98,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo await u.po.signIn.waitForModal(); await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); - await u.page.getByText('Sign in to oauth-provider').waitFor(); + await u.page.getByText(/Sign in to .*oauth-provider/).waitFor(); await u.po.signIn.setIdentifier(fakeUser.email); await u.po.signIn.continue(); @@ -126,7 +126,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo }); await u.po.signIn.waitForModal(); await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); - await u.page.getByText('Sign in to oauth-provider').waitFor(); + await u.page.getByText(/Sign in to .*oauth-provider/).waitFor(); await u.po.signIn.setIdentifier(modalSignUpUser.email); await u.po.signIn.continue(); @@ -187,7 +187,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo await u.po.signUp.waitForModal(); await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); - await u.page.getByText('Sign in to oauth-provider').waitFor(); + await u.page.getByText(/Sign in to .*oauth-provider/).waitFor(); await u.po.signIn.setIdentifier(fakeUser.email); await u.po.signIn.continue(); @@ -223,7 +223,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo // Use OAuth provider await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); - await u.page.getByText('Sign in to oauth-provider').waitFor(); + await u.page.getByText(/Sign in to .*oauth-provider/).waitFor(); // Sign in with existing account await u.po.signIn.setIdentifier(fakeUser.email); @@ -252,7 +252,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); const popup = await popupPromise; const popupUtils = createTestUtils({ app, page: popup, context }); - await popupUtils.page.getByText('Sign in to oauth-provider').waitFor(); + await popupUtils.page.getByText(/Sign in to .*oauth-provider/).waitFor(); await popupUtils.po.signIn.setIdentifier(fakeUser.email); await popupUtils.po.signIn.continue(); @@ -345,7 +345,7 @@ testAgainstRunningApps({ withPattern: ['react.vite.withLegalConsent'] })( await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); const popup = await popupPromise; const popupUtils = createTestUtils({ app, page: popup, context }); - await popupUtils.page.getByText('Sign in to oauth-provider').waitFor(); + await popupUtils.page.getByText(/Sign in to .*oauth-provider/).waitFor(); // Complete OAuth in the popup await popupUtils.po.signIn.setIdentifier(fakeUser.email); @@ -402,7 +402,7 @@ testAgainstRunningApps({ withPattern: ['react.vite.withLegalConsent'] })( await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); const popup = await popupPromise; const popupUtils = createTestUtils({ app, page: popup, context }); - await popupUtils.page.getByText('Sign in to oauth-provider').waitFor(); + await popupUtils.page.getByText(/Sign in to .*oauth-provider/).waitFor(); // Complete OAuth in the popup await popupUtils.po.signIn.setIdentifier(fakeUser.email); @@ -460,7 +460,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withLegalConsent] })( await u.po.signIn.goTo(); await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); - await u.page.getByText('Sign in to oauth-provider').waitFor(); + await u.page.getByText(/Sign in to .*oauth-provider/).waitFor(); await u.po.signIn.setIdentifier(fakeUser.email); await u.po.signIn.continue(); @@ -488,7 +488,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withLegalConsent] })( await u.po.signIn.waitForModal(); await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); - await u.page.getByText('Sign in to oauth-provider').waitFor(); + await u.page.getByText(/Sign in to .*oauth-provider/).waitFor(); await u.po.signIn.setIdentifier(fakeUser.email); await u.po.signIn.continue(); @@ -519,7 +519,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withLegalConsent] })( // Sign in via OAuth on the first tab await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); - await u.page.getByText('Sign in to oauth-provider').waitFor(); + await u.page.getByText(/Sign in to .*oauth-provider/).waitFor(); await u.po.signIn.setIdentifier(fakeUser.email); await u.po.signIn.continue(); await u.po.signIn.enterTestOtpCode(); diff --git a/integration/tests/session-tasks-sign-in.test.ts b/integration/tests/session-tasks-sign-in.test.ts index a48611c57d6..ba987b85d54 100644 --- a/integration/tests/session-tasks-sign-in.test.ts +++ b/integration/tests/session-tasks-sign-in.test.ts @@ -78,7 +78,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( // Performs sign-in with SSO await u.po.signIn.goTo(); await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); - await u.page.getByText('Sign in to oauth-provider').waitFor(); + await u.page.getByText(/Sign in to .*oauth-provider/).waitFor(); await u.po.signIn.setIdentifier(userFromOAuth.email); await u.po.signIn.continue(); await u.po.signIn.enterTestOtpCode(); From fb4cf00d0df4bd30e38e8e22906adfc0a78aad70 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:29:17 -0500 Subject: [PATCH 2/7] share oauth provider setup --- integration/configs/with-email-codes.js | 53 +---------------------- integration/configs/with-legal-consent.js | 4 +- integration/configs/with-session-tasks.js | 4 +- integration/presets/setupOAuthProvider.js | 53 +++++++++++++++++++++++ 4 files changed, 59 insertions(+), 55 deletions(-) create mode 100644 integration/presets/setupOAuthProvider.js diff --git a/integration/configs/with-email-codes.js b/integration/configs/with-email-codes.js index 15cea4aa707..f348085c371 100644 --- a/integration/configs/with-email-codes.js +++ b/integration/configs/with-email-codes.js @@ -1,8 +1,5 @@ -import { createClerkClient } from '@clerk/backend'; -import { parsePublishableKey } from '@clerk/shared/keys'; - -import { instanceKeys } from '../presets/instanceKeys'; import { defineConfig } from '../presets/platformApplication.js'; +import { setupOAuthProvider } from '../presets/setupOAuthProvider.js'; export default defineConfig({ config: { @@ -101,52 +98,6 @@ export default defineConfig({ setup: async context => { // setup allowed origins for the electron tests await context.clerkClient.instance.update({ allowedOrigins: ['clerk://app'] }); - await setup(context); + await setupOAuthProvider(context); }, }); - -export async function setup({ applicationName, publishableKey, patchConfig }, consentScreenEnabled = true) { - const parsedPublishableKey = parsePublishableKey(publishableKey); - if (!parsedPublishableKey) { - throw new Error('The created application has an invalid publishable key.'); - } - - const oauthProviderUrl = `https://${parsePublishableKey(instanceKeys.get('oauth-provider').pk).frontendApi}`; - - const client = await createClerkClient({ - secretKey: instanceKeys.get('oauth-provider').sk, - }).oauthApplications.create({ - name: applicationName, - redirectUris: [`https://${parsedPublishableKey.frontendApi}/v1/oauth_callback`], - scopes: 'profile email', - public: false, - consentScreenEnabled, - }); - - if (typeof client.clientId !== 'string' || typeof client.clientSecret !== 'string') { - throw new Error('The OAuth client registration response does not contain a client ID and secret.'); - } - - await patchConfig({ - connections_oauth_custom: { - e2e_oauth_provider: { - auth_url: `${oauthProviderUrl}/oauth/authorize`, - authenticatable: true, - base_scopes: [], - client_id: client.clientId, - client_secret: client.clientSecret, - discovery_url: `${oauthProviderUrl}/.well-known/openid-configuration`, - enabled: true, - name: 'E2E OAuth Provider', - requires_pkce: true, - token_url: `${oauthProviderUrl}/oauth/token`, - user_info_url: `${oauthProviderUrl}/oauth/userinfo`, - user_mapping: { - id: { - path: 'user_id', - }, - }, - }, - }, - }); -} diff --git a/integration/configs/with-legal-consent.js b/integration/configs/with-legal-consent.js index ee9406dc631..c5b8f72f3fb 100644 --- a/integration/configs/with-legal-consent.js +++ b/integration/configs/with-legal-consent.js @@ -1,5 +1,5 @@ import { defineConfig } from '../presets/platformApplication.js'; -import { setup } from './with-email-codes.js'; +import { setupOAuthProvider } from '../presets/setupOAuthProvider.js'; export default defineConfig({ config: { @@ -20,5 +20,5 @@ export default defineConfig({ }, }, }, - setup: context => setup(context, false), + setup: context => setupOAuthProvider(context, { consentScreenEnabled: false }), }); diff --git a/integration/configs/with-session-tasks.js b/integration/configs/with-session-tasks.js index 4a2c74316c5..a57b3939460 100644 --- a/integration/configs/with-session-tasks.js +++ b/integration/configs/with-session-tasks.js @@ -1,5 +1,5 @@ import { defineConfig } from '../presets/platformApplication.js'; -import { setup } from './with-email-codes.js'; +import { setupOAuthProvider } from '../presets/setupOAuthProvider.js'; export default defineConfig({ config: { @@ -29,5 +29,5 @@ export default defineConfig({ multi_session_enabled: true, }, }, - setup: context => setup(context, false), + setup: context => setupOAuthProvider(context, { consentScreenEnabled: false }), }); diff --git a/integration/presets/setupOAuthProvider.js b/integration/presets/setupOAuthProvider.js new file mode 100644 index 00000000000..361217084b7 --- /dev/null +++ b/integration/presets/setupOAuthProvider.js @@ -0,0 +1,53 @@ +import { createClerkClient } from '@clerk/backend'; +import { parsePublishableKey } from '@clerk/shared/keys'; + +import { instanceKeys } from './instanceKeys'; + +export async function setupOAuthProvider( + { applicationName, publishableKey, patchConfig }, + { consentScreenEnabled = true } = {}, +) { + const parsedPublishableKey = parsePublishableKey(publishableKey); + if (!parsedPublishableKey) { + throw new Error('The created application has an invalid publishable key.'); + } + + const oauthProviderUrl = `https://${parsePublishableKey(instanceKeys.get('oauth-provider').pk).frontendApi}`; + + const client = await createClerkClient({ + secretKey: instanceKeys.get('oauth-provider').sk, + }).oauthApplications.create({ + name: applicationName, + redirectUris: [`https://${parsedPublishableKey.frontendApi}/v1/oauth_callback`], + scopes: 'profile email', + public: false, + consentScreenEnabled, + }); + + if (typeof client.clientId !== 'string' || typeof client.clientSecret !== 'string') { + throw new Error('The OAuth client registration response does not contain a client ID and secret.'); + } + + await patchConfig({ + connections_oauth_custom: { + e2e_oauth_provider: { + auth_url: `${oauthProviderUrl}/oauth/authorize`, + authenticatable: true, + base_scopes: [], + client_id: client.clientId, + client_secret: client.clientSecret, + discovery_url: `${oauthProviderUrl}/.well-known/openid-configuration`, + enabled: true, + name: 'E2E OAuth Provider', + requires_pkce: true, + token_url: `${oauthProviderUrl}/oauth/token`, + user_info_url: `${oauthProviderUrl}/oauth/userinfo`, + user_mapping: { + id: { + path: 'user_id', + }, + }, + }, + }, + }); +} From cb84d0bcb3b30f00c0b03ed8e55d1c3a1eda3f94 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:29:55 -0500 Subject: [PATCH 3/7] use run key instead of e2e app key --- integration/presets/envs.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 1a3ab444dd5..4ec9c931433 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -39,7 +39,7 @@ const getPlatformApplication = async ( if (!platformApiKey) { throw new Error('CLERK_PLATFORM_API_KEY is required to create a Platform API application.'); } - if (!constants.E2E_APP_ID) { + if (!constants.INTEGRATION_TEST_RUN_KEY && !constants.E2E_APP_ID) { const application = await createApplicationFromConfig( platformApiKey, keyName, @@ -52,8 +52,7 @@ const getPlatformApplication = async ( const cacheKey = createHash('sha256') .update(keyName) .update(JSON.stringify(definition.config)) - .update(constants.INTEGRATION_TEST_RUN_KEY || '') - .update(constants.E2E_APP_ID) + .update(constants.INTEGRATION_TEST_RUN_KEY || constants.E2E_APP_ID || '') .digest('hex'); const cachePath = resolve(constants.TMP_DIR, 'platform-applications', `${cacheKey}.json`); platformApplicationCachePaths.add(cachePath); @@ -278,13 +277,10 @@ const withSignInOrUpEmailLinksFlow = withEmailLinks .setId('withSignInOrUpEmailLinksFlow') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', undefined); -const withSignInOrUpwithRestrictedModeFlow = await withInstanceKeys( - 'with-restricted-mode', - withEmailCodes - .clone() - .setId('withSignInOrUpwithRestrictedModeFlow') - .setEnvVariable('public', 'CLERK_SIGN_UP_URL', undefined), -); +const withSignInOrUpwithRestrictedModeFlow = withRestrictedMode + .clone() + .setId('withSignInOrUpwithRestrictedModeFlow') + .setEnvVariable('public', 'CLERK_SIGN_UP_URL', undefined); const withSessionTasks = await withInstanceKeys( 'with-session-tasks', From fd1ff21a44d9839ca316bf2f9ebfb59b0adf4614 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:41:18 -0500 Subject: [PATCH 4/7] only create applications on instance resolution --- integration/models/application.ts | 1 + integration/models/environment.ts | 16 +++ integration/models/longRunningApplication.ts | 8 +- integration/presets/envs.ts | 110 ++++++++++++++---- integration/presets/setupOAuthProvider.js | 7 +- integration/tests/chrome-extension/helpers.ts | 2 + integration/tests/custom-flows/oauth.test.ts | 6 +- integration/tests/electron/fixtures.ts | 1 + .../localhost-switch-instance.test.ts | 2 +- integration/tests/oauth-flows.test.ts | 18 +-- .../tests/session-tasks-sign-in.test.ts | 6 +- .../tests/session-tasks-sign-up.test.ts | 6 +- integration/tests/sessions/utils.ts | 10 +- 13 files changed, 141 insertions(+), 52 deletions(-) diff --git a/integration/models/application.ts b/integration/models/application.ts index b8ca49851e0..033dc82fd2c 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -80,6 +80,7 @@ export const application = ( return state.env; }, withEnv: async (env: EnvironmentConfig) => { + await env.resolve(); state.env = env; return envWriter(appDirPath, env); }, diff --git a/integration/models/environment.ts b/integration/models/environment.ts index 5aa6a3a39a9..c678a50ec86 100644 --- a/integration/models/environment.ts +++ b/integration/models/environment.ts @@ -12,16 +12,29 @@ export type EnvironmentConfig = { toJson(): { public: Record; private: Record }; fromJson(json: ReturnType): EnvironmentConfig; clone(): EnvironmentConfig; + setResolver(resolver: (env: EnvironmentConfig) => Promise): EnvironmentConfig; + resolve(): Promise; }; export const environmentConfig = () => { let id = ''; + let resolver: ((env: EnvironmentConfig) => Promise) | undefined; + let resolution: Promise | undefined; const envVars: EnvironmentVariables = { public: new Map(), private: new Map(), }; const self: EnvironmentConfig = { + setResolver: value => { + resolver = value; + resolution = undefined; + return self; + }, + resolve: () => { + resolution ||= resolver ? resolver(self) : Promise.resolve(); + return resolution; + }, setId: (newId: string) => { id = newId; return self; @@ -54,6 +67,9 @@ export const environmentConfig = () => { const res = environmentConfig(); envVars.private.forEach((v, k) => res.setEnvVariable('private', k, v)); envVars.public.forEach((v, k) => res.setEnvVariable('public', k, v)); + if (resolver) { + res.setResolver(resolver); + } return res; }, }; diff --git a/integration/models/longRunningApplication.ts b/integration/models/longRunningApplication.ts index 18be6c14204..62f3c320687 100644 --- a/integration/models/longRunningApplication.ts +++ b/integration/models/longRunningApplication.ts @@ -5,7 +5,6 @@ import { awaitableTreekill, fs } from '../scripts'; import type { Application } from './application'; import type { ApplicationConfig } from './applicationConfig'; import type { EnvironmentConfig } from './environment'; -import { environmentConfig } from './environment'; import { stateFile } from './stateFile'; const getPort = (_url: string) => { @@ -40,7 +39,7 @@ export const longRunningApplication = (params: LongRunningApplicationParams) => let port = getPort(params.serverUrl); let serverUrl: string = params.serverUrl; let appDir: string; - let env: EnvironmentConfig = params.env; + const env: EnvironmentConfig = params.env; const readFromStateFile = () => { if (!stateFile.getLongRunningApps() || [port, serverUrl, pid, appDir, env].filter(Boolean).length === 0) { @@ -51,7 +50,9 @@ export const longRunningApplication = (params: LongRunningApplicationParams) => serverUrl ||= data.serverUrl; pid ||= data.pid; appDir ||= data.appDir; - env ||= environmentConfig().fromJson(data.env); + if (data.env) { + env.fromJson(data.env); + } }; const self = new Proxy( @@ -62,6 +63,7 @@ export const longRunningApplication = (params: LongRunningApplicationParams) => const log = (msg: string) => console.log(`[${name}] ${msg}`); log('Starting init...'); try { + await params.env.resolve(); const publishableKey = params.env.publicVariables.get('CLERK_PUBLISHABLE_KEY'); const secretKey = params.env.privateVariables.get('CLERK_SECRET_KEY'); const apiUrl = params.env.privateVariables.get('CLERK_API_URL'); diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 4ec9c931433..53da8970a74 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -1,7 +1,9 @@ import { createHash } from 'node:crypto'; +import { open } from 'node:fs/promises'; import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; +import { retry } from '@clerk/shared/retry'; import { automatedEnvironmentVariables } from '@clerk/shared/utils'; import fs from 'fs-extra'; @@ -9,17 +11,25 @@ import { constants } from '../constants'; import type { EnvironmentConfig } from '../models/environment'; import { environmentConfig } from '../models/environment'; import { instanceKeys } from './instanceKeys'; -import type { PlatformApplication, PlatformApplicationConfig } from './platformApplication'; +import type { InstanceKeys, PlatformApplication, PlatformApplicationConfig } from './platformApplication'; import { createApplicationFromConfig } from './platformApplication'; export { instanceKeys }; const STAGING_API_URL = 'https://api.clerkstage.dev'; const STAGING_KEY_PREFIX = 'clerkstage-'; -const platformApplicationCachePaths = new Set(); +const canProvisionPlatformApplications = process.env.E2E_STAGING !== '1' && Boolean(constants.CLERK_PLATFORM_API_KEY); +const platformApplicationCacheDir = resolve( + constants.TMP_DIR, + 'platform-applications', + createHash('sha256') + .update(constants.INTEGRATION_TEST_RUN_KEY || constants.E2E_APP_ID || '') + .digest('hex'), +); +const instanceResolutions = new Map>(); export const removePlatformApplicationCache = async () => { - await Promise.all([...platformApplicationCachePaths].map(cachePath => fs.remove(cachePath))); + await fs.remove(platformApplicationCacheDir); }; const isPlatformApplication = (value: unknown): value is PlatformApplication => { @@ -54,8 +64,7 @@ const getPlatformApplication = async ( .update(JSON.stringify(definition.config)) .update(constants.INTEGRATION_TEST_RUN_KEY || constants.E2E_APP_ID || '') .digest('hex'); - const cachePath = resolve(constants.TMP_DIR, 'platform-applications', `${cacheKey}.json`); - platformApplicationCachePaths.add(cachePath); + const cachePath = resolve(platformApplicationCacheDir, `${cacheKey}.json`); const cached = (await fs.pathExists(cachePath)) ? await fs.readJSON(cachePath, { throws: false }) : null; if (isPlatformApplication(cached)) { @@ -63,15 +72,33 @@ const getPlatformApplication = async ( return cached; } - const application = await createApplicationFromConfig( - platformApiKey, - keyName, - definition, - constants.INTEGRATION_TEST_RUN_KEY, - ); - await fs.outputJSON(cachePath, application, { mode: 0o600 }); - console.log(`Created Platform API application ${application.applicationId} for ${keyName}.`); - return application; + await fs.ensureDir(platformApplicationCacheDir); + const lockPath = `${cachePath}.lock`; + const startedAt = Date.now(); + const lock = await retry(() => open(lockPath, 'wx', 0o600), { + maxDelayBetweenRetries: 1000, + shouldRetry: error => (error as NodeJS.ErrnoException).code === 'EEXIST' && Date.now() - startedAt < 120_000, + }); + + try { + const cached = (await fs.pathExists(cachePath)) ? await fs.readJSON(cachePath, { throws: false }) : null; + if (isPlatformApplication(cached)) { + console.log(`Using Platform API application ${cached.applicationId} for ${keyName}.`); + return cached; + } + const application = await createApplicationFromConfig( + platformApiKey, + keyName, + definition, + constants.INTEGRATION_TEST_RUN_KEY, + ); + await fs.outputJSON(cachePath, application, { mode: 0o600 }); + console.log(`Created Platform API application ${application.applicationId} for ${keyName}.`); + return application; + } finally { + await lock.close(); + await fs.remove(lockPath); + } }; const loadPlatformApplicationConfig = async (configPath: string): Promise => { @@ -89,6 +116,36 @@ const loadPlatformApplicationConfig = async (configPath: string): Promise => { + let keys = instanceKeys.get(keyName); + + if (canProvisionPlatformApplications) { + const configPath = resolve(import.meta.dirname, '..', 'configs', `${keyName}.js`); + if (await fs.pathExists(configPath)) { + const definition = await loadPlatformApplicationConfig(configPath); + keys = await getPlatformApplication(keyName, definition); + } + } + + if (!keys) { + throw new Error(`No instance keys found for ${keyName}.`); + } + + instanceKeys.set(keyName, keys); + return keys; +}; + +export const resolveInstanceKeys = (keyName: string): Promise => { + const cached = instanceResolutions.get(keyName); + if (cached) { + return cached; + } + + const resolution = loadInstanceKeys(keyName); + instanceResolutions.set(keyName, resolution); + return resolution; +}; + /** * Check whether an env config is ready for staging tests. * In non-staging mode, always returns true. @@ -103,7 +160,7 @@ export function isStagingReady(env: EnvironmentConfig): boolean { } /** - * Creates an application from a matching config file or sets PK/SK from the instance keys map. + * Sets PK/SK from the instance keys map and defers matching application creation until env.resolve() * When E2E_STAGING=1 is set, swaps PK/SK to staging keys (looked up as `clerkstage-`) * and adds CLERK_API_URL. If the staging key doesn't exist, removes any inherited CLERK_API_URL * so the config falls back to production and is filtered from long-running apps by isStagingReady. @@ -111,15 +168,18 @@ export function isStagingReady(env: EnvironmentConfig): boolean { */ async function withInstanceKeys(keyName: string, env: EnvironmentConfig): Promise { const configPath = resolve(import.meta.dirname, '..', 'configs', `${keyName}.js`); - // if we're not testing against staging, and the keyName provided matches a config file on disk, and we have a PLAPI - // key, create an application and obtain its keys, otherwise use the existing instance keys - const keys = - process.env.E2E_STAGING !== '1' && (await fs.pathExists(configPath)) && constants.CLERK_PLATFORM_API_KEY - ? await getPlatformApplication(keyName, await loadPlatformApplicationConfig(configPath)) - : instanceKeys.get(keyName)!; - instanceKeys.set(keyName, keys); + const keys = instanceKeys.get(keyName)!; env.setEnvVariable('private', 'CLERK_SECRET_KEY', keys.sk).setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', keys.pk); + if (canProvisionPlatformApplications && (await fs.pathExists(configPath))) { + env.setResolver(async target => { + const resolved = await resolveInstanceKeys(keyName); + target + .setEnvVariable('private', 'CLERK_SECRET_KEY', resolved.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', resolved.pk); + }); + } + if (process.env.E2E_STAGING !== '1') { return env; } @@ -251,7 +311,11 @@ const withDynamicKeys = withEmailCodes .clone() .setId('withDynamicKeys') .setEnvVariable('private', 'CLERK_SECRET_KEY', '') - .setEnvVariable('private', 'CLERK_DYNAMIC_SECRET_KEY', withEmailCodes.privateVariables.get('CLERK_SECRET_KEY')); + .setResolver(async env => { + const keys = await resolveInstanceKeys('with-email-codes'); + env.setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', keys.pk); + env.setEnvVariable('private', 'CLERK_DYNAMIC_SECRET_KEY', keys.sk); + }); const withRestrictedMode = await withInstanceKeys( 'with-restricted-mode', diff --git a/integration/presets/setupOAuthProvider.js b/integration/presets/setupOAuthProvider.js index 361217084b7..2f55c266804 100644 --- a/integration/presets/setupOAuthProvider.js +++ b/integration/presets/setupOAuthProvider.js @@ -1,7 +1,7 @@ import { createClerkClient } from '@clerk/backend'; import { parsePublishableKey } from '@clerk/shared/keys'; -import { instanceKeys } from './instanceKeys'; +import { resolveInstanceKeys } from './envs'; export async function setupOAuthProvider( { applicationName, publishableKey, patchConfig }, @@ -12,10 +12,11 @@ export async function setupOAuthProvider( throw new Error('The created application has an invalid publishable key.'); } - const oauthProviderUrl = `https://${parsePublishableKey(instanceKeys.get('oauth-provider').pk).frontendApi}`; + const providerKeys = await resolveInstanceKeys('oauth-provider'); + const oauthProviderUrl = `https://${parsePublishableKey(providerKeys.pk).frontendApi}`; const client = await createClerkClient({ - secretKey: instanceKeys.get('oauth-provider').sk, + secretKey: providerKeys.sk, }).oauthApplications.create({ name: applicationName, redirectUris: [`https://${parsedPublishableKey.frontendApi}/v1/oauth_callback`], diff --git a/integration/tests/chrome-extension/helpers.ts b/integration/tests/chrome-extension/helpers.ts index f68c4c578e1..8b10b67417b 100644 --- a/integration/tests/chrome-extension/helpers.ts +++ b/integration/tests/chrome-extension/helpers.ts @@ -28,6 +28,7 @@ export function getAuthFromBackground( * Set up Clerk testing environment (clerkSetup) for extension tests that use build() instead of dev(). */ export async function setupClerkTestingEnv(env: EnvironmentConfig) { + await env.resolve(); const publishableKey = env.publicVariables.get('CLERK_PUBLISHABLE_KEY'); const secretKey = env.privateVariables.get('CLERK_SECRET_KEY'); const apiUrl = env.privateVariables.get('CLERK_API_URL'); @@ -80,6 +81,7 @@ export async function getExtensionId(context: BrowserContext) { * Create a fake user from an env config and register it via the Backend API. */ export async function createTestUser(env: EnvironmentConfig, test: PlaywrightTest): Promise { + await env.resolve(); const clerkClient = withRetry( backendCreateClerkClient({ apiUrl: env.privateVariables.get('CLERK_API_URL'), diff --git a/integration/tests/custom-flows/oauth.test.ts b/integration/tests/custom-flows/oauth.test.ts index d342bbcc5be..5ddbb33a4d4 100644 --- a/integration/tests/custom-flows/oauth.test.ts +++ b/integration/tests/custom-flows/oauth.test.ts @@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test'; import type { Application } from '../../models/application'; import { appConfigs } from '../../presets'; -import { instanceKeys } from '../../presets/envs'; +import { resolveInstanceKeys } from '../../presets/envs'; import type { FakeUser } from '../../testUtils'; import { createTestUtils } from '../../testUtils'; import { withRetry } from '../../testUtils/retryableClerkClient'; @@ -23,8 +23,8 @@ test.describe('Custom Flows OAuth @custom', () => { await app.dev(); const client = createClerkClient({ - secretKey: instanceKeys.get('oauth-provider').sk, - publishableKey: instanceKeys.get('oauth-provider').pk, + secretKey: (await resolveInstanceKeys('oauth-provider')).sk, + publishableKey: (await resolveInstanceKeys('oauth-provider')).pk, }); const users = createUserService(withRetry(client)); fakeUser = users.createFakeUser(test, { withUsername: true }); diff --git a/integration/tests/electron/fixtures.ts b/integration/tests/electron/fixtures.ts index 4b88d655f4d..aa915a46cf6 100644 --- a/integration/tests/electron/fixtures.ts +++ b/integration/tests/electron/fixtures.ts @@ -23,6 +23,7 @@ const electronExecutable = (app: Application) => path.resolve(app.appDir, 'node_modules', '.bin', process.platform === 'win32' ? 'electron.cmd' : 'electron'); async function setupClerkTestingEnv(env: EnvironmentConfig) { + await env.resolve(); const publishableKey = env.publicVariables.get('CLERK_PUBLISHABLE_KEY'); const secretKey = env.privateVariables.get('CLERK_SECRET_KEY'); const apiUrl = env.privateVariables.get('CLERK_API_URL'); diff --git a/integration/tests/localhost/localhost-switch-instance.test.ts b/integration/tests/localhost/localhost-switch-instance.test.ts index 21852d9a6a8..21fb2e4d0aa 100644 --- a/integration/tests/localhost/localhost-switch-instance.test.ts +++ b/integration/tests/localhost/localhost-switch-instance.test.ts @@ -44,7 +44,7 @@ test.describe('switching instances on localhost same port @localhost', () => { await app.stop(); // Create app and user for the 2nd app with a different instance key - await app.withEnv(getEnvForMultiAppInstance('sessions-dev-2')); + await app.withEnv(await getEnvForMultiAppInstance('sessions-dev-2')); await app.dev({ port }); page = await context.newPage(); diff --git a/integration/tests/oauth-flows.test.ts b/integration/tests/oauth-flows.test.ts index 75052c3510b..4f9bc43e9d6 100644 --- a/integration/tests/oauth-flows.test.ts +++ b/integration/tests/oauth-flows.test.ts @@ -3,7 +3,7 @@ import type { Page } from '@playwright/test'; import { expect, test } from '@playwright/test'; import { appConfigs } from '../presets'; -import { instanceKeys } from '../presets/envs'; +import { resolveInstanceKeys } from '../presets/envs'; import type { FakeUser } from '../testUtils'; import { createTestUtils, testAgainstRunningApps } from '../testUtils'; import { withRetry } from '../testUtils/retryableClerkClient'; @@ -24,8 +24,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo test.beforeAll(async () => { // Create a clerkClient for the OAuth provider instance. const client = createClerkClient({ - secretKey: instanceKeys.get('oauth-provider').sk, - publishableKey: instanceKeys.get('oauth-provider').pk, + secretKey: (await resolveInstanceKeys('oauth-provider')).sk, + publishableKey: (await resolveInstanceKeys('oauth-provider')).pk, }); const users = createUserService(withRetry(client)); fakeUser = users.createFakeUser(test, { @@ -317,8 +317,8 @@ testAgainstRunningApps({ withPattern: ['react.vite.withLegalConsent'] })( test.beforeAll(async () => { const client = createClerkClient({ - secretKey: instanceKeys.get('oauth-provider').sk, - publishableKey: instanceKeys.get('oauth-provider').pk, + secretKey: (await resolveInstanceKeys('oauth-provider')).sk, + publishableKey: (await resolveInstanceKeys('oauth-provider')).pk, }); const users = createUserService(withRetry(client)); fakeUser = users.createFakeUser(test, { @@ -374,8 +374,8 @@ testAgainstRunningApps({ withPattern: ['react.vite.withLegalConsent'] })( test.beforeAll(async () => { const client = createClerkClient({ - secretKey: instanceKeys.get('oauth-provider').sk, - publishableKey: instanceKeys.get('oauth-provider').pk, + secretKey: (await resolveInstanceKeys('oauth-provider')).sk, + publishableKey: (await resolveInstanceKeys('oauth-provider')).pk, }); const users = createUserService(withRetry(client)); fakeUser = users.createFakeUser(test, { @@ -435,8 +435,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withLegalConsent] })( test.beforeAll(async () => { // Create a clerkClient for the OAuth provider instance. const client = createClerkClient({ - secretKey: instanceKeys.get('oauth-provider').sk, - publishableKey: instanceKeys.get('oauth-provider').pk, + secretKey: (await resolveInstanceKeys('oauth-provider')).sk, + publishableKey: (await resolveInstanceKeys('oauth-provider')).pk, }); const users = createUserService(withRetry(client)); fakeUser = users.createFakeUser(test, { diff --git a/integration/tests/session-tasks-sign-in.test.ts b/integration/tests/session-tasks-sign-in.test.ts index ba987b85d54..d3936d2883c 100644 --- a/integration/tests/session-tasks-sign-in.test.ts +++ b/integration/tests/session-tasks-sign-in.test.ts @@ -2,7 +2,7 @@ import { createClerkClient } from '@clerk/backend'; import { test } from '@playwright/test'; import { appConfigs } from '../presets'; -import { instanceKeys } from '../presets/envs'; +import { resolveInstanceKeys } from '../presets/envs'; import type { FakeUser } from '../testUtils'; import { createTestUtils, testAgainstRunningApps } from '../testUtils'; import { withRetry } from '../testUtils/retryableClerkClient'; @@ -65,8 +65,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( // Create a clerkClient for the OAuth provider instance const client = createClerkClient({ - secretKey: instanceKeys.get('oauth-provider').sk, - publishableKey: instanceKeys.get('oauth-provider').pk, + secretKey: (await resolveInstanceKeys('oauth-provider')).sk, + publishableKey: (await resolveInstanceKeys('oauth-provider')).pk, }); const users = createUserService(withRetry(client)); const userFromOAuth = users.createFakeUser(test, { diff --git a/integration/tests/session-tasks-sign-up.test.ts b/integration/tests/session-tasks-sign-up.test.ts index 33f0cab0f8b..bdc5e15274a 100644 --- a/integration/tests/session-tasks-sign-up.test.ts +++ b/integration/tests/session-tasks-sign-up.test.ts @@ -2,7 +2,7 @@ import { createClerkClient } from '@clerk/backend'; import { expect, test } from '@playwright/test'; import { appConfigs } from '../presets'; -import { instanceKeys } from '../presets/envs'; +import { resolveInstanceKeys } from '../presets/envs'; import type { FakeUser } from '../testUtils'; import { createTestUtils, testAgainstRunningApps } from '../testUtils'; import { withRetry } from '../testUtils/retryableClerkClient'; @@ -37,8 +37,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( // Delete user from OAuth provider instance const client = createClerkClient({ - secretKey: instanceKeys.get('oauth-provider').sk, - publishableKey: instanceKeys.get('oauth-provider').pk, + secretKey: (await resolveInstanceKeys('oauth-provider')).sk, + publishableKey: (await resolveInstanceKeys('oauth-provider')).pk, }); const users = createUserService(withRetry(client)); await users.deleteIfExists({ email: fakeUserForOAuth.email }); diff --git a/integration/tests/sessions/utils.ts b/integration/tests/sessions/utils.ts index 882bcf4080b..8e36b6d8836 100644 --- a/integration/tests/sessions/utils.ts +++ b/integration/tests/sessions/utils.ts @@ -1,10 +1,12 @@ import { appConfigs } from '../../presets'; +import { resolveInstanceKeys } from '../../presets/envs'; -export const getEnvForMultiAppInstance = (envKey: string) => { +export const getEnvForMultiAppInstance = async (envKey: string) => { + const keys = await resolveInstanceKeys(envKey); const res = appConfigs.envs.base .clone() - .setEnvVariable('private', 'CLERK_SECRET_KEY', appConfigs.secrets.instanceKeys.get(envKey).sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', appConfigs.secrets.instanceKeys.get(envKey).pk); + .setEnvVariable('private', 'CLERK_SECRET_KEY', keys.sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', keys.pk); if (envKey.includes('clerkstage')) { res.setEnvVariable('private', 'CLERK_API_URL', 'https://api.clerkstage.dev'); @@ -16,7 +18,7 @@ export const getEnvForMultiAppInstance = (envKey: string) => { export const prepareApplication = async (envKey: string, port?: number) => { const app = await appConfigs.next.appRouter.clone().commit(); await app.setup(); - await app.withEnv(getEnvForMultiAppInstance(envKey)); + await app.withEnv(await getEnvForMultiAppInstance(envKey)); const { serverUrl } = await app.dev({ port }); return { app, serverUrl }; }; From e85b19388eed9c5af3f84b779f7502cf3e904adf Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:54:45 -0500 Subject: [PATCH 5/7] resolve keys before reading them --- integration/tests/next-quickstart-keyless.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/integration/tests/next-quickstart-keyless.test.ts b/integration/tests/next-quickstart-keyless.test.ts index 7ff28369c58..2f8c9b31fe9 100644 --- a/integration/tests/next-quickstart-keyless.test.ts +++ b/integration/tests/next-quickstart-keyless.test.ts @@ -51,6 +51,7 @@ test.describe('Keyless mode @quickstart', () => { * Seed claimed keyless state directly: the SDK no longer mints keys, so write the * keys fixture to `.clerk/.tmp/keyless.json` and copy the matching keys into `.env`. */ + await appConfigs.envs.withEmailCodes.resolve(); const publishableKey = appConfigs.envs.withEmailCodes.publicVariables.get('CLERK_PUBLISHABLE_KEY'); const secretKey = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_SECRET_KEY'); await fs.ensureDir(path.join(app.appDir, '.clerk', '.tmp')); From 6af8ad12a1c93ce84ad5a3f5ee9efa4a7d33812e Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:42:38 -0500 Subject: [PATCH 6/7] use proper-lockfile instead of rolling our own --- integration/presets/envs.ts | 15 ++++++--------- package.json | 2 ++ pnpm-lock.yaml | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 53da8970a74..e935a33adac 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -1,11 +1,10 @@ import { createHash } from 'node:crypto'; -import { open } from 'node:fs/promises'; import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; -import { retry } from '@clerk/shared/retry'; import { automatedEnvironmentVariables } from '@clerk/shared/utils'; import fs from 'fs-extra'; +import lockfile from 'proper-lockfile'; import { constants } from '../constants'; import type { EnvironmentConfig } from '../models/environment'; @@ -73,11 +72,10 @@ const getPlatformApplication = async ( } await fs.ensureDir(platformApplicationCacheDir); - const lockPath = `${cachePath}.lock`; - const startedAt = Date.now(); - const lock = await retry(() => open(lockPath, 'wx', 0o600), { - maxDelayBetweenRetries: 1000, - shouldRetry: error => (error as NodeJS.ErrnoException).code === 'EEXIST' && Date.now() - startedAt < 120_000, + const release = await lockfile.lock(cachePath, { + realpath: false, + stale: 30_000, + retries: { retries: 120, factor: 1, minTimeout: 1000, maxTimeout: 1000 }, }); try { @@ -96,8 +94,7 @@ const getPlatformApplication = async ( console.log(`Created Platform API application ${application.applicationId} for ${keyName}.`); return application; } finally { - await lock.close(); - await fs.remove(lockPath); + await release(); } }; diff --git a/package.json b/package.json index c7c706f5e61..e5a0c872cf6 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "@testing-library/user-event": "^14.6.5", "@types/cross-spawn": "^6.0.6", "@types/node": "^22.20.1", + "@types/proper-lockfile": "^4.1.4", "@types/react": "catalog:react", "@types/react-dom": "catalog:react", "@vitejs/plugin-react": "^4.7.0", @@ -142,6 +143,7 @@ "prettier-plugin-astro": "^0.14.1", "prettier-plugin-packagejson": "^2.5.22", "prettier-plugin-tailwindcss": "^0.6.12", + "proper-lockfile": "^4.1.2", "publint": "^0.3.18", "react": "catalog:react", "react-dom": "catalog:react", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 474f60c5838..e235d41173a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,6 +185,9 @@ importers: '@types/node': specifier: ^22.20.1 version: 22.20.1 + '@types/proper-lockfile': + specifier: ^4.1.4 + version: 4.1.4 '@types/react': specifier: catalog:react version: 18.3.28 @@ -314,6 +317,9 @@ importers: prettier-plugin-tailwindcss: specifier: ^0.6.12 version: 0.6.14(prettier-plugin-astro@0.14.1)(prettier@3.8.3) + proper-lockfile: + specifier: ^4.1.2 + version: 4.1.2 publint: specifier: ^0.3.18 version: 0.3.18 @@ -6247,6 +6253,9 @@ packages: '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + '@types/proper-lockfile@4.1.4': + resolution: {integrity: sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==} + '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -6267,6 +6276,9 @@ packages: '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/retry@0.12.5': + resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==} + '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} @@ -21529,6 +21541,10 @@ snapshots: '@types/prop-types@15.7.15': {} + '@types/proper-lockfile@4.1.4': + dependencies: + '@types/retry': 0.12.5 + '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} @@ -21548,6 +21564,8 @@ snapshots: dependencies: '@types/node': 22.20.1 + '@types/retry@0.12.5': {} + '@types/semver@7.7.1': {} '@types/send@0.17.6': From de6379e22f1e7f8aeb7280fcc41f75d54233eb7d Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:03:35 -0500 Subject: [PATCH 7/7] read from app.env instead of appConfigs --- integration/tests/transitions.test.ts | 7 ++++--- integration/tests/transitive-state.test.ts | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/integration/tests/transitions.test.ts b/integration/tests/transitions.test.ts index 6c82ef4bf05..37bab17c535 100644 --- a/integration/tests/transitions.test.ts +++ b/integration/tests/transitions.test.ts @@ -32,9 +32,10 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('transitio test.beforeAll(async () => { const u = createTestUtils({ app }); - const publishableKey = appConfigs.envs.withEmailCodes.publicVariables.get('CLERK_PUBLISHABLE_KEY'); - const secretKey = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_SECRET_KEY'); - const apiUrl = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_API_URL'); + const env = app.env; + const publishableKey = env.publicVariables.get('CLERK_PUBLISHABLE_KEY'); + const secretKey = env.privateVariables.get('CLERK_SECRET_KEY'); + const apiUrl = env.privateVariables.get('CLERK_API_URL'); const { frontendApi: frontendApiUrl } = parsePublishableKey(publishableKey); // Not needed for the normal test setup, but makes it easier to run the tests against a manually started app diff --git a/integration/tests/transitive-state.test.ts b/integration/tests/transitive-state.test.ts index 3d83edc4727..cec264340f5 100644 --- a/integration/tests/transitive-state.test.ts +++ b/integration/tests/transitive-state.test.ts @@ -26,9 +26,10 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('transitiv test.beforeAll(async () => { const u = createTestUtils({ app }); - const publishableKey = appConfigs.envs.withEmailCodes.publicVariables.get('CLERK_PUBLISHABLE_KEY'); - const secretKey = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_SECRET_KEY'); - const apiUrl = appConfigs.envs.withEmailCodes.privateVariables.get('CLERK_API_URL'); + const env = app.env; + const publishableKey = env.publicVariables.get('CLERK_PUBLISHABLE_KEY'); + const secretKey = env.privateVariables.get('CLERK_SECRET_KEY'); + const apiUrl = env.privateVariables.get('CLERK_API_URL'); const { frontendApi: frontendApiUrl } = parsePublishableKey(publishableKey); await clerkSetup({