1mod env;
37pub(crate) mod kit;
38mod rekey;
39
40pub use env::{Environment, MintedToken, Revocation, TokenInfo};
41pub use kit::{KIT_VERSION, Kit, KitKind, default_file_name};
42pub use rekey::{PendingRekey, RekeyAborted, RekeyOutcome};
43
44use std::collections::{BTreeMap, HashMap};
45use std::sync::Arc;
46
47use crate::client::{Api, Events, NoEvents, Progress, Warning, now};
48use crate::keys::{NodeKey, seal_child_key};
49use crate::proto::children::{ChildEntry, ChildMode};
50use crate::proto::codec::FormatError;
51use crate::proto::ids::B64;
52use crate::proto::path::{EnvPath, Segment};
53
54use crate::audit::{self, AuditReport};
55use crate::error::{Error, code};
56use crate::state::{LocalState, ProjectState, StateStore, TreeNode};
57use crate::store::{KeyName, KeyStore};
58use crate::vault::{DAY, Handle};
59
60pub type Connector = Arc<dyn Fn(&str) -> Result<Api, Error> + Send + Sync>;
62
63#[derive(Debug, Clone, Copy)]
67pub struct GrantsSubtreeOwnership;
68
69#[derive(Debug, Clone, Copy)]
72pub struct RetiresAllTokens;
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76#[non_exhaustive]
77pub struct ProjectInfo {
78 pub path: EnvPath,
80 pub server: String,
82 pub vault_id: String,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88#[non_exhaustive]
89pub struct Discovery {
90 pub root: EnvPath,
92 pub found: Vec<EnvPath>,
94 pub unreachable: Vec<EnvPath>,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101#[non_exhaustive]
102pub struct Removal {
103 pub deleted: Vec<EnvPath>,
105 pub parent_not_updated: Option<EnvPath>,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112#[non_exhaustive]
113pub struct Repair {
114 pub created: bool,
117 pub children: usize,
119 pub lost: Vec<EnvPath>,
122}
123
124pub struct Owner {
126 keys: Arc<dyn KeyStore>,
127 store: Arc<dyn StateStore>,
128 events: Arc<dyn Events>,
129 connect: Connector,
130 apis: HashMap<String, Api>,
131 state: LocalState,
132}
133
134impl std::fmt::Debug for Owner {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 f.debug_struct("Owner")
137 .field("projects", &self.state.projects.keys().collect::<Vec<_>>())
138 .finish_non_exhaustive()
139 }
140}
141
142fn default_connector() -> Connector {
143 #[cfg(feature = "http")]
144 {
145 Arc::new(|server: &str| {
146 crate::client::ClientBuilder::new(server)
147 .build()
148 .map_err(Error::build)
149 })
150 }
151 #[cfg(not(feature = "http"))]
152 {
153 Arc::new(|server: &str| {
154 Err(Error::other(format!(
155 "no transport to reach {server}: build galata-vault with the `http` feature, or give the owner a connector"
156 )))
157 })
158 }
159}
160
161fn chain_end(chain: Vec<(EnvPath, NodeKey)>) -> Result<NodeKey, Error> {
162 chain
163 .into_iter()
164 .last()
165 .map(|(_, k)| k)
166 .ok_or_else(|| Error::other("a key chain includes its target"))
167}
168
169fn children_error(e: impl std::fmt::Display) -> Error {
170 Error::other(e.to_string())
171}
172
173fn announces_expiry(vault: &Handle) -> bool {
175 vault.status().is_some_and(|s| s.expires_at.is_some())
176}
177
178fn server_expires(api: &Api, vault: &Handle) -> bool {
182 let announced = matches!(
183 api.capabilities(),
184 Ok(Some(caps)) if caps.idle_expiry_days.is_some()
185 );
186 announced || announces_expiry(vault)
187}
188
189fn levenshtein(a: &str, b: &str) -> usize {
190 let b: Vec<char> = b.chars().collect();
191 let mut row: Vec<usize> = (0..=b.len()).collect();
192 for (i, ca) in a.chars().enumerate() {
193 let mut prev = row[0];
194 row[0] = i + 1;
195 for (j, cb) in b.iter().enumerate() {
196 let cur = row[j + 1];
197 row[j + 1] = (prev + usize::from(ca != *cb)).min(row[j] + 1).min(cur + 1);
198 prev = cur;
199 }
200 }
201 row[b.len()]
202}
203
204fn closest<'a>(target: &str, candidates: impl IntoIterator<Item = &'a str>) -> Option<&'a str> {
206 let limit = (target.len() / 3).max(2);
207 candidates
208 .into_iter()
209 .map(|c| (levenshtein(target, c), c))
210 .filter(|(d, _)| *d <= limit)
211 .min()
212 .map(|(_, c)| c)
213}
214
215impl Owner {
216 pub fn open(keys: Arc<dyn KeyStore>, store: Arc<dyn StateStore>) -> Result<Owner, Error> {
219 let state = store.load().map_err(Error::store)?;
220 Ok(Owner {
221 keys,
222 store,
223 events: Arc::new(NoEvents),
224 connect: default_connector(),
225 apis: HashMap::new(),
226 state,
227 })
228 }
229
230 pub fn with_events(mut self, events: Arc<dyn Events>) -> Owner {
232 self.events = events;
233 self.apis.clear();
234 self
235 }
236
237 pub fn with_connector(
240 mut self,
241 connect: impl Fn(&str) -> Result<Api, Error> + Send + Sync + 'static,
242 ) -> Owner {
243 self.connect = Arc::new(connect);
244 self.apis.clear();
245 self
246 }
247
248 pub fn events(&self) -> &Arc<dyn Events> {
250 &self.events
251 }
252
253 pub fn state(&self) -> &LocalState {
255 &self.state
256 }
257
258 pub fn save(&self) -> Result<(), Error> {
260 self.store.save(&self.state).map_err(Error::store)
261 }
262
263 pub fn reload(&mut self) -> Result<(), Error> {
265 self.state = self.store.load().map_err(Error::store)?;
266 Ok(())
267 }
268
269 pub fn api(&mut self, server: &str) -> Result<Api, Error> {
271 if let Some(api) = self.apis.get(server) {
272 return Ok(api.clone());
273 }
274 let api = (self.connect)(server)?.with_events(self.events.clone());
275 self.apis.insert(server.to_owned(), api.clone());
276 Ok(api)
277 }
278
279 fn api_for(&mut self, path: &EnvPath) -> Result<Api, Error> {
280 let server = self.state.project(path)?.server.clone();
281 self.api(&server)
282 }
283
284 fn held_key(&self, path: &EnvPath) -> Result<NodeKey, Error> {
285 let stored = self
286 .keys
287 .get(&KeyName::Node(path.clone()))
288 .map_err(Error::store)?
289 .ok_or_else(|| {
290 Error::local(
291 code::NO_KEY,
292 format!("the key for {path} is missing from the credential store"),
293 )
294 })?;
295 NodeKey::parse(&stored).map_err(|e| match e {
296 FormatError::UnknownVersion { found } => Error::other(format!(
297 "the stored key for {path} carries version {found}, which this build does not know"
298 )),
299 _ => Error::other(format!("the stored key for {path} is damaged")),
300 })
301 }
302
303 fn store_key(&self, path: &EnvPath, key: &NodeKey) -> Result<(), Error> {
304 self.keys
305 .set(&KeyName::Node(path.clone()), &key.encode())
306 .map_err(Error::store)
307 }
308
309 pub fn closest_known(&self, path: &EnvPath) -> Option<EnvPath> {
314 let known = self.state.known_paths();
315 closest(&path.to_string(), known.iter().map(String::as_str)).and_then(|s| s.parse().ok())
316 }
317
318 pub fn require_known(&self, path: &EnvPath) -> Result<(), Error> {
321 let project_known = self
322 .state
323 .projects
324 .contains_key(path.project_name().as_str());
325 if project_known && self.state.project(path)?.knows(path) {
326 return Ok(());
327 }
328 let message = if project_known {
329 format!("unknown environment {path}")
330 } else {
331 format!("unknown project {}", path.project_name())
332 };
333 Err(Error::UnknownPath {
334 path: path.to_string(),
335 suggestion: self.closest_known(path).map(|p| p.to_string()),
336 project_known,
337 message,
338 })
339 }
340
341 pub fn tree(&self, project: &EnvPath) -> Result<&BTreeMap<String, TreeNode>, Error> {
343 Ok(&self.state.project(project)?.tree)
344 }
345
346 fn chain(&self, api: &Api, path: &EnvPath) -> Result<Vec<(EnvPath, NodeKey)>, Error> {
350 let project = self.state.project(path)?.clone();
351 let root = project.held_root_for(path).ok_or_else(|| {
352 Error::local(
353 code::NO_KEY,
354 format!("no key is held for {path} or any node above it"),
355 )
356 })?;
357 let mut key = self.held_key(&root)?;
358 let mut here = root.clone();
359 let mut out = vec![(here.clone(), key.clone())];
360 for seg in &path.segments()[root.depth()..] {
361 let child = here
362 .child(seg.clone())
363 .map_err(|e| Error::local(code::INVALID_PATH, format!("{here}/{seg}: {e}")))?;
364 let sealed = project
365 .tree
366 .get(&child.to_string())
367 .is_some_and(|n| n.sealed);
368 key = if sealed {
369 let parent = Handle::open_owner(api, &key, &here.to_string())?;
370 let (record, _) = parent.children()?;
371 let entry = record.get(seg).ok_or_else(|| {
372 Error::other(format!("{here}'s children record no longer lists {seg}"))
373 })?;
374 match &entry.mode {
375 ChildMode::Derived => key.child(seg),
376 ChildMode::Sealed { key_ct } => {
377 parent.owner()?.open_child_key(&key_ct.0).map_err(|e| {
378 Error::key(e, format!("{child}'s sealed key does not open"))
379 })?
380 }
381 }
382 } else {
383 key.child(seg)
384 };
385 here = child;
386 out.push((here.clone(), key.clone()));
387 }
388 Ok(out)
389 }
390
391 pub fn environment(&mut self, path: &EnvPath) -> Result<Environment, Error> {
398 self.require_known(path)?;
399 let api = self.api_for(path)?;
400 let mut chain = self.chain(&api, path)?;
401 let key = chain
402 .pop()
403 .map(|(_, k)| k)
404 .ok_or_else(|| Error::other("a key chain includes its target"))?;
405 let vault = Handle::open_owner(&api, &key, &path.to_string())?;
406 let id = vault.vault_id.to_hex();
407 if let Some(pins) = self.state.pins.get(&id) {
408 vault.set_pins(pins.clone())?;
409 }
410 self.state.touched.insert(id.clone(), now());
411 let ancestors: Vec<String> = chain
412 .iter()
413 .map(|(_, k)| k.owner().vault_id().to_hex())
414 .collect();
415 if server_expires(&api, &vault) {
416 self.state.no_expiry.remove(&id);
419 for a in &ancestors {
420 self.state.no_expiry.remove(a);
421 }
422 self.keep_alive(&api, &chain);
423 } else {
424 self.state.no_expiry.insert(id);
428 self.state.no_expiry.extend(ancestors);
429 }
430 Environment::new(vault, path.clone())
431 }
432
433 pub fn remember(&mut self, env: &Environment) {
436 self.state.pins.insert(env.vault_id(), env.pins());
437 }
438
439 pub fn close(&mut self, env: Environment) -> Result<(), Error> {
441 self.remember(&env);
442 drop(env);
443 self.save()
444 }
445
446 pub fn audit(&mut self, env: &Environment, earlier: usize) -> Result<AuditReport, Error> {
451 audit::verify_recorded(env.handle(), &mut self.state, earlier)
452 }
453
454 fn keep_alive(&mut self, api: &Api, ancestors: &[(EnvPath, NodeKey)]) {
459 let t = now();
460 for (path, key) in ancestors {
461 let id = key.owner().vault_id().to_hex();
462 if self.state.no_expiry.contains(&id)
463 || self
464 .state
465 .touched
466 .get(&id)
467 .is_some_and(|last| t - last < DAY)
468 {
469 continue;
470 }
471 match Handle::open_owner(api, key, &path.to_string()) {
472 Ok(vault) => {
473 if !server_expires(api, &vault) {
474 self.state.no_expiry.insert(id.clone());
475 }
476 self.state.touched.insert(id, t);
477 }
478 Err(e) => self.events.warning(&Warning::KeepAliveFailed {
479 path: path.to_string(),
480 message: e.to_string(),
481 }),
482 }
483 }
484 }
485
486 fn discover_from(
490 &mut self,
491 api: &Api,
492 root: &EnvPath,
493 key: NodeKey,
494 ) -> Result<Vec<EnvPath>, Error> {
495 let root_sealed = self
496 .state
497 .project(root)?
498 .tree
499 .get(&root.to_string())
500 .is_some_and(|n| n.sealed);
501 let mut found: BTreeMap<String, TreeNode> = BTreeMap::new();
502 let mut unreachable: Vec<EnvPath> = Vec::new();
503 let mut queue: Vec<(EnvPath, NodeKey, bool, Option<NodeKey>)> =
504 vec![(root.clone(), key, root_sealed, None)];
505 while let Some((path, key, sealed, parent_key)) = queue.pop() {
506 let opened = match Handle::open_owner(api, &key, &path.to_string()) {
507 Ok(v) => Ok((v, key.clone(), sealed)),
508 Err(e) => match self.relink(api, &path, &key, parent_key.as_ref()) {
509 Some((v, held)) => Ok((v, held, true)),
510 None => Err(e),
511 },
512 };
513 let (vault, key, sealed) = match opened {
514 Ok(found) => found,
515 Err(e) if &path == root => return Err(e),
516 Err(e) => {
517 self.events.warning(&Warning::Unreachable {
518 path: path.to_string(),
519 message: e.to_string(),
520 vault_missing: matches!(e, Error::VaultNotFound { .. }),
521 });
522 found.insert(
523 path.to_string(),
524 TreeNode::new(key.owner().vault_id(), sealed),
525 );
526 unreachable.push(path);
527 continue;
528 }
529 };
530 self.state.touched.insert(vault.vault_id.to_hex(), now());
531 found.insert(path.to_string(), TreeNode::new(vault.vault_id, sealed));
532 let (record, _) = vault.children()?;
533 for entry in record.children() {
534 let child = match path.child(entry.seg.clone()) {
535 Ok(c) => c,
536 Err(e) => {
537 self.events.warning(&Warning::ChildTooDeep {
538 parent: path.to_string(),
539 message: e.to_string(),
540 });
541 continue;
542 }
543 };
544 let next = match &entry.mode {
545 ChildMode::Derived => (child, key.child(&entry.seg), false, Some(key.clone())),
546 ChildMode::Sealed { key_ct } => {
547 match vault.owner()?.open_child_key(&key_ct.0) {
548 Ok(k) => (child, k, true, Some(key.clone())),
549 Err(e) => {
550 self.events.warning(&Warning::SealedKeyUnopenable {
551 path: child.to_string(),
552 message: e.to_string(),
553 });
554 continue;
555 }
556 }
557 }
558 };
559 queue.push(next);
560 }
561 }
562 let project = self.state.project_mut(root)?;
563 project.tree.retain(|k, _| {
564 let Ok(p) = k.parse::<EnvPath>() else {
565 return false;
566 };
567 !p.starts_with(root) || unreachable.iter().any(|u| &p != u && p.starts_with(u))
568 });
569 project.tree.extend(found);
570 Ok(unreachable)
571 }
572
573 fn relink(
577 &self,
578 api: &Api,
579 path: &EnvPath,
580 stale: &NodeKey,
581 parent_key: Option<&NodeKey>,
582 ) -> Option<(Handle, NodeKey)> {
583 let parent_key = parent_key?;
584 let parent = path.parent()?;
585 let stored = self.keys.get(&KeyName::Node(path.clone())).ok()??;
586 let held = NodeKey::parse(&stored).ok()?;
587 if held.owner().vault_id() == stale.owner().vault_id() {
588 return None;
589 }
590 let vault = Handle::open_owner(api, &held, &path.to_string()).ok()?;
591 let relinked = Handle::open_owner(api, parent_key, &parent.to_string())
592 .and_then(|pv| pv.seal_child(path.last(), &held));
593 match relinked {
594 Ok(()) => self.events.progress(&Progress::Relinked {
595 path: path.to_string(),
596 parent: parent.to_string(),
597 }),
598 Err(e) => self.events.warning(&Warning::RelinkFailed {
599 path: path.to_string(),
600 parent: parent.to_string(),
601 message: e.to_string(),
602 }),
603 }
604 Some((vault, held))
605 }
606
607 pub fn refresh(&mut self, project: &EnvPath) -> Result<(), Error> {
610 let held: Vec<EnvPath> = self
611 .state
612 .project(project)?
613 .held
614 .iter()
615 .filter_map(|h| h.parse().ok())
616 .collect();
617 if held.is_empty() {
618 return Err(Error::local(
619 code::NO_KEY,
620 format!("no key is held for any node of {project}"),
621 ));
622 }
623 let api = self.api_for(project)?;
624 for root in held {
625 let key = self.held_key(&root)?;
626 self.discover_from(&api, &root, key)?;
627 }
628 Ok(())
629 }
630
631 pub fn discover(&mut self, root: &EnvPath) -> Result<Discovery, Error> {
634 self.require_known(root)?;
635 let api = self.api_for(root)?;
636 let key = chain_end(self.chain(&api, root)?)?;
637 let unreachable = self.discover_from(&api, root, key)?;
638 let found = self.found_below(root)?;
639 self.save()?;
640 Ok(Discovery {
641 root: root.clone(),
642 found,
643 unreachable,
644 })
645 }
646
647 fn found_below(&self, root: &EnvPath) -> Result<Vec<EnvPath>, Error> {
648 Ok(self
649 .state
650 .project(root)?
651 .tree
652 .keys()
653 .filter_map(|k| k.parse::<EnvPath>().ok())
654 .filter(|p| p.starts_with(root))
655 .collect())
656 }
657
658 pub fn begin_init(&mut self, project: &str, server: &str) -> Result<PendingInit<'_>, Error> {
664 let seg = Segment::new(project).map_err(|e| {
665 Error::local(
666 code::INVALID_PATH,
667 format!("{project:?} is not a project name: {e}"),
668 )
669 })?;
670 let path = EnvPath::project(seg);
671 let server = crate::proto::url::validate_server_url(server)
672 .map_err(|e| Error::local(code::INVALID_SERVER, e))?;
673 if self.state.projects.contains_key(project) {
674 return Err(Error::local(
675 code::PROJECT_EXISTS,
676 format!("a project named {project} is already configured here"),
677 ));
678 }
679 let key = NodeKey::generate();
680 let kit = Kit::new(KitKind::Recovery, path.clone(), &server, &key);
681 Ok(PendingInit {
682 owner: self,
683 path,
684 server,
685 key,
686 kit,
687 })
688 }
689
690 pub fn env_add(&mut self, path: &EnvPath) -> Result<(), Error> {
694 let Some(parent) = path.parent() else {
695 return Err(Error::local(
696 code::IS_PROJECT,
697 format!("{path} is a project"),
698 ));
699 };
700 self.require_known(&parent)?;
701 if self.state.project(path)?.knows(path) {
702 return Err(Error::local(
703 code::ENV_EXISTS,
704 format!("{path} already exists"),
705 ));
706 }
707 let api = self.api_for(path)?;
708 let chain = self.chain(&api, &parent)?;
709 let Some((_, parent_key)) = chain.last().cloned() else {
710 return Err(Error::other("a key chain includes its target"));
711 };
712 self.keep_alive(&api, &chain[..chain.len() - 1]);
713 let parent_vault = Handle::open_owner(&api, &parent_key, &parent.to_string())?;
714 let seg = path.last().clone();
715 if parent_vault.children()?.0.get(&seg).is_some() {
716 return Err(Error::local(
717 code::ENV_EXISTS,
718 format!(
719 "{path} already exists (the known tree of {} is out of date)",
720 path.project_name()
721 ),
722 ));
723 }
724
725 let key = parent_key.child(&seg);
726 Handle::create(&api, &key, &path.to_string())?;
727 parent_vault.update_children(|r| {
728 if r.get(&seg).is_none() {
729 r.insert(ChildEntry::new(seg.clone(), now(), ChildMode::Derived))
730 .map_err(children_error)?;
731 }
732 Ok(())
733 })?;
734 let vault_id = key.owner().vault_id();
735 self.state
736 .project_mut(path)?
737 .tree
738 .insert(path.to_string(), TreeNode::new(vault_id, false));
739 self.state.touched.insert(vault_id.to_hex(), now());
740 self.save()
741 }
742
743 pub fn env_remove(&mut self, path: &EnvPath, recursive: bool) -> Result<Removal, Error> {
747 self.require_known(path)?;
748 let api = self.api_for(path)?;
749 let chain = self.chain(&api, path)?;
750 let Some((_, key)) = chain.last().cloned() else {
751 return Err(Error::other("a key chain includes its target"));
752 };
753 self.discover_from(&api, path, key)?;
755 let below = self.state.project(path)?.descendants(path);
756 if !below.is_empty() && !recursive {
757 let names: Vec<String> = below.iter().map(ToString::to_string).collect();
758 return Err(Error::local(
759 code::HAS_CHILDREN,
760 format!("{path} has environments below it ({})", names.join(", ")),
761 ));
762 }
763
764 let mut deleted = Vec::new();
765 for node in below.iter().chain(std::iter::once(path)) {
767 let node_key = chain_end(self.chain(&api, node)?)?;
768 match Handle::open_owner(&api, &node_key, &node.to_string()) {
769 Ok(v) => {
770 v.delete_vault()?;
771 let id = v.vault_id.to_hex();
772 self.state.pins.remove(&id);
773 self.state.audit.remove(&id);
774 }
775 Err(e) => self.events.warning(&Warning::Unreachable {
776 path: node.to_string(),
777 message: e.to_string(),
778 vault_missing: matches!(e, Error::VaultNotFound { .. }),
779 }),
780 }
781 let p = self.state.project_mut(node)?;
782 p.tree.remove(&node.to_string());
783 if let Some(i) = p.held.iter().position(|h| h == &node.to_string()) {
784 p.held.remove(i);
785 self.keys
786 .delete(&KeyName::Node(node.clone()))
787 .map_err(Error::store)?;
788 }
789 self.events.progress(&Progress::Deleted {
790 path: node.to_string(),
791 });
792 deleted.push(node.clone());
793 }
794
795 let mut parent_not_updated = None;
796 if let Some(parent) = path.parent() {
797 if chain.len() >= 2 {
798 let (_, parent_key) = &chain[chain.len() - 2];
799 let seg = path.last().clone();
800 Handle::open_owner(&api, parent_key, &parent.to_string())?.update_children(
801 |r| {
802 let _ = r.remove(&seg);
803 Ok(())
804 },
805 )?;
806 } else {
807 parent_not_updated = Some(parent);
808 }
809 } else {
810 self.state.projects.remove(path.project_name().as_str());
811 }
812 self.save()?;
813 Ok(Removal {
814 deleted,
815 parent_not_updated,
816 })
817 }
818
819 pub fn env_repair(&mut self, path: &EnvPath) -> Result<Repair, Error> {
824 self.require_known(path)?;
825 let api = self.api_for(path)?;
826 let key = chain_end(self.chain(&api, path)?)?;
827 let created = Handle::create(&api, &key, &path.to_string())?;
828 let vault = Handle::open_owner(&api, &key, &path.to_string())?;
829 if created {
830 self.state.pins.remove(&vault.vault_id.to_hex());
833 self.state.audit.remove(&vault.vault_id.to_hex());
834 }
835 let owner_box = vault.owner()?.box_pub();
836
837 let project = self.state.project(path)?.clone();
838 let mut entries = Vec::new();
839 let mut lost = Vec::new();
840 for child in project.children_of(path) {
841 let Some(node) = project.tree.get(&child.to_string()) else {
842 continue;
843 };
844 let mode = if node.sealed {
845 match self
846 .keys
847 .get(&KeyName::Node(child.clone()))
848 .map_err(Error::store)?
849 {
850 Some(k) => {
851 let k = NodeKey::parse(&k).map_err(|_| {
852 Error::other(format!("the stored key for {child} is damaged"))
853 })?;
854 let key_ct = seal_child_key(&owner_box, &vault.vault_id, &k)
855 .map_err(|e| Error::key(e, format!("the sealed key for {child}")))?;
856 ChildMode::Sealed {
857 key_ct: B64(key_ct),
858 }
859 }
860 None => {
861 lost.push(child);
862 continue;
863 }
864 }
865 } else {
866 ChildMode::Derived
867 };
868 entries.push(ChildEntry::new(child.last().clone(), now(), mode));
869 }
870 vault.update_children(|r| {
871 for e in &entries {
872 if r.get(&e.seg).is_none() {
873 r.insert(e.clone()).map_err(children_error)?;
874 }
875 }
876 Ok(())
877 })?;
878 self.state.touched.insert(vault.vault_id.to_hex(), now());
879 drop(vault);
880 self.save()?;
881 Ok(Repair {
882 created,
883 children: entries.len(),
884 lost,
885 })
886 }
887
888 pub fn export_kit(&mut self, path: &EnvPath, _: GrantsSubtreeOwnership) -> Result<Kit, Error> {
892 self.require_known(path)?;
893 let api = self.api_for(path)?;
894 let key = chain_end(self.chain(&api, path)?)?;
895 let server = self.state.project(path)?.server.clone();
896 let kit = Kit::new(KitKind::Delegation, path.clone(), &server, &key);
897 self.save()?;
898 Ok(kit)
899 }
900
901 pub fn import(&mut self, kit: &Kit) -> Result<Discovery, Error> {
904 let path = kit.path().clone();
905 let key = kit.key()?;
906 let name = path.project_name().to_string();
907 let vault_id = key.owner().vault_id();
908 let api = self.api(kit.server())?;
909 let fresh = match self.state.projects.get(&name) {
910 Some(p) if p.server != kit.server() => {
911 return Err(Error::local(
912 code::SERVER_MISMATCH,
913 format!(
914 "the project {name} here is pinned to {}, but the kit is for {}",
915 p.server,
916 kit.server()
917 ),
918 ));
919 }
920 Some(p) => {
921 if p.tree
922 .get(&path.to_string())
923 .is_some_and(|n| n.vault_id != vault_id)
924 {
925 return Err(Error::local(
926 code::SERVER_MISMATCH,
927 format!(
928 "{path} here is a different vault from the kit's (another project with the same name?)"
929 ),
930 ));
931 }
932 false
933 }
934 None => {
935 self.state
936 .projects
937 .insert(name.clone(), ProjectState::new(kit.server()));
938 true
939 }
940 };
941 let unreachable = match self.discover_from(&api, &path, key.clone()) {
942 Ok(u) => u,
943 Err(e) => {
944 if fresh {
945 self.state.projects.remove(&name);
946 }
947 return Err(e);
948 }
949 };
950 self.store_key(&path, &key)?;
951 let project = self.state.project_mut(&path)?;
952 if !project.held.contains(&path.to_string()) {
953 project.held.push(path.to_string());
954 }
955 let found = self.found_below(&path)?;
956 self.save()?;
957 Ok(Discovery {
958 root: path,
959 found,
960 unreachable,
961 })
962 }
963
964 pub fn recover(&mut self, kit: &Kit) -> Result<Discovery, Error> {
967 if kit.kind() != KitKind::Recovery {
968 return Err(Error::local(
969 code::INVALID_KIT,
970 format!("this is a delegation kit for {}", kit.path()),
971 ));
972 }
973 self.import(kit)
974 }
975}
976
977pub struct PendingInit<'a> {
980 owner: &'a mut Owner,
981 path: EnvPath,
982 server: String,
983 key: NodeKey,
984 kit: Kit,
985}
986
987impl std::fmt::Debug for PendingInit<'_> {
988 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
989 f.debug_struct("PendingInit")
990 .field("path", &self.path)
991 .field("server", &self.server)
992 .finish_non_exhaustive()
993 }
994}
995
996#[derive(Debug, Clone)]
999pub struct InitError {
1000 error: Error,
1001 vault_created: bool,
1002}
1003
1004impl InitError {
1005 pub fn error(&self) -> &Error {
1007 &self.error
1008 }
1009
1010 pub fn vault_created(&self) -> bool {
1013 self.vault_created
1014 }
1015
1016 pub fn into_error(self) -> Error {
1018 self.error
1019 }
1020}
1021
1022impl std::fmt::Display for InitError {
1023 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1024 self.error.fmt(f)
1025 }
1026}
1027
1028impl std::error::Error for InitError {
1029 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1030 Some(&self.error)
1031 }
1032}
1033
1034impl From<InitError> for Error {
1035 fn from(e: InitError) -> Error {
1036 e.error
1037 }
1038}
1039
1040impl PendingInit<'_> {
1041 pub fn path(&self) -> &EnvPath {
1043 &self.path
1044 }
1045
1046 pub fn server(&self) -> &str {
1048 &self.server
1049 }
1050
1051 pub fn recovery_kit(&self) -> &Kit {
1054 &self.kit
1055 }
1056
1057 pub fn confirm_kit_stored(self) -> Result<ProjectInfo, InitError> {
1061 let PendingInit {
1062 owner,
1063 path,
1064 server,
1065 key,
1066 kit: _,
1067 } = self;
1068 let before = |error| InitError {
1069 error,
1070 vault_created: false,
1071 };
1072 let after = |error| InitError {
1073 error,
1074 vault_created: true,
1075 };
1076 let api = owner.api(&server).map_err(before)?;
1077 let created = Handle::create(&api, &key, &path.to_string()).map_err(before)?;
1078 if !created {
1079 return Err(before(Error::local(
1080 code::VAULT_EXISTS,
1081 "the server already has a vault for this key; nothing was changed",
1082 )));
1083 }
1084 owner.store_key(&path, &key).map_err(after)?;
1085 let vault_id = key.owner().vault_id();
1086 let mut project = ProjectState::new(server.clone());
1087 project.held.push(path.to_string());
1088 project
1089 .tree
1090 .insert(path.to_string(), TreeNode::new(vault_id, false));
1091 owner.state.projects.insert(path.to_string(), project);
1092 owner.state.touched.insert(vault_id.to_hex(), now());
1093 owner.save().map_err(after)?;
1094 Ok(ProjectInfo {
1095 path,
1096 server,
1097 vault_id: vault_id.to_hex(),
1098 })
1099 }
1100}
1101
1102#[cfg(test)]
1103mod tests {
1104 use super::*;
1105
1106 #[test]
1107 fn suggestions() {
1108 let known = ["acme", "acme/prod", "acme/dev", "acme/staging"];
1109 assert_eq!(closest("acme/prdo", known), Some("acme/prod"));
1110 assert_eq!(closest("acme/stagin", known), Some("acme/staging"));
1111 assert_eq!(closest("zzzz/qqqq", known), None);
1112 }
1113}