From 031ea5b179fe169e78c2d51e55b4b6726c470188 Mon Sep 17 00:00:00 2001 From: Mike Wickett Date: Wed, 27 May 2026 16:09:01 -0400 Subject: [PATCH 1/7] fix(repo): disable keyless in automated environments --- .changeset/keyless-ci-guard.md | 10 +++++ packages/astro/src/utils/feature-flags.ts | 4 +- .../src/utils/__tests__/feature-flags.test.ts | 21 +++++++++ packages/nextjs/src/utils/feature-flags.ts | 4 +- .../nuxt/src/runtime/utils/feature-flags.ts | 4 +- .../react-router/src/utils/feature-flags.ts | 4 +- .../src/__tests__/runtimeEnvironment.spec.ts | 22 ++++++++++ .../shared/src/utils/runtimeEnvironment.ts | 44 +++++++++++++++++++ .../src/utils/feature-flags.ts | 4 +- 9 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 .changeset/keyless-ci-guard.md create mode 100644 packages/nextjs/src/utils/__tests__/feature-flags.test.ts create mode 100644 packages/shared/src/__tests__/runtimeEnvironment.spec.ts diff --git a/.changeset/keyless-ci-guard.md b/.changeset/keyless-ci-guard.md new file mode 100644 index 00000000000..82c93bf8915 --- /dev/null +++ b/.changeset/keyless-ci-guard.md @@ -0,0 +1,10 @@ +--- +'@clerk/astro': patch +'@clerk/nextjs': patch +'@clerk/nuxt': patch +'@clerk/react-router': patch +'@clerk/shared': patch +'@clerk/tanstack-react-start': patch +--- + +Prevent keyless mode from activating in CI and other automated environments. diff --git a/packages/astro/src/utils/feature-flags.ts b/packages/astro/src/utils/feature-flags.ts index 94421cb9937..c0131261a0f 100644 --- a/packages/astro/src/utils/feature-flags.ts +++ b/packages/astro/src/utils/feature-flags.ts @@ -1,10 +1,10 @@ import { getEnvVariable } from '@clerk/shared/getEnvVariable'; import { isTruthy } from '@clerk/shared/underscore'; -import { isDevelopmentEnvironment } from '@clerk/shared/utils'; +import { isAutomatedEnvironment, isDevelopmentEnvironment } from '@clerk/shared/utils'; const KEYLESS_DISABLED = isTruthy(getEnvVariable('PUBLIC_CLERK_KEYLESS_DISABLED')) || isTruthy(getEnvVariable('CLERK_KEYLESS_DISABLED')) || false; -export const canUseKeyless = isDevelopmentEnvironment() && !KEYLESS_DISABLED; +export const canUseKeyless = isDevelopmentEnvironment() && !isAutomatedEnvironment() && !KEYLESS_DISABLED; diff --git a/packages/nextjs/src/utils/__tests__/feature-flags.test.ts b/packages/nextjs/src/utils/__tests__/feature-flags.test.ts new file mode 100644 index 00000000000..5a061ad1e3f --- /dev/null +++ b/packages/nextjs/src/utils/__tests__/feature-flags.test.ts @@ -0,0 +1,21 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +async function loadCanUseKeyless() { + vi.resetModules(); + const { canUseKeyless } = await import('../feature-flags.js'); + return canUseKeyless; +} + +describe('canUseKeyless', () => { + afterEach(() => { + vi.unstubAllEnvs(); + vi.resetModules(); + }); + + it('disables keyless in CI even when the app runs in development mode', async () => { + vi.stubEnv('NODE_ENV', 'development'); + vi.stubEnv('CI', 'true'); + + await expect(loadCanUseKeyless()).resolves.toBe(false); + }); +}); diff --git a/packages/nextjs/src/utils/feature-flags.ts b/packages/nextjs/src/utils/feature-flags.ts index 86cac903a1b..ec967dc5093 100644 --- a/packages/nextjs/src/utils/feature-flags.ts +++ b/packages/nextjs/src/utils/feature-flags.ts @@ -1,7 +1,7 @@ -import { isDevelopmentEnvironment } from '@clerk/shared/utils'; +import { isAutomatedEnvironment, isDevelopmentEnvironment } from '@clerk/shared/utils'; import { KEYLESS_DISABLED } from '../server/constants'; // Next.js will inline the value of 'development' or 'production' on the client bundle, so this is client-safe. -const canUseKeyless = isDevelopmentEnvironment() && !KEYLESS_DISABLED; +const canUseKeyless = isDevelopmentEnvironment() && !isAutomatedEnvironment() && !KEYLESS_DISABLED; export { canUseKeyless }; diff --git a/packages/nuxt/src/runtime/utils/feature-flags.ts b/packages/nuxt/src/runtime/utils/feature-flags.ts index 774cdc71e99..35a2a9f09fa 100644 --- a/packages/nuxt/src/runtime/utils/feature-flags.ts +++ b/packages/nuxt/src/runtime/utils/feature-flags.ts @@ -1,10 +1,10 @@ import { getEnvVariable } from '@clerk/shared/getEnvVariable'; import { isTruthy } from '@clerk/shared/underscore'; -import { isDevelopmentEnvironment } from '@clerk/shared/utils'; +import { isAutomatedEnvironment, isDevelopmentEnvironment } from '@clerk/shared/utils'; const KEYLESS_DISABLED = isTruthy(getEnvVariable('NUXT_PUBLIC_CLERK_KEYLESS_DISABLED')) || isTruthy(getEnvVariable('CLERK_KEYLESS_DISABLED')) || false; -export const canUseKeyless = isDevelopmentEnvironment() && !KEYLESS_DISABLED; +export const canUseKeyless = isDevelopmentEnvironment() && !isAutomatedEnvironment() && !KEYLESS_DISABLED; diff --git a/packages/react-router/src/utils/feature-flags.ts b/packages/react-router/src/utils/feature-flags.ts index bd40eaca25e..f28932ab26a 100644 --- a/packages/react-router/src/utils/feature-flags.ts +++ b/packages/react-router/src/utils/feature-flags.ts @@ -1,10 +1,10 @@ import { getEnvVariable } from '@clerk/shared/getEnvVariable'; import { isTruthy } from '@clerk/shared/underscore'; -import { isDevelopmentEnvironment } from '@clerk/shared/utils'; +import { isAutomatedEnvironment, isDevelopmentEnvironment } from '@clerk/shared/utils'; const KEYLESS_DISABLED = isTruthy(getEnvVariable('VITE_CLERK_KEYLESS_DISABLED')) || isTruthy(getEnvVariable('CLERK_KEYLESS_DISABLED')) || false; -export const canUseKeyless = isDevelopmentEnvironment() && !KEYLESS_DISABLED; +export const canUseKeyless = isDevelopmentEnvironment() && !isAutomatedEnvironment() && !KEYLESS_DISABLED; diff --git a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts new file mode 100644 index 00000000000..1d1c7fc6f2e --- /dev/null +++ b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts @@ -0,0 +1,22 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { isAutomatedEnvironment } from '../utils/runtimeEnvironment'; + +describe('isAutomatedEnvironment', () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('returns true when a CI environment variable is enabled', () => { + vi.stubEnv('CI', 'true'); + + expect(isAutomatedEnvironment()).toBe(true); + }); + + it('returns false when automation environment variables are explicitly falsey', () => { + vi.stubEnv('CI', 'false'); + vi.stubEnv('GITHUB_ACTIONS', '0'); + + expect(isAutomatedEnvironment()).toBe(false); + }); +}); diff --git a/packages/shared/src/utils/runtimeEnvironment.ts b/packages/shared/src/utils/runtimeEnvironment.ts index 43331de2b05..032103da12e 100644 --- a/packages/shared/src/utils/runtimeEnvironment.ts +++ b/packages/shared/src/utils/runtimeEnvironment.ts @@ -1,3 +1,43 @@ +const automatedEnvironmentVariables = [ + 'CI', + 'CONTINUOUS_INTEGRATION', + 'GITHUB_ACTIONS', + 'GITLAB_CI', + 'CIRCLECI', + 'TRAVIS', + 'BUILDKITE', + 'BITBUCKET_BUILD_NUMBER', + 'APPVEYOR', + 'CODEBUILD_BUILD_ID', + 'TF_BUILD', + 'TEAMCITY_VERSION', + 'JENKINS_URL', + 'HUDSON_URL', + 'BAMBOO_BUILDKEY', + 'VERCEL', + 'NETLIFY', + 'CF_PAGES', + 'CODESPACES', + 'GITPOD_WORKSPACE_ID', +] as const; + +const isTruthyEnvValue = (value: string | undefined): boolean => { + if (!value) { + return false; + } + + return !['0', 'false', 'off', 'no'].includes(value.toLowerCase()); +}; + +const getEnvVariable = (name: string): string | undefined => { + try { + return process.env[name]; + // eslint-disable-next-line no-empty + } catch {} + + return undefined; +}; + export const isDevelopmentEnvironment = (): boolean => { try { return process.env.NODE_ENV === 'development'; @@ -28,3 +68,7 @@ export const isProductionEnvironment = (): boolean => { // TODO: add support for import.meta.env.DEV that is being used by vite return false; }; + +export const isAutomatedEnvironment = (): boolean => { + return automatedEnvironmentVariables.some(name => isTruthyEnvValue(getEnvVariable(name))); +}; diff --git a/packages/tanstack-react-start/src/utils/feature-flags.ts b/packages/tanstack-react-start/src/utils/feature-flags.ts index a2ec57f481f..7012b3d10fd 100644 --- a/packages/tanstack-react-start/src/utils/feature-flags.ts +++ b/packages/tanstack-react-start/src/utils/feature-flags.ts @@ -1,6 +1,6 @@ import { getEnvVariable } from '@clerk/shared/getEnvVariable'; import { isTruthy } from '@clerk/shared/underscore'; -import { isDevelopmentEnvironment } from '@clerk/shared/utils'; +import { isAutomatedEnvironment, isDevelopmentEnvironment } from '@clerk/shared/utils'; // Support both Vite-style and generic env var names for disabling keyless mode const KEYLESS_DISABLED = @@ -16,4 +16,4 @@ const KEYLESS_DISABLED = * - `VITE_CLERK_KEYLESS_DISABLED=1` (for Vite-based projects) * - `CLERK_KEYLESS_DISABLED=1` (generic) */ -export const canUseKeyless = isDevelopmentEnvironment() && !KEYLESS_DISABLED; +export const canUseKeyless = isDevelopmentEnvironment() && !isAutomatedEnvironment() && !KEYLESS_DISABLED; From 11632fe579ad642775b28ad3523622250d6028e8 Mon Sep 17 00:00:00 2001 From: Mike Wickett Date: Wed, 27 May 2026 16:23:30 -0400 Subject: [PATCH 2/7] fix(repo): include source on accountless SDK requests --- .changeset/keyless-ci-guard.md | 3 +- packages/astro/src/server/keyless/index.ts | 6 +- .../AccountlessApplicationsApi.test.ts | 61 +++++++++++++++++ .../endpoints/AccountlessApplicationsAPI.ts | 15 ++++- packages/nextjs/src/server/keyless-node.ts | 6 +- .../nuxt/src/runtime/server/keyless/index.ts | 6 +- .../react-router/src/server/keyless/index.ts | 6 +- .../src/keyless/__tests__/service.spec.ts | 67 +++++++++++++++++++ packages/shared/src/keyless/service.ts | 24 +++++-- .../src/server/keyless/index.ts | 6 +- 10 files changed, 183 insertions(+), 17 deletions(-) create mode 100644 packages/backend/src/api/__tests__/AccountlessApplicationsApi.test.ts create mode 100644 packages/shared/src/keyless/__tests__/service.spec.ts diff --git a/.changeset/keyless-ci-guard.md b/.changeset/keyless-ci-guard.md index 82c93bf8915..2afd43a94ac 100644 --- a/.changeset/keyless-ci-guard.md +++ b/.changeset/keyless-ci-guard.md @@ -1,5 +1,6 @@ --- '@clerk/astro': patch +'@clerk/backend': patch '@clerk/nextjs': patch '@clerk/nuxt': patch '@clerk/react-router': patch @@ -7,4 +8,4 @@ '@clerk/tanstack-react-start': patch --- -Prevent keyless mode from activating in CI and other automated environments. +Prevent keyless mode from activating in CI and other automated environments, and include a source value on SDK accountless application requests. diff --git a/packages/astro/src/server/keyless/index.ts b/packages/astro/src/server/keyless/index.ts index 7c1bb31353e..15df38e46b6 100644 --- a/packages/astro/src/server/keyless/index.ts +++ b/packages/astro/src/server/keyless/index.ts @@ -12,21 +12,23 @@ export function keyless(context: APIContext) { keylessServiceInstance = createKeylessService({ storage: createFileStorage(), api: { - async createAccountlessApplication(requestHeaders?: Headers) { + async createAccountlessApplication(requestHeaders?: Headers, source?: string) { try { return await clerkClient(context).__experimental_accountlessApplications.createAccountlessApplication({ requestHeaders, + source, }); } catch { return null; } }, - async completeOnboarding(requestHeaders?: Headers) { + async completeOnboarding(requestHeaders?: Headers, source?: string) { try { return await clerkClient( context, ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ requestHeaders, + source, }); } catch { return null; diff --git a/packages/backend/src/api/__tests__/AccountlessApplicationsApi.test.ts b/packages/backend/src/api/__tests__/AccountlessApplicationsApi.test.ts new file mode 100644 index 00000000000..dca6f971c26 --- /dev/null +++ b/packages/backend/src/api/__tests__/AccountlessApplicationsApi.test.ts @@ -0,0 +1,61 @@ +import { http, HttpResponse } from 'msw'; +import { describe, expect, it } from 'vitest'; + +import { server } from '../../mock-server'; +import { createBackendApiClient } from '../factory'; + +describe('AccountlessApplications', () => { + const mockAccountlessApplication = { + object: 'accountless_application', + publishable_key: 'pk_test_keyless', + secret_key: 'sk_test_keyless', + claim_url: 'https://dashboard.clerk.com/claim', + api_keys_url: 'https://dashboard.clerk.com/api-keys', + }; + + it('creates an accountless application with a source query parameter', async () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + }); + + server.use( + http.post('https://api.clerk.test/v1/accountless_applications', ({ request }) => { + const url = new URL(request.url); + expect(url.searchParams.get('source')).toBe('nextjs'); + expect(request.headers.get('Clerk-API-Version')).toBeTruthy(); + expect(request.headers.get('User-Agent')).toBe('@clerk/backend@0.0.0-test'); + + return HttpResponse.json(mockAccountlessApplication); + }), + ); + + const response = await apiClient.__experimental_accountlessApplications.createAccountlessApplication({ + source: 'nextjs', + }); + + expect(response.publishableKey).toBe('pk_test_keyless'); + }); + + it('completes accountless application onboarding with a source query parameter', async () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + }); + + server.use( + http.post('https://api.clerk.test/v1/accountless_applications/complete', ({ request }) => { + const url = new URL(request.url); + expect(url.searchParams.get('source')).toBe('nextjs'); + expect(request.headers.get('Clerk-API-Version')).toBeTruthy(); + expect(request.headers.get('User-Agent')).toBe('@clerk/backend@0.0.0-test'); + + return HttpResponse.json(mockAccountlessApplication); + }), + ); + + const response = await apiClient.__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ + source: 'nextjs', + }); + + expect(response.publishableKey).toBe('pk_test_keyless'); + }); +}); diff --git a/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts b/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts index 1a11b532b98..634ed6f8d5f 100644 --- a/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts +++ b/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts @@ -4,22 +4,33 @@ import { AbstractAPI } from './AbstractApi'; const basePath = '/accountless_applications'; +type AccountlessApplicationParams = { + requestHeaders?: Headers; + source?: string; +}; + export class AccountlessApplicationAPI extends AbstractAPI { - public async createAccountlessApplication(params?: { requestHeaders?: Headers }) { + public async createAccountlessApplication(params?: AccountlessApplicationParams) { const headerParams = params?.requestHeaders ? Object.fromEntries(params.requestHeaders.entries()) : undefined; return this.request({ method: 'POST', path: basePath, headerParams, + queryParams: { + source: params?.source, + }, }); } - public async completeAccountlessApplicationOnboarding(params?: { requestHeaders?: Headers }) { + public async completeAccountlessApplicationOnboarding(params?: AccountlessApplicationParams) { const headerParams = params?.requestHeaders ? Object.fromEntries(params.requestHeaders.entries()) : undefined; return this.request({ method: 'POST', path: joinPaths(basePath, 'complete'), headerParams, + queryParams: { + source: params?.source, + }, }); } } diff --git a/packages/nextjs/src/server/keyless-node.ts b/packages/nextjs/src/server/keyless-node.ts index eb01ef20691..5df56892ab9 100644 --- a/packages/nextjs/src/server/keyless-node.ts +++ b/packages/nextjs/src/server/keyless-node.ts @@ -24,19 +24,21 @@ export function keyless() { keylessServiceInstance = createKeylessService({ storage: createFileStorage(), api: { - async createAccountlessApplication(requestHeaders?: Headers) { + async createAccountlessApplication(requestHeaders?: Headers, source?: string) { try { return await client.__experimental_accountlessApplications.createAccountlessApplication({ requestHeaders, + source, }); } catch { return null; } }, - async completeOnboarding(requestHeaders?: Headers) { + async completeOnboarding(requestHeaders?: Headers, source?: string) { try { return await client.__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ requestHeaders, + source, }); } catch { return null; diff --git a/packages/nuxt/src/runtime/server/keyless/index.ts b/packages/nuxt/src/runtime/server/keyless/index.ts index 0ee1a4fac47..14072688105 100644 --- a/packages/nuxt/src/runtime/server/keyless/index.ts +++ b/packages/nuxt/src/runtime/server/keyless/index.ts @@ -12,21 +12,23 @@ export function keyless(event: H3Event) { keylessServiceInstance = createKeylessService({ storage: createFileStorage(), api: { - async createAccountlessApplication(requestHeaders?: Headers) { + async createAccountlessApplication(requestHeaders?: Headers, source?: string) { try { return await clerkClient(event).__experimental_accountlessApplications.createAccountlessApplication({ requestHeaders, + source, }); } catch { return null; } }, - async completeOnboarding(requestHeaders?: Headers) { + async completeOnboarding(requestHeaders?: Headers, source?: string) { try { return await clerkClient( event, ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ requestHeaders, + source, }); } catch { return null; diff --git a/packages/react-router/src/server/keyless/index.ts b/packages/react-router/src/server/keyless/index.ts index 2874e022481..b347fa888e1 100644 --- a/packages/react-router/src/server/keyless/index.ts +++ b/packages/react-router/src/server/keyless/index.ts @@ -13,24 +13,26 @@ export function keyless(args: DataFunctionArgs, options?: ClerkMiddlewareOptions keylessServiceInstance = createKeylessService({ storage: createFileStorage(), api: { - async createAccountlessApplication(requestHeaders?: Headers) { + async createAccountlessApplication(requestHeaders?: Headers, source?: string) { try { return await clerkClient(args, options).__experimental_accountlessApplications.createAccountlessApplication( { requestHeaders, + source, }, ); } catch { return null; } }, - async completeOnboarding(requestHeaders?: Headers) { + async completeOnboarding(requestHeaders?: Headers, source?: string) { try { return await clerkClient( args, options, ).__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ requestHeaders, + source, }); } catch { return null; diff --git a/packages/shared/src/keyless/__tests__/service.spec.ts b/packages/shared/src/keyless/__tests__/service.spec.ts new file mode 100644 index 00000000000..379f9b75c6a --- /dev/null +++ b/packages/shared/src/keyless/__tests__/service.spec.ts @@ -0,0 +1,67 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { createKeylessService, type KeylessAPI, type KeylessStorage } from '../service'; +import type { AccountlessApplication } from '../types'; + +const accountlessApplication: AccountlessApplication = { + publishableKey: 'pk_test_keyless', + secretKey: 'sk_test_keyless', + claimUrl: 'https://dashboard.clerk.com/claim', + apiKeysUrl: 'https://dashboard.clerk.com/api-keys', +}; + +const createStorage = (): KeylessStorage => { + let value = ''; + + return { + read: vi.fn(() => value), + write: vi.fn(data => { + value = data; + }), + remove: vi.fn(() => { + value = ''; + }), + }; +}; + +describe('createKeylessService', () => { + it('passes the framework as the source when creating an accountless application', async () => { + const createAccountlessApplication = vi.fn(() => + Promise.resolve(accountlessApplication), + ); + + const service = createKeylessService({ + storage: createStorage(), + api: { + createAccountlessApplication, + completeOnboarding: vi.fn(() => Promise.resolve(accountlessApplication)), + }, + framework: 'nextjs', + }); + + await service.getOrCreateKeys(); + + const [headers, source] = createAccountlessApplication.mock.calls[0]; + expect(headers).toBeInstanceOf(Headers); + expect(source).toBe('nextjs'); + }); + + it('passes the framework as the source when completing accountless application onboarding', async () => { + const completeOnboarding = vi.fn(() => Promise.resolve(accountlessApplication)); + + const service = createKeylessService({ + storage: createStorage(), + api: { + createAccountlessApplication: vi.fn(() => Promise.resolve(accountlessApplication)), + completeOnboarding, + }, + framework: 'nextjs', + }); + + await service.completeOnboarding(); + + const [headers, source] = completeOnboarding.mock.calls[0]; + expect(headers).toBeInstanceOf(Headers); + expect(source).toBe('nextjs'); + }); +}); diff --git a/packages/shared/src/keyless/service.ts b/packages/shared/src/keyless/service.ts index 20b989ff364..844fa7546cf 100644 --- a/packages/shared/src/keyless/service.ts +++ b/packages/shared/src/keyless/service.ts @@ -1,6 +1,9 @@ import { clerkDevelopmentCache, createConfirmationMessage, createKeylessModeMessage } from './devCache'; import type { AccountlessApplication } from './types'; +const KEYLESS_SOURCE_FALLBACK = 'javascript'; +const KEYLESS_SOURCE_MAX_LENGTH = 36; + /** * Storage adapter interface for keyless mode. * Implementations can use file system, cookies, or other storage mechanisms. @@ -38,17 +41,19 @@ export interface KeylessAPI { * Creates a new accountless application. * * @param requestHeaders - Optional headers to include with the request. + * @param source - Optional source value to include with the request. * @returns The created AccountlessApplication or null if failed. */ - createAccountlessApplication(requestHeaders?: Headers): Promise; + createAccountlessApplication(requestHeaders?: Headers, source?: string): Promise; /** * Notifies the backend that onboarding is complete (instance has been claimed). * * @param requestHeaders - Optional headers to include with the request. + * @param source - Optional source value to include with the request. * @returns The updated AccountlessApplication or null if failed. */ - completeOnboarding(requestHeaders?: Headers): Promise; + completeOnboarding(requestHeaders?: Headers, source?: string): Promise; } /** @@ -145,6 +150,16 @@ function createMetadataHeaders(framework?: string, frameworkVersion?: string): H return headers; } +function createSource(framework?: string): string { + const source = (framework || KEYLESS_SOURCE_FALLBACK) + .toLowerCase() + .replace(/[^a-z0-9._-]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, KEYLESS_SOURCE_MAX_LENGTH); + + return source || KEYLESS_SOURCE_FALLBACK; +} + /** * Creates a keyless service that handles accountless application creation and storage. * This provides a simple API for frameworks to integrate keyless mode. @@ -172,6 +187,7 @@ export function createKeylessService(options: KeylessServiceOptions): KeylessSer const { storage, api, framework, frameworkVersion } = options; let hasLoggedKeylessMessage = false; + const source = createSource(framework); const safeParseConfig = (): AccountlessApplication | undefined => { try { @@ -197,7 +213,7 @@ export function createKeylessService(options: KeylessServiceOptions): KeylessSer const headers = createMetadataHeaders(framework, frameworkVersion); // Create new keys via the API - const accountlessApplication = await api.createAccountlessApplication(headers); + const accountlessApplication = await api.createAccountlessApplication(headers, source); if (accountlessApplication) { storage.write(JSON.stringify(accountlessApplication)); @@ -216,7 +232,7 @@ export function createKeylessService(options: KeylessServiceOptions): KeylessSer async completeOnboarding(): Promise { const headers = createMetadataHeaders(framework, frameworkVersion); - return api.completeOnboarding(headers); + return api.completeOnboarding(headers, source); }, logKeylessMessage(claimUrl: string): void { diff --git a/packages/tanstack-react-start/src/server/keyless/index.ts b/packages/tanstack-react-start/src/server/keyless/index.ts index 590edfa9d84..bb91fbedfa9 100644 --- a/packages/tanstack-react-start/src/server/keyless/index.ts +++ b/packages/tanstack-react-start/src/server/keyless/index.ts @@ -11,19 +11,21 @@ export function keyless() { keylessServiceInstance = createKeylessService({ storage: createFileStorage(), api: { - async createAccountlessApplication(requestHeaders?: Headers) { + async createAccountlessApplication(requestHeaders?: Headers, source?: string) { try { return await clerkClient().__experimental_accountlessApplications.createAccountlessApplication({ requestHeaders, + source, }); } catch { return null; } }, - async completeOnboarding(requestHeaders?: Headers) { + async completeOnboarding(requestHeaders?: Headers, source?: string) { try { return await clerkClient().__experimental_accountlessApplications.completeAccountlessApplicationOnboarding({ requestHeaders, + source, }); } catch { return null; From ba1bafee92275d7b4c0fdf8408eb84a82f4ef02c Mon Sep 17 00:00:00 2001 From: Mike Wickett Date: Wed, 27 May 2026 16:33:19 -0400 Subject: [PATCH 3/7] fix(repo): address accountless review feedback --- .../src/api/endpoints/AccountlessApplicationsAPI.ts | 6 ++++-- .../shared/src/__tests__/runtimeEnvironment.spec.ts | 8 ++++++++ packages/shared/src/utils/runtimeEnvironment.ts | 11 ++--------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts b/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts index 634ed6f8d5f..37b69809753 100644 --- a/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts +++ b/packages/backend/src/api/endpoints/AccountlessApplicationsAPI.ts @@ -10,7 +10,7 @@ type AccountlessApplicationParams = { }; export class AccountlessApplicationAPI extends AbstractAPI { - public async createAccountlessApplication(params?: AccountlessApplicationParams) { + public async createAccountlessApplication(params?: AccountlessApplicationParams): Promise { const headerParams = params?.requestHeaders ? Object.fromEntries(params.requestHeaders.entries()) : undefined; return this.request({ method: 'POST', @@ -22,7 +22,9 @@ export class AccountlessApplicationAPI extends AbstractAPI { }); } - public async completeAccountlessApplicationOnboarding(params?: AccountlessApplicationParams) { + public async completeAccountlessApplicationOnboarding( + params?: AccountlessApplicationParams, + ): Promise { const headerParams = params?.requestHeaders ? Object.fromEntries(params.requestHeaders.entries()) : undefined; return this.request({ method: 'POST', diff --git a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts index 1d1c7fc6f2e..4250b6634f7 100644 --- a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts +++ b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts @@ -5,6 +5,7 @@ import { isAutomatedEnvironment } from '../utils/runtimeEnvironment'; describe('isAutomatedEnvironment', () => { afterEach(() => { vi.unstubAllEnvs(); + vi.unstubAllGlobals(); }); it('returns true when a CI environment variable is enabled', () => { @@ -19,4 +20,11 @@ describe('isAutomatedEnvironment', () => { expect(isAutomatedEnvironment()).toBe(false); }); + + it('detects automation environment variables from shared runtime fallbacks', () => { + vi.stubEnv('CI', undefined); + vi.stubGlobal('CI', 'true'); + + expect(isAutomatedEnvironment()).toBe(true); + }); }); diff --git a/packages/shared/src/utils/runtimeEnvironment.ts b/packages/shared/src/utils/runtimeEnvironment.ts index 032103da12e..795417201e7 100644 --- a/packages/shared/src/utils/runtimeEnvironment.ts +++ b/packages/shared/src/utils/runtimeEnvironment.ts @@ -1,3 +1,5 @@ +import { getEnvVariable } from '../getEnvVariable'; + const automatedEnvironmentVariables = [ 'CI', 'CONTINUOUS_INTEGRATION', @@ -29,15 +31,6 @@ const isTruthyEnvValue = (value: string | undefined): boolean => { return !['0', 'false', 'off', 'no'].includes(value.toLowerCase()); }; -const getEnvVariable = (name: string): string | undefined => { - try { - return process.env[name]; - // eslint-disable-next-line no-empty - } catch {} - - return undefined; -}; - export const isDevelopmentEnvironment = (): boolean => { try { return process.env.NODE_ENV === 'development'; From 60e0d912411189479363f464b6086f3e7c062d55 Mon Sep 17 00:00:00 2001 From: Mike Wickett Date: Wed, 27 May 2026 16:49:03 -0400 Subject: [PATCH 4/7] fix(repo): harden keyless review follow-ups --- .changeset/keyless-ci-guard.md | 4 +- .../src/utils/__tests__/feature-flags.test.ts | 58 +++++++++++++++++++ .../src/utils/__tests__/feature-flags.test.ts | 40 ++++++++++++- packages/nextjs/src/utils/feature-flags.ts | 3 +- .../utils/__tests__/feature-flags.test.ts | 58 +++++++++++++++++++ .../src/utils/__tests__/feature-flags.test.ts | 58 +++++++++++++++++++ .../src/__tests__/runtimeEnvironment.spec.ts | 46 ++++++++++++++- packages/shared/src/getEnvVariable.ts | 3 +- .../src/utils/__tests__/feature-flags.test.ts | 58 +++++++++++++++++++ .../src/utils/feature-flags.ts | 3 +- 10 files changed, 322 insertions(+), 9 deletions(-) create mode 100644 packages/astro/src/utils/__tests__/feature-flags.test.ts create mode 100644 packages/nuxt/src/runtime/utils/__tests__/feature-flags.test.ts create mode 100644 packages/react-router/src/utils/__tests__/feature-flags.test.ts create mode 100644 packages/tanstack-react-start/src/utils/__tests__/feature-flags.test.ts diff --git a/.changeset/keyless-ci-guard.md b/.changeset/keyless-ci-guard.md index 2afd43a94ac..f920ff36e65 100644 --- a/.changeset/keyless-ci-guard.md +++ b/.changeset/keyless-ci-guard.md @@ -8,4 +8,6 @@ '@clerk/tanstack-react-start': patch --- -Prevent keyless mode from activating in CI and other automated environments, and include a source value on SDK accountless application requests. +Prevent keyless mode from activating in CI and other automated environments in framework SDKs. + +Framework SDKs now include a source value on accountless application requests, and `@clerk/backend` forwards that source value to the Backend API. diff --git a/packages/astro/src/utils/__tests__/feature-flags.test.ts b/packages/astro/src/utils/__tests__/feature-flags.test.ts new file mode 100644 index 00000000000..c2228b095f2 --- /dev/null +++ b/packages/astro/src/utils/__tests__/feature-flags.test.ts @@ -0,0 +1,58 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const automatedEnvironmentVariables = [ + 'CI', + 'CONTINUOUS_INTEGRATION', + 'GITHUB_ACTIONS', + 'GITLAB_CI', + 'CIRCLECI', + 'TRAVIS', + 'BUILDKITE', + 'BITBUCKET_BUILD_NUMBER', + 'APPVEYOR', + 'CODEBUILD_BUILD_ID', + 'TF_BUILD', + 'TEAMCITY_VERSION', + 'JENKINS_URL', + 'HUDSON_URL', + 'BAMBOO_BUILDKEY', + 'VERCEL', + 'NETLIFY', + 'CF_PAGES', + 'CODESPACES', + 'GITPOD_WORKSPACE_ID', +] as const; + +async function loadCanUseKeyless() { + vi.resetModules(); + const { canUseKeyless } = await import('../feature-flags.js'); + return canUseKeyless; +} + +describe('canUseKeyless', () => { + beforeEach(() => { + vi.stubEnv('NODE_ENV', 'development'); + vi.stubEnv('PUBLIC_CLERK_KEYLESS_DISABLED', undefined); + vi.stubEnv('CLERK_KEYLESS_DISABLED', undefined); + automatedEnvironmentVariables.forEach(name => { + vi.stubEnv(name, undefined); + vi.stubGlobal(name, undefined); + }); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + vi.resetModules(); + }); + + it('enables keyless in development when automation signals are absent', async () => { + await expect(loadCanUseKeyless()).resolves.toBe(true); + }); + + it('disables keyless in CI even when the app runs in development mode', async () => { + vi.stubEnv('CI', 'true'); + + await expect(loadCanUseKeyless()).resolves.toBe(false); + }); +}); diff --git a/packages/nextjs/src/utils/__tests__/feature-flags.test.ts b/packages/nextjs/src/utils/__tests__/feature-flags.test.ts index 5a061ad1e3f..52cf15f2d53 100644 --- a/packages/nextjs/src/utils/__tests__/feature-flags.test.ts +++ b/packages/nextjs/src/utils/__tests__/feature-flags.test.ts @@ -1,4 +1,27 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const automatedEnvironmentVariables = [ + 'CI', + 'CONTINUOUS_INTEGRATION', + 'GITHUB_ACTIONS', + 'GITLAB_CI', + 'CIRCLECI', + 'TRAVIS', + 'BUILDKITE', + 'BITBUCKET_BUILD_NUMBER', + 'APPVEYOR', + 'CODEBUILD_BUILD_ID', + 'TF_BUILD', + 'TEAMCITY_VERSION', + 'JENKINS_URL', + 'HUDSON_URL', + 'BAMBOO_BUILDKEY', + 'VERCEL', + 'NETLIFY', + 'CF_PAGES', + 'CODESPACES', + 'GITPOD_WORKSPACE_ID', +] as const; async function loadCanUseKeyless() { vi.resetModules(); @@ -7,13 +30,26 @@ async function loadCanUseKeyless() { } describe('canUseKeyless', () => { + beforeEach(() => { + vi.stubEnv('NODE_ENV', 'development'); + vi.stubEnv('NEXT_PUBLIC_CLERK_KEYLESS_DISABLED', undefined); + automatedEnvironmentVariables.forEach(name => { + vi.stubEnv(name, undefined); + vi.stubGlobal(name, undefined); + }); + }); + afterEach(() => { vi.unstubAllEnvs(); + vi.unstubAllGlobals(); vi.resetModules(); }); + it('enables keyless in development when automation signals are absent', async () => { + await expect(loadCanUseKeyless()).resolves.toBe(true); + }); + it('disables keyless in CI even when the app runs in development mode', async () => { - vi.stubEnv('NODE_ENV', 'development'); vi.stubEnv('CI', 'true'); await expect(loadCanUseKeyless()).resolves.toBe(false); diff --git a/packages/nextjs/src/utils/feature-flags.ts b/packages/nextjs/src/utils/feature-flags.ts index ec967dc5093..026705b7c50 100644 --- a/packages/nextjs/src/utils/feature-flags.ts +++ b/packages/nextjs/src/utils/feature-flags.ts @@ -1,7 +1,8 @@ import { isAutomatedEnvironment, isDevelopmentEnvironment } from '@clerk/shared/utils'; import { KEYLESS_DISABLED } from '../server/constants'; -// Next.js will inline the value of 'development' or 'production' on the client bundle, so this is client-safe. +// Next.js inlines NODE_ENV on the client bundle. CI detection is reliable server-side, +// while client bundles only see automation signals that are explicitly exposed. const canUseKeyless = isDevelopmentEnvironment() && !isAutomatedEnvironment() && !KEYLESS_DISABLED; export { canUseKeyless }; diff --git a/packages/nuxt/src/runtime/utils/__tests__/feature-flags.test.ts b/packages/nuxt/src/runtime/utils/__tests__/feature-flags.test.ts new file mode 100644 index 00000000000..57b1fe31958 --- /dev/null +++ b/packages/nuxt/src/runtime/utils/__tests__/feature-flags.test.ts @@ -0,0 +1,58 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const automatedEnvironmentVariables = [ + 'CI', + 'CONTINUOUS_INTEGRATION', + 'GITHUB_ACTIONS', + 'GITLAB_CI', + 'CIRCLECI', + 'TRAVIS', + 'BUILDKITE', + 'BITBUCKET_BUILD_NUMBER', + 'APPVEYOR', + 'CODEBUILD_BUILD_ID', + 'TF_BUILD', + 'TEAMCITY_VERSION', + 'JENKINS_URL', + 'HUDSON_URL', + 'BAMBOO_BUILDKEY', + 'VERCEL', + 'NETLIFY', + 'CF_PAGES', + 'CODESPACES', + 'GITPOD_WORKSPACE_ID', +] as const; + +async function loadCanUseKeyless() { + vi.resetModules(); + const { canUseKeyless } = await import('../feature-flags.js'); + return canUseKeyless; +} + +describe('canUseKeyless', () => { + beforeEach(() => { + vi.stubEnv('NODE_ENV', 'development'); + vi.stubEnv('NUXT_PUBLIC_CLERK_KEYLESS_DISABLED', undefined); + vi.stubEnv('CLERK_KEYLESS_DISABLED', undefined); + automatedEnvironmentVariables.forEach(name => { + vi.stubEnv(name, undefined); + vi.stubGlobal(name, undefined); + }); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + vi.resetModules(); + }); + + it('enables keyless in development when automation signals are absent', async () => { + await expect(loadCanUseKeyless()).resolves.toBe(true); + }); + + it('disables keyless in CI even when the app runs in development mode', async () => { + vi.stubEnv('CI', 'true'); + + await expect(loadCanUseKeyless()).resolves.toBe(false); + }); +}); diff --git a/packages/react-router/src/utils/__tests__/feature-flags.test.ts b/packages/react-router/src/utils/__tests__/feature-flags.test.ts new file mode 100644 index 00000000000..f64ea0d7486 --- /dev/null +++ b/packages/react-router/src/utils/__tests__/feature-flags.test.ts @@ -0,0 +1,58 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const automatedEnvironmentVariables = [ + 'CI', + 'CONTINUOUS_INTEGRATION', + 'GITHUB_ACTIONS', + 'GITLAB_CI', + 'CIRCLECI', + 'TRAVIS', + 'BUILDKITE', + 'BITBUCKET_BUILD_NUMBER', + 'APPVEYOR', + 'CODEBUILD_BUILD_ID', + 'TF_BUILD', + 'TEAMCITY_VERSION', + 'JENKINS_URL', + 'HUDSON_URL', + 'BAMBOO_BUILDKEY', + 'VERCEL', + 'NETLIFY', + 'CF_PAGES', + 'CODESPACES', + 'GITPOD_WORKSPACE_ID', +] as const; + +async function loadCanUseKeyless() { + vi.resetModules(); + const { canUseKeyless } = await import('../feature-flags.js'); + return canUseKeyless; +} + +describe('canUseKeyless', () => { + beforeEach(() => { + vi.stubEnv('NODE_ENV', 'development'); + vi.stubEnv('VITE_CLERK_KEYLESS_DISABLED', undefined); + vi.stubEnv('CLERK_KEYLESS_DISABLED', undefined); + automatedEnvironmentVariables.forEach(name => { + vi.stubEnv(name, undefined); + vi.stubGlobal(name, undefined); + }); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + vi.resetModules(); + }); + + it('enables keyless in development when automation signals are absent', async () => { + await expect(loadCanUseKeyless()).resolves.toBe(true); + }); + + it('disables keyless in CI even when the app runs in development mode', async () => { + vi.stubEnv('CI', 'true'); + + await expect(loadCanUseKeyless()).resolves.toBe(false); + }); +}); diff --git a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts index 4250b6634f7..7f31ee296fb 100644 --- a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts +++ b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts @@ -1,8 +1,38 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { isAutomatedEnvironment } from '../utils/runtimeEnvironment'; +const automatedEnvironmentVariables = [ + 'CI', + 'CONTINUOUS_INTEGRATION', + 'GITHUB_ACTIONS', + 'GITLAB_CI', + 'CIRCLECI', + 'TRAVIS', + 'BUILDKITE', + 'BITBUCKET_BUILD_NUMBER', + 'APPVEYOR', + 'CODEBUILD_BUILD_ID', + 'TF_BUILD', + 'TEAMCITY_VERSION', + 'JENKINS_URL', + 'HUDSON_URL', + 'BAMBOO_BUILDKEY', + 'VERCEL', + 'NETLIFY', + 'CF_PAGES', + 'CODESPACES', + 'GITPOD_WORKSPACE_ID', +] as const; + describe('isAutomatedEnvironment', () => { + beforeEach(() => { + automatedEnvironmentVariables.forEach(name => { + vi.stubEnv(name, undefined); + vi.stubGlobal(name, undefined); + }); + }); + afterEach(() => { vi.unstubAllEnvs(); vi.unstubAllGlobals(); @@ -15,8 +45,11 @@ describe('isAutomatedEnvironment', () => { }); it('returns false when automation environment variables are explicitly falsey', () => { - vi.stubEnv('CI', 'false'); - vi.stubEnv('GITHUB_ACTIONS', '0'); + const falseyValues = ['false', '0', 'off', 'no']; + + automatedEnvironmentVariables.forEach((name, index) => { + vi.stubEnv(name, falseyValues[index % falseyValues.length]); + }); expect(isAutomatedEnvironment()).toBe(false); }); @@ -27,4 +60,11 @@ describe('isAutomatedEnvironment', () => { expect(isAutomatedEnvironment()).toBe(true); }); + + it('ignores non-string automation environment variables from shared runtime fallbacks', () => { + vi.stubEnv('CI', undefined); + vi.stubGlobal('CI', true); + + expect(isAutomatedEnvironment()).toBe(false); + }); }); diff --git a/packages/shared/src/getEnvVariable.ts b/packages/shared/src/getEnvVariable.ts index 5d3904a2d81..24033e1c1c1 100644 --- a/packages/shared/src/getEnvVariable.ts +++ b/packages/shared/src/getEnvVariable.ts @@ -42,7 +42,8 @@ export const getEnvVariable = (name: string, context?: Record): str // Cloudflare workers try { - return globalThis[name as keyof typeof globalThis]; + const globalValue = globalThis[name as keyof typeof globalThis]; + return typeof globalValue === 'string' ? globalValue : ''; } catch { // This will raise an error in Cloudflare Pages } diff --git a/packages/tanstack-react-start/src/utils/__tests__/feature-flags.test.ts b/packages/tanstack-react-start/src/utils/__tests__/feature-flags.test.ts new file mode 100644 index 00000000000..f64ea0d7486 --- /dev/null +++ b/packages/tanstack-react-start/src/utils/__tests__/feature-flags.test.ts @@ -0,0 +1,58 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const automatedEnvironmentVariables = [ + 'CI', + 'CONTINUOUS_INTEGRATION', + 'GITHUB_ACTIONS', + 'GITLAB_CI', + 'CIRCLECI', + 'TRAVIS', + 'BUILDKITE', + 'BITBUCKET_BUILD_NUMBER', + 'APPVEYOR', + 'CODEBUILD_BUILD_ID', + 'TF_BUILD', + 'TEAMCITY_VERSION', + 'JENKINS_URL', + 'HUDSON_URL', + 'BAMBOO_BUILDKEY', + 'VERCEL', + 'NETLIFY', + 'CF_PAGES', + 'CODESPACES', + 'GITPOD_WORKSPACE_ID', +] as const; + +async function loadCanUseKeyless() { + vi.resetModules(); + const { canUseKeyless } = await import('../feature-flags.js'); + return canUseKeyless; +} + +describe('canUseKeyless', () => { + beforeEach(() => { + vi.stubEnv('NODE_ENV', 'development'); + vi.stubEnv('VITE_CLERK_KEYLESS_DISABLED', undefined); + vi.stubEnv('CLERK_KEYLESS_DISABLED', undefined); + automatedEnvironmentVariables.forEach(name => { + vi.stubEnv(name, undefined); + vi.stubGlobal(name, undefined); + }); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + vi.resetModules(); + }); + + it('enables keyless in development when automation signals are absent', async () => { + await expect(loadCanUseKeyless()).resolves.toBe(true); + }); + + it('disables keyless in CI even when the app runs in development mode', async () => { + vi.stubEnv('CI', 'true'); + + await expect(loadCanUseKeyless()).resolves.toBe(false); + }); +}); diff --git a/packages/tanstack-react-start/src/utils/feature-flags.ts b/packages/tanstack-react-start/src/utils/feature-flags.ts index 7012b3d10fd..4d129d8887c 100644 --- a/packages/tanstack-react-start/src/utils/feature-flags.ts +++ b/packages/tanstack-react-start/src/utils/feature-flags.ts @@ -10,7 +10,8 @@ const KEYLESS_DISABLED = /** * Whether keyless mode can be used in the current environment. - * Keyless mode is only available in development and when not explicitly disabled. + * Keyless mode is only available in development, when not explicitly disabled, + * and when not running in an automated/CI environment. * * To disable keyless mode, set either: * - `VITE_CLERK_KEYLESS_DISABLED=1` (for Vite-based projects) From b26336fd7c6031942fb1e1fa98bec86d75764507 Mon Sep 17 00:00:00 2001 From: Mike Wickett Date: Wed, 27 May 2026 17:13:01 -0400 Subject: [PATCH 5/7] fix(repo): keep keyless e2e apps out of CI mode --- .../models/__tests__/application.test.ts | 16 +++++++++++- integration/models/application.ts | 22 ++++++++++++++-- integration/presets/envs.ts | 5 ++++ .../src/utils/__tests__/feature-flags.test.ts | 24 +----------------- .../src/utils/__tests__/feature-flags.test.ts | 24 +----------------- .../utils/__tests__/feature-flags.test.ts | 24 +----------------- .../src/utils/__tests__/feature-flags.test.ts | 24 +----------------- .../src/__tests__/runtimeEnvironment.spec.ts | 25 +------------------ .../shared/src/utils/runtimeEnvironment.ts | 2 +- .../src/utils/__tests__/feature-flags.test.ts | 24 +----------------- 10 files changed, 47 insertions(+), 143 deletions(-) diff --git a/integration/models/__tests__/application.test.ts b/integration/models/__tests__/application.test.ts index 6e2d52d0e2e..b3eb9f1bccc 100644 --- a/integration/models/__tests__/application.test.ts +++ b/integration/models/__tests__/application.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { resolveServerUrl } from '../application'; +import { createAppRuntimeEnv, resolveServerUrl } from '../application'; +import { environmentConfig } from '../environment'; describe('resolveServerUrl', () => { describe('with opts.serverUrl', () => { @@ -49,3 +50,16 @@ describe('resolveServerUrl', () => { }); }); }); + +describe('createAppRuntimeEnv', () => { + it('passes configured falsey values through to spawned app processes', () => { + const env = environmentConfig() + .setEnvVariable('private', 'CI', 'false') + .setEnvVariable('public', 'CLERK_KEYLESS_DISABLED', false); + + expect(createAppRuntimeEnv(env)).toMatchObject({ + CI: 'false', + CLERK_KEYLESS_DISABLED: 'false', + }); + }); +}); diff --git a/integration/models/application.ts b/integration/models/application.ts index 79918bd9c29..3d9fd4f9901 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -34,6 +34,23 @@ export const resolveServerUrl = ( return fallbackServerUrl || `http://localhost:${port}`; }; +export const createAppRuntimeEnv = (env?: EnvironmentConfig): Record => { + if (!env?.publicVariables || !env?.privateVariables) { + return {}; + } + + const runtimeEnv: Record = {}; + for (const [key, value] of [...env.publicVariables, ...env.privateVariables]) { + if (value === undefined || value === null) { + continue; + } + + runtimeEnv[key] = String(value); + } + + return runtimeEnv; +}; + export const application = ( config: ApplicationConfig, appDirPath: string, @@ -103,7 +120,7 @@ export const application = ( const proc = run(scripts.dev, { cwd: appDirPath, - env: { PORT: port.toString() }, + env: { ...createAppRuntimeEnv(state.env), PORT: port.toString() }, detached: opts.detached, stdout: opts.detached ? fs.openSync(stdoutFilePath, 'a') : undefined, stderr: opts.detached ? fs.openSync(stderrFilePath, 'a') : undefined, @@ -158,6 +175,7 @@ export const application = ( const log = logger.child({ prefix: 'build' }).info; await run(scripts.build, { cwd: appDirPath, + env: createAppRuntimeEnv(state.env), log: (msg: string) => { buildOutput += `\n${msg}`; log(msg); @@ -200,7 +218,7 @@ export const application = ( const proc = run(scripts.serve, { cwd: appDirPath, - env: { ...envFromFile, PORT: port.toString() }, + env: { ...envFromFile, ...createAppRuntimeEnv(state.env), PORT: port.toString() }, detached: opts.detached, stdout: opts.detached ? fs.openSync(stdoutFilePath, 'a') : undefined, stderr: opts.detached ? fs.openSync(stderrFilePath, 'a') : undefined, diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 5c87b72647c..31a81470de1 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -1,5 +1,6 @@ import { resolve } from 'node:path'; +import { automatedEnvironmentVariables } from '@clerk/shared/utils'; import fs from 'fs-extra'; import { constants } from '../constants'; @@ -91,6 +92,10 @@ const withKeyless = base .setEnvVariable('private', 'CLERK_API_URL', 'https://api.clerkstage.dev') .setEnvVariable('public', 'CLERK_KEYLESS_DISABLED', false); +automatedEnvironmentVariables.forEach(name => { + withKeyless.setEnvVariable('private', name, 'false'); +}); + const withEmailCodes = withInstanceKeys( 'with-email-codes', base diff --git a/packages/astro/src/utils/__tests__/feature-flags.test.ts b/packages/astro/src/utils/__tests__/feature-flags.test.ts index c2228b095f2..dac985d743f 100644 --- a/packages/astro/src/utils/__tests__/feature-flags.test.ts +++ b/packages/astro/src/utils/__tests__/feature-flags.test.ts @@ -1,28 +1,6 @@ +import { automatedEnvironmentVariables } from '@clerk/shared/utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -const automatedEnvironmentVariables = [ - 'CI', - 'CONTINUOUS_INTEGRATION', - 'GITHUB_ACTIONS', - 'GITLAB_CI', - 'CIRCLECI', - 'TRAVIS', - 'BUILDKITE', - 'BITBUCKET_BUILD_NUMBER', - 'APPVEYOR', - 'CODEBUILD_BUILD_ID', - 'TF_BUILD', - 'TEAMCITY_VERSION', - 'JENKINS_URL', - 'HUDSON_URL', - 'BAMBOO_BUILDKEY', - 'VERCEL', - 'NETLIFY', - 'CF_PAGES', - 'CODESPACES', - 'GITPOD_WORKSPACE_ID', -] as const; - async function loadCanUseKeyless() { vi.resetModules(); const { canUseKeyless } = await import('../feature-flags.js'); diff --git a/packages/nextjs/src/utils/__tests__/feature-flags.test.ts b/packages/nextjs/src/utils/__tests__/feature-flags.test.ts index 52cf15f2d53..e16bbf0aef8 100644 --- a/packages/nextjs/src/utils/__tests__/feature-flags.test.ts +++ b/packages/nextjs/src/utils/__tests__/feature-flags.test.ts @@ -1,28 +1,6 @@ +import { automatedEnvironmentVariables } from '@clerk/shared/utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -const automatedEnvironmentVariables = [ - 'CI', - 'CONTINUOUS_INTEGRATION', - 'GITHUB_ACTIONS', - 'GITLAB_CI', - 'CIRCLECI', - 'TRAVIS', - 'BUILDKITE', - 'BITBUCKET_BUILD_NUMBER', - 'APPVEYOR', - 'CODEBUILD_BUILD_ID', - 'TF_BUILD', - 'TEAMCITY_VERSION', - 'JENKINS_URL', - 'HUDSON_URL', - 'BAMBOO_BUILDKEY', - 'VERCEL', - 'NETLIFY', - 'CF_PAGES', - 'CODESPACES', - 'GITPOD_WORKSPACE_ID', -] as const; - async function loadCanUseKeyless() { vi.resetModules(); const { canUseKeyless } = await import('../feature-flags.js'); diff --git a/packages/nuxt/src/runtime/utils/__tests__/feature-flags.test.ts b/packages/nuxt/src/runtime/utils/__tests__/feature-flags.test.ts index 57b1fe31958..0ea51f76f45 100644 --- a/packages/nuxt/src/runtime/utils/__tests__/feature-flags.test.ts +++ b/packages/nuxt/src/runtime/utils/__tests__/feature-flags.test.ts @@ -1,28 +1,6 @@ +import { automatedEnvironmentVariables } from '@clerk/shared/utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -const automatedEnvironmentVariables = [ - 'CI', - 'CONTINUOUS_INTEGRATION', - 'GITHUB_ACTIONS', - 'GITLAB_CI', - 'CIRCLECI', - 'TRAVIS', - 'BUILDKITE', - 'BITBUCKET_BUILD_NUMBER', - 'APPVEYOR', - 'CODEBUILD_BUILD_ID', - 'TF_BUILD', - 'TEAMCITY_VERSION', - 'JENKINS_URL', - 'HUDSON_URL', - 'BAMBOO_BUILDKEY', - 'VERCEL', - 'NETLIFY', - 'CF_PAGES', - 'CODESPACES', - 'GITPOD_WORKSPACE_ID', -] as const; - async function loadCanUseKeyless() { vi.resetModules(); const { canUseKeyless } = await import('../feature-flags.js'); diff --git a/packages/react-router/src/utils/__tests__/feature-flags.test.ts b/packages/react-router/src/utils/__tests__/feature-flags.test.ts index f64ea0d7486..312f8b5dbf8 100644 --- a/packages/react-router/src/utils/__tests__/feature-flags.test.ts +++ b/packages/react-router/src/utils/__tests__/feature-flags.test.ts @@ -1,28 +1,6 @@ +import { automatedEnvironmentVariables } from '@clerk/shared/utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -const automatedEnvironmentVariables = [ - 'CI', - 'CONTINUOUS_INTEGRATION', - 'GITHUB_ACTIONS', - 'GITLAB_CI', - 'CIRCLECI', - 'TRAVIS', - 'BUILDKITE', - 'BITBUCKET_BUILD_NUMBER', - 'APPVEYOR', - 'CODEBUILD_BUILD_ID', - 'TF_BUILD', - 'TEAMCITY_VERSION', - 'JENKINS_URL', - 'HUDSON_URL', - 'BAMBOO_BUILDKEY', - 'VERCEL', - 'NETLIFY', - 'CF_PAGES', - 'CODESPACES', - 'GITPOD_WORKSPACE_ID', -] as const; - async function loadCanUseKeyless() { vi.resetModules(); const { canUseKeyless } = await import('../feature-flags.js'); diff --git a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts index 7f31ee296fb..dcb29164de2 100644 --- a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts +++ b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts @@ -1,29 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { isAutomatedEnvironment } from '../utils/runtimeEnvironment'; - -const automatedEnvironmentVariables = [ - 'CI', - 'CONTINUOUS_INTEGRATION', - 'GITHUB_ACTIONS', - 'GITLAB_CI', - 'CIRCLECI', - 'TRAVIS', - 'BUILDKITE', - 'BITBUCKET_BUILD_NUMBER', - 'APPVEYOR', - 'CODEBUILD_BUILD_ID', - 'TF_BUILD', - 'TEAMCITY_VERSION', - 'JENKINS_URL', - 'HUDSON_URL', - 'BAMBOO_BUILDKEY', - 'VERCEL', - 'NETLIFY', - 'CF_PAGES', - 'CODESPACES', - 'GITPOD_WORKSPACE_ID', -] as const; +import { automatedEnvironmentVariables, isAutomatedEnvironment } from '../utils/runtimeEnvironment'; describe('isAutomatedEnvironment', () => { beforeEach(() => { diff --git a/packages/shared/src/utils/runtimeEnvironment.ts b/packages/shared/src/utils/runtimeEnvironment.ts index 795417201e7..a2932a5344d 100644 --- a/packages/shared/src/utils/runtimeEnvironment.ts +++ b/packages/shared/src/utils/runtimeEnvironment.ts @@ -1,6 +1,6 @@ import { getEnvVariable } from '../getEnvVariable'; -const automatedEnvironmentVariables = [ +export const automatedEnvironmentVariables = [ 'CI', 'CONTINUOUS_INTEGRATION', 'GITHUB_ACTIONS', diff --git a/packages/tanstack-react-start/src/utils/__tests__/feature-flags.test.ts b/packages/tanstack-react-start/src/utils/__tests__/feature-flags.test.ts index f64ea0d7486..312f8b5dbf8 100644 --- a/packages/tanstack-react-start/src/utils/__tests__/feature-flags.test.ts +++ b/packages/tanstack-react-start/src/utils/__tests__/feature-flags.test.ts @@ -1,28 +1,6 @@ +import { automatedEnvironmentVariables } from '@clerk/shared/utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -const automatedEnvironmentVariables = [ - 'CI', - 'CONTINUOUS_INTEGRATION', - 'GITHUB_ACTIONS', - 'GITLAB_CI', - 'CIRCLECI', - 'TRAVIS', - 'BUILDKITE', - 'BITBUCKET_BUILD_NUMBER', - 'APPVEYOR', - 'CODEBUILD_BUILD_ID', - 'TF_BUILD', - 'TEAMCITY_VERSION', - 'JENKINS_URL', - 'HUDSON_URL', - 'BAMBOO_BUILDKEY', - 'VERCEL', - 'NETLIFY', - 'CF_PAGES', - 'CODESPACES', - 'GITPOD_WORKSPACE_ID', -] as const; - async function loadCanUseKeyless() { vi.resetModules(); const { canUseKeyless } = await import('../feature-flags.js'); From 283776c1931470e059fd442dfb3cedfdb0d207cd Mon Sep 17 00:00:00 2001 From: Mike Wickett Date: Wed, 27 May 2026 17:36:22 -0400 Subject: [PATCH 6/7] fix(repo): address keyless review follow-ups --- integration/models/application.ts | 1 + .../src/utils/__tests__/feature-flags.test.ts | 12 ++++ .../AccountlessApplicationsApi.test.ts | 38 +++++++++++ .../src/utils/__tests__/feature-flags.test.ts | 12 ++++ .../utils/__tests__/feature-flags.test.ts | 12 ++++ .../src/utils/__tests__/feature-flags.test.ts | 12 ++++ .../src/__tests__/getEnvVariable.spec.ts | 17 +++++ .../src/__tests__/runtimeEnvironment.spec.ts | 15 +++++ packages/shared/src/getEnvVariable.ts | 3 +- .../src/keyless/__tests__/service.spec.ts | 64 ++++++++++++++++--- packages/shared/src/keyless/service.ts | 1 + .../shared/src/utils/runtimeEnvironment.ts | 9 +-- .../src/utils/__tests__/feature-flags.test.ts | 12 ++++ 13 files changed, 192 insertions(+), 16 deletions(-) create mode 100644 packages/shared/src/__tests__/getEnvVariable.spec.ts diff --git a/integration/models/application.ts b/integration/models/application.ts index 3d9fd4f9901..2afb03d134b 100644 --- a/integration/models/application.ts +++ b/integration/models/application.ts @@ -40,6 +40,7 @@ export const createAppRuntimeEnv = (env?: EnvironmentConfig): Record = {}; + // Private variables intentionally win when the same runtime key exists in both maps. for (const [key, value] of [...env.publicVariables, ...env.privateVariables]) { if (value === undefined || value === null) { continue; diff --git a/packages/astro/src/utils/__tests__/feature-flags.test.ts b/packages/astro/src/utils/__tests__/feature-flags.test.ts index dac985d743f..5baa1304a80 100644 --- a/packages/astro/src/utils/__tests__/feature-flags.test.ts +++ b/packages/astro/src/utils/__tests__/feature-flags.test.ts @@ -33,4 +33,16 @@ describe('canUseKeyless', () => { await expect(loadCanUseKeyless()).resolves.toBe(false); }); + + it('disables keyless outside development mode', async () => { + vi.stubEnv('NODE_ENV', 'production'); + + await expect(loadCanUseKeyless()).resolves.toBe(false); + }); + + it('disables keyless when explicitly disabled', async () => { + vi.stubEnv('PUBLIC_CLERK_KEYLESS_DISABLED', 'true'); + + await expect(loadCanUseKeyless()).resolves.toBe(false); + }); }); diff --git a/packages/backend/src/api/__tests__/AccountlessApplicationsApi.test.ts b/packages/backend/src/api/__tests__/AccountlessApplicationsApi.test.ts index dca6f971c26..98f0e1c9c19 100644 --- a/packages/backend/src/api/__tests__/AccountlessApplicationsApi.test.ts +++ b/packages/backend/src/api/__tests__/AccountlessApplicationsApi.test.ts @@ -36,6 +36,25 @@ describe('AccountlessApplications', () => { expect(response.publishableKey).toBe('pk_test_keyless'); }); + it('creates an accountless application without a source query parameter when source is omitted', async () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + }); + + server.use( + http.post('https://api.clerk.test/v1/accountless_applications', ({ request }) => { + const url = new URL(request.url); + expect(url.searchParams.has('source')).toBe(false); + + return HttpResponse.json(mockAccountlessApplication); + }), + ); + + const response = await apiClient.__experimental_accountlessApplications.createAccountlessApplication(); + + expect(response.publishableKey).toBe('pk_test_keyless'); + }); + it('completes accountless application onboarding with a source query parameter', async () => { const apiClient = createBackendApiClient({ apiUrl: 'https://api.clerk.test', @@ -58,4 +77,23 @@ describe('AccountlessApplications', () => { expect(response.publishableKey).toBe('pk_test_keyless'); }); + + it('completes accountless application onboarding without a source query parameter when source is omitted', async () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + }); + + server.use( + http.post('https://api.clerk.test/v1/accountless_applications/complete', ({ request }) => { + const url = new URL(request.url); + expect(url.searchParams.has('source')).toBe(false); + + return HttpResponse.json(mockAccountlessApplication); + }), + ); + + const response = await apiClient.__experimental_accountlessApplications.completeAccountlessApplicationOnboarding(); + + expect(response.publishableKey).toBe('pk_test_keyless'); + }); }); diff --git a/packages/nextjs/src/utils/__tests__/feature-flags.test.ts b/packages/nextjs/src/utils/__tests__/feature-flags.test.ts index e16bbf0aef8..97a34b1be51 100644 --- a/packages/nextjs/src/utils/__tests__/feature-flags.test.ts +++ b/packages/nextjs/src/utils/__tests__/feature-flags.test.ts @@ -32,4 +32,16 @@ describe('canUseKeyless', () => { await expect(loadCanUseKeyless()).resolves.toBe(false); }); + + it('disables keyless outside development mode', async () => { + vi.stubEnv('NODE_ENV', 'production'); + + await expect(loadCanUseKeyless()).resolves.toBe(false); + }); + + it('disables keyless when explicitly disabled', async () => { + vi.stubEnv('NEXT_PUBLIC_CLERK_KEYLESS_DISABLED', 'true'); + + await expect(loadCanUseKeyless()).resolves.toBe(false); + }); }); diff --git a/packages/nuxt/src/runtime/utils/__tests__/feature-flags.test.ts b/packages/nuxt/src/runtime/utils/__tests__/feature-flags.test.ts index 0ea51f76f45..c24fb08fceb 100644 --- a/packages/nuxt/src/runtime/utils/__tests__/feature-flags.test.ts +++ b/packages/nuxt/src/runtime/utils/__tests__/feature-flags.test.ts @@ -33,4 +33,16 @@ describe('canUseKeyless', () => { await expect(loadCanUseKeyless()).resolves.toBe(false); }); + + it('disables keyless outside development mode', async () => { + vi.stubEnv('NODE_ENV', 'production'); + + await expect(loadCanUseKeyless()).resolves.toBe(false); + }); + + it('disables keyless when explicitly disabled', async () => { + vi.stubEnv('NUXT_PUBLIC_CLERK_KEYLESS_DISABLED', 'true'); + + await expect(loadCanUseKeyless()).resolves.toBe(false); + }); }); diff --git a/packages/react-router/src/utils/__tests__/feature-flags.test.ts b/packages/react-router/src/utils/__tests__/feature-flags.test.ts index 312f8b5dbf8..693278f61f9 100644 --- a/packages/react-router/src/utils/__tests__/feature-flags.test.ts +++ b/packages/react-router/src/utils/__tests__/feature-flags.test.ts @@ -33,4 +33,16 @@ describe('canUseKeyless', () => { await expect(loadCanUseKeyless()).resolves.toBe(false); }); + + it('disables keyless outside development mode', async () => { + vi.stubEnv('NODE_ENV', 'production'); + + await expect(loadCanUseKeyless()).resolves.toBe(false); + }); + + it('disables keyless when explicitly disabled', async () => { + vi.stubEnv('VITE_CLERK_KEYLESS_DISABLED', 'true'); + + await expect(loadCanUseKeyless()).resolves.toBe(false); + }); }); diff --git a/packages/shared/src/__tests__/getEnvVariable.spec.ts b/packages/shared/src/__tests__/getEnvVariable.spec.ts new file mode 100644 index 00000000000..918f6cf33b9 --- /dev/null +++ b/packages/shared/src/__tests__/getEnvVariable.spec.ts @@ -0,0 +1,17 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { getEnvVariable } from '../getEnvVariable'; + +describe('getEnvVariable', () => { + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + }); + + it('preserves raw globalThis fallback values for backward compatibility', () => { + vi.stubEnv('CLERK_TEST_GLOBAL_VALUE', undefined); + vi.stubGlobal('CLERK_TEST_GLOBAL_VALUE', true); + + expect(getEnvVariable('CLERK_TEST_GLOBAL_VALUE') as unknown).toBe(true); + }); +}); diff --git a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts index dcb29164de2..e139341f76b 100644 --- a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts +++ b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts @@ -38,6 +38,21 @@ describe('isAutomatedEnvironment', () => { expect(isAutomatedEnvironment()).toBe(true); }); + it('detects build-only hosted provider variables', () => { + vi.stubEnv('NOW_BUILDER', '1'); + + expect(isAutomatedEnvironment()).toBe(true); + }); + + it('does not treat interactive development host variables as automation signals', () => { + vi.stubEnv('CODESPACES', 'true'); + vi.stubEnv('GITPOD_WORKSPACE_ID', 'workspace-id'); + vi.stubEnv('VERCEL', '1'); + vi.stubEnv('NETLIFY', 'true'); + + expect(isAutomatedEnvironment()).toBe(false); + }); + it('ignores non-string automation environment variables from shared runtime fallbacks', () => { vi.stubEnv('CI', undefined); vi.stubGlobal('CI', true); diff --git a/packages/shared/src/getEnvVariable.ts b/packages/shared/src/getEnvVariable.ts index 24033e1c1c1..40011125eb1 100644 --- a/packages/shared/src/getEnvVariable.ts +++ b/packages/shared/src/getEnvVariable.ts @@ -42,8 +42,7 @@ export const getEnvVariable = (name: string, context?: Record): str // Cloudflare workers try { - const globalValue = globalThis[name as keyof typeof globalThis]; - return typeof globalValue === 'string' ? globalValue : ''; + return globalThis[name as keyof typeof globalThis] as string; } catch { // This will raise an error in Cloudflare Pages } diff --git a/packages/shared/src/keyless/__tests__/service.spec.ts b/packages/shared/src/keyless/__tests__/service.spec.ts index 379f9b75c6a..7a76b26cb3e 100644 --- a/packages/shared/src/keyless/__tests__/service.spec.ts +++ b/packages/shared/src/keyless/__tests__/service.spec.ts @@ -24,6 +24,12 @@ const createStorage = (): KeylessStorage => { }; }; +const createApi = (overrides: Partial = {}): KeylessAPI => ({ + createAccountlessApplication: vi.fn(() => Promise.resolve(accountlessApplication)), + completeOnboarding: vi.fn(() => Promise.resolve(accountlessApplication)), + ...overrides, +}); + describe('createKeylessService', () => { it('passes the framework as the source when creating an accountless application', async () => { const createAccountlessApplication = vi.fn(() => @@ -32,10 +38,7 @@ describe('createKeylessService', () => { const service = createKeylessService({ storage: createStorage(), - api: { - createAccountlessApplication, - completeOnboarding: vi.fn(() => Promise.resolve(accountlessApplication)), - }, + api: createApi({ createAccountlessApplication }), framework: 'nextjs', }); @@ -51,10 +54,7 @@ describe('createKeylessService', () => { const service = createKeylessService({ storage: createStorage(), - api: { - createAccountlessApplication: vi.fn(() => Promise.resolve(accountlessApplication)), - completeOnboarding, - }, + api: createApi({ completeOnboarding }), framework: 'nextjs', }); @@ -64,4 +64,52 @@ describe('createKeylessService', () => { expect(headers).toBeInstanceOf(Headers); expect(source).toBe('nextjs'); }); + + it('sanitizes the framework before passing it as the source', async () => { + const createAccountlessApplication = vi.fn(() => + Promise.resolve(accountlessApplication), + ); + + const service = createKeylessService({ + storage: createStorage(), + api: createApi({ createAccountlessApplication }), + framework: 'Next.js @ Canary!', + }); + + await service.getOrCreateKeys(); + + expect(createAccountlessApplication.mock.calls[0][1]).toBe('next.js-canary'); + }); + + it('falls back to javascript when framework sanitization produces an empty source', async () => { + const createAccountlessApplication = vi.fn(() => + Promise.resolve(accountlessApplication), + ); + + const service = createKeylessService({ + storage: createStorage(), + api: createApi({ createAccountlessApplication }), + framework: '!!!', + }); + + await service.getOrCreateKeys(); + + expect(createAccountlessApplication.mock.calls[0][1]).toBe('javascript'); + }); + + it('truncates the source before passing it to the accountless application API', async () => { + const createAccountlessApplication = vi.fn(() => + Promise.resolve(accountlessApplication), + ); + + const service = createKeylessService({ + storage: createStorage(), + api: createApi({ createAccountlessApplication }), + framework: 'a'.repeat(50), + }); + + await service.getOrCreateKeys(); + + expect(createAccountlessApplication.mock.calls[0][1]).toBe('a'.repeat(36)); + }); }); diff --git a/packages/shared/src/keyless/service.ts b/packages/shared/src/keyless/service.ts index 844fa7546cf..7e776a3e9ac 100644 --- a/packages/shared/src/keyless/service.ts +++ b/packages/shared/src/keyless/service.ts @@ -2,6 +2,7 @@ import { clerkDevelopmentCache, createConfirmationMessage, createKeylessModeMess import type { AccountlessApplication } from './types'; const KEYLESS_SOURCE_FALLBACK = 'javascript'; +// Keep the source compact for BAPI metadata dimensions while covering common framework identifiers. const KEYLESS_SOURCE_MAX_LENGTH = 36; /** diff --git a/packages/shared/src/utils/runtimeEnvironment.ts b/packages/shared/src/utils/runtimeEnvironment.ts index a2932a5344d..b011dc95899 100644 --- a/packages/shared/src/utils/runtimeEnvironment.ts +++ b/packages/shared/src/utils/runtimeEnvironment.ts @@ -16,15 +16,12 @@ export const automatedEnvironmentVariables = [ 'JENKINS_URL', 'HUDSON_URL', 'BAMBOO_BUILDKEY', - 'VERCEL', - 'NETLIFY', + 'NOW_BUILDER', 'CF_PAGES', - 'CODESPACES', - 'GITPOD_WORKSPACE_ID', ] as const; -const isTruthyEnvValue = (value: string | undefined): boolean => { - if (!value) { +const isTruthyEnvValue = (value: unknown): boolean => { + if (typeof value !== 'string' || !value) { return false; } diff --git a/packages/tanstack-react-start/src/utils/__tests__/feature-flags.test.ts b/packages/tanstack-react-start/src/utils/__tests__/feature-flags.test.ts index 312f8b5dbf8..693278f61f9 100644 --- a/packages/tanstack-react-start/src/utils/__tests__/feature-flags.test.ts +++ b/packages/tanstack-react-start/src/utils/__tests__/feature-flags.test.ts @@ -33,4 +33,16 @@ describe('canUseKeyless', () => { await expect(loadCanUseKeyless()).resolves.toBe(false); }); + + it('disables keyless outside development mode', async () => { + vi.stubEnv('NODE_ENV', 'production'); + + await expect(loadCanUseKeyless()).resolves.toBe(false); + }); + + it('disables keyless when explicitly disabled', async () => { + vi.stubEnv('VITE_CLERK_KEYLESS_DISABLED', 'true'); + + await expect(loadCanUseKeyless()).resolves.toBe(false); + }); }); From 58dfaf711de9ff0ce304b3ee221eee551a9d3ed6 Mon Sep 17 00:00:00 2001 From: Mike Wickett Date: Wed, 27 May 2026 18:01:36 -0400 Subject: [PATCH 7/7] fix(shared): avoid NOW_BUILDER keyless automation signal --- packages/shared/src/__tests__/runtimeEnvironment.spec.ts | 4 ++-- packages/shared/src/utils/runtimeEnvironment.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts index e139341f76b..e792bbb45a6 100644 --- a/packages/shared/src/__tests__/runtimeEnvironment.spec.ts +++ b/packages/shared/src/__tests__/runtimeEnvironment.spec.ts @@ -38,10 +38,10 @@ describe('isAutomatedEnvironment', () => { expect(isAutomatedEnvironment()).toBe(true); }); - it('detects build-only hosted provider variables', () => { + it('does not treat presence-sensitive build tool variables as automation signals', () => { vi.stubEnv('NOW_BUILDER', '1'); - expect(isAutomatedEnvironment()).toBe(true); + expect(isAutomatedEnvironment()).toBe(false); }); it('does not treat interactive development host variables as automation signals', () => { diff --git a/packages/shared/src/utils/runtimeEnvironment.ts b/packages/shared/src/utils/runtimeEnvironment.ts index b011dc95899..72014f1e15c 100644 --- a/packages/shared/src/utils/runtimeEnvironment.ts +++ b/packages/shared/src/utils/runtimeEnvironment.ts @@ -16,7 +16,6 @@ export const automatedEnvironmentVariables = [ 'JENKINS_URL', 'HUDSON_URL', 'BAMBOO_BUILDKEY', - 'NOW_BUILDER', 'CF_PAGES', ] as const;