feat(licence): révocation vérifiée en ligne — une clé remboursée retombe en Gratuite #365

Closed
opened 2026-09-30 23:53:53 +00:00 by maximus · 5 comments
Owner

Corps réécrit par /analyze le 2026-10-01. Le corps d'origine du 2026-09-30 est archivé en commentaire.

Contexte

Constat de Max le 2026-09-30, pendant le go-live Stripe (maximus/la-compagnie-maximus#141) : achat réel d'une licence Base, remboursement complet, licence révoquée côté serveur, mais la clé reste pleinement fonctionnelle dans l'app.

La cause, vérifiée sur main (a9fa34d) :

  • L'édition se résout hors ligne, depuis la clé seule : current_entitlements → entitlements_from_dir → resolve_license_entitlements (signature Ed25519 et exp). Les clés durent 36525 jours depuis maximus-api#63 : exp ne joue aucun rôle d'anti-révocation.
  • /v1/licenses/verify n'est appelé que pour lister les machines (verify_inner). Une réponse {valid:false, reason:"revoked"} pose un drapeau de session dans LicenseContext (#347, D13) : il affiche un message, ne touche pas l'édition et disparaît à la fermeture de l'app.
  • auto-update est gatée sur l'édition locale (FEATURE_TIERS, entitlements.rs) : une clé révoquée garde aussi les mises à jour automatiques.

Les conditions de vente du site disent « le remboursement entraîne la révocation de la licence » (sales.refund_desc). Seule la moitié serveur est tenue.

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

Ce contrôle est la première requête que l'app fait seule au démarrage. Aujourd'hui les mises à jour, les cours et la rétroaction partent sur un clic ; le contrôle du compte Maximus (check_subscription_status) a une garde de 24 h mais ne part qu'à l'ouverture de Paramètres → Utilisateurs. Les bornes qui encadrent cette exception :

  • aucune requête sans clé enregistrée : l'édition Gratuite ne contacte jamais le serveur ;
  • au plus une tentative par 24 h ;
  • la requête porte le produit et la clé, rien d'autre : ni identifiant machine, ni nom d'ordinateur, ni donnée d'usage ;
  • chaque tentative est écrite dans le Journal de l'app, et le guide dit ce qui part et quand ;
  • la mention dans la politique de confidentialité est suivie par maximus/la-compagnie-maximus#184.

Décisions (2026-10-01)

  • D1 — au démarrage, au plus une tentative par 24 h. Le contrôle part au lancement, si une clé est enregistrée et que la dernière tentative date de plus de 24 h, qu'elle ait abouti ou non. C'est la garde de check_subscription_status (auth_commands.rs). Écartés : un appel à chaque démarrage (le serveur verrait chaque ouverture de l'app) et un contrôle à la seule ouverture de la carte Licence (un remboursé qui n'ouvre jamais cette page resterait en Base).
  • D2 — la révocation est nommée là où le verrou apparaît. La carte Licence l'affiche, et chaque écran verrouillé (UpsellGate) ajoute une ligne « votre clé a été révoquée » au-dessus de l'invite d'achat. Pas de bandeau global.
  • D3 — fail-open. Erreur réseau, délai dépassé, 429, 5xx, réponse illisible : l'état ne change pas. Un utilisateur légitime hors ligne n'est jamais rétrogradé, et une clé déjà connue comme révoquée le reste.
  • D4 — un seul état, un seul point d'écriture. Toute réponse « révoquée » du serveur pour la clé enregistrée, quelle que soit la commande qui l'apprend, pose le même état persistant. Cela remplace D13 de #347 (« rien n'est persisté ») pour le cas révoqué. D12 est inchangée : le jeton d'activation ne pèse toujours pas sur l'édition.

Conception

État persistant

Un fichier license.check dans le dossier de données, en JSON :

{"key_sha256": "<condensé SHA-256 de la clé>", "attempted_at": 1759363200, "revoked": false}
  • Il remplace le marqueur license.revoked et l'horodatage séparé du corps d'origine. Une autre clé enregistrée ne correspond plus au condensé : l'état est ignoré, donc un contrôle est dû au démarrage suivant.
  • Jamais la clé en clair. sha2 est déjà une dépendance.
  • Absent, illisible, JSON invalide ou condensé d'une autre clé : traité comme « pas d'état », c'est-à-dire non révoquée et contrôle dû. Un fichier corrompu ne doit pas rétrograder un client légitime.

Résolution de l'édition

entitlements_from_dir rend ("free", []) quand l'état dit revoked: true pour la clé courante. resolve_license_entitlements reste pure. get_edition, check_entitlement (auto-update) et le gating du front suivent sans autre changement : isEntitled refuse tout en Gratuite.

Contrôle

Une commande, par exemple check_license_revocation, dont la logique vit dans des fonctions sans AppHandle (dossier temporaire et serveur mockito) :

  • Dû quand une clé est enregistrée et que l'état est absent, ou que attempted_at date de plus de 24 h, ou qu'il est dans le futur (horloge reculée).
  • attempted_at est écrit avant l'appel : deux appels concurrents n'en font qu'un, et « une tentative par 24 h » tient quel que soit le résultat.
  • POST /v1/licenses/verify avec {product, license_key}. Le serveur répond toujours 200.
  • {valid:false, reason:"revoked"} : revoked: true. {valid:true} : revoked: false. not_found, expired et toute erreur : état inchangé.
  • La commande rend l'issue du contrôle (non dû, valide, révoquée, serveur non joint, autre), que le front écrit dans le Journal.

Aucun chemin serveur ne réactive une licence révoquée aujourd'hui (revokeLicense passe is_revoked à vrai et désactive les machines, rien ne fait l'inverse) : la levée sur valid:true ne sert qu'à une correction manuelle de la base.

Autres sources de la révocation

  • activate_machine, list_activated_machines et deactivate_machine : sur LicenseError::LicenseRevoked, poser l'état avant de rendre l'erreur.
  • Cours du Bilan. fetch_price_inner (balance_commands.rs) traduit tout 403 en PremiumRequired sans lire error.code, alors que licenseAuth.ts distingue license_revoked de premium_required. Lire error.code, ajouter LicenseRevoked à FetchPriceError et à PriceErrorCode, le relier à la clé existante balance.priceFetching.errors.licenseRevoked, et poser l'état. Le jeton envoyé appartient à la clé enregistrée : store_license_in le supprime quand la clé change (#347).

