licenseseat 0.5.3

Official Rust SDK for LicenseSeat - simple, secure software licensing
Documentation

LicenseSeat Rust SDK

Crates.io Documentation License: MIT Rust

Official Rust SDK for LicenseSeat — simple, secure software licensing for desktop apps, games, CLI tools, and plugins.

Table of Contents

Installation

Add to your Cargo.toml:

cargo add licenseseat

Or manually:

[dependencies]
licenseseat = "0.5.3"

Offline support is included in the default build. If you disable default features and still want machine-file / offline-token verification, add offline back explicitly:

cargo add licenseseat --no-default-features --features "native-tls,offline"

Quick Start

Use a pk_* publishable API key in client applications. Keep sk_* secret keys server-side only.

use licenseseat::{LicenseSeat, Config};

#[tokio::main]
async fn main() -> licenseseat::Result<()> {
    // 1. Create SDK instance
    let sdk = LicenseSeat::new(Config::new("pk_live_xxx", "your-product"));

    // 2. Activate a license (typically on first launch)
    let license = sdk.activate("USER-LICENSE-KEY").await?;
    println!("Activated on device: {}", license.device_id);

    // 3. Validate the license (on subsequent launches)
    let result = sdk.validate().await?;
    if result.valid {
        println!("License valid until: {:?}", result.license.expires_at);
    }

    // 4. Check entitlements for feature gating
    if sdk.has_entitlement("pro-features") {
        enable_pro_features();
    }

    // 5. Deactivate when uninstalling (releases the seat)
    sdk.deactivate().await?;

    Ok(())
}

License Lifecycle

Activation

Activation binds a license key to this device and consumes a seat:

use licenseseat::{LicenseSeat, Config, ActivationOptions};

let sdk = LicenseSeat::new(Config::new("pk_live_xxx", "product"));

// Simple activation
let license = sdk.activate("USER-LICENSE-KEY").await?;
println!("Fingerprint: {}", license.fingerprint());
println!("Activation ID: {:?}", license.activation_id);

// Activation with options
let license = sdk.activate_with_options(
    "USER-LICENSE-KEY",
    ActivationOptions {
        fingerprint: None,
        device_name: Some("John's MacBook".into()),
        ..Default::default()
    }
).await?;

When to activate:

  • First app launch with a new license key
  • When the user enters a different license key
  • After a deactivation (switching devices)

Validation

Validation checks the license status without consuming a new seat:

let result = sdk.validate().await?;

if result.valid {
    println!("License is valid!");
    println!("Plan: {}", result.license.plan_key);
    println!("Entitlements: {:?}", result.license.active_entitlements);
} else {
    match result.code.as_deref() {
        Some("license_expired") => show_renewal_prompt(),
        Some("device_limit_exceeded") => show_device_limit_error(),
        Some("license_suspended") => show_suspension_notice(),
        _ => show_generic_error(),
    }
}

// Check for warnings (e.g., expiring soon)
if let Some(warnings) = &result.warnings {
    for warning in warnings {
        println!("Warning: {}", warning);
    }
}

When to validate:

  • On app launch (after initial activation)
  • Periodically in the background (SDK does this automatically)
  • Before performing license-gated operations

Deactivation

Deactivation releases the seat, allowing activation on another device:

// Deactivate current device
sdk.deactivate().await?;
println!("Seat released successfully");

When to deactivate:

  • User clicks "Deactivate" in settings
  • During app uninstall (if you have an uninstaller)
  • When switching to a different license key

Entitlements

Entitlements provide fine-grained feature gating beyond simple license validity.

Quick Check

// Simple boolean check
if sdk.has_entitlement("cloud-sync") {
    enable_cloud_sync();
}

if sdk.has_entitlement("api-access") {
    enable_api_features();
}

Detailed Status

use licenseseat::EntitlementReason;

let status = sdk.check_entitlement("pro-features");

println!("Active: {}", status.active);
println!("Expires: {:?}", status.expires_at);

match status.reason {
    None if status.active => enable_feature(),
    Some(EntitlementReason::Expired) => {
        // Was active, now expired
        show_upgrade_prompt();
    }
    Some(EntitlementReason::NotFound) => {
        // Not included in the user's plan
        show_plan_upgrade_prompt();
    }
    Some(EntitlementReason::NoLicense) => {
        // No license is active
        show_activation_prompt();
    }
    _ => {}
}

List All Entitlements

if let Some(license) = sdk.current_license() {
    if let Some(validation) = license.validation {
        for entitlement in validation.license.active_entitlements {
            println!("Key: {}", entitlement.key);
            println!("Expires: {:?}", entitlement.expires_at);
            println!("Metadata: {:?}", entitlement.metadata);
        }
    }
}

