discovery-media-player 0.1.40 → 0.1.42

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.40",
3
+ "version": "0.1.42",
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",
@@ -4,4 +4,4 @@
4
4
  // Injecté par api/doc.js dans un <script nonce=…> de la visionneuse. Le code navigateur du
5
5
  // player vit en modules TypeScript testés sous player/src/ — c'est ici qu'il atterrit.
6
6
  "use strict";
7
- module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var G=Object.defineProperty;var Me=Object.getOwnPropertyDescriptor;var xe=Object.getOwnPropertyNames;var we=Object.prototype.hasOwnProperty;var y=(e,n)=>{for(var t in n)G(e,t,{get:n[t],enumerable:!0})},Te=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of xe(n))!we.call(e,o)&&o!==t&&G(e,o,{get:()=>n[o],enumerable:!(r=Me(n,o))||r.enumerable});return e};var _e=e=>Te(G({},\"__esModule\",{value:!0}),e);var vn={};y(vn,{bridge:()=>L,chat:()=>O,live:()=>$,presentation:()=>N,presentationState:()=>D,tracking:()=>C,viewer:()=>H});var L={};y(L,{onHostMessage:()=>$e,onPlayerMessage:()=>Ce,parseHostMessage:()=>pe,parsePlayerMessage:()=>de,sendToHost:()=>Ae,sendToPlayer:()=>Re,toWire:()=>B});var K=\"3dd-doc-\",ke=/^[A-Za-z0-9_-]{1,64}$/,Se=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Ee=[\"embed-denied\"],Ie=[\"present-invite\",\"present-handover\",\"present-switch\"],Pe=[\"handover-done\"];function B(e){let n={type:K+e.type};return\"slug\"in e&&e.slug&&(n.slug=e.slug),\"reason\"in e&&e.reason&&(n.reason=String(e.reason).slice(0,40)),n}function ce(e,n,t){if(!e||typeof e!=\"object\")return null;let r=e.type;if(typeof r!=\"string\"||!r.startsWith(K))return null;let o=r.slice(K.length);if(n.includes(o))return{type:o};if(Ee.includes(o)){let a=e.reason;return{type:o,reason:/^[a-z-]{1,40}$/.test(String(a||\"\"))?String(a):\"unknown\"}}if(!t.includes(o))return null;let s=e.slug;return typeof s!=\"string\"||!ke.test(s)?null:{type:o,slug:s}}function de(e){return ce(e,Se,Ie)}function pe(e){return ce(e,Pe,[])}function Ae(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(B(e),\"*\")}catch(t){}}function Re(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(B(n),\"*\")}catch(r){}}function Le(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function Ce(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=s=>{if(!Le(t,s.source))return;let a=de(s.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function $e(e,n){var s;let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=(s=t.parent)!=null?s:null,o=a=>{if(!r||r===t||a.source!==r)return;let c=pe(a.data);c&&e(c)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var C={};y(C,{createTracker:()=>Ve});var Oe=Math.ceil(300),Ne=25,xn=Oe*Ne,fe=18e4;var De=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function Ue(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(16));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}return Math.random().toString(36).slice(2)+Date.now().toString(36)}function We(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;o&&typeof o.sendBeacon==\"function\"?o.sendBeacon(e,new Blob([r],{type:\"application/json\"})):n.fetch(e,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:r,keepalive:!0})}catch(r){}}}function Ve(e={}){var ae,le;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",s=(ae=e.idleMs)!=null?ae:fe,a=(le=e.sessionEveryMs)!=null?le:12e3,c=e.slug||\"\",d=e.internal||null,m=e.sessionId||Ue(),b=e.send||We(o,n),f={},h=0,M=0,u=0,v=0,g=null,k=r(),S=!1,U=!1,A=[],ve=()=>!!c||!!d,re=i=>{if(ve())try{b(i)}catch(l){}},W=()=>!!t&&t.visibilityState===\"visible\"&&!S,oe=i=>{let l=r()-i,w=Math.max(0,k-i)+s;return Math.max(0,Math.min(l,w))},V=()=>{g!=null&&h>0&&(f[h]=(f[h]||0)+oe(g)/1e3),g=W()?r():null},F=()=>{V(),g=null},z=()=>{g==null&&W()&&(g=r())},j=()=>{let i=0;for(let l in f)i+=f[l];return g!=null&&(i+=oe(g)/1e3),Math.round(i)},ie=()=>{let i={};for(let l in f)i[l]=Math.round(f[l]);return i},E=()=>{k=r(),S&&(S=!1,z())},se=(i,l)=>{d||re({slug:c,event:i,page:l||M,maxPage:u,seconds:j(),sessionId:m})},R=()=>{V();let i={event:\"session\",sessionId:m,numPages:v,maxPage:u,totalSeconds:j(),pagesTime:ie()};d?(i.internal=!0,i.docId=d.docId,i.email=d.email,i.name=d.name,d.it&&(i.it=d.it)):i.slug=c,re(i)},x=(i,l,w,ue)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(l,w,ue),A.push(()=>{i.removeEventListener(l,w,ue)}))};return{sessionId:m,totalSeconds:j,maxPage:()=>u,pageTimes:ie,start(){if(U)return;U=!0,se(\"open\",1),h=1,k=r(),g=W()?r():null,x(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(F(),R()):(E(),z())}),x(n,\"blur\",F),x(n,\"focus\",()=>{E(),z()});for(let w of De)x(n,w,E,{passive:!0});x(e.scrollElement,\"scroll\",E,{passive:!0}),x(n,\"beforeunload\",R);let i=n.setInterval(()=>{!S&&r()-k>s&&(S=!0,F())},5e3),l=n.setInterval(R,a);A.push(()=>{n.clearInterval(i),n.clearInterval(l)})},setPage(i){!i||i===M||(k=r(),V(),h=i,M=i,i>u&&(u=i,se(\"page\",i)))},setPageCount(i){v=i||0},noteActivity:E,flush:R,stop(){for(;A.length;){let i=A.pop();try{i==null||i()}catch(l){}}U=!1}}}var $={};y($,{STORAGE_KEYS:()=>I,attendeeKey:()=>Ge,authorToken:()=>Be,avatarHtml:()=>Z,canModerate:()=>X,createScheduler:()=>Xe,escapeHtml:()=>p,flattenPresence:()=>Fe,formatMessageBody:()=>Y,initials:()=>me,isMentioned:()=>Ze,isMine:()=>T,reactorId:()=>q,shouldNotify:()=>Ye,unreadLabel:()=>qe});var I={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\"};function p(e){return String(e==null?\"\":e).replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\").replace(/\"/g,\"&quot;\")}function me(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function Z(e,n){return e?`<img src=\"${p(e)}\" alt=\"\">`:p(me(n))}function Y(e){let n=p(e);return n=n.replace(/(https?:\\/\\/[^\\s<]+)/g,t=>`<a href=\"${t}\" target=_blank rel=noreferrer class=cm-link>${t}</a>`),n=n.replace(/(^|\\s)@([\\p{L}0-9_'.-]+)/gu,(t,r,o)=>`${r}<span class=cm-mention>@${o}</span>`),n}function Fe(e){let n={},t=[];for(let r in e||{})for(let o of(e||{})[r]||[]){let s=String(o.uid||o.email||o.name||\"\").toLowerCase()||`_${r}_${t.length}`;n[s]?o.role===\"presenter\"&&(n[s]=o):(n[s]=o,t.push(s))}return t.map(r=>n[r])}function ze(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function je(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function Ge(e,n,t=\"anon\",r=()=>Math.random().toString(36).slice(2,10)){if(e!=null&&e.email)return String(e.email).toLowerCase();try{if(!n)return`anon-${t}`;let o=n.getItem(I.attendeeKey);if(o)return o;let s=`anon-${r()}`;return n.setItem(I.attendeeKey,s),s}catch(o){return`anon-${t}`}}function Ke(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(24));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}try{console.warn(\"[player] crypto indisponible : jeton d'auteur affaibli\")}catch(n){}return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}function Be(e,n=Ke){let t=ze(e,I.authorToken);if(t)return t;let r=n();return je(e,I.authorToken,r),r}function q(e){return String(e&&(e.email||e.name)||\"\").toLowerCase()}function T(e,n){return!e||!n?!1:e.author_email&&n.email?e.author_email===n.email:!e.author_email&&!n.email?e.author_name===n.name:!1}function X(e){return!!(e&&e.role===\"presenter\")}function Ze(e,n){if(!n||!n.name)return!1;let t=(n.name.split(/\\s+/)[0]||\"\").toLowerCase();return t?String(e&&e.body||\"\").toLowerCase().includes(`@${t}`)&&!T(e,n):!1}function Ye(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||T(n,t)?!1:o}function qe(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function Xe(e,n={}){var M;let t=(M=n.minMs)!=null?M:300,r=n.now||(()=>Date.now()),o=n.setTimer||((u,v)=>setTimeout(u,v)),s=n.clearTimer||(u=>clearTimeout(u)),a=!1,c=!1,d=-1/0,m=null,b=!1;function f(){if(b||a){c=!0;return}let u=t-(r()-d);if(u>0){h(u);return}a=!0,c=!1,d=r();let v=!1;e(()=>{v||(v=!0,a=!1,c&&!b&&f())})}function h(u){b||m!==null||(m=o(()=>{m=null,f()},Math.max(0,u)))}return{signaler(){b||f()},maintenant(){b||(d=-1/0,f())},arreter(){b=!0,m!==null&&(s(m),m=null)}}}var O={};y(O,{messageClassName:()=>Qe,renderActions:()=>ye,renderAttachment:()=>be,renderMessage:()=>Je,renderReactions:()=>ge});function ge(e,n){let t=e&&e.reactions||{},r=q(n),o=\"\";for(let s in t){let a=t[s]||[];if(!a.length)continue;let c=a.indexOf(r)>=0;o+=`<button class=\"re-chip${c?\" on\":\"\"}\" data-e=\"${p(s)}\">${p(s)} ${a.length}</button>`}return o}function be(e){if(!e||!e.url)return\"\";let n=p(e.url);if(e.kind===\"image\")return`<a class=cm-att href=\"${n}\" target=_blank rel=noreferrer><img src=\"${n}\" alt=\"\" loading=lazy></a>`;let t=p(e.name||\"Document.pdf\");return`<a class=cm-att-pdf href=\"${n}\" target=_blank rel=noreferrer data-pdf=\"${n}\"><span class=cm-att-ph></span><span class=cm-pdflabel>${t}</span></a>`}function ye(e,n){let t=T(e,n.me),r=`<button class=cm-react title=\"R\\xE9agir\">${n.reactIcon||\"\"}</button><button class=cm-reply title=\"R\\xE9pondre\">\\u21A9</button>`;return t&&(r+='<button class=cm-edit title=\"Modifier\">\\u270E</button>'),(t||X(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function Je(e,n){if(e.deleted)return'<span class=a></span><span class=b><div class=\"txt cm-del\">Message supprim\\xE9</div></span>';let t=e.reply_to?`<div class=cm-q><b>${p(e.reply_name||\"\")}</b> ${p(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${Y(e.body)}${r}</div>`:\"\",s=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${Z(e.author_avatar,e.author_name)}</span><span class=b><div class=who><b>${p(e.author_name||\"Invit\\xE9\")}</b>${s}</div>`+t+o+be(e.attachment)+`<div class=cm-re>${ge(e,n.me)}</div></span><span class=cm-act>${ye(e,n)}</span>`}function Qe(e,n,t){return\"cm\"+(T(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var N={};y(N,{DEFAULT_CENTER:()=>ee,DEFAULT_ZOOM:()=>ne,MAP_TYPES:()=>P,cycleMapType:()=>en,initialMapContent:()=>tn,mapTypeLabel:()=>nn,sanitizeContent:()=>te});var P=[\"roadmap\",\"satellite\",\"hybrid\"],ee=[46.6,2.5],ne=6;function _(e){let n=Number(e);return Number.isFinite(n)?n:null}function J(e){if(!Array.isArray(e))return null;let n=_(e[0]),t=_(e[1]);return n!=null&&t!=null?[n,t]:null}var Q=(e,n,t)=>Math.max(n,Math.min(t,e));function te(e){if(!e||typeof e!=\"object\")return null;let n=e;if(n.kind===\"pdf\"||n.kind==null)return null;if(n.kind===\"streetview\"){let t=J(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:_(r.heading)||0,pitch:Q(_(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:Q(_(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:J(n.center)||[...ee],zoom:Q(Math.trunc(_(n.zoom)||ne),1,21),marker:J(n.marker),mapType:P.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function en(e){let n=P.indexOf(e);return P[(n+1)%P.length]}function nn(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function tn(){return{kind:\"map\",center:[...ee],zoom:ne,marker:null,mapType:null,label:null}}var H={};y(H,{CURRENT_PAGE_MARGIN:()=>un,DEFAULT_ASPECT:()=>sn,MAX_ZOOM:()=>on,MIN_PAGE_WIDTH:()=>an,MIN_ZOOM:()=>rn,PRERENDER_MARGIN:()=>ln,arrowState:()=>pn,averageColor:()=>gn,clampPage:()=>he,clampZoom:()=>cn,fitWidth:()=>mn,isImageDocument:()=>bn,progressPercent:()=>dn});var rn=.5,on=3,sn=1.35,an=280,ln=\"500px 0px\",un=\"-48% 0px -48% 0px\";function cn(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function he(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function dn(e,n){return n?Math.round(he(e,n)/n*100):0}function pn(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var fn=28;function mn(e){var a;let n=(a=e.zoom)!=null?a:1,t=Math.max(280,e.containerWidth||900)*n;if(!e.onePage)return t;let r=e.aspect||1.35,s=((e.containerHeight||600)-fn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function gn(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let s=0;s<e.length;s+=4)t+=e[s],r+=e[s+1],o+=e[s+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function bn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var D={};y(D,{presentationTransition:()=>yn,switchDocUrl:()=>hn});function yn(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=te(e.content);if(t)return[{kind:\"show-map\",content:t}];let r=[{kind:\"leave-map\"}];return e.file_url&&e.file_url!==n.docUrl?(r.push({kind:\"switch-doc\",url:e.file_url,name:e.file_name||void 0,title:e.doc_title||void 0,updatedAt:e.updated_at||void 0}),r):(e.current_page&&r.push({kind:\"show-page\",page:e.current_page}),r)}function hn(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return _e(vn);})();", SOURCE_HASH: "5a5b02da1632ec1a" };
7
+ module.exports = { PLAYER_BROWSER_JS: "\"use strict\";var Player=(()=>{var j=Object.defineProperty;var _e=Object.getOwnPropertyDescriptor;var we=Object.getOwnPropertyNames;var Te=Object.prototype.hasOwnProperty;var y=(e,n)=>{for(var t in n)j(e,t,{get:n[t],enumerable:!0})},Ee=(e,n,t,r)=>{if(n&&typeof n==\"object\"||typeof n==\"function\")for(let o of we(n))!Te.call(e,o)&&o!==t&&j(e,o,{get:()=>n[o],enumerable:!(r=_e(n,o))||r.enumerable});return e};var Se=e=>Ee(j({},\"__esModule\",{value:!0}),e);var wn={};y(wn,{bridge:()=>C,chat:()=>N,live:()=>O,presentation:()=>$,presentationState:()=>U,tracking:()=>L,viewer:()=>H});var C={};y(C,{onHostMessage:()=>$e,onPlayerMessage:()=>Ne,parseHostMessage:()=>pe,parsePlayerMessage:()=>de,sendToHost:()=>Ce,sendToPlayer:()=>Le,toWire:()=>K});var G=\"3dd-doc-\",ke=/^[A-Za-z0-9_-]{1,64}$/,Re=[\"close\",\"share\",\"embed-ready\",\"present-left\",\"present-denied\"],Ie=[\"embed-denied\"],Pe=[\"present-invite\",\"present-handover\",\"present-switch\"],Ae=[\"handover-done\"];function K(e){let n={type:G+e.type};return\"slug\"in e&&e.slug&&(n.slug=e.slug),\"reason\"in e&&e.reason&&(n.reason=String(e.reason).slice(0,40)),n}function ce(e,n,t){if(!e||typeof e!=\"object\")return null;let r=e.type;if(typeof r!=\"string\"||!r.startsWith(G))return null;let o=r.slice(G.length);if(n.includes(o))return{type:o};if(Ie.includes(o)){let a=e.reason;return{type:o,reason:/^[a-z-]{1,40}$/.test(String(a||\"\"))?String(a):\"unknown\"}}if(!t.includes(o))return null;let s=e.slug;return typeof s!=\"string\"||!ke.test(s)?null:{type:o,slug:s}}function de(e){return ce(e,Re,Pe)}function pe(e){return ce(e,Ae,[])}function Ce(e,n){try{let t=n||(typeof window!=\"undefined\"?window.parent:null);t&&t.postMessage(K(e),\"*\")}catch(t){}}function Le(e,n){var t;try{(t=e==null?void 0:e.contentWindow)==null||t.postMessage(K(n),\"*\")}catch(r){}}function Oe(e,n){var r;if(!e)return!0;let t=(r=e.contentWindow)!=null?r:e;return n===t}function Ne(e,n,t){let r=n||(typeof window!=\"undefined\"?window:null);if(!r)return()=>{};let o=s=>{if(!Oe(t,s.source))return;let a=de(s.data);a&&e(a)};return r.addEventListener(\"message\",o),()=>r.removeEventListener(\"message\",o)}function $e(e,n){var s;let t=n||(typeof window!=\"undefined\"?window:null);if(!t)return()=>{};let r=(s=t.parent)!=null?s:null,o=a=>{if(!r||r===t||a.source!==r)return;let c=pe(a.data);c&&e(c)};return t.addEventListener(\"message\",o),()=>t.removeEventListener(\"message\",o)}var L={};y(L,{createTracker:()=>Ke});var He=Math.ceil(300),fe=25,En=He*fe,me=18e4,Ue=25e3,De=Math.ceil(36e5/Ue),We=720,Ve=De+We,Sn=Ve*fe;var ze=[\"mousemove\",\"mousedown\",\"keydown\",\"wheel\",\"touchstart\",\"pointerdown\"];function je(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(16));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}return Math.random().toString(36).slice(2)+Date.now().toString(36)}function Ge(e,n){return t=>{try{let r=JSON.stringify(t),o=n.navigator;o&&typeof o.sendBeacon==\"function\"?o.sendBeacon(e,new Blob([r],{type:\"application/json\"})):n.fetch(e,{method:\"POST\",headers:{\"Content-Type\":\"application/json\"},body:r,keepalive:!0})}catch(r){}}}function Ke(e={}){var ae,le;let n=e.win||(typeof window!=\"undefined\"?window:null),t=e.doc||n&&n.document,r=e.now||(()=>Date.now()),o=e.endpoint||\"/api/doc\",s=(ae=e.idleMs)!=null?ae:me,a=(le=e.sessionEveryMs)!=null?le:12e3,c=e.slug||\"\",d=e.internal||null,m=e.sessionId||je(),b=e.send||Ge(o,n),f={},h=0,M=0,u=0,v=0,g=null,T=r(),E=!1,D=!1,P=[],xe=()=>!!c||!!d,te=i=>{if(xe())try{b(i)}catch(l){}},W=()=>!!t&&t.visibilityState===\"visible\"&&!E,re=i=>{let l=r()-i,x=Math.max(0,T-i)+s;return Math.max(0,Math.min(l,x))},V=()=>{g!=null&&h>0&&(f[h]=(f[h]||0)+re(g)/1e3),g=W()?r():null},oe=()=>{V(),g=null},F=()=>{g==null&&W()&&(g=r())},z=()=>{let i=0;for(let l in f)i+=f[l];return g!=null&&(i+=re(g)/1e3),Math.round(i)},ie=()=>{let i={};for(let l in f)i[l]=Math.round(f[l]);return i},S=()=>{T=r(),E&&(E=!1,F())},se=(i,l)=>{d||te({slug:c,event:i,page:l||M,maxPage:u,seconds:z(),sessionId:m})},A=()=>{V();let i={event:\"session\",sessionId:m,numPages:v,maxPage:u,totalSeconds:z(),pagesTime:ie()};d?(i.internal=!0,i.docId=d.docId,i.email=d.email,i.name=d.name,d.it&&(i.it=d.it)):i.slug=c,te(i)},k=(i,l,x,ue)=>{!i||typeof i.addEventListener!=\"function\"||(i.addEventListener(l,x,ue),P.push(()=>{i.removeEventListener(l,x,ue)}))};return{sessionId:m,totalSeconds:z,maxPage:()=>u,pageTimes:ie,start(){if(D)return;D=!0,se(\"open\",1),h=1,T=r(),g=W()?r():null,k(t,\"visibilitychange\",()=>{t.visibilityState===\"hidden\"?(oe(),A()):(S(),F())}),k(n,\"focus\",()=>{S(),F()});for(let x of ze)k(n,x,S,{passive:!0});k(e.scrollElement,\"scroll\",S,{passive:!0}),k(n,\"beforeunload\",A);let i=n.setInterval(()=>{!E&&r()-T>s&&(E=!0,oe())},5e3),l=n.setInterval(A,a);P.push(()=>{n.clearInterval(i),n.clearInterval(l)})},setPage(i){!i||i===M||(T=r(),V(),h=i,M=i,i>u&&(u=i,se(\"page\",i)))},setPageCount(i){v=i||0},noteActivity:S,flush:A,stop(){for(;P.length;){let i=P.pop();try{i==null||i()}catch(l){}}D=!1}}}var O={};y(O,{STORAGE_KEYS:()=>R,attendeeKey:()=>qe,authorToken:()=>Xe,avatarHtml:()=>Y,canModerate:()=>q,createScheduler:()=>nn,escapeHtml:()=>p,flattenPresence:()=>Ye,formatMessageBody:()=>B,initials:()=>ge,isMentioned:()=>Qe,isMine:()=>_,reactorId:()=>Z,shouldNotify:()=>Je,unreadLabel:()=>en});var R={attendeeKey:\"3dd-present-attkey\",authorToken:\"3dd-present-authtoken\",chatMuted:\"3dd-present-mute\"};function p(e){return String(e==null?\"\":e).replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/>/g,\"&gt;\").replace(/\"/g,\"&quot;\")}function ge(e){let n=(e||\"?\").trim().split(/\\s+/);return(((n[0]||\"\")[0]||\"?\")+((n[1]||\"\")[0]||\"\")).toUpperCase()}function Y(e,n){return e?`<img src=\"${p(e)}\" alt=\"\">`:p(ge(n))}function B(e){let n=p(e);return n=n.replace(/(https?:\\/\\/[^\\s<]+)/g,t=>`<a href=\"${t}\" target=_blank rel=noreferrer class=cm-link>${t}</a>`),n=n.replace(/(^|\\s)@([\\p{L}0-9_'.-]+)/gu,(t,r,o)=>`${r}<span class=cm-mention>@${o}</span>`),n}function Ye(e){let n={},t=[];for(let r in e||{})for(let o of(e||{})[r]||[]){let s=String(o.uid||o.email||o.name||\"\").toLowerCase()||`_${r}_${t.length}`;n[s]?o.role===\"presenter\"&&(n[s]=o):(n[s]=o,t.push(s))}return t.map(r=>n[r])}function Be(e,n){try{return e?e.getItem(n):null}catch(t){return null}}function Ze(e,n,t){try{e==null||e.setItem(n,t)}catch(r){}}function qe(e,n=\"anon\",t=be){try{if(!e)return`anon-${n}`;let r=e.getItem(R.attendeeKey);if(r)return r;let o=`anon-${t()}`;return e.setItem(R.attendeeKey,o),o}catch(r){return`anon-${n}`}}function be(){let e=typeof globalThis!=\"undefined\"?globalThis.crypto:void 0;if(e&&typeof e.randomUUID==\"function\")return e.randomUUID().replace(/-/g,\"\");if(e&&typeof e.getRandomValues==\"function\"){let n=e.getRandomValues(new Uint8Array(24));return Array.from(n,t=>t.toString(16).padStart(2,\"0\")).join(\"\")}try{console.warn(\"[player] crypto indisponible : jeton d'auteur affaibli\")}catch(n){}return Math.random().toString(36).slice(2)+Math.random().toString(36).slice(2)}function Xe(e,n=be){let t=Be(e,R.authorToken);if(t)return t;let r=n();return Ze(e,R.authorToken,r),r}function Z(e){return String(e&&(e.email||e.name)||\"\").toLowerCase()}function _(e,n){return!e||!n?!1:e.author_email&&n.email?e.author_email===n.email:!e.author_email&&!n.email?e.author_name===n.name:!1}function q(e){return!!(e&&e.role===\"presenter\")}function Qe(e,n){if(!n||!n.name)return!1;let t=(n.name.split(/\\s+/)[0]||\"\").toLowerCase();return t?String(e&&e.body||\"\").toLowerCase().includes(`@${t}`)&&!_(e,n):!1}function Je(e){let{msg:n,me:t,historyLoaded:r,chatHidden:o}=e;return!r||!n||n.deleted||_(n,t)?!1:o}function en(e){return!e||e<=0?\"\":e>9?\"9+\":String(e)}function nn(e,n={}){var M;let t=(M=n.minMs)!=null?M:300,r=n.now||(()=>Date.now()),o=n.setTimer||((u,v)=>setTimeout(u,v)),s=n.clearTimer||(u=>clearTimeout(u)),a=!1,c=!1,d=-1/0,m=null,b=!1;function f(){if(b||a){c=!0;return}let u=t-(r()-d);if(u>0){h(u);return}a=!0,c=!1,d=r();let v=!1;e(()=>{v||(v=!0,a=!1,c&&!b&&f())})}function h(u){b||m!==null||(m=o(()=>{m=null,f()},Math.max(0,u)))}return{signaler(){b||f()},maintenant(){b||(d=-1/0,f())},arreter(){b=!0,m!==null&&(s(m),m=null)}}}var N={};y(N,{messageClassName:()=>rn,renderActions:()=>ve,renderAttachment:()=>he,renderMessage:()=>tn,renderReactions:()=>ye});function ye(e,n){let t=e&&e.reactions||{},r=Z(n),o=\"\";for(let s in t){let a=t[s]||[];if(!a.length)continue;let c=a.indexOf(r)>=0;o+=`<button class=\"re-chip${c?\" on\":\"\"}\" data-e=\"${p(s)}\">${p(s)} ${a.length}</button>`}return o}function he(e){if(!e||!e.url)return\"\";let n=p(e.url);if(e.kind===\"image\")return`<a class=cm-att href=\"${n}\" target=_blank rel=noreferrer><img src=\"${n}\" alt=\"\" loading=lazy></a>`;let t=p(e.name||\"Document.pdf\");return`<a class=cm-att-pdf href=\"${n}\" target=_blank rel=noreferrer data-pdf=\"${n}\"><span class=cm-att-ph></span><span class=cm-pdflabel>${t}</span></a>`}function ve(e,n){let t=_(e,n.me),r=`<button class=cm-react title=\"R\\xE9agir\">${n.reactIcon||\"\"}</button><button class=cm-reply title=\"R\\xE9pondre\">\\u21A9</button>`;return t&&(r+='<button class=cm-edit title=\"Modifier\">\\u270E</button>'),(t||q(n.me))&&(r+='<button class=cm-del-btn title=\"Supprimer\">\\u{1F5D1}</button>'),r}function tn(e,n){if(e.deleted)return'<span class=a></span><span class=b><div class=\"txt cm-del\">Message supprim\\xE9</div></span>';let t=e.reply_to?`<div class=cm-q><b>${p(e.reply_name||\"\")}</b> ${p(e.reply_text||\"\")}</div>`:\"\",r=e.edited?\" <span class=cm-ed>(modifi\\xE9)</span>\":\"\",o=e.body?`<div class=txt>${B(e.body)}${r}</div>`:\"\",s=e.is_presenter?\" <span class=tag>pr\\xE9sentateur</span>\":\"\";return`<span class=a>${Y(e.author_avatar,e.author_name)}</span><span class=b><div class=who><b>${p(e.author_name||\"Invit\\xE9\")}</b>${s}</div>`+t+o+he(e.attachment)+`<div class=cm-re>${ye(e,n.me)}</div></span><span class=cm-act>${ve(e,n)}</span>`}function rn(e,n,t){return\"cm\"+(_(e,n)?\" me\":\"\")+(e.deleted?\" isdel\":\"\")+(t?\" mentioned\":\"\")}var $={};y($,{DEFAULT_CENTER:()=>J,DEFAULT_ZOOM:()=>ee,MAP_TYPES:()=>I,cycleMapType:()=>on,initialMapContent:()=>an,mapTypeLabel:()=>sn,sanitizeContent:()=>ne});var I=[\"roadmap\",\"satellite\",\"hybrid\"],J=[46.6,2.5],ee=6;function w(e){let n=Number(e);return Number.isFinite(n)?n:null}function X(e){if(!Array.isArray(e))return null;let n=w(e[0]),t=w(e[1]);return n!=null&&t!=null?[n,t]:null}var Q=(e,n,t)=>Math.max(n,Math.min(t,e));function ne(e){if(!e||typeof e!=\"object\")return null;let n=e;if(n.kind===\"pdf\"||n.kind==null)return null;if(n.kind===\"streetview\"){let t=X(n.position);if(!t)return null;let r=n.pov&&typeof n.pov==\"object\"?n.pov:null,o=r?{heading:w(r.heading)||0,pitch:Q(w(r.pitch)||0,-90,90)}:{heading:0,pitch:0};return{kind:\"streetview\",position:t,pov:o,zoom:Q(w(n.zoom)||1,0,5)}}return n.kind!==\"map\"?null:{kind:\"map\",center:X(n.center)||[...J],zoom:Q(Math.trunc(w(n.zoom)||ee),1,21),marker:X(n.marker),mapType:I.includes(n.mapType)?n.mapType:null,label:String(n.label||\"\").slice(0,160)||null}}function on(e){let n=I.indexOf(e);return I[(n+1)%I.length]}function sn(e){return e===\"roadmap\"?\"\\u{1F6F0} Satellite\":e===\"satellite\"?\"\\u{1F5FA} Hybride\":\"\\u{1F5FA} Plan\"}function an(){return{kind:\"map\",center:[...J],zoom:ee,marker:null,mapType:null,label:null}}var H={};y(H,{CURRENT_PAGE_MARGIN:()=>fn,DEFAULT_ASPECT:()=>cn,MAX_ZOOM:()=>un,MIN_PAGE_WIDTH:()=>dn,MIN_ZOOM:()=>ln,PRERENDER_MARGIN:()=>pn,arrowState:()=>bn,averageColor:()=>vn,clampPage:()=>Me,clampZoom:()=>mn,fitWidth:()=>hn,isImageDocument:()=>Mn,progressPercent:()=>gn});var ln=.5,un=3,cn=1.35,dn=280,pn=\"500px 0px\",fn=\"-48% 0px -48% 0px\";function mn(e){let n=Math.round((Number(e)||0)*10)/10;return Math.max(.5,Math.min(3,n))}function Me(e,n){return Math.max(1,Math.min(n||1,Number(e)||1))}function gn(e,n){return n?Math.round(Me(e,n)/n*100):0}function bn(e,n){return{prevDisabled:e<=1,nextDisabled:!!n&&e>=n}}var yn=28;function hn(e){var a;let n=(a=e.zoom)!=null?a:1,t=Math.max(280,e.containerWidth||900)*n;if(!e.onePage)return t;let r=e.aspect||1.35,s=((e.containerHeight||600)-yn-(e.overlap||0)-(e.reserve||0))/r;return s>120?Math.min(t,s):t}function vn(e){let n=e.length/4;if(!n)return\"rgb(0,0,0)\";let t=0,r=0,o=0;for(let s=0;s<e.length;s+=4)t+=e[s],r+=e[s+1],o+=e[s+2];return`rgb(${Math.round(t/n)},${Math.round(r/n)},${Math.round(o/n)})`}function Mn(e,n){return/[.](png|jpe?g|webp|gif|avif)($|[?])/i.test(String(e||n||\"\"))}var U={};y(U,{presentationTransition:()=>xn,switchDocUrl:()=>_n});function xn(e,n={}){if(!e)return[{kind:\"nothing\"}];if(e.active===!1)return[{kind:\"ended\"}];let t=ne(e.content);if(t)return[{kind:\"show-map\",content:t}];let r=[{kind:\"leave-map\"}];return e.file_url&&e.file_url!==n.docUrl?(r.push({kind:\"switch-doc\",url:e.file_url,name:e.file_name||void 0,title:e.doc_title||void 0,updatedAt:e.updated_at||void 0}),r):(e.current_page&&r.push({kind:\"show-page\",page:e.current_page}),r)}function _n(e,n){let t=encodeURIComponent(n||String(Date.now()));return`${e}${e.includes(\"?\")?\"&\":\"?\"}v=${t}`}return Se(wn);})();", SOURCE_HASH: "bcdddbf20bceaa1b" };
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 { SESSION_QUOTA_PER_HOUR } = require("./shared.generated.js");
9
+ const { SESSION_QUOTA_PER_HOUR, PRESENT_QUOTA_PER_HOUR } = require("./shared.generated.js");
10
10
  const { createPresentation, getPresentation, setPage, endPresentation, addMessage, listMessages, toggleReaction, editMessage, deleteMessage, setChatLock, createUploadUrl, reclaimPresentation, touchPresentation, listActivePresentations, handoverPresentation, endPresentationByOwner, recordAttendance, presentationStats, listPresentationsForDoc, switchPresentationDoc, setPresentationContent } = require("./presentations");
11
11
  // CONTEXTE INJECTÉ : tout ce que le player emprunte à l'application hôte passe par ici — stockage,
12
12
  // base, identité, limites, marque, journalisation — et rien d'autre. C'est la frontière qui permettra
@@ -282,14 +282,14 @@ var Live=(function(){
282
282
  var MYID=Math.random().toString(36).slice(2,9);
283
283
  function _store(){try{return window.localStorage;}catch(e){return null;}}
284
284
  // Clé d'assistance STABLE (analytics de présentation) : email si connu, sinon id persistant par navigateur.
285
- function attKey(me){ return Player.live.attendeeKey(me,_store(),MYID); }
285
+ function attKey(){ return Player.live.attendeeKey(_store(),MYID); }
286
286
  // Heartbeat d'assistance → le serveur journalise qui suit, combien de temps, et les pages vues (via la page
287
287
  // courante de la présentation). Envoyé à la connexion puis toutes les 25 s. Best-effort (silencieux).
288
288
  // Le serveur ne croit plus 'isMember' ni 'isPresenter' sur parole : l'appartenance se prouve par
289
289
  // le jeton d'acces de la session, le titre de presentateur par le control_token. Ce qui part d'ici
290
290
  // n'est plus qu'une AFFIRMATION, et le serveur la remplace par ce qu'il a verifie.
291
291
  function sendAttend(){ if(!SLUG||!ME)return; try{ var h={'Content-Type':'application/json'};var jw=accessToken();if(jw)h.Authorization='Bearer '+jw;
292
- fetch('/api/doc',{method:'POST',headers:h,body:JSON.stringify({action:'present-attend',slug:SLUG,control:CONTROL,key:attKey(ME),name:ME.name||'',email:ME.email||'',avatar:ME.avatar||''})}); }catch(e){} }
292
+ 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){} }
293
293
  var EMOJIS=['👍','❤️','😂','😮','👏','🎉'];
294
294
  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>';
295
295
  function esc(s){return Player.live.escapeHtml(s);}
@@ -533,7 +533,7 @@ var Live=(function(){
533
533
  // par accident : le jour où quelqu'un rebranche un paramètre, la charge d'un canal public
534
534
  // redeviendrait crue sans que rien ne le signale. On coupe le chemin, pas seulement l'usage.
535
535
  ch.on('broadcast',{event:'map'},function(){if(_onMap)_onMap();});
536
- ch.subscribe(function(st){if(st==='SUBSCRIBED'){ch.track({name:me.name,email:me.email,avatar:me.avatar,role:me.role,member:!!me.member,uid:attKey(me)});sendAttend();}});
536
+ ch.subscribe(function(st){if(st==='SUBSCRIBED'){ch.track({name:me.name,email:me.email,avatar:me.avatar,role:me.role,member:!!me.member,uid:attKey()});sendAttend();}});
537
537
  _tyIv=setInterval(renderTyping,1500);
538
538
  _atIv=setInterval(sendAttend,25000);
539
539
  // Filet de sécurité : au déchargement de la page/iframe (fermeture, reload, switch), on retire la présence
@@ -629,7 +629,13 @@ var Map3DD=(function(){
629
629
  // prix pour que personne d'autre que le présentateur ne pilote l'écran de l'audience.
630
630
  var _ordPersist=null;
631
631
  function persistOrd(){ if(!_ordPersist&&window.Player&&Player.live&&Player.live.createScheduler){
632
- _ordPersist=Player.live.createScheduler(function(fini){ try{ if(map&&persist)persist(state()); }catch(e){} fini(); },{minMs:500}); }
632
+ _ordPersist=Player.live.createScheduler(function(fini){
633
+ // ⚠️ ON ATTEND L'ÉCRITURE. Appeler fini() tout de suite disait à l'ordonnanceur que le
634
+ // travail était fait alors qu'il commençait : sa garantie du « dernier état gagne »
635
+ // portait sur l'ordre des appels et non sur celui des arrivées.
636
+ var p=null; try{ if(map&&persist)p=persist(state()); }catch(e){}
637
+ if(p&&p.then)p.then(fini,fini); else fini();
638
+ },{minMs:500}); }
633
639
  return _ordPersist; }
634
640
  function broadcast(){ if(!map)return; var n=Date.now(); if(n-_bcT<200)return; _bcT=n;
635
641
  var o=persistOrd(); if(o)o.signaler();
@@ -676,7 +682,10 @@ var Map3DD=(function(){
676
682
  // Même règle qu'au-dessus : on persiste, on signale, on ne transporte pas la position.
677
683
  var _ordSv=null;
678
684
  function svOrd(){ if(!_ordSv&&window.Player&&Player.live&&Player.live.createScheduler){
679
- _ordSv=Player.live.createScheduler(function(fini){ try{ if(persist&&pano)persist(svState()); }catch(e){} fini(); },{minMs:500}); }
685
+ _ordSv=Player.live.createScheduler(function(fini){
686
+ var p=null; try{ if(persist&&pano)p=persist(svState()); }catch(e){}
687
+ if(p&&p.then)p.then(fini,fini); else fini();
688
+ },{minMs:500}); }
680
689
  return _ordSv; }
681
690
  function svBcast(){ var n=Date.now(); if(n-_svBcT<200)return; _svBcT=n; if(!svState())return;
682
691
  var o=svOrd(); if(o)o.signaler();
@@ -1362,6 +1371,25 @@ const CLE_INVITE = "dmp-present-me";
1362
1371
  *
1363
1372
  * Signalé par la seconde passe d'audit (P1-6).
1364
1373
  */
1374
+ /**
1375
+ * La clé d'un participant qui ne peut RIEN prouver.
1376
+ *
1377
+ * Un anonyme n'a pas de jeton : sa clé vient forcément de son navigateur, et le serveur ne peut pas
1378
+ * la lui contester. Ce qu'il peut faire, c'est l'empêcher de sortir de son espace de noms — donc de
1379
+ * ressembler à la clé d'un membre, qui est une adresse e-mail.
1380
+ *
1381
+ * ⚠️ C'est la même logique que le quota fondé sur l'adresse : on ne fonde pas une garantie sur ce
1382
+ * que l'appelant choisit. Ici on ne peut pas éviter qu'il choisisse ; on peut borner ce qu'il peut
1383
+ * choisir. Deux anonymes restent séparés par une valeur imprévisible, comme deux sessions.
1384
+ *
1385
+ * Une clé absente ou hors forme n'est pas une erreur : elle vaut « inconnu ». Un participant mal
1386
+ * compté vaut mieux qu'une audience refusée.
1387
+ */
1388
+ function cleAnonyme(brut) {
1389
+ const v = String(brut == null ? "" : brut).trim().slice(0, 120);
1390
+ return /^anon-[A-Za-z0-9_-]{4,}$/.test(v) ? v : "anon-inconnu";
1391
+ }
1392
+
1365
1393
  async function profilDuJeton(req) {
1366
1394
  try {
1367
1395
  const u = await PLAYER.identity.verifyToken((req.headers && req.headers.authorization) || "");
@@ -2033,12 +2061,52 @@ ${LEGAL_CSS}
2033
2061
  //
2034
2062
  // sendBeacon ne s'attend pas, mais il rend la main une fois la requête MISE EN FILE : signaler
2035
2063
  // juste après, puis couper, respecte l'ordre autant que ce transport le permet.
2036
- function endPresent(){ if(!PRES)return; var p=PRES; PRES=null; clearInterval(_hbIv); clearCtl(p.slug);
2037
- var pb=document.getElementById('pbar'); if(pb)pb.style.display='none';
2038
- try{ var b=JSON.stringify({action:'present-end',slug:p.slug,control:p.control});
2039
- if(navigator.sendBeacon){navigator.sendBeacon('/api/doc',new Blob([b],{type:'application/json'}));}
2040
- else {fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:b,keepalive:true});} }catch(e){}
2041
- try{ if(window.Live){ Live.sendState(); Live.disconnect(); } }catch(e){} }
2064
+ // FERMER UNE PRÉSENTATION EST UN ACTE, PAS UNE INTENTION.
2065
+ //
2066
+ // ⚠️ Cette fonction annonçait la fin avant de l'obtenir. « sendBeacon » ne rend AUCUNE réponse —
2067
+ // ni « enregistré » ni « refusé » — et pourtant l'interface se nettoyait, le jeton de contrôle
2068
+ // était effacé et le canal fermé dans la foulée. Si l'appel échouait, la présentation restait
2069
+ // VIVANTE pour l'audience pendant que le présentateur la croyait close ; et il n'avait même plus
2070
+ // de quoi la refermer, « clearCtl » ayant déjà jeté son jeton.
2071
+ //
2072
+ // ⚠️ Le beacon n'achetait rien ici : il sert à faire partir une requête pendant que la page
2073
+ // MEURT. Or le seul appelant était un bouton — on a tout le temps d'attendre. Il est désormais à
2074
+ // sa place, sur « pagehide », et là seulement.
2075
+ //
2076
+ // ⚠️ ET L'ORDRE COMPTE : l'audience ne relit l'état qu'APRÈS le 2xx. Diffuser avant, c'était lui
2077
+ // faire relire une présentation encore marquée active — elle rejouait donc l'ancien état.
2078
+ //
2079
+ // On n'efface donc rien tant que le serveur n'a pas confirmé : en cas d'échec le pilotage reste
2080
+ // entier et le présentateur peut réessayer. Une présentation vraiment abandonnée est rattrapée
2081
+ // par la péremption (STALE_MS, 3 min), qui existe exactement pour ce cas.
2082
+ //
2083
+ // Signalé par un audit externe.
2084
+ function endPresent(){
2085
+ if(!PRES) return Promise.resolve();
2086
+ var p=PRES, btn=document.getElementById('pbarEnd'), libelle=btn?btn.textContent:'';
2087
+ if(btn){ btn.disabled=true; btn.textContent='…'; }
2088
+ return fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},keepalive:true,
2089
+ body:JSON.stringify({action:'present-end',slug:p.slug,control:p.control})})
2090
+ .then(function(r){ if(!r||!r.ok) throw new Error('present-end'); })
2091
+ .then(function(){
2092
+ PRES=null; clearInterval(_hbIv); clearCtl(p.slug);
2093
+ var pb=document.getElementById('pbar'); if(pb)pb.style.display='none';
2094
+ try{ if(window.Live){ Live.sendState(); Live.disconnect(); } }catch(e){}
2095
+ }, function(){
2096
+ if(btn){ btn.disabled=false; btn.textContent=libelle||'Terminer'; btn.title='La fin n’a pas été enregistrée — réessayez.'; }
2097
+ });
2098
+ }
2099
+ // ⚠️ LE SEUL ENDROIT OÙ « sendBeacon » A UN SENS : la page s'en va, aucune réponse ne pourra être
2100
+ // lue, et une présentation laissée ouverte ferait suivre l'audience dans le vide jusqu'à la
2101
+ // péremption. On ne nettoie rien ici — la page disparaît de toute façon.
2102
+ window.addEventListener('pagehide',function(){
2103
+ if(!PRES) return;
2104
+ try{
2105
+ var b=JSON.stringify({action:'present-end',slug:PRES.slug,control:PRES.control});
2106
+ if(navigator.sendBeacon) navigator.sendBeacon('/api/doc',new Blob([b],{type:'application/json'}));
2107
+ else fetch('/api/doc',{method:'POST',headers:{'Content-Type':'application/json'},body:b,keepalive:true});
2108
+ }catch(e){}
2109
+ });
2042
2110
  // Transfert : je passe la main → je cesse de piloter SANS clôturer la présentation (le nouvel owner reprendra).
2043
2111
  function stopPilotingLocally(){ if(!PRES)return; var s=PRES.slug; PRES=null; clearInterval(_hbIv); clearCtl(s); var pb=document.getElementById('pbar'); if(pb)pb.style.display='none'; try{ if(window.Live) Live.disconnect(); }catch(e){} }
2044
2112
  function liveConnect(slug,control){ try{ if(window.Live){ var P=CFG.present||{}; Live.connect(slug,{name:P.by||'Présentateur',email:P.email||'',avatar:P.av||'',role:'presenter',member:true},control); } }catch(e){} }
@@ -2068,9 +2136,17 @@ ${LEGAL_CSS}
2068
2136
  var tries=0; (function jump(){ var el=(document.getElementById('pages')||document).querySelector('.page[data-p="'+target+'"]'); if(el){ el.scrollIntoView({block:'start'}); } else if(tries++<40){ setTimeout(jump,150); } })();
2069
2137
  }).catch(function(){});
2070
2138
  }
2071
- // Carte live : persiste le contenu (present-content, JWT) → l'audience bascule/suit via Realtime.
2072
- function presentContent(content){ if(!PRES)return; var h={'Content-Type':'application/json'}; var tk=appToken(); if(tk) h['Authorization']='Bearer '+tk;
2073
- fetch('/api/doc',{method:'POST',headers:h,body:JSON.stringify({action:'present-content',slug:PRES.slug,content:content})})
2139
+ // Carte live : persiste le contenu (present-content) → l'audience bascule/suit via Realtime.
2140
+ // On envoie le jeton de contrôle EN PLUS de la session : une présentation démarrée sans
2141
+ // session pilote sa carte comme elle tourne ses pages. Sans lui, l'appel repartait en 401,
2142
+ // avale par le .catch ci-dessous, et la carte ne suivait pas sans que rien ne le dise.
2143
+ // ⚠️ REND SA PROMESSE, ET CE N'EST PAS DÉCORATIF. L'ordonnanceur des écritures de carte
2144
+ // appelait fini() juste après l'appel, sans attendre : il croyait donc l'écriture terminée
2145
+ // alors qu'elle venait de PARTIR. Sa garantie « une seule en vol, la dernière gagne » ne
2146
+ // s'appliquait qu'à l'ordre des APPELS, pas à celui des écritures — plusieurs PATCH pouvaient
2147
+ // voler ensemble, et une position ancienne atterrir après une récente.
2148
+ function presentContent(content){ if(!PRES)return Promise.resolve(); var h={'Content-Type':'application/json'}; var tk=appToken(); if(tk) h['Authorization']='Bearer '+tk;
2149
+ return fetch('/api/doc',{method:'POST',headers:h,body:JSON.stringify({action:'present-content',slug:PRES.slug,control:PRES.control,content:content})})
2074
2150
  .then(function(r){ if(r&&r.ok)diffuserEtat(); })
2075
2151
  .catch(function(){}); }
2076
2152
  function showMap(){ if(!PRES||!window.Map3DD)return; var wrap=document.getElementById('mapWrap'); if(wrap&&wrap.classList.contains('on')){ Map3DD.enter(null,true,presentContent); return; } var init=Player.presentation.initialMapContent(); presentContent(init); Map3DD.enter(init,true,presentContent); }
@@ -2784,8 +2860,18 @@ async function handler(req, res) {
2784
2860
  // ⚠️ Une identité prouvée REMPLACE celle qu'on affirme — elle ne s'y ajoute pas. Sinon la
2785
2861
  // vérification ne servirait qu'à décorer une affirmation qu'on croit toujours.
2786
2862
  const profil = await profilDuJeton(req);
2863
+ // ⚠️ ET LA CLÉ EN FAIT PARTIE — C'EST MÊME LA SEULE QUI COMPTE. Le correctif ci-dessus a
2864
+ // remplacé le nom, l'e-mail et l'avatar par ceux du jeton, et laissé passer `key`, qui est
2865
+ // pourtant l'IDENTITÉ DE LA LIGNE. Or pour un membre cette clé EST son e-mail : un
2866
+ // participant anonyme n'avait qu'à poster la vôtre pour écraser votre ligne — nom, avatar —
2867
+ // et gonfler votre temps de présence. Ce sont les statistiques de présentation, c'est-à-dire
2868
+ // ce que ce produit vend.
2869
+ //
2870
+ // On dérive donc la clé de ce qui est prouvé, et jamais de ce qui est affirmé. Un anonyme,
2871
+ // lui, ne peut rien prouver : sa clé reste la sienne, mais enfermée dans un espace de noms
2872
+ // dont elle ne peut pas sortir — elle ne pourra jamais ressembler à l'e-mail d'un membre.
2787
2873
  const r = await recordAttendance(String(body.slug || ""), {
2788
- key: body.key,
2874
+ key: profil ? profil.email : cleAnonyme(body.key),
2789
2875
  name: (profil && profil.name) || body.name,
2790
2876
  email: profil ? profil.email : body.email,
2791
2877
  avatar: (profil && profil.avatar) || body.avatar,
@@ -2799,8 +2885,18 @@ async function handler(req, res) {
2799
2885
  const jp = (status, obj) => { res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(obj)); };
2800
2886
  try {
2801
2887
  const u = await PLAYER.identity.verifyToken(req.headers.authorization);
2802
- if (!u || !u.email) return jp(401, { ok: false, error: "auth" });
2803
- const isAdmin = PLAYER.identity.isAdmin(u);
2888
+ // ⚠️ UNE EXCEPTION, ET UNE SEULE. `present-content` pilote ce que la présentation AFFICHE,
2889
+ // exactement comme `present-page` tourne les pages — et `present-page` se contente du
2890
+ // `control_token`, sans session. Exiger un JWT ici cassait les présentations démarrées
2891
+ // sans session, que `present-start` autorise pourtant : la carte ne suivait pas, en
2892
+ // silence. Le jeton de contrôle est vérifié plus bas, contre la ligne de la présentation.
2893
+ //
2894
+ // ⚠️ Les autres actions de ce groupe restent réservées à une session, et `present-switch`
2895
+ // — changer le DOCUMENT montré — au propriétaire : décider ce qu'on montre n'est pas
2896
+ // piloter l'affichage.
2897
+ const pilotage = body.action === "present-content" && !!body.control;
2898
+ if ((!u || !u.email) && !pilotage) return jp(401, { ok: false, error: "auth" });
2899
+ const isAdmin = !!u && PLAYER.identity.isAdmin(u);
2804
2900
  let r;
2805
2901
  if (body.action === "present-list") r = { ok: true, presentations: await listActivePresentations(u.email) };
2806
2902
  else if (body.action === "present-reclaim") r = await reclaimPresentation(String(body.slug || ""), u.email);
@@ -2811,7 +2907,7 @@ async function handler(req, res) {
2811
2907
  if (!isAllowedStorageUrl(String(body.fileUrl || ""))) return jp(400, { ok: false, error: "url" });
2812
2908
  r = await switchPresentationDoc(String(body.slug || ""), u.email, isAdmin, { fileUrl: body.fileUrl, fileName: body.fileName, docTitle: body.docTitle, docId: body.docId });
2813
2909
  }
2814
- else if (body.action === "present-content") r = await setPresentationContent(String(body.slug || ""), u.email, isAdmin, body.content);
2910
+ else if (body.action === "present-content") r = await setPresentationContent(String(body.slug || ""), (u && u.email) || "", isAdmin, body.content, String(body.control || ""));
2815
2911
  else r = await handoverPresentation(String(body.slug || ""), u.email, body.newOwner);
2816
2912
  return jp(r.ok ? 200 : (r.status || 400), r);
2817
2913
  } catch { return jp(500, { ok: false }); }
@@ -3281,6 +3377,36 @@ async function handler(req, res) {
3281
3377
  // Mode « Présenter » côté AUDIENCE : `?present=<slug>` → page live (suit le présentateur via Realtime) ;
3282
3378
  // `&file=1` → stream le PDF de la présentation (Range, même origine pour pdf.js).
3283
3379
  if (q.present) {
3380
+ // ⚠️ LA GARDE PASSE DEVANT L'INTERROGATION DE BASE, PAS DERRIÈRE.
3381
+ //
3382
+ // `state=1` et `chat=1` sont servis sans session, sur un lien public, et chaque appel coûte
3383
+ // une requête base. Une boucle sur une URL connue faisait donc d'un lien de présentation un
3384
+ // amplificateur : une requête HTTP triviale contre une requête base, autant de fois qu'on
3385
+ // veut. Douze actions d'écriture étaient limitées ; ces deux LECTURES ne l'étaient pas.
3386
+ //
3387
+ // ⚠️ Et c'est la ressource PARTAGÉE qui paie : la base est la même pour tous les documents de
3388
+ // l'instance. Le coût d'un abus ne retombe pas sur celui qui le commet.
3389
+ //
3390
+ // Placée AVANT `getPresentation` — écrite après, elle aurait laissé passer très exactement la
3391
+ // requête qu'elle est censée épargner. Le quota se déduit de la cadence de l'audience
3392
+ // (`src/cadence.ts`), il n'est pas choisi à la main.
3393
+ const sondage = String(q.state || "") === "1" || String(q.chat || "") === "1";
3394
+ if (sondage) {
3395
+ const ipSondage = adresseAppelant(req) || "anon";
3396
+ if (!(await PLAYER.limits.allow(`pread:${ipSondage}`, PRESENT_QUOTA_PER_HOUR, 3600))) {
3397
+ // ⚠️ UN REFUS MUET FAIT UNE AUDIENCE QUI DÉCROCHE SANS CAUSE NOMMÉE. Le 429 n'apparaît
3398
+ // que dans la console du spectateur ; l'exploitant, lui, verrait des pages qui ne tournent
3399
+ // plus chez tout un bâtiment. Une fois par heure suffit à nommer la cause sans inonder.
3400
+ if (await PLAYER.limits.allow("pread:quota-avert", 1, 3600)) {
3401
+ try {
3402
+ console.warn(`[player] relectures de présentation refusées : quota horaire par adresse atteint (${PRESENT_QUOTA_PER_HOUR}/h). Une audience nombreuse derrière une sortie unique verra ses pages cesser de tourner.`);
3403
+ } catch { /* sans console */ }
3404
+ }
3405
+ res.statusCode = 429; res.setHeader("Content-Type", "application/json"); res.setHeader("Cache-Control", "no-store");
3406
+ res.end(JSON.stringify({ ok: false, error: "rate" }));
3407
+ return;
3408
+ }
3409
+ }
3284
3410
  const pres = await getPresentation(String(q.present));
3285
3411
  if (!pres) return sendRefusal(res, "ended", embed);
3286
3412
  // Historique du chat (chargé au join par présentateur ET audience).
@@ -364,11 +364,32 @@ async function switchPresentationDoc(slug, email, isAdmin, { fileUrl, fileName,
364
364
  // finissaient par diverger, et une audience qui ne voit pas la bonne carte n'émet aucune erreur.
365
365
  const { sanitizeContent } = require("./shared.generated.js");
366
366
 
367
- async function setPresentationContent(slug, email, isAdmin, content) {
367
+ /**
368
+ * Ce que la présentation AFFICHE : carte, Street View, ou rien.
369
+ *
370
+ * ⚠️ DEUX AUTORITÉS, ET C'EST LE CORRECTIF. Cette fonction n'acceptait que le PROPRIÉTAIRE, alors
371
+ * que `setPage` — qui pilote exactement de la même façon — accepte le `control_token`. Conséquence :
372
+ * une présentation démarrée SANS session (c'est permis, `present-start` ne l'exige pas) pouvait
373
+ * tourner les pages mais pas déplacer sa carte. L'appel repartait en 401, avalé côté navigateur, et
374
+ * le présentateur voyait sa carte ne pas suivre sans qu'aucun message ne le dise.
375
+ *
376
+ * Piloter ce qui s'affiche et tourner une page sont le même acte : le `control_token` suffit aux
377
+ * deux. Le propriétaire garde son chemin — il pilote sa présentation depuis l'application sans
378
+ * détenir le jeton de contrôle du navigateur qui la présente.
379
+ *
380
+ * ⚠️ Ce qui reste réservé au propriétaire est `switchPresentationDoc` : changer le DOCUMENT n'est
381
+ * pas piloter l'affichage, c'est décider ce qui est montré. Les deux étaient groupés par voisinage
382
+ * dans la route, pas par autorité.
383
+ *
384
+ * Signalé par un audit externe : « le comportement actuel est ambigu ».
385
+ */
386
+ async function setPresentationContent(slug, email, isAdmin, content, control) {
368
387
  if (!slug) return { ok: false, status: 400 };
369
388
  const row = await getPresentation(slug);
370
389
  if (!row) return { ok: false, status: 404 };
371
- if (!isAdmin && (!row.owner_email || row.owner_email !== lc(email))) return { ok: false, status: 403 };
390
+ const pilote = control && tokenMatches(control, row.control_hash);
391
+ const proprietaire = isAdmin || (row.owner_email && row.owner_email === lc(email));
392
+ if (!pilote && !proprietaire) return { ok: false, status: 403 };
372
393
  await PLAYER.db.request(`doc_presentations?slug=eq.${enc(slug)}`, { method: "PATCH", headers: { Prefer: "return=minimal" }, body: { content: sanitizeContent(content), active: true, last_seen: new Date().toISOString(), updated_at: new Date().toISOString() } });
373
394
  return { ok: true };
374
395
  }
@@ -28,7 +28,12 @@ __export(shared_exports, {
28
28
  DEFAULT_CENTER: () => DEFAULT_CENTER,
29
29
  DEFAULT_ZOOM: () => DEFAULT_ZOOM,
30
30
  MAP_TYPES: () => MAP_TYPES,
31
+ PRESENTER_ACTIONS_PER_HOUR: () => PRESENTER_ACTIONS_PER_HOUR,
32
+ PRESENT_QUOTA_PER_HOUR: () => PRESENT_QUOTA_PER_HOUR,
33
+ PRESENT_READS_PER_HOUR: () => PRESENT_READS_PER_HOUR,
34
+ PRESENT_RESYNC_MS: () => PRESENT_RESYNC_MS,
31
35
  READERS_PER_EGRESS: () => READERS_PER_EGRESS,
36
+ RESYNC_READS_PER_HOUR: () => RESYNC_READS_PER_HOUR,
32
37
  SESSION_IDLE_MS: () => SESSION_IDLE_MS,
33
38
  SESSION_INTERVAL_MS: () => SESSION_INTERVAL_MS,
34
39
  SESSION_QUOTA_PER_HOUR: () => SESSION_QUOTA_PER_HOUR,
@@ -95,12 +100,22 @@ var SESSION_WRITES_PER_HOUR = Math.ceil(36e5 / SESSION_INTERVAL_MS);
95
100
  var READERS_PER_EGRESS = 25;
96
101
  var SESSION_QUOTA_PER_HOUR = SESSION_WRITES_PER_HOUR * READERS_PER_EGRESS;
97
102
  var SESSION_IDLE_MS = 18e4;
103
+ var PRESENT_RESYNC_MS = 25e3;
104
+ var RESYNC_READS_PER_HOUR = Math.ceil(36e5 / PRESENT_RESYNC_MS);
105
+ var PRESENTER_ACTIONS_PER_HOUR = 720;
106
+ var PRESENT_READS_PER_HOUR = RESYNC_READS_PER_HOUR + PRESENTER_ACTIONS_PER_HOUR;
107
+ var PRESENT_QUOTA_PER_HOUR = PRESENT_READS_PER_HOUR * READERS_PER_EGRESS;
98
108
  // Annotate the CommonJS export names for ESM import in node:
99
109
  0 && (module.exports = {
100
110
  DEFAULT_CENTER,
101
111
  DEFAULT_ZOOM,
102
112
  MAP_TYPES,
113
+ PRESENTER_ACTIONS_PER_HOUR,
114
+ PRESENT_QUOTA_PER_HOUR,
115
+ PRESENT_READS_PER_HOUR,
116
+ PRESENT_RESYNC_MS,
103
117
  READERS_PER_EGRESS,
118
+ RESYNC_READS_PER_HOUR,
104
119
  SESSION_IDLE_MS,
105
120
  SESSION_INTERVAL_MS,
106
121
  SESSION_QUOTA_PER_HOUR,