Front

  • LicenseContext lit l'état révoqué depuis Rust (par exemple un champ revoked de LicenseInfo, rendu par read_license et store_license). Le drapeau de session LICENSE_REVOKED et markRevoked disparaissent : après une réponse « révoquée », useLicenseMachines recharge le contexte.
  • Une clé révoquée ressaisie reste en Gratuite, y compris juste après la saisie (aujourd'hui VALIDATE_DONE prend l'édition de la clé).
  • Le contrôle part une fois par montage du fournisseur, après le premier chargement réussi avec une clé. Si l'état a changé, le contexte est rechargé.
  • Carte Licence : édition Gratuite, bandeau license.errors.licenseRevoked, boutons « Entrer une clé » et « Acheter ». La section Machines disparaît avec l'édition.
  • UpsellGate : une ligne (nouvelle clé upsell.revoked, FR et EN) quand la clé enregistrée est révoquée.

Tâches

  • Rust : état license.check (lecture tolérante, écriture, condensé de la clé) et prise en compte dans entitlements_from_dir.
  • Rust : commande de contrôle, garde de 24 h, attempted_at écrit avant l'appel, fail-open, issue du contrôle rendue au front.
  • Rust : état posé sur LicenseRevoked par les trois commandes réseau, et par le 403 license_revoked des cours (FetchPriceError::LicenseRevoked).
  • Rust : read_license et store_license rendent l'état révoqué de la clé ; enregistrement de la commande dans lib.rs.
  • TS : LicenseContext (état lu depuis Rust, contrôle au démarrage, rechargement), useLicenseMachines (rechargement au lieu de markRevoked), LicenseCard, UpsellGate, licenseService.ts, balance.service.ts.
  • TS : chaque tentative dans le Journal (logInfo).
  • i18n FR et EN : upsell.revoked ; relire license.errors.licenseRevoked et balance.priceFetching.errors.licenseRevoked.
  • Docs : amendement #365 de l'ADR 0017 (D13 remplacée, première requête au démarrage et ses bornes), docs/architecture.md, docs/guide-utilisateur.md et docs.editions.tips (clé révoquée, et ce que l'app envoie et quand), CHANGELOG.md et CHANGELOG.fr.md.

Tests

