Skip to content

abedinm/license-server

Repository files navigation

EnterpriseCore License Server

Standalone FastAPI service that issues, verifies, and revokes license keys for EnterpriseCore AI Suite (and any other product you point at it).

Endpoints

Method Path Purpose
GET /api/health Liveness
POST /api/v1/licenses/issue Issue a new key (admin / webhook)
POST /api/v1/licenses/verify Desktop client checks a key + machine
POST /api/v1/licenses/revoke Revoke a key (refund / abuse)
POST /api/v1/licenses/deactivate Customer removes a machine slot
POST /api/v1/webhooks/lemon-squeezy Lemon Squeezy order_created / order_refunded

Local dev

python -m venv .venv && .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
alembic upgrade head
uvicorn app.main:app --reload --port 8765

Open http://localhost:8765/docs for the OpenAPI explorer.

Tests

pytest -q

Deploy (Fly.io free tier)

fly launch                    # first time only — accept the existing fly.toml
fly secrets set \
  LEMON_SQUEEZY_WEBHOOK_SECRET=...  \
  RESEND_API_KEY=...                \
  LICENSE_SECRET_KEY=$(openssl rand -base64 32)
fly deploy

DATABASE_URL is auto-injected by Fly if you attach a Postgres app:

fly postgres create --name ec-license-pg --region sin
fly postgres attach --app ec-license-server ec-license-pg

Lemon Squeezy setup

  1. Create a store + product in https://app.lemonsqueezy.com
  2. Settings → Webhooks → Add endpoint
    • URL: https://ec-license-server.fly.dev/api/v1/webhooks/lemon-squeezy
    • Signing secret: copy → put into LEMON_SQUEEZY_WEBHOOK_SECRET
    • Events: order_created, order_refunded
  3. Test with the "Send test event" button → check that webhook_events table has a row with processed=true

Client integration (desktop app)

# EnterpriseCore/backend/app/core/license_key.py
import httpx, uuid, json, datetime
from pathlib import Path

LICENSE_API = "https://ec-license-server.fly.dev/api/v1/licenses"
CACHE_FILE = Path.home() / ".enterprisecore" / "license.json"

def verify(key: str, machine_id: str) -> dict:
    # Try remote first, fall back to local cache if offline
    try:
        r = httpx.post(f"{LICENSE_API}/verify",
                       json={"key": key, "machine_id": machine_id, "app_version": "0.1.0"},
                       timeout=8)
        if r.status_code == 200:
            body = r.json()
            if body["valid"]:
                CACHE_FILE.parent.mkdir(exist_ok=True, parents=True)
                CACHE_FILE.write_text(json.dumps({
                    "key": key, "verified_at": datetime.datetime.utcnow().isoformat(),
                    "renewal_days": body["renewal_days"], "tier": body["tier"],
                }))
            return body
    except httpx.HTTPError:
        pass
    # Offline path — accept cache if within renewal window
    if CACHE_FILE.exists():
        cached = json.loads(CACHE_FILE.read_text())
        if cached["key"] == key:
            verified = datetime.datetime.fromisoformat(cached["verified_at"])
            age_days = (datetime.datetime.utcnow() - verified).days
            if age_days <= cached["renewal_days"]:
                return {"valid": True, "tier": cached["tier"], "offline": True}
    return {"valid": False, "reason": "offline_no_cache"}

License keys

Format: EC-XXXX-XXXX-XXXX-XXXX — base32 alphabet without ambiguous characters (no 0/O/I/L/1).

Default policy: 3 machines per key, 7-day offline grace period.

About

Standalone license issuance, verification, and revocation service for EnterpriseCore.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors