Split runtime capabilities from commercial entitlements

This commit is contained in:
rcourtman 2026-04-07 11:40:57 +01:00
parent e4fa4824b9
commit f8ad57a1f2
106 changed files with 2907 additions and 1013 deletions

View file

@ -84,6 +84,22 @@ type EntitlementPayload struct {
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"`
}
// LimitStatus represents a quantitative limit with current usage state.
type LimitStatus struct {
// Key is the limit identifier (e.g., "max_monitored_systems").
@ -141,6 +157,35 @@ func BuildEntitlementPayload(status *LicenseStatus, subscriptionState string) En
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: append([]string(nil), entitlementPayload.Capabilities...),
Limits: append([]LimitStatus(nil), entitlementPayload.Limits...),
HostedMode: entitlementPayload.HostedMode,
MaxHistoryDays: entitlementPayload.MaxHistoryDays,
}
}
// BuildEntitlementPayloadWithUsage constructs the normalized payload from LicenseStatus and observed usage.
func BuildEntitlementPayloadWithUsage(
status *LicenseStatus,

View file

@ -240,6 +240,45 @@ func TestBuildEntitlementPayload_NilStatus_MaxHistoryDays(t *testing.T) {
}
}
func TestBuildRuntimeCapabilitiesPayloadWithUsage_CurrentValues(t *testing.T) {
status := &LicenseStatus{
Valid: true,
Tier: TierRelay,
Features: append([]string(nil), TierFeatures[TierRelay]...),
MaxMonitoredSystems: 12,
}
payload := BuildRuntimeCapabilitiesPayloadWithUsage(status, "", EntitlementUsageSnapshot{
MonitoredSystems: 5,
})
if !reflect.DeepEqual(payload.Capabilities, status.Features) {
t.Fatalf("expected capabilities to match status features")
}
if payload.MaxHistoryDays != TierHistoryDays[TierRelay] {
t.Fatalf("expected relay max history days %d, got %d", TierHistoryDays[TierRelay], payload.MaxHistoryDays)
}
if len(payload.Limits) != 1 {
t.Fatalf("expected one runtime limit, got %d", len(payload.Limits))
}
if payload.Limits[0].Key != MaxMonitoredSystemsLicenseGateKey {
t.Fatalf("expected runtime limit key %q, got %q", MaxMonitoredSystemsLicenseGateKey, payload.Limits[0].Key)
}
if payload.Limits[0].Current != 5 {
t.Fatalf("expected runtime current 5, got %d", payload.Limits[0].Current)
}
}
func TestBuildRuntimeCapabilitiesPayload_NilStatusDefaultsToFreeTier(t *testing.T) {
payload := BuildRuntimeCapabilitiesPayload(nil, "")
if payload.MaxHistoryDays != TierHistoryDays[TierFree] {
t.Fatalf("expected MaxHistoryDays=%d for nil status, got %d", TierHistoryDays[TierFree], payload.MaxHistoryDays)
}
if len(payload.Capabilities) != 0 {
t.Fatalf("expected no runtime capabilities for nil status, got %+v", payload.Capabilities)
}
}
func TestLimitState(t *testing.T) {
tests := []struct {
name string

View file

@ -9,6 +9,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
@ -20,6 +21,21 @@ type LicenseServerClient struct {
httpClient *http.Client
}
// CheckoutSessionResult is the canonical purchase-fulfillment response returned
// by the commercial checkout session endpoint.
type CheckoutSessionResult struct {
Status string `json:"status"`
Message string `json:"message,omitempty"`
CheckoutStatus string `json:"checkout_status,omitempty"`
PaymentStatus string `json:"payment_status,omitempty"`
LicenseID string `json:"license_id,omitempty"`
ActivationKey string `json:"activation_key,omitempty"`
ActivationKeyPrefix string `json:"activation_key_prefix,omitempty"`
OwnerEmail string `json:"owner_email,omitempty"`
Tier string `json:"tier,omitempty"`
PlanKey string `json:"plan_key,omitempty"`
}
// NewLicenseServerClient creates a client for the license server.
// The base URL defaults to DefaultLicenseServerURL. The PULSE_LICENSE_SERVER_URL
// env var override is only allowed in non-release builds.
@ -140,6 +156,42 @@ func (c *LicenseServerClient) RefreshGrant(ctx context.Context, installationID,
return &result, nil
}
// GetCheckoutSessionResult resolves a commercial checkout session into its
// authoritative fulfillment payload.
func (c *LicenseServerClient) GetCheckoutSessionResult(ctx context.Context, sessionID string) (*CheckoutSessionResult, error) {
sessionID = strings.TrimSpace(sessionID)
if sessionID == "" {
return nil, fmt.Errorf("checkout session id is required")
}
httpReq, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
c.baseURL+"/v1/checkout/session?session_id="+url.QueryEscape(sessionID),
nil,
)
if err != nil {
return nil, fmt.Errorf("create checkout session result request: %w", err)
}
httpReq.Header.Set("Accept", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("checkout session result request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, c.parseError(resp)
}
var result CheckoutSessionResult
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&result); err != nil {
return nil, fmt.Errorf("decode checkout session result response: %w", err)
}
return &result, nil
}
// BootstrapQuickstart exchanges a runtime identity for a short-lived
// quickstart token and the current authoritative quickstart inventory.
// Callers must pass a server-verified quickstart authority token as

View file

@ -270,6 +270,58 @@ func TestClientExchangeLegacyLicense(t *testing.T) {
})
}
func TestClientGetCheckoutSessionResult(t *testing.T) {
t.Run("successful checkout session lookup", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("Method = %q, want GET", r.Method)
}
if r.URL.Path != "/v1/checkout/session" {
t.Errorf("Path = %q, want /v1/checkout/session", r.URL.Path)
}
if got := r.URL.Query().Get("session_id"); got != "cs_test_123" {
t.Errorf("session_id = %q, want cs_test_123", got)
}
if r.Header.Get("Accept") != "application/json" {
t.Errorf("Accept = %q, want application/json", r.Header.Get("Accept"))
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(CheckoutSessionResult{
Status: "fulfilled",
CheckoutStatus: "complete",
PaymentStatus: "paid",
ActivationKeyPrefix: "ppk_live_",
Tier: "pro",
})
}))
defer server.Close()
client := NewLicenseServerClient(server.URL)
result, err := client.GetCheckoutSessionResult(context.Background(), " cs_test_123 ")
if err != nil {
t.Fatalf("GetCheckoutSessionResult failed: %v", err)
}
if result.Status != "fulfilled" {
t.Fatalf("Status = %q, want fulfilled", result.Status)
}
if result.CheckoutStatus != "complete" {
t.Fatalf("CheckoutStatus = %q, want complete", result.CheckoutStatus)
}
})
t.Run("empty session id fails before transport", func(t *testing.T) {
client := NewLicenseServerClient("https://license.example.com")
_, err := client.GetCheckoutSessionResult(context.Background(), " ")
if err == nil {
t.Fatal("expected error")
}
if err.Error() != "checkout session id is required" {
t.Fatalf("err = %q, want checkout session id is required", err.Error())
}
})
}
func TestClientRefreshGrant(t *testing.T) {
t.Run("successful refresh", func(t *testing.T) {
newGrantJWT := makeTestGrantJWT(t, &GrantClaims{