Guide des créateurs
Protéger un script avec une licence
Chaque acheteur reçoit sa propre clé. Au démarrage, votre script la fait vérifier par BDKSHOP et s’arrête si elle est refusée : une copie partagée ou une commande remboursée ne fonctionne plus. Aucun serveur à installer de votre côté.
Comment ça marche
- Vous activez la clé de licence sur votre annonce et collez quelques lignes de code dans votre script.
- À chaque vente, BDKSHOP crée une clé du type
BDK-XXXX-XXXX-XXXX-XXXX, affichée sur la page de commande de l’acheteur. - L’acheteur la copie dans la configuration de votre produit (par exemple son
server.cfgou sonconfig.yml). - Au démarrage, votre script envoie la clé et un identifiant du serveur à BDKSHOP, qui répond si la licence est valide.
La clé fonctionne sur un nombre limité de serveurs, que vous choisissez. L’acheteur qui change de serveur clique sur « Changer de serveur » depuis sa commande, une fois par semaine au plus ; vous pouvez aussi réinitialiser une licence vous-même.
1. Activer la protection sur une annonce
- Dans Mon compte › Mes annonces, modifiez l’annonce, rubrique Protection anti-fuite.
- Choisissez Clé de licence et indiquez le nombre d’installations par licence (0 = illimité, 50 au plus).
- Enregistrez : le lien Code d’intégration et licences émises apparaît. Il donne l’identifiant produit de l’annonce (
prd_…), le code prêt à copier et la liste des clés vendues.
Indépendamment de la clé, chaque archive .zip ou .jar téléchargée porte l’empreinte invisible de la commande. Si votre création circule, la page Anti-fuite retrouve la commande d’origine à partir du fichier ; vous signalez alors la fuite en un clic et l’équipe révoque la licence correspondante.
2. Ajouter la vérification à votre script
Remplacez prd_votre_identifiant par l’identifiant produit de votre annonce. Sur la page de vos licences, le même code est déjà rempli avec le bon identifiant.
-- server.lua : vérification de la licence BDKSHOP au démarrage de la ressource.
-- Dans le server.cfg de l'acheteur : set bdkshop_licence "BDK-XXXX-XXXX-XXXX-XXXX"
local PRODUIT = 'prd_votre_identifiant'
CreateThread(function()
Wait(5000)
local cle = GetConvar('bdkshop_licence', '')
local serveur = GetConvar('web_baseUrl', '')
if serveur == '' then serveur = GetConvar('sv_hostname', 'serveur') end
local ressource = GetCurrentResourceName()
PerformHttpRequest('https://bdkshop.fr/api/v1/licenses/verify', function(code, body)
local ok, data = pcall(json.decode, body or '')
if code ~= 200 or not ok or type(data) ~= 'table' then
-- BDKSHOP injoignable : on n'arrête pas le serveur de l'acheteur pour autant.
print(('^3[%s] Vérification de licence impossible pour le moment (%s).^0'):format(ressource, tostring(code)))
return
end
if not data.valid then
print(('^1[%s] Licence refusée : %s^0'):format(ressource, data.message))
StopResource(ressource)
return
end
print(('^2[%s] Licence valide.^0'):format(ressource))
end, 'POST', json.encode({
product = PRODUIT, key = cle, identifier = serveur, label = GetConvar('sv_hostname', 'Serveur FiveM'),
}), { ['Content-Type'] = 'application/json', ['Accept'] = 'application/json' })
end)
// Plugin Paper / Spigot (Java 17+). Dans config.yml de l'acheteur : licence: "BDK-XXXX-XXXX-XXXX-XXXX"
private static final String PRODUIT = "prd_votre_identifiant";
@Override
public void onEnable() {
saveDefaultConfig();
verifierLicence();
}
private void verifierLicence() {
String cle = getConfig().getString("licence", "").replace("\"", "");
String json = "{\"product\":\"" + PRODUIT + "\",\"key\":\"" + cle + "\",\"identifier\":\"" + instance() + "\",\"label\":\"Serveur Minecraft\"}";
HttpRequest requete = HttpRequest.newBuilder(URI.create("https://bdkshop.fr/api/v1/licenses/verify"))
.header("Content-Type", "application/json").header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json)).build();
HttpClient.newHttpClient().sendAsync(requete, HttpResponse.BodyHandlers.ofString()).thenAccept(reponse -> {
if (reponse.statusCode() != 200) {
getLogger().warning("Vérification de licence impossible pour le moment (" + reponse.statusCode() + ").");
return;
}
if (!reponse.body().contains("\"valid\":true")) {
getLogger().severe("Licence refusée : le plugin est désactivé.");
getServer().getScheduler().runTask(this, () -> getServer().getPluginManager().disablePlugin(this));
}
}).exceptionally(erreur -> {
getLogger().warning("BDKSHOP injoignable : " + erreur.getMessage());
return null;
});
}
/** Identifiant stable de l'installation, créé au premier démarrage. */
private String instance() {
File fichier = new File(getDataFolder(), ".instance");
try {
if (!fichier.exists()) {
getDataFolder().mkdirs();
Files.writeString(fichier.toPath(), UUID.randomUUID().toString());
}
return Files.readString(fichier.toPath()).trim();
} catch (IOException e) {
return "port-" + getServer().getPort();
}
}
// Node.js 18+ (bot Discord, back-end de site). Clé de l'acheteur dans la variable BDKSHOP_LICENCE.
const os = require('node:os');
const PRODUIT = 'prd_votre_identifiant';
async function verifierLicence() {
try {
const reponse = await fetch('https://bdkshop.fr/api/v1/licenses/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
product: PRODUIT,
key: process.env.BDKSHOP_LICENCE ?? '',
identifier: os.hostname(),
label: 'Mon application',
}),
});
if (!reponse.ok) return console.warn(`Vérification de licence impossible pour le moment (${reponse.status}).`);
const data = await reponse.json();
if (!data.valid) {
console.error(`Licence refusée : ${data.message}`);
process.exit(1);
}
} catch (erreur) {
console.warn('BDKSHOP injoignable :', erreur.message);
}
}
verifierLicence();
curl -X POST https://bdkshop.fr/api/v1/licenses/verify \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"product":"prd_votre_identifiant","key":"BDK-XXXX-XXXX-XXXX-XXXX","identifier":"mon-serveur-1","nonce":"a1b2c3"}'
# Réponse
{"valid":true,"status":"active","message":"Licence valide.","licensee":"pseudo","activations":1,"max_activations":1,"product":"prd_votre_identifiant","nonce":"a1b2c3","timestamp":1760000000,"signature":"…"}
Une règle d’or : si BDKSHOP ne répond pas (coupure réseau, code HTTP différent de 200), laissez le script fonctionner. Vos acheteurs ne doivent jamais être pénalisés par une panne. Seule une réponse "valid": false doit arrêter le produit.
3. Référence de l’API
POST https://bdkshop.fr/api/v1/licenses/verify, corps en JSON (Content-Type: application/json). Aucune authentification : l’identifiant produit et la clé suffisent.
| Paramètre | Obligatoire | Description |
|---|---|---|
product | oui | Identifiant produit de l’annonce (prd_…). |
key | oui | Clé de l’acheteur. Les majuscules et les espaces autour ne comptent pas. |
identifier | oui | Identifiant stable du serveur ou de l’installation (3 à 200 caractères). Il n’est jamais stocké en clair, seulement son empreinte. |
label | non | Nom de l’installation montré à l’acheteur et à vous (80 caractères au plus). |
nonce | non | Valeur aléatoire renvoyée telle quelle, pour vérifier la signature (64 caractères au plus). |
La réponse est toujours un code HTTP 200 avec ce contenu (sauf requête mal formée : 422, ou trop de requêtes : 429) :
curl -X POST https://bdkshop.fr/api/v1/licenses/verify \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"product":"prd_votre_identifiant","key":"BDK-XXXX-XXXX-XXXX-XXXX","identifier":"mon-serveur-1","nonce":"a1b2c3"}'
# Réponse
{"valid":true,"status":"active","message":"Licence valide.","licensee":"pseudo","activations":1,"max_activations":1,"product":"prd_votre_identifiant","nonce":"a1b2c3","timestamp":1760000000,"signature":"…"}
status | valid | Signification | Ce que fait votre script |
|---|---|---|---|
active | true | Licence valide, installation enregistrée. | Fonctionne normalement. |
protection_disabled | true | Vous avez désactivé la clé sur l’annonce : toutes les clés sont acceptées. | Fonctionne normalement. |
invalid_key | false | Clé inconnue pour ce produit (faute de frappe, clé inventée). | S’arrête et affiche le message. |
revoked | false | Commande remboursée, ou licence révoquée après une fuite. | S’arrête. |
expired | false | Produit vendu par abonnement et abonnement terminé. | S’arrête et invite à se réabonner. |
activation_limit | false | Clé déjà utilisée sur le nombre maximum de serveurs. | S’arrête et invite à « Changer de serveur » depuis la commande. |
unknown_product | false | Identifiant produit inconnu (erreur dans votre code). | À corriger avant de publier. |
Limites : 60 vérifications par minute et par adresse IP, et 20 par minute pour une même clé. Une vérification au démarrage, puis toutes les quelques heures au plus, suffit largement.
4. Vérifier la signature (avancé)
Un acheteur malin pourrait détourner l’adresse de BDKSHOP vers un faux serveur qui répond toujours « valide ». Pour l’en empêcher, chaque réponse est signée : signature vaut le HMAC-SHA256 du texte valide|statut|produit|nonce|horodatage (valide vaut 1 ou 0), calculé avec le secret de votre annonce. Envoyez un nonce aléatoire, recalculez la signature et vérifiez que l’horodatage est récent.
// Node.js 18+ : vérification de la signature (protège contre une fausse réponse).
const crypto = require('node:crypto');
const PRODUIT = 'prd_votre_identifiant';
const SECRET = 'votre_secret'; // Mon compte › Anti-fuite › Licences de l'annonce › « Vérifier la signature des réponses »
async function verifierLicence(cle, identifiant) {
const nonce = crypto.randomBytes(16).toString('hex');
const reponse = await fetch('https://bdkshop.fr/api/v1/licenses/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ product: PRODUIT, key: cle, identifier: identifiant, nonce }),
});
const data = await reponse.json();
const texte = [data.valid ? '1' : '0', data.status, data.product, data.nonce ?? '', String(data.timestamp)].join('|');
const attendue = crypto.createHmac('sha256', SECRET).update(texte).digest('hex');
const recente = Math.abs(Date.now() / 1000 - data.timestamp) < 300;
if (data.nonce !== nonce || data.signature !== attendue || !recente) {
throw new Error('Réponse de licence non authentique.');
}
return data.valid;
}
Le secret se trouve sur la page des licences de l’annonce, rubrique « Vérifier la signature des réponses (avancé) ». Il est lisible par quiconque ouvre votre script : obfusquez la partie qui le contient, et générez-en un nouveau si vous pensez qu’il a fuité.
Bonnes pratiques
- Lisez la clé dans la configuration de l’acheteur, jamais écrite en dur dans le code.
- Utilisez un identifiant qui ne change pas à chaque redémarrage (nom du serveur, fichier créé au premier lancement), sinon chaque démarrage compte comme une nouvelle installation.
- Expliquez dans la description de l’annonce où l’acheteur doit coller sa clé.
- Affichez le
messagede la réponse dans la console : il est clair pour l’acheteur. - Ne bloquez jamais le serveur en cas d’erreur réseau.
Questions fréquentes
Que se passe-t-il en cas de remboursement ? La licence est révoquée automatiquement : la vérification suivante répond revoked.
Et pour un abonnement ? La clé reste la même à chaque renouvellement. À la fin de l’abonnement, elle répond expired après quelques jours de grâce.
Je retire la protection de mon annonce ? L’API répond alors « valide » à toutes les clés : les serveurs de vos acheteurs continuent de fonctionner.
Mon script est en Python, PHP ou autre ? Toute technologie capable d’envoyer une requête HTTP POST en JSON convient : inspirez-vous de la requête brute.
Une question qui n’est pas ici ? Utilisez « Signaler un bug ou une idée » en bas de page, ou contactez-nous.