truthlinked_sdk/
client.rs

1use crate::error::{Result, TruthlinkedError};
2use crate::license::LicenseKey;
3use crate::logging::{LoggingConfig, RequestLogger, RequestTimer};
4use crate::retry::{RetryConfig, RetryExecutor};
5use crate::signing::RequestSigner;
6use crate::types::*;
7use reqwest::{Client as HttpClient, StatusCode};
8use std::time::Duration;
9
10/// Truthlinked Authority Fabric API client
11///
12/// Provides type-safe access to the Truthlinked Authority Fabric API with
13/// enterprise-grade security and reliability features.
14///
15/// # Security Features
16/// - HTTPS-only communication (HTTP requests are rejected)
17/// - TLS certificate validation (no self-signed certificates)
18/// - License key memory protection (zeroized on drop)
19/// - Safe error handling (no credential leakage)
20/// - Connection pooling with reasonable limits
21/// - Request timeouts to prevent hanging
22///
23/// # Thread Safety
24/// This client is `Send + Sync` and can be safely shared across threads.
25/// Consider using `Arc<Client>` for shared access.
26///
27/// # Example
28/// ```rust,no_run
29/// use truthlinked_sdk::Client;
30///
31/// #[tokio::main]
32/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
33///     let client = Client::new(
34///         "https://api.truthlinked.org",
35///         std::env::var("TRUTHLINKED_LICENSE_KEY")?
36///     )?;
37///     
38///     let health = client.health().await?;
39///     println!("Server status: {}", health.status);
40///     Ok(())
41/// }
42/// ```
43pub struct Client {
44    /// HTTP client with security hardening and connection pooling
45    http_client: HttpClient,
46    /// Base URL for API requests (must be HTTPS)
47    base_url: String,
48    /// License key with automatic memory protection
49    license_key: LicenseKey,
50    /// Request signer for replay attack prevention
51    signer: RequestSigner,
52    /// Retry executor with exponential backoff
53    retry_executor: RetryExecutor,
54    /// Request/response logger with credential redaction
55    logger: RequestLogger,
56}
57
58impl Client {
59    /// Creates a new Truthlinked API client
60    /// 
61    /// # Arguments
62    /// * `base_url` - API base URL (must be HTTPS)
63    /// * `license_key` - Your Truthlinked license key
64    /// 
65    /// # Security Guarantees
66    /// - Enforces HTTPS only (HTTP requests are rejected at client creation)
67    /// - Uses rustls TLS implementation (no OpenSSL vulnerabilities)
68    /// - Validates TLS certificates (rejects self-signed certificates)
69    /// - Configures reasonable timeouts (prevents indefinite hanging)
70    /// - Enables connection pooling (improves performance and reliability)
71    /// 
72    /// # Errors
73    /// Returns `TruthlinkedError::InvalidRequest` if:
74    /// - Base URL does not start with "https://"
75    /// - HTTP client cannot be configured
76    /// 
77    /// # Example
78    /// ```rust,no_run
79    /// use truthlinked_sdk::Client;
80    /// 
81    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
82    /// let client = Client::new(
83    ///     "https://api.truthlinked.org",
84    ///     "tl_free_..."
85    /// )?;
86    /// # Ok(())
87    /// # }
88    /// ```
89    pub fn new(base_url: impl Into<String>, license_key: impl Into<String>) -> Result<Self> {
90        let base_url_string = base_url.into();
91        let license_key_string = license_key.into();
92        
93        // Enforce HTTPS
94        if !base_url_string.starts_with("https://") {
95            return Err(TruthlinkedError::InvalidRequest(
96                "Base URL must use HTTPS".to_string()
97            ));
98        }
99        
100        // Build HTTP client with security settings
101        let http_client = HttpClient::builder()
102            .timeout(Duration::from_secs(30))
103            .connect_timeout(Duration::from_secs(10))
104            .pool_idle_timeout(Duration::from_secs(90))
105            .pool_max_idle_per_host(10)
106            .https_only(true)  // Enforce HTTPS
107            .build()?;
108        
109        Ok(Self {
110            http_client,
111            signer: RequestSigner::new(&license_key_string),
112            base_url: base_url_string,
113            license_key: LicenseKey::new(license_key_string),
114            retry_executor: RetryExecutor::new(RetryConfig::production()),
115            logger: RequestLogger::new(LoggingConfig::production()),
116        })
117    }
118    
119    /// Create client with custom configuration (used by ClientBuilder)
120    pub(crate) fn with_config(
121        http_client: HttpClient,
122        base_url: String,
123        license_key: String,
124        retry_config: RetryConfig,
125        logging_config: LoggingConfig,
126    ) -> Result<Self> {
127        Ok(Self {
128            http_client,
129            base_url,
130            signer: RequestSigner::new(&license_key),
131            license_key: LicenseKey::new(license_key),
132            retry_executor: RetryExecutor::new(retry_config),
133            logger: RequestLogger::new(logging_config),
134        })
135    }
136    
137    /// Performs a health check against the Truthlinked API
138    /// 
139    /// This endpoint does not require authentication and can be used to verify
140    /// that the API is accessible and responding correctly.
141    /// 
142    /// # Returns
143    /// - `Ok(HealthResponse)` - Server is healthy and responding
144    /// - `Err(TruthlinkedError)` - Network error or server unavailable
145    /// 
146    /// # Example
147    /// ```rust,no_run
148    /// # use truthlinked_sdk::Client;
149    /// # #[tokio::main]
150    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
151    /// # let client = Client::new("https://api.truthlinked.org", "key")?;
152    /// let health = client.health().await?;
153    /// assert_eq!(health.status, "healthy");
154    /// # Ok(())
155    /// # }
156    /// ```
157    pub async fn health(&self) -> Result<HealthResponse> {
158        let url = format!("{}/health", self.base_url);
159        
160        self.retry_executor.execute(|| async {
161            let timer = RequestTimer::new();
162            let timestamp = RequestSigner::current_timestamp();
163            let signature = self.signer.sign_request("GET", "/health", b"", timestamp);
164            
165            // Log request
166            let timestamp_str = timestamp.to_string();
167            let headers = vec![
168                ("X-Timestamp", timestamp_str.as_str()),
169                ("X-Signature", signature.as_str()),
170            ];
171            self.logger.log_request("GET", &url, &headers, b"");
172            
173            match self.http_client
174                .get(&url)
175                .header("X-Timestamp", timestamp.to_string())
176                .header("X-Signature", signature)
177                .send()
178                .await
179            {
180                Ok(response) => {
181                    let status = response.status().as_u16();
182                    let response_headers = vec![]; // Would extract from response
183                    
184                    match response.status() {
185                        StatusCode::OK => {
186                            let body = response.bytes().await?;
187                            self.logger.log_response(status, &response_headers, &body, timer.elapsed());
188                            
189                            let health: HealthResponse = serde_json::from_slice(&body)?;
190                            Ok(health)
191                        }
192                        _ => {
193                            let body = response.bytes().await?;
194                            self.logger.log_response(status, &response_headers, &body, timer.elapsed());
195                            self.handle_error_status(StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR))
196                        }
197                    }
198                }
199                Err(e) => {
200                    self.logger.log_error("GET", &url, &e.to_string(), timer.elapsed());
201                    Err(e.into())
202                }
203            }
204        }).await
205    }
206    
207    /// Exchanges an SSO token for an Authority Fabric token
208    /// 
209    /// This operation requires a Professional tier license or higher.
210    /// The SSO token is validated and, if successful, an AF token is issued
211    /// with the requested scope (potentially narrowed based on policy).
212    /// 
213    /// # Arguments
214    /// * `sso_token` - Valid SSO token from your identity provider
215    /// * `requested_scope` - List of permissions requested (e.g., ["read:users"])
216    /// * `nonce` - 32-byte cryptographic nonce (prevents replay attacks)
217    /// * `channel_binding` - 32-byte channel binding (prevents MITM attacks)
218    /// 
219    /// # Security Notes
220    /// - Nonce must be cryptographically random and unique per request
221    /// - Channel binding should be derived from the TLS channel
222    /// - The granted scope may be narrower than requested based on policy
223    /// 
224    /// # Errors
225    /// - `Unauthorized` - Invalid license key or SSO token
226    /// - `Forbidden` - License tier doesn't support token exchange
227    /// - `InvalidRequest` - Malformed request parameters
228    /// 
229    /// # Example
230    /// ```rust,no_run
231    /// # use truthlinked_sdk::Client;
232    /// # #[tokio::main]
233    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
234    /// # let client = Client::new("https://api.truthlinked.org", "tl_pro_...")?;
235    /// use rand::Rng;
236    /// 
237    /// let nonce: [u8; 32] = rand::thread_rng().gen();
238    /// let channel_binding: [u8; 32] = rand::thread_rng().gen();
239    /// 
240    /// let response = client.exchange_token(
241    ///     "eyJ0eXAiOiJKV1QiLCJhbGc...",
242    ///     vec!["read:users".to_string()],
243    ///     nonce,
244    ///     channel_binding,
245    /// ).await?;
246    /// 
247    /// println!("AF Token: {}", response.af_token);
248    /// # Ok(())
249    /// # }
250    /// ```
251    pub async fn exchange_token(
252        &self,
253        sso_token: impl Into<String>,
254        requested_scope: Vec<String>,
255        nonce: [u8; 32],
256        channel_binding: [u8; 32],
257    ) -> Result<TokenResponse> {
258        let url = format!("{}/v1/tokens", self.base_url);
259        
260        let request = TokenRequest {
261            sso_token: sso_token.into(),
262            requested_scope,
263            nonce: hex::encode(nonce),
264            channel_binding: hex::encode(channel_binding),
265        };
266        
267        let response = self.http_client
268            .post(&url)
269            .header("Authorization", format!("Bearer {}", self.license_key.as_str()))
270            .json(&request)
271            .send()
272            .await?;
273        
274        self.handle_response(response).await
275    }
276    
277    /// Validate AF token
278    pub async fn validate_token(&self, token_id: impl Into<String>) -> Result<ValidateResponse> {
279        let url = format!("{}/v1/tokens/{}/validate", self.base_url, token_id.into());
280        
281        let response = self.http_client
282            .get(&url)
283            .header("Authorization", format!("Bearer {}", self.license_key.as_str()))
284            .send()
285            .await?;
286        
287        self.handle_response(response).await
288    }
289    
290    /// Retrieves shadow decisions showing breach prevention activity
291    /// 
292    /// Shadow mode runs your IAM decisions through the Authority Fabric policy
293    /// engine in parallel, identifying cases where IAM would have allowed access
294    /// but AF would have denied it (indicating a potential security breach).
295    /// 
296    /// This endpoint is available to all license tiers.
297    /// 
298    /// # Returns
299    /// A list of shadow decisions, where each decision represents a divergence
300    /// between IAM and AF policy evaluation. Decisions with `breach_prevented: true`
301    /// indicate cases where AF would have prevented a security breach.
302    /// 
303    /// # Example
304    /// ```rust,no_run
305    /// # use truthlinked_sdk::Client;
306    /// # #[tokio::main]
307    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
308    /// # let client = Client::new("https://api.truthlinked.org", "tl_free_...")?;
309    /// let decisions = client.get_shadow_decisions().await?;
310    /// 
311    /// let breaches_prevented = decisions.iter()
312    ///     .filter(|d| d.breach_prevented)
313    ///     .count();
314    /// 
315    /// println!("Breaches prevented: {}", breaches_prevented);
316    /// # Ok(())
317    /// # }
318    /// ```
319    pub async fn get_shadow_decisions(&self) -> Result<Vec<ShadowDecision>> {
320        let url = format!("{}/v1/shadow/decisions", self.base_url);
321        
322        let response = self.http_client
323            .get(&url)
324            .header("Authorization", format!("Bearer {}", self.license_key.as_str()))
325            .send()
326            .await?;
327        
328        self.handle_response(response).await
329    }
330    
331    /// Replay IAM logs through AF policy engine
332    pub async fn replay_iam_logs(
333        &self,
334        logs: Vec<String>,
335        adapter: impl Into<String>,
336    ) -> Result<ReplayResponse> {
337        let url = format!("{}/v1/shadow/replay", self.base_url);
338        
339        let request = ReplayRequest {
340            logs,
341            adapter: adapter.into(),
342        };
343        
344        let response = self.http_client
345            .post(&url)
346            .header("Authorization", format!("Bearer {}", self.license_key.as_str()))
347            .json(&request)
348            .send()
349            .await?;
350        
351        self.handle_response(response).await
352    }
353    
354    /// Get SOX compliance report
355    pub async fn get_sox_report(&self) -> Result<SoxReport> {
356        let url = format!("{}/v1/compliance/sox", self.base_url);
357        
358        let response = self.http_client
359            .get(&url)
360            .header("Authorization", format!("Bearer {}", self.license_key.as_str()))
361            .send()
362            .await?;
363        
364        self.handle_response(response).await
365    }
366    
367    /// Get PCI-DSS compliance report
368    pub async fn get_pci_report(&self) -> Result<PciReport> {
369        let url = format!("{}/v1/compliance/pci", self.base_url);
370        
371        let response = self.http_client
372            .get(&url)
373            .header("Authorization", format!("Bearer {}", self.license_key.as_str()))
374            .send()
375            .await?;
376        
377        self.handle_response(response).await
378    }
379    
380    /// Get audit logs
381    pub async fn get_audit_logs(&self) -> Result<Vec<AuditLog>> {
382        let url = format!("{}/v1/audit/logs", self.base_url);
383        
384        let response = self.http_client
385            .get(&url)
386            .header("Authorization", format!("Bearer {}", self.license_key.as_str()))
387            .send()
388            .await?;
389        
390        self.handle_response(response).await
391    }
392    
393    /// Get usage statistics
394    pub async fn get_usage(&self) -> Result<UsageResponse> {
395        let url = format!("{}/v1/usage", self.base_url);
396        
397        let response = self.http_client
398            .get(&url)
399            .header("Authorization", format!("Bearer {}", self.license_key.as_str()))
400            .send()
401            .await?;
402        
403        self.handle_response(response).await
404    }
405    
406    /// Handle HTTP response with proper error mapping
407    async fn handle_response<T: serde::de::DeserializeOwned>(
408        &self,
409        response: reqwest::Response,
410    ) -> Result<T> {
411        let status = response.status();
412        
413        match status {
414            StatusCode::OK => {
415                response.json::<T>().await.map_err(|_| TruthlinkedError::InvalidResponse)
416            }
417            StatusCode::UNAUTHORIZED => Err(TruthlinkedError::Unauthorized),
418            StatusCode::FORBIDDEN => Err(TruthlinkedError::Forbidden),
419            StatusCode::TOO_MANY_REQUESTS => {
420                let body = response.text().await.unwrap_or_default();
421                Err(TruthlinkedError::RateLimitExceeded(body))
422            }
423            StatusCode::BAD_REQUEST | StatusCode::UNPROCESSABLE_ENTITY => {
424                let body = response.text().await.unwrap_or_default();
425                Err(TruthlinkedError::InvalidRequest(body))
426            }
427            _ if status.is_server_error() => Err(TruthlinkedError::ServerError),
428            _ => Err(TruthlinkedError::InvalidResponse),
429        }
430    }
431
432    // ========== Witness Chain Methods ==========
433
434    /// Submit event to witness chain
435    pub async fn submit_witness(&self, submission: WitnessSubmission) -> Result<WitnessEvent> {
436        let url = format!("{}/witness/submit", self.base_url);
437        
438        let response = self.http_client
439            .post(&url)
440            .header("Authorization", format!("Bearer {}", self.license_key.as_str()))
441            .json(&serde_json::json!({ "submission": submission }))
442            .send()
443            .await?;
444        
445        self.handle_response(response).await
446    }
447
448    /// Get witness event by sequence number
449    pub async fn get_witness_event(&self, sequence: u64, include_proof: bool) -> Result<WitnessEvent> {
450        let url = format!("{}/witness/event/{}", self.base_url, sequence);
451        
452        let response = self.http_client
453            .get(&url)
454            .header("Authorization", format!("Bearer {}", self.license_key.as_str()))
455            .query(&[("include_proof", include_proof.to_string())])
456            .send()
457            .await?;
458        
459        self.handle_response(response).await
460    }
461
462    /// Get latest signed tree head
463    pub async fn get_latest_sth(&self) -> Result<SignedTreeHead> {
464        let url = format!("{}/witness/sth/latest", self.base_url);
465        
466        let response = self.http_client
467            .get(&url)
468            .header("Authorization", format!("Bearer {}", self.license_key.as_str()))
469            .send()
470            .await?;
471        
472        self.handle_response(response).await
473    }
474
475    /// Get signed tree head at specific tree size
476    pub async fn get_sth(&self, tree_size: u64) -> Result<SignedTreeHead> {
477        let url = format!("{}/witness/sth/{}", self.base_url, tree_size);
478        
479        let response = self.http_client
480            .get(&url)
481            .header("Authorization", format!("Bearer {}", self.license_key.as_str()))
482            .send()
483            .await?;
484        
485        self.handle_response(response).await
486    }
487
488    /// Export witness chain segment
489    pub async fn export_witness_chain(&self, start_seq: Option<u64>, end_seq: Option<u64>) -> Result<Vec<u8>> {
490        let url = format!("{}/witness/export", self.base_url);
491        
492        let mut request = self.http_client
493            .get(&url)
494            .header("Authorization", format!("Bearer {}", self.license_key.as_str()));
495
496        if let Some(start) = start_seq {
497            request = request.query(&[("start_seq", start.to_string())]);
498        }
499        if let Some(end) = end_seq {
500            request = request.query(&[("end_seq", end.to_string())]);
501        }
502
503        let response = request.send().await?;
504        
505        if !response.status().is_success() {
506            return self.handle_error_status(response.status());
507        }
508
509        Ok(response.bytes().await?.to_vec())
510    }
511
512    /// Check witness chain health
513    pub async fn witness_health(&self) -> Result<WitnessHealthResponse> {
514        let url = format!("{}/witness/health", self.base_url);
515        
516        let response = self.http_client
517            .get(&url)
518            .header("Authorization", format!("Bearer {}", self.license_key.as_str()))
519            .send()
520            .await?;
521        
522        self.handle_response(response).await
523    }
524
525    /// Handle HTTP error status codes
526    fn handle_error_status<T>(&self, status: StatusCode) -> Result<T> {
527        match status {
528            StatusCode::UNAUTHORIZED => Err(TruthlinkedError::Unauthorized),
529            StatusCode::FORBIDDEN => Err(TruthlinkedError::Forbidden),
530            StatusCode::TOO_MANY_REQUESTS => {
531                Err(TruthlinkedError::RateLimitExceeded("Rate limit exceeded".to_string()))
532            }
533            StatusCode::BAD_REQUEST | StatusCode::UNPROCESSABLE_ENTITY => {
534                Err(TruthlinkedError::InvalidRequest("Invalid request".to_string()))
535            }
536            _ if status.is_server_error() => Err(TruthlinkedError::ServerError),
537            _ => Err(TruthlinkedError::InvalidResponse),
538        }
539    }
540}
541
542impl std::fmt::Debug for Client {
543    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
544        f.debug_struct("Client")
545            .field("base_url", &self.base_url)
546            .field("license_key", &self.license_key.redacted())
547            .finish()
548    }
549}