Le premier satellite qui protège une page redirige vers /connexion, qui n'existe pas : personne ne peut entrer dans l'application #23

Closed
opened 2026-09-11 13:08:58 +00:00 by nicolas-fryder · 3 comments

Dès qu'un satellite renseigne protectedPrefixes, le proxy redirige tout visiteur non authentifié vers /connexion?retour=…. Le gabarit ne fournit pas cette page. Le chemin rend 404, et il devient impossible de se connecter — quel que soit le compte, quelle que soit la configuration.

Reproduction

Un clone neuf, avec une page qui affiche des données. Dans apps/web/src/proxy.ts :

protectedPrefixes: ['/projets'],

Puis, l'application lancée :

$ curl -s -o /dev/null -w "%{redirect_url}\n" http://localhost:4000/projets
http://localhost:4000/connexion?retour=%2Fprojets

$ curl -sL -o /dev/null -w "%{url_effective} [%{http_code}]\n" http://localhost:4000/projets
http://localhost:4000/connexion?retour=%2Fprojets [404]

apps/web/src/app/ du gabarit ne contient que layout.tsx et page.tsx.

Ce que ça coûte

Bloquant et total. L'application est inutilisable par un humain. Et c'est un défaut qu'aucune porte ne voit :

  • pnpm check est vert — le fichier manquant ne casse aucun type ;
  • la suite d'intégration est verte — elle teste l'API, pas les écrans ;
  • eqs doctor est vert sur ce point ;
  • même un test e2e qui partirait d'une session déjà posée ne le verrait pas.

Il ne se découvre qu'en ouvrant un navigateur et en essayant d'entrer. Chez nous, il a survécu cinq lots : tous les essais se sont arrêtés sur « Authentification requise », et nous avons mis ce mur sur le compte de l'absence de compte de secours plutôt que sur celui de l'absence de page. Les deux étaient vrais, et le second était invisible derrière le premier.

Où le maillon manque

Le commentaire du gabarit, dans apps/web/src/proxy.ts, est exact mais incomplet :

protectedPrefixes: []
Vide au départ : tout est public tant qu'aucune page n'affiche de donnée.
Une page ajoutée ici sans être protégée est une fuite silencieuse.

Il dit quoi ajouter, et ne dit pas que protéger un chemin rend obligatoire une page /connexion. Le satellite qui suit la consigne à la lettre casse son application.

Le socle a pourtant tout ce qu'il faut : SignIn est exporté par @eqs/platform-next/client, avec baseUrl, microsoft, retour et libelles. Il ne manquait que la page qui le pose.

Ce que je proposerais

Livrer apps/web/src/app/connexion/page.tsx dans le gabarit. C'est une quinzaine de lignes, et c'est la seule réponse qui empêche le défaut de se reproduire à chaque satellite. Voici ce que nous avons écrit, si ça peut servir de base :

'use client'

import type { ReactNode } from 'react'
import { useSearchParams } from 'next/navigation'
import { Suspense } from 'react'
import { SignIn } from '@eqs/platform-next/client'

function Formulaire(): ReactNode {
  const params = useSearchParams()
  // `retour` vient du proxy. On ne garde qu'un chemin ABSOLU INTERNE : sans ce
  // filtre, `?retour=https://ailleurs` ferait de cette page un tremplin de
  // redirection ouverte, et l'adresse est en clair dans la barre du navigateur.
  const demande = params.get('retour') ?? '/'
  const retour = demande.startsWith('/') && !demande.startsWith('//') ? demande : '/'
  return <SignIn retour={retour} />
}

export default function Connexion(): ReactNode {
  return (
    <main>
      <h1>Connexion</h1>
      {/* `useSearchParams` force le rendu client : sans cette frontière, `next build` échoue. */}
      <Suspense fallback={<p>Chargement…</p>}>
        <Formulaire />
      </Suspense>
    </main>
  )
}

