# nginx config for serving SecureBit.chat (static PWA) on Fly.io.
# Mirrors the behavior of the Apache .htaccess: correct JS MIME for ES modules
# (.jsx/.mjs), no-cache for the app shell / service worker / versioning files,
# long-immutable cache for hashed/static assets, security headers, SPA fallback.

worker_processes auto;
events { worker_connections 1024; }

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    # ES modules must be served as JavaScript. nginx's default mime.types maps
    # .js but not .mjs/.jsx — declare them explicitly (this overrides .js too).
    types {
        application/javascript  js mjs jsx;
        text/css                css;
        application/json        json map;
        application/manifest+json webmanifest;
        font/woff2              woff2;
        font/woff               woff;
        image/svg+xml           svg;
    }

    sendfile      on;
    tcp_nopush    on;
    server_tokens off;

    gzip            on;
    gzip_vary       on;
    gzip_min_length 256;
    gzip_types      text/plain text/css application/javascript application/json image/svg+xml font/woff2;

    # Decide Cache-Control from the request path. Keeping all add_header calls at
    # one level avoids nginx's header-inheritance reset between blocks.
    map $uri $sb_cache {
        default                  "public, max-age=31536000, immutable";
        ~^/index\.html$          "no-cache, no-store, must-revalidate";
        ~^/$                     "no-cache, no-store, must-revalidate";
        ~^/sw\.js$               "no-cache, no-store, must-revalidate";
        ~^/manifest\.json$       "no-cache, no-store, must-revalidate";
        ~^/meta\.json$           "no-cache, no-store, must-revalidate";
        ~^/dist/                 "no-cache, no-store, must-revalidate";
    }

    server {
        listen      8080;
        listen      [::]:8080;
        server_name _;
        root        /usr/share/nginx/html;
        index       index.html;

        # Security headers (frame-ancestors complements the in-page CSP meta tag).
        add_header X-Content-Type-Options   "nosniff"                          always;
        add_header Referrer-Policy          "strict-origin-when-cross-origin"  always;
        add_header X-Frame-Options          "DENY"                             always;
        add_header Content-Security-Policy   "frame-ancestors 'none';"         always;
        # Force HTTPS for two years and preload, closing the first-visit SSL-strip
        # window that upgrade-insecure-requests alone does not cover.
        add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
        # Lock down powerful features. Camera is allowed for in-page QR scanning;
        # microphone/geolocation and other sensors are denied outright.
        add_header Permissions-Policy        "camera=(self), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()" always;
        add_header Cache-Control            $sb_cache                          always;
        add_header Service-Worker-Allowed   "/"                                always;

        # Real asset files must return 404 when missing — never fall back to the
        # HTML shell, which would be served with the wrong content type and break
        # module/script loading (e.g. a missing config/ice-servers.js).
        location ~* \.(js|mjs|jsx|css|json|map|woff2?|ttf|otf|png|jpe?g|gif|webp|svg|ico|mp3|mp4|webm)$ {
            try_files $uri =404;
        }

        # SPA-style fallback so unknown navigation routes still load the app shell.
        location / {
            try_files $uri $uri/ /index.html;
        }
    }
}
