Aller au contenu

PD-298 — Plan d'implémentation

Projet cible : ProbatioVault-app (React Native + Expo SDK 54 + TypeScript) Backend consommé : ProbatioVault-backend via endpoints PD-287 (DONE) Dépendances natives validées PO : react-native-safe-area-context, @react-native-community/datetimepicker Traçabilité : spec PD-298-specification.md v2026-04-22, tests PD-298-tests.md, review step3 PD-298-review-step3.md.

1. Découpage en composants

La story est décomposée en 10 modules ; la liste correspond 1→1 aux entrées du fichier PD-298-code-contracts.yaml (un module = un agent step 6b).

# Module Rôle Racine fichiers
C01 sharing-types Types TypeScript, branded types (ShareId, ProofId, UserId), énums ShareState/EventType, payloads API, schémas Zod partagés. src/sharing/types/
C02 sharing-validation Validation locale : email (regex D-287-03 via hypothèse HT-01), TTL [15..43200], UUID v4, bornes numériques, normalisation email contractualisée (lowercase + trim, pas de dot-removal). src/sharing/validation/
C03 sharing-masking Fonctions déterministes : maskIp(raw) v4 x.x.*.* et v6 (4 premiers hextets + *:*:*:*), scrubEmail(value) pour télémétrie. src/sharing/masking/
C04 sharing-drm-prefs Lecture/écriture de drmWarningSeen(userId) en AsyncStorage, clé @pv/sharing/drm-seen/<userId>, purge au logout via hook onLogout existant. src/sharing/drm-prefs/
C05 sharing-api-client Client HTTP typé pour les 4 endpoints PD-287 : POST /shares, GET /shares?offset&limit&state, POST /shares/:id/revoke, GET /shares/:id/events. Validation Zod à la réponse. Timeout 30 s. Idempotency-key UUID v4 sur POST (HT-07). src/sharing/api/
C06 sharing-hooks Hooks React : useCreateShare, useShareList, useProofShares, useShareDetail, useShareEvents, useRevokeShare, useOwnership, useDrmWarning. Cache-busting pour forcer l'appel réseau à chaque mount (no-cache INV-298-05). src/sharing/hooks/
C07 sharing-components Composants UI : ShareCreateForm, TtlPicker (presets + custom @react-native-community/datetimepicker), StateBadge, RgpdNotice, DrmWarningModal, RevokeConfirmModal (ARB-7), ShareListItem, EventListItem, EmptyJournalMessage. Tous 100 % i18n. src/sharing/components/
C08 sharing-screens 5 points d'entrée écran : ShareCreateScreen, ProofShareListSection (embarquée dans l'écran preuve existant), MyShareScreen (infinite scroll), ShareDetailScreen, ShareEventsScreen. src/sharing/screens/
C09 sharing-guards Cross-module : guard de propriété UI sur fiche preuve (CTA caché si proof.owner_user_id !== currentUser.id), guard de navigation React Navigation (route share/:id validée UUID v4 avant mount), purge state au 401. src/sharing/guards/ + override dans src/screens/proofs/ProofDetailScreen.tsx
C10 sharing-telemetry Journalisation structurée sans PII : logShareEvent({shareId, action, …}), interdiction d'émettre recipientEmail ; wrappers Sentry beforeSend pour scrub PII des breadcrumbs réseau/rendu (S-01, S-02). src/sharing/telemetry/

1.1 i18n (transverse)

Toutes les chaînes sont introduites dans le bundle unique src/i18n/fr/sharing.json. Aucune chaîne literal n'est autorisée dans C07/C08/C09. Les clés sont typées via le générateur i18next existant.

1.2 Définitions de placeholders contractuels

Les textes normatifs absents (écarts bloquants review step 3 : A-01, A-04, A-05) sont injectés via clés i18n dédiées à valeurs placeholder ; la bascule vers le texte officiel se fait par simple update du bundle FR sans recompilation logique :

Clé i18n Défaut placeholder (MVP dev/test) À fournir par
sharing.arb7.revoke.title « À définir — PD-287 ARB-7 » Product/Legal (HT-03)
sharing.arb7.revoke.body idem idem
sharing.arb8.drm.title « À définir — PD-287 ARB-8 » Product/Legal (HT-03)
sharing.arb8.drm.body idem idem
sharing.rgpd.inline_notice Template RGPD structuré (finalité/base/rétention/destinataires/droits) avec {{retention_days}} Legal (HT-02)
sharing.validation.email.pattern_version "fallback-rfc5322" si D-287-03 non fourni ; sinon "d-287-03" Backend PD-287 (HT-01)

