Sessions auditables
Une session est l'unité atomique de validation : elle commence par une authentification (OTP SMS ou TOTP), enregistre N actions signées, et se clôture avec un récap cryptographique.
Machine à états
POST /v1/sessions
│
▼
┌─────────────┐ OTP × 3 invalides
│ PENDING_OTP │ ───────────────────────┐
└─────────────┘ │
│ POST /validate │
▼ ▼
┌─────────────┐ POST /refuse ┌────────────┐
│ OPEN │ ───────────────▶ │ ABANDONED │
└─────────────┘ └────────────┘
│ │
│ POST /actions × N │ entrée REFUSED
│ POST /artifacts (option) │ co-signée dans
▼ │ la chaîne
┌─────────────┐ │
│ CLOSED │ │
└─────────────┘ │
│ │
│ POST /v1/proofs │
▼ ▼
bundle.asice bundle.asice
(verdict MATCH/PARTIAL/...) (avec entry REFUSED)
ABANDONED est un terminal alternatif à CLOSED. Une session ABANDONED a quand même un bundle de preuve générable : il prouve que l'utilisateur a explicitement décliné ou que l'OTP a échoué 3 fois. C'est une preuve opposable autant qu'un MATCH.
Modes : sync vs async (magic-link)
L'organisation pilote intégralement le flow : il appelle open, transmet l'OTP à son utilisateur, appelle validate + actions + close séquentiellement.
Pour les cas où l'utilisateur valide sur un device séparé : tu passes callback_url + consent_content à l'open, le service renvoie un magic_link_url. Quand l'utilisateur valide via /consent/{token}, un webhook HMAC-signé est posté sur ton callback_url.
Types d'événements (EventType)
Chaque entrée de la chaîne d'audit porte un type d'event : il dit quelle nature d'acte l'entrée enregistre. C'est le champ event_type que tu passes à POST /v1/sessions/{id}/actions, validé contre la liste fermée ci-dessous — un type inconnu est rejeté.
À quoi ça sert : classer et filtrer les entrées (timeline, bundle lisible), piloter les règles métier (certains types exigent une session OPEN, cf. colonne ci-dessous), et donner à un tiers une lecture immédiate de ce qui s'est passé, acte par acte.
| Type | Requiert session OPEN ? | Quand |
|---|---|---|
| LOGIN | — | Émis automatiquement après validate OTP réussi. |
| VALIDATION_ACTION | ✓ | L'utilisateur valide un document, un consentement, une décision. |
| SEAL_REQUEST | ✓ | Demande de scellement envoyée à un prestataire externe (Yousign, DocuSign). |
| SEAL_COMPLETED | — | Le prestataire confirme que le sceau est apposé. |
| ARTIFACT_ATTACHED | ✓ | Lien d'un PDF scellé externe à la chaîne (artifact_hash = payload_hash de l'entrée précédente). |
| DOCUMENT_READ | — | Trace de lecture d'un document, optionnellement liée à une session. |
| EMAIL_SENT | — | Envoi d'email opérationnel à journaliser. |
| SESSION_PLAN_COMMITTED | ✓ | S41 : scellement du manifest prévisionnel à validate OTP. |
| SESSION_PLAN_RESOLVED | ✓ | S41 : verdict comparatif émis à la clôture. |
| SIGNING_CONTEXT_ATTESTED | ✓ | S49 : mandat OWN/DELEGATED scellé à validate OTP. |
| REFUSED | ✓ | S55 : refus explicite (user_explicit) OU auto-déclenché sur otp_max_attempts. payload_hash = sha256(session_id|REFUSED|reason). Session passe à ABANDONED. |
Opération, type d'event, event webhook — ne pas confondre
Trois notions portent le mot « type ». Elles vivent à des moments différents de la vie d'une session :
| Notion | C'est quoi | À quoi ça sert |
|---|---|---|
| Opération prévu |
Une ligne du manifest prévisionnel : {op_id, type, payload_hash}. Une intention déclarée à l'ouverture, scellée à l'OTP. |
Délimiter le périmètre attendu et obtenir un verdict (MATCH/PARTIAL/DIVERGENT) à la clôture. |
| Type d'event réel |
Le event_type (EventType) d'une entrée effectivement écrite dans la chaîne — la liste fermée ci-dessus. |
Classer et qualifier chaque acte immuable du journal d'audit. |
| Event webhook notification |
Le message session.* / anchor.* que ProofIt pousse à ton serveur quand un état change. |
Réagir en temps réel côté SI, sans polling. |
Comment ils se relient : pour réaliser une opération prévue, tu enregistres une action avec son event_type (réel) et metadata.op_id qui pointe l'opération. Le type de l'opération annonce l'event_type attendu ; à la clôture, op_id + payload_hash sont confrontés au plan pour calculer le verdict. L'action écrite déclenche alors l'event webhook session.action_recorded.
Refuser explicitement (S55)
L'organisation ou l'utilisateur peut refuser une session à tout moment avant son CLOSED. Cas d'usage :
- Bouton "Refuser" sur la page magic-link
/consent/{token} - Annulation côté SI organisation après vérification fraude
- Auto-déclenchement après 3 OTP invalides (reason=
otp_max_attempts)
POST /v1/sessions/{id}/refuse
Authorization: Bearer eyJ...
X-Organisation-Id: 01900000-...
Content-Type: application/json
{ "reason": "user_explicit" } // libre, ex: organisation_revoked, fraud_detected…
Idempotent : repasser refuse sur une session déjà ABANDONED → 200 sans changement. Une session CLOSED ne peut pas être ABANDONED (422 SESSION_CLOSED).
Champs émis à l'ouverture
session_id(UUIDv7, ordre temporel garanti)session_open_hash= sha256(session_id ‖ organisation_id ‖ actor_user_id ‖ opened_at ‖ channel) — affiché dans le mail témoin + bénéficiairemagic_link_url(si mode async)planned_root_hash+planned_operation_count(si manifest fourni)
Champs émis à la clôture
session_root_hash= hash agrégé des entrées de la session — affiché dans le mail récap + dans le bundleexecuted_root_hash+planned_verdict(si manifest)- Mail récap DKIM-signé envoyé au
beneficiary_email(chiffré at-rest libsodium secretbox)