Trade the gateway token for an edge JWT on every connection
Windows discovery tool / windows (push) Successful in 1m24s

The proxy put its configured gateway_token straight into the upgrade's
Authorization header, but the backend only accepts a one-hour edge JWT
(scope "edge") there, minted by POST /auth/edge-token from the farm's edge
proxy token. Nothing in the proxy ever made that call, so a gateway
configured with the token the console hands out could not connect at all,
and one given a hand-minted JWT (which is what every test did) would have
been locked out an hour later on its first reconnect.

Each connection attempt now starts by trading gateway_token for a fresh JWT,
at an address derived from the socket URL so there is one to configure. A
401 is reported as what it means (token rotated or wrong), since retrying
will not fix it. Config's Debug no longer prints the token.

tests/uplink_late_result.rs now plays the cloud's exchange endpoint and
asserts the socket opens with the minted JWT, not the configured token.
.env.example and continuum.example.toml say what the token is.
This commit is contained in:
2026-10-29 15:10:27 +00:00
parent 5fc09aef04
commit b4948076a0
6 changed files with 184 additions and 7 deletions
+6 -1
View File
@@ -26,6 +26,7 @@
//! readings for no consumer. See PLAN.md for what would actually be worth
//! buffering.
mod auth;
mod download;
mod webrtc;
pub mod protocol;
@@ -176,6 +177,10 @@ pub async fn run(
/// handshake from a connection that opened and later dropped; they want
/// different retry behaviour.
async fn connect(config: &Config) -> anyhow::Result<WsStream> {
// A fresh edge JWT per attempt: it lives an hour, so one fetched at
// startup would lock a long-running gateway out on its first reconnect.
let edge_token = auth::fetch_edge_token(config).await?;
// The edge token goes on the upgrade request, not in the first message.
// The backend verifies it before the socket exists, so a bad token is an
// HTTP 401 here rather than a connection that opens and is then closed -
@@ -184,7 +189,7 @@ async fn connect(config: &Config) -> anyhow::Result<WsStream> {
// those tools already know to redact.
let request = Request::builder()
.uri(&config.uplink_url)
.header("Authorization", format!("Bearer {}", config.gateway_token))
.header("Authorization", format!("Bearer {edge_token}"))
// tokio-tungstenite fills these in for a plain URL, but not once we
// hand it a request we built ourselves - and a handshake missing any
// of them is rejected by the server rather than ignored.