# docker/nginx.conf
# ---------------------------------------------------------------------------
# nginx server block for the Wisp static web app (used by docker/web.Dockerfile).
#
# Responsibilities:
#   1. Serve the Expo/Metro static SPA, falling back to /index.html for client
#      routes (expo-router uses HTML5 history routing).
#   2. Emit the cross-origin isolation headers required for multi-threaded WASM
#      (SharedArrayBuffer). Without these, `crossOriginIsolated` is false in the
#      browser and the threaded Whisper WASM backend cannot start.
#   3. gzip responses and cache immutable hashed assets aggressively.
#   4. Serve the signed Android APK at /wisp.apk when one is present.
#
# This file is copied to /etc/nginx/conf.d/default.conf, replacing the stock
# nginx site, so it is a `server { }` block (the surrounding http{}/events{}
# come from the base image's /etc/nginx/nginx.conf).
# ---------------------------------------------------------------------------

# Map the request to a Cache-Control value: hashed build assets get a 1-year
# immutable cache; everything else (HTML, the service worker, the APK) is
# revalidated so deploys take effect immediately.
map $uri $wisp_cache_control {
    default                         "no-cache";
    # Expo emits content-hashed files under /_expo/ and /assets/. These are safe
    # to cache forever because the hash changes whenever the content changes.
    ~*^/_expo/                      "public, max-age=31536000, immutable";
    ~*^/assets/                     "public, max-age=31536000, immutable";
    ~*\.(?:js|css|woff2?|ttf|otf|png|jpg|jpeg|gif|svg|webp|wasm)$ "public, max-age=31536000, immutable";
}

server {
    # Non-privileged port; docker-compose maps/exposes 8080 and the host's
    # reverse proxy forwards the wisp host/path here.
    listen      8080;
    listen      [::]:8080;
    server_name _;

    root        /usr/share/nginx/html;
    index       index.html;

    # ---- Compression --------------------------------------------------------
    gzip              on;
    gzip_vary         on;            # vary on Accept-Encoding so proxies cache correctly
    gzip_comp_level   6;
    gzip_min_length   1024;          # don't bother compressing tiny responses
    gzip_proxied      any;
    gzip_types
        text/plain
        text/css
        text/javascript
        application/javascript
        application/json
        application/wasm
        image/svg+xml
        font/ttf
        font/otf;
    # NOTE: nginx cannot brotli-compress without the (non-default) ngx_brotli
    # module. The large .wasm model-runtime files benefit most from brotli; if
    # you need it, switch to an nginx image that bundles ngx_brotli and add
    # `brotli on; brotli_static on;` here.

    # ---- Cross-origin isolation (REQUIRED for threaded WASM) ----------------
    # These two headers make the document "cross-origin isolated", which is what
    # unlocks SharedArrayBuffer and therefore multi-threaded WASM. They are set
    # on EVERY response (`always`) so they apply even to error responses.
    #
    #   COOP same-origin     -> process-isolate this page from cross-origin openers
    #   COEP require-corp    -> every subresource must explicitly opt in (via CORP
    #                           or CORS) to being embedded here
    add_header Cross-Origin-Opener-Policy   "same-origin"  always;
    add_header Cross-Origin-Embedder-Policy "require-corp" always;
    # Our own assets are same-origin; declare CORP so they remain loadable under
    # COEP and can also be used by other isolated origins if ever needed.
    add_header Cross-Origin-Resource-Policy "same-origin"  always;

    # ----------------------------------------------------------------------
    # IMPORTANT — Hugging Face model weights & CORS/CORP interaction
    # ----------------------------------------------------------------------
    # The app downloads Whisper model weights at runtime from the Hugging Face
    # Hub (a cross-origin host). Under `Cross-Origin-Embedder-Policy: require-corp`
    # every cross-origin subresource must be served with either:
    #     Cross-Origin-Resource-Policy: cross-origin    (a CORP header), or
    #     valid CORS headers AND be fetched with crossorigin/CORS mode.
    #
    # The HF Hub / its CDN generally DO send permissive CORS (Access-Control-
    # Allow-Origin) headers, so CORS-mode fetches usually work under require-corp.
    # However, if HF (or a future CDN) ever omits CORP/CORS for a given asset,
    # require-corp will BLOCK the download and the model load will fail.
    #
    # The robust fallback is COEP "credentialless": it keeps the page cross-origin
    # isolated (SharedArrayBuffer stays available) but lets no-CORS cross-origin
    # resources load by sending them WITHOUT credentials, removing the hard CORP
    # requirement. To switch, comment out the `require-corp` line above and
    # enable the line below instead:
    #
    #     add_header Cross-Origin-Embedder-Policy "credentialless" always;
    #
    # (Browser support: credentialless is supported in modern Chromium/Firefox;
    #  Safari only supports require-corp, so require-corp is the safer default.)

    # ---- SPA routing --------------------------------------------------------
    location / {
        # Serve the file if it exists, else a matching directory, else fall back
        # to index.html so client-side (expo-router) routes resolve. This is the
        # canonical SPA fallback.
        try_files $uri $uri/ /index.html;

        # Apply the computed cache policy (see the map{} above). The header is
        # re-asserted here because `add_header` does not inherit into locations
        # once a location defines its own add_header directives.
        add_header Cache-Control                $wisp_cache_control;
        # Re-assert the isolation headers inside this location (add_header in an
        # outer scope is dropped as soon as a location sets ANY add_header).
        add_header Cross-Origin-Opener-Policy   "same-origin"  always;
        add_header Cross-Origin-Embedder-Policy "require-corp" always;
        add_header Cross-Origin-Resource-Policy "same-origin"  always;
    }

    # ---- Android APK download ----------------------------------------------
    # The CI build-apk job scps the signed APK to the server, where it is mounted
    # into the web root as /usr/share/nginx/html/wisp.apk (see docker-compose.yml).
    # If no APK is present yet, this 404s cleanly rather than falling through to
    # the SPA index.html (which would download an HTML file named wisp.apk).
    location = /wisp.apk {
        # Don't cache the APK aggressively so a freshly deployed build is served.
        add_header Cache-Control "no-cache" always;
        # CORP must still be present on this response under COEP.
        add_header Cross-Origin-Resource-Policy "same-origin" always;
        # Force a download with a sensible filename and correct MIME type.
        types { }                                  # clear inherited type map
        default_type application/vnd.android.package-archive;
        add_header Content-Disposition 'attachment; filename="wisp.apk"' always;
        try_files /wisp.apk =404;
    }

    # Health check endpoint for the reverse proxy / container orchestrator.
    location = /healthz {
        access_log off;
        add_header Content-Type text/plain;
        return 200 "ok\n";
    }
}
