Skip to main content

void_core/sdk/
builder.rs

1//! RepoBuilder — configures and opens a Repo.
2
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use camino::Utf8PathBuf;
7
8use crate::collab::Identity;
9use crate::crypto::KeyVault;
10use crate::support::void_context::{
11    CryptoContext, NetworkConfig, RepoPaths, RepoMeta, SealConfig,
12};
13use crate::{config, Result, VoidContext, VoidError};
14
15use super::Repo;
16
17/// Builder for opening a [`Repo`].
18///
19/// Must provide exactly one key source: `key()`, `content_key()`, `token()`,
20/// or `identity()`.
21pub struct RepoBuilder {
22    path: PathBuf,
23    key: Option<[u8; 32]>,
24    content_key: Option<[u8; 32]>,
25    token: Option<crate::crypto::machine_token::MachineToken>,
26    identity: Option<Identity>,
27    signing_key: Option<ed25519_dalek::SigningKey>,
28    target_shard_size: Option<u64>,
29    remote: Option<Arc<dyn crate::store::RemoteStore>>,
30}
31
32impl RepoBuilder {
33    pub(crate) fn new(path: impl AsRef<Path>) -> Self {
34        Self {
35            path: path.as_ref().to_path_buf(),
36            key: None,
37            content_key: None,
38            token: None,
39            identity: None,
40            signing_key: None,
41            target_shard_size: None,
42            remote: None,
43        }
44    }
45
46    /// Set the repository root key (full read-write access).
47    pub fn key(mut self, key: &[u8; 32]) -> Self {
48        self.key = Some(*key);
49        self
50    }
51
52    /// Set a content key (read-only access to one commit).
53    pub fn content_key(mut self, key: &[u8; 32]) -> Self {
54        self.content_key = Some(*key);
55        self
56    }
57
58    /// Set a machine token (read-only access + signing identity).
59    pub fn token(mut self, token: crate::crypto::machine_token::MachineToken) -> Self {
60        self.token = Some(token);
61        self
62    }
63
64    /// Set an identity for key unwrapping (CLI path).
65    ///
66    /// The builder will find the manifest, ECIES-unwrap the repo key using
67    /// the identity's recipient key, and extract the signing key automatically.
68    pub fn identity(mut self, identity: Identity) -> Self {
69        self.identity = Some(identity);
70        self
71    }
72
73    /// Set a signing key for commit operations.
74    pub fn signing_key(mut self, key: ed25519_dalek::SigningKey) -> Self {
75        self.signing_key = Some(key);
76        self
77    }
78
79    /// Override the target shard size (default: from config or 100KB).
80    pub fn target_shard_size(mut self, size: u64) -> Self {
81        self.target_shard_size = Some(size);
82        self
83    }
84
85    /// Set a remote store for push/pull operations.
86    ///
87    /// When set, `Repo::push()` and `Repo::pull()` use this remote instead
88    /// of creating an IpfsStore from config. Pass a `NetworkRemoteStore`
89    /// wrapping a `VoidNode` for daemon-backed networking.
90    pub fn remote(mut self, remote: Arc<dyn crate::store::RemoteStore>) -> Self {
91        self.remote = Some(remote);
92        self
93    }
94
95    /// Build the Repo. Finds `.void`, loads config, creates vault.
96    pub fn build(self) -> Result<Repo> {
97        // Find .void directory
98        let void_dir = find_void_dir(&self.path)?;
99        let root = void_dir
100            .parent()
101            .ok_or_else(|| VoidError::NotFound("void_dir has no parent".into()))?
102            .to_path_buf();
103
104        // Build vault from the provided key source
105        let key_sources = [
106            self.key.is_some(),
107            self.content_key.is_some(),
108            self.token.is_some(),
109            self.identity.is_some(),
110        ]
111        .iter()
112        .filter(|x| **x)
113        .count();
114
115        if key_sources == 0 {
116            return Err(VoidError::Unauthorized(
117                "no key provided — use key(), content_key(), token(), or identity()".into(),
118            ));
119        }
120        if key_sources > 1 {
121            return Err(VoidError::Unauthorized(
122                "multiple key sources provided — use exactly one".into(),
123            ));
124        }
125
126        let (vault, signing_key) = if let Some(key) = self.key {
127            let vault = KeyVault::new(key)?;
128            (vault, self.signing_key.map(Arc::new))
129        } else if let Some(ck) = self.content_key {
130            let vault = KeyVault::from_content_key_bytes(ck);
131            (vault, self.signing_key.map(Arc::new))
132        } else if let Some(ref token) = self.token {
133            let vault = token.to_vault()?;
134            let sk = token.to_signing_key()?;
135            (vault, Some(Arc::new(sk)))
136        } else if let Some(ref identity) = self.identity {
137            // ECIES unwrap: identity → manifest → repo key → vault
138            let repo_key = crate::collab::manifest::load_repo_key(&void_dir, Some(identity))?;
139            let vault = KeyVault::new(*repo_key.as_bytes())?;
140            let sk = identity.signing_key().clone();
141            (vault, Some(Arc::new(sk)))
142        } else {
143            unreachable!()
144        };
145
146        let vault = Arc::new(vault);
147
148        // Load config
149        let root_utf8 = Utf8PathBuf::try_from(root)
150            .map_err(|e| VoidError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;
151        let void_dir_utf8 = Utf8PathBuf::try_from(void_dir)
152            .map_err(|e| VoidError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;
153
154        let cfg = config::load(void_dir_utf8.as_std_path())?;
155
156        let secret = config::load_repo_secret(void_dir_utf8.as_std_path(), &vault)
157            .unwrap_or_else(|_| crate::crypto::RepoSecret::new([0u8; 32]));
158
159        let mut seal_cfg = SealConfig::from(&cfg);
160        if let Some(size) = self.target_shard_size {
161            seal_cfg.target_shard_size = size;
162            seal_cfg.max_shard_size = size * 3 / 2;
163        }
164
165        let ctx = VoidContext {
166            paths: RepoPaths {
167                root: root_utf8,
168                void_dir: void_dir_utf8.clone(),
169                workspace_dir: void_dir_utf8,
170            },
171            crypto: CryptoContext {
172                vault,
173                epoch: 0,
174                signing_key,
175                nostr_pubkey: None,
176            },
177            repo: RepoMeta {
178                id: cfg.repo_id.clone(),
179                name: cfg.repo_name.clone(),
180                secret,
181            },
182            seal: seal_cfg,
183            network: NetworkConfig::from(&cfg),
184            user: cfg.user.clone(),
185        };
186
187        Ok(Repo::from_context_with_remote(ctx, self.remote))
188    }
189}
190
191/// Walk up from `start` to find a `.void` directory.
192fn find_void_dir(start: &Path) -> Result<PathBuf> {
193    let mut current = if start.is_absolute() {
194        start.to_path_buf()
195    } else {
196        std::env::current_dir()
197            .map_err(|e| VoidError::Io(e))?
198            .join(start)
199    };
200
201    loop {
202        let candidate = current.join(".void");
203        if candidate.is_dir() {
204            return Ok(candidate);
205        }
206        if !current.pop() {
207            return Err(VoidError::NotFound(
208                "no .void directory found in any parent directory".into(),
209            ));
210        }
211    }
212}