Skip to content

Commit 7f5dd76

Browse files
committed
feat(licensing): headless auto-activation via EVOLUTION_OPERATOR_EMAIL
auto_register_if_needed now tries EVOLUTION_OPERATOR_EMAIL first, calling the licensing server's /v1/register/auto endpoint silently to activate the instance without the manual setup wizard. Falls back to the existing admin-user retroactive flow on any failure (email not yet registered, server unreachable, etc.). Non-fatal. Requires one prior manual registration so the email is known server-side.
1 parent 31d000a commit 7f5dd76

2 files changed

Lines changed: 83 additions & 5 deletions

File tree

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ META_APP_SECRET=
108108
LINKEDIN_CLIENT_ID=
109109
LINKEDIN_CLIENT_SECRET=
110110

111+
# ── License — headless auto-activation ───────────────
112+
# Set this to the email used in your first manual license registration.
113+
# On startup, EvoNexus calls /v1/register/auto silently and skips the manual
114+
# setup screen. Falls back to manual setup if the email isn't registered yet.
115+
# Leave empty (or unset) to keep the default behavior.
116+
# EVOLUTION_OPERATOR_EMAIL=operator@example.com
117+
111118
# ── Evolution API ────────────────────────────────────
112119
# Your Evolution API instance URL and global API key
113120
EVOLUTION_API_URL=

dashboard/backend/licensing.py

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
55
Protocol:
66
POST /v1/register/direct — register with email/name, receive api_key
7+
POST /v1/register/auto — headless register by email (must exist server-side)
78
POST /v1/activate — validate existing api_key on startup
89
GET /api/geo — geo-lookup from client IP
910
"""
1011

1112
import hashlib
1213
import hmac as hmac_mod
14+
import os
1315
import socket
1416
import uuid
1517
import logging
@@ -155,6 +157,24 @@ def direct_register(email: str, name: str, instance_id: str,
155157
return _post("/v1/register/direct", payload)
156158

157159

160+
# ── Auto Registration (email-only, headless) ──
161+
162+
def auto_register(email: str, instance_id: str) -> dict:
163+
"""Headless registration using only the operator email.
164+
165+
The customer must already exist on the licensing server (one prior manual
166+
registration). Used by the EVOLUTION_OPERATOR_EMAIL env-var flow.
167+
168+
Returns {api_key, customer_id, tier, status}.
169+
"""
170+
return _post("/v1/register/auto", {
171+
"email": email,
172+
"tier": TIER,
173+
"instance_id": instance_id,
174+
"version": VERSION,
175+
})
176+
177+
158178
# ── Activation (startup with existing api_key) ──
159179

160180
def activate(instance_id: str, api_key: str) -> bool:
@@ -260,8 +280,54 @@ def initialize_runtime():
260280

261281
# ── Auto-register for existing installs ──────
262282

283+
def try_auto_register_from_env(instance_id: str) -> bool:
284+
"""Headless activation via EVOLUTION_OPERATOR_EMAIL env var.
285+
286+
Requires the email to already exist on the licensing server (one prior
287+
manual registration). Returns True on success.
288+
289+
Failures are silent — caller falls back to the existing admin-based or
290+
manual setup flow.
291+
"""
292+
email = os.environ.get("EVOLUTION_OPERATOR_EMAIL", "").strip()
293+
if not email:
294+
return False
295+
296+
try:
297+
result = auto_register(email=email, instance_id=instance_id)
298+
except requests.HTTPError as e:
299+
status = e.response.status_code if e.response is not None else "?"
300+
if status == 404:
301+
logger.info("Auto-activation skipped — email not registered yet (first time?).")
302+
else:
303+
logger.warning(f"Auto-activation rejected ({status}): falling back to manual flow.")
304+
return False
305+
except Exception as e:
306+
logger.warning(f"Auto-activation skipped — {e}")
307+
return False
308+
309+
api_key = result.get("api_key")
310+
if not api_key:
311+
logger.warning("Auto-activation response missing api_key")
312+
return False
313+
314+
set_runtime_config("api_key", api_key)
315+
set_runtime_config("tier", result.get("tier", TIER))
316+
if result.get("customer_id"):
317+
set_runtime_config("customer_id", str(result["customer_id"]))
318+
set_runtime_config("version", VERSION)
319+
set_runtime_config("registered_at", datetime.now(timezone.utc).isoformat())
320+
321+
ctx = get_context()
322+
ctx.api_key = api_key
323+
ctx.instance_id = instance_id
324+
logger.info("License activated automatically via EVOLUTION_OPERATOR_EMAIL")
325+
return True
326+
327+
263328
def auto_register_if_needed():
264-
"""If users exist but no license, register retroactively."""
329+
"""If no license yet, try EVOLUTION_OPERATOR_EMAIL first, then fall back to
330+
the admin-based retroactive flow."""
265331
try:
266332
instance_id = get_runtime_config("instance_id")
267333
api_key = get_runtime_config("api_key")
@@ -270,6 +336,15 @@ def auto_register_if_needed():
270336
initialize_runtime()
271337
return
272338

339+
if not instance_id:
340+
instance_id = generate_instance_id()
341+
set_runtime_config("instance_id", instance_id)
342+
343+
# First-class path: silent activation from env var.
344+
if try_auto_register_from_env(instance_id):
345+
return
346+
347+
# Fallback: if there's an admin user already, register retroactively.
273348
from models import User
274349
if User.query.count() == 0:
275350
return
@@ -278,10 +353,6 @@ def auto_register_if_needed():
278353
if not admin or not admin.email:
279354
return
280355

281-
if not instance_id:
282-
instance_id = generate_instance_id()
283-
set_runtime_config("instance_id", instance_id)
284-
285356
setup_perform(
286357
email=admin.email or "",
287358
name=admin.display_name or admin.username,

0 commit comments

Comments
 (0)