astraguard 1.1.7

Official AstraGuard SDK - license validation, HWID binding, anti-debug, and offline cache for Rust applications
Documentation
//! Public types used throughout the AstraGuard SDK.

use serde::{Deserialize, Serialize};
use thiserror::Error;

/// All errors that the AstraGuard SDK can return.
#[derive(Debug, Error)]
pub enum AstraGuardError {
    /// HTTP transport error (network unreachable, TLS failure, timeout, etc.).
    #[error("network error: {0}")]
    Network(#[from] reqwest::Error),

    /// The server returned a non-success HTTP status.
    #[error("server error {status}: {body}")]
    ServerError { status: u16, body: String },

    /// JSON (de)serialization failed.
    #[error("json error: {0}")]
    Json(#[from] serde_json::Error),

    /// License is invalid, expired, banned, or otherwise rejected.
    #[error("license rejected: {0}")]
    LicenseRejected(String),

    /// HMAC signature on the server response did not match (possible MITM).
    #[error("response signature mismatch  -  possible tampering detected")]
    SignatureMismatch,

    /// `with_response_auth()` was never called - the SDK refuses to trust
    /// an unsigned response rather than skip verification silently.
    #[error("response auth key not configured - call with_response_auth() before validating")]
    ResponseAuthNotConfigured,

    /// HTTP client could not be initialized (TLS backend unavailable, etc.).
    #[error("failed to build HTTP client: {0}")]
    Build(String),
}

/// Convenience alias for `Result<T, AstraGuardError>`.
pub type Result<T> = std::result::Result<T, AstraGuardError>;

// ─── API response types ───────────────────────────────────────────────────────

/// The full response returned by the `/validate` and `/activate` endpoints.
///
/// Field names use `#[serde(rename = ...)]` to match the API's camelCase
/// JSON keys.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LicenseResponse {
    /// Whether the license is currently valid.
    ///
    /// `/validate` always sends this field explicitly. `/activate` does NOT -
    /// it sends `success` instead, so this defaults to `false` on deserialize
    /// and `AstraGuardClient::activate()` sets it from `success` right after
    /// parsing. Deserializing directly without going through `activate()`
    /// (not a supported usage) would see `false` for an actual success.
    #[serde(default)]
    pub valid: bool,
    /// Present on `/activate` responses instead of `valid` - `activate()`
    /// copies this into `valid` after parsing. Not present on `/validate`.
    #[serde(default)]
    pub success: Option<bool>,
    /// Machine-readable reason when `valid` is `false`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
    /// ISO 8601 expiry timestamp, or an empty string for lifetime licenses.
    #[serde(rename = "expiresAt", default, skip_serializing_if = "Option::is_none")]
    pub expires_at: Option<String>,
    /// Names of the feature flags enabled on this license. The API returns
    /// plain strings (not `{name, enabled}` objects) - only enabled features
    /// are ever included.
    #[serde(default)]
    pub features: Vec<String>,
    /// Remote key-value variables from the developer's product config.
    #[serde(default)]
    pub variables: std::collections::HashMap<String, String>,
    /// Set to `true` by the SDK when the response was loaded from the local
    /// offline cache rather than fetched live from the server.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_offline_cache: Option<bool>,

    // ─── Anti-MITM / anti-replay response-authentication fields ─────────────
    // Present when the server signs responses. The SDK verifies these against
    // the per-request nonce it generated, so a captured response cannot be
    // replayed via a mock server for a different (or fake) request.
    /// Base64 HMAC-SHA256 response token over `nonce|rts|valid|productId`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rt: Option<String>,
    /// Server timestamp (ms since epoch) the token was minted at.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rts: Option<i64>,
    /// The nonce the client sent, echoed back by the server.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rn: Option<String>,
}

/// The parsed representation of a validated license, returned by
/// [`AstraGuardClient::verify_license`].
#[derive(Debug, Clone)]
pub struct LicenseDetails {
    /// Raw API response, available for custom logic.
    pub raw: LicenseResponse,
    /// License key that was validated.
    pub license_key: String,
    /// Parsed expiry time, if any.
    pub expires_at: Option<std::time::SystemTime>,
    /// Whether this result came from the offline grace-period cache.
    pub is_offline: bool,
}

/// Response from the check-update endpoint.
///
/// Field names use `#[serde(rename = ...)]` to match the API's camelCase
/// JSON keys (`updateAvailable`, `latestVersion`, `downloadUrl`).
/// `latest_version` is `Option` because a product with no releases uploaded
/// yet returns a short `{ updateAvailable: false, message: "..." }` shape
/// with no `latestVersion` field at all.
#[derive(Debug, Clone, Deserialize)]
pub struct UpdateCheckResponse {
    /// Whether an update is available relative to the version sent.
    #[serde(rename = "updateAvailable")]
    pub has_update: bool,
    /// Latest published version string. `None` if the product has no
    /// releases uploaded yet.
    #[serde(rename = "latestVersion", default)]
    pub latest_version: Option<String>,
    /// Download URL for the latest release.
    #[serde(
        rename = "downloadUrl",
        default,
        skip_serializing_if = "Option::is_none"
    )]
    pub download_url: Option<String>,
    /// Changelog for the latest release.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub changelog: Option<String>,
    /// Present instead of the fields above when the product has no releases
    /// uploaded yet (e.g. "No releases available").
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}