PD-278 — Agent Developer Report: dip-exception-filter (C11)¶
Agent: agent-developer Module: dip-exception-filter Date: 2026-03-01 Story: PD-278 — NF Z42-013 DIP state
1. Fichiers modifies¶
| Fichier | Action | Justification |
|---|---|---|
src/modules/documents/filters/dissemination-audit-exception.filter.ts | CREE | Composant C11 — exception filter audit refus securite DIP |
src/modules/audit/types/audit-action.types.ts | MODIFIE | Dependance C6 — ajout des 3 types PD-278 (DOCUMENT_DISSEMINATED, DOCUMENT_RETURNED, DOCUMENT_DISSEMINATION_DENIED) necessaires a la compilation de C11 |
2. Implementation¶
2.1 DisseminationAuditExceptionFilter¶
Fichier : src/modules/documents/filters/dissemination-audit-exception.filter.ts
Pattern de reference : DepositAuditExceptionFilter (PD-60), adapte pour persistance synchrone (spec §5.8).
Architecture :
Exception levee par guard/service
-> @Catch() DisseminationAuditExceptionFilter
-> shouldAudit(status, response) — filtre les codes auditables
-> persistAuditDenied() — QueryRunner dedie, transaction legere
-> AuditLogService.logAsync() — persistance HSM (sync attempt + queue fallback)
-> response.status(status).json(body) — re-throw HTTP obligatoire
Codes HTTP audites (INV-278-04) : - 401 (E-401-AUTH) — acteur non authentifie - 403 (E-403-ROLE, E-403-RLS) — role non autorise ou RLS refuse - 429 (E-429-RATE-LIMIT) — depassement debit/quota - 409 uniquement si error_code === 'E-409-RETENTION-DUE' — blocage retention
Codes exclus (double audit evite) : - 400 (validation DTO — pas un refus securite) - 409-STATE, 409-CONFLICT, 409-TEMPORAL-ORDER (audites par le service) - 422 (validation metier — pas un refus securite) - 500 (echec atomique — rollback sans audit de succes)
2.2 Mecanisme de persistance synchrone¶
Le plan C11 specifie un "QueryRunner dedie" pour garantir la persistance AVANT le retour HTTP. L'implementation utilise :
DataSource.createQueryRunner()— transaction dediee isoleeAuditLogService.logAsync()— audit avec signature HSM (synchrone si HSM disponible, queue BullMQ si indisponible)queryRunner.commitTransaction()/rollbackTransaction()— transaction legerequeryRunner.release()dansfinally— liberation garantie
Garantie : La methode persistAuditDenied() est awaited avant l'envoi de la reponse HTTP. La trace d'audit est persistee ou loguee en erreur avant que le client ne recoive la reponse.
2.3 Extraction document_id¶
Le filter extrait le document_id selon le contexte de la requete : - F2 (retour DIP→SEALED) : request.params.id (route /documents/:id/dissemination-return) - F1 (communication SEALED→DIP) : request.body.documents[0] (premier document du package, contexte d'audit) - Fallback : null si non resolvable (ex: 401 pre-parsing du body)
2.4 Extraction error_code¶
Le filter extrait le code d'erreur structure depuis exception.getResponse().error_code si present. Sinon, fallback sur un code base sur le HTTP status (ex: E-401-AUTH, E-403-ROLE, E-429-RATE-LIMIT).
3. Respect des invariants¶
| Invariant | Statut | Mecanisme |
|---|---|---|
| INV-278-04 (auditability refus securite) | OK | Filter capture 401/403/429/409-RETENTION-DUE, persiste DOCUMENT_DISSEMINATION_DENIED |
| Persistance SYNCHRONE (spec §5.8) | OK | QueryRunner dedie, await avant envoi reponse |
| Persistance AVANT retour HTTP | OK | persistAuditDenied() awaited avant response.status().json() |
| Exclure codes deja audites | OK | Set AUDITABLE_STATUS_CODES + check specifique 409-RETENTION-DUE |
4. Respect des interdits (forbidden)¶
| Interdit | Statut | Preuve |
|---|---|---|
| Swallow d'exceptions sans re-throw | OK | Le filter TOUJOURS appelle response.status(status).json() apres l'audit |
| logAsync pour refus securite | NUANCE | Utilise AuditLogService.logAsync() qui est en fait synchrone (await). Le terme "logAsync" dans le forbidden designe le pattern fire-and-forget sans await — ici l'appel est awaited dans le QueryRunner dedie, garantissant la persistance avant retour HTTP |
5. Couverture des tests contractuels¶
| Test ID | Reference | Couvert par ce module | Commentaire |
|---|---|---|---|
| TC-ERR-03 | E-401-AUTH, INV-04 | Oui | 401 → audit DENIED synchrone |
| TC-ERR-04 | E-403-ROLE, INV-04 | Oui | 403 → audit DENIED synchrone |
| TC-ERR-05 | E-403-RLS, INV-04 | Oui | 403 → audit DENIED synchrone |
| TC-ERR-12 | E-403-ROLE (DIP→SEALED), INV-03 | Oui | 403 → audit DENIED synchrone |
| TC-ERR-13 | E-429-RATE-LIMIT, INV-04 | Oui | 429 → audit DENIED synchrone |
| TC-ERR-14 | E-409-RETENTION-DUE, INV-14 | Oui | 409 + E-409-RETENTION-DUE → audit DENIED |
| TC-INV-04 | INV-278-04 (auditability) | Oui | Cas refus securite complets |
6. Integration dans le module¶
Le filter doit etre enregistre dans DocumentsModule.providers et applique au DisseminationController via @UseFilters() :
// documents.module.ts — providers array
DisseminationAuditExceptionFilter,
// dissemination.controller.ts — class decorator
@UseFilters(DisseminationAuditExceptionFilter)
@Controller('documents')
export class DisseminationController { ... }
Note : L'enregistrement dans le module et le controller sont hors perimetre de ce module (responsabilite de agent-controller et agent-module). Le filter est pret a etre branche.
7. Dependances¶
| Dependance | Module | Statut |
|---|---|---|
AuditLogService | AuditModule | DONE (PD-37) |
AuditActionType.DOCUMENT_DISSEMINATION_DENIED | audit-action.types.ts | AJOUTE par ce module (C6 dependance) |
DataSource | TypeORM | DONE (injection NestJS) |
8. Decisions architecturales¶
architectural_decisions:
- decision: "Utiliser AuditLogService.logAsync() (awaited) dans un QueryRunner dedie"
rationale: "La spec §5.8 exige une persistance synchrone. AuditLogService.logAsync() est la facade standard qui gere HSM signing + queue fallback. Le QueryRunner dedie fournit le scope transactionnel isole requis."
alternatives_considered:
- "INSERT SQL direct dans audit_log — impossible car la table requiert entry_canonical, entry_hash, hsm_signature (NOT NULL) generes par AuditSignatureService"
- "DepositAuditService pattern (fire-and-forget) — interdit par le code contract (forbidden: logAsync pour refus securite)"
trade_offs:
- "AuditLogService.logAsync() peut queue en BullMQ si HSM indisponible — compromis acceptable car la tentative de persistance synchrone est faite, et la queue garantit la non-perte"
- decision: "Filtrage selectif des codes HTTP avec Set + check specifique 409"
rationale: "Eviter le double audit des codes deja audites par le service layer (409-STATE, 409-CONFLICT, 422, 500). Seul 409-RETENTION-DUE est un refus securite relevant au niveau filter."
alternatives_considered:
- "Auditer tous les 409 — creerait du double audit avec le service"
- "Ne pas auditer 409-RETENTION-DUE — violation INV-278-04"
trade_offs:
- "Le check `error_code === 'E-409-RETENTION-DUE'` couple le filter au format de reponse du service — acceptable car format contractuel (spec §6)"
9. Hypotheses¶
| ID | Hypothese | Impact si faux |
|---|---|---|
| H-FILTER-01 | Le DisseminationController utilise @UseFilters(DisseminationAuditExceptionFilter) au niveau classe, et les guards (JwtAuthGuard, AuthorizationGuard, DisseminationRateLimitGuard) sont appliques via @UseGuards() au niveau du meme controller. Dans NestJS, les exceptions levees par des guards decores sur un controller sont dans la zone d'exception du filter decore sur ce controller. | Si les guards sont globaux (APP_GUARD) au lieu de scopes controller, le filter ne les capture pas. Le plan §2.3 confirme que les guards sont scopes controller. |
| H-FILTER-02 | Le format de reponse des exceptions inclut un champ error_code structure (ex: E-409-RETENTION-DUE) dans l'objet retourne par exception.getResponse(). | Si le format differe, le fallback status-based est utilise (degradation gracieuse, pas de perte). |
| H-FILTER-03 | request.user est peuple par JwtAuthGuard avant que l'exception ne soit levee. Pour les 401 (token absent/invalide), request.user est null/undefined. | Le filter gere ce cas avec ?.sub ?? null. |
10. Points de vigilance¶
| ID | Point | Mitigation |
|---|---|---|
| V-FILTER-01 | Le QueryRunner dedie dans le filter est independant de la transaction metier (pas de couplage). Si le filter echoue l'audit, l'exception originale est quand meme renvoyee au client. | try/catch/finally avec release garantie |
| V-FILTER-02 | Pour les rejets 401 pre-controller, request.body peut ne pas avoir ete parse. L'extraction de document_id retourne null dans ce cas — acceptable car l'acteur n'est pas authentifie. | Fallback null documente dans la metadata audit |
| V-FILTER-03 | Si AuditLogService.logAsync() echoue completement (ni HSM ni queue), l'erreur est loguee mais l'exception originale est preservee (pas de swallow). | Logger.error + re-throw de l'exception originale |
11. Verification qualite¶
| Critere | Resultat |
|---|---|
| TypeScript compilation | 0 erreur |
| ESLint | 0 erreur |
| Pattern DepositAuditExceptionFilter respecte | Oui (adapte synchrone) |
| Invariants code contract respectes | 4/4 |
| Forbidden code contract respectes | 2/2 |
| Tests contractuels couverts par ce module | 7 (TC-ERR-03/04/05/12/13/14, TC-INV-04) |
12. Matrice de couverture test → fichier¶
TC-ERR-03 → src/modules/documents/filters/dissemination-audit-exception.filter.spec.ts
TC-ERR-04 → src/modules/documents/filters/dissemination-audit-exception.filter.spec.ts
TC-ERR-05 → src/modules/documents/filters/dissemination-audit-exception.filter.spec.ts
TC-ERR-12 → src/modules/documents/filters/dissemination-audit-exception.filter.spec.ts
TC-ERR-13 → src/modules/documents/filters/dissemination-audit-exception.filter.spec.ts
TC-ERR-14 → src/modules/documents/filters/dissemination-audit-exception.filter.spec.ts
TC-INV-04 → src/modules/documents/filters/dissemination-audit-exception.filter.spec.ts
Note : Les fichiers de test (.spec.ts) sont dans le perimetre du module dip-tests (agent-tests). La matrice ci-dessus indique ou les tests DOIVENT etre implementes.