keylight 0.3.0

Keylight licensing SDK — activate/validate licenses with offline Ed25519 lease verification.
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
# Keylight Rust SDK

[![Crates.io](https://img.shields.io/crates/v/keylight.svg)](https://crates.io/crates/keylight)
[![Documentation](https://docs.rs/keylight/badge.svg)](https://docs.rs/keylight)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Edition](https://img.shields.io/badge/rust-2024-orange.svg)](https://www.rust-lang.org)
[![MSRV](https://img.shields.io/badge/MSRV-1.85-blue.svg)](https://www.rust-lang.org)
[![Conformance](https://img.shields.io/badge/conformance-cross--SDK%20vectors-success.svg)](#conformance)

Open-source Rust SDK and Tauri plugin for [Keylight](https://keylight.dev) — license your Rust
apps, CLIs, daemons, and Tauri desktop apps with online activation and offline Ed25519 license
verification.

> **In one line:** a software-licensing SDK for Rust — license-key activation and validation,
> entitlement/feature gating, trials and free tiers, and tamper-resistant **offline license
> verification** (signed `v3` lease, Ed25519 + clock-skew tolerance) for CLIs, daemons, and
> desktop apps. Synchronous and runtime-free — no `async`/Tokio required.

## Why Keylight

Licensing shouldn't mean bolting a heavyweight, phone-home-or-die SDK onto your app.

- **Works offline.** The license is a signed lease your app verifies locally with Ed25519 — no
  network round-trip to gate a feature, no lockout when the machine is offline.
- **Tamper-resistant by design.** Entitlements live *inside* the signature; a forged or hand-edited
  lease can't pass verification without the tenant's private key.
- **Synchronous & runtime-free.** No `async`/Tokio, no background threads — you call `activate` /
  `refresh_if_needed` on launch and on events, and decide exactly when to check in. Ideal for CLIs
  and daemons.
- **One SDK family.** Verifies licenses identically to the Swift and JavaScript SDKs, proven by
  shared conformance vectors.

## Table of Contents

- [Why Keylight]#why-keylight
- [Features]#features
- [Packages]#packages
- [Quick Start]#quick-start
  - [Pure Rust]#pure-rust
  - [Tauri Apps]#tauri-apps
- [License Lifecycle]#license-lifecycle
- [License States]#license-states
- [Entitlements]#entitlements
- [Offline Validation]#offline-validation
- [Refresh, Trials & Free Tier]#refresh-trials--free-tier
- [Lifecycle Events]#lifecycle-events
- [Configuration Reference]#configuration-reference
- [CLI & Demo]#cli--demo
- [Conformance]#conformance
- [Documentation]#documentation
- [Other SDKs]#other-sdks
- [License]#license

## Features

- **License Lifecycle** — Activate, validate, and deactivate license keys with a small, explicit API.
- **Offline Verification** — The single offline artifact is a signed `v3` **lease**, verified with
  **Ed25519** and a 300-second clock-skew tolerance. An optional `max_offline_days` grace caps how
  long a device may run without checking in.
- **Synchronous & runtime-free** — Blocking HTTP (`ureq`); no `async`/Tokio, no background threads.
  You call `refresh_if_needed()` / `check_on_launch()` on launch and on app events. Ideal for CLIs
  and daemons.
- **Entitlements** — Feature gating from the cached lease: `has_entitlement("pro")`.
- **Trials & Free Tier** — Built-in local trial timer, free-tier mode, and an anonymous "keyless"
  usage beacon.
- **Lifecycle Events** — Optional callback fires `Renewed` / `Cancelled` / `Expired` / `Restored`
  as the resolved state changes.
- **Clock-Manipulation Detection** — Flags backward/forward system-clock tampering.
- **Device Telemetry** — Auto-attaches `sdk_version`, `platform`, and (optional) `app_version`.
- **Network Resilience** — Automatic retry with exponential backoff + jitter; honors `Retry-After`.
- **Secure by Default** — TLS via **rustls** (no OpenSSL), **ChaCha20-Poly1305** device-bound
  encrypted on-disk storage, and **no `unsafe`** in the SDK crate.
- **Pluggable** — Swap the storage backend (`LicenseStore`) or HTTP transport (`Transport`) via
  traits for tests or custom platforms.

## Packages

This workspace contains:

| Crate | Description | Distribution |
|-------|-------------|--------------|
| [`keylight`]./keylight | Core Rust SDK for any Rust application | [![crates.io]https://img.shields.io/crates/v/keylight.svg]https://crates.io/crates/keylight [![docs]https://docs.rs/keylight/badge.svg]https://docs.rs/keylight |
| [`keylight-cli`]./keylight-cli | Reference CLI / template for white-labeled `yourapp activate` commands (also a dev/ops & CI utility) | Prebuilt binaries on [GitHub Releases]https://github.com/keylight-dev/keylight-rust/releases |
| [`tauri-plugin-keylight`]./tauri-plugin-keylight | Tauri v2 plugin (Rust side) with capability permissions | [![crates.io]https://img.shields.io/crates/v/tauri-plugin-keylight.svg]https://crates.io/crates/tauri-plugin-keylight |
| [`tauri-plugin-keylight-api`]./tauri-plugin-keylight | Tauri v2 plugin JS/TS bindings (ESM/CJS + `.d.ts`) | [![npm]https://img.shields.io/npm/v/tauri-plugin-keylight-api.svg]https://www.npmjs.com/package/tauri-plugin-keylight-api |
| [`keylight-notes-demo`]./demo-app | "Keylight Notes" example app | Example (not published) |

## Quick Start

### Pure Rust

```bash
cargo add keylight
```

```rust
use keylight::{Keylight, KeylightConfig};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Build a config. Fetch the tenant's trusted Ed25519 keyset so leases can be
    // verified offline. (You can also pin keys explicitly with `.trusted_key(kid, pub_b64)`.)
    let mut cfg = KeylightConfig::builder("your-tenant", "your-product", "sdk_live_…")
        .key_prefix("PROD")        // optional client-side key-format check
        .max_offline_days(7)       // optional offline grace window
        .build();
    if let Some((_, keys)) = keylight::keyset::fetch_keyset(
        &keylight::http::ureq_transport::UreqTransport::default(),
        &cfg.base_url,
        &cfg.tenant_id,
    ) {
        cfg.trusted_keys.extend(keys);
    }

    let kl = Keylight::new(cfg)?;

    // Activate a license key (online). The returned lease is Ed25519-verified
    // *before* anything is persisted.
    let res = kl.activate("USER-LICENSE-KEY")?;
    println!("activated: {}", res.activated);

    // Gate features on entitlements — works offline from the cached lease.
    if kl.has_entitlement("pro") {
        println!("Pro features unlocked");
    }

    // Release the seat when uninstalling / switching devices.
    kl.deactivate()?;
    Ok(())
}
```

> Note the **synchronous** API — there is no `.await` and no async runtime to set up.

### Tauri Apps

Add the Rust-side Tauri v2 plugin and the JS bindings:

```bash
# Rust side
cargo add tauri-plugin-keylight
# JavaScript side
npm add tauri-plugin-keylight-api
```

Register it with a prebuilt `KeylightConfig` (your app supplies tenant/product/keys):

```rust
// src-tauri/src/main.rs
use keylight::KeylightConfig;

fn main() {
    let cfg = KeylightConfig::builder("your-tenant", "your-product", "sdk_live_…").build();
    tauri::Builder::default()
        .plugin(tauri_plugin_keylight::init(cfg))
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

Grant the plugin's default permission set in your capability file:

```json
// src-tauri/capabilities/default.json
{
  "permissions": ["keylight:default"]
}
```

`keylight:default` allows `activate`, `validate`, and `has_entitlement` (per-command permissions
`keylight:allow-activate` etc. are also generated).

Use the typed JS/TS bindings ([`tauri-plugin-keylight-api`](https://www.npmjs.com/package/tauri-plugin-keylight-api),
ESM/CJS + `.d.ts`) from your frontend:

```typescript
import { activate, validate, hasEntitlement } from 'tauri-plugin-keylight-api';

await activate('USER-LICENSE-KEY');
const ok = await validate();
if (await hasEntitlement('pro')) {
  // unlock pro features
}
```

## License Lifecycle

```
┌─────────────┐     ┌─────────────┐     ┌──────────────┐
│   activate  │────▶│   validate  │────▶│  deactivate  │
└─────────────┘     └─────────────┘     └──────────────┘
                          │ on launch / on events (no background threads)
                    ┌───────────────────┐
                    │ refresh_if_needed │
                    └───────────────────┘
```

| Method | Description |
|--------|-------------|
| `activate(key) -> ActivationResult` | Activates a key on this device. Verifies the returned lease before persisting; returns `instance_id`, the lease, and expiry. |
| `validate() -> ValidationResult` | Re-checks the stored license online. Decodes hard-expiry (`422`) responses and preserves fallback/expired leases so state can resolve. |
| `deactivate()` | Releases the seat and clears local license state. Call on uninstall or device switch. |
| `refresh_if_needed() -> Option<ValidationResult>` | Validates only if due (debounce 5 min, stale 6 h, or within 24 h of expiry). Safe to call often. |
| `check_on_launch()` | Convenience: refresh if a license is stored, else no-op. |

## License States

`state()` resolves a single high-level status from the cached lease, trial, and free-tier config
(no network):

| State | Meaning |
|-------|---------|
| `Licensed` | Current, signature-valid `active` lease. |
| `Limited` | Signature-valid `fallback` lease (grace mode). |
| `Trial { days_left }` | No license, but a local trial is active. |
| `FreeTier` | No license, free tier enabled. |
| `Expired` | Lease expired, or a license was stored but is no longer current. |
| `Invalid` | No license, no trial, no free tier. |

## Entitlements

Entitlements are feature keys carried inside the signed lease and checked offline:

```rust
if kl.has_entitlement("cloud-sync") {
    enable_cloud_sync();
}
```

`has_entitlement` returns `true` only when the cached lease is signature-valid, unexpired, and not
`expired`-status — so offline feature gating never disagrees with the resolved `Expired` state.

## Offline Validation

The offline artifact is a signed **`v3` lease** issued by the Keylight API. The SDK reconstructs
the exact signed payload (entitlements sorted, pipe-delimited) and verifies it with **Ed25519**
against the tenant's trusted keyset, applying a **300-second clock-skew** tolerance:

```rust
use keylight::KeylightConfig;

let cfg = KeylightConfig::builder("your-tenant", "your-product", "sdk_live_…")
    // Pin trusted keys explicitly instead of fetching them:
    .trusted_key("k1", "<raw ed25519 public key, base64>")
    .max_offline_days(7) // None = run offline as long as the lease itself is current
    .build();
```

- The trusted keyset can be fetched once from `GET /{tenant}/.well-known/keylight-keys`
  (`keylight::keyset::fetch_keyset`) or pinned at build time.
- `cached_lease()` returns the lease only when it is `kid`-known, signature-valid, unexpired, and
  (if set) within `max_offline_days` of the last online validation.
- Encrypted lease/key material is stored device-bound with **ChaCha20-Poly1305** (key derived from a
  per-device identity via BLAKE3) — copying the files to another machine won't decrypt them.

## Refresh, Trials & Free Tier

There are **no background threads**. The host drives refresh on launch and on meaningful events:

```rust
kl.check_on_launch()?;       // validate if due, on startup
kl.refresh_if_needed()?;     // call again on window-focus / purchase / resume
```

Trials and free tier are local and offline-first:

```rust
kl.start_trial()?;                       // begins the trial clock once
match kl.check_trial() {                 // NotStarted | Active { days_left } | Expired
    keylight::TrialStatus::Active { days_left } => println!("{days_left} days left"),
    _ => {}
}

// Anonymous, debounced usage beacon for trial / free-tier / expired devices:
kl.report_keyless_state(keylight::KeylessState::Trial);

// Tamper check and a pre-filled hosted upgrade link:
// `state()` already forces `Invalid` if the clock was rolled back past tolerance
// (the offline vector for reviving an expired lease). `is_clock_manipulated()`
// additionally surfaces large forward jumps if you want to react to them too.
let tampered = kl.is_clock_manipulated();
if let Some(url) = kl.upgrade_url() { println!("Upgrade: {url}"); }
```

## Lifecycle Events

Register a handler to react when the resolved state crosses a transition:

```rust
use keylight::{Keylight, LicenseLifecycleEvent};

let kl = Keylight::new(cfg)?
    .with_event_handler(|event| match event {
        LicenseLifecycleEvent::Renewed   => println!("license renewed"),
        LicenseLifecycleEvent::Cancelled => println!("dropped to limited/expired"),
        LicenseLifecycleEvent::Expired   => println!("license expired"),
        LicenseLifecycleEvent::Restored  => println!("license restored"),
    });
```

| Event | Fires when |
|-------|-----------|
| `Renewed` | Stayed `Licensed` and the expiry moved later. |
| `Cancelled` | `Licensed``Limited` or `Expired`. |
| `Expired` | Any state → `Expired`. |
| `Restored` | `Expired`/`Limited`/`Invalid``Licensed`. |

Events are evaluated during `validate()` and re-derive the previous state from the persisted lease,
so a transition won't re-fire across restarts.

## Configuration Reference

Built with `KeylightConfig::builder(tenant_id, product_id, sdk_key)`:

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `tenant_id` | `String` || Your Keylight tenant (required). |
| `product_id` | `String` || Your product (required). |
| `sdk_key` | `String` || Tenant SDK key (required), sent as `X-Keylight-SDK-Key` on every call. |
| `trusted_keys` | `map<kid, pub>` | empty | Trusted Ed25519 public keys for offline verification (`.trusted_key()` or `fetch_keyset`). |
| `max_offline_days` | `Option<u32>` | `None` | Offline grace window since last online validation. `None` = until the lease itself expires. |
| `trial_duration_days` | `u32` | `14` | Local trial length. |
| `free_tier_enabled` | `bool` | `false` | Resolve to `FreeTier` when there's no license/trial. |
| `app_version` | `Option<String>` | `None` | Reported in telemetry. |
| `base_url` | `String` | `https://api.keylight.dev` | API base URL. |
| `key_prefix` | `Option<String>` | `None` | Client-side key-format check (e.g. `"PROD"`). |

## CLI & Demo

### `keylight-cli` — a reference to build on, not a product to rebrand

`keylight-cli` is a **reference implementation**: a thin [clap](https://docs.rs/clap) wrapper
around the SDK (see [`keylight-cli/src/main.rs`](./keylight-cli/src/main.rs)). Its main purpose is
to be the worked example for **adding white-labeled licensing commands to your own CLI**.

You don't ship this binary renamed — you embed the `keylight` *library* in **your** tool, bake in
your tenant/product, and expose your own branded subcommand. The end user then just runs
`yourapp activate <KEY>` (no `--tenant`/`--product` to pass):

```rust
// In your CLI `mole`: `mole activate <KEY>`
Cmd::Activate { key } => {
    let kl = Keylight::new(KeylightConfig::builder("mole-co", "mole", "sdk_live_…").build())?;
    let unlocked = kl.activate(&key)?.activated;
    println!("{}", if unlocked { "Mole Pro unlocked 🎉" } else { "Invalid key" });
}
// gate features elsewhere:  if kl.has_entitlement("pro") { /* ... */ }
```

You can also run the **generic** binary as-is — useful for **local development, testing a tenant,
or exit-code gating in scripts/CI** (it is not a customer-facing tool):

```bash
cargo install --git https://github.com/keylight-dev/keylight-rust keylight-cli

keylight --tenant your-tenant --product your-product --fetch-keys activate USER-LICENSE-KEY
keylight --tenant your-tenant --product your-product validate || echo "license invalid"
```

### `keylight-notes-demo`

The demo app shows entitlement gating end-to-end (free = 3 notes; the `pro`
entitlement unlocks unlimited notes + export) against the live public demo tenant:

```bash
cargo run -p keylight-notes-demo -- add "first note"
cargo run -p keylight-notes-demo -- activate NOTES-PRO0-0000-0001
cargo run -p keylight-notes-demo -- export /tmp/notes.txt   # pro-only
```

## Conformance

The security-critical lease verifier is gated by Keylight's frozen **cross-SDK conformance vectors**
(`keylight/tests/conformance.rs`). The Rust verifier must agree with every vector on
`{ kid_known, signature_valid, expired }`, which keeps offline verification behavior identical
across the Keylight SDK family (Swift, Rust, …).

```bash
cargo test -p keylight --test conformance
```

## Documentation

- **API docs (docs.rs):** [docs.rs/keylight]https://docs.rs/keylight
- **Platform docs:** [docs.keylight.dev]https://docs.keylight.dev
- **Website:** [keylight.dev]https://keylight.dev
- **API host:** `https://api.keylight.dev`

## Other SDKs

| Platform | Status | Repository |
|----------|--------|------------|
| Swift (macOS/iOS) | Available | [keylight-swift]https://github.com/keylight-dev/keylight-swift |
| Rust (this repo) | Available | [keylight-rust]https://github.com/keylight-dev/keylight-rust |
| JavaScript/TypeScript | Available | [keylight-js]https://github.com/keylight-dev/keylight-js |
| C# · C++ | Planned | unified by the same cross-SDK conformance vectors |

## License

MIT License. See [LICENSE](LICENSE) for details.

---

<sub>Keylight Rust SDK — software licensing for Rust: license-key activation & validation, offline
Ed25519 lease verification, entitlement/feature gating, trials and free tiers, device-bound
encrypted storage, and a Tauri v2 plugin for CLIs, daemons, and desktop apps.</sub>