Aller au contenu

PD-286 / C8 — Livrable agent app-volume-verifier

Agent : agent-developer (Claude) Module : app-volume-verifier Projet : ProbatioVault-app (React Native + Expo SDK 54 + TypeScript) Référence plan : PD-286-plan.md §1.2 C8, §3 (INV-286-07), §5 (TC-NOM-04 / TC-ERR-04) Référence contract : PD-286-code-contracts.yaml module app-volume-verifier

1. Synthèse

Implémentation du vérificateur d'intégrité de volume utilisé par l'orchestrator app dans le flux multi-volumes (INV-286-07) :

  • recompute = SHA3-256(JCS_RFC8785(manifest \ {integrityHash}))
  • comparaison stricte recomputed === expected (case-sensitive)
  • en cas de mismatch : HashMismatchException synchronique avec uniquement les 8 premiers chars de chaque hash
  • aucune mutation du manifest reçu (clone défensif JSON)
  • aucune dépendance npm canonicalize ni alternative — réutilisation de la lib RFC 8785 interne PD-54 (src/crypto/merkle-tree/canonicalizer)

2. Fichiers livrés

Fichier Lignes Rôle
src/export/volume-verifier.ts 130 API publique : verifyVolume, defaultVolumeVerifier, HashMismatchException, VolumeVerificationInput, VolumeVerificationOk, HashMismatchDetail, VolumeVerifier
src/export/__tests__/volume-verifier.test.ts 245 14 tests, couvrent TC-NOM-04 + TC-ERR-04 + invariants forbidden

Aucun fichier hors périmètre n'a été modifié.

3. Conformité au code contract

3.1 Interfaces (contract interfaces)

Contract Implémenté Localisation
VolumeVerifier Interface TS exportée + defaultVolumeVerifier (impl par défaut) volume-verifier.ts:67-69 + 131-133
HashMismatchException class HashMismatchException extends Error volume-verifier.ts:43-58

3.2 Invariants (contract invariants)

