Skip to main content

tauri_plugin_licenseseat/
lib.rs

1//! # Tauri Plugin for LicenseSeat
2//!
3//! This plugin provides LicenseSeat software licensing integration for Tauri apps.
4//!
5//! ## Features
6//!
7//! - License activation, validation, and deactivation
8//! - Machine-file-first offline validation with Ed25519 + AES-256-GCM
9//! - Automatic re-validation in the background
10//! - Entitlement checking for feature flags
11//! - Event emission to the frontend
12//!
13//! ## Installation
14//!
15//! Add the plugin to your `Cargo.toml`:
16//!
17//! ```toml
18//! [dependencies]
19//! tauri-plugin-licenseseat = "0.5.3"
20//! ```
21//!
22//! Register the plugin in your Tauri app:
23//!
24//! ```rust,ignore
25//! fn main() {
26//!     tauri::Builder::default()
27//!         .plugin(tauri_plugin_licenseseat::init())
28//!         .run(tauri::generate_context!())
29//!         .expect("error while running tauri application");
30//! }
31//! ```
32//!
33//! ## Configuration
34//!
35//! Use a `pk_*` publishable API key in Tauri applications.
36//! Keep `sk_*` secret keys server-side only.
37//!
38//! Add configuration to `tauri.conf.json`:
39//!
40//! ```json
41//! {
42//!   "plugins": {
43//!     "licenseseat": {
44//!       "apiKey": "pk_live_xxx",
45//!       "productSlug": "your-product"
46//!     }
47//!   }
48//! }
49//! ```
50//!
51//! ## JavaScript API
52//!
53//! ```typescript
54//! import { activate, validate, deactivate, checkEntitlement } from '@licenseseat/tauri-plugin';
55//!
56//! // Activate a license
57//! const license = await activate('LICENSE-KEY');
58//!
59//! // Check entitlements
60//! const hasPro = await checkEntitlement('pro-features');
61//! ```
62
63// Re-export the core SDK for Rust users
64pub use licenseseat;
65
66mod commands;
67mod config;
68mod error;
69
70use tauri::{
71    Emitter, Manager, Runtime,
72    plugin::{Builder, TauriPlugin},
73};
74
75pub use config::PluginConfig;
76pub use error::{Error, Result};
77
78fn resolve_env_placeholder(value: String) -> String {
79    if let Some(name) = value.strip_prefix('$') {
80        std::env::var(name).unwrap_or(value)
81    } else {
82        value
83    }
84}
85
86fn resolve_optional_env_placeholder(value: Option<String>) -> Option<String> {
87    value.map(resolve_env_placeholder)
88}
89
90/// Initialize the LicenseSeat plugin.
91///
92/// # Example
93///
94/// ```rust,ignore
95/// fn main() {
96///     tauri::Builder::default()
97///         .plugin(tauri_plugin_licenseseat::init())
98///         .run(tauri::generate_context!())
99///         .expect("error while running tauri application");
100/// }
101/// ```
102pub fn init<R: Runtime>() -> TauriPlugin<R, PluginConfig> {
103    Builder::<R, PluginConfig>::new("licenseseat")
104        .setup(|app, api| {
105            let config = api.config().clone();
106            let api_key = resolve_env_placeholder(config.api_key.clone());
107            let product_slug = resolve_env_placeholder(config.product_slug.clone());
108            let api_base_url = resolve_optional_env_placeholder(config.api_base_url.clone());
109            let storage_prefix = resolve_optional_env_placeholder(config.storage_prefix.clone());
110            let storage_path = resolve_optional_env_placeholder(config.storage_path.clone());
111            let device_identifier =
112                resolve_optional_env_placeholder(config.device_identifier.clone());
113            let signing_public_key =
114                resolve_optional_env_placeholder(config.signing_public_key.clone());
115            let signing_key_id = resolve_optional_env_placeholder(config.signing_key_id.clone());
116            let app_version = resolve_optional_env_placeholder(
117                config
118                    .app_version
119                    .clone()
120                    .or_else(|| Some(app.package_info().version.to_string())),
121            );
122            let app_build = resolve_optional_env_placeholder(
123                config
124                    .app_build
125                    .clone()
126                    .or_else(|| Some(app.package_info().name.clone())),
127            );
128
129            let offline_fallback_mode = match config.offline_fallback_mode.as_deref() {
130                Some("always")
131                | Some("allow_offline")
132                | Some("offline_first")
133                | Some("offlineFirst") => licenseseat::OfflineFallbackMode::Always,
134                _ => licenseseat::OfflineFallbackMode::NetworkOnly,
135            };
136
137            // Convert plugin config to SDK config
138            let sdk_config = licenseseat::Config {
139                api_key,
140                product_slug,
141                api_base_url: api_base_url
142                    .unwrap_or_else(|| "https://licenseseat.com/api/v1".into()),
143                storage_prefix: storage_prefix.unwrap_or_else(|| "licenseseat_".into()),
144                storage_path: storage_path.map(Into::into),
145                device_identifier,
146                signing_public_key,
147                signing_key_id,
148                auto_validate_interval: std::time::Duration::from_secs(
149                    config.auto_validate_interval.unwrap_or(3600),
150                ),
151                heartbeat_interval: std::time::Duration::from_secs(
152                    config.heartbeat_interval.unwrap_or(300),
153                ),
154                network_recheck_interval: std::time::Duration::from_secs(
155                    config.network_recheck_interval.unwrap_or(30),
156                ),
157                request_timeout: std::time::Duration::from_secs(
158                    config.timeout_seconds.unwrap_or(30),
159                ),
160                verify_ssl: config.verify_ssl.unwrap_or(true),
161                offline_fallback_mode,
162                offline_token_refresh_interval: std::time::Duration::from_secs(
163                    config.offline_token_refresh_interval.unwrap_or(72 * 3600),
164                ),
165                enable_legacy_offline_tokens: config.enable_legacy_offline_tokens.unwrap_or(false),
166                max_offline_days: config.max_offline_days.unwrap_or(0),
167                debug: config.debug.unwrap_or(false),
168                telemetry_enabled: config.telemetry_enabled.unwrap_or(true),
169                app_version,
170                app_build,
171                ..Default::default()
172            };
173
174            // Create the SDK instance and manage it
175            app.manage(sdk_config);
176            let sdk =
177                licenseseat::LicenseSeat::new(app.state::<licenseseat::Config>().inner().clone());
178            let event_sdk = sdk.clone();
179            let app_handle = app.clone();
180            tauri::async_runtime::spawn(async move {
181                let mut rx = event_sdk.subscribe();
182                while let Ok(event) = rx.recv().await {
183                    let event_name =
184                        format!("licenseseat://{}", event.kind.to_string().replace(':', "-"));
185                    let payload = commands::event_payload_to_json(event.data);
186                    let _ = app_handle.emit(&event_name, payload);
187                }
188            });
189            app.manage(sdk);
190
191            tracing::info!("LicenseSeat plugin initialized");
192            Ok(())
193        })
194        .invoke_handler(tauri::generate_handler![
195            commands::activate,
196            commands::validate_key,
197            commands::validate,
198            commands::deactivate,
199            commands::deactivate_key,
200            commands::heartbeat,
201            commands::heartbeat_key,
202            commands::get_status,
203            commands::get_client_status,
204            commands::is_online,
205            commands::get_fingerprint,
206            commands::restore_license,
207            commands::health,
208            commands::check_entitlement,
209            commands::get_entitlements,
210            commands::has_entitlement,
211            commands::get_license,
212            commands::get_state,
213            commands::get_admin_snapshot,
214            commands::get_latest_release,
215            commands::list_releases,
216            commands::generate_download_token,
217            commands::generate_offline_token,
218            commands::verify_offline_token,
219            commands::checkout_machine_file,
220            commands::fetch_signing_key,
221            commands::sync_offline_assets,
222            commands::verify_machine_file,
223            commands::reset,
224        ])
225        .build()
226}