Aller au contenu

PD-85 — Plan d'implémentation

1. Découpage en composants

C1 — export-dto : DTOs de requête et de réponse

Responsabilité : Validation d'entrée (ComplaintFileRequestDto) et sérialisation de sortie (ComplaintFileResponseDto, ManifestDto, ChronologyDto, RejectedProofDto).

  • ComplaintFileRequestDto : proofIds: UUID[] (1..500), validé par class-validator (regex UUID v4, @ArrayMinSize(1), @ArrayMaxSize(500), @ArrayUnique()).
  • ComplaintFileResponseDto : payload 200 complet (exportId, manifest, chronology, signedUrls, guideUrl, readmeVerification, rejectedProofs).
  • ManifestDto : version, exportId, exportedAt, exportedBy, proofCount, proofs[], integrityHash.
  • ChronologyDto : events[] triés par timestamp non décroissant.
  • RejectedProofDto : proofId, reason.

C2 — export-exceptions : Codes d'erreur métier PD-85

Responsabilité : ExportException étendant HttpException avec codes machine-readable.

Code HTTP Condition
PAYLOAD_INVALID 400 proofIds absent/vide/format/doublons
PREMIUM_REQUIRED 403 Plan FREE (réutilise FreemiumException)
PROOF_NOT_FOUND 404 proofId inexistant ou non possédé (fail-fast)
EXPORT_SIZE_EXCEEDED 413 Taille > 1 GB
ALL_PROOFS_REJECTED 422 Toutes preuves invalides
EXPORT_INTERNAL_ERROR 500 Incohérence interne

C3 — export-validators : Validations invariants RFC par preuve

Responsabilité : Implémentation des 5 validations RFC (INV-02, INV-03, INV-09, INV-11, INV-12) sur chaque ProofEnvelope. Retourne {valid: boolean, reason?: string} par preuve.

  • FiveSectionsCompleteValidator : vérifie les 5 sections RFC présentes et non vides.
  • ReKeyActiveValidator : vérifie qu'aucun ReKey actif n'existe sur la preuve.
  • SecretExposureValidator : scan déterministe des patterns sensibles (DEK, ReKey fragments, mots de passe).
  • EnvelopeSealValidator : vérifie la validité cryptographique du sceau HSM.
  • OfflineMaterialValidator : vérifie la présence du matériel de vérification offline.

C4 — export-service : Service d'orchestration export

Responsabilité : Logique métier principale. Orchestre la chaîne complète : validation requête → contrôle plan → RLS ownership → validation invariants → calcul taille → génération manifest/chronology → URLs signées → audit WORM.

  • Traitement synchrone (pas de queue/worker).
  • Rejet partiel : les preuves invalides sont exclues du manifest et collectées dans rejectedProofs.
  • Fail-fast sur 404 : la première preuve non possédée/inexistante interrompt le traitement.
  • Audit : événement WORM émis pour TOUT résultat (200, 400, 403, 404, 413, 422).

C5 — export-manifest-builder : Construction manifest + chronology

Responsabilité : Assemblage du manifest.json (avec integrityHash SHA3-256 JCS) et du chronology.json (événements triés). Construction du README_VERIFICATION.txt.

  • Canonicalisation via json-canonicalize (RFC 8785) — réutilisation du service existant tsa/utils/canonical-json.utils.ts.
  • Hash SHA3-256 via node:crypto (createHash('sha3-256')), pas SHA-256 (différent du TSA existant qui utilise SHA-256).
  • Champ integrityHash exclu de la canonicalisation avant calcul du hash.

C6 — export-controller : Endpoint REST

Responsabilité : Remplacement du stub PD-84 ExportController par l'implémentation réelle. Endpoint POST /exports/complaint-file (nouvelle route, distincte des stubs folders/:id/exports/*).

  • Guards : OidcJwtAuthGuard (auth) + PremiumGuard (plan).
  • Rate limiting : EXPORT_RATE_LIMIT_PER_HOUR (défaut 10, configurable).
  • Injection du ExportService.