2. Flux techniques

F1 — Création d'un partage (F-298-01)

  1. ProofDetailScreen passe proof.id, proof.owner_user_id au bouton « Partager ».
  2. useOwnership(proofId) compare à auth.user.id ; si false, le CTA n'est jamais monté (INV-298-12).
  3. Navigation vers ShareCreateScreen. useDrmWarning(userId) :
  4. lit drmWarningSeen en AsyncStorage ;
  5. si false, monte DrmWarningModal (contenu ARB-8) ; à l'acquittement, setDrmWarningSeen(true) ;
  6. si true, rend le formulaire directement.
  7. Formulaire ShareCreateForm :
  8. email (validé via validateEmail) ;
  9. ttlMinutes via TtlPicker (presets {15,60,1440,10080,43200} + custom datetimepicker, validation [15..43200]) ;
  10. maxViews entier optionnel ≥1 ;
  11. notificationsEnabled booléen ;
  12. RgpdNotice monté au-dessus du CTA submit, visibilité vérifiée avant soumission par mesure measureInWindow > 0 (INV-298-06, ERR-298-10).
  13. À la soumission :
  14. validation locale ; si échec, aucun appel (INV-298-01, ERR-298-01, ERR-298-08).
  15. génération idempotencyKey = uuidv4() ;
  16. sharesApi.create({proofId, recipientEmail, ttlMinutes, maxViews?, notificationsEnabled}, {idempotencyKey}) ;
  17. bouton submit disabled durant in-flight (anti double-tap, H-03) ;
  18. validation Zod de la réponse 201 ; mapping vers ShareSummary ; invalidation queryClient.invalidateQueries(['proof-shares', proofId]).
  19. Retour fiche preuve + toast succès + refresh liste locale via useProofShares.

F2 — Liste par preuve (F-298-02)

ProofShareListSection monte useProofShares(proofId) : - appelle GET /shares?proofId=... si endpoint PD-287 le supporte (HT-07) ; sinon filtre côté client à partir de GET /shares?offset=0&limit=20 paginé jusqu'à couverture (à défaut backend, documenté en §9). - pull-to-refresh re-exécute la query avec cacheTime: 0, staleTime: 0 → appel réseau frais garanti (INV-298-05).

F3 — Écran global « Mes partages » (F-298-03)

MyShareScreen : - Utilise useInfiniteQuery de @tanstack/react-query avec limit=20 fixe, getNextPageParam = pages.length * 20. - Tri serveur created_at desc (contract API) ; tri additionnel côté client si nécessaire pour stabilité. - Filtres d'état exposés via segmented control → passe state en query param si supporté, sinon filtre post-réponse et refetch. - cacheTime: 0 pour toutes les queries ['shares', ...] (INV-298-05).

F4 — Détail et révocation (F-298-04)

ShareDetailScreen(shareId) : 1. Validation UUID v4 au mount (rejet navigation si invalide, TC-NEG-01). 2. useShareDetail(shareId)GET /shares/:id. À chaque focus écran, refetch forcé. 3. Matrice StateActions: | État | Révoquer | Voir journal | Partager à nouveau | Détails | |------|----------|-------------|--------------------|---------| | PENDING_ACTIVATION | ✅ | ✅ | ❌ | ✅ | | ACTIVE | ✅ | ✅ | ❌ | ✅ | | OTP_BLOCKED | ✅ | ✅ | ❌ | ✅ + message contextuel | | REVOKED | ❌ | ✅ | ❌ | ✅ lecture seule | | EXPIRED | ❌ | ✅ | ❌ | ✅ lecture seule | 4. Sur « Révoquer » : - monte RevokeConfirmModal avec sharing.arb7.revoke.body (i18n, HT-03) ; - confirmation explicite (bouton distinct du bouton de fermeture) ; - avant POST, re-fetch état courant (anti-race H-07) ; si état devenu terminal, abort + toast ; - sinon sharesApi.revoke(shareId, {idempotencyKey}).

F5 — Journal d'accès (F-298-05)