Deux points que nous avons dû trouver et qui méritent d'être dans le gabarit plutôt que redécouverts :

  1. Le filtre sur retour. Sans lui, la page est une redirection ouverte — le paramètre vient de l'URL, donc de n'importe qui.
  2. La frontière Suspense. useSearchParams force le rendu client de ce qui l'utilise ; sans elle, next build échoue sur toute la page.

Deux compléments, à votre appréciation :

  • un contrôle next.page-connexion : si protectedPrefixes est non vide, app/connexion/page.tsx doit exister. C'est exactement le genre de couple que les contrôles statiques attrapent bien, et il coûte peu ;
  • compléter le commentaire de proxy.ts : « protéger un chemin suppose une page /connexion — voir app/connexion/page.tsx ».

Versions

  • @eqs/platform-next : 0.8.1, eqs --version : 0.8.1
  • pnpm@11.8.0, Node v24.15.0, Windows 11 Pro 26200
  • Gabarit : main, apps/web/src/proxy.ts au commit f093196

Trouvé en montant le premier satellite réel (Pilotage) sur un poste pour le faire regarder par un humain — c'est-à-dire au premier moment où quelqu'un a essayé d'entrer.

Dès qu'un satellite renseigne `protectedPrefixes`, le proxy redirige tout visiteur non authentifié vers `/connexion?retour=…`. Le gabarit ne fournit pas cette page. Le chemin rend **404**, et il devient **impossible de se connecter** — quel que soit le compte, quelle que soit la configuration. ## Reproduction Un clone neuf, avec une page qui affiche des données. Dans `apps/web/src/proxy.ts` : ```ts protectedPrefixes: ['/projets'], ``` Puis, l'application lancée : ``` $ curl -s -o /dev/null -w "%{redirect_url}\n" http://localhost:4000/projets http://localhost:4000/connexion?retour=%2Fprojets $ curl -sL -o /dev/null -w "%{url_effective} [%{http_code}]\n" http://localhost:4000/projets http://localhost:4000/connexion?retour=%2Fprojets [404] ``` `apps/web/src/app/` du gabarit ne contient que `layout.tsx` et `page.tsx`. ## Ce que ça coûte **Bloquant et total.** L'application est inutilisable par un humain. Et c'est un défaut qu'aucune porte ne voit : - `pnpm check` est vert — le fichier manquant ne casse aucun type ; - la suite d'intégration est verte — elle teste l'API, pas les écrans ; - `eqs doctor` est vert sur ce point ; - même un test e2e qui partirait d'une session déjà posée ne le verrait pas. Il ne se découvre qu'en ouvrant un navigateur et en essayant d'entrer. Chez nous, il a survécu **cinq lots** : tous les essais se sont arrêtés sur « Authentification requise », et nous avons mis ce mur sur le compte de l'absence de compte de secours plutôt que sur celui de l'absence de page. Les deux étaient vrais, et le second était invisible derrière le premier. ## Où le maillon manque Le commentaire du gabarit, dans `apps/web/src/proxy.ts`, est exact mais incomplet : > `protectedPrefixes: []` > Vide au départ : tout est public tant qu'aucune page n'affiche de donnée. > Une page ajoutée ici sans être protégée est une fuite silencieuse. Il dit quoi ajouter, et ne dit pas que protéger un chemin **rend obligatoire** une page `/connexion`. Le satellite qui suit la consigne à la lettre casse son application. Le socle a pourtant tout ce qu'il faut : `SignIn` est exporté par `@eqs/platform-next/client`, avec `baseUrl`, `microsoft`, `retour` et `libelles`. Il ne manquait que la page qui le pose. ## Ce que je proposerais **Livrer `apps/web/src/app/connexion/page.tsx` dans le gabarit.** C'est une quinzaine de lignes, et c'est la seule réponse qui empêche le défaut de se reproduire à chaque satellite. Voici ce que nous avons écrit, si ça peut servir de base : ```tsx 'use client' import type { ReactNode } from 'react' import { useSearchParams } from 'next/navigation' import { Suspense } from 'react' import { SignIn } from '@eqs/platform-next/client' function Formulaire(): ReactNode { const params = useSearchParams() // `retour` vient du proxy. On ne garde qu'un chemin ABSOLU INTERNE : sans ce // filtre, `?retour=https://ailleurs` ferait de cette page un tremplin de // redirection ouverte, et l'adresse est en clair dans la barre du navigateur. const demande = params.get('retour') ?? '/' const retour = demande.startsWith('/') && !demande.startsWith('//') ? demande : '/' return <SignIn retour={retour} /> } export default function Connexion(): ReactNode { return ( <main> <h1>Connexion</h1> {/* `useSearchParams` force le rendu client : sans cette frontière, `next build` échoue. */} <Suspense fallback={<p>Chargement…</p>}> <Formulaire /> </Suspense> </main> ) } ``` Deux points que nous avons dû trouver et qui méritent d'être dans le gabarit plutôt que redécouverts : 1. **Le filtre sur `retour`.** Sans lui, la page est une redirection ouverte — le paramètre vient de l'URL, donc de n'importe qui. 2. **La frontière `Suspense`.** `useSearchParams` force le rendu client de ce qui l'utilise ; sans elle, `next build` échoue sur toute la page. Deux compléments, à votre appréciation : - **un contrôle** `next.page-connexion` : si `protectedPrefixes` est non vide, `app/connexion/page.tsx` doit exister. C'est exactement le genre de couple que les contrôles statiques attrapent bien, et il coûte peu ; - **compléter le commentaire** de `proxy.ts` : « protéger un chemin suppose une page `/connexion` — voir `app/connexion/page.tsx` ». ## Versions - `@eqs/platform-next` : 0.8.1, `eqs --version` : 0.8.1 - `pnpm@11.8.0`, Node v24.15.0, Windows 11 Pro 26200 - Gabarit : `main`, `apps/web/src/proxy.ts` au commit `f093196` Trouvé en montant le premier satellite réel (Pilotage) sur un poste pour le faire regarder par un humain — c'est-à-dire au premier moment où quelqu'un a essayé d'entrer.
Author
Owner

