diff --git a/.changeset/bright-parks-search.md b/.changeset/bright-parks-search.md new file mode 100644 index 00000000000..93198c97468 --- /dev/null +++ b/.changeset/bright-parks-search.md @@ -0,0 +1,7 @@ +--- +'@clerk/nextjs': patch +--- + +Add new telemetry event KEYLESS_ENV_DRIFT_DETECTED to detect drift between publishable and secret keys in keyless apps and values in the .env file. + +This event only fires once as controlled with the .clerk/.tmp/telemetry.json file to prevent telemetry event noise \ No newline at end of file diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index 19e18261db6..5517416af46 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -69,6 +69,15 @@ export async function ClerkProvider( let output: ReactNode; + try { + const detectKeylessEnvDrift = await import('../../server/keyless-telemetry.js').then( + mod => mod.detectKeylessEnvDrift, + ); + await detectKeylessEnvDrift(); + } catch { + // ignore + } + if (shouldRunAsKeyless) { output = ( - Returns true if the flag file was successfully created (meaning + * the event should be fired), false if the file already exists (meaning the event was + * already fired) or if there was an error creating the file + */ +async function tryMarkTelemetryEventAsFired(): Promise { + try { + const flagFilePath = getTelemetryFlagFilePath(); + const flagDirectory = dirname(flagFilePath); + + // Ensure the directory exists before attempting to write the file + await fs.mkdir(flagDirectory, { recursive: true }); + + const flagData = { + firedAt: new Date().toISOString(), + event: EVENT_KEYLESS_ENV_DRIFT_DETECTED, + }; + await fs.writeFile(flagFilePath, JSON.stringify(flagData, null, 2), { flag: 'wx' }); + return true; + } catch (error: unknown) { + if ((error as { code?: string })?.code === 'EEXIST') { + return false; + } + console.warn('Failed to create telemetry flag file:', error); + return false; + } +} + +/** + * Detects and reports environment drift between keyless configuration and environment variables. + * + * This function compares the Clerk keys stored in the keyless configuration file (.clerk/clerk.json) + * with the keys set in environment variables (NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY). + * It only reports drift when there's an actual mismatch between existing keys, not when keys are simply missing. + * + * The function handles several scenarios and only reports drift in specific cases: + * - **Normal keyless mode**: env vars missing but keyless file has keys → no drift (expected) + * - **No configuration**: neither env vars nor keyless file have keys → no drift (nothing to compare) + * - **Actual drift**: env vars exist and don't match keyless file keys → drift detected + * - **Empty keyless file**: keyless file exists but has no keys → no drift (nothing to compare) + * + * Drift is only detected when: + * 1. Both environment variables and keyless file contain keys + * 2. The keys in environment variables don't match the keys in the keyless file + * + * Telemetry events are only fired once per application lifecycle using a flag file mechanism + * to prevent duplicate reporting. + * + * @returns Promise - Function completes silently, errors are logged but don't throw + */ +export async function detectKeylessEnvDrift(): Promise { + // Only run on server side + if (typeof window !== 'undefined') { + return; + } + + try { + // Dynamically import server-side dependencies to avoid client-side issues + const { safeParseClerkFile } = await import('./keyless-node.js'); + + // Read the keyless configuration file + const keylessFile = safeParseClerkFile(); + + if (!keylessFile) { + return; + } + + // Get environment variables + const envPublishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; + const envSecretKey = process.env.CLERK_SECRET_KEY; + + // Check the state of environment variables and keyless file + const hasEnvVars = Boolean(envPublishableKey || envSecretKey); + const keylessFileHasKeys = Boolean(keylessFile?.publishableKey && keylessFile?.secretKey); + const envVarsMissing = !envPublishableKey && !envSecretKey; + + // Early return conditions - no drift to detect in these scenarios: + if (!hasEnvVars && !keylessFileHasKeys) { + // Neither env vars nor keyless file have keys - nothing to compare + return; + } + + if (envVarsMissing && keylessFileHasKeys) { + // Environment variables are missing but keyless file has keys - this is normal for keyless mode + return; + } + + if (!keylessFileHasKeys) { + // Keyless file doesn't have keys, so no drift can be detected + return; + } + + // Only proceed with drift detection if we have something meaningful to compare + if (!hasEnvVars) { + return; + } + + // Compare keys only when both sides have values to compare + const publicKeyMatch = Boolean( + envPublishableKey && keylessFile.publishableKey && envPublishableKey === keylessFile.publishableKey, + ); + + const secretKeyMatch = Boolean(envSecretKey && keylessFile.secretKey && envSecretKey === keylessFile.secretKey); + + // Determine if there's an actual drift: + // Drift occurs when we have env vars that don't match the keyless file keys + const hasActualDrift = + (envPublishableKey && keylessFile.publishableKey && !publicKeyMatch) || + (envSecretKey && keylessFile.secretKey && !secretKeyMatch); + + // Only fire telemetry if there's an actual drift (not just missing keys) + if (!hasActualDrift) { + return; + } + + const payload: EventKeylessEnvDriftPayload = { + publicKeyMatch, + secretKeyMatch, + envVarsMissing, + keylessFileHasKeys, + keylessPublishableKey: keylessFile.publishableKey ?? '', + envPublishableKey: envPublishableKey ?? '', + }; + + // Create a clerk client to access telemetry + const clerkClient = createClerkClientWithOptions({ + publishableKey: keylessFile.publishableKey, + secretKey: keylessFile.secretKey, + }); + + const shouldFireEvent = await tryMarkTelemetryEventAsFired(); + + if (shouldFireEvent) { + // Fire drift detected event only if we successfully created the flag + const driftDetectedEvent: TelemetryEventRaw = { + event: EVENT_KEYLESS_ENV_DRIFT_DETECTED, + eventSamplingRate: EVENT_SAMPLING_RATE, + payload, + }; + + clerkClient.telemetry?.record(driftDetectedEvent); + } + } catch (error) { + // Silently handle errors to avoid breaking the application + console.warn('Failed to detect keyless environment drift:', error); + } +}