C7 — export-config : Configuration et constantes

Responsabilité : Variables d'environnement et constantes PD-85.

Variable Défaut Validation
EXPORT_SIGNED_URL_TTL_MIN 15 Joi.number().min(1).max(30)
EXPORT_RATE_LIMIT_PER_HOUR 10 Joi.number().min(1).max(100)
MAX_BACKEND_EXPORT_BYTES 1073741824 (1 GB) Joi.number().positive()
MAX_PROOF_IDS 500 Joi.number().min(1).max(10000)

C8 — export-audit : Extension audit WORM

Responsabilité : Ajout du type EXPORT_GENERATED à AuditActionType et émission des événements audit pour chaque appel (succès ET erreurs). Champs obligatoires : actorId, exportId, proofCount, result (état terminal), timestamp_utc, correlationId.

C9 — export-minor-evidence : Évidences B2C-mineurs

Responsabilité : Enrichissement conditionnel du payload export avec les évidences mineur/représentant légal (Mandate, Dual Validation, ReKey Lifecycle). Consultation des données existantes du module dual-validation et freemium.

C10 — export-tests : Tests unitaires et d'intégration

Responsabilité : Couverture complète des TC-* du cahier de tests.

2. Flux techniques

F1 — Export nominal complet (200, rejectedProofs=[])

Client → POST /exports/complaint-file {proofIds}
  → OidcJwtAuthGuard (auth)
  → PremiumGuard (plan != FREE → 403 sinon)
  → ExportController.createComplaintFile()
    → ExportService.execute(userId, proofIds)
      1. Validation DTO (proofIds format, doublons → 400)
      2. RLS ownership check (fail-fast → 404)
      3. Chargement ProofEnvelopes depuis DB
      4. Validation invariants RFC par preuve (5 validators)
         → Si toutes invalides → 422
         → Sinon → partition valides/rejetées
      5. Calcul taille totale chiffrée → si > 1 GB → 413
      6. ExportManifestBuilder.build(validProofs, userId, role)
         → manifest.json (JCS canonicalisé + SHA3-256 integrityHash)
         → chronology.json (events triés)
         → README_VERIFICATION.txt (asset statique)
      7. S3PresignService.getSignedUrl() pour chaque document chiffré
         → TTL configurable (défaut 15 min, max 30 min)
      8. Guide statique guideUrl (asset serveur)
      9. Audit WORM (EXPORT_GENERATED, fail-closed)
    → Response 200 {exportId, manifest, chronology, signedUrls, guideUrl, readmeVerification, rejectedProofs=[]}

F2 — Export partiel avec rejets (200, rejectedProofs non vide)

Identique à F1, étapes 1-3, puis à l'étape 4 certaines preuves sont rejetées. Les preuves rejetées sont collectées avec motif, les valides continuent le flux F1 (étapes 5-9). proofCount = nombre de valides uniquement.

À l'étape 6 du flux F1, ExportMinorEvidenceService est invoqué conditionnellement : - Si user.accountRole == LEGAL_GUARDIANexportedBy.role = LEGAL_GUARDIAN - Si dossier mineur actif → enrichissement avec évidences (Mandate si présent, Dual Validation si présent, ReKey Lifecycle si présent) - Aucune évidence non applicable n'est ajoutée (test TC-NOM-03)

2bis. Diagrammes Mermaid

Graphe de dépendances entre composants

