feat(licence): retirer la clé enregistrée — arrêter l'envoi quotidien d'une clé révoquée #370
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#370
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?
Contexte
Depuis #365 (PR #368, mergée le 2026-10-03), l'app vérifie au démarrage, au plus une fois par 24 h, que la clé enregistrée n'a pas été révoquée. À la revue, Max a décidé qu'une clé connue comme révoquée reste contrôlée tant qu'elle est enregistrée, pour reprendre une correction faite côté serveur (décision 3 du 2026-10-03, consignée dans l'amendement #365 de l'ADR 0017).
Or aucune interface ne retire une clé enregistrée. Aucune commande Rust ne supprime
license.key; seulsstore_license_inet la désactivation supprimentactivation.token. Un acheteur remboursé qui continue en Gratuite envoie donc sa clé révoquée au serveur une fois par 24 h, sauf s'il entre une autre clé ou supprime le fichier à la main. Le guide le dit tel quel depuis #365.Historique : la traduction
license.removeLicenseexistait sans bouton ni commande ; la décision D5 du/plan-rundu 2026-09-24 l'a retirée et #347 l'a supprimée. Cette décision précède #365 : la raison d'offrir le retrait est nouvelle.Analyse (
/analyze, 2026-10-03,main0e4fef5)Prémisse confirmée dans le code. Points établis :
entitlements_from_dirn'a besoin d'aucun changement : sanslicense.key, il rend déjà("free", [])(license_commands.rs:368-376), etcheck_revocation_inrendno_licensesans rien envoyer (L577-579).license_commands.rsne litlicense.key.activation.tokenest aussi lu par la route des cours (balance_commands.rs:102-111) : le retirer coupe les cours, ce qui est voulu.LicenseCard.tsx:30,:171) et le serveur a déjà libéré les places (revokeLicense). La question de la place ne se pose que pour une clé valide et activée.refresh(),LOAD_DONEarrive avecinfo: nulletrevokedvautfalse(LicenseContext.tsx:244) ; la carte se remonte enno-license(licenseIdentity,LicenseCard.tsx:18-21) etuseLicenseMachinesrepart de zéro. Un Premium de compte Maximus reste résolu pargetEdition.window.confirm(t(...))(TemplateActions.tsx:61,AccountsPage.tsx:126,ProfileFormModal.tsx:91, etc.).Décisions
license.checkest conservé (décision de l'analyse). Il est indexé par le condensé de la clé, donc il ne gêne aucune autre clé. Le supprimer rendrait son édition à une clé révoquée retirée puis ressaisie, jusqu'au contrôle suivant : un contournement mis à la portée de tous par un bouton. Avec #373, une clé ressaisie est de toute façon recontrôlée si le contrôle est dû.Point de conception. Depuis #350,
ActivationStatusne porte plus d'identifiant de machine. Le front connaîtis_activatedsans requête (useLicenseMachines,loadActivationStatus) et choisit avec lui le texte de confirmation etrelease_slot; l'identifiant à désactiver reste côté Rust. Par exemple,remove_license(release_slot: bool): avecrelease_slot, lire l'identifiant du jeton valide pour ce poste (stored_token_machine_id), le désactiver (deactivate_in), puis supprimer les fichiers. Un échec réseau rend une erreur codée sans rien supprimer, et le front repose la question puis rappelle avecrelease_slot: false. Une forme en deux commandes est acceptable si elle garde ces règles.Travail à faire
remove_license_in(dir, ...)sansAppHandle(modèlestore_license/store_license_impl/store_license_in), commanderemove_licenseenregistrée danslib.rs. Supprimelicense.keyd'abord (c'est ce qui arrête l'envoi), puisactivation.tokenvia le helper partagéremove_activation_token_in(voir « Décisions »), gardelicense.check. Une suppression qui échoue rendstorage; aucun état où le jeton est parti et la clé reste. Idempotente sans clé ;storagesi un fichier ne peut pas être supprimé. Désactivation préalable comme décrit ci-dessus,note_revocationsur la clé envoyée si le serveur répondlicense_revoked.removeLicensedanslicenseService.ts(à côté destoreLicense) ; orchestration dans une fonction exportée à dépendances injectéesperformRemoveLicense(deps)(confirm,removeLicense,refresh,logInfo,logWarn, étatis_activated/revoked), sur le modèle deperformSubmitKey; bouton « Retirer la clé » dansLicenseCard, qui passewindow.confirmett(...), visible quandinfo !== null(clé enregistrée, valide ou révoquée), pas selonhasLicense(edition !== "free", qui le cacherait pour une clé révoquée) ; une ligne au Journal par retrait (logInfo), et par désactivation préalable échouée (logWarn).license.*(bouton, deux ou trois textes de confirmation, avertissement de place occupée, erreur nommée).docs/guide-utilisateur.md:566) etdocs.editions.tips(fr.json/en.json:1130), où « Seul l'enregistrement d'une autre clé arrête l'envoi de la précédente » devient le retrait de la clé ;docs/architecture.md(liste des commandes L340-348, et L350/L354) ; ADR 0017, amendement #365 (L19 : « aucune interface ne retire une clé enregistrée ») ;CHANGELOG.mdetCHANGELOG.fr.md(Added/Ajouté).Surface de test
store_*(L1629-1700),missing_key_file_resolves_free(L1608),a_revoked_key_resolves_free_without_features(L2159),nothing_is_sent_without_a_valid_stored_key_nor_before_24_hours(L2325),deactivation_drops_the_token_that_stood_for_the_freed_slot(L2002), helperverify_route(L2112) ; TSLicenseContext.test.ts,LicenseCard.render.test.tsx,licenseService.test.ts.license.keyetactivation.tokensupprimés,license.checkintact, rien d'autre ne bouge.entitlements_from_dirrend("free", [])etcheck_revocation_inrendno_licensesans requête (un seul mock attrape-toutexpect(0), sans mock valide à côté, cf. la sélection des mocks de mockito).release_slot: la désactivation part avec l'identifiant du jeton (vérifié parmatch_request), puis les fichiers partent ; un échec réseau ne supprime rien et rend une erreur codée.performRemoveLicense: chaque ligne de la table d'erreurs ; la seconde confirmation n'apparaît que pournetwork,server,rate_limit_exceeded,invalid_response.license.key→storage, jeton intact.type:feature), mutations attendues sur chaque règle ci-dessus.Critères d'acceptation
license.checksi aucune autre clé n'a été contrôlée entre-temps, sinon par le contrôle à la saisie de #373 (en ligne). Hors ligne après le contrôle d'une autre clé, elle garde son édition jusqu'au contrôle suivant (fail-open assumé, ADR 0017).cargo test,npm testet la CI verts.Vérification manuelle de #346 : faite le 2026-10-04
Faite par Max sur la v0.16.0 (Linux) sans attendre cette issue : l'édition Gratuite venait de la clé révoquée de l'achat de contrôle (#365), et non d'un retrait de clé. Sélecteur de profils, création d'un profil, écran verrouillé dans le modal (avec la ligne qui nomme la révocation), puis « J'ai déjà une clé » : le modal se ferme et la carte Licence s'affiche, sans modal resté par-dessus. La seule mutation de #346 que ses tests ne gardent pas (
ProfileFormModalsansonNavigate={onClose}, PR #362) est donc couverte, et cette issue n'a plus à l'attendre.Complexité estimée
Moyenne (Rust + front + docs, la désactivation préalable ajoute un chemin réseau).
Ordre
Après #373 : même fichier
LicenseContext.tsx, même amendement de l'ADR 0017. Avant ou après #367 indifféremment, mais pas en parallèle : les trois touchentlicense_commands.rset la carte Licence.Liens
license.removeLicense, D5 du 2026-09-24)Fichiers concernés
src-tauri/src/commands/license_commands.rs(remove_license_in, commande, helperremove_activation_token_inpartagé avec #367),src-tauri/src/lib.rs(enregistrement)src/services/licenseService.ts(+licenseService.test.ts)src/contexts/LicenseContext.tsx(+LicenseContext.test.ts) ousrc/hooks/useLicenseMachines.tspourperformRemoveLicensesrc/components/settings/LicenseCard.tsx(+LicenseCard.render.test.tsx)src/i18n/locales/fr.json,en.json;docs/guide-utilisateur.md,docs/architecture.md,docs/adr/0017-feature-gating-par-tier.md;CHANGELOG.md,CHANGELOG.fr.mdDepends on
Décisions prises en planification
/plan-rundu 2026-10-03, revue/review-spec(3 experts).release_slot) :license_revoked(avecnote_revocationsous la clé envoyée),license_not_found,no_active_activation→ la place est déjà libre, le retrait continue ;network,server,rate_limit_exceeded,invalid_response→ erreur codée, rien n'est supprimé, le front repose la question (« retirer quand même ? ») puis rappelle avecrelease_slot: false;stored_token_machine_idrendNone), ou clé connue comme révoquée → retrait sans appel réseau,release_slotignoré.license.key, puisactivation.token.pub(crate) fn remove_activation_token_in(dir) -> Result<(), LicenseError>qui tolèreNotFound, utilisé ici et par #367 ;store_license_inetdrop_token_of_deactivated_machine_inpeuvent l'adopter sans changer leur comportement d'erreur.Fixes #370: la vérification manuelle de #346, qui justifiaitRefset un passage enstatus:blockedaprès la PR, est faite depuis le 2026-10-04.Review caveats
hasLicenseexclut la clé révoquée → bouton surinfo !== null.release_slot→ table ci-dessus.license.checkne garde qu'une clé → critère borné au cas en ligne.main0e4fef5et dérivent dans la pile : se fier aux noms de fonctions, intitulés de paragraphes et clés i18n (décision de planification).Après merge (Max)
Spec source
spec-plan-suites-licence.md (local, ignoré par git)
Vérification manuelle de #346 faite le 2026-10-04 par Max, sur la v0.16.0 (Linux), sans attendre cette issue. L'édition Gratuite venait de la clé révoquée de l'achat de contrôle (#365), encore enregistrée sur le poste. Depuis le sélecteur de profils, la création d'un profil affiche l'écran verrouillé dans le modal, avec la ligne qui nomme la révocation ; « J'ai déjà une clé » ferme le modal et affiche la carte Licence, sans rien par-dessus. La mutation que les tests de #346 ne gardent pas (
ProfileFormModalsansonNavigate={onClose}, PR #362) est couverte.Corps mis à jour en conséquence : la section devient « Vérification manuelle de #346 : faite le 2026-10-04 », et la PR de cette issue portera
Fixes #370au lieu deRefs; plus de passage enstatus:blockedaprès la PR. Reste après merge le texte pour maximus/la-compagnie-maximus#184, rédigé dans le DAILY-REPORT et posté par Max.