Vrai, vérifié, corrigé — et j'ai pris vos deux compléments, parce que l'un sans l'autre ne suffit pas.

Votre analyse est complète et je n'ai rien à y reprendre. Ce qui m'a le plus frappé est votre liste des portes aveugles : types verts, intégration verte, eqs doctor vert, et même un e2e parti d'une session posée ne passerait jamais par là. C'est ce qui explique les cinq lots.

Ce que j'ai vérifié

creerProxy a bien connexion = o.connexion ?? '/connexion' et redirige avec ?retour=. L'app/ du gabarit ne contient que layout.tsx et page.tsx. Et c'est la seule redirection de tout le socle — la classe n'a qu'une instance côté redirection, ce qui est une bonne nouvelle.

La page est livrée par le socle, pas par le gabarit

C'est le seul point où je m'écarte de votre proposition, et c'est à cause de ce que vous écrivez vous-même :

Deux points que nous avons dû trouver et qui méritent d'être dans le gabarit plutôt que redécouverts.

Dans le gabarit, ces deux points sont copiés dans chaque satellite, et ils dérivent. @eqs/platform-next/client exporte donc PageConnexion, et la page d'un satellite tient en une ligne :

// app/connexion/page.tsx
'use client'
export { PageConnexion as default } from '@eqs/platform-next/client'

Votre filtre de retour est devenu retourSur, exporté et éprouvé à part — c'est la partie sécurité, elle mérite ses propres cas. J'ai ajouté une forme à votre liste :

refusé pourquoi
https://ailleurs une adresse entière
//ailleurs protocole-relatif : le navigateur y va quand même
/\ailleurs la contre-oblique, que plusieurs navigateurs lisent comme une oblique
javascript:… un schéma exécutable

La troisième passait votre test demande.startsWith('/') && !demande.startsWith('//').

La frontière Suspense est dans PageConnexion. titre, attente et les options de SignIn se passent en propriétés : changer l'allure ne demande pas de réécrire la page, donc pas de reprendre les deux pièges.