Offline Validation

The Rust SDK now matches the C++ SDK's machine-file-first offline flow.

use licenseseat::{Config, OfflineFallbackMode};

let config = Config {
    api_key: "pk_live_xxx".into(),
    product_slug: "your-product".into(),

    // Fall back to locally cached offline artifacts when the network is unavailable.
    offline_fallback_mode: OfflineFallbackMode::Always,

    // Maximum time the app may continue operating without a successful online validation.
    max_offline_days: 7,

    // Optional pinned signing key. If omitted, the SDK fetches keys by `kid` on demand.
    signing_public_key: None,
    signing_key_id: None,

    // Legacy offline tokens are disabled by default. Machine files are preferred.
    enable_legacy_offline_tokens: false,

    ..Default::default()
};

let sdk = LicenseSeat::new(config);

Fallback Modes

Mode Behavior
NetworkOnly Always require network. Fail if offline. (Default)
Always Try online first, then fall back to a cached machine file or legacy offline token

How It Works

  1. Activation binds the license to a canonical device fingerprint.
  2. The SDK checks out a machine file from /machine-file after activation.
  3. The machine file is Ed25519-signed and AES-256-GCM encrypted using a key derived from license_key || fingerprint.
  4. When offline, the SDK verifies the signature, decrypts the payload, and enforces expiry / grace / fingerprint binding locally.
  5. Legacy offline tokens remain available only as an optional compatibility fallback via enable_legacy_offline_tokens.

Clock Tampering Protection

The SDK includes safeguards against clock manipulation:

  • Offline artifacts include nbf (not before) and exp (expiration) timestamps
  • Significant clock jumps are detected and flagged
  • Backward clock movement invalidates offline validation

Heartbeat & Seat Tracking

Heartbeats enable real-time seat tracking for concurrent user limits:

use std::time::Duration;

let config = Config {
    api_key: "pk_live_xxx".into(),
    product_slug: "your-product".into(),
    heartbeat_interval: Duration::from_secs(300), // 5 minutes
    ..Default::default()
};

let sdk = LicenseSeat::new(config);

// Manual heartbeat
let response = sdk.heartbeat().await?;
println!("Acknowledged at: {}", response.received_at);

Seat Release

If heartbeats stop (app crash, network loss, user closes app), the seat is released after the grace period configured in your LicenseSeat dashboard.

Continuous Heartbeat Loop

use tokio::time::interval;

let sdk = LicenseSeat::new(config);
let sdk_clone = sdk.clone();

tokio::spawn(async move {
    let mut ticker = interval(Duration::from_secs(300));

    loop {
        ticker.tick().await;

        match sdk_clone.heartbeat().await {
            Ok(resp) => println!("Heartbeat OK: {}", resp.received_at),
            Err(e) => eprintln!("Heartbeat failed: {}", e),
        }
    }
});

Event System

Subscribe to SDK events for reactive UI updates:

use licenseseat::{LicenseSeat, Config, EventKind};

let sdk = LicenseSeat::new(config);

// Get event receiver
let mut events = sdk.subscribe();

// Spawn event handler
tokio::spawn(async move {
    while let Ok(event) = events.recv().await {
        match event.kind {
            EventKind::ActivationSuccess => {
                update_ui_license_active();
            }
            EventKind::ActivationError => {
                show_activation_error();
            }
            EventKind::ValidationSuccess => {
                refresh_entitlements_ui();
            }
            EventKind::ValidationFailed => {
                show_validation_error();
            }
            EventKind::DeactivationSuccess => {
                reset_to_unlicensed_state();
            }
            EventKind::HeartbeatSuccess => {
                update_connection_indicator(true);
            }
            EventKind::HeartbeatError => {
                update_connection_indicator(false);
            }
            _ => {}
        }
    }
});

Event Types

Event Description
ActivationSuccess License successfully activated
ActivationError Activation failed (invalid key, limit exceeded, etc.)
ValidationSuccess License validated successfully
ValidationFailed Validation failed (expired, suspended, etc.)
DeactivationSuccess License deactivated, seat released
DeactivationError Deactivation failed
HeartbeatSuccess Server acknowledged heartbeat
HeartbeatError Heartbeat failed (network error, etc.)

Configuration

Full Configuration Example

use licenseseat::{Config, OfflineFallbackMode};
use std::time::Duration;

let config = Config {
    // Required
    api_key: "pk_live_xxx".into(),
    product_slug: "your-product".into(),

    // API endpoint (default: production)
    api_base_url: "https://licenseseat.com/api/v1".into(),

    // Background validation interval (default: 1 hour)
    auto_validate_interval: Duration::from_secs(3600),

    // Heartbeat interval (default: 5 minutes)
    heartbeat_interval: Duration::from_secs(300),

    // Offline validation (requires `offline` feature)
    offline_fallback_mode: OfflineFallbackMode::AllowOffline,
    max_offline_days: 7,

    // Telemetry
    telemetry_enabled: true,
    app_version: Some("1.2.3".into()),

    // Debug logging
    debug: false,
};

