discovery-media-player 0.1.27 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.27",
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, presenterKey, presentationStats, listPresentationsForDoc, switchPresentationDoc, setPresentationContent } = require("./presentations");
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=[],PRESKEY='',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
+ 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é')+(m.uid&&m.uid===PRESKEY?' <span class=tag>présentateur</span>':'')+'</div>'+(m.email?'<div class=e>'+esc(m.email)+'</div>':'')+'</span></div>';}).join('');}
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);}
@@ -484,8 +485,8 @@ var Live=(function(){
484
485
  // Le serveur renvoie la CLÉ de celui qui a prouvé le control_token ; l'audience compare. Pas
485
486
  // de clé, pas de titre : mieux vaut aucun titre qu'un titre usurpé.
486
487
  function etatDuServeur(st){
487
- if(typeof st.presenter_key!=='undefined'){var k=st.presenter_key||'';
488
- if(k!==PRESKEY){PRESKEY=k;try{if(ch)renderPres(ch.presenceState());}catch(e){}}}
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){}}}
489
490
  if(_onState)_onState(st);
490
491
  }
491
492
  relire('&state=1',function(d){if(d.state)etatDuServeur(d.state);});
@@ -3073,10 +3074,26 @@ async function handler(req, res) {
3073
3074
  file_name: pres.file_name || null,
3074
3075
  doc_title: pres.doc_title || null,
3075
3076
  updated_at: pres.updated_at || null,
3076
- // Qui porte le titre, d'après le serveur. La liste des participants le tirait de la charge
3077
- // de présence, que chacun compose : il suffisait de s'annoncer présentateur pour l'être aux
3078
- // yeux de tous. Une clé, pas un booléen — l'audience compare, elle ne croit pas.
3079
- presenter_key: await presenterKey(String(q.present || "")),
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,
3080
3097
  } }));
3081
3098
  return;
3082
3099
  }
@@ -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, presenterKey, presentationStats, listPresentationsForDoc, switchPresentationDoc, setPresentationContent };
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 };