File size: 3,919 Bytes
cb6a2d8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | /** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
reactStrictMode: true,
swcMinify: true,
// better-sqlite3 is a native module — exclude from bundling.
experimental: {
serverComponentsExternalPackages: ['better-sqlite3'],
},
// Security + CORS headers
async headers() {
// Allow the Vercel frontend (and localhost for dev) to call our API.
const allowedOrigins = (process.env.ALLOWED_ORIGINS || '')
.split(',')
.map((o) => o.trim())
.filter(Boolean);
const corsHeaders = [
{
key: 'Access-Control-Allow-Methods',
value: 'GET, POST, PUT, DELETE, OPTIONS',
},
{
key: 'Access-Control-Allow-Headers',
value: 'Content-Type, Authorization',
},
{
key: 'Access-Control-Allow-Credentials',
value: 'true',
},
// If no ALLOWED_ORIGINS env, allow all (open API for dev/HF iframe).
{
key: 'Access-Control-Allow-Origin',
value: allowedOrigins[0] || '*',
},
];
return [
// CORS preflight for all /api/ routes
{
source: '/api/:path*',
headers: corsHeaders,
},
{
source: '/(.*)',
headers: [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
// Note: HF Spaces renders apps inside an iframe — SAMEORIGIN blocks it.
// Use CSP frame-ancestors instead (more flexible, same security).
// { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'X-XSS-Protection', value: '1; mode=block' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
// HSTS — only in production. Using includeSubDomains without
// `preload` so operators can opt into the HSTS preload list
// explicitly once they've confirmed every subdomain serves HTTPS.
// Gated on NODE_ENV so local `next dev` over http still works.
...(process.env.NODE_ENV === 'production'
? [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains',
},
]
: []),
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com data:",
"img-src 'self' https: data: blob:",
"connect-src 'self' https://router.huggingface.co https://api-inference.huggingface.co https://overpass-api.de https://nominatim.openstreetmap.org https://*.hf.space wss:",
"frame-src 'self' https://www.openstreetmap.org",
"frame-ancestors 'self' https://huggingface.co https://*.hf.space",
"media-src 'self' blob:",
"worker-src 'self' blob:",
].join('; '),
},
{
key: 'Permissions-Policy',
value: 'microphone=(self), camera=(self), geolocation=*',
},
],
},
{
source: '/sw.js',
headers: [
{ key: 'Cache-Control', value: 'no-cache, no-store, must-revalidate' },
{ key: 'Service-Worker-Allowed', value: '/' },
],
},
{
source: '/manifest.json',
headers: [
{ key: 'Content-Type', value: 'application/manifest+json' },
],
},
];
},
// Image optimization
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [
{ protocol: 'https', hostname: '*.huggingface.co' },
],
},
// Disable powered-by header
poweredByHeader: false,
// Compress responses
compress: true,
};
module.exports = nextConfig;
|