feat: maximus-api — license server core (Ed25519, no Stripe) #49

Closed
opened 2026-04-09 01:53:52 +00:00 by maximus · 2 comments
Owner

Contexte

Serveur de licences backend. Scope réduit au core licence (sans Stripe) car le compte Stripe n'est pas encore créé. Les webhooks Stripe sont scindés dans #136.

Le code vit dans un nouveau repo Forgejo privé maximus-api (licence propriétaire, hors monorepo GPL). Conçu dès le départ comme multi-produit : servira simpl-resultat d'abord, simpl-liste et futures apps ensuite (une seule API, un seul déploiement, un seul compte Stripe à terme).

Ref : spec-monetisation.md — Phase 2, Issue 4
Dépend : rien — débloque #53 (activation en ligne, client déjà prêt dans PR #65).

Contrat client

Le client Rust (src-tauri/src/commands/license_commands.rs) impose un contrat strict :

  • Clé publique Ed25519 hardcodée (ligne 27-29) — la clé privée serveur doit matcher. Une nouvelle paire sera générée (aucune licence n'a été émise en prod, rebuild indolore).
  • Format licence : SR-BASE-<JWT> ou SR-PREMIUM-<JWT> (préfixe obligatoire, validé par strip_prefix en ligne 85-93).
  • Algo : EdDSA (Ed25519) via jsonwebtoken crate v9, validation stricte (exp + iat requis, leeway: 0).
  • Claims JWT licence : sub (email), iss ("lacompagniemaximus.com"), iat, exp, edition ("base"|"premium"), features (array), machine_limit (u32).
  • Claims JWT activation token : sub (license id/hash), iat, exp, machine_id (doit matcher le client au verify).

Tâches

Infrastructure

  • Créer le repo Forgejo privé maximus/maximus-api (licence propriétaire)
  • Scaffolding Hono + TypeScript + Zod + pnpm
  • Structure multi-produit :
    • src/core/ — middleware auth, rate limit, PG pool, JWT signer Ed25519
    • src/products/simpl-resultat/ — clé publique, features, machine_limit par défaut
    • src/routes/licenses.ts — handlers paramétrés par produit
  • Générer la paire Ed25519 ; publier la clé publique dans src-tauri/src/commands/license_commands.rs:27-29 et bumper la version desktop
  • Clé privée stockée en env var ED25519_PRIVATE_KEY_PEM sur Coolify

Base de données

  • Nouvelle instance PostgreSQL dédiée maximus-api-db sur Coolify
  • Migrations :
    • licenses — colonne product ajoutée au schéma du spec (multi-produit)
    • license_activations
    • subscriptions (table créée mais populée dans #136)

Endpoints non-Stripe

  • POST /licenses/generateadmin-only (API key). Body : {product, user_email, edition, machine_limit, expires_in_days}. Retour : SR-BASE-<JWT> ou SR-PREMIUM-<JWT>. Utile pour licences manuelles, tests, support.
  • POST /licenses/activate — Body : {license_key, machine_id, machine_name?}. Vérifie machine_limit. Retour : {activation_token: "<JWT Ed25519 avec machine_id binding>"}.
  • POST /licenses/verify — Body : {license_key}. Retour : {machines: [{machine_id, machine_name?, activated_at, last_seen_at}, ...]}.
  • POST /licenses/deactivate — Body : {license_key, machine_id}. Libère un slot.
  • POST /licenses/revokeadmin-only (API key). Body : {license_key, reason?}.
  • GET /healthz — 200 OK

Sécurité

  • Rate limiting 5 req/min par IP sur activate/verify/deactivate (OWASP API4:2023)
  • Auth : endpoints publics valident la signature du license_key JWT ; endpoints admin via API key bearer
  • Claim exp obligatoire sur tous les JWT émis (CWE-613)
  • Activation token signé avec machine_id (empêche la copie de activation.token entre machines, CWE-copy)
  • Pas d'endpoint public pour lister les licences (OWASP API1:2023)

Déploiement

  • Dockerfile + docker-compose.yml
  • Déploiement Coolify à api.lacompagniemaximus.com (Traefik TLS, Let's Encrypt)
  • Env vars : DATABASE_URL, ED25519_PRIVATE_KEY_PEM, ADMIN_API_KEY, LOGTO_JWKS_URL, NODE_ENV
  • Healthcheck Coolify sur /healthz
  • Backups PG automatiques (Coolify)

Tests

  • Unit : signature Ed25519, validation JWT, enforcement machine_limit
  • Intégration (Vitest + Testcontainers PG) :
    • Happy path activation
    • Limite machine_limit atteinte → HTTP 409 + liste des machines
    • Licence révoquée → HTTP 403
    • Rate limit déclenché → HTTP 429
    • Replay attack bloqué (activation token signé per-machine)
  • Contract test : payloads/réponses compatibles avec les structs LicenseClaims, ActivationClaims, MachineInfo du client

Fichiers concernés

Nouveau repo maximus-api/aucun fichier de ce repo modifié dans cette issue.

Après livraison, suivi dans #53 (online activation) :

  • src-tauri/src/commands/license_commands.rs : clé publique mise à jour + éventuels ajustements si le format de réponse diverge.

Critères d'acceptation

  • POST /licenses/activate avec clé valide + machine_id neuf retourne un activation_token Ed25519 validable par le client
  • La 4ème activation sur licence machine_limit=3 retourne HTTP 409 avec la liste des machines existantes
  • POST /licenses/verify retourne {machines: MachineInfo[]} au format attendu par license_commands.rs
  • Clé publique Ed25519 du serveur matche celle embarquée dans license_commands.rs:27-29
  • Rate limit 5 req/min/IP actif sur endpoints publics
  • Admin endpoints (generate, revoke) refusent sans API key
  • Déploiement Coolify vert, TLS OK sur api.lacompagniemaximus.com
  • /healthz retourne 200
  • README avec procédure : génération manuelle de licence de test
  • Schéma PG créé avec colonne product (multi-produit ready)

Suite

  • #136 — webhooks Stripe (bloqué sur compte Stripe)
  • #53 — activation en ligne côté client (débloqué par cette issue)

Complexité

Complex — nouveau repo, nouvelle DB, nouveau déploiement, crypto Ed25519, multi-produit dès le départ, sécurité critique.

## Contexte Serveur de licences backend. Scope réduit au **core licence** (sans Stripe) car le compte Stripe n'est pas encore créé. Les webhooks Stripe sont scindés dans #136. Le code vit dans un **nouveau repo Forgejo privé `maximus-api`** (licence propriétaire, hors monorepo GPL). Conçu dès le départ comme **multi-produit** : servira simpl-resultat d'abord, simpl-liste et futures apps ensuite (une seule API, un seul déploiement, un seul compte Stripe à terme). Ref : `spec-monetisation.md` — Phase 2, Issue 4 Dépend : rien — débloque #53 (activation en ligne, client déjà prêt dans PR #65). ## Contrat client Le client Rust (`src-tauri/src/commands/license_commands.rs`) impose un contrat strict : - Clé publique Ed25519 hardcodée (ligne 27-29) — la clé privée serveur doit matcher. **Une nouvelle paire sera générée** (aucune licence n'a été émise en prod, rebuild indolore). - Format licence : **`SR-BASE-<JWT>`** ou **`SR-PREMIUM-<JWT>`** (préfixe obligatoire, validé par `strip_prefix` en ligne 85-93). - Algo : EdDSA (Ed25519) via `jsonwebtoken` crate v9, validation stricte (`exp` + `iat` requis, `leeway: 0`). - Claims JWT licence : `sub` (email), `iss` ("lacompagniemaximus.com"), `iat`, `exp`, `edition` ("base"|"premium"), `features` (array), `machine_limit` (u32). - Claims JWT activation token : `sub` (license id/hash), `iat`, `exp`, `machine_id` (doit matcher le client au verify). ## Tâches ### Infrastructure - [ ] Créer le repo Forgejo privé `maximus/maximus-api` (licence propriétaire) - [ ] Scaffolding Hono + TypeScript + Zod + pnpm - [ ] Structure multi-produit : - `src/core/` — middleware auth, rate limit, PG pool, JWT signer Ed25519 - `src/products/simpl-resultat/` — clé publique, features, machine_limit par défaut - `src/routes/licenses.ts` — handlers paramétrés par produit - [ ] Générer la paire Ed25519 ; publier la clé publique dans `src-tauri/src/commands/license_commands.rs:27-29` et bumper la version desktop - [ ] Clé privée stockée en env var `ED25519_PRIVATE_KEY_PEM` sur Coolify ### Base de données - [ ] Nouvelle instance PostgreSQL dédiée `maximus-api-db` sur Coolify - [ ] Migrations : - `licenses` — colonne `product` ajoutée au schéma du spec (multi-produit) - `license_activations` - `subscriptions` (table créée mais populée dans #136) ### Endpoints non-Stripe - [ ] `POST /licenses/generate` — **admin-only** (API key). Body : `{product, user_email, edition, machine_limit, expires_in_days}`. Retour : `SR-BASE-<JWT>` ou `SR-PREMIUM-<JWT>`. Utile pour licences manuelles, tests, support. - [ ] `POST /licenses/activate` — Body : `{license_key, machine_id, machine_name?}`. Vérifie `machine_limit`. Retour : `{activation_token: "<JWT Ed25519 avec machine_id binding>"}`. - [ ] `POST /licenses/verify` — Body : `{license_key}`. Retour : `{machines: [{machine_id, machine_name?, activated_at, last_seen_at}, ...]}`. - [ ] `POST /licenses/deactivate` — Body : `{license_key, machine_id}`. Libère un slot. - [ ] `POST /licenses/revoke` — **admin-only** (API key). Body : `{license_key, reason?}`. - [ ] `GET /healthz` — 200 OK ### Sécurité - [ ] Rate limiting 5 req/min par IP sur activate/verify/deactivate (OWASP API4:2023) - [ ] Auth : endpoints publics valident la signature du `license_key` JWT ; endpoints admin via API key bearer - [ ] Claim `exp` obligatoire sur tous les JWT émis (CWE-613) - [ ] Activation token signé avec `machine_id` (empêche la copie de `activation.token` entre machines, CWE-copy) - [ ] Pas d'endpoint public pour lister les licences (OWASP API1:2023) ### Déploiement - [ ] Dockerfile + docker-compose.yml - [ ] Déploiement Coolify à `api.lacompagniemaximus.com` (Traefik TLS, Let's Encrypt) - [ ] Env vars : `DATABASE_URL`, `ED25519_PRIVATE_KEY_PEM`, `ADMIN_API_KEY`, `LOGTO_JWKS_URL`, `NODE_ENV` - [ ] Healthcheck Coolify sur `/healthz` - [ ] Backups PG automatiques (Coolify) ### Tests - [ ] Unit : signature Ed25519, validation JWT, enforcement `machine_limit` - [ ] Intégration (Vitest + Testcontainers PG) : - Happy path activation - Limite `machine_limit` atteinte → HTTP 409 + liste des machines - Licence révoquée → HTTP 403 - Rate limit déclenché → HTTP 429 - Replay attack bloqué (activation token signé per-machine) - [ ] Contract test : payloads/réponses compatibles avec les structs `LicenseClaims`, `ActivationClaims`, `MachineInfo` du client ## Fichiers concernés Nouveau repo `maximus-api/` — **aucun fichier de ce repo modifié dans cette issue**. Après livraison, suivi dans #53 (online activation) : - `src-tauri/src/commands/license_commands.rs` : clé publique mise à jour + éventuels ajustements si le format de réponse diverge. ## Critères d'acceptation - [ ] `POST /licenses/activate` avec clé valide + `machine_id` neuf retourne un `activation_token` Ed25519 validable par le client - [ ] La 4ème activation sur licence `machine_limit=3` retourne HTTP 409 avec la liste des machines existantes - [ ] `POST /licenses/verify` retourne `{machines: MachineInfo[]}` au format attendu par `license_commands.rs` - [ ] Clé publique Ed25519 du serveur matche celle embarquée dans `license_commands.rs:27-29` - [ ] Rate limit 5 req/min/IP actif sur endpoints publics - [ ] Admin endpoints (generate, revoke) refusent sans API key - [ ] Déploiement Coolify vert, TLS OK sur `api.lacompagniemaximus.com` - [ ] `/healthz` retourne 200 - [ ] README avec procédure : génération manuelle de licence de test - [ ] Schéma PG créé avec colonne `product` (multi-produit ready) ## Suite - #136 — webhooks Stripe (bloqué sur compte Stripe) - #53 — activation en ligne côté client (débloqué par cette issue) ## Complexité **Complex** — nouveau repo, nouvelle DB, nouveau déploiement, crypto Ed25519, multi-produit dès le départ, sécurité critique.
maximus added this to the spec-monetisation milestone 2026-04-09 01:53:52 +00:00
maximus added the
status:ready
type:feature
source:human
labels 2026-04-09 01:53:52 +00:00
maximus changed title from feat: license server API (Node.js, proprietary) to feat: maximus-api — license server core (Ed25519, no Stripe) 2026-04-24 01:00:10 +00:00
Author
Owner

Progression

Scaffold initial livré — voir https://git.lacompagniemaximus.com/maximus/maximus-api

Commits initiaux (main) :

  • e8ac739 scaffold Hono + Drizzle + PostgreSQL
  • fe46b80 crypto + route tests (10 passing)
  • 2e3d076 /licenses/verify retourne machines[] (match client)
  • d718926 rename maximus-api + refactor multi-produit
  • c6a6abd README + script gen-ed25519.sh

Ce qui est fait

  • Repo privé maximus/maximus-api créé et pushé
  • Stack : Hono + TypeScript + Drizzle + PostgreSQL + jose (Ed25519) + Zod + Vitest
  • Schema Drizzle avec colonne product (multi-produit dès le départ)
  • Structure src/products/simpl-resultat/config.ts (features, limits, prefixes)
  • Tous les endpoints /licenses/* filtrent par (product, license_key)
  • Endpoints : /healthz, /licenses/{generate,activate,verify,deactivate,revoke}
  • Rate limiting 5 req/min per IP (OWASP API4:2023)
  • Auth admin via bearer ADMIN_API_KEY sur generate/revoke
  • Claim product dans JWT licence et activation token
  • Format clé SR-BASE-<JWT> / SR-PREMIUM-<JWT> respecté
  • Script scripts/gen-ed25519.sh pour générer la paire
  • README avec setup, contrat API, rotation des clés, ajout d'un nouveau produit
  • Dockerfile et docker-compose pour dev local
  • 11 tests unitaires passants

Ce qui reste à faire sur #49

  • Générer la paire Ed25519 de prod (via ./scripts/gen-ed25519.sh)
  • Hardcoder la clé publique dans simpl-resultat/src-tauri/src/commands/license_commands.rs:27-29 et bump version desktop
  • Créer l'instance PostgreSQL dédiée maximus-api-db sur Coolify
  • Générer les migrations Drizzle (npm run db:generate avec DB accessible)
  • Déployer sur Coolify à api.lacompagniemaximus.com (Traefik TLS)
  • Healthcheck Coolify sur /healthz
  • Tests d'intégration avec Testcontainers PG (happy path activation, limite atteinte, révoquée, rate limit)

Notes

  • Aucun fichier de simpl-resultat modifié dans ce scaffold (la clé publique sera mise à jour dans une PR séparée au moment du bump desktop)
  • À l'acceptation finale, cette issue pourra se clore et #53 (activation en ligne côté client) devient débloquée
## Progression **Scaffold initial livré** — voir https://git.lacompagniemaximus.com/maximus/maximus-api Commits initiaux (main) : - `e8ac739` scaffold Hono + Drizzle + PostgreSQL - `fe46b80` crypto + route tests (10 passing) - `2e3d076` /licenses/verify retourne `machines[]` (match client) - `d718926` rename maximus-api + refactor multi-produit - `c6a6abd` README + script gen-ed25519.sh ### Ce qui est fait - Repo privé `maximus/maximus-api` créé et pushé - Stack : Hono + TypeScript + Drizzle + PostgreSQL + jose (Ed25519) + Zod + Vitest - Schema Drizzle avec colonne `product` (multi-produit dès le départ) - Structure `src/products/simpl-resultat/config.ts` (features, limits, prefixes) - Tous les endpoints `/licenses/*` filtrent par `(product, license_key)` - Endpoints : `/healthz`, `/licenses/{generate,activate,verify,deactivate,revoke}` - Rate limiting 5 req/min per IP (OWASP API4:2023) - Auth admin via bearer `ADMIN_API_KEY` sur generate/revoke - Claim `product` dans JWT licence et activation token - Format clé `SR-BASE-<JWT>` / `SR-PREMIUM-<JWT>` respecté - Script `scripts/gen-ed25519.sh` pour générer la paire - README avec setup, contrat API, rotation des clés, ajout d'un nouveau produit - Dockerfile et docker-compose pour dev local - 11 tests unitaires passants ### Ce qui reste à faire sur #49 - [ ] Générer la paire Ed25519 de prod (via `./scripts/gen-ed25519.sh`) - [ ] Hardcoder la clé publique dans `simpl-resultat/src-tauri/src/commands/license_commands.rs:27-29` et bump version desktop - [ ] Créer l'instance PostgreSQL dédiée `maximus-api-db` sur Coolify - [ ] Générer les migrations Drizzle (`npm run db:generate` avec DB accessible) - [ ] Déployer sur Coolify à `api.lacompagniemaximus.com` (Traefik TLS) - [ ] Healthcheck Coolify sur `/healthz` - [ ] Tests d'intégration avec Testcontainers PG (happy path activation, limite atteinte, révoquée, rate limit) ### Notes - Aucun fichier de `simpl-resultat` modifié dans ce scaffold (la clé publique sera mise à jour dans une PR séparée au moment du bump desktop) - À l'acceptation finale, cette issue pourra se clore et #53 (activation en ligne côté client) devient débloquée
maximus added
status:in-progress
and removed
status:ready
labels 2026-04-24 01:43:49 +00:00
Author
Owner

Déployé en prod 2026-04-25

maximus-api est UP à https://api.lacompagniemaximus.com.

Smoke test complet (vert)

Test Résultat
GET /healthz 200 {"status":"ok"}
POST /licenses/generate (admin auth) 201, clé SR-BASE-<JWT> Ed25519
POST /licenses/verify 200, format {valid, machines:[]} conforme au contrat client
POST /licenses/activate 200, activation_token Ed25519 avec machine_id binding
Rejet product:"unknown-app" 400 avec message Zod explicite
Rate limiting 5 req/min/IP actif (cf. rateLimit.ts)

Infra

  • PG : pas une nouvelle instance Coolify dédiée mais un rôle maximus_api + DB maximus_api sur l'instance PG partagée existante (pattern simpliste). Économise les ressources VPS, isolation correcte via rôle dédié. Documenté dans la-compagnie-maximus/docs/postgres-ops.md.
  • Coolify : project maximus-api, app uuid vscosooog08ogwwc8s48scs8. Build Dockerfile, deploy via SSH key RSA-4096, healthcheck sur /healthz.
  • Migration Drizzle : appliquée, 3 tables (licenses avec colonne product multi-produit, license_activations, subscriptions).

Gotchas rencontrés (tous documentés dans coolify-ops.md)

  1. PermitRootLogin no du sshd hardening bloquait Coolify → prohibit-password
  2. id.root@<server_uuid> vs ssh_key@<key_uuid> mismatch → symlink
  3. Forgejo SSH sur port 22222, parser convertGitUrl cassé sur ssh:// → format scp-like avec port inline
  4. Coolify écrit deploy key dans id_rsa même si ed25519 → utiliser RSA-4096
  5. PEM avec vrais newlines casse l'injection ARG du Dockerfile → \n échappés
  6. NODE_ENV=production au buildtime fait skipper devDeps → tsc not found → runtime-only

Reste à faire (post-#49)

  • Merger #137 (rotation clé publique embarquée) + /release desktop. Tant que pas fait, les clés émises par ce serveur ne peuvent pas être validées par le client en prod.
  • #53 (activation client) sera débloquée par le merge ci-dessus.
  • #136 (Stripe webhooks) reste bloquée tant que compte Stripe pas créé.

Cette issue peut être close.

## Déployé en prod 2026-04-25 `maximus-api` est UP à `https://api.lacompagniemaximus.com`. ### Smoke test complet (vert) | Test | Résultat | |---|---| | `GET /healthz` | 200 `{"status":"ok"}` | | `POST /licenses/generate` (admin auth) | 201, clé `SR-BASE-<JWT>` Ed25519 | | `POST /licenses/verify` | 200, format `{valid, machines:[]}` conforme au contrat client | | `POST /licenses/activate` | 200, `activation_token` Ed25519 avec `machine_id` binding | | Rejet `product:"unknown-app"` | 400 avec message Zod explicite | | Rate limiting 5 req/min/IP | actif (cf. `rateLimit.ts`) | ### Infra - **PG** : pas une nouvelle instance Coolify dédiée mais un rôle `maximus_api` + DB `maximus_api` sur l'instance PG partagée existante (pattern `simpliste`). Économise les ressources VPS, isolation correcte via rôle dédié. Documenté dans `la-compagnie-maximus/docs/postgres-ops.md`. - **Coolify** : project `maximus-api`, app uuid `vscosooog08ogwwc8s48scs8`. Build Dockerfile, deploy via SSH key RSA-4096, healthcheck sur `/healthz`. - **Migration Drizzle** : appliquée, 3 tables (`licenses` avec colonne `product` multi-produit, `license_activations`, `subscriptions`). ### Gotchas rencontrés (tous documentés dans `coolify-ops.md`) 1. `PermitRootLogin no` du sshd hardening bloquait Coolify → `prohibit-password` 2. `id.root@<server_uuid>` vs `ssh_key@<key_uuid>` mismatch → symlink 3. Forgejo SSH sur port 22222, parser `convertGitUrl` cassé sur `ssh://` → format scp-like avec port inline 4. Coolify écrit deploy key dans `id_rsa` même si ed25519 → utiliser RSA-4096 5. PEM avec vrais newlines casse l'injection `ARG` du Dockerfile → `\n` échappés 6. `NODE_ENV=production` au buildtime fait skipper devDeps → `tsc not found` → runtime-only ### Reste à faire (post-#49) - [ ] Merger #137 (rotation clé publique embarquée) + `/release` desktop. Tant que pas fait, les clés émises par ce serveur ne peuvent pas être validées par le client en prod. - [ ] #53 (activation client) sera débloquée par le merge ci-dessus. - [ ] #136 (Stripe webhooks) reste bloquée tant que compte Stripe pas créé. Cette issue peut être close.
maximus added
status:approved
and removed
status:in-progress
labels 2026-04-26 13:22:22 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#49
No description provided.