# API v1 — moteur de pré-lancement Le serveur est implémenté dans `backend/`, avec SQLite persistante et administration uniquement par SSH. Le site public reste en pré-lancement : aucun token, aucun vote ouvert. Même origine, JSON UTF-8, HTTPS en production, `Cache-Control: no-store`. Retourner une erreur HTTP en cas d'échec (400, 403, 409, 429, 503), jamais un faux succès. Limite de réponse acceptée : 250 000 caractères. Dates ISO 8601 UTC. Identifiants : `[a-zA-Z0-9_-]{1,80}`. ## GET /api/v1/state ```ts type Identity = { id: string; sequence: number; // entier >= 0 name: string; // 1..80 caractères description: string; // 1..320 caractères }; type State = { version: 1; serverTime: string; // heure de génération, pas celle d'un cache ancien token: { mint: string; chain: 'solana:mainnet' | 'solana:devnet' | 'solana:testnet' }; identity: Identity | null; history: Identity[]; // max 100, plus récent d'abord, identifiants uniques round: null | { id: string; status: 'scheduled' | 'open' | 'closed'; opensAt: string; closesAt: string; // strictement après opensAt totalVotes: number; // entier sûr >= 0, égal à la somme des candidats candidates: Array<{ // 2..12, identifiants uniques id: string; name: string; // 1..80 description: string; // 1..160 votes: number; // entier sûr >= 0 }>; }; }; ``` Snapshot atomique : identité, round et totaux cohérents. Publier une identité uniquement après finalisation réussie. Une panne du collecteur ne doit pas être masquée en remplaçant ses données par zéro. L'archive est limitée aux 100 identités les plus récentes pour ce premier frontend ; la pagination sera à ajouter si nécessaire. ## POST /api/v1/vote-challenges Requête : `{ roundId, candidateId, address }`. Réponse : `{ nonce, expiresAt }`. Nonce aléatoire cryptographique, base64url sans padding, 32..128 caractères. Expiration au maximum 5 minutes, et pas après la clôture du scrutin. Conserver côté serveur le nonce et ses liens à l'origine autorisée, au réseau, au mint, au wallet, au round et au candidat. Contrôler éligibilité et état du round avant de délivrer un challenge. Ne pas se fier aux vérifications frontend. Le texte signé UTF-8 est construit par `voteMessage()` dans `src/data.js`. Le serveur reconstruit **exactement** ce texte à partir de son challenge stocké et de sa configuration. Ne jamais accepter librement le champ `message` reçu. L'origine du site doit être configurée côté serveur, pas déduite d'un Host non vérifié. ## POST /api/v1/votes En-tête `Idempotency-Key: `. Requête : `{ roundId, candidateId, address, nonce, message, signature }`. `signature` = signature Ed25519 de 64 octets encodée en base64 standard. Réponse après commit durable seulement : `{ status: 'accepted', receiptId: string, roundId: string, candidateId: string }`. Le serveur vérifie l'adresse Solana décodée (32 octets), la signature Ed25519 sur les octets exacts attendus, expiration, contexte, éligibilité et absence de rejeu. Consommer le nonce et écrire le vote dans une seule transaction atomique. Une répétition identique de la clé d'idempotence retourne le même reçu ; un contenu différent avec la même clé retourne 409. Vérifier la clôture côté serveur même si la signature a commencé avant. Les doubles votes doivent respecter la règle produit définie et être protégés par une contrainte de base de données. Aucune authentification par cookie n'est supposée : la preuve est la signature liée à cette action. Limiter taille des bodies, fréquence des challenges et tentatives de vérification ; journaliser les résultats sans secrets. Aucun endpoint ne doit accepter une adresse seule comme preuve de possession. Aucune clé privée ne transite par le serveur. Le frontend ne rejoue pas automatiquement un POST. En cas de timeout, il indique « non confirmé » : le backend peut avoir committé. Pour un produit final, prévoir une consultation de reçu par nonce/signature ou une politique de revote documentée afin de résoudre explicitement ces cas. ## État actuel et administration `GET /config.json` retourne `{apiBase:"/api/v1",chain:null,mint:null}` tant que le token n’existe pas. `GET /api/v1/state` retourne alors `token:null`, `identity:null`, `round:null`, `history:[]`, `votingEnabled:false`, `launchStatus:"prelaunch"`. `GET /api/health` vérifie la base et un heartbeat récent (<90 s). Le worker ferme les tours expirés toutes les 15 s. `GET /api/v1/history?before=` pagine l’historique (20 résultats). `GET /api/v1/receipts/` retrouve un reçu accepté. Une répétition du POST signé exact récupère aussi le même reçu après clôture ; la signature doit donc être conservée côté client pour une récupération automatique, qui reste à ajouter. ### Administration privée Depuis le projet : `bash ops/admin.sh status`, `list` ou `audit`. Les mutations prennent du JSON sur stdin : `bash ops/admin.sh candidate < candidate.json`. - `candidate`: `{name,description}`. Crée une proposition privée ; aucun effet public. - `round`: `{candidateIds,opensAt,closesAt,electorate:[{address,weight}]}`. Crée un brouillon. `closesAt` est facultatif : par défaut, clôture une heure après `opensAt`. Durée maximale : 60 minutes ; les répétitions peuvent être plus courtes. Les poids sont des entiers positifs, immuables pendant le tour. - `schedule`: `{roundId}`. Disponible uniquement en répétition isolée, après configuration devnet. - `publish`: `{roundId,candidateId,reason}`. Nécessite un tour fermé et un candidat à égalité avec le meilleur score, strictement positif. Les égalités nécessitent une décision motivée, inscrite dans l’audit. Aucun tirage ou règle de départage implicite. - `cancel`: `{roundId,reason}`. Conserve les traces ; aucun effacement de votes. - `configure`: `{mint,chain:"solana:devnet"}`. Seulement avant tout tour, dans la base de répétition. - `tick`: clôture les tours dus ; `status`, `list`, `audit`: lecture seule. Ajouter `rehearsal` après la commande utilise **une base distincte**, jamais servie par le site : `bash ops/admin.sh candidate rehearsal < candidate.json`. Ce mode utilise une liste d’éligibilité importée manuellement, **pas une preuve de détention on-chain**. La production ne permet pas d’ouvrir les votes. ### Publication et exploitation Publication de l’identité et clôture définitive dans une seule transaction. Une relance identique conserve le même résultat. Aucune transaction Solana ni mise à jour des métadonnées du token n’est envoyée. Déploiement par releases et bascule atomique avec rollback serveur si le health check échoue. La base reste dans `/var/lib/molt`, hors des releases et hors du répertoire public. Sauvegarde SQLite cohérente toutes les 15 minutes, 96 copies locales conservées. **La copie hors VPS et une restauration de secours opérationnelle restent à configurer.** Watchdog systemd indépendant toutes les deux minutes ; les échecs sont visibles dans systemd/journald et le projet est enregistré dans CONTROL. Pas de notification Telegram/email ajoutée. Dépendances du moteur : Node 24 (`node:sqlite`, `node:crypto`, `node:http`), aucune clé privée ni secret Privy requis pour cette preuve par signature. Références : [SQLite Node](https://nodejs.org/docs/latest-v24.x/api/sqlite.html), [crypto Node](https://nodejs.org/docs/latest-v24.x/api/crypto.html). ### Avant des votes publics Définir le token, le réseau, les règles d’éligibilité/poids et les égalités ; implémenter et vérifier le snapshot des holders et les exclusions de pools ; ajouter les médias d’identité et leur publication ; ajouter l’administration web/validation Privy si souhaitée. L’ingestion de tendances, la génération de branding et les changements d’urgence automatiques ne sont pas implémentés.