# /etc/nginx/sites-available/hsc — written by hsc-setup.sh, do not edit here.
#
# Edit selfhost/templates/nginx-site.conf.tmpl and re-run `hsc-setup.sh install`.
# certbot rewrites the TLS block in place on first issue; the installer leaves
# its edits alone on later runs unless you pass --force-nginx.
#
# nginx owns TLS, authentication and rate limiting. The Bun service on
# 127.0.0.1:@PORT@ never sees an unauthenticated request. The key map lives in
# /etc/nginx/conf.d/hsc-keys.conf, also written by the installer.

# One bucket per site, not per IP: a site's key is its identity here. 1 MB holds
# roughly 16 000 of them. Ten audits a minute, with a burst of five, is far more
# than a cron job needs and far less than a loop can do damage with.
limit_req_zone $hsc_site zone=hsc:1m rate=10r/m;

# The public form needs a second bucket, and a different key. Behind one key
# stand thousands of strangers, so a per-key quota is the wrong unit: the first
# visitor with a loop would spend everyone else's audits and the form would
# answer 429 to people who did nothing wrong.
#
# An empty key disables a zone for that request, so this costs the named sites
# nothing — they keep their per-key quota and are not counted here at all.
#
# $binary_remote_addr is the visitor only because the Cloudflare snippet has
# already run set_real_ip_from/real_ip_header. Without it this would bucket
# every request under Cloudflare's edge address, which is one bucket for the
# whole internet.
map $hsc_site $hsc_visitor {
    default        "";
    "@PUBLIC_SITE@" $binary_remote_addr;
}
limit_req_zone $hsc_visitor zone=hsc_visitor:10m rate=3r/m;

server {
    listen 80;
    listen [::]:80;
    server_name @DOMAIN@;
@CF_INCLUDE@
    # Leave this open: certbot renews through it. Behind Cloudflare the
    # challenge arrives from Cloudflare, which the guard above allows.
    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name @DOMAIN@;
@CF_INCLUDE@
    # certbot --nginx replaces these four lines with the real certificate on
    # first issue. Until then nginx serves its default snakeoil pair, which is
    # enough to start and be reloaded.
    ssl_certificate     /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;

    access_log /var/log/nginx/hsc.access.log;
    error_log  /var/log/nginx/hsc.error.log;

    # Every error this block returns is JSON, like every answer the service
    # gives. A caller never has to guess which of the two spoke.
    default_type application/json;
    limit_req_status 429;

    # The largest legitimate body is a long `urls` list. 16 KB is generous.
    client_max_body_size 16k;

    # Uptime monitoring, no key. The service answers {"ok":true}.
    location = /healthz {
        proxy_pass http://127.0.0.1:@PORT@;
    }

    location = /audit {
        # `if` with nothing but a `return` is the one shape of `if` that is
        # safe inside a location. An unknown key maps to "" and stops here.
        if ($hsc_site = "") {
            return 401 '{"error":"unauthorized"}';
        }

        limit_req zone=hsc burst=5;
        # Three a minute per visitor, burst two. Enough to retype a domain after
        # a typo; not enough to walk a list of sites. Inert for every key but
        # the public one, whose $hsc_visitor is the only non-empty value.
        limit_req zone=hsc_visitor burst=2;

        proxy_pass http://127.0.0.1:@PORT@;
        proxy_http_version 1.1;

        # Set, never appended: whatever the client sent under this name is
        # overwritten. The service's only notion of who is calling comes from
        # this line.
        proxy_set_header X-HSC-Site $hsc_site;
        # The key itself has done its job and goes no further.
        proxy_set_header Authorization "";

        # Above the service's own kill, so a timeout is always the service's
        # 504 with its explanatory body, never nginx's.
        proxy_read_timeout @PROXY_TIMEOUT@;
        proxy_send_timeout @PROXY_TIMEOUT@;
    }

    location / {
        return 404 '{"error":"no such endpoint"}';
    }
}
