Compare commits
2 Commits
9c354543fc
...
dd0dd9c2dc
| Author | SHA1 | Date | |
|---|---|---|---|
| dd0dd9c2dc | |||
| 59988a597e |
100
next.config.ts
100
next.config.ts
@ -4,24 +4,92 @@ const s3Host = process.env.S3_ENDPOINT
|
|||||||
? new URL(process.env.S3_ENDPOINT).hostname
|
? new URL(process.env.S3_ENDPOINT).hostname
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
// Derive the Clerk frontend API host from the publishable key so the
|
||||||
|
// CSP allowlist always matches the active environment. Clerk supports
|
||||||
|
// two publishable key formats:
|
||||||
|
//
|
||||||
|
// 1) "Encoded" form (older): pk_test_<base64slug>$
|
||||||
|
// The base64 portion decodes to "<slug>.clerk.accounts.dev" (test)
|
||||||
|
// or "<slug>.clerk.services" (live). Trailing '$' is a separator.
|
||||||
|
//
|
||||||
|
// 2) "Readable" form (newer): pk_test_<slug>-<randomSuffix>
|
||||||
|
// -> <slug>.clerk.accounts.dev (test) or <slug>.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();
|
||||||
|
return m[1].toLowerCase() === "test"
|
||||||
|
? `${slug}.clerk.accounts.dev`
|
||||||
|
: `${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 = [
|
||||||
|
"default-src 'self'",
|
||||||
|
// Clerk user avatars are served from img.clerk.com; S3 hosts are
|
||||||
|
// also allowed for memorial uploads. data:/blob: for in-app previews.
|
||||||
|
"img-src 'self' data: blob: https://img.clerk.com https:",
|
||||||
|
"font-src 'self' data:",
|
||||||
|
"style-src 'self' 'unsafe-inline'",
|
||||||
|
// script-src: must include the Clerk FAPI host because Clerk JS is
|
||||||
|
// loaded from <fapiHost>/npm/@clerk/clerk-js@<v>/dist/clerk.browser.js
|
||||||
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'" +
|
||||||
|
(clerkFapiHost ? ` https://${clerkFapiHost}` : ""),
|
||||||
|
// connect-src: Clerk JS talks to <fapiHost> for all session calls.
|
||||||
|
"connect-src 'self' https://api.clerk.com" +
|
||||||
|
(clerkFapiHost ? ` https://${clerkFapiHost} wss://${clerkFapiHost}` : ""),
|
||||||
|
"frame-ancestors 'self'",
|
||||||
|
].join("; ");
|
||||||
|
|
||||||
const securityHeaders = [
|
const securityHeaders = [
|
||||||
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
|
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
|
||||||
{ key: "X-Frame-Options", value: "SAMEORIGIN" },
|
{ key: "X-Frame-Options", value: "SAMEORIGIN" },
|
||||||
{ key: "X-Content-Type-Options", value: "nosniff" },
|
{ key: "X-Content-Type-Options", value: "nosniff" },
|
||||||
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
||||||
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
|
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
|
||||||
{
|
{ key: "Content-Security-Policy", value: csp },
|
||||||
key: "Content-Security-Policy",
|
|
||||||
value: [
|
|
||||||
"default-src 'self'",
|
|
||||||
"img-src 'self' data: blob: https:",
|
|
||||||
"font-src 'self' data:",
|
|
||||||
"style-src 'self' 'unsafe-inline'",
|
|
||||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
|
||||||
"connect-src 'self' https://*.clerk.accounts.dev https://api.clerk.com",
|
|
||||||
"frame-ancestors 'self'",
|
|
||||||
].join("; "),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
@ -29,9 +97,10 @@ const nextConfig: NextConfig = {
|
|||||||
poweredByHeader: false,
|
poweredByHeader: false,
|
||||||
compress: true,
|
compress: true,
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: s3Host
|
remotePatterns: [
|
||||||
? [{ protocol: "https", hostname: s3Host }]
|
{ protocol: "https", hostname: "img.clerk.com" },
|
||||||
: [],
|
...(s3Host ? [{ protocol: "https", hostname: s3Host }] : []),
|
||||||
|
] as NonNullable<NonNullable<NextConfig["images"]>["remotePatterns"]>,
|
||||||
},
|
},
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
@ -44,3 +113,4 @@ const nextConfig: NextConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user