Aller au contenu

PD-79 — Spécification Technique

Metadata

Champ Valeur
Story ID PD-79
Version 1.0
Date 2026-02-16
Auteur ChatGPT (gov-balanced)

0) Contexte et objectifs

Cette spécification étend le module PD-60 sans modifier le moteur probatoire ni créer de nouvel endpoint. L'extension introduit une catégorie documentaire déclarative B2C_EVIDENCE_MINOR avec des règles configurables de formats MIME et de taille maximale.

Principes directeurs: - Réutiliser POST /documents/upload (aucun endpoint additionnel) - Préserver l'atomicité probatoire PD-60 (transaction englobante + audit append-only + JWS) - Conserver le modèle zero-knowledge (pas de fichier en clair persistant côté serveur) - Rendre les règles de catégorie paramétrables (formats, taille max)

1) Modèle de données

1.1 Fonctionnalités (F-79-xx)

ID Fonctionnalité Description
F-79-01 Catégorie optionnelle Ajouter category dans la requête d'upload, optionnelle, défaut DEFAULT
F-79-02 Catégorie B2C_EVIDENCE_MINOR Activer une catégorie dédiée avec whitelist MIME stricte
F-79-03 Règles configurables Formats autorisés et taille max gérés en base (config)
F-79-04 Journalisation renforcée Ajouter les attributs de catégorie et décision de validation dans l'audit append-only
F-79-05 SLA de scellement Respect du temps de scellement < 1s pour les uploads valides

1.2 Extension de l'entité Deposit

Ajout de colonnes non disruptives (compatible historique PD-60):

export enum DocumentCategory {
  DEFAULT = 'DEFAULT',
  B2C_EVIDENCE_MINOR = 'B2C_EVIDENCE_MINOR',
}

@Entity('deposits', { schema: 'vault_secure' })
export class Deposit {
  // ...champs existants PD-60

  @Column({
    name: 'document_category',
    type: 'varchar',
    length: 40,
    default: DocumentCategory.DEFAULT,
  })
  documentCategory!: DocumentCategory;

  @Column({
    name: 'detected_mime_type',
    type: 'varchar',
    length: 120,
    nullable: true,
  })
  detectedMimeType?: string;

  @Column({
    name: 'uploaded_size_bytes',
    type: 'bigint',
    nullable: true,
  })
  uploadedSizeBytes?: string;
}

1.3 Table de configuration des catégories

Objectif: externaliser les règles de validation par catégorie pour éviter le hard-code.

@Entity('document_category_configs', { schema: 'vault_secure' })
export class DocumentCategoryConfig {
  @PrimaryGeneratedColumn('uuid')
  id!: string;

  @Column({ name: 'category', type: 'varchar', length: 40, unique: true })
  category!: string;

  @Column({ name: 'allowed_mime_types', type: 'jsonb' })
  allowedMimeTypes!: string[];

  @Column({ name: 'max_size_bytes', type: 'bigint' })
  maxSizeBytes!: string;

  @Column({ name: 'sealing_sla_ms', type: 'integer', default: 1000 })
  sealingSlaMs!: number;

  @Column({ name: 'audit_level', type: 'varchar', length: 20, default: 'STANDARD' })
  auditLevel!: 'STANDARD' | 'REINFORCED';

  @Column({ name: 'is_active', type: 'boolean', default: true })
  isActive!: boolean;

  @CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
  createdAt!: Date;

  @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
  updatedAt!: Date;
}

1.4 Données de configuration initiales (seed)

category allowed_mime_types max_size_bytes sealing_sla_ms audit_level
DEFAULT (règles PD-60 actuelles) (valeur PD-60) 1000 STANDARD
B2C_EVIDENCE_MINOR image/png, image/jpeg, audio/mp4, audio/mpeg, audio/wav, video/mp4, video/quicktime 104857600 1000 REINFORCED

2) API Contract

2.1 Endpoint (inchangé)

  • POST /documents/upload
  • Pas de nouvel endpoint
  • Même moteur de scellement PD-60

