PD-60 — Plan d'implémentation : Dépôt probatoire d'un document (Upload)
Référence spec : PD-60-specification.md (v2 — amendée 3 février 2026, incluant INV-60-15..17 et CA-60-19..21). Toutes les règles référencées ci-dessous (INV-60-01..17, CA-60-01..21, ERR-60-01..12) sont présentes dans la spécification canonique amendée.
1. Découpage en composants
Composants impactés
| Composant | Responsabilité PD-60 | Fichiers principaux |
| DepositController (nouveau) | Endpoints POST /documents/upload et GET /documents/deposits/:id/proof, validation DTO, orchestration | src/modules/documents/controllers/deposit.controller.ts |
| DepositService (nouveau) | Logique métier : validation, idempotence, staging, confirmation probatoire, récupération preuve | src/modules/documents/services/deposit.service.ts |
| ProofReceiptService (nouveau) | Génération et vérification du reçu JWS (proof_receipt) | src/modules/documents/services/proof-receipt.service.ts |
| DepositAuditService (nouveau) | Écriture traces d'audit spécifiques au dépôt probatoire | src/modules/documents/services/deposit-audit.service.ts |
| NtpHealthService (nouveau) | Vérification dérive NTP, exposition statut horodatage | src/modules/documents/services/ntp-health.service.ts |
| AuditModule (existant) | Journalisation append-only, signature HSM | src/modules/audit/ |
| CryptoModule (existant) | HashService (SHA-256 pour fingerprint), HSM signing | src/modules/crypto/ |
| UploadModule (existant) | Réception contenu opaque (multipart streaming) | src/modules/upload/ |
| StorageModule (existant) | Stockage S3 du contenu opaque | src/modules/storage/ |
| AuthModule (existant) | JWT guards, RLS context | src/modules/auth/ |
| Database (migration) | Table deposits, table deposit_stagings | src/database/migrations/ |
Endpoints
| Méthode | Path | Responsabilité | Référence spec |
POST | /documents/upload | Dépôt probatoire (FN-60-01..04), staging dégradé | Spec §5 |
GET | /documents/deposits/:id/proof | Récupération du reçu probatoire d'un dépôt existant | ERR-60-09 (refus si staging) |
Nouveaux fichiers
src/modules/documents/
├── controllers/
│ └── deposit.controller.ts
├── dto/
│ ├── create-deposit.dto.ts
│ └── deposit-response.dto.ts
├── entities/
│ ├── deposit.entity.ts
│ └── deposit-staging.entity.ts
├── services/
│ ├── deposit.service.ts
│ ├── proof-receipt.service.ts
│ ├── deposit-audit.service.ts
│ └── ntp-health.service.ts
└── __tests__/
├── deposit.controller.spec.ts
├── deposit.service.spec.ts
├── proof-receipt.service.spec.ts
└── deposit-audit.service.spec.ts
2. Flux techniques
FN-60-01 — Confirmation probatoire nominale
Client DepositController DepositService ProofReceiptService AuditModule S3
│ │ │ │ │ │
│ POST /documents/upload │ │ │ │ │
│ [JWT + body multipart] │ │ │ │ │
│────────────────────────────>│ │ │ │ │
│ │ validate DTO │ │ │ │
│ │ (class-validator) │ │ │ │
│ │─────────────────────>│ │ │ │
│ │ │ 1. Check auth+RLS │ │ │
│ │ │ 2. Check notice_ack │ │ │
│ │ │ + notice_text │ │ │
│ │ │ 3. SHA-256(content) │ │ │
│ │ │ == fingerprint? │ │ │
│ │ │ 4. Check idempotence│ │ │
│ │ │ (client_request_id) │ │
│ │ │ 5. Check NTP health │ │ │
│ │ │ │ │ │
│ │ │ 6. Store content ───────────────────────────────────>│
│ │ │ │ │ │
│ │ │ 7. Write audit ─────────────────────>│ │
│ │ │ (append-only + │ │ HSM sign │
│ │ │ HSM signature) │ │ │
│ │ │ ← audit_result │ │ │
│ │ │ │ │ │
│ │ │ 8. Generate JWS ───>│ (tenté INDÉPENDAMMENT │
│ │ │ │ du résultat audit) │
│ │ │ │ sign(payload, │ │
│ │ │ │ HSM private key)│ │
│ │ │ │<── receipt_result │ │
│ │ │ │ │ │
│ │ │ 9. Check double │ │ │
│ │ │ condition : │ │ │
│ │ │ audit OK AND │ │ │
│ │ │ receipt OK ? │ │ │
│ │ │ → OUI: INSERT deposit (PROBATIVE_DEPOSIT) │
│ │ │ → NON: INSERT staging (NON_PROBATIVE_STAGING) │
│ │ │ │ │ │
│ │<─────────────────────│ │ │ │
│ 201 Created │ │ │ │ │
│ { deposit_id, timestamp, │ │ │ │ │
│ proof_receipt, ... } │ │ │ │ │
│<────────────────────────────│ │ │ │ │
FN-60-03 — Contexte dégradé (staging)
DepositService tente audit ET receipt indépendamment.
Si l'un ou l'autre échoue (ou les deux) :
→ INSERT deposit_stagings (NON_PROBATIVE_STAGING, degraded_reason indique le(s) composant(s) en échec)
→ Retourne staging_id, act_type=NON_PROBATIVE_STAGING
→ PAS de deposit_id, PAS de proof_receipt, PAS de timestamp
FN-60-04 — Confirmation après staging
Client rejoue POST /documents/upload avec même client_request_id
→ DepositService détecte staging existant + conditions OK
→ Applique FN-60-01 (promotion staging → deposit)
→ DELETE staging, INSERT deposit
ERR-60-09 — Demande de preuve sur staging
Client appelle GET /documents/deposits/:staging_id/proof
→ DepositService cherche par id dans deposits (non trouvé)
→ DepositService cherche par id dans deposit_stagings (trouvé, act_type=NON_PROBATIVE_STAGING)
→ Refus explicite : HTTP 400 { error: "PROOF_NOT_AVAILABLE", message: "staging has no probative status" }
→ Audit entry : DEPOSIT_PROOF_REFUSED_STAGING
2b. Diagrammes Mermaid
Graphe de dépendances des modules
graph TD
Client([Client HTTP])
subgraph DocumentsModule
DC[DepositController]
DS[DepositService]
PRS[ProofReceiptService]
DAS[DepositAuditService]
NTHS[NtpHealthService]
end
subgraph Modules existants
AM[AuditModule]
CM[CryptoModule / HashService]
UM[UploadModule]
SM[StorageModule / S3]
AuthM[AuthModule / JWT + RLS]
end
subgraph Persistance
DB[(PostgreSQL<br/>deposits + deposit_stagings)]
end
Client -->|POST /documents/upload<br/>GET /deposits/:id/proof| DC
DC --> AuthM
DC --> DS
DS --> CM
DS --> NTHS
DS --> UM
DS --> SM
DS --> DAS
DS --> PRS
DS --> DB
DAS --> AM
PRS --> CM
AM -->|HSM sign| CM
Séquence — Dépôt probatoire nominal (FN-60-01)
sequenceDiagram
actor C as Client
participant DC as DepositController
participant Auth as AuthModule
participant DS as DepositService
participant CM as CryptoModule
participant NTHS as NtpHealthService
participant SM as StorageModule (S3)
participant DAS as DepositAuditService
participant PRS as ProofReceiptService
participant DB as PostgreSQL
C->>DC: POST /documents/upload [JWT + multipart]
DC->>Auth: Validate JWT + extract RLS context
Auth-->>DC: userId, vaultId
DC->>DS: createDeposit(dto, user)
DS->>DS: Validate notice_ack + notice_text
DS->>CM: SHA-256(content)
CM-->>DS: fingerprint
DS->>DS: Compare fingerprint vs dto.fingerprint
DS->>DS: Check idempotence (client_request_id)
DS->>NTHS: checkDrift()
NTHS-->>DS: NTP OK
DS->>SM: store(content, key)
SM-->>DS: s3_key
par Audit + Receipt (indépendants)
DS->>DAS: writeAudit(DEPOSIT_CREATED, metadata)
DAS-->>DS: audit OK
and
DS->>PRS: generateReceipt(payload)
PRS->>CM: HSM sign(payload)
CM-->>PRS: JWS
PRS-->>DS: proof_receipt
end
alt audit OK AND receipt OK
DS->>DB: INSERT deposits (PROBATIVE_DEPOSIT)
else un ou les deux en échec
DS->>DB: INSERT deposit_stagings (NON_PROBATIVE_STAGING)
end
DS-->>DC: depositResult
DC-->>C: 201 Created {deposit_id, timestamp, proof_receipt}
Séquence — Confirmation après staging (FN-60-04)
sequenceDiagram
actor C as Client
participant DS as DepositService
participant DAS as DepositAuditService
participant PRS as ProofReceiptService
participant DB as PostgreSQL
C->>DS: POST /documents/upload (même client_request_id)
DS->>DB: Lookup staging par client_request_id
DB-->>DS: staging trouvé
DS->>DAS: writeAudit(DEPOSIT_CREATED, metadata)
DAS-->>DS: audit OK
DS->>PRS: generateReceipt(payload)
PRS-->>DS: proof_receipt
alt double condition OK
DS->>DB: DELETE deposit_stagings
DS->>DB: INSERT deposits (PROBATIVE_DEPOSIT)
DS-->>C: 201 Created {deposit_id, proof_receipt}
else échec persistant
DS->>DB: UPDATE deposit_stagings (degraded_reason)
DS-->>C: 201 Created {staging_id, NON_PROBATIVE_STAGING}
end
3. Mapping invariants → mécanismes
| Invariant ID | Exigence | Mécanisme | Composant | Observable | Risque |
| INV-60-01 | Auth + autorisation obligatoire | @UseGuards(JwtAuthGuard, AuthorizationGuard) + @Roles('user') | AuthModule / DepositController | 401/403 si non autorisé | Faible — patterns existants |
| INV-60-02 | Atomicité confirmation | Transaction unique englobante avec sérialisation inter-table : L'ensemble du traitement s'exécute dans une unique transaction PostgreSQL explicite (BEGIN … COMMIT/ROLLBACK). Les trois phases ci-dessous sont toutes à l'intérieur de cette transaction. Phase 0 (sérialisation, dans la TX) — SELECT pg_advisory_xact_lock(hashtext(user_id::text || '/' || client_request_id)) acquiert un verrou advisory lié à la transaction courante. Ce verrou est maintenu jusqu'au COMMIT ou ROLLBACK de la TX englobante, garantissant l'exclusion mutuelle entre les tables deposits et deposit_stagings pour le même (user_id, client_request_id). Phase A (opérations externes, dans la TX) — audit write et receipt generation sont tentés indépendamment (voir INV-60-10/INV-60-16). Ces opérations appellent des services externes (HSM) mais le verrou advisory reste acquis car la TX est toujours ouverte. Ces opérations sont idempotentes ou annulables côté externe. Phase B (INSERT final, dans la TX) — selon les résultats de Phase A : si les deux ont réussi → INSERT deposit (PROBATIVE_DEPOSIT) ; si l'un a échoué → INSERT staging (NON_PROBATIVE_STAGING). Puis COMMIT. L'atomicité d'INV-60-02 porte sur la TX entière : l'INSERT final est tout-ou-rien, et le verrou empêche toute interleaving concurrente. Implémentation NestJS : DataSource.transaction(async (manager) => { /* Phase 0 + A + B */ }) | DepositService | Pas d'état partiel observable en DB ; pas de doublon inter-table | Moyen — latence HSM maintient la TX ouverte en Phase A |
| INV-60-03 | deposit_id unique + timestamp UTC | deposit_id = UUIDv7, existence_timestamp_utc = horodatage UTC tronqué à la seconde (format YYYY-MM-DDTHH:mm:ssZ, sans millisecondes). Implémentation : new Date().toISOString().replace(/\.\d{3}Z$/, 'Z') | DepositService | Champs présents dans response + DB, format sans millisecondes | Faible |
| INV-60-04 | Pas de contenu intelligible | Le contenu est traité comme Buffer opaque. Aucun log ne contient le contenu. Response ne retourne que des références | DepositController / AuditModule | Grep logs/response pour marqueur de test | Moyen — vigilance logs |
| INV-60-05 | Reçu JWS vérifiable hors plateforme | ProofReceiptService.sign() → JWS compact (RS256/ES256). Clé publique publiée à GET /documents/.well-known/jwks.json (endpoint public, sans JWT). Dépendance explicite : la vérification tiers nécessite que le tiers accède à cet endpoint OU dispose d'une copie de la clé publique obtenue par un canal externe (documentation, export). Le plan prévoit l'endpoint JWKS comme seul mécanisme de distribution. | ProofReceiptService | JWS décodable + signature valide avec clé publique | Moyen — gestion clé publique |
| INV-60-06 | Journalisation append-only | Utilise AuditModule existant (PD-37) : table audit.audit_log avec trigger PostgreSQL BEFORE UPDATE OR DELETE → RAISE EXCEPTION. Signature HSM ECDSA P-384. Note : la résistance à un acteur disposant d'un accès administrateur au stockage est hors périmètre de cette US (PC-60-13, spec §4 INV-60-06). | AuditModule | Entrée audit_log corrélée à client_request_id | Faible — infrastructure existante |
| INV-60-07 | Lien depositor + scope | depositor_identity_ref = JWT.sub, responsibility_scope_ref = JWT.tenant_id. Le champ tenant_id DOIT être présent dans le JWT ; requête rejetée sinon (HTTP 400). | DepositService | Champs dans response, receipt, audit | Faible |
| INV-60-08 | Détection altération | Reçu JWS contient document_fingerprint (SHA-256 du contenu). Tiers compare hash(document) vs fingerprint du reçu | ProofReceiptService | Vérification tiers échoue si document modifié | Faible |
| INV-60-09 | Statut explicite | Enum TypeORM : PROBATIVE_DEPOSIT / NON_PROBATIVE_STAGING. Champ act_type dans response | DepositService / Entities | Champ act_type dans chaque réponse | Faible |
| INV-60-10 | Double condition existence juridique | L'audit et le receipt sont tentés indépendamment (le receipt n'est pas conditionné au succès de l'audit). legal_existence_status=ESTABLISHED uniquement si les deux ont réussi (audit_trace_ref IS NOT NULL AND proof_receipt IS NOT NULL). Sinon → staging | DepositService | Vérification en base + response | Moyen |
| INV-60-11 | Staging en dégradé | Si l'audit OU le receipt échoue (l'autre peut réussir) → INSERT staging. Les deux opérations sont tentées indépendamment ; le staging enregistre laquelle a échoué (degraded_reason) | DepositService | staging_id retourné, pas de deposit_id | Moyen |
| INV-60-12 | Notice ack obligatoire + texte exact | DTO validation : probative_notice_ack (@IsBoolean() @Equals(true)) ET probative_notice_text (@IsString() @Equals(PROBATIVE_NOTICE_TEXT)). Décision d'implémentation (tracée) : La spec §3 impose probative_notice_ack=true comme condition d'acceptation système. CA-60-16 impose en complément que la « valeur exacte de notice acceptée » soit "Vous réalisez un acte probatoire daté.". Le champ probative_notice_text est la matérialisation côté requête de CA-60-16 : sans ce champ, il est impossible de vérifier que le client a soumis la valeur exacte (et non une valeur arbitraire), et les tests TC-INV-12 (correspondance textuelle exacte) et TC-NEG-03 (rejet de variations textuelles) deviennent irréalisables — constat confirmé comme BLOQUANT lors de la review v3 du plan. Ce champ est donc une conséquence directe de CA-60-16, pas un ajout de contrainte. Les deux conditions (ack=true + text=valeur exacte) sont complémentaires et non redondantes : ack matérialise le consentement (spec §3), text matérialise la vérification d'exactitude (CA-60-16) | CreateDepositDto | 400 si ack absent/false OU texte inexact | Faible |
| INV-60-13 | Répartition responsabilités | Champ responsibility_declaration dans le payload JWS du reçu : { depositor: "document_submitted", organization: "authorizations_mandates", probatiovault: "traceability_date_integrity" } | ProofReceiptService | Présence dans JWS payload | Faible |
| INV-60-14 | Obligation limitée | Notice contractuelle + absence de clauses supplémentaires dans le reçu | ProofReceiptService | Vérification syntaxique | Faible |
| INV-60-15 | Vérification SHA-256 serveur | crypto.createHash('sha256').update(content).digest('hex') comparé au document_fingerprint du DTO | DepositService | ERR-60-11 si mismatch | Faible |
| INV-60-16 | Injection de faute pour tests | Interface AuditWriter et ReceiptSigner injectables. En test : mocks qui lèvent des exceptions sélectivement | DepositService (DI) | Tests TC-NOM-09 reproductibles | Moyen — design DI |
| INV-60-17 | Limite testabilité juridique | Vérification syntaxique dans tests (présence des 3 volets) | Tests contractuels | Couverture partielle documentée | Faible |
4. Mapping critères d'acceptation → mécanismes
| Critère ID | Mécanisme(s) | Composant | Observable | Risque |
| CA-60-01 | JwtAuthGuard rejette requêtes sans token | AuthModule | HTTP 401 | Faible |
| CA-60-02 | AuthorizationGuard vérifie @Roles('user') | AuthModule | HTTP 403 | Faible |
| CA-60-03 | UUIDv7 pour deposit_id + horodatage UTC tronqué à la seconde (YYYY-MM-DDTHH:mm:ssZ) | DepositService | Champs dans response, format sans millisecondes | Faible |
| CA-60-04 | JWS payload contient tous les champs contractuels | ProofReceiptService | Décodage JWS | Faible |
| CA-60-05 | Transaction unique explicite englobant Phase 0 + A + B (DataSource.transaction()). Phase 0 : pg_advisory_xact_lock acquis dans la TX, maintenu jusqu'au COMMIT. Phase A : appels HSM (audit/receipt) exécutés pendant que la TX est ouverte (verrou maintenu). Phase B : INSERT final (deposit ou staging). COMMIT libère le verrou et finalise l'écriture. ROLLBACK annule tout. Aucune phase n'est hors TX | DepositService | Pas de row incomplète ; pas de doublon inter-table ; verrou effectif pendant toute la durée | Moyen — TX longue si HSM lent |
| CA-60-06 | Verrou advisory inter-table (Phase 0) + contrainte UNIQUE (user_id, client_request_id) par table. Logique applicative : SELECT par (user_id, client_request_id) dans deposits puis deposit_stagings (sous verrou) → si existe avec même document_fingerprint → retourner le deposit existant (idempotence) | DepositService | Même deposit_id retourné | Faible |
| CA-60-07 | Verrou advisory inter-table (Phase 0) + contrainte UNIQUE (user_id, client_request_id) par table. Logique applicative : SELECT par (user_id, client_request_id) → si existe avec document_fingerprint différent → HTTP 409 Conflict | DepositService | HTTP 409 Conflict | Faible |
| CA-60-08 | Aucun content dans logs/response. Log audit ne contient que des refs | DepositService / AuditModule | Grep marqueur absent | Moyen |
| CA-60-09 | Audit entry pour chaque tentative (succès/erreur/staging) | DepositAuditService | Entrée corrélée à client_request_id | Faible |
| CA-60-10 | JWS vérifiable avec clé publique obtenue via GET /documents/.well-known/jwks.json (endpoint public). Vérification tiers : (1) récupérer JWKS, (2) vérifier signature JWS, (3) comparer fingerprint vs hash document | ProofReceiptService | Test de vérification hors plateforme | Moyen |
| CA-60-11 | SHA-256(doc modifié) ≠ fingerprint du reçu | ProofReceiptService | Échec vérification | Faible |
| CA-60-12 | JWT.sub → depositor_identity_ref, JWT.tenant_id → responsibility_scope_ref | DepositService | Champs dans response + audit | Faible |
| CA-60-13 | @Equals(true) sur probative_notice_ack dans DTO | CreateDepositDto | HTTP 400 si ack absent ou false | Faible |
| CA-60-14 | Audit et receipt tentés indépendamment. Check après les deux : audit_trace_ref != null && proof_receipt != null → ESTABLISHED ; sinon → staging | DepositService | Response legal_existence_status | Moyen |
| CA-60-15 | Audit et receipt tentés indépendamment. Si l'un échoue → INSERT staging avec degraded_reason | DepositService | act_type=NON_PROBATIVE_STAGING | Moyen |
| CA-60-16 | @Equals("Vous réalisez un acte probatoire daté.") sur probative_notice_text dans DTO (matérialisation de CA-60-16, voir justification INV-60-12). Toute variation textuelle (casse, espace, ponctuation) est rejetée HTTP 400. La valeur exacte est embarquée dans le reçu JWS et l'entrée d'audit. Ce mécanisme complète ack=true (spec §3) sans le remplacer | CreateDepositDto / ProofReceiptService | HTTP 400 si texte inexact ; valeur exacte dans artefacts | Faible |
| CA-60-17 | responsibility_declaration dans JWS payload | ProofReceiptService | Présence dans payload | Faible |
| CA-60-18 | JWS signature invalide si audit_trace_ref altéré | ProofReceiptService | Échec vérif signature | Faible |
| CA-60-19 | SHA-256 serveur recalculé et comparé. Couverture test : TC-ERR-04 couvre « empreinte invalide/incohérente » (incluant ERR-60-11 hash mismatch). Observable : HTTP 422 FINGERPRINT_MISMATCH + audit DEPOSIT_FINGERPRINT_MISMATCH | DepositService | HTTP 422 si mismatch (TC-ERR-04) | Faible |
| CA-60-20 | JWS format compact, algo RS256/ES256 | ProofReceiptService | jose library vérifie | Faible |
| CA-60-21 | NtpHealthService vérifie offset NTP < 1s. Couverture test : TC-NOM-03 couvre le contexte dégradé (dont NTP drift > 1s via mock NtpHealthService). Observable : NON_PROBATIVE_STAGING avec degraded_reason indiquant NTP | NtpHealthService | Contexte dégradé si > 1s (TC-NOM-03) | Moyen |
5. Mapping tests (TC-*) → mécanismes + observables
Tests nominaux
| Test ID | Référence spec | Mécanisme(s) | Point(s) d'observation | Niveau |
| TC-NOM-01 | FN-60-01, INV-60-02/03/09/10 | TX complète : audit + receipt + deposit | Response 201 + tous champs contractuels | Integration |
| TC-NOM-02 | FN-60-02, CA-60-06 | SELECT existant par (user_id, client_request_id), compare fingerprint → idempotent | Même deposit_id, pas de nouvelle row | Integration |
| TC-NOM-03 | FN-60-03, INV-60-09/11, CA-60-21 | Cas 1 : Mock AuditWriter → exception → staging. Cas 2 : Mock NtpHealthService → drift > 1s → staging (couvre CA-60-21). Dans tous les cas : degraded_reason indique le composant en échec | Response NON_PROBATIVE_STAGING + degraded_reason | Integration |
| TC-NOM-04 | FN-60-04, INV-60-02/10/11 | Staging exists + retry → promotion | deposit créé, staging supprimé | Integration |
| TC-NOM-05 | FN-60-05, INV-60-05 | JWS verify avec clé publique obtenue via GET /documents/.well-known/jwks.json | Signature valide, payload cohérent | E2E |
| TC-NOM-06 | INV-60-04, CA-60-08 | Marqueur dans content → grep response/logs | Marqueur absent partout | Integration |
| TC-NOM-07 | INV-60-07, CA-60-12 | JWT.sub → depositor_identity_ref, JWT.tenant_id → responsibility_scope_ref | Champs cohérents dans response + audit | Integration |
| TC-NOM-08 | INV-60-08, CA-60-11 | SHA-256(doc_modifié) vs fingerprint reçu | Mismatch détecté | Unit |
| TC-NOM-09 | INV-60-10, CA-60-14 | Cas A : mock audit OK + mock receipt OK → ESTABLISHED. Cas B : mock audit OK + mock receipt FAIL → NOT_ESTABLISHED (staging). Cas C : mock audit FAIL + mock receipt OK → NOT_ESTABLISHED (staging). L'audit et le receipt sont tentés indépendamment (receipt n'est PAS conditionné au succès de l'audit). La double condition est vérifiée après les deux opérations. Injection via interfaces AuditWriter et ReceiptSigner (INV-60-16) | legal_existence_status correct par cas | Integration |
| TC-NOM-10 | INV-60-13, CA-60-17 | JWS payload.responsibility_declaration | 3 volets présents : depositor, organization, probatiovault | Unit |
Tests d'erreur
| Test ID | Référence spec | Mécanisme(s) | Point(s) d'observation | Niveau |
| TC-ERR-01 | ERR-60-01, CA-60-01 | Requête sans JWT | HTTP 401 + audit entry DEPOSIT_AUTH_FAILED | Integration |
| TC-ERR-02 | ERR-60-02, CA-60-02 | JWT sans rôle 'user' | HTTP 403 + audit entry DEPOSIT_AUTHZ_FAILED | Integration |
| TC-ERR-03 | ERR-60-03 | DTO incomplet (champ manquant) | HTTP 400 + audit entry DEPOSIT_VALIDATION_FAILED | Integration |
| TC-ERR-04 | ERR-60-04, ERR-60-11, CA-60-19 | Cas 1 : fingerprint mal formaté (non hex, longueur ≠ 64) → HTTP 422. Cas 2 : fingerprint bien formaté mais hash serveur ≠ fingerprint client (contenu altéré) → HTTP 422 FINGERPRINT_MISMATCH | HTTP 422 + audit entry DEPOSIT_FINGERPRINT_MISMATCH | Integration |
| TC-ERR-05 | ERR-60-05, CA-60-07 | SELECT par (user_id, client_request_id) → existe avec fingerprint différent | HTTP 409 + audit entry DEPOSIT_CONFLICT | Integration |
| TC-ERR-06 | ERR-60-06, INV-60-02 | Exception injectée avant TX (mock service technique) | HTTP 500 + pas de deposit ni staging | Integration |
| TC-ERR-07 | ERR-60-07, CA-60-13/16 | probative_notice_ack absent ou false, OU probative_notice_text absent | HTTP 400 | Integration |
| TC-ERR-08 | ERR-60-08 | Rejeu identique (même client_request_id + même fingerprint) après succès | Même response que TC-NOM-01 (idempotent) | Integration |
| TC-ERR-09 | ERR-60-09 | GET /documents/deposits/:staging_id/proof sur un NON_PROBATIVE_STAGING | HTTP 400 PROOF_NOT_AVAILABLE + audit entry DEPOSIT_PROOF_REFUSED_STAGING | Integration |
| TC-ERR-10 | ERR-60-10, CA-60-18 | Altérer audit_trace_ref dans le JWS manuellement → vérifier signature | Signature JWS invalide | Unit |
Tests d'invariants
| Test ID | Référence spec | Mécanisme(s) | Point(s) d'observation | Niveau |
| TC-INV-06 | INV-60-06, CA-60-09 | 3 tentatives contrôlées (succès, refus auth, staging) | 3 entrées audit_log corrélées à client_request_id ; tentative UPDATE/DELETE sur audit_log → rejeté par trigger | Integration |
| TC-INV-12 | INV-60-12, CA-60-16 | Soumission avec probative_notice_text = "Vous réalisez un acte probatoire daté." + probative_notice_ack = true → accepté. Soumission avec texte modifié (casse, espace, ponctuation) → rejeté HTTP 400. Vérification que la valeur exacte est présente dans le reçu JWS et l'entrée d'audit | Acceptation/rejet basé sur correspondance exacte du texte ; présence dans artefacts | Unit + Integration |
| TC-INV-13 | INV-60-13, CA-60-17 | Décodage JWS payload → vérification structure responsibility_declaration | 3 volets depositor, organization, probatiovault présents avec valeurs non vides | Unit |
| TC-INV-14 | INV-60-14 | Décodage JWS payload → énumération des clés | Pas de champs d'obligation au-delà de ceux définis dans la spec | Unit |
Tests de non-régression
| Test ID | Référence spec | Mécanisme(s) | Point(s) d'observation | Niveau |
| TC-NR-01 | FN-60-01 | Re-run TC-NOM-01 | Même ensemble de champs contractuels obligatoires dans la réponse 201 | Integration |
| TC-NR-02 | FN-60-02, CA-60-06 | Re-run TC-NOM-02 | Même deposit_id, aucun doublon en DB (COUNT = 1) | Integration |
| TC-NR-03 | INV-60-04 | Re-run TC-NOM-06 | Marqueur de contenu absent de response + logs + audit | Integration |
| TC-NR-04 | INV-60-10 | Re-run TC-NOM-09 (cas A uniquement) | legal_existence_status=ESTABLISHED si et seulement si audit + receipt OK | Integration |
| TC-NR-05 | INV-60-05/08, CA-60-10/18 | Re-run TC-NOM-05 + TC-NOM-08 | Vérification tiers pass/fail cohérente | E2E + Unit |
| TC-NR-06 | INV-60-11, CA-60-15 | Re-run TC-NOM-03 | NON_PROBATIVE_STAGING sans deposit_id ni proof_receipt | Integration |
Tests adversariaux
| Test ID | Référence spec | Mécanisme(s) | Point(s) d'observation | Niveau |
| TC-NEG-01 | CA-60-06/07 | Deux requêtes concurrentes avec même client_request_id + documents différents. Mécanisme : verrou advisory pg_advisory_xact_lock (Phase 0) sérialise les requêtes inter-table + contrainte UNIQUE par table. La seconde requête attend le verrou, puis constate le deposit existant avec fingerprint différent | Exactement un deposit créé ; la seconde requête reçoit HTTP 409. COUNT deposits + stagings par client_request_id = 1 | Integration |
| TC-NEG-02 | INV-60-05, CA-60-18 | Altération manuelle de audit_trace_ref dans le JWS compact → vérification signature | Signature JWS invalide car payload altéré. Rapport d'incohérence reçu/audit | Unit |
| TC-NEG-03 | INV-60-12, CA-60-13/16 | Soumission avec probative_notice_ack=true mais probative_notice_text différent de la valeur contractuelle (ex: texte en anglais, texte tronqué, texte avec espace supplémentaire). Puis soumission avec probative_notice_ack absent/false | HTTP 400 pour chaque variation. Aucun deposit ni staging créé. Audit entry DEPOSIT_VALIDATION_FAILED | Integration |
| TC-NEG-04 | ERR-60-09, INV-60-11 | Boucle de 10 appels GET /documents/deposits/:staging_id/proof sur un staging | Refus constant HTTP 400 à chaque appel. act_type reste NON_PROBATIVE_STAGING. legal_existence_status reste NOT_ESTABLISHED | Integration |
| TC-NEG-05 | INV-60-04 | Document contenant 5 marqueurs sensibles distincts (UUID, chaîne identifiable, etc.) → dépôt succès puis staging | Grep des 5 marqueurs dans : response body, stdout logs, stderr logs, audit_log.payload → 0 occurrence | Integration |
6. Gestion des erreurs
| Code HTTP | Condition | Corps réponse | Audit |
| 400 | DTO invalide : champ manquant, probative_notice_ack absent/false, probative_notice_text inexact, tenant_id manquant dans JWT | { error: "VALIDATION_ERROR", message: "...", fields: [...] } | Oui — DEPOSIT_VALIDATION_FAILED |
| 400 | GET /documents/deposits/:id/proof sur un staging (ERR-60-09) | { error: "PROOF_NOT_AVAILABLE", message: "staging has no probative status" } | Oui — DEPOSIT_PROOF_REFUSED_STAGING |
| 401 | Pas de JWT ou JWT invalide | { error: "UNAUTHORIZED" } | Oui — DEPOSIT_AUTH_FAILED |
| 403 | JWT valide mais pas de permission de dépôt | { error: "FORBIDDEN" } | Oui — DEPOSIT_AUTHZ_FAILED |
| 404 | GET /documents/deposits/:id/proof : id non trouvé (ni deposit ni staging) | { error: "NOT_FOUND" } | Oui — DEPOSIT_NOT_FOUND |
| 409 | client_request_id déjà utilisé avec document différent (ERR-60-05) | { error: "CONFLICT", message: "client_request_id already used with different document" } | Oui — DEPOSIT_CONFLICT |
| 409 | Rejeu staging avec document_fingerprint différent (ERR-60-12) | { error: "CONFLICT", message: "staging exists with different document fingerprint" } | Oui — DEPOSIT_STAGING_CONFLICT |
| 422 | Hash serveur ≠ fingerprint client (INV-60-15, ERR-60-11) | { error: "FINGERPRINT_MISMATCH" } | Oui — DEPOSIT_FINGERPRINT_MISMATCH |
| 500 | Erreur technique (DB, S3, HSM) avant confirmation | { error: "INTERNAL_ERROR" } | Oui — DEPOSIT_TECHNICAL_ERROR |
| 503 | Contexte dégradé (NTP drift > 1s, audit indisponible) → staging | { staging_id, act_type: "NON_PROBATIVE_STAGING", ... } | Oui — DEPOSIT_DEGRADED_STAGING |
7. Impacts sécurité
| Risque | Mitigation | Composant |
| Fuite contenu intelligible dans logs | Aucun content ou Buffer dans les arguments de logger. Intercepteur de log sanitize | DepositService, LoggerModule |
| Falsification d'empreinte | Vérification serveur SHA-256 (INV-60-15) | DepositService |
| Rejeu cross-tenant | RLS PostgreSQL isole par user_id. Verrou advisory + client_request_id UNIQUE par (user_id, client_request_id) par table, avec sérialisation inter-table | RlsMiddleware, DB constraints, pg_advisory_xact_lock |
| Compromission clé de signature JWS | Clé HSM non-extractable (FIPS 140-2 L3 — PD-7). Rotation via KeyRotationModule | CryptoModule / HSM |
| Timing attack sur idempotence | Réponse en temps constant (même durée pour hit vs miss) | DepositService |
| Injection dans audit | Paramètres liés (prepared statements), pas de concaténation | AuditModule / TypeORM |
| RGPD — données personnelles | depositor_identity_ref = UUID opaque (pas d'email, pas de nom). Contenu chiffré côté client | Architecture zero-knowledge |
8. Hypothèses techniques
| ID | Hypothèse | Impact si faux |
| HT-01 | Le HashService existant peut être étendu pour SHA-256 (actuellement SHA3-256) | Si non → ajouter méthode hashSha256() dans HashService. Impact faible |
| HT-02 | La bibliothèque jose (npm) est disponible ou ajoutée pour la génération JWS | Si non → utiliser jsonwebtoken ou implémentation manuelle. Impact faible |
| HT-03 | La clé HSM PD-7 peut signer des JWS avec un algorithme contractuellement autorisé : ES256 (ECDSA P-256) ou RS256 (RSA ≥ 2048 bits), conformément à la spec §3 (« algorithme RS256 ou ES256 »). Note : la clé HSM ECDSA P-384 existante (PD-7) est utilisée pour la signature d'audit (INV-60-06) mais pas pour le reçu JWS. Le reçu JWS nécessite une clé P-256 (ES256) ou RSA (RS256) dédiée sur le HSM | Si la clé HSM ne supporte pas P-256 ou RSA → générer une paire de clés dédiée pour le reçu probatoire (ES256 recommandé). Impact moyen |
| HT-04 | Le module ntp-client ou équivalent existe en npm pour vérifier la dérive NTP | Si non → appel ntpdate -q via child_process ou vérification via API externe. Impact faible |
| HT-05 | Le champ tenant_id est présent dans le JWT pour responsibility_scope_ref | Si non → la requête est rejetée HTTP 400 (pas de fallback). Le JWT DOIT contenir tenant_id. Impact moyen — nécessite coordination avec l'AuthModule. |
| HT-06 | L'upload multipart existant (UploadModule) peut être réutilisé pour recevoir le contenu opaque + JSON metadata dans la même requête | Si non → adapter l'UploadModule pour supporter ce format. Aucun comportement alternatif « deux appels séparés » n'est prévu (la spec impose un flux unique POST /documents/upload). Impact moyen |
| HT-07 | Le trigger PostgreSQL BEFORE UPDATE OR DELETE sur audit_log existe déjà (PD-37) | Si non → ajouter migration pour le créer. Impact faible (confirmé existant) |
| HT-08 | L'endpoint JWKS (GET /documents/.well-known/jwks.json) est accessible publiquement (sans JWT) et est le mécanisme de distribution de la clé publique pour la vérification tiers | Si non → nécessite un canal alternatif de distribution. Impact moyen — affecte INV-60-05, CA-60-10 |
9. Points de vigilance (risques, dette, pièges)
-
Latence HSM en Phase A (TX ouverte) : Les signatures HSM (audit + receipt) sont exécutées en Phase A, à l'intérieur de la transaction englobante (voir INV-60-02). La TX reste ouverte pendant les appels HSM, ce qui maintient le verrou advisory mais allonge la durée de la TX. Si la latence HSM > 500ms, la TX est plus longue mais l'atomicité et la sérialisation restent garanties. Mitigation : timeout configurable par opération HSM (avec ROLLBACK en cas de timeout), monitoring latence HSM, alerte si durée TX > seuil.
-
Taille du contenu opaque : La spec ne définit pas de limite de taille. L'UploadModule existant gère le multipart streaming mais la vérification SHA-256 serveur nécessite de hasher tout le contenu. Pour des fichiers > 1 Go, utiliser un hash streaming (déjà disponible via HashStreamService).
-
Migration des documents existants : Les DocumentSecure existants n'ont pas de deposit_id ni de proof_receipt. PD-60 crée de nouvelles tables (deposits, deposit_stagings), pas de migration des données existantes.
-
Clé publique JWKS : L'endpoint /documents/.well-known/jwks.json expose la clé publique. Il doit être public (pas de JWT requis) mais protégé contre les abus (rate-limiting infra, cache CDN). C'est le mécanisme unique de distribution de la clé publique pour la vérification tiers (HT-08).
-
Concurrence sur client_request_id (sérialisation inter-table) : Deux requêtes simultanées avec le même (user_id, client_request_id) sont sérialisées par pg_advisory_xact_lock(hashtext(user_id::text || '/' || client_request_id)) (Phase 0). Ce verrou couvre les deux tables (deposits et deposit_stagings), empêchant qu'un même (user_id, client_request_id) produise un deposit dans une table et un staging dans l'autre par interleaving concurrent. La deuxième requête attend la libération du verrou puis applique la logique idempotence/conflit sur l'état résultant.
-
NTP monitoring : Le NtpHealthService doit être un health check périodique (cron toutes les 30s), pas un appel NTP par requête. Le statut est mis en cache.
10. Hors périmètre
- Consultation, modification, partage, suppression, recherche, versioning (PD-61 à PD-69)
- Rate-limiting sur l'endpoint upload (relève de l'infra — PC-60-17)
- Résistance admin-level de l'append-only audit — explicitement hors périmètre de cette US par décision contractuelle PC-60-13 (spec §4 INV-60-06 : « La non-altérabilité absolue (résistance à un acteur disposant d'un accès administrateur au stockage) est hors périmètre de cette US et relève de l'infrastructure »). Cette exclusion est actée dans la spec canonique, pas une hypothèse du plan.
- Qualification juridictionnelle de la valeur probatoire
- Définition des performances (latence, débit, volumétrie)
- Gestion des clés de signature (rotation, publication) — relève du module crypto existant
Schéma base de données
Table vault_secure.deposits
CREATE TABLE vault_secure.deposits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- deposit_id
user_id UUID NOT NULL REFERENCES vault_secure.users(id),
client_request_id VARCHAR(255) NOT NULL,
document_fingerprint VARCHAR(64) NOT NULL, -- SHA-256 hex lowercase
existence_timestamp_utc TIMESTAMPTZ NOT NULL,
depositor_identity_ref UUID NOT NULL, -- == user_id (JWT.sub)
responsibility_scope_ref VARCHAR(255) NOT NULL, -- JWT.tenant_id (obligatoire)
proof_receipt TEXT NOT NULL, -- JWS compact
audit_trace_ref UUID NOT NULL, -- FK audit_log.id
act_type VARCHAR(30) NOT NULL DEFAULT 'PROBATIVE_DEPOSIT',
legal_existence_status VARCHAR(30) NOT NULL DEFAULT 'ESTABLISHED',
storage_path VARCHAR(512) NOT NULL, -- S3 path
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_deposits_client_request UNIQUE (user_id, client_request_id)
);
-- RLS
ALTER TABLE vault_secure.deposits ENABLE ROW LEVEL SECURITY;
CREATE POLICY deposits_user_isolation ON vault_secure.deposits
USING (user_id = current_setting('app.current_user_id')::uuid);
Sérialisation inter-table (applicative, Phase 0 — dans la TX englobante) :
-- Exécuté en Phase 0, DANS la transaction explicite (BEGIN déjà émis)
-- Le verrou est maintenu jusqu'au COMMIT ou ROLLBACK de cette TX
SELECT pg_advisory_xact_lock(hashtext(user_id::text || '/' || client_request_id));
Ce verrou advisory lié à la transaction (pg_advisory_xact_lock, pas pg_advisory_lock) garantit qu'un seul processus à la fois traite un (user_id, client_request_id) donné tant que la TX est ouverte. Il est libéré automatiquement au COMMIT/ROLLBACK, empêchant toute race condition entre les tables deposits et deposit_stagings. Logique d'idempotence/conflit (applicative, sous verrou advisory) : - SELECT par (user_id, client_request_id) dans deposits → si existe : - document_fingerprint identique → retourner le deposit existant (CA-60-06, ERR-60-08) - document_fingerprint différent → HTTP 409 (CA-60-07, ERR-60-05) - Si n'existe pas → SELECT dans deposit_stagings : - Staging trouvé avec même fingerprint + conditions OK → promouvoir (FN-60-04) - Staging trouvé avec fingerprint différent → HTTP 409 (ERR-60-12) - Rien trouvé → nouveau dépôt (FN-60-01)
Table vault_secure.deposit_stagings
CREATE TABLE vault_secure.deposit_stagings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- staging_id
user_id UUID NOT NULL REFERENCES vault_secure.users(id),
client_request_id VARCHAR(255) NOT NULL,
document_fingerprint VARCHAR(64) NOT NULL,
storage_path VARCHAR(512) NOT NULL,
act_type VARCHAR(30) NOT NULL DEFAULT 'NON_PROBATIVE_STAGING',
legal_existence_status VARCHAR(30) NOT NULL DEFAULT 'NOT_ESTABLISHED',
degraded_reason VARCHAR(255), -- Reason for degraded context
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_stagings_client_request UNIQUE (user_id, client_request_id)
);
-- RLS
ALTER TABLE vault_secure.deposit_stagings ENABLE ROW LEVEL SECURITY;
CREATE POLICY stagings_user_isolation ON vault_secure.deposit_stagings
USING (user_id = current_setting('app.current_user_id')::uuid);
Ordre d'implémentation
| Phase | Tâche | Dépendances | Estimation relative |
| 1 | Migration DB : tables deposits + deposit_stagings + RLS | — | S |
| 2 | Entities TypeORM : Deposit, DepositStaging | Phase 1 | S |
| 3 | DTO : CreateDepositDto (avec probative_notice_ack + probative_notice_text), DepositResponseDto | — | S |
| 4 | NtpHealthService : vérification dérive NTP | — | S |
| 5 | ProofReceiptService : génération JWS + endpoint JWKS public | CryptoModule/HSM | M |
| 6 | DepositAuditService : wrapper audit spécialisé dépôt | AuditModule | S |
| 7 | DepositService : logique métier complète (dépôt, idempotence, staging, promotion, preuve) | Phases 2-6 | L |
| 8 | DepositController : endpoints POST /documents/upload + GET /documents/deposits/:id/proof + guards + DTO binding | Phase 7 | M |
| 9 | Tests unitaires : ProofReceiptService, NtpHealthService, DTO (notice ack + text validation), TC-NOM-08/10, TC-ERR-10, TC-INV-12/13/14 | Phases 3-5 | M |
| 10 | Tests d'intégration : TC-NOM-01..09, TC-ERR-01..09, TC-INV-06 | Phase 8 | L |
| 11 | Tests non-régression : TC-NR-01..06 | Phase 10 | M |
| 12 | Tests adversariaux : TC-NEG-01..05 | Phase 10 | M |
Tailles : S = petite, M = moyenne, L = grande
Références
- Spécification : PD-60-specification.md (v2 — amendée 3 février 2026, incluant INV-60-15..17 et CA-60-19..21)
- Tests contractuels : PD-60-tests.md
- Review spécification : PD-60-specification-review.md
- Epic : PD-192 — API Documents & preuves probatoires
- Architecture existante : NestJS 10.3 + TypeORM 0.3 + PostgreSQL + CloudHSM (PD-7)