graph TD
    C6["C6 — export-controller<br/>(endpoint REST)"]
    C4["C4 — export-service<br/>(orchestration)"]
    C1["C1 — export-dto<br/>(DTOs)"]
    C2["C2 — export-exceptions<br/>(erreurs métier)"]
    C3["C3 — export-validators<br/>(validations RFC)"]
    C5["C5 — export-manifest-builder<br/>(manifest + chronology)"]
    C7["C7 — export-config<br/>(configuration)"]
    C8["C8 — export-audit<br/>(audit WORM)"]
    C9["C9 — export-minor-evidence<br/>(évidences B2C-mineurs)"]
    C10["C10 — export-tests<br/>(tests)"]

    C6 --> C4
    C6 --> C1
    C6 --> C7
    C4 --> C1
    C4 --> C2
    C4 --> C3
    C4 --> C5
    C4 --> C7
    C4 --> C8
    C4 --> C9
    C5 --> C7
    C10 -.->|teste| C1
    C10 -.->|teste| C2
    C10 -.->|teste| C3
    C10 -.->|teste| C4
    C10 -.->|teste| C5
    C10 -.->|teste| C6
    C10 -.->|teste| C8
    C10 -.->|teste| C9

    EXT_S3["S3PresignService<br/>(PD-46)"]
    EXT_AUDIT["AuditLogService<br/>(existant)"]
    EXT_FREEMIUM["FreemiumModule<br/>(PD-84)"]
    EXT_PROOF["ProofEnvelopeRepo<br/>(legal-pre)"]
    EXT_DUAL["DualValidationRepo<br/>(dual-validation)"]

    C4 --> EXT_S3
    C8 --> EXT_AUDIT
    C6 --> EXT_FREEMIUM
    C4 --> EXT_PROOF
    C9 --> EXT_DUAL

    style C6 fill:#4a90d9,color:#fff
    style C4 fill:#d94a4a,color:#fff
    style C3 fill:#d9a44a,color:#fff
    style C5 fill:#4ad98a,color:#fff
    style EXT_S3 fill:#888,color:#fff
    style EXT_AUDIT fill:#888,color:#fff
    style EXT_FREEMIUM fill:#888,color:#fff
    style EXT_PROOF fill:#888,color:#fff
    style EXT_DUAL fill:#888,color:#fff

Diagramme de séquence — Flux nominal F1 (export complet)

sequenceDiagram
    participant Client
    participant C6 as C6 — ExportController
    participant Auth as OidcJwtAuthGuard
    participant Plan as PremiumGuard (C6/PD-84)
    participant C4 as C4 — ExportService
    participant DB as ProofEnvelopeRepo (legal-pre)
    participant C3 as C3 — Validators (x5)
    participant C5 as C5 — ManifestBuilder
    participant C9 as C9 — MinorEvidence
    participant S3 as S3PresignService (PD-46)
    participant C8 as C8 — ExportAudit
    participant Audit as AuditLogService (existant)

    Client->>C6: POST /exports/complaint-file {proofIds}
    C6->>Auth: JWT validation
    Auth-->>C6: userId
    C6->>Plan: plan != FREE ?
    Plan-->>C6: OK (ou 403)

    C6->>C4: execute(userId, proofIds)

    Note over C4: 1. Validation DTO (C1)
    C4->>DB: findByIds(proofIds) WHERE user_id = userId
    DB-->>C4: ProofEnvelopes (ou 404 fail-fast)

    Note over C4: 4. Validation invariants RFC
    loop Pour chaque preuve
        C4->>C3: validate(envelope)
        C3-->>C4: {valid, reason?}
    end
    Note over C4: Partition valides / rejetées

    Note over C4: 5. Calcul taille (> 1 GB -> 413)

    C4->>C9: enrichIfMinor(userId, validProofs)
    C9-->>C4: évidences conditionnelles

    C4->>C5: build(validProofs, userId, role, evidences)
    C5-->>C4: manifest + chronology + README

    loop Pour chaque document chiffré
        C4->>S3: getSignedUrl(s3Key, ttl)
        S3-->>C4: signedUrl
    end

    C4->>C8: emitExportAudit(exportId, result=SUCCESS)
    C8->>Audit: logAsync(EXPORT_GENERATED, ...)
    Audit-->>C8: OK (ou 500 fail-closed)
    C8-->>C4: OK

    C4-->>C6: {exportId, manifest, chronology, signedUrls, ...}
    C6-->>Client: 200 ComplaintFileResponseDto

