Keylight Rust SDK
Open-source Rust SDK and Tauri plugin for Keylight — 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
v3lease, Ed25519 + clock-skew tolerance) for CLIs, daemons, and desktop apps. Synchronous and runtime-free — noasync/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 callactivate/refresh_if_neededon 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
- Features
- Packages
- Quick Start
- License Lifecycle
- License States
- Entitlements
- Offline Validation
- Refresh, Trials & Free Tier
- Lifecycle Events
- Configuration Reference
- CLI & Demo
- Conformance
- Documentation
- Other SDKs
- License
Features
- License Lifecycle — Activate, validate, and deactivate license keys with a small, explicit API.
- Offline Verification — The single offline artifact is a signed
v3lease, verified with Ed25519 and a 300-second clock-skew tolerance. An optionalmax_offline_daysgrace caps how long a device may run without checking in. - Synchronous & runtime-free — Blocking HTTP (
ureq); noasync/Tokio, no background threads. You callrefresh_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/Restoredas 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
unsafein 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 |
Core Rust SDK for any Rust application | |
keylight-cli |
Reference CLI / template for white-labeled yourapp activate commands (also a dev/ops & CI utility) |
Prebuilt binaries on GitHub Releases |
tauri-plugin-keylight |
Tauri v2 plugin (Rust side) with capability permissions | |
tauri-plugin-keylight-api |
Tauri v2 plugin JS/TS bindings (ESM/CJS + .d.ts) |
|
keylight-notes-demo |
"Keylight Notes" example app | Example (not published) |
Quick Start
Pure Rust
use ;
Note the synchronous API — there is no
.awaitand no async runtime to set up.
Tauri Apps
Add the Rust-side Tauri v2 plugin and the JS bindings:
# Rust side
# JavaScript side
Register it with a prebuilt KeylightConfig (your app supplies tenant/product/keys):
// src-tauri/src/main.rs
use KeylightConfig;
Grant the plugin's default permission set in your capability file:
// src-tauri/capabilities/default.json
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,
ESM/CJS + .d.ts) from your frontend:
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:
if kl.has_entitlement
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:
use KeylightConfig;
let cfg = builder
// Pin trusted keys explicitly instead of fetching them:
.trusted_key
.max_offline_days // 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 iskid-known, signature-valid, unexpired, and (if set) withinmax_offline_daysof 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:
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:
kl.start_trial?; // begins the trial clock once
match kl.check_trial
// Anonymous, debounced usage beacon for trial / free-tier / expired devices:
kl.report_keyless_state;
// 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 = kl.upgrade_url
Lifecycle Events
Register a handler to react when the resolved state crosses a transition:
use ;
let kl = new?
.with_event_handler;
| 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 wrapper
around the SDK (see 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):
// In your CLI `mole`: `mole activate <KEY>`
Activate =>
// 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):
||
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:
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, …).
Documentation
- API docs (docs.rs): docs.rs/keylight
- Platform docs: docs.keylight.dev
- Website: keylight.dev
- API host:
https://api.keylight.dev
Other SDKs
| Platform | Status | Repository |
|---|---|---|
| Swift (macOS/iOS) | Available | keylight-swift |
| Rust (this repo) | Available | keylight-rust |
| JavaScript/TypeScript | Available | keylight-js |
| C# · C++ | Planned | unified by the same cross-SDK conformance vectors |
License
MIT License. See LICENSE for details.
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.