fix: carte Licence — erreurs nommées, clé révoquée affichée, édition indépendante du jeton d'activation, machines n/limite, dates #347
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#347
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Après #270 et #346, la carte Licence garde cinq défauts :
C'est l'écran qu'un acheteur voit quand sa clé pose problème.
Décisions
features[]suivent la seule clé ; le jeton sert à l'état « Activée » de la carte et aux cours du Bilan, que le serveur vérifie. D12 remplace D10 (jeton expiré de cette machine = absent), qui en devient un cas particulier. Motif : unelicense.keycopiée seule garde déjà son édition (état pré-activation toléré), donc la règle ne protégeait rien et ne pénalisait que la restauration légitime des données.useLicenseMachines, avec le message existantlicense.errors.licenseRevokedposé par #270. Aucune clé i18n nouvelle.count,activating,deactivating,notActivated), deux retirées (removeLicense,activateError). Pas de commande de suppression de licence.license_expired), et « Sans échéance » s'affiche au-delà de 50 ans.Contexte vérifié (2026-09-29)
Relevé sur
main5d549d6et, côté serveur, surmaximus-apimain3673d09. Se repérer aux noms de fonctions.Client,
src-tauri/src/commands/license_commands.rset la carte :strip_prefix,validate_with_key(« Invalid license: {erreur jsonwebtoken} », « Unknown edition »),store_licenseetread_licenserenvoient des chaînes anglaises.LicenseCard.tsxaffichevalidationErroret l'erreur de chargement (licenseStatus === "error") telles quelles ;LicenseContextgarde la chaîne brute.validate_with_key:validate_license_keyetstore_licensela renvoient,read_licensefait.ok(),resolve_license_entitlementsfaitlet Ok(..) else.LicenseError(posé par #270) porte déjàLicenseExpired,StorageetInternal; son commentaire annonce l'extension de #347.resolve_license_entitlementsretombe en Gratuite dès qu'un jeton existe et ne valide pas (strict_validation(),leeway = 0). Un jeton absent est toléré : le testmissing_activation_token_keeps_edition_and_featuresprouve qu'une clé copiée seule garde son édition. La contrainte 2 de l'ADR 0017 (« unelicense.keycopiée sur une autre machine est déclassée enfree») est donc inexacte.get_activation_statusvalide strictement : un jeton expiré ou d'une autre machine s'affiche « non activé », et le bouton « Activer » apparaît. Mais la section Machines est masquée en Gratuite (hasLicense), d'où l'impasse de #360.LicenseInfo.machine_limitest un claim signé, disponible localement.license.machines.count,.activating,.deactivatinget.notActivatedexistent sans être utilisées ;license.removeLicenseetlicense.machines.activateErrorsont mortes.toLocaleDateString()est appelé sans locale (formatExpiryet la date d'activation d'une machine). Convention du dépôt :i18n.language === "fr" ? "fr-CA" : "en-CA", écrit en ligne.jsonwebtoken9.3.1 vérifie la présence des claims exigés indépendamment devalidate_exp.license_revokedaffiche déjà « Cette clé de licence a été révoquée. » là où le refus arrive (#270) : sous l'en-tête Machines pour activate, dans la liste pour verify. Le message disparaît quand on referme la liste, l'édition garde sa coche verte, et « Activer » reste cliquable alors que le serveur répondra 403 à chaque essai.balance.priceFetching.errors.authFailed(« Échec d'authentification — vérifie ta licence »), sans indiquer qu'il suffit de réactiver la machine.Serveur,
maximus-api:signActivationJwt,src/crypto/ed25519.ts) ; #63 n'y a pas touché. Claims :product,machine_id,jti,sub(UUID de la licence),iat,exp.LICENSE_DURATION_DAYS = 36525pour/generateet pour Stripe, soit unexpvers 2126, au-delà de 2^32.license_revoked, même pour une machine active.{valid:false, reason: "revoked" | "not_found" | "expired"}.no_active_activation(la révocation désactive toutes les machines), que le client traite comme un succès./v1/pricesvalide le jeton strictement (exp,jtirévoqué, licence révoquée) : il reste la seule barrière dure./v1/licenses/*.Tâches
Erreurs locales nommées (Rust). Étendre
LicenseErroravecInvalidPrefix,InvalidSignature,MalformedetUnknownEdition, et réutiliserLicenseExpired,StorageetInternal.validate_with_keyrenvoieResult<LicenseInfo, LicenseError>, à sémantique inchangée : mêmes clés acceptées, mêmes refus. Correspondance :strip_prefix→InvalidPrefix;ErrorKind::ExpiredSignature→LicenseExpired;ErrorKind::InvalidSignature→InvalidSignature;Malformed;basenipremium→UnknownEdition;Internal.validate_license_key,store_licenseetread_licenserenvoient le JSON vialicense_error_to_string; leurs erreurs de fichier deviennentStorage.Côté TS. Ajouter les codes à
LicenseErrorCodeet àLICENSE_ERROR_I18N_MAP, et les clés FR/ENlicense.errors.invalidPrefix,invalidSignature,malformedetunknownEdition, au vouvoiement.LicenseCard.tsxpassevalidationErroret l'erreur de chargement parparseLicenseError, puis part(...); le contexte garde la chaîne brute.L'édition ne dépend plus du jeton (D12, ferme #360). L'édition et les
features[]se résolvent depuis la seule clé : une clé valide et non expirée donne son édition, quel que soit le jeton (absent, expiré, d'une autre machine, illisible) ; une clé invalide ou expirée donne("free", []).resolve_license_entitlementsne prend plus le jeton ni l'identifiant machine, etcurrent_entitlementsne les lit plus. Pour tester au point de décision, extraire la lecture de fichiers decurrent_entitlementsdans une fonction qui prend le dossier de données, testée avecstd::env::temp_dir()comme danstoken_store.rs.Le jeton garde une validation stricte là où il décide de quelque chose :
get_activation_status(« Activée » / « Non activée ») etpersist_activation_token(refus d'un jeton serveur qui ne vaut pas pour cette machine). Mettre à jour les commentaires qui décrivent l'ancienne règle : en-tête du module,get_edition,current_entitlements,machine_id_internal.Conséquence voulue : après une réinstallation avec restauration des données, la clé garde son édition, et la section Machines reste visible avec « Non activée » et « Activer ». L'activation écrase l'ancien jeton ; elle consomme une place, et la limite ouvre la liste comme aujourd'hui. Avec un jeton expiré de cette machine, « Activer » renouvelle le jeton sans consommer de place.
Clé révoquée (D13). Ajouter
revoked: booleanàLicenseMachinesState, posé par toutlicense_revoked(ACTIVATE_ERROR,LIST_LOAD_ERROR,DEACTIVATE_ERROR), jamais remis à zéro pendant la session, jamais persisté. La carte affichelicense.errors.licenseRevokedà son niveau, visible liste fermée ; une icône d'alerte remplace la coche verte de l'édition, etselectCanActivaterenvoiefalse. L'édition suit toujours la validation locale (ADR 0017).Machines n/limite. Une fois la liste chargée sans erreur, afficher
license.machines.countavec{count: machines.length, limit: info.machine_limit}.Clés orphelines (D5).
license.machines.activatingetdeactivatingservent de libellés aux boutons pendant le chargement ;license.machines.notActivateds'affiche quand la machine n'est pas activée ;license.removeLicenseetlicense.machines.activateErrorsont retirées des deux langues ;license.errors.unknownreste le seul repli générique.Échéance (D9). Fonction pure dans
src/shared/licenseExpiry.ts: au-delà de 50 ans, « Sans échéance » / « No expiry date » (clélicense.noExpiry) ; sinon, la date dans la langue de l'app.Dates.
formatExpiryet la date d'activation d'une machine :toLocaleDateString(i18n.language === "fr" ? "fr-CA" : "en-CA").Cours du Bilan. Reformuler
balance.priceFetching.errors.authFailed(FR/EN) pour indiquer d'activer la machine dans Paramètres → Utilisateurs, dans le registre des messages voisins. Le même 401 couvre un jeton absent, expiré ou invalide.Tests.
rejects_*existants vérifient la variante, plus seulementis_err();rejects_expired_licensereste vert et vérifieLicenseExpired.license.key) : clé expirée → Gratuite sans features ; clé valide avec unactivation.tokenexpiré de cette machine, d'une autre machine ou illisible → édition et features conservées. Ces tests remplacentmachine_match_keeps_edition_and_features,machine_mismatch_downgrades_to_free_and_drops_featuresetmissing_activation_token_keeps_edition_and_features, dont le sujet disparaît.validate_activation_with_keyrefuse un jeton expiré et un jeton d'une autre machine.exp = now + 36525 * 86400est acceptée, etexpires_atvaut cette valeur ; la même clé avec unexppassé est refusée.license.errors.*présentes en FR et en EN (test existant) ; seuil « Sans échéance », dont une clé de 100 ans ; transitionrevokeddu reducer etselectCanActivate.LicenseCard(renderToStaticMarkup, contexte et hook remplacés par des faux, commeUpsellCtas.render.test.tsx) : aucune erreur anglaise brute sur la carte en français, message révoqué visible liste fermée, « n/limite », « Sans échéance ».Documentation.
CHANGELOG.mdetCHANGELOG.fr.md: Fixed / Corrigé (l'édition ne dépend plus du jeton d'activation, qu'il soit expiré ou issu d'une réinstallation ou d'une migration de poste ; erreurs de saisie nommées ; dates dans la langue de l'app) et Changed / Modifié (clé révoquée affichée, compteur de machines, « Sans échéance »).docs/guide-utilisateur.md§12 : nouvelle sous-section sur la carte Licence (activation et réactivation pour les cours, clé révoquée, compteur de machines, « Sans échéance »), avec son pendant dans les docs intégrées (docs.editions.*, FR/EN).docs/architecture.md, §license_commands.rs: règle de résolution (la clé seule) et rôle du jeton (état de la carte, serveur).features[]signés qui suivent la clé.Fichiers concernés
src-tauri/src/commands/license_commands.rssrc/services/licenseService.ts,src/services/licenseService.test.tssrc/hooks/useLicenseMachines.ts,src/hooks/useLicenseMachines.test.tssrc/shared/licenseExpiry.ts,src/shared/licenseExpiry.test.ts(nouveaux)src/components/settings/LicenseCard.tsx,src/components/settings/LicenseCard.render.test.tsx(nouveau)src/i18n/locales/fr.json,src/i18n/locales/en.jsondocs/adr/0017-feature-gating-par-tier.md,docs/guide-utilisateur.md,docs/architecture.mdCHANGELOG.md,CHANGELOG.fr.mdCritères d'acceptation
license.errors.*.("free", []).license_revokedreçu du serveur reste affiché au niveau de la carte pendant la session, et « Activer » est désactivé.fr-CA/en-CA).license.removeLicenseetlicense.machines.activateErrorn'existent plus ;activating,deactivating,notActivatedetcountsont utilisées.cargo test,npm testetnpm run buildverts ; CHANGELOG FR/EN ; guide §12, docs intégrées et ADR 0017 à jour.Points de vigilance
expetiatexigés,leeway = 0, CWE-613). Ne jamais lui appliquer une validation qui n'impose pasexp.get_activation_statusetpersist_activation_tokengardent la validation stricte (signature,machine_id,exp) : ne pas les assouplir.validate_with_key.license.errors.unknownreste le seul repli générique (#270) ;activateErroraffichait « Activation failed » en anglais.fr.json,en.json, les deux CHANGELOG, le guide §12 et l'ADR 0017 ont été modifiés par #346, mergée : partir demain.Hors périmètre
/v1/pricesne vérifie pas que la machine est encore active : une machine désactivée garde les cours jusqu'à l'expiration de son jeton. À traiter dans maximus-api./issue-licenseà 36525 jours une fois maximus-api#63 déployée, et essai d'une vraie clé émise après #63.Liens
Spec source
spec-plan-paiements.md(séance/plan-rundu 2026-09-24, fichier de travail ignoré par git), puis analyse/analyzedu 2026-09-29. Le corps du 09-24 est archivé en commentaire. Ce corps se suffit à lui-même.bug: carte Licence — erreurs en anglais brut, clé expirée ou révoquée muette, limite de machines jamais affichéeto fix: carte Licence — erreurs de saisie nommées, clé révoquée explicite, jeton d'activation expiré toléré, machines n/limite, datesCorps du 2026-09-24 (séance
/plan-run, revu en/review-spec), archivé avant la réécriture par l'analyse du 2026-09-29. Deux décisions nouvelles le modifient : D12 (le jeton d'activation ne fait plus jamais retomber en Gratuite, ce qui ferme #360 et englobe D10) et D13 (clé révoquée : drapeau de session, message existant de #270). Le reste du plan tient, vérifié contremain5d549d6etmaximus-apimain3673d09.Titre et corps d'origine
Titre : fix: carte Licence — erreurs de saisie nommées, clé révoquée explicite, jeton d'activation expiré toléré, machines n/limite, dates
Après #270, les refus du serveur sont nommés. Il reste quatre problèmes dans la carte Licence :
La revue a aussi trouvé un défaut de fond : une machine qui a cliqué « Activer » retombe en Gratuite, sans message, au bout de 2 ans, parce que le jeton d'activation expire avant la clé. C'est l'écran qu'un acheteur voit quand sa clé pose problème.
Contexte vérifié (2026-09-24)
Numéros de ligne relevés sur
main. #270 et #346 les déplacent : se repérer aux noms de fonctions.LicenseCard.tsxaffiche telles quelles des erreurs locales en anglais (validationErrorl.182-187, erreur de chargement l.143-148) :strip_prefix(license_commands.rs:94) : « License key must start with SR-BASE- or SR-PREMIUM- » ;validate_with_key(l.122) : « Invalid license: {erreur jsonwebtoken} », par exempleExpiredSignatureouInvalidSignature;store_license(l.170-179) : erreurs d'I/O.LicenseContext.submitKey(l.138-147) garde la chaîne brute dansvalidationError.Les appelants de
validate_with_keyignorent tous le détail de l'erreur :validate_license_keyetstore_licensela renvoient,read_licensefait.ok(),resolve_license_entitlementsfaitlet Ok(..) else. Son type d'erreur peut donc devenirLicenseErrorsans rien changer à la résolution d'édition.Jeton d'activation :
maximus-api/src/crypto/ed25519.ts:256,signActivationJwt,2 * 365 * 24 * 3600), alors que la clé dure 3650 jours ;strict_validation()(license_commands.rs:141-155), qui appliqueexpavecleeway = 0;resolve_license_entitlements(l.249-266) retombe en Gratuite dès qu'un jeton existe et ne valide pas.Résultat : une machine activée perd son édition au bout de 2 ans, alors qu'une machine jamais activée la garde (état « pré-activation » toléré, l.258-264).
get_activation_status(l.535-558) valide aussi strictement. Un jeton expiré s'affiche donc « non activé », et le bouton « Activer » réapparaît. Le serveur renouvelle le jeton d'une machine déjà activée (brancheexistingdelicenseService.ts) sans consommer de place.LicenseInfo.machine_limit(l.44) est un claim signé, disponible localement.Clés i18n inutilisées :
license.machines.count(« {{count}}/{{limit}} machines activées »),.notActivated,.activatinget.deactivatingsont à brancher ;license.removeLicenseetlicense.machines.activateErrorsont à retirer.Dates :
toLocaleDateString()est appelé sans locale (LicenseCard.tsx:109et l.283), donc dans la langue du système. Convention du dépôt :i18n.language === "fr" ? "fr-CA" : "en-CA", écrit en ligne (BalanceOverviewCard.tsx:65,FilePreviewTable.tsx:36).Les clés vont passer à 100 ans (maximus/maximus-api#63) : « Expire le 24/09/2126 » n'aurait pas de sens.
Tâches
Étendre
LicenseError(posé par #270) aux erreurs locales, avec une variante par sens :InvalidPrefix(strip_prefix) ;InvalidSignature(signée par une autre clé) ;Malformed(JWT illisible, claims manquants) ;LicenseExpired, réutilisée pourErrorKind::ExpiredSignature;UnknownEdition;Storage, réutilisée pour l'écriture du fichier.validate_with_keyrenvoieResult<LicenseInfo, LicenseError>, à sémantique inchangée : mêmes clés acceptées, mêmes refus.validate_license_keyetstore_licenserenvoient le JSON vialicense_error_to_string. Côté TS : codes ajoutés àLicenseErrorCodeet àLICENSE_ERROR_I18N_MAP, et clés FR/ENlicense.errors.invalidPrefix,invalidSignature,malformed,unknownEdition, au vouvoiement.LicenseCard.tsx:validationErroret l'erreur de chargement (licenseStatus === "error") passent parparseLicenseError, puis part(...). Le contexte garde la chaîne brute.Jeton d'activation expiré toléré (D10), dans
resolve_license_entitlements:machine_idcorrespond à cette machine mais dontexpest dépassé, est traité comme absent : l'édition de la clé s'applique ;Utiliser une validation dédiée au jeton d'activation, pour ce seul usage (
expexigé mais non appliqué).strict_validation()etvalidate_with_keyrestent stricts pour la clé.get_activation_statusreste strict aussi : la carte affiche « Non activée » et le bouton « Activer », car les cours du Bilan exigent un jeton valide côté serveur. Consigner la règle en une ligne dans l'amendement de l'ADR 0017 posé par #346.Clé révoquée : quand activate ou verify renvoie
license_revoked, la carte affiche un état dédié « Cette clé a été révoquée » (clélicense.revoked). Cet état porté paruseLicenseMachines(#270) ne dure que la session : rien n'est persisté, et l'édition suit toujours la validation locale (ADR 0017).Machines : une fois la liste chargée, afficher
license.machines.countavec{count: machines.length, limit: info.machine_limit}.Clés orphelines (D5) :
license.machines.activatingetdeactivatingservent de libellés aux boutons pendant le chargement ;license.machines.notActivateds'affiche quand la machine n'est pas activée ;license.removeLicenseetlicense.machines.activateErrorsont retirées des deux langues ;license.errors.unknownreste le seul repli générique.Échéance : si
expires_atest à plus de 50 ans, afficher « Sans échéance » / « No expiry date » (clélicense.noExpiry) au lieu de la date. Sinon, afficher la date dans la langue de l'app. La règle vit dans une fonction pure desrc/shared/licenseExpiry.ts, testée.Dates (
formatExpiry, date d'activation d'une machine) :toLocaleDateString(i18n.language === "fr" ? "fr-CA" : "en-CA").Tests :
resolve_license_entitlements:expired_license_resolves_free_without_features; jeton d'activation expiré de cette machine → édition et features conservées ; jeton expiré d'une autre machine → Gratuite sans features ; jeton mal signé → Gratuite.rejects_expired_licensereste vert.license.errors.*présentes en FR et en EN ; seuil « Sans échéance » ; transition « révoquée » du reducer deuseLicenseMachines.Documentation :
CHANGELOG.mdetCHANGELOG.fr.md(Fixed / Corrigé) : jeton d'activation expiré, erreurs de saisie, dates ;docs/guide-utilisateur.md§12 : clé révoquée, compteur de machines, « Sans échéance », réactivation pour les cours ;docs/architecture.md, §license_commands.rs: règle du jeton expiré.Fichiers concernés
src-tauri/src/commands/license_commands.rssrc/services/licenseService.tssrc/services/licenseService.test.tssrc/hooks/useLicenseMachines.tssrc/hooks/useLicenseMachines.test.tssrc/shared/licenseExpiry.ts(nouveau)src/shared/licenseExpiry.test.ts(nouveau)src/components/settings/LicenseCard.tsxsrc/i18n/locales/fr.jsonsrc/i18n/locales/en.jsondocs/adr/0017-feature-gating-par-tier.mddocs/guide-utilisateur.mddocs/architecture.mdCHANGELOG.mdCHANGELOG.fr.mdDepends on
Critères d'acceptation
license.errors.*.license_revokedreçu du serveur affiche l'état révoqué (FR/EN).fr-CA/en-CA).license.removeLicenseetlicense.machines.activateErrorn'existent plus ;activating,deactivating,notActivatedetcountsont utilisées.cargo test,npm testetnpm run buildverts ; CHANGELOG FR/EN ; guide §12 à jour.Review caveats
license.errors.unknown.activateErroraffichait « Activation failed » en anglais, la chaîne même que #270 interdit, et sur des échecs qui ne sont pas des activations.machine_id(CWE-863). Ne jamais réutiliser la validation sansexppour la clé de licence (CWE-613).resolve_license_entitlements, pas seulementvalidate_with_key.mainle 2026-09-24 ; #270 et #346 les déplacent.Décisions prises en planification
count,activating,deactivating,notActivated), deux retirées (removeLicense,activateError). Pas de commande de suppression de licence.license_expired), et « Sans échéance » s'affiche au-delà de 50 ans.fr.json,en.json, les deux CHANGELOG, le guide §12 et l'amendement de l'ADR 0017.Hors périmètre
Spec source
spec-plan-paiements.md(séance/plan-rundu 2026-09-24). C'est un fichier de travail ignoré par git : ce corps se suffit à lui-même.fix: carte Licence — erreurs de saisie nommées, clé révoquée explicite, jeton d'activation expiré toléré, machines n/limite, datesto fix: carte Licence — erreurs nommées, clé révoquée affichée, édition indépendante du jeton d'activation, machines n/limite, dates