Skip to main content

hasp_core/
lib.rs

1//! Core contracts for the hasp secrets library.
2//!
3//! This crate defines the `Backend` trait, the error taxonomy,
4//! and the `Entry` type shared by every backend implementation.
5//! It intentionally has no profile, TTY, or config dependencies —
6//! those live in `hasp-cli`.
7
8pub mod audit;
9pub mod cache;
10pub mod error;
11pub mod field;
12pub mod hardening;
13pub mod proxy;
14pub mod retry;
15pub mod secret_mem;
16
17#[cfg(any(test, feature = "test-utils"))]
18pub mod test_utils;
19
20#[cfg(unix)]
21pub use audit::SyslogSink;
22pub use audit::{AuditEvent, AuditSink, CacheEvent, FileSink, NoopSink, StderrSink, Verb};
23pub use cache::{CacheKey, CachePolicy, ProcessCache};
24pub use error::{BackendFailureKind, Error};
25pub use field::{extract_field, extract_field_from_str};
26#[cfg(feature = "memory-lock")]
27pub use hardening::lock_secret_pages;
28pub use hardening::{
29    apply_mitigations, check_refusal_conditions, harden_process, install, HardenRefusal,
30    HardeningToken, MitigationOutcome,
31};
32pub use proxy::{is_no_proxy, resolve_proxy_from_env, ProxyConfig};
33pub use retry::RetryBackend;
34pub use secrecy::{ExposeSecret, SecretString};
35pub use subtle;
36
37use url::Url;
38
39/// Unified backend trait for secret stores.
40///
41/// Each backend owns its URL grammar and maps native errors into
42/// `hasp_core::Error`. Secrets are wrapped in `SecretString` at the
43/// earliest possible boundary — before returning from `get`.
44///
45/// Implementors must ensure that secret values never appear in
46/// `Debug` output or error messages.
47pub trait Backend: Send + Sync {
48    /// Returns the URL scheme this backend handles (e.g., `"env"`).
49    fn scheme(&self) -> &'static str;
50
51    /// Fetch the secret at the given URL.
52    ///
53    /// # Errors
54    ///
55    /// Returns `Error::NotFound` if the secret does not exist.
56    /// Returns `Error::Backend { kind: Transient, .. }` for retryable
57    /// platform failures.
58    fn get(&self, url: &Url) -> Result<SecretString, Error>;
59
60    /// Store a secret at the given URL.
61    ///
62    /// # Errors
63    ///
64    /// Returns `Error::UnsupportedOperation` if the backend is
65    /// read-only (e.g., `env://`).
66    fn put(&self, url: &Url, value: &SecretString) -> Result<(), Error>;
67
68    /// List entries matching the URL prefix or pattern.
69    ///
70    /// # Errors
71    ///
72    /// Returns `Error::UnsupportedOperation` if listing is not
73    /// supported by the backend.
74    fn list(&self, url: &Url) -> Result<Vec<Entry>, Error>;
75
76    /// Delete the secret at the given URL.
77    ///
78    /// # Errors
79    ///
80    /// Returns `Error::UnsupportedOperation` if deletion is not
81    /// supported by the backend.
82    fn delete(&self, url: &Url) -> Result<(), Error>;
83
84    /// Returns `true` if a secret exists at the given URL.
85    fn exists(&self, url: &Url) -> Result<bool, Error>;
86
87    /// Validate URL grammar without performing I/O.
88    ///
89    /// Backends override by delegating to their existing URL `TryFrom`.
90    /// Used by `Store::resolve` so `--explain` rejects the same URLs
91    /// `get` would — keeps the dry-run path honest about what an actual
92    /// operation would do. Default impl is a no-op for backends that
93    /// have no grammar to validate beyond the scheme.
94    fn validate(&self, _url: &Url) -> Result<(), Error> {
95        Ok(())
96    }
97}
98
99/// A named entry returned by `Backend::list`.
100///
101/// `name` is the human-readable identifier; `url` is the canonical
102/// address that can be passed back to `Store::get`.
103#[derive(Debug, Clone)]
104pub struct Entry {
105    pub name: String,
106    pub url: Url,
107}
108
109/// Extract the scheme prefix from a URL string.
110///
111/// Returns the substring before `://`, or an error if the separator
112/// is absent. This is the only central URL knowledge in `hasp-core`;
113/// all grammar validation lives in backend crates.
114pub fn scheme_from_url(url: &str) -> Result<&str, Error> {
115    url.split_once("://")
116        .map(|(scheme, _)| scheme)
117        .ok_or_else(|| Error::InvalidUrl("missing scheme separator".into()))
118}