Aller au contenu

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_DATA et 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é -> RECEIVED INTERDIT (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_PENDING autorisé uniquement pour retry technique traçable.
  • ANCHOR_PENDING : autorisées -> SEALED ; retour -> TSA_SEALED interdit (raison : non-régression d'horodatage, retry au même état).
  • SEALED : -> * : INTERDITE (état terminal). Résolution manuelle : API admin POST /admin/seals/{id}/resubmit (rôle admin requis, 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 admin POST /admin/seals/{id}/retry (rôle admin requis, 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 :

  1. 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.
  2. 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.
  3. Après épuisement des 4 retries (5e échec consécutif sur la même étape) : le job reste dans son état courant (TSA_PENDING ou ANCHOR_PENDING), aucun nouveau retry n'est planifié, et un événement retries_exhausted est émis dans le journal d'audit (incluant seal_id, état courant, nombre de tentatives). Le job attend passivement l'expiration du final_timeout. À l'expiration : transition forcée vers FAILED_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

  1. Réception document + métadonnées valides (§5.1).
  2. Détection account_type=minor.
  3. Contrôles quota/rate-limit.
  4. Admission en file prioritaire TSA (RECEIVED -> QUEUED_PRIORITY).
  5. Traitement TSA puis mini-batch Merkle/ancrage prioritaire.
  6. Génération proof package.
  7. Notification multi-canal selon configuration.
  8. État final SEALED sous SLA global.

5.6 Flux nominal B — Fast-track manuel utilisateur éligible

  1. Requête explicite de scellement urgent.
  2. Contrôles éligibilité plan + quota + rate-limit.
  3. Même pipeline prioritaire que flux A.
  4. État final SEALED ou FAILED_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_TIMEOUT interne de chaîne : dépassement final_timeout => transition FAILED_TIMEOUT.
  • En FAILED_TIMEOUT : notification échec obligatoire (incluant webhook proof_failed si 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)

  1. GWT-01 Mineur auto fast-track Given un compte minor et un upload valide, When le document est reçu, Then le workflow urgent démarre sans action utilisateur et atteint SEALED.

  2. GWT-02 Manuel premium Given un compte premium avec quota disponible, When il déclenche urgent, Then la demande est admise en priorité et notifiée à SEALED.

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

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

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

  6. 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 webhook proof_failed est envoyé si configuré.

  7. GWT-07 Formats invalides Given un hash_document non conforme regex, When la demande est validée, Then réponse 400 et aucun enqueuing.

  8. GWT-08 Terminalité des états Given une preuve en SEALED, When une transition sortante est demandée, Then elle est refusée (état terminal).

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

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

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

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

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

  14. 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 Le calcul P95 est basé sur une fenêtre d'observation opérationnelle définie Résolu : fenêtre 24h glissante, N≥100 (§3, §5.2) n/a
H-04 Le plan enterprise "illimité" n'impose pas de plafond technique caché Résolu : plafonné à 1000/mois + 10/heure (§5.2) 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 async BullMQ + Redis pour 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 P95 sur fenêtre glissante 24h.
  • Synchronisation horloge : NTP obligatoire, tolérance ±1s (§5.13).

10.2 Points à clarifier (données manquantes)

  1. Fenêtre exacte de calcul SLA P95 Résolu : 24h glissante, N≥100 (§3).
  2. Politique de paiement freemium pay-per-use hors Stripe dans ce périmètre (autoriser/retarder le scellement en absence de paiement confirmé).
  3. Canal "push mobile" pour comptes sans device enregistré Résolu : fallback email (§5.12).
  4. Format exact et versionnage du proof package téléchargeable (schéma JSON/ZIP canonique).
  5. Règles d'escalade opérationnelle (destinataires, délai d'acquittement, criticité).
  6. Politique de conservation/rétention des artefacts d'échec FAILED_TIMEOUT.
  7. Valeurs cibles de SLO pour priority_queue_depth (seuils d'alerte non fournis).
  8. Confirmation explicite du repo cible (backend uniquement 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.