Formulaires hors ligne
Extension payante. Un formulaire peut être rempli sans réseau. Les réponses restent sur l’appareil, chiffrées, et partent au retour de la connexion.
Ce document décrit ce que le module garantit et ce qu’il refuse. Le guide utilisateur en donne la version courte, dans la section « Remplir sans réseau ».
Le principe
La saisie est conservée dans le navigateur, dans IndexedDB, chiffrée. Rien n’est envoyé au serveur avant l’envoi : aucun brouillon serveur, aucun courriel, aucun suivi. Le site n’apprend l’existence de cette saisie qu’au moment où elle devient une soumission.
Le réglage vit dans les réglages du formulaire, sous offline :
{
"offline": {
"enabled": true,
"retention_days": 30
}
}
La rétention se raccourcit, jamais ne s’allonge : trente jours au plus. Un brouillon est une copie de données personnelles posée sur l’appareil de quelqu’un, et laisser régler « trois ans » depuis une case de l’interface ferait porter ce choix par celui qui ne le subit pas.
Ce que le chiffrement protège — et ce qu’il ne protège pas
C’est le point sur lequel il serait facile d’en promettre trop.
La clé est produite dans le navigateur, en AES-GCM 256, et marquée non
exportable. Aucun script ne peut en lire les octets — pas même un script
injecté : crypto.subtle refuse d’exporter une clé créée ainsi. Elle ne peut
donc pas sortir de l’appareil.
Ce qui est protégé : le contenu des brouillons au repos. Une sauvegarde du profil, un disque consulté, un coup d’œil dans l’inspecteur ne montrent que du chiffré. C’est le risque réel d’une saisie laissée plusieurs jours sur une machine partagée — un poste d’accueil, une tablette de terrain.
Ce qui ne l’est pas : un script qui s’exécute sur l’origine peut se servir de la clé, donc déchiffrer. Le chiffrement n’est pas une protection contre l’injection de script ; il empêche l’exfiltration de la clé et la lecture en clair du stockage, rien de plus.
Chaque écriture emploie un vecteur d’initialisation neuf : le réemployer avec la même clé, en AES-GCM, reviendrait à publier la différence entre deux brouillons.
Sans contexte sécurisé, rien n’est conservé
crypto.subtle n’existe pas en HTTP simple, et IndexedDB manque dans certains
navigateurs ou en navigation privée. Dans ces cas, le module n’écrit rien :
le formulaire se comporte exactement comme avant.
Le repli n’est donc pas « conserver en clair ». Une saisie en clair sur le disque de quelqu’un est pire que l’absence de la fonctionnalité, et personne n’en aurait été prévenu.
Ce qui n’est jamais conservé
Les mots de passe, les fichiers, les signatures et les paiements. Et, au-delà de
cette liste, tout ce que le cœur déclare sensible — SensitiveValues fait
autorité, de sorte qu’un type ajouté à cette liste demain cesse d’être conservé
sans qu’on ait à y penser.
- Fichier : la valeur ne désigne pas le contenu mais un fichier téléversé sur le serveur. Hors ligne, le téléversement n’a pas eu lieu.
- Signature : le tracé engage la personne ; le garder en attente reviendrait à conserver une signature détachée de ce qu’elle signe.
- Paiement : le règlement se tient avec la passerelle, en ligne.
- Mot de passe : déjà sensible pour le cœur.
L’exclusion a lieu avant l’écriture, et non après : écrire puis effacer laisserait un intervalle, si court soit-il, pendant lequel la valeur existe sur le disque. Le serveur publie les identifiants concernés dans le balisage, et le constructeur prévient l’administrateur pendant qu’il construit — plutôt que de le laisser découvrir au retour du réseau qu’une pièce jointe a disparu.
Le jeton de soumission n’est pas conservé non plus : celui d’un brouillon de la semaine dernière ne vaut plus rien, et un jeton frais est réclamé au moment de l’envoi.
Un brouillon, une soumission
C’est la garantie du module, et le navigateur ne peut pas la donner : il perd la réponse au moment précis où le réseau tombe, et ne sait alors pas distinguer « jamais parti » de « parti, réponse perdue ».
Elle tient donc côté serveur. Le navigateur tire un identifiant d’envoi au
hasard — trente-deux octets — et la table slf_offline_submissions en fait sa
clé primaire. Deux soumissions issues du même brouillon sont structurellement
impossibles.
La réservation précède la soumission. L’insertion a lieu avant d’appeler le gestionnaire, avec une entrée encore inconnue. Deux envois simultanés — deux onglets, un réseau qui revient pendant que la file part — ne peuvent pas franchir la porte ensemble : c’est l’échec d’insertion qui tranche, non une lecture suivie d’une écriture entre lesquelles tout peut arriver.
Une soumission refusée relâche sa réservation : un captcha expiré ou un champ obligatoire vide ne doit pas condamner définitivement un brouillon que le visiteur pourra corriger.
Un envoi déjà traité reçoit un succès, sans confirmation à afficher — le visiteur a vu la sienne au premier envoi. Ce que la file attend là, c’est l’autorisation d’oublier ce brouillon.
Cette table ne contient aucune réponse : un identifiant tiré au hasard, la soumission qu’il a produite, une date. Ce n’est pas un stockage serveur de brouillons, que le plan exclut : la ligne naît après l’entrée, jamais avant. Elle disparaît avec la soumission qu’elle désigne, et une tâche quotidienne efface celles qui dépassent la rétention maximale.
La route d’envoi différé
Le plan demandait d’appeler la route de soumission normale. Le module appelle le gestionnaire normal — mêmes validations, même anti-spam, même garde de paiement, mêmes événements, même entrée — par une route Pro qui n’en est qu’une porte. Rien du pipeline n’est dupliqué.
Ce que cette porte ajoute, et que la route du cœur ne peut pas donner, c’est une réponse idempotente. Si un second envoi recevait une erreur, la file le retiendrait et boucherait tout ; s’il recevait un succès sans vérification, il créerait une seconde entrée.
Obtenir cela depuis la route du cœur aurait demandé d’y introduire la notion d’identifiant d’envoi — c’est-à-dire de nommer dans le cœur une fonctionnalité payante, pour un seul consommateur.
La route exige que le mode soit actif sur le formulaire visé : sans cette vérification, elle offrirait à tout formulaire du site une seconde porte d’entrée que son auteur n’a pas ouverte. L’identifiant doit faire soixante-quatre caractères hexadécimaux — un identifiant deviné permettrait de réclamer la réponse d’un envoi qui n’est pas le sien.
L’envoi est retenu, non intercepté
Le module n’intercepte pas la soumission avant le cœur : il emploie
context.hold(), le point d’extension que le module de paiement utilise déjà
pour retenir un envoi tant que le règlement n’est pas confirmé. Court-circuiter
les écouteurs du cœur aurait demandé de reproduire ensuite tout ce qu’ils font.
Reprise et effacement
Un brouillon antérieur est proposé, jamais imposé : remplir le formulaire sans le dire ferait croire à une saisie qu’on n’a pas faite. Deux boutons — reprendre, effacer — et l’effacement est immédiat et local.
Les brouillons vides ne sont pas conservés : un formulaire ouvert puis quitté sans rien remplir remplirait sinon la liste d’entrées que personne ne reconnaît.
Ce que le module ne fait pas
Ni synchronisation entre appareils, ni stockage serveur automatique, ni gestion de conflit d’édition, ni fonctionnement sans IndexedDB. Pas d’application installable non plus : le plan la réserve à une version ultérieure, après celle-ci.
Un brouillon ne quitte pas l’appareil et le navigateur où il a été saisi. Changer de navigateur, vider les données du site ou passer en navigation privée le fait disparaître — c’est la contrepartie de ne rien déposer sur le serveur.
