diff --git a/next.config.ts b/next.config.ts index ecc1933..dbc420a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -5,15 +5,43 @@ const s3Host = process.env.S3_ENDPOINT : ""; // Derive the Clerk frontend API host from the publishable key so the -// CSP allowlist always matches the active environment. -// Clerk key format is: -// pk_test_- -> .clerk.accounts.dev -// pk_live_- -> .clerk.services -// The slug itself may contain hyphens and digits, so the suffix is -// only the final dash-group. The regex greedily captures the slug. +// CSP allowlist always matches the active environment. Clerk supports +// two publishable key formats: +// +// 1) "Encoded" form (older): pk_test_$ +// The base64 portion decodes to ".clerk.accounts.dev" (test) +// or ".clerk.services" (live). Trailing '$' is a separator. +// +// 2) "Readable" form (newer): pk_test_- +// -> .clerk.accounts.dev (test) or .clerk.services. +// The slug may itself contain hyphens and digits, so only the +// final dash-group is captured as the suffix. function clerkFrontendApiHost(): string | null { const key = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; if (!key) return null; + + // Form 1: base64-encoded FAPI URL. Match everything between the + // 'pk_test_'/'pk_live_' prefix and an optional trailing '$'. + const enc = key.match(/^pk_(test|live)_([A-Za-z0-9+/=_-]+)\$?$/); + if (enc) { + const b64 = enc[2].replace(/-/g, "+").replace(/_/g, "/"); + if (/^[A-Za-z0-9+/=]+$/.test(b64)) { + try { + const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4); + const decoded = Buffer.from(padded, "base64").toString("utf8"); + // If the decoded string doesn't look like a Clerk FAPI host + // (e.g. it's garbage from decoding a non-base64 readable-form + // key), fall through to form 2 rather than returning null. + const fapi = decodeFapiHost(decoded); + if (fapi) return fapi; + } catch { + // fall through to form 2 + } + } + // fall through to form 2 if the slug is non-base64 (e.g. readable form) + } + + // Form 2: readable slug + random suffix. const m = key.match(/^pk_(test|live)_(.+?)-([a-z0-9]+)$/i); if (!m) return null; const slug = m[2].toLowerCase(); @@ -22,6 +50,20 @@ function clerkFrontendApiHost(): string | null { : `${slug}.clerk.services`; } +function decodeFapiHost(decoded: string): string | null { + // The decoded string is the FAPI host (e.g. + // 'useful-louse-74.clerk.accounts.dev$'). Note: the encoded base64 + // payload always carries the literal FAPI host regardless of test vs + // live mode — both `pk_test_...` and `pk_live_...` can decode to an + // '.accounts.dev' host when the deployment is on the test endpoint. + // We accept either well-known Clerk FAPI host pattern. + const host = decoded.trim().replace(/\$$/, "").trim().toLowerCase(); + if (host.endsWith(".clerk.accounts.dev") || host.endsWith(".clerk.services")) { + return host; + } + return null; +} + const clerkFapiHost = clerkFrontendApiHost(); const csp = [