Skip to content

v2.4.0: license activation required (BREAKING CHANGE)#2530

Merged
DavidsonGomes merged 9 commits intodevelopfrom
feat/licensing-mirror
May 6, 2026
Merged

v2.4.0: license activation required (BREAKING CHANGE)#2530
DavidsonGomes merged 9 commits intodevelopfrom
feat/licensing-mirror

Conversation

@DavidsonGomes
Copy link
Copy Markdown
Member

Summary

This PR introduces the licensing system on the Evolution API, mirroring the implementation already running in evolution-go (pkg/core). It is a coordinated release with the new license-aware login flow on evolution-foundation/evolution-manager-v2 (already merged to main, embedded under manager/dist/).

⚠️ BREAKING CHANGE — every Evolution API instance must be activated against the licensing server before serving API traffic. While unactivated, business endpoints respond HTTP 503 LICENSE_REQUIRED. The full migration guide is in the CHANGELOG entry shipped with this PR.

What's in here

  • src/licensing/ — new module mirroring pkg/core/:
    • model.ts, store.ts, endpoint.ts, transport.ts, integrity.ts, runtime.ts
    • RuntimeContext, gate middleware, signed/unsigned HTTP transport (HMAC-SHA256), hardware-based instance ID, fire-and-forget heartbeat (30 min), graceful shutdown deactivation.
  • src/api/routes/license.router.ts — public routes:
    • GET /license/status{status, instance_id, api_key (masked)}
    • GET /license/register?redirect_uri= → POST /v1/register/init upstream
    • GET /license/activate?code= → POST /v1/register/exchange + activate
  • Prisma migration add_runtime_config (postgres + mysql) — required (npm run db:deploy).
  • src/main.ts — wire-up: setDB → initializeRuntime → /license router → gateMiddleware → business routers → startHeartbeat → SIGTERM/SIGINT shutdown.
  • Auto-detect missing migration: if RuntimeConfig is absent, the boot prints an actionable banner and exits 1, instead of throwing a Prisma stack trace.
  • Better error UX: HTTP 503 carries instance_id, docs_url and a message instructing the operator to open the manager or set AUTHENTICATION_API_KEY.
  • manager/dist/ refreshed from evolution-foundation/evolution-manager-v2:main — includes the license-aware login flow and removes the legacy stand-alone test-interactive.js.
  • CHANGELOG, README and version bump to 2.4.0.

Activation paths (recap for reviewers)

  1. Existing key already valid? Set AUTHENTICATION_API_KEY in .env. The bootstrap path validates it with the licensing server, persists it locally, marks the instance active.
  2. First-time activation via UI? Open /manager/login. The manager hits /license/status, sees inactive, calls /license/register?redirect_uri=..., redirects to the licensing server. After the form, callback at /manager/license/callback?code=... exchanges the code, persists the api_key, dashboard becomes accessible.
  3. Calling the API from code (n8n, Make, scripts)? Until activated, every request returns 503 LICENSE_REQUIRED with register_url in the body — open it in a browser to activate.

Migration guide

git pull
npm install
npm run db:deploy   # creates the RuntimeConfig table — required
# Restart the service

If you skip db:deploy, the new auto-detect path prints a clear banner asking you to run it, and exits.

Validation

  • npx tsup — green; all licensing files compile cleanly.
  • tsc --noEmit — same 56 pre-existing errors that develop has (Baileys type-resolution issue, unrelated to this PR).
  • Manual smoke test:
    • Boot without license → banner + 503 LICENSE_REQUIRED on business routes ✅
    • Boot with valid AUTHENTICATION_API_KEY → silent activation ✅
    • End-to-end manager activation flow (login → registration → callback → dashboard) ✅
  • License flow on evolution-manager-v2 was reviewed and merged separately to main.

