@clerk/shared
Version:
Internal package utils used by the Clerk SDKs
218 lines (216 loc) • 9.55 kB
JavaScript
import { DEV_OR_STAGING_SUFFIXES, LEGACY_DEV_INSTANCE_SUFFIXES } from "./constants.mjs";
import { isomorphicAtob } from "./isomorphicAtob.mjs";
import { isomorphicBtoa } from "./isomorphicBtoa.mjs";
//#region src/keys.ts
/** Prefix used for production publishable keys */
const PUBLISHABLE_KEY_LIVE_PREFIX = "pk_live_";
/** Prefix used for development publishable keys */
const PUBLISHABLE_KEY_TEST_PREFIX = "pk_test_";
/**
* Regular expression that matches development frontend API keys.
* Matches patterns like: foo-bar-13.clerk.accounts.dev.
*/
const PUBLISHABLE_FRONTEND_API_DEV_REGEX = /^(([a-z]+)-){2}([0-9]{1,2})\.clerk\.accounts([a-z.]*)(dev|com)$/i;
/**
* Converts a frontend API URL into an unpadded base64-encoded publishable key.
*
* @param frontendApi - The frontend API URL (e.g., 'clerk.example.com').
* @returns An unpadded base64-encoded publishable key with appropriate prefix (pk_live_ or pk_test_).
*/
function buildPublishableKey(frontendApi) {
return `${PUBLISHABLE_FRONTEND_API_DEV_REGEX.test(frontendApi) || frontendApi.startsWith("clerk.") && LEGACY_DEV_INSTANCE_SUFFIXES.some((s) => frontendApi.endsWith(s)) ? PUBLISHABLE_KEY_TEST_PREFIX : PUBLISHABLE_KEY_LIVE_PREFIX}${isomorphicBtoa(`${frontendApi}$`).replace(/=+$/, "")}`;
}
/**
* Derives a publishable key from the current hostname. Intended for multi-domain
* setups (e.g. custom domains on top of a default domain) where the correct key
* must be resolved per request.
*
* Pass the configured publishable key as `fallbackKey` so that development
* instances (pk_test_) are returned as-is instead of being incorrectly derived
* from the host (e.g. localhost).
*
* @example
* // React (use window.location.hostname, not window.location.host, to avoid including the port)
* <ClerkProvider publishableKey={publishableKeyFromHost(window.location.hostname, import.meta.env.VITE_CLERK_PUBLISHABLE_KEY)}>
*
* @example
* // Express (inside clerkMiddleware callback)
* // Validate req.hostname against a known allowlist before passing it in.
* // When `trust proxy` is enabled, req.hostname reads from X-Forwarded-Host
* // and can be spoofed if your proxy is not properly configured.
* const ALLOWED_HOSTS = ['domain-a.com', 'domain-b.com'];
* clerkMiddleware((req) => {
* if (!ALLOWED_HOSTS.includes(req.hostname)) throw new Error('Unknown host');
* return { publishableKey: publishableKeyFromHost(req.hostname, process.env.CLERK_PUBLISHABLE_KEY) };
* })
*/
function publishableKeyFromHost(host, fallbackKey) {
if (fallbackKey && isDevelopmentFromPublishableKey(fallbackKey)) return fallbackKey;
const hostname = host.toLowerCase().replace(/:\d+$/, "");
if (!hostname) throw new Error("Host must not be empty.");
return buildPublishableKey(`clerk.${hostname}`);
}
/**
* Validates that a decoded publishable key has the correct format.
* The decoded value should be a frontend API followed by exactly one '$' at the end.
*
* @param decoded - The decoded publishable key string to validate.
* @returns `true` if the decoded key has valid format, `false` otherwise.
*/
function isValidDecodedPublishableKey(decoded) {
if (!decoded.endsWith("$")) return false;
const withoutTrailing = decoded.slice(0, -1);
if (withoutTrailing.includes("$")) return false;
return withoutTrailing.includes(".");
}
/**
* Parses and validates a publishable key, extracting the frontend API and instance type.
*
* @param key - The publishable key to parse.
* @param options - Configuration options for parsing.
* @param options.fatal
* @param options.domain
* @param options.proxyUrl
* @param options.isSatellite
* @returns Parsed publishable key object with instanceType and frontendApi, or null if invalid.
*
* @throws {Error} When options.fatal is true and key is missing or invalid.
*/
function parsePublishableKey(key, options = {}) {
key = key || "";
if (!key || !isPublishableKey(key)) {
if (options.fatal && !key) throw new Error("Publishable key is missing. Ensure that your publishable key is correctly configured. Double-check your environment configuration for your keys, or access them here: https://dashboard.clerk.com/last-active?path=api-keys");
if (options.fatal && !isPublishableKey(key)) throw new Error("Publishable key not valid.");
return null;
}
const instanceType = key.startsWith(PUBLISHABLE_KEY_LIVE_PREFIX) ? "production" : "development";
let decodedFrontendApi;
try {
decodedFrontendApi = isomorphicAtob(key.split("_")[2]);
} catch {
if (options.fatal) throw new Error("Publishable key not valid: Failed to decode key.");
return null;
}
if (!isValidDecodedPublishableKey(decodedFrontendApi)) {
if (options.fatal) throw new Error("Publishable key not valid: Decoded key has invalid format.");
return null;
}
let frontendApi = decodedFrontendApi.slice(0, -1);
if (options.proxyUrl) frontendApi = options.proxyUrl;
else if (instanceType !== "development" && options.domain && options.isSatellite) frontendApi = `clerk.${options.domain}`;
return {
instanceType,
frontendApi
};
}
/**
* Checks if the provided key is a valid publishable key.
*
* @param key - The key to be checked. Defaults to an empty string if not provided.
* @returns `true` if 'key' is a valid publishable key, `false` otherwise.
*/
function isPublishableKey(key = "") {
try {
if (!(key.startsWith(PUBLISHABLE_KEY_LIVE_PREFIX) || key.startsWith(PUBLISHABLE_KEY_TEST_PREFIX))) return false;
const parts = key.split("_");
if (parts.length !== 3) return false;
const encodedPart = parts[2];
if (!encodedPart) return false;
return isValidDecodedPublishableKey(isomorphicAtob(encodedPart));
} catch {
return false;
}
}
/**
* Creates a memoized cache for checking if URLs are development or staging environments.
* Uses a Map to cache results for better performance on repeated checks.
*
* @returns An object with an isDevOrStagingUrl method that checks if a URL is dev/staging.
*/
function createDevOrStagingUrlCache() {
const devOrStagingUrlCache = /* @__PURE__ */ new Map();
return {
/**
* Checks if a URL is a development or staging environment.
*
* @param url - The URL to check (string or URL object).
* @returns `true` if the URL is a development or staging environment, `false` otherwise.
*/
isDevOrStagingUrl: (url) => {
if (!url) return false;
const hostname = typeof url === "string" ? url : url.hostname;
let res = devOrStagingUrlCache.get(hostname);
if (res === void 0) {
res = DEV_OR_STAGING_SUFFIXES.some((s) => hostname.endsWith(s));
devOrStagingUrlCache.set(hostname, res);
}
return res;
} };
}
/**
* Checks if a publishable key is for a development environment.
* Supports both legacy format (test_) and new format (pk_test_).
*
* @param apiKey - The API key to check.
* @returns `true` if the key is for development, `false` otherwise.
*/
function isDevelopmentFromPublishableKey(apiKey) {
return apiKey.startsWith("test_") || apiKey.startsWith("pk_test_");
}
/**
* Checks if a publishable key is for a production environment.
* Supports both legacy format (live_) and new format (pk_live_).
*
* @param apiKey - The API key to check.
* @returns `true` if the key is for production, `false` otherwise.
*/
function isProductionFromPublishableKey(apiKey) {
return apiKey.startsWith("live_") || apiKey.startsWith("pk_live_");
}
/**
* Checks if a secret key is for a development environment.
* Supports both legacy format (test_) and new format (sk_test_).
*
* @param apiKey - The secret key to check.
* @returns `true` if the key is for development, `false` otherwise.
*/
function isDevelopmentFromSecretKey(apiKey) {
return apiKey.startsWith("test_") || apiKey.startsWith("sk_test_");
}
/**
* Checks if a secret key is for a production environment.
* Supports both legacy format (live_) and new format (sk_live_).
*
* @param apiKey - The secret key to check.
* @returns `true` if the key is for production, `false` otherwise.
*/
function isProductionFromSecretKey(apiKey) {
return apiKey.startsWith("live_") || apiKey.startsWith("sk_live_");
}
/**
* Generates a unique cookie suffix based on the publishable key using SHA-1 hashing.
* The suffix is base64-encoded and URL-safe (+ and / characters are replaced).
*
* @param publishableKey - The publishable key to generate suffix from.
* @param subtle - The SubtleCrypto interface to use for hashing (defaults to globalThis.crypto.subtle).
* @returns A promise that resolves to an 8-character URL-safe base64 string.
*/
async function getCookieSuffix(publishableKey, subtle = globalThis.crypto.subtle) {
const data = new TextEncoder().encode(publishableKey);
const digest = await subtle.digest("sha-1", data);
return isomorphicBtoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/gi, "-").replace(/\//gi, "_").substring(0, 8);
}
/**
* Creates a suffixed cookie name by appending the cookie suffix to the base name.
* Used to create unique cookie names based on the publishable key.
*
* @param cookieName - The base cookie name.
* @param cookieSuffix - The suffix to append (typically generated by getCookieSuffix).
* @returns The suffixed cookie name in format: `${cookieName}_${cookieSuffix}`.
*/
const getSuffixedCookieName = (cookieName, cookieSuffix) => {
return `${cookieName}_${cookieSuffix}`;
};
//#endregion
export { buildPublishableKey, createDevOrStagingUrlCache, getCookieSuffix, getSuffixedCookieName, isDevelopmentFromPublishableKey, isDevelopmentFromSecretKey, isProductionFromPublishableKey, isProductionFromSecretKey, isPublishableKey, parsePublishableKey, publishableKeyFromHost };
//# sourceMappingURL=keys.mjs.map