Aller au contenu

PD-286-SPECIFICATION

1. Objectif

Spécifier contractuellement l’export de dossiers probatoires au-delà de 1 GB, avec partition en volumes transparents côté utilisateur, sans perte de preuve, sans changement du format final .pvproof (conteneur), et avec intégrité vérifiable volume par volume.

2. Périmètre / Hors périmètre

Inclus

  • Backend : remplacement du rejet 413 (quand taille totale > seuil volume) par un export partitionné en N volumes.
  • Backend : évolution du contrat de réponse pour supporter volumes[] tout en conservant la rétrocompatibilité volume unique.
  • Backend : manifest racine multi-volumes + manifest partiel par volume.
  • Backend : inclusion du manifest partiel dans chaque volume (ExportVolumeDto.manifest) pour vérification côté app.
  • App : orchestration séquentielle des téléchargements multi-volumes, assemblage en un .pvproof unique.
  • App : progression unique utilisateur (multi-volumes transparent).
  • Traçabilité : audit incluant volumes_count et empreintes d’intégrité par volume.
  • Limite globale d’export : MAX_TOTAL_EXPORT_BYTES = 10_737_418_240 bytes (10 GB).

Exclu

  • Configuration utilisateur/admin des seuils (seuils figés au build).
  • Changement du format final .pvproof (conteneur inchangé).
  • Modification guards auth/premium et rate limiting existants.
  • Modification de la pipeline de validation des preuves.
  • Optimisation parallèle des téléchargements volumes (séquentiel uniquement).
  • Toute exigence UX non objectivable (ex. “ressenti de fluidité”) : hors périmètre.

3. Définitions

  • Preuve : unité atomique exportable (fichier(s) + métadonnées) non scindable entre volumes.
  • Volume standard : sous-ensemble de preuves tel que somme des tailles <= VOLUME_MAX_BYTES.
  • Volume dédié exceptionnel : volume contenant une seule preuve atomique dont la taille est > VOLUME_MAX_BYTES et <= MAX_TOTAL_EXPORT_BYTES.
  • Manifest partiel : manifest décrivant un volume unique.
  • Manifest racine : manifest décrivant l’ensemble des volumes exportés.
  • Export multi-volumes : export contenant totalVolumes >= 2.
  • Intégrité volume : vérification de integrityHash = SHA3-256(manifest partiel canonicalisé).
  • Canonicalisation JSON : RFC 8785 (JSON Canonicalization Scheme / JCS).
  • Etat terminal : état sans transition sortante autorisée.

4. Invariants (non négociables)

ID Règle Justification
INV-286-01 Pour tout volume v : soit 0 < v.estimatedBytes <= 805_306_368 (volume standard), soit 805_306_368 < v.estimatedBytes <= 10_737_418_240 uniquement si le volume contient une seule preuve atomique (volume dédié exceptionnel). Hors borne: rejet de la réponse backend (5xx interne) et export marqué FAILED. Contrainte technique mobile/backend + atomicité
INV-286-02 Taille totale exportée <= 10_737_418_240 bytes (10 GB). Hors borne: rejet métier explicite (413 EXPORT_TOTAL_LIMIT_EXCEEDED). Protection anti-abus
INV-286-03 Union des preuves de tous les volumes = ensemble des preuves validées, sans doublon ni omission. Intégrité fonctionnelle
INV-286-04 Une preuve est atomique: aucune preuve ne peut être répartie sur plusieurs volumes. Cohérence vérification
INV-286-05 Export single-volume legacy (totalSize <= 805_306_368) conserve le contrat legacy (manifest/signedUrls utilisables sans volumes[]). Rétrocompatibilité
INV-286-06 Export avec volumes[] expose volumeIndex continu [0..totalVolumes-1] sans trou ni doublon. Déterminisme client
INV-286-07 integrityHash de chaque volume est vérifié fonctionnellement par l’app avant assemblage, à partir du manifest reçu dans ExportVolumeDto. Échec hash => export global FAILED. Détection corruption
INV-286-08 Le .pvproof final est unique. Le conteneur .pvproof reste inchangé; seul pvproof.json interne inclut volumes_count + assembled_from[] si export avec volumes[]. Traçabilité assemblage + compatibilité format
INV-286-09 Audit WORM fail-closed inclut exportId, volumes_count, integrityHash[], statut final. Conformité/audit
INV-286-10 Machine à états d’export contractuelle (cf. transitions ci-dessous) : toute transition non listée = interdite. Robustesse comportementale
INV-286-11 Etats terminaux COMPLETED, FAILED, EXPIRED : → * INTERDITE (état terminal, résolution manuelle/nouvelle demande uniquement). Fermeture explicite

