1
1
import type { TelemetryEventRaw } from '@clerk/types' ;
2
+ import { promises as fs } from 'fs' ;
3
+ import { dirname , join } from 'path' ;
2
4
3
5
import { createClerkClientWithOptions } from './createClerkClient' ;
4
6
5
7
const EVENT_KEYLESS_ENV_DRIFT_DETECTED = 'KEYLESS_ENV_DRIFT_DETECTED' ;
6
8
const EVENT_SAMPLING_RATE = 1 ; // 100% sampling rate
9
+ const TELEMETRY_FLAG_FILE = '.clerk/.tmp/telemetry.json' ;
7
10
8
11
type EventKeylessEnvDriftPayload = {
9
12
publicKeyMatch : boolean ;
@@ -15,14 +18,72 @@ type EventKeylessEnvDriftPayload = {
15
18
} ;
16
19
17
20
/**
18
- * Detects environment variable drift for keyless Next.js applications and fires telemetry events .
21
+ * Gets the absolute path to the telemetry flag file .
19
22
*
20
- * This function compares the publishableKey and secretKey values from `.clerk/.tmp/keyless.json`
21
- * with the `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` environment variables .
23
+ * This file is used to track whether telemetry events have already been fired
24
+ * to prevent duplicate event reporting during the application lifecycle .
22
25
*
23
- * If there's a mismatch, it fires a `KEYLESS_ENV_DRIFT_DETECTED` event.
24
- * For local testing purposes, it also fires a `KEYLESS_ENV_DRIFT_NOT_DETECTED` event when
25
- * keys exist and match the environment variables.
26
+ * @returns The absolute path to the telemetry flag file in the project's .clerk/.tmp directory
27
+ */
28
+ function getTelemetryFlagFilePath ( ) : string {
29
+ return join ( process . cwd ( ) , TELEMETRY_FLAG_FILE ) ;
30
+ }
31
+
32
+ /**
33
+ * Attempts to create a telemetry flag file to mark that a telemetry event has been fired.
34
+ *
35
+ * This function uses the 'wx' flag to create the file atomically - it will only succeed
36
+ * if the file doesn't already exist. This ensures that telemetry events are only fired
37
+ * once per application lifecycle, preventing duplicate event reporting.
38
+ *
39
+ * @returns Promise<boolean> - Returns true if the flag file was successfully created (meaning
40
+ * the event should be fired), false if the file already exists (meaning the event was
41
+ * already fired) or if there was an error creating the file
42
+ */
43
+ async function tryMarkTelemetryEventAsFired ( ) : Promise < boolean > {
44
+ try {
45
+ const flagFilePath = getTelemetryFlagFilePath ( ) ;
46
+ const flagDirectory = dirname ( flagFilePath ) ;
47
+
48
+ // Ensure the directory exists before attempting to write the file
49
+ await fs . mkdir ( flagDirectory , { recursive : true } ) ;
50
+
51
+ const flagData = {
52
+ firedAt : new Date ( ) . toISOString ( ) ,
53
+ event : EVENT_KEYLESS_ENV_DRIFT_DETECTED ,
54
+ } ;
55
+ await fs . writeFile ( flagFilePath , JSON . stringify ( flagData , null , 2 ) , { flag : 'wx' } ) ;
56
+ return true ;
57
+ } catch ( error : unknown ) {
58
+ if ( ( error as { code ?: string } ) ?. code === 'EEXIST' ) {
59
+ return false ;
60
+ }
61
+ console . warn ( 'Failed to create telemetry flag file:' , error ) ;
62
+ return false ;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Detects and reports environment drift between keyless configuration and environment variables.
68
+ *
69
+ * This function compares the Clerk keys stored in the keyless configuration file (.clerk/clerk.json)
70
+ * with the keys set in environment variables (NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY).
71
+ * It only reports drift when there's an actual mismatch between existing keys, not when keys are simply missing.
72
+ *
73
+ * The function handles several scenarios and only reports drift in specific cases:
74
+ * - **Normal keyless mode**: env vars missing but keyless file has keys → no drift (expected)
75
+ * - **No configuration**: neither env vars nor keyless file have keys → no drift (nothing to compare)
76
+ * - **Actual drift**: env vars exist and don't match keyless file keys → drift detected
77
+ * - **Empty keyless file**: keyless file exists but has no keys → no drift (nothing to compare)
78
+ *
79
+ * Drift is only detected when:
80
+ * 1. Both environment variables and keyless file contain keys
81
+ * 2. The keys in environment variables don't match the keys in the keyless file
82
+ *
83
+ * Telemetry events are only fired once per application lifecycle using a flag file mechanism
84
+ * to prevent duplicate reporting.
85
+ *
86
+ * @returns Promise<void> - Function completes silently, errors are logged but don't throw
26
87
*/
27
88
export async function detectKeylessEnvDrift ( ) : Promise < void > {
28
89
// Only run on server side
@@ -45,31 +106,57 @@ export async function detectKeylessEnvDrift(): Promise<void> {
45
106
const envPublishableKey = process . env . NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY ;
46
107
const envSecretKey = process . env . CLERK_SECRET_KEY ;
47
108
48
- // Check if environment variables are missing, and keys exist in keyless file
49
- const envVarsMissing = ! envPublishableKey && ! envSecretKey ;
109
+ // Check the state of environment variables and keyless file
110
+ const hasEnvVars = Boolean ( envPublishableKey || envSecretKey ) ;
50
111
const keylessFileHasKeys = Boolean ( keylessFile ?. publishableKey && keylessFile ?. secretKey ) ;
112
+ const envVarsMissing = ! envPublishableKey && ! envSecretKey ;
113
+
114
+ // Early return conditions - no drift to detect in these scenarios:
115
+ if ( ! hasEnvVars && ! keylessFileHasKeys ) {
116
+ // Neither env vars nor keyless file have keys - nothing to compare
117
+ return ;
118
+ }
51
119
52
120
if ( envVarsMissing && keylessFileHasKeys ) {
53
121
// Environment variables are missing but keyless file has keys - this is normal for keyless mode
54
122
return ;
55
123
}
56
124
57
- // Compare publishable keys
58
- const publicKeyMatch = Boolean ( envPublishableKey === keylessFile ?. publishableKey ) ;
125
+ if ( ! keylessFileHasKeys ) {
126
+ // Keyless file doesn't have keys, so no drift can be detected
127
+ return ;
128
+ }
129
+
130
+ // Only proceed with drift detection if we have something meaningful to compare
131
+ if ( ! hasEnvVars ) {
132
+ return ;
133
+ }
134
+
135
+ // Compare keys only when both sides have values to compare
136
+ const publicKeyMatch = Boolean (
137
+ envPublishableKey && keylessFile . publishableKey && envPublishableKey === keylessFile . publishableKey ,
138
+ ) ;
59
139
60
- // Compare secret keys
61
- const secretKeyMatch = Boolean ( envSecretKey === keylessFile ?. secretKey ) ;
140
+ const secretKeyMatch = Boolean ( envSecretKey && keylessFile . secretKey && envSecretKey === keylessFile . secretKey ) ;
62
141
63
- // Check if there's a drift (mismatch between env vars and keyless file)
64
- const hasDrift = ! publicKeyMatch || ! secretKeyMatch ;
142
+ // Determine if there's an actual drift:
143
+ // Drift occurs when we have env vars that don't match the keyless file keys
144
+ const hasActualDrift =
145
+ ( envPublishableKey && keylessFile . publishableKey && ! publicKeyMatch ) ||
146
+ ( envSecretKey && keylessFile . secretKey && ! secretKeyMatch ) ;
147
+
148
+ // Only fire telemetry if there's an actual drift (not just missing keys)
149
+ if ( ! hasActualDrift ) {
150
+ return ;
151
+ }
65
152
66
153
const payload : EventKeylessEnvDriftPayload = {
67
154
publicKeyMatch,
68
155
secretKeyMatch,
69
156
envVarsMissing,
70
157
keylessFileHasKeys,
71
- keylessPublishableKey : keylessFile . publishableKey ,
72
- envPublishableKey : envPublishableKey as string ,
158
+ keylessPublishableKey : keylessFile . publishableKey ?? '' ,
159
+ envPublishableKey : envPublishableKey ?? '' ,
73
160
} ;
74
161
75
162
// Create a clerk client to access telemetry
@@ -78,8 +165,10 @@ export async function detectKeylessEnvDrift(): Promise<void> {
78
165
secretKey : keylessFile . secretKey ,
79
166
} ) ;
80
167
81
- if ( hasDrift ) {
82
- // Fire drift detected event
168
+ const shouldFireEvent = await tryMarkTelemetryEventAsFired ( ) ;
169
+
170
+ if ( shouldFireEvent ) {
171
+ // Fire drift detected event only if we successfully created the flag
83
172
const driftDetectedEvent : TelemetryEventRaw < EventKeylessEnvDriftPayload > = {
84
173
event : EVENT_KEYLESS_ENV_DRIFT_DETECTED ,
85
174
eventSamplingRate : EVENT_SAMPLING_RATE ,
0 commit comments