dd28f2ac9c
Generalises #37747. The WS Origin guard (_ws_host_origin_is_allowed) only trusted the packaged Electron app's non-web origin (file:// / null / app://) when the bind was NOT OAuth-gated. The packaged Hermes Desktop renderer loads over file://, so when it drives a remote OAuth-gated gateway its /api/ws upgrade was rejected with HTTP 403 even though _ws_auth_ok had already validated the single-use ?ticket= one line earlier. This guard runs only AFTER _ws_auth_ok has accepted the WS credential, which is the real auth boundary in every mode: * loopback bind -> legacy dashboard session token * non-loopback --insecure -> legacy session token (Tailscale / LAN, #37747) * OAuth-gated public bind -> single-use, 30s-TTL, identity-bound ?ticket= A non-web origin can only come from a native client; a DNS-rebinding attack always arrives from an http(s) origin and is still match-checked against the bound host. So once the upstream credential check has passed, the Origin guard adds nothing for a non-web origin. Collapsed the loopback/non-gated special cases to 'return True' for non-web origins. http(s) origins keep the strict same-host check, so browser DNS-rebinding defence is unchanged. Tests: gated file:///null/app:// now asserted ALLOWED; cross-site http(s) still rejected on gated and loopback binds; #37747's loopback and non-loopback-insecure cases retained. 37/37 test_dashboard_auth_ws_auth + test_web_server_host_header pass.