39 KiB
Spec — Monétisation Simpl'Résultat
Date: 2026-04-08 Projet: simpl-resultat Statut: En cours (Phase 1 complétée) Dépendances: spec-simpl-resultat-web.md (version web/SaaS), Logto IdP (Compte Maximus)
Contexte
Simpl'Résultat est une app desktop Tauri v2 open-source (GPL-3.0) pour la gestion des finances personnelles, actuellement gratuite. Le moment de monétiser approche. L'objectif est de mettre en place l'infrastructure complète pour vendre le logiciel desktop (achat unique) et offrir une version web SaaS (abonnement mensuel), tout en préservant le principe privacy-first.
Modèle retenu : Open Core + Hybride
- Le code desktop reste GPL-3.0 sur Forgejo (open core)
- Le code serveur (API licence, paiement, web/SaaS) est propriétaire
- Achat unique pour l'édition Base desktop
- Abonnement mensuel pour l'édition Premium (version web + sync + features avancées)
Objectif
Implémenter les 3 piliers de la monétisation : (1) un système de licence offline pour l'app desktop, (2) l'intégration du Compte Maximus optionnel pour les features premium, et (3) l'infrastructure de paiement (achat unique + abonnement). La version web/SaaS est couverte par le spec existant spec-simpl-resultat-web.md.
Scope
IN
- Système de clé de licence offline (Ed25519, vérification côté Rust)
- API serveur de licences (génération, validation, révocation)
- Connexion optionnelle au Compte Maximus (Logto) dans l'app desktop
- Page d'achat sur lacompagniemaximus.com (processeur de paiement)
- Gestion des abonnements (Stripe Billing ou alternative)
- UI licence et compte dans les paramètres de l'app desktop
- Webhook paiement → génération de licence automatique
- Gestion TPS/TVQ pour les ventes au Canada
OUT (explicitement exclu)
- Version web/SaaS (couvert par
spec-simpl-resultat-web.md) - Sync desktop ↔ web (couvert par
spec-simpl-resultat-web.md, Issue 5) - DRM agressif ou protection anti-compilation (incompatible GPL)
- App mobile
- Programme de revente / affiliés
- Facturation papier / comptabilité (usage externe, ex: Wave/QuickBooks)
Design
Modèle de tarification
| Tier | Prix | Contenu | Licence |
|---|---|---|---|
| Gratuit | 0$ | App desktop complète, sans clé, mises à jour manuelles uniquement | Aucune |
| Base | ~29-49$ CAD (unique) | App desktop + mises à jour automatiques + support email | Clé offline Ed25519 |
| Premium | ~7-12$ CAD/mois | Base + version web + sync desktop↔web + features avancées futures | Compte Maximus (abonnement actif) |
Note : Les prix exacts seront déterminés avant le lancement. Les fourchettes ci-dessus sont des recommandations basées sur le marché des apps de finances personnelles.
Architecture de licence
Clé de licence offline (Édition Base)
Format : JWT signé Ed25519, encodé Base64, vérifiable sans serveur.
SR-BASE-<base64url(JWT)>
Payload JWT :
{
"sub": "user@email.com",
"iss": "lacompagniemaximus.com",
"iat": 1712534400,
"edition": "base",
"features": ["auto-update"],
"machine_limit": 3
}
🔴 SECURITE — JWT sans claim
exp: une licence signée est valide à jamais, même après révocation serveur, car la vérification offline ne peut pas checker le statut de révocation. Résolution : Ajouter un claimexpobligatoire (ex: 2 ans). L'app doit demander une re-validation en ligne avant expiration pour obtenir une clé rafraîchie. Ref : CWE-613 (Insufficient Session Expiration)
Flux de validation :
- L'utilisateur entre sa clé dans Paramètres > Licence
- Le backend Rust décode le JWT et vérifie la signature Ed25519 avec la clé publique embarquée
- Si valide : stocke la clé dans un fichier
license.keydans le répertoire app data - L'app vérifie la clé au démarrage (lecture locale, aucun appel réseau)
- Features débloquées : mises à jour automatiques (l'updater vérifie la présence d'une licence valide)
Sécurité :
- La clé privée Ed25519 réside UNIQUEMENT sur le serveur de licences (VPS)
- La clé publique est embarquée dans le binaire Rust (hardcoded)
- Opérations crypto dans Rust uniquement (jamais dans le WebView)
machine_limitvérifié lors de l'activation en ligne (premier lancement)
🔴 SECURITE —
license.keyen clair est copiable entre machines, contournantmachine_limit. La vérification offline ne peut pas détecter la copie car il n'y a pas de binding machine dans la signature JWT. Résolution : Stocker un token d'activation séparé (signé par le serveur avec lemachine_idinclus). Vérifier à la fois le JWT licence + le token d'activation au démarrage.
🔴 TECHNIQUE — Le spec propose
ed25519-dalekmais c'estjsonwebtoken(avec feature EdDSA) qui décode les JWT.ed25519-dalekseul ne gère pas les headers/claims JWT. Résolution : Clarifier dans l'Issue 1 quejsonwebtokenest la dépendance primaire pour la validation JWT.ed25519-dalekpeut ne pas être nécessaire sijsonwebtokensupporte EdDSA nativement.
Compte Maximus (Édition Premium)
L'intégration Logto dans l'app desktop utilise le flow OAuth2 PKCE :
- L'utilisateur clique "Se connecter" dans Paramètres
- Tauri ouvre le navigateur système vers Logto (OAuth2 Authorization Code + PKCE)
- Après auth, callback vers
simpl-resultat://auth/callback - L'app échange le code pour un access token + refresh token
- Les tokens sont stockés de façon sécurisée (fichier chiffré dans app data)
🔴 SECURITE — Le chiffrement des tokens avec une clé dérivée du machine ID est faible. Les machine IDs sont à faible entropie, déterministes et lisibles publiquement (ex:
/etc/machine-idsur Linux). Équivaut à chiffrer avec une clé connue. Résolution : Utiliser le stockage natif de l'OS (keyring/credential manager via le cratekeyringoutauri-plugin-storeavec OS keychain). Si basé fichier, dériver la clé du machine ID + un secret aléatoire par installation stocké séparément. Ref : CWE-321 (Hard Coded Cryptographic Key)
🟡 TECHNIQUE — Le flow OAuth2 PKCE via
simpl-resultat://auth/callbacknécessite une config plateforme spécifique (registre Windows, etc.) non documentée dans le spec.tauri-plugin-deep-linkv2 requiert des entrées explicites danstauri.conf.jsonet les manifestes plateforme. Résolution : Ajouter une sous-tâche à l'Issue 6 pour la registration deep-link par plateforme. Référencer la doctauri-plugin-deep-linkv2.
- L'access token JWT contient les claims :
{ "apps": {"simpl-resultat": "premium"}, "subscription_status": "active" } - Vérification périodique du statut d'abonnement (1x/jour, graceful si offline)
Dégradation gracieuse :
- Si le token expire et le refresh échoue → affiche un avertissement mais ne bloque pas l'usage Base
- Si l'abonnement expire → l'app repasse en mode Base (clé offline toujours valide)
- Grace period de 7 jours après expiration de l'abonnement
Comparaison des processeurs de paiement
| Critère | Stripe | Square | Paddle | LemonSqueezy | FastSpring |
|---|---|---|---|---|---|
| Frais | 2.9% + 0.30$ | 2.9% + 0.30$ | 5% + 0.50$ | 5% | ~8.9% |
| Merchant of Record | Non (tu gères les taxes) | Non | Oui | Oui | Oui |
| Gestion TPS/TVQ | Via Stripe Tax (add-on) | Manuel | Inclus | Inclus | Inclus |
| Abonnements | Stripe Billing (excellent) | Square Subscriptions | Natif | Natif | Natif |
| Clés de licence | Non (API custom) | Non | Non | Intégré | Intégré |
| API/Webhooks | Excellent | Bon | Bon | Bon | Bon |
| Desktop software | Bon (généraliste) | Faible (focus commerce) | Bon | Bon | Excellent (spécialisé) |
| Canada | Oui (HQ Montréal inaccessible mais opère au Canada) | Oui | Oui | Oui (Stripe subsidiary) | Oui |
| Paiement CAD | Oui | Oui | Oui | Oui | Oui |
Recommandation : Stripe pour les raisons suivantes :
- Frais les plus bas (2.9% + 0.30$)
- API la plus robuste et documentée
- Stripe Billing pour les abonnements récurrents
- Stripe Tax pour la collecte TPS/TVQ automatique (0.5% add-on)
- Stripe Checkout pour une page de paiement hébergée (conforme PCI sans effort)
- Webhooks fiables pour automatiser la génération de licences
- LemonSqueezy est maintenant une filiale Stripe — si tu veux le MoR plus tard, migration facile
Alternative viable : Paddle/LemonSqueezy si tu veux zéro gestion fiscale (MoR). Plus cher (~5%) mais ils gèrent TPS/TVQ/TVA mondialement. Recommandé si tu vends internationalement dès le départ.
Square : Non recommandé pour la vente de logiciel. Orienté commerce de détail/point de vente physique. API d'abonnement moins mature que Stripe. Pas de Merchant of Record. Pas d'intégration licence.
UX / Interface
Paramètres > Licence (nouveau card)
┌─────────────────────────────────────────────┐
│ 🔑 Licence │
│ │
│ Édition : Gratuite │
│ │
│ [Entrer une clé de licence] │
│ │
│ ─────────────────────────────────────────── │
│ │
│ Acheter Simpl'Résultat Base — 39$ CAD │
│ → Ouvre le navigateur vers la page d'achat │
│ │
│ Découvrir Premium — 9$/mois │
│ → Ouvre le navigateur vers la page Premium │
└─────────────────────────────────────────────┘
Paramètres > Compte Maximus (nouveau card, sous Licence)
┌─────────────────────────────────────────────┐
│ 👤 Compte Maximus Optionnel │
│ │
│ Non connecté │
│ │
│ [Se connecter] │
│ │
│ Le compte est requis uniquement pour les │
│ fonctionnalités Premium (version web, sync). │
└─────────────────────────────────────────────┘
État connecté + Premium actif :
┌─────────────────────────────────────────────┐
│ 🔑 Licence │
│ │
│ Édition : Premium ✓ │
│ Abonnement actif jusqu'au 2026-05-08 │
│ │
│ [Gérer mon abonnement] │
│ → Ouvre Stripe Customer Portal │
├─────────────────────────────────────────────┤
│ 👤 Compte Maximus │
│ │
│ Connecté : max@example.com │
│ Dernière vérification : il y a 2 heures │
│ │
│ [Se déconnecter] │
└─────────────────────────────────────────────┘
Données
Fichiers locaux (app data directory)
simpl-resultat/
├── profiles.json # Existant
├── license.key # NOUVEAU — clé de licence Base (JWT signé)
├── auth/ # NOUVEAU
│ ├── tokens.enc # Access + refresh tokens chiffrés (AES-256-GCM, clé dérivée du machine ID)
│ └── account.json # Métadonnées du compte (email, edition, subscription_status)
└── profile_*.db # Existant — bases SQLite par profil
Aucune modification au schéma SQLite existant. La licence et l'auth sont stockées hors des bases de profils.
API Serveur de licences (propriétaire, sur VPS)
Nouveau microservice déployé sur le VPS (Coolify), base URL : https://api.lacompagniemaximus.com/licenses
| Endpoint | Méthode | Description |
|---|---|---|
/licenses/generate |
POST | Webhook Stripe → génère une clé signée Ed25519 |
/licenses/activate |
POST | Activation (vérifie machine_limit, enregistre machine_id) |
/licenses/verify |
POST | Vérification en ligne (optionnelle, pour refresh status) |
/licenses/deactivate |
POST | Désactive une machine (libère un slot) |
/licenses/revoke |
POST | Révoque une licence (admin) |
/subscriptions/status |
GET | Statut d'abonnement (pour l'app desktop, auth JWT) |
/subscriptions/webhook |
POST | Webhook Stripe Billing (subscription events) |
🟡 SECURITE — Aucune authentification ou rate limiting décrit sur les endpoints
/licenses/activateet/licenses/verify. Un attaquant pourrait énumérer des clés valides ou brute-forcer les activations. Résolution : Requérir le JWT licence comme bearer token pour activation/verify. Ajouter rate limiting (ex: 5 req/min par IP). Utiliser des request bodies signés HMAC pour les webhooks. Ref : OWASP API4:2023 (Unrestricted Resource Consumption)
🟡 SECURITE — Le spec mentionne les idempotency keys mais ne spécifie pas la vérification de signature des webhooks Stripe (
Stripe-Signatureheader). Sans cela, n'importe qui peut forger des appels webhook pour générer des licences gratuites. Résolution : Exiger explicitement la vérification de signature Stripe webhook via le webhook signing secret dans les handlers/licenses/generateet/subscriptions/webhook. Ref : CWE-345 (Insufficient Verification of Data Authenticity)
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ DESKTOP (Tauri v2) │
│ │
│ React UI Rust Backend │
│ ┌──────────────┐ ┌───────────────────┐ │
│ │ LicenseCard │◄────────────────►│ license_commands │ │
│ │ AccountCard │ │ - validate_key() │ │
│ │ SettingsPage │ │ - read_license() │ │
│ └──────────────┘ │ - store_license() │ │
│ │ - get_edition() │ │
│ └───────────────────┘ │
│ ┌───────────────────┐ │
│ │ auth_commands │ │
│ │ - start_oauth() │ │
│ │ - handle_callback()│ │
│ │ - refresh_token() │ │
│ │ - get_account() │ │
│ │ - logout() │ │
│ └───────────────────┘ │
│ │ │
└─────────────────────────────────────────────┼────────────────────┘
│ HTTPS (optionnel)
▼
┌──────────────────────────────────────────────────────────────────┐
│ SERVEUR (VPS, propriétaire) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Logto (IdP) │ │ License API │ │ Stripe Webhooks │ │
│ │ OAuth2/OIDC │ │ Ed25519 sign │ │ payment events │ │
│ └──────┬──────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └────────────────┼─────────────────────┘ │
│ ▼ │
│ ┌──────────────┐ │
│ │ PostgreSQL │ │
│ │ licenses │ │
│ │ activations │ │
│ │ subscriptions│ │
│ └──────────────┘ │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ lacompagniemaximus.com (page d'achat) │ │
│ │ Stripe Checkout / Customer Portal │ │
│ └───────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Schéma PostgreSQL (serveur de licences)
-- Table des licences
CREATE TABLE licenses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_email TEXT NOT NULL,
edition TEXT NOT NULL DEFAULT 'base', -- 'base' | 'premium'
license_key TEXT NOT NULL UNIQUE, -- JWT signé complet
stripe_payment_id TEXT, -- Référence Stripe (payment_intent ou subscription)
machine_limit INTEGER NOT NULL DEFAULT 3,
is_revoked BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ -- NULL = perpétuel (Base), date pour Premium
);
-- Activations (machines liées à une licence)
CREATE TABLE license_activations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
license_id UUID NOT NULL REFERENCES licenses(id) ON DELETE CASCADE,
machine_id TEXT NOT NULL, -- Hash unique de la machine
machine_name TEXT, -- Nom lisible (ex: "Max-ThinkPad")
activated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
UNIQUE(license_id, machine_id)
);
-- Abonnements (lié au Compte Maximus)
CREATE TABLE subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL, -- FK vers Logto user
stripe_subscription_id TEXT NOT NULL UNIQUE,
stripe_customer_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active', -- 'active', 'past_due', 'canceled', 'expired'
current_period_end TIMESTAMPTZ NOT NULL,
cancel_at_period_end BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_licenses_email ON licenses(user_email);
CREATE INDEX idx_licenses_stripe ON licenses(stripe_payment_id);
CREATE INDEX idx_activations_license ON license_activations(license_id);
CREATE INDEX idx_activations_machine ON license_activations(machine_id);
CREATE INDEX idx_subscriptions_user ON subscriptions(user_id);
CREATE INDEX idx_subscriptions_stripe ON subscriptions(stripe_subscription_id);
Plan de travail
Phase 1 — Infrastructure de licence (desktop)
Issue 1 — Commandes Tauri pour la gestion de licence [type:feature] ✅ COMPLÉTÉE
Forgejo: #46 (fermée), PR #56 (mergée 2026-04-09)
- Ajouter les dépendances
jsonwebtoken,serde_jsonau Cargo.toml - Créer
src-tauri/src/commands/license_commands.rs(6 commandes) - Créer
src-tauri/src/commands/entitlements.rs(système d'entitlements par édition) - Embarquer la clé publique Ed25519 dans le code Rust (constante)
- Enregistrer les commandes dans
lib.rs - Tests unitaires (licence valide, expirée, signature invalide, clé corrompue)
Issue 2 — UI Licence dans les paramètres [type:feature] ✅ COMPLÉTÉE
Forgejo: #47 (fermée), PR #57 (mergée 2026-04-10)
- Créer
src/components/settings/LicenseCard.tsx— affiche l'édition, permet d'entrer une clé - Créer
src/services/licenseService.ts— wrapper des commandes Tauri license_* - Créer
src/hooks/useLicense.ts— hook useReducer pour l'état de la licence - Ajouter les clés i18n dans
fr.jsoneten.json(sectionlicense.*) - Intégrer le LicenseCard dans
SettingsPage.tsx - Feedback visuel : édition courante, statut de la clé, erreurs de validation
Issue 3 — Conditionner les mises à jour auto à la licence [type:task] ✅ COMPLÉTÉE
Forgejo: #48 (fermée), PR #58 (mergée 2026-04-10)
- Modifier
useUpdater.ts: vérifier l'entitlementauto-updateviacheck_entitlement - Si édition "free" → afficher un message "Mises à jour automatiques disponibles avec l'édition Base"
- Si édition "base" ou "premium" → flux normal de mise à jour
- Mettre à jour les traductions i18n
🟡 TECHNIQUE — L'auto-update est le seul feature gaté par la licence Base. Les utilisateurs peuvent télécharger manuellement les nouvelles releases depuis le repo Forgejo public, rendant le paywall trivialement contournable sans modification du code. Résolution : Soit gater des features additionnelles (rapports avancés, multi-profil, export chiffré), soit assumer un soft paywall et reframer en "édition supporter". Considérer que la valeur réelle est dans le support + web/Premium.
🔴 TECHNIQUE — Le projet est GPL-3.0-only (
Cargo.toml,CLAUDE.md). Gater des features derrière une clé payante dans du code GPL signifie que quiconque peut forker et retirer le check. Le spec ne traite pas cette tension fondamentale. Résolution : Accepter explicitement que le gate est un honor system (cohérent avec Open Core), ou déplacer la validation de licence vers un composant serveur. Documenter cette décision dans la section "Décisions prises".
Phase 2 — Serveur de licences + paiement
Issue 4 — API serveur de licences [type:feature] 🔜 PROCHAINE
Forgejo: #49 (ouverte, status:ready)
- Créer le projet Node.js/Express (ou Hono) pour l'API de licences
- Générer la paire de clés Ed25519 (clé privée sur le serveur uniquement)
- Implémenter les endpoints : generate, activate, verify, deactivate, revoke
- Schéma PostgreSQL (tables licenses, license_activations, subscriptions)
- Middleware auth (API key pour les webhooks Stripe, JWT Logto pour les endpoints utilisateur)
- Rate limiting
- Déployer sur Coolify (api.lacompagniemaximus.com)
- Tests d'intégration
Issue 5 — Intégration Stripe (paiement + webhooks) [type:feature]
Forgejo: #50 (ouverte, status:ready). Dépendances : Issue 4
- Créer le compte Stripe et configurer pour le Canada (TPS/TVQ via Stripe Tax)
- Configurer Stripe Checkout pour l'achat unique (édition Base)
- Configurer Stripe Billing pour l'abonnement mensuel (édition Premium)
- Configurer Stripe Customer Portal (gestion d'abonnement par l'utilisateur)
- Implémenter les webhooks Stripe :
checkout.session.completed→ générer licence Base + envoyer par emailinvoice.payment_succeeded→ renouveler/activer abonnement Premiumcustomer.subscription.deleted→ marquer abonnement expirécustomer.subscription.updated→ mettre à jour statut
- Page d'achat sur lacompagniemaximus.com (lien vers Stripe Checkout)
- Email de confirmation avec clé de licence (via Stripe email ou service dédié)
Issue 6 — Intégration Logto dans l'app desktop (Compte Maximus) [type:feature]
Forgejo: #51 (ouverte, status:ready). Dépendances : Issue 4, Logto déployé
🟡 ARCHITECTURE —
get_edition()danslicense_commandsdoit aussi vérifier le statut d'abonnement viaauth_commands, créant un couplage entre les deux modules. Ce n'est pas documenté. Résolution : Soit ajouter unget_edition()top-level dansmod.rsqui combine les deux sources, soit documenter explicitement quelicense_commands::get_edition()délègue àauth_commandspour la détection Premium.
- Ajouter
tauri-plugin-deep-linkpour gérer le callback OAuth2 (simpl-resultat://auth/callback) - Créer
src-tauri/src/commands/auth_commands.rs:start_oauth() -> Result<String, String>— génère PKCE, ouvre le navigateurhandle_auth_callback(code: String) -> Result<AccountInfo, String>— échange code → tokensrefresh_auth_token() -> Result<AccountInfo, String>— refresh le tokenget_account_info() -> Result<Option<AccountInfo>, String>— lit les infos stockéeslogout() -> Result<(), String>— supprime les tokens
- Stocker les tokens chiffrés dans
auth/tokens.enc(AES-256-GCM, clé = machine_id dérivé) - Créer
src/components/settings/AccountCard.tsx— connexion/déconnexion, infos compte - Créer
src/services/authService.ts— wrapper des commandes Tauri auth_* - Ajouter les clés i18n
- Vérification périodique du statut d'abonnement (1x/jour au lancement)
Phase 3 — Page d'achat et lancement
Issue 7 — Page d'achat sur le site web [type:feature]
Forgejo: #52 (ouverte, status:ready). Dépendances : Issue 5
- Créer la page
/simpl-resultatou/achetersur lacompagniemaximus.com - Présentation des tiers (Gratuit / Base / Premium) avec comparaison
- Boutons d'achat → Stripe Checkout (Base) ou inscription + Stripe Billing (Premium)
- FAQ (licence, remboursement, support, vie privée)
- Mentions légales (TPS/TVQ, politique de remboursement)
- i18n FR/EN
Issue 8 — Activation en ligne et machine limit [type:feature]
Forgejo: #53 (ouverte, status:ready). Dépendances : Issue 1 ✅, Issue 4
- Au premier lancement avec une clé, appeler
/licenses/activateavec le machine_id - Si machine_limit atteint → message d'erreur avec option de désactiver une autre machine
- Page de gestion des machines dans le Customer Portal ou sur le site
- Graceful degradation si le serveur est injoignable (accepter la clé offline, activer plus tard)
Ordre d'exécution
Phase 1 (desktop, offline) Phase 2 (serveur, online)
======================== ========================
Issue 1 (Commandes licence) Issue 4 (API licences)
├── Issue 2 (UI Licence) ├── Issue 5 (Stripe)
├── Issue 3 (Updater conditionnel) ├── Issue 6 (Logto desktop)
└── Issue 8 (Activation) ───────────┘
└── Issue 7 (Page d'achat)
Les Phases 1 et 2 peuvent avancer en parallèle. La Phase 1 est autonome (tout offline).
Fichiers concernés
Nouveaux fichiers
| Fichier | Raison | Statut |
|---|---|---|
src-tauri/src/commands/license_commands.rs |
Commandes Tauri : validation, stockage, lecture de licence | ✅ #46 |
src-tauri/src/commands/entitlements.rs |
Système d'entitlements par édition | ✅ #46 |
src-tauri/src/commands/auth_commands.rs |
Commandes Tauri : OAuth2 PKCE, tokens, compte | 🔜 #51 |
src/components/settings/LicenseCard.tsx |
UI licence dans les paramètres | ✅ #47 |
src/components/settings/AccountCard.tsx |
UI compte Maximus dans les paramètres | 🔜 #51 |
src/services/licenseService.ts |
Service TypeScript pour les opérations de licence | ✅ #47 |
src/services/authService.ts |
Service TypeScript pour l'auth Compte Maximus | 🔜 #51 |
src/hooks/useLicense.ts |
Hook useReducer pour l'état de la licence | ✅ #47 |
src/hooks/useAuth.ts |
Hook useReducer pour l'auth Compte Maximus | 🔜 #51 |
Fichiers modifiés
| Fichier | Action | Raison |
|---|---|---|
src-tauri/Cargo.toml |
Modifier | Ajouter jsonwebtoken, serde_json ✅, tauri-plugin-deep-link (🔜 #51) |
src-tauri/src/lib.rs |
Modifier | Enregistrer les nouvelles commandes et plugins ✅ |
src-tauri/tauri.conf.json |
Modifier | Configurer deep-link protocol simpl-resultat:// |
src/pages/SettingsPage.tsx |
Modifier | Intégrer LicenseCard ✅ et AccountCard (🔜 #51) |
src/hooks/useUpdater.ts |
Modifier | Conditionner les mises à jour à la licence ✅ |
src/i18n/locales/fr.json |
Modifier | Clés license.* ✅ et account.* (🔜 #51) |
src/i18n/locales/en.json |
Modifier | Clés license.* ✅ et account.* (🔜 #51) |
Nouveau projet (serveur, hors du repo simpl-resultat)
| Projet | Description |
|---|---|
simpl-resultat-api |
API de licences + webhooks Stripe (Node.js, propriétaire) |
Critères d'acceptation
- Un utilisateur peut entrer une clé de licence valide et voir son édition passer à "Base"
- Une clé invalide ou corrompue est rejetée avec un message d'erreur clair
- Les mises à jour automatiques ne sont proposées qu'aux éditions Base et Premium
- L'édition "Gratuite" fonctionne sans restriction fonctionnelle (sauf auto-update)
- La connexion au Compte Maximus est optionnelle et ne bloque aucune fonctionnalité Base
- Un abonnement Premium actif est détecté et affiche l'édition "Premium"
- L'expiration d'un abonnement repasse gracieusement en mode Base (7 jours de grâce)
- L'achat via Stripe Checkout génère automatiquement une clé de licence envoyée par email
- Le webhook Stripe met à jour le statut d'abonnement en temps réel
- La TPS/TVQ est correctement collectée pour les ventes au Canada
- L'app fonctionne 100% offline avec une clé Base validée (aucun appel réseau requis)
- L'activation machine fonctionne en mode dégradé si le serveur est injoignable
- Toutes les chaînes UI sont traduites FR/EN
Edge cases et risques
| Cas | Mitigation |
|---|---|
| L'utilisateur change de machine | Machine limit (3 par défaut) + endpoint de désactivation. UI de gestion des machines. |
| La clé publique Ed25519 est extraite du binaire | Risque accepté (GPL). La clé publique ne permet que la vérification, pas la génération. |
| Le serveur de licences est down | La validation offline fonctionne toujours. L'activation et le refresh Premium échouent gracieusement. |
| L'utilisateur compile le code sans licence | Fonctionnel mais sans auto-update ni features Premium. Acceptable sous GPL. |
| Stripe webhook rate | Implémenter idempotency keys et retry logic. Stripe a un mécanisme de retry intégré (jusqu'à 72h). |
| Remboursement Stripe | Webhook charge.refunded → révoquer la licence automatiquement. |
| Changement de prix | Stripe permet de modifier les prix sans affecter les abonnements existants (grandfather). |
| Double achat (même email) | Vérifier si une licence active existe déjà avant d'en générer une nouvelle. |
| Token OAuth expiré en mode offline | Grace period : si le refresh échoue, garder le statut Premium pendant 7 jours (stocké localement). |
| Migration de machine_id (réinstall OS) | Le machine_id change → l'utilisateur doit désactiver l'ancienne machine ou contacter le support. Prévoir un flow self-service. |
🔴 SECURITE — Le PIN hashing existant dans
profile_commands.rsutilise SHA-256 salé (hash rapide), pas Argon2. Un PIN de 4-6 chiffres avec SHA-256 est brute-forceable trivialement (~1M combinaisons). Avec l'ajout de tokens OAuth, la surface d'attaque locale augmente. Résolution : Migrerhash_pin/verify_pinvers Argon2id (déjà une dépendance via le crateargon2utilisé dans l'export). Vulnérabilité pré-existante à corriger en Phase 1. Ref : CWE-916 (Use of Password Hash With Insufficient Computational Effort)
🟢 SECURITE — Le CSP est désactivé (
"csp": nulldanstauri.conf.json). Avec des tokens OAuth stockés dans l'app, un XSS dans le WebView pourrait exfiltrer les tokens. Résolution : Activer un CSP restrictif avant la Phase 2 :default-src 'self'; script-src 'self'; connect-src 'self' https://api.lacompagniemaximus.com. Ref : OWASP A03:2021 (Injection)
🟢 TECHNIQUE — Le spec ne spécifie pas la stratégie
get_machine_id()cross-plateforme (Windows vs Linux). Différentes approches (/etc/machine-id, WMI, SMBIOS UUID) ont des garanties de stabilité différentes. Résolution : Spécifier la stratégie par plateforme dans l'Issue 1 (ex: cratemachine-uid). Documenter que la réinstallation de l'OS peut changer l'ID.
Décisions prises
| Question | Décision | Raison |
|---|---|---|
| Modèle de prix | Hybride : achat unique desktop + abonnement web | Maximise les revenus sur les deux segments, faible friction pour le desktop |
| Modèle de licence | Open Core (GPL desktop + serveur propriétaire) | Cohérent avec l'existant, la valeur est dans le service |
| Processeur de paiement | Stripe (recommandé), avec Stripe Tax pour TPS/TVQ | Meilleure API, frais les plus bas, Stripe Billing pour abonnements |
| Validation de licence | Ed25519 offline (JWT signé) | Privacy-first, fonctionne sans internet, crypto solide |
| Auth | Logto (OAuth2 PKCE, comme prévu dans le spec web) | Déjà planifié, self-hosted, standard OIDC |
| Stockage licence | Fichier license.key dans app data (hors SQLite) |
Indépendant des profils, simple, pas de migration de schéma |
| Machine limit | 3 machines par licence Base | Standard industrie, équilibre entre flexibilité et protection |
| Grace period Premium | 7 jours après expiration | Évite de bloquer l'utilisateur pour un problème de carte |
| Square | Non retenu | Orienté commerce de détail, API d'abonnement immature, pas de MoR |
Références
| Source | Pertinence |
|---|---|
| Keyforge — License Tauri App | Pattern Ed25519 + JWT pour validation offline dans Tauri, avec stockage sécurisé |
| Keygen.sh for Tauri | Plugin Tauri existant pour licence (alternative SaaS à notre API custom) |
| tauri-plugin-better-auth-license | Device-bound licensing avec X25519 + JWE, offline-verifiable JWTs |
| Stripe vs Paddle vs LemonSqueezy Comparison | Comparaison détaillée des frais, features, et cas d'usage 2026 |
| LemonSqueezy 2026 Update (Stripe acquisition) | LemonSqueezy intégré à l'écosystème Stripe depuis 2024 |
| Stripe Tax — Canada | Documentation Stripe Tax pour la collecte TPS/TVQ automatique au Canada |
| Monetizing Open Source: Open Core Strategies | Pricing strategies pour Open Core SaaS, validation du modèle hybride |
| Open Core Business Model Handbook | Guide structuré du modèle Open Core, délimitation free vs premium |
| FastSpring — Desktop Software Sales | FastSpring comme alternative spécialisée pour la vente de logiciels desktop |
Revision — Synthese
Date: 2026-04-08 | Experts: Securite, Architecture, Technique
Verdict
🔴 CRITIQUES A CORRIGER — Le spec a une architecture solide mais présente des failles de sécurité crypto (JWT sans expiration, chiffrement tokens faible, PIN SHA-256) et des incohérences avec les patterns du codebase existant (hooks manquants).
Resume
| Expert | 🔴 | 🟡 | 🟢 | Points cles |
|---|---|---|---|---|
| Securite | 4 | 2 | 1 | JWT sans expiry irrevocable, token encryption faible (machine ID), PIN SHA-256, license.key copiable, webhook signature manquante |
| Architecture | 1 | 1 | 0 | Hooks useLicense/useAuth manquants (pattern du projet), couplage get_edition() entre modules |
| Technique | 2 | 2 | 1 | Confusion ed25519-dalek vs jsonwebtoken, tension GPL vs feature gating, auto-update seul gate fragile |
Actions requises
- 🔴 Ajouter claim
expobligatoire au JWT licence (CWE-613) - 🔴 Remplacer le chiffrement machine-ID des tokens par OS keychain ou secret par installation (CWE-321)
- ✅
Migrer PIN hashing de SHA-256 vers Argon2id— Corrigé dans #54 (PR #55, en attente de merge) - 🔴 Ajouter un token d'activation signé avec machine_id pour empêcher la copie de license.key —
store_activation_token()implémenté dans #46, activation serveur dans #53 - ✅
Clarifier que— Implémenté avecjsonwebtoken(pased25519-dalek) est la dépendance primaire pour JWTjsonwebtokendans #46 - 🔴 Documenter explicitement que le feature gating GPL est un honor system (décision Open Core)
- ✅
Ajouter—src/hooks/useLicense.tsetsrc/hooks/useAuth.tsuseLicense.tsajouté dans #47.useAuth.tsà créer dans #51 - 🟡 Ajouter vérification signature Stripe webhook (CWE-345)
- 🟡 Ajouter rate limiting + auth sur les endpoints licence (OWASP API4)
- 🟡 Documenter la config deep-link par plateforme pour OAuth2 callback
- 🟡 Clarifier le couplage
get_edition()entre license_commands et auth_commands - 🟡 Considérer des gates additionnels ou assumer le soft paywall pour l'édition Base