feat(r4.3): file d'écriture, verrou optimiste D2, Synchro & conflits

Serveur : updatedAt exposé au détail OT ; baseUpdatedAt optionnel sur
transition/coche/bilan → 409 « Conflit de version » contextualisé (qui,
quand) ; toute écriture secondaire (coche, bilan, commentaire, conso,
MO) fait avancer la version — sans version fournie, le web est
inchangé. ADR-003 : sécurité & protocole de routage mobile (qui vit où
sur l'appareil, purge complète à la déconnexion — correctif réel : la
file et le cache persisté survivaient au logout).

Mobile : file persistée AsyncStorage rejouée dans l'ordre — succès →
propagation de la version fraîche aux saisies restantes du même OT
(nos écritures ne se conflictent pas entre elles, un écart étranger
reste détecté) ; coupure → tout attend ; refus → CONFLIT, la file
s'arrête, l'humain tranche (voir l'OT / rejouer sur version à jour /
abandonner). Transitions, coches, bilan et photos (D5, compressées
~1600 px) passent par la file avec patch optimiste du cache ; écran
Synchro (badge tabbar ambre/rouge) ; préchargement parc + référentiels
(le bilan hors-ligne a ses vocabulaires).

Recette « mode avion » 13/13 en Expo web piloté : gestes hors-ligne →
3 en file → modification concurrente de Salma → conflit tranché →
serveur Terminé avec bilan. 17 tests jest-expo, 74 tests API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-17 09:50:02 +01:00
parent a22ea60f83
commit c8b3c1769a
26 changed files with 1218 additions and 84 deletions

View File

@@ -0,0 +1,53 @@
# ADR-003 — Sécurité et protocole de routage de l'app mobile (R4)
- **Statut** : acceptée (R4.3, 17 juillet 2026)
- **Contexte** : l'app technicien est offline-first (D1) — elle garde des données métier
et des saisies en attente **sur l'appareil**. Il faut dire précisément ce qui vit où,
ce qui le protège, et comment on navigue (deep links compris). Demande du référent
pendant la construction de R4.3.
## Sécurité — ce qui vit sur le téléphone, et ce qui le protège
| Donnée | Où | Protection | Purge |
|---|---|---|---|
| Jeton JWT | **SecureStore** (trousseau iOS / Keystore Android) | chiffré par l'OS, jamais dans AsyncStorage | à la déconnexion |
| Cache de lecture (OT, parc, référentiels) | AsyncStorage (`siop.cache`, persister TanStack, 7 j max) | sandbox applicative de l'OS | à la déconnexion (loi 09-08 — minimisation) |
| File d'écriture (D1) | AsyncStorage (`siop.file.v1`) | sandbox applicative | à la déconnexion — **jamais** un compte ne rejoue les saisies d'un autre |
| Photos en attente (D5) | fichier compressé (~1 600 px) référencé par la file | sandbox applicative | sortent de la file à l'envoi |
Règles :
1. **L'API reste la seule autorité** : guard JWT global fermé par défaut, matrice de
permissions, invariant « voir autre », garde de clôture, verrou optimiste (D2 —
`baseUpdatedAt`, 409 contextualisé). Le mobile n'a AUCUNE logique de permission à lui :
ce qu'il affiche vient d'une réponse déjà filtrée ; ce qu'il écrit est revalidé serveur.
2. Sur **web** (vérifications de dev seulement), SecureStore n'existe pas : repli
AsyncStorage assumé. CORS est **opt-in** (`CORS_ORIGINS`, vide par défaut) — les apps
natives n'envoient pas d'Origin, le web de prod reste derrière son proxy même-origine.
3. **Ni géolocalisation ni audio en R4** (décision D5, loi 09-08) ; la caméra ne sert
qu'à lire les étiquettes du parc (scan local, D4) et aux photos d'intervention.
4. La déconnexion purge **tout** ce qui précède (`useLogout`) ; la file survit au
redémarrage mais pas au changement de session.
## Protocole de routage (expo-router)
- **Arborescence** : `/connexion` (publique) ; `/` = aiguillage de session ;
`(tabs)/journee|scanner|preventif|synchro` (coquille tabbar) ; fiches empilées
`/ascenseur/[id]`, `/ot/[id]`, `/ot/[id]/cloture`, `/ot/[id]/grille`.
- **Garde de session** : `/` redirige vers `/connexion` sans session, vers
`(tabs)/journee` sinon. Les écrans profonds ne « fuient » rien sans jeton :
leurs requêtes reçoivent 401 et l'écran reste vide — la donnée est protégée par
l'API, pas par l'obscurité du routage.
- **Deep links** : scheme **`siop://`** (app.json). Le scan QR n'ouvre PAS l'URL du
portail : il en **extrait la référence** (`analyseScan`) et résout localement
(D4) — un QR étranger est refusé avec un message, jamais suivi aveuglément.
- **Hors-ligne** : la navigation reste entière (cache + file) ; seul le contenu
jamais synchronisé affiche son état honnêtement.
## Conséquences
- Recette sécurité mobile : changer de compte démo sur le même appareil → file vide,
cache vide, aucune trace de la session précédente.
- À durcir plus tard si exigence client : chiffrement applicatif de la file
(`expo-secure-store` ne convient pas aux volumes ; envisager SQLCipher/MMKV chiffré),
et épinglage TLS.