auths_cli/commands/device/pair/
mod.rs1mod common;
7mod join;
8#[cfg(feature = "lan-pairing")]
9mod lan;
10#[cfg(feature = "lan-pairing")]
11mod lan_server;
12mod offline;
13mod online;
14
15use std::sync::Arc;
16
17use anyhow::Result;
18use auths_sdk::core_config::EnvironmentConfig;
19use auths_sdk::signing::PassphraseProvider;
20use chrono::Utc;
21use clap::Parser;
22
23#[cfg(not(feature = "lan-pairing"))]
25const DEFAULT_REGISTRY: &str = "http://localhost:3000";
26
27#[derive(Parser, Debug, Clone)]
28#[command(
29 about = "Link devices to your identity",
30 after_help = "Examples:
31 auths pair # Start LAN pairing session (shows QR code)
32 auths pair --join CODE # Join an existing pairing session using short code
33 auths pair --registry URL # Pair via relay server
34 auths pair --offline # Offline mode (for testing, no network)
35
36Modes:
37 LAN mode (default) — Local pairing via mDNS, fastest and most secure
38 Online mode — Via relay server, works across networks
39 Offline mode — For testing, no network required
40
41Related:
42 auths status — Check linked devices
43 auths device — Manage device authorizations
44 auths init — Initial setup wizard"
45)]
46pub struct PairCommand {
47 #[clap(long, value_name = "CODE")]
49 pub join: Option<String>,
50
51 #[clap(long, value_name = "URL")]
53 pub registry: Option<String>,
54
55 #[clap(long, hide_short_help = true)]
57 pub no_qr: bool,
58
59 #[clap(
61 long,
62 visible_alias = "expiry",
63 value_name = "SECONDS",
64 default_value = "300"
65 )]
66 pub timeout: u64,
67
68 #[clap(long, hide_short_help = true)]
70 pub offline: bool,
71
72 #[clap(
74 long,
75 value_delimiter = ',',
76 default_value = "sign_commit",
77 hide_short_help = true
78 )]
79 pub capabilities: Vec<String>,
80
81 #[cfg(feature = "lan-pairing")]
83 #[clap(long, hide_short_help = true)]
84 pub no_mdns: bool,
85
86 #[clap(long)]
94 pub verify: bool,
95
96 #[clap(long, value_name = "OLD_DID")]
101 pub recover: Option<String>,
102}
103
104pub fn handle_pair(
114 cmd: PairCommand,
115 passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
116 env_config: &EnvironmentConfig,
117) -> Result<()> {
118 #[allow(clippy::disallowed_methods)]
119 let now = Utc::now();
120
121 if let Err(e) = crate::commands::agent::ensure_agent_running(true) {
127 log::debug!("auths-agent auto-start skipped: {e}");
128 }
129
130 match (&cmd.join, &cmd.registry, cmd.offline) {
131 (None, _, true) => {
133 offline::handle_initiate_offline(now, cmd.no_qr, cmd.timeout, &cmd.capabilities)
134 }
135
136 (Some(code), Some(registry), _) => {
138 let rt = tokio::runtime::Runtime::new()?;
139 rt.block_on(join::handle_join(now, code, registry, env_config))
140 }
141
142 #[cfg(feature = "lan-pairing")]
144 (Some(code), None, _) => {
145 let rt = tokio::runtime::Runtime::new()?;
146 rt.block_on(lan::handle_join_lan(now, code, env_config))
147 }
148
149 #[cfg(not(feature = "lan-pairing"))]
151 (Some(code), None, _) => {
152 let rt = tokio::runtime::Runtime::new()?;
153 rt.block_on(join::handle_join(now, code, DEFAULT_REGISTRY, env_config))
154 }
155
156 (None, Some(registry), _) => {
158 let rt = tokio::runtime::Runtime::new()?;
159 rt.block_on(online::handle_initiate_online(
160 now,
161 registry,
162 cmd.no_qr,
163 cmd.timeout,
164 &cmd.capabilities,
165 env_config,
166 cmd.recover.clone(),
167 ))
168 }
169
170 #[cfg(feature = "lan-pairing")]
172 (None, None, false) => {
173 let rt = tokio::runtime::Runtime::new()?;
174 rt.block_on(lan::handle_initiate_lan(
175 now,
176 cmd.no_qr,
177 cmd.no_mdns,
178 cmd.verify,
179 cmd.recover.clone(),
180 cmd.timeout,
181 &cmd.capabilities,
182 passphrase_provider,
183 env_config,
184 ))
185 }
186
187 #[cfg(not(feature = "lan-pairing"))]
189 (None, None, false) => {
190 let rt = tokio::runtime::Runtime::new()?;
191 rt.block_on(online::handle_initiate_online(
192 now,
193 DEFAULT_REGISTRY,
194 cmd.no_qr,
195 cmd.timeout,
196 &cmd.capabilities,
197 env_config,
198 cmd.recover.clone(),
199 ))
200 }
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use auths_sdk::pairing::normalize_short_code;
207
208 #[test]
209 fn test_code_normalization() {
210 let codes = vec![
211 ("AB3DEF", "AB3DEF"),
212 ("ab3def", "AB3DEF"),
213 ("AB3 DEF", "AB3DEF"),
214 ("AB3-DEF", "AB3DEF"),
215 ("a b 3 d e f", "AB3DEF"),
216 ];
217
218 for (input, expected) in codes {
219 let normalized = normalize_short_code(input);
220 assert_eq!(normalized, expected, "Input: '{}'", input);
221 }
222 }
223}