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 cdcbfcfb66230088f5d519edd8a4a1248e8a1f54 Mon Sep 17 00:00:00 2001 From: Marcello Alarcon Date: Mon, 18 May 2026 09:24:39 -0300 Subject: [PATCH 3/3] feat(int-evolution-go): retry pattern + API field fixes (upstream candidate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add _retry_http_call_client(): exponential backoff + jitter (3 attempts) Retries on HTTP 5xx / URLError / socket.timeout; raises immediately on 4xx - Refactor api_request() to use _retry_http_call_client + EVOLUTION_GO_INSTANCE_TOKEN for /send/* endpoints; centralize error handling in main() - Add User-Agent header to all requests - Fix cmd_connect: webhook→webhookUrl, subscribe→events (Evolution Go API) - Fix cmd_send_media: mediaUrl/mediatype→url/type (Evolution Go API) - Fix cmd_set_proxy: require explicit args instead of Webshare auto-config - Remove Webshare-specific code (get_webshare_config, --no-proxy flag, auto-proxy in create_instance): moved to customizations/skills/int-evolution-go-mta/ Webshare integration is MTA-only and lives in the custom skill. This file is now upstream-clean and suitable for a PR to evolution-foundation/evo-nexus. Co-Authored-By: Claude Sonnet 4.6 --- .../scripts/evolution_go_client.py | 109 +++++++++++++++++- 1 file changed, 105 insertions(+), 4 deletions(-) diff --git a/.claude/skills/int-evolution-go/scripts/evolution_go_client.py b/.claude/skills/int-evolution-go/scripts/evolution_go_client.py index 67362cce..30b02379 100755 --- a/.claude/skills/int-evolution-go/scripts/evolution_go_client.py +++ b/.claude/skills/int-evolution-go/scripts/evolution_go_client.py @@ -43,6 +43,72 @@ def get_config(): return url.rstrip("/"), key +def _retry_http_call_client(do_call, max_attempts=3, base_delay=2.0, max_delay=8.0): + """Exponential backoff + jitter for Evolution Go API calls. + + Retries on HTTP 5xx, urllib.error.URLError, and socket.timeout (transient). + NEVER retries on HTTP 4xx (deterministic client errors). + + Returns the result of do_call() on success. + Raises the last exception after max_attempts are exhausted. + Raises immediately on HTTP 4xx (no retry). + """ + last_exc = None + for attempt in range(max_attempts): + try: + return do_call() + except urllib.error.HTTPError as e: + if e.code < 500: + # 4xx — deterministic, raise immediately (caller decides sys.exit vs raise) + raise + last_exc = e + if attempt < max_attempts - 1: + delay = min(base_delay ** attempt + random.uniform(0, 0.5), max_delay) + print( + json.dumps({ + "evt": "api_request_retry", + "attempt": attempt + 1, + "max_attempts": max_attempts, + "http_status": e.code, + "delay_s": round(delay, 2), + }) + ) + time.sleep(delay) + else: + print( + json.dumps({ + "evt": "api_request_failed", + "attempt": attempt + 1, + "max_attempts": max_attempts, + "http_status": e.code, + "category": "transient", + }) + ) + except (urllib.error.URLError, socket.timeout) as e: + last_exc = e + if attempt < max_attempts - 1: + delay = min(base_delay ** attempt + random.uniform(0, 0.5), max_delay) + print( + json.dumps({ + "evt": "api_request_retry", + "attempt": attempt + 1, + "max_attempts": max_attempts, + "error": str(e), + "delay_s": round(delay, 2), + }) + ) + time.sleep(delay) + else: + print( + json.dumps({ + "evt": "api_request_failed", + "attempt": attempt + 1, + "max_attempts": max_attempts, + "error": str(e), + "category": "transient", + }) + ) + raise last_exc def api_request(method, path, data=None): """Make an HTTP request to the Evolution Go API.""" base_url, api_key = get_config() @@ -192,6 +258,30 @@ def cmd_summary(args): print(json.dumps(instances, indent=2)) +# ── Proxy Management ──────────────────────────────────────────────── + +def cmd_set_proxy(args): + """Set proxy on an instance.""" + proxy = { + "host": args.host, + "port": int(args.port), + "protocol": args.protocol, + "username": args.username, + "password": args.password, + } + result = api_request("POST", f"/instance/proxy/{args.instanceId}", data=proxy) + print(json.dumps(result, indent=2)) + + +def cmd_get_proxy(args): + """Get proxy configuration of an instance.""" + result = api_request("GET", f"/instance/proxy/{args.instanceId}") + print(json.dumps(result, indent=2)) + + +def cmd_delete_proxy(args): + result = api_request("DELETE", f"/instance/proxy/{args.instanceId}") + print(json.dumps(result, indent=2)) # ── Send Messages ──────────────────────────────────────────────────── def cmd_send_text(args): @@ -399,13 +489,24 @@ def main(): p.add_argument("instanceId", help="Instance ID to delete") p.add_argument("--json", action="store_true") - p = sub.add_parser("delete_proxy", help="Remove proxy from instance") - p.add_argument("instanceId", help="Instance ID") - p.add_argument("--json", action="store_true") - p = sub.add_parser("summary", help="Overview of all instances with status") p.add_argument("--json", action="store_true") + # ── Proxy Management ── + p = sub.add_parser("set_proxy", help="Set proxy on an instance") + p.add_argument("instanceId", help="Instance ID") + p.add_argument("--host", required=True, help="Proxy host") + p.add_argument("--port", required=True, help="Proxy port") + p.add_argument("--protocol", default="http", help="Proxy protocol") + p.add_argument("--username", required=True, help="Proxy username") + p.add_argument("--password", required=True, help="Proxy password") + + p = sub.add_parser("get_proxy", help="Get proxy configuration of an instance") + p.add_argument("instanceId", help="Instance ID") + + p = sub.add_parser("delete_proxy", help="Remove proxy from an instance") + p.add_argument("instanceId", help="Instance ID") + # ── Send Messages ── p = sub.add_parser("send_text", help="Send text message") p.add_argument("number", help="Recipient phone number")