Aller au contenu

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/urgentadmitManual() 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)

  1. 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).

  2. Interaction queues prioritaires / standard : le ratio 5:1 via BullMQ limiter doit ê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.

  3. 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=true aide au debug mais ne suffit pas — l'idempotence doit être assurée par vérification d'état avant action.

  4. 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).

  5. 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é.

  6. 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.

  7. Learning PD-282 : crypto.verify(null, hash, key, sig) pour raw ECDSA HSM. Ne pas utiliser createVerify() dans les vérifications de signature.

  8. 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).