@licenseseat/tauri-plugin
v0.6.0
Published
Tauri plugin for LicenseSeat software licensing
Maintainers
Readme
LicenseSeat for Tauri 2
Native Tauri 2 plugin and typed JavaScript bindings for the LicenseSeat Rust SDK.
The Rust plugin owns durable state, network/crypto work, restoration, and background tasks. The JavaScript package exposes commands, normalized errors, stable state snapshots, and serialized subscriptions for the renderer.
Requirements
- Rust 1.88+
- Tauri 2
- Node 18+ for building the JavaScript package
@tauri-apps/api >=2 <3- a publishable LicenseSeat
pk_*key
An sk_* secret key is rejected during plugin setup. Secret keys must remain on a server.
Install and register
cargo add tauri-plugin-licenseseat
npm add @licenseseat/tauri-pluginfn main() {
tauri::Builder::default()
.plugin(tauri_plugin_licenseseat::init())
.run(tauri::generate_context!())
.expect("failed to run Tauri application");
}Production configuration
{
"plugins": {
"licenseseat": {
"apiKey": "pk_live_your_publishable_key",
"productSlug": "your-product",
"signingPublicKey": "BASE64_ED25519_PUBLIC_KEY",
"signingKeyId": "production-key-v1",
"offlineFallbackMode": "networkOnly",
"telemetryEnabled": true,
"sendFingerprintComponents": false,
"emitFrontendEvents": true
}
}
}The default storage directory is app_data_dir()/licenseseat, which gives each Tauri application an app-scoped location. The core SDK applies an additional product-scoped prefix.
Release builds reject "$VARIABLE" runtime placeholders for trust configuration. Client keys and signing keys are public material and should be compiled into the signed application/config generated by the release pipeline. Runtime environment expansion remains available in debug builds for local development.
Configuration reference
All durations are seconds.
| JSON field | Default | Notes |
| --- | --- | --- |
| apiKey | required | Publishable key; sk_* rejected |
| productSlug | required | Product identity |
| apiBaseUrl | LicenseSeat production API | Remote URLs require HTTPS |
| storagePrefix | product-scoped default | Filename-normalized |
| storagePath | app data licenseseat/ | Preflighted before plugin startup completes |
| deviceIdentifier | durable random UUID | Explicit installation override |
| sendFingerprintComponents | false | Opt-in raw hostname/hardware component collection |
| signingPublicKey / signingKeyId | none | Pin both for offline startup |
| autoValidateInterval | 3600 | Zero disables |
| heartbeatInterval | 300 | Zero disables |
| networkRecheckInterval | 30 | Zero disables |
| timeoutSeconds | 30 | Must be greater than zero |
| maxRetries | 3 | Retryable availability failures only |
| retryDelaySeconds | 1 | Exponential delay is capped |
| verifySsl | true | May be false only for loopback development |
| offlineFallbackMode | networkOnly | Canonical values: networkOnly, always; documented legacy aliases remain accepted |
| maxOfflineDays | 0 | No extra host-age cap; signed expiry still applies |
| maxClockSkewSeconds | 300 | Signed artifact time checks |
| offlineTokenRefreshInterval | 259200 | Zero disables all periodic artifact refresh |
| enableLegacyOfflineTokens | false | Machine files remain preferred |
| telemetryEnabled | true | OS/SDK/app/coarse-capacity/locale fields |
| debug | false | Sensitive fields stay redacted |
| emitFrontendEvents | true | Set false for a native-facade integration; native Rust subscribers are unaffected |
| appVersion | Tauri package version | Telemetry |
| appBuild | none | Telemetry |
Unknown fallback spellings fail setup rather than silently changing policy. networkOnly permits signed fallback only after transport/timeout/408/5xx availability failure. always additionally permits it after 429. Neither overrides authentication, configuration, ordinary client/business, malformed-response, identity-binding, or local race errors.
Capabilities and least privilege
No renderer command is usable until its Tauri capability grants permission.
Default application surface
{
"identifier": "main",
"windows": ["main"],
"permissions": ["licenseseat:default"]
}licenseseat:default grants ordinary activation, validation, deactivation, heartbeat, restoration, health, status/state, fingerprint, license, and entitlement commands. It does not grant raw offline artifacts, explicit arbitrary key/fingerprint operations, destructive reset, detailed diagnostics, or release/download commands.
Optional permission sets
| Set | Commands and exposure |
| --- | --- |
| licenseseat:diagnostics | health, detailed admin snapshot; may reveal cached license/artifact data and local paths, but not raw API/signing keys |
| licenseseat:advanced-lifecycle | explicit validateKey, deactivateKey, heartbeatKey, and destructive reset |
| licenseseat:offline-management | raw machine-file/token checkout, verification, signing-key lookup, and refresh |
| licenseseat:releases | release listing/latest lookup and license-bound download-token generation |
Grant sets only to the windows/webviews that require them. Do not combine LicenseSeat capabilities with untrusted remote web content.
The generic frontend surface necessarily sees a user-entered license key and can retrieve license state. For a higher-assurance product, register the plugin/core SDK natively with emitFrontendEvents: false, grant no generic LicenseSeat permissions to the renderer, and expose a small app-specific Rust command facade that returns redacted state and performs entitlement checks again around native value-producing operations. Disabling the bridge prevents raw generic lifecycle payloads from being broadcast to renderers; native LicenseSeat::subscribe() consumers still receive the full event stream.
Typed compile-time configuration is also available when release trust material is injected by the build pipeline:
let config = tauri_plugin_licenseseat::PluginConfig {
api_key: env!("LICENSESEAT_API_KEY").into(),
product_slug: "your-product".into(),
signing_public_key: Some(env!("LICENSESEAT_SIGNING_PUBLIC_KEY").into()),
signing_key_id: Some(env!("LICENSESEAT_SIGNING_KEY_ID").into()),
emit_frontend_events: Some(false),
..Default::default()
};
tauri::Builder::default()
.plugin(tauri_plugin_licenseseat::init_with_config(config));init_with_config replaces the optional JSON plugin configuration rather than merging two sources of truth. These values are public client configuration embedded in the application, not secrets.
Startup and state
Plugin setup:
- validates configuration and durable storage;
- constructs the core SDK with
LicenseSeat::try_new; - starts a resilient native-to-Tauri event bridge;
- manages the SDK in Tauri state;
- asynchronously calls the idempotent
restore_license.
Persisted unsigned online state is pending after restart and grants no entitlements until current online or pinned-key signed-offline verification establishes trust.
import {
bootstrapState,
stateHasEntitlement,
subscribeState,
type LicenseSeatState,
} from '@licenseseat/tauri-plugin';
let state: LicenseSeatState = await bootstrapState();
const unsubscribe = await subscribeState(
({ eventName, state: nextState }) => {
state = nextState;
updateUi({
status: state.clientStatus,
paid: stateHasEntitlement(state, 'pro-features'),
source: eventName,
});
},
{
emitCurrent: true,
onError: (error) => reportError(error),
},
);bootstrapState() calls the idempotent native restore and returns the latest observable state. It defaults validateIfActivated to false, avoiding duplicate validation with plugin setup. Pass true only when an explicit extra online validation is desired.
subscribeState serializes refresh and handler delivery so slow async handlers cannot overlap or observe snapshots out of order. The native getState() response is itself derived from one coherent core SDK state observation, and heartbeat grant changes are included in the default refresh events. Handler/get-state failures are normalized and sent to onError; one failure does not kill future deliveries. Unsubscribe stops new delivery, removes every native listener, and drains the already queued work.
Because the underlying event bus may interleave network and background events, always treat getState() as the snapshot source of truth.
Activation and entitlement gating
import {
activateAndGetState,
checkEntitlement,
hasEntitlement,
} from '@licenseseat/tauri-plugin';
const state = await activateAndGetState(customerLicenseKey, {
deviceName: 'Jane’s Mac',
});
if (await hasEntitlement('cloud-sync')) {
enableCloudSyncUi();
}
const detail = await checkEntitlement('cloud-sync');
// reason: 'nolicense' | 'invalidlicense' | 'notfound' | 'expired' | undefinedActivation immediately establishes a trusted online grant. activateAndGetState performs no additional validation request — the activation response already contains the validated grant — and simply returns one getState() snapshot read after the activation succeeds.
Use hasEntitlement, checkEntitlement, getEntitlements, or the entitlement fields in getState. Do not gate from getLicense or raw validation.license.activeEntitlements; diagnostic data can exist while the process is pending/untrusted.
Paid native operations should repeat the entitlement check inside Rust. Hiding a button in JavaScript is not a security boundary.
API groups
State and lifecycle
activate,activateAndGetStatevalidate,deactivate,heartbeatrestoreLicense,restoreAndGetState,bootstrapStategetState,getStatus,getClientStatus,getLicenseisOnline,health,getFingerprintcheckEntitlement,hasEntitlement,getEntitlements- state-only helpers
stateHasEntitlement,stateHasAnyEntitlement,stateHasAllEntitlements
Advanced lifecycle
validateKeydeactivateKeyheartbeatKeyreset
Fingerprint aliases (fingerprint, deviceId, deviceFingerprint) must agree if more than one is provided. Ambiguous input fails locally.
Offline management
checkoutMachineFileverifyMachineFilesyncOfflineAssetsfetchSigningKey- legacy
generateOfflineToken/verifyOfflineToken
Machine files are fetched and verified before cache commit. They are bound to license, product, activation, and installation and constrained by signed/host time policies. A fetched signing key works for the current online process but is not a cross-process trust anchor; pin the key pair in production.
Raw hardware components are omitted by default. fingerprintComponents opts in for one checkout; sendFingerprintComponents opts in for automatic checkouts.
Releases
getLatestReleaselistReleasesgenerateDownloadToken
These validate returned product/channel/platform metadata and expiry. They do not package, sign, download, hash-verify, install, restart, roll back, or stage updates.
Events
LICENSESEAT_EVENTS contains the typed event-name constants, and listenEvent registers a raw listener. LICENSESEAT_STATE_EVENTS is the curated set that triggers subscribeState refreshes.
Event names use licenseseat:// and include lifecycle, offline artifact, offline validation, auto-validation, network, revocation, reset, and SDK error events. Event payloads are structured license/validation/error/message/timestamp data where available.
The bridge handles Tokio broadcast lag by warning and continuing rather than terminating permanently. Consumers should resynchronize through getState after any suspected gap.
Events are not gated by the plugin permission system. The bridge emits through Tauri's global event system, and listening requires only core:event:allow-listen (part of core:default) — no licenseseat:* permission. Any window holding core defaults can therefore observe every bridged payload, including activation-success/license-loaded payloads that carry the license key. If any window renders remote or third-party content, set emitFrontendEvents: false and have trusted windows poll getState, or withhold core:event:allow-listen from that window's capability. The native-facade pattern above avoids the exposure entirely.
Error handling
Every command rejects with a normalized LicenseSeatPluginError:
import {
activate,
normalizeError,
type LicenseSeatPluginError,
} from '@licenseseat/tauri-plugin';
try {
await activate(customerLicenseKey);
} catch (cause) {
const error: LicenseSeatPluginError = normalizeError(cause);
console.error(error.code, error.message, error.status, error.cause);
}Normalization handles Error objects, strings, structured objects, nested Tauri payloads, and nested JSON strings with a bounded recursion depth. API messages and transport errors are already sanitized/bounded by the Rust layer, and request URLs containing license keys are removed.
Admin diagnostics
getAdminSnapshot is diagnostic and non-authoritative. It includes redacted configuration, runtime scheduling/connectivity information, cached state/artifacts, signing-key metadata, trusted-license source, and cache paths. It never returns the configured API key or signing private material; nevertheless, restrict licenseseat:diagnostics because the snapshot can contain customer license/artifact data and filesystem paths.
Build and verify
# Rust side, from the workspace root
cargo test -p tauri-plugin-licenseseat --all-features
cargo clippy --workspace --all-targets --all-features -- -D warnings
# JavaScript side
cd crates/tauri-plugin-licenseseat
npm ci
npm test
npm run pack:checknpm test compiles TypeScript and runs tests for error normalization, bounded nested payload handling, serialized state delivery, handler-error recovery, and unsubscribe behavior. pack:check verifies the publishable npm contents without publishing.
See the workspace production hardening audit for the complete threat model and release evidence.
