capitalisation: triage 95_a_capitaliser + création domaine infra

Triage des 27 propositions du buffer de capitalisation (skill
capitalisation-triage), avec vérification des doublons contre la base.

Intégré dans knowledge/ (23 entrées):
- backend: redis (compensation incrBy non-atomique), nestjs (injection
  cassée sous tsx watch; guard write mode dégradé), async (test rollback
  pipeline multi-fichiers), contracts (idempotence POST), auth (disclosure
  comptes soft-deleted), prisma (index partial soft-delete), llm-providers
  (nouveau: OAuth vs API key, prompt caching).
- frontend: tests (garde-fous parking Later), navigation (fichiers
  non-route sous src/app Expo Router), general (type client vs payload
  backend), state (fallback catch-all mapping DB→UI).
- workflow: story-tracking (statut BMAD vs narratif obsolète).
- product: general (nouveau: doc feature store sans UI).
- infra: NOUVEAU DOMAINE (traefik, tailscale, docker, docker-networking,
  reverse-proxy-paths, sidecar tailscale) + 00_INDEX.md.

Autres:
- 90_debug_et_postmortem.md: post-mortem réseau Docker partagé hors compose.
- Rejeté 3 doublons (types enum contracts, getter PrismaService, $transaction).
- Buffer 95_a_capitaliser.md purgé et restauré à son état initial.
- _projects.conf: MAJ statuts epics + ajout app-rl799.
This commit is contained in:
MaksTinyWorkshop
2026-06-25 10:31:22 +02:00
parent 1c876309f1
commit ef24d85d57
31 changed files with 1042 additions and 27 deletions
+4 -4
View File
@@ -8,12 +8,12 @@ Avant toute proposition backend, identifie le fichier dont le nom et la descript
| Fichier | Domaine | Entrées clés |
|---------|---------|--------------|
| `auth.md` | Auth, sessions, guards, accès | AuthN/AuthZ dispersée, guard global manquant, null-check request.user, AdminRoleGuard sans @RequireAdminRole, GET sans contrôle accès, cookie après révocation, mock session sans expiresAt, buildApp partagé e2e, champ absent JWT, email login vs contact |
| `auth.md` | Auth, sessions, guards, accès | AuthN/AuthZ dispersée, guard global manquant, null-check request.user, AdminRoleGuard sans @RequireAdminRole, GET sans contrôle accès, cookie après révocation, mock session sans expiresAt, buildApp partagé e2e, champ absent JWT, email login vs contact, disclosure comptes soft-deleted dans login() |
| `contracts.md` | Contrats, validation, codes erreur | Contrats implicites, erreurs non standardisées, duplication constantes, schema orphelin, code erreur générique 409, ForbiddenException pour validation, process.env direct, statut métier non propagé |
| `prisma.md` | Prisma, DB, transactions, migrations | @unique nullable, TOCTOU transaction, OR tenantId null, nextOrder race condition, tenantId sans FK, schema divergence spec, getter manquant, init module build, clearAllMocks imbriqué, cursor non validé, enum-like String, migration manuelle hors git, relation 1:1 sans unique |
| `prisma.md` | Prisma, DB, transactions, migrations | @unique nullable, TOCTOU transaction, OR tenantId null, nextOrder race condition, tenantId sans FK, schema divergence spec, getter manquant, init module build, clearAllMocks imbriqué, cursor non validé, enum-like String, migration manuelle hors git, relation 1:1 sans unique, index partial soft-delete (perf) |
| `stripe.md` | Stripe, paiements, webhooks, subscriptions | billing_cycle_anchor vs current_period_end, list() sans has_more, concurrence trial→payant, non-idempotence, 200 pendant processing |
| `nestjs.md` | NestJS, controllers, providers | TooManyRequestsException NestJS 11, controller corrompu insertions, repository dead layer, interface provider incomplète |
| `redis.md` | Redis, cache, quotas, TTL | Thrash connexion sous charge, entitlements TTL > SLA, compteurs in-memory, TTL heure locale ±12h |
| `nestjs.md` | NestJS, controllers, providers | TooManyRequestsException NestJS 11, controller corrompu insertions, repository dead layer, interface provider incomplète, guard multi-statut READ_METHODS, bootstrap OK mais injection cassée (tsx watch), guard écriture mode dégradé bloque le support |
| `redis.md` | Redis, cache, quotas, TTL | Thrash connexion sous charge, entitlements TTL > SLA, compteurs in-memory, TTL heure locale ±12h, compensation incrBy non-atomique (quota fantôme) |
| `nextjs.md` | Next.js, build, routing | Prisma init au chargement module, server-only dans repositories, redirect boucle infinie feature flags, dossiers `_*` exclus du routing App Router |
| `general.md` | Observabilité, migrations, performance, architecture | Observabilité insuffisante, migrations non reproductibles, upsert N+1, authorize-after-fetch, valeur sentinelle DTO, idempotence endpoint, fichier orphelin, mélange Date UTC/locale, champ fantôme Zod, catch vide, params non validés, cast TS brut, chevauchement temporel, TOCTOU, biais agrégation, couplage types erreur, service HTTP-aware, count sans filtre, env top-level, dérive DTO liste vs détail, notification linkUrl rôle-aware, matrice documentée vs code, format `User.id` mixte, Web Push topic > 32 chars, lib npm types non embarqués, form HTML POST dans un mail, env vars frontend-facing fail-fast |
| `tests.md` | Isolation des tests d'intégration | `vi.stubEnv` sans restauration, `maxWorkers: 1` masque l'isolation, flakiness inter-fichiers DB partagée |
+39 -3
View File
@@ -2,10 +2,10 @@
title: Backend — Risques & vigilance : Auth
domain: backend
bucket: risques
tags: [auth, guards, request-user, sessions, admin]
tags: [auth, guards, request-user, sessions, admin, enumeration, soft-delete]
applies_to: [implementation, review, debug]
severity: high
validated_on: 2026-04-07
validated_on: 2026-06-25
source_projects: [app-alexandrie, RL799_V2]
---
@@ -458,4 +458,40 @@ Quand on prévoit de supprimer un flag auth profondément câblé :
**Anti-pattern** : déprécier en douceur en gardant le flag avec un commentaire `// @deprecated` sans supprimer les usages. Le code mort s'accumule, les futurs devs hésitent à le nettoyer ("pourquoi c'est encore là ?"), la dépréciation ne se finit jamais.
- Contexte technique : auth / refactor schema — RL799_V2 28-04-2026
- Contexte technique : auth / refactor schema — RL799_V2 28-04-2026
---
<a id="risque-disclosure-comptes-soft-deleted-login"></a>
## Information disclosure sur comptes soft-deleted dans `login()`
### Risques
- Retourner un code d'erreur distinct (ex : `ACCOUNT_DELETED`) pour les comptes supprimés dans `login()` permet l'**énumération** : un attaquant distingue « compte supprimé » de « identifiants invalides » par email.
- Complément du pattern anti-énumération (cf. `pattern-...anti-énumération` dans `patterns/auth.md`) appliqué au cas soft-delete.
### Symptômes
- `login()` lève `ACCOUNT_DELETED` au lieu de `INVALID_CREDENTIALS` pour un compte soft-deleted
- L'existence (et le statut) d'un compte fuite via la réponse d'authentification
### Bonnes pratiques / mitigations
```typescript
// ❌ DANGEREUX — révèle l'existence d'un compte supprimé
if (user.deletedAt !== null) {
throw new UnauthorizedException({ error: { code: 'ACCOUNT_DELETED' } });
}
// ✅ CORRECT — même code que des identifiants invalides
if (user.deletedAt !== null) {
throw new UnauthorizedException({
error: { code: 'INVALID_CREDENTIALS', message: 'Email ou mot de passe invalide.' },
});
}
```
- **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
+49 -2
View File
@@ -2,10 +2,10 @@
title: Backend — Risques & vigilance : NestJS
domain: backend
bucket: risques
tags: [nestjs, controllers, guards, providers, review]
tags: [nestjs, controllers, guards, providers, review, injection, tsx-watch, mode-degrade]
applies_to: [implementation, review, debug]
severity: high
validated_on: 2026-03-30
validated_on: 2026-06-25
source_projects: [app-alexandrie]
---
@@ -144,3 +144,50 @@ if (user.sessionStatus === 'BLOCKED') throw new HttpException(...);
- **Règle** : définir une whitelist explicite des writes autorisés même en état bloqué.
- Contexte technique : NestJS / guard statut — app-alexandrie 30-03-2026
---
<a id="risque-bootstrap-ok-injection-cassee-tsx-watch"></a>
## Bootstrap NestJS OK mais injection runtime cassée sous `tsx watch`
### Risques
- En dev, un serveur NestJS peut afficher `Nest application successfully started` et exposer ses routes, tout en cassant à la **première requête** : des dépendances injectées (`Reflector`, services consommés par les guards globaux) arrivent à `undefined` au runtime.
- Le symptôme initial ressemble à un problème réseau/mobile alors que la cause réelle est l'injection backend.
### Symptômes
- `GET /` ou une route d'auth répond `500` alors que le bootstrap est nominal
- Stack runtime du type `Cannot read properties of undefined (reading 'getAllAndOverride')`
- Les guards globaux (`AuthGuard`, guards basés sur `Reflector`) sont les premiers à tomber
### Bonnes pratiques / mitigations
- Ne pas conclure trop vite à un problème réseau dès que le port écoute : traiter « le port écoute » et « les requêtes fonctionnent » comme **deux validations distinctes**.
- Tester immédiatement `GET /`, l'endpoint OpenAPI et une route publique métier, puis lire la stack runtime **après le premier hit** (pas seulement les logs de bootstrap).
- Si un lanceur `tsx watch` est utilisé avec NestJS, vérifier explicitement la compatibilité avec l'injection runtime ; en cas de doute, expliciter les injections critiques avec `@Inject(...)` sur guards et services exposés dès le premier hit.
- Contexte technique : NestJS / tsx watch / injection — app-alexandrie 01-04-2026
---
<a id="risque-writemodeguard-bloque-endpoints-support"></a>
## Guard d'écriture en mode dégradé bloque les endpoints de support
### Risques
- Un guard global qui refuse toutes les écritures (`POST/PUT/PATCH/DELETE`) quand un service critique est down (ex : Redis) bloque aussi les endpoints dont l'utilité est **maximale en panne** : soumission de ticket de support, feedback, remontée de logs d'erreur client.
- Cas typique : un endpoint de ticketing (`POST /support/...`) devient inaccessible précisément au moment où l'utilisateur veut signaler le dysfonctionnement qu'il subit.
### Symptômes
- Soumission de ticket de support impossible quand le service de quota/cache est indisponible
- Le guard rejette des routes qui devraient rester ouvertes en mode dégradé
### Bonnes pratiques / mitigations
- Lors de l'ajout d'un guard d'écriture en mode dégradé, **recenser explicitement les endpoints critiques qui doivent rester disponibles** : support, feedback, logs d'erreur client.
- Whitelister ces préfixes dès la création du guard — pas lors de la code review d'une story ultérieure. Exemple de whitelist : `/auth`, `/billing/.../webhooks`, `/support`.
- **Why** : un service de ticketing bloqué quand le backend dysfonctionne est contre-productif — l'utilisateur ne peut pas signaler le problème qu'il est en train de subir.
- Contexte technique : NestJS / guard mode dégradé — app-alexandrie 01-04-2026
+32 -2
View File
@@ -2,10 +2,10 @@
title: Backend — Risques & vigilance : Prisma
domain: backend
bucket: risques
tags: [prisma, transactions, tenant, schema, race-condition]
tags: [prisma, transactions, tenant, schema, race-condition, index, soft-delete, performance]
applies_to: [implementation, review, debug, architecture]
severity: high
validated_on: 2026-04-07
validated_on: 2026-06-25
source_projects: [app-template-resto, app-alexandrie, RL799_V2]
---
@@ -619,3 +619,33 @@ const resolveDbUrl = (): string | undefined => {
**Règle générale** : toute stratégie template-based doit auditer le chemin du `DB_URL` à travers les sub-processes de bootstrap. Le bootstrap ouvre une connexion sur la template, mais le seed transitif exécuté via un sub-process peut être sujet à des transformations agressives du DSN qui le redirigent ailleurs.
- Contexte technique : Prisma / template database / Vitest — RL799_V2 01-05-2026
---
<a id="risque-index-partial-soft-delete-perf"></a>
## Index partial sur colonnes soft-delete (nuance perf)
### Risques
- Un index plein sur une colonne soft-delete nullable (`deleted_at`) indexe **tous les NULL**, c'est-à-dire la quasi-totalité des lignes (les comptes actifs). L'index est volumineux et peu sélectif pour les requêtes qui ne ciblent que les lignes supprimées.
- Distinct de l'index **unique** partiel déjà documenté (cf. `risque-prisma-unique-nullable`, qui traite de l'idempotence) : ici l'enjeu est purement la **performance / le coût d'indexation**.
### Symptômes
- `CREATE INDEX ... ON ("deleted_at")` sans clause `WHERE` sur une table où >99 % des lignes ont `deleted_at IS NULL`
- Index gros pour un bénéfice quasi nul sur les requêtes « lister les comptes supprimés »
### Bonnes pratiques / mitigations
```sql
-- ❌ Index plein — indexe la masse des NULL (lignes actives)
CREATE INDEX "users_deleted_at_idx" ON "users"("deleted_at");
-- ✅ Index partial — n'indexe que les lignes effectivement supprimées
CREATE INDEX "users_deleted_at_idx" ON "users"("deleted_at")
WHERE "deleted_at" IS NOT NULL;
```
- **Règle** : pour une colonne soft-delete nullable à majorité `NULL`, préférer un index partial `WHERE deleted_at IS NOT NULL`.
- Contexte technique : Prisma / PostgreSQL / index partial — app-alexandrie 13-04-2026
+38
View File
@@ -103,3 +103,41 @@ endOfDay.setHours(23, 59, 59, 999); // dérive selon TZ serveur
- Règle : tout `expireAt` ou `TTL` de quota journalier doit utiliser `Date.UTC()` — vérifier systématiquement en review
- Contexte technique : Redis / NestJS — app-alexandrie 20-03-2026
---
<a id="risque-compensation-incrby-non-atomique"></a>
## Compensation `incrBy(-1)` non-atomique après dépassement de quota
### Risques
- Pattern courant de quota Redis : `INCR` → vérifier `> limit` → si oui, décrémenter (`incrBy(-1)`) et lever 429. La compensation n'est PAS atomique avec le check.
- Si Redis devient indisponible (flap) entre l'incrément et la compensation, l'appel de décrément échoue silencieusement et retourne `null`. Le compteur reste incrémenté → quota fantôme jusqu'à l'expiration de la clé.
### Symptômes
- Utilisateur bloqué par le quota alors qu'il n'a pas atteint la limite réelle
- Aucune erreur visible : la valeur de retour `null` du décrément n'est pas vérifiée
- Comportement intermittent corrélé aux instabilités Redis
### Bonnes pratiques / mitigations
```typescript
// ❌ Compensation non vérifiée — échec silencieux possible
await redis.incr(key);
if (current > limit) {
await redis.incrBy(key, -1); // retourne null si Redis flap → compensation perdue
throw quotaExceeded();
}
// ✅ Vérifier le retour de la compensation et loguer
const compensated = await redis.incrBy(key, -1);
if (compensated === null) {
logger.error({ type: 'quota', event: 'compensation_failed', key });
}
```
- **Règle** : toujours vérifier le retour du décrément de compensation et loguer explicitement si `null`. Documenter ce choix dans les Dev Notes de la story (comportement intentionnel vs bug).
- **Solution robuste** : encapsuler incrément + compensation conditionnelle dans le même pipeline `MULTI/EXEC` ou un script Lua (atomicité garantie), au prix d'une complexité plus élevée.
- Contexte technique : Redis / NestJS — app-alexandrie 01-04-2026