import type { NextConfig } from "next"; import createNextIntlPlugin from "next-intl/plugin"; import { withSentryConfig } from "@sentry/nextjs"; const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); function getSupabaseHostname(): string { const url = process.env.NEXT_PUBLIC_SUPABASE_URL; if (url) { try { return new URL(url).hostname; } catch { /* fall through */ } } return "*.supabase.co"; } // Drafted but NOT wired into headers() yet — enabling it blanked the entire // app (ClerkProvider wraps the whole tree in layout.tsx; a blocked resource // during its client-side init throws, and nothing catches it, so the app // crashes to a blank page instead of just breaking the login widget). // Confirmed live on 2026-08-10 that disabling this CSP fixes it; adding // wss://clerk.pawfeed.org to connect-src did NOT fix it, so the real blocked // directive is still unidentified. Next attempt should ship as // Content-Security-Policy-Report-Only first (with a /api/csp-report endpoint) // to see actual violation reports before enforcing on production again. function buildCsp(): string { const supabase = `https://${getSupabaseHostname()}`; const directives: Record = { "default-src": ["'self'"], "script-src": ["'self'", "https://clerk.pawfeed.org"], // Radix/shadcn primitives set inline style="" attributes for positioning — // no nonce mechanism covers style attributes, so 'unsafe-inline' is required here. "style-src": ["'self'", "'unsafe-inline'"], "img-src": ["'self'", "data:", supabase, "https://image.mux.com", "https://img.clerk.com"], "font-src": ["'self'", "data:"], "media-src": ["'self'", "blob:", "https://stream.mux.com"], "worker-src": ["'self'", "blob:"], "connect-src": [ "'self'", "https://clerk.pawfeed.org", "wss://clerk.pawfeed.org", "https://accounts.pawfeed.org", supabase, "https://stream.mux.com", "https://image.mux.com", "https://litix.io", "https://storage.googleapis.com", ], "frame-src": ["'self'", "https://clerk.pawfeed.org"], "frame-ancestors": ["'none'"], "object-src": ["'none'"], "base-uri": ["'self'"], }; return Object.entries(directives) .map(([key, values]) => `${key} ${values.join(" ")}`) .join("; "); } const nextConfig: NextConfig = { output: "standalone", allowedDevOrigins: ["192.168.1.92"], images: { remotePatterns: [ { protocol: "https", hostname: getSupabaseHostname() }, ], }, async headers() { return [ { source: "/(.*)", headers: [ { key: "X-Content-Type-Options", value: "nosniff" }, { key: "X-Frame-Options", value: "DENY" }, { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" }, { key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" }, ], }, ]; }, }; export default withSentryConfig(withNextIntl(nextConfig), { org: process.env.SENTRY_ORG, project: process.env.SENTRY_PROJECT, authToken: process.env.SENTRY_AUTH_TOKEN, silent: true, // No auth token configured on this self-hosted deploy yet — skip // source-map upload rather than failing the Docker build. sourcemaps: { disable: !process.env.SENTRY_AUTH_TOKEN }, disableLogger: true, });