"""Instance activation endpoint. This is the PUBLIC endpoint that client instances call to validate their license and activate with the Hub. """ import hashlib import secrets from fastapi import APIRouter, HTTPException, status from sqlalchemy import select from app.db import AsyncSessionDep from app.models.base import utc_now from app.models.instance import Instance from app.schemas.instance import ActivationRequest, ActivationResponse router = APIRouter(prefix="/api/v1/instances", tags=["Activation"]) @router.post("/activate", response_model=ActivationResponse) async def activate_instance( request: ActivationRequest, db: AsyncSessionDep, ) -> ActivationResponse: """ Activate an instance with its license key. Called by local_bootstrap.sh before running migrations. Returns: - 200 + ActivationResponse on success - 400 with error details on failure Privacy guarantee: - Only receives license_key and instance_id - Never receives sensitive client data """ # Find instance by instance_id result = await db.execute( select(Instance).where(Instance.instance_id == request.instance_id) ) instance = result.scalar_one_or_none() if instance is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={"error": "Instance not found", "code": "instance_not_found"}, ) # Validate license key using constant-time comparison provided_hash = hashlib.sha256(request.license_key.encode()).hexdigest() if not secrets.compare_digest(provided_hash, instance.license_key_hash): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={"error": "Invalid license key", "code": "invalid_license"}, ) # Check license status if instance.license_status == "suspended": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={"error": "License suspended", "code": "suspended"}, ) if instance.license_status == "revoked": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={"error": "License revoked", "code": "revoked"}, ) # Check expiry now = utc_now() if instance.license_expires_at and instance.license_expires_at < now: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={"error": "License expired", "code": "expired"}, ) # Update activation state if instance.activated_at is None: instance.activated_at = now instance.last_activation_at = now instance.activation_count += 1 instance.status = "active" # Generate hub_api_key if not already set hub_api_key: str if instance.hub_api_key_hash: # Key was pre-generated, client should use existing key hub_api_key = "USE_EXISTING" else: # Generate new hub_api_key hub_api_key = f"hk_{secrets.token_hex(24)}" instance.hub_api_key_hash = hashlib.sha256(hub_api_key.encode()).hexdigest() await db.commit() return ActivationResponse( status="ok", instance_id=instance.instance_id, hub_api_key=hub_api_key, config={ "telemetry_enabled": True, "telemetry_interval_seconds": 60, }, )