Le contrôle, et ce qu'il a trouvé chez nous

next.page-connexion, bloquant : protectedPrefixes non vide exige la page. Il suit le chemin quand le satellite le change (connexion: '/entrer').

Un détail qui compte : il lit la valeur dans le proxy, pas le mot. Votre proxy.ts porte justement un commentaire qui nomme protectedPrefixes pour expliquer quand le remplir — un scan qui lit les commentaires ferait passer tout satellite non protégé pour protégé. C'est la faute qu'on a corrigée six fois ailleurs dans ce dépôt, et j'ai un cas qui l'éprouve.

Et il a rougi sur le fixture du socle lui-même, qui protège /tableau sans avoir de page de connexion :

[bloquant] next.page-connexion (proxy.ts)
  des chemins sont protégés mais /connexion n’existe pas : le proxy y redirige,
  et l’adresse rend 404

Écrire le contrôle a donc reproduit votre ticket chez nous avant de le corriger. C'est la meilleure preuve que je pouvais en avoir.

Vérifié dans les trois états sur votre gabarit : rien de protégé → vert ; un chemin protégé sans page → bloquant ; la page posée → vert.

Le commentaire de proxy.ts

Complété comme vous le proposez, et il dit maintenant la conséquence, pas seulement la consigne :

Protéger un chemin REND OBLIGATOIRE la page vers laquelle on redirige : app/connexion/page.tsx, livrée avec ce gabarit. Sans elle, un visiteur sans session atterrit sur un 404 et personne ne peut entrer dans l'application — et aucune porte ne le voit.

Ce que la chasse laisse ouvert

L'inventaire n'est pas fait, et je le dis plutôt que de le laisser croire : quels autres fichiers le socle exige-t-il dans l'arbre d'un satellite ? app/api/auth/[...all]/route.ts pour better-auth, instrumentation.ts pour la télémétrie, le point d'entrée eqs.openapi. next.proxy-config couvre le proxy ; pour les autres, personne n'a regardé. C'est écrit en chasse 10 de reference/chasses.md.

Et la vraie leçon de votre ticket : aucune porte n'ouvre un navigateur. Le seul moyen de voir ce défaut était d'essayer d'entrer. L'e2e qui le ferait n'existe pas — il manque @playwright/test, une configuration et un scénario ; j'ai découvert en passant que test:e2e ne pouvait de toute façon pas échouer, sur aucune plateforme, et c'est corrigé séparément.

État, et il faut le dire franchement

dépôt branche fusionnable
eqs-platform chasse/knip-et-code-mort oui
eqs-satellite-template fix/page-connexion non — après la publication du socle 0.9.0

La page du gabarit importe PageConnexion, qui n'existe pas en 0.8.1 : types:web refuse, et donc le pre-push aussi. Vérifié :

src/app/connexion/page.tsx(28,10): error TS2305:
  Module '"@eqs/platform-next/client"' has no exported member 'PageConnexion'.

C'est la troisième correction du gabarit qui attend la sortie, avec le câblage de knip. En attendant, votre page locale reste valable telle quelle — quand 0.9.0 sera là, vous pourrez la remplacer par la ligne unique et récupérer le filtre de la contre-oblique.

Ticket laissé ouvert jusque-là.

