use anyhow::{Context, Result};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use colored::Colorize;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LicenseTier {
Professional,
Team,
Enterprise,
}
impl std::fmt::Display for LicenseTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LicenseTier::Professional => write!(f, "Professional"),
LicenseTier::Team => write!(f, "Team"),
LicenseTier::Enterprise => write!(f, "Enterprise"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[allow(clippy::upper_case_acronyms)]
pub enum PremiumFeature {
HybridSearch,
AdvancedFiltering,
EncryptionAtRest,
Snapshots,
MultiTenancy,
RBAC,
SSO,
GpuAcceleration,
AuditLogging,
}
impl std::fmt::Display for PremiumFeature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PremiumFeature::HybridSearch => write!(f, "Hybrid Search"),
PremiumFeature::AdvancedFiltering => write!(f, "Advanced Filtering"),
PremiumFeature::EncryptionAtRest => write!(f, "Encryption at Rest"),
PremiumFeature::Snapshots => write!(f, "Snapshots & Backups"),
PremiumFeature::MultiTenancy => write!(f, "Multi-Tenancy"),
PremiumFeature::RBAC => write!(f, "RBAC"),
PremiumFeature::SSO => write!(f, "SSO"),
PremiumFeature::GpuAcceleration => write!(f, "GPU Acceleration"),
PremiumFeature::AuditLogging => write!(f, "Audit Logging"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LicenseInfo {
pub key: String,
pub tier: LicenseTier,
pub organization: String,
pub expires_at: u64,
pub max_instances: i32,
pub features: Vec<PremiumFeature>,
}
impl LicenseInfo {
pub fn is_expired(&self) -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
now > self.expires_at
}
#[allow(dead_code)] pub fn has_feature(&self, feature: PremiumFeature) -> bool {
self.features.contains(&feature)
}
pub fn expires_at_formatted(&self) -> String {
use chrono::{DateTime, Utc};
#[allow(clippy::cast_possible_wrap)]
let timestamp = self.expires_at as i64;
DateTime::<Utc>::from_timestamp(timestamp, 0).map_or_else(
|| "Unknown".to_string(),
|dt| dt.format("%Y-%m-%d").to_string(),
)
}
}
#[derive(Debug, Clone)]
pub struct SignedLicense {
pub payload: Vec<u8>,
pub signature: Signature,
}
impl SignedLicense {
pub fn parse(license_string: &str) -> Result<Self> {
let parts: Vec<&str> = license_string.split('.').collect();
if parts.len() != 2 {
anyhow::bail!("Invalid license format. Expected: <payload>.<signature>");
}
let payload = BASE64
.decode(parts[0])
.context("Failed to decode license payload (invalid base64)")?;
let sig_bytes = BASE64
.decode(parts[1])
.context("Failed to decode license signature (invalid base64)")?;
let sig_array: [u8; 64] = sig_bytes
.try_into()
.map_err(|_| anyhow::anyhow!("Invalid signature length (expected 64 bytes)"))?;
let signature = Signature::from_bytes(&sig_array);
Ok(Self { payload, signature })
}
pub fn verify_with_key(&self, public_key_b64: &str) -> Result<()> {
let key_bytes = BASE64
.decode(public_key_b64)
.context("Failed to decode public key (invalid base64)")?;
let raw_key = if key_bytes.len() == 32 {
key_bytes
} else if key_bytes.len() == 44 && key_bytes.starts_with(&[0x30, 0x2a]) {
key_bytes[12..].to_vec()
} else {
anyhow::bail!("Invalid public key length (expected 32 or 44 bytes)");
};
let key_array: [u8; 32] = raw_key
.try_into()
.map_err(|_| anyhow::anyhow!("Invalid public key format"))?;
let verifying_key =
VerifyingKey::from_bytes(&key_array).context("Failed to create verifying key")?;
verifying_key
.verify(&self.payload, &self.signature)
.map_err(|_| anyhow::anyhow!("Invalid signature - license may have been tampered with"))
}
pub fn extract_info(&self) -> Result<LicenseInfo> {
serde_json::from_slice(&self.payload)
.context("Failed to parse license payload (invalid JSON)")
}
}
pub fn validate_license(license_string: &str, public_key_b64: &str) -> Result<LicenseInfo> {
let signed = SignedLicense::parse(license_string)?;
signed.verify_with_key(public_key_b64)?;
let info = signed.extract_info()?;
if info.is_expired() {
anyhow::bail!("License has expired on {}", info.expires_at_formatted());
}
Ok(info)
}
pub fn get_license_config_path() -> Result<PathBuf> {
let home = dirs::home_dir().context("Could not determine home directory")?;
let config_dir = home.join(".velesdb");
fs::create_dir_all(&config_dir).context("Failed to create .velesdb config directory")?;
Ok(config_dir.join("license"))
}
pub fn save_license_key(license_key: &str) -> Result<()> {
let path = get_license_config_path()?;
fs::write(&path, license_key)
.with_context(|| format!("Failed to write license to {}", path.display()))?;
Ok(())
}
pub fn load_license_key() -> Result<String> {
let path = get_license_config_path()?;
fs::read_to_string(&path)
.with_context(|| format!("Failed to read license from {}", path.display()))
}
pub fn display_license_info(info: &LicenseInfo) {
println!("\n{}", "License Information".green().bold());
println!("{}", "=".repeat(60).green());
println!(" {} {}", "Key:".cyan(), info.key);
println!(" {} {}", "Organization:".cyan(), info.organization.bold());
println!(" {} {}", "Tier:".cyan(), info.tier.to_string().yellow());
println!(
" {} {}",
"Max Instances:".cyan(),
if info.max_instances == -1 {
"Unlimited".to_string()
} else {
info.max_instances.to_string()
}
);
println!(" {} {}", "Expires:".cyan(), info.expires_at_formatted());
if info.is_expired() {
println!(" {} {}", "Status:".cyan(), "EXPIRED".red().bold());
} else {
println!(" {} {}", "Status:".cyan(), "VALID".green().bold());
}
println!("\n{}", "Enabled Features:".cyan().bold());
for feature in &info.features {
println!(" {} {}", "✓".green(), feature);
}
println!();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_license_tier_display() {
assert_eq!(LicenseTier::Professional.to_string(), "Professional");
assert_eq!(LicenseTier::Team.to_string(), "Team");
assert_eq!(LicenseTier::Enterprise.to_string(), "Enterprise");
}
#[test]
fn test_license_info_has_feature() {
let info = LicenseInfo {
key: "TEST-KEY".to_string(),
tier: LicenseTier::Professional,
organization: "Test Corp".to_string(),
expires_at: u64::MAX,
max_instances: 1,
features: vec![PremiumFeature::HybridSearch, PremiumFeature::Snapshots],
};
assert!(info.has_feature(PremiumFeature::HybridSearch));
assert!(info.has_feature(PremiumFeature::Snapshots));
assert!(!info.has_feature(PremiumFeature::MultiTenancy));
}
#[test]
fn test_license_info_is_expired() {
let expired = LicenseInfo {
key: "TEST-KEY".to_string(),
tier: LicenseTier::Professional,
organization: "Test Corp".to_string(),
expires_at: 1_000_000, max_instances: 1,
features: vec![],
};
assert!(expired.is_expired());
let valid = LicenseInfo {
key: "TEST-KEY".to_string(),
tier: LicenseTier::Professional,
organization: "Test Corp".to_string(),
expires_at: u64::MAX,
max_instances: 1,
features: vec![],
};
assert!(!valid.is_expired());
}
#[test]
fn test_signed_license_parse_invalid_format() {
let result = SignedLicense::parse("invalid-format");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid license format"));
}
#[test]
fn test_signed_license_parse_invalid_base64() {
let result = SignedLicense::parse("not-base64.also-not-base64");
assert!(result.is_err());
}
}