Resserre les plafonds de score, ajoute une vérif par temps réel écoulé sur Icare, corrige le contraste de l'Urne
Build and deploy / deploy (push) Successful in 36s

5000 restait trop haut pour être un vrai plafond réaliste : Icare abaissé à 500 (calculé sur le rythme le plus rapide théoriquement atteignable dans le jeu), Corne à 2000 (estimation plus prudente, économie de score plus dure à borner). Ajoute icarus_runs/start_icarus_run() : le serveur enregistre l'instant réel de début de partie et rejette un score incohérent avec le temps écoulé, sans jamais faire confiance à une durée envoyée par le client. Corrige aussi le nombre de jetons de /urne, peu lisible en gold-bright sur fond marbre (passe à text-sea, même convention qu'Icare/Corne).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Valentin ROBIN
2026-08-24 11:28:02 +02:00
parent dc99af6bb3
commit 35704e50fc
7 changed files with 151 additions and 25 deletions
+100 -13
View File
@@ -552,10 +552,15 @@ create table if not exists public.icarus_scores (
-- score inventé directement via l'API pour se faire attribuer des gloires
-- au Tribunal — voir la contrainte identique sur melon_scores. La
-- contrainte inline de create table ne s'applique qu'à la création ; sur
-- une table déjà existante il faut explicitement la remplacer.
-- une table déjà existante il faut explicitement la remplacer. Encore
-- resserré de 5000 à 500 (toujours généreux — largement au-dessus de ce
-- qu'une partie sans faute atteint en pratique, voir le calcul détaillé
-- au niveau de submit_icarus_score) après un deuxième passage d'audit :
-- 5000 restait un ordre de grandeur trop haut pour être vraiment un
-- plafond réaliste plutôt qu'une simple borne anti-débordement.
alter table public.icarus_scores drop constraint if exists icarus_scores_best_score_check;
alter table public.icarus_scores add constraint icarus_scores_best_score_check
check (best_score between 0 and 5000);
check (best_score between 0 and 500);
alter table public.icarus_scores enable row level security;
@@ -574,10 +579,53 @@ create policy "icarus_scores readable by authenticated"
-- événement, pas de notion de jour comme pour la Course du Char).
alter table public.settings add column if not exists icarus_points_awarded boolean not null default false;
-- Deuxième couche, orthogonale à la borne fixe ci-dessus (audit de
-- sécurité) : le jeu n'a aucune vérification de gameplay (génération
-- procédurale entièrement côté client, voir V6/§3bis) — un score
-- jusqu'à 500 reste soumettable tel quel sans avoir vraiment joué. Cette
-- table fait tenir un compte du temps RÉEL écoulé (horloge du serveur,
-- jamais une durée envoyée par le client — sinon aussi falsifiable que le
-- score lui-même) entre le début d'une partie et sa soumission, pour
-- rejeter un score incohérent avec le temps réellement passé. Aucun accès
-- direct côté client (pas de grant) : seules les deux RPC ci-dessous
-- (SECURITY DEFINER) la lisent/écrivent.
create table if not exists public.icarus_runs (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references public.profiles (id) on delete cascade,
started_at timestamptz not null default now()
);
alter table public.icarus_runs enable row level security;
create or replace function public.start_icarus_run()
returns uuid
language plpgsql
security definer
set search_path = public
as $$
declare
v_id uuid;
begin
if auth.uid() is null then
raise exception 'authentication required';
end if;
insert into public.icarus_runs (user_id) values (auth.uid()) returning id into v_id;
return v_id;
end;
$$;
grant execute on function public.start_icarus_run() to authenticated;
-- Signature changée (ajout de p_run_id) : l'ancienne (integer) est
-- explicitement supprimée, sinon create or replace créerait une 2e
-- surcharge au lieu de remplacer (même remarque que post_wall_note).
drop function if exists public.submit_icarus_score(integer);
-- Seul point d'entrée pour soumettre un score. Une fois la date du Tribunal
-- atteinte, les scores sont figés : la RPC ne fait plus rien (retourne le
-- record existant sans le modifier) plutôt que d'échouer bruyamment.
create or replace function public.submit_icarus_score(p_score integer)
create or replace function public.submit_icarus_score(p_score integer, p_run_id uuid)
returns public.icarus_scores
language plpgsql
security definer
@@ -586,16 +634,30 @@ as $$
declare
v_tribunal_date timestamptz;
v_row public.icarus_scores;
v_started_at timestamptz;
v_elapsed_seconds double precision;
-- Temps minimal réel pour passer une colonne, au rythme le plus rapide
-- jamais atteignable dans le jeu : vitesse de défilement pleinement
-- montée en difficulté (FORWARD_SPEED × SPEED_MAX_MULTIPLIER) ET
-- bouclier du boost actif en permanence (× BOOST_SPEED_MULTIPLIER en
-- plus) — un cas déjà irréaliste en soi (le bouclier n'est ni continu
-- ni permanent), donc une marge de sécurité généreuse avant même le
-- ×0.9 ci-dessous. Doit rester aligné avec COLUMN_SPACING/FORWARD_SPEED/
-- SPEED_MAX_MULTIPLIER/BOOST_SPEED_MULTIPLIER (src/lib/icarus/constants.ts).
-- 210 / (130 × 1.6 × 1.7) ≈ 0.594s, encore réduit de 10% (marge contre
-- le jitter d'arrondi de la boucle de jeu) : jamais assez strict pour
-- rejeter un score légitime, seulement pour rejeter l'impossible.
c_min_seconds_per_column constant double precision := 0.53;
begin
if auth.uid() is null then
raise exception 'authentication required';
end if;
-- Plafond réaliste (5000, largement au-delà de ce qu'une vraie partie
-- peut atteindre), pas juste borné à 1 000 000 — voir la contrainte de
-- table associée (best_score_check), la vraie garantie ; cette
-- vérification donne juste un message d'erreur clair côté client.
if p_score is null or p_score < 0 or p_score > 5000 then
-- Plafond réaliste (500, largement au-delà de ce qu'une vraie partie
-- sans faute atteint), pas une simple borne anti-débordement — voir la
-- contrainte de table associée (best_score_check), la vraie garantie ;
-- cette vérification donne juste un message d'erreur clair côté client.
if p_score is null or p_score < 0 or p_score > 500 then
raise exception 'invalid score';
end if;
@@ -606,6 +668,23 @@ begin
return v_row;
end if;
if p_score > 0 then
select started_at into v_started_at
from public.icarus_runs
where id = p_run_id and user_id = auth.uid();
if v_started_at is null then
raise exception 'invalid run';
end if;
v_elapsed_seconds := extract(epoch from (now() - v_started_at));
if p_score > floor(v_elapsed_seconds / c_min_seconds_per_column) then
raise exception 'score incohérent avec le temps de jeu écoulé';
end if;
delete from public.icarus_runs where id = p_run_id;
end if;
insert into public.icarus_scores (user_id, best_score, updated_at)
values (auth.uid(), p_score, now())
on conflict (user_id) do update
@@ -618,7 +697,7 @@ begin
end;
$$;
grant execute on function public.submit_icarus_score(integer) to authenticated;
grant execute on function public.submit_icarus_score(integer, uuid) to authenticated;
do $$
begin
@@ -1509,10 +1588,18 @@ create table if not exists public.melon_scores (
);
-- Plafond abaissé (audit de sécurité) : voir la remarque identique sur
-- icarus_scores.
-- icarus_scores. Resserré une deuxième fois à 2000 (au lieu de 500 comme
-- Icare) : l'économie de score est différente ici (pas de vitesse de
-- défilement fixe à borner dans le temps — le score dépend du nombre de
-- fusions enchaînées, beaucoup plus dur à borner analytiquement sans
-- données de vraies parties) donc une estimation plus prudente plutôt
-- qu'un calcul aussi précis que pour Icare ; pas de deuxième couche par
-- temps réel écoulé pour Corne dans cette passe, contrairement à Icare
-- (icarus_runs) — à ajouter plus tard si des scores encore trop hauts sont
-- observés en pratique.
alter table public.melon_scores drop constraint if exists melon_scores_best_score_check;
alter table public.melon_scores add constraint melon_scores_best_score_check
check (best_score between 0 and 5000);
check (best_score between 0 and 2000);
alter table public.melon_scores enable row level security;
@@ -1548,11 +1635,11 @@ begin
raise exception 'authentication required';
end if;
-- Plafond réaliste (5000, largement au-delà de ce qu'une vraie partie
-- Plafond réaliste (2000, largement au-delà de ce qu'une vraie partie
-- peut atteindre), pas juste borné à 1 000 000 — voir la contrainte de
-- table associée (best_score_check), la vraie garantie ; cette
-- vérification donne juste un message d'erreur clair côté client.
if p_score is null or p_score < 0 or p_score > 5000 then
if p_score is null or p_score < 0 or p_score > 2000 then
raise exception 'invalid score';
end if;