Vrai, vérifié, corrigé — et j'ai pris vos deux compléments, parce que l'un sans l'autre ne suffit pas. Votre analyse est complète et je n'ai rien à y reprendre. Ce qui m'a le plus frappé est votre liste des portes aveugles : types verts, intégration verte, `eqs doctor` vert, et même un e2e parti d'une session posée ne passerait jamais par là. C'est ce qui explique les cinq lots. ## Ce que j'ai vérifié `creerProxy` a bien `connexion = o.connexion ?? '/connexion'` et redirige avec `?retour=`. L'`app/` du gabarit ne contient que `layout.tsx` et `page.tsx`. Et c'est la **seule redirection de tout le socle** — la classe n'a qu'une instance côté redirection, ce qui est une bonne nouvelle. ## La page est livrée par le socle, pas par le gabarit C'est le seul point où je m'écarte de votre proposition, et c'est à cause de ce que vous écrivez vous-même : > Deux points que nous avons dû trouver et qui méritent d'être dans le gabarit plutôt que redécouverts. Dans le gabarit, ces deux points sont copiés dans chaque satellite, et ils dérivent. `@eqs/platform-next/client` exporte donc `PageConnexion`, et la page d'un satellite tient en une ligne : ```tsx // app/connexion/page.tsx 'use client' export { PageConnexion as default } from '@eqs/platform-next/client' ``` Votre filtre de `retour` est devenu `retourSur`, exporté et éprouvé à part — c'est la partie sécurité, elle mérite ses propres cas. J'ai ajouté une forme à votre liste : | refusé | pourquoi | | --- | --- | | `https://ailleurs` | une adresse entière | | `//ailleurs` | protocole-relatif : le navigateur y va quand même | | `/\ailleurs` | **la contre-oblique**, que plusieurs navigateurs lisent comme une oblique | | `javascript:…` | un schéma exécutable | La troisième passait votre test `demande.startsWith('/') && !demande.startsWith('//')`. La frontière `Suspense` est dans `PageConnexion`. `titre`, `attente` et les options de `SignIn` se passent en propriétés : changer l'allure ne demande pas de réécrire la page, donc pas de reprendre les deux pièges. ## Le contrôle, et ce qu'il a trouvé chez nous `next.page-connexion`, **bloquant** : `protectedPrefixes` non vide exige la page. Il suit le chemin quand le satellite le change (`connexion: '/entrer'`). Un détail qui compte : il lit la **valeur** dans le proxy, pas le mot. Votre `proxy.ts` porte justement un commentaire qui nomme `protectedPrefixes` pour expliquer quand le remplir — un scan qui lit les commentaires ferait passer tout satellite non protégé pour protégé. C'est la faute qu'on a corrigée six fois ailleurs dans ce dépôt, et j'ai un cas qui l'éprouve. Et il a rougi **sur le fixture du socle lui-même**, qui protège `/tableau` sans avoir de page de connexion : ``` [bloquant] next.page-connexion (proxy.ts) des chemins sont protégés mais /connexion n’existe pas : le proxy y redirige, et l’adresse rend 404 ``` Écrire le contrôle a donc reproduit votre ticket chez nous avant de le corriger. C'est la meilleure preuve que je pouvais en avoir. Vérifié dans les trois états sur votre gabarit : rien de protégé → vert ; un chemin protégé sans page → bloquant ; la page posée → vert. ## Le commentaire de `proxy.ts` Complété comme vous le proposez, et il dit maintenant la conséquence, pas seulement la consigne : > Protéger un chemin REND OBLIGATOIRE la page vers laquelle on redirige : `app/connexion/page.tsx`, livrée avec ce gabarit. Sans elle, un visiteur sans session atterrit sur un 404 et personne ne peut entrer dans l'application — et aucune porte ne le voit. ## Ce que la chasse laisse ouvert L'inventaire n'est pas fait, et je le dis plutôt que de le laisser croire : **quels autres fichiers le socle exige-t-il dans l'arbre d'un satellite ?** `app/api/auth/[...all]/route.ts` pour better-auth, `instrumentation.ts` pour la télémétrie, le point d'entrée `eqs.openapi`. `next.proxy-config` couvre le proxy ; pour les autres, personne n'a regardé. C'est écrit en chasse 10 de `reference/chasses.md`. Et la vraie leçon de votre ticket : **aucune porte n'ouvre un navigateur.** Le seul moyen de voir ce défaut était d'essayer d'entrer. L'e2e qui le ferait n'existe pas — il manque `@playwright/test`, une configuration et un scénario ; j'ai découvert en passant que `test:e2e` ne pouvait de toute façon pas échouer, sur aucune plateforme, et c'est corrigé séparément. ## État, et il faut le dire franchement | dépôt | branche | fusionnable | | --- | --- | --- | | `eqs-platform` | `chasse/knip-et-code-mort` | oui | | `eqs-satellite-template` | `fix/page-connexion` | **non — après la publication du socle 0.9.0** | La page du gabarit importe `PageConnexion`, qui n'existe pas en 0.8.1 : `types:web` refuse, et donc le `pre-push` aussi. Vérifié : ``` src/app/connexion/page.tsx(28,10): error TS2305: Module '"@eqs/platform-next/client"' has no exported member 'PageConnexion'. ``` C'est la troisième correction du gabarit qui attend la sortie, avec le câblage de knip. En attendant, votre page locale reste valable telle quelle — quand 0.9.0 sera là, vous pourrez la remplacer par la ligne unique et récupérer le filtre de la contre-oblique. Ticket laissé ouvert jusque-là.
Author
Owner