ShareEventsScreen(shareId) : 1. GET /shares/:id/events sans cache. 2. Mapping eventType backend → label i18n via EVENT_TYPE_LABELS (HT-04). Valeur inconnue → UNKNOWN_EVENT (tests TC-NEG-06). 3. Chaque ligne : eventAt (format ISO local via Intl.DateTimeFormat), eventType i18n, recipientEmail, maskIp(eventIpRaw), deviceType. 4. EmptyJournalMessage si events.length === 0 : le message distingue « jamais activé » / « pas d'événement récent » via l'état courant du share (PENDING/ACTIVE sans event, OTP_BLOCKED, REVOKED/EXPIRED sans event). 3 clés i18n dédiées (A-10).

2bis. Diagramme de dépendances agents

graph LR
    subgraph "Wave 1 — Fondations (parallèle)"
        A01[agent-types<br/>C01 sharing-types]
        A02[agent-i18n<br/>i18n bundle FR]
        A03[agent-masking<br/>C03 sharing-masking]
        A04[agent-drm<br/>C04 sharing-drm-prefs]
        A05[agent-validation<br/>C02 sharing-validation]
    end

    subgraph "Wave 2 — Infra métier (parallèle)"
        A06[agent-api<br/>C05 sharing-api-client]
        A07[agent-telemetry<br/>C10 sharing-telemetry]
    end

    subgraph "Wave 3 — Composition (parallèle)"
        A08[agent-hooks<br/>C06 sharing-hooks]
        A09[agent-components<br/>C07 sharing-components]
        A10[agent-guards<br/>C09 sharing-guards]
    end

    subgraph "Wave 4 — Intégration"
        A11[agent-screens<br/>C08 sharing-screens]
    end

    subgraph "Wave 5 — Qualité"
        A12[agent-tests<br/>tests UT + integration]
    end

    A01 --> A06
    A01 --> A05
    A01 --> A08
    A05 --> A08
    A03 --> A09
    A02 --> A09
    A04 --> A08
    A06 --> A08
    A06 --> A07
    A08 --> A11
    A09 --> A11
    A10 --> A11
    A11 --> A12
    A07 --> A11

Parallélisation : Wave 1 (5 agents en parallèle), Wave 2 (2 agents en parallèle après Wave 1), Wave 3 (3 agents en parallèle après Wave 2), Wave 4 (1 agent), Wave 5 (1 agent). Temps de chemin critique : 5 waves séquentielles.

2ter. Diagramme de séquence enrichi (révocation, manquant dans la spec — C-07)

sequenceDiagram
    participant U as Propriétaire
    participant SD as ShareDetailScreen
    participant H as useRevokeShare
    participant M as RevokeConfirmModal
    participant API as sharing-api-client
    participant BE as Backend PD-287

    U->>SD: tap "Révoquer"
    SD->>M: monter modale + i18n(arb7)
    U->>M: lire ARB-7, confirmer explicitement
    M->>H: onConfirm()
    H->>API: getShare(shareId) (re-check anti-race)
    API->>BE: GET /shares/:id
    BE-->>API: {state}
    alt état terminal (REVOKED/EXPIRED)
        H->>SD: toast "État déjà modifié"
        SD->>SD: refetch forcé
    else état actif
        H->>API: revoke(shareId, idempotencyKey)
        API->>BE: POST /shares/:id/revoke
        BE-->>API: 204 | 409
        alt succès
            API-->>H: ok
            H->>SD: refetch + toast succès
        else 409
            H->>SD: toast "État déjà modifié"
            SD->>SD: refetch forcé
        end
    end

3. Mapping invariants → mécanismes