Invariant contract Mécanisme Test
recompute = SHA3-256(JCS(manifest \ {integrityHash})) ; comparaison stricte (===) verifyVolume() : clone → delete integrityHashcanonicalizeOrThrowhashBytes=== TC-NOM-04 (4 cas)
Lib JCS partagée interne uniquement Import direct ../crypto/merkle-tree/canonicalizer (PD-54, RFC 8785) — (vérifié à l'inspection imports)
Lib SHA3-256 audité Réutilisation streaming-hasher.hashBytes qui s'appuie sur @noble/hashes/sha3
Mismatch → throw avec uniquement 8 premiers chars HashMismatchException({volumeIndex, expectedPrefix, recomputedPrefix}) ; slice(0, 8) ; aucune occurrence du hash complet dans message « exposes only the 8-char prefix »
Réutilise streaming-hasher.ts hashBytes(canonicalBytes) depuis ./streaming-hasher

3.3 Forbidden (contract forbidden)

Interdiction Vérification
Comparaison case-insensitive Test « rejects an uppercased version of the correct hash » (un hash valide en majuscules est explicitement rejeté)
Continuation après mismatch throw synchronique en plein milieu de verifyVolume, aucun try/catch interne ; testé par « throws synchronously (no orphan promise) »
Log du hash complet HashMismatchException.message ne contient jamais le hash complet : assertion expect(ex.message).not.toContain(expectedHash)
Import npm canonicalize ou alternative Aucun import de canonicalize, json-canonicalize, etc. ; seule la lib interne PD-54 est utilisée
Vérification asynchrone non awaited verifyVolume est synchrone (retourne VolumeVerificationOk, pas Promise<...>)
Mutation du manifest reçu JSON.parse(JSON.stringify(manifest)) → clone profond ; testé par 2 tests (« does not mutate the manifest object passed in » + « does not mutate nested arrays/objects either »)

Note sur le clone défensif : le contrat citait structuredClone comme exemple, mais cette API n'est pas disponible dans l'environnement Jest utilisé par le projet (et incertaine sur tous les builds Hermes). Le manifest reçu est garanti JSON-sérialisable (validé par Zod en amont via multiVolumeExportApiResponseSchema), donc JSON.parse(JSON.stringify(...)) est strictement équivalent et portable. La règle métier — « pas de mutation » — est respectée et testée.

3.4 Fichiers (contract files)

Contract Livré
src/export/volume-verifier.ts
src/export/__tests__/volume-verifier.test.ts

4. Couverture des critères et tests du plan

4.1 Critères d'acceptation couverts (PD-286-tests.md)

ID Critère Mécanisme dans C8
CA-286-04 Hash de chaque volume vérifié via manifest API verifyVolume() est l'unique point de vérification, appelé volume par volume par l'orchestrator
CA-286-05 Échec d'un volume → échec export global HashMismatchException levée synchroniquement → l'orchestrator (C7) doit la propager vers state FAILED

4.2 Tests couverts

Test ID plan Implémenté ici Cas
TC-NOM-04 « returns the recomputed hash when manifest matches » + « works when manifest contains nested objects and arrays » + « supports Unicode payloads » + « is deterministic regardless of integrityHash position »
TC-ERR-04 « throws when manifest content differs » + « throws when expectedIntegrityHash is forged » + « exposes only the 8-char prefix » + « throws synchronously »

Tests additionnels qualité :

  • strict case-sensitive comparison (1 cas) — protège INV-286-07 contre régression future qui ajouterait toLowerCase()
  • defensive cloning of input manifest (2 cas) — vérifie que ni les clés du manifest ni les sous-objets ne sont mutés
  • determinism (1 cas) — appels répétés produisent le même hash
  • defaultVolumeVerifier façade (2 cas) — vérifie que la façade DI reflète exactement le comportement de verifyVolume

5. Décisions architecturales

Format architectural_decisions du code contract — max 1-2 décisions, focalisées sur des choix non triviaux.

architectural_decisions:
  - decision: |
      API synchrone (`verifyVolume(input): VolumeVerificationOk`) plutôt
      qu'asynchrone (`async`).
    rationale: |
      Le manifest est en mémoire (< 1 MB par contrat), la canonicalisation
      JCS et SHA3-256 sont CPU-bound. Une signature sync élimine par
      construction le risque d'orphan promise (forbidden) et fait apparaître
      le throw `HashMismatchException` directement dans le call stack de
      l'orchestrator, simplifiant l'enforcement de l'arrêt immédiat.
    alternatives_considered:
      - "API async + Promise.reject  rejetée : risque orphan promise + signature non alignée avec la nature CPU-bound"
      - "Callback (err, ok)  rejetée : style non idiomatique du reste du code export"
    trade_offs:
      - "+ Throw synchronique inarrêtable, alignement direct avec INV-286-07"
      - "+ Stack trace simple pour debug"
      - "- Si la canonicalisation devenait coûteuse (manifest > 1MB), bloquerait le thread JS  mitigation : assertion taille au boundary upstream (orchestrator/api-client)"

  - decision: |
      Réutilisation directe de `canonicalizeOrThrow` (PD-54,
      `src/crypto/merkle-tree/canonicalizer`) plutôt qu'un nouveau package
      `@probatiovault/jcs`.
    rationale: |
      Le contract H-PD286-PLAN-01 nomme `@probatiovault/jcs` mais aucun
      package npm interne n'existe à ce nom. Le canonicalizer PD-54 EST
      déjà la lib JCS interne du monorepo, RFC 8785-conforme, audité,
      utilisé par le moteur Merkle. La créer en doublon serait du copy-paste
      ; la promotion en package interne est une dette à traiter par une
      story dédiée (cf. §6).
    alternatives_considered:
      - "Créer `packages/jcs` cross-projet  hors scope module C8, demande coordination backend (le manifest doit utiliser strictement la même implémentation côté backend C3)"
      - "Inliner une copie locale dans `src/export/`  rejeté : duplication de code crypto critique"
    trade_offs:
      - "+ Zéro duplication, lib auditée"
      - "+ Implémentation byte-identical garantie au sein du projet app"
      - "- Couplage `src/export/`  `src/crypto/merkle-tree/` ; à factoriser le jour  un package interne sera créé"
      - "- Le typage `Event` de l'API publique force un cast `as unknown as Event` (le runtime `serializeValue` est totalement générique, le cast est inoffensif)"

6. Hypothèses confirmées et points de vigilance

6.1 Hypothèses du plan validées

  • H-PD286-PLAN-01 (JCS cross-runtime) : confirmé par tests Unicode et structures imbriquées. Le canonicalizer PD-54 produit un résultat déterministe sur le même input (test « determinism »). La validation byte-identical avec la canonicalisation backend reste à confirmer côté CI roundtrip (responsabilité agent C3 + suite intégration plus large).

6.2 Points de vigilance pour l'orchestrator (C7)

L'orchestrator app DOIT respecter les invariants suivants pour ne pas dégrader la garantie INV-286-07 :

  1. Appeler verify() AVANT pvproof-assembler.appendVolume() — pas après, pas en parallèle. C'est le point d'observation clé de TC-NOM-04 (« log volume_hash_verified{volumeIndex} AVANT volume_appended »).
  2. Catcher HashMismatchException explicitement et transitionner vers state FAILED puis abort. Ne JAMAIS continuer le traitement des volumes restants après mismatch (forbidden : continuation après mismatch).
  3. Ne JAMAIS logger l'objet HashMismatchException en raw via console.error(ex) sans veiller à n'inclure que expectedPrefix/recomputedPrefix — la classe garantit déjà que message n'expose que les préfixes, mais des sérialiseurs JSON (Sentry, Bugsnag) peuvent capturer toutes les propriétés énumérables, ce qui est acceptable ici (les propriétés exposées sont explicitement les préfixes).

6.3 Dette technique laissée en l'état

Sujet Description Action recommandée
Lib JCS partagée @probatiovault/jcs n'existe pas comme package npm interne. Le canonicalizer PD-54 fait office de lib interne mais est typé Event. Story dédiée pour extraire un package packages/jcs (cross-projet backend + app) avec API générique canonicalizeJson(value: unknown): Uint8Array. Tant que cette story n'est pas faite, le cast as unknown as Event reste nécessaire dans volume-verifier.ts:115.

7. Vérification locale

Vérification Commande Résultat
Type-check sur les 2 fichiers livrés npx tsc --noEmit 2>&1 \| grep volume-verifier 0 erreur
Tests unitaires C8 npx jest src/export/__tests__/volume-verifier.test.ts 14/14 passés (557 ms)

Note : tsc --noEmit global remonte des erreurs préexistantes dans d'autres modules (src/__tests__/AppNavigator.test.tsx, src/services/biometricKeychain.qwen.test.ts, src/crypto/aes-gcm.ts, etc.). Aucune n'est imputable à C8 — elles préexistaient à mon travail et sortent du périmètre du contract app-volume-verifier.

8. Hors périmètre (signalé, non modifié)

  • Intégration dans l'orchestrator (C7) : l'orchestrator appellera defaultVolumeVerifier.verify(...) ; non modifié ici (responsabilité de l'agent app-export-orchestrator-multi).
  • Audit côté app sur mismatch : gov_signal_* ou audit local, à brancher dans C7.
  • Création d'un package packages/jcs : décrit en §6.3 comme dette à traiter par une story dédiée.