Simpl-Resultat/.claude/skills/issue-license/SKILL.md
le king fu 11e12ab7a3 docs(skills): add /issue-license and a release verification script
- issue-license (new): issue licenses by hand through maximus-api's admin
  endpoint (testers, gifts, refund compensation, admin account); the admin
  key and the issued keys never enter the transcript. Third-party
  beneficiaries are named by their Logto id, never by email: the
  repository is public.
- release step 0: when a release tightens edition gating, tester keys must
  be entered before the tag, since an install without a key receives that
  version and then nothing more.
- release step 9: scripts/verify-release.sh checks the 7 assets by suffix
  and the content of latest.json, instead of a script rewritten each time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 19:51:01 -04:00

78 lines
10 KiB
Markdown

---
name: issue-license
description: Émettre à la main une ou plusieurs licences Simpl-Résultat (SR-BASE / SR-PREMIUM) via l'endpoint admin de maximus-api, sans passer par Stripe, pour tout bénéficiaire : testeur, cadeau, compensation après un remboursement, compte admin, utilisateur hors Stripe ; et les distribuer avant une release qui verrouille des fonctionnalités par édition. Use when the user says "émets une licence", "clé pour Michel", "offre une licence à X", "donne une licence Premium à X", "il me faut une clé de test", "génère une licence", "compense le remboursement de X", "les testeurs doivent garder l'accès", "issue a license", "generate license key", "gift a license", or before tagging a release that changes FEATURE_TIERS.
user-invocable: true
allowed-tools: Bash, Read, Write
created: 2026-09-13
updated: 2026-09-24
---
# /issue-license — Émettre des licences à la main (testeurs, cadeaux, compensations, admin)
Émission via `POST /v1/licenses/generate` de maximus-api (route admin, Bearer `ADMIN_API_KEY`), hors Stripe, pour tout bénéficiaire : testeur, cadeau, compensation après un remboursement, compte admin. La clé admin et les clés émises ne transitent jamais par la conversation.
## Faits vérifiés (2026-09-06, code v0.14.0 → main + maximus-api prod)
- Validation **locale** de la clé (Ed25519, clé publique embarquée + `exp`), aucun réseau, déverrouillage immédiat sans redémarrage. La clé publique est identique entre v0.14.0 et `main` → une clé peut être entrée **avant** la mise à jour.
- Endpoint : `POST https://api.lacompagniemaximus.com/v1/licenses/generate`, Bearer `ADMIN_API_KEY` (celle de prod vit dans Coolify → maximus-api → Environment Variables ; le `.env` local en a une autre). Payload : `product: "simpl-resultat"`, `email` (affiché dans la carte licence → vrai courriel), `edition` (`base` | `premium`), `expires_in_days` (défaut et max 3650), `machine_limit` (défaut 3, max 10). Réponse `{ key: "SR-<EDITION>-<JWT>", expires_at }`. **N'envoie aucun courriel** (seul le flux Stripe le fait) → distribution manuelle. Rate limit 5/min sur `/v1/licenses/*`.
- Activation machine optionnelle : l'édition résout sans `activation.token` (`license_commands.rs`, état « graceful pre-activation »). Seule exception : le fetch de cours du Bilan envoie `activation.token` en Bearer à `/v1/prices` (`balance_commands.rs`) → un testeur Premium qui utilise les cours doit cliquer « Activer cette machine ». Une clé non activée marche sur n'importe quel poste ; activée, elle est liée (3 machines par défaut).
- Un compte Logto n'est **pas** requis : `generate` prend un courriel libre. Un compte Logto sans abonnement `active` ne donne rien côté app.
- Clé révoquée (`is_revoked = t`) : elle continue de déverrouiller l'UI hors ligne, seul le serveur (activation, cours) la refuse. Une nouvelle clé saisie écrase l'ancienne dans `license.key` du client.
- Bénéficiaires connus (testeurs) : Max (`maxime2tremblay@protonmail.com`) et Michel (Logto `michelt` ; son courriel est dans la table `users` de Logto et dans la mémoire projet `project-tester-licenses`, pas ici). Les deux en **Premium**, 3650 j, 3 machines (décision Max 2026-09-06).
## Workflow
1. **Inventaire des licences existantes** (commande à Max en `!`, le classifier bloque les `ssh` VPS en mode auto) :
```
! ssh ubuntu@100.86.30.21 "docker exec zgkgc8soo8k4k44woo00ckg8 psql -U postgres -d maximus_api -t -A -F ' | ' -c \"SELECT product, edition, user_email, machine_limit, is_revoked, created_at::date, expires_at::date FROM licenses ORDER BY created_at;\""
```
Repérer les clés révoquées (`is_revoked = t`) et les clés déjà émises pour le même courriel avant d'en créer une nouvelle.
2. **Clé admin hors transcript** : Max la dépose depuis son propre terminal (jamais en `!`, qui écrirait la clé dans la transcription). Valeur : Coolify → maximus-api → Environment Variables → `ADMIN_API_KEY`.
```
printf '%s' 'LA_CLÉ' > ~/.maximus-admin-key && chmod 600 ~/.maximus-admin-key
```
Guillemets simples obligatoires (pas d'expansion, pas de retour à la ligne). Si l'historique shell est actif : `history -d $(history 1 | awk '{print $1}')`.
3. **Confirmer les paramètres** avec Max : courriel du bénéficiaire (affiché dans la carte licence), `edition` (`base` | `premium`), `machine_limit` (défaut 3, max 10), `expires_in_days` (défaut et max 3650). Puis **émettre** avec le script (lit `~/.maximus-admin-key`, écrit les clés dans `~/simpl-resultat-licences-<date>.txt` en 600, n'affiche que courriel / édition / expiration) :
```bash
#!/usr/bin/env bash
set -euo pipefail
KEY=$(cat ~/.maximus-admin-key); OUT=~/simpl-resultat-licences-$(date +%F).txt; umask 077; : > "$OUT"
while IFS='|' read -r email edition; do
resp=$(curl -sS -X POST https://api.lacompagniemaximus.com/v1/licenses/generate \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d "{\"product\":\"simpl-resultat\",\"email\":\"$email\",\"edition\":\"$edition\",\"expires_in_days\":3650,\"machine_limit\":3}")
printf '%s | %s | %s\n' "$email" "$edition" "$(echo "$resp" | python3 -c 'import json,sys;print(json.load(sys.stdin).get("expires_at"))')"
printf '%s\t%s\t%s\n' "$email" "$edition" "$(echo "$resp" | python3 -c 'import json,sys;print(json.load(sys.stdin)["key"])')" >> "$OUT"
sleep 13 # rate limit 5/min
done <<'EOF'
<courriel>|premium
EOF
echo "Clés dans $OUT"
```
Une ligne `courriel|edition` par bénéficiaire (une seule pour un cadeau ou une compensation). Le script vit dans le repo (`scripts/issue-license.sh`, jamais la liste des clés) ou dans le scratchpad — pas de clé dans git ni dans la conversation. Ne jamais `cat` le fichier de sortie dans la session.
4. **Vérifier la clé sans l'afficher**, contre le validateur du client (`src-tauri/src/commands/license_commands.rs` : `PUBLIC_KEY_PEM` embarquée, identique entre les versions publiées, et `strict_validation()`) : lire la clé depuis le fichier dans un script (jamais en argument de commande), vérifier le préfixe `SR-<EDITION>-`, la signature Ed25519 du JWT, la présence de `exp` et `iat`, `edition` et `machine_limit` attendus. N'imprimer que le verdict.
5. **Livrer** : le fichier va sur le poste de Max via `transfer-asset` (Pop!_OS, `~/Documents/Projets/simpl-resultat/`), avec `scp -p` pour garder le 600 et `sha256sum` des deux côtés ; annoncer chemin, taille, permissions et empreinte tronquée, jamais le contenu. Fournir la marche à suivre (FR/EN) au bénéficiaire : Paramètres → Utilisateurs → carte Licence → coller → « Activer ». Premium avec cours : cliquer aussi « Activer cette machine ».
6. **Tracer sans la clé** : mémoire projet (qui, quand, édition, où est le fichier), ligne dans `STATE.md`. Proposer `rm ~/.maximus-admin-key` une fois terminé (la clé reste dans Coolify).
7. **Si le déclencheur est une release** : `/release` seulement après. Ordre inverse = bénéficiaires coincés en Free sans auto-update.
## Règles
- La clé admin et les clés émises ne vont **jamais** dans la conversation, une mémoire, un repo ou une issue.
- Le courriel d'un bénéficiaire tiers ne va pas dans ce fichier ni ailleurs dans le repo : le dépôt `Simpl-Resultat` est **public**. Le désigner par son identifiant Logto ; le courriel se relit au moment d'émettre.
- Émettre **avant** de tagger une release qui resserre le gating : l'auto-update est Base+ depuis v0.15.0, donc une installation sans clé reçoit cette version, puis plus rien.
- L'activation machine (`activation.token`) n'est requise que pour les endpoints serveur (cours du Bilan) ; la clé seule débloque tout le reste, hors ligne.
## Gotchas
- Le classifieur du mode auto bloque tout `ssh` vers le VPS (Tailscale comme IP publique) : remettre la commande à Max en `!` avec le texte exact plutôt que la reformuler ou contourner.
- `Connection timed out` sans lien de reauth Tailscale = clé de nœud expirée (180 j), pas une attente navigateur — voir le skill global `transfer-asset`.
- `.rpm` : aucune mise à jour quelle que soit la licence (#320).
- Un testeur multi-profils qui monte en v0.15.0 sans clé reste coincé sur le profil actif au moment de l'upgrade (données intactes, ADR 0017 §5). Pas de clause de grand-père ni de période de grâce dans le code ; `SR_DEV_EDITION` est compilé hors des builds publiés.
- Le binaire v0.14.0 appelle encore `/licenses/activate` (#270 non livré) : le serveur répond 308 → `/v1/licenses/activate` et infère le produit du préfixe `SR-` (#48) — ça passe.
- Tenir la liste des bénéficiaires ici, par identifiant Logto, pas dans Forgejo (aucune org, 3 comptes seulement) : la source des courriels est la table `users` de Logto (tenant `default`).
## Changelog
- 2026-09-13 — Creation initiale via /skills-review-inbox (draft 2026-09-06, source 690a6129). Procedure d'emission de licences testeurs reconstituee avant la release v0.15.0 (verrouillage par edition) ; faits verifies sur le code v0.14.0 -> main et maximus-api prod au 2026-09-06.
- 2026-09-13 — Fusion de la proposition `issue-tester-licence` (draft 2026-09-12, même source 690a6129) et élargissement du périmètre à tout bénéficiaire hors Stripe (cadeaux, compensation après remboursement, compte admin), décision Max. Ajouts : sémantique des clés révoquées, dépôt de la clé admin hors historique shell, étape de vérification locale de la clé sans l'afficher, livraison `scp -p` + `sha256sum`, traçabilité sans la clé et retrait de la clé admin, section Règles, gotchas classifieur `ssh` VPS et clé de nœud Tailscale expirée. Heredoc du script passé en placeholder.
- 2026-09-24 — Courriel personnel de Michel retiré avant le premier commit : le dépôt est public et l'adresse n'était dans aucun fichier suivi. Il est désigné par son identifiant Logto `michelt`, et une règle interdit le courriel d'un tiers dans le repo. Le courriel de Max est conservé : il figure déjà dans trois fichiers suivis (ADR 0011, ADR 0013, `docs/api-contract-prices.md`). Source : session 997f21af (/warmup).