feat(license): online revocation check — a revoked key falls back to Free (#365) #368

Merged
maximus merged 3 commits from issue-365-online-revocation-check into main 2026-10-03 13:43:02 +00:00
Owner

Refs #365

Refs et non une fermeture automatique : le dernier critère d'acceptation demande une vérification manuelle avec une licence réellement révoquée, que seul Max peut faire (voir « Vérification manuelle due »).

Ce que la PR change

Constat du go-live (2026-09-30) : une licence remboursée, révoquée côté serveur, restait pleinement fonctionnelle dans l'app, mises à jour automatiques comprises. L'édition se résout hors ligne depuis la clé seule, et le signalement de #347 (D13) ne vivait que le temps de la session, sans toucher l'édition.

Rust (license_commands.rs, balance_commands.rs, lib.rs)

  • Un fichier d'état license.check, à côté de la clé : {key_sha256, attempted_at, revoked}. La clé n'y figure que par son condensé SHA-256. Absent, illisible, JSON invalide ou condensé d'une autre clé : « pas d'état », donc non révoquée et contrôle dû.
  • entitlements_from_dir rend ("free", []) pour une clé que l'état dit révoquée. get_edition, check_entitlement (auto-update) et la matrice du front suivent sans autre changement. read_license et store_license rendent revoked.
  • Nouvelle commande check_license_revocation : POST /v1/licenses/verify avec le produit et la clé, rien d'autre. Rien ne part sans une clé enregistrée qui se valide localement, ni quand la dernière tentative a moins de 24 h. attempted_at est écrit avant l'appel. Fail-open : seule une réponse valid:true lève l'état, seule une réponse « révoquée » le pose.
  • L'activation, la liste des machines et la désactivation enregistrent un license_revoked sous la clé qu'elles viennent d'envoyer (note_revocation).
  • Les cours : fetch_price_inner lit error.code d'un 403 et rend license_revoked au lieu de premium_required. Il n'écrit rien (premier écart ci-dessous).

Front

  • LicenseContext lit l'état depuis Rust (info.revoked). Le drapeau de session LICENSE_REVOKED et markRevoked disparaissent. Le contrôle part une fois par démarrage, écrit son issue dans le Journal de l'app, et ne recharge le contexte que si l'état a changé.
  • Une clé révoquée ressaisie reste en Gratuite : performSubmitKey relit l'édition depuis Rust au lieu de prendre celle de la clé.
  • useLicenseMachines recharge le contexte après un license_revoked, puisque Rust vient de l'enregistrer.
  • La carte Licence (édition Gratuite, bandeau, « Entrer une clé » et « Acheter », plus d'échéance affichée) et chaque écran verrouillé (UpsellGate, nouvelle clé upsell.revoked) nomment la révocation. Pas de bandeau global (D2). La carte Mises à jour, en Gratuite, reprend la même ligne (décision de Max du 2026-10-03, à la revue) ; la page d'erreur reste sans.

Docs : amendement #365 de l'ADR 0017 (D13 remplacée, la première requête non sollicitée et ses bornes), docs/architecture.md, guide utilisateur et docs.* (clé révoquée, ce que l'app envoie et quand), annexe du contrat des cours, CHANGELOG.md et CHANGELOG.fr.md. L'entrée #347 du changelog qui décrivait le drapeau de session est réécrite plutôt que contredite : les deux changements sortiront dans la même release.

Ce que cette PR change au principe « rien ne part sans action »

C'est la première requête que l'app envoie d'elle-même. Vérifié dans le code avant de l'écrire dans le guide : aucune autre requête ne part au démarrage (les mises à jour, les cours et la rétroaction partent sur un clic ; le contrôle d'un compte Maximus connecté part à l'ouverture de Paramètres → Utilisateurs ou du formulaire de rétroaction). Les bornes, chacune gardée par un test :

  • aucune requête sans clé enregistrée qui se valide localement ;
  • au plus une tentative par 24 h, quel qu'en soit le résultat ;
  • la requête porte product et license_key, rien d'autre ;
  • chaque contrôle écrit son issue dans le Journal, y compris « rien envoyé » ;
  • le fichier d'état ne contient pas la clé.

Une clé connue comme révoquée reste contrôlée au même rythme, tant qu'elle est enregistrée (décision de Max du 2026-10-03, à la revue) : une correction faite côté serveur est ainsi reprise au démarrage suivant. C'est déclaré dans le guide et dans les docs intégrées. Aucune interface ne retire une clé enregistrée : seul l'enregistrement d'une autre clé arrête l'envoi de la précédente.

La mention dans la politique de confidentialité du site est suivie par maximus/la-compagnie-maximus#184 : à publier avant la release qui embarque ce contrôle.

Écarts assumés par rapport au corps de l'issue

  1. La route des cours nomme le refus sans l'enregistrer (décision de Max, 2026-10-02). Le corps demandait que le 403 license_revoked des cours pose l'état. Le plan checker a trouvé pourquoi ce serait faux : /v1/prices juge la licence du jeton d'activation, que le client ne sait pas relier à la clé enregistrée (le sub du jeton est l'identifiant de la licence, la clé porte le courriel), et la v0.15.0 n'effaçait pas le jeton quand la clé changeait (git show v0.15.0:src-tauri/src/commands/license_commands.rs, store_license). Un dossier écrit par la v0.15.0 peut donc contenir une clé valide à côté du jeton d'une licence révoquée : enregistrer ce refus aurait rétrogradé la clé valide. Le message des cours nomme l'activation, pas la clé. Le jeton périmé lui-même est suivi en #367.
  2. Le contrôle ne part qu'au démarrage. Le corps disait « après le premier chargement réussi avec une clé ». Ici le lanceur est consommé par le premier état ready, qu'une clé soit enregistrée ou non : une clé saisie en cours de session est contrôlée au démarrage suivant. C'est la lecture stricte de D1, et cela évite qu'une saisie de clé déclenche une requête que l'utilisateur n'a pas demandée.
  3. Aucun rechargement du contexte depuis les cours. Conséquence de l'écart 1 : PriceFetchControl n'est pas touché, et la saisie d'un snapshot en cours n'est jamais interrompue.
  4. Deux issues de contrôle en plus de celles du corps : no_license (rien n'est parti, faute de clé valide) et storage (la tentative n'a pas pu être écrite, et rien n'est parti ; ou le verdict du serveur ne l'a pas pu, et rien n'a changé).
  5. Les branches « clé révoquée sous une édition payante » de la carte Licence sont conservées. Elles ne s'affichent plus que si un compte Maximus donne Premium pendant qu'une clé révoquée est enregistrée, chemin dormant tant qu'aucun abonnement n'est vendu (réserve déjà notée dans l'ADR 0017).

Preuves

  • cargo test : 187 tests verts (168 avant). npm test : 1348 tests verts (1314 avant). npm run build (tsc + vite) vert, cargo check sans avertissement.
  • Mutations, jouées une à la fois sur l'arbre commité, restaurées par git checkout, arbre vérifié propre à la fin : 33 rouges sur 33 côté Rust, 27 rouges sur 27 côté front, plus 4 sur 4 pour les corrections de la revue (b107cf4).
  • La première passe avait laissé cinq mutations vertes, pour quatre manques ; le commit 4225dad ajoute ce qui les rend rouges :
    • la valeur revoked exposée par le fournisseur (aucun test ne lisait la valeur du contexte) : dérivée par licenseContextValue, fonction testée ;
    • le message des cours absent du français, ou de l'anglais (deux mutations) ;
    • attempted_at écrit après la requête au lieu d'avant : un test lit maintenant le fichier d'état depuis le serveur mockito, pendant que la requête est en vol ;
    • l'intervalle ramené de 24 h à 1 h : les lignes de bord étaient construites depuis la constante et la suivaient (mesuré sur e24c30f : 184 tests verts avec l'intervalle à 1 h). Les deux côtés du bord sont maintenant écrits en littéraux.
  • Revue (APPROVE, commentaire 18362) : ses suggestions 1 et 2 sont appliquées dans b107cf4, et Max a tranché les deux autres le 2026-10-03. La mutation restée verte en revue (une tentative non inscriptible envoyée quand même) passe de 0 rouge sur 185 à 1 rouge sur 187.
  • Contrat serveur sondé en production avec une clé factice : POST /v1/licenses/verify répond 200 {"valid":false,"reason":"not_found"}, que le contrôle lit comme « aucun changement ».

Ce que les tests ne gardent pas, faute de jsdom et d'AppHandle :

  • le câblage de l'effet du fournisseur (l'appel du lanceur avec l'état courant, et les dépendances réelles passées à performRevocationCheck) ;
  • l'enregistrement de la commande dans lib.rs et son enveloppe (app_data_dir, clé publique embarquée, URL du serveur, horloge) ;
  • la réponse réelle du serveur pour une clé révoquée.

Ces trois points sont exactement ce que la vérification manuelle ci-dessous exerce.

Vérification manuelle due

  • Clé réellement révoquée (celle du contrôle d'achat du 2026-09-30). L'enregistrer : l'édition Base s'affiche, puisque rien n'a encore été demandé au serveur. Redémarrer l'app : le Journal porte « License check: the license server says the key was revoked », l'édition est Gratuite, la carte Licence affiche le bandeau, un écran verrouillé (Budget) affiche la ligne de révocation, et « Vérifier les mises à jour » (Paramètres → Système) répond que les mises à jour automatiques sont incluses dans l'édition Base, avec la même ligne, sans rien vérifier. Redémarrer encore : toujours Gratuite, et le Journal porte « last attempt less than 24 h ago, nothing sent ».
  • Autre clé valide après cela : l'édition revient à la saisie. Rappel de la revue : enregistrer la clé révoquée efface l'activation de la clé de testeur, et la ressaisir ensuite l'efface aussi, donc prévoir un « Activer » pour retrouver les cours. Pour rejouer le contrôle avant 24 h, supprimer license.check.
  • Hors ligne, avec une clé valide et un license.check supprimé : aucune rétrogradation, le Journal porte « could not be reached or answered with an error, nothing changed ».
  • Si un dossier de données dans l'état de #367 est disponible : les cours affichent « L'activation de cette machine est liée à une licence révoquée » et l'édition de la clé valide ne bouge pas.

Limites assumées

La page d'erreur, verrouillée en Gratuite, ne nomme pas la révocation : page de repli après un plantage, elle doit dépendre du moins de choses possible (avis de la revue, retenu par Max).

Reprises de l'issue et de l'ADR : aucun changement hors ligne (bloquer le serveur de licences suffit à garder l'édition, c'est le soft-paywall de l'ADR 0017) ; supprimer license.check rend l'édition jusqu'au contrôle suivant, dû dès le prochain démarrage ; une app laissée ouverte n'est pas recontrôlée ; not_found et expired n'ont aucun effet ; le contrôle compte dans les 5 requêtes par minute et par adresse IP des routes de licence, que le parcours « limite de machines » consomme déjà à 4.

Suites

  • #367 : 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).
  • maximus/la-compagnie-maximus#184 : mention du contrôle dans la politique de confidentialité.
  • Dérive préexistante, non corrigée ici : docs/architecture.md annonce 36 commandes Tauri (35 avant cette PR), alors que lib.rs en enregistre 42. Six commandes n'y sont pas comptées : send_feedback, get_feedback_user_agent, ensure_backup_dir, get_file_size, file_exists, et fetch_price, encore décrite comme à venir.

🤖 Generated with Claude Code

Refs #365 `Refs` et non une fermeture automatique : le dernier critère d'acceptation demande une vérification manuelle avec une licence réellement révoquée, que seul Max peut faire (voir « Vérification manuelle due »). ## Ce que la PR change Constat du go-live (2026-09-30) : une licence remboursée, révoquée côté serveur, restait pleinement fonctionnelle dans l'app, mises à jour automatiques comprises. L'édition se résout hors ligne depuis la clé seule, et le signalement de #347 (D13) ne vivait que le temps de la session, sans toucher l'édition. **Rust** (`license_commands.rs`, `balance_commands.rs`, `lib.rs`) - Un fichier d'état `license.check`, à côté de la clé : `{key_sha256, attempted_at, revoked}`. La clé n'y figure que par son condensé SHA-256. Absent, illisible, JSON invalide ou condensé d'une autre clé : « pas d'état », donc non révoquée et contrôle dû. - `entitlements_from_dir` rend `("free", [])` pour une clé que l'état dit révoquée. `get_edition`, `check_entitlement` (`auto-update`) et la matrice du front suivent sans autre changement. `read_license` et `store_license` rendent `revoked`. - Nouvelle commande `check_license_revocation` : `POST /v1/licenses/verify` avec le produit et la clé, rien d'autre. Rien ne part sans une clé enregistrée qui se valide localement, ni quand la dernière tentative a moins de 24 h. `attempted_at` est écrit avant l'appel. Fail-open : seule une réponse `valid:true` lève l'état, seule une réponse « révoquée » le pose. - L'activation, la liste des machines et la désactivation enregistrent un `license_revoked` sous la clé qu'elles viennent d'envoyer (`note_revocation`). - Les cours : `fetch_price_inner` lit `error.code` d'un 403 et rend `license_revoked` au lieu de `premium_required`. Il n'écrit rien (premier écart ci-dessous). **Front** - `LicenseContext` lit l'état depuis Rust (`info.revoked`). Le drapeau de session `LICENSE_REVOKED` et `markRevoked` disparaissent. Le contrôle part une fois par démarrage, écrit son issue dans le Journal de l'app, et ne recharge le contexte que si l'état a changé. - Une clé révoquée ressaisie reste en Gratuite : `performSubmitKey` relit l'édition depuis Rust au lieu de prendre celle de la clé. - `useLicenseMachines` recharge le contexte après un `license_revoked`, puisque Rust vient de l'enregistrer. - La carte Licence (édition Gratuite, bandeau, « Entrer une clé » et « Acheter », plus d'échéance affichée) et chaque écran verrouillé (`UpsellGate`, nouvelle clé `upsell.revoked`) nomment la révocation. Pas de bandeau global (D2). La carte Mises à jour, en Gratuite, reprend la même ligne (décision de Max du 2026-10-03, à la revue) ; la page d'erreur reste sans. **Docs** : amendement #365 de l'ADR 0017 (D13 remplacée, la première requête non sollicitée et ses bornes), `docs/architecture.md`, guide utilisateur et `docs.*` (clé révoquée, ce que l'app envoie et quand), annexe du contrat des cours, `CHANGELOG.md` et `CHANGELOG.fr.md`. L'entrée #347 du changelog qui décrivait le drapeau de session est réécrite plutôt que contredite : les deux changements sortiront dans la même release. ## Ce que cette PR change au principe « rien ne part sans action » C'est la première requête que l'app envoie d'elle-même. Vérifié dans le code avant de l'écrire dans le guide : aucune autre requête ne part au démarrage (les mises à jour, les cours et la rétroaction partent sur un clic ; le contrôle d'un compte Maximus connecté part à l'ouverture de Paramètres → Utilisateurs ou du formulaire de rétroaction). Les bornes, chacune gardée par un test : - aucune requête sans clé enregistrée qui se valide localement ; - au plus une tentative par 24 h, quel qu'en soit le résultat ; - la requête porte `product` et `license_key`, rien d'autre ; - chaque contrôle écrit son issue dans le Journal, y compris « rien envoyé » ; - le fichier d'état ne contient pas la clé. Une clé connue comme révoquée reste contrôlée au même rythme, tant qu'elle est enregistrée (décision de Max du 2026-10-03, à la revue) : une correction faite côté serveur est ainsi reprise au démarrage suivant. C'est déclaré dans le guide et dans les docs intégrées. Aucune interface ne retire une clé enregistrée : seul l'enregistrement d'une autre clé arrête l'envoi de la précédente. La mention dans la politique de confidentialité du site est suivie par maximus/la-compagnie-maximus#184 : à publier avant la release qui embarque ce contrôle. ## Écarts assumés par rapport au corps de l'issue 1. **La route des cours nomme le refus sans l'enregistrer** (décision de Max, 2026-10-02). Le corps demandait que le 403 `license_revoked` des cours pose l'état. Le plan checker a trouvé pourquoi ce serait faux : `/v1/prices` juge la licence du jeton d'activation, que le client ne sait pas relier à la clé enregistrée (le `sub` du jeton est l'identifiant de la licence, la clé porte le courriel), et la v0.15.0 n'effaçait pas le jeton quand la clé changeait (`git show v0.15.0:src-tauri/src/commands/license_commands.rs`, `store_license`). Un dossier écrit par la v0.15.0 peut donc contenir une clé valide à côté du jeton d'une licence révoquée : enregistrer ce refus aurait rétrogradé la clé valide. Le message des cours nomme l'activation, pas la clé. Le jeton périmé lui-même est suivi en #367. 2. **Le contrôle ne part qu'au démarrage.** Le corps disait « après le premier chargement réussi avec une clé ». Ici le lanceur est consommé par le premier état `ready`, qu'une clé soit enregistrée ou non : une clé saisie en cours de session est contrôlée au démarrage suivant. C'est la lecture stricte de D1, et cela évite qu'une saisie de clé déclenche une requête que l'utilisateur n'a pas demandée. 3. **Aucun rechargement du contexte depuis les cours.** Conséquence de l'écart 1 : `PriceFetchControl` n'est pas touché, et la saisie d'un snapshot en cours n'est jamais interrompue. 4. **Deux issues de contrôle en plus de celles du corps** : `no_license` (rien n'est parti, faute de clé valide) et `storage` (la tentative n'a pas pu être écrite, et rien n'est parti ; ou le verdict du serveur ne l'a pas pu, et rien n'a changé). 5. **Les branches « clé révoquée sous une édition payante » de la carte Licence sont conservées.** Elles ne s'affichent plus que si un compte Maximus donne Premium pendant qu'une clé révoquée est enregistrée, chemin dormant tant qu'aucun abonnement n'est vendu (réserve déjà notée dans l'ADR 0017). ## Preuves - `cargo test` : 187 tests verts (168 avant). `npm test` : 1348 tests verts (1314 avant). `npm run build` (tsc + vite) vert, `cargo check` sans avertissement. - **Mutations**, jouées une à la fois sur l'arbre commité, restaurées par `git checkout`, arbre vérifié propre à la fin : 33 rouges sur 33 côté Rust, 27 rouges sur 27 côté front, plus 4 sur 4 pour les corrections de la revue (`b107cf4`). - La première passe avait laissé cinq mutations vertes, pour quatre manques ; le commit `4225dad` ajoute ce qui les rend rouges : - la valeur `revoked` exposée par le fournisseur (aucun test ne lisait la valeur du contexte) : dérivée par `licenseContextValue`, fonction testée ; - le message des cours absent du français, ou de l'anglais (deux mutations) ; - `attempted_at` écrit après la requête au lieu d'avant : un test lit maintenant le fichier d'état depuis le serveur mockito, pendant que la requête est en vol ; - l'intervalle ramené de 24 h à 1 h : les lignes de bord étaient construites depuis la constante et la suivaient (mesuré sur `e24c30f` : 184 tests verts avec l'intervalle à 1 h). Les deux côtés du bord sont maintenant écrits en littéraux. - **Revue** (APPROVE, commentaire 18362) : ses suggestions 1 et 2 sont appliquées dans `b107cf4`, et Max a tranché les deux autres le 2026-10-03. La mutation restée verte en revue (une tentative non inscriptible envoyée quand même) passe de 0 rouge sur 185 à 1 rouge sur 187. - Contrat serveur sondé en production avec une clé factice : `POST /v1/licenses/verify` répond `200 {"valid":false,"reason":"not_found"}`, que le contrôle lit comme « aucun changement ». Ce que les tests ne gardent pas, faute de jsdom et d'`AppHandle` : - le câblage de l'effet du fournisseur (l'appel du lanceur avec l'état courant, et les dépendances réelles passées à `performRevocationCheck`) ; - l'enregistrement de la commande dans `lib.rs` et son enveloppe (`app_data_dir`, clé publique embarquée, URL du serveur, horloge) ; - la réponse réelle du serveur pour une clé révoquée. Ces trois points sont exactement ce que la vérification manuelle ci-dessous exerce. ## Vérification manuelle due - [ ] **Clé réellement révoquée** (celle du contrôle d'achat du 2026-09-30). L'enregistrer : l'édition Base s'affiche, puisque rien n'a encore été demandé au serveur. Redémarrer l'app : le Journal porte « License check: the license server says the key was revoked », l'édition est Gratuite, la carte Licence affiche le bandeau, un écran verrouillé (Budget) affiche la ligne de révocation, et « Vérifier les mises à jour » (Paramètres → Système) répond que les mises à jour automatiques sont incluses dans l'édition Base, avec la même ligne, sans rien vérifier. Redémarrer encore : toujours Gratuite, et le Journal porte « last attempt less than 24 h ago, nothing sent ». - [ ] **Autre clé valide** après cela : l'édition revient à la saisie. Rappel de la revue : enregistrer la clé révoquée efface l'activation de la clé de testeur, et la ressaisir ensuite l'efface aussi, donc prévoir un « Activer » pour retrouver les cours. Pour rejouer le contrôle avant 24 h, supprimer `license.check`. - [ ] **Hors ligne**, avec une clé valide et un `license.check` supprimé : aucune rétrogradation, le Journal porte « could not be reached or answered with an error, nothing changed ». - [ ] Si un dossier de données dans l'état de #367 est disponible : les cours affichent « L'activation de cette machine est liée à une licence révoquée » et l'édition de la clé valide ne bouge pas. ## Limites assumées La page d'erreur, verrouillée en Gratuite, ne nomme pas la révocation : page de repli après un plantage, elle doit dépendre du moins de choses possible (avis de la revue, retenu par Max). Reprises de l'issue et de l'ADR : aucun changement hors ligne (bloquer le serveur de licences suffit à garder l'édition, c'est le soft-paywall de l'ADR 0017) ; supprimer `license.check` rend l'édition jusqu'au contrôle suivant, dû dès le prochain démarrage ; une app laissée ouverte n'est pas recontrôlée ; `not_found` et `expired` n'ont aucun effet ; le contrôle compte dans les 5 requêtes par minute et par adresse IP des routes de licence, que le parcours « limite de machines » consomme déjà à 4. ## Suites - #367 : 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). - maximus/la-compagnie-maximus#184 : mention du contrôle dans la politique de confidentialité. - Dérive préexistante, non corrigée ici : `docs/architecture.md` annonce 36 commandes Tauri (35 avant cette PR), alors que `lib.rs` en enregistre 42. Six commandes n'y sont pas comptées : `send_feedback`, `get_feedback_user_agent`, `ensure_backup_dir`, `get_file_size`, `file_exists`, et `fetch_price`, encore décrite comme à venir. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 2 commits 2026-10-02 19:11:42 +00:00
A refunded license was revoked on the server but kept working in the app:
the edition is resolved offline from the key alone, and the flag added by
#347 lasted the session only and never touched the edition.

Rust
- license.check, next to the key, holds what the license server last said
  about it: {key_sha256, attempted_at, revoked}. Never the key itself. A
  missing, unreadable or invalid file, or a state about another key, reads
  as "no state": not revoked, check due.
- entitlements_from_dir resolves a revoked key to ("free", []), which also
  locks auto-update. read_license and store_license report the state.
- check_license_revocation: POST /v1/licenses/verify with the product and
  the key only. Nothing is sent without a stored key that validates, nor
  within 24 h of the last attempt, which is written before the request.
  Fail-open: only "valid" lifts the state, only "revoked" sets it.
- Activation, machine list and deactivation record a license_revoked answer
  under the key they sent.
- fetch_price reads error.code of a 403 and names license_revoked instead of
  premium_required. It records nothing: the price route judges the license
  of the activation token, which the client cannot tie to the stored key,
  and 0.15.0 left the token of a previous key next to a new one (#367).

Front
- LicenseContext takes the revoked state from Rust (info.revoked); the
  session flag and markRevoked are gone. The check starts once per app
  start, writes its outcome to the in-app log, and reloads the context only
  when the state changed. A revoked key entered again stays Free.
- UpsellGate and the License card name the revocation.

Docs: ADR 0017 amendment (D13 replaced, the first unsolicited request and
its bounds), architecture, user guide, price contract annex, changelogs.

Refs #365

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test(license): guard what the first mutation pass left green (#365)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m43s
PR Check — Rust / rust (pull_request) Successful in 9m3s
4225dadebe
Five mutations survived the first pass, for four gaps:
- the revoked flag exposed by the provider: the value is now derived by
  licenseContextValue, a plain function, and tested;
- the price message for a revoked license missing from either locale;
- the attempt written after the request instead of before: a test now reads
  the state file from the server's side, while the request is in flight;
- the 24 h interval, which the boundary rows followed because they were
  built from the constant: both sides of the boundary are written out.

Refs #365

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

CI verte sur 4225dad, lue dans les logs des deux jobs.

  • frontend (run 446) : build tsc + vite vert, 68 fichiers de test, 1347 tests verts.
  • rust (run 447) : cargo check --all-targets sans aucun avertissement de compilation, garde-fou des suppressions 2 checks, exit 0, 185 tests verts, cargo audit sur 666 crates sans vulnérabilité (12 avertissements autorisés ; ni Cargo.lock ni Cargo.toml ne bougent dans cette PR).

Les deux logs partent bien de 4225dad (HEAD is now at 4225dad).

**CI verte sur `4225dad`, lue dans les logs des deux jobs.** - `frontend` (run 446) : build `tsc` + `vite` vert, 68 fichiers de test, **1347 tests verts**. - `rust` (run 447) : `cargo check --all-targets` sans aucun avertissement de compilation, garde-fou des suppressions `2 checks, exit 0`, **185 tests verts**, `cargo audit` sur 666 crates sans vulnérabilité (12 avertissements autorisés ; ni `Cargo.lock` ni `Cargo.toml` ne bougent dans cette PR). Les deux logs partent bien de `4225dad` (`HEAD is now at 4225dad`).
Author
Owner

Revue adversariale — PR #368 (#365)

Verdict : APPROVE

Le contrôle de révocation tient les bornes annoncées : rien ne part sans clé valide, une tentative par 24 h écrite avant l'appel, fail-open, et un corps limité à product + license_key. L'édition suit l'état que Rust persiste à côté de la clé, et les écarts au corps de l'issue sont justifiés dans le code. Une borne correcte n'est gardée par aucun test, et deux points sont à trancher par Max. Rien ne bloque.

Ce qui a été rejoué

  • CI de la tête 4225dad, lue dans les logs : run 446 (tsc && vite build, puis 1347 tests vitest) et run 447 (Suppression guard: 2 checks, exit 0, puis 185 tests Rust).
  • Copie hors dépôt (git archive de 4225dad, dossier cible Cargo séparé) : vitest 1347/1347, cargo test --lib 185/185.
  • 37 mutations du relecteur, une à la fois, restaurées par git checkout sur la copie, propreté vérifiée après chacune : 35 rouges (détail en fin de commentaire).
  • Contrat serveur relu dans maximus-api (5dd6e05) : verifyLicense répond {valid:false, reason:"revoked"} avant le test d'expiration, et licenseAuth répond 403 {error:{code:"license_revoked"}} pour la licence du jeton d'activation. verdict_from_verify et fetch_price_inner lisent exactement ces formes.
  • Prémisse de l'écart 1 : git show v0.15.0:src-tauri/src/commands/license_commands.rs confirme que store_license écrivait la clé sans toucher activation.token. Enregistrer le 403 des cours aurait donc rétrogradé la clé valide de ces dossiers.
  • « Aucune autre requête au démarrage », vérifié dans le code : useAuth n'est monté que par AccountCard et FeedbackDialog, checkForUpdate n'est branché que sur des onClick, le setup de lib.rs ne fait aucun appel réseau, et ni index.html ni styles.css ne chargent de ressource distante.
  • Pas de fuite de features : licenseContextValue expose encore les features signées d'une clé révoquée, mais isEntitled court-circuite free avant l'override (src/shared/entitlements.ts:48), et Rust rend ("free", []).
  • Vecteur SHA-256 recalculé hors du code (sha256sum) : f4753f0b…89c9, conforme.
  • Fermeture : la timeline de #365 porte un pull_ref de #368 avec ref_action: none, et aucun mot-clé de fermeture n'apparaît dans le corps ni dans les deux commits (format conventionnel). Refs tiendra au merge.
  • Capacités Tauri : build.rs se limite à tauri_build::build(), sans liste blanche de commandes applicatives, donc check_license_revocation ne demande aucune permission.

Suggestions (non bloquantes)

  1. Épingle manquante : une tentative non inscriptible ne doit rien envoyer. Mutation R21 (ignorer l'échec de la première écriture et envoyer quand même) : 185 verts sur 185. Le code est correct (license_commands.rs:599-601), mais sans test, un dossier de données en lecture seule enverrait une requête à chaque démarrage, et la borne (2) tomberait sans un rouge. Ce test passe sur la tête et rougit sous R21 (left: Unreachable, right: Storage), vérifié sur la copie :

    #[tokio::test]
    async fn an_attempt_that_cannot_be_written_sends_nothing() {
        let (enc, dec) = default_keys();
        let key = base_license_with_features(&enc, vec![]);
        let dir = TestDir::new("check_unwritable");
        dir.write(LICENSE_FILE, &key);
        // A directory in place of the state file: it reads as "no state" and cannot be written.
        fs::create_dir(dir.0.join(CHECK_FILE)).unwrap();
        let mut server = mockito::Server::new_async().await;
        let any_request = server.mock("POST", mockito::Matcher::Any).expect(0).create_async().await;
    
        let result = check_revocation_in(&dir.0, &server.url(), &dec, NOW).await;
    
        assert_eq!(result, RevocationCheck { outcome: RevocationOutcome::Storage, revoked: false });
        any_request.assert_async().await;
    }
    
  2. storage ne veut pas toujours dire « rien n'est parti ». check_revocation_in rend Storage à deux endroits : avant la requête (:599-601, rien n'est parti) et après, quand le verdict a changé mais ne peut pas être écrit (:621-622, la requête est partie et le serveur a répondu). Le commentaire de la variante Rust le dit (« The attempt or its verdict could not be written »). En revanche licenseService.ts:42 (« no_license, not_due and storage mean nothing was sent ») et la ligne check_license_revocation de docs/architecture.md:348 affirment davantage que le code, et le message du Journal (LicenseContext.tsx:182, « the attempt could not be recorded ») est faux dans le second cas : la tentative est écrite, c'est le verdict qui ne l'est pas. Corriger ces trois phrases, ou séparer les deux issues.

  3. À trancher : une clé révoquée est vérifiée tous les jours, sans fin. Aucun chemin ne retire license.key : seule la saisie d'une autre clé le remplace. Un acheteur remboursé qui continue en édition Gratuite enverra donc sa clé révoquée une fois par 24 h tant qu'il utilise l'app, alors que la levée n'arrive que sur correction manuelle de la base (amendement, « Fail-open »). C'est conforme à D1 tel qu'il est écrit, mais pour ces postes la requête quotidienne ne change rien tant que personne ne corrige la base. Deux options : l'écrire dans le guide et dans la politique de confidentialité (maximus/la-compagnie-maximus#184), ou arrêter le contrôle automatique quand l'état dit revoked.

  4. À trancher (question posée par la PR) : la carte Mises à jour. Avis : ajouter la ligne upsell.revoked dans sa branche Gratuite (UpdateCard.tsx:81-91). C'est là qu'un acheteur Base remboursé constate d'abord la perte des mises à jour automatiques, et « incluses dans l'édition Base », sans la raison, se lit comme un défaut quand on a payé Base. Laisser ErrorPage telle quelle : une page de repli après plantage doit dépendre du moins de choses possible.

Vérification manuelle : rappel pratique

Enregistrer la clé révoquée efface l'activation de la clé de testeur, et ressaisir cette dernière l'efface aussi (store_license_in, #347) : prévoir un « Activer » pour retrouver les cours. Pour rejouer le contrôle avant 24 h, supprimer license.check.

Mutations du relecteur

Front, suite vitest complète à chaque mutation : 13 rouges sur 14.

# Mutation Rouges
F1 lanceur consommé seulement quand une clé est enregistrée 1
F2 le contrôle recharge toujours le contexte 1
F3 rechargement seulement sur revoked (levée non rechargée) 2
F4 performSubmitKey reprend l'édition de la clé 3
F5 repli de performSubmitKey sans le drapeau revoked 1
F6 licenseContextValue ne dit jamais revoked 1
F7 UpsellGate sans la ligne de révocation 1
F8 échéance affichée pour une clé révoquée 1
F9 license_revoked ne recharge plus le contexte 4
F10 license_revoked retenté comme un 5xx 1
F11 license_revoked relié au message premium 1
F12 le lanceur passe false au lieu de l'état chargé 1
F13 le lanceur part aussi sur un état error 2
F14 refresh non attendu après license_revoked 0, sans effet observable (mutant équivalent)

Rust, cargo test --lib complet à chaque mutation : 22 rouges sur 23.

# Mutation Rouges
R1 entitlements_from_dir ignore l'état 4
R2 état lu sans comparer le condensé 4
R3 condensé de la clé non rognée 1
R4 borne des 24 h inclusive 5
R5 date future non due 1
R6 date en débordement jamais due 1
R7 fail-closed : toute autre issue lève l'état 2
R8 tout valid:false vaut révoquée 1
R9 note_revocation enregistre toute erreur 1
R10 le contrôle envoie aussi un machine_id 1
R11 contrôle envoyé pour une clé qui ne se valide pas 1
R12 license_revoked de l'enveloppe lu comme injoignable 1
R13 store_license ne rend jamais revoked 1
R14 is_revoked_in vrai pour tout état de la clé 4
R15 désactivation refusée qui supprime quand même le jeton 1
R16 tout 403 des cours redevient premium_required 1
R17 attempted_at repris de l'ancien état 3
R18 read_license ne rend jamais revoked 1
R19 la liste n'enregistre plus la révocation 2
R20 l'activation n'enregistre plus la révocation 1
R21 tentative non inscriptible envoyée quand même 0, voir suggestion 1
R22 valid:true exige une liste de machines 1
R23 note_revocation sans horodatage de tentative 1

Actions faites

  • Copie hors dépôt /tmp/pr368-copy, dossier cible /tmp/pr368-target, script de mutations et logs CI /tmp/pr368-* : supprimés en fin de revue. Le dépôt de travail n'a pas été touché (git status vide, tête 4225dad).
  • Label de #365 : status:review remplacé par status:approved.
## Revue adversariale — PR #368 (#365) **Verdict : APPROVE** Le contrôle de révocation tient les bornes annoncées : rien ne part sans clé valide, une tentative par 24 h écrite avant l'appel, fail-open, et un corps limité à `product` + `license_key`. L'édition suit l'état que Rust persiste à côté de la clé, et les écarts au corps de l'issue sont justifiés dans le code. Une borne correcte n'est gardée par aucun test, et deux points sont à trancher par Max. Rien ne bloque. ### Ce qui a été rejoué - **CI de la tête `4225dad`, lue dans les logs** : run 446 (`tsc && vite build`, puis 1347 tests vitest) et run 447 (`Suppression guard: 2 checks, exit 0`, puis 185 tests Rust). - **Copie hors dépôt** (`git archive` de `4225dad`, dossier cible Cargo séparé) : vitest 1347/1347, `cargo test --lib` 185/185. - **37 mutations du relecteur**, une à la fois, restaurées par `git checkout` sur la copie, propreté vérifiée après chacune : 35 rouges (détail en fin de commentaire). - **Contrat serveur relu dans maximus-api** (`5dd6e05`) : `verifyLicense` répond `{valid:false, reason:"revoked"}` avant le test d'expiration, et `licenseAuth` répond 403 `{error:{code:"license_revoked"}}` pour la licence du jeton d'activation. `verdict_from_verify` et `fetch_price_inner` lisent exactement ces formes. - **Prémisse de l'écart 1** : `git show v0.15.0:src-tauri/src/commands/license_commands.rs` confirme que `store_license` écrivait la clé sans toucher `activation.token`. Enregistrer le 403 des cours aurait donc rétrogradé la clé valide de ces dossiers. - **« Aucune autre requête au démarrage »**, vérifié dans le code : `useAuth` n'est monté que par `AccountCard` et `FeedbackDialog`, `checkForUpdate` n'est branché que sur des `onClick`, le `setup` de `lib.rs` ne fait aucun appel réseau, et ni `index.html` ni `styles.css` ne chargent de ressource distante. - **Pas de fuite de features** : `licenseContextValue` expose encore les `features` signées d'une clé révoquée, mais `isEntitled` court-circuite `free` avant l'override (`src/shared/entitlements.ts:48`), et Rust rend `("free", [])`. - **Vecteur SHA-256** recalculé hors du code (`sha256sum`) : `f4753f0b…89c9`, conforme. - **Fermeture** : la timeline de #365 porte un `pull_ref` de #368 avec `ref_action: none`, et aucun mot-clé de fermeture n'apparaît dans le corps ni dans les deux commits (format conventionnel). `Refs` tiendra au merge. - **Capacités Tauri** : `build.rs` se limite à `tauri_build::build()`, sans liste blanche de commandes applicatives, donc `check_license_revocation` ne demande aucune permission. ### Suggestions (non bloquantes) 1. **Épingle manquante : une tentative non inscriptible ne doit rien envoyer.** Mutation R21 (ignorer l'échec de la première écriture et envoyer quand même) : **185 verts sur 185**. Le code est correct (`license_commands.rs:599-601`), mais sans test, un dossier de données en lecture seule enverrait une requête à chaque démarrage, et la borne (2) tomberait sans un rouge. Ce test passe sur la tête et rougit sous R21 (`left: Unreachable`, `right: Storage`), vérifié sur la copie : ```rust #[tokio::test] async fn an_attempt_that_cannot_be_written_sends_nothing() { let (enc, dec) = default_keys(); let key = base_license_with_features(&enc, vec![]); let dir = TestDir::new("check_unwritable"); dir.write(LICENSE_FILE, &key); // A directory in place of the state file: it reads as "no state" and cannot be written. fs::create_dir(dir.0.join(CHECK_FILE)).unwrap(); let mut server = mockito::Server::new_async().await; let any_request = server.mock("POST", mockito::Matcher::Any).expect(0).create_async().await; let result = check_revocation_in(&dir.0, &server.url(), &dec, NOW).await; assert_eq!(result, RevocationCheck { outcome: RevocationOutcome::Storage, revoked: false }); any_request.assert_async().await; } ``` 2. **`storage` ne veut pas toujours dire « rien n'est parti ».** `check_revocation_in` rend `Storage` à deux endroits : avant la requête (`:599-601`, rien n'est parti) et après, quand le verdict a changé mais ne peut pas être écrit (`:621-622`, la requête est partie et le serveur a répondu). Le commentaire de la variante Rust le dit (« The attempt or its verdict could not be written »). En revanche `licenseService.ts:42` (« `no_license`, `not_due` and `storage` mean nothing was sent ») et la ligne `check_license_revocation` de `docs/architecture.md:348` affirment davantage que le code, et le message du Journal (`LicenseContext.tsx:182`, « the attempt could not be recorded ») est faux dans le second cas : la tentative est écrite, c'est le verdict qui ne l'est pas. Corriger ces trois phrases, ou séparer les deux issues. 3. **À trancher : une clé révoquée est vérifiée tous les jours, sans fin.** Aucun chemin ne retire `license.key` : seule la saisie d'une autre clé le remplace. Un acheteur remboursé qui continue en édition Gratuite enverra donc sa clé révoquée une fois par 24 h tant qu'il utilise l'app, alors que la levée n'arrive que sur correction manuelle de la base (amendement, « Fail-open »). C'est conforme à D1 tel qu'il est écrit, mais pour ces postes la requête quotidienne ne change rien tant que personne ne corrige la base. Deux options : l'écrire dans le guide et dans la politique de confidentialité (maximus/la-compagnie-maximus#184), ou arrêter le contrôle automatique quand l'état dit `revoked`. 4. **À trancher (question posée par la PR) : la carte Mises à jour.** Avis : ajouter la ligne `upsell.revoked` dans sa branche Gratuite (`UpdateCard.tsx:81-91`). C'est là qu'un acheteur Base remboursé constate d'abord la perte des mises à jour automatiques, et « incluses dans l'édition Base », sans la raison, se lit comme un défaut quand on a payé Base. Laisser `ErrorPage` telle quelle : une page de repli après plantage doit dépendre du moins de choses possible. ### Vérification manuelle : rappel pratique Enregistrer la clé révoquée efface l'activation de la clé de testeur, et ressaisir cette dernière l'efface aussi (`store_license_in`, #347) : prévoir un « Activer » pour retrouver les cours. Pour rejouer le contrôle avant 24 h, supprimer `license.check`. ### Mutations du relecteur Front, suite vitest complète à chaque mutation : 13 rouges sur 14. | # | Mutation | Rouges | |---|---|---| | F1 | lanceur consommé seulement quand une clé est enregistrée | 1 | | F2 | le contrôle recharge toujours le contexte | 1 | | F3 | rechargement seulement sur `revoked` (levée non rechargée) | 2 | | F4 | `performSubmitKey` reprend l'édition de la clé | 3 | | F5 | repli de `performSubmitKey` sans le drapeau `revoked` | 1 | | F6 | `licenseContextValue` ne dit jamais `revoked` | 1 | | F7 | `UpsellGate` sans la ligne de révocation | 1 | | F8 | échéance affichée pour une clé révoquée | 1 | | F9 | `license_revoked` ne recharge plus le contexte | 4 | | F10 | `license_revoked` retenté comme un 5xx | 1 | | F11 | `license_revoked` relié au message premium | 1 | | F12 | le lanceur passe `false` au lieu de l'état chargé | 1 | | F13 | le lanceur part aussi sur un état `error` | 2 | | F14 | `refresh` non attendu après `license_revoked` | **0**, sans effet observable (mutant équivalent) | Rust, `cargo test --lib` complet à chaque mutation : 22 rouges sur 23. | # | Mutation | Rouges | |---|---|---| | R1 | `entitlements_from_dir` ignore l'état | 4 | | R2 | état lu sans comparer le condensé | 4 | | R3 | condensé de la clé non rognée | 1 | | R4 | borne des 24 h inclusive | 5 | | R5 | date future non due | 1 | | R6 | date en débordement jamais due | 1 | | R7 | fail-closed : toute autre issue lève l'état | 2 | | R8 | tout `valid:false` vaut révoquée | 1 | | R9 | `note_revocation` enregistre toute erreur | 1 | | R10 | le contrôle envoie aussi un `machine_id` | 1 | | R11 | contrôle envoyé pour une clé qui ne se valide pas | 1 | | R12 | `license_revoked` de l'enveloppe lu comme injoignable | 1 | | R13 | `store_license` ne rend jamais `revoked` | 1 | | R14 | `is_revoked_in` vrai pour tout état de la clé | 4 | | R15 | désactivation refusée qui supprime quand même le jeton | 1 | | R16 | tout 403 des cours redevient `premium_required` | 1 | | R17 | `attempted_at` repris de l'ancien état | 3 | | R18 | `read_license` ne rend jamais `revoked` | 1 | | R19 | la liste n'enregistre plus la révocation | 2 | | R20 | l'activation n'enregistre plus la révocation | 1 | | R21 | tentative non inscriptible envoyée quand même | **0**, voir suggestion 1 | | R22 | `valid:true` exige une liste de machines | 1 | | R23 | `note_revocation` sans horodatage de tentative | 1 | ### Actions faites - Copie hors dépôt `/tmp/pr368-copy`, dossier cible `/tmp/pr368-target`, script de mutations et logs CI `/tmp/pr368-*` : supprimés en fin de revue. Le dépôt de travail n'a pas été touché (`git status` vide, tête `4225dad`). - Label de #365 : `status:review` remplacé par `status:approved`.
maximus added 1 commit 2026-10-03 13:31:25 +00:00
fix(license): apply the review of #368
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m45s
PR Check — Rust / rust (pull_request) Successful in 8m54s
b107cf42ce
- A state file that cannot be written now has a test on both of its paths:
  the attempt (nothing is sent, or a read-only data directory would send a
  request at every start) and the verdict (the key is not reported revoked
  when nothing on disk says so). The first path was correct but unguarded:
  the review's mutation left all 185 tests green.
- The storage outcome no longer claims that nothing was sent: it also covers
  a verdict that could not be written. Reworded in licenseService.ts, the
  in-app log line and docs/architecture.md.
- The Updates card names a revoked key in its Free branch, between the
  message and the CTAs: a refunded Base buyer first meets the loss of
  automatic updates there (Max, 2026-10-03). The error page stays as it is.
- A revoked key keeps being checked once per 24 h while it stays stored, so
  that a correction made on the license server is picked up (Max,
  2026-10-03). Declared in the user guide, the in-app docs, the changelogs
  and the ADR 0017 amendment.

Refs #365

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Revue appliquée dans b107cf4. Ce commit n'a pas été relu : la revue (APPROVE, commentaire 18362) portait sur 4225dad. Max a demandé d'appliquer puis de merger.

Suggestions 1 et 2, appliquées

  • Le test proposé par la revue est ajouté (an_attempt_that_cannot_be_written_sends_nothing), avec un second test pour l'autre cas de storage : le verdict ne peut pas être écrit, et la clé n'est pas dite révoquée tant que rien sur le disque ne le dit (a_verdict_that_cannot_be_written_is_not_reported_as_applied).
  • storage ne prétend plus que rien n'est parti : licenseService.ts, la ligne du Journal et docs/architecture.md couvrent maintenant les deux cas.

Suggestions 3 et 4, tranchées par Max le 2026-10-03

  • 3, garder et déclarer : une clé révoquée reste contrôlée une fois par 24 h tant qu'elle est enregistrée, pour reprendre une correction faite côté serveur. C'est écrit dans le guide, les docs intégrées, les deux changelogs et l'amendement de l'ADR 0017. Aucune interface ne retire une clé enregistrée : seul l'enregistrement d'une autre clé arrête l'envoi.
  • 4, la ligne upsell.revoked est ajoutée à la carte Mises à jour en Gratuite, entre le message et les boutons. La page d'erreur ne change pas.

Preuve du rouge (étape 6a bis)

Mutation Avant (4225dad) Sous mutation (b107cf4) Après
Tentative non inscriptible envoyée quand même (mutation de la revue) 0 rouge / 185 1 rouge / 187 187 verts
Verdict non inscriptible annoncé comme appliqué n/a 1 rouge / 187 187 verts
Carte Mises à jour sans la ligne n/a 1 rouge / 1348 1348 verts
Ligne affichée sans clé révoquée n/a 1 rouge / 1348 1348 verts

Chaque mutation a été appliquée après le commit, vérifiée par git diff, puis restaurée par git checkout, et l'arbre est vérifié propre à la fin. En local : cargo check --all-targets sans avertissement, npm run build vert.

**Revue appliquée dans `b107cf4`.** Ce commit n'a pas été relu : la revue (APPROVE, commentaire 18362) portait sur `4225dad`. Max a demandé d'appliquer puis de merger. **Suggestions 1 et 2, appliquées** - Le test proposé par la revue est ajouté (`an_attempt_that_cannot_be_written_sends_nothing`), avec un second test pour l'autre cas de `storage` : le verdict ne peut pas être écrit, et la clé n'est pas dite révoquée tant que rien sur le disque ne le dit (`a_verdict_that_cannot_be_written_is_not_reported_as_applied`). - `storage` ne prétend plus que rien n'est parti : `licenseService.ts`, la ligne du Journal et `docs/architecture.md` couvrent maintenant les deux cas. **Suggestions 3 et 4, tranchées par Max le 2026-10-03** - 3, garder et déclarer : une clé révoquée reste contrôlée une fois par 24 h tant qu'elle est enregistrée, pour reprendre une correction faite côté serveur. C'est écrit dans le guide, les docs intégrées, les deux changelogs et l'amendement de l'ADR 0017. Aucune interface ne retire une clé enregistrée : seul l'enregistrement d'une autre clé arrête l'envoi. - 4, la ligne `upsell.revoked` est ajoutée à la carte Mises à jour en Gratuite, entre le message et les boutons. La page d'erreur ne change pas. **Preuve du rouge (étape 6a bis)** | Mutation | Avant (`4225dad`) | Sous mutation (`b107cf4`) | Après | |---|---|---|---| | Tentative non inscriptible envoyée quand même (mutation de la revue) | 0 rouge / 185 | 1 rouge / 187 | 187 verts | | Verdict non inscriptible annoncé comme appliqué | n/a | 1 rouge / 187 | 187 verts | | Carte Mises à jour sans la ligne | n/a | 1 rouge / 1348 | 1348 verts | | Ligne affichée sans clé révoquée | n/a | 1 rouge / 1348 | 1348 verts | Chaque mutation a été appliquée après le commit, vérifiée par `git diff`, puis restaurée par `git checkout`, et l'arbre est vérifié propre à la fin. En local : `cargo check --all-targets` sans avertissement, `npm run build` vert.
Author
Owner

CI verte sur b107cf4, lue dans les logs. frontend (run 449) : build vert, 1348 tests verts. rust (run 450) : garde-fou des suppressions 2 checks, exit 0, 187 tests verts, cargo audit sur 666 crates sans vulnérabilité (12 avertissements autorisés), aucun avertissement de compilation. Merge en fast-forward demandé par Max.

**CI verte sur `b107cf4`, lue dans les logs.** `frontend` (run 449) : build vert, 1348 tests verts. `rust` (run 450) : garde-fou des suppressions `2 checks, exit 0`, 187 tests verts, `cargo audit` sur 666 crates sans vulnérabilité (12 avertissements autorisés), aucun avertissement de compilation. Merge en fast-forward demandé par Max.
maximus merged commit b107cf42ce into main 2026-10-03 13:43:02 +00:00
maximus deleted branch issue-365-online-revocation-check 2026-10-03 13:43:03 +00:00
Sign in to join this conversation.
No reviewers
No milestone
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#368
No description provided.