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::{IdentityDID, 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 delegated_primary_device_did(repo_path: &Path, root_did: &IdentityDID) -> Option<IdentityDID> {
40 let root_prefix = auths_sdk::keri::parse_did_keri(root_did.as_str()).ok()?;
41 let backend = auths_sdk::storage::GitRegistryBackend::from_config_unchecked(
42 auths_sdk::storage::RegistryConfig::single_tenant(repo_path),
43 );
44 let devices =
45 auths_sdk::keri::delegation::list_delegated_devices(&backend, &root_prefix).ok()?;
46 let device = devices.iter().find(|d| !d.revoked)?;
47 IdentityDID::from_prefix(device.device_prefix.as_str()).ok()
48}
49
50fn select_device_key_interactive(aliases: &[KeyAlias]) -> Result<String> {
51 let display_items: Vec<&str> = aliases.iter().map(|a| a.as_str()).collect();
52
53 let selection = Select::new()
54 .with_prompt("Select signing key")
55 .items(&display_items)
56 .default(0)
57 .interact()
58 .context("Key selection cancelled")?;
59
60 Ok(aliases[selection].as_str().to_string())
61}
62
63pub fn auto_detect_device_key(
78 repo_opt: Option<&Path>,
79 env_config: &EnvironmentConfig,
80) -> Result<String> {
81 let repo_path =
82 auths_sdk::storage_layout::resolve_repo_path(repo_opt.map(|p| p.to_path_buf()))?;
83 let identity_storage = RegistryIdentityStorage::new(repo_path.clone());
84 let identity = identity_storage
85 .load_identity()
86 .map_err(|_| anyhow!("No identity found. Run `auths init` to get started."))?;
87
88 let keychain = auths_sdk::keychain::get_platform_keychain_with_config(env_config)
89 .context("Failed to access keychain")?;
90 let signing_identity = delegated_primary_device_did(&repo_path, &identity.controller_did)
94 .unwrap_or_else(|| identity.controller_did.clone());
95 let aliases = keychain
96 .list_aliases_for_identity(&signing_identity)
97 .map_err(|e| anyhow!("Failed to list key aliases: {e}"))?;
98
99 let signing_aliases = filter_signing_aliases(aliases);
100
101 match signing_aliases.len() {
102 0 => Err(anyhow!(
103 "No signing keys found for identity {}.\n\n\
104 All keys are rotation keys (--next- prefixed) or no keys exist.\n\
105 Run `auths status` to see your identity details, or `auths pair` to link another device.",
106 identity.controller_did
107 )),
108 1 => Ok(signing_aliases[0].as_str().to_string()),
109 _ => {
110 if std::io::stdin().is_terminal() && !is_json_mode() {
111 select_device_key_interactive(&signing_aliases)
112 } else {
113 let alias_list: Vec<&str> = signing_aliases.iter().map(|a| a.as_str()).collect();
114 Err(anyhow!(
115 "Multiple device keys found. Specify with --device-key.\n\n\
116 Available aliases: {}",
117 alias_list.join(", ")
118 ))
119 }
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use auths_sdk::keychain::KeyAlias;
128
129 #[test]
130 fn filter_removes_next_aliases() {
131 let aliases = vec![
132 KeyAlias::new("main").unwrap(),
133 KeyAlias::new("main--next-0").unwrap(),
134 KeyAlias::new("secondary").unwrap(),
135 KeyAlias::new("main--next-1").unwrap(),
136 ];
137 let result = filter_signing_aliases(aliases);
138 let names: Vec<&str> = result.iter().map(|a| a.as_str()).collect();
139 assert_eq!(names, vec!["main", "secondary"]);
140 }
141
142 #[test]
143 fn filter_all_next_returns_empty() {
144 let aliases = vec![
145 KeyAlias::new("main--next-0").unwrap(),
146 KeyAlias::new("main--next-1").unwrap(),
147 ];
148 let result = filter_signing_aliases(aliases);
149 assert!(result.is_empty());
150 }
151}