Version 0.21.0 : les treize tickets du rétrofit d'ATLAS (#137 à #149) #150

Merged
nicolas-fryder merged 16 commits from nicolas-fryder/dev into main 2026-10-13 10:02:17 +00:00 AGit

Sécurité : compte suspendu sans session ni clé (et le refus de la garde était mort, mesuré), un seau par client derrière proxy (better-auth et Express), OAuth 2.1 par liste blanche et greffons typés. Auth : roleParDefaut, principal de l'application, session seule, permission calculée, jetonDelegue (retrait vs panne), refus de liaison nommé. Contrat/erreurs : paramètres répétés, borne par route, binaires et 3xx, corps facultatif, Retry-After et UNAVAILABLE, route-sans-withapi sur le marqueur, register léger et creerProxy configurable. Outillage : dispenses dans coolify verify et les harnais, label eqs.hors-profil, .eqs/conformance.json, routes de conformité next. Satellites : ajouter TRUSTED_PROXIES= au .env.example ; un DepotCles maison doit renseigner proprietaireSuspendu. Chasses 44 à 48, D-063. pnpm check vert. Closes #137, closes #138, closes #139, closes #140, closes #141, closes #142, closes #143, closes #144, closes #145, closes #146, closes #147, closes #148, closes #149. 🤖 Generated with Claude Code

Sécurité : compte suspendu sans session ni clé (et le refus de la garde était mort, mesuré), un seau par client derrière proxy (better-auth et Express), OAuth 2.1 par liste blanche et greffons typés. Auth : roleParDefaut, principal de l'application, session seule, permission calculée, jetonDelegue (retrait vs panne), refus de liaison nommé. Contrat/erreurs : paramètres répétés, borne par route, binaires et 3xx, corps facultatif, Retry-After et UNAVAILABLE, route-sans-withapi sur le marqueur, register léger et creerProxy configurable. Outillage : dispenses dans coolify verify et les harnais, label eqs.hors-profil, .eqs/conformance.json, routes de conformité next. Satellites : ajouter TRUSTED_PROXIES= au .env.example ; un DepotCles maison doit renseigner proprietaireSuspendu. Chasses 44 à 48, D-063. pnpm check vert. Closes #137, closes #138, closes #139, closes #140, closes #141, closes #142, closes #143, closes #144, closes #145, closes #146, closes #147, closes #148, closes #149. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Trois défauts, dont un que le ticket ne voyait pas :

1. La connexion d'un compte `banned` créait la session (better-auth ne
   lit `banned` qu'avec son greffon `admin`). Un hook `session.create.before`
   la refuse en 403, tous chemins confondus. `APIError` plutôt que `false` :
   `false` fait échouer la création en silence.
2. Mesuré en écrivant le test : `banned` n'était pas déclaré dans le schéma
   utilisateur de better-auth, qui le retirait de l'utilisateur de session.
   Le refus « Compte suspendu » de `principalDepuisUtilisateur` était MORT :
   un compte suspendu après sa connexion gardait l'accès. ATLAS ne le voyait
   pas, parce que son propre greffon refusait plus tôt.
3. `principalDepuisCle` ne lisait pas le propriétaire. `proprietaireSuspendu`
   devient obligatoire dans `CleStockee` — un dépôt maison doit le dire, à la
   compilation — et `depotClesPrisma` le lit par `include`.

Contrôle `auth.cle-compte-suspendu`. Rouge sur l'ancien code : 4 cas sur 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
better-auth ne fait confiance qu'à un `X-Forwarded-For` à valeur unique,
sauf si on lui nomme des proxys de confiance ; `creerAuth` ne le faisait
pas. Derrière Traefik la chaîne a plusieurs valeurs : clé commune, et tout
le monde partageait le plafond de connexion.

`advanced.ipAddress.trustedProxies` est posé, depuis `TRUSTED_PROXIES`
(nouveau dans `baseSchema`), par défaut les plages privées. better-auth
ignore en silence une entrée illisible : `creerAuth` la refuse au démarrage.

Tests : deux clients derrière le même proxy ont deux seaux ; la gauche de
la chaîne, que le client écrit, ne compte pas. Rouge sur l'ancien code.

Conséquence mesurée sur nos propres tests : deux suites séparaient leurs
seaux avec des adresses 10.x, désormais prises pour des proxys. Elles
passent à 203.0.113.x (TEST-NET-3). Même limite chez un satellite dont les
clients arrivent d'une adresse privée (VPN) : documentée, la parade est de
nommer la plage exacte du proxy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1. `creerAuth({ plugins: [oauthProvider(…), jwt()] })` ne compilait pas
   sous le tsconfig du socle — les déclarations du greffon portent
   `items?: undefined`, refusé par `exactOptionalPropertyTypes` — et
   `auth.api` ignorait les greffons. Les greffons deviennent le paramètre
   de type de `creerAuth` ; `AuthAvec<P>` ajoute leurs points d'entrée.
   Reproduit avec `@better-auth/oauth-provider@1.7.3` (dépendance de
   développement), test qui le monte.

2. `ROUTES_AUTH_FERMEES` fermait quatre routes du greffon et laissait le
   reste : consentements, `userinfo`, `end-session`, lecture des clients.
   `/oauth2/*` est désormais fermé SAUF le trajet d'autorisation — une
   route ajoutée par une version future est fermée d'office. `/token` du
   greffon `jwt` est fermé, `/jwks` reste ouvert.