Invariant Exigence Mécanisme Composant Observable Risque
INV-298-01 Email valide avant tout appel. validateEmail() synchrone avant sharesApi.create ; bouton submit disabled si invalide ; aucune requête émise si !valid. C02, C07, C05 Snapshot réseau : 0 POST /shares avec email invalide. Divergence regex D-287-03 (HT-01).
INV-298-02 ARB-7 exact avant revoke. RevokeConfirmModal affiche t('sharing.arb7.revoke.body') figé dans le bundle FR ; onConfirm est le seul trigger revoke. Snapshot test de rendu. C07, C06 Snapshot string === i18n key ; 0 POST sans confirmation. Texte ARB-7 non fourni (HT-03, placeholder).
INV-298-03 DRM warning 1ʳᵉ ouverture puis mémorisé. useDrmWarning(userId) lit AsyncStorage ; si absent → rend modal et await ack ; puis setItem(true). C04, C07 Spy AsyncStorage + spy render modal. Multi-device (S-05, documenté §9).
INV-298-04 TTL ∈ [15..43200], défaut 10080. TtlPicker presets figés + slider custom clampé ; validateTtl(v) bloque submit. C02, C07 Tests bornes (15, 43200, 14, 43201).
INV-298-05 Pas de cache persistant. React Query : cacheTime: 0, staleTime: 0, gcTime: 0 sur clés ['shares', *], ['share-events', *] ; aucun persistQueryClient sur ces clés ; désactivation select/memoization agressive. C06 Spy réseau : n ouvertures écran → n appels. Collision avec drmWarningSeen (C-01) : scope différent (préférence UX).
INV-298-06 Encart RGPD visible avant submit. RgpdNotice rendu avant SubmitButton ; onLayout + measureInWindow enregistrent la visibilité ; submit bloqué tant que isVisible non true au moins une fois. C07 Spy measureInWindow retourne y + h dans le viewport. Texte RGPD absent (HT-02, placeholder).
INV-298-07 Emails non loggés. C10 : logger transport ignore champs recipientEmail, eventRecipientEmail ; Sentry beforeSend scrub email dans request.data et breadcrumbs ; console.log banni via ESLint no-console. C10 Test scan logs + Sentry mock : 0 email brut. Analytics tierces non scrubées (S-01, §9).
INV-298-08 Badge = état backend strict. Enum ShareState = union littérale 5 valeurs ; Zod z.enum rejette autre ; StateBadge = map exhaustif ; état inconnu → erreur bloquante + toast + actions disabled. C01, C07 Test Zod parse error ; test rendu erreur. Valeur inattendue backend (TC-NEG-05).
INV-298-09 IP partiellement masquée. maskIp(raw) : IPv4 regex → o1.o2.*.* ; IPv6 parse via ipaddr.js (canonical form) → 4 premiers hextets + *:*:*:* (HT-02). Fallback → t('sharing.events.ip_masked_unavailable'). C03, C07 Tests unitaires pour v4, v6 canonical, v6 zero-compressed, input invalide. Règle IPv6 non canonique (HT-03).
INV-298-10 100 % i18n. ESLint règle react/no-literal-string activée sur src/sharing/** ; audit i18n-unused en CI. C07, C08 Linter vert sur PR ; snapshot no-literal. Littéraux techniques (HT-09).
INV-298-11 Actions selon état. Table ALLOWED_ACTIONS: Record<ShareState, readonly ShareAction[]> exhaustive, exposée via canPerform(state, action). Composants testent if (!canPerform(...)) return null. C06, C07 Tests matrice 5 états × 5 actions (25 cas). Divergence backend (HT-07).
INV-298-12 CTA partage owner-only. useOwnership(proofId)proof.owner_user_id === auth.user.id ; sinon null rendu. Deep-link share/create/:proofId idem (C09). C09, C08 Tests render : composant null si non-owner. Navigation programmatique (S-11).
INV-298-13 Pagination offset/limit=20. Constante SHARE_PAGE_SIZE = 20 ; useInfiniteQuery pages.length * 20 comme offset. C05, C06 Tests getNextPageParam ; snapshot request URL. Backend cursor-based (HT-06).
INV-298-14 Transitions hors §5.5 interdites. Mapping ALLOWED_ACTIONS n'expose que les transitions UI-initiables (revoke uniquement) ; pour les transitions serveur, Zod valide la nouvelle réponse ; transition détectée client inconnue → log + erreur + refetch. C06, C10 Test : transition non listée dans response → erreur. Mal attribuée UI (C-03, cf. §9).
INV-298-15 REVOKED/EXPIRED terminaux. ALLOWED_ACTIONS : pour ces deux états, actions: ['view_events'] uniquement. Aucune action modifiante rendue. C06, C07 Tests matrice (INV-298-11).
INV-298-16 Pas d'appels exploratoires. C05 ne prend en entrée que des ShareId validés UUID v4 ; refuse de concaténer un input utilisateur brut. Pas d'iteration/probing d'IDs. C05 Test fuzz : IDs non-UUID → 0 appel. Critère subjectif (A-09).
INV-298-17 Pas de mode offline. useNetInfoOnline() ; si !online → toutes les actions sharing renvoient OfflineError immédiat ; pas d'enqueue local. Écrans concernés affichent banner hors-ligne. C06, C08 Tests avec NetInfo mock. UX 3G lente (H-06, §9).

4. Mapping critères d'acceptation → mécanismes

Critère Mécanisme(s) Composant Observable Risque
CA-298-01 F1 complet + StateBadge sur PENDING_ACTIVATION. C05, C06, C07, C08 HAR : POST /shares 201 + badge rendu.
CA-298-02 Validation locale email (INV-298-01). C02, C07 0 appel réseau si email invalide. HT-01.
CA-298-03 validateTtl (INV-298-04). C02, C07 Submit disabled hors bornes.
CA-298-04 RevokeConfirmModal + trigger onConfirm uniquement (INV-298-02). C07, C06 Modal rendu + 1 appel post-confirm. HT-03.
CA-298-05 useInfiniteQuery + tri serveur (INV-298-13). C05, C06, C08 Requêtes offset=0,20,40…. HT-06.
CA-298-06 stateFilter param + refetch. C06, C08 Appel ?state= observable. HT-08.
CA-298-07 cacheTime=0 sur clés partage (INV-298-05). C06 n affichages → n requêtes.
CA-298-08 ShareEventsScreen + EventListItem. C07, C08 Colonnes présentes. HT-04.
CA-298-09 maskIp (INV-298-09). C03, C07 Regex rendu côté UI. HT-03.
CA-298-10 useDrmWarning first-open (INV-298-03). C04 Spy AsyncStorage + render. S-05.
CA-298-11 RgpdNotice + visibilité (INV-298-06). C07 measureInWindow > 0. HT-02.
CA-298-12 useOwnership (INV-298-12). C09 CTA null.
CA-298-13 ESLint no-literal-string + i18n-unused. C07, C08 CI verte. A-10.
CA-298-14 C10 scrubbing. C10 Logs scrubés. S-01.
CA-298-15 ALLOWED_ACTIONS (INV-298-11). C06, C07 Snapshot matrice.
CA-287-27 Alias de CA-298-11 (RGPD). C07 idem CA-298-11. HT-02.

5. Mapping tests (TC-*) → mécanismes + observables

Test Référence spec Mécanisme(s) Observation Niveau
TC-NOM-01 INV-298-04, INV-298-08, CA-298-01 F1 complet, StateBadge, default TTL=10080. Mock HTTP + render tree. Integration
TC-NOM-02 INV-298-01, CA-298-02 validateEmail + submit disabled. Spy fetch = 0. Unit
TC-NOM-03 INV-298-03, CA-298-10 useDrmWarning + AsyncStorage spy. Spy getItem/setItem. Integration
TC-NOM-04 INV-298-06, CA-287-27, CA-298-11 RgpdNotice visibility. measureInWindow mock. Integration
TC-NOM-05 INV-298-13, CA-298-05 useInfiniteQuery fixture 45 items. Requêtes ordonnées offset. Integration
TC-NOM-06 INV-298-08, CA-298-06 stateFilter + refetch. URL capture. Integration
TC-NOM-07 INV-298-12, CA-298-12 useOwnership false → null. Render tree. Unit
TC-NOM-08 INV-298-11, CA-298-15 ALLOWED_ACTIONS pour 5 états. Snapshot. Unit
TC-NOM-09 INV-298-02, CA-298-04 Modal ARB-7 + i18n key sharing.arb7.revoke.body. Render assert + call count. Integration
TC-NOM-10 ERR-298-02 Modal dismiss sans confirm. Spy API = 0. Integration
TC-NOM-11 CA-298-08, INV-298-09 EventListItem rendu + maskIp. Colonnes + regex. Integration
TC-NOM-12 INV-298-09, CA-298-09 maskIp v4 + v6. Tests unitaires. Unit
TC-NOM-13 INV-298-05, CA-298-07 cacheTime=0. Requête fresh à chaque mount. Integration
TC-NOM-14 INV-298-10, CA-298-13 ESLint + i18n-unused. CI job. Lint
TC-NOM-15 INV-298-07, CA-298-14 C10 scrubbing. Spy transport logger. Unit
TC-NOM-16 INV-298-14, INV-298-15, CA-298-15 Zod rejet + ALLOWED_ACTIONS vide. Snapshot. Unit
TC-NOM-17 INV-298-17 useNetInfoOnline false. Render banner + actions disabled. Integration
TC-ERR-01..12 ERR-298-01..12 Voir §6. Tests dédiés par code erreur. Unit/Integration
TC-NR-01..08 Non-régression Snapshots + ESLint + tests matrices. CI verte. Unit/Integration
TC-NEG-01 INV-298-16 UUID v4 guard C05. 0 appel pour IDs invalides. Unit
TC-NEG-02 INV-298-13 SHARE_PAGE_SIZE = 20 constante. URL capture. Unit
TC-NEG-03 INV-298-01 validateEmail. Idem TC-NOM-02. Unit
TC-NEG-04 INV-298-15 ALLOWED_ACTIONS terminaux. Snapshot. Unit
TC-NEG-05 INV-298-08 Zod z.enum sur shareState. Parse error. Unit
TC-NEG-06 HT-04 (eventType inconnu) Mapping avec fallback UNKNOWN_EVENT. Label rendu. Unit
TC-NEG-07 INV-298-09 maskIp fallback. Message i18n. Unit
TC-NEG-08 INV-298-16 C05 ne concatène pas d'input. Audit code. Static

Couverture : 17 TC-NOM, 12 TC-ERR, 8 TC-NR, 8 TC-NEG = 45 tests. Aucune exclusion.

6. Gestion des erreurs

Code Condition Mécanisme Observable
ERR-298-01 Email invalide. validateEmail bloque submit. Bouton disabled, 0 appel.
ERR-298-02 Révocation non confirmée. Modal onConfirm seul déclencheur. 0 appel revoke.
ERR-298-03 État UI obsolète. Refetch forcé à chaque focus + invalidation post-mutation. Appel réseau observable.
ERR-298-04 Email loggé (bug). C10 + CI lint + Sentry scrub. 0 occurrence dans logs.
ERR-298-05 Warning DRM non affiché. Submit bloqué si drmWarningSeen && !ackInSession (premier mount). Bouton disabled.
ERR-298-06 Journal vide. EmptyJournalMessage contextuel (3 variantes i18n, cf. A-10). Texte rendu non vide.
ERR-298-07 Preuve non possédée. Guard C09 + backend 403 mappé → message générique t('sharing.errors.generic_403') (pas de fuite S-08). CTA absent, 403 → toast.
ERR-298-08 TTL hors bornes. validateTtl. Submit disabled.
ERR-298-09 OTP_BLOCKED. ShareDetailScreen affiche explication + CTA « Révoquer » visible. Render conditionnel.
ERR-298-10 Encart RGPD absent avant submit. Visibility gate (INV-298-06). Submit disabled.
ERR-298-11 401 session expirée. Axios interceptor existant → redirect flux auth ; C10 purge toutes les queries ['shares', *] + in-memory state (S-04). Navigation auth + queryClient.clear partiel.
ERR-298-12 Timeout/offline. useNetInfoOnline + axios timeout 30 s → NetworkError + toast i18n. Toast rendu, 0 faux succès.

7. Impacts sécurité

ID Risque Mitigation
SEC-01 PII email dans Sentry/Datadog (S-01). beforeSend scrub recipientEmail, eventRecipientEmail ; beforeBreadcrumb masque request.data.recipient_email ; test d'intégration mock Sentry.
SEC-02 Artefacts de test contiennent emails/IPs brutes (S-02). Fixture test utilise pool d'emails factices déterministes ; HAR post-traité par script de scrubbing CI avant archivage ; captures Detox générées sur comptes de test dédiés.
SEC-03 Enumeration anti-pattern côté backend (S-03). Rappel explicite dans §9 : INV-298-16 est complémentaire à une protection backend PD-287 ; C05 se contente de ne pas itérer.
SEC-04 Résidus en mémoire au logout (S-04). Hook onLogout (existant) branché sur : queryClient.removeQueries(['shares']), queryClient.removeQueries(['share-events']), C04 purgeAllDrmFlags(), queryClient.removeQueries(['proof-shares']).
SEC-05 DRM warning multi-device faible (S-05). Documenté §9 comme limitation MVP ; option produit future : déplacer drmWarningSeen côté backend (user-level flag).
SEC-06 Deep-link de création non gardé (S-11). Route share/create/:proofId valide UUID v4 + appelle useOwnership au mount ; si non-owner, redirect ProofDetailScreen + toast générique.
SEC-07 Pas de rate-limit / anti-spam (S-07). Submit disabled + debounce 2 s au premier tap ; idempotency-key UUID envoyée au backend pour dedup serveur (H-03).
SEC-08 403 backend sans fuite (S-08). Mapping HTTP → i18n générique sharing.errors.generic_403 ; message backend jamais affiché.
SEC-09 Clipboard/sharesheet fuite email (S-06). ShareListItem email rendu en Text selectable={false} ; pas de Share.share() exposant l'email.

8. Hypothèses techniques

ID Hypothèse Source review Impact si faux
HT-01 Regex email D-287-03 est fournie par PD-287 avant step 6b ; sinon fallback RFC 5322 simplifié intégré en C02, pattern_version tracée dans le contrat C05. A-01, PC-298-01 Divergence backend/UI silencieuse. Test d'intégration contre backend dev OBLIGATOIRE à Gate 8.
HT-02 Contenu normatif RGPD (finalité, base légale, rétention chiffrée, droits) est fourni par Product/Legal et inséré dans sharing.rgpd.inline_notice ; défaut placeholder explicite jusque-là. A-04, PC-298-06 Non-conformité RGPD. Escalade PO avant Gate 8.
HT-03 Textes ARB-7 et ARB-8 sont fournis par PD-287/Legal et insérés dans le bundle i18n. A-05, NT-03 INV-298-02 / INV-298-03 non vérifiables contractuellement. Escalade PO avant Gate 8.
HT-04 Mapping eventType backend→UI est aligné sur {CREATION, ACTIVATION, CONSULTATION, EXPORT, ECHEC_OTP, REVOCATION} ; sinon UNKNOWN_EVENT. A-07, PC-298-03 Libellés UNKNOWN_EVENT en production.
HT-05 Borne max maxViews n'est pas validée côté client ; l'erreur backend (400) est rendue telle quelle. A-06, PC-298-02 Validation partielle, UX dégradée.
HT-06 GET /shares expose offset/limit (pas cursor). À vérifier via Swagger PD-287 avant step 6b. H-01, H-02 Réécriture de la liste globale, glissement de périmètre.
HT-07 Matrice transitions backend compatible §5.5 ; POST /shares/:id/revoke renvoie 204/409 documenté. H-02, PC-298-04 Divergence machine d'états ; refetch + message État déjà modifié.
HT-08 GET /shares?state= accepté par le backend ; sinon filtre client-side après pagination complète. CA-298-06 Pagination moins efficace, documenté §9.
HT-09 ESLint react/no-literal-string toléré sur testID, clés techniques et noms d'icônes ; une allowlist est documentée dans eslint-plugin-i18n.config.js. NT-01 Faux positifs lint vs i18n scope.
HT-10 @react-native-community/datetimepicker supporte iOS et Android pour le choix TTL custom, avec fallback modal react-native-modal-datetime-picker si nécessaire. PO 2026-04-22 UX incohérente entre OS.
HT-11 ipaddr.js (librairie npm) fournit la normalisation IPv6 canonique nécessaire à maskIp. A-02 Règle de masquage IPv6 non déterministe.

8.1 Plan de levée des hypothèses avant Gate 8

Hypothèse Action Deadline
HT-01 Récupérer la regex D-287-03 du code backend (ProbatioVault-backend/src/sharing/dto/*.ts). Wave 1 fin.
HT-02, HT-03 Escalade PO via gov_ask_po ; sans réponse, step 6b démarre sur placeholders mais Gate 8 NON_CONFORME. Avant Wave 1.
HT-06, HT-07, HT-08 Lire Swagger PD-287 + tests d'intégration contre backend dev. Wave 2 fin.

9. Points de vigilance

ID Vigilance Source Mitigation
V-01 INV-298-05 (no-cache) crée friction sur 3G/edge (H-06). Review H-06 Skeleton + spinner ≤ 1 s ; timeout réseau 30 s ; UX documentée.
V-02 drmWarningSeen par device → garantie faible (S-05). Review S-05 Option future : flag côté user backend. MVP : documenté.
V-03 INV-298-14/15 sont mal attribués à l'UI (C-03) : la FSM est serveur. Review C-03 UI ne fait que refléter l'état. Les tests TC-NOM-16 vérifient que l'UI rejette toute réponse à état inattendu (via Zod). Pas de simulation FSM complète.
V-04 Self-loops state → state : INTERDITE (diagramme) absents de la matrice §5.5. Review C-06 UI ne traite pas comme erreur (un refetch peut renvoyer le même état). Documenté dans ALLOWED_ACTIONS : pas d'auto-transition considérée.
V-05 Journal → captures d'écran possibles (fuite PII destinataire). Review S-09 Pas de mitigation stricte en MVP ; documenté dans la policy utilisateur.
V-06 TC-NOM-13 non-déterministe en CI (mutation multi-device). Review NT-07 Remplacé par mock HTTP qui change shareState entre 2 appels GET successifs.
V-07 ESLint no-literal-string peut introduire des faux positifs. NT-01, HT-09 Allowlist testID, identifiants techniques documentée.
V-08 Clock skew UI vs backend sur expiresAt (H-05). Review H-05 Afficher « Expire le » plutôt que compte à rebours côté UI.
V-09 Pagination cursor-based non supportée par plan actuel (HT-06). Review H-01 Si backend est cursor-based, adaptation isolée à C05 + C06 ; ne remet pas en cause INV-298-13 si la sémantique (tri desc + page stable) est préservée.
V-10 Test fuzz UUID (TC-NEG-01) doit couvrir lowercase/uppercase. Spec §5.1 (case-insensitive) Regex UUID insensible à la casse avec /i.

10. Hors périmètre

  • Portail destinataire, deep-linking destinataire (spec §2).
  • Push notifications (spec §2) — seul notificationsEnabled est transmis au backend.
  • Mode hors-ligne / queue offline (INV-298-17).
  • Modification des endpoints backend PD-287.
  • RGAA complet (MVP sur i18n + a11y minimale).
  • Qualification juridique RGPD (H-298-08).
  • Partage multi-preuves.

Mécanismes cross-module (OBLIGATOIRE)

La story modifie 1 module externe : ProofDetailScreen (module preuves).

Élément Valeur
Routes de l'autre module à protéger Écran ProofDetailScreen (route interne, non exposée backend) — section CTA « Partager ». Route additionnelle share/create/:proofId (nouveau, C08).
Controller et méthode src/screens/proofs/ProofDetailScreen.tsx — ajout d'un bloc conditionnel rendant <ShareCta proof={proof} /> (composant de C09).
Effet du guard ShareCta retourne null si proof.owner_user_id !== auth.user.id. La route share/create/:proofId redirige + toast si non-owner.
Mécanisme de jointure cross-schéma N/A (UI only). Mapping logique : proof.owner_user_id (déjà exposé par l'API preuves) comparé à auth.user.id (store auth existant).
Scope d'enregistrement Local : le guard est un composant React monté par ProofDetailScreen uniquement. Pas de HOC global, pas de middleware navigation global. La route share/create/:proofId est déclarée dans src/navigation/SharingStack.tsx uniquement.
Exceptions d'accès Aucune exception de rôle (admin, support, etc.) spécifiée dans le besoin. Le CTA est strictement owner-only.

Périmètre de test (OBLIGATOIRE)

Niveau In scope Hors scope (justification)
Unitaire C01 (types + Zod), C02 (validateEmail, validateTtl, validateUuid, normalizeEmail), C03 (maskIp v4/v6/fallback), C04 (DRM prefs AsyncStorage), C05 (wrappers API + Zod response), C06 (ALLOWED_ACTIONS, useOwnership, pagination, cache-busting), C07 (composants UI en isolation), C09 (guards), C10 (scrubbing logger). Couverture ≥ 80 % Istanbul.
Intégration Flux F1 complet (F-298-01), F2 (F-298-02), F3 (F-298-03), F4 (F-298-04), F5 (F-298-05) sur MSW (Mock Service Worker) + @testing-library/react-native. React Query + AsyncStorage + NetInfo mockés.
E2E Flux utilisateur principal (création → liste → révocation) via Detox sur builds iOS/Android Hors scope MVP : infrastructure Detox n'est pas configurée dans ProbatioVault-app à date (pas de job CI Detox). Ticket de suivi : à créer après Gate 8 pour story dédiée « E2E sharing ». Les flux sont couverts en intégration + manuel QA.
Lint / statique ESLint no-literal-string sur src/sharing/**, i18n-unused, tsc --noEmit incrémental post-agent.
Tests de contrat Mock responses validées par Zod ; fixtures alignées sur Swagger PD-287 (à vérifier Wave 2, HT-06/07).

Stubs inter-PD : aucun. Toutes les dépendances externes (PD-287 backend) sont DONE et consommées via API. Les placeholders textuels (HT-02, HT-03) ne sont pas des stubs de code mais des valeurs de bundle i18n à mettre à jour ; STUB: PD-287-arb7-content n'est donc pas un stub inter-PD mais une dépendance éditoriale tracée dans §8.

Couverture minimale attendue : 80 % lignes / 80 % branches sur src/sharing/** + src/screens/proofs/ProofDetailScreen.tsx (bloc sharing uniquement). Les placeholders et les UNKNOWN_* fallback sont couverts.

Code contracts similaires (réutilisation décisions)

{{SIMILAR_CONTRACTS}}