PD-80 — Spécification (v3)¶
1. Objectif¶
Cette User Story contractualise un mode de scellement prioritaire permettant, pour les comptes mineurs (auto) et les utilisateurs autorisés (manuel), d'obtenir une preuve probatoire complète (TSA RFC 3161 + inclusion Merkle + signature HSM + ancrage blockchain L2 + package de preuve + notification) en P95 < 15 minutes.
Objectif mesurable principal : latence upload -> SEALED en P95 < 15 min sur le contexte de référence défini en §10.1, mesuré sur une fenêtre glissante de 24 heures avec un minimum de 100 scellements pour validité statistique. Pire-cas sans dégradation externe : < 25 min. Pire-cas absolu (retries TSA + congestion blockchain) : < 75 min. Timeout final : 120 min.
2. Périmètre / Hors périmètre¶
Inclus¶
- Déclenchement fast-track hybride : automatique (mineur) et manuel (utilisateur standard éligible).
- Files BullMQ prioritaires TSA et ancrage.
- Mini-batch Merkle prioritaire.
- Dégradation fail-soft avec retries contractuels.
- Timeout final et escalade opérationnelle.
- Quotas mensuels et rate limiting urgent.
- Notifications push, email, webhook.
- Exposition de métriques de latence et de profondeur de queue.
- Invariants de sécurité crypto (dont chiffrement au repos des artefacts temporaires).
Exclu¶
- Composants UX front-end (PD-284).
- Facturation Stripe pay-per-use (intégration paiement).
- Dashboard opérationnel dédié.
- Décision judiciaire d'admissibilité de la preuve (hors périmètre, non testable en environnement technique).
3. Définitions¶
- Fast-track : traitement prioritaire d'un scellement.
- TSA : Time Stamping Authority RFC 3161.
- Mini-batch prioritaire : lot Merkle de preuves urgentes.
- Proof package : artefacts vérifiables remis à l'utilisateur (hash, preuve Merkle, références TSA/blockchain).
- SLA global : délai total entre réception upload et état final
SEALED. - État terminal : état sans transition sortante automatique.
- P95 : 95e percentile de latence, calculé sur une fenêtre glissante de 24 heures avec un minimum de N=100 scellements pour validité statistique. Si N < 100 sur la fenêtre, le P95 est marqué
INSUFFICIENT_DATAet aucune violation SLA n'est émise. - Contexte de référence perf : backend production ProbatioVault (NestJS + BullMQ + PostgreSQL + Redis), voir §10.1.
4. Invariants (non négociables)¶
| ID | Règle | Justification |
|---|---|---|
| INV-80-01 | Toute demande urgent suit une machine d'états explicitement définie, sans transition implicite. | Évite ambiguïté et comportements divergents. |
| INV-80-02 | account.type=minor déclenche fast-track automatique sans action utilisateur additionnelle. | Protection des mineurs et réduction friction. |
| INV-80-03 | Déclenchement manuel urgent autorisé uniquement pour utilisateurs éligibles au quota/rate-limit. | Anti-abus et conformité offre. |
| INV-80-04 | SLA global contractuel : upload -> SEALED doit respecter P95 < 15 min (fenêtre 24h glissante, N≥100). Pire-cas sans dégradation externe < 25 min. Dépassement à >=120 min force FAILED_TIMEOUT + notification + escalade. | Exigence métier critique. Budget détaillé : batch ≤5 min + TSA ≤1 min + submit ≤1 min + finality ≤2 min + proof/notif ≤1 min. |
| INV-80-05 | Mini-batch prioritaire : aucune preuve urgente n'est ancrée individuellement hors mini-batch. | Cohérence économique et vérifiabilité homogène. |
| INV-80-06 | En indisponibilité TSA/blockchain, le flux ne doit pas échouer immédiatement : retries selon politique contractuelle (4 retries max, délais ⅕/15/30 min). Chaque retry reste dans le même état. Après épuisement des 4 retries (5e échec consécutif), le job reste dans son état courant et le timer final_timeout détermine la transition FAILED_TIMEOUT. | Fail-soft obligatoire. |
| INV-80-07 | Quotas mensuels et rate-limit horaire sont appliqués avant admission en queue prioritaire. | Protection anti-flood économique. |
| INV-80-08 | Notification de résultat obligatoire : succès (SEALED) ou échec final (FAILED_TIMEOUT) selon canaux configurés. Si aucun device push n'est enregistré, l'email est le fallback primaire. INV-80-08 est satisfait si au moins un canal de notification réussit. | Transparence utilisateur et traçabilité. |
| INV-80-09 | INV-80-envelope-encryption : tout artefact cryptographique temporaire (clé, fragment, DEK, ReKey) est chiffré au repos (AES-256-GCM ou HSM envelope). Aucun secret en clair en base. | Exigence crypto non négociable. |
| INV-80-10 | Flux DB + queue/append-only respecte atomicité contractuelle : ACID synchrone, async idempotent retry-safe, rattrapage post-crash via worker de réconciliation (cf. §5.10). | Non-régression probatoire et robustesse. |
| INV-80-11 | Aucune starvation du flux standard : sous charge mixte, le débit standard ne descend pas en dessous de 1/(priority_weight+standard_weight) du débit total (soit ≥⅙ ≈ 16.7% avec ratio 5:1), observé sur une fenêtre de 10 minutes. | Équité de service globale. |
5. Flux nominaux¶
5.1 Modèle de données contractuel (formats et contraintes)¶
Formats définis une seule fois ici ; toute autre section y fait référence.
| Donnée | Format / encodage | Taille | Jeu caractères | Case | Regex/validation | Si invalide |
|---|---|---|---|---|---|---|
document_id | UUID v4 (texte canonique) | 36 chars | [0-9a-f-] | insensitive hex, forme canonique en sortie lowercase | ^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ | Rejet 400 |
user_id | UUID v4 | 36 chars | [0-9a-f-] | idem | idem | Rejet 400 |
account_type | enum | minor\|standard\|premium\|enterprise | ASCII lower | sensitive | appartenance enum | Rejet 400 |
hash_document | SHA-256 hex | 64 chars (32 bytes) | [a-f0-9] | sensitive | ^[a-f0-9]{64}$ | Rejet 400 |
tsa_token | DER binaire encodé base64 | 1..16384 bytes décodés | base64 standard | sensitive | décodage base64 + parse RFC3161 valide | 422 |
merkle_root | SHA-256 hex | 64 chars | [a-f0-9] | sensitive | ^[a-f0-9]{64}$ | 422 |
merkle_proof[] | tableau ordonné de nœuds SHA-256 hex | 1..64 éléments, chaque 64 chars | [a-f0-9] | sensitive | tous éléments valides | 422 |
blockchain_tx | hex préfixé 0x | 66 chars (0x + 64) | [a-f0-9x] | sensitive | ^0x[a-f0-9]{64}$ | 422 |
sealed_status | enum d'état | voir §5.4 | ASCII upper _ | sensitive | appartenance enum | 500 si état inconnu persisté |
webhook_payload.status | enum sortie | SEALED\|FAILED_TIMEOUT | ASCII upper _ | sensitive | appartenance enum | non émission webhook |
timestamp_rfc3339 | date-heure UTC | ISO-8601 | UTF-8 | sensitive | parse RFC3339 UTC | 422 |
Note enum account_type : le plan freemium correspond à account_type=standard avec quota_freemium_month appliqué. Il n'existe pas de valeur enum freemium distincte. Le quota applicable est déterminé par le champ plan du compte (freemium, premium, enterprise), tandis que account_type distingue uniquement minor|standard|premium|enterprise. Un compte freemium a account_type=standard et plan=freemium.
5.2 Paramètres numériques contractuels¶
| Paramètre | Défaut | Min | Max | Unité | Contexte/percentile (si perf) | Hors bornes |
|---|---|---|---|---|---|---|
sla_upload_hash_target | 5 | 1 | 30 | secondes | P95, §10.1 | Alerte si >target; non bloquant tant que global SLA respecté |
sla_upload_hash_max | 30 | 5 | 60 | secondes | pire-cas | Marquage retard étape |
sla_tsa_target | 60 | 5 | 120 | secondes | P95 | Retry si timeout étape |
sla_tsa_max | 600 | 60 | 900 | secondes | pire-cas (1 retry) | Retry |
sla_batch_wait_max | 300 | 60 | 600 | secondes | P95 (batch_time_max) | Flush forcé |
sla_merkle_build_max | 30 | 5 | 60 | secondes | P95 | Retry |
sla_anchor_target | 120 | 10 | 300 | secondes | P95 (finality 12 blocs) | Retry/attente queue |
sla_anchor_max | 900 | 60 | 900 | secondes | pire-cas (timeout) | Retry |
sla_proof_max | 5 | 1 | 60 | secondes | P95 | FAILED_TIMEOUT si global >=120 min |
sla_notify_max | 5 | 1 | 60 | secondes | P95 | Retry notification |
sla_global_max | 15 | 5 | 60 | minutes | P95 (fenêtre 24h, N≥100) | >15: SLO violation; >=120 min: FAILED_TIMEOUT |
p95_window | 24 | 1 | 168 | heures | n/a | clamp + alerte WARN |
p95_min_samples | 100 | 10 | 10000 | scellements | n/a | P95 marqué INSUFFICIENT_DATA |
retry_delays | [1,5,15,30] | 1 | 30 | minutes | n/a | config invalide rejetée au démarrage |
retry_max_attempts | 4 | 1 | 10 | tentatives | n/a | clamp + alerte WARN |
final_timeout | 120 | 60 | 240 | minutes | n/a | transition forcée FAILED_TIMEOUT |
batch_size_min | 5 | 1 | 20 | éléments | n/a | clamp à min=1 en config; alerte WARN au démarrage |
batch_size_max | 20 | 5 | 100 | éléments | n/a | clamp à max autorisé; alerte WARN au démarrage |
batch_time_max | 5 | 1 | 15 | minutes | n/a | clamp + alerte WARN au démarrage |
priority_weight | 5 | 1 | 10 | ratio | n/a | clamp; alerte WARN au démarrage |
standard_weight | 1 | 1 | 10 | ratio | n/a | clamp; alerte WARN au démarrage |
rate_limit_urgent | 1 | 1 | 10 | requête/heure/utilisateur | n/a | Rejet 429 |
quota_freemium_month | 3 | 0 | 100 | scellements/mois | n/a | Rejet 403 |
quota_premium_month | 10 | 0 | 500 | scellements/mois | n/a | Rejet 403 |
quota_enterprise_month | 1000 | 100 | 10000 | scellements/mois | n/a | Rejet 403 |
rate_limit_minor_hour | 2 | 1 | 10 | requête/heure/compte | n/a | Rejet 429 + alerte sécurité |
rate_limit_enterprise_hour | 10 | 1 | 100 | requête/heure/utilisateur | n/a | Rejet 429 |
webhook_retry_delays | [1,5,15] | 1 | 15 | minutes | n/a | config invalide rejetée au démarrage |
webhook_retry_max | 3 | 1 | 5 | tentatives | n/a | clamp + alerte WARN |
reconciliation_scan_interval | 5 | 1 | 30 | minutes | n/a | clamp + alerte WARN |
reconciliation_orphan_threshold | 10 | 5 | 60 | minutes | n/a | clamp + alerte WARN |
reconciliation_max_catchup_delay | 30 | 10 | 120 | minutes | n/a | alerte CRITICAL si dépassé |
Comportement clamp : toute valeur de configuration hors bornes [Min, Max] est ramenée à la borne la plus proche (clamp). Chaque clamp appliqué est journalisé au niveau WARN au démarrage du service, incluant le paramètre concerné, la valeur fournie et la valeur clampée.
5.3 SLA temporels (transitions d'état)¶
| Transition | Défaut | Min | Max | Configurable | Comportement à expiration |
|---|---|---|---|---|---|
RECEIVED -> QUEUED_PRIORITY | immédiat | 0 | 1 min | non | reste en queue, alerte si >1 min |
QUEUED_PRIORITY -> TSA_PENDING | immédiat | 0 | 1 min | non | reste en queue prioritaire, alerte si >1 min |
TSA_PENDING -> TSA_SEALED | 5 min cible | 1 min | 10 min | oui (borné) | retry selon backoff (même état) |
TSA_SEALED -> ANCHOR_PENDING | immédiat | 0 | 1 min | non | alerte opérationnelle |
ANCHOR_PENDING -> SEALED | 45 min cible | 5 min | 50 min | oui (borné) | retry selon backoff (même état) |
ANY_NON_TERMINAL -> FAILED_TIMEOUT | 120 min | 60 min | 240 min | oui (borné) | notification échec + escalade |
5.4 Machine d'états et transitions (incluant transitions retour)¶
États : RECEIVED, QUEUED_PRIORITY, TSA_PENDING, TSA_SEALED, ANCHOR_PENDING, SEALED (terminal), FAILED_TIMEOUT (terminal).
RECEIVED: autorisées-> QUEUED_PRIORITY; interdites-> *autres.QUEUED_PRIORITY: autorisées-> TSA_PENDING; retour autorisé-> RECEIVEDINTERDIT (raison : admission immuable après journalisation).TSA_PENDING: autorisées-> TSA_SEALED; autorisée-> QUEUED_PRIORITY(retry requeue interne uniquement, sans nouvel upload).TSA_SEALED: autorisées-> ANCHOR_PENDING; retour-> TSA_PENDINGautorisé uniquement pour retry technique traçable.ANCHOR_PENDING: autorisées-> SEALED; retour-> TSA_SEALEDinterdit (raison : non-régression d'horodatage, retry au même état).SEALED:-> * : INTERDITE(état terminal). Résolution manuelle : API adminPOST /admin/seals/{id}/resubmit(rôleadminrequis, crée un nouveau scellement, ne modifie pas l'existant). Hors périmètre de cette story (PD futur).FAILED_TIMEOUT:-> * : INTERDITE(état terminal). Résolution manuelle : API adminPOST /admin/seals/{id}/retry(rôleadminrequis, crée un nouveau job urgent avec référence au job original). Hors périmètre de cette story (PD futur).
Interaction retries / transitions retour :
- Chaque retry reste dans le même état. Le compteur de retry est incrémenté sur le job BullMQ, sans changement d'état dans la machine d'états.
- Les transitions retour (
TSA_PENDING -> QUEUED_PRIORITY,TSA_SEALED -> TSA_PENDING) sont des requeue internes distincts des retries. Elles sont déclenchées uniquement par une décision technique explicite (ex : invalidation du résultat TSA détectée avant ancrage) et sont tracées dans le journal d'audit avec motif. - Après épuisement des 4 retries (5e échec consécutif sur la même étape) : le job reste dans son état courant (
TSA_PENDINGouANCHOR_PENDING), aucun nouveau retry n'est planifié, et un événementretries_exhaustedest émis dans le journal d'audit (incluantseal_id, état courant, nombre de tentatives). Le job attend passivement l'expiration dufinal_timeout. À l'expiration : transition forcée versFAILED_TIMEOUT+ notification échec + escalade opérationnelle.
Invariant transitions : aucune transition non listée n'est permise.
5.5 Flux nominal A — Fast-track automatique mineur¶
- Réception document + métadonnées valides (§5.1).
- Détection
account_type=minor. - Contrôles quota/rate-limit.
- Admission en file prioritaire TSA (
RECEIVED -> QUEUED_PRIORITY). - Traitement TSA puis mini-batch Merkle/ancrage prioritaire.
- Génération proof package.
- Notification multi-canal selon configuration.
- État final
SEALEDsous SLA global.
5.6 Flux nominal B — Fast-track manuel utilisateur éligible¶
- Requête explicite de scellement urgent.
- Contrôles éligibilité plan + quota + rate-limit.
- Même pipeline prioritaire que flux A.
- État final
SEALEDouFAILED_TIMEOUT.
5.7 Atomicité multi-composant (DB + async)¶
| Scope | Synchrone/Async | Garantie |
|---|---|---|
| Persistance état + audit + admission logique | Synchrone transaction DB | Atomicité ACID |
| Publication travail queue prioritaire | Async post-commit | Idempotent, retry-safe |
| Append-only journal probatoire | Async post-commit | Append-only, idempotent |
| Agrégation Merkle | Async post-commit | Rattrapage via worker réconciliation (§5.10) |
| Crash pré-commit | — | rollback total, aucun artefact persistant |
| Crash post-commit | — | état DB source de vérité, async rattrapé par réconciliation (§5.10) |
5.8 Contraintes inter-modules¶
Aucune contrainte inter-module de type "guard bloquant des routes d'un autre module" n'est identifiée dans le besoin courant. Clause explicite : "Aucune contrainte inter-module applicable".
5.9 Migration DDL¶
Aucune modification de colonne existante n'est explicitement demandée par le besoin. Statut : hors périmètre pour cette spec tant qu'aucun changement de type/nullabilité/contrainte existante n'est imposé.
5.10 Worker de réconciliation post-crash¶
Un worker de réconciliation périodique garantit qu'aucun scellement ne reste orphelin après un crash ou une indisponibilité.
| Paramètre | Valeur | Description |
|---|---|---|
reconciliation_scan_interval | 5 min (configurable, §5.2) | Fréquence de scan des jobs potentiellement orphelins |
reconciliation_orphan_threshold | 10 min (configurable, §5.2) | Un job non terminal sans activité depuis ce délai est considéré orphelin |
reconciliation_max_catchup_delay | 30 min (configurable, §5.2) | Délai max toléré pour rattraper un orphelin. Au-delà : alerte CRITICAL |
Critère de détection d'orphelin : job dans un état non terminal (RECEIVED, QUEUED_PRIORITY, TSA_PENDING, TSA_SEALED, ANCHOR_PENDING) dont le last_activity_at est antérieur à now() - reconciliation_orphan_threshold et sans job BullMQ actif correspondant.
Protection contre double exécution : avant de recréer un job, le worker acquiert un lock distribué Redis (clé reconciliation:lock:{seal_id}, TTL = reconciliation_scan_interval × 2). Si le lock est déjà détenu (job en cours de traitement par un autre worker ou une autre instance du réconciliateur), le job est ignoré pour ce cycle de scan. Le lock est libéré automatiquement par TTL. Ce mécanisme garantit qu'un job ne peut jamais être recréé en doublon — éliminant le risque de double TSA ou double ancrage.
Action de rattrapage : le worker recrée le job BullMQ manquant dans la queue prioritaire au même état, avec un flag reconciled=true pour traçabilité. Le compteur de retry est préservé. Le lock distribué est maintenu pendant toute la durée du rattrapage.
5.11 Politique de notification webhook¶
Les webhooks suivent une politique de retry indépendante du retry du scellement :
| Paramètre | Valeur | Description |
|---|---|---|
webhook_retry_delays | [1, 5, 15] min | Délais entre tentatives de livraison webhook |
webhook_retry_max | 3 tentatives | Nombre max de tentatives (4 livraisons totales) |
Événements webhook : - proof_sealed : émis sur transition vers SEALED (payload : document_id, status=SEALED, sealed_at, blockchain_tx, merkle_root) - proof_failed : émis sur transition vers FAILED_TIMEOUT (payload : document_id, status=FAILED_TIMEOUT, failed_at, reason, last_state)
Chaque tentative et son résultat (HTTP status, durée) sont journalisés. Après épuisement des retries, un événement webhook_delivery_failed est émis dans le journal d'audit (sans bloquer le flux principal).
5.12 Fallback notification push¶
Si aucun device push n'est enregistré pour l'utilisateur : 1. L'email est le fallback primaire (toujours émis). 2. La tentative push est omise (pas d'erreur, journalisation INFO "no device registered"). 3. INV-80-08 est satisfait si au moins un canal (email ou webhook) réussit.
5.13 Synchronisation horloge¶
Tous les serveurs participant au flux de scellement DOIVENT être synchronisés via NTP avec une tolérance maximale de 1 seconde par rapport à une source de temps de référence (stratum 2 ou mieux). Le non-respect de cette tolérance est détecté au démarrage du service et journalisé en alerte CRITICAL.
5.14 Cycle de vie DEK (Data Encryption Key)¶
Les DEK utilisées pour chiffrer les artefacts temporaires (INV-80-09) suivent un cycle de vie strict :
| Phase | Action | Détail |
|---|---|---|
| Génération | Une DEK AES-256 est générée par scellement via crypto.randomBytes(32) | Unicité par scellement, jamais réutilisée |
| Wrapping | La DEK est immédiatement wrappée par la KEK (Key Encryption Key) HSM via envelope encryption | La DEK en clair n'existe qu'en mémoire, jamais persistée en clair |
| Usage | Chiffrement AES-256-GCM des artefacts temporaires (fragments, tokens intermédiaires) | IV unique par opération de chiffrement |
| Destruction post-scellement | À la transition vers SEALED ou FAILED_TIMEOUT, la DEK wrappée est supprimée de la base | Suppression synchrone dans la même transaction que la transition terminale |
| Rotation KEK | La rotation de la KEK HSM est gérée par PD-40 (hors périmètre) | Les DEK wrappées avec l'ancienne KEK restent déchiffrables via key version |
Invariant : à tout instant, la DEK en clair n'existe qu'en mémoire du processus actif. Aucune DEK en clair n'est persistée en base, en log, ou sur disque.
5bis. Diagrammes¶
5bis.1 Machine d'états du scellement (statechart)¶
Référence : §5.4, INV-80-01 (machine d'états explicite), INV-80-06 (retries dans le même état).
stateDiagram-v2
[*] --> RECEIVED : upload valide (§5.1)
RECEIVED --> QUEUED_PRIORITY : admission\n(quota + rate-limit OK, INV-80-03/07)
QUEUED_PRIORITY --> TSA_PENDING : dispatch worker TSA
TSA_PENDING --> TSA_SEALED : token RFC 3161 obtenu
TSA_PENDING --> TSA_PENDING : retry (1/5/15/30 min)\n4 max, INV-80-06
TSA_PENDING --> QUEUED_PRIORITY : requeue interne\n(invalidation TSA détectée)
TSA_SEALED --> ANCHOR_PENDING : inclusion mini-batch\nMerkle (INV-80-05)
TSA_SEALED --> TSA_PENDING : retry technique traçable
ANCHOR_PENDING --> SEALED : finality L2 confirmée\n(blockchain_tx validé)
ANCHOR_PENDING --> ANCHOR_PENDING : retry (1/5/15/30 min)\n4 max, INV-80-06
RECEIVED --> FAILED_TIMEOUT : final_timeout >= 120 min\n(INV-80-04)
QUEUED_PRIORITY --> FAILED_TIMEOUT : final_timeout >= 120 min
TSA_PENDING --> FAILED_TIMEOUT : final_timeout >= 120 min
TSA_SEALED --> FAILED_TIMEOUT : final_timeout >= 120 min
ANCHOR_PENDING --> FAILED_TIMEOUT : final_timeout >= 120 min
SEALED --> [*]
FAILED_TIMEOUT --> [*]
note right of SEALED : Terminal — aucune\ntransition sortante (CA-14)
note right of FAILED_TIMEOUT : Terminal — notification\néchec + escalade (INV-80-08) 5bis.2 Flux nominal — Scellement fast-track (séquence)¶
Référence : §5.5/5.6 (flux A et B), INV-80-02 (mineur auto), INV-80-04 (SLA P95 < 15 min), INV-80-05 (mini-batch), INV-80-08 (notification), INV-80-09 (envelope encryption DEK), INV-80-10 (atomicité).
sequenceDiagram
participant Client
participant API as API Gateway
participant DB as PostgreSQL
participant Redis as Redis / BullMQ
participant TSA as TSA RFC 3161
participant Merkle as Worker Merkle
participant BC as Blockchain L2
participant HSM as HSM (KEK)
participant Notif as Notification Service
Client->>API: POST /seals (document + hash)
API->>API: Validation formats (§5.1)
API->>DB: Vérif quota + rate-limit (INV-80-03/07)
alt account_type=minor (INV-80-02)
API->>API: fast-track automatique
else manuel + éligible
API->>API: fast-track sur demande
end
Note over API,DB: Transaction ACID (INV-80-10)
API->>HSM: Générer DEK + wrap (INV-80-09)
HSM-->>API: wrapped_dek
API->>DB: INSERT seal (RECEIVED) + audit log
API->>DB: COMMIT
API->>Redis: Enqueue prioritaire (post-commit)
DB-->>API: QUEUED_PRIORITY
API-->>Client: 202 Accepted (seal_id)
Redis->>TSA: Requête horodatage RFC 3161
alt TSA disponible
TSA-->>Redis: token TSA (DER/base64)
Redis->>DB: UPDATE état → TSA_SEALED
else TSA indisponible (INV-80-06)
TSA-->>Redis: erreur
Redis->>Redis: retry backoff (1/5/15/30 min)
end
Merkle->>DB: Collecte preuves TSA_SEALED
Note over Merkle: Mini-batch 1..20 (INV-80-05)
Merkle->>Merkle: Construction arbre Merkle
Merkle->>DB: UPDATE état → ANCHOR_PENDING
Merkle->>BC: Ancrage merkle_root sur L2
alt Finality confirmée
BC-->>Merkle: blockchain_tx (0x...)
Merkle->>DB: UPDATE état → SEALED + suppression DEK
else Blockchain indisponible (INV-80-06)
BC-->>Merkle: erreur
Merkle->>Merkle: retry backoff (1/5/15/30 min)
end
Merkle->>Notif: Déclencher notification (INV-80-08)
alt Device push enregistré
Notif->>Client: Push notification
else Pas de device (§5.12)
Notif->>Client: Email fallback
end
Notif->>Client: Webhook proof_sealed (si configuré) 5bis.3 Cycle de vie DEK — Envelope encryption (séquence)¶
Référence : §5.14, INV-80-09 (chiffrement au repos).
sequenceDiagram
participant Worker as Worker Scellement
participant HSM as HSM (KEK maître)
participant DB as PostgreSQL
participant Artefact as Artefacts temporaires
Note over Worker,HSM: Génération (1 DEK par scellement)
Worker->>Worker: crypto.randomBytes(32) → DEK clair
Worker->>HSM: Wrap DEK avec KEK (AES-256)
HSM-->>Worker: wrapped_dek + key_version
Note over Worker,DB: Persistance (DEK jamais en clair en DB)
Worker->>DB: INSERT wrapped_dek (INV-80-09)
Note over Worker,Artefact: Usage (AES-256-GCM, IV unique)
Worker->>Artefact: Chiffrer fragments avec DEK clair
Note right of Worker: DEK clair uniquement en mémoire
alt Transition → SEALED
Worker->>DB: DELETE wrapped_dek (transaction ACID)
Worker->>Worker: Zéroïser DEK mémoire
else Transition → FAILED_TIMEOUT
Worker->>DB: DELETE wrapped_dek (transaction ACID)
Worker->>Worker: Zéroïser DEK mémoire
end
Note over DB: Post-destruction : aucune DEK en base 6. Cas d'erreur¶
400 BAD_REQUEST: format invalide (document_id,hash_document, timestamps, enums).403 FORBIDDEN: quota mensuel atteint.429 TOO_MANY_REQUESTS: rate limit urgent dépassé.422 UNPROCESSABLE_ENTITY: réponse de dépendance externe invalide (token TSA non parseable reçu du TSA, Merkle proof invalide retourné par le worker Merkle, tx hash invalide retourné par le service blockchain). Ces erreurs sont internes au pipeline et déclenchent un retry, pas un rejet API vers l'utilisateur.503 SERVICE_UNAVAILABLE: dépendance TSA/blockchain indisponible au moment de tentative, avec replanification retry.504 GATEWAY_TIMEOUTinterne de chaîne : dépassementfinal_timeout=> transitionFAILED_TIMEOUT.- En
FAILED_TIMEOUT: notification échec obligatoire (incluant webhookproof_failedsi configuré) + événement d'escalade opérationnelle. - Aucun échec silencieux autorisé pour les transitions terminales.
Note risque : disclosure de plan via codes d'erreur : les réponses 403 (quota) et 429 (rate-limit) peuvent théoriquement permettre à un attaquant de déduire le plan d'un utilisateur. Ce risque est accepté car : (a) l'information de plan n'est pas classifiée sensible, (b) unifier en un seul code d'erreur dégraderait l'expérience développeur. Les corps de réponse d'erreur ne DOIVENT PAS inclure le nom du plan ni le quota restant.
7. Critères d'acceptation (testables)¶
| ID | Critère | Observable |
|---|---|---|
| CA-01 | Compte mineur déclenche fast-track automatique | état passe à QUEUED_PRIORITY sans action manuelle |
| CA-02 | Utilisateur éligible déclenche fast-track manuel | requête urgente acceptée et pipeline prioritaire lancé |
| CA-03 | SLA global respecte P95 < 15 min (fenêtre 24h glissante, N≥100). Pire-cas sans dégradation externe < 25 min. Timeout final >=120 min. | métrique seal_latency_seconds P95 < 900s |
| CA-04 | Mini-batch prioritaire respecte bornes | lot ancrage contient 1..20 preuves, flush <= 5 min |
| CA-05 | Backoff retry appliqué, chaque retry reste dans le même état | traces retries aux délais ⅕/15/30 min, état inchangé |
| CA-06 | Timeout final force échec | à >=120 min, statut FAILED_TIMEOUT + notification + escalade |
| CA-07 | Quotas mensuels par plan appliqués | freemium 3/mois, premium 10/mois, enterprise 1000/mois |
| CA-08 | Rate-limit urgent appliqué (y compris enterprise 10/h) | >limite requête/heure/utilisateur => 429 |
| CA-09 | Notification push envoyée au scellement (ou fallback email si pas de device) | événement push horodaté sur transition SEALED, ou email si pas de device |
| CA-10 | Email contient champs probatoires requis | hash + tx blockchain + lien preuve + horodatage TSA présents |
| CA-11 | Webhook proof_sealed émis si configuré | POST observé avec payload contractuel |
| CA-11b | Webhook proof_failed émis si configuré sur FAILED_TIMEOUT | POST observé avec payload contractuel |
| CA-12 | Métriques exposées | seal_latency_seconds, priority_queue_depth, tsa_latency_seconds, anchor_latency_seconds disponibles |
| CA-13 | Pas de starvation standard | débit standard ≥ 1/(priority_weight+standard_weight) du débit total sur fenêtre 10 min |
| CA-14 | États terminaux bloquent toute sortie | SEALED -> * et FAILED_TIMEOUT -> * refusés explicitement |
| CA-15 | Secrets crypto temporaires jamais en clair en base | audit stockage prouve chiffrement at-rest conforme INV-80-09 |
| CA-16 | Worker réconciliation rattrape les orphelins | jobs orphelins détectés et rattrapés sous reconciliation_max_catchup_delay |
| CA-17 | Webhook retry avec backoff | retries webhook observés aux délais ⅕/15 min |
| CA-18 | Clamps journalisés au démarrage | log WARN pour chaque paramètre clampé |
8. Scénarios de test (Given / When / Then)¶
-
GWT-01 Mineur auto fast-track Given un compte
minoret un upload valide, When le document est reçu, Then le workflow urgent démarre sans action utilisateur et atteintSEALED. -
GWT-02 Manuel premium Given un compte
premiumavec quota disponible, When il déclenche urgent, Then la demande est admise en priorité et notifiée àSEALED. -
GWT-03 Quota freemium atteint Given un compte standard/freemium ayant consommé 3 scellements du mois, When il demande urgent, Then la requête est rejetée
403. -
GWT-04 Rate-limit horaire Given un utilisateur ayant déjà fait 1 urgent dans l'heure (ou 10 pour enterprise), When il soumet une demande urgente supplémentaire, Then la requête est rejetée
429. -
GWT-05 Indisponibilité TSA transitoire Given TSA indisponible temporairement, When un job urgent TSA échoue, Then retries suivent ⅕/15/30 min sans passage immédiat en échec final, l'état reste
TSA_PENDING. -
GWT-06 Timeout final Given une indisponibilité prolongée >120 min, When le délai final est atteint, Then statut devient
FAILED_TIMEOUT, escalade est émise, et webhookproof_failedest envoyé si configuré. -
GWT-07 Formats invalides Given un
hash_documentnon conforme regex, When la demande est validée, Then réponse400et aucun enqueuing. -
GWT-08 Terminalité des états Given une preuve en
SEALED, When une transition sortante est demandée, Then elle est refusée (état terminal). -
GWT-09 Non-starvation Given charge mixte prioritaire + standard, When le scheduler applique ratio 5:1, Then le débit standard reste ≥ 16.7% du débit total sur 10 min.
-
GWT-10 Chiffrement des secrets temporaires Given des artefacts crypto temporaires persistés, When un audit de stockage est exécuté, Then aucun secret en clair n'est présent.
-
GWT-11 Épuisement retries Given TSA indisponible et 4 retries épuisés, When le 5e échec consécutif survient, Then le job reste dans son état courant et attend
final_timeout. -
GWT-12 Réconciliation post-crash Given un crash serveur avec un job en cours, When le worker de réconciliation scanne après restart, Then le job orphelin est détecté et rattrapé sous 30 min.
-
GWT-13 Push sans device Given un utilisateur sans device push enregistré, When un scellement atteint
SEALED, Then l'email est envoyé comme fallback, aucune erreur push n'est levée. -
GWT-14 Enterprise quota Given un compte enterprise ayant atteint 1000 scellements/mois, When il demande un scellement urgent, Then la requête est rejetée
403.
9. Hypothèses explicites¶
| ID | Hypothèse | Impact si faux |
|---|---|---|
| H-01 | Les dépendances PD-37/39/54/55/21/30/105 sont stables et disponibles | Le SLA <1h peut devenir inatteignable |
| H-02 | Le webhook cible accepte TLS et disponibilité suffisante | Retards/échecs notification webhook |
| H-03 | n/a | |
| H-04 | n/a | |
| H-05 | Aucun changement DDL destructif de colonnes existantes n'est requis | Si faux, une stratégie migration détaillée devient obligatoire |
| H-06 | La synchronisation NTP des serveurs est maintenue à ±1s | Si faux, les calculs de latence P95 et les SLA par étape sont faussés |
10. Contraintes techniques et points à clarifier¶
10.1 Contraintes techniques (obligatoire)¶
- Projet cible pressenti :
ProbatioVault-backend. - Stack contractuelle :
NestJS + TypeORM + PostgreSQL(et orchestration asyncBullMQ + Redispour ce périmètre). - Interdits de formulation : aucune référence à Swift/SwiftUI, Spring Boot ou stack non présente dans ce projet.
- Contexte de référence performance : environnement backend de production ProbatioVault (instance applicative NestJS, queue BullMQ/Redis, base PostgreSQL), mesure en
P95sur fenêtre glissante 24h. - Synchronisation horloge : NTP obligatoire, tolérance ±1s (§5.13).
10.2 Points à clarifier (données manquantes)¶
Fenêtre exacte de calcul SLA P95Résolu : 24h glissante, N≥100 (§3).- Politique de paiement freemium pay-per-use hors Stripe dans ce périmètre (autoriser/retarder le scellement en absence de paiement confirmé).
Canal "push mobile" pour comptes sans device enregistréRésolu : fallback email (§5.12).- Format exact et versionnage du
proof packagetéléchargeable (schéma JSON/ZIP canonique). - Règles d'escalade opérationnelle (destinataires, délai d'acquittement, criticité).
- Politique de conservation/rétention des artefacts d'échec
FAILED_TIMEOUT. - Valeurs cibles de SLO pour
priority_queue_depth(seuils d'alerte non fournis). - Confirmation explicite du repo cible (
backenduniquement ou backend + app pour API de déclenchement manuel).
Références¶
- Epic : Référence épique non fournie (donnée manquante).
- JIRA :
PD-80 - Repos concernés :
ProbatioVault-backend(principal),ProbatioVault-app(consommation API, hors UX PD-284). - Documents associés : PD-37, PD-39, PD-54, PD-55, PD-21, PD-30, PD-105, PD-284, learnings PD-55 / PD-81 / BATCH-RETRO.