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

Closed
opened 2026-10-03 14:48:13 +00:00 by maximus · 0 comments
Owner

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, main 0e4fef5)

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_in rend revoked: is_revoked_in(...) (license_commands.rs:249-251) et performSubmitKey prend 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'état license.check parle 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 dans performSubmitKey, qui laisse l'indicateur de validation tourner jusqu'à 15 s hors ligne (délai de post_license_api, license_commands.rs:803).

Course à garder. createRevocationCheckStarter (LicenseContext.tsx:165-172) est consommé par le premier état ready. Si la clé est saisie avant ce premier état (chargement initial pas fini, ou erreur de démarrage avec reprise), VALIDATE_DONE passe en ready avec info et 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 de attempted_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

  • SubmitKeyDeps reçoit un seul lanceur startRevocationCheck(wasRevoked) fourni par le provider (il appelle performRevocationCheck avec checkLicenseRevocation, refresh, logInfo, logWarn) ; performSubmitKey, après un enregistrement réussi et le VALIDATE_DONE, l'appelle sans l'attendre.
  • Rust : un verrou async statique (tokio::sync::Mutex<()>, ou équivalent déjà disponible dans les dépendances) sérialise check_license_revocation sur toute la séquence lecture de l'état → écriture de la tentative → requête → écriture du verdict. La logique reste dans check_revocation_in ; le verrou peut l'envelopper dans une fonction testable sans AppHandle.
  • Aucun contrôle après un enregistrement refusé.
  • Un seul envoi quand la saisie précède le premier état ready : garanti par le verrou Rust (le second appel lit not_due). createRevocationCheckStarter reste inchangé.
  • Docstrings : LicenseContext.tsx:43-49 et :159-164 (createRevocationCheckStarter), license_commands.rs:892 et :1163-1167 (« The front calls it once at app start »).
  • ADR 0017 : amender l'amendement #365, point (5) (L17), la phrase « une app laissée ouverte n'est pas recontrôlée » (L25) et L145. Le contrôle part « au démarrage et à la saisie d'une clé », toujours borné à une tentative par 24 h et par clé.
  • Guide (docs/guide-utilisateur.md:514 et :566), docs.editions.tips (fr.json/en.json:1130), docs/architecture.md:187, :348, :354.
  • CHANGELOG.md et CHANGELOG.fr.md sous [Unreleased] (Fixed/Corrigé).

Fichiers concernés

  • src/contexts/LicenseContext.tsx : performSubmitKey, SubmitKeyDeps, submitKey du 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.
  • Docs listées ci-dessus. Côté Rust, aucun changement de logique : check_license_revocation est réutilisé tel quel.

Surface de test

  • Tests existants : 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.
  • À ajouter :
    • Un enregistrement réussi lance le contrôle, après VALIDATE_DONE, sans que submitKey l'attende (le résultat de la saisie arrive avant celui du contrôle).
    • Une clé révoquée saisie : le contrôle rend revoked: true, le contexte est rechargé.
    • Un contrôle qui échoue (commande rejetée ou unreachable) laisse l'édition de la clé et écrit une ligne au Journal.
    • Un enregistrement refusé n'appelle pas le contrôle.
    • Rust : deux appels concurrents au contrôle verrouillé, sur un serveur mockito à un seul mock expect(1) → une seule requête ; le second rend not_due. Prouvé rouge sans le verrou.
    • Front : performSubmitKey appelle le lanceur une fois après un succès, jamais après un refus.
  • Test de régression requis : oui (type:bug). Chaque test prouvé rouge par mutation de la ligne qu'il garde.

Critères d'acceptation

  • Une clé révoquée saisie en ligne passe en Gratuite pendant la session, sans redémarrage ni activation.
  • La saisie rend la main sans attendre le serveur ; hors ligne, elle fonctionne comme avant.
  • La même clé ressaisie dans les 24 h n'envoie rien ; une clé saisie avant le premier état ready n'envoie qu'une requête.
  • Chaque contrôle à la saisie écrit sa ligne dans le Journal.
  • ADR 0017, guide, docs intégrées et architecture disent « au démarrage et à la saisie d'une clé ».
  • npm test, cargo test et la CI verts.

Complexité estimée

Simple à moyenne (front seul, la course demande un test soigné).

Ordre

