discovery-media-player 0.1.113 → 0.1.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -685,6 +685,13 @@ function createStandaloneContext(env = process.env) {
685
685
  // Une fois les bundles déployés des deux côtés ET `presence.sansJeton` retombé à zéro : un
686
686
  // battement sans jeton de présence valide cesse d'être enregistré. Off par défaut (transition).
687
687
  presenceStrict: String(env.PLAYER_PRESENCE_STRICT || "") === "1",
688
+ // ⚠️ SEL DU HACHAGE D'IP. Un SHA-256 d'adresse IPv4 n'anonymise rien : l'espace entier se
689
+ // recalcule en quelques minutes, et la CNIL considère une IP hachée comme une donnée
690
+ // PSEUDONYMISÉE — toujours personnelle. Avec un sel secret, elle cesse d'être recalculable par
691
+ // qui obtient la base. Dédié si l'hôte en pose un ; à défaut on réutilise le secret de présence
692
+ // avec une SÉPARATION DE DOMAINE (préfixe distinct) — il est déjà requis par la fonctionnalité
693
+ // qui produit ce hachage, ce qui évite une variable obligatoire de plus.
694
+ ipHashSecret: String(env.PLAYER_IP_HASH_SECRET || env.PLAYER_PRESENCE_SECRET || ""),
688
695
  },
689
696
  };
690
697
  }
package/docs/RETENTION.md CHANGED
@@ -92,7 +92,7 @@ Une présentation **inactive** (terminée ou abandonnée) est une archive : **pu
92
92
  | `doc_presentation_messages.client_key` | clé d'idempotence d'envoi | idem |
93
93
  | `doc_presentation_attendees.name` / `doc_presentation_attendees.email` / `doc_presentation_attendees.avatar` | identité du participant | purgées avec la présentation |
94
94
  | `doc_presentation_attendees.attendee_key` | identifiant de présence | idem |
95
- | `doc_presentation_attendees.creator_ip_hash` | **empreinte tronquée** de l'IP qui a créé la ligne (jamais l'IP en clair) — sert au plafond de création anonyme (migration 0015) | purgée avec la présentation |
95
+ | `doc_presentation_attendees.creator_ip_hash` | **empreinte tronquée** de l'IP qui a créé la ligne (jamais l'IP en clair) — sert au plafond de création anonyme (migration 0015). ⚠️ **Donnée PSEUDONYMISÉE, pas anonyme** : une IP hachée reste une donnée personnelle au sens du RGPD, et un SHA-256 non salé se recalcule intégralement sur l'espace IPv4. Depuis 0.1.114 l'empreinte est un HMAC salé par `PLAYER_IP_HASH_SECRET` (à défaut `PLAYER_PRESENCE_SECRET`, avec séparation de domaine) et liée au `slug`, ce qui empêche de corréler une même adresse d'une présentation à l'autre. Sans sel configuré, l'ancienne empreinte non salée subsiste. | purgée avec la présentation |
96
96
  | `doc_presentation_attendees.last_token_at` / `doc_presentation_attendees.last_no_token_at` | horodatage du dernier battement avec / sans jeton de présence — sert au compteur de transition (migration 0017), aucune donnée d'identité | purgés avec la présentation |
97
97
  | `doc_presentation_attendees.pages` | pages vues par le participant | idem |
98
98
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.113",
3
+ "version": "0.1.114",
4
4
  "description": "Self-hosted document viewer: per-recipient tracked links, reading analytics, live presentation. The core knows nothing about the application hosting it — everything it borrows arrives through an injected context.",
5
5
  "keywords": [
6
6
  "pdf-viewer",
@@ -94,11 +94,6 @@ async function traiter(req, res, body, _slug) {
94
94
  // On dérive donc la clé de ce qui est prouvé, et jamais de ce qui est affirmé. Un anonyme,
95
95
  // lui, ne peut rien prouver : sa clé reste la sienne, mais enfermée dans un espace de noms
96
96
  // dont elle ne peut pas sortir — elle ne pourra jamais ressembler à l'e-mail d'un membre.
97
- // ⚠️ L'IP NE PART PAS EN CLAIR VERS LA BASE : le plafond de création anonyme compte par
98
- // (slug, empreinte d'IP), et une empreinte tronquée suffit à mesurer la pression d'un même
99
- // appelant sans conserver de donnée personnelle. Le plafond lui-même est configurable par
100
- // l'hôte, défaut ATTENDEES_PER_EGRESS × 1,3 (325) — la même cible que le quota de battement.
101
- const ipHash = require("crypto").createHash("sha256").update("att:" + ip).digest("hex").slice(0, 32);
102
97
  const anonCap = (PLAYER.config && Number(PLAYER.config.presenceAnonCap) > 0)
103
98
  ? Math.trunc(Number(PLAYER.config.presenceAnonCap))
104
99
  : Math.ceil(ATTENDEES_PER_EGRESS * 1.3);
@@ -112,6 +107,26 @@ async function traiter(req, res, body, _slug) {
112
107
  ? PLAYER.identity.verifyPresenceToken(String(body.pt || "")) : null;
113
108
  const jetonValide = !!(jetonEntrant && jetonEntrant.slug === slug && jetonEntrant.key);
114
109
  const cleAnon = jetonValide ? String(jetonEntrant.key) : cleAnonyme(body.key);
110
+ // ⚠️ L'IP NE PART PAS EN CLAIR VERS LA BASE — mais NE PRÉTENDONS PAS QUE C'EST ANONYME.
111
+ //
112
+ // Ce commentaire disait « sans conserver de donnée personnelle », et c'était FAUX : un
113
+ // SHA-256 d'adresse IPv4 se recalcule intégralement (quatre milliards de valeurs, quelques
114
+ // minutes), donc l'empreinte se remonte à l'adresse par qui obtient la base. La CNIL est
115
+ // explicite : une IP hachée reste une donnée PSEUDONYMISÉE, soumise au RGPD. La description
116
+ // était plus forte que l'implémentation — la classe même qu'on traque ailleurs.
117
+ //
118
+ // Avec un SEL SECRET, la table cesse d'être recalculable sans lui. Le `slug` entre dans
119
+ // l'empreinte pour qu'on ne puisse pas CORRÉLER la présence d'une même adresse d'une
120
+ // présentation à l'autre — le plafond, lui, compte déjà par (slug, empreinte), donc rien ne
121
+ // change pour lui. Le préfixe de domaine sépare cet usage de toute autre signature faite
122
+ // avec le même secret. Sans sel (hôte qui n'en pose aucun), on retombe sur l'ancienne
123
+ // empreinte : la fonction marche, la protection est moindre, et c'est écrit ici plutôt que
124
+ // découvert plus tard. Le plafond reste configurable, défaut ATTENDEES_PER_EGRESS × 1,3.
125
+ const sel = String((PLAYER.config && PLAYER.config.ipHashSecret) || "");
126
+ const ipHash = sel
127
+ ? require("crypto").createHmac("sha256", sel).update("attendance-ip\0" + slug + "\0" + ip).digest("hex").slice(0, 32)
128
+ : require("crypto").createHash("sha256").update("att:" + ip).digest("hex").slice(0, 32);
129
+
115
130
  // ⚠️ ON SIGNE AVANT D'ÉCRIRE, ET C'EST CE QUI FERMAIT UNE BOUCLE. Le jeton était fabriqué
116
131
  // APRÈS l'écriture : on déclarait donc un bootstrap « moderne » sans savoir si un jeton
117
132
  // pourrait seulement sortir. Sans `PLAYER_PRESENCE_SECRET`, la ligne était marquée réclamée