🔐 License Platform
Documentation
v1.1
Plateforme de licences centralisée

Une seule API pour licencier tous vos logiciels.

API REST, SDK client, bot Discord et dashboard web — adossés à PostgreSQL. Chaque décision de sécurité (produit, expiration, ban, HWID) est prise côté serveur. Le client n'est jamais cru.

4composants (API · SDK · Bot · Dashboard)
6tables PostgreSQL indexées
RBAC4 rôles (Owner → Support)
HWIDempreinte SHA-256 multi-source

Ce que fait la plateforme

🔑

Keys & produits

Générez des licences cryptographiquement aléatoires, liées à un produit précis. Une key d'un produit ne marche jamais pour un autre.

🖥️

Verrouillage machine

Chaque licence se lie à un HWID à la première activation. Reset/rotation possible en un clic.

⏱️

Expiration différée

Le décompte démarre à la première utilisation, pas à la création. Une key en stock ne perd pas de temps.

🤖

Gestion Discord

Générer, bannir, éditer, reset HWID — le tout par commandes, avec permissions par rôle.

📊

Dashboard web

Interface sombre : stats, recherche, filtres, CRUD produits & licences, logs.

✉️

DM à l'expiration

Attribuez une key à un membre Discord : il reçoit un message privé dès qu'elle expire.

🛡️

Principe fondamental. Le SDK ne fait qu'appeler l'API et lit data.valid. Toute logique (expiration, ban, produit, HWID) est vérifiée par le serveur — un binaire patché ne peut pas s'auto-valider.

Structure

Architecture

Trois clients parlent à une API unique, chacun avec sa propre authentification. L'API est la seule à toucher la base.

Logiciel (SDK) X-Product-Key Dashboard JWT (Bearer) Bot Discord X-Service-Token API — Fastify Public · Admin · Internal RBAC · rate-limit · Zod PostgreSQL Prisma
SurfaceAuthUtilisée par
Client (public)X-Product-KeyVos logiciels via le SDK
AdminAuthorization: Bearer (JWT) ou X-Api-KeyDashboard, automations
InternalX-Service-TokenBot Discord
5 minutes

Démarrage rapide

Le plus simple : Docker Compose (base + API + dashboard + bot).

# 1. Configurer
cp .env.example .env
# Générer de vrais secrets :
openssl rand -hex 32   # JWT_SECRET, LICENSE_SIGNING_SECRET, DATA_ENCRYPTION_KEY
openssl rand -hex 24   # INTERNAL_SERVICE_TOKEN

# 2. Lancer
docker compose up -d --build

# 3. Migrer la base + créer le 1er admin OWNER
docker compose exec api npx prisma migrate deploy
docker compose exec api npm run prisma:seed
  • API : http://localhost:4000 — santé : /health
  • Dashboard : http://localhost:3000
💡

Pas de bot pour l'instant ? Laissez DISCORD_TOKEN vide et lancez seulement db api dashboard.

Production

Installer sur un VPS

De zéro sur Ubuntu/Debian. Deux sous-domaines recommandés : licenses.mondomaine.com (API) et dash.mondomaine.com (dashboard).

1 · Docker sur le serveur

curl -fsSL https://get.docker.com | sh
docker --version && docker compose version

2 · Envoyer le projet

# Depuis votre PC Windows (PowerShell)
scp -r C:\license-platform root@IP_DU_VPS:/opt/license-platform

3 · Configurer .env

NODE_ENV=production
API_BASE_URL=https://licenses.mondomaine.com
CORS_ORIGINS=https://dash.mondomaine.com
NEXT_PUBLIC_API_BASE_URL=https://licenses.mondomaine.com
POSTGRES_PASSWORD=<mot_de_passe_fort>
JWT_SECRET=<openssl rand -hex 32>
LICENSE_SIGNING_SECRET=<openssl rand -hex 32>
INTERNAL_SERVICE_TOKEN=<openssl rand -hex 24>
BOOTSTRAP_ADMIN_USERNAME=owner
BOOTSTRAP_ADMIN_PASSWORD=<mot_de_passe_long>
⚠️

