Skip to main content

auths_cli/factories/
storage.rs

1use std::path::Path;
2use std::sync::Arc;
3
4use anyhow::Result;
5use auths_infra_git::GitRepo;
6use auths_sdk::attestation::AttestationSink;
7use auths_sdk::context::AuthsContext;
8use auths_sdk::core_config::EnvironmentConfig;
9use auths_sdk::keychain::get_platform_keychain_with_config;
10use auths_sdk::ports::AttestationSource;
11use auths_sdk::ports::CoreStorageError as StorageError;
12use auths_sdk::ports::IdentityStorage;
13use auths_sdk::ports::RegistryBackend;
14use auths_sdk::ports::SystemClock;
15use auths_sdk::signing::PassphraseProvider;
16use auths_sdk::storage::{
17    GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage,
18};
19
20/// Opens an existing Git repository at the given path.
21///
22/// Args:
23/// * `path`: Filesystem path to the repository root.
24///
25/// Usage:
26/// ```ignore
27/// use auths_cli::factories::storage::open_git_repo;
28///
29/// let repo = open_git_repo(Path::new("/home/user/.auths"))?;
30/// ```
31pub fn open_git_repo(path: &Path) -> Result<GitRepo, StorageError> {
32    GitRepo::open(path)
33}
34
35/// Initializes a new Git repository at the given path.
36///
37/// Args:
38/// * `path`: Filesystem path where the repository will be created.
39///
40/// Usage:
41/// ```ignore
42/// let repo = init_git_repo(Path::new("/tmp/new-repo"))?;
43/// ```
44pub fn init_git_repo(path: &Path) -> Result<GitRepo, StorageError> {
45    GitRepo::init(path)
46}
47
48/// Opens an existing Git repository or initializes a new one.
49///
50/// If the path exists and contains a Git repository, opens it.
51/// If the path exists but is not a Git repository, initializes one.
52/// If the path does not exist, creates directories and initializes.
53///
54/// Args:
55/// * `path`: Filesystem path to open or create a repository at.
56///
57/// Usage:
58/// ```ignore
59/// let repo = ensure_git_repo(Path::new("/data/auths"))?;
60/// ```
61pub fn ensure_git_repo(path: &Path) -> Result<GitRepo, StorageError> {
62    if path.exists() {
63        match GitRepo::open(path) {
64            Ok(repo) => Ok(repo),
65            Err(_) => GitRepo::init(path),
66        }
67    } else {
68        std::fs::create_dir_all(path)
69            .map_err(|e| StorageError::Io(format!("failed to create directory: {}", e)))?;
70        GitRepo::init(path)
71    }
72}
73
74/// Discovers a Git repository starting from the given path, walking up parent directories.
75///
76/// Returns the working directory of the discovered repository.
77///
78/// Args:
79/// * `start_path`: Directory to begin searching from.
80///
81/// Usage:
82/// ```ignore
83/// let repo_root = discover_git_repo(Path::new("."))?;
84/// ```
85pub fn discover_git_repo(start_path: &Path) -> Result<std::path::PathBuf, StorageError> {
86    let repo = git2::Repository::discover(start_path)
87        .map_err(|e| StorageError::not_found(format!("no Git repository found: {}", e)))?;
88    let path: &Path = repo
89        .workdir()
90        .or_else(|| repo.path().parent())
91        .ok_or_else(|| StorageError::Io("could not determine repository path".into()))?;
92    Ok(path.to_path_buf())
93}
94
95/// Builds a canonical `AuthsContext` for CLI commands.
96///
97/// This is the single composition root for all storage and keychain wiring.
98/// All commands that need an `AuthsContext` must use this function instead
99/// of assembling the context inline.
100///
101/// Args:
102/// * `repo_path`: Path to the auths registry Git repository.
103/// * `env_config`: Environment configuration used to select the keychain backend.
104/// * `passphrase_provider`: Optional passphrase provider; `None` uses the keychain default.
105///
106/// Usage:
107/// ```ignore
108/// let ctx = build_auths_context(&repo_path, &env_config, Some(passphrase_provider))?;
109/// ```
110pub fn build_auths_context(
111    repo_path: &Path,
112    env_config: &EnvironmentConfig,
113    passphrase_provider: Option<Arc<dyn PassphraseProvider + Send + Sync>>,
114) -> Result<AuthsContext> {
115    let backend: Arc<dyn RegistryBackend + Send + Sync> = Arc::new(
116        GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(repo_path)),
117    );
118    let identity_storage: Arc<dyn IdentityStorage + Send + Sync> =
119        Arc::new(RegistryIdentityStorage::new(repo_path.to_path_buf()));
120    let attestation_store = Arc::new(RegistryAttestationStorage::new(repo_path));
121    let attestation_sink: Arc<dyn AttestationSink + Send + Sync> =
122        Arc::clone(&attestation_store) as Arc<dyn AttestationSink + Send + Sync>;
123    let attestation_source: Arc<dyn AttestationSource + Send + Sync> =
124        attestation_store as Arc<dyn AttestationSource + Send + Sync>;
125    let key_storage = get_platform_keychain_with_config(env_config)
126        .map_err(|e| anyhow::anyhow!("Failed to initialize keychain: {}", e))?;
127    let mut builder = AuthsContext::builder()
128        .registry(backend)
129        .key_storage(Arc::from(key_storage))
130        .clock(Arc::new(SystemClock))
131        .identity_storage(identity_storage)
132        .attestation_sink(attestation_sink)
133        .attestation_source(attestation_source);
134    builder = builder.agent_signing(crate::factories::build_agent_provider());
135    if let Some(pp) = passphrase_provider {
136        builder = builder.passphrase_provider(pp);
137    }
138    Ok(builder.build())
139}
140
141/// Reads a Git configuration value from the default config.
142///
143/// Args:
144/// * `key`: The Git configuration key (e.g. "gpg.ssh.allowedSignersFile").
145///
146/// Usage:
147/// ```ignore
148/// let value = read_git_config("user.email")?;
149/// ```
150pub fn read_git_config(key: &str) -> Result<Option<String>, StorageError> {
151    let config = git2::Config::open_default()
152        .map_err(|e| StorageError::Io(format!("failed to open git config: {}", e)))?;
153    match config.get_string(key) {
154        Ok(value) => Ok(Some(value)),
155        Err(_) => Ok(None),
156    }
157}