# LicenseSeat Rust SDK
[](https://crates.io/crates/licenseseat)
[](https://docs.rs/licenseseat)
[](../../LICENSE)
[](https://www.rust-lang.org)
Official Rust SDK for [LicenseSeat](https://licenseseat.com) — simple, secure software licensing for desktop apps, games, CLI tools, and plugins.
## Table of Contents
- [Installation](#installation)
- [Quick Start](#quick-start)
- [License Lifecycle](#license-lifecycle)
- [Activation](#activation)
- [Validation](#validation)
- [Deactivation](#deactivation)
- [Entitlements](#entitlements)
- [Offline Validation](#offline-validation)
- [Heartbeat & Seat Tracking](#heartbeat--seat-tracking)
- [Event System](#event-system)
- [Configuration](#configuration)
- [Error Handling](#error-handling)
- [Telemetry & Privacy](#telemetry--privacy)
- [Examples](#examples)
- [Feature Flags](#feature-flags)
- [API Reference](#api-reference)
## Installation
Add to your `Cargo.toml`:
```bash
cargo add licenseseat
```
Or manually:
```toml
[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:
```bash
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.
```rust
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:
```rust
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:
```rust
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:
```rust
// 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
```rust
// Simple boolean check
if sdk.has_entitlement("cloud-sync") {
enable_cloud_sync();
}
if sdk.has_entitlement("api-access") {
enable_api_features();
}
```
### Detailed Status
```rust
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
```rust
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.
```rust
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
| `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:
```rust
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
```rust
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:
```rust
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
| `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
```rust
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
| `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:
```rust
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
| `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
```rust
let config = Config {
telemetry_enabled: false,
..Default::default()
};
```
## Examples
### DevHeartbeat
Simple demo showing the full license lifecycle:
```bash
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:
```bash
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
| `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](https://docs.rs/licenseseat).
### Key Types
```rust
// 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](../../LICENSE) for details.