Simpl-Resultat/spec-monetisation.md
le king fu 4912ae39b0 docs: add WIP specs for OAuth keychain, monetisation, reports, and web
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:41:00 -04:00

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 claim exp obligatoire (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 :

  1. L'utilisateur entre sa clé dans Paramètres > Licence
  2. Le backend Rust décode le JWT et vérifie la signature Ed25519 avec la clé publique embarquée
  3. Si valide : stocke la clé dans un fichier license.key dans le répertoire app data
  4. L'app vérifie la clé au démarrage (lecture locale, aucun appel réseau)
  5. 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_limit vérifié lors de l'activation en ligne (premier lancement)

🔴 SECURITElicense.key en clair est copiable entre machines, contournant machine_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 le machine_id inclus). Vérifier à la fois le JWT licence + le token d'activation au démarrage.

🔴 TECHNIQUE — Le spec propose ed25519-dalek mais c'est jsonwebtoken (avec feature EdDSA) qui décode les JWT. ed25519-dalek seul ne gère pas les headers/claims JWT. Résolution : Clarifier dans l'Issue 1 que jsonwebtoken est la dépendance primaire pour la validation JWT. ed25519-dalek peut ne pas être nécessaire si jsonwebtoken supporte EdDSA nativement.

Compte Maximus (Édition Premium)

L'intégration Logto dans l'app desktop utilise le flow OAuth2 PKCE :

  1. L'utilisateur clique "Se connecter" dans Paramètres
  2. Tauri ouvre le navigateur système vers Logto (OAuth2 Authorization Code + PKCE)
  3. Après auth, callback vers simpl-resultat://auth/callback
  4. L'app échange le code pour un access token + refresh token
  5. 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-id sur Linux). Équivaut à chiffrer avec une clé connue. Résolution : Utiliser le stockage natif de l'OS (keyring/credential manager via le crate keyring ou tauri-plugin-store avec 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/callback nécessite une config plateforme spécifique (registre Windows, etc.) non documentée dans le spec. tauri-plugin-deep-link v2 requiert des entrées explicites dans tauri.conf.json et les manifestes plateforme. Résolution : Ajouter une sous-tâche à l'Issue 6 pour la registration deep-link par plateforme. Référencer la doc tauri-plugin-deep-link v2.

  1. L'access token JWT contient les claims : { "apps": {"simpl-resultat": "premium"}, "subscription_status": "active" }
  2. 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/activate et /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-Signature header). 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/generate et /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_json au 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.json et en.json (section license.*)
  • 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'entitlement auto-update via check_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 email
    • invoice.payment_succeeded → renouveler/activer abonnement Premium
    • customer.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é

🟡 ARCHITECTUREget_edition() dans license_commands doit aussi vérifier le statut d'abonnement via auth_commands, créant un couplage entre les deux modules. Ce n'est pas documenté. Résolution : Soit ajouter un get_edition() top-level dans mod.rs qui combine les deux sources, soit documenter explicitement que license_commands::get_edition() délègue à auth_commands pour la détection Premium.

  • Ajouter tauri-plugin-deep-link pour 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 navigateur
    • handle_auth_callback(code: String) -> Result<AccountInfo, String> — échange code → tokens
    • refresh_auth_token() -> Result<AccountInfo, String> — refresh le token
    • get_account_info() -> Result<Option<AccountInfo>, String> — lit les infos stockées
    • logout() -> 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-resultat ou /acheter sur 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/activate avec 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.rs utilise 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 : Migrer hash_pin/verify_pin vers Argon2id (déjà une dépendance via le crate argon2 utilisé 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": null dans tauri.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: crate machine-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

  1. 🔴 Ajouter claim exp obligatoire au JWT licence (CWE-613)
  2. 🔴 Remplacer le chiffrement machine-ID des tokens par OS keychain ou secret par installation (CWE-321)
  3. Migrer PIN hashing de SHA-256 vers Argon2id — Corrigé dans #54 (PR #55, en attente de merge)
  4. 🔴 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
  5. Clarifier que jsonwebtoken (pas ed25519-dalek) est la dépendance primaire pour JWT — Implémenté avec jsonwebtoken dans #46
  6. 🔴 Documenter explicitement que le feature gating GPL est un honor system (décision Open Core)
  7. Ajouter src/hooks/useLicense.ts et src/hooks/useAuth.tsuseLicense.ts ajouté dans #47. useAuth.ts à créer dans #51
  8. 🟡 Ajouter vérification signature Stripe webhook (CWE-345)
  9. 🟡 Ajouter rate limiting + auth sur les endpoints licence (OWASP API4)
  10. 🟡 Documenter la config deep-link par plateforme pour OAuth2 callback
  11. 🟡 Clarifier le couplage get_edition() entre license_commands et auth_commands
  12. 🟡 Considérer des gates additionnels ou assumer le soft paywall pour l'édition Base