Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/bright-parks-search.md
Original file line numberDiff line numberDiff line change
@@ -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
9 changes: 9 additions & 0 deletions packages/nextjs/src/app-router/server/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 = (
<KeylessProvider
Expand Down
184 changes: 184 additions & 0 deletions packages/nextjs/src/server/keyless-telemetry.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
import type { TelemetryEventRaw } from '@clerk/types';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';

import { createClerkClientWithOptions } from './createClerkClient';

const EVENT_KEYLESS_ENV_DRIFT_DETECTED = 'KEYLESS_ENV_DRIFT_DETECTED';
const EVENT_SAMPLING_RATE = 1; // 100% sampling rate
const TELEMETRY_FLAG_FILE = '.clerk/.tmp/telemetry.json';

Comment thread
heatlikeheatwave marked this conversation as resolved.
type EventKeylessEnvDriftPayload = {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
keylessPublishableKey: string;
envPublishableKey: string;
};
Comment on lines +11 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not send raw keys; mask publishable keys and make fields optional

Raw publishable keys shouldn’t be sent in telemetry. Also, casting/forcing missing values into strings can hide absence. Make fields optional and masked, and prefer interface for an extendable payload shape per guidelines.

-type EventKeylessEnvDriftPayload = {+interface EventKeylessEnvDriftPayload {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
- keylessPublishableKey: string;- envPublishableKey: string;-};+ keylessPublishableKeyMasked?: string;+ envPublishableKeyMasked?: string;+}

Add this helper outside the shown range:

functionmaskKeyForTelemetry(key?: string): string|undefined{if(!key)returnundefined;consthead=key.slice(0,8);consttail=key.slice(-4);return`${head}${tail}`;}
🤖 Prompt for AI Agents
In packages/nextjs/src/server/keyless-telemetry.ts around lines 11 to 18, the
telemetry payload currently exposes raw publishable keys and forces values into
strings; change the type to an interface with optional fields (publicKeyMatch?:
boolean; secretKeyMatch?: boolean; envVarsMissing?: boolean;
keylessFileHasKeys?: boolean; keylessPublishableKey?: string;
envPublishableKey?: string) so absent values are not coerced, and remove any
code that sends raw keys; instead add the provided maskKeyForTelemetry helper
(outside this range) and set keylessPublishableKey and envPublishableKey to the
masked results (or undefined) before sending telemetry so raw keys are never
transmitted.


/**
* Gets the absolute path to the telemetry flag file.
*
* This file is used to track whether telemetry events have already been fired
* to prevent duplicate event reporting during the application lifecycle.
*
* @returns The absolute path to the telemetry flag file in the project's .clerk/.tmp directory
*/
function getTelemetryFlagFilePath(): string {
return join(process.cwd(), TELEMETRY_FLAG_FILE);
}

/**
* Attempts to create a telemetry flag file to mark that a telemetry event has been fired.
*
* This function uses the 'wx' flag to create the file atomically - it will only succeed
* if the file doesn't already exist. This ensures that telemetry events are only fired
* once per application lifecycle, preventing duplicate event reporting.
*
* @returns Promise<boolean> - 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<boolean> {
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<void> - Function completes silently, errors are logged but don't throw
*/
export async function detectKeylessEnvDrift(): Promise<void> {
// 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 ?? '',
};
Comment thread
heatlikeheatwave marked this conversation as resolved.

// Create a clerk client to access telemetry
const clerkClient = createClerkClientWithOptions({
publishableKey: keylessFile.publishableKey,
secretKey: keylessFile.secretKey,
});

Comment thread
heatlikeheatwave marked this conversation as resolved.
const shouldFireEvent = await tryMarkTelemetryEventAsFired();

if (shouldFireEvent) {
// Fire drift detected event only if we successfully created the flag
const driftDetectedEvent: TelemetryEventRaw<EventKeylessEnvDriftPayload> = {
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);
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(nextjs): Environment drift telemetry event for keyless applications by heatlikeheatwave · Pull Request #6522 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/bright-parks-search.md
Original file line numberDiff line numberDiff line change
@@ -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
9 changes: 9 additions & 0 deletions packages/nextjs/src/app-router/server/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 = (
<KeylessProvider
Expand Down
184 changes: 184 additions & 0 deletions packages/nextjs/src/server/keyless-telemetry.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
import type { TelemetryEventRaw } from '@clerk/types';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';

import { createClerkClientWithOptions } from './createClerkClient';

const EVENT_KEYLESS_ENV_DRIFT_DETECTED = 'KEYLESS_ENV_DRIFT_DETECTED';
const EVENT_SAMPLING_RATE = 1; // 100% sampling rate
const TELEMETRY_FLAG_FILE = '.clerk/.tmp/telemetry.json';

Comment thread
heatlikeheatwave marked this conversation as resolved.
type EventKeylessEnvDriftPayload = {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
keylessPublishableKey: string;
envPublishableKey: string;
};
Comment on lines +11 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not send raw keys; mask publishable keys and make fields optional

Raw publishable keys shouldn’t be sent in telemetry. Also, casting/forcing missing values into strings can hide absence. Make fields optional and masked, and prefer interface for an extendable payload shape per guidelines.

-type EventKeylessEnvDriftPayload = {+interface EventKeylessEnvDriftPayload {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
- keylessPublishableKey: string;- envPublishableKey: string;-};+ keylessPublishableKeyMasked?: string;+ envPublishableKeyMasked?: string;+}

Add this helper outside the shown range:

functionmaskKeyForTelemetry(key?: string): string|undefined{if(!key)returnundefined;consthead=key.slice(0,8);consttail=key.slice(-4);return`${head}${tail}`;}
🤖 Prompt for AI Agents
In packages/nextjs/src/server/keyless-telemetry.ts around lines 11 to 18, the
telemetry payload currently exposes raw publishable keys and forces values into
strings; change the type to an interface with optional fields (publicKeyMatch?:
boolean; secretKeyMatch?: boolean; envVarsMissing?: boolean;
keylessFileHasKeys?: boolean; keylessPublishableKey?: string;
envPublishableKey?: string) so absent values are not coerced, and remove any
code that sends raw keys; instead add the provided maskKeyForTelemetry helper
(outside this range) and set keylessPublishableKey and envPublishableKey to the
masked results (or undefined) before sending telemetry so raw keys are never
transmitted.


/**
* Gets the absolute path to the telemetry flag file.
*
* This file is used to track whether telemetry events have already been fired
* to prevent duplicate event reporting during the application lifecycle.
*
* @returns The absolute path to the telemetry flag file in the project's .clerk/.tmp directory
*/
function getTelemetryFlagFilePath(): string {
return join(process.cwd(), TELEMETRY_FLAG_FILE);
}

/**
* Attempts to create a telemetry flag file to mark that a telemetry event has been fired.
*
* This function uses the 'wx' flag to create the file atomically - it will only succeed
* if the file doesn't already exist. This ensures that telemetry events are only fired
* once per application lifecycle, preventing duplicate event reporting.
*
* @returns Promise<boolean> - 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<boolean> {
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<void> - Function completes silently, errors are logged but don't throw
*/
export async function detectKeylessEnvDrift(): Promise<void> {
// 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 ?? '',
};
Comment thread
heatlikeheatwave marked this conversation as resolved.

// Create a clerk client to access telemetry
const clerkClient = createClerkClientWithOptions({
publishableKey: keylessFile.publishableKey,
secretKey: keylessFile.secretKey,
});

Comment thread
heatlikeheatwave marked this conversation as resolved.
const shouldFireEvent = await tryMarkTelemetryEventAsFired();

if (shouldFireEvent) {
// Fire drift detected event only if we successfully created the flag
const driftDetectedEvent: TelemetryEventRaw<EventKeylessEnvDriftPayload> = {
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);
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(nextjs): Environment drift telemetry event for keyless applications by heatlikeheatwave · Pull Request #6522 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/bright-parks-search.md
Original file line numberDiff line numberDiff line change
@@ -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
9 changes: 9 additions & 0 deletions packages/nextjs/src/app-router/server/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 = (
<KeylessProvider
Expand Down
184 changes: 184 additions & 0 deletions packages/nextjs/src/server/keyless-telemetry.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
import type { TelemetryEventRaw } from '@clerk/types';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';

import { createClerkClientWithOptions } from './createClerkClient';

const EVENT_KEYLESS_ENV_DRIFT_DETECTED = 'KEYLESS_ENV_DRIFT_DETECTED';
const EVENT_SAMPLING_RATE = 1; // 100% sampling rate
const TELEMETRY_FLAG_FILE = '.clerk/.tmp/telemetry.json';

Comment thread
heatlikeheatwave marked this conversation as resolved.
type EventKeylessEnvDriftPayload = {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
keylessPublishableKey: string;
envPublishableKey: string;
};
Comment on lines +11 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not send raw keys; mask publishable keys and make fields optional

Raw publishable keys shouldn’t be sent in telemetry. Also, casting/forcing missing values into strings can hide absence. Make fields optional and masked, and prefer interface for an extendable payload shape per guidelines.

-type EventKeylessEnvDriftPayload = {+interface EventKeylessEnvDriftPayload {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
- keylessPublishableKey: string;- envPublishableKey: string;-};+ keylessPublishableKeyMasked?: string;+ envPublishableKeyMasked?: string;+}

Add this helper outside the shown range:

functionmaskKeyForTelemetry(key?: string): string|undefined{if(!key)returnundefined;consthead=key.slice(0,8);consttail=key.slice(-4);return`${head}${tail}`;}
🤖 Prompt for AI Agents
In packages/nextjs/src/server/keyless-telemetry.ts around lines 11 to 18, the
telemetry payload currently exposes raw publishable keys and forces values into
strings; change the type to an interface with optional fields (publicKeyMatch?:
boolean; secretKeyMatch?: boolean; envVarsMissing?: boolean;
keylessFileHasKeys?: boolean; keylessPublishableKey?: string;
envPublishableKey?: string) so absent values are not coerced, and remove any
code that sends raw keys; instead add the provided maskKeyForTelemetry helper
(outside this range) and set keylessPublishableKey and envPublishableKey to the
masked results (or undefined) before sending telemetry so raw keys are never
transmitted.


/**
* Gets the absolute path to the telemetry flag file.
*
* This file is used to track whether telemetry events have already been fired
* to prevent duplicate event reporting during the application lifecycle.
*
* @returns The absolute path to the telemetry flag file in the project's .clerk/.tmp directory
*/
function getTelemetryFlagFilePath(): string {
return join(process.cwd(), TELEMETRY_FLAG_FILE);
}

/**
* Attempts to create a telemetry flag file to mark that a telemetry event has been fired.
*
* This function uses the 'wx' flag to create the file atomically - it will only succeed
* if the file doesn't already exist. This ensures that telemetry events are only fired
* once per application lifecycle, preventing duplicate event reporting.
*
* @returns Promise<boolean> - 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<boolean> {
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<void> - Function completes silently, errors are logged but don't throw
*/
export async function detectKeylessEnvDrift(): Promise<void> {
// 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 ?? '',
};
Comment thread
heatlikeheatwave marked this conversation as resolved.

// Create a clerk client to access telemetry
const clerkClient = createClerkClientWithOptions({
publishableKey: keylessFile.publishableKey,
secretKey: keylessFile.secretKey,
});

Comment thread
heatlikeheatwave marked this conversation as resolved.
const shouldFireEvent = await tryMarkTelemetryEventAsFired();

if (shouldFireEvent) {
// Fire drift detected event only if we successfully created the flag
const driftDetectedEvent: TelemetryEventRaw<EventKeylessEnvDriftPayload> = {
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);
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(nextjs): Environment drift telemetry event for keyless applications by heatlikeheatwave · Pull Request #6522 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/bright-parks-search.md
Original file line numberDiff line numberDiff line change
@@ -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
9 changes: 9 additions & 0 deletions packages/nextjs/src/app-router/server/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 = (
<KeylessProvider
Expand Down
184 changes: 184 additions & 0 deletions packages/nextjs/src/server/keyless-telemetry.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
import type { TelemetryEventRaw } from '@clerk/types';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';

import { createClerkClientWithOptions } from './createClerkClient';

const EVENT_KEYLESS_ENV_DRIFT_DETECTED = 'KEYLESS_ENV_DRIFT_DETECTED';
const EVENT_SAMPLING_RATE = 1; // 100% sampling rate
const TELEMETRY_FLAG_FILE = '.clerk/.tmp/telemetry.json';

Comment thread
heatlikeheatwave marked this conversation as resolved.
type EventKeylessEnvDriftPayload = {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
keylessPublishableKey: string;
envPublishableKey: string;
};
Comment on lines +11 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not send raw keys; mask publishable keys and make fields optional

Raw publishable keys shouldn’t be sent in telemetry. Also, casting/forcing missing values into strings can hide absence. Make fields optional and masked, and prefer interface for an extendable payload shape per guidelines.

-type EventKeylessEnvDriftPayload = {+interface EventKeylessEnvDriftPayload {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
- keylessPublishableKey: string;- envPublishableKey: string;-};+ keylessPublishableKeyMasked?: string;+ envPublishableKeyMasked?: string;+}

Add this helper outside the shown range:

functionmaskKeyForTelemetry(key?: string): string|undefined{if(!key)returnundefined;consthead=key.slice(0,8);consttail=key.slice(-4);return`${head}${tail}`;}
🤖 Prompt for AI Agents
In packages/nextjs/src/server/keyless-telemetry.ts around lines 11 to 18, the
telemetry payload currently exposes raw publishable keys and forces values into
strings; change the type to an interface with optional fields (publicKeyMatch?:
boolean; secretKeyMatch?: boolean; envVarsMissing?: boolean;
keylessFileHasKeys?: boolean; keylessPublishableKey?: string;
envPublishableKey?: string) so absent values are not coerced, and remove any
code that sends raw keys; instead add the provided maskKeyForTelemetry helper
(outside this range) and set keylessPublishableKey and envPublishableKey to the
masked results (or undefined) before sending telemetry so raw keys are never
transmitted.


/**
* Gets the absolute path to the telemetry flag file.
*
* This file is used to track whether telemetry events have already been fired
* to prevent duplicate event reporting during the application lifecycle.
*
* @returns The absolute path to the telemetry flag file in the project's .clerk/.tmp directory
*/
function getTelemetryFlagFilePath(): string {
return join(process.cwd(), TELEMETRY_FLAG_FILE);
}

/**
* Attempts to create a telemetry flag file to mark that a telemetry event has been fired.
*
* This function uses the 'wx' flag to create the file atomically - it will only succeed
* if the file doesn't already exist. This ensures that telemetry events are only fired
* once per application lifecycle, preventing duplicate event reporting.
*
* @returns Promise<boolean> - 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<boolean> {
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<void> - Function completes silently, errors are logged but don't throw
*/
export async function detectKeylessEnvDrift(): Promise<void> {
// 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 ?? '',
};
Comment thread
heatlikeheatwave marked this conversation as resolved.

// Create a clerk client to access telemetry
const clerkClient = createClerkClientWithOptions({
publishableKey: keylessFile.publishableKey,
secretKey: keylessFile.secretKey,
});

Comment thread
heatlikeheatwave marked this conversation as resolved.
const shouldFireEvent = await tryMarkTelemetryEventAsFired();

if (shouldFireEvent) {
// Fire drift detected event only if we successfully created the flag
const driftDetectedEvent: TelemetryEventRaw<EventKeylessEnvDriftPayload> = {
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);
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(nextjs): Environment drift telemetry event for keyless applications by heatlikeheatwave · Pull Request #6522 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/bright-parks-search.md
Original file line numberDiff line numberDiff line change
@@ -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
9 changes: 9 additions & 0 deletions packages/nextjs/src/app-router/server/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 = (
<KeylessProvider
Expand Down
184 changes: 184 additions & 0 deletions packages/nextjs/src/server/keyless-telemetry.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
import type { TelemetryEventRaw } from '@clerk/types';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';

import { createClerkClientWithOptions } from './createClerkClient';

const EVENT_KEYLESS_ENV_DRIFT_DETECTED = 'KEYLESS_ENV_DRIFT_DETECTED';
const EVENT_SAMPLING_RATE = 1; // 100% sampling rate
const TELEMETRY_FLAG_FILE = '.clerk/.tmp/telemetry.json';

Comment thread
heatlikeheatwave marked this conversation as resolved.
type EventKeylessEnvDriftPayload = {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
keylessPublishableKey: string;
envPublishableKey: string;
};
Comment on lines +11 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not send raw keys; mask publishable keys and make fields optional

Raw publishable keys shouldn’t be sent in telemetry. Also, casting/forcing missing values into strings can hide absence. Make fields optional and masked, and prefer interface for an extendable payload shape per guidelines.

-type EventKeylessEnvDriftPayload = {+interface EventKeylessEnvDriftPayload {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
- keylessPublishableKey: string;- envPublishableKey: string;-};+ keylessPublishableKeyMasked?: string;+ envPublishableKeyMasked?: string;+}

Add this helper outside the shown range:

functionmaskKeyForTelemetry(key?: string): string|undefined{if(!key)returnundefined;consthead=key.slice(0,8);consttail=key.slice(-4);return`${head}${tail}`;}
🤖 Prompt for AI Agents
In packages/nextjs/src/server/keyless-telemetry.ts around lines 11 to 18, the
telemetry payload currently exposes raw publishable keys and forces values into
strings; change the type to an interface with optional fields (publicKeyMatch?:
boolean; secretKeyMatch?: boolean; envVarsMissing?: boolean;
keylessFileHasKeys?: boolean; keylessPublishableKey?: string;
envPublishableKey?: string) so absent values are not coerced, and remove any
code that sends raw keys; instead add the provided maskKeyForTelemetry helper
(outside this range) and set keylessPublishableKey and envPublishableKey to the
masked results (or undefined) before sending telemetry so raw keys are never
transmitted.


/**
* Gets the absolute path to the telemetry flag file.
*
* This file is used to track whether telemetry events have already been fired
* to prevent duplicate event reporting during the application lifecycle.
*
* @returns The absolute path to the telemetry flag file in the project's .clerk/.tmp directory
*/
function getTelemetryFlagFilePath(): string {
return join(process.cwd(), TELEMETRY_FLAG_FILE);
}

/**
* Attempts to create a telemetry flag file to mark that a telemetry event has been fired.
*
* This function uses the 'wx' flag to create the file atomically - it will only succeed
* if the file doesn't already exist. This ensures that telemetry events are only fired
* once per application lifecycle, preventing duplicate event reporting.
*
* @returns Promise<boolean> - 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<boolean> {
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<void> - Function completes silently, errors are logged but don't throw
*/
export async function detectKeylessEnvDrift(): Promise<void> {
// 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 ?? '',
};
Comment thread
heatlikeheatwave marked this conversation as resolved.

// Create a clerk client to access telemetry
const clerkClient = createClerkClientWithOptions({
publishableKey: keylessFile.publishableKey,
secretKey: keylessFile.secretKey,
});

Comment thread
heatlikeheatwave marked this conversation as resolved.
const shouldFireEvent = await tryMarkTelemetryEventAsFired();

if (shouldFireEvent) {
// Fire drift detected event only if we successfully created the flag
const driftDetectedEvent: TelemetryEventRaw<EventKeylessEnvDriftPayload> = {
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);
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(nextjs): Environment drift telemetry event for keyless applications by heatlikeheatwave · Pull Request #6522 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/bright-parks-search.md
Original file line numberDiff line numberDiff line change
@@ -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
9 changes: 9 additions & 0 deletions packages/nextjs/src/app-router/server/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 = (
<KeylessProvider
Expand Down
184 changes: 184 additions & 0 deletions packages/nextjs/src/server/keyless-telemetry.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
import type { TelemetryEventRaw } from '@clerk/types';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';

import { createClerkClientWithOptions } from './createClerkClient';

const EVENT_KEYLESS_ENV_DRIFT_DETECTED = 'KEYLESS_ENV_DRIFT_DETECTED';
const EVENT_SAMPLING_RATE = 1; // 100% sampling rate
const TELEMETRY_FLAG_FILE = '.clerk/.tmp/telemetry.json';

Comment thread
heatlikeheatwave marked this conversation as resolved.
type EventKeylessEnvDriftPayload = {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
keylessPublishableKey: string;
envPublishableKey: string;
};
Comment on lines +11 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not send raw keys; mask publishable keys and make fields optional

Raw publishable keys shouldn’t be sent in telemetry. Also, casting/forcing missing values into strings can hide absence. Make fields optional and masked, and prefer interface for an extendable payload shape per guidelines.

-type EventKeylessEnvDriftPayload = {+interface EventKeylessEnvDriftPayload {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
- keylessPublishableKey: string;- envPublishableKey: string;-};+ keylessPublishableKeyMasked?: string;+ envPublishableKeyMasked?: string;+}

Add this helper outside the shown range:

functionmaskKeyForTelemetry(key?: string): string|undefined{if(!key)returnundefined;consthead=key.slice(0,8);consttail=key.slice(-4);return`${head}${tail}`;}
🤖 Prompt for AI Agents
In packages/nextjs/src/server/keyless-telemetry.ts around lines 11 to 18, the
telemetry payload currently exposes raw publishable keys and forces values into
strings; change the type to an interface with optional fields (publicKeyMatch?:
boolean; secretKeyMatch?: boolean; envVarsMissing?: boolean;
keylessFileHasKeys?: boolean; keylessPublishableKey?: string;
envPublishableKey?: string) so absent values are not coerced, and remove any
code that sends raw keys; instead add the provided maskKeyForTelemetry helper
(outside this range) and set keylessPublishableKey and envPublishableKey to the
masked results (or undefined) before sending telemetry so raw keys are never
transmitted.


/**
* Gets the absolute path to the telemetry flag file.
*
* This file is used to track whether telemetry events have already been fired
* to prevent duplicate event reporting during the application lifecycle.
*
* @returns The absolute path to the telemetry flag file in the project's .clerk/.tmp directory
*/
function getTelemetryFlagFilePath(): string {
return join(process.cwd(), TELEMETRY_FLAG_FILE);
}

/**
* Attempts to create a telemetry flag file to mark that a telemetry event has been fired.
*
* This function uses the 'wx' flag to create the file atomically - it will only succeed
* if the file doesn't already exist. This ensures that telemetry events are only fired
* once per application lifecycle, preventing duplicate event reporting.
*
* @returns Promise<boolean> - 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<boolean> {
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<void> - Function completes silently, errors are logged but don't throw
*/
export async function detectKeylessEnvDrift(): Promise<void> {
// 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 ?? '',
};
Comment thread
heatlikeheatwave marked this conversation as resolved.

// Create a clerk client to access telemetry
const clerkClient = createClerkClientWithOptions({
publishableKey: keylessFile.publishableKey,
secretKey: keylessFile.secretKey,
});

Comment thread
heatlikeheatwave marked this conversation as resolved.
const shouldFireEvent = await tryMarkTelemetryEventAsFired();

if (shouldFireEvent) {
// Fire drift detected event only if we successfully created the flag
const driftDetectedEvent: TelemetryEventRaw<EventKeylessEnvDriftPayload> = {
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);
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(nextjs): Environment drift telemetry event for keyless applications by heatlikeheatwave · Pull Request #6522 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/bright-parks-search.md
Original file line numberDiff line numberDiff line change
@@ -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
9 changes: 9 additions & 0 deletions packages/nextjs/src/app-router/server/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 = (
<KeylessProvider
Expand Down
184 changes: 184 additions & 0 deletions packages/nextjs/src/server/keyless-telemetry.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
import type { TelemetryEventRaw } from '@clerk/types';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';

import { createClerkClientWithOptions } from './createClerkClient';

const EVENT_KEYLESS_ENV_DRIFT_DETECTED = 'KEYLESS_ENV_DRIFT_DETECTED';
const EVENT_SAMPLING_RATE = 1; // 100% sampling rate
const TELEMETRY_FLAG_FILE = '.clerk/.tmp/telemetry.json';

Comment thread
heatlikeheatwave marked this conversation as resolved.
type EventKeylessEnvDriftPayload = {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
keylessPublishableKey: string;
envPublishableKey: string;
};
Comment on lines +11 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not send raw keys; mask publishable keys and make fields optional

Raw publishable keys shouldn’t be sent in telemetry. Also, casting/forcing missing values into strings can hide absence. Make fields optional and masked, and prefer interface for an extendable payload shape per guidelines.

-type EventKeylessEnvDriftPayload = {+interface EventKeylessEnvDriftPayload {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
- keylessPublishableKey: string;- envPublishableKey: string;-};+ keylessPublishableKeyMasked?: string;+ envPublishableKeyMasked?: string;+}

Add this helper outside the shown range:

functionmaskKeyForTelemetry(key?: string): string|undefined{if(!key)returnundefined;consthead=key.slice(0,8);consttail=key.slice(-4);return`${head}${tail}`;}
🤖 Prompt for AI Agents
In packages/nextjs/src/server/keyless-telemetry.ts around lines 11 to 18, the
telemetry payload currently exposes raw publishable keys and forces values into
strings; change the type to an interface with optional fields (publicKeyMatch?:
boolean; secretKeyMatch?: boolean; envVarsMissing?: boolean;
keylessFileHasKeys?: boolean; keylessPublishableKey?: string;
envPublishableKey?: string) so absent values are not coerced, and remove any
code that sends raw keys; instead add the provided maskKeyForTelemetry helper
(outside this range) and set keylessPublishableKey and envPublishableKey to the
masked results (or undefined) before sending telemetry so raw keys are never
transmitted.


/**
* Gets the absolute path to the telemetry flag file.
*
* This file is used to track whether telemetry events have already been fired
* to prevent duplicate event reporting during the application lifecycle.
*
* @returns The absolute path to the telemetry flag file in the project's .clerk/.tmp directory
*/
function getTelemetryFlagFilePath(): string {
return join(process.cwd(), TELEMETRY_FLAG_FILE);
}

/**
* Attempts to create a telemetry flag file to mark that a telemetry event has been fired.
*
* This function uses the 'wx' flag to create the file atomically - it will only succeed
* if the file doesn't already exist. This ensures that telemetry events are only fired
* once per application lifecycle, preventing duplicate event reporting.
*
* @returns Promise<boolean> - 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<boolean> {
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<void> - Function completes silently, errors are logged but don't throw
*/
export async function detectKeylessEnvDrift(): Promise<void> {
// 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 ?? '',
};
Comment thread
heatlikeheatwave marked this conversation as resolved.

// Create a clerk client to access telemetry
const clerkClient = createClerkClientWithOptions({
publishableKey: keylessFile.publishableKey,
secretKey: keylessFile.secretKey,
});

Comment thread
heatlikeheatwave marked this conversation as resolved.
const shouldFireEvent = await tryMarkTelemetryEventAsFired();

if (shouldFireEvent) {
// Fire drift detected event only if we successfully created the flag
const driftDetectedEvent: TelemetryEventRaw<EventKeylessEnvDriftPayload> = {
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);
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(nextjs): Environment drift telemetry event for keyless applications by heatlikeheatwave · Pull Request #6522 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/bright-parks-search.md
Original file line numberDiff line numberDiff line change
@@ -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
9 changes: 9 additions & 0 deletions packages/nextjs/src/app-router/server/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 = (
<KeylessProvider
Expand Down
184 changes: 184 additions & 0 deletions packages/nextjs/src/server/keyless-telemetry.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
import type { TelemetryEventRaw } from '@clerk/types';
import { promises as fs } from 'fs';
import { dirname, join } from 'path';

import { createClerkClientWithOptions } from './createClerkClient';

const EVENT_KEYLESS_ENV_DRIFT_DETECTED = 'KEYLESS_ENV_DRIFT_DETECTED';
const EVENT_SAMPLING_RATE = 1; // 100% sampling rate
const TELEMETRY_FLAG_FILE = '.clerk/.tmp/telemetry.json';

Comment thread
heatlikeheatwave marked this conversation as resolved.
type EventKeylessEnvDriftPayload = {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
keylessPublishableKey: string;
envPublishableKey: string;
};
Comment on lines +11 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not send raw keys; mask publishable keys and make fields optional

Raw publishable keys shouldn’t be sent in telemetry. Also, casting/forcing missing values into strings can hide absence. Make fields optional and masked, and prefer interface for an extendable payload shape per guidelines.

-type EventKeylessEnvDriftPayload = {+interface EventKeylessEnvDriftPayload {
publicKeyMatch: boolean;
secretKeyMatch: boolean;
envVarsMissing: boolean;
keylessFileHasKeys: boolean;
- keylessPublishableKey: string;- envPublishableKey: string;-};+ keylessPublishableKeyMasked?: string;+ envPublishableKeyMasked?: string;+}

Add this helper outside the shown range:

functionmaskKeyForTelemetry(key?: string): string|undefined{if(!key)returnundefined;consthead=key.slice(0,8);consttail=key.slice(-4);return`${head}${tail}`;}
🤖 Prompt for AI Agents
In packages/nextjs/src/server/keyless-telemetry.ts around lines 11 to 18, the
telemetry payload currently exposes raw publishable keys and forces values into
strings; change the type to an interface with optional fields (publicKeyMatch?:
boolean; secretKeyMatch?: boolean; envVarsMissing?: boolean;
keylessFileHasKeys?: boolean; keylessPublishableKey?: string;
envPublishableKey?: string) so absent values are not coerced, and remove any
code that sends raw keys; instead add the provided maskKeyForTelemetry helper
(outside this range) and set keylessPublishableKey and envPublishableKey to the
masked results (or undefined) before sending telemetry so raw keys are never
transmitted.


/**
* Gets the absolute path to the telemetry flag file.
*
* This file is used to track whether telemetry events have already been fired
* to prevent duplicate event reporting during the application lifecycle.
*
* @returns The absolute path to the telemetry flag file in the project's .clerk/.tmp directory
*/
function getTelemetryFlagFilePath(): string {
return join(process.cwd(), TELEMETRY_FLAG_FILE);
}

/**
* Attempts to create a telemetry flag file to mark that a telemetry event has been fired.
*
* This function uses the 'wx' flag to create the file atomically - it will only succeed
* if the file doesn't already exist. This ensures that telemetry events are only fired
* once per application lifecycle, preventing duplicate event reporting.
*
* @returns Promise<boolean> - 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<boolean> {
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<void> - Function completes silently, errors are logged but don't throw
*/
export async function detectKeylessEnvDrift(): Promise<void> {
// 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 ?? '',
};
Comment thread
heatlikeheatwave marked this conversation as resolved.

// Create a clerk client to access telemetry
const clerkClient = createClerkClientWithOptions({
publishableKey: keylessFile.publishableKey,
secretKey: keylessFile.secretKey,
});

Comment thread
heatlikeheatwave marked this conversation as resolved.
const shouldFireEvent = await tryMarkTelemetryEventAsFired();

if (shouldFireEvent) {
// Fire drift detected event only if we successfully created the flag
const driftDetectedEvent: TelemetryEventRaw<EventKeylessEnvDriftPayload> = {
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);
}
}
Loading