Introduces the offline license infrastructure for the Base/Premium editions. - jsonwebtoken (EdDSA) verifies license JWTs against an embedded Ed25519 public key. The exp claim is mandatory (CWE-613) and is enforced via Validation::set_required_spec_claims. - Activation tokens (server-issued, machine-bound) prevent license.key copying between machines. Storage is wired up; the actual issuance flow ships with Issue #49. - get_edition() fails closed to "free" when the license is missing, invalid, expired, or activated for a different machine. - New commands/entitlements module centralizes feature → tier mapping so Issue #48 (and any future gate) reads from a single source of truth. - machine-uid provides the cross-platform machine identifier; OS reinstall invalidates the activation token by design. - Tests cover happy path, expiry, wrong-key signature, malformed JWT, unknown edition, and machine_id matching for activation tokens. The embedded PUBLIC_KEY_PEM is the RFC 8410 §10.3 test vector, clearly labelled as a development placeholder; replacing it with the production public key is a release-time task.
67 lines
2.2 KiB
Rust
67 lines
2.2 KiB
Rust
// Centralized feature → tier mapping for license entitlements.
|
|
//
|
|
// This module is the single source of truth for which features are gated by which tier.
|
|
// To change what is gated where, modify FEATURE_TIERS only — never sprinkle edition checks
|
|
// throughout the codebase.
|
|
|
|
/// Editions, ordered from least to most privileged.
|
|
pub const EDITION_FREE: &str = "free";
|
|
pub const EDITION_BASE: &str = "base";
|
|
pub const EDITION_PREMIUM: &str = "premium";
|
|
|
|
/// Maps feature name → list of editions allowed to use it.
|
|
/// A feature absent from this list is denied for all editions.
|
|
const FEATURE_TIERS: &[(&str, &[&str])] = &[
|
|
("auto-update", &[EDITION_BASE, EDITION_PREMIUM]),
|
|
("web-sync", &[EDITION_PREMIUM]),
|
|
("cloud-backup", &[EDITION_PREMIUM]),
|
|
("advanced-reports", &[EDITION_PREMIUM]),
|
|
];
|
|
|
|
/// Pure check: does `edition` grant access to `feature`?
|
|
pub fn is_feature_allowed(feature: &str, edition: &str) -> bool {
|
|
FEATURE_TIERS
|
|
.iter()
|
|
.find(|(name, _)| *name == feature)
|
|
.map(|(_, tiers)| tiers.contains(&edition))
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn check_entitlement(app: tauri::AppHandle, feature: String) -> Result<bool, String> {
|
|
let edition = crate::commands::license_commands::current_edition(&app);
|
|
Ok(is_feature_allowed(&feature, &edition))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn free_blocks_auto_update() {
|
|
assert!(!is_feature_allowed("auto-update", EDITION_FREE));
|
|
}
|
|
|
|
#[test]
|
|
fn base_unlocks_auto_update() {
|
|
assert!(is_feature_allowed("auto-update", EDITION_BASE));
|
|
}
|
|
|
|
#[test]
|
|
fn premium_unlocks_everything() {
|
|
assert!(is_feature_allowed("auto-update", EDITION_PREMIUM));
|
|
assert!(is_feature_allowed("web-sync", EDITION_PREMIUM));
|
|
assert!(is_feature_allowed("cloud-backup", EDITION_PREMIUM));
|
|
}
|
|
|
|
#[test]
|
|
fn base_does_not_unlock_premium_features() {
|
|
assert!(!is_feature_allowed("web-sync", EDITION_BASE));
|
|
assert!(!is_feature_allowed("cloud-backup", EDITION_BASE));
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_feature_denied() {
|
|
assert!(!is_feature_allowed("nonexistent", EDITION_PREMIUM));
|
|
}
|
|
}
|