diff --git a/.claude/skills/int-linkedin/scripts/linkedin_client.py b/.claude/skills/int-linkedin/scripts/linkedin_client.py index 48fcfd24..a2aa68fb 100644 --- a/.claude/skills/int-linkedin/scripts/linkedin_client.py +++ b/.claude/skills/int-linkedin/scripts/linkedin_client.py @@ -233,6 +233,41 @@ def all_accounts_summary() -> dict: result = org_followers(acc) elif cmd == "summary": result = all_accounts_summary() + elif cmd == "smoke": + import time as _time + _t0 = _time.time() + _steps = [] + _overall = "PASS" + + # step 1: auth — load accounts + _ts = _time.time() + try: + _accounts = _get_accounts() + _steps.append({"step": "auth", "status": "PASS", "duration_ms": round((_time.time() - _ts) * 1000)}) + except Exception as _e: + _steps.append({"step": "auth", "status": "FAIL", "error": str(_e)[:300], "duration_ms": round((_time.time() - _ts) * 1000)}) + _overall = "FAIL" + _accounts = [] + + # step 2: profile read + _ts = _time.time() + if not _accounts: + _steps.append({"step": "profile_read", "status": "SKIP", "duration_ms": 0}) + else: + try: + _acc = _accounts[0] + _r = profile(_acc) + if "error" in _r: + _steps.append({"step": "profile_read", "status": "FAIL", "error": str(_r["error"])[:300], "duration_ms": round((_time.time() - _ts) * 1000)}) + _overall = "FAIL" + else: + _steps.append({"step": "profile_read", "status": "PASS", "duration_ms": round((_time.time() - _ts) * 1000)}) + except Exception as _e: + _steps.append({"step": "profile_read", "status": "FAIL", "error": str(_e)[:300], "duration_ms": round((_time.time() - _ts) * 1000)}) + _overall = "FAIL" + + print(json.dumps({"overall": _overall, "steps": _steps, "duration_ms": round((_time.time() - _t0) * 1000)}, indent=2)) + sys.exit(0) else: print(f"Unknown command: {cmd}") sys.exit(1) diff --git a/.claude/skills/int-stripe/scripts/stripe_query.py b/.claude/skills/int-stripe/scripts/stripe_query.py index 49ca0168..b0f6ab43 100644 --- a/.claude/skills/int-stripe/scripts/stripe_query.py +++ b/.claude/skills/int-stripe/scripts/stripe_query.py @@ -165,10 +165,60 @@ def cmd_update(args): print(json.dumps(result, indent=2)) +def cmd_smoke(args): + """E2E gate read-only — valida auth + balance. Sempre exit 0 + JSON.""" + import time as _time + + try: + out = {"steps": [], "overall": "PASS"} + t0 = _time.monotonic() + + def _ms(since): + return round((_time.monotonic() - since) * 1000) + + # Step 1: auth (API key presente) + ts = _time.monotonic() + try: + key = os.environ.get("STRIPE_SECRET_KEY") + if not key: + out["steps"].append({"step": "auth", "status": "FAIL", "error": "STRIPE_SECRET_KEY ausente", "duration_ms": _ms(ts)}) + out["overall"] = "FAIL" + print(json.dumps({**out, "duration_ms": _ms(t0)})) + return + out["steps"].append({"step": "auth", "status": "PASS", "duration_ms": _ms(ts)}) + except Exception as exc: + out["steps"].append({"step": "auth", "status": "FAIL", "error": str(exc)[:300], "duration_ms": _ms(ts)}) + out["overall"] = "FAIL" + print(json.dumps({**out, "duration_ms": _ms(t0)})) + return + + # Step 2: retrieve balance (cheap read-only endpoint) + ts = _time.monotonic() + try: + req = urllib.request.Request( + f"{BASE_URL}/balance", + headers={"Authorization": f"Bearer {key}"}, + ) + with urllib.request.urlopen(req, timeout=15) as resp: + data = json.loads(resp.read()) + currency = data.get("available", [{}])[0].get("currency", "?") if data.get("available") else "?" + out["steps"].append({"step": "balance", "status": "PASS", "currency": currency, "duration_ms": _ms(ts)}) + except Exception as exc: + out["steps"].append({"step": "balance", "status": "FAIL", "error": str(exc)[:300], "duration_ms": _ms(ts)}) + out["overall"] = "FAIL" + + print(json.dumps({**out, "duration_ms": _ms(t0)})) + except BaseException as exc: + print(json.dumps({"overall": "FAIL", "steps": [], "error": str(exc)[:300], "duration_ms": 0})) + + def main(): parser = argparse.ArgumentParser(description="Query Stripe via REST API") sub = parser.add_subparsers(dest="command") + # smoke + sub.add_parser("smoke", help="E2E gate read-only — valida auth + balance") + # list list_p = sub.add_parser("charges"); list_p.set_defaults(command="list", resource="charges") for r in ["customers", "invoices", "subscriptions", "payment_intents", "refunds", "products", "prices", "balance_transactions"]: @@ -209,7 +259,9 @@ def main(): parser.print_help() sys.exit(1) - if args.command == "list": + if args.command == "smoke": + cmd_smoke(args) + elif args.command == "list": cmd_list(args) elif args.command == "get": cmd_get(args) diff --git a/.claude/skills/int-youtube/scripts/youtube_client.py b/.claude/skills/int-youtube/scripts/youtube_client.py index 341f8103..fbb9c95d 100644 --- a/.claude/skills/int-youtube/scripts/youtube_client.py +++ b/.claude/skills/int-youtube/scripts/youtube_client.py @@ -416,6 +416,40 @@ def all_accounts_summary() -> dict: result = comments(acc, vid_id, n) elif cmd == "summary": result = all_accounts_summary() + elif cmd == "smoke": + import time as _time + _t0 = _time.time() + _steps = [] + _overall = "PASS" + + # step 1: auth — load accounts + _ts = _time.time() + try: + _accounts = _get_accounts() + _steps.append({"step": "auth", "status": "PASS", "duration_ms": round((_time.time() - _ts) * 1000)}) + except Exception as _e: + _steps.append({"step": "auth", "status": "FAIL", "error": str(_e)[:300], "duration_ms": round((_time.time() - _ts) * 1000)}) + _overall = "FAIL" + _accounts = [] + + # step 2: channel_stats read (cheap: 1 quota unit) + _ts = _time.time() + if not _accounts: + _steps.append({"step": "channel_stats", "status": "SKIP", "duration_ms": 0}) + else: + try: + _r = channel_stats(_accounts[0]) + if "error" in _r: + _steps.append({"step": "channel_stats", "status": "FAIL", "error": str(_r["error"])[:300], "duration_ms": round((_time.time() - _ts) * 1000)}) + _overall = "FAIL" + else: + _steps.append({"step": "channel_stats", "status": "PASS", "duration_ms": round((_time.time() - _ts) * 1000)}) + except Exception as _e: + _steps.append({"step": "channel_stats", "status": "FAIL", "error": str(_e)[:300], "duration_ms": round((_time.time() - _ts) * 1000)}) + _overall = "FAIL" + + print(json.dumps({"overall": _overall, "steps": _steps, "duration_ms": round((_time.time() - _t0) * 1000)}, indent=2)) + sys.exit(0) else: print(f"Unknown command: {cmd}") sys.exit(1) diff --git a/.env.example b/.env.example index 11610d8e..1ebe24fd 100644 --- a/.env.example +++ b/.env.example @@ -108,6 +108,13 @@ META_APP_SECRET= LINKEDIN_CLIENT_ID= LINKEDIN_CLIENT_SECRET= +# ── License — headless auto-activation ─────────────── +# Set this to the email used in your first manual license registration. +# On startup, EvoNexus calls /v1/register/auto silently and skips the manual +# setup screen. Falls back to manual setup if the email isn't registered yet. +# Leave empty (or unset) to keep the default behavior. +# EVOLUTION_OPERATOR_EMAIL=operator@example.com + # ── Evolution API ──────────────────────────────────── # Your Evolution API instance URL and global API key EVOLUTION_API_URL= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f4fd5855..8070b35b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Harassment, discrimination, or abusive behavior will not be tolerated. ### Reporting Bugs -1. Check existing [issues](https://github.com/EvolutionAPI/evo-nexus/issues) +1. Check existing [issues](https://github.com/evolution-foundation/evo-nexus/issues) to avoid duplicates 2. Open a new issue with: - Clear, descriptive title diff --git a/README.md b/README.md index 73e204c7..70be4e38 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@