Les décisions se testent sans AppHandle : dossier temporaire pour l'état, serveur mockito pour verify, valeurs attendues en littéraux.

  • Clé valide et état révoqué de la même clé : Gratuite, sans features.
  • État d'une autre clé, fichier illisible, JSON invalide : édition de la clé.
  • auto-update refusé après révocation (is_entitled sur l'édition résolue).
  • Contrôle : revoked pose l'état ; valid:true le lève ; not_found, expired, erreur réseau, 429, 5xx, corps illisible le laissent tel quel.
  • Garde : aucune requête sans clé, ni avant 24 h (mock avec expect(0)) ; requête due sans état, après 24 h, avec une autre clé, avec un attempted_at dans le futur ; attempted_at écrit même quand l'appel échoue.
  • La requête ne porte que product et license_key.
  • Le fichier d'état ne contient pas la clé.
  • Les trois commandes réseau et le 403 des cours posent l'état sur license_revoked ; un 403 premium_required ne le pose pas.
  • Front : le contexte prend l'état révoqué de Rust ; une clé révoquée ressaisie reste en Gratuite ; UpsellGate et LicenseCard nomment la révocation ; le contrôle part une seule fois et recharge le contexte quand l'état change.
  • Le bloc « revocation lasts the session » de LicenseContext.test.ts est réécrit : l'état vient de Rust et survit à la session.

Tests existants touchés : entitlements_from_dir et verify_* (license_commands.rs), fetch_price_inner (balance_commands.rs), entitlements.rs, LicenseContext.test.ts, useLicenseMachines.test.ts, LicenseCard.render.test.tsx.

Critères d'acceptation

  • Après un remboursement complet, l'app en ligne retombe en Gratuite au premier démarrage où un contrôle est dû, auto-update compris, et l'état révoqué reste affiché d'une session à l'autre.
  • Hors ligne, ou si le serveur est injoignable ou en erreur, aucune rétrogradation.
  • Une autre clé valide redonne son édition.
  • Aucune requête sans clé enregistrée, et au plus une tentative par 24 h.
  • La requête ne porte que le produit et la clé ; le fichier d'état ne contient pas la clé.
  • Un refus license_revoked des cours s'affiche comme une révocation, plus comme « abonnement premium requis ».
  • cargo test, npm test et la CI verts.
  • Vérification manuelle avec une licence réellement révoquée : la clé du contrôle d'achat du 2026-09-30 l'est déjà.

Limites assumées

  • Aucun changement hors ligne. Bloquer le serveur de licences suffit à garder l'édition : c'est le soft-paywall de l'ADR 0017, pas un contrôle d'accès.
  • Supprimer le fichier d'état rend l'édition jusqu'au contrôle suivant, qui est alors dû dès le prochain démarrage.
  • Une app laissée ouverte n'est pas recontrôlée avant son redémarrage : aucun minuteur en cours de session.
  • not_found et expired n'ont aucun effet. not_found peut venir d'une base serveur restaurée ou d'une licence émise en mode test.
  • Budget de requêtes. Les routes de licence partagent 5 requêtes par minute et par adresse IP. Le parcours « limite de machines » en prend 4 : avec le contrôle du démarrage on atteint 5, sans marge. Un 429 est déjà expliqué à l'utilisateur, et n'a aucun effet sur le contrôle.

Dépendances

Complexité

Moyenne, plus large que #350 : un PR, Rust et front, sans migration de base ni changement serveur.

> Corps réécrit par `/analyze` le 2026-10-01. Le corps d'origine du 2026-09-30 est archivé en commentaire. ## Contexte Constat de Max le 2026-09-30, pendant le go-live Stripe (maximus/la-compagnie-maximus#141) : achat réel d'une licence Base, remboursement complet, licence **révoquée côté serveur**, mais la clé reste pleinement fonctionnelle dans l'app. La cause, vérifiée sur `main` (`a9fa34d`) : - L'édition se résout **hors ligne, depuis la clé seule** : `current_entitlements` → `entitlements_from_dir` → `resolve_license_entitlements` (signature Ed25519 et `exp`). Les clés durent 36525 jours depuis maximus-api#63 : `exp` ne joue aucun rôle d'anti-révocation. - `/v1/licenses/verify` n'est appelé que pour lister les machines (`verify_inner`). Une réponse `{valid:false, reason:"revoked"}` pose un drapeau de session dans `LicenseContext` (#347, D13) : il affiche un message, ne touche pas l'édition et disparaît à la fermeture de l'app. - `auto-update` est gatée sur l'édition locale (`FEATURE_TIERS`, `entitlements.rs`) : une clé révoquée garde aussi les mises à jour automatiques. Les conditions de vente du site disent « le remboursement entraîne la révocation de la licence » (`sales.refund_desc`). Seule la moitié serveur est tenue. ## Ce que cette issue change au principe « rien ne part sans action » **Ce contrôle est la première requête que l'app fait seule au démarrage.** Aujourd'hui les mises à jour, les cours et la rétroaction partent sur un clic ; le contrôle du compte Maximus (`check_subscription_status`) a une garde de 24 h mais ne part qu'à l'ouverture de Paramètres → Utilisateurs. Les bornes qui encadrent cette exception : - aucune requête sans clé enregistrée : l'édition Gratuite ne contacte jamais le serveur ; - au plus **une tentative par 24 h** ; - la requête porte le produit et la clé, rien d'autre : ni identifiant machine, ni nom d'ordinateur, ni donnée d'usage ; - chaque tentative est écrite dans le Journal de l'app, et le guide dit ce qui part et quand ; - la mention dans la politique de confidentialité est suivie par maximus/la-compagnie-maximus#184. ## Décisions (2026-10-01) - **D1 — au démarrage, au plus une tentative par 24 h.** Le contrôle part au lancement, si une clé est enregistrée et que la dernière tentative date de plus de 24 h, qu'elle ait abouti ou non. C'est la garde de `check_subscription_status` (`auth_commands.rs`). Écartés : un appel à chaque démarrage (le serveur verrait chaque ouverture de l'app) et un contrôle à la seule ouverture de la carte Licence (un remboursé qui n'ouvre jamais cette page resterait en Base). - **D2 — la révocation est nommée là où le verrou apparaît.** La carte Licence l'affiche, et chaque écran verrouillé (`UpsellGate`) ajoute une ligne « votre clé a été révoquée » au-dessus de l'invite d'achat. Pas de bandeau global. - **D3 — fail-open.** Erreur réseau, délai dépassé, 429, 5xx, réponse illisible : l'état ne change pas. Un utilisateur légitime hors ligne n'est jamais rétrogradé, et une clé déjà connue comme révoquée le reste. - **D4 — un seul état, un seul point d'écriture.** Toute réponse « révoquée » du serveur pour la clé enregistrée, quelle que soit la commande qui l'apprend, pose le même état persistant. Cela remplace D13 de #347 (« rien n'est persisté ») pour le cas révoqué. D12 est inchangée : le jeton d'activation ne pèse toujours pas sur l'édition. ## Conception ### État persistant Un fichier `license.check` dans le dossier de données, en JSON : ```json {"key_sha256": "<condensé SHA-256 de la clé>", "attempted_at": 1759363200, "revoked": false} ``` - Il remplace le marqueur `license.revoked` et l'horodatage séparé du corps d'origine. Une autre clé enregistrée ne correspond plus au condensé : l'état est ignoré, donc un contrôle est dû au démarrage suivant. - **Jamais la clé en clair.** `sha2` est déjà une dépendance. - Absent, illisible, JSON invalide ou condensé d'une autre clé : traité comme « pas d'état », c'est-à-dire non révoquée et contrôle dû. Un fichier corrompu ne doit pas rétrograder un client légitime. ### Résolution de l'édition `entitlements_from_dir` rend `("free", [])` quand l'état dit `revoked: true` pour la clé courante. `resolve_license_entitlements` reste pure. `get_edition`, `check_entitlement` (`auto-update`) et le gating du front suivent sans autre changement : `isEntitled` refuse tout en Gratuite. ### Contrôle Une commande, par exemple `check_license_revocation`, dont la logique vit dans des fonctions sans `AppHandle` (dossier temporaire et serveur mockito) : - **Dû** quand une clé est enregistrée et que l'état est absent, ou que `attempted_at` date de plus de 24 h, ou qu'il est dans le futur (horloge reculée). - `attempted_at` est écrit **avant** l'appel : deux appels concurrents n'en font qu'un, et « une tentative par 24 h » tient quel que soit le résultat. - `POST /v1/licenses/verify` avec `{product, license_key}`. Le serveur répond toujours 200. - `{valid:false, reason:"revoked"}` : `revoked: true`. `{valid:true}` : `revoked: false`. `not_found`, `expired` et toute erreur : état inchangé. - La commande rend l'issue du contrôle (non dû, valide, révoquée, serveur non joint, autre), que le front écrit dans le Journal. Aucun chemin serveur ne réactive une licence révoquée aujourd'hui (`revokeLicense` passe `is_revoked` à vrai et désactive les machines, rien ne fait l'inverse) : la levée sur `valid:true` ne sert qu'à une correction manuelle de la base. ### Autres sources de la révocation - `activate_machine`, `list_activated_machines` et `deactivate_machine` : sur `LicenseError::LicenseRevoked`, poser l'état avant de rendre l'erreur. - **Cours du Bilan.** `fetch_price_inner` (`balance_commands.rs`) traduit tout 403 en `PremiumRequired` sans lire `error.code`, alors que `licenseAuth.ts` distingue `license_revoked` de `premium_required`. Lire `error.code`, ajouter `LicenseRevoked` à `FetchPriceError` et à `PriceErrorCode`, le relier à la clé existante `balance.priceFetching.errors.licenseRevoked`, et poser l'état. Le jeton envoyé appartient à la clé enregistrée : `store_license_in` le supprime quand la clé change (#347). ### Front - `LicenseContext` lit l'état révoqué depuis Rust (par exemple un champ `revoked` de `LicenseInfo`, rendu par `read_license` et `store_license`). Le drapeau de session `LICENSE_REVOKED` et `markRevoked` disparaissent : après une réponse « révoquée », `useLicenseMachines` recharge le contexte. - Une clé révoquée ressaisie reste en Gratuite, y compris juste après la saisie (aujourd'hui `VALIDATE_DONE` prend l'édition de la clé). - Le contrôle part une fois par montage du fournisseur, après le premier chargement réussi avec une clé. Si l'état a changé, le contexte est rechargé. - Carte Licence : édition Gratuite, bandeau `license.errors.licenseRevoked`, boutons « Entrer une clé » et « Acheter ». La section Machines disparaît avec l'édition. - `UpsellGate` : une ligne (nouvelle clé `upsell.revoked`, FR et EN) quand la clé enregistrée est révoquée. ## Tâches - [ ] Rust : état `license.check` (lecture tolérante, écriture, condensé de la clé) et prise en compte dans `entitlements_from_dir`. - [ ] Rust : commande de contrôle, garde de 24 h, `attempted_at` écrit avant l'appel, fail-open, issue du contrôle rendue au front. - [ ] Rust : état posé sur `LicenseRevoked` par les trois commandes réseau, et par le 403 `license_revoked` des cours (`FetchPriceError::LicenseRevoked`). - [ ] Rust : `read_license` et `store_license` rendent l'état révoqué de la clé ; enregistrement de la commande dans `lib.rs`. - [ ] TS : `LicenseContext` (état lu depuis Rust, contrôle au démarrage, rechargement), `useLicenseMachines` (rechargement au lieu de `markRevoked`), `LicenseCard`, `UpsellGate`, `licenseService.ts`, `balance.service.ts`. - [ ] TS : chaque tentative dans le Journal (`logInfo`). - [ ] i18n FR et EN : `upsell.revoked` ; relire `license.errors.licenseRevoked` et `balance.priceFetching.errors.licenseRevoked`. - [ ] Docs : amendement #365 de l'ADR 0017 (D13 remplacée, première requête au démarrage et ses bornes), `docs/architecture.md`, `docs/guide-utilisateur.md` et `docs.editions.tips` (clé révoquée, et ce que l'app envoie et quand), `CHANGELOG.md` et `CHANGELOG.fr.md`. ## Tests Les décisions se testent sans `AppHandle` : dossier temporaire pour l'état, serveur mockito pour `verify`, valeurs attendues en littéraux. - [ ] Clé valide et état révoqué de la même clé : Gratuite, sans features. - [ ] État d'une autre clé, fichier illisible, JSON invalide : édition de la clé. - [ ] `auto-update` refusé après révocation (`is_entitled` sur l'édition résolue). - [ ] Contrôle : `revoked` pose l'état ; `valid:true` le lève ; `not_found`, `expired`, erreur réseau, 429, 5xx, corps illisible le laissent tel quel. - [ ] Garde : aucune requête sans clé, ni avant 24 h (mock avec `expect(0)`) ; requête due sans état, après 24 h, avec une autre clé, avec un `attempted_at` dans le futur ; `attempted_at` écrit même quand l'appel échoue. - [ ] La requête ne porte que `product` et `license_key`. - [ ] Le fichier d'état ne contient pas la clé. - [ ] Les trois commandes réseau et le 403 des cours posent l'état sur `license_revoked` ; un 403 `premium_required` ne le pose pas. - [ ] Front : le contexte prend l'état révoqué de Rust ; une clé révoquée ressaisie reste en Gratuite ; `UpsellGate` et `LicenseCard` nomment la révocation ; le contrôle part une seule fois et recharge le contexte quand l'état change. - [ ] Le bloc « revocation lasts the session » de `LicenseContext.test.ts` est réécrit : l'état vient de Rust et survit à la session. Tests existants touchés : `entitlements_from_dir` et `verify_*` (`license_commands.rs`), `fetch_price_inner` (`balance_commands.rs`), `entitlements.rs`, `LicenseContext.test.ts`, `useLicenseMachines.test.ts`, `LicenseCard.render.test.tsx`. ## Critères d'acceptation - [ ] Après un remboursement complet, l'app en ligne retombe en Gratuite au premier démarrage où un contrôle est dû, `auto-update` compris, et l'état révoqué reste affiché d'une session à l'autre. - [ ] Hors ligne, ou si le serveur est injoignable ou en erreur, aucune rétrogradation. - [ ] Une autre clé valide redonne son édition. - [ ] Aucune requête sans clé enregistrée, et au plus une tentative par 24 h. - [ ] La requête ne porte que le produit et la clé ; le fichier d'état ne contient pas la clé. - [ ] Un refus `license_revoked` des cours s'affiche comme une révocation, plus comme « abonnement premium requis ». - [ ] `cargo test`, `npm test` et la CI verts. - [ ] Vérification manuelle avec une licence réellement révoquée : la clé du contrôle d'achat du 2026-09-30 l'est déjà. ## Limites assumées - **Aucun changement hors ligne.** Bloquer le serveur de licences suffit à garder l'édition : c'est le soft-paywall de l'ADR 0017, pas un contrôle d'accès. - **Supprimer le fichier d'état** rend l'édition jusqu'au contrôle suivant, qui est alors dû dès le prochain démarrage. - **Une app laissée ouverte** n'est pas recontrôlée avant son redémarrage : aucun minuteur en cours de session. - **`not_found` et `expired`** n'ont aucun effet. `not_found` peut venir d'une base serveur restaurée ou d'une licence émise en mode test. - **Budget de requêtes.** Les routes de licence partagent 5 requêtes par minute et par adresse IP. Le parcours « limite de machines » en prend 4 : avec le contrôle du démarrage on atteint 5, sans marge. Un 429 est déjà expliqué à l'utilisateur, et n'a aucun effet sur le contrôle. ## Dépendances - #347 (D12, D13) et #350, mergées. Cette issue touche le même fichier que #350 : rien d'autre ne doit le réécrire en parallèle. - maximus/la-compagnie-maximus#184 : mention du contrôle périodique dans la politique de confidentialité. - maximus/la-compagnie-maximus#141 (go-live) : la vente est ouverte depuis le 2026-09-29. ## Complexité Moyenne, plus large que #350 : un PR, Rust et front, sans migration de base ni changement serveur.
maximus added the
status:triage
type:feature
source:human
labels 2026-09-30 23:53:53 +00:00
Author
Owner

Trouvé par la revue de la PR #364 (#347) et vérifié dans le code de main : côté cours du Bilan, une licence révoquée s'affiche comme une licence non Premium.

  • fetch_price_inner (src-tauri/src/commands/balance_commands.rs, match status) traduit tout 403 en FetchPriceError::PremiumRequired, sans lire error.code.
  • Or licenseAuth.ts (maximus-api) distingue license_revoked de premium_required, et docs/api-contract-prices.md prévoit errors.licenseRevoked pour le premier.
  • La clé balance.priceFetching.errors.licenseRevoked existe en FR et en EN, mais aucun code ne l'appelle.

Conséquence : un acheteur Premium remboursé lit « Cette fonction nécessite un abonnement premium » au lieu d'un message de révocation. Le correctif est à faire là où cette issue reprend l'affichage de la révocation : lire error.code sur un 403, ajouter une variante LicenseRevoked à FetchPriceError, et la relier à la clé existante.

PR #364 ne touche pas à ce chemin. Elle pose, côté carte Licence, un drapeau de session revoked dans LicenseContext (markRevoked), alimenté par activate, verify et deactivate. Ce drapeau peut servir de point d'entrée pour un refus de révocation venu des cours.

Trouvé par la revue de la PR #364 (#347) et vérifié dans le code de `main` : côté cours du Bilan, une licence révoquée s'affiche comme une licence non Premium. - `fetch_price_inner` (`src-tauri/src/commands/balance_commands.rs`, `match status`) traduit **tout** 403 en `FetchPriceError::PremiumRequired`, sans lire `error.code`. - Or `licenseAuth.ts` (maximus-api) distingue `license_revoked` de `premium_required`, et `docs/api-contract-prices.md` prévoit `errors.licenseRevoked` pour le premier. - La clé `balance.priceFetching.errors.licenseRevoked` existe en FR et en EN, mais aucun code ne l'appelle. Conséquence : un acheteur Premium remboursé lit « Cette fonction nécessite un abonnement premium » au lieu d'un message de révocation. Le correctif est à faire là où cette issue reprend l'affichage de la révocation : lire `error.code` sur un 403, ajouter une variante `LicenseRevoked` à `FetchPriceError`, et la relier à la clé existante. PR #364 ne touche pas à ce chemin. Elle pose, côté carte Licence, un drapeau de session `revoked` dans `LicenseContext` (`markRevoked`), alimenté par activate, verify et deactivate. Ce drapeau peut servir de point d'entrée pour un refus de révocation venu des cours.
Author
Owner

Analyse du 2026-10-01 (/analyze) : le corps est réécrit, le texte d'origine du 2026-09-30 est conservé plus bas.

Ce que l'analyse établit, vérifié sur main (a9fa34d), sur maximus-api (ac91da4) et dans les textes du site :

  • La prémisse tient. L'édition se résout hors ligne depuis la clé seule (current_entitlements, entitlements_from_dir), /v1/licenses/verify ne sert qu'à lister les machines, et les conditions de vente disent « le remboursement entraîne la révocation de la licence ».
  • Ce serait la première requête que l'app fait seule au démarrage. Mises à jour, cours et rétroaction partent sur un clic ; le contrôle du compte Maximus a une garde de 24 h mais ne part qu'à l'ouverture de Paramètres → Utilisateurs. Le corps d'origine ne le disait pas.
  • Deux inexactitudes du corps. Le drapeau « révoquée » vit dans LicenseContext, pas dans useLicenseMachines. La lecture de l'état va dans entitlements_from_dir : resolve_license_entitlements est pure et ne lit aucun fichier.
  • Un seul fichier d'état remplace le marqueur et l'horodatage séparés : une autre clé enregistrée rend l'état caduc, donc un contrôle dû.
  • Le refus license_revoked des cours (commentaire précédent) entre dans le périmètre, comme toute réponse « révoquée » apprise par une autre commande.
  • Côté serveur, aucun chemin ne réactive une licence révoquée aujourd'hui : la levée de l'état sur valid:true ne sert qu'à une correction manuelle de la base.

Décisions de Max (2026-10-01) : contrôle au démarrage, au plus une tentative par 24 h ; la révocation est nommée sur la carte Licence et sur chaque écran verrouillé, sans bandeau global.

Corps d'origine (2026-09-30)

Contexte

Constat de Max le 2026-09-30, pendant le go-live Stripe (maximus/la-compagnie-maximus#141) : achat reel d'une licence Base, remboursement complet, licence revoquee cote serveur (visible « revoquee » dans /compte), mais la cle reste pleinement fonctionnelle dans l'app.

Cause (code actuel, src-tauri/src/commands/license_commands.rs) :

  • L'edition se resout hors ligne, depuis la seule cle : signature Ed25519 + exp (current_entitlements -> resolve_license_entitlements). Depuis maximus-api#63 les cles durent 36525 jours : exp ne jouera jamais le role d'anti-revocation prevu a l'origine (decision #15 de spec-decisions-paiements).
  • /v1/licenses/verify n'est appele que pour lister les machines (verify_inner, useLicenseMachines). Un {valid:false, reason:"revoked"} affiche un message mais ne touche ni l'edition ni la cle.
  • #347 (analyse du 2026-09-29) l'a ecrit : D12 l'edition suit la seule cle ; D13 cle revoquee = drapeau de session, rien n'est persiste ; /v1/prices reste la seule barriere dure.

Consequence : apres un remboursement, l'acheteur garde l'edition Base hors ligne indefiniment, y compris la feature auto-update (FEATURE_TIERS dans entitlements.rs, gatee sur l'edition locale, #301). Les CGV du site (/conditions-de-vente, politique de remboursement) disent « le remboursement entraine la revocation de la licence » : seule la moitie serveur est tenue.

Decision proposee (option A, choisie par Max le 2026-09-30)

Verification de revocation en ligne, opportuniste, qui retrograde l'edition quand le serveur repond revoked ; aucun changement hors ligne.

  • Quand : au demarrage de l'app (apres chargement de la cle), puis au plus une fois par 24 h (horodatage du dernier controle persiste). Seulement si une cle est presente. Echec reseau / timeout / 5xx / 429 = aucune action (fail-open) : un utilisateur legitime hors ligne n'est jamais retrograde.
  • Quoi : POST /v1/licenses/verify ({product, license_key}), qui repond toujours 200 : {valid:true,...} ou {valid:false, reason: "revoked" | "not_found" | "expired"}.
  • Si reason == "revoked" : persister un marqueur de revocation lie a cette cle (fichier dans app_data_dir, par ex. license.revoked contenant un condense SHA-256 de la cle, jamais la cle en clair). resolve_license_entitlements retourne ("free", []) tant que le marqueur correspond a la cle courante. Coller une autre cle (nouvel achat) ne correspond plus au marqueur -> edition normale.
  • Si valid == true et qu'un marqueur existe pour cette cle (licence reactivee cote serveur) : supprimer le marqueur.
  • not_found / expired : hors perimetre, aucune action (a trancher separement ; not_found peut venir d'une base serveur restauree).
  • UI : la carte Licence affiche l'etat revoque de facon persistante (reutilise license.errors.licenseRevoked, #270) avec l'invite d'achat (#346). Cela remplace D13 (« rien n'est persiste ») pour le seul cas revoque.

Compatibilite :

  • D12 inchange : le jeton d'activation ne pese toujours pas sur l'edition. Cette issue ajoute une seule entree a la resolution : le marqueur de revocation.
  • Fail-closed deja en place (CWE-863) : toute erreur de lecture du marqueur doit etre traitee comme « pas de marqueur » (sinon un fichier corrompu retrograderait un client legitime).
  • Charge serveur : /v1/licenses/* partage une limite de 5 req/min/IP (maximus-api src/routes/licenses.ts). Un appel par jour et par poste est negligeable ; un 429 = aucune action.
  • Vie privee : la cle est deja envoyee a /activate et /verify ; ajouter une mention « verification periodique de la licence » dans la politique de confidentialite du site si elle n'y est pas (maximus/la-compagnie-maximus).

Taches

  • Rust : controle opportuniste au demarrage + garde 24 h (horodatage persiste), fail-open sur toute erreur reseau/serveur.
  • Rust : marqueur license.revoked (condense de la cle), lu par resolve_license_entitlements ; pose sur revoked, retire sur valid:true ; lecture en erreur = absent.
  • TS : carte Licence en etat revoque persistant (plus seulement de session) + invite d'achat ; useLicenseMachines reprend l'etat depuis Rust.
  • Tests Rust au point de decision : cle valide + marqueur de la meme cle -> Gratuite sans features ; marqueur d'une autre cle -> edition de la cle ; marqueur illisible -> edition de la cle ; valid:true efface le marqueur ; erreur reseau / 429 / 5xx -> aucune ecriture.
  • Test : auto-update refuse apres revocation (via check_entitlement).
  • CHANGELOG.md / CHANGELOG.fr.md (Added / Ajoute) + docs/guide-utilisateur.md §12 (licence revoquee apres remboursement).

Criteres d'acceptation

  • Apres un remboursement complet (licence revoquee cote serveur), l'app en ligne retombe en Gratuite au plus tard au demarrage suivant (ou dans les 24 h), auto-update compris, et l'etat revoque reste affiche d'une session a l'autre.
  • Hors ligne, ou si le serveur est injoignable / en erreur, aucune retrogradation.
  • Une nouvelle cle valide collee apres revocation redonne son edition.
  • Une licence reactivee cote serveur retrouve son edition au controle suivant.
  • Aucune cle en clair dans le marqueur.

Dependances

  • Apres #347 (D12 et le drapeau revoked de useLicenseMachines), dont cette issue amende D13.
  • Lie a maximus/la-compagnie-maximus#141 (go-live) : a livrer avant ou peu apres la premiere vente reelle.

Questions ouvertes (a trancher au /analyze)

  • Delai de 24 h vs verification a chaque demarrage seulement.
  • Faut-il prevenir l'utilisateur par une notification a la retrogradation, ou la carte Licence suffit-elle ?
**Analyse du 2026-10-01 (`/analyze`)** : le corps est réécrit, le texte d'origine du 2026-09-30 est conservé plus bas. Ce que l'analyse établit, vérifié sur `main` (`a9fa34d`), sur maximus-api (`ac91da4`) et dans les textes du site : - **La prémisse tient.** L'édition se résout hors ligne depuis la clé seule (`current_entitlements`, `entitlements_from_dir`), `/v1/licenses/verify` ne sert qu'à lister les machines, et les conditions de vente disent « le remboursement entraîne la révocation de la licence ». - **Ce serait la première requête que l'app fait seule au démarrage.** Mises à jour, cours et rétroaction partent sur un clic ; le contrôle du compte Maximus a une garde de 24 h mais ne part qu'à l'ouverture de Paramètres → Utilisateurs. Le corps d'origine ne le disait pas. - **Deux inexactitudes du corps.** Le drapeau « révoquée » vit dans `LicenseContext`, pas dans `useLicenseMachines`. La lecture de l'état va dans `entitlements_from_dir` : `resolve_license_entitlements` est pure et ne lit aucun fichier. - **Un seul fichier d'état** remplace le marqueur et l'horodatage séparés : une autre clé enregistrée rend l'état caduc, donc un contrôle dû. - **Le refus `license_revoked` des cours** (commentaire précédent) entre dans le périmètre, comme toute réponse « révoquée » apprise par une autre commande. - **Côté serveur**, aucun chemin ne réactive une licence révoquée aujourd'hui : la levée de l'état sur `valid:true` ne sert qu'à une correction manuelle de la base. Décisions de Max (2026-10-01) : contrôle au démarrage, au plus une tentative par 24 h ; la révocation est nommée sur la carte Licence et sur chaque écran verrouillé, sans bandeau global. <details> <summary>Corps d'origine (2026-09-30)</summary> ## Contexte Constat de Max le 2026-09-30, pendant le go-live Stripe (maximus/la-compagnie-maximus#141) : achat reel d'une licence Base, remboursement complet, licence **revoquee cote serveur** (visible « revoquee » dans /compte), mais **la cle reste pleinement fonctionnelle dans l'app**. Cause (code actuel, `src-tauri/src/commands/license_commands.rs`) : - L'edition se resout **hors ligne, depuis la seule cle** : signature Ed25519 + `exp` (`current_entitlements` -> `resolve_license_entitlements`). Depuis maximus-api#63 les cles durent 36525 jours : `exp` ne jouera jamais le role d'anti-revocation prevu a l'origine (decision #15 de spec-decisions-paiements). - `/v1/licenses/verify` n'est appele que pour lister les machines (`verify_inner`, `useLicenseMachines`). Un `{valid:false, reason:"revoked"}` affiche un message mais ne touche ni l'edition ni la cle. - #347 (analyse du 2026-09-29) l'a ecrit : **D12** l'edition suit la seule cle ; **D13** cle revoquee = drapeau de session, rien n'est persiste ; `/v1/prices` reste la seule barriere dure. Consequence : apres un remboursement, l'acheteur garde l'edition Base hors ligne indefiniment, y compris la feature `auto-update` (`FEATURE_TIERS` dans `entitlements.rs`, gatee sur l'edition locale, #301). Les CGV du site (`/conditions-de-vente`, politique de remboursement) disent « le remboursement entraine la revocation de la licence » : seule la moitie serveur est tenue. ## Decision proposee (option A, choisie par Max le 2026-09-30) Verification de revocation **en ligne, opportuniste**, qui retrograde l'edition quand le serveur repond `revoked` ; aucun changement hors ligne. - **Quand** : au demarrage de l'app (apres chargement de la cle), puis au plus **une fois par 24 h** (horodatage du dernier controle persiste). Seulement si une cle est presente. Echec reseau / timeout / 5xx / 429 = aucune action (fail-open) : un utilisateur legitime hors ligne n'est jamais retrograde. - **Quoi** : `POST /v1/licenses/verify` (`{product, license_key}`), qui repond toujours 200 : `{valid:true,...}` ou `{valid:false, reason: "revoked" | "not_found" | "expired"}`. - **Si `reason == "revoked"`** : persister un marqueur de revocation lie a **cette** cle (fichier dans `app_data_dir`, par ex. `license.revoked` contenant un condense SHA-256 de la cle, jamais la cle en clair). `resolve_license_entitlements` retourne `("free", [])` tant que le marqueur correspond a la cle courante. Coller une autre cle (nouvel achat) ne correspond plus au marqueur -> edition normale. - **Si `valid == true`** et qu'un marqueur existe pour cette cle (licence reactivee cote serveur) : supprimer le marqueur. - **`not_found` / `expired`** : hors perimetre, aucune action (a trancher separement ; `not_found` peut venir d'une base serveur restauree). - **UI** : la carte Licence affiche l'etat revoque de facon persistante (reutilise `license.errors.licenseRevoked`, #270) avec l'invite d'achat (#346). Cela remplace D13 (« rien n'est persiste ») pour le seul cas revoque. Compatibilite : - **D12 inchange** : le jeton d'activation ne pese toujours pas sur l'edition. Cette issue ajoute une seule entree a la resolution : le marqueur de revocation. - Fail-closed deja en place (CWE-863) : toute erreur de lecture du marqueur doit etre traitee comme « pas de marqueur » (sinon un fichier corrompu retrograderait un client legitime). - Charge serveur : `/v1/licenses/*` partage une limite de **5 req/min/IP** (maximus-api `src/routes/licenses.ts`). Un appel par jour et par poste est negligeable ; un 429 = aucune action. - Vie privee : la cle est deja envoyee a `/activate` et `/verify` ; ajouter une mention « verification periodique de la licence » dans la politique de confidentialite du site si elle n'y est pas (maximus/la-compagnie-maximus). ## Taches - [ ] Rust : controle opportuniste au demarrage + garde 24 h (horodatage persiste), fail-open sur toute erreur reseau/serveur. - [ ] Rust : marqueur `license.revoked` (condense de la cle), lu par `resolve_license_entitlements` ; pose sur `revoked`, retire sur `valid:true` ; lecture en erreur = absent. - [ ] TS : carte Licence en etat revoque persistant (plus seulement de session) + invite d'achat ; `useLicenseMachines` reprend l'etat depuis Rust. - [ ] Tests Rust au point de decision : cle valide + marqueur de la meme cle -> Gratuite sans features ; marqueur d'une autre cle -> edition de la cle ; marqueur illisible -> edition de la cle ; `valid:true` efface le marqueur ; erreur reseau / 429 / 5xx -> aucune ecriture. - [ ] Test : `auto-update` refuse apres revocation (via `check_entitlement`). - [ ] CHANGELOG.md / CHANGELOG.fr.md (Added / Ajoute) + `docs/guide-utilisateur.md` §12 (licence revoquee apres remboursement). ## Criteres d'acceptation - [ ] Apres un remboursement complet (licence revoquee cote serveur), l'app en ligne retombe en Gratuite au plus tard au demarrage suivant (ou dans les 24 h), `auto-update` compris, et l'etat revoque reste affiche d'une session a l'autre. - [ ] Hors ligne, ou si le serveur est injoignable / en erreur, aucune retrogradation. - [ ] Une nouvelle cle valide collee apres revocation redonne son edition. - [ ] Une licence reactivee cote serveur retrouve son edition au controle suivant. - [ ] Aucune cle en clair dans le marqueur. ## Dependances - Apres #347 (D12 et le drapeau `revoked` de `useLicenseMachines`), dont cette issue amende D13. - Lie a maximus/la-compagnie-maximus#141 (go-live) : a livrer avant ou peu apres la premiere vente reelle. ## Questions ouvertes (a trancher au /analyze) - Delai de 24 h vs verification a chaque demarrage seulement. - Faut-il prevenir l'utilisateur par une notification a la retrogradation, ou la carte Licence suffit-elle ? </details>
maximus changed title from feat(licence): révocation vérifiée en ligne — une clé remboursée retombe en Gratuite (option A) to feat(licence): révocation vérifiée en ligne — une clé remboursée retombe en Gratuite 2026-10-02 01:37:01 +00:00
maximus added
status:ready
and removed
status:triage
labels 2026-10-02 01:37:15 +00:00
maximus added
status:in-progress
and removed
status:ready
labels 2026-10-02 17:49:52 +00:00
Author
Owner

Décision du 2026-10-02, prise pendant /fix-issue : la route des cours nomme le refus license_revoked sans l'enregistrer comme une révocation de la clé. Le corps demandait le contraire (« Lire error.code […] et poser l'état »).

Ce que le plan checker a trouvé, et que le code confirme :

  • /v1/prices juge la licence du jeton d'activation. Le client ne sait pas relier ce jeton à la clé enregistrée : le sub du jeton est l'identifiant de la licence, la clé porte le courriel.
  • La phrase du corps « le jeton envoyé appartient à la clé enregistrée : store_license_in le supprime quand la clé change (#347) » n'est vraie que pour un dossier écrit depuis #347. Dans la v0.15.0, store_license écrit la clé sans toucher au jeton (git show v0.15.0:src-tauri/src/commands/license_commands.rs). Un dossier de données peut donc contenir une clé valide à côté du jeton d'une licence révoquée, par exemple après un rachat suivant un remboursement.
  • Enregistrer ce refus aurait rétrogradé la clé valide de ces dossiers en Gratuite.

Trois variantes ont été examinées : enregistrer quand même, confirmer par un appel à verify avant d'enregistrer (une requête de plus, hors des bornes de D1), ou nommer sans enregistrer. Max a retenu la troisième. Conséquences :

  • le critère « un refus license_revoked des cours s'affiche comme une révocation » est tenu, avec un message qui nomme l'activation plutôt que la clé ;
  • la tâche « état posé […] par le 403 license_revoked des cours » et le test correspondant sont abandonnés ;
  • la révocation de la clé enregistrée reste apprise par le contrôle du démarrage, l'activation, la liste des machines et la désactivation ;
  • le jeton périmé laissé par la v0.15.0 est suivi en #367.

L'amendement #365 de l'ADR 0017 consigne la décision.

**Décision du 2026-10-02, prise pendant `/fix-issue`** : la route des cours **nomme** le refus `license_revoked` sans l'enregistrer comme une révocation de la clé. Le corps demandait le contraire (« Lire `error.code` […] et poser l'état »). Ce que le plan checker a trouvé, et que le code confirme : - `/v1/prices` juge la licence du **jeton d'activation**. Le client ne sait pas relier ce jeton à la clé enregistrée : le `sub` du jeton est l'identifiant de la licence, la clé porte le courriel. - La phrase du corps « le jeton envoyé appartient à la clé enregistrée : `store_license_in` le supprime quand la clé change (#347) » n'est vraie que pour un dossier écrit depuis #347. Dans la v0.15.0, `store_license` écrit la clé sans toucher au jeton (`git show v0.15.0:src-tauri/src/commands/license_commands.rs`). Un dossier de données peut donc contenir une clé valide à côté du jeton d'une licence révoquée, par exemple après un rachat suivant un remboursement. - Enregistrer ce refus aurait rétrogradé la clé valide de ces dossiers en Gratuite. Trois variantes ont été examinées : enregistrer quand même, confirmer par un appel à `verify` avant d'enregistrer (une requête de plus, hors des bornes de D1), ou nommer sans enregistrer. Max a retenu la troisième. Conséquences : - le critère « un refus `license_revoked` des cours s'affiche comme une révocation » est tenu, avec un message qui nomme l'activation plutôt que la clé ; - la tâche « état posé […] par le 403 `license_revoked` des cours » et le test correspondant sont abandonnés ; - la révocation de la clé enregistrée reste apprise par le contrôle du démarrage, l'activation, la liste des machines et la désactivation ; - le jeton périmé laissé par la v0.15.0 est suivi en #367. L'amendement #365 de l'ADR 0017 consigne la décision.
maximus added
status:review
and removed
status:in-progress
labels 2026-10-02 19:12:00 +00:00
maximus added
status:approved
and removed
status:review
labels 2026-10-03 13:13:07 +00:00
maximus added
status:review
and removed
status:approved
labels 2026-10-03 13:31:24 +00:00
maximus added
status:blocked
and removed
status:review
labels 2026-10-03 13:43:27 +00:00
Author
Owner

Mergée le 2026-10-03 par la PR #368 (main b107cf4, fast-forward, CI verte sur la tête : runs 449 et 450). L'issue reste ouverte, en status:blocked, jusqu'à la vérification manuelle : la PR portait Refs, comme #350.

Ce qui reste à faire, par Max, avant le tag (détail dans la section « Vérification manuelle due » de la PR) :

  1. Enregistrer la clé du contrôle d'achat du 2026-09-30, réellement révoquée. L'édition Base s'affiche, puisque rien n'a encore été demandé au serveur.
  2. Redémarrer l'app. Attendu : le Journal porte « License check: the license server says the key was revoked » ; édition Gratuite ; bandeau sur la carte Licence ; ligne de révocation sur un écran verrouillé et sur la carte Mises à jour (après « Vérifier les mises à jour »).
  3. Redémarrer encore. Attendu : toujours Gratuite, et « last attempt less than 24 h ago, nothing sent » dans le Journal.
  4. Ressaisir la clé de testeur : l'édition revient. L'activation de la machine a été effacée par le changement de clé, il faut cliquer « Activer » pour retrouver les cours.

Pour rejouer le contrôle avant 24 h, supprimer license.check dans le dossier de données. Une fois ces points vérifiés, fermer l'issue à la main.

Décisions prises à la revue (2026-10-03) : une clé révoquée reste contrôlée une fois par 24 h tant qu'elle est enregistrée, et c'est déclaré dans le guide ; la carte Mises à jour nomme la révocation en Gratuite. Suites : #367 (jeton périmé laissé par la v0.15.0), maximus/la-compagnie-maximus#184 (politique de confidentialité).

**Mergée le 2026-10-03 par la PR #368** (`main` `b107cf4`, fast-forward, CI verte sur la tête : runs 449 et 450). L'issue reste ouverte, en `status:blocked`, jusqu'à la vérification manuelle : la PR portait `Refs`, comme #350. **Ce qui reste à faire, par Max, avant le tag** (détail dans la section « Vérification manuelle due » de la PR) : 1. Enregistrer la clé du contrôle d'achat du 2026-09-30, réellement révoquée. L'édition Base s'affiche, puisque rien n'a encore été demandé au serveur. 2. Redémarrer l'app. Attendu : le Journal porte « License check: the license server says the key was revoked » ; édition Gratuite ; bandeau sur la carte Licence ; ligne de révocation sur un écran verrouillé et sur la carte Mises à jour (après « Vérifier les mises à jour »). 3. Redémarrer encore. Attendu : toujours Gratuite, et « last attempt less than 24 h ago, nothing sent » dans le Journal. 4. Ressaisir la clé de testeur : l'édition revient. L'activation de la machine a été effacée par le changement de clé, il faut cliquer « Activer » pour retrouver les cours. Pour rejouer le contrôle avant 24 h, supprimer `license.check` dans le dossier de données. Une fois ces points vérifiés, fermer l'issue à la main. Décisions prises à la revue (2026-10-03) : une clé révoquée reste contrôlée une fois par 24 h tant qu'elle est enregistrée, et c'est déclaré dans le guide ; la carte Mises à jour nomme la révocation en Gratuite. Suites : #367 (jeton périmé laissé par la v0.15.0), maximus/la-compagnie-maximus#184 (politique de confidentialité).
Author
Owner

Vérification manuelle faite par Max le 2026-10-03, sur la v0.16.0 publiée (Linux) : la clé réellement révoquée du contrôle d'achat du 2026-09-30 fait passer l'app en Gratuite au démarrage, et le Journal porte la ligne du contrôle. Le dernier critère d'acceptation est tenu ; l'issue est fermée.

Observé en route, suivi ailleurs : saisie en cours de session, cette clé gardait l'édition Base jusqu'au clic sur « Activer », dont le refus license_revoked l'a fait passer en Gratuite. C'est le comportement livré (écart 2 de la PR #368 : contrôle au démarrage seulement) ; le contrôle dès la saisie est proposé en #373.

À rouvrir si une app redémarrée avec une clé révoquée reste dans l'édition de la clé alors que le serveur est joignable : le contrôle du démarrage serait alors en défaut.

**Vérification manuelle faite par Max le 2026-10-03, sur la v0.16.0 publiée (Linux)** : la clé réellement révoquée du contrôle d'achat du 2026-09-30 fait passer l'app en Gratuite au démarrage, et le Journal porte la ligne du contrôle. Le dernier critère d'acceptation est tenu ; l'issue est fermée. **Observé en route, suivi ailleurs** : saisie en cours de session, cette clé gardait l'édition Base jusqu'au clic sur « Activer », dont le refus `license_revoked` l'a fait passer en Gratuite. C'est le comportement livré (écart 2 de la PR #368 : contrôle au démarrage seulement) ; le contrôle dès la saisie est proposé en #373. À rouvrir si une app redémarrée avec une clé révoquée reste dans l'édition de la clé alors que le serveur est joignable : le contrôle du démarrage serait alors en défaut.
maximus added
status:approved
and removed
status:blocked
labels 2026-10-03 14:48:33 +00:00
Sign in to join this conversation.
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#365
No description provided.