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

10 KiB

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) :
    #!/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).