mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
Provider-hosted MSP client workspaces previously sat at Community tier forever: the runtime refreshed leases against the built-in Pulse Cloud URL (hibernated, 522) instead of the provider control plane, and release-build images verify leases only against the embedded Pulse key, which an operator-generated CP_TRIAL_ACTIVATION_PRIVATE_KEY can never satisfy. - Inject PULSE_PRO_TRIAL_SIGNUP_URL=CP_BASE_URL into client containers so lease refresh targets the provider control plane. - Chain trust through the Pulse-signed provider MSP license: the license binds the provider's lease signing public key (entitlement_signing_public_key claim); the control plane embeds the license in every lease (provider_license claim); release-build runtimes verify embedded Pulse root -> provider license -> lease signature. - Cap chain-verified leases at ProviderChainedLeaseCapabilities: MSP tier plus white_label (branded per-client reports), minus Pulse-service-backed relay/mobile_app/push_notifications, which otherwise loop doomed registrations against Pulse's relay. - Fail fast at control-plane startup when the license does not bind the configured signing key, instead of provisioning silently unlicensed client workspaces. Verified live on a Colima harness: release-tagged tenant image with test embedded root, Traefik TLS, full provider-msp proof, tenant reports valid=true plan_version=msp_growth with white_label and zero relay failures.
190 lines
6 KiB
Go
190 lines
6 KiB
Go
package licensing
|
|
|
|
import (
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
// Claims represents the JWT claims in a Pulse Pro license.
|
|
type Claims struct {
|
|
// License ID (unique identifier)
|
|
LicenseID string `json:"lid"`
|
|
|
|
// Email of the license holder
|
|
Email string `json:"email"`
|
|
|
|
// License tier (pro, pro_annual, lifetime, msp, enterprise)
|
|
Tier Tier `json:"tier"`
|
|
|
|
// Issued at (Unix timestamp)
|
|
IssuedAt int64 `json:"iat"`
|
|
|
|
// Expires at (Unix timestamp, 0 for lifetime)
|
|
ExpiresAt int64 `json:"exp,omitempty"`
|
|
|
|
// Features explicitly granted (optional, tier implies features)
|
|
Features []string `json:"features,omitempty"`
|
|
|
|
// Max guests (0 = unlimited)
|
|
MaxGuests int `json:"max_guests,omitempty"`
|
|
|
|
// Entitlement primitives (B1) - when present, these override tier-based derivation.
|
|
// When absent (nil/empty), entitlements are derived from Tier + existing fields.
|
|
Capabilities []string `json:"capabilities,omitempty"`
|
|
Limits map[string]int64 `json:"limits,omitempty"`
|
|
MetersEnabled []string `json:"meters_enabled,omitempty"`
|
|
PlanVersion string `json:"plan_version,omitempty"`
|
|
SubState SubscriptionState `json:"subscription_state,omitempty"`
|
|
|
|
// EntitlementSigningPublicKey binds a provider-operated control plane's
|
|
// entitlement lease signing key (base64 Ed25519 public key) to this
|
|
// license. Hosted entitlement leases signed by the bound key chain back
|
|
// to the embedded Pulse trust root through this license, so release
|
|
// builds can verify provider-minted leases without trusting environment
|
|
// wiring. Only meaningful on MSP-tier licenses.
|
|
EntitlementSigningPublicKey string `json:"entitlement_signing_public_key,omitempty"`
|
|
|
|
// CoreMonitoringUncapped is a local runtime marker used when a self-hosted
|
|
// activation/grant should remain uncapped even if stale legacy limit fields
|
|
// are still present in the persisted claims. It is intentionally excluded
|
|
// from the JWT/storage wire contract.
|
|
CoreMonitoringUncapped bool `json:"-"`
|
|
}
|
|
|
|
// EffectiveCapabilities returns explicit capabilities when present; otherwise tier-derived capabilities.
|
|
func (c Claims) EffectiveCapabilities() []string {
|
|
if c.Capabilities != nil && len(c.Capabilities) > 0 {
|
|
return c.Capabilities
|
|
}
|
|
return DeriveCapabilitiesFromTier(c.Tier, c.Features)
|
|
}
|
|
|
|
// EffectiveLimits returns explicit limits when present; otherwise limits derived from legacy fields.
|
|
func (c Claims) EffectiveLimits() map[string]int64 {
|
|
limits := NormalizeMonitoredSystemLimits(c.Limits)
|
|
stripSelfHostedCommercialVolumeCaps(limits, c.EntitlementPlanVersion(), c.Tier, c.CoreMonitoringUncapped)
|
|
if len(limits) == 0 {
|
|
limits = make(map[string]int64)
|
|
if c.MaxGuests > 0 {
|
|
limits["max_guests"] = int64(c.MaxGuests)
|
|
}
|
|
stripSelfHostedCommercialVolumeCaps(limits, c.EntitlementPlanVersion(), c.Tier, c.CoreMonitoringUncapped)
|
|
}
|
|
return limits
|
|
}
|
|
|
|
// EntitlementMetersEnabled returns metering keys for evaluator sources.
|
|
func (c *Claims) EntitlementMetersEnabled() []string {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
return c.MetersEnabled
|
|
}
|
|
|
|
// EntitlementPlanVersion returns plan metadata for evaluator sources.
|
|
func (c *Claims) EntitlementPlanVersion() string {
|
|
if c == nil {
|
|
return ""
|
|
}
|
|
return CanonicalizePlanVersion(c.PlanVersion)
|
|
}
|
|
|
|
// EntitlementSubscriptionState returns normalized subscription state for evaluator sources.
|
|
func (c *Claims) EntitlementSubscriptionState() SubscriptionState {
|
|
if c == nil || c.SubState == "" {
|
|
return SubStateActive
|
|
}
|
|
return c.SubState
|
|
}
|
|
|
|
// License represents a validated Pulse Pro license.
|
|
type License struct {
|
|
// Raw JWT token
|
|
Raw string `json:"-"`
|
|
|
|
// Validated claims
|
|
Claims Claims `json:"claims"`
|
|
|
|
// Validation metadata
|
|
ValidatedAt time.Time `json:"validated_at"`
|
|
|
|
// Grace period end (if license was validated during grace period)
|
|
GracePeriodEnd *time.Time `json:"grace_period_end,omitempty"`
|
|
}
|
|
|
|
// IsExpired checks if the license has expired.
|
|
func (l *License) IsExpired() bool {
|
|
if l.Claims.ExpiresAt == 0 {
|
|
return false // Lifetime license never expires
|
|
}
|
|
return time.Now().Unix() > l.Claims.ExpiresAt
|
|
}
|
|
|
|
// IsLifetime returns true if this is a lifetime license.
|
|
func (l *License) IsLifetime() bool {
|
|
return l.Claims.ExpiresAt == 0 || l.Claims.Tier == TierLifetime
|
|
}
|
|
|
|
// DaysRemaining returns the number of days until expiration.
|
|
// Returns -1 for lifetime licenses.
|
|
func (l *License) DaysRemaining() int {
|
|
if l.IsLifetime() {
|
|
return -1
|
|
}
|
|
remaining := time.Until(time.Unix(l.Claims.ExpiresAt, 0))
|
|
if remaining < 0 {
|
|
return 0
|
|
}
|
|
return int(remaining.Hours() / 24)
|
|
}
|
|
|
|
// ExpiresAt returns the expiration time, or nil for lifetime.
|
|
func (l *License) ExpiresAt() *time.Time {
|
|
if l.IsLifetime() {
|
|
return nil
|
|
}
|
|
t := time.Unix(l.Claims.ExpiresAt, 0)
|
|
return &t
|
|
}
|
|
|
|
// HasFeature checks if the license grants a specific feature.
|
|
func (l *License) HasFeature(feature string) bool {
|
|
for _, capability := range l.Claims.EffectiveCapabilities() {
|
|
if capability == feature {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// AllFeatures returns all features granted by this license.
|
|
func (l *License) AllFeatures() []string {
|
|
features := append([]string(nil), l.Claims.EffectiveCapabilities()...)
|
|
sort.Strings(features)
|
|
return features
|
|
}
|
|
|
|
// LicenseState represents the current state of the license.
|
|
type LicenseState string
|
|
|
|
const (
|
|
LicenseStateNone LicenseState = "none"
|
|
LicenseStateActive LicenseState = "active"
|
|
LicenseStateExpired LicenseState = "expired"
|
|
LicenseStateGracePeriod LicenseState = "grace_period"
|
|
)
|
|
|
|
// LicenseStatus is the JSON response for license status API.
|
|
type LicenseStatus struct {
|
|
Valid bool `json:"valid"`
|
|
Tier Tier `json:"tier"`
|
|
PlanVersion string `json:"plan_version,omitempty"`
|
|
Email string `json:"email,omitempty"`
|
|
ExpiresAt *string `json:"expires_at,omitempty"`
|
|
IsLifetime bool `json:"is_lifetime"`
|
|
DaysRemaining int `json:"days_remaining"`
|
|
Features []string `json:"features"`
|
|
MaxGuests int `json:"max_guests,omitempty"`
|
|
InGracePeriod bool `json:"in_grace_period,omitempty"`
|
|
GracePeriodEnd *string `json:"grace_period_end,omitempty"`
|
|
}
|