Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions apps/cli/src/domains/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,5 +439,22 @@ fn summarize_event(event: &Event) -> String {
Event::Custom { event_type, data } => {
format!("Custom event: {} - {:?}", event_type, data)
}

// Pairing events
Event::PairingConfirmationRequired {
session_id,
device_name,
device_os,
confirmation_code,
expires_at,
} => {
format!(
"Pairing confirmation required: '{}' ({}) wants to pair. Code: {} (expires: {})",
device_name, device_os, confirmation_code, expires_at
)
}
Event::PairingRejected { session_id, reason } => {
format!("Pairing rejected for session {}: {}", session_id, reason)
}
}
}
33 changes: 31 additions & 2 deletions apps/cli/src/domains/network/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use sd_core::{
domain::addressing::SdPath,
ops::network::{
pair::{
cancel::input::PairCancelInput, generate::input::PairGenerateInput,
join::input::PairJoinInput,
cancel::input::PairCancelInput, confirm::input::PairConfirmInput,
generate::input::PairGenerateInput, join::input::PairJoinInput,
},
revoke::input::DeviceRevokeInput,
spacedrop::send::input::SpacedropSendInput,
Expand All @@ -29,6 +29,17 @@ pub enum PairCmd {
Status,
/// Cancel a pairing session
Cancel { session_id: Uuid },
/// Confirm a pairing request
Confirm {
/// Session ID of the pairing request
session_id: Uuid,
/// Accept the pairing request
#[arg(long, conflicts_with = "reject")]
accept: bool,
/// Reject the pairing request
#[arg(long, conflicts_with = "accept")]
reject: bool,
},
}

impl PairCmd {
Expand Down Expand Up @@ -80,6 +91,24 @@ impl PairCmd {
_ => None,
}
}

pub fn to_confirm_input(&self) -> Option<PairConfirmInput> {
match self {
Self::Confirm {
session_id,
accept,
reject,
} => {
// Require explicit --accept or --reject flag
let accepted = if *reject { false } else { *accept };
Some(PairConfirmInput {
session_id: *session_id,
accepted,
})
}
_ => None,
}
}
}

#[derive(Args, Debug, Clone)]
Expand Down
128 changes: 128 additions & 0 deletions apps/cli/src/domains/network/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use sd_core::ops::network::{
devices::{output::ListPairedDevicesOutput, query::ListPairedDevicesInput},
pair::{
cancel::output::PairCancelOutput,
confirm::output::PairConfirmOutput,
generate::output::PairGenerateOutput,
join::output::PairJoinOutput,
status::{output::PairStatusOutput, query::PairStatusQuery},
Expand Down Expand Up @@ -99,6 +100,11 @@ pub async fn run(ctx: &Context, cmd: NetworkCmd) -> Result<()> {
println!("Session: {}", o.session_id);
println!("Expires at: {}", o.expires_at);
});

// Wait for pairing completion or confirmation request
println!("\nWaiting for device to connect...");
println!("(Press Ctrl+C to cancel)\n");
run_pairing_confirmation_loop(ctx, out.session_id).await?;
}
PairCmd::Join {
ref code,
Expand Down Expand Up @@ -140,6 +146,20 @@ pub async fn run(ctx: &Context, cmd: NetworkCmd) -> Result<()> {
println!("Cancelled: {}", o.cancelled);
});
}
PairCmd::Confirm { .. } => {
let input = pc.to_confirm_input().unwrap();
let out: PairConfirmOutput = execute_action!(ctx, input);
print_output!(ctx, &out, |o: &PairConfirmOutput| {
if o.success {
println!("Pairing confirmed successfully");
} else {
println!(
"Pairing confirmation failed: {}",
o.error.as_ref().unwrap_or(&"Unknown error".to_string())
);
}
});
}
},
NetworkCmd::Devices { connected } => {
let input = ListPairedDevicesInput {
Expand Down Expand Up @@ -207,6 +227,114 @@ pub async fn run(ctx: &Context, cmd: NetworkCmd) -> Result<()> {
Ok(())
}

/// Poll for pairing status and handle confirmation requests
async fn run_pairing_confirmation_loop(ctx: &Context, session_id: uuid::Uuid) -> Result<()> {
use crate::util::confirm::text;
use std::io::{self, Write};

loop {
// Check pairing status
let status: PairStatusOutput = execute_core_query!(
ctx,
sd_core::ops::network::pair::status::query::PairStatusQueryInput
);

// Find our session
let session = status.sessions.iter().find(|s| s.id == session_id);

match session {
Some(s) => {
// Check state
match &s.state {
sd_core::ops::network::pair::status::output::SerializablePairingState::Completed => {
println!("\n✓ Pairing completed successfully!");
if let Some(device_name) = &s.remote_device_name {
println!(" Paired with: {}", device_name);
}
break;
}
sd_core::ops::network::pair::status::output::SerializablePairingState::AwaitingUserConfirmation {
confirmation_code,
expires_at,
} => {
// Prompt user for confirmation
println!("\n═══════════════════════════════════════════════════════");
println!(" PAIRING REQUEST RECEIVED");
println!("═══════════════════════════════════════════════════════");
if let Some(device_name) = &s.remote_device_name {
println!(" Device: {}", device_name);
}
if let Some(device_os) = &s.remote_device_os {
println!(" OS: {}", device_os);
}
println!();
println!(" Confirmation Code: {}", confirmation_code);
println!(" Expires at: {}", expires_at);
println!("═══════════════════════════════════════════════════════");
println!();

// Prompt for code
print!("Enter the code to accept (or 'n' to reject): ");
io::stdout().flush()?;

let input = text("", false)?.unwrap_or_default();

let accepted = if input.to_lowercase() == "n" || input.to_lowercase() == "no" {
false
} else if input == *confirmation_code {
true
} else {
println!("Code doesn't match! Rejecting request.");
false
};

// Send confirmation
let confirm_input = sd_core::ops::network::pair::confirm::input::PairConfirmInput {
session_id,
accepted,
};
let out: PairConfirmOutput = execute_action!(ctx, confirm_input);

if !out.success {
println!(
"Failed to confirm: {}",
out.error.as_ref().unwrap_or(&"Unknown error".to_string())
);
}

if !accepted {
println!("Pairing rejected.");
break;
}
}
sd_core::ops::network::pair::status::output::SerializablePairingState::Failed { reason } => {
println!("\n✗ Pairing failed: {}", reason);
break;
}
sd_core::ops::network::pair::status::output::SerializablePairingState::Rejected { reason } => {
println!("\n✗ Pairing rejected: {}", reason);
break;
}
_ => {
// Still waiting
print!(".");
io::stdout().flush()?;
}
}
}
None => {
println!("\n✗ Session not found - may have expired");
break;
}
}

// Wait before polling again
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
}

Ok(())
}

async fn run_interactive_pair_join(
ctx: &Context,
code: Option<&str>,
Expand Down
Loading
Loading