2.2 DTO étendu

export const PROBATIVE_NOTICE_TEXT = 'Vous réalisez un acte probatoire daté.';

export enum DocumentCategory {
  DEFAULT = 'DEFAULT',
  B2C_EVIDENCE_MINOR = 'B2C_EVIDENCE_MINOR',
}

export class CreateDepositDto {
  @IsString()
  @IsNotEmpty()
  @MaxLength(255)
  clientRequestId!: string;

  @IsString()
  @IsNotEmpty()
  @Matches(/^[a-f0-9]{64}$/)
  documentFingerprint!: string;

  @IsBoolean()
  @Equals(true)
  probativeNoticeAck!: boolean;

  @IsString()
  @Equals(PROBATIVE_NOTICE_TEXT)
  probativeNoticeText!: string;

  @IsOptional()
  @IsEnum(DocumentCategory)
  category?: DocumentCategory;
}

Règle d'interprétation: - si category absent ou null -> DEFAULT - si category fourni -> doit appartenir à l'enum sinon 400

2.3 Contrat de réponse (ajout non bloquant)

Le schéma de réponse peut inclure, sans rupture, les métadonnées suivantes:

type UploadResponse = {
  depositId: string;
  proofReceipt: string;
  existenceTimestampUtc: string;
  category: 'DEFAULT' | 'B2C_EVIDENCE_MINOR';
};

3) Logique métier

3.1 CategoryConfigService

Responsabilités: - Charger la configuration active d'une catégorie - Résoudre DEFAULT si catégorie absente - Vérifier whitelist MIME - Vérifier taille max - Exposer un cache local court (TTL) pour minimiser latence sans compromettre la cohérence

export interface ResolvedCategoryConfig {
  category: 'DEFAULT' | 'B2C_EVIDENCE_MINOR';
  allowedMimeTypes: string[];
  maxSizeBytes: number;
  sealingSlaMs: number;
  auditLevel: 'STANDARD' | 'REINFORCED';
}

export interface CategoryConfigService {
  resolve(category?: string): Promise<ResolvedCategoryConfig>;
  assertMimeAllowed(config: ResolvedCategoryConfig, mime: string): void;
  assertSizeAllowed(config: ResolvedCategoryConfig, bytes: number): void;
}

3.2 Enchaînement de validation dans DepositService

Ordre de traitement (avant scellement final): 1. Parser category (défaut DEFAULT) 2. Charger config catégorie 3. Détecter MIME réel (sniffing serveur, sans confiance au header client) 4. Contrôler MIME contre whitelist catégorie 5. Contrôler taille réelle contre max_size_bytes 6. Valider fingerprint SHA-256 (server-side constant-time compare) 7. Exécuter transaction probatoire PD-60 (S3 opaque + DB + audit + JWS)

Apprentissages réutilisés: - PD-43: traitement streaming (pas de buffering complet mémoire) - PD-38: hash probatoire validé côté serveur, comparaison constant-time - PD-60: transaction englobante, idempotence advisory lock, audit append-only

3.3 Journalisation renforcée (audit append-only)

Pour B2C_EVIDENCE_MINOR, enrichir l'événement d'audit avec: - documentCategory - detectedMimeType - uploadedSizeBytes - categoryConfigVersion (ou hash config) - validationDecision (ACCEPTED | REJECTED + motif)

Exigence: écriture append-only uniquement, aucune mise à jour destructrice.

4) Flux nominal (diagramme de séquence textuel)

Client -> API POST /documents/upload (multipart + metadata + category?)
API -> DTO Validation (class-validator)
API -> CategoryConfigService.resolve(category || DEFAULT)
API -> Stream Processor: detect MIME + count size + compute SHA-256
API -> CategoryConfigService.assertMimeAllowed
API -> CategoryConfigService.assertSizeAllowed
API -> DepositService.validateFingerprintConstantTime
API -> Transaction (advisory lock by clientRequestId)
Transaction -> S3 opaque write (no clear-file persistence)
Transaction -> Insert vault_secure.deposits (with document_category)
Transaction -> Append audit trace (reinforced fields)
Transaction -> Generate JWS proof receipt
Transaction -> Commit
API -> 201 Created (depositId, proofReceipt, existenceTimestampUtc, category)

