- 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>
10 KiB
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 etmain→ une clé peut être entrée avant la mise à jour. - Endpoint :
POST https://api.lacompagniemaximus.com/v1/licenses/generate, BearerADMIN_API_KEY(celle de prod vit dans Coolify → maximus-api → Environment Variables ; le.envlocal 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 envoieactivation.tokenen 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 :
generateprend un courriel libre. Un compte Logto sans abonnementactivene 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 danslicense.keydu client. - Bénéficiaires connus (testeurs) : Max (
maxime2tremblay@protonmail.com) et Michel (Logtomichelt; son courriel est dans la tableusersde Logto et dans la mémoire projetproject-tester-licenses, pas ici). Les deux en Premium, 3650 j, 3 machines (décision Max 2026-09-06).
Workflow
- Inventaire des licences existantes (commande à Max en
!, le classifier bloque lessshVPS en mode auto) :
Repérer les clés révoquées (! 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;\""is_revoked = t) et les clés déjà émises pour le même courriel avant d'en créer une nouvelle. - 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.
Guillemets simples obligatoires (pas d'expansion, pas de retour à la ligne). Si l'historique shell est actif :printf '%s' 'LA_CLÉ' > ~/.maximus-admin-key && chmod 600 ~/.maximus-admin-keyhistory -d $(history 1 | awk '{print $1}'). - 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>.txten 600, n'affiche que courriel / édition / expiration) :
Une ligne#!/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"courriel|editionpar 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 jamaiscatle fichier de sortie dans la session. - Vérifier la clé sans l'afficher, contre le validateur du client (
src-tauri/src/commands/license_commands.rs:PUBLIC_KEY_PEMembarquée, identique entre les versions publiées, etstrict_validation()) : lire la clé depuis le fichier dans un script (jamais en argument de commande), vérifier le préfixeSR-<EDITION>-, la signature Ed25519 du JWT, la présence deexpetiat,editionetmachine_limitattendus. N'imprimer que le verdict. - Livrer : le fichier va sur le poste de Max via
transfer-asset(Pop!_OS,~/Documents/Projets/simpl-resultat/), avecscp -ppour garder le 600 etsha256sumdes 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 ». - Tracer sans la clé : mémoire projet (qui, quand, édition, où est le fichier), ligne dans
STATE.md. Proposerrm ~/.maximus-admin-keyune fois terminé (la clé reste dans Coolify). - Si le déclencheur est une release :
/releaseseulement 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-Resultatest 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
sshvers le VPS (Tailscale comme IP publique) : remettre la commande à Max en!avec le texte exact plutôt que la reformuler ou contourner. Connection timed outsans lien de reauth Tailscale = clé de nœud expirée (180 j), pas une attente navigateur — voir le skill globaltransfer-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_EDITIONest 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/activateet infère le produit du préfixeSR-(#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
usersde Logto (tenantdefault).
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, livraisonscp -p+sha256sum, traçabilité sans la clé et retrait de la clé admin, section Règles, gotchas classifieursshVPS 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).