fix(license): check a revoked key as soon as it is entered #386
No reviewers
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#386
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-373-revocation-check-on-entry"
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?
Fixes #373
Generated autonomously by /autopilot run of 2026-10-04
Ce qui change
Pour l'utilisateur. Une clé révoquée saisie en cours de session repasse en Gratuite pendant la session, sans redémarrage ni activation. La clé est vérifiée dès son enregistrement, en arrière-plan : la saisie rend la main tout de suite (hors ligne comme en ligne), puis une clé révoquée affiche son édition une à deux secondes avant de passer en Gratuite, comme décidé à l'
/analyze. La vérification part toujours au plus une fois par 24 h pour une même clé, et chaque vérification écrit sa ligne dans le Journal. Une clé déjà connue comme révoquée sur ce poste passait déjà en Gratuite à la saisie (#365) ; rien ne change pour elle.Front (
src/contexts/LicenseContext.tsx).SubmitKeyDepsreçoit un seul lanceur,startRevocationCheck(wasRevoked).performSubmitKeyl'appelle aprèsVALIDATE_DONEavecinfo.revoked, sans l'attendre. Un enregistrement refusé ne lance rien, et un échec degetEditionn'empêche pas le contrôle (la clé est enregistrée).revocationCheckLauncher(deps): elle rend un lanceur qui appelleperformRevocationChecket rend la main aussitôt. Le provider la construit une fois aveccheckLicenseRevocation,refresh,logInfoetlogWarn, puis la passe à la saisie et àcreateRevocationCheckStarter, dont le corps ne change pas.performSubmitKeyet du starter réécrits.Rust (
src-tauri/src/commands/license_commands.rs).static REVOCATION_CHECK_TURN: tauri::async_runtime::Mutex<()>: réexport detokio::sync::Mutexpar Tauri 2.10.2, donc niCargo.tomlniCargo.lockne bougent (cargo metadata --lockedpasse).check_revocation_in_turn(turn, dir, api_base, decoding_key, now)prend le tour avant de lire l'état et le garde jusqu'au verdict, requête comprise, puis appellecheck_revocation_in, qui ne change pas. La commandecheck_license_revocationpasse&REVOCATION_CHECK_TURNetunix_now.now: impl FnOnce() -> i64). Lue avant l'attente, elle pouvait dater dans le futur la tentative d'un contrôle passé entre-temps, etrevocation_check_duetient une telle date pour due : seconde requête.Docs. Amendement #365 de l'ADR 0017 (point (5), la phrase « une app laissée ouverte n'est pas recontrôlée », la ligne des Liens et la liste des issues) ; guide (Astuces de Paramètres, « Vérification de la licence ») ;
docs.settings.tipsetdocs.editions.tipsen FR et en EN ;docs/architecture.md(licenseService.ts,LicenseContext, la commande, le paragraphe Révocation) ; commentaire du budget de requêtes deuseLicenseMachines.ts;CHANGELOG.md/CHANGELOG.fr.mdsous[Unreleased], section Fixed / Corrigé ajoutée après Changed / Modifié.Écarts et précisions par rapport au corps de l'issue
staticexiste, maischeck_revocation_in_turnle reçoit en paramètre : la commande passe lestatic, et chaque test crée son propre tour. Dans une première version, la fonction prenait lestaticelle-même. Les tests partageaient alors le verrou avec ceux d'autres fils, et le test « clé saisie pendant le contrôle » a échoué une fois sur deux lancé avec ses voisins. Contrepartie : la ligne de la commande qui passe lestaticn'est gardée par aucun test, comme l'enregistrement de la commande danslib.rs.expect(1), une seule requête, le second rendnot_due, prouvé rouge sans le verrou ». Le test fait tout cela, mais sans verrou il rougit surrevoked(le second litfalseau lieu detrue), pas sur le nombre de requêtes. Le double envoi exige deux fils entre la lecture de l'état et l'écriture de la tentative, une fenêtre sansawait. Sur le fil unique d'un#[tokio::test], le second contrôle lit déjà la tentative du premier, écrite avant l'appel depuis #365. Le reproduire demanderait unyieldsous#[cfg(test)]danscheck_revocation_in, que l'issue veut inchangée. Le test prouve donc que le tour couvre tout le contrôle, verdict compris. Comme ce tour est pris avant la lecture de l'état, aucun autre fil ne peut entrer dans la fenêtre pendant un contrôle.docs.settings.tipsest la jumelle intégrée de la ligne L514 (aujourd'hui L588) du guide, que l'issue cite. Il faut aussi corriger la ligneLicenseContextd'architecture.mdet le commentaire deuseLicenseMachines.ts, qui disaient « une fois par démarrage ». Les numéros de ligne de l'issue datent de0e4fef5: la correspondance a été vérifiée avecgit show 0e4fef5:<fichier>.readyest contrôlée une fois, par la saisie. Le second vérifie qu'une clé saisie avant ce premier étatreadyreçoit deux lancements, le démarrage et la saisie, et renvoie au test Rust qui les ramène à une requête.Tests
Rust (3,
license_commands.rs) :two_checks_side_by_side_send_one_request,the_check_of_a_key_entered_meanwhile_waits_for_the_verdict_in_flight(mock unique, réponse choisie selon la clé envoyée,expect(2), puis un « démarrage suivant » qui n'envoie rien),a_check_reads_the_time_once_its_turn_has_come. Chacun lance deux branches avectokio::join!(biased; …). La première branche est donc interrogée la première à chaque tour, et c'est elle qui démarre en premier ; sansbiased,join!alterne son point de départ après le premier tour (second commit,0b94ccf). Toutlicense_commandsa été passé 25 fois avant puis 25 fois après ce commit, sans aucun échec.Front (
LicenseContext.test.ts) : dansperformSubmitKey, deux tests (le lancement suitVALIDATE_DONE, une seule fois ; le lancement porte l'étatrevokedvenu de Rust) et deux assertions ajoutées (aucun lancement après un refus ; lancement même sigetEditionéchoue). Le nouveaudescribe« a key entered in session is checked right away » câble l'entrée au vrai lanceur. La saisie répond avant le contrôle (course explicite contresetTimeout(0)), puis une clé révoquée passe en Gratuite après rechargement. Une commande rejetée ou un serveur injoignable gardent l'édition de la clé et écrivent une ligne au Journal. Enfin, le test du starter est réécrit (point 5).Mutations, une à la fois, restauration par
git checkout --depuis l'index :let _ = turn;)NotDue/false; état final sur la clé remboursée ;Unreachable(une requête partie)a_check_reads_the_time_once_its_turn_has_comerouge (Unreachable) ; les deux autres verts, attendulet _ = turn.lock().await;)two_checks_side_by_side…etthe_check_of_a_key_entered_meanwhile…rouges ; le test de l'heure vert, attenduperformSubmitKeyperformSubmitKey, les 3 de l'entrée câblée, les 2 du starter composés avec l'entrée)VALIDATE_DONEstartRevocationCheck(false)au lieu deinfo.revokedawaitet un lanceur qui rend la promesse)Expected "entry",Received "nothing yet". Une première version ne rougissait que par le délai de 5 s de vitest, d'où la course explicitewasRevokedValidation (worktree, tête
0b94ccf)npm run build(tsc + vite) : vert.npm test: 1446 verts (1440 avant, 6 de plus).cargo test: 195 verts (192 avant, 3 de plus).cargo checksans avertissement,cargo metadata --lockedaccepté,Cargo.tomletCargo.lockintacts.Pour reviewer
check_revocation_in_turnetREVOCATION_CHECK_TURN(license_commands.rs), puis les trois tests qui suiventthe_check_sends_the_product_and_the_key_and_nothing_else. Côté front :performSubmitKey,revocationCheckLauncheret le câblage du provider.submitKeyqui passelaunchRevocationCheck, et le starter construit sur le même lanceur, car aucun effet ne tourne sousrenderToStaticMarkup;check_license_revocation, qui passe&REVOCATION_CHECK_TURNetunix_now, faute d'AppHandle;readydéclenche deux rechargements du contexte, car le second contrôle rendnot_dueavecrevoked: true; c'est sans gravité ;Après merge (Max) : texte pour maximus/la-compagnie-maximus#184
🤖 Generated with Claude Code
Auto-revue du worker (autopilot) : APPROVE
Ce verdict vient du worker qui a écrit la PR : ce n'est pas une revue indépendante, et le label
status:approvedn'est pas posé. La revue porte sur le diff complet de la PR (11 fichiers, têtesab78aa9puis0b94ccf), relu sous quatre angles : sécurité, exactitude, qualité et données.Résumé. La saisie d'une clé lance le contrôle de révocation après
VALIDATE_DONE, sans l'attendre, et jamais après un refus. Un seul lanceur,revocationCheckLauncher, sert au démarrage et à la saisie. Côté Rust, un tour statique (REVOCATION_CHECK_TURN) couvre tout le contrôle, de la lecture de l'état jusqu'au verdict, et l'heure n'est lue qu'une fois le tour obtenu.check_revocation_inne change pas, niCargo.tomlniCargo.lock. Les docs suivent toutes : ADR 0017, guide, docs intégrées FR/EN, architecture et les deux changelogs.store_license_inne touche paslicense.check(reading_and_storing_a_key_report_its_revoked_state), et une tentative de moins de 24 h donnenot_due(nothing_is_sent_without_a_valid_stored_key_nor_before_24_hours). Toutes les mutations rougissent comme annoncé (tableau de la description) : R1 à R3 ont été rejouées après0b94ccf.logInfoetlogWarn, aucun texte d'interface n'est écrit en dur, et les clés i18n sont modifiées dans les deux langues.Points relevés (aucun bloquant)
src/contexts/LicenseContext.tsx:307et:318: le câblage du provider n'est gardé par aucun test. Il s'agit desubmitKey, qui passelaunchRevocationCheck, et du starter, construit sur le même lanceur. La cause : aucun effet ne tourne sousrenderToStaticMarkup. À couvrir par la vérification manuelle décrite dans la PR, avec la clé révoquée du contrôle d'achat du 2026-09-30.src-tauri/src/commands/license_commands.rs:1204: la commande passe&REVOCATION_CHECK_TURNetunix_nowsans test, faute d'AppHandle. C'est la contrepartie du tour reçu en paramètre (écart 1 de la PR). Une mutation « un verrou neuf à chaque appel » à cet endroit passerait.src/contexts/LicenseContext.tsx:224: une clé saisie avant le premier étatreadyreçoit deux contrôles. Le second rendnot_dueavecrevoked: true, lu après le verdict du premier, et recharge donc le contexte une seconde fois. C'est sans gravité : la même lecture est faite deux fois, avec un bref passage enloading.src/hooks/useLicenseMachines.ts:25: le contrôle à la saisie consomme une requête du budget de 5 par minute et par IP. Saisie puis parcours « limite de machines » font 5 requêtes. Avec, dans la même minute, le contrôle au démarrage d'une autre clé, la dernière requête peut recevoir un 429, que l'UI nomme.Suggestions (facultatives)
RevocationCheck.revokeddécrit la clé que le contrôle a envoyée, qui n'est plus forcément la clé enregistrée quand une autre clé est saisie pendant le contrôle. Au pire, le front recharge une fois de trop. Le comportement est antérieur à cette PR et ne demande rien ici.Revue adversariale — PR #386 (#373)
Verdict : APPROVE
Résumé. Le correctif fait ce que demande l'issue : la saisie lance le contrôle après
VALIDATE_DONEsans l'attendre, jamais après un refus, et Rust ne laisse passer qu'un contrôle à la fois, tour pris avant la lecture de l'état et tenu jusqu'au verdict, heure lue une fois le tour venu. Mes mutations confirment celles de l'auteur ; une seule survit, sur un maillon que la PR rend testable sans l'épingler (suggestion 1).Ce que j'ai rejoué
Copie jetable hors dépôt à
0b94ccf(clone local,npm ci, cache cargo copié), supprimée en fin de revue.npm run buildvert ;npm test: 1446 verts ;cargo test: 195 verts ;cargo checksans avertissement ;cargo metadata --lockedaccepté,Cargo.tomletCargo.lockintacts.HEAD is now at 0b94ccf, 1446 tests) et run 480 (Rust, 195 tests dont les trois nouveaux, cache restauré enfull match).license_commandslancé 15 fois de suite : aucun échec.Mutations du relecteur, une à la fois, restauration par
git checkout --dans la copie, arbre propre vérifié après chacune :let _ = turn;)NotDue/falseau lieu deNotDue/true; état final sur la clé remboursée ;Unreachableau lieu deNotDuea_check_reads_the_time_once_its_turn_has_come)drop(turn.lock().await))two_checks_side_by_side_send_one_request,the_check_of_a_key_entered_meanwhile_waits_for_the_verdict_in_flight)falseau lieu dewasRevokedtruegetEditiona réussiSonde à deux fils (test jetable, runtime
multi_threadà 4 workers, deux contrôles lâchés par une barrière, 200 rondes) : aucun double envoi, avec ou sans tour ; dans cette sonde, la fenêtre entre la lecture de l'état et l'écriture de la tentative ne s'est pas ouverte une seule fois. Cela confirme l'écart 2 : les tests ne peuvent garder que le verdict vu par le second contrôle et l'écrasement clé A / clé B, et RM-1 et RM-3 montrent que ces deux effets sont bien tenus.Vérifié par lecture
check_revocation_in_turnprend le tour avant toute lecture delicense.keyet delicense.check, et le garde jusqu'au retour decheck_revocation_in, requête et verdict compris ;check_revocation_inest inchangée. La commande passe&REVOCATION_CHECK_TURNetunix_now.performSubmitKey: lancement aprèsVALIDATE_DONE, non attendu, avecinfo.revoked; rien après un refus ; lancement maintenu quandgetEditionéchoue.useState), partagé par la saisie et le starter ;refresh(useCallbacksans dépendance) etsubmitKeygardent leur identité.pull_refclosesvenant de #386, et le corps de la PR ne contient queFixes #373comme mot-clé de fermeture.docs.settings.tipsetdocs.editions.tipsen FR et en EN,architecture.mdet CHANGELOG FR/EN sous[Unreleased]disent « au démarrage et à la saisie d'une clé ». Plus aucune mention d'un contrôle au seul démarrage, hors de l'entrée 0.16.0 du CHANGELOG, qui est historique.git diff --checkpropre, commits conventionnels, niskipnionly.Les trous déclarés par la PR sont exacts : câblage du provider,
staticpassé par la commande, double envoi sur deux fils, réponse réelle du serveur. La vérification manuelle proposée couvre le premier et le dernier.Suggestions (non bloquantes)
wasRevokedparrevocationCheckLauncher. FM-A passe toute la suite : chaque test du lanceur part d'une clé non révoquée. Effet en production : une révocation levée côté serveur, le chemin que la décision du 2026-10-03 garde ouvert, ne rechargerait plus le contexte avant le démarrage suivant, et une clé connue révoquée rechargerait le contexte à chaque contrôle. Le maillon vivait avant dans le provider, sans test : le trou n'est pas nouveau, maisrevocationCheckLauncherle rend testable. Ce test, essayé dans la copie, est vert sur la tête et rouge sous FM-A :useLicenseMachines.ts. « with it, the machine-limit path reaches the 5 requests of the first minute » vaut pour un seul contrôle. Le corps de la PR nomme le cas de deux contrôles dans la même minute (démarrage pour une clé précédente, puis saisie d'une nouvelle) : le parcours atteint alors 6 requêtes et la dernière peut recevoir un 429. C'est dans ce commentaire que le prochain lecteur du budget cherchera cette limite.Actions faites
status:reviewàstatus:approved./tmp/pr386-review, scripts de mutation/tmp/pr386-mutet logs CI/tmp/pr386-run479.log,/tmp/pr386-run480.logsupprimés.maximus referenced this pull request2026-10-05 23:24:06 +00:00