CRITICAL REVENUE PROTECTION: Fix $0 recurring revenue by enforcing BZZZ licensing This commit implements Phase 2A license enforcement, transforming BZZZ from having zero license validation to comprehensive revenue protection integrated with KACHING license authority. KEY BUSINESS IMPACT: • PREVENTS unlimited free usage - BZZZ now requires valid licensing to operate • ENABLES real-time license control - licenses can be suspended immediately via KACHING • PROTECTS against license sharing - unique cluster IDs bind licenses to specific deployments • ESTABLISHES recurring revenue foundation - licensing is now technically enforced CRITICAL FIXES: 1. Setup Manager Revenue Protection (api/setup_manager.go): - FIXED: License data was being completely discarded during setup (line 2085) - NOW: License data is extracted, validated, and saved to configuration - IMPACT: Closes $0 recurring revenue loophole - licenses are now required for deployment 2. Configuration System Integration (pkg/config/config.go): - ADDED: Complete LicenseConfig struct with KACHING integration fields - ADDED: License validation in config validation pipeline - IMPACT: Makes licensing a core requirement, not optional 3. Runtime License Enforcement (main.go): - ADDED: License validation before P2P node initialization (line 175) - ADDED: Fail-closed design - BZZZ exits if license validation fails - ADDED: Grace period support for offline operations - IMPACT: Prevents unlicensed BZZZ instances from starting 4. KACHING License Authority Integration: - REPLACED: Mock license validation (hardcoded BZZZ-2025-DEMO-EVAL-001) - ADDED: Real-time KACHING API integration for license activation - ADDED: Cluster ID generation for license binding - IMPACT: Enables centralized license management and immediate suspension 5. Frontend License Validation Enhancement: - UPDATED: License validation UI to indicate KACHING integration - MAINTAINED: Existing UX while adding revenue protection backend - IMPACT: Users now see real license validation, not mock responses TECHNICAL DETAILS: • Version bump: 1.0.8 → 1.1.0 (significant license enforcement features) • Fail-closed security design: System stops rather than degrading on license issues • Unique cluster ID generation prevents license sharing across deployments • Grace period support (24h default) for offline/network issue scenarios • Comprehensive error handling and user guidance for license issues TESTING REQUIREMENTS: • Test that BZZZ refuses to start without valid license configuration • Verify license data is properly saved during setup (no longer discarded) • Test KACHING integration for license activation and validation • Confirm cluster ID uniqueness and license binding DEPLOYMENT IMPACT: • Existing BZZZ deployments will require license configuration on next restart • Setup process now enforces license validation before deployment • Invalid/missing licenses will prevent BZZZ startup (revenue protection) This implementation establishes the foundation for recurring revenue by making valid licensing technically required for BZZZ operation. 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
303 lines
10 KiB
TypeScript
303 lines
10 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import {
|
|
KeyIcon,
|
|
CheckCircleIcon,
|
|
ExclamationTriangleIcon,
|
|
UserIcon,
|
|
DocumentTextIcon
|
|
} from '@heroicons/react/24/outline'
|
|
|
|
interface LicenseValidationProps {
|
|
systemInfo: any
|
|
configData: any
|
|
onComplete: (data: any) => void
|
|
onBack?: () => void
|
|
isCompleted: boolean
|
|
}
|
|
|
|
interface LicenseData {
|
|
email: string
|
|
licenseKey: string
|
|
organizationName?: string
|
|
acceptedAt?: string
|
|
}
|
|
|
|
export default function LicenseValidation({
|
|
systemInfo,
|
|
configData,
|
|
onComplete,
|
|
onBack,
|
|
isCompleted
|
|
}: LicenseValidationProps) {
|
|
const [licenseData, setLicenseData] = useState<LicenseData>({
|
|
email: configData?.license?.email || '',
|
|
licenseKey: configData?.license?.licenseKey || '',
|
|
organizationName: configData?.license?.organizationName || ''
|
|
})
|
|
|
|
const [validating, setValidating] = useState(false)
|
|
const [validationResult, setValidationResult] = useState<{
|
|
valid: boolean
|
|
message: string
|
|
details?: any
|
|
} | null>(null)
|
|
const [error, setError] = useState('')
|
|
|
|
// Email validation function
|
|
const isValidEmail = (email: string): boolean => {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
return emailRegex.test(email)
|
|
}
|
|
|
|
// Check if form is ready for validation
|
|
const canValidate = licenseData.email &&
|
|
isValidEmail(licenseData.email) &&
|
|
licenseData.licenseKey
|
|
|
|
const validateLicense = async () => {
|
|
if (!licenseData.email || !licenseData.licenseKey) {
|
|
setError('Both email and license key are required')
|
|
return
|
|
}
|
|
|
|
if (!isValidEmail(licenseData.email)) {
|
|
setError('Please enter a valid email address')
|
|
return
|
|
}
|
|
|
|
setValidating(true)
|
|
setError('')
|
|
setValidationResult(null)
|
|
|
|
try {
|
|
const response = await fetch('/api/setup/license/validate', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
email: licenseData.email,
|
|
licenseKey: licenseData.licenseKey,
|
|
organizationName: licenseData.organizationName
|
|
}),
|
|
})
|
|
|
|
const result = await response.json()
|
|
|
|
if (response.ok && result.valid) {
|
|
setValidationResult({
|
|
valid: true,
|
|
message: result.message || 'License validated successfully',
|
|
details: result.details
|
|
})
|
|
} else {
|
|
setValidationResult({
|
|
valid: false,
|
|
message: result.message || 'License validation failed',
|
|
details: result.details
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.error('License validation error:', error)
|
|
setValidationResult({
|
|
valid: false,
|
|
message: 'Failed to validate license. Please check your connection and try again.'
|
|
})
|
|
} finally {
|
|
setValidating(false)
|
|
}
|
|
}
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
if (!licenseData.email || !licenseData.licenseKey) {
|
|
setError('Both email and license key are required')
|
|
return
|
|
}
|
|
|
|
if (!validationResult?.valid) {
|
|
setError('Please validate your license before continuing')
|
|
return
|
|
}
|
|
|
|
setError('')
|
|
onComplete({
|
|
license: {
|
|
...licenseData,
|
|
validatedAt: new Date().toISOString(),
|
|
validationDetails: validationResult.details
|
|
}
|
|
})
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-8">
|
|
|
|
{/* License Information */}
|
|
<div className="card">
|
|
<div className="flex items-center mb-4">
|
|
<KeyIcon className="h-6 w-6 text-bzzz-primary mr-2" />
|
|
<h3 className="text-lg font-medium text-gray-900">License Information</h3>
|
|
{validationResult?.valid && <CheckCircleIcon className="h-5 w-5 text-eucalyptus-600 ml-2" />}
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Email Address
|
|
</label>
|
|
<div className="relative">
|
|
<UserIcon className="h-5 w-5 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" />
|
|
<input
|
|
type="email"
|
|
value={licenseData.email}
|
|
onChange={(e) => setLicenseData(prev => ({ ...prev, email: e.target.value }))}
|
|
placeholder="your-email@company.com"
|
|
className={`w-full pl-10 pr-4 py-3 border rounded-lg focus:ring-bzzz-primary focus:border-bzzz-primary ${
|
|
licenseData.email && !isValidEmail(licenseData.email)
|
|
? 'border-red-300 bg-red-50'
|
|
: 'border-gray-300'
|
|
}`}
|
|
required
|
|
/>
|
|
</div>
|
|
{licenseData.email && !isValidEmail(licenseData.email) ? (
|
|
<p className="text-sm text-red-600 mt-1">Please enter a valid email address</p>
|
|
) : (
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
The email address associated with your CHORUS:agents license
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
License Key
|
|
</label>
|
|
<div className="relative">
|
|
<KeyIcon className="h-5 w-5 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" />
|
|
<input
|
|
type="text"
|
|
value={licenseData.licenseKey}
|
|
onChange={(e) => setLicenseData(prev => ({ ...prev, licenseKey: e.target.value }))}
|
|
placeholder="BZZZ-XXXX-XXXX-XXXX-XXXX"
|
|
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-bzzz-primary focus:border-bzzz-primary font-mono"
|
|
required
|
|
/>
|
|
</div>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
Your unique CHORUS:agents license key (found in your purchase confirmation email).
|
|
Validation is powered by KACHING license authority.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Organization Name (Optional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={licenseData.organizationName}
|
|
onChange={(e) => setLicenseData(prev => ({ ...prev, organizationName: e.target.value }))}
|
|
placeholder="Your Company Name"
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-bzzz-primary focus:border-bzzz-primary"
|
|
/>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
Optional: Organization name for license tracking
|
|
</p>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={validateLicense}
|
|
disabled={validating || !canValidate}
|
|
className={`w-full py-3 px-4 rounded-lg font-medium transition-colors ${
|
|
validating || !canValidate
|
|
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
|
: 'bg-bzzz-primary text-white hover:bg-bzzz-primary-dark'
|
|
}`}
|
|
>
|
|
{validating ? 'Validating License...' : 'Validate License'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Validation Result */}
|
|
{validationResult && (
|
|
<div className={`panel ${validationResult.valid ? 'panel-success' : 'panel-error'}`}>
|
|
<div className="flex items-start">
|
|
<div className="flex-shrink-0">
|
|
{validationResult.valid ? (
|
|
<CheckCircleIcon className="h-6 w-6 text-eucalyptus-600 dark:text-eucalyptus-50" />
|
|
) : (
|
|
<ExclamationTriangleIcon className="h-6 w-6 text-coral-950 dark:text-coral-50" />
|
|
)}
|
|
</div>
|
|
<div className="ml-3">
|
|
<h4 className={`text-sm font-medium panel-title`}>
|
|
{validationResult.valid ? 'License Valid' : 'License Invalid'}
|
|
</h4>
|
|
<p className={`text-sm mt-1 panel-body`}>
|
|
{validationResult.message}
|
|
</p>
|
|
|
|
{validationResult.valid && validationResult.details && (
|
|
<div className="mt-3 text-sm panel-body">
|
|
<p><strong>License Type:</strong> {validationResult.details.licenseType || 'Standard'}</p>
|
|
<p><strong>Max Nodes:</strong> {validationResult.details.maxNodes || 'Unlimited'}</p>
|
|
<p><strong>Expires:</strong> {validationResult.details.expiresAt || 'Never'}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="flex items-center text-red-600 text-sm">
|
|
<ExclamationTriangleIcon className="h-4 w-4 mr-1" />
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Need a License Panel */}
|
|
<div className="rounded-lg p-4 border bg-chorus-warm border-chorus-border-subtle dark:bg-mulberry-900 dark:border-chorus-border-defined">
|
|
<div className="flex items-start">
|
|
<DocumentTextIcon className="h-5 w-5 text-chorus-text-primary mt-0.5 mr-2 opacity-80" />
|
|
<div className="text-sm">
|
|
<h4 className="font-medium text-chorus-text-primary mb-1">Need a License?</h4>
|
|
<p className="text-chorus-text-secondary">
|
|
If you don't have a CHORUS:agents license yet, you can:
|
|
</p>
|
|
<ul className="text-chorus-text-secondary mt-1 space-y-1 ml-4">
|
|
<li>• Visit <a href="https://chorus.services/bzzz" target="_blank" className="underline hover:no-underline text-chorus-text-primary">chorus.services/bzzz</a> to purchase a license</li>
|
|
<li>• Contact our sales team at <a href="mailto:sales@chorus.services" className="underline hover:no-underline text-chorus-text-primary">sales@chorus.services</a></li>
|
|
<li>• Request a trial license for evaluation purposes</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-between pt-6 border-t border-gray-200">
|
|
<div>
|
|
{onBack && (
|
|
<button type="button" onClick={onBack} className="btn-outline">
|
|
Back
|
|
</button>
|
|
)}
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
disabled={!validationResult?.valid}
|
|
className={`${validationResult?.valid ? 'btn-primary' : 'btn-disabled'}`}
|
|
>
|
|
{isCompleted ? 'Continue' : 'Next: System Detection'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)
|
|
}
|