3. Mapping invariants → mécanismes

Invariant ID Exigence Mécanisme Composant Observable Risque
INV-02 FiveSectionsComplete FiveSectionsCompleteValidator vérifie 5 sections RFC non vides C3 Preuve rejetée avec motif FIVE_SECTIONS_INCOMPLETE Faible — validation déterministe
INV-03 Pas de ReKey actif ReKeyActiveValidator requête DB rekey_state != ACTIVE C3 Preuve rejetée avec motif REKEY_ACTIVE Faible — requête DB simple
INV-09 Aucun secret exposé SecretExposureValidator scan patterns sensibles (regex) sur payload sérialisé C3 Preuve rejetée avec motif SECRET_EXPOSED Moyen — patterns à maintenir exhaustifs
INV-11 envelopeSeal valide EnvelopeSealValidator vérifie signature ECDSA-P384-SHA3-384 via crypto.verify(null, ...) C3 Preuve rejetée avec motif SEAL_INVALID Moyen — dépend HSM/PKI. Attention : crypto.verify(null, hash, key, sig) (raw ECDSA, pas createVerify)
INV-12 Matériel offline suffisant OfflineMaterialValidator vérifie présence verificationMaterial complet C3 Preuve rejetée avec motif OFFLINE_MATERIAL_MISSING Faible — vérification structurelle
INV-85-01 Backend ne déchiffre jamais Architecture Zero-Knowledge : S3PresignService génère URL sans lecture. Aucun appel GetObject / déchiffrement dans ExportService C4, C6 Absence d'appel S3 GetObject dans traces/audit. Test : grep code pour opérations decrypt Faible — pattern déjà prouvé PD-63
INV-85-02 exportId UUID v4 unique crypto.randomUUID() à chaque requête C4 UUID v4 format + non-collision sur série Faible
INV-85-03 integrityHash = SHA3-256 JCS ExportManifestBuilder : canonicalise manifest sans integrityHash, hash SHA3-256, injecte C5 Recalcul externe identique Moyen — attention SHA3-256 ≠ SHA-256
INV-85-04 URLs signées <= 30 min S3PresignService.getSignedUrl({expiresIn}) avec TTL configurable borné Joi max(30) C7, C4 expiresAt - now ∈ [1, 30] min Faible — borne Joi + test
INV-85-05 Audit WORM immuable AuditLogService.logAsync() fail-closed pour TOUT résultat HTTP C8 Entrée audit WORM présente pour chaque requête Moyen — fail-closed si audit indisponible → 500
INV-85-06 Export n'altère pas ProofEnvelope Lecture seule des ProofEnvelopes (find, pas save/update) C4 Hash metadata avant/après identiques Faible — code review statique
INV-85-07 Taille <= 1 GB Sommation encrypted_size des documents, comparaison à MAX_BACKEND_EXPORT_BYTES C4 413 si dépassement Faible
INV-85-08-transitions États terminaux sans transition Machine d'états : 6 états terminaux, aucune transition sortante. Enum ExportResultState sans méthode de transition C2 Test unitaire vérifiant absence de transition Faible
INV-85-09-envelope-encryption Secrets crypto chiffrés au repos Aucun artefact crypto temporaire créé par PD-85 (lecture seule). Vérification que les ProofEnvelopes existantes respectent la contrainte C3 Scan DB : aucun champ DEK/fragment en clair Faible — PD-85 ne crée pas de secrets

4. Mapping critères d'acceptation → mécanismes