L'API refuse de démarrer en production si un secret est manquant, trop court, ou contient une valeur d'exemple (REPLACE_…). C'est voulu.

4 · Lancer & initialiser

docker compose up -d --build
docker compose exec api npx prisma migrate deploy   # crée les tables
docker compose exec api npm run prisma:seed          # crée l'admin OWNER

5 · HTTPS avec Caddy (certificats auto)

licenses.mondomaine.com {
    reverse_proxy localhost:4000
}
dash.mondomaine.com {
    reverse_proxy localhost:3000
}
systemctl reload caddy
curl https://licenses.mondomaine.com/health   # {"status":"ok",...}

6 · Firewall & sauvegardes

ufw allow 22 && ufw allow 80 && ufw allow 443 && ufw enable
# Le port PostgreSQL n'est PAS exposé publiquement (volontaire).
docker compose exec db pg_dump -U license license_platform > backup_$(date +%F).sql
Modèle

Produits, keys & statuts

Un produit (ex. mysoftware) regroupe des licences. Chaque key est unique, au format XXXX-XXXX-XXXX-XXXX, et rattachée à un produit.

Une licence contient

id · key · product · status · hwid · maxActivations · durationSeconds · discordUserId · createdAt · activatedAt · expiresAt · lastUsedAt · notes

Statuts

UNUSED · ACTIVE · EXPIRED · BANNED · DISABLED

🔒

Isolation par produit. Une key de mysoftware renverra toujours PRODUCT_MISMATCH si on tente de l'utiliser avec mytool. La clé API CLIENT est elle-même liée à un seul produit.

Permissions

Rôles & permissions (RBAC)

RôlePeut…
OWNERTout — admins, clés API, produits, licences, logs
ADMINProduits + licences + logs + lecture des clés API
MODERATORGérer les licences (générer, éditer, ban, reset HWID)
SUPPORTConsultation (licences, produits, stats)

Côté Discord, chaque rôle RBAC est mappé à un ou plusieurs rôles du serveur (variables DISCORD_ROLE_*). Le bot vérifie la permission avant d'appeler l'API.

Verrouillage machine

HWID — empreinte matérielle

Le HWID est calculé côté client à partir de plusieurs signaux stables (CPU, cœurs, architecture, mémoire en Gio, hostname, 1re MAC non-interne, utilisateur), normalisés puis hashés en SHA-256.

  • Multi-source — changer un seul composant (ex. une carte réseau) n'invalide pas la licence.
  • Normalisé — casse, espaces et ordre n'affectent pas le résultat.
  • Seul le hash est transmis — jamais les informations matérielles brutes.
  • Salé par produit — un même PC produit des HWID différents selon le logiciel (pas de corrélation).
🛡️

Le serveur ne stocke que le SHA-256 du HWID. Un reset HWID (admin ou license.reset()) permet de re-lier la licence à une nouvelle machine, sans relancer l'abonnement.

Comportement clé

Expiration différée — le temps démarre à la 1re utilisation

Pour une durée non-lifetime, le compte à rebours ne commence pas à la création mais à la première activation.

ÉtapeÉtat de la licence
Création (ex. 30 jours)Budget durationSeconds = 2592000, expiresAt = nullaucun temps ne descend
1re activationexpiresAt = maintenant + 30 j — le décompte démarre
Reset HWIDLa machine change, mais l'expiration est conservée (l'abonnement ne repart pas de zéro)
LifetimedurationSeconds = null → n'expire jamais

Dans le dashboard et les embeds Discord, une key non démarrée affiche ⏸️ non démarrée au lieu d'une date.

Nouveauté

Attribution & DM Discord à l'expiration

Attribuez une licence à un membre Discord (par son ID). Il reçoit automatiquement un message privé dès que sa licence expire.

Attribuer une key

# À la génération (mention ou ID) :
+key generate mysoftware 30d 1 @membre
+key generate mysoftware 30d 1 123456789012345678

