/* Volt Platform — License Enforcement Runtime enforcement of tier-based feature gating. Commands call RequireFeature() at the top of their RunE functions to gate access. If the current license tier doesn't include the requested feature, the user sees a clear upgrade message. No license on disk = Community tier (free). Trial licenses are checked for expiration. */ package license import "fmt" // RequireFeature checks if the current license tier includes the named feature. // If no license file exists, defaults to Community tier. // Returns nil if allowed, error with upgrade message if not. func RequireFeature(feature string) error { store := NewStore() lic, err := store.Load() if err != nil { // No license = Community tier — check Community features if TierIncludes(TierCommunity, feature) { return nil } return fmt.Errorf("feature %q requires a Pro or Enterprise license\n Register at: https://armoredgate.com/pricing\n Or run: volt system register --license VOLT-PRO-XXXX-...", feature) } // Check trial expiration if lic.IsTrialExpired() { // Expired trial — fall back to Community tier if TierIncludes(TierCommunity, feature) { return nil } return fmt.Errorf("trial license expired on %s — feature %q requires an active Pro or Enterprise license\n Upgrade at: https://armoredgate.com/pricing\n Or run: volt system register --license VOLT-PRO-XXXX-...", lic.TrialEndsAt.Format("2006-01-02"), feature) } // Check license expiration (non-trial) if !lic.ExpiresAt.IsZero() { expired, _ := store.IsExpired() if expired { if TierIncludes(TierCommunity, feature) { return nil } return fmt.Errorf("license expired on %s — feature %q requires an active Pro or Enterprise license\n Renew at: https://armoredgate.com/pricing", lic.ExpiresAt.Format("2006-01-02"), feature) } } if TierIncludes(lic.Tier, feature) { return nil } return fmt.Errorf("feature %q requires %s tier (current: %s)\n Upgrade at: https://armoredgate.com/pricing", feature, requiredTier(feature), TierName(lic.Tier)) } // RequireFeatureWithStore checks feature access using a caller-provided Store. // Useful for testing with a custom license directory. func RequireFeatureWithStore(store *Store, feature string) error { lic, err := store.Load() if err != nil { if TierIncludes(TierCommunity, feature) { return nil } return fmt.Errorf("feature %q requires a Pro or Enterprise license\n Register at: https://armoredgate.com/pricing\n Or run: volt system register --license VOLT-PRO-XXXX-...", feature) } if lic.IsTrialExpired() { if TierIncludes(TierCommunity, feature) { return nil } return fmt.Errorf("trial license expired on %s — feature %q requires an active Pro or Enterprise license\n Upgrade at: https://armoredgate.com/pricing\n Or run: volt system register --license VOLT-PRO-XXXX-...", lic.TrialEndsAt.Format("2006-01-02"), feature) } if !lic.ExpiresAt.IsZero() { expired, _ := store.IsExpired() if expired { if TierIncludes(TierCommunity, feature) { return nil } return fmt.Errorf("license expired on %s — feature %q requires an active Pro or Enterprise license\n Renew at: https://armoredgate.com/pricing", lic.ExpiresAt.Format("2006-01-02"), feature) } } if TierIncludes(lic.Tier, feature) { return nil } return fmt.Errorf("feature %q requires %s tier (current: %s)\n Upgrade at: https://armoredgate.com/pricing", feature, requiredTier(feature), TierName(lic.Tier)) } // RequireContainerLimit checks if adding one more container would exceed // the tier's per-node container limit. func RequireContainerLimit(currentCount int) error { store := NewStore() tier := TierCommunity lic, err := store.Load() if err == nil { if lic.IsTrialExpired() { tier = TierCommunity } else { tier = lic.Tier } } limit := MaxContainersPerNode(tier) if limit == 0 { // 0 = unlimited (Enterprise) return nil } if currentCount >= limit { return fmt.Errorf("container limit reached: %d/%d (%s tier)\n Upgrade at: https://armoredgate.com/pricing", currentCount, limit, TierName(tier)) } return nil } // RequireContainerLimitWithStore checks container limits using a caller-provided Store. func RequireContainerLimitWithStore(store *Store, currentCount int) error { tier := TierCommunity lic, err := store.Load() if err == nil { if lic.IsTrialExpired() { tier = TierCommunity } else { tier = lic.Tier } } limit := MaxContainersPerNode(tier) if limit == 0 { return nil } if currentCount >= limit { return fmt.Errorf("container limit reached: %d/%d (%s tier)\n Upgrade at: https://armoredgate.com/pricing", currentCount, limit, TierName(tier)) } return nil } // requiredTier returns the human-readable name of the minimum tier that // includes the given feature. Checks from lowest to highest. func requiredTier(feature string) string { if TierIncludes(TierCommunity, feature) { return TierName(TierCommunity) } if TierIncludes(TierPro, feature) { return TierName(TierPro) } if TierIncludes(TierEnterprise, feature) { return TierName(TierEnterprise) } return "Unknown" }