mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-25 08:52:06 +00:00
Dead-code audit batch 1: five functions with zero references from main, tests, or pulse-enterprise (deadcode -test, grep-verified per symbol, enterprise compile-checked). Also renames the tlsutil test that exercised the deleted compat alias's target to name the Unverified function it actually calls. Skipped from the audit list after re-verification: IsNilAlertPayload (live pulse-enterprise consumer in internal/aialertanalysis).
532 lines
18 KiB
Go
532 lines
18 KiB
Go
package licensing
|
|
|
|
import (
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// EntitlementPayload is the normalized entitlement response for frontend consumption.
|
|
// Frontend should use this instead of inferring capabilities from tier names.
|
|
type EntitlementPayload struct {
|
|
// Capabilities lists all granted capability keys.
|
|
Capabilities []string `json:"capabilities"`
|
|
|
|
// Limits lists quantitative limits with current usage.
|
|
Limits []LimitStatus `json:"limits"`
|
|
|
|
// SubscriptionState is the current subscription lifecycle state.
|
|
SubscriptionState string `json:"subscription_state"`
|
|
|
|
// UpgradeReasons provides user-actionable upgrade prompts.
|
|
UpgradeReasons []UpgradeReason `json:"upgrade_reasons"`
|
|
|
|
// PlanVersion preserves grandfathered terms.
|
|
PlanVersion string `json:"plan_version,omitempty"`
|
|
|
|
// Tier is the marketing tier name (for display only, never gate on this).
|
|
Tier string `json:"tier"`
|
|
|
|
// TrialExpiresAt is the trial expiration Unix timestamp when in trial state.
|
|
TrialExpiresAt *int64 `json:"trial_expires_at,omitempty"`
|
|
|
|
// TrialDaysRemaining is the number of whole or partial days remaining in trial.
|
|
TrialDaysRemaining *int `json:"trial_days_remaining,omitempty"`
|
|
|
|
// HostedMode indicates that this server is running in Pulse hosted mode.
|
|
// It is used by the frontend to gate hosted-control-plane-only UI.
|
|
HostedMode bool `json:"hosted_mode"`
|
|
|
|
// Valid mirrors the effective license validity for display surfaces.
|
|
Valid bool `json:"valid"`
|
|
|
|
// LicensedEmail is the activated license email when available.
|
|
LicensedEmail string `json:"licensed_email,omitempty"`
|
|
|
|
// ExpiresAt is the RFC3339 expiration timestamp when available.
|
|
ExpiresAt *string `json:"expires_at,omitempty"`
|
|
|
|
// IsLifetime indicates a lifetime entitlement with no expiration.
|
|
IsLifetime bool `json:"is_lifetime"`
|
|
|
|
// DaysRemaining is the number of days left until expiration.
|
|
DaysRemaining int `json:"days_remaining"`
|
|
|
|
// InGracePeriod indicates whether the entitlement is currently in grace.
|
|
InGracePeriod bool `json:"in_grace_period,omitempty"`
|
|
|
|
// GracePeriodEnd is the RFC3339 grace period end timestamp when available.
|
|
GracePeriodEnd *string `json:"grace_period_end,omitempty"`
|
|
|
|
// TrialEligible is retained for compatibility with retired self-hosted trial clients.
|
|
// New self-hosted payloads leave it false; active trial state is exposed through
|
|
// SubscriptionState, TrialExpiresAt, and TrialDaysRemaining.
|
|
TrialEligible bool `json:"trial_eligible"`
|
|
|
|
// TrialEligibilityReason is retained for compatibility with retired self-hosted trial clients.
|
|
TrialEligibilityReason string `json:"trial_eligibility_reason,omitempty"`
|
|
|
|
// MaxHistoryDays is the maximum metrics history retention in days for the current tier.
|
|
MaxHistoryDays int `json:"max_history_days"`
|
|
|
|
// OverflowDaysRemaining is set when the onboarding overflow (+1 host) is active.
|
|
// Indicates the number of days remaining in the 14-day overflow window.
|
|
OverflowDaysRemaining *int `json:"overflow_days_remaining,omitempty"`
|
|
|
|
// LegacyConnections is retained for response compatibility and informational
|
|
// estate review only.
|
|
LegacyConnections LegacyConnectionCounts `json:"legacy_connections"`
|
|
|
|
// HasMigrationGap is retained for response compatibility. It no longer
|
|
// describes a commercial volume limit.
|
|
HasMigrationGap bool `json:"has_migration_gap"`
|
|
|
|
// CommercialMigration reports unresolved paid-license migration work entering
|
|
// from v5-era commercial state.
|
|
CommercialMigration *CommercialMigrationStatus `json:"commercial_migration,omitempty"`
|
|
|
|
// Runtime identifies the binary/channel serving this entitlement payload.
|
|
Runtime *RuntimeIdentity `json:"runtime,omitempty"`
|
|
}
|
|
|
|
// CommercialPosturePayload is the canonical non-billing commercial contract
|
|
// for upgrade messaging and paid migration copy.
|
|
// It intentionally excludes billing identity, grandfathered plan terms, and
|
|
// other full-entitlement details that belong only to billing surfaces.
|
|
type CommercialPosturePayload struct {
|
|
// SubscriptionState is the current subscription lifecycle state.
|
|
SubscriptionState string `json:"subscription_state"`
|
|
|
|
// UpgradeReasons provides user-actionable upgrade prompts.
|
|
UpgradeReasons []UpgradeReason `json:"upgrade_reasons"`
|
|
|
|
// Tier is the marketing tier name (for display only, never gate on this).
|
|
Tier string `json:"tier"`
|
|
|
|
// TrialExpiresAt is the trial expiration Unix timestamp when in trial state.
|
|
TrialExpiresAt *int64 `json:"trial_expires_at,omitempty"`
|
|
|
|
// TrialDaysRemaining is the number of whole or partial days remaining in trial.
|
|
TrialDaysRemaining *int `json:"trial_days_remaining,omitempty"`
|
|
|
|
// TrialEligible is retained for compatibility with retired self-hosted trial clients.
|
|
TrialEligible bool `json:"trial_eligible"`
|
|
|
|
// TrialEligibilityReason is retained for compatibility with retired self-hosted trial clients.
|
|
TrialEligibilityReason string `json:"trial_eligibility_reason,omitempty"`
|
|
|
|
// OverflowDaysRemaining is set when the onboarding overflow (+1 host) is active.
|
|
OverflowDaysRemaining *int `json:"overflow_days_remaining,omitempty"`
|
|
|
|
// LegacyConnections is retained for response compatibility and informational
|
|
// estate review only.
|
|
LegacyConnections LegacyConnectionCounts `json:"legacy_connections"`
|
|
|
|
// HasMigrationGap is retained for response compatibility. It no longer
|
|
// describes a commercial volume limit.
|
|
HasMigrationGap bool `json:"has_migration_gap"`
|
|
|
|
// CommercialMigration reports unresolved paid-license migration work entering
|
|
// from v5-era commercial state.
|
|
CommercialMigration *CommercialMigrationStatus `json:"commercial_migration,omitempty"`
|
|
}
|
|
|
|
// RuntimeCapabilitiesPayload is the canonical non-commercial license contract
|
|
// for feature gating and runtime retention/limit decisions.
|
|
type RuntimeCapabilitiesPayload struct {
|
|
// Capabilities lists all granted capability keys.
|
|
Capabilities []string `json:"capabilities"`
|
|
|
|
// Limits lists quantitative limits with current usage.
|
|
Limits []LimitStatus `json:"limits"`
|
|
|
|
// HostedMode indicates that this server is running in Pulse hosted mode.
|
|
HostedMode bool `json:"hosted_mode"`
|
|
|
|
// MaxHistoryDays is the maximum metrics history retention in days for the current tier.
|
|
MaxHistoryDays int `json:"max_history_days"`
|
|
|
|
// Runtime identifies the binary/channel serving the runtime capability contract.
|
|
Runtime *RuntimeIdentity `json:"runtime,omitempty"`
|
|
|
|
// BlockedCapabilities lists licensed capabilities that this binary cannot serve.
|
|
BlockedCapabilities []RuntimeCapabilityBlock `json:"blocked_capabilities,omitempty"`
|
|
}
|
|
|
|
const (
|
|
RuntimeBuildCommunity = "community"
|
|
RuntimeBuildPro = "pro"
|
|
|
|
PulseProDownloadURL = "https://pulserelay.pro/download.html"
|
|
)
|
|
|
|
type RuntimeIdentity struct {
|
|
Build string `json:"build"`
|
|
Label string `json:"label"`
|
|
DownloadURL string `json:"download_url,omitempty"`
|
|
}
|
|
|
|
type RuntimeCapabilityBlock struct {
|
|
Key string `json:"key"`
|
|
Reason string `json:"reason"`
|
|
ActionURL string `json:"action_url,omitempty"`
|
|
}
|
|
|
|
// LimitStatus represents a quantitative limit with current usage state.
|
|
type LimitStatus struct {
|
|
// Key is the limit identifier (e.g., "max_guests").
|
|
Key string `json:"key"`
|
|
|
|
// Limit is the maximum allowed value (0 = unlimited).
|
|
Limit int64 `json:"limit"`
|
|
|
|
// Current is the observed current usage.
|
|
Current int64 `json:"current"`
|
|
|
|
// State describes the over-limit UX state.
|
|
// Values: "ok", "warning", "enforced"
|
|
State string `json:"state"`
|
|
}
|
|
|
|
// UpgradeReason provides context for why a user should upgrade.
|
|
type UpgradeReason struct {
|
|
// Key is the capability or limit this reason relates to.
|
|
Key string `json:"key"`
|
|
|
|
// Reason is a user-facing description of why upgrading helps.
|
|
Reason string `json:"reason"`
|
|
|
|
// ActionURL is where the user can go to upgrade.
|
|
ActionURL string `json:"action_url,omitempty"`
|
|
}
|
|
|
|
type LegacyConnectionCounts struct {
|
|
ProxmoxNodes int64 `json:"proxmox_nodes"`
|
|
DockerHosts int64 `json:"docker_hosts"`
|
|
KubernetesClusters int64 `json:"kubernetes_clusters"`
|
|
}
|
|
|
|
func cloneCapabilityKeys(values []string) []string {
|
|
if len(values) == 0 {
|
|
return []string{}
|
|
}
|
|
return append([]string(nil), values...)
|
|
}
|
|
|
|
func cloneLimitStatuses(values []LimitStatus) []LimitStatus {
|
|
if len(values) == 0 {
|
|
return []LimitStatus{}
|
|
}
|
|
return append([]LimitStatus(nil), values...)
|
|
}
|
|
|
|
func CommunityRuntimeIdentity() RuntimeIdentity {
|
|
return RuntimeIdentity{
|
|
Build: RuntimeBuildCommunity,
|
|
Label: "Pulse Community runtime",
|
|
}
|
|
}
|
|
|
|
func ProRuntimeIdentity() RuntimeIdentity {
|
|
return RuntimeIdentity{
|
|
Build: RuntimeBuildPro,
|
|
Label: "Pulse Pro runtime",
|
|
DownloadURL: PulseProDownloadURL,
|
|
}
|
|
}
|
|
|
|
func NormalizeRuntimeIdentity(identity RuntimeIdentity) RuntimeIdentity {
|
|
build := strings.ToLower(strings.TrimSpace(identity.Build))
|
|
switch build {
|
|
case RuntimeBuildPro:
|
|
normalized := ProRuntimeIdentity()
|
|
if label := strings.TrimSpace(identity.Label); label != "" {
|
|
normalized.Label = label
|
|
}
|
|
if downloadURL := strings.TrimSpace(identity.DownloadURL); downloadURL != "" {
|
|
normalized.DownloadURL = downloadURL
|
|
}
|
|
return normalized
|
|
default:
|
|
normalized := CommunityRuntimeIdentity()
|
|
if label := strings.TrimSpace(identity.Label); label != "" {
|
|
normalized.Label = label
|
|
}
|
|
return normalized
|
|
}
|
|
}
|
|
|
|
func IsProRuntime(identity RuntimeIdentity) bool {
|
|
return NormalizeRuntimeIdentity(identity).Build == RuntimeBuildPro
|
|
}
|
|
|
|
func CloneRuntimeIdentity(identity RuntimeIdentity) *RuntimeIdentity {
|
|
normalized := NormalizeRuntimeIdentity(identity)
|
|
return &normalized
|
|
}
|
|
|
|
func FilterCapabilitiesForRuntimeIdentity(
|
|
capabilities []string,
|
|
identity RuntimeIdentity,
|
|
) ([]string, []RuntimeCapabilityBlock) {
|
|
cloned := cloneCapabilityKeys(capabilities)
|
|
if IsProRuntime(identity) {
|
|
return cloned, nil
|
|
}
|
|
|
|
privateRuntimeCapabilities := map[string]struct{}{
|
|
FeatureAIAlerts: {},
|
|
FeatureAIAutoFix: {},
|
|
FeatureKubernetesAI: {},
|
|
FeatureAgentProfiles: {},
|
|
FeatureRBAC: {},
|
|
FeatureAuditLogging: {},
|
|
}
|
|
filtered := make([]string, 0, len(cloned))
|
|
blocked := make([]RuntimeCapabilityBlock, 0)
|
|
for _, capability := range cloned {
|
|
normalized := strings.TrimSpace(capability)
|
|
if _, requiresPrivateRuntime := privateRuntimeCapabilities[normalized]; requiresPrivateRuntime {
|
|
blocked = append(blocked, RuntimeCapabilityBlock{
|
|
Key: normalized,
|
|
Reason: "paid_runtime_required",
|
|
ActionURL: PulseProDownloadURL,
|
|
})
|
|
continue
|
|
}
|
|
filtered = append(filtered, capability)
|
|
}
|
|
if blocked == nil {
|
|
blocked = []RuntimeCapabilityBlock{}
|
|
}
|
|
return filtered, blocked
|
|
}
|
|
|
|
func (c LegacyConnectionCounts) Total() int64 {
|
|
return c.ProxmoxNodes + c.DockerHosts + c.KubernetesClusters
|
|
}
|
|
|
|
type EntitlementUsageSnapshot struct {
|
|
MonitoredSystems int64
|
|
MonitoredSystemsAvailable bool
|
|
MonitoredSystemsUnavailableReason string
|
|
// Nodes is retained only as a deprecated compatibility field. V6 monitored-
|
|
// system billing must be backed by an explicit canonical availability signal.
|
|
Nodes int64
|
|
Guests int64
|
|
LegacyConnections LegacyConnectionCounts
|
|
}
|
|
|
|
// BuildEntitlementPayload constructs the normalized payload from LicenseStatus.
|
|
func BuildEntitlementPayload(status *LicenseStatus, subscriptionState string) EntitlementPayload {
|
|
return BuildEntitlementPayloadWithUsage(status, subscriptionState, EntitlementUsageSnapshot{}, nil)
|
|
}
|
|
|
|
// BuildRuntimeCapabilitiesPayload constructs the canonical non-commercial
|
|
// runtime capability payload from LicenseStatus.
|
|
func BuildRuntimeCapabilitiesPayload(
|
|
status *LicenseStatus,
|
|
subscriptionState string,
|
|
) RuntimeCapabilitiesPayload {
|
|
return BuildRuntimeCapabilitiesPayloadWithUsage(
|
|
status,
|
|
subscriptionState,
|
|
EntitlementUsageSnapshot{},
|
|
)
|
|
}
|
|
|
|
// BuildRuntimeCapabilitiesPayloadWithUsage constructs the canonical
|
|
// non-commercial runtime capability payload from LicenseStatus and observed usage.
|
|
func BuildRuntimeCapabilitiesPayloadWithUsage(
|
|
status *LicenseStatus,
|
|
subscriptionState string,
|
|
usage EntitlementUsageSnapshot,
|
|
) RuntimeCapabilitiesPayload {
|
|
entitlementPayload := BuildEntitlementPayloadWithUsage(status, subscriptionState, usage, nil)
|
|
return RuntimeCapabilitiesPayload{
|
|
Capabilities: cloneCapabilityKeys(entitlementPayload.Capabilities),
|
|
Limits: cloneLimitStatuses(entitlementPayload.Limits),
|
|
HostedMode: entitlementPayload.HostedMode,
|
|
MaxHistoryDays: entitlementPayload.MaxHistoryDays,
|
|
}
|
|
}
|
|
|
|
// BuildCommercialPosturePayloadWithUsage constructs the canonical non-billing
|
|
// commercial posture payload from LicenseStatus and observed usage.
|
|
func BuildCommercialPosturePayloadWithUsage(
|
|
status *LicenseStatus,
|
|
subscriptionState string,
|
|
usage EntitlementUsageSnapshot,
|
|
trialEndsAtUnix *int64,
|
|
) CommercialPosturePayload {
|
|
return CommercialPosturePayloadFromEntitlementPayload(
|
|
BuildEntitlementPayloadWithUsage(status, subscriptionState, usage, trialEndsAtUnix),
|
|
)
|
|
}
|
|
|
|
// CommercialPosturePayloadFromEntitlementPayload projects the non-billing
|
|
// commercial posture fields out of the full entitlement payload.
|
|
func CommercialPosturePayloadFromEntitlementPayload(
|
|
payload EntitlementPayload,
|
|
) CommercialPosturePayload {
|
|
sanitized := CommercialPosturePayload{
|
|
SubscriptionState: payload.SubscriptionState,
|
|
UpgradeReasons: append([]UpgradeReason(nil), payload.UpgradeReasons...),
|
|
Tier: payload.Tier,
|
|
TrialExpiresAt: payload.TrialExpiresAt,
|
|
TrialDaysRemaining: payload.TrialDaysRemaining,
|
|
TrialEligible: payload.TrialEligible,
|
|
TrialEligibilityReason: payload.TrialEligibilityReason,
|
|
OverflowDaysRemaining: payload.OverflowDaysRemaining,
|
|
LegacyConnections: payload.LegacyConnections,
|
|
HasMigrationGap: payload.HasMigrationGap,
|
|
}
|
|
if payload.CommercialMigration != nil {
|
|
sanitized.CommercialMigration = CloneCommercialMigrationStatus(payload.CommercialMigration)
|
|
}
|
|
if sanitized.UpgradeReasons == nil {
|
|
sanitized.UpgradeReasons = []UpgradeReason{}
|
|
}
|
|
return sanitized
|
|
}
|
|
|
|
// BuildEntitlementPayloadWithUsage constructs the normalized payload from LicenseStatus and observed usage.
|
|
func BuildEntitlementPayloadWithUsage(
|
|
status *LicenseStatus,
|
|
subscriptionState string,
|
|
usage EntitlementUsageSnapshot,
|
|
trialEndsAtUnix *int64,
|
|
) EntitlementPayload {
|
|
if status == nil {
|
|
return EntitlementPayload{
|
|
Capabilities: []string{},
|
|
Limits: []LimitStatus{},
|
|
SubscriptionState: string(SubStateExpired),
|
|
UpgradeReasons: []UpgradeReason{},
|
|
Tier: string(TierFree),
|
|
MaxHistoryDays: TierHistoryDays[TierFree],
|
|
}
|
|
}
|
|
|
|
maxHistDays := TierHistoryDays[status.Tier]
|
|
if maxHistDays == 0 {
|
|
maxHistDays = TierHistoryDays[TierFree]
|
|
}
|
|
|
|
payload := EntitlementPayload{
|
|
Capabilities: FilterPublicCapabilities(status.Features),
|
|
Limits: []LimitStatus{},
|
|
PlanVersion: status.PlanVersion,
|
|
Tier: string(status.Tier),
|
|
UpgradeReasons: []UpgradeReason{},
|
|
Valid: status.Valid,
|
|
LicensedEmail: status.Email,
|
|
ExpiresAt: status.ExpiresAt,
|
|
IsLifetime: status.IsLifetime,
|
|
DaysRemaining: status.DaysRemaining,
|
|
InGracePeriod: status.InGracePeriod,
|
|
GracePeriodEnd: status.GracePeriodEnd,
|
|
MaxHistoryDays: maxHistDays,
|
|
LegacyConnections: usage.LegacyConnections,
|
|
HasMigrationGap: false,
|
|
}
|
|
if payload.Capabilities == nil {
|
|
payload.Capabilities = []string{}
|
|
}
|
|
|
|
// Use provided subscription state when present; otherwise derive from status.
|
|
if subscriptionState == "" {
|
|
subState := SubStateActive
|
|
if !status.Valid {
|
|
subState = SubStateExpired
|
|
} else if status.InGracePeriod {
|
|
subState = SubStateGrace
|
|
}
|
|
subscriptionState = string(subState)
|
|
}
|
|
payload.SubscriptionState = string(GetBehavior(SubscriptionState(subscriptionState)).State)
|
|
|
|
if payload.SubscriptionState == string(SubStateTrial) {
|
|
applyTrialWindow(&payload, status, trialEndsAtUnix, time.Now().Unix())
|
|
}
|
|
|
|
// When subscription state doesn't grant paid features, cap history to free tier.
|
|
if !subscriptionStateHasPaidFeatures(SubscriptionState(payload.SubscriptionState)) {
|
|
payload.MaxHistoryDays = TierHistoryDays[TierFree]
|
|
}
|
|
|
|
// Build hosted-only runtime limits. Self-hosted commercial volume caps are retired.
|
|
if status.Tier == TierCloud && status.MaxGuests > 0 {
|
|
payload.Limits = append(payload.Limits, LimitStatus{
|
|
Key: "max_guests",
|
|
Limit: int64(status.MaxGuests),
|
|
Current: usage.Guests,
|
|
State: LimitState(usage.Guests, int64(status.MaxGuests)),
|
|
})
|
|
}
|
|
|
|
reasons := GenerateUpgradeReasons(payload.Capabilities)
|
|
payload.UpgradeReasons = make([]UpgradeReason, 0, len(reasons))
|
|
for _, reason := range reasons {
|
|
payload.UpgradeReasons = append(payload.UpgradeReasons, UpgradeReason{
|
|
Key: reason.Feature,
|
|
Reason: reason.Reason,
|
|
ActionURL: reason.ActionURL,
|
|
})
|
|
}
|
|
|
|
return payload
|
|
}
|
|
|
|
func applyTrialWindow(payload *EntitlementPayload, status *LicenseStatus, trialEndsAtUnix *int64, nowUnix int64) {
|
|
if payload == nil || status == nil {
|
|
return
|
|
}
|
|
// Prefer billing-state trial timestamps (hosted/self-hosted trial) over license ExpiresAt.
|
|
if trialEndsAtUnix != nil {
|
|
expiresAtUnix := *trialEndsAtUnix
|
|
payload.TrialExpiresAt = &expiresAtUnix
|
|
daysRemaining := remainingTrialDays(expiresAtUnix, nowUnix)
|
|
payload.TrialDaysRemaining = &daysRemaining
|
|
return
|
|
}
|
|
if status.ExpiresAt == nil {
|
|
return
|
|
}
|
|
expiresAt, err := time.Parse(time.RFC3339, *status.ExpiresAt)
|
|
if err != nil {
|
|
return
|
|
}
|
|
expiresAtUnix := expiresAt.Unix()
|
|
payload.TrialExpiresAt = &expiresAtUnix
|
|
daysRemaining := remainingTrialDays(expiresAtUnix, nowUnix)
|
|
payload.TrialDaysRemaining = &daysRemaining
|
|
}
|
|
|
|
func remainingTrialDays(expiresAtUnix, nowUnix int64) int {
|
|
daysRemaining := int(math.Ceil(float64(expiresAtUnix-nowUnix) / 86400.0))
|
|
if daysRemaining < 0 {
|
|
daysRemaining = 0
|
|
}
|
|
return daysRemaining
|
|
}
|
|
|
|
// LimitState returns the over-limit UX state string.
|
|
func LimitState(current, limit int64) string {
|
|
if limit <= 0 {
|
|
return "ok" // unlimited
|
|
}
|
|
if current >= limit {
|
|
return "enforced"
|
|
}
|
|
// For small limits (≤10, but >1), warn at N-1 so users get notice before hitting the wall.
|
|
// For larger limits, use 90% threshold.
|
|
if limit > 1 && limit <= 10 {
|
|
if current >= limit-1 {
|
|
return "warning"
|
|
}
|
|
} else if current*10 >= limit*9 {
|
|
return "warning"
|
|
}
|
|
return "ok"
|
|
}
|