discovery-media-player 0.1.113 → 0.1.115
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.
- package/context/standalone.js +7 -0
- package/docs/RETENTION.md +1 -1
- package/package.json +3 -2
- package/server/handler.js +14 -0
- package/server/presentations.js +26 -2
- package/server/routes-direct.js +20 -5
package/context/standalone.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1.115",
|
|
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",
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
"typecheck": "node -e \"require('fs').existsSync('tsconfig.json')||(console.error('Ce script ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && tsc --noEmit",
|
|
67
67
|
"prepublishOnly": "npm run build && npm test",
|
|
68
68
|
"test:e2e": "node -e \"require('fs').existsSync('vitest.e2e.config.mjs')||(console.error('Ce banc ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && vitest run --config vitest.e2e.config.mjs",
|
|
69
|
-
"test:base": "node -e \"require('fs').existsSync('vitest.base.config.mjs')||(console.error('Ce banc ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && vitest run --config vitest.base.config.mjs"
|
|
69
|
+
"test:base": "node -e \"require('fs').existsSync('vitest.base.config.mjs')||(console.error('Ce banc ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && vitest run --config vitest.base.config.mjs",
|
|
70
|
+
"test:charge": "node -e \"require('fs').existsSync('vitest.charge.config.mjs')||(console.error('Ce banc ne vit pas dans le paquet publié - le champ scripts annonce plus que le tarball ne contient. Clonez le depot puis npm ci : https://github.com/Juli1artha/discovery-media-player'),process.exit(1))\" && vitest run --config vitest.charge.config.mjs"
|
|
70
71
|
},
|
|
71
72
|
"engines": {
|
|
72
73
|
"node": ">=22"
|
package/server/handler.js
CHANGED
|
@@ -610,6 +610,20 @@ async function handler(req, res) {
|
|
|
610
610
|
return typeof signer === "function" && !!signer("carte-sonde", "carte-sonde", 60);
|
|
611
611
|
} catch { return false; }
|
|
612
612
|
})(),
|
|
613
|
+
// ⚠️ LA PORTE REFUSE-T-ELLE, OU EST-ELLE SEULEMENT CONFIGURÉE POUR REFUSER ? `presenceStrict`
|
|
614
|
+
// au-dessus dit que la porte refuse les battements sans preuve — vrai, et mesuré (il exige de
|
|
615
|
+
// pouvoir émettre). Mais le contrôle anti-usurpation des BOOTSTRAPS, lui, peut être désarmé
|
|
616
|
+
// sans que rien ne le dise : 0018 absente, ou — avant 0.1.111 — un simple hoquet réseau. On
|
|
617
|
+
// avait fermé cet écart sur `presenceJetons` en mesurant plutôt qu'en déclarant, et laissé le
|
|
618
|
+
// champ voisin ouvert. (Nommé par le second hôte.)
|
|
619
|
+
//
|
|
620
|
+
// ⚠️ TROIS ÉTATS. « inconnu » n'est pas « actif » : un processus qui n'a pas encore servi de
|
|
621
|
+
// bootstrap n'a rien constaté, et annoncer une garde active sur une absence d'observation est
|
|
622
|
+
// exactement ce qu'on refuse ailleurs. On ne peut pas sonder la fonction sans l'appeler — elle
|
|
623
|
+
// écrit — donc on rend ce qui a été CONSTATÉ à l'usage, jamais une déduction.
|
|
624
|
+
presenceDurcissement: (() => {
|
|
625
|
+
try { return require("./presentations").etatDurcissementBootstrap(); } catch { return "inconnu"; }
|
|
626
|
+
})(),
|
|
613
627
|
// ⚠️ LES JETONS DE PRÉSENCE SONT-ILS RÉELLEMENT ÉMIS ? Sans ce booléen, un exploitant qui vient
|
|
614
628
|
// de poser `PLAYER_PRESENCE_SECRET` n'a AUCUN moyen de vérifier que son réglage a pris : la
|
|
615
629
|
// carte affiche `presence: {0,0}` aussi bien quand le secret est actif que quand la variable est
|
package/server/presentations.js
CHANGED
|
@@ -6,7 +6,25 @@ const crypto = require("crypto");
|
|
|
6
6
|
// ⚠️ Le contexte est REÇU, pas construit. Ce module ne doit pas savoir d'où il vient : c'est ce
|
|
7
7
|
// qui lui permettra de partir dans le dépôt du player sans emporter le studio avec lui.
|
|
8
8
|
let PLAYER = null;
|
|
9
|
-
function init(ctx) { PLAYER = ctx; _bumpSansDurcissementJusqua = 0; _avertRpcPresence = false; }
|
|
9
|
+
function init(ctx) { PLAYER = ctx; _bumpSansDurcissementJusqua = 0; _avertRpcPresence = false; _bumpDurcissementTente = false; }
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* L'état OBSERVÉ du durcissement des bootstraps — pas sa configuration.
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ LA CARTE RAPPORTAIT UNE CONFIGURATION, PAS UN COMPORTEMENT, et c'est le second hôte qui l'a
|
|
15
|
+
* nommé : `presenceStrict` pouvait dire « la porte refuse » alors que le contrôle anti-usurpation
|
|
16
|
+
* était désarmé — 0018 absente, ou (avant 0.1.111) un simple hoquet réseau. On a fermé cet écart sur
|
|
17
|
+
* `presenceJetons` en MESURANT plutôt qu'en déclarant, et on l'avait laissé ouvert sur le champ voisin.
|
|
18
|
+
*
|
|
19
|
+
* On ne peut pas sonder la fonction sans l'appeler — elle écrit. Ce qu'on peut rendre, c'est ce que le
|
|
20
|
+
* processus a CONSTATÉ en s'en servant : « degrade » = la dernière tentative a trouvé la signature
|
|
21
|
+
* absente ; « actif » = une tentative a abouti ; « inconnu » = on n'a pas encore essayé, et c'est un
|
|
22
|
+
* état à part entière, pas un feu vert.
|
|
23
|
+
*/
|
|
24
|
+
function etatDurcissementBootstrap() {
|
|
25
|
+
if (Date.now() < _bumpSansDurcissementJusqua) return "degrade";
|
|
26
|
+
return _bumpDurcissementTente ? "actif" : "inconnu";
|
|
27
|
+
}
|
|
10
28
|
|
|
11
29
|
|
|
12
30
|
const enc = encodeURIComponent;
|
|
@@ -811,12 +829,18 @@ function signatureAbsente(erreur) {
|
|
|
811
829
|
// silence. Un « oui » (la signature existe) n'a pas besoin d'expirer : une fonction ne disparaît pas.
|
|
812
830
|
let _bumpSansDurcissementJusqua = 0;
|
|
813
831
|
const MEMO_SANS_DURCISSEMENT_MS = 60 * 1000;
|
|
832
|
+
// ⚠️ A-T-ON SEULEMENT ESSAYÉ ? « Pas dégradé » n'est pas « vérifié » : un processus qui vient de
|
|
833
|
+
// démarrer n'a rien tenté, et rendre `true` reviendrait à annoncer une garde active sur la foi d'une
|
|
834
|
+
// absence d'observation. Trois états, donc — la même règle que le verdict du schéma, où « rien de
|
|
835
|
+
// manquant » se lisait « tout va bien » tant qu'on ne distinguait pas « rien demandé ».
|
|
836
|
+
let _bumpDurcissementTente = false;
|
|
814
837
|
async function appelerBump(corps, durcissementVoulu) {
|
|
815
838
|
const appel = (b) => PLAYER.db.request("rpc/player_attendance_bump", { method: "POST", body: b });
|
|
816
839
|
if (durcissementVoulu && Date.now() < _bumpSansDurcissementJusqua) {
|
|
817
840
|
const { p_only_if_unclaimed: _retire, ...sansDurcissement } = corps;
|
|
818
841
|
return appel(sansDurcissement);
|
|
819
842
|
}
|
|
843
|
+
if (durcissementVoulu) _bumpDurcissementTente = true;
|
|
820
844
|
try {
|
|
821
845
|
return await appel(corps);
|
|
822
846
|
} catch (erreur) {
|
|
@@ -1183,4 +1207,4 @@ async function listPresentationsForDoc(docId, email, isAdmin, autoriseLarge) {
|
|
|
1183
1207
|
module.exports = {
|
|
1184
1208
|
reacteurDepuisJeton,
|
|
1185
1209
|
purgerPerimees,
|
|
1186
|
-
messagePublic, CHAMPS_PUBLICS, cheminPieceJointe, init, createPresentation, getPresentation, setPage, endPresentation, addMessage, listMessages, toggleReaction, editMessage, deleteMessage, setChatLock, createUploadUrl, reclaimPresentation, touchPresentation, listActivePresentations, handoverPresentation, endPresentationByOwner, recordAttendance, presentationStats, listPresentationsForDoc, switchPresentationDoc, setPresentationContent , STALE_MS};
|
|
1210
|
+
messagePublic, CHAMPS_PUBLICS, etatDurcissementBootstrap, cheminPieceJointe, init, createPresentation, getPresentation, setPage, endPresentation, addMessage, listMessages, toggleReaction, editMessage, deleteMessage, setChatLock, createUploadUrl, reclaimPresentation, touchPresentation, listActivePresentations, handoverPresentation, endPresentationByOwner, recordAttendance, presentationStats, listPresentationsForDoc, switchPresentationDoc, setPresentationContent , STALE_MS};
|
package/server/routes-direct.js
CHANGED
|
@@ -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
|