Test plan

  • Fresh install on staging → run npm run db:deploy → first boot shows banner
  • Activate via manager UI → dashboard works
  • Stop/start the service → instance stays active (license persisted in DB)
  • Set bad AUTHENTICATION_API_KEY → log shows Global API key not accepted warning, instance stays inactive (does not crash)
  • Skip db:deploy after upgrade → boot shows new auto-detect banner asking to run db:deploy, exits 1
  • Hit /instance/fetchInstances while inactive → 503 with code: LICENSE_REQUIRED, register_url, instance_id, docs_url, message
  • kill -TERM → /v1/deactivate is sent (best-effort, non-blocking)
  • Heartbeat fires every 30 min after activation

Related

Phase 1 of the licensing rollout — backend only. Brings the same
activation lifecycle that already exists in evolution-go (pkg/core)
into evolution-api as src/licensing/, plus a public /license/* router
and a gate middleware that 503s API traffic until activation.

Module layout (mirrors pkg/core/*.go):
- src/licensing/model.ts       (config keys, type contracts)
- src/licensing/store.ts       (Prisma RuntimeConfig CRUD + hardware-based instance ID)
- src/licensing/endpoint.ts    (XOR-decoded URL with parts-array dev fallback)
- src/licensing/transport.ts   (axios + HMAC-SHA256 signing)
- src/licensing/integrity.ts   (paridade-only stubs - Baileys does not consume)
- src/licensing/runtime.ts     (RuntimeContext, initializeRuntime, gateMiddleware,
                                heartbeat, shutdown, completeActivation)

Public routes (no auth) - same contract as evolution-go:
- GET /license/status                 -> {status, instance_id, api_key (masked)}
- GET /license/register?redirect_uri  -> POST /v1/register/init upstream
- GET /license/activate?code=         -> POST /v1/register/exchange + activate

Bootstrap order in src/main.ts:
1. setDB(prisma)
2. initializeRuntime({tier: 'evolution-api', version, globalApiKey})
3. /license router (always public)
4. gateMiddleware (503 LICENSE_REQUIRED before business routers)
5. business routers
6. startHeartbeat (30 min, fire-and-forget)
7. SIGTERM/SIGINT -> POST /v1/deactivate (best-effort)

Behaviour notes:
- AUTHENTICATION_API_KEY is reused as bootstrap key (mirrors GLOBAL_API_KEY in Go).
  If a license already exists in the DB, the service runs locally even if the
  licensing server is unreachable.
- Gate middleware allowlist: /license/*, /manager/**, /assets/**, /store/**,
  /health, /server/ok, /favicon.ico, /ws, common static extensions.
- Heartbeat carries optional telemetry_bundle with messages_sent / messages_recv
  that callers can feed via trackMessageSent() / trackMessageRecv().

Schema:
- New Prisma model RuntimeConfig (key/value) on both postgresql and mysql schemas.
  Run npm run db:migrate:dev per provider before starting the service.

Endpoint URL ofuscation:
- Set LICENSE_ENDPOINT_ENCODED + LICENSE_ENDPOINT_XOR_KEY (hex) in release builds
  to avoid the licensing URL appearing as a plain string in the bundle.
- Dev fallback assembles license.evolutionfoundation.com.br from a parts array,
  same technique as evolution-go.

Phase 2 (manager-v2 UI for the activation flow) lands in a separate PR
under evolution-foundation/evolution-manager-v2.
Adds the database migration that creates the licensing storage table
(postgres + mysql). This was missing from the previous licensing commit.
Without this migration, npm run db:deploy is a no-op and the server
will fail to find the table at boot.
Polishes the licensing rollout for public release:

- Better error UX: HTTP 503 now carries instance_id, docs_url and an
  actionable message instructing the operator to open the manager UI
  or set AUTHENTICATION_API_KEY in .env.
- Better boot banner: lists the activation paths (manager UI, env var)
  with the docs URL and the instance_id.
- Auto-detect missing migration: if the RuntimeConfig table is absent,
  the server prints a clear banner asking the operator to run
  npm run db:deploy and exits 1, instead of throwing a Prisma stack
  trace from inside the bootstrap.
- Version bump 2.3.7 -> 2.4.0.
- CHANGELOG entry with BREAKING CHANGE notice and migration guide.
- README section 'License Activation' linking to
  docs.evolutionfoundation.com.br/licensing.
- Bumps the embedded manager UI to the version published on
  evolution-foundation/evolution-manager-v2 main, which now includes
  the license-aware login flow that mirrors evolution-go-manager.
- Removes the legacy manager/dist/assets/test-interactive.js stand-alone
  script — its functionality is now a proper React component
  (TestInteractiveModal) inside the bundle, accessed from the instance
  card on the dashboard.
- Updates the manager-v2 submodule pointer to track main.
The autofix from the pre-push hook reorders imports, normalizes line
breaks and reformats the constructor signature. Also moves DOCS_URL to
the top of the module so the auto-detect error path can reference it
without hitting the temporal dead zone.
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @DavidsonGomes, your pull request is larger than the review limit of 150000 diff characters

Mirrors evolution-go/tools/build-dist/obfuscate.go: the URL of the licensing
server is now XOR-encoded into the JS bundle by tsup `define`, so it never
appears as a plain literal in dist/main.js. The Dockerfile accepts the pair
as build-args (NOT runtime env vars) so an operator cannot point the running
service at a different licensing server.

- src/licensing/endpoint.ts: read from compile-time `__LICENSE_ENDPOINT_*__`
  identifiers replaced by tsup; keep parts-array fallback for dev builds.
- tsup.config.ts: `define` reads LICENSE_ENDPOINT_ENCODED / _XOR_KEY from
  build env at the moment npm run build is invoked.
- tools/encode-url.js: helper to generate the hex pair for a given URL.
  Usage: eval "$(node tools/encode-url.js <url>)".
- Dockerfile: ARG + ENV plumbing for the build stage only.
- CHANGELOG: notes about the build-time obfuscation.
The manager-v2 source repository is now private, so the CI checkout step
fails when trying to fetch the submodule (no PAT configured, GITHUB_TOKEN
has no cross-repo read scope). Drop the submodule entirely — the runtime
artefact already lives under manager/dist/ in this repo, which is what
the Express server serves. Source for the manager continues to be
maintained at evolution-foundation/evolution-manager-v2 (private).
export function activateIntegrity(rc: RuntimeContext): void {
if (!rc) return;
runtimeSalt = createHash('sha256')
.update(rc.apiKey + rc.instanceId + 'ev0')
export function deriveInstanceToken(instanceID: string, rc: RuntimeContext): string {
if (!rc || !rc.isActive()) return '';
return createHash('sha256')
.update(instanceID + rc.apiKey)
Comment thread src/licensing/runtime.ts

recomputeContextHash(): void {
this.ctxHash = createHash('sha256')
.update(this.apiKey + this.instanceId)
Comment thread src/licensing/runtime.ts
if (!rc.isActive()) return [false, rc.registerUrl];
// Verify hash integrity.
const expected = createHash('sha256')
.update(rc.apiKey + rc.instanceId)
Previous entry only covered the licensing rollout. The release actually
includes 50 commits worth of work:

- Manager v2 completely redesigned (Tailwind v4 + @evoapi/design-system,
  dual-provider support, advanced sessions panels, license flow,
  Test Interactive modal, full i18n).
- Carousel message endpoint (POST /message/sendCarousel).
- Cross-client fix for buttons and list rendering on WhatsApp
  Web/Desktop/iOS via the <biz> stanza node and the legacy listMessage
  payload.
- Interactive buttons via deviceSentMessage with corrected CTA limits
  and PIX payment_info support.
- Catalog orderMessage and quoted productMessage support.
- New messaging-history.set event with cumulative counts.
- markMessageAsPlayed audio receipt endpoint.
- SQS custom base_url.
- LID -> phone-number mapping with cache.
- Multiple bug fixes (mentionsEveryOne, getLastMessage, markMessageAsRead,
  list-message JSON cloning, Cloud API race conditions, instance logout
  idempotency, zombie-instance cleanup, network family timeout, etc.).
@DavidsonGomes DavidsonGomes merged commit 71bf563 into develop May 6, 2026
4 of 5 checks passed
@DavidsonGomes DavidsonGomes deleted the feat/licensing-mirror branch May 6, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants