//! `spectra open`: sign in to Radar (spec §8.3). The owner's CLI signs a //! request for a one-time link with the owner's key; the link, valid for a //! minute, opens a level-2 browser session. use super::identity::{owner_home, recorded_owner}; use super::{ui, Ctx}; use anyhow::{bail, Context, Result}; use spectra_core::ledger::Ledger; use spectra_mcp::client::Client; use std::path::PathBuf; /// The sign-in link, landing on `page` if one is named. fn with_page(url: String, page: Option<&str>) -> String { match page.filter(|p| !p.is_empty() && *p != "live") { Some(p) => format!("{url}&next={}", p.trim_start_matches('/')), None => url, } } pub async fn open(ctx: &Ctx, owner_home_flag: Option, launch: bool, page: Option<&str>) -> Result { let socket = ctx.socket(); if !Client::alive(&socket).await { bail!("Spectra is not serving {} (spectra serve)", ctx.data.display()); } let home = owner_home(ctx, owner_home_flag); let ledger = Ledger::open_read_only(&ctx.db()?).await?; let owner = recorded_owner(&ledger, &home).await?; let r = Client::new(&socket, Some(owner)).send("POST", "/radar/link", &[], Vec::new()).await?; if r.status != 200 { bail!("the daemon refused the sign-in: {}", String::from_utf8_lossy(&r.body)); } let url = r.json()?.get("url").and_then(|u| u.as_str()).map(String::from).context("no link in the answer")?; let url = with_page(url, page); println!("{} {}", ui::done("Signed"), ui::subject("a one-time link to Radar, good for a minute")); show(&url, launch); Ok(0) } fn show(url: &str, launch: bool) { println!(" {}", ui::id(url)); if launch && std::process::Command::new("xdg-open") .arg(url) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .spawn() .is_ok() { println!(" {} {}", ui::note(), ui::dim("opened in your browser")); } } /// `spectra open` with Spectra on another machine (spec §8.6): serve Radar /// through a loopback proxy to the pinned server, and sign in through it. pub async fn open_remote(ctx: &Ctx, owner_home_flag: Option, launch: bool, port: u16, page: Option<&str>) -> Result { let cfg = ctx.remote()?.context("no server configured")?; let home = owner_home(ctx, owner_home_flag); let listener = tokio::net::TcpListener::bind(("127.0.0.1", port)) .await .with_context(|| format!("binding 127.0.0.1:{port} for Radar (another `spectra open` running? --port picks another)"))?; let port = listener.local_addr()?.port(); let client = ctx.owner_client(&home).await?.context("no server configured")?; let r = client.send("POST", "/radar/link", &[], Vec::new()).await?; if r.status != 200 { bail!("the server refused the sign-in: {}", String::from_utf8_lossy(&r.body)); } let url = r.json()?.get("url").and_then(|u| u.as_str()).map(String::from).context("no link in the answer")?; let local = with_page(spectra_mcp::proxy::local_link(&url, port), page); println!("{} {}{}{}", ui::done("Serving"), ui::subject("Radar"), ui::sep(), ui::dim(format!("from {} over pinned TLS", cfg.server))); println!("{}", ui::kv("radar", format!("http://127.0.0.1:{port}"))); println!("{} {}", ui::done("Signed"), ui::subject("a one-time link, good for a minute")); show(&local, launch); println!(" {} {}", ui::note(), ui::dim("Ctrl-C stops the proxy")); use std::io::Write; std::io::stdout().flush().ok(); let app = spectra_mcp::proxy::router(cfg.endpoint()?, port); axum::serve(listener, app) .with_graceful_shutdown(async { let _ = tokio::signal::ctrl_c().await; }) .await?; Ok(0) }