Skip to main content

darkstrata_credential_check/
types.rs

1//! Types and structs for the DarkStrata Credential Check SDK.
2
3use chrono::{DateTime, NaiveDate, Utc};
4use serde::{Deserialize, Serialize};
5use std::time::Duration;
6
7/// Configuration options for the DarkStrata client.
8#[derive(Debug, Clone)]
9pub struct ClientOptions {
10    /// Required: API key (JWT token) for authentication.
11    pub api_key: String,
12    /// Base URL for the API. Defaults to `https://api.darkstrata.io/v1/`.
13    pub base_url: Option<String>,
14    /// Request timeout. Defaults to 30 seconds.
15    pub timeout: Option<Duration>,
16    /// Number of retry attempts for transient failures. Defaults to 3.
17    pub retries: Option<u32>,
18    /// Enable response caching. Defaults to true.
19    pub enable_caching: Option<bool>,
20    /// Cache time-to-live. Defaults to 1 hour.
21    pub cache_ttl: Option<Duration>,
22}
23
24impl ClientOptions {
25    /// Create new client options with the given API key.
26    pub fn new(api_key: impl Into<String>) -> Self {
27        Self {
28            api_key: api_key.into(),
29            base_url: None,
30            timeout: None,
31            retries: None,
32            enable_caching: None,
33            cache_ttl: None,
34        }
35    }
36
37    /// Set the base URL.
38    pub fn base_url(mut self, url: impl Into<String>) -> Self {
39        self.base_url = Some(url.into());
40        self
41    }
42
43    /// Set the request timeout.
44    pub fn timeout(mut self, timeout: Duration) -> Self {
45        self.timeout = Some(timeout);
46        self
47    }
48
49    /// Set the number of retries.
50    pub fn retries(mut self, retries: u32) -> Self {
51        self.retries = Some(retries);
52        self
53    }
54
55    /// Enable or disable caching.
56    pub fn enable_caching(mut self, enable: bool) -> Self {
57        self.enable_caching = Some(enable);
58        self
59    }
60
61    /// Set the cache TTL.
62    pub fn cache_ttl(mut self, ttl: Duration) -> Self {
63        self.cache_ttl = Some(ttl);
64        self
65    }
66}
67
68/// Resolved client configuration with defaults applied.
69#[derive(Debug, Clone)]
70pub(crate) struct ResolvedConfig {
71    pub api_key: String,
72    pub base_url: String,
73    pub timeout: Duration,
74    pub retries: u32,
75    pub enable_caching: bool,
76    pub cache_ttl: Duration,
77}
78
79/// Options for individual check operations.
80#[derive(Debug, Clone, Default)]
81pub struct CheckOptions {
82    /// Custom 256-bit HMAC key (64+ hex characters) for deterministic results.
83    /// When provided, bypasses server-side HMAC rotation.
84    pub client_hmac: Option<String>,
85    /// Filter results to breaches since this date/time.
86    /// Can be specified as epoch day (days since Unix epoch) or Unix timestamp.
87    pub since: Option<SinceFilter>,
88}
89
90impl CheckOptions {
91    /// Create new check options.
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    /// Set a custom HMAC key.
97    pub fn client_hmac(mut self, hmac: impl Into<String>) -> Self {
98        self.client_hmac = Some(hmac.into());
99        self
100    }
101
102    /// Filter by date using a DateTime.
103    pub fn since_datetime(mut self, datetime: DateTime<Utc>) -> Self {
104        self.since = Some(SinceFilter::DateTime(datetime));
105        self
106    }
107
108    /// Filter by date using a NaiveDate.
109    pub fn since_date(mut self, date: NaiveDate) -> Self {
110        self.since = Some(SinceFilter::Date(date));
111        self
112    }
113
114    /// Filter by epoch day (days since Unix epoch).
115    pub fn since_epoch_day(mut self, epoch_day: u32) -> Self {
116        self.since = Some(SinceFilter::EpochDay(epoch_day));
117        self
118    }
119
120    /// Filter by Unix timestamp (seconds since Unix epoch).
121    pub fn since_timestamp(mut self, timestamp: i64) -> Self {
122        self.since = Some(SinceFilter::Timestamp(timestamp));
123        self
124    }
125}
126
127/// Filter for specifying a "since" date for breach results.
128#[derive(Debug, Clone)]
129pub enum SinceFilter {
130    /// Filter by DateTime.
131    DateTime(DateTime<Utc>),
132    /// Filter by NaiveDate.
133    Date(NaiveDate),
134    /// Filter by epoch day (days since Unix epoch).
135    EpochDay(u32),
136    /// Filter by Unix timestamp (seconds since Unix epoch).
137    Timestamp(i64),
138}
139
140impl SinceFilter {
141    /// Convert the filter to an epoch day value for the API.
142    pub fn to_epoch_day(&self) -> u32 {
143        match self {
144            SinceFilter::DateTime(dt) => (dt.timestamp() / 86400) as u32,
145            SinceFilter::Date(date) => {
146                let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
147                date.signed_duration_since(epoch).num_days() as u32
148            }
149            SinceFilter::EpochDay(day) => *day,
150            SinceFilter::Timestamp(ts) => (*ts / 86400) as u32,
151        }
152    }
153}
154
155/// A credential (email and password pair) to check.
156#[derive(Debug, Clone)]
157pub struct Credential {
158    /// The email address.
159    pub email: String,
160    /// The password.
161    pub password: String,
162}
163
164impl Credential {
165    /// Create a new credential.
166    pub fn new(email: impl Into<String>, password: impl Into<String>) -> Self {
167        Self {
168            email: email.into(),
169            password: password.into(),
170        }
171    }
172}
173
174/// Result of a credential check.
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct CheckResult {
177    /// Whether the credential was found in a data breach.
178    pub found: bool,
179    /// Information about the credential that was checked.
180    pub credential: CredentialInfo,
181    /// Metadata about the check operation.
182    pub metadata: CheckMetadata,
183}
184
185/// Information about the checked credential.
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct CredentialInfo {
188    /// The email address that was checked.
189    pub email: String,
190    /// Always true - password is never included in results for security.
191    pub masked: bool,
192}
193
194impl CredentialInfo {
195    /// Create credential info for a checked email.
196    pub fn new(email: impl Into<String>) -> Self {
197        Self {
198            email: email.into(),
199            masked: true,
200        }
201    }
202
203    /// Create masked credential info (for hash-only checks).
204    pub fn masked() -> Self {
205        Self {
206            email: "[hash]".to_string(),
207            masked: true,
208        }
209    }
210}
211
212/// Metadata about a check operation.
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct CheckMetadata {
215    /// The 5 or 6-character hash prefix used for k-anonymity.
216    pub prefix: String,
217    /// Total number of matching hashes returned by the API.
218    pub total_results: usize,
219    /// Source of the HMAC key used.
220    pub hmac_source: HmacSource,
221    /// Server time window (only present when using server HMAC).
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub time_window: Option<i64>,
224    /// The epoch day filter that was applied (if any).
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub filter_since: Option<u32>,
227    /// Whether this result was served from cache.
228    pub cached_result: bool,
229    /// When the check was performed.
230    pub checked_at: DateTime<Utc>,
231}
232
233/// Source of the HMAC key used for hashing.
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
235#[serde(rename_all = "lowercase")]
236pub enum HmacSource {
237    /// HMAC key provided by the server (rotates hourly).
238    Server,
239    /// HMAC key provided by the client (deterministic results).
240    Client,
241}
242
243impl std::fmt::Display for HmacSource {
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        match self {
246            HmacSource::Server => write!(f, "server"),
247            HmacSource::Client => write!(f, "client"),
248        }
249    }
250}
251
252impl std::str::FromStr for HmacSource {
253    type Err = String;
254
255    fn from_str(s: &str) -> Result<Self, Self::Err> {
256        match s.to_lowercase().as_str() {
257            "server" => Ok(HmacSource::Server),
258            "client" => Ok(HmacSource::Client),
259            _ => Err(format!("Invalid HMAC source: {}", s)),
260        }
261    }
262}
263
264/// Internal representation of API response.
265#[derive(Debug, Clone)]
266pub(crate) struct ApiResponse {
267    /// List of HMAC'd hashes from the API.
268    pub hashes: Vec<String>,
269    /// The prefix that was queried.
270    #[allow(dead_code)]
271    pub prefix: String,
272    /// The HMAC key used by the API.
273    pub hmac_key: String,
274    /// Source of the HMAC key.
275    pub hmac_source: HmacSource,
276    /// Server time window (if server HMAC).
277    pub time_window: Option<i64>,
278    /// Filter since epoch day (if applied).
279    pub filter_since: Option<u32>,
280}
281
282/// Internal credential with pre-computed hash.
283#[derive(Debug, Clone)]
284pub(crate) struct HashedCredential {
285    /// Original credential (if available).
286    pub credential: Option<Credential>,
287    /// SHA-256 hash of the credential.
288    pub hash: String,
289    /// First 5 or 6 characters of the hash (k-anonymity prefix).
290    pub prefix: String,
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn test_client_options_builder() {
299        let options = ClientOptions::new("test-key")
300            .base_url("https://custom.api.com/")
301            .timeout(Duration::from_secs(60))
302            .retries(5)
303            .enable_caching(false)
304            .cache_ttl(Duration::from_secs(1800));
305
306        assert_eq!(options.api_key, "test-key");
307        assert_eq!(
308            options.base_url,
309            Some("https://custom.api.com/".to_string())
310        );
311        assert_eq!(options.timeout, Some(Duration::from_secs(60)));
312        assert_eq!(options.retries, Some(5));
313        assert_eq!(options.enable_caching, Some(false));
314        assert_eq!(options.cache_ttl, Some(Duration::from_secs(1800)));
315    }
316
317    #[test]
318    fn test_check_options_builder() {
319        let options = CheckOptions::new()
320            .client_hmac("abc123")
321            .since_epoch_day(19724);
322
323        assert_eq!(options.client_hmac, Some("abc123".to_string()));
324        assert!(matches!(options.since, Some(SinceFilter::EpochDay(19724))));
325    }
326
327    #[test]
328    fn test_since_filter_to_epoch_day() {
329        // Epoch day 0 = 1970-01-01
330        let filter = SinceFilter::EpochDay(19723);
331        assert_eq!(filter.to_epoch_day(), 19723);
332
333        // Timestamp: 1704067200 = 2024-01-01 00:00:00 UTC
334        // 1704067200 / 86400 = 19723 days since epoch
335        let filter = SinceFilter::Timestamp(1704067200);
336        assert_eq!(filter.to_epoch_day(), 19723);
337    }
338
339    #[test]
340    fn test_hmac_source_parsing() {
341        assert_eq!("server".parse::<HmacSource>().unwrap(), HmacSource::Server);
342        assert_eq!("client".parse::<HmacSource>().unwrap(), HmacSource::Client);
343        assert_eq!("SERVER".parse::<HmacSource>().unwrap(), HmacSource::Server);
344        assert!("invalid".parse::<HmacSource>().is_err());
345    }
346
347    #[test]
348    fn test_credential_info() {
349        let info = CredentialInfo::new("test@example.com");
350        assert_eq!(info.email, "test@example.com");
351        assert!(info.masked);
352
353        let masked = CredentialInfo::masked();
354        assert_eq!(masked.email, "[hash]");
355        assert!(masked.masked);
356    }
357}