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é enNvolumes. - 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
.pvproofunique. - App : progression unique utilisateur (multi-volumes transparent).
- Traçabilité : audit incluant
volumes_countet empreintes d’intégrité par volume. - Limite globale d’export :
MAX_TOTAL_EXPORT_BYTES = 10_737_418_240bytes (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_BYTESet<= 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(sitotalSize <= 768 MBet contrat legacy applicable)REQUESTED -> PLANNED_MULTI(si contratvolumes[]requis, y compris volume dédié exceptionnel)REQUESTED -> FAILED(erreur de validation métier/technique)PLANNED_SINGLE -> DOWNLOADINGPLANNED_MULTI -> DOWNLOADINGDOWNLOADING -> ASSEMBLINGDOWNLOADING -> FAILED(erreur réseau/intégrité)ASSEMBLING -> COMPLETEDASSEMBLING -> FAILEDREQUESTED|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¶
- Le client demande un export.
- Le backend valide ownership/éligibilité et calcule la taille totale.
- Si
totalSize <= 805_306_368et pas de contrainte imposantvolumes[], backend renvoie contrat single-volume compatible legacy. - L’app télécharge, vérifie intégrité, assemble
.pvproof. - L’app notifie succès unique et purge temporaire.
5.4 Flux nominal B — Export avec volumes[] (multi-volumes ou volume dédié exceptionnel)¶
- Le client demande un export.
- Le backend valide puis partitionne les preuves en
Nvolumes (N>=1), en respectantINV-286-01. - Le backend renvoie
volumes[]+ métadonnées communes; chaque volume contientmanifest+integrityHash. - L’app traite les volumes séquentiellement: téléchargement -> recalcul hash sur
manifest(RFC 8785 + SHA3-256) -> comparaison -> ajout assembleur. - Après le dernier volume validé, l’app génère le
.pvprooffinal unique (conteneur inchangé) avec métadonnées internes d’assemblage. - 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.pvproofunique. -
GWT-286-02 (multi-volumes nominal)
Given un dossier de 2GB validé
When l’utilisateur déclenche l’export
Then l’API renvoievolumes[](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 recalculeSHA3-256du manifest partiel canonicalisé RFC 8785
Then le hash diffère, l’export passeFAILED, 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 rejet422. -
GWT-286-05 (preuve >10GB)
Given une preuve unique de 11GB
When le backend valide la demande
Then le backend répond413 PROOF_TOO_LARGEsans 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 passeEXPIRED, l’export est arrêté et une nouvelle requête est requise. -
GWT-286-07 (cohérence index volumes)
Given une réponse API avecvolumeIndexmanquant (trou)
When l’app valide la structurevolumes[]
Then l’assemblage est refusé et l’export passeFAILED.
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 + PostgreSQLProbatioVault-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