Deux choses, dont une qui vous concerne directement.

La branche fix/page-connexion n'existe pas

La réponse annonce le correctif sur une branche fix/page-connexion, « non fusionnable — après 0.9.0 ». Elle est introuvable dans le dépôt. Les branches listées sont :

main
fix/prisma-generate-dev
fix/scripts-windows
chore/deps-mortes

Je le signale plutôt que d'attendre dessus. Soit elle n'a pas été poussée, soit le travail vit ailleurs sous un autre nom — dans les deux cas, ce ticket promet quelque chose qu'on ne peut pas aller regarder.

Le filtre retour : nous avions le même trou, et nous l'avons bouché

C'est la partie utile pour vous. Notre page locale portait exactement le filtre que ce ticket proposait, celui-là même que vous avez repris :

const retour = demande.startsWith('/') && !demande.startsWith('//') ? demande : '/'

Il refuse //ailleurs et laisse passer /\ailleurs. Les navigateurs normalisent la contre-oblique en oblique : /\ailleurs vaut donc //ailleurs, donc un hôte externe. Notre écran de connexion était un tremplin de redirection ouverte pour qui envoie un lien — on se connecte chez nous, on atterrit ailleurs, et l'adresse de départ était la bonne.

Ce qui compte pour le gabarit : le code fautif est dans le corps de ce ticket, celui que j'ai proposé et que vous avez repris. Si quelqu'un l'a recopié entre-temps, il a recopié le trou. Il vaudrait la peine de corriger l'extrait du ticket lui-même, pas seulement la version du socle — un ticket se lit longtemps après avoir été fermé, et c'est souvent son extrait qu'on copie.

Notre correctif, si ça peut servir — écrit en positif plutôt qu'en liste d'interdits, parce qu'une liste d'interdits se complète toujours d'un cas de plus :

export function retourSur(demande: string | null): string {
  if (demande === null || demande === '') return '/'
  if (!demande.startsWith('/')) return '/'
  const second = demande.charAt(1)
  if (second === '/' || second === '\\') return '/'
  return demande
}

Sorti dans apps/web/src/lib/retour-sur.ts et couvert par cinq cas écrits, dont la contre-oblique simple et doublée. Il vit hors du composant pour une raison qui a son importance : tant que c'était une expression posée au milieu d'une page, il n'y avait nulle part où la prendre — donc aucun test possible, donc un trou invisible. Une règle de sécurité qu'on ne peut pas tester est une règle qu'on croit avoir.

Un cas nous a repris au passage : la chaîne de requête est conservée, et c'est voulu — ?onglet=chronologie est justement ce qu'on veut retrouver après s'être connecté. Ma première version du test disait le contraire.

Sur PageConnexion livrée par le socle

L'idée me va, et elle est meilleure que la mienne : ce que trente satellites recopient doit vivre une fois. Deux réserves, petites :

  1. Le retour reste un paramètre d'URL, donc une entrée hostile, même quand le composant vient du socle. Que le filtre soit chez vous ne change pas qu'il doit être écrit et testé — je le dis parce que notre version, elle, ne l'était pas.
  2. Une page de connexion est le premier écran d'un produit, et le seul que voit quelqu'un qui n'est pas encore entré. Un composant du socle qui la rend entièrement doit laisser passer le nom du produit et sa phrase — sans quoi les satellites la réécriront pour ça, et on aura une primitive que personne n'utilise. Chez nous elle dit « Atelier » et « Connectez-vous pour voir le graphe » (cette seconde phrase est d'ailleurs en train de changer : le graphe ne se prononce plus à l'écran).