# Après coup :
+key edit <key> discord @membre
+key edit <key> discord none      # retirer l'attribution

Depuis le dashboard : champ « Attribuer à (ID Discord) » dans la fenêtre de génération.

✉️

Un job du bot vérifie les licences expirées toutes les 5 min (EXPIRY_CHECK_INTERVAL) et envoie un seul DM par licence. Le bot doit tourner et partager un serveur avec la personne.

Intégration

Intégrer dans vos logiciels

Votre logiciel envoie key + produit + HWID, et se fie uniquement à data.valid. Choisissez votre langage :

import { LicenseClient } from '@license/sdk-node';

const license = new LicenseClient({
  apiUrl: 'https://licenses.mondomaine.com',
  productKey: 'lk_cli_xxxxxxxx',   // clé API CLIENT du produit
  product: 'mysoftware',           // slug du produit
});

await license.initialize();               // calcule le HWID
const res = await license.login(key);   // active + lie le HWID
if (!res.valid) {
  console.error('Licence invalide :', res.reason);
  process.exit(1);                       // NE PAS continuer
}
// … démarrer le logiciel, puis re-vérifier périodiquement :
setInterval(() => license.isValid(key), 600000);
import hashlib, platform, uuid, sys, requests

API_URL, PRODUCT_KEY, PRODUCT = "https://licenses.mondomaine.com", "lk_cli_xxx", "mysoftware"

def compute_hwid():
    signals = [
        f"cpu:{platform.processor().strip().lower()}",
        f"arch:{platform.machine().strip().lower()}",
        f"host:{platform.node().strip().lower()}",
        f"mac:{uuid.getnode()}",
    ]
    canonical = "|".join(sorted(signals))
    return hashlib.sha256(f"{PRODUCT}::{canonical}".encode()).hexdigest()

def check(endpoint, key):
    r = requests.post(f"{API_URL}/api/v1/license/{endpoint}",
        headers={"X-Product-Key": PRODUCT_KEY},
        json={"license": key.strip().upper(), "product": PRODUCT, "hwid": compute_hwid()}, timeout=8)
    r.raise_for_status()
    return r.json()["data"]

res = check("activate", input("Clé : "))
if not res["valid"]:
    print("Licence invalide :", res["reason"]); sys.exit(1)
POST https://licenses.mondomaine.com/api/v1/license/activate
Content-Type: application/json
X-Product-Key: lk_cli_xxxxxxxx

{ "license": "XXXX-XXXX-XXXX-XXXX", "product": "mysoftware", "hwid": "<sha256>" }

// Réponse — lisez data.valid :
{ "data": { "valid": true, "reason": "OK", "expiresAt": "2026-09-01T…" },
  "signature": "…" }
🧭

Fail-closed. Si la réponse n'est pas valid: true, arrêtez le logiciel. Re-vérifiez à chaque lancement et périodiquement — ne stockez jamais un « oui » définitif en local.

Codes reason possibles

OK · NOT_FOUND · PRODUCT_MISMATCH · BANNED · DISABLED · EXPIRED · HWID_MISMATCH · NOT_ACTIVATED

Référence

API — endpoints

Base : {API_BASE_URL}/api/v1. Erreurs : { "error": { "code", "message" } }.

Client (logiciel) · X-Product-Key

MéthodeEndpointRôle
POST/license/activateActive + lie le HWID
POST/license/validateVérifie (heartbeat)
POST/license/deactivateLibère le HWID
POST/license/infoInfos publiques minimales

Admin · JWT / X-Api-Key / X-Service-Token

