PD-60 — Retour d'expérience (REX)¶
1. Résumé exécutif¶
Objectif initial : Implémenter POST /documents/upload pour qu'un acteur autorisé puisse déposer un document avec création d'un acte probatoire daté, traçable et juridiquement imputable, vérifiable par un tiers hors plateforme, sans exposition du contenu intelligible.
Résultat obtenu : L'endpoint est opérationnel avec l'ensemble des flux nominaux (dépôt probatoire, rejeu idempotent, staging dégradé, confirmation après staging), les flux d'erreur (401, 403, 400, 409, 422, 500), et la vérification tiers via JWKS public.
Verdict d'acceptabilité : ✅ ACCEPTÉ — 102/102 tests contractuels PASS (91 Jest + 11 Vitest). Aucun écart d'implémentation constaté dans le périmètre contractuel.
2. Points fluides¶
- Spécification exhaustive : Les 17 invariants (INV-60-01..17) et 21 critères d'acceptation (CA-60-01..21) ont fourni un cadre non ambigu pour l'implémentation. Les décisions contractuelles (PC-60-01..13) ont clarifié les zones grises en amont.
- Architecture NestJS modulaire : La séparation Controller / Service / DTO / Entity a permis une implémentation progressive et testable composant par composant.
- Transaction unique englobante : Le pattern
DataSource.transaction()englobant Phase 0 (advisory lock) + Phase A (audit + reçu) + Phase B (INSERT) a implémenté INV-60-02 (atomicité) de manière directe. - Idempotence par
client_request_id: L'advisory lock PostgreSQL + détection de doublon a couvert FN-60-02 et ERR-60-05 sans complexité excessive. - Tests de non-régression et adversariaux : Les suites TC-NR-01..06 et TC-NEG-01..05 ont été implémentées sans difficulté grâce aux mocks bien structurés.
3. Points difficiles¶
- jose v6 ESM-only : La bibliothèque
josev6 utilisée pour JWS (ES256/RS256) est exclusivement ESM. Jest (CommonJS) ne peut pas l'importer. Cela a nécessité une suite Vitest séparée (proof-receipt.service.vitest.ts) pour les 11 tests liés au reçu probatoire. - Audit des rejets au niveau framework : Les guards NestJS (
JwtAuthGuard,AuthorizationGuard) lèvent des exceptions avant l'exécution de la méthode du contrôleur. Les tests TC-ERR-01 et TC-ERR-02 exigeaient la preuve que ces rejets sont audités. La solution initiale (audit uniquement dans le service) ne couvrait pas ce cas. Il a fallu 3 itérations d'acceptabilité pour identifier et résoudre ce point. - Instanciation du filtre d'exception avec DI : La première tentative utilisait
@UseFilters(new DepositAuditExceptionFilter(...))qui contourne l'injection de dépendances NestJS. Il a fallu passer à@UseFilters(DepositAuditExceptionFilter)(référence de classe) + enregistrement dans les providers du module. - Distinction erreur technique vs staging dégradé : TC-ERR-06 (ERR-60-06) exige un échec technique explicite (S3/DB indisponible) distinct du staging dégradé (FN-60-03). La première implémentation des tests confondait les deux cas.
4. Hypothèses révélées tardivement¶
- H-tardive-01 : L'audit des tentatives rejetées par les guards (401, 403) nécessite un mécanisme au niveau framework (exception filter), pas au niveau service. Cette hypothèse n'était pas explicite dans le plan d'implémentation initial qui ne mentionnait pas de filtre d'exception.
- H-tardive-02 : Les tests Jest du contrôleur nécessitent que le
DepositAuditExceptionFiltersoit fourni dans les providers du module de test (via mock deDepositAuditService), car@UseFilters(Class)déclenche la résolution DI même en test unitaire. - H-tardive-03 : La distinction entre « non-altérabilité applicative » (testable, append-only) et « non-altérabilité absolue » (hors périmètre, résistance admin) devait être argumentée explicitement pour obtenir l'acceptation sans réserves.
5. Invariants complexes¶
- INV-60-10 (double condition d'existence juridique) : L'implémentation via transaction englobante + injection de faute contrôlée (INV-60-16) a bien fonctionné mais les 3 cas (A: audit fail, B: reçu fail, C: les deux OK) requièrent des mocks distincts et une attention particulière aux assertions. Couvert par TC-NOM-09.
- INV-60-06 (journalisation non altérable) : La couverture partielle (append-only applicatif) vs la non-altérabilité absolue (infrastructure) a été le principal point de friction lors de l'acceptabilité. Résolu par la décision contractuelle PC-60-13 (hors périmètre US).
- INV-60-05 (vérification tiers) : L'implémentation JWS avec endpoint JWKS public fonctionne mais les tests de vérification tiers complète (signature + fingerprint + cohérence payload) nécessitent Vitest à cause de jose ESM.
- INV-60-02 (atomicité) : Le mécanisme de transaction unique englobante est sensible aux régressions si un futur développeur ajoute des opérations hors transaction. Le test TC-NOM-09 est le garde-fou principal.
6. Dette technique¶
- Double runner de tests : La coexistence Jest (91 tests) + Vitest (11 tests) pour la même US est un compromis lié à jose v6 ESM. Le script
npm run test:pd60agrège les deux, mais cette dualité ajoute de la complexité de maintenance. - Clé éphémère ES256 : En l'absence de CloudHSM configuré, le système génère une paire de clés ES256 éphémère au démarrage. Cette clé change à chaque redémarrage, invalidant les reçus précédents. L'intégration CloudHSM (RS256, clé persistante) est prévue dans une US séparée.
- Filtre d'exception
@Catch()global au contrôleur : LeDepositAuditExceptionFilterintercepte toutes les exceptions du contrôleur. Il filtre les 409/422/404 (déjà audités au niveau service) viareturn null, mais ce couplage implicite entre le filtre et le service pourrait être fragile si de nouveaux codes d'erreur sont ajoutés.
7. Risques résiduels¶
- Dérive NTP non testée en intégration réelle : Le
NtpHealthServicevérifie la dérive NTP, mais les tests unitaires mockent ce service. Un environnement dont le NTP est mal configuré pourrait produire des horodatages incorrects sans alerte. - Perte de clé éphémère : En mode éphémère (sans HSM), un redémarrage du service invalide tous les reçus probatoires émis précédemment. Risque opérationnel jusqu'à l'intégration CloudHSM.
- Scalabilité de l'advisory lock : Le mécanisme d'advisory lock PostgreSQL pour l'idempotence peut devenir un goulot d'étranglement sous forte concurrence sur le même
client_request_id. Le risque est faible en usage normal mais non mesuré. - Couplage filtre/service pour l'audit : Si un nouveau type d'exception HTTP est ajouté au service sans mise à jour du filtre, il pourrait être audité deux fois (service + filtre) ou pas du tout.
8. Améliorations de processus¶
- Expliciter les mécanismes framework dans le plan d'implémentation : Le plan listait les composants métier (Controller, Service, DTO) mais pas les composants framework transverses (exception filters, interceptors). L'ajout d'une section « mécanismes framework » dans le template de plan éviterait la découverte tardive (H-tardive-01).
- Inclure un critère d'acceptation pour l'audit des rejets framework : La spécification mentionne CA-60-09 « toute tentative est auditée » mais les TC-ERR-01/02 initiaux ne vérifiaient pas l'émission effective de l'action d'audit. Un critère explicite « le rejet par un guard DOIT émettre l'action d'audit correspondante » aurait anticipé le problème.
- Documenter les contraintes ESM/CJS dans les hypothèses techniques : La contrainte jose v6 ESM → Vitest a été découverte en implémentation. L'ajout d'une vérification de compatibilité des dépendances critiques dans l'étape 4 (plan) éviterait les surprises.
- Limiter les itérations d'acceptabilité : 4 rounds ont été nécessaires. Les 2 derniers portaient sur des exclusions de périmètre déjà documentées (PC-60-13, PC-60-12). Une section « exclusions rappelées » dans le document d'acceptabilité permettrait de pré-désamorcer ces points.
9. Enseignements clés¶
-
Les guards et pipes NestJS échappent au code métier : Toute exigence d'audit exhaustive (« toute tentative est auditée ») nécessite un mécanisme au niveau framework (exception filter), pas uniquement au niveau service. Ce pattern est réutilisable pour toute US ayant un invariant d'auditabilité complète.
-
Les exclusions de périmètre contractuelles doivent être rappelées à chaque gate : Les décisions PC-60-13 (non-altérabilité absolue hors périmètre) et PC-60-12 (qualification juridique hors périmètre) ont été documentées dans la spec mais oubliées lors de l'acceptabilité. Un rappel systématique dans le document d'acceptabilité éviterait les itérations superflues.
-
La dualité Jest/Vitest est un coût structurel des dépendances ESM-only : Lorsqu'une dépendance critique (ici jose v6) est ESM-only et que le projet utilise Jest (CJS), il faut assumer le coût de deux runners. La décision doit être prise au moment du choix de la dépendance, pas en cours d'implémentation.
-
L'injection de dépendances NestJS impose des contraintes sur les tests unitaires : L'utilisation de
@UseFilters(Class)(référence de classe, pour le DI) au lieu de@UseFilters(new Instance())a des implications directes sur la configuration des modules de test. Ce pattern doit être documenté comme convention projet. -
La transaction unique englobante est le mécanisme le plus sûr pour l'atomicité probatoire : Plutôt que de coordonner plusieurs opérations avec compensation/rollback, encapsuler l'ensemble (audit + reçu + persistance) dans une seule transaction DB garantit l'atomicité sans complexité de saga. Ce pattern est directement applicable aux futures US probatoires.