Version 0.24.0 #158

Merged
marc-agbanavor merged 24 commits from dev into main 2026-10-15 07:40:54 +00:00

Version 0.24.0 : le socle du hub d'identité (#157) — routes fermées réglables, fragments Prisma du serveur d'autorisation (dont two-factor), crochets de l'application, rotation du secret, limites de débit par chemin, Redis, @Authentifie(), identifiant de requête dans better-auth, reprise OAuth à la connexion, jetons du hub acceptés par tout satellite (D-067 à D-081), et deux correctifs de sécurité (retourSur, porteAuth).

Sortie selon D-037 : publication de la candidate sous next depuis main fusionné, bac à sable eqs-demo, puis promotion en latest.

🤖 Generated with Claude Code

Version 0.24.0 : le socle du hub d'identité (#157) — routes fermées réglables, fragments Prisma du serveur d'autorisation (dont two-factor), crochets de l'application, rotation du secret, limites de débit par chemin, Redis, `@Authentifie()`, identifiant de requête dans better-auth, reprise OAuth à la connexion, jetons du hub acceptés par tout satellite (D-067 à D-081), et deux correctifs de sécurité (`retourSur`, `porteAuth`). Sortie selon D-037 : publication de la candidate sous `next` depuis `main` fusionné, bac à sable `eqs-demo`, puis promotion en `latest`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`@eqs/auth` passe de `better-auth ^1.7.3` à `^1.7.7`, les plateformes
suivent en développement. Le plan du hub proposait un pair ; mesuré dans un
projet pnpm jetable, aucun cas ne l'exige :

- satellite verrouillé sur 1.7.3 en direct : deux copies, mais un adaptateur
  1.7.3 dans une instance 1.7.7 crée un compte et connecte (200). En pair,
  `@eqs/auth` aurait tourné sur la 1.7.3 du satellite, sans le correctif ;
- hub (`better-auth ^1.7.7` + `@better-auth/oauth-provider 1.7.7`) : une
  seule copie.

Relu sur 1.7.7 ce qui avait été mesuré sur 1.7.3 : le filtre `input: false`
du profil (D-052) et le format chiffré de `account` (`ressembleAUnChiffre`)
sont inchangés, `entra-rappel.test.ts` est vert.

Ce que 1.7.4 à 1.7.7 change pour le socle, chacun par un test :
- la connexion par `id_token` respecte `disableSignUp` : en 1.7.3,
  `provision: 'ferme'` laissait un inconnu se créer un compte par
  `sign-in/social` + `idToken`. Rouge sur 1.7.3, vert sur 1.7.7 ;
- `sign-in/email` refuse au-delà de 128 caractères avant de dériver
  (`PASSWORD_TOO_LONG`). Les commentaires qui disaient le contraire sont
  corrigés ; la borne du corps (D-054) garde sa raison.

Les exclusions `minimumReleaseAgeExclude` de 1.7.3 sont retirées : 1.7.7 a
douze jours, le délai de 24 h joue pour lui.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`ROUTES_AUTH_FERMEES` était une constante : `porteAuth`, `monterBetterAuth`
et `authRoute()` de platform-next la lisaient chacun. Elle interdit à bon
droit `/oauth2/register` à un satellite, et interdisait du même coup au hub
d'identité l'enregistrement dynamique (RFC 7591) par lequel Claude et les
clients MCP se déclarent auprès d'un serveur d'autorisation.

- `porteAuth(basePath, plafond, routesFermees)` et
  `monterBetterAuth(…, routesFermees)`, défaut la constante, inchangée ;
- `auth.routesFermees` dans `createEqsApp` et `creerApi` ;
- `ROUTES_AUTH_FERMEES_SANS_REGISTER`, la seule variante livrée.

Une liste qui laisse passer l'inscription ou l'administration de
better-auth est refusée au démarrage (`routesFermeesValides`) ; côté Next, à
la construction de `creerApi`.

Écart au plan (A-1) : l'option n'est pas sur `OptionsAuth`. `creerAuth` ne
monte pas la porte, personne ne l'y aurait lue.

Prouvé sur `createEqsApp` avec `jwt()` + `oauthProvider()` : par défaut
`POST /api/auth/oauth2/register` rend 404 enveloppé, greffon monté ; avec la
variante il enregistre le client, `/admin/*` et le CRUD restent 404
enveloppés, `auth.inscription-fermee` reste vert. Côté Next, une fausse
instance mesure ce qui lui parvient.

