mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-06-28 01:53:40 +02:00
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:
@@ -597,3 +597,278 @@ Trigger : `workflow_run` du workflow `Tests` quand vert sur main, OU `workflow_d
|
||||
4. **Permissions dossier `/srv/sites/<app>/`** : si owner historique = autre user, `deploy` ne peut pas `scp`. Solution propre = groupe partagé avec `setgid`.
|
||||
|
||||
5. **Healthcheck timeout** : si l'API met longtemps à boot (Chromium/Puppeteer lazy load, migrations longues), augmenter au-delà du défaut 60 s.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-controle-acces-conditionnel-contexte-metier"></a>
|
||||
## Pattern : Contrôle d'accès conditionnel par contexte métier (pure logic shared)
|
||||
|
||||
- Objectif : décider l'accès à une ressource selon une **capacité dérivée** du user (pas seulement son rôle/grade) + un **contexte d'usage** porté par la ressource, avec une logique unique et testable, partagée front et back.
|
||||
- Contexte : ressource (typiquement un Document) avec accès inconditionnel pour certains rôles de curation (archiviste/admin) ET accès conditionnel pour les autres, basé sur une capacité du user (`canInvestigate`, `canTreasure`, …) et un contexte porté par la ressource (`'enquete:template'`, `'officier:mdc:memento'`).
|
||||
- Quand l'utiliser : l'accès dépend d'un contexte métier extensible et doit être calculable côté frontend (afficher/masquer un bouton sans round-trip) ET côté backend (gate d'autorisation).
|
||||
- Quand l'éviter : accès purement rôle/grade → un helper RBAC standard suffit.
|
||||
- Avantage :
|
||||
- une seule source de vérité, pas de divergence front/back possible
|
||||
- extensible sans cascade : un nouveau contexte = un `case` dans le `switch` shared
|
||||
- testable en pure logic (matrice de tests sans Prisma ni mocks)
|
||||
- allow-list strict : contexte non implémenté = `false` par défaut (pas de fuite d'autorisation)
|
||||
- Limites / vigilance :
|
||||
- défense en profondeur obligatoire : gater AUSSI le listing (`GET /documents?type=...`), pas seulement le `view`, sinon la ressource leak via la liste
|
||||
- `usageContext String?` simple en V1 ; migrer vers `String[]` seulement si un match multiple devient nécessaire (anticiper en `String[]` est de la sur-ingénierie)
|
||||
- n'envisager un service générique paramétré (`canUserViewContextualResource`) qu'à partir du 3ᵉ usage
|
||||
- Validé le : 06-05-2026
|
||||
- Contexte technique : Prisma / packages shared / Next.js + Vue — RL799_V2
|
||||
|
||||
### Forme du pattern
|
||||
|
||||
**1. Sur la ressource : champ `usageContext`** (NULL = pas de gate contextuelle, la ressource utilise les gates standard de son type).
|
||||
|
||||
```prisma
|
||||
model X {
|
||||
/// Contexte métier qui conditionne l'accès. NULL = pas de gate contextuelle.
|
||||
usageContext String? @map("usage_context")
|
||||
}
|
||||
```
|
||||
|
||||
**2. Service shared `canUserViewX(ctx, resource)` — pure logic**, exporté depuis `packages/shared` pour être consommé par l'API et le frontend.
|
||||
|
||||
```typescript
|
||||
export type XViewerContext = {
|
||||
role: UserRole;
|
||||
capabilities: { canInvestigate: boolean /* … */ };
|
||||
};
|
||||
|
||||
export function canUserViewX(
|
||||
ctx: XViewerContext,
|
||||
resource: { type: string; usageContext: string | null },
|
||||
): boolean {
|
||||
if (resource.type !== 'X_TYPE_GATED') return true; // hors scope gate
|
||||
if (ctx.role === 'archiviste' || ctx.role === 'admin') return true; // curation
|
||||
if (!resource.usageContext) return false; // orpheline → curation-only
|
||||
switch (resource.usageContext) {
|
||||
case 'enquete:template':
|
||||
return ctx.capabilities.canInvestigate;
|
||||
default:
|
||||
return false; // allow-list strict
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. Côté API** : la gate appelle `canUserViewX` dans le handler de view → `403 FORBIDDEN` si refusé.
|
||||
**4. Côté frontend** : la même fonction calcule le rendu conditionnel (`v-if="canSeeHelper"`), zéro round-trip.
|
||||
|
||||
### Anti-patterns à éviter
|
||||
|
||||
- Logique backend-only : force le frontend à un round-trip pour savoir s'il affiche un bouton.
|
||||
- Champ `accessRoles: string[]` sur la ressource : ne capture pas une capacité dérivée (`canInvestigate` n'est pas un rôle).
|
||||
- `switch` éparpillé dans plusieurs handlers : centraliser dans un seul service shared.
|
||||
- Gate uniquement sur `view` sans gater le listing : fuite par la liste.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-safe-path-resolution-streaming-db-driven"></a>
|
||||
## Pattern : Safe path resolution pour streaming de fichiers DB-driven (anti path traversal)
|
||||
|
||||
- Objectif : empêcher un path traversal (`../../../etc/passwd`, chemin absolu hors racine) lors du streaming d'un fichier dont le chemin est stocké en DB, même si la requête vient d'un user authentifié et autorisé.
|
||||
- Contexte : endpoint qui streame un fichier disque dont le path vient de la DB (`Document.filePath`, `ConvocationIssue.pdfPathsByGrade`, …), en particulier quand le champ est un JSON libre côté Prisma que Zod ne peut pas valider strictement à l'écriture.
|
||||
- Quand l'utiliser : tout streaming de fichier dont le chemin n'est pas une constante du code.
|
||||
- Quand l'éviter : fichier servi depuis un chemin entièrement contrôlé par le code (constante, pas d'entrée DB).
|
||||
- Avantage :
|
||||
- défense en profondeur : protège même si la DB est compromise (row corrompue, migration foireuse, accès SQL direct d'un attaquant)
|
||||
- re-validation à la LECTURE qui couvre les rows insérées avant l'existence d'un schéma Zod
|
||||
- Limites / vigilance :
|
||||
- retourner `500` (+ `console.error`) et non `404` : un path hors racine est un bug applicatif/compromission, pas une erreur user — à surveiller via les logs prod
|
||||
- utiliser `path.relative`, **jamais** `String.startsWith(ROOT)` (casse avec les liens symboliques)
|
||||
- un blocage Zod en amont est insuffisant seul : Zod valide à l'écriture, pas à la lecture
|
||||
- Validé le : 06-05-2026
|
||||
- Contexte technique : Node.js / Prisma — RL799_V2
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
import { isAbsolute, join, normalize, relative } from 'node:path';
|
||||
|
||||
const SAFE_ROOT = '/srv/uploads/x';
|
||||
|
||||
const safeResolvePath = (storedPath: string): string | null => {
|
||||
const absolute = isAbsolute(storedPath) ? storedPath : join(SAFE_ROOT, storedPath);
|
||||
const normalized = normalize(absolute); // résout les `..`
|
||||
const rel = relative(SAFE_ROOT, normalized);
|
||||
if (rel.startsWith('..') || isAbsolute(rel)) return null; // s'évade de SAFE_ROOT
|
||||
return normalized;
|
||||
};
|
||||
|
||||
// Dans le handler :
|
||||
const absolutePath = safeResolvePath(storedPath);
|
||||
if (!absolutePath) {
|
||||
console.error('[handler] path stocké hors SAFE_ROOT:', storedPath);
|
||||
return errorResponse(500, 'FILE_NOT_FOUND', 'Fichier indisponible');
|
||||
}
|
||||
```
|
||||
|
||||
### Anti-patterns
|
||||
|
||||
- `path.join(ROOT, userInput)` puis `fs.readFile` sans normalisation : `'../../../etc/passwd'` passe.
|
||||
- `resolved.startsWith(ROOT)` : casse avec les symlinks — `path.relative` est la bonne API.
|
||||
- "Trust DB" ("c'est moi qui écris") : ignore le bug de migration et l'attaquant à accès SQL direct.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-alias-import-convention-migration"></a>
|
||||
## Pattern : Alias d'import `@/*` — convention et migration en masse
|
||||
|
||||
- Objectif : remplacer les imports relatifs profonds (`../../X`) par un alias `@/X` résolu depuis la racine `src/`, et migrer un projet établi sans casser les exceptions.
|
||||
- Contexte : projet (monorepo ou non) à 200+ fichiers où la profondeur des remontées relatives varie au fil du temps.
|
||||
- Quand l'utiliser : convention à graver tôt ; migration en masse d'un existant.
|
||||
- Quand l'éviter : voisins directs (un seul `../`) — les forcer en `@/` n'apporte que du bruit dans le diff.
|
||||
- Avantage :
|
||||
- refactor-friendly : déplacer un fichier dans `src/` ne casse aucun import en aval
|
||||
- lisibilité (`@/services/X` dit où on va, `../../../services/X` dit combien on remonte)
|
||||
- linter-friendly (`import/no-relative-parent-imports`)
|
||||
- Limites / vigilance :
|
||||
- `tsconfig.json` ET la config bundler doivent toutes deux connaître l'alias, sinon erreurs runtime obscures
|
||||
- typecheck + tests OBLIGATOIRES immédiatement après un sed de masse (faux positifs, fichiers hors `src/`)
|
||||
- Validé le : 11-05-2026
|
||||
- Contexte technique : monorepo pnpm (Next.js 16 + Vue 3.5 + Vite 7) — RL799_V2
|
||||
|
||||
### Setup
|
||||
|
||||
```json
|
||||
// tsconfig.json
|
||||
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }
|
||||
```
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }
|
||||
```
|
||||
|
||||
Next.js : alias TS reconnu automatiquement. Vitest : reconnaît les paths TS automatiquement.
|
||||
|
||||
### Convention de migration
|
||||
|
||||
**À migrer** : tout import qui traverse ≥ 2 niveaux (`../../X` ou plus) ET pointe vers un fichier sous `src/`.
|
||||
|
||||
**À laisser en relatif** (exceptions) :
|
||||
1. Voisins directs (`./sibling`, `../sibling` un seul niveau).
|
||||
2. Fichiers hors `src/` (ex. `next.config.ts` à la racine de l'app) — laisser en relatif + commentaire JSDoc justifiant l'exception.
|
||||
3. `__dirname`-relatif dans les `.mjs` (`resolve(here, '../../../src/...')`) : c'est un chemin filesystem, pas un import.
|
||||
4. Imports vers un sous-fichier précis pour éviter un cycle réintroduit par le barrel `@/X/index.ts`.
|
||||
|
||||
### Migration en masse via sed (avec garde-fous)
|
||||
|
||||
```bash
|
||||
# Repérer les candidats
|
||||
grep -rE "from '\.\./\.\./" src/ --include='*.ts' --include='*.tsx' -l
|
||||
|
||||
# DRY-RUN d'abord (sans -i) pour relire le diff
|
||||
sed -E "s|from '(\.\./){2,}([^']+)'|from '@/\2'|g" src/services/foo.ts
|
||||
|
||||
# Appliquer après validation du dry-run
|
||||
find src -type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.vue' \) \
|
||||
-exec sed -i.bak -E "s|from '(\.\./){2,}([^']+)'|from '@/\2'|g" {} \;
|
||||
find src -name '*.bak' -delete
|
||||
```
|
||||
|
||||
Récupération après sed : `pnpm typecheck` → lire chaque import cassé → identifier les exceptions (fichiers hors `src/`) → `git checkout -p` ou correction manuelle.
|
||||
|
||||
### Anti-patterns à éviter
|
||||
|
||||
- Migrer les voisins directs "pour la cohérence" → bruit sans gain.
|
||||
- Migrer sans typecheck immédiat → l'exception se découvre en CI 3 commits plus tard.
|
||||
- Regex sed trop permissive (`s|\.\./|@/|g`) qui matche aussi `import.meta.url` ou des strings non-imports.
|
||||
|
||||
### Cas vécu
|
||||
|
||||
RL799_V2 (2026-05-11) : 246 imports migrés sur 124 fichiers côté `apps/api/`. Sed + typecheck → 1 seule erreur (`@/next.config`, fichier hors `src/`), corrigée à la main avec JSDoc. ~5 min de migration + 2 min de fix.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-test-invariant-structure-scan-statique"></a>
|
||||
## Pattern : Test d'invariant de structure — verrouiller par scan statique une propriété d'architecture
|
||||
|
||||
- Objectif : transformer en BUILD ROUGE une régression silencieuse sur une propriété architecturale correcte-par-construction (ex. "auth opt-in par route : une route est publique parce que son handler n'appelle aucun helper d'auth", "les handlers Z loggent toujours W").
|
||||
- Contexte : propriété de sécurité/correction vraie aujourd'hui par convention, mais vulnérable à la dérive (un dev colle `requireRoleAccess` dans un handler public par copier-coller → la route marche pour un membre loggé, casse pour un guest → régression silencieuse).
|
||||
- Quand l'utiliser : tout invariant architectural reposant sur une convention plutôt que sur un mécanisme dur (pas de middleware global, pas de contrainte DB).
|
||||
- Quand l'éviter : si l'invariant est déjà garanti par construction dure (middleware central, contrainte SQL) — le test devient redondant.
|
||||
- Avantage :
|
||||
- une régression de convention casse le build au lieu de passer en review
|
||||
- le scan statique n'a aucune dépendance runtime vivante
|
||||
- Limites / vigilance :
|
||||
- **prouver la mordacité** : un test d'invariant qui ne peut jamais échouer est un placebo
|
||||
- Validé le : 15-06-2026
|
||||
- Contexte technique : Vitest / scan statique `readFileSync` — RL799_V2 (K1.6 `keycloakGuestInvariant.test.ts`)
|
||||
|
||||
### Règles
|
||||
|
||||
1. **Scan statique récursif** (`readFileSync`, pas de runtime) sur la SURFACE pertinente. Pour l'auth : `route.ts` + imports DIRECTS de 1ᵉʳ niveau, **PAS** la fermeture transitive complète (un handler public importe légitimement des services partagés dont d'autres fonctions sont protégées → faux positifs). Documenter la granularité choisie et sa limite.
|
||||
2. **Garde anti-angle-mort DYNAMIQUE** : énumérer l'arborescence (`app/api/public/**`) plutôt qu'une liste figée → une nouvelle route entre AUTO dans le périmètre. Exceptions hors-arborescence : liste explicite versionnée + note assumant l'angle mort résiduel.
|
||||
3. **Checkpoint conscient** : une liste `KNOWN_*` comparée par égalité casse le build quand le périmètre change, forçant une DÉCISION humaine ("cette nouvelle route est-elle bien guest ?"). Ne pas le confondre avec la couverture de scan (c'est l'énumération dynamique qui couvre).
|
||||
4. **Invariant comportemental par SPIES non-appelés**, pas par code de réponse : injecter des hooks espions (`vi.fn` + `not.toHaveBeenCalled()`). NE PAS asserter sur un `error.code` qui peut être un homonyme métier (ex. `INVALID_TOKEN` domaine présence ≠ `INVALID_TOKEN` Keycloak). NE PAS `throw` dans le spy (un `catch` du handler pourrait l'avaler et masquer la violation).
|
||||
5. **CRITÈRE DE RÉUSSITE — prouver la mordacité** : en dev comme en revue, injecter temporairement une violation, vérifier que le test devient ROUGE avec un message pointant le fichier exact, puis retirer la probe.
|
||||
6. **Auto-documentation** : commentaire en tête expliquant POURQUOI (la propriété protégée) et COMMENT l'étendre sans casser l'esprit.
|
||||
|
||||
Modèle de référence : scan de couverture de catalogue (`auditCatalogCoverage` — toute action loggée doit figurer au catalogue).
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-script-ops-batch-fail-closed"></a>
|
||||
## Pattern : Script ops batch fail-closed — rapport actionnable, dry-run honnête, revue par exécution réelle
|
||||
|
||||
- Objectif : un script ops one-shot qui mute des données en masse via un service externe (migration d'identités, back-fill, réconciliation) doit produire un rapport exploitable, ne jamais sur-promettre en dry-run, et être revu en l'EXÉCUTANT réellement (pas seulement via ses tests).
|
||||
- Contexte : script batch qui appelle un service de prod best-effort retournant souvent `null`/booléen opaque (qui mélange "externe down transitoire" et "entrée refusée définitivement").
|
||||
- Quand l'utiliser : tout script de migration/back-fill batch qui dépend d'un externe.
|
||||
- Quand l'éviter : mutation transactionnelle simple en un seul appel (pas de classification par item nécessaire).
|
||||
- Avantage :
|
||||
- le rapport devient l'outil de décision de l'ops (et d'une gate en aval)
|
||||
- l'exécution réelle révèle des findings que les tests (dépendance mockée) masquent
|
||||
- Limites / vigilance :
|
||||
- au-delà des invariants classiques (idempotent : `where: null` + chercher-avant-créer ; PII masquée ; secrets jamais loggés ; exit codes ; runner projet), 4 exigences spécifiques au BATCH sont souvent ratées (voir règles)
|
||||
- Validé le : 15-06-2026
|
||||
- Contexte technique : script ops Node + service externe — RL799_V2 (K1.7 `keycloak-migrate`)
|
||||
|
||||
### Règles
|
||||
|
||||
1. **FAIL-CLOSED ≠ FAIL-SAFE** : un batch doit CLASSER chaque item (`migrated`/`reused`/`blocked`/`failed`/`skipped`). Implémenter un orchestrateur DÉDIÉ qui appelle les mêmes briques basses et OBSERVE le résultat, SANS toucher le service de prod (qui reste fail-safe pour son usage).
|
||||
2. **RAPPORT EXPLOITABLE** : la raison d'un `failed` ne doit JAMAIS être `err.name` seul (sur une panne `fetch`, ça donne "Error", inactionnable). Remonter `name: message` nettoyé/borné (sans secret) + le statut HTTP des erreurs typées.
|
||||
3. **DRY-RUN HONNÊTE** : décider et DOCUMENTER ce que le dry-run touche. S'il interroge l'externe en LECTURE, le dire en tête ("interroge X en lecture seule"). Un dry-run NE DOIT PAS écrire d'artefact sur disque (stdout suffit). Documenter qu'il sur-estime les succès s'il ne tente pas l'écriture (ne détecte pas les refus de création).
|
||||
4. **ACTION SENSIBLE = AUDIT** : toute action destructive/sensible (révocation de token, ré-émission d'invitation) trace un audit persistant (`logAction`), pas un `console.log` volatil — même sans userId admin (tracer sur la cible). Ajouter l'action au catalogue d'audit si un test de couverture le vérifie.
|
||||
|
||||
### Méthode de review (l'apprentissage clé)
|
||||
|
||||
Pour un script ops, NE PAS se contenter des tests verts (la dépendance externe y est mockée). **EXÉCUTER le script en réel** (dry-run contre la vraie DB locale, externe absent). Cas vécu RL799 K1.7 : cette exécution a sorti M1 (raison `failed: Error` opaque) + M2 (dry-run qui écrit un fichier), tous deux invisibles en test. Les tests prouvent la logique ; l'exécution prouve l'expérience ops.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-helper-transverse-send-mail-with-retry"></a>
|
||||
## Pattern : Helper transverse paramétrable + wrapper local par caller (ex. `sendMailWithRetry`)
|
||||
|
||||
- Objectif : partager un helper technique entre deux domaines tout en préservant une configuration spécifique à un caller, sans réintroduire de duplication ni de couplage inter-domaines.
|
||||
- Contexte : une mécanique technique (boucle de retry mail, backoff) utilisée par plusieurs domaines, dont l'un a des paramètres distincts.
|
||||
- Quand l'utiliser : ≥ 2 domaines partagent la même logique technique mais avec des réglages différents.
|
||||
- Quand l'éviter : un seul caller → pas besoin de paramétrer ni de wrapper.
|
||||
- Avantage :
|
||||
- zéro duplication de la logique de retry (mono-source)
|
||||
- zéro couplage inter-domaines : le helper vit dans `lib/`, neutre
|
||||
- chaque domaine garde sa config co-localisée avec son usage
|
||||
- Limites / vigilance :
|
||||
- le helper neutre ne doit connaître aucun domaine (pas d'import repository métier)
|
||||
- Validé le : 23-06-2026
|
||||
- Contexte technique : Node.js / lib transverse — RL799_V2 (v2-6-2)
|
||||
|
||||
### Forme
|
||||
|
||||
```typescript
|
||||
// lib/mailRetry.ts — neutre, config en 2e param avec défaut
|
||||
export const sendMailWithRetry = (args: MailArgs, retry = MAIL_RETRY) => { /* … */ };
|
||||
|
||||
// Caller standard : appel direct (config par défaut)
|
||||
await sendMailWithRetry(args);
|
||||
|
||||
// Caller spécifique (flux visiteur : attempts:2, backoffMs:[1000]) :
|
||||
// const de config dédiée + wrapper local nommé qui fige la config
|
||||
const VISITOR_MAIL_RETRY = { attempts: 2, backoffMs: [1000] };
|
||||
const sendVisitorMailWithRetry = (args: MailArgs) => sendMailWithRetry(args, VISITOR_MAIL_RETRY);
|
||||
```
|
||||
|
||||
Le wrapper garde un nom métier lisible aux sites d'appel, la config custom reste près de son domaine, la logique de retry reste mono-source.
|
||||
|
||||
Reference in New Issue
Block a user