4bis) Diagrammes Mermaid

4bis.1 Diagramme d'états — Cycle de vie d'un dépôt

Le dépôt traverse plusieurs états de validation avant scellement. Tout rejet est final et audité (INV-79-04). Aucun upload non scellé ne persiste (INV-79-02).

stateDiagram-v2
    [*] --> RECEIVED : POST /documents/upload

    RECEIVED --> DTO_VALIDATED : class-validator OK
    RECEIVED --> REJECTED_400 : category invalide (ERR-79-001)

    DTO_VALIDATED --> CONFIG_RESOLVED : CategoryConfigService.resolve()
    DTO_VALIDATED --> REJECTED_422_CONFIG : config absente/inactive (ERR-79-002)

    CONFIG_RESOLVED --> STREAM_ANALYZED : MIME détecté + taille comptée + SHA-256
    STREAM_ANALYZED --> MIME_VALIDATED : MIME ∈ whitelist catégorie
    STREAM_ANALYZED --> REJECTED_415 : MIME hors whitelist (ERR-79-003)

    MIME_VALIDATED --> SIZE_VALIDATED : taille ≤ max_size_bytes
    MIME_VALIDATED --> REJECTED_413 : taille > max_size_bytes (ERR-79-004)

    SIZE_VALIDATED --> FINGERPRINT_VALIDATED : SHA-256 constant-time match
    SIZE_VALIDATED --> REJECTED_422_FP : fingerprint mismatch (ERR-79-005)

    FINGERPRINT_VALIDATED --> SEALED : transaction probatoire PD-60 commit
    FINGERPRINT_VALIDATED --> REJECTED_500 : erreur interne (ERR-79-007)

    SEALED --> [*] : 201 Created + proofReceipt

    REJECTED_400 --> [*] : 400 Bad Request
    REJECTED_422_CONFIG --> [*] : 422 Unprocessable Entity
    REJECTED_415 --> [*] : 415 Unsupported Media Type
    REJECTED_413 --> [*] : 413 Payload Too Large
    REJECTED_422_FP --> [*] : 422 Unprocessable Entity
    REJECTED_500 --> [*] : 500 Internal Server Error

    note right of SEALED
        INV-79-01 : aucun fichier clair persistant
        INV-79-03 : dépôt immuable (insert-only)
        INV-79-05 : preuve JWS traçable
        INV-79-06 : scellement < 1s
    end note

    note right of REJECTED_415
        INV-79-02 : aucune persistance
        INV-79-04 : audit append-only du rejet
    end note

4bis.2 Diagramme de séquence — Upload avec catégorie B2C_EVIDENCE_MINOR

Interactions multi-services pour un upload catégorisé, incluant la détection MIME serveur, la validation configurable et la transaction probatoire PD-60.

