Skip to main content

tauri_plugin_licensemanager/
lib.rs

1#![deny(missing_docs)]
2//! Tauri v2 plugin for [licensemanager.dev](https://licensemanager.dev): license
3//! key activation, offline tokens, background renewal, clock tampering
4//! detection and machine fingerprinting.
5//!
6//! Register the plugin with a [`Builder`], then reach the [`LicenseManager`]
7//! with [`LicensemanagerExt::license`]:
8//!
9//! ```no_run
10//! use std::time::Duration;
11//! use tauri_plugin_licensemanager::{LicenseState, LicensemanagerExt};
12//!
13//! const PUBLIC_KEY: &str = "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----";
14//!
15//! #[tauri::command]
16//! fn export_pdf(app: tauri::AppHandle) -> Result<(), String> {
17//!     if app.license().state() != LicenseState::Active {
18//!         return Err("A license is required".into());
19//!     }
20//!     Ok(())
21//! }
22//!
23//! // `context` is `tauri::generate_context!()` in an app.
24//! fn run(context: tauri::Context) {
25//!     tauri::Builder::default()
26//!         .plugin(
27//!             tauri_plugin_licensemanager::Builder::new("x7Kp2QaZ", PUBLIC_KEY)
28//!                 .min_offline_duration(Duration::from_secs(14 * 86_400))
29//!                 .build(),
30//!         )
31//!         .invoke_handler(tauri::generate_handler![export_pdf])
32//!         .run(context)
33//!         .expect("error while running tauri application");
34//! }
35//! # fn main() {}
36//! ```
37//!
38//! See <https://licensemanager.dev/docs/tauri>.
39
40use std::path::PathBuf;
41use std::time::Duration;
42
43use tauri::plugin::TauriPlugin;
44use tauri::{Manager, RunEvent, Runtime};
45
46mod commands;
47mod core;
48mod models;
49
50use crate::core::options::{LicenseManagerOptions, TestHooks};
51pub use crate::core::{
52    ConfigError, LatestAllowedVersion, LicenseError, LicenseErrorCode, LicenseManager,
53    LicenseState, MachineFingerprint, OfflineState, Subscription,
54};
55pub use crate::models::{LicenseErrorDto, OfflineStateDto, Snapshot};
56
57/// Name of this SDK.
58pub const SDK_NAME: &str = "tauri-plugin-licensemanager";
59
60/// Event emitted to every webview on each state change, with a [`Snapshot`].
61pub const STATE_CHANGED_EVENT: &str = "licensemanager://state-changed";
62
63const MOBILE_ERROR: &str =
64    "tauri-plugin-licensemanager supports desktop only (macOS, Windows, Linux)";
65
66/// Configures and builds the plugin.
67///
68/// Configuration is in code only: hard-code the public key in the app.
69#[derive(Debug)]
70#[must_use = "call `.build()` and pass the result to `tauri::Builder::plugin`"]
71pub struct Builder {
72    product_id: String,
73    public_key: String,
74    fingerprint: Option<MachineFingerprint>,
75    version: Option<String>,
76    machine_name: Option<String>,
77    platform: Option<String>,
78    min_offline_duration: Duration,
79    api_base_url: Option<String>,
80    storage_dirs: Option<Vec<PathBuf>>,
81    testing: Option<TestHooks>,
82}
83
84impl Builder {
85    /// A plugin for `product_id`, verifying tokens with `public_key`, the
86    /// product's Ed25519 public key (SPKI PEM).
87    pub fn new(product_id: impl Into<String>, public_key: impl Into<String>) -> Self {
88        Self {
89            product_id: product_id.into(),
90            public_key: public_key.into(),
91            fingerprint: None,
92            version: None,
93            machine_name: None,
94            platform: None,
95            min_offline_duration: Duration::ZERO,
96            api_base_url: None,
97            storage_dirs: None,
98            testing: None,
99        }
100    }
101
102    /// Machine component of the fingerprint, replacing the native machine id
103    /// (still hashed). A string is used verbatim; see
104    /// [`MachineFingerprint::from_fn`] for a function.
105    pub fn fingerprint(mut self, fingerprint: impl Into<MachineFingerprint>) -> Self {
106        self.fingerprint = Some(fingerprint.into());
107        self
108    }
109
110    /// App version, sent on validation and checked offline against the
111    /// license's max version. Defaults to the version in `tauri.conf.json`
112    /// (or `Cargo.toml`).
113    pub fn version(mut self, version: impl Into<String>) -> Self {
114        self.version = Some(version.into());
115        self
116    }
117
118    /// Machine name shown in the dashboard. Defaults to the hostname.
119    pub fn machine_name(mut self, machine_name: impl Into<String>) -> Self {
120        self.machine_name = Some(machine_name.into());
121        self
122    }
123
124    /// Platform shown in the dashboard. Defaults to `macOS`, `Windows` or
125    /// `Linux`.
126    pub fn platform(mut self, platform: impl Into<String>) -> Self {
127        self.platform = Some(platform.into());
128        self
129    }
130
131    /// How long the app must keep working offline. The license is
132    /// re-validated at 2/3 of the token lifetime, or earlier so that at least
133    /// this much validity remains, but never sooner than 1 day after the last
134    /// validation. Default: zero.
135    pub fn min_offline_duration(mut self, duration: Duration) -> Self {
136        self.min_offline_duration = duration;
137        self
138    }
139
140    /// Override the API origin. Intended for tests.
141    pub fn api_base_url(mut self, url: impl Into<String>) -> Self {
142        self.api_base_url = Some(url.into());
143        self
144    }
145
146    /// Override persistence locations. Not part of the semver API.
147    #[cfg(feature = "test-util")]
148    #[doc(hidden)]
149    pub fn storage_dirs(mut self, dirs: Vec<PathBuf>) -> Self {
150        self.storage_dirs = Some(dirs);
151        self
152    }
153
154    /// Replace the SDK's I/O. Not part of the semver API.
155    #[cfg(feature = "test-util")]
156    #[doc(hidden)]
157    pub fn testing(mut self, hooks: testing::TestHooks) -> Self {
158        self.testing = Some(hooks);
159        self
160    }
161
162    #[cfg(test)]
163    fn test_hooks(mut self, dirs: Vec<PathBuf>, hooks: TestHooks) -> Self {
164        self.storage_dirs = Some(dirs);
165        self.testing = Some(hooks);
166        self
167    }
168
169    fn into_options(
170        self,
171        default_version: String,
172        runtime: tokio::runtime::Handle,
173    ) -> LicenseManagerOptions {
174        LicenseManagerOptions {
175            product_id: self.product_id,
176            public_key: self.public_key,
177            fingerprint: self.fingerprint,
178            version: self.version.unwrap_or(default_version),
179            machine_name: self.machine_name,
180            platform: self.platform,
181            min_offline_duration: self.min_offline_duration,
182            api_base_url: self.api_base_url,
183            storage_dirs: self.storage_dirs,
184            runtime,
185            testing: self.testing,
186        }
187    }
188
189    /// Build the plugin. A configuration error fails the app setup.
190    pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
191        tauri::plugin::Builder::new("licensemanager")
192            .invoke_handler(tauri::generate_handler![
193                commands::get_state,
194                commands::ready,
195                commands::get_license,
196                commands::set_license_key,
197                commands::refresh,
198                commands::get_offline_state,
199                commands::get_latest_allowed_version,
200                commands::get_portal_url,
201                commands::is_clock_tampered,
202                commands::get_license_key_hint,
203                commands::get_license_key,
204                commands::get_fingerprint,
205            ])
206            .setup(move |app, _api| {
207                setup(app, self)?;
208                Ok(())
209            })
210            .on_event(|app, event| {
211                if let RunEvent::Exit = event {
212                    if let Some(manager) = app.try_state::<LicenseManager>() {
213                        manager.dispose();
214                    }
215                }
216            })
217            .build()
218    }
219}
220
221/// Keeps the event forwarding alive for the app's lifetime.
222struct EventForwarding {
223    _subscription: Subscription,
224}
225
226#[cfg(mobile)]
227fn setup<R: Runtime>(_app: &tauri::AppHandle<R>, _builder: Builder) -> Result<(), String> {
228    Err(MOBILE_ERROR.into())
229}
230
231#[cfg(not(mobile))]
232fn setup<R: Runtime>(app: &tauri::AppHandle<R>, builder: Builder) -> Result<(), String> {
233    use tauri::Emitter;
234
235    let _ = MOBILE_ERROR;
236    let options = builder.into_options(
237        app.package_info().version.to_string(),
238        tauri::async_runtime::handle().inner().clone(),
239    );
240    let manager = LicenseManager::new(options).map_err(|e| e.to_string())?;
241    let handle = app.clone();
242    let subscription = manager.on_state_change(move |state| {
243        if let Some(manager) = handle.try_state::<LicenseManager>() {
244            let snapshot = Snapshot {
245                state,
246                ..Snapshot::of(&manager)
247            };
248            let _ = handle.emit(STATE_CHANGED_EVENT, snapshot);
249        }
250    });
251    app.manage(manager);
252    app.manage(EventForwarding {
253        _subscription: subscription,
254    });
255    Ok(())
256}
257
258/// Access to the [`LicenseManager`] from an `App`, `AppHandle`, `Window`…
259pub trait LicensemanagerExt<R: Runtime> {
260    /// The app's license manager. Panics if the plugin isn't registered.
261    fn license(&self) -> &LicenseManager;
262}
263
264impl<R: Runtime, T: Manager<R>> LicensemanagerExt<R> for T {
265    fn license(&self) -> &LicenseManager {
266        self.state::<LicenseManager>().inner()
267    }
268}
269
270/// Test hooks and fakes. Not part of the semver API.
271#[cfg(feature = "test-util")]
272#[doc(hidden)]
273#[allow(missing_docs)]
274pub mod testing {
275    use std::sync::Arc;
276
277    pub use crate::core::api::{post_validate_key, ValidateKeyRequest};
278    pub use crate::core::clock::Clock;
279    pub use crate::core::options::TestHooks;
280    pub use crate::core::store::{SecretBackend, SecretBackendError, SecureStore};
281    pub use crate::core::test_support::*;
282    pub use crate::core::transport::{HttpResponse, Transport, TransportError};
283    use crate::{Builder, ConfigError, LicenseManager};
284
285    /// A [`LicenseManager`] without Tauri, on the current Tokio runtime.
286    pub fn manager(builder: Builder, default_version: &str) -> Result<LicenseManager, ConfigError> {
287        LicenseManager::new(
288            builder.into_options(default_version.into(), tokio::runtime::Handle::current()),
289        )
290    }
291
292    /// The production HTTP transport.
293    pub fn reqwest_transport() -> Arc<dyn Transport> {
294        Arc::new(crate::core::transport::ReqwestTransport::new())
295    }
296
297    /// The OS keyring backend, if available.
298    pub fn os_keyring() -> Option<Arc<dyn SecretBackend>> {
299        crate::core::store::default_backend()
300    }
301}
302
303#[cfg(test)]
304mod tests;