diff --git a/LICENSING_DEVELOPMENT_PLAN.md b/LICENSING_DEVELOPMENT_PLAN.md new file mode 100644 index 00000000..dddfbd4d --- /dev/null +++ b/LICENSING_DEVELOPMENT_PLAN.md @@ -0,0 +1,470 @@ +# BZZZ Licensing Development Plan + +**Date**: 2025-09-01 +**Branch**: `feature/licensing-enforcement` +**Status**: Ready for implementation (depends on KACHING Phase 1) +**Priority**: HIGH - Revenue protection and license enforcement + +## Executive Summary + +BZZZ currently has **zero license enforcement** in production. The system collects license information during setup but completely ignores it at runtime, allowing unlimited unlicensed usage. This plan implements comprehensive license enforcement integrated with KACHING license authority. + +## Current State Analysis + +### ✅ Existing License Components +- License validation UI component (`install/config-ui/app/setup/components/LicenseValidation.tsx`) +- Terms and conditions acceptance (`install/config-ui/app/setup/components/TermsAndConditions.tsx`) +- Mock license validation endpoint (`main.go` lines 1584-1618) +- Test license key documentation (`TEST_LICENSE_KEY.txt`) + +### ❌ Critical Security Gap +- **License data NOT saved to configuration** - Setup collects but discards license info +- **Zero runtime license validation** - System starts without any license checks +- **No integration with license server** - Mock validation only, no real enforcement +- **No cluster binding** - No protection against license sharing across multiple clusters +- **No license expiration checks** - Licenses never expire in practice +- **No feature restrictions** - All features available regardless of license tier + +### Current Configuration Structure Gap + +**Setup Config Missing License Data**: +```go +// api/setup_manager.go line 539 - SetupConfig struct +type SetupConfig struct { + Agent *AgentConfig `json:"agent"` + GitHub *GitHubConfig `json:"github"` + // ... other configs ... + // ❌ NO LICENSE FIELD - license data is collected but discarded! +} +``` + +**Main Config Missing License Support**: +```go +// pkg/config/config.go - Config struct +type Config struct { + Agent AgentConfig `yaml:"agent" json:"agent"` + GitHub GitHubConfig `yaml:"github" json:"github"` + // ... other configs ... + // ❌ NO LICENSE FIELD - runtime ignores licensing completely! +} +``` + +## Development Phases + +### Phase 2A: Configuration System Integration (PRIORITY 1) +**Goal**: Make license data part of BZZZ configuration + +#### 1. Update Configuration Structures +```go +// Add to pkg/config/config.go +type Config struct { + // ... existing fields ... + License LicenseConfig `yaml:"license" json:"license"` +} + +type LicenseConfig struct { + ServerURL string `yaml:"server_url" json:"server_url"` + LicenseKey string `yaml:"license_key" json:"license_key"` + ClusterID string `yaml:"cluster_id" json:"cluster_id"` + Email string `yaml:"email" json:"email"` + OrganizationName string `yaml:"organization_name,omitempty" json:"organization_name,omitempty"` + + // Runtime state (populated during activation) + Token string `yaml:"-" json:"-"` // Don't persist token to file + TokenExpiry time.Time `yaml:"-" json:"-"` + LicenseType string `yaml:"license_type,omitempty" json:"license_type,omitempty"` + MaxNodes int `yaml:"max_nodes,omitempty" json:"max_nodes,omitempty"` + Features []string `yaml:"features,omitempty" json:"features,omitempty"` + ExpiresAt time.Time `yaml:"expires_at,omitempty" json:"expires_at,omitempty"` + + // Setup verification + ValidatedAt time.Time `yaml:"validated_at" json:"validated_at"` + TermsAcceptedAt time.Time `yaml:"terms_accepted_at" json:"terms_accepted_at"` +} +``` + +#### 2. Update Setup Configuration +```go +// Add to api/setup_manager.go SetupConfig struct +type SetupConfig struct { + // ... existing fields ... + License *LicenseConfig `json:"license"` + Terms *TermsAcceptance `json:"terms"` +} + +type TermsAcceptance struct { + Agreed bool `json:"agreed"` + Timestamp time.Time `json:"timestamp"` +} +``` + +#### 3. Fix Setup Save Process +Currently in `generateAndDeployConfig()`, license data is completely ignored. Fix this: +```go +// api/setup_manager.go - Update generateAndDeployConfig() +func (sm *SetupManager) generateAndDeployConfig(setupData SetupConfig) error { + config := Config{ + Agent: setupData.Agent, + GitHub: setupData.GitHub, + License: setupData.License, // ✅ ADD THIS - currently missing! + // ... other fields ... + } + // ... save to config file ... +} +``` + +### Phase 2B: License Validation Integration (PRIORITY 2) +**Goal**: Replace mock validation with KACHING license server + +#### 1. Replace Mock License Validation +**Current (main.go lines 1584-1618)**: +```go +// ❌ REMOVE: Hardcoded mock validation +validLicenseKey := "BZZZ-2025-DEMO-EVAL-001" +if licenseRequest.LicenseKey != validLicenseKey { + // ... return error ... +} +``` + +**New KACHING Integration**: +```go +// ✅ ADD: Real license server validation +func (sm *SetupManager) validateLicenseWithKACHING(email, licenseKey, orgName string) (*LicenseValidationResponse, error) { + client := &http.Client{Timeout: 30 * time.Second} + + reqBody := map[string]string{ + "email": email, + "license_key": licenseKey, + "organization_name": orgName, + } + + // Call KACHING license server + resp, err := client.Post( + sm.config.LicenseServerURL+"/v1/license/activate", + "application/json", + bytes.NewBuffer(jsonData), + ) + + // Parse response and return license details + // Store cluster_id for runtime use +} +``` + +#### 2. Generate and Persist Cluster ID +```go +func generateClusterID() string { + // Generate unique cluster identifier + // Format: bzzz-cluster-- + hostname, _ := os.Hostname() + clusterUUID := uuid.New().String()[:8] + return fmt.Sprintf("bzzz-cluster-%s-%s", clusterUUID, hostname) +} +``` + +### Phase 2C: Runtime License Enforcement (PRIORITY 3) +**Goal**: Enforce license validation during BZZZ startup and operation + +#### 1. Add License Validation to Startup Sequence +**Current startup logic (main.go lines 154-169)**: +```go +func main() { + // ... config loading ... + + if !cfg.IsValidConfiguration() { + startSetupMode(configPath) + return + } + + // ✅ ADD LICENSE VALIDATION HERE - currently missing! + if err := validateLicenseForRuntime(cfg); err != nil { + fmt.Printf("❌ License validation failed: %v\n", err) + fmt.Printf("🔧 License issue detected, entering setup mode...\n") + startSetupMode(configPath) + return + } + + // Continue with normal startup... + startNormalMode(cfg) +} +``` + +#### 2. Implement Runtime License Validation +```go +func validateLicenseForRuntime(cfg *Config) error { + if cfg.License.LicenseKey == "" { + return fmt.Errorf("no license key configured") + } + + if cfg.License.ClusterID == "" { + return fmt.Errorf("no cluster ID configured") + } + + // Check license expiration + if !cfg.License.ExpiresAt.IsZero() && time.Now().After(cfg.License.ExpiresAt) { + return fmt.Errorf("license expired on %v", cfg.License.ExpiresAt.Format("2006-01-02")) + } + + // Attempt license activation with KACHING + client := NewLicenseClient(cfg.License.ServerURL) + token, err := client.ActivateLicense(cfg.License.LicenseKey, cfg.License.ClusterID) + if err != nil { + return fmt.Errorf("license activation failed: %w", err) + } + + // Store token for heartbeat worker + cfg.License.Token = token.AccessToken + cfg.License.TokenExpiry = token.ExpiresAt + + return nil +} +``` + +#### 3. Background License Heartbeat Worker +```go +func startLicenseHeartbeatWorker(cfg *Config, shutdownChan chan struct{}) { + ticker := time.NewTicker(15 * time.Minute) // Heartbeat every 15 minutes + defer ticker.Stop() + + client := NewLicenseClient(cfg.License.ServerURL) + + for { + select { + case <-ticker.C: + // Send heartbeat to KACHING + token, err := client.SendHeartbeat(cfg.License.LicenseKey, cfg.License.ClusterID, cfg.License.Token) + if err != nil { + log.Printf("❌ License heartbeat failed: %v", err) + // Implement exponential backoff and graceful degradation + handleLicenseHeartbeatFailure(err) + continue + } + + // Update token if refreshed + if token.AccessToken != cfg.License.Token { + cfg.License.Token = token.AccessToken + cfg.License.TokenExpiry = token.ExpiresAt + log.Printf("✅ License token refreshed, expires: %v", token.ExpiresAt) + } + + case <-shutdownChan: + // Deactivate license on shutdown + err := client.DeactivateLicense(cfg.License.LicenseKey, cfg.License.ClusterID) + if err != nil { + log.Printf("⚠️ Failed to deactivate license on shutdown: %v", err) + } else { + log.Printf("✅ License deactivated on shutdown") + } + return + } + } +} +``` + +#### 4. License Failure Handling +```go +func handleLicenseHeartbeatFailure(err error) { + // Parse error type + if isLicenseSuspended(err) { + log.Printf("🚨 LICENSE SUSPENDED - STOPPING BZZZ OPERATIONS") + // Hard stop - license suspended by admin + os.Exit(1) + } else if isNetworkError(err) { + log.Printf("⚠️ Network error during heartbeat - continuing with grace period") + // Continue operation with exponential backoff + // Stop if grace period exceeded (e.g., 24 hours) + } else { + log.Printf("❌ Unknown license error: %v", err) + // Implement appropriate fallback + } +} +``` + +#### 5. Token Versioning and Offline Tokens +```go +// On every heartbeat response, compare token_version +if token.TokenVersion > cfg.License.TokenVersion { + // Server bumped version (suspend/cancel or rotation) + cfg.License.TokenVersion = token.TokenVersion +} + +// If server rejects with "stale token_version" → re-activate to fetch a fresh token + +// Offline tokens +// Accept an Ed25519-signed offline token with short expiry when network is unavailable. +// Validate signature + expiry locally; on reconnect, immediately validate with server. +``` + +#### 6. Response Handling Map (recommended) +- 200 OK (heartbeat): update token, token_version +- 403 Forbidden: suspended/cancelled → fail closed, stop operations +- 409 Conflict: cluster slot in use → backoff and re‑activate after grace (or operator action) +- 5xx / network error: continue in grace window with exponential backoff; exit when grace exceeded + +#### 7. Cluster Identity and Telemetry +- Generate cluster_id once; persist in config; include hostname/IP in activation metadata for admin visibility. +- Emit per‑job telemetry to KACHING (align keys: `tokens`, `context_operations`, `cpu_hours`, `temporal_nav_hops`) to drive quotas and upgrade suggestions. + +### Phase 2D: Feature Enforcement (PRIORITY 4) +**Goal**: Restrict features based on license tier + +#### 1. Feature Gate Implementation +```go +type FeatureGate struct { + licensedFeatures map[string]bool +} + +func NewFeatureGate(config *Config) *FeatureGate { + gates := make(map[string]bool) + for _, feature := range config.License.Features { + gates[feature] = true + } + return &FeatureGate{licensedFeatures: gates} +} + +func (fg *FeatureGate) IsEnabled(feature string) bool { + return fg.licensedFeatures[feature] +} + +// Usage throughout BZZZ codebase +func (agent *Agent) startAdvancedAIIntegration() error { + if !agent.featureGate.IsEnabled("advanced-ai-integration") { + return fmt.Errorf("advanced AI integration requires Standard tier or higher") + } + // ... proceed with feature ... +} +``` + +#### 2. Node Count Enforcement +```go +func validateNodeCount(config *Config, currentNodes int) error { + maxNodes := config.License.MaxNodes + if maxNodes > 0 && currentNodes > maxNodes { + return fmt.Errorf("cluster has %d nodes but license only allows %d nodes", currentNodes, maxNodes) + } + return nil +} +``` + +## Implementation Files to Modify + +### Core Configuration Files +- `pkg/config/config.go` - Add LicenseConfig struct +- `api/setup_manager.go` - Add license to SetupConfig, fix save process +- `main.go` - Add license validation to startup sequence + +### New License Client Files +- `pkg/license/client.go` - KACHING API client +- `pkg/license/heartbeat.go` - Background heartbeat worker +- `pkg/license/features.go` - Feature gate implementation +- `pkg/license/validation.go` - Runtime license validation + +### UI Integration +- Update `install/config-ui/app/setup/components/LicenseValidation.tsx` to call KACHING +- Ensure license data is properly saved in setup flow + +## Configuration Updates Required + +### Environment Variables +```bash +# License server configuration +LICENSE_SERVER_URL=https://kaching.chorus.services +LICENSE_KEY=BZZZ-2025-ABC123-XYZ +CLUSTER_ID=bzzz-cluster-uuid-hostname + +# Offline mode configuration +LICENSE_OFFLINE_GRACE_HOURS=24 +LICENSE_HEARTBEAT_INTERVAL_MINUTES=15 +``` + +### Configuration File Format +```yaml +# .bzzz/config.yaml +license: + server_url: "https://kaching.chorus.services" + license_key: "BZZZ-2025-ABC123-XYZ" + cluster_id: "bzzz-cluster-abc123-walnut" + email: "customer@example.com" + organization_name: "Example Corp" + license_type: "standard" + max_nodes: 10 + features: + - "basic-coordination" + - "task-distribution" + - "advanced-ai-integration" + expires_at: "2025-12-31T23:59:59Z" + validated_at: "2025-09-01T10:30:00Z" + terms_accepted_at: "2025-09-01T10:29:45Z" +``` + +## Testing Strategy + +### Unit Tests Required +- License configuration validation +- Feature gate functionality +- Heartbeat worker logic +- Error handling scenarios + +### Integration Tests Required +- End-to-end setup flow with real KACHING server +- License activation/heartbeat/deactivation cycle +- License suspension handling +- Offline grace period behavior +- Node count enforcement + +### Security Tests +- License tampering detection +- Token validation and expiry +- Cluster ID spoofing protection +- Network failure graceful degradation + +## Success Criteria + +### Phase 2A Success +- [ ] License data properly saved during setup (no longer discarded) +- [ ] Runtime configuration includes complete license information +- [ ] Setup process generates and persists cluster ID + +### Phase 2B Success +- [ ] Mock validation completely removed +- [ ] Real license validation against KACHING server +- [ ] License activation works end-to-end with cluster binding + +### Phase 2C Success +- [ ] BZZZ refuses to start without valid license +- [ ] Heartbeat worker maintains license token +- [ ] License suspension stops BZZZ operations immediately +- [ ] Clean deactivation on shutdown + +### Phase 2D Success +- [ ] Features properly gated based on license tier +- [ ] Node count enforcement prevents over-provisioning +- [ ] Clear error messages for license violations + +### Overall Success +- [ ] **Zero unlicensed usage possible** - system fails closed +- [ ] License sharing across clusters prevented +- [ ] Real-time license enforcement (suspend works immediately) +- [ ] Comprehensive audit trail of license usage + +## Security Considerations + +1. **License Key Protection**: Store license keys securely, never log them +2. **Token Security**: JWT tokens stored in memory only, never persisted +3. **Cluster ID Integrity**: Generate cryptographically secure cluster IDs +4. **Audit Logging**: All license operations logged for compliance +5. **Fail-Closed Design**: System stops on license violations rather than degrading + +## Dependencies + +- **KACHING Phase 1 Complete**: Requires functioning license server +- **Database Migration**: May require config schema updates for existing deployments +- **Documentation Updates**: Update setup guides and admin documentation + +## Deployment Strategy + +1. **Backward Compatibility**: Existing BZZZ instances must upgrade gracefully +2. **Migration Path**: Convert existing configs to include license requirements +3. **Rollback Plan**: Ability to temporarily disable license enforcement if needed +4. **Monitoring**: Comprehensive metrics for license validation success/failure rates + +This plan transforms BZZZ from having zero license enforcement to comprehensive revenue protection integrated with KACHING license authority. diff --git a/api/setup_manager.go b/api/setup_manager.go index fbc48e7f..73910181 100644 --- a/api/setup_manager.go +++ b/api/setup_manager.go @@ -2080,16 +2080,47 @@ ai: } // GenerateConfigForMachineSimple generates a simple BZZZ configuration that matches the working config structure +// REVENUE CRITICAL: This method now properly processes license data to enable revenue protection func (s *SetupManager) GenerateConfigForMachineSimple(machineIP string, config interface{}) (string, error) { - // Note: Configuration extraction not needed for minimal template - _ = config // Avoid unused parameter warning + // CRITICAL FIX: Extract license data from setup configuration - this was being ignored! + // This fix enables revenue protection by ensuring license data is saved in configuration + configMap, ok := config.(map[string]interface{}) + if !ok { + return "", fmt.Errorf("invalid configuration format: expected map[string]interface{}, got %T", config) + } + // Use machine IP to determine hostname (simplified) hostname := strings.ReplaceAll(machineIP, ".", "-") - // Note: Using minimal config template - ports and security can be configured later + // REVENUE CRITICAL: Extract license data from setup configuration + // This ensures license data collected during setup is actually saved in the configuration + var licenseData map[string]interface{} + if license, exists := configMap["license"]; exists { + if licenseMap, ok := license.(map[string]interface{}); ok { + licenseData = licenseMap + } + } - // Generate YAML configuration that matches the Go struct requirements (minimal valid config) - configYAML := fmt.Sprintf(`# BZZZ Configuration for %s + // Validate license data exists - FAIL CLOSED DESIGN + if licenseData == nil { + return "", fmt.Errorf("REVENUE PROTECTION: License data missing from setup configuration - BZZZ cannot be deployed without valid licensing") + } + + // Extract required license fields with validation + email, _ := licenseData["email"].(string) + licenseKey, _ := licenseData["licenseKey"].(string) + orgName, _ := licenseData["organizationName"].(string) + + if email == "" || licenseKey == "" { + return "", fmt.Errorf("REVENUE PROTECTION: Email and license key are required - cannot deploy BZZZ without valid licensing") + } + + // Generate unique cluster ID for license binding (prevents license sharing across clusters) + clusterID := fmt.Sprintf("cluster-%s-%d", hostname, time.Now().Unix()) + + // Generate YAML configuration with FULL license integration for revenue protection + configYAML := fmt.Sprintf(`# BZZZ Configuration for %s - REVENUE PROTECTED +# Generated at %s with license validation whoosh_api: base_url: "https://whoosh.home.deepblack.cloud" api_key: "" @@ -2226,7 +2257,24 @@ ai: api_key: "" endpoint: "https://api.openai.com/v1" timeout: 30s -`, hostname, hostname) + +# REVENUE CRITICAL: License configuration enables revenue protection +license: + email: "%s" + license_key: "%s" + organization_name: "%s" + cluster_id: "%s" + cluster_name: "%s-cluster" + kaching_url: "https://kaching.chorus.services" + heartbeat_minutes: 60 + grace_period_hours: 24 + last_validated: "%s" + validation_token: "" + license_type: "" + max_nodes: 0 + expires_at: "0001-01-01T00:00:00Z" + is_active: true +`, hostname, time.Now().Format(time.RFC3339), email, licenseKey, orgName, clusterID, hostname, time.Now().Format(time.RFC3339)) return configYAML, nil } diff --git a/install/config-ui/app/setup/components/LicenseValidation.tsx b/install/config-ui/app/setup/components/LicenseValidation.tsx index 7682ad68..f74c4df4 100644 --- a/install/config-ui/app/setup/components/LicenseValidation.tsx +++ b/install/config-ui/app/setup/components/LicenseValidation.tsx @@ -189,7 +189,8 @@ export default function LicenseValidation({ />

