discovery-media-player 0.1.26 → 0.1.28
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 +14 -0
- package/package.json +1 -1
- package/server/handler.js +69 -18
- package/server/presentations.js +1 -24
package/context/standalone.js
CHANGED
|
@@ -457,6 +457,20 @@ function createStandaloneContext(env = process.env) {
|
|
|
457
457
|
supabasePublishableKey: env.SUPABASE_PUBLISHABLE_KEY || "",
|
|
458
458
|
mapsKey: env.GOOGLE_MAPS_API_KEY || "",
|
|
459
459
|
extraFrameAncestors: String(env.DOC_FRAME_ANCESTORS || "").split(/\s+/).filter(Boolean),
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Clé de `localStorage` sous laquelle VOTRE application range la session de ses membres.
|
|
463
|
+
*
|
|
464
|
+
* ⚠️ Vide par défaut, donc la détection des membres est inactive : aucun membre reconnu,
|
|
465
|
+
* aucune usurpation possible. Ce paquet portait `3dd-supabase-auth` en dur — la clé d'un seul
|
|
466
|
+
* hôte, qui ne trouvait rien chez tous les autres, et qui depuis 0.1.25 porte une propriété
|
|
467
|
+
* de sécurité : c'est par elle que l'appartenance se prouve.
|
|
468
|
+
*
|
|
469
|
+
* ⚠️ Et ça ne marche QUE si votre application et le player partagent la même origine. Deux
|
|
470
|
+
* sous-domaines, ce sont deux `localStorage` : aucune valeur ici n'y changera rien. C'est une
|
|
471
|
+
* transition — le bon mécanisme est que l'hôte injecte son membre au rendu de la page.
|
|
472
|
+
*/
|
|
473
|
+
hostAuthStorageKey: String(env.PLAYER_HOST_AUTH_STORAGE_KEY || "").trim(),
|
|
460
474
|
// ⚠️ Un BOOLÉEN, jamais l'URL. La carte d'identité doit pouvoir dire « un émetteur distinct
|
|
461
475
|
// est configuré » sans dire lequel : c'est le dernier endroit où un hôte devait deviner.
|
|
462
476
|
// `host-auth` dit que le code SAIT faire la séparation ; ceci dit qu'elle est POSÉE. Sans
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
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",
|
package/server/handler.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
const crypto = require("crypto");
|
|
7
7
|
const { getShareBySlug, logView, upsertSession, createReshare, sendReshareEmail, upsertInternalSession,
|
|
8
8
|
createShare, revokeShare, setShareAuth, overview: docOverview, listSharesForDoc, listSessionsForDoc, internalStatsForDoc } = require("./shares");
|
|
9
|
-
const { createPresentation, getPresentation, setPage, endPresentation, addMessage, listMessages, toggleReaction, editMessage, deleteMessage, setChatLock, createUploadUrl, reclaimPresentation, touchPresentation, listActivePresentations, handoverPresentation, endPresentationByOwner, recordAttendance,
|
|
9
|
+
const { createPresentation, getPresentation, setPage, endPresentation, addMessage, listMessages, toggleReaction, editMessage, deleteMessage, setChatLock, createUploadUrl, reclaimPresentation, touchPresentation, listActivePresentations, handoverPresentation, endPresentationByOwner, recordAttendance, presentationStats, listPresentationsForDoc, switchPresentationDoc, setPresentationContent } = require("./presentations");
|
|
10
10
|
// CONTEXTE INJECTÉ : tout ce que le player emprunte à l'application hôte passe par ici — stockage,
|
|
11
11
|
// base, identité, limites, marque, journalisation — et rien d'autre. C'est la frontière qui permettra
|
|
12
12
|
// de brancher un second projet, puis d'ouvrir le cœur. Cf. api/_player-context.js.
|
|
@@ -119,6 +119,7 @@ const LIVE_CSS = `
|
|
|
119
119
|
.pres-item{display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:8px}
|
|
120
120
|
.pres-item .a,.cm .a{width:28px;height:28px;border-radius:50%;flex:none;background:#e6e2db;color:#555;font-size:11px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;overflow:hidden}
|
|
121
121
|
.pres-item .a img,.cm .a img{width:100%;height:100%;object-fit:cover}
|
|
122
|
+
.pres-by{font-size:12px;opacity:.72;margin:0 0 8px;padding:0 2px}
|
|
122
123
|
.pres-item .n{font-size:13px;font-weight:600}
|
|
123
124
|
.pres-item .e{font-size:11px;color:#888}
|
|
124
125
|
.tag{font-size:9.5px;font-weight:800;color:#e5384d;text-transform:uppercase;letter-spacing:.02em}
|
|
@@ -233,7 +234,7 @@ const LIVE_PANEL = `<div class="chat hidden" id=chatPanel><div class=chat-grip i
|
|
|
233
234
|
// JS partagé : présence + chat via Supabase Realtime. Live.connect(slug, me) / Live.disconnect().
|
|
234
235
|
const LIVE_JS = `
|
|
235
236
|
var Live=(function(){
|
|
236
|
-
var sb=null,ch=null,ME=null,SLUG=null,CONTROL=null,LOCKED=false,AUTHTOK=null,PRESENT=[],
|
|
237
|
+
var sb=null,ch=null,ME=null,SLUG=null,CONTROL=null,LOCKED=false,AUTHTOK=null,PRESENT=[],PRESNAME='',seen={},msgEls={},msgData={},replyCtx=null,typers={},pdfCache={},_tyT=0,_tyIv=0,_atIv=0,unread=0,autoOpened=false,_histDone=false,_phWired=false,_onMap=null,_onState=null,_peekT=0,MUTED=false;
|
|
237
238
|
try{ MUTED=localStorage.getItem('3dd-present-mute')==='1'; }catch(e){}
|
|
238
239
|
// Couper/rétablir les notifications du chat (cloche) : coupé = plus de ticker ni de pulse (badge silencieux gardé).
|
|
239
240
|
function applyMute(){ var b=document.getElementById('chatMute'); if(b){b.classList.toggle('muted',MUTED);b.title=MUTED?'Réactiver les notifications du chat':'Couper les notifications du chat';} setBadge(); }
|
|
@@ -298,7 +299,7 @@ var Live=(function(){
|
|
|
298
299
|
function isMentioned(m){return Player.live.isMentioned(m,ME);}
|
|
299
300
|
function renderPres(st){var l=flat(st),c=l.length;PRESENT=l;var e=document.getElementById('presCount');if(e)e.textContent=c;
|
|
300
301
|
var a=document.getElementById('presAvs');if(a)a.innerHTML=l.slice(0,4).map(function(m){return '<span class=pres-av>'+av(m.avatar,m.name)+'</span>';}).join('');
|
|
301
|
-
var p=document.getElementById('presList');if(p)p.innerHTML='<h5>'+c+' en ligne</h5>'+l.map(function(m){return '<div class=pres-item><span class=a>'+av(m.avatar,m.name)+'</span><span class=b><div class=n>'+esc(m.name||'Invité')+
|
|
302
|
+
var p=document.getElementById('presList');if(p)p.innerHTML='<h5>'+c+' en ligne</h5>'+(PRESNAME?'<div class=pres-by>Présenté par '+esc(PRESNAME)+'</div>':'')+l.map(function(m){return '<div class=pres-item><span class=a>'+av(m.avatar,m.name)+'</span><span class=b><div class=n>'+esc(m.name||'Invité')+'</div>'+(m.email?'<div class=e>'+esc(m.email)+'</div>':'')+'</span></div>';}).join('');}
|
|
302
303
|
// Rendu d'un message → player/src/chat.ts (échappement testé sous jsdom : on y vérifie ce que
|
|
303
304
|
// le NAVIGATEUR fabrique, pas seulement la chaîne produite).
|
|
304
305
|
function renderRe(m){return Player.chat.renderReactions(m,ME);}
|
|
@@ -415,7 +416,13 @@ var Live=(function(){
|
|
|
415
416
|
var pb=document.getElementById('presBtn');if(pb)pb.style.display='inline-flex';
|
|
416
417
|
var cb=document.getElementById('chatBtn');if(cb)cb.style.display='inline-flex';var _fb=document.getElementById('chatFab');if(_fb)_fb.classList.add('on');
|
|
417
418
|
wire();history();
|
|
418
|
-
try{sb=window.supabase.createClient(LIVECFG.supaUrl,LIVECFG.supaKey,{realtime:{params:{eventsPerSecond:10}}
|
|
419
|
+
try{sb=window.supabase.createClient(LIVECFG.supaUrl,LIVECFG.supaKey,{realtime:{params:{eventsPerSecond:10}},
|
|
420
|
+
// ⚠️ UNE CLÉ DÉCLARÉE, PAS CELLE PAR DÉFAUT. Ce client vivra un jour une session anonyme
|
|
421
|
+
// (canal Realtime prive). S'il ecrit sous la cle par defaut et que l'application de l'hote
|
|
422
|
+
// l'utilise aussi sur la meme origine, la session anonyme ECRASE celle du membre connecte.
|
|
423
|
+
// Chez nous les deux cles different deja, mais par heureux hasard : le declarer rend
|
|
424
|
+
// intentionnel ce qui n'etait qu'une consequence, et une topologie peut changer.
|
|
425
|
+
auth:{storageKey:LIVECFG.liveAuthKey||'dmp-live-auth',persistSession:true,autoRefreshToken:true}});
|
|
419
426
|
ch=sb.channel('plive-'+slug,{config:{presence:{key:(me.email||me.name||'x')+':'+MYID}}});
|
|
420
427
|
ch.on('presence',{event:'sync'},function(){renderPres(ch.presenceState());});
|
|
421
428
|
// Plus d'abonnement à la table des messages : elle n'est plus publiée ni lisible
|
|
@@ -478,8 +485,8 @@ var Live=(function(){
|
|
|
478
485
|
// Le serveur renvoie la CLÉ de celui qui a prouvé le control_token ; l'audience compare. Pas
|
|
479
486
|
// de clé, pas de titre : mieux vaut aucun titre qu'un titre usurpé.
|
|
480
487
|
function etatDuServeur(st){
|
|
481
|
-
if(typeof st.
|
|
482
|
-
if(
|
|
488
|
+
if(typeof st.presenter_name!=='undefined'){var n2=st.presenter_name||'';
|
|
489
|
+
if(n2!==PRESNAME){PRESNAME=n2;try{if(ch)renderPres(ch.presenceState());}catch(e){}}}
|
|
483
490
|
if(_onState)_onState(st);
|
|
484
491
|
}
|
|
485
492
|
relire('&state=1',function(d){if(d.state)etatDuServeur(d.state);});
|
|
@@ -503,9 +510,9 @@ var Live=(function(){
|
|
|
503
510
|
// Membre de l'équipe reconnu via la session app (MÊME ORIGINE, localStorage) → avatar + nom auto.
|
|
504
511
|
// Le jeton d'acces de la session locale, quand il y en a une. C'est la SEULE chose qui prouve au
|
|
505
512
|
// serveur qu'on est un membre ; 'member:true' dans la page ne prouve rien, il ne sert qu'a l'affichage.
|
|
506
|
-
function accessToken(){try{var raw=localStorage.getItem('
|
|
513
|
+
function accessToken(){try{var raw=localStorage.getItem(LIVECFG.hostAuthKey||'');if(!raw)return '';var s=JSON.parse(raw);
|
|
507
514
|
return String((s&&(s.access_token||(s.currentSession&&s.currentSession.access_token)||(s.session&&s.session.access_token)))||'');}catch(e){return '';}}
|
|
508
|
-
function detectMember(){try{var raw=localStorage.getItem('
|
|
515
|
+
function detectMember(){try{var raw=localStorage.getItem(LIVECFG.hostAuthKey||'');if(!raw)return null;var s=JSON.parse(raw);var u=s&&(s.user||(s.currentSession&&s.currentSession.user)||(s.session&&s.session.user));if(u&&u.email){var m=u.user_metadata||{};return{name:m.name||u.email,email:u.email,avatar:m.avatarUrl||'',member:true,role:'viewer'};}}catch(e){}return null;}
|
|
509
516
|
return {connect:connect,disconnect:disconnect,detectMember:detectMember,sendMap:sendMap,onMap:onMap,sendState:sendState,onState:onState};
|
|
510
517
|
})();`;
|
|
511
518
|
|
|
@@ -1243,6 +1250,34 @@ function adresseAppelant(req) {
|
|
|
1243
1250
|
* Le jeton d'accès, lui, est vérifié par l'hôte. Absent ou invalide : pas membre. Jamais de repli
|
|
1244
1251
|
* sur ce que l'appelant affirme, sinon la vérification ne sert qu'aux honnêtes.
|
|
1245
1252
|
*/
|
|
1253
|
+
// ── Clés de `localStorage`, et pourquoi ce ne sont plus des constantes ─────────────────────────
|
|
1254
|
+
//
|
|
1255
|
+
// ⚠️ `3dd-supabase-auth` — la clé de session du studio 3D Discovery — était écrite EN DUR à cinq
|
|
1256
|
+
// endroits de ce paquet open source. Conséquence pour tout autre hôte : `detectMember()` et
|
|
1257
|
+
// `accessToken()` ne trouvent jamais rien, donc AUCUN de ses membres n'est reconnu comme tel. La
|
|
1258
|
+
// séparation des populations interne/externe, que ce produit vend, ne fonctionnait que chez nous.
|
|
1259
|
+
//
|
|
1260
|
+
// ⚠️ Et depuis 0.1.25 cette clé porte une propriété de sécurité : c'est par elle que l'appartenance
|
|
1261
|
+
// se prouve. Une constante d'un hôte devenue porteuse pour tous les autres.
|
|
1262
|
+
//
|
|
1263
|
+
// Défaut VIDE, et c'est délibéré : sans clé déclarée, la détection est simplement inactive — aucun
|
|
1264
|
+
// membre, donc aucune usurpation. Un défaut à `3dd-supabase-auth` aurait gardé notre instance en
|
|
1265
|
+
// marche en laissant le défaut de conception intact, et le prochain hôte l'aurait découvert comme
|
|
1266
|
+
// ADV : en constatant que ses statistiques ne séparent rien.
|
|
1267
|
+
//
|
|
1268
|
+
// ⚠️ CE N'EST QU'UNE TRANSITION. Lire le `localStorage` d'une autre application ne peut PAS marcher
|
|
1269
|
+
// quand les origines diffèrent — l'instance ADV est sur `doc.adnfamily.com` et son application sur
|
|
1270
|
+
// `app.adnfamily.com` : deux `localStorage`, aucune configuration n'y changera rien. Le bon
|
|
1271
|
+
// mécanisme est que l'hôte INJECTE son membre au rendu de la page, comme il injecte déjà sa marque.
|
|
1272
|
+
// Tracé dans docs/AUDIT-2026-08-14-SUIVI.md.
|
|
1273
|
+
function cleSessionHote() {
|
|
1274
|
+
return String((PLAYER.config && PLAYER.config.hostAuthStorageKey) || "");
|
|
1275
|
+
}
|
|
1276
|
+
// La session du client Realtime du player, et l'identité d'un invité : à lui, sous son nom. Elles
|
|
1277
|
+
// ne dépendent d'aucun hôte, donc elles restent des constantes — mais plus des constantes d'AUTRUI.
|
|
1278
|
+
const CLE_SESSION_PLAYER = "dmp-live-auth";
|
|
1279
|
+
const CLE_INVITE = "dmp-present-me";
|
|
1280
|
+
|
|
1246
1281
|
async function membreVerifie(req) {
|
|
1247
1282
|
try {
|
|
1248
1283
|
const u = await PLAYER.identity.verifyToken((req.headers && req.headers.authorization) || "");
|
|
@@ -1796,7 +1831,7 @@ ${LEGAL_CSS}
|
|
|
1796
1831
|
<script nonce="${nonce}">${PLAYER_BROWSER_JS}</script>
|
|
1797
1832
|
<script nonce="${nonce}" src="${PDFJS}/pdf.min.js"></script>
|
|
1798
1833
|
${preview ? `<script nonce="${nonce}" src="${SUPAJS}"></script>
|
|
1799
|
-
<script nonce="${nonce}">var LIVECFG={supaUrl:${JSON.stringify(share.supa_url || "")},supaKey:${JSON.stringify(share.supa_key || "")}};var GMAPS_KEY=${JSON.stringify((PLAYER.config && PLAYER.config.mapsKey) || "" || "")};${LIVE_JS}
|
|
1834
|
+
<script nonce="${nonce}">var LIVECFG={supaUrl:${JSON.stringify(share.supa_url || "")},supaKey:${JSON.stringify(share.supa_key || "")},hostAuthKey:${JSON.stringify(cleSessionHote())},liveAuthKey:${JSON.stringify(CLE_SESSION_PLAYER)},guestKey:${JSON.stringify(CLE_INVITE)}};var GMAPS_KEY=${JSON.stringify((PLAYER.config && PLAYER.config.mapsKey) || "" || "")};${LIVE_JS}
|
|
1800
1835
|
${MAP_JS}</script>` : ""}
|
|
1801
1836
|
<script nonce="${nonce}">
|
|
1802
1837
|
(function(){
|
|
@@ -1871,7 +1906,7 @@ ${LEGAL_CSS}
|
|
|
1871
1906
|
var PRES=null, _pushT=null, _hbIv=0;
|
|
1872
1907
|
// JWT de la session app (MÊME ORIGINE, localStorage) → autorise le rattachement de la présentation au membre
|
|
1873
1908
|
// (reprise / liste / transfert) et les actions authentifiées (reclaim).
|
|
1874
|
-
function appToken(){ try{ var raw=localStorage.getItem('
|
|
1909
|
+
function appToken(){ try{ var raw=localStorage.getItem(LIVECFG.hostAuthKey||''); if(!raw)return''; var s=JSON.parse(raw); var t=(s&&(s.access_token||(s.currentSession&&s.currentSession.access_token)||(s.session&&s.session.access_token)))||''; return t; }catch(e){ return ''; } }
|
|
1875
1910
|
function ctlKey(slug){ return '3dd-pres-ctl-'+slug; }
|
|
1876
1911
|
function saveCtl(slug,control){ try{ localStorage.setItem(ctlKey(slug),control); }catch(e){} }
|
|
1877
1912
|
function clearCtl(slug){ try{ localStorage.removeItem(ctlKey(slug)); }catch(e){} }
|
|
@@ -2314,7 +2349,7 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
|
|
|
2314
2349
|
})();
|
|
2315
2350
|
</script>
|
|
2316
2351
|
<script nonce="${nonce}">
|
|
2317
|
-
var LIVECFG={supaUrl:${JSON.stringify(supaUrl || "")},supaKey:${JSON.stringify(supaKey || "")}};var GMAPS_KEY=${JSON.stringify((PLAYER.config && PLAYER.config.mapsKey) || "" || "")};
|
|
2352
|
+
var LIVECFG={supaUrl:${JSON.stringify(supaUrl || "")},supaKey:${JSON.stringify(supaKey || "")},hostAuthKey:${JSON.stringify(cleSessionHote())},liveAuthKey:${JSON.stringify(CLE_SESSION_PLAYER)},guestKey:${JSON.stringify(CLE_INVITE)}};var GMAPS_KEY=${JSON.stringify((PLAYER.config && PLAYER.config.mapsKey) || "" || "")};
|
|
2318
2353
|
${LIVE_JS}
|
|
2319
2354
|
${MAP_JS}
|
|
2320
2355
|
(function(){
|
|
@@ -2337,19 +2372,19 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
|
|
|
2337
2372
|
// « Reprendre la main » : si le membre connecté (même origine) devient propriétaire de CETTE présentation
|
|
2338
2373
|
// (après un transfert), on affiche un bouton pour ouvrir la visionneuse en pilotage — sinon on ne peut pas
|
|
2339
2374
|
// piloter depuis la page audience.
|
|
2340
|
-
function appTok(){ try{ var raw=localStorage.getItem('
|
|
2375
|
+
function appTok(){ try{ var raw=localStorage.getItem(LIVECFG.hostAuthKey||''); if(!raw)return''; var s=JSON.parse(raw); return (s&&(s.access_token||(s.currentSession&&s.currentSession.access_token)||(s.session&&s.session.access_token)))||''; }catch(e){return'';} }
|
|
2341
2376
|
function checkOwner(){ var tk=appTok(); if(!tk)return; fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+tk},body:JSON.stringify({action:'present-list'})}).then(function(r){return r.json();}).then(function(d){ var mine=false; if(d&&d.presentations){ for(var i=0;i<d.presentations.length;i++){ if(d.presentations[i].slug===slug&&d.presentations[i].mine){mine=true;break;} } } var b=document.getElementById('takeOver'); if(b)b.style.display=mine?'inline-flex':'none'; }).catch(function(){}); }
|
|
2342
2377
|
function startOwnerWatch(){ var b=document.getElementById('takeOver'); if(b&&!b._w){b._w=1;b.addEventListener('click',function(){ location.href='/app/documents?resume='+encodeURIComponent(slug); });} checkOwner(); setInterval(checkOwner,12000); }
|
|
2343
2378
|
var me=Live.detectMember();
|
|
2344
2379
|
if(me){ Live.connect(slug, me); startOwnerWatch(); return; }
|
|
2345
|
-
var saved=null; try{ saved=JSON.parse(localStorage.getItem('
|
|
2380
|
+
var saved=null; try{ saved=JSON.parse(localStorage.getItem(LIVECFG.guestKey||'dmp-present-me')||'null'); }catch(e){}
|
|
2346
2381
|
if(saved&&saved.name){ Live.connect(slug, saved); return; }
|
|
2347
2382
|
// Externe : on demande le nom pour participer.
|
|
2348
2383
|
var o=document.createElement('div'); o.className='join';
|
|
2349
2384
|
o.innerHTML='<div class=join-card><h4>Rejoindre la présentation</h4><p>Votre nom pour participer à la discussion.</p><input id=jName placeholder="Votre nom" maxlength=60 autocomplete=name><input id=jMail placeholder="Email (facultatif)" maxlength=120 autocomplete=email><button id=jGo>Rejoindre</button></div>';
|
|
2350
2385
|
document.body.appendChild(o);
|
|
2351
2386
|
var n=o.querySelector('#jName'); try{ n.focus(); }catch(e){}
|
|
2352
|
-
function go(){ var name=(n.value||'').trim()||'Invité'; var email=(o.querySelector('#jMail').value||'').trim(); var me2={name:name,email:email,avatar:'',member:false,role:'viewer'}; try{ localStorage.setItem('
|
|
2387
|
+
function go(){ var name=(n.value||'').trim()||'Invité'; var email=(o.querySelector('#jMail').value||'').trim(); var me2={name:name,email:email,avatar:'',member:false,role:'viewer'}; try{ localStorage.setItem(LIVECFG.guestKey||'dmp-present-me',JSON.stringify(me2)); }catch(e){} o.parentNode&&o.parentNode.removeChild(o); Live.connect(slug, me2); }
|
|
2353
2388
|
o.querySelector('#jGo').addEventListener('click',go);
|
|
2354
2389
|
n.addEventListener('keydown',function(e){ if(e.key==='Enter') go(); });
|
|
2355
2390
|
})();
|
|
@@ -3039,10 +3074,26 @@ async function handler(req, res) {
|
|
|
3039
3074
|
file_name: pres.file_name || null,
|
|
3040
3075
|
doc_title: pres.doc_title || null,
|
|
3041
3076
|
updated_at: pres.updated_at || null,
|
|
3042
|
-
//
|
|
3043
|
-
//
|
|
3044
|
-
//
|
|
3045
|
-
|
|
3077
|
+
// ⚠️ CE CHAMP A PORTÉ `presenter_key` PENDANT UNE JOURNÉE, ET C'ÉTAIT DEUX FAUTES.
|
|
3078
|
+
//
|
|
3079
|
+
// 1. FUITE. `attendeeKey()` renvoie l'ADRESSE E-MAIL quand le participant en a une. La clé
|
|
3080
|
+
// du présentateur était donc son e-mail, servi à tout visiteur anonyme du lien — sur la
|
|
3081
|
+
// route même dont le commentaire ci-dessus promet « rien que ce que l'audience doit
|
|
3082
|
+
// savoir ». Le champ portait un nom technique, et personne (moi compris) n'est allé
|
|
3083
|
+
// voir ce qu'il contenait.
|
|
3084
|
+
//
|
|
3085
|
+
// 2. CE N'ÉTAIT PAS UNE PREUVE. Le badge comparait cette clé à l'`uid` de la charge de
|
|
3086
|
+
// présence — que le client COMPOSE. Lire la clé publique puis s'annoncer avec elle
|
|
3087
|
+
// suffisait à porter le titre. On avait remplacé « le client déclare son rôle » par
|
|
3088
|
+
// « le client déclare une valeur que le serveur lui a donnée » : plus laborieux à
|
|
3089
|
+
// exploiter, pas plus vrai.
|
|
3090
|
+
//
|
|
3091
|
+
// ⚠️ Comparer deux valeurs que le client choisit ou connaît n'est pas une preuve. Le nom
|
|
3092
|
+
// du présentateur vient de l'hôte, il ne se compare à rien, et il s'affiche à part de la
|
|
3093
|
+
// liste des participants — laquelle ne porte plus aucun titre.
|
|
3094
|
+
//
|
|
3095
|
+
// Signalé par la seconde passe d'audit (P0-4) ; la fuite n'y était pas.
|
|
3096
|
+
presenter_name: String(pres.presenter_name || "") || null,
|
|
3046
3097
|
} }));
|
|
3047
3098
|
return;
|
|
3048
3099
|
}
|
package/server/presentations.js
CHANGED
|
@@ -281,29 +281,6 @@ async function toggleReaction(slug, msgId, emoji, reactor) {
|
|
|
281
281
|
return { ok: true, message: premierPublic(maj) };
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
/**
|
|
285
|
-
* La clé du participant qui porte le titre de présentateur, telle que le SERVEUR la connaît.
|
|
286
|
-
*
|
|
287
|
-
* ⚠️ Elle existe parce que la liste des participants affichait le titre d'après la charge de
|
|
288
|
-
* PRÉSENCE, que chacun compose lui-même : `track({role:'presenter'})` suffisait à apparaître comme
|
|
289
|
-
* présentateur devant toute l'audience, avec le nom et l'avatar de son choix. Le canal ne peut pas
|
|
290
|
-
* arbitrer ça — un participant légitime a le droit d'y écrire sa présence.
|
|
291
|
-
*
|
|
292
|
-
* Le titre est désormais accordé ici, à partir de `is_presenter`, lui-même dérivé du `control_token`.
|
|
293
|
-
* Null quand personne ne l'a prouvé : mieux vaut aucun titre qu'un titre usurpé.
|
|
294
|
-
*/
|
|
295
|
-
async function presenterKey(slug) {
|
|
296
|
-
if (!slug) return null;
|
|
297
|
-
// ⚠️ NE JAMAIS FAIRE ÉCHOUER LA RELECTURE D'ÉTAT. Cette recherche est un ajout à une route dont
|
|
298
|
-
// l'audience dépend pour savoir quelle page est affichée. Une requête de plus, c'est une raison
|
|
299
|
-
// de plus de renvoyer 500 — et perdre TOUT l'état parce qu'on n'a pas su dire qui porte un badge
|
|
300
|
-
// serait un très mauvais échange. Sans réponse : pas de clé, donc pas de titre, et le reste passe.
|
|
301
|
-
try {
|
|
302
|
-
const rows = await PLAYER.db.request(`doc_presentation_attendees?slug=eq.${enc(slug)}&is_presenter=is.true&select=attendee_key&order=last_seen.desc&limit=1`);
|
|
303
|
-
return (Array.isArray(rows) && rows[0] && rows[0].attendee_key) || null;
|
|
304
|
-
} catch { return null; }
|
|
305
|
-
}
|
|
306
|
-
|
|
307
284
|
// ── Statistiques de présentation (assistance) ────────────────────────────────────────────────────────────
|
|
308
285
|
// Heartbeat d'un participant : upsert de sa ligne d'assistance. On accumule le temps de présence (intervalles
|
|
309
286
|
// < 60 s → un aller-retour ne gonfle pas total_ms) et l'ensemble des pages vues (page courante de la présentation).
|
|
@@ -410,4 +387,4 @@ async function listPresentationsForDoc(docId) {
|
|
|
410
387
|
}
|
|
411
388
|
|
|
412
389
|
module.exports = {
|
|
413
|
-
messagePublic, CHAMPS_PUBLICS, init, createPresentation, getPresentation, setPage, endPresentation, addMessage, listMessages, toggleReaction, editMessage, deleteMessage, setChatLock, createUploadUrl, reclaimPresentation, touchPresentation, listActivePresentations, handoverPresentation, endPresentationByOwner, recordAttendance,
|
|
390
|
+
messagePublic, CHAMPS_PUBLICS, init, createPresentation, getPresentation, setPage, endPresentation, addMessage, listMessages, toggleReaction, editMessage, deleteMessage, setChatLock, createUploadUrl, reclaimPresentation, touchPresentation, listActivePresentations, handoverPresentation, endPresentationByOwner, recordAttendance, presentationStats, listPresentationsForDoc, switchPresentationDoc, setPresentationContent };
|