bug: jeton d'activation d'une ancienne licence resté à côté d'une autre clé — carte « Activée », cours refusés, aucun moyen de réactiver #367
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#367
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
Un dossier de données écrit par la v0.15.0 ou une version antérieure peut contenir une clé de licence valide à côté du jeton d'activation d'une autre licence. Trouvé le 2026-10-02 par le plan checker de #365, et vérifié dans le code.
Comment l'état se crée : dans la v0.15.0,
store_licenseécrit la clé sans toucher àactivation.token(git show v0.15.0:src-tauri/src/commands/license_commands.rs). Saisir une clé, activer la machine, puis enregistrer une autre clé laisse donc le jeton de la première. #347 a corrigé la cause pour l'avenir (store_license_insupprime le jeton quand la clé change,license_commands.rs:234-247), mais ne répare pas un dossier déjà dans cet état. Le cas concret : un achat suivi d'un remboursement, puis une autre clé enregistrée avec la v0.15.0. Le jeton restant est celui d'une licence révoquée.Ce que l'utilisateur voit
activation_status_in(L1157) vérifie la signature, l'expiration et l'identifiant de la machine, et ce jeton les satisfait. Le client ne peut pas relier un jeton à une clé : lesubdu jeton est l'identifiant de la licence, la clé porte le courriel.license_revokedpour la licence du jeton, nommé depuis #365 (balance_commands.rs:171-181,balance.service.ts:2672-2674).Analyse (
/analyze, 2026-10-03,main0e4fef5)Prémisse confirmée, avec deux nuances sur la piste d'origine (réconcilier avec la liste des machines) :
is_this_machine→ supprimer le jeton » rate le cas où la machine est aussi activée sous la clé actuelle : une ligne est marquée, et le jeton de l'autre licence reste.machines_from_verify(L909-929) écarte en silence toute ligne qui ne se désérialise pas (filter_map(... .ok()), L924 ; unactivated_atànullsuffit). Une liste peut donc paraître complète sans l'être.license_revokedde la route des cours : le jeton qu'elle présente appartient à une licence révoquée et ne sert plus à rien, quelle que soit la clé enregistrée.Décision de Max (2026-10-03) : deux signaux, 403 des cours + liste.
license_revokedde/v1/prices→ suppression deactivation.token. Aucune écriture de l'état révoqué de la clé : la décision de #365 du 2026-10-02 tient, parce que ce refus juge la licence du jeton, pas la clé. Le jeton part dans tous les cas : si la clé enregistrée est elle-même révoquée, « Activer » le dira (license_revoked, enregistré parnote_revocationsous la clé envoyée) ; sinon, « Activer » répare. La carte repropose « Activer » au prochain chargement de l'état d'activation.list_machines_in(L999-1008), qui adir, la liste et les identifiants : après une réponseOk, le jeton est supprimé si aucune ligne ne nomme ce poste (names_this_machine, formes dérivée et brute, ADR 0020), et seulement si toutes ces gardes tiennent :idsvautSome) ; sinon aucune ligne n'est marquée et la suppression serait fausse ;machines_from_verify, qui rend désormais aussi le nombre de lignes écartées (l'affichage de la liste reste tolérant, pas d'erreurinvalid_response) ; seule la réconciliation lit ce compteur ;license_revokedou unrate_limit_exceeded.Cette règle couvre aussi une machine désactivée depuis un autre poste, qui garde aujourd'hui son jeton local (la route des cours ne vérifie pas qu'une machine est encore active, suivi côté maximus-api à ouvrir).
Côté front,
useLicenseMachinesrelit l'état d'activation (loadActivationStatus) aprèsLIST_LOAD_DONE, pour que « Activer » réapparaisse sans recharger la page. La liste reste chargée à la demande (budget de 5 req/min/IP) : aucune requête ajoutée.Travail à faire
FetchPriceError::LicenseRevoked, supprimeractivation.tokenseulement si le fichier vaut encore le jeton présenté dans la requête refusée (un 403 tardif ne doit pas effacer le jeton neuf d'une réactivation), viaremove_activation_token_in(helper de #370), best effort.read_stored_activation_tokenpasse en version_in(dir)et utilise la constanteACTIVATION_FILEau lieu du nom en dur.machines_from_verifyrend aussi le nombre de lignes écartées ; la réconciliation vit dans une fonction dédiée sansAppHandle(modèle :drop_token_of_deactivated_machine_in) appelée parlist_machines_in;list_activated_machinesrend{ machines, token_dropped: "not_listed" | null }.licenseService.tsetuseLicenseMachines.tssuivent le nouveau contrat ; relire l'état d'activation après le chargement de la liste ; après un refus des courslicense_revoked, le message invite à réactiver, et le messageauth(requêtes du même lot parties sans jeton) invite aussi à activer (i18n FR/EN).logInfo), à code fixe sur le modèle deREVOCATION_LOG(not_listed_for_this_machine,price_refused_revoked), sans jeton, identifiant de licence ni identifiant de machine (ADR 0020). Côté liste, déclenchée partoken_dropped; côté cours, par le codelicense_revoked.docs/architecture.md:354(« la route des cours nomme le refus sans rien écrire » devient « supprime le jeton, n'écrit pas l'état de la clé ») ; amendement #365 de l'ADR 0017 (paragraphe « Les autres sources ») ; ADR 0020 si la règle de suppression du jeton y est énumérée ;CHANGELOG.mdetCHANGELOG.fr.md(Fixed/Corrigé).Fichiers concernés
src-tauri/src/commands/balance_commands.rs: lecture du jeton (L102-111), 403 (L171-181).src-tauri/src/commands/license_commands.rs:machines_from_verify,flag_this_machine(L978),list_machines_in,activation_status_in.src/hooks/useLicenseMachines.ts,src/services/balance.service.ts, i18n.Surface de test
activation_status_*(L1715-1768),the_machine_list_flags_this_machine_under_either_identifier(L1936),deactivation_drops_the_token_that_stood_for_the_freed_slot(L2002),verify_answers_map_to_machines_or_codes(L2850),verify_revoked_key_is_a_refusal_not_an_empty_list(L3075),verify_rate_limit_and_html_outage(L3089) ;it_names_a_revoked_license_on_403(balance_commands.rs:451) ;useLicenseMachines.test.ts.type:bug) :license_revokeddes cours, le jeton est parti,license.checkn'est pas écrit, l'état lit « non activée ».idsàNone, erreur, 429 oulicense_revoked→ jeton gardé.match_requestde mockito écrit un autre jeton) → le nouveau jeton est gardé.token_droppedvautnot_listedseulement quand la réconciliation a supprimé le jeton.token_droppedet surlicense_revoked.Critères d'acceptation
cargo test,npm testet la CI verts.Complexité estimée
Moyenne.
Ordre
Les trois issues de licence (#373, #370, #367) touchent
license_commands.rs, la carte Licence ou l'ADR 0017 : à enchaîner, pas à paralléliser. #367 peut passer en dernier.Liens
is_this_machine, ADR 0020), #370 (retrait de la clé)Depends on
Décisions prises en planification
/plan-rundu 2026-10-03, revue/review-spec(3 experts).{ machines, token_dropped: "not_listed" | null }(écarté : pas de journal sur ce chemin).remove_activation_token_inintroduit par #370, réutilisé ici.Fixes #367.Review caveats
token_dropped.HMAC-SHA256souscom.simpl.resultat/license-machine-id/v1) ; un test Rust avec une ligne au format dérivé attendu le fige côté client.main0e4fef5et dérivent dans la pile : se fier aux noms de fonctions, intitulés de paragraphes et clés i18n (décision de planification).Spec source
spec-plan-suites-licence.md (local, ignoré par git)