Skip to main content

auths_cli/commands/
key_detect.rs

1//! Shared device key alias auto-detection.
2
3use std::path::Path;
4
5use anyhow::{Context, Result, anyhow};
6use auths_sdk::core_config::EnvironmentConfig;
7use auths_sdk::keychain::{KeyAlias, KeyStorage};
8use auths_sdk::ports::IdentityStorage;
9use auths_sdk::storage::RegistryIdentityStorage;
10use dialoguer::Select;
11use std::io::IsTerminal;
12
13use crate::ux::format::is_json_mode;
14
15fn filter_signing_aliases(aliases: Vec<KeyAlias>) -> Vec<KeyAlias> {
16    aliases
17        .into_iter()
18        .filter(|a| !a.as_str().contains("--next-"))
19        .collect()
20}
21
22fn select_device_key_interactive(aliases: &[KeyAlias]) -> Result<String> {
23    let display_items: Vec<&str> = aliases.iter().map(|a| a.as_str()).collect();
24
25    let selection = Select::new()
26        .with_prompt("Select signing key")
27        .items(&display_items)
28        .default(0)
29        .interact()
30        .context("Key selection cancelled")?;
31
32    Ok(aliases[selection].as_str().to_string())
33}
34
35/// Auto-detect the device key alias when not explicitly provided.
36///
37/// Loads the identity from the repository, then lists all key aliases
38/// associated with that identity. Filters out `--next-` rotation keys,
39/// then either auto-selects (single key) or prompts interactively (multiple keys).
40///
41/// Args:
42/// * `repo_opt`: Optional path to the identity repository.
43/// * `env_config`: Environment configuration for keychain access.
44///
45/// Usage:
46/// ```ignore
47/// let alias = auto_detect_device_key(repo_opt.as_deref(), env_config)?;
48/// ```
49pub fn auto_detect_device_key(
50    repo_opt: Option<&Path>,
51    env_config: &EnvironmentConfig,
52) -> Result<String> {
53    let repo_path =
54        auths_sdk::storage_layout::resolve_repo_path(repo_opt.map(|p| p.to_path_buf()))?;
55    let identity_storage = RegistryIdentityStorage::new(repo_path.clone());
56    let identity = identity_storage
57        .load_identity()
58        .map_err(|_| anyhow!("No identity found. Run `auths init` to get started."))?;
59
60    let keychain = auths_sdk::keychain::get_platform_keychain_with_config(env_config)
61        .context("Failed to access keychain")?;
62    let aliases = keychain
63        .list_aliases_for_identity(&identity.controller_did)
64        .map_err(|e| anyhow!("Failed to list key aliases: {e}"))?;
65
66    let signing_aliases = filter_signing_aliases(aliases);
67
68    match signing_aliases.len() {
69        0 => Err(anyhow!(
70            "No signing keys found for identity {}.\n\n\
71             All keys are rotation keys (--next- prefixed) or no keys exist.\n\
72             Run `auths status` to see your identity details, or `auths pair` to link another device.",
73            identity.controller_did
74        )),
75        1 => Ok(signing_aliases[0].as_str().to_string()),
76        _ => {
77            if std::io::stdin().is_terminal() && !is_json_mode() {
78                select_device_key_interactive(&signing_aliases)
79            } else {
80                let alias_list: Vec<&str> = signing_aliases.iter().map(|a| a.as_str()).collect();
81                Err(anyhow!(
82                    "Multiple device keys found. Specify with --device-key.\n\n\
83                     Available aliases: {}",
84                    alias_list.join(", ")
85                ))
86            }
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use auths_sdk::keychain::KeyAlias;
95
96    #[test]
97    fn filter_removes_next_aliases() {
98        let aliases = vec![
99            KeyAlias::new("main").unwrap(),
100            KeyAlias::new("main--next-0").unwrap(),
101            KeyAlias::new("secondary").unwrap(),
102            KeyAlias::new("main--next-1").unwrap(),
103        ];
104        let result = filter_signing_aliases(aliases);
105        let names: Vec<&str> = result.iter().map(|a| a.as_str()).collect();
106        assert_eq!(names, vec!["main", "secondary"]);
107    }
108
109    #[test]
110    fn filter_all_next_returns_empty() {
111        let aliases = vec![
112            KeyAlias::new("main--next-0").unwrap(),
113            KeyAlias::new("main--next-1").unwrap(),
114        ];
115        let result = filter_signing_aliases(aliases);
116        assert!(result.is_empty());
117    }
118}