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.
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.
Architecture
Trois clients parlent à une API unique, chacun avec sa propre authentification. L'API est la seule à toucher la base.
| Surface | Auth | Utilisée par |
|---|---|---|
| Client (public) | X-Product-Key | Vos logiciels via le SDK |
| Admin | Authorization: Bearer (JWT) ou X-Api-Key | Dashboard, automations |
| Internal | X-Service-Token | Bot Discord |
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.
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 version2 · Envoyer le projet
# Depuis votre PC Windows (PowerShell)
scp -r C:\license-platform root@IP_DU_VPS:/opt/license-platform3 · 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 OWNER5 · 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).sqlProduits, 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.
Rôles & permissions (RBAC)
| Rôle | Peut… |
|---|---|
| OWNER | Tout — admins, clés API, produits, licences, logs |
| ADMIN | Produits + licences + logs + lecture des clés API |
| MODERATOR | Gérer les licences (générer, éditer, ban, reset HWID) |
| SUPPORT | Consultation (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.
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.
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 = null — aucun temps ne descend |
| 1re activation | expiresAt = maintenant + 30 j — le décompte démarre |
| Reset HWID | La machine change, mais l'expiration est conservée (l'abonnement ne repart pas de zéro) |
| Lifetime | durationSeconds = 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.
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'attributionDepuis 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é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
API — endpoints
Base : {API_BASE_URL}/api/v1. Erreurs : { "error": { "code", "message" } }.
Client (logiciel) · X-Product-Key
| Méthode | Endpoint | Rôle |
|---|---|---|
| POST | /license/activate | Active + lie le HWID |
| POST | /license/validate | Vérifie (heartbeat) |
| POST | /license/deactivate | Libère le HWID |
| POST | /license/info | Infos publiques minimales |
Admin · JWT / X-Api-Key / X-Service-Token
| Méthode | Endpoint | Rôle min. |
|---|---|---|
| POST | /auth/login | — |
| GET | /stats | SUPPORT |
| GET | /products · /products/:id | SUPPORT |
| POST PATCH DEL | /products… | ADMIN |
| GET | /licenses · /licenses/lookup/:key | SUPPORT |
| POST | /licenses (générer) | MODERATOR |
| PATCH DEL | /licenses/:id | MODERATOR |
| POST | /licenses/:id/hwid/reset · /ban · /unban | MODERATOR |
| GET | /licenses/notifications/expired | SUPPORT |
| GET | /logs | ADMIN |
| GET POST DEL | /admins · /apikeys | OWNER |
# 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"}'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.
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.
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).
Variables d'environnement
| Variable | Description |
|---|---|
DATABASE_URL | Chaîne de connexion PostgreSQL |
JWT_SECRET | Signe les JWT du dashboard (≥ 32 car.) |
LICENSE_SIGNING_SECRET | Signe (HMAC) les réponses de licence |
INTERNAL_SERVICE_TOKEN | Jeton du bot vers l'API admin |
CORS_ORIGINS | Origines autorisées (dashboard) |
API_BASE_URL / NEXT_PUBLIC_API_BASE_URL | URL 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_INTERVAL | Fréquence de vérif. des expirations (DM), ms (min 60000) |
BOOTSTRAP_ADMIN_* | Premier admin OWNER (seed) |
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é.