Vendre des mises à jour : comment une licence décide des versions qu'elle peut exécuter
Une licence perpétuelle, c'est pour toujours ; « un an de mises à jour », non — et les deux vivent dans la même clé. La couverture de versions, c'est la manière dont une licence déclare quelles versions elle a le droit d'exécuter, sous forme d'octrois signés de plage de versions et de fenêtre temporelle que la build vérifie localement, hors ligne, sans réactivation. Voici comment se façonne un octroi de couverture, comment la date d'éligibilité (et non celle d'installation) décide d'un correctif, comment le bloc signé voyage avec le bail et comment votre build l'applique sans un seul appel réseau sur le chemin critique.
Une licence perpétuelle et une fenêtre de maintenance sont des promesses distinctes que les clients achètent couramment comme un seul SKU. « Perpétuelle, avec un an de mises à jour » signifie que la licence est valide pour toujours mais n’a droit qu’aux versions sorties la première année. Encodez cela comme une expiration et vous avez rompu la promesse de perpétuité : l’application cesse de fonctionner au bout d’un an. Ignorez-le et vous avez offert chaque version future contre un paiement unique.
Keyright sépare les deux. La validité — signature, siège, expiration, révocation — répond à cette licence est-elle bonne ? La couverture de versions répond à une autre question : cette licence peut-elle exécuter cette version ? La couverture est un ensemble d’octrois signés attachés à une licence, évalués localement dans votre build contre la version qu’il est, sans réactivation et sans réseau sur le chemin critique. Cet article explique comment cette mécanique est façonnée et comment vous la câblez dans une build .NET.
Un octroi est une plage de versions, une fenêtre temporelle, ou les deux
Un octroi de couverture est la plus petite unité de « vous avez droit à ces versions ». Il peut contraindre par version, par date, ou par les deux à la fois :
- Plage de versions —
[2.0.0, 3.0.0): toute version 2.x, rien à partir de la 3.0. C’est « une version majeure ». - Fenêtre temporelle — versions sorties entre deux dates. C’est « un an de mises à jour », exprimé comme les versions dont la date d’éligibilité tombe dans la fenêtre.
- Les deux — une plage et une fenêtre, en intersection : « versions 2.x sorties pendant votre première année ».
Vous ajoutez rarement des octrois bruts à la main. Un preset développe un terme en langage clair vers la bonne forme d’octroi — lifetime, updates-for-period, one-major, one-major-plus-upgrade-protection, subscription, subscription-with-fallback — de sorte que « un an de mises à jour à partir de l’achat » devienne un octroi de fenêtre temporelle ancré sur la date d’achat sans que vous calculiez quoi que ce soit.
# Apply a preset to a license; it expands into the concrete grants.
curl -X POST "$BASE/admin/licenses/$ID/coverage/preset" \
-H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
-d '{"preset":"updates-for-period","months":12,"from":"2026-10-10"}'
La date d’éligibilité est toute l’astuce
La partie subtile d’une fenêtre temporelle est quelle date mesure une version. Ce n’est pas la date d’installation du client, ni l’horloge système, ni l’horodatage du fichier. Chaque version que vous enregistrez porte une date d’éligibilité — la date contre laquelle la couverture est jugée — et un octroi de fenêtre temporelle couvre une version quand cette date d’éligibilité tombe dans la fenêtre.
# Register each release so coverage has a date to judge it by.
curl -X POST "$BASE/admin/products/acme-app/releases" \
-H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
-d '{"version":"3.1.0","entitlementDate":"2026-09-01","channel":"stable"}'
C’est surtout important pour les correctifs. Si la fenêtre de mises à jour d’un client s’est fermée le 1er août et que vous publiez 3.1.4 en octobre pour corriger un bug de 3.1.0 (qui était dans sa fenêtre), juger 3.1.4 par sa propre date de sortie la pousserait hors de la fenêtre et le priverait d’un correctif auquel il a manifestement droit. Un correctif hérite au contraire de la date d’éligibilité de sa version de base, donc 3.1.4 est jugé exactement comme 3.1.0 l’a été. La règle : le temps calendaire sur la machine du client n’entre jamais en jeu — seule la date d’éligibilité de la version compte.
Le bloc signé voyage avec le bail
La couverture n’est fiable que si la build ne peut pas se mentir à elle-même à son sujet, donc les octrois sont signés dans un bloc de couverture avec la même clé de locataire qui signe la licence — vérifiable hors ligne contre la clé publique compilée dans votre binaire. L’activation livre ce bloc et le met en cache à côté du bail, de sorte qu’au moment où votre application demande « suis-je couvert ? », la réponse est déjà sur disque. Pas de téléchargement séparé, pas de serveur de couverture, pas de réseau sur le chemin qui conditionne le démarrage.
L’appliquer dans une build .NET
Le paquet Keyright.NET embarque des cibles MSBuild qui estampillent l’identité de cette build dans l’assembly. Réglez-les depuis votre pipeline de publication pour que le binaire livré sache quelle version il est et contre quelle date il doit être jugé :
<PropertyGroup>
<KeyrightCoverageVersion>3.1.0</KeyrightCoverageVersion> <!-- this build -->
<KeyrightCoverageEntitlementDate>2026-09-01</KeyrightCoverageEntitlementDate> <!-- base release's date for a hotfix -->
<KeyrightCoverageChannel>stable</KeyrightCoverageChannel> <!-- or beta -->
<KeyrightRequireCoverage>true</KeyrightRequireCoverage> <!-- enforce; build fails (KR0001) if the date is missing -->
</PropertyGroup>
Au démarrage, vous demandez une fois. Le SDK lit le bloc en cache, décide localement, rafraîchit silencieusement si la réponse en cache est « non » (la couverture a pu être étendue depuis l’activation) et ne lève jamais d’exception pour un problème réseau :
using Keyright.Coverage;
if (CoverageBuildInfo.Current.RequireCoverage)
{
CoverageCheckResult check = await License.CheckCoverageAsync(ct: ct);
if (!check.IsCovered) // Covered and NotConfigured both pass
{
// check.Status: NotCovered (Reason = WindowLapsed → renew,
// OutsideVersionRange → upgrade entitlement)
// or UnavailableOffline (nothing cached, no network → import a coverage file)
RunReadOnlyOrBlock(check);
}
}
// For your own "update available" banner — local, no network, no gating:
bool canTake31 = License.IsCovered("3.1.0");
CoverageInfo cov = License.GetCoverage(); // the terms + support-until date
Que NotConfigured passe est délibéré : un produit qui n’a jamais configuré de couverture doit continuer à tourner, donc l’application est optionnelle par build et une licence sans octroi n’est jamais verrouillée par accident.
Activer l’application sans verrouiller ceux qui ont déjà payé
Le seul moment dangereux est la première fois que vous appliquez sur un produit qui vend des licences perpétuelles depuis des années : aucune ne porte d’octroi, donc un basculement naïf refuserait chaque client existant. Keyright conditionne cela à deux étapes :
# 1. How many licenses still have no coverage? This must reach zero first.
curl "$BASE/admin/products/acme-app/coverage/readiness" -H "X-Admin-Token: $TOKEN"
# 2. Give every legacy license a grant so enforcing is a no-op for them.
curl -X POST "$BASE/admin/products/acme-app/coverage/backfill" \
-H "X-Admin-Token: $TOKEN" -H "content-type: application/json" \
-d '{"grant":"lifetime"}' # or a support-end date, or CSV rows
Le backfill donne aux licences existantes un octroi legacy — à vie, une date de fin de support, ou des lignes par licence explicites d’un CSV — de sorte que l’application, une fois activée, ne change rien pour quiconque a acheté avant que vous ayez une couverture. Vous n’appliquez qu’après que la préparation signale zéro licence sans couverture. Un endpoint migrate avec dryRun prévisualise le gain avant d’appliquer un octroi à tout un produit.
En milieu isolé : la même réponse, sans jamais de réseau
Une machine qui ne peut jamais atteindre le service applique quand même la couverture, car l’évaluation est locale de toute façon. Exportez le bloc de couverture signé depuis le tableau de bord et importez-le sur la machine hors ligne :
License.ImportCoverageBlock(File.ReadAllText("acme.coverage.json"));
CoverageCheckResult check = await License.CheckCoverageAsync(ct: ct); // fully offline
L’import est la seule étape de couverture qu’effectue un client isolé, et il porte sa propre signature : un bloc altéré échoue la vérification et se résout en non couvert, pas en laissez-passer.
À retenir
Gardez la validité et la couverture séparées : l’une dit que la licence est bonne, l’autre quelles versions elle peut exécuter, et les confondre casse soit une promesse perpétuelle, soit offre les versions futures. Modélisez la couverture comme des octrois signés — une plage de versions, une fenêtre temporelle, ou les deux —, jugez chaque version par sa date d’éligibilité et non par une quelconque horloge de la machine du client, laissez les correctifs hériter de la date de leur version de base, et livrez le bloc signé avec le bail pour que la build décide localement et hors ligne. Avant d’appliquer, faites un backfill des licences antérieures à la couverture pour que le basculement ne coûte rien à vos clients existants. La récompense est ce que perpétuelle-plus-maintenance a toujours voulu être : une application qui continue de tourner pour toujours, et un flux de mises à jour qui s’arrête exactement là où s’arrête le droit du client — décidé dans le binaire, sans réactivation et sans rien sur le chemin critique.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.