@zevra/legal
v1.3.0
Published
Les documents légaux de Zevra — CGVU, mentions légales, confidentialité, DPA. Source unique, versionnée et archivée.
Readme
@zevra/legal
Les documents légaux de Zevra — CGVU, mentions légales, politique de confidentialité, DPA — en un seul exemplaire, daté et archivé.
Ce dépôt existe pour une raison simple : le même texte s'affichait dans la vitrine, dans Mémoire, dans Campus et dans le tunnel d'inscription, et rien ne garantissait que c'était le même. Il est désormais écrit une fois.
Ce que le paquet garantit — et ce qu'il ne garantit pas
Il garantit qu'un texte existe en un seul endroit, qu'il est daté, et qu'une version publiée ne change jamais (voir « L'archive est immuable »).
Il ne garantit pas, à lui seul, que toutes les applications affichent la même
chose : une application épinglée à @zevra/[email protected] continuera d'afficher
le texte de la 1.0.0. Le paquet est la source, pas le facteur. La
propagation, c'est la section suivante, et c'est un choix par usage.
Faire qu'une mise à jour se répercute partout
Trois modes, du plus sûr au plus souple. Une application peut les mélanger.
1. Le lien — à préférer partout où c'est possible
Un pied de page, une mention au bas d'un formulaire, un e-mail : on lie
vers la page canonique (document.canonique, par exemple
https://zevra.tech/cgvu) au lieu de reproduire le texte.
C'est le seul mode où la dérive est structurellement impossible : il n'y a qu'un texte, à une adresse. Zéro build, zéro version, zéro entretien. Tout ce qui n'a pas une bonne raison d'embarquer le texte doit lier.
2. La lecture à l'exécution — quand le texte doit être DANS l'app
Le tunnel d'inscription, une page légale in-app : l'utilisateur ne doit pas quitter le flux. L'application lit alors le JSON servi à une adresse stable :
// Next : revalidation toutes les heures, propagation sans reconstruire
const doc = await fetch('https://zevra.tech/legal/cgvu.json', {
next: { revalidate: 3600 },
}).then((r) => r.json())Ces fichiers sont produits par pnpm bundle (dossier bundle/) et servis
par le site. Une mise à jour se propage à la revalidation suivante, sans
reconstruire aucune application — c'est ce mode qui répond à « je veux que ça
se répercute partout ».
⚠️ Prévoir le cas où la lecture échoue : le repli est le texte embarqué via le paquet (mode 3), jamais une page vide.
3. La construction — quand il faut du déterminisme
import { enVigueur } from '@zevra/legal'
const cgvu = enVigueur('cgvu')Le texte est figé au build. C'est le mode du repli et celui des usages où la reproductibilité prime (génération d'un PDF contractuel, e-mail transactionnel). Il exige une reconstruction pour se mettre à jour : c'est son défaut, et c'est aussi sa qualité.
Détecter une application en retard
bundle/index.json pèse quelques centaines d'octets et dit ce qui est en
vigueur. Une application, ou un contrôle en intégration continue, peut le
comparer à ce qu'elle embarque :
import { enVigueur } from '@zevra/legal'
const manifeste = await fetch('https://zevra.tech/legal/index.json').then((r) => r.json())
const distant = manifeste.documents.find((d) => d.id === 'cgvu')
if (distant.version !== enVigueur('cgvu').version) {
// cette app sert un texte périmé — à signaler, pas à corriger en silence
}Le rendu React — @zevra/legal/react
Le paquet principal est des données : il dit ce qu'un texte est, pas comment il se peint. Ce sous-chemin (depuis la 1.3.0) est le rendu commun, extrait de lexform, pour qu'une app n'ait plus à réécrire le sien — et qu'un correctif d'affichage se fasse une fois pour toutes les apps.
Il exporte deux composants et la mémoire du consentement :
| Export | Quoi | Où |
|---|---|---|
| <PageLegale id="cgvu" /> | la page d'un document, dans sa version en vigueur | composant serveur, sans état |
| <BandeauCookies onDecision={…} /> | le bandeau de consentement | composant client |
| lireDecision() / enregistrerDecision() | la décision mémorisée ('all' | 'no' | null) | sans React |
Prérequis : react >= 19 et la feuille de @zevra/ui chargée par l'app —
les composants n'emploient que ses tokens (--ink, --paper,
--accent…), aucune couleur en dur, rayon 0.
Adopter dans une app Next — quatre routes et un bandeau
// src/app/cgvu/page.tsx — même chose pour mentions-legales, confidentialite, dpa
import type { Metadata } from 'next'
import { enVigueur } from '@zevra/legal'
import { PageLegale } from '@zevra/legal/react'
export const metadata: Metadata = { title: enVigueur('cgvu').titre }
export default function Page() {
return (
<main className="zvl">
<PageLegale id="cgvu" marque={{ nom: 'Campus', href: '/' }} />
</main>
)
}// src/app/layout.tsx
import { BandeauCookies } from '@zevra/legal/react'
export default function RootLayout({ children }) {
return (
<html lang="fr">
<body>
{children}
{/* Sans tracker : l'app relaie la décision à son outil de mesure,
au chargement (décision mémorisée) comme au clic. */}
<BandeauCookies lien="/confidentialite" />
</body>
</html>
)
}Une app qui remplace son propre bandeau garde ses clés de mémorisation, sinon chaque visiteur revoit la question une fois :
<BandeauCookies cles={{ stockage: 'lf-cookie-consent', cookie: 'lf-consent' }} onDecision={appliquer} />Options de <PageLegale> : document={…} pour une version précise (une
archive, ou le JSON lu à l'exécution — mode 2 ci-dessus), langue="en" pour
une traduction (servie avec l'avertissement ; si elle manque, le français
est rendu sans prétendre autre chose), le="2026-03-01" pour lire le texte
applicable à une date, textes et styles={false} pour l'habillage.
L'identifiant qui compte
version — par exemple cgvu-2026-02-22 — et jamais le numéro npm.
Quand un utilisateur accepte un document, c'est cet identifiant qu'on enregistre, avec la date d'acceptation prise sur l'horloge du serveur :
import { referenceAcceptation } from '@zevra/legal'
await db.acceptations.insert({
utilisateur: id,
...referenceAcceptation('cgvu'), // { document, version, applicableAu, canonique }
accepteLe: new Date(),
})Enregistrer « CGV acceptées : oui » ne prouve rien — il faut savoir lesquelles. Le jour où il faut le prouver :
import { version } from '@zevra/legal'
const texteAccepte = version('cgvu-2026-02-22')L'archive est immuable
Une version publiée ne se modifie pas : des gens l'ont acceptée. Corriger une clause — même une virgule — c'est une nouvelle version datée, et l'ancienne reste.
Deux gardes le tiennent, parce qu'un mot changé dans un contrat de 3 000 mots ne se voit pas à la relecture d'un diff :
scripts/build-empreintes.mjsrefuse d'écrire si l'empreinte SHA-256 d'une archive existante a changé, et le dit ;tests/documents.test.jsvérifie que chaque archive porte encore son empreinte, et qu'aucune n'échappe au registre.
Publier une mise à jour
- Créer
documents/<id>/<date-d-entrée-en-vigueur>.json— copier la version précédente et l'amender. Ne jamais toucher l'ancien fichier. pnpm test— les gardes passent, l'ancienne archive est intacte.npm version minor --no-git-tag-versionpuis pousser : la CI publie.- Reconstruire le site, qui sert la page canonique et le
bundle/.
La date du nom de fichier est la date d'entrée en vigueur, pas celle de
la rédaction : un texte daté du futur n'est pas encore en vigueur
(enVigueur l'ignore), ce qui permet d'annoncer une mise à jour avant
qu'elle ne s'applique — comme l'article « Modifications » de la CGVU le
prévoit.
Reprise initiale
Les quatre documents ont été importés depuis les pages Astro de
zevra-website par scripts/importer-depuis-site.py, verbatim : le
script ne corrige ni l'orthographe, ni la ponctuation, et il échoue si le
texte produit ne contient pas exactement les mots de la page — ni un de
moins, ni un de plus.
À partir de maintenant, ce dépôt est la source : le site devrait lire le paquet plutôt que porter le texte en dur. Tant que ce n'est pas fait, les deux existent en parallèle et peuvent diverger — c'est la première chose à régler.
Les accents (rectification du 27/08/2026)
Les quatre documents avaient été publiés sans accents (« Conditions
generales », « donnees personnelles »). C'est corrigé : les versions
*-2026-08-27 portent nature: 'rectification' et rectifieDe, et les
versions du 22/02 restent servies telles quelles — quelqu'un les a acceptées
sans accents, c'est ce texte-là qui l'engage.
nature: 'rectification' dit qu'aucune clause n'a bougé : une application
peut s'en servir pour ne pas redemander l'acceptation. Redemander à
chaque virgule use le consentement et finit par le vider de son sens.
La restitution est faite par scripts/restituer-accents.py, qui échoue
si, en retirant les accents du texte produit, on ne retrouve pas l'ancien au
caractère près : aucun mot ne peut être ajouté, retiré ni altéré en chemin.
Les formes dont l'accent dépend du contexte ont été relevées une par une —
les 154 « a » (préposition partout sauf trois : a commencé, a reconnu,
a recours), les quatre « dès », et « encadre / autorise / informe », tantôt
verbe tantôt participe.
Ce que la rectification n'a PAS touché
Quatre défauts du texte source subsistent : ce ne sont pas des accents, et les corriger changerait des mots — donc le fond, donc une décision d'édition, pas une rectification typographique.
| Où | Écrit | Devrait être |
|---|---|---|
| CGVU, art. 11 | definitivamente (espagnol) | définitivement |
| DPA, ×3 | liceiete | licéité |
| CGVU, en-tête | une SAS immatricule | immatriculée (accord) |
| 5 occurrences | met en oeuvre | met en œuvre (ligature) |
À trancher : les trois premiers sont des fautes, le quatrième une finesse
typographique. Une fois décidés, ils donneront une nouvelle version — de
nature: 'modification' pour les fautes de mot, puisque le texte change.
