Skip to main content

auths_cli/commands/device/pair/
mod.rs

1//! Device pairing commands.
2//!
3//! Provides commands to initiate and join device pairing sessions
4//! for cross-device identity linking using X25519 ECDH key exchange.
5
6mod common;
7mod join;
8#[cfg(feature = "lan-pairing")]
9mod lan;
10#[cfg(feature = "lan-pairing")]
11mod lan_server;
12#[cfg(feature = "lan-pairing")]
13mod mdns;
14mod offline;
15mod online;
16
17use anyhow::Result;
18use auths_core::config::EnvironmentConfig;
19use clap::Parser;
20
21/// Default registry URL for local development.
22#[cfg(not(feature = "lan-pairing"))]
23const DEFAULT_REGISTRY: &str = "http://localhost:3000";
24
25#[derive(Parser, Debug, Clone)]
26#[command(about = "Link devices to your identity")]
27pub struct PairCommand {
28    /// Join an existing pairing session using a short code
29    #[clap(long, value_name = "CODE")]
30    pub join: Option<String>,
31
32    /// Registry URL for pairing relay (omit for LAN mode)
33    #[clap(long, value_name = "URL")]
34    pub registry: Option<String>,
35
36    /// Don't display QR code (only show short code)
37    #[clap(long, hide_short_help = true)]
38    pub no_qr: bool,
39
40    /// Custom timeout in seconds for the pairing session (default: 300 = 5 minutes)
41    #[clap(
42        long,
43        visible_alias = "expiry",
44        value_name = "SECONDS",
45        default_value = "300"
46    )]
47    pub timeout: u64,
48
49    /// Skip registry server (offline mode, for testing)
50    #[clap(long, hide_short_help = true)]
51    pub offline: bool,
52
53    /// Capabilities to grant the paired device (comma-separated)
54    #[clap(
55        long,
56        value_delimiter = ',',
57        default_value = "sign_commit",
58        hide_short_help = true
59    )]
60    pub capabilities: Vec<String>,
61
62    /// Disable mDNS advertisement/discovery in LAN mode
63    #[cfg(feature = "lan-pairing")]
64    #[clap(long, hide_short_help = true)]
65    pub no_mdns: bool,
66}
67
68/// Dispatch table:
69///
70/// | Flags                        | Behavior                              |
71/// |------------------------------|---------------------------------------|
72/// | `pair` (no flags)            | LAN mode: start local server, show QR |
73/// | `pair --registry URL`        | Online mode (existing)                |
74/// | `pair --join CODE`           | LAN join: mDNS discover -> join       |
75/// | `pair --join CODE --registry`| Online join (existing)                |
76/// | `pair --offline`             | Offline mode (no network)             |
77pub fn handle_pair(cmd: PairCommand, env_config: &EnvironmentConfig) -> Result<()> {
78    match (&cmd.join, &cmd.registry, cmd.offline) {
79        // Offline mode takes priority
80        (None, _, true) => {
81            offline::handle_initiate_offline(cmd.no_qr, cmd.timeout, &cmd.capabilities)
82        }
83
84        // Join with explicit registry -> online join
85        (Some(code), Some(registry), _) => {
86            let rt = tokio::runtime::Runtime::new()?;
87            rt.block_on(join::handle_join(code, registry, env_config))
88        }
89
90        // Join without registry -> LAN join via mDNS
91        #[cfg(feature = "lan-pairing")]
92        (Some(code), None, _) => {
93            let rt = tokio::runtime::Runtime::new()?;
94            rt.block_on(lan::handle_join_lan(code, env_config))
95        }
96
97        // Join without registry and no LAN feature -> use default registry
98        #[cfg(not(feature = "lan-pairing"))]
99        (Some(code), None, _) => {
100            let rt = tokio::runtime::Runtime::new()?;
101            rt.block_on(join::handle_join(code, DEFAULT_REGISTRY, env_config))
102        }
103
104        // Initiate with explicit registry -> online mode
105        (None, Some(registry), _) => {
106            let rt = tokio::runtime::Runtime::new()?;
107            rt.block_on(online::handle_initiate_online(
108                registry,
109                cmd.no_qr,
110                cmd.timeout,
111                &cmd.capabilities,
112                env_config,
113            ))
114        }
115
116        // Initiate without registry -> LAN mode
117        #[cfg(feature = "lan-pairing")]
118        (None, None, false) => {
119            let rt = tokio::runtime::Runtime::new()?;
120            rt.block_on(lan::handle_initiate_lan(
121                cmd.no_qr,
122                cmd.no_mdns,
123                cmd.timeout,
124                &cmd.capabilities,
125                env_config,
126            ))
127        }
128
129        // Initiate without registry and no LAN feature -> use default registry
130        #[cfg(not(feature = "lan-pairing"))]
131        (None, None, false) => {
132            let rt = tokio::runtime::Runtime::new()?;
133            rt.block_on(online::handle_initiate_online(
134                DEFAULT_REGISTRY,
135                cmd.no_qr,
136                cmd.timeout,
137                &cmd.capabilities,
138                env_config,
139            ))
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use auths_core::pairing::normalize_short_code;
147
148    #[test]
149    fn test_code_normalization() {
150        let codes = vec![
151            ("AB3DEF", "AB3DEF"),
152            ("ab3def", "AB3DEF"),
153            ("AB3 DEF", "AB3DEF"),
154            ("AB3-DEF", "AB3DEF"),
155            ("a b 3 d e f", "AB3DEF"),
156        ];
157
158        for (input, expected) in codes {
159            let normalized = normalize_short_code(input);
160            assert_eq!(normalized, expected, "Input: '{}'", input);
161        }
162    }
163}