Plan d'implémentation — PD-63
Story : PD-63 — GET /documents/:id/download Date : 2026-02-20 Auteur : Claude (Orchestrateur) Version : v2 — Corrections Gate 5 v1
1. Analyse de l'existant
1.1 Base PD-46 (DONE)
L'endpoint GET /documents/:id/download est déjà implémenté par PD-46 :
| Composant | Fichier | Statut |
| Controller | download.controller.ts | ✅ Existant |
| Service | download.service.ts | ✅ Existant |
| Guard (owner) | document-access.guard.ts | ✅ Existant (owner only) |
| S3 Presign | s3-presign.service.ts | ✅ Existant |
| Audit | audit-download.service.ts | ✅ Existant (non-bloquant) |
| Errors | download.errors.ts | ✅ Existant |
1.2 Deltas PD-63 vs PD-46
PD-63 ajoute/modifie les éléments suivants :
| Delta | Description | Impact |
| Δ1 | Audit fail-closed (INV-63-05) | Modifier download.service.ts |
| Δ2 | ERR-63-11 (audit indisponible) | Ajouter code erreur |
| Δ3 | Flux Partage actif (H-63-07) | Compléter document-access.guard.ts |
| Δ4 | Flux B2B co-détention (H-63-07) | Compléter document-access.guard.ts |
| Δ5 | Statuts document (§3.1) | Ajouter vérification statut |
| Δ6 | Champs modèle 410/423 (5 champs) | Migration + Entity update |
| Δ7 | Schéma audit (CA-63-07) | Enrichir événements audit |
| Δ8 | Mapping ERR-46→ERR-63 | Renommer/mapper codes |
| Δ9 | Intercepteur audit global | Journaliser rejets 401/403/404 précoces |
2. Découpage en composants
2.1 Module Download (modifications)
src/modules/documents/download/
├── download.controller.ts # Pas de modification
├── download.service.ts # Δ1: fail-closed
├── download.errors.ts # Δ2, Δ8: nouveaux codes
├── download.constants.ts # Pas de modification
├── download.dto.ts # Pas de modification
├── download.module.ts # Pas de modification
├── interceptors/
│ └── audit-reject.interceptor.ts # Δ9: journaliser rejets précoces
└── guards/
└── document-access.guard.ts # Δ3, Δ4, Δ5: partage + B2B + statuts
2.2 Module Audit (modifications)
src/modules/documents/audit/
└── audit-download.service.ts # Δ7: schéma enrichi
2.3 Entity DocumentSecure (modifications)
src/modules/documents/entities/
└── document-secure.entity.ts # Δ6: 5 champs (deleted_at, deletion_reason, legal_lock, legal_lock_reason, legal_lock_until)
2.4 Migration (nouveau)
src/database/migrations/
└── YYYYMMDDHHMMSS-add-document-status-fields.ts # Δ6 (5 champs)
2.5 Diagrammes Mermaid
Graphe de dépendances des composants
graph TD
subgraph "Module Download"
CTRL["download.controller.ts"]
SVC["download.service.ts"]
ERR["download.errors.ts"]
GUARD["document-access.guard.ts"]
INTCPT["audit-reject.interceptor.ts"]
end
subgraph "Module Audit"
AUDIT["audit-download.service.ts"]
end
subgraph "Module Documents"
ENTITY["document-secure.entity.ts"]
MIG["migration: add-document-status-fields"]
end
subgraph "Externe"
S3["s3-presign.service.ts"]
JWT["OidcJwtAuthGuard"]
DB[(PostgreSQL)]
end
CTRL -->|"@UseGuards"| JWT
CTRL -->|"@UseGuards"| GUARD
CTRL -->|"@UseInterceptors"| INTCPT
CTRL --> SVC
SVC --> S3
SVC -->|"fail-closed Δ1"| AUDIT
GUARD -->|"check owner/partage/B2B Δ3-Δ5"| ENTITY
GUARD --> DB
INTCPT -->|"log rejets 401/403/404 Δ9"| AUDIT
SVC --> ERR
GUARD --> ERR
ENTITY -->|"5 champs Δ6"| MIG
MIG --> DB
Diagramme de séquence — Téléchargement nominal (owner)
sequenceDiagram
actor Client
participant CTRL as download.controller
participant JWT as OidcJwtAuthGuard
participant GUARD as document-access.guard
participant SVC as download.service
participant AUDIT as audit-download.service
participant S3 as s3-presign.service
Client->>CTRL: GET /documents/:id/download
CTRL->>JWT: canActivate()
JWT-->>CTRL: ✓ JWT valide
CTRL->>GUARD: canActivate()
GUARD->>GUARD: check owner (PD-46)
GUARD->>GUARD: check statut document (Δ5)
GUARD-->>CTRL: ✓ accès autorisé
CTRL->>SVC: getDownloadUrl(documentId, userId)
SVC->>AUDIT: emit(DOWNLOAD_REQUESTED)
alt Audit OK
AUDIT-->>SVC: ✓
SVC->>S3: getSignedUrl(GetObjectCommand, TTL=300s)
S3-->>SVC: presignedUrl
SVC-->>CTRL: { url, expiresIn: 300 }
CTRL-->>Client: 200 { url, expiresIn }
else Audit KO (fail-closed Δ1)
AUDIT-->>SVC: ✗ erreur
SVC-->>CTRL: ERR-63-11 (503)
CTRL-->>Client: 503 Service Unavailable
end
Diagramme de séquence — Rejet précoce (intercepteur Δ9)
sequenceDiagram
actor Client
participant INTCPT as audit-reject.interceptor
participant JWT as OidcJwtAuthGuard
participant AUDIT as audit-download.service
Client->>INTCPT: GET /documents/:id/download
INTCPT->>JWT: canActivate()
JWT-->>INTCPT: ✗ 401 Unauthorized
INTCPT->>AUDIT: emit(DOWNLOAD_REJECTED, { reason: "401", documentId })
AUDIT-->>INTCPT: ✓
INTCPT-->>Client: 401 Unauthorized
3. Mapping Invariants → Mécanismes
| Invariant | Mécanisme | Composant | Observable |
| INV-63-01 | Backend ne lit pas le contenu | S3PresignService | GetObjectCommand sans read |
| INV-63-02 | Aucune clé exposée | DownloadResponseDto | Inspection payload |
| INV-63-03 | TTL 5 min | DOWNLOAD_TTL_SECONDS | URL expire après 300s |
| INV-63-04 | Auth requise | OidcJwtAuthGuard | 401 sans JWT |
| INV-63-05 | Audit fail-closed + rejets précoces | DownloadService + AuditRejectInterceptor | Δ1 + Δ9 : throw si audit échoue, log 401/403/404 |
| INV-63-06 | Révocation bloque | DocumentAccessGuard | Δ3/Δ4 : check révocation |
| INV-63-07 | Multi-cloud | ConfigService | Même code OVH/AWS |
| INV-63-08 | Droits explicites | DocumentAccessGuard | Δ3/Δ4 : check explicite |
| INV-63-09 | Pas d'altération WORM | Aucune opération PUT | Vérifiable avant/après |
| INV-63-10 | Erreurs neutres | DownloadError | Messages redacted |
4. Mapping Critères → Tests
| CA | Mécanisme | Test(s) |
| CA-63-01 | DownloadService.getDownloadUrl() | TC-INT-01 |
| CA-63-02 | OidcJwtAuthGuard | TC-INT-04 |
| CA-63-03 | DocumentAccessGuard | TC-ERR-02, TC-INT-06 |
| CA-63-04 | DocumentAccessGuard (révocation) | TC-ERR-03, TC-INT-05 |
| CA-63-05 | TTL constant | TC-INT-15, TC-E2E-05 |
| CA-63-06 | Inspection payload/logs | TC-SEC-01, TC-SEC-16 |
| CA-63-07 | AuditDownloadService + AuditRejectInterceptor | TC-NOM-06, TC-INT-14, ST-63-12 |
| CA-63-08 | Pas d'opération PUT | TC-INT-13 |
| CA-63-09 | DocumentAccessGuard (3 flux) | TC-NOM-02/03/04 |
| CA-63-10 | Config multi-provider | TC-INT-11/12 |
4b. Mapping ERR → Mécanismes/Tâches
| Code erreur | Composant | Tâche(s) | Test(s) |
| ERR-63-01 (400) | Validation DTO (existant) | - | TC-ERR-01 |
| ERR-63-02 (401) | OidcJwtAuthGuard (existant) | Tâche 15 (intercepteur) | TC-INT-04 |
| ERR-63-03 (403) | DocumentAccessGuard | Tâches 6-8 | TC-ERR-02, TC-INT-06 |
| ERR-63-04 (403) | DocumentAccessGuard (révocation) | Tâches 6-8 | TC-ERR-03, TC-INT-05 |
| ERR-63-05 (404) | Query DB (existant) | Tâche 15 (intercepteur) | TC-INT-06 |
| ERR-63-06 (410) | DocumentAccessGuard (statut) | Tâche 8 | TC-INT-07 |
| ERR-63-07 (423) | DocumentAccessGuard (legal_lock) | Tâche 8 | TC-INT-08 |
| ERR-63-08 (429) | Rate limiter global (optionnel) | Hors périmètre | - |
| ERR-63-09 (500) | Exception filter NestJS (existant) | - | TC-INT-10 |
| ERR-63-10 (503) | S3PresignService (existant) | - | TC-INT-09 |
| ERR-63-11 (503) | DownloadService (fail-closed) | Tâche 12 | TC-INT-14 |
5. Plan de tâches
Phase 0 — Vérifications préalables (go/no-go)
| # | Tâche | Agent | Critère go/no-go | Action si no-go |
| 0a | Vérifier table document_shares | agent-developer | Table existe | Créer ticket PD-XXX |
| 0b | Vérifier claims JWT tenant_id/roles | agent-developer | Claims présents | Ticket config Keycloak |
| 0c | Vérifier RLS vault_secure.documents | agent-developer | RLS active | Documenter migration requise |
| 0d | Vérifier schéma audit enrichi | agent-developer | Service compatible | Adapter AuditService |
Règle : Si un critère échoue, escalader avant Phase 1. Ne pas bloquer l'implémentation sans décision explicite.
Phase 1 — Migration et Entity (Δ6)
| # | Tâche | Agent | Fichiers | Dépend de |
| 1 | Créer migration add-document-status-fields (5 champs) | agent-developer | migrations/*.ts | 0a-0d |
| 2 | Ajouter champs Entity DocumentSecure | agent-developer | document-secure.entity.ts | 1 |
| 3 | Tests unitaires Entity | agent-qa-unit-integration | document-secure.entity.spec.ts | 2 |
Champs migration : - deleted_at TIMESTAMP NULL - deletion_reason VARCHAR(50) NULL - legal_lock BOOLEAN DEFAULT false - legal_lock_reason VARCHAR(50) NULL - legal_lock_until TIMESTAMP NULL
Phase 2 — Codes d'erreur (Δ2, Δ8)
| # | Tâche | Agent | Fichiers | Dépend de |
| 4 | Ajouter ERR-63-11 + mapping ERR-46→63 | agent-developer | download.errors.ts | - |
| 5 | Tests codes d'erreur | agent-qa-unit-integration | download.errors.spec.ts | 4 |
Phase 3 — Guard enrichi (Δ3, Δ4, Δ5)
| # | Tâche | Agent | Fichiers | Dépend de |
| 6 | Implémenter check partage actif | agent-developer | document-access.guard.ts | 2 |
| 7 | Implémenter check B2B co-détention | agent-developer | document-access.guard.ts | 6 |
| 8 | Implémenter check statut document | agent-developer | document-access.guard.ts | 7 |
| 9 | Tests unitaires guard | agent-qa-unit-integration | document-access.guard.spec.ts | 8 |
Note tâche 8 : Pour statut ARCHIVED, vérifier accessibilité S3 via HEAD Object. Si non accessible (Glacier non restauré), retourner ERR-63-10 (503).
Phase 4 — Audit fail-closed (Δ1, Δ7)
| # | Tâche | Agent | Fichiers | Dépend de |
| 10 | Enrichir schéma événement audit | agent-developer | audit-download.service.ts | - |
| 11 | Implémenter fail-closed dans service | agent-developer | download.service.ts | 10 |
| 12 | Tests fail-closed | agent-qa-unit-integration | download.service.spec.ts | 11 |
Phase 5 — Intercepteur audit global (Δ9)
| # | Tâche | Agent | Fichiers | Dépend de |
| 13 | Créer AuditRejectInterceptor | agent-developer | audit-reject.interceptor.ts | 10 |
| 14 | Enregistrer intercepteur sur controller | agent-developer | download.controller.ts | 13 |
| 15 | Tests intercepteur (401/403/404) | agent-qa-unit-integration | audit-reject.interceptor.spec.ts | 14 |
Phase 6 — Tests d'intégration
| # | Tâche | Agent | Fichiers | Dépend de |
| 16 | Tests intégration endpoint | agent-qa-unit-integration | download.controller.e2e-spec.ts | 9, 12, 15 |
Total : 16 tâches (+ 4 vérifications Phase 0)
6. Code Contracts
Voir fichier séparé PD-63-code-contracts.yaml.
7. Contraintes techniques
7.1 Dépendances inter-PD
| Story | Statut | Nature |
| PD-46 | DONE | Base de l'endpoint — réutilisation complète |
| PD-37 | DONE | HSM audit signature — H-63-03 |
| PD-31 | DONE | Pattern audit — réutilisation |
| PD-60 | DONE | Endpoint miroir (upload) |
7.2 Framework de test
- Runner : Jest (existant)
- Tests d'intégration : Avec services réels (PostgreSQL, Redis)
- Mocks : S3 via mock-aws-sdk
- Environnement CI :
DATABASE_URL, REDIS_URL, S3_* vars
7.3 Compatibilité ESM/CJS
- Projet en CommonJS (pas d'ESM)
- Pas de dépendance ESM-only identifiée
8. Hypothèses d'implémentation
| ID | Hypothèse | Impact si faux | Vérification (Phase 0) |
| H-IMPL-01 | Table document_shares existe pour partage actif | Créer table/entity si absente | Tâche 0a |
| H-IMPL-02 | Claims JWT tenant_id et roles disponibles (B2B) | Configurer Keycloak si absent | Tâche 0b |
| H-IMPL-03 | RLS active sur vault_secure.documents | Vérifier migration RLS | Tâche 0c |
| H-IMPL-04 | Service audit supporte schéma enrichi | Adapter AuditService si nécessaire | Tâche 0d |
9. Points de vigilance
| # | Point | Risque | Mitigation |
| 1 | Audit fail-closed peut bloquer téléchargements | Service audit indisponible → 503 pour tous | Infrastructure : Haute disponibilité service audit (réplication, health checks). Applicatif : Fail-closed strict, pas de retry applicatif — conformité INV-63-05. |
| 2 | Partage/B2B = TODO dans guard actuel | Régression possible | Tests exhaustifs (Phase 3) |
| 3 | Champs modèle (5 champs) = migration | Downtime potentiel | Migration non-bloquante (ajout colonnes NULL) |
| 4 | Timing attack (TC-SEC-12) | Oracle d'énumération | Latence constante |
| 5 | Rejets précoces non journalisés | Non-conformité traçabilité | Intercepteur audit global (Phase 5) |
10. Estimation
| Phase | Tâches | Complexité |
| 0 — Vérifications | 4 | Faible |
| 1 — Migration/Entity | 3 | Faible |
| 2 — Codes d'erreur | 2 | Faible |
| 3 — Guard enrichi | 4 | Moyenne |
| 4 — Audit fail-closed | 3 | Moyenne |
| 5 — Intercepteur audit | 3 | Moyenne |
| 6 — Tests intégration | 1 | Moyenne |
| Total | 20 | Moyenne |
Généré par : Claude (Orchestrateur) Date : 2026-02-20 Version : v2 — Corrections Gate 5 v1
Corrections appliquées (Gate 5 v1)
| Écart | Correction |
| ECT-01 | Ajout Phase 5 — Intercepteur audit global (Δ9) pour journaliser rejets 401/403/404 |
| ECT-02 | Migration complétée : 5 champs (pas 2) |
| AMB-01 | Note tâche 8 : ARCHIVED → HEAD Object → 503 |
| ECT-03 | Ajout section 4b : Mapping ERR → Mécanismes/Tâches |
| SEC-01 | Point de vigilance 1 reformulé : distinction infra vs appli |
| AMB-02 | Ajout Phase 0 — Vérifications préalables (go/no-go) |