server { listen 80; listen [::]:80; server_name chat.lotusguild.org; # Brotli compression (better than gzip for modern browsers) brotli on; brotli_static on; brotli_comp_level 6; brotli_types text/plain text/css application/javascript application/json image/svg+xml application/wasm font/woff2; root /var/www/html; server_tokens off; client_max_body_size 50m; limit_req zone=chat_limit burst=60 nodelay; limit_conn chat_conn 25; index index.html; # Security headers (incl. CSP) — see the snippet include snippets/cinny-security-headers.conf; # HSTS: TLS terminates upstream (this server is listen 80), so this reaches # the browser only if the front proxy passes upstream response headers # through; otherwise set it at the TLS terminator. includeSubDomains covers # all *.lotusguild.org (all HTTPS); `preload` is inert until submitted to # hstspreload.org. # Permissions-Policy: allow only what the app uses (self) — calls # (camera/microphone/display-capture), location share (geolocation), sounds # (autoplay), Element Call (fullscreen/encrypted-media) — and deny the rest. # Block all source map files and dotfiles from public access location ~* \.(js|css)\.map$ { deny all; return 404; } location ~ /\. { deny all; return 404; } location = /netlify.toml { deny all; return 404; } # Service worker must never be cached so updates are picked up immediately location = /sw.js { include snippets/cinny-security-headers.conf; expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate" always; } # Bundled Element Call: framed by the app itself, so it must not carry the # app CSP (frame-ancestors 'none' blocked every web call). Same caching as # below: HTML never cached, hashed assets for a year. location ^~ /public/element-call/ { include snippets/cinny-security-headers-framed.conf; location ~* \.html$ { include snippets/cinny-security-headers-framed.conf; expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate" always; } location ~* \.(?:js|css|woff2?|png|svg|ico|webp|wasm)$ { include snippets/cinny-security-headers-framed.conf; expires 1y; add_header Cache-Control "public, immutable" always; } } # Cache content-addressed static assets aggressively location ~* \.(?:js|css|woff2?|png|svg|ico|webp)$ { include snippets/cinny-security-headers.conf; expires 1y; add_header Cache-Control "public, immutable" always; } # Never cache HTML or JSON (index.html, config.json, manifest.json) location ~* \.(json|html)$ { include snippets/cinny-security-headers.conf; expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate" always; } # [Gitea #155] PWA share target. The service worker normally answers this # POST itself; if it isn't controlling the page yet, land on /share # (the shared files are lost, but nothing 405s). location = /share-target { absolute_redirect off; return 303 /share; } # Auto-deploy webhook — proxied to local webhook service location = /hooks/lotus-deploy { proxy_pass http://127.0.0.1:9001/hooks/lotus-deploy; proxy_set_header Host $host; proxy_read_timeout 300; proxy_connect_timeout 5; } location / { rewrite ^/config\.json$ /config.json break; rewrite ^/manifest\.json$ /manifest.json break; rewrite ^/sw\.js$ /sw.js break; rewrite ^/pdf\.worker\.min\.js$ /pdf.worker.min.js break; rewrite ^/public/(.*)$ /public/$1 break; rewrite ^/assets/(.*)$ /assets/$1 break; rewrite ^(.+)$ /index.html break; } }