Transitions autorisées (et retours)

  • REQUESTED -> PLANNED_SINGLE (si totalSize <= 768 MB et contrat legacy applicable)
  • REQUESTED -> PLANNED_MULTI (si contrat volumes[] requis, y compris volume dédié exceptionnel)
  • REQUESTED -> FAILED (erreur de validation métier/technique)
  • PLANNED_SINGLE -> DOWNLOADING
  • PLANNED_MULTI -> DOWNLOADING
  • DOWNLOADING -> ASSEMBLING
  • DOWNLOADING -> FAILED (erreur réseau/intégrité)
  • ASSEMBLING -> COMPLETED
  • ASSEMBLING -> FAILED
  • REQUESTED|PLANNED_*|DOWNLOADING|ASSEMBLING -> EXPIRED (TTL dépassé)

Transitions retour explicites (downgrade/retour)

  • PLANNED_MULTI -> PLANNED_SINGLE : INTERDITE dans un même export (nouvelle requête obligatoire).
  • FAILED -> REQUESTED : INTERDITE dans un même export (recréation exportId obligatoire).
  • EXPIRED -> REQUESTED : INTERDITE dans un même export.
  • COMPLETED -> ASSEMBLING|DOWNLOADING|PLANNED_*|REQUESTED : INTERDITE.
  • Réapplication des quotas/limites: sur nouvelle requête uniquement (immédiate, sans conservation d’état de progression).
  • Données temporaires d’un export terminal: conservées selon politique de rétention existante, non réutilisables pour reprise.

5. Flux nominaux

5.1 Modèle de données contractuel (formats et validations)

Donnée Format / encodage Taille / bornes Caractères / casse Regex Comportement invalide
exportId UUID v4 (texte) 36 chars hex + -, case-insensitive à la lecture, normalisé lowercase à l’écriture /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ 400 INVALID_EXPORT_ID
volumeIndex entier signé JSON min 0, max dynamique totalVolumes-1 n/a n/a 500 INVALID_VOLUME_INDEX_RANGE côté backend
totalVolumes entier signé JSON min 1, pas de borne max fixe contractuelle n/a n/a 500 INVALID_TOTAL_VOLUMES
estimatedBytes entier signé JSON (bytes) min 1, max 10_737_418_240 (règles INV-286-01) n/a n/a volume rejeté, export FAILED
integrityHash SHA3-256 hex 64 chars (32 bytes) [0-9a-f], case-sensitive /^[0-9a-f]{64}$/ 422 VOLUME_INTEGRITY_HASH_INVALID
manifest JSON objet (RFC 8785 canonicalisable) non vide UTF-8 JSON n/a 500 VOLUME_MANIFEST_INVALID
signedUrl URL HTTPS max 4096 chars UTF-8 URL, case-sensitive sur path/query n/a 422 SIGNED_URL_INVALID
manifestRootHash SHA3-256 hex 64 chars [0-9a-f], case-sensitive /^[0-9a-f]{64}$/ 500 MANIFEST_ROOT_HASH_INVALID
assembled_from[] tableau d’objets {volumeIndex, integrityHash, estimatedBytes} cardinalité = totalVolumes selon champs référencés n/a export FAILED

Règle d’unicité: les formats ci-dessus sont la source unique; toute autre section les référence sans redéfinition.