sequenceDiagram
    participant C as Client
    participant API as DepositController
    participant DTO as class-validator
    participant CCS as CategoryConfigService
    participant SP as StreamProcessor
    participant DS as DepositService
    participant S3 as S3 Opaque Storage
    participant DB as PostgreSQL (vault_secure)
    participant AUD as AuditLog (append-only)
    participant JWS as JWS Signer

    C->>API: POST /documents/upload<br/>(multipart + category=B2C_EVIDENCE_MINOR)
    API->>DTO: validate(CreateDepositDto)
    DTO-->>API: ✓ DTO valide

    API->>CCS: resolve("B2C_EVIDENCE_MINOR")
    CCS->>DB: SELECT FROM document_category_configs<br/>WHERE category = 'B2C_EVIDENCE_MINOR' AND is_active
    DB-->>CCS: ResolvedCategoryConfig (cache TTL)
    CCS-->>API: config {allowedMimeTypes, maxSizeBytes, auditLevel: REINFORCED}

    API->>SP: stream(file) → detect MIME + count size + compute SHA-256
    SP-->>API: {detectedMime, sizeBytes, sha256Hash}

    API->>CCS: assertMimeAllowed(config, detectedMime)
    Note over CCS: INV-79-02 : rejet si MIME ∉ whitelist

    API->>CCS: assertSizeAllowed(config, sizeBytes)
    Note over CCS: INV-79-02 : rejet si taille > 100 Mo

    API->>DS: validateFingerprintConstantTime(sha256Hash, documentFingerprint)
    Note over DS: INV-79-05 : comparaison constant-time (PD-38)

    rect rgb(230, 245, 230)
        Note over API,JWS: Transaction probatoire PD-60 (advisory lock by clientRequestId)

        API->>S3: PUT objet opaque (stream chiffré)
        Note over S3: INV-79-01 : aucun fichier clair persistant

        API->>DB: INSERT vault_secure.deposits<br/>(document_category, detected_mime_type, uploaded_size_bytes)
        Note over DB: INV-79-03 : insert-only, pas d'UPDATE/DELETE

        API->>AUD: append(DEPOSIT_ACCEPTED,<br/>{category, detectedMime, sizeBytes, configVersion, decision: ACCEPTED})
        Note over AUD: INV-79-04 : audit renforcé (auditLevel=REINFORCED)

        API->>JWS: sign(depositId, fingerprint, timestampUTC)
        JWS-->>API: proofReceipt (JWS compact)
        Note over JWS: INV-79-06 : scellement < 1s (SLA 1000ms)

        API->>DB: COMMIT
    end

    API-->>C: 201 Created {depositId, proofReceipt, existenceTimestampUtc, category}

5) Flux alternatifs

5.1 Catégorie invalide

  • Condition: category non présente dans enum
  • Résultat: 400 Bad Request
  • Aucune persistance fichier ni dépôt

5.2 Catégorie inconnue en configuration

  • Condition: enum valide mais config absente/inactive
  • Résultat: 422 Unprocessable Entity
  • Audit de rejet append-only avec motif CATEGORY_CONFIG_MISSING

5.3 Format non autorisé

  • Condition: MIME détecté hors whitelist de la catégorie
  • Résultat: 415 Unsupported Media Type
  • Arrêt avant transaction de scellement
  • Aucune persistance d'upload

5.4 Taille dépassée

  • Condition: taille stream > max_size_bytes
  • Résultat: 413 Payload Too Large
  • Stream interrompu immédiatement
  • Aucune persistance d'upload

5.5 Fingerprint invalide

  • Condition: hash recalculé != documentFingerprint
  • Résultat: 422 Unprocessable Entity
  • Aucune persistance d'upload

5.6 Rejeu idempotent

  • Condition: même clientRequestId déjà scellé
  • Résultat: réponse idempotente conforme PD-60 (même preuve/identifiant logique)

6) Invariants techniques (mapping INV-79-xx)

Invariant Exigence Implémentation technique Vérification
INV-79-01 Aucun fichier en clair côté serveur Stream direct vers stockage opaque, chiffrement/opaque path, pas de disque local persistant Test d'intégration + inspection runtime
INV-79-02 Aucun upload non scellé ne persiste Validation complète avant commit final, rollback transactionnel en erreur Tests de rollback sur erreurs 4xx/5xx
INV-79-03 Aucune modification ultérieure Dépôt immuable (insert-only), absence d'API update/delete dépôt Tests contractuels API + contrôle schéma
INV-79-04 Aucun effacement silencieux Audit append-only systématique des rejets et succès Tests audit + requêtes DB append-only
INV-79-05 Preuve traçable indépendamment de la plateforme proofReceipt JWS + documentFingerprint + timestamp UTC Vérification JWS + test de relecture externe
INV-79-06 Scellement < 1 seconde SLA configuré (sealing_sla_ms=1000), métriques de latence p95/p99 Tests perf et monitoring en production

7) Critères d'acceptation (CA-79-xx)

ID Critère SMART
CA-79-01 Spécifique: Quand category est absent, l'API traite la demande avec DEFAULT. Mesurable: 100% des tests d'intégration "category absent" retournent 201 sur payload valide. Temporel: validé en CI à chaque merge.
CA-79-02 Spécifique: category=B2C_EVIDENCE_MINOR accepte uniquement les 7 MIME autorisés. Mesurable: 7 tests passants + au moins 1 test rejet par famille MIME. Temporel: exécution en pipeline de tests.
CA-79-03 Spécifique: Tout MIME hors whitelist B2C retourne 415 avec code métier dédié. Mesurable: taux de conformité 100% sur matrice de tests négatifs. Temporel: vérifié sur suite d'intégration.
CA-79-04 Spécifique: Taille > 100 Mo pour B2C retourne 413 et interrompt le stream. Mesurable: aucun objet persistant observé après test (DB+stockage). Temporel: vérifié à chaque release candidate.
CA-79-05 Spécifique: Hash serveur SHA-256 en comparaison constant-time reste obligatoire pour toute catégorie. Mesurable: tests existants PD-60 non régressés (100% pass). Temporel: CI continue.
CA-79-06 Spécifique: L'audit append-only contient documentCategory, detectedMimeType, uploadedSizeBytes, validationDecision pour B2C. Mesurable: 100% des événements B2C contrôlés en test ont ces champs. Temporel: validé en environnement de test avant mise en prod.
CA-79-07 Spécifique: Scellement B2C respecte < 1s. Mesurable: p95 <= 1000 ms sur N>=1000 uploads de référence (<=100 Mo). Temporel: campagne de perf pré-release.
CA-79-08 Spécifique: Idempotence par clientRequestId inchangée. Mesurable: 100% des tests de rejeu renvoient un résultat cohérent sans doublon DB. Temporel: exécuté en CI.

8) Codes d'erreur

ID HTTP Condition Message API (exemple)
ERR-79-001 400 Champ category invalide (enum) Invalid category value
ERR-79-002 422 Catégorie valide mais non configurée/inactive Category configuration unavailable
ERR-79-003 415 MIME détecté non autorisé pour la catégorie Unsupported media type for category
ERR-79-004 413 Taille max catégorie dépassée File too large for category
ERR-79-005 422 Fingerprint SHA-256 invalide Document fingerprint mismatch
ERR-79-006 409 Conflit idempotence non récupérable (cas limite lock) Idempotency conflict
ERR-79-007 500 Erreur interne pendant transaction probatoire Probative deposit processing failed

9) Contraintes de non-régression et implémentation

  • Ne pas créer de nouvel endpoint.
  • Ne pas modifier la logique de scellement probatoire PD-60 (transaction unique, audit append-only, JWS).
  • Introduire la catégorie comme extension déclarative et configurable.
  • Préserver les performances via traitement streaming et cache de configuration court TTL.

10) Plan de tests minimal associé à la spécification

  • Tests unitaires CategoryConfigService:
  • résolution défaut DEFAULT
  • résolution B2C_EVIDENCE_MINOR
  • rejet config absente/inactive
  • validation MIME/taille
  • Tests intégration upload:
  • 7 MIME autorisés B2C -> 201
  • MIME non autorisé -> 415
  • 100 Mo -> 413

  • hash invalide -> 422
  • category absent -> comportement DEFAULT
  • idempotence clientRequestId
  • Tests audit:
  • présence des champs renforcés en succès/rejet B2C
  • append-only strict
  • Tests performance:
  • p95 scellement <= 1s pour scénario de charge de référence

11) Décisions techniques explicites

  1. category est optionnel pour compatibilité ascendante des clients PD-60.
  2. La whitelist est validée sur MIME détecté côté serveur, pas sur extension du fichier.
  3. Les limites de taille et MIME sont pilotées par configuration en base pour permettre des évolutions sans refonte API.
  4. Le régime probatoire reste strictement identique à PD-60, seule la politique d'admission du fichier varie selon la catégorie.