Skip to main content

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}