Fix
OAuth callbacks now build the public URL when Tesserae runs behind a reverse proxy.
When Tesserae sits behind NGINX Proxy Manager, Caddy, Cloudflare Tunnel, or any other reverse proxy, the proxy terminates TLS and forwards plaintext HTTP to Tesserae on its internal port (default 8765). Without ProxyFix wired into the WSGI stack, Flask's `url_for(..., _external=True)` built external URLs from that internal HTTP connection. Plugin OAuth flows (e.g. Spotify Core) generated redirect URIs like `http://internal-host/plugins/spotify_core/callback\` instead of the real public `https://tesserae.example.org:8443/plugins/spotify_core/callback\` the browser saw, and the OAuth provider rejected the mismatch.
Fix: `werkzeug.middleware.proxy_fix.ProxyFix` wraps the WSGI stack, trusting one proxy hop by default. `X-Forwarded-Proto` / `X-Forwarded-Host` / `X-Forwarded-Port` from the upstream proxy now reach Flask, so external URLs match what the browser saw.
Configuration
- Default: one hop trusted, works for the standard "one reverse proxy in front of Tesserae" topology (HA add-on behind NPM, Docker behind Caddy, etc.).
- `TESSERAE_FORWARDED_HOPS=2` (env var): trust two hops, e.g. Cloudflare Tunnel → NPM → Tesserae.
- `TESSERAE_FORWARDED_HOPS=0` (env var): disable ProxyFix entirely, for bare-metal installs where no proxy is in front and a malicious client could otherwise spoof `X-Forwarded-*` headers.
Upgrade notes
If you were already behind a reverse proxy and your OAuth Connect buttons were failing with "redirect URI mismatch" errors, this release fixes that without any config change on your end (assuming your reverse proxy sets the standard headers, which NGINX Proxy Manager does by default with "Websockets Support" enabled).
If your reverse proxy needs explicit header forwarding configuration (uncommon), make sure these are set on the upstream:
```nginx
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
```
Tests
+3 regression tests in `tests/test_proxyfix.py` covering scheme promotion (HTTP→HTTPS), host:port preservation on non-standard ports, and the no-headers fallback path for bare-metal installs. 975 passing, ruff + mypy strict clean.