À faire avant #370 : les deux touchent LicenseContext.tsx et 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

  • Séance /plan-run du 2026-10-03, revue /review-spec (3 experts) sur le plan des trois issues #373, #370, #367.
  • Contrôle à la saisie en arrière-plan, non attendu (décision /analyze).
  • Garde « au plus un envoi » côté Rust : verrou async statique autour de 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.
  • Politique de confidentialité : signalement seul, la publication de la-compagnie-maximus#184 n'est pas une condition du tag (décision de Max).
  • PR fermante : Fixes #373.

Review caveats

  • 🟡 SECURITE, ARCHITECTURE, TECHNIQUE : garde de 24 h non atomique et course clé A / clé B (license.check est un fichier unique) → résolu par le verrou Rust.
  • 🟡 ARCHITECTURE : ne pas recopier RevocationCheckDeps dans SubmitKeyDeps → un seul lanceur injecté.
  • 🟡 TECHNIQUE : le useEffect du provider ne s'exécute pas sous renderToStaticMarkup → tester la course en Rust, et performSubmitKey en fonction pure.
  • Les numéros de ligne cités datent de main 0e4fef5 et 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)

  • Le worker ne commente pas maximus/la-compagnie-maximus#184 : il rédige dans le DAILY-REPORT le texte à y poster (« le contrôle part au démarrage et à la saisie d'une clé, au plus une fois par 24 h et par clé »). Max le poste.

Spec source

spec-plan-suites-licence.md (local, ignoré par git)