Critère ID Mécanisme(s) Composant Observable Risque
CA-01 Chaîne complète F1 : auth → plan → RLS → validation → manifest → URLs → audit C4, C5, C6 200 + exportId UUID + manifest + signedUrls non vides Faible
CA-02 class-validator sur ComplaintFileRequestDto : @IsArray, @ArrayMinSize(1), @IsUUID('4', {each: true}) C1 400 + message validation Faible
CA-03 PremiumGuard vérifie user.plan != FREE C6 403 + motif PREMIUM_REQUIRED Faible — réutilise pattern PD-84
CA-04 RLS ownership check fail-fast dans ExportService : WHERE proof.user_id = :userId C4 404 corrélé au premier proofId fautif Faible
CA-05 Sommation encrypted_size vs MAX_BACKEND_EXPORT_BYTES C4 413 + taille estimée + limite 1 GB Faible
CA-06 Si partition valides/rejetées → toutes rejetées → 422 C4 422 + rejectedProofs exhaustif (3 entrées dans ST-05) Faible
CA-07 Partition valides/rejetées → export partiel C4, C5 200 + proofCount = valides + rejectedProofs non vide Faible
CA-08 ExportManifestBuilder : JCS canonicalisation + SHA3-256 C5 Recalcul externe == manifest.integrityHash Moyen — JCS doit être byte-identical
CA-09 ChronologyBuilder : events.sort((a, b) => a.timestamp.localeCompare(b.timestamp)) C5 Ordre non décroissant vérifié Faible
CA-10 S3PresignService.getSignedUrl({expiresIn: ttlMinutes * 60}) + calculateExpiresAt() C4 Accès OK avant expiration, refus après Moyen — test d'intégration S3 requis
CA-11 Zero-Knowledge : aucun GetObject/decrypt dans code PD-85 C4 Absence de trace/appel déchiffrement Faible
CA-12 AuditLogService.logAsync() pour TOUT résultat (200 + 4xx + 422) C8 Entrée audit immuable pour chaque réponse HTTP Moyen — nécessite fail-closed
CA-13 ExportMinorEvidenceService enrichissement conditionnel C9 Évidences présentes si applicables, absentes sinon Moyen — dépend données B2C
CA-14 manifest.exportedBy.userId = JWT sub + role = USER LEGAL_GUARDIAN C5 Champs présents et valides

5. Mapping tests (TC-*) → mécanismes + observables

