use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use console::style;
use auths_sdk::core_config::EnvironmentConfig;
use auths_sdk::pairing::CreateSessionRequest;
use auths_sdk::pairing::{PairingToken, QrOptions, render_qr};
use auths_sdk::signing::PassphraseProvider;
use super::common::*;
use super::lan_server::{LanPairingServer, detect_lan_ip};
#[allow(clippy::too_many_arguments)]
pub async fn handle_initiate_lan(
now: chrono::DateTime<chrono::Utc>,
no_qr: bool,
no_mdns: bool,
verify: bool,
recover: Option<String>,
expiry_secs: u64,
capabilities: &[String],
passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
env_config: &EnvironmentConfig,
) -> Result<()> {
if let Some(ref old_did) = recover {
let _ = old_did;
return Err(anyhow::anyhow!(
"Device recovery (--recover) over LAN is not yet supported. Use online recovery \
(`auths pair --recover <did> --registry <url>`), or remove the lost device with \
`auths device remove <did>` and pair a replacement."
));
}
let auths_dir = auths_sdk::paths::auths_home_with_config(env_config)
.context("Could not determine Auths home directory. Check $AUTHS_HOME or $HOME.")?;
let identity_storage = auths_sdk::storage::RegistryIdentityStorage::new(auths_dir.clone());
let controller_did =
auths_sdk::pairing::load_controller_did(&identity_storage).map_err(anyhow::Error::from)?;
let lan_ip =
detect_lan_ip().context("Failed to detect LAN IP. Are you connected to a network?")?;
let expiry = chrono::Duration::seconds(expiry_secs as i64);
let mut session = PairingToken::generate_with_expiry(
now,
controller_did.clone(),
"http://placeholder".to_string(), capabilities.to_vec(),
expiry,
)
.context("Failed to generate pairing token")?;
let session_id = session.token.session_id.clone();
let request = CreateSessionRequest {
session_id: session_id.clone(),
controller_did: session.token.controller_did.clone(),
ephemeral_pubkey: auths_sdk::pairing::Base64UrlEncoded::from_raw(
session.token.ephemeral_pubkey.clone(),
),
short_code: session.token.short_code.clone(),
capabilities: session.token.capabilities.clone(),
expires_at: session.token.expires_at.timestamp(),
recovery_target: recover.clone(),
};
let mut server = LanPairingServer::start(request, lan_ip).await?;
let port = server.addr().port();
let endpoint = format!("http://{}:{}", lan_ip, port);
session.token.endpoint = format!("{}?token={}", endpoint, server.pairing_token());
let health_url = format!("{}/health", &endpoint);
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(3))
.build()
.unwrap_or_default();
match client.get(&health_url).send().await {
Ok(resp) if resp.status().is_success() => {}
_ => {
let warning = format!(
"LAN server started but self-test failed. Your Mac's firewall may be blocking port {}.",
port
);
println!(
" {} {}",
style("Warning:").yellow().bold(),
style(&warning).yellow()
);
println!(
" {} Try: {}",
style("→").yellow(),
style("sudo pfctl -d").bold()
);
println!();
}
}
print_pairing_header("LAN", &endpoint, &controller_did);
let _advertiser = if !no_mdns {
match server.advertise(port, &session.token.short_code, &controller_did) {
Some(Ok(adv)) => {
println!(
" {} {}",
style("mDNS:").dim(),
style("advertising on local network").green()
);
Some(adv)
}
Some(Err(e)) => {
println!(" {} {} {}", WARN, style("mDNS unavailable:").yellow(), e);
None
}
None => {
println!(
" {} {} not available (mdns feature disabled)",
WARN,
style("mDNS:").yellow()
);
None
}
}
} else {
None
};
if !no_qr {
println!();
let options = QrOptions::default();
let qr = render_qr(&session.token, &options).context("Failed to render QR code")?;
println!("{}", qr);
}
let sc = &session.token.short_code;
let formatted_code = format!("{}-{}", &sc[..3], &sc[3..]);
println!();
println!(" Scan the QR code above, or enter this code manually:");
println!();
println!(" {}", style(&formatted_code).bold().cyan());
println!();
if !capabilities.is_empty() {
println!(
" {} {}",
style("Capabilities:").dim(),
capabilities.join(", ")
);
}
println!(
" {} {} ({}s remaining)",
style("Expires:").dim(),
session.token.expires_at.format("%H:%M:%S"),
expiry_secs
);
println!();
println!(" {}", style("(Press Ctrl+C to cancel)").dim());
println!();
println!(
" {} Test from another terminal: {}",
style("Debug:").dim(),
style(format!("curl {}/health", &endpoint)).dim()
);
println!();
let wait_spinner = create_wait_spinner(&format!("{PHONE}Waiting for device on LAN..."));
let expiry_duration = Duration::from_secs(expiry_secs);
match server.wait_for_response(expiry_duration).await {
Ok(response_data) => {
wait_spinner.finish_with_message(format!("{CHECK}Response received!"));
if let Some(adv) = _advertiser {
adv.shutdown();
}
let _confirmation = server.wait_for_confirmation(Duration::from_secs(5)).await;
handle_pairing_response(
now,
&mut session,
response_data,
&auths_dir,
capabilities,
Arc::clone(&passphrase_provider),
env_config,
verify,
)?;
server.shutdown();
}
Err(auths_sdk::error::PairingError::LanTimeout) => {
wait_spinner.finish_with_message(format!("{}", style("Session expired.").yellow()));
if let Some(adv) = _advertiser {
adv.shutdown();
}
server.shutdown();
}
Err(e) => {
wait_spinner.finish_and_clear();
if let Some(adv) = _advertiser {
adv.shutdown();
}
server.shutdown();
return Err(anyhow::anyhow!("LAN pairing failed: {}", e));
}
}
Ok(())
}
pub async fn handle_join_lan(
now: chrono::DateTime<chrono::Utc>,
code: &str,
env_config: &EnvironmentConfig,
) -> Result<()> {
use auths_sdk::pairing::normalize_short_code;
let normalized = normalize_short_code(code);
if normalized.len() != 6 {
anyhow::bail!(
"Short code must be exactly 6 characters (got {})",
normalized.len()
);
}
let formatted = format!("{}-{}", &normalized[..3], &normalized[3..]);
println!();
println!(
"{}",
style(format!("━━━ {LINK}Discovering LAN Peer ━━━")).bold()
);
println!();
println!(
" {} {}",
style("Code:").dim(),
style(&formatted).bold().cyan()
);
println!();
let discover_spinner = create_wait_spinner(&format!("{GEAR}Searching for peer via mDNS..."));
let discovery = auths_pairing_daemon::MdnsDiscovery;
let addr = auths_pairing_daemon::NetworkDiscovery::discover(
&discovery,
&normalized,
Duration::from_secs(30),
)
.map_err(|e| anyhow::anyhow!("mDNS discovery failed: {}", e))?;
discover_spinner.finish_with_message(format!("{CHECK}Found peer at {}", style(addr).cyan()));
let registry = format!("http://{}", addr);
super::join::handle_join(now, &normalized, ®istry, env_config).await
}