fix(license): named key-entry errors, edition independent of the activation token, revoked key flagged (#347) #364

Merged
maximus merged 3 commits from issue-347-license-card into main 2026-10-01 00:57:33 +00:00
Owner

Fixes #347
Fixes #360
Refs #363

Ce que la PR change

Rust (license_commands.rs)

  • LicenseError gagne InvalidPrefix, InvalidSignature, Malformed et UnknownEdition. validate_with_key le renvoie (ExpiredSignature devient LicenseExpired), et store_license, read_license et validate_license_key rejettent avec le même JSON que les commandes réseau. store_license_in et read_license_in prennent un dossier, ce qui teste la correspondance Storage à la source.
  • D12 : l'édition vient de la seule clé. resolve_license_entitlements ne prend plus le jeton d'activation, et current_entitlements ne lit plus que license.key (entitlements_from_dir). get_activation_status et persist_activation_token gardent la validation stricte du jeton ; la clé garde la sienne (exp et iat exigés, CWE-613).

Front

  • LicenseCard nomme l'erreur de chargement, et LicenseKeyForm (extrait) nomme une clé refusée, via parseLicenseError : aucune chaîne brute n'atteint la carte.
  • D13 : un license_revoked est consigné dans LicenseContext pour toute la session ; message au niveau de la carte, icône d'alerte, « Activer » désactivé via selectCanActivate.
  • Compteur « n/limite », « Non activée », libellés d'activation et de désactivation en cours, dates en fr-CA / en-CA, « Sans échéance » au-delà de 50 ans (licenseExpiry.ts).
  • license.removeLicense et license.machines.activateError retirées ; le message authFailed des cours renvoie vers l'activation.

Docs : CHANGELOG FR/EN, guide §12 (nouvelle section « La carte Licence ») et docs intégrées, architecture.md, amendement #347 de l'ADR 0017, qa-update-cycle.md, api-contract-prices.md.

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

  • Le drapeau « révoquée » vit dans LicenseContext, pas dans LicenseMachinesState. Le reducer du hook meurt avec la carte : quitter Paramètres → Utilisateurs ou changer de profil l'aurait effacé, et « Activer » serait redevenu cliquable. LicenseContext est monté une fois dans main.tsx, au-dessus de ProfileProvider. Relevé par le plan checker.
  • Le drapeau s'efface quand une autre clé est enregistrée ; la même clé ressaisie le garde. L'issue disait « jamais remis à zéro pendant la session », mais une clé de remplacement bloquée jusqu'au redémarrage serait un piège.
  • Le compteur n'apparaît pas pour une liste vide, qui a déjà sa ligne « Aucune machine activée », ni sans clé stockée (Premium par compte : aucune limite signée).
  • LicenseKeyForm est extrait : le formulaire était derrière un état interne de la carte, donc hors de portée d'un rendu statique. Sans l'extraction, remettre la chaîne brute dans le formulaire passait la suite.
  • refresh() reste appelé après une activation ou une désactivation, commentaire corrigé : ni l'une ni l'autre ne change plus l'édition.
  • qa-update-cycle.md est nuancé, pas vidé : le cycle de test part d'une version publiée, et v0.15.0 applique encore l'ancienne règle.

Preuves

Rouge avant le correctif, sur 5d549d6 :

  • Rust : un jeton expiré de cette machine fait tomber une clé Base valide (left: "free", right: "base").
  • vitest : la carte affiche l'erreur de chargement brute (2 rouges).

Suites sur d2ded51 : cargo test 128 → 140 ; vitest 1266 → 1305 (68 fichiers) ; npm run build (tsc + vite) vert ; cargo check sans avertissement.

Mutations rejouées sur l'arbre commité, 27 sur 27 rouges (chaque fichier restauré depuis HEAD, arbre propre vérifié) :

Mutation Rouges
un activation.token présent refait tomber en Gratuite (D12 annulé) 3 / 140
ExpiredSignature plus nommée 1 / 140
exp de la clé plus appliqué (CWE-613) 4 / 140
expires_at tronqué à 32 bits (#363) 1 / 140
store_license_in écrit avant de valider 1 / 140
fichier de clé illisible lu comme absent 1 / 140
exp du jeton plus appliqué 1 / 140
préfixe invalide plus nommé 2 / 140
erreur de chargement affichée brute 2 / 1305
erreur de saisie affichée brute 2 / 1305
même clé ressaisie : révocation effacée 1 / 1305
autre clé : révocation gardée 1 / 1305
LICENSE_REVOKED ignoré 4 / 1305
le hook ignore revoked du contexte 1 / 1305
le hook ne transmet pas markRevoked 1 / 1305
révocation de la liste non consignée 1 / 1305
selectCanActivate ignore revoked 3 / 1305
garde info du compteur retirée 1 / 1305
locale retirée de la date d'activation 2 / 1305
locale retirée de la date d'échéance 3 / 1305
horizon de 50 ans retiré 3 / 1305
bandeau « révoquée » retiré 2 / 1305
révocation répétée sous Machines 1 / 1305
libellé « Non activée » retiré 1 / 1305
libellé « Activation... » retiré 1 / 1305
noExpiry absent de en.json 1 / 1305
removeLicense remis dans en.json 1 / 1305

À vérifier à la main

  • En français, entrer abc comme clé : le message parle du préfixe SR-BASE- / SR-PREMIUM-.
  • Remplacer activation.token par celui d'une autre machine : l'édition reste, la carte affiche « Non activée » et « Activer ».

Hors périmètre

  • Côté serveur, /v1/prices ne vérifie pas que la machine est encore active (maximus-api, issue à ouvrir).
  • Reste de #363 : plafond de /issue-license après le déploiement de maximus-api#63, et essai d'une vraie clé de 100 ans.

🤖 Generated with Claude Code

Fixes #347 Fixes #360 Refs #363 ## Ce que la PR change **Rust (`license_commands.rs`)** - `LicenseError` gagne `InvalidPrefix`, `InvalidSignature`, `Malformed` et `UnknownEdition`. `validate_with_key` le renvoie (`ExpiredSignature` devient `LicenseExpired`), et `store_license`, `read_license` et `validate_license_key` rejettent avec le même JSON que les commandes réseau. `store_license_in` et `read_license_in` prennent un dossier, ce qui teste la correspondance `Storage` à la source. - **D12** : l'édition vient de la seule clé. `resolve_license_entitlements` ne prend plus le jeton d'activation, et `current_entitlements` ne lit plus que `license.key` (`entitlements_from_dir`). `get_activation_status` et `persist_activation_token` gardent la validation stricte du jeton ; la clé garde la sienne (`exp` et `iat` exigés, CWE-613). **Front** - `LicenseCard` nomme l'erreur de chargement, et `LicenseKeyForm` (extrait) nomme une clé refusée, via `parseLicenseError` : aucune chaîne brute n'atteint la carte. - **D13** : un `license_revoked` est consigné dans `LicenseContext` pour toute la session ; message au niveau de la carte, icône d'alerte, « Activer » désactivé via `selectCanActivate`. - Compteur « n/limite », « Non activée », libellés d'activation et de désactivation en cours, dates en `fr-CA` / `en-CA`, « Sans échéance » au-delà de 50 ans (`licenseExpiry.ts`). - `license.removeLicense` et `license.machines.activateError` retirées ; le message `authFailed` des cours renvoie vers l'activation. **Docs** : CHANGELOG FR/EN, guide §12 (nouvelle section « La carte Licence ») et docs intégrées, `architecture.md`, amendement #347 de l'ADR 0017, `qa-update-cycle.md`, `api-contract-prices.md`. ## Écarts assumés par rapport au corps de l'issue - **Le drapeau « révoquée » vit dans `LicenseContext`, pas dans `LicenseMachinesState`.** Le reducer du hook meurt avec la carte : quitter Paramètres → Utilisateurs ou changer de profil l'aurait effacé, et « Activer » serait redevenu cliquable. `LicenseContext` est monté une fois dans `main.tsx`, au-dessus de `ProfileProvider`. Relevé par le plan checker. - **Le drapeau s'efface quand une autre clé est enregistrée** ; la même clé ressaisie le garde. L'issue disait « jamais remis à zéro pendant la session », mais une clé de remplacement bloquée jusqu'au redémarrage serait un piège. - **Le compteur n'apparaît pas pour une liste vide**, qui a déjà sa ligne « Aucune machine activée », ni sans clé stockée (Premium par compte : aucune limite signée). - **`LicenseKeyForm` est extrait** : le formulaire était derrière un état interne de la carte, donc hors de portée d'un rendu statique. Sans l'extraction, remettre la chaîne brute dans le formulaire passait la suite. - **`refresh()` reste appelé** après une activation ou une désactivation, commentaire corrigé : ni l'une ni l'autre ne change plus l'édition. - **`qa-update-cycle.md` est nuancé, pas vidé** : le cycle de test part d'une version publiée, et v0.15.0 applique encore l'ancienne règle. ## Preuves **Rouge avant le correctif**, sur `5d549d6` : - Rust : un jeton expiré de cette machine fait tomber une clé Base valide (`left: "free"`, `right: "base"`). - vitest : la carte affiche l'erreur de chargement brute (2 rouges). **Suites sur `d2ded51`** : `cargo test` 128 → **140** ; vitest 1266 → **1305** (68 fichiers) ; `npm run build` (tsc + vite) vert ; `cargo check` sans avertissement. **Mutations rejouées sur l'arbre commité, 27 sur 27 rouges** (chaque fichier restauré depuis `HEAD`, arbre propre vérifié) : | Mutation | Rouges | |---|---| | un `activation.token` présent refait tomber en Gratuite (D12 annulé) | 3 / 140 | | `ExpiredSignature` plus nommée | 1 / 140 | | `exp` de la clé plus appliqué (CWE-613) | 4 / 140 | | `expires_at` tronqué à 32 bits (#363) | 1 / 140 | | `store_license_in` écrit avant de valider | 1 / 140 | | fichier de clé illisible lu comme absent | 1 / 140 | | `exp` du jeton plus appliqué | 1 / 140 | | préfixe invalide plus nommé | 2 / 140 | | erreur de chargement affichée brute | 2 / 1305 | | erreur de saisie affichée brute | 2 / 1305 | | même clé ressaisie : révocation effacée | 1 / 1305 | | autre clé : révocation gardée | 1 / 1305 | | `LICENSE_REVOKED` ignoré | 4 / 1305 | | le hook ignore `revoked` du contexte | 1 / 1305 | | le hook ne transmet pas `markRevoked` | 1 / 1305 | | révocation de la liste non consignée | 1 / 1305 | | `selectCanActivate` ignore `revoked` | 3 / 1305 | | garde `info` du compteur retirée | 1 / 1305 | | locale retirée de la date d'activation | 2 / 1305 | | locale retirée de la date d'échéance | 3 / 1305 | | horizon de 50 ans retiré | 3 / 1305 | | bandeau « révoquée » retiré | 2 / 1305 | | révocation répétée sous Machines | 1 / 1305 | | libellé « Non activée » retiré | 1 / 1305 | | libellé « Activation... » retiré | 1 / 1305 | | `noExpiry` absent de `en.json` | 1 / 1305 | | `removeLicense` remis dans `en.json` | 1 / 1305 | ## À vérifier à la main - En français, entrer `abc` comme clé : le message parle du préfixe `SR-BASE-` / `SR-PREMIUM-`. - Remplacer `activation.token` par celui d'une autre machine : l'édition reste, la carte affiche « Non activée » et « Activer ». ## Hors périmètre - Côté serveur, `/v1/prices` ne vérifie pas que la machine est encore active (maximus-api, issue à ouvrir). - Reste de #363 : plafond de `/issue-license` après le déploiement de maximus-api#63, et essai d'une vraie clé de 100 ans. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 1 commit 2026-09-30 01:47:41 +00:00
fix(license): name key-entry errors, keep the edition off the activation token, flag revoked keys
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m43s
PR Check — Rust / rust (pull_request) Successful in 9m11s
d2ded515fd
Rust (license_commands.rs)
- LicenseError gains InvalidPrefix, InvalidSignature, Malformed and
  UnknownEdition. validate_with_key returns it (ExpiredSignature maps
  to LicenseExpired), and store_license, read_license and
  validate_license_key reject with the same JSON as the network
  commands. store_license_in and read_license_in take a directory, so
  the Storage mapping is tested at the source.
- D12: the edition comes from the license key alone.
  resolve_license_entitlements no longer takes the activation token,
  and current_entitlements reads only license.key
  (entitlements_from_dir). A token that had expired (2-year server
  lifetime) or came from another machine (restored data) used to drop
  a valid key to free, while a key copied without its token kept its
  edition. get_activation_status and persist_activation_token keep the
  strict token validation, and the key keeps its strict validation
  (CWE-613).

Front
- LicenseCard names the load error, and LicenseKeyForm (extracted so a
  static render reaches it) names a refused key, through
  parseLicenseError: no raw backend string reaches the card.
- D13: a license_revoked answer is recorded in LicenseContext for the
  whole session, because the card unmounts when the user leaves
  Settings > Users. Only a different key clears it. Card-level
  message, alert icon, Activate disabled through selectCanActivate.
- Machine counter n/limit, "Not activated", activating and
  deactivating labels, dates in fr-CA/en-CA, and "No expiry date"
  beyond 50 years (licenseExpiry.ts, for the 100-year keys of
  maximus-api#63).
- license.removeLicense and license.machines.activateError removed;
  the price authFailed message points to activating the machine.

Tests: 15 new Rust tests (resolution from a data directory, named
variants, the 100-year key of #363, a key without exp) replace 3
machine-binding tests; vitest covers the card and form renders, the
context revocation, the hook wiring and the locales.
test: reproduces #347 (an expired token of this machine resolved a
valid Base key to free on 5d549d6).

Docs: CHANGELOG FR/EN, guide section 12 and in-app docs,
architecture.md, ADR 0017 amendment for #347, qa-update-cycle.md,
api-contract-prices.md.

Fixes #347
Fixes #360
Refs #363

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

CI verte au head d2ded51, vérifiée dans les logs :

  • run 433 (check-frontend) : HEAD is now at d2ded51, build (tsc + vite) OK, 68 fichiers, 1305 tests vitest.
  • run 434 (check-rust) : HEAD is now at d2ded51, cargo check OK, garde-fou exécuté (Suppression guard: 2 checks, exit 0), cargo test 140 passés, cargo audit sans vulnérabilité (12 avertissements autorisés, comme sur main).
CI verte au head `d2ded51`, vérifiée dans les logs : - **run 433 (`check-frontend`)** : `HEAD is now at d2ded51`, build (tsc + vite) OK, 68 fichiers, **1305** tests vitest. - **run 434 (`check-rust`)** : `HEAD is now at d2ded51`, `cargo check` OK, garde-fou exécuté (`Suppression guard: 2 checks, exit 0`), `cargo test` **140** passés, `cargo audit` sans vulnérabilité (12 avertissements autorisés, comme sur `main`).
Author
Owner

Revue adversariale — PR #364 (#347, #360)

Verdict : APPROVE

Résumé — D12, les erreurs nommées et la révocation de session tiennent, y compris sous des mutations du relecteur : D12 annulé fait rougir 2 tests Rust, et LOAD_DONE ou LOAD_ERROR qui effacent la révocation en font rougir 1 chacun. Trois chemins restent sans garde (0 rouge chacun), et le message invalidSignature déclare « non émise par le serveur » une clé authentique mal copiée dans environ une troncature sur trois (3 longueurs sur 8 mesurées). Rien de bloquant.

Vérifié

  • CI de la tête d2ded51, lue dans les logs et pas sur la pastille : run 434 (rust) 140 passed, garde-fou Suppression guard: 2 checks, exit 0 ; run 433 (frontend) tsc && vite build vert, 68 fichiers / 1305 tests.
  • Rejoué dans une copie hors dépôt (git archive d2ded51, target Cargo séparé dans le scratchpad) : cargo test --lib 140, sans avertissement ; vitest 1305. Le dépôt n'a pas été touché.
  • Timeline : #347 et #360 portent un pull_ref closes venant de #364 ; #363 porte none. Le Refs #363 est respecté.
  • Lignes ajoutées : aucun secret, aucune donnée personnelle (user@example.com seulement).
  • Côté serveur, licenseAuth.ts (maximus-api) répond 401 à un jeton absent, expiré ou invalide : le nouveau authFailed (« activez cette machine ») vise le bon cas.
  • Critères de #347 recoupés contre le code : édition résolue depuis la seule clé (entitlements_from_dir) ; clé toujours validée strictement (exp et iat exigés, leeway 0, nouveau rejects_license_without_exp) ; get_activation_status et persist_activation_token gardent la validation stricte du jeton ; compteur, « Non activée », libellés en cours, dates fr-CA / en-CA, « Sans échéance », clés retirées ; docs à jour (CHANGELOG ×2, guide §12 et docs.editions.tips 8/8 dans les deux langues, architecture.md, amendement de l'ADR 0017).

Mutations du relecteur

Sur la copie ; chaque fichier restauré depuis sa sauvegarde et comparé octet pour octet.

Mutation Rouges
D12 annulé dans entitlements_from_dir : jeton relu et validé strictement contre machine_id_internal() 2 / 140
ErrorKind::InvalidSignature lu comme Malformed 1 / 140
LOAD_DONE efface revoked (tout refresh() l'effacerait, dont celui qui suit une désactivation réussie) 1 / 1305
LOAD_ERROR efface revoked 1 / 1305
bandeau « révoquée » affiché seulement liste ouverte 1 / 1305
get_activation_status lit tout jeton lisible comme activé 0 / 140
shownUnderMachines masque toutes les erreurs sous Machines 0 / 1305
coche verte gardée pour une clé révoquée 0 / 1305

Deux autres mutations restent vertes, sans conséquence pratique : isSameLicense sans expires_at, et le compteur affiché à côté d'une erreur de désactivation.

Suggestions (non bloquantes)

  1. src/components/settings/LicenseCard.tsx:53-57 — le filtre shownUnderMachines n'a aucun garde à l'affichage. Le remplacer par null fait disparaître toutes les erreurs sous Machines (limite de machines, trop de tentatives, réseau) et laisse les 1305 tests verts. Le parcours D2 de #270, où le message de limite reste affiché pendant le chargement de la liste, n'est gardé qu'au niveau du reducer, et c'est cette PR qui insère un filtre sur son chemin d'affichage. Deux rendus suffisent : activationError: machine_limit_reached → license.errors.machineLimitReached présent ; listOpen avec listError: network → license.errors.network présent.

  2. src-tauri/src/commands/license_commands.rs:686 (get_activation_status) — la décision du critère 3 n'est pas testée à son point. Depuis D12, cette fonction décide seule de « Non activée » et du bouton « Activer », c'est-à-dire de la sortie de l'impasse de #360. La faire répondre true pour tout jeton lisible laisse 140/140 verts : seuls validate_activation_with_key et ses deux refus sont testés, ce que l'issue demandait. Le découpage déjà appliqué à entitlements_from_dir s'y transpose : activation_status_in(dir, machine_id, key), testée dans un TestDir (jeton absent, expiré, d'une autre machine ou illisible → false ; jeton valide → true).

  3. license.errors.invalidSignature (FR/EN, ligne 1220) et guide §12 (ligne 561) — une clé authentique mal copiée est déclarée « non émise par le serveur de licences ». Mesuré sur une clé correctement signée, avec un test sonde temporaire dans la copie :

    Dégât Variante renvoyée
    1, 3, 5, 7 ou 8 derniers caractères manquants Malformed
    2, 4 ou 6 derniers caractères manquants InvalidSignature
    espace ou retour à la ligne dans la charge utile InvalidSignature
    espace dans la signature, segment de signature absent Malformed

    jsonwebtoken vérifie la signature sur en-tête.charge avant de décoder la charge : toute altération de la charge, et une partie des troncatures de la signature, finissent en InvalidSignature. Un retour à la ligne collé dans le champ y est retiré ou changé en espace selon le moteur ; dans le second cas, on retombe sur InvalidSignature. Le guide range « copiée en partie ou coupée par un retour à la ligne » sous « illisible », et le message malformed conseille « sans espace », alors que ces cas affichent souvent « Cette clé de licence n'a pas été émise par le serveur de licences de Simpl'Résultat. ». Sur l'écran qu'un acheteur voit quand sa clé pose problème, ce message suggère une clé contrefaite. Formulation proposée : « Cette clé de licence n'a pas pu être vérifiée. Vérifiez qu'elle a été copiée en entier ; si c'est le cas, elle n'a pas été émise par le serveur de licences de Simpl'Résultat. », avec la puce du guide alignée.

  4. src/components/settings/LicenseCard.tsx:79-84 — l'icône d'alerte de la tâche 4 n'est pas gardée. Garder la CheckCircle pour une clé révoquée laisse la suite verte. Une assertion suffit : clé révoquée et machine non activée → aucune classe lucide-circle-check-big dans le rendu. La machine doit être non activée, car le badge « Activée » porte la même icône.

Hors PR, trouvés en route

  • Préexistant, à ouvrir en issue : un jeton d'activation issu d'une clé précédente survit à l'enregistrement d'une autre clé et masque « Activer ». store_license_in n'écrit que license.key. Le jeton porte en sub l'UUID de l'ancienne licence, la clé porte le courriel : le client ne peut pas les rapprocher, et get_activation_status ne vérifie que la signature, exp et machine_id. La carte affiche donc « Activée », sans bouton « Activer », pendant que /v1/prices authentifie l'ancienne licence (licenseAuth.ts : licence révoquée → 403 license_revoked, licence Base → 403 premium_required). Cas concernés : rachat après un remboursement, réémission après une révocation, passage de Base à Premium. L'argument de la PR pour effacer revoked à l'enregistrement d'une autre clé (« une clé de remplacement bloquée jusqu'au redémarrage serait un piège ») n'atteint pas ce sous-cas : la clé de remplacement reste bloquée, par le jeton cette fois. Correctif simple : store_license_in supprime activation.token quand la clé enregistrée change, testé dans un TestDir.
  • Sans lien avec la PR : npm ci annonce 7 vulnérabilités (3 moderate, 4 high) dans les runs 430 (#362) et 433, alors que CLAUDE.md n'en décrit que 2 high acceptées (#317).
## Revue adversariale — PR #364 (#347, #360) **Verdict : APPROVE** **Résumé** — D12, les erreurs nommées et la révocation de session tiennent, y compris sous des mutations du relecteur : D12 annulé fait rougir 2 tests Rust, et `LOAD_DONE` ou `LOAD_ERROR` qui effacent la révocation en font rougir 1 chacun. Trois chemins restent sans garde (0 rouge chacun), et le message `invalidSignature` déclare « non émise par le serveur » une clé authentique mal copiée dans environ une troncature sur trois (3 longueurs sur 8 mesurées). Rien de bloquant. ### Vérifié - CI de la tête `d2ded51`, lue dans les logs et pas sur la pastille : run 434 (rust) `140 passed`, garde-fou `Suppression guard: 2 checks, exit 0` ; run 433 (frontend) `tsc && vite build` vert, 68 fichiers / 1305 tests. - Rejoué dans une copie hors dépôt (`git archive d2ded51`, target Cargo séparé dans le scratchpad) : `cargo test --lib` **140**, sans avertissement ; vitest **1305**. Le dépôt n'a pas été touché. - Timeline : #347 et #360 portent un `pull_ref` `closes` venant de #364 ; #363 porte `none`. Le `Refs #363` est respecté. - Lignes ajoutées : aucun secret, aucune donnée personnelle (`user@example.com` seulement). - Côté serveur, `licenseAuth.ts` (maximus-api) répond 401 à un jeton absent, expiré ou invalide : le nouveau `authFailed` (« activez cette machine ») vise le bon cas. - Critères de #347 recoupés contre le code : édition résolue depuis la seule clé (`entitlements_from_dir`) ; clé toujours validée strictement (`exp` et `iat` exigés, leeway 0, nouveau `rejects_license_without_exp`) ; `get_activation_status` et `persist_activation_token` gardent la validation stricte du jeton ; compteur, « Non activée », libellés en cours, dates `fr-CA` / `en-CA`, « Sans échéance », clés retirées ; docs à jour (CHANGELOG ×2, guide §12 et `docs.editions.tips` 8/8 dans les deux langues, `architecture.md`, amendement de l'ADR 0017). ### Mutations du relecteur Sur la copie ; chaque fichier restauré depuis sa sauvegarde et comparé octet pour octet. | Mutation | Rouges | |---|---| | D12 annulé dans `entitlements_from_dir` : jeton relu et validé strictement contre `machine_id_internal()` | 2 / 140 | | `ErrorKind::InvalidSignature` lu comme `Malformed` | 1 / 140 | | `LOAD_DONE` efface `revoked` (tout `refresh()` l'effacerait, dont celui qui suit une désactivation réussie) | 1 / 1305 | | `LOAD_ERROR` efface `revoked` | 1 / 1305 | | bandeau « révoquée » affiché seulement liste ouverte | 1 / 1305 | | **`get_activation_status` lit tout jeton lisible comme activé** | **0 / 140** | | **`shownUnderMachines` masque toutes les erreurs sous Machines** | **0 / 1305** | | **coche verte gardée pour une clé révoquée** | **0 / 1305** | Deux autres mutations restent vertes, sans conséquence pratique : `isSameLicense` sans `expires_at`, et le compteur affiché à côté d'une erreur de désactivation. ### Suggestions (non bloquantes) 1. **`src/components/settings/LicenseCard.tsx:53-57` — le filtre `shownUnderMachines` n'a aucun garde à l'affichage.** Le remplacer par `null` fait disparaître toutes les erreurs sous Machines (limite de machines, trop de tentatives, réseau) et laisse les 1305 tests verts. Le parcours D2 de #270, où le message de limite reste affiché pendant le chargement de la liste, n'est gardé qu'au niveau du reducer, et c'est cette PR qui insère un filtre sur son chemin d'affichage. Deux rendus suffisent : `activationError: machine_limit_reached` → `license.errors.machineLimitReached` présent ; `listOpen` avec `listError: network` → `license.errors.network` présent. 2. **`src-tauri/src/commands/license_commands.rs:686` (`get_activation_status`) — la décision du critère 3 n'est pas testée à son point.** Depuis D12, cette fonction décide seule de « Non activée » et du bouton « Activer », c'est-à-dire de la sortie de l'impasse de #360. La faire répondre `true` pour tout jeton lisible laisse 140/140 verts : seuls `validate_activation_with_key` et ses deux refus sont testés, ce que l'issue demandait. Le découpage déjà appliqué à `entitlements_from_dir` s'y transpose : `activation_status_in(dir, machine_id, key)`, testée dans un `TestDir` (jeton absent, expiré, d'une autre machine ou illisible → `false` ; jeton valide → `true`). 3. **`license.errors.invalidSignature` (FR/EN, ligne 1220) et guide §12 (ligne 561) — une clé authentique mal copiée est déclarée « non émise par le serveur de licences ».** Mesuré sur une clé correctement signée, avec un test sonde temporaire dans la copie : | Dégât | Variante renvoyée | |---|---| | 1, 3, 5, 7 ou 8 derniers caractères manquants | `Malformed` | | 2, 4 ou 6 derniers caractères manquants | `InvalidSignature` | | espace ou retour à la ligne dans la charge utile | `InvalidSignature` | | espace dans la signature, segment de signature absent | `Malformed` | `jsonwebtoken` vérifie la signature sur `en-tête.charge` **avant** de décoder la charge : toute altération de la charge, et une partie des troncatures de la signature, finissent en `InvalidSignature`. Un retour à la ligne collé dans le champ y est retiré ou changé en espace selon le moteur ; dans le second cas, on retombe sur `InvalidSignature`. Le guide range « copiée en partie ou coupée par un retour à la ligne » sous « illisible », et le message `malformed` conseille « sans espace », alors que ces cas affichent souvent « Cette clé de licence n'a pas été émise par le serveur de licences de Simpl'Résultat. ». Sur l'écran qu'un acheteur voit quand sa clé pose problème, ce message suggère une clé contrefaite. Formulation proposée : « Cette clé de licence n'a pas pu être vérifiée. Vérifiez qu'elle a été copiée en entier ; si c'est le cas, elle n'a pas été émise par le serveur de licences de Simpl'Résultat. », avec la puce du guide alignée. 4. **`src/components/settings/LicenseCard.tsx:79-84` — l'icône d'alerte de la tâche 4 n'est pas gardée.** Garder la `CheckCircle` pour une clé révoquée laisse la suite verte. Une assertion suffit : clé révoquée et machine non activée → aucune classe `lucide-circle-check-big` dans le rendu. La machine doit être non activée, car le badge « Activée » porte la même icône. ### Hors PR, trouvés en route - **Préexistant, à ouvrir en issue : un jeton d'activation issu d'une clé précédente survit à l'enregistrement d'une autre clé et masque « Activer ».** `store_license_in` n'écrit que `license.key`. Le jeton porte en `sub` l'UUID de l'ancienne licence, la clé porte le courriel : le client ne peut pas les rapprocher, et `get_activation_status` ne vérifie que la signature, `exp` et `machine_id`. La carte affiche donc « Activée », sans bouton « Activer », pendant que `/v1/prices` authentifie l'**ancienne** licence (`licenseAuth.ts` : licence révoquée → 403 `license_revoked`, licence Base → 403 `premium_required`). Cas concernés : rachat après un remboursement, réémission après une révocation, passage de Base à Premium. L'argument de la PR pour effacer `revoked` à l'enregistrement d'une autre clé (« une clé de remplacement bloquée jusqu'au redémarrage serait un piège ») n'atteint pas ce sous-cas : la clé de remplacement reste bloquée, par le jeton cette fois. Correctif simple : `store_license_in` supprime `activation.token` quand la clé enregistrée change, testé dans un `TestDir`. - **Sans lien avec la PR :** `npm ci` annonce 7 vulnérabilités (3 moderate, 4 high) dans les runs 430 (#362) et 433, alors que `CLAUDE.md` n'en décrit que 2 high acceptées (#317).
maximus added 1 commit 2026-10-01 00:00:57 +00:00
fix(license): apply the review of #364
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m56s
PR Check — Rust / rust (pull_request) Successful in 10m35s
3f90635298
- store_license_in drops activation.token when the stored key changes.
  The token names the previous license to the price server (its sub is
  the license id, the key carries the email), so the card showed the
  machine as activated while prices were refused for the old license.
  The same key entered again keeps its token; a refused key changes
  nothing.
- get_activation_status goes through activation_status_in(dir,
  machine_id, key), tested in a temporary directory: a missing,
  expired, foreign-machine, foreign-key or unreadable token reads as
  not activated. It alone decides "Not activated" and "Activate".
- LicenseCard is keyed by licenseIdentity(info): another key remounts
  the body, which re-reads the activation and drops the previous key's
  machine list.
- invalidSignature asks to check the copy first. jsonwebtoken checks
  the signature before decoding the payload, so a damaged copy often
  reads as a signature failure (characterization test). User guide and
  changelogs aligned.
- Render tests: the other errors stay visible under Machines, and the
  edition's green check gives way to an alert for a revoked key.

Refs #347

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

Revue appliquée : 3f90635

Les quatre suggestions et le défaut trouvé hors PR sont traités dans un seul commit, fix(license): apply the review of #364.

  1. Erreurs sous Machines : un rendu vérifie que machine_limit_reached (parcours D2 de #270) et une erreur réseau de la liste restent affichés. Seule la révocation passe au niveau de la carte.
  2. « Non activée » / « Activer » : get_activation_status passe par activation_status_in(dir, machine_id, key), testée dans un dossier temporaire. Un jeton valide rend true ; un jeton absent, expiré, d'une autre machine, signé par une autre clé ou illisible rend false.
  3. invalidSignature : la formulation proposée est reprise en FR et en EN (« n'a pas pu être vérifiée. Vérifiez qu'elle a été copiée en entier ; si c'est le cas… »), et le guide §12 ainsi que les deux CHANGELOG sont alignés. Un test de caractérisation épingle la cause : une espace glissée dans la charge utile ressort en InvalidSignature, parce que jsonwebtoken vérifie la signature avant de décoder la charge.
  4. Icône d'alerte : pour une clé révoquée, machine non activée, le rendu ne contient plus lucide-circle-check-big ; sans révocation, il la contient.
  5. Jeton d'une clé précédente (hors PR dans la revue, traité ici parce qu'il touche store_license_in et le critère « peut (ré)activer » de #347) : store_license_in supprime activation.token quand la clé enregistrée change. La même clé ressaisie garde son jeton, une clé refusée ne touche à rien, et un jeton sans clé est supprimé. Côté carte, LicenseCard porte la clé React licenseIdentity(info) : une autre clé remonte le corps, qui relit l'activation et oublie la liste de la clé précédente.

Avant / mutation / après

« Avant » = mesure du relecteur sur d2ded51. « Mutation » = même mutation sur 3f90635. « Après » = fichier restauré depuis HEAD et comparé ; la suite complète est verte sur cet arbre (local et CI ci-dessous).

Mutation Avant Mutation Après
get_activation_status lit tout jeton lisible comme activé 0 / 140 3 / 151 vert
shownUnderMachines masque toutes les erreurs sous Machines 0 / 1305 1 / 1308 vert
coche verte gardée pour une clé révoquée 0 / 1305 1 / 1308 vert
ErrorKind::InvalidSignature lu comme Malformed 1 / 140 2 / 151 vert
store_license_in garde le jeton d'une clé précédente — 2 / 151 vert
store_license_in supprime aussi le jeton de la même clé — 1 / 151 vert
LicenseCard sans clé React liée à la licence — 1 / 1308 vert
licenseIdentity sans la date d'émission — 2 / 1308 vert

CI

  • run 437 (check-rust) : HEAD is now at 3f90635, garde-fou exécuté (Suppression guard: 2 checks, exit 0), cargo test 151 passés, cargo audit sans vulnérabilité (12 avertissements autorisés).
  • run 436 (check-frontend) : HEAD is now at 3f90635, build (tsc + vite) OK, 68 fichiers, 1308 tests.

Les 7 vulnérabilités npm relevées en passant sont suivies par #358.

## Revue appliquée : `3f90635` Les quatre suggestions et le défaut trouvé hors PR sont traités dans un seul commit, `fix(license): apply the review of #364`. 1. **Erreurs sous Machines** : un rendu vérifie que `machine_limit_reached` (parcours D2 de #270) et une erreur réseau de la liste restent affichés. Seule la révocation passe au niveau de la carte. 2. **« Non activée » / « Activer »** : `get_activation_status` passe par `activation_status_in(dir, machine_id, key)`, testée dans un dossier temporaire. Un jeton valide rend `true` ; un jeton absent, expiré, d'une autre machine, signé par une autre clé ou illisible rend `false`. 3. **`invalidSignature`** : la formulation proposée est reprise en FR et en EN (« n'a pas pu être vérifiée. Vérifiez qu'elle a été copiée en entier ; si c'est le cas… »), et le guide §12 ainsi que les deux CHANGELOG sont alignés. Un test de caractérisation épingle la cause : une espace glissée dans la charge utile ressort en `InvalidSignature`, parce que `jsonwebtoken` vérifie la signature avant de décoder la charge. 4. **Icône d'alerte** : pour une clé révoquée, machine non activée, le rendu ne contient plus `lucide-circle-check-big` ; sans révocation, il la contient. 5. **Jeton d'une clé précédente** (hors PR dans la revue, traité ici parce qu'il touche `store_license_in` et le critère « peut (ré)activer » de #347) : `store_license_in` supprime `activation.token` quand la clé enregistrée change. La même clé ressaisie garde son jeton, une clé refusée ne touche à rien, et un jeton sans clé est supprimé. Côté carte, `LicenseCard` porte la clé React `licenseIdentity(info)` : une autre clé remonte le corps, qui relit l'activation et oublie la liste de la clé précédente. ### Avant / mutation / après « Avant » = mesure du relecteur sur `d2ded51`. « Mutation » = même mutation sur `3f90635`. « Après » = fichier restauré depuis `HEAD` et comparé ; la suite complète est verte sur cet arbre (local et CI ci-dessous). | Mutation | Avant | Mutation | Après | |---|---|---|---| | `get_activation_status` lit tout jeton lisible comme activé | 0 / 140 | **3 / 151** | vert | | `shownUnderMachines` masque toutes les erreurs sous Machines | 0 / 1305 | **1 / 1308** | vert | | coche verte gardée pour une clé révoquée | 0 / 1305 | **1 / 1308** | vert | | `ErrorKind::InvalidSignature` lu comme `Malformed` | 1 / 140 | **2 / 151** | vert | | `store_license_in` garde le jeton d'une clé précédente | — | **2 / 151** | vert | | `store_license_in` supprime aussi le jeton de la même clé | — | **1 / 151** | vert | | `LicenseCard` sans clé React liée à la licence | — | **1 / 1308** | vert | | `licenseIdentity` sans la date d'émission | — | **2 / 1308** | vert | ### CI - **run 437 (`check-rust`)** : `HEAD is now at 3f90635`, garde-fou exécuté (`Suppression guard: 2 checks, exit 0`), `cargo test` **151** passés, `cargo audit` sans vulnérabilité (12 avertissements autorisés). - **run 436 (`check-frontend`)** : `HEAD is now at 3f90635`, build (tsc + vite) OK, 68 fichiers, **1308** tests. Les 7 vulnérabilités npm relevées en passant sont suivies par #358.
Author
Owner

Revue adversariale — PR #364 (#347, #360), tête 3f90635

Verdict : APPROVE

Résumé — La revue précédente est appliquée et tient sous des mutations du relecteur : chaque mutation tentée sur le code neuf de 3f90635 (jeton supprimé au changement de clé, activation_status_in, carte indexée par la licence) fait rougir la suite, et les 151 tests Rust et 1308 tests vitest passent, en CI comme dans une copie hors dépôt. Restent une course étroite qui peut reposer le jeton de l'ancienne clé, un badge « Activée » vert pour une clé révoquée et un renvoi incomplet dans l'ADR 0017. Rien de bloquant.

Vérifié

  • CI de la tête 3f90635, lue dans les logs : run 436 (frontend) HEAD is now at 3f90635, tsc && vite build vert, 68 fichiers / 1308 tests ; run 437 (rust) HEAD is now at 3f90635, Suppression guard: 2 checks, exit 0, 151 passed, aucun avertissement du compilateur, cargo audit à 12 avertissements autorisés.
  • Rejoué hors dépôt (git archive 3f90635, target Cargo dans le scratchpad) : cargo test --lib 151, vitest 1308 sur 68 fichiers. Le dépôt n'a pas été touché.
  • Fermetures : la timeline porte un pull_ref closes venant de #364 sur #347 et #360, none sur #363. Aucun mot-clé de fermeture dans les commentaires de la PR.
  • Lignes ajoutées : aucun secret ni donnée personnelle (user@example.com seulement), aucun .only, .skip ou #[ignore], commits au format conventionnel.
  • Rendu i18n réel : les tests de rendu renvoient les clés sans les traduire. Avec i18next 25.8.4 et les vraies locales, license.machines.count, qui n'a pas de formes plurielles, retombe sur la clé nue : « 1/3 machines activées », « 2/3 machines activées », « 1/3 machines activated ». noExpiry, invalidSignature et authFailed se résolvent dans les deux langues.
  • Côté serveur (maximus-api ac91da4) : licenseAuth.ts répond 401 à un jeton absent, expiré ou invalide, ce que vise le nouveau authFailed ; revokeLicense passe toutes les activations de la licence à is_active = false.

Mutations du relecteur (sur 3f90635)

Copie hors dépôt ; chaque fichier restauré depuis sa sauvegarde et comparé par sha256.

Mutation Rouges
store_license_in compare le courriel des deux clés au lieu des clés (un passage Base → Premium du même acheteur garderait le jeton) 1 / 151
store_license_in ne supprime le jeton que si une clé était déjà stockée 1 / 151
activation_status_in ignore l'exp du jeton 1 / 151
LicenseCard indexée par le seul courriel 1 / 1308
LicenseCard passe validationError={null} au formulaire 0 / 1308
useLicenseMachines ne lit plus l'activation au montage 0 / 1308

Les deux mutations vertes ne demandent pas d'action, mais elles bornent ce que la suite prouve :

  • Le passage carte → formulaire reste derrière l'état interne showInput, qu'un rendu statique n'ouvre jamais : l'extraction de LicenseKeyForm garde le formulaire, pas la prop que la carte lui passe. La prop étant requise, le risque est faible.
  • L'effet de montage date de #270, et renderToStaticMarkup n'exécute pas les effets. 3f90635 s'appuie dessus (« une autre clé remonte le corps, qui relit l'activation ») : le remontage est prouvé par le test de clé, la relecture ne l'est pas. La garder demanderait un rendu client.

Suggestions (non bloquantes)

  1. Une activation en vol peut reposer le jeton de la clé précédente. activate_machine_impl (src-tauri/src/commands/license_commands.rs:635-648) lit la clé stockée (l. 636), attend le serveur jusqu'à 15 s (l. 639-645), puis écrit le jeton (l. 647) sans relire la clé. Pendant cet appel, « Entrer une clé de licence » reste cliquable (src/components/settings/LicenseCard.tsx:127-135) et l'envoi du formulaire n'est bloqué que par validating (LicenseKeyForm.tsx:53). Si une autre clé est enregistrée dans l'intervalle, store_license_in supprime le jeton, puis l'activation de l'ancienne clé en écrit un. La carte remontée affiche « Non activée » ou « Activée » selon l'ordre des deux écritures, et « Activée » à la visite suivante. On retombe alors dans l'état que 3f90635 ferme : cette machine absente de la liste de la nouvelle clé (aucun « Désactiver »), pas de bouton « Activer », la ressaisie de la même clé garde le jeton, et /v1/prices authentifie l'ancienne licence (révoquée ou Base : 403). La seule sortie dans l'interface est d'enregistrer une autre clé, puis la bonne.
    La fenêtre est étroite et le correctif tient en une ligne : désactiver « Entrer une clé de licence » tant que machines.activating est vrai, ce qu'un rendu statique vérifie (hookView({ activation: notActivated, activating: true }) → le bouton license.enterKey porte disabled ; « Activer » le porte déjà dans cet état, l'assertion doit viser le bon bouton). Variante côté Rust : relire la clé stockée juste avant persist_activation_token et ne rien écrire si elle a changé.
    Le même état existe déjà sur disque là où la clé a changé après une activation sous v0.15.0 ou un build antérieur à 3f90635 : la mise à jour ne le corrige pas, la ressaisie de la clé courante non plus. En pratique, il s'agit des postes des testeurs, et la sortie est la même.

  2. Le badge « Activée » reste vert pour une clé révoquée (LicenseCard.tsx:187-192). La révocation désactive toutes les machines côté serveur (revokeLicense, maximus-api), mais le jeton local reste valide : la carte montre le bandeau rouge et l'icône d'alerte de l'édition, puis, sous Machines, « Activée » avec la coche verte que la tâche 4 retire de l'édition. Le test de rendu le relève en commentaire. Pour une clé révoquée, masquer ce badge ou le rendre neutre aligne la section Machines sur le reste de la carte.

  3. ADR 0017 : renvoi incomplet. L'amendement #347 renvoie aux « mentions de liaison à la machine plus bas (Contexte, §5, Liens) », mais le §1 en porte aussi : « qu'elle soit native ou issue d'un déclassement machine-binding » (l. 53), et le commentaire « une clé déclassée » du bloc de code (l. 49). Ajouter §1 à la liste.

Hors PR, trouvé en route

  • Un 403 license_revoked des cours s'affiche comme « Cette fonction nécessite un abonnement premium ». fetch_price_inner (src-tauri/src/commands/balance_commands.rs:165) traduit tout 403 en PremiumRequired sans lire error.code, alors que licenseAuth.ts distingue license_revoked de premium_required et que docs/api-contract-prices.md prévoit errors.licenseRevoked. La clé balance.priceFetching.errors.licenseRevoked existe dans les deux langues sans aucun appelant. Un acheteur Premium remboursé lit donc qu'il lui faut Premium. À rattacher à #365, qui reprend l'affichage de la révocation.
## Revue adversariale — PR #364 (#347, #360), tête `3f90635` **Verdict : APPROVE** **Résumé** — La revue précédente est appliquée et tient sous des mutations du relecteur : chaque mutation tentée sur le code neuf de `3f90635` (jeton supprimé au changement de clé, `activation_status_in`, carte indexée par la licence) fait rougir la suite, et les 151 tests Rust et 1308 tests vitest passent, en CI comme dans une copie hors dépôt. Restent une course étroite qui peut reposer le jeton de l'ancienne clé, un badge « Activée » vert pour une clé révoquée et un renvoi incomplet dans l'ADR 0017. Rien de bloquant. ### Vérifié - **CI de la tête `3f90635`, lue dans les logs** : run 436 (frontend) `HEAD is now at 3f90635`, `tsc && vite build` vert, 68 fichiers / 1308 tests ; run 437 (rust) `HEAD is now at 3f90635`, `Suppression guard: 2 checks, exit 0`, `151 passed`, aucun avertissement du compilateur, `cargo audit` à 12 avertissements autorisés. - **Rejoué hors dépôt** (`git archive 3f90635`, target Cargo dans le scratchpad) : `cargo test --lib` 151, vitest 1308 sur 68 fichiers. Le dépôt n'a pas été touché. - **Fermetures** : la timeline porte un `pull_ref` `closes` venant de #364 sur #347 et #360, `none` sur #363. Aucun mot-clé de fermeture dans les commentaires de la PR. - **Lignes ajoutées** : aucun secret ni donnée personnelle (`user@example.com` seulement), aucun `.only`, `.skip` ou `#[ignore]`, commits au format conventionnel. - **Rendu i18n réel** : les tests de rendu renvoient les clés sans les traduire. Avec i18next 25.8.4 et les vraies locales, `license.machines.count`, qui n'a pas de formes plurielles, retombe sur la clé nue : « 1/3 machines activées », « 2/3 machines activées », « 1/3 machines activated ». `noExpiry`, `invalidSignature` et `authFailed` se résolvent dans les deux langues. - **Côté serveur** (maximus-api `ac91da4`) : `licenseAuth.ts` répond 401 à un jeton absent, expiré ou invalide, ce que vise le nouveau `authFailed` ; `revokeLicense` passe toutes les activations de la licence à `is_active = false`. ### Mutations du relecteur (sur `3f90635`) Copie hors dépôt ; chaque fichier restauré depuis sa sauvegarde et comparé par sha256. | Mutation | Rouges | |---|---| | `store_license_in` compare le courriel des deux clés au lieu des clés (un passage Base → Premium du même acheteur garderait le jeton) | 1 / 151 | | `store_license_in` ne supprime le jeton que si une clé était déjà stockée | 1 / 151 | | `activation_status_in` ignore l'`exp` du jeton | 1 / 151 | | `LicenseCard` indexée par le seul courriel | 1 / 1308 | | `LicenseCard` passe `validationError={null}` au formulaire | **0 / 1308** | | `useLicenseMachines` ne lit plus l'activation au montage | **0 / 1308** | Les deux mutations vertes ne demandent pas d'action, mais elles bornent ce que la suite prouve : - Le passage carte → formulaire reste derrière l'état interne `showInput`, qu'un rendu statique n'ouvre jamais : l'extraction de `LicenseKeyForm` garde le formulaire, pas la prop que la carte lui passe. La prop étant requise, le risque est faible. - L'effet de montage date de #270, et `renderToStaticMarkup` n'exécute pas les effets. `3f90635` s'appuie dessus (« une autre clé remonte le corps, qui relit l'activation ») : le remontage est prouvé par le test de clé, la relecture ne l'est pas. La garder demanderait un rendu client. ### Suggestions (non bloquantes) 1. **Une activation en vol peut reposer le jeton de la clé précédente.** `activate_machine_impl` (`src-tauri/src/commands/license_commands.rs:635-648`) lit la clé stockée (l. 636), attend le serveur jusqu'à 15 s (l. 639-645), puis écrit le jeton (l. 647) sans relire la clé. Pendant cet appel, « Entrer une clé de licence » reste cliquable (`src/components/settings/LicenseCard.tsx:127-135`) et l'envoi du formulaire n'est bloqué que par `validating` (`LicenseKeyForm.tsx:53`). Si une autre clé est enregistrée dans l'intervalle, `store_license_in` supprime le jeton, puis l'activation de l'ancienne clé en écrit un. La carte remontée affiche « Non activée » ou « Activée » selon l'ordre des deux écritures, et « Activée » à la visite suivante. On retombe alors dans l'état que `3f90635` ferme : cette machine absente de la liste de la nouvelle clé (aucun « Désactiver »), pas de bouton « Activer », la ressaisie de la même clé garde le jeton, et `/v1/prices` authentifie l'ancienne licence (révoquée ou Base : 403). La seule sortie dans l'interface est d'enregistrer une autre clé, puis la bonne. La fenêtre est étroite et le correctif tient en une ligne : désactiver « Entrer une clé de licence » tant que `machines.activating` est vrai, ce qu'un rendu statique vérifie (`hookView({ activation: notActivated, activating: true })` → le bouton `license.enterKey` porte `disabled` ; « Activer » le porte déjà dans cet état, l'assertion doit viser le bon bouton). Variante côté Rust : relire la clé stockée juste avant `persist_activation_token` et ne rien écrire si elle a changé. Le même état existe déjà sur disque là où la clé a changé après une activation sous v0.15.0 ou un build antérieur à `3f90635` : la mise à jour ne le corrige pas, la ressaisie de la clé courante non plus. En pratique, il s'agit des postes des testeurs, et la sortie est la même. 2. **Le badge « Activée » reste vert pour une clé révoquée** (`LicenseCard.tsx:187-192`). La révocation désactive toutes les machines côté serveur (`revokeLicense`, maximus-api), mais le jeton local reste valide : la carte montre le bandeau rouge et l'icône d'alerte de l'édition, puis, sous Machines, « Activée » avec la coche verte que la tâche 4 retire de l'édition. Le test de rendu le relève en commentaire. Pour une clé révoquée, masquer ce badge ou le rendre neutre aligne la section Machines sur le reste de la carte. 3. **ADR 0017 : renvoi incomplet.** L'amendement #347 renvoie aux « mentions de liaison à la machine plus bas (Contexte, §5, Liens) », mais le §1 en porte aussi : « qu'elle soit native ou issue d'un déclassement machine-binding » (l. 53), et le commentaire « une clé déclassée » du bloc de code (l. 49). Ajouter §1 à la liste. ### Hors PR, trouvé en route - **Un 403 `license_revoked` des cours s'affiche comme « Cette fonction nécessite un abonnement premium ».** `fetch_price_inner` (`src-tauri/src/commands/balance_commands.rs:165`) traduit tout 403 en `PremiumRequired` sans lire `error.code`, alors que `licenseAuth.ts` distingue `license_revoked` de `premium_required` et que `docs/api-contract-prices.md` prévoit `errors.licenseRevoked`. La clé `balance.priceFetching.errors.licenseRevoked` existe dans les deux langues sans aucun appelant. Un acheteur Premium remboursé lit donc qu'il lui faut Premium. À rattacher à #365, qui reprend l'affichage de la révocation.
maximus added 1 commit 2026-10-01 00:45:44 +00:00
fix(license): apply the second review of #364
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m43s
PR Check — Rust / rust (pull_request) Successful in 9m15s
cdf4df85f7
- Key entry and machine activation can no longer overlap. An activation
  in flight writes the token of the key it read: a key stored meanwhile
  ended up next to the previous key's token, the dead end 3f90635
  closes. "Enter a key" and the form's submit are disabled while an
  activation runs (LicenseKeyForm gains a busy prop), and "Activate"
  while a key is being stored.
- The "Activated" badge is hidden for a revoked key: the server
  deactivated every machine of the license.
- ADR 0017: the #347 amendment also points at section 1, which still
  mentions the machine-binding downgrade.

Refs #347

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

Deuxième revue appliquée : cdf4df8

  1. Course entre activation et nouvelle clé : les deux sens sont fermés côté carte. Pendant une activation, « Entrer une clé » et l'envoi du formulaire sont désactivés (LicenseKeyForm gagne une prop busy). Pendant l'enregistrement d'une clé, « Activer » l'est aussi : l'activation aurait lu l'ancienne clé et écrit son jeton après la nouvelle. La variante Rust (relire la clé avant d'écrire le jeton) n'est pas retenue, car la carte est le seul chemin d'écriture d'une clé.
  2. Badge « Activée » masqué pour une clé révoquée : le serveur a désactivé toutes ses machines.
  3. ADR 0017 : l'amendement #347 renvoie aussi au §1.

Le 403 license_revoked des cours, affiché comme « abonnement premium requis », est consigné sur #365 (commentaire du 2026-10-01).

Mutations, sur l'arbre commité

Mutation Rouges
« Entrer une clé » cliquable pendant une activation 1 / 1311
envoi du formulaire possible pendant une activation 1 / 1311
« Activer » cliquable pendant l'enregistrement d'une clé 1 / 1311
badge « Activée » gardé pour une clé révoquée 1 / 1311

Chaque fichier a été restauré depuis HEAD, et l'arbre vérifié propre.

CI

  • run 439 (check-rust) : HEAD is now at cdf4df8, garde-fou exécuté, cargo test 151 passés, cargo audit sans vulnérabilité (12 avertissements autorisés).
  • run 438 (check-frontend) : HEAD is now at cdf4df8, build (tsc + vite) OK, 68 fichiers, 1311 tests.
## Deuxième revue appliquée : `cdf4df8` 1. **Course entre activation et nouvelle clé** : les deux sens sont fermés côté carte. Pendant une activation, « Entrer une clé » et l'envoi du formulaire sont désactivés (`LicenseKeyForm` gagne une prop `busy`). Pendant l'enregistrement d'une clé, « Activer » l'est aussi : l'activation aurait lu l'ancienne clé et écrit son jeton après la nouvelle. La variante Rust (relire la clé avant d'écrire le jeton) n'est pas retenue, car la carte est le seul chemin d'écriture d'une clé. 2. **Badge « Activée »** masqué pour une clé révoquée : le serveur a désactivé toutes ses machines. 3. **ADR 0017** : l'amendement #347 renvoie aussi au §1. Le 403 `license_revoked` des cours, affiché comme « abonnement premium requis », est consigné sur #365 (commentaire du 2026-10-01). ### Mutations, sur l'arbre commité | Mutation | Rouges | |---|---| | « Entrer une clé » cliquable pendant une activation | 1 / 1311 | | envoi du formulaire possible pendant une activation | 1 / 1311 | | « Activer » cliquable pendant l'enregistrement d'une clé | 1 / 1311 | | badge « Activée » gardé pour une clé révoquée | 1 / 1311 | Chaque fichier a été restauré depuis `HEAD`, et l'arbre vérifié propre. ### CI - **run 439 (`check-rust`)** : `HEAD is now at cdf4df8`, garde-fou exécuté, `cargo test` **151** passés, `cargo audit` sans vulnérabilité (12 avertissements autorisés). - **run 438 (`check-frontend`)** : `HEAD is now at cdf4df8`, build (tsc + vite) OK, 68 fichiers, **1311** tests.
maximus merged commit cdf4df85f7 into main 2026-10-01 00:57:33 +00:00
maximus deleted branch issue-347-license-card 2026-10-01 00:57:33 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

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