## 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, `main` `0e4fef5`) **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_in` rend `revoked: is_revoked_in(...)` (`license_commands.rs:249-251`) et `performSubmitKey` prend 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'état `license.check` parle 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 dans `performSubmitKey`, qui laisse l'indicateur de validation tourner jusqu'à 15 s hors ligne (délai de `post_license_api`, `license_commands.rs:803`). **Course à garder.** `createRevocationCheckStarter` (`LicenseContext.tsx:165-172`) est consommé par le premier état `ready`. Si la clé est saisie avant ce premier état (chargement initial pas fini, ou erreur de démarrage avec reprise), `VALIDATE_DONE` passe en `ready` avec `info` et 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 de `attempted_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 - [ ] `SubmitKeyDeps` reçoit un seul lanceur `startRevocationCheck(wasRevoked)` fourni par le provider (il appelle `performRevocationCheck` avec `checkLicenseRevocation`, `refresh`, `logInfo`, `logWarn`) ; `performSubmitKey`, après un enregistrement réussi et le `VALIDATE_DONE`, l'appelle sans l'attendre. - [ ] Rust : un verrou async statique (`tokio::sync::Mutex<()>`, ou équivalent déjà disponible dans les dépendances) sérialise `check_license_revocation` sur toute la séquence lecture de l'état → écriture de la tentative → requête → écriture du verdict. La logique reste dans `check_revocation_in` ; le verrou peut l'envelopper dans une fonction testable sans `AppHandle`. - [ ] Aucun contrôle après un enregistrement refusé. - [ ] Un seul envoi quand la saisie précède le premier état `ready` : garanti par le verrou Rust (le second appel lit `not_due`). `createRevocationCheckStarter` reste inchangé. - [ ] Docstrings : `LicenseContext.tsx:43-49` et `:159-164` (`createRevocationCheckStarter`), `license_commands.rs:892` et `:1163-1167` (« The front calls it once at app start »). - [ ] ADR 0017 : amender l'amendement #365, point (5) (L17), la phrase « une app laissée ouverte n'est pas recontrôlée » (L25) et L145. Le contrôle part « au démarrage et à la saisie d'une clé », toujours borné à une tentative par 24 h et par clé. - [ ] Guide (`docs/guide-utilisateur.md:514` et `:566`), `docs.editions.tips` (`fr.json`/`en.json:1130`), `docs/architecture.md:187`, `:348`, `:354`. - [ ] `CHANGELOG.md` et `CHANGELOG.fr.md` sous `[Unreleased]` (Fixed/Corrigé). ## Fichiers concernés - `src/contexts/LicenseContext.tsx` : `performSubmitKey`, `SubmitKeyDeps`, `submitKey` du 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. - Docs listées ci-dessus. Côté Rust, aucun changement de logique : `check_license_revocation` est réutilisé tel quel. ## Surface de test - Tests existants : `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`. - À ajouter : - [ ] Un enregistrement réussi lance le contrôle, après `VALIDATE_DONE`, sans que `submitKey` l'attende (le résultat de la saisie arrive avant celui du contrôle). - [ ] Une clé révoquée saisie : le contrôle rend `revoked: true`, le contexte est rechargé. - [ ] Un contrôle qui échoue (commande rejetée ou `unreachable`) laisse l'édition de la clé et écrit une ligne au Journal. - [ ] Un enregistrement refusé n'appelle pas le contrôle. - [ ] Rust : deux appels concurrents au contrôle verrouillé, sur un serveur mockito à un seul mock `expect(1)` → une seule requête ; le second rend `not_due`. Prouvé rouge sans le verrou. - [ ] Front : `performSubmitKey` appelle le lanceur une fois après un succès, jamais après un refus. - Test de régression requis : oui (`type:bug`). Chaque test prouvé rouge par mutation de la ligne qu'il garde. ## Critères d'acceptation - [ ] Une clé révoquée saisie en ligne passe en Gratuite pendant la session, sans redémarrage ni activation. - [ ] La saisie rend la main sans attendre le serveur ; hors ligne, elle fonctionne comme avant. - [ ] La même clé ressaisie dans les 24 h n'envoie rien ; une clé saisie avant le premier état `ready` n'envoie qu'une requête. - [ ] Chaque contrôle à la saisie écrit sa ligne dans le Journal. - [ ] ADR 0017, guide, docs intégrées et architecture disent « au démarrage et à la saisie d'une clé ». - [ ] `npm test`, `cargo test` et la CI verts. ## Complexité estimée Simple à moyenne (front seul, la course demande un test soigné). ## Ordre À faire avant #370 : les deux touchent `LicenseContext.tsx` et 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 - #365 (PR #368, contrôle au démarrage seulement), #347 (D12) - #370 (retrait de la clé), maximus/la-compagnie-maximus#184 (politique de confidentialité) ## Depends on (aucune, première de la pile) ## Décisions prises en planification - Séance `/plan-run` du 2026-10-03, revue `/review-spec` (3 experts) sur le plan des trois issues #373, #370, #367. - Contrôle à la saisie en arrière-plan, non attendu (décision `/analyze`). - **Garde « au plus un envoi » côté Rust** : verrou async statique autour de `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. - Politique de confidentialité : **signalement seul**, la publication de la-compagnie-maximus#184 n'est pas une condition du tag (décision de Max). - PR fermante : `Fixes #373`. ## Review caveats - 🟡 SECURITE, ARCHITECTURE, TECHNIQUE : garde de 24 h non atomique et course clé A / clé B (`license.check` est un fichier unique) → résolu par le verrou Rust. - 🟡 ARCHITECTURE : ne pas recopier `RevocationCheckDeps` dans `SubmitKeyDeps` → un seul lanceur injecté. - 🟡 TECHNIQUE : le `useEffect` du provider ne s'exécute pas sous `renderToStaticMarkup` → tester la course en Rust, et `performSubmitKey` en fonction pure. - Les numéros de ligne cités datent de `main` `0e4fef5` et 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) - Le worker ne commente pas maximus/la-compagnie-maximus#184 : il rédige dans le DAILY-REPORT le texte à y poster (« le contrôle part au démarrage et à la saisie d'une clé, au plus une fois par 24 h et par clé »). Max le poste. ## Spec source spec-plan-suites-licence.md (local, ignoré par git)
maximus added the
status:triage
type:bug
source:human
labels 2026-10-03 14:48:13 +00:00
maximus added
status:ready
and removed
status:triage
labels 2026-10-03 17:52:32 +00:00
maximus added this to the planned-2026-10-03-suites-licence milestone 2026-10-03 19:08:57 +00:00
maximus added
status:in-progress
and removed
status:ready
labels 2026-10-04 19:59:00 +00:00
maximus added
status:review
and removed
status:in-progress
labels 2026-10-04 20:27:34 +00:00
maximus added
status:approved
and removed
status:review
labels 2026-10-05 01:21:39 +00:00
maximus removed the
status:approved
label 2026-10-05 23:23:26 +00:00
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#373
No description provided.