Test ID Référence spec Mécanisme(s) Point(s) d'observation Niveau de test visé
TC-NOM-01 CA-01, INV-02/09/11/12, INV-85-02/06 Chaîne F1 complète 200 + payload complet + immutabilité source Integration
TC-NOM-02 CA-07, INV-03 Partition valides/rejetées 200 + proofCount=8 + 2 rejets Integration
TC-NOM-03 CA-13 ExportMinorEvidenceService 200 + LEGAL_GUARDIAN + évidences conditionnelles Integration
TC-NOM-04 CA-09, CA-14 ChronologyBuilder + exportedBy Ordre chrono + userId/role valides Unit
TC-NOM-05 CA-08, INV-85-03 ExportManifestBuilder.computeIntegrityHash() Recalcul SHA3-256 == integrityHash Unit
TC-NOM-06 CA-10, INV-85-04 S3PresignService.getSignedUrl() + TTL Accès OK avant expiration + HTTPS + TTL ≤ 30 min Integration
TC-NOM-07 CA-10, INV-85-04 URL expirée → refus Accès refusé post-expiration Integration
TC-ERR-01 CA-02 class-validator rejet 400 + message validation Unit
TC-ERR-02 CA-03 PremiumGuard 403 + motif plan Unit
TC-ERR-03 CA-04 RLS fail-fast 404 corrélé proofId Integration
TC-ERR-04 CA-05, INV-85-07 Sommation taille 413 + estimée + limite Unit
TC-ERR-05 CA-06 Toutes invalides 422 + 3 rejets + motifs Integration
TC-ERR-06 CA-02 UUID format invalide 400 + format invalide Unit
TC-INV-02 INV-02 FiveSectionsCompleteValidator Preuve rejetée si section manquante Unit
TC-INV-03 INV-03 ReKeyActiveValidator Preuve rejetée si ReKey actif Unit
TC-INV-09 INV-09 SecretExposureValidator Aucun pattern sensible dans output Unit
TC-INV-11 INV-11 EnvelopeSealValidator Signature ECDSA valide (roundtrip sign-verify) Unit
TC-INV-12 INV-12 OfflineMaterialValidator Matériel complet et cohérent Unit
TC-INV-8501 INV-85-01 Absence opération déchiffrement Scan code + audit technique Unit (statique)
TC-INV-8502 INV-85-02 crypto.randomUUID() UUID v4 unique × N requêtes Unit
TC-INV-8503 INV-85-03 JCS + SHA3-256 Hash recalculé identique Unit
TC-INV-8504 INV-85-04 TTL borné Joi + calcul expiresAt - now ∈ [1, 30] min Unit + Integration
TC-INV-8505 INV-85-05 AuditLogService.logAsync() fail-closed Entrée WORM pour chaque requête Integration
TC-INV-8506 INV-85-06 Lecture seule ProofEnvelope Checksum avant/après identiques Integration
TC-INV-8507 INV-85-07 Sommation > 1 GB → 413 413 + taille Unit
TC-INV-8508 INV-85-08 Enum ExportResultState sans transition Pas de méthode next()/transition() Unit
TC-INV-8509 INV-85-09 Aucun secret temporaire PD-85 Scan : pas de DEK/fragment en clair Unit (statique)
TC-NR-01 Stabilité contrat 200 Schéma JSON snapshot Champs obligatoires présents Unit
TC-NR-02 Stabilité codes erreur Jeu de données → mêmes codes Pas de régression mapping Integration
TC-NR-03 Déterminisme proofCount proofCount == proofs.length Cohérence interne Unit
TC-NR-04 Intégrité hash inter-exécutions Mêmes données → même hash Déterminisme JCS Unit
TC-NR-05 Non-régression URL signées TTL ≤ 30 min à chaque release Borne respectée Unit
TC-NR-06 Non-régression WORM Entrée audit sur succès Traçabilité minimale Integration
TC-NEG-01 Doublons proofIds 400 (doublons détectés) Pas de déduplication Unit
TC-NEG-02 Mélange valides + format invalide 400 prioritaire Aucun export Unit
TC-NEG-03 URL non-HTTPS (robustesse) Rejet interne HTTPS strict Unit
TC-NEG-04 Horodatage non UTC Rejet preuve UTC Z strict Unit
TC-NEG-05 URL expirée répétée Refus systématique Pas de réactivation Integration
TC-NEG-06 Secrets en clair stockage temp Aucun détecté Conformité INV-85-09 Unit (statique)

6. Gestion des erreurs

Code Condition Traitement Observable
400 proofIds absent, vide, format UUID invalide, doublons class-validator + ExportException(PAYLOAD_INVALID). Aucun export créé, aucune URL signée Message validation explicite + audit WORM
403 Plan FREE PremiumGuardFreemiumException(PREMIUM_REQUIRED) (réutilise PD-84) Motif plan + audit WORM
404 proofId inexistant OU non possédé Fail-fast : la première erreur interrompt, ExportException(PROOF_NOT_FOUND, {proofId}) proofId corrélé + audit WORM
413 Taille totale > 1 GB ExportException(EXPORT_SIZE_EXCEEDED, {estimatedBytes, limitBytes}) Taille estimée + limite + audit WORM
422 Toutes preuves rejetées par validateurs ExportException(ALL_PROOFS_REJECTED, {rejectedProofs}) Détail par preuve/motif + audit WORM
429 Rate limit dépassé Throttler NestJS (@nestjs/throttler) avec EXPORT_RATE_LIMIT_PER_HOUR Message retry-after
500 Incohérence interne (exportedBy.role absent, proofCount != proofs.length, etc.) Exception interne capturée par filtre global. Audit WORM avec result: ERROR Logs internes, pas de fuite d'info

