Skip to main content

auths_sdk/workflows/
provision.rs

1//! Declarative provisioning workflow for enterprise node setup.
2//!
3//! Receives a pre-deserialized `NodeConfig` and reconciles the node's identity
4//! state. All I/O (TOML loading, env expansion) is handled by the caller.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8
9use auths_core::signing::PassphraseProvider;
10use auths_core::storage::keychain::{KeyAlias, KeyStorage};
11use auths_id::{
12    identity::initialize::initialize_registry_identity,
13    ports::registry::RegistryBackend,
14    storage::identity::IdentityStorage,
15    witness_config::{WitnessConfig, WitnessPolicy},
16};
17use serde::Deserialize;
18
19/// Top-level node configuration for declarative provisioning.
20#[derive(Debug, Deserialize)]
21pub struct NodeConfig {
22    /// Identity configuration section.
23    pub identity: IdentityConfig,
24    /// Optional witness configuration section.
25    pub witness: Option<WitnessOverride>,
26}
27
28/// Identity section of the node configuration.
29#[derive(Debug, Deserialize)]
30pub struct IdentityConfig {
31    /// Key alias for storing the generated private key.
32    #[serde(default = "default_key_alias")]
33    pub key_alias: String,
34
35    /// Path to the Git repository storing identity data.
36    #[serde(default = "default_repo_path")]
37    pub repo_path: String,
38
39    /// Storage layout preset (default, radicle, gitoxide).
40    #[serde(default = "default_preset")]
41    pub preset: String,
42
43    /// Optional metadata key-value pairs attached to the identity.
44    #[serde(default)]
45    pub metadata: HashMap<String, String>,
46}
47
48/// Witness section of the node configuration (TOML-friendly view).
49#[derive(Debug, Deserialize)]
50pub struct WitnessOverride {
51    /// Witness server URLs.
52    #[serde(default)]
53    pub urls: Vec<String>,
54
55    /// Minimum witness receipts required (k-of-n threshold).
56    #[serde(default = "default_threshold")]
57    pub threshold: usize,
58
59    /// Per-witness timeout in milliseconds.
60    #[serde(default = "default_timeout_ms")]
61    pub timeout_ms: u64,
62
63    /// Witness policy: `enforce`, `warn`, or `skip`.
64    #[serde(default = "default_policy")]
65    pub policy: String,
66}
67
68fn default_key_alias() -> String {
69    "main".to_string()
70}
71
72fn default_repo_path() -> String {
73    auths_core::paths::auths_home()
74        .map(|p| p.display().to_string())
75        .unwrap_or_else(|_| "~/.auths".to_string())
76}
77
78fn default_preset() -> String {
79    "default".to_string()
80}
81
82fn default_threshold() -> usize {
83    1
84}
85
86fn default_timeout_ms() -> u64 {
87    5000
88}
89
90fn default_policy() -> String {
91    "enforce".to_string()
92}
93
94/// Result of a successful provisioning run.
95#[derive(Debug)]
96pub struct ProvisionResult {
97    /// The controller DID of the newly provisioned identity.
98    pub controller_did: String,
99    /// The keychain alias under which the signing key was stored.
100    pub key_alias: KeyAlias,
101}
102
103/// Errors from the provisioning workflow.
104#[derive(Debug, thiserror::Error)]
105pub enum ProvisionError {
106    /// The platform keychain could not be accessed.
107    #[error("failed to access platform keychain: {0}")]
108    KeychainUnavailable(String),
109
110    /// The identity initialization step failed.
111    #[error("failed to initialize identity: {0}")]
112    IdentityInit(String),
113
114    /// An identity already exists and `force` was not set.
115    #[error("identity already exists (use force=true to overwrite)")]
116    IdentityExists,
117}
118
119/// Check for an existing identity and create one if absent (or if force=true).
120///
121/// Args:
122/// * `config`: The resolved node configuration.
123/// * `force`: Overwrite an existing identity when true.
124/// * `passphrase_provider`: Provider used to encrypt the generated key.
125/// * `keychain`: Platform keychain for key storage.
126/// * `registry`: Pre-initialized registry backend.
127/// * `identity_storage`: Pre-initialized identity storage adapter.
128///
129/// Usage:
130/// ```ignore
131/// let result = enforce_identity_state(
132///     &config, false, passphrase_provider.as_ref(), keychain.as_ref(), registry, identity_storage,
133/// )?;
134/// println!("DID: {}", result.controller_did);
135/// ```
136pub fn enforce_identity_state(
137    config: &NodeConfig,
138    force: bool,
139    passphrase_provider: &dyn PassphraseProvider,
140    keychain: &(dyn KeyStorage + Send + Sync),
141    registry: Arc<dyn RegistryBackend + Send + Sync>,
142    identity_storage: Arc<dyn IdentityStorage + Send + Sync>,
143) -> Result<Option<ProvisionResult>, ProvisionError> {
144    if identity_storage.load_identity().is_ok() && !force {
145        return Ok(None);
146    }
147
148    let witness_config = build_witness_config(config.witness.as_ref());
149
150    let alias = KeyAlias::new_unchecked(&config.identity.key_alias);
151    let (controller_did, key_alias) = initialize_registry_identity(
152        registry,
153        &alias,
154        passphrase_provider,
155        keychain,
156        witness_config.as_ref(),
157    )
158    .map_err(|e| ProvisionError::IdentityInit(e.to_string()))?;
159
160    Ok(Some(ProvisionResult {
161        controller_did: controller_did.into_inner(),
162        key_alias,
163    }))
164}
165
166fn build_witness_config(witness: Option<&WitnessOverride>) -> Option<WitnessConfig> {
167    let w = witness?;
168    if w.urls.is_empty() {
169        return None;
170    }
171    let policy = match w.policy.as_str() {
172        "warn" => WitnessPolicy::Warn,
173        "skip" => WitnessPolicy::Skip,
174        _ => WitnessPolicy::Enforce,
175    };
176    Some(WitnessConfig {
177        witness_urls: w.urls.iter().filter_map(|u| u.parse().ok()).collect(),
178        threshold: w.threshold,
179        timeout_ms: w.timeout_ms,
180        policy,
181    })
182}