evault_tui/provider.rs
1//! Adapter trait between the TUI and any data source.
2
3use evault_core::model::{Group, VarId, VarKind};
4use secrecy::SecretString;
5use thiserror::Error;
6use time::OffsetDateTime;
7
8/// One row of dashboard data.
9///
10/// Deliberately flat and **value-free**: the dashboard never holds a
11/// secret value in memory, only its metadata. Implementations of
12/// [`VarProvider`] derive this struct from whatever backing store
13/// they wrap.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct VarSummary {
16 /// Stable identifier of the variable.
17 pub id: VarId,
18 /// Display name (e.g. `DATABASE_URL`).
19 pub name: String,
20 /// Logical group the variable belongs to.
21 pub group: Group,
22 /// Whether the value lives in the keyring (secret) or alongside
23 /// metadata (plain).
24 pub kind: VarKind,
25 /// Length of the underlying value, in characters (not bytes).
26 /// Surfaced in the dashboard as a privacy-preserving size hint.
27 pub value_len: usize,
28 /// How many projects currently link to this variable.
29 pub linked_projects: usize,
30 /// Last time the variable's metadata changed.
31 pub updated_at: OffsetDateTime,
32}
33
34/// Errors produced by a [`VarProvider`].
35///
36/// Intentionally a small enum — the TUI only renders the message to
37/// the user and does not branch on the cause. Concrete error types
38/// from the wrapped backend should be stringified into one of these
39/// variants by the [`VarProvider`] implementation.
40#[non_exhaustive]
41#[derive(Error, Debug)]
42pub enum ProviderError {
43 /// The provider could not be reached or its data is temporarily
44 /// unavailable (e.g. locked metadata store, dropped connection).
45 #[error("data unavailable: {0}")]
46 DataUnavailable(String),
47
48 /// The underlying backend reported an error.
49 #[error("backend error: {0}")]
50 Backend(String),
51}
52
53/// Read-side data source for the dashboard.
54///
55/// Implementations adapt the registry / manifest / store layer into
56/// the flat [`VarSummary`] row representation the TUI consumes. The
57/// trait is intentionally narrow so adapters can be added incrementally
58/// (an in-memory test stub today, a `RegistryService` wrapper tomorrow,
59/// a remote facade later) without churning view code.
60///
61/// Implementations must be safe to share across threads.
62pub trait VarProvider: Send + Sync {
63 /// Return the full list of variables. The dashboard re-runs this
64 /// on explicit refresh, so the implementation should make each
65 /// call deterministic with respect to the underlying state at
66 /// call time.
67 ///
68 /// # Errors
69 /// Returns [`ProviderError`] if the backend is unreachable or
70 /// returns an error. The TUI surfaces the message as a toast.
71 fn list(&self) -> Result<Vec<VarSummary>, ProviderError>;
72
73 /// Resolve the actual (decrypted) value of a variable.
74 ///
75 /// Used by the `v` key in the TUI to surface the value inside a
76 /// view-value modal. Returns `None` if the variable's metadata
77 /// exists but its value is missing in the secret tier (rare —
78 /// usually indicates external tampering).
79 ///
80 /// # Errors
81 /// Returns [`ProviderError`] on storage failure.
82 fn get_value(&self, id: VarId) -> Result<Option<SecretString>, ProviderError>;
83}
84
85/// Write-side counterpart to [`VarProvider`].
86///
87/// The TUI invokes [`VarMutator`] methods on user-confirmed actions
88/// (delete in phase 2b2; create / update / link in phase 2c). The
89/// trait is split from `VarProvider` so adapters that only support
90/// reading (e.g. a remote read-only view) need not implement writes,
91/// though [`crate::run_tui`] requires both for now.
92///
93/// Implementations must be safe to share across threads.
94pub trait VarMutator: Send + Sync {
95 /// Delete the variable with the given id.
96 ///
97 /// # Performance contract
98 ///
99 /// Implementations **must** return promptly — target ≤ 100 ms,
100 /// never more than ≈ 1 s. The TUI calls `delete` synchronously
101 /// inside the event loop: a slow implementation will freeze the
102 /// UI and prevent `Ctrl-C` from being handled until the call
103 /// returns. **Do not** perform network I/O or block on
104 /// user-facing prompts (OS keyring access counts — wrap it with a
105 /// timeout). Phase 3 will move mutator calls onto a worker
106 /// thread; until then, treat synchronous responsiveness as part
107 /// of the API contract.
108 ///
109 /// # Idempotency
110 ///
111 /// Implementations should make the call idempotent if at all
112 /// possible: re-deleting an already-absent variable should not
113 /// fail. The TUI refreshes its row buffer after every successful
114 /// delete, so a non-idempotent backend will surface spurious
115 /// errors when the user clicks delete twice on a stale view.
116 ///
117 /// # Errors
118 /// Returns [`ProviderError`] if the backend refused or could not
119 /// perform the delete. The TUI surfaces the message as a sticky
120 /// error toast and the user can retry from the dashboard.
121 fn delete(&self, id: VarId) -> Result<(), ProviderError>;
122
123 /// Create a new variable from a user-supplied draft, returning
124 /// its assigned id. Same performance contract as [`Self::delete`].
125 ///
126 /// # Errors
127 /// Returns [`ProviderError`] on validation failure (invalid name,
128 /// duplicate, empty value) or storage failure.
129 fn create(&self, draft: VarDraft) -> Result<VarId, ProviderError>;
130
131 /// Replace an existing variable's value. Same performance
132 /// contract as [`Self::delete`].
133 ///
134 /// # Errors
135 /// Returns [`ProviderError`] on validation failure or storage
136 /// failure.
137 fn update_value(&self, id: VarId, value: SecretString) -> Result<(), ProviderError>;
138
139 /// Link a variable to a project's manifest and (optionally)
140 /// materialise the project's `.env` file in one step.
141 ///
142 /// Performs the equivalent of `evault link NAME --project PATH`
143 /// followed by an optional `evault gen --project PATH`:
144 ///
145 /// 1. Resolves (or creates) the project record for `project_path`.
146 /// 2. Records the link in the registry's link table.
147 /// 3. Reads or creates `<project_path>/evault.toml` and adds a
148 /// binding pointing at the variable.
149 /// 4. If `materialize` is `true`, resolves every binding of the
150 /// given profile and writes `<project_path>/.env` (atomically,
151 /// with the `.gitignore` entry).
152 ///
153 /// # Errors
154 /// Returns [`ProviderError`] on missing variable, FS failure,
155 /// manifest serialization failure, or registry/secret-store
156 /// failure.
157 fn link_to_project(
158 &self,
159 var_id: VarId,
160 var_name: String,
161 project_path: std::path::PathBuf,
162 profile: String,
163 materialize: bool,
164 ) -> Result<(), ProviderError>;
165
166 /// Spawn a child process with the project's resolved environment
167 /// overlay injected — equivalent of `evault run --project PATH
168 /// [--profile P] -- CMD [ARGS...]` triggered from the TUI.
169 ///
170 /// The implementation loads `<project_path>/evault.toml`, resolves
171 /// every binding for `profile` (secrets via the secret store,
172 /// inline literals straight from the manifest), and spawns
173 /// `program` with `args` inheriting the parent's stdio so the
174 /// user interacts with the child directly.
175 ///
176 /// **Terminal lifecycle is the caller's responsibility.** The TUI
177 /// runtime restores the terminal before invoking this method and
178 /// re-enters raw mode + alternate screen after it returns, so the
179 /// implementation may safely block on the child without corrupting
180 /// the parent's screen.
181 ///
182 /// Returns the child's exit code (`None` if killed by a signal).
183 ///
184 /// # Errors
185 /// Returns [`ProviderError`] on missing manifest, value
186 /// resolution failure, invalid command, or spawn / I/O failure.
187 fn run_in_project(
188 &self,
189 project_path: std::path::PathBuf,
190 profile: String,
191 program: String,
192 args: Vec<String>,
193 ) -> Result<Option<i32>, ProviderError>;
194}
195
196/// A drafted variable awaiting backend creation.
197///
198/// Carries the four fields the TUI's `n` (new-var) prompt captures
199/// from the user before emitting `DispatchOutcome::CreateRequested`.
200/// `value` is wrapped in [`SecretString`] so it gets zeroized on drop.
201#[derive(Debug, Clone)]
202pub struct VarDraft {
203 /// Variable name (validated by the registry on create).
204 pub name: String,
205 /// Logical group (`user` / `system` / `project` / custom).
206 pub group: Group,
207 /// Storage tier — secret (keyring) or plain (metadata DB).
208 pub kind: VarKind,
209 /// The value to store.
210 pub value: SecretString,
211}