5.2 SLA temporels (transitions avec temps)

SLA Défaut Min Max Configurabilité Comportement à expiration
SIGNED_URL_TTL 24h 1h 72h Oui (env) état export -> EXPIRED, téléchargement refusé, reprise interdite
EXPORT_SESSION_TTL 24h 1h 72h Oui (env) état export -> EXPIRED, nouvelle requête requise

Unité: heures (h).
Contexte perf de référence: réseau mobile 4G standard, appareil classe iPhone 12 équivalent.
Si non-respect bornes config: démarrage service refusé (fail-closed).

5.3 Flux nominal A — Export single-volume

  1. Le client demande un export.
  2. Le backend valide ownership/éligibilité et calcule la taille totale.
  3. Si totalSize <= 805_306_368 et pas de contrainte imposant volumes[], backend renvoie contrat single-volume compatible legacy.
  4. L’app télécharge, vérifie intégrité, assemble .pvproof.
  5. L’app notifie succès unique et purge temporaire.

5.4 Flux nominal B — Export avec volumes[] (multi-volumes ou volume dédié exceptionnel)

  1. Le client demande un export.
  2. Le backend valide puis partitionne les preuves en N volumes (N>=1), en respectant INV-286-01.
  3. Le backend renvoie volumes[] + métadonnées communes; chaque volume contient manifest + integrityHash.
  4. L’app traite les volumes séquentiellement: téléchargement -> recalcul hash sur manifest (RFC 8785 + SHA3-256) -> comparaison -> ajout assembleur.
  5. Après le dernier volume validé, l’app génère le .pvproof final unique (conteneur inchangé) avec métadonnées internes d’assemblage.
  6. L’app notifie succès unique et purge temporaire.

5.5 Atomicité / distribution / inter-modules

  • Atomicité DB + queue/append-only: Aucune atomicité multi-composant additionnelle applicable (pas de flux DB+queue nouveau introduit par ce besoin).
  • Protection distribuée (lock/idempotence/réconciliation/rate-limit/clearing): Aucun mécanisme de protection distribuée additionnel applicable (module d’export synchrone côté API + orchestration locale app; rate-limit inchangé hors périmètre).
  • Contraintes inter-modules: Aucune contrainte inter-module additionnelle applicable.

5bis. Diagrammes (applicable)

Diagramme d’état (>= 3 états)

stateDiagram-v2
    [*] --> REQUESTED
    REQUESTED --> PLANNED_SINGLE: legacy_eligible
    REQUESTED --> PLANNED_MULTI: volumes_contract_required
    REQUESTED --> FAILED: validation_error
    REQUESTED --> EXPIRED: ttl_expired

    PLANNED_SINGLE --> DOWNLOADING
    PLANNED_MULTI --> DOWNLOADING
    PLANNED_SINGLE --> EXPIRED: ttl_expired
    PLANNED_MULTI --> EXPIRED: ttl_expired

    DOWNLOADING --> ASSEMBLING: all_volumes_ok
    DOWNLOADING --> FAILED: network_or_hash_error
    DOWNLOADING --> EXPIRED: ttl_expired

    ASSEMBLING --> COMPLETED: pvproof_built
    ASSEMBLING --> FAILED: assemble_error
    ASSEMBLING --> EXPIRED: ttl_expired

    COMPLETED --> REQUESTED: INTERDITE
    COMPLETED --> DOWNLOADING: INTERDITE
    FAILED --> REQUESTED: INTERDITE
    EXPIRED --> REQUESTED: INTERDITE

    COMPLETED --> [*]
    FAILED --> [*]
    EXPIRED --> [*]

Diagramme de séquence (>= 2 services + opérations crypto)

