PD-80 — Plan d'implémentation¶
Metadata¶
- Story : PD-80 — Implémenter scellement instantané < 1h
- Epic : PD-185 (B2C-MINEURS)
- Projet : ProbatioVault-backend
- Stack : NestJS + TypeORM + PostgreSQL + BullMQ + Redis
- Date : 2026-03-12
- Spec version : v3 (Gate 3 RESERVE 8.875/10)
Résolution des réserves Gate 3¶
| Réserve Gate 3 | Résolution dans ce plan |
|---|---|
| ECT-v3-01 (MAJ) Priorité rate-limit si mineur ET enterprise | DA-01 : account_type=minor prime sur tout autre rate-limit. Le rate-limit appliqué est rate_limit_minor_hour=2 pour les mineurs, quelle que soit l'offre. Voir §1 composant C2. |
| ECT-v3-02 (MAJ) Définition "job BullMQ actif" pour orphelin | DA-02 : Un job BullMQ est considéré "actif" s'il est dans l'un des états active, waiting, delayed, prioritized de la queue correspondante. Un job completed, failed ou absent n'est pas actif. Voir §1 composant C7. |
| ECT-v3-03 (MAJ) Conditions transitions retour exhaustives | DA-03 : Liste fermée — (1) TSA_PENDING → QUEUED_PRIORITY : token TSA reçu mais DER invalide (parse RFC3161 échoue) après 4 retries épuisés sur le même serveur TSA ; (2) TSA_SEALED → TSA_PENDING : invalidation du token TSA détectée avant soumission Merkle (ex : vérification nonce échoue, token expiré). Aucune autre transition retour n'est autorisée. |
| ECT-v3-04 (MIN) Cycle oscillant non borné | DA-04 : Maximum 2 transitions retour par scellement. Au 3e retour, le job reste dans son état et attend final_timeout. Compteur return_transitions persisté sur le job. |
| ECT-v3-05 (MIN) DEK destruction non testée directement | Couvert par TC-NOM-09 (absence clair) + ajout assertion en intégration : vérifier que la colonne wrapped_dek est NULL après transition terminale. |
| ECT-v3-06 (MIN) NTP non testé au démarrage | Couvert par TC-NOM-15 (clamps logging) — le check NTP au bootstrap émet un log CRITICAL, observable dans les mêmes tests d'observabilité. |
| ECT-v3-07 (MIN) TC-ERR-04 "rejeté" vs retry interne | Clarifié dans mapping §5 : l'artefact invalide est rejeté (non persisté) et un retry est déclenché. Pas de rejet API vers l'utilisateur. |
| ECT-v3-08 (MIN) retries_exhausted contenu | Le TC vérifie seal_id, current_state, attempt_count dans l'événement. Voir mapping TC-ERR-11 §5. |
Décisions architecturales¶
| ID | Décision | Justification |
|---|---|---|
| DA-01 | Rate-limit mineur prime sur tout autre profil | Un compte mineur est protégé par design (INV-80-02). Le rate-limit le plus restrictif s'applique (rate_limit_minor_hour=2). |
| DA-02 | Job BullMQ "actif" = états active\|waiting\|delayed\|prioritized | Exhaustivité nécessaire pour éviter faux positifs de réconciliation. |
| DA-03 | Transitions retour : liste fermée (2 cas) | INV-80-01 exige aucune transition implicite. |
| DA-04 | Max 2 transitions retour par scellement | Borne le cycle oscillant sans compromettre la résilience. |
| DA-05 | Deux queues BullMQ séparées (priority-tsa, priority-anchor) | Isolation des étapes TSA et ancrage. Les queues standard existantes restent inchangées. Le ratio 5:1 est appliqué via limiter BullMQ sur les workers partagés. |
| DA-06 | Machine d'états implémentée via enum TypeORM + guard méthode canTransition() | Pattern déjà utilisé dans PD-274 (AnchorStatusEnum). Close-world : toute transition non listée lève IllegalStateTransitionError. |
| DA-07 | DEK wrappée stockée en colonne wrapped_dek BYTEA sur l'entité UrgentSeal | Supprimée (SET NULL) dans la même transaction que la transition terminale. |
| DA-08 | Worker de réconciliation comme @Cron NestJS Schedule + lock Redis SET NX EX | Pattern déjà utilisé dans PD-265 (monitoring TSA lifecycle). |
| DA-09 | Métriques exposées via Prometheus client (prom-client) | Cohérent avec l'existant PD-55 (anchor_latency_seconds). |
1. Découpage en composants¶
C1 — Module urgent-seal (entité + machine d'états)¶
Responsabilité : Entité UrgentSeal (TypeORM), machine d'états close-world, transitions autorisées, gardes de transition.
Fichiers : src/modules/urgent-seal/entities/, src/modules/urgent-seal/enums/, src/modules/urgent-seal/guards/
Détails : - Entité UrgentSeal : id (UUID v4 PK), document_id (FK), user_id (FK), account_type (enum), sealed_status (enum 7 valeurs), trigger_mode (enum AUTO|MANUAL), retry_count (int, défaut 0), return_transition_count (int, défaut 0), last_activity_at (timestamptz), wrapped_dek (bytea nullable), reconciled (boolean, défaut false), final_timeout_at (timestamptz, calculé à l'admission), created_at, updated_at. - Enum SealStatus : RECEIVED, QUEUED_PRIORITY, TSA_PENDING, TSA_SEALED, ANCHOR_PENDING, SEALED, FAILED_TIMEOUT. - Méthode canTransition(from, to): boolean — table close-world. - Méthode applyTransition(from, to): void — lève IllegalStateTransitionError si interdit, met à jour last_activity_at. - Transitions retour : limitées à 2 (DA-04), vérification via return_transition_count.
C2 — Service d'admission (UrgentSealAdmissionService)¶
Responsabilité : Validation d'entrée (§5.1), vérification quota + rate-limit, détection account_type=minor, création UrgentSeal, publication en queue prioritaire.
Fichiers : src/modules/urgent-seal/services/admission.service.ts, src/modules/urgent-seal/dto/
Détails : - DTOs : CreateUrgentSealDto (validation class-validator, formats §5.1). - Flux : validate → check quota (via FreemiumService existant) → check rate-limit (Redis INCR + TTL 1h) → create entity RECEIVED → transaction commit → publish job priority-tsa. - Rate-limit : rate_limit_minor_hour=2 si account_type=minor (DA-01), rate_limit_enterprise_hour=10 si enterprise, rate_limit_urgent=1 sinon. - Quota : délégué au module freemium existant (FreemiumGuard / FreemiumService). Les seuils quota_freemium_month=3, quota_premium_month=10, quota_enterprise_month=1000 sont injectés via config. - Atomicité §5.7 : transaction DB synchrone (ACID) pour état + audit, publication queue async post-commit.
C3 — Processor TSA prioritaire (PriorityTsaProcessor)¶
Responsabilité : Consomme la queue priority-tsa, appelle le TsaService existant (PD-39), gère les retries contractuels, met à jour l'état.
Fichiers : src/modules/urgent-seal/processors/priority-tsa.processor.ts
Détails : - Hérite du pattern BaseProcessor existant (module jobs). - Retry : BullMQ backoff custom [1, 5, 15, 30] min, attempts: 5 (4 retries + 1 tentative initiale). - Sur succès TSA : transition TSA_PENDING → TSA_SEALED, stocker token TSA (DER base64). - Sur échec retry : état reste TSA_PENDING, compteur BullMQ incrémenté. - Sur 5e échec : événement retries_exhausted en audit (seal_id, current_state=TSA_PENDING, attempt_count=5), aucun nouveau retry. - Validation réponse TSA : parse DER RFC3161. Si invalide après 4 retries épuisés → transition retour TSA_PENDING → QUEUED_PRIORITY (si return_transition_count < 2).
C4 — Processor ancrage prioritaire (PriorityAnchorProcessor)¶
Responsabilité : Consomme la queue priority-anchor, orchestre le mini-batch Merkle, appelle le AnchorService existant (PD-55), gère les retries.
Fichiers : src/modules/urgent-seal/processors/priority-anchor.processor.ts
Détails : - Mini-batch : agrège les jobs prêts (état TSA_SEALED) en lot de 1..20 (batch_size_min..batch_size_max), flush après batch_time_max (5 min) ou batch_size_max atteint. - Appel MerkleService existant (PD-54) pour construire l'arbre, puis AnchorService (PD-55) pour ancrage L2. - Sur succès : transition ANCHOR_PENDING → SEALED pour chaque job du batch, suppression wrapped_dek (DA-07), publication événement notification. - Retry : même politique [1, 5, 15, 30] min, état reste ANCHOR_PENDING. - INV-80-05 : aucune preuve urgente ancrée individuellement hors mini-batch. Le processor refuse de traiter un job seul sauf si batch_time_max est expiré.
C5 — Service de chiffrement DEK (SealCryptoService)¶
Responsabilité : Génération, wrapping, usage et destruction des DEK (§5.14, INV-80-09).
Fichiers : src/modules/urgent-seal/services/seal-crypto.service.ts
Détails : - generateDek() : crypto.randomBytes(32), unicité par scellement. - wrapDek(dek, kekId) : envelope encryption via HSM (module crypto existant, PD-35). - unwrapDek(wrappedDek, kekId) : pour déchiffrement temporaire en mémoire uniquement. - destroyDek(sealId) : SET wrapped_dek = NULL dans la même transaction que la transition terminale. - Invariant : DEK en clair jamais persistée en base/log/disque. Jamais loggée même en DEBUG.
C6 — Service de notification multi-canal (SealNotificationService)¶
Responsabilité : Notifications push, email, webhook sur transitions terminales (SEALED, FAILED_TIMEOUT).
Fichiers : src/modules/urgent-seal/services/seal-notification.service.ts
Détails : - Sur SEALED : push (via module notifications/dispatch existant, PD-105) + email + webhook proof_sealed. - Sur FAILED_TIMEOUT : email + webhook proof_failed + événement escalade. - Fallback push (§5.12) : si pas de device enregistré, omission push (log INFO), email comme fallback primaire. - INV-80-08 satisfait si ≥ 1 canal réussit. - Email : contient hash_document, blockchain_tx, lien preuve, horodatage TSA (CA-10). - Webhook payload contractuel (§5.11) : document_id, status, sealed_at/failed_at, blockchain_tx, merkle_root / reason, last_state. - Webhook retry : politique indépendante [1, 5, 15] min, max 3 retries. Après épuisement : événement webhook_delivery_failed en audit.
C7 — Worker de réconciliation (SealReconciliationWorker)¶
Responsabilité : Détection et rattrapage des jobs orphelins post-crash (§5.10).
Fichiers : src/modules/urgent-seal/services/seal-reconciliation.service.ts
Détails : - @Cron toutes les reconciliation_scan_interval (5 min). - Critère orphelin : état non terminal + last_activity_at < now() - reconciliation_orphan_threshold (10 min) + aucun job BullMQ actif (DA-02 : active|waiting|delayed|prioritized). - Lock distribué Redis : SET reconciliation:lock:{seal_id} NX EX {scan_interval * 2} (DA-08). - Si lock acquis : recréer job BullMQ dans la queue appropriée au même état, flag reconciled=true, retry_count préservé. - Si lock déjà détenu : skip ce cycle (log INFO). - Alerte CRITICAL si reconciliation_max_catchup_delay (30 min) dépassé.
C8 — Service de métriques (SealMetricsService)¶
Responsabilité : Exposition des métriques contractuelles (CA-12) et calcul P95 (INV-80-04).
Fichiers : src/modules/urgent-seal/services/seal-metrics.service.ts
Détails : - Métriques Prometheus (prom-client, DA-09) : - seal_latency_seconds (Histogram, buckets adaptés 0-7200s) — P95 calculé sur fenêtre 24h glissante. - priority_queue_depth (Gauge) — profondeur file TSA + ancrage. - tsa_latency_seconds (Histogram). - anchor_latency_seconds (Histogram). - P95 : si N < p95_min_samples (100), exposer label status=INSUFFICIENT_DATA, aucune violation SLA émise. - Compteur N exposé pour vérification.
C9 — Timer final_timeout (SealTimeoutScheduler)¶
Responsabilité : Surveillance du timeout global et transition forcée FAILED_TIMEOUT.
Fichiers : src/modules/urgent-seal/schedulers/seal-timeout.scheduler.ts
Détails : - @Cron toutes les 1 min : scan des UrgentSeal non terminaux dont final_timeout_at <= now(). - Transition forcée vers FAILED_TIMEOUT + notification échec (via C6) + escalade opérationnelle. - final_timeout_at calculé à l'admission : created_at + final_timeout (120 min configurable).
C10 — Configuration et validation (SealConfigService)¶
Responsabilité : Chargement, clamp et validation des paramètres §5.2 au démarrage.
Fichiers : src/modules/urgent-seal/services/seal-config.service.ts
Détails : - Implémente OnModuleInit pour validation au bootstrap. - Clamp : chaque valeur hors bornes [Min, Max] ramenée à la borne. Log WARN pour chaque clamp (CA-18). - Validation retry_delays et webhook_retry_delays : config invalide rejetée (pas de clamp). - Check NTP : alerte CRITICAL si dérive > 1s (§5.13).
C11 — Module NestJS (UrgentSealModule)¶
Responsabilité : Enregistrement de tous les composants, injection des dépendances, déclaration des queues BullMQ.
Fichiers : src/modules/urgent-seal/urgent-seal.module.ts
Détails : - Importe : JobsModule, TsaModule, AnchorModule, MerkleModule, NotificationsModule, CryptoModule, AuditModule, FreemiumModule, EmailModule. - Enregistre les queues : BullModule.registerQueue({ name: 'priority-tsa' }), BullModule.registerQueue({ name: 'priority-anchor' }). - Exporte : UrgentSealAdmissionService, SealMetricsService.
C12 — Migration DDL¶
Responsabilité : Création de la table urgent_seals et des index.
Fichiers : src/database/migrations/XXXXXXXXX-CreateUrgentSeals.ts
Détails : - Table vault_secure.urgent_seals avec colonnes de C1. - Index : idx_urgent_seals_status (sealed_status), idx_urgent_seals_timeout (final_timeout_at WHERE sealed_status NOT IN ('SEALED', 'FAILED_TIMEOUT')). - Enum PostgreSQL seal_status_enum. - RLS activé. - down() vide (sécurité production, convention PD-79).
C13 — Controller API (UrgentSealController)¶
Responsabilité : Endpoints REST pour déclenchement manuel et consultation.
Fichiers : src/modules/urgent-seal/controllers/urgent-seal.controller.ts
Détails : - POST /seals/urgent : déclenchement manuel (flux B, §5.6). Auth JWT + guard éligibilité. - GET /seals/urgent/:id : consultation état d'un scellement urgent. - Le flux automatique mineur (flux A, §5.5) est déclenché par hook dans le pipeline d'upload existant (module upload), pas par endpoint dédié.
2. Flux techniques¶
2.1 Flux A — Fast-track automatique mineur¶
Upload (module upload) → detect account_type=minor
→ UrgentSealAdmissionService.admitAutomatic(document, user)
→ validate formats §5.1
→ check rate_limit_minor_hour (Redis INCR)
→ check quota freemium (FreemiumService)
→ BEGIN TRANSACTION
→ INSERT UrgentSeal (RECEIVED)
→ UPDATE sealed_status = QUEUED_PRIORITY
→ INSERT audit_log (admission, trigger=AUTO)
→ COMMIT
→ SealCryptoService.generateAndWrapDek(sealId)
→ publish job priority-tsa (async post-commit)
PriorityTsaProcessor.process(job)
→ TsaService.requestTimestamp(hash_document)
→ validate DER RFC3161
→ BEGIN TRANSACTION
→ UPDATE sealed_status = TSA_SEALED
→ STORE tsa_token (base64)
→ INSERT audit_log (tsa_complete)
→ COMMIT
→ publish job priority-anchor (async post-commit)
PriorityAnchorProcessor.process(batch)
→ collect TSA_SEALED jobs (1..20, flush ≤ 5 min)
→ MerkleService.buildTree(hashes)
→ AnchorService.anchorBatch(merkle_root)
→ FOR EACH job IN batch:
→ BEGIN TRANSACTION
→ UPDATE sealed_status = SEALED
→ SET wrapped_dek = NULL (DEK destruction)
→ INSERT audit_log (sealed)
→ COMMIT
→ SealNotificationService.notifySealed(job)
2.2 Flux B — Fast-track manuel¶
POST /seals/urgent (UrgentSealController)
→ UrgentSealAdmissionService.admitManual(dto, user)
→ validate formats §5.1
→ check eligibility (plan != freemium OR quota > 0)
→ check rate_limit (1/h standard, 10/h enterprise)
→ check quota monthly
→ [même pipeline que flux A à partir de la transaction]
2.3 Flux timeout¶
SealTimeoutScheduler.checkTimeouts() [cron 1 min]
→ SELECT * FROM urgent_seals WHERE final_timeout_at <= now() AND sealed_status NOT IN ('SEALED', 'FAILED_TIMEOUT')
→ FOR EACH expired:
→ BEGIN TRANSACTION
→ UPDATE sealed_status = FAILED_TIMEOUT
→ SET wrapped_dek = NULL
→ INSERT audit_log (timeout, escalation)
→ COMMIT
→ SealNotificationService.notifyFailed(seal)
2.4 Flux réconciliation¶
SealReconciliationWorker.scan() [cron 5 min]
→ SELECT * FROM urgent_seals WHERE sealed_status NOT IN ('SEALED', 'FAILED_TIMEOUT') AND last_activity_at < now() - interval '10 min'
→ FOR EACH orphan:
→ check BullMQ states (active|waiting|delayed|prioritized) for seal_id
→ IF no active job:
→ SET NX reconciliation:lock:{seal_id} EX 600
→ IF lock acquired:
→ recreate BullMQ job (same queue, same state, reconciled=true)
→ INSERT audit_log (reconciliation)
→ ELSE: log INFO "lock held, skipping"
2.5 Diagrammes Mermaid¶
Graphe de dépendances inter-composants¶
graph TD
C13[C13 — UrgentSealController] --> C2[C2 — UrgentSealAdmissionService]
UPLOAD[Module upload hook] --> C2
C2 --> C5[C5 — SealCryptoService]
C2 --> C10[C10 — SealConfigService]
C2 --> FREEMIUM[FreemiumService existant]
C2 --> REDIS[(Redis rate-limit)]
C2 -->|publish job| Q_TSA{{Queue priority-tsa}}
Q_TSA --> C3[C3 — PriorityTsaProcessor]
C3 --> TSA[TsaService existant PD-39]
C3 --> C1[C1 — UrgentSeal entité + machine états]
C3 -->|publish job| Q_ANCHOR{{Queue priority-anchor}}
Q_ANCHOR --> C4[C4 — PriorityAnchorProcessor]
C4 --> MERKLE[MerkleService existant PD-54]
C4 --> ANCHOR[AnchorService existant PD-55]
C4 --> C5
C4 --> C1
C4 --> C6[C6 — SealNotificationService]
C6 --> NOTIF[Module notifications PD-105]
C6 --> EMAIL[Module email]
C6 --> WEBHOOK[Webhook externe]
C7[C7 — SealReconciliationWorker] --> C1
C7 --> Q_TSA
C7 --> Q_ANCHOR
C7 --> REDIS
C8[C8 — SealMetricsService] --> PROM[prom-client Prometheus]
C9[C9 — SealTimeoutScheduler] --> C1
C9 --> C6
C11[C11 — UrgentSealModule] -.->|registre| C1 & C2 & C3 & C4 & C5 & C6 & C7 & C8 & C9 & C10 & C13
C12[C12 — Migration DDL] -.->|crée table| C1 Séquence — Flux A (fast-track automatique mineur)¶
sequenceDiagram
participant U as Module Upload
participant A as C2 — AdmissionService
participant R as Redis
participant DB as PostgreSQL
participant CR as C5 — SealCryptoService
participant QT as Queue priority-tsa
participant T as C3 — TsaProcessor
participant TSA as TsaService (PD-39)
participant QA as Queue priority-anchor
participant AN as C4 — AnchorProcessor
participant MK as MerkleService (PD-54)
participant BC as AnchorService (PD-55)
participant N as C6 — NotificationService
U->>A: admitAutomatic(document, user)
A->>R: INCR rate_limit_minor (TTL 1h)
R-->>A: count <= 2
A->>DB: BEGIN TX
A->>DB: INSERT UrgentSeal (RECEIVED → QUEUED_PRIORITY)
A->>DB: INSERT audit_log (admission, trigger=AUTO)
A->>DB: COMMIT
A->>CR: generateAndWrapDek(sealId)
CR->>DB: STORE wrapped_dek
A->>QT: publish job priority-tsa
QT->>T: process(job)
T->>TSA: requestTimestamp(hash_document)
TSA-->>T: token TSA (DER)
T->>DB: BEGIN TX
T->>DB: UPDATE sealed_status = TSA_SEALED
T->>DB: INSERT audit_log (tsa_complete)
T->>DB: COMMIT
T->>QA: publish job priority-anchor
QA->>AN: process(batch 1..20)
AN->>MK: buildTree(hashes)
MK-->>AN: merkle_root
AN->>BC: anchorBatch(merkle_root)
BC-->>AN: blockchain_tx
loop Pour chaque job du batch
AN->>DB: BEGIN TX
AN->>DB: UPDATE sealed_status = SEALED
AN->>DB: SET wrapped_dek = NULL
AN->>DB: INSERT audit_log (sealed)
AN->>DB: COMMIT
AN->>N: notifySealed(job)
end
N->>N: push + email + webhook proof_sealed Séquence — Flux timeout et réconciliation¶
sequenceDiagram
participant TO as C9 — TimeoutScheduler
participant RW as C7 — ReconciliationWorker
participant DB as PostgreSQL
participant R as Redis
participant Q as Queues BullMQ
participant N as C6 — NotificationService
Note over TO: Cron toutes les 1 min
TO->>DB: SELECT non-terminaux WHERE final_timeout_at <= now()
DB-->>TO: [expired seals]
loop Pour chaque expired
TO->>DB: BEGIN TX
TO->>DB: UPDATE sealed_status = FAILED_TIMEOUT
TO->>DB: SET wrapped_dek = NULL
TO->>DB: INSERT audit_log (timeout, escalation)
TO->>DB: COMMIT
TO->>N: notifyFailed(seal)
end
Note over RW: Cron toutes les 5 min
RW->>DB: SELECT non-terminaux WHERE last_activity_at < now() - 10min
DB-->>RW: [orphan candidates]
loop Pour chaque orphelin
RW->>Q: check BullMQ states (active|waiting|delayed|prioritized)
Q-->>RW: no active job
RW->>R: SET NX reconciliation:lock:{seal_id} EX 600
R-->>RW: OK (lock acquired)
RW->>Q: recreate job (same queue, reconciled=true)
RW->>DB: INSERT audit_log (reconciliation)
end 3. Mapping invariants → mécanismes¶
| Invariant ID | Exigence | Mécanisme | Composant | Observable | Risque |
|---|---|---|---|---|---|
| INV-80-01 | Machine d'états explicite, aucune transition implicite | Enum SealStatus + canTransition() close-world + IllegalStateTransitionError | C1 | Rejet toute transition non listée, audit refus | Faible — pattern éprouvé PD-274 |
| INV-80-02 | Minor → fast-track auto | Hook dans pipeline upload, détection account_type=minor, appel admitAutomatic() | C2 | État QUEUED_PRIORITY sans action manuelle, audit trigger=AUTO | Faible |
| INV-80-03 | Manuel urgent = éligible + quota + rate-limit | Guards admission : quota (FreemiumService) + rate-limit (Redis INCR TTL 1h) | C2, C13 | Rejets 403/429 avant enqueuing | Moyen — interaction rate-limits (mitigé par DA-01) |
| INV-80-04 | SLA P95 < 60 min (24h, N≥100), timeout >=120 → FAILED_TIMEOUT | Métriques Histogram P95 + scheduler timeout 1 min | C8, C9 | seal_latency_seconds P95, transition FAILED_TIMEOUT | Moyen — dépend disponibilité TSA/blockchain |
| INV-80-05 | Mini-batch, pas d'ancrage individuel urgent | PriorityAnchorProcessor collecte 1..20 jobs, refuse traitement individuel sauf flush timeout | C4 | Lot toujours >= 1, flush <= 5 min | Faible |
| INV-80-06 | Fail-soft retries [1,5,15,30], même état, post-épuisement passif | BullMQ backoff custom, retry_count sur job, événement retries_exhausted après 5e échec | C3, C4 | Traces retries avec délais, état inchangé, audit retries_exhausted | Faible — BullMQ supporte nativement |
| INV-80-07 | Quotas + rate-limit avant admission queue | Vérification synchrone dans admitAutomatic/admitManual avant transaction | C2 | Rejets 403/429, compteurs Redis | Faible |
| INV-80-08 | Notification obligatoire (≥ 1 canal) | SealNotificationService multi-canal, fallback email si pas de device push | C6 | Événements notification horodatés, webhook payloads | Moyen — dépend disponibilité APNS/SMTP |
| INV-80-09 | Secrets crypto chiffrés au repos (AES-256-GCM ou HSM envelope) | SealCryptoService : DEK wrappée en DB, destruction à transition terminale | C5 | Absence secret clair en base (audit stockage) | Faible — pattern envelope encryption existant PD-35 |
| INV-80-10 | Atomicité DB + async, rattrapage post-crash | Transaction ACID synchrone + async idempotent + réconciliation | C1, C7 | Rollback pré-commit total, rattrapage post-crash sous 30 min | Moyen — complexité réconciliation |
| INV-80-11 | Pas de starvation standard (≥ 16.7% débit sur 10 min) | BullMQ limiter ratio 5:1 sur workers partagés | C3, C4 | Débit standard mesuré ≥ 16.7% | Moyen — tuning ratio en charge réelle |
4. Mapping critères d'acceptation → mécanismes¶
| Critère ID | Mécanisme(s) | Composant | Observable | Risque |
|---|---|---|---|---|
| CA-01 | Hook upload → admitAutomatic() → transition QUEUED_PRIORITY | C2 | État QUEUED_PRIORITY sans action manuelle | Faible |
| CA-02 | Endpoint POST /seals/urgent → admitManual() | C2, C13 | Requête acceptée, pipeline prioritaire lancé | Faible |
| CA-03 | Histogram seal_latency_seconds P95 sur fenêtre 24h | C8 | P95 < 3600s, label INSUFFICIENT_DATA si N < 100 | Moyen |
| CA-04 | PriorityAnchorProcessor batch 1..20, flush ≤ 5 min | C4 | Taille lot et délai flush mesurables | Faible |
| CA-05 | BullMQ backoff custom [1,5,15,30] + retry_count | C3, C4 | Traces retries aux délais contractuels, état inchangé | Faible |
| CA-06 | SealTimeoutScheduler cron 1 min + transition FAILED_TIMEOUT | C9 | Transition forcée ≥ 120 min + notification + escalade | Faible |
| CA-07 | FreemiumService quota check + rejets 403 | C2 | Rejets 403 pour freemium 3, premium 10, enterprise 1000 | Faible |
| CA-08 | Redis INCR TTL rate-limit + rejets 429 | C2 | Rejets 429 au-delà limite horaire (½/10 selon profil) | Faible |
| CA-09 | SealNotificationService push + fallback email | C6 | Événement push horodaté ou email si pas de device | Moyen |
| CA-10 | Template email avec champs probatoires | C6 | Email contient hash, tx, lien, horodatage TSA | Faible |
| CA-11 | Webhook proof_sealed POST | C6 | POST observé, payload contractuel | Faible |
| CA-11b | Webhook proof_failed POST | C6 | POST observé sur FAILED_TIMEOUT | Faible |
| CA-12 | 4 métriques Prometheus exposées | C8 | Endpoint /metrics contient les 4 métriques | Faible |
| CA-13 | BullMQ limiter ratio 5:1 | C3, C4 | Débit standard ≥ 16.7% sur fenêtre 10 min | Moyen |
| CA-14 | canTransition() close-world, terminaux bloqués | C1 | Transitions sortantes SEALED/FAILED_TIMEOUT refusées | Faible |
| CA-15 | SealCryptoService envelope encryption + destruction | C5 | Audit : aucun secret clair en base | Faible |
| CA-16 | SealReconciliationWorker scan + recréation | C7 | Jobs orphelins détectés et rattrapés sous 30 min | Moyen |
| CA-17 | Webhook retry [1,5,15] min | C6 | Retries observés aux délais contractuels | Faible |
| CA-18 | SealConfigService clamp + log WARN | C10 | Log WARN pour chaque paramètre clampé | Faible |
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 | INV-80-02, CA-01 | Hook upload + admitAutomatic + transition | État QUEUED_PRIORITY, audit trigger=AUTO | Integration |
| TC-NOM-02 | INV-80-03, CA-02 | Endpoint POST + admitManual + guards | Admission en queue, audit trigger=MANUAL | Integration |
| TC-NOM-03 | INV-80-04, CA-03 | Histogram P95 + fenêtre 24h + N min | seal_latency_seconds P95, N exposé, INSUFFICIENT_DATA | Perf (charge) |
| TC-NOM-04 | INV-80-05, CA-04 | PriorityAnchorProcessor batch collect + flush | Taille lot 1..20, délai flush ≤ 5 min | Integration |
| TC-NOM-05 | INV-80-08, CA-09-10 | SealNotificationService push + email | Push horodaté, email avec champs probatoires | Integration |
| TC-NOM-06 | INV-80-08, CA-11 | Webhook proof_sealed POST | Payload contractuel, livraison journalisée | Integration |
| TC-NOM-07 | CA-12 | SealMetricsService 4 métriques | Présence sur /metrics, unités cohérentes | Unit |
| TC-NOM-08 | INV-80-11, CA-13 | BullMQ limiter ratio 5:1 | Débit standard ≥ 16.7% sur 10 min | Perf (charge) |
| TC-NOM-09 | INV-80-09, CA-15 | SealCryptoService + audit stockage | Absence secret clair, chiffrement at-rest | Integration + Sec |
| TC-NOM-10 | INV-80-10, §5.7 | Transaction ACID + crash simulé | Rollback pré-commit, état DB post-commit | Integration |
| TC-NOM-11 | INV-80-10, CA-16 | SealReconciliationWorker scan + recréation | Orphelin détecté, rattrapé, reconciled=true | Integration |
| TC-NOM-12 | INV-80-08, CA-11b | Webhook proof_failed | Payload FAILED_TIMEOUT contractuel | Integration |
| TC-NOM-13 | INV-80-08, §5.12 | Fallback email, omission push | Email envoyé, log INFO "no device registered" | Unit |
| TC-NOM-14 | CA-17 | Webhook retry [1,5,15] | Retries journalisés, webhook_delivery_failed | Integration |
| TC-NOM-15 | CA-18 | SealConfigService clamp + WARN | Log WARN pour chaque clamp, valeurs corrigées | Unit |
| TC-NOM-16 | INV-80-01, §5.4 | Transition retour TSA_PENDING→QUEUED_PRIORITY | Audit motif, retry_count préservé, return_transition_count++ | Unit |
| TC-NOM-17 | INV-80-01, §5.4 | Transition retour TSA_SEALED→TSA_PENDING | Audit motif, token TSA invalidé, retry++ | Unit |
| TC-NOM-18 | §5.10 | Lock Redis NX EX | Lock échoue si détenu, skip + log INFO | Integration |
| TC-ERR-01 | §6, §5.1 | DTO validation class-validator | Rejet 400, aucun enqueuing | Unit |
| TC-ERR-02 | §6, CA-07 | Quota check FreemiumService | Rejet 403, corps sans plan/quota | Integration |
| TC-ERR-03 | §6, CA-08 | Rate-limit Redis INCR | Rejet 429, corps sans plan/quota | Integration |
| TC-ERR-04 | §6, §5.1 | Validation réponse dépendance + retry | Artefact invalide non persisté, retry déclenché, audit | Integration |
| TC-ERR-05 | INV-80-06, CA-05 | BullMQ retry TSA | État reste TSA_PENDING, retries [1,5,15,30] | Integration |
| TC-ERR-06 | INV-80-06, CA-05 | BullMQ retry anchor | État reste ANCHOR_PENDING | Integration |
| TC-ERR-07 | INV-80-04, CA-06 | SealTimeoutScheduler | Transition FAILED_TIMEOUT, notification, escalade | Integration |
| TC-ERR-08 | §5.1 | Enum guard SealStatus | Erreur 500, anomalie journalisée | Unit |
| TC-ERR-09 | INV-80-01, CA-14 | canTransition() close-world | Transition refusée, état inchangé | Unit |
| TC-ERR-10 | INV-80-01, §5.4 | canTransition() close-world | Transition non listée rejetée, audit refus | Unit |
| TC-ERR-11 | INV-80-06, §5.4 | 5e échec, retries_exhausted | État inchangé, audit avec seal_id/state/attempt_count, timer continue | Integration |
| TC-ERR-12 | INV-80-07, CA-08 | Rate-limit enterprise 10/h | Rejet 429 à la 11e demande | Integration |
| TC-INV-01 | INV-80-01 | = TC-ERR-09 + TC-ERR-10 | Close-world vérifié | Unit |
| TC-INV-09 | INV-80-09 | = TC-NOM-09 | Crypto at-rest vérifié | Integration + Sec |
| TC-INV-10 | INV-80-10 | = TC-NOM-10 | Atomicité vérifiée | Integration |
| TC-NR-01..13 | Non-régression | Guards et assertions stables entre releases | CI pipeline vert | Unit + Integration |
| TC-NEG-01..12 | Adversariaux | Validation stricte + guards + rate-limit | Rejets contractuels | Unit + Integration |
6. Gestion des erreurs¶
| Code HTTP | Condition | Traitement | Observable |
|---|---|---|---|
400 | Format invalide (UUID, hash, enum, timestamp) | DTO class-validator rejette avant tout traitement | Réponse API 400 + corps erreur structuré |
403 | Quota mensuel atteint | FreemiumService vérifie compteur, rejette | Réponse 403, corps sans nom plan ni quota restant |
429 | Rate-limit horaire dépassé | Redis INCR + TTL vérifie compteur, rejette | Réponse 429, corps sans nom plan ni quota restant |
422 | Réponse dépendance invalide (TSA/Merkle/blockchain) | Artefact rejeté (non persisté), retry interne déclenché | Audit erreur + retry backoff, pas de rejet API utilisateur |
503 | Dépendance TSA/blockchain indisponible | Retry selon backoff [1,5,15,30] min, état inchangé | Audit tentative, état reste TSA_PENDING ou ANCHOR_PENDING |
500 | État persisté inconnu (hors enum) | Détection au load, journalisation CRITICAL | Alerte opérationnelle |
| — | Timeout global ≥ 120 min | Transition forcée FAILED_TIMEOUT | Notification échec + escalade + webhook proof_failed |
| — | Échec notification | INV-80-08 satisfait si ≥ 1 canal réussit ; webhook retry indépendant | Audit webhook_delivery_failed après épuisement retries |
Anti-disclosure (§6 note risque) : les corps de réponse 403 et 429 ne contiennent JAMAIS le nom du plan, le quota restant, ou le rate-limit restant. Message générique uniquement.
Anti-catch-absorb (learning PD-85/PD-63/PD-250) : tout catch appelant auditLog DOIT propager l'erreur. Pattern finally { await audit() } privilégié.
7. Impacts sécurité¶
| Risque | Mitigation | Composant |
|---|---|---|
| Secrets DEK en clair en base | Envelope encryption HSM, DEK jamais persistée en clair, destruction à transition terminale | C5 |
| Anti-enumeration sur 403/429 | Message générique uniforme, pas de disclosure plan/quota | C2, C13 |
| Abus rate-limit mineur compromis | rate_limit_minor_hour=2 + alerte sécurité si dépassement | C2 |
| Starvation flux standard | Ratio 5:1 avec seuil ≥ 16.7% mesuré sur 10 min | C3, C4 |
| Double exécution post-crash | Lock distribué Redis NX EX sur réconciliation | C7 |
| Transition non autorisée | Close-world canTransition(), audit de tout refus | C1 |
| Cycle oscillant infini | Max 2 transitions retour par scellement (DA-04) | C1 |
| NTP drift | Check au démarrage, alerte CRITICAL si > 1s | C10 |
8. Hypothèses techniques¶
| ID | Hypothèse | Impact si faux |
|---|---|---|
| HT-01 | Le module tsa (PD-39) expose un TsaService.requestTimestamp(hash) stable et fonctionnel | Le processor TSA prioritaire ne peut pas fonctionner — SLA impossible |
| HT-02 | Le module anchor (PD-55) supporte l'ancrage de batches Merkle via AnchorService.anchorBatch() | Le mini-batch prioritaire doit être ré-architecturé |
| HT-03 | Le module merkle (PD-54) expose MerkleService.buildTree(hashes) pour construction d'arbre | Construction manuelle nécessaire |
| HT-04 | Le module freemium expose un FreemiumService avec vérification de quota | Implémentation quota from scratch dans C2 |
| HT-05 | Le module notifications/dispatch (PD-105) supporte push multi-canal + fallback | Fallback email à implémenter manuellement |
| HT-06 | Le module crypto (PD-35) supporte envelope encryption (wrap/unwrap DEK via HSM) | Chiffrement DEK à implémenter from scratch |
| HT-07 | BullMQ v5+ supporte backoff: { type: 'custom' } avec délais personnalisés [1,5,15,30] min | Implémentation backoff manuelle via delayed jobs |
| HT-08 | Redis est disponible pour lock distribué (réconciliation) et rate-limiting (INCR TTL) | Mécanisme de réconciliation dégradé sans Redis |
| HT-09 | prom-client est déjà intégré au projet (métrique anchor_latency_seconds de PD-55) | Installation + configuration nécessaire |
| HT-10 | Aucune modification DDL de colonnes existantes requise (§5.9) | Migration complexe avec stratégie de rollback |
9. Points de vigilance (risques, dette, pièges)¶
-
Complexité du mini-batch prioritaire (C4) : l'agrégation de jobs TSA_SEALED en lot avec flush timeout est le composant le plus complexe. Le timing entre arrivée des jobs et flush doit être testé en charge. Risque de lots sous-optimaux en faible charge (lots de 1 systématiques si peu d'urgents).
-
Interaction queues prioritaires / standard : le ratio 5:1 via BullMQ
limiterdoit être validé en charge mixte réelle. Le seuil de 16.7% est théorique — en pratique, la granularité de scheduling BullMQ peut créer des micro-starvations transitoires. -
Réconciliation et idempotence : le worker de réconciliation recrée des jobs BullMQ. Il faut garantir que le processor est idempotent (un job recréé ne doit pas produire de doublon TSA/ancrage). Le flag
reconciled=trueaide au debug mais ne suffit pas — l'idempotence doit être assurée par vérification d'état avant action. -
DEK lifecycle et crash : si le serveur crashe entre génération DEK et wrapping, la DEK en clair est perdue (en mémoire). Le scellement est récupérable (nouvelle DEK au rattrapage) mais l'artefact temporaire éventuel est orphelin. Couvert par
purgeStale()au démarrage (learning PD-283/PD-262). -
Performance P95 sur charge réelle : le SLA < 60 min P95 dépend fortement de la disponibilité TSA et blockchain. Les retries [1,5,15,30] consomment jusqu'à 51 min. Un seul retry TSA + un retry ancrage = 2 min de retard minimum. Le budget de latence est serré.
-
Webhook retry indépendant : la politique de retry webhook [1,5,15] ne doit pas bloquer le flux principal. L'implémentation via BullMQ delayed jobs séparés garantit l'isolation.
-
Learning PD-282 :
crypto.verify(null, hash, key, sig)pour raw ECDSA HSM. Ne pas utilisercreateVerify()dans les vérifications de signature. -
Learning PD-265 : ESLint rules à respecter —
security/detect-object-injection,restrict-template-expressions,no-unused-vars,require-await.
10. Hors périmètre¶
| Élément | Raison | Story de destination |
|---|---|---|
| UX front-end (déclenchement, statut, notifications) | Découplé en PD-284 | PD-284 |
| Facturation Stripe pay-per-use | Intégration paiement séparée | PD futur (non créé) |
| Dashboard opérationnel dédié | Monitoring via Prometheus/Grafana existant | PD futur |
API admin resubmit/retry (POST /admin/seals/{id}/resubmit, /retry) | Résolution manuelle post-terminaux | PD futur |
| Admissibilité judiciaire de la preuve | Hors périmètre technique | n/a |
| Schéma/version du proof package téléchargeable | Format canonique non fourni par PO | PD futur |
| Escalade opérationnelle détaillée (destinataires, acquittement) | Données métier manquantes | PD futur |
| Politique de rétention artefacts FAILED_TIMEOUT | Non contractualisée | PD futur |
11. Mécanismes cross-module¶
Aucune modification de routes d'autres modules n'est prévue. Le flux automatique mineur est déclenché par un hook d'événement dans le module upload (appel UrgentSealAdmissionService.admitAutomatic() si account_type=minor), sans ajout de guard ni modification de controller existant.
Clause explicite : "Aucune modification d'autres modules" au sens guard/middleware/intercepteur. Seul un appel de service est ajouté dans le pipeline upload existant.
12. Périmètre de test¶
| Niveau de test | In scope | Hors scope (justification) |
|---|---|---|
| Unitaire | Tous les composants C1-C13 : machine d'états, DTOs, config clamp, métriques, fallback push | — |
| Intégration | Flux complet admission → TSA → ancrage → notification, réconciliation, webhook retry, rate-limit Redis, quota FreemiumService, lock distribué | — |
| E2E | Flux A (mineur auto) et flux B (manuel) de bout en bout avec dépendances mockées (TSA, blockchain) | TSA/blockchain réels hors scope (dépendances externes non contrôlables en CI) |
| Performance | P95 < 60 min sur charge mixte (TC-NOM-03), anti-starvation 16.7% (TC-NOM-08) | Charge production réelle (environnement perf dédié hors scope CI) |
| Sécurité | Chiffrement at-rest DEK (TC-NOM-09), anti-disclosure 403/429 (TC-NEG-11), anti-abus mineur (TC-NEG-12) | Pentest complet (hors périmètre story) |
Tous les niveaux de test entre composants d'une même story sont couverts. Les tests E2E utilisent des mocks pour les dépendances externes (TSA server, blockchain L2, APNS) car ces services ne sont pas contrôlables en pipeline CI. Les interactions internes (BullMQ, Redis, PostgreSQL) sont testées avec des containers réels (testcontainers).
Couverture minimale attendue : 80% sur le périmètre in scope (composants C1-C13).