licenseseat 0.5.3

Official Rust SDK for LicenseSeat - simple, secure software licensing
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
# LicenseSeat Rust SDK

[![Crates.io](https://img.shields.io/crates/v/licenseseat.svg)](https://crates.io/crates/licenseseat)
[![Documentation](https://docs.rs/licenseseat/badge.svg)](https://docs.rs/licenseseat)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](../../LICENSE)
[![Rust](https://img.shields.io/badge/rust-1.85%2B-orange.svg)](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

| 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:

```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

| 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

```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

| 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:

```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

| 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

```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

| 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](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.