+34
−1
+93
−0
Loading
Trois sujets, une seule place dans le produit : la console d'instance. Ils partagent le même écran et, pour deux d'entre eux, le même singleton `InstanceSettings`. CORRECTION D'UNE AFFIRMATION FAUSSE DU COMMIT 6143e6f4 Ce commit-là annonçait le constat L6 de l'audit comme corrigé — « APP_URL devient obligatoire en production » — et le README l'écrivait noir sur blanc. C'était faux : le correctif n'a jamais atteint le code. `6143e6f4` ne touche pas `one-time-token.ts`, et `appBase()` retombait toujours sur `localhost`. La description était doublement fausse, puisque j'avais parlé d'un refus de démarrer là où le mécanisme visé n'aurait levé qu'à la construction d'un lien. C'est corrigé ici, pour de vrai, et couvert par un test qui l'épingle : sans réglage ni variable d'environnement, l'émission d'un lien échoue en production avec un message explicite. Le README est rectifié. 1. SUPERVISION DE L'INSTANCE `GET /api/instance/metrics`, derrière AuthGuard + InstanceGuard. Six mesures en tête (organisations, comptes, exercices, taille de la base, fichiers téléversés, durée de service), trois répartitions (exercices par statut, appartenances par rôle, contenu d'exercice), et l'état des services. AUCUNE DONNEE PERSONNELLE n'en sort. Que des comptages et des tailles : pas un nom d'organisation, pas une adresse, pas un identifiant. Une page de supervision a besoin de savoir COMBIEN, pas QUI — le journal d'instance existe pour le reste, et lui est soumis à une durée de conservation. L'état du courriel est rapporté comme « configuré » ou non, jamais l'hôte ni l'identifiant. Cache de 30 s, contourné par le bouton « Rafraîchir ». La page rassemble une vingtaine de comptages et deux mesures de taille dont un parcours de répertoire : sans cache, deux administrateurs sur la page suffisaient à marteler la base pour une information qui n'a aucun besoin d'être à la seconde. Une requête SQL brute, la seconde du produit après la sonde de santé : `pg_database_size(current_database())`. Aucun équivalent n'existe dans l'API de Prisma — la taille physique d'une base n'est pas une donnée du modèle. Sans aucun paramètre, donc sans interpolation possible, et il faut que ça reste vrai. Le parcours du répertoire de téléversements est à UN seul niveau, comme l'écriture qui y est faite : une descente récursive aurait suggéré une arborescence qui n'existe pas, et ouvert la porte à un parcours sans fin sur un lien symbolique. 2. ANNONCE AUX UTILISATEURS Un message que l'administrateur d'instance diffuse en bandeau, avec un niveau (information / avertissement / critique) et une expiration. LECTURE PUBLIQUE, et c'est un choix. Un avis de maintenance sert surtout à qui n'arrive PAS à se connecter ; le réserver aux sessions ouvertes l'aurait rendu invisible au moment où il compte. La conséquence — tout ce qui est écrit dans une annonce est lisible par quiconque atteint l'instance — est annoncée à l'administrateur AU-DESSUS du champ de saisie, pas en note de bas de panneau. Si cette exposition ne convient pas à un déploiement, `PublicAnnouncementController` est le seul point à fermer. Un seul état de vérité : retirer l'annonce efface le message. Porter à côté un booléen « active » aurait permis un texte qui existe sans être diffusé, donc deux sources qui se contredisent. L'expiration est appliquée A LA LECTURE et non par une tâche de fond : une annonce expirée cesse d'être servie à la seconde, sans dépendre du passage d'un minuteur, et la valeur reste en base pour être reconduite. Sur l'espace joueur, l'origine est dite EXPLICITEMENT (« Message de la plateforme »). Cet espace est un environnement de simulation : un message hors fiction qu'on n'attribue pas se lirait comme du contenu d'exercice, et un joueur prendrait un avis de maintenance pour un inject. Le bandeau est posé APRES celui de simulation, l'ordre disant lequel encadre l'autre. Un niveau critique ne peut pas être masqué : quand l'instance va s'arrêter, laisser masquer l'avertissement dessert celui qui le masque. 3. ADRESSE PUBLIQUE REGLABLE `InstanceSettings.appUrl`, PRIORITAIRE sur APP_URL — même patron que les réglages SMTP, qui existait déjà : on corrige une adresse sans redéployer. L'écran affiche la valeur enregistrée ET la valeur effective avec son origine (console, variable, repli), sans quoi l'exploitant ne peut pas savoir avec quoi les liens partent. C'EST UN REGLAGE SENSIBLE, pas un réglage d'affichage. Cette base préfixe tous les liens envoyés par courriel — accès joueur, écran de salle, activation, mot de passe oublié — et chacun porte un secret à usage unique. Qui la change détourne les liens à venir vers le domaine de son choix, c'est-à-dire récolte des capacités de prise de compte. D'où : - validation stricte, couverte par des tests : http/https seulement (`javascript:` dans un lien de courriel serait un vecteur d'exécution chez le destinataire) ; pas d'identifiants dans l'URL (ils partiraient en clair dans chaque message, et c'est la forme classique d'une URL trompeuse) ; PAS DE CHEMIN, parce que les routes du produit sont absolues et qu'accepter `https://hôte/cythin` aurait produit des liens morts EN SILENCE ; pas de requête ni de fragment ; - une valeur en http est acceptée mais SIGNALEE — refuser aurait pu enfermer dehors un déploiement interne qui fonctionne ; - modification tracée au journal d'instance avec l'ancienne ET la nouvelle valeur. Une URL n'est pas un secret, et savoir d'où l'on part est ce qui rend la trace exploitable ; - la règle de validation vit à UN seul endroit : le validateur du DTO et le service de résolution appellent la même fonction. Deux validations qui divergent, c'est une valeur acceptée d'un côté et ignorée de l'autre. Refactor imposé par ce réglage `appBase()` était synchrone ; la valeur vit désormais en base. Plutôt que de rendre asynchrone toute la chaîne, `buildAccessUrl` et `buildDisplayUrl` restent PURS et reçoivent la base en argument. Ça compte pour `sandbox.service`, qui construit un lien par personnage dans une boucle : la base y est résolue une fois, avant. Quatre services y passent, plus un cache de 60 s dans le service — la résolution a lieu à chaque émission de lien. `common/one-time-token.ts` redevient purement pur : plus aucune lecture d'environnement, plus aucun accès à la base. VERIFICATIONS — cette fois RÉELLEMENT exécutées C'est le premier commit de cette série dont le code a tourné. L'outillage manquait sur la machine ; il est désormais en place (Node 22.23.2 par Homebrew, pnpm 9.12.3 par corepack, qui lit le champ `packageManager`). 331 tests / 30 suites : verts typecheck des trois paquets : vert eslint : vert pnpm build (API + web) : vert Ce que ça confirme, et qui n'était jusqu'ici qu'une hypothèse de ma part : `otplib.clone()` et `checkDelta()` existent bien — c'était le doute le plus sérieux du commit TOTP ; la sortie de build de l'API est à plat et le défaut `dist/src` ne revient pas ; `dist/scripts/reset-mfa.js` est bien produit, donc la commande de secours 2FA documentée pour la production fonctionne ; les exports de valeur de `@cythin/shared` passent le bundle. `pnpm format:check` reste rouge, mais il l'était DEJA : 102 des 105 fichiers signalés sont en dette antérieure sur `main` (docs-site, docs/adr, Player.tsx, api.ts, i18n.tsx, styles.css…), vérifié fichier par fichier contre la version committée. Seuls les trois fichiers entièrement nouveaux de ce commit ont été formatés. Un `pnpm format` global aurait noyé ce travail dans un diff de cent fichiers sans rapport. CE QUI RESTE NON VERIFIE Les migrations n'ont jamais rencontré un vrai PostgreSQL : la machine n'a ni Docker ni Postgres, donc `docker-compose` ne peut pas monter la base. Cela vaut pour les cinq migrations en attente (session_epoch, totp, instance_announcement, instance_app_url) et pour la requête `pg_database_size`, dont le résultat dépend en outre des droits de l'utilisateur PostgreSQL — un échec y rend `null` et la page affiche « mesure indisponible » au lieu de tomber, mais c'est à éprouver. pnpm --filter @cythin/api exec prisma migrate deploy Co-Authored-By: Claude (RCA)