docs(knowledge): capitalisation backend — intégration du triage local (mai-juin 2026)

Triage et intégration des propositions backend du buffer 95_a_capitaliser.md
(lot local RL799_V2 + app-alexandrie, mai-juin 2026), distinct de la capitalisation
remote antérieure (triage 2026-05-02).

~73 entrées intégrées sur knowledge/backend/, dont :
- patterns/auth.md : série "membrane d'auth fédérée BFF/OIDC" (9 patterns) + jose algo whitelist
- patterns/prisma.md : recette fusionnée "Migration String/Int → enum" (backfill + Cas A/B/C),
  row réactivable, endpoint replace atomique, updateMany conditionnel, etc.
- risques/general.md : 19 risques (epoch s vs ms, keepAliveTimeout=0, upsert+filtre liste,
  fail-safe catch-all, retrait asymétrique front/back, anti-énumération rate-limit, etc.)
- patterns/general, async, nestjs, contracts, tests + risques/auth, contracts, prisma, redis, stripe, tests
- compléments d'entrées existantes (authorize-after-fetch, P3014, cursor opaque, DI swc, Stripe v20...)
- README patterns/risques mis à jour

Doublons internes corrigés en relecture (suppression-champ .map() → general seul ;
e2e DB-based → tests.md seul). Doublons hors backend / entrées projet / rejets non intégrés.
Source 95_a_capitaliser.md non purgée à ce stade (purge en fin de capitalisation complète).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MaksTinyWorkshop
2026-06-25 11:25:02 +02:00
parent ef24d85d57
commit f1b783407a
18 changed files with 2896 additions and 24 deletions
+76 -1
View File
@@ -307,6 +307,21 @@ it('retourne 403 si subscription inactive', async () => {
- Contexte technique : auth / refresh token — RL799_V2 08-04-2026
### Complément — rotation du refresh token IdP en BFF : cookie rafraîchi non réécrit → déconnexions erratiques
Angle distinct mais lié à la rotation : en archi BFF, si le cookie de session rafraîchi (`sessionCookieToApply`) n'est réécrit que par UN seul handler (typiquement `/me`), la rotation du refresh token côté IdP déconnecte les utilisateurs de façon erratique.
- Les N autres call-sites d'auth déclenchent bien le refresh (access token rafraîchi en mémoire → requête autorisée → 200) mais JETTENT le nouveau cookie.
- Tant que l'IdP n'a PAS la rotation activée, c'est inoffensif (l'ancien refresh token reste valide). MAIS si le realm a `Revoke Refresh Token` / rotation activée (durcissement prod COURANT chez Keycloak/Auth0/etc.), chaque refresh INVALIDE l'ancien refresh token côté IdP : le cookie non réécrit garde un refresh token révoqué → la requête suivante échoue → `SESSION_EXPIRED` → re-login forcé.
- **Piège** : INVISIBLE en dev (rotation souvent off par défaut), il n'apparaît qu'au déploiement quand un ops active la rotation pour durcir.
Règles :
1. Si la réécriture du cookie n'est pas généralisée à TOUS les handlers (via un wrapper qui attache `sessionCookieToApply` systématiquement), alors NE PAS activer la rotation du refresh token côté realm — et le documenter comme garde-fou de déploiement explicite.
2. Inversement, si on veut la rotation (recommandé en sécurité), généraliser la réécriture du cookie AVANT.
3. Ne jamais traiter « le refresh marche en dev » comme preuve que la rotation marchera en prod — tester avec la rotation activée.
- Cas vécu : RL799 K1.5, seul `/me` consomme `sessionCookieToApply`, ~202 autres call-sites l'ignorent ; garde-fou « pas de rotation realm avant généralisation » renvoyé au Lot 6 déploiement — 15-06-2026.
---
<a id="risque-drift-auth-copier-coller"></a>
@@ -331,6 +346,16 @@ it('retourne 403 si subscription inactive', async () => {
- Contexte technique : auth / architecture — RL799_V2 08-04-2026
### Complément — cohérence des filtres d'autorisation entre TOUS les chemins ciblant la même population
Le drift ne touche pas que les codes d'erreur : il touche aussi les FILTRES appliqués sur la même population résolue à plusieurs endroits.
- Quand une même population (ex. « les membres actifs d'un grade ») est résolue à plusieurs endroits — un chemin de NOTIFICATION qui filtre `isActive: true` et un chemin d'AUTORISATION qui fait `getUserByEmail` sans filtre `isActive` — la divergence crée une faille : un compte désactivé/démissionnaire avec un JWT encore valide (fenêtre ≤ TTL) n'est pas notifié MAIS peut encore agir.
- **Règle** : tout contrôle d'autorisation basé sur un fetch user doit re-vérifier `isActive` à chaque requête (le JWT ne reflète pas une désactivation survenue après émission).
- **Audit** : grep des `getUserByEmail` / `findUser*` dans les services, vérifier que chaque usage en contexte d'autorisation filtre/contrôle `isActive`.
- **Symptôme de l'incohérence** : « la liste des destinataires d'un effet et la liste des autorisés à le déclencher ne coïncident pas ».
- Cas vécu : isolation de réponse aux instructions RL799 — le fetch DB avait été ajouté EXPRÈS pour capter les changements d'état à chaque requête, mais ignorait `isActive`, annulant le bénéfice.
---
<a id="risque-auth-acl-unique-champ-sensible"></a>
@@ -494,4 +519,54 @@ if (user.deletedAt !== null) {
- **Règle** : dans `login()`, toujours répondre `INVALID_CREDENTIALS` pour un compte soft-deleted — jamais un code spécifique.
- **Nuance** : un code `ACCOUNT_DELETED` reste acceptable dans un flux `exchange()` OAuth, où le provider a déjà confirmé l'identité (pas d'énumération possible côté attaquant).
- Contexte technique : auth / soft-delete / anti-énumération — app-alexandrie 13-04-2026
- Contexte technique : auth / soft-delete / anti-énumération — app-alexandrie 13-04-2026
---
<a id="risque-guard-abonnement-vs-droit-acquis"></a>
## Guard d'abonnement global vs droits acquis permanents
### Risques
- Un guard de gating « abonnement actif » (ex. `RequireSubscriptionActive` / `RequireAccessLevel(FULL)`) posé uniformément sur TOUTES les routes d'un domaine coupe l'accès à un contenu déjà payé en one-shot (« possession à vie ») dès que l'abonnement est résilié
- Violation silencieuse d'un invariant métier : « je garde ce que j'ai payé même sans abo »
### Symptômes
- Couper l'abonnement rend inaccessible un contenu acheté de façon permanente
- Aucun test ne couvre le cas « droit permanent + abo coupé » → régression non détectée
### Bonnes pratiques / mitigations
- Avant d'appliquer un guard « abonnement actif » uniformément, distinguer deux natures de droit :
- **droit RÉCURRENT** (lié à l'abo : feed, communauté, contenu inclus)
- **droit ACQUIS/permanent** (achat one-shot, possession « à vie »)
- **Règle** : gater la LECTURE d'un bien acquis par la POSSESSION (helper `canAccess…`), pas par l'abonnement. Réserver le guard abo aux routes d'écriture/progression et aux contenus récurrents.
- TOUJOURS écrire un test « bien possédé + abo coupé → lisible » : c'est l'angle mort classique qui laisse passer ce type de régression.
- Contexte technique : auth / gating abonnement — app-alexandrie 02-06-2026
---
<a id="risque-validite-jeton-vs-duree-acces"></a>
## Confondre la validité du JETON d'octroi avec la durée de l'ACCÈS octroyé
### Risques
- Un helper d'accès lit le `expiresAt` d'un jeton d'octroi (code de déblocage, lien/token d'invitation) comme SOURCE D'ACCÈS directe
- Mais `expiresAt` borne la fenêtre d'ACTIVATION du jeton (ex. 72 h), pas la durée de l'accès octroyé (censé être permanent) → l'accès expire en même temps que le jeton
### Symptômes
- L'accès « à vie » expire 72 h après l'émission du code
- Bug non détecté par les tests (qui valident le helper tel qu'écrit, pas l'intention)
### Bonnes pratiques / mitigations
- Ne JAMAIS faire dépendre la vérification d'accès du `expiresAt` du jeton.
- À l'activation, **matérialiser l'accès dans son entité propre** (ex. `UserPack`/possession) et vérifier l'accès via CETTE entité — pas via le jeton.
- **Règle** : « le jeton expire, le droit qu'il a créé persiste. »
- Test obligatoire : « jeton activé puis expiré → l'accès reste valide ».
- **Corollaire** : un helper d'accès ne doit pas « anticiper » un mécanisme pas encore implémenté en lisant un état intermédiaire — il introduit un modèle d'accès parallèle qui diverge du modèle cible (la branche aurait dû passer par `UserPack` dès le départ).
- Contexte technique : auth / activation vs possession — app-alexandrie 02-06-2026