Dev mode: sign the web client in so live mode reaches it
A dev run's web client showed no live tracks while the .exe beside it did. Root cause: the local API relays a request to the DHP only when it carries a token that API minted itself (main.cpp's forwarder, the guard that keeps it from being an open proxy). The .exe mints one with ODIAN_MINT_SECRET; the web client was opened with ?user= only, so every /session/* call it made was served from this PC's database. The launcher now mints a token from this stack's API for the run's operator (stack::mint_web_token) and opens the browser with it in the URL FRAGMENT (StackConfig::web_url_signed). A fragment never reaches a request line or a server log, and the launcher's own log still prints the tokenless address. If the mint fails the web opens as before and the log says it shows this PC's data only. Needs ODIAN_Engine's _WEB/src/auth.ts to read #token=. Spot-check: launch dev mode with the web target while online; the API log reads "now LIVE on the DHP" and the browser shows live tracks.
This commit is contained in:
@@ -2016,7 +2016,16 @@ impl Launcher {
|
||||
|
||||
if let Some(url) = config.web_url() {
|
||||
self.log(app, "info", format!("The web client is at {url}"));
|
||||
if let Err(e) = app.opener().open_url(url.as_str(), None::<&str>) {
|
||||
// Signed in the way the .exe is, so live mode reaches the browser too. The token
|
||||
// rides only in the address handed to the browser, never in the line logged above.
|
||||
let opened = match stack::mint_web_token(&self.http, &config).await {
|
||||
Ok(token) => config.web_url_signed(&token).unwrap_or(url),
|
||||
Err(e) => {
|
||||
self.log(app, "warn", format!("The web client opens without a session ({e}), so it shows this PC's data only."));
|
||||
url
|
||||
}
|
||||
};
|
||||
if let Err(e) = app.opener().open_url(opened.as_str(), None::<&str>) {
|
||||
// Not a failure: the address is in the log and in the panel, and a developer can
|
||||
// paste it. Refusing the whole launch over a browser that would not open would be.
|
||||
self.log(app, "warn", format!("Could not open a browser: {e}. Open the address above yourself."));
|
||||
|
||||
@@ -282,6 +282,17 @@ impl StackConfig {
|
||||
})
|
||||
}
|
||||
|
||||
/// `web_url` with a session token for the web client, in the FRAGMENT.
|
||||
///
|
||||
/// The local API relays to the DHP only a request carrying a token it minted itself -
|
||||
/// the forwarder's guard against being an open proxy - so a tokenless web client stays
|
||||
/// on this PC's database while the .exe beside it is live. A fragment never leaves the
|
||||
/// browser: not in the request line, not in Vite's log. Never log this address;
|
||||
/// `web_url` is the one to print.
|
||||
pub fn web_url_signed(&self, token: &str) -> Option<String> {
|
||||
self.web_url().map(|url| format!("{url}#token={}", url_value(token)))
|
||||
}
|
||||
|
||||
/// Everything this run takes from the checkout rather than the install, for the log.
|
||||
pub fn dev_parts(&self) -> Vec<String> {
|
||||
match &self.dev_engine {
|
||||
@@ -655,6 +666,31 @@ async fn start_web(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A session token from THIS stack's API for the web client, minted with the secret the
|
||||
/// .exe signs in with and for the same operator - so both clients reach the DHP alike.
|
||||
pub async fn mint_web_token(http: &reqwest::Client, config: &StackConfig) -> Result<String, String> {
|
||||
#[derive(Deserialize)]
|
||||
struct Minted {
|
||||
access_token: String,
|
||||
}
|
||||
let response = http
|
||||
.post(format!("http://127.0.0.1:{API_PORT}/auth/mint"))
|
||||
.header("x-mint-secret", &config.mint_secret)
|
||||
.json(&serde_json::json!({ "username": config.operator }))
|
||||
.timeout(Duration::from_secs(10))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("the local API did not answer: {e}"))?;
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("the local API refused it (HTTP {})", response.status().as_u16()));
|
||||
}
|
||||
response
|
||||
.json::<Minted>()
|
||||
.await
|
||||
.map(|minted| minted.access_token)
|
||||
.map_err(|e| format!("the local API's answer made no sense: {e}"))
|
||||
}
|
||||
|
||||
fn failed(emit: Emit<'_>, component: Component, message: String) -> String {
|
||||
emit(StackEvent { component, state: ComponentState::Failed, message: Some(message.clone()) });
|
||||
message
|
||||
@@ -1337,6 +1373,22 @@ mod tests {
|
||||
assert!(cfg.web_url().unwrap().ends_with("&user=m2op"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_web_token_rides_in_the_fragment_and_never_in_the_printed_address() {
|
||||
let mut cfg = config(&[("dark", "protomaps_20260423")]);
|
||||
cfg.dev_engine = Some(PathBuf::from(r"E:\dev\ODIAN_Engine"));
|
||||
cfg.dev_target = DevTarget::Web;
|
||||
let plain = cfg.web_url().unwrap();
|
||||
let signed = cfg.web_url_signed("a+b/c=").unwrap();
|
||||
// The query is untouched - the token is after `#`, so the browser never sends it.
|
||||
assert_eq!(signed, format!("{plain}#token=a%2Bb%2Fc%3D"));
|
||||
assert!(!plain.contains("token"), "the address the launcher logs carries no token");
|
||||
|
||||
// No web client this run, no address at all.
|
||||
cfg.dev_target = DevTarget::Client;
|
||||
assert_eq!(cfg.web_url_signed("t"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_web_server_is_started_on_the_port_the_launcher_advertises() {
|
||||
// Checked as ARGUMENTS rather than by reading the binary: these are short literals that
|
||||
|
||||
Reference in New Issue
Block a user