auths_cli/commands/
key_detect.rs1use 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
35pub 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}