mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-20 02:42:11 +00:00
217 lines
8.6 KiB
Go
217 lines
8.6 KiB
Go
package licensing
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type CommercialMigrationSource string
|
|
type CommercialMigrationState string
|
|
type CommercialMigrationReason string
|
|
type CommercialMigrationAction string
|
|
|
|
const (
|
|
CommercialMigrationSourceV5License CommercialMigrationSource = "v5_license"
|
|
|
|
CommercialMigrationStatePending CommercialMigrationState = "pending"
|
|
CommercialMigrationStateFailed CommercialMigrationState = "failed"
|
|
|
|
CommercialMigrationReasonExchangeUnavailable CommercialMigrationReason = "exchange_unavailable"
|
|
CommercialMigrationReasonExchangeRateLimited CommercialMigrationReason = "exchange_rate_limited"
|
|
CommercialMigrationReasonExchangeConflict CommercialMigrationReason = "exchange_conflict"
|
|
CommercialMigrationReasonExchangeInstallationLimit CommercialMigrationReason = "exchange_installation_limit"
|
|
CommercialMigrationReasonExchangeInvalid CommercialMigrationReason = "exchange_invalid"
|
|
CommercialMigrationReasonExchangeMalformed CommercialMigrationReason = "exchange_malformed"
|
|
CommercialMigrationReasonExchangeRevoked CommercialMigrationReason = "exchange_revoked"
|
|
CommercialMigrationReasonExchangeNonMigratable CommercialMigrationReason = "exchange_non_migratable"
|
|
CommercialMigrationReasonExchangeUnsupportedKey CommercialMigrationReason = "exchange_unsupported"
|
|
CommercialMigrationReasonExchangeStaleKey CommercialMigrationReason = "exchange_stale_key"
|
|
CommercialMigrationReasonExchangeConnectivity CommercialMigrationReason = "exchange_connectivity_required"
|
|
|
|
CommercialMigrationActionRetryActivation CommercialMigrationAction = "retry_activation"
|
|
CommercialMigrationActionUseV6Activation CommercialMigrationAction = "use_v6_activation_key"
|
|
CommercialMigrationActionEnterSupportedV5 CommercialMigrationAction = "enter_supported_v5_key"
|
|
CommercialMigrationActionFreeInstallationSlot CommercialMigrationAction = "free_installation_slot"
|
|
CommercialMigrationActionRetrieveCurrentKey CommercialMigrationAction = "retrieve_current_key"
|
|
CommercialMigrationActionAllowLicenseEgress CommercialMigrationAction = "allow_license_egress"
|
|
)
|
|
|
|
const CommercialMigrationSustainedExchangeUnavailableSeconds int64 = 24 * 60 * 60
|
|
|
|
// CommercialMigrationStatus is the explicit v6-owned contract for unresolved
|
|
// paid-license migrations entering from pre-v6 commercial state.
|
|
type CommercialMigrationStatus struct {
|
|
Source CommercialMigrationSource `json:"source,omitempty"`
|
|
State CommercialMigrationState `json:"state,omitempty"`
|
|
Reason CommercialMigrationReason `json:"reason,omitempty"`
|
|
RecommendedAction CommercialMigrationAction `json:"recommended_action,omitempty"`
|
|
FirstFailedAt int64 `json:"first_failed_at,omitempty"`
|
|
}
|
|
|
|
func (s *CommercialMigrationStatus) Active() bool {
|
|
return s != nil && strings.TrimSpace(string(s.State)) != ""
|
|
}
|
|
|
|
func NormalizeCommercialMigrationStatus(status *CommercialMigrationStatus) *CommercialMigrationStatus {
|
|
if status == nil {
|
|
return nil
|
|
}
|
|
|
|
normalized := *status
|
|
normalized.Source = CommercialMigrationSource(strings.TrimSpace(string(normalized.Source)))
|
|
normalized.State = CommercialMigrationState(strings.TrimSpace(string(normalized.State)))
|
|
normalized.Reason = CommercialMigrationReason(strings.TrimSpace(string(normalized.Reason)))
|
|
normalized.RecommendedAction = CommercialMigrationAction(strings.TrimSpace(string(normalized.RecommendedAction)))
|
|
if normalized.FirstFailedAt < 0 {
|
|
normalized.FirstFailedAt = 0
|
|
}
|
|
|
|
switch normalized.State {
|
|
case CommercialMigrationStatePending, CommercialMigrationStateFailed:
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
if normalized.Source == "" {
|
|
normalized.Source = CommercialMigrationSourceV5License
|
|
}
|
|
if normalized.RecommendedAction == "" {
|
|
switch normalized.State {
|
|
case CommercialMigrationStatePending:
|
|
normalized.RecommendedAction = CommercialMigrationActionRetryActivation
|
|
case CommercialMigrationStateFailed:
|
|
normalized.RecommendedAction = CommercialMigrationActionUseV6Activation
|
|
}
|
|
}
|
|
|
|
return &normalized
|
|
}
|
|
|
|
func CloneCommercialMigrationStatus(status *CommercialMigrationStatus) *CommercialMigrationStatus {
|
|
if status == nil {
|
|
return nil
|
|
}
|
|
cloned := *status
|
|
return &cloned
|
|
}
|
|
|
|
func ApplyCommercialMigrationFailureTiming(status, previous *CommercialMigrationStatus, now time.Time) *CommercialMigrationStatus {
|
|
normalized := NormalizeCommercialMigrationStatus(status)
|
|
if normalized == nil {
|
|
return nil
|
|
}
|
|
if !commercialMigrationTransportUnavailableReason(normalized.Reason) || normalized.State != CommercialMigrationStatePending {
|
|
normalized.FirstFailedAt = 0
|
|
return normalized
|
|
}
|
|
if now.IsZero() {
|
|
now = time.Now()
|
|
}
|
|
|
|
firstFailedAt := normalized.FirstFailedAt
|
|
if prev := NormalizeCommercialMigrationStatus(previous); prev != nil &&
|
|
prev.State == CommercialMigrationStatePending &&
|
|
commercialMigrationTransportUnavailableReason(prev.Reason) &&
|
|
prev.FirstFailedAt > 0 {
|
|
firstFailedAt = prev.FirstFailedAt
|
|
}
|
|
if firstFailedAt <= 0 {
|
|
firstFailedAt = now.Unix()
|
|
}
|
|
|
|
normalized.FirstFailedAt = firstFailedAt
|
|
if now.Unix()-firstFailedAt >= CommercialMigrationSustainedExchangeUnavailableSeconds {
|
|
normalized.Reason = CommercialMigrationReasonExchangeConnectivity
|
|
normalized.RecommendedAction = CommercialMigrationActionAllowLicenseEgress
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
func commercialMigrationTransportUnavailableReason(reason CommercialMigrationReason) bool {
|
|
switch reason {
|
|
case CommercialMigrationReasonExchangeUnavailable, CommercialMigrationReasonExchangeConnectivity:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ClassifyLegacyExchangeError converts startup/manual exchange errors into a
|
|
// retryable or terminal v6 migration contract.
|
|
func ClassifyLegacyExchangeError(err error) *CommercialMigrationStatus {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
status := &CommercialMigrationStatus{
|
|
Source: CommercialMigrationSourceV5License,
|
|
State: CommercialMigrationStatePending,
|
|
Reason: CommercialMigrationReasonExchangeUnavailable,
|
|
RecommendedAction: CommercialMigrationActionRetryActivation,
|
|
}
|
|
|
|
var serverErr *LicenseServerError
|
|
if errors.As(err, &serverErr) {
|
|
serverCode := strings.ToUpper(strings.TrimSpace(serverErr.Code))
|
|
switch serverErr.StatusCode {
|
|
case 400:
|
|
status.State = CommercialMigrationStateFailed
|
|
status.Reason = CommercialMigrationReasonExchangeMalformed
|
|
status.RecommendedAction = CommercialMigrationActionEnterSupportedV5
|
|
case 401:
|
|
if serverCode == "RENEWED_KEY_AVAILABLE" {
|
|
status.State = CommercialMigrationStateFailed
|
|
status.Reason = CommercialMigrationReasonExchangeStaleKey
|
|
status.RecommendedAction = CommercialMigrationActionRetrieveCurrentKey
|
|
} else {
|
|
status.State = CommercialMigrationStateFailed
|
|
status.Reason = CommercialMigrationReasonExchangeInvalid
|
|
status.RecommendedAction = CommercialMigrationActionEnterSupportedV5
|
|
}
|
|
case 403:
|
|
status.State = CommercialMigrationStateFailed
|
|
status.Reason = CommercialMigrationReasonExchangeRevoked
|
|
status.RecommendedAction = CommercialMigrationActionUseV6Activation
|
|
case 409:
|
|
if strings.EqualFold(strings.TrimSpace(serverErr.Code), "MAX_INSTALLATIONS") {
|
|
// The key has used up its installation slots server-side.
|
|
// Retrying from this instance can never succeed until a slot
|
|
// is freed, so this is terminal, not pending.
|
|
status.State = CommercialMigrationStateFailed
|
|
status.Reason = CommercialMigrationReasonExchangeInstallationLimit
|
|
status.RecommendedAction = CommercialMigrationActionFreeInstallationSlot
|
|
} else {
|
|
status.State = CommercialMigrationStatePending
|
|
status.Reason = CommercialMigrationReasonExchangeConflict
|
|
}
|
|
case 410:
|
|
status.State = CommercialMigrationStateFailed
|
|
status.Reason = CommercialMigrationReasonExchangeNonMigratable
|
|
status.RecommendedAction = CommercialMigrationActionUseV6Activation
|
|
case 429:
|
|
status.State = CommercialMigrationStatePending
|
|
status.Reason = CommercialMigrationReasonExchangeRateLimited
|
|
default:
|
|
if serverErr.Retryable || serverErr.StatusCode >= 500 {
|
|
status.State = CommercialMigrationStatePending
|
|
status.Reason = CommercialMigrationReasonExchangeUnavailable
|
|
}
|
|
}
|
|
return status
|
|
}
|
|
|
|
msg := strings.ToLower(strings.TrimSpace(err.Error()))
|
|
switch {
|
|
case strings.Contains(msg, "not a supported v6 activation key or migratable v5 license"):
|
|
status.State = CommercialMigrationStateFailed
|
|
status.Reason = CommercialMigrationReasonExchangeUnsupportedKey
|
|
status.RecommendedAction = CommercialMigrationActionEnterSupportedV5
|
|
case strings.Contains(msg, "license server client not configured"):
|
|
status.State = CommercialMigrationStatePending
|
|
status.Reason = CommercialMigrationReasonExchangeUnavailable
|
|
}
|
|
|
|
return status
|
|
}
|