stealthreq 0.1.0

Trait-driven, human-like request mutation primitives for crawlers and scrapers.
Documentation
use serde::{Deserialize, Serialize};

use crate::{TimingJitter, TlsRotationPolicy};
use crate::{HeaderPolicyConfig, TlsRotationConfig};

/// Configure a stealth profile via TOML.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StealthProfileConfig {
    /// Minimum delay in milliseconds before sending a request.
    pub jitter_ms_min: u64,
    /// Maximum delay in milliseconds before sending a request.
    pub jitter_ms_max: u64,
    /// Number of random header families to include.
    pub header_budget: usize,
    /// Optional deterministic seed used for repeatable tests.
    pub seed: Option<u64>,
    /// Whether to rotate TLS fingerprints per request.
    pub rotate_tls: bool,
    /// Header preset section.
    pub headers: HeaderPolicyConfig,
    /// Optional custom TLS profile definitions.
    pub tls: TlsRotationConfig,
}

impl Default for StealthProfileConfig {
    fn default() -> Self {
        Self {
            jitter_ms_min: 80,
            jitter_ms_max: 350,
            header_budget: 4,
            seed: None,
            rotate_tls: true,
            headers: HeaderPolicyConfig::default(),
            tls: TlsRotationConfig::default(),
        }
    }
}

impl StealthProfileConfig {
    /// Parse this configuration from TOML text.
    pub fn from_toml(toml: &str) -> Result<Self, crate::StealthError> {
        let cfg: Self = toml::from_str(toml).map_err(|err| crate::StealthError::Config(err.to_string()))?;
        cfg.validate()?;
        Ok(cfg)
    }

    /// Build a concrete profile from this config.
    pub fn build(self) -> crate::StealthPolicy {
        let jitter = TimingJitter::new(self.jitter_ms_min, self.jitter_ms_max);
        let headers = self.headers.into_profile();
        let tls = TlsRotationPolicy::from_config(self.tls);

        crate::StealthPolicy::default()
            .with_seed(self.seed)
            .with_timing(jitter)
            .with_header_budget(self.header_budget)
            .with_headers(headers)
            .with_tls_rotation(tls)
            .with_rotate_tls(self.rotate_tls)
    }

    fn validate(&self) -> crate::Result<()> {
        if self.jitter_ms_min > self.jitter_ms_max {
            return Err(crate::StealthError::Config("jitter_ms_min cannot exceed jitter_ms_max".to_string()));
        }
        if self.header_budget == 0 {
            return Err(crate::StealthError::Config("header_budget must be >= 1".to_string()));
        }
        Ok(())
    }
}