From 31d000a73dcb243a1c5bd011a954f4960f7b340c Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 6 May 2026 14:36:06 -0300 Subject: [PATCH 1/3] docs(org): update GitHub URLs from EvolutionAPI to evolution-foundation Co-Authored-By: Claude Opus 4.7 (1M context) --- CONTRIBUTING.md | 2 +- README.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) 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 From 7f5dd760b5854f2217a376fd34c48b32195f6d76 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 12 May 2026 12:21:02 -0300 Subject: [PATCH 2/3] 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. --- .env.example | 7 +++ dashboard/backend/licensing.py | 81 +++++++++++++++++++++++++++++++--- 2 files changed, 83 insertions(+), 5 deletions(-) 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/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, From a4003e71c928a2b267f29e45fbc13147637454d3 Mon Sep 17 00:00:00 2001 From: Marcello Alarcon Date: Sun, 31 May 2026 17:10:30 -0300 Subject: [PATCH 3/3] feat(skills): add `smoke` health-check command to int-stripe/linkedin/youtube Add a uniform `smoke` connectivity command to three integration skills. The command runs auth + a representative read-only step, always exits 0, and emits structured JSON so callers (CI, heartbeats, manual validation) can distinguish PASS from FAIL without parsing stderr or relying on exit codes that crash on failure. Contract: {"overall": "PASS"|"FAIL", "steps": [{"step": "...", "status": "PASS"|"FAIL", "error": "...", "duration_ms": N}], "duration_ms": N} - Always exits 0, even on failure (missing creds report overall=FAIL, never crash) - Each step wrapped in try/except; errors captured into the `error` field int-stripe, int-linkedin and int-youtube are clean reference implementations. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../int-linkedin/scripts/linkedin_client.py | 35 ++++++++++++ .../skills/int-stripe/scripts/stripe_query.py | 54 ++++++++++++++++++- .../int-youtube/scripts/youtube_client.py | 34 ++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) 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)