fix(licence): une clé révoquée saisie en cours de session garde son édition jusqu'au redémarrage — la contrôler dès sa saisie #373
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#373
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
Constat de Max le 2026-10-03, en testant la v0.16.0 sous Linux. Après la saisie de la clé révoquée du contrôle d'achat du 2026-09-30, les fonctionnalités Base restaient utilisables jusqu'au clic sur « Activer ». Le serveur a alors refusé l'activation (
license_revoked), l'état révoqué a été enregistré et l'app est passée en Gratuite. Après un redémarrage, le contrôle du démarrage fonctionne, confirmé par Max.C'est le comportement livré par #365 (PR #368) et écrit dans l'ADR 0017 (amendement #365, point (5) : « le contrôle ne part qu'au démarrage [...] une clé saisie en cours de session est contrôlée au démarrage suivant »). Rien n'oblige à activer une machine : depuis #347 (D12), l'édition ne dépend que de la clé.
Analyse (
/analyze, 2026-10-03,main0e4fef5)Prémisse confirmée, et bornée. Une clé déjà connue comme révoquée sur ce poste passe déjà en Gratuite à la saisie :
store_license_inrendrevoked: is_revoked_in(...)(license_commands.rs:249-251) etperformSubmitKeyprend l'édition résolue par Rust (LicenseContext.tsx:150-155). Le défaut ne vise que la clé jamais contrôlée sur ce poste. Pour elle, l'étatlicense.checkparle d'une autre clé (ou n'existe pas), donc un contrôle est dû.Décision de Max (2026-10-03) : contrôle en arrière-plan, pas attendu dans la saisie. La saisie rend la main tout de suite ; le contrôle part ensuite par
performRevocationCheck(deps, info.revoked), écrit sa ligne dans le Journal et recharge le contexte si l'état change. Une clé révoquée affiche donc l'édition Base une à deux secondes avant de basculer. Écarté : attendre la réponse dansperformSubmitKey, qui laisse l'indicateur de validation tourner jusqu'à 15 s hors ligne (délai depost_license_api,license_commands.rs:803).Course à garder.
createRevocationCheckStarter(LicenseContext.tsx:165-172) est consommé par le premier étatready. Si la clé est saisie avant ce premier état (chargement initial pas fini, ou erreur de démarrage avec reprise),VALIDATE_DONEpasse enreadyavecinfoet déclenche le starter, en plus du contrôle de la saisie. La garde de 24 h n'est pas atomique : lecture de l'état (check_revocation_in, L585) puis écriture deattempted_at(L600), sans verrou. Deux appels concurrents peuvent donc partir tous les deux. Le correctif doit garantir un seul envoi dans ce cas. Décidé en planification : verrou côté Rust (voir « Décisions prises en planification »).Travail à faire
SubmitKeyDepsreçoit un seul lanceurstartRevocationCheck(wasRevoked)fourni par le provider (il appelleperformRevocationCheckaveccheckLicenseRevocation,refresh,logInfo,logWarn) ;performSubmitKey, après un enregistrement réussi et leVALIDATE_DONE, l'appelle sans l'attendre.tokio::sync::Mutex<()>, ou équivalent déjà disponible dans les dépendances) sérialisecheck_license_revocationsur toute la séquence lecture de l'état → écriture de la tentative → requête → écriture du verdict. La logique reste danscheck_revocation_in; le verrou peut l'envelopper dans une fonction testable sansAppHandle.ready: garanti par le verrou Rust (le second appel litnot_due).createRevocationCheckStarterreste inchangé.LicenseContext.tsx:43-49et:159-164(createRevocationCheckStarter),license_commands.rs:892et:1163-1167(« The front calls it once at app start »).docs/guide-utilisateur.md:514et:566),docs.editions.tips(fr.json/en.json:1130),docs/architecture.md:187,:348,:354.CHANGELOG.mdetCHANGELOG.fr.mdsous[Unreleased](Fixed/Corrigé).Fichiers concernés
src/contexts/LicenseContext.tsx:performSubmitKey,SubmitKeyDeps,submitKeydu provider (L273-276).src/contexts/LicenseContext.test.ts:describe("performSubmitKey")(L181-256),createRevocationCheckStarter(L295), dont le test L332 « a key entered later waits for the next start », à réécrire.check_license_revocationest réutilisé tel quel.Surface de test
LicenseContext.test.ts(performSubmitKey, starter, performRevocationCheck) ; Rust :nothing_is_sent_without_a_valid_stored_key_nor_before_24_hours,the_check_moves_the_state_on_a_verdict_and_on_nothing_else.VALIDATE_DONE, sans quesubmitKeyl'attende (le résultat de la saisie arrive avant celui du contrôle).revoked: true, le contexte est rechargé.unreachable) laisse l'édition de la clé et écrit une ligne au Journal.expect(1)→ une seule requête ; le second rendnot_due. Prouvé rouge sans le verrou.performSubmitKeyappelle le lanceur une fois après un succès, jamais après un refus.type:bug). Chaque test prouvé rouge par mutation de la ligne qu'il garde.Critères d'acceptation
readyn'envoie qu'une requête.npm test,cargo testet la CI verts.Complexité estimée
Simple à moyenne (front seul, la course demande un test soigné).
Ordre
À faire avant #370 : les deux touchent
LicenseContext.tsxet l'ADR 0017. #370 compte sur ce contrôle pour qu'une clé révoquée retirée puis ressaisie soit recontrôlée.Liens
Depends on
(aucune, première de la pile)
Décisions prises en planification
/plan-rundu 2026-10-03, revue/review-spec(3 experts) sur le plan des trois issues #373, #370, #367./analyze).check_license_revocation. Écartés : lanceur unique côté front (garantie limitée à ce front, course clé A / clé B laissée ouverte), ou les deux.Fixes #373.Review caveats
license.checkest un fichier unique) → résolu par le verrou Rust.RevocationCheckDepsdansSubmitKeyDeps→ un seul lanceur injecté.useEffectdu provider ne s'exécute pas sousrenderToStaticMarkup→ tester la course en Rust, etperformSubmitKeyen fonction pure.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)