Fail-closed audit : si AuditLogService.logAsync() échoue, l'export DOIT échouer (500). L'opération métier et l'audit sont atomiques au niveau applicatif — un export sans trace audit violerait INV-85-05.

7. Impacts sécurité

Zero-Knowledge (INV-85-01)

L'endpoint POST /exports/complaint-file ne lit JAMAIS le contenu des documents. Il : - Lit les métadonnées des ProofEnvelopes depuis PostgreSQL (structures, hashes, seals) - Génère des URLs signées S3 via S3PresignService (opération de signature, pas de GetObject) - Assemble le manifest/chronology à partir des métadonnées uniquement

Mécanisme de vérification : Revue statique du code + test TC-INV-8501 (grep absence GetObject, decrypt, decipher).

Authentification et autorisation

  • OidcJwtAuthGuard : auth JWT Keycloak (existant)
  • PremiumGuard : vérifie user.plan != FREE (nouveau, inspiré de PlanStubGuard)
  • RLS ownership : WHERE proof.user_id = :userId (PostgreSQL RLS + vérification applicative, défense en profondeur)
  • Rate limiting : @nestjs/throttler par utilisateur (EXPORT_RATE_LIMIT_PER_HOUR)

Données sensibles

  • Aucun secret (DEK, K_master, fragments ReKey) dans le payload de réponse (INV-09)
  • SecretExposureValidator scanne le payload sérialisé avant émission
  • Les URLs signées contiennent des credentials temporaires (S3 HMAC) — TTL borné ≤ 30 min

Journalisation

  • Audit WORM pour TOUT résultat (succès + erreurs), fail-closed
  • Champs : actorId, exportId, proofIds, proofCount, result, rejectedCount, timestamp_utc, correlationId
  • Aucune donnée sensible dans les logs (pas de contenu document, pas de clés)

Conformité

  • Art. 1366 Code civil : auto-vérifiabilité via integrityHash + matériel offline
  • NF Z42-013 / ISO 14641 : audit WORM immuable
  • eIDAS : matériel de vérification embarqué (PD-282)

8. Hypothèses techniques

