discovery-media-player 0.1.97 → 0.1.99

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.97",
3
+ "version": "0.1.99",
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",
@@ -179,7 +179,7 @@ var Live=(function(){
179
179
  // battait deux fois. On les déclare au niveau du CYCLE DE VIE : ce qui doit être arrêté doit
180
180
  // être visible depuis l'endroit qui arrête.
181
181
  var _ordEtat=null,_ordChat=null,_filet=null;
182
- var sb=null,ch=null,ME=null,SLUG=null,CONTROL=null,LOCKED=false,AUTHTOK=null,PRESENT=[],PRESNAME='',seen=Object.create(null),msgEls=Object.create(null),msgData=Object.create(null),replyCtx=null,typers=Object.create(null),pdfCache=Object.create(null),_tyT=0,_tyIv=0,_atIv=0,unread=0,autoOpened=false,_histDone=false,_phWired=false,_onMap=null,_onState=null,_peekT=0,MUTED=false,_maxSeq=0;
182
+ var sb=null,ch=null,ME=null,SLUG=null,CONTROL=null,LOCKED=false,AUTHTOK=null,PRESENT=[],PRESNAME='',seen=Object.create(null),msgEls=Object.create(null),msgData=Object.create(null),replyCtx=null,typers=Object.create(null),pdfCache=Object.create(null),_tyT=0,_tyIv=0,_atIv=0,unread=0,autoOpened=false,_histDone=false,_phWired=false,_onMap=null,_onState=null,_peekT=0,MUTED=false,_maxSeq=0,_ptJeton='';
183
183
  try{ MUTED=localStorage.getItem('3dd-present-mute')==='1'; }catch(e){}
184
184
  // Couper/rétablir les notifications du chat (cloche) : coupé = plus de ticker ni de pulse (badge silencieux gardé).
185
185
  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(); }
@@ -253,8 +253,21 @@ var Live=(function(){
253
253
  // Le serveur ne croit plus 'isMember' ni 'isPresenter' sur parole : l'appartenance se prouve par
254
254
  // le jeton d'acces de la session, le titre de presentateur par le control_token. Ce qui part d'ici
255
255
  // n'est plus qu'une AFFIRMATION, et le serveur la remplace par ce qu'il a verifie.
256
+ // ⚠️ JETON DE PRÉSENCE (P1c étape 2, incrément 3). Le battement PORTE le jeton reçu au précédent
257
+ // (champ pt) : le serveur en dérive la clé de la ligne, donc un tiers ne peut plus poster notre clé
258
+ // pour écraser notre présence. Tant qu'on n'en a pas, on demande l'émission (wantToken) — ce marqueur
259
+ // distingue un BOOTSTRAP moderne d'un client LEGACY, sans quoi sansJeton ne retomberait jamais à
260
+ // zéro et la porte ne pourrait jamais se fermer. La réponse est lue pour ranger le jeton émis ; un
261
+ // hôte sans PLAYER_PRESENCE_SECRET n'en renvoie pas, et tout continue comme avant (legacy).
262
+ // ⚠️ AUCUN BACKTICK DANS CE COMMENTAIRE : ce fichier ÉMET du JS via des template literals — un
263
+ // backtick ici ferme la chaîne et casse tout le module (vu à l'instant, 33 essais rouges d'un coup).
256
264
  function sendAttend(){ if(!SLUG||!ME)return; try{ var h={'Content-Type':'application/json'};var jw=accessToken();if(jw)h.Authorization='Bearer '+jw;
257
- fetch('/api/doc',{method:'POST',headers:h,body:JSON.stringify({action:'present-attend',slug:SLUG,control:CONTROL,key:attKey(),name:ME.name||'',email:ME.email||'',avatar:ME.avatar||''})}); }catch(e){} }
265
+ var corps={action:'present-attend',slug:SLUG,control:CONTROL,key:attKey(),name:ME.name||'',email:ME.email||'',avatar:ME.avatar||''};
266
+ if(_ptJeton)corps.pt=_ptJeton;else corps.wantToken='1';
267
+ fetch('/api/doc',{method:'POST',headers:h,body:JSON.stringify(corps)})
268
+ .then(function(r){return r.json();})
269
+ .then(function(d){if(d&&d.pt)_ptJeton=d.pt;})
270
+ .catch(function(){}); }catch(e){} }
258
271
  var EMOJIS=['👍','❤️','😂','😮','👏','🎉'];
259
272
  var RSVG='<svg viewBox="0 0 24 24" fill=none stroke=currentColor stroke-width=2 stroke-linecap=round><circle cx=12 cy=12 r=9 /><path d="M8.5 14.5s1.4 1.7 3.5 1.7 3.5-1.7 3.5-1.7"/><line x1=9 y1=9.2 x2=9.01 y2=9.2 /><line x1=15 y1=9.2 x2=15.01 y2=9.2 /></svg>';
260
273
  function esc(s){return Player.live.escapeHtml(s);}
@@ -787,20 +787,30 @@ async function recordAttendance(slug, participant, { presentation = null, ipHash
787
787
  // lire-modifier-réécrire ci-dessous — toujours correcte, mais SANS le plafond — et on le dit une
788
788
  // fois. Même patron que player_rate_limit_bump : dégrader, jamais casser, jamais en silence.
789
789
  const capAnon = Number(anonCap) > 0 ? Math.trunc(Number(anonCap)) : PLAFOND_CREATION_ANON_DEFAUT;
790
+ // ⚠️ ON N'ENVOIE `p_has_token` QUE SI 0017 EST LÀ — ET C'EST LA MOITIÉ MANQUANTE DE LA COMPATIBILITÉ.
791
+ //
792
+ // PostgREST résout une RPC par JEU D'ARGUMENTS NOMMÉS : un argument en trop ne « prend pas son
793
+ // défaut », il ne correspond à AUCUNE fonction → 404. Le `DEFAULT null` de 0017 rend donc la base
794
+ // NEUVE compatible avec du CODE ANCIEN ; il ne peut rien pour le sens INVERSE — code neuf, base
795
+ // ancienne — qui est précisément l'ordre d'un déploiement réel : le code part avant la migration.
796
+ //
797
+ // Sans cette garde, un hôte à jour mais pas encore migré tombait dans le repli lire-modifier-
798
+ // réécrire, c'est-à-dire PERDAIT LE PLAFOND de création de faux participants apporté par 0015 : une
799
+ // garde disparaissait en silence entre deux migrations. On sonde donc la colonne (la réponse existe
800
+ // déjà) et on appelle à 10 arguments quand elle manque — l'ancien contrat, valide sur les DEUX bases.
801
+ // La dégradation redevient alors ce qu'on annonce : pas de compteur de transition, rien d'autre.
802
+ // (relevé du second hôte, qui l'a MESURÉ sur sa base au lieu de le supposer)
803
+ const transitionDispo = await require("./schema").attendue("jetonPresence");
804
+ const corpsRpc = {
805
+ p_slug: String(slug), p_key: String(key), p_ip_hash: ipHash || null, p_page: page,
806
+ p_name: (name || "").slice(0, 120), p_avatar: (avatar || "").slice(0, 600),
807
+ p_is_member: !!isMember, p_is_presenter: !!isPresenter,
808
+ p_max_gap_ms: ATTEND_MAX_GAP_MS, p_anon_cap: capAnon,
809
+ };
810
+ // true → last_token_at, false → last_no_token_at, null → ni l'un ni l'autre. 0017 seulement.
811
+ if (transitionDispo) corpsRpc.p_has_token = hasToken == null ? null : !!hasToken;
790
812
  try {
791
- const r = await PLAYER.db.request("rpc/player_attendance_bump", {
792
- method: "POST",
793
- body: {
794
- p_slug: String(slug), p_key: String(key), p_ip_hash: ipHash || null, p_page: page,
795
- p_name: (name || "").slice(0, 120), p_avatar: (avatar || "").slice(0, 600),
796
- p_is_member: !!isMember, p_is_presenter: !!isPresenter,
797
- p_max_gap_ms: ATTEND_MAX_GAP_MS, p_anon_cap: capAnon,
798
- // Transition du jeton (0017) : true → last_token_at, false → last_no_token_at, null → ni l'un ni
799
- // l'autre. La RPC 11-args (p_has_token DEFAULT null) accepte l'appel même sans la 0017 côté code
800
- // ancien ; sans la 0017 côté base, la 11-args n'existe pas → on tombe dans le repli ci-dessous.
801
- p_has_token: hasToken == null ? null : !!hasToken,
802
- },
803
- });
813
+ const r = await PLAYER.db.request("rpc/player_attendance_bump", { method: "POST", body: corpsRpc });
804
814
  const ligne = Array.isArray(r) ? r[0] : r;
805
815
  if (ligne && typeof ligne.ok === "boolean") {
806
816
  // Plafond de création atteint : on ne crée pas ce faux participant. 429 = « trop », pas une panne.
@@ -812,8 +822,17 @@ async function recordAttendance(slug, participant, { presentation = null, ipHash
812
822
  if (!_avertRpcPresence) {
813
823
  _avertRpcPresence = true;
814
824
  try {
825
+ // ⚠️ ON NOMME LE FICHIER QU'ON VIENT VRAIMENT D'ESSAYER — un nom FAUX est pire qu'un nom
826
+ // absent, parce qu'il est ACTIONNABLE : l'exploitant vérifie la migration nommée, la trouve
827
+ // appliquée, et conclut au faux positif. C'est ce qui arrivait quand ce message accusait 0015
828
+ // alors que l'échec venait de l'argument `p_has_token` de 0017 (relevé du second hôte). La
829
+ // règle « on nomme le fichier, pas l'erreur » ne tient que tant qu'UN SEUL fichier peut causer
830
+ // l'échec ; dès qu'un second emprunte le même chemin, le nom doit se DÉDUIRE de la tentative.
831
+ const fichier = transitionDispo
832
+ ? "supabase/migrations/0017-jeton-presence.sql (ou 0015-presence-atomique.sql)"
833
+ : "supabase/migrations/0015-presence-atomique.sql";
815
834
  PLAYER.errors.capture(new Error(
816
- "présence non atomique : appliquez supabase/migrations/0015-presence-atomique.sql. "
835
+ "présence non atomique : appliquez " + fichier + ". "
817
836
  + "Sans elle, la présence est écrite par lire-modifier-réécrire (correct) mais le plafond "
818
837
  + "de création de faux participants anonymes n'est pas appliqué. "
819
838
  + "(" + ((erreur && erreur.message) || erreur) + ")",