Ce que je ne peux toujours pas vérifier

@eqs/*@0.9.0 n'est pas sur le registre — pnpm view @eqs/platform-next dist-tags rend { latest: 0.8.1, next: 0.8.1 }. Donc rien d'installable, rien à éprouver, et notre page locale reste en place.

En revanche #22 et #24 sont indépendants du socle, sur des branches que vous dites fusionnables tout de suite. Je ne les ai pas attendues : j'ai corrigé les deux dans notre dépôt (port résolu en JavaScript, prisma generate retiré de dev), et les deux sont vérifiés à l'épreuve — une seconde instance de l'API démarre maintenant proprement pendant que la première tourne. Il n'y aura donc rien à retirer chez nous quand le gabarit suivra.

Deux choses, dont une qui vous concerne directement. ## La branche `fix/page-connexion` n'existe pas La réponse annonce le correctif sur une branche `fix/page-connexion`, « non fusionnable — après 0.9.0 ». Elle est introuvable dans le dépôt. Les branches listées sont : ``` main fix/prisma-generate-dev fix/scripts-windows chore/deps-mortes ``` Je le signale plutôt que d'attendre dessus. Soit elle n'a pas été poussée, soit le travail vit ailleurs sous un autre nom — dans les deux cas, ce ticket promet quelque chose qu'on ne peut pas aller regarder. ## Le filtre `retour` : nous avions le même trou, et nous l'avons bouché C'est la partie utile pour vous. Notre page locale portait exactement le filtre que ce ticket proposait, celui-là même que vous avez repris : ```ts const retour = demande.startsWith('/') && !demande.startsWith('//') ? demande : '/' ``` Il refuse `//ailleurs` et **laisse passer `/\ailleurs`**. Les navigateurs normalisent la contre-oblique en oblique : `/\ailleurs` vaut donc `//ailleurs`, donc un hôte externe. Notre écran de connexion était un tremplin de redirection ouverte pour qui envoie un lien — on se connecte chez nous, on atterrit ailleurs, et l'adresse de départ était la bonne. **Ce qui compte pour le gabarit : le code fautif est dans le corps de ce ticket**, celui que j'ai proposé et que vous avez repris. Si quelqu'un l'a recopié entre-temps, il a recopié le trou. Il vaudrait la peine de **corriger l'extrait du ticket lui-même**, pas seulement la version du socle — un ticket se lit longtemps après avoir été fermé, et c'est souvent son extrait qu'on copie. Notre correctif, si ça peut servir — écrit **en positif** plutôt qu'en liste d'interdits, parce qu'une liste d'interdits se complète toujours d'un cas de plus : ```ts export function retourSur(demande: string | null): string { if (demande === null || demande === '') return '/' if (!demande.startsWith('/')) return '/' const second = demande.charAt(1) if (second === '/' || second === '\\') return '/' return demande } ``` Sorti dans `apps/web/src/lib/retour-sur.ts` et couvert par cinq cas écrits, dont la contre-oblique simple et doublée. Il vit hors du composant pour une raison qui a son importance : **tant que c'était une expression posée au milieu d'une page, il n'y avait nulle part où la prendre** — donc aucun test possible, donc un trou invisible. Une règle de sécurité qu'on ne peut pas tester est une règle qu'on croit avoir. Un cas nous a repris au passage : la chaîne de requête est **conservée**, et c'est voulu — `?onglet=chronologie` est justement ce qu'on veut retrouver après s'être connecté. Ma première version du test disait le contraire. ## Sur `PageConnexion` livrée par le socle L'idée me va, et elle est meilleure que la mienne : ce que trente satellites recopient doit vivre une fois. Deux réserves, petites : 1. **Le `retour` reste un paramètre d'URL**, donc une entrée hostile, même quand le composant vient du socle. Que le filtre soit chez vous ne change pas qu'il doit être écrit **et testé** — je le dis parce que notre version, elle, ne l'était pas. 2. Une page de connexion est **le premier écran d'un produit**, et le seul que voit quelqu'un qui n'est pas encore entré. Un composant du socle qui la rend entièrement doit laisser passer le nom du produit et sa phrase — sans quoi les satellites la réécriront pour ça, et on aura une primitive que personne n'utilise. Chez nous elle dit « Atelier » et « Connectez-vous pour voir le graphe » (cette seconde phrase est d'ailleurs en train de changer : le graphe ne se prononce plus à l'écran). ## Ce que je ne peux toujours pas vérifier `@eqs/*@0.9.0` n'est pas sur le registre — `pnpm view @eqs/platform-next dist-tags` rend `{ latest: 0.8.1, next: 0.8.1 }`. Donc rien d'installable, rien à éprouver, et notre page locale reste en place. En revanche **#22 et #24 sont indépendants du socle**, sur des branches que vous dites fusionnables tout de suite. Je ne les ai pas attendues : j'ai corrigé les deux dans notre dépôt (port résolu en JavaScript, `prisma generate` retiré de `dev`), et les deux sont vérifiés à l'épreuve — une seconde instance de l'API démarre maintenant proprement pendant que la première tourne. Il n'y aura donc rien à retirer chez nous quand le gabarit suivra.
Author
Owner

Fermé : tout est livré, et vérifié le 2026-10-07.

  • La page : apps/web/src/app/connexion/page.tsx est dans le gabarit, en une ligne : export { PageConnexion as default } from '@eqs/platform-next/client'.
  • Le filtre retour : retourSur du socle refuse les cinq formes dangereuses, chacune avec un cas de test : adresse entière, //, /\ (contre-oblique), javascript: et chaîne vide. La requête est gardée (/projets?page=2).
  • Le contrôle : next.page-connexion (bloquant) exige la page dès que protectedPrefixes n'est pas vide.
  • Votre réserve 2, la seule qui restait : PageConnexion prend désormais une accroche sous le titre (socle 0.15.1). Le nom et la phrase du produit se posent sans réécrire la page :
    export default function Connexion() {
      return <PageConnexion titre="Atelier" accroche="Le graphe du projet." />
    }
    

⚠️ L'extrait en tête de ce ticket est vulnérable, comme vous le signaliez : startsWith('/') && !startsWith('//') laisse passer /\ailleurs. Ne le recopiez pas. La bonne version est retourSur de @eqs/platform-next/client.

Fermé : tout est livré, et vérifié le 2026-10-07. - **La page** : `apps/web/src/app/connexion/page.tsx` est dans le gabarit, en une ligne : `export { PageConnexion as default } from '@eqs/platform-next/client'`. - **Le filtre `retour`** : `retourSur` du socle refuse les cinq formes dangereuses, chacune avec un cas de test : adresse entière, `//`, **`/\` (contre-oblique)**, `javascript:` et chaîne vide. La requête est gardée (`/projets?page=2`). - **Le contrôle** : `next.page-connexion` (bloquant) exige la page dès que `protectedPrefixes` n'est pas vide. - **Votre réserve 2**, la seule qui restait : `PageConnexion` prend désormais une `accroche` sous le titre (socle 0.15.1). Le nom et la phrase du produit se posent sans réécrire la page : ```tsx export default function Connexion() { return <PageConnexion titre="Atelier" accroche="Le graphe du projet." /> } ``` ⚠️ **L'extrait en tête de ce ticket est vulnérable**, comme vous le signaliez : `startsWith('/') && !startsWith('//')` laisse passer `/\ailleurs`. Ne le recopiez pas. La bonne version est `retourSur` de `@eqs/platform-next/client`.
Sign in to join this conversation.
No labels
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
Infrastructure/eqs-satellite-template#23
No description provided.