sequenceDiagram
    participant App as ProbatioVault App
    participant API as Export API
    participant Store as Object Storage
    participant Audit as WORM Audit

    App->>API: POST /exports {complaintId}
    API->>API: compute totalSize(bytes)
    API->>API: partition proofs -> volumes[]
    API->>API: for each volume: integrityHash = sha3_256(jcs_rfc8785(manifest_partial))
    API-->>App: ComplaintFileResponseDto {volumes[], metadata}

    loop volumeIndex = 0..N-1
        App->>Store: GET signedUrl(file_i)
        Store-->>App: file bytes
        App->>App: read manifest from ExportVolumeDto.manifest
        App->>App: recompute sha3_256(jcs_rfc8785(manifest))
        App->>App: compare recomputedHash vs integrityHash
        alt hash mismatch
            App->>Audit: append {exportId, volumeIndex, hash_mismatch}
            App-->>App: state -> FAILED
        else hash ok
            App->>App: append volume data to assembler stream
            App->>Audit: append {exportId, volumeIndex, hash_ok}
        end
    end

    App->>App: finalize pvproof.json + volumes_count + assembled_from[]
    App->>API: POST /exports/{exportId}/finalize {finalStatus: COMPLETED}
    API->>Audit: append FINAL {exportId, status: COMPLETED, integrityHashes[], volumes_count}
    API-->>App: 204 No Content

5.6 Endpoint de finalisation (app → backend)

L'app DOIT appeler POST /exports/:exportId/finalize après assemblage du .pvproof (COMPLETED) ou en cas d'échec (FAILED). Le backend : 1. Vérifie l'ownership (anti-énumération : 404 uniforme si inconnu OU non détenu). 2. Transite la state machine vers l'état terminal (COMPLETED ou FAILED). 3. Émet l'audit FINAL via ExportAuditService.appendFinal() (INV-286-09, fail-closed).

Champ Type Description
finalStatus "COMPLETED" \| "FAILED" Statut final de l'export côté app
reasonCode string? Code de raison en cas de FAILED (optionnel, max 64 chars)
Code HTTP Condition
204 Finalisation réussie
404 Export inconnu ou non détenu (anti-énumération)
409 Session déjà en état terminal

6. Cas d’erreur

ID Condition Réponse contractuelle
ERR-286-01 totalSize > 10GB 413 EXPORT_TOTAL_LIMIT_EXCEEDED, aucun volume émis
ERR-286-02 Une preuve atomique seule > 10GB 413 PROOF_TOO_LARGE, aucun volume émis
ERR-286-03 integrityHash format invalide en réponse backend échec contrat backend, export FAILED, audit fail-closed
ERR-286-04 Hash volume recalculé différent du hash attendu arrêt immédiat, export global FAILED, message explicite utilisateur
ERR-286-05 Expiration signed URL/session pendant traitement état EXPIRED, export échoué, nouvelle demande requise
ERR-286-06 volumeIndex/totalVolumes incohérents (trous, doublons, hors plage) refus assemblage, export FAILED
ERR-286-07 Echec téléchargement d’un volume export global FAILED (pas de succès partiel)
ERR-286-08 Donnée d’entrée mal formée (exportId, URL) 400/422 selon champ, sans tentative d’assemblage

7. Critères d’acceptation (testables)

ID Critère Observable
CA-286-01 Export 2GB produit multi-volumes conformes (INV-286-01/02/06) Réponse API avec totalVolumes>=2; contraintes volume/index respectées
CA-286-02 Export 500MB reste single-volume rétrocompatible (INV-286-05) Réponse exploitable sans volumes[] obligatoire
CA-286-03 Si preuve unique 900MB (<10GB), volume dédié exceptionnel accepté (INV-286-01/04) Réponse avec volumes[], volume unique contenant une seule preuve atomique
CA-286-04 Assemblage app produit un unique .pvproof (INV-286-08) Un seul fichier final, statut COMPLETED, conteneur inchangé
CA-286-05 Hash de chaque volume vérifié via manifest API (INV-286-07) Logs/tests montrent recalcul RFC8785+SHA3-256 et comparaison effective
CA-286-06 Echec d’un volume => échec export global explicite (INV-286-07/11) Statut final FAILED, aucun succès partiel
CA-286-07 Audit inclut volumes_count et hashes volumes (INV-286-09) Entrées WORM présentes et corrélées à exportId
CA-286-08 Machine à états refuse transitions non autorisées (INV-286-10/11) Tests de transition retournent erreur contractuelle
CA-286-09 Si TTL expire pendant flux, statut devient EXPIRED (INV-286-11) Transition observée + reprise interdite sur même exportId
CA-286-10 Preuve atomique >10GB rejetée explicitement 413 PROOF_TOO_LARGE, aucun assemblage engagé

