/* eslint-disable @next/next/no-server-import-in-page */ import {NextResponse} from 'next/server' const nonceCharset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' function generateNonce() { const array = new Uint8Array(16) crypto.getRandomValues(array) let nonce = '' for (const byte of array) { nonce += nonceCharset[byte % 64] } return nonce } function extractOrigin(url) { if (!url) { return '' } try { return new URL(url).origin } catch { return '' } } function extractHostSource(raw) { if (!raw) { return '' } const [hostname, port] = raw.split(':') if (!hostname) { return '' } if (hostname === 'localhost' || hostname === '127.0.0.1') { return `http://${hostname}${port ? `:${port}` : ''}` } return `https://${hostname}${port ? `:${port}` : ''}` } function buildCsp(nonce) { const apiUrl = process.env.NEXT_PUBLIC_API_URL || process.env.NEXT_PUBLIC_API_URL_ROOT || '' const plausibleUrl = process.env.NEXT_PUBLIC_PLAUSIBLE_URL || '' const mizikUrl = process.env.NEXT_PUBLIC_OKI_MIZIK_URL || '' const imageDomainsRaw = process.env.NEXT_PUBLIC_DOMAINS_IMAGE || '' const apiOrigin = extractOrigin(apiUrl) const plausibleOrigin = extractOrigin(plausibleUrl) const mizikOrigin = extractOrigin(mizikUrl) const imageOrigins = imageDomainsRaw .split(' ') .filter(Boolean) .map(entry => extractHostSource(entry)) .filter(Boolean) const imageOriginsSet = new Set([apiOrigin, ...imageOrigins]) const imageOriginsUnique = [...imageOriginsSet].filter(Boolean) const connectSrc = ['\'self\'', apiOrigin, plausibleOrigin, mizikOrigin].filter(Boolean) const imgSrc = ['\'self\'', 'data:', 'blob:', ...imageOriginsUnique].filter(Boolean) const mediaSrc = ['\'self\'', apiOrigin, mizikOrigin].filter(Boolean) // Frame-src is intentionally broad: the application embeds user-supplied // PeerTube instances in addition to known platforms (Tidal, Deezer, Spotify, // Soundcloud, Apple Music). Restricting this to a fixed list would break // user-generated content. const frameSrc = ['\'self\'', 'https:'] const directives = [ 'default-src \'self\'', `script-src 'nonce-${nonce}' 'strict-dynamic'`, `style-src-elem 'self' 'nonce-${nonce}'`, 'style-src-attr \'unsafe-inline\'', `img-src ${imgSrc.join(' ')}`, 'font-src \'self\'', `connect-src ${connectSrc.join(' ')}`, `frame-src ${frameSrc.join(' ')}`, `media-src ${mediaSrc.join(' ')}`, 'manifest-src \'self\'', 'object-src \'none\'', 'base-uri \'self\'', 'form-action \'self\'', 'frame-ancestors \'self\'', 'upgrade-insecure-requests' ] return directives.join('; ') } export function middleware(request) { const nonce = generateNonce() const requestHeaders = new Headers(request.headers) requestHeaders.set('x-nonce', nonce) const response = NextResponse.next({ request: { headers: requestHeaders } }) response.headers.set('Content-Security-Policy', buildCsp(nonce)) return response } export const config = { matcher: [ { source: '/((?!api|_next/static|_next/image|favicon.ico).*)', missing: [ {type: 'header', key: 'next-router-prefetch'}, {type: 'header', key: 'purpose', value: 'prefetch'} ] } ] }