@eqs/platform-next/client expose SignIn mais rien en face : chaque satellite doit écrire sa déconnexion à la main #56

Closed
opened 2026-09-11 14:26:00 +00:00 by nicolas-fryder · 2 comments

Le socle fournit l'entrée dans la session (SignIn) et ne fournit pas la sortie. Tout satellite qui pose un bouton « Déconnexion » doit donc écrire lui-même un fetch vers better-auth — c'est-à-dire dupliquer un bout de contrat que le socle possède déjà, et le dupliquer à trente exemplaires.

Ce qui existe, et ce qui manque

node_modules/@eqs/platform-next/dist/client/index.d.ts :

EqsProvider, useSession, useCan, EqsErrorBoundary, SignIn

node_modules/@eqs/auth/dist/client.d.ts (réexporté par le précédent) :

Moi, peut, chargerMoi

Cherché signOut, sign-out, logout, deconnex dans les deux paquets et dans le client généré par orval : aucun résultat. Les routes better-auth ne figurent pas dans openapi.json (elles ne sont pas décrites par le document de l'application), donc aucun client n'est généré pour elles non plus.

Reproduction

Dans un satellite issu du gabarit, poser une barre de navigation avec un bouton de déconnexion. Il n'y a rien à appeler.

Ce que ça coûte

Le premier réflexe d'un agent de codage discipliné est de ne rien faire : la règle « aucun fetch direct, uniquement les clients générés » est dans l'AGENTS.md du satellite, et l'endpoint n'est documenté nulle part côté client. Le nôtre a donc livré un lien visible mais inerte, en le signalant dans son rapport — ce qui est le bon comportement, et un mauvais produit : un bouton mort est pire qu'un bouton absent.

Nous avons ensuite mesuré l'endpoint à la main :

$ curl -s -i -X POST http://localhost:4100/api/auth/sign-out \
    -H 'content-type: application/json' -d '{}'
HTTP/1.1 200 OK

et écrit le fetch nous-mêmes, avec credentials: 'include' (sans lui le cookie ne part pas, l'API ne sait pas qui se déconnecte, et l'appel rend 200 sans rien faire — un échec parfaitement silencieux) puis un window.location.assign pour qu'aucune donnée de la session précédente ne reste peinte à l'écran.

Rien de tout cela n'était écrit quelque part. Chacune de ces trois décisions est un endroit où un satellite peut se tromper sans que rien ne le dise.

Ce que je proposerais

Par ordre de préférence :

  1. Un SignOut symétrique de SignIn dans @eqs/platform-next/client, prenant le même baseUrl, et remettant useSession() à zéro en plus de poster. C'est le geste attendu, et le seul qui garantisse que l'état du provider et le cookie se vident ensemble.
  2. À défaut, une fonction deconnecter(baseUrl) dans @eqs/auth/client, à côté de chargerMoi — les satellites gardent leur bouton, le socle garde la route, la méthode et le credentials.
  3. Au minimum, documenter l'endpoint (méthode, corps, credentials: 'include', ce qu'il faut faire de l'état client après) dans le README de @eqs/platform-next. C'est la solution la plus faible : elle laisse trente satellites réécrire la même chose, mais au moins ils la réécrivent juste.

Accessoirement : SignIn prend baseUrl et poste sur ${baseUrl}/sign-in/email. Que le symétrique n'existe pas est d'autant plus surprenant que la mécanique, elle, est déjà là.

Contournement posé chez nous, à retirer quand ce ticket sera traité

apps/web/src/components/coquille.tsx, fonction BoutonDeconnexion — le seul fetch écrit à la main de toute l'interface, et son commentaire dit pourquoi.

Versions

  • @eqs/platform-next, @eqs/auth : 0.8.1
  • Next 16.3.4 (Turbopack), Node v24.15.0, Windows 11 Pro 26200

Trouvé en construisant la première vraie interface d'un satellite (Pilotage).

Le socle fournit l'entrée dans la session (`SignIn`) et ne fournit pas la sortie. Tout satellite qui pose un bouton « Déconnexion » doit donc écrire lui-même un `fetch` vers better-auth — c'est-à-dire dupliquer un bout de contrat que le socle possède déjà, et le dupliquer à trente exemplaires. ## Ce qui existe, et ce qui manque `node_modules/@eqs/platform-next/dist/client/index.d.ts` : ``` EqsProvider, useSession, useCan, EqsErrorBoundary, SignIn ``` `node_modules/@eqs/auth/dist/client.d.ts` (réexporté par le précédent) : ``` Moi, peut, chargerMoi ``` Cherché `signOut`, `sign-out`, `logout`, `deconnex` dans les deux paquets et dans le client généré par orval : **aucun résultat**. Les routes better-auth ne figurent pas dans `openapi.json` (elles ne sont pas décrites par le document de l'application), donc aucun client n'est généré pour elles non plus. ## Reproduction Dans un satellite issu du gabarit, poser une barre de navigation avec un bouton de déconnexion. Il n'y a rien à appeler. ## Ce que ça coûte Le premier réflexe d'un agent de codage discipliné est **de ne rien faire** : la règle « aucun `fetch` direct, uniquement les clients générés » est dans l'`AGENTS.md` du satellite, et l'endpoint n'est documenté nulle part côté client. Le nôtre a donc livré un lien **visible mais inerte**, en le signalant dans son rapport — ce qui est le bon comportement, et un mauvais produit : un bouton mort est pire qu'un bouton absent. Nous avons ensuite mesuré l'endpoint à la main : ``` $ curl -s -i -X POST http://localhost:4100/api/auth/sign-out \ -H 'content-type: application/json' -d '{}' HTTP/1.1 200 OK ``` et écrit le `fetch` nous-mêmes, avec `credentials: 'include'` (sans lui le cookie ne part pas, l'API ne sait pas qui se déconnecte, et l'appel rend 200 sans rien faire — un échec parfaitement silencieux) puis un `window.location.assign` pour qu'aucune donnée de la session précédente ne reste peinte à l'écran. Rien de tout cela n'était écrit quelque part. Chacune de ces trois décisions est un endroit où un satellite peut se tromper sans que rien ne le dise. ## Ce que je proposerais Par ordre de préférence : 1. **Un `SignOut` symétrique de `SignIn`** dans `@eqs/platform-next/client`, prenant le même `baseUrl`, et remettant `useSession()` à zéro en plus de poster. C'est le geste attendu, et le seul qui garantisse que l'état du provider et le cookie se vident ensemble. 2. À défaut, **une fonction `deconnecter(baseUrl)`** dans `@eqs/auth/client`, à côté de `chargerMoi` — les satellites gardent leur bouton, le socle garde la route, la méthode et le `credentials`. 3. Au minimum, **documenter l'endpoint** (méthode, corps, `credentials: 'include'`, ce qu'il faut faire de l'état client après) dans le README de `@eqs/platform-next`. C'est la solution la plus faible : elle laisse trente satellites réécrire la même chose, mais au moins ils la réécrivent juste. Accessoirement : `SignIn` prend `baseUrl` et poste sur `${baseUrl}/sign-in/email`. Que le symétrique n'existe pas est d'autant plus surprenant que la mécanique, elle, est déjà là. ## Contournement posé chez nous, à retirer quand ce ticket sera traité `apps/web/src/components/coquille.tsx`, fonction `BoutonDeconnexion` — le seul `fetch` écrit à la main de toute l'interface, et son commentaire dit pourquoi. ## Versions - `@eqs/platform-next`, `@eqs/auth` : 0.8.1 - Next 16.3.4 (Turbopack), Node v24.15.0, Windows 11 Pro 26200 Trouvé en construisant la première vraie interface d'un satellite (Pilotage).
Author
Owner

Vrai, et j'ai pris vos options 1 et 2 — elles ne sont pas concurrentes, elles sont à deux étages.

Votre paragraphe sur l'agent est le plus important du ticket, et je veux le reprendre tel quel :

Le premier réflexe d'un agent de codage discipliné est de ne rien faire […] Le nôtre a donc livré un lien visible mais inerte, en le signalant dans son rapport — ce qui est le bon comportement, et un mauvais produit.

C'est exactement ça. La règle était bonne, le produit était mauvais, et personne n'a fauté. Une lacune du socle devient un défaut de l'application, par obéissance. C'est ce qui en fait une classe et pas un oubli, et c'est consigné comme telle (chasse 12 de reference/chasses.md).

Les deux étages

deconnecter() dans @eqs/auth/client, à côté de chargerMoi : la route, la méthode, le corps, le cookie. Le mécanisme, écrit une fois.

SignOut dans @eqs/platform-next/client, le pendant de SignIn. Il fait les trois gestes que vous avez dû retrouver un par un :

  1. poster sign-out avec le cookie ;
  2. remettre useSession() à zéro — sinon useCan() continue d'autoriser après la déconnexion ;
  3. quitter la page, pour qu'aucune donnée de la session d'avant ne reste peinte.

C'est votre troisième point qui justifie que le composant existe plutôt qu'une simple fonction : vider la session ne démonte pas ce que les composants ont déjà affiché. onDeconnecte reprend la main sur cette décision quand le satellite sait ce qu'il fait ; par défaut, on navigue.

Utilisable hors d'un EqsProvider — il ne remet à zéro que ce qu'il trouve.

Votre credentials touchait aussi le code existant

C'est la partie que je ne cherchais pas. En écrivant deconnecter, j'ai regardé comment chargerMoi s'y prenait :

init: RequestInit = { credentials: 'include' }

Le réglage était le défaut d'un paramètre. Passer le moindre init — un en-tête, un signal — le perdait, et l'appelant se retrouvait avec un /moi en 401, donc « vous n'êtes pas connecté », sans rien pour l'expliquer. Le même échec silencieux que le vôtre, dans l'autre sens, et personne ne l'avait signalé parce que personne n'avait encore passé d'init.

Il est maintenant appliqué après l'init : l'omission ne coûte plus rien, et credentials: 'omit' reste possible pour qui le veut vraiment.

Un réglage indispensable ne se met pas dans une valeur par défaut qu'un appelant peut écraser sans le savoir.

Éprouvé en le retirant : deux cas rougissent, un par fonction.

Votre option 3

Elle devient sans objet — il n'y a plus rien à réécrire, donc plus rien à documenter pour que trente satellites le réécrivent juste. Ce qui reste documenté, c'est le pourquoi : les commentaires de deconnecter et de SignOut portent les trois décisions et le piège du cookie.

Ce que ça change chez vous

BoutonDeconnexion dans apps/web/src/components/coquille.tsx — votre seul fetch écrit à la main — se remplace par :

import { SignOut } from '@eqs/platform-next/client'
// …
<SignOut />

Si votre API est sur une autre origine, baseUrl={${apiBaseUrl(process.env.NEXT_PUBLIC_API_URL ?? '')}/api/auth}, comme pour SignIn.

Ce que je n'ai pas fait

Votre remarque de fin mérite sa réponse : les routes better-auth ne sont dans aucun contrat. openapi.json ne les décrit pas, donc aucun client n'est généré pour elles, donc tout ce qui les touche sera toujours écrit à la main quelque part. Je n'ai pas cherché à les faire entrer dans le document — c'est un vrai sujet, avec un effet sur le contrat servi et sur les clients de toute la flotte. Autant que ce « quelque part » soit le socle, ce qui est maintenant le cas pour les deux gestes qui comptent.

Et l'inventaire des autres paires n'est pas fait : defineTarget / oublierCibles, eqs e2e up / down, preparerCle et sa révocation. Certaines existent, aucune n'a été vérifiée comme paire. C'est écrit dans la chasse 12 avec le signe à chercher — un satellite qui écrit un fetch à la main, ou un bouton inerte.

Branche chasse/knip-et-code-mort. Ticket laissé ouvert jusqu'à la publication.

Vrai, et j'ai pris vos options 1 **et** 2 — elles ne sont pas concurrentes, elles sont à deux étages. Votre paragraphe sur l'agent est le plus important du ticket, et je veux le reprendre tel quel : > Le premier réflexe d'un agent de codage discipliné est **de ne rien faire** […] Le nôtre a donc livré un lien **visible mais inerte**, en le signalant dans son rapport — ce qui est le bon comportement, et un mauvais produit. C'est exactement ça. La règle était bonne, le produit était mauvais, et personne n'a fauté. **Une lacune du socle devient un défaut de l'application, par obéissance.** C'est ce qui en fait une classe et pas un oubli, et c'est consigné comme telle (chasse 12 de `reference/chasses.md`). ## Les deux étages **`deconnecter()` dans `@eqs/auth/client`**, à côté de `chargerMoi` : la route, la méthode, le corps, le cookie. Le mécanisme, écrit une fois. **`SignOut` dans `@eqs/platform-next/client`**, le pendant de `SignIn`. Il fait les **trois** gestes que vous avez dû retrouver un par un : 1. poster `sign-out` avec le cookie ; 2. remettre `useSession()` à zéro — sinon `useCan()` continue d'autoriser après la déconnexion ; 3. quitter la page, pour qu'aucune donnée de la session d'avant ne reste peinte. C'est votre troisième point qui justifie que le composant existe plutôt qu'une simple fonction : vider la session ne démonte pas ce que les composants ont déjà affiché. `onDeconnecte` reprend la main sur cette décision quand le satellite sait ce qu'il fait ; par défaut, on navigue. Utilisable hors d'un `EqsProvider` — il ne remet à zéro que ce qu'il trouve. ## Votre `credentials` touchait aussi le code existant C'est la partie que je ne cherchais pas. En écrivant `deconnecter`, j'ai regardé comment `chargerMoi` s'y prenait : ```ts init: RequestInit = { credentials: 'include' } ``` Le réglage était le **défaut d'un paramètre**. Passer le moindre `init` — un en-tête, un `signal` — le perdait, et l'appelant se retrouvait avec un `/moi` en 401, donc « vous n'êtes pas connecté », sans rien pour l'expliquer. Le même échec silencieux que le vôtre, dans l'autre sens, et personne ne l'avait signalé parce que personne n'avait encore passé d'`init`. Il est maintenant appliqué **après** l'`init` : l'omission ne coûte plus rien, et `credentials: 'omit'` reste possible pour qui le veut vraiment. > **Un réglage indispensable ne se met pas dans une valeur par défaut qu'un appelant peut écraser sans le savoir.** Éprouvé en le retirant : deux cas rougissent, un par fonction. ## Votre option 3 Elle devient sans objet — il n'y a plus rien à réécrire, donc plus rien à documenter pour que trente satellites le réécrivent juste. Ce qui reste documenté, c'est le **pourquoi** : les commentaires de `deconnecter` et de `SignOut` portent les trois décisions et le piège du cookie. ## Ce que ça change chez vous `BoutonDeconnexion` dans `apps/web/src/components/coquille.tsx` — votre seul `fetch` écrit à la main — se remplace par : ```tsx import { SignOut } from '@eqs/platform-next/client' // … <SignOut /> ``` Si votre API est sur une autre origine, `baseUrl={`${apiBaseUrl(process.env.NEXT_PUBLIC_API_URL ?? '')}/api/auth`}`, comme pour `SignIn`. ## Ce que je n'ai pas fait Votre remarque de fin mérite sa réponse : **les routes better-auth ne sont dans aucun contrat.** `openapi.json` ne les décrit pas, donc aucun client n'est généré pour elles, donc tout ce qui les touche sera toujours écrit à la main quelque part. Je n'ai pas cherché à les faire entrer dans le document — c'est un vrai sujet, avec un effet sur le contrat servi et sur les clients de toute la flotte. Autant que ce « quelque part » soit le socle, ce qui est maintenant le cas pour les deux gestes qui comptent. Et l'inventaire des **autres paires** n'est pas fait : `defineTarget` / `oublierCibles`, `eqs e2e up` / `down`, `preparerCle` et sa révocation. Certaines existent, aucune n'a été vérifiée comme paire. C'est écrit dans la chasse 12 avec le signe à chercher — un satellite qui écrit un `fetch` à la main, ou un bouton inerte. Branche `chasse/knip-et-code-mort`. Ticket laissé ouvert jusqu'à la publication.
nicolas-fryder referenced this issue from a commit 2026-09-12 14:30:20 +00:00
Author
Owner

Corrigé. @eqs/platform-next/client exporte SignOut et ses OptionsSignOut (packages/platform-next/src/client/index.tsx) : la paire est complète, plus aucun satellite n'écrit sa déconnexion à la main.

Ce ticket a aussi fait bouger une règle du dépôt, et c'est écrit dans AGENTS.md : « un bouton de déconnexion livré inerte parce que la règle interdisait le fetch qui manquait au socle (#56) ». C'est l'un des trois cas qui ont fondé le principe « le signe qu'une règle est à revoir : quelqu'un a dû contourner un garde-fou pour faire la chose juste ».

Fermé après vérification contre le code de dev le 2026-09-21. Ce ticket restait ouvert faute d'un Closes #56 dans le commit, pas faute de correctif.

Corrigé. `@eqs/platform-next/client` exporte `SignOut` et ses `OptionsSignOut` (`packages/platform-next/src/client/index.tsx`) : la paire est complète, plus aucun satellite n'écrit sa déconnexion à la main. Ce ticket a aussi fait bouger une règle du dépôt, et c'est écrit dans `AGENTS.md` : « un bouton de déconnexion livré inerte parce que la règle interdisait le `fetch` qui manquait au socle (#56) ». C'est l'un des trois cas qui ont fondé le principe « le signe qu'une règle est à revoir : quelqu'un a dû contourner un garde-fou pour faire la chose juste ». Fermé après vérification contre le code de `dev` le 2026-09-21. Ce ticket restait ouvert faute d'un `Closes #56` dans le commit, pas faute de correctif.
Sign in to join this conversation.
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-platform#56
No description provided.