Coverage for integrations / agent_engine / build_distribution.py: 43.3%
150 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""
2Linux Build Distribution — Licensed commercial builds gated by payment.
4Purchase → signed license → time-limited download URL.
5All compute should fall under one basket — treading carefully in a cautious market.
7Service Pattern: static methods, db: Session, db.flush() not db.commit().
8Blueprint Pattern: Blueprint('build_distribution', __name__).
9"""
10import hashlib
11import hmac
12import logging
13import secrets
14import time
15from datetime import datetime, timedelta
16from typing import Dict, List, Optional
18from flask import Blueprint, g, jsonify, request
19from sqlalchemy.orm import Session
21logger = logging.getLogger('hevolve_social')
23# Build type → (max_downloads, validity_days)
24BUILD_CONFIG = {
25 'community': (3, 365), # 3 downloads, 1 year
26 'pro': (10, 730), # 10 downloads, 2 years
27 'enterprise': (100, 36500), # 100 downloads, ~perpetual (100 years)
28}
30# HMAC secret for signed download URLs (rotated at boot)
31_DOWNLOAD_SECRET = secrets.token_bytes(32)
34class BuildDistributionService:
35 """Static service for licensed Linux build distribution."""
37 @staticmethod
38 def create_build_license(db: Session, user_id: str,
39 build_type: str = 'community',
40 platform: str = 'linux_x64',
41 payment_reference: str = None) -> Dict:
42 """Create a signed build license after payment verification."""
43 from integrations.social.models import BuildLicense
45 if build_type not in BUILD_CONFIG:
46 return {'error': f'Invalid build_type: {build_type}. Valid: {list(BUILD_CONFIG.keys())}'}
48 if platform not in ('linux_x64', 'linux_arm64'):
49 return {'error': f'Invalid platform: {platform}. Valid: linux_x64, linux_arm64'}
51 max_downloads, validity_days = BUILD_CONFIG[build_type]
52 license_key = secrets.token_urlsafe(32)
54 # Sign with node key if available
55 signed_by = None
56 sig_hex = None
57 try:
58 from security.node_integrity import get_public_key_hex, sign_message
59 signed_by = get_public_key_hex()
60 sig_hex = sign_message(license_key.encode('utf-8')).hex()
61 except Exception:
62 pass
64 bl = BuildLicense(
65 user_id=user_id,
66 license_key=license_key,
67 build_type=build_type,
68 platform=platform,
69 payment_reference=payment_reference,
70 max_downloads=max_downloads,
71 signed_by=signed_by,
72 signature_hex=sig_hex,
73 expires_at=datetime.utcnow() + timedelta(days=validity_days),
74 )
75 db.add(bl)
76 db.flush()
77 return bl.to_dict()
79 @staticmethod
80 def verify_build_license(db: Session, license_key: str) -> Dict:
81 """Verify a license key. Returns {valid: bool, license: dict, reason: str}."""
82 from integrations.social.models import BuildLicense
84 bl = db.query(BuildLicense).filter_by(license_key=license_key).first()
85 if not bl:
86 return {'valid': False, 'license': None, 'reason': 'License not found'}
87 if not bl.is_active:
88 return {'valid': False, 'license': bl.to_dict(), 'reason': 'License deactivated'}
89 if bl.expires_at and bl.expires_at < datetime.utcnow():
90 return {'valid': False, 'license': bl.to_dict(), 'reason': 'License expired'}
91 if bl.download_count >= bl.max_downloads:
92 return {'valid': False, 'license': bl.to_dict(), 'reason': 'Download limit reached'}
94 return {'valid': True, 'license': bl.to_dict(), 'reason': 'ok'}
96 @staticmethod
97 def record_download(db: Session, license_id: str) -> Dict:
98 """Record a download. Returns error if limit reached."""
99 from integrations.social.models import BuildLicense
101 bl = db.query(BuildLicense).filter_by(id=license_id).first()
102 if not bl:
103 return {'error': 'License not found'}
104 if bl.download_count >= bl.max_downloads:
105 return {'error': 'Download limit reached'}
106 if not bl.is_active:
107 return {'error': 'License deactivated'}
109 bl.download_count = (bl.download_count or 0) + 1
110 db.flush()
111 return bl.to_dict()
113 @staticmethod
114 def get_download_url(db: Session, license_id: str) -> Dict:
115 """Generate a time-limited signed download URL (1 hour)."""
116 from integrations.social.models import BuildLicense
118 bl = db.query(BuildLicense).filter_by(id=license_id).first()
119 if not bl:
120 return {'error': 'License not found'}
122 # Verify validity
123 if not bl.is_active:
124 return {'error': 'License deactivated'}
125 if bl.expires_at and bl.expires_at < datetime.utcnow():
126 return {'error': 'License expired'}
127 if bl.download_count >= bl.max_downloads:
128 return {'error': 'Download limit reached'}
130 # Increment download count
131 bl.download_count = (bl.download_count or 0) + 1
132 db.flush()
134 # Generate signed URL (HMAC-based, 1 hour validity)
135 expires = int(time.time()) + 3600
136 payload = f"{license_id}:{bl.platform}:{bl.build_type}:{expires}"
137 signature = hmac.new(
138 _DOWNLOAD_SECRET, payload.encode(), hashlib.sha256
139 ).hexdigest()[:32]
141 url = (f"/api/v1/builds/files/{bl.platform}/{bl.build_type}"
142 f"?license={license_id}&expires={expires}&sig={signature}")
144 return {
145 'url': url,
146 'expires_at': datetime.utcfromtimestamp(expires).isoformat(),
147 'platform': bl.platform,
148 'build_type': bl.build_type,
149 'downloads_remaining': bl.max_downloads - bl.download_count,
150 }
152 @staticmethod
153 def list_licenses(db: Session, user_id: str) -> List[Dict]:
154 """List all licenses for a user."""
155 from integrations.social.models import BuildLicense
156 licenses = db.query(BuildLicense).filter_by(
157 user_id=user_id).order_by(BuildLicense.created_at.desc()).all()
158 return [bl.to_dict() for bl in licenses]
161# ═══════════════════════════════════════════════════════════════
162# Blueprint
163# ═══════════════════════════════════════════════════════════════
165build_distribution_bp = Blueprint('build_distribution', __name__)
168@build_distribution_bp.route('/api/v1/builds/purchase', methods=['POST'])
169def purchase_build():
170 """Purchase a build license (requires JWT auth)."""
171 auth_header = request.headers.get('Authorization', '')
172 if not auth_header.startswith('Bearer '):
173 return jsonify({'success': False, 'error': 'Authorization required'}), 401
175 from integrations.social.auth import _get_user_from_token
176 token = auth_header[7:]
177 user, db = _get_user_from_token(token)
178 if not user:
179 if db:
180 db.close()
181 return jsonify({'success': False, 'error': 'Invalid token'}), 401
183 try:
184 data = request.get_json() or {}
185 result = BuildDistributionService.create_build_license(
186 db, str(user.id),
187 build_type=data.get('build_type', 'community'),
188 platform=data.get('platform', 'linux_x64'),
189 payment_reference=data.get('payment_reference'),
190 )
191 if 'error' in result:
192 return jsonify({'success': False, 'error': result['error']}), 400
193 db.commit()
194 return jsonify({'success': True, 'license': result}), 201
195 finally:
196 db.close()
199@build_distribution_bp.route('/api/v1/builds/download/<license_id>', methods=['GET'])
200def download_build(license_id):
201 """Get signed download URL for a license (requires JWT auth)."""
202 auth_header = request.headers.get('Authorization', '')
203 if not auth_header.startswith('Bearer '):
204 return jsonify({'success': False, 'error': 'Authorization required'}), 401
206 from integrations.social.auth import _get_user_from_token
207 token = auth_header[7:]
208 user, db = _get_user_from_token(token)
209 if not user:
210 if db:
211 db.close()
212 return jsonify({'success': False, 'error': 'Invalid token'}), 401
214 try:
215 result = BuildDistributionService.get_download_url(db, license_id)
216 if 'error' in result:
217 return jsonify({'success': False, 'error': result['error']}), 400
218 db.commit()
219 return jsonify({'success': True, 'download': result})
220 finally:
221 db.close()
224@build_distribution_bp.route('/api/v1/builds/licenses', methods=['GET'])
225def list_build_licenses():
226 """List user's build licenses (requires JWT auth)."""
227 auth_header = request.headers.get('Authorization', '')
228 if not auth_header.startswith('Bearer '):
229 return jsonify({'success': False, 'error': 'Authorization required'}), 401
231 from integrations.social.auth import _get_user_from_token
232 token = auth_header[7:]
233 user, db = _get_user_from_token(token)
234 if not user:
235 if db:
236 db.close()
237 return jsonify({'success': False, 'error': 'Invalid token'}), 401
239 try:
240 licenses = BuildDistributionService.list_licenses(db, str(user.id))
241 return jsonify({'success': True, 'licenses': licenses})
242 finally:
243 db.close()
246@build_distribution_bp.route('/api/v1/builds/verify', methods=['POST'])
247def verify_license():
248 """Public license verification endpoint."""
249 from integrations.social.models import get_db
251 data = request.get_json() or {}
252 license_key = data.get('license_key', '')
253 if not license_key:
254 return jsonify({'success': False, 'error': 'license_key required'}), 400
256 db = get_db()
257 try:
258 result = BuildDistributionService.verify_build_license(db, license_key)
259 return jsonify({'success': True, 'verification': result})
260 finally:
261 db.close()