Chasse : le parcours délégué (genericOAuth) n'a pas de route sous
`/oauth2/` en 1.7 — il passe par `/callback/:id` et `/link-social`, vérifié
dans les sources. `platform-next` lit la même liste : couvert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Un compte jit sans groupe connu entrait sans rôle ni message. Le catalogue
déclare un rôle par défaut, validé comme un rôle connu ; un groupe connu
l'emporte. Le cahier §8 bis dit la reprise des identités Entra : oid et
non sub, relier par adresse sur un locataire nommé.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le test qui fige les clés du schéma n'avait pas suivi : la suite n'avait
pas été rejouée en entier avant le commit de #139.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1. better-auth avale la cause d'un rafraîchissement raté (même
   FAILED_TO_GET_ACCESS_TOKEN pour invalid_grant et pour un 502).
   `jetonDelegue` refait son chemin (déchiffrer, refreshAccessToken du
   fournisseur monté, chiffrer, ranger) en lisant l'erreur : `retire` sur
   invalid_grant (RFC 6749 §5.2), `indisponible` sur tout le reste. Un
   rafraîchissement à la fois par liaison. `@eqs/http` accepte cet état et
   lève DELEGATION_RETIREE (403) ou DELEGATION_INDISPONIBLE (503).

2. `email_does_not_match` ne disait pas l'adresse vue. Première voie
   essayée, `user.validateUserInfo` : rejetée, elle impose un contexte de
   point d'entrée à TOUTE création d'utilisateur — 12 tests rouges, et tout
   amorçage hors requête l'aurait été. Voie retenue, sans envelopper le
   fournisseur : `mapProfileToUser` relève l'adresse dans un
   AsyncLocalStorage dont `creerAuth` pose la portée par `run` autour de
   `auth.handler` (chasse 38), et un crochet `after` complète la
   redirection. La redirection levée est l'erreur de better-call, pas
   l'APIError importée : lue par sa forme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Une erreur porte des en-têtes que les deux rendus posent ; rateLimited et
unavailable acceptent retryAfterSeconds. UNAVAILABLE (503) entre dans le
vocabulaire de base, distinct d'UPSTREAM (502) : ATLAS avait inventé trois
codes pour dire « la dépendance n'est pas là ».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#141 — la requête était aplatie par Object.fromEntries : une clé répétée
perdait ses valeurs. Les clés tableau sont lues sur le schéma JSON
d'entrée (celui que le contrat publie) et rendues par getAll.

#147 — bodyLimit par route ; lireCorpsBorne exporté ; contentType publie
le type réel d'un binaire ou d'un flux ; un 3xx publie une redirection et
`eqs audit` cesse de la compter en écart ; un corps .optional() est publié
facultatif.

#140 — DepsPrincipal.autre : le principal que l'application résout
(résolveur câblé dans les DEUX plateformes). Une route publique devient
anonyme devant un Authorization INCONNU, mais une identité du socle
invalide y reste refusée — la décision écrite dans la garde Nest tient.
`session: true` et `@SessionSeule()` : la paire est livrée des deux côtés
(chasse 12). `permission` peut se calculer d'après le chemin ; le harnais
de conformité la dit « calculée » au lieu de la publier.

Au passage : l'exception ESLint de platform-nest sur `creerAuth` n'a plus
d'objet depuis que son type est explicite (#145) ; retirée.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le harnais avait déjà la bonne source (les routes nues d'exportOpenApi, sur
metaDe) ; eqs doctor n'avait que la lecture textuelle, fausse dans les deux
sens. Le contrôle consulte d'abord le module d'export que le dépôt déclare
(eqs.openapi) ; le repli textuel est rétrogradé en écart et le dit.

Chasse 1, dans le même fichier : next.corps-borne lisait les commentaires —
les deux autres lectures textuelles les ôtaient. next.reponses réclamait un
schéma à un 302 (#147).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
registerEqs passe dans @eqs/platform-next/register, qui ne tire pas
@eqs/auth : son graphe est gardé par un test sur dist/. L'environnement est
validé avec les règles du satellite avant la télémétrie, qu'il nomme.
creerProxy gagne le refus par défaut (publicPrefixes), le nom du paramètre
de retour, le mode Report-Only et un matcher qui garde les images sous le
proxy ; les deux postures ensemble sont refusées à la construction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Les dispenses n'étaient appliquées que par eqs doctor et eqs audit : la
même classe que #149 (runNextConformance), consignée en chasse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1. runConformance écrit .eqs/conformance.json (résultat + état par règle),
   avant de lever.
2. Les dispenses s'appliquaient aux seules règles jamais jouées dans les
   harnais : un écart dispensé faisait échouer le cas. Le cœur des
   dispenses passe dans @eqs/testing (les harnais ne peuvent pas importer
   @eqs/cli, qui dépend de lui) ; une seule définition de « couvert ».
3. eqs inspect lit l'état des règles : cinq champs « non observés » malgré
   une suite verte sortent de nonObserve.
4. Les routes de conformité de Nest (D-058) ont leur pendant Next, montées
   par demarrerRoutes, par withApi.

Classe commune avec #148 (coolify verify) : les dispenses n'étaient lues que
par doctor et audit. Consignée en chasse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Même classe que #137, autre instance : Express ne faisait confiance qu'au
dernier saut. Rouge sur trust proxy 1 (un seau pour deux clients derrière
le même proxy), vert sur la liste de plages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
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!150
No description provided.