8. Scénarios de test (Given / When / Then)

  • GWT-286-01 (single-volume legacy)
    Given un dossier de 500MB validé
    When l’utilisateur déclenche l’export
    Then l’API renvoie un contrat single-volume compatible legacy et l’app génère un .pvproof unique.

  • GWT-286-02 (multi-volumes nominal)
    Given un dossier de 2GB validé
    When l’utilisateur déclenche l’export
    Then l’API renvoie volumes[] (N>=2), chaque volume respecte INV-286-01, et l’app assemble un unique .pvproof.

  • GWT-286-03 (intégrité volume KO)
    Given un volume téléchargé altéré
    When l’app recalcule SHA3-256 du manifest partiel canonicalisé RFC 8785
    Then le hash diffère, l’export passe FAILED, et un audit d’échec est émis.

  • GWT-286-04 (preuve >768MB, <10GB)
    Given une preuve unique de 900MB
    When le backend planifie les volumes
    Then le backend émet un volume dédié exceptionnel (preuve non scindée), sans rejet 422.

  • GWT-286-05 (preuve >10GB)
    Given une preuve unique de 11GB
    When le backend valide la demande
    Then le backend répond 413 PROOF_TOO_LARGE sans volume émis.

  • GWT-286-06 (expiration TTL)
    Given un export en cours avec TTL atteint
    When l’app tente le volume suivant
    Then le flux passe EXPIRED, l’export est arrêté et une nouvelle requête est requise.

  • GWT-286-07 (cohérence index volumes)
    Given une réponse API avec volumeIndex manquant (trou)
    When l’app valide la structure volumes[]
    Then l’assemblage est refusé et l’export passe FAILED.

9. Hypothèses explicites

ID Hypothèse Impact si faux
H-286-01 Le module IntegrityHashComputer implémente RFC 8785 de manière stable entre backend et app. Hash non déterministe, faux négatifs d’intégrité.
H-286-02 Les seuils 768MB et 10GB sont acceptés conformité produit/réglementaire. Requalification métier, risque de non-conformité.
H-286-03 Les clients legacy tolèrent la présence optionnelle de volumes[] sans régression. Risque de casse client, besoin de versionnement API.
H-286-04 La politique de rétention temporaire existante couvre les exports terminaux sans reprise. Dette opérationnelle de stockage/temp files.

10. Points à clarifier

10.1 Contraintes techniques (stack réelle cible)

  • ProbatioVault-backend: NestJS + TypeORM + PostgreSQL
  • ProbatioVault-app: React Native + Expo SDK 54 + TypeScript
  • Aucun composant iOS natif Swift/SwiftUI requis par cette story.

10.2 Questions ouvertes

ID Point Donnée manquante / décision attendue
Q-286-02 Codes d’erreur finaux et taxonomie Validation de la nomenclature (EXPORT_TOTAL_LIMIT_EXCEEDED, PROOF_TOO_LARGE, etc.).
Q-286-03 Politique de rétention post-FAILED/EXPIRED Durée chiffrée et obligations conformité.
Q-286-04 Valeurs finales TTL en production Confirmation des bornes contractuelles proposées (24h défaut, 1h min, 72h max).

Références

  • Epic parent : EPIC-286 — Export probatoire et restitution
  • JIRA : PD-286
  • Repos concernés : ProbatioVault-backend, ProbatioVault-app
  • Documents associés : PD-85, PD-283, PD-101, besoin complémentaire PD-286, learnings injectés PD-283/PD-101