MéthodeEndpointRôle min.
POST/auth/login
GET/statsSUPPORT
GET/products · /products/:idSUPPORT
POST PATCH DEL/products…ADMIN
GET/licenses · /licenses/lookup/:keySUPPORT
POST/licenses (générer)MODERATOR
PATCH DEL/licenses/:idMODERATOR
POST/licenses/:id/hwid/reset · /ban · /unbanMODERATOR
GET/licenses/notifications/expiredSUPPORT
GET/logsADMIN
GET POST DEL/admins · /apikeysOWNER
# Générer 10 licences 30 jours (service token)
curl -X POST $API/api/v1/licenses \
  -H "x-service-token: $INTERNAL_SERVICE_TOKEN" \
  -H "content-type: application/json" \
  -d '{"productId":"<id>","quantity":10,"duration":"30d"}'
Discord

Commandes du bot

# Licences
+key generate <produit> <durée> [quantité] [@user]   # ex: +key generate mysoftware 30d 10
+key info <key>
+key edit <key> <product|expire|status|discord> <valeur>
+key delete <key>                                    # confirmation requise
+key hwid reset <key>
+key ban <key>   |   +key unban <key>

# Produits
+product create <slug> <nom>
+product list  |  +product info <id|slug>
+product edit <id> <name|version|status|description> <valeur>
+product delete <id>

Les grosses générations sont renvoyées en pièce jointe .txt pour ne pas polluer le salon.

Web

Dashboard

Interface sombre, connexion admin, pages : Dashboard (stats), Products, Licenses, Users/Admins, Logs, API, Settings.

Page Licenses

Recherche, filtres par produit/statut, génération (avec attribution Discord), ban/unban, reset HWID, suppression.

Page API

Créer des clés CLIENT (par produit) ou ADMIN. Le secret n'est affiché qu'une seule fois.

Confiance zéro côté client

Sécurité

🔑

Secrets

Jamais dans le code — tout via .env. Démarrage refusé en prod si un secret est absent ou factice.

🧂

Données au repos

Mots de passe en Argon2id. Keys, HWID et clés API stockés en SHA-256. Secrets d'API montrés une seule fois.

⏱️

Rate limiting

Par IP (global), par IP+licence (endpoints client), par IP+identifiant (login anti-brute-force).

🧱

Validation

Zod sur toutes les entrées, requêtes paramétrées (Prisma), helmet, CORS restreint.

🕵️

Réponses minimales

Key inconnue et key supprimée renvoient le même NOT_FOUND. Aucune donnée sensible exposée.

📜

Journalisation

Audit (gestion) + activation logs (client). En-têtes sensibles retirés des logs.

🔐

Les réponses de licence sont signées en HMAC-SHA256. Pour un anti-tamper hors-ligne renforcé, on peut migrer vers Ed25519 (clé publique embarquée dans le logiciel).

Configuration

Variables d'environnement

VariableDescription
DATABASE_URLChaîne de connexion PostgreSQL
JWT_SECRETSigne les JWT du dashboard (≥ 32 car.)
LICENSE_SIGNING_SECRETSigne (HMAC) les réponses de licence
INTERNAL_SERVICE_TOKENJeton du bot vers l'API admin
CORS_ORIGINSOrigines autorisées (dashboard)
API_BASE_URL / NEXT_PUBLIC_API_BASE_URLURL publique de l'API
RATE_LIMIT_* / CLIENT_RATE_LIMIT_* / LOGIN_RATE_LIMIT_*Limites de débit
DISCORD_TOKEN / DISCORD_ROLE_*Bot & mapping des rôles RBAC
EXPIRY_CHECK_INTERVALFréquence de vérif. des expirations (DM), ms (min 60000)
BOOTSTRAP_ADMIN_*Premier admin OWNER (seed)
Aide

FAQ & dépannage

« Invalid environment configuration » au démarrage

Un secret est manquant ou trop court dans .env. Regénérez avec openssl rand -hex 32.

Le conteneur API démarre mais aucune table

Lancez la migration : docker compose exec api npx prisma migrate deploy.

La personne ne reçoit pas de DM

Vérifiez que le bot tourne, partage un serveur avec elle, et que ses DM sont ouverts. Un seul DM est envoyé par licence (anti-spam).

Changer de machine

+key hwid reset <key> (ou bouton dashboard). L'abonnement n'est pas relancé — le temps restant est conservé.