fix: carte Licence — erreurs nommées, clé révoquée affichée, édition indépendante du jeton d'activation, machines n/limite, dates #347

Closed
opened 2026-09-14 00:19:31 +00:00 by maximus · 1 comment
Owner

Après #270 et #346, la carte Licence garde cinq défauts :

  • les erreurs de saisie de clé et l'erreur de chargement s'affichent en anglais brut ;
  • une clé révoquée n'est signalée qu'au moment du refus, puis le message disparaît ;
  • la limite de machines n'est jamais affichée ;
  • les dates suivent la langue du système, pas celle de l'app, et une clé de 100 ans afficherait une échéance en 2126 ;
  • le jeton d'activation peut faire retomber une clé valide en Gratuite, sans issue dans l'interface : au bout de 2 ans, parce que le jeton expire avant la clé, ou après une réinstallation ou une migration de poste avec restauration des données (#360).

C'est l'écran qu'un acheteur voit quand sa clé pose problème.

Décisions

  • D12 (analyse du 2026-09-29, ferme #360) : le jeton d'activation ne fait plus jamais retomber en Gratuite. L'édition et les features[] suivent la seule clé ; le jeton sert à l'état « Activée » de la carte et aux cours du Bilan, que le serveur vérifie. D12 remplace D10 (jeton expiré de cette machine = absent), qui en devient un cas particulier. Motif : une license.key copiée seule garde déjà son édition (état pré-activation toléré), donc la règle ne protégeait rien et ne pénalisait que la restauration légitime des données.
  • D13 (analyse du 2026-09-29) : clé révoquée = drapeau de session dans useLicenseMachines, avec le message existant license.errors.licenseRevoked posé par #270. Aucune clé i18n nouvelle.
  • D5, révisée en revue le 09-24 : quatre clés orphelines branchées (count, activating, deactivating, notActivated), deux retirées (removeLicense, activateError). Pas de commande de suppression de licence.
  • D9 : clés de 100 ans (maximus/maximus-api#63, mergée par la PR maximus-api#72). L'état « clé expirée le {date} » est abandonné. Une clé déjà expirée qu'on saisit reste nommée (license_expired), et « Sans échéance » s'affiche au-delà de 50 ans.
  • L'état révoqué ne dure que la session : rien n'est persisté.

Contexte vérifié (2026-09-29)

Relevé sur main 5d549d6 et, côté serveur, sur maximus-api main 3673d09. Se repérer aux noms de fonctions.

Client, src-tauri/src/commands/license_commands.rs et la carte :

  • strip_prefix, validate_with_key (« Invalid license: {erreur jsonwebtoken} », « Unknown edition »), store_license et read_license renvoient des chaînes anglaises. LicenseCard.tsx affiche validationError et l'erreur de chargement (licenseStatus === "error") telles quelles ; LicenseContext garde la chaîne brute.
  • Aucun appelant ne lit le détail de l'erreur de validate_with_key : validate_license_key et store_license la renvoient, read_license fait .ok(), resolve_license_entitlements fait let Ok(..) else.
  • LicenseError (posé par #270) porte déjà LicenseExpired, Storage et Internal ; son commentaire annonce l'extension de #347.
  • resolve_license_entitlements retombe en Gratuite dès qu'un jeton existe et ne valide pas (strict_validation(), leeway = 0). Un jeton absent est toléré : le test missing_activation_token_keeps_edition_and_features prouve qu'une clé copiée seule garde son édition. La contrainte 2 de l'ADR 0017 (« une license.key copiée sur une autre machine est déclassée en free ») est donc inexacte.
  • get_activation_status valide strictement : un jeton expiré ou d'une autre machine s'affiche « non activé », et le bouton « Activer » apparaît. Mais la section Machines est masquée en Gratuite (hasLicense), d'où l'impasse de #360.
  • LicenseInfo.machine_limit est un claim signé, disponible localement.
  • i18n : license.machines.count, .activating, .deactivating et .notActivated existent sans être utilisées ; license.removeLicense et license.machines.activateError sont mortes.
  • toLocaleDateString() est appelé sans locale (formatExpiry et la date d'activation d'une machine). Convention du dépôt : i18n.language === "fr" ? "fr-CA" : "en-CA", écrit en ligne.
  • jsonwebtoken 9.3.1 vérifie la présence des claims exigés indépendamment de validate_exp.
  • Un license_revoked affiche déjà « Cette clé de licence a été révoquée. » là où le refus arrive (#270) : sous l'en-tête Machines pour activate, dans la liste pour verify. Le message disparaît quand on referme la liste, l'édition garde sa coche verte, et « Activer » reste cliquable alors que le serveur répondra 403 à chaque essai.
  • Une fois le jeton expiré, le Bilan affiche balance.priceFetching.errors.authFailed (« Échec d'authentification — vérifie ta licence »), sans indiquer qu'il suffit de réactiver la machine.

Serveur, maximus-api :

  • Le jeton d'activation est signé pour 2 ans (signActivationJwt, src/crypto/ed25519.ts) ; #63 n'y a pas touché. Claims : product, machine_id, jti, sub (UUID de la licence), iat, exp.
  • Clés de 100 ans : LICENSE_DURATION_DAYS = 36525 pour /generate et pour Stripe, soit un exp vers 2126, au-delà de 2^32.
  • Activate contrôle dans cet ordre : clé introuvable, révoquée, expirée, machine déjà active, limite. Une machine déjà active reçoit un jeton neuf sans consommer de place. Une licence révoquée répond 403 license_revoked, même pour une machine active.
  • Verify répond toujours 200, avec {valid:false, reason: "revoked" | "not_found" | "expired"}.
  • Deactivate sur une licence révoquée répond 404 no_active_activation (la révocation désactive toutes les machines), que le client traite comme un succès.
  • /v1/prices valide le jeton strictement (exp, jti révoqué, licence révoquée) : il reste la seule barrière dure.
  • Un seul quota de 5 requêtes par minute et par IP est partagé par les routes /v1/licenses/*.

Tâches

  1. Erreurs locales nommées (Rust). Étendre LicenseError avec InvalidPrefix, InvalidSignature, Malformed et UnknownEdition, et réutiliser LicenseExpired, Storage et Internal. validate_with_key renvoie Result<LicenseInfo, LicenseError>, à sémantique inchangée : mêmes clés acceptées, mêmes refus. Correspondance :

    • strip_prefix → InvalidPrefix ;
    • ErrorKind::ExpiredSignature → LicenseExpired ;
    • ErrorKind::InvalidSignature → InvalidSignature ;
    • tout autre échec de décodage (JWT illisible, claim manquant, algorithme inattendu) → Malformed ;
    • édition ni base ni premium → UnknownEdition ;
    • clé publique embarquée illisible → Internal.

    validate_license_key, store_license et read_license renvoient le JSON via license_error_to_string ; leurs erreurs de fichier deviennent Storage.

  2. Côté TS. Ajouter les codes à LicenseErrorCode et à LICENSE_ERROR_I18N_MAP, et les clés FR/EN license.errors.invalidPrefix, invalidSignature, malformed et unknownEdition, au vouvoiement. LicenseCard.tsx passe validationError et l'erreur de chargement par parseLicenseError, puis par t(...) ; le contexte garde la chaîne brute.

  3. L'édition ne dépend plus du jeton (D12, ferme #360). L'édition et les features[] se résolvent depuis la seule clé : une clé valide et non expirée donne son édition, quel que soit le jeton (absent, expiré, d'une autre machine, illisible) ; une clé invalide ou expirée donne ("free", []). resolve_license_entitlements ne prend plus le jeton ni l'identifiant machine, et current_entitlements ne les lit plus. Pour tester au point de décision, extraire la lecture de fichiers de current_entitlements dans une fonction qui prend le dossier de données, testée avec std::env::temp_dir() comme dans token_store.rs.

    Le jeton garde une validation stricte là où il décide de quelque chose : get_activation_status (« Activée » / « Non activée ») et persist_activation_token (refus d'un jeton serveur qui ne vaut pas pour cette machine). Mettre à jour les commentaires qui décrivent l'ancienne règle : en-tête du module, get_edition, current_entitlements, machine_id_internal.

    Conséquence voulue : après une réinstallation avec restauration des données, la clé garde son édition, et la section Machines reste visible avec « Non activée » et « Activer ». L'activation écrase l'ancien jeton ; elle consomme une place, et la limite ouvre la liste comme aujourd'hui. Avec un jeton expiré de cette machine, « Activer » renouvelle le jeton sans consommer de place.

  4. Clé révoquée (D13). Ajouter revoked: boolean à LicenseMachinesState, posé par tout license_revoked (ACTIVATE_ERROR, LIST_LOAD_ERROR, DEACTIVATE_ERROR), jamais remis à zéro pendant la session, jamais persisté. La carte affiche license.errors.licenseRevoked à son niveau, visible liste fermée ; une icône d'alerte remplace la coche verte de l'édition, et selectCanActivate renvoie false. L'édition suit toujours la validation locale (ADR 0017).

  5. Machines n/limite. Une fois la liste chargée sans erreur, afficher license.machines.count avec {count: machines.length, limit: info.machine_limit}.

  6. Clés orphelines (D5). license.machines.activating et deactivating servent de libellés aux boutons pendant le chargement ; license.machines.notActivated s'affiche quand la machine n'est pas activée ; license.removeLicense et license.machines.activateError sont retirées des deux langues ; license.errors.unknown reste le seul repli générique.

  7. Échéance (D9). Fonction pure dans src/shared/licenseExpiry.ts : au-delà de 50 ans, « Sans échéance » / « No expiry date » (clé license.noExpiry) ; sinon, la date dans la langue de l'app.

  8. Dates. formatExpiry et la date d'activation d'une machine : toLocaleDateString(i18n.language === "fr" ? "fr-CA" : "en-CA").

  9. Cours du Bilan. Reformuler balance.priceFetching.errors.authFailed (FR/EN) pour indiquer d'activer la machine dans Paramètres → Utilisateurs, dans le registre des messages voisins. Le même 401 couvre un jeton absent, expiré ou invalide.

  10. Tests.

    • Rust : chaque variante locale nommée (préfixe, signature d'une autre clé, JWT corrompu, clé expirée, édition inconnue). Les rejects_* existants vérifient la variante, plus seulement is_err() ; rejects_expired_license reste vert et vérifie LicenseExpired.
    • Rust, au point de décision (dossier temporaire avec license.key) : clé expirée → Gratuite sans features ; clé valide avec un activation.token expiré de cette machine, d'une autre machine ou illisible → édition et features conservées. Ces tests remplacent machine_match_keeps_edition_and_features, machine_mismatch_downgrades_to_free_and_drops_features et missing_activation_token_keeps_edition_and_features, dont le sujet disparaît.
    • Rust, la validation stricte du jeton reste : validate_activation_with_key refuse un jeton expiré et un jeton d'une autre machine.
    • Rust, tests de #363 : une clé avec exp = now + 36525 * 86400 est acceptée, et expires_at vaut cette valeur ; la même clé avec un exp passé est refusée.
    • vitest : nouveaux codes → clés ; toutes les clés license.errors.* présentes en FR et en EN (test existant) ; seuil « Sans échéance », dont une clé de 100 ans ; transition revoked du reducer et selectCanActivate.
    • vitest, rendu de LicenseCard (renderToStaticMarkup, contexte et hook remplacés par des faux, comme UpsellCtas.render.test.tsx) : aucune erreur anglaise brute sur la carte en français, message révoqué visible liste fermée, « n/limite », « Sans échéance ».
  11. Documentation.

    • CHANGELOG.md et CHANGELOG.fr.md : Fixed / Corrigé (l'édition ne dépend plus du jeton d'activation, qu'il soit expiré ou issu d'une réinstallation ou d'une migration de poste ; erreurs de saisie nommées ; dates dans la langue de l'app) et Changed / Modifié (clé révoquée affichée, compteur de machines, « Sans échéance »).
    • docs/guide-utilisateur.md §12 : nouvelle sous-section sur la carte Licence (activation et réactivation pour les cours, clé révoquée, compteur de machines, « Sans échéance »), avec son pendant dans les docs intégrées (docs.editions.*, FR/EN).
    • docs/architecture.md, §license_commands.rs : règle de résolution (la clé seule) et rôle du jeton (état de la carte, serveur).
    • ADR 0017 : amendement (#347) qui corrige la contrainte 2 (la licence n'est liée à la machine que côté serveur) et reformule la règle CWE-863 locale : fail-closed en Gratuite pour une clé invalide ou expirée, features[] signés qui suivent la clé.

Fichiers concernés

  • src-tauri/src/commands/license_commands.rs
  • src/services/licenseService.ts, src/services/licenseService.test.ts
  • src/hooks/useLicenseMachines.ts, src/hooks/useLicenseMachines.test.ts
  • src/shared/licenseExpiry.ts, src/shared/licenseExpiry.test.ts (nouveaux)
  • src/components/settings/LicenseCard.tsx, src/components/settings/LicenseCard.render.test.tsx (nouveau)
  • src/i18n/locales/fr.json, src/i18n/locales/en.json
  • docs/adr/0017-feature-gating-par-tier.md, docs/guide-utilisateur.md, docs/architecture.md
  • CHANGELOG.md, CHANGELOG.fr.md

Critères d'acceptation

  • Aucune chaîne anglaise en dur n'atteint la carte Licence en français : saisie, chargement et refus serveur passent par license.errors.*.
  • Une clé valide garde son édition et ses features quel que soit le jeton (absent, expiré, d'une autre machine, illisible), prouvé par des tests Rust au point de décision. Une clé invalide ou expirée résout toujours ("free", []).
  • Une machine dont le jeton est expiré ou appartient à une autre machine affiche « Non activée » et peut (ré)activer depuis la carte.
  • Un license_revoked reçu du serveur reste affiché au niveau de la carte pendant la session, et « Activer » est désactivé.
  • La liste des machines affiche « {n}/{limite} machines activées ».
  • Une clé dont l'échéance est à plus de 50 ans affiche « Sans échéance » (clé de 100 ans testée) ; les autres dates suivent la langue de l'app (fr-CA / en-CA).
  • license.removeLicense et license.machines.activateError n'existent plus ; activating, deactivating, notActivated et count sont utilisées.
  • cargo test, npm test et npm run build verts ; CHANGELOG FR/EN ; guide §12, docs intégrées et ADR 0017 à jour.

Points de vigilance

  • La clé de licence reste validée strictement (exp et iat exigés, leeway = 0, CWE-613). Ne jamais lui appliquer une validation qui n'impose pas exp.
  • D12 ne touche que le poids du jeton sur l'édition locale. get_activation_status et persist_activation_token gardent la validation stricte (signature, machine_id, exp) : ne pas les assouplir.
  • Prouver la règle au point de décision : tester la résolution à partir du dossier de données, pas seulement validate_with_key.
  • license.errors.unknown reste le seul repli générique (#270) ; activateError affichait « Activation failed » en anglais.
  • fr.json, en.json, les deux CHANGELOG, le guide §12 et l'ADR 0017 ont été modifiés par #346, mergée : partir de main.

Hors périmètre

  • Avertissement avant l'échéance : les clés durent désormais 100 ans.
  • Alignement côté serveur de la durée du jeton d'activation (2 ans).
  • Hachage de l'identifiant machine → #350. D12 retire le risque de sa transition pour l'édition locale ; la réactivation pour les cours reste nécessaire.
  • Côté serveur, /v1/prices ne vérifie pas que la machine est encore active : une machine désactivée garde les cours jusqu'à l'expiration de son jeton. À traiter dans maximus-api.
  • Reste de #363 : plafond de /issue-license à 36525 jours une fois maximus-api#63 déployée, et essai d'une vraie clé émise après #63.

Liens

Spec source

spec-plan-paiements.md (séance /plan-run du 2026-09-24, fichier de travail ignoré par git), puis analyse /analyze du 2026-09-29. Le corps du 09-24 est archivé en commentaire. Ce corps se suffit à lui-même.

Après #270 et #346, la carte Licence garde cinq défauts : - les erreurs de saisie de clé et l'erreur de chargement s'affichent en anglais brut ; - une clé révoquée n'est signalée qu'au moment du refus, puis le message disparaît ; - la limite de machines n'est jamais affichée ; - les dates suivent la langue du système, pas celle de l'app, et une clé de 100 ans afficherait une échéance en 2126 ; - le jeton d'activation peut faire retomber une clé valide en Gratuite, sans issue dans l'interface : au bout de 2 ans, parce que le jeton expire avant la clé, ou après une réinstallation ou une migration de poste avec restauration des données (#360). C'est l'écran qu'un acheteur voit quand sa clé pose problème. ## Décisions - **D12 (analyse du 2026-09-29, ferme #360)** : le jeton d'activation ne fait plus jamais retomber en Gratuite. L'édition et les `features[]` suivent la seule clé ; le jeton sert à l'état « Activée » de la carte et aux cours du Bilan, que le serveur vérifie. D12 remplace D10 (jeton expiré de cette machine = absent), qui en devient un cas particulier. Motif : une `license.key` copiée seule garde déjà son édition (état pré-activation toléré), donc la règle ne protégeait rien et ne pénalisait que la restauration légitime des données. - **D13 (analyse du 2026-09-29)** : clé révoquée = drapeau de session dans `useLicenseMachines`, avec le message existant `license.errors.licenseRevoked` posé par #270. Aucune clé i18n nouvelle. - D5, révisée en revue le 09-24 : quatre clés orphelines branchées (`count`, `activating`, `deactivating`, `notActivated`), deux retirées (`removeLicense`, `activateError`). Pas de commande de suppression de licence. - D9 : clés de 100 ans (maximus/maximus-api#63, mergée par la PR maximus-api#72). L'état « clé expirée le {date} » est abandonné. Une clé déjà expirée qu'on saisit reste nommée (`license_expired`), et « Sans échéance » s'affiche au-delà de 50 ans. - L'état révoqué ne dure que la session : rien n'est persisté. ## Contexte vérifié (2026-09-29) Relevé sur `main` `5d549d6` et, côté serveur, sur `maximus-api` `main` `3673d09`. Se repérer aux noms de fonctions. Client, `src-tauri/src/commands/license_commands.rs` et la carte : - `strip_prefix`, `validate_with_key` (« Invalid license: {erreur jsonwebtoken} », « Unknown edition »), `store_license` et `read_license` renvoient des chaînes anglaises. `LicenseCard.tsx` affiche `validationError` et l'erreur de chargement (`licenseStatus === "error"`) telles quelles ; `LicenseContext` garde la chaîne brute. - Aucun appelant ne lit le détail de l'erreur de `validate_with_key` : `validate_license_key` et `store_license` la renvoient, `read_license` fait `.ok()`, `resolve_license_entitlements` fait `let Ok(..) else`. - `LicenseError` (posé par #270) porte déjà `LicenseExpired`, `Storage` et `Internal` ; son commentaire annonce l'extension de #347. - `resolve_license_entitlements` retombe en Gratuite dès qu'un jeton existe et ne valide pas (`strict_validation()`, `leeway = 0`). Un jeton absent est toléré : le test `missing_activation_token_keeps_edition_and_features` prouve qu'une clé copiée seule garde son édition. La contrainte 2 de l'ADR 0017 (« une `license.key` copiée sur une autre machine est déclassée en `free` ») est donc inexacte. - `get_activation_status` valide strictement : un jeton expiré ou d'une autre machine s'affiche « non activé », et le bouton « Activer » apparaît. Mais la section Machines est masquée en Gratuite (`hasLicense`), d'où l'impasse de #360. - `LicenseInfo.machine_limit` est un claim signé, disponible localement. - i18n : `license.machines.count`, `.activating`, `.deactivating` et `.notActivated` existent sans être utilisées ; `license.removeLicense` et `license.machines.activateError` sont mortes. - `toLocaleDateString()` est appelé sans locale (`formatExpiry` et la date d'activation d'une machine). Convention du dépôt : `i18n.language === "fr" ? "fr-CA" : "en-CA"`, écrit en ligne. - `jsonwebtoken` 9.3.1 vérifie la présence des claims exigés indépendamment de `validate_exp`. - Un `license_revoked` affiche déjà « Cette clé de licence a été révoquée. » là où le refus arrive (#270) : sous l'en-tête Machines pour activate, dans la liste pour verify. Le message disparaît quand on referme la liste, l'édition garde sa coche verte, et « Activer » reste cliquable alors que le serveur répondra 403 à chaque essai. - Une fois le jeton expiré, le Bilan affiche `balance.priceFetching.errors.authFailed` (« Échec d'authentification — vérifie ta licence »), sans indiquer qu'il suffit de réactiver la machine. Serveur, `maximus-api` : - Le jeton d'activation est signé pour 2 ans (`signActivationJwt`, `src/crypto/ed25519.ts`) ; #63 n'y a pas touché. Claims : `product`, `machine_id`, `jti`, `sub` (UUID de la licence), `iat`, `exp`. - Clés de 100 ans : `LICENSE_DURATION_DAYS = 36525` pour `/generate` et pour Stripe, soit un `exp` vers 2126, au-delà de 2^32. - Activate contrôle dans cet ordre : clé introuvable, révoquée, expirée, machine déjà active, limite. Une machine déjà active reçoit un jeton neuf sans consommer de place. Une licence révoquée répond 403 `license_revoked`, même pour une machine active. - Verify répond toujours 200, avec `{valid:false, reason: "revoked" | "not_found" | "expired"}`. - Deactivate sur une licence révoquée répond 404 `no_active_activation` (la révocation désactive toutes les machines), que le client traite comme un succès. - `/v1/prices` valide le jeton strictement (`exp`, `jti` révoqué, licence révoquée) : il reste la seule barrière dure. - Un seul quota de 5 requêtes par minute et par IP est partagé par les routes `/v1/licenses/*`. ## Tâches 1. **Erreurs locales nommées (Rust).** Étendre `LicenseError` avec `InvalidPrefix`, `InvalidSignature`, `Malformed` et `UnknownEdition`, et réutiliser `LicenseExpired`, `Storage` et `Internal`. `validate_with_key` renvoie `Result<LicenseInfo, LicenseError>`, à sémantique inchangée : mêmes clés acceptées, mêmes refus. Correspondance : - `strip_prefix` → `InvalidPrefix` ; - `ErrorKind::ExpiredSignature` → `LicenseExpired` ; - `ErrorKind::InvalidSignature` → `InvalidSignature` ; - tout autre échec de décodage (JWT illisible, claim manquant, algorithme inattendu) → `Malformed` ; - édition ni `base` ni `premium` → `UnknownEdition` ; - clé publique embarquée illisible → `Internal`. `validate_license_key`, `store_license` et `read_license` renvoient le JSON via `license_error_to_string` ; leurs erreurs de fichier deviennent `Storage`. 2. **Côté TS.** Ajouter les codes à `LicenseErrorCode` et à `LICENSE_ERROR_I18N_MAP`, et les clés FR/EN `license.errors.invalidPrefix`, `invalidSignature`, `malformed` et `unknownEdition`, au vouvoiement. `LicenseCard.tsx` passe `validationError` et l'erreur de chargement par `parseLicenseError`, puis par `t(...)` ; le contexte garde la chaîne brute. 3. **L'édition ne dépend plus du jeton (D12, ferme #360).** L'édition et les `features[]` se résolvent depuis la seule clé : une clé valide et non expirée donne son édition, quel que soit le jeton (absent, expiré, d'une autre machine, illisible) ; une clé invalide ou expirée donne `("free", [])`. `resolve_license_entitlements` ne prend plus le jeton ni l'identifiant machine, et `current_entitlements` ne les lit plus. Pour tester au point de décision, extraire la lecture de fichiers de `current_entitlements` dans une fonction qui prend le dossier de données, testée avec `std::env::temp_dir()` comme dans `token_store.rs`. Le jeton garde une validation stricte là où il décide de quelque chose : `get_activation_status` (« Activée » / « Non activée ») et `persist_activation_token` (refus d'un jeton serveur qui ne vaut pas pour cette machine). Mettre à jour les commentaires qui décrivent l'ancienne règle : en-tête du module, `get_edition`, `current_entitlements`, `machine_id_internal`. Conséquence voulue : après une réinstallation avec restauration des données, la clé garde son édition, et la section Machines reste visible avec « Non activée » et « Activer ». L'activation écrase l'ancien jeton ; elle consomme une place, et la limite ouvre la liste comme aujourd'hui. Avec un jeton expiré de cette machine, « Activer » renouvelle le jeton sans consommer de place. 4. **Clé révoquée (D13).** Ajouter `revoked: boolean` à `LicenseMachinesState`, posé par tout `license_revoked` (`ACTIVATE_ERROR`, `LIST_LOAD_ERROR`, `DEACTIVATE_ERROR`), jamais remis à zéro pendant la session, jamais persisté. La carte affiche `license.errors.licenseRevoked` à son niveau, visible liste fermée ; une icône d'alerte remplace la coche verte de l'édition, et `selectCanActivate` renvoie `false`. L'édition suit toujours la validation locale (ADR 0017). 5. **Machines n/limite.** Une fois la liste chargée sans erreur, afficher `license.machines.count` avec `{count: machines.length, limit: info.machine_limit}`. 6. **Clés orphelines (D5).** `license.machines.activating` et `deactivating` servent de libellés aux boutons pendant le chargement ; `license.machines.notActivated` s'affiche quand la machine n'est pas activée ; `license.removeLicense` et `license.machines.activateError` sont retirées des deux langues ; `license.errors.unknown` reste le seul repli générique. 7. **Échéance (D9).** Fonction pure dans `src/shared/licenseExpiry.ts` : au-delà de 50 ans, « Sans échéance » / « No expiry date » (clé `license.noExpiry`) ; sinon, la date dans la langue de l'app. 8. **Dates.** `formatExpiry` et la date d'activation d'une machine : `toLocaleDateString(i18n.language === "fr" ? "fr-CA" : "en-CA")`. 9. **Cours du Bilan.** Reformuler `balance.priceFetching.errors.authFailed` (FR/EN) pour indiquer d'activer la machine dans Paramètres → Utilisateurs, dans le registre des messages voisins. Le même 401 couvre un jeton absent, expiré ou invalide. 10. **Tests.** - Rust : chaque variante locale nommée (préfixe, signature d'une autre clé, JWT corrompu, clé expirée, édition inconnue). Les `rejects_*` existants vérifient la variante, plus seulement `is_err()` ; `rejects_expired_license` reste vert et vérifie `LicenseExpired`. - Rust, au point de décision (dossier temporaire avec `license.key`) : clé expirée → Gratuite sans features ; clé valide avec un `activation.token` expiré de cette machine, d'une autre machine ou illisible → édition et features conservées. Ces tests remplacent `machine_match_keeps_edition_and_features`, `machine_mismatch_downgrades_to_free_and_drops_features` et `missing_activation_token_keeps_edition_and_features`, dont le sujet disparaît. - Rust, la validation stricte du jeton reste : `validate_activation_with_key` refuse un jeton expiré et un jeton d'une autre machine. - Rust, tests de #363 : une clé avec `exp = now + 36525 * 86400` est acceptée, et `expires_at` vaut cette valeur ; la même clé avec un `exp` passé est refusée. - vitest : nouveaux codes → clés ; toutes les clés `license.errors.*` présentes en FR et en EN (test existant) ; seuil « Sans échéance », dont une clé de 100 ans ; transition `revoked` du reducer et `selectCanActivate`. - vitest, rendu de `LicenseCard` (`renderToStaticMarkup`, contexte et hook remplacés par des faux, comme `UpsellCtas.render.test.tsx`) : aucune erreur anglaise brute sur la carte en français, message révoqué visible liste fermée, « n/limite », « Sans échéance ». 11. **Documentation.** - `CHANGELOG.md` et `CHANGELOG.fr.md` : Fixed / Corrigé (l'édition ne dépend plus du jeton d'activation, qu'il soit expiré ou issu d'une réinstallation ou d'une migration de poste ; erreurs de saisie nommées ; dates dans la langue de l'app) et Changed / Modifié (clé révoquée affichée, compteur de machines, « Sans échéance »). - `docs/guide-utilisateur.md` §12 : nouvelle sous-section sur la carte Licence (activation et réactivation pour les cours, clé révoquée, compteur de machines, « Sans échéance »), avec son pendant dans les docs intégrées (`docs.editions.*`, FR/EN). - `docs/architecture.md`, §`license_commands.rs` : règle de résolution (la clé seule) et rôle du jeton (état de la carte, serveur). - ADR 0017 : amendement (#347) qui corrige la contrainte 2 (la licence n'est liée à la machine que côté serveur) et reformule la règle CWE-863 locale : fail-closed en Gratuite pour une clé invalide ou expirée, `features[]` signés qui suivent la clé. ## Fichiers concernés - `src-tauri/src/commands/license_commands.rs` - `src/services/licenseService.ts`, `src/services/licenseService.test.ts` - `src/hooks/useLicenseMachines.ts`, `src/hooks/useLicenseMachines.test.ts` - `src/shared/licenseExpiry.ts`, `src/shared/licenseExpiry.test.ts` (nouveaux) - `src/components/settings/LicenseCard.tsx`, `src/components/settings/LicenseCard.render.test.tsx` (nouveau) - `src/i18n/locales/fr.json`, `src/i18n/locales/en.json` - `docs/adr/0017-feature-gating-par-tier.md`, `docs/guide-utilisateur.md`, `docs/architecture.md` - `CHANGELOG.md`, `CHANGELOG.fr.md` ## Critères d'acceptation - [ ] Aucune chaîne anglaise en dur n'atteint la carte Licence en français : saisie, chargement et refus serveur passent par `license.errors.*`. - [ ] Une clé valide garde son édition et ses features quel que soit le jeton (absent, expiré, d'une autre machine, illisible), prouvé par des tests Rust au point de décision. Une clé invalide ou expirée résout toujours `("free", [])`. - [ ] Une machine dont le jeton est expiré ou appartient à une autre machine affiche « Non activée » et peut (ré)activer depuis la carte. - [ ] Un `license_revoked` reçu du serveur reste affiché au niveau de la carte pendant la session, et « Activer » est désactivé. - [ ] La liste des machines affiche « {n}/{limite} machines activées ». - [ ] Une clé dont l'échéance est à plus de 50 ans affiche « Sans échéance » (clé de 100 ans testée) ; les autres dates suivent la langue de l'app (`fr-CA` / `en-CA`). - [ ] `license.removeLicense` et `license.machines.activateError` n'existent plus ; `activating`, `deactivating`, `notActivated` et `count` sont utilisées. - [ ] `cargo test`, `npm test` et `npm run build` verts ; CHANGELOG FR/EN ; guide §12, docs intégrées et ADR 0017 à jour. ## Points de vigilance - La clé de licence reste validée strictement (`exp` et `iat` exigés, `leeway = 0`, CWE-613). Ne jamais lui appliquer une validation qui n'impose pas `exp`. - D12 ne touche que le poids du jeton sur l'édition locale. `get_activation_status` et `persist_activation_token` gardent la validation stricte (signature, `machine_id`, `exp`) : ne pas les assouplir. - Prouver la règle au point de décision : tester la résolution à partir du dossier de données, pas seulement `validate_with_key`. - `license.errors.unknown` reste le seul repli générique (#270) ; `activateError` affichait « Activation failed » en anglais. - `fr.json`, `en.json`, les deux CHANGELOG, le guide §12 et l'ADR 0017 ont été modifiés par #346, mergée : partir de `main`. ## Hors périmètre - Avertissement avant l'échéance : les clés durent désormais 100 ans. - Alignement côté serveur de la durée du jeton d'activation (2 ans). - Hachage de l'identifiant machine → #350. D12 retire le risque de sa transition pour l'édition locale ; la réactivation pour les cours reste nécessaire. - Côté serveur, `/v1/prices` ne vérifie pas que la machine est encore active : une machine désactivée garde les cours jusqu'à l'expiration de son jeton. À traiter dans maximus-api. - Reste de #363 : plafond de `/issue-license` à 36525 jours une fois maximus-api#63 déployée, et essai d'une vraie clé émise après #63. ## Liens - Dépend de #270 (PR #359) et de #346 (PR #362), mergées. - Ferme #360 (D12). - Absorbe les tests de #363. ## Spec source `spec-plan-paiements.md` (séance `/plan-run` du 2026-09-24, fichier de travail ignoré par git), puis analyse `/analyze` du 2026-09-29. Le corps du 09-24 est archivé en commentaire. Ce corps se suffit à lui-même.
maximus added this to the spec-paiements milestone 2026-09-14 00:19:31 +00:00
maximus added the
status:ready
type:bug
source:analyste
labels 2026-09-14 00:19:31 +00:00
maximus changed title from bug: carte Licence — erreurs en anglais brut, clé expirée ou révoquée muette, limite de machines jamais affichée to fix: carte Licence — erreurs de saisie nommées, clé révoquée explicite, jeton d'activation expiré toléré, machines n/limite, dates 2026-09-25 00:49:57 +00:00
maximus modified the milestone from spec-paiements to planned-2026-09-24-paiements 2026-09-25 00:49:57 +00:00
Author
Owner

Corps du 2026-09-24 (séance /plan-run, revu en /review-spec), archivé avant la réécriture par l'analyse du 2026-09-29. Deux décisions nouvelles le modifient : D12 (le jeton d'activation ne fait plus jamais retomber en Gratuite, ce qui ferme #360 et englobe D10) et D13 (clé révoquée : drapeau de session, message existant de #270). Le reste du plan tient, vérifié contre main 5d549d6 et maximus-api main 3673d09.

Titre et corps d'origine

Titre : fix: carte Licence — erreurs de saisie nommées, clé révoquée explicite, jeton d'activation expiré toléré, machines n/limite, dates

Après #270, les refus du serveur sont nommés. Il reste quatre problèmes dans la carte Licence :

  • les erreurs de saisie de clé s'affichent en anglais brut ;
  • une clé révoquée n'a pas d'état dédié ;
  • la limite de machines n'est jamais affichée ;
  • les dates suivent la langue du système, pas celle de l'app.

La revue a aussi trouvé un défaut de fond : une machine qui a cliqué « Activer » retombe en Gratuite, sans message, au bout de 2 ans, parce que le jeton d'activation expire avant la clé. C'est l'écran qu'un acheteur voit quand sa clé pose problème.

Contexte vérifié (2026-09-24)

Numéros de ligne relevés sur main. #270 et #346 les déplacent : se repérer aux noms de fonctions.

  • LicenseCard.tsx affiche telles quelles des erreurs locales en anglais (validationError l.182-187, erreur de chargement l.143-148) :

    • strip_prefix (license_commands.rs:94) : « License key must start with SR-BASE- or SR-PREMIUM- » ;
    • validate_with_key (l.122) : « Invalid license: {erreur jsonwebtoken} », par exemple ExpiredSignature ou InvalidSignature ;
    • l.126 : « Unknown edition » ;
    • store_license (l.170-179) : erreurs d'I/O.

    LicenseContext.submitKey (l.138-147) garde la chaîne brute dans validationError.

  • Les appelants de validate_with_key ignorent tous le détail de l'erreur : validate_license_key et store_license la renvoient, read_license fait .ok(), resolve_license_entitlements fait let Ok(..) else. Son type d'erreur peut donc devenir LicenseError sans rien changer à la résolution d'édition.

  • Jeton d'activation :

    • maximus-api le signe pour 2 ans (maximus-api/src/crypto/ed25519.ts:256, signActivationJwt, 2 * 365 * 24 * 3600), alors que la clé dure 3650 jours ;
    • le client le valide avec strict_validation() (license_commands.rs:141-155), qui applique exp avec leeway = 0 ;
    • resolve_license_entitlements (l.249-266) retombe en Gratuite dès qu'un jeton existe et ne valide pas.

    Résultat : une machine activée perd son édition au bout de 2 ans, alors qu'une machine jamais activée la garde (état « pré-activation » toléré, l.258-264).

  • get_activation_status (l.535-558) valide aussi strictement. Un jeton expiré s'affiche donc « non activé », et le bouton « Activer » réapparaît. Le serveur renouvelle le jeton d'une machine déjà activée (branche existing de licenseService.ts) sans consommer de place.

  • LicenseInfo.machine_limit (l.44) est un claim signé, disponible localement.

  • Clés i18n inutilisées : license.machines.count (« {{count}}/{{limit}} machines activées »), .notActivated, .activating et .deactivating sont à brancher ; license.removeLicense et license.machines.activateError sont à retirer.

  • Dates : toLocaleDateString() est appelé sans locale (LicenseCard.tsx:109 et l.283), donc dans la langue du système. Convention du dépôt : i18n.language === "fr" ? "fr-CA" : "en-CA", écrit en ligne (BalanceOverviewCard.tsx:65, FilePreviewTable.tsx:36).

  • Les clés vont passer à 100 ans (maximus/maximus-api#63) : « Expire le 24/09/2126 » n'aurait pas de sens.

Tâches

  1. Étendre LicenseError (posé par #270) aux erreurs locales, avec une variante par sens :

    • InvalidPrefix (strip_prefix) ;
    • InvalidSignature (signée par une autre clé) ;
    • Malformed (JWT illisible, claims manquants) ;
    • LicenseExpired, réutilisée pour ErrorKind::ExpiredSignature ;
    • UnknownEdition ;
    • Storage, réutilisée pour l'écriture du fichier.

    validate_with_key renvoie Result<LicenseInfo, LicenseError>, à sémantique inchangée : mêmes clés acceptées, mêmes refus. validate_license_key et store_license renvoient le JSON via license_error_to_string. Côté TS : codes ajoutés à LicenseErrorCode et à LICENSE_ERROR_I18N_MAP, et clés FR/EN license.errors.invalidPrefix, invalidSignature, malformed, unknownEdition, au vouvoiement.

  2. LicenseCard.tsx : validationError et l'erreur de chargement (licenseStatus === "error") passent par parseLicenseError, puis par t(...). Le contexte garde la chaîne brute.

  3. Jeton d'activation expiré toléré (D10), dans resolve_license_entitlements :

    • un jeton signé, dont le machine_id correspond à cette machine mais dont exp est dépassé, est traité comme absent : l'édition de la clé s'applique ;
    • un jeton d'une autre machine, mal signé ou illisible, fait toujours retomber en Gratuite (CWE-863 inchangé).

    Utiliser une validation dédiée au jeton d'activation, pour ce seul usage (exp exigé mais non appliqué). strict_validation() et validate_with_key restent stricts pour la clé. get_activation_status reste strict aussi : la carte affiche « Non activée » et le bouton « Activer », car les cours du Bilan exigent un jeton valide côté serveur. Consigner la règle en une ligne dans l'amendement de l'ADR 0017 posé par #346.

  4. Clé révoquée : quand activate ou verify renvoie license_revoked, la carte affiche un état dédié « Cette clé a été révoquée » (clé license.revoked). Cet état porté par useLicenseMachines (#270) ne dure que la session : rien n'est persisté, et l'édition suit toujours la validation locale (ADR 0017).

  5. Machines : une fois la liste chargée, afficher license.machines.count avec {count: machines.length, limit: info.machine_limit}.

  6. Clés orphelines (D5) :

    • license.machines.activating et deactivating servent de libellés aux boutons pendant le chargement ;
    • license.machines.notActivated s'affiche quand la machine n'est pas activée ;
    • license.removeLicense et license.machines.activateError sont retirées des deux langues ;
    • license.errors.unknown reste le seul repli générique.
  7. Échéance : si expires_at est à plus de 50 ans, afficher « Sans échéance » / « No expiry date » (clé license.noExpiry) au lieu de la date. Sinon, afficher la date dans la langue de l'app. La règle vit dans une fonction pure de src/shared/licenseExpiry.ts, testée.

  8. Dates (formatExpiry, date d'activation d'une machine) : toLocaleDateString(i18n.language === "fr" ? "fr-CA" : "en-CA").

  9. Tests :

    • Rust, chaque variante locale : préfixe, signature d'une autre clé, JWT corrompu, clé expirée, édition inconnue.
    • Rust, sur resolve_license_entitlements : expired_license_resolves_free_without_features ; jeton d'activation expiré de cette machine → édition et features conservées ; jeton expiré d'une autre machine → Gratuite sans features ; jeton mal signé → Gratuite.
    • Le test existant rejects_expired_license reste vert.
    • vitest : nouveaux codes → clés ; toutes les clés license.errors.* présentes en FR et en EN ; seuil « Sans échéance » ; transition « révoquée » du reducer de useLicenseMachines.
  10. Documentation :

    • CHANGELOG.md et CHANGELOG.fr.md (Fixed / Corrigé) : jeton d'activation expiré, erreurs de saisie, dates ;
    • docs/guide-utilisateur.md §12 : clé révoquée, compteur de machines, « Sans échéance », réactivation pour les cours ;
    • docs/architecture.md, §license_commands.rs : règle du jeton expiré.

Fichiers concernés

  • src-tauri/src/commands/license_commands.rs
  • src/services/licenseService.ts
  • src/services/licenseService.test.ts
  • src/hooks/useLicenseMachines.ts
  • src/hooks/useLicenseMachines.test.ts
  • src/shared/licenseExpiry.ts (nouveau)
  • src/shared/licenseExpiry.test.ts (nouveau)
  • src/components/settings/LicenseCard.tsx
  • src/i18n/locales/fr.json
  • src/i18n/locales/en.json
  • docs/adr/0017-feature-gating-par-tier.md
  • docs/guide-utilisateur.md
  • docs/architecture.md
  • CHANGELOG.md
  • CHANGELOG.fr.md

Depends on

Critères d'acceptation

  • Aucune chaîne anglaise en dur n'atteint l'interface française de la carte Licence : erreurs de saisie comme refus serveur passent par license.errors.*.
  • Une machine dont le jeton d'activation a expiré garde l'édition de sa clé (test Rust), affiche « Non activée » et peut se réactiver. Un jeton d'une autre machine fait toujours retomber en Gratuite.
  • Un license_revoked reçu du serveur affiche l'état révoqué (FR/EN).
  • La liste des machines affiche « {n}/{limite} machines activées ».
  • Une clé dont l'échéance est à plus de 50 ans affiche « Sans échéance ». Les autres dates suivent la langue de l'app (fr-CA / en-CA).
  • license.removeLicense et license.machines.activateError n'existent plus ; activating, deactivating, notActivated et count sont utilisées.
  • cargo test, npm test et npm run build verts ; CHANGELOG FR/EN ; guide §12 à jour.

Review caveats

  • 🔴 Revue du 2026-09-24. Le repli générique est unique : license.errors.unknown. activateError affichait « Activation failed » en anglais, la chaîne même que #270 interdit, et sur des échecs qui ne sont pas des activations.
  • 🟡 Le jeton d'activation expiré n'est toléré que s'il est signé pour cette machine. Ne pas assouplir la vérification de la signature ni celle du machine_id (CWE-863). Ne jamais réutiliser la validation sans exp pour la clé de licence (CWE-613).
  • 🟡 L'expiration se prouve au point de décision : tester resolve_license_entitlements, pas seulement validate_with_key.
  • 🟢 Numéros de ligne relevés sur main le 2026-09-24 ; #270 et #346 les déplacent.

Décisions prises en planification

  • D5, révisée en revue — Quatre clés orphelines branchées (count, activating, deactivating, notActivated), deux retirées (removeLicense, activateError). Pas de commande de suppression de licence.
  • D9 — Clés de 100 ans (maximus/maximus-api#63 et maximus/la-compagnie-maximus#157). L'état « clé expirée le {date} » est abandonné. Une clé déjà expirée qu'on saisit reste nommée (license_expired), et « Sans échéance » s'affiche au-delà de 50 ans.
  • D10 — Un jeton d'activation expiré mais signé pour cette machine compte comme absent pour le gating local.
  • L'état révoqué ne dure que la session ; il n'est pas persisté.
  • La dépendance à #346 sert à l'ordre seulement : les deux issues partagent fr.json, en.json, les deux CHANGELOG, le guide §12 et l'amendement de l'ADR 0017.

Hors périmètre

  • Avertissement avant l'échéance : les clés durent 10 ans, bientôt 100.
  • Alignement côté serveur de la durée du jeton d'activation.
  • Hachage de l'identifiant machine → #350.

Spec source

spec-plan-paiements.md (séance /plan-run du 2026-09-24). C'est un fichier de travail ignoré par git : ce corps se suffit à lui-même.

Corps du 2026-09-24 (séance `/plan-run`, revu en `/review-spec`), archivé avant la réécriture par l'analyse du 2026-09-29. Deux décisions nouvelles le modifient : **D12** (le jeton d'activation ne fait plus jamais retomber en Gratuite, ce qui ferme #360 et englobe D10) et **D13** (clé révoquée : drapeau de session, message existant de #270). Le reste du plan tient, vérifié contre `main` `5d549d6` et `maximus-api` `main` `3673d09`. <details><summary>Titre et corps d'origine</summary> **Titre** : fix: carte Licence — erreurs de saisie nommées, clé révoquée explicite, jeton d'activation expiré toléré, machines n/limite, dates Après #270, les refus du serveur sont nommés. Il reste quatre problèmes dans la carte Licence : - les erreurs de saisie de clé s'affichent en anglais brut ; - une clé révoquée n'a pas d'état dédié ; - la limite de machines n'est jamais affichée ; - les dates suivent la langue du système, pas celle de l'app. La revue a aussi trouvé un défaut de fond : une machine qui a cliqué « Activer » retombe en Gratuite, sans message, au bout de 2 ans, parce que le jeton d'activation expire avant la clé. C'est l'écran qu'un acheteur voit quand sa clé pose problème. ## Contexte vérifié (2026-09-24) Numéros de ligne relevés sur `main`. #270 et #346 les déplacent : se repérer aux noms de fonctions. - `LicenseCard.tsx` affiche telles quelles des erreurs locales en anglais (`validationError` l.182-187, erreur de chargement l.143-148) : - `strip_prefix` (`license_commands.rs:94`) : « License key must start with SR-BASE- or SR-PREMIUM- » ; - `validate_with_key` (l.122) : « Invalid license: {erreur jsonwebtoken} », par exemple `ExpiredSignature` ou `InvalidSignature` ; - l.126 : « Unknown edition » ; - `store_license` (l.170-179) : erreurs d'I/O. `LicenseContext.submitKey` (l.138-147) garde la chaîne brute dans `validationError`. - Les appelants de `validate_with_key` ignorent tous le détail de l'erreur : `validate_license_key` et `store_license` la renvoient, `read_license` fait `.ok()`, `resolve_license_entitlements` fait `let Ok(..) else`. Son type d'erreur peut donc devenir `LicenseError` sans rien changer à la résolution d'édition. - **Jeton d'activation** : - maximus-api le signe pour 2 ans (`maximus-api/src/crypto/ed25519.ts:256`, `signActivationJwt`, `2 * 365 * 24 * 3600`), alors que la clé dure 3650 jours ; - le client le valide avec `strict_validation()` (`license_commands.rs:141-155`), qui applique `exp` avec `leeway = 0` ; - `resolve_license_entitlements` (l.249-266) retombe en Gratuite dès qu'un jeton existe et ne valide pas. Résultat : une machine activée perd son édition au bout de 2 ans, alors qu'une machine jamais activée la garde (état « pré-activation » toléré, l.258-264). - `get_activation_status` (l.535-558) valide aussi strictement. Un jeton expiré s'affiche donc « non activé », et le bouton « Activer » réapparaît. Le serveur renouvelle le jeton d'une machine déjà activée (branche `existing` de `licenseService.ts`) sans consommer de place. - `LicenseInfo.machine_limit` (l.44) est un claim signé, disponible localement. - Clés i18n inutilisées : `license.machines.count` (« {{count}}/{{limit}} machines activées »), `.notActivated`, `.activating` et `.deactivating` sont à brancher ; `license.removeLicense` et `license.machines.activateError` sont à retirer. - Dates : `toLocaleDateString()` est appelé sans locale (`LicenseCard.tsx:109` et l.283), donc dans la langue du système. Convention du dépôt : `i18n.language === "fr" ? "fr-CA" : "en-CA"`, écrit en ligne (`BalanceOverviewCard.tsx:65`, `FilePreviewTable.tsx:36`). - Les clés vont passer à 100 ans (maximus/maximus-api#63) : « Expire le 24/09/2126 » n'aurait pas de sens. ## Tâches 1. Étendre `LicenseError` (posé par #270) aux erreurs locales, avec une variante par sens : - `InvalidPrefix` (`strip_prefix`) ; - `InvalidSignature` (signée par une autre clé) ; - `Malformed` (JWT illisible, claims manquants) ; - `LicenseExpired`, réutilisée pour `ErrorKind::ExpiredSignature` ; - `UnknownEdition` ; - `Storage`, réutilisée pour l'écriture du fichier. `validate_with_key` renvoie `Result<LicenseInfo, LicenseError>`, à sémantique inchangée : mêmes clés acceptées, mêmes refus. `validate_license_key` et `store_license` renvoient le JSON via `license_error_to_string`. Côté TS : codes ajoutés à `LicenseErrorCode` et à `LICENSE_ERROR_I18N_MAP`, et clés FR/EN `license.errors.invalidPrefix`, `invalidSignature`, `malformed`, `unknownEdition`, au vouvoiement. 2. `LicenseCard.tsx` : `validationError` et l'erreur de chargement (`licenseStatus === "error"`) passent par `parseLicenseError`, puis par `t(...)`. Le contexte garde la chaîne brute. 3. Jeton d'activation expiré toléré (D10), dans `resolve_license_entitlements` : - un jeton **signé**, dont le `machine_id` correspond à cette machine mais dont `exp` est dépassé, est traité comme absent : l'édition de la clé s'applique ; - un jeton d'une autre machine, mal signé ou illisible, fait toujours retomber en Gratuite (CWE-863 inchangé). Utiliser une validation dédiée au jeton d'activation, pour ce seul usage (`exp` exigé mais non appliqué). `strict_validation()` et `validate_with_key` restent stricts pour la clé. `get_activation_status` reste strict aussi : la carte affiche « Non activée » et le bouton « Activer », car les cours du Bilan exigent un jeton valide côté serveur. Consigner la règle en une ligne dans l'amendement de l'ADR 0017 posé par #346. 4. Clé révoquée : quand activate ou verify renvoie `license_revoked`, la carte affiche un état dédié « Cette clé a été révoquée » (clé `license.revoked`). Cet état porté par `useLicenseMachines` (#270) ne dure que la session : rien n'est persisté, et l'édition suit toujours la validation locale (ADR 0017). 5. Machines : une fois la liste chargée, afficher `license.machines.count` avec `{count: machines.length, limit: info.machine_limit}`. 6. Clés orphelines (D5) : - `license.machines.activating` et `deactivating` servent de libellés aux boutons pendant le chargement ; - `license.machines.notActivated` s'affiche quand la machine n'est pas activée ; - `license.removeLicense` et `license.machines.activateError` sont retirées des deux langues ; - `license.errors.unknown` reste le seul repli générique. 7. Échéance : si `expires_at` est à plus de 50 ans, afficher « Sans échéance » / « No expiry date » (clé `license.noExpiry`) au lieu de la date. Sinon, afficher la date dans la langue de l'app. La règle vit dans une fonction pure de `src/shared/licenseExpiry.ts`, testée. 8. Dates (`formatExpiry`, date d'activation d'une machine) : `toLocaleDateString(i18n.language === "fr" ? "fr-CA" : "en-CA")`. 9. Tests : - Rust, chaque variante locale : préfixe, signature d'une autre clé, JWT corrompu, clé expirée, édition inconnue. - Rust, sur `resolve_license_entitlements` : `expired_license_resolves_free_without_features` ; jeton d'activation expiré de cette machine → édition et features conservées ; jeton expiré d'une autre machine → Gratuite sans features ; jeton mal signé → Gratuite. - Le test existant `rejects_expired_license` reste vert. - vitest : nouveaux codes → clés ; toutes les clés `license.errors.*` présentes en FR et en EN ; seuil « Sans échéance » ; transition « révoquée » du reducer de `useLicenseMachines`. 10. Documentation : - `CHANGELOG.md` et `CHANGELOG.fr.md` (Fixed / Corrigé) : jeton d'activation expiré, erreurs de saisie, dates ; - `docs/guide-utilisateur.md` §12 : clé révoquée, compteur de machines, « Sans échéance », réactivation pour les cours ; - `docs/architecture.md`, §`license_commands.rs` : règle du jeton expiré. ## Fichiers concernés - `src-tauri/src/commands/license_commands.rs` - `src/services/licenseService.ts` - `src/services/licenseService.test.ts` - `src/hooks/useLicenseMachines.ts` - `src/hooks/useLicenseMachines.test.ts` - `src/shared/licenseExpiry.ts` (nouveau) - `src/shared/licenseExpiry.test.ts` (nouveau) - `src/components/settings/LicenseCard.tsx` - `src/i18n/locales/fr.json` - `src/i18n/locales/en.json` - `docs/adr/0017-feature-gating-par-tier.md` - `docs/guide-utilisateur.md` - `docs/architecture.md` - `CHANGELOG.md` - `CHANGELOG.fr.md` ## Depends on - #270 - #346 ## Critères d'acceptation - [ ] Aucune chaîne anglaise en dur n'atteint l'interface française de la carte Licence : erreurs de saisie comme refus serveur passent par `license.errors.*`. - [ ] Une machine dont le jeton d'activation a expiré garde l'édition de sa clé (test Rust), affiche « Non activée » et peut se réactiver. Un jeton d'une autre machine fait toujours retomber en Gratuite. - [ ] Un `license_revoked` reçu du serveur affiche l'état révoqué (FR/EN). - [ ] La liste des machines affiche « {n}/{limite} machines activées ». - [ ] Une clé dont l'échéance est à plus de 50 ans affiche « Sans échéance ». Les autres dates suivent la langue de l'app (`fr-CA` / `en-CA`). - [ ] `license.removeLicense` et `license.machines.activateError` n'existent plus ; `activating`, `deactivating`, `notActivated` et `count` sont utilisées. - [ ] `cargo test`, `npm test` et `npm run build` verts ; CHANGELOG FR/EN ; guide §12 à jour. ## Review caveats - 🔴 Revue du 2026-09-24. Le repli générique est unique : `license.errors.unknown`. `activateError` affichait « Activation failed » en anglais, la chaîne même que #270 interdit, et sur des échecs qui ne sont pas des activations. - 🟡 Le jeton d'activation expiré n'est toléré **que** s'il est signé pour cette machine. Ne pas assouplir la vérification de la signature ni celle du `machine_id` (CWE-863). Ne jamais réutiliser la validation sans `exp` pour la clé de licence (CWE-613). - 🟡 L'expiration se prouve au point de décision : tester `resolve_license_entitlements`, pas seulement `validate_with_key`. - 🟢 Numéros de ligne relevés sur `main` le 2026-09-24 ; #270 et #346 les déplacent. ## Décisions prises en planification - D5, révisée en revue — Quatre clés orphelines branchées (`count`, `activating`, `deactivating`, `notActivated`), deux retirées (`removeLicense`, `activateError`). Pas de commande de suppression de licence. - D9 — Clés de 100 ans (maximus/maximus-api#63 et maximus/la-compagnie-maximus#157). L'état « clé expirée le {date} » est abandonné. Une clé déjà expirée qu'on saisit reste nommée (`license_expired`), et « Sans échéance » s'affiche au-delà de 50 ans. - D10 — Un jeton d'activation expiré mais signé pour cette machine compte comme absent pour le gating local. - L'état révoqué ne dure que la session ; il n'est pas persisté. - La dépendance à #346 sert à l'ordre seulement : les deux issues partagent `fr.json`, `en.json`, les deux CHANGELOG, le guide §12 et l'amendement de l'ADR 0017. ## Hors périmètre - Avertissement avant l'échéance : les clés durent 10 ans, bientôt 100. - Alignement côté serveur de la durée du jeton d'activation. - Hachage de l'identifiant machine → #350. ## Spec source `spec-plan-paiements.md` (séance `/plan-run` du 2026-09-24). C'est un fichier de travail ignoré par git : ce corps se suffit à lui-même. </details>
maximus changed title from fix: carte Licence — erreurs de saisie nommées, clé révoquée explicite, jeton d'activation expiré toléré, machines n/limite, dates to fix: carte Licence — erreurs nommées, clé révoquée affichée, édition indépendante du jeton d'activation, machines n/limite, dates 2026-09-30 00:54:00 +00:00
maximus added
status:in-progress
and removed
status:ready
labels 2026-09-30 01:23:36 +00:00
maximus added
status:review
and removed
status:in-progress
labels 2026-09-30 01:47:52 +00:00
maximus added
status:approved
and removed
status:review
labels 2026-09-30 23:48:22 +00:00
maximus added
status:review
and removed
status:approved
labels 2026-10-01 00:01:07 +00:00
maximus added
status:approved
and removed
status:review
labels 2026-10-01 00:35:42 +00:00
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

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