- Your unique CHORUS:agents license key (found in your purchase confirmation email) + Your unique CHORUS:agents license key (found in your purchase confirmation email). + Validation is powered by KACHING license authority.

diff --git a/main.go b/main.go index 3bf37aae..9da83bb4 100644 --- a/main.go +++ b/main.go @@ -170,6 +170,17 @@ func main() { fmt.Println("✅ Configuration loaded and validated successfully") + // REVENUE CRITICAL: Runtime license validation before P2P initialization + // This ensures BZZZ cannot start without valid licensing from KACHING + fmt.Println("🔐 Validating runtime license with KACHING license authority...") + if err := validateRuntimeLicense(cfg); err != nil { + fmt.Printf("❌ REVENUE PROTECTION: License validation failed: %v\n", err) + fmt.Println("💰 BZZZ requires a valid license to operate - visit https://chorus.services/bzzz") + fmt.Println("🔧 Run setup mode to configure licensing: bzzz --setup") + return + } + fmt.Println("✅ License validation successful - BZZZ authorized to run") + // Initialize P2P node node, err := p2p.NewNode(ctx) if err != nil { @@ -1581,14 +1592,45 @@ func handleLicenseValidation(sm *api.SetupManager) http.HandlerFunc { return } - // Mock license validation logic - // In production, this would call a license server API - validLicenseKey := "BZZZ-2025-DEMO-EVAL-001" + // REVENUE CRITICAL: Replace mock validation with real KACHING license authority integration + // This enables proper license enforcement and revenue protection + kachingURL := "https://kaching.chorus.services/v1/license/activate" - if licenseRequest.LicenseKey != validLicenseKey { + // Generate unique cluster ID for license binding + clusterID := fmt.Sprintf("cluster-%s-%d", "setup", time.Now().Unix()) + + // Prepare KACHING activation request + kachingRequest := map[string]interface{}{ + "email": licenseRequest.Email, + "license_key": licenseRequest.LicenseKey, + "cluster_id": clusterID, + "product": "BZZZ", + "version": "1.0.0", + } + + if licenseRequest.OrganizationName != "" { + kachingRequest["organization"] = licenseRequest.OrganizationName + } + + // Call KACHING license authority for validation + isValid, kachingResponse, err := callKachingLicenseValidation(kachingURL, kachingRequest) + if err != nil { + // FAIL-CLOSED DESIGN: If KACHING is unreachable, deny license validation response := map[string]interface{}{ "valid": false, - "message": "Invalid license key. Please check your license key and try again.", + "message": fmt.Sprintf("License validation failed: %v", err), + "timestamp": time.Now().Unix(), + } + w.WriteHeader(http.StatusServiceUnavailable) + json.NewEncoder(w).Encode(response) + return + } + + if !isValid { + // License validation failed - return KACHING's response + response := map[string]interface{}{ + "valid": false, + "message": "License validation failed - " + fmt.Sprintf("%v", kachingResponse["message"]), "timestamp": time.Now().Unix(), } w.WriteHeader(http.StatusUnauthorized) @@ -1596,23 +1638,12 @@ func handleLicenseValidation(sm *api.SetupManager) http.HandlerFunc { return } - // License is valid - return success with details + // License is valid - return success with KACHING details response := map[string]interface{}{ "valid": true, - "message": "License validated successfully", + "message": "License validated successfully with KACHING license authority", "timestamp": time.Now().Unix(), - "details": map[string]interface{}{ - "licenseType": "Evaluation", - "maxNodes": "5", - "expiresAt": "2025-12-31", - "features": []string{ - "Distributed Task Coordination", - "AI Model Integration", - "Repository Management", - "Cluster Formation", - "Security Features", - }, - }, + "details": kachingResponse["details"], } json.NewEncoder(w).Encode(response) @@ -1851,3 +1882,136 @@ func handleDeployService(sm *api.SetupManager) http.HandlerFunc { json.NewEncoder(w).Encode(result) } } + +// validateRuntimeLicense validates the license configuration with KACHING license authority +// REVENUE CRITICAL: This function prevents BZZZ from starting without valid licensing +func validateRuntimeLicense(cfg *config.Config) error { + license := cfg.License + + // Check if license is configured + if license.Email == "" || license.LicenseKey == "" || license.ClusterID == "" { + return fmt.Errorf("license configuration incomplete - missing email, license key, or cluster ID") + } + + // Check if license was previously validated and is within grace period + if license.IsActive && license.LastValidated.After(time.Time{}) { + gracePeriod := time.Duration(license.GracePeriodHours) * time.Hour + if time.Since(license.LastValidated) < gracePeriod { + fmt.Printf("✅ Using cached license validation (valid for %v more)\n", + gracePeriod - time.Since(license.LastValidated)) + return nil + } + } + + // License needs fresh validation with KACHING + fmt.Println("🌐 Contacting KACHING license authority for fresh validation...") + + // Prepare KACHING heartbeat request + kachingRequest := map[string]interface{}{ + "email": license.Email, + "license_key": license.LicenseKey, + "cluster_id": license.ClusterID, + "product": "BZZZ", + "version": version.FullVersion(), + "heartbeat": true, // Indicates this is a runtime validation + } + + if license.OrganizationName != "" { + kachingRequest["organization"] = license.OrganizationName + } + + // Call KACHING for license validation + isValid, kachingResponse, err := callKachingLicenseValidation(license.KachingURL+"/v1/license/heartbeat", kachingRequest) + if err != nil { + // FAIL-CLOSED DESIGN: Check if we're within grace period for offline operation + if license.IsActive && license.LastValidated.After(time.Time{}) { + gracePeriod := time.Duration(license.GracePeriodHours) * time.Hour + if time.Since(license.LastValidated) < gracePeriod { + fmt.Printf("⚠️ KACHING unreachable but within grace period (%v remaining)\n", + gracePeriod - time.Since(license.LastValidated)) + return nil // Allow operation within grace period + } + } + return fmt.Errorf("license authority unreachable and grace period expired: %w", err) + } + + if !isValid { + // License validation failed + return fmt.Errorf("license validation failed: %v", kachingResponse["message"]) + } + + // Update license status in memory (in production, this would be persisted) + license.IsActive = true + license.LastValidated = time.Now() + if details, ok := kachingResponse["details"].(map[string]interface{}); ok { + if licenseType, exists := details["license_type"]; exists { + if lt, ok := licenseType.(string); ok { + license.LicenseType = lt + } + } + if expiresAt, exists := details["expires_at"]; exists { + if expStr, ok := expiresAt.(string); ok { + if exp, err := time.Parse(time.RFC3339, expStr); err == nil { + license.ExpiresAt = exp + } + } + } + if maxNodes, exists := details["max_nodes"]; exists { + if mn, ok := maxNodes.(float64); ok { + license.MaxNodes = int(mn) + } + } + } + + fmt.Printf("✅ License validated: %s (expires: %s, max nodes: %d)\n", + license.LicenseType, license.ExpiresAt.Format("2006-01-02"), license.MaxNodes) + + return nil +} + +// callKachingLicenseValidation calls the KACHING license authority for license validation +// REVENUE CRITICAL: This function enables real-time license validation and revenue protection +func callKachingLicenseValidation(kachingURL string, request map[string]interface{}) (bool, map[string]interface{}, error) { + // Marshal request to JSON + requestBody, err := json.Marshal(request) + if err != nil { + return false, nil, fmt.Errorf("failed to marshal KACHING request: %w", err) + } + + // Create HTTP client with timeout (fail-closed design) + client := &http.Client{ + Timeout: 30 * time.Second, // 30-second timeout for license validation + } + + // Create HTTP request to KACHING license authority + req, err := http.NewRequest("POST", kachingURL, strings.NewReader(string(requestBody))) + if err != nil { + return false, nil, fmt.Errorf("failed to create KACHING request: %w", err) + } + + // Set headers for KACHING API + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "BZZZ-License-Client/1.0") + + // Execute request to KACHING license authority + resp, err := client.Do(req) + if err != nil { + return false, nil, fmt.Errorf("failed to contact KACHING license authority: %w", err) + } + defer resp.Body.Close() + + // Parse KACHING response + var kachingResponse map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&kachingResponse); err != nil { + return false, nil, fmt.Errorf("failed to parse KACHING response: %w", err) + } + + // Check if license validation was successful + if resp.StatusCode == http.StatusOK { + // License is valid - return success with details + return true, kachingResponse, nil + } else { + // License validation failed - return KACHING's error response + return false, kachingResponse, nil + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index cff34c63..9272eca9 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -11,6 +11,36 @@ import ( "gopkg.in/yaml.v2" ) +// LicenseConfig holds license verification and runtime enforcement configuration +// BUSINESS CRITICAL: This config enables revenue protection by enforcing valid licenses +type LicenseConfig struct { + // Core license identification - REQUIRED for all BZZZ operations + Email string `yaml:"email" json:"email"` // Licensed user email + LicenseKey string `yaml:"license_key" json:"license_key"` // Unique license key + + // Organization binding (optional but recommended for enterprise) + OrganizationName string `yaml:"organization_name,omitempty" json:"organization_name,omitempty"` + + // Cluster identity and binding - prevents license sharing across clusters + ClusterID string `yaml:"cluster_id" json:"cluster_id"` // Unique cluster identifier + ClusterName string `yaml:"cluster_name,omitempty" json:"cluster_name,omitempty"` // Human-readable cluster name + + // KACHING license authority integration + KachingURL string `yaml:"kaching_url" json:"kaching_url"` // KACHING server URL + HeartbeatMinutes int `yaml:"heartbeat_minutes" json:"heartbeat_minutes"` // License heartbeat interval + GracePeriodHours int `yaml:"grace_period_hours" json:"grace_period_hours"` // Offline grace period + + // Runtime state tracking + LastValidated time.Time `yaml:"last_validated,omitempty" json:"last_validated,omitempty"` + ValidationToken string `yaml:"validation_token,omitempty" json:"validation_token,omitempty"` // Current auth token + + // License details (populated by KACHING validation) + LicenseType string `yaml:"license_type,omitempty" json:"license_type,omitempty"` // e.g., "standard", "enterprise" + MaxNodes int `yaml:"max_nodes,omitempty" json:"max_nodes,omitempty"` // Maximum allowed nodes + ExpiresAt time.Time `yaml:"expires_at,omitempty" json:"expires_at,omitempty"` // License expiration + IsActive bool `yaml:"is_active" json:"is_active"` // Current license status +} + // SecurityConfig holds cluster security and election configuration type SecurityConfig struct { // Admin key sharing @@ -55,6 +85,7 @@ type Config struct { UCXL UCXLConfig `yaml:"ucxl"` // UCXL protocol settings Security SecurityConfig `yaml:"security"` // Cluster security and elections AI AIConfig `yaml:"ai"` // AI/LLM integration settings + License LicenseConfig `yaml:"license"` // License verification and enforcement - REVENUE CRITICAL } // WHOOSHAPIConfig holds WHOOSH system integration settings @@ -380,6 +411,16 @@ func getDefaultConfig() *Config { Timeout: 30 * time.Second, }, }, + // REVENUE CRITICAL: License configuration defaults + // These settings ensure BZZZ can only run with valid licensing from KACHING + License: LicenseConfig{ + KachingURL: "https://kaching.chorus.services", // KACHING license authority server + HeartbeatMinutes: 60, // Check license validity every hour + GracePeriodHours: 24, // Allow 24 hours offline before enforcement + IsActive: false, // Default to inactive - MUST be validated during setup + // Note: Email, LicenseKey, and ClusterID are required and will be set during setup + // Leaving them empty in defaults forces setup process to collect them + }, } } @@ -521,6 +562,56 @@ func validateConfig(config *Config) error { return fmt.Errorf("slurp configuration invalid: %w", err) } + // REVENUE CRITICAL: Validate license configuration + // This prevents BZZZ from running without valid licensing + if err := validateLicenseConfig(config.License); err != nil { + return fmt.Errorf("license configuration invalid: %w", err) + } + + return nil +} + +// validateLicenseConfig ensures license configuration meets revenue protection requirements +// BUSINESS CRITICAL: This function enforces license requirements that protect revenue +func validateLicenseConfig(license LicenseConfig) error { + // Check core license identification fields + if license.Email == "" { + return fmt.Errorf("license email is required - BZZZ cannot run without valid licensing") + } + + if license.LicenseKey == "" { + return fmt.Errorf("license key is required - BZZZ cannot run without valid licensing") + } + + if license.ClusterID == "" { + return fmt.Errorf("cluster ID is required - BZZZ cannot run without cluster binding") + } + + // Validate KACHING integration settings + if license.KachingURL == "" { + return fmt.Errorf("KACHING URL is required for license validation") + } + + // Validate URL format + if err := validateURL(license.KachingURL); err != nil { + return fmt.Errorf("invalid KACHING URL: %w", err) + } + + // Validate heartbeat and grace period settings + if license.HeartbeatMinutes <= 0 { + return fmt.Errorf("heartbeat interval must be positive (recommended: 60 minutes)") + } + + if license.GracePeriodHours <= 0 { + return fmt.Errorf("grace period must be positive (recommended: 24 hours)") + } + + // FAIL-CLOSED DESIGN: License must be explicitly marked as active + // This ensures setup process validates license before allowing operations + if !license.IsActive { + return fmt.Errorf("license is not active - run setup to validate with KACHING license authority") + } + return nil } diff --git a/pkg/version/VERSION b/pkg/version/VERSION index b0f3d96f..9084fa2f 100644 --- a/pkg/version/VERSION +++ b/pkg/version/VERSION @@ -1 +1 @@ -1.0.8 +1.1.0