mod env;
pub(crate) mod kit;
mod rekey;
pub use env::{Environment, MintedToken, Revocation, TokenInfo};
pub use kit::{KIT_VERSION, Kit, KitKind, default_file_name};
pub use rekey::{PendingRekey, RekeyAborted, RekeyOutcome};
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
use crate::client::{Api, Events, NoEvents, Progress, Warning, now};
use crate::keys::{NodeKey, seal_child_key};
use crate::proto::children::{ChildEntry, ChildMode};
use crate::proto::codec::FormatError;
use crate::proto::ids::B64;
use crate::proto::path::{EnvPath, Segment};
use crate::audit::{self, AuditReport};
use crate::error::{Error, code};
use crate::state::{LocalState, ProjectState, StateStore, TreeNode};
use crate::store::{KeyName, KeyStore};
use crate::vault::{DAY, Handle};
pub type Connector = Arc<dyn Fn(&str) -> Result<Api, Error> + Send + Sync>;
#[derive(Debug, Clone, Copy)]
pub struct GrantsSubtreeOwnership;
#[derive(Debug, Clone, Copy)]
pub struct RetiresAllTokens;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ProjectInfo {
pub path: EnvPath,
pub server: String,
pub vault_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Discovery {
pub root: EnvPath,
pub found: Vec<EnvPath>,
pub unreachable: Vec<EnvPath>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Removal {
pub deleted: Vec<EnvPath>,
pub parent_not_updated: Option<EnvPath>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Repair {
pub created: bool,
pub children: usize,
pub lost: Vec<EnvPath>,
}
pub struct Owner {
keys: Arc<dyn KeyStore>,
store: Arc<dyn StateStore>,
events: Arc<dyn Events>,
connect: Connector,
apis: HashMap<String, Api>,
state: LocalState,
}
impl std::fmt::Debug for Owner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Owner")
.field("projects", &self.state.projects.keys().collect::<Vec<_>>())
.finish_non_exhaustive()
}
}
fn default_connector() -> Connector {
#[cfg(feature = "http")]
{
Arc::new(|server: &str| {
crate::client::ClientBuilder::new(server)
.build()
.map_err(Error::build)
})
}
#[cfg(not(feature = "http"))]
{
Arc::new(|server: &str| {
Err(Error::other(format!(
"no transport to reach {server}: build galata-vault with the `http` feature, or give the owner a connector"
)))
})
}
}
fn chain_end(chain: Vec<(EnvPath, NodeKey)>) -> Result<NodeKey, Error> {
chain
.into_iter()
.last()
.map(|(_, k)| k)
.ok_or_else(|| Error::other("a key chain includes its target"))
}
fn children_error(e: impl std::fmt::Display) -> Error {
Error::other(e.to_string())
}
fn announces_expiry(vault: &Handle) -> bool {
vault.status().is_some_and(|s| s.expires_at.is_some())
}
fn server_expires(api: &Api, vault: &Handle) -> bool {
let announced = matches!(
api.capabilities(),
Ok(Some(caps)) if caps.idle_expiry_days.is_some()
);
announced || announces_expiry(vault)
}
fn levenshtein(a: &str, b: &str) -> usize {
let b: Vec<char> = b.chars().collect();
let mut row: Vec<usize> = (0..=b.len()).collect();
for (i, ca) in a.chars().enumerate() {
let mut prev = row[0];
row[0] = i + 1;
for (j, cb) in b.iter().enumerate() {
let cur = row[j + 1];
row[j + 1] = (prev + usize::from(ca != *cb)).min(row[j] + 1).min(cur + 1);
prev = cur;
}
}
row[b.len()]
}
fn closest<'a>(target: &str, candidates: impl IntoIterator<Item = &'a str>) -> Option<&'a str> {
let limit = (target.len() / 3).max(2);
candidates
.into_iter()
.map(|c| (levenshtein(target, c), c))
.filter(|(d, _)| *d <= limit)
.min()
.map(|(_, c)| c)
}
impl Owner {
pub fn open(keys: Arc<dyn KeyStore>, store: Arc<dyn StateStore>) -> Result<Owner, Error> {
let state = store.load().map_err(Error::store)?;
Ok(Owner {
keys,
store,
events: Arc::new(NoEvents),
connect: default_connector(),
apis: HashMap::new(),
state,
})
}
pub fn with_events(mut self, events: Arc<dyn Events>) -> Owner {
self.events = events;
self.apis.clear();
self
}
pub fn with_connector(
mut self,
connect: impl Fn(&str) -> Result<Api, Error> + Send + Sync + 'static,
) -> Owner {
self.connect = Arc::new(connect);
self.apis.clear();
self
}
pub fn events(&self) -> &Arc<dyn Events> {
&self.events
}
pub fn state(&self) -> &LocalState {
&self.state
}
pub fn save(&self) -> Result<(), Error> {
self.store.save(&self.state).map_err(Error::store)
}
pub fn reload(&mut self) -> Result<(), Error> {
self.state = self.store.load().map_err(Error::store)?;
Ok(())
}
pub fn api(&mut self, server: &str) -> Result<Api, Error> {
if let Some(api) = self.apis.get(server) {
return Ok(api.clone());
}
let api = (self.connect)(server)?.with_events(self.events.clone());
self.apis.insert(server.to_owned(), api.clone());
Ok(api)
}
fn api_for(&mut self, path: &EnvPath) -> Result<Api, Error> {
let server = self.state.project(path)?.server.clone();
self.api(&server)
}
fn held_key(&self, path: &EnvPath) -> Result<NodeKey, Error> {
let stored = self
.keys
.get(&KeyName::Node(path.clone()))
.map_err(Error::store)?
.ok_or_else(|| {
Error::local(
code::NO_KEY,
format!("the key for {path} is missing from the credential store"),
)
})?;
NodeKey::parse(&stored).map_err(|e| match e {
FormatError::UnknownVersion { found } => Error::other(format!(
"the stored key for {path} carries version {found}, which this build does not know"
)),
_ => Error::other(format!("the stored key for {path} is damaged")),
})
}
fn store_key(&self, path: &EnvPath, key: &NodeKey) -> Result<(), Error> {
self.keys
.set(&KeyName::Node(path.clone()), &key.encode())
.map_err(Error::store)
}
pub fn closest_known(&self, path: &EnvPath) -> Option<EnvPath> {
let known = self.state.known_paths();
closest(&path.to_string(), known.iter().map(String::as_str)).and_then(|s| s.parse().ok())
}
pub fn require_known(&self, path: &EnvPath) -> Result<(), Error> {
let project_known = self
.state
.projects
.contains_key(path.project_name().as_str());
if project_known && self.state.project(path)?.knows(path) {
return Ok(());
}
let message = if project_known {
format!("unknown environment {path}")
} else {
format!("unknown project {}", path.project_name())
};
Err(Error::UnknownPath {
path: path.to_string(),
suggestion: self.closest_known(path).map(|p| p.to_string()),
project_known,
message,
})
}
pub fn tree(&self, project: &EnvPath) -> Result<&BTreeMap<String, TreeNode>, Error> {
Ok(&self.state.project(project)?.tree)
}
fn chain(&self, api: &Api, path: &EnvPath) -> Result<Vec<(EnvPath, NodeKey)>, Error> {
let project = self.state.project(path)?.clone();
let root = project.held_root_for(path).ok_or_else(|| {
Error::local(
code::NO_KEY,
format!("no key is held for {path} or any node above it"),
)
})?;
let mut key = self.held_key(&root)?;
let mut here = root.clone();
let mut out = vec![(here.clone(), key.clone())];
for seg in &path.segments()[root.depth()..] {
let child = here
.child(seg.clone())
.map_err(|e| Error::local(code::INVALID_PATH, format!("{here}/{seg}: {e}")))?;
let sealed = project
.tree
.get(&child.to_string())
.is_some_and(|n| n.sealed);
key = if sealed {
let parent = Handle::open_owner(api, &key, &here.to_string())?;
let (record, _) = parent.children()?;
let entry = record.get(seg).ok_or_else(|| {
Error::other(format!("{here}'s children record no longer lists {seg}"))
})?;
match &entry.mode {
ChildMode::Derived => key.child(seg),
ChildMode::Sealed { key_ct } => {
parent.owner()?.open_child_key(&key_ct.0).map_err(|e| {
Error::key(e, format!("{child}'s sealed key does not open"))
})?
}
}
} else {
key.child(seg)
};
here = child;
out.push((here.clone(), key.clone()));
}
Ok(out)
}
pub fn environment(&mut self, path: &EnvPath) -> Result<Environment, Error> {
self.require_known(path)?;
let api = self.api_for(path)?;
let mut chain = self.chain(&api, path)?;
let key = chain
.pop()
.map(|(_, k)| k)
.ok_or_else(|| Error::other("a key chain includes its target"))?;
let vault = Handle::open_owner(&api, &key, &path.to_string())?;
let id = vault.vault_id.to_hex();
if let Some(pins) = self.state.pins.get(&id) {
vault.set_pins(pins.clone())?;
}
self.state.touched.insert(id.clone(), now());
let ancestors: Vec<String> = chain
.iter()
.map(|(_, k)| k.owner().vault_id().to_hex())
.collect();
if server_expires(&api, &vault) {
self.state.no_expiry.remove(&id);
for a in &ancestors {
self.state.no_expiry.remove(a);
}
self.keep_alive(&api, &chain);
} else {
self.state.no_expiry.insert(id);
self.state.no_expiry.extend(ancestors);
}
Environment::new(vault, path.clone())
}
pub fn remember(&mut self, env: &Environment) {
self.state.pins.insert(env.vault_id(), env.pins());
}
pub fn close(&mut self, env: Environment) -> Result<(), Error> {
self.remember(&env);
drop(env);
self.save()
}
pub fn audit(&mut self, env: &Environment, earlier: usize) -> Result<AuditReport, Error> {
audit::verify_recorded(env.handle(), &mut self.state, earlier)
}
fn keep_alive(&mut self, api: &Api, ancestors: &[(EnvPath, NodeKey)]) {
let t = now();
for (path, key) in ancestors {
let id = key.owner().vault_id().to_hex();
if self.state.no_expiry.contains(&id)
|| self
.state
.touched
.get(&id)
.is_some_and(|last| t - last < DAY)
{
continue;
}
match Handle::open_owner(api, key, &path.to_string()) {
Ok(vault) => {
if !server_expires(api, &vault) {
self.state.no_expiry.insert(id.clone());
}
self.state.touched.insert(id, t);
}
Err(e) => self.events.warning(&Warning::KeepAliveFailed {
path: path.to_string(),
message: e.to_string(),
}),
}
}
}
fn discover_from(
&mut self,
api: &Api,
root: &EnvPath,
key: NodeKey,
) -> Result<Vec<EnvPath>, Error> {
let root_sealed = self
.state
.project(root)?
.tree
.get(&root.to_string())
.is_some_and(|n| n.sealed);
let mut found: BTreeMap<String, TreeNode> = BTreeMap::new();
let mut unreachable: Vec<EnvPath> = Vec::new();
let mut queue: Vec<(EnvPath, NodeKey, bool, Option<NodeKey>)> =
vec![(root.clone(), key, root_sealed, None)];
while let Some((path, key, sealed, parent_key)) = queue.pop() {
let opened = match Handle::open_owner(api, &key, &path.to_string()) {
Ok(v) => Ok((v, key.clone(), sealed)),
Err(e) => match self.relink(api, &path, &key, parent_key.as_ref()) {
Some((v, held)) => Ok((v, held, true)),
None => Err(e),
},
};
let (vault, key, sealed) = match opened {
Ok(found) => found,
Err(e) if &path == root => return Err(e),
Err(e) => {
self.events.warning(&Warning::Unreachable {
path: path.to_string(),
message: e.to_string(),
vault_missing: matches!(e, Error::VaultNotFound { .. }),
});
found.insert(
path.to_string(),
TreeNode::new(key.owner().vault_id(), sealed),
);
unreachable.push(path);
continue;
}
};
self.state.touched.insert(vault.vault_id.to_hex(), now());
found.insert(path.to_string(), TreeNode::new(vault.vault_id, sealed));
let (record, _) = vault.children()?;
for entry in record.children() {
let child = match path.child(entry.seg.clone()) {
Ok(c) => c,
Err(e) => {
self.events.warning(&Warning::ChildTooDeep {
parent: path.to_string(),
message: e.to_string(),
});
continue;
}
};
let next = match &entry.mode {
ChildMode::Derived => (child, key.child(&entry.seg), false, Some(key.clone())),
ChildMode::Sealed { key_ct } => {
match vault.owner()?.open_child_key(&key_ct.0) {
Ok(k) => (child, k, true, Some(key.clone())),
Err(e) => {
self.events.warning(&Warning::SealedKeyUnopenable {
path: child.to_string(),
message: e.to_string(),
});
continue;
}
}
}
};
queue.push(next);
}
}
let project = self.state.project_mut(root)?;
project.tree.retain(|k, _| {
let Ok(p) = k.parse::<EnvPath>() else {
return false;
};
!p.starts_with(root) || unreachable.iter().any(|u| &p != u && p.starts_with(u))
});
project.tree.extend(found);
Ok(unreachable)
}
fn relink(
&self,
api: &Api,
path: &EnvPath,
stale: &NodeKey,
parent_key: Option<&NodeKey>,
) -> Option<(Handle, NodeKey)> {
let parent_key = parent_key?;
let parent = path.parent()?;
let stored = self.keys.get(&KeyName::Node(path.clone())).ok()??;
let held = NodeKey::parse(&stored).ok()?;
if held.owner().vault_id() == stale.owner().vault_id() {
return None;
}
let vault = Handle::open_owner(api, &held, &path.to_string()).ok()?;
let relinked = Handle::open_owner(api, parent_key, &parent.to_string())
.and_then(|pv| pv.seal_child(path.last(), &held));
match relinked {
Ok(()) => self.events.progress(&Progress::Relinked {
path: path.to_string(),
parent: parent.to_string(),
}),
Err(e) => self.events.warning(&Warning::RelinkFailed {
path: path.to_string(),
parent: parent.to_string(),
message: e.to_string(),
}),
}
Some((vault, held))
}
pub fn refresh(&mut self, project: &EnvPath) -> Result<(), Error> {
let held: Vec<EnvPath> = self
.state
.project(project)?
.held
.iter()
.filter_map(|h| h.parse().ok())
.collect();
if held.is_empty() {
return Err(Error::local(
code::NO_KEY,
format!("no key is held for any node of {project}"),
));
}
let api = self.api_for(project)?;
for root in held {
let key = self.held_key(&root)?;
self.discover_from(&api, &root, key)?;
}
Ok(())
}
pub fn discover(&mut self, root: &EnvPath) -> Result<Discovery, Error> {
self.require_known(root)?;
let api = self.api_for(root)?;
let key = chain_end(self.chain(&api, root)?)?;
let unreachable = self.discover_from(&api, root, key)?;
let found = self.found_below(root)?;
self.save()?;
Ok(Discovery {
root: root.clone(),
found,
unreachable,
})
}
fn found_below(&self, root: &EnvPath) -> Result<Vec<EnvPath>, Error> {
Ok(self
.state
.project(root)?
.tree
.keys()
.filter_map(|k| k.parse::<EnvPath>().ok())
.filter(|p| p.starts_with(root))
.collect())
}
pub fn begin_init(&mut self, project: &str, server: &str) -> Result<PendingInit<'_>, Error> {
let seg = Segment::new(project).map_err(|e| {
Error::local(
code::INVALID_PATH,
format!("{project:?} is not a project name: {e}"),
)
})?;
let path = EnvPath::project(seg);
let server = crate::proto::url::validate_server_url(server)
.map_err(|e| Error::local(code::INVALID_SERVER, e))?;
if self.state.projects.contains_key(project) {
return Err(Error::local(
code::PROJECT_EXISTS,
format!("a project named {project} is already configured here"),
));
}
let key = NodeKey::generate();
let kit = Kit::new(KitKind::Recovery, path.clone(), &server, &key);
Ok(PendingInit {
owner: self,
path,
server,
key,
kit,
})
}
pub fn env_add(&mut self, path: &EnvPath) -> Result<(), Error> {
let Some(parent) = path.parent() else {
return Err(Error::local(
code::IS_PROJECT,
format!("{path} is a project"),
));
};
self.require_known(&parent)?;
if self.state.project(path)?.knows(path) {
return Err(Error::local(
code::ENV_EXISTS,
format!("{path} already exists"),
));
}
let api = self.api_for(path)?;
let chain = self.chain(&api, &parent)?;
let Some((_, parent_key)) = chain.last().cloned() else {
return Err(Error::other("a key chain includes its target"));
};
self.keep_alive(&api, &chain[..chain.len() - 1]);
let parent_vault = Handle::open_owner(&api, &parent_key, &parent.to_string())?;
let seg = path.last().clone();
if parent_vault.children()?.0.get(&seg).is_some() {
return Err(Error::local(
code::ENV_EXISTS,
format!(
"{path} already exists (the known tree of {} is out of date)",
path.project_name()
),
));
}
let key = parent_key.child(&seg);
Handle::create(&api, &key, &path.to_string())?;
parent_vault.update_children(|r| {
if r.get(&seg).is_none() {
r.insert(ChildEntry::new(seg.clone(), now(), ChildMode::Derived))
.map_err(children_error)?;
}
Ok(())
})?;
let vault_id = key.owner().vault_id();
self.state
.project_mut(path)?
.tree
.insert(path.to_string(), TreeNode::new(vault_id, false));
self.state.touched.insert(vault_id.to_hex(), now());
self.save()
}
pub fn env_remove(&mut self, path: &EnvPath, recursive: bool) -> Result<Removal, Error> {
self.require_known(path)?;
let api = self.api_for(path)?;
let chain = self.chain(&api, path)?;
let Some((_, key)) = chain.last().cloned() else {
return Err(Error::other("a key chain includes its target"));
};
self.discover_from(&api, path, key)?;
let below = self.state.project(path)?.descendants(path);
if !below.is_empty() && !recursive {
let names: Vec<String> = below.iter().map(ToString::to_string).collect();
return Err(Error::local(
code::HAS_CHILDREN,
format!("{path} has environments below it ({})", names.join(", ")),
));
}
let mut deleted = Vec::new();
for node in below.iter().chain(std::iter::once(path)) {
let node_key = chain_end(self.chain(&api, node)?)?;
match Handle::open_owner(&api, &node_key, &node.to_string()) {
Ok(v) => {
v.delete_vault()?;
let id = v.vault_id.to_hex();
self.state.pins.remove(&id);
self.state.audit.remove(&id);
}
Err(e) => self.events.warning(&Warning::Unreachable {
path: node.to_string(),
message: e.to_string(),
vault_missing: matches!(e, Error::VaultNotFound { .. }),
}),
}
let p = self.state.project_mut(node)?;
p.tree.remove(&node.to_string());
if let Some(i) = p.held.iter().position(|h| h == &node.to_string()) {
p.held.remove(i);
self.keys
.delete(&KeyName::Node(node.clone()))
.map_err(Error::store)?;
}
self.events.progress(&Progress::Deleted {
path: node.to_string(),
});
deleted.push(node.clone());
}
let mut parent_not_updated = None;
if let Some(parent) = path.parent() {
if chain.len() >= 2 {
let (_, parent_key) = &chain[chain.len() - 2];
let seg = path.last().clone();
Handle::open_owner(&api, parent_key, &parent.to_string())?.update_children(
|r| {
let _ = r.remove(&seg);
Ok(())
},
)?;
} else {
parent_not_updated = Some(parent);
}
} else {
self.state.projects.remove(path.project_name().as_str());
}
self.save()?;
Ok(Removal {
deleted,
parent_not_updated,
})
}
pub fn env_repair(&mut self, path: &EnvPath) -> Result<Repair, Error> {
self.require_known(path)?;
let api = self.api_for(path)?;
let key = chain_end(self.chain(&api, path)?)?;
let created = Handle::create(&api, &key, &path.to_string())?;
let vault = Handle::open_owner(&api, &key, &path.to_string())?;
if created {
self.state.pins.remove(&vault.vault_id.to_hex());
self.state.audit.remove(&vault.vault_id.to_hex());
}
let owner_box = vault.owner()?.box_pub();
let project = self.state.project(path)?.clone();
let mut entries = Vec::new();
let mut lost = Vec::new();
for child in project.children_of(path) {
let Some(node) = project.tree.get(&child.to_string()) else {
continue;
};
let mode = if node.sealed {
match self
.keys
.get(&KeyName::Node(child.clone()))
.map_err(Error::store)?
{
Some(k) => {
let k = NodeKey::parse(&k).map_err(|_| {
Error::other(format!("the stored key for {child} is damaged"))
})?;
let key_ct = seal_child_key(&owner_box, &vault.vault_id, &k)
.map_err(|e| Error::key(e, format!("the sealed key for {child}")))?;
ChildMode::Sealed {
key_ct: B64(key_ct),
}
}
None => {
lost.push(child);
continue;
}
}
} else {
ChildMode::Derived
};
entries.push(ChildEntry::new(child.last().clone(), now(), mode));
}
vault.update_children(|r| {
for e in &entries {
if r.get(&e.seg).is_none() {
r.insert(e.clone()).map_err(children_error)?;
}
}
Ok(())
})?;
self.state.touched.insert(vault.vault_id.to_hex(), now());
drop(vault);
self.save()?;
Ok(Repair {
created,
children: entries.len(),
lost,
})
}
pub fn export_kit(&mut self, path: &EnvPath, _: GrantsSubtreeOwnership) -> Result<Kit, Error> {
self.require_known(path)?;
let api = self.api_for(path)?;
let key = chain_end(self.chain(&api, path)?)?;
let server = self.state.project(path)?.server.clone();
let kit = Kit::new(KitKind::Delegation, path.clone(), &server, &key);
self.save()?;
Ok(kit)
}
pub fn import(&mut self, kit: &Kit) -> Result<Discovery, Error> {
let path = kit.path().clone();
let key = kit.key()?;
let name = path.project_name().to_string();
let vault_id = key.owner().vault_id();
let api = self.api(kit.server())?;
let fresh = match self.state.projects.get(&name) {
Some(p) if p.server != kit.server() => {
return Err(Error::local(
code::SERVER_MISMATCH,
format!(
"the project {name} here is pinned to {}, but the kit is for {}",
p.server,
kit.server()
),
));
}
Some(p) => {
if p.tree
.get(&path.to_string())
.is_some_and(|n| n.vault_id != vault_id)
{
return Err(Error::local(
code::SERVER_MISMATCH,
format!(
"{path} here is a different vault from the kit's (another project with the same name?)"
),
));
}
false
}
None => {
self.state
.projects
.insert(name.clone(), ProjectState::new(kit.server()));
true
}
};
let unreachable = match self.discover_from(&api, &path, key.clone()) {
Ok(u) => u,
Err(e) => {
if fresh {
self.state.projects.remove(&name);
}
return Err(e);
}
};
self.store_key(&path, &key)?;
let project = self.state.project_mut(&path)?;
if !project.held.contains(&path.to_string()) {
project.held.push(path.to_string());
}
let found = self.found_below(&path)?;
self.save()?;
Ok(Discovery {
root: path,
found,
unreachable,
})
}
pub fn recover(&mut self, kit: &Kit) -> Result<Discovery, Error> {
if kit.kind() != KitKind::Recovery {
return Err(Error::local(
code::INVALID_KIT,
format!("this is a delegation kit for {}", kit.path()),
));
}
self.import(kit)
}
}
pub struct PendingInit<'a> {
owner: &'a mut Owner,
path: EnvPath,
server: String,
key: NodeKey,
kit: Kit,
}
impl std::fmt::Debug for PendingInit<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PendingInit")
.field("path", &self.path)
.field("server", &self.server)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub struct InitError {
error: Error,
vault_created: bool,
}
impl InitError {
pub fn error(&self) -> &Error {
&self.error
}
pub fn vault_created(&self) -> bool {
self.vault_created
}
pub fn into_error(self) -> Error {
self.error
}
}
impl std::fmt::Display for InitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.error.fmt(f)
}
}
impl std::error::Error for InitError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.error)
}
}
impl From<InitError> for Error {
fn from(e: InitError) -> Error {
e.error
}
}
impl PendingInit<'_> {
pub fn path(&self) -> &EnvPath {
&self.path
}
pub fn server(&self) -> &str {
&self.server
}
pub fn recovery_kit(&self) -> &Kit {
&self.kit
}
pub fn confirm_kit_stored(self) -> Result<ProjectInfo, InitError> {
let PendingInit {
owner,
path,
server,
key,
kit: _,
} = self;
let before = |error| InitError {
error,
vault_created: false,
};
let after = |error| InitError {
error,
vault_created: true,
};
let api = owner.api(&server).map_err(before)?;
let created = Handle::create(&api, &key, &path.to_string()).map_err(before)?;
if !created {
return Err(before(Error::local(
code::VAULT_EXISTS,
"the server already has a vault for this key; nothing was changed",
)));
}
owner.store_key(&path, &key).map_err(after)?;
let vault_id = key.owner().vault_id();
let mut project = ProjectState::new(server.clone());
project.held.push(path.to_string());
project
.tree
.insert(path.to_string(), TreeNode::new(vault_id, false));
owner.state.projects.insert(path.to_string(), project);
owner.state.touched.insert(vault_id.to_hex(), now());
owner.save().map_err(after)?;
Ok(ProjectInfo {
path,
server,
vault_id: vault_id.to_hex(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn suggestions() {
let known = ["acme", "acme/prod", "acme/dev", "acme/staging"];
assert_eq!(closest("acme/prdo", known), Some("acme/prod"));
assert_eq!(closest("acme/stagin", known), Some("acme/staging"));
assert_eq!(closest("zzzz/qqqq", known), None);
}
}