- Latest version + Latest version License: Apache 2.0 Documentation Community @@ -46,7 +46,7 @@ It turns a single CLI installation into a team of **38 specialized agents** orga ## Part of the Evolution Foundation ecosystem -EvoNexus is one of the projects maintained by Evolution Foundation. It is the operating layer that orchestrates the Foundation's own work — including the development of [Evo CRM Community](https://github.com/EvolutionAPI/evo-crm-community), [Evolution API](https://github.com/EvolutionAPI/evolution-api) and [Evolution Go](https://github.com/EvolutionAPI/evolution-go). +EvoNexus is one of the projects maintained by Evolution Foundation. It is the operating layer that orchestrates the Foundation's own work — including the development of [Evo CRM Community](https://github.com/evolution-foundation/evo-crm-community), [Evolution API](https://github.com/evolution-foundation/evolution-api) and [Evolution Go](https://github.com/evolution-foundation/evolution-go). ### Why EvoNexus? @@ -101,7 +101,7 @@ EvoNexus is one of the projects maintained by Evolution Foundation. It is the op ### Method 1 — Docker (no setup, runs anywhere) ```bash -curl -O https://raw.githubusercontent.com/EvolutionAPI/evo-nexus/main/docker-compose.hub.yml +curl -O https://raw.githubusercontent.com/evolution-foundation/evo-nexus/main/docker-compose.hub.yml docker compose -f docker-compose.hub.yml up -d open http://localhost:8080 ``` @@ -117,7 +117,7 @@ npx @evoapi/evo-nexus ### Method 3 — Manual clone (developers / contributors) ```bash -git clone --depth 1 https://github.com/EvolutionAPI/evo-nexus.git +git clone --depth 1 https://github.com/evolution-foundation/evo-nexus.git cd evo-nexus # Interactive setup wizard diff --git a/dashboard/backend/licensing.py b/dashboard/backend/licensing.py index 60ed26f2..9677cf24 100644 --- a/dashboard/backend/licensing.py +++ b/dashboard/backend/licensing.py @@ -4,12 +4,14 @@ Protocol: POST /v1/register/direct — register with email/name, receive api_key + POST /v1/register/auto — headless register by email (must exist server-side) POST /v1/activate — validate existing api_key on startup GET /api/geo — geo-lookup from client IP """ import hashlib import hmac as hmac_mod +import os import socket import uuid import logging @@ -155,6 +157,24 @@ def direct_register(email: str, name: str, instance_id: str, return _post("/v1/register/direct", payload) +# ── Auto Registration (email-only, headless) ── + +def auto_register(email: str, instance_id: str) -> dict: + """Headless registration using only the operator email. + + The customer must already exist on the licensing server (one prior manual + registration). Used by the EVOLUTION_OPERATOR_EMAIL env-var flow. + + Returns {api_key, customer_id, tier, status}. + """ + return _post("/v1/register/auto", { + "email": email, + "tier": TIER, + "instance_id": instance_id, + "version": VERSION, + }) + + # ── Activation (startup with existing api_key) ── def activate(instance_id: str, api_key: str) -> bool: @@ -260,8 +280,54 @@ def initialize_runtime(): # ── Auto-register for existing installs ────── +def try_auto_register_from_env(instance_id: str) -> bool: + """Headless activation via EVOLUTION_OPERATOR_EMAIL env var. + + Requires the email to already exist on the licensing server (one prior + manual registration). Returns True on success. + + Failures are silent — caller falls back to the existing admin-based or + manual setup flow. + """ + email = os.environ.get("EVOLUTION_OPERATOR_EMAIL", "").strip() + if not email: + return False + + try: + result = auto_register(email=email, instance_id=instance_id) + except requests.HTTPError as e: + status = e.response.status_code if e.response is not None else "?" + if status == 404: + logger.info("Auto-activation skipped — email not registered yet (first time?).") + else: + logger.warning(f"Auto-activation rejected ({status}): falling back to manual flow.") + return False + except Exception as e: + logger.warning(f"Auto-activation skipped — {e}") + return False + + api_key = result.get("api_key") + if not api_key: + logger.warning("Auto-activation response missing api_key") + return False + + set_runtime_config("api_key", api_key) + set_runtime_config("tier", result.get("tier", TIER)) + if result.get("customer_id"): + set_runtime_config("customer_id", str(result["customer_id"])) + set_runtime_config("version", VERSION) + set_runtime_config("registered_at", datetime.now(timezone.utc).isoformat()) + + ctx = get_context() + ctx.api_key = api_key + ctx.instance_id = instance_id + logger.info("License activated automatically via EVOLUTION_OPERATOR_EMAIL") + return True + + def auto_register_if_needed(): - """If users exist but no license, register retroactively.""" + """If no license yet, try EVOLUTION_OPERATOR_EMAIL first, then fall back to + the admin-based retroactive flow.""" try: instance_id = get_runtime_config("instance_id") api_key = get_runtime_config("api_key") @@ -270,6 +336,15 @@ def auto_register_if_needed(): initialize_runtime() return + if not instance_id: + instance_id = generate_instance_id() + set_runtime_config("instance_id", instance_id) + + # First-class path: silent activation from env var. + if try_auto_register_from_env(instance_id): + return + + # Fallback: if there's an admin user already, register retroactively. from models import User if User.query.count() == 0: return @@ -278,10 +353,6 @@ def auto_register_if_needed(): if not admin or not admin.email: return - if not instance_id: - instance_id = generate_instance_id() - set_runtime_config("instance_id", instance_id) - setup_perform( email=admin.email or "", name=admin.display_name or admin.username,