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, WitnessRef},
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/// A configured witness in node config: where to reach it and its pinned AID.
49#[derive(Debug, Deserialize)]
50pub struct WitnessTomlEntry {
51    /// Witness server URL.
52    pub url: String,
53    /// Witness AID (curve-tagged CESR verkey prefix).
54    pub aid: String,
55}
56
57/// Witness section of the node configuration (TOML-friendly view).
58#[derive(Debug, Deserialize)]
59pub struct WitnessOverride {
60    /// Configured witnesses as `(url, aid)` pairs.
61    #[serde(default)]
62    pub witnesses: Vec<WitnessTomlEntry>,
63
64    /// Minimum witness receipts required (k-of-n threshold).
65    #[serde(default = "default_threshold")]
66    pub threshold: usize,
67
68    /// Per-witness timeout in milliseconds.
69    #[serde(default = "default_timeout_ms")]
70    pub timeout_ms: u64,
71
72    /// Witness policy: `enforce`, `warn`, or `skip`.
73    #[serde(default = "default_policy")]
74    pub policy: String,
75}
76
77fn default_key_alias() -> String {
78    "main".to_string()
79}
80
81fn default_repo_path() -> String {
82    auths_core::paths::auths_home()
83        .map(|p| p.display().to_string())
84        .unwrap_or_else(|_| "~/.auths".to_string())
85}
86
87fn default_preset() -> String {
88    "default".to_string()
89}
90
91fn default_threshold() -> usize {
92    1
93}
94
95fn default_timeout_ms() -> u64 {
96    5000
97}
98
99fn default_policy() -> String {
100    "enforce".to_string()
101}
102
103/// Result of a successful provisioning run.
104#[derive(Debug)]
105pub struct ProvisionResult {
106    /// The controller DID of the newly provisioned identity.
107    pub controller_did: String,
108    /// The keychain alias under which the signing key was stored.
109    pub key_alias: KeyAlias,
110}
111
112/// Errors from the provisioning workflow.
113#[derive(Debug, thiserror::Error)]
114pub enum ProvisionError {
115    /// The platform keychain could not be accessed.
116    #[error("failed to access platform keychain: {0}")]
117    KeychainUnavailable(String),
118
119    /// The identity initialization step failed.
120    #[error("failed to initialize identity: {0}")]
121    IdentityInit(String),
122
123    /// An identity already exists and `force` was not set.
124    #[error("identity already exists (use force=true to overwrite)")]
125    IdentityExists,
126}
127
128/// Check for an existing identity and create one if absent (or if force=true).
129///
130/// Args:
131/// * `config`: The resolved node configuration.
132/// * `force`: Overwrite an existing identity when true.
133/// * `passphrase_provider`: Provider used to encrypt the generated key.
134/// * `keychain`: Platform keychain for key storage.
135/// * `registry`: Pre-initialized registry backend.
136/// * `identity_storage`: Pre-initialized identity storage adapter.
137///
138/// Usage:
139/// ```ignore
140/// let result = enforce_identity_state(
141///     &config, false, passphrase_provider.as_ref(), keychain.as_ref(), registry, identity_storage,
142/// )?;
143/// println!("DID: {}", result.controller_did);
144/// ```
145pub fn enforce_identity_state(
146    config: &NodeConfig,
147    force: bool,
148    passphrase_provider: &dyn PassphraseProvider,
149    keychain: &(dyn KeyStorage + Send + Sync),
150    registry: Arc<dyn RegistryBackend + Send + Sync>,
151    identity_storage: Arc<dyn IdentityStorage + Send + Sync>,
152) -> Result<Option<ProvisionResult>, ProvisionError> {
153    if identity_storage.load_identity().is_ok() && !force {
154        return Ok(None);
155    }
156
157    let witness_config = build_witness_config(config.witness.as_ref());
158
159    let alias = KeyAlias::new_unchecked(&config.identity.key_alias);
160    let (controller_did, key_alias) = initialize_registry_identity(
161        registry,
162        &alias,
163        passphrase_provider,
164        keychain,
165        witness_config.as_ref(),
166        auths_crypto::CurveType::default(),
167    )
168    .map_err(|e| ProvisionError::IdentityInit(e.to_string()))?;
169
170    Ok(Some(ProvisionResult {
171        controller_did: controller_did.into_inner(),
172        key_alias,
173    }))
174}
175
176fn build_witness_config(witness: Option<&WitnessOverride>) -> Option<WitnessConfig> {
177    let w = witness?;
178    if w.witnesses.is_empty() {
179        return None;
180    }
181    let policy = match w.policy.as_str() {
182        "warn" => WitnessPolicy::Warn,
183        "skip" => WitnessPolicy::Skip,
184        _ => WitnessPolicy::Enforce,
185    };
186    let witnesses: Vec<WitnessRef> = w
187        .witnesses
188        .iter()
189        .filter_map(|e| {
190            let url = e.url.parse().ok()?;
191            let aid = auths_keri::Prefix::new(e.aid.clone()).ok()?;
192            Some(WitnessRef {
193                url,
194                aid,
195                operator_info: None,
196            })
197        })
198        .collect();
199    if witnesses.is_empty() {
200        return None;
201    }
202    Some(WitnessConfig {
203        witnesses,
204        threshold: w.threshold,
205        timeout_ms: w.timeout_ms,
206        policy,
207        ..Default::default()
208    })
209}