Ajoute un ping serveur régulier comme troisième couche d'anti-triche sur Icare et la Corne
Build and deploy / deploy (push) Successful in 36s

duration_ms/actions/merges restaient entièrement déclaratifs (calculés et envoyés par le client en une fois à la fin), donc falsifiables par un appel RPC direct. Le client envoie maintenant un ping toutes les 5s pendant la partie (ping_game_run), horodaté par le serveur, jamais par le client. submit_icarus_score/submit_melon_score corrèlent le nombre de pings et leur écart réel avec la durée annoncée (parties ≥ 15s uniquement) et signalent toute incohérence dans game_cheat_flags (missing_pings, ping_span_mismatch) — toujours en signalement silencieux, jamais de blocage, comme pour le reste de l'anti-triche.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Valentin ROBIN
2026-08-25 16:58:51 +02:00
parent 088cd92e5f
commit 00743067cc
6 changed files with 290 additions and 11 deletions
+7 -3
View File
@@ -6,7 +6,7 @@ Application web pour un groupe d'amis (~12 personnes) qui anime un jeu et des ol
## 1. État actuel
V1 à V12 sont livrées :
V1 à V13 sont livrées :
- **V1** : comptes, profils, leaderboard statique.
- **V2** : authentification par email réel (plus de pseudo+email interne), rôles `public`/`judge`, verrou de pseudo (modifiable une fois), section Admin.
- **V3** : attribution de points par les juges (RPC sécurisée, +/- avec confirmation explicite), journal public « le crieur », podium top 3, flèches de progression, recadrage/compression photo côté client.
@@ -21,6 +21,8 @@ V1 à V12 sont livrées :
- **V12** : deuxième volet de l'anti-triche des mini-jeux, ajouté par Alexandre (pas depuis cette session, appliqué directement en base puis récupéré/recommité dans `schema.sql` a posteriori — dump de schéma + `pg_policies` + `pg_get_functiondef`) et remplaçant l'approche par rejet serveur strict décrite en §3bis (`icarus_runs`/`start_icarus_run()`, désormais **supprimées**, `drop table`/`drop function` en tête de la section 11 de `schema.sql`) par un **signalement plutôt qu'un blocage** : `IcarusGame`/`MelonGame` calculent une télémétrie de partie côté client (`duration_ms`, nombre d'actions, et pour Corne le nombre de fusions — types `IcarusRunTelemetry`/`MelonRunTelemetry`) et l'envoient avec le score à `submit_icarus_score`/`submit_melon_score` (nouveau paramètre `p_run jsonb`, en plus d'une surcharge à 1 paramètre conservée pour compat qui délègue avec `p_run = null`). Le score reste **toujours accepté** (jamais de `raise exception` pour une télémétrie suspecte, contrairement à l'ancien plafond/l'ancienne vérification de temps qui restent, eux, des rejets durs) ; en parallèle, une partie est journalisée dans `public.game_cheat_flags` (`user_id, game, score, severity, trigger_code, reason, details`) dès que sa télémétrie est absente/invalide, ou incohérente avec ce que le jeu permet physiquement — pour Icare : rythme de score trop rapide (< 450 ms/point), score positif sans aucun battement d'aile, cadence de battements extrême ; pour Corne : lâchers plus rapides que `DROP_COOLDOWN_MS`, plus de fusions que de pièces lâchées ne le permettent, score dépassant le gain maximal théorique par fusion (`maxTier × 2`, la valeur du bonus de fusion finale). Nouvelle section « Détection de triche » sur `/admin` (`admin-cheat-flags.tsx`), filtrable par jeu, pour que les Archontes vérifient les signalements à la main.
- **V13** : troisième couche de l'anti-triche des mini-jeux (à la demande explicite de l'utilisateur, après qu'un premier signalement réel via V12 a confirmé que le mécanisme fonctionnait) — un ping régulier pendant la partie, en plus du signalement fondé sur la télémétrie déclarative de V12. `public.game_run_pings` (`id, user_id, game, session_token, created_at`) journalise chaque ping, horodaté par le **serveur** (jamais par le client, contrairement à `duration_ms`/`actions`/`merges` qui restent déclaratifs) ; `ping_game_run(p_game, p_session_token)` l'écrit. Côté client, `IcarusGame`/`MelonGame` retrouvent un prop `onStart` (le composant de jeu reste agnostique de Supabase, comme avant — c'est `IcareView`/`CorneView` qui gèrent le token/l'intervalle) déclenché au premier battement d'aile pour Icare, au montage et à chaque `resetGame()` pour Corne ; `IcareView`/`CorneView` y génèrent un `session_token` (`crypto.randomUUID()`), lancent un `setInterval` de 5s qui appelle `ping_game_run()` (best-effort, erreurs ignorées), l'arrêtent à la fin de la partie ou au démontage du composant, et l'incluent dans `p_run.session_token` envoyé à `submit_icarus_score`/`submit_melon_score`. Ces deux RPC corrèlent alors le nombre de pings et l'écart réel entre le premier et le dernier avec `duration_ms` annoncé (uniquement si celui-ci dépasse 15s, pour ne jamais signaler une partie courte/un échec immédiat) et ajoutent deux nouveaux `trigger_code` à `game_cheat_flags` : `missing_pings` (zéro ping reçu) et `ping_span_mismatch` (écart réel entre pings très inférieur à la durée annoncée) — toujours en signalement, jamais en blocage, même philosophie que V12. Décision produit assumée (l'utilisateur l'a explicitement refusée pour V12 aussi) : les signalements restent **volontairement invisibles** pour la personne concernée — le but est de savoir qui triche sans le prévenir, y compris si un score signalé finit par recevoir de vraies gloires automatiquement au Tribunal.
Hors périmètre pour l'instant (voir roadmap en fin de doc) : éditions/saisons, mode grand écran.
---
@@ -77,8 +79,9 @@ Le script est **idempotent** : toujours le ré-exécuter en entier après une mo
- `user_id, best_score, updated_at` — même gabarit exact que `icarus_scores` (clé primaire `user_id`, verrouillée en écriture, uniquement via `submit_melon_score`), avec sa propre contrainte `check (best_score between 0 and 2000)` — pas la même valeur qu'Icare, l'économie de score est différente (voir §3bis).
- `public.game_cheat_flags` (`id, user_id, game, score, severity, trigger_code, reason, details jsonb, created_at`) : anti-triche commun à Icare et Corne depuis V12 (voir §1) — journalise les parties suspectes plutôt que de les bloquer, affiché aux Archontes sur `/admin`. RLS `select` réservée aux juges (même pattern que `chariot_questions`), aucun grant d'écriture pour `authenticated` (comme `points_log`/`icarus_scores`) — écrit uniquement par `submit_icarus_score`/`submit_melon_score`.
- `public.game_run_pings` (`id, user_id, game, session_token, created_at`) : depuis V13 (voir §1) — un ping toutes les 5s pendant une partie, horodaté par le serveur, pour corroborer `duration_ms` (déclaratif) avec une preuve indépendante. Aucun grant du tout pour `authenticated`/`anon` (même principe que l'ancien `icarus_runs`) : seules `ping_game_run()` (insert) et `submit_icarus_score`/`submit_melon_score` (lecture + nettoyage) y touchent.
- `settings.melon_points_awarded` : marqueur d'idempotence, même principe que `icarus_points_awarded`.
- `submit_melon_score(p_score)` / `submit_melon_score(p_score, p_run)` / `award_melon_points_if_due()` : mêmes deux surcharges qu'`submit_icarus_score` (1 paramètre = compat, délègue avec `p_run = null` ; 2 paramètres = vrai point d'entrée), même borne fixe (2000, pas 500) et mêmes principes de signalement dans `game_cheat_flags` — voir V12 en §1 pour le détail des vérifications propres à Corne (cooldown de lâcher, nombre de fusions, gain max par fusion). `award_melon_points_if_due` reste une copie conforme d'`award_icarus_points_if_due` (gel à `tribunal_date`, top 3 ex-aequo inclus, `judge_id = null` dans `points_log`, tâche `pg_cron` `award-melon-points`, `execute` révoqué).
- `submit_melon_score(p_score)` / `submit_melon_score(p_score, p_run)` / `award_melon_points_if_due()` : mêmes deux surcharges qu'`submit_icarus_score` (1 paramètre = compat, délègue avec `p_run = null` ; 2 paramètres = vrai point d'entrée), même borne fixe (2000, pas 500) et mêmes principes de signalement dans `game_cheat_flags`, y compris les vérifications par ping de V13 — voir V12/V13 en §1 pour le détail des vérifications propres à Corne (cooldown de lâcher, nombre de fusions, gain max par fusion). `award_melon_points_if_due` reste une copie conforme d'`award_icarus_points_if_due` (gel à `tribunal_date`, top 3 ex-aequo inclus, `judge_id = null` dans `points_log`, tâche `pg_cron` `award-melon-points`, `execute` révoqué).
- Le pool d'avatars des pièces (Citoyens uniquement, filtré côté serveur dans `corne/page.tsx` — un palier de taille par Citoyen) et les paliers/rayons du jeu vivent côté client (`src/lib/melon/constants.ts`), pas en base. N'importe qui peut jouer et apparaître dans `melon_scores`/le classement (Archontes compris) — seul le skin des pièces est restreint aux Citoyens.
### `public.chariot_questions` / `public.chariot_slots` / `public.chariot_entries` (Le Jeu de l'Agora — Le Char)
@@ -128,7 +131,8 @@ Deuxième volet de l'audit (revue complète de tout le code applicatif, pas seul
- `reset_rank_reference()` — authenticated, vérifie `role = 'judge'`, fige le classement courant dans `previous_rank`.
- ~~`start_icarus_run()`~~ — **supprimée** (voir §3bis/V12), remplacée par `game_cheat_flags`.
- `submit_icarus_score(p_score)` — authenticated, surcharge à 1 paramètre conservée pour compat (ancien client, appel RPC direct) : délègue à la version à 2 paramètres avec `p_run = null`, systématiquement signalée comme suspecte (`missing_run_telemetry`) mais jamais bloquée.
- `submit_icarus_score(p_score, p_run)` — authenticated, seul vrai point d'entrée du client actuel (`p_run` en `jsonb`, la télémétrie de partie). Vérifie côté serveur si la date du Tribunal est déjà passée (scores figés : no-op silencieux plutôt qu'une erreur, mais après le signalement — voir ci-dessous), n'écrase le record que s'il est strictement battu, rejette un score hors bornes (0-500, défense en profondeur — la contrainte `check` sur `icarus_scores.best_score` est la garantie réelle). Signale dans `game_cheat_flags` (jamais de rejet) : télémétrie manquante/invalide, rythme de score trop rapide pour la vitesse max du jeu, score positif sans battement d'aile enregistré, ou cadence de battements extrême — voir V12 en §1.
- `ping_game_run(p_game, p_session_token)` — authenticated, appelée toutes les 5s pendant une partie (Icare ou Corne) : horodate un ping dans `game_run_pings` pour la vérification par ping de `submit_icarus_score`/`submit_melon_score` — voir V13 en §1.
- `submit_icarus_score(p_score, p_run)` — authenticated, seul vrai point d'entrée du client actuel (`p_run` en `jsonb`, la télémétrie de partie, avec `session_token` depuis V13). Vérifie côté serveur si la date du Tribunal est déjà passée (scores figés : no-op silencieux plutôt qu'une erreur, mais après le signalement — voir ci-dessous), n'écrase le record que s'il est strictement battu, rejette un score hors bornes (0-500, défense en profondeur — la contrainte `check` sur `icarus_scores.best_score` est la garantie réelle). Signale dans `game_cheat_flags` (jamais de rejet) : télémétrie manquante/invalide, rythme de score trop rapide pour la vitesse max du jeu, score positif sans battement d'aile enregistré, cadence de battements extrême, ou (V13, uniquement si `duration_ms` ≥ 15s) aucun ping serveur reçu / écart réel entre pings très inférieur à la durée annoncée — voir V12/V13 en §1.
- `award_icarus_points_if_due()`**`execute` explicitement révoqué de `public`/`anon`/`authenticated`** (voir §3bis), appelée uniquement par la tâche planifiée `pg_cron` (ou depuis le SQL Editor) : dès que la date du Tribunal est atteinte, attribue les gloires du top 3 automatiquement (`judge_id = null` dans `points_log`, affiché comme « Le Tribunal » dans Le Crieur), une seule fois (`settings.icarus_points_awarded`).
- `reroll_gardien(p_force default false)` — authenticated. Tire un nouveau Gardien parmi les Citoyens (jamais le détenteur actuel si possible) et repousse `gardien_expires_at` de 5 minutes. `p_force=true` (vérifie `role = 'judge'`) reroll immédiatement ; `p_force=false` (appelée par le minuteur côté client à expiration, et par `pg_cron` en filet) ne fait rien tant que `gardien_expires_at` n'est pas atteint — un seul `UPDATE ... WHERE` atomique (pas de `SELECT` puis `UPDATE`), pour qu'un reroll naturel avec plusieurs téléphones ouverts au même moment ne reroll qu'une seule fois.
- `submit_chariot_question(p_text)` — authenticated, rejette les juges (seuls les Citoyens proposent), valide un texte non vide (≤ 300 caractères), et applique le même gel que `submit_icarus_score` via `settings.tribunal_date` (no-op silencieux une fois l'Agora commencée). `insert ... on conflict (user_id) do update` : une ligne par personne dans `chariot_submissions`, plus une ligne append-only dans `chariot_submission_history` à chaque écriture réelle (pas lors du no-op figé).