let sdk = LicenseSeat::new(config);

Configuration Reference

Option Type Default Description
api_key String Your publishable LicenseSeat API key (pk_*, required). Keep sk_* server-side only.
product_slug String Your product slug (required)
api_base_url String https://licenseseat.com/api/v1 API base URL
auto_validate_interval Duration 1 hour Background validation interval
heartbeat_interval Duration 5 minutes Heartbeat interval
offline_fallback_mode OfflineFallbackMode NetworkOnly Offline validation behavior
max_offline_days u32 0 Grace period for offline mode (days)
telemetry_enabled bool true Send device telemetry
app_version Option<String> None Your app version (for analytics)
debug bool false Enable debug logging

Error Handling

The SDK uses a unified Error type:

use licenseseat::{LicenseSeat, Config, Error};

async fn activate_license(sdk: &LicenseSeat, key: &str) {
    match sdk.activate(key).await {
        Ok(license) => {
            println!("Activated: {}", license.device_id);
        }
        Err(Error::InvalidLicenseKey) => {
            show_error("Invalid license key");
        }
        Err(Error::DeviceLimitExceeded) => {
            show_error("Too many devices. Deactivate one first.");
        }
        Err(Error::LicenseExpired) => {
            show_error("License has expired");
        }
        Err(Error::NetworkError(e)) => {
            show_error(&format!("Network error: {}", e));
        }
        Err(e) => {
            show_error(&format!("Unexpected error: {}", e));
        }
    }
}

Error Types

Error Description
InvalidLicenseKey The license key is invalid or doesn't exist
LicenseExpired The license has expired
LicenseSuspended The license has been suspended
DeviceLimitExceeded Maximum device limit reached
NotActivated Tried to validate/deactivate without activation
NetworkError Network request failed
OfflineValidationFailed Offline token invalid or expired
InvalidSignature Ed25519 signature verification failed

Telemetry & Privacy

The SDK collects minimal telemetry to help you understand your user base:

Collected automatically:

  • Device ID (hardware-based, stable identifier)
  • OS name and version
  • Platform (e.g., "macos-arm64")
  • SDK version

You can add:

  • App version via config.app_version

Not collected:

  • Personal information
  • File system data
  • Network information beyond API calls
  • User behavior or analytics

Disabling Telemetry

let config = Config {
    telemetry_enabled: false,
    ..Default::default()
};

Examples

DevHeartbeat

Simple demo showing the full license lifecycle:

LICENSESEAT_API_KEY=your_key \
LICENSESEAT_PRODUCT_SLUG=your_product \
LICENSESEAT_LICENSE_KEY=your_license \
cargo run --example dev_heartbeat

Stress Test

Comprehensive test covering 12 scenarios:

cargo run --example stress_test

Scenarios tested:

  1. Activation with valid key
  2. Validation after activation
  3. Heartbeat functionality
  4. Telemetry collection
  5. Entitlement checking
  6. Non-existent entitlement handling
  7. Offline configuration
  8. Event subscription
  9. Multiple subscriptions
  10. Concurrent operations
  11. Full lifecycle
  12. SDK cloning

Feature Flags

Feature Description Dependencies Added
default Uses rustls for TLS and enables offline machine-file support reqwest/rustls-tls, ed25519-dalek, sha2, base64, aes-gcm
native-tls Use system TLS instead reqwest/native-tls
offline Enable offline support when using --no-default-features ed25519-dalek, sha2, base64, aes-gcm

API Reference

Full API documentation is available at docs.rs/licenseseat.

Key Types

// Main SDK instance
pub struct LicenseSeat { ... }

// Configuration
pub struct Config { ... }
pub enum OfflineFallbackMode { NetworkOnly, Always }

// License data
pub struct License { ... }
pub enum LicenseStatus { Active, Expired, Suspended, Revoked }

// Entitlements
pub struct Entitlement { ... }
pub struct EntitlementStatus { ... }
pub enum EntitlementReason { Expired, NotFound, NoLicense }

// Events
pub struct Event { ... }
pub enum EventKind { ... }

// Responses
pub struct ValidationResult { ... }
pub struct ActivationResponse { ... }
pub struct DeactivationResponse { ... }
pub struct HeartbeatResponse { ... }

// Errors
pub enum Error { ... }
pub type Result<T> = std::result::Result<T, Error>;

License

MIT License. See LICENSE for details.