Trouvé au passage : sous `exactOptionalPropertyTypes`, le type
d'`oauthProvider()` 1.7.7 n'est pas assignable à `BetterAuthPlugin`. Le test
transtype en le disant ; le hub aura la même ligne.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Le hub monte les greffons `jwt` et `oauthProvider` : huit tables que
`auth.prisma` ne porte pas, et que la chasse 27 laissait à la charge du
satellite — à recopier, ce que #88 a montré qu'on fait mal.

- `schemas/jwks.prisma` (`Jwks`) et `schemas/oauth-provider.prisma` (les
  sept modèles d'`@better-auth/oauth-provider`), à inclure en plus
  d'`auth.prisma`, qui ne bouge pas ;
- noms de la bibliothèque, tables en snake_case, types du générateur de
  better-auth pour PostgreSQL, champs de relation nommés comme l'adaptateur
  Prisma les joint ;
- `fragment-prisma.test.ts` confronte les trois fichiers à `getAuthTables`
  calculé avec les deux greffons : type, liste, nullabilité, unicité, index,
  relation, `onDelete`, et ni doublon ni modèle inconnu. Rouge, mesuré, sur
  une colonne retirée, un index ôté, un `Json` en `String`, un `onDelete`
  faux.

Limite assumée, écrite dans le fragment : `userId` et `sessionId` sont
indexés sans `@relation` — Prisma exigerait le champ inverse dans
`auth.prisma`, qui doit rester valide seul.

Éprouvé hors du dépôt, Prisma 6.19 et PostgreSQL 16 jetable : `prisma
validate` vert avec `auth.prisma` seul et avec les trois, `migrate dev`
crée les huit tables, `diffSchema` de better-auth ne trouve aucun écart, et
un trajet réel `creerAuth` + `jwt` + `oauthProvider` (enregistrement, JWKS,
code + PKCE, consentement, jeton, rafraîchissement) écrit dans chaque table
qu'il touche.

Le script du gabarit (`package.json#eqs.schemasAuth`, défaut `["auth"]`)
n'est pas dans ce dépôt : documenté dans son cahier, §4.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Un client OAuth cherche les métadonnées du serveur d'autorisation là où
l'émetteur les place : à la racine quand l'émetteur est l'origine, en
insertion de chemin (RFC 8414 §3.1) sinon. better-auth, monté sous
`/api/auth`, ne voyait ni l'un ni l'autre — et mesuré sur 1.7.7, avec un
émetteur égal à `PUBLIC_URL`, `oauthProvider` ne sert plus rien sous
`/api/auth/.well-known/`.

`auth.wellKnownRacine` (défaut `false`, rien ne change) : les `GET`/`HEAD`
sous `/.well-known/` sont transmis tels quels à better-auth, dont le greffon
sait selon l'émetteur quels chemins il sert ; le reste rend le 404
enveloppé. Refusé au démarrage sans better-auth.

Écart au plan (A-4) : pas de réécriture vers `<basePath>/.well-known/:doc`,
fausse dès que l'émetteur change ; pas de `/.well-known/jwks.json`, que rien
n'annonce (`jwks_uri` pointe sur `<basePath>/jwks`, qui répond).

Trouvé en l'écrivant : `/.well-known/../api/auth/…` (et `%2e%2e`)
atteignait better-auth en contournant la porte. Le chemin est relu normalisé
et ne passe que s'il reste sous `/.well-known/` — test rouge sans la garde.

Prouvé : défaut → 404 enveloppé ; émetteur = `PUBLIC_URL` →
`openid-configuration` et `oauth-authorization-server` à la racine, issuer
égal à `PUBLIC_URL`, chaque point sous `PUBLIC_URL/api/auth/`, le JWKS
annoncé répond ; émetteur avec chemin → insertion de chemin ; absent de
`/openapi.json` et de `routesMontees`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`createEqsApp` n'avait qu'un régime : origines de confiance et
`credentials: true`, partout. Un serveur d'autorisation a des points —
`oauth2/token`, `register`, `jwks`, `/.well-known/…` — appelés depuis des
origines qu'il ne connaît pas, par des clients qui s'authentifient par ce
qu'ils envoient, jamais par un cookie.

`cors.publicPaths` (défaut vide : l'appel à `enableCors` est celui d'avant) :
pour un chemin listé, `Access-Control-Allow-Origin: *`, pas
d'`Allow-Credentials`, `GET, POST, OPTIONS`, `authorization`,
`content-type`, `dpop` ; `WWW-Authenticate` et `DPoP-Nonce` exposés. Une
chaîne couvre le chemin et ce qui est dessous, par segment ; une expression
régulière est testée sur le chemin. Le chemin est jugé normalisé : un `..`
ne fait pas passer `/api/moi` pour public (test rouge sans).

Prouvé : par défaut, une origine inconnue ne reçoit rien sur
`/api/auth/oauth2/token` ; avec la liste, son pré-vol reçoit `*` sans
identifiants, `GET /api/moi` depuis la même origine reste refusé, l'origine
de confiance garde `credentials`. `platform.cors` n'a pas changé : vert avec
la liste du hub, rouge si l'on y met `/health`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Le plan du hub (A-8) partait de « l'`oid` Entra n'est pas conservé » —
vrai chez HQ sur better-auth 1.6, faux sur le socle : depuis 1.7 le
fournisseur Microsoft déclare `accountSubject: profile.oid`, donc
`account.accountId` EST l'`oid`. Prouvé sur le rappel rejoué, et la personne
se retrouve par `findAccountByKey` sans passer par l'adresse.

Ce qui reste utile, et livré : faire suivre l'annuaire à d'autres colonnes de
`user` (`upn`, matricule). `mapProfileToUser` ne peut pas les porter — un
champ `input: false` est jeté (D-052), un champ saisissable serait écrit par
la personne elle-même.

- `attenteDesRoles` devient `attente<T>` : le rôle par adresse comme avant,
  les champs par `oid` ;
- `champsUtilisateur` (forcé `input: false`, facultatif ; `role` et les
  colonnes de better-auth refusés à la construction) et
  `profilVersUtilisateur`, écrits par un hook `after` du rappel : à la
  création `jit`, dans la même requête, et à chaque connexion suivante ;
- transmis par `createEqsApp` ; sans eux, ni colonne ni hook.

Écart au plan, consigné dans D-072 : il voulait ces champs dans un modèle 1-1
(`IdentiteHub`) ET par `additionalFields`, qui écrit dans `user`. Un
satellite qui inclut le fragment ne peut pas ajouter de colonne à `User` ;
l'option sert l'application qui possède son modèle, et le hub n'en a pas
besoin pour l'`oid`.

ETAT : la branche, et deux points ouverts — `oauthProvider()` non assignable
sous `exactOptionalPropertyTypes`, et les hooks git suivis en 644, que git
ignore sur un clone Linux ou macOS.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sous `exactOptionalPropertyTypes`, le type d'`oauthProvider()` 1.7.7 n'est
pas assignable à `BetterAuthPlugin`. Cette branche l'avait réglé en élargissant
`OptionsAuth.plugins` ; `main` l'avait réglé entre-temps de la même façon
(#145, 0.21.0 : `readonly object[]`, et `creerAuth<P>` qui type `auth.api`).

Au rebase sur 0.23.0, la forme de #145 est gardée et `GreffonBetterAuth`
abandonné ; il reste de ce commit les tests du serveur d'autorisation, qui
n'ont plus de transtypage, et l'amendement de D-068.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Au rebase sur 0.23.0, la variante de D-068 était restée la liste noire
d'avant #145 et la chasse 46 : elle rouvrait `get-access-token`,
`refresh-token`, `change-email`, `delete-user` et la lecture des clients, et
aucun test ne rougissait — ils n'éprouvaient que ce qu'elle ouvre.

- les listes sont construites par une seule fonction, exportée,
  `routesAuthFermees(oauth2Ouvertes)` (liste BLANCHE des routes `/oauth2/*`,
  `TRAJET_OAUTH2` par défaut) ;
- `ROUTES_AUTH_FERMEES_SANS_REGISTER` : le défaut, `register` en plus ;
- `ROUTES_AUTH_FERMEES_SERVEUR_OIDC`, nouvelle : `register`, `userinfo`,
  `end-session`, `continue` — ce dont le hub se sert (la variante seule les
  aurait fermés) ;
- les tests comparent les variantes au défaut route par route.

Et un test de D-072 qui supposait l'absence de crochet `after` : il existe
toujours depuis #144 (0.21.0) ; on vérifie désormais les colonnes.

D-068 amendée, instance consignée dans la chasse 46.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`proxy-de-confiance.test.ts` dérive 31 mots de passe par test : 4,6 s seul,
au-delà des 5 s par défaut dès que la machine est chargée. La porte est tombée
deux fois de suite sur ces deux tests, verts isolés. Délai porté à 30 s, la
raison écrite dans le test ; instance consignée dans la chasse 6.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sécurité. `retourSur` laissait passer `/<TAB>/ailleurs` : le navigateur
retire tabulations et retours à la ligne avant d'analyser une adresse, et
partait sur https://ailleurs après la connexion — redirection ouverte depuis
`/connexion?retour=…`. Il refuse désormais tout caractère de contrôle,
d'espacement ou de format Unicode, toute contre-oblique, `//` en tête, et
les mêmes formes ENCODÉES à chaque niveau de décodage (encodage invalide
refusé) ; ce qu'il garde est vérifié par `new URL` sur une origine fictive.
38 charges éprouvées, dont la tabulation, CR/LF, NUL, DEL, C1, NBSP, U+2028,
U+200B, U+202E, BOM, `%2F`, `%5C`, `%09`, `%0d%0a`, double et triple encodage ;
`%20` dans une requête reste permis. Référence : `suiteSure` d'eqs-mcp.

Même classe, cherchée ailleurs : `porteAuth` jugeait le chemin brut, quand
l'adaptateur Node de better-auth refait une `URL` qui résout `..`. Mesuré :
`GET /api/auth/x/../oauth2/get-clients` envoyé tel quel atteignait
better-auth (401) au lieu du 404 de la porte. Jugé désormais sur le chemin
normalisé ; test de bout en bout par une requête HTTP brute, rouge sans le
correctif. Les formes encodées ne sont pas routées par better-auth (mesuré).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Plan du hub A-9. `hooks` de better-auth n'a qu'une place et `creerAuth`
l'occupe (#144, D-072, #80) ; le hub passait les siens par un greffon local.

- `creerAuth({ hooks: { before, after }, databaseHooks })`, repris par
  `auth.hooks` et `auth.databaseHooks` de `createEqsApp` ;
- `before` joue après le socle et avant les greffons, `after` en dernier :
  deux greffons internes placés en tête et en queue ;
- crochets de base composés, le socle d'abord (#138 tient toujours), un
  `{ data }` passe de l'un à l'autre, `false` arrête de part et d'autre ;
- `auth.plugins` de `createEqsApp` accepte `(env) => greffons`.

Absents, rien ne change. Prouvé par sept tests dans `@eqs/auth` (refus 403,
ordre greffon puis application, re-hachage #80 toujours joué, refus #138 avant
l'application, données modifiées, création arrêtée) et un passage complet par
`createEqsApp`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Plan du hub A-11. `creerAuth` ne passait que `AUTH_SECRET` ; le hub validait
`BETTER_AUTH_SECRETS` lui-même et comptait sur better-auth pour la relire
dans `process.env`.

- schéma de base : `<version>:<secret>`, le courant d'abord, 32 caractères,
  versions distinctes, refus au démarrage ; valeur d'exemple bloquante ;
- `creerAuth` la passe en `secrets` ; `AUTH_SECRET` devient le secret hérité,
  l'un des deux est requis. Absente : comme avant.

Mesuré : sous [v2, v1] un chiffré v1 se relit et le neuf est en v2 ; sous [v2]
la relecture lève ; un chiffré d'`AUTH_SECRET` se relit après la bascule. Et
ce que la rotation ne fait pas : le cookie de session est signé par le seul
courant — chaque changement déconnecte tout le monde (get-session rend null),
écrit dans le cahier.

En montant : `BETTER_AUTH_SECRETS=` dans `.env.example`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Plan du hub A-10. Les proxys de confiance sont déjà sur `main` (#137,
0.21.0) : rien à y ajouter.

- `rateLimitRules` (creerAuth, `auth.rateLimitRules` de createEqsApp) :
  ajoutées à `LIMITES_PAR_DEFAUT` ou les remplaçant chemin par chemin ;
  `false` refusé au démarrage sur `/sign-in/email`, `/forget-password`,
  `/reset-password` ; règle illisible refusée ;
- `IP_ADDRESS_HEADERS` (schéma de base) → `ipAddressHeaders` de better-auth,
  et le limiteur de Nest lit l'adresse de la même façon, depuis la droite à
  travers `TRUSTED_PROXIES` (chasse 45).

Absents, rien ne change. Prouvé : 429 à la troisième connexion sous `max: 2`,
quatre règles fautives refusées, le seau suit `CF-Connecting-IP` dans
better-auth et dans Nest (le test Nest est rouge sans la lecture de l'en-tête).

En montant : `IP_ADDRESS_HEADERS=` dans `.env.example`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Plan du hub A-6. Le hub avait mesuré (L-039) que better-auth lit
`secondaryStorage` et le stockage de la limitation AVANT l'`init` des
greffons : posé par un greffon, seul l'adaptateur interne passait sur Redis.
La parade est au niveau de `creerAuth`.

- `creerAuth({ secondaryStorage })` : passé avec le reste, entraîne
  `session.storeSessionInDatabase` et la limitation sur le stockage ;
- `@eqs/auth/redis` : `clientRedis`, `stockageRedis` (`getAndDelete` et
  `increment` atomiques par script Lua, clés préfixées) ; `ioredis` chargé
  seulement par qui pose `REDIS_URL` ;
- `createEqsApp` : client sur `REDIS_URL` (schéma de base), donné à
  better-auth et au limiteur de Nest (`LimiteurRedis`), sonde `redis` non
  vitale, fermé à l'arrêt ; `redis` injecte un client.

Sans `REDIS_URL`, rien ne change. Prouvé sur un vrai Redis jetable
(`EQS_TEST_REDIS_URL`, sauté sinon) : deux répliques à bases distinctes
partagent le seau du limiteur et la session ; et, sans Redis, la mécanique
better-auth sur un stockage de même forme.

En montant : `REDIS_URL=` dans `.env.example`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Le hub a renoncé au TOTP sur son compte de secours (L-035) : `twoFactor`
décide sur `user.twoFactorEnabled`, et Prisma n'étend pas `User` depuis un
autre fichier — or `auth.prisma` est généré, refusé à l'édition. Même trou
pour les `champsUtilisateur` de D-072.

- un fragment déclare ses colonnes de `User` par `// @eqs:user <champ>` ;
  `schemaAuth({ fragments, colonnesUtilisateur })` (`@eqs/auth/prisma`)
  compose et les injecte, en signant chaque ligne de sa source ;
- `schemas/two-factor.prisma` : `TwoFactor` et `User.twoFactorEnabled` ;
- `eqs auth prisma [--out] [--check]` lit `package.json#eqs.schemasAuth`
  (défaut `["auth"]`, la copie de toujours) et `eqs.colonnesUtilisateur` : le
  script que D-069 laissait au gabarit devient une commande du socle.

Prouvé : fragment composé confronté au schéma de better-auth avec
`twoFactor` ; trajet TOTP complet sur `creerAuth` (activation,
`twoFactorRedirect` sans session, code faux refusé, session au bon code) ;
hors dépôt, PostgreSQL 16 jetable + Prisma 6.19 : `validate`, `db push` du
schéma composé, même trajet par `prismaAdapter`, colonne et table écrites.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Seul `moi.lire` échappait à la permission, en dur. Le hub, pour ses routes
« pour soi » (sessions, audit, consentement), n'avait que `@Public()` plus un
contrôle à la main, ou `@Permission()` vers un droit donné à tous — écart de
`auth.route-sans-decision` (L-028, L-033).

- `@Authentifie()` (Nest) et `withApi({ authentifie: true })` / `garder` (Next) :
  identité exigée (401 sans), aucune permission ; `@SessionSeule()` se combine ;
- décision énumérée `authentifiee`, acceptée par `auth.route-sans-decision` ;
  `@Permission()` croisé est un écart qui dit de la retirer ;
- contrat Next : `x-eqs-authentifie` ; les deux harnais écartent ces routes de
  `auth.403-permission`, comme `moi.lire` ;
- `/moi` de la plateforme porte `@Authentifie()` ; `LIBRES` reste.

Déjà sur `main` : les dispenses s'appliquent dans les harnais (#149) ; le
message de l'écart la nomme désormais.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Le hub écrit des lignes d'audit depuis ses crochets better-auth ; better-auth,
monté hors de Nest, ne voit que les en-têtes de la requête, et
`requestIdMiddleware` ne posait l'identifiant que sur `req.requestId` et la
réponse. Sans `x-request-id` du client, l'audit n'avait pas l'identifiant
de la réponse et des journaux.

- `requestIdMiddleware` réécrit l'en-tête de la requête avec l'identifiant
  retenu — et un identifiant refusé n'est plus re-tiré au hasard à chaque
  `resoudreRequestId` de la même requête (même classe, trouvée en cherchant) ;
- `authRoute()` (Next) le pose sur la requête passée à better-auth ;
- `requestIdDe(ctx)` le lit dans un crochet.

Prouvé : un crochet lit l'identifiant fourni et, sans en-tête, celui que la
réponse annonce (rouge sans la réécriture) ; sous Next, better-auth reçoit
le même, corps intact.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Le hub réécrivait le corps de `sign-in/social` par un `fetchImpl` pour y
glisser `oauth_query`, recopiait l'extraction des paramètres signés, et sa
page `/connexion?echec=…` n'était alimentée par rien : `errorCallbackURL`
n'était jamais posé.

- `urlDeConnexion` et `SignIn` : `oauthQuery` (→ `oauth_query`, sur
  `sign-in/social` et `sign-in/email` ; par mot de passe, la connexion suit
  l'adresse que better-auth rend) et `retourEchec` (→ `errorCallbackURL`,
  filtré par `retourSur`) ;
- `repriseOAuth(recherche)` : les seuls paramètres signés ;
- `PageConnexion({ reprendreOAuth: true })`.

Absents, le corps de toujours (testé). Un `retourEchec` hors du site n'est pas
transmis, tabulation comprise.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Plan du hub A-7. Jeton du hub : `at+jwt` EdDSA, `iss` = le hub, `aud` = le
service, `scope` = permissions natives, `oid`, `act: { sub, typ: 'agent' }`.

- `DepsPrincipal.emetteurs` : `resoudrePrincipal` lit l'`iss` sans vérifier,
  pour choisir qui vérifiera, et confie le jeton à cet émetteur : signature
  contre SES clés, `typ`, EdDSA, expiration (401) ; audience (403) ; la
  personne retrouvée ICI (`utilisateurParOid`, compte Microsoft ; 403 si
  inconnue ou suspendue) ; permissions = rôles d'ici ∩ portée ;
  `Principal.agent = { clientId, scopes }`. Un `iss` inconnu : Entra, sinon 401 ;
- `HUB_ISSUER`, `HUB_AUDIENCE` (défaut `PUBLIC_URL`), `HUB_JWKS_URL` (défaut :
  découverte RFC 8414, un `issuer` différent annoncé est refusé), deux règles
  bloquantes ; `auth.hub` (jwks, utilisateur, now) dans `createEqsApp` et
  `creerApi` ;
- conformité : `auth.jeton-hub-emetteur` (401), `-audience` (403), `-portee`
  (403), sans objet sans `HUB_ISSUER` ; `hubDeTest()` dans `@eqs/testing`.

Sans `HUB_ISSUER`, rien ne change (testé). Prouvé : 18 cas sur
`resoudrePrincipal` dont la découverte sur un serveur local ; de bout en bout
sur `createEqsApp` et `creerApi` ; les trois contrôles verts contre un service
qui accepte le hub, l'audience rouge contre un qui ne l'accepte pas.

En montant : `HUB_ISSUER=`, `HUB_AUDIENCE=`, `HUB_JWKS_URL=` dans `.env.example`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ce qui est sur la branche (D-067 à D-081), ce que `main` avait déjà réglé
(#145, #137, #149), ce qu'un satellite a à faire en montant (six variables
dans `.env.example`), et ce qui reste ouvert : les hooks en 644, le harnais
next qui ne voit pas `HUB_ISSUER`, le script de schéma du gabarit, Redis
absent de la CI.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ETAT.md : la section de la branche du hub garde sa place, suivie du titre
et du paragraphe de dev (0.23.0 publiée et promue).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <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!158
No description provided.