diff --git a/.changeset/eleven-cougars-film.md b/.changeset/eleven-cougars-film.md new file mode 100644 index 00000000000..a6cdfb0640b --- /dev/null +++ b/.changeset/eleven-cougars-film.md @@ -0,0 +1,13 @@ +--- +"@clerk/shared": minor +--- + +Introduce unified environment variable handling across all supported platforms + +Usage: + +```ts +import { getEnvVariable } from '@clerk/shared/getEnvVariable' + +const publishableKey = getEnvVariable('CLERK_PUBLISHABLE_KEY') +``` diff --git a/.changeset/unlucky-gifts-obey.md b/.changeset/unlucky-gifts-obey.md new file mode 100644 index 00000000000..773fffbbc2d --- /dev/null +++ b/.changeset/unlucky-gifts-obey.md @@ -0,0 +1,6 @@ +--- +'@clerk/tanstack-start': patch +'@clerk/react-router': patch +--- + +Internal changes to use new `getEnvVariable` utility from `@clerk/shared` diff --git a/packages/react-router/src/ssr/loadOptions.ts b/packages/react-router/src/ssr/loadOptions.ts index 79a15dd0113..a958a39d9c6 100644 --- a/packages/react-router/src/ssr/loadOptions.ts +++ b/packages/react-router/src/ssr/loadOptions.ts @@ -1,10 +1,11 @@ import { createClerkRequest } from '@clerk/backend/internal'; import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey'; +import { getEnvVariable } from '@clerk/shared/getEnvVariable'; import { isDevelopmentFromSecretKey } from '@clerk/shared/keys'; import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy'; import { handleValueOrFn } from '@clerk/shared/utils'; -import { getEnvVariable, getPublicEnvVariables } from '../utils/env'; +import { getPublicEnvVariables } from '../utils/env'; import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../utils/errors'; import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types'; import { patchRequest } from './utils'; diff --git a/packages/react-router/src/utils/env.ts b/packages/react-router/src/utils/env.ts index 078a503a245..d151fbcfb98 100644 --- a/packages/react-router/src/utils/env.ts +++ b/packages/react-router/src/utils/env.ts @@ -1,59 +1,7 @@ +import { getEnvVariable } from '@clerk/shared/getEnvVariable'; import { isTruthy } from '@clerk/shared/underscore'; import type { AppLoadContext } from 'react-router'; -type CloudflareEnv = { env: Record }; - -const hasCloudflareProxyContext = (context: any): context is { cloudflare: CloudflareEnv } => { - return !!context?.cloudflare?.env; -}; - -const hasCloudflareContext = (context: any): context is CloudflareEnv => { - return !!context?.env; -}; - -/** - * - * Utility function to get env variables across Node and Edge runtimes. - * - * @param name - * @returns string - */ -export const getEnvVariable = (name: string, context: AppLoadContext | undefined): string => { - // Node envs - if (typeof process !== 'undefined' && process.env && typeof process.env[name] === 'string') { - return process.env[name]; - } - - // @ts-expect-error - Vite specific - if (typeof import.meta !== 'undefined' && import.meta.env && typeof import.meta.env[name] === 'string') { - // @ts-expect-error - Vite specific - return import.meta.env[name]; - } - - if (hasCloudflareProxyContext(context)) { - return context.cloudflare.env[name] || ''; - } - - // Cloudflare - if (hasCloudflareContext(context)) { - return context.env[name] || ''; - } - - // Check whether the value exists in the context object directly - if (context && typeof context[name] === 'string') { - return context[name]; - } - - // Cloudflare workers - try { - return globalThis[name as keyof typeof globalThis]; - } catch { - // This will raise an error in Cloudflare Pages - } - - return ''; -}; - export const getPublicEnvVariables = (context: AppLoadContext | undefined) => { return { publishableKey: diff --git a/packages/shared/global.d.ts b/packages/shared/global.d.ts index b91055a700d..5776b61ae17 100644 --- a/packages/shared/global.d.ts +++ b/packages/shared/global.d.ts @@ -2,3 +2,11 @@ declare const PACKAGE_NAME: string; declare const PACKAGE_VERSION: string; declare const JS_PACKAGE_VERSION: string; declare const __DEV__: boolean; + +interface ImportMetaEnv { + readonly [key: string]: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/packages/shared/package.json b/packages/shared/package.json index ce01dd916b6..2e8c5aeff29 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -115,6 +115,7 @@ "object", "oauth", "web3", + "getEnvVariable", "pathMatcher" ], "scripts": { diff --git a/packages/shared/src/getEnvVariable.ts b/packages/shared/src/getEnvVariable.ts new file mode 100644 index 00000000000..24cd5d5e3fe --- /dev/null +++ b/packages/shared/src/getEnvVariable.ts @@ -0,0 +1,50 @@ +type CloudflareEnv = { env: Record }; + +const hasCloudflareProxyContext = (context: any): context is { cloudflare: CloudflareEnv } => { + return !!context?.cloudflare?.env; +}; + +const hasCloudflareContext = (context: any): context is CloudflareEnv => { + return !!context?.env; +}; + +/** + * Retrieves an environment variable across runtime environments. + * @param name - The environment variable name to retrieve + * @param context - Optional context object that may contain environment values + * @returns The environment variable value or empty string if not found + */ +export const getEnvVariable = (name: string, context?: Record): string => { + // Node envs + if (typeof process !== 'undefined' && process.env && typeof process.env[name] === 'string') { + return process.env[name]; + } + + // Vite specific + if (typeof import.meta !== 'undefined' && import.meta.env && typeof import.meta.env[name] === 'string') { + return import.meta.env[name]; + } + + if (hasCloudflareProxyContext(context)) { + return context.cloudflare.env[name] || ''; + } + + // Cloudflare + if (hasCloudflareContext(context)) { + return context.env[name] || ''; + } + + // Check whether the value exists in the context object directly + if (context && typeof context[name] === 'string') { + return context[name]; + } + + // Cloudflare workers + try { + return globalThis[name as keyof typeof globalThis]; + } catch { + // This will raise an error in Cloudflare Pages + } + + return ''; +}; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 984c4c510c1..6c0d63672b1 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -35,4 +35,5 @@ export * from './object'; export * from './logger'; export { createWorkerTimers } from './workerTimers'; export { DEV_BROWSER_JWT_KEY, extractDevBrowserJWTFromURL, setDevBrowserJWTInURL } from './devBrowser'; +export { getEnvVariable } from './getEnvVariable'; export * from './pathMatcher'; diff --git a/packages/shared/tsup.config.ts b/packages/shared/tsup.config.ts index 695703ac940..6515b9da11d 100644 --- a/packages/shared/tsup.config.ts +++ b/packages/shared/tsup.config.ts @@ -24,6 +24,7 @@ export default defineConfig(overrideOptions => { minify: false, sourcemap: true, dts: true, + target: 'es2020', external: ['react', 'react-dom'], esbuildPlugins: [WebWorkerMinifyPlugin as any], define: { diff --git a/packages/tanstack-start/src/server/constants.ts b/packages/tanstack-start/src/server/constants.ts index 779ba2deb3f..ee90395c551 100644 --- a/packages/tanstack-start/src/server/constants.ts +++ b/packages/tanstack-start/src/server/constants.ts @@ -1,11 +1,12 @@ import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey'; +import { getEnvVariable } from '@clerk/shared/getEnvVariable'; import { getEvent } from 'vinxi/http'; -import { getEnvVariable, getPublicEnvVariables } from '../utils/env'; +import { getPublicEnvVariables } from '../utils/env'; export const commonEnvs = () => { const event = getEvent(); - const publicEnvs = getPublicEnvVariables(event); + const publicEnvs = getPublicEnvVariables(event.context); return { // Public environment variables @@ -21,16 +22,16 @@ export const commonEnvs = () => { TELEMETRY_DEBUG: publicEnvs.telemetryDebug, // Server-only environment variables - API_VERSION: getEnvVariable('CLERK_API_VERSION', 'v1', event), - SECRET_KEY: getEnvVariable('CLERK_SECRET_KEY', '', event), - ENCRYPTION_KEY: getEnvVariable('CLERK_ENCRYPTION_KEY', '', event), - CLERK_JWT_KEY: getEnvVariable('CLERK_JWT_KEY', '', event), - API_URL: getEnvVariable('CLERK_API_URL', '', event) || apiUrlFromPublishableKey(publicEnvs.publishableKey), + API_VERSION: getEnvVariable('CLERK_API_VERSION', event.context) || 'v1', + SECRET_KEY: getEnvVariable('CLERK_SECRET_KEY', event.context), + ENCRYPTION_KEY: getEnvVariable('CLERK_ENCRYPTION_KEY', event.context), + CLERK_JWT_KEY: getEnvVariable('CLERK_JWT_KEY', event.context), + API_URL: getEnvVariable('CLERK_API_URL', event.context) || apiUrlFromPublishableKey(publicEnvs.publishableKey), SDK_METADATA: { name: PACKAGE_NAME, version: PACKAGE_VERSION, - environment: getEnvVariable('NODE_ENV', '', event), + environment: getEnvVariable('NODE_ENV', event.context), }, } as const; }; diff --git a/packages/tanstack-start/src/server/loadOptions.ts b/packages/tanstack-start/src/server/loadOptions.ts index a17346f47db..f45128334e1 100644 --- a/packages/tanstack-start/src/server/loadOptions.ts +++ b/packages/tanstack-start/src/server/loadOptions.ts @@ -1,12 +1,13 @@ import { createClerkRequest } from '@clerk/backend/internal'; import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey'; +import { getEnvVariable } from '@clerk/shared/getEnvVariable'; import { isDevelopmentFromSecretKey } from '@clerk/shared/keys'; import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy'; import { handleValueOrFn } from '@clerk/shared/utils'; import { getEvent } from 'vinxi/http'; import { errorThrower } from '../utils'; -import { getEnvVariable, getPublicEnvVariables } from '../utils/env'; +import { getPublicEnvVariables } from '../utils/env'; import { commonEnvs } from './constants'; import type { LoaderOptions } from './types'; import { patchRequest } from './utils'; @@ -18,14 +19,14 @@ export const loadOptions = (request: Request, overrides: LoaderOptions = {}) => const secretKey = overrides.secretKey || commonEnv.SECRET_KEY; const publishableKey = overrides.publishableKey || commonEnv.PUBLISHABLE_KEY; const jwtKey = overrides.jwtKey || commonEnv.CLERK_JWT_KEY; - const apiUrl = getEnvVariable('CLERK_API_URL', '', event) || apiUrlFromPublishableKey(publishableKey); + const apiUrl = getEnvVariable('CLERK_API_URL', event.context) || apiUrlFromPublishableKey(publishableKey); const domain = handleValueOrFn(overrides.domain, new URL(request.url)) || commonEnv.DOMAIN; const isSatellite = handleValueOrFn(overrides.isSatellite, new URL(request.url)) || commonEnv.IS_SATELLITE; const relativeOrAbsoluteProxyUrl = handleValueOrFn(overrides?.proxyUrl, clerkRequest.clerkUrl, commonEnv.PROXY_URL); const signInUrl = overrides.signInUrl || commonEnv.SIGN_IN_URL; const signUpUrl = overrides.signUpUrl || commonEnv.SIGN_UP_URL; - const afterSignInUrl = overrides.afterSignInUrl || getPublicEnvVariables(event).afterSignInUrl; - const afterSignUpUrl = overrides.afterSignUpUrl || getPublicEnvVariables(event).afterSignUpUrl; + const afterSignInUrl = overrides.afterSignInUrl || getPublicEnvVariables(event.context).afterSignInUrl; + const afterSignUpUrl = overrides.afterSignUpUrl || getPublicEnvVariables(event.context).afterSignUpUrl; let proxyUrl; if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) { diff --git a/packages/tanstack-start/src/server/utils/index.ts b/packages/tanstack-start/src/server/utils/index.ts index ae52dff32a1..e9b431bd644 100644 --- a/packages/tanstack-start/src/server/utils/index.ts +++ b/packages/tanstack-start/src/server/utils/index.ts @@ -1,8 +1,9 @@ import type { RequestState } from '@clerk/backend/internal'; import { debugRequestState } from '@clerk/backend/internal'; +import { getEnvVariable } from '@clerk/shared/getEnvVariable'; import { isTruthy } from '@clerk/shared/underscore'; +import { getEvent } from 'vinxi/http'; -import { getEnvVariable } from '../../utils/env'; import type { AdditionalStateOptions } from '../types'; /** @@ -22,6 +23,7 @@ export const wrapWithClerkState = (data: any) => { */ export function getResponseClerkState(requestState: RequestState, additionalStateOptions: AdditionalStateOptions = {}) { const { reason, message, isSignedIn, ...rest } = requestState; + const event = getEvent(); const clerkInitialState = wrapWithClerkState({ __clerk_ssr_state: rest.toAuth(), @@ -34,18 +36,26 @@ export function getResponseClerkState(requestState: RequestState, additionalStat __afterSignInUrl: requestState.afterSignInUrl, __afterSignUpUrl: requestState.afterSignUpUrl, __clerk_debug: debugRequestState(requestState), - __clerkJSUrl: getEnvVariable('CLERK_JS'), - __clerkJSVersion: getEnvVariable('CLERK_JS_VERSION'), - __telemetryDisabled: isTruthy(getEnvVariable('CLERK_TELEMETRY_DISABLED')), - __telemetryDebug: isTruthy(getEnvVariable('CLERK_TELEMETRY_DEBUG')), + __clerkJSUrl: getEnvVariable('CLERK_JS', event.context), + __clerkJSVersion: getEnvVariable('CLERK_JS_VERSION', event.context), + __telemetryDisabled: isTruthy(getEnvVariable('CLERK_TELEMETRY_DISABLED', event.context)), + __telemetryDebug: isTruthy(getEnvVariable('CLERK_TELEMETRY_DEBUG', event.context)), __signInForceRedirectUrl: - additionalStateOptions.signInForceRedirectUrl || getEnvVariable('CLERK_SIGN_IN_FORCE_REDIRECT_URL') || '', + additionalStateOptions.signInForceRedirectUrl || + getEnvVariable('CLERK_SIGN_IN_FORCE_REDIRECT_URL', event.context) || + '', __signUpForceRedirectUrl: - additionalStateOptions.signUpForceRedirectUrl || getEnvVariable('CLERK_SIGN_UP_FORCE_REDIRECT_URL') || '', + additionalStateOptions.signUpForceRedirectUrl || + getEnvVariable('CLERK_SIGN_UP_FORCE_REDIRECT_URL', event.context) || + '', __signInFallbackRedirectUrl: - additionalStateOptions.signInFallbackRedirectUrl || getEnvVariable('CLERK_SIGN_IN_FALLBACK_REDIRECT_URL') || '', + additionalStateOptions.signInFallbackRedirectUrl || + getEnvVariable('CLERK_SIGN_IN_FALLBACK_REDIRECT_URL', event.context) || + '', __signUpFallbackRedirectUrl: - additionalStateOptions.signUpFallbackRedirectUrl || getEnvVariable('CLERK_SIGN_UP_FALLBACK_REDIRECT_URL') || '', + additionalStateOptions.signUpFallbackRedirectUrl || + getEnvVariable('CLERK_SIGN_UP_FALLBACK_REDIRECT_URL', event.context) || + '', }); return { diff --git a/packages/tanstack-start/src/utils/env.ts b/packages/tanstack-start/src/utils/env.ts index c0e677e2ef6..039a16c99ca 100644 --- a/packages/tanstack-start/src/utils/env.ts +++ b/packages/tanstack-start/src/utils/env.ts @@ -1,40 +1,10 @@ +import { getEnvVariable } from '@clerk/shared/getEnvVariable'; import { isTruthy } from '@clerk/shared/underscore'; -import type { HTTPEvent } from 'vinxi/http'; +import type { H3EventContext } from 'vinxi/http'; -/** - * - * Utility function to get env variables. - * - * @param name env variable name - * @param defaultVaue default value to return if the env variable is not set - * @param event - H3Event object for accessing runtime environment variables - * @returns string - * - * @internal - */ -export const getEnvVariable = (name: string, defaultValue: string = '', event?: HTTPEvent) => { - // Cloudflare context check - const cfValue = event?.context?.cloudflare?.env[name]; - if (cfValue) { - return cfValue; - } - - // Node envs - if (typeof process !== 'undefined' && process.env && typeof process.env[name] === 'string') { - return process.env[name]; - } - - // Vite specific envs - if (typeof import.meta !== 'undefined' && import.meta.env && typeof import.meta.env[name] === 'string') { - return import.meta.env[name]; - } - - return defaultValue; -}; - -export const getPublicEnvVariables = (event?: HTTPEvent) => { +export const getPublicEnvVariables = (context?: H3EventContext) => { const getValue = (name: string): string => { - return getEnvVariable(`VITE_${name}`, '', event) || getEnvVariable(name, '', event); + return getEnvVariable(`VITE_${name}`, context) || getEnvVariable(name, context); }; return {