--- 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--", 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-.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' |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--`, 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).