mod common;
mod join;
#[cfg(feature = "lan-pairing")]
mod lan;
#[cfg(feature = "lan-pairing")]
mod lan_server;
mod offline;
mod online;
use std::sync::Arc;
use anyhow::Result;
use auths_sdk::core_config::EnvironmentConfig;
use auths_sdk::signing::PassphraseProvider;
use chrono::Utc;
use clap::Parser;
#[cfg(not(feature = "lan-pairing"))]
const DEFAULT_REGISTRY: &str = "http://localhost:3000";
#[derive(Parser, Debug, Clone)]
#[command(
about = "Link devices to your identity",
after_help = "Examples:
auths pair # Start LAN pairing session (shows QR code)
auths pair --join CODE # Join an existing pairing session using short code
auths pair --registry URL # Pair via relay server
auths pair --offline # Offline mode (for testing, no network)
Modes:
LAN mode (default) — Local pairing via mDNS, fastest and most secure
Online mode — Via relay server, works across networks
Offline mode — For testing, no network required
Related:
auths status — Check linked devices
auths device — Manage device authorizations
auths init — Initial setup wizard"
)]
pub struct PairCommand {
#[clap(long, value_name = "CODE")]
pub join: Option<String>,
#[clap(long, value_name = "URL")]
pub registry: Option<String>,
#[clap(long, hide_short_help = true)]
pub no_qr: bool,
#[clap(
long,
visible_alias = "expiry",
value_name = "SECONDS",
default_value = "300"
)]
pub timeout: u64,
#[clap(long, hide_short_help = true)]
pub offline: bool,
#[clap(
long,
value_delimiter = ',',
default_value = "sign_commit",
hide_short_help = true
)]
pub capabilities: Vec<String>,
#[cfg(feature = "lan-pairing")]
#[clap(long, hide_short_help = true)]
pub no_mdns: bool,
#[clap(long)]
pub verify: bool,
#[clap(long, value_name = "OLD_DID")]
pub recover: Option<String>,
}
pub fn handle_pair(
cmd: PairCommand,
passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
env_config: &EnvironmentConfig,
) -> Result<()> {
#[allow(clippy::disallowed_methods)]
let now = Utc::now();
if let Err(e) = crate::commands::agent::ensure_agent_running(true) {
log::debug!("auths-agent auto-start skipped: {e}");
}
match (&cmd.join, &cmd.registry, cmd.offline) {
(None, _, true) => {
offline::handle_initiate_offline(now, cmd.no_qr, cmd.timeout, &cmd.capabilities)
}
(Some(code), Some(registry), _) => {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(join::handle_join(now, code, registry, env_config))
}
#[cfg(feature = "lan-pairing")]
(Some(code), None, _) => {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(lan::handle_join_lan(now, code, env_config))
}
#[cfg(not(feature = "lan-pairing"))]
(Some(code), None, _) => {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(join::handle_join(now, code, DEFAULT_REGISTRY, env_config))
}
(None, Some(registry), _) => {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(online::handle_initiate_online(
now,
registry,
cmd.no_qr,
cmd.timeout,
&cmd.capabilities,
env_config,
cmd.recover.clone(),
))
}
#[cfg(feature = "lan-pairing")]
(None, None, false) => {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(lan::handle_initiate_lan(
now,
cmd.no_qr,
cmd.no_mdns,
cmd.verify,
cmd.recover.clone(),
cmd.timeout,
&cmd.capabilities,
passphrase_provider,
env_config,
))
}
#[cfg(not(feature = "lan-pairing"))]
(None, None, false) => {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(online::handle_initiate_online(
now,
DEFAULT_REGISTRY,
cmd.no_qr,
cmd.timeout,
&cmd.capabilities,
env_config,
cmd.recover.clone(),
))
}
}
}
#[cfg(test)]
mod tests {
use auths_sdk::pairing::normalize_short_code;
#[test]
fn test_code_normalization() {
let codes = vec![
("AB3DEF", "AB3DEF"),
("ab3def", "AB3DEF"),
("AB3 DEF", "AB3DEF"),
("AB3-DEF", "AB3DEF"),
("a b 3 d e f", "AB3DEF"),
];
for (input, expected) in codes {
let normalized = normalize_short_code(input);
assert_eq!(normalized, expected, "Input: '{}'", input);
}
}
}