ID Hypothèse Impact si faux
HT-01 La bibliothèque json-canonicalize (npm) implémente RFC 8785 de manière strictement conforme Hash non reproductible inter-systèmes. Mitigation : utiliser la même lib que tsa/utils/canonical-json.utils.ts (déjà validée)
HT-02 node:crypto supporte sha3-256 via createHash('sha3-256') (Node.js >= 18 avec OpenSSL 3) Fallback : utiliser @noble/hashes/sha3 si indisponible
HT-03 La table des ProofEnvelopes contient un champ encrypted_size ou permet de calculer la taille Si absent : requête S3 HeadObject par document (latence additionnelle). À vérifier en implémentation
HT-04 Le service AuditLogService.logAsync() accepte les nouveaux AuditActionType sans modification de schéma Sinon : migration DDL pour la table audit
HT-05 Le S3PresignService existant (PD-46) supporte un TTL paramétrable et peut servir plusieurs URLs par requête Sinon : extension mineure du service avec boucle
HT-06 L'asset guide_plainte_france.pdf est un fichier statique servi depuis le serveur (pas S3) Si S3 : URL signée supplémentaire à générer
HT-07 Les données B2C-mineurs (Mandate, Dual Validation, ReKey Lifecycle) sont accessibles via les repositories TypeORM existants Si non : création de méthodes de requête dans les services existants
HT-08 La route POST /exports/complaint-file est distincte des stubs PD-84 (/folders/:id/exports/*) — pas de collision Si collision : refactoring des routes PD-84

9. Points de vigilance (risques, dette, pièges)

SHA3-256 vs SHA-256

Le codebase existant (tsa/utils/canonical-json.utils.ts) utilise SHA-256 (sha256Hash). PD-85 requiert SHA3-256 pour integrityHash. Ne PAS réutiliser canonicalizeAndHash() directement — créer une fonction sha3_256Hash() dédiée ou utiliser createHash('sha3-256') directement.

Fail-fast 404 vs rejets partiels

Le 404 (proof non possédée/inexistante) est fail-fast : le traitement s'arrête à la première erreur. Les rejets partiels (422/200) ne s'appliquent qu'aux validations d'invariants RFC (INV-02/03/09/11/12), PAS à l'ownership. L'ordre de traitement est critique : 1. Validation payload (400) 2. Contrôle plan (403) 3. RLS ownership — fail-fast (404) 4. Validation taille (413) 5. Validation invariants RFC — rejet partiel (200 ou 422)

Signature ECDSA raw pour INV-11

Attention au piège double-hash (REX PD-282) : CloudHSM signe en mode CKM_ECDSA (raw). Utiliser crypto.verify(null, hash, publicKey, signature), jamais createVerify('SHA3-384').update(hash).

Rate limiting

Le rate limit (10/h par défaut) doit être par utilisateur, pas global. Utiliser @nestjs/throttler avec la clé userId extraite du JWT.

Atomicité audit

L'audit est fail-closed : si l'écriture WORM échoue, l'export doit échouer (500). Cela implique que AuditLogService.logAsync() doit être attendu (await) malgré son nom async — il s'agit d'un fire-and-await, pas d'un fire-and-forget pour cet usage.

Doublons proofIds

La spec contractualise le rejet 400 pour les doublons (pas de déduplication). Le DTO doit utiliser @ArrayUnique() de class-validator.

10. Hors périmètre

  • Assemblage ZIP : PD-283 (côté app).
  • Déchiffrement documents : interdit (Zero-Knowledge).
  • Script de vérification automatisé : V2.
  • Timeline PDF riche : V2.
  • Internationalisation du guide : V2.
  • Notification automatique : V2.
  • Tests de performance P95 : contexte matériel non spécifié (cf. H-06 spec). Les seuils 5s/50 preuves et 10s/100 preuves sont documentés mais non testables sans environnement de référence.
  • Migration DDL : aucun changement de schéma contractualisé. Si encrypted_size manque sur les entités, c'est une adaptation d'implémentation (HT-03), pas un changement de spec.

Périmètre de test

Niveau de test In scope Hors scope (justification)
Unitaire Tous les composants C1-C9 : DTOs, validators, manifest builder, service, exceptions, config
Intégration Interactions entre ExportService ↔ Validators ↔ ManifestBuilder ↔ S3PresignService ↔ AuditLogService
E2E Flux complet POST /exports/complaint-file avec DB PostgreSQL + S3 mock S3 réel (OVH) hors périmètre E2E local — testé en CI/CD

Tous les niveaux de test sont couverts, aucune exclusion. Les tests d'intégration S3 (TC-NOM-06, TC-NOM-07) utilisent un mock S3 local (localstack ou mock S3PresignService). Les tests E2E vérifient le flux HTTP complet avec base de données réelle.

Mécanismes cross-module

Élément Détail
Routes d'autres modules à protéger Aucune — PD-85 ne modifie pas de routes existantes. Le stub PD-84 ExportController est remplacé, pas les routes folders/:id/exports/* (celles-ci restent en 403/501 pour FREE/PREMIUM respectivement)
Modules consommés en lecture freemium (User.plan, accountRole), legal-pre (ProofEnvelope, EnvelopeSeal), dual-validation (données Mandate, Dual Validation), documents (DocumentSecure metadata), storage (S3PresignService), audit (AuditLogService)
Modules modifiés audit (ajout EXPORT_GENERATED à AuditActionType), config (ajout variables EXPORT_*)
FK/résolution cross-module Via repositories TypeORM existants : ProofEnvelope.userId, DocumentSecure.s3Key, User.plan